From ad26a53f211f763e5b00284ee762a3c6dbc595e3 Mon Sep 17 00:00:00 2001 From: "Kemas M. Aryadary Rasyad" <112590882+aryadary0706@users.noreply.github.com> Date: Sun, 28 Jun 2026 08:12:56 +0700 Subject: [PATCH] feat: update project task card to use new task type and improve navigation fix: update project card to reflect actual tasks done from project data feat: enhance task form components to accept project ID for submissions refactor: clean up certificate detail page by removing unused elements chore: streamline certificate generation page by removing unnecessary navigation refactor: update certificate list page to use dynamic project data feat: improve interns management page with API integration and better state handling feat: add modal for assigning interns to projects with project selection feat: create a new component for displaying project cards in certificate list fix: correct import paths in create project dialog component refactor: enhance project hooks to include task filtering and refetch capabilities fix: update mock certificate data to reflect correct status feat: extend certificate types to include new fields for better tracking feat: add task_done field to project types for improved task management --- src/App.tsx | 6 +- src/Pages/NotFoundPage.tsx | 65 +++++++++ src/Pages/internify/NotificationList.tsx | 3 +- src/Pages/internify/intern/ForumTab.tsx | 6 +- .../internify/intern/ProjectDetailPage.tsx | 2 + .../internify/intern/SertificateList.tsx | 52 +++++-- .../internify/intern/SertificatePage.tsx | 70 ++++++++-- src/Pages/internify/intern/TaskSubmission.tsx | 6 +- .../intern/components/CertificateAvailabe.tsx | 33 ++++- .../intern/components/CertificateHistory.tsx | 77 +++++++---- .../components/CertificateNotAvailable.tsx | 123 ++++++++++++----- .../intern/components/ProjectTaskCard.tsx | 29 ++-- .../intern/components/ProjectsCard.tsx | 2 +- .../intern/components/TaskForm-file.tsx | 5 +- .../intern/components/TaskForm-link.tsx | 5 +- .../internify/mentor/CertificateDetail.tsx | 32 +---- .../internify/mentor/CertificateGenerate.tsx | 8 -- .../internify/mentor/CertificateListPage.tsx | 127 ++---------------- src/Pages/internify/mentor/InternsPage.tsx | 87 ++++++++---- .../components/AddInternsToProjects.tsx | 108 ++++++++------- .../components/CertificateProjectCard.tsx | 62 +++++++++ .../mentor/components/CreateProjectDialog.tsx | 2 +- src/hooks/useProjects.ts | 19 ++- src/lib/mockSertificates.ts | 2 +- src/types/certificate.types.ts | 2 + src/types/project.types.ts | 1 + 26 files changed, 587 insertions(+), 347 deletions(-) create mode 100644 src/Pages/NotFoundPage.tsx create mode 100644 src/Pages/internify/mentor/components/CertificateProjectCard.tsx diff --git a/src/App.tsx b/src/App.tsx index dc7cd2e..7e7bb7a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -50,6 +50,7 @@ import SettingsPage from "./Pages/Internify/mentor/MentorSettingsPage"; import ProtectedRoutes from "./Pages/utils/ProtectedRoute"; import CertificateDetail from "./Pages/Internify/mentor/CertificateDetail"; import CertificateResult from "./Pages/Internify/mentor/CertificateGenerate"; +import NotFoundPage from "./Pages/NotFoundPage"; function App() { return ( @@ -111,7 +112,7 @@ function App() { } /> } /> } /> - } /> + } /> } /> } /> } /> @@ -123,6 +124,9 @@ function App() { + {/* 404 catch-all */} + } /> + ); diff --git a/src/Pages/NotFoundPage.tsx b/src/Pages/NotFoundPage.tsx new file mode 100644 index 0000000..3e3232f --- /dev/null +++ b/src/Pages/NotFoundPage.tsx @@ -0,0 +1,65 @@ +import { useNavigate } from "react-router-dom"; +import { Home, ArrowLeft, Search } from "lucide-react"; + +export default function NotFoundPage() { + const navigate = useNavigate(); + + return ( +
+ {/* Background decorative blobs */} +
+
+ +
+ + {/* 404 Large Text */} +
+ + 404 + +
+ +
+
+ + {/* Title */} +

+ Halaman Tidak Ditemukan +

+ + {/* Description */} +

+ Sepertinya halaman yang kamu cari tidak ada atau telah dipindahkan. + Coba kembali ke halaman sebelumnya atau beranda. +

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

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

+
+
+ ); +} diff --git a/src/Pages/internify/NotificationList.tsx b/src/Pages/internify/NotificationList.tsx index 43425cf..1068c75 100644 --- a/src/Pages/internify/NotificationList.tsx +++ b/src/Pages/internify/NotificationList.tsx @@ -90,7 +90,8 @@ export default function NotificationList() { {/* Actions */} {type === "new-task" && !isOld && (
- + + ) : ( + <> +

Sertifikat Belum Tersedia

+

+ Anda harus menyelesaikan semua tugas proyek sebelum dapat mengunduh sertifikat kelulusan. + Silakan periksa kembali progres tugas Anda di menu Proyek. +

+ + {/* Progress Bar */} +
+

+ Selesaikan {remainingTasks} tugas lagi untuk membuka akses. +

+
+ Progres Proyek + {progress}% +
+
+
+
+ +
+ + )}
); } \ No newline at end of file diff --git a/src/Pages/internify/intern/components/ProjectTaskCard.tsx b/src/Pages/internify/intern/components/ProjectTaskCard.tsx index 299dd57..26a3479 100644 --- a/src/Pages/internify/intern/components/ProjectTaskCard.tsx +++ b/src/Pages/internify/intern/components/ProjectTaskCard.tsx @@ -1,5 +1,5 @@ -import { useNavigate, useParams } from "react-router-dom"; -import type { ProjectTask } from "../../../../types/task.types"; +import { useNavigate } from "react-router-dom"; +import type { InternTaskItem } from "../../../../types/project.types"; const formatDate = (dateStr: string) => new Date(dateStr).toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' }); @@ -7,37 +7,36 @@ const formatDate = (dateStr: string) => const formatTime = (dateStr: string) => new Date(dateStr).toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }); -export default function TaskCard({ task }: { task: ProjectTask }) { +export default function TaskCard({ task }: { task: InternTaskItem }) { const navigate = useNavigate(); - const { slug } = useParams<{ slug: string }>(); return (
navigate(`/intern/projects/${slug}/${task.slug}?type=${task.submission_type}`)} + className="border border-card-outline rounded-xl px-6 py-4 mb-3 bg-white flex items-start justify-between gap-3 cursor-pointer hover:bg-gray-50 transition-colors" + onClick={() => navigate(`/intern/projects/${task.project_slug}/${task.slug}?type=${task.submission_type}`)} >
-
+
Deadline : {formatDate(task.deadline_at)} at {formatTime(task.deadline_at)}
-
+
{task.title}
-
+
{task.description}
-
+
Tipe Submission : {task.submission_type === 'file_upload' ? "File" : "Link"}
-
- + {task.submission_status} +
); diff --git a/src/Pages/internify/intern/components/ProjectsCard.tsx b/src/Pages/internify/intern/components/ProjectsCard.tsx index 37684aa..0176689 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 ( 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/mentor/CertificateDetail.tsx b/src/Pages/internify/mentor/CertificateDetail.tsx index 475cd02..ad4943e 100644 --- a/src/Pages/internify/mentor/CertificateDetail.tsx +++ b/src/Pages/internify/mentor/CertificateDetail.tsx @@ -9,7 +9,6 @@ import { ChevronLeft, ChevronRight, Sparkles, - ArrowLeft, } from "lucide-react"; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -67,7 +66,6 @@ function Avatar({ initials }: { initials: string }) { ); } -// ─── Progress Bar ───────────────────────────────────────────────────────────── function ProgressBar({ value, total }: { value: number; total: number }) { const pct = Math.round((value / total) * 100); const isComplete = value === total; @@ -86,7 +84,6 @@ function ProgressBar({ value, total }: { value: number; total: number }) { ); } -// ─── Status Badge ───────────────────────────────────────────────────────────── function StatusBadge({ status }: { status: InternStatus }) { const isEligible = status === "ELIGIBLE"; return ( @@ -99,7 +96,6 @@ function StatusBadge({ status }: { status: InternStatus }) { ); } -// ─── Main Page ──────────────────────────────────────────────────────────────── export default function CertificateDetail() { const navigate = useNavigate(); @@ -135,26 +131,14 @@ export default function CertificateDetail() { 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 */}
@@ -162,18 +146,15 @@ export default function CertificateDetail() { COMPLETED
- {/* Title */}

{mockProject.title}

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

Project Scope @@ -183,7 +164,6 @@ export default function CertificateDetail() {

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

{mockProject.totalInterns}

@@ -196,10 +176,8 @@ export default function CertificateDetail() {
- {/* ── RIGHT: Intern Roster ── */}
- {/* Roster Header */}

Intern Roster

@@ -207,7 +185,6 @@ export default function CertificateDetail() { Confirm completion status before final certificate generation.

- {/* Select All button */} +
- {/* 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/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..13cc837 --- /dev/null +++ b/src/Pages/internify/mentor/components/CertificateProjectCard.tsx @@ -0,0 +1,62 @@ +import { CalendarDays } from "lucide-react"; +import type { Project } from "../../../../types/project.types"; + +// ─── StatusBadge ───────────────────────────────────────────────────────────── +function StatusBadge({ status }: { status: string }) { + const isCompleted = status === "COMPLETED"; + return ( + + {status} + + ); +} + +// ─── ProjectCard ────────────────────────────────────────────────────────────── +export default function CertificateProjectCard({ + project, + onClick, +}: { + project: Project; + onClick: () => void; +}) { + return ( +
+ {/* Top: status badge */} +
+ +
+ + {/* Title + period */} +
+

+ {project.project_name} +

+
+ + {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 602b0cd..0b1181d 100644 --- a/src/Pages/internify/mentor/components/CreateProjectDialog.tsx +++ b/src/Pages/internify/mentor/components/CreateProjectDialog.tsx @@ -1,6 +1,6 @@ import { useRef, useEffect, useState } from "react"; import { X } from "lucide-react"; -import { PROJECT_ICON_MAP, PROJECT_ICON_CODES } from "../../..//../lib/ProjectIcons"; +import { PROJECT_ICON_MAP, PROJECT_ICON_CODES } from "../../../../lib/ProjectIcons"; import { LuSendHorizontal, LuMail } from "react-icons/lu"; import { useCreateProject } from "../../../../hooks/useProjects"; diff --git a/src/hooks/useProjects.ts b/src/hooks/useProjects.ts index 2cb267c..aa8d4b0 100644 --- a/src/hooks/useProjects.ts +++ b/src/hooks/useProjects.ts @@ -24,6 +24,7 @@ import { getProjectMembers, removeMember } from "../services/ProjectService"; +import { useMemo } from "react"; // GET /project-api/get export const useProjects = (status?: "active" | "completed" | "archived") => { @@ -136,14 +137,17 @@ export const useAllInterns = () => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - useEffect(() => { + const refetch = useCallback(() => { + setLoading(true); getAllInterns() .then(setInterns) .catch(() => setError("Gagal memuat data interns.")) .finally(() => setLoading(false)); }, []); - return { interns, loading, error }; + useEffect(() => { refetch(); }, [refetch]); + + return { interns, loading, error, refetch }; }; // POST /project-api/add @@ -167,6 +171,17 @@ export const useCreateProject = () => { return { create, loading, error }; }; +export const useProjectMyTasks = (projectId: number) => { + const { tasks, loading, error } = useMyTasks(); + + const projectTasks = useMemo(() => { + if (!projectId || !tasks.length) return []; + return tasks.filter(task => task.id_project === projectId); + }, [tasks, projectId]); + + return { tasks: projectTasks, loading, error }; +}; + // PATCH /project-api/update/{id} export const useUpdateProject = () => { const [loading, setLoading] = useState(false); diff --git a/src/lib/mockSertificates.ts b/src/lib/mockSertificates.ts index c4e8529..28fc585 100644 --- a/src/lib/mockSertificates.ts +++ b/src/lib/mockSertificates.ts @@ -24,7 +24,7 @@ export interface CertificateData { } export const certificateData: CertificateData = { - isReceived: true, + isReceived: false, projectProgress: 90, remainingTasks: 0, certificate: { diff --git a/src/types/certificate.types.ts b/src/types/certificate.types.ts index c40e318..4f8bc01 100644 --- a/src/types/certificate.types.ts +++ b/src/types/certificate.types.ts @@ -18,8 +18,10 @@ export interface Certificate { id_user: number; certificate_no: string; issued_at: string; + uuid: string; user: CertificateUser; project: CertificateProject; + certificate_status: string; } export interface CertificateVerifyResult { diff --git a/src/types/project.types.ts b/src/types/project.types.ts index ad8e362..b33911e 100644 --- a/src/types/project.types.ts +++ b/src/types/project.types.ts @@ -24,6 +24,7 @@ export interface Project { status: ProjectStatus; admin: ProjectAdmin; total_members: number; + task_done: number; total_tasks: number; created_at: string; updated_at: string;