feat(geoportal-v2): inline CF order modal — confirmation + animated steps
Marius: click "Comandă CF" from the card itself, no new-tab to
parcel-sync. Show "Ești sigur? Costă 1 credit, mai ai X" first.
Animate the order through its phases until done.
New component cf-order-modal.tsx — a 7-state machine over a single
shadcn-style dialog:
loading-status — checks /api/ancpi/session for connection + credits
not-connected — ePay session offline → prompt to connect via
parcel-sync (the only place credentials live)
no-credits — 0 credits, can't proceed
ready — confirmation: 1 credit cost, current balance,
projected balance after the order, all in
rounded chips with Coins icon
placing — POST /api/ancpi/order, spinner on step 1
processing — poll /api/ancpi/orders every 3s until status
becomes completed/done/minioPath populated.
Shows live elapsed seconds; 90s timeout falls
through to error with "verifică din nou peste
câteva minute".
done — checkmark anim + "Descarcă PDF" if document URL
came back
error — destructive panel + Reîncearcă button
Animations (tailwindcss-animate utilities):
- Modal backdrop: fade-in 200ms
- Modal card: zoom-in-95 + slide-in-from-bottom 200ms
- Step rows: active row gets primary-tinted bg + Loader2 spin,
done rows turn emerald + Check icon zooms in 300ms
- Success/error final state: rounded badge + icon zooms in 500ms
Footer adapts per phase: Anulează+Confirmă (ready), Conectează ePay
(not-connected), Închide (loading/no-credits), Închide fereastra
(placing/processing — order continues in bg), Gata (done), Închide+
Reîncearcă (error).
Wires into feature-info-panel by replacing the "open /parcel-sync"
click handler with setCfModalOpen(true). Modal mounts at the
panel's root with fixed positioning + z-50 so it overlays the map.
Backdrop click dismisses except during placing/processing.
Uses the legacy /api/ancpi/* endpoints (not /api/cf/* gis-ac route)
per Marius's earlier decision to keep credit tracking on his own
ePay session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,572 @@
|
||||
"use client";
|
||||
|
||||
// Inline CF order flow for the V2 panel — replaces the deep-link to
|
||||
// /parcel-sync. Marius rejected the gis-api shared-pool model
|
||||
// ([[architools-cutover-state-2026-05-19]]), so this stays on the
|
||||
// legacy `/api/ancpi/order` route which uses HIS connected ePay
|
||||
// session and decrements his own credit balance.
|
||||
//
|
||||
// State machine:
|
||||
// loading-status → checking ePay session + credit count
|
||||
// not-connected → user must connect ePay first (deep-link out)
|
||||
// no-credits → 0 credits, nothing to do
|
||||
// ready → confirmation step ("1 credit, mai ai X")
|
||||
// placing → POST /api/ancpi/order
|
||||
// processing → poll /api/ancpi/orders for completion
|
||||
// done → final state, optional download link
|
||||
// error → any failure with retry option
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
X, Loader2, Check, AlertCircle, FileText, Coins,
|
||||
Plug, ArrowRight, Download,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
|
||||
type Phase =
|
||||
| "loading-status"
|
||||
| "not-connected"
|
||||
| "no-credits"
|
||||
| "ready"
|
||||
| "placing"
|
||||
| "processing"
|
||||
| "done"
|
||||
| "error";
|
||||
|
||||
interface SessionStatus {
|
||||
connected: boolean;
|
||||
credits?: number | null;
|
||||
}
|
||||
|
||||
interface OrderListResponse {
|
||||
total?: number;
|
||||
orders?: Array<{
|
||||
id: string;
|
||||
nrCadastral: string;
|
||||
status: string;
|
||||
epayStatus?: string | null;
|
||||
documentName?: string | null;
|
||||
minioPath?: string | null;
|
||||
completedAt?: string | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
cadastralRef: string;
|
||||
siruta: string;
|
||||
/** Optional — passed through to the order body. The legacy route
|
||||
* tolerates blanks when judet/uat ids default to 0. */
|
||||
judetName?: string;
|
||||
uatName?: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const POLL_INTERVAL_MS = 3_000;
|
||||
const POLL_TIMEOUT_MS = 90_000;
|
||||
|
||||
export function CfOrderModal({
|
||||
open,
|
||||
cadastralRef,
|
||||
siruta,
|
||||
judetName,
|
||||
uatName,
|
||||
onClose,
|
||||
}: Props) {
|
||||
const [phase, setPhase] = useState<Phase>("loading-status");
|
||||
const [status, setStatus] = useState<SessionStatus | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [orderId, setOrderId] = useState<string | null>(null);
|
||||
const [documentUrl, setDocumentUrl] = useState<string | null>(null);
|
||||
const [elapsed, setElapsed] = useState(0);
|
||||
|
||||
const startedAtRef = useRef<number>(0);
|
||||
const pollAbortRef = useRef<AbortController | null>(null);
|
||||
|
||||
// Reset state on open
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setPhase("loading-status");
|
||||
setStatus(null);
|
||||
setError(null);
|
||||
setOrderId(null);
|
||||
setDocumentUrl(null);
|
||||
setElapsed(0);
|
||||
void loadStatus();
|
||||
return () => {
|
||||
pollAbortRef.current?.abort();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, cadastralRef, siruta]);
|
||||
|
||||
// Elapsed timer during processing
|
||||
useEffect(() => {
|
||||
if (phase !== "processing" && phase !== "placing") return;
|
||||
const id = setInterval(() => {
|
||||
if (startedAtRef.current) {
|
||||
setElapsed(Math.round((Date.now() - startedAtRef.current) / 1000));
|
||||
}
|
||||
}, 500);
|
||||
return () => clearInterval(id);
|
||||
}, [phase]);
|
||||
|
||||
const loadStatus = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/ancpi/session", { cache: "no-store" });
|
||||
const data = (await res.json()) as SessionStatus;
|
||||
setStatus(data);
|
||||
if (!data.connected) {
|
||||
setPhase("not-connected");
|
||||
return;
|
||||
}
|
||||
if (data.credits != null && data.credits < 1) {
|
||||
setPhase("no-credits");
|
||||
return;
|
||||
}
|
||||
setPhase("ready");
|
||||
} catch {
|
||||
setError("Nu pot verifica sesiunea ePay.");
|
||||
setPhase("error");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const placeOrder = useCallback(async () => {
|
||||
setPhase("placing");
|
||||
setError(null);
|
||||
startedAtRef.current = Date.now();
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/ancpi/order", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
parcels: [
|
||||
{
|
||||
nrCadastral: cadastralRef,
|
||||
nrCF: null,
|
||||
siruta,
|
||||
judetIndex: 0,
|
||||
judetName: judetName ?? "",
|
||||
uatId: 0,
|
||||
uatName: uatName ?? "",
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
const data = (await res.json()) as {
|
||||
orders?: Array<{ id: string; nrCadastral: string; status: string }>;
|
||||
error?: string;
|
||||
};
|
||||
if (!res.ok || data.error) {
|
||||
setError(data.error ?? "Eroare la plasarea comenzii");
|
||||
setPhase("error");
|
||||
return;
|
||||
}
|
||||
const placed = (data.orders ?? [])[0];
|
||||
if (!placed) {
|
||||
setError("Răspuns invalid de la server");
|
||||
setPhase("error");
|
||||
return;
|
||||
}
|
||||
setOrderId(placed.id);
|
||||
setPhase("processing");
|
||||
void pollUntilDone(placed.id);
|
||||
} catch {
|
||||
setError("Eroare de rețea");
|
||||
setPhase("error");
|
||||
}
|
||||
}, [cadastralRef, siruta, judetName, uatName]);
|
||||
|
||||
const pollUntilDone = useCallback(
|
||||
async (id: string) => {
|
||||
pollAbortRef.current?.abort();
|
||||
const ctrl = new AbortController();
|
||||
pollAbortRef.current = ctrl;
|
||||
|
||||
const startedAt = Date.now();
|
||||
while (!ctrl.signal.aborted) {
|
||||
if (Date.now() - startedAt > POLL_TIMEOUT_MS) {
|
||||
setError(
|
||||
"Procesarea durează mai mult decât ne-am așteptat. Comanda e plasată — verifică din nou peste câteva minute.",
|
||||
);
|
||||
setPhase("error");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/ancpi/orders?nrCadastral=${encodeURIComponent(cadastralRef)}&limit=5`,
|
||||
{ signal: ctrl.signal, cache: "no-store" },
|
||||
);
|
||||
if (res.ok) {
|
||||
const data = (await res.json()) as OrderListResponse;
|
||||
const row = (data.orders ?? []).find((o) => o.id === id);
|
||||
if (row) {
|
||||
const s = (row.status || "").toLowerCase();
|
||||
if (
|
||||
s === "completed" ||
|
||||
s === "done" ||
|
||||
row.minioPath ||
|
||||
row.documentName
|
||||
) {
|
||||
setDocumentUrl(`/api/ancpi/download?id=${row.id}`);
|
||||
setPhase("done");
|
||||
return;
|
||||
}
|
||||
if (s === "failed" || s === "error") {
|
||||
setError("Comanda a eșuat la ANCPI.");
|
||||
setPhase("error");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if (ctrl.signal.aborted) return;
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
||||
}
|
||||
},
|
||||
[cadastralRef],
|
||||
);
|
||||
|
||||
const goToParcelSync = useCallback(() => {
|
||||
const url = `/parcel-sync?tab=epay&cad=${encodeURIComponent(cadastralRef)}`;
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}, [cadastralRef]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4 backdrop-blur-sm animate-in fade-in duration-200"
|
||||
onClick={(e) => {
|
||||
// dismiss on backdrop click only when not mid-flight
|
||||
if (
|
||||
e.target === e.currentTarget &&
|
||||
phase !== "placing" &&
|
||||
phase !== "processing"
|
||||
) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="relative w-full max-w-sm rounded-lg border bg-background shadow-2xl animate-in zoom-in-95 fade-in slide-in-from-bottom-2 duration-200">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between gap-2 border-b px-4 py-3">
|
||||
<div className="min-w-0">
|
||||
<h2 className="flex items-center gap-1.5 text-sm font-semibold">
|
||||
<FileText className="h-4 w-4" />
|
||||
Comandă extras CF
|
||||
</h2>
|
||||
<p className="mt-0.5 truncate font-mono text-xs text-muted-foreground">
|
||||
{cadastralRef}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
disabled={phase === "placing" || phase === "processing"}
|
||||
aria-label="Închide"
|
||||
className="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-30"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="px-4 py-4">
|
||||
{/* Loading session status */}
|
||||
{phase === "loading-status" && (
|
||||
<div className="flex items-center gap-2 py-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span>Verificare credite ePay…</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Not connected */}
|
||||
{phase === "not-connected" && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-2 rounded-md border border-amber-500/30 bg-amber-50 p-3 text-sm dark:bg-amber-950/30">
|
||||
<Plug className="mt-0.5 h-4 w-4 shrink-0 text-amber-600" />
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium text-amber-900 dark:text-amber-200">
|
||||
ePay neconectat
|
||||
</p>
|
||||
<p className="text-xs text-amber-800/80 dark:text-amber-200/80">
|
||||
Conectează-te în secțiunea Parcel Sync → ePay pentru
|
||||
a putea comanda extrase CF.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={goToParcelSync}
|
||||
className="inline-flex w-full items-center justify-center gap-1.5 rounded-md border bg-primary px-3 py-2 text-sm font-medium text-primary-foreground hover:opacity-90"
|
||||
>
|
||||
Conectează ePay
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No credits */}
|
||||
{phase === "no-credits" && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/10 p-3 text-sm">
|
||||
<Coins className="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium text-destructive">
|
||||
Credite insuficiente
|
||||
</p>
|
||||
<p className="text-xs text-destructive/80">
|
||||
Ai 0 credite. Reîncarcă-ți contul ePay ANCPI înainte
|
||||
de a comanda.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Ready — confirmation */}
|
||||
{phase === "ready" && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm">
|
||||
Ești pe cale să comanzi un extras de Carte Funciară pentru
|
||||
această parcelă.
|
||||
</p>
|
||||
<div className="flex items-center justify-between rounded-md border bg-muted/40 px-3 py-2.5 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Coins className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Cost</span>
|
||||
</div>
|
||||
<span className="font-mono font-semibold">1 credit</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-md border bg-muted/40 px-3 py-2.5 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Coins className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Sold disponibil</span>
|
||||
</div>
|
||||
<span className="font-mono font-semibold tabular-nums">
|
||||
{status?.credits ?? "—"} credite
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-md border border-emerald-500/30 bg-emerald-50 px-3 py-2.5 text-sm dark:bg-emerald-950/30">
|
||||
<span className="text-muted-foreground">După comandă</span>
|
||||
<span className="font-mono font-semibold tabular-nums text-emerald-700 dark:text-emerald-300">
|
||||
{status?.credits != null
|
||||
? `${status.credits - 1} credite`
|
||||
: "—"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Placing or Processing */}
|
||||
{(phase === "placing" || phase === "processing") && (
|
||||
<div className="space-y-3">
|
||||
<StepRow
|
||||
done={phase !== "placing"}
|
||||
active={phase === "placing"}
|
||||
label="Plasare comandă ANCPI"
|
||||
/>
|
||||
<StepRow
|
||||
done={false}
|
||||
active={phase === "processing"}
|
||||
pending={phase === "placing"}
|
||||
label={
|
||||
phase === "processing"
|
||||
? `Procesare la ANCPI… (${elapsed}s)`
|
||||
: "Procesare la ANCPI"
|
||||
}
|
||||
/>
|
||||
<StepRow
|
||||
done={false}
|
||||
active={false}
|
||||
pending
|
||||
label="Document gata"
|
||||
/>
|
||||
{phase === "processing" && (
|
||||
<p className="pt-1 text-[11px] italic text-muted-foreground">
|
||||
Poate dura până la 60 de secunde. Poți închide fereastra —
|
||||
comanda continuă în fundal.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Done */}
|
||||
{phase === "done" && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-col items-center gap-2 py-2 text-center animate-in zoom-in-95 duration-300">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-emerald-100 dark:bg-emerald-950">
|
||||
<Check className="h-7 w-7 text-emerald-600 animate-in zoom-in-50 duration-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Extras CF gata!</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Documentul a fost generat și descărcat în spațiul tău.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{documentUrl && (
|
||||
<a
|
||||
href={documentUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex w-full items-center justify-center gap-1.5 rounded-md border bg-primary px-3 py-2 text-sm font-medium text-primary-foreground hover:opacity-90"
|
||||
>
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
Descarcă PDF
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{phase === "error" && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-col items-center gap-2 py-2 text-center animate-in zoom-in-95 duration-300">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-destructive/10">
|
||||
<AlertCircle className="h-7 w-7 text-destructive" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Comanda nu a putut fi plasată</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{error ?? "Eroare necunoscută"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end gap-2 border-t bg-muted/30 px-4 py-3">
|
||||
{phase === "ready" && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="rounded-md px-3 py-1.5 text-sm font-medium hover:bg-muted"
|
||||
>
|
||||
Anulează
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={placeOrder}
|
||||
className="inline-flex items-center gap-1 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90"
|
||||
>
|
||||
Confirmă · 1 credit
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{(phase === "loading-status" || phase === "no-credits") && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="rounded-md px-3 py-1.5 text-sm font-medium hover:bg-muted"
|
||||
>
|
||||
Închide
|
||||
</button>
|
||||
)}
|
||||
{phase === "not-connected" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="rounded-md px-3 py-1.5 text-sm font-medium hover:bg-muted"
|
||||
>
|
||||
Renunță
|
||||
</button>
|
||||
)}
|
||||
{(phase === "placing" || phase === "processing") && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="rounded-md px-3 py-1.5 text-sm font-medium hover:bg-muted"
|
||||
title="Comanda continuă în fundal"
|
||||
>
|
||||
Închide fereastra
|
||||
</button>
|
||||
)}
|
||||
{phase === "done" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90"
|
||||
>
|
||||
Gata
|
||||
</button>
|
||||
)}
|
||||
{phase === "error" && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="rounded-md px-3 py-1.5 text-sm font-medium hover:bg-muted"
|
||||
>
|
||||
Închide
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void loadStatus()}
|
||||
className="inline-flex items-center gap-1 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90"
|
||||
>
|
||||
Reîncearcă
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StepRow({
|
||||
done,
|
||||
active,
|
||||
pending,
|
||||
label,
|
||||
}: {
|
||||
done: boolean;
|
||||
active: boolean;
|
||||
pending?: boolean;
|
||||
label: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-md border px-3 py-2 text-sm transition-colors",
|
||||
active && "border-primary/30 bg-primary/5",
|
||||
done && "border-emerald-500/30 bg-emerald-50 dark:bg-emerald-950/30",
|
||||
pending && !active && "opacity-50",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-5 w-5 shrink-0 items-center justify-center rounded-full",
|
||||
active && "bg-primary text-primary-foreground",
|
||||
done && "bg-emerald-500 text-white",
|
||||
pending && !active && "bg-muted text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{done ? (
|
||||
<Check className="h-3 w-3 animate-in zoom-in-50 duration-300" />
|
||||
) : active ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-current" />
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
done && "text-emerald-700 dark:text-emerald-300",
|
||||
active && "font-medium",
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Factory, Warehouse,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import { CfOrderModal } from "./cf-order-modal";
|
||||
|
||||
const AUTH_RETRY_KEY = "gis_panel_auth_retry";
|
||||
|
||||
@@ -403,6 +404,7 @@ export function FeatureInfoPanel({ feature, onClose, onSelectFeature, basic = fa
|
||||
const [buildings, setBuildings] = useState<BuildingItem[] | null>(null);
|
||||
const [condoOwners, setCondoOwners] = useState<CondoOwner[] | null>(null);
|
||||
const [condoLoading, setCondoLoading] = useState(false);
|
||||
const [cfModalOpen, setCfModalOpen] = useState(false);
|
||||
|
||||
const authRetriedRef = useRef<boolean>(
|
||||
typeof window !== "undefined" &&
|
||||
@@ -1215,18 +1217,23 @@ export function FeatureInfoPanel({ feature, onClose, onSelectFeature, basic = fa
|
||||
<div className="flex flex-wrap gap-1 border-t bg-muted/30 p-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const cad = encodeURIComponent(feature.cadastralRef);
|
||||
const url = `/parcel-sync?tab=epay&cad=${cad}`;
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}}
|
||||
className="inline-flex items-center gap-1 rounded bg-background px-2 py-1 text-[11px] font-medium hover:bg-muted"
|
||||
title="Deschide ePay în parcel-sync (cont propriu)"
|
||||
onClick={() => setCfModalOpen(true)}
|
||||
disabled={!feature.cadastralRef || !feature.siruta}
|
||||
className="inline-flex items-center gap-1 rounded bg-background px-2 py-1 text-[11px] font-medium hover:bg-muted disabled:opacity-50"
|
||||
title="Comandă extras Carte Funciară (1 credit ePay)"
|
||||
>
|
||||
<FileText className="h-3 w-3" />
|
||||
Comandă CF
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* CF order modal — confirmation + animated multi-step progress */}
|
||||
<CfOrderModal
|
||||
open={cfModalOpen}
|
||||
cadastralRef={feature.cadastralRef}
|
||||
siruta={feature.siruta}
|
||||
onClose={() => setCfModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user