Yvonne Priscilla commited on
Commit
66ed0c8
·
1 Parent(s): e394370
src/app/api/cv-profile/charts/major/route.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { prisma } from "@/lib/prisma";
2
+ import { NextResponse } from "next/server";
3
+
4
+ function randomColor() {
5
+ return `#${Math.floor(Math.random() * 16777215).toString(16)}`;
6
+ }
7
+
8
+ export async function GET() {
9
+ const profiles = await prisma.cv_profile.findMany({
10
+ select: {
11
+ major_edu_1: true,
12
+ major_edu_2: true,
13
+ major_edu_3: true,
14
+ },
15
+ });
16
+
17
+ const majorCount: Record<string, number> = {};
18
+
19
+ profiles.forEach((profile) => {
20
+ [profile.major_edu_1, profile.major_edu_2, profile.major_edu_3]
21
+ .filter(Boolean)
22
+ .forEach((univ) => {
23
+ if (!univ) return;
24
+
25
+ majorCount[univ] = (majorCount[univ] || 0) + 1;
26
+ });
27
+ });
28
+
29
+
30
+
31
+ return NextResponse.json(Object.entries(majorCount).map(([name, value]) => ({
32
+ name,
33
+ value,
34
+ color: randomColor(),
35
+ })));
36
+ }
src/app/api/cv-profile/charts/university/route.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { prisma } from "@/lib/prisma";
2
+ import { NextResponse } from "next/server";
3
+
4
+ function randomColor() {
5
+ return `#${Math.floor(Math.random() * 16777215).toString(16)}`;
6
+ }
7
+
8
+ export async function GET() {
9
+ const profiles = await prisma.cv_profile.findMany({
10
+ select: {
11
+ univ_edu_1: true,
12
+ univ_edu_2: true,
13
+ univ_edu_3: true,
14
+ },
15
+ });
16
+
17
+ const universityCount: Record<string, number> = {};
18
+
19
+ profiles.forEach((profile) => {
20
+ [profile.univ_edu_1, profile.univ_edu_2, profile.univ_edu_3]
21
+ .filter(Boolean)
22
+ .forEach((univ) => {
23
+ if (!univ) return;
24
+
25
+ universityCount[univ] = (universityCount[univ] || 0) + 1;
26
+ });
27
+ });
28
+
29
+
30
+
31
+ return NextResponse.json(Object.entries(universityCount).map(([name, value]) => ({
32
+ name,
33
+ value,
34
+ color: randomColor(),
35
+ })));
36
+ }
src/app/recruitment/page.tsx CHANGED
@@ -1,12 +1,14 @@
1
  'use client';
2
 
3
- import { MetricsRow } from '@/components/dashboard/metrics-row';
4
  import CandidateTable from '@/components/dashboard/candidates-table';
 
 
5
 
6
  export default function DashboardPage() {
7
  return (
8
  <div>
9
  <MetricsRow />
 
10
  <CandidateTable />
11
  </div>
12
  );
 
1
  'use client';
2
 
 
3
  import CandidateTable from '@/components/dashboard/candidates-table';
4
+ import { ChartsSection } from '@/components/dashboard/charts-section';
5
+ import { MetricsRow } from '@/components/dashboard/metrics-row';
6
 
7
  export default function DashboardPage() {
8
  return (
9
  <div>
10
  <MetricsRow />
11
+ <ChartsSection />
12
  <CandidateTable />
13
  </div>
14
  );
src/components/dashboard/charts-section.tsx CHANGED
@@ -1,21 +1,21 @@
1
  'use client';
2
 
3
  import { Card } from '@/components/ui/card';
4
- import { SuitabilityPie } from './charts/suitability-pie';
5
  import { UniversityBar } from './charts/university-bar';
6
 
7
  export function ChartsSection() {
8
  return (
9
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
10
  <Card className="p-6 border-2">
11
  <h3 className="text-base font-semibold text-gray-900 text-center text-green-500">
12
- Distribusi Suitability % (Eligible)
13
  </h3>
14
- <SuitabilityPie />
15
  </Card>
16
  <Card className="p-6 border-2">
17
  <h3 className="text-base font-semibold text-gray-900 text-center text-green-500">
18
- Top Universitas (Eligible)
19
  </h3>
20
  <UniversityBar />
21
  </Card>
 
1
  'use client';
2
 
3
  import { Card } from '@/components/ui/card';
4
+ import { MajorChart } from './charts/major-chart';
5
  import { UniversityBar } from './charts/university-bar';
6
 
7
  export function ChartsSection() {
8
  return (
9
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 my-2 mt-4">
10
  <Card className="p-6 border-2">
11
  <h3 className="text-base font-semibold text-gray-900 text-center text-green-500">
12
+ All Major
13
  </h3>
14
+ <MajorChart />
15
  </Card>
16
  <Card className="p-6 border-2">
17
  <h3 className="text-base font-semibold text-gray-900 text-center text-green-500">
18
+ All University
19
  </h3>
20
  <UniversityBar />
21
  </Card>
src/components/dashboard/charts/major-chart.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { authFetch } from '@/lib/api';
4
+ import { useQuery } from '@tanstack/react-query';
5
+ import { ChartPie } from '../../ui/ChartPie';
6
+
7
+ export function MajorChart() {
8
+ const fetchMajor = async (): Promise<{
9
+ name: string
10
+ value: number
11
+ color: string
12
+ }[]> => {
13
+ const res = await authFetch(`/api/cv-profile/charts/major`)
14
+ if (!res.ok) throw new Error("Failed to fetch major")
15
+ return res.json()
16
+ }
17
+
18
+
19
+ const { data = [], isLoading } = useQuery({
20
+ queryKey: ["charts-major"],
21
+ queryFn: () => fetchMajor(),
22
+ staleTime: 0, // always fresh
23
+ placeholderData: (prev) => prev, // keep previous data while loading (no flicker)
24
+ refetchOnWindowFocus: false,
25
+ refetchOnMount: false,
26
+ })
27
+ return (
28
+ <ChartPie loading={isLoading} data={data} />
29
+ );
30
+ }
src/components/dashboard/charts/university-bar.tsx CHANGED
@@ -1,42 +1,33 @@
1
- 'use client';
2
 
3
- import {
4
- BarChart,
5
- Bar,
6
- XAxis,
7
- YAxis,
8
- CartesianGrid,
9
- Tooltip,
10
- ResponsiveContainer,
11
- Cell,
12
- } from 'recharts';
13
-
14
- const data = [
15
- { name: 'Universitas Indonesia', value: 1200 },
16
- { name: 'Institut Teknologi Bandung', value: 1100 },
17
- { name: 'Binus University', value: 950 },
18
- { name: 'Universitas Diponegoro', value: 850 },
19
- { name: 'Universitas Gadah Mada', value: 800 },
20
- ];
21
 
22
  export function UniversityBar() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  return (
24
- <ResponsiveContainer width="100%" height={300}>
25
- <BarChart
26
- data={data}
27
- layout="vertical"
28
- margin={{ top: 5, right: 30, bottom: 5 }}
29
- >
30
- <CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
31
- <XAxis type="number" />
32
- <YAxis dataKey="name" type="category" width={195} tick={{ fontSize: 12 }} />
33
- <Tooltip />
34
- <Bar dataKey="value" fill="#22c55e" radius={[0, 8, 8, 0]}>
35
- {data.map((_, index) => (
36
- <Cell key={`cell-${index}`} fill="#22c55e" />
37
- ))}
38
- </Bar>
39
- </BarChart>
40
- </ResponsiveContainer>
41
- );
42
- }
 
1
+ 'use client'
2
 
3
+ import { authFetch } from '@/lib/api'
4
+ import { useQuery } from '@tanstack/react-query'
5
+ import { ChartBar } from '../../ui/ChartBar'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  export function UniversityBar() {
8
+ const fetchScore = async (): Promise<
9
+ {
10
+ name: string
11
+ value: number
12
+ color: string
13
+ }[]
14
+ > => {
15
+ const res = await authFetch(`/api/cv-profile/charts/university`)
16
+ if (!res.ok) throw new Error('Failed to fetch score')
17
+ return res.json()
18
+ }
19
+
20
+ const { data = [], isLoading } = useQuery({
21
+ queryKey: ['charts-university'],
22
+ queryFn: fetchScore,
23
+ staleTime: 0,
24
+ placeholderData: (prev) => prev,
25
+ refetchOnWindowFocus: false,
26
+ refetchOnMount: false,
27
+ })
28
+
29
+
30
  return (
31
+ <ChartBar loading={isLoading} data={data} />
32
+ )
33
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/components/ui/ChartBar.tsx ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { ChartData } from '@/types/chart'
4
+ import {
5
+ Bar,
6
+ BarChart,
7
+ CartesianGrid,
8
+ Cell,
9
+ ResponsiveContainer,
10
+ Tooltip,
11
+ XAxis,
12
+ YAxis,
13
+ } from 'recharts'
14
+
15
+ type ChartBarProps = {
16
+ loading: boolean
17
+ data: ChartData[]
18
+ }
19
+
20
+ export function ChartBar({
21
+ loading,
22
+ data
23
+ }: ChartBarProps) {
24
+ const rows = 8
25
+ if (loading) return (
26
+ <div
27
+ className="w-full"
28
+ style={{ height: rows * 40 }}
29
+ >
30
+ <div className="flex flex-col gap-4 animate-pulse">
31
+ {Array.from({ length: rows }).map((_, index) => (
32
+ <div key={index} className="flex items-center gap-4">
33
+ {/* Fake Y label */}
34
+ <div className="h-4 bg-gray-200 rounded w-48" />
35
+
36
+ {/* Fake Bar */}
37
+ <div
38
+ className="h-5 bg-gray-200 rounded"
39
+ style={{
40
+ width: `${40 + Math.random() * 50}%`,
41
+ }}
42
+ />
43
+ </div>
44
+ ))}
45
+ </div>
46
+ </div>
47
+ )
48
+
49
+ return (
50
+ <ResponsiveContainer
51
+ width="100%"
52
+ height={data.length * 40} // 40px per row
53
+ >
54
+ <BarChart
55
+ data={data}
56
+ layout="vertical"
57
+ margin={{ top: 5, right: 30, bottom: 5 }}
58
+ >
59
+ <CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
60
+ <XAxis type="number" />
61
+ <YAxis
62
+ dataKey="name"
63
+ type="category"
64
+ width={220}
65
+ tickFormatter={(value: string) =>
66
+ value.length > 25 ? value.slice(0, 25) + '...' : value
67
+ }
68
+ />
69
+ <Tooltip />
70
+
71
+ <Bar dataKey="value" radius={[0, 8, 8, 0]}>
72
+ {data.map((entry, index) => (
73
+ <Cell key={`cell-${index}`} fill={entry.color} />
74
+ ))}
75
+ </Bar>
76
+ </BarChart>
77
+ </ResponsiveContainer>
78
+ )
79
+ }
src/components/ui/ChartPie.tsx ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { ChartData } from '@/types/chart';
4
+ import {
5
+ Cell,
6
+ Legend,
7
+ Pie,
8
+ PieChart,
9
+ ResponsiveContainer,
10
+ Tooltip,
11
+ } from 'recharts';
12
+
13
+ type ChartPieProps = {
14
+ loading: boolean
15
+ data: ChartData[]
16
+ }
17
+
18
+ export function ChartPie({
19
+ loading,
20
+ data
21
+ }: ChartPieProps) {
22
+ const items = 5
23
+ if (loading) return (
24
+ <div className="w-full flex items-center justify-center gap-10 animate-pulse">
25
+
26
+ {/* Fake Pie Circle */}
27
+ <div className="relative">
28
+ <div className="w-48 h-48 bg-gray-200 rounded-full" />
29
+ <div className="absolute inset-6 bg-white rounded-full" />
30
+ </div>
31
+
32
+ {/* Fake Legend */}
33
+ <div className="flex flex-col gap-4">
34
+ {Array.from({ length: items }).map((_, index) => (
35
+ <div key={index} className="flex items-center gap-3">
36
+ <div className="w-4 h-4 bg-gray-200 rounded-sm" />
37
+ <div className="h-4 bg-gray-200 rounded w-32" />
38
+ </div>
39
+ ))}
40
+ </div>
41
+
42
+ </div>
43
+ )
44
+ return (
45
+ <ResponsiveContainer
46
+ width="100%"
47
+ height={data.length * 40} // 40px per row
48
+ >
49
+ <PieChart>
50
+ <Pie
51
+ data={data}
52
+ dataKey="value"
53
+ >
54
+ {data.map((entry, index) => (
55
+ <Cell key={`cell-${index}`} fill={entry.color} />
56
+ ))}
57
+ </Pie>
58
+ <Tooltip formatter={(value) => `${value}%`} />
59
+ <Legend />
60
+ </PieChart>
61
+ </ResponsiveContainer>
62
+ );
63
+ }
src/types/chart.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export type ChartData = {
2
+ name: string;
3
+ value: number;
4
+ color: string;
5
+ };