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:
@@ -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 };
|
||||
};
|
||||
Reference in New Issue
Block a user