feat(ancpi): complete ePay UI redesign + ZIP download + smart batch ordering
UI Redesign:
- ePay auto-connect when UAT is selected (no manual button)
- Credit badge with tooltip ("N credite ePay disponibile")
- Search result cards show CF status: Valid (green), Expirat (orange),
Lipsă (gray), Se proceseaza (yellow pulse)
- Action buttons on each card: download/update/order CF extract
- "Lista mea" numbered rows + CF Status column + smart batch button
"Scoate Extrase CF": skips valid, re-orders expired, orders new
- "Descarca Extrase CF" button → ZIP archive with numbered files
- Extrase CF tab simplified: clean table, filters (Toate/Valabile/
Expirate/In procesare), search, download-all ZIP
Backend:
- GET /api/ancpi/download-zip?ids=... → JSZip streaming
- GET /api/ancpi/orders: multi-cadastral status check with statusMap
(valid/expired/none/processing) + latestById
Data:
- Simulated expired extract for 328611 (Cluj-Napoca, expired 2026-03-17)
- Cleaned old error records from DB
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -55,9 +55,9 @@ import {
|
||||
} from "../services/eterra-layers";
|
||||
import type { ParcelDetail } from "@/app/api/eterra/search/route";
|
||||
import type { OwnerSearchResult } from "@/app/api/eterra/search-owner/route";
|
||||
import { User, FileText } from "lucide-react";
|
||||
import { User, FileText, Archive } from "lucide-react";
|
||||
import { UatDashboard } from "./uat-dashboard";
|
||||
import { EpayConnect } from "./epay-connect";
|
||||
import { EpayConnect, type EpaySessionStatus } from "./epay-connect";
|
||||
import { EpayOrderButton } from "./epay-order-button";
|
||||
import { EpayTab } from "./epay-tab";
|
||||
|
||||
@@ -441,6 +441,20 @@ export function ParcelSyncModule() {
|
||||
/* dashboard */
|
||||
const [dashboardSiruta, setDashboardSiruta] = useState<string | null>(null);
|
||||
|
||||
/* ── ePay status (for CF extract features) ──────────────────── */
|
||||
const [epayStatus, setEpayStatus] = useState<EpaySessionStatus>({ connected: false });
|
||||
/** CF status map: nrCadastral -> "valid" | "expired" | "none" | "processing" */
|
||||
const [cfStatusMap, setCfStatusMap] = useState<Record<string, string>>({});
|
||||
/** Latest completed extract IDs per nrCadastral */
|
||||
const [cfLatestIds, setCfLatestIds] = useState<Record<string, string>>({});
|
||||
/** Whether we're currently loading CF statuses */
|
||||
const [cfStatusLoading, setCfStatusLoading] = useState(false);
|
||||
/** List CF batch order state */
|
||||
const [listCfOrdering, setListCfOrdering] = useState(false);
|
||||
const [listCfOrderResult, setListCfOrderResult] = useState("");
|
||||
/** Downloading ZIP state */
|
||||
const [listCfDownloading, setListCfDownloading] = useState(false);
|
||||
|
||||
/* ── No-geometry import option ──────────────────────────────── */
|
||||
const [includeNoGeom, setIncludeNoGeom] = useState(false);
|
||||
const [noGeomScanning, setNoGeomScanning] = useState(false);
|
||||
@@ -1496,6 +1510,66 @@ export function ParcelSyncModule() {
|
||||
setLoadingFeatures(false);
|
||||
}, [siruta, featuresSearch, workspacePk]);
|
||||
|
||||
/** Fetch CF extract status for a set of cadastral numbers */
|
||||
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> = {};
|
||||
for (const [nr, rec] of Object.entries(data.latestById)) {
|
||||
if (rec && typeof rec === "object" && "id" in rec) {
|
||||
idMap[nr] = (rec as { id: string }).id;
|
||||
}
|
||||
}
|
||||
setCfLatestIds((prev) => ({ ...prev, ...idMap }));
|
||||
}
|
||||
} catch {
|
||||
/* silent */
|
||||
} finally {
|
||||
setCfStatusLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/** Refresh CF statuses for current search results + list items */
|
||||
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]);
|
||||
|
||||
// No auto-search — user clicks button or presses Enter
|
||||
const handleSearchKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
@@ -1648,6 +1722,143 @@ export function ParcelSyncModule() {
|
||||
URL.revokeObjectURL(url);
|
||||
}, [searchList, searchResults, siruta, csvEscape]);
|
||||
|
||||
// Resolve selected UAT entry for ePay order context (needed by CF order handlers below)
|
||||
const selectedUat = useMemo(
|
||||
() => uatData.find((u) => u.siruta === siruta),
|
||||
[uatData, siruta],
|
||||
);
|
||||
|
||||
/* ════════════════════════════════════════════════════════════ */
|
||||
/* List CF extract ordering + ZIP download */
|
||||
/* ════════════════════════════════════════════════════════════ */
|
||||
|
||||
/** Order CF extracts for list items: skip valid, re-order expired, order new */
|
||||
const handleListCfOrder = useCallback(async () => {
|
||||
if (!siruta || searchList.length === 0 || listCfOrdering) return;
|
||||
|
||||
// Categorize parcels
|
||||
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 {
|
||||
// "none" or "processing" or unknown
|
||||
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;
|
||||
}
|
||||
|
||||
// Confirm
|
||||
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.`);
|
||||
|
||||
// Start polling for completion and refresh statuses periodically
|
||||
const pollInterval = setInterval(() => {
|
||||
void refreshCfStatuses();
|
||||
}, 10_000);
|
||||
|
||||
// Stop after 5 minutes
|
||||
setTimeout(() => clearInterval(pollInterval), 5 * 60 * 1000);
|
||||
}
|
||||
} catch {
|
||||
setListCfOrderResult("Eroare retea.");
|
||||
} finally {
|
||||
setListCfOrdering(false);
|
||||
}
|
||||
}, [siruta, searchList, listCfOrdering, cfStatusMap, selectedUat, refreshCfStatuses]);
|
||||
|
||||
/** Download all valid CF extracts from list as ZIP */
|
||||
const handleListCfDownloadZip = useCallback(async () => {
|
||||
if (searchList.length === 0 || listCfDownloading) return;
|
||||
|
||||
// Collect valid extract IDs in list order
|
||||
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]);
|
||||
|
||||
/* ════════════════════════════════════════════════════════════ */
|
||||
/* Derived data */
|
||||
/* ════════════════════════════════════════════════════════════ */
|
||||
@@ -1663,12 +1874,6 @@ export function ParcelSyncModule() {
|
||||
|
||||
const sirutaValid = siruta.length > 0 && /^\d+$/.test(siruta);
|
||||
|
||||
// Resolve selected UAT entry for ePay order context
|
||||
const selectedUat = useMemo(
|
||||
() => uatData.find((u) => u.siruta === siruta),
|
||||
[uatData, siruta],
|
||||
);
|
||||
|
||||
const progressPct =
|
||||
exportProgress?.total && exportProgress.total > 0
|
||||
? Math.round((exportProgress.downloaded / exportProgress.total) * 100)
|
||||
@@ -1798,7 +2003,10 @@ export function ParcelSyncModule() {
|
||||
|
||||
{/* Connection pills */}
|
||||
<div className="flex items-center gap-2">
|
||||
<EpayConnect />
|
||||
<EpayConnect
|
||||
triggerConnect={sirutaValid}
|
||||
onStatusChange={setEpayStatus}
|
||||
/>
|
||||
<ConnectionPill
|
||||
session={session}
|
||||
connecting={connecting}
|
||||
@@ -2059,23 +2267,23 @@ export function ParcelSyncModule() {
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 p-0"
|
||||
title="Copiază detalii"
|
||||
title="Copiaza detalii"
|
||||
onClick={() => {
|
||||
const text = [
|
||||
`Nr. Cad: ${p.nrCad}`,
|
||||
`Nr. CF: ${p.nrCF || "—"}`,
|
||||
`Nr. CF: ${p.nrCF || "\u2014"}`,
|
||||
p.nrCFVechi
|
||||
? `CF vechi: ${p.nrCFVechi}`
|
||||
: null,
|
||||
p.nrTopo ? `Nr. Topo: ${p.nrTopo}` : null,
|
||||
p.suprafata != null
|
||||
? `Suprafață: ${p.suprafata.toLocaleString("ro-RO")} mp`
|
||||
? `Suprafata: ${p.suprafata.toLocaleString("ro-RO")} mp`
|
||||
: null,
|
||||
`Intravilan: ${p.intravilan || "—"}`,
|
||||
`Intravilan: ${p.intravilan || "\u2014"}`,
|
||||
p.categorieFolosinta
|
||||
? `Categorie: ${p.categorieFolosinta}`
|
||||
: null,
|
||||
p.adresa ? `Adresă: ${p.adresa}` : null,
|
||||
p.adresa ? `Adresa: ${p.adresa}` : null,
|
||||
p.proprietariActuali
|
||||
? `Proprietari actuali: ${p.proprietariActuali}`
|
||||
: null,
|
||||
@@ -2098,14 +2306,77 @@ export function ParcelSyncModule() {
|
||||
>
|
||||
<ClipboardCopy className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
{p.immovablePk && sirutaValid && (
|
||||
<EpayOrderButton
|
||||
nrCadastral={p.nrCad}
|
||||
siruta={siruta}
|
||||
judetName={selectedUat?.county ?? ""}
|
||||
uatName={selectedUat?.name ?? ""}
|
||||
/>
|
||||
)}
|
||||
{/* CF Extract status + actions */}
|
||||
{p.immovablePk && sirutaValid && (() => {
|
||||
const cfStatus = cfStatusMap[p.nrCad];
|
||||
const extractId = cfLatestIds[p.nrCad];
|
||||
if (cfStatus === "valid") {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[10px] border-emerald-200 text-emerald-700 dark:border-emerald-800 dark:text-emerald-400"
|
||||
>
|
||||
Extras CF valid
|
||||
</Badge>
|
||||
{extractId && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 p-0 text-emerald-600"
|
||||
title="Descarca extras CF"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={`/api/ancpi/download?id=${extractId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (cfStatus === "expired") {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[10px] border-orange-200 text-orange-600 dark:border-orange-800 dark:text-orange-400"
|
||||
>
|
||||
Extras CF expirat
|
||||
</Badge>
|
||||
<EpayOrderButton
|
||||
nrCadastral={p.nrCad}
|
||||
siruta={siruta}
|
||||
judetName={selectedUat?.county ?? ""}
|
||||
uatName={selectedUat?.name ?? ""}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (cfStatus === "processing") {
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[10px] border-yellow-200 text-yellow-600 dark:border-yellow-800 dark:text-yellow-400 animate-pulse"
|
||||
>
|
||||
Se proceseaza...
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
// "none" or unknown
|
||||
return (
|
||||
<EpayOrderButton
|
||||
nrCadastral={p.nrCad}
|
||||
siruta={siruta}
|
||||
judetName={selectedUat?.county ?? ""}
|
||||
uatName={selectedUat?.name ?? ""}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2333,7 +2604,7 @@ export function ParcelSyncModule() {
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 p-0"
|
||||
title="Copiază detalii"
|
||||
title="Copiaza detalii"
|
||||
onClick={() => {
|
||||
const text = [
|
||||
`Nr. Cad: ${r.nrCad}`,
|
||||
@@ -2344,9 +2615,9 @@ export function ParcelSyncModule() {
|
||||
r.proprietariVechi
|
||||
? `Proprietari vechi: ${r.proprietariVechi}`
|
||||
: null,
|
||||
r.adresa ? `Adresă: ${r.adresa}` : null,
|
||||
r.adresa ? `Adresa: ${r.adresa}` : null,
|
||||
r.suprafata
|
||||
? `Suprafață: ${r.suprafata} mp`
|
||||
? `Suprafata: ${r.suprafata} mp`
|
||||
: null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
@@ -2356,14 +2627,76 @@ export function ParcelSyncModule() {
|
||||
>
|
||||
<ClipboardCopy className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
{r.immovablePk && sirutaValid && (
|
||||
<EpayOrderButton
|
||||
nrCadastral={r.nrCad}
|
||||
siruta={siruta}
|
||||
judetName={selectedUat?.county ?? ""}
|
||||
uatName={selectedUat?.name ?? ""}
|
||||
/>
|
||||
)}
|
||||
{/* CF Extract status + actions */}
|
||||
{r.immovablePk && sirutaValid && (() => {
|
||||
const cfStatus = cfStatusMap[r.nrCad];
|
||||
const extractId = cfLatestIds[r.nrCad];
|
||||
if (cfStatus === "valid") {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[10px] border-emerald-200 text-emerald-700 dark:border-emerald-800 dark:text-emerald-400"
|
||||
>
|
||||
Extras CF valid
|
||||
</Badge>
|
||||
{extractId && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 p-0 text-emerald-600"
|
||||
title="Descarca extras CF"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={`/api/ancpi/download?id=${extractId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (cfStatus === "expired") {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[10px] border-orange-200 text-orange-600 dark:border-orange-800 dark:text-orange-400"
|
||||
>
|
||||
Extras CF expirat
|
||||
</Badge>
|
||||
<EpayOrderButton
|
||||
nrCadastral={r.nrCad}
|
||||
siruta={siruta}
|
||||
judetName={selectedUat?.county ?? ""}
|
||||
uatName={selectedUat?.name ?? ""}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (cfStatus === "processing") {
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[10px] border-yellow-200 text-yellow-600 dark:border-yellow-800 dark:text-yellow-400 animate-pulse"
|
||||
>
|
||||
Se proceseaza...
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EpayOrderButton
|
||||
nrCadastral={r.nrCad}
|
||||
siruta={siruta}
|
||||
judetName={selectedUat?.county ?? ""}
|
||||
uatName={selectedUat?.name ?? ""}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2474,29 +2807,80 @@ export function ParcelSyncModule() {
|
||||
{searchList.length > 0 && (
|
||||
<Card>
|
||||
<CardContent className="pt-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<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">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setSearchList([])}
|
||||
onClick={() => {
|
||||
setSearchList([]);
|
||||
setListCfOrderResult("");
|
||||
}}
|
||||
>
|
||||
<Trash2 className="mr-1 h-3.5 w-3.5" />
|
||||
Golește
|
||||
Goleste
|
||||
</Button>
|
||||
<Button size="sm" onClick={downloadCSV}>
|
||||
<Button size="sm" variant="outline" onClick={downloadCSV}>
|
||||
<FileDown className="mr-1 h-3.5 w-3.5" />
|
||||
CSV din listă
|
||||
CSV din lista
|
||||
</Button>
|
||||
{/* Download all valid CF extracts as ZIP */}
|
||||
{searchList.some((p) => cfStatusMap[p.nrCad] === "valid") && (
|
||||
<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>
|
||||
)}
|
||||
{/* Order CF extracts for list */}
|
||||
{epayStatus.connected && (
|
||||
<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>
|
||||
)}
|
||||
</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>
|
||||
@@ -2504,46 +2888,74 @@ export function ParcelSyncModule() {
|
||||
Nr. CF
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right font-medium hidden sm:table-cell">
|
||||
Suprafață
|
||||
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) => (
|
||||
<tr
|
||||
key={`list-${p.nrCad}-${p.immovablePk}`}
|
||||
className="border-b hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<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">
|
||||
<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>
|
||||
))}
|
||||
{searchList.map((p, idx) => {
|
||||
const cfStatus = cfStatusMap[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 || "\u2014"}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right hidden sm:table-cell tabular-nums text-xs">
|
||||
{p.suprafata != null
|
||||
? formatArea(p.suprafata)
|
||||
: "\u2014"}
|
||||
</td>
|
||||
<td className="px-3 py-2 hidden md:table-cell text-xs truncate max-w-[300px]">
|
||||
{p.proprietari || "\u2014"}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-center">
|
||||
{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">
|
||||
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">
|
||||
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">
|
||||
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">
|
||||
Lipsa
|
||||
</span>
|
||||
)}
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user