+
{type === "file" ? "File submission" : "Link submission"}
{type === "file" ? (
-
-
- {filePath ? filePath.split("/").pop() : "File submitted"}
-
+
+
+ {resolveImageUrl(filePath ?? "").split("/").pop()}
+
) : (
diff --git a/src/Pages/internify/intern/components/TaskForm-file.tsx b/src/Pages/internify/intern/components/TaskForm-file.tsx
index 9b1ce84..2360778 100644
--- a/src/Pages/internify/intern/components/TaskForm-file.tsx
+++ b/src/Pages/internify/intern/components/TaskForm-file.tsx
@@ -65,7 +65,12 @@ export default function TaskFormFile({ taskId, projectId, deadline, initialSubmi
if (ok) { setFiles([]); setIsEditing(false); }
};
- // Sudah ada submission dan tidak sedang edit -> tampilkan status
+ // Batal edit — kembali ke tampilan status tanpa kehilangan submission lama
+ const handleCancelEdit = () => {
+ setFiles([]);
+ setIsEditing(false);
+ };
+
if (submission && !isEditing) {
return (
{files.length > 0 ? (
+ // File baru sudah dipilih — tampilkan itu (menggantikan file lama)
{files.map((file, index) => (
e.stopPropagation()}>
@@ -110,6 +118,17 @@ export default function TaskFormFile({ taskId, projectId, deadline, initialSubmi
))}
+ ) : isEditing && currentFileName ? (
+ // Mode edit, belum pilih file baru — tampilkan file submission lama sebagai referensi
+
+
+
+ File saat ini: {currentFileName}
+
+
+ Klik atau drag & drop untuk mengganti file
+
+
) : (
@@ -118,13 +137,25 @@ export default function TaskFormFile({ taskId, projectId, deadline, initialSubmi
)}
-
+
+
+ {isEditing && (
+
+ )}
+
+
);
}
\ No newline at end of file
diff --git a/src/Pages/internify/mentor/components/EligibleInternTable.tsx b/src/Pages/internify/mentor/components/EligibleInternTable.tsx
index 42f2bd0..b8cb517 100644
--- a/src/Pages/internify/mentor/components/EligibleInternTable.tsx
+++ b/src/Pages/internify/mentor/components/EligibleInternTable.tsx
@@ -14,7 +14,7 @@ import {
import type { InternSubmissionProgress } from "../../../../hooks/useInternProgress";
import { useProjectDetail } from "../../../../hooks/useProjects";
import JSZip from "jszip";
-import { generateCertificate } from "../../../utils/SertificateGenerator";
+import { generateCertificate, resolveImageUrl } from "../../../utils/SertificateGenerator";
import { useGenerateCertificates } from "../../../../hooks/useCertificates";
import Avatar from "../../Avatar";
import ProgressBar from "../../ProgressBar";
@@ -40,15 +40,15 @@ function StatusBadge({ status }: { status: boolean }) {
}
// Main Component
-export default function EligibleInternTable({
- interns,
- loading,
- eligibleCount,
- projectId
+export default function EligibleInternTable({
+ interns,
+ loading,
+ eligibleCount,
+ projectId
}: EligibleInternTableProps) {
const navigate = useNavigate();
const { project } = useProjectDetail(String(projectId));
- const { generate: SubmitCertificates, loading: generatingCertificates, error, success, reset } = useGenerateCertificates();
+ const { generate: SubmitCertificates } = useGenerateCertificates();
const initialSelected = new Set(
interns.filter(i => i.is_eligible).map(i => i.id_user)
@@ -57,7 +57,6 @@ export default function EligibleInternTable({
const [generating, setGenerating] = useState(false);
const [generateError, setGenerateError] = useState
(null);
- // Halaman (Pagination)
const [page, setPage] = useState(1);
const totalPages = Math.ceil(interns.length / PER_PAGE);
const paginated = interns.slice((page - 1) * PER_PAGE, page * PER_PAGE);
@@ -78,48 +77,52 @@ export default function EligibleInternTable({
};
const handleGenerate = async () => {
- if (!project?.certificate_template) {
- setGenerateError("Template sertifikat belum diupload.");
- return;
- }
- if (selected.size === 0) {
- setGenerateError("Pilih minimal satu intern.");
- return;
- }
+ if (!project?.certificate_template) {
+ setGenerateError("Template sertifikat belum diupload.");
+ return;
+ }
+ if (selected.size === 0) {
+ setGenerateError("Pilih minimal satu intern.");
+ return;
+ }
- setGenerating(true);
- setGenerateError(null);
+ setGenerating(true);
+ setGenerateError(null);
- try {
- // 1. Generate blob PNG per intern & kumpulkan ke ZIP
- const selectedInterns = interns.filter((i) => selected.has(i.id_user));
- const zip = new JSZip();
+ try {
+ const result = await SubmitCertificates(projectId, Array.from(selected));
+ if (!result) return;
- await Promise.all(
- selectedInterns.map(async (intern) => {
- const blob = await generateCertificate(
- project.certificate_template,
- intern.full_name
+ const zip = new JSZip();
+ const templateUrl = resolveImageUrl(project.certificate_template);
+
+ await Promise.all(
+ result.map(async (cert) => {
+ const verifyUrl = `${window.location.origin}/verify/${cert.uuid}`;
+
+ const blob = await generateCertificate(
+ templateUrl,
+ cert.user.full_name,
+ cert.project.project_name,
+ cert.certificate_no,
+ verifyUrl
);
- zip.file(`Sertifikat - ${intern.full_name}.png`, blob);
- })
- );
- // 2. POST ke backend untuk mencatat sertifikat sudah di-generate
- const ok = await SubmitCertificates(projectId, Array.from(selected));
- if (!ok) return;
+ zip.file(`Sertifikat - ${cert.user.full_name}.png`, blob);
+ })
+ );
- // 3. Navigasi ke result page
- navigate(`/mentor/certificates/${projectId}/result`);
- } catch (err) {
- console.error(err);
- setGenerateError(
- err instanceof Error ? err.message : "Gagal generate sertifikat."
- );
- } finally {
- setGenerating(false);
- }
-};
+ // 3. Navigasi ke result page
+ navigate(`/mentor/certificates/${projectId}/result`);
+ } catch (err) {
+ console.error(err);
+ setGenerateError(
+ err instanceof Error ? err.message : "Gagal generate sertifikat."
+ );
+ } finally {
+ setGenerating(false);
+ }
+ };
if (loading) {
return (
@@ -281,10 +284,10 @@ export default function EligibleInternTable({
{/* Bottom Action Bar */}
+ className="text-gray-700 hover:underline"
+ onClick={() => navigate(`/mentor/certificates/${project?.slug}/result`)}>
+ Ke Hasil
+
)}
-
+
);
diff --git a/src/Pages/utils/SertificateGenerator.ts b/src/Pages/utils/SertificateGenerator.ts
index 0d22a13..ed11657 100644
--- a/src/Pages/utils/SertificateGenerator.ts
+++ b/src/Pages/utils/SertificateGenerator.ts
@@ -1,9 +1,6 @@
import api from "../../lib/api";
import QRCode from "qrcode";
-// Flag supaya font hanya di-load sekali (tidak berulang tiap generate)
-let fontsLoaded = false;
-
export function resolveImageUrl(path: string): string {
if (!path) return "";
if (path.startsWith("data:")) return path; // untuk QR data URL
@@ -36,13 +33,13 @@ export async function ensureFontsLoaded(): Promise