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({ function IssuedDocsPanel({
applicationPk, applicationPk,
workspaceId, workspaceId,
dueDate,
}: { }: {
applicationPk: number; applicationPk: number;
workspaceId: number; workspaceId: number;
dueDate: number;
}) { }) {
const [docs, setDocs] = useState<IssuedDoc[] | null>(null); const [docs, setDocs] = useState<IssuedDoc[] | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -258,12 +260,22 @@ function IssuedDocsPanel({
); );
} }
const isLocked = dueDate > Date.now();
return ( return (
<div className="space-y-1.5 py-2"> <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} document{docs.length > 1 ? "e" : ""} eliberat
{docs.length > 1 ? "e" : ""}: {docs.length > 1 ? "e" : ""}
</p> </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) => ( {docs.map((doc, i) => (
<div <div
key={doc.documentPk || i} key={doc.documentPk || i}
@@ -295,13 +307,13 @@ function IssuedDocsPanel({
</div> </div>
</div> </div>
<div className="flex items-center gap-1 shrink-0"> <div className="flex items-center gap-1 shrink-0">
{/* Server-side download (works when fileVisibility returns OK) */} {isLocked ? (
<Button <Badge variant="secondary" className="text-[10px] text-muted-foreground">
size="sm" <Clock className="h-3 w-3 mr-0.5" />
variant="outline" {fmtTs(dueDate)}
className="gap-1" </Badge>
asChild ) : (
> <Button size="sm" variant="outline" className="gap-1" asChild>
<a <a
href={`/api/eterra/rgi/download-doc?workspaceId=${doc.workspaceId || workspaceId}&applicationId=${doc.applicationId || applicationPk}&documentPk=${doc.documentPk}&documentTypeId=${doc.documentTypeId}`} href={`/api/eterra/rgi/download-doc?workspaceId=${doc.workspaceId || workspaceId}&applicationId=${doc.applicationId || applicationPk}&documentPk=${doc.documentPk}&documentTypeId=${doc.documentTypeId}`}
target="_blank" target="_blank"
@@ -311,6 +323,7 @@ function IssuedDocsPanel({
Descarca Descarca
</a> </a>
</Button> </Button>
)}
</div> </div>
</div> </div>
))} ))}
@@ -330,7 +343,7 @@ export default function RgiTestPage() {
const [error, setError] = useState(""); const [error, setError] = useState("");
const [applications, setApplications] = useState<App[]>([]); const [applications, setApplications] = useState<App[]>([]);
const [totalCount, setTotalCount] = useState(0); const [totalCount, setTotalCount] = useState(0);
const [filterSolved, setFilterSolved] = useState(true); // filterSolved removed — replaced by filterMode
const [expandedPk, setExpandedPk] = useState<number | null>(null); const [expandedPk, setExpandedPk] = useState<number | null>(null);
const [showColumnPicker, setShowColumnPicker] = useState(false); const [showColumnPicker, setShowColumnPicker] = useState(false);
@@ -390,14 +403,19 @@ export default function RgiTestPage() {
setLoading(false); setLoading(false);
}, [workspaceId, orgUnitId, year]); }, [workspaceId, orgUnitId, year]);
const [filterMode, setFilterMode] = useState<"all" | "ready" | "pending">("ready");
// Client-side filter // Client-side filter
const filtered = useMemo(() => { const filtered = useMemo(() => {
if (!filterSolved) return applications; if (filterMode === "all") return applications;
const now = Date.now(); const now = Date.now();
return applications.filter( return applications.filter((a) => {
(a) => a.hasSolution === 1 && a.dueDate > now, if (a.hasSolution !== 1) return false;
); if (filterMode === "ready") return a.dueDate <= now; // termen trecut = descarcabil
}, [applications, filterSolved]); if (filterMode === "pending") return a.dueDate > now; // termen viitor = blocat
return true;
});
}, [applications, filterMode]);
return ( return (
<div className="space-y-4 max-w-[1400px] mx-auto"> <div className="space-y-4 max-w-[1400px] mx-auto">
@@ -477,16 +495,28 @@ export default function RgiTestPage() {
)} )}
{/* Filter toggle */} {/* Filter toggle */}
<div className="flex items-center gap-3 pt-2 border-t"> <div className="flex items-center gap-3 pt-2 border-t flex-wrap">
<label className="flex items-center gap-2 cursor-pointer select-none"> <div className="flex gap-1 p-0.5 bg-muted rounded-md">
<input {([
type="checkbox" { id: "ready" as const, label: "Descarcabile acum", desc: "solutionate + termen trecut" },
checked={filterSolved} { id: "pending" as const, label: "In asteptare", desc: "solutionate + termen viitor (blocate)" },
onChange={(e) => setFilterSolved(e.target.checked)} { id: "all" as const, label: "Toate", desc: "" },
className="h-4 w-4 rounded accent-emerald-600" ]).map((opt) => (
/> <button
<span className="text-sm">Doar solutionate cu termen viitor</span> key={opt.id}
</label> 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 && ( {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
@@ -542,6 +572,7 @@ export default function RgiTestPage() {
const pk = app.applicationPk; const pk = app.applicationPk;
const isExpanded = expandedPk === pk; const isExpanded = expandedPk === pk;
const solved = app.hasSolution === 1; const solved = app.hasSolution === 1;
const downloadable = solved && app.dueDate <= Date.now();
return ( return (
<React.Fragment key={pk}> <React.Fragment key={pk}>
@@ -554,9 +585,11 @@ export default function RgiTestPage() {
setExpandedPk(isExpanded ? null : pk) setExpandedPk(isExpanded ? null : pk)
} }
> >
<td className="px-2 py-2.5 w-8"> <td className="px-2 py-2.5 w-8" title={downloadable ? "Descarcabil" : solved ? `Blocat pana la ${fmtTs(app.dueDate)}` : "In lucru"}>
{solved ? ( {downloadable ? (
<CheckCircle2 className="h-4 w-4 text-emerald-500" /> <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" /> <Clock className="h-4 w-4 text-muted-foreground" />
)} )}
@@ -610,6 +643,7 @@ export default function RgiTestPage() {
<IssuedDocsPanel <IssuedDocsPanel
applicationPk={pk} applicationPk={pk}
workspaceId={app.workspaceId} workspaceId={app.workspaceId}
dueDate={app.dueDate}
/> />
</td> </td>
</tr> </tr>
@@ -629,12 +663,10 @@ export default function RgiTestPage() {
<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>Nicio lucrare {filterSolved ? "solutionata cu termen viitor" : ""} gasita.</p> <p>Nicio lucrare gasita pentru filtrul selectat.</p>
{filterSolved && (
<p className="text-xs mt-1"> <p className="text-xs mt-1">
Debifati filtrul pentru a vedea toate lucrarile. Schimba filtrul pentru a vedea alte lucrari.
</p> </p>
)}
</CardContent> </CardContent>
</Card> </Card>
)} )}