MIF_E31231003/app/Http/Controllers/Siswa/SpkController.php

492 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Http\Controllers\Siswa;
use App\Http\Controllers\Controller;
use App\Models\Siswa;
use App\Models\Tes;
use App\Models\SoalMinat;
use App\Models\JawabanMinat;
use App\Models\Jurusan;
use App\Models\HasilSaw;
use App\Models\Upload;
use App\Models\TesPdf;
use App\Models\Penyakit;
use App\Services\ActivityLogger;
use App\Services\TesPdfService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class SpkController extends Controller
{
/**
* Tampilkan halaman tes siswa
*/
public function index()
{
$user = Auth::user();
$siswa = Siswa::with('user')->where('user_id', $user->id)->first();
if (!$siswa) {
return redirect()->route('landing.home')
->withErrors('Akses ditolak: akun ini bukan siswa.');
}
$soal = SoalMinat::where('is_active', true)
->orderBy('id')
->get();
if ($soal->count() === 0) {
session()->flash('info', 'Belum ada soal minat aktif. Hubungi admin untuk melengkapi data soal.');
}
$tesTerakhir = Tes::with('penyakit')->where('siswa_id', $siswa->id)->latest()->first();
$riwayatHasil = $tesTerakhir
? HasilSaw::where('tes_id', $tesTerakhir->id)->orderBy('peringkat')->get()
: collect();
$jurusan = Jurusan::where('is_active', true)
->orderBy('nama_jurusan')
->get();
$penyakits = Penyakit::where('is_active', true)
->orderBy('nama_penyakit')
->get();
return view('pages.siswa.tes', compact(
'siswa',
'soal',
'tesTerakhir',
'riwayatHasil',
'jurusan',
'penyakits'
));
}
/**
* Simpan tes siswa, jawaban minat, hitung SAW, dan generate PDF
*/
public function store(Request $request)
{
$user = Auth::user();
$siswa = Siswa::with('user')->where('user_id', $user->id)->first();
if (!$siswa) {
return redirect()->route('landing.home')
->withErrors('Akses ditolak: akun ini bukan siswa.');
}
$soalAktif = SoalMinat::where('is_active', true)
->orderBy('id')
->get();
if ($soalAktif->count() === 0) {
return back()
->withErrors('Belum ada soal minat aktif. Hubungi admin.')
->withInput();
}
$rules = [
'jurusan_pilihan_1' => 'required|exists:jurusan,id',
'jurusan_pilihan_2' => 'required|exists:jurusan,id|different:jurusan_pilihan_1',
'tinggi_badan' => 'required|numeric|min:100|max:220',
'berat_badan' => 'required|numeric|min:20|max:200',
'buta_warna' => 'required|in:ya,tidak',
'penyakit_id' => 'nullable|exists:penyakit,id',
'nilai_matematika' => 'required|numeric|min:0|max:100',
'nilai_bahasa_indonesia' => 'required|numeric|min:0|max:100',
'nilai_bahasa_inggris' => 'required|numeric|min:0|max:100',
'nilai_ipa' => 'required|numeric|min:0|max:100',
'setuju' => 'required|in:1',
];
foreach ($soalAktif as $index => $item) {
$nomor = $index + 1;
$rules["bakat_q{$nomor}"] = 'required|integer|min:1|max:4';
}
$validated = $request->validate($rules);
$jawabanBakat = [];
foreach ($soalAktif as $index => $item) {
$nomor = $index + 1;
$jawabanBakat[] = (int) $validated["bakat_q{$nomor}"];
}
$sumSkor = array_sum($jawabanBakat);
$jumlahSoal = count($jawabanBakat);
$skorMinatBakat = (int) round(($sumSkor / ($jumlahSoal * 4)) * 100);
DB::transaction(function () use ($validated, $siswa, $skorMinatBakat, $jawabanBakat, $soalAktif) {
$tes = Tes::create([
'siswa_id' => $siswa->id,
'nilai_matematika' => $validated['nilai_matematika'],
'nilai_bahasa_indonesia' => $validated['nilai_bahasa_indonesia'],
'nilai_bahasa_inggris' => $validated['nilai_bahasa_inggris'],
'nilai_ipa' => $validated['nilai_ipa'],
'skor_minat_bakat' => $skorMinatBakat,
'tinggi_badan' => $validated['tinggi_badan'],
'berat_badan' => $validated['berat_badan'],
'buta_warna' => ($validated['buta_warna'] === 'ya'),
'penyakit_id' => $validated['penyakit_id'] ?? null,
'minat_jurusan_1_id' => $validated['jurusan_pilihan_1'],
'minat_jurusan_2_id' => $validated['jurusan_pilihan_2'],
]);
foreach ($soalAktif as $index => $soal) {
JawabanMinat::create([
'tes_id' => $tes->id,
'soal_minat_id' => $soal->id,
'skor' => $jawabanBakat[$index],
]);
}
$this->prosesSaw($tes);
$ok = app(TesPdfService::class)->generateAndStore($tes);
if (!$ok) {
Log::warning('PDF gagal dibuat saat penyimpanan tes', [
'tes_id' => $tes->id,
]);
}
});
ActivityLogger::log("Siswa cek minat bakat: skor {$skorMinatBakat}");
return redirect()->route('siswa.tes.hasil');
}
/**
* Proses SAW:
* - Alternatif = jurusan
* - Kriteria = 8 kriteria final
* - Aturan wajib_lolos diperlakukan sebagai aturan bisnis tambahan
*/
private function prosesSaw(Tes $tes): void
{
$jurusans = Jurusan::with(['jurusanKriteria.kriteria', 'penyakit'])
->where('is_active', true)
->get();
// ambil nilai siswa (01)
$nilaiDasar = $this->getNilaiKriteriaSiswa($tes);
$rows = [];
foreach ($jurusans as $jurusan) {
// 🔵 mapping kriteria biar rapi
$map = $this->mapJurusanKriteriaByKode($jurusan);
$nilai = $nilaiDasar;
$nilai['C8'] = $this->getSkorPenyakitUntukJurusan($tes, $jurusan);
// 🔴 1. WAJIB LOLOS (HARD CONSTRAINT)
if ($this->isJurusanDiskualifikasi($map, $nilai)) {
$rows[] = [
'jurusan_id' => $jurusan->id,
'nilai_preferensi' => 0,
];
continue;
}
$nilaiPreferensi = 0;
// 🔵 2. LOOP KRITERIA
foreach ($map as $kode => $jk) {
if (!isset($nilai[$kode])) continue;
$nilaiSiswa = $nilai[$kode];
$bobot = (float) $jk->bobot;
// 🟡 3. PENYESUAIAN MIN & MAX
$nilaiFix = $this->hitungNilaiKecocokan(
$nilaiSiswa,
$jk->nilai_min,
$jk->nilai_max,
$kode
);
// 🟢 4. RUMUS SAW (TETAP)
$nilaiPreferensi += $bobot * $nilaiFix;
}
$rows[] = [
'jurusan_id' => $jurusan->id,
'nilai_preferensi' => round($nilaiPreferensi, 6),
];
}
// 🔽 SORTING
usort($rows, fn($a, $b) => $b['nilai_preferensi'] <=> $a['nilai_preferensi']);
// 🔄 SIMPAN ULANG
HasilSaw::where('tes_id', $tes->id)->delete();
foreach ($rows as $i => $row) {
HasilSaw::create([
'tes_id' => $tes->id,
'jurusan_id' => $row['jurusan_id'],
'nilai_preferensi' => $row['nilai_preferensi'],
'peringkat' => $i + 1,
]);
}
}
/**
* Nilai final 8 kriteria siswa (Normalisasi 0-1)
*/
private function getNilaiKriteriaSiswa(Tes $tes): array
{
return [
'C1' => $tes->nilai_matematika / 100,
'C2' => $tes->nilai_bahasa_indonesia / 100,
'C3' => $tes->nilai_ipa / 100,
'C4' => $tes->nilai_bahasa_inggris / 100,
'C5' => $this->getSkorFisikDariBmi($tes->tinggi_badan, $tes->berat_badan) / 100,
'C6' => $tes->buta_warna ? 0 : 1,
'C7' => $tes->skor_minat_bakat / 100,
];
}
private function getSkorPenyakitUntukJurusan(Tes $tes, Jurusan $jurusan): int
{
if (!$tes->penyakit_id) {
return 1;
}
return $jurusan->penyakit->contains('id', $tes->penyakit_id) ? 0 : 1;
}
/**
* Hitung BMI siswa
*/
private function hitungBmi(float $tinggiBadan, float $beratBadan): float
{
$tinggiMeter = $tinggiBadan / 100;
if ($tinggiMeter <= 0) return 0;
return round($beratBadan / ($tinggiMeter * $tinggiMeter), 2);
}
/**
* Konversi BMI menjadi skor fisik
*/
private function getSkorFisikDariBmi(float $tinggiBadan, float $beratBadan): int
{
$bmi = $this->hitungBmi($tinggiBadan, $beratBadan);
if ($bmi >= 18.5 && $bmi <= 24.9) {
return 100;
}
if ($bmi >= 25 && $bmi <= 29.9) {
return 80;
}
return 70;
}
/**
* Ubah koleksi jurusanKriteria menjadi map berdasarkan kode kriteria
*/
private function mapJurusanKriteriaByKode($jurusan): array
{
$map = [];
foreach ($jurusan->jurusanKriteria as $jurusanKriteria) {
$kode = $jurusanKriteria->kriteria?->kode_kriteria;
if ($kode) {
$map[$kode] = $jurusanKriteria;
}
}
return $map;
}
/**
* Hitung nilai kecocokan siswa terhadap aturan jurusan
*/
private function hitungNilaiKecocokan(float $nilaiSiswa, $nilaiMin = null, $nilaiMax = null, $kodeKriteria = null): float
{
// Khusus C6 & C8 adalah biner (0/1), kriteria lain 0-100 di database
$isBinary = in_array($kodeKriteria, ['C6', 'C8']);
$divider = $isBinary ? 1 : 100;
$nilaiMin = is_null($nilaiMin) ? null : $nilaiMin / $divider;
$nilaiMax = is_null($nilaiMax) ? null : $nilaiMax / $divider;
$score = $nilaiSiswa;
// 🔻 Jika di bawah minimum → penalti
if (!is_null($nilaiMin) && $nilaiMin > 0 && $nilaiSiswa < $nilaiMin) {
$score = ($nilaiSiswa / $nilaiMin) * $nilaiSiswa;
}
// 🔺 Jika di atas maksimum → penalti
if (!is_null($nilaiMax) && $nilaiSiswa > 0 && $nilaiSiswa > $nilaiMax) {
$score = ($nilaiMax / $nilaiSiswa) * $score;
}
return round(max($score, 0), 6);
}
/**
* Aturan bisnis tambahan:
* Jika kriteria wajib_lolos tidak terpenuhi, jurusan didiskualifikasi.
*/
private function isJurusanDiskualifikasi(array $mapJurusanKriteria, array $nilaiSiswa): bool
{
foreach ($mapJurusanKriteria as $kode => $jurusanKriteria) {
if (!(bool) $jurusanKriteria->wajib_lolos) {
continue;
}
$nilai = $nilaiSiswa[$kode] ?? null;
if (is_null($nilai)) continue;
// C6 & C8 biner (0/1), sisanya 0-100 di database
$isBinary = in_array($kode, ['C6', 'C8']);
$divider = $isBinary ? 1 : 100;
$nilaiMin = is_null($jurusanKriteria->nilai_min) ? null : $jurusanKriteria->nilai_min / $divider;
$nilaiMax = is_null($jurusanKriteria->nilai_max) ? null : $jurusanKriteria->nilai_max / $divider;
if (!is_null($nilaiMin) && $nilai < $nilaiMin) return true;
if (!is_null($nilaiMax) && $nilai > $nilaiMax) return true;
}
return false;
}
/**
* Tampilkan hasil tes terakhir
*/
public function hasil()
{
$user = Auth::user();
$siswa = Siswa::with('user')->where('user_id', $user->id)->first();
if (!$siswa) {
return redirect()->route('landing.home')
->withErrors('Akses ditolak: akun ini bukan siswa.');
}
$tesTerakhir = Tes::where('siswa_id', $siswa->id)->latest()->first();
if (!$tesTerakhir) {
return redirect()->route('siswa.tes.index');
}
$hasilList = HasilSaw::where('tes_id', $tesTerakhir->id)
->with('jurusan')
->orderBy('peringkat')
->get();
if ($hasilList->isEmpty()) {
return redirect()->route('siswa.tes.index');
}
$jurusanPilihan1 = Jurusan::find($tesTerakhir->minat_jurusan_1_id);
$jurusanPilihan2 = Jurusan::find($tesTerakhir->minat_jurusan_2_id);
$skorPilihan1 = $tesTerakhir->minat_jurusan_1_id
? $hasilList->firstWhere('jurusan_id', $tesTerakhir->minat_jurusan_1_id)
: null;
$skorPilihan2 = $tesTerakhir->minat_jurusan_2_id
? $hasilList->firstWhere('jurusan_id', $tesTerakhir->minat_jurusan_2_id)
: null;
return view('pages.siswa.hasil', compact(
'siswa',
'tesTerakhir',
'hasilList',
'jurusanPilihan1',
'jurusanPilihan2',
'skorPilihan1',
'skorPilihan2'
));
}
/**
* Download PDF hasil tes terakhir
*/
public function cetakPdf()
{
$user = Auth::user();
$siswa = Siswa::where('user_id', $user->id)->first();
if (!$siswa) abort(403);
$tes = Tes::with('penyakit')->where('siswa_id', $siswa->id)->latest()->first();
if (!$tes) abort(404);
$tesPdf = app(TesPdfService::class)->ensureAvailable($tes);
if (!$tesPdf?->upload || !$tesPdf->upload->existsOnPublicDisk()) {
return redirect()->route('siswa.tes.hasil')->withErrors('File PDF tidak ditemukan.');
}
return Storage::disk('public')->download(
$tesPdf->upload->normalizedStoragePath(),
'hasil_tes_' . $tes->id . '.pdf'
);
}
/**
* Riwayat tes siswa
*/
public function history()
{
$siswa = Siswa::where('user_id', Auth::id())->first();
if (!$siswa) return view('pages.siswa.history', ['histories' => collect()]);
$histories = Tes::where('siswa_id', $siswa->id)
->with(['hasilSaw.jurusan', 'tesPDF.upload', 'penyakit'])
->latest()
->paginate(10);
return view('pages.siswa.history', compact('histories'));
}
public function hasilByTes(Tes $tes)
{
$user = Auth::user();
$siswa = Siswa::where('user_id', $user->id)->first();
if (!$siswa || $tes->siswa_id != $siswa->id) abort(403);
$tes->loadMissing('penyakit');
$hasilList = HasilSaw::where('tes_id', $tes->id)->with('jurusan')->orderBy('peringkat')->get();
if ($hasilList->isEmpty()) return redirect()->route('siswa.tes.index');
$jurusanPilihan1 = Jurusan::find($tes->minat_jurusan_1_id);
$jurusanPilihan2 = Jurusan::find($tes->minat_jurusan_2_id);
$skorPilihan1 = $hasilList->firstWhere('jurusan_id', $tes->minat_jurusan_1_id);
$skorPilihan2 = $hasilList->firstWhere('jurusan_id', $tes->minat_jurusan_2_id);
return view('pages.siswa.hasil', compact('siswa', 'tes', 'hasilList', 'jurusanPilihan1', 'jurusanPilihan2', 'skorPilihan1', 'skorPilihan2'))->with('tesTerakhir', $tes);
}
public function cetakPdfByTes(Tes $tes)
{
$siswa = Siswa::where('user_id', Auth::id())->first();
if (!$siswa || $tes->siswa_id != $siswa->id) abort(403);
$tesPdf = app(TesPdfService::class)->ensureAvailable($tes);
if (!$tesPdf?->upload || !$tesPdf->upload->existsOnPublicDisk()) {
return back()->withErrors('File PDF tidak tersedia.');
}
return Storage::disk('public')->download(
$tesPdf->upload->normalizedStoragePath(),
'hasil_tes_' . $tes->id . '.pdf'
);
}
}