Yvonne Priscilla commited on
Commit
1d1e96d
Β·
1 Parent(s): 3253dc0

update auth when token invalid and fix pie chart & change UI for metric

Browse files
src/app/recruitment/layout.tsx CHANGED
@@ -1,66 +1,65 @@
1
  // app/recruitment/layout.tsx
 
2
  import { Header } from '@/components/dashboard/header';
3
  import { HeaderMenu } from '@/components/dashboard/header-menu';
4
- import { cookies } from 'next/headers';
5
- import { redirect } from 'next/navigation';
 
6
 
7
- async function getUser() {
8
- console.log('🟑 [Layout] Checking authentication...');
9
-
10
- const cookieStore = await cookies();
11
- const token = cookieStore.get('auth_token')?.value;
12
-
13
- console.log('🟑 [Layout] Token exists:', token ? 'YES' : 'NO');
14
- console.log('🟑 [Layout] Token value:', token ? token.substring(0, 20) + '...' : 'none');
15
-
16
- if (!token) {
17
- console.log('🟑 [Layout] No token found, returning null');
18
- return null;
19
- }
20
-
21
- try {
22
- console.log('🟑 [Layout] Verifying token with backend...');
23
-
24
- const response = await fetch(
25
- 'https://byteriot-candidateexplorer.hf.space/CandidateExplorer/admin/me',
26
- {
27
- headers: { Authorization: `Bearer ${token}` },
28
- cache: 'no-store',
29
- }
30
- );
31
-
32
- console.log('🟑 [Layout] Backend response status:', response.status);
33
-
34
- if (!response.ok) {
35
- console.log('🟑 [Layout] Token invalid, backend returned:', response.status);
36
- return null;
37
- }
38
-
39
- const userData = await response.json();
40
- console.log('🟑 [Layout] User authenticated:', userData);
41
- return userData;
42
-
43
- } catch (error) {
44
- console.error('🟑 [Layout] Error fetching user:', error);
45
- return null;
46
- }
47
  }
48
 
49
- export default async function RecruitmentLayout({
50
  children,
51
  }: {
52
  children: React.ReactNode;
53
  }) {
54
- console.log('🟑 [Layout] Layout rendering...');
55
-
56
- const user = await getUser();
57
 
58
- if (!user) {
59
- console.log('🟑 [Layout] No user, redirecting to login');
60
- redirect('/login');
 
 
 
 
 
61
  }
62
 
63
- console.log('🟑 [Layout] User found, rendering layout');
 
 
 
64
 
65
  return (
66
  <div className="flex h-screen bg-background">
 
1
  // app/recruitment/layout.tsx
2
+ "use client"
3
  import { Header } from '@/components/dashboard/header';
4
  import { HeaderMenu } from '@/components/dashboard/header-menu';
5
+ import { useAuth } from '@/composables/useAuth';
6
+ import { useRouter } from 'next/navigation';
7
+ import { useEffect } from 'react';
8
 
9
+ // Loading Component
10
+ function AuthLoadingScreen() {
11
+ return (
12
+ <div className="flex h-screen w-full items-center justify-center bg-gradient-to-br from-gray-50 to-gray-100">
13
+ <div className="text-center space-y-6">
14
+ {/* Spinner */}
15
+ <div className="relative mx-auto w-20 h-20">
16
+ <div className="absolute inset-0 rounded-full border-4 border-blue-200"></div>
17
+ <div className="absolute inset-0 rounded-full border-4 border-transparent border-t-blue-600 animate-spin"></div>
18
+ </div>
19
+
20
+ {/* Text */}
21
+ <div className="space-y-2">
22
+ <h2 className="text-xl font-semibold text-gray-800">
23
+ Loading...
24
+ </h2>
25
+ <p className="text-sm text-gray-500">
26
+ Please wait while we verify your credentials
27
+ </p>
28
+ </div>
29
+
30
+ {/* Dots animation */}
31
+ <div className="flex justify-center gap-2">
32
+ <div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
33
+ <div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
34
+ <div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ );
 
 
 
 
 
 
 
 
 
 
39
  }
40
 
41
+ export default function RecruitmentLayout({
42
  children,
43
  }: {
44
  children: React.ReactNode;
45
  }) {
46
+ const { isLoadingUser, isAuthenticated, logout } = useAuth();
47
+ const router = useRouter();
 
48
 
49
+ useEffect(() => {
50
+ if (!isLoadingUser && !isAuthenticated) {
51
+ router.replace("/login");
52
+ }
53
+ }, [isLoadingUser, isAuthenticated, router]);
54
+
55
+ if (isLoadingUser) {
56
+ return <AuthLoadingScreen />;
57
  }
58
 
59
+ if (!isAuthenticated) {
60
+ logout();
61
+ return null;
62
+ }
63
 
64
  return (
65
  <div className="flex h-screen bg-background">
src/components/dashboard/metrics-row.tsx CHANGED
@@ -1,6 +1,7 @@
 
1
  "use client";
2
 
3
- import { MetricsCard } from "@/components/ui/metrics-card";
4
  import { authFetch } from "@/lib/api";
5
  import { useQuery } from "@tanstack/react-query";
6
 
@@ -20,6 +21,43 @@ const fallbackScore: ScoreData = {
20
  }
21
  }
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  export function MetricsRow() {
24
 
25
  const fetchScore = async (): Promise<ScoreData> => {
@@ -31,27 +69,94 @@ export function MetricsRow() {
31
  const { data, isLoading: loadingScore, isError } = useQuery({
32
  queryKey: ["score-data"],
33
  queryFn: () => fetchScore(),
34
- staleTime: 0, // always fresh
35
- placeholderData: (prev) => prev, // keep previous data while loading (no flicker)
36
  refetchOnWindowFocus: false,
37
  refetchOnMount: false,
38
  })
39
 
40
  const scoreData = isError ? fallbackScore : data ?? fallbackScore
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  return (
43
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
44
- <MetricsCard loading={loadingScore} label="Extracted Profile" value={`${scoreData?.data.total_extracted}`} />
45
- <MetricsCard loading={loadingScore} label="Processed" value={`${scoreData?.data.percent_extracted}%`}>
46
- <div className="grid grid-cols-2 gap-5">
47
- <div className="text-gray-500">Processed</div>
48
- <div className="font-bold text-gray-900">{scoreData?.data.total_extracted}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  </div>
50
- <div className="grid grid-cols-2 gap-5">
51
- <div className="text-gray-500">Total Profile</div>
52
- <div className="font-bold text-gray-900">{scoreData?.data.total_file}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  </div>
54
- </MetricsCard>
55
  </div>
56
  );
57
- }
 
1
+ // components/dashboard/metrics-row.tsx
2
  "use client";
3
 
4
+ import { Card } from "@/components/ui/card";
5
  import { authFetch } from "@/lib/api";
6
  import { useQuery } from "@tanstack/react-query";
7
 
 
21
  }
22
  }
23
 
24
+ function CircularProgress({ percentage }: { percentage: number }) {
25
+ // Format to max 2 decimal places
26
+ const formattedPercentage = Number(percentage.toFixed(2));
27
+
28
+ return (
29
+ <div className="relative w-24 h-24 flex-shrink-0">
30
+ <svg className="w-full h-full transform -rotate-90">
31
+ <circle
32
+ cx="48"
33
+ cy="48"
34
+ r="40"
35
+ stroke="#E5E7EB"
36
+ strokeWidth="8"
37
+ fill="none"
38
+ />
39
+ <circle
40
+ cx="48"
41
+ cy="48"
42
+ r="40"
43
+ stroke="#10B981"
44
+ strokeWidth="8"
45
+ fill="none"
46
+ strokeDasharray={`${2 * Math.PI * 40}`}
47
+ strokeDashoffset={`${2 * Math.PI * 40 * (1 - percentage / 100)}`}
48
+ strokeLinecap="round"
49
+ className="transition-all duration-500"
50
+ />
51
+ </svg>
52
+ <div className="absolute inset-0 flex items-center justify-center">
53
+ <span className="text-xl font-bold text-gray-700">
54
+ {formattedPercentage}%
55
+ </span>
56
+ </div>
57
+ </div>
58
+ );
59
+ }
60
+
61
  export function MetricsRow() {
62
 
63
  const fetchScore = async (): Promise<ScoreData> => {
 
69
  const { data, isLoading: loadingScore, isError } = useQuery({
70
  queryKey: ["score-data"],
71
  queryFn: () => fetchScore(),
72
+ staleTime: 0,
73
+ placeholderData: (prev) => prev,
74
  refetchOnWindowFocus: false,
75
  refetchOnMount: false,
76
  })
77
 
78
  const scoreData = isError ? fallbackScore : data ?? fallbackScore
79
 
80
+ // Format percentage to max 2 decimal places
81
+ const formattedPercentage = Number(scoreData.data.percent_extracted.toFixed(2));
82
+
83
+ const lastProcessed = new Date().toLocaleString('en-US', {
84
+ weekday: 'short',
85
+ month: 'short',
86
+ day: '2-digit',
87
+ year: 'numeric',
88
+ hour: '2-digit',
89
+ minute: '2-digit',
90
+ second: '2-digit',
91
+ hour12: true
92
+ });
93
+
94
+ if (loadingScore) {
95
+ return (
96
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
97
+ {[1, 2].map((i) => (
98
+ <Card key={i} className="p-6 border bg-white animate-pulse">
99
+ <div className="flex items-start gap-4">
100
+ <div className="w-24 h-24 rounded-full bg-gray-200" />
101
+ <div className="flex-1 space-y-3">
102
+ <div className="h-6 bg-gray-200 rounded w-40" />
103
+ <div className="h-8 bg-gray-200 rounded w-24" />
104
+ <div className="h-4 bg-gray-200 rounded w-full" />
105
+ </div>
106
+ </div>
107
+ </Card>
108
+ ))}
109
+ </div>
110
+ );
111
+ }
112
+
113
  return (
114
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
115
+ {/* Extracted Profile Card */}
116
+ <Card className="p-6 border bg-white hover:shadow-md transition-shadow">
117
+ <div className="flex items-start gap-4">
118
+ <div className="flex-1">
119
+ <div className="flex items-center gap-3 mb-3">
120
+ <span className="inline-flex items-center px-3 py-1.5 rounded-full text-sm font-semibold bg-green-100 text-green-800">
121
+ Extracted Profile
122
+ </span>
123
+ <span className="text-3xl font-bold text-gray-900">
124
+ {scoreData.data.total_extracted}
125
+ </span>
126
+ </div>
127
+ <div className="flex items-center gap-2 text-sm text-gray-600">
128
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
129
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
130
+ </svg>
131
+ <span>
132
+ Active Candidates: <strong>{scoreData.data.total_extracted}</strong> / Total Profiles: <strong>{scoreData.data.total_file}</strong>
133
+ </span>
134
+ </div>
135
+ </div>
136
  </div>
137
+ </Card>
138
+
139
+ {/* Processed Card */}
140
+ <Card className="p-6 border bg-white hover:shadow-md transition-shadow">
141
+ <div className="flex items-start gap-6">
142
+ <CircularProgress percentage={formattedPercentage} />
143
+
144
+ <div className="flex-1">
145
+ <div className="mb-4">
146
+ <div className="text-3xl font-bold text-green-600 mb-1">
147
+ {formattedPercentage}%
148
+ </div>
149
+ <div className="text-lg font-semibold text-gray-900">
150
+ Total {scoreData.data.total_file}
151
+ </div>
152
+ </div>
153
+
154
+ <div className="text-xs text-gray-500">
155
+ Last Processed: {lastProcessed}
156
+ </div>
157
+ </div>
158
  </div>
159
+ </Card>
160
  </div>
161
  );
162
+ }
src/components/ui/ChartPie.tsx CHANGED
@@ -15,16 +15,20 @@ type ChartPieProps = {
15
  data: ChartData[]
16
  }
17
 
18
- export function ChartPie({
19
- loading,
20
- data
21
- }: ChartPieProps) {
22
 
23
- const items = 5
 
 
 
 
 
 
 
 
 
24
 
25
- // -----------------------
26
- // πŸ”Ή LOADING STATE
27
- // -----------------------
28
  if (loading) {
29
  return (
30
  <div className="w-full flex items-center justify-center gap-10 animate-pulse">
@@ -32,7 +36,6 @@ export function ChartPie({
32
  <div className="w-48 h-48 bg-gray-200 rounded-full" />
33
  <div className="absolute inset-6 bg-white rounded-full" />
34
  </div>
35
-
36
  <div className="flex flex-col gap-4">
37
  {Array.from({ length: items }).map((_, index) => (
38
  <div key={index} className="flex items-center gap-3">
@@ -42,42 +45,50 @@ export function ChartPie({
42
  ))}
43
  </div>
44
  </div>
45
- )
46
  }
47
 
48
- // -----------------------
49
- // πŸ”Ή EMPTY STATE
50
- // -----------------------
51
  if (!data || data.length === 0) {
52
  return (
53
  <div className="w-full h-60 flex flex-col items-center justify-center text-gray-400">
54
  <div className="w-40 h-40 border-4 border-dashed border-gray-200 rounded-full mb-4" />
55
  <p className="text-sm">No data available</p>
56
  </div>
57
- )
58
  }
59
 
60
- // -----------------------
61
- // πŸ”Ή CHART
62
- // -----------------------
63
  return (
64
- <ResponsiveContainer
65
- width="100%"
66
- height={Math.max(250, data.length * 40)}
67
- >
68
- <PieChart>
69
- <Pie
70
- data={data}
71
- dataKey="value"
72
- >
73
- {data.map((entry, index) => (
74
- <Cell key={`cell-${index}`} fill={entry.color} />
75
- ))}
76
- </Pie>
77
 
78
- <Tooltip formatter={(value) => `${value}%`} />
79
- <Legend />
80
- </PieChart>
81
- </ResponsiveContainer>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  );
83
  }
 
15
  data: ChartData[]
16
  }
17
 
18
+ export function ChartPie({ loading, data }: ChartPieProps) {
19
+ const items = 5;
 
 
20
 
21
+ // Calculate percentages
22
+ const dataWithPercentages = data ? (() => {
23
+ const total = data.reduce((sum, item) => sum + item.value, 0);
24
+ return data.map(item => ({
25
+ ...item,
26
+ percentage: total > 0 ? Number(((item.value / total) * 100).toFixed(1)) : 0
27
+ }));
28
+ })() : [];
29
+
30
+ const total = dataWithPercentages.reduce((sum, item) => sum + item.value, 0);
31
 
 
 
 
32
  if (loading) {
33
  return (
34
  <div className="w-full flex items-center justify-center gap-10 animate-pulse">
 
36
  <div className="w-48 h-48 bg-gray-200 rounded-full" />
37
  <div className="absolute inset-6 bg-white rounded-full" />
38
  </div>
 
39
  <div className="flex flex-col gap-4">
40
  {Array.from({ length: items }).map((_, index) => (
41
  <div key={index} className="flex items-center gap-3">
 
45
  ))}
46
  </div>
47
  </div>
48
+ );
49
  }
50
 
 
 
 
51
  if (!data || data.length === 0) {
52
  return (
53
  <div className="w-full h-60 flex flex-col items-center justify-center text-gray-400">
54
  <div className="w-40 h-40 border-4 border-dashed border-gray-200 rounded-full mb-4" />
55
  <p className="text-sm">No data available</p>
56
  </div>
57
+ );
58
  }
59
 
 
 
 
60
  return (
61
+ <div className="space-y-4">
62
+ {/* Total Count */}
63
+ <div className="text-center">
64
+ <p className="text-sm text-gray-600">Total</p>
65
+ <p className="text-2xl font-bold text-gray-800">{total}</p>
66
+ </div>
 
 
 
 
 
 
 
67
 
68
+ <ResponsiveContainer width="100%" height={Math.max(250, dataWithPercentages.length * 40)}>
69
+ <PieChart>
70
+ <Pie
71
+ data={dataWithPercentages}
72
+ dataKey="value"
73
+ label={({ percentage }) => `${percentage}%`}
74
+ >
75
+ {dataWithPercentages.map((entry, index) => (
76
+ <Cell key={`cell-${index}`} fill={entry.color} />
77
+ ))}
78
+ </Pie>
79
+ <Tooltip
80
+ formatter={(value: number, name: string, props: any) => [
81
+ `${value} (${props.payload.percentage}%)`,
82
+ name
83
+ ]}
84
+ />
85
+ <Legend
86
+ formatter={(value: string, entry: any) =>
87
+ `${value}: ${entry.payload.value} (${entry.payload.percentage}%)`
88
+ }
89
+ />
90
+ </PieChart>
91
+ </ResponsiveContainer>
92
+ </div>
93
  );
94
  }