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,
|
Factory, Warehouse,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { cn } from "@/shared/lib/utils";
|
import { cn } from "@/shared/lib/utils";
|
||||||
|
import { CfOrderModal } from "./cf-order-modal";
|
||||||
|
|
||||||
const AUTH_RETRY_KEY = "gis_panel_auth_retry";
|
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 [buildings, setBuildings] = useState<BuildingItem[] | null>(null);
|
||||||
const [condoOwners, setCondoOwners] = useState<CondoOwner[] | null>(null);
|
const [condoOwners, setCondoOwners] = useState<CondoOwner[] | null>(null);
|
||||||
const [condoLoading, setCondoLoading] = useState(false);
|
const [condoLoading, setCondoLoading] = useState(false);
|
||||||
|
const [cfModalOpen, setCfModalOpen] = useState(false);
|
||||||
|
|
||||||
const authRetriedRef = useRef<boolean>(
|
const authRetriedRef = useRef<boolean>(
|
||||||
typeof window !== "undefined" &&
|
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">
|
<div className="flex flex-wrap gap-1 border-t bg-muted/30 p-1.5">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => setCfModalOpen(true)}
|
||||||
const cad = encodeURIComponent(feature.cadastralRef);
|
disabled={!feature.cadastralRef || !feature.siruta}
|
||||||
const url = `/parcel-sync?tab=epay&cad=${cad}`;
|
className="inline-flex items-center gap-1 rounded bg-background px-2 py-1 text-[11px] font-medium hover:bg-muted disabled:opacity-50"
|
||||||
window.open(url, "_blank", "noopener,noreferrer");
|
title="Comandă extras Carte Funciară (1 credit ePay)"
|
||||||
}}
|
|
||||||
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)"
|
|
||||||
>
|
>
|
||||||
<FileText className="h-3 w-3" />
|
<FileText className="h-3 w-3" />
|
||||||
Comandă CF
|
Comandă CF
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* CF order modal — confirmation + animated multi-step progress */}
|
||||||
|
<CfOrderModal
|
||||||
|
open={cfModalOpen}
|
||||||
|
cadastralRef={feature.cadastralRef}
|
||||||
|
siruta={feature.siruta}
|
||||||
|
onClose={() => setCfModalOpen(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user