fix(rgi): correct field mapping + configurable columns + download fix
Mapped all eTerra RGI fields correctly: - App: appNo, applicationPk, appDate, dueDate, deponent, requester, applicationObject, identifiers, statusName, resolutionName, hasSolution - Docs: docType, documentPk (fileId), documentTypeId (docId), fileExtension, digitallySigned, startDate Features: - Configurable columns: click "Coloane" to toggle 17 available columns - Table layout with proper column rendering - Click row to expand issued documents - Documents show type name, date, extension, digital signature badge - Download button with correct fileId/docId mapping - Filter: hasSolution===1 && dueDate > now (not string matching) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+392
-393
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import { Input } from "@/shared/components/ui/input";
|
||||
import { Label } from "@/shared/components/ui/label";
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
AlertTriangle,
|
||||
Settings2,
|
||||
Shield,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
|
||||
@@ -23,35 +25,64 @@ import { cn } from "@/shared/lib/utils";
|
||||
/* Types */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
type Application = Record<string, unknown>;
|
||||
type IssuedDoc = Record<string, unknown>;
|
||||
type App = {
|
||||
actorName: string;
|
||||
adminUnit: number;
|
||||
appDate: number;
|
||||
appNo: number;
|
||||
applicationObject: string;
|
||||
applicationPk: number;
|
||||
colorNumber: number;
|
||||
communicationType: string;
|
||||
deponent: string;
|
||||
dueDate: number;
|
||||
hasSolution: number;
|
||||
identifiers: string;
|
||||
initialAppNo: string;
|
||||
orgUnit: string;
|
||||
requester: string;
|
||||
resolutionName: string;
|
||||
stateCode: string;
|
||||
statusName: string;
|
||||
totalFee: number;
|
||||
uat: string;
|
||||
workspace: string;
|
||||
workspaceId: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
type IssuedDoc = {
|
||||
applicationId: number;
|
||||
docType: string;
|
||||
documentPk: number;
|
||||
documentTypeCode: string;
|
||||
documentTypeId: number;
|
||||
fileExtension: string;
|
||||
digitallySigned: number;
|
||||
startDate: number;
|
||||
lastUpdatedDtm: number;
|
||||
initialAppNo: string;
|
||||
workspaceId: number;
|
||||
identifierDetails: string | null;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Helpers */
|
||||
/* Column definitions */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function str(obj: Record<string, unknown>, ...keys: string[]): string {
|
||||
for (const k of keys) {
|
||||
const v = obj[k];
|
||||
if (typeof v === "string" && v.length > 0) return v;
|
||||
if (typeof v === "number") return String(v);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
type ColumnDef = {
|
||||
key: string;
|
||||
label: string;
|
||||
defaultVisible: boolean;
|
||||
render: (app: App) => string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function num(obj: Record<string, unknown>, ...keys: string[]): number | null {
|
||||
for (const k of keys) {
|
||||
const v = obj[k];
|
||||
if (typeof v === "number") return v;
|
||||
if (typeof v === "string" && /^\d+$/.test(v)) return parseInt(v, 10);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function fmtDate(raw: unknown): string {
|
||||
if (!raw) return "-";
|
||||
const d = typeof raw === "number" ? new Date(raw) : new Date(String(raw));
|
||||
if (isNaN(d.getTime())) return String(raw);
|
||||
function fmtTs(ts: number | null | undefined): string {
|
||||
if (!ts) return "-";
|
||||
const d = new Date(ts);
|
||||
if (isNaN(d.getTime())) return "-";
|
||||
return d.toLocaleDateString("ro-RO", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
@@ -59,194 +90,213 @@ function fmtDate(raw: unknown): string {
|
||||
});
|
||||
}
|
||||
|
||||
function isFutureDate(raw: unknown): boolean {
|
||||
if (!raw) return false;
|
||||
const d = typeof raw === "number" ? new Date(raw) : new Date(String(raw));
|
||||
return !isNaN(d.getTime()) && d > new Date();
|
||||
}
|
||||
|
||||
function isSolved(app: Application): boolean {
|
||||
const status = str(app, "state", "status", "stare", "applicationState")
|
||||
.toLowerCase();
|
||||
const stateName = str(
|
||||
app,
|
||||
"stateName",
|
||||
"statusName",
|
||||
"stateDescription",
|
||||
).toLowerCase();
|
||||
return (
|
||||
status.includes("solutionat") ||
|
||||
stateName.includes("solutionat") ||
|
||||
status.includes("finalizat") ||
|
||||
stateName.includes("finalizat") ||
|
||||
status === "closed" ||
|
||||
status === "resolved"
|
||||
);
|
||||
}
|
||||
const ALL_COLUMNS: ColumnDef[] = [
|
||||
{
|
||||
key: "appNo",
|
||||
label: "Nr. cerere",
|
||||
defaultVisible: true,
|
||||
render: (a) => String(a.appNo ?? "-"),
|
||||
className: "font-mono font-semibold",
|
||||
},
|
||||
{
|
||||
key: "initialAppNo",
|
||||
label: "Nr. initial",
|
||||
defaultVisible: false,
|
||||
render: (a) => a.initialAppNo || "-",
|
||||
className: "font-mono text-xs",
|
||||
},
|
||||
{
|
||||
key: "applicationObject",
|
||||
label: "Obiect",
|
||||
defaultVisible: true,
|
||||
render: (a) => a.applicationObject || "-",
|
||||
},
|
||||
{
|
||||
key: "identifiers",
|
||||
label: "Identificatori (IE/CF)",
|
||||
defaultVisible: true,
|
||||
render: (a) => a.identifiers || "-",
|
||||
className: "text-xs max-w-[300px] truncate",
|
||||
},
|
||||
{
|
||||
key: "deponent",
|
||||
label: "Deponent",
|
||||
defaultVisible: true,
|
||||
render: (a) => a.deponent || "-",
|
||||
},
|
||||
{
|
||||
key: "requester",
|
||||
label: "Solicitant",
|
||||
defaultVisible: false,
|
||||
render: (a) => a.requester || "-",
|
||||
},
|
||||
{
|
||||
key: "appDate",
|
||||
label: "Data depunere",
|
||||
defaultVisible: true,
|
||||
render: (a) => fmtTs(a.appDate),
|
||||
className: "tabular-nums",
|
||||
},
|
||||
{
|
||||
key: "dueDate",
|
||||
label: "Termen",
|
||||
defaultVisible: true,
|
||||
render: (a) => fmtTs(a.dueDate),
|
||||
className: "tabular-nums",
|
||||
},
|
||||
{
|
||||
key: "statusName",
|
||||
label: "Status",
|
||||
defaultVisible: true,
|
||||
render: (a) => a.statusName || a.stateCode || "-",
|
||||
},
|
||||
{
|
||||
key: "resolutionName",
|
||||
label: "Rezolutie",
|
||||
defaultVisible: true,
|
||||
render: (a) => a.resolutionName || "-",
|
||||
},
|
||||
{
|
||||
key: "hasSolution",
|
||||
label: "Solutionat",
|
||||
defaultVisible: false,
|
||||
render: (a) => (a.hasSolution === 1 ? "DA" : "NU"),
|
||||
},
|
||||
{
|
||||
key: "totalFee",
|
||||
label: "Taxa (lei)",
|
||||
defaultVisible: false,
|
||||
render: (a) => (a.totalFee != null ? String(a.totalFee) : "-"),
|
||||
className: "tabular-nums",
|
||||
},
|
||||
{
|
||||
key: "uat",
|
||||
label: "UAT",
|
||||
defaultVisible: false,
|
||||
render: (a) => a.uat || "-",
|
||||
},
|
||||
{
|
||||
key: "orgUnit",
|
||||
label: "OCPI",
|
||||
defaultVisible: false,
|
||||
render: (a) => a.orgUnit || "-",
|
||||
},
|
||||
{
|
||||
key: "communicationType",
|
||||
label: "Comunicare",
|
||||
defaultVisible: false,
|
||||
render: (a) => a.communicationType || "-",
|
||||
className: "text-xs",
|
||||
},
|
||||
{
|
||||
key: "actorName",
|
||||
label: "Actor curent",
|
||||
defaultVisible: false,
|
||||
render: (a) => a.actorName || "-",
|
||||
},
|
||||
{
|
||||
key: "applicationPk",
|
||||
label: "Application PK",
|
||||
defaultVisible: false,
|
||||
render: (a) => String(a.applicationPk ?? "-"),
|
||||
className: "font-mono text-xs",
|
||||
},
|
||||
];
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Issued Documents sub-component */
|
||||
/* Issued Documents panel */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function IssuedDocsPanel({
|
||||
applicationId,
|
||||
applicationPk,
|
||||
workspaceId,
|
||||
}: {
|
||||
applicationId: string;
|
||||
workspaceId: string;
|
||||
applicationPk: number;
|
||||
workspaceId: number;
|
||||
}) {
|
||||
const [docs, setDocs] = useState<IssuedDoc[] | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const [rawData, setRawData] = useState<unknown>(null);
|
||||
|
||||
const loadDocs = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
// Auto-load
|
||||
useState(() => {
|
||||
void (async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/eterra/rgi/issued-docs?applicationId=${encodeURIComponent(applicationId)}&workspaceId=${workspaceId}`,
|
||||
`/api/eterra/rgi/issued-docs?applicationId=${applicationPk}&workspaceId=${workspaceId}`,
|
||||
);
|
||||
const data = await res.json();
|
||||
setRawData(data);
|
||||
|
||||
// Try to extract docs array from various response shapes
|
||||
let items: IssuedDoc[] = [];
|
||||
if (Array.isArray(data)) {
|
||||
items = data;
|
||||
} else if (data?.content && Array.isArray(data.content)) {
|
||||
items = data.content;
|
||||
} else if (data?.data && Array.isArray(data.data)) {
|
||||
items = data.data;
|
||||
} else if (data?.list && Array.isArray(data.list)) {
|
||||
items = data.list;
|
||||
} else if (data?.results && Array.isArray(data.results)) {
|
||||
items = data.results;
|
||||
}
|
||||
const items: IssuedDoc[] = Array.isArray(data)
|
||||
? data
|
||||
: data?.content ?? data?.data ?? data?.list ?? [];
|
||||
setDocs(items);
|
||||
} catch {
|
||||
setError("Eroare la incarcarea documentelor");
|
||||
}
|
||||
setLoading(false);
|
||||
}, [applicationId, workspaceId]);
|
||||
|
||||
// Auto-load on mount
|
||||
useState(() => {
|
||||
void loadDocs();
|
||||
})();
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-3 text-sm text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Se incarca documentele eliberate...
|
||||
Se incarca documentele...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="py-3 text-sm text-destructive">
|
||||
{error}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="ml-2 h-6 text-xs"
|
||||
onClick={() => void loadDocs()}
|
||||
>
|
||||
Reincearca
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
return <p className="py-3 text-sm text-destructive">{error}</p>;
|
||||
}
|
||||
|
||||
if (!docs || docs.length === 0) {
|
||||
return (
|
||||
<div className="py-3 text-sm text-muted-foreground">
|
||||
Niciun document eliberat gasit.
|
||||
{rawData != null && (
|
||||
<details className="mt-2">
|
||||
<summary className="text-[10px] cursor-pointer opacity-50">
|
||||
Raspuns brut
|
||||
</summary>
|
||||
<pre className="text-[10px] mt-1 bg-muted/50 rounded p-2 overflow-auto max-h-40">
|
||||
{JSON.stringify(rawData, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
<p className="py-3 text-sm text-muted-foreground">
|
||||
Niciun document eliberat.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2 py-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
<div className="space-y-1.5 py-2">
|
||||
<p className="text-xs font-medium text-muted-foreground mb-2">
|
||||
{docs.length} document{docs.length > 1 ? "e" : ""} eliberat
|
||||
{docs.length > 1 ? "e" : ""}:
|
||||
</p>
|
||||
{docs.map((doc, i) => {
|
||||
const docName = str(
|
||||
doc,
|
||||
"documentName",
|
||||
"name",
|
||||
"fileName",
|
||||
"tipDocument",
|
||||
"documentType",
|
||||
"description",
|
||||
);
|
||||
const docId = str(
|
||||
doc,
|
||||
"issuedDocumentId",
|
||||
"documentId",
|
||||
"id",
|
||||
"pk",
|
||||
"docId",
|
||||
);
|
||||
const fileId = str(
|
||||
doc,
|
||||
"fileId",
|
||||
"documentFileId",
|
||||
"filePk",
|
||||
);
|
||||
const docDate = doc.createdDate ?? doc.dateCreated ?? doc.date ?? doc.issuedDate;
|
||||
const docStatus = str(doc, "status", "state", "documentStatus");
|
||||
|
||||
return (
|
||||
{docs.map((doc, i) => (
|
||||
<div
|
||||
key={docId || i}
|
||||
className="flex items-center justify-between gap-3 rounded-lg border px-3 py-2"
|
||||
key={doc.documentPk || i}
|
||||
className="flex items-center justify-between gap-3 rounded-lg border px-3 py-2 hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
||||
<FileText className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium truncate">
|
||||
{docName || `Document #${docId || i + 1}`}
|
||||
<p className="text-sm font-medium">
|
||||
{doc.docType || doc.documentTypeCode || "Document"}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 text-[11px] text-muted-foreground">
|
||||
{docDate != null && <span>{fmtDate(docDate)}</span>}
|
||||
{docId && (
|
||||
<span className="font-mono opacity-60">ID: {docId}</span>
|
||||
<span>{fmtTs(doc.startDate || doc.lastUpdatedDtm)}</span>
|
||||
<span className="font-mono opacity-60">
|
||||
.{(doc.fileExtension || "PDF").toLowerCase()}
|
||||
</span>
|
||||
{doc.digitallySigned === 1 && (
|
||||
<span className="inline-flex items-center gap-0.5 text-emerald-600 dark:text-emerald-400">
|
||||
<Shield className="h-3 w-3" />
|
||||
semnat
|
||||
</span>
|
||||
)}
|
||||
{docStatus && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[9px] h-4"
|
||||
>
|
||||
{docStatus}
|
||||
</Badge>
|
||||
{doc.identifierDetails && (
|
||||
<span className="truncate max-w-[200px]">
|
||||
{doc.identifierDetails}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{(docId || fileId) && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="shrink-0 gap-1"
|
||||
asChild
|
||||
>
|
||||
<Button size="sm" variant="outline" className="shrink-0 gap-1" asChild>
|
||||
<a
|
||||
href={`/api/eterra/rgi/download-doc?workspaceId=${workspaceId}&applicationId=${encodeURIComponent(applicationId)}${fileId ? `&fileId=${encodeURIComponent(fileId)}` : `&docId=${encodeURIComponent(docId)}`}`}
|
||||
href={`/api/eterra/rgi/download-doc?workspaceId=${doc.workspaceId || workspaceId}&applicationId=${doc.applicationId || applicationPk}&docId=${doc.documentTypeId}&fileId=${doc.documentPk}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@@ -254,160 +304,8 @@ function IssuedDocsPanel({
|
||||
Descarca
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Application row */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function ApplicationRow({
|
||||
app,
|
||||
workspaceId,
|
||||
}: {
|
||||
app: Application;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const appNo = str(
|
||||
app,
|
||||
"applicationNumber",
|
||||
"number",
|
||||
"nrCerere",
|
||||
"registrationNumber",
|
||||
"appNo",
|
||||
"applicationNo",
|
||||
);
|
||||
const appId = str(
|
||||
app,
|
||||
"applicationId",
|
||||
"id",
|
||||
"pk",
|
||||
"applicationPk",
|
||||
);
|
||||
const services = str(
|
||||
app,
|
||||
"servicesDescription",
|
||||
"services",
|
||||
"serviceName",
|
||||
"serviceType",
|
||||
"tipServicii",
|
||||
);
|
||||
const deponent = str(
|
||||
app,
|
||||
"deponent",
|
||||
"deponentName",
|
||||
"applicant",
|
||||
"applicantName",
|
||||
"solicitant",
|
||||
);
|
||||
const filingDate =
|
||||
app.filingDate ?? app.dataDepunere ?? app.createdDate ?? app.registrationDate;
|
||||
const dueDate = app.dueDate ?? app.termen ?? app.deadlineDate ?? app.deadline;
|
||||
const status = str(
|
||||
app,
|
||||
"stateName",
|
||||
"state",
|
||||
"status",
|
||||
"stare",
|
||||
"statusName",
|
||||
"applicationState",
|
||||
);
|
||||
const solved = isSolved(app);
|
||||
const future = isFutureDate(dueDate);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border rounded-lg transition-colors",
|
||||
solved
|
||||
? "border-emerald-200/50 dark:border-emerald-800/30"
|
||||
: "border-border",
|
||||
)}
|
||||
>
|
||||
{/* Main row */}
|
||||
<div
|
||||
className="flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-muted/30 transition-colors"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{/* Status icon */}
|
||||
<div className="shrink-0">
|
||||
{solved ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-emerald-500" />
|
||||
) : future ? (
|
||||
<Clock className="h-4 w-4 text-amber-500" />
|
||||
) : (
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* App number */}
|
||||
<div className="w-28 shrink-0">
|
||||
<p className="text-sm font-mono font-semibold">{appNo || appId || "-"}</p>
|
||||
</div>
|
||||
|
||||
{/* Service description */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm truncate">{services || "-"}</p>
|
||||
{deponent && (
|
||||
<p className="text-[11px] text-muted-foreground truncate">
|
||||
{deponent}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dates */}
|
||||
<div className="hidden sm:flex items-center gap-4 shrink-0 text-xs text-muted-foreground">
|
||||
<div className="text-center w-20">
|
||||
<p className="text-[10px] uppercase tracking-wide opacity-50">
|
||||
Depus
|
||||
</p>
|
||||
<p>{fmtDate(filingDate)}</p>
|
||||
</div>
|
||||
<div className="text-center w-20">
|
||||
<p className="text-[10px] uppercase tracking-wide opacity-50">
|
||||
Termen
|
||||
</p>
|
||||
<p className={cn(future && "text-amber-600 dark:text-amber-400 font-medium")}>
|
||||
{fmtDate(dueDate)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status badge */}
|
||||
<Badge
|
||||
variant={solved ? "default" : "secondary"}
|
||||
className={cn(
|
||||
"text-[10px] shrink-0",
|
||||
solved &&
|
||||
"bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-400",
|
||||
)}
|
||||
>
|
||||
{status || "-"}
|
||||
</Badge>
|
||||
|
||||
{/* Expand arrow */}
|
||||
<div className="shrink-0">
|
||||
{expanded ? (
|
||||
<ChevronUp className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded: issued documents */}
|
||||
{expanded && appId && (
|
||||
<div className="px-4 pb-3 border-t bg-muted/10">
|
||||
<IssuedDocsPanel applicationId={appId} workspaceId={workspaceId} />
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -422,14 +320,36 @@ export default function RgiTestPage() {
|
||||
const [year, setYear] = useState("2026");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [applications, setApplications] = useState<Application[]>([]);
|
||||
const [applications, setApplications] = useState<App[]>([]);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [filterSolved, setFilterSolved] = useState(true);
|
||||
const [expandedPk, setExpandedPk] = useState<number | null>(null);
|
||||
const [showColumnPicker, setShowColumnPicker] = useState(false);
|
||||
|
||||
// Column visibility — saved per session
|
||||
const [visibleCols, setVisibleCols] = useState<Set<string>>(
|
||||
() => new Set(ALL_COLUMNS.filter((c) => c.defaultVisible).map((c) => c.key)),
|
||||
);
|
||||
|
||||
const toggleColumn = (key: string) => {
|
||||
setVisibleCols((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(key)) next.delete(key);
|
||||
else next.add(key);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const columns = useMemo(
|
||||
() => ALL_COLUMNS.filter((c) => visibleCols.has(c.key)),
|
||||
[visibleCols],
|
||||
);
|
||||
|
||||
const loadApplications = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
setApplications([]);
|
||||
setExpandedPk(null);
|
||||
try {
|
||||
const res = await fetch("/api/eterra/rgi/applications", {
|
||||
method: "POST",
|
||||
@@ -443,57 +363,36 @@ export default function RgiTestPage() {
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.error) {
|
||||
setError(data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract applications array
|
||||
let items: Application[] = [];
|
||||
if (Array.isArray(data)) {
|
||||
items = data;
|
||||
} else if (data?.content && Array.isArray(data.content)) {
|
||||
items = data.content;
|
||||
const items: App[] = Array.isArray(data)
|
||||
? data
|
||||
: data?.content ?? data?.data ?? data?.list ?? [];
|
||||
setApplications(items);
|
||||
setTotalCount(
|
||||
typeof data.totalElements === "number"
|
||||
typeof data?.totalElements === "number"
|
||||
? data.totalElements
|
||||
: items.length,
|
||||
);
|
||||
} else if (data?.data && Array.isArray(data.data)) {
|
||||
items = data.data;
|
||||
} else if (data?.list && Array.isArray(data.list)) {
|
||||
items = data.list;
|
||||
} else if (data?.results && Array.isArray(data.results)) {
|
||||
items = data.results;
|
||||
} else {
|
||||
// Try the whole response as an array
|
||||
setError(
|
||||
"Format raspuns necunoscut. Verifica consola (F12) pentru detalii.",
|
||||
);
|
||||
console.log("RGI response:", data);
|
||||
return;
|
||||
}
|
||||
|
||||
setApplications(items);
|
||||
if (!totalCount) setTotalCount(items.length);
|
||||
} catch {
|
||||
setError("Eroare de retea. Verifica conexiunea la eTerra.");
|
||||
}
|
||||
setLoading(false);
|
||||
}, [workspaceId, orgUnitId, year, totalCount]);
|
||||
}, [workspaceId, orgUnitId, year]);
|
||||
|
||||
// Apply client-side filter
|
||||
const filtered = filterSolved
|
||||
? applications.filter((app) => {
|
||||
const dueDate =
|
||||
app.dueDate ?? app.termen ?? app.deadlineDate ?? app.deadline;
|
||||
return isSolved(app) && isFutureDate(dueDate);
|
||||
})
|
||||
: applications;
|
||||
// Client-side filter
|
||||
const filtered = useMemo(() => {
|
||||
if (!filterSolved) return applications;
|
||||
const now = Date.now();
|
||||
return applications.filter(
|
||||
(a) => a.hasSolution === 1 && a.dueDate > now,
|
||||
);
|
||||
}, [applications, filterSolved]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 max-w-5xl mx-auto">
|
||||
<div className="space-y-4 max-w-[1400px] mx-auto">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">Documente Eliberate eTerra</h1>
|
||||
@@ -504,24 +403,22 @@ export default function RgiTestPage() {
|
||||
|
||||
{/* Filters */}
|
||||
<Card>
|
||||
<CardContent className="pt-4">
|
||||
<CardContent className="pt-4 space-y-3">
|
||||
<div className="flex items-end gap-3 flex-wrap">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Judet (workspaceId)</Label>
|
||||
<Label className="text-xs">Judet (workspace)</Label>
|
||||
<Input
|
||||
value={workspaceId}
|
||||
onChange={(e) => setWorkspaceId(e.target.value)}
|
||||
className="w-28"
|
||||
placeholder="127"
|
||||
className="w-24"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">OCPI (orgUnitId)</Label>
|
||||
<Label className="text-xs">OCPI (orgUnit)</Label>
|
||||
<Input
|
||||
value={orgUnitId}
|
||||
onChange={(e) => setOrgUnitId(e.target.value)}
|
||||
className="w-28"
|
||||
placeholder="127002"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
@@ -529,8 +426,7 @@ export default function RgiTestPage() {
|
||||
<Input
|
||||
value={year}
|
||||
onChange={(e) => setYear(e.target.value)}
|
||||
className="w-24"
|
||||
placeholder="2026"
|
||||
className="w-20"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={() => void loadApplications()} disabled={loading}>
|
||||
@@ -541,10 +437,39 @@ export default function RgiTestPage() {
|
||||
)}
|
||||
Incarca lucrari
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-1"
|
||||
onClick={() => setShowColumnPicker(!showColumnPicker)}
|
||||
>
|
||||
<Settings2 className="h-3.5 w-3.5" />
|
||||
Coloane
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Column picker */}
|
||||
{showColumnPicker && (
|
||||
<div className="flex flex-wrap gap-1.5 pt-2 border-t">
|
||||
{ALL_COLUMNS.map((col) => (
|
||||
<button
|
||||
key={col.key}
|
||||
onClick={() => toggleColumn(col.key)}
|
||||
className={cn(
|
||||
"px-2 py-1 rounded text-[11px] border transition-colors",
|
||||
visibleCols.has(col.key)
|
||||
? "bg-foreground/10 border-foreground/20 font-medium"
|
||||
: "bg-transparent border-muted-foreground/15 text-muted-foreground hover:border-foreground/20",
|
||||
)}
|
||||
>
|
||||
{col.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Filter toggle */}
|
||||
<div className="flex items-center gap-3 mt-3 pt-3 border-t">
|
||||
<div className="flex items-center gap-3 pt-2 border-t">
|
||||
<label className="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -552,15 +477,12 @@ export default function RgiTestPage() {
|
||||
onChange={(e) => setFilterSolved(e.target.checked)}
|
||||
className="h-4 w-4 rounded accent-emerald-600"
|
||||
/>
|
||||
<span className="text-sm">
|
||||
Doar solutionate cu termen viitor
|
||||
</span>
|
||||
<span className="text-sm">Doar solutionate cu termen viitor</span>
|
||||
</label>
|
||||
{applications.length > 0 && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{filtered.length} din {applications.length} lucrari
|
||||
{totalCount > applications.length &&
|
||||
` (${totalCount} total pe server)`}
|
||||
{totalCount > applications.length && ` (${totalCount} total)`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -583,45 +505,126 @@ export default function RgiTestPage() {
|
||||
<CardContent className="py-12 text-center text-muted-foreground">
|
||||
<Loader2 className="h-8 w-8 mx-auto mb-3 animate-spin opacity-40" />
|
||||
<p>Se incarca lucrarile din eTerra RGI...</p>
|
||||
<p className="text-xs mt-1 opacity-60">
|
||||
Prima incarcare poate dura 10-30s (autentificare + paginare).
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Results */}
|
||||
{/* Results table */}
|
||||
{!loading && filtered.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{filtered.length} lucrar{filtered.length > 1 ? "i" : "e"}
|
||||
{filterSolved && " solutionate cu termen viitor"}
|
||||
. Click pe un rand pentru a vedea documentele eliberate.
|
||||
</p>
|
||||
{filtered.map((app, i) => (
|
||||
<ApplicationRow
|
||||
key={
|
||||
str(app, "applicationId", "id", "pk") || String(i)
|
||||
}
|
||||
app={app}
|
||||
workspaceId={workspaceId}
|
||||
/>
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/40">
|
||||
<th className="px-2 py-2 w-8"></th>
|
||||
{columns.map((col) => (
|
||||
<th
|
||||
key={col.key}
|
||||
className="px-3 py-2 text-left font-medium text-xs text-muted-foreground whitespace-nowrap"
|
||||
>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
<th className="px-2 py-2 w-8"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filtered.map((app) => {
|
||||
const pk = app.applicationPk;
|
||||
const isExpanded = expandedPk === pk;
|
||||
const solved = app.hasSolution === 1;
|
||||
|
||||
return (
|
||||
<tr key={pk} className="group">
|
||||
<td colSpan={columns.length + 2} className="p-0">
|
||||
{/* Row */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center cursor-pointer hover:bg-muted/30 transition-colors border-b",
|
||||
isExpanded && "bg-muted/20",
|
||||
)}
|
||||
onClick={() =>
|
||||
setExpandedPk(isExpanded ? null : pk)
|
||||
}
|
||||
>
|
||||
{/* Status icon */}
|
||||
<div className="px-2 py-2.5 shrink-0">
|
||||
{solved ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-emerald-500" />
|
||||
) : (
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
{/* Columns */}
|
||||
{columns.map((col) => (
|
||||
<div
|
||||
key={col.key}
|
||||
className={cn(
|
||||
"px-3 py-2.5 text-sm",
|
||||
col.className,
|
||||
)}
|
||||
title={col.render(app)}
|
||||
>
|
||||
{col.key === "statusName" ? (
|
||||
<Badge
|
||||
variant={solved ? "default" : "secondary"}
|
||||
className={cn(
|
||||
"text-[10px]",
|
||||
solved &&
|
||||
"bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-400",
|
||||
)}
|
||||
>
|
||||
{col.render(app)}
|
||||
</Badge>
|
||||
) : col.key === "resolutionName" ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[10px]"
|
||||
>
|
||||
{col.render(app)}
|
||||
</Badge>
|
||||
) : (
|
||||
col.render(app)
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{/* Expand arrow */}
|
||||
<div className="px-2 py-2.5 shrink-0">
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded: issued documents */}
|
||||
{isExpanded && (
|
||||
<div className="px-4 pb-3 bg-muted/10 border-b">
|
||||
<IssuedDocsPanel
|
||||
applicationPk={pk}
|
||||
workspaceId={app.workspaceId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Empty state */}
|
||||
{/* Empty states */}
|
||||
{!loading && applications.length > 0 && filtered.length === 0 && (
|
||||
<Card>
|
||||
<CardContent className="py-8 text-center text-muted-foreground">
|
||||
<FileText className="h-8 w-8 mx-auto mb-2 opacity-30" />
|
||||
<p>
|
||||
Nicio lucrare
|
||||
{filterSolved
|
||||
? " solutionata cu termen viitor"
|
||||
: ""}{" "}
|
||||
gasita.
|
||||
</p>
|
||||
<p>Nicio lucrare {filterSolved ? "solutionata cu termen viitor" : ""} gasita.</p>
|
||||
{filterSolved && (
|
||||
<p className="text-xs mt-1">
|
||||
Debifati filtrul pentru a vedea toate lucrarile.
|
||||
@@ -631,15 +634,11 @@ export default function RgiTestPage() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Initial state */}
|
||||
{!loading && applications.length === 0 && !error && (
|
||||
<Card>
|
||||
<CardContent className="py-8 text-center text-muted-foreground">
|
||||
<Search className="h-8 w-8 mx-auto mb-2 opacity-30" />
|
||||
<p>Apasa "Incarca lucrari" pentru a incepe.</p>
|
||||
<p className="text-xs mt-1">
|
||||
Necesita conexiune eTerra activa (se autentifica automat).
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user