2b8d144924
The \u0103, \u00ee etc. escape sequences were rendering literally in JSX text nodes instead of displaying ă, î, ț, ș characters. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1450 lines
57 KiB
TypeScript
1450 lines
57 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useCallback, useEffect } from "react";
|
|
import {
|
|
Search,
|
|
Loader2,
|
|
Plus,
|
|
FileDown,
|
|
ClipboardCopy,
|
|
Trash2,
|
|
XCircle,
|
|
Download,
|
|
Archive,
|
|
FileText,
|
|
User,
|
|
} from "lucide-react";
|
|
import { Button } from "@/shared/components/ui/button";
|
|
import { Input } from "@/shared/components/ui/input";
|
|
import { Label } from "@/shared/components/ui/label";
|
|
import { Badge } from "@/shared/components/ui/badge";
|
|
import { Card, CardContent } from "@/shared/components/ui/card";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from "@/shared/components/ui/tooltip";
|
|
import { cn } from "@/shared/lib/utils";
|
|
import { EpayOrderButton } from "../epay-order-button";
|
|
import type { EpaySessionStatus } from "../epay-connect";
|
|
import type {
|
|
SessionStatus,
|
|
UatEntry,
|
|
ParcelDetail,
|
|
OwnerSearchResult,
|
|
} from "../parcel-sync-types";
|
|
import { formatArea, formatShortDate } from "../parcel-sync-types";
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Props */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
type SearchTabProps = {
|
|
siruta: string;
|
|
workspacePk: number | null;
|
|
sirutaValid: boolean;
|
|
session: SessionStatus;
|
|
selectedUat?: UatEntry;
|
|
epayStatus: EpaySessionStatus;
|
|
};
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Component */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
export function SearchTab({
|
|
siruta,
|
|
workspacePk,
|
|
sirutaValid,
|
|
session,
|
|
selectedUat,
|
|
epayStatus,
|
|
}: SearchTabProps) {
|
|
/* ── Parcel search ────────────────────────────────────────── */
|
|
const [searchMode, setSearchMode] = useState<"cadastral" | "owner">(
|
|
"cadastral",
|
|
);
|
|
const [searchResults, setSearchResults] = useState<ParcelDetail[]>([]);
|
|
const [searchList, setSearchList] = useState<ParcelDetail[]>([]);
|
|
const [featuresSearch, setFeaturesSearch] = useState("");
|
|
const [loadingFeatures, setLoadingFeatures] = useState(false);
|
|
const [searchError, setSearchError] = useState("");
|
|
|
|
/* ── Owner search ──────────────────────────────────────────── */
|
|
const [ownerSearch, setOwnerSearch] = useState("");
|
|
const [ownerResults, setOwnerResults] = useState<OwnerSearchResult[]>([]);
|
|
const [ownerLoading, setOwnerLoading] = useState(false);
|
|
const [ownerError, setOwnerError] = useState("");
|
|
const [ownerNote, setOwnerNote] = useState("");
|
|
|
|
/* ── CF extract statuses ──────────────────────────────────── */
|
|
const [cfStatusMap, setCfStatusMap] = useState<Record<string, string>>({});
|
|
const [cfLatestIds, setCfLatestIds] = useState<Record<string, string>>({});
|
|
const [cfExpiryDates, setCfExpiryDates] = useState<Record<string, string>>(
|
|
{},
|
|
);
|
|
const [cfStatusLoading, setCfStatusLoading] = useState(false);
|
|
const [listCfOrdering, setListCfOrdering] = useState(false);
|
|
const [listCfOrderResult, setListCfOrderResult] = useState("");
|
|
const [listCfDownloading, setListCfDownloading] = useState(false);
|
|
|
|
/* ── Search handlers ──────────────────────────────────────── */
|
|
|
|
const handleSearch = useCallback(async () => {
|
|
if (!siruta || !/^\d+$/.test(siruta)) return;
|
|
if (!featuresSearch.trim()) {
|
|
setSearchResults([]);
|
|
setSearchError("");
|
|
return;
|
|
}
|
|
setLoadingFeatures(true);
|
|
setSearchError("");
|
|
try {
|
|
const res = await fetch("/api/eterra/search", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
siruta,
|
|
search: featuresSearch.trim(),
|
|
...(workspacePk ? { workspacePk } : {}),
|
|
}),
|
|
});
|
|
const data = (await res.json()) as {
|
|
results?: ParcelDetail[];
|
|
total?: number;
|
|
error?: string;
|
|
};
|
|
if (data.error) {
|
|
setSearchResults([]);
|
|
setSearchError(data.error);
|
|
} else {
|
|
setSearchResults(data.results ?? []);
|
|
setSearchError("");
|
|
}
|
|
} catch {
|
|
setSearchError("Eroare de rețea.");
|
|
}
|
|
setLoadingFeatures(false);
|
|
}, [siruta, featuresSearch, workspacePk]);
|
|
|
|
const handleSearchKeyDown = useCallback(
|
|
(e: React.KeyboardEvent) => {
|
|
if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
void handleSearch();
|
|
}
|
|
},
|
|
[handleSearch],
|
|
);
|
|
|
|
/* ── Owner search ──────────────────────────────────────────── */
|
|
|
|
const handleOwnerSearch = useCallback(async () => {
|
|
if (!siruta || !/^\d+$/.test(siruta)) return;
|
|
if (!ownerSearch.trim() || ownerSearch.trim().length < 2) {
|
|
setOwnerError("Minim 2 caractere.");
|
|
return;
|
|
}
|
|
setOwnerLoading(true);
|
|
setOwnerError("");
|
|
setOwnerNote("");
|
|
try {
|
|
const res = await fetch("/api/eterra/search-owner", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
siruta,
|
|
ownerName: ownerSearch.trim(),
|
|
...(workspacePk ? { workspacePk } : {}),
|
|
}),
|
|
});
|
|
const data = (await res.json()) as {
|
|
results?: OwnerSearchResult[];
|
|
total?: number;
|
|
dbSearched?: boolean;
|
|
eterraSearched?: boolean;
|
|
eterraNote?: string;
|
|
error?: string;
|
|
};
|
|
if (data.error) {
|
|
setOwnerResults([]);
|
|
setOwnerError(data.error);
|
|
} else {
|
|
setOwnerResults(data.results ?? []);
|
|
const notes: string[] = [];
|
|
if (data.dbSearched) notes.push("DB local");
|
|
if (data.eterraSearched) notes.push("eTerra API");
|
|
if (data.eterraNote) notes.push(data.eterraNote);
|
|
setOwnerNote(
|
|
notes.length > 0
|
|
? `Surse: ${notes.join(" + ")}${data.total ? ` · ${data.total} rezultate` : ""}`
|
|
: "",
|
|
);
|
|
}
|
|
} catch {
|
|
setOwnerError("Eroare de rețea.");
|
|
}
|
|
setOwnerLoading(false);
|
|
}, [siruta, ownerSearch, workspacePk]);
|
|
|
|
const handleOwnerKeyDown = useCallback(
|
|
(e: React.KeyboardEvent) => {
|
|
if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
void handleOwnerSearch();
|
|
}
|
|
},
|
|
[handleOwnerSearch],
|
|
);
|
|
|
|
const ownerResultToParcelDetail = useCallback(
|
|
(r: OwnerSearchResult): ParcelDetail => ({
|
|
nrCad: r.nrCad,
|
|
nrCF: r.nrCF,
|
|
nrCFVechi: "",
|
|
nrTopo: "",
|
|
intravilan: r.intravilan,
|
|
categorieFolosinta: r.categorieFolosinta,
|
|
adresa: r.adresa,
|
|
proprietari: r.proprietari || r.proprietariVechi,
|
|
proprietariActuali: r.proprietari,
|
|
proprietariVechi: r.proprietariVechi,
|
|
suprafata: typeof r.suprafata === "number" ? r.suprafata : null,
|
|
solicitant: "",
|
|
immovablePk: r.immovablePk,
|
|
}),
|
|
[],
|
|
);
|
|
|
|
/* ── List management ───────────────────────────────────────── */
|
|
|
|
const addToList = useCallback((item: ParcelDetail) => {
|
|
setSearchList((prev) => {
|
|
if (
|
|
prev.some(
|
|
(p) => p.nrCad === item.nrCad && p.immovablePk === item.immovablePk,
|
|
)
|
|
)
|
|
return prev;
|
|
return [...prev, item];
|
|
});
|
|
}, []);
|
|
|
|
const removeFromList = useCallback((nrCad: string) => {
|
|
setSearchList((prev) => prev.filter((p) => p.nrCad !== nrCad));
|
|
}, []);
|
|
|
|
const csvEscape = useCallback((val: string | number | null | undefined) => {
|
|
const s = val != null ? String(val) : "";
|
|
return `"${s.replace(/"/g, '""')}"`;
|
|
}, []);
|
|
|
|
const downloadCSV = useCallback(() => {
|
|
const items = searchList.length > 0 ? searchList : searchResults;
|
|
if (items.length === 0) return;
|
|
const headers = [
|
|
"NR_CAD",
|
|
"NR_CF",
|
|
"NR_CF_VECHI",
|
|
"NR_TOPO",
|
|
"SUPRAFATA",
|
|
"INTRAVILAN",
|
|
"CATEGORIE_FOLOSINTA",
|
|
"ADRESA",
|
|
"PROPRIETARI_ACTUALI",
|
|
"PROPRIETARI_VECHI",
|
|
"SOLICITANT",
|
|
];
|
|
const rows = items.map((p) => [
|
|
csvEscape(p.nrCad),
|
|
csvEscape(p.nrCF),
|
|
csvEscape(p.nrCFVechi),
|
|
csvEscape(p.nrTopo),
|
|
csvEscape(p.suprafata),
|
|
csvEscape(p.intravilan),
|
|
csvEscape(p.categorieFolosinta),
|
|
csvEscape(p.adresa),
|
|
csvEscape(p.proprietariActuali ?? p.proprietari),
|
|
csvEscape(p.proprietariVechi),
|
|
csvEscape(p.solicitant),
|
|
]);
|
|
const csv = [headers.join(","), ...rows.map((r) => r.join(","))].join("\n");
|
|
const blob = new Blob(["\uFEFF" + csv], {
|
|
type: "text/csv;charset=utf-8",
|
|
});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `parcele_${siruta}_${Date.now()}.csv`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}, [searchList, searchResults, siruta, csvEscape]);
|
|
|
|
/* ── CF extract status fetching ────────────────────────────── */
|
|
|
|
const fetchCfStatuses = useCallback(async (cadastralNumbers: string[]) => {
|
|
if (cadastralNumbers.length === 0) return;
|
|
setCfStatusLoading(true);
|
|
try {
|
|
const nrs = cadastralNumbers.join(",");
|
|
const res = await fetch(
|
|
`/api/ancpi/orders?nrCadastral=${encodeURIComponent(nrs)}&limit=1`,
|
|
);
|
|
const data = (await res.json()) as {
|
|
statusMap?: Record<string, string>;
|
|
latestById?: Record<
|
|
string,
|
|
{ id: string; expiresAt: string | null }
|
|
>;
|
|
};
|
|
if (data.statusMap) {
|
|
setCfStatusMap((prev) => ({ ...prev, ...data.statusMap }));
|
|
}
|
|
if (data.latestById) {
|
|
const idMap: Record<string, string> = {};
|
|
const expiryMap: Record<string, string> = {};
|
|
for (const [nr, rec] of Object.entries(data.latestById)) {
|
|
if (rec && typeof rec === "object" && "id" in rec) {
|
|
idMap[nr] = (rec as { id: string }).id;
|
|
const expires = (rec as { expiresAt: string | null }).expiresAt;
|
|
if (expires) {
|
|
expiryMap[nr] = expires;
|
|
}
|
|
}
|
|
}
|
|
setCfLatestIds((prev) => ({ ...prev, ...idMap }));
|
|
setCfExpiryDates((prev) => ({ ...prev, ...expiryMap }));
|
|
}
|
|
} catch {
|
|
/* silent */
|
|
} finally {
|
|
setCfStatusLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const refreshCfStatuses = useCallback(() => {
|
|
const allNrs = new Set<string>();
|
|
for (const r of searchResults) {
|
|
if (r.nrCad) allNrs.add(r.nrCad);
|
|
}
|
|
for (const p of searchList) {
|
|
if (p.nrCad) allNrs.add(p.nrCad);
|
|
}
|
|
if (allNrs.size > 0) {
|
|
void fetchCfStatuses(Array.from(allNrs));
|
|
}
|
|
}, [searchResults, searchList, fetchCfStatuses]);
|
|
|
|
// Auto-fetch CF statuses when search results change
|
|
useEffect(() => {
|
|
const nrs = searchResults.map((r) => r.nrCad).filter(Boolean);
|
|
if (nrs.length > 0) void fetchCfStatuses(nrs);
|
|
}, [searchResults, fetchCfStatuses]);
|
|
|
|
// Auto-fetch CF statuses when list changes
|
|
useEffect(() => {
|
|
const nrs = searchList.map((p) => p.nrCad).filter(Boolean);
|
|
if (nrs.length > 0) void fetchCfStatuses(nrs);
|
|
}, [searchList, fetchCfStatuses]);
|
|
|
|
/* ── List CF ordering + ZIP download ──────────────────────── */
|
|
|
|
const handleListCfOrder = useCallback(async () => {
|
|
if (!siruta || searchList.length === 0 || listCfOrdering) return;
|
|
const toOrder: typeof searchList = [];
|
|
const toReorder: typeof searchList = [];
|
|
const alreadyValid: typeof searchList = [];
|
|
for (const p of searchList) {
|
|
const status = cfStatusMap[p.nrCad];
|
|
if (status === "valid") {
|
|
alreadyValid.push(p);
|
|
} else if (status === "expired") {
|
|
toReorder.push(p);
|
|
} else {
|
|
if (status !== "processing") toOrder.push(p);
|
|
}
|
|
}
|
|
const newCount = toOrder.length;
|
|
const updateCount = toReorder.length;
|
|
const existingCount = alreadyValid.length;
|
|
if (newCount === 0 && updateCount === 0) {
|
|
setListCfOrderResult(
|
|
`Toate cele ${existingCount} extrase sunt valide.`,
|
|
);
|
|
return;
|
|
}
|
|
const msg = [
|
|
newCount > 0 ? `${newCount} extrase noi` : null,
|
|
updateCount > 0 ? `${updateCount} actualizari` : null,
|
|
existingCount > 0 ? `${existingCount} existente (skip)` : null,
|
|
]
|
|
.filter(Boolean)
|
|
.join(", ");
|
|
if (!window.confirm(`Comanda extrase CF:\n${msg}\n\nContinui?`)) return;
|
|
setListCfOrdering(true);
|
|
setListCfOrderResult("");
|
|
try {
|
|
const allToProcess = [...toOrder, ...toReorder];
|
|
const parcels = allToProcess.map((p) => ({
|
|
nrCadastral: p.nrCad,
|
|
siruta,
|
|
judetIndex: 0,
|
|
judetName: selectedUat?.county ?? "",
|
|
uatId: 0,
|
|
uatName: selectedUat?.name ?? "",
|
|
}));
|
|
const res = await fetch("/api/ancpi/order", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ parcels }),
|
|
});
|
|
const data = (await res.json()) as {
|
|
orders?: unknown[];
|
|
error?: string;
|
|
};
|
|
if (!res.ok || data.error) {
|
|
setListCfOrderResult(
|
|
`Eroare: ${data.error ?? "Eroare la comanda"}`,
|
|
);
|
|
} else {
|
|
const count = data.orders?.length ?? allToProcess.length;
|
|
setListCfOrderResult(
|
|
`${count} extras${count > 1 ? "e" : ""} CF trimis${count > 1 ? "e" : ""} la procesare.`,
|
|
);
|
|
const pollInterval = setInterval(() => {
|
|
void refreshCfStatuses();
|
|
}, 10_000);
|
|
setTimeout(() => clearInterval(pollInterval), 5 * 60 * 1000);
|
|
}
|
|
} catch {
|
|
setListCfOrderResult("Eroare retea.");
|
|
} finally {
|
|
setListCfOrdering(false);
|
|
}
|
|
}, [
|
|
siruta,
|
|
searchList,
|
|
listCfOrdering,
|
|
cfStatusMap,
|
|
selectedUat,
|
|
refreshCfStatuses,
|
|
]);
|
|
|
|
const handleListCfDownloadZip = useCallback(async () => {
|
|
if (searchList.length === 0 || listCfDownloading) return;
|
|
const ids: string[] = [];
|
|
for (const p of searchList) {
|
|
const status = cfStatusMap[p.nrCad];
|
|
const extractId = cfLatestIds[p.nrCad];
|
|
if (status === "valid" && extractId) ids.push(extractId);
|
|
}
|
|
if (ids.length === 0) {
|
|
setListCfOrderResult("Niciun extras CF valid in lista.");
|
|
return;
|
|
}
|
|
setListCfDownloading(true);
|
|
try {
|
|
const res = await fetch(`/api/ancpi/download-zip?ids=${ids.join(",")}`);
|
|
if (!res.ok) throw new Error("Eroare descarcare ZIP");
|
|
const blob = await res.blob();
|
|
const cd = res.headers.get("Content-Disposition") ?? "";
|
|
const match = /filename="?([^"]+)"?/.exec(cd);
|
|
const filename = match?.[1]
|
|
? decodeURIComponent(match[1])
|
|
: "Extrase_CF_lista.zip";
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
URL.revokeObjectURL(url);
|
|
} catch {
|
|
setListCfOrderResult("Eroare la descarcarea ZIP.");
|
|
} finally {
|
|
setListCfDownloading(false);
|
|
}
|
|
}, [searchList, cfStatusMap, cfLatestIds, listCfDownloading]);
|
|
|
|
/* ── CF status badge helper ────────────────────────────────── */
|
|
|
|
const CfStatusBadge = useCallback(
|
|
({
|
|
nrCad,
|
|
immovablePk,
|
|
}: {
|
|
nrCad: string;
|
|
immovablePk?: number | string | null;
|
|
}) => {
|
|
if (!immovablePk || !sirutaValid) return null;
|
|
const cfStatus = cfStatusMap[nrCad];
|
|
const extractId = cfLatestIds[nrCad];
|
|
const cfExpiry = cfExpiryDates[nrCad];
|
|
|
|
if (cfStatus === "valid") {
|
|
return (
|
|
<TooltipProvider>
|
|
<div className="flex items-center gap-1">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Badge
|
|
variant="outline"
|
|
className="text-[10px] border-emerald-200 text-emerald-700 dark:border-emerald-800 dark:text-emerald-400 cursor-default"
|
|
>
|
|
Extras CF
|
|
</Badge>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
{cfExpiry
|
|
? `Valid pana la ${formatShortDate(cfExpiry)}`
|
|
: "Extras CF valid"}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
{extractId && (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
className="h-7 w-7 p-0 text-emerald-600"
|
|
asChild
|
|
>
|
|
<a
|
|
href={`/api/ancpi/download?id=${extractId}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<Download className="h-3.5 w-3.5" />
|
|
</a>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Descarca extras CF</TooltipContent>
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
</TooltipProvider>
|
|
);
|
|
}
|
|
if (cfStatus === "expired") {
|
|
return (
|
|
<TooltipProvider>
|
|
<div className="flex items-center gap-1">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Badge
|
|
variant="outline"
|
|
className="text-[10px] border-orange-200 text-orange-600 dark:border-orange-800 dark:text-orange-400 cursor-default"
|
|
>
|
|
Expirat
|
|
</Badge>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
{cfExpiry
|
|
? `Expirat pe ${formatShortDate(cfExpiry)}`
|
|
: "Extras CF expirat"}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
<EpayOrderButton
|
|
nrCadastral={nrCad}
|
|
siruta={siruta}
|
|
judetName={selectedUat?.county ?? ""}
|
|
uatName={selectedUat?.name ?? ""}
|
|
label="Actualizeaza"
|
|
tooltipText="Comanda extras CF nou (1 credit)"
|
|
/>
|
|
</div>
|
|
</TooltipProvider>
|
|
);
|
|
}
|
|
if (cfStatus === "processing") {
|
|
return (
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Badge
|
|
variant="outline"
|
|
className="text-[10px] border-yellow-200 text-yellow-600 dark:border-yellow-800 dark:text-yellow-400 animate-pulse cursor-default"
|
|
>
|
|
Se proceseaza...
|
|
</Badge>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Comanda in curs de procesare</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
);
|
|
}
|
|
// "none" or unknown
|
|
return (
|
|
<EpayOrderButton
|
|
nrCadastral={nrCad}
|
|
siruta={siruta}
|
|
judetName={selectedUat?.county ?? ""}
|
|
uatName={selectedUat?.name ?? ""}
|
|
tooltipText="Comanda extras CF (1 credit)"
|
|
/>
|
|
);
|
|
},
|
|
[cfStatusMap, cfLatestIds, cfExpiryDates, sirutaValid, siruta, selectedUat],
|
|
);
|
|
|
|
/* ── Render ─────────────────────────────────────────────────── */
|
|
|
|
if (!sirutaValid) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="py-12 text-center text-muted-foreground">
|
|
<Search className="h-10 w-10 mx-auto mb-3 opacity-30" />
|
|
<p>Selectează un UAT mai sus pentru a căuta parcele.</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{/* Search input — mode toggle + input */}
|
|
<Card>
|
|
<CardContent className="pt-4 space-y-3">
|
|
{/* Mode toggle */}
|
|
<div className="flex gap-1 p-0.5 bg-muted rounded-md w-fit">
|
|
<button
|
|
onClick={() => setSearchMode("cadastral")}
|
|
className={cn(
|
|
"px-3 py-1 text-xs rounded font-medium transition-colors",
|
|
searchMode === "cadastral"
|
|
? "bg-background shadow text-foreground"
|
|
: "text-muted-foreground hover:text-foreground",
|
|
)}
|
|
>
|
|
<Search className="inline h-3 w-3 mr-1 -mt-0.5" />
|
|
Nr. Cadastral
|
|
</button>
|
|
<button
|
|
onClick={() => setSearchMode("owner")}
|
|
className={cn(
|
|
"px-3 py-1 text-xs rounded font-medium transition-colors",
|
|
searchMode === "owner"
|
|
? "bg-background shadow text-foreground"
|
|
: "text-muted-foreground hover:text-foreground",
|
|
)}
|
|
>
|
|
<User className="inline h-3 w-3 mr-1 -mt-0.5" />
|
|
Proprietar
|
|
</button>
|
|
</div>
|
|
|
|
{/* Cadastral search input */}
|
|
{searchMode === "cadastral" && (
|
|
<div className="flex gap-3 items-end">
|
|
<div className="space-y-1 flex-1">
|
|
<Label className="text-xs">
|
|
Numere cadastrale (separate prin virgulă sau Enter)
|
|
</Label>
|
|
<div className="relative">
|
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="ex: 62580 sau 62580, 62581, 62582"
|
|
className="pl-9"
|
|
value={featuresSearch}
|
|
onChange={(e) => setFeaturesSearch(e.target.value)}
|
|
onKeyDown={handleSearchKeyDown}
|
|
disabled={!session.connected}
|
|
/>
|
|
</div>
|
|
{!session.connected && (
|
|
<p className="text-xs text-muted-foreground">
|
|
Necesită conexiune eTerra. Folosește modul Proprietar
|
|
pentru a căuta offline în DB.
|
|
</p>
|
|
)}
|
|
</div>
|
|
<Button
|
|
onClick={() => void handleSearch()}
|
|
disabled={
|
|
loadingFeatures ||
|
|
!featuresSearch.trim() ||
|
|
!session.connected
|
|
}
|
|
>
|
|
{loadingFeatures ? (
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
) : (
|
|
<Search className="mr-2 h-4 w-4" />
|
|
)}
|
|
Caută
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Owner search input */}
|
|
{searchMode === "owner" && (
|
|
<div className="flex gap-3 items-end">
|
|
<div className="space-y-1 flex-1">
|
|
<Label className="text-xs">
|
|
Nume proprietar (caută în DB local + eTerra)
|
|
</Label>
|
|
<div className="relative">
|
|
<User className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="ex: Popescu Ion"
|
|
className="pl-9"
|
|
value={ownerSearch}
|
|
onChange={(e) => setOwnerSearch(e.target.value)}
|
|
onKeyDown={handleOwnerKeyDown}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
onClick={() => void handleOwnerSearch()}
|
|
disabled={ownerLoading || ownerSearch.trim().length < 2}
|
|
>
|
|
{ownerLoading ? (
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
) : (
|
|
<Search className="mr-2 h-4 w-4" />
|
|
)}
|
|
Caută
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{searchMode === "cadastral" && searchError && (
|
|
<p className="text-xs text-destructive">{searchError}</p>
|
|
)}
|
|
{searchMode === "owner" && ownerError && (
|
|
<p className="text-xs text-destructive">{ownerError}</p>
|
|
)}
|
|
{searchMode === "owner" && ownerNote && (
|
|
<p className="text-xs text-muted-foreground">{ownerNote}</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* ─── Cadastral search results ────────────── */}
|
|
{searchMode === "cadastral" && (
|
|
<>
|
|
{loadingFeatures && searchResults.length === 0 && (
|
|
<Card>
|
|
<CardContent className="py-12 text-center text-muted-foreground">
|
|
<Loader2 className="h-10 w-10 mx-auto mb-3 animate-spin opacity-50" />
|
|
<p>Se caută în eTerra...</p>
|
|
<p className="text-xs mt-1 opacity-60">
|
|
Prima căutare pe un UAT nou poate dura ~10-30s (se
|
|
încarcă lista de județe).
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{searchResults.length > 0 && (
|
|
<>
|
|
{/* Action bar */}
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-muted-foreground">
|
|
{searchResults.length} rezultat
|
|
{searchResults.length > 1 ? "e" : ""}
|
|
{searchList.length > 0 && (
|
|
<span className="ml-2">
|
|
· <strong>{searchList.length}</strong> în listă
|
|
</span>
|
|
)}
|
|
</span>
|
|
<div className="flex gap-2">
|
|
{searchResults.length > 0 && (
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
for (const r of searchResults) addToList(r);
|
|
}}
|
|
>
|
|
<Plus className="mr-1 h-3.5 w-3.5" />
|
|
Adaugă toate în listă
|
|
</Button>
|
|
)}
|
|
<Button
|
|
size="sm"
|
|
variant="default"
|
|
onClick={downloadCSV}
|
|
disabled={
|
|
searchResults.length === 0 && searchList.length === 0
|
|
}
|
|
>
|
|
<FileDown className="mr-1 h-3.5 w-3.5" />
|
|
Descarcă CSV
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Detail cards */}
|
|
<div className="space-y-3">
|
|
{searchResults.map((p, idx) => (
|
|
<Card
|
|
key={`${p.nrCad}-${p.immovablePk}-${idx}`}
|
|
className={cn(
|
|
"transition-colors",
|
|
!p.immovablePk && "opacity-60",
|
|
)}
|
|
>
|
|
<CardContent className="pt-4">
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div>
|
|
<h3 className="text-lg font-semibold tabular-nums">
|
|
Nr. Cad. {p.nrCad}
|
|
</h3>
|
|
{!p.immovablePk && (
|
|
<p className="text-xs text-destructive">
|
|
Parcela nu a fost găsită în eTerra.
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="flex gap-1">
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
className="h-7 w-7 p-0"
|
|
title="Adaugă în listă"
|
|
onClick={() => addToList(p)}
|
|
disabled={!p.immovablePk}
|
|
>
|
|
<Plus className="h-3.5 w-3.5" />
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
className="h-7 w-7 p-0"
|
|
title="Copiaza detalii"
|
|
onClick={() => {
|
|
const text = [
|
|
`Nr. Cad: ${p.nrCad}`,
|
|
`Nr. CF: ${p.nrCF || "—"}`,
|
|
p.nrCFVechi
|
|
? `CF vechi: ${p.nrCFVechi}`
|
|
: null,
|
|
p.nrTopo
|
|
? `Nr. Topo: ${p.nrTopo}`
|
|
: null,
|
|
p.suprafata != null
|
|
? `Suprafata: ${p.suprafata.toLocaleString("ro-RO")} mp`
|
|
: null,
|
|
`Intravilan: ${p.intravilan || "—"}`,
|
|
p.categorieFolosinta
|
|
? `Categorie: ${p.categorieFolosinta}`
|
|
: null,
|
|
p.adresa ? `Adresa: ${p.adresa}` : null,
|
|
p.proprietariActuali
|
|
? `Proprietari actuali: ${p.proprietariActuali}`
|
|
: null,
|
|
p.proprietariVechi
|
|
? `Proprietari vechi: ${p.proprietariVechi}`
|
|
: null,
|
|
!p.proprietariActuali &&
|
|
!p.proprietariVechi &&
|
|
p.proprietari
|
|
? `Proprietari: ${p.proprietari}`
|
|
: null,
|
|
p.solicitant
|
|
? `Solicitant: ${p.solicitant}`
|
|
: null,
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
void navigator.clipboard.writeText(text);
|
|
}}
|
|
>
|
|
<ClipboardCopy className="h-3.5 w-3.5" />
|
|
</Button>
|
|
<CfStatusBadge
|
|
nrCad={p.nrCad}
|
|
immovablePk={p.immovablePk}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{p.immovablePk && (
|
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-x-6 gap-y-2 text-sm">
|
|
<div>
|
|
<span className="text-xs text-muted-foreground block">
|
|
Nr. CF
|
|
</span>
|
|
<span className="font-medium">
|
|
{p.nrCF || "—"}
|
|
</span>
|
|
</div>
|
|
{p.nrCFVechi && (
|
|
<div>
|
|
<span className="text-xs text-muted-foreground block">
|
|
CF vechi
|
|
</span>
|
|
<span>{p.nrCFVechi}</span>
|
|
</div>
|
|
)}
|
|
<div>
|
|
<span className="text-xs text-muted-foreground block">
|
|
Nr. Topo
|
|
</span>
|
|
<span>{p.nrTopo || "—"}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-xs text-muted-foreground block">
|
|
Suprafață
|
|
</span>
|
|
<span className="tabular-nums">
|
|
{p.suprafata != null
|
|
? formatArea(p.suprafata)
|
|
: "—"}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-xs text-muted-foreground block">
|
|
Intravilan
|
|
</span>
|
|
<Badge
|
|
variant={
|
|
p.intravilan === "Da"
|
|
? "default"
|
|
: p.intravilan === "Nu"
|
|
? "secondary"
|
|
: "outline"
|
|
}
|
|
className="text-[11px]"
|
|
>
|
|
{p.intravilan || "—"}
|
|
</Badge>
|
|
</div>
|
|
{p.categorieFolosinta && (
|
|
<div className="col-span-2">
|
|
<span className="text-xs text-muted-foreground block">
|
|
Categorii folosință
|
|
</span>
|
|
<span className="text-xs">
|
|
{p.categorieFolosinta}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{p.adresa && (
|
|
<div className="col-span-2">
|
|
<span className="text-xs text-muted-foreground block">
|
|
Adresă
|
|
</span>
|
|
<span>{p.adresa}</span>
|
|
</div>
|
|
)}
|
|
{(p.proprietariActuali || p.proprietariVechi) && (
|
|
<div className="col-span-2 lg:col-span-4">
|
|
{p.proprietariActuali && (
|
|
<div className="mb-1">
|
|
<span className="text-xs text-muted-foreground block">
|
|
Proprietari actuali
|
|
</span>
|
|
<span className="font-medium text-sm">
|
|
{p.proprietariActuali}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{p.proprietariVechi && (
|
|
<div>
|
|
<span className="text-xs text-muted-foreground block">
|
|
Proprietari anteriori
|
|
</span>
|
|
<span className="text-[11px] text-muted-foreground/80">
|
|
{p.proprietariVechi}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{!p.proprietariActuali &&
|
|
!p.proprietariVechi &&
|
|
p.proprietari && (
|
|
<div>
|
|
<span className="text-xs text-muted-foreground block">
|
|
Proprietari
|
|
</span>
|
|
<span>{p.proprietari}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
{p.solicitant && (
|
|
<div className="col-span-2">
|
|
<span className="text-xs text-muted-foreground block">
|
|
Solicitant
|
|
</span>
|
|
<span>{p.solicitant}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Empty state */}
|
|
{searchMode === "cadastral" &&
|
|
searchResults.length === 0 &&
|
|
!loadingFeatures &&
|
|
!searchError && (
|
|
<Card>
|
|
<CardContent className="py-12 text-center text-muted-foreground">
|
|
<Search className="h-10 w-10 mx-auto mb-3 opacity-30" />
|
|
<p>Introdu un număr cadastral și apasă Caută.</p>
|
|
<p className="text-xs mt-1 opacity-60">
|
|
Poți căuta mai multe parcele simultan, separate prin
|
|
virgulă.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* ─── Owner search results ────────────────── */}
|
|
{searchMode === "owner" && (
|
|
<>
|
|
{ownerLoading && ownerResults.length === 0 && (
|
|
<Card>
|
|
<CardContent className="py-12 text-center text-muted-foreground">
|
|
<Loader2 className="h-10 w-10 mx-auto mb-3 animate-spin opacity-50" />
|
|
<p>Se caută proprietar...</p>
|
|
<p className="text-xs mt-1 opacity-60">
|
|
Caută mai întâi în DB local (date îmbogățite), apoi pe
|
|
eTerra.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{ownerResults.length > 0 && (
|
|
<>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-muted-foreground">
|
|
{ownerResults.length} rezultat
|
|
{ownerResults.length > 1 ? "e" : ""} pentru "
|
|
{ownerSearch}"
|
|
</span>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
for (const r of ownerResults)
|
|
addToList(ownerResultToParcelDetail(r));
|
|
}}
|
|
>
|
|
<Plus className="mr-1 h-3.5 w-3.5" />
|
|
Adaugă toate în listă
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="default"
|
|
onClick={downloadCSV}
|
|
disabled={
|
|
ownerResults.length === 0 && searchList.length === 0
|
|
}
|
|
>
|
|
<FileDown className="mr-1 h-3.5 w-3.5" />
|
|
Descarcă CSV
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{ownerResults.map((r, idx) => (
|
|
<Card key={`owner-${r.nrCad}-${idx}`}>
|
|
<CardContent className="pt-4">
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div>
|
|
<h3 className="text-lg font-semibold tabular-nums">
|
|
Nr. Cad. {r.nrCad}
|
|
</h3>
|
|
<Badge
|
|
variant="outline"
|
|
className="text-[10px] mt-1"
|
|
>
|
|
{r.source === "db"
|
|
? "din baza de date"
|
|
: "eTerra online"}
|
|
</Badge>
|
|
</div>
|
|
<div className="flex gap-1">
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
className="h-7 w-7 p-0"
|
|
title="Adaugă în listă"
|
|
onClick={() =>
|
|
addToList(ownerResultToParcelDetail(r))
|
|
}
|
|
>
|
|
<Plus className="h-3.5 w-3.5" />
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
className="h-7 w-7 p-0"
|
|
title="Copiaza detalii"
|
|
onClick={() => {
|
|
const text = [
|
|
`Nr. Cad: ${r.nrCad}`,
|
|
r.nrCF ? `Nr. CF: ${r.nrCF}` : null,
|
|
r.proprietari
|
|
? `Proprietari: ${r.proprietari}`
|
|
: null,
|
|
r.proprietariVechi
|
|
? `Proprietari vechi: ${r.proprietariVechi}`
|
|
: null,
|
|
r.adresa ? `Adresa: ${r.adresa}` : null,
|
|
r.suprafata
|
|
? `Suprafata: ${r.suprafata} mp`
|
|
: null,
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
void navigator.clipboard.writeText(text);
|
|
}}
|
|
>
|
|
<ClipboardCopy className="h-3.5 w-3.5" />
|
|
</Button>
|
|
<CfStatusBadge
|
|
nrCad={r.nrCad}
|
|
immovablePk={r.immovablePk}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-x-6 gap-y-2 text-sm">
|
|
{r.nrCF && (
|
|
<div>
|
|
<span className="text-xs text-muted-foreground block">
|
|
Nr. CF
|
|
</span>
|
|
<span className="font-medium">{r.nrCF}</span>
|
|
</div>
|
|
)}
|
|
{r.suprafata && (
|
|
<div>
|
|
<span className="text-xs text-muted-foreground block">
|
|
Suprafață
|
|
</span>
|
|
<span className="tabular-nums">
|
|
{typeof r.suprafata === "number"
|
|
? formatArea(r.suprafata)
|
|
: `${r.suprafata} mp`}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{r.intravilan && (
|
|
<div>
|
|
<span className="text-xs text-muted-foreground block">
|
|
Intravilan
|
|
</span>
|
|
<Badge
|
|
variant={
|
|
r.intravilan === "Da"
|
|
? "default"
|
|
: r.intravilan === "Nu"
|
|
? "secondary"
|
|
: "outline"
|
|
}
|
|
className="text-[11px]"
|
|
>
|
|
{r.intravilan}
|
|
</Badge>
|
|
</div>
|
|
)}
|
|
{r.categorieFolosinta && (
|
|
<div className="col-span-2">
|
|
<span className="text-xs text-muted-foreground block">
|
|
Categorii folosință
|
|
</span>
|
|
<span className="text-xs">
|
|
{r.categorieFolosinta}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{r.adresa && (
|
|
<div className="col-span-2">
|
|
<span className="text-xs text-muted-foreground block">
|
|
Adresă
|
|
</span>
|
|
<span>{r.adresa}</span>
|
|
</div>
|
|
)}
|
|
{r.proprietari && (
|
|
<div className="col-span-2 lg:col-span-4">
|
|
<span className="text-xs text-muted-foreground block">
|
|
Proprietari actuali
|
|
</span>
|
|
<span className="font-medium text-sm">
|
|
{r.proprietari}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{r.proprietariVechi && (
|
|
<div className="col-span-2 lg:col-span-4">
|
|
<span className="text-xs text-muted-foreground block">
|
|
Proprietari anteriori
|
|
</span>
|
|
<span className="text-[11px] text-muted-foreground/80">
|
|
{r.proprietariVechi}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{ownerResults.length === 0 && !ownerLoading && !ownerError && (
|
|
<Card>
|
|
<CardContent className="py-12 text-center text-muted-foreground">
|
|
<User className="h-10 w-10 mx-auto mb-3 opacity-30" />
|
|
<p>Introdu numele proprietarului și apasă Caută.</p>
|
|
<p className="text-xs mt-1 opacity-60">
|
|
Caută în datele îmbogățite (DB local) și pe eTerra.
|
|
<br />
|
|
Pentru rezultate complete, lansează "Sync fundal —
|
|
Magic" în tab-ul Export.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Saved list */}
|
|
{searchList.length > 0 && (
|
|
<Card>
|
|
<CardContent className="pt-4">
|
|
<div className="flex items-center justify-between mb-3 flex-wrap gap-2">
|
|
<h3 className="text-sm font-medium">
|
|
Lista mea ({searchList.length} parcele)
|
|
</h3>
|
|
<div className="flex gap-2 flex-wrap">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
setSearchList([]);
|
|
setListCfOrderResult("");
|
|
}}
|
|
>
|
|
<Trash2 className="mr-1 h-3.5 w-3.5" />
|
|
Goleste
|
|
</Button>
|
|
<Button size="sm" variant="outline" onClick={downloadCSV}>
|
|
<FileDown className="mr-1 h-3.5 w-3.5" />
|
|
CSV din lista
|
|
</Button>
|
|
{/* Download all valid CF extracts as ZIP */}
|
|
{searchList.some(
|
|
(p) => cfStatusMap[p.nrCad] === "valid",
|
|
) &&
|
|
(() => {
|
|
const validCount = searchList.filter(
|
|
(p) => cfStatusMap[p.nrCad] === "valid",
|
|
).length;
|
|
return (
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="border-emerald-200 text-emerald-700 dark:border-emerald-800 dark:text-emerald-400"
|
|
disabled={listCfDownloading}
|
|
onClick={() =>
|
|
void handleListCfDownloadZip()
|
|
}
|
|
>
|
|
{listCfDownloading ? (
|
|
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
|
) : (
|
|
<Archive className="mr-1 h-3.5 w-3.5" />
|
|
)}
|
|
Descarca Extrase CF
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{`Descarca ZIP cu ${validCount} extrase valide din lista`}</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
);
|
|
})()}
|
|
{/* Order CF extracts for list */}
|
|
{epayStatus.connected &&
|
|
(() => {
|
|
const newCount = searchList.filter((p) => {
|
|
const s = cfStatusMap[p.nrCad];
|
|
return (
|
|
s !== "valid" &&
|
|
s !== "expired" &&
|
|
s !== "processing"
|
|
);
|
|
}).length;
|
|
const updateCount = searchList.filter(
|
|
(p) => cfStatusMap[p.nrCad] === "expired",
|
|
).length;
|
|
const totalCredits = newCount + updateCount;
|
|
const validCount = searchList.filter(
|
|
(p) => cfStatusMap[p.nrCad] === "valid",
|
|
).length;
|
|
return (
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
size="sm"
|
|
disabled={listCfOrdering}
|
|
onClick={() => void handleListCfOrder()}
|
|
>
|
|
{listCfOrdering ? (
|
|
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
|
) : (
|
|
<FileText className="mr-1 h-3.5 w-3.5" />
|
|
)}
|
|
Scoate Extrase CF
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
{`Comanda ${newCount} extrase noi + ${updateCount} actualizari = ${totalCredits} credite. ${validCount} existente valide raman.`}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
);
|
|
})()}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Order result message */}
|
|
{listCfOrderResult && (
|
|
<p
|
|
className={cn(
|
|
"text-xs mb-2",
|
|
listCfOrderResult.startsWith("Eroare")
|
|
? "text-destructive"
|
|
: "text-emerald-600 dark:text-emerald-400",
|
|
)}
|
|
>
|
|
{listCfOrderResult}
|
|
</p>
|
|
)}
|
|
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b bg-muted/40">
|
|
<th className="px-2 py-2 text-center font-medium w-8 text-muted-foreground">
|
|
#
|
|
</th>
|
|
<th className="px-3 py-2 text-left font-medium">
|
|
Nr. Cad
|
|
</th>
|
|
<th className="px-3 py-2 text-left font-medium">
|
|
Nr. CF
|
|
</th>
|
|
<th className="px-3 py-2 text-right font-medium hidden sm:table-cell">
|
|
Suprafata
|
|
</th>
|
|
<th className="px-3 py-2 text-left font-medium hidden md:table-cell">
|
|
Proprietari
|
|
</th>
|
|
<th className="px-3 py-2 text-center font-medium">
|
|
Extras CF
|
|
</th>
|
|
<th className="px-3 py-2 w-8"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{searchList.map((p, idx) => {
|
|
const cfStatus = cfStatusMap[p.nrCad];
|
|
const cfExpiry = cfExpiryDates[p.nrCad];
|
|
return (
|
|
<tr
|
|
key={`list-${p.nrCad}-${p.immovablePk}`}
|
|
className="border-b hover:bg-muted/30 transition-colors"
|
|
>
|
|
<td className="px-2 py-2 text-center text-xs text-muted-foreground tabular-nums">
|
|
{idx + 1}
|
|
</td>
|
|
<td className="px-3 py-2 font-mono text-xs font-medium">
|
|
{p.nrCad}
|
|
</td>
|
|
<td className="px-3 py-2 text-xs">
|
|
{p.nrCF || "—"}
|
|
</td>
|
|
<td className="px-3 py-2 text-right hidden sm:table-cell tabular-nums text-xs">
|
|
{p.suprafata != null
|
|
? formatArea(p.suprafata)
|
|
: "—"}
|
|
</td>
|
|
<td className="px-3 py-2 hidden md:table-cell text-xs truncate max-w-[300px]">
|
|
{p.proprietari || "—"}
|
|
</td>
|
|
<td className="px-3 py-2 text-center">
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
{cfStatus === "valid" ? (
|
|
<span className="inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-400 dark:border-emerald-800 cursor-default">
|
|
Valid
|
|
</span>
|
|
) : cfStatus === "expired" ? (
|
|
<span className="inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-950/40 dark:text-orange-400 dark:border-orange-800 cursor-default">
|
|
Expirat
|
|
</span>
|
|
) : cfStatus === "processing" ? (
|
|
<span className="inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium bg-yellow-100 text-yellow-700 border-yellow-200 dark:bg-yellow-950/40 dark:text-yellow-400 dark:border-yellow-800 animate-pulse cursor-default">
|
|
Procesare
|
|
</span>
|
|
) : (
|
|
<span className="inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium bg-muted text-muted-foreground border-muted-foreground/20 cursor-default">
|
|
Lipsa
|
|
</span>
|
|
)}
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
{cfStatus === "valid"
|
|
? cfExpiry
|
|
? `Extras CF valid pana la ${formatShortDate(cfExpiry)}`
|
|
: "Extras CF valid"
|
|
: cfStatus === "expired"
|
|
? cfExpiry
|
|
? `Extras CF expirat pe ${formatShortDate(cfExpiry)}. Va fi actualizat automat la 'Scoate Extrase CF'.`
|
|
: "Extras CF expirat. Va fi actualizat automat la 'Scoate Extrase CF'."
|
|
: cfStatus === "processing"
|
|
? "Comanda in curs de procesare"
|
|
: "Nu exista extras CF. Apasa 'Scoate Extrase CF' pentru a comanda."}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</td>
|
|
<td className="px-3 py-2">
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
className="h-6 w-6 p-0"
|
|
onClick={() => removeFromList(p.nrCad)}
|
|
>
|
|
<XCircle className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</>
|
|
);
|
|
}
|