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 && (
- empty notification + empty notification

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 }) { - +{/* + */} )) 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 */}
setStartDate(e.target.value)} + onChange={(e) => { setStartDate(e.target.value); setDateError(""); }} className="w-full px-3 py-2.5 bg-white border border-card-outline rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium appearance-none" /> @@ -281,14 +289,14 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat {/* End Date */}
setEndDate(e.target.value)} + onChange={(e) => { setEndDate(e.target.value); setDateError(""); }} className="w-full px-3 py-2.5 bg-white border border-card-outline rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium appearance-none" /> @@ -296,9 +304,12 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
+ {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 (
diff --git a/src/Pages/internify/mentor/components/EditSubmissionTask.tsx b/src/Pages/internify/mentor/components/EditSubmissionTask.tsx index 3ad8b95..886c0fc 100644 --- a/src/Pages/internify/mentor/components/EditSubmissionTask.tsx +++ b/src/Pages/internify/mentor/components/EditSubmissionTask.tsx @@ -3,6 +3,7 @@ import { X } from "lucide-react"; import { LuSave } from "react-icons/lu"; import { useUpdateTask } from "../../../../hooks/useTasks"; import type { ProjectTask, SubmissionType, UpdateTaskPayload } from "../../../../types/task.types"; +import { customToast } from "../../../utils/showToast"; interface EditTaskModalProps { isOpen: boolean; @@ -79,9 +80,12 @@ export default function EditSubmissionModal({ return; } const result = await update(task.id, payload, String(task.id_project)); - if (result) { - onSuccess(result); + if (result.data) { + customToast.success("Task diperbarui", result.message); + onSuccess(result.data); onClose(); + } else { + customToast.error("Gagal", result.message); } }; diff --git a/src/Pages/internify/mentor/components/EligibleInternTable.tsx b/src/Pages/internify/mentor/components/EligibleInternTable.tsx index 0c13a13..205d3e1 100644 --- a/src/Pages/internify/mentor/components/EligibleInternTable.tsx +++ b/src/Pages/internify/mentor/components/EligibleInternTable.tsx @@ -124,7 +124,7 @@ export default function EligibleInternTable({ return; } if (selected.size === 0) { - customToast.error("Tidak ada intern dipilih", "Pilih minimal satu intern untuk generate sertifikat."); + customToast.error("Tidak ada intern dipilih", "Checklist intern yang sudah eligible untuk membuat sertifikat."); return; } @@ -134,15 +134,14 @@ export default function EligibleInternTable({ const ok = await customToast.promise( (async () => { const result = await SubmitCertificates(project.id, Array.from(selected)); - if (!result) throw new Error("Tidak ada data sertifikat yang dikembalikan."); + if (!result.data) throw new Error("Tidak ada data sertifikat yang dikembalikan."); const zip = new JSZip(); const templateUrl = resolveImageUrl(project.certificate_template!); - const duration = formatDateRange(project.start_date, project.end_date); await Promise.all( - result.map(async (cert) => { + result.data.map(async (cert) => { const blob = await generateCertificate( templateUrl, cert.user.full_name, diff --git a/src/Pages/internify/mentor/components/MentorProjectCard.tsx b/src/Pages/internify/mentor/components/MentorProjectCard.tsx index ebea10e..1d49fa7 100644 --- a/src/Pages/internify/mentor/components/MentorProjectCard.tsx +++ b/src/Pages/internify/mentor/components/MentorProjectCard.tsx @@ -37,15 +37,15 @@ export default function MentorProjectCard({ onArchived, onCompleted, ...project const result = await archive(project.id); setShowArchiveDialog(false); - if (result) { - customToast.success("Berhasil!", "Projek berubah status menjadi Archived (Diarsipkan"); + if (result.success) { + customToast.success("Berhasil!", result.message); onArchived?.(); } else { - customToast.error("Gagal!", "Projek gagal diarsipkan (archived)"); + customToast.error("Gagal!", result.message); } }; - /* ── Complete ────────────────────────────────────────────── */ + /* Complete */ const handleCompleteButtonClick = (e: React.MouseEvent) => { e.stopPropagation(); setShowCompleteDialog(true); @@ -55,11 +55,11 @@ export default function MentorProjectCard({ onArchived, onCompleted, ...project const result = await complete(project.id); setShowCompleteDialog(false); - if (result) { - customToast.success("Berhasil!", "Projek berubah status menjadi completed (selesai)"); + if (result?.success) { + customToast.success("Berhasil!", result.message); onCompleted?.(); } else { - customToast.error("Gagal!", "Projek gagal diselesaikan (completed)"); + customToast.error("Gagal!", result?.message); } }; diff --git a/src/Pages/utils/SertificateGenerator.ts b/src/Pages/utils/SertificateGenerator.ts index 937fef7..6bf74da 100644 --- a/src/Pages/utils/SertificateGenerator.ts +++ b/src/Pages/utils/SertificateGenerator.ts @@ -36,7 +36,7 @@ const LAYOUT = { internNameSmall: { x: 0.82, y: 0.889, size: 33, font: "Great Vibes", color: "#800000" }, projectName: { x: 0.815, y: 0.598, size: 46, font: "Grenze", color: "#800000" }, duration: { x: 0.692, y: 0.645, size: 48, font: "Grenze", color: "#090909" }, - certNo: { x: 0.6, y: 0.305, size: 50, font: "Grenze", color: "#090909" }, + certNo: { x: 0.6, y: 0.28, size: 50, font: "Grenze", color: "#090909" }, certNoSmall: { x: 0.825, y: 0.91, size: 22, font: "Grenze", color: "#800000" }, qr: { x: 0.77, y: 0.715, size: 0.103 }, }; diff --git a/src/Pages/utils/resolveObjectError.ts b/src/Pages/utils/resolveObjectError.ts new file mode 100644 index 0000000..dcdb475 --- /dev/null +++ b/src/Pages/utils/resolveObjectError.ts @@ -0,0 +1,12 @@ +// Ubah message backend (string ATAU object field-errors) jadi satu string aman +export function resolveErrorMessage(message: unknown, fallback = "Silakan coba lagi."): string { + if (typeof message === "string") return message; + if (message && typeof message === "object") { + // ambil semua value error, gabung: { end_date: "...", start_date: "..." } -> "... \n ..." + return Object.values(message as Record) + .flat() + .filter((v): v is string => typeof v === "string") + .join(" ") || fallback; + } + return fallback; +} \ No newline at end of file diff --git a/src/Pages/utils/showToast.tsx b/src/Pages/utils/showToast.tsx index e640741..da27052 100644 --- a/src/Pages/utils/showToast.tsx +++ b/src/Pages/utils/showToast.tsx @@ -53,6 +53,16 @@ export const customToast = { const loadingId = toast.loading(messages.loading); try { const data = await promise; + const isHookFailure = typeof data === 'object' && data !== null && 'success' in data && (data as { success?: boolean }).success === false; + + if (isHookFailure) { + toast.dismiss(loadingId); + const result = data as { success: boolean; message?: string }; + const { title, description } = messages.error(result); + customToast.error(title, description ?? result.message); + throw new Error(result.message ?? 'Operasi gagal.'); + } + toast.dismiss(loadingId); const { title, description } = messages.success(data); customToast.success(title, description); @@ -60,7 +70,7 @@ export const customToast = { } catch (err) { toast.dismiss(loadingId); const { title, description } = messages.error(err); - customToast.error(title, description); + customToast.error(title, description ?? (err as Error)?.message); throw err; } }, diff --git a/src/hooks/useCertificates.ts b/src/hooks/useCertificates.ts index 90ff369..6f6c876 100644 --- a/src/hooks/useCertificates.ts +++ b/src/hooks/useCertificates.ts @@ -11,7 +11,6 @@ import { generateCertificate, verifyCertificateByUuid } from "../services/CertificateService"; -import { AxiosError } from "axios"; // GET /certificate-api/my-certificates export const useMyCertificates = () => { @@ -93,10 +92,18 @@ export const useClaimCertificate = () => { const [error, setError] = useState(null); const claim = async (id_project: number) => { - setLoading(true); setError(null); - try { return await claimCertificate(id_project); } - catch { setError("Gagal claim sertifikat."); return null; } - finally { setLoading(false); } + setLoading(true); + setError(null); + try { + const { data, message } = await claimCertificate(id_project); + return { success: true as const, data, message }; + } catch (err: any) { + const msg = err?.response?.data?.message ?? "Gagal claim sertifikat."; + setError(msg); + return { success: false as const, message: msg }; + } finally { + setLoading(false); + } }; return { claim, loading, error }; @@ -107,39 +114,18 @@ export const useGenerateCertificates = () => { const [error, setError] = useState(null); const [success, setSuccess] = useState(false); - const generate = async ( - id_project: number, - id_users: number[] - ): Promise => { + const generate = async (id_project: number, id_users: number[]) => { setLoading(true); setError(null); setSuccess(false); try { - const data = await generateCertificate(id_project, id_users); + const { data, message } = await generateCertificate(id_project, id_users); setSuccess(true); - return data; - } catch (err) { - if (err instanceof AxiosError) { - switch (err.response?.status) { - case 400: - setError("Ada intern yang tidak eligible atau parameter tidak lengkap."); - break; - case 401: - setError("Sesi kamu berakhir, silakan login ulang."); - break; - case 403: - setError("Hanya mentor/admin project ini yang bisa generate sertifikat."); - break; - case 409: - setError("Sertifikat sudah pernah di-generate untuk satu atau lebih intern."); - break; - default: - setError("Gagal generate sertifikat."); - } - } else { - setError("Gagal generate sertifikat."); - } - return null; + return { data, message }; + } catch (err: any) { + const message = err?.response?.data?.message ?? "Gagal generate sertifikat."; + setError(message); + return { message }; } finally { setLoading(false); } @@ -159,10 +145,18 @@ export const useUploadCertificateTemplate = () => { const [error, setError] = useState(null); const upload = async (id_project: number, file: File) => { - setLoading(true); setError(null); - try { return await uploadCertificateTemplate(id_project, file); } - catch { setError("Gagal upload template."); return null; } - finally { setLoading(false); } + setLoading(true); + setError(null); + try { + const { data, message } = await uploadCertificateTemplate(id_project, file); + return { success: true as const, data, message }; + } catch (err: any) { + const msg = err?.response?.data?.message ?? "Gagal upload template."; + setError(msg); + return { success: false as const, message: msg }; + } finally { + setLoading(false); + } }; return { upload, loading, error }; diff --git a/src/hooks/useMentors.ts b/src/hooks/useMentors.ts index c622dd8..4310d1e 100644 --- a/src/hooks/useMentors.ts +++ b/src/hooks/useMentors.ts @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from "react"; -import type { MentorData, CreateMentorPayload, UpdateMentorPayload } from "../services/MentorServices"; +import type { MentorData, CreateMentorPayload, UpdateMentorPayload } from "../types/user.types"; import { createMentorService, getMentorsService, @@ -8,7 +8,7 @@ import { deleteMentorService, } from "../services/MentorServices" -// ── GET ALL ────────────────────────────────────────────────────────────────── +// GET ALL MENTORS export const useMentors = () => { const [mentors, setMentors] = useState([]); const [loading, setLoading] = useState(false); @@ -31,7 +31,7 @@ export const useMentors = () => { return { mentors, loading, error, refetch: fetch }; }; -// ── GET DETAIL ─────────────────────────────────────────────────────────────── +// GET DETAIL export const useMentorDetail = (id: number | null) => { const [mentor, setMentor] = useState(null); const [loading, setLoading] = useState(false); @@ -55,18 +55,19 @@ export const useMentorDetail = (id: number | null) => { return { mentor, loading, error, refetch: fetch }; }; -// ── CREATE ─────────────────────────────────────────────────────────────────── +// CREATE export const useCreateMentor = () => { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const createMentor = async (payload: CreateMentorPayload): Promise => { + const createMentor = async (payload: CreateMentorPayload): Promise<{ data: MentorData; message: string } | null> => { setLoading(true); setError(null); try { return await createMentorService(payload); - } catch { - setError("Gagal membuat akun mentor."); + } catch (err: any) { + const msg = err?.response?.data?.message ?? "Gagal membuat akun mentor."; + setError(msg); return null; } finally { setLoading(false); @@ -76,7 +77,7 @@ export const useCreateMentor = () => { return { createMentor, loading, error }; }; -// ── UPDATE ─────────────────────────────────────────────────────────────────── +// UPDATE export const useUpdateMentor = () => { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -84,13 +85,14 @@ export const useUpdateMentor = () => { const updateMentor = async ( id: number, payload: UpdateMentorPayload - ): Promise => { + ): Promise<{ data: MentorData; message: string } | null> => { setLoading(true); setError(null); try { return await updateMentorService(id, payload); - } catch { - setError("Gagal memperbarui data mentor."); + } catch (err: any) { + const msg = err?.response?.data?.message ?? "Gagal memperbarui data mentor."; + setError(msg); return null; } finally { setLoading(false); @@ -100,20 +102,21 @@ export const useUpdateMentor = () => { return { updateMentor, loading, error }; }; -// ── DELETE ─────────────────────────────────────────────────────────────────── +// DELETE export const useDeleteMentor = () => { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const deleteMentor = async (id: number): Promise => { + const deleteMentor = async (id: number): Promise<{ success: boolean; message: string }> => { setLoading(true); setError(null); try { - await deleteMentorService(id); - return true; - } catch { - setError("Gagal menghapus mentor."); - return false; + const res = await deleteMentorService(id); + return { success: true, message: res.message }; + } catch (err: any) { + const msg = err?.response?.data?.message ?? "Gagal menghapus mentor."; + setError(msg); + return { success: false, message: msg }; } finally { setLoading(false); } diff --git a/src/hooks/useNotification.ts b/src/hooks/useNotification.ts index 1f30ba2..553fb2d 100644 --- a/src/hooks/useNotification.ts +++ b/src/hooks/useNotification.ts @@ -24,18 +24,28 @@ export const useNotifications = () => { const markAllRead = async () => { try { - await markAllAsRead(); + const { message } = await markAllAsRead(); setNotifications(prev => prev.map(n => ({ ...n, is_read: true }))); - } catch { setError("Gagal menandai semua notifikasi."); } + return { success: true as const, message }; + } catch (err: any) { + const msg = err?.response?.data?.message ?? "Gagal menandai semua notifikasi."; + setError(msg); + return { success: false as const, message: msg }; + } }; const markOneRead = async (id: number) => { try { - await markAsRead(id); + const { message } = await markAsRead(id); setNotifications(prev => prev.map(n => n.id === id ? { ...n, is_read: true } : n) ); - } catch { setError("Gagal menandai notifikasi."); } + return { success: true as const, message }; + } catch (err: any) { + const msg = err?.response?.data?.message ?? "Gagal menandai notifikasi."; + setError(msg); + return { success: false as const, message: msg }; + } }; const unreadCount = notifications.filter(n => !n.is_read).length; diff --git a/src/hooks/useProjects.ts b/src/hooks/useProjects.ts index 612a579..3cef22b 100644 --- a/src/hooks/useProjects.ts +++ b/src/hooks/useProjects.ts @@ -188,10 +188,12 @@ export const useCreateProject = () => { setLoading(true); setError(null); try { - return await createProject(payload); - } catch { - setError("Gagal membuat project."); - return null; + const { data, message } = await createProject(payload); + return { success: true as const, data, message }; + } catch (err: any) { + const msg = err?.response?.data?.message ?? "Gagal membuat project."; + setError(msg); + return { success: false as const, message: msg }; } finally { setLoading(false); } @@ -220,10 +222,12 @@ export const useUpdateProject = () => { setLoading(true); setError(null); try { - return await updateProject(id, payload); - } catch { - setError("Gagal mengupdate project."); - return null; + const { data, message } = await updateProject(id, payload); + return { success: true as const, data, message }; + } catch (err: any) { + const msg = err?.response?.data?.message ?? "Gagal mengupdate project."; + setError(msg); + return { success: false as const, message: msg }; } finally { setLoading(false); } @@ -241,10 +245,12 @@ export const useArchiveProject = () => { setLoading(true); setError(null); try { - return await archiveProject(id); - } catch { - setError("Gagal mengarsipkan project."); - return null; + const { data, message } = await archiveProject(id); + return { success: true as const, data, message }; + } catch (err: any) { + const msg = err?.response?.data?.message ?? "Gagal mengarsipkan project."; + setError(msg); + return { success: false as const, message: msg }; } finally { setLoading(false); } @@ -261,10 +267,12 @@ export const useCompleteProject = () => { setLoading(true); setError(null); try { - return await completeProject(id); - } catch { - setError("Gagal menyelesaikan project."); - return null; + const { data, message } = await completeProject(id); + return { success: true as const, data, message} + } catch (err: any) { + const msg = err?.response?.data?.message ?? "Gagal menyelesaikan project."; + setError(msg); + return { success: false as const, message: msg}; } finally { setLoading(false); } @@ -282,10 +290,12 @@ export const useAssignMember = () => { setLoading(true); setError(null); try { - return await assignMember(payload); - } catch { - setError("Gagal assign member."); - return null; + const { data, message } = await assignMember(payload); + return { success: true as const, data, message }; + } catch (err: any) { + const msg = err?.response?.data?.message ?? "Gagal assign member."; + setError(msg); + return { success: false as const, message: msg }; } finally { setLoading(false); } @@ -303,10 +313,12 @@ export const useRemoveMember = () => { setLoading(true); setError(null); try { - return await removeMember(payload); - } catch { - setError("Gagal remove member."); - return null; + const { data, message } = await removeMember(payload); + return { success: true as const, data, message }; + } catch (err: any) { + const msg = err?.response?.data?.message ?? "Gagal remove member."; + setError(msg); + return { success: false as const, message: msg }; } finally { setLoading(false); } diff --git a/src/hooks/useTasks.ts b/src/hooks/useTasks.ts index 95bbb91..1d4c181 100644 --- a/src/hooks/useTasks.ts +++ b/src/hooks/useTasks.ts @@ -97,16 +97,16 @@ 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) => { setLoading(true); setError(null); try { - return await createTask(projectId, payload); - } catch { - setError("Gagal membuat task."); - return null; + const { data, message } = await createTask(projectId, payload); + return { data, message }; + } catch (err: any) { + const message = err?.response?.data?.message ?? "Gagal membuat task."; + setError(message); + return { message }; } finally { setLoading(false); } @@ -128,10 +128,12 @@ export const useUpdateTask = () => { setLoading(true); setError(null); try { - return await updateTask(taskId, payload, projectId); - } catch { - setError("Gagal mengupdate task."); - return null; + const { data, message } = await updateTask(taskId, payload, projectId); + return { data, message }; + } catch (err: any) { + const message = err?.response?.data?.message ?? "Gagal mengupdate task."; + setError(message); + return { message }; } finally { setLoading(false); } @@ -149,16 +151,17 @@ export const useDeleteTask = () => { setLoading(true); setError(null); try { - await deleteTask(taskId, projectId); - return true; - } catch { - setError("Gagal menghapus task."); - return false; + const data = await deleteTask(taskId, projectId); + const msg = data?.message ?? "Task berhasil dihapus."; + return { message: msg }; + } catch (err: any) { + const msg = err?.response?.data?.message ?? "Gagal menghapus task."; + setError(msg); + return { message: msg, failed: true as const }; } finally { setLoading(false); } }; - return { remove, loading, error }; }; @@ -179,12 +182,12 @@ export const useSubmission = ( setError(null); try { const res = await submitTaskFile(taskId, files, projectId); - if (res) setSubmission(res); + if (res.data) setSubmission(res.data); return res; - } catch (err){ - setError("Gagal submit file."); - throw error; - return null; + } catch (err: any) { + const msg = err?.response?.data?.message ?? "Gagal submit file."; + setError(msg); + throw err; } finally { setLoading(false); } @@ -195,10 +198,11 @@ export const useSubmission = ( setError(null); try { const res = await submitTaskLink(taskId, url, projectId); - if (res) setSubmission(res); + if (res.data) setSubmission(res.data); return res; - } catch (err){ - setError("Gagal submit link."); + } catch (err: any) { + const msg = err?.response?.data?.message ?? "Gagal submit link."; + setError(msg); throw err; } finally { setLoading(false); @@ -210,10 +214,11 @@ export const useSubmission = ( setError(null); try { const res = await updateSubmissionFile(submissionId, files); - if (res) setSubmission(res); + if (res.data) setSubmission(res.data); return res; - } catch (err){ - setError("Gagal update submission."); + } catch (err: any) { + const msg = err?.response?.data?.message ?? "Gagal update submission."; + setError(msg); throw err; } finally { setLoading(false); @@ -225,10 +230,11 @@ export const useSubmission = ( setError(null); try { const res = await updateSubmissionLink(submissionId, url); - if (res) setSubmission(res); + if (res.data) setSubmission(res.data); return res; - } catch (err){ - setError("Gagal update submission."); + } catch (err: any) { + const msg = err?.response?.data?.message ?? "Gagal update submission."; + setError(msg); throw err; } finally { setLoading(false); @@ -239,12 +245,13 @@ export const useSubmission = ( setLoading(true); setError(null); try { - await deleteSubmission(submissionId); + const res = await deleteSubmission(submissionId); setSubmission(null); - return true; - } catch (err) { - setError("Gagal hapus submission."); - throw err; + return { success: true as const, message: res.message }; + } catch (err: any) { + const msg = err?.response?.data?.message ?? "Gagal hapus submission."; + setError(msg); + return { success: false as const, message: msg }; } finally { setLoading(false); } @@ -305,16 +312,14 @@ export const useDeadlines = () => { if (!user) return []; return isIntern ? internTasks.map((task) => ({ - date: new Date(task.deadline_at), - label: task.title, - })) + date: new Date(task.deadline_at), + label: task.title, + })) : mentorTasks.map((task) => ({ - date: new Date(task.deadline_at), - label: `[${task.project_name}] ${task.title}`, - })); + date: new Date(task.deadline_at), + label: `[${task.project_name}] ${task.title}`, + })); }, [isIntern, internTasks, mentorTasks, user]); - - // Loading: tunggu user dulu, baru tunggu task yang relevan dengan rolenya const loading = userLoading || (isIntern ? internLoading : mentorLoading); const error = userError || (isIntern ? internError : mentorError); diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts index aa43a64..8b85fcd 100644 --- a/src/hooks/useUser.ts +++ b/src/hooks/useUser.ts @@ -16,7 +16,7 @@ let store: UserStore = { user: null, loading: true, error: null }; const listeners = new Set<() => void>(); const setStore = (patch: Partial) => { - store = { ...store, ...patch }; // objek baru -> getSnapshot berubah referensinya + store = { ...store, ...patch }; listeners.forEach((l) => l()); }; @@ -27,12 +27,10 @@ const subscribe = (l: () => void) => { const getSnapshot = () => store; -// useUpdateProfile setelah PATCH sukses -> broadcast ke semua konsumen export const setCurrentUser = (user: CurrentUser | null) => { setStore({ user, loading: false, error: null }); }; -// dipakai saat logout export const clearCurrentUser = () => { fetched = false; inflight = null; @@ -87,15 +85,11 @@ export const useLogin = () => { setError(null); try { const data = await loginUser({ email, password }); - clearCurrentUser(); // buang user lama agar tidak bocor antar-sesi - return data; // { token } + clearCurrentUser(); + return data; } catch (err: any) { - const status = err?.response?.status; - if (status === 401 || status === 400) { - setError("Email atau password salah."); - } else { - setError("Terjadi kesalahan. Coba beberapa saat lagi."); - } + const msg = err?.response?.data?.message ?? "Terjadi kesalahan. Coba beberapa saat lagi."; + setError(msg); return null; } finally { setLoading(false); @@ -105,6 +99,7 @@ export const useLogin = () => { return { login, loading, error, setError }; }; +// Ambil 2 huruf (nama depan + nama belakang user) export const getInitials = (fullName?: string | null): string => { if (!fullName) return "U"; const parts = fullName.trim().split(/\s+/).filter(Boolean); @@ -113,7 +108,7 @@ export const getInitials = (fullName?: string | null): string => { return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase(); }; - +// Untuk resolve gambar (duplikat dari utils/...) export const resolveFileUrl = (path?: string | null): string | null => { if (!path) return null; if (/^(https?:|data:|blob:)/i.test(path)) return path; @@ -124,71 +119,22 @@ export const resolveFileUrl = (path?: string | null): string | null => { // PATCH /auth-api/update-profile export const useUpdateProfile = () => { const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [successMsg, setSuccessMsg] = useState(null); - - const msgRef = useRef<{ success: string | null; error: string | null }>({ - success: null, - error: null, - }); const save = async (payload: UpdateProfilePayload) => { setLoading(true); - setError(null); - setSuccessMsg(null); - msgRef.current = { success: null, error: null }; - try { const updated = await updateProfile(payload); - - setCurrentUser(updated); - - const msg = "Profile updated successfully!"; - msgRef.current.success = msg; - setSuccessMsg(msg); + setCurrentUser(updated.data); return updated; - } catch (err: any) { - const status = err?.response?.status; - const apiMessage = err?.response?.data?.message; - - let message: string; - switch (status) { - case 400: - message = - apiMessage || - "Data yang dikirim tidak valid. Periksa kembali isian Anda."; - break; - case 401: - message = "Sesi Anda telah berakhir. Silakan login kembali."; - break; - case 403: - message = apiMessage || "Anda tidak memiliki izin untuk mengubah data ini."; - break; - case 409: - message = apiMessage || "Email sudah digunakan, atau nama sudah pernah diubah."; - break; - case 413: - message = "Ukuran file terlalu besar. Maksimal 2MB."; - break; - case 417: - message = apiMessage || "Validasi gagal. Periksa kembali isian Anda."; - break; - case 500: - message = "Terjadi kesalahan di server. Coba beberapa saat lagi."; - break; - default: - message = apiMessage || "Failed to save changes."; - } - - msgRef.current.error = message; - setError(message); - throw new Error(message); +; } catch (err: any) { + const message = (err?.response?.data?.message, "Gagal menyimpan perubahan."); + throw new Error(message) } finally { setLoading(false); } }; - return { save, loading, error, successMsg, setSuccessMsg, setError, msgRef }; + return { save, loading }; }; // Ambil Seluruh Mahasiswa diff --git a/src/services/CertificateService.ts b/src/services/CertificateService.ts index 152829f..807773e 100644 --- a/src/services/CertificateService.ts +++ b/src/services/CertificateService.ts @@ -7,15 +7,20 @@ import type { } from "../types/certificate.types"; // POST /certificate-api/claim -export const claimCertificate = async (id_project: number): Promise => { +export const claimCertificate = async ( + id_project: number +): Promise<{ data: Certificate; message: string }> => { const res = await api.post("/certificate-api/claim", { id_project }); - return res.data.data; + return { data: res.data.data, message: res.data.message }; }; // POST /certificate-api/generate -export const generateCertificate = async (id_project: number, id_users: number[]): Promise => { +export const generateCertificate = async ( + id_project: number, + id_users: number[] +): Promise<{ data: Certificate[]; message: string }> => { const res = await api.post("/certificate-api/generate", { id_project, id_users }); - return res.data.data.certificates; + return { data: res.data.data.certificates, message: res.data.message }; }; // GET /certificate-api/my-certificates @@ -52,9 +57,9 @@ export const verifyCertificateByUuid = async (uuid: string): Promise => { +): Promise<{ data: CertificateTemplate; message: string }> => { const formData = new FormData(); formData.append("template", file); const res = await api.post(`/certificate-api/projects/${id_project}/template`, formData); - return res.data.data; + return { data: res.data.data, message: res.data.message }; }; \ No newline at end of file diff --git a/src/services/MentorServices.ts b/src/services/MentorServices.ts index 3b34b08..4354cbb 100644 --- a/src/services/MentorServices.ts +++ b/src/services/MentorServices.ts @@ -1,39 +1,5 @@ import api from "../lib/api"; - -export interface MentorData { - id: number; - nama_depan: string; - nama_belakang: string; - full_name: string; - email: string; - role: string; - signature: string; - profile_picture: string | null; - professional_bio: string | null; - created_at: string; - updated_at: string; -} - -export interface CreateMentorPayload { - nama_depan: string; - nama_belakang: string; - full_name: string; - email: string; - password: string; - signature?: string; - professional_bio?: string; - profile_picture?: File; -} - -export interface UpdateMentorPayload { - nama_depan?: string; - nama_belakang?: string; - email?: string; - password?: string; - signature?: string; - professional_bio?: string; - profile_picture?: File; -} +import type { CreateMentorPayload, UpdateMentorPayload, MentorData } from "../types/user.types"; const toFormData = (payload: CreateMentorPayload | UpdateMentorPayload): FormData => { const form = new FormData(); @@ -45,11 +11,11 @@ const toFormData = (payload: CreateMentorPayload | UpdateMentorPayload): FormDat return form; }; -export const createMentorService = async (payload: CreateMentorPayload): Promise => { - const { data } = await api.post("/admin-api/mentors", toFormData(payload), { +export const createMentorService = async (payload: CreateMentorPayload): Promise<{data: MentorData, message: string}> => { + const { data } = await api.post<{data: MentorData, message: string}>("/admin-api/mentors", toFormData(payload), { headers: { "Content-Type": "multipart/form-data" }, }); - return data.data; + return data; }; export const getMentorsService = async (): Promise => { @@ -65,13 +31,14 @@ export const getMentorDetailService = async (id: number): Promise => export const updateMentorService = async ( id: number, payload: UpdateMentorPayload -): Promise => { - const { data } = await api.patch(`/admin-api/mentors/${id}`, toFormData(payload), { +): Promise<{ data: MentorData; message: string }> => { + const { data } = await api.patch<{ data: MentorData; message: string }>(`/admin-api/mentors/${id}`, toFormData(payload), { headers: { "Content-Type": "multipart/form-data" }, }); - return data.data; + return data; }; -export const deleteMentorService = async (id: number): Promise => { - await api.delete(`/admin-api/mentors/${id}`); +export const deleteMentorService = async (id: number): Promise<{ message: string }> => { + const { data } = await api.delete<{ message: string }>(`/admin-api/mentors/${id}`); + return data; }; \ No newline at end of file diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index a668dfd..f610b1b 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -1,4 +1,3 @@ -// services/NotificationService.ts import api from "../lib/api"; import type { BackendNotification, UINotification } from "../types/notification.types"; @@ -7,12 +6,16 @@ export const getNotifications = async (): Promise => { return res.data.data; }; -export const markAllAsRead = async (): Promise => { - await api.patch("/notification-api/read-all"); +export const markAllAsRead = async (): Promise<{ message: string }> => { + const res = await api.patch("/notification-api/read-all"); + return { message: res.data.message }; }; -export const markAsRead = async (id: number): Promise => { - await api.patch(`/notification-api/${id}/read`); +export const markAsRead = async ( + id: number +): Promise<{ data: BackendNotification; message: string }> => { + const res = await api.patch(`/notification-api/${id}/read`); + return { data: res.data.data, message: res.data.message }; }; export const mapType = (title: string): string => { diff --git a/src/services/ProjectService.ts b/src/services/ProjectService.ts index 6acbbb9..83c2f66 100644 --- a/src/services/ProjectService.ts +++ b/src/services/ProjectService.ts @@ -16,9 +16,9 @@ import type { // POST /project-api/add export const createProject = async ( payload: CreateProjectPayload -): Promise => { +): Promise<{ data: Project, message: string }> => { const res = await api.post("/project-api/add", payload); - return res.data.data; + return { data: res.data.data, message: res.data.message }; }; // GET /project-api/get @@ -72,25 +72,25 @@ export const getProjectById = async ( export const updateProject = async ( id: string | number, payload: UpdateProjectPayload -): Promise => { +): Promise<{data: ProjectDetail, message: string}> => { const res = await api.patch(`/project-api/update/${id}`, payload); - return res.data.data; + return {data: res.data.data, message: res.data.message}; }; // DELETE /project-api/delete/{id} — soft delete (archive) export const archiveProject = async ( id: string | number -): Promise => { +): Promise<{data: ProjectDetail, message: string}> => { const res = await api.delete(`/project-api/delete/${id}`); - return res.data.data; + return {data: res.data.data, message: res.data.message}; }; export const completeProject = async ( id: string | number -): Promise => { +): Promise<{data: ProjectDetail, message: string}> => { const res = await api.patch(`/project-api/complete/${id}`); - return res.data.data; + return {data: res.data.data, message: res.data.message}; }; // GET /project-api/my-projects (intern only) @@ -126,9 +126,9 @@ export const getAssignableInterns = async (): Promise => { // POST /project-api/assign-member (admin/mentor only) export const assignMember = async ( payload: AssignMemberPayload -): Promise => { +): Promise<{data: MemberAssignment, message: string}> => { const res = await api.post("/project-api/assign-member", payload); - return res.data.data; + return {data: res.data.data, message: res.data.message}; }; export const removeMember = async ( @@ -136,7 +136,7 @@ export const removeMember = async ( id_project: string | number; id_user: number; } -): Promise => { +): Promise<{data: MemberAssignment, message: string}> => { const res = await api.post("/project-api/remove-member", { id_project: payload.id_project, id_user: payload.id_user }); - return res.data.data; + return {data: res.data.data, message: res.data.message}; }; \ No newline at end of file diff --git a/src/services/TaskService.ts b/src/services/TaskService.ts index 7ef8479..496f764 100644 --- a/src/services/TaskService.ts +++ b/src/services/TaskService.ts @@ -10,14 +10,12 @@ import type { import type { InternTaskItem, Project } from "../types/project.types"; import type { MentorTaskItem, AdminTaskDetail } from "../types/task.types"; -// ── Task ────────────────────────────────────────────── - export const createTask = async ( projectId: string | number, payload: CreateTaskPayload -): Promise => { +): Promise<{ data: ProjectTask; message: string }> => { const res = await api.post(`/task-api/projects/${projectId}/tasks`, payload); - return res.data.data; + return { data: res.data.data, message: res.data.message }; }; export const getProjectTasks = async ( @@ -51,20 +49,21 @@ export const updateTask = async ( taskId: string | number, payload: UpdateTaskPayload, projectId?: string | number -): Promise => { +): Promise<{ data: ProjectTask; message: string }> => { const res = await api.patch(`/task-api/tasks/${taskId}`, payload, { params: projectId ? { project: projectId } : undefined, }); - return res.data.data; + return { data: res.data.data, message: res.data.message }; }; export const deleteTask = async ( taskId: string | number, projectId?: string | number -): Promise => { - await api.delete(`/task-api/tasks/${taskId}`, { +) => { + const res = await api.delete(`/task-api/tasks/${taskId}`, { params: projectId ? { project: projectId } : undefined, }); + return res.data; }; export const getAllMentorTasks = async (): Promise => { @@ -87,54 +86,54 @@ export const getAllMentorTasks = async (): Promise => { -// ── Submission ──────────────────────────────────────── - +// Submission export const submitTaskFile = async ( taskId: string | number, files: File[], projectId?: string | number -): Promise => { +): Promise<{ data: TaskSubmissionData; message: string }> => { const formData = new FormData(); files.forEach((file) => formData.append("submission_files", file)); const res = await api.post(`/task-api/tasks/${taskId}/submissions`, formData, { params: projectId ? { project: projectId } : undefined, }); - return res.data.data; + return { data: res.data.data, message: res.data.message }; }; export const submitTaskLink = async ( taskId: string | number, url_link: string, projectId?: string | number -): Promise => { +): Promise<{ data: TaskSubmissionData; message: string }> => { const formData = new FormData(); formData.append("url_link", url_link); const res = await api.post(`/task-api/tasks/${taskId}/submissions`, formData, { params: projectId ? { project: projectId } : undefined, }); - return res.data.data; + return { data: res.data.data, message: res.data.message }; }; export const updateSubmissionFile = async ( submissionId: number, files: File[] -): Promise => { +): Promise<{ data: TaskSubmissionData; message: string }> => { const formData = new FormData(); files.forEach((file) => formData.append("submission_files", file)); const res = await api.patch(`/task-api/submissions/${submissionId}`, formData); - return res.data.data; + return { data: res.data.data, message: res.data.message }; }; export const updateSubmissionLink = async ( submissionId: number, url_link: string -): Promise => { +): Promise<{ data: TaskSubmissionData; message: string }> => { const formData = new FormData(); formData.append("url_link", url_link); const res = await api.patch(`/task-api/submissions/${submissionId}`, formData); - return res.data.data; + return { data: res.data.data, message: res.data.message }; }; -export const deleteSubmission = async (submissionId: number): Promise => { - await api.delete(`/task-api/submissions/${submissionId}`); +export const deleteSubmission = async (submissionId: number): Promise<{ message: string }> => { + const res = await api.delete(`/task-api/submissions/${submissionId}`); + return { message: res.data.message }; }; \ No newline at end of file diff --git a/src/services/UserService.ts b/src/services/UserService.ts index ea3487d..61d60f3 100644 --- a/src/services/UserService.ts +++ b/src/services/UserService.ts @@ -7,9 +7,9 @@ import type { Mahasiswa, MahasiswaResponse } from "../types/project.types"; export const loginUser = async (payload: { email: string; password: string; -}): Promise<{ token: string }> => { +}): Promise<{ token: string, message: string }> => { const res = await api.post("/auth-api/login", payload); - return res.data.data; + return { token: res.data.data.token, message: res.data.message }; }; // GET /auth-api/me @@ -37,7 +37,7 @@ export function getInitials(fullName: string | undefined | null): string { // PATCH /auth-api/update-profile export const updateProfile = async ( payload: UpdateProfilePayload, -): Promise => { +): Promise<{ data: CurrentUser, message: string }> => { const formData = new FormData(); if (payload.full_name !== undefined) { @@ -51,9 +51,11 @@ export const updateProfile = async ( if (payload.profile_picture instanceof File) { formData.append("profile_picture", payload.profile_picture); } - const res = await api.patch("/auth-api/update-profile", formData); - return normalizeUser(res.data.data); + if (!res.data?.status) { + throw { response: { data: { message: res.data?.message } } }; + } + return { data: normalizeUser(res.data.data), message: res.data.message }; }; export const getMahasiswa = async (): Promise => { diff --git a/src/types/user.types.ts b/src/types/user.types.ts index 5f82018..5945f7e 100644 --- a/src/types/user.types.ts +++ b/src/types/user.types.ts @@ -18,4 +18,39 @@ export interface UpdateProfilePayload { email?: string; professional_bio?: string; profile_picture?: File | string | null; +} + +export interface MentorData { + id: number; + nama_depan: string; + nama_belakang: string; + full_name: string; + email: string; + role: string; + signature: string; + profile_picture: string | null; + professional_bio: string | null; + created_at: string; + updated_at: string; +} + +export interface CreateMentorPayload { + nama_depan: string; + nama_belakang: string; + full_name: string; + email: string; + password: string; + signature?: string; + professional_bio?: string; + profile_picture?: File; +} + +export interface UpdateMentorPayload { + nama_depan?: string; + nama_belakang?: string; + email?: string; + password?: string; + signature?: string; + professional_bio?: string; + profile_picture?: File; } \ No newline at end of file