Yvonne Priscilla commited on
Commit
1e81caa
·
1 Parent(s): f59d2a5

add 500 error reason

Browse files
src/app/api/agentic/route.ts CHANGED
@@ -5,65 +5,113 @@ import { NextRequest, NextResponse } from "next/server";
5
 
6
  function toFilterBody(filters: FilterFormValues) {
7
  return {
8
- gpa_edu_1: filters.educations[0]?.gpa || 0,
9
- univ_edu_1: filters.educations[0]?.university || [],
10
- major_edu_1: filters.educations[0]?.major || [],
11
- gpa_edu_2: filters.educations[1]?.gpa || 0,
12
- univ_edu_2: filters.educations[1]?.university || [],
13
- major_edu_2: filters.educations[1]?.major || [],
14
- gpa_edu_3: filters.educations[2]?.gpa || 0,
15
- univ_edu_3: filters.educations[2]?.university || [],
16
- major_edu_3: filters.educations[2]?.major || [],
17
- domicile: filters.domicile || "",
18
- yoe: filters.yoe || 0,
19
- hardskills: filters.hardskills?.length ? [filters.hardskills] : [],
20
- softskills: filters.softskills?.length ? [filters.softskills] : [],
21
- certifications: filters.certifications?.length ? [filters.certifications] : [],
22
  business_domain: filters.businessDomain?.length ? [filters.businessDomain] : [],
23
- }
24
  }
25
 
26
  function toWeightBody(value: CalculateWeightPayload): WeightBody {
27
- const edu = (i: number) => value.education[i] ?? { university: 0, major: 0, gpa: 0 }
 
 
28
  return {
29
- univ_edu_1: edu(0).university / 100, major_edu_1: edu(0).major / 100, gpa_edu_1: edu(0).gpa / 100,
30
- univ_edu_2: edu(1).university / 100, major_edu_2: edu(1).major / 100, gpa_edu_2: edu(1).gpa / 100,
31
- univ_edu_3: edu(2).university / 100, major_edu_3: edu(2).major / 100, gpa_edu_3: edu(2).gpa / 100,
32
- domicile: value.others.domicile / 100,
33
- yoe: value.others.yearOfExperiences / 100,
34
- hardskills: value.others.hardskills / 100,
35
- softskills: value.others.softskills / 100,
36
- certifications: value.others.certifications / 100,
 
 
 
 
 
 
37
  business_domain: value.others.businessDomain / 100,
38
- }
39
  }
40
 
41
  export async function POST(request: NextRequest) {
42
- const cookieStore = await cookies();
43
- const token = cookieStore.get('auth_token')?.value;
44
- const requestJSON = await request.json()
45
- const body = {
46
- criteria: toFilterBody(requestJSON.filters),
47
- criteria_weight: toWeightBody(requestJSON.weights),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  }
49
- const res = await fetch(
50
- `https://byteriot-candidateexplorer.hf.space/CandidateExplorer/agentic/v2/calculate_score`,
51
- {
52
- method: "POST",
53
- headers: {
54
- Authorization: `Bearer ${token}`,
55
- "Content-Type": "application/json",
56
- },
57
- body: JSON.stringify(body),
 
 
 
 
 
 
 
 
 
 
58
  }
59
- )
60
 
61
- const data = await res.json(); // <-- read body
 
 
 
 
 
 
 
 
 
62
 
63
- if (!res.ok) {
64
- console.error("API Error:", data);
65
- throw new Error(data?.detail || data?.message || "Something went wrong");
66
- }
67
 
68
- return NextResponse.json(data, { status: res.status })
 
 
 
 
 
 
 
69
  }
 
5
 
6
  function toFilterBody(filters: FilterFormValues) {
7
  return {
8
+ gpa_edu_1: filters.educations[0]?.gpa || 0,
9
+ univ_edu_1: filters.educations[0]?.university || [],
10
+ major_edu_1: filters.educations[0]?.major || [],
11
+ gpa_edu_2: filters.educations[1]?.gpa || 0,
12
+ univ_edu_2: filters.educations[1]?.university || [],
13
+ major_edu_2: filters.educations[1]?.major || [],
14
+ gpa_edu_3: filters.educations[2]?.gpa || 0,
15
+ univ_edu_3: filters.educations[2]?.university || [],
16
+ major_edu_3: filters.educations[2]?.major || [],
17
+ domicile: filters.domicile || "",
18
+ yoe: filters.yoe || 0,
19
+ hardskills: filters.hardskills?.length ? [filters.hardskills] : [],
20
+ softskills: filters.softskills?.length ? [filters.softskills] : [],
21
+ certifications: filters.certifications?.length ? [filters.certifications] : [],
22
  business_domain: filters.businessDomain?.length ? [filters.businessDomain] : [],
23
+ };
24
  }
25
 
26
  function toWeightBody(value: CalculateWeightPayload): WeightBody {
27
+ const edu = (i: number) =>
28
+ value.education[i] ?? { university: 0, major: 0, gpa: 0 };
29
+
30
  return {
31
+ univ_edu_1: edu(0).university / 100,
32
+ major_edu_1: edu(0).major / 100,
33
+ gpa_edu_1: edu(0).gpa / 100,
34
+ univ_edu_2: edu(1).university / 100,
35
+ major_edu_2: edu(1).major / 100,
36
+ gpa_edu_2: edu(1).gpa / 100,
37
+ univ_edu_3: edu(2).university / 100,
38
+ major_edu_3: edu(2).major / 100,
39
+ gpa_edu_3: edu(2).gpa / 100,
40
+ domicile: value.others.domicile / 100,
41
+ yoe: value.others.yearOfExperiences / 100,
42
+ hardskills: value.others.hardskills / 100,
43
+ softskills: value.others.softskills / 100,
44
+ certifications: value.others.certifications / 100,
45
  business_domain: value.others.businessDomain / 100,
46
+ };
47
  }
48
 
49
  export async function POST(request: NextRequest) {
50
+ try {
51
+ const cookieStore = await cookies();
52
+ const token = cookieStore.get("auth_token")?.value;
53
+
54
+ const requestJSON = await request.json();
55
+
56
+ const body = {
57
+ criteria: toFilterBody(requestJSON.filters),
58
+ criteria_weight: toWeightBody(requestJSON.weights),
59
+ };
60
+
61
+ console.log("🔵 Outgoing Request Body:", body);
62
+
63
+ const res = await fetch(
64
+ "https://byteriot-candidateexplorer.hf.space/CandidateExplorer/agentic/v2/calculate_score",
65
+ {
66
+ method: "POST",
67
+ headers: {
68
+ Authorization: `Bearer ${token}`,
69
+ "Content-Type": "application/json",
70
+ },
71
+ body: JSON.stringify(body),
72
  }
73
+ );
74
+
75
+ const text = await res.text(); // SAFER than res.json()
76
+
77
+ console.log("🟡 External API Status:", res.status);
78
+ console.log("🟡 External API Raw Response:", text);
79
+
80
+ let data;
81
+ try {
82
+ data = text ? JSON.parse(text) : null;
83
+ } catch (parseError) {
84
+ console.error("❌ JSON Parse Error:", parseError);
85
+ return NextResponse.json(
86
+ {
87
+ error: "Invalid JSON response from external API",
88
+ raw: text,
89
+ },
90
+ { status: 500 }
91
+ );
92
  }
 
93
 
94
+ if (!res.ok) {
95
+ console.error("❌ External API Error:", data);
96
+ return NextResponse.json(
97
+ {
98
+ error: data?.detail || data?.message || "External API error",
99
+ status: res.status,
100
+ },
101
+ { status: res.status }
102
+ );
103
+ }
104
 
105
+ return NextResponse.json(data, { status: res.status });
106
+ } catch (error: any) {
107
+ console.error("🔥 Route Crash:", error);
 
108
 
109
+ return NextResponse.json(
110
+ {
111
+ error: "Internal server error",
112
+ message: error.message,
113
+ },
114
+ { status: 500 }
115
+ );
116
+ }
117
  }
src/app/api/cv-profile/route.ts CHANGED
@@ -4,34 +4,31 @@ import { NextRequest, NextResponse } from "next/server";
4
 
5
  const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6
 
 
 
7
  async function getMatchingArrayValues(
8
  column: string,
9
  search: string,
10
  client: PrismaClient
11
  ): Promise<string[]> {
12
- // Whitelist allowed columns to prevent SQL injection
13
- const allowedColumns = ['hardskills', 'softskills', 'certifications', 'business_domain'];
14
- if (!allowedColumns.includes(column)) {
15
- throw new Error(`Invalid column: ${column}`);
16
- }
17
 
18
- const result = await client.$queryRawUnsafe(
19
- `SELECT DISTINCT unnest("${column}") as val
20
- FROM cv_profile
21
- WHERE EXISTS (
22
- SELECT 1 FROM unnest("${column}") AS elem
23
- WHERE elem ILIKE '%' || $1 || '%'
24
- )`,
25
  search
26
- ) as { val: string }[];
27
 
28
- return result
29
- .map((r) => r.val)
30
- .filter((v) => v.toLowerCase().includes(search.toLowerCase()));
31
  }
32
 
33
- // Resolve all profile_ids that belong to a given user_id
34
- // cv_profile.file_id → cv_file.file_id → cv_file.user_id
35
  async function getProfileIdsByUserId(user_id: string): Promise<string[]> {
36
  const rows = await prisma.$queryRaw<{ profile_id: string }[]>`
37
  SELECT p.profile_id
@@ -42,13 +39,37 @@ async function getProfileIdsByUserId(user_id: string): Promise<string[]> {
42
  return rows.map((r) => r.profile_id);
43
  }
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  export async function GET(request: NextRequest) {
46
  const { searchParams } = new URL(request.url);
47
 
48
  // --- PAGINATION ---
49
- const page = Math.max(1, Number.parseInt(searchParams.get("page") ?? "1") || 1);
50
  const limit = Math.min(100, Math.max(1, Number.parseInt(searchParams.get("limit") ?? "10") || 10));
51
- const skip = (page - 1) * limit;
52
 
53
  // --- SEARCH ---
54
  const search = searchParams.get("search");
@@ -66,17 +87,16 @@ export async function GET(request: NextRequest) {
66
  }
67
 
68
  // --- FILTERS ---
69
- const domicile = searchParams.get("domicile");
70
- const yoe = searchParams.get("yoe");
71
- const softskills = searchParams.getAll("softskills");
72
- const hardskills = searchParams.getAll("hardskills");
73
- const certifications = searchParams.getAll("certifications");
74
  const business_domain = searchParams.getAll("business_domain");
75
-
76
- // Education filters - now supporting multiple values
77
- const univ_edu_1 = searchParams.getAll("univ_edu_1");
78
- const univ_edu_2 = searchParams.getAll("univ_edu_2");
79
- const univ_edu_3 = searchParams.getAll("univ_edu_3");
80
  const major_edu_1 = searchParams.getAll("major_edu_1");
81
  const major_edu_2 = searchParams.getAll("major_edu_2");
82
  const major_edu_3 = searchParams.getAll("major_edu_3");
@@ -85,169 +105,122 @@ export async function GET(request: NextRequest) {
85
  const gpa_3 = searchParams.get("gpa_3");
86
 
87
  // --- SORT ---
88
- const sortBy = searchParams.get("sortBy") ?? "created_at";
89
  const sortOrder = searchParams.get("sortOrder") === "asc" ? "asc" : "desc";
90
 
 
91
  const allowedSortFields = [
92
  "fullname", "domicile", "yoe", "gpa_edu_1", "gpa_edu_2", "gpa_edu_3",
93
  "univ_edu_1", "univ_edu_2", "univ_edu_3", "major_edu_1", "major_edu_2",
94
- "major_edu_3", "created_at", "score",
95
  ];
96
 
97
  const isScoreSort = sortBy === "score" && !!criteria_id;
98
- const orderBy = !isScoreSort && allowedSortFields.includes(sortBy)
99
  ? { [sortBy]: sortOrder }
100
  : { created_at: "desc" as const };
101
 
102
  try {
103
- // --- RESOLVE user_id profile_ids FILTER ---
104
- // Run in parallel with array search since both are independent
105
  const [
106
  matchingHardskills,
107
  matchingSoftskills,
108
  matchingCertifications,
109
  matchingBusinessDomain,
110
  userProfileIds,
 
111
  ] = await Promise.all([
112
- search ? getMatchingArrayValues("hardskills", search, prisma) : Promise.resolve([]),
113
- search ? getMatchingArrayValues("softskills", search, prisma) : Promise.resolve([]),
114
- search ? getMatchingArrayValues("certifications", search, prisma) : Promise.resolve([]),
115
- search ? getMatchingArrayValues("business_domain", search, prisma) : Promise.resolve([]),
116
- user_id ? getProfileIdsByUserId(user_id) : Promise.resolve(null), // null = no filter
 
117
  ]);
118
 
119
  // --- BUILD WHERE ---
 
 
 
120
  const where: any = {
121
- // Filter by user's profiles if user_id is provided
122
- ...(userProfileIds !== null && {
123
- profile_id: { in: userProfileIds },
124
- }),
125
 
126
- ...(search ? {
127
  OR: [
128
- { fullname: { contains: search, mode: "insensitive" } },
129
- { domicile: { contains: search, mode: "insensitive" } },
130
  { univ_edu_1: { contains: search, mode: "insensitive" } },
131
  { univ_edu_2: { contains: search, mode: "insensitive" } },
132
  { univ_edu_3: { contains: search, mode: "insensitive" } },
133
- { major_edu_1: { contains: search, mode: "insensitive" } },
134
- { major_edu_2: { contains: search, mode: "insensitive" } },
135
- { major_edu_3: { contains: search, mode: "insensitive" } },
136
- { filename: { contains: search, mode: "insensitive" } },
137
- ...(matchingHardskills.length > 0 ? [{ hardskills: { hasSome: matchingHardskills } }] : []),
138
- ...(matchingSoftskills.length > 0 ? [{ softskills: { hasSome: matchingSoftskills } }] : []),
139
- ...(matchingCertifications.length > 0 ? [{ certifications: { hasSome: matchingCertifications } }] : []),
140
- ...(matchingBusinessDomain.length > 0 ? [{ business_domain: { hasSome: matchingBusinessDomain } }] : []),
141
- ...(Number.isNaN(Number.parseFloat(search)) ? [] : [
142
- { gpa_edu_1: { equals: Number.parseFloat(search) } },
143
- { gpa_edu_2: { equals: Number.parseFloat(search) } },
144
- { gpa_edu_3: { equals: Number.parseFloat(search) } },
145
- ]),
146
- ...(Number.isNaN(Number.parseInt(search)) ? [] : [
147
- { yoe: { equals: Number.parseInt(search) } },
148
- ]),
149
  ],
150
- } : {}),
151
 
152
- ...(domicile && { domicile }),
153
- ...(yoe && { yoe: { gte: Number.parseInt(yoe) } }),
154
- ...(softskills.length > 0 && { softskills: { hasSome: softskills } }),
155
- ...(hardskills.length > 0 && { hardskills: { hasSome: hardskills } }),
156
- ...(certifications.length > 0 && { certifications: { hasSome: certifications } }),
157
  ...(business_domain.length > 0 && { business_domain: { hasSome: business_domain } }),
158
-
159
- // Education filters - using OR for multiple values within same field
160
- ...(univ_edu_1.length > 0 && {
161
- univ_edu_1: { in: univ_edu_1 }
162
- }),
163
- ...(major_edu_1.length > 0 && {
164
- major_edu_1: { in: major_edu_1 }
165
- }),
166
- ...(gpa_1 && { gpa_edu_1: { gte: Number.parseFloat(gpa_1) } }),
167
-
168
- ...(univ_edu_2.length > 0 && {
169
- univ_edu_2: { in: univ_edu_2 }
170
- }),
171
- ...(major_edu_2.length > 0 && {
172
- major_edu_2: { in: major_edu_2 }
173
- }),
174
- ...(gpa_2 && { gpa_edu_2: { gte: Number.parseFloat(gpa_2) } }),
175
-
176
- ...(univ_edu_3.length > 0 && {
177
- univ_edu_3: { in: univ_edu_3 }
178
- }),
179
- ...(major_edu_3.length > 0 && {
180
- major_edu_3: { in: major_edu_3 }
181
- }),
182
- ...(gpa_3 && { gpa_edu_3: { gte: Number.parseFloat(gpa_3) } }),
183
- };
184
 
185
- // --- RESOLVE SCORE MAP ---
186
- const scoreMap = new Map<string, number | null>();
 
187
 
188
- if (criteria_id) {
189
- const weight = await prisma.cv_weight.findFirst({
190
- where: { criteria_id },
191
- select: { weight_id: true },
192
- });
193
 
194
- if (weight) {
195
- const matchings = await prisma.cv_matching.findMany({
196
- where: { weight_id: weight.weight_id },
197
- select: { matching_id: true, profile_id: true },
198
- });
199
-
200
- if (matchings.length > 0) {
201
- const matchingIds = matchings.map((m) => m.matching_id);
202
- const scores = await prisma.cv_score.findMany({
203
- where: { matching_id: { in: matchingIds } },
204
- select: { matching_id: true, profile_id: true, score: true },
205
- });
206
-
207
- const matchingToProfile = new Map(
208
- matchings.map((m) => [m.matching_id, m.profile_id])
209
- );
210
-
211
- for (const s of scores) {
212
- const profileId = s.profile_id ?? matchingToProfile.get(s.matching_id ?? "");
213
- if (profileId) scoreMap.set(profileId, s.score ?? null);
214
- }
215
- }
216
- }
217
- }
218
 
219
- // --- FETCH PROFILES + TOTAL COUNT ---
220
  const [profiles, total] = await Promise.all([
221
  prisma.cv_profile.findMany({
222
  where,
223
  orderBy,
 
224
  ...(isScoreSort ? {} : { skip, take: limit }),
225
  }),
226
  prisma.cv_profile.count({ where }),
227
  ]);
228
 
229
- // --- EARLY RETURN IF NO criteria_id ---
 
 
 
 
 
 
 
 
 
230
  if (!criteria_id) {
231
- return NextResponse.json({
232
- data: profiles,
233
- pagination: {
234
- total,
235
- page,
236
- limit,
237
- totalPages: Math.ceil(total / limit),
238
- hasNext: page < Math.ceil(total / limit),
239
- hasPrev: page > 1,
240
- },
241
- });
242
  }
243
 
244
- // --- ATTACH SCORE ---
245
  const profilesWithScore = profiles.map((profile) => ({
246
  ...profile,
247
  score: scoreMap.get(profile.profile_id) ?? null,
248
  }));
249
 
250
- // --- IN-MEMORY SORT + PAGINATE FOR SCORE SORT ---
251
  if (isScoreSort) {
252
  profilesWithScore.sort((a, b) => {
253
  const aScore = a.score ?? -Infinity;
@@ -257,31 +230,30 @@ export async function GET(request: NextRequest) {
257
 
258
  return NextResponse.json({
259
  data: profilesWithScore.slice(skip, skip + limit),
260
- pagination: {
261
- total,
262
- page,
263
- limit,
264
- totalPages: Math.ceil(total / limit),
265
- hasNext: page < Math.ceil(total / limit),
266
- hasPrev: page > 1,
267
- },
268
  });
269
  }
270
 
271
- return NextResponse.json({
272
- data: profilesWithScore,
273
- pagination: {
274
- total,
275
- page,
276
- limit,
277
- totalPages: Math.ceil(total / limit),
278
- hasNext: page < Math.ceil(total / limit),
279
- hasPrev: page > 1,
280
- },
281
- });
282
 
283
  } catch (error) {
284
- console.error(error);
285
- return NextResponse.json({ error: "Failed to fetch profiles" }, { status: 500 });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  }
287
  }
 
4
 
5
  const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6
 
7
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
8
+
9
  async function getMatchingArrayValues(
10
  column: string,
11
  search: string,
12
  client: PrismaClient
13
  ): Promise<string[]> {
14
+ const allowedColumns = ["hardskills", "softskills", "certifications", "business_domain"];
15
+ if (!allowedColumns.includes(column)) throw new Error(`Invalid column: ${column}`);
 
 
 
16
 
17
+ const result = await client.$queryRawUnsafe<{ val: string }[]>(
18
+ `SELECT DISTINCT unnest("${column}") AS val
19
+ FROM cv_profile
20
+ WHERE EXISTS (
21
+ SELECT 1 FROM unnest("${column}") AS elem
22
+ WHERE elem ILIKE '%' || $1 || '%'
23
+ )`,
24
  search
25
+ );
26
 
27
+ const lower = search.toLowerCase();
28
+ return result.map((r) => r.val).filter((v) => v.toLowerCase().includes(lower));
 
29
  }
30
 
31
+ // One query joining both tables instead of two round-trips
 
32
  async function getProfileIdsByUserId(user_id: string): Promise<string[]> {
33
  const rows = await prisma.$queryRaw<{ profile_id: string }[]>`
34
  SELECT p.profile_id
 
39
  return rows.map((r) => r.profile_id);
40
  }
41
 
42
+ // Fetch all score data in 2 queries instead of 3
43
+ async function buildScoreMap(criteria_id: string): Promise<Map<string, number | null>> {
44
+ const scoreMap = new Map<string, number | null>();
45
+
46
+ // Single query: join cv_weight → cv_matching → cv_score
47
+ const rows = await prisma.$queryRaw<{ profile_id: string; score: number | null }[]>`
48
+ SELECT
49
+ COALESCE(s.profile_id, m.profile_id) AS profile_id,
50
+ s.score
51
+ FROM cv_weight w
52
+ JOIN cv_matching m ON m.weight_id = w.weight_id
53
+ JOIN cv_score s ON s.matching_id = m.matching_id
54
+ WHERE w.criteria_id = ${criteria_id}::uuid
55
+ `;
56
+
57
+ for (const row of rows) {
58
+ if (row.profile_id) scoreMap.set(row.profile_id, row.score ?? null);
59
+ }
60
+
61
+ return scoreMap;
62
+ }
63
+
64
+ // ─── GET /api/cv-profiles ─────────────────────────────────────────────────────
65
+
66
  export async function GET(request: NextRequest) {
67
  const { searchParams } = new URL(request.url);
68
 
69
  // --- PAGINATION ---
70
+ const page = Math.max(1, Number.parseInt(searchParams.get("page") ?? "1") || 1);
71
  const limit = Math.min(100, Math.max(1, Number.parseInt(searchParams.get("limit") ?? "10") || 10));
72
+ const skip = (page - 1) * limit;
73
 
74
  // --- SEARCH ---
75
  const search = searchParams.get("search");
 
87
  }
88
 
89
  // --- FILTERS ---
90
+ const domicile = searchParams.get("domicile");
91
+ const yoe = searchParams.get("yoe");
92
+ const softskills = searchParams.getAll("softskills");
93
+ const hardskills = searchParams.getAll("hardskills");
94
+ const certifications = searchParams.getAll("certifications");
95
  const business_domain = searchParams.getAll("business_domain");
96
+
97
+ const univ_edu_1 = searchParams.getAll("univ_edu_1");
98
+ const univ_edu_2 = searchParams.getAll("univ_edu_2");
99
+ const univ_edu_3 = searchParams.getAll("univ_edu_3");
 
100
  const major_edu_1 = searchParams.getAll("major_edu_1");
101
  const major_edu_2 = searchParams.getAll("major_edu_2");
102
  const major_edu_3 = searchParams.getAll("major_edu_3");
 
105
  const gpa_3 = searchParams.get("gpa_3");
106
 
107
  // --- SORT ---
108
+ const sortBy = searchParams.get("sortBy") ?? "created_at";
109
  const sortOrder = searchParams.get("sortOrder") === "asc" ? "asc" : "desc";
110
 
111
+ // "score" is intentionally excluded — it's not a DB column, handled in-memory
112
  const allowedSortFields = [
113
  "fullname", "domicile", "yoe", "gpa_edu_1", "gpa_edu_2", "gpa_edu_3",
114
  "univ_edu_1", "univ_edu_2", "univ_edu_3", "major_edu_1", "major_edu_2",
115
+ "major_edu_3", "created_at",
116
  ];
117
 
118
  const isScoreSort = sortBy === "score" && !!criteria_id;
119
+ const orderBy = allowedSortFields.includes(sortBy)
120
  ? { [sortBy]: sortOrder }
121
  : { created_at: "desc" as const };
122
 
123
  try {
124
+ // --- ALL INDEPENDENT LOOKUPS IN ONE PARALLEL BATCH ---
 
125
  const [
126
  matchingHardskills,
127
  matchingSoftskills,
128
  matchingCertifications,
129
  matchingBusinessDomain,
130
  userProfileIds,
131
+ scoreMap,
132
  ] = await Promise.all([
133
+ search ? getMatchingArrayValues("hardskills", search, prisma) : Promise.resolve([] as string[]),
134
+ search ? getMatchingArrayValues("softskills", search, prisma) : Promise.resolve([] as string[]),
135
+ search ? getMatchingArrayValues("certifications", search, prisma) : Promise.resolve([] as string[]),
136
+ search ? getMatchingArrayValues("business_domain", search, prisma) : Promise.resolve([] as string[]),
137
+ user_id ? getProfileIdsByUserId(user_id) : Promise.resolve(null as string[] | null),
138
+ criteria_id ? buildScoreMap(criteria_id) : Promise.resolve(new Map<string, number | null>()),
139
  ]);
140
 
141
  // --- BUILD WHERE ---
142
+ const searchFloat = search ? Number.parseFloat(search) : NaN;
143
+ const searchInt = search ? Number.parseInt(search) : NaN;
144
+
145
  const where: any = {
146
+ ...(userProfileIds !== null && { profile_id: { in: userProfileIds } }),
 
 
 
147
 
148
+ ...(search && {
149
  OR: [
150
+ { fullname: { contains: search, mode: "insensitive" } },
151
+ { domicile: { contains: search, mode: "insensitive" } },
152
  { univ_edu_1: { contains: search, mode: "insensitive" } },
153
  { univ_edu_2: { contains: search, mode: "insensitive" } },
154
  { univ_edu_3: { contains: search, mode: "insensitive" } },
155
+ { major_edu_1:{ contains: search, mode: "insensitive" } },
156
+ { major_edu_2:{ contains: search, mode: "insensitive" } },
157
+ { major_edu_3:{ contains: search, mode: "insensitive" } },
158
+ { filename: { contains: search, mode: "insensitive" } },
159
+ ...(matchingHardskills.length > 0 ? [{ hardskills: { hasSome: matchingHardskills } }] : []),
160
+ ...(matchingSoftskills.length > 0 ? [{ softskills: { hasSome: matchingSoftskills } }] : []),
161
+ ...(matchingCertifications.length> 0 ? [{ certifications: { hasSome: matchingCertifications} }] : []),
162
+ ...(matchingBusinessDomain.length> 0 ? [{ business_domain: { hasSome: matchingBusinessDomain} }] : []),
163
+ ...(!Number.isNaN(searchFloat) ? [
164
+ { gpa_edu_1: { equals: searchFloat } },
165
+ { gpa_edu_2: { equals: searchFloat } },
166
+ { gpa_edu_3: { equals: searchFloat } },
167
+ ] : []),
168
+ ...(!Number.isNaN(searchInt) ? [{ yoe: { equals: searchInt } }] : []),
 
 
169
  ],
170
+ }),
171
 
172
+ ...(domicile && { domicile }),
173
+ ...(yoe && { yoe: { gte: Number.parseInt(yoe) } }),
174
+ ...(softskills.length > 0 && { softskills: { hasSome: softskills } }),
175
+ ...(hardskills.length > 0 && { hardskills: { hasSome: hardskills } }),
176
+ ...(certifications.length > 0 && { certifications: { hasSome: certifications } }),
177
  ...(business_domain.length > 0 && { business_domain: { hasSome: business_domain } }),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
+ ...(univ_edu_1.length > 0 && { univ_edu_1: { in: univ_edu_1 } }),
180
+ ...(major_edu_1.length > 0 && { major_edu_1: { in: major_edu_1 } }),
181
+ ...(gpa_1 && { gpa_edu_1: { gte: Number.parseFloat(gpa_1) } }),
182
 
183
+ ...(univ_edu_2.length > 0 && { univ_edu_2: { in: univ_edu_2 } }),
184
+ ...(major_edu_2.length > 0 && { major_edu_2: { in: major_edu_2 } }),
185
+ ...(gpa_2 && { gpa_edu_2: { gte: Number.parseFloat(gpa_2) } }),
 
 
186
 
187
+ ...(univ_edu_3.length > 0 && { univ_edu_3: { in: univ_edu_3 } }),
188
+ ...(major_edu_3.length > 0 && { major_edu_3: { in: major_edu_3 } }),
189
+ ...(gpa_3 && { gpa_edu_3: { gte: Number.parseFloat(gpa_3) } }),
190
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
+ // --- FETCH PROFILES + COUNT IN PARALLEL ---
193
  const [profiles, total] = await Promise.all([
194
  prisma.cv_profile.findMany({
195
  where,
196
  orderBy,
197
+ // Skip DB-level pagination when we need to sort by score in-memory
198
  ...(isScoreSort ? {} : { skip, take: limit }),
199
  }),
200
  prisma.cv_profile.count({ where }),
201
  ]);
202
 
203
+ const pagination = {
204
+ total,
205
+ page,
206
+ limit,
207
+ totalPages: Math.ceil(total / limit),
208
+ hasNext: page < Math.ceil(total / limit),
209
+ hasPrev: page > 1,
210
+ };
211
+
212
+ // --- NO criteria_id → return early ---
213
  if (!criteria_id) {
214
+ return NextResponse.json({ data: profiles, pagination });
 
 
 
 
 
 
 
 
 
 
215
  }
216
 
217
+ // --- ATTACH SCORES ---
218
  const profilesWithScore = profiles.map((profile) => ({
219
  ...profile,
220
  score: scoreMap.get(profile.profile_id) ?? null,
221
  }));
222
 
223
+ // --- IN-MEMORY SORT + PAGINATE (score sort only) ---
224
  if (isScoreSort) {
225
  profilesWithScore.sort((a, b) => {
226
  const aScore = a.score ?? -Infinity;
 
230
 
231
  return NextResponse.json({
232
  data: profilesWithScore.slice(skip, skip + limit),
233
+ pagination,
 
 
 
 
 
 
 
234
  });
235
  }
236
 
237
+ return NextResponse.json({ data: profilesWithScore, pagination });
 
 
 
 
 
 
 
 
 
 
238
 
239
  } catch (error) {
240
+ const isDev = process.env.NODE_ENV !== "production";
241
+
242
+ const message = error instanceof Error ? error.message : String(error);
243
+ const stack = error instanceof Error ? error.stack : undefined;
244
+
245
+ console.error("[GET /cv-profiles]", error);
246
+
247
+ return NextResponse.json(
248
+ {
249
+ error: "Failed to fetch profiles",
250
+ // Full detail in dev; safe summary in prod so you can still read logs
251
+ ...(isDev
252
+ ? { detail: message, stack }
253
+ : { detail: message } // remove this line if you want prod fully silent
254
+ ),
255
+ },
256
+ { status: 500 }
257
+ );
258
  }
259
  }