From 87d6461a245b6f880de42ce7ae17c838c99d2823 Mon Sep 17 00:00:00 2001 From: Billy Marcello <145271272+BeLieM@users.noreply.github.com> Date: Sun, 18 Jan 2026 14:52:38 +0700 Subject: [PATCH] added new universitas chart, more chart colors variety, added new warning for login field --- src/Pages/Dashboard.tsx | 398 ++++++++++++++++++++++----------------- src/Pages/LoginAdmin.tsx | 80 ++++++-- 2 files changed, 287 insertions(+), 191 deletions(-) diff --git a/src/Pages/Dashboard.tsx b/src/Pages/Dashboard.tsx index 9fb4149..ac79bb9 100644 --- a/src/Pages/Dashboard.tsx +++ b/src/Pages/Dashboard.tsx @@ -20,7 +20,6 @@ type ChartDataItem = { type StatCard = { title: string; value: number; - subtitle: string; icon: React.ReactNode; iconColor: string; }; @@ -78,19 +77,79 @@ type BatchResponse = { }>; }; -const UNIVERSITAS_COLORS = ["#001a66", "#334d99", "#6699cc", "#99b3ff"]; -const PENERIMAAN_COLORS = ["#0088ff", "#00cc00", "#ff3333"]; -const NEGARA_COLORS = ["#cc3333", "#dd6666", "#ee9999", "#f0b3b3", "#f5cccc"]; +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([]); @@ -107,9 +166,7 @@ const Dashboard: React.FC = () => { const api = useMemo(() => { const instance = axios.create({ baseURL: API_BASE_URL, - headers: { - Accept: "application/json", - }, + headers: { Accept: "application/json" }, timeout: 15000, }); @@ -153,15 +210,13 @@ const Dashboard: React.FC = () => { const token = getTokenFromCookie(); if (!token) { - setErrorMessage( - "Token tidak ditemukan di cookie. Silakan login ulang." - ); + setErrorMessage("Token tidak ditemukan di cookie. Silakan login ulang."); setLoading(false); return; } - const [statsRes, positionRes, countryRes, batchRes] = await Promise.all( - [ + const [statsRes, positionRes, countryRes, batchRes, appsRes] = + await Promise.all([ api.get( "/lamaran-magang-api/statistics/dashboard" ), @@ -172,13 +227,14 @@ const Dashboard: React.FC = () => { "/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( @@ -213,61 +269,83 @@ const Dashboard: React.FC = () => { 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, - subtitle: "+200 dari batch sebelumnya", icon: , iconColor: "#0088ff", }, { title: "Total Diterima", value: stats.total_diterima, - subtitle: `${stats.acceptance_rate} acceptance rate`, icon: , iconColor: "#00cc00", }, { title: "Total Ditolak", value: stats.total_ditolak, - subtitle: `${stats.rejection_rate} rejection rate`, icon: , iconColor: "#ff3333", }, { title: "Sedang Diproses", value: stats.sedang_diproses, - subtitle: `${stats.pending_rate} pending review`, icon: , iconColor: "#ffaa00", }, ]; - const penerimaanFromApi: ChartDataItem[] = [ + // === 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 }, ]; - const universitasFromApi: ChartDataItem[] = positions.map((p) => ({ - name: p.posisi, - value: p.total, - })); - - const negaraFromApi: ChartDataItem[] = countries.map((c) => ({ - name: c.negara, - value: c.total, - })); - setStatCards(statCardsFromApi); - setPenerimaanData(penerimaanFromApi); - setUniversitasData(universitasFromApi); - setNegaraData(negaraFromApi); + setPositionData(positionCounts); + setUniversitasData(uniCounts); + setNegaraData(negaraCounts); + setPenerimaanData(penerimaanCounts); setLoading(false); } catch (err) { @@ -313,9 +391,7 @@ const Dashboard: React.FC = () => { {/* Loading / Error State */} {loading && ( -

- Memuat data dashboard... -

+

Memuat data dashboard...

)} {!loading && errorMessage && ( @@ -342,44 +418,40 @@ const Dashboard: React.FC = () => { {card.title}
-
-

- {card.value} -

-
-

- {card.subtitle} + +

+ {card.value}

))} - {/* Charts Section */} + {/* ====== Charts ====== */}
- {/* Universitas Chart */} + {/* 1) Position */}

- Universitas + Position

- + - {universitasData.map((_, index) => ( + {positionData.map((_, index) => ( @@ -389,140 +461,114 @@ const Dashboard: React.FC = () => {
- {/* Legend */} -
- {universitasData.map((item, index) => ( -
-
-
- {item.name} -
- - {item.value}% - -
- ))} -
+
- {/* Right Column - Penerimaan & Negara */} -
- {/* Penerimaan Chart */} -
-

- Penerimaan -

+ {/* 2) Universitas */} +
+

+ Universitas +

-
- - - - {penerimaanData.map((_, index) => ( - - ))} - - - -
- - {/* Legend */} -
- {penerimaanData.map((item, index) => ( -
+ + + -
- {item.name} -
- ))} -
+ {universitasData.map((_, index) => ( + + ))} + + +
- {/* Negara Chart */} -
-

- Negara -

+ +
-
- - - - {negaraData.map((_, index) => ( - - ))} - - - -
+ {/* 3) Negara */} +
+

+ Negara +

- {/* Legend */} -
- {negaraData.map((item, index) => ( -
+ + + -
- {item.name} -
- ))} -
+ {negaraData.map((_, index) => ( + + ))} + + +
+ + +
+ + {/* 4) Penerimaan */} +
+

+ Penerimaan +

+ +
+ + + + {penerimaanData.map((_, index) => ( + + ))} + + + +
+ +
diff --git a/src/Pages/LoginAdmin.tsx b/src/Pages/LoginAdmin.tsx index c247b51..0d26a31 100644 --- a/src/Pages/LoginAdmin.tsx +++ b/src/Pages/LoginAdmin.tsx @@ -10,16 +10,50 @@ const LoginAdmin = () => { const [errorMsg, setErrorMsg] = useState(""); const [isLoading, setIsLoading] = useState(false); + // helper validasi sederhana + const validate = () => { + const trimmedEmail = email.trim(); + const trimmedPassword = password.trim(); + + if (!trimmedEmail && !trimmedPassword) { + setErrorMsg("Field Email dan Password kosong, mohon diisi terlebih dahulu"); + return false; + } + + if (!trimmedEmail) { + setErrorMsg("Field Email kosong, mohon diisi terlebih dahulu"); + return false; + } + + if (!trimmedPassword) { + setErrorMsg("Field Password kosong, mohon diisi terlebih dahulu"); + return false; + } + + // opsional: validasi format email sederhana + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(trimmedEmail)) { + setErrorMsg("Format email tidak valid"); + return false; + } + + return true; + }; + const handleLogin = async () => { + // reset error dulu + setErrorMsg(""); + + // validasi sebelum request + if (!validate()) return; + setIsLoading(true); try { const response = await axios.post( `${import.meta.env.VITE_API_BASE_URL}/auth-api/login`, - { email, password }, + { email: email.trim(), password: password.trim() }, { - headers: { - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, } ); @@ -30,14 +64,19 @@ const LoginAdmin = () => { setErrorMsg(response.data.message || "Login failed"); } } catch (error: any) { - setErrorMsg( - error?.response?.data?.message || "Something went wrong" - ); + setErrorMsg(error?.response?.data?.message || "Something went wrong"); } finally { setIsLoading(false); } }; + // Enter submit: cukup pakai form onSubmit + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (isLoading) return; + handleLogin(); + }; + return (
@@ -45,7 +84,11 @@ const LoginAdmin = () => { Logo
-
+ {/* Pakai form agar Enter bisa submit */} +
{/* Email */}
@@ -70,7 +113,11 @@ const LoginAdmin = () => { placeholder="Enter your email" className="w-full p-4 bg-[#EDF2F7] rounded-xl" value={email} - onChange={(e) => setEmail(e.target.value)} + onChange={(e) => { + setEmail(e.target.value); + if (errorMsg) setErrorMsg(""); // opsional: hapus warning saat user mulai mengetik + }} + autoComplete="email" />
@@ -99,7 +146,11 @@ const LoginAdmin = () => { placeholder="Enter your password" className="w-full p-4 bg-[#EDF2F7] rounded-xl" value={password} - onChange={(e) => setPassword(e.target.value)} + onChange={(e) => { + setPassword(e.target.value); + if (errorMsg) setErrorMsg(""); + }} + autoComplete="current-password" />
@@ -108,10 +159,10 @@ const LoginAdmin = () => {

{errorMsg}

)} - {/* Sign In Button */} + {/* Sign In Button: type="submit" biar form bisa Enter */}

- {" "} { Halaman Khusus Login Petugas HUMIC

-
+
);