Files
ArchiTools/src/modules/parcel-sync/components/epay-order-button.tsx
T
AI Assistant a826f45b24 feat(ancpi): re-download with CF matching + tooltips + animations
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>
2026-03-23 09:38:23 +02:00

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>
);
}