a826f45b24
Re-download: all 7 orders re-downloaded using documentsByCadastral for correct CF→document matching. No more hardcoded order→parcel map. Tooltips on all CF extract UI elements: - No extract: "Comandă extras CF (1 credit)" - Valid: "Valid până la DD.MM.YYYY" + "Descarcă extras CF" - Expired: "Expirat pe DD.MM.YYYY" + "Comandă extras CF nou (1 credit)" - Processing: "Comanda în curs de procesare" Animations: Loader2 spinner while ordering, transition to green check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
216 lines
6.2 KiB
TypeScript
216 lines
6.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useCallback, useRef } from "react";
|
|
import { FileText, Loader2, Check, RefreshCw } from "lucide-react";
|
|
import { Button } from "@/shared/components/ui/button";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from "@/shared/components/ui/tooltip";
|
|
import { cn } from "@/shared/lib/utils";
|
|
import type { EpaySessionStatus } from "./epay-connect";
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Props */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
type Props = {
|
|
nrCadastral: string;
|
|
siruta: string;
|
|
judetName: string;
|
|
uatName: string;
|
|
/** Custom label for the button (e.g. "Actualizeaza" for expired extracts) */
|
|
label?: string;
|
|
/** Custom tooltip text */
|
|
tooltipText?: string;
|
|
};
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Component */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
export function EpayOrderButton({
|
|
nrCadastral,
|
|
siruta,
|
|
judetName,
|
|
uatName,
|
|
label,
|
|
tooltipText,
|
|
}: Props) {
|
|
const [ordering, setOrdering] = useState(false);
|
|
const [ordered, setOrdered] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const [epayStatus, setEpayStatus] = useState<EpaySessionStatus>({
|
|
connected: false,
|
|
});
|
|
const mountedRef = useRef(true);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
mountedRef.current = false;
|
|
};
|
|
}, []);
|
|
|
|
// Check ePay status and whether an extract exists
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
|
|
const check = async () => {
|
|
try {
|
|
// Check session
|
|
const sRes = await fetch("/api/ancpi/session");
|
|
const sData = (await sRes.json()) as EpaySessionStatus;
|
|
if (!cancelled) setEpayStatus(sData);
|
|
|
|
// Check if a completed extract already exists
|
|
const oRes = await fetch(
|
|
`/api/ancpi/orders?nrCadastral=${encodeURIComponent(nrCadastral)}&status=completed&limit=1`,
|
|
);
|
|
const oData = (await oRes.json()) as { orders?: unknown[]; total?: number };
|
|
if (!cancelled && oData.total && oData.total > 0) {
|
|
setOrdered(true);
|
|
}
|
|
} catch {
|
|
/* silent */
|
|
}
|
|
};
|
|
void check();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [nrCadastral]);
|
|
|
|
const handleOrder = useCallback(async () => {
|
|
setOrdering(true);
|
|
setError("");
|
|
try {
|
|
const res = await fetch("/api/ancpi/order", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
parcels: [
|
|
{
|
|
nrCadastral,
|
|
siruta,
|
|
judetIndex: 0, // server resolves from SIRUTA
|
|
judetName,
|
|
uatId: 0, // server resolves from SIRUTA
|
|
uatName,
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
const data = (await res.json()) as { orders?: unknown[]; error?: string };
|
|
if (!res.ok || data.error) {
|
|
if (mountedRef.current) setError(data.error ?? "Eroare comanda");
|
|
} else {
|
|
if (mountedRef.current) setOrdered(true);
|
|
}
|
|
} catch {
|
|
if (mountedRef.current) setError("Eroare retea");
|
|
} finally {
|
|
if (mountedRef.current) setOrdering(false);
|
|
}
|
|
}, [nrCadastral, siruta, judetName, uatName]);
|
|
|
|
const disabled =
|
|
!epayStatus.connected ||
|
|
(epayStatus.credits != null && epayStatus.credits < 1) ||
|
|
ordering;
|
|
|
|
const isRenew = !!label; // "Actualizeaza" mode for expired extracts
|
|
const Icon = isRenew ? RefreshCw : FileText;
|
|
|
|
const resolveTooltip = (): string => {
|
|
if (error) return error;
|
|
if (ordering) return "Se comanda...";
|
|
if (ordered) return "Extras CF valid";
|
|
if (!epayStatus.connected) return "ePay neconectat";
|
|
if (epayStatus.credits != null && epayStatus.credits < 1) return "Credite insuficiente";
|
|
return tooltipText ?? "Comanda extras CF (1 credit)";
|
|
};
|
|
|
|
if (ordered) {
|
|
return (
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
className="h-7 gap-1 px-1.5 text-emerald-600 dark:text-emerald-400"
|
|
disabled
|
|
>
|
|
<Check className="h-3.5 w-3.5" />
|
|
<span className="text-[10px]">Extras CF valid</span>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Extras CF comandat cu succes</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
);
|
|
}
|
|
|
|
if (label) {
|
|
// Render as a compact text button with label (for "Actualizeaza" etc.)
|
|
return (
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
className={cn(
|
|
"h-7 gap-1 px-1.5 text-[10px]",
|
|
error && "text-destructive",
|
|
ordering && "text-muted-foreground",
|
|
)}
|
|
disabled={disabled}
|
|
onClick={() => void handleOrder()}
|
|
>
|
|
{ordering ? (
|
|
<Loader2 className="h-3 w-3 animate-spin" />
|
|
) : (
|
|
<Icon className="h-3 w-3" />
|
|
)}
|
|
{ordering ? "Se comanda..." : label}
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{resolveTooltip()}</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
);
|
|
}
|
|
|
|
// Default: icon-only button
|
|
return (
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
className={cn(
|
|
"h-7 w-7 p-0",
|
|
error && "text-destructive",
|
|
ordering && "text-muted-foreground",
|
|
)}
|
|
disabled={disabled}
|
|
onClick={() => void handleOrder()}
|
|
>
|
|
{ordering ? (
|
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
) : (
|
|
<FileText className="h-3.5 w-3.5" />
|
|
)}
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{resolveTooltip()}</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
);
|
|
}
|