Yvonne Priscilla commited on
Commit
934218a
·
1 Parent(s): fb2a8ad
src/app/api/cv-profile/route.ts CHANGED
@@ -24,6 +24,18 @@ async function getMatchingArrayValues(
24
  .filter((v) => v.toLowerCase().includes(search.toLowerCase()));
25
  }
26
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  export async function GET(request: NextRequest) {
28
  const { searchParams } = new URL(request.url);
29
 
@@ -37,11 +49,16 @@ export async function GET(request: NextRequest) {
37
 
38
  // --- CRITERIA ---
39
  const criteria_id = searchParams.get("criteria_id");
40
-
41
  if (criteria_id && !UUID_REGEX.test(criteria_id)) {
42
  return NextResponse.json({ error: "Invalid criteria_id format" }, { status: 400 });
43
  }
44
 
 
 
 
 
 
 
45
  // --- FILTERS ---
46
  const domicile = searchParams.get("domicile");
47
  const yoe = searchParams.get("yoe");
@@ -49,7 +66,6 @@ export async function GET(request: NextRequest) {
49
  const hardskills = searchParams.getAll("hardskills");
50
  const certifications = searchParams.getAll("certifications");
51
  const business_domain = searchParams.getAll("business_domain");
52
-
53
  const univ_edu_1 = searchParams.get("univ_edu_1");
54
  const univ_edu_2 = searchParams.get("univ_edu_2");
55
  const univ_edu_3 = searchParams.get("univ_edu_3");
@@ -71,69 +87,77 @@ export async function GET(request: NextRequest) {
71
  ];
72
 
73
  const isScoreSort = sortBy === "score" && !!criteria_id;
74
-
75
  const orderBy = !isScoreSort && allowedSortFields.includes(sortBy)
76
  ? { [sortBy]: sortOrder }
77
  : { created_at: "desc" as const };
78
 
79
- // --- PARALLEL ARRAY SEARCH (only when search is provided) ---
80
- const [matchingHardskills, matchingSoftskills, matchingCertifications, matchingBusinessDomain] =
81
- search
82
- ? await Promise.all([
83
- getMatchingArrayValues("hardskills", search, prisma),
84
- getMatchingArrayValues("softskills", search, prisma),
85
- getMatchingArrayValues("certifications", search, prisma),
86
- getMatchingArrayValues("business_domain", search, prisma),
87
- ])
88
- : [[], [], [], []];
89
-
90
- // --- BUILD WHERE ---
91
- const where: any = {
92
- ...(search ? {
93
- OR: [
94
- { fullname: { contains: search, mode: "insensitive" } },
95
- { domicile: { contains: search, mode: "insensitive" } },
96
- { univ_edu_1: { contains: search, mode: "insensitive" } },
97
- { univ_edu_2: { contains: search, mode: "insensitive" } },
98
- { univ_edu_3: { contains: search, mode: "insensitive" } },
99
- { major_edu_1: { contains: search, mode: "insensitive" } },
100
- { major_edu_2: { contains: search, mode: "insensitive" } },
101
- { major_edu_3: { contains: search, mode: "insensitive" } },
102
- { filename: { contains: search, mode: "insensitive" } },
103
- // ✅ Guard against empty arrays which cause Prisma errors
104
- ...(matchingHardskills.length > 0 ? [{ hardskills: { hasSome: matchingHardskills } }] : []),
105
- ...(matchingSoftskills.length > 0 ? [{ softskills: { hasSome: matchingSoftskills } }] : []),
106
- ...(matchingCertifications.length > 0 ? [{ certifications: { hasSome: matchingCertifications } }] : []),
107
- ...(matchingBusinessDomain.length > 0 ? [{ business_domain: { hasSome: matchingBusinessDomain } }] : []),
108
- ...(Number.isNaN(Number.parseFloat(search)) ? [] : [
109
- { gpa_edu_1: { equals: Number.parseFloat(search) } },
110
- { gpa_edu_2: { equals: Number.parseFloat(search) } },
111
- { gpa_edu_3: { equals: Number.parseFloat(search) } },
112
- ]),
113
- ...(Number.isNaN(Number.parseInt(search)) ? [] : [
114
- { yoe: { equals: Number.parseInt(search) } },
115
- ]),
116
- ],
117
- } : {}),
118
-
119
- ...(domicile && { domicile }),
120
- ...(yoe && { yoe: { gte: Number.parseInt(yoe) } }),
121
- ...(softskills.length > 0 && { softskills: { hasSome: softskills } }),
122
- ...(hardskills.length > 0 && { hardskills: { hasSome: hardskills } }),
123
- ...(certifications.length > 0 && { certifications: { hasSome: certifications } }),
124
- ...(business_domain.length > 0 && { business_domain: { hasSome: business_domain } }),
125
- ...(univ_edu_1 && { univ_edu_1 }),
126
- ...(major_edu_1 && { major_edu_1 }),
127
- ...(gpa_1 && { gpa_edu_1: { gte: Number.parseFloat(gpa_1) } }),
128
- ...(univ_edu_2 && { univ_edu_2 }),
129
- ...(major_edu_2 && { major_edu_2 }),
130
- ...(gpa_2 && { gpa_edu_2: { gte: Number.parseFloat(gpa_2) } }),
131
- ...(univ_edu_3 && { univ_edu_3 }),
132
- ...(major_edu_3 && { major_edu_3 }),
133
- ...(gpa_3 && { gpa_edu_3: { gte: Number.parseFloat(gpa_3) } }),
134
- };
135
-
136
  try {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  // --- RESOLVE SCORE MAP ---
138
  const scoreMap = new Map<string, number | null>();
139
 
@@ -142,17 +166,15 @@ export async function GET(request: NextRequest) {
142
  where: { criteria_id },
143
  select: { weight_id: true },
144
  });
 
145
  if (weight) {
146
  const matchings = await prisma.cv_matching.findMany({
147
  where: { weight_id: weight.weight_id },
148
  select: { matching_id: true, profile_id: true },
149
  });
150
 
151
- console.log(matchings)
152
-
153
  if (matchings.length > 0) {
154
  const matchingIds = matchings.map((m) => m.matching_id);
155
-
156
  const scores = await prisma.cv_score.findMany({
157
  where: { matching_id: { in: matchingIds } },
158
  select: { matching_id: true, profile_id: true, score: true },
@@ -164,9 +186,7 @@ export async function GET(request: NextRequest) {
164
 
165
  for (const s of scores) {
166
  const profileId = s.profile_id ?? matchingToProfile.get(s.matching_id ?? "");
167
- if (profileId) {
168
- scoreMap.set(profileId, s.score ?? null);
169
- }
170
  }
171
  }
172
  }
@@ -182,7 +202,7 @@ export async function GET(request: NextRequest) {
182
  prisma.cv_profile.count({ where }),
183
  ]);
184
 
185
- // --- EARLY RETURN IF NO criteria_id (no score enrichment needed) ---
186
  if (!criteria_id) {
187
  return NextResponse.json({
188
  data: profiles,
@@ -240,4 +260,4 @@ export async function GET(request: NextRequest) {
240
  console.error(error);
241
  return NextResponse.json({ error: "Failed to fetch profiles" }, { status: 500 });
242
  }
243
- }
 
24
  .filter((v) => v.toLowerCase().includes(search.toLowerCase()));
25
  }
26
 
27
+ // Resolve all profile_ids that belong to a given user_id
28
+ // cv_profile.file_id → cv_file.file_id → cv_file.user_id
29
+ async function getProfileIdsByUserId(user_id: string): Promise<string[]> {
30
+ const rows = await prisma.$queryRaw<{ profile_id: string }[]>`
31
+ SELECT p.profile_id
32
+ FROM cv_profile p
33
+ INNER JOIN cv_file f ON f.file_id = p.file_id
34
+ WHERE f.user_id = ${user_id}::uuid
35
+ `;
36
+ return rows.map((r) => r.profile_id);
37
+ }
38
+
39
  export async function GET(request: NextRequest) {
40
  const { searchParams } = new URL(request.url);
41
 
 
49
 
50
  // --- CRITERIA ---
51
  const criteria_id = searchParams.get("criteria_id");
 
52
  if (criteria_id && !UUID_REGEX.test(criteria_id)) {
53
  return NextResponse.json({ error: "Invalid criteria_id format" }, { status: 400 });
54
  }
55
 
56
+ // --- USER FILTER ---
57
+ const user_id = searchParams.get("user_id");
58
+ if (user_id && !UUID_REGEX.test(user_id)) {
59
+ return NextResponse.json({ error: "Invalid user_id format" }, { status: 400 });
60
+ }
61
+
62
  // --- FILTERS ---
63
  const domicile = searchParams.get("domicile");
64
  const yoe = searchParams.get("yoe");
 
66
  const hardskills = searchParams.getAll("hardskills");
67
  const certifications = searchParams.getAll("certifications");
68
  const business_domain = searchParams.getAll("business_domain");
 
69
  const univ_edu_1 = searchParams.get("univ_edu_1");
70
  const univ_edu_2 = searchParams.get("univ_edu_2");
71
  const univ_edu_3 = searchParams.get("univ_edu_3");
 
87
  ];
88
 
89
  const isScoreSort = sortBy === "score" && !!criteria_id;
 
90
  const orderBy = !isScoreSort && allowedSortFields.includes(sortBy)
91
  ? { [sortBy]: sortOrder }
92
  : { created_at: "desc" as const };
93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  try {
95
+ // --- RESOLVE user_id → profile_ids FILTER ---
96
+ // Run in parallel with array search since both are independent
97
+ const [
98
+ matchingHardskills,
99
+ matchingSoftskills,
100
+ matchingCertifications,
101
+ matchingBusinessDomain,
102
+ userProfileIds,
103
+ ] = await Promise.all([
104
+ search ? getMatchingArrayValues("hardskills", search, prisma) : Promise.resolve([]),
105
+ search ? getMatchingArrayValues("softskills", search, prisma) : Promise.resolve([]),
106
+ search ? getMatchingArrayValues("certifications", search, prisma) : Promise.resolve([]),
107
+ search ? getMatchingArrayValues("business_domain", search, prisma) : Promise.resolve([]),
108
+ user_id ? getProfileIdsByUserId(user_id) : Promise.resolve(null), // null = no filter
109
+ ]);
110
+
111
+ // --- BUILD WHERE ---
112
+ const where: any = {
113
+ // Filter by user's profiles if user_id is provided
114
+ ...(userProfileIds !== null && {
115
+ profile_id: { in: userProfileIds },
116
+ }),
117
+
118
+ ...(search ? {
119
+ OR: [
120
+ { fullname: { contains: search, mode: "insensitive" } },
121
+ { domicile: { contains: search, mode: "insensitive" } },
122
+ { univ_edu_1: { contains: search, mode: "insensitive" } },
123
+ { univ_edu_2: { contains: search, mode: "insensitive" } },
124
+ { univ_edu_3: { contains: search, mode: "insensitive" } },
125
+ { major_edu_1: { contains: search, mode: "insensitive" } },
126
+ { major_edu_2: { contains: search, mode: "insensitive" } },
127
+ { major_edu_3: { contains: search, mode: "insensitive" } },
128
+ { filename: { contains: search, mode: "insensitive" } },
129
+ ...(matchingHardskills.length > 0 ? [{ hardskills: { hasSome: matchingHardskills } }] : []),
130
+ ...(matchingSoftskills.length > 0 ? [{ softskills: { hasSome: matchingSoftskills } }] : []),
131
+ ...(matchingCertifications.length > 0 ? [{ certifications: { hasSome: matchingCertifications } }] : []),
132
+ ...(matchingBusinessDomain.length > 0 ? [{ business_domain: { hasSome: matchingBusinessDomain } }] : []),
133
+ ...(Number.isNaN(Number.parseFloat(search)) ? [] : [
134
+ { gpa_edu_1: { equals: Number.parseFloat(search) } },
135
+ { gpa_edu_2: { equals: Number.parseFloat(search) } },
136
+ { gpa_edu_3: { equals: Number.parseFloat(search) } },
137
+ ]),
138
+ ...(Number.isNaN(Number.parseInt(search)) ? [] : [
139
+ { yoe: { equals: Number.parseInt(search) } },
140
+ ]),
141
+ ],
142
+ } : {}),
143
+
144
+ ...(domicile && { domicile }),
145
+ ...(yoe && { yoe: { gte: Number.parseInt(yoe) } }),
146
+ ...(softskills.length > 0 && { softskills: { hasSome: softskills } }),
147
+ ...(hardskills.length > 0 && { hardskills: { hasSome: hardskills } }),
148
+ ...(certifications.length > 0 && { certifications: { hasSome: certifications } }),
149
+ ...(business_domain.length > 0 && { business_domain: { hasSome: business_domain } }),
150
+ ...(univ_edu_1 && { univ_edu_1 }),
151
+ ...(major_edu_1 && { major_edu_1 }),
152
+ ...(gpa_1 && { gpa_edu_1: { gte: Number.parseFloat(gpa_1) } }),
153
+ ...(univ_edu_2 && { univ_edu_2 }),
154
+ ...(major_edu_2 && { major_edu_2 }),
155
+ ...(gpa_2 && { gpa_edu_2: { gte: Number.parseFloat(gpa_2) } }),
156
+ ...(univ_edu_3 && { univ_edu_3 }),
157
+ ...(major_edu_3 && { major_edu_3 }),
158
+ ...(gpa_3 && { gpa_edu_3: { gte: Number.parseFloat(gpa_3) } }),
159
+ };
160
+
161
  // --- RESOLVE SCORE MAP ---
162
  const scoreMap = new Map<string, number | null>();
163
 
 
166
  where: { criteria_id },
167
  select: { weight_id: true },
168
  });
169
+
170
  if (weight) {
171
  const matchings = await prisma.cv_matching.findMany({
172
  where: { weight_id: weight.weight_id },
173
  select: { matching_id: true, profile_id: true },
174
  });
175
 
 
 
176
  if (matchings.length > 0) {
177
  const matchingIds = matchings.map((m) => m.matching_id);
 
178
  const scores = await prisma.cv_score.findMany({
179
  where: { matching_id: { in: matchingIds } },
180
  select: { matching_id: true, profile_id: true, score: true },
 
186
 
187
  for (const s of scores) {
188
  const profileId = s.profile_id ?? matchingToProfile.get(s.matching_id ?? "");
189
+ if (profileId) scoreMap.set(profileId, s.score ?? null);
 
 
190
  }
191
  }
192
  }
 
202
  prisma.cv_profile.count({ where }),
203
  ]);
204
 
205
+ // --- EARLY RETURN IF NO criteria_id ---
206
  if (!criteria_id) {
207
  return NextResponse.json({
208
  data: profiles,
 
260
  console.error(error);
261
  return NextResponse.json({ error: "Failed to fetch profiles" }, { status: 500 });
262
  }
263
+ }
src/components/dashboard/candidates-table.tsx CHANGED
@@ -6,6 +6,7 @@ import { Checkbox } from "@/components/ui/checkbox"
6
  import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
7
  import { Form, FormControl, FormField, FormItem, FormLabel } from "@/components/ui/form"
8
  import { Input } from "@/components/ui/input"
 
9
  import { authFetch } from "@/lib/api"
10
  import { exportCandidatesToCSV } from "@/lib/export-service"
11
  import { createAndCalculateScore } from "@/lib/scoring-service"
@@ -29,8 +30,10 @@ const fetchCandidates = async (
29
  search: string,
30
  sortConfig: { key: string | null; direction: "asc" | "desc" },
31
  filters: FilterFormValues,
32
- criteriaId: string | null
 
33
  ) => {
 
34
  const params = new URLSearchParams()
35
  params.set("page", String(page))
36
  params.set("limit", "10")
@@ -54,6 +57,7 @@ const fetchCandidates = async (
54
  if (filters.certifications) params.append("certifications", filters.certifications)
55
  if (filters.businessDomain) params.append("business_domain", filters.businessDomain)
56
  if (criteriaId) params.append("criteria_id", criteriaId)
 
57
 
58
  const res = await authFetch(`/api/cv-profile?${params}`)
59
  if (!res.ok) throw new Error("Failed to fetch candidates")
@@ -140,13 +144,13 @@ const TableRow = memo(({
140
  {visibleColumns.fullname && <td className="p-3 text-sm">{row.fullname}</td>}
141
  {visibleColumns.univ_edu_1 && <td className="p-3 text-sm">{row.univ_edu_1 ?? "-"}</td>}
142
  {visibleColumns.major_edu_1 && <td className="p-3 text-sm">{row.major_edu_1 ?? "-"}</td>}
143
- {visibleColumns.gpa_edu_1 && <td className="p-3 text-sm">{row.gpa_edu_1 ?? "-"}</td>}
144
  {visibleColumns.univ_edu_2 && <td className="p-3 text-sm">{row.univ_edu_2 ?? "-"}</td>}
145
  {visibleColumns.major_edu_2 && <td className="p-3 text-sm">{row.major_edu_2 ?? "-"}</td>}
146
- {visibleColumns.gpa_edu_2 && <td className="p-3 text-sm">{row.gpa_edu_2 ?? "-"}</td>}
147
  {visibleColumns.univ_edu_3 && <td className="p-3 text-sm">{row.univ_edu_3 ?? "-"}</td>}
148
  {visibleColumns.major_edu_3 && <td className="p-3 text-sm">{row.major_edu_3 ?? "-"}</td>}
149
- {visibleColumns.gpa_edu_3 && <td className="p-3 text-sm">{row.gpa_edu_3 ?? "-"}</td>}
150
  {visibleColumns.domicile && <td className="p-3 text-sm">{row.domicile ?? "-"}</td>}
151
  {visibleColumns.yoe && <td className="p-3 text-sm">{row.yoe ?? "-"}</td>}
152
  {visibleColumns.hardskills && <td className="p-3 text-sm">{row.hardskills?.join(", ") ?? "-"}</td>}
@@ -890,6 +894,7 @@ export default function CandidateTable() {
890
  const [debouncedSearch, setDebouncedSearch] = useState("")
891
  const [currentPage, setCurrentPage] = useState(1)
892
  const [sortConfig, setSortConfig] = useState<{ key: string | null; direction: "asc" | "desc" }>({ key: null, direction: "asc" })
 
893
 
894
  const defaultColumns = allColumns.reduce(
895
  (acc, col) => ({ ...acc, [col.id]: col.visible }),
@@ -935,8 +940,8 @@ export default function CandidateTable() {
935
 
936
  // ✅ Candidates — always fresh, refetches on any param change
937
  const { data: candidatesData, isLoading, refetch } = useQuery({
938
- queryKey: ["cv-profiles", currentPage, debouncedSearch, sortConfig, appliedFilters],
939
- queryFn: () => fetchCandidates(currentPage, debouncedSearch, sortConfig, appliedFilters, criteriaId),
940
  staleTime: 0, // always fresh
941
  placeholderData: (prev) => prev, // keep previous data while loading (no flicker)
942
  refetchOnWindowFocus: false,
 
6
  import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
7
  import { Form, FormControl, FormField, FormItem, FormLabel } from "@/components/ui/form"
8
  import { Input } from "@/components/ui/input"
9
+ import { useUser } from "@/composables/useUser"
10
  import { authFetch } from "@/lib/api"
11
  import { exportCandidatesToCSV } from "@/lib/export-service"
12
  import { createAndCalculateScore } from "@/lib/scoring-service"
 
30
  search: string,
31
  sortConfig: { key: string | null; direction: "asc" | "desc" },
32
  filters: FilterFormValues,
33
+ criteriaId: string | null,
34
+ userId: string
35
  ) => {
36
+ if (!userId) return []
37
  const params = new URLSearchParams()
38
  params.set("page", String(page))
39
  params.set("limit", "10")
 
57
  if (filters.certifications) params.append("certifications", filters.certifications)
58
  if (filters.businessDomain) params.append("business_domain", filters.businessDomain)
59
  if (criteriaId) params.append("criteria_id", criteriaId)
60
+ params.append("user_id", userId)
61
 
62
  const res = await authFetch(`/api/cv-profile?${params}`)
63
  if (!res.ok) throw new Error("Failed to fetch candidates")
 
144
  {visibleColumns.fullname && <td className="p-3 text-sm">{row.fullname}</td>}
145
  {visibleColumns.univ_edu_1 && <td className="p-3 text-sm">{row.univ_edu_1 ?? "-"}</td>}
146
  {visibleColumns.major_edu_1 && <td className="p-3 text-sm">{row.major_edu_1 ?? "-"}</td>}
147
+ {visibleColumns.gpa_edu_1 && <td className="p-3 text-sm">{row.gpa_edu_1?.toFixed(2) ?? "-"}</td>}
148
  {visibleColumns.univ_edu_2 && <td className="p-3 text-sm">{row.univ_edu_2 ?? "-"}</td>}
149
  {visibleColumns.major_edu_2 && <td className="p-3 text-sm">{row.major_edu_2 ?? "-"}</td>}
150
+ {visibleColumns.gpa_edu_2 && <td className="p-3 text-sm">{row.gpa_edu_2?.toFixed(2) ?? "-"}</td>}
151
  {visibleColumns.univ_edu_3 && <td className="p-3 text-sm">{row.univ_edu_3 ?? "-"}</td>}
152
  {visibleColumns.major_edu_3 && <td className="p-3 text-sm">{row.major_edu_3 ?? "-"}</td>}
153
+ {visibleColumns.gpa_edu_3 && <td className="p-3 text-sm">{row.gpa_edu_3?.toFixed(2) ?? "-"}</td>}
154
  {visibleColumns.domicile && <td className="p-3 text-sm">{row.domicile ?? "-"}</td>}
155
  {visibleColumns.yoe && <td className="p-3 text-sm">{row.yoe ?? "-"}</td>}
156
  {visibleColumns.hardskills && <td className="p-3 text-sm">{row.hardskills?.join(", ") ?? "-"}</td>}
 
894
  const [debouncedSearch, setDebouncedSearch] = useState("")
895
  const [currentPage, setCurrentPage] = useState(1)
896
  const [sortConfig, setSortConfig] = useState<{ key: string | null; direction: "asc" | "desc" }>({ key: null, direction: "asc" })
897
+ const { user } = useUser()
898
 
899
  const defaultColumns = allColumns.reduce(
900
  (acc, col) => ({ ...acc, [col.id]: col.visible }),
 
940
 
941
  // ✅ Candidates — always fresh, refetches on any param change
942
  const { data: candidatesData, isLoading, refetch } = useQuery({
943
+ queryKey: ["cv-profiles", currentPage, debouncedSearch, sortConfig, appliedFilters, user],
944
+ queryFn: () => fetchCandidates(currentPage, debouncedSearch, sortConfig, appliedFilters, criteriaId, user?.user_id ?? ""),
945
  staleTime: 0, // always fresh
946
  placeholderData: (prev) => prev, // keep previous data while loading (no flicker)
947
  refetchOnWindowFocus: false,
src/components/dashboard/header.tsx CHANGED
@@ -1,19 +1,7 @@
1
  "use client";
2
 
3
  import { Button } from "@/components/ui/button";
4
- import { authFetch } from "@/lib/api";
5
- import { useQuery } from "@tanstack/react-query";
6
-
7
- interface MeResponse {
8
- user_id: string
9
- username: string
10
- email: string
11
- full_name: string
12
- role: string
13
- is_active: boolean
14
- tenant_id: string
15
- created_at: string
16
- }
17
 
18
  function getInitials(name: string) {
19
  return name
@@ -25,15 +13,7 @@ function getInitials(name: string) {
25
  }
26
 
27
  export function Header() {
28
- const { data: user } = useQuery<MeResponse>({
29
- queryKey: ["me"],
30
- queryFn: async () => {
31
- const res = await authFetch("/api/me")
32
- if (!res.ok) throw new Error("Failed to fetch user")
33
- return res.json()
34
- },
35
- staleTime: 1000 * 60 * 5, // cache 5 minutes
36
- })
37
 
38
  const initials = user ? getInitials(user.full_name) : "?"
39
  const displayName = user?.full_name ?? "Loading..."
 
1
  "use client";
2
 
3
  import { Button } from "@/components/ui/button";
4
+ import { useUser } from "@/composables/useUser";
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  function getInitials(name: string) {
7
  return name
 
13
  }
14
 
15
  export function Header() {
16
+ const { user } = useUser()
 
 
 
 
 
 
 
 
17
 
18
  const initials = user ? getInitials(user.full_name) : "?"
19
  const displayName = user?.full_name ?? "Loading..."
src/components/dashboard/metrics-row.tsx CHANGED
@@ -4,50 +4,54 @@ import { MetricsCard } from "@/components/ui/metrics-card";
4
  import { authFetch } from "@/lib/api";
5
  import { useQuery } from "@tanstack/react-query";
6
 
7
- const BASE_URL = "https://byteriot-candidateexplorer.hf.space/CandidateExplorer/agentic"
8
 
9
  type ScoreData = {
10
- total_file: number
11
- total_extracted: number
12
- percent_extracted: number
 
 
13
  }
14
 
15
  const fallbackScore: ScoreData = {
16
- total_file: 0,
17
- total_extracted: 0,
18
- percent_extracted: 0,
 
 
19
  }
20
 
21
  export function MetricsRow() {
22
 
23
  const fetchScore = async (): Promise<ScoreData> => {
24
- const res = await authFetch(`${BASE_URL}/file/score`)
25
  if (!res.ok) throw new Error("Failed to fetch score")
26
  return res.json()
27
  }
28
 
29
  const { data, isLoading: loadingScore, isError } = useQuery({
30
- queryKey: ["score-data"],
31
- queryFn: () => fetchScore(),
32
- staleTime: 0, // always fresh
33
- placeholderData: (prev) => prev, // keep previous data while loading (no flicker)
34
- refetchOnWindowFocus: false,
35
- refetchOnMount: false,
36
- })
37
 
38
- const scoreData = isError ? fallbackScore : data ?? fallbackScore
39
 
40
  return (
41
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
42
- <MetricsCard loading={loadingScore} label="Extracted Profile" value={`${scoreData?.total_extracted}`} />
43
- <MetricsCard loading={loadingScore} label="Processed" value={`${scoreData?.percent_extracted}%`}>
44
  <div className="grid grid-cols-2 gap-5">
45
  <div className="text-gray-500">Processed</div>
46
- <div className="font-bold text-gray-900">{scoreData?.total_extracted}</div>
47
  </div>
48
- <div className="grid grid-cols-2 gap-5">
49
  <div className="text-gray-500">Total Profile</div>
50
- <div className="font-bold text-gray-900">{scoreData?.total_file}</div>
51
  </div>
52
  </MetricsCard>
53
  </div>
 
4
  import { authFetch } from "@/lib/api";
5
  import { useQuery } from "@tanstack/react-query";
6
 
7
+ const BASE_URL = "https://byteriot-candidateexplorer.hf.space/CandidateExplorer"
8
 
9
  type ScoreData = {
10
+ data: {
11
+ total_file: number
12
+ total_extracted: number
13
+ percent_extracted: number
14
+ }
15
  }
16
 
17
  const fallbackScore: ScoreData = {
18
+ data: {
19
+ total_file: 0,
20
+ total_extracted: 0,
21
+ percent_extracted: 0,
22
+ }
23
  }
24
 
25
  export function MetricsRow() {
26
 
27
  const fetchScore = async (): Promise<ScoreData> => {
28
+ const res = await authFetch(`${BASE_URL}/file/score_card`)
29
  if (!res.ok) throw new Error("Failed to fetch score")
30
  return res.json()
31
  }
32
 
33
  const { data, isLoading: loadingScore, isError } = useQuery({
34
+ queryKey: ["score-data"],
35
+ queryFn: () => fetchScore(),
36
+ staleTime: 0, // always fresh
37
+ placeholderData: (prev) => prev, // keep previous data while loading (no flicker)
38
+ refetchOnWindowFocus: false,
39
+ refetchOnMount: false,
40
+ })
41
 
42
+ const scoreData = isError ? fallbackScore : data ?? fallbackScore
43
 
44
  return (
45
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
46
+ <MetricsCard loading={loadingScore} label="Extracted Profile" value={`${scoreData?.data.total_extracted}`} />
47
+ <MetricsCard loading={loadingScore} label="Processed" value={`${scoreData?.data.percent_extracted}%`}>
48
  <div className="grid grid-cols-2 gap-5">
49
  <div className="text-gray-500">Processed</div>
50
+ <div className="font-bold text-gray-900">{scoreData?.data.total_extracted}</div>
51
  </div>
52
+ <div className="grid grid-cols-2 gap-5">
53
  <div className="text-gray-500">Total Profile</div>
54
+ <div className="font-bold text-gray-900">{scoreData?.data.total_file}</div>
55
  </div>
56
  </MetricsCard>
57
  </div>
src/composables/useUser.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { authFetch } from "@/lib/api";
2
+ import { useQuery } from "@tanstack/react-query";
3
+
4
+ interface MeResponse {
5
+ user_id: string;
6
+ username: string;
7
+ email: string;
8
+ full_name: string;
9
+ role: string;
10
+ is_active: boolean;
11
+ tenant_id: string;
12
+ created_at: string;
13
+ }
14
+
15
+ export const useUser = () => {
16
+ const { data: user } = useQuery<MeResponse>({
17
+ queryKey: ["me"],
18
+ queryFn: async () => {
19
+ const res = await authFetch("/api/me");
20
+ if (!res.ok) throw new Error("Failed to fetch user");
21
+ const json = await res.json();
22
+ return json;
23
+ },
24
+ staleTime: 1000 * 60 * 5, // cache 5 minutes
25
+ });
26
+
27
+ return {
28
+ user,
29
+ };
30
+ };