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:
@@ -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()}
|
||||
{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);
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5 mr-1" />
|
||||
Adauga
|
||||
</Button>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user