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";
|
"use client";
|
||||||
|
|
||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback, useMemo } from "react";
|
||||||
import { Button } from "@/shared/components/ui/button";
|
import { Button } from "@/shared/components/ui/button";
|
||||||
import { Input } from "@/shared/components/ui/input";
|
import { Input } from "@/shared/components/ui/input";
|
||||||
import { Label } from "@/shared/components/ui/label";
|
import { Label } from "@/shared/components/ui/label";
|
||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Clock,
|
Clock,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
|
Settings2,
|
||||||
|
Shield,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { cn } from "@/shared/lib/utils";
|
import { cn } from "@/shared/lib/utils";
|
||||||
|
|
||||||
@@ -23,35 +25,64 @@ import { cn } from "@/shared/lib/utils";
|
|||||||
/* Types */
|
/* Types */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
type Application = Record<string, unknown>;
|
type App = {
|
||||||
type IssuedDoc = Record<string, unknown>;
|
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 {
|
type ColumnDef = {
|
||||||
for (const k of keys) {
|
key: string;
|
||||||
const v = obj[k];
|
label: string;
|
||||||
if (typeof v === "string" && v.length > 0) return v;
|
defaultVisible: boolean;
|
||||||
if (typeof v === "number") return String(v);
|
render: (app: App) => string;
|
||||||
}
|
className?: string;
|
||||||
return "";
|
};
|
||||||
}
|
|
||||||
|
|
||||||
function num(obj: Record<string, unknown>, ...keys: string[]): number | null {
|
function fmtTs(ts: number | null | undefined): string {
|
||||||
for (const k of keys) {
|
if (!ts) return "-";
|
||||||
const v = obj[k];
|
const d = new Date(ts);
|
||||||
if (typeof v === "number") return v;
|
if (isNaN(d.getTime())) return "-";
|
||||||
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);
|
|
||||||
return d.toLocaleDateString("ro-RO", {
|
return d.toLocaleDateString("ro-RO", {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
@@ -59,194 +90,213 @@ function fmtDate(raw: unknown): string {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFutureDate(raw: unknown): boolean {
|
const ALL_COLUMNS: ColumnDef[] = [
|
||||||
if (!raw) return false;
|
{
|
||||||
const d = typeof raw === "number" ? new Date(raw) : new Date(String(raw));
|
key: "appNo",
|
||||||
return !isNaN(d.getTime()) && d > new Date();
|
label: "Nr. cerere",
|
||||||
}
|
defaultVisible: true,
|
||||||
|
render: (a) => String(a.appNo ?? "-"),
|
||||||
function isSolved(app: Application): boolean {
|
className: "font-mono font-semibold",
|
||||||
const status = str(app, "state", "status", "stare", "applicationState")
|
},
|
||||||
.toLowerCase();
|
{
|
||||||
const stateName = str(
|
key: "initialAppNo",
|
||||||
app,
|
label: "Nr. initial",
|
||||||
"stateName",
|
defaultVisible: false,
|
||||||
"statusName",
|
render: (a) => a.initialAppNo || "-",
|
||||||
"stateDescription",
|
className: "font-mono text-xs",
|
||||||
).toLowerCase();
|
},
|
||||||
return (
|
{
|
||||||
status.includes("solutionat") ||
|
key: "applicationObject",
|
||||||
stateName.includes("solutionat") ||
|
label: "Obiect",
|
||||||
status.includes("finalizat") ||
|
defaultVisible: true,
|
||||||
stateName.includes("finalizat") ||
|
render: (a) => a.applicationObject || "-",
|
||||||
status === "closed" ||
|
},
|
||||||
status === "resolved"
|
{
|
||||||
);
|
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({
|
function IssuedDocsPanel({
|
||||||
applicationId,
|
applicationPk,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
}: {
|
}: {
|
||||||
applicationId: string;
|
applicationPk: number;
|
||||||
workspaceId: string;
|
workspaceId: number;
|
||||||
}) {
|
}) {
|
||||||
const [docs, setDocs] = useState<IssuedDoc[] | null>(null);
|
const [docs, setDocs] = useState<IssuedDoc[] | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [rawData, setRawData] = useState<unknown>(null);
|
|
||||||
|
|
||||||
const loadDocs = useCallback(async () => {
|
// Auto-load
|
||||||
setLoading(true);
|
useState(() => {
|
||||||
setError("");
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
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();
|
const data = await res.json();
|
||||||
setRawData(data);
|
const items: IssuedDoc[] = Array.isArray(data)
|
||||||
|
? data
|
||||||
// Try to extract docs array from various response shapes
|
: data?.content ?? data?.data ?? data?.list ?? [];
|
||||||
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;
|
|
||||||
}
|
|
||||||
setDocs(items);
|
setDocs(items);
|
||||||
} catch {
|
} catch {
|
||||||
setError("Eroare la incarcarea documentelor");
|
setError("Eroare la incarcarea documentelor");
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [applicationId, workspaceId]);
|
})();
|
||||||
|
|
||||||
// Auto-load on mount
|
|
||||||
useState(() => {
|
|
||||||
void loadDocs();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 py-3 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2 py-3 text-sm text-muted-foreground">
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
Se incarca documentele eliberate...
|
Se incarca documentele...
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return <p className="py-3 text-sm text-destructive">{error}</p>;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!docs || docs.length === 0) {
|
if (!docs || docs.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="py-3 text-sm text-muted-foreground">
|
<p className="py-3 text-sm text-muted-foreground">
|
||||||
Niciun document eliberat gasit.
|
Niciun document eliberat.
|
||||||
{rawData != null && (
|
</p>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2 py-2">
|
<div className="space-y-1.5 py-2">
|
||||||
<p className="text-xs font-medium text-muted-foreground">
|
<p className="text-xs font-medium text-muted-foreground mb-2">
|
||||||
{docs.length} document{docs.length > 1 ? "e" : ""} eliberat
|
{docs.length} document{docs.length > 1 ? "e" : ""} eliberat
|
||||||
{docs.length > 1 ? "e" : ""}:
|
{docs.length > 1 ? "e" : ""}:
|
||||||
</p>
|
</p>
|
||||||
{docs.map((doc, i) => {
|
{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 (
|
|
||||||
<div
|
<div
|
||||||
key={docId || i}
|
key={doc.documentPk || i}
|
||||||
className="flex items-center justify-between gap-3 rounded-lg border px-3 py-2"
|
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" />
|
<FileText className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-sm font-medium truncate">
|
<p className="text-sm font-medium">
|
||||||
{docName || `Document #${docId || i + 1}`}
|
{doc.docType || doc.documentTypeCode || "Document"}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-2 text-[11px] text-muted-foreground">
|
<div className="flex items-center gap-2 text-[11px] text-muted-foreground">
|
||||||
{docDate != null && <span>{fmtDate(docDate)}</span>}
|
<span>{fmtTs(doc.startDate || doc.lastUpdatedDtm)}</span>
|
||||||
{docId && (
|
<span className="font-mono opacity-60">
|
||||||
<span className="font-mono opacity-60">ID: {docId}</span>
|
.{(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 && (
|
{doc.identifierDetails && (
|
||||||
<Badge
|
<span className="truncate max-w-[200px]">
|
||||||
variant="outline"
|
{doc.identifierDetails}
|
||||||
className="text-[9px] h-4"
|
</span>
|
||||||
>
|
|
||||||
{docStatus}
|
|
||||||
</Badge>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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
|
<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"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
@@ -254,160 +304,8 @@ function IssuedDocsPanel({
|
|||||||
Descarca
|
Descarca
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -422,14 +320,36 @@ export default function RgiTestPage() {
|
|||||||
const [year, setYear] = useState("2026");
|
const [year, setYear] = useState("2026");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [applications, setApplications] = useState<Application[]>([]);
|
const [applications, setApplications] = useState<App[]>([]);
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
const [filterSolved, setFilterSolved] = useState(true);
|
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 () => {
|
const loadApplications = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
setApplications([]);
|
setApplications([]);
|
||||||
|
setExpandedPk(null);
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/eterra/rgi/applications", {
|
const res = await fetch("/api/eterra/rgi/applications", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -443,57 +363,36 @@ export default function RgiTestPage() {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
setError(data.error);
|
setError(data.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const items: App[] = Array.isArray(data)
|
||||||
// Extract applications array
|
? data
|
||||||
let items: Application[] = [];
|
: data?.content ?? data?.data ?? data?.list ?? [];
|
||||||
if (Array.isArray(data)) {
|
setApplications(items);
|
||||||
items = data;
|
|
||||||
} else if (data?.content && Array.isArray(data.content)) {
|
|
||||||
items = data.content;
|
|
||||||
setTotalCount(
|
setTotalCount(
|
||||||
typeof data.totalElements === "number"
|
typeof data?.totalElements === "number"
|
||||||
? data.totalElements
|
? data.totalElements
|
||||||
: items.length,
|
: 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 {
|
} catch {
|
||||||
setError("Eroare de retea. Verifica conexiunea la eTerra.");
|
setError("Eroare de retea. Verifica conexiunea la eTerra.");
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [workspaceId, orgUnitId, year, totalCount]);
|
}, [workspaceId, orgUnitId, year]);
|
||||||
|
|
||||||
// Apply client-side filter
|
// Client-side filter
|
||||||
const filtered = filterSolved
|
const filtered = useMemo(() => {
|
||||||
? applications.filter((app) => {
|
if (!filterSolved) return applications;
|
||||||
const dueDate =
|
const now = Date.now();
|
||||||
app.dueDate ?? app.termen ?? app.deadlineDate ?? app.deadline;
|
return applications.filter(
|
||||||
return isSolved(app) && isFutureDate(dueDate);
|
(a) => a.hasSolution === 1 && a.dueDate > now,
|
||||||
})
|
);
|
||||||
: applications;
|
}, [applications, filterSolved]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 max-w-5xl mx-auto">
|
<div className="space-y-4 max-w-[1400px] mx-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold">Documente Eliberate eTerra</h1>
|
<h1 className="text-xl font-bold">Documente Eliberate eTerra</h1>
|
||||||
@@ -504,24 +403,22 @@ export default function RgiTestPage() {
|
|||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="pt-4">
|
<CardContent className="pt-4 space-y-3">
|
||||||
<div className="flex items-end gap-3 flex-wrap">
|
<div className="flex items-end gap-3 flex-wrap">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">Judet (workspaceId)</Label>
|
<Label className="text-xs">Judet (workspace)</Label>
|
||||||
<Input
|
<Input
|
||||||
value={workspaceId}
|
value={workspaceId}
|
||||||
onChange={(e) => setWorkspaceId(e.target.value)}
|
onChange={(e) => setWorkspaceId(e.target.value)}
|
||||||
className="w-28"
|
className="w-24"
|
||||||
placeholder="127"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">OCPI (orgUnitId)</Label>
|
<Label className="text-xs">OCPI (orgUnit)</Label>
|
||||||
<Input
|
<Input
|
||||||
value={orgUnitId}
|
value={orgUnitId}
|
||||||
onChange={(e) => setOrgUnitId(e.target.value)}
|
onChange={(e) => setOrgUnitId(e.target.value)}
|
||||||
className="w-28"
|
className="w-28"
|
||||||
placeholder="127002"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -529,8 +426,7 @@ export default function RgiTestPage() {
|
|||||||
<Input
|
<Input
|
||||||
value={year}
|
value={year}
|
||||||
onChange={(e) => setYear(e.target.value)}
|
onChange={(e) => setYear(e.target.value)}
|
||||||
className="w-24"
|
className="w-20"
|
||||||
placeholder="2026"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => void loadApplications()} disabled={loading}>
|
<Button onClick={() => void loadApplications()} disabled={loading}>
|
||||||
@@ -541,10 +437,39 @@ export default function RgiTestPage() {
|
|||||||
)}
|
)}
|
||||||
Incarca lucrari
|
Incarca lucrari
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-1"
|
||||||
|
onClick={() => setShowColumnPicker(!showColumnPicker)}
|
||||||
|
>
|
||||||
|
<Settings2 className="h-3.5 w-3.5" />
|
||||||
|
Coloane
|
||||||
|
</Button>
|
||||||
</div>
|
</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 */}
|
{/* 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">
|
<label className="flex items-center gap-2 cursor-pointer select-none">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -552,15 +477,12 @@ export default function RgiTestPage() {
|
|||||||
onChange={(e) => setFilterSolved(e.target.checked)}
|
onChange={(e) => setFilterSolved(e.target.checked)}
|
||||||
className="h-4 w-4 rounded accent-emerald-600"
|
className="h-4 w-4 rounded accent-emerald-600"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm">
|
<span className="text-sm">Doar solutionate cu termen viitor</span>
|
||||||
Doar solutionate cu termen viitor
|
|
||||||
</span>
|
|
||||||
</label>
|
</label>
|
||||||
{applications.length > 0 && (
|
{applications.length > 0 && (
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{filtered.length} din {applications.length} lucrari
|
{filtered.length} din {applications.length} lucrari
|
||||||
{totalCount > applications.length &&
|
{totalCount > applications.length && ` (${totalCount} total)`}
|
||||||
` (${totalCount} total pe server)`}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -583,45 +505,126 @@ export default function RgiTestPage() {
|
|||||||
<CardContent className="py-12 text-center text-muted-foreground">
|
<CardContent className="py-12 text-center text-muted-foreground">
|
||||||
<Loader2 className="h-8 w-8 mx-auto mb-3 animate-spin opacity-40" />
|
<Loader2 className="h-8 w-8 mx-auto mb-3 animate-spin opacity-40" />
|
||||||
<p>Se incarca lucrarile din eTerra RGI...</p>
|
<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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Results */}
|
{/* Results table */}
|
||||||
{!loading && filtered.length > 0 && (
|
{!loading && filtered.length > 0 && (
|
||||||
<div className="space-y-2">
|
<Card>
|
||||||
<p className="text-sm text-muted-foreground">
|
<CardContent className="p-0">
|
||||||
{filtered.length} lucrar{filtered.length > 1 ? "i" : "e"}
|
<div className="overflow-x-auto">
|
||||||
{filterSolved && " solutionate cu termen viitor"}
|
<table className="w-full text-sm">
|
||||||
. Click pe un rand pentru a vedea documentele eliberate.
|
<thead>
|
||||||
</p>
|
<tr className="border-b bg-muted/40">
|
||||||
{filtered.map((app, i) => (
|
<th className="px-2 py-2 w-8"></th>
|
||||||
<ApplicationRow
|
{columns.map((col) => (
|
||||||
key={
|
<th
|
||||||
str(app, "applicationId", "id", "pk") || String(i)
|
key={col.key}
|
||||||
}
|
className="px-3 py-2 text-left font-medium text-xs text-muted-foreground whitespace-nowrap"
|
||||||
app={app}
|
>
|
||||||
workspaceId={workspaceId}
|
{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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Empty state */}
|
{/* Empty states */}
|
||||||
{!loading && applications.length > 0 && filtered.length === 0 && (
|
{!loading && applications.length > 0 && filtered.length === 0 && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="py-8 text-center text-muted-foreground">
|
<CardContent className="py-8 text-center text-muted-foreground">
|
||||||
<FileText className="h-8 w-8 mx-auto mb-2 opacity-30" />
|
<FileText className="h-8 w-8 mx-auto mb-2 opacity-30" />
|
||||||
<p>
|
<p>Nicio lucrare {filterSolved ? "solutionata cu termen viitor" : ""} gasita.</p>
|
||||||
Nicio lucrare
|
|
||||||
{filterSolved
|
|
||||||
? " solutionata cu termen viitor"
|
|
||||||
: ""}{" "}
|
|
||||||
gasita.
|
|
||||||
</p>
|
|
||||||
{filterSolved && (
|
{filterSolved && (
|
||||||
<p className="text-xs mt-1">
|
<p className="text-xs mt-1">
|
||||||
Debifati filtrul pentru a vedea toate lucrarile.
|
Debifati filtrul pentru a vedea toate lucrarile.
|
||||||
@@ -631,15 +634,11 @@ export default function RgiTestPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Initial state */}
|
|
||||||
{!loading && applications.length === 0 && !error && (
|
{!loading && applications.length === 0 && !error && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="py-8 text-center text-muted-foreground">
|
<CardContent className="py-8 text-center text-muted-foreground">
|
||||||
<Search className="h-8 w-8 mx-auto mb-2 opacity-30" />
|
<Search className="h-8 w-8 mx-auto mb-2 opacity-30" />
|
||||||
<p>Apasa "Incarca lucrari" pentru a incepe.</p>
|
<p>Apasa "Incarca lucrari" pentru a incepe.</p>
|
||||||
<p className="text-xs mt-1">
|
|
||||||
Necesita conexiune eTerra activa (se autentifica automat).
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user