feat(parcel-sync): live eTerra search by cadastral number

- Add /api/eterra/search  queries eTerra ArcGIS REST API directly
  by NATIONAL_CADASTRAL_REFERENCE, NATIONAL_CADNR, or INSPIRE_ID
  across TERENURI_ACTIVE + CLADIRI_ACTIVE layers
- Search tab now queries eTerra live (not local DB) with 600ms debounce
- Requires session connected + UAT selected to search
- Updated placeholder and empty state messages in Romanian
This commit is contained in:
AI Assistant
2026-03-06 19:18:18 +02:00
parent bd90c4e30f
commit c98ce81cb7
2 changed files with 249 additions and 15 deletions
@@ -595,38 +595,66 @@ export function ParcelSyncModule() {
/* ════════════════════════════════════════════════════════════ */
/* Load features (parcel search tab) */
/* - When search term is present → live query eTerra */
/* - When search is empty → show nothing (prompt user) */
/* ════════════════════════════════════════════════════════════ */
const searchDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const loadFeatures = useCallback(async () => {
if (!siruta || !/^\d+$/.test(siruta)) return;
if (!featuresSearch.trim()) {
// No search term → clear results
setFeatures([]);
setFeaturesTotal(0);
return;
}
setLoadingFeatures(true);
try {
const res = await fetch("/api/eterra/features", {
// Live search against eTerra
const res = await fetch("/api/eterra/search", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
siruta,
search: featuresSearch.trim(),
layerId: featuresLayerFilter || undefined,
search: featuresSearch || undefined,
page: featuresPage,
pageSize: PAGE_SIZE,
}),
});
const data = (await res.json()) as {
features?: ParcelFeature[];
total?: number;
error?: string;
};
if (data.features) setFeatures(data.features);
if (data.total != null) setFeaturesTotal(data.total);
if (data.error) {
setFeatures([]);
setFeaturesTotal(0);
} else {
if (data.features) setFeatures(data.features);
if (data.total != null) setFeaturesTotal(data.total);
}
} catch {
/* ignore */
}
setLoadingFeatures(false);
}, [siruta, featuresLayerFilter, featuresSearch, featuresPage]);
}, [siruta, featuresLayerFilter, featuresSearch]);
// Debounced search — waits 600ms after user stops typing
useEffect(() => {
if (siruta && /^\d+$/.test(siruta)) void loadFeatures();
}, [siruta, featuresLayerFilter, featuresSearch, featuresPage, loadFeatures]);
if (!siruta || !/^\d+$/.test(siruta)) return;
if (!featuresSearch.trim()) {
setFeatures([]);
setFeaturesTotal(0);
return;
}
if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current);
searchDebounceRef.current = setTimeout(() => {
void loadFeatures();
}, 600);
return () => {
if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current);
};
}, [siruta, featuresLayerFilter, featuresSearch, loadFeatures]);
/* ════════════════════════════════════════════════════════════ */
/* Derived data */
@@ -754,11 +782,15 @@ export function ParcelSyncModule() {
{/* Tab 1: Parcel search */}
{/* ═══════════════════════════════════════════════════════ */}
<TabsContent value="search" className="space-y-4">
{!sirutaValid ? (
{!sirutaValid || !session.connected ? (
<Card>
<CardContent className="py-12 text-center text-muted-foreground">
<Search className="h-10 w-10 mx-auto mb-3 opacity-30" />
<p>Selectează un UAT mai sus pentru a căuta parcele.</p>
<p>
{!session.connected
? "Conectează-te la eTerra și selectează un UAT."
: "Selectează un UAT mai sus pentru a căuta parcele."}
</p>
</CardContent>
</Card>
) : (
@@ -772,7 +804,7 @@ export function ParcelSyncModule() {
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Ref. cadastrală sau INSPIRE ID..."
placeholder="Număr cadastral (ex: 62580)..."
className="pl-9"
value={featuresSearch}
onChange={(e) => {
@@ -850,9 +882,9 @@ export function ParcelSyncModule() {
colSpan={6}
className="px-4 py-8 text-center text-muted-foreground"
>
{featuresSearch
? "Nicio parcelă găsită pentru căutarea curentă."
: "Nicio parcelă sincronizată. Exportează date din tabul Export."}
{featuresSearch.trim()
? "Nicio parcelă găsită în eTerra pentru căutarea curentă."
: "Introdu un număr cadastral sau INSPIRE ID pentru a căuta în eTerra."}
</td>
</tr>
) : (