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"; "use client";
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback, useDeferredValue, useRef } from "react";
import { import {
Loader2, Loader2,
RefreshCw, RefreshCw,
@@ -12,6 +12,7 @@ import {
XCircle, XCircle,
Clock, Clock,
MapPin, MapPin,
Search,
} from "lucide-react"; } from "lucide-react";
import { Button } from "@/shared/components/ui/button"; import { Button } from "@/shared/components/ui/button";
import { Input } from "@/shared/components/ui/input"; import { Input } from "@/shared/components/ui/input";
@@ -72,10 +73,13 @@ export default function WeekendDeepSyncPage() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [actionLoading, setActionLoading] = useState(false); const [actionLoading, setActionLoading] = useState(false);
// Add city form // UAT autocomplete for adding cities
const [newSiruta, setNewSiruta] = useState(""); type UatEntry = { siruta: string; name: string; county?: string };
const [newName, setNewName] = useState(""); const [uatData, setUatData] = useState<UatEntry[]>([]);
const [newCounty, setNewCounty] = useState(""); const [uatQuery, setUatQuery] = useState("");
const [uatResults, setUatResults] = useState<UatEntry[]>([]);
const [showUatResults, setShowUatResults] = useState(false);
const uatRef = useRef<HTMLDivElement>(null);
const fetchState = useCallback(async () => { const fetchState = useCallback(async () => {
try { try {
@@ -90,8 +94,46 @@ export default function WeekendDeepSyncPage() {
useEffect(() => { useEffect(() => {
void fetchState(); 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]); }, [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>) => { const doAction = async (body: Record<string, unknown>) => {
setActionLoading(true); setActionLoading(true);
try { try {
@@ -107,18 +149,17 @@ export default function WeekendDeepSyncPage() {
setActionLoading(false); setActionLoading(false);
}; };
const handleAdd = async () => { const handleAddUat = async (uat: UatEntry) => {
if (!newSiruta.trim() || !newName.trim()) return;
await doAction({ await doAction({
action: "add", action: "add",
siruta: newSiruta.trim(), siruta: uat.siruta,
name: newName.trim(), name: uat.name,
county: newCounty.trim(), county: uat.county ?? "",
priority: 3, priority: 3,
}); });
setNewSiruta(""); setUatQuery("");
setNewName(""); setUatResults([]);
setNewCounty(""); setShowUatResults(false);
}; };
if (loading) { if (loading) {
@@ -353,53 +394,58 @@ export default function WeekendDeepSyncPage() {
})} })}
</div> </div>
{/* Add city form */} {/* Add city — UAT autocomplete */}
<Card> <Card>
<CardContent className="py-4"> <CardContent className="py-4">
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2"> <h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
Adauga oras in coada Adauga oras in coada
</h3> </h3>
<div className="flex gap-2 items-end flex-wrap"> <div className="relative" ref={uatRef}>
<div className="space-y-1"> <div className="relative">
<label className="text-xs text-muted-foreground">SIRUTA</label> <Search className="absolute left-2.5 top-2 h-4 w-4 text-muted-foreground pointer-events-none" />
<Input <Input
placeholder="ex: 54975" placeholder="Cauta UAT — scrie nume sau cod SIRUTA..."
value={newSiruta} value={uatQuery}
onChange={(e) => setNewSiruta(e.target.value)} onChange={(e) => {
className="w-28 h-8 text-sm" setUatQuery(e.target.value);
setShowUatResults(true);
}}
onFocus={() => setShowUatResults(true)}
onBlur={() => setTimeout(() => setShowUatResults(false), 150)}
className="pl-9 h-9"
autoComplete="off"
/> />
</div> </div>
<div className="space-y-1 flex-1 min-w-[150px]"> {showUatResults && uatResults.length > 0 && (
<label className="text-xs text-muted-foreground"> <div className="absolute z-50 mt-1 w-full rounded-md border bg-popover shadow-lg max-h-60 overflow-auto">
Nume oras {uatResults.map((item) => (
</label> <button
<Input key={item.siruta}
placeholder="ex: Cluj-Napoca" type="button"
value={newName} className="flex w-full items-center justify-between px-3 py-2 text-sm hover:bg-accent transition-colors text-left"
onChange={(e) => setNewName(e.target.value)} onMouseDown={(e) => {
className="h-8 text-sm" e.preventDefault();
/> void handleAddUat(item);
</div> }}
<div className="space-y-1"> >
<label className="text-xs text-muted-foreground">Judet</label> <span className="flex items-center gap-1.5">
<Input <MapPin className="h-3.5 w-3.5 text-muted-foreground" />
placeholder="ex: Cluj" <span className="font-medium">{item.name}</span>
value={newCounty} <span className="text-muted-foreground">
onChange={(e) => setNewCounty(e.target.value)} ({item.siruta})
className="w-32 h-8 text-sm" </span>
/> {item.county && (
</div> <span className="text-muted-foreground">
<Button jud. {item.county}
size="sm" </span>
disabled={ )}
actionLoading || !newSiruta.trim() || !newName.trim() </span>
} <Plus className="h-3.5 w-3.5 text-muted-foreground" />
onClick={() => void handleAdd()} </button>
> ))}
<Plus className="h-3.5 w-3.5 mr-1" /> </div>
Adauga )}
</Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>