feat: implement mentor dashboard features including project task management, submission tracking, and modal-based task creation.

This commit is contained in:
Kemas M. Aryadary Rasyad
2026-06-26 18:05:54 +07:00
parent 53b270921d
commit 60aba4180e
19 changed files with 471 additions and 275 deletions
+2 -2
View File
@@ -71,8 +71,8 @@ export default function InternifyLogin() {
HUMIC LMS PORTAL HUMIC LMS PORTAL
</p> </p>
</div> </div>
{/* MAIN CONTENT */}
<div className="max-w-md bg-white rounded-2xl border border-box-border p-8 md:p-12"> <div className="max-w-md bg-box-primary rounded-2xl border border-box-border p-8 md:p-12 shadow-sm">
<h2 className="text-xl font-bold text-gray-900 mb-1">Welcome back</h2> <h2 className="text-xl font-bold text-gray-900 mb-1">Welcome back</h2>
<p className="text-sm text-gray-500 mb-6">Access your dashboard with your credentials.</p> <p className="text-sm text-gray-500 mb-6">Access your dashboard with your credentials.</p>
@@ -1,8 +1,9 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { ChevronLeft, ChevronRight } from 'lucide-react' import { ChevronLeft, ChevronRight } from 'lucide-react'
import { useMyTasks } from '../../../../hooks/useProjects'; import { useMyTasks } from '../../../../hooks/useProjects';
//SEMENTARA USERSERVICES BELUM KUBUAT
import api from '../../../../lib/api'; 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 DAY_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
const MONTH_NAMES = [ const MONTH_NAMES = [
@@ -18,24 +19,23 @@ function getFirstDayOfMonth(year: number, month: number) {
return new Date(year, month, 1).getDay() return new Date(year, month, 1).getDay()
} }
interface roleUser {
role: "mentor" | "intern" | "admin";
}
export default function HomeCalendar() { export default function HomeCalendar() {
const [today, setToday] = useState(new Date()); const [today, setToday] = useState(new Date());
const [role, setRole] = useState<"mentor" | "intern" | "admin" | null>(null); const { user } = useCurrentUser();
const { tasks } = useMyTasks();
useEffect(() => { const isIntern = user?.role === "intern";
api.get("/auth-api/me") const { tasks: internTasks } = useMyTasks();
.then((res) => setRole(res.data.data.role)) const { tasks: mentorTasks } = useAllMentorTasks(!isIntern && user !== null);
.catch((err) => console.error("Failed to fetch user:", err));
}, []);
const DEADLINES = role === "intern" const DEADLINES = isIntern
? tasks.map(task => ({ date: new Date(task.deadline_at), label: task.title, })) ? 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(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
@@ -1,33 +1,41 @@
import { useParams, useNavigate } from "react-router-dom"; import { useParams, useNavigate } from "react-router-dom";
import { useState } from "react"; import { useState } from "react";
import { mockProjects } from "../../../lib/mockProjects"; import { Calendar, Edit } from "lucide-react";
import { Calendar, Edit, SlidersHorizontal, Search } from "lucide-react";
import EditSubmissionModal from "./components/EditSubmissionTask"; import EditSubmissionModal from "./components/EditSubmissionTask";
import { useTaskSubmissions } from "../../../hooks/useTasks";
const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-'); const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-');
export default function MentorProjectsDetailPage() { export default function MentorProjectsDetailPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const { slug, taskSlug } = useParams<{ slug: string; taskSlug: string }>(); const { slug, taskSlug } = useParams<{ slug: string; taskSlug: string }>();
const targetedProject = mockProjects.find((p) => toSlug(p.name) === slug); const { task, loading, error } = useTaskSubmissions(taskSlug!, slug);
if (!targetedProject) {
return <div className="p-10 text-[#888]">Project tidak ditemukan.</div>;
}
const targetedTask = targetedProject.tasks.find((t) => toSlug(t.title) === taskSlug);
if (!targetedTask) {
return <div className="p-10 text-[#888]">Tugas tidak ditemukan.</div>;
}
const [openModal, setOpenModal] = useState(false); const [openModal, setOpenModal] = useState(false);
if (loading) return (
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<div key={i} className="h-32 bg-box-secondary rounded-2xl animate-pulse" />
))}
</div>
);
if (error) return <p className="text-red-500 text-sm">{error}</p>;
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 ( return (
<div> <div>
{/* Header Utama */} {/* Header Utama */}
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-[#1a1a1a]">{targetedTask.title}</h1> <h1 className="text-3xl font-bold text-[#1a1a1a]">{task.title}</h1>
<button <button
className="flex items-center gap-2 border border-gray-400 text-gray-700 px-4 py-2 rounded-lg text-sm font-medium hover:bg-gray-50 transition-all" className="flex items-center gap-2 border border-gray-400 text-gray-700 px-4 py-2 rounded-lg text-sm font-medium hover:bg-gray-50 transition-all"
onClick={() => setOpenModal(true)} onClick={() => setOpenModal(true)}
@@ -42,7 +50,7 @@ export default function MentorProjectsDetailPage() {
<div className="flex-1 md:border-r border-gray-200 md:pr-6"> <div className="flex-1 md:border-r border-gray-200 md:pr-6">
<h2 className="text-xs font-sans text-gray-400 tracking-wider uppercase mb-2">Description</h2> <h2 className="text-xs font-sans text-gray-400 tracking-wider uppercase mb-2">Description</h2>
<p className="text-sm text-gray-700 leading-relaxed whitespace-pre-line"> <p className="text-sm text-gray-700 leading-relaxed whitespace-pre-line">
{targetedTask.description} {task.description}
</p> </p>
</div> </div>
<div className="w-full md:w-120 flex flex-col justify-start"> <div className="w-full md:w-120 flex flex-col justify-start">
@@ -52,8 +60,7 @@ export default function MentorProjectsDetailPage() {
<Calendar size={20} /> <Calendar size={20} />
</div> </div>
<div> <div>
<div className="text-base font-bold text-gray-900">{targetedTask.deadline.label}</div> <div className="text-base font-bold text-gray-900">{formatDeadline(task.deadline_at)}</div>
<div className="text-xs text-gray-400">at {targetedTask.deadline.time}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -66,13 +73,9 @@ export default function MentorProjectsDetailPage() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<h2 className="text-xl font-bold text-gray-900">Intern Submissions</h2> <h2 className="text-xl font-bold text-gray-900">Intern Submissions</h2>
<span className="bg-gray-100 text-gray-500 font-semibold text-xs px-2.5 py-0.5 rounded-md border border-gray-200"> <span className="bg-gray-100 text-gray-500 font-semibold text-xs px-2.5 py-0.5 rounded-md border border-gray-200">
{targetedTask.submissions?.length || 0} total {task.submissions?.length || 0} total
</span> </span>
</div> </div>
<div className="flex items-center gap-4 text-gray-500">
<button className="hover:text-gray-800"><SlidersHorizontal size={18} /></button>
<button className="hover:text-gray-800"><Search size={18} /></button>
</div>
</div> </div>
{/* Isi Tabel */} {/* Isi Tabel */}
@@ -87,37 +90,37 @@ export default function MentorProjectsDetailPage() {
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200"> <tbody className="divide-y divide-gray-200">
{targetedTask.submissions?.map((sub, idx) => { {task.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;
return ( return (
<tr key={idx} className="hover:bg-gray-50/50 transition-colors"> <tr key={idx} className="hover:bg-gray-50/50 transition-colors">
{/* Kolom Nama & Profil */}
<td className="px-6 py-4 flex items-center gap-3"> <td className="px-6 py-4 flex items-center gap-3">
<div className="w-9 h-9 rounded-full bg-red-800 text-gray-500 font-semibold text-xs flex items-center justify-center border border-gray-300" /> {sub.profile_picture
<span className="font-semibold text-sm text-gray-800">{internProfile.name}</span> ? <img src={sub.profile_picture} className="w-9 h-9 rounded-full object-cover" />
: <div className="w-9 h-9 rounded-full bg-red-800 flex-shrink-0" />
}
<span className="font-semibold text-sm text-gray-800">{sub.full_name}</span>
</td> </td>
{/* Kolom Tanggal Kumpul */} {/* Kolom Tanggal Kumpul */}
<td className="px-6 py-4 text-sm text-gray-500"> <td className="px-6 py-4 text-sm text-gray-500">
{sub.submittedAt || "-"} {sub.submitted_at
? new Date(sub.submitted_at).toLocaleDateString("id-ID")
: "-"}
</td> </td>
{/* Kolom Badge Status */} {/* Kolom Badge Status */}
<td className="px-6 py-4"> <td className="px-6 py-4">
{sub.status === "Done" && ( {sub.display_status === "done" && (
<span className="inline-flex items-center gap-1.5 px-3 py-1 text-xs font-semibold rounded-full bg-green-50 text-green-700 border border-green-200"> <span className="inline-flex items-center gap-1.5 px-3 py-1 text-xs font-semibold rounded-full bg-green-50 text-green-700 border border-green-200">
<span className="w-1.5 h-1.5 bg-green-500 rounded-full"></span> Done <span className="w-1.5 h-1.5 bg-green-500 rounded-full"></span> Done
</span> </span>
)} )}
{sub.status === "Pending" && ( {sub.display_status === "pending" && (
<span className="inline-flex items-center gap-1.5 px-3 py-1 text-xs font-semibold rounded-full bg-amber-50 text-amber-700 border border-amber-200"> <span className="inline-flex items-center gap-1.5 px-3 py-1 text-xs font-semibold rounded-full bg-amber-50 text-amber-700 border border-amber-200">
<span className="w-1.5 h-1.5 bg-amber-500 rounded-full"></span> Pending <span className="w-1.5 h-1.5 bg-amber-500 rounded-full"></span> Pending
</span> </span>
)} )}
{sub.status === "Overdue" && ( {sub.display_status === "overdue" && (
<span className="inline-flex items-center gap-1.5 px-3 py-1 text-xs font-semibold rounded-full bg-red-700 text-white border"> <span className="inline-flex items-center gap-1.5 px-3 py-1 text-xs font-semibold rounded-full bg-red-700 text-white border">
<span className="w-1.5 h-1.5 bg-white rounded-full"></span> Overdue <span className="w-1.5 h-1.5 bg-white rounded-full"></span> Overdue
</span> </span>
@@ -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" className="text-sm font-bold text-red-800 hover:text-red-900 transition-colors inline-flex items-center gap-1"
onClick={() => onClick={() =>
navigate( navigate(
`/mentor/projects/${toSlug(targetedProject.name)}/${toSlug(targetedTask.title)}/${sub.internEmail.split("@")[0]}`, `/mentor/projects/${slug}/${taskSlug}/${toSlug(sub.full_name)}`,
{ state: { status: sub.status === "Done" ? "submitted" : sub.status === "Overdue" ? "overdue" : "pending" } } { state: { status: sub.display_status === "done" ? "submitted" : sub.display_status === "overdue" ? "overdue" : "pending" } }
) )
} }
> >
@@ -145,8 +148,6 @@ export default function MentorProjectsDetailPage() {
</table> </table>
</div> </div>
</div> </div>
{/* EditSubmissionModal sekarang berada di dalam induk div utama */}
{openModal && ( {openModal && (
<EditSubmissionModal <EditSubmissionModal
isOpen={openModal} isOpen={openModal}
@@ -2,6 +2,7 @@ import { useState } from 'react';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import ManageInternsModal from './components/AddInternsDialog'; import ManageInternsModal from './components/AddInternsDialog';
import type { ProjectDetail, ProjectMember } from '../../../types/project.types'; import type { ProjectDetail, ProjectMember } from '../../../types/project.types';
import { useProjectInterns } from '../../../hooks/useProjects';
export default function InternsTab({ project }: { project: ProjectDetail }) { export default function InternsTab({ project }: { project: ProjectDetail }) {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
@@ -54,7 +55,7 @@ export default function InternsTab({ project }: { project: ProjectDetail }) {
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className=" text-sm text-[#333]"> <span className=" text-sm text-[#333]">
{member.full_name} {member.email}
</span> </span>
</div> </div>
<span className=" text-[13px] text-[#555] font-semibold"> <span className=" text-[13px] text-[#555] font-semibold">
@@ -1,144 +1,90 @@
import { useParams, useLocation } from "react-router-dom"; import { useParams, useLocation } from "react-router-dom";
import { FileText, Download, Clock, CheckCircle2, AlertCircle, MinusCircle } from "lucide-react"; import { Download, Clock, AlertCircle } from "lucide-react";
import { mockProjects } from "../../../lib/mockProjects"; import { useTaskSubmissions } from "../../../hooks/useTasks";
import type { DisplayStatus } from "../../../types/task.types";
const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, "-"); import { StatusTable } from "./components/StatusSubmissionTable";
type SubmissionStatus = "submitted" | "pending" | "overdue";
// ─── status badge ───────────────────────────────────────────────────────────
function StatusBadge({ status }: { status: SubmissionStatus }) {
if (status === "submitted")
return (
<span className="inline-flex items-center gap-1.5 text-xs font-semibold text-green-700 bg-green-50 border border-green-200 px-2.5 py-1 rounded-full">
<CheckCircle2 className="w-3.5 h-3.5" />
Submitted for grading
</span>
);
if (status === "overdue")
return (
<span className="inline-flex items-center gap-1.5 text-xs font-semibold text-red-700 bg-red-50 border border-red-200 px-2.5 py-1 rounded-full">
<AlertCircle className="w-3.5 h-3.5" />
Overdue
</span>
);
return (
<span className="inline-flex items-center gap-1.5 text-xs font-semibold text-gray-500 bg-gray-100 border border-gray-200 px-2.5 py-1 rounded-full">
<MinusCircle className="w-3.5 h-3.5" />
Not submitted
</span>
);
}
// ─── status table ────────────────────────────────────────────────────────────
function StatusTable({ status }: { status: SubmissionStatus }) {
const rows: { label: string; value: React.ReactNode }[] = [
{
label: "Submission status",
value: <StatusBadge status={status} />,
},
{
label: "Grading status",
value: <span className="text-sm text-gray-400">-</span>,
},
{
label: "Time Remaining",
value:
status === "overdue" ? (
<span className="text-sm text-red-600 font-medium">Overdue</span>
) : (
<span className="text-sm text-gray-400">-</span>
),
},
{
label: "File submission",
value:
status === "submitted" ? (
<div className="flex items-center gap-2 text-sm text-gray-800">
<FileText className="w-4 h-4 text-red-700 stroke-[2.5]" />
<span className="text-gray-400 italic">-</span>
</div>
) : (
<span className="text-sm text-gray-400">-</span>
),
},
];
return (
<div className="bg-box-secondary border border-box-border rounded-2xl overflow-hidden">
<div className="divide-y divide-box-border">
{rows.map((row) => (
<div key={row.label} className="grid grid-cols-[180px_1fr]">
<div className="px-4 py-3 text-sm text-gray-500 border-r border-box-border bg-gray-50/50">
{row.label}
</div>
<div className="px-4 py-3">{row.value}</div>
</div>
))}
</div>
</div>
);
}
// ─── main page ───────────────────────────────────────────────────────────────
export default function ViewInternSubmission() { export default function ViewInternSubmission() {
const { slug, taskSlug, internName } = useParams<{ slug: string; taskSlug: string; internName: string }>(); const { slug, taskSlug, nameIntern } = useParams<{
const location = useLocation(); slug: string;
const status: SubmissionStatus = location.state?.status ?? "pending"; 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, loading, error } = useTaskSubmissions(taskSlug!, slug);
const task = project?.tasks.find((t) => toSlug(t.title) === taskSlug);
if (!task) {
return (
<div className="flex flex-col items-center justify-center h-64 gap-3 text-gray-400">
<AlertCircle className="w-10 h-10" />
<p className="text-sm font-medium">Task not found.</p>
</div>
);
}
if (loading)
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{/* Header */} {[...Array(3)].map((_, i) => (
<div className="w-full bg-box-secondary rounded-2xl border border-box-border p-6"> <div key={i} className="h-32 bg-box-secondary rounded-2xl animate-pulse" />
<div className="flex items-start justify-between"> ))}
<div className="space-y-1"> </div>
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">
{task.title}
</h1>
<p className="text-sm text-gray-500 flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5" />
Due {task.deadline.label} at {task.deadline.time}
</p>
</div>
</div>
</div>
{/* Task description */}
<div className="w-full bg-box-secondary rounded-2xl border border-box-border p-6 space-y-3">
<h2 className="text-base font-bold text-gray-800">Task Description</h2>
<p className="text-sm text-foreground font-medium leading-relaxed">
{task.description}
</p>
</div>
{/* Status table */}
<div className="w-full bg-box-secondary rounded-2xl border border-box-border p-6 space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-base font-bold text-gray-800">Submission Detail</h2>
{status === "submitted" && (
<button className="flex items-center gap-2 bg-primary text-white text-xs font-bold px-4 py-2 rounded-xl hover:bg-red-800 transition-colors">
<Download className="w-4 h-4" />
Download
</button>
)}
</div>
<StatusTable status={status} />
</div>
</div>
); );
if (error) return <p className="text-red-500 text-sm">{error}</p>;
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 (
<div className="flex flex-col items-center justify-center h-64 gap-3 text-gray-400">
<AlertCircle className="w-10 h-10" />
<p className="text-sm font-medium">Submission not found.</p>
</div>
);
return (
<div className="space-y-4">
{/* Header */}
<div className="w-full bg-box-secondary rounded-2xl border border-box-border p-6">
<div className="space-y-1">
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">
{task.title}
</h1>
<p className="text-sm text-gray-500 flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5" />
{internSubmission.full_name} — {internSubmission.email}
</p>
</div>
</div>
{/* Task Description */}
<div className="w-full bg-box-secondary rounded-2xl border border-box-border p-6 space-y-3">
<h2 className="text-base font-bold text-gray-800">Task Description</h2>
<p className="text-sm text-foreground font-medium leading-relaxed">
{task.description}
</p>
</div>
{/* Submission Detail */}
<div className="w-full bg-box-secondary rounded-2xl border border-box-border p-6 space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-base font-bold text-gray-800">Submission Detail</h2>
{internSubmission.submission?.file_path && (
<a
href={internSubmission.submission.file_path}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 bg-primary text-white text-xs font-bold px-4 py-2 rounded-xl hover:bg-red-800 transition-colors"
>
<Download className="w-4 h-4" />
Download
</a>
)}
</div>
<StatusTable
status={status}
submission={internSubmission.submission}
/>
</div>
</div>
);
} }
@@ -1,19 +1,8 @@
import { useRef, useEffect, useState } from "react"; import { useRef, useEffect, useState } from "react";
import { X, Calendar } from "lucide-react"; import { X } from "lucide-react";
import { import { PROJECT_ICON_MAP, PROJECT_ICON_CODES } from "../../..//../lib/ProjectIcons";
LuCode, import { LuSendHorizontal, LuMail } from "react-icons/lu";
LuAlbum, import { useCreateProject } from "../../../../hooks/useProjects";
LuCloud,
LuSmartphone,
LuSettings,
LuUsers,
LuClipboardList,
LuPenTool,
LuLightbulb,
LuShield,
LuMail,
LuSendHorizontal
} from "react-icons/lu";
interface CreateProjectModalProps { interface CreateProjectModalProps {
isOpen: boolean; isOpen: boolean;
@@ -24,7 +13,7 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
const dialogRef = useRef<HTMLDialogElement>(null); const dialogRef = useRef<HTMLDialogElement>(null);
// State Form Management // State Form Management
const [selectedIcon, setSelectedIcon] = useState("code"); const [selectedIcon, setSelectedIcon] = useState(PROJECT_ICON_CODES[0]);
const [projectName, setProjectName] = useState(""); const [projectName, setProjectName] = useState("");
const [projectDescription, setProjectDescription] = useState(""); const [projectDescription, setProjectDescription] = useState("");
const [emailInput, setEmailInput] = useState(""); const [emailInput, setEmailInput] = useState("");
@@ -34,20 +23,12 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
]); ]);
const [startDate, setStartDate] = useState(""); const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState(""); const [endDate, setEndDate] = useState("");
const { create, loading, error } = useCreateProject();
// Daftar Icon Pilihan di baris atas const projectIcons = PROJECT_ICON_CODES.map((id) => ({
const projectIcons = [ id,
{ id: "code", icon: LuCode }, icon: PROJECT_ICON_MAP[id],
{ 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 },
];
useEffect(() => { useEffect(() => {
const dialog = dialogRef.current; const dialog = dialogRef.current;
@@ -88,19 +69,19 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
}; };
// Handler Submit Utama // Handler Submit Utama
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
const payload = { const result = await create({
icon: selectedIcon, project_icon: selectedIcon,
name: projectName, project_name: projectName,
description: projectDescription, description: projectDescription,
emails: invitedEmails, member_emails: invitedEmails,
startDate, start_date: startDate,
endDate end_date: endDate,
}; });
console.log("Launching Project:", payload); if (result) {
alert("Project Launched Successfully!"); onClose();
onClose(); }
}; };
return ( return (
@@ -125,8 +106,7 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
</div> </div>
{/* Body Form Modal */} {/* Body Form Modal */}
<form onSubmit={handleSubmit} className="p-6 md:p-8 space-y-3 md:space-y-4 overflow-y-auto flex-1"> <form onSubmit={handleSubmit} className="p-4 md:p-6 space-y-3 md:space-y-4 overflow-y-auto flex-1">
{/* Section: Select Project Icon */} {/* Section: Select Project Icon */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-bold text-gray-700 tracking-wide"> <label className="text-xs font-bold text-gray-700 tracking-wide">
@@ -141,9 +121,9 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
key={item.id} key={item.id}
type="button" type="button"
onClick={() => setSelectedIcon(item.id)} onClick={() => setSelectedIcon(item.id)}
className={`w-8 h-8 flex items-center justify-center rounded-lg border text-lg transition-all ${isSelected className={`w-10 h-10 flex items-center justify-center rounded-lg border text-lg transition-all ${isSelected
? "border-[#B30000] bg-red-50/50 text-[#B30000] ring-2 ring-[#B30000]/20 font-bold" ? "border-red bg-red-foreground text-red ring-2 ring-red/20 font-bold"
: "border-gray-200 bg-gray-50 text-gray-500 hover:bg-gray-100" : "border-card-outline bg-card text-font hover:bg-gray-100"
}`} }`}
> >
<IconComponent className={isSelected ? "stroke-[2.5]" : "stroke-[2]"} /> <IconComponent className={isSelected ? "stroke-[2.5]" : "stroke-[2]"} />
@@ -174,7 +154,7 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
Project Description Project Description
</label> </label>
<textarea <textarea
rows={4} rows={3}
placeholder="Outline the project goals, deliverables, and technical requirements..." placeholder="Outline the project goals, deliverables, and technical requirements..."
value={projectDescription} value={projectDescription}
onChange={(e) => setProjectDescription(e.target.value)} onChange={(e) => setProjectDescription(e.target.value)}
@@ -249,7 +229,6 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
className="w-full pl-4 pr-10 py-2.5 bg-white border border-gray-200 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 pl-4 pr-10 py-2.5 bg-white border border-gray-200 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">
<Calendar className="w-4 h-4" />
</span> </span>
</div> </div>
</div> </div>
@@ -268,12 +247,15 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
className="w-full pl-4 pr-10 py-2.5 bg-white border border-gray-200 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 pl-4 pr-10 py-2.5 bg-white border border-gray-200 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">
<Calendar className="w-4 h-4" />
</span> </span>
</div> </div>
</div> </div>
</div> </div>
{error && (
<p className="text-xs text-red-600 text-center font-medium">{error}</p>
)}
{/* Action Row Buttons */} {/* Action Row Buttons */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 pt-6"> <div className="flex flex-col sm:flex-row items-center justify-center gap-3 pt-6">
<button <button
@@ -286,9 +268,10 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
<button <button
type="submit" type="submit"
className="w-full sm:w-[240px] py-2.5 bg-[#B30000] hover:bg-[#990000] text-white text-sm font-bold rounded-xl transition-colors shadow-md flex items-center justify-center gap-2 tracking-wide" disabled={loading}
className="w-full sm:w-[240px] py-2.5 bg-[#B30000] hover:bg-[#990000] disabled:opacity-60 text-white text-sm font-bold rounded-xl transition-colors shadow-md flex items-center justify-center gap-2 tracking-wide"
> >
Launch Project {loading ? "Launching..." : "Launch Project"}
<LuSendHorizontal className="w-4 h-4 stroke-[2.5]" /> <LuSendHorizontal className="w-4 h-4 stroke-[2.5]" />
</button> </button>
</div> </div>
@@ -41,8 +41,6 @@ export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess
}); });
if (result) onSuccess(); if (result) onSuccess();
}; };
// JSX sama persis, hanya ganti handleSubmit dan tambah disabled pada button submit:
return ( return (
<dialog ref={dialogRef} className="custom-dialog p-[1.5rem]"> <dialog ref={dialogRef} className="custom-dialog p-[1.5rem]">
<div className="dialog-header"> <div className="dialog-header">
@@ -54,28 +52,43 @@ export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess
<X className="w-5 h-5 stroke-[2.5]" /> <X className="w-5 h-5 stroke-[2.5]" />
</button> </button>
</div> </div>
<form onSubmit={handleSubmit} className="px-6 space-y-4 overflow-y-auto flex-1"> <form onSubmit={handleSubmit} className="px-6 space-y-2 overflow-y-auto flex-1">
<div> <div>
<label className="text-xs font-bold text-gray-700 tracking-wide">Task Title</label> <label className="text-xs font-bold text-gray-700 tracking-wide">Task Title</label>
<input type="text" required placeholder="e.g., Design High-Fidelity Prototypes" value={taskTitle} <input
type="text"
required
placeholder="e.g., Design High-Fidelity Prototypes"
value={taskTitle}
onChange={(e) => setTaskTitle(e.target.value)} onChange={(e) => setTaskTitle(e.target.value)}
className="w-full px-3 py-4 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]" /> className="w-full px-3 py-4 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]" />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-xs font-bold text-gray-700 tracking-wide">Description</label> <label className="text-xs font-bold text-gray-700 tracking-wide">Description</label>
<textarea rows={4} placeholder="Outline the requirements and objectives..." value={description} <textarea
rows={4}
placeholder="Outline the requirements and objectives..."
value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
className="w-full px-3 py-2 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000] resize-none leading-relaxed" /> className="w-full px-3 py-2 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000] resize-none leading-relaxed" />
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-xs font-bold text-gray-700 tracking-wide">Deadline Date</label> <label className="text-xs font-bold text-gray-700 tracking-wide">Deadline Date</label>
<input type="date" required value={deadlineDate} onChange={(e) => setDeadlineDate(e.target.value)} <input
type="date"
required
value={deadlineDate}
onChange={(e) => setDeadlineDate(e.target.value)}
className="w-full pl-3 pr-9 py-3 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]" /> className="w-full pl-3 pr-9 py-3 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]" />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-xs font-bold text-gray-700 tracking-wide">Specific Time</label> <label className="text-xs font-bold text-gray-700 tracking-wide">Specific Time</label>
<input type="time" required value={specificTime} onChange={(e) => setSpecificTime(e.target.value)} <input
type="time"
required
value={specificTime}
onChange={(e) => setSpecificTime(e.target.value)}
className="w-full pl-3 pr-9 py-3 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]" /> className="w-full pl-3 pr-9 py-3 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]" />
</div> </div>
</div> </div>
@@ -84,9 +97,8 @@ export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
{(["file_upload", "url_link"] as SubmissionType[]).map((type) => ( {(["file_upload", "url_link"] as SubmissionType[]).map((type) => (
<div key={type} onClick={() => setSubmissionType(type)} <div key={type} onClick={() => setSubmissionType(type)}
className={`flex items-center gap-3 px-4 py-3 rounded-xl border cursor-pointer transition-all ${ className={`flex items-center gap-3 px-4 py-3 rounded-xl border cursor-pointer transition-all ${submissionType === type ? "border-[#B30000] bg-[#B30000] text-white font-semibold" : "border-gray-200 bg-white text-gray-600 hover:bg-gray-50"
submissionType === type ? "border-[#B30000] bg-[#B30000] text-white font-semibold" : "border-gray-200 bg-white text-gray-600 hover:bg-gray-50" }`}>
}`}>
<div className={`w-3.5 h-3.5 rounded-full border flex items-center justify-center ${submissionType === type ? "border-white bg-white" : "border-gray-300"}`}> <div className={`w-3.5 h-3.5 rounded-full border flex items-center justify-center ${submissionType === type ? "border-white bg-white" : "border-gray-300"}`}>
{submissionType === type && <div className="w-1.5 h-1.5 bg-[#B30000] rounded-full" />} {submissionType === type && <div className="w-1.5 h-1.5 bg-[#B30000] rounded-full" />}
</div> </div>
@@ -1,4 +1,3 @@
import type { Task } from "../../../../lib/mockProjects";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import type { ProjectTask } from "../../../../types/task.types"; import type { ProjectTask } from "../../../../types/task.types";
@@ -8,14 +7,12 @@ const formatDate = (date: string) =>
const formatTime = (dateStr: string) => const formatTime = (dateStr: string) =>
new Date(dateStr).toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }); new Date(dateStr).toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' });
const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-');
export default function MentorTaskCard({ task }: {task: ProjectTask}) { export default function MentorTaskCard({ task }: {task: ProjectTask}) {
const navigate = useNavigate(); const navigate = useNavigate();
const { slug } = useParams<{ slug: string }>(); const { slug } = useParams<{ slug: string }>();
const handleCardClick = () => { const handleCardClick = () => {
navigate(`/intern/projects/${slug}/${task.slug}?type=${task.submission_type}`) navigate(`/mentor/projects/${slug}/${task.slug}?type=${task.submission_type}`)
}; };
return ( return (
@@ -0,0 +1,102 @@
// components/SubmissionStatusPanel.tsx
import { FileText, CheckCircle2, AlertCircle, MinusCircle } from "lucide-react";
import type { TaskSubmissionData, DisplayStatus } from "../../../../types/task.types";
// ─── StatusBadge ────────────────────────────────────────────────────────────
export function StatusBadge({ status }: { status: DisplayStatus }) {
if (status === "submitted" || status === "done")
return (
<span className="inline-flex items-center gap-1.5 text-xs font-semibold text-green-700 bg-green-50 border border-green-200 px-2.5 py-1 rounded-full">
<CheckCircle2 className="w-3.5 h-3.5" />
{status === "done" ? "Done" : "Submitted for grading"}
</span>
);
if (status === "overdue")
return (
<span className="inline-flex items-center gap-1.5 text-xs font-semibold text-red-700 bg-red-50 border border-red-200 px-2.5 py-1 rounded-full">
<AlertCircle className="w-3.5 h-3.5" />
Overdue
</span>
);
return (
<span className="inline-flex items-center gap-1.5 text-xs font-semibold text-gray-500 bg-gray-100 border border-gray-200 px-2.5 py-1 rounded-full">
<MinusCircle className="w-3.5 h-3.5" />
Not submitted
</span>
);
}
// ─── StatusTable ────────────────────────────────────────────────────────────
interface StatusTableProps {
status: DisplayStatus;
submission: TaskSubmissionData | null;
}
export function StatusTable({ status, submission }: StatusTableProps) {
const hasSubmission = status === "submitted" || status === "done";
const rows: { label: string; value: React.ReactNode }[] = [
{
label: "Submission status",
value: <StatusBadge status={status} />,
},
{
label: "Grading status",
value: <span className="text-sm text-gray-400">-</span>,
},
{
label: "Time Remaining",
value: status === "overdue"
? <span className="text-sm text-red-600 font-medium">Overdue</span>
: <span className="text-sm text-gray-400">-</span>,
},
{
label: "Submitted at",
value: submission?.submitted_at
? <span className="text-sm text-gray-700">{new Date(submission.submitted_at).toLocaleString("id-ID")}</span>
: <span className="text-sm text-gray-400">-</span>,
},
{
label: "File submission",
value: hasSubmission && submission?.file_path
? (
<div className="flex items-center gap-2 text-sm text-gray-800">
<FileText className="w-4 h-4 text-red-700 stroke-[2.5]" />
<span>{submission.file_path.split("/").pop()}</span>
</div>
)
: hasSubmission && submission?.url_link
? (
<a
href={submission.url_link}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 underline break-all"
>
{submission.url_link}
</a>
)
: <span className="text-sm text-gray-400">-</span>,
},
];
return (
<div className="bg-box-secondary border border-box-border rounded-2xl overflow-hidden">
<div className="divide-y divide-box-border">
{rows.map((row) => (
<div key={row.label} className="grid grid-cols-[180px_1fr]">
<div className="px-4 py-3 text-sm text-gray-500 border-r border-box-border bg-gray-50/50">
{row.label}
</div>
<div className="px-4 py-3">{row.value}</div>
</div>
))}
</div>
</div>
);
}
View File
+1 -1
View File
@@ -65,7 +65,7 @@ export const useProjectDetail = (id: string) => {
//Khusus Ambil Intern dari Project/{id} //Khusus Ambil Intern dari Project/{id}
export const useProjectInterns = (projectId: string) => { export const useProjectInterns = (projectId: string) => {
const [interns, setInterns] = useState<ProjectMember[]>([]); const [interns, setInterns] = useState<ProjectMember[]>([] );
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
+41 -1
View File
@@ -1,6 +1,6 @@
// hooks/useTasks.ts // hooks/useTasks.ts
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
import type { ProjectTask, ProjectTaskDetail, CreateTaskPayload, UpdateTaskPayload, TaskSubmissionData } from "../types/task.types"; import type { ProjectTask, ProjectTaskDetail, CreateTaskPayload, UpdateTaskPayload, TaskSubmissionData, AdminTaskDetail } from "../types/task.types";
import { import {
getProjectTasks, getProjectTasks,
getTaskById, getTaskById,
@@ -12,7 +12,10 @@ import {
updateSubmissionFile, updateSubmissionFile,
updateSubmissionLink, updateSubmissionLink,
deleteSubmission, deleteSubmission,
getAllMentorTasks,
getTaskSubmissions,
} from "../services/TaskService"; } from "../services/TaskService";
import type { MentorTaskItem } from "../types/task.types";
// GET /task-api/projects/{id_project}/tasks // GET /task-api/projects/{id_project}/tasks
export const useProjectTasks = (projectId: string) => { export const useProjectTasks = (projectId: string) => {
@@ -54,6 +57,25 @@ export const useTaskDetail = (taskId: string, projectId?: string) => {
return { task, loading, error, refetch }; return { task, loading, error, refetch };
}; };
export const useTaskSubmissions = (taskId: string, projectId?: string) => {
const [task, setTask] = useState<AdminTaskDetail | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const refetch = useCallback(() => {
if (!taskId) return;
setLoading(true);
getTaskSubmissions(taskId, projectId)
.then(setTask)
.catch(() => setError("Gagal memuat submissions."))
.finally(() => setLoading(false));
}, [taskId, projectId]);
useEffect(() => { refetch(); }, [refetch]);
return { task, loading, error, refetch };
};
// POST /task-api/projects/{id_project}/tasks // POST /task-api/projects/{id_project}/tasks
export const useCreateTask = (projectId: string) => { export const useCreateTask = (projectId: string) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -179,4 +201,22 @@ export const useSubmission = (taskId: string, initialSubmission?: TaskSubmission
}; };
return { submission, submitFile, submitLink, updateFile, updateLink, remove, loading, error }; return { submission, submitFile, submitLink, updateFile, updateLink, remove, loading, error };
};
// hooks/useAllMentorTasks.ts
export const useAllMentorTasks = (enabled: boolean) => {
const [tasks, setTasks] = useState<MentorTaskItem[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!enabled) return; // tidak fetch kalau intern
setLoading(true);
getAllMentorTasks()
.then(setTasks)
.catch(() => setError("Gagal memuat tasks mentor."))
.finally(() => setLoading(false));
}, [enabled]);
return { tasks, loading, error };
}; };
+24
View File
@@ -0,0 +1,24 @@
import { useState, useEffect } from "react";
import api from "../lib/api";
export interface CurrentUser {
id: number;
full_name: string;
email: string;
role: "mentor" | "intern" | "admin";
}
export const useCurrentUser = () => {
const [user, setUser] = useState<CurrentUser | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
api.get("/auth-api/me")
.then((res) => setUser(res.data.data))
.catch(() => setError("Gagal memuat user."))
.finally(() => setLoading(false));
}, []);
return { user, loading, error };
};
+1 -1
View File
@@ -124,7 +124,7 @@ dialog.custom-dialog::backdrop {
padding-left: 1rem; padding-left: 1rem;
padding-right: 1.5rem; padding-right: 1.5rem;
padding-top: 1rem; padding-top: 1rem;
border-bottom: 2px solid #b3b8c0; /* border-gray-100 */ border-bottom: 1px solid #b3b8c0; /* border-gray-100 */
} }
@media (max-width: 1024px) { @media (max-width: 1024px) {
+30
View File
@@ -0,0 +1,30 @@
import {
LuClipboardList,
LuLightbulb,
LuShield,
} from "react-icons/lu";
import type { IconType } from "react-icons";
import { MdOutlineInsertChart } from "react-icons/md";
import { IoMdCode, IoMdCloudOutline } from "react-icons/io";
import { CiMobile3 } from "react-icons/ci";
import { RiSettings3Fill } from "react-icons/ri";
import { FiUsers } from "react-icons/fi";
import { PiSpeedometer } from "react-icons/pi";
export const PROJECT_ICON_MAP: Record<string, IconType> = {
code: IoMdCode,
chart: MdOutlineInsertChart,
cloud: IoMdCloudOutline,
mobile: CiMobile3,
gear: RiSettings3Fill,
users:FiUsers,
clipboard: LuClipboardList,
speedometer: PiSpeedometer,
lightbulb: LuLightbulb,
shield: LuShield,
};
export const PROJECT_ICON_CODES = Object.keys(PROJECT_ICON_MAP);
export const getProjectIcon = (code: string): IconType =>
PROJECT_ICON_MAP[code] ?? IoMdCode;
+24 -2
View File
@@ -35,7 +35,16 @@ export const getProjectMembers = async (
id: string | number id: string | number
): Promise<ProjectMember[]> => { ): Promise<ProjectMember[]> => {
const res = await api.get(`/project-api/get/${id}`); const res = await api.get(`/project-api/get/${id}`);
return res.data.data.members; console.log(res.data.data.member);
return res.data.data.members.map((m: any) => ({
id: m.id_user,
avatar: m.user.profile_picture ?? null,
full_name: m.user.full_name,
email: m.user.email,
professional_bio: m.user.professional_bio,
position: m.user.position,
kelompok_peminatan: m.user.kelompok_peminatan,
}));
}; };
// GET /project-api/get/{id} // GET /project-api/get/{id}
@@ -43,7 +52,20 @@ export const getProjectById = async (
id: string | number id: string | number
): Promise<ProjectDetail> => { ): Promise<ProjectDetail> => {
const res = await api.get(`/project-api/get/${id}`); const res = await api.get(`/project-api/get/${id}`);
return res.data.data; const raw = res.data.data;
return {
...raw,
members: raw.members.map((m: any) => ({
id: m.id_user,
avatar: m.user?.profile_picture ?? null,
full_name: m.user?.full_name,
email: m.user?.email,
professional_bio: m.user?.professional_bio ?? null,
position: m.user?.position,
kelompok_peminatan: m.user?.kelompok_peminatan,
})),
};
}; };
// PATCH /project-api/update/{id} // PATCH /project-api/update/{id}
+32
View File
@@ -7,6 +7,8 @@ import type {
UpdateTaskPayload, UpdateTaskPayload,
TaskSubmissionData, TaskSubmissionData,
} from "../types/task.types"; } from "../types/task.types";
import type { Project } from "../types/project.types";
import type { MentorTaskItem, AdminTaskDetail } from "../types/task.types";
// ── Task ────────────────────────────────────────────── // ── Task ──────────────────────────────────────────────
@@ -35,6 +37,16 @@ export const getTaskById = async (
return res.data.data; return res.data.data;
}; };
export const getTaskSubmissions = async (
taskId: string | number,
projectId?: string | number
): Promise<AdminTaskDetail> => {
const res = await api.get(`/task-api/tasks/${taskId}`, {
params: projectId ? { project: projectId } : undefined,
});
return res.data.data;
};
export const updateTask = async ( export const updateTask = async (
taskId: string | number, taskId: string | number,
payload: UpdateTaskPayload, payload: UpdateTaskPayload,
@@ -55,6 +67,26 @@ export const deleteTask = async (
}); });
}; };
export const getAllMentorTasks = async (): Promise<MentorTaskItem[]> => {
const projectsRes = await api.get("/project-api/mentor-projects");
const projects: Project[] = projectsRes.data.data;
const tasksByProject = await Promise.all(
projects.map(async (project) => {
const tasksRes = await api.get(`/task-api/projects/${project.id}/tasks`);
return tasksRes.data.data.map((task: any) => ({
...task,
project_name: project.project_name,
project_slug: project.slug,
}));
})
);
return tasksByProject.flat();
};
// ── Submission ──────────────────────────────────────── // ── Submission ────────────────────────────────────────
export const submitTaskFile = async ( export const submitTaskFile = async (
+1 -9
View File
@@ -11,15 +11,6 @@ export interface ProjectAdmin {
professional_bio: string | null; professional_bio: string | null;
} }
export interface ProjectMember {
avatar: string;
id: number;
id_user: number;
status?: MemberStatus;
created_at?: string;
updated_at?: string;
}
// Response GET /project-api/get dan GET /project-api/get/{id} // Response GET /project-api/get dan GET /project-api/get/{id}
export interface Project { export interface Project {
id: number; id: number;
@@ -112,6 +103,7 @@ export interface MemberAssignment {
} }
export interface ProjectMember { export interface ProjectMember {
avatar: string
id: number; id: number;
full_name: string; full_name: string;
email: string; email: string;
+18 -4
View File
@@ -51,19 +51,33 @@ export interface AdminTaskDetail extends ProjectTask {
submissions: Array<{ submissions: Array<{
id_user: number; id_user: number;
full_name: string; full_name: string;
email: string;
file_path: string | null; file_path: string | null;
url_link: string | null; profile_picture: string | null;
submitted_at: string | null; submitted_at: string | null;
updated_at: string | null; updated_at: string | null;
display_status: DisplayStatus;
submission: TaskSubmissionData | null;
}>; }>;
} }
export interface CreateTaskPayload { export interface CreateTaskPayload {
title: string; title: string;
description: string; description: string;
deadline_date: string; // "YYYY-MM-DD" deadline_date: string;
specific_time: string; // "HH:mm" specific_time: string;
submission_type: SubmissionType; submission_type: SubmissionType;
} }
export interface UpdateTaskPayload extends Partial<CreateTaskPayload> {} export interface UpdateTaskPayload extends Partial<CreateTaskPayload> {}
export interface MentorTaskItem {
id: number;
slug: string;
title: string;
description: string;
deadline_at: string;
submission_type: "file_upload" | "url_link";
project_name: string;
project_slug: string;
}