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