validate(['file' => 'required|file|mimes:csv,xlsx,xls|max:20480']); $file = $request->file('file'); $result = $this->fastApi->classifyFile($file->getRealPath(), $file->getClientOriginalName()); if (isset($result['error'])) { if ($request->wantsJson()) return response()->json(['success' => false, 'message' => $result['error']], 400); return back()->with('error', $result['error']); } if ($request->wantsJson()) { return response()->json([ 'success' => true, 'job_id' => $result['job_id'] ?? null, 'message' => $result['message'] ?? 'Proses dimulai...' ]); } return back()->with('success', 'File sedang diproses. Silakan refresh beberapa saat lagi.'); } public function checkStatus(Request $request, string $jobId) { $status = $this->fastApi->getClassifyStatus($jobId); if (!isset($status['status']) || in_array($status['status'], ['pending', 'processing'])) { return response()->json($status); } if ($status['status'] === 'error') { return response()->json($status); } if ($status['status'] === 'success' && isset($status['results'])) { try { DB::transaction(function () use ($status) { $chunks = array_chunk($status['results'], 500); foreach ($chunks as $chunk) { $kemendikUpsertData = []; $internalResultsUpsertData = []; $internalRawUpsertData = []; foreach ($chunk as $row) { if ($row['source_type'] === 'kemendik') { $raw = $row['raw_data'] ?? []; $kemendikUpsertData[] = [ 'nimhsmsmh' => $row['nim'], 'source_type' => 'kemendik', 'nmmhsmsmh' => $row['nama'], 'tahun_lulus' => $row['tahun_lulus'] ?? null, 'f5c_jabatan_kode' => $raw['F5c'] ?? null, 'f5c_jabatan_text' => $this->mapF5c($raw['F5c'] ?? null), 'f1101_jenis_instansi_kode' => $raw['F1101'] ?? null, 'f1101_jenis_instansi_text' => $this->mapF1101($raw['F1101'] ?? null), 'f5a1_provinsi' => $raw['F5a1'] ?? null, 'raw_data' => json_encode($raw) ]; } else { $internalResultsUpsertData[] = [ 'nim' => $row['nim'], 'source_type' => 'internal_mif', 'job_text_raw' => $row['job_text_raw'] ?? '', 'predicted_profile' => $row['predicted_profile'], 'confidence_score' => $row['confidence_score'] ?? 0, 'classification_method' => $row['classification_method'], 'status' => $row['status'], 'error_detail' => $row['error_detail'] ?? null ]; if (isset($row['raw_data'])) { $mapped = ['nim' => $row['nim']]; $cols = [ 'nama_lengkap', 'email', 'no_telepon', 'alamat_domisili', 'jurusan', 'program_studi', 'tahun_masuk', 'tahun_lulus', 'status_pekerjaan', 'klasifikasi_pekerjaan', 'nama_perusahaan', 'jenis_perusahaan', 'jabatan', 'lokasi_perusahaan', 'alamat_perusahaan', 'deskripsi_pekerjaan', 'tahun_mulai_kerja', 'masa_kerja_bulan', 'jumlah_lamaran_dikirim', 'jumlah_respons_lamaran', 'jumlah_undangan_wawancara', 'masa_tunggu_pra_lulus', 'masa_tunggu_pasca_lulus', 'total_masa_tunggu', 'instansi_terupdate', 'jabatan_terupdate', 'tahun_bekerja', 'status_pekerjaan_terupdate', 'linkedin_profile', 'sosmed_ig' ]; foreach ($cols as $col) { $val = $row['raw_data'][$col] ?? null; $mapped[$col] = $val === '' ? null : $val; } $internalRawUpsertData[] = $mapped; } } } if (!empty($kemendikUpsertData)) { KemendikRawData::upsert( $kemendikUpsertData, ['nimhsmsmh', 'source_type'], ['nmmhsmsmh', 'tahun_lulus', 'f5c_jabatan_kode', 'f5c_jabatan_text', 'f1101_jenis_instansi_kode', 'f1101_jenis_instansi_text', 'f5a1_provinsi', 'raw_data'] ); } if (!empty($internalResultsUpsertData)) { ClassificationResult::upsert( $internalResultsUpsertData, ['nim', 'source_type'], ['job_text_raw', 'predicted_profile', 'confidence_score', 'classification_method', 'status', 'error_detail'] ); } if (!empty($internalRawUpsertData)) { $cols = [ 'nama_lengkap', 'email', 'no_telepon', 'alamat_domisili', 'jurusan', 'program_studi', 'tahun_masuk', 'tahun_lulus', 'status_pekerjaan', 'klasifikasi_pekerjaan', 'nama_perusahaan', 'jenis_perusahaan', 'jabatan', 'lokasi_perusahaan', 'alamat_perusahaan', 'deskripsi_pekerjaan', 'tahun_mulai_kerja', 'masa_kerja_bulan', 'jumlah_lamaran_dikirim', 'jumlah_respons_lamaran', 'jumlah_undangan_wawancara', 'masa_tunggu_pra_lulus', 'masa_tunggu_pasca_lulus', 'total_masa_tunggu', 'instansi_terupdate', 'jabatan_terupdate', 'tahun_bekerja', 'status_pekerjaan_terupdate', 'linkedin_profile', 'sosmed_ig' ]; InternalRawData::upsert($internalRawUpsertData, ['nim'], $cols); } } }); $source_type = $status['source_type'] ?? 'unknown'; if ($source_type === 'kemendik') { $count = collect($status['results'])->where('source_type','kemendik')->count(); $msg = "Selesai. {$status['processed_rows']} baris diproses. {$count} data Kemendik masuk."; } else { $classified = collect($status['results'])->where('source_type','internal_mif')->where('status','auto_classified')->count(); $msg = "Selesai. {$status['processed_rows']} data diproses. {$classified} otomatis (MIF)."; } // Hapus data results dari file status agar tidak berat saat diambil UI selanjutnya? Tidak perlu. return response()->json(['status' => 'completed', 'message' => $msg]); } catch (\Exception $e) { Log::error("DB Upsert failed: " . $e->getMessage()); return response()->json(['status' => 'error', 'message' => "Gagal menyimpan database: " . $e->getMessage()]); } } return response()->json(['status' => 'error', 'message' => 'Status tidak dikenali.']); } private function mapF5c(?string $c): ?string { return ['1'=>'Founder','2'=>'Co-Founder','3'=>'Staff','4'=>'Freelance'][$c] ?? null; } private function mapF1101(?string $c): ?string { return ['1'=>'Pemerintah','2'=>'Non-profit','3'=>'Swasta','4'=>'Wiraswasta','6'=>'BUMN','7'=>'Multilateral'][$c] ?? null; } public function retrain(Request $request) { $overrideData = ClassificationResult::where('source_type', 'internal_mif') ->where('status', 'manual_override') ->whereNotNull('job_text_raw') ->whereNotNull('predicted_profile') ->get(['nim', 'job_text_raw', 'predicted_profile']); $csvPath = null; if ($overrideData->count() > 0) { $csvLines = ['nim;job_text_raw;label']; foreach ($overrideData as $row) { $nim = $row->nim; $text = str_replace([';', "\n", "\r"], [',', ' ', ' '], $row->job_text_raw); $label = $row->predicted_profile; $csvLines[] = "{$nim};{$text};{$label}"; } $tmpDir = storage_path('app/temp'); if (!is_dir($tmpDir)) { mkdir($tmpDir, 0755, true); } $csvPath = $tmpDir . '/manual_override_' . Str::uuid() . '.csv'; file_put_contents($csvPath, implode("\n", $csvLines)); Log::info("Retrain: Exported {$overrideData->count()} manual_override rows to {$csvPath}"); } else { Log::info('Retrain: Tidak ada data manual_override, training hanya dengan corpus asli.'); } $result = $this->fastApi->triggerRetrain($csvPath); if ($csvPath && file_exists($csvPath)) { try { unlink($csvPath); } catch (\Exception $e) { Log::warning("Gagal menghapus temp CSV: " . $e->getMessage()); } } if (isset($result['error'])) { return response()->json(['success' => false, 'message' => $result['error']], 422); } return response()->json([ 'success' => true, 'message' => 'Re-training dimulai di background.', 'override_count' => $overrideData->count(), ]); } public function retrainStatus(Request $request) { $status = $this->fastApi->getRetrainStatus(); return response()->json($status); } }