getMahasiswaUser($request); $outline = DB::table('tbpraoutline as tp') ->leftJoin('tbrekaphasil as trh', 'tp.id', '=', 'trh.idpraoutline') ->leftJoin('tb_kelompok_keahlian as kk', 'tp.kelompokKeahlian', '=', 'kk.idKK') ->select([ 'tp.id', 'tp.judul', 'tp.deskripsi', 'tp.status_usulan', 'tp.tgl_upload', 'tp.wkt_upload', 'trh.ket', 'kk.namaKK as kelompok_keahlian', ]) ->where('tp.nim', $user['nim']) ->where('tp.idProdi', $user['prodi']) ->orderByDesc('tp.id') ->first(); $reviews = []; if ($outline) { $reviews = DB::table('tbreview as tr') ->leftJoin('tbdosen as td', 'tr.reviewer', '=', 'td.nip') ->leftJoin('tbmhs as tm', 'tr.reviewer', '=', 'tm.nim') ->select([ 'tr.review_text', 'tr.jenis_review', 'tr.putusan', 'tr.tgl', 'tr.wkt', 'tr.reviewer', 'td.nmLengkap as reviewer_name', 'tm.nmLengkap as mahasiswa_name', ]) ->where('tr.idpraoutline', $outline->id) ->orderBy('tr.tgl') ->orderBy('tr.wkt') ->get() ->map(function ($item) { $timestamp = trim(($item->tgl ?? '').' '.($item->wkt ?? '')); return [ 'author' => $item->reviewer_name ?: ($item->mahasiswa_name ?: $item->reviewer), 'role' => $item->reviewer_name ? 'Dosen' : 'Mahasiswa', 'timestamp' => $this->formatDateTime($timestamp), 'type' => $item->jenis_review === 'P' ? 'Putusan' : 'Review', 'decision' => $this->decisionLabel($item->putusan), 'body' => $item->review_text ?: '
Tidak ada isi review.
', ]; }) ->all(); } return view('mahasiswa.pages.status-usulan', [ 'title' => 'Status Usulan Mahasiswa | SPOTA Rebuild', 'pageTitle' => 'Status Usulan', 'pageDescription' => 'Ringkasan draft praoutline terakhir, status keputusan, dan riwayat review dosen untuk mahasiswa yang sedang login.', 'pageDate' => Carbon::now()->locale('id')->translatedFormat('j F Y, H:i'), 'sidebar' => $this->buildSidebar('mahasiswa.status-usulan'), 'user' => $user, 'outline' => $outline ? [ 'id' => $outline->id, 'judul' => $outline->judul, 'deskripsi' => $outline->deskripsi, 'status' => $this->statusLabel($outline->status_usulan), 'statusClass' => $this->statusBadgeClass($outline->status_usulan), 'tanggal' => $this->formatDateTime(trim(($outline->tgl_upload ?? '').' '.($outline->wkt_upload ?? ''))), 'catatan' => $outline->ket, 'kelompokKeahlian' => $outline->kelompok_keahlian ?: '-', ] : null, 'reviews' => $reviews, 'pageActions' => $this->statusPageActions($outline), ]); } public function uploadPraoutline(Request $request): View { $user = $this->getMahasiswaUser($request); $hasActiveDraft = DB::table('tbpraoutline') ->where('nim', $user['nim']) ->whereIn('status_usulan', ['0', '1']) ->exists(); $dosen = DB::table('tbdosen') ->select(['nmLengkap']) ->where('idProdi', $user['prodi']) ->where('status', 'A') ->orderBy('nmLengkap') ->get(); $kelompokKeahlian = DB::table('tb_kelompok_keahlian') ->select(['idKK', 'namaKK']) ->orderBy('namaKK') ->get(); return view('mahasiswa.pages.upload-praoutline', [ 'title' => 'Ajukan Praoutline | SPOTA Rebuild', 'pageTitle' => 'Ajukan Outline Baru', 'pageDescription' => 'Form pengajuan draft praoutline mengikuti field utama pada SPOTA lama. File PDF tetap divalidasi sebelum disimpan.', 'pageDate' => Carbon::now()->locale('id')->translatedFormat('j F Y, H:i'), 'sidebar' => $this->buildSidebar('mahasiswa.praoutline.upload'), 'user' => $user, 'hasActiveDraft' => $hasActiveDraft, 'dosen' => $dosen, 'kelompokKeahlian' => $kelompokKeahlian, ]); } public function storePraoutline(Request $request): RedirectResponse { $user = $this->getMahasiswaUser($request); $hasActiveDraft = DB::table('tbpraoutline') ->where('nim', $user['nim']) ->whereIn('status_usulan', ['0', '1']) ->exists(); if ($hasActiveDraft) { return back()->with('error', 'Draft praoutline aktif masih ada. Silakan lihat status usulan/review terlebih dahulu.'); } $validated = $request->validate([ 'judul' => ['required', 'string', 'max:255'], 'deskripsi' => ['nullable', 'string'], 'berkas' => ['required', 'file', 'mimes:pdf', 'max:10240'], 'dosenpa' => ['required', 'string', 'max:255'], 'pilpemb1' => ['nullable', 'string', 'max:255'], 'pilpemb2' => ['nullable', 'string', 'max:255'], 'pilpemb3' => ['nullable', 'string', 'max:255'], 'pilpemb4' => ['nullable', 'string', 'max:255'], 'drekomjudul' => ['nullable', 'string', 'max:255'], 'kelompokKeahlian' => ['required', 'integer'], ]); $file = $request->file('berkas'); $filename = $user['nim'].'-'.time().'.'.$file->getClientOriginalExtension(); $file->move(base_path('../files'), $filename); DB::table('tbpraoutline')->insert([ 'nim' => $user['nim'], 'judul' => $validated['judul'], 'deskripsi' => $validated['deskripsi'] ?? '', 'berkas' => $filename, 'idProdi' => $user['prodi'], 'tgl_upload' => Carbon::now()->toDateString(), 'wkt_upload' => Carbon::now()->format('H:i:s'), 'semester' => $this->currentSemester(), 'thn_ajaran' => $this->currentAcademicYear(), 'status_usulan' => '0', 'ket' => '', 'kelompokKeahlian' => (string) $validated['kelompokKeahlian'], 'kkTerkait' => '', ]); return redirect()->route('mahasiswa.status-usulan')->with('success', 'Draft praoutline berhasil diajukan.'); } public function pengumuman(Request $request): View { $user = $this->getMahasiswaUser($request); $pengumuman = DB::table('tbpengumuman') ->select(['id', 'judul', 'isi', 'tgl']) ->where('idProdi', $user['prodi']) ->whereIn('tujuan', ['A', 'M']) ->orderByDesc('id') ->limit(25) ->get() ->map(function ($item) { return [ 'id' => $item->id, 'judul' => $item->judul, 'preview' => str($item->isi)->stripTags()->squish()->limit(180)->toString(), 'tgl' => $item->tgl, 'detailHref' => route('mahasiswa.pengumuman.show', ['id' => $item->id], false), ]; }) ->all(); return view('mahasiswa.pages.pengumuman', [ 'title' => 'Pengumuman Mahasiswa | SPOTA Rebuild', 'pageTitle' => 'Pengumuman', 'pageDescription' => 'Daftar pengumuman program studi yang ditujukan untuk mahasiswa pada data SPOTA aktif.', 'pageDate' => Carbon::now()->locale('id')->translatedFormat('j F Y, H:i'), 'sidebar' => $this->buildSidebar('mahasiswa.pengumuman.index'), 'user' => $user, 'pengumuman' => $pengumuman, ]); } public function showPengumuman(Request $request, int $id): View { $user = $this->getMahasiswaUser($request); $pengumuman = DB::table('tbpengumuman') ->select(['id', 'judul', 'isi', 'tgl']) ->where('id', $id) ->where('idProdi', $user['prodi']) ->whereIn('tujuan', ['A', 'M']) ->first(); abort_unless($pengumuman, 404); return view('mahasiswa.pages.pengumuman-detail', [ 'title' => 'Detail Pengumuman Mahasiswa | SPOTA Rebuild', 'pageTitle' => 'Detail Pengumuman', 'pageDescription' => 'Isi pengumuman mahasiswa dari basis data SPOTA aktif.', 'pageDate' => Carbon::now()->locale('id')->translatedFormat('j F Y, H:i'), 'sidebar' => $this->buildSidebar('mahasiswa.pengumuman.index'), 'user' => $user, 'pageActions' => [ ['label' => 'Kembali ke Pengumuman', 'href' => route('mahasiswa.pengumuman.index', [], false)], ], 'pengumuman' => [ 'judul' => $pengumuman->judul, 'isi' => $pengumuman->isi, 'tgl' => $this->formatDateTime($pengumuman->tgl), ], ]); } public function penawaran(Request $request): View { $user = $this->getMahasiswaUser($request); $status = $request->query('status', '0'); $kk = $request->query('kk', 'Semua'); $query = DB::table('tb_penawaran_judul as tpj') ->leftJoin('tbdosen as td', 'tpj.idDosen', '=', 'td.iddosen') ->leftJoin('tb_kelompok_keahlian as kk', 'tpj.kk', '=', 'kk.idKK') ->leftJoin('tb_ambil_judul as taj', function ($join) { $join->on('tpj.idPenawaran', '=', 'taj.idPenawaranAmbil') ->whereRaw('taj.idAmbil = (select max(taj2.idAmbil) from tb_ambil_judul taj2 where taj2.idPenawaranAmbil = tpj.idPenawaran)'); }) ->leftJoin('tbmhs as tm', 'taj.idMhs', '=', 'tm.idmhs') ->select([ 'tpj.idPenawaran', 'tpj.judul', 'tpj.deskripsi', 'tpj.waktuInput', 'td.nmLengkap as dosen', 'kk.namaKK as kk', 'taj.statusPengambilan', 'tm.nmLengkap as diambil_oleh', ]) ->orderByDesc('tpj.idPenawaran'); if ($kk !== 'Semua') { $query->where('tpj.kk', $kk); } if ($status === '0') { $query->where(function ($query) { $query->whereNull('taj.idAmbil')->orWhere('taj.statusPengambilan', '2'); }); } elseif ($status === '1') { $query->whereNotNull('taj.idAmbil')->where('taj.statusPengambilan', '!=', '2'); } $penawaran = $query->paginate(15)->withQueryString(); $kelompokKeahlian = DB::table('tb_kelompok_keahlian') ->select(['idKK', 'namaKK']) ->where('idKK', '!=', '8') ->orderBy('namaKK') ->get(); return view('mahasiswa.pages.penawaran', [ 'title' => 'Penawaran Judul Mahasiswa | SPOTA Rebuild', 'pageTitle' => 'Penawaran Judul', 'pageDescription' => 'Daftar judul yang ditawarkan dosen. Mahasiswa dapat melihat detail dan booking judul yang masih tersedia.', 'pageDate' => Carbon::now()->locale('id')->translatedFormat('j F Y, H:i'), 'sidebar' => $this->buildSidebar('mahasiswa.penawaran.index'), 'user' => $user, 'penawaran' => $penawaran, 'kelompokKeahlian' => $kelompokKeahlian, 'filters' => ['status' => $status, 'kk' => $kk], ]); } public function showPenawaran(Request $request, int $id): View { $user = $this->getMahasiswaUser($request); $penawaran = DB::table('tb_penawaran_judul as tpj') ->leftJoin('tbdosen as td', 'tpj.idDosen', '=', 'td.iddosen') ->leftJoin('tb_kelompok_keahlian as kk', 'tpj.kk', '=', 'kk.idKK') ->select([ 'tpj.idPenawaran', 'tpj.judul', 'tpj.deskripsi', 'tpj.waktuInput', 'td.nmLengkap as dosen', 'kk.namaKK as kk', ]) ->where('tpj.idPenawaran', $id) ->first(); abort_unless($penawaran, 404); return view('mahasiswa.pages.penawaran-detail', [ 'title' => 'Detail Penawaran Judul | SPOTA Rebuild', 'pageTitle' => 'Detail Penawaran Judul', 'pageDescription' => 'Detail judul yang ditawarkan dosen dan tombol booking apabila mahasiswa ingin mengambil judul ini.', 'pageDate' => Carbon::now()->locale('id')->translatedFormat('j F Y, H:i'), 'sidebar' => $this->buildSidebar('mahasiswa.penawaran.index'), 'user' => $user, 'pageActions' => [ ['label' => 'Kembali ke Penawaran', 'href' => route('mahasiswa.penawaran.index', [], false)], ], 'penawaran' => $penawaran, ]); } public function bookPenawaran(Request $request, int $id): RedirectResponse { $user = $this->getMahasiswaUser($request); $latestStudentBooking = DB::table('tb_ambil_judul') ->where('idMhs', $user['id']) ->orderByDesc('waktuPengambilan') ->first(); if ($latestStudentBooking?->statusPengambilan === '0') { return back()->with('error', 'Tidak dapat booking judul ini karena masih ada booking lain yang menunggu verifikasi dosen.'); } if ($latestStudentBooking?->statusPengambilan === '1' && strtotime($latestStudentBooking->waktuPengambilan) >= strtotime('-30 days')) { return back()->with('error', 'Tidak dapat booking judul ini karena booking sebelumnya sudah disetujui dosen.'); } $latestTitleBooking = DB::table('tb_ambil_judul') ->where('idPenawaranAmbil', $id) ->orderByDesc('waktuPengambilan') ->first(); if ($latestTitleBooking && $latestTitleBooking->statusPengambilan !== '2') { return back()->with('error', 'Judul ini sudah dibooking mahasiswa lain.'); } DB::table('tb_ambil_judul')->insert([ 'idPenawaranAmbil' => $id, 'idMhs' => $user['id'], 'statusPengambilan' => '0', 'waktuPengambilan' => Carbon::now()->toDateTimeString(), ]); return redirect()->route('mahasiswa.penawaran.index')->with('success', 'Berhasil membooking judul ini. Tunggu verifikasi dari dosen penawar.'); } private function getMahasiswaUser(Request $request): array { $auth = $request->session()->get('legacy_auth'); abort_unless(($auth['role'] ?? null) === 'mahasiswa', 403); return $auth['user']; } private function buildSidebar(string $activeRoute): array { return [ 'main' => [ ['title' => 'Dashboard', 'href' => route('dashboard.mahasiswa'), 'icon' => 'home', 'active' => $activeRoute === 'dashboard.mahasiswa'], ], 'sections' => [ [ 'title' => 'Praoutline', 'icon' => 'folder', 'items' => [ ['title' => 'Status Usulan', 'href' => route('mahasiswa.status-usulan', [], false), 'icon' => 'chart', 'active' => $activeRoute === 'mahasiswa.status-usulan'], ['title' => 'Ajukan Outline Baru', 'href' => route('mahasiswa.praoutline.upload', [], false), 'icon' => 'clipboard', 'active' => $activeRoute === 'mahasiswa.praoutline.upload'], ['title' => 'Penawaran Judul', 'href' => route('mahasiswa.penawaran.index', [], false), 'icon' => 'briefcase', 'active' => $activeRoute === 'mahasiswa.penawaran.index'], ], ], [ 'title' => 'Informasi', 'icon' => 'bell', 'items' => [ ['title' => 'Pengumuman', 'href' => route('mahasiswa.pengumuman.index', [], false), 'icon' => 'megaphone', 'active' => $activeRoute === 'mahasiswa.pengumuman.index'], ], ], ], ]; } private function formatDateTime(?string $value): string { if (! $value) { return '-'; } return Carbon::parse($value)->locale('id')->translatedFormat('j F Y, H:i'); } private function statusLabel(?string $status): string { return match ($status) { '0' => 'Dalam Review', '1' => 'Disetujui', '2' => 'Ditolak', default => 'Belum Ada Draft', }; } private function statusBadgeClass(?string $status): string { return match ($status) { '0' => 'bg-sky-100 text-sky-800', '1' => 'bg-emerald-100 text-emerald-800', '2' => 'bg-rose-100 text-rose-800', default => 'bg-slate-100 text-slate-700', }; } private function decisionLabel(?string $status): ?string { return match ($status) { '1' => 'Setuju', '0' => 'Tidak Setuju', '2' => 'Tolak', default => null, }; } private function statusPageActions(?object $outline): array { $actions = [ ['label' => 'Ajukan Outline Baru', 'href' => route('mahasiswa.praoutline.upload', [], false), 'variant' => ! $outline || $outline->status_usulan === '2' ? 'dark' : 'light'], ['label' => 'Lihat Penawaran Judul', 'href' => route('mahasiswa.penawaran.index', [], false)], ]; return $actions; } private function currentSemester(): string { return (int) date('n') >= 8 || (int) date('n') <= 1 ? 'Ganjil' : 'Genap'; } private function currentAcademicYear(): string { $year = (int) date('Y'); if ((int) date('n') >= 8) { return $year.'/'.($year + 1); } return ($year - 1).'/'.$year; } }