feat(rgi): filter by downloadable/pending + locked document indicator

eTerra blocks document downloads until dueDate passes (new rule).
Now the page shows:

Filter modes:
- "Descarcabile acum" (default) — solved + dueDate passed
- "In asteptare" — solved + dueDate future (documents locked)
- "Toate" — no filter

UI indicators:
- Green download icon: ready to download
- Amber clock icon: solved but locked until dueDate
- Documents panel shows "Disponibile de la DD.MM.YYYY" badge when locked
- Download button replaced with date badge for locked documents

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-24 23:12:48 +02:00
parent 3614c2fc4a
commit a191a684b2
+64 -32
View File
@@ -210,9 +210,11 @@ const ALL_COLUMNS: ColumnDef[] = [
function IssuedDocsPanel({
applicationPk,
workspaceId,
dueDate,
}: {
applicationPk: number;
workspaceId: number;
dueDate: number;
}) {
const [docs, setDocs] = useState<IssuedDoc[] | null>(null);
const [loading, setLoading] = useState(true);
@@ -258,12 +260,22 @@ function IssuedDocsPanel({
);
}
const isLocked = dueDate > Date.now();
return (
<div className="space-y-1.5 py-2">
<p className="text-xs font-medium text-muted-foreground mb-2">
<div className="flex items-center gap-2 mb-2">
<p className="text-xs font-medium text-muted-foreground">
{docs.length} document{docs.length > 1 ? "e" : ""} eliberat
{docs.length > 1 ? "e" : ""}:
{docs.length > 1 ? "e" : ""}
</p>
{isLocked && (
<Badge variant="outline" className="text-[10px] border-amber-300 text-amber-600 dark:text-amber-400">
<Clock className="h-3 w-3 mr-0.5" />
Disponibile de la {fmtTs(dueDate)}
</Badge>
)}
</div>
{docs.map((doc, i) => (
<div
key={doc.documentPk || i}
@@ -295,13 +307,13 @@ function IssuedDocsPanel({
</div>
</div>
<div className="flex items-center gap-1 shrink-0">
{/* Server-side download (works when fileVisibility returns OK) */}
<Button
size="sm"
variant="outline"
className="gap-1"
asChild
>
{isLocked ? (
<Badge variant="secondary" className="text-[10px] text-muted-foreground">
<Clock className="h-3 w-3 mr-0.5" />
{fmtTs(dueDate)}
</Badge>
) : (
<Button size="sm" variant="outline" className="gap-1" asChild>
<a
href={`/api/eterra/rgi/download-doc?workspaceId=${doc.workspaceId || workspaceId}&applicationId=${doc.applicationId || applicationPk}&documentPk=${doc.documentPk}&documentTypeId=${doc.documentTypeId}`}
target="_blank"
@@ -311,6 +323,7 @@ function IssuedDocsPanel({
Descarca
</a>
</Button>
)}
</div>
</div>
))}
@@ -330,7 +343,7 @@ export default function RgiTestPage() {
const [error, setError] = useState("");
const [applications, setApplications] = useState<App[]>([]);
const [totalCount, setTotalCount] = useState(0);
const [filterSolved, setFilterSolved] = useState(true);
// filterSolved removed — replaced by filterMode
const [expandedPk, setExpandedPk] = useState<number | null>(null);
const [showColumnPicker, setShowColumnPicker] = useState(false);
@@ -390,14 +403,19 @@ export default function RgiTestPage() {
setLoading(false);
}, [workspaceId, orgUnitId, year]);
const [filterMode, setFilterMode] = useState<"all" | "ready" | "pending">("ready");
// Client-side filter
const filtered = useMemo(() => {
if (!filterSolved) return applications;
if (filterMode === "all") return applications;
const now = Date.now();
return applications.filter(
(a) => a.hasSolution === 1 && a.dueDate > now,
);
}, [applications, filterSolved]);
return applications.filter((a) => {
if (a.hasSolution !== 1) return false;
if (filterMode === "ready") return a.dueDate <= now; // termen trecut = descarcabil
if (filterMode === "pending") return a.dueDate > now; // termen viitor = blocat
return true;
});
}, [applications, filterMode]);
return (
<div className="space-y-4 max-w-[1400px] mx-auto">
@@ -477,16 +495,28 @@ export default function RgiTestPage() {
)}
{/* Filter toggle */}
<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"
checked={filterSolved}
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>
</label>
<div className="flex items-center gap-3 pt-2 border-t flex-wrap">
<div className="flex gap-1 p-0.5 bg-muted rounded-md">
{([
{ id: "ready" as const, label: "Descarcabile acum", desc: "solutionate + termen trecut" },
{ id: "pending" as const, label: "In asteptare", desc: "solutionate + termen viitor (blocate)" },
{ id: "all" as const, label: "Toate", desc: "" },
]).map((opt) => (
<button
key={opt.id}
onClick={() => setFilterMode(opt.id)}
className={cn(
"px-3 py-1 text-xs rounded font-medium transition-colors",
filterMode === opt.id
? "bg-background shadow text-foreground"
: "text-muted-foreground hover:text-foreground",
)}
title={opt.desc}
>
{opt.label}
</button>
))}
</div>
{applications.length > 0 && (
<span className="text-xs text-muted-foreground">
{filtered.length} din {applications.length} lucrari
@@ -542,6 +572,7 @@ export default function RgiTestPage() {
const pk = app.applicationPk;
const isExpanded = expandedPk === pk;
const solved = app.hasSolution === 1;
const downloadable = solved && app.dueDate <= Date.now();
return (
<React.Fragment key={pk}>
@@ -554,9 +585,11 @@ export default function RgiTestPage() {
setExpandedPk(isExpanded ? null : pk)
}
>
<td className="px-2 py-2.5 w-8">
{solved ? (
<CheckCircle2 className="h-4 w-4 text-emerald-500" />
<td className="px-2 py-2.5 w-8" title={downloadable ? "Descarcabil" : solved ? `Blocat pana la ${fmtTs(app.dueDate)}` : "In lucru"}>
{downloadable ? (
<Download className="h-4 w-4 text-emerald-500" />
) : solved ? (
<Clock className="h-4 w-4 text-amber-500" />
) : (
<Clock className="h-4 w-4 text-muted-foreground" />
)}
@@ -610,6 +643,7 @@ export default function RgiTestPage() {
<IssuedDocsPanel
applicationPk={pk}
workspaceId={app.workspaceId}
dueDate={app.dueDate}
/>
</td>
</tr>
@@ -629,12 +663,10 @@ export default function RgiTestPage() {
<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>
{filterSolved && (
<p>Nicio lucrare gasita pentru filtrul selectat.</p>
<p className="text-xs mt-1">
Debifati filtrul pentru a vedea toate lucrarile.
Schimba filtrul pentru a vedea alte lucrari.
</p>
)}
</CardContent>
</Card>
)}