b87c908415
- ePay + eTerra pills: removed animate-ping, now show static green dot when connected (no more spinning appearance) - Legend moved to top-left, hides when FeatureInfoPanel is open (no more overlap) - Boundary mismatch parcels now show cadastral numbers as labels (orange for foreign, purple for edge parcels) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
251 lines
7.8 KiB
TypeScript
251 lines
7.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useCallback, useRef } from "react";
|
|
import {
|
|
Loader2,
|
|
LogOut,
|
|
CreditCard,
|
|
} from "lucide-react";
|
|
import { Button } from "@/shared/components/ui/button";
|
|
import { Badge } from "@/shared/components/ui/badge";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from "@/shared/components/ui/tooltip";
|
|
import { cn } from "@/shared/lib/utils";
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Types */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
export type EpaySessionStatus = {
|
|
connected: boolean;
|
|
username?: string;
|
|
connectedAt?: string;
|
|
credits?: number;
|
|
creditsCheckedAt?: string;
|
|
};
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Component */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
export function EpayConnect({
|
|
onStatusChange,
|
|
triggerConnect,
|
|
}: {
|
|
onStatusChange?: (status: EpaySessionStatus) => void;
|
|
/** When set to true, triggers auto-connect (e.g. when user types UAT) */
|
|
triggerConnect?: boolean;
|
|
}) {
|
|
const [status, setStatus] = useState<EpaySessionStatus>({ connected: false });
|
|
const [connecting, setConnecting] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
const cbRef = useRef(onStatusChange);
|
|
cbRef.current = onStatusChange;
|
|
const autoConnectAttempted = useRef(false);
|
|
const autoConnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
const fetchStatus = useCallback(async () => {
|
|
try {
|
|
const res = await fetch("/api/ancpi/session");
|
|
const data = (await res.json()) as EpaySessionStatus;
|
|
setStatus(data);
|
|
cbRef.current?.(data);
|
|
if (data.connected) setError("");
|
|
return data;
|
|
} catch {
|
|
/* silent */
|
|
return null;
|
|
}
|
|
}, []);
|
|
|
|
// Poll every 30s — detect disconnection and allow re-connect
|
|
useEffect(() => {
|
|
void fetchStatus();
|
|
pollRef.current = setInterval(() => {
|
|
void fetchStatus().then((data) => {
|
|
if (data && !data.connected && autoConnectAttempted.current) {
|
|
autoConnectAttempted.current = false;
|
|
}
|
|
});
|
|
}, 30_000);
|
|
return () => {
|
|
if (pollRef.current) clearInterval(pollRef.current);
|
|
};
|
|
}, [fetchStatus]);
|
|
|
|
const connect = useCallback(async () => {
|
|
if (connecting || status.connected) return;
|
|
setConnecting(true);
|
|
setError("");
|
|
try {
|
|
const res = await fetch("/api/ancpi/session", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({}),
|
|
});
|
|
const data = (await res.json()) as { success?: boolean; credits?: number; error?: string };
|
|
if (!res.ok || data.error) {
|
|
setError(data.error ?? "Eroare conectare ePay");
|
|
} else {
|
|
await fetchStatus();
|
|
}
|
|
} catch {
|
|
setError("Eroare retea");
|
|
} finally {
|
|
setConnecting(false);
|
|
}
|
|
}, [connecting, status.connected, fetchStatus]);
|
|
|
|
// Auto-connect when triggerConnect becomes true, with retry on failure
|
|
useEffect(() => {
|
|
if (!triggerConnect || status.connected || connecting || autoConnectAttempted.current) return;
|
|
autoConnectAttempted.current = true;
|
|
|
|
let cancelled = false;
|
|
const maxRetries = 2;
|
|
|
|
const attemptConnect = async (attempt: number) => {
|
|
if (cancelled) return;
|
|
|
|
// On first attempt, check session to avoid unnecessary connect
|
|
if (attempt === 0) {
|
|
const current = await fetchStatus();
|
|
if (cancelled) return;
|
|
if (current?.connected) return;
|
|
}
|
|
|
|
setConnecting(true);
|
|
setError("");
|
|
let shouldRetry = false;
|
|
try {
|
|
const res = await fetch("/api/ancpi/session", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({}),
|
|
});
|
|
const data = (await res.json()) as { success?: boolean; credits?: number; error?: string };
|
|
if (cancelled) return;
|
|
|
|
if (!res.ok || data.error) {
|
|
setError(data.error ?? "Eroare conectare ePay");
|
|
shouldRetry = attempt < maxRetries;
|
|
} else {
|
|
await fetchStatus();
|
|
}
|
|
} catch {
|
|
if (cancelled) return;
|
|
setError("Eroare retea");
|
|
shouldRetry = attempt < maxRetries;
|
|
}
|
|
|
|
if (cancelled) return;
|
|
|
|
if (shouldRetry) {
|
|
// Keep connecting state true during retry wait
|
|
autoConnectTimerRef.current = setTimeout(() => {
|
|
void attemptConnect(attempt + 1);
|
|
}, 3000);
|
|
} else {
|
|
setConnecting(false);
|
|
}
|
|
};
|
|
|
|
void attemptConnect(0);
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
if (autoConnectTimerRef.current) {
|
|
clearTimeout(autoConnectTimerRef.current);
|
|
autoConnectTimerRef.current = null;
|
|
}
|
|
};
|
|
}, [triggerConnect, status.connected, connecting, fetchStatus]);
|
|
|
|
const disconnect = async () => {
|
|
try {
|
|
await fetch("/api/ancpi/session", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ action: "disconnect" }),
|
|
});
|
|
autoConnectAttempted.current = false;
|
|
if (autoConnectTimerRef.current) {
|
|
clearTimeout(autoConnectTimerRef.current);
|
|
autoConnectTimerRef.current = null;
|
|
}
|
|
await fetchStatus();
|
|
} catch {
|
|
/* silent */
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex items-center gap-1">
|
|
{/* Status pill */}
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<div
|
|
className={cn(
|
|
"flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium",
|
|
status.connected
|
|
? "border-emerald-200 bg-emerald-50/80 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-400"
|
|
: error
|
|
? "border-rose-200 bg-rose-50/80 text-rose-600 dark:border-rose-800 dark:bg-rose-950/40 dark:text-rose-400"
|
|
: connecting
|
|
? "border-muted-foreground/20 bg-muted/50 text-muted-foreground"
|
|
: "border-muted-foreground/20 bg-muted/50 text-muted-foreground",
|
|
)}
|
|
>
|
|
{connecting ? (
|
|
<Loader2 className="h-3 w-3 animate-spin" />
|
|
) : status.connected ? (
|
|
<span className="inline-flex h-2 w-2 rounded-full bg-emerald-500" />
|
|
) : null}
|
|
|
|
<span className="hidden sm:inline">ePay</span>
|
|
|
|
{status.connected && status.credits != null && (
|
|
<Badge
|
|
variant="secondary"
|
|
className="ml-0.5 h-4 px-1.5 text-[10px] font-semibold"
|
|
>
|
|
<CreditCard className="mr-0.5 h-2.5 w-2.5" />
|
|
{status.credits}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom" className="text-xs">
|
|
{status.connected
|
|
? `${status.credits ?? "?"} credite ePay disponibile`
|
|
: error
|
|
? error
|
|
: connecting
|
|
? "Se conecteaza..."
|
|
: "Se conecteaza automat la selectia UAT"}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
|
|
{/* Logout button — only when connected */}
|
|
{status.connected && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 px-1.5 text-[10px]"
|
|
onClick={() => void disconnect()}
|
|
title="Deconectare ePay"
|
|
>
|
|
<LogOut className="h-3 w-3" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|