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