feat: implement project management dashboard and certification module for mentors and interns
This commit is contained in:
+5
-1
@@ -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 />} />
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user