"use client"; import { useState, useEffect, useCallback, useRef, useDeferredValue } from "react"; import { Search, Download, Layers, MapPin, Database, FileText, Map as MapIcon, } from "lucide-react"; import { Input } from "@/shared/components/ui/input"; import { Badge } from "@/shared/components/ui/badge"; import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@/shared/components/ui/tabs"; import type { SessionStatus, UatEntry, SyncRunInfo, DbSummary, } from "./parcel-sync-types"; import { normalizeText } from "./parcel-sync-types"; import { ConnectionPill } from "./connection-pill"; import { EpayConnect, type EpaySessionStatus } from "./epay-connect"; import { SearchTab } from "./tabs/search-tab"; import { LayersTab } from "./tabs/layers-tab"; import { ExportTab } from "./tabs/export-tab"; import { DatabaseTab } from "./tabs/database-tab"; import { CfTab } from "./tabs/cf-tab"; import { MapTab } from "./tabs/map-tab"; /* ------------------------------------------------------------------ */ /* Main Component */ /* ------------------------------------------------------------------ */ export function ParcelSyncModule() { /* ── Server session ─────────────────────────────────────────── */ const [session, setSession] = useState({ connected: false, activeJobCount: 0, }); const [connecting, setConnecting] = useState(false); const [connectionError, setConnectionError] = useState(""); const autoConnectAttempted = useRef(false); const sessionPollRef = useRef | null>(null); /* ── UAT autocomplete ───────────────────────────────────────── */ const [uatData, setUatData] = useState([]); const [uatQuery, setUatQuery] = useState(""); const [uatResults, setUatResults] = useState([]); const [showUatResults, setShowUatResults] = useState(false); const [siruta, setSiruta] = useState(""); const [workspacePk, setWorkspacePk] = useState(null); const uatRef = useRef(null); /* ── Sync status (shared between layers + export tabs) ──────── */ const [syncLocalCounts, setSyncLocalCounts] = useState>({}); const [syncRuns, setSyncRuns] = useState([]); const [syncingSiruta, setSyncingSiruta] = useState(""); /* ── Global DB summary ──────────────────────────────────────── */ const [dbSummary, setDbSummary] = useState(null); const [dbSummaryLoading, setDbSummaryLoading] = useState(false); /* ── Export state (shared flag) ─────────────────────────────── */ const [exporting, setExporting] = useState(false); /* ── ePay status ────────────────────────────────────────────── */ const [epayStatus, setEpayStatus] = useState({ connected: false }); /* ── Derived ────────────────────────────────────────────────── */ const sirutaValid = siruta.length > 0 && /^\d+$/.test(siruta); const selectedUat = uatData.find((u) => u.siruta === siruta); /* ════════════════════════════════════════════════════════════ */ /* Session management */ /* ════════════════════════════════════════════════════════════ */ const fetchSession = useCallback(async () => { try { const res = await fetch("/api/eterra/session"); const data = (await res.json()) as SessionStatus; setSession((prev) => { if (prev.eterraMaintenance && data.eterraAvailable && !data.eterraMaintenance) { autoConnectAttempted.current = false; } return data; }); if (data.connected) setConnectionError(""); return data; } catch { return null; } }, []); useEffect(() => { // Load UATs from local DB fetch("/api/eterra/uats") .then((res) => res.json()) .then((data: { uats?: UatEntry[]; total?: number }) => { if (data.uats && data.uats.length > 0) { setUatData(data.uats); } else { fetch("/api/eterra/uats", { method: "POST" }).catch(() => {}); fetch("/uat.json") .then((res) => res.json()) .then((fallback: UatEntry[]) => setUatData(fallback)) .catch(() => {}); } }) .catch(() => { fetch("/uat.json") .then((res) => res.json()) .then((fallback: UatEntry[]) => setUatData(fallback)) .catch(() => {}); }); void fetchSession(); sessionPollRef.current = setInterval(() => void fetchSession(), 30_000); return () => { if (sessionPollRef.current) clearInterval(sessionPollRef.current); }; }, [fetchSession]); /* ── Reload UATs when session connects ─────────────────────── */ const prevConnected = useRef(false); useEffect(() => { if (session.connected && !prevConnected.current) { const timer = setTimeout(() => { fetch("/api/eterra/uats") .then((res) => res.json()) .then((data: { uats?: UatEntry[] }) => { if (data.uats && data.uats.length > 0) setUatData(data.uats); }) .catch(() => {}); }, 5000); return () => clearTimeout(timer); } prevConnected.current = session.connected; }, [session.connected]); /* ── UAT autocomplete filter ───────────────────────────────── */ 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: typeof uatData = []; const countyOnlyMatches: typeof uatData = []; for (const item of uatData) { if (isDigit) { if (item.siruta.startsWith(raw)) nameMatches.push(item); } else { const nameMatch = normalizeText(item.name).includes(query); const countyMatch = item.county && normalizeText(item.county).includes(query); if (nameMatch) nameMatches.push(item); else if (countyMatch) countyOnlyMatches.push(item); } } setUatResults([...nameMatches, ...countyOnlyMatches].slice(0, 12)); }, [deferredUatQuery, uatData]); /* ── Auto-connect on first UAT keystroke ───────────────────── */ const triggerAutoConnect = useCallback(async () => { if (session.connected || connecting || autoConnectAttempted.current) return; if (session.eterraMaintenance) return; autoConnectAttempted.current = true; setConnecting(true); setConnectionError(""); try { const res = await fetch("/api/eterra/session", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "connect" }), }); const data = (await res.json()) as { success?: boolean; error?: string; maintenance?: boolean; }; if (data.success) { await fetchSession(); } else if (data.maintenance) { setSession((prev) => ({ ...prev, eterraMaintenance: true, eterraAvailable: false, eterraHealthMessage: data.error ?? "eTerra în mentenanță", })); autoConnectAttempted.current = false; } else { setConnectionError(data.error ?? "Eroare conectare"); } } catch { setConnectionError("Eroare rețea"); } setConnecting(false); }, [session.connected, session.eterraMaintenance, connecting, fetchSession]); /* ── Disconnect ────────────────────────────────────────────── */ const handleDisconnect = useCallback(async () => { try { const res = await fetch("/api/eterra/session", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "disconnect" }), }); const data = (await res.json()) as { success?: boolean; error?: string }; if (data.success) { setSession({ connected: false, activeJobCount: 0 }); autoConnectAttempted.current = false; } else { setConnectionError(data.error ?? "Nu se poate deconecta"); } } catch { setConnectionError("Eroare rețea"); } }, []); /* ── Sync status ───────────────────────────────────────────── */ const fetchSyncStatus = useCallback(async () => { if (!siruta) return; try { const res = await fetch(`/api/eterra/sync-status?siruta=${siruta}`); const data = (await res.json()) as { localCounts?: Record; runs?: SyncRunInfo[]; }; if (data.localCounts) setSyncLocalCounts(data.localCounts); if (data.runs) setSyncRuns(data.runs); setSyncingSiruta(siruta); } catch { // silent } }, [siruta]); useEffect(() => { if (siruta && /^\d+$/.test(siruta)) { void fetchSyncStatus(); } }, [siruta, fetchSyncStatus]); /* ── DB summary ────────────────────────────────────────────── */ const fetchDbSummary = useCallback(async () => { setDbSummaryLoading(true); try { const res = await fetch("/api/eterra/db-summary"); const data = (await res.json()) as DbSummary; if (data.uats) setDbSummary(data); } catch { // silent } setDbSummaryLoading(false); }, []); useEffect(() => { void fetchDbSummary(); }, [fetchDbSummary]); /* ════════════════════════════════════════════════════════════ */ /* Render */ /* ════════════════════════════════════════════════════════════ */ return ( {/* ═══════════════════════ Persistent header ═══════════════ */}
{/* UAT + Connection row */}
{/* UAT autocomplete */}
{ setUatQuery(e.target.value); setShowUatResults(true); if (e.target.value.trim().length >= 1) { void triggerAutoConnect(); } }} onFocus={() => setShowUatResults(true)} onBlur={() => setTimeout(() => setShowUatResults(false), 150)} className="pl-9 font-medium" autoComplete="off" />
{sirutaValid && (
SIRUTA {siruta}
)} {showUatResults && uatResults.length > 0 && (
{uatResults.map((item) => ( ))}
)}
{/* Connection pills */}
{/* Tab bar */} Căutare Parcele Catalog Layere Export Baza de Date Extrase CF Harta
{/* ═══════════════════════ Tab content ═════════════════════ */} void fetchSyncStatus()} exporting={exporting} /> void fetchSyncStatus()} onDbRefresh={() => void fetchDbSummary()} exporting={exporting} setExporting={setExporting} /> void fetchDbSummary()} />
); }