Yvonne Priscilla commited on
Commit
58a84ad
·
1 Parent(s): 473dc74
src/app/api/agentic/route.ts CHANGED
@@ -5,21 +5,21 @@ import { NextRequest, NextResponse } from "next/server";
5
 
6
  function toFilterBody(filters: FilterFormValues) {
7
  return {
8
- gpa_edu_1: filters.educations[0]?.gpa || null,
9
- univ_edu_1: filters.educations[0]?.university || null,
10
- major_edu_1: filters.educations[0]?.major || null,
11
- gpa_edu_2: filters.educations[1]?.gpa || null,
12
- univ_edu_2: filters.educations[1]?.university || null,
13
- major_edu_2: filters.educations[1]?.major || null,
14
- gpa_edu_3: filters.educations[2]?.gpa || null,
15
- univ_edu_3: filters.educations[2]?.university || null,
16
- major_edu_3: filters.educations[2]?.major || null,
17
- domicile: filters.domicile || null,
18
- yoe: filters.yoe || null,
19
- hardskills: filters.hardskills?.length ? [filters.hardskills] : null,
20
- softskills: filters.softskills?.length ? [filters.softskills] : null,
21
- certifications: filters.certifications?.length ? [filters.certifications] : null,
22
- business_domain: filters.businessDomain?.length ? [filters.businessDomain] : null,
23
  }
24
  }
25
 
@@ -41,46 +41,29 @@ function toWeightBody(value: CalculateWeightPayload): WeightBody {
41
  export async function POST(request: NextRequest) {
42
  const cookieStore = await cookies();
43
  const token = cookieStore.get('auth_token')?.value;
44
- const body = await request.json()
45
-
46
- const resCreateFilter = await fetch(
47
- `https://byteriot-candidateexplorer.hf.space/CandidateExplorer/agentic/create_filter`,
48
- {
49
- method: "POST",
50
- headers: {
51
- Authorization: `Bearer ${token}`,
52
- "Content-Type": "application/json",
53
- },
54
- body: JSON.stringify(toFilterBody(body.filters)),
55
- }
56
- )
57
-
58
- const dataFilter = await resCreateFilter.json()
59
-
60
- const resCreateWeight = await fetch(
61
- `https://byteriot-candidateexplorer.hf.space/CandidateExplorer/agentic/create_weight?criteria_id=${dataFilter.criteria_id}`,
62
  {
63
  method: "POST",
64
  headers: {
65
  Authorization: `Bearer ${token}`,
66
  "Content-Type": "application/json",
67
  },
68
- body: JSON.stringify(toWeightBody(body.weights)),
69
  }
70
  )
71
 
72
- const dataWeight = await resCreateWeight.json()
73
 
74
- const resCalculate = await fetch(
75
- `https://byteriot-candidateexplorer.hf.space/CandidateExplorer/agentic/calculate_score?weight_id=${dataWeight.data.weight_id}`,
76
- {
77
- method: "POST",
78
- headers: {
79
- Authorization: `Bearer ${token}`,
80
- "Content-Type": "application/json",
81
- }
82
- }
83
- )
84
 
85
- return NextResponse.json(dataFilter, { status: resCalculate.status })
86
  }
 
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
 
 
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
  }
src/app/api/cv-profile/charts/major/route.ts CHANGED
@@ -2,9 +2,12 @@ import { prisma } from "@/lib/prisma";
2
  import { NextResponse } from "next/server";
3
 
4
  function randomColor() {
5
- return `#${Math.floor(Math.random() * 16777215)
6
- .toString(16)
7
- .padStart(6, "0")}`;
 
 
 
8
  }
9
 
10
  export async function GET(req: Request) {
 
2
  import { NextResponse } from "next/server";
3
 
4
  function randomColor() {
5
+ const hue = Math.floor(Math.random() * 360);
6
+
7
+ const saturation = 50 + Math.random() * 20; // 50–70%
8
+ const lightness = 60 + Math.random() * 10; // 60–70%
9
+
10
+ return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
11
  }
12
 
13
  export async function GET(req: Request) {
src/app/api/cv-profile/route.ts CHANGED
@@ -72,12 +72,14 @@ export async function GET(request: NextRequest) {
72
  const hardskills = searchParams.getAll("hardskills");
73
  const certifications = searchParams.getAll("certifications");
74
  const business_domain = searchParams.getAll("business_domain");
75
- const univ_edu_1 = searchParams.get("univ_edu_1");
76
- const univ_edu_2 = searchParams.get("univ_edu_2");
77
- const univ_edu_3 = searchParams.get("univ_edu_3");
78
- const major_edu_1 = searchParams.get("major_edu_1");
79
- const major_edu_2 = searchParams.get("major_edu_2");
80
- const major_edu_3 = searchParams.get("major_edu_3");
 
 
81
  const gpa_1 = searchParams.get("gpa_1");
82
  const gpa_2 = searchParams.get("gpa_2");
83
  const gpa_3 = searchParams.get("gpa_3");
@@ -153,14 +155,30 @@ export async function GET(request: NextRequest) {
153
  ...(hardskills.length > 0 && { hardskills: { hasSome: hardskills } }),
154
  ...(certifications.length > 0 && { certifications: { hasSome: certifications } }),
155
  ...(business_domain.length > 0 && { business_domain: { hasSome: business_domain } }),
156
- ...(univ_edu_1 && { univ_edu_1 }),
157
- ...(major_edu_1 && { major_edu_1 }),
 
 
 
 
 
 
158
  ...(gpa_1 && { gpa_edu_1: { gte: Number.parseFloat(gpa_1) } }),
159
- ...(univ_edu_2 && { univ_edu_2 }),
160
- ...(major_edu_2 && { major_edu_2 }),
 
 
 
 
 
161
  ...(gpa_2 && { gpa_edu_2: { gte: Number.parseFloat(gpa_2) } }),
162
- ...(univ_edu_3 && { univ_edu_3 }),
163
- ...(major_edu_3 && { major_edu_3 }),
 
 
 
 
 
164
  ...(gpa_3 && { gpa_edu_3: { gte: Number.parseFloat(gpa_3) } }),
165
  };
166
 
@@ -266,4 +284,4 @@ export async function GET(request: NextRequest) {
266
  console.error(error);
267
  return NextResponse.json({ error: "Failed to fetch profiles" }, { status: 500 });
268
  }
269
- }
 
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");
83
  const gpa_1 = searchParams.get("gpa_1");
84
  const gpa_2 = searchParams.get("gpa_2");
85
  const gpa_3 = searchParams.get("gpa_3");
 
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
 
 
284
  console.error(error);
285
  return NextResponse.json({ error: "Failed to fetch profiles" }, { status: 500 });
286
  }
287
+ }
src/app/recruitment/upload/page.tsx CHANGED
@@ -116,7 +116,21 @@ export default function CandidateExplorer() {
116
  // ============================================================================
117
  // COMBINE SERVER FILES + UPLOADING FILES
118
  // ============================================================================
119
- const allFiles = [...uploadingFiles, ...serverFiles];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
  // ============================================================================
122
  // EXTRACT MUTATION
@@ -168,9 +182,38 @@ export default function CandidateExplorer() {
168
  // DELETE MUTATION
169
  // ============================================================================
170
  const deleteMutation = useMutation({
171
- mutationFn: (fileId: string) => deleteFileApi(fileId),
172
- onSuccess: () => {
173
- queryClient.invalidateQueries({ queryKey: ['userFiles', user?.user_id] });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  },
175
  });
176
 
@@ -338,22 +381,29 @@ export default function CandidateExplorer() {
338
  // DELETE FILE
339
  // ============================================================================
340
  const deleteFile = (fileId: string) => {
341
- const file = allFiles.find(f => f.id === fileId);
342
-
343
- // If uploading, abort
344
- if (file?.uploadStatus === 'uploading') {
345
- const abortController = abortControllersRef.current.get(fileId);
346
- if (abortController) {
347
- abortController.abort();
348
- abortControllersRef.current.delete(fileId);
349
- }
350
- setUploadingFiles(prev => prev.filter(f => f.id !== fileId));
351
- return;
352
  }
 
 
 
353
 
354
- // If uploaded, delete from server
355
- deleteMutation.mutate(file?.filename ?? "");
356
- };
 
 
 
 
 
 
357
 
358
  // ============================================================================
359
  // CALCULATE STATISTICS
 
116
  // ============================================================================
117
  // COMBINE SERVER FILES + UPLOADING FILES
118
  // ============================================================================
119
+ const allFiles = React.useMemo(() => {
120
+ const map = new Map<string, UploadedFile>();
121
+
122
+ // Put server files first
123
+ serverFiles.forEach(file => {
124
+ map.set(file.filename, file);
125
+ });
126
+
127
+ // Uploading files override same filename
128
+ uploadingFiles.forEach(file => {
129
+ map.set(file.filename, file);
130
+ });
131
+
132
+ return Array.from(map.values());
133
+ }, [serverFiles, uploadingFiles]);
134
 
135
  // ============================================================================
136
  // EXTRACT MUTATION
 
182
  // DELETE MUTATION
183
  // ============================================================================
184
  const deleteMutation = useMutation({
185
+ mutationFn: (filename: string) => deleteFileApi(filename),
186
+
187
+ onMutate: async (filename) => {
188
+ await queryClient.cancelQueries({ queryKey: ['userFiles', user?.user_id] });
189
+
190
+ const previousFiles = queryClient.getQueryData<UploadedFile[]>([
191
+ 'userFiles',
192
+ user?.user_id
193
+ ]);
194
+
195
+ // Optimistically remove from cache
196
+ queryClient.setQueryData<UploadedFile[]>(
197
+ ['userFiles', user?.user_id],
198
+ (old = []) => old.filter(file => file.filename !== filename)
199
+ );
200
+
201
+ return { previousFiles };
202
+ },
203
+
204
+ onError: (_err, _filename, context) => {
205
+ // Rollback if failed
206
+ if (context?.previousFiles) {
207
+ queryClient.setQueryData(
208
+ ['userFiles', user?.user_id],
209
+ context.previousFiles
210
+ );
211
+ }
212
+ },
213
+
214
+ onSettled: () => {
215
+ // ❌ No invalidateQueries
216
+ // We trust optimistic update
217
  },
218
  });
219
 
 
381
  // DELETE FILE
382
  // ============================================================================
383
  const deleteFile = (fileId: string) => {
384
+ const file = allFiles.find(f => f.id === fileId);
385
+ if (!file) return;
386
+
387
+ // 1️⃣ If still uploading → abort
388
+ if (file.uploadStatus === 'uploading') {
389
+ const abortController = abortControllersRef.current.get(fileId);
390
+ if (abortController) {
391
+ abortController.abort();
392
+ abortControllersRef.current.delete(fileId);
 
 
393
  }
394
+ setUploadingFiles(prev => prev.filter(f => f.id !== fileId));
395
+ return;
396
+ }
397
 
398
+ // 2️⃣ If upload failed or cancelled → just remove locally
399
+ if (file.uploadStatus === 'error' || file.uploadStatus === 'cancelled') {
400
+ setUploadingFiles(prev => prev.filter(f => f.id !== fileId));
401
+ return;
402
+ }
403
+
404
+ // 3️⃣ If success → delete from backend (optimistic)
405
+ deleteMutation.mutate(file.filename);
406
+ };
407
 
408
  // ============================================================================
409
  // CALCULATE STATISTICS
src/components/dashboard/candidates-table.tsx CHANGED
@@ -17,6 +17,7 @@ import { Check, ChevronDown, ChevronUp, Columns, Eye, Filter, Plus, Search, Tras
17
  import { memo, useCallback, useEffect, useMemo, useState } from "react"
18
  import { useFieldArray, useForm } from "react-hook-form"
19
  import { Combobox, ComboboxOption } from "../ui/combobox"
 
20
 
21
  // ============= FETCHER FUNCTIONS (outside component) =============
22
  const fetchOptions = async (): Promise<OptionsData> => {
@@ -48,14 +49,17 @@ const fetchCandidates = async (
48
  if (filters.yoe) params.set("yoe", filters.yoe)
49
  filters.educations.forEach((edu, i) => {
50
  const n = i + 1
51
- if (edu.university) params.set(`univ_edu_${n}`, edu.university)
52
- if (edu.major) params.set(`major_edu_${n}`, edu.major)
 
53
  if (edu.gpa) params.set(`gpa_${n}`, edu.gpa)
54
  })
55
- if (filters.softskills) params.append("softskills", filters.softskills)
56
- if (filters.hardskills) params.append("hardskills", filters.hardskills)
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
 
@@ -313,13 +317,13 @@ const FilterDialog = memo(({
313
 
314
  const handleReset = () => {
315
  filterForm.reset({
316
- educations: [{ university: "", major: "", gpa: "" }],
317
  domicile: "",
318
  yoe: "",
319
- hardskills: "",
320
- softskills: "",
321
- certifications: "",
322
- businessDomain: "",
323
  })
324
  }
325
 
@@ -330,7 +334,7 @@ const FilterDialog = memo(({
330
 
331
  return (
332
  <Dialog open={open} onOpenChange={onOpenChangeDialog}>
333
- <DialogContent className="max-w-2xl max-h-[90vh] flex flex-col" size="4xl">
334
  <DialogHeader>
335
  <DialogTitle>Filter</DialogTitle>
336
  </DialogHeader>
@@ -370,12 +374,11 @@ const FilterDialog = memo(({
370
  University
371
  </FormLabel>
372
  <FormControl>
373
- <Combobox
374
  options={toOptions(options.univ_edu)}
375
  value={field.value}
376
- onValueChange={field.onChange}
377
- placeholder="Select university"
378
- searchPlaceholder="Search..."
379
  />
380
  </FormControl>
381
  </FormItem>
@@ -390,12 +393,11 @@ const FilterDialog = memo(({
390
  Major
391
  </FormLabel>
392
  <FormControl>
393
- <Combobox
394
  options={toOptions(options.major_edu)}
395
  value={field.value}
396
- onValueChange={field.onChange}
397
- placeholder="Select major"
398
- searchPlaceholder="Search..."
399
  />
400
  </FormControl>
401
  </FormItem>
@@ -486,20 +488,17 @@ const FilterDialog = memo(({
486
  <FormItem>
487
  <FormLabel className="text-sm text-muted-foreground">Softskills</FormLabel>
488
  <FormControl>
489
- <Combobox
490
  options={toOptions(options.softskills)}
491
  value={field.value}
492
- onValueChange={field.onChange}
493
- placeholder="Select softskill"
494
- searchPlaceholder="Search..."
495
  />
496
  </FormControl>
497
  </FormItem>
498
  )}
499
  />
500
 
501
-
502
-
503
  <FormField
504
  control={filterForm.control}
505
  name="hardskills"
@@ -507,12 +506,11 @@ const FilterDialog = memo(({
507
  <FormItem>
508
  <FormLabel className="text-sm text-muted-foreground">Hardskills</FormLabel>
509
  <FormControl>
510
- <Combobox
511
  options={toOptions(options.hardskills)}
512
  value={field.value}
513
- onValueChange={field.onChange}
514
- placeholder="Select certification"
515
- searchPlaceholder="Search..."
516
  />
517
  </FormControl>
518
  </FormItem>
@@ -526,12 +524,11 @@ const FilterDialog = memo(({
526
  <FormItem>
527
  <FormLabel className="text-sm text-muted-foreground">Certifications</FormLabel>
528
  <FormControl>
529
- <Combobox
530
  options={toOptions(options.certifications)}
531
  value={field.value}
532
- onValueChange={field.onChange}
533
- placeholder="Select certification"
534
- searchPlaceholder="Search..."
535
  />
536
  </FormControl>
537
  </FormItem>
@@ -545,12 +542,11 @@ const FilterDialog = memo(({
545
  <FormItem>
546
  <FormLabel className="text-sm text-muted-foreground">Business Domain</FormLabel>
547
  <FormControl>
548
- <Combobox
549
  options={toOptions(options.business_domain)}
550
  value={field.value}
551
- onValueChange={field.onChange}
552
- placeholder="Select domain"
553
- searchPlaceholder="Search..."
554
  />
555
  </FormControl>
556
  </FormItem>
@@ -609,47 +605,315 @@ const SliderRow = memo(({
609
  ))
610
  SliderRow.displayName = "SliderRow"
611
 
612
- // ============= FILTER DIALOG =============
613
- const CalculateDialog = memo(({
614
- open, onOpenChange, filter, onApplyCalculation
615
  }: {
616
  open: boolean
617
  onOpenChange: (open: boolean) => void
618
- filter: FilterFormValues
619
- onApplyCalculation: (values: CalculateWeightPayload) => void,
 
620
  }) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
621
 
622
- // ---- Which fields are active based on filter ----
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
623
  const activeFields = useMemo(() => ({
624
  // Education — check per index
625
- education: filter.educations.map((e) => ({
626
- university: !!e.university,
627
- major: !!e.major,
628
  gpa: !!(e.gpa),
629
  })),
630
  others: {
631
- domicile: !!filter.domicile,
632
- yearOfExperiences: !!(filter.yoe),
633
- hardskills: !!(filter.hardskills),
634
- softskills: !!filter.softskills,
635
- certifications: !!filter.certifications,
636
- businessDomain: !!filter.businessDomain,
637
  }
638
- }), [filter])
639
-
640
- // ---- Reset values for disabled fields to 0 when filter changes ----
641
- useEffect(() => {
642
- Object.entries(activeFields.others).forEach(([key, active]) => {
643
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
644
- if (!active) setValue(`others.${key}` as any, 0)
645
- })
646
- activeFields.education.forEach((edu, i) => {
647
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
648
- if (!edu.university) setValue(`education.${i}.university` as any, 0)
649
- if (!edu.major) setValue(`education.${i}.major`, 0)
650
- if (!edu.gpa) setValue(`education.${i}.gpa`, 0)
651
- })
652
- }, [activeFields])
653
 
654
  const { control, watch, setValue, reset, handleSubmit } = useForm({
655
  defaultValues: {
@@ -674,6 +938,20 @@ const CalculateDialog = memo(({
674
 
675
  const watchAll = watch();
676
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
  const calculateTotal = () => {
678
  let total = 0;
679
 
@@ -731,13 +1009,84 @@ const CalculateDialog = memo(({
731
 
732
  const total = calculateTotal();
733
 
 
 
 
 
 
 
 
 
 
 
 
734
  return (
735
  <Dialog open={open} onOpenChange={onOpenChange}>
736
- <DialogContent className="max-w-2xl max-h-[90vh] flex flex-col" size="2xl">
737
  <DialogHeader className="flex-shrink-0">
738
  <DialogTitle>Calculate Score</DialogTitle>
739
  </DialogHeader>
740
  <div className="flex-1 overflow-y-auto px-1">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
741
  <p className="text-sm text-gray-600 mb-6">
742
  Allocate weight to each category. The total must equal exactly 100 points,
743
  then hit the calculate button if you finish with allocation.
@@ -790,7 +1139,6 @@ const CalculateDialog = memo(({
790
  </div>
791
  ))}
792
 
793
-
794
  {
795
  fields.length < 3 ? <Button
796
  type="button"
@@ -854,7 +1202,7 @@ const CalculateDialog = memo(({
854
  </Dialog>
855
  )
856
  })
857
- CalculateDialog.displayName = "CalculateDialog"
858
 
859
  // ============= LOADING DIALOGS =============
860
  const LoadingDialog = memo(({ open }: { open: boolean }) => (
@@ -911,12 +1259,15 @@ export default function CandidateTable() {
911
  const [tempVisibleColumns, setTempVisibleColumns] = useState(visibleColumns)
912
 
913
  const defaultFilters: FilterFormValues = {
914
- educations: [{ university: "", major: "", gpa: "" }],
915
- domicile: "", yoe: "", hardskills: "",
916
- softskills: "", certifications: "", businessDomain: "",
917
  }
918
  const [appliedFilters, setAppliedFilters] = useState<FilterFormValues>(defaultFilters)
 
 
919
  const [showFilter, setShowFilter] = useState(false)
 
920
  const [showColumnSettings, setShowColumnSettings] = useState(false)
921
  const [showCalculateScore, setShowCalculateScore] = useState(false)
922
  const [isCalculating, setIsCalculating] = useState(false)
@@ -945,7 +1296,7 @@ export default function CandidateTable() {
945
  staleTime: 0, // always fresh
946
  placeholderData: (prev) => prev, // keep previous data while loading (no flicker)
947
  refetchOnWindowFocus: false,
948
- refetchOnMount: false,
949
  })
950
 
951
  const candidates: Candidate[] = useMemo(
@@ -995,7 +1346,7 @@ refetchOnMount: false,
995
 
996
  const { mutate: createScore } = useMutation({
997
  mutationFn: ({ value }: { value: CalculateWeightPayload }) =>
998
- createAndCalculateScore(appliedFilters, value),
999
  onSuccess: ({ criteriaId }) => {
1000
  setCriteriaId(criteriaId)
1001
  refetch()
@@ -1022,6 +1373,17 @@ refetchOnMount: false,
1022
  setCriteriaId(null)
1023
  }
1024
 
 
 
 
 
 
 
 
 
 
 
 
1025
  const handleExport = async () => {
1026
  try {
1027
  setIsExporting(true)
@@ -1029,7 +1391,7 @@ refetchOnMount: false,
1029
  debouncedSearch,
1030
  sortConfig,
1031
  appliedFilters,
1032
- "7e6e4e8f-7ab9-4fda-8398-b42380b1b834"
1033
  )
1034
  } catch (error) {
1035
  console.error("Export failed:", error)
@@ -1111,11 +1473,21 @@ refetchOnMount: false,
1111
  onApply={handleFilterSubmit}
1112
  />
1113
 
1114
- <CalculateDialog
 
 
 
 
 
 
 
 
1115
  open={showCalculateScore}
1116
  onOpenChange={setShowCalculateScore}
1117
- filter={appliedFilters}
 
1118
  onApplyCalculation={onApplyCalculation}
 
1119
  />
1120
 
1121
  <DetailDialog
 
17
  import { memo, useCallback, useEffect, useMemo, useState } from "react"
18
  import { useFieldArray, useForm } from "react-hook-form"
19
  import { Combobox, ComboboxOption } from "../ui/combobox"
20
+ import { MultiSelect } from "./multiple-dropdown"
21
 
22
  // ============= FETCHER FUNCTIONS (outside component) =============
23
  const fetchOptions = async (): Promise<OptionsData> => {
 
49
  if (filters.yoe) params.set("yoe", filters.yoe)
50
  filters.educations.forEach((edu, i) => {
51
  const n = i + 1
52
+ // Handle arrays for university and major
53
+ edu.university.forEach((univ) => params.append(`univ_edu_${n}`, univ))
54
+ edu.major.forEach((maj) => params.append(`major_edu_${n}`, maj))
55
  if (edu.gpa) params.set(`gpa_${n}`, edu.gpa)
56
  })
57
+ // Updated to handle arrays
58
+ filters.softskills.forEach((skill) => params.append("softskills", skill))
59
+ filters.hardskills.forEach((skill) => params.append("hardskills", skill))
60
+ filters.certifications.forEach((cert) => params.append("certifications", cert))
61
+ filters.businessDomain.forEach((domain) => params.append("business_domain", domain))
62
+
63
  if (criteriaId) params.append("criteria_id", criteriaId)
64
  params.append("user_id", userId)
65
 
 
317
 
318
  const handleReset = () => {
319
  filterForm.reset({
320
+ educations: [{ university: [], major: [], gpa: "" }],
321
  domicile: "",
322
  yoe: "",
323
+ hardskills: [],
324
+ softskills: [],
325
+ certifications: [],
326
+ businessDomain: [],
327
  })
328
  }
329
 
 
334
 
335
  return (
336
  <Dialog open={open} onOpenChange={onOpenChangeDialog}>
337
+ <DialogContent className="flex flex-col" size="5xl">
338
  <DialogHeader>
339
  <DialogTitle>Filter</DialogTitle>
340
  </DialogHeader>
 
374
  University
375
  </FormLabel>
376
  <FormControl>
377
+ <MultiSelect
378
  options={toOptions(options.univ_edu)}
379
  value={field.value}
380
+ onChange={field.onChange}
381
+ placeholder="Select universities"
 
382
  />
383
  </FormControl>
384
  </FormItem>
 
393
  Major
394
  </FormLabel>
395
  <FormControl>
396
+ <MultiSelect
397
  options={toOptions(options.major_edu)}
398
  value={field.value}
399
+ onChange={field.onChange}
400
+ placeholder="Select majors"
 
401
  />
402
  </FormControl>
403
  </FormItem>
 
488
  <FormItem>
489
  <FormLabel className="text-sm text-muted-foreground">Softskills</FormLabel>
490
  <FormControl>
491
+ <MultiSelect
492
  options={toOptions(options.softskills)}
493
  value={field.value}
494
+ onChange={field.onChange}
495
+ placeholder="Select softskills"
 
496
  />
497
  </FormControl>
498
  </FormItem>
499
  )}
500
  />
501
 
 
 
502
  <FormField
503
  control={filterForm.control}
504
  name="hardskills"
 
506
  <FormItem>
507
  <FormLabel className="text-sm text-muted-foreground">Hardskills</FormLabel>
508
  <FormControl>
509
+ <MultiSelect
510
  options={toOptions(options.hardskills)}
511
  value={field.value}
512
+ onChange={field.onChange}
513
+ placeholder="Select hardskills"
 
514
  />
515
  </FormControl>
516
  </FormItem>
 
524
  <FormItem>
525
  <FormLabel className="text-sm text-muted-foreground">Certifications</FormLabel>
526
  <FormControl>
527
+ <MultiSelect
528
  options={toOptions(options.certifications)}
529
  value={field.value}
530
+ onChange={field.onChange}
531
+ placeholder="Select certifications"
 
532
  />
533
  </FormControl>
534
  </FormItem>
 
542
  <FormItem>
543
  <FormLabel className="text-sm text-muted-foreground">Business Domain</FormLabel>
544
  <FormControl>
545
+ <MultiSelect
546
  options={toOptions(options.business_domain)}
547
  value={field.value}
548
+ onChange={field.onChange}
549
+ placeholder="Select domains"
 
550
  />
551
  </FormControl>
552
  </FormItem>
 
605
  ))
606
  SliderRow.displayName = "SliderRow"
607
 
608
+ // ============= CRITERIA EDIT DIALOG =============
609
+ const CriteriaEditDialog = memo(({
610
+ open, onOpenChange, criteriaData, options, onApply
611
  }: {
612
  open: boolean
613
  onOpenChange: (open: boolean) => void
614
+ criteriaData: FilterFormValues
615
+ options: OptionsData
616
+ onApply: (values: FilterFormValues) => void
617
  }) => {
618
+ const toOptions = (arr: string[]): ComboboxOption[] =>
619
+ arr.map((v) => ({ value: v, label: v }))
620
+
621
+ const criteriaForm = useForm<FilterFormValues>({
622
+ defaultValues: criteriaData,
623
+ })
624
+
625
+ const { fields, append, remove } = useFieldArray({
626
+ control: criteriaForm.control,
627
+ name: "educations",
628
+ })
629
+
630
+ const handleApply = criteriaForm.handleSubmit((values) => {
631
+ onApply(values)
632
+ })
633
+
634
+ const handleReset = () => {
635
+ criteriaForm.reset({
636
+ educations: [{ university: [], major: [], gpa: "" }],
637
+ domicile: "",
638
+ yoe: "",
639
+ hardskills: [],
640
+ softskills: [],
641
+ certifications: [],
642
+ businessDomain: [],
643
+ })
644
+ }
645
+
646
+ const onOpenChangeDialog = (open: boolean) => {
647
+ criteriaForm.reset(criteriaData)
648
+ onOpenChange(open)
649
+ }
650
+
651
+ return (
652
+ <Dialog open={open} onOpenChange={onOpenChangeDialog}>
653
+ <DialogContent className="max-w-2xl max-h-[90vh] flex flex-col" size="5xl">
654
+ <DialogHeader>
655
+ <DialogTitle>Scoring Criteria</DialogTitle>
656
+ </DialogHeader>
657
+
658
+ <div className="flex-1 overflow-y-auto pr-1">
659
+ <Form {...criteriaForm}>
660
+ <form className="space-y-6">
661
+
662
+ {/* EDUCATION */}
663
+ <div>
664
+ <h4 className="font-medium mb-3">Education</h4>
665
+ <div className="space-y-4">
666
+ {fields.map((field, index) => (
667
+ <div key={field.id} className="border rounded-lg p-4 relative">
668
+ <p className="text-xs text-muted-foreground mb-3 font-medium">
669
+ Education {index + 1}
670
+ </p>
671
+
672
+ {/* Remove button (only show if more than 1) */}
673
+ {fields.length > 1 && (
674
+ <button
675
+ type="button"
676
+ onClick={() => remove(index)}
677
+ className="absolute top-3 right-3 text-xs text-red-500 hover:text-red-700"
678
+ >
679
+ <Trash2 size={16} />
680
+ </button>
681
+ )}
682
+
683
+ <div className="grid grid-cols-3 gap-4">
684
+ <FormField
685
+ control={criteriaForm.control}
686
+ name={`educations.${index}.university`}
687
+ render={({ field }) => (
688
+ <FormItem>
689
+ <FormLabel className="text-sm text-muted-foreground">
690
+ University
691
+ </FormLabel>
692
+ <FormControl>
693
+ <MultiSelect
694
+ options={toOptions(options.univ_edu)}
695
+ value={field.value}
696
+ onChange={field.onChange}
697
+ placeholder="Select universities"
698
+ />
699
+ </FormControl>
700
+ </FormItem>
701
+ )}
702
+ />
703
+ <FormField
704
+ control={criteriaForm.control}
705
+ name={`educations.${index}.major`}
706
+ render={({ field }) => (
707
+ <FormItem>
708
+ <FormLabel className="text-sm text-muted-foreground">
709
+ Major
710
+ </FormLabel>
711
+ <FormControl>
712
+ <MultiSelect
713
+ options={toOptions(options.major_edu)}
714
+ value={field.value}
715
+ onChange={field.onChange}
716
+ placeholder="Select majors"
717
+ />
718
+ </FormControl>
719
+ </FormItem>
720
+ )}
721
+ />
722
+ <div className="space-y-1">
723
+ <FormLabel className="text-sm text-muted-foreground">
724
+ GPA Range
725
+ </FormLabel>
726
+ <div className="flex gap-2">
727
+ <FormField
728
+ control={criteriaForm.control}
729
+ name={`educations.${index}.gpa`}
730
+ render={({ field }) => (
731
+ <FormItem className="flex-1">
732
+ <FormControl>
733
+ <Input type="number" onChange={(e) => field.onChange(e.target.value)} value={field.value} placeholder="Type GPA" />
734
+ </FormControl>
735
+ </FormItem>
736
+ )}
737
+ />
738
+ </div>
739
+ </div>
740
+ </div>
741
+ </div>
742
+ ))}
743
+ </div>
744
+
745
+ {/* Add Education Button — max 3 */}
746
+ {fields.length < 3 && (
747
+ <Button
748
+ type="button"
749
+ variant="link"
750
+ className="text-green-600 mt-2 p-0 h-auto"
751
+ onClick={() => append({ university: "", major: "", gpa: "" })}
752
+ >
753
+ + Add Education
754
+ </Button>
755
+ )}
756
+ </div>
757
 
758
+ {/* OTHERS */}
759
+ <div>
760
+ <h4 className="font-medium mb-3">Others</h4>
761
+ <div className="grid grid-cols-2 gap-4">
762
+ <FormField
763
+ control={criteriaForm.control}
764
+ name="domicile"
765
+ render={({ field }) => (
766
+ <FormItem>
767
+ <FormLabel className="text-sm text-muted-foreground">Domicile</FormLabel>
768
+ <FormControl>
769
+ <Combobox
770
+ options={toOptions(options.domicile)}
771
+ value={field.value}
772
+ onValueChange={field.onChange}
773
+ placeholder="Select city"
774
+ searchPlaceholder="Search city..."
775
+ />
776
+ </FormControl>
777
+ </FormItem>
778
+ )}
779
+ />
780
+
781
+ <div className="space-y-1">
782
+ <FormLabel className="text-sm text-muted-foreground">YoE Range</FormLabel>
783
+ <div className="flex gap-2">
784
+ <FormField
785
+ control={criteriaForm.control}
786
+ name="yoe"
787
+ render={({ field }) => (
788
+ <FormItem className="flex-1">
789
+ <FormControl>
790
+ <Input type="number" onChange={(e) => field.onChange(e.target.value)} value={field.value} placeholder="Type YoE" />
791
+ </FormControl>
792
+ </FormItem>
793
+ )}
794
+ />
795
+ </div>
796
+ </div>
797
+
798
+ <FormField
799
+ control={criteriaForm.control}
800
+ name="softskills"
801
+ render={({ field }) => (
802
+ <FormItem>
803
+ <FormLabel className="text-sm text-muted-foreground">Softskills</FormLabel>
804
+ <FormControl>
805
+ <MultiSelect
806
+ options={toOptions(options.softskills)}
807
+ value={field.value}
808
+ onChange={field.onChange}
809
+ placeholder="Select softskills"
810
+ />
811
+ </FormControl>
812
+ </FormItem>
813
+ )}
814
+ />
815
+
816
+ <FormField
817
+ control={criteriaForm.control}
818
+ name="hardskills"
819
+ render={({ field }) => (
820
+ <FormItem>
821
+ <FormLabel className="text-sm text-muted-foreground">Hardskills</FormLabel>
822
+ <FormControl>
823
+ <MultiSelect
824
+ options={toOptions(options.hardskills)}
825
+ value={field.value}
826
+ onChange={field.onChange}
827
+ placeholder="Select hardskills"
828
+ />
829
+ </FormControl>
830
+ </FormItem>
831
+ )}
832
+ />
833
+
834
+ <FormField
835
+ control={criteriaForm.control}
836
+ name="certifications"
837
+ render={({ field }) => (
838
+ <FormItem>
839
+ <FormLabel className="text-sm text-muted-foreground">Certifications</FormLabel>
840
+ <FormControl>
841
+ <MultiSelect
842
+ options={toOptions(options.certifications)}
843
+ value={field.value}
844
+ onChange={field.onChange}
845
+ placeholder="Select certifications"
846
+ />
847
+ </FormControl>
848
+ </FormItem>
849
+ )}
850
+ />
851
+
852
+ <FormField
853
+ control={criteriaForm.control}
854
+ name="businessDomain"
855
+ render={({ field }) => (
856
+ <FormItem>
857
+ <FormLabel className="text-sm text-muted-foreground">Business Domain</FormLabel>
858
+ <FormControl>
859
+ <MultiSelect
860
+ options={toOptions(options.business_domain)}
861
+ value={field.value}
862
+ onChange={field.onChange}
863
+ placeholder="Select domains"
864
+ />
865
+ </FormControl>
866
+ </FormItem>
867
+ )}
868
+ />
869
+ </div>
870
+ </div>
871
+
872
+ </form>
873
+ </Form>
874
+ </div>
875
+
876
+ <DialogFooter>
877
+ <Button variant="outline" onClick={handleReset}>Reset</Button>
878
+ <Button onClick={handleApply} className="bg-green-600 hover:bg-green-700 text-white">
879
+ Apply
880
+ </Button>
881
+ </DialogFooter>
882
+ </DialogContent>
883
+ </Dialog>
884
+ )
885
+ })
886
+ CriteriaEditDialog.displayName = "CriteriaEditDialog"
887
+
888
+ // ============= CALCULATE SCORE DIALOG =============
889
+ const CalculateScoreDialog = memo(({
890
+ open, onOpenChange, targetData, options, onApplyCalculation, onEditCriteria
891
+ }: {
892
+ open: boolean
893
+ onOpenChange: (open: boolean) => void
894
+ targetData: FilterFormValues
895
+ options: OptionsData
896
+ onApplyCalculation: (values: CalculateWeightPayload) => void
897
+ onEditCriteria: () => void
898
+ }) => {
899
+
900
+ // ---- Which fields are active based on targetData ----
901
  const activeFields = useMemo(() => ({
902
  // Education — check per index
903
+ education: targetData.educations.map((e) => ({
904
+ university: e.university.length > 0,
905
+ major: e.major.length > 0,
906
  gpa: !!(e.gpa),
907
  })),
908
  others: {
909
+ domicile: !!targetData.domicile,
910
+ yearOfExperiences: !!(targetData.yoe),
911
+ hardskills: targetData.hardskills.length > 0,
912
+ softskills: targetData.softskills.length > 0,
913
+ certifications: targetData.certifications.length > 0,
914
+ businessDomain: targetData.businessDomain.length > 0,
915
  }
916
+ }), [targetData])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
917
 
918
  const { control, watch, setValue, reset, handleSubmit } = useForm({
919
  defaultValues: {
 
938
 
939
  const watchAll = watch();
940
 
941
+ // ---- Reset values for disabled fields to 0 when targetData changes ----
942
+ useEffect(() => {
943
+ Object.entries(activeFields.others).forEach(([key, active]) => {
944
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
945
+ if (!active) setValue(`others.${key}` as any, 0)
946
+ })
947
+ activeFields.education.forEach((edu, i) => {
948
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
949
+ if (!edu.university) setValue(`education.${i}.university` as any, 0)
950
+ if (!edu.major) setValue(`education.${i}.major`, 0)
951
+ if (!edu.gpa) setValue(`education.${i}.gpa`, 0)
952
+ })
953
+ }, [activeFields, setValue])
954
+
955
  const calculateTotal = () => {
956
  let total = 0;
957
 
 
1009
 
1010
  const total = calculateTotal();
1011
 
1012
+ // Check if there's any criteria set
1013
+ const hasCriteria = useMemo(() => {
1014
+ return targetData.educations.some(e => e.university.length > 0 || e.major.length > 0 || e.gpa) ||
1015
+ !!targetData.domicile ||
1016
+ !!targetData.yoe ||
1017
+ targetData.hardskills.length > 0 ||
1018
+ targetData.softskills.length > 0 ||
1019
+ targetData.certifications.length > 0 ||
1020
+ targetData.businessDomain.length > 0
1021
+ }, [targetData])
1022
+
1023
  return (
1024
  <Dialog open={open} onOpenChange={onOpenChange}>
1025
+ <DialogContent className="flex flex-col" size="3xl">
1026
  <DialogHeader className="flex-shrink-0">
1027
  <DialogTitle>Calculate Score</DialogTitle>
1028
  </DialogHeader>
1029
  <div className="flex-1 overflow-y-auto px-1">
1030
+ {/* Scoring Criteria Section */}
1031
+ <div className="mb-6 p-4 border border-gray-200 rounded-lg bg-gray-50">
1032
+ <div className="flex items-center justify-between mb-3">
1033
+ <h3 className="font-semibold text-sm">Scoring Criteria</h3>
1034
+ <Button
1035
+ variant="outline"
1036
+ size="sm"
1037
+ onClick={onEditCriteria}
1038
+ className="h-8 text-xs"
1039
+ >
1040
+ Edit
1041
+ </Button>
1042
+ </div>
1043
+
1044
+ {!hasCriteria ? (
1045
+ <p className="text-sm text-muted-foreground italic">
1046
+ No criteria set. Click "Edit" to define scoring criteria.
1047
+ </p>
1048
+ ) : (
1049
+ <div className="space-y-3 text-sm">
1050
+ {/* Education Criteria */}
1051
+ {targetData.educations.some(e => e.university.length > 0 || e.major.length > 0 || e.gpa) && (
1052
+ <div>
1053
+ <p className="font-medium text-xs text-gray-500 mb-2">Education</p>
1054
+ <div className="space-y-2">
1055
+ {targetData.educations.map((edu, idx) => {
1056
+ const items = []
1057
+ if (edu.university.length > 0) items.push(`University: ${edu.university.join(", ")}`)
1058
+ if (edu.major.length > 0) items.push(`Major: ${edu.major.join(", ")}`)
1059
+ if (edu.gpa) items.push(`GPA: ${edu.gpa}`)
1060
+
1061
+ return items.length > 0 ? (
1062
+ <div key={idx} className="pl-3">
1063
+ <span className="text-xs text-gray-400">#{idx + 1}</span> {items.join(', ')}
1064
+ </div>
1065
+ ) : null
1066
+ })}
1067
+ </div>
1068
+ </div>
1069
+ )}
1070
+
1071
+ {/* Others Criteria */}
1072
+ {(targetData.domicile || targetData.yoe || targetData.hardskills.length > 0 ||
1073
+ targetData.softskills.length > 0 || targetData.certifications.length > 0 || targetData.businessDomain.length > 0) && (
1074
+ <div>
1075
+ <p className="font-medium text-xs text-gray-500 mb-2">Others</p>
1076
+ <div className="space-y-1 pl-3">
1077
+ {targetData.domicile && <div>Domicile: {targetData.domicile}</div>}
1078
+ {targetData.yoe && <div>Years of Experience: {targetData.yoe}</div>}
1079
+ {targetData.hardskills.length > 0 && <div>Hardskills: {targetData.hardskills.join(", ")}</div>}
1080
+ {targetData.softskills.length > 0 && <div>Softskills: {targetData.softskills.join(", ")}</div>}
1081
+ {targetData.certifications.length > 0 && <div>Certifications: {targetData.certifications.join(", ")}</div>}
1082
+ {targetData.businessDomain.length > 0 && <div>Business Domain: {targetData.businessDomain.join(", ")}</div>}
1083
+ </div>
1084
+ </div>
1085
+ )}
1086
+ </div>
1087
+ )}
1088
+ </div>
1089
+
1090
  <p className="text-sm text-gray-600 mb-6">
1091
  Allocate weight to each category. The total must equal exactly 100 points,
1092
  then hit the calculate button if you finish with allocation.
 
1139
  </div>
1140
  ))}
1141
 
 
1142
  {
1143
  fields.length < 3 ? <Button
1144
  type="button"
 
1202
  </Dialog>
1203
  )
1204
  })
1205
+ CalculateScoreDialog.displayName = "CalculateScoreDialog"
1206
 
1207
  // ============= LOADING DIALOGS =============
1208
  const LoadingDialog = memo(({ open }: { open: boolean }) => (
 
1259
  const [tempVisibleColumns, setTempVisibleColumns] = useState(visibleColumns)
1260
 
1261
  const defaultFilters: FilterFormValues = {
1262
+ educations: [{ university: [], major: [], gpa: "" }],
1263
+ domicile: "", yoe: "", hardskills: [],
1264
+ softskills: [], certifications: [], businessDomain: [],
1265
  }
1266
  const [appliedFilters, setAppliedFilters] = useState<FilterFormValues>(defaultFilters)
1267
+ const [scoringCriteria, setScoringCriteria] = useState<FilterFormValues>(defaultFilters)
1268
+
1269
  const [showFilter, setShowFilter] = useState(false)
1270
+ const [showCriteriaEdit, setShowCriteriaEdit] = useState(false)
1271
  const [showColumnSettings, setShowColumnSettings] = useState(false)
1272
  const [showCalculateScore, setShowCalculateScore] = useState(false)
1273
  const [isCalculating, setIsCalculating] = useState(false)
 
1296
  staleTime: 0, // always fresh
1297
  placeholderData: (prev) => prev, // keep previous data while loading (no flicker)
1298
  refetchOnWindowFocus: false,
1299
+ refetchOnMount: false,
1300
  })
1301
 
1302
  const candidates: Candidate[] = useMemo(
 
1346
 
1347
  const { mutate: createScore } = useMutation({
1348
  mutationFn: ({ value }: { value: CalculateWeightPayload }) =>
1349
+ createAndCalculateScore(scoringCriteria, value),
1350
  onSuccess: ({ criteriaId }) => {
1351
  setCriteriaId(criteriaId)
1352
  refetch()
 
1373
  setCriteriaId(null)
1374
  }
1375
 
1376
+ const handleCriteriaSubmit = (values: FilterFormValues) => {
1377
+ setScoringCriteria(values)
1378
+ setShowCriteriaEdit(false)
1379
+ setShowCalculateScore(true) // Reopen calculate score dialog
1380
+ }
1381
+
1382
+ const handleEditCriteria = () => {
1383
+ setShowCalculateScore(false)
1384
+ setShowCriteriaEdit(true)
1385
+ }
1386
+
1387
  const handleExport = async () => {
1388
  try {
1389
  setIsExporting(true)
 
1391
  debouncedSearch,
1392
  sortConfig,
1393
  appliedFilters,
1394
+ criteriaId
1395
  )
1396
  } catch (error) {
1397
  console.error("Export failed:", error)
 
1473
  onApply={handleFilterSubmit}
1474
  />
1475
 
1476
+ <CriteriaEditDialog
1477
+ open={showCriteriaEdit}
1478
+ onOpenChange={setShowCriteriaEdit}
1479
+ criteriaData={scoringCriteria}
1480
+ options={options}
1481
+ onApply={handleCriteriaSubmit}
1482
+ />
1483
+
1484
+ <CalculateScoreDialog
1485
  open={showCalculateScore}
1486
  onOpenChange={setShowCalculateScore}
1487
+ targetData={scoringCriteria}
1488
+ options={options}
1489
  onApplyCalculation={onApplyCalculation}
1490
+ onEditCriteria={handleEditCriteria}
1491
  />
1492
 
1493
  <DetailDialog
src/components/dashboard/filter-dialog.tsx CHANGED
@@ -1,13 +1,13 @@
1
  'use client';
2
 
 
 
3
  import {
4
  Dialog,
5
  DialogContent,
6
  DialogHeader,
7
  DialogTitle,
8
  } from '@/components/ui/dialog';
9
- import { Button } from '@/components/ui/button';
10
- import { Checkbox } from '@/components/ui/checkbox';
11
  import { Label } from '@/components/ui/label';
12
 
13
  interface FilterDialogProps {
 
1
  'use client';
2
 
3
+ import { Button } from '@/components/ui/button';
4
+ import { Checkbox } from '@/components/ui/checkbox';
5
  import {
6
  Dialog,
7
  DialogContent,
8
  DialogHeader,
9
  DialogTitle,
10
  } from '@/components/ui/dialog';
 
 
11
  import { Label } from '@/components/ui/label';
12
 
13
  interface FilterDialogProps {
src/components/dashboard/multiple-dropdown.tsx CHANGED
@@ -1,7 +1,6 @@
1
  "use client";
2
 
3
- import * as React from "react";
4
- import { Check, ChevronsUpDown } from "lucide-react";
5
  import {
6
  Command,
7
  CommandEmpty,
@@ -10,12 +9,14 @@ import {
10
  CommandItem,
11
  CommandList,
12
  } from "@/components/ui/command";
13
- import { cn } from "@/lib/utils";
14
  import {
15
  Popover,
16
  PopoverContent,
17
  PopoverTrigger,
18
  } from "@/components/ui/popover";
 
 
 
19
 
20
  export function MultiSelect({
21
  options,
@@ -36,20 +37,34 @@ export function MultiSelect({
36
  } else {
37
  onChange([...value, val]);
38
  }
 
39
  };
40
 
41
  return (
42
  <Popover open={open} onOpenChange={setOpen}>
43
- {/* IMPORTANT FIX: use asChild & replace button with div */}
44
  <PopoverTrigger asChild>
45
  <div
46
  className={cn(
47
- "border w-full px-3 py-2 rounded-md flex items-center justify-between cursor-pointer",
48
  value.length === 0 && "text-muted-foreground"
49
  )}
50
  >
51
- {value.length > 0 ? value.join(", ") : placeholder}
52
- <ChevronsUpDown className="h-4 w-4 opacity-50" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  </div>
54
  </PopoverTrigger>
55
 
@@ -69,12 +84,14 @@ export function MultiSelect({
69
  onSelect={() => toggleValue(option.value)}
70
  className="flex items-center gap-2 cursor-pointer"
71
  >
72
- <Check
73
  className={cn(
74
- "h-4 w-4",
75
- selected ? "opacity-100" : "opacity-0"
76
  )}
77
- />
 
 
78
  {option.label}
79
  </CommandItem>
80
  );
@@ -85,4 +102,4 @@ export function MultiSelect({
85
  </PopoverContent>
86
  </Popover>
87
  );
88
- }
 
1
  "use client";
2
 
3
+ import { Badge } from "@/components/ui/badge";
 
4
  import {
5
  Command,
6
  CommandEmpty,
 
9
  CommandItem,
10
  CommandList,
11
  } from "@/components/ui/command";
 
12
  import {
13
  Popover,
14
  PopoverContent,
15
  PopoverTrigger,
16
  } from "@/components/ui/popover";
17
+ import { cn } from "@/lib/utils";
18
+ import { Check, ChevronsUpDown } from "lucide-react";
19
+ import * as React from "react";
20
 
21
  export function MultiSelect({
22
  options,
 
37
  } else {
38
  onChange([...value, val]);
39
  }
40
+ // Don't close the popover - keep it open for multiple selections
41
  };
42
 
43
  return (
44
  <Popover open={open} onOpenChange={setOpen}>
 
45
  <PopoverTrigger asChild>
46
  <div
47
  className={cn(
48
+ "border w-full min-h-[40px] px-3 py-2 rounded-md flex items-center justify-between cursor-pointer gap-2",
49
  value.length === 0 && "text-muted-foreground"
50
  )}
51
  >
52
+ <div className="flex flex-wrap gap-1 flex-1">
53
+ {value.length === 0 ? (
54
+ <span>{placeholder}</span>
55
+ ) : (
56
+ value.map((val) => (
57
+ <Badge
58
+ key={val}
59
+ variant="secondary"
60
+ className="text-xs px-2 py-0.5 gap-1"
61
+ >
62
+ {val}
63
+ </Badge>
64
+ ))
65
+ )}
66
+ </div>
67
+ <ChevronsUpDown className="h-4 w-4 opacity-50 shrink-0" />
68
  </div>
69
  </PopoverTrigger>
70
 
 
84
  onSelect={() => toggleValue(option.value)}
85
  className="flex items-center gap-2 cursor-pointer"
86
  >
87
+ <div
88
  className={cn(
89
+ "h-4 w-4 border rounded-sm flex items-center justify-center",
90
+ selected ? "bg-primary border-primary" : "border-input"
91
  )}
92
+ >
93
+ {selected && <Check className="h-3 w-3 text-primary-foreground" />}
94
+ </div>
95
  {option.label}
96
  </CommandItem>
97
  );
 
102
  </PopoverContent>
103
  </Popover>
104
  );
105
+ }
src/types/candidate-table.ts CHANGED
@@ -40,16 +40,16 @@ export interface PaginationProps {
40
 
41
  export interface FilterFormValues {
42
  educations: {
43
- university: string
44
- major: string
45
  gpa: string
46
  }[]
47
  domicile: string
48
  yoe: string
49
- softskills: string
50
- hardskills: string
51
- certifications: string
52
- businessDomain: string
53
  }
54
 
55
  export interface OptionsData {
 
40
 
41
  export interface FilterFormValues {
42
  educations: {
43
+ university: string[]
44
+ major: string[]
45
  gpa: string
46
  }[]
47
  domicile: string
48
  yoe: string
49
+ softskills: string[]
50
+ hardskills: string[]
51
+ certifications: string[]
52
+ businessDomain: string[]
53
  }
54
 
55
  export interface OptionsData {