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:
AI Assistant
2026-03-24 21:33:10 +02:00
parent 64f10a63ff
commit 227c363e13
+392 -393
View File
@@ -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 &quot;Incarca lucrari&quot; pentru a incepe.</p> <p>Apasa &quot;Incarca lucrari&quot; pentru a incepe.</p>
<p className="text-xs mt-1">
Necesita conexiune eTerra activa (se autentifica automat).
</p>
</CardContent> </CardContent>
</Card> </Card>
)} )}