feat: implement mentor dashboard features including project task management, submission tracking, and modal-based task creation.
This commit is contained in:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
@@ -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 };
|
||||||
};
|
};
|
||||||
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user