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.
This commit is contained in:
@@ -46,6 +46,7 @@ export default function InternifyLogin() {
|
|||||||
document.cookie = `token=${token}; path=/; SameSite=Strict`;
|
document.cookie = `token=${token}; path=/; SameSite=Strict`;
|
||||||
|
|
||||||
const decoded = decodeJWT(token);
|
const decoded = decodeJWT(token);
|
||||||
|
console.log(decoded)
|
||||||
const role = decoded?.role;
|
const role = decoded?.role;
|
||||||
if (role === "intern") {
|
if (role === "intern") {
|
||||||
navigate("/intern");
|
navigate("/intern");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
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 { LuFileSpreadsheet } from "react-icons/lu";
|
||||||
import { io } from "socket.io-client";
|
import { io } from "socket.io-client";
|
||||||
import * as NotificationService from "../../services/NotificationService";
|
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 () => {
|
const handleMarkAllRead = async () => {
|
||||||
try {
|
try {
|
||||||
await NotificationService.markAllAsRead();
|
const res = await NotificationService.markAllAsRead();
|
||||||
setNotifications([]);
|
setNotifications([]);
|
||||||
customToast.success("All notifications marked as read!");
|
customToast.success("Berhasil", res.message);
|
||||||
} catch (err:any) {
|
} catch (err: any) {
|
||||||
customToast.error("Failed to mark all as read", err?.message);
|
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 && (
|
{notifications.length === 0 && (
|
||||||
<div className="flex flex-col items-center justify-center pt-14 gap-3">
|
<div className="flex flex-col items-center justify-center pt-14 gap-3">
|
||||||
<img src={mailRafiki} alt="empty notification" className="w-[280px]"/>
|
<img src={mailRafiki} alt="empty notification" className="w-[280px]" />
|
||||||
<p className="text-lg text-font">Belum ada pesan notifikasi yang diterima</p>
|
<p className="text-lg text-font">Belum ada pesan notifikasi yang diterima</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -110,7 +121,7 @@ export default function NotificationList() {
|
|||||||
{notifications.filter(n => n.group === "TODAY").map((item) => (
|
{notifications.filter(n => n.group === "TODAY").map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className={`relative flex items-start gap-4 p-5 bg-white border border-gray-100 rounded-xl shadow-[0_2px_10px_rgba(0,0,0,0.01)] ${item.type === 'new-task' ? 'border-l-4 border-l-[#B30000]' : ''
|
className={`relative flex items-start gap-4 p-5 bg-box border border-card-outline/40 rounded-xl shadow-[0_2px_10px_rgba(0,0,0,0.01)] ${item.type === 'new-task' ? 'border-l-4 border-l-[#B30000]' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Icon Container */}
|
{/* Icon Container */}
|
||||||
@@ -137,16 +148,19 @@ export default function NotificationList() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Conditional Actions */}
|
{/* Conditional Actions */}
|
||||||
{item.type === 'new-task' && item.link &&(
|
{item.type === 'new-task' && item.link && (
|
||||||
<div className="flex items-center gap-2 pt-3">
|
<div className="flex items-center gap-2 pt-3">
|
||||||
<button
|
<button
|
||||||
className="px-4 py-1.5 bg-[#B30000] hover:bg-[#990000] text-white text-xs font-bold rounded-lg shadow-sm transition-colors"
|
className="px-4 py-1.5 bg-[#B30000] hover:bg-[#990000] text-white text-xs font-bold rounded-lg shadow-sm transition-colors"
|
||||||
onClick={() => navigate(`${item.link}`)}>
|
onClick={() => navigate(`${item.link}`)}>
|
||||||
View Task
|
View Task
|
||||||
</button>
|
</button>
|
||||||
{/* <button className="px-4 py-1.5 border border-gray-200 text-gray-500 hover:text-gray-700 hover:bg-gray-50 text-xs font-semibold rounded-lg transition-colors">
|
<button
|
||||||
|
className="px-4 py-1.5 border border-card-outline text-font hover:text-font-shade hover:bg-gray-50 text-xs font-semibold rounded-lg transition-colors"
|
||||||
|
onClick={() => handleReadMessage(item.id)}
|
||||||
|
>
|
||||||
Later
|
Later
|
||||||
</button> */}
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -167,34 +181,6 @@ export default function NotificationList() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Header Section YESTERDAY */}
|
|
||||||
<div className="space-y-3 pt-2">
|
|
||||||
{/* <h2 className="text-[10px] font-bold tracking-widest text-gray-400 uppercase">
|
|
||||||
YESTERDAY
|
|
||||||
</h2> */}
|
|
||||||
|
|
||||||
{notifications.filter(n => n.group === "YESTERDAY").map((item) => (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
className="relative flex items-start gap-4 p-5 bg-gray-50 border border-gray-100 rounded-xl"
|
|
||||||
>
|
|
||||||
<div className="p-2.5 bg-gray-200 text-gray-500 rounded-xl">
|
|
||||||
<Info className="w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-grow space-y-1">
|
|
||||||
<h3 className="text-sm font-bold text-gray-700">{item.title}</h3>
|
|
||||||
<p className="text-xs text-gray-500 font-medium leading-relaxed max-w-2xl">
|
|
||||||
{item.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span className="text-[11px] text-gray-400 font-medium whitespace-nowrap md:absolute md:top-5 md:right-5">
|
|
||||||
{item.time}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import UpdateProfileDialog from "./UpdateProfileDialogue";
|
|||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const { user, loading: userLoading } = useCurrentUser();
|
const { user, loading: userLoading } = useCurrentUser();
|
||||||
const { save, loading: saving, msgRef } = useUpdateProfile();
|
const { save, loading: saving } = useUpdateProfile();
|
||||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
|
|
||||||
const isIntern = user?.role === "intern";
|
const isIntern = user?.role === "intern";
|
||||||
@@ -93,15 +93,15 @@ export default function SettingsPage() {
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
loading: "Menyimpan perubahan...",
|
loading: "Menyimpan perubahan...",
|
||||||
success: () => ({
|
success: (updated) => ({
|
||||||
title: "Perubahan berhasil disimpan!",
|
title: "Perubahan berhasil disimpan!",
|
||||||
description: emailChanged
|
description: emailChanged
|
||||||
? "Email berubah, silakan login ulang..."
|
? "Email berubah, silakan login ulang..."
|
||||||
: msgRef.current.success ?? "",
|
: updated.message
|
||||||
}),
|
}),
|
||||||
error: () => ({
|
error: (err) => ({
|
||||||
title: "Gagal menyimpan perubahan!",
|
title: "Gagal menyimpan perubahan!",
|
||||||
description: msgRef.current.error ?? "",
|
description: err.message ?? "",
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -62,9 +62,8 @@ export default function CertificateAvailable({ project, certificate, templateUrl
|
|||||||
setDownloadingImg(true);
|
setDownloadingImg(true);
|
||||||
try {
|
try {
|
||||||
await downloadCertificateImage(certificate, templateUrl, "png");
|
await downloadCertificateImage(certificate, templateUrl, "png");
|
||||||
customToast.success("Sertifikat berhasil diunduh!");
|
|
||||||
} catch {
|
} catch {
|
||||||
customToast.error("Gagal mengunduh sertifikat!");
|
customToast.error("Gagal mengunduh sertifikat!", "Terjadi kendala saat mengunduh sertifikat!");
|
||||||
} finally {
|
} finally {
|
||||||
setDownloadingImg(false);
|
setDownloadingImg(false);
|
||||||
}
|
}
|
||||||
@@ -74,9 +73,8 @@ export default function CertificateAvailable({ project, certificate, templateUrl
|
|||||||
setDownloadingPdf(true);
|
setDownloadingPdf(true);
|
||||||
try {
|
try {
|
||||||
await downloadCertificatePdf(certificate, templateUrl);
|
await downloadCertificatePdf(certificate, templateUrl);
|
||||||
customToast.success("Sertifikat berhasil diunduh!");
|
|
||||||
} catch {
|
} catch {
|
||||||
customToast.error("Gagal mengunduh sertifikat!");
|
customToast.error("Gagal mengunduh sertifikat!", "Terjadi kendala saat mengunduh sertifikat!");
|
||||||
} finally {
|
} finally {
|
||||||
setDownloadingPdf(false);
|
setDownloadingPdf(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export default function TaskFormFile({ taskId, projectId, deadline, initialSubmi
|
|||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
customToast.error(
|
customToast.error(
|
||||||
"File belum dipilih!",
|
"File belum dipilih!",
|
||||||
"Silakan pilih file terlebih dahulu sebelum mengirim."
|
"Choose your file before submit it!"
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -63,11 +63,12 @@ export default function TaskFormFile({ taskId, projectId, deadline, initialSubmi
|
|||||||
isEditing ? updateFile(submission!.id, files) : submitFile(files),
|
isEditing ? updateFile(submission!.id, files) : submitFile(files),
|
||||||
{
|
{
|
||||||
loading: isEditing ? "Memperbarui file..." : "Mengirim file...",
|
loading: isEditing ? "Memperbarui file..." : "Mengirim file...",
|
||||||
success: () => ({
|
success: (res) => ({
|
||||||
title: isEditing ? "File berhasil diperbarui!" : "File berhasil dikirim!",
|
title: isEditing ? "File berhasil diperbarui!" : "File berhasil dikirim!",
|
||||||
description: isEditing
|
description: res?.message ??
|
||||||
? "Perubahan submission Anda telah disimpan."
|
(isEditing
|
||||||
: "Submission Anda telah berhasil dikirim.",
|
? "Perubahan submission Anda telah disimpan."
|
||||||
|
: "Link berhasil dikirim."),
|
||||||
}),
|
}),
|
||||||
error: (err) => ({
|
error: (err) => ({
|
||||||
title: isEditing ? "Gagal memperbarui file!" : "Gagal mengirim file!",
|
title: isEditing ? "Gagal memperbarui file!" : "Gagal mengirim file!",
|
||||||
@@ -86,8 +87,6 @@ export default function TaskFormFile({ taskId, projectId, deadline, initialSubmi
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!confirm("Yakin ingin menghapus submission ini?")) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ok = await remove(submission!.id);
|
const ok = await remove(submission!.id);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
@@ -95,7 +94,7 @@ export default function TaskFormFile({ taskId, projectId, deadline, initialSubmi
|
|||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
customToast.success(
|
customToast.success(
|
||||||
"Submission berhasil dihapus!",
|
"Submission berhasil dihapus!",
|
||||||
"Anda dapat mengirim submission baru kapan saja."
|
ok.message
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -27,24 +27,18 @@ export default function TaskFormLink({
|
|||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!inputLink.trim()) {
|
|
||||||
customToast.error(
|
|
||||||
"Link belum dibuat",
|
|
||||||
"Masukkan link untuk mengirim submission"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await customToast.promise(
|
await customToast.promise(
|
||||||
isEditing ? updateLink(submission!.id, inputLink) : submitLink(inputLink),
|
isEditing ? updateLink(submission!.id, inputLink) : submitLink(inputLink),
|
||||||
{
|
{
|
||||||
loading: isEditing ? "Memperbarui Link..." : "Mengirim Link...",
|
loading: isEditing ? "Memperbarui Link..." : "Mengirim Link...",
|
||||||
success: () => ({
|
success: (res) => ({
|
||||||
title: isEditing ? "Link diperbaharui" : "Link berhasil dikirim",
|
title: isEditing ? "Link diperbaharui" : "Link berhasil dikirim",
|
||||||
description: isEditing
|
description:
|
||||||
? "Perubahan submission Anda telah disimpan."
|
res?.message ??
|
||||||
: "Submission Anda telah berhasil dikirim.",
|
(isEditing
|
||||||
|
? "Perubahan submission Anda telah disimpan."
|
||||||
|
: "Link berhasil dikirim."),
|
||||||
}),
|
}),
|
||||||
error: (err) => ({
|
error: (err) => ({
|
||||||
title: isEditing ? "Gagal memperbarui link!" : "Gagal mengirim link!",
|
title: isEditing ? "Gagal memperbarui link!" : "Gagal mengirim link!",
|
||||||
@@ -56,13 +50,10 @@ export default function TaskFormLink({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
} catch {
|
} catch { }
|
||||||
// error sudah ditangani toast, tetap di form
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!confirm("Yakin ingin menghapus submission ini?")) return;
|
|
||||||
try {
|
try {
|
||||||
const ok = await remove(submission!.id);
|
const ok = await remove(submission!.id);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
@@ -70,7 +61,7 @@ export default function TaskFormLink({
|
|||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
customToast.success(
|
customToast.success(
|
||||||
"Submission berhasil dihapus!",
|
"Submission berhasil dihapus!",
|
||||||
"Anda dapat mengirim submission baru kapan saja."
|
ok.message
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -90,7 +81,7 @@ export default function TaskFormLink({
|
|||||||
deadline={deadline}
|
deadline={deadline}
|
||||||
onEdit={() => setIsEditing(true)}
|
onEdit={() => setIsEditing(true)}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
disabled={!isProjectActive}
|
disabled={!isProjectActive}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,13 +36,15 @@ export default function CertificateDetail() {
|
|||||||
upload(project.id, selectedFile),
|
upload(project.id, selectedFile),
|
||||||
{
|
{
|
||||||
loading: "Mengupload template...",
|
loading: "Mengupload template...",
|
||||||
success: () => ({
|
success: (result) => ({
|
||||||
title: "Upload berhasil",
|
title: result?.success === false ? "Upload gagal" : "Upload berhasil",
|
||||||
description: "Template sertifikat berhasil disimpan.",
|
description: result?.message ?? (result?.success === false
|
||||||
|
? "Terjadi kesalahan saat upload."
|
||||||
|
: "Template sertifikat berhasil disimpan."),
|
||||||
}),
|
}),
|
||||||
error: (err) => ({
|
error: (err) => ({
|
||||||
title: "Upload gagal",
|
title: "Upload gagal",
|
||||||
description: err instanceof Error ? err.message : "Terjadi kesalahan saat upload.",
|
description: err?.message ?? "Terjadi kesalahan saat upload.",
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
).then(async (result) => {
|
).then(async (result) => {
|
||||||
|
|||||||
@@ -58,11 +58,11 @@ export default function InternsManagement() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const res = await remove({ id_project: matchedProject.id, id_user: intern.id });
|
const res = await remove({ id_project: matchedProject.id, id_user: intern.id });
|
||||||
if (res !== null) {
|
if (res.success) {
|
||||||
customToast.success('Intern removed', `${intern.name} has been successfully removed from the project.`);
|
customToast.success('Intern removed', res.message);
|
||||||
refetch();
|
refetch();
|
||||||
} else {
|
} else {
|
||||||
customToast.error('Failed to remove', 'An error occurred while removing the intern. Please try again.');
|
customToast.error('Failed to remove', res.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,19 +14,13 @@ export default function TaskTab({ project }: { project: ProjectDetail }) {
|
|||||||
const [selectedTask, setSelectedTask] = useState<InternTaskItem | null>(null);
|
const [selectedTask, setSelectedTask] = useState<InternTaskItem | null>(null);
|
||||||
const isProjectActive = project?.status === "active"
|
const isProjectActive = project?.status === "active"
|
||||||
|
|
||||||
const handleDeleteTask = async (taskId: number, taskTitle: string) => {
|
const handleDeleteTask = async (taskId: number) => {
|
||||||
const confirmed = window.confirm(`Yakin ingin menghapus task “${taskTitle}”?`);
|
|
||||||
if (!confirmed) return;
|
|
||||||
|
|
||||||
const del = await remove(taskId, String(project.id));
|
const del = await remove(taskId, String(project.id));
|
||||||
if (del) {
|
if (!del.failed) {
|
||||||
try {
|
customToast.success("Berhasil Menghapus Task", del.message);
|
||||||
customToast.success("Berhasil", `Task ${taskTitle} berhasil dihapus`)
|
refetch();
|
||||||
} catch (error) {
|
} else {
|
||||||
customToast.error("Gagal", `Task ${taskTitle} gagal dihapus`)
|
customToast.error("Gagal Menghapus Task", del.message);
|
||||||
} finally {
|
|
||||||
refetch();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,7 +77,7 @@ export default function TaskTab({ project }: { project: ProjectDetail }) {
|
|||||||
<Pencil size={20} />
|
<Pencil size={20} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDeleteTask(task.id, task.title)}
|
onClick={() => handleDeleteTask(task.id)}
|
||||||
disabled={!isProjectActive}
|
disabled={!isProjectActive}
|
||||||
className="border border-gray-500 text-font-shade rounded-lg px-2 py-1 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
className="border border-gray-500 text-font-shade rounded-lg px-2 py-1 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ export default function InternsTab({ project }: { project: ProjectDetail }) {
|
|||||||
|
|
||||||
const handleRemove = async (id_user: number) => {
|
const handleRemove = async (id_user: number) => {
|
||||||
const res = await remove({ id_project: project.id, id_user });
|
const res = await remove({ id_project: project.id, id_user });
|
||||||
if (res !== null) {
|
if (res.success) {
|
||||||
setMembers(prev => prev.filter(m => m.id !== id_user));
|
setMembers(prev => prev.filter(m => m.id !== id_user));
|
||||||
customToast.success('Member removed', 'The intern has been successfully removed from this project.');
|
customToast.success('Member removed', res.message);
|
||||||
} else {
|
} else {
|
||||||
customToast.error('Failed to remove', 'An error occurred while removing the member. Please try again.');
|
customToast.error('Failed to remove', res.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -74,16 +74,15 @@ export default function ManageInternsModal({ isOpen, onClose, projectId, initial
|
|||||||
|
|
||||||
const handleAssign = async (intern: AssignableIntern) => {
|
const handleAssign = async (intern: AssignableIntern) => {
|
||||||
const result = await assign({ id_project: projectId, id_user: intern.id });
|
const result = await assign({ id_project: projectId, id_user: intern.id });
|
||||||
if (result) {
|
if (result.success) {
|
||||||
// Tandai sebagai sudah ditambah — intern hilang dari dialog (tidak muncul lagi)
|
|
||||||
setAddedIds(prev => new Set([...prev, intern.id]));
|
setAddedIds(prev => new Set([...prev, intern.id]));
|
||||||
customToast.success('Intern added', `${intern.full_name} has been successfully added to the project.`);
|
customToast.success('Intern added', result.message);
|
||||||
} else {
|
} else {
|
||||||
customToast.error('Failed to add', 'An error occurred while adding the intern. Please try again.');
|
customToast.error('Failed to add', result.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getInitials = (m: AssignableIntern) =>
|
const getInitials = (m: AssignableIntern) =>
|
||||||
m.full_name.split(' ').slice(0, 2).map(n => n[0]).join('').toUpperCase();
|
m.full_name.split(' ').slice(0, 2).map(n => n[0]).join('').toUpperCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -59,11 +59,11 @@ export default function AddInternsModal({ isOpen, onClose, userId }: AddInternsM
|
|||||||
id_project: Number(selectedProjectId),
|
id_project: Number(selectedProjectId),
|
||||||
id_user: Number(selectedUserId),
|
id_user: Number(selectedUserId),
|
||||||
});
|
});
|
||||||
if (res !== null) {
|
if (res.success) {
|
||||||
customToast.success('Intern assigned', 'The intern has been successfully assigned to the project.');
|
customToast.success('Intern assigned', res.message);
|
||||||
onClose();
|
onClose();
|
||||||
} else {
|
} else {
|
||||||
customToast.error('Failed to assign', 'An error occurred while assigning the intern. Please try again.');
|
customToast.error('Failed to assign', res.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Search, Eye, Download, Share2,
|
Search, Eye, Download,
|
||||||
ChevronLeft, ChevronRight, Loader2,
|
ChevronLeft, ChevronRight, Loader2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { Certificate } from "../../../../types/certificate.types";
|
import type { Certificate } from "../../../../types/certificate.types";
|
||||||
@@ -132,13 +132,13 @@ export default function CertResultTable({ certificates, templateUrl }: Certifica
|
|||||||
: <Download className="w-4 h-4" />
|
: <Download className="w-4 h-4" />
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
|
{/*
|
||||||
<button
|
<button
|
||||||
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-md transition-colors"
|
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-md transition-colors"
|
||||||
title="Share"
|
title="Share"
|
||||||
>
|
>
|
||||||
<Share2 className="w-4 h-4" />
|
<Share2 className="w-4 h-4" />
|
||||||
</button>
|
</button> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
|
|||||||
const [invitedEmails, setInvitedEmails] = useState<string[]>([]);
|
const [invitedEmails, setInvitedEmails] = useState<string[]>([]);
|
||||||
const [startDate, setStartDate] = useState("");
|
const [startDate, setStartDate] = useState("");
|
||||||
const [endDate, setEndDate] = useState("");
|
const [endDate, setEndDate] = useState("");
|
||||||
|
const [dateError, setDateError] = useState("");
|
||||||
const { create, loading, error } = useCreateProject();
|
const { create, loading, error } = useCreateProject();
|
||||||
const { interns: assignableInterns, loading: loadingInterns } = useAssignableInterns();
|
const { interns: assignableInterns, loading: loadingInterns } = useAssignableInterns();
|
||||||
const [showDropdown, setShowDropdown] = useState(false);
|
const [showDropdown, setShowDropdown] = useState(false);
|
||||||
@@ -68,6 +69,14 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
|
|||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
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 = {
|
const payload: CreateProjectPayload = {
|
||||||
project_icon: selectedIcon,
|
project_icon: selectedIcon,
|
||||||
project_name: projectName,
|
project_name: projectName,
|
||||||
@@ -81,13 +90,12 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await create(payload);
|
const result = await create(payload);
|
||||||
if (result) {
|
if (result.success) {
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
onClose();
|
onClose();
|
||||||
customToast.success(
|
customToast.success("Project berhasil dibuat!", (result.message));
|
||||||
"Project berhasil dibuat!",
|
} else {
|
||||||
`${projectName} sudah aktif dan siap dikelola.`
|
customToast.error("Gagal membuat project", (result.message));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -263,14 +271,14 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
|
|||||||
{/* Start Date */}
|
{/* Start Date */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="text-xs font-bold text-gray-700 tracking-wide">
|
<label className="text-xs font-bold text-gray-700 tracking-wide">
|
||||||
Start Date
|
Tanggal Mulai
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
required
|
required
|
||||||
value={startDate}
|
value={startDate}
|
||||||
onChange={(e) => 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"
|
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"
|
||||||
/>
|
/>
|
||||||
<span className="absolute right-3.5 top-3 text-gray-400 pointer-events-none">
|
<span className="absolute right-3.5 top-3 text-gray-400 pointer-events-none">
|
||||||
@@ -281,14 +289,14 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
|
|||||||
{/* End Date */}
|
{/* End Date */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="text-xs font-bold text-gray-700 tracking-wide">
|
<label className="text-xs font-bold text-gray-700 tracking-wide">
|
||||||
End Date
|
Tanggal Berakhir
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
required
|
required
|
||||||
value={endDate}
|
value={endDate}
|
||||||
onChange={(e) => 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"
|
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"
|
||||||
/>
|
/>
|
||||||
<span className="absolute right-3.5 top-3 text-gray-400 pointer-events-none">
|
<span className="absolute right-3.5 top-3 text-gray-400 pointer-events-none">
|
||||||
@@ -296,9 +304,12 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{dateError && (
|
||||||
|
<p className="text-xs text-red font-medium">{dateError}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-xs text-red-600 text-center font-medium">{error}</p>
|
<p className="text-xs text-red text-center font-medium">{error}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Action Row Buttons */}
|
{/* Action Row Buttons */}
|
||||||
|
|||||||
@@ -58,18 +58,14 @@ export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess
|
|||||||
submission_type: submissionType,
|
submission_type: submissionType,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result) {
|
if (result.data) {
|
||||||
customToast.success(
|
customToast.success('Task created', result.message);
|
||||||
'Task created',
|
|
||||||
`"${taskTitle}" has been successfully added to the project.`);
|
|
||||||
onSuccess();
|
onSuccess();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
customToast.error(
|
customToast.error('Failed to create task', result.message);
|
||||||
'Failed to create task',
|
|
||||||
'An error occurred while saving the task. Please try again.');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dialog ref={dialogRef} className="custom-dialog">
|
<dialog ref={dialogRef} className="custom-dialog">
|
||||||
<div className="dialog-header">
|
<div className="dialog-header">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { X } from "lucide-react";
|
|||||||
import { LuSave } from "react-icons/lu";
|
import { LuSave } from "react-icons/lu";
|
||||||
import { useUpdateTask } from "../../../../hooks/useTasks";
|
import { useUpdateTask } from "../../../../hooks/useTasks";
|
||||||
import type { ProjectTask, SubmissionType, UpdateTaskPayload } from "../../../../types/task.types";
|
import type { ProjectTask, SubmissionType, UpdateTaskPayload } from "../../../../types/task.types";
|
||||||
|
import { customToast } from "../../../utils/showToast";
|
||||||
|
|
||||||
interface EditTaskModalProps {
|
interface EditTaskModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -79,9 +80,12 @@ export default function EditSubmissionModal({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = await update(task.id, payload, String(task.id_project));
|
const result = await update(task.id, payload, String(task.id_project));
|
||||||
if (result) {
|
if (result.data) {
|
||||||
onSuccess(result);
|
customToast.success("Task diperbarui", result.message);
|
||||||
|
onSuccess(result.data);
|
||||||
onClose();
|
onClose();
|
||||||
|
} else {
|
||||||
|
customToast.error("Gagal", result.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export default function EligibleInternTable({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (selected.size === 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,15 +134,14 @@ export default function EligibleInternTable({
|
|||||||
const ok = await customToast.promise(
|
const ok = await customToast.promise(
|
||||||
(async () => {
|
(async () => {
|
||||||
const result = await SubmitCertificates(project.id, Array.from(selected));
|
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 zip = new JSZip();
|
||||||
const templateUrl = resolveImageUrl(project.certificate_template!);
|
const templateUrl = resolveImageUrl(project.certificate_template!);
|
||||||
|
|
||||||
const duration = formatDateRange(project.start_date, project.end_date);
|
const duration = formatDateRange(project.start_date, project.end_date);
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
result.map(async (cert) => {
|
result.data.map(async (cert) => {
|
||||||
const blob = await generateCertificate(
|
const blob = await generateCertificate(
|
||||||
templateUrl,
|
templateUrl,
|
||||||
cert.user.full_name,
|
cert.user.full_name,
|
||||||
|
|||||||
@@ -37,15 +37,15 @@ export default function MentorProjectCard({ onArchived, onCompleted, ...project
|
|||||||
const result = await archive(project.id);
|
const result = await archive(project.id);
|
||||||
setShowArchiveDialog(false);
|
setShowArchiveDialog(false);
|
||||||
|
|
||||||
if (result) {
|
if (result.success) {
|
||||||
customToast.success("Berhasil!", "Projek berubah status menjadi Archived (Diarsipkan");
|
customToast.success("Berhasil!", result.message);
|
||||||
onArchived?.();
|
onArchived?.();
|
||||||
} else {
|
} else {
|
||||||
customToast.error("Gagal!", "Projek gagal diarsipkan (archived)");
|
customToast.error("Gagal!", result.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ── Complete ────────────────────────────────────────────── */
|
/* Complete */
|
||||||
const handleCompleteButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const handleCompleteButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setShowCompleteDialog(true);
|
setShowCompleteDialog(true);
|
||||||
@@ -55,11 +55,11 @@ export default function MentorProjectCard({ onArchived, onCompleted, ...project
|
|||||||
const result = await complete(project.id);
|
const result = await complete(project.id);
|
||||||
setShowCompleteDialog(false);
|
setShowCompleteDialog(false);
|
||||||
|
|
||||||
if (result) {
|
if (result?.success) {
|
||||||
customToast.success("Berhasil!", "Projek berubah status menjadi completed (selesai)");
|
customToast.success("Berhasil!", result.message);
|
||||||
onCompleted?.();
|
onCompleted?.();
|
||||||
} else {
|
} else {
|
||||||
customToast.error("Gagal!", "Projek gagal diselesaikan (completed)");
|
customToast.error("Gagal!", result?.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const LAYOUT = {
|
|||||||
internNameSmall: { x: 0.82, y: 0.889, size: 33, font: "Great Vibes", color: "#800000" },
|
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" },
|
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" },
|
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" },
|
certNoSmall: { x: 0.825, y: 0.91, size: 22, font: "Grenze", color: "#800000" },
|
||||||
qr: { x: 0.77, y: 0.715, size: 0.103 },
|
qr: { x: 0.77, y: 0.715, size: 0.103 },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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<string, unknown>)
|
||||||
|
.flat()
|
||||||
|
.filter((v): v is string => typeof v === "string")
|
||||||
|
.join(" ") || fallback;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
@@ -53,6 +53,16 @@ export const customToast = {
|
|||||||
const loadingId = toast.loading(messages.loading);
|
const loadingId = toast.loading(messages.loading);
|
||||||
try {
|
try {
|
||||||
const data = await promise;
|
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);
|
toast.dismiss(loadingId);
|
||||||
const { title, description } = messages.success(data);
|
const { title, description } = messages.success(data);
|
||||||
customToast.success(title, description);
|
customToast.success(title, description);
|
||||||
@@ -60,7 +70,7 @@ export const customToast = {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.dismiss(loadingId);
|
toast.dismiss(loadingId);
|
||||||
const { title, description } = messages.error(err);
|
const { title, description } = messages.error(err);
|
||||||
customToast.error(title, description);
|
customToast.error(title, description ?? (err as Error)?.message);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
generateCertificate,
|
generateCertificate,
|
||||||
verifyCertificateByUuid
|
verifyCertificateByUuid
|
||||||
} from "../services/CertificateService";
|
} from "../services/CertificateService";
|
||||||
import { AxiosError } from "axios";
|
|
||||||
|
|
||||||
// GET /certificate-api/my-certificates
|
// GET /certificate-api/my-certificates
|
||||||
export const useMyCertificates = () => {
|
export const useMyCertificates = () => {
|
||||||
@@ -93,10 +92,18 @@ export const useClaimCertificate = () => {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const claim = async (id_project: number) => {
|
const claim = async (id_project: number) => {
|
||||||
setLoading(true); setError(null);
|
setLoading(true);
|
||||||
try { return await claimCertificate(id_project); }
|
setError(null);
|
||||||
catch { setError("Gagal claim sertifikat."); return null; }
|
try {
|
||||||
finally { setLoading(false); }
|
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 };
|
return { claim, loading, error };
|
||||||
@@ -107,39 +114,18 @@ export const useGenerateCertificates = () => {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [success, setSuccess] = useState(false);
|
const [success, setSuccess] = useState(false);
|
||||||
|
|
||||||
const generate = async (
|
const generate = async (id_project: number, id_users: number[]) => {
|
||||||
id_project: number,
|
|
||||||
id_users: number[]
|
|
||||||
): Promise<Certificate[] | null> => {
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setSuccess(false);
|
setSuccess(false);
|
||||||
try {
|
try {
|
||||||
const data = await generateCertificate(id_project, id_users);
|
const { data, message } = await generateCertificate(id_project, id_users);
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
return data;
|
return { data, message };
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
if (err instanceof AxiosError) {
|
const message = err?.response?.data?.message ?? "Gagal generate sertifikat.";
|
||||||
switch (err.response?.status) {
|
setError(message);
|
||||||
case 400:
|
return { message };
|
||||||
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;
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -159,10 +145,18 @@ export const useUploadCertificateTemplate = () => {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const upload = async (id_project: number, file: File) => {
|
const upload = async (id_project: number, file: File) => {
|
||||||
setLoading(true); setError(null);
|
setLoading(true);
|
||||||
try { return await uploadCertificateTemplate(id_project, file); }
|
setError(null);
|
||||||
catch { setError("Gagal upload template."); return null; }
|
try {
|
||||||
finally { setLoading(false); }
|
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 };
|
return { upload, loading, error };
|
||||||
|
|||||||
+21
-18
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import type { MentorData, CreateMentorPayload, UpdateMentorPayload } from "../services/MentorServices";
|
import type { MentorData, CreateMentorPayload, UpdateMentorPayload } from "../types/user.types";
|
||||||
import {
|
import {
|
||||||
createMentorService,
|
createMentorService,
|
||||||
getMentorsService,
|
getMentorsService,
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
deleteMentorService,
|
deleteMentorService,
|
||||||
} from "../services/MentorServices"
|
} from "../services/MentorServices"
|
||||||
|
|
||||||
// ── GET ALL ──────────────────────────────────────────────────────────────────
|
// GET ALL MENTORS
|
||||||
export const useMentors = () => {
|
export const useMentors = () => {
|
||||||
const [mentors, setMentors] = useState<MentorData[]>([]);
|
const [mentors, setMentors] = useState<MentorData[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -31,7 +31,7 @@ export const useMentors = () => {
|
|||||||
return { mentors, loading, error, refetch: fetch };
|
return { mentors, loading, error, refetch: fetch };
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── GET DETAIL ───────────────────────────────────────────────────────────────
|
// GET DETAIL
|
||||||
export const useMentorDetail = (id: number | null) => {
|
export const useMentorDetail = (id: number | null) => {
|
||||||
const [mentor, setMentor] = useState<MentorData | null>(null);
|
const [mentor, setMentor] = useState<MentorData | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -55,18 +55,19 @@ export const useMentorDetail = (id: number | null) => {
|
|||||||
return { mentor, loading, error, refetch: fetch };
|
return { mentor, loading, error, refetch: fetch };
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── CREATE ───────────────────────────────────────────────────────────────────
|
// CREATE
|
||||||
export const useCreateMentor = () => {
|
export const useCreateMentor = () => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const createMentor = async (payload: CreateMentorPayload): Promise<MentorData | null> => {
|
const createMentor = async (payload: CreateMentorPayload): Promise<{ data: MentorData; message: string } | null> => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
return await createMentorService(payload);
|
return await createMentorService(payload);
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
setError("Gagal membuat akun mentor.");
|
const msg = err?.response?.data?.message ?? "Gagal membuat akun mentor.";
|
||||||
|
setError(msg);
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -76,7 +77,7 @@ export const useCreateMentor = () => {
|
|||||||
return { createMentor, loading, error };
|
return { createMentor, loading, error };
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── UPDATE ───────────────────────────────────────────────────────────────────
|
// UPDATE
|
||||||
export const useUpdateMentor = () => {
|
export const useUpdateMentor = () => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -84,13 +85,14 @@ export const useUpdateMentor = () => {
|
|||||||
const updateMentor = async (
|
const updateMentor = async (
|
||||||
id: number,
|
id: number,
|
||||||
payload: UpdateMentorPayload
|
payload: UpdateMentorPayload
|
||||||
): Promise<MentorData | null> => {
|
): Promise<{ data: MentorData; message: string } | null> => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
return await updateMentorService(id, payload);
|
return await updateMentorService(id, payload);
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
setError("Gagal memperbarui data mentor.");
|
const msg = err?.response?.data?.message ?? "Gagal memperbarui data mentor.";
|
||||||
|
setError(msg);
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -100,20 +102,21 @@ export const useUpdateMentor = () => {
|
|||||||
return { updateMentor, loading, error };
|
return { updateMentor, loading, error };
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── DELETE ───────────────────────────────────────────────────────────────────
|
// DELETE
|
||||||
export const useDeleteMentor = () => {
|
export const useDeleteMentor = () => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const deleteMentor = async (id: number): Promise<boolean> => {
|
const deleteMentor = async (id: number): Promise<{ success: boolean; message: string }> => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
await deleteMentorService(id);
|
const res = await deleteMentorService(id);
|
||||||
return true;
|
return { success: true, message: res.message };
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
setError("Gagal menghapus mentor.");
|
const msg = err?.response?.data?.message ?? "Gagal menghapus mentor.";
|
||||||
return false;
|
setError(msg);
|
||||||
|
return { success: false, message: msg };
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,18 +24,28 @@ export const useNotifications = () => {
|
|||||||
|
|
||||||
const markAllRead = async () => {
|
const markAllRead = async () => {
|
||||||
try {
|
try {
|
||||||
await markAllAsRead();
|
const { message } = await markAllAsRead();
|
||||||
setNotifications(prev => prev.map(n => ({ ...n, is_read: true })));
|
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) => {
|
const markOneRead = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
await markAsRead(id);
|
const { message } = await markAsRead(id);
|
||||||
setNotifications(prev =>
|
setNotifications(prev =>
|
||||||
prev.map(n => n.id === id ? { ...n, is_read: true } : n)
|
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;
|
const unreadCount = notifications.filter(n => !n.is_read).length;
|
||||||
|
|||||||
+36
-24
@@ -188,10 +188,12 @@ export const useCreateProject = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
return await createProject(payload);
|
const { data, message } = await createProject(payload);
|
||||||
} catch {
|
return { success: true as const, data, message };
|
||||||
setError("Gagal membuat project.");
|
} catch (err: any) {
|
||||||
return null;
|
const msg = err?.response?.data?.message ?? "Gagal membuat project.";
|
||||||
|
setError(msg);
|
||||||
|
return { success: false as const, message: msg };
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -220,10 +222,12 @@ export const useUpdateProject = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
return await updateProject(id, payload);
|
const { data, message } = await updateProject(id, payload);
|
||||||
} catch {
|
return { success: true as const, data, message };
|
||||||
setError("Gagal mengupdate project.");
|
} catch (err: any) {
|
||||||
return null;
|
const msg = err?.response?.data?.message ?? "Gagal mengupdate project.";
|
||||||
|
setError(msg);
|
||||||
|
return { success: false as const, message: msg };
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -241,10 +245,12 @@ export const useArchiveProject = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
return await archiveProject(id);
|
const { data, message } = await archiveProject(id);
|
||||||
} catch {
|
return { success: true as const, data, message };
|
||||||
setError("Gagal mengarsipkan project.");
|
} catch (err: any) {
|
||||||
return null;
|
const msg = err?.response?.data?.message ?? "Gagal mengarsipkan project.";
|
||||||
|
setError(msg);
|
||||||
|
return { success: false as const, message: msg };
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -261,10 +267,12 @@ export const useCompleteProject = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
return await completeProject(id);
|
const { data, message } = await completeProject(id);
|
||||||
} catch {
|
return { success: true as const, data, message}
|
||||||
setError("Gagal menyelesaikan project.");
|
} catch (err: any) {
|
||||||
return null;
|
const msg = err?.response?.data?.message ?? "Gagal menyelesaikan project.";
|
||||||
|
setError(msg);
|
||||||
|
return { success: false as const, message: msg};
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -282,10 +290,12 @@ export const useAssignMember = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
return await assignMember(payload);
|
const { data, message } = await assignMember(payload);
|
||||||
} catch {
|
return { success: true as const, data, message };
|
||||||
setError("Gagal assign member.");
|
} catch (err: any) {
|
||||||
return null;
|
const msg = err?.response?.data?.message ?? "Gagal assign member.";
|
||||||
|
setError(msg);
|
||||||
|
return { success: false as const, message: msg };
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -303,10 +313,12 @@ export const useRemoveMember = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
return await removeMember(payload);
|
const { data, message } = await removeMember(payload);
|
||||||
} catch {
|
return { success: true as const, data, message };
|
||||||
setError("Gagal remove member.");
|
} catch (err: any) {
|
||||||
return null;
|
const msg = err?.response?.data?.message ?? "Gagal remove member.";
|
||||||
|
setError(msg);
|
||||||
|
return { success: false as const, message: msg };
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-44
@@ -97,16 +97,16 @@ export const useCreateTask = (projectId: string) => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const create = async (
|
const create = async (payload: CreateTaskPayload) => {
|
||||||
payload: CreateTaskPayload,
|
|
||||||
): Promise<ProjectTask | null> => {
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
return await createTask(projectId, payload);
|
const { data, message } = await createTask(projectId, payload);
|
||||||
} catch {
|
return { data, message };
|
||||||
setError("Gagal membuat task.");
|
} catch (err: any) {
|
||||||
return null;
|
const message = err?.response?.data?.message ?? "Gagal membuat task.";
|
||||||
|
setError(message);
|
||||||
|
return { message };
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -128,10 +128,12 @@ export const useUpdateTask = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
return await updateTask(taskId, payload, projectId);
|
const { data, message } = await updateTask(taskId, payload, projectId);
|
||||||
} catch {
|
return { data, message };
|
||||||
setError("Gagal mengupdate task.");
|
} catch (err: any) {
|
||||||
return null;
|
const message = err?.response?.data?.message ?? "Gagal mengupdate task.";
|
||||||
|
setError(message);
|
||||||
|
return { message };
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -149,16 +151,17 @@ export const useDeleteTask = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
await deleteTask(taskId, projectId);
|
const data = await deleteTask(taskId, projectId);
|
||||||
return true;
|
const msg = data?.message ?? "Task berhasil dihapus.";
|
||||||
} catch {
|
return { message: msg };
|
||||||
setError("Gagal menghapus task.");
|
} catch (err: any) {
|
||||||
return false;
|
const msg = err?.response?.data?.message ?? "Gagal menghapus task.";
|
||||||
|
setError(msg);
|
||||||
|
return { message: msg, failed: true as const };
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { remove, loading, error };
|
return { remove, loading, error };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -179,12 +182,12 @@ export const useSubmission = (
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const res = await submitTaskFile(taskId, files, projectId);
|
const res = await submitTaskFile(taskId, files, projectId);
|
||||||
if (res) setSubmission(res);
|
if (res.data) setSubmission(res.data);
|
||||||
return res;
|
return res;
|
||||||
} catch (err){
|
} catch (err: any) {
|
||||||
setError("Gagal submit file.");
|
const msg = err?.response?.data?.message ?? "Gagal submit file.";
|
||||||
throw error;
|
setError(msg);
|
||||||
return null;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -195,10 +198,11 @@ export const useSubmission = (
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const res = await submitTaskLink(taskId, url, projectId);
|
const res = await submitTaskLink(taskId, url, projectId);
|
||||||
if (res) setSubmission(res);
|
if (res.data) setSubmission(res.data);
|
||||||
return res;
|
return res;
|
||||||
} catch (err){
|
} catch (err: any) {
|
||||||
setError("Gagal submit link.");
|
const msg = err?.response?.data?.message ?? "Gagal submit link.";
|
||||||
|
setError(msg);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -210,10 +214,11 @@ export const useSubmission = (
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const res = await updateSubmissionFile(submissionId, files);
|
const res = await updateSubmissionFile(submissionId, files);
|
||||||
if (res) setSubmission(res);
|
if (res.data) setSubmission(res.data);
|
||||||
return res;
|
return res;
|
||||||
} catch (err){
|
} catch (err: any) {
|
||||||
setError("Gagal update submission.");
|
const msg = err?.response?.data?.message ?? "Gagal update submission.";
|
||||||
|
setError(msg);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -225,10 +230,11 @@ export const useSubmission = (
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const res = await updateSubmissionLink(submissionId, url);
|
const res = await updateSubmissionLink(submissionId, url);
|
||||||
if (res) setSubmission(res);
|
if (res.data) setSubmission(res.data);
|
||||||
return res;
|
return res;
|
||||||
} catch (err){
|
} catch (err: any) {
|
||||||
setError("Gagal update submission.");
|
const msg = err?.response?.data?.message ?? "Gagal update submission.";
|
||||||
|
setError(msg);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -239,12 +245,13 @@ export const useSubmission = (
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
await deleteSubmission(submissionId);
|
const res = await deleteSubmission(submissionId);
|
||||||
setSubmission(null);
|
setSubmission(null);
|
||||||
return true;
|
return { success: true as const, message: res.message };
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
setError("Gagal hapus submission.");
|
const msg = err?.response?.data?.message ?? "Gagal hapus submission.";
|
||||||
throw err;
|
setError(msg);
|
||||||
|
return { success: false as const, message: msg };
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -305,16 +312,14 @@ export const useDeadlines = () => {
|
|||||||
if (!user) return [];
|
if (!user) return [];
|
||||||
return isIntern
|
return isIntern
|
||||||
? internTasks.map((task) => ({
|
? internTasks.map((task) => ({
|
||||||
date: new Date(task.deadline_at),
|
date: new Date(task.deadline_at),
|
||||||
label: task.title,
|
label: task.title,
|
||||||
}))
|
}))
|
||||||
: mentorTasks.map((task) => ({
|
: mentorTasks.map((task) => ({
|
||||||
date: new Date(task.deadline_at),
|
date: new Date(task.deadline_at),
|
||||||
label: `[${task.project_name}] ${task.title}`,
|
label: `[${task.project_name}] ${task.title}`,
|
||||||
}));
|
}));
|
||||||
}, [isIntern, internTasks, mentorTasks, user]);
|
}, [isIntern, internTasks, mentorTasks, user]);
|
||||||
|
|
||||||
// Loading: tunggu user dulu, baru tunggu task yang relevan dengan rolenya
|
|
||||||
const loading = userLoading || (isIntern ? internLoading : mentorLoading);
|
const loading = userLoading || (isIntern ? internLoading : mentorLoading);
|
||||||
const error = userError || (isIntern ? internError : mentorError);
|
const error = userError || (isIntern ? internError : mentorError);
|
||||||
|
|
||||||
|
|||||||
+12
-66
@@ -16,7 +16,7 @@ let store: UserStore = { user: null, loading: true, error: null };
|
|||||||
const listeners = new Set<() => void>();
|
const listeners = new Set<() => void>();
|
||||||
|
|
||||||
const setStore = (patch: Partial<UserStore>) => {
|
const setStore = (patch: Partial<UserStore>) => {
|
||||||
store = { ...store, ...patch }; // objek baru -> getSnapshot berubah referensinya
|
store = { ...store, ...patch };
|
||||||
listeners.forEach((l) => l());
|
listeners.forEach((l) => l());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -27,12 +27,10 @@ const subscribe = (l: () => void) => {
|
|||||||
|
|
||||||
const getSnapshot = () => store;
|
const getSnapshot = () => store;
|
||||||
|
|
||||||
// useUpdateProfile setelah PATCH sukses -> broadcast ke semua konsumen
|
|
||||||
export const setCurrentUser = (user: CurrentUser | null) => {
|
export const setCurrentUser = (user: CurrentUser | null) => {
|
||||||
setStore({ user, loading: false, error: null });
|
setStore({ user, loading: false, error: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
// dipakai saat logout
|
|
||||||
export const clearCurrentUser = () => {
|
export const clearCurrentUser = () => {
|
||||||
fetched = false;
|
fetched = false;
|
||||||
inflight = null;
|
inflight = null;
|
||||||
@@ -87,15 +85,11 @@ export const useLogin = () => {
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const data = await loginUser({ email, password });
|
const data = await loginUser({ email, password });
|
||||||
clearCurrentUser(); // buang user lama agar tidak bocor antar-sesi
|
clearCurrentUser();
|
||||||
return data; // { token }
|
return data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const status = err?.response?.status;
|
const msg = err?.response?.data?.message ?? "Terjadi kesalahan. Coba beberapa saat lagi.";
|
||||||
if (status === 401 || status === 400) {
|
setError(msg);
|
||||||
setError("Email atau password salah.");
|
|
||||||
} else {
|
|
||||||
setError("Terjadi kesalahan. Coba beberapa saat lagi.");
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -105,6 +99,7 @@ export const useLogin = () => {
|
|||||||
return { login, loading, error, setError };
|
return { login, loading, error, setError };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Ambil 2 huruf (nama depan + nama belakang user)
|
||||||
export const getInitials = (fullName?: string | null): string => {
|
export const getInitials = (fullName?: string | null): string => {
|
||||||
if (!fullName) return "U";
|
if (!fullName) return "U";
|
||||||
const parts = fullName.trim().split(/\s+/).filter(Boolean);
|
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();
|
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 => {
|
export const resolveFileUrl = (path?: string | null): string | null => {
|
||||||
if (!path) return null;
|
if (!path) return null;
|
||||||
if (/^(https?:|data:|blob:)/i.test(path)) return path;
|
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
|
// PATCH /auth-api/update-profile
|
||||||
export const useUpdateProfile = () => {
|
export const useUpdateProfile = () => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [successMsg, setSuccessMsg] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const msgRef = useRef<{ success: string | null; error: string | null }>({
|
|
||||||
success: null,
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const save = async (payload: UpdateProfilePayload) => {
|
const save = async (payload: UpdateProfilePayload) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
|
||||||
setSuccessMsg(null);
|
|
||||||
msgRef.current = { success: null, error: null };
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updated = await updateProfile(payload);
|
const updated = await updateProfile(payload);
|
||||||
|
setCurrentUser(updated.data);
|
||||||
setCurrentUser(updated);
|
|
||||||
|
|
||||||
const msg = "Profile updated successfully!";
|
|
||||||
msgRef.current.success = msg;
|
|
||||||
setSuccessMsg(msg);
|
|
||||||
return updated;
|
return updated;
|
||||||
} catch (err: any) {
|
; } catch (err: any) {
|
||||||
const status = err?.response?.status;
|
const message = (err?.response?.data?.message, "Gagal menyimpan perubahan.");
|
||||||
const apiMessage = err?.response?.data?.message;
|
throw new Error(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);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { save, loading, error, successMsg, setSuccessMsg, setError, msgRef };
|
return { save, loading };
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ambil Seluruh Mahasiswa
|
// Ambil Seluruh Mahasiswa
|
||||||
|
|||||||
@@ -7,15 +7,20 @@ import type {
|
|||||||
} from "../types/certificate.types";
|
} from "../types/certificate.types";
|
||||||
|
|
||||||
// POST /certificate-api/claim
|
// POST /certificate-api/claim
|
||||||
export const claimCertificate = async (id_project: number): Promise<Certificate> => {
|
export const claimCertificate = async (
|
||||||
|
id_project: number
|
||||||
|
): Promise<{ data: Certificate; message: string }> => {
|
||||||
const res = await api.post("/certificate-api/claim", { id_project });
|
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
|
// POST /certificate-api/generate
|
||||||
export const generateCertificate = async (id_project: number, id_users: number[]): Promise<Certificate[]> => {
|
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 });
|
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
|
// GET /certificate-api/my-certificates
|
||||||
@@ -52,9 +57,9 @@ export const verifyCertificateByUuid = async (uuid: string): Promise<Certificate
|
|||||||
export const uploadCertificateTemplate = async (
|
export const uploadCertificateTemplate = async (
|
||||||
id_project: number,
|
id_project: number,
|
||||||
file: File
|
file: File
|
||||||
): Promise<CertificateTemplate> => {
|
): Promise<{ data: CertificateTemplate; message: string }> => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("template", file);
|
formData.append("template", file);
|
||||||
const res = await api.post(`/certificate-api/projects/${id_project}/template`, formData);
|
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 };
|
||||||
};
|
};
|
||||||
@@ -1,39 +1,5 @@
|
|||||||
import api from "../lib/api";
|
import api from "../lib/api";
|
||||||
|
import type { CreateMentorPayload, UpdateMentorPayload, MentorData } from "../types/user.types";
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const toFormData = (payload: CreateMentorPayload | UpdateMentorPayload): FormData => {
|
const toFormData = (payload: CreateMentorPayload | UpdateMentorPayload): FormData => {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
@@ -45,11 +11,11 @@ const toFormData = (payload: CreateMentorPayload | UpdateMentorPayload): FormDat
|
|||||||
return form;
|
return form;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createMentorService = async (payload: CreateMentorPayload): Promise<MentorData> => {
|
export const createMentorService = async (payload: CreateMentorPayload): Promise<{data: MentorData, message: string}> => {
|
||||||
const { data } = await api.post("/admin-api/mentors", toFormData(payload), {
|
const { data } = await api.post<{data: MentorData, message: string}>("/admin-api/mentors", toFormData(payload), {
|
||||||
headers: { "Content-Type": "multipart/form-data" },
|
headers: { "Content-Type": "multipart/form-data" },
|
||||||
});
|
});
|
||||||
return data.data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMentorsService = async (): Promise<MentorData[]> => {
|
export const getMentorsService = async (): Promise<MentorData[]> => {
|
||||||
@@ -65,13 +31,14 @@ export const getMentorDetailService = async (id: number): Promise<MentorData> =>
|
|||||||
export const updateMentorService = async (
|
export const updateMentorService = async (
|
||||||
id: number,
|
id: number,
|
||||||
payload: UpdateMentorPayload
|
payload: UpdateMentorPayload
|
||||||
): Promise<MentorData> => {
|
): Promise<{ data: MentorData; message: string }> => {
|
||||||
const { data } = await api.patch(`/admin-api/mentors/${id}`, toFormData(payload), {
|
const { data } = await api.patch<{ data: MentorData; message: string }>(`/admin-api/mentors/${id}`, toFormData(payload), {
|
||||||
headers: { "Content-Type": "multipart/form-data" },
|
headers: { "Content-Type": "multipart/form-data" },
|
||||||
});
|
});
|
||||||
return data.data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteMentorService = async (id: number): Promise<void> => {
|
export const deleteMentorService = async (id: number): Promise<{ message: string }> => {
|
||||||
await api.delete(`/admin-api/mentors/${id}`);
|
const { data } = await api.delete<{ message: string }>(`/admin-api/mentors/${id}`);
|
||||||
|
return data;
|
||||||
};
|
};
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
// services/NotificationService.ts
|
|
||||||
import api from "../lib/api";
|
import api from "../lib/api";
|
||||||
import type { BackendNotification, UINotification } from "../types/notification.types";
|
import type { BackendNotification, UINotification } from "../types/notification.types";
|
||||||
|
|
||||||
@@ -7,12 +6,16 @@ export const getNotifications = async (): Promise<BackendNotification[]> => {
|
|||||||
return res.data.data;
|
return res.data.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const markAllAsRead = async (): Promise<void> => {
|
export const markAllAsRead = async (): Promise<{ message: string }> => {
|
||||||
await api.patch("/notification-api/read-all");
|
const res = await api.patch("/notification-api/read-all");
|
||||||
|
return { message: res.data.message };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const markAsRead = async (id: number): Promise<void> => {
|
export const markAsRead = async (
|
||||||
await api.patch(`/notification-api/${id}/read`);
|
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 => {
|
export const mapType = (title: string): string => {
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ import type {
|
|||||||
// POST /project-api/add
|
// POST /project-api/add
|
||||||
export const createProject = async (
|
export const createProject = async (
|
||||||
payload: CreateProjectPayload
|
payload: CreateProjectPayload
|
||||||
): Promise<Project> => {
|
): Promise<{ data: Project, message: string }> => {
|
||||||
const res = await api.post("/project-api/add", payload);
|
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
|
// GET /project-api/get
|
||||||
@@ -72,25 +72,25 @@ export const getProjectById = async (
|
|||||||
export const updateProject = async (
|
export const updateProject = async (
|
||||||
id: string | number,
|
id: string | number,
|
||||||
payload: UpdateProjectPayload
|
payload: UpdateProjectPayload
|
||||||
): Promise<ProjectDetail> => {
|
): Promise<{data: ProjectDetail, message: string}> => {
|
||||||
const res = await api.patch(`/project-api/update/${id}`, payload);
|
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)
|
// DELETE /project-api/delete/{id} — soft delete (archive)
|
||||||
export const archiveProject = async (
|
export const archiveProject = async (
|
||||||
id: string | number
|
id: string | number
|
||||||
): Promise<ProjectDetail> => {
|
): Promise<{data: ProjectDetail, message: string}> => {
|
||||||
const res = await api.delete(`/project-api/delete/${id}`);
|
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 (
|
export const completeProject = async (
|
||||||
id: string | number
|
id: string | number
|
||||||
): Promise<ProjectDetail> => {
|
): Promise<{data: ProjectDetail, message: string}> => {
|
||||||
const res = await api.patch(`/project-api/complete/${id}`);
|
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)
|
// GET /project-api/my-projects (intern only)
|
||||||
@@ -126,9 +126,9 @@ export const getAssignableInterns = async (): Promise<AssignableIntern[]> => {
|
|||||||
// POST /project-api/assign-member (admin/mentor only)
|
// POST /project-api/assign-member (admin/mentor only)
|
||||||
export const assignMember = async (
|
export const assignMember = async (
|
||||||
payload: AssignMemberPayload
|
payload: AssignMemberPayload
|
||||||
): Promise<MemberAssignment> => {
|
): Promise<{data: MemberAssignment, message: string}> => {
|
||||||
const res = await api.post("/project-api/assign-member", payload);
|
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 (
|
export const removeMember = async (
|
||||||
@@ -136,7 +136,7 @@ export const removeMember = async (
|
|||||||
id_project: string | number;
|
id_project: string | number;
|
||||||
id_user: number;
|
id_user: number;
|
||||||
}
|
}
|
||||||
): Promise<MemberAssignment> => {
|
): Promise<{data: MemberAssignment, message: string}> => {
|
||||||
const res = await api.post("/project-api/remove-member", { id_project: payload.id_project, id_user: payload.id_user });
|
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};
|
||||||
};
|
};
|
||||||
+19
-20
@@ -10,14 +10,12 @@ import type {
|
|||||||
import type { InternTaskItem, Project } from "../types/project.types";
|
import type { InternTaskItem, Project } from "../types/project.types";
|
||||||
import type { MentorTaskItem, AdminTaskDetail } from "../types/task.types";
|
import type { MentorTaskItem, AdminTaskDetail } from "../types/task.types";
|
||||||
|
|
||||||
// ── Task ──────────────────────────────────────────────
|
|
||||||
|
|
||||||
export const createTask = async (
|
export const createTask = async (
|
||||||
projectId: string | number,
|
projectId: string | number,
|
||||||
payload: CreateTaskPayload
|
payload: CreateTaskPayload
|
||||||
): Promise<ProjectTask> => {
|
): Promise<{ data: ProjectTask; message: string }> => {
|
||||||
const res = await api.post(`/task-api/projects/${projectId}/tasks`, payload);
|
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 (
|
export const getProjectTasks = async (
|
||||||
@@ -51,20 +49,21 @@ export const updateTask = async (
|
|||||||
taskId: string | number,
|
taskId: string | number,
|
||||||
payload: UpdateTaskPayload,
|
payload: UpdateTaskPayload,
|
||||||
projectId?: string | number
|
projectId?: string | number
|
||||||
): Promise<ProjectTask> => {
|
): Promise<{ data: ProjectTask; message: string }> => {
|
||||||
const res = await api.patch(`/task-api/tasks/${taskId}`, payload, {
|
const res = await api.patch(`/task-api/tasks/${taskId}`, payload, {
|
||||||
params: projectId ? { project: projectId } : undefined,
|
params: projectId ? { project: projectId } : undefined,
|
||||||
});
|
});
|
||||||
return res.data.data;
|
return { data: res.data.data, message: res.data.message };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteTask = async (
|
export const deleteTask = async (
|
||||||
taskId: string | number,
|
taskId: string | number,
|
||||||
projectId?: string | number
|
projectId?: string | number
|
||||||
): Promise<void> => {
|
) => {
|
||||||
await api.delete(`/task-api/tasks/${taskId}`, {
|
const res = await api.delete(`/task-api/tasks/${taskId}`, {
|
||||||
params: projectId ? { project: projectId } : undefined,
|
params: projectId ? { project: projectId } : undefined,
|
||||||
});
|
});
|
||||||
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAllMentorTasks = async (): Promise<MentorTaskItem[]> => {
|
export const getAllMentorTasks = async (): Promise<MentorTaskItem[]> => {
|
||||||
@@ -87,54 +86,54 @@ export const getAllMentorTasks = async (): Promise<MentorTaskItem[]> => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ── Submission ────────────────────────────────────────
|
// Submission
|
||||||
|
|
||||||
export const submitTaskFile = async (
|
export const submitTaskFile = async (
|
||||||
taskId: string | number,
|
taskId: string | number,
|
||||||
files: File[],
|
files: File[],
|
||||||
projectId?: string | number
|
projectId?: string | number
|
||||||
): Promise<TaskSubmissionData> => {
|
): Promise<{ data: TaskSubmissionData; message: string }> => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
files.forEach((file) => formData.append("submission_files", file));
|
files.forEach((file) => formData.append("submission_files", file));
|
||||||
const res = await api.post(`/task-api/tasks/${taskId}/submissions`, formData, {
|
const res = await api.post(`/task-api/tasks/${taskId}/submissions`, formData, {
|
||||||
params: projectId ? { project: projectId } : undefined,
|
params: projectId ? { project: projectId } : undefined,
|
||||||
});
|
});
|
||||||
return res.data.data;
|
return { data: res.data.data, message: res.data.message };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const submitTaskLink = async (
|
export const submitTaskLink = async (
|
||||||
taskId: string | number,
|
taskId: string | number,
|
||||||
url_link: string,
|
url_link: string,
|
||||||
projectId?: string | number
|
projectId?: string | number
|
||||||
): Promise<TaskSubmissionData> => {
|
): Promise<{ data: TaskSubmissionData; message: string }> => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("url_link", url_link);
|
formData.append("url_link", url_link);
|
||||||
const res = await api.post(`/task-api/tasks/${taskId}/submissions`, formData, {
|
const res = await api.post(`/task-api/tasks/${taskId}/submissions`, formData, {
|
||||||
params: projectId ? { project: projectId } : undefined,
|
params: projectId ? { project: projectId } : undefined,
|
||||||
});
|
});
|
||||||
return res.data.data;
|
return { data: res.data.data, message: res.data.message };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateSubmissionFile = async (
|
export const updateSubmissionFile = async (
|
||||||
submissionId: number,
|
submissionId: number,
|
||||||
files: File[]
|
files: File[]
|
||||||
): Promise<TaskSubmissionData> => {
|
): Promise<{ data: TaskSubmissionData; message: string }> => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
files.forEach((file) => formData.append("submission_files", file));
|
files.forEach((file) => formData.append("submission_files", file));
|
||||||
const res = await api.patch(`/task-api/submissions/${submissionId}`, formData);
|
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 (
|
export const updateSubmissionLink = async (
|
||||||
submissionId: number,
|
submissionId: number,
|
||||||
url_link: string
|
url_link: string
|
||||||
): Promise<TaskSubmissionData> => {
|
): Promise<{ data: TaskSubmissionData; message: string }> => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("url_link", url_link);
|
formData.append("url_link", url_link);
|
||||||
const res = await api.patch(`/task-api/submissions/${submissionId}`, formData);
|
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<void> => {
|
export const deleteSubmission = async (submissionId: number): Promise<{ message: string }> => {
|
||||||
await api.delete(`/task-api/submissions/${submissionId}`);
|
const res = await api.delete(`/task-api/submissions/${submissionId}`);
|
||||||
|
return { message: res.data.message };
|
||||||
};
|
};
|
||||||
@@ -7,9 +7,9 @@ import type { Mahasiswa, MahasiswaResponse } from "../types/project.types";
|
|||||||
export const loginUser = async (payload: {
|
export const loginUser = async (payload: {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
}): Promise<{ token: string }> => {
|
}): Promise<{ token: string, message: string }> => {
|
||||||
const res = await api.post("/auth-api/login", payload);
|
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
|
// GET /auth-api/me
|
||||||
@@ -37,7 +37,7 @@ export function getInitials(fullName: string | undefined | null): string {
|
|||||||
// PATCH /auth-api/update-profile
|
// PATCH /auth-api/update-profile
|
||||||
export const updateProfile = async (
|
export const updateProfile = async (
|
||||||
payload: UpdateProfilePayload,
|
payload: UpdateProfilePayload,
|
||||||
): Promise<CurrentUser> => {
|
): Promise<{ data: CurrentUser, message: string }> => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
if (payload.full_name !== undefined) {
|
if (payload.full_name !== undefined) {
|
||||||
@@ -51,9 +51,11 @@ export const updateProfile = async (
|
|||||||
if (payload.profile_picture instanceof File) {
|
if (payload.profile_picture instanceof File) {
|
||||||
formData.append("profile_picture", payload.profile_picture);
|
formData.append("profile_picture", payload.profile_picture);
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await api.patch("/auth-api/update-profile", formData);
|
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<Mahasiswa[]> => {
|
export const getMahasiswa = async (): Promise<Mahasiswa[]> => {
|
||||||
|
|||||||
@@ -18,4 +18,39 @@ export interface UpdateProfilePayload {
|
|||||||
email?: string;
|
email?: string;
|
||||||
professional_bio?: string;
|
professional_bio?: string;
|
||||||
profile_picture?: File | string | null;
|
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;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user