-
+
{deadlineDate.toLocaleDateString("id-ID")} AT{" "}
{deadlineDate.toLocaleTimeString("id-ID", {
hour: "2-digit",
diff --git a/src/Pages/internify/mentor/components/EligibleInternTable.tsx b/src/Pages/internify/mentor/components/EligibleInternTable.tsx
index b8cb517..b1bf1a7 100644
--- a/src/Pages/internify/mentor/components/EligibleInternTable.tsx
+++ b/src/Pages/internify/mentor/components/EligibleInternTable.tsx
@@ -45,6 +45,7 @@ export default function EligibleInternTable({
loading,
eligibleCount,
projectId
+ // Durasi tanggal Proyek
}: EligibleInternTableProps) {
const navigate = useNavigate();
const { project } = useProjectDetail(String(projectId));
@@ -98,14 +99,15 @@ export default function EligibleInternTable({
await Promise.all(
result.map(async (cert) => {
- const verifyUrl = `${window.location.origin}/verify/${cert.uuid}`;
+ const verifyUrl = `${window.location.origin}/verify-certificate/${cert.uuid}`;
const blob = await generateCertificate(
templateUrl,
cert.user.full_name,
cert.project.project_name,
cert.certificate_no,
- verifyUrl
+ verifyUrl,
+ "20 Juni 2024 - 20 September 2024" // Placeholder untuk durasi proyek
);
zip.file(`Sertifikat - ${cert.user.full_name}.png`, blob);
diff --git a/src/Pages/utils/SertificateGenerator.ts b/src/Pages/utils/SertificateGenerator.ts
index ed11657..ec6d782 100644
--- a/src/Pages/utils/SertificateGenerator.ts
+++ b/src/Pages/utils/SertificateGenerator.ts
@@ -39,6 +39,7 @@ export async function generateCertificate(
internName: string,
projectName: string,
certificateNo: string,
+ ProjectDuration: string,
verifyUrl: string,
): Promise {
// Pastikan font siap sebelum menggambar teks
@@ -78,8 +79,13 @@ export async function generateCertificate(
ctx.fillStyle = "#800000";
ctx.fillText(certificateNo, canvas.width * 0.825, canvas.height * 0.918);
+ //Durasi Project — besar (Grenze)
+ ctx.font = `50px "Grenze", serif`;
+ ctx.fillStyle = "#090909";
+ ctx.fillText(ProjectDuration, canvas.width * 0.57, canvas.height * 0.715);
+
// QR code — kalau verifyUrl diisi
- const qrDataUrl = await QRCode.toDataURL(verifyUrl, {
+ const qrDataUrl = await QRCode.toDataURL('http://localhost:5172/verify-certificate/' + verifyUrl, {
width: 200,
margin: 1,
});
@@ -87,8 +93,8 @@ export async function generateCertificate(
const qrSize = canvas.width * 0.103;
ctx.drawImage(
qrImg,
- canvas.width * 0.77 - qrSize / 2, // posisi X — sesuaikan
- canvas.height * 0.715 - qrSize / 2, // posisi Y — sesuaikan
+ canvas.width * 0.77 - qrSize,
+ canvas.height * 0.715 - qrSize,
qrSize,
qrSize,
);
diff --git a/src/Pages/utils/showToast.tsx b/src/Pages/utils/showToast.tsx
new file mode 100644
index 0000000..e0940a4
--- /dev/null
+++ b/src/Pages/utils/showToast.tsx
@@ -0,0 +1,67 @@
+// utils/toast.tsx
+import toast, { type Toast } from 'react-hot-toast';
+import { FiXCircle, FiX } from 'react-icons/fi';
+import { FaCheck } from "react-icons/fa";
+import { IoAlertOutline } from "react-icons/io5";
+
+function ToastCard({ t, type, title, description }: {
+ t: Toast;
+ type: 'success' | 'error';
+ title: string;
+ description?: string;
+}) {
+ const isSuccess = type === 'success';
+ return (
+
+
+ {isSuccess ? : }
+
+
+
+ {title}
+
+ {description &&
{description}
}
+
+
+
+ );
+}
+
+export const customToast = {
+ success: (title: string, description?: string) =>
+ toast.custom((t) => ),
+
+ error: (title: string, description?: string) =>
+ toast.custom((t) => ),
+
+ promise: async (
+ promise: Promise,
+ messages: {
+ loading: string;
+ success: (data: T) => { title: string; description?: string };
+ error: (err: any) => { title: string; description?: string };
+ }
+ ): Promise => {
+ const loadingId = toast.loading(messages.loading);
+ try {
+ const data = await promise;
+ toast.dismiss(loadingId);
+ const { title, description } = messages.success(data);
+ customToast.success(title, description);
+ return data;
+ } catch (err) {
+ toast.dismiss(loadingId);
+ const { title, description } = messages.error(err);
+ customToast.error(title, description);
+ throw err;
+ }
+ },
+};
\ No newline at end of file
diff --git a/src/hooks/useTasks.ts b/src/hooks/useTasks.ts
index 36867d4..a24ce59 100644
--- a/src/hooks/useTasks.ts
+++ b/src/hooks/useTasks.ts
@@ -1,6 +1,13 @@
// hooks/useTasks.ts
import { useState, useEffect, useCallback, useMemo } from "react";
-import type { ProjectTask, ProjectTaskDetail, CreateTaskPayload, UpdateTaskPayload, TaskSubmissionData, AdminTaskDetail } from "../types/task.types";
+import type {
+ ProjectTask,
+ ProjectTaskDetail,
+ CreateTaskPayload,
+ UpdateTaskPayload,
+ TaskSubmissionData,
+ AdminTaskDetail,
+} from "../types/task.types";
import {
getProjectTasks,
getTaskById,
@@ -34,7 +41,9 @@ export const useProjectTasks = (projectId: string) => {
.finally(() => setLoading(false));
}, [projectId]);
- useEffect(() => { refetch(); }, [refetch]);
+ useEffect(() => {
+ refetch();
+ }, [refetch]);
return { tasks, loading, error, refetch }; // tambah refetch
};
@@ -54,7 +63,9 @@ export const useTaskDetail = (taskId: string, projectId?: string) => {
.finally(() => setLoading(false));
}, [taskId, projectId]);
- useEffect(() => { refetch(); }, [refetch]);
+ useEffect(() => {
+ refetch();
+ }, [refetch]);
return { task, loading, error, refetch };
};
@@ -73,7 +84,9 @@ export const useTaskSubmissions = (taskId: string, projectId?: string) => {
.finally(() => setLoading(false));
}, [taskId, projectId]);
- useEffect(() => { refetch(); }, [refetch]);
+ useEffect(() => {
+ refetch();
+ }, [refetch]);
return { task, loading, error, refetch };
};
@@ -83,7 +96,9 @@ export const useCreateTask = (projectId: string) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
- const create = async (payload: CreateTaskPayload): Promise => {
+ const create = async (
+ payload: CreateTaskPayload,
+ ): Promise => {
setLoading(true);
setError(null);
try {
@@ -107,7 +122,7 @@ export const useUpdateTask = () => {
const update = async (
taskId: string | number,
payload: UpdateTaskPayload,
- projectId?: string
+ projectId?: string,
) => {
setLoading(true);
setError(null);
@@ -147,62 +162,102 @@ export const useDeleteTask = () => {
};
// POST + PATCH + DELETE submission — digabung dalam satu hook per task
-export const useSubmission = (taskId: string, initialSubmission?: TaskSubmissionData | null, projectId?: string) => {
- const [submission, setSubmission] = useState(initialSubmission ?? null);
+export const useSubmission = (
+ taskId: string,
+ initialSubmission?: TaskSubmissionData | null,
+ projectId?: string,
+) => {
+ const [submission, setSubmission] = useState(
+ initialSubmission ?? null,
+ );
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
- const submitFile = async (file: File) => {
- setLoading(true); setError(null);
+ const submitFile = async (files: File[]) => {
+ setLoading(true);
+ setError(null);
try {
- const res = await submitTaskFile(taskId, file, projectId);
+ const res = await submitTaskFile(taskId, files, projectId);
if (res) setSubmission(res);
return res;
- } catch { setError("Gagal submit file."); return null; }
- finally { setLoading(false); }
+ } catch {
+ setError("Gagal submit file.");
+ return null;
+ } finally {
+ setLoading(false);
+ }
};
const submitLink = async (url: string) => {
- setLoading(true); setError(null);
+ setLoading(true);
+ setError(null);
try {
const res = await submitTaskLink(taskId, url, projectId);
if (res) setSubmission(res);
return res;
- } catch { setError("Gagal submit link."); return null; }
- finally { setLoading(false); }
+ } catch {
+ setError("Gagal submit link.");
+ return null;
+ } finally {
+ setLoading(false);
+ }
};
- const updateFile = async (submissionId: number, file: File) => {
- setLoading(true); setError(null);
+ const updateFile = async (submissionId: number, files: File[]) => {
+ setLoading(true);
+ setError(null);
try {
- const res = await updateSubmissionFile(submissionId, file);
+ const res = await updateSubmissionFile(submissionId, files);
if (res) setSubmission(res);
return res;
- } catch { setError("Gagal update submission."); return null; }
- finally { setLoading(false); }
+ } catch {
+ setError("Gagal update submission.");
+ return null;
+ } finally {
+ setLoading(false);
+ }
};
const updateLink = async (submissionId: number, url: string) => {
- setLoading(true); setError(null);
+ setLoading(true);
+ setError(null);
try {
const res = await updateSubmissionLink(submissionId, url);
if (res) setSubmission(res);
return res;
- } catch { setError("Gagal update submission."); return null; }
- finally { setLoading(false); }
+ } catch {
+ setError("Gagal update submission.");
+ return null;
+ } finally {
+ setLoading(false);
+ }
};
const remove = async (submissionId: number) => {
- setLoading(true); setError(null);
+ setLoading(true);
+ setError(null);
try {
await deleteSubmission(submissionId);
setSubmission(null);
return true;
- } catch { setError("Gagal hapus submission."); return false; }
- finally { setLoading(false); }
+ } catch {
+ setError("Gagal hapus submission.");
+ return false;
+ } finally {
+ setLoading(false);
+ }
};
- return { submission, submitFile, submitLink, updateFile, updateLink, remove, loading, error };
+ return {
+ submission,
+ submitFile,
+ submitLink,
+ updateFile,
+ updateLink,
+ remove,
+ loading,
+ error,
+ };
};
// hooks/useAllMentorTasks.ts
@@ -233,10 +288,16 @@ export const useDeadlines = () => {
const isIntern = user?.role === "intern";
- const { tasks: internTasks, loading: internLoading, error: internError } = useMyTasks();
- const { tasks: mentorTasks, loading: mentorLoading, error: mentorError } = useAllMentorTasks(
- !isIntern && user !== null
- );
+ const {
+ tasks: internTasks,
+ loading: internLoading,
+ error: internError,
+ } = useMyTasks();
+ const {
+ tasks: mentorTasks,
+ loading: mentorLoading,
+ error: mentorError,
+ } = useAllMentorTasks(!isIntern && user !== null);
const deadlines: Deadline[] = useMemo(() => {
if (!user) return [];
@@ -256,4 +317,4 @@ export const useDeadlines = () => {
const error = userError || (isIntern ? internError : mentorError);
return { deadlines, loading, error, isIntern };
-};
\ No newline at end of file
+};
diff --git a/src/services/TaskService.ts b/src/services/TaskService.ts
index e1a87ab..f78ffa8 100644
--- a/src/services/TaskService.ts
+++ b/src/services/TaskService.ts
@@ -91,11 +91,11 @@ export const getAllMentorTasks = async (): Promise => {
export const submitTaskFile = async (
taskId: string | number,
- file: File,
+ files: File[],
projectId?: string | number
): Promise => {
const formData = new FormData();
- formData.append("submission_file", file);
+ files.forEach((file) => formData.append("submission_files", file));
const res = await api.post(`/task-api/tasks/${taskId}/submissions`, formData, {
params: projectId ? { project: projectId } : undefined,
});
@@ -117,10 +117,10 @@ export const submitTaskLink = async (
export const updateSubmissionFile = async (
submissionId: number,
- file: File
+ files: File[]
): Promise => {
const formData = new FormData();
- formData.append("submission_file", file);
+ files.forEach((file) => formData.append("submission_files", file));
const res = await api.patch(`/task-api/submissions/${submissionId}`, formData);
return res.data.data;
};
diff --git a/src/types/task.types.ts b/src/types/task.types.ts
index 67437f7..f5e8704 100644
--- a/src/types/task.types.ts
+++ b/src/types/task.types.ts
@@ -20,6 +20,15 @@ export interface TaskSubmissionData {
id_task: number;
id_user: number;
file_path: string | null;
+ files: Array<{
+ id: number;
+ file_path: string;
+ original_name: string;
+ mime_type: string;
+ size_bytes: number;
+ created_at: string;
+ }>
+ file_paths:string[];
url_link: string | null;
submitted_at: string;
updated_at: string;