diff --git a/src/Pages/internify/intern/components/HomeCalendar.tsx b/src/Pages/internify/intern/components/HomeCalendar.tsx
index 8026a4b..8f587c6 100644
--- a/src/Pages/internify/intern/components/HomeCalendar.tsx
+++ b/src/Pages/internify/intern/components/HomeCalendar.tsx
@@ -1,7 +1,6 @@
import { useState, useEffect } from 'react'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { useMyTasks } from '../../../../hooks/useProjects';
-import api from '../../../../lib/api';
import { useCurrentUser } from '../../../../hooks/useUser';
import { useAllMentorTasks } from '../../../../hooks/useTasks';
diff --git a/src/Pages/internify/mentor/CertificateDetail.tsx b/src/Pages/internify/mentor/CertificateDetail.tsx
deleted file mode 100644
index ad4943e..0000000
--- a/src/Pages/internify/mentor/CertificateDetail.tsx
+++ /dev/null
@@ -1,331 +0,0 @@
-// src/pages/mentor/CertificateDetail.tsx
-import { useState } from "react";
-import { useNavigate } from "react-router-dom";
-import {
- CalendarDays,
- CheckCircle2,
- AlertCircle,
- Eye,
- ChevronLeft,
- ChevronRight,
- Sparkles,
-} from "lucide-react";
-
-// ─── Types ────────────────────────────────────────────────────────────────────
-type InternStatus = "ELIGIBLE" | "INCOMPLETE";
-
-interface Intern {
- id: number;
- name: string;
- role: string;
- initials: string;
- taskProgress: number;
- taskTotal: number;
- status: InternStatus;
-}
-
-// ─── Mock Data ────────────────────────────────────────────────────────────────
-const mockProject = {
- title: "Edge Computing Framework",
- period: "Dec 2023 — Mar 2024",
- status: "COMPLETED" as const,
- scope:
- "Advanced research and implementation of low-latency computing nodes at the network edge, focusing on real-time data processing for autonomous systems.",
- totalInterns: 21,
- eligibleInterns: 18,
-};
-
-const mockInterns: Intern[] = [
- { id: 1, name: "Marcus Rodriguez", role: "Software Engineer Intern", initials: "MR", taskProgress: 10, taskTotal: 10, status: "ELIGIBLE" },
- { id: 2, name: "Priya Sharma", role: "Frontend Developer Intern", initials: "PS", taskProgress: 8, taskTotal: 10, status: "INCOMPLETE" },
- { id: 3, name: "James Smith", role: "QA Engineering Intern", initials: "JS", taskProgress: 10, taskTotal: 10, status: "ELIGIBLE" },
- { id: 4, name: "Nina Castillo", role: "Mobile Development Intern", initials: "NC", taskProgress: 9, taskTotal: 10, status: "ELIGIBLE" },
- { id: 5, name: "Derek Lim", role: "DevOps & Cloud Intern", initials: "DL", taskProgress: 7, taskTotal: 10, status: "INCOMPLETE" },
- { id: 6, name: "Alex Johnson", role: "Backend Engineering Intern", initials: "AJ", taskProgress: 10, taskTotal: 10, status: "ELIGIBLE" },
-];
-
-const TOTAL_ENTRIES = 1284;
-const PER_PAGE = 3;
-
-// ─── Avatar ───────────────────────────────────────────────────────────────────
-const avatarColors: Record
= {
- MR: "bg-blue-100 text-blue-700",
- PS: "bg-purple-100 text-purple-700",
- JS: "bg-green-100 text-green-700",
- NC: "bg-pink-100 text-pink-700",
- DL: "bg-teal-100 text-teal-700",
- AJ: "bg-orange-100 text-orange-700",
-};
-
-function Avatar({ initials }: { initials: string }) {
- const color = avatarColors[initials] ?? "bg-gray-100 text-gray-500";
- return (
-
- {initials}
-
- );
-}
-
-function ProgressBar({ value, total }: { value: number; total: number }) {
- const pct = Math.round((value / total) * 100);
- const isComplete = value === total;
- return (
-
-
-
- {value}/{total}
-
-
- );
-}
-
-function StatusBadge({ status }: { status: InternStatus }) {
- const isEligible = status === "ELIGIBLE";
- return (
-
- {isEligible
- ?
- :
}
- {status}
-
- );
-}
-
-export default function CertificateDetail() {
- const navigate = useNavigate();
-
- // Selected interns (checked)
- const initialSelected = new Set(
- mockInterns.filter((i) => i.status === "ELIGIBLE").map((i) => i.id)
- );
- const [selected, setSelected] = useState>(initialSelected);
- const [page, setPage] = useState(1);
-
- const totalPages = Math.ceil(mockInterns.length / PER_PAGE);
- const paginated = mockInterns.slice((page - 1) * PER_PAGE, page * PER_PAGE);
-
- const eligibleIds = mockInterns.filter((i) => i.status === "ELIGIBLE").map((i) => i.id);
- const allEligibleSelected = eligibleIds.every((id) => selected.has(id));
-
- const toggleSelectAll = () => {
- if (allEligibleSelected) {
- setSelected(new Set());
- } else {
- setSelected(new Set(eligibleIds));
- }
- };
-
- const toggleOne = (id: number, status: InternStatus) => {
- if (status === "INCOMPLETE") return; // tidak bisa dipilih
- setSelected((prev) => {
- const next = new Set(prev);
- next.has(id) ? next.delete(id) : next.add(id);
- return next;
- });
- };
-
- return (
-
-
-
Certificates
-
- Administrate, issue, and track professional certifications for all interns.
-
-
-
-
-
- {/* Status badge */}
-
-
- COMPLETED
-
-
-
- {mockProject.title}
-
-
-
-
- {mockProject.period}
-
-
-
-
- Project Scope
-
-
- {mockProject.scope}
-
-
-
-
-
-
{mockProject.totalInterns}
-
Total Interns
-
-
-
{mockProject.eligibleInterns}
-
Eligible
-
-
-
-
-
-
-
-
-
Intern Roster
-
- Confirm completion status before final certificate generation.
-
-
-
-
-
- {/* Column Headers */}
-
-
-
Intern Name
-
Task Progress
-
Status
-
Action
-
-
- {/* Rows */}
-
- {paginated.map((intern) => {
- const isChecked = selected.has(intern.id);
- const isIncomplete = intern.status === "INCOMPLETE";
- return (
-
- {/* Checkbox */}
-
-
- {/* Name + role */}
-
-
-
-
{intern.name}
-
{intern.role}
-
-
-
- {/* Task Progress */}
-
-
- {/* Status */}
-
-
-
-
- {/* Action */}
-
- {isIncomplete ? (
-
- ) : (
-
- )}
-
-
- );
- })}
-
-
- {/* Pagination Footer */}
-
-
- Showing {(page - 1) * PER_PAGE + 1} to {Math.min(page * PER_PAGE, mockInterns.length)} of {TOTAL_ENTRIES.toLocaleString()} entries
-
-
-
- {Array.from({ length: totalPages }, (_, i) => i + 1).map((n) => (
-
- ))}
-
-
-
-
-
-
- {/* ── Bottom Action Bar ── */}
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/src/Pages/internify/mentor/CertificateForm.tsx b/src/Pages/internify/mentor/CertificateForm.tsx
new file mode 100644
index 0000000..32138b4
--- /dev/null
+++ b/src/Pages/internify/mentor/CertificateForm.tsx
@@ -0,0 +1,188 @@
+import { useRef, useState } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import { CalendarDays, UploadCloud, CheckCircle2, XCircle, Loader2 } from "lucide-react";
+import { useUploadCertificateTemplate } from "../../../hooks/useSertificates";
+import { useProjectDetail } from "../../../hooks/useProjects";
+import EligibleInternTable from "./components/EligibleInternTable";
+import { useProjectInternProgress } from "../../../hooks/useInternProgress"
+
+export default function CertificateDetail() {
+ const navigate = useNavigate();
+ const { slug } = useParams<{ slug: string }>();
+ const { project, loading, error } = useProjectDetail(slug ?? "");
+ const { upload, loading: uploading, error: uploadError } = useUploadCertificateTemplate();
+ const { data: internProgress, loading: progressLoading } = useProjectInternProgress(slug ?? "");
+ const eligibleCount = internProgress.filter(i => i.is_eligible).length;
+
+ const fileInputRef = useRef(null);
+ const [selectedFile, setSelectedFile] = useState(null);
+ const [uploadSuccess, setUploadSuccess] = useState(false);
+
+ const handleFileChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ setSelectedFile(file);
+ setUploadSuccess(false);
+ };
+
+ const handleUpload = async () => {
+ if (!selectedFile || !project) return;
+ const result = await upload(project.id, selectedFile);
+ if (result) {
+ setUploadSuccess(true);
+ setSelectedFile(null);
+ if (fileInputRef.current) fileInputRef.current.value = "";
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+ Memuat data project...
+
+ );
+ }
+
+ if (error || !project) {
+ return (
+
+
+
{error ?? "Project tidak ditemukan."}
+
+
+ );
+ }
+
+ return (
+
+
+
Certificates
+
+ Administrate, issue, and track professional certifications for all interns.
+
+
+
+ {/* ── Left Column ── */}
+
+
+ {project.project_name}
+
+
+
+ {new Date(project.start_date).toLocaleDateString("en-US", { month: "short", year: "numeric" })}
+ {" — "}
+ {new Date(project.end_date).toLocaleDateString("en-US", { month: "short", year: "numeric" })}
+
+
+ {/* ── Upload Template ── */}
+
+
+
+ {/* Drop Zone */}
+
fileInputRef.current?.click()}
+ >
+
+
+
+ {selectedFile ? (
+ <>
+
+ {selectedFile.name}
+
+
+ {(selectedFile.size / 1024 / 1024).toFixed(2)} MB
+
+ >
+ ) : (
+ <>
+
Upload Certificate Image
+
PNG, JPG up to 5MB
+ >
+ )}
+
+
+
+
+ {/* Feedback */}
+ {uploadSuccess && (
+
+
+ Template berhasil diupload!
+
+ )}
+ {uploadError && (
+
+
+ {uploadError}
+
+ )}
+
+ {/* Upload Button */}
+ {selectedFile && (
+
+ )}
+
+
+ {/* ── Project Scope ── */}
+
+
+ Project Scope
+
+
{project.description}
+
+
+ {/* ── Stats ── */}
+
+
+
{project.total_members}
+
Total Interns
+
+
+
{eligibleCount}
+
Eligible
+
+
+
+
+ {/* ── Right Column ── */}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/Pages/internify/mentor/CertificateListPage.tsx b/src/Pages/internify/mentor/CertificateListPage.tsx
index 5fc708a..4bd03ea 100644
--- a/src/Pages/internify/mentor/CertificateListPage.tsx
+++ b/src/Pages/internify/mentor/CertificateListPage.tsx
@@ -23,7 +23,7 @@ export default function CertificateList() {
{/* Section: List Projects */}
-
List Projects
+
List Projects
{projects.map((project) => (
diff --git a/src/Pages/internify/mentor/CertificateGenerate.tsx b/src/Pages/internify/mentor/CertificateResult.tsx
similarity index 100%
rename from src/Pages/internify/mentor/CertificateGenerate.tsx
rename to src/Pages/internify/mentor/CertificateResult.tsx
diff --git a/src/Pages/internify/mentor/components/CertificateProjectCard.tsx b/src/Pages/internify/mentor/components/CertificateProjectCard.tsx
index 13cc837..3f216bf 100644
--- a/src/Pages/internify/mentor/components/CertificateProjectCard.tsx
+++ b/src/Pages/internify/mentor/components/CertificateProjectCard.tsx
@@ -3,7 +3,7 @@ import type { Project } from "../../../../types/project.types";
// ─── StatusBadge ─────────────────────────────────────────────────────────────
function StatusBadge({ status }: { status: string }) {
- const isCompleted = status === "COMPLETED";
+ const isCompleted = status === "archived";
return (
{/* Top: status badge */}
@@ -37,21 +50,21 @@ export default function CertificateProjectCard({
{/* Title + period */}
-
+
{project.project_name}
-
+
- {project.start_date} - {project.end_date}
+ {formatDateRange(project.start_date, project.end_date)}
-
+
{/* Intern count */}
-
Interns
+
Interns
{project.total_members} interns
diff --git a/src/Pages/internify/mentor/components/EligibleInternTable.tsx b/src/Pages/internify/mentor/components/EligibleInternTable.tsx
new file mode 100644
index 0000000..aaa9538
--- /dev/null
+++ b/src/Pages/internify/mentor/components/EligibleInternTable.tsx
@@ -0,0 +1,266 @@
+// src/pages/mentor/EligibleInternTable.tsx
+import { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import {
+ CheckCircle2,
+ AlertCircle,
+ Eye,
+ ChevronLeft,
+ ChevronRight,
+ Sparkles,
+} from "lucide-react";
+import type { InternSubmissionProgress } from "../../../../hooks/useInternProgress";
+import { useClaimCertificate } from "../../../../hooks/useSertificates";
+import { Loader2, Check } from "lucide-react";
+
+interface EligibleInternTableProps {
+ interns: InternSubmissionProgress[];
+ loading: boolean;
+ eligibleCount: number;
+}
+
+const PER_PAGE = 5;
+
+function ProgressBar({ value, total }: { value: number; total: number }) {
+ const pct = Math.round((value / total) * 100);
+ const isComplete = value === total;
+ return (
+
+
+
+ {value}/{total}
+
+
+ );
+}
+
+function StatusBadge({ status }: { status: boolean }) {
+ const isEligible = status
+ return (
+
+ {isEligible
+ ?
+ :
}
+ {status}
+
+ );
+}
+
+interface EligibleInternTableProps {
+ interns: InternSubmissionProgress[];
+ loading: boolean;
+ eligibleCount: number;
+}
+
+function Avatar({ initials }: { initials: string }) {
+ return (
+
+ {initials}
+
+ );
+}
+
+// ─── Component ────────────────────────────────────────────────────────────────
+export default function EligibleInternTable({ interns, loading, eligibleCount }: EligibleInternTableProps) {
+ const navigate = useNavigate();
+
+ const initialSelected = new Set(
+ interns.filter(i => i.is_eligible).map(i => i.id_user)
+ );
+ const [selected, setSelected] = useState
>(initialSelected);
+ 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 toggleSelectAll = () => {
+ if (allEligibleSelected) {
+ setSelected(new Set());
+ } else {
+ setSelected(new Set(eligibleIds));
+ }
+ };
+
+ const toggleOne = (id: number, status: boolean) => {
+ if (status) return;
+ setSelected((prev) => {
+ const next = new Set(prev);
+ next.has(id) ? next.delete(id) : next.add(id);
+ return next;
+ });
+ };
+
+ if (loading) {
+ return (
+
+
+ Memuat data intern...
+
+ );
+}
+
+ return (
+
+ {/* Roster Card */}
+
+
+
+
Intern Roster
+
+ Confirm completion status before final certificate generation.
+
+
+
+
+
+ {/* Column Headers */}
+
+
+
Intern Name
+
Task Progress
+
Status
+
Action
+
+
+ {/* Rows */}
+
+ {paginated.map((intern) => {
+ const isChecked = selected.has(intern.id_user);
+ const isIncomplete = !intern.is_eligible;
+ const initials = intern.full_name
+ .split(" ")
+ .slice(0, 2)
+ .map(n => n[0])
+ .join("")
+ .toUpperCase();
+ return (
+
+ {/* Checkbox */}
+
+
+ {/* Name + role */}
+
+ {intern.avatar ? (
+

+ ) : (
+
+ )}
+
+
{intern.full_name}
+
{intern.email}
+
+
+
+ {/* Task Progress */}
+
+
+ {/* Status */}
+
+
+
+
+ {/* Action */}
+
+ {isIncomplete ? (
+
+ ) : (
+
+ )}
+
+
+ );
+ })}
+
+
+ {/* Pagination Footer */}
+
+
+ Showing {(page - 1) * PER_PAGE + 1} to {Math.min(page * PER_PAGE, interns.length)} of {interns.length} entries
+
+
+
+ {Array.from({ length: totalPages }, (_, i) => i + 1).map((n) => (
+
+ ))}
+
+
+
+
+
+ {/* ── Bottom Action Bar ── */}
+
+
+
+
+ );
+}
diff --git a/src/Pages/internify/mentor/components/MentorProjectCard.tsx b/src/Pages/internify/mentor/components/MentorProjectCard.tsx
index fa9f6e1..bc33bb5 100644
--- a/src/Pages/internify/mentor/components/MentorProjectCard.tsx
+++ b/src/Pages/internify/mentor/components/MentorProjectCard.tsx
@@ -4,7 +4,11 @@ import * as LucideIcons from 'lucide-react';
import type { LucideProps } from 'lucide-react';
import type { Project } from '../../../../types/project.types';
-
+const getDynamicIcon = (iconName: string) => {
+ const formatted = iconName.charAt(0).toUpperCase() + iconName.slice(1);
+ const Icon = (LucideIcons as unknown as Record>)[formatted];
+ return Icon ?? LucideIcons.FolderOpen;
+ };
const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-');
diff --git a/src/hooks/useInternProgress.ts b/src/hooks/useInternProgress.ts
new file mode 100644
index 0000000..4b50354
--- /dev/null
+++ b/src/hooks/useInternProgress.ts
@@ -0,0 +1,81 @@
+// hooks/useProjectInternProgress.ts
+import { useState, useEffect, useCallback } from "react";
+import { getProjectTasks, getTaskSubmissions } from "../services/TaskService";
+import type { AdminTaskDetail } from "../types/task.types";
+
+export interface InternSubmissionProgress {
+ id_user: number;
+ full_name: string;
+ email: string;
+ avatar: string | null;
+ submitted: number;
+ total_tasks: number;
+ is_eligible: boolean;
+}
+
+export const useProjectInternProgress = (projectSlug: string) => {
+ const [data, setData] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const refetch = useCallback(async () => {
+ if (!projectSlug) return;
+ setLoading(true);
+ setError(null);
+
+ try {
+ const tasks = await getProjectTasks(projectSlug);
+
+ const taskDetails = await Promise.all(
+ tasks.map((task) =>
+ getTaskSubmissions(task.slug, projectSlug) as Promise
+ )
+ );
+
+ const internMap = new Map();
+
+ for (const taskDetail of taskDetails) {
+ 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);
+
+ if (existing) {
+ existing.total_tasks += 1;
+ if (hasSubmitted) existing.submitted += 1;
+ } else {
+ internMap.set(sub.id_user, {
+ id_user: sub.id_user,
+ full_name: sub.full_name,
+ email: sub.email,
+ avatar: sub.profile_picture, // field yang benar
+ submitted: hasSubmitted ? 1 : 0,
+ total_tasks: 1,
+ is_eligible: false,
+ });
+ }
+ }
+ }
+
+ const result = Array.from(internMap.values()).map((intern) => ({
+ ...intern,
+ is_eligible:
+ intern.total_tasks > 0 && intern.submitted === intern.total_tasks,
+ }));
+
+ setData(result);
+ } catch {
+ setError("Gagal memuat progress intern.");
+ } finally {
+ setLoading(false);
+ }
+ }, [projectSlug]);
+
+ useEffect(() => {
+ refetch();
+ }, [refetch]);
+
+ return { data, loading, error, refetch };
+};
\ No newline at end of file
diff --git a/src/services/ProjectService.ts b/src/services/ProjectService.ts
index 5f5e2e6..27f5213 100644
--- a/src/services/ProjectService.ts
+++ b/src/services/ProjectService.ts
@@ -53,7 +53,8 @@ export const getProjectById = async (
): Promise => {
const res = await api.get(`/project-api/get/${id}`);
const raw = res.data.data;
-
+ console.log("raw fields:", Object.keys(raw));
+console.log("raw full:", raw);
return {
...raw,
members: raw.members.map((m: any) => ({
diff --git a/src/types/project.types.ts b/src/types/project.types.ts
index b33911e..5825a66 100644
--- a/src/types/project.types.ts
+++ b/src/types/project.types.ts
@@ -1,4 +1,4 @@
-// types/project.types.ts
+import type { ProjectTask } from "./task.types";
export type ProjectStatus = "active" | "completed" | "archived";
export type MemberStatus = "active" | "inactive";
@@ -33,6 +33,7 @@ export interface Project {
// Detail project (includes members array)
export interface ProjectDetail extends Project {
members: ProjectMember[];
+ tasks: ProjectTask[];
}
// Response GET /project-api/my-tasks