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:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user