diff --git a/src/App.tsx b/src/App.tsx index a6b06fc..cd16b03 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -31,23 +31,26 @@ import EditLowongan from "./Pages/EditLowongan"; import HomeInternPage from "./Pages/Internify/intern/HomeInternPage"; import { InternLayout, MentorLayout } from "./Pages/Internify/layout"; import ProjectsPage from "./Pages/Internify/intern/ProjectsPage"; -import SertificatePage from "./Pages/Internify/intern/SertificatePage"; +import SertificatePage from "./Pages/Internify/intern/InternClaimCert"; import ProjectDetailPage from "./Pages/Internify/intern/ProjectDetailPage"; import HomeMentorPage from "./Pages/Internify/mentor/HomeMentorPage"; 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 MentorDetailProject from "./Pages/Internify/mentor/ProjectsPage"; import InternifyLogin from "./Pages/Internify/LoginInternify" import FAQPage from "./Pages/Internify/FAQpage"; import NotificationList from "./Pages/Internify/NotificationList"; import SettingsContent from "./Pages/Internify/SettingsPage"; +import MentorSettingsPage from "./Pages/Internify/mentor/MentorSettingsPage"; import TaskSubmission from "./Pages/Internify/intern/TaskSubmission"; -import SertificateList from "./Pages/Internify/intern/SertificateList"; +import SertificateList from "./Pages/Internify/intern/InternCertList"; import MentorProjectsDetailPage from "./Pages/Internify/mentor/MentorProjectDetailPage"; import ViewInternSubmission from "./Pages/Internify/mentor/ViewInternSubmission"; -import SettingsPage from "./Pages/Internify/mentor/MentorSettingsPage"; import ProtectedRoutes from "./Pages/utils/ProtectedRoute"; +import CertificateDetail from "./Pages/Internify/mentor/CertificateForm"; +import CertificateResult from "./Pages/Internify/mentor/CertificateResult"; +import NotFoundPage from "./Pages/NotFoundPage"; function App() { return ( @@ -93,8 +96,8 @@ function App() { }> } /> } /> - } /> - } /> + } /> + } /> } /> } /> } /> @@ -103,22 +106,27 @@ function App() { - {/* Mentor only */} - }> + {/* Mentor/Admin only */} + }> }> } /> } /> } /> + } /> + } /> } /> } /> } /> } /> } /> } /> - } /> + } /> + {/* 404 catch-all */} + } /> + ); diff --git a/src/Layout/Header-internify.tsx b/src/Layout/Header-internify.tsx index 553e2f7..990cdb7 100644 --- a/src/Layout/Header-internify.tsx +++ b/src/Layout/Header-internify.tsx @@ -1,14 +1,36 @@ import { Bell, Menu } from "lucide-react" import humiclogo from "../assets/logo.png"; import { useNavigate } from "react-router-dom"; +import { useCurrentUser } from "../hooks/useUser"; + +/** Singkatan nama: ambil huruf pertama setiap kata, maksimal 2 huruf. + * "Andi Mahesa" → "AM" + * "Andi Bayu Cendika" → "AC" (huruf pertama + huruf pertama kata terakhir) + */ +function getInitials(fullName: string | undefined | null): string { + if (!fullName) return "U"; + const parts = fullName.trim().split(/\s+/).filter(Boolean); + if (parts.length === 0) return "U"; + if (parts.length === 1) return parts[0].charAt(0).toUpperCase(); + return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase(); +} export default function Header({ toggleSidebar }: { toggleSidebar: () => void }) { const nav = useNavigate(); + const { user } = useCurrentUser(); + + // Tentukan base path settings sesuai role + const settingsPath = + user?.role === "intern" ? "/intern/settings" : "/mentor/settings"; + + const initials = user?.full_name ? getInitials(user.full_name) : "U"; + const hasPhoto = !!user?.profile_picture; + return ( -
+
-
- -
-
+ {/* Gambar Profil */} +
) diff --git a/src/Pages/NotFoundPage.tsx b/src/Pages/NotFoundPage.tsx new file mode 100644 index 0000000..0f5932a --- /dev/null +++ b/src/Pages/NotFoundPage.tsx @@ -0,0 +1,63 @@ +import { useNavigate } from "react-router-dom"; +import { Home, ArrowLeft } from "lucide-react"; + +export default function NotFoundPage() { + const navigate = useNavigate(); + + return ( +
+ {/* Background decorative blobs */} +
+
+ +
+ + {/* 404 Large Text */} +
+ + 404 + +
+
+
+ + {/* Title */} +

+ Not Found +

+ + {/* Description */} +

+ Sorry, the page you are looking for does not exist or has been moved. Let's get back on track. +

+ + {/* Divider */} +
+ + {/* Buttons */} +
+ + + +
+ + {/* Footer note */} +

+ Internify © {new Date().getFullYear()} — HUMIC Engineering +

+
+
+ ); +} diff --git a/src/Pages/internify/LoginInternify.tsx b/src/Pages/internify/LoginInternify.tsx index 8799e4d..facd508 100644 --- a/src/Pages/internify/LoginInternify.tsx +++ b/src/Pages/internify/LoginInternify.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import logo from "../../assets/logo.png"; import { useNavigate } from "react-router-dom"; import { Mail, Lock, Eye, EyeOff } from 'lucide-react'; -import api from "../../lib/api"; +import { useLogin } from "../../hooks/useUser"; const decodeJWT = (token: string) => { try { @@ -19,49 +19,37 @@ export default function InternifyLogin() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [showPassword, setShowPassword] = useState(false); - const [error, setError] = useState(""); - const [loading, setLoading] = useState(false); + + const { login, loading, error, setError } = useLogin(); const handleSignIn = async (e: React.FormEvent) => { e.preventDefault(); - setError(""); + setError(null); if (!email || !password) { setError("Email dan password wajib diisi."); return; } - try { - setLoading(true); - const response = await api.post("/auth-api/login", { email, password }); + const data = await login(email, password); + if (!data) return; - const { token } = response.data.data; - document.cookie = `token=${token}; path=/; SameSite=Strict`; - - const decoded = decodeJWT(token); - const role = decoded?.role; - if (role === "intern") { - navigate("/intern"); - } else if (role === "admin") { - navigate("/mentor"); - } else { - setError("Role user tidak dikenali."); - } - } catch (err: any) { - if (err.response?.status === 401 || err.response?.status === 400) { - setError("Email atau password salah."); - } else { - setError("Terjadi kesalahan. Coba beberapa saat lagi."); - } - } finally { - setLoading(false); + const { token } = data; + document.cookie = `token=${token}; path=/; SameSite=Strict`; + + const decoded = decodeJWT(token); + const role = decoded?.role; + if (role === "intern") { + navigate("/intern"); + } else if (role === "mentor" || role === "admin") { + navigate("/mentor"); + } else { + setError("Role user tidak dikenali."); } }; return ( -
-
- +
@@ -71,8 +59,8 @@ export default function InternifyLogin() { HUMIC LMS PORTAL

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

Welcome back

Access your dashboard with your credentials.

@@ -132,7 +120,7 @@ export default function InternifyLogin() { disabled={loading} className="w-full bg-[#B30000] hover:bg-[#990000] text-white font-medium text-sm py-2.5 rounded-lg transition-colors shadow-sm mt-2" > - Sign In + {loading ? "Signing in..." : "Sign In"} @@ -147,14 +135,14 @@ export default function InternifyLogin() {
- + SUPPORT
- */} +
- {/* List Group Section */} + {notifications.length === 0 && ( +

Belum ada notifikasi.

+ )} +
{/* Render Group TODAY */} @@ -184,6 +187,13 @@ export default function NotificationList() { ))}
+ {/* OLDER */} + {olderNotifs.length > 0 && ( +
+

Older

+ {olderNotifs.map(n => renderNotifCard(n, true))} +
+ )}
); diff --git a/src/Pages/internify/SettingsPage.tsx b/src/Pages/internify/SettingsPage.tsx index 01284d1..9ac53ed 100644 --- a/src/Pages/internify/SettingsPage.tsx +++ b/src/Pages/internify/SettingsPage.tsx @@ -1,36 +1,26 @@ import { useState, useEffect } from "react"; -import api from "../../lib/api"; +import { useCurrentUser, useUpdateProfile } from "../../hooks/useUser"; export default function SettingsContent() { + const { user, loading } = useCurrentUser(); + const { save, loading: saving, error: errorMsg, successMsg } = useUpdateProfile(); + const [formData, setFormData] = useState({ fullName: "", email: "", - bio: "" + bio: "", }); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [successMsg, setSuccessMsg] = useState(""); - const [errorMsg, setErrorMsg] = useState(""); - + // Sync form data when user is loaded useEffect(() => { - const fetchData = async () => { - try { - const response = await api.patch("/auth-api/update-profile"); - const data = response.data.data; - setFormData({ - fullName: data.full_name || "", - email: data.email || "", - bio: data.professional_bio || "", - }); - } catch (err) { - console.error("Failed to fetch profile:", err) - } finally { - setLoading(false); - } - }; - fetchData(); - }, []); + if (user) { + setFormData({ + fullName: user.full_name || "", + email: user.email || "", + bio: user.professional_bio || "", + }); + } + }, [user]); const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; @@ -39,25 +29,14 @@ export default function SettingsContent() { const handleSaveChanges = async (e: React.FormEvent) => { e.preventDefault(); - setSuccessMsg(""); - setErrorMsg(""); - setSaving(true); - - try { - await api.patch("/auth-api/update-profile", { - full_name: formData.fullName, - email: formData.email, - professional_bio: formData.bio, - }); - setSuccessMsg("Profile updated successfully!"); - } catch (err: any) { - setErrorMsg(err?.response?.data?.message || "Failed to save changes."); - } finally { - setSaving(false); - } + await save({ + full_name: formData.fullName, + email: formData.email, + professional_bio: formData.bio, + }); }; - const initialLetter = formData.fullName ? formData.fullName.charAt(1).toUpperCase() : "U"; + const initialLetter = formData.fullName ? formData.fullName.charAt(0).toUpperCase() : "U"; if (loading) { return ( diff --git a/src/Pages/internify/intern/ForumTab.tsx b/src/Pages/internify/intern/ForumTab.tsx index ace03e0..235fdbf 100644 --- a/src/Pages/internify/intern/ForumTab.tsx +++ b/src/Pages/internify/intern/ForumTab.tsx @@ -1,16 +1,13 @@ -import { useParams } from 'react-router-dom'; import ProjectTimeline from './components/ProjectTimeline'; import TaskCard from './components/ProjectTaskCard'; import HeroProject from './components/Hero-project'; -import { useProjectTasks } from '../../../hooks/useTasks'; +import { useProjectMyTasks } from '../../../hooks/useProjects'; import type { Project } from '../../../types/project.types'; export default function ForumTab({ project }: { project: Project }) { - const { slug } = useParams<{ slug: string }>(); - const { tasks, loading, error } = useProjectTasks(slug ?? ""); + const { tasks, loading, error } = useProjectMyTasks(project.id); return (
- {/* Hero banner */} {/* Loading */} diff --git a/src/Pages/internify/intern/InternCertList.tsx b/src/Pages/internify/intern/InternCertList.tsx new file mode 100644 index 0000000..b8cfc9b --- /dev/null +++ b/src/Pages/internify/intern/InternCertList.tsx @@ -0,0 +1,94 @@ +import { Loader2, AlertCircle } from "lucide-react"; +import { useMyProjects } from "../../../hooks/useProjects"; +import type { Project } from "../../../types/project.types"; +import { useNavigate } from "react-router-dom"; +import * as LucideIcons from 'lucide-react'; +import type { LucideProps } from 'lucide-react'; + + +export default function SertificateList() { + const { projects, loading, error } = useMyProjects(); + const navigate = useNavigate(); + + const getDynamicIcon = (iconName: string) => { + const formatted = iconName.charAt(0).toUpperCase() + iconName.slice(1); + const Icon = (LucideIcons as unknown as Record>)[formatted]; + return Icon ?? LucideIcons.FolderOpen; + }; + return ( +
+ {/* Header */} +
+

Certificate History

+

All your earned internship certificates

+
+ + {/* Loading */} + {loading && ( +
+ + Loading certificates... +
+ )} + + {/* Error */} + {error && !loading && ( +
+ + {error} +
+ )} + + {/* Empty */} + {!loading && !error && projects.length === 0 && ( +
+

No ongoing projects yet.

+
+ )} + + {/* Grid */} + {!loading && !error && projects.length > 0 && ( +
+ {projects.map((item: Project) => ( +
navigate(`/intern/certificates/${item.slug}`, { state: { project: item } })} + > + {/* Thumbnail */} +
+
+
+ {(() => { + const Icon = getDynamicIcon(item.project_icon); + return ; + })()} +
+
+
+ + {/* Info */} +
+

+ {new Date(item.start_date).toLocaleDateString("id-ID", { + year: "numeric", + month: "long", + })} until {new Date(item.end_date).toLocaleDateString("id-ID", { + year: "numeric", + month: "long", + })} +

+

+ {item.project_name} +

+

+ No: {item.status} +

+
+
+ ))} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/Pages/internify/intern/InternClaimCert.tsx b/src/Pages/internify/intern/InternClaimCert.tsx new file mode 100644 index 0000000..a588137 --- /dev/null +++ b/src/Pages/internify/intern/InternClaimCert.tsx @@ -0,0 +1,79 @@ +import { useMemo } from "react"; +import { useParams } from "react-router-dom"; +import { useLocation } from "react-router-dom"; +import CertificateAvailable from "./components/CertificateAvailabe"; +import CertificateNotAvailable from "./components/CertificateNotAvailable"; +import SertificateHistory from "./components/CertificateHistory"; +import { useMyCertificates } from "../../../hooks/useSertificates"; +import { useProjectDetail } from "../../../hooks/useProjects"; +import type { Project } from "../../../types/project.types"; + +const SertificatePage = () => { + const { slug } = useParams<{ slug: string }>(); + const location = useLocation(); + const projectFromList = location.state?.project as Project | undefined; + + const { project: projectDetail, loading: projectLoading } = useProjectDetail(slug ?? ""); + const { certificates, loading: certLoading } = useMyCertificates(); + + const project = useMemo(() => { + if (!projectDetail) return null; + return { + ...projectDetail, + task_done: projectFromList?.task_done ?? 0, + total_tasks: projectFromList?.total_tasks ?? projectDetail.total_tasks, + }; + }, [projectDetail, projectFromList]); + + const allTasksDone = !projectLoading && !!project && project.task_done >= project.total_tasks; + + const alreadyDone = useMemo(() => { + if (!project || !certificates.length) return false; + return certificates.some((cert) => cert.project.id === project.id); + }, [certificates, project]); + + const progress = + project && project.total_tasks > 0 + ? Math.round((project.task_done / project.total_tasks) * 100) + : 0; + + const remainingTasks = project ? project.total_tasks - project.task_done : 0; + + const isLoading = projectLoading || certLoading; + const showCertificate = alreadyDone; + + if (isLoading) { + return ( +
+ Loading... +
+ ); + } + + return ( +
+
+

Certificates

+

View and download internship certificates

+
+
+
+ {showCertificate ? ( + + ) : ( + + )} +
+
+ +
+
+
+ ); +}; + +export default SertificatePage; \ No newline at end of file diff --git a/src/Pages/internify/intern/ParticipantTab.tsx b/src/Pages/internify/intern/ParticipantTab.tsx index d851ac6..82bf71f 100644 --- a/src/Pages/internify/intern/ParticipantTab.tsx +++ b/src/Pages/internify/intern/ParticipantTab.tsx @@ -1,11 +1,6 @@ -import { useParams } from 'react-router-dom'; -import { } from '../../../hooks/useProjects'; -import type { Project } from '../../../types/project.types'; - -export default function ParticipantsTab({ project }: { project: Project }) { - const { slug } = useParams<{ slug: string }>(); - const { interns, loading, error } = useProjectInterns(slug ?? ""); +import type { ProjectDetail, ProjectMember } from '../../../types/project.types'; +export default function ParticipantsTab({ project }: { project: ProjectDetail }) { return (
{/* Mentor */} @@ -30,44 +25,29 @@ export default function ParticipantsTab({ project }: { project: Project }) { {/* Members */}

Members

- - {loading && ( -
- {[...Array(3)].map((_, i) => ( -
- ))} -
- )} - - {error &&

{error}

} - - {!loading && !error && interns.map((intern, idx) => ( + {project.members.map((member: ProjectMember, idx) => (
- {intern.avatar ? ( - {intern.full_name} + {member.avatar ? ( + {member.full_name} ) : (
)}
- {intern.full_name} - {intern.email} + {member.full_name} + {member.email}
- {intern.kelompok_peminatan} + {member.kelompok_peminatan}
))} - {!loading && !error && interns.length === 0 && ( + {project.members.length === 0 && (

Belum ada members.

)}
diff --git a/src/Pages/internify/intern/ProjectDetailPage.tsx b/src/Pages/internify/intern/ProjectDetailPage.tsx index 7b97d40..fa8a315 100644 --- a/src/Pages/internify/intern/ProjectDetailPage.tsx +++ b/src/Pages/internify/intern/ProjectDetailPage.tsx @@ -1,6 +1,5 @@ import { useState } from 'react'; import { useParams } from 'react-router-dom'; -// import { useProjectDetail } from '../../../hooks/useProjectDetail'; import { useProjectDetail } from '../../../hooks/useProjects'; import ForumTab from './ForumTab'; import ParticipantsTab from './ParticipantTab'; @@ -10,9 +9,9 @@ export default function ProjectDetailPage() { const { project, loading, error } = useProjectDetail(slug ?? ""); const [activeTab, setActiveTab] = useState<'forum' | 'participants'>('forum'); - // Fallback sederhana if (loading) return

Memuat project...

; if (error) return

{error}

; + if (!project) return

Project tidak ditemukan.

; const tabClass = (tab: string) => diff --git a/src/Pages/internify/intern/SertificateList.tsx b/src/Pages/internify/intern/SertificateList.tsx deleted file mode 100644 index 74a7264..0000000 --- a/src/Pages/internify/intern/SertificateList.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Award } from "lucide-react"; -import { history, type CertificateHistory } from "../../../lib/mockSertificates"; -import certificateImg from "../../../assets/certificate.png"; - -export default function SertificateList() { - return ( -
- {/* Header */} -
-

Certificate History

-

All your earned internship certificates

-
- - {/* Grid */} - {history.length === 0 ? ( -
- -

No certificates yet.

-
- ) : ( -
- {history.map((item: CertificateHistory) => ( -
- {/* Thumbnail */} -
-
- {item.title} -
-
- - {/* Info */} -
-

{item.month}

-

{item.title}

-

Grade: {item.grade}

-
-
- ))} -
- )} -
- ); -} \ No newline at end of file diff --git a/src/Pages/internify/intern/SertificatePage.tsx b/src/Pages/internify/intern/SertificatePage.tsx deleted file mode 100644 index 004bb47..0000000 --- a/src/Pages/internify/intern/SertificatePage.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { certificateData } from "../../../lib/mockSertificates"; -import CertificateAvailable from "./components/CertificateAvailabe"; -import CertificateNotAvailable from "./components/CertificateNotAvailable"; -import SertificateHistory from "./components/CertificateHistory"; - -const SertificatePage = () => { - const data = certificateData; - - return ( -
- {/* Header */} -
-

Certificates

-

View and download internship certificates

-
-
- {/* Kiri: Content */} -
- {data.isReceived ? ( - - ) : ( - - )} -
- {/* Kanan: History */} -
- -
-
-
- ); -}; - -export default SertificatePage; diff --git a/src/Pages/internify/intern/TaskSubmission.tsx b/src/Pages/internify/intern/TaskSubmission.tsx index f149654..29e5ea2 100644 --- a/src/Pages/internify/intern/TaskSubmission.tsx +++ b/src/Pages/internify/intern/TaskSubmission.tsx @@ -49,13 +49,15 @@ export default function TaskSubmission() { {submissionType === "file_upload" ? ( ) : ( diff --git a/src/Pages/internify/intern/components/CertificateAvailabe.tsx b/src/Pages/internify/intern/components/CertificateAvailabe.tsx index 028f62d..70757f6 100644 --- a/src/Pages/internify/intern/components/CertificateAvailabe.tsx +++ b/src/Pages/internify/intern/components/CertificateAvailabe.tsx @@ -1,10 +1,13 @@ import { useState } from "react"; import { Download, Link, Printer } from "lucide-react"; -import { type CertificateData } from "../../../../lib/mockSertificates"; +import type { ProjectDetail } from "../../../../types/project.types"; import certificateImg from "../../../../assets/certificate.png"; -export default function CertificateAvailable({ data }: { data: CertificateData }) { - const { certificate } = data; +interface CertificateAvailableProps { + project: ProjectDetail | null; +} + +export default function CertificateAvailable({ project }: CertificateAvailableProps) { const [copied, setCopied] = useState(false); const handleCopyLink = () => { @@ -13,10 +16,25 @@ export default function CertificateAvailable({ data }: { data: CertificateData } setTimeout(() => setCopied(false), 2000); }; + // Hitung durasi dari start_date - end_date + const duration = project + ? (() => { + const start = new Date(project.start_date); + const end = new Date(project.end_date); + const diffMs = end.getTime() - start.getTime(); + const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24)); + const months = Math.floor(diffDays / 30); + const days = diffDays % 30; + if (months > 0 && days > 0) return `${months} Bulan ${days} Hari`; + if (months > 0) return `${months} Bulan`; + return `${diffDays} Hari`; + })() + : "-"; + return (
{/* Certificate Card */} -
+
Certificate
{[ - { label: "Nama Program", value: certificate.programName }, - { label: "Durasi", value: certificate.duration }, + { label: "Nama Program", value: project?.project_name ?? "-" }, + { label: "Durasi", value: duration }, + { label: "Total Tugas", value: project ? `${project.total_tasks} Tugas` : "-" }, { label: "Status", value: ( - VERIFIED + UNVERIFIED ), }, diff --git a/src/Pages/internify/intern/components/CertificateHistory.tsx b/src/Pages/internify/intern/components/CertificateHistory.tsx index 0f0048d..55b37e5 100644 --- a/src/Pages/internify/intern/components/CertificateHistory.tsx +++ b/src/Pages/internify/intern/components/CertificateHistory.tsx @@ -1,38 +1,69 @@ import { useNavigate } from "react-router-dom"; -import { History } from "lucide-react"; -import { type CertificateHistory, history as certificateHistory } from "../../../../lib/mockSertificates"; +import { History, Loader2, AlertCircle } from "lucide-react"; +import type { Certificate } from "../../../../types/certificate.types"; import certificateImg from "../../../../assets/certificate.png"; +import { useMyCertificates } from "../../../../hooks/useSertificates"; export default function SertificateHistory() { const navigate = useNavigate(); + const { certificates, loading, error } = useMyCertificates(); + const displayed = certificates.slice(0, 2); + return ( -
+

Certificate History

-
- {certificateHistory.slice(0, 2).map((item: CertificateHistory) => ( -
-
- Certificate + {loading && ( +
+ + Loading certificates... +
+ )} + + {error && !loading && ( +
+ + {error} +
+ )} + + {!loading && !error && displayed.length === 0 && ( +

No certificates yet.

+ )} + + {!loading && !error && displayed.length > 0 && ( +
+ {displayed.map((item: Certificate) => ( +
+
+ Certificate +
+
+

+ {new Date(item.issued_at).toLocaleDateString("id-ID", { + year: "numeric", + month: "long", + })} +

+

+ {item.project.project_name} +

+

No: {item.certificate_no}

+
-
-

{item.month}

-

{item.title}

-

Grade: {item.grade}

-
-
- ))} -
+ ))} +
+ )}
+ {task.submission_status} +
); diff --git a/src/Pages/internify/intern/components/ProjectsCard.tsx b/src/Pages/internify/intern/components/ProjectsCard.tsx index 2cbea7c..2183882 100644 --- a/src/Pages/internify/intern/components/ProjectsCard.tsx +++ b/src/Pages/internify/intern/components/ProjectsCard.tsx @@ -14,7 +14,7 @@ const ProjectCard = (project: Project) => { const Icon = getDynamicIcon(project.project_icon); - const tasksDone = -1; + const tasksDone = project.task_done; const tasksTotal = project.total_tasks; return ( @@ -23,7 +23,7 @@ const ProjectCard = (project: Project) => { className=" rounded-xl overflow-hidden shadow-sm border border-gray-100 w-full flex flex-col cursor-pointer hover:shadow-md transition-shadow duration-200" > {/* Thumbnail */} -
+
diff --git a/src/Pages/internify/intern/components/SidebarIntern.tsx b/src/Pages/internify/intern/components/SidebarIntern.tsx index 6f1c2f6..135c8c7 100644 --- a/src/Pages/internify/intern/components/SidebarIntern.tsx +++ b/src/Pages/internify/intern/components/SidebarIntern.tsx @@ -48,7 +48,7 @@ export default function SidebarIntern({ isOpen, closeSidebar }: SidebarProps) { {/* Brand & Tombol Close untuk Mobile */}
-
+

Internify

LMS Portal

diff --git a/src/Pages/internify/intern/components/TaskForm-file.tsx b/src/Pages/internify/intern/components/TaskForm-file.tsx index 903446a..2dd2c2c 100644 --- a/src/Pages/internify/intern/components/TaskForm-file.tsx +++ b/src/Pages/internify/intern/components/TaskForm-file.tsx @@ -7,12 +7,13 @@ import type { TaskSubmissionData } from "../../../../types/task.types"; interface TaskFormFileProps { taskId: string; + projectId?: string; deadline?: Date; initialSubmission?: TaskSubmissionData | null; } -export default function TaskFormFile({ taskId, deadline, initialSubmission }: TaskFormFileProps) { - const { submission, loading, submitFile, updateFile, remove } = useSubmission(taskId, initialSubmission); +export default function TaskFormFile({ taskId, projectId, deadline, initialSubmission }: TaskFormFileProps) { + const { submission, loading, submitFile, updateFile, remove } = useSubmission(taskId, initialSubmission, projectId); const [files, setFiles] = useState([]); const [isDragActive, setIsDragActive] = useState(false); const [isEditing, setIsEditing] = useState(false); diff --git a/src/Pages/internify/intern/components/TaskForm-link.tsx b/src/Pages/internify/intern/components/TaskForm-link.tsx index 618682d..ae12080 100644 --- a/src/Pages/internify/intern/components/TaskForm-link.tsx +++ b/src/Pages/internify/intern/components/TaskForm-link.tsx @@ -7,12 +7,13 @@ import type { TaskSubmissionData } from "../../../../types/task.types"; interface TaskFormLinkProps { taskId: string; + projectId?: string; deadline?: Date; initialSubmission?: TaskSubmissionData | null; } -export default function TaskFormLink({ taskId, deadline, initialSubmission }: TaskFormLinkProps) { - const { submission, loading, submitLink, updateLink, remove } = useSubmission(taskId, initialSubmission); +export default function TaskFormLink({ taskId, projectId, deadline, initialSubmission }: TaskFormLinkProps) { + const { submission, loading, submitLink, updateLink, remove } = useSubmission(taskId, initialSubmission, projectId); const [inputLink, setInputLink] = useState(initialSubmission?.url_link ?? ""); const [isEditing, setIsEditing] = useState(false); diff --git a/src/Pages/internify/layout.tsx b/src/Pages/internify/layout.tsx index 2c05d3e..8c147a5 100644 --- a/src/Pages/internify/layout.tsx +++ b/src/Pages/internify/layout.tsx @@ -47,7 +47,7 @@ export const MentorLayout = () => { /> )}
- {/* */} +
diff --git a/src/Pages/internify/mentor/BaseProjectsPage.tsx b/src/Pages/internify/mentor/BaseProjectsPage.tsx index 420d919..337f706 100644 --- a/src/Pages/internify/mentor/BaseProjectsPage.tsx +++ b/src/Pages/internify/mentor/BaseProjectsPage.tsx @@ -1,11 +1,13 @@ -import { mockProjects } from '../../../lib/mockProjects'; import MentorProjectCard from './components/MentorProjectCard'; import { Plus } from 'lucide-react'; import { useState } from 'react'; import CreateProjectModal from './components/CreateProjectDialog'; +import { useProjects } from '../../../hooks/useProjects'; -export default function MentorProjectsPage(){ + +export default function MentorProjectsPage() { const [isModalOpen, setIsModalOpen] = useState(false); + const { projects, loading, error } = useProjects("active"); const openDialog = () => { setIsModalOpen(true); @@ -15,8 +17,18 @@ export default function MentorProjectsPage(){ setIsModalOpen(false); }; + if (loading) return ( +
+ {[...Array(3)].map((_, i) => ( +
+ ))} +
+ ); + + if (error) return

{error}

; + return ( -
+

List Projects

@@ -24,8 +36,8 @@ export default function MentorProjectsPage(){
{/* Tombol pemicu buka modal */} -
- {mockProjects.map(project => ( - + {projects.map(project => ( + ))}
{isModalOpen && ( - )}
diff --git a/src/Pages/internify/mentor/CertificateDetail.tsx b/src/Pages/internify/mentor/CertificateDetail.tsx deleted file mode 100644 index 475cd02..0000000 --- a/src/Pages/internify/mentor/CertificateDetail.tsx +++ /dev/null @@ -1,361 +0,0 @@ -// src/pages/mentor/CertificateDetail.tsx -import { useState } from "react"; -import { useNavigate } from "react-router-dom"; -import { - CalendarDays, - CheckCircle2, - AlertCircle, - Eye, - ChevronLeft, - ChevronRight, - Sparkles, - ArrowLeft, -} from "lucide-react"; - -// ─── Types ──────────────────────────────────────────────────────────────────── -type InternStatus = "ELIGIBLE" | "INCOMPLETE"; - -interface Intern { - id: number; - name: string; - role: string; - initials: string; - taskProgress: number; - taskTotal: number; - status: InternStatus; -} - -// ─── Mock Data ──────────────────────────────────────────────────────────────── -const mockProject = { - title: "Edge Computing Framework", - period: "Dec 2023 — Mar 2024", - status: "COMPLETED" as const, - scope: - "Advanced research and implementation of low-latency computing nodes at the network edge, focusing on real-time data processing for autonomous systems.", - totalInterns: 21, - eligibleInterns: 18, -}; - -const mockInterns: Intern[] = [ - { id: 1, name: "Marcus Rodriguez", role: "Software Engineer Intern", initials: "MR", taskProgress: 10, taskTotal: 10, status: "ELIGIBLE" }, - { id: 2, name: "Priya Sharma", role: "Frontend Developer Intern", initials: "PS", taskProgress: 8, taskTotal: 10, status: "INCOMPLETE" }, - { id: 3, name: "James Smith", role: "QA Engineering Intern", initials: "JS", taskProgress: 10, taskTotal: 10, status: "ELIGIBLE" }, - { id: 4, name: "Nina Castillo", role: "Mobile Development Intern", initials: "NC", taskProgress: 9, taskTotal: 10, status: "ELIGIBLE" }, - { id: 5, name: "Derek Lim", role: "DevOps & Cloud Intern", initials: "DL", taskProgress: 7, taskTotal: 10, status: "INCOMPLETE" }, - { id: 6, name: "Alex Johnson", role: "Backend Engineering Intern", initials: "AJ", taskProgress: 10, taskTotal: 10, status: "ELIGIBLE" }, -]; - -const TOTAL_ENTRIES = 1284; -const PER_PAGE = 3; - -// ─── Avatar ─────────────────────────────────────────────────────────────────── -const avatarColors: Record = { - MR: "bg-blue-100 text-blue-700", - PS: "bg-purple-100 text-purple-700", - JS: "bg-green-100 text-green-700", - NC: "bg-pink-100 text-pink-700", - DL: "bg-teal-100 text-teal-700", - AJ: "bg-orange-100 text-orange-700", -}; - -function Avatar({ initials }: { initials: string }) { - const color = avatarColors[initials] ?? "bg-gray-100 text-gray-500"; - return ( -
- {initials} -
- ); -} - -// ─── Progress Bar ───────────────────────────────────────────────────────────── -function ProgressBar({ value, total }: { value: number; total: number }) { - const pct = Math.round((value / total) * 100); - const isComplete = value === total; - return ( -
-
-
-
- - {value}/{total} - -
- ); -} - -// ─── Status Badge ───────────────────────────────────────────────────────────── -function StatusBadge({ status }: { status: InternStatus }) { - const isEligible = status === "ELIGIBLE"; - return ( -
- {isEligible - ? - : } - {status} -
- ); -} - -// ─── Main Page ──────────────────────────────────────────────────────────────── -export default function CertificateDetail() { - const navigate = useNavigate(); - - // Selected interns (checked) - const initialSelected = new Set( - mockInterns.filter((i) => i.status === "ELIGIBLE").map((i) => i.id) - ); - const [selected, setSelected] = useState>(initialSelected); - const [page, setPage] = useState(1); - - const totalPages = Math.ceil(mockInterns.length / PER_PAGE); - const paginated = mockInterns.slice((page - 1) * PER_PAGE, page * PER_PAGE); - - const eligibleIds = mockInterns.filter((i) => i.status === "ELIGIBLE").map((i) => i.id); - const allEligibleSelected = eligibleIds.every((id) => selected.has(id)); - - const toggleSelectAll = () => { - if (allEligibleSelected) { - setSelected(new Set()); - } else { - setSelected(new Set(eligibleIds)); - } - }; - - const toggleOne = (id: number, status: InternStatus) => { - if (status === "INCOMPLETE") return; // tidak bisa dipilih - setSelected((prev) => { - const next = new Set(prev); - next.has(id) ? next.delete(id) : next.add(id); - return next; - }); - }; - - return ( -
- {/* Breadcrumb */} -
navigate("/mentor/certificates/all")} - > - Certificates › -
- - {/* Page Header */} -
-

Certificates

-

- Administrate, issue, and track professional certifications for all interns. -

-
- - {/* ── Two-column layout ── */} -
- - {/* ── LEFT: Project Info ── */} -
- {/* Status badge */} -
- - COMPLETED -
- - {/* Title */} -

- {mockProject.title} -

- - {/* Period */} -
- - {mockProject.period} -
- - {/* Project Scope */} -
-

- Project Scope -

-

- {mockProject.scope} -

-
- - {/* Stats: Total & Eligible */} -
-
-

{mockProject.totalInterns}

-

Total Interns

-
-
-

{mockProject.eligibleInterns}

-

Eligible

-
-
-
- - {/* ── RIGHT: Intern Roster ── */} -
- - {/* Roster Header */} -
-
-

Intern Roster

-

- Confirm completion status before final certificate generation. -

-
- {/* Select All button */} - -
- - {/* Column Headers */} -
-
- Intern Name - Task Progress - Status - Action -
- - {/* Rows */} -
- {paginated.map((intern) => { - const isChecked = selected.has(intern.id); - const isIncomplete = intern.status === "INCOMPLETE"; - return ( -
- {/* Checkbox */} - - - {/* Name + role */} -
- -
-

{intern.name}

-

{intern.role}

-
-
- - {/* Task Progress */} -
- -
- - {/* Status */} -
- -
- - {/* Action */} -
- {isIncomplete ? ( - - ) : ( - - )} -
-
- ); - })} -
- - {/* Pagination Footer */} -
-

- Showing {(page - 1) * PER_PAGE + 1} to {Math.min(page * PER_PAGE, mockInterns.length)} of {TOTAL_ENTRIES.toLocaleString()} entries -

-
- - {Array.from({ length: totalPages }, (_, i) => i + 1).map((n) => ( - - ))} - -
-
-
-
- - {/* ── Bottom Action Bar ── */} -
- - -
-
- ); -} \ No newline at end of file diff --git a/src/Pages/internify/mentor/CertificateForm.tsx b/src/Pages/internify/mentor/CertificateForm.tsx new file mode 100644 index 0000000..32138b4 --- /dev/null +++ b/src/Pages/internify/mentor/CertificateForm.tsx @@ -0,0 +1,188 @@ +import { useRef, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { CalendarDays, UploadCloud, CheckCircle2, XCircle, Loader2 } from "lucide-react"; +import { useUploadCertificateTemplate } from "../../../hooks/useSertificates"; +import { useProjectDetail } from "../../../hooks/useProjects"; +import EligibleInternTable from "./components/EligibleInternTable"; +import { useProjectInternProgress } from "../../../hooks/useInternProgress" + +export default function CertificateDetail() { + const navigate = useNavigate(); + const { slug } = useParams<{ slug: string }>(); + const { project, loading, error } = useProjectDetail(slug ?? ""); + const { upload, loading: uploading, error: uploadError } = useUploadCertificateTemplate(); + const { data: internProgress, loading: progressLoading } = useProjectInternProgress(slug ?? ""); + const eligibleCount = internProgress.filter(i => i.is_eligible).length; + + const fileInputRef = useRef(null); + const [selectedFile, setSelectedFile] = useState(null); + const [uploadSuccess, setUploadSuccess] = useState(false); + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + setSelectedFile(file); + setUploadSuccess(false); + }; + + const handleUpload = async () => { + if (!selectedFile || !project) return; + const result = await upload(project.id, selectedFile); + if (result) { + setUploadSuccess(true); + setSelectedFile(null); + if (fileInputRef.current) fileInputRef.current.value = ""; + } + }; + + if (loading) { + return ( +
+ + Memuat data project... +
+ ); + } + + if (error || !project) { + return ( +
+ +

{error ?? "Project tidak ditemukan."}

+ +
+ ); + } + + return ( +
+
+

Certificates

+

+ Administrate, issue, and track professional certifications for all interns. +

+
+
+ {/* ── Left Column ── */} +
+

+ {project.project_name} +

+
+ + {new Date(project.start_date).toLocaleDateString("en-US", { month: "short", year: "numeric" })} + {" — "} + {new Date(project.end_date).toLocaleDateString("en-US", { month: "short", year: "numeric" })} +
+ + {/* ── Upload Template ── */} +
+ + + {/* Drop Zone */} +
fileInputRef.current?.click()} + > +
+ +
+ {selectedFile ? ( + <> +

+ {selectedFile.name} +

+

+ {(selectedFile.size / 1024 / 1024).toFixed(2)} MB +

+ + ) : ( + <> +

Upload Certificate Image

+

PNG, JPG up to 5MB

+ + )} +
+ + + + {/* Feedback */} + {uploadSuccess && ( +
+ + Template berhasil diupload! +
+ )} + {uploadError && ( +
+ + {uploadError} +
+ )} + + {/* Upload Button */} + {selectedFile && ( + + )} +
+ + {/* ── Project Scope ── */} +
+

+ Project Scope +

+

{project.description}

+
+ + {/* ── Stats ── */} +
+
+

{project.total_members}

+

Total Interns

+
+
+

{eligibleCount}

+

Eligible

+
+
+
+ + {/* ── Right Column ── */} + +
+
+ ); +} \ No newline at end of file diff --git a/src/Pages/internify/mentor/CertificateListPage.tsx b/src/Pages/internify/mentor/CertificateListPage.tsx index 54420bd..4bd03ea 100644 --- a/src/Pages/internify/mentor/CertificateListPage.tsx +++ b/src/Pages/internify/mentor/CertificateListPage.tsx @@ -1,127 +1,18 @@ -// src/pages/mentor/CertificateList.tsx +// src/pages/mentor/CertificateListPage.tsx import { useNavigate } from "react-router-dom"; -import {CalendarDays} from "lucide-react"; - -// ─── Types ─────────────────────────────────────────────────────────────────── -type CertStatus = "COMPLETED" | "PENDING CERTS"; - -interface Project { - id: number; - title: string; - period: string; - internCount: number; - status: CertStatus; -} - -// ─── Mock Data ──────────────────────────────────────────────────────────────── -const mockProjects: Project[] = [ - { - id: 1, - title: "Autonomous Navigation Systems", - period: "Jan 2024 - Apr 2024", - internCount: 14, - status: "COMPLETED", - }, - { - id: 2, - title: "Full-Stack Fintech Solution", - period: "Feb 2024 - June 2024", - internCount: 8, - status: "PENDING CERTS", - }, - { - id: 3, - title: "User Experience Audit 2024", - period: "Mar 2024 - May 2024", - internCount: 5, - status: "PENDING CERTS", - }, - { - id: 4, - title: "Edge Computing Framework", - period: "Dec 2023 - Mar 2024", - internCount: 21, - status: "COMPLETED", - }, -]; - -// ─── Sub Components ─────────────────────────────────────────────────────────── - -/** Badge status: COMPLETED (hijau) vs PENDING CERTS (kuning) */ -function StatusBadge({ status }: { status: CertStatus }) { - const isCompleted = status === "COMPLETED"; - return ( - - {status} - - ); -} - -/** Satu card project */ -function ProjectCard({ - project, - onClick, -}: { - project: Project; - onClick: () => void; -}) { - return ( -
- {/* Top: icon + status */} -
- -
- - {/* Title + period */} -
-

- {project.title} -

-
- - {project.period} -
-
- -
- - {/* Intern count + avatars */} -
-
-

Interns

-

- {project.internCount} {project.internCount === 1 ? "intern" : "Interns"} -

-
-
-
- ); -} - +import CertificateProjectCard from "./components/CertificateProjectCard"; +import { useProjects } from "../../../hooks/useProjects"; // ─── Main Page ──────────────────────────────────────────────────────────────── export default function CertificateList() { const navigate = useNavigate(); + const { projects, loading, error } = useProjects(); - const handleCardClick = (id: number) => { - navigate(`/mentor/certificates/${id}`); + const handleCardClick = (slug: string) => { + navigate(`/mentor/certificates/${slug}`); }; return (
- {/* Breadcrumb */} -
- Certificates › -
- {/* Page Header */}

Certificates

@@ -132,14 +23,14 @@ export default function CertificateList() { {/* Section: List Projects */}
-

List Projects

+

List Projects

- {mockProjects.map((project) => ( - ( + handleCardClick(project.id)} + onClick={() => handleCardClick(project.slug)} /> ))}
diff --git a/src/Pages/internify/mentor/CertificatePage.tsx b/src/Pages/internify/mentor/CertificatePage.tsx index a93cb7e..1423465 100644 --- a/src/Pages/internify/mentor/CertificatePage.tsx +++ b/src/Pages/internify/mentor/CertificatePage.tsx @@ -11,7 +11,6 @@ import { } from 'lucide-react'; import { LuHistory, LuSendHorizontal } from 'react-icons/lu'; -// Mock Data untuk Tabel Riwayat Sertifikat const initialHistoryData = [ { 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" }, @@ -20,7 +19,6 @@ const initialHistoryData = [ ]; const MentorCertificatePage = () => { - // State Form Management const [selectedIntern, setSelectedIntern] = useState("JonathanKristina@gmail.com"); const [selectedProject, setSelectedProject] = useState("Internify Project (Web Development)"); const [issueDate, setIssueDate] = useState("2024-10-24"); @@ -49,17 +47,13 @@ const MentorCertificatePage = () => { return (
- {/* Title */}

Certificates

Administrate, issue, and track professional certifications for all interns.

- - {/* Main Grid Content */}
- {/* Kiri: Form Issue New Certificate (4 Kolom) */}
@@ -67,7 +61,6 @@ const MentorCertificatePage = () => {
- {/* Select Intern */}
- {/* Select Project / Course */}
- - {/* Certificate Image Upload Area */}
diff --git a/src/Pages/internify/mentor/CertificateGenerate.tsx b/src/Pages/internify/mentor/CertificateResult.tsx similarity index 97% rename from src/Pages/internify/mentor/CertificateGenerate.tsx rename to src/Pages/internify/mentor/CertificateResult.tsx index 1769de3..4398e81 100644 --- a/src/Pages/internify/mentor/CertificateGenerate.tsx +++ b/src/Pages/internify/mentor/CertificateResult.tsx @@ -1,6 +1,5 @@ // src/pages/mentor/CertificateResult.tsx import { useState } from "react"; -import { useNavigate } from "react-router-dom"; import { CheckCircle2, FolderArchive, @@ -55,7 +54,6 @@ function Avatar({ initials }: { initials: string }) { // ─── Main Page ──────────────────────────────────────────────────────────────── export default function CertificateResult() { - const navigate = useNavigate(); const [search, setSearch] = useState(""); const [page, setPage] = useState(1); @@ -75,12 +73,6 @@ export default function CertificateResult() { return (
- {/* Breadcrumb */} -
navigate("/mentor/certificates/all")}> - Certificates › -
- {/* Page Header */}

Certificates

diff --git a/src/Pages/internify/mentor/HomeMentorPage.tsx b/src/Pages/internify/mentor/HomeMentorPage.tsx index 7e17b73..4d0b6a3 100644 --- a/src/Pages/internify/mentor/HomeMentorPage.tsx +++ b/src/Pages/internify/mentor/HomeMentorPage.tsx @@ -8,7 +8,7 @@ const HomeMentorPage = () => {
-
+
diff --git a/src/Pages/internify/mentor/InternsPage.tsx b/src/Pages/internify/mentor/InternsPage.tsx index b80fbd8..59544a1 100644 --- a/src/Pages/internify/mentor/InternsPage.tsx +++ b/src/Pages/internify/mentor/InternsPage.tsx @@ -1,32 +1,55 @@ import { useState, useMemo } from "react"; import { Search, SlidersHorizontal, Plus, Trash2 } from "lucide-react"; -import { mockInternsData } from "../../../lib/mockData"; +import { useAllInterns, useRemoveMember, useProjects } from "../../../hooks/useProjects"; import AddInternsModal from "./components/AddInternsToProjects"; export default function InternsManagement() { - // State Filter & Search const [searchEmail, setSearchEmail] = useState(""); const [selectedProject, setSelectedProject] = useState("All Projects"); const [isModalOpen, setIsModalOpen] = useState(false); - // Handler Aksi Kursor/Tombol - const handleAddAssignment = (id: number) => { - alert(`Assign/Add action clicked for intern ID: ${id}`); - }; - const handleDeleteIntern = (id: number) => { - alert(`Deleted intern ID: ${id}`); - }; + const { interns, loading, error, refetch } = useAllInterns(); + const { remove, loading: removing } = useRemoveMember(); + const { projects: activeProjects } = useProjects("active"); - // Logic Filtering Data menggunakan useMemo + // Filter data berdasarkan email dan nama project const filteredInterns = useMemo(() => { - return mockInternsData.filter((intern) => { + return interns.filter((intern) => { const matchesEmail = intern.email.toLowerCase().includes(searchEmail.toLowerCase()); const matchesProject = selectedProject === "All Projects" || - intern.projectName.toLowerCase() === selectedProject.toLowerCase(); + (intern.projectName || "Unassigned").toLowerCase() === selectedProject.toLowerCase(); return matchesEmail && matchesProject; }); - }, [searchEmail, selectedProject]); + }, [interns, searchEmail, selectedProject]); + + // Hitung statistik dari data asli + const totalInterns = interns.length; + // Active assignments = total member dari semua project yang statusnya active + const activeAssignments = activeProjects.length; + + // Handler hapus intern dari project + // Catatan: useRemoveMember memerlukan id_project & id_user. + // Karena InternDetail tidak menyimpan id_project, aksi ini saat ini hanya placeholder. + // Untuk implementasi penuh, perlu endpoint khusus atau data id_project dari InternDetail. + const handleDeleteIntern = async (internId: number) => { + const confirmed = window.confirm("Yakin ingin menghapus intern ini dari project?"); + if (!confirmed) return; + alert(`Remove intern ID ${internId} — perlu id_project dari backend untuk dilanjutkan.`); + // Contoh penggunaan jika id_project tersedia: + // const res = await remove({ id_project: intern.id_project, id_user: internId }); + // if (res !== null) refetch(); + }; + + if (loading) return ( +
+ {[...Array(5)].map((_, i) => ( +
+ ))} +
+ ); + + if (error) return

{error}

; return (
@@ -64,8 +87,6 @@ export default function InternsManagement() { className="w-full pl-3 pr-8 py-2 bg-gray-100 border border-transparent rounded-lg text-sm text-gray-700 font-medium appearance-none focus:outline-none focus:bg-white focus:border-gray-200" > - -
@@ -81,17 +102,21 @@ export default function InternsManagement() {
- {/* Right Info Box Stats */} + {/* Right Info Box Stats — data real dari API */}
- 124 + + {totalInterns} + TOTAL INTERNS
- 92 + + {activeAssignments} + ACTIVE ASSIGNMENTS @@ -100,7 +125,10 @@ export default function InternsManagement() {
- @@ -120,7 +148,7 @@ export default function InternsManagement() {
{filteredInterns.length > 0 ? ( filteredInterns.map((intern, index) => { - const isUnassigned = intern.projectName === "Unassigned"; + const isUnassigned = !intern.projectName || intern.projectName === "Unassigned"; return (
@@ -147,7 +175,7 @@ export default function InternsManagement() { {/* Column 2: Current Project Title & Role Accent */}
- {intern.projectName} + {intern.projectName || "Unassigned"}

{intern.role} @@ -156,15 +184,20 @@ export default function InternsManagement() { {/* Column 3: Action Buttons Block Right aligned */}

+ {/* Tombol Add: membuka modal assign member */} + {/* Tombol Delete: remove intern dari project */} @@ -183,7 +216,7 @@ export default function InternsManagement() { {/* Red Table Pagination Bar Footer */}
- Showing {filteredInterns.length} of 124 interns + Showing {filteredInterns.length} of {totalInterns} interns
@@ -196,10 +229,14 @@ export default function InternsManagement() {
+ {isModalOpen && setIsModalOpen(false)} + onClose={() => { + setIsModalOpen(false); + refetch(); // Refresh data setelah modal assign ditutup + }} /> }
diff --git a/src/Pages/internify/mentor/MentorProjectDetailPage.tsx b/src/Pages/internify/mentor/MentorProjectDetailPage.tsx index bedd9b9..3762fde 100644 --- a/src/Pages/internify/mentor/MentorProjectDetailPage.tsx +++ b/src/Pages/internify/mentor/MentorProjectDetailPage.tsx @@ -1,33 +1,41 @@ import { useParams, useNavigate } from "react-router-dom"; import { useState } from "react"; -import { mockProjects } from "../../../lib/mockProjects"; -import { Calendar, Edit, SlidersHorizontal, Search } from "lucide-react"; +import { Calendar, Edit } from "lucide-react"; import EditSubmissionModal from "./components/EditSubmissionTask"; - +import { useTaskSubmissions } from "../../../hooks/useTasks"; const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-'); export default function MentorProjectsDetailPage() { const navigate = useNavigate(); const { slug, taskSlug } = useParams<{ slug: string; taskSlug: string }>(); - const targetedProject = mockProjects.find((p) => toSlug(p.name) === slug); - - if (!targetedProject) { - return
Project tidak ditemukan.
; - } - - const targetedTask = targetedProject.tasks.find((t) => toSlug(t.title) === taskSlug); - if (!targetedTask) { - return
Tugas tidak ditemukan.
; - } - + const { task, loading, error } = useTaskSubmissions(taskSlug!, slug); const [openModal, setOpenModal] = useState(false); + if (loading) return ( +
+ {[...Array(3)].map((_, i) => ( +
+ ))} +
+ ); + if (error) return

{error}

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

{targetedTask.title}

+

{task.title}

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

Intern Submissions

- {targetedTask.submissions?.length || 0} total + {task.submissions?.length || 0} total
-
- - -
{/* Isi Tabel */} @@ -87,37 +90,37 @@ export default function MentorProjectsDetailPage() { - {targetedTask.submissions?.map((sub, idx) => { - // Cari relasi profil lengkap intern lewat email - const internProfile = targetedProject.interns.find(i => i.email === sub.internEmail); - if (!internProfile) return null; - + {task.submissions.map((sub, idx) => { return ( - {/* Kolom Nama & Profil */} -
- {internProfile.name} + {sub.profile_picture + ? + :
+ } + {sub.full_name} {/* Kolom Tanggal Kumpul */} - {sub.submittedAt || "-"} + {sub.submitted_at + ? new Date(sub.submitted_at).toLocaleDateString("id-ID") + : "-"} {/* Kolom Badge Status */} - {sub.status === "Done" && ( + {sub.display_status === "done" && ( Done )} - {sub.status === "Pending" && ( + {sub.display_status === "pending" && ( Pending )} - {sub.status === "Overdue" && ( + {sub.display_status === "overdue" && ( Overdue @@ -130,8 +133,8 @@ export default function MentorProjectsDetailPage() { className="text-sm font-bold text-red-800 hover:text-red-900 transition-colors inline-flex items-center gap-1" onClick={() => navigate( - `/mentor/projects/${toSlug(targetedProject.name)}/${toSlug(targetedTask.title)}/${sub.internEmail.split("@")[0]}`, - { state: { status: sub.status === "Done" ? "submitted" : sub.status === "Overdue" ? "overdue" : "pending" } } + `/mentor/projects/${slug}/${taskSlug}/${toSlug(sub.full_name)}`, + { state: { status: sub.display_status === "done" ? "submitted" : sub.display_status === "overdue" ? "overdue" : "pending" } } ) } > @@ -145,8 +148,6 @@ export default function MentorProjectsDetailPage() {
- - {/* EditSubmissionModal sekarang berada di dalam induk div utama */} {openModal && ( (null); const [photoPreview, setPhotoPreview] = useState(null); const fileInputRef = useRef(null); + // Sync form data ketika user sudah di-fetch + useEffect(() => { + if (user) { + setFormData({ + fullName: user.full_name || "", + email: user.email || "", + bio: user.professional_bio || "", + }); + if (user.profile_picture) { + setPhotoPreview(user.profile_picture); + } + } + }, [user]); + const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); }; - const handleSaveChanges = (e: React.FormEvent) => { - e.preventDefault(); - alert("Changes saved successfully!"); - console.log("Saved Data:", formData); - }; - const handlePhotoUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { + setPhotoFile(file); const reader = new FileReader(); reader.onloadend = () => setPhotoPreview(reader.result as string); reader.readAsDataURL(file); @@ -32,12 +46,35 @@ export default function MentorSettingsPage() { }; const handleRemovePhoto = () => { + setPhotoFile(null); setPhotoPreview(null); if (fileInputRef.current) fileInputRef.current.value = ""; }; + const handleSaveChanges = async (e: React.FormEvent) => { + e.preventDefault(); + await save({ + full_name: formData.fullName, + email: formData.email, + professional_bio: formData.bio, + ...(photoFile ? { profile_picture: photoFile } : {}), + }); + // Reset file state setelah berhasil save + if (!errorMsg) { + setPhotoFile(null); + } + }; + const initialLetter = formData.fullName ? formData.fullName.charAt(0).toUpperCase() : "U"; + if (userLoading) { + return ( +
+ Loading profile... +
+ ); + } + return (
{/* Header Title */} @@ -47,6 +84,7 @@ export default function MentorSettingsPage() { Manage and edit your profile and account information.

+ {/* White Card Container */}
@@ -91,6 +129,11 @@ export default function MentorSettingsPage() {

JPG, GIF or PNG. Recommended size 800x800px. Max size of 800K.

+ {photoFile && ( +

+ 📎 {photoFile.name} +

+ )}
+ {/* Feedback Messages */} + {successMsg && ( +

+ {successMsg} +

+ )} + {errorMsg && ( +

+ {errorMsg} +

+ )} + {/* Footer Action */}
diff --git a/src/Pages/internify/mentor/ProjectForumTab.tsx b/src/Pages/internify/mentor/ProjectForumTab.tsx index ba73be3..90ac57b 100644 --- a/src/Pages/internify/mentor/ProjectForumTab.tsx +++ b/src/Pages/internify/mentor/ProjectForumTab.tsx @@ -1,27 +1,31 @@ -import type { Project } from '../../../lib/mockProjects'; +import { useParams } from 'react-router-dom'; import ProjectTimeline from '../intern/components/ProjectTimeline'; import HeroProject from '../intern/components/Hero-project'; import MentorTaskCard from './components/MentorTaskCard'; +import { useProjectTasks } from '../../../hooks/useTasks'; +import type { Project } from '../../../types/project.types'; export default function MentorForumTab({ project }: { project: Project }) { + const { slug } = useParams<{ slug: string }>(); + const { tasks, loading, error } = useProjectTasks(slug ?? ""); return (
{/* Hero banner */} - + {/* Timeline + Task list */} -
-
- +
+
+
- -
- {project.tasks.map((task, idx) => ( - - ))} +
+ {tasks.length === 0 ? ( +

Belum ada task.

+ ) : ( + tasks.map((task) => ( + + )) + )}
diff --git a/src/Pages/internify/mentor/ProjectsInternTab.tsx b/src/Pages/internify/mentor/ProjectsInternTab.tsx index 6d1a33c..006cd8e 100644 --- a/src/Pages/internify/mentor/ProjectsInternTab.tsx +++ b/src/Pages/internify/mentor/ProjectsInternTab.tsx @@ -1,10 +1,22 @@ import { useState } from 'react'; -import type { Project, Intern } from '../../../lib/mockProjects'; import { Plus } from 'lucide-react'; import ManageInternsModal from './components/AddInternsDialog'; +import type { ProjectDetail, ProjectMember } from '../../../types/project.types'; +import { useRemoveMember } from '../../../hooks/useProjects'; -export default function InternsTab({ project }: { project: Project }) { +export default function InternsTab({ project }: { project: ProjectDetail }) { 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 (
@@ -15,8 +27,17 @@ export default function InternsTab({ project }: { project: Project }) {
+ {project.admin.profile_picture ? ( + {project.admin.full_name} + ) : ( +
+ )} - {project.mentor} + {project.admin.full_name}
@@ -37,22 +58,26 @@ export default function InternsTab({ project }: { project: Project }) {
- {project.interns.map((intern: Intern, idx: number) => ( + {project.members.map((member: ProjectMember, idx) => (
- {intern.name} + {member.email}
- {intern.role} + {member.position}
-
@@ -61,7 +86,9 @@ export default function InternsTab({ project }: { project: Project }) {
{isModalOpen && setIsModalOpen(false)} + onClose={() => setIsModalOpen(false)} + projectId={project.id} + initialMembers={project.members} />}
); diff --git a/src/Pages/internify/mentor/ProjectsPage.tsx b/src/Pages/internify/mentor/ProjectsPage.tsx index aeef2f9..442fe44 100644 --- a/src/Pages/internify/mentor/ProjectsPage.tsx +++ b/src/Pages/internify/mentor/ProjectsPage.tsx @@ -1,17 +1,18 @@ import { useState } from 'react'; import { useParams } from 'react-router-dom'; -import { mockProjects } from '../../../lib/mockProjects'; +import { useProjectDetail } from '../../../hooks/useProjects'; import MentorForumTab from './ProjectForumTab'; import InternsTab from './ProjectsInternTab'; import TaskTab from './TaskTab'; -const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-'); - export default function ProjectDetailPage() { const { slug } = useParams<{ slug: string }>(); const [activeTab, setActiveTab] = useState<'forum' | 'participants' | 'Task'>('forum'); + const { project, loading, error } = useProjectDetail(slug ?? ""); - const project = mockProjects.find((p) => toSlug(p.name) === slug); + if (loading) return

Memuat project...

; + if (error) return

{error}

; + if (!project) return

Project tidak ditemukan.

if (!project) { return ( diff --git a/src/Pages/internify/mentor/TaskTab.tsx b/src/Pages/internify/mentor/TaskTab.tsx index a17c6a0..2c6d73b 100644 --- a/src/Pages/internify/mentor/TaskTab.tsx +++ b/src/Pages/internify/mentor/TaskTab.tsx @@ -1,28 +1,36 @@ -import type { Project, Task } from '../../../lib/mockData'; +import { useProjectTasks, useCreateTask, useDeleteTask } from '../../../hooks/useTasks'; +import type { ProjectDetail } from '../../../types/project.types'; +import type { ProjectTask } from '../../../types/task.types'; import { ClipboardList, Pencil, Trash2 } from 'lucide-react'; import { useState } from 'react'; import CreateTaskModal from './components/CreateTaskDialog'; import EditTaskModal from './components/EditTaskDialog'; -export default function TaskTab({ project }: { project: Project }) { - +export default function TaskTab({ project }: { project: ProjectDetail }) { + const { tasks, loading, error, refetch } = useProjectTasks(String(project.id)); + const { remove } = useDeleteTask(); const [isModalOpen, setIsModalOpen] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false); + const [selectedTask, setSelectedTask] = useState(null); - const handleEditTask = (taskTitle: string) => { - console.log('Edit task:', taskTitle); + const handleDeleteTask = async (taskId: number) => { + const ok = await remove(taskId, String(project.id)); + if (ok) refetch(); }; - const handleDeleteTask = (taskTitle: string) => { - console.log('Delete task:', taskTitle); - }; + if (loading) return ( +
+ {[...Array(3)].map((_, i) => ( +
+ ))} +
+ ); + if (error) return

{error}

; return (
- {/* Header Section */}

Tasks

-
-
- {/* Task List Section */}
- {project.tasks && project.tasks.length > 0 ? ( - project.tasks.map((task: Task, idx: number) => ( + {tasks.length > 0 ? ( + tasks.map((task, idx) => (
- {/* Task Title & Icon */}
- - {task.title} - + {task.title}
- - {/* Action Buttons (Edit & Delete) */}
@@ -75,18 +75,23 @@ export default function TaskTab({ project }: { project: Project }) {
)}
- {isModalOpen && + + {isModalOpen && ( setIsModalOpen(false)} + projectId={String(project.id)} + onSuccess={() => { setIsModalOpen(false); refetch(); }} /> - } - {editModalOpen && + )} + {editModalOpen && selectedTask && ( setEditModalOpen(false)} + onClose={() => { setEditModalOpen(false); setSelectedTask(null); }} + task={selectedTask} + onSuccess={() => { setEditModalOpen(false); setSelectedTask(null); refetch(); }} /> - } + )}
); } \ No newline at end of file diff --git a/src/Pages/internify/mentor/ViewInternSubmission.tsx b/src/Pages/internify/mentor/ViewInternSubmission.tsx index 587dd1c..a68b5d3 100644 --- a/src/Pages/internify/mentor/ViewInternSubmission.tsx +++ b/src/Pages/internify/mentor/ViewInternSubmission.tsx @@ -1,144 +1,90 @@ import { useParams, useLocation } from "react-router-dom"; -import { FileText, Download, Clock, CheckCircle2, AlertCircle, MinusCircle } from "lucide-react"; -import { mockProjects } from "../../../lib/mockProjects"; - -const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, "-"); - -type SubmissionStatus = "submitted" | "pending" | "overdue"; - -// ─── status badge ─────────────────────────────────────────────────────────── - -function StatusBadge({ status }: { status: SubmissionStatus }) { - if (status === "submitted") - return ( - - - Submitted for grading - - ); - if (status === "overdue") - return ( - - - Overdue - - ); - return ( - - - Not submitted - - ); -} - -// ─── status table ──────────────────────────────────────────────────────────── - -function StatusTable({ status }: { status: SubmissionStatus }) { - const rows: { label: string; value: React.ReactNode }[] = [ - { - label: "Submission status", - value: , - }, - { - label: "Grading status", - value: -, - }, - { - label: "Time Remaining", - value: - status === "overdue" ? ( - Overdue - ) : ( - - - ), - }, - { - label: "File submission", - value: - status === "submitted" ? ( -
- - - -
- ) : ( - - - ), - }, - ]; - - return ( -
-
- {rows.map((row) => ( -
-
- {row.label} -
-
{row.value}
-
- ))} -
-
- ); -} - -// ─── main page ─────────────────────────────────────────────────────────────── +import { Download, Clock, AlertCircle } from "lucide-react"; +import { useTaskSubmissions } from "../../../hooks/useTasks"; +import type { DisplayStatus } from "../../../types/task.types"; +import { StatusTable } from "./components/StatusSubmissionTable"; export default function ViewInternSubmission() { - const { slug, taskSlug, internName } = useParams<{ slug: string; taskSlug: string; internName: string }>(); - const location = useLocation(); - const status: SubmissionStatus = location.state?.status ?? "pending"; + const { slug, taskSlug, nameIntern } = useParams<{ + slug: string; + taskSlug: string; + nameIntern: string; + }>(); + const location = useLocation(); + const status: DisplayStatus = location.state?.status ?? "pending"; - const project = mockProjects.find((p) => toSlug(p.name) === slug); - const task = project?.tasks.find((t) => toSlug(t.title) === taskSlug); - - if (!task) { - return ( -
- -

Task not found.

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

- {task.title} -

-

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

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

Task Description

-

- {task.description} -

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

Submission Detail

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

{error}

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

Submission not found.

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

+ {task.title} +

+

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

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

Task Description

+

+ {task.description} +

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

Submission Detail

+ {internSubmission.submission?.file_path && ( + + + Download + + )} +
+ +
+
+ ); } \ No newline at end of file diff --git a/src/Pages/internify/mentor/components/AddInternsDialog.tsx b/src/Pages/internify/mentor/components/AddInternsDialog.tsx index e7541f9..0b48c78 100644 --- a/src/Pages/internify/mentor/components/AddInternsDialog.tsx +++ b/src/Pages/internify/mentor/components/AddInternsDialog.tsx @@ -1,28 +1,34 @@ +// ManageInternsModal.tsx import { useRef, useEffect, useState } from "react"; -import { X, Search, Plus, Trash2 } from "lucide-react"; +import { Search, Plus, Trash2 } from "lucide-react"; +import { useMahasiswa } from "../../../../hooks/useInterns"; +import { useAssignMember } from '../../../../hooks/useProjects'; +import type { ProjectMember } from '../../../../types/project.types'; +import type { Mahasiswa } from '../../../../types/project.types'; interface ManageInternsModalProps { isOpen: boolean; onClose: () => void; + projectId: number; + initialMembers: ProjectMember[]; } -export default function ManageInternsModal({ isOpen, onClose }: ManageInternsModalProps) { +export default function ManageInternsModal({ isOpen, onClose, projectId, initialMembers }: ManageInternsModalProps) { const dialogRef = useRef(null); - const [searchQuery, setSearchQuery] = useState("Jonathankristin@gmail.com"); + const [searchQuery, setSearchQuery] = useState(""); + const [invitedList, setInvitedList] = useState(initialMembers); - const [invitedList, setInvitedList] = useState([ - { id: 1, email: "Jonathankristina@gmail.com", initials: "JK" } - ]); + const { mahasiswa, loading: loadingMahasiswa } = useMahasiswa(); + const { assign, loading: assigning } = useAssignMember(); - // Data Rekomendasi/Hasil Pencarian (Simulasi list bawah yang ada ikon plus) - const [searchResult, setSearchResult] = useState([ - { id: 2, email: "Jonathankristin@gmail.com", initials: "JK" } - ]); + // Sync saat modal dibuka + useEffect(() => { + if (isOpen) setInvitedList(initialMembers); + }, [isOpen, initialMembers]); useEffect(() => { const dialog = dialogRef.current; if (!dialog) return; - if (isOpen) { dialog.showModal(); document.body.style.overflow = "hidden"; @@ -30,12 +36,7 @@ export default function ManageInternsModal({ isOpen, onClose }: ManageInternsMod dialog.close(); document.body.style.overflow = "unset"; } - - const handleCancel = (e: Event) => { - e.preventDefault(); - onClose(); - }; - + const handleCancel = (e: Event) => { e.preventDefault(); onClose(); }; dialog.addEventListener("cancel", handleCancel); return () => { dialog.removeEventListener("cancel", handleCancel); @@ -43,38 +44,66 @@ export default function ManageInternsModal({ isOpen, onClose }: ManageInternsMod }; }, [isOpen, onClose]); + // Filter mahasiswa yang belum jadi member & sesuai search + const invitedIds = new Set(invitedList.map(m => m.id)); + const searchResult = mahasiswa.filter(m => + !invitedIds.has(m.id) && + (m.email.toLowerCase().includes(searchQuery.toLowerCase()) || + `${m.nama_depan} ${m.nama_belakang}`.toLowerCase().includes(searchQuery.toLowerCase())) + ); + + const handleAssign = async (intern: Mahasiswa) => { + const result = await assign({ id_project: projectId, id_user: intern.id }); + if (result) { + setInvitedList(prev => [...prev, { + id: intern.id, + id_user: intern.id, + full_name: `${intern.nama_depan} ${intern.nama_belakang}`, + email: intern.email, + avatar: "", + kelompok_peminatan: intern.jurusan, + professional_bio: "", + position: intern.role, + } as ProjectMember]); + } + }; + + const getInitials = (m: Mahasiswa) => + `${m.nama_depan[0]}${m.nama_belakang[0]}`.toUpperCase(); + return ( - +
- {/* Search Input Area */}
setSearchQuery(e.target.value)} - placeholder="Search by email..." + placeholder="Cari nama atau email..." className="w-full pl-4 pr-10 py-3 bg-white border border-gray-200 rounded-xl text-sm placeholder-gray-400 text-gray-700 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium shadow-sm" />
- {/* List Container (Scrollable jika data banyak) */}
- - {/* List 1: Intern Yang Sudah Tergabung / Ditambahkan */} - {invitedList.map((intern) => ( -
+ {/* Sudah jadi member */} + {invitedList.map((member) => ( +
-
- {intern.initials} + {member.avatar ? ( + {member.full_name} + ) : ( +
+ {member.full_name.slice(0, 2).toUpperCase()} +
+ )} +
+ {member.full_name} + {member.email}
- {intern.email}
))} - {/* List 2: Hasil Pencarian / Belum Ditambahkan */} - {searchResult.map((intern) => ( -
-
-
- {intern.initials} + {/* Hasil pencarian — belum jadi member */} + {loadingMahasiswa ? ( +

Memuat...

+ ) : ( + searchResult.map((intern) => ( +
+
+
+ {getInitials(intern)} +
+
+ {intern.nama_depan} {intern.nama_belakang} + {intern.email} +
- {intern.email} +
- -
- ))} + )) + )} + {!loadingMahasiswa && searchQuery && searchResult.length === 0 && ( +

Tidak ada mahasiswa ditemukan.

+ )}
diff --git a/src/Pages/internify/mentor/components/AddInternsToProjects.tsx b/src/Pages/internify/mentor/components/AddInternsToProjects.tsx index 8fd1d62..d67bc50 100644 --- a/src/Pages/internify/mentor/components/AddInternsToProjects.tsx +++ b/src/Pages/internify/mentor/components/AddInternsToProjects.tsx @@ -1,6 +1,7 @@ import { useRef, useEffect, useState } from "react"; import { X } from "lucide-react"; -import { LuUserPlus, LuMail, LuBriefcase, LuFolderOpen } from "react-icons/lu"; +import { LuUserPlus, LuMail, LuFolderOpen } from "react-icons/lu"; +import { useAssignMember, useProjects } from "../../../../hooks/useProjects"; interface AddInternsModalProps { isOpen: boolean; @@ -12,8 +13,11 @@ export default function AddInternsModal({ isOpen, onClose }: AddInternsModalProp // Form States const [email, setEmail] = useState(""); - const [position, setPosition] = useState(""); - const [projectName, setProjectName] = useState(""); + const [selectedProjectId, setSelectedProjectId] = useState(""); + + // Hooks + const { assign, loading: assigning, error: assignError } = useAssignMember(); + const { projects, loading: loadingProjects } = useProjects("active"); useEffect(() => { const dialog = dialogRef.current; @@ -39,11 +43,29 @@ export default function AddInternsModal({ isOpen, onClose }: AddInternsModalProp }; }, [isOpen, onClose]); - const handleSubmit = (e: React.FormEvent) => { + // Reset form saat modal dibuka kembali + useEffect(() => { + if (isOpen) { + setEmail(""); + setSelectedProjectId(""); + } + }, [isOpen]); + + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - const payload = { email, position, projectName }; - console.log("Adding Intern:", payload); - alert("Intern Added Successfully!"); + + // Catatan: useAssignMember memerlukan id_user (number), bukan email. + // Endpoint /project-api/assign-member menerima { id_project: number, id_user: number }. + // Untuk implementasi berbasis email, perlu endpoint tambahan atau lookup user by email. + // Saat ini form ini dikomen karena tidak ada lookup email → id_user di service. + + // Contoh penggunaan jika id_user sudah diketahui: + // const res = await assign({ id_project: Number(selectedProjectId), id_user: userId }); + // if (res !== null) { + // onClose(); + // } + + alert(`Assign intern "${email}" ke project ID ${selectedProjectId} — perlu lookup id_user dari email via backend.`); onClose(); }; @@ -69,7 +91,7 @@ export default function AddInternsModal({ isOpen, onClose }: AddInternsModalProp {/* Form Body */} - + {/* Email Address */}
- {/* Position */} + {/* Project Selection */}
- + @@ -122,33 +145,15 @@ export default function AddInternsModal({ isOpen, onClose }: AddInternsModalProp
- {/* Project Name */} -
- -
- - - - - - - - - -
-
+ {/* Field Position dikomentari — tidak ada dalam payload assign-member */} + {/*
+ + +
*/} + + {assignError && ( +

{assignError}

+ )}
@@ -163,10 +168,11 @@ export default function AddInternsModal({ isOpen, onClose }: AddInternsModalProp
diff --git a/src/Pages/internify/mentor/components/CertificateProjectCard.tsx b/src/Pages/internify/mentor/components/CertificateProjectCard.tsx new file mode 100644 index 0000000..3f216bf --- /dev/null +++ b/src/Pages/internify/mentor/components/CertificateProjectCard.tsx @@ -0,0 +1,75 @@ +import { CalendarDays } from "lucide-react"; +import type { Project } from "../../../../types/project.types"; + +// ─── StatusBadge ───────────────────────────────────────────────────────────── +function StatusBadge({ status }: { status: string }) { + const isCompleted = status === "archived"; + return ( + + {status} + + ); +} + +function formatDateRange(start: string, end: string): string { + const options: Intl.DateTimeFormatOptions = { + day: "numeric", + month: "long", + year: "numeric", + }; + + const startDate = new Date(start).toLocaleDateString("id-ID", options); + const endDate = new Date(end).toLocaleDateString("id-ID", options); + + return `${startDate} – ${endDate}`; +} + +// ─── ProjectCard ────────────────────────────────────────────────────────────── +export default function CertificateProjectCard({ + project, + onClick, +}: { + project: Project; + onClick: () => void; +}) { + return ( +
+ {/* Top: status badge */} +
+ +
+ + {/* Title + period */} +
+

+ {project.project_name} +

+
+ + {formatDateRange(project.start_date, project.end_date)} +
+
+ +
+ + {/* Intern count */} +
+
+

Interns

+

+ {project.total_members} interns +

+
+
+
+ ); +} diff --git a/src/Pages/internify/mentor/components/CreateProjectDialog.tsx b/src/Pages/internify/mentor/components/CreateProjectDialog.tsx index 7287a0c..0b1181d 100644 --- a/src/Pages/internify/mentor/components/CreateProjectDialog.tsx +++ b/src/Pages/internify/mentor/components/CreateProjectDialog.tsx @@ -1,19 +1,8 @@ import { useRef, useEffect, useState } from "react"; -import { X, Calendar } from "lucide-react"; -import { - LuCode, - LuAlbum, - LuCloud, - LuSmartphone, - LuSettings, - LuUsers, - LuClipboardList, - LuPenTool, - LuLightbulb, - LuShield, - LuMail, - LuSendHorizontal -} from "react-icons/lu"; +import { X } from "lucide-react"; +import { PROJECT_ICON_MAP, PROJECT_ICON_CODES } from "../../../../lib/ProjectIcons"; +import { LuSendHorizontal, LuMail } from "react-icons/lu"; +import { useCreateProject } from "../../../../hooks/useProjects"; interface CreateProjectModalProps { isOpen: boolean; @@ -22,32 +11,19 @@ interface CreateProjectModalProps { export default function CreateProjectModal({ isOpen, onClose }: CreateProjectModalProps) { const dialogRef = useRef(null); - - // State Form Management - const [selectedIcon, setSelectedIcon] = useState("code"); + const [selectedIcon, setSelectedIcon] = useState(PROJECT_ICON_CODES[0]); const [projectName, setProjectName] = useState(""); const [projectDescription, setProjectDescription] = useState(""); const [emailInput, setEmailInput] = useState(""); - const [invitedEmails, setInvitedEmails] = useState([ - "alex.dev@gmail.com", - "sarah.engineer@gmail.com" - ]); + const [invitedEmails, setInvitedEmails] = useState([]); const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); + const { create, loading, error } = useCreateProject(); - // Daftar Icon Pilihan di baris atas - const projectIcons = [ - { id: "code", icon: LuCode }, - { id: "chart", icon: LuAlbum }, - { id: "cloud", icon: LuCloud }, - { id: "mobile", icon: LuSmartphone }, - { id: "settings", icon: LuSettings }, - { id: "users", icon: LuUsers }, - { id: "task", icon: LuClipboardList }, - { id: "design", icon: LuPenTool }, - { id: "idea", icon: LuLightbulb }, - { id: "secure", icon: LuShield }, - ]; + const projectIcons = PROJECT_ICON_CODES.map((id) => ({ + id, + icon: PROJECT_ICON_MAP[id], + })); useEffect(() => { const dialog = dialogRef.current; @@ -73,7 +49,6 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod }; }, [isOpen, onClose]); - // Fungsi menambah email undangan baru const handleAddEmail = (e: React.FormEvent) => { e.preventDefault(); if (emailInput.trim() && !invitedEmails.includes(emailInput.trim())) { @@ -82,25 +57,29 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod } }; - // Fungsi menghapus email undangan const handleRemoveEmail = (emailToRemove: string) => { setInvitedEmails(invitedEmails.filter(email => email !== emailToRemove)); }; // Handler Submit Utama - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - const payload = { - icon: selectedIcon, - name: projectName, + const payload: any = { + project_icon: selectedIcon, + project_name: projectName, description: projectDescription, - emails: invitedEmails, - startDate, - endDate + start_date: startDate, + end_date: endDate, }; - console.log("Launching Project:", payload); - alert("Project Launched Successfully!"); - onClose(); + + if (invitedEmails.length > 0) { + payload.member_emails = invitedEmails; + } + + const result = await create(payload); + if (result) { + onClose(); + } }; return ( @@ -125,8 +104,7 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
{/* Body Form Modal */} - - + {/* Section: Select Project Icon */}