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>
This commit is contained in:
+9
-1
@@ -68,6 +68,10 @@ function useAppraisals(query: SearchQuery | null, blockOverride?: string, plotOv
|
||||
plot,
|
||||
decisive_appraiser: appraiser,
|
||||
max_results: 30,
|
||||
// Include same-block-other-plots and nearby-block tiers when we have
|
||||
// a block to anchor on.
|
||||
include_nearby: Boolean(block),
|
||||
nearby_block_radius: 2,
|
||||
}),
|
||||
enabled: Boolean(block || appraiser),
|
||||
});
|
||||
@@ -192,7 +196,9 @@ export default function App() {
|
||||
שמאי מכריע
|
||||
{appraisals.data && (
|
||||
<span className="ms-2 text-xs bg-muted-foreground/15 rounded-full px-2 py-0.5">
|
||||
{appraisals.data.returned}
|
||||
{appraisals.data.returned +
|
||||
(appraisals.data.same_block?.count ?? 0) +
|
||||
(appraisals.data.nearby_blocks?.count ?? 0)}
|
||||
</span>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
@@ -227,6 +233,8 @@ export default function App() {
|
||||
<AppraisalsTable
|
||||
decisions={appraisals.data.decisions}
|
||||
totalInDb={appraisals.data.total_in_db}
|
||||
sameBlockDecisions={appraisals.data.same_block?.decisions}
|
||||
nearbyBlockDecisions={appraisals.data.nearby_blocks?.decisions}
|
||||
/>
|
||||
)}
|
||||
{!appraisals.isFetching && !appraisals.error && !appraisals.data && (
|
||||
|
||||
@@ -72,6 +72,8 @@ export interface AppraisalsSearchRequest {
|
||||
search_text?: string;
|
||||
appraisal_header?: string;
|
||||
max_results?: number;
|
||||
include_nearby?: boolean;
|
||||
nearby_block_radius?: number;
|
||||
}
|
||||
|
||||
export interface AppraisalDocument {
|
||||
@@ -96,9 +98,16 @@ export interface AppraisalDecision {
|
||||
all_documents: AppraisalDocument[];
|
||||
}
|
||||
|
||||
export interface AppraisalsBucket {
|
||||
count: number;
|
||||
decisions: AppraisalDecision[];
|
||||
}
|
||||
|
||||
export interface AppraisalsSearchResponse {
|
||||
total_in_db: number;
|
||||
returned: number;
|
||||
filters: Record<string, unknown>;
|
||||
decisions: AppraisalDecision[];
|
||||
same_block?: AppraisalsBucket;
|
||||
nearby_blocks?: AppraisalsBucket;
|
||||
}
|
||||
|
||||
@@ -7,10 +7,94 @@ import { formatDate } from "../lib/utils";
|
||||
interface Props {
|
||||
decisions: AppraisalDecision[];
|
||||
totalInDb: number;
|
||||
sameBlockDecisions?: AppraisalDecision[];
|
||||
nearbyBlockDecisions?: AppraisalDecision[];
|
||||
}
|
||||
|
||||
export function AppraisalsTable({ decisions, totalInDb }: Props) {
|
||||
if (!decisions.length) {
|
||||
function DecisionsTable({ decisions }: { decisions: AppraisalDecision[] }) {
|
||||
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-start font-medium">סוג שומה</th>
|
||||
<th className="p-3 text-start font-medium">כותרת ההחלטה</th>
|
||||
<th className="p-3 text-start font-medium">PDF</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{decisions.map((d) => (
|
||||
<tr key={d.id} className="border-t hover:bg-accent/40 align-top">
|
||||
<td className="p-3 whitespace-nowrap">{formatDate(d.decision_date)}</td>
|
||||
<td className="p-3 whitespace-nowrap">{d.decisive_appraiser || "—"}</td>
|
||||
<td className="p-3 whitespace-nowrap">{d.committee || "—"}</td>
|
||||
<td className="p-3 whitespace-nowrap tabular-nums text-center">
|
||||
{d.block ?? "—"}
|
||||
{d.plot ? ` / ${d.plot}` : ""}
|
||||
</td>
|
||||
<td className="p-3">{d.appraisal_type || "—"}</td>
|
||||
<td className="p-3 max-w-md">
|
||||
<span className="line-clamp-2">{d.appraisal_header || "—"}</span>
|
||||
</td>
|
||||
<td className="p-3 text-start whitespace-nowrap">
|
||||
{d.pdf_url ? (
|
||||
<div className="flex justify-end gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
asChild
|
||||
title="פתח PDF בלשונית חדשה"
|
||||
>
|
||||
<a
|
||||
href={api.pdfProxyUrl(d.pdf_url)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<FileText />
|
||||
פתח
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
asChild
|
||||
title="הורד PDF"
|
||||
>
|
||||
<a
|
||||
href={api.pdfProxyUrl(d.pdf_url) + "&download=1"}
|
||||
download={`${d.block || "appraisal"}-${d.plot || "0"}-${d.id}.pdf`}
|
||||
>
|
||||
<Download />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppraisalsTable({
|
||||
decisions,
|
||||
totalInDb,
|
||||
sameBlockDecisions,
|
||||
nearbyBlockDecisions,
|
||||
}: Props) {
|
||||
const hasMain = decisions.length > 0;
|
||||
const hasSameBlock = (sameBlockDecisions?.length ?? 0) > 0;
|
||||
const hasNearby = (nearbyBlockDecisions?.length ?? 0) > 0;
|
||||
|
||||
if (!hasMain && !hasSameBlock && !hasNearby) {
|
||||
return (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
לא נמצאו החלטות שמאי מכריע באזור הזה
|
||||
@@ -19,78 +103,54 @@ export function AppraisalsTable({ decisions, totalInDb }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
נמצאו {decisions.length} מתוך {totalInDb.toLocaleString("he-IL")} החלטות במאגר
|
||||
</div>
|
||||
<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-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-start font-medium">PDF</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{decisions.map((d) => (
|
||||
<tr key={d.id} className="border-t hover:bg-accent/40 align-top">
|
||||
<td className="p-3 whitespace-nowrap">{formatDate(d.decision_date)}</td>
|
||||
<td className="p-3 whitespace-nowrap">{d.decisive_appraiser || "—"}</td>
|
||||
<td className="p-3 whitespace-nowrap">{d.committee || "—"}</td>
|
||||
<td className="p-3 whitespace-nowrap tabular-nums">
|
||||
{d.block ?? "—"}
|
||||
{d.plot ? ` / ${d.plot}` : ""}
|
||||
</td>
|
||||
<td className="p-3">{d.appraisal_type || "—"}</td>
|
||||
<td className="p-3 max-w-md">
|
||||
<span className="line-clamp-2">{d.appraisal_header || "—"}</span>
|
||||
</td>
|
||||
<td className="p-3 text-start whitespace-nowrap">
|
||||
{d.pdf_url ? (
|
||||
<div className="flex justify-end gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
asChild
|
||||
title="פתח PDF בלשונית חדשה"
|
||||
>
|
||||
<a
|
||||
href={api.pdfProxyUrl(d.pdf_url)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<FileText />
|
||||
פתח
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
asChild
|
||||
title="הורד PDF"
|
||||
>
|
||||
<a
|
||||
href={api.pdfProxyUrl(d.pdf_url) + "&download=1"}
|
||||
download={`${d.block || "appraisal"}-${d.plot || "0"}-${d.id}.pdf`}
|
||||
>
|
||||
<Download />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<section className="space-y-3">
|
||||
<div className="flex items-baseline justify-between">
|
||||
<h3 className="text-base font-semibold">החלטות תואמות</h3>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{hasMain
|
||||
? `נמצאו ${decisions.length} מתוך ${totalInDb.toLocaleString("he-IL")} החלטות במאגר`
|
||||
: "לא נמצאו החלטות תואמות מדויקות"}
|
||||
</div>
|
||||
</div>
|
||||
{hasMain ? (
|
||||
<DecisionsTable decisions={decisions} />
|
||||
) : (
|
||||
<div className="text-center py-6 text-sm text-muted-foreground border rounded-md">
|
||||
אין החלטות בגוש/חלקה המבוקשים
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{hasSameBlock && (
|
||||
<section className="space-y-3">
|
||||
<div className="flex items-baseline justify-between">
|
||||
<h3 className="text-base font-semibold">
|
||||
החלטות נוספות באותו גוש{" "}
|
||||
<span className="text-muted-foreground font-normal">(חלקות אחרות)</span>
|
||||
</h3>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{sameBlockDecisions!.length} החלטות
|
||||
</div>
|
||||
</div>
|
||||
<DecisionsTable decisions={sameBlockDecisions!} />
|
||||
</section>
|
||||
)}
|
||||
|
||||
{hasNearby && (
|
||||
<section className="space-y-3">
|
||||
<div className="flex items-baseline justify-between">
|
||||
<h3 className="text-base font-semibold">
|
||||
החלטות בגושים סמוכים{" "}
|
||||
<span className="text-muted-foreground font-normal">(±2)</span>
|
||||
</h3>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{nearbyBlockDecisions!.length} החלטות
|
||||
</div>
|
||||
</div>
|
||||
<DecisionsTable decisions={nearbyBlockDecisions!} />
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -87,14 +87,14 @@ export function DealsTable({ deals, resolvedAddress, searchedBlock, searchedPlot
|
||||
<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-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-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-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>
|
||||
@@ -109,18 +109,18 @@ export function DealsTable({ deals, resolvedAddress, searchedBlock, searchedPlot
|
||||
<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-start tabular-nums">{d.rooms ?? "—"}</td>
|
||||
<td className="p-3 text-start tabular-nums">{d.floor ?? "—"}</td>
|
||||
<td className="p-3 text-start tabular-nums">{formatNumber(d.asset_area)}</td>
|
||||
<td className="p-3 text-start tabular-nums">
|
||||
<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-start tabular-nums">
|
||||
<td className="p-3 text-center tabular-nums">
|
||||
{formatCurrencyILS(d.price_per_sqm)}
|
||||
</td>
|
||||
<td className="p-3 text-start tabular-nums">{d.gushNum ?? "—"}</td>
|
||||
<td className="p-3 text-start tabular-nums">{d.parcelNum ?? "—"}</td>
|
||||
<td className="p-3 text-start tabular-nums">{d.subParcelNum ?? "—"}</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
|
||||
|
||||
Reference in New Issue
Block a user