Add: Web UI (React 19 + Vite + shadcn/ui) + FastAPI REST layer
Adds a self-contained web interface that wraps the MCP tools so end users
don't need to issue MCP commands. Single Docker container serves:
/ → React SPA (RTL Hebrew, Heebo font, shadcn/ui components)
/api/* → FastAPI REST endpoints used by the SPA
/api/docs → OpenAPI / Swagger
/mcp → existing FastMCP streamable-http endpoint
/health → liveness probe
REST endpoints (in nadlan_mcp/web_app.py):
GET /api/search/address?q=... — autocomplete + best-effort gush/חלקה
via Govmap PARCEL_ALL layer
POST /api/search/deals — Govmap deals around an address
POST /api/search/appraisals — Decisive-appraiser search (gov.il)
GET /api/appraisals/pdf?url=.. — Stream PDFs through this server,
carrying the upstream x-client-id
header (a normal browser fetch fails)
UI flow:
- Address search → resolves gush/חלקה → shows BOTH deals (Govmap) AND
appraiser decisions (gov.il) for the same parcel side-by-side in tabs.
- Block+plot search → goes straight to appraisals.
- Appraiser-name search → all decisions by that appraiser.
Stack chosen for RTL-first usability and longevity: React 19, Vite 6,
TypeScript, Tailwind, shadcn/ui (Radix primitives), TanStack Query,
Lucide icons. ~295KB gzipped JS, no SSR overhead.
Multi-stage Dockerfile: Node 22 builds web/ in stage 1, Python 3.13
runtime serves the dist + the FastAPI app in stage 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { Deal } from "@/api/types";
|
||||
import { formatCurrencyILS, formatDate, formatNumber } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
deals: Deal[];
|
||||
}
|
||||
|
||||
const SOURCE_LABELS: Record<string, string> = {
|
||||
same_building: "אותו בניין",
|
||||
street: "באותו רחוב",
|
||||
neighborhood: "בשכונה",
|
||||
};
|
||||
|
||||
export function DealsTable({ deals }: Props) {
|
||||
if (!deals.length) {
|
||||
return (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
לא נמצאו עסקאות באזור הזה
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto rounded-md border">
|
||||
<table 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-end font-medium">חדרים</th>
|
||||
<th className="p-3 text-end font-medium">קומה</th>
|
||||
<th className="p-3 text-end font-medium">שטח (מ"ר)</th>
|
||||
<th className="p-3 text-end font-medium">מחיר</th>
|
||||
<th className="p-3 text-end font-medium">מחיר/מ"ר</th>
|
||||
<th className="p-3 text-start font-medium">קרבה</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{deals.map((d) => {
|
||||
const street = (d.streetNameHeb as string) || "";
|
||||
const houseNum = (d.house_num as string | number | undefined) ?? "";
|
||||
const settlement = (d.settlementNameHeb as string) || "";
|
||||
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-end tabular-nums">{d.rooms ?? "—"}</td>
|
||||
<td className="p-3 text-end tabular-nums">{d.floor ?? "—"}</td>
|
||||
<td className="p-3 text-end tabular-nums">{formatNumber(d.asset_area)}</td>
|
||||
<td className="p-3 text-end tabular-nums">
|
||||
{formatCurrencyILS(d.deal_amount)}
|
||||
</td>
|
||||
<td className="p-3 text-end tabular-nums">
|
||||
{formatCurrencyILS(d.price_per_sqm)}
|
||||
</td>
|
||||
<td className="p-3">
|
||||
{d.deal_source ? (
|
||||
<span
|
||||
className={
|
||||
"inline-block rounded-full px-2 py-0.5 text-xs " +
|
||||
(d.deal_source === "same_building"
|
||||
? "bg-emerald-100 text-emerald-800"
|
||||
: d.deal_source === "street"
|
||||
? "bg-blue-100 text-blue-800"
|
||||
: "bg-slate-100 text-slate-700")
|
||||
}
|
||||
>
|
||||
{SOURCE_LABELS[d.deal_source] || d.deal_source}
|
||||
</span>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user