From 62aaf2bcbadaf3bf30fb5b6f3f8dbf0b96d7db0c Mon Sep 17 00:00:00 2001
From: "Kemas M. Aryadary Rasyad"
<112590882+aryadary0706@users.noreply.github.com>
Date: Thu, 16 Jul 2026 22:50:56 +0700
Subject: [PATCH] feat: enhance project and mentor management features
- Updated MentorProjectCard to provide more detailed success/error messages upon archiving and completing projects.
- Adjusted layout for certificate generation to improve visual alignment.
- Introduced resolveErrorMessage utility to standardize error message handling across the application.
- Enhanced customToast functionality to handle hook failures more gracefully.
- Refactored useCertificates hook to return structured responses for certificate claims and generation.
- Updated useMentors hook to improve error handling and response structure for mentor operations.
- Improved useNotification hook to return structured responses for marking notifications as read.
- Refactored useProjects hook to provide structured responses for project creation, updates, and member assignments.
- Enhanced useTasks hook to return structured responses for task creation, updates, and deletions.
- Updated services to return structured responses with messages for various API calls related to certificates, mentors, notifications, projects, tasks, and user management.
- Moved mentor-related types to a dedicated user.types file for better organization.
---
src/Pages/internify/LoginInternify.tsx | 1 +
src/Pages/internify/NotificationList.tsx | 64 +++++--------
src/Pages/internify/SettingsPage.tsx | 10 +-
.../intern/components/CertificateAvailabe.tsx | 6 +-
.../intern/components/TaskForm-file.tsx | 15 ++-
.../intern/components/TaskForm-link.tsx | 27 ++----
.../internify/mentor/CertificateForm.tsx | 10 +-
src/Pages/internify/mentor/InternsPage.tsx | 6 +-
src/Pages/internify/mentor/ProjectTaskTab.tsx | 20 ++--
.../internify/mentor/ProjectsInternTab.tsx | 6 +-
.../mentor/components/AddInternsDialog.tsx | 9 +-
.../components/AddInternsToProjects.tsx | 6 +-
.../mentor/components/CertResultTable.tsx | 6 +-
.../mentor/components/CreateProjectDialog.tsx | 31 +++++--
.../mentor/components/CreateTaskDialog.tsx | 12 +--
.../mentor/components/EditSubmissionTask.tsx | 8 +-
.../mentor/components/EligibleInternTable.tsx | 7 +-
.../mentor/components/MentorProjectCard.tsx | 14 +--
src/Pages/utils/SertificateGenerator.ts | 2 +-
src/Pages/utils/resolveObjectError.ts | 12 +++
src/Pages/utils/showToast.tsx | 12 ++-
src/hooks/useCertificates.ts | 68 +++++++-------
src/hooks/useMentors.ts | 39 ++++----
src/hooks/useNotification.ts | 18 +++-
src/hooks/useProjects.ts | 60 +++++++-----
src/hooks/useTasks.ts | 93 ++++++++++---------
src/hooks/useUser.ts | 78 +++-------------
src/services/CertificateService.ts | 17 ++--
src/services/MentorServices.ts | 53 ++---------
src/services/NotificationService.ts | 13 ++-
src/services/ProjectService.ts | 24 ++---
src/services/TaskService.ts | 39 ++++----
src/services/UserService.ts | 12 ++-
src/types/user.types.ts | 35 +++++++
34 files changed, 408 insertions(+), 425 deletions(-)
create mode 100644 src/Pages/utils/resolveObjectError.ts
diff --git a/src/Pages/internify/LoginInternify.tsx b/src/Pages/internify/LoginInternify.tsx
index 169b90c..b87c80e 100644
--- a/src/Pages/internify/LoginInternify.tsx
+++ b/src/Pages/internify/LoginInternify.tsx
@@ -46,6 +46,7 @@ export default function InternifyLogin() {
document.cookie = `token=${token}; path=/; SameSite=Strict`;
const decoded = decodeJWT(token);
+ console.log(decoded)
const role = decoded?.role;
if (role === "intern") {
navigate("/intern");
diff --git a/src/Pages/internify/NotificationList.tsx b/src/Pages/internify/NotificationList.tsx
index 3a4c999..abd781a 100644
--- a/src/Pages/internify/NotificationList.tsx
+++ b/src/Pages/internify/NotificationList.tsx
@@ -1,6 +1,6 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
-import { Check, Download, Info } from "lucide-react";
+import { Check, Download } from "lucide-react";
import { LuFileSpreadsheet } from "react-icons/lu";
import { io } from "socket.io-client";
import * as NotificationService from "../../services/NotificationService";
@@ -66,14 +66,25 @@ export default function NotificationList() {
};
}, []);
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleMarkAllRead = async () => {
try {
- await NotificationService.markAllAsRead();
+ const res = await NotificationService.markAllAsRead();
setNotifications([]);
- customToast.success("All notifications marked as read!");
- } catch (err:any) {
- customToast.error("Failed to mark all as read", err?.message);
+ customToast.success("Berhasil", res.message);
+ } catch (err: any) {
+ const msg = err?.response?.data?.message ?? "Gagal menandai semua notifikasi.";
+ customToast.error("Gagal", msg);
+ }
+ };
+
+ const handleReadMessage = async (id: number) => {
+ try {
+ const res = await NotificationService.markAsRead(id);
+ setNotifications(prev => prev.filter(n => n.id !== id));
+ customToast.success("Ditandai dibaca", res.message);
+ } catch (err: any) {
+ const msg = err?.response?.data?.message ?? "Gagal menandai notifikasi.";
+ customToast.error("Gagal", msg);
}
};
@@ -98,7 +109,7 @@ export default function NotificationList() {
{notifications.length === 0 && (
-

+
Belum ada pesan notifikasi yang diterima
)}
@@ -110,7 +121,7 @@ export default function NotificationList() {
{notifications.filter(n => n.group === "TODAY").map((item) => (
{/* Icon Container */}
@@ -137,16 +148,19 @@ export default function NotificationList() {
{/* Conditional Actions */}
- {item.type === 'new-task' && item.link &&(
+ {item.type === 'new-task' && item.link && (
- {/*
)}
@@ -167,34 +181,6 @@ export default function NotificationList() {
))}
-
- {/* Header Section YESTERDAY */}
-
- {/*
- YESTERDAY
-
*/}
-
- {notifications.filter(n => n.group === "YESTERDAY").map((item) => (
-
-
-
-
-
-
{item.title}
-
- {item.description}
-
-
-
- {item.time}
-
-
- ))}
-
-
);
diff --git a/src/Pages/internify/SettingsPage.tsx b/src/Pages/internify/SettingsPage.tsx
index 49cdf8e..f83d679 100644
--- a/src/Pages/internify/SettingsPage.tsx
+++ b/src/Pages/internify/SettingsPage.tsx
@@ -6,7 +6,7 @@ import UpdateProfileDialog from "./UpdateProfileDialogue";
export default function SettingsPage() {
const { user, loading: userLoading } = useCurrentUser();
- const { save, loading: saving, msgRef } = useUpdateProfile();
+ const { save, loading: saving } = useUpdateProfile();
const [confirmOpen, setConfirmOpen] = useState(false);
const isIntern = user?.role === "intern";
@@ -93,15 +93,15 @@ export default function SettingsPage() {
}),
{
loading: "Menyimpan perubahan...",
- success: () => ({
+ success: (updated) => ({
title: "Perubahan berhasil disimpan!",
description: emailChanged
? "Email berubah, silakan login ulang..."
- : msgRef.current.success ?? "",
+ : updated.message
}),
- error: () => ({
+ error: (err) => ({
title: "Gagal menyimpan perubahan!",
- description: msgRef.current.error ?? "",
+ description: err.message ?? "",
}),
}
);
diff --git a/src/Pages/internify/intern/components/CertificateAvailabe.tsx b/src/Pages/internify/intern/components/CertificateAvailabe.tsx
index 86d0e80..94661df 100644
--- a/src/Pages/internify/intern/components/CertificateAvailabe.tsx
+++ b/src/Pages/internify/intern/components/CertificateAvailabe.tsx
@@ -62,9 +62,8 @@ export default function CertificateAvailable({ project, certificate, templateUrl
setDownloadingImg(true);
try {
await downloadCertificateImage(certificate, templateUrl, "png");
- customToast.success("Sertifikat berhasil diunduh!");
} catch {
- customToast.error("Gagal mengunduh sertifikat!");
+ customToast.error("Gagal mengunduh sertifikat!", "Terjadi kendala saat mengunduh sertifikat!");
} finally {
setDownloadingImg(false);
}
@@ -74,9 +73,8 @@ export default function CertificateAvailable({ project, certificate, templateUrl
setDownloadingPdf(true);
try {
await downloadCertificatePdf(certificate, templateUrl);
- customToast.success("Sertifikat berhasil diunduh!");
} catch {
- customToast.error("Gagal mengunduh sertifikat!");
+ customToast.error("Gagal mengunduh sertifikat!", "Terjadi kendala saat mengunduh sertifikat!");
} finally {
setDownloadingPdf(false);
}
diff --git a/src/Pages/internify/intern/components/TaskForm-file.tsx b/src/Pages/internify/intern/components/TaskForm-file.tsx
index 2edf76a..ed4bf8c 100644
--- a/src/Pages/internify/intern/components/TaskForm-file.tsx
+++ b/src/Pages/internify/intern/components/TaskForm-file.tsx
@@ -54,7 +54,7 @@ export default function TaskFormFile({ taskId, projectId, deadline, initialSubmi
if (files.length === 0) {
customToast.error(
"File belum dipilih!",
- "Silakan pilih file terlebih dahulu sebelum mengirim."
+ "Choose your file before submit it!"
);
return;
}
@@ -63,11 +63,12 @@ export default function TaskFormFile({ taskId, projectId, deadline, initialSubmi
isEditing ? updateFile(submission!.id, files) : submitFile(files),
{
loading: isEditing ? "Memperbarui file..." : "Mengirim file...",
- success: () => ({
+ success: (res) => ({
title: isEditing ? "File berhasil diperbarui!" : "File berhasil dikirim!",
- description: isEditing
- ? "Perubahan submission Anda telah disimpan."
- : "Submission Anda telah berhasil dikirim.",
+ description: res?.message ??
+ (isEditing
+ ? "Perubahan submission Anda telah disimpan."
+ : "Link berhasil dikirim."),
}),
error: (err) => ({
title: isEditing ? "Gagal memperbarui file!" : "Gagal mengirim file!",
@@ -86,8 +87,6 @@ export default function TaskFormFile({ taskId, projectId, deadline, initialSubmi
};
const handleDelete = async () => {
- if (!confirm("Yakin ingin menghapus submission ini?")) return;
-
try {
const ok = await remove(submission!.id);
if (ok) {
@@ -95,7 +94,7 @@ export default function TaskFormFile({ taskId, projectId, deadline, initialSubmi
setIsEditing(false);
customToast.success(
"Submission berhasil dihapus!",
- "Anda dapat mengirim submission baru kapan saja."
+ ok.message
);
}
} catch (err: any) {
diff --git a/src/Pages/internify/intern/components/TaskForm-link.tsx b/src/Pages/internify/intern/components/TaskForm-link.tsx
index 40cc819..84d46ee 100644
--- a/src/Pages/internify/intern/components/TaskForm-link.tsx
+++ b/src/Pages/internify/intern/components/TaskForm-link.tsx
@@ -27,24 +27,18 @@ export default function TaskFormLink({
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
- if (!inputLink.trim()) {
- customToast.error(
- "Link belum dibuat",
- "Masukkan link untuk mengirim submission"
- );
- return;
- }
-
try {
await customToast.promise(
isEditing ? updateLink(submission!.id, inputLink) : submitLink(inputLink),
{
loading: isEditing ? "Memperbarui Link..." : "Mengirim Link...",
- success: () => ({
+ success: (res) => ({
title: isEditing ? "Link diperbaharui" : "Link berhasil dikirim",
- description: isEditing
- ? "Perubahan submission Anda telah disimpan."
- : "Submission Anda telah berhasil dikirim.",
+ description:
+ res?.message ??
+ (isEditing
+ ? "Perubahan submission Anda telah disimpan."
+ : "Link berhasil dikirim."),
}),
error: (err) => ({
title: isEditing ? "Gagal memperbarui link!" : "Gagal mengirim link!",
@@ -56,13 +50,10 @@ export default function TaskFormLink({
}
);
setIsEditing(false);
- } catch {
- // error sudah ditangani toast, tetap di form
- }
+ } catch { }
};
const handleDelete = async () => {
- if (!confirm("Yakin ingin menghapus submission ini?")) return;
try {
const ok = await remove(submission!.id);
if (ok) {
@@ -70,7 +61,7 @@ export default function TaskFormLink({
setIsEditing(false);
customToast.success(
"Submission berhasil dihapus!",
- "Anda dapat mengirim submission baru kapan saja."
+ ok.message
);
}
} catch (err: any) {
@@ -90,7 +81,7 @@ export default function TaskFormLink({
deadline={deadline}
onEdit={() => setIsEditing(true)}
onDelete={handleDelete}
- disabled={!isProjectActive}
+ disabled={!isProjectActive}
/>
);
}
diff --git a/src/Pages/internify/mentor/CertificateForm.tsx b/src/Pages/internify/mentor/CertificateForm.tsx
index a8cd879..7c9711e 100644
--- a/src/Pages/internify/mentor/CertificateForm.tsx
+++ b/src/Pages/internify/mentor/CertificateForm.tsx
@@ -36,13 +36,15 @@ export default function CertificateDetail() {
upload(project.id, selectedFile),
{
loading: "Mengupload template...",
- success: () => ({
- title: "Upload berhasil",
- description: "Template sertifikat berhasil disimpan.",
+ success: (result) => ({
+ title: result?.success === false ? "Upload gagal" : "Upload berhasil",
+ description: result?.message ?? (result?.success === false
+ ? "Terjadi kesalahan saat upload."
+ : "Template sertifikat berhasil disimpan."),
}),
error: (err) => ({
title: "Upload gagal",
- description: err instanceof Error ? err.message : "Terjadi kesalahan saat upload.",
+ description: err?.message ?? "Terjadi kesalahan saat upload.",
}),
}
).then(async (result) => {
diff --git a/src/Pages/internify/mentor/InternsPage.tsx b/src/Pages/internify/mentor/InternsPage.tsx
index bb3184a..e89ed21 100644
--- a/src/Pages/internify/mentor/InternsPage.tsx
+++ b/src/Pages/internify/mentor/InternsPage.tsx
@@ -58,11 +58,11 @@ export default function InternsManagement() {
return;
}
const res = await remove({ id_project: matchedProject.id, id_user: intern.id });
- if (res !== null) {
- customToast.success('Intern removed', `${intern.name} has been successfully removed from the project.`);
+ if (res.success) {
+ customToast.success('Intern removed', res.message);
refetch();
} else {
- customToast.error('Failed to remove', 'An error occurred while removing the intern. Please try again.');
+ customToast.error('Failed to remove', res.message);
}
};
diff --git a/src/Pages/internify/mentor/ProjectTaskTab.tsx b/src/Pages/internify/mentor/ProjectTaskTab.tsx
index fa75882..d594186 100644
--- a/src/Pages/internify/mentor/ProjectTaskTab.tsx
+++ b/src/Pages/internify/mentor/ProjectTaskTab.tsx
@@ -14,19 +14,13 @@ export default function TaskTab({ project }: { project: ProjectDetail }) {
const [selectedTask, setSelectedTask] = useState(null);
const isProjectActive = project?.status === "active"
- const handleDeleteTask = async (taskId: number, taskTitle: string) => {
- const confirmed = window.confirm(`Yakin ingin menghapus task “${taskTitle}”?`);
- if (!confirmed) return;
-
+ const handleDeleteTask = async (taskId: number) => {
const del = await remove(taskId, String(project.id));
- if (del) {
- try {
- customToast.success("Berhasil", `Task ${taskTitle} berhasil dihapus`)
- } catch (error) {
- customToast.error("Gagal", `Task ${taskTitle} gagal dihapus`)
- } finally {
- refetch();
- }
+ if (!del.failed) {
+ customToast.success("Berhasil Menghapus Task", del.message);
+ refetch();
+ } else {
+ customToast.error("Gagal Menghapus Task", del.message);
}
};
@@ -83,7 +77,7 @@ export default function TaskTab({ project }: { project: ProjectDetail }) {
handleDeleteTask(task.id, task.title)}
+ onClick={() => handleDeleteTask(task.id)}
disabled={!isProjectActive}
className="border border-gray-500 text-font-shade rounded-lg px-2 py-1 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
diff --git a/src/Pages/internify/mentor/ProjectsInternTab.tsx b/src/Pages/internify/mentor/ProjectsInternTab.tsx
index 457c540..077a31f 100644
--- a/src/Pages/internify/mentor/ProjectsInternTab.tsx
+++ b/src/Pages/internify/mentor/ProjectsInternTab.tsx
@@ -22,11 +22,11 @@ export default function InternsTab({ project }: { project: ProjectDetail }) {
const handleRemove = async (id_user: number) => {
const res = await remove({ id_project: project.id, id_user });
- if (res !== null) {
+ if (res.success) {
setMembers(prev => prev.filter(m => m.id !== id_user));
- customToast.success('Member removed', 'The intern has been successfully removed from this project.');
+ customToast.success('Member removed', res.message);
} else {
- customToast.error('Failed to remove', 'An error occurred while removing the member. Please try again.');
+ customToast.error('Failed to remove', res.message);
}
};
diff --git a/src/Pages/internify/mentor/components/AddInternsDialog.tsx b/src/Pages/internify/mentor/components/AddInternsDialog.tsx
index 87407a8..819cc91 100644
--- a/src/Pages/internify/mentor/components/AddInternsDialog.tsx
+++ b/src/Pages/internify/mentor/components/AddInternsDialog.tsx
@@ -74,16 +74,15 @@ export default function ManageInternsModal({ isOpen, onClose, projectId, initial
const handleAssign = async (intern: AssignableIntern) => {
const result = await assign({ id_project: projectId, id_user: intern.id });
- if (result) {
- // Tandai sebagai sudah ditambah — intern hilang dari dialog (tidak muncul lagi)
+ if (result.success) {
setAddedIds(prev => new Set([...prev, intern.id]));
- customToast.success('Intern added', `${intern.full_name} has been successfully added to the project.`);
+ customToast.success('Intern added', result.message);
} else {
- customToast.error('Failed to add', 'An error occurred while adding the intern. Please try again.');
+ customToast.error('Failed to add', result.message);
}
};
- const getInitials = (m: AssignableIntern) =>
+ const getInitials = (m: AssignableIntern) =>
m.full_name.split(' ').slice(0, 2).map(n => n[0]).join('').toUpperCase();
return (
diff --git a/src/Pages/internify/mentor/components/AddInternsToProjects.tsx b/src/Pages/internify/mentor/components/AddInternsToProjects.tsx
index c84a145..5c4de51 100644
--- a/src/Pages/internify/mentor/components/AddInternsToProjects.tsx
+++ b/src/Pages/internify/mentor/components/AddInternsToProjects.tsx
@@ -59,11 +59,11 @@ export default function AddInternsModal({ isOpen, onClose, userId }: AddInternsM
id_project: Number(selectedProjectId),
id_user: Number(selectedUserId),
});
- if (res !== null) {
- customToast.success('Intern assigned', 'The intern has been successfully assigned to the project.');
+ if (res.success) {
+ customToast.success('Intern assigned', res.message);
onClose();
} else {
- customToast.error('Failed to assign', 'An error occurred while assigning the intern. Please try again.');
+ customToast.error('Failed to assign', res.message);
}
};
diff --git a/src/Pages/internify/mentor/components/CertResultTable.tsx b/src/Pages/internify/mentor/components/CertResultTable.tsx
index 0414a35..89fd47e 100644
--- a/src/Pages/internify/mentor/components/CertResultTable.tsx
+++ b/src/Pages/internify/mentor/components/CertResultTable.tsx
@@ -1,6 +1,6 @@
import { useState } from "react";
import {
- Search, Eye, Download, Share2,
+ Search, Eye, Download,
ChevronLeft, ChevronRight, Loader2,
} from "lucide-react";
import type { Certificate } from "../../../../types/certificate.types";
@@ -132,13 +132,13 @@ export default function CertResultTable({ certificates, templateUrl }: Certifica
:
}
-
+{/*
-
+ */}
))
diff --git a/src/Pages/internify/mentor/components/CreateProjectDialog.tsx b/src/Pages/internify/mentor/components/CreateProjectDialog.tsx
index 7e9e31f..9a9c6f7 100644
--- a/src/Pages/internify/mentor/components/CreateProjectDialog.tsx
+++ b/src/Pages/internify/mentor/components/CreateProjectDialog.tsx
@@ -22,6 +22,7 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
const [invitedEmails, setInvitedEmails] = useState([]);
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
+ const [dateError, setDateError] = useState("");
const { create, loading, error } = useCreateProject();
const { interns: assignableInterns, loading: loadingInterns } = useAssignableInterns();
const [showDropdown, setShowDropdown] = useState(false);
@@ -68,6 +69,14 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
+
+ setDateError("");
+
+ if (startDate && endDate && new Date(endDate) < new Date(startDate)) {
+ setDateError("Tanggal selesai tidak boleh lebih awal dari tanggal mulai.");
+ return;
+ }
+
const payload: CreateProjectPayload = {
project_icon: selectedIcon,
project_name: projectName,
@@ -81,13 +90,12 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
}
const result = await create(payload);
- if (result) {
+ if (result.success) {
onSuccess?.();
onClose();
- customToast.success(
- "Project berhasil dibuat!",
- `${projectName} sudah aktif dan siap dikelola.`
- );
+ customToast.success("Project berhasil dibuat!", (result.message));
+ } else {
+ customToast.error("Gagal membuat project", (result.message));
}
};
@@ -263,14 +271,14 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
{/* Start Date */}
+ {dateError && (
+
{dateError}
+ )}
{error && (
-
{error}
+
{error}
)}
{/* Action Row Buttons */}
diff --git a/src/Pages/internify/mentor/components/CreateTaskDialog.tsx b/src/Pages/internify/mentor/components/CreateTaskDialog.tsx
index 5cc0a16..ad70a34 100644
--- a/src/Pages/internify/mentor/components/CreateTaskDialog.tsx
+++ b/src/Pages/internify/mentor/components/CreateTaskDialog.tsx
@@ -58,18 +58,14 @@ export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess
submission_type: submissionType,
});
- if (result) {
- customToast.success(
- 'Task created',
- `"${taskTitle}" has been successfully added to the project.`);
+ if (result.data) {
+ customToast.success('Task created', result.message);
onSuccess();
-
} else {
- customToast.error(
- 'Failed to create task',
- 'An error occurred while saving the task. Please try again.');
+ customToast.error('Failed to create task', result.message);
}
};
+
return (