feat(ancpi): complete ePay UI + dedup protection

UI Components (Phase 4):
- epay-connect.tsx: connection widget with credit badge, auto-connect
- epay-order-button.tsx: per-parcel "Extras CF" button with status
- epay-tab.tsx: full "Extrase CF" tab with orders table, filters,
  download/refresh actions, new order form
- Minimal changes to parcel-sync-module.tsx: 5th tab + button on
  search results + ePay connect widget

Dedup Protection:
- epay-queue.ts: batch-level dedup (60s window, canonical key from
  sorted cadastral numbers)
- order/route.ts: request nonce idempotency (60s cache)
- test/route.ts: refresh protection (30s cache)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-23 04:19:19 +02:00
parent fcc6f8cc20
commit c9ecd284c7
7 changed files with 1221 additions and 26 deletions
@@ -55,8 +55,11 @@ import {
} from "../services/eterra-layers";
import type { ParcelDetail } from "@/app/api/eterra/search/route";
import type { OwnerSearchResult } from "@/app/api/eterra/search-owner/route";
import { User } from "lucide-react";
import { User, FileText } from "lucide-react";
import { UatDashboard } from "./uat-dashboard";
import { EpayConnect } from "./epay-connect";
import { EpayOrderButton } from "./epay-order-button";
import { EpayTab } from "./epay-tab";
/* ------------------------------------------------------------------ */
/* Types */
@@ -1660,6 +1663,12 @@ export function ParcelSyncModule() {
const sirutaValid = siruta.length > 0 && /^\d+$/.test(siruta);
// Resolve selected UAT entry for ePay order context
const selectedUat = useMemo(
() => uatData.find((u) => u.siruta === siruta),
[uatData, siruta],
);
const progressPct =
exportProgress?.total && exportProgress.total > 0
? Math.round((exportProgress.downloaded / exportProgress.total) * 100)
@@ -1787,13 +1796,16 @@ export function ParcelSyncModule() {
)}
</div>
{/* Connection pill */}
<ConnectionPill
session={session}
connecting={connecting}
connectionError={connectionError}
onDisconnect={handleDisconnect}
/>
{/* Connection pills */}
<div className="flex items-center gap-2">
<EpayConnect />
<ConnectionPill
session={session}
connecting={connecting}
connectionError={connectionError}
onDisconnect={handleDisconnect}
/>
</div>
</div>
{/* Tab bar */}
@@ -1814,6 +1826,10 @@ export function ParcelSyncModule() {
<Database className="h-4 w-4" />
Baza de Date
</TabsTrigger>
<TabsTrigger value="extracts" className="gap-1.5">
<FileText className="h-4 w-4" />
Extrase CF
</TabsTrigger>
</TabsList>
</div>
@@ -2082,6 +2098,14 @@ export function ParcelSyncModule() {
>
<ClipboardCopy className="h-3.5 w-3.5" />
</Button>
{p.immovablePk && sirutaValid && (
<EpayOrderButton
nrCadastral={p.nrCad}
siruta={siruta}
judetName={selectedUat?.county ?? ""}
uatName={selectedUat?.name ?? ""}
/>
)}
</div>
</div>
@@ -2332,6 +2356,14 @@ export function ParcelSyncModule() {
>
<ClipboardCopy className="h-3.5 w-3.5" />
</Button>
{r.immovablePk && sirutaValid && (
<EpayOrderButton
nrCadastral={r.nrCad}
siruta={siruta}
judetName={selectedUat?.county ?? ""}
uatName={selectedUat?.name ?? ""}
/>
)}
</div>
</div>
@@ -4133,6 +4165,13 @@ export function ParcelSyncModule() {
</>
)}
</TabsContent>
{/* ═══════════════════════════════════════════════════════ */}
{/* Tab 5: Extrase CF */}
{/* ═══════════════════════════════════════════════════════ */}
<TabsContent value="extracts" className="space-y-4">
<EpayTab />
</TabsContent>
</Tabs>
);
}