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:
Claude VM
2026-05-20 07:53:15 +03:00
parent 8f86bab337
commit 5e4618b309
2 changed files with 586 additions and 7 deletions
@@ -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>
);
}