feat(wds): replace 3-field form with UAT autocomplete search

Same search pattern as parcel-sync module: type name or SIRUTA code,
pick from dropdown, city is added instantly. Already-queued cities
are filtered out from results.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-26 22:57:07 +02:00
parent a6d7e1d87f
commit 798b3e4f6b
+97 -51
View File
@@ -1,6 +1,6 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { useState, useEffect, useCallback, useDeferredValue, useRef } from "react";
import {
Loader2,
RefreshCw,
@@ -12,6 +12,7 @@ import {
XCircle,
Clock,
MapPin,
Search,
} from "lucide-react";
import { Button } from "@/shared/components/ui/button";
import { Input } from "@/shared/components/ui/input";
@@ -72,10 +73,13 @@ export default function WeekendDeepSyncPage() {
const [loading, setLoading] = useState(true);
const [actionLoading, setActionLoading] = useState(false);
// Add city form
const [newSiruta, setNewSiruta] = useState("");
const [newName, setNewName] = useState("");
const [newCounty, setNewCounty] = useState("");
// UAT autocomplete for adding cities
type UatEntry = { siruta: string; name: string; county?: string };
const [uatData, setUatData] = useState<UatEntry[]>([]);
const [uatQuery, setUatQuery] = useState("");
const [uatResults, setUatResults] = useState<UatEntry[]>([]);
const [showUatResults, setShowUatResults] = useState(false);
const uatRef = useRef<HTMLDivElement>(null);
const fetchState = useCallback(async () => {
try {
@@ -90,8 +94,46 @@ export default function WeekendDeepSyncPage() {
useEffect(() => {
void fetchState();
// Load UAT list for autocomplete
fetch("/api/eterra/uats")
.then((r) => r.json())
.then((data: { uats?: UatEntry[] }) => {
if (data.uats) setUatData(data.uats);
})
.catch(() => {
fetch("/uat.json")
.then((r) => r.json())
.then((fallback: UatEntry[]) => setUatData(fallback))
.catch(() => {});
});
}, [fetchState]);
// UAT autocomplete filter
const normalizeText = (text: string) =>
text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim();
const deferredUatQuery = useDeferredValue(uatQuery);
useEffect(() => {
const raw = deferredUatQuery.trim();
if (raw.length < 2) { setUatResults([]); return; }
const isDigit = /^\d+$/.test(raw);
const query = normalizeText(raw);
const nameMatches: UatEntry[] = [];
const countyOnly: UatEntry[] = [];
for (const item of uatData) {
// Skip cities already in queue
if (state?.cities.some((c) => c.siruta === item.siruta)) continue;
if (isDigit) {
if (item.siruta.startsWith(raw)) nameMatches.push(item);
} else {
if (normalizeText(item.name).includes(query)) nameMatches.push(item);
else if (item.county && normalizeText(item.county).includes(query))
countyOnly.push(item);
}
}
setUatResults([...nameMatches, ...countyOnly].slice(0, 10));
}, [deferredUatQuery, uatData, state?.cities]);
const doAction = async (body: Record<string, unknown>) => {
setActionLoading(true);
try {
@@ -107,18 +149,17 @@ export default function WeekendDeepSyncPage() {
setActionLoading(false);
};
const handleAdd = async () => {
if (!newSiruta.trim() || !newName.trim()) return;
const handleAddUat = async (uat: UatEntry) => {
await doAction({
action: "add",
siruta: newSiruta.trim(),
name: newName.trim(),
county: newCounty.trim(),
siruta: uat.siruta,
name: uat.name,
county: uat.county ?? "",
priority: 3,
});
setNewSiruta("");
setNewName("");
setNewCounty("");
setUatQuery("");
setUatResults([]);
setShowUatResults(false);
};
if (loading) {
@@ -353,53 +394,58 @@ export default function WeekendDeepSyncPage() {
})}
</div>
{/* Add city form */}
{/* Add city — UAT autocomplete */}
<Card>
<CardContent className="py-4">
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
<Plus className="h-4 w-4" />
Adauga oras in coada
</h3>
<div className="flex gap-2 items-end flex-wrap">
<div className="space-y-1">
<label className="text-xs text-muted-foreground">SIRUTA</label>
<div className="relative" ref={uatRef}>
<div className="relative">
<Search className="absolute left-2.5 top-2 h-4 w-4 text-muted-foreground pointer-events-none" />
<Input
placeholder="ex: 54975"
value={newSiruta}
onChange={(e) => setNewSiruta(e.target.value)}
className="w-28 h-8 text-sm"
placeholder="Cauta UAT — scrie nume sau cod SIRUTA..."
value={uatQuery}
onChange={(e) => {
setUatQuery(e.target.value);
setShowUatResults(true);
}}
onFocus={() => setShowUatResults(true)}
onBlur={() => setTimeout(() => setShowUatResults(false), 150)}
className="pl-9 h-9"
autoComplete="off"
/>
</div>
<div className="space-y-1 flex-1 min-w-[150px]">
<label className="text-xs text-muted-foreground">
Nume oras
</label>
<Input
placeholder="ex: Cluj-Napoca"
value={newName}
onChange={(e) => setNewName(e.target.value)}
className="h-8 text-sm"
/>
</div>
<div className="space-y-1">
<label className="text-xs text-muted-foreground">Judet</label>
<Input
placeholder="ex: Cluj"
value={newCounty}
onChange={(e) => setNewCounty(e.target.value)}
className="w-32 h-8 text-sm"
/>
</div>
<Button
size="sm"
disabled={
actionLoading || !newSiruta.trim() || !newName.trim()
}
onClick={() => void handleAdd()}
>
<Plus className="h-3.5 w-3.5 mr-1" />
Adauga
</Button>
{showUatResults && uatResults.length > 0 && (
<div className="absolute z-50 mt-1 w-full rounded-md border bg-popover shadow-lg max-h-60 overflow-auto">
{uatResults.map((item) => (
<button
key={item.siruta}
type="button"
className="flex w-full items-center justify-between px-3 py-2 text-sm hover:bg-accent transition-colors text-left"
onMouseDown={(e) => {
e.preventDefault();
void handleAddUat(item);
}}
>
<span className="flex items-center gap-1.5">
<MapPin className="h-3.5 w-3.5 text-muted-foreground" />
<span className="font-medium">{item.name}</span>
<span className="text-muted-foreground">
({item.siruta})
</span>
{item.county && (
<span className="text-muted-foreground">
jud. {item.county}
</span>
)}
</span>
<Plus className="h-3.5 w-3.5 text-muted-foreground" />
</button>
))}
</div>
)}
</div>
</CardContent>
</Card>