From 17e3928d352207e9a78ce5e7dcceaf3e4e645f3e Mon Sep 17 00:00:00 2001 From: "Kemas M. Aryadary Rasyad" <112590882+aryadary0706@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:01:25 +0700 Subject: [PATCH] feat: implement profile settings and comprehensive mentor-intern project management system --- src/App.tsx | 6 +- src/Layout/Header-internify.tsx | 7 +- src/Pages/Dashboard.tsx | 272 +++++++++--------- src/Pages/internify/LoginInternify.tsx | 4 +- src/Pages/internify/SettingsPage.tsx | 17 +- src/Pages/internify/intern/ForumTab.tsx | 6 +- src/Pages/internify/intern/TaskSubmission.tsx | 6 + .../intern/components/HomeHeader.tsx | 2 +- .../intern/components/ProjectTimeline.tsx | 7 +- .../components/SubmissionStatusTable.tsx | 8 +- .../intern/components/TaskForm-file.tsx | 8 +- .../intern/components/TaskForm-link.tsx | 9 +- .../internify/mentor/BaseProjectsPage.tsx | 14 +- .../mentor/MentorProjectDetailPage.tsx | 9 +- .../internify/mentor/ProjectForumTab.tsx | 7 - .../internify/mentor/ProjectsInternTab.tsx | 9 +- src/Pages/internify/mentor/TaskTab.tsx | 20 +- .../internify/mentor/ViewInternSubmission.tsx | 2 +- .../components/ArchiveProjectDialog.tsx | 10 +- .../mentor/components/CreateProjectDialog.tsx | 40 +-- .../mentor/components/EditTaskDialog.tsx | 5 +- .../mentor/components/EligibleInternTable.tsx | 36 ++- .../mentor/components/HomeMentorProject.tsx | 31 +- .../mentor/components/InternsTable.tsx | 14 +- .../mentor/components/MentorProjectCard.tsx | 6 +- .../mentor/components/MentorTaskCard.tsx | 4 +- .../components/StatusSubmissionTable.tsx | 12 - src/hooks/useTasks.ts | 3 +- src/services/ProjectService.ts | 2 +- src/services/TaskService.ts | 4 +- 30 files changed, 302 insertions(+), 278 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 3e9ded9..cee8be6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -41,7 +41,7 @@ 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 SettingsPage from "./Pages/Internify/SettingsPage"; import TaskSubmission from "./Pages/Internify/intern/TaskSubmission"; import SertificateList from "./Pages/Internify/intern/InternCertList"; import MentorProjectsDetailPage from "./Pages/Internify/mentor/MentorProjectDetailPage"; @@ -116,7 +116,7 @@ function App() { } /> } /> } /> - } /> + } /> @@ -134,7 +134,7 @@ function App() { } /> } /> } /> - } /> + } /> diff --git a/src/Layout/Header-internify.tsx b/src/Layout/Header-internify.tsx index 71406c6..7e4b8bd 100644 --- a/src/Layout/Header-internify.tsx +++ b/src/Layout/Header-internify.tsx @@ -4,10 +4,6 @@ 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); @@ -25,9 +21,8 @@ export default function Header({ toggleSidebar }: { toggleSidebar: () => void }) setImageError(false); }, [user?.profile_picture, user?.full_name]); - // Tentukan base path settings sesuai role const settingsPath = - user?.role === "intern" ? "/intern/settings" : "/mentor/settings"; + user?.role === "intern" ? "/intern" : "/mentor"; const initials = user?.full_name ? getInitials(user.full_name) : "U"; const hasPhoto = !!user?.profile_picture && !imageError; diff --git a/src/Pages/Dashboard.tsx b/src/Pages/Dashboard.tsx index 8864868..cbdc27d 100644 --- a/src/Pages/Dashboard.tsx +++ b/src/Pages/Dashboard.tsx @@ -86,26 +86,6 @@ type ApplicationsListResponse = { code?: number; }; -//type baru untuk menyesuaikan perubahan pada batch -type LamaranItem = { - id: number; - id_mahasiswa: number; - id_lowongan_magang: string; - status: string; - batch: number; - batch_data: { id: number; batch_number: number; is_active: boolean }; - lowongan_magang: { posisi: string; kelompok_peminatan: string }; - mahasiswa: { nama_depan: string; nama_belakang: string }; - created_at: string; - update_at: string; -}; - -type LamaranListResponse = { - status: boolean; - data: LamaranItem[]; - message?: string; -}; - const CATEGORY_COLORS = [ "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf", @@ -133,9 +113,6 @@ function groupCount(list: Array) { .sort((a, b) => b.value - a.value); } -const EMPTY_PIE = [{ name: "Tidak ada data", value: 1 }]; -const EMPTY_COLOR = ["#e5e7eb"] - function PieLegend({ data, colors, @@ -143,11 +120,6 @@ function PieLegend({ data: ChartDataItem[]; colors: string[]; }) { - if (data.length === 0) { - return ( -

Tidak ada data

- ) - } return (
{data.map((item, index) => ( @@ -185,9 +157,7 @@ const Dashboard: React.FC = () => { const [batches, setBatches] = useState([]); const [selectedBatchId, setSelectedBatchId] = useState(null); const [batchDropdownOpen, setBatchDropdownOpen] = useState(false); - - const selectedBatch = - batches.find((b) => b.id === selectedBatchId) ?? null; + const selectedBatch = batches.find((b) => b.id === selectedBatchId) ?? null; const getTokenFromCookie = () => { return document.cookie @@ -235,10 +205,11 @@ const Dashboard: React.FC = () => { return msgFromBe || `Request gagal (status ${status ?? "unknown"}).`; }; + // Effect 1: ambil daftar batch sekali, set default = batch aktif useEffect(() => { const fetchBatches = async () => { try { - const res = await api.get("/batch-api/"); + const res = await api.get("/batch-api"); if (res.data?.status) { const list = res.data.data || []; setBatches(list); @@ -258,9 +229,7 @@ const Dashboard: React.FC = () => { useEffect(() => { if (selectedBatchId === null) return; - const batch = batches.find((b) => b.id === selectedBatchId); - if (!batch) return; - const batchNumber = batch.batch_number; + const batchId = selectedBatchId; const fetchDashboardData = async () => { try { @@ -274,95 +243,142 @@ const Dashboard: React.FC = () => { return; } - const batchParams = { params: { batch: batchNumber } }; + const params = { batch_id: batchId }; - const handle404 = (err: unknown) => { - if (axios.isAxiosError(err) && err.response?.status === 404) { - return { data: { status: true, data: [] } } as any; - } - throw err; - }; + const [statsRes, positionRes, countryRes, appsRes] = + await Promise.all([ + api.get( + "/lamaran-magang-api/statistics/dashboard", { params } + ), + api.get( + "/lamaran-magang-api/statistics/position", { params } + ), + api.get( + "/lamaran-magang-api/statistics/country", { params } + ), + api.get("/lamaran-magang-api/statistics/university", { params }), + ]); - const [lamaranRes, countryRes, uniRes] = await Promise.all([ - api.get("/lamaran-magang-api/get", { - params: { limit: 9999, page: 1 }, - }), - api - .get("/lamaran-magang-api/statistics/country", batchParams) - .catch(handle404), - api - .get("/lamaran-magang-api/statistics/university", batchParams) - .catch(handle404), - ]); + const statsJson = statsRes.data; + const positionJson = positionRes.data; + const countryJson = countryRes.data; + const appsJson = appsRes.data; - const all = lamaranRes.data?.data ?? []; + if (!statsJson?.status) { + setErrorMessage( + statsJson?.message || + "Gagal mengambil data dashboard (statistics/dashboard)." + ); + setLoading(false); + return; + } - // filter manual per batch - const rows = all.filter((r) => Number(r.batch) === Number(batchNumber)); + if (!positionJson?.status) { + setErrorMessage( + positionJson?.message || + "Gagal mengambil data dashboard (statistics/position)." + ); + setLoading(false); + return; + } - const norm = (s?: string) => (s ?? "").toLowerCase().trim(); + if (!countryJson?.status) { + setErrorMessage( + countryJson?.message || + "Gagal mengambil data dashboard (statistics/country)." + ); + setLoading(false); + return; + } + + if (!appsJson?.status) { + console.warn( + "Fetch applicants list gagal (untuk universitas chart):", + appsJson?.message + ); + } + + const stats = statsJson.data; + const positions = positionJson.data || []; + const countries = countryJson.data || []; // === Stat Cards === - const totalPendaftar = rows.length; - const totalDiterima = rows.filter((r) => norm(r.status) === "diterima").length; - const totalDitolak = rows.filter((r) => norm(r.status) === "ditolak").length; - const sedangDiproses = rows.filter((r) => norm(r.status) === "diproses").length; - const statCardsFromApi: StatCard[] = [ - { title: "Total Pendaftar", value: totalPendaftar, icon: , iconColor: "#0088ff" }, - { title: "Total Diterima", value: totalDiterima, icon: , iconColor: "#00cc00" }, - { title: "Total Ditolak", value: totalDitolak, icon: , iconColor: "#ff3333" }, - { title: "Sedang Diproses", value: sedangDiproses, icon: , iconColor: "#ffaa00" }, + { + title: "Total Pendaftar", + value: stats.total_pendaftar, + icon: , + iconColor: "#0088ff", + }, + { + title: "Total Diterima", + value: stats.total_diterima, + icon: , + iconColor: "#00cc00", + }, + { + title: "Total Ditolak", + value: stats.total_ditolak, + icon: , + iconColor: "#ff3333", + }, + { + title: "Sedang Diproses", + value: stats.sedang_diproses, + icon: , + iconColor: "#ffaa00", + }, ]; - // === Position (dari nested lowongan_magang.posisi) === - const positionCounts = groupCount( - rows.map((r) => (r.lowongan_magang?.posisi ?? "").trim()) + // === Position === + const positionCounts: ChartDataItem[] = positions.map((p) => ({ + name: p.posisi, + value: p.total, + })); + + // === Universitas === + const applicants = appsJson?.status ? appsJson.data || [] : []; + const uniCountsRaw = groupCount( + applicants.map((a) => (a.universitas ?? "").toString()) ); - // === Penerimaan === - // hanya isi kalau ada data, biar fallback EMPTY_PIE aktif saat kosong - const penerimaanCounts: ChartDataItem[] = totalPendaftar > 0 - ? [ - { name: "Total Pendaftar", value: totalPendaftar }, - { name: "Total Diterima", value: totalDiterima }, - { name: "Total Ditolak", value: totalDitolak }, - ] - : []; - // === Negara (dari /statistics/country?batch=batchNumber) === - const countries = countryRes.data?.data ?? []; + // Top 5 + Lainnya + const topN = 5; + const top = uniCountsRaw.slice(0, topN); + const rest = uniCountsRaw.slice(topN); + const othersValue = rest.reduce((acc, x) => acc + x.value, 0); + const uniCounts: ChartDataItem[] = + othersValue > 0 ? [...top, { name: "Lainnya", value: othersValue }] : top; + + // === Negara === const negaraCounts: ChartDataItem[] = countries.map((c) => ({ name: c.negara, value: c.total, })); - // === Universitas (dari /statistics/university?batch=batchNumber) === - const uniRaw: Array<{ universitas: string; total: number }> = - (uniRes.data as any)?.data ?? []; - const uniSorted = [...uniRaw].sort((a, b) => b.total - a.total); - const top = uniSorted.slice(0, 5); - const othersValue = uniSorted.slice(5).reduce((acc, x) => acc + x.total, 0); - const uniCounts: ChartDataItem[] = [ - ...top.map((u) => ({ name: u.universitas, value: u.total })), - ...(othersValue > 0 ? [{ name: "Lainnya", value: othersValue }] : []), + // === Penerimaan === + const penerimaanCounts: ChartDataItem[] = [ + { name: "Total Pendaftar", value: stats.total_pendaftar }, + { name: "Total Diterima", value: stats.total_diterima }, + { name: "Total Ditolak", value: stats.total_ditolak }, ]; setStatCards(statCardsFromApi); setPositionData(positionCounts); - setPenerimaanData(penerimaanCounts); - setNegaraData(negaraCounts); setUniversitasData(uniCounts); + setNegaraData(negaraCounts); + setPenerimaanData(penerimaanCounts); setLoading(false); } catch (err) { - console.error("Error fetching dashboard:", err); + console.error("Terjadi kesalahan saat mengambil data dashboard:", err); setErrorMessage(getAxiosErrorMessage(err)); setLoading(false); } }; fetchDashboardData(); - }, [api, selectedBatchId, batches]); + }, [api, selectedBatchId]); return (
@@ -377,11 +393,13 @@ const Dashboard: React.FC = () => {

Dashboard

+ {/* Batch Filter */} + {/* Batch Filter */}
- {/* Batch Filter */}
)} +
- {/* Search Box */} - {/*
- - -
*/} + {/* Search Box */} +
+ +
@@ -477,7 +495,7 @@ const Dashboard: React.FC = () => { 0 ? positionData : EMPTY_PIE} + data={positionData} cx="50%" cy="50%" innerRadius={0} @@ -485,13 +503,13 @@ const Dashboard: React.FC = () => { paddingAngle={2} dataKey="value" > - {(positionData.length > 0 ? positionData : EMPTY_PIE).map((_, index) => ( + {positionData.map((_, index) => ( 0 - ? CATEGORY_COLORS[index % CATEGORY_COLORS.length] - : EMPTY_COLOR[0] + CATEGORY_COLORS[ + index % CATEGORY_COLORS.length + ] } /> ))} @@ -513,7 +531,7 @@ const Dashboard: React.FC = () => { 0 ? universitasData : EMPTY_PIE} + data={universitasData} cx="50%" cy="50%" innerRadius={0} @@ -521,13 +539,13 @@ const Dashboard: React.FC = () => { paddingAngle={2} dataKey="value" > - {(universitasData.length > 0 ? universitasData : EMPTY_PIE).map((_, index) => ( + {universitasData.map((_, index) => ( 0 - ? CATEGORY_COLORS[index % CATEGORY_COLORS.length] - : EMPTY_COLOR[0] + CATEGORY_COLORS[ + index % CATEGORY_COLORS.length + ] } /> ))} @@ -552,7 +570,7 @@ const Dashboard: React.FC = () => { 0 ? negaraData : EMPTY_PIE} + data={negaraData} cx="50%" cy="50%" innerRadius={0} @@ -560,14 +578,10 @@ const Dashboard: React.FC = () => { paddingAngle={2} dataKey="value" > - {(negaraData.length > 0 ? negaraData : EMPTY_PIE).map((_, index) => ( + {negaraData.map((_, index) => ( 0 - ? CATEGORY_COLORS[index % CATEGORY_COLORS.length] - : EMPTY_COLOR[0] - } + fill={CATEGORY_COLORS[index % CATEGORY_COLORS.length]} /> ))} @@ -588,7 +602,7 @@ const Dashboard: React.FC = () => { 0 ? penerimaanData : EMPTY_PIE} + data={penerimaanData} cx="50%" cy="50%" innerRadius={0} @@ -596,13 +610,13 @@ const Dashboard: React.FC = () => { paddingAngle={2} dataKey="value" > - {(penerimaanData.length > 0 ? penerimaanData : EMPTY_PIE).map((_, index) => ( + {penerimaanData.map((_, index) => ( 0 - ? CATEGORY_COLORS[index % CATEGORY_COLORS.length] - : EMPTY_COLOR[0] + CATEGORY_COLORS[ + index % CATEGORY_COLORS.length + ] } /> ))} @@ -622,4 +636,4 @@ const Dashboard: React.FC = () => { ); }; -export default Dashboard; +export default Dashboard; \ No newline at end of file diff --git a/src/Pages/internify/LoginInternify.tsx b/src/Pages/internify/LoginInternify.tsx index 7011bfc..fb40dea 100644 --- a/src/Pages/internify/LoginInternify.tsx +++ b/src/Pages/internify/LoginInternify.tsx @@ -74,7 +74,7 @@ export default function InternifyLogin() { setEmail(e.target.value)} className="w-full pl-10 pr-4 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:border-red-500 focus:ring-1 focus:ring-red-500 transition-colors" @@ -90,7 +90,7 @@ export default function InternifyLogin() { setPassword(e.target.value)} className="w-full pl-10 pr-10 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:border-red-500 focus:ring-1 focus:ring-red-500 transition-colors" diff --git a/src/Pages/internify/SettingsPage.tsx b/src/Pages/internify/SettingsPage.tsx index ed0c717..6a185f1 100644 --- a/src/Pages/internify/SettingsPage.tsx +++ b/src/Pages/internify/SettingsPage.tsx @@ -3,7 +3,7 @@ import { useCurrentUser, useUpdateProfile } from "../../hooks/useUser"; import { customToast } from "../utils/showToast"; import { Pen } from "lucide-react"; -export default function MentorSettingsPage() { +export default function SettingsPage() { const { user, loading: userLoading } = useCurrentUser(); const { save, loading: saving, error: errorMsg, successMsg } = useUpdateProfile(); const isIntern = user?.role === "intern"; @@ -23,9 +23,7 @@ export default function MentorSettingsPage() { if (user) { setFormData({ fullName: - user.full_name || - `${user.nama_depan ?? ""} ${user.nama_belakang ?? ""}`.trim() || - "", + user.full_name || "", email: user.email || "", bio: user.professional_bio || "", }); @@ -121,7 +119,7 @@ export default function MentorSettingsPage() {
{/* Avatar dengan badge edit */}
-
+
{photoPreview && !imageError ? ( {/* Info & Action Buttons */} - {!isIntern && + {!isIntern ? (

Profile Photo

@@ -175,7 +173,12 @@ export default function MentorSettingsPage() {

- } + ) : ( +
+

Nama

+

{user?.full_name || formData.fullName || 'User'}

+
+ )} {/* Hidden file input */} {/* Loading skeleton */} - {/* {loading && ( + {loading && (
{[...Array(3)].map((_, i) => (
))}
- )} */} + )} {/* Error */} {error &&

{error}

} - {/* Timeline + Task list — hanya tampil jika ada task */} - {/* {!loading && tasks.length === 0 ? ( */} {tasks.length === 0 ? (

Belum ada tugas yang dapat dikerjakan.

) : ( diff --git a/src/Pages/internify/intern/TaskSubmission.tsx b/src/Pages/internify/intern/TaskSubmission.tsx index 29e5ea2..9d5ad9c 100644 --- a/src/Pages/internify/intern/TaskSubmission.tsx +++ b/src/Pages/internify/intern/TaskSubmission.tsx @@ -3,12 +3,16 @@ import { FileText, Info } from "lucide-react"; import TaskFormFile from "./components/TaskForm-file"; import TaskFormLink from "./components/TaskForm-link"; import { useTaskDetail } from "../../../hooks/useTasks"; +import { useProjectDetail } from "../../../hooks/useProjects"; export default function TaskSubmission() { const [searchParams] = useSearchParams(); const { slug, taskSlug } = useParams<{ slug: string; taskSlug: string }>(); const submissionType = (searchParams.get("type") ?? "file") as "file_upload" | "url_link"; const { task, loading, error } = useTaskDetail(taskSlug!, slug); + const { project } = useProjectDetail(slug!); + + const isProjectActive = project?.status === "active"; const formatDeadline = (dateStr: string) => new Date(dateStr).toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }); @@ -53,6 +57,7 @@ export default function TaskSubmission() { projectId={String(task.project.id)} deadline={new Date(task.deadline_at)} initialSubmission={task.my_submission} + isProjectActive={isProjectActive} /> ) : ( )} diff --git a/src/Pages/internify/intern/components/HomeHeader.tsx b/src/Pages/internify/intern/components/HomeHeader.tsx index e92091b..16475ae 100644 --- a/src/Pages/internify/intern/components/HomeHeader.tsx +++ b/src/Pages/internify/intern/components/HomeHeader.tsx @@ -7,7 +7,7 @@ const HomeHeader = () => { useEffect(() => { api.get("/auth-api/me") .then((res) => { - setName(res.data.data.full_name || res.data.data.nama_depan); + setName(res.data.data.full_name || (res.data.data.nama_depan + " " + res.data.data.nama_belakang)); }) .catch((err) => { console.error("Failed to fetch user:", err); diff --git a/src/Pages/internify/intern/components/ProjectTimeline.tsx b/src/Pages/internify/intern/components/ProjectTimeline.tsx index 8b2314c..b0deaa3 100644 --- a/src/Pages/internify/intern/components/ProjectTimeline.tsx +++ b/src/Pages/internify/intern/components/ProjectTimeline.tsx @@ -8,7 +8,7 @@ export default function ProjectTimeline({ tasks }: { tasks: InternTaskItem[] }) today.setHours(0, 0, 0, 0); return (
-
+
Timeline
    @@ -20,7 +20,6 @@ export default function ProjectTimeline({ tasks }: { tasks: InternTaskItem[] }) const active = isToday || isPast; return (
  • - {/* Vertical line ke bawah, kecuali item terakhir */} {index < tasks.length - 1 && (
    )} @@ -30,10 +29,10 @@ export default function ProjectTimeline({ tasks }: { tasks: InternTaskItem[] }) active ? 'bg-red' : 'bg-gray-400' } ${isToday ? 'shadow-md' : ''}`} /> -
    +
    {isToday ? 'Hari ini' : formatDate(task.deadline_at)}
    -
    +
    {task.title}
  • diff --git a/src/Pages/internify/intern/components/SubmissionStatusTable.tsx b/src/Pages/internify/intern/components/SubmissionStatusTable.tsx index b902f73..4bce00b 100644 --- a/src/Pages/internify/intern/components/SubmissionStatusTable.tsx +++ b/src/Pages/internify/intern/components/SubmissionStatusTable.tsx @@ -10,6 +10,7 @@ interface SubmitStatusTableProps { deadline?: Date; onEdit: () => void; onDelete: () => void; + disabled: boolean; } function getTimeEarly(submittedAt: Date, deadline: Date): string { @@ -32,6 +33,7 @@ export default function SubmitStatusTable({ deadline, onEdit, onDelete, + disabled = false, }: SubmitStatusTableProps) { const [confirmDelete, setConfirmDelete] = useState(false); @@ -86,13 +88,15 @@ export default function SubmitStatusTable({
    diff --git a/src/Pages/internify/intern/components/TaskForm-file.tsx b/src/Pages/internify/intern/components/TaskForm-file.tsx index 4588d6f..2edf76a 100644 --- a/src/Pages/internify/intern/components/TaskForm-file.tsx +++ b/src/Pages/internify/intern/components/TaskForm-file.tsx @@ -11,9 +11,10 @@ interface TaskFormFileProps { projectId?: string; deadline?: Date; initialSubmission?: TaskSubmissionData | null; + isProjectActive?: boolean; } -export default function TaskFormFile({ taskId, projectId, deadline, initialSubmission }: TaskFormFileProps) { +export default function TaskFormFile({ taskId, projectId, deadline, initialSubmission, isProjectActive = true }: TaskFormFileProps) { const { submission, loading, submitFile, updateFile, remove } = useSubmission(taskId, initialSubmission, projectId); const [files, setFiles] = useState([]); const [isDragActive, setIsDragActive] = useState(false); @@ -119,6 +120,7 @@ export default function TaskFormFile({ taskId, projectId, deadline, initialSubmi deadline={deadline} onEdit={() => setIsEditing(true)} onDelete={handleDelete} + disabled={!isProjectActive} /> ); } @@ -188,8 +190,8 @@ export default function TaskFormFile({ taskId, projectId, deadline, initialSubmi )}
    diff --git a/src/Pages/internify/mentor/TaskTab.tsx b/src/Pages/internify/mentor/TaskTab.tsx index d882675..7ce84e9 100644 --- a/src/Pages/internify/mentor/TaskTab.tsx +++ b/src/Pages/internify/mentor/TaskTab.tsx @@ -1,6 +1,5 @@ import { useProjectTasks, useDeleteTask } from '../../../hooks/useTasks'; -import type { ProjectDetail } from '../../../types/project.types'; -import type { ProjectTask } from '../../../types/task.types'; +import type { InternTaskItem, ProjectDetail } from '../../../types/project.types'; import { ClipboardList, Pencil, Trash2 } from 'lucide-react'; import { useState } from 'react'; import CreateTaskModal from './components/CreateTaskDialog'; @@ -12,9 +11,13 @@ export default function TaskTab({ project }: { project: ProjectDetail }) { const { remove } = useDeleteTask(); const [isModalOpen, setIsModalOpen] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false); - const [selectedTask, setSelectedTask] = useState(null); + const [selectedTask, setSelectedTask] = useState(null); + const isProjectActive = project?.status === "active" + + const handleDeleteTask = async (taskId: number, taskTitle: string) => { + const confirmed = window.confirm(`Yakin ingin menghapus task “${taskTitle}”?`); + if (!confirmed) return; - const handleDeleteTask = async (taskId: number,taskTitle: string) => { const del = await remove(taskId, String(project.id)); if (del) { try { @@ -42,7 +45,8 @@ export default function TaskTab({ project }: { project: ProjectDetail }) {

    Tasks

    @@ -65,13 +69,15 @@ export default function TaskTab({ project }: { project: ProjectDetail }) {
    diff --git a/src/Pages/internify/mentor/ViewInternSubmission.tsx b/src/Pages/internify/mentor/ViewInternSubmission.tsx index a68b5d3..db629ef 100644 --- a/src/Pages/internify/mentor/ViewInternSubmission.tsx +++ b/src/Pages/internify/mentor/ViewInternSubmission.tsx @@ -42,7 +42,7 @@ export default function ViewInternSubmission() { ); return ( -
    +
    {/* Header */}
    diff --git a/src/Pages/internify/mentor/components/ArchiveProjectDialog.tsx b/src/Pages/internify/mentor/components/ArchiveProjectDialog.tsx index 1c46ab5..8988f4b 100644 --- a/src/Pages/internify/mentor/components/ArchiveProjectDialog.tsx +++ b/src/Pages/internify/mentor/components/ArchiveProjectDialog.tsx @@ -47,8 +47,8 @@ export default function ArchiveProjectDialog({ {/* Header */}
    -
    - +
    +

    @@ -74,8 +74,8 @@ export default function ArchiveProjectDialog({ Apakah kamu yakin ingin mengarsipkan project{" "} "{projectName}"?

    -
    -

    +

    +

    Perhatian: Project yang diarsipkan tidak akan bisa diakses oleh intern dan tidak muncul di daftar aktif. Kamu masih bisa melihatnya di tab Archived. @@ -96,7 +96,7 @@ export default function ArchiveProjectDialog({ type="button" onClick={onConfirm} disabled={loading} - className="w-full sm:flex-1 py-2.5 bg-yellow-500 hover:bg-yellow-600 disabled:opacity-60 text-white text-sm font-bold rounded-xl transition-colors shadow-md flex items-center justify-center gap-2" + className="w-full sm:flex-1 py-2.5 bg-red-500 hover:bg-red-600 disabled:opacity-60 text-white text-sm font-bold rounded-xl transition-colors shadow-md flex items-center justify-center gap-2" > {loading ? "Mengarsipkan..." : "Ya, Arsipkan"} diff --git a/src/Pages/internify/mentor/components/CreateProjectDialog.tsx b/src/Pages/internify/mentor/components/CreateProjectDialog.tsx index b053330..43f8058 100644 --- a/src/Pages/internify/mentor/components/CreateProjectDialog.tsx +++ b/src/Pages/internify/mentor/components/CreateProjectDialog.tsx @@ -27,18 +27,6 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat const [showDropdown, setShowDropdown] = useState(false); const emailSectionRef = useRef(null); - // Tutup dropdown saat klik di luar area input - const handleClickOutside = useCallback((e: MouseEvent) => { - if (emailSectionRef.current && !emailSectionRef.current.contains(e.target as Node)) { - setShowDropdown(false); - } - }, []); - - useEffect(() => { - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, [handleClickOutside]); - const projectIcons = PROJECT_ICON_CODES.map((id) => ({ id, icon: PROJECT_ICON_MAP[id], @@ -77,11 +65,6 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat } }; - // const handleAddEmailFromForm = (e: React.FormEvent) => { - // e.preventDefault(); - // handleAddEmail(); - // }; - const handleRemoveEmail = (emailToRemove: string) => { setInvitedEmails(invitedEmails.filter(email => email !== emailToRemove)); }; @@ -171,7 +154,7 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat placeholder="e.g., Q3 Infrastructure Upgrade" value={projectName} onChange={(e) => setProjectName(e.target.value)} - className="w-full px-4 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium" + className="w-full px-4 py-2.5 bg-white border border-card-outline rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium" />

    @@ -185,7 +168,7 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat placeholder="Outline the project goals, deliverables, and technical requirements..." value={projectDescription} onChange={(e) => setProjectDescription(e.target.value)} - className="w-full px-4 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium resize-none leading-relaxed" + className="w-full px-4 py-2.5 bg-white border border-card-outline rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium resize-none leading-relaxed" />
    @@ -213,12 +196,12 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat } if (e.key === 'Escape') setShowDropdown(false); }} - className="w-full pl-10 pr-20 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium" + className="w-full pl-10 pr-20 py-2.5 bg-white border border-card-outline rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium" /> @@ -226,7 +209,7 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat {/* Dropdown daftar intern */} {showDropdown && ( -
    +
    {loadingInterns ? (

    Memuat data intern...

    ) : (() => { @@ -244,13 +227,10 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat key={intern.id} type="button" onMouseDown={(e) => { e.preventDefault(); handleAddEmail(intern.email); }} - className="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-gray-50 transition-colors text-left" + className="w-full flex items-center gap-3 px-6 py-2.5 hover:bg-gray-50 transition-colors text-left" > -
    - {intern.full_name.split(' ').slice(0, 2).map((n) => n[0]).join('').toUpperCase()} -
    - {intern.full_name} + {intern.full_name} {intern.email}
    @@ -294,7 +274,7 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat required value={startDate} onChange={(e) => setStartDate(e.target.value)} - className="w-full px-3 py-2.5 bg-white border border-gray-200 rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium appearance-none" + className="w-full px-3 py-2.5 bg-white border border-card-outline rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium appearance-none" /> @@ -312,7 +292,7 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat required value={endDate} onChange={(e) => setEndDate(e.target.value)} - className="w-full px-3 py-2.5 bg-white border border-gray-200 rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium appearance-none" + className="w-full px-3 py-2.5 bg-white border border-card-outline rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium appearance-none" /> @@ -329,7 +309,7 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat diff --git a/src/Pages/internify/mentor/components/EditTaskDialog.tsx b/src/Pages/internify/mentor/components/EditTaskDialog.tsx index b205027..e1ee44e 100644 --- a/src/Pages/internify/mentor/components/EditTaskDialog.tsx +++ b/src/Pages/internify/mentor/components/EditTaskDialog.tsx @@ -1,13 +1,14 @@ import { useRef, useEffect, useState } from "react"; import { X, FileText, Link } from "lucide-react"; import { useUpdateTask } from '../../../../hooks/useTasks'; -import type { ProjectTask, SubmissionType } from '../../../../types/task.types'; +import type { SubmissionType } from '../../../../types/task.types'; import { customToast } from '../../../utils/showToast'; +import type { InternTaskItem } from "../../../../types/project.types"; interface EditTaskModalProps { isOpen: boolean; onClose: () => void; - task: ProjectTask; + task: InternTaskItem; onSuccess: () => void; } diff --git a/src/Pages/internify/mentor/components/EligibleInternTable.tsx b/src/Pages/internify/mentor/components/EligibleInternTable.tsx index 0a84bf5..7dbc097 100644 --- a/src/Pages/internify/mentor/components/EligibleInternTable.tsx +++ b/src/Pages/internify/mentor/components/EligibleInternTable.tsx @@ -19,6 +19,8 @@ import Avatar from "../../Avatar"; import ProgressBar from "../../ProgressBar"; import { customToast } from "../../../utils/showToast"; import type { Project } from "../../../../types/project.types"; +import { previewCertificate } from "../../../utils/Certificates"; +import type { Certificate } from "../../../../types/certificate.types"; interface EligibleInternTableProps { interns: InternSubmissionProgress[]; @@ -72,6 +74,32 @@ export default function EligibleInternTable({ const [generating, setGenerating] = useState(false); const [generateError, setGenerateError] = useState(null); + const handlePreview = (intern: InternSubmissionProgress) => { + if (!project?.certificate_template) { + customToast.error("Template belum ada", "Template sertifikat belum diupload."); + return; + } + const mockCert: Certificate = { + id: intern.id_user, + id_project: project.id, + id_user: intern.id_user, + certificate_no: "PREVIEW", + issued_at: new Date().toISOString(), + uuid: "preview-mode", + user: { + id: intern.id_user, + full_name: intern.full_name, + email: intern.email, + }, + project: { + id: project.id, + project_name: project.project_name, + description: project.description, + }, + }; + previewCertificate(mockCert, project.certificate_template); + }; + const [page, setPage] = useState(1); const totalPages = Math.ceil(interns.length / PER_PAGE); const paginated = interns.slice((page - 1) * PER_PAGE, page * PER_PAGE); @@ -113,7 +141,7 @@ export default function EligibleInternTable({ const templateUrl = resolveImageUrl(project.certificate_template!); const duration = formatDateRange(project.start_date, project.end_date); - + await Promise.all( result.map(async (cert) => { const verifyUrl = `${window.location.origin}/verify-certificate/${cert.uuid}`; @@ -253,7 +281,11 @@ export default function EligibleInternTable({ Nag Intern ) : ( - )} diff --git a/src/Pages/internify/mentor/components/HomeMentorProject.tsx b/src/Pages/internify/mentor/components/HomeMentorProject.tsx index efd5c0a..d11d2ee 100644 --- a/src/Pages/internify/mentor/components/HomeMentorProject.tsx +++ b/src/Pages/internify/mentor/components/HomeMentorProject.tsx @@ -3,29 +3,23 @@ import { useState } from 'react'; import { Plus } from 'lucide-react'; import CreateProjectModal from './CreateProjectDialog'; import { useProjects } from '../../../../hooks/useProjects'; -import * as LucideIcons from 'lucide-react'; -import type { LucideProps } from 'lucide-react'; - -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; -}; +import { getProjectIcon } from '../../../../lib/ProjectIcons'; const HomeMentorProject = () => { const navigate = useNavigate(); const [isModalOpen, setIsModalOpen] = useState(false); - const { projects, loading, error } = useProjects(); - const openDialog = () => { - setIsModalOpen(true); - }; - const closeDialog = () => { - setIsModalOpen(false); - }; + const { projects, loading, error, refetch } = useProjects(); + const openDialog = () => { setIsModalOpen(true) }; + const closeDialog = () => { setIsModalOpen(false) }; + + const handleRefetch = () => { + closeDialog(); + refetch(); + } return (
    -
    -

    Projects

    +
    +

    Projects

    diff --git a/src/Pages/internify/mentor/components/InternsTable.tsx b/src/Pages/internify/mentor/components/InternsTable.tsx index 7c375d4..a5dd9ea 100644 --- a/src/Pages/internify/mentor/components/InternsTable.tsx +++ b/src/Pages/internify/mentor/components/InternsTable.tsx @@ -82,31 +82,31 @@ export default function InternsTable({ {intern.name} ) : ( -
    +
    {intern.name.charAt(0)}
    )}
    -

    {intern.name}

    -

    {intern.email}

    +

    {intern.name}

    +

    {intern.email}

    {/* Column 2: Current Project Title & Role Accent */}
    -
    +
    {intern.projectName || "Unassigned"}
    -

    +

    {intern.role}

    {/* Column 3: Action Buttons Block Right aligned */} -
    +
    {/* Tombol Add: membuka modal assign member untuk intern ini */}