Skip to content

Commit 97d91e5

Browse files
committed
feat(upload): upload history page with stats and pagination
1 parent eab917f commit 97d91e5

1 file changed

Lines changed: 185 additions & 0 deletions

File tree

  • codebenders-dashboard/app/admin/upload/history
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
"use client"
2+
3+
import { useState, useEffect, useCallback } from "react"
4+
import { useRouter } from "next/navigation"
5+
import { Button } from "@/components/ui/button"
6+
import { Loader2 } from "lucide-react"
7+
8+
interface UploadEntry {
9+
id: number
10+
userEmail: string
11+
filename: string
12+
fileType: string
13+
rowsInserted: number
14+
rowsSkipped: number
15+
errorCount: number
16+
status: "success" | "partial" | "failed"
17+
uploadedAt: string
18+
}
19+
20+
const FILE_TYPE_COLORS: Record<string, string> = {
21+
pdp_cohort_ar: "bg-green-50 text-green-700",
22+
pdp_cohort_submission: "bg-green-50 text-green-700",
23+
course_ar: "bg-blue-50 text-blue-700",
24+
course_submission: "bg-blue-50 text-blue-700",
25+
ml_predictions: "bg-purple-50 text-purple-700",
26+
}
27+
28+
const FILE_TYPE_LABELS: Record<string, string> = {
29+
pdp_cohort_ar: "PDP Cohort AR",
30+
pdp_cohort_submission: "PDP Cohort Submission",
31+
course_ar: "Course AR",
32+
course_submission: "Course Submission",
33+
ml_predictions: "ML Predictions",
34+
}
35+
36+
const STATUS_STYLES: Record<string, string> = {
37+
success: "bg-green-100 text-green-700",
38+
partial: "bg-amber-100 text-amber-700",
39+
failed: "bg-red-100 text-red-700",
40+
}
41+
42+
export default function UploadHistoryPage() {
43+
const router = useRouter()
44+
const [entries, setEntries] = useState<UploadEntry[]>([])
45+
const [total, setTotal] = useState(0)
46+
const [page, setPage] = useState(1)
47+
const [loading, setLoading] = useState(true)
48+
const pageSize = 20
49+
50+
const fetchHistory = useCallback(async (p: number) => {
51+
setLoading(true)
52+
try {
53+
const res = await fetch(
54+
`/api/admin/upload/history?page=${p}&pageSize=${pageSize}`
55+
)
56+
const data = await res.json()
57+
setEntries(data.data ?? [])
58+
setTotal(data.total ?? 0)
59+
} catch {
60+
setEntries([])
61+
} finally {
62+
setLoading(false)
63+
}
64+
}, [])
65+
66+
useEffect(() => {
67+
fetchHistory(page)
68+
}, [page, fetchHistory])
69+
70+
const pageCount = Math.ceil(total / pageSize)
71+
72+
const statusCounts = entries.reduce(
73+
(acc, e) => {
74+
acc[e.status] = (acc[e.status] ?? 0) + 1
75+
return acc
76+
},
77+
{} as Record<string, number>
78+
)
79+
80+
return (
81+
<div className="container mx-auto px-4 py-6 max-w-5xl">
82+
<div className="flex items-center justify-between mb-6">
83+
<div>
84+
<h1 className="text-xl font-bold">Upload History</h1>
85+
<p className="text-sm text-muted-foreground">
86+
All data file uploads by admin and IR users
87+
</p>
88+
</div>
89+
<Button
90+
className="bg-purple-600 hover:bg-purple-700"
91+
onClick={() => router.push("/admin/upload")}
92+
>
93+
+ New Upload
94+
</Button>
95+
</div>
96+
97+
<div className="grid grid-cols-4 gap-3 mb-6">
98+
<div className="bg-muted/30 border rounded-lg px-4 py-3">
99+
<div className="text-[11px] text-muted-foreground uppercase tracking-wide">Total Uploads</div>
100+
<div className="text-2xl font-bold mt-1">{total}</div>
101+
</div>
102+
<div className="bg-green-50 border border-green-200 rounded-lg px-4 py-3">
103+
<div className="text-[11px] text-green-700 uppercase tracking-wide">Successful</div>
104+
<div className="text-2xl font-bold text-green-700 mt-1">{statusCounts.success ?? 0}</div>
105+
</div>
106+
<div className="bg-amber-50 border border-amber-200 rounded-lg px-4 py-3">
107+
<div className="text-[11px] text-amber-700 uppercase tracking-wide">Partial</div>
108+
<div className="text-2xl font-bold text-amber-700 mt-1">{statusCounts.partial ?? 0}</div>
109+
</div>
110+
<div className="bg-red-50 border border-red-200 rounded-lg px-4 py-3">
111+
<div className="text-[11px] text-red-700 uppercase tracking-wide">Failed</div>
112+
<div className="text-2xl font-bold text-red-700 mt-1">{statusCounts.failed ?? 0}</div>
113+
</div>
114+
</div>
115+
116+
{loading ? (
117+
<div className="flex items-center justify-center py-12 text-muted-foreground">
118+
<Loader2 className="h-5 w-5 animate-spin mr-2" /> Loading…
119+
</div>
120+
) : entries.length === 0 ? (
121+
<div className="text-center py-12 text-muted-foreground text-sm">
122+
No uploads yet. Click &quot;+ New Upload&quot; to get started.
123+
</div>
124+
) : (
125+
<div className="border rounded-lg overflow-hidden">
126+
<table className="w-full text-sm">
127+
<thead>
128+
<tr className="bg-muted">
129+
<th className="px-4 py-2.5 text-left font-semibold">File</th>
130+
<th className="px-4 py-2.5 text-left font-semibold">Type</th>
131+
<th className="px-4 py-2.5 text-right font-semibold">Inserted</th>
132+
<th className="px-4 py-2.5 text-right font-semibold">Skipped</th>
133+
<th className="px-4 py-2.5 text-right font-semibold">Errors</th>
134+
<th className="px-4 py-2.5 text-left font-semibold">Status</th>
135+
<th className="px-4 py-2.5 text-left font-semibold">Uploaded By</th>
136+
<th className="px-4 py-2.5 text-left font-semibold">Date</th>
137+
</tr>
138+
</thead>
139+
<tbody>
140+
{entries.map((e, i) => (
141+
<tr key={e.id} className={i % 2 === 1 ? "bg-muted/20" : ""}>
142+
<td className="px-4 py-2.5 font-mono text-xs">{e.filename}</td>
143+
<td className="px-4 py-2.5">
144+
<span className={`text-[11px] px-2 py-0.5 rounded ${FILE_TYPE_COLORS[e.fileType] ?? "bg-muted text-muted-foreground"}`}>
145+
{FILE_TYPE_LABELS[e.fileType] ?? e.fileType}
146+
</span>
147+
</td>
148+
<td className="px-4 py-2.5 text-right font-medium">{e.rowsInserted.toLocaleString()}</td>
149+
<td className="px-4 py-2.5 text-right text-amber-700">{e.rowsSkipped}</td>
150+
<td className="px-4 py-2.5 text-right text-red-700">{e.errorCount}</td>
151+
<td className="px-4 py-2.5">
152+
<span className={`text-[11px] px-2 py-0.5 rounded-full font-medium ${STATUS_STYLES[e.status] ?? ""}`}>
153+
{e.status}
154+
</span>
155+
</td>
156+
<td className="px-4 py-2.5 text-xs text-muted-foreground">{e.userEmail}</td>
157+
<td className="px-4 py-2.5 text-xs text-muted-foreground">{new Date(e.uploadedAt).toLocaleDateString()}</td>
158+
</tr>
159+
))}
160+
</tbody>
161+
</table>
162+
</div>
163+
)}
164+
165+
{pageCount > 1 && (
166+
<div className="flex items-center justify-between mt-4 text-xs text-muted-foreground">
167+
<span>Showing {(page - 1) * pageSize + 1}{Math.min(page * pageSize, total)} of {total} uploads</span>
168+
<div className="flex gap-1">
169+
<button className="border px-2 py-1 rounded disabled:opacity-40" disabled={page <= 1} onClick={() => setPage(page - 1)}>← Prev</button>
170+
{Array.from({ length: pageCount }, (_, i) => i + 1).slice(0, 5).map((p) => (
171+
<button
172+
key={p}
173+
className={`px-2 py-1 rounded ${p === page ? "bg-purple-600 text-white" : "border hover:bg-muted"}`}
174+
onClick={() => setPage(p)}
175+
>
176+
{p}
177+
</button>
178+
))}
179+
<button className="border px-2 py-1 rounded disabled:opacity-40" disabled={page >= pageCount} onClick={() => setPage(page + 1)}>Next →</button>
180+
</div>
181+
</div>
182+
)}
183+
</div>
184+
)
185+
}

0 commit comments

Comments
 (0)