feat(ancpi): complete ePay UI redesign + ZIP download + smart batch ordering

UI Redesign:
- ePay auto-connect when UAT is selected (no manual button)
- Credit badge with tooltip ("N credite ePay disponibile")
- Search result cards show CF status: Valid (green), Expirat (orange),
  Lipsă (gray), Se proceseaza (yellow pulse)
- Action buttons on each card: download/update/order CF extract
- "Lista mea" numbered rows + CF Status column + smart batch button
  "Scoate Extrase CF": skips valid, re-orders expired, orders new
- "Descarca Extrase CF" button → ZIP archive with numbered files
- Extrase CF tab simplified: clean table, filters (Toate/Valabile/
  Expirate/In procesare), search, download-all ZIP

Backend:
- GET /api/ancpi/download-zip?ids=... → JSZip streaming
- GET /api/ancpi/orders: multi-cadastral status check with statusMap
  (valid/expired/none/processing) + latestById

Data:
- Simulated expired extract for 328611 (Cluj-Napoca, expired 2026-03-17)
- Cleaned old error records from DB

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-23 09:13:51 +02:00
parent b7302d274a
commit a59d9bc923
5 changed files with 955 additions and 337 deletions
+85 -3
View File
@@ -7,18 +7,38 @@ export const dynamic = "force-dynamic";
/**
* GET /api/ancpi/orders — list all CF extract orders.
*
* Query params: ?nrCadastral=&status=&limit=50&offset=0
* Query params:
* ?nrCadastral=123 — single cadastral number
* ?nrCadastral=123,456 — comma-separated for batch status check
* ?status=completed — filter by status
* ?limit=50&offset=0 — pagination
*
* When nrCadastral contains commas, returns an extra `statusMap` field:
* { orders, total, statusMap: { "123": "valid", "456": "expired", "789": "none" } }
* - "valid" = completed + expiresAt > now
* - "expired" = completed + expiresAt <= now
* - "none" = no completed record
*/
export async function GET(req: Request) {
try {
const url = new URL(req.url);
const nrCadastral = url.searchParams.get("nrCadastral") || undefined;
const nrCadastralParam = url.searchParams.get("nrCadastral") || undefined;
const status = url.searchParams.get("status") || undefined;
const limit = Math.min(parseInt(url.searchParams.get("limit") ?? "50"), 200);
const offset = parseInt(url.searchParams.get("offset") ?? "0");
// Check if multi-cadastral query
const cadastralNumbers = nrCadastralParam
? nrCadastralParam.split(",").map((s) => s.trim()).filter(Boolean)
: [];
const isMulti = cadastralNumbers.length > 1;
const where: Record<string, unknown> = {};
if (nrCadastral) where.nrCadastral = nrCadastral;
if (cadastralNumbers.length === 1) {
where.nrCadastral = cadastralNumbers[0];
} else if (isMulti) {
where.nrCadastral = { in: cadastralNumbers };
}
if (status) where.status = status;
const [orders, total] = await Promise.all([
@@ -31,6 +51,68 @@ export async function GET(req: Request) {
prisma.cfExtract.count({ where }),
]);
// Build statusMap for multi-cadastral queries (or single if requested)
if (cadastralNumbers.length > 0) {
const now = new Date();
// For status map, we need completed records for each cadastral number
const completedRecords = await prisma.cfExtract.findMany({
where: {
nrCadastral: { in: cadastralNumbers },
status: "completed",
},
orderBy: { createdAt: "desc" },
select: {
id: true,
nrCadastral: true,
expiresAt: true,
completedAt: true,
minioPath: true,
},
});
const statusMap: Record<string, string> = {};
const latestById: Record<string, typeof completedRecords[number]> = {};
// Find latest completed record per cadastral number
for (const rec of completedRecords) {
const existing = latestById[rec.nrCadastral];
if (!existing) {
latestById[rec.nrCadastral] = rec;
}
}
for (const nr of cadastralNumbers) {
const rec = latestById[nr];
if (!rec) {
statusMap[nr] = "none";
} else if (rec.expiresAt && rec.expiresAt <= now) {
statusMap[nr] = "expired";
} else {
statusMap[nr] = "valid";
}
}
// Also check for active (in-progress) orders
const activeRecords = await prisma.cfExtract.findMany({
where: {
nrCadastral: { in: cadastralNumbers },
status: {
in: ["pending", "queued", "cart", "searching", "ordering", "polling", "downloading"],
},
},
select: { nrCadastral: true },
});
for (const rec of activeRecords) {
// If there's an active order, mark as "processing" (takes priority over "none")
if (statusMap[rec.nrCadastral] === "none") {
statusMap[rec.nrCadastral] = "processing";
}
}
return NextResponse.json({ orders, total, statusMap, latestById });
}
return NextResponse.json({ orders, total });
} catch (error) {
const message = error instanceof Error ? error.message : "Eroare server";