feat: implement project management dashboard and certification module for mentors and interns

This commit is contained in:
Kemas M. Aryadary Rasyad
2026-06-26 22:53:19 +07:00
parent 60aba4180e
commit 6ec9620f1f
13 changed files with 73 additions and 29 deletions
+5 -1
View File
@@ -35,7 +35,7 @@ import SertificatePage from "./Pages/Internify/intern/SertificatePage";
import ProjectDetailPage from "./Pages/Internify/intern/ProjectDetailPage"; import ProjectDetailPage from "./Pages/Internify/intern/ProjectDetailPage";
import HomeMentorPage from "./Pages/Internify/mentor/HomeMentorPage"; import HomeMentorPage from "./Pages/Internify/mentor/HomeMentorPage";
import MentorProjectsPage from "./Pages/Internify/mentor/BaseProjectsPage"; import MentorProjectsPage from "./Pages/Internify/mentor/BaseProjectsPage";
import MentorCertificatePage from "./Pages/Internify/mentor/CertificatePage"; import MentorCertificatePage from "./Pages/Internify/mentor/CertificateListPage";
import MentorInternPage from "./Pages/Internify/mentor/InternsPage"; import MentorInternPage from "./Pages/Internify/mentor/InternsPage";
import MentorDetailProject from "./Pages/Internify/mentor/ProjectsPage"; import MentorDetailProject from "./Pages/Internify/mentor/ProjectsPage";
import InternifyLogin from "./Pages/Internify/LoginInternify" import InternifyLogin from "./Pages/Internify/LoginInternify"
@@ -48,6 +48,8 @@ import MentorProjectsDetailPage from "./Pages/Internify/mentor/MentorProjectDeta
import ViewInternSubmission from "./Pages/Internify/mentor/ViewInternSubmission"; import ViewInternSubmission from "./Pages/Internify/mentor/ViewInternSubmission";
import SettingsPage from "./Pages/Internify/mentor/MentorSettingsPage"; import SettingsPage from "./Pages/Internify/mentor/MentorSettingsPage";
import ProtectedRoutes from "./Pages/utils/ProtectedRoute"; import ProtectedRoutes from "./Pages/utils/ProtectedRoute";
import CertificateDetail from "./Pages/Internify/mentor/CertificateDetail";
import CertificateResult from "./Pages/Internify/mentor/CertificateGenerate";
function App() { function App() {
return ( return (
@@ -109,6 +111,8 @@ function App() {
<Route index element={<HomeMentorPage />} /> <Route index element={<HomeMentorPage />} />
<Route path="projects" element={<MentorProjectsPage />} /> <Route path="projects" element={<MentorProjectsPage />} />
<Route path="certificates" element={<MentorCertificatePage />} /> <Route path="certificates" element={<MentorCertificatePage />} />
<Route path="certificates/:id" element={<CertificateDetail />} />
<Route path="certificates/result" element={<CertificateResult />} />
<Route path="intern" element={<MentorInternPage />} /> <Route path="intern" element={<MentorInternPage />} />
<Route path="projects/:slug" element={<MentorDetailProject />} /> <Route path="projects/:slug" element={<MentorDetailProject />} />
<Route path="projects/:slug/:taskSlug" element={<MentorProjectsDetailPage />} /> <Route path="projects/:slug/:taskSlug" element={<MentorProjectsDetailPage />} />
-2
View File
@@ -60,8 +60,6 @@ export default function InternifyLogin() {
return ( return (
<div className="min-h-screen bg-background flex flex-col justify-between items-center py-8 px-4 font-sans selection:bg-red-foreground"> <div className="min-h-screen bg-background flex flex-col justify-between items-center py-8 px-4 font-sans selection:bg-red-foreground">
<div className="hidden md:block"></div>
<div className="w-full max-w-[440px] flex flex-col items-center"> <div className="w-full max-w-[440px] flex flex-col items-center">
<div className="text-center mb-6"> <div className="text-center mb-6">
<div className="flex items-center justify-center gap-2 font-bold text-2xl tracking-tight text-black"> <div className="flex items-center justify-center gap-2 font-bold text-2xl tracking-tight text-black">
-1
View File
@@ -10,7 +10,6 @@ export default function ForumTab({ project }: { project: Project }) {
const { tasks, loading, error } = useProjectTasks(slug ?? ""); const { tasks, loading, error } = useProjectTasks(slug ?? "");
return ( return (
<div> <div>
{/* Hero banner */}
<HeroProject title={project.project_name} description={project.description} /> <HeroProject title={project.project_name} description={project.description} />
{/* Loading */} {/* Loading */}
@@ -8,8 +8,6 @@ export default function ProjectDetailPage() {
const { slug } = useParams<{ slug: string }>(); const { slug } = useParams<{ slug: string }>();
const { project, loading, error } = useProjectDetail(slug ?? ""); const { project, loading, error } = useProjectDetail(slug ?? "");
const [activeTab, setActiveTab] = useState<'forum' | 'participants'>('forum'); const [activeTab, setActiveTab] = useState<'forum' | 'participants'>('forum');
// Fallback sederhana
if (loading) return <p className="p-10 text-gray-400">Memuat project...</p>; if (loading) return <p className="p-10 text-gray-400">Memuat project...</p>;
if (error) return <p className="p-10 text-red-500">{error}</p>; if (error) return <p className="p-10 text-red-500">{error}</p>;
if (!project) return <p className="p-10 text-gray-400">Project tidak ditemukan.</p>; if (!project) return <p className="p-10 text-gray-400">Project tidak ditemukan.</p>;
@@ -11,7 +11,6 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { LuHistory, LuSendHorizontal } from 'react-icons/lu'; import { LuHistory, LuSendHorizontal } from 'react-icons/lu';
// Mock Data untuk Tabel Riwayat Sertifikat
const initialHistoryData = [ const initialHistoryData = [
{ id: 1, name: "Jonathan Kristina", email: "JonathanKristina@gmail.com", project: "Internify Project", date: "24 Oct 2024", initials: "JK" }, { id: 1, name: "Jonathan Kristina", email: "JonathanKristina@gmail.com", project: "Internify Project", date: "24 Oct 2024", initials: "JK" },
{ id: 2, name: "Ahmad Faisal", email: "ahmad.faisal@gmail.com", project: "Web Dev Fundamentals", date: "15 Sep 2024", initials: "AF" }, { id: 2, name: "Ahmad Faisal", email: "ahmad.faisal@gmail.com", project: "Web Dev Fundamentals", date: "15 Sep 2024", initials: "AF" },
@@ -2,10 +2,21 @@ 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'; import { useRemoveMember } 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);
const { remove, loading } = useRemoveMember();
const handleRemove = async (id_user: number) => {
if (window.confirm('Are you sure you want to remove this member?')) {
const res = await remove({ id_project: project.id, id_user });
if (res !== null) {
alert('Member removed successfully');
window.location.reload();
}
}
};
return ( return (
<div> <div>
@@ -62,7 +73,11 @@ export default function InternsTab({ project }: { project: ProjectDetail }) {
{member.position} {member.position}
</span> </span>
<div> <div>
<button className='flex items-center gap-1 text-[14px] bg-red-600 text-white p-1 rounded-xl'> <button
onClick={() => handleRemove(member.id)}
disabled={loading}
className='flex items-center gap-1 text-[14px] bg-red-600 text-white p-1 rounded-xl disabled:opacity-50 hover:bg-red-700 transition-colors'
>
<Plus className='w-4 h-4'/>Delete <Plus className='w-4 h-4'/>Delete
</button> </button>
</div> </div>
@@ -11,16 +11,11 @@ interface CreateProjectModalProps {
export default function CreateProjectModal({ isOpen, onClose }: CreateProjectModalProps) { export default function CreateProjectModal({ isOpen, onClose }: CreateProjectModalProps) {
const dialogRef = useRef<HTMLDialogElement>(null); const dialogRef = useRef<HTMLDialogElement>(null);
// State Form Management
const [selectedIcon, setSelectedIcon] = useState(PROJECT_ICON_CODES[0]); 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("");
const [invitedEmails, setInvitedEmails] = useState<string[]>([ const [invitedEmails, setInvitedEmails] = useState<string[]>([]);
"alex.dev@gmail.com",
"sarah.engineer@gmail.com"
]);
const [startDate, setStartDate] = useState(""); const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState(""); const [endDate, setEndDate] = useState("");
const { create, loading, error } = useCreateProject(); const { create, loading, error } = useCreateProject();
@@ -54,7 +49,6 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
}; };
}, [isOpen, onClose]); }, [isOpen, onClose]);
// Fungsi menambah email undangan baru
const handleAddEmail = (e: React.FormEvent) => { const handleAddEmail = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (emailInput.trim() && !invitedEmails.includes(emailInput.trim())) { if (emailInput.trim() && !invitedEmails.includes(emailInput.trim())) {
@@ -63,7 +57,6 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
} }
}; };
// Fungsi menghapus email undangan
const handleRemoveEmail = (emailToRemove: string) => { const handleRemoveEmail = (emailToRemove: string) => {
setInvitedEmails(invitedEmails.filter(email => email !== emailToRemove)); setInvitedEmails(invitedEmails.filter(email => email !== emailToRemove));
}; };
@@ -71,14 +64,19 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
// Handler Submit Utama // Handler Submit Utama
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
const result = await create({ const payload: any = {
project_icon: selectedIcon, project_icon: selectedIcon,
project_name: projectName, project_name: projectName,
description: projectDescription, description: projectDescription,
member_emails: invitedEmails,
start_date: startDate, start_date: startDate,
end_date: endDate, end_date: endDate,
}); };
if (invitedEmails.length > 0) {
payload.member_emails = invitedEmails;
}
const result = await create(payload);
if (result) { if (result) {
onClose(); onClose();
} }
@@ -226,7 +224,7 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
required required
value={startDate} value={startDate}
onChange={(e) => setStartDate(e.target.value)} onChange={(e) => setStartDate(e.target.value)}
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 px-3 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">
</span> </span>
@@ -244,7 +242,7 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
required required
value={endDate} value={endDate}
onChange={(e) => setEndDate(e.target.value)} onChange={(e) => setEndDate(e.target.value)}
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 px-3 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">
</span> </span>
@@ -80,7 +80,7 @@ export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess
required required
value={deadlineDate} value={deadlineDate}
onChange={(e) => setDeadlineDate(e.target.value)} 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 px-3 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>
@@ -89,7 +89,7 @@ export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess
required required
value={specificTime} value={specificTime}
onChange={(e) => setSpecificTime(e.target.value)} 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 px-3 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>
<div className="space-y-1.5"> <div className="space-y-1.5">
@@ -1,6 +1,6 @@
import { useRef, useEffect, useState } from "react"; import { useRef, useEffect, useState } from "react";
import { X } from "lucide-react"; import { X } from "lucide-react";
import { LuSave, LuCalendar } from "react-icons/lu"; import { LuSave } from "react-icons/lu";
interface EditTaskModalProps { interface EditTaskModalProps {
isOpen: boolean; isOpen: boolean;
@@ -20,13 +20,14 @@ export default function MentorProjectCard(project: Project) {
const Icon = getDynamicIcon(project.project_icon); const Icon = getDynamicIcon(project.project_icon);
const handleDeleteClick = (e: React.MouseEvent<HTMLButtonElement>) => { const handleDeleteClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
} }
return ( return (
<div <div
onClick={handleCardClick} onClick={handleCardClick}
className="flex flex-col bg-white rounded-xl overflow-hidden shadow-sm border border-gray-100 w-[180px] min-h-[270px]hover:bg-gray-50 transition-colors"> className="flex flex-col bg-white rounded-xl overflow-hidden shadow-sm border border-gray-100 w-[180px] min-h-[270px]hover:bg-gray-50 transition-colors">
{/* Thumbnail */} {/* Thumbnail */}
<div className="h-[150px] bg-gradient-to-tr from-red-700 to-red-800 overflow-hidden shrink-0 flex items-center justify-center"> <div style={{ backgroundColor: project.background_color ?? '#dc2626' }} className="h-[150px] overflow-hidden shrink-0 flex items-center justify-center">
<Icon className="w-12 h-12 text-white opacity-80" /> <Icon className="w-12 h-12 text-white opacity-80" />
</div> </div>
+22 -1
View File
@@ -21,7 +21,8 @@ import {
updateProject, updateProject,
archiveProject, archiveProject,
assignMember, assignMember,
getProjectMembers getProjectMembers,
removeMember
} from "../services/ProjectService"; } from "../services/ProjectService";
// GET /project-api/get // GET /project-api/get
@@ -228,3 +229,23 @@ export const useAssignMember = () => {
return { assign, loading, error }; return { assign, loading, error };
}; };
export const useRemoveMember = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const remove = async (payload: { id_project: string | number, id_user: number }) => {
setLoading(true);
setError(null);
try {
return await removeMember(payload);
} catch {
setError("Gagal remove member.");
return null;
} finally {
setLoading(false);
}
};
return { remove, loading, error };
};
+12 -2
View File
@@ -15,7 +15,7 @@ import type {
// POST /project-api/add // POST /project-api/add
export const createProject = async ( export const createProject = async (
payload: CreateProjectPayload payload: CreateProjectPayload
): Promise<ProjectDetail> => { ): Promise<Project> => {
const res = await api.post("/project-api/add", payload); const res = await api.post("/project-api/add", payload);
return res.data.data; return res.data.data;
}; };
@@ -23,7 +23,7 @@ export const createProject = async (
// GET /project-api/get // GET /project-api/get
export const getProjects = async ( export const getProjects = async (
status?: "active" | "completed" | "archived" status?: "active" | "completed" | "archived"
): Promise<Project[]> => { ): Promise<ProjectDetail[]> => {
const res = await api.get("/project-api/get", { const res = await api.get("/project-api/get", {
params: status ? { status } : undefined, params: status ? { status } : undefined,
}); });
@@ -116,3 +116,13 @@ export const assignMember = async (
const res = await api.post("/project-api/assign-member", payload); const res = await api.post("/project-api/assign-member", payload);
return res.data.data; return res.data.data;
}; };
export const removeMember = async (
payload: {
id_project: string | number;
id_user: number;
}
): Promise<MemberAssignment> => {
const res = await api.post("/project-api/remove-member", { id_project: payload.id_project, id_user: payload.id_user });
return res.data.data;
};
+1
View File
@@ -16,6 +16,7 @@ export interface Project {
id: number; id: number;
slug: string; slug: string;
project_icon: string; project_icon: string;
background_color: string;
project_name: string; project_name: string;
description: string; description: string;
start_date: string; start_date: string;