feat: add CertificateForm and CertificateResult components for managing intern certificates

- Implemented CertificateForm for uploading certificate templates and displaying project details.
- Created CertificateResult to show generated certificates with pagination and search functionality.
- Added EligibleInternTable component to display eligible interns and their progress.
- Updated CertificateListPage to reflect changes in project listing styles.
- Enhanced MentorProjectCard with dynamic icon rendering.
- Introduced useProjectInternProgress hook for fetching intern progress data.
- Modified ProjectService to include task details in project responses.
- Updated project types to accommodate new task structures.
This commit is contained in:
Kemas M. Aryadary Rasyad
2026-06-28 22:22:03 +07:00
parent 37e21a1f39
commit 732684200f
17 changed files with 612 additions and 371 deletions
+81
View File
@@ -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<InternSubmissionProgress[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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<AdminTaskDetail>
)
);
const internMap = new Map<number, InternSubmissionProgress>();
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 };
};