From 4170a93173f3a0e94a4da006891362ac2344e6f0 Mon Sep 17 00:00:00 2001 From: "Kemas M. Aryadary Rasyad" <112590882+aryadary0706@users.noreply.github.com> Date: Tue, 21 Jul 2026 09:10:47 +0700 Subject: [PATCH] minor update: mentor-generate sertifikat: tambahin status jika intern sudah ada sertifikat --- .../internify/mentor/CertificateForm.tsx | 2 +- .../mentor/components/EligibleInternTable.tsx | 70 +++++++++++++------ src/hooks/useCertificates.ts | 2 +- src/hooks/useInternProgress.ts | 25 ++++--- src/hooks/useUser.ts | 8 --- 5 files changed, 68 insertions(+), 39 deletions(-) diff --git a/src/Pages/internify/mentor/CertificateForm.tsx b/src/Pages/internify/mentor/CertificateForm.tsx index 7883e9e..c4f498f 100644 --- a/src/Pages/internify/mentor/CertificateForm.tsx +++ b/src/Pages/internify/mentor/CertificateForm.tsx @@ -13,7 +13,7 @@ export default function CertificateDetail() { const { slug } = useParams<{ slug: string }>(); const { project, loading, error, refetch } = useProjectDetail(slug ?? ""); const { upload, loading: uploading, error: uploadError } = useUploadCertificateTemplate(); - const { data: internProgress, loading: progressLoading } = useProjectInternProgress(slug ?? ""); + const { data: internProgress, loading: progressLoading } = useProjectInternProgress(project?.id ?? 0); const eligibleCount = internProgress.filter(i => i.is_eligible).length; const fileInputRef = useRef(null); diff --git a/src/Pages/internify/mentor/components/EligibleInternTable.tsx b/src/Pages/internify/mentor/components/EligibleInternTable.tsx index 3ebbce7..9983c36 100644 --- a/src/Pages/internify/mentor/components/EligibleInternTable.tsx +++ b/src/Pages/internify/mentor/components/EligibleInternTable.tsx @@ -7,6 +7,7 @@ import { Eye, ChevronLeft, ChevronRight, + BadgeCheck, Sparkles, Check, Loader2 @@ -43,7 +44,7 @@ function formatDateRange(startDate: string, endDate: string): string { return `${formatDateIndo(startDate)} - ${formatDateIndo(endDate)}`; } -function ProgressBar({ value, total }: {value: number, total: number}) { +function ProgressBar({ value, total }: { value: number, total: number }) { const pct = total === 0 ? 0 : Math.round((value / total) * 100); const isComplete = value === total; @@ -62,13 +63,31 @@ function ProgressBar({ value, total }: {value: number, total: number}) { ); } -function StatusBadge({ status }: { status: boolean }) { +type BadgeState = "certified" | "eligible" | "incomplete"; + +function StatusBadge({ state }: { state: BadgeState }) { + const config = { + certified: { + className: "text-gray-500", + icon: , + label: "Sudah Ada", + }, + eligible: { + className: "text-green-600", + icon: , + label: "Eligible", + }, + incomplete: { + className: "text-orange-500", + icon: , + label: "Incomplete", + }, + }[state]; + return ( -
- {status - ? - : } - {status ? "Eligible" : "Incomplete"} +
+ {config.icon} + {config.label}
); } @@ -84,7 +103,7 @@ export default function EligibleInternTable({ const { generate: SubmitCertificates } = useGenerateCertificates(); const initialSelected = new Set( - interns.filter(i => i.is_eligible).map(i => i.id_user) + interns.filter(i => i.is_eligible && !i.has_certificate).map(i => i.id_user) ); const [selected, setSelected] = useState>(initialSelected); const [generating, setGenerating] = useState(false); @@ -99,7 +118,7 @@ export default function EligibleInternTable({ id: intern.id_user, id_project: project.id, id_user: intern.id_user, - certificate_no: "CERT/YYDDMM/XXX", + certificate_no: "CERT/yymmxx/XXX", issued_at: new Date().toISOString().slice(0, 10), uuid: "HANYA SEBATAS PREVIEW", user: { @@ -121,11 +140,11 @@ export default function EligibleInternTable({ const [page, setPage] = useState(1); const totalPages = Math.ceil(interns.length / PER_PAGE); const paginated = interns.slice((page - 1) * PER_PAGE, page * PER_PAGE); - const eligibleIds = interns.filter(i => i.is_eligible).map(i => i.id_user); - const allEligibleSelected = eligibleIds.every(id => selected.has(id)); + const selectableIds = interns.filter(i => i.is_eligible && !i.has_certificate).map(i => i.id_user); + const allEligibleSelected = selectableIds.length > 0 && selectableIds.every(id => selected.has(id)); const toggleSelectAll = () => { - setSelected(allEligibleSelected ? new Set() : new Set(eligibleIds)); + setSelected(allEligibleSelected ? new Set() : new Set(selectableIds)); }; const toggleOne = (id: number, isEligible: boolean) => { @@ -158,7 +177,7 @@ export default function EligibleInternTable({ const zip = new JSZip(); const templateUrl = resolveFileUrl(project.certificate_template!); if (!templateUrl) throw new Error("Template sertifikat tidak ditemukan."); - + const duration = formatDateRange(project.start_date, project.end_date); await Promise.all( @@ -242,6 +261,8 @@ export default function EligibleInternTable({ {/* Rows */}
{paginated.map((intern) => { + const isCertified = intern.has_certificate; + const isSelectable = intern.is_eligible && !isCertified const isChecked = selected.has(intern.id_user); const isIncomplete = !intern.is_eligible; const initials = intern.full_name @@ -257,8 +278,8 @@ export default function EligibleInternTable({ > {/* Checkbox */} @@ -291,13 +316,17 @@ export default function EligibleInternTable({ {/* Status */}
- +
{/* Action */}
- {isIncomplete ? ( - ) : ( @@ -379,7 +408,6 @@ export default function EligibleInternTable({ )} -
); diff --git a/src/hooks/useCertificates.ts b/src/hooks/useCertificates.ts index 6f6c876..0878fec 100644 --- a/src/hooks/useCertificates.ts +++ b/src/hooks/useCertificates.ts @@ -176,5 +176,5 @@ export const useCertificateVerification = (uuid: string) => { }, [uuid]); return { verification, loading, error }; +} -} \ No newline at end of file diff --git a/src/hooks/useInternProgress.ts b/src/hooks/useInternProgress.ts index 4b50354..49a2597 100644 --- a/src/hooks/useInternProgress.ts +++ b/src/hooks/useInternProgress.ts @@ -1,6 +1,7 @@ -// hooks/useProjectInternProgress.ts +// hooks/useInternProgress.ts import { useState, useEffect, useCallback } from "react"; import { getProjectTasks, getTaskSubmissions } from "../services/TaskService"; +import { getCertificatesByProject } from "../services/CertificateService"; import type { AdminTaskDetail } from "../types/task.types"; export interface InternSubmissionProgress { @@ -11,24 +12,32 @@ export interface InternSubmissionProgress { submitted: number; total_tasks: number; is_eligible: boolean; + has_certificate: boolean; } -export const useProjectInternProgress = (projectSlug: string) => { +export const useProjectInternProgress = (projectid: number) => { const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const refetch = useCallback(async () => { - if (!projectSlug) return; + if (!projectid) return; setLoading(true); setError(null); try { - const tasks = await getProjectTasks(projectSlug); + const [tasks, certificates] = await Promise.all([ + getProjectTasks(projectid), + getCertificatesByProject(projectid).catch(() => []), + ]); + + const certifiedUserIds = new Set( + (certificates ?? []).map((cert) => cert.id_user) + ); const taskDetails = await Promise.all( tasks.map((task) => - getTaskSubmissions(task.slug, projectSlug) as Promise + getTaskSubmissions(task.slug, projectid) as Promise ) ); @@ -38,7 +47,6 @@ export const useProjectInternProgress = (projectSlug: string) => { if (!taskDetail?.submissions) continue; for (const sub of taskDetail.submissions) { - // field yang benar: sub.submission (bukan submission_details) const hasSubmitted = !!sub.submission; const existing = internMap.get(sub.id_user); @@ -50,10 +58,11 @@ export const useProjectInternProgress = (projectSlug: string) => { id_user: sub.id_user, full_name: sub.full_name, email: sub.email, - avatar: sub.profile_picture, // field yang benar + avatar: sub.profile_picture, submitted: hasSubmitted ? 1 : 0, total_tasks: 1, is_eligible: false, + has_certificate: certifiedUserIds.has(sub.id_user), }); } } @@ -71,7 +80,7 @@ export const useProjectInternProgress = (projectSlug: string) => { } finally { setLoading(false); } - }, [projectSlug]); + }, [projectid]); useEffect(() => { refetch(); diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts index b464e2f..3c5ee7f 100644 --- a/src/hooks/useUser.ts +++ b/src/hooks/useUser.ts @@ -108,14 +108,6 @@ export const getInitials = (fullName?: string | null): string => { return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase(); }; -// Untuk resolve gambar (duplikat dari utils/...) -// export const resolveFileUrl = (path?: string | null): string | null => { -// if (!path) return null; -// if (/^(https?:|data:|blob:)/i.test(path)) return path; -// const base = import.meta.env.VITE_API_BASE_URL as string; -// return `${base}${path.startsWith("/") ? path : `/${path}`}`; -// }; - // PATCH /auth-api/update-profile export const useUpdateProfile = () => { const [loading, setLoading] = useState(false);