diff --git a/src/Pages/Dashboard.tsx b/src/Pages/Dashboard.tsx index ac79bb9..8864868 100644 --- a/src/Pages/Dashboard.tsx +++ b/src/Pages/Dashboard.tsx @@ -66,7 +66,7 @@ type CountryStatsResponse = { }; type BatchResponse = { - success: boolean; + status: boolean; message: string; data: Array<{ id: number; @@ -86,6 +86,26 @@ 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", @@ -113,6 +133,9 @@ 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, @@ -120,6 +143,11 @@ function PieLegend({ data: ChartDataItem[]; colors: string[]; }) { + if (data.length === 0) { + return ( +

Tidak ada data

+ ) + } return (
{data.map((item, index) => ( @@ -154,7 +182,12 @@ const Dashboard: React.FC = () => { const [penerimaanData, setPenerimaanData] = useState([]); const [negaraData, setNegaraData] = useState([]); - const [, setBatches] = useState([]); + const [batches, setBatches] = useState([]); + const [selectedBatchId, setSelectedBatchId] = useState(null); + const [batchDropdownOpen, setBatchDropdownOpen] = useState(false); + + const selectedBatch = + batches.find((b) => b.id === selectedBatchId) ?? null; const getTokenFromCookie = () => { return document.cookie @@ -203,6 +236,32 @@ const Dashboard: React.FC = () => { }; useEffect(() => { + const fetchBatches = async () => { + try { + const res = await api.get("/batch-api/"); + if (res.data?.status) { + const list = res.data.data || []; + setBatches(list); + const defaultBatch = list.find((b) => b.is_active) ?? list[0]; + if (defaultBatch) setSelectedBatchId(defaultBatch.id); + } else { + setErrorMessage("Gagal mengambil daftar batch."); + setLoading(false); + } + } catch (err) { + setErrorMessage(getAxiosErrorMessage(err)); + setLoading(false); + } + }; + fetchBatches(); + }, [api]); + + useEffect(() => { + if (selectedBatchId === null) return; + const batch = batches.find((b) => b.id === selectedBatchId); + if (!batch) return; + const batchNumber = batch.batch_number; + const fetchDashboardData = async () => { try { setLoading(true); @@ -215,148 +274,95 @@ const Dashboard: React.FC = () => { return; } - const [statsRes, positionRes, countryRes, batchRes, appsRes] = - await Promise.all([ - api.get( - "/lamaran-magang-api/statistics/dashboard" - ), - api.get( - "/lamaran-magang-api/statistics/position" - ), - api.get( - "/lamaran-magang-api/statistics/country" - ), - api.get("/batch-api"), - api.get("/lamaran-magang-api/statistics/university"), - ]); + const batchParams = { params: { batch: batchNumber } }; - const statsJson = statsRes.data; - const positionJson = positionRes.data; - const countryJson = countryRes.data; - const batchJson = batchRes.data; - const appsJson = appsRes.data; + const handle404 = (err: unknown) => { + if (axios.isAxiosError(err) && err.response?.status === 404) { + return { data: { status: true, data: [] } } as any; + } + throw err; + }; - if (!statsJson?.status) { - setErrorMessage( - statsJson?.message || - "Gagal mengambil data dashboard (statistics/dashboard)." - ); - setLoading(false); - return; - } + 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), + ]); - if (!positionJson?.status) { - setErrorMessage( - positionJson?.message || - "Gagal mengambil data dashboard (statistics/position)." - ); - setLoading(false); - return; - } + const all = lamaranRes.data?.data ?? []; - if (!countryJson?.status) { - setErrorMessage( - countryJson?.message || - "Gagal mengambil data dashboard (statistics/country)." - ); - setLoading(false); - return; - } + // filter manual per batch + const rows = all.filter((r) => Number(r.batch) === Number(batchNumber)); - if (batchJson?.success) { - setBatches(batchJson.data || []); - } else { - console.warn("Fetch batch gagal:", batchJson?.message); - } - - 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 || []; + const norm = (s?: string) => (s ?? "").toLowerCase().trim(); // === 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: 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", - }, + { 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" }, ]; - // === 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()) + // === Position (dari nested lowongan_magang.posisi) === + const positionCounts = groupCount( + rows.map((r) => (r.lowongan_magang?.posisi ?? "").trim()) ); - // 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 === + // === 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 ?? []; const negaraCounts: ChartDataItem[] = countries.map((c) => ({ name: c.negara, value: c.total, })); - // === 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 }, + // === 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 }] : []), ]; setStatCards(statCardsFromApi); setPositionData(positionCounts); - setUniversitasData(uniCounts); - setNegaraData(negaraCounts); setPenerimaanData(penerimaanCounts); + setNegaraData(negaraCounts); + setUniversitasData(uniCounts); setLoading(false); } catch (err) { - console.error("Terjadi kesalahan saat mengambil data dashboard:", err); + console.error("Error fetching dashboard:", err); setErrorMessage(getAxiosErrorMessage(err)); setLoading(false); } }; fetchDashboardData(); - }, [api]); + }, [api, selectedBatchId, batches]); return (
@@ -371,20 +377,53 @@ const Dashboard: React.FC = () => {

Dashboard

- {/* Batch Filter */} - +
+ {/* Batch Filter */} + - {/* Search Box */} -
- - + {batchDropdownOpen && ( +
+ {batches.map((b) => ( + + ))} +
+ )} + + {/* Search Box */} + {/*
+ + +
*/}
@@ -438,7 +477,7 @@ const Dashboard: React.FC = () => { 0 ? positionData : EMPTY_PIE} cx="50%" cy="50%" innerRadius={0} @@ -446,13 +485,13 @@ const Dashboard: React.FC = () => { paddingAngle={2} dataKey="value" > - {positionData.map((_, index) => ( + {(positionData.length > 0 ? positionData : EMPTY_PIE).map((_, index) => ( 0 + ? CATEGORY_COLORS[index % CATEGORY_COLORS.length] + : EMPTY_COLOR[0] } /> ))} @@ -474,7 +513,7 @@ const Dashboard: React.FC = () => { 0 ? universitasData : EMPTY_PIE} cx="50%" cy="50%" innerRadius={0} @@ -482,13 +521,13 @@ const Dashboard: React.FC = () => { paddingAngle={2} dataKey="value" > - {universitasData.map((_, index) => ( + {(universitasData.length > 0 ? universitasData : EMPTY_PIE).map((_, index) => ( 0 + ? CATEGORY_COLORS[index % CATEGORY_COLORS.length] + : EMPTY_COLOR[0] } /> ))} @@ -513,7 +552,7 @@ const Dashboard: React.FC = () => { 0 ? negaraData : EMPTY_PIE} cx="50%" cy="50%" innerRadius={0} @@ -521,10 +560,14 @@ const Dashboard: React.FC = () => { paddingAngle={2} dataKey="value" > - {negaraData.map((_, index) => ( + {(negaraData.length > 0 ? negaraData : EMPTY_PIE).map((_, index) => ( 0 + ? CATEGORY_COLORS[index % CATEGORY_COLORS.length] + : EMPTY_COLOR[0] + } /> ))} @@ -545,7 +588,7 @@ const Dashboard: React.FC = () => { 0 ? penerimaanData : EMPTY_PIE} cx="50%" cy="50%" innerRadius={0} @@ -553,13 +596,13 @@ const Dashboard: React.FC = () => { paddingAngle={2} dataKey="value" > - {penerimaanData.map((_, index) => ( + {(penerimaanData.length > 0 ? penerimaanData : EMPTY_PIE).map((_, index) => ( 0 + ? CATEGORY_COLORS[index % CATEGORY_COLORS.length] + : EMPTY_COLOR[0] } /> ))} diff --git a/src/Pages/internify/intern/components/HomeProjectList.tsx b/src/Pages/internify/intern/components/HomeProjectList.tsx index 91e8ac3..1fe9408 100644 --- a/src/Pages/internify/intern/components/HomeProjectList.tsx +++ b/src/Pages/internify/intern/components/HomeProjectList.tsx @@ -1,16 +1,9 @@ import { useNavigate } from 'react-router-dom'; import { useProjects } from '../../../../hooks/useProjects'; -import * as LucideIcons from 'lucide-react'; -import type { LucideProps } from 'lucide-react'; +import { getProjectIcon } from '../../../../lib/ProjectIcons'; const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-'); -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; -}; - const HomeProjectList = () => { const navigate = useNavigate(); const { projects, loading, error } = useProjects(); @@ -27,7 +20,7 @@ const HomeProjectList = () => { return (

Projects

-
+
{projects.map((project) => (
{ >
{(() => { - const Icon = getDynamicIcon(project.project_icon); + const Icon = getProjectIcon(project.project_icon); return ; })()}
diff --git a/src/Pages/internify/intern/components/ProjectTimeline.tsx b/src/Pages/internify/intern/components/ProjectTimeline.tsx index 3340e7b..8b2314c 100644 --- a/src/Pages/internify/intern/components/ProjectTimeline.tsx +++ b/src/Pages/internify/intern/components/ProjectTimeline.tsx @@ -1,9 +1,9 @@ -import type { ProjectTask } from "../../../../types/task.types"; +import type { InternTaskItem } from "../../../../types/project.types"; const formatDate = (dateStr: string) => new Date(dateStr).toLocaleDateString('id-ID', { day: 'numeric', month: 'short' }); -export default function ProjectTimeline({ tasks }: { tasks: ProjectTask[] }) { +export default function ProjectTimeline({ tasks }: { tasks: InternTaskItem[] }) { const today = new Date(); today.setHours(0, 0, 0, 0); return ( diff --git a/src/Pages/internify/intern/components/ProjectsCard.tsx b/src/Pages/internify/intern/components/ProjectsCard.tsx index 2183882..4fd59d2 100644 --- a/src/Pages/internify/intern/components/ProjectsCard.tsx +++ b/src/Pages/internify/intern/components/ProjectsCard.tsx @@ -1,17 +1,10 @@ import { useNavigate } from 'react-router-dom'; -import * as LucideIcons from 'lucide-react'; -import type { LucideProps } from 'lucide-react'; +import { getProjectIcon } from '../../../../lib/ProjectIcons'; import type { Project } from '../../../../types/project.types'; -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; -}; - const ProjectCard = (project: Project) => { const navigate = useNavigate(); - const Icon = getDynamicIcon(project.project_icon); + const Icon = getProjectIcon(project.project_icon); const tasksDone = project.task_done; diff --git a/src/Pages/internify/intern/components/TaskTimeline.tsx b/src/Pages/internify/intern/components/TaskTimeline.tsx index fd01609..26bb5f4 100644 --- a/src/Pages/internify/intern/components/TaskTimeline.tsx +++ b/src/Pages/internify/intern/components/TaskTimeline.tsx @@ -20,7 +20,7 @@ const TaskTimeline = () => { return (

Timeline Tugas

-
+
    {tasks.map((item: InternTaskItem, index) => { const overdue = isOverdue(item.deadline_at); diff --git a/src/Pages/internify/mentor/HomeMentorPage.tsx b/src/Pages/internify/mentor/HomeMentorPage.tsx index 9d7497d..997288c 100644 --- a/src/Pages/internify/mentor/HomeMentorPage.tsx +++ b/src/Pages/internify/mentor/HomeMentorPage.tsx @@ -11,7 +11,6 @@ const HomeMentorPage = () => {
    -
diff --git a/src/Pages/internify/mentor/components/HomeMentorProject.tsx b/src/Pages/internify/mentor/components/HomeMentorProject.tsx index 6c338fd..efd5c0a 100644 --- a/src/Pages/internify/mentor/components/HomeMentorProject.tsx +++ b/src/Pages/internify/mentor/components/HomeMentorProject.tsx @@ -35,7 +35,7 @@ const HomeMentorProject = () => {
-
+
{loading &&

Memuat...

} {error &&

{error}

} {projects.map((project) => ( diff --git a/src/Pages/internify/mentor/components/MentorProjectCard.tsx b/src/Pages/internify/mentor/components/MentorProjectCard.tsx index b71c221..2bc2657 100644 --- a/src/Pages/internify/mentor/components/MentorProjectCard.tsx +++ b/src/Pages/internify/mentor/components/MentorProjectCard.tsx @@ -1,19 +1,12 @@ import { useState } from 'react'; import { ClipboardCheck, Trash2, UsersRound } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; -import * as LucideIcons from 'lucide-react'; -import type { LucideProps } from 'lucide-react'; import type { Project } from '../../../../types/project.types'; import { useArchiveProject, useCompleteProject } from '../../../../hooks/useProjects'; import { customToast } from '../../../utils/showToast'; import ArchiveProjectDialog from './ArchiveProjectDialog'; import CompleteProjectDialog from './CompleteProjectDialog'; - -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'; interface MentorProjectCardProps extends Project { onArchived?: () => void; @@ -32,7 +25,7 @@ export default function MentorProjectCard({ onArchived, onCompleted, ...project navigate(`/mentor/projects/${project.slug}`); }; - const Icon = getDynamicIcon(project.project_icon); + const Icon = getProjectIcon(project.project_icon); /* ── Archive ─────────────────────────────────────────────── */ const handleArchiveButtonClick = (e: React.MouseEvent) => {