From 5dec77862ccbb1a508a9e247f1575b72bf979ff4 Mon Sep 17 00:00:00 2001 From: "Kemas M. Aryadary Rasyad" <112590882+aryadary0706@users.noreply.github.com> Date: Thu, 2 Jul 2026 10:27:28 +0700 Subject: [PATCH] feat: update certificate handling and project management features - Refactored CertificateHistory component to display dynamic certificate images. - Modified CertificateNotAvailable component text for clarity on certificate publication. - Enhanced CertificateForm to include file preview and clear functionality for uploaded images. - Removed obsolete CertificatePage component to streamline mentor interface. - Updated CertificateResult to handle project details and loading states more effectively. - Improved HomeMentorPage structure by adjusting imports for better organization. - Enhanced EligibleInternTable with navigation to results and improved error handling for certificate generation. - Updated HomeMentorProject styles for consistency with design system. - Added utility function to resolve image URLs for certificates. - Introduced new hooks for generating certificates and managing notifications. - Expanded project service functions to include project completion and assignable interns retrieval. - Updated types for certificates and projects to reflect new data structures. - Added user types for better type safety in user management. --- src/App.tsx | 4 +- .../{intern/components => }/HomeCalendar.tsx | 32 +-- src/Pages/internify/intern/HomeInternPage.tsx | 2 +- .../intern/components/CertificateAvailabe.tsx | 5 +- .../intern/components/CertificateHistory.tsx | 3 +- .../components/CertificateNotAvailable.tsx | 5 +- .../internify/mentor/CertificateForm.tsx | 112 ++++++--- .../internify/mentor/CertificatePage.tsx | 232 ------------------ .../internify/mentor/CertificateResult.tsx | 18 +- src/Pages/internify/mentor/HomeMentorPage.tsx | 2 +- .../mentor/components/EligibleInternTable.tsx | 13 +- .../mentor/components/HomeMentorProject.tsx | 5 +- src/Pages/utils/SertificateGenerator.ts | 17 +- src/hooks/useCertificates.ts | 47 ++++ src/hooks/useNotification.ts | 12 +- src/hooks/useProjects.ts | 43 +++- src/hooks/useTasks.ts | 39 ++- src/hooks/useUser.ts | 28 +-- src/services/CertificateService.ts | 5 + src/services/ProjectService.ts | 17 +- src/types/certificate.types.ts | 5 + src/types/project.types.ts | 13 +- src/types/user.types.ts | 17 ++ 23 files changed, 326 insertions(+), 350 deletions(-) rename src/Pages/internify/{intern/components => }/HomeCalendar.tsx (88%) delete mode 100644 src/Pages/internify/mentor/CertificatePage.tsx diff --git a/src/App.tsx b/src/App.tsx index 4b1c47e..42885ef 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -118,8 +118,8 @@ function App() { } /> } /> } /> - } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/src/Pages/internify/intern/components/HomeCalendar.tsx b/src/Pages/internify/HomeCalendar.tsx similarity index 88% rename from src/Pages/internify/intern/components/HomeCalendar.tsx rename to src/Pages/internify/HomeCalendar.tsx index 8f587c6..1368adf 100644 --- a/src/Pages/internify/intern/components/HomeCalendar.tsx +++ b/src/Pages/internify/HomeCalendar.tsx @@ -1,8 +1,6 @@ import { useState, useEffect } from 'react' import { ChevronLeft, ChevronRight } from 'lucide-react' -import { useMyTasks } from '../../../../hooks/useProjects'; -import { useCurrentUser } from '../../../../hooks/useUser'; -import { useAllMentorTasks } from '../../../../hooks/useTasks'; +import { useDeadlines } from '../../hooks/useTasks'; const DAY_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'] const MONTH_NAMES = [ @@ -20,21 +18,7 @@ function getFirstDayOfMonth(year: number, month: number) { export default function HomeCalendar() { const [today, setToday] = useState(new Date()); - const { user } = useCurrentUser(); - - const isIntern = user?.role === "intern"; - const { tasks: internTasks } = useMyTasks(); - const { tasks: mentorTasks } = useAllMentorTasks(!isIntern && user !== null); - - const DEADLINES = isIntern - ? internTasks.map(task => ({ - date: new Date(task.deadline_at), - label: task.title, - })) - : mentorTasks.map(task => ({ - date: new Date(task.deadline_at), - label: `[${task.project_name}] ${task.title}`, - })); + const { deadlines, loading } = useDeadlines(); useEffect(() => { const interval = setInterval(() => { @@ -74,13 +58,13 @@ export default function HomeCalendar() { currentYear === today.getFullYear() const hasDeadline = (day: number) => - DEADLINES.some(d => + deadlines.some(d => d.date.getDate() === day && d.date.getMonth() === currentMonth && d.date.getFullYear() === currentYear ) - const upcomingDeadlines = DEADLINES.filter(d => { + const upcomingDeadlines = deadlines.filter(d => { return ( d.date.getMonth() === currentMonth && d.date.getFullYear() === currentYear @@ -104,6 +88,14 @@ export default function HomeCalendar() { return d === 0 || d === 6 // Sun or Sat } + if (loading) { + return ( +
+

Loading calendar...

+
+ ) + } + return (
{/* Header */} diff --git a/src/Pages/internify/intern/HomeInternPage.tsx b/src/Pages/internify/intern/HomeInternPage.tsx index 8212928..c523941 100644 --- a/src/Pages/internify/intern/HomeInternPage.tsx +++ b/src/Pages/internify/intern/HomeInternPage.tsx @@ -1,6 +1,6 @@ import HomeHeader from "./components/HomeHeader"; import HomeProjectList from "./components/HomeProjectList"; -import HomeCalendar from "./components/HomeCalendar"; +import HomeCalendar from "../HomeCalendar"; import TaskTimeline from "./components/TaskTimeline"; export default function HomeInternPage() { diff --git a/src/Pages/internify/intern/components/CertificateAvailabe.tsx b/src/Pages/internify/intern/components/CertificateAvailabe.tsx index 2df227f..89d4f7c 100644 --- a/src/Pages/internify/intern/components/CertificateAvailabe.tsx +++ b/src/Pages/internify/intern/components/CertificateAvailabe.tsx @@ -4,6 +4,7 @@ import { Download, Link, Printer } from "lucide-react"; import type { ProjectDetail } from "../../../../types/project.types"; import type { Certificate } from "../../../../types/certificate.types"; import { downloadCertificateAsImage, downloadCertificateAsPdf } from "../../../utils/DownloadImagetoDevice"; +import { resolveImageUrl } from "../../../utils/SertificateGenerator"; interface CertificateAvailableProps { project: ProjectDetail | null; @@ -62,7 +63,7 @@ export default function CertificateAvailable({ project, certificate }: Certifica {/* Certificate Card */}
Certificate @@ -83,7 +84,7 @@ export default function CertificateAvailable({ project, certificate }: Certifica label: "Status", value: ( - {certificate.certificate_status.toUpperCase()} + {certificate.certificate_status} ), }, diff --git a/src/Pages/internify/intern/components/CertificateHistory.tsx b/src/Pages/internify/intern/components/CertificateHistory.tsx index 62c5676..34d20d4 100644 --- a/src/Pages/internify/intern/components/CertificateHistory.tsx +++ b/src/Pages/internify/intern/components/CertificateHistory.tsx @@ -1,7 +1,6 @@ import { useNavigate } from "react-router-dom"; import { History, Loader2, AlertCircle } from "lucide-react"; import type { Certificate } from "../../../../types/certificate.types"; -import certificateImg from "../../../../assets/certificate.png"; //Placeholder import { useMyCertificates } from "../../../../hooks/useCertificates"; export default function SertificateHistory() { @@ -43,7 +42,7 @@ export default function SertificateHistory() { >
Certificate diff --git a/src/Pages/internify/intern/components/CertificateNotAvailable.tsx b/src/Pages/internify/intern/components/CertificateNotAvailable.tsx index 6a0190e..dfb346b 100644 --- a/src/Pages/internify/intern/components/CertificateNotAvailable.tsx +++ b/src/Pages/internify/intern/components/CertificateNotAvailable.tsx @@ -40,8 +40,8 @@ export default function CertificateNotAvailable({ <>

Selamat!

- Semua tugas proyek telah selesai. Klik tombol di bawah untuk{" "} - mengklaim sertifikat kelulusan Anda. + Semua tugas proyek telah selesai. Silahkan tunggu Mentor untuk{" "} + Mempublish Sertifikat kelulusan Anda.

{/* Progress Bar — 100% */} @@ -53,7 +53,6 @@ export default function CertificateNotAvailable({
-

Semua tugas telah diselesaikan! Silahkan tunggu untuk mendapatkan sertifikat

)}
- )} -
- {/* ── Project Scope ── */} -
-

- Project Scope -

-

{project.description}

-
- - {/* Stats: Total Intern dalam project dan jumlah intern eligible */} -
-
-

{project.total_members}

-

Total Interns

+ {/* ── Project Scope ── */} +
+

+ Project Scope +

+

{project.description}

-
-

{eligibleCount}

-

Eligible

+ + {/* Stats: Total Intern dalam project dan jumlah intern eligible */} +
+
+

{project.total_members}

+

Total Interns

+
+
+

{eligibleCount}

+

Eligible

+
diff --git a/src/Pages/internify/mentor/CertificatePage.tsx b/src/Pages/internify/mentor/CertificatePage.tsx deleted file mode 100644 index 1423465..0000000 --- a/src/Pages/internify/mentor/CertificatePage.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import React, { useState } from 'react'; -import { - PlusCircle, - UploadCloud, - Eye, - Download, - Trash2, - ChevronLeft, - ChevronRight, - DownloadCloud -} from 'lucide-react'; -import { LuHistory, LuSendHorizontal } from 'react-icons/lu'; - -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" }, - { id: 3, name: "Siti Rahma", email: "siti.rahma@gmail.com", project: "UI/UX Essentials", date: "10 Aug 2024", initials: "SR" }, - { id: 4, name: "David Beckham", email: "david.b@gmail.com", project: "Engineering Intern 101", date: "05 Aug 2024", initials: "DB" }, -]; - -const MentorCertificatePage = () => { - const [selectedIntern, setSelectedIntern] = useState("JonathanKristina@gmail.com"); - const [selectedProject, setSelectedProject] = useState("Internify Project (Web Development)"); - const [issueDate, setIssueDate] = useState("2024-10-24"); - const [expiryDate, setExpiryDate] = useState(""); - - // State Data Tabel - const [historyData, setHistoryData] = useState(initialHistoryData); - - const handleIssueCertificate = (e: React.FormEvent) => { - e.preventDefault(); - const payload = { - intern: selectedIntern, - project: selectedProject, - issueDate, - expiryDate - }; - console.log("Issuing Certificate:", payload); - alert("Certificate Issued Successfully!"); - }; - - const handleDeleteHistory = (id: number) => { - if(confirm("Are you sure you want to delete this certificate log?")) { - setHistoryData(historyData.filter(item => item.id !== id)); - } - }; - - return ( -
-
-

Certificates

-

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

-
-
-
-
- -

Issue New Certificate

-
- -
-
- - -
- -
- - -
-
- -
-
- -
-

Upload Certificate Image

-

PNG, JPG up to 5MB

-
-
- - {/* Date Grid */} -
-
- - setIssueDate(e.target.value)} - className="w-full px-3 py-2 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600" - /> -
-
- - setExpiryDate(e.target.value)} - className="w-full px-3 py-2 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600" - /> -
-
- - {/* Submit Button */} - -
-
- - {/* Kanan: Issued Certificates History (8 Kolom) */} -
-
- {/* Header Table */} -
-
- -

Issued Certificates History

-
- -
- - {/* Responsive Table Wrapper */} -
- - - - - - - - - - - {historyData.map((row) => ( - - {/* Kolom Nama Intern + Avatar Ringkas */} - - {/* Kolom Nama Proyek */} - - {/* Kolom Tanggal Terbit */} - - {/* Kolom Deretan Aksi */} - - - ))} - -
Intern NameProjectIssue DateActions
-
-
- {row.initials} -
-
-

{row.name}

-

{row.email}

-
-
-
-

{row.project}

-
-

{row.date}

-
-
- - - -
-
-
-
- - {/* Bagian Footer Tabel & Paginasi */} -
-

Showing 1 to {historyData.length} of 1,284 entries

- -
- - - - - -
-
- -
- -
-
- ); -} - -export default MentorCertificatePage; \ No newline at end of file diff --git a/src/Pages/internify/mentor/CertificateResult.tsx b/src/Pages/internify/mentor/CertificateResult.tsx index 2cabc93..d287f96 100644 --- a/src/Pages/internify/mentor/CertificateResult.tsx +++ b/src/Pages/internify/mentor/CertificateResult.tsx @@ -10,7 +10,8 @@ import { ChevronLeft, ChevronRight, } from "lucide-react"; -import { useProjectCertificates } from "../../../hooks/useCertificates"; +import { useProjectCertificates } from "../../../hooks/useCertificates"; //Problemnya cuma role "admin" yang bisa +import { useProjectDetail } from "../../../hooks/useProjects"; import type { Certificate } from "../../../types/certificate.types"; import { useParams } from "react-router-dom"; @@ -97,10 +98,17 @@ function CertificateResultSkeleton() { // ─── Main Page ──────────────────────────────────────────────────────────────── export default function CertificateResult() { - const { id } = useParams<{ id: string }>(); + const { slug } = useParams<{ slug: string }>(); const [search, setSearch] = useState(""); const [page, setPage] = useState(1); - const {certificates, loading, error} = useProjectCertificates(Number(id)); + + const { project, loading: projectLoading, error: projectError } = useProjectDetail(slug ?? ""); + const { certificates, loading: certLoading, error: certError } = useProjectCertificates( + project?.id ?? 0 + ); + + const loading = projectLoading || certLoading; + const error = projectError || certError; if (loading) return ; if (error) return
Gagal memuat data sertifikat.
; @@ -262,7 +270,7 @@ export default function CertificateResult() {
{/* Preview Card (placeholder — gambar belum diimplementasi) */} -
+ {/*

Institutional Excellence @@ -271,7 +279,7 @@ export default function CertificateResult() { Certified via Mentor Portal v2.4

-
+
*/}
diff --git a/src/Pages/internify/mentor/HomeMentorPage.tsx b/src/Pages/internify/mentor/HomeMentorPage.tsx index 4d0b6a3..9d7497d 100644 --- a/src/Pages/internify/mentor/HomeMentorPage.tsx +++ b/src/Pages/internify/mentor/HomeMentorPage.tsx @@ -1,5 +1,5 @@ import HomeHeader from '../intern/components/HomeHeader' -import HomeCalendar from '../intern/components/HomeCalendar' +import HomeCalendar from '../HomeCalendar' import HomeMentorProject from './components/HomeMentorProject' const HomeMentorPage = () => { diff --git a/src/Pages/internify/mentor/components/EligibleInternTable.tsx b/src/Pages/internify/mentor/components/EligibleInternTable.tsx index 29a965a..689e5bd 100644 --- a/src/Pages/internify/mentor/components/EligibleInternTable.tsx +++ b/src/Pages/internify/mentor/components/EligibleInternTable.tsx @@ -78,7 +78,8 @@ export default function EligibleInternTable({ }; const handleGenerate = async () => { - if (!project?.template_sertificate) { + // NOTE: DI RESPONSE GAK DIKASIH TAHU DIMANA SIMPAN TEMPLATE SERTIF NYA + if (!project?.certificate_template) { setGenerateError("Template sertifikat belum diupload."); return; } @@ -98,7 +99,7 @@ export default function EligibleInternTable({ await Promise.all( selectedInterns.map(async (intern) => { const blob = await generateCertificate( - project.template_sertificate, + project.certificate_template, intern.full_name ); // Nama file: "Sertifikat - Nama Intern.png" @@ -286,7 +287,12 @@ export default function EligibleInternTable({ )} {/* Bottom Action Bar */} -
+
+ +
); diff --git a/src/Pages/internify/mentor/components/HomeMentorProject.tsx b/src/Pages/internify/mentor/components/HomeMentorProject.tsx index 6c3b823..438d3e9 100644 --- a/src/Pages/internify/mentor/components/HomeMentorProject.tsx +++ b/src/Pages/internify/mentor/components/HomeMentorProject.tsx @@ -1,5 +1,4 @@ import { useNavigate } from 'react-router-dom' -import { mockProjects } from '../../../../lib/mockData' import { useState } from 'react'; import { Plus } from 'lucide-react'; import CreateProjectModal from './CreateProjectDialog'; @@ -20,9 +19,9 @@ const HomeMentorProject = () => {

Projects