From 60aba4180eed35a00aef8708479e978365b186ba Mon Sep 17 00:00:00 2001 From: "Kemas M. Aryadary Rasyad" <112590882+aryadary0706@users.noreply.github.com> Date: Fri, 26 Jun 2026 18:05:54 +0700 Subject: [PATCH] feat: implement mentor dashboard features including project task management, submission tracking, and modal-based task creation. --- src/Pages/internify/LoginInternify.tsx | 4 +- .../intern/components/HomeCalendar.tsx | 30 +-- .../mentor/MentorProjectDetailPage.tsx | 79 +++---- .../internify/mentor/ProjectsInternTab.tsx | 3 +- .../internify/mentor/ViewInternSubmission.tsx | 218 +++++++----------- .../mentor/components/CreateProjectDialog.tsx | 83 +++---- .../mentor/components/CreateTaskDialog.tsx | 32 ++- .../mentor/components/MentorTaskCard.tsx | 5 +- .../components/StatusSubmissionTable.tsx | 102 ++++++++ src/hooks/useAuthToken.ts | 0 src/hooks/useProjects.ts | 2 +- src/hooks/useTasks.ts | 42 +++- src/hooks/useUser.ts | 24 ++ src/index.css | 2 +- src/lib/ProjectIcons.ts | 30 +++ src/services/ProjectService.ts | 26 ++- src/services/TaskService.ts | 32 +++ src/types/project.types.ts | 10 +- src/types/task.types.ts | 22 +- 19 files changed, 471 insertions(+), 275 deletions(-) create mode 100644 src/Pages/internify/mentor/components/StatusSubmissionTable.tsx delete mode 100644 src/hooks/useAuthToken.ts create mode 100644 src/hooks/useUser.ts create mode 100644 src/lib/ProjectIcons.ts diff --git a/src/Pages/internify/LoginInternify.tsx b/src/Pages/internify/LoginInternify.tsx index f728d83..16bed3a 100644 --- a/src/Pages/internify/LoginInternify.tsx +++ b/src/Pages/internify/LoginInternify.tsx @@ -71,8 +71,8 @@ export default function InternifyLogin() { HUMIC LMS PORTAL

- -
+ {/* MAIN CONTENT */} +

Welcome back

Access your dashboard with your credentials.

diff --git a/src/Pages/internify/intern/components/HomeCalendar.tsx b/src/Pages/internify/intern/components/HomeCalendar.tsx index 7b34212..8026a4b 100644 --- a/src/Pages/internify/intern/components/HomeCalendar.tsx +++ b/src/Pages/internify/intern/components/HomeCalendar.tsx @@ -1,8 +1,9 @@ import { useState, useEffect } from 'react' import { ChevronLeft, ChevronRight } from 'lucide-react' import { useMyTasks } from '../../../../hooks/useProjects'; -//SEMENTARA USERSERVICES BELUM KUBUAT import api from '../../../../lib/api'; +import { useCurrentUser } from '../../../../hooks/useUser'; +import { useAllMentorTasks } from '../../../../hooks/useTasks'; const DAY_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'] const MONTH_NAMES = [ @@ -18,24 +19,23 @@ function getFirstDayOfMonth(year: number, month: number) { return new Date(year, month, 1).getDay() } -interface roleUser { - role: "mentor" | "intern" | "admin"; -} - export default function HomeCalendar() { const [today, setToday] = useState(new Date()); - const [role, setRole] = useState<"mentor" | "intern" | "admin" | null>(null); - const { tasks } = useMyTasks(); + const { user } = useCurrentUser(); - useEffect(() => { - api.get("/auth-api/me") - .then((res) => setRole(res.data.data.role)) - .catch((err) => console.error("Failed to fetch user:", err)); - }, []); + const isIntern = user?.role === "intern"; + const { tasks: internTasks } = useMyTasks(); + const { tasks: mentorTasks } = useAllMentorTasks(!isIntern && user !== null); - const DEADLINES = role === "intern" - ? tasks.map(task => ({ date: new Date(task.deadline_at), label: task.title, })) - : []; + const DEADLINES = isIntern + ? internTasks.map(task => ({ + date: new Date(task.deadline_at), + label: task.title, + })) + : mentorTasks.map(task => ({ + date: new Date(task.deadline_at), + label: `[${task.project_name}] ${task.title}`, + })); useEffect(() => { const interval = setInterval(() => { diff --git a/src/Pages/internify/mentor/MentorProjectDetailPage.tsx b/src/Pages/internify/mentor/MentorProjectDetailPage.tsx index bedd9b9..3762fde 100644 --- a/src/Pages/internify/mentor/MentorProjectDetailPage.tsx +++ b/src/Pages/internify/mentor/MentorProjectDetailPage.tsx @@ -1,33 +1,41 @@ import { useParams, useNavigate } from "react-router-dom"; import { useState } from "react"; -import { mockProjects } from "../../../lib/mockProjects"; -import { Calendar, Edit, SlidersHorizontal, Search } from "lucide-react"; +import { Calendar, Edit } from "lucide-react"; import EditSubmissionModal from "./components/EditSubmissionTask"; - +import { useTaskSubmissions } from "../../../hooks/useTasks"; const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-'); export default function MentorProjectsDetailPage() { const navigate = useNavigate(); const { slug, taskSlug } = useParams<{ slug: string; taskSlug: string }>(); - const targetedProject = mockProjects.find((p) => toSlug(p.name) === slug); - - if (!targetedProject) { - return
Project tidak ditemukan.
; - } - - const targetedTask = targetedProject.tasks.find((t) => toSlug(t.title) === taskSlug); - if (!targetedTask) { - return
Tugas tidak ditemukan.
; - } - + const { task, loading, error } = useTaskSubmissions(taskSlug!, slug); const [openModal, setOpenModal] = useState(false); + if (loading) return ( +
+ {[...Array(3)].map((_, i) => ( +
+ ))} +
+ ); + if (error) return

{error}

; + if (!task) return null; + + const formatDeadline = (isoString: string) => { + const date = new Date(isoString); + const dd = String(date.getDate()).padStart(2, '0'); + const mm = String(date.getMonth() + 1).padStart(2, '0'); + const yy = String(date.getFullYear()).slice(-2); + const hh = String(date.getHours()).padStart(2, '0'); + const min = String(date.getMinutes()).padStart(2, '0'); + return `${dd}/${mm}/${yy} pada jam ${hh}:${min}`; + }; return (
{/* Header Utama */}
-

{targetedTask.title}

+

{task.title}

@@ -66,13 +73,9 @@ export default function MentorProjectsDetailPage() {

Intern Submissions

- {targetedTask.submissions?.length || 0} total + {task.submissions?.length || 0} total
-
- - -
{/* Isi Tabel */} @@ -87,37 +90,37 @@ export default function MentorProjectsDetailPage() { - {targetedTask.submissions?.map((sub, idx) => { - // Cari relasi profil lengkap intern lewat email - const internProfile = targetedProject.interns.find(i => i.email === sub.internEmail); - if (!internProfile) return null; - + {task.submissions.map((sub, idx) => { return ( - {/* Kolom Nama & Profil */} -
- {internProfile.name} + {sub.profile_picture + ? + :
+ } + {sub.full_name} {/* Kolom Tanggal Kumpul */} - {sub.submittedAt || "-"} + {sub.submitted_at + ? new Date(sub.submitted_at).toLocaleDateString("id-ID") + : "-"} {/* Kolom Badge Status */} - {sub.status === "Done" && ( + {sub.display_status === "done" && ( Done )} - {sub.status === "Pending" && ( + {sub.display_status === "pending" && ( Pending )} - {sub.status === "Overdue" && ( + {sub.display_status === "overdue" && ( Overdue @@ -130,8 +133,8 @@ export default function MentorProjectsDetailPage() { className="text-sm font-bold text-red-800 hover:text-red-900 transition-colors inline-flex items-center gap-1" onClick={() => navigate( - `/mentor/projects/${toSlug(targetedProject.name)}/${toSlug(targetedTask.title)}/${sub.internEmail.split("@")[0]}`, - { state: { status: sub.status === "Done" ? "submitted" : sub.status === "Overdue" ? "overdue" : "pending" } } + `/mentor/projects/${slug}/${taskSlug}/${toSlug(sub.full_name)}`, + { state: { status: sub.display_status === "done" ? "submitted" : sub.display_status === "overdue" ? "overdue" : "pending" } } ) } > @@ -145,8 +148,6 @@ export default function MentorProjectsDetailPage() {
- - {/* EditSubmissionModal sekarang berada di dalam induk div utama */} {openModal && (
- {member.full_name} + {member.email}
diff --git a/src/Pages/internify/mentor/ViewInternSubmission.tsx b/src/Pages/internify/mentor/ViewInternSubmission.tsx index 587dd1c..a68b5d3 100644 --- a/src/Pages/internify/mentor/ViewInternSubmission.tsx +++ b/src/Pages/internify/mentor/ViewInternSubmission.tsx @@ -1,144 +1,90 @@ import { useParams, useLocation } from "react-router-dom"; -import { FileText, Download, Clock, CheckCircle2, AlertCircle, MinusCircle } from "lucide-react"; -import { mockProjects } from "../../../lib/mockProjects"; - -const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, "-"); - -type SubmissionStatus = "submitted" | "pending" | "overdue"; - -// ─── status badge ─────────────────────────────────────────────────────────── - -function StatusBadge({ status }: { status: SubmissionStatus }) { - if (status === "submitted") - return ( - - - Submitted for grading - - ); - if (status === "overdue") - return ( - - - Overdue - - ); - return ( - - - Not submitted - - ); -} - -// ─── status table ──────────────────────────────────────────────────────────── - -function StatusTable({ status }: { status: SubmissionStatus }) { - const rows: { label: string; value: React.ReactNode }[] = [ - { - label: "Submission status", - value: , - }, - { - label: "Grading status", - value: -, - }, - { - label: "Time Remaining", - value: - status === "overdue" ? ( - Overdue - ) : ( - - - ), - }, - { - label: "File submission", - value: - status === "submitted" ? ( -
- - - -
- ) : ( - - - ), - }, - ]; - - return ( -
-
- {rows.map((row) => ( -
-
- {row.label} -
-
{row.value}
-
- ))} -
-
- ); -} - -// ─── main page ─────────────────────────────────────────────────────────────── +import { Download, Clock, AlertCircle } from "lucide-react"; +import { useTaskSubmissions } from "../../../hooks/useTasks"; +import type { DisplayStatus } from "../../../types/task.types"; +import { StatusTable } from "./components/StatusSubmissionTable"; export default function ViewInternSubmission() { - const { slug, taskSlug, internName } = useParams<{ slug: string; taskSlug: string; internName: string }>(); - const location = useLocation(); - const status: SubmissionStatus = location.state?.status ?? "pending"; + const { slug, taskSlug, nameIntern } = useParams<{ + slug: string; + taskSlug: string; + nameIntern: string; + }>(); + const location = useLocation(); + const status: DisplayStatus = location.state?.status ?? "pending"; - const project = mockProjects.find((p) => toSlug(p.name) === slug); - const task = project?.tasks.find((t) => toSlug(t.title) === taskSlug); - - if (!task) { - return ( -
- -

Task not found.

-
- ); - } + const { task, loading, error } = useTaskSubmissions(taskSlug!, slug); + if (loading) return ( -
- {/* Header */} -
-
-
-

- {task.title} -

-

- - Due {task.deadline.label} at {task.deadline.time} -

-
-
-
- - {/* Task description */} -
-

Task Description

-

- {task.description} -

-
- - {/* Status table */} -
-
-

Submission Detail

- {status === "submitted" && ( - - )} -
- -
-
+
+ {[...Array(3)].map((_, i) => ( +
+ ))} +
); + + if (error) return

{error}

; + if (!task) return null; + + const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-'); + + const internSubmission = task.submissions.find( + (s) => toSlug(s.full_name) === nameIntern + ); + + if (!internSubmission) + return ( +
+ +

Submission not found.

+
+ ); + + return ( +
+ {/* Header */} +
+
+

+ {task.title} +

+

+ + {internSubmission.full_name} — {internSubmission.email} +

+
+
+ + {/* Task Description */} +
+

Task Description

+

+ {task.description} +

+
+ + {/* Submission Detail */} +
+
+

Submission Detail

+ {internSubmission.submission?.file_path && ( + + + Download + + )} +
+ +
+
+ ); } \ No newline at end of file diff --git a/src/Pages/internify/mentor/components/CreateProjectDialog.tsx b/src/Pages/internify/mentor/components/CreateProjectDialog.tsx index 7287a0c..91d565a 100644 --- a/src/Pages/internify/mentor/components/CreateProjectDialog.tsx +++ b/src/Pages/internify/mentor/components/CreateProjectDialog.tsx @@ -1,19 +1,8 @@ import { useRef, useEffect, useState } from "react"; -import { X, Calendar } from "lucide-react"; -import { - LuCode, - LuAlbum, - LuCloud, - LuSmartphone, - LuSettings, - LuUsers, - LuClipboardList, - LuPenTool, - LuLightbulb, - LuShield, - LuMail, - LuSendHorizontal -} from "react-icons/lu"; +import { X } from "lucide-react"; +import { PROJECT_ICON_MAP, PROJECT_ICON_CODES } from "../../..//../lib/ProjectIcons"; +import { LuSendHorizontal, LuMail } from "react-icons/lu"; +import { useCreateProject } from "../../../../hooks/useProjects"; interface CreateProjectModalProps { isOpen: boolean; @@ -24,7 +13,7 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod const dialogRef = useRef(null); // State Form Management - const [selectedIcon, setSelectedIcon] = useState("code"); + const [selectedIcon, setSelectedIcon] = useState(PROJECT_ICON_CODES[0]); const [projectName, setProjectName] = useState(""); const [projectDescription, setProjectDescription] = useState(""); const [emailInput, setEmailInput] = useState(""); @@ -34,20 +23,12 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod ]); const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); + const { create, loading, error } = useCreateProject(); - // Daftar Icon Pilihan di baris atas - const projectIcons = [ - { id: "code", icon: LuCode }, - { id: "chart", icon: LuAlbum }, - { id: "cloud", icon: LuCloud }, - { id: "mobile", icon: LuSmartphone }, - { id: "settings", icon: LuSettings }, - { id: "users", icon: LuUsers }, - { id: "task", icon: LuClipboardList }, - { id: "design", icon: LuPenTool }, - { id: "idea", icon: LuLightbulb }, - { id: "secure", icon: LuShield }, - ]; + const projectIcons = PROJECT_ICON_CODES.map((id) => ({ + id, + icon: PROJECT_ICON_MAP[id], + })); useEffect(() => { const dialog = dialogRef.current; @@ -88,19 +69,19 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod }; // Handler Submit Utama - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - const payload = { - icon: selectedIcon, - name: projectName, + const result = await create({ + project_icon: selectedIcon, + project_name: projectName, description: projectDescription, - emails: invitedEmails, - startDate, - endDate - }; - console.log("Launching Project:", payload); - alert("Project Launched Successfully!"); - onClose(); + member_emails: invitedEmails, + start_date: startDate, + end_date: endDate, + }); + if (result) { + onClose(); + } }; return ( @@ -125,8 +106,7 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
{/* Body Form Modal */} -
- + {/* Section: Select Project Icon */}