Yvonne Priscilla commited on
Commit
1fbd3b4
·
1 Parent(s): 250ae22

finishing extract data and delete

Browse files
src/app/api/file/delete/route.ts ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // app/api/file/upload/route.ts
2
+ import { cookies } from 'next/headers';
3
+ import type { NextRequest } from 'next/server';
4
+ import { NextResponse } from 'next/server';
5
+
6
+ export async function POST(request: NextRequest) {
7
+ try {
8
+ // Get FormData from request
9
+ const { searchParams } = new URL(request.url);
10
+ const filename = searchParams.get('filename');
11
+
12
+ // Get auth token from cookies
13
+ const cookieStore = await cookies();
14
+ const token = cookieStore.get('auth_token')?.value;
15
+
16
+ if (!token) {
17
+ return NextResponse.json(
18
+ { message: 'Not authenticated' },
19
+ { status: 401 }
20
+ );
21
+ }
22
+
23
+ const url = new URL(
24
+ `https://byteriot-candidateexplorer.hf.space/CandidateExplorer/file/${filename}`
25
+ );
26
+
27
+ // Forward the FormData to external API
28
+ const response = await fetch(
29
+ url.toString(),
30
+ {
31
+ method: 'DELETE',
32
+ headers: {
33
+ Authorization: `Bearer ${token}`,
34
+ },
35
+ }
36
+ );
37
+
38
+ if (!response.ok) {
39
+ const errorData = await response.json().catch(() => null);
40
+ return NextResponse.json(
41
+ { message: errorData?.message, error: errorData },
42
+ { status: response.status }
43
+ );
44
+ }
45
+
46
+ // Get response data from external API
47
+ const data = await response.json();
48
+
49
+ return NextResponse.json(
50
+ {
51
+ success: true,
52
+ message: 'Extract uploaded file successfully',
53
+ data: data
54
+ },
55
+ { status: 200 }
56
+ );
57
+
58
+ } catch (error) {
59
+ console.error('Extract uploaded file error:', error);
60
+ return NextResponse.json(
61
+ { message: 'Failed to extract uploaded file ', error: String(error) },
62
+ { status: 500 }
63
+ );
64
+ }
65
+ }
src/app/api/profile/extract-data/route.ts CHANGED
@@ -6,7 +6,8 @@ import { NextResponse } from 'next/server';
6
  export async function POST(request: NextRequest) {
7
  try {
8
  // Get FormData from request
9
- const body = await request.json();
 
10
 
11
  // Get auth token from cookies
12
  const cookieStore = await cookies();
@@ -22,7 +23,7 @@ export async function POST(request: NextRequest) {
22
  const url = new URL(
23
  "https://byteriot-candidateexplorer.hf.space/CandidateExplorer/profile/extract_profile"
24
  );
25
- url.searchParams.append("filename", body.filename);
26
 
27
  // Forward the FormData to external API
28
  const response = await fetch(
 
6
  export async function POST(request: NextRequest) {
7
  try {
8
  // Get FormData from request
9
+ const { searchParams } = new URL(request.url);
10
+ const filename = searchParams.get('filename');
11
 
12
  // Get auth token from cookies
13
  const cookieStore = await cookies();
 
23
  const url = new URL(
24
  "https://byteriot-candidateexplorer.hf.space/CandidateExplorer/profile/extract_profile"
25
  );
26
+ url.searchParams.append("filename", filename ?? "");
27
 
28
  // Forward the FormData to external API
29
  const response = await fetch(
src/app/recruitment/upload/page.tsx CHANGED
@@ -29,7 +29,7 @@ const fetchUserFiles = async (userId: string): Promise<UploadedFile[]> => {
29
  throw new Error('Failed to fetch files');
30
  }
31
  const data = await response.json();
32
-
33
  return data.map((file: any) => ({
34
  id: file.file_id, // ← Map file_id to id
35
  filename: file.filename,
@@ -44,7 +44,7 @@ const fetchUserFiles = async (userId: string): Promise<UploadedFile[]> => {
44
  const uploadFile = async (file: File, signal?: AbortSignal) => {
45
  const formData = new FormData();
46
  formData.append('files', file);
47
-
48
  const response = await fetch('/api/file/upload', {
49
  method: 'POST',
50
  body: formData,
@@ -72,8 +72,8 @@ const extractFileData = async (filename: string) => {
72
  return response.json();
73
  };
74
 
75
- const deleteFileApi = async (fileId: string) => {
76
- const response = await fetch(`/api/file/delete?fileId=${fileId}`, {
77
  method: 'DELETE',
78
  });
79
 
@@ -91,19 +91,19 @@ export default function CandidateExplorer() {
91
  const [dragActive, setDragActive] = useState(false);
92
  const [uploadingFiles, setUploadingFiles] = useState<UploadedFile[]>([]);
93
  const [extractingFiles, setExtractingFiles] = useState<Set<string>>(new Set());
94
- const { user } = useAuth()
95
-
96
  const queryClient = useQueryClient();
97
  const abortControllersRef = useRef<Map<string, AbortController>>(new Map());
98
 
99
  // ============================================================================
100
  // FETCH USER FILES (AUTO-FETCHES ON MOUNT)
101
  // ============================================================================
102
- const {
103
- data: serverFiles = [],
104
- isLoading,
105
  error,
106
- refetch
107
  } = useQuery({
108
  queryKey: ['userFiles', user],
109
  queryFn: () => fetchUserFiles(user?.user_id ?? ""),
@@ -121,30 +121,48 @@ export default function CandidateExplorer() {
121
  // EXTRACT MUTATION
122
  // ============================================================================
123
  const extractMutation = useMutation({
124
- mutationFn: ({ filename }: { filename: string }) => extractFileData(filename),
 
 
 
 
125
  onSuccess: (data, variables) => {
126
- // Update the file in server data
127
- queryClient.invalidateQueries({ queryKey: ['userFiles', user?.user_id] });
128
-
129
  // Remove from extracting set
130
  setExtractingFiles(prev => {
131
  const newSet = new Set(prev);
132
- const file = allFiles.find(f => f.filename === variables.filename);
133
- if (file) newSet.delete(file.id);
134
  return newSet;
135
  });
 
 
 
136
  },
137
  onError: (error, variables) => {
138
  // Remove from extracting set
139
  setExtractingFiles(prev => {
140
  const newSet = new Set(prev);
141
- const file = allFiles.find(f => f.filename === variables.filename);
142
- if (file) newSet.delete(file.id);
143
  return newSet;
144
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  },
146
  });
147
 
 
148
  // ============================================================================
149
  // DELETE MUTATION
150
  // ============================================================================
@@ -159,7 +177,7 @@ export default function CandidateExplorer() {
159
  // UPLOAD SINGLE FILE
160
  // ============================================================================
161
  const uploadSingleFile = async (
162
- file: File,
163
  tempId: string,
164
  abortController: AbortController
165
  ): Promise<{ success: boolean; data?: any; error?: string; cancelled?: boolean }> => {
@@ -170,10 +188,10 @@ export default function CandidateExplorer() {
170
  if (error instanceof Error && error.name === 'AbortError') {
171
  return { success: false, cancelled: true };
172
  }
173
-
174
- return {
175
- success: false,
176
- error: error instanceof Error ? error.message : 'Upload failed'
177
  };
178
  }
179
  };
@@ -184,7 +202,7 @@ export default function CandidateExplorer() {
184
  const uploadToAPI = async (files: FileList) => {
185
  const tempFiles: UploadedFile[] = Array.from(files).map((file) => {
186
  const id = `temp-${Date.now()}-${Math.random()}`;
187
-
188
  return {
189
  id,
190
  filename: file.name,
@@ -202,7 +220,7 @@ export default function CandidateExplorer() {
202
  const uploadPromises = tempFiles.map((tempFile, index) => {
203
  const abortController = new AbortController();
204
  abortControllersRef.current.set(tempFile.id, abortController);
205
-
206
  return uploadSingleFile(files[index], tempFile.id, abortController);
207
  });
208
 
@@ -232,10 +250,10 @@ export default function CandidateExplorer() {
232
  };
233
  } else {
234
  abortControllersRef.current.delete(file.id);
235
- const errorMessage = result.status === 'fulfilled'
236
- ? result.value.error
237
  : 'Upload failed';
238
-
239
  return {
240
  ...file,
241
  uploadStatus: 'error' as const,
@@ -270,8 +288,7 @@ export default function CandidateExplorer() {
270
  // EXTRACT DATA FROM FILE
271
  // ============================================================================
272
  const extractData = async (fileId: string, filename: string) => {
273
- setExtractingFiles(prev => new Set(prev).add(fileId));
274
- extractMutation.mutate({ filename });
275
  };
276
 
277
  // ============================================================================
@@ -280,7 +297,6 @@ export default function CandidateExplorer() {
280
  const retryExtraction = async (fileId: string, filename: string) => {
281
  await extractData(fileId, filename);
282
  };
283
-
284
  // ============================================================================
285
  // HANDLE FILE DRAG EVENTS
286
  // ============================================================================
@@ -301,7 +317,7 @@ export default function CandidateExplorer() {
301
  e.preventDefault();
302
  e.stopPropagation();
303
  setDragActive(false);
304
-
305
  if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
306
  await uploadToAPI(e.dataTransfer.files);
307
  }
@@ -322,7 +338,7 @@ export default function CandidateExplorer() {
322
  // ============================================================================
323
  const deleteFile = (fileId: string) => {
324
  const file = allFiles.find(f => f.id === fileId);
325
-
326
  // If uploading, abort
327
  if (file?.uploadStatus === 'uploading') {
328
  const abortController = abortControllersRef.current.get(fileId);
@@ -333,9 +349,9 @@ export default function CandidateExplorer() {
333
  setUploadingFiles(prev => prev.filter(f => f.id !== fileId));
334
  return;
335
  }
336
-
337
  // If uploaded, delete from server
338
- deleteMutation.mutate(fileId);
339
  };
340
 
341
  // ============================================================================
@@ -343,7 +359,7 @@ export default function CandidateExplorer() {
343
  // ============================================================================
344
  const totalCVs = allFiles.filter(f => f.uploadStatus === 'success').length;
345
  const profilesExtracted = allFiles.filter(f => f.extractStatus === 'extracted').length;
346
- const extractionRate = totalCVs > 0 ? ((profilesExtracted / totalCVs) * 100).toFixed(1) : '0.0';
347
  const uploadingCount = uploadingFiles.filter(f => f.uploadStatus === 'uploading').length;
348
  const extractingCount = extractingFiles.size;
349
 
@@ -362,17 +378,17 @@ export default function CandidateExplorer() {
362
  // ============================================================================
363
  const renderStatusBadge = (file: UploadedFile) => {
364
  const displayFile = getFileDisplayStatus(file);
365
-
366
  // Upload status
367
  if (displayFile.uploadStatus === 'uploading') {
368
  return (
369
- <span className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-700">
370
  <Loader2 className="w-3 h-3 animate-spin" />
371
  Uploading
372
  </span>
373
  );
374
  }
375
-
376
  if (displayFile.uploadStatus === 'error') {
377
  return (
378
  <span className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-700">
@@ -428,261 +444,260 @@ export default function CandidateExplorer() {
428
  };
429
 
430
  return (
431
- <div className="">
432
- {/* ============================================================================ */}
433
- {/* TITLE AND DESCRIPTION */}
434
- {/* ============================================================================ */}
435
- <div className="mb-8">
436
- <p className="text-gray-600">
437
- Upload CVs, extract candidate profiles, and track your workspace.
438
- </p>
 
 
 
 
 
 
 
 
 
439
  </div>
440
 
441
- {/* ============================================================================ */}
442
- {/* DASHBOARD STATISTICS CARDS */}
443
- {/* ============================================================================ */}
444
- <section className="mb-8">
445
- <div className="flex items-center gap-2 mb-4">
446
- <BarChart3 className="w-5 h-5 text-blue-600" />
447
- <h2 className="text-lg font-semibold text-blue-600">Dashboard Overview</h2>
 
 
 
 
 
 
 
448
  </div>
449
 
450
- <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
451
- {/* Total CVs Card */}
452
- <div className="bg-white rounded-lg border border-gray-200 p-6 shadow-sm">
453
- <div className="flex items-center gap-2 text-blue-600 text-sm font-medium mb-3">
454
- <Upload className="w-4 h-4" />
455
- TOTAL CVS UPLOADED
456
- </div>
457
- <div className="text-5xl font-bold text-blue-600 mb-1">{totalCVs}</div>
458
- <div className="text-sm text-gray-500">
459
- files in your workspace
460
- {uploadingCount > 0 && (
461
- <span className="ml-2 text-blue-600">({uploadingCount} uploading)</span>
462
- )}
463
- </div>
464
  </div>
465
-
466
- {/* Profiles Extracted Card */}
467
- <div className="bg-white rounded-lg border border-gray-200 p-6 shadow-sm">
468
- <div className="flex items-center gap-2 text-blue-600 text-sm font-medium mb-3">
469
- <CheckSquare className="w-4 h-4" />
470
- PROFILES EXTRACTED
471
- </div>
472
- <div className="text-5xl font-bold text-blue-600 mb-1">{profilesExtracted}</div>
473
- <div className="text-sm text-gray-500">
474
- structured profiles
475
- {extractingCount > 0 && (
476
- <span className="ml-2 text-purple-600">({extractingCount} extracting)</span>
477
- )}
478
- </div>
479
  </div>
 
480
 
481
- {/* Extraction Rate Card */}
482
- <div className="bg-white rounded-lg border border-gray-200 p-6 shadow-sm">
483
- <div className="flex items-center gap-2 text-blue-600 text-sm font-medium mb-3">
484
- <TrendingUp className="w-4 h-4" />
485
- EXTRACTION RATE
486
- </div>
487
- <div className="text-5xl font-bold text-yellow-500 mb-1">{extractionRate}%</div>
488
- <div className="text-sm text-gray-500">of uploaded CVs processed</div>
489
  </div>
 
 
490
  </div>
491
- </section>
492
-
493
- {/* ============================================================================ */}
494
- {/* UPLOAD CV SECTION */}
495
- {/* ============================================================================ */}
496
- <section className="mb-8">
497
- <h2 className="text-xl font-semibold text-gray-900 mb-4">Upload Candidate CVs</h2>
498
-
499
- {/* File Upload Dropzone */}
500
- <div
501
- className={`relative border-2 border-dashed rounded-lg p-12 mb-6 transition-colors ${
502
- dragActive
503
- ? 'border-blue-500 bg-blue-50'
504
- : 'border-gray-300 bg-white'
505
  } ${uploadingCount > 0 ? 'pointer-events-none opacity-60' : ''}`}
506
- onDragEnter={handleDrag}
507
- onDragLeave={handleDrag}
508
- onDragOver={handleDrag}
509
- onDrop={handleDrop}
510
- >
511
- {/* Loading Overlay */}
512
- {uploadingCount > 0 && (
513
- <div className="absolute inset-0 bg-white bg-opacity-90 flex items-center justify-center rounded-lg z-10">
514
- <div className="text-center">
515
- <Loader2 className="w-12 h-12 text-blue-600 animate-spin mx-auto mb-4" />
516
- <p className="text-gray-700 font-medium">Uploading files...</p>
517
- <p className="text-sm text-gray-500 mt-1">
518
- {uploadingCount} file(s) in progress
519
- </p>
520
- </div>
521
  </div>
522
- )}
 
523
 
524
- <div className="text-center">
525
- <div className="mb-4 flex justify-center">
526
- <div className="w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center">
527
- <Upload className="w-8 h-8 text-gray-400" />
528
- </div>
529
  </div>
530
- <p className="text-gray-700 font-medium mb-2">
531
- Drop PDF files here or click to browse
532
- </p>
533
- <p className="text-gray-500 text-sm mb-6">
534
- Drag and drop files here
535
- <br />
536
- Limit 200MB per file • PDF
537
- </p>
538
- <label htmlFor="file-upload" className="cursor-pointer">
539
- <span className="inline-block bg-blue-100 text-blue-600 px-6 py-2 rounded-md font-medium hover:bg-blue-200 transition-colors">
540
- Browse files
541
- </span>
542
- <input
543
- id="file-upload"
544
- type="file"
545
- className="hidden"
546
- multiple
547
- accept=".pdf"
548
- onChange={handleChange}
549
- disabled={uploadingCount > 0}
550
- />
551
- </label>
552
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
553
  </div>
554
- </section>
555
-
556
- {/* ============================================================================ */}
557
- {/* UPLOADED FILES LIST SECTION */}
558
- {/* ============================================================================ */}
559
- <section>
560
- <div className="flex items-center justify-between mb-4">
561
- <h2 className="text-xl font-semibold text-gray-700">Your Uploaded Files</h2>
562
- <button
563
- onClick={() => refetch()}
564
- disabled={isLoading}
565
- className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-md font-medium hover:bg-blue-700 transition-colors disabled:opacity-50"
566
- >
567
- <RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
568
- Refresh List
569
- </button>
570
- </div>
 
571
 
572
- {/* Files Table */}
573
- <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
574
- <table className="w-full">
575
- <thead className="bg-gray-50 border-b border-gray-200">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
  <tr>
577
- <th className="text-left px-6 py-3 text-sm font-medium text-gray-700">
578
- Filename
579
- </th>
580
- <th className="text-left px-6 py-3 text-sm font-medium text-gray-700">
581
- Status
582
- </th>
583
- <th className="text-left px-6 py-3 text-sm font-medium text-gray-700">
584
- Uploaded
585
- </th>
586
- <th className="text-right px-6 py-3 text-sm font-medium text-gray-700">
587
- Actions
588
- </th>
589
  </tr>
590
- </thead>
591
- <tbody>
592
- {isLoading ? (
593
- <tr>
594
- <td colSpan={4} className="px-6 py-8 text-center">
595
- <Loader2 className="w-6 h-6 animate-spin mx-auto text-blue-600" />
596
- </td>
597
- </tr>
598
- ) : error ? (
599
- <tr>
600
- <td colSpan={4} className="px-6 py-8 text-center text-red-600">
601
- Failed to load files. Please try again.
602
- </td>
603
- </tr>
604
- ) : allFiles.length === 0 ? (
605
- <tr>
606
- <td colSpan={4} className="px-6 py-8 text-center text-gray-500">
607
- No files uploaded yet
608
- </td>
609
- </tr>
610
- ) : (
611
- allFiles.map((file) => {
612
- const displayFile = getFileDisplayStatus(file);
613
- return (
614
- <tr key={file.id} className="border-b border-gray-100 hover:bg-gray-50">
615
- <td className="px-6 py-4">
616
- <div>
617
- <p className="text-sm font-medium text-gray-900">{file.filename}</p>
618
- {file.uploadError && (
619
- <p className="text-xs text-red-600 mt-1">Upload: {file.uploadError}</p>
620
- )}
621
- {file.extractError && (
622
- <p className="text-xs text-orange-600 mt-1">Extract: {file.extractError}</p>
623
- )}
624
- </div>
625
- </td>
626
- <td className="px-6 py-4">
627
- {renderStatusBadge(file)}
628
- </td>
629
- <td className="px-6 py-4 text-sm text-gray-600">
630
- {file.uploadStatus === 'uploading' ? (
631
- <span className="text-gray-400">In progress...</span>
632
- ) : (
633
- new Date(file.uploaded).toLocaleString('en-US', {
634
- year: 'numeric',
635
- month: '2-digit',
636
- day: '2-digit',
637
- hour: '2-digit',
638
- minute: '2-digit',
639
- hour12: false
640
- }).replace(',', '')
641
  )}
642
- </td>
643
- <td className="px-6 py-4">
644
- <div className="flex items-center justify-end gap-2">
645
- {/* Extract Button */}
646
- {file.uploadStatus === 'success' &&
647
- displayFile.extractStatus !== 'extracted' &&
648
- displayFile.extractStatus !== 'extracting' && (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
649
  <button
650
  onClick={() => extractData(file.id, file.filename)}
651
- className="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
652
  >
653
  <FileText className="w-3 h-3" />
654
  Extract
655
  </button>
656
  )}
657
 
658
- {/* Retry Extract Button */}
659
- {file.extractStatus === 'extract_error' && (
660
- <button
661
- onClick={() => retryExtraction(file.id, file.filename)}
662
- className="text-xs text-purple-600 hover:text-purple-800 font-medium"
663
- >
664
- Retry Extract
665
- </button>
666
- )}
667
-
668
- {/* Delete Button */}
669
  <button
670
- onClick={() => deleteFile(file.id)}
671
- className="text-xs text-red-600 hover:text-red-800 font-medium"
672
- disabled={file.uploadStatus === 'uploading' || displayFile.extractStatus === 'extracting'}
673
  >
674
- {file.uploadStatus === 'uploading' ? 'Cancel' : 'Delete'}
675
  </button>
676
- </div>
677
- </td>
678
- </tr>
679
- );
680
- })
681
- )}
682
- </tbody>
683
- </table>
684
- </div>
685
- </section>
686
- </div>
 
 
 
 
 
 
 
 
 
 
687
  );
688
  }
 
29
  throw new Error('Failed to fetch files');
30
  }
31
  const data = await response.json();
32
+
33
  return data.map((file: any) => ({
34
  id: file.file_id, // ← Map file_id to id
35
  filename: file.filename,
 
44
  const uploadFile = async (file: File, signal?: AbortSignal) => {
45
  const formData = new FormData();
46
  formData.append('files', file);
47
+
48
  const response = await fetch('/api/file/upload', {
49
  method: 'POST',
50
  body: formData,
 
72
  return response.json();
73
  };
74
 
75
+ const deleteFileApi = async (filename: string) => {
76
+ const response = await fetch(`/api/file/delete?filename=${filename}`, {
77
  method: 'DELETE',
78
  });
79
 
 
91
  const [dragActive, setDragActive] = useState(false);
92
  const [uploadingFiles, setUploadingFiles] = useState<UploadedFile[]>([]);
93
  const [extractingFiles, setExtractingFiles] = useState<Set<string>>(new Set());
94
+ const { user } = useAuth()
95
+
96
  const queryClient = useQueryClient();
97
  const abortControllersRef = useRef<Map<string, AbortController>>(new Map());
98
 
99
  // ============================================================================
100
  // FETCH USER FILES (AUTO-FETCHES ON MOUNT)
101
  // ============================================================================
102
+ const {
103
+ data: serverFiles = [],
104
+ isLoading,
105
  error,
106
+ refetch
107
  } = useQuery({
108
  queryKey: ['userFiles', user],
109
  queryFn: () => fetchUserFiles(user?.user_id ?? ""),
 
121
  // EXTRACT MUTATION
122
  // ============================================================================
123
  const extractMutation = useMutation({
124
+ mutationFn: ({ filename, fileId }: { filename: string; fileId: string }) => extractFileData(filename),
125
+ onMutate: (variables) => {
126
+ // Add to extracting set immediately
127
+ setExtractingFiles(prev => new Set(prev).add(variables.fileId));
128
+ },
129
  onSuccess: (data, variables) => {
 
 
 
130
  // Remove from extracting set
131
  setExtractingFiles(prev => {
132
  const newSet = new Set(prev);
133
+ newSet.delete(variables.fileId);
 
134
  return newSet;
135
  });
136
+
137
+ // Update the file in server data
138
+ queryClient.invalidateQueries({ queryKey: ['userFiles', user?.user_id] });
139
  },
140
  onError: (error, variables) => {
141
  // Remove from extracting set
142
  setExtractingFiles(prev => {
143
  const newSet = new Set(prev);
144
+ newSet.delete(variables.fileId);
 
145
  return newSet;
146
  });
147
+
148
+ // Update the specific file with error status
149
+ queryClient.setQueryData(['userFiles', user?.user_id], (oldData: UploadedFile[] | undefined) => {
150
+ if (!oldData) return oldData;
151
+
152
+ return oldData.map(file =>
153
+ file.id === variables.fileId
154
+ ? {
155
+ ...file,
156
+ extractStatus: 'extract_error' as const,
157
+ extractError: error instanceof Error ? error.message : 'Extraction failed'
158
+ }
159
+ : file
160
+ );
161
+ });
162
  },
163
  });
164
 
165
+
166
  // ============================================================================
167
  // DELETE MUTATION
168
  // ============================================================================
 
177
  // UPLOAD SINGLE FILE
178
  // ============================================================================
179
  const uploadSingleFile = async (
180
+ file: File,
181
  tempId: string,
182
  abortController: AbortController
183
  ): Promise<{ success: boolean; data?: any; error?: string; cancelled?: boolean }> => {
 
188
  if (error instanceof Error && error.name === 'AbortError') {
189
  return { success: false, cancelled: true };
190
  }
191
+
192
+ return {
193
+ success: false,
194
+ error: error instanceof Error ? error.message : 'Upload failed'
195
  };
196
  }
197
  };
 
202
  const uploadToAPI = async (files: FileList) => {
203
  const tempFiles: UploadedFile[] = Array.from(files).map((file) => {
204
  const id = `temp-${Date.now()}-${Math.random()}`;
205
+
206
  return {
207
  id,
208
  filename: file.name,
 
220
  const uploadPromises = tempFiles.map((tempFile, index) => {
221
  const abortController = new AbortController();
222
  abortControllersRef.current.set(tempFile.id, abortController);
223
+
224
  return uploadSingleFile(files[index], tempFile.id, abortController);
225
  });
226
 
 
250
  };
251
  } else {
252
  abortControllersRef.current.delete(file.id);
253
+ const errorMessage = result.status === 'fulfilled'
254
+ ? result.value.error
255
  : 'Upload failed';
256
+
257
  return {
258
  ...file,
259
  uploadStatus: 'error' as const,
 
288
  // EXTRACT DATA FROM FILE
289
  // ============================================================================
290
  const extractData = async (fileId: string, filename: string) => {
291
+ extractMutation.mutate({ filename, fileId });
 
292
  };
293
 
294
  // ============================================================================
 
297
  const retryExtraction = async (fileId: string, filename: string) => {
298
  await extractData(fileId, filename);
299
  };
 
300
  // ============================================================================
301
  // HANDLE FILE DRAG EVENTS
302
  // ============================================================================
 
317
  e.preventDefault();
318
  e.stopPropagation();
319
  setDragActive(false);
320
+
321
  if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
322
  await uploadToAPI(e.dataTransfer.files);
323
  }
 
338
  // ============================================================================
339
  const deleteFile = (fileId: string) => {
340
  const file = allFiles.find(f => f.id === fileId);
341
+
342
  // If uploading, abort
343
  if (file?.uploadStatus === 'uploading') {
344
  const abortController = abortControllersRef.current.get(fileId);
 
349
  setUploadingFiles(prev => prev.filter(f => f.id !== fileId));
350
  return;
351
  }
352
+
353
  // If uploaded, delete from server
354
+ deleteMutation.mutate(file?.filename ?? "");
355
  };
356
 
357
  // ============================================================================
 
359
  // ============================================================================
360
  const totalCVs = allFiles.filter(f => f.uploadStatus === 'success').length;
361
  const profilesExtracted = allFiles.filter(f => f.extractStatus === 'extracted').length;
362
+ const extractionRate = totalCVs > 0 ? ((profilesExtracted / totalCVs) * 100).toFixed(0) : '0';
363
  const uploadingCount = uploadingFiles.filter(f => f.uploadStatus === 'uploading').length;
364
  const extractingCount = extractingFiles.size;
365
 
 
378
  // ============================================================================
379
  const renderStatusBadge = (file: UploadedFile) => {
380
  const displayFile = getFileDisplayStatus(file);
381
+
382
  // Upload status
383
  if (displayFile.uploadStatus === 'uploading') {
384
  return (
385
+ <span className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-700">
386
  <Loader2 className="w-3 h-3 animate-spin" />
387
  Uploading
388
  </span>
389
  );
390
  }
391
+
392
  if (displayFile.uploadStatus === 'error') {
393
  return (
394
  <span className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-700">
 
444
  };
445
 
446
  return (
447
+ <div className="">
448
+ {/* ============================================================================ */}
449
+ {/* TITLE AND DESCRIPTION */}
450
+ {/* ============================================================================ */}
451
+ <div className="mb-8">
452
+ <p className="text-gray-600">
453
+ Upload CVs, extract candidate profiles, and track your workspace.
454
+ </p>
455
+ </div>
456
+
457
+ {/* ============================================================================ */}
458
+ {/* DASHBOARD STATISTICS CARDS */}
459
+ {/* ============================================================================ */}
460
+ <section className="mb-8">
461
+ <div className="flex items-center gap-2 mb-4">
462
+ <BarChart3 className="w-5 h-5 text-green-600" />
463
+ <h2 className="text-lg font-semibold text-green-600">Dashboard Overview</h2>
464
  </div>
465
 
466
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
467
+ {/* Total CV Card */}
468
+ <div className="bg-white rounded-lg border border-gray-200 p-6 shadow-sm">
469
+ <div className="flex items-center gap-2 text-green-600 text-sm font-medium mb-3">
470
+ <Upload className="w-4 h-4" />
471
+ TOTAL CV
472
+ </div>
473
+ <div className="text-5xl font-bold text-green-600 mb-1">{totalCVs}</div>
474
+ <div className="text-sm text-gray-500">
475
+ files in your workspace
476
+ {uploadingCount > 0 && (
477
+ <span className="ml-2 text-green-600">({uploadingCount} uploading)</span>
478
+ )}
479
+ </div>
480
  </div>
481
 
482
+ {/* Profiles Extracted Card */}
483
+ <div className="bg-white rounded-lg border border-gray-200 p-6 shadow-sm">
484
+ <div className="flex items-center gap-2 text-green-600 text-sm font-medium mb-3">
485
+ <CheckSquare className="w-4 h-4" />
486
+ PROFILES EXTRACTED
 
 
 
 
 
 
 
 
 
487
  </div>
488
+ <div className="text-5xl font-bold text-green-600 mb-1">{profilesExtracted}</div>
489
+ <div className="text-sm text-gray-500">
490
+ structured profiles
491
+ {extractingCount > 0 && (
492
+ <span className="ml-2 text-purple-600">({extractingCount} extracting)</span>
493
+ )}
 
 
 
 
 
 
 
 
494
  </div>
495
+ </div>
496
 
497
+ {/* Extraction Rate Card */}
498
+ <div className="bg-white rounded-lg border border-gray-200 p-6 shadow-sm">
499
+ <div className="flex items-center gap-2 text-green-600 text-sm font-medium mb-3">
500
+ <TrendingUp className="w-4 h-4" />
501
+ EXTRACTION RATE
 
 
 
502
  </div>
503
+ <div className="text-5xl font-bold text-yellow-500 mb-1">{extractionRate}%</div>
504
+ <div className="text-sm text-gray-500">of uploaded CV processed</div>
505
  </div>
506
+ </div>
507
+ </section>
508
+
509
+ {/* ============================================================================ */}
510
+ {/* UPLOAD CV SECTION */}
511
+ {/* ============================================================================ */}
512
+ <section className="mb-8">
513
+ <h2 className="text-xl font-semibold text-gray-900 mb-4">Upload Candidate CVs</h2>
514
+
515
+ {/* File Upload Dropzone */}
516
+ <div
517
+ className={`relative border-2 border-dashed rounded-lg p-12 mb-6 transition-colors ${dragActive
518
+ ? 'border-green-500 bg-green-50'
519
+ : 'border-gray-300 bg-white'
520
  } ${uploadingCount > 0 ? 'pointer-events-none opacity-60' : ''}`}
521
+ onDragEnter={handleDrag}
522
+ onDragLeave={handleDrag}
523
+ onDragOver={handleDrag}
524
+ onDrop={handleDrop}
525
+ >
526
+ {/* Loading Overlay */}
527
+ {uploadingCount > 0 && (
528
+ <div className="absolute inset-0 bg-white bg-opacity-90 flex items-center justify-center rounded-lg z-10">
529
+ <div className="text-center">
530
+ <Loader2 className="w-12 h-12 text-green-600 animate-spin mx-auto mb-4" />
531
+ <p className="text-gray-700 font-medium">Uploading files...</p>
532
+ <p className="text-sm text-gray-500 mt-1">
533
+ {uploadingCount} file(s) in progress
534
+ </p>
 
535
  </div>
536
+ </div>
537
+ )}
538
 
539
+ <div className="text-center">
540
+ <div className="mb-4 flex justify-center">
541
+ <div className="w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center">
542
+ <Upload className="w-8 h-8 text-gray-400" />
 
543
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
544
  </div>
545
+ <p className="text-gray-700 font-medium mb-2">
546
+ Drop PDF files here or click to browse
547
+ </p>
548
+ <p className="text-gray-500 text-sm mb-6">
549
+ Drag and drop files here
550
+ <br />
551
+ Limit 3MB per file • PDF
552
+ </p>
553
+ <label htmlFor="file-upload" className="cursor-pointer">
554
+ <span className="inline-block bg-green-100 text-green-600 px-6 py-2 rounded-md font-medium hover:bg-green-200 transition-colors">
555
+ Browse files
556
+ </span>
557
+ <input
558
+ id="file-upload"
559
+ type="file"
560
+ className="hidden"
561
+ multiple
562
+ accept=".pdf"
563
+ onChange={handleChange}
564
+ disabled={uploadingCount > 0}
565
+ />
566
+ </label>
567
  </div>
568
+ </div>
569
+ </section>
570
+
571
+ {/* ============================================================================ */}
572
+ {/* UPLOADED FILES LIST SECTION */}
573
+ {/* ============================================================================ */}
574
+ <section>
575
+ <div className="flex items-center justify-between mb-4">
576
+ <h2 className="text-xl font-semibold text-gray-700">Your Uploaded Files</h2>
577
+ <button
578
+ onClick={() => refetch()}
579
+ disabled={isLoading}
580
+ className="flex items-center gap-2 bg-green-600 text-white px-4 py-2 rounded-md font-medium hover:bg-green-700 transition-colors disabled:opacity-50"
581
+ >
582
+ <RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
583
+ Refresh List
584
+ </button>
585
+ </div>
586
 
587
+ {/* Files Table */}
588
+ <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
589
+ <table className="w-full">
590
+ <thead className="bg-gray-50 border-b border-gray-200">
591
+ <tr>
592
+ <th className="text-left px-6 py-3 text-sm font-medium text-gray-700">
593
+ Filename
594
+ </th>
595
+ <th className="text-left px-6 py-3 text-sm font-medium text-gray-700">
596
+ Status
597
+ </th>
598
+ <th className="text-left px-6 py-3 text-sm font-medium text-gray-700">
599
+ Uploaded
600
+ </th>
601
+ <th className="text-right px-6 py-3 text-sm font-medium text-gray-700">
602
+ Actions
603
+ </th>
604
+ </tr>
605
+ </thead>
606
+ <tbody>
607
+ {isLoading ? (
608
  <tr>
609
+ <td colSpan={4} className="px-6 py-8 text-center">
610
+ <Loader2 className="w-6 h-6 animate-spin mx-auto text-green-600" />
611
+ </td>
 
 
 
 
 
 
 
 
 
612
  </tr>
613
+ ) : error ? (
614
+ <tr>
615
+ <td colSpan={4} className="px-6 py-8 text-center text-red-600">
616
+ Failed to load files. Please try again.
617
+ </td>
618
+ </tr>
619
+ ) : allFiles.length === 0 ? (
620
+ <tr>
621
+ <td colSpan={4} className="px-6 py-8 text-center text-gray-500">
622
+ No files uploaded yet
623
+ </td>
624
+ </tr>
625
+ ) : (
626
+ allFiles.map((file) => {
627
+ const displayFile = getFileDisplayStatus(file);
628
+ return (
629
+ <tr key={file.id} className="border-b border-gray-100 hover:bg-gray-50">
630
+ <td className="px-6 py-4">
631
+ <div>
632
+ <p className="text-sm font-medium text-gray-900">{file.filename}</p>
633
+ {file.uploadError && (
634
+ <p className="text-xs text-red-600 mt-1">Upload: {file.uploadError}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
635
  )}
636
+ {file.extractError && (
637
+ <p className="text-xs text-orange-600 mt-1">Extract: {file.extractError}</p>
638
+ )}
639
+ </div>
640
+ </td>
641
+ <td className="px-6 py-4">
642
+ {renderStatusBadge(file)}
643
+ </td>
644
+ <td className="px-6 py-4 text-sm text-gray-600">
645
+ {file.uploadStatus === 'uploading' ? (
646
+ <span className="text-gray-400">In progress...</span>
647
+ ) : (
648
+ new Date(file.uploaded).toLocaleString('en-US', {
649
+ year: 'numeric',
650
+ month: '2-digit',
651
+ day: '2-digit',
652
+ hour: '2-digit',
653
+ minute: '2-digit',
654
+ hour12: false
655
+ }).replace(',', '')
656
+ )}
657
+ </td>
658
+ <td className="px-6 py-4">
659
+ <div className="flex items-center justify-end gap-2">
660
+ {/* Extract Button */}
661
+ {file.uploadStatus === 'success' &&
662
+ displayFile.extractStatus !== 'extracted' &&
663
+ displayFile.extractStatus !== 'extracting' && (
664
  <button
665
  onClick={() => extractData(file.id, file.filename)}
666
+ className="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium text-white bg-green-600 hover:bg-green-700 rounded-md transition-colors"
667
  >
668
  <FileText className="w-3 h-3" />
669
  Extract
670
  </button>
671
  )}
672
 
673
+ {/* Retry Extract Button */}
674
+ {file.extractStatus === 'extract_error' && (
 
 
 
 
 
 
 
 
 
675
  <button
676
+ onClick={() => retryExtraction(file.id, file.filename)}
677
+ className="text-xs text-purple-600 hover:text-purple-800 font-medium"
 
678
  >
679
+ Retry Extract
680
  </button>
681
+ )}
682
+
683
+ {/* Delete Button */}
684
+ <button
685
+ onClick={() => deleteFile(file.id)}
686
+ className="text-xs text-red-600 hover:text-red-800 font-medium"
687
+ disabled={file.uploadStatus === 'uploading' || displayFile.extractStatus === 'extracting'}
688
+ >
689
+ {file.uploadStatus === 'uploading' ? 'Cancel' : 'Delete'}
690
+ </button>
691
+ </div>
692
+ </td>
693
+ </tr>
694
+ );
695
+ })
696
+ )}
697
+ </tbody>
698
+ </table>
699
+ </div>
700
+ </section>
701
+ </div>
702
  );
703
  }
src/components/dashboard/metrics-row.tsx CHANGED
@@ -4,6 +4,8 @@
4
  import { Card } from "@/components/ui/card";
5
  import { authFetch } from "@/lib/api";
6
  import { useQuery } from "@tanstack/react-query";
 
 
7
 
8
  type ScoreData = {
9
  data: {
@@ -24,7 +26,7 @@ const fallbackScore: ScoreData = {
24
  function CircularProgress({ percentage }: { percentage: number }) {
25
  // Format to max 2 decimal places
26
  const formattedPercentage = Number(percentage.toFixed(2));
27
-
28
  return (
29
  <div className="relative w-24 h-24 flex-shrink-0">
30
  <svg className="w-full h-full transform -rotate-90">
@@ -59,7 +61,7 @@ function CircularProgress({ percentage }: { percentage: number }) {
59
  }
60
 
61
  export function MetricsRow() {
62
-
63
  const fetchScore = async (): Promise<ScoreData> => {
64
  const res = await authFetch(`/api/file/score_card`)
65
  if (!res.ok) throw new Error("Failed to fetch score")
@@ -91,6 +93,10 @@ export function MetricsRow() {
91
  hour12: true
92
  });
93
 
 
 
 
 
94
  if (loadingScore) {
95
  return (
96
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -111,36 +117,41 @@ export function MetricsRow() {
111
  }
112
 
113
  return (
114
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
115
  {/* Extracted Profile Card */}
116
  <Card className="p-6 border bg-white hover:shadow-md transition-shadow">
117
- <div className="flex items-start gap-4">
118
- <div className="flex-1">
119
- <div className="flex items-center gap-3 mb-3">
120
- <span className="inline-flex items-center px-3 py-1.5 rounded-full text-sm font-semibold bg-green-100 text-green-800">
121
- Extracted Profile
122
- </span>
123
- <span className="text-3xl font-bold text-gray-900">
124
- {scoreData.data.total_extracted}
125
- </span>
126
- </div>
127
- <div className="flex items-center gap-2 text-sm text-gray-600">
128
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
129
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
130
- </svg>
131
- <span>
132
- Active Candidates: <strong>{scoreData.data.total_extracted}</strong> / Total Profiles: <strong>{scoreData.data.total_file}</strong>
133
- </span>
 
 
134
  </div>
135
  </div>
 
 
 
136
  </div>
137
  </Card>
138
 
139
  {/* Processed Card */}
140
- <Card className="p-6 border bg-white hover:shadow-md transition-shadow">
141
  <div className="flex items-start gap-6">
142
  <CircularProgress percentage={formattedPercentage} />
143
-
144
  <div className="flex-1">
145
  <div className="mb-4">
146
  <div className="text-3xl font-bold text-green-600 mb-1">
@@ -150,13 +161,13 @@ export function MetricsRow() {
150
  Total {scoreData.data.total_file}
151
  </div>
152
  </div>
153
-
154
  <div className="text-xs text-gray-500">
155
  Last Processed: {lastProcessed}
156
  </div>
157
  </div>
158
  </div>
159
- </Card>
160
  </div>
161
  );
162
  }
 
4
  import { Card } from "@/components/ui/card";
5
  import { authFetch } from "@/lib/api";
6
  import { useQuery } from "@tanstack/react-query";
7
+ import { Plus } from "lucide-react";
8
+ import { useRouter } from "next/navigation";
9
 
10
  type ScoreData = {
11
  data: {
 
26
  function CircularProgress({ percentage }: { percentage: number }) {
27
  // Format to max 2 decimal places
28
  const formattedPercentage = Number(percentage.toFixed(2));
29
+
30
  return (
31
  <div className="relative w-24 h-24 flex-shrink-0">
32
  <svg className="w-full h-full transform -rotate-90">
 
61
  }
62
 
63
  export function MetricsRow() {
64
+ const router = useRouter()
65
  const fetchScore = async (): Promise<ScoreData> => {
66
  const res = await authFetch(`/api/file/score_card`)
67
  if (!res.ok) throw new Error("Failed to fetch score")
 
93
  hour12: true
94
  });
95
 
96
+ const handleAddData = () => {
97
+ router.push("/recruitment/upload")
98
+ }
99
+
100
  if (loadingScore) {
101
  return (
102
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
 
117
  }
118
 
119
  return (
120
+ <div className="grid grid-cols-1 gap-4">
121
  {/* Extracted Profile Card */}
122
  <Card className="p-6 border bg-white hover:shadow-md transition-shadow">
123
+ <div className="flex justify-between items-center">
124
+ <div className="flex items-start gap-4">
125
+ <div className="flex-1">
126
+ <div className="flex items-center gap-3 mb-3">
127
+ <span className="inline-flex items-center px-3 py-1.5 rounded-full text-sm font-semibold bg-green-100 text-green-800">
128
+ Profile Extracted
129
+ </span>
130
+ <span className="text-3xl font-bold text-gray-900">
131
+ {scoreData.data.total_extracted}
132
+ </span>
133
+ </div>
134
+ <div className="flex items-center gap-2 text-sm text-gray-600">
135
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
136
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
137
+ </svg>
138
+ <span>
139
+ <strong>{scoreData.data.total_extracted}</strong> / <strong>{scoreData.data.total_file}</strong>Total Profiles
140
+ </span>
141
+ </div>
142
  </div>
143
  </div>
144
+ <button onClick={handleAddData} className="cursor-pointer w-10 h-10 rounded-full bg-[#22c55e] text-white flex items-center justify-center hover:bg-[#16a34a] transition-colors">
145
+ <Plus className="w-5 h-5" />
146
+ </button>
147
  </div>
148
  </Card>
149
 
150
  {/* Processed Card */}
151
+ {/* <Card className="p-6 border bg-white hover:shadow-md transition-shadow">
152
  <div className="flex items-start gap-6">
153
  <CircularProgress percentage={formattedPercentage} />
154
+
155
  <div className="flex-1">
156
  <div className="mb-4">
157
  <div className="text-3xl font-bold text-green-600 mb-1">
 
161
  Total {scoreData.data.total_file}
162
  </div>
163
  </div>
164
+
165
  <div className="text-xs text-gray-500">
166
  Last Processed: {lastProcessed}
167
  </div>
168
  </div>
169
  </div>
170
+ </Card> */}
171
  </div>
172
  );
173
  }