feat(parcel-sync): county-aware UAT autocomplete with workspace resolution
- New /api/eterra/uats endpoint fetches all counties + UATs from eTerra, caches server-side for 1 hour, returns enriched data with county name and workspacePk for each UAT - When eTerra is connected, auto-fetches enriched UAT list (replaces static uat.json fallback) shows 'FELEACU (57582), CLUJ' format - UAT autocomplete now searches both UAT name and county name - Selected UAT stores workspacePk in state, passes it directly to /api/eterra/search eliminates slow per-search county resolution - Search route accepts optional workspacePk, falls back to resolveWorkspace() - Dropdown shows UAT name, SIRUTA code, and county prominently - Increased autocomplete results from 8 to 12 items
This commit is contained in:
@@ -51,7 +51,12 @@ import type { ParcelDetail } from "@/app/api/eterra/search/route";
|
||||
/* Types */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
type UatEntry = { siruta: string; name: string; county?: string };
|
||||
type UatEntry = {
|
||||
siruta: string;
|
||||
name: string;
|
||||
county?: string;
|
||||
workspacePk?: number;
|
||||
};
|
||||
|
||||
type SessionStatus = {
|
||||
connected: boolean;
|
||||
@@ -277,7 +282,9 @@ export function ParcelSyncModule() {
|
||||
const [uatResults, setUatResults] = useState<UatEntry[]>([]);
|
||||
const [showUatResults, setShowUatResults] = useState(false);
|
||||
const [siruta, setSiruta] = useState("");
|
||||
const [workspacePk, setWorkspacePk] = useState<number | null>(null);
|
||||
const uatRef = useRef<HTMLDivElement>(null);
|
||||
const enrichedUatsFetched = useRef(false);
|
||||
|
||||
/* ── Export state ────────────────────────────────────────────── */
|
||||
const [exportJobId, setExportJobId] = useState<string | null>(null);
|
||||
@@ -318,6 +325,7 @@ export function ParcelSyncModule() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Load static UAT data as fallback
|
||||
fetch("/uat.json")
|
||||
.then((res) => res.json())
|
||||
.then((data: UatEntry[]) => setUatData(data))
|
||||
@@ -333,6 +341,36 @@ export function ParcelSyncModule() {
|
||||
};
|
||||
}, [fetchSession]);
|
||||
|
||||
/* ════════════════════════════════════════════════════════════ */
|
||||
/* Fetch enriched UAT list (with county + workspace) when */
|
||||
/* connected to eTerra. Falls back to static uat.json. */
|
||||
/* ════════════════════════════════════════════════════════════ */
|
||||
|
||||
useEffect(() => {
|
||||
if (!session.connected || enrichedUatsFetched.current) return;
|
||||
enrichedUatsFetched.current = true;
|
||||
|
||||
fetch("/api/eterra/uats")
|
||||
.then((res) => res.json())
|
||||
.then(
|
||||
(data: {
|
||||
uats?: {
|
||||
siruta: string;
|
||||
name: string;
|
||||
county: string;
|
||||
workspacePk: number;
|
||||
}[];
|
||||
}) => {
|
||||
if (data.uats && data.uats.length > 0) {
|
||||
setUatData(data.uats);
|
||||
}
|
||||
},
|
||||
)
|
||||
.catch(() => {
|
||||
// Keep static uat.json data
|
||||
});
|
||||
}, [session.connected]);
|
||||
|
||||
/* ════════════════════════════════════════════════════════════ */
|
||||
/* UAT autocomplete filter */
|
||||
/* ════════════════════════════════════════════════════════════ */
|
||||
@@ -346,12 +384,15 @@ export function ParcelSyncModule() {
|
||||
const isDigit = /^\d+$/.test(raw);
|
||||
const query = normalizeText(raw);
|
||||
const results = uatData
|
||||
.filter((item) =>
|
||||
isDigit
|
||||
? item.siruta.startsWith(raw)
|
||||
: normalizeText(item.name).includes(query),
|
||||
)
|
||||
.slice(0, 8);
|
||||
.filter((item) => {
|
||||
if (isDigit) return item.siruta.startsWith(raw);
|
||||
// Match UAT name or county name
|
||||
if (normalizeText(item.name).includes(query)) return true;
|
||||
if (item.county && normalizeText(item.county).includes(query))
|
||||
return true;
|
||||
return false;
|
||||
})
|
||||
.slice(0, 12);
|
||||
setUatResults(results);
|
||||
}, [uatQuery, uatData]);
|
||||
|
||||
@@ -611,6 +652,7 @@ export function ParcelSyncModule() {
|
||||
body: JSON.stringify({
|
||||
siruta,
|
||||
search: featuresSearch.trim(),
|
||||
...(workspacePk ? { workspacePk } : {}),
|
||||
}),
|
||||
});
|
||||
const data = (await res.json()) as {
|
||||
@@ -629,7 +671,7 @@ export function ParcelSyncModule() {
|
||||
setSearchError("Eroare de rețea.");
|
||||
}
|
||||
setLoadingFeatures(false);
|
||||
}, [siruta, featuresSearch]);
|
||||
}, [siruta, featuresSearch, workspacePk]);
|
||||
|
||||
// No auto-search — user clicks button or presses Enter
|
||||
const handleSearchKeyDown = useCallback(
|
||||
@@ -643,16 +685,17 @@ export function ParcelSyncModule() {
|
||||
);
|
||||
|
||||
// Add result(s) to list for CSV export
|
||||
const addToList = useCallback(
|
||||
(item: ParcelDetail) => {
|
||||
setSearchList((prev) => {
|
||||
if (prev.some((p) => p.nrCad === item.nrCad && p.immovablePk === item.immovablePk))
|
||||
return prev;
|
||||
return [...prev, item];
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
const addToList = useCallback((item: ParcelDetail) => {
|
||||
setSearchList((prev) => {
|
||||
if (
|
||||
prev.some(
|
||||
(p) => p.nrCad === item.nrCad && p.immovablePk === item.immovablePk,
|
||||
)
|
||||
)
|
||||
return prev;
|
||||
return [...prev, item];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const removeFromList = useCallback((nrCad: string) => {
|
||||
setSearchList((prev) => prev.filter((p) => p.nrCad !== nrCad));
|
||||
@@ -772,18 +815,31 @@ export function ParcelSyncModule() {
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
const label = item.county
|
||||
? `${item.name} (${item.siruta}) — ${item.county}`
|
||||
? `${item.name} (${item.siruta}), ${item.county}`
|
||||
: `${item.name} (${item.siruta})`;
|
||||
setUatQuery(label);
|
||||
setSiruta(item.siruta);
|
||||
setWorkspacePk(item.workspacePk ?? null);
|
||||
setShowUatResults(false);
|
||||
setSearchResults([]);
|
||||
}}
|
||||
>
|
||||
<span className="font-medium">{item.name}</span>
|
||||
<span className="text-xs text-muted-foreground font-mono ml-2">
|
||||
<span>
|
||||
<span className="font-medium">{item.name}</span>
|
||||
<span className="text-muted-foreground ml-1.5">
|
||||
({item.siruta})
|
||||
</span>
|
||||
{item.county && (
|
||||
<span className="text-muted-foreground">
|
||||
,{" "}
|
||||
<span className="font-medium text-foreground/70">
|
||||
{item.county}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground font-mono ml-2 shrink-0">
|
||||
{item.siruta}
|
||||
{item.county ? ` · ${item.county}` : ""}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
@@ -878,7 +934,8 @@ export function ParcelSyncModule() {
|
||||
<Loader2 className="h-10 w-10 mx-auto mb-3 animate-spin opacity-50" />
|
||||
<p>Se caută în eTerra...</p>
|
||||
<p className="text-xs mt-1 opacity-60">
|
||||
Prima căutare pe un UAT nou poate dura ~10-30s (se încarcă lista de județe).
|
||||
Prima căutare pe un UAT nou poate dura ~10-30s (se încarcă
|
||||
lista de județe).
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -889,7 +946,8 @@ export function ParcelSyncModule() {
|
||||
{/* Action bar */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{searchResults.length} rezultat{searchResults.length > 1 ? "e" : ""}
|
||||
{searchResults.length} rezultat
|
||||
{searchResults.length > 1 ? "e" : ""}
|
||||
{searchList.length > 0 && (
|
||||
<span className="ml-2">
|
||||
· <strong>{searchList.length}</strong> în listă
|
||||
@@ -913,7 +971,9 @@ export function ParcelSyncModule() {
|
||||
size="sm"
|
||||
variant="default"
|
||||
onClick={downloadCSV}
|
||||
disabled={searchResults.length === 0 && searchList.length === 0}
|
||||
disabled={
|
||||
searchResults.length === 0 && searchList.length === 0
|
||||
}
|
||||
>
|
||||
<FileDown className="mr-1 h-3.5 w-3.5" />
|
||||
Descarcă CSV
|
||||
@@ -963,7 +1023,9 @@ export function ParcelSyncModule() {
|
||||
const text = [
|
||||
`Nr. Cad: ${p.nrCad}`,
|
||||
`Nr. CF: ${p.nrCF || "—"}`,
|
||||
p.nrCFVechi ? `CF vechi: ${p.nrCFVechi}` : null,
|
||||
p.nrCFVechi
|
||||
? `CF vechi: ${p.nrCFVechi}`
|
||||
: null,
|
||||
p.nrTopo ? `Nr. Topo: ${p.nrTopo}` : null,
|
||||
p.suprafata != null
|
||||
? `Suprafață: ${p.suprafata.toLocaleString("ro-RO")} mp`
|
||||
@@ -973,8 +1035,12 @@ export function ParcelSyncModule() {
|
||||
? `Categorie: ${p.categorieFolosinta}`
|
||||
: null,
|
||||
p.adresa ? `Adresă: ${p.adresa}` : null,
|
||||
p.proprietari ? `Proprietari: ${p.proprietari}` : null,
|
||||
p.solicitant ? `Solicitant: ${p.solicitant}` : null,
|
||||
p.proprietari
|
||||
? `Proprietari: ${p.proprietari}`
|
||||
: null,
|
||||
p.solicitant
|
||||
? `Solicitant: ${p.solicitant}`
|
||||
: null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
@@ -1087,7 +1153,8 @@ export function ParcelSyncModule() {
|
||||
<Search className="h-10 w-10 mx-auto mb-3 opacity-30" />
|
||||
<p>Introdu un număr cadastral și apasă Caută.</p>
|
||||
<p className="text-xs mt-1 opacity-60">
|
||||
Poți căuta mai multe parcele simultan, separate prin virgulă.
|
||||
Poți căuta mai multe parcele simultan, separate prin
|
||||
virgulă.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -1120,10 +1187,18 @@ export function ParcelSyncModule() {
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/40">
|
||||
<th className="px-3 py-2 text-left font-medium">Nr. Cad</th>
|
||||
<th className="px-3 py-2 text-left font-medium">Nr. CF</th>
|
||||
<th className="px-3 py-2 text-right font-medium hidden sm:table-cell">Suprafață</th>
|
||||
<th className="px-3 py-2 text-left font-medium hidden md:table-cell">Proprietari</th>
|
||||
<th className="px-3 py-2 text-left font-medium">
|
||||
Nr. Cad
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-medium">
|
||||
Nr. CF
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right font-medium hidden sm:table-cell">
|
||||
Suprafață
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-medium hidden md:table-cell">
|
||||
Proprietari
|
||||
</th>
|
||||
<th className="px-3 py-2 w-8"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -1140,7 +1215,9 @@ export function ParcelSyncModule() {
|
||||
{p.nrCF || "—"}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right hidden sm:table-cell tabular-nums text-xs">
|
||||
{p.suprafata != null ? formatArea(p.suprafata) : "—"}
|
||||
{p.suprafata != null
|
||||
? formatArea(p.suprafata)
|
||||
: "—"}
|
||||
</td>
|
||||
<td className="px-3 py-2 hidden md:table-cell text-xs truncate max-w-[300px]">
|
||||
{p.proprietari || "—"}
|
||||
|
||||
Reference in New Issue
Block a user