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";
|
"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>
|
||||||
|
|||||||
Reference in New Issue
Block a user