'valid', 'menunggu' => 'menunggu', 'ditolak' => 'tidak valid' ]; const REJECTION_REASONS_BY_CRITERIA = [ 'Pekerjaan Orang Tua' => [ 'Surat keterangan kerja tidak valid', 'Tidak ada bukti usaha (untuk wiraswasta)', 'Dokumen kadaluarsa', 'Dokumen tidak sesuai', 'Lainnya' ], 'Penghasilan Orang Tua' => [ 'Jumlah gaji tidak sesuai', 'Slip gaji tidak terbaca', 'Dokumen tidak sesuai', 'Lainnya' ], 'Status Orang Tua' => [ 'Dokumen tidak sesuai', 'Dokumen tidak terbaca', 'Tidak ada bukti surat perceraian', 'Tidak ada bukti surat keterangan kematian', 'Lainnya' ], 'Tanggungan Orang Tua' => [ 'Kartu Keluarga tidak terbaca', 'Jumlah tanggungan tidak sesuai', 'Dokumen tidak sesuai', 'Lainnya' ], 'Hunian' => [ 'Bukti kepemilikan tidak valid', 'Dokumen tidak terbaca', 'Dokumen tidak sesuai', 'Lainnya' ], 'Kendaraan' => [ 'Dokumen tidak sesuai', 'STNK tidak valid', 'STNK tidak terbaca', 'Lainnya' ], 'IPK' => [ 'KHS tidak valid', 'KHS tidak terbaca', 'IPK tidak sesuai dengan sistem', 'Lainnya' ], 'Semester' => [ 'KHS tidak valid', 'KHS tidak terbaca', 'Semester tidak sesuai dengan sistem', 'Lainnya' ], 'Slip Pembayaran UKT' => [ 'Dokumen tidak terbaca', 'Nominal tidak sesuai', 'Slip pembayaran tidak sesuai', 'Lainnya' ] ]; /** * Display list of UKT submissions with filtering */ public function index() { $statusFilter = $this->mapStatusFilter(request('status')); $query = PengajuanUkt::with(['mahasiswa.user:id,name','form']) ->withCount('details'); if (request()->has('status_form_id') && request('status_form_id') != '') { $query->where('status_form_id', request('status_form_id')); } // Filter by jenis form if (request()->has('jenis_form') && request('jenis_form') != '') { $query->whereHas('form', function($q) { $q->where('jenis_form', request('jenis_form')); }); } if ($statusFilter) { $query->where('status_validasi', $statusFilter); } $pengajuan = $query->latest()->paginate(10); $forms = Form::select('id', 'nama_form') ->orderBy('created_at', 'desc') ->get(); $stats = [ 'totalPengajuan' => PengajuanUkt::count(), 'valid' => PengajuanUkt::valid()->count(), 'menunggu' => PengajuanUkt::menunggu()->count(), 'tidak valid' => PengajuanUkt::tidakValid()->count() ]; return view('admin.pengajuan.index', compact('pengajuan', 'stats', 'forms')); } /** * Map status filter to consistent values */ protected function mapStatusFilter($status) { return self::STATUS_MAPPING[$status] ?? $status; } public function detail($id) { $pengajuan = PengajuanUkt::with([ 'details' => function($query) { $query->select([ 'id', 'pengajuan_ukt_id', 'kriteria', 'subkriteria_text', 'file_dokumen', 'verified', 'rejection_reason', 'verified_by', 'verified_at' ])->with(['verifier:id,name']); }, 'mahasiswa.user:id,name' ])->select([ 'id', 'mahasiswa_id', 'jenis_pengajuan', 'alasan_pengajuan', 'ukt_saat_ini', 'status_validasi', 'created_at', 'updated_at' ])->findOrFail($id); return view('admin.pengajuan.detail', [ 'pengajuan' => $pengajuan, 'rejectionReasonsByCriteria' => self::REJECTION_REASONS_BY_CRITERIA ]); } public function bulkVerify(Request $request, $id) { if (auth()->user()->role !== 'karyawan') { abort(403, 'Hanya karyawan yang dapat melakukan verifikasi'); } $data = $request->all(); $rules = [ 'valid' => 'required|array', 'valid.*' => 'in:0,1', 'alasan' => 'nullable|array', 'alasan_lainnya' => 'nullable|array', ]; foreach ($data['valid'] as $detailId => $value) { if ((string) $value === '0') { $rules["alasan.$detailId"] = 'required|string'; if (isset($data['alasan'][$detailId]) && $data['alasan'][$detailId] === 'Lainnya') { $rules["alasan_lainnya.$detailId"] = 'required|string'; } } } $validator = Validator::make($data, $rules); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validasi gagal', 'errors' => $validator->errors() ], 422); } DB::beginTransaction(); try { $pengajuan = PengajuanUkt::with(['details', 'mahasiswa.user'])->findOrFail($id); foreach ($pengajuan->details as $detail) { $isValid = $request->input("valid.{$detail->id}") == '1'; $alasan = $request->input("alasan.{$detail->id}"); $alasanLainnya = $request->input("alasan_lainnya.{$detail->id}"); $rejectionReason = $this->processRejectionReason($alasan, $alasanLainnya); $detail->update([ 'verified' => $isValid, 'verified_by' => auth()->id(), 'verified_at' => now(), 'rejection_reason' => $isValid ? null : $rejectionReason, ]); } $status = $this->updatePengajuanStatus($pengajuan); DB::commit(); return response()->json([ 'success' => true, 'message' => 'Validasi dokumen berhasil disimpan! Status: ' . ($status === 'valid' ? 'Valid' : 'Tidak Valid'), 'redirect' => route('admin.pengajuan.detail', $id) ]); } catch (\Exception $e) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => 'Gagal memvalidasi pengajuan: ' . $e->getMessage() ], 500); } } public function revalidate(Request $request, $id) { $validator = Validator::make($request->all(), [ 'valid' => 'required|array', 'valid.*' => 'in:0,1', 'alasan' => 'required_without:valid.*|array', 'alasan_lainnya' => 'required_if:alasan.*,Lainnya|array' ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validasi gagal', 'errors' => $validator->errors() ], 422); } DB::beginTransaction(); try { $pengajuan = PengajuanUkt::with('details')->findOrFail($id); $changesMade = false; foreach ($request->input('valid') as $detailId => $isValid) { $detail = $pengajuan->details->firstWhere('id', $detailId); if (!$detail) continue; $alasan = $request->input("alasan.{$detailId}"); $alasanLainnya = $request->input("alasan_lainnya.{$detailId}"); $rejectionReason = $isValid ? null : $this->processRejectionReason($alasan, $alasanLainnya); // Check if changes are needed if ($detail->verified != $isValid || $detail->rejection_reason != $rejectionReason) { $detail->verified = $isValid; $detail->rejection_reason = $rejectionReason; $detail->verified_by = auth()->id(); $detail->verified_at = now(); $detail->save(); $changesMade = true; } } if ($changesMade) { $status = $this->updatePengajuanStatus($pengajuan); DB::commit(); return response()->json([ 'success' => true, 'message' => 'Revalidasi berhasil disimpan', 'redirect' => route('admin.pengajuan.detail', $id) ]); } DB::commit(); return response()->json([ 'success' => true, 'message' => 'Tidak ada perubahan yang dilakukan', 'redirect' => route('admin.pengajuan.detail', $id) ]); } catch (\Exception $e) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => 'Gagal menyimpan revalidasi: ' . $e->getMessage() ], 500); } } public function verifyUkt(Request $request, $id) { $validator = Validator::make($request->all(), [ 'valid_ukt' => 'required|boolean', 'alasan_ukt' => 'required_if:valid_ukt,0|nullable|string', 'alasan_lainnya_ukt' => 'required_if:alasan_ukt,Lainnya|nullable|string' ]); if ($validator->fails()) { return back() ->withErrors($validator) ->withInput(); } DB::beginTransaction(); try { $pengajuan = PengajuanUkt::with(['details', 'mahasiswa.user']) ->findOrFail($id); $alasanUkt = $this->processRejectionReason( $request->alasan_ukt, $request->alasan_lainnya_ukt ); $pengajuan->update([ 'file_ukt_valid' => $request->valid_ukt, 'file_ukt_alasan' => $request->valid_ukt ? null : $alasanUkt, 'updated_at' => now() ]); $this->updatePengajuanStatus($pengajuan); DB::commit(); return redirect() ->route('admin.pengajuan.detail', $id) ->with('success', 'Verifikasi Slip UKT berhasil disimpan'); } catch (\Exception $e) { DB::rollBack(); return back() ->withInput() ->with('error', 'Gagal memverifikasi Slip UKT: ' . $e->getMessage()); } } protected function processRejectionReason($alasan, $alasanLainnya) { if ($alasan === 'Lainnya') { return 'Lainnya: ' . $alasanLainnya; } return $alasan; } protected function updatePengajuanStatus($pengajuan) { $invalidCount = $pengajuan->details() ->where('verified', false) ->orWhereNotNull('rejection_reason') ->count(); $status = $invalidCount === 0 ? 'valid' : 'tidak valid'; $pengajuan->update([ 'status_validasi' => $status, 'updated_at' => now() ]); $this->sendNotification($pengajuan, $status); return $status; } private function sendNotification($pengajuan, $status) { try { if ($status === 'valid') { $pengajuan->mahasiswa->user->notify(new PengajuanValidNotification($pengajuan)); } else { $reasons = $pengajuan->details() ->whereNotNull('rejection_reason') ->pluck('rejection_reason', 'kriteria') ->map(function($reason, $kriteria) { return "$kriteria: " . (strpos($reason, 'Lainnya:') === 0 ? substr($reason, 8) : $reason); }) ->implode("\n"); $pengajuan->mahasiswa->user->notify( new PengajuanDitolakNotification($pengajuan, $reasons) ); } } catch (\Exception $e) { // Silent fail for notifications } } public function destroy($id) { DB::beginTransaction(); try { $pengajuan = PengajuanUkt::with(['details', 'hasilPenilaian'])->findOrFail($id); foreach ($pengajuan->details as $detail) { if ($detail->file_dokumen) { $filePath = 'public/' . ltrim($detail->file_dokumen, '/'); if (Storage::exists($filePath)) { Storage::delete($filePath); } } } $pengajuan->hasilPenilaian()->delete(); $pengajuan->details()->delete(); $pengajuan->delete(); DB::commit(); return redirect()->route('admin.pengajuan') ->with('success', 'Pengajuan berhasil dihapus'); } catch (\Exception $e) { DB::rollBack(); return back()->with('error', 'Gagal menghapus pengajuan'); } } public function storeRankingReport(Request $request) { $request->validate([ 'ranking_file' => 'required|file|mimes:pdf|max:10240' ]); DB::beginTransaction(); try { $existingFile = RankingFile::first(); if ($existingFile) { Storage::delete('public/' . $existingFile->path); $existingFile->delete(); } $file = $request->file('ranking_file'); $path = $file->store('public/ranking_files'); RankingFile::create([ 'path' => str_replace('public/', '', $path), 'uploaded_by' => auth()->id() ]); DB::commit(); return redirect()->route('admin.pengajuan') ->with('report_status', [ 'type' => 'success', 'message' => 'Laporan ranking berhasil dikirim ke mahasiswa!' ]); } catch (\Exception $e) { DB::rollBack(); return back()->with('report_status', [ 'type' => 'danger', 'message' => 'Gagal mengunggah laporan ranking: ' . $e->getMessage() ]); } } public function deleteRankingReport() { DB::beginTransaction(); try { $rankingFile = RankingFile::first(); if ($rankingFile) { Storage::delete('public/' . $rankingFile->path); $rankingFile->delete(); $message = 'Laporan ranking berhasil dibatalkan!'; $type = 'warning'; } else { $message = 'Tidak ada laporan ranking aktif'; $type = 'info'; } DB::commit(); return redirect()->route('admin.pengajuan') ->with('report_status', [ 'type' => $type, 'message' => $message ]); } catch (\Exception $e) { DB::rollBack(); return back()->with('report_status', [ 'type' => 'danger', 'message' => 'Gagal membatalkan laporan ranking: ' . $e->getMessage() ]); } } public function viewRankingReport($id) { $rankingFile = RankingFile::findOrFail($id); $filePath = storage_path('app/public/' . $rankingFile->path); if (!file_exists($filePath)) { abort(404, 'File laporan tidak ditemukan'); } return response()->download( $filePath, 'Laporan-UKT-'.$rankingFile->created_at->format('Y-m-d').'.pdf' ); } }