import React, { useEffect, useMemo, useState } from "react"; import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts"; import { Search, ChevronDown, Users, UserCheck, UserX, Clock, } from "lucide-react"; import axios, { AxiosError } from "axios"; import NavbarAdmin from "../Layout/NavbarAdmin"; import SidebarAdmin from "../Layout/SidebarAdmin"; type ChartDataItem = { name: string; value: number; }; type StatCard = { title: string; value: number; icon: React.ReactNode; iconColor: string; }; // ====== BE RESPONSE TYPES ====== type DashboardStatsResponse = { status: boolean; data: { total_pendaftar: number; total_diterima: number; total_ditolak: number; sedang_diproses: number; acceptance_rate: string; rejection_rate: string; pending_rate: string; }; message: string; code?: number; }; type PositionStatsResponse = { status: boolean; data: Array<{ posisi: string; total: number; diterima: number; ditolak: number; }>; message?: string; code?: number; }; type CountryStatsResponse = { status: boolean; data: Array<{ negara: string; total: number; diterima: number; ditolak: number; diproses: number; }>; message?: string; code?: number; }; type BatchResponse = { success: boolean; message: string; data: Array<{ id: number; batch_number: number; is_active: boolean; created_at: string; updated_at: string; }>; }; type ApplicationsListResponse = { status: boolean; data: Array<{ universitas?: string | null; }>; message?: string; code?: number; }; const CATEGORY_COLORS = [ "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf", "#393b79", "#637939", "#8c6d31", "#843c39", "#7b4173", "#3182bd", "#e6550d", "#31a354", "#756bb1", "#636363", "#6baed6", "#fd8d3c", "#74c476", "#9e9ac8", "#969696", "#9ecae1", "#fdae6b", "#a1d99b", "#bcbddc", "#bdbdbd", ]; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL?.trim() || "https://internify-ruddy.vercel.app"; // helper: group by key -> count function groupCount(list: Array) { const map = new Map(); for (const raw of list) { const key = (raw || "").trim(); if (!key) continue; map.set(key, (map.get(key) || 0) + 1); } return Array.from(map.entries()) .map(([name, value]) => ({ name, value })) .sort((a, b) => b.value - a.value); } function PieLegend({ data, colors, }: { data: ChartDataItem[]; colors: string[]; }) { return (
{data.map((item, index) => (
{item.name}
{item.value}
))}
); } const Dashboard: React.FC = () => { const [loading, setLoading] = useState(true); const [errorMessage, setErrorMessage] = useState(""); const [statCards, setStatCards] = useState([]); const [positionData, setPositionData] = useState([]); const [universitasData, setUniversitasData] = useState([]); const [penerimaanData, setPenerimaanData] = useState([]); const [negaraData, setNegaraData] = useState([]); const [, setBatches] = useState([]); const getTokenFromCookie = () => { return document.cookie .split("; ") .find((row) => row.startsWith("token=")) ?.split("=")[1]; }; const api = useMemo(() => { const instance = axios.create({ baseURL: API_BASE_URL, headers: { Accept: "application/json" }, timeout: 15000, }); instance.interceptors.request.use((config) => { const token = getTokenFromCookie(); if (token) { config.headers = config.headers ?? {}; config.headers.Authorization = `Bearer ${token}`; } return config; }); return instance; }, []); const getAxiosErrorMessage = (err: unknown) => { if (!axios.isAxiosError(err)) { return "Terjadi kesalahan saat mengambil data dashboard."; } const axErr = err as AxiosError; const status = axErr.response?.status; const msgFromBe = axErr.response?.data?.message || axErr.response?.data?.error || axErr.message; if (status === 401) { return msgFromBe || "Unauthorized (401). Silakan login ulang."; } return msgFromBe || `Request gagal (status ${status ?? "unknown"}).`; }; useEffect(() => { const fetchDashboardData = async () => { try { setLoading(true); setErrorMessage(""); const token = getTokenFromCookie(); if (!token) { setErrorMessage("Token tidak ditemukan di cookie. Silakan login ulang."); setLoading(false); 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 statsJson = statsRes.data; const positionJson = positionRes.data; const countryJson = countryRes.data; const batchJson = batchRes.data; const appsJson = appsRes.data; if (!statsJson?.status) { setErrorMessage( statsJson?.message || "Gagal mengambil data dashboard (statistics/dashboard)." ); setLoading(false); return; } if (!positionJson?.status) { setErrorMessage( positionJson?.message || "Gagal mengambil data dashboard (statistics/position)." ); setLoading(false); return; } if (!countryJson?.status) { setErrorMessage( countryJson?.message || "Gagal mengambil data dashboard (statistics/country)." ); setLoading(false); return; } 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 || []; // === Stat Cards === 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", }, ]; // === 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()) ); // 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, })); // === 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); setUniversitasData(uniCounts); setNegaraData(negaraCounts); setPenerimaanData(penerimaanCounts); setLoading(false); } catch (err) { console.error("Terjadi kesalahan saat mengambil data dashboard:", err); setErrorMessage(getAxiosErrorMessage(err)); setLoading(false); } }; fetchDashboardData(); }, [api]); return (
{/* Header Section */}

Dashboard

{/* Batch Filter */} {/* Search Box */}
{/* Loading / Error State */} {loading && (

Memuat data dashboard...

)} {!loading && errorMessage && (

{errorMessage}

)} {!loading && !errorMessage && ( <> {/* Stat Cards */}
{statCards.map((card, index) => (
{card.icon}

{card.title}

{card.value}

))}
{/* ====== Charts ====== */}
{/* 1) Position */}

Position

{positionData.map((_, index) => ( ))}
{/* 2) Universitas */}

Universitas

{universitasData.map((_, index) => ( ))}
{/* 3) Negara */}

Negara

{negaraData.map((_, index) => ( ))}
{/* 4) Penerimaan */}

Penerimaan

{penerimaanData.map((_, index) => ( ))}
)}
); }; export default Dashboard;