-
Notifications
You must be signed in to change notification settings - Fork 5
Created an option to view and download file for admins when approving a file. #200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
508867a
fa09968
34415c9
76d3c4b
09b782c
0ff3dfd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| import React, { useState, useEffect } from "react"; | ||
| import { | ||
| fetchAllPendingContributions, | ||
| handleContribution, | ||
| getFileDownloadUrl, | ||
| } from "@/apis/courses"; | ||
| import { toast } from "react-toastify"; | ||
| import { FiFile, FiCheck, FiX, FiEye, FiDownload, FiInbox } from "react-icons/fi"; | ||
|
|
||
| export default function PendingApprovals() { | ||
| const [pending, setPending] = useState([]); | ||
| const [loading, setLoading] = useState(true); | ||
| const [processingId, setProcessingId] = useState(null); | ||
|
|
||
| const loadPending = async () => { | ||
| try { | ||
| setLoading(true); | ||
| const response = await fetchAllPendingContributions(); | ||
| setPending(response.pending || []); | ||
| } catch (err) { | ||
| toast.error(err.message || "Failed to load pending approvals."); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| loadPending(); | ||
| }, []); | ||
|
|
||
| const onAction = async (contributionId, action) => { | ||
| if (processingId) return; | ||
| try { | ||
| setProcessingId(contributionId); | ||
| await handleContribution(contributionId, action); | ||
| toast.success(action === "approve" ? "Contribution approved." : "Contribution rejected."); | ||
| // Drop the handled row without a full refetch. | ||
| setPending((prev) => prev.filter((c) => c.contributionId !== contributionId)); | ||
| } catch (err) { | ||
| toast.error(err.message || `Failed to ${action} contribution.`); | ||
| } finally { | ||
| setProcessingId(null); | ||
| } | ||
| }; | ||
|
|
||
| const onDownload = async (fileId) => { | ||
| try { | ||
| const data = await getFileDownloadUrl(fileId); | ||
| if (data?.url) { | ||
| window.location.href = data.url; | ||
| } | ||
| } catch (err) { | ||
| toast.error("Failed to get download link."); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="p-6 space-y-6"> | ||
| <div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-lg border border-gray-200/60 p-6 flex flex-col md:flex-row items-start md:items-center justify-between gap-4"> | ||
| <div> | ||
| <h1 className="text-2xl font-bold text-gray-900">Pending Approvals</h1> | ||
| <p className="text-gray-600 mt-1"> | ||
| All unverified files across every course, awaiting review. | ||
| </p> | ||
| </div> | ||
| {!loading && ( | ||
| <span className="rounded-full bg-amber-50 px-3 py-1 text-sm font-semibold text-amber-700"> | ||
| {pending.length} pending | ||
| </span> | ||
| )} | ||
| </div> | ||
|
|
||
| <div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-lg border border-gray-200/60 p-6"> | ||
| {loading ? ( | ||
| <p className="text-sm text-slate-500">Loading pending approvals...</p> | ||
| ) : pending.length === 0 ? ( | ||
| <div className="flex flex-col items-center justify-center py-16 text-center"> | ||
| <FiInbox className="mb-3 text-3xl text-slate-300" /> | ||
| <div className="text-sm font-semibold text-slate-900">You're all caught up</div> | ||
| <div className="mt-0.5 text-sm text-slate-500"> | ||
| No files are waiting for approval. | ||
| </div> | ||
| </div> | ||
| ) : ( | ||
| <div className="space-y-3"> | ||
| {pending.map((contribution) => ( | ||
| <div | ||
| key={contribution.contributionId} | ||
| className="flex flex-wrap items-center gap-3 rounded-lg border border-slate-200 px-3.5 py-3" | ||
| > | ||
| <span className="grid h-9 w-9 flex-shrink-0 place-items-center rounded-lg bg-amber-50 text-base text-amber-700"> | ||
| <FiFile /> | ||
| </span> | ||
|
|
||
| <div className="min-w-0 flex-1"> | ||
| <div className="text-xs font-semibold uppercase text-blue-700"> | ||
| {contribution.courseCode} | ||
| </div> | ||
| {contribution.files && contribution.files.length > 0 ? ( | ||
| <div className="mt-1 flex flex-col gap-1"> | ||
| {contribution.files.map((file) => ( | ||
| <div | ||
| key={file._id} | ||
| className="flex flex-wrap items-center gap-2" | ||
| > | ||
| <span className="truncate text-sm font-medium text-slate-900"> | ||
| {file.name} | ||
| </span> | ||
| {file.webUrl && ( | ||
| <a | ||
| href={file.webUrl} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="inline-flex items-center gap-1 rounded-md border border-slate-200 bg-white px-2 py-0.5 text-xs font-medium text-slate-600 hover:border-slate-300 hover:bg-slate-50" | ||
| > | ||
| <FiEye /> View | ||
| </a> | ||
| )} | ||
| {file.fileId && ( | ||
| <button | ||
| onClick={() => onDownload(file.fileId)} | ||
| className="inline-flex items-center gap-1 rounded-md border border-slate-200 bg-white px-2 py-0.5 text-xs font-medium text-slate-600 hover:border-slate-300 hover:bg-slate-50" | ||
| > | ||
| <FiDownload /> Download | ||
| </button> | ||
| )} | ||
| </div> | ||
| ))} | ||
| </div> | ||
| ) : ( | ||
| <div className="mt-1 text-sm text-slate-500"> | ||
| {contribution.contributionId} (no files) | ||
| </div> | ||
| )} | ||
| <div className="mt-1 text-xs text-slate-500">Awaiting review</div> | ||
| </div> | ||
|
|
||
| <div className="flex flex-shrink-0 gap-2"> | ||
| <button | ||
| onClick={() => onAction(contribution.contributionId, "approve")} | ||
| disabled={processingId === contribution.contributionId} | ||
| className="inline-flex items-center gap-1.5 rounded-lg bg-green-600 px-3.5 py-2 text-sm font-semibold text-white hover:bg-green-700 disabled:opacity-60" | ||
| > | ||
| <FiCheck /> Approve | ||
| </button> | ||
| <button | ||
| onClick={() => onAction(contribution.contributionId, "reject")} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reject deletes files without confirmation. On the server, rejecting permanently deletes every file in the contribution from storage, removes them from the parent folder and deletes the contribution record. One misclick here loses a student's upload for good. The course dashboard confirms before rejecting; this page should too. |
||
| disabled={processingId === contribution.contributionId} | ||
| className="inline-flex items-center gap-1.5 rounded-lg border border-slate-200 bg-white px-3.5 py-2 text-sm font-semibold text-red-600 hover:border-red-200 hover:bg-red-50 disabled:opacity-60" | ||
| > | ||
| <FiX /> Reject | ||
| </button> | ||
| </div> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Crash when a course has pending contributions.
onActionandprocessingIdaren't defined inCourseDashboard(they look copied fromPendingContributions.jsx). As soon as an unapproved contribution renders, this throwsReferenceError: processingId is not definedand the dashboard can't be used to approve/reject. The existinghandleContributionActionis still here but no longer called. Suggest wiring these buttons back tohandleContributionActionand adding a local processing-id state (same for the Reject button below).