Compare commits
10 Commits
c86ed6511e
...
ed8bb6c905
| Author | SHA1 | Date |
|---|---|---|
|
|
ed8bb6c905 | |
|
|
2905df7309 | |
|
|
24a7387cfb | |
|
|
a7e4d0e5f0 | |
|
|
e45b75531b | |
|
|
71b7e1df7a | |
|
|
84b3fc4469 | |
|
|
b48f27505e | |
|
|
3f0ce730a4 | |
|
|
fcac0ac627 |
22
.env.example
22
.env.example
|
|
@ -1,6 +1,6 @@
|
|||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_KEY=base64:PLACEHOLDER_APP_KEY_HERE
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ LOG_LEVEL=debug
|
|||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=laravel
|
||||
DB_DATABASE=db_rekomendasi_polije
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
|
||||
|
|
@ -28,14 +28,18 @@ REDIS_HOST=127.0.0.1
|
|||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
GEMINI_API_KEY=YOUR_GEMINI_API_KEY_HERE
|
||||
GEMINI_BACKEND_URL=http://127.0.0.1:5000
|
||||
GEMINI_BACKEND_TOKEN=
|
||||
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=mailpit
|
||||
MAIL_PORT=1025
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
MAIL_HOST=smtp.gmail.com
|
||||
MAIL_PORT=587
|
||||
MAIL_USERNAME=your_email@gmail.com
|
||||
MAIL_PASSWORD=your_app_password
|
||||
MAIL_ENCRYPTION=tls
|
||||
MAIL_FROM_ADDRESS=kakapatria22@gmail.com
|
||||
MAIL_FROM_NAME="SPK Jurusan Polije"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
|
@ -0,0 +1,240 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\Alumni;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
|
||||
class ImportAlumni extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'alumni:import {file : Path to Excel file}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Import data alumni dari file Excel ke database';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$filePath = $this->argument('file');
|
||||
|
||||
// Check if file exists
|
||||
if (!file_exists($filePath)) {
|
||||
$this->error("❌ File tidak ditemukan: {$filePath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Check if file is Excel
|
||||
$ext = pathinfo($filePath, PATHINFO_EXTENSION);
|
||||
if (!in_array(strtolower($ext), ['xlsx', 'xls', 'csv'])) {
|
||||
$this->error("❌ Format file tidak didukung. Gunakan .xlsx, .xls, atau .csv");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->info("📂 Membaca file: {$filePath}");
|
||||
|
||||
// Read Excel file
|
||||
$spreadsheet = IOFactory::load($filePath);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$data = $sheet->toArray();
|
||||
|
||||
if (count($data) < 2) {
|
||||
$this->error("❌ File Excel kosong atau hanya memiliki header");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Get headers
|
||||
$headers = array_map('strtolower', $data[0]);
|
||||
$headers = array_map(fn($h) => trim(str_replace([' ', '-', '_'], '_', $h)), $headers);
|
||||
|
||||
$this->line("✓ Header ditemukan: " . implode(', ', $headers));
|
||||
|
||||
// Normalize column mapping
|
||||
$columnMap = $this->normalizeColumns($headers);
|
||||
$this->line("✓ Kolom di-mapping");
|
||||
|
||||
// Process rows
|
||||
$rows = array_slice($data, 1);
|
||||
$successCount = 0;
|
||||
$errorCount = 0;
|
||||
$errors = [];
|
||||
|
||||
$this->info("\n📊 Memproses " . count($rows) . " baris data...");
|
||||
|
||||
// Progress bar
|
||||
$bar = $this->output->createProgressBar(count($rows));
|
||||
$bar->start();
|
||||
|
||||
foreach ($rows as $idx => $row) {
|
||||
$record = $this->mapRow($row, $headers, $columnMap);
|
||||
|
||||
if ($record === null || empty($record['nama_alumni'])) {
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
$validator = Validator::make($record, [
|
||||
'nama_alumni' => 'required|string',
|
||||
'nis' => 'nullable|string',
|
||||
'kelompok_asal' => 'nullable|string',
|
||||
'nilai_rata_rata' => 'nullable|numeric',
|
||||
'minat' => 'nullable|string',
|
||||
'cita_cita' => 'nullable|string',
|
||||
'preferensi_studi' => 'nullable|string',
|
||||
'prestasi' => 'nullable|string',
|
||||
'major_masuk' => 'nullable|string',
|
||||
'tahun_lulus_polije' => 'nullable|numeric',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$errorCount++;
|
||||
$errors[] = "Baris " . ($idx + 2) . ": " . implode(', ', $validator->errors()->all());
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if already exists
|
||||
$existing = Alumni::where('nis', $record['nis'] ?? null)
|
||||
->where('nama_alumni', $record['nama_alumni'] ?? null)
|
||||
->first();
|
||||
|
||||
if (!$existing) {
|
||||
Alumni::create($record);
|
||||
$successCount++;
|
||||
} else {
|
||||
// Update jika sudah ada
|
||||
$existing->update($record);
|
||||
$successCount++;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errorCount++;
|
||||
$errors[] = "Baris " . ($idx + 2) . ": " . $e->getMessage();
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
|
||||
// Summary
|
||||
$this->newLine(2);
|
||||
$this->info("=" . str_repeat("=", 58) . "=");
|
||||
$this->info("✓ IMPORT SELESAI");
|
||||
$this->info("=" . str_repeat("=", 58) . "=");
|
||||
$this->line("✓ Data berhasil di-import: {$successCount}");
|
||||
if ($errorCount > 0) {
|
||||
$this->line("⚠ Baris error/skip: {$errorCount}");
|
||||
|
||||
if (count($errors) > 0 && count($errors) <= 20) {
|
||||
$this->newLine();
|
||||
$this->warn("Errors:");
|
||||
foreach ($errors as $error) {
|
||||
$this->line(" - {$error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->newLine();
|
||||
|
||||
return 0;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error("❌ Error: " . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize column headers to database column names
|
||||
*/
|
||||
private function normalizeColumns($headers)
|
||||
{
|
||||
$mapping = [
|
||||
'nama' => 'nama_alumni',
|
||||
'nama_alumni' => 'nama_alumni',
|
||||
'nis' => 'nis',
|
||||
'no_induk_siswa' => 'nis',
|
||||
'kelompok_asal' => 'kelompok_asal',
|
||||
'kelompok' => 'kelompok_asal',
|
||||
'nilai_(rata_rata)' => 'nilai_rata_rata',
|
||||
'nilai_rata_rata' => 'nilai_rata_rata',
|
||||
'rata_rata' => 'nilai_rata_rata',
|
||||
'average' => 'nilai_rata_rata',
|
||||
'minat' => 'minat',
|
||||
'interest' => 'minat',
|
||||
'cita_cita' => 'cita_cita',
|
||||
'cita' => 'cita_cita',
|
||||
'dream_job' => 'cita_cita',
|
||||
'preferensi_studi' => 'preferensi_studi',
|
||||
'preferensi' => 'preferensi_studi',
|
||||
'preference' => 'preferensi_studi',
|
||||
'prestasi' => 'prestasi',
|
||||
'achievement' => 'prestasi',
|
||||
'major_masuk' => 'major_masuk',
|
||||
'jurusan_masuk' => 'major_masuk',
|
||||
'jurusan_keterima_di_polije' => 'major_masuk',
|
||||
'jurusan' => 'major_masuk',
|
||||
'major' => 'major_masuk',
|
||||
'tahun_lulus_polije' => 'tahun_lulus_polije',
|
||||
'tahun_lulus' => 'tahun_lulus_polije',
|
||||
'graduation_year' => 'tahun_lulus_polije',
|
||||
'tahun' => 'tahun_lulus_polije',
|
||||
'catatan' => 'catatan',
|
||||
'keterangan' => 'catatan',
|
||||
'notes' => 'catatan',
|
||||
];
|
||||
|
||||
return $mapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map row data to Alumni model
|
||||
*/
|
||||
private function mapRow($row, $headers, $columnMap)
|
||||
{
|
||||
$record = [];
|
||||
|
||||
foreach ($headers as $idx => $header) {
|
||||
$value = $row[$idx] ?? null;
|
||||
|
||||
// Map column name
|
||||
$dbColumn = $columnMap[strtolower($header)] ?? null;
|
||||
|
||||
if (!$dbColumn) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Type conversion
|
||||
if (in_array($dbColumn, ['nilai_rata_rata', 'tahun_lulus_polije'])) {
|
||||
$record[$dbColumn] = $value ? (float) $value : null;
|
||||
} else {
|
||||
$cleanValue = $value ? trim((string) $value) : null;
|
||||
|
||||
// Special handling for preferensi_studi - truncate to enum value
|
||||
if ($dbColumn === 'preferensi_studi' && $cleanValue) {
|
||||
// Extract only the category part (before the parenthesis)
|
||||
$parts = explode('(', $cleanValue);
|
||||
$cleanValue = trim($parts[0]);
|
||||
}
|
||||
|
||||
$record[$dbColumn] = $cleanValue;
|
||||
}
|
||||
}
|
||||
|
||||
return empty($record['nama_alumni'] ?? null) ? null : $record;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\User;
|
||||
use App\Http\Controllers\RekomendasiController;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class TestScoringInput extends Command
|
||||
{
|
||||
protected $signature = 'test:scoring
|
||||
{--minat=saya senang coding dan web development : Input minat}
|
||||
{--cita-cita=menjadi web developer profesional : Input cita-cita}
|
||||
{--prestasi=juara 1 kompetisi coding : Input prestasi}
|
||||
{--nilai=85 : Nilai rata-rata untuk test}';
|
||||
|
||||
protected $description = 'Test scoring algorithm dengan input detail untuk minat, cita-cita, prestasi';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->info('╔════════════════════════════════════════════════════════════════╗');
|
||||
$this->info('║ TEST SCORING INPUT - COMPREHENSIVE TEST ║');
|
||||
$this->info('╚════════════════════════════════════════════════════════════════╝');
|
||||
$this->newLine();
|
||||
|
||||
// Test input - COMPREHENSIVE
|
||||
$minat = $this->option('minat');
|
||||
$citaCita = $this->option('cita-cita');
|
||||
$prestasi = $this->option('prestasi');
|
||||
$nilaiBase = (int)$this->option('nilai');
|
||||
|
||||
// Display ALL inputs
|
||||
$this->info('📝 SEMUA INPUT YANG DITEST:');
|
||||
$this->line('');
|
||||
$this->line(' ┌─ NILAI AKADEMIK (Kriteria 1):');
|
||||
$this->line(" │ ├─ MTK: $nilaiBase");
|
||||
$this->line(" │ ├─ Fisika: " . ($nilaiBase - 2));
|
||||
$this->line(" │ ├─ Kimia: " . ($nilaiBase - 3));
|
||||
$this->line(" │ └─ Biologi: " . ($nilaiBase - 1));
|
||||
$this->line(' │');
|
||||
$this->line(" ├─ MINAT (Kriteria 2): \"$minat\"");
|
||||
$this->line(" ├─ PREFERENSI STUDI (Kriteria 3): Sains & Teknologi");
|
||||
$this->line(" ├─ CITA-CITA (Kriteria 4): \"$citaCita\"");
|
||||
$this->line(" └─ PRESTASI (Kriteria 5): \"$prestasi\"");
|
||||
$this->newLine();
|
||||
|
||||
try {
|
||||
// Get or create test user
|
||||
$testUser = User::firstOrCreate(
|
||||
['email' => 'test@scoring.local'],
|
||||
[
|
||||
'name' => 'Test User',
|
||||
'password' => bcrypt('password'),
|
||||
'nis' => '12345',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'role' => 'siswa',
|
||||
]
|
||||
);
|
||||
|
||||
// Login as test user
|
||||
Auth::login($testUser);
|
||||
|
||||
// Create request object
|
||||
$request = Request::create('/rekomendasi/proses', 'POST', [
|
||||
'mtk' => $nilaiBase,
|
||||
'fisika' => $nilaiBase - 2,
|
||||
'kimia' => $nilaiBase - 3,
|
||||
'biologi' => $nilaiBase - 1,
|
||||
'minat' => $minat,
|
||||
'pref_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => $citaCita,
|
||||
'prestasi' => $prestasi,
|
||||
]);
|
||||
|
||||
// Call controller proses method
|
||||
$controller = new RekomendasiController();
|
||||
$response = $controller->proses($request);
|
||||
|
||||
$this->info('✅ SCORING BERHASIL');
|
||||
$this->newLine();
|
||||
|
||||
// Check if response is a view
|
||||
if (method_exists($response, 'getData')) {
|
||||
$data = $response->getData();
|
||||
|
||||
if (isset($data['hasilAkhir']) && is_array($data['hasilAkhir'])) {
|
||||
$this->info('🏆 HASIL TOP 3 REKOMENDASI JURUSAN:');
|
||||
$this->line('');
|
||||
$hasilAkhir = $data['hasilAkhir'];
|
||||
|
||||
for ($i = 0; $i < min(3, count($hasilAkhir)); $i++) {
|
||||
$r = $hasilAkhir[$i];
|
||||
$no = $i + 1;
|
||||
$this->line(" ┌─ #{$no}. {$r['jurusan']}");
|
||||
$this->line(" │ Score: " . number_format($r['skor'], 4) . " (" . round($r['skor'] * 100, 1) . "%)");
|
||||
$this->line(' │');
|
||||
|
||||
// Show detail scoring per kriteria
|
||||
$detail = $r['detail'] ?? [];
|
||||
$this->line(' │ 📊 Detail Scoring:');
|
||||
$this->line(" │ ├─ Nilai Akademik: " . number_format($detail['nilai'] ?? 0, 4));
|
||||
$this->line(" │ ├─ Minat (" . ($r['kecocokan_minat'] ?? 'N/A') . "): " . number_format($detail['minat'] ?? 0, 4));
|
||||
$this->line(" │ ├─ Preferensi Studi: " . number_format($detail['pref'] ?? 0, 4));
|
||||
$this->line(" │ ├─ Cita-cita: " . number_format($detail['cita'] ?? 0, 4));
|
||||
if ($detail['prestasi'] ?? null) {
|
||||
$this->line(" │ └─ Prestasi: " . number_format($detail['prestasi'], 4));
|
||||
}
|
||||
|
||||
// Show explanations
|
||||
$exp = $r['explanation'] ?? [];
|
||||
$this->line(' │');
|
||||
$this->line(' │ 📝 Penjelasan:');
|
||||
if ($exp['nilai'] ?? null) {
|
||||
$this->line(' │ ├─ ' . substr($exp['nilai'], 0, 65) . '...');
|
||||
}
|
||||
if ($exp['minat'] ?? null) {
|
||||
$this->line(' │ ├─ ' . substr($exp['minat'], 0, 65) . '...');
|
||||
}
|
||||
if ($exp['cita'] ?? null) {
|
||||
$this->line(' │ ├─ ' . substr($exp['cita'], 0, 65) . '...');
|
||||
}
|
||||
if ($exp['prestasi'] ?? null) {
|
||||
$this->line(' │ └─ ' . substr($exp['prestasi'], 0, 65) . '...');
|
||||
}
|
||||
|
||||
if ($i < 2) {
|
||||
$this->line(' │');
|
||||
}
|
||||
}
|
||||
$this->line(' └─────────────────────────────────────────────');
|
||||
}
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info('✅ TEST SELESAI - SEMUA 5 KRITERIA DITEST:');
|
||||
$this->line(' ✓ Nilai Akademik (MTK, Fisika, Kimia, Biologi)');
|
||||
$this->line(' ✓ Minat (coverage-based mapping)');
|
||||
$this->line(' ✓ Preferensi Studi (enum validation)');
|
||||
$this->line(' ✓ Cita-cita (career category mapping)');
|
||||
$this->line(' ✓ Prestasi (level classification)');
|
||||
$this->newLine();
|
||||
$this->line('💡 Hasil disimpan di database table recommendations');
|
||||
$this->line('💡 Check logs: storage/logs/laravel.log');
|
||||
|
||||
// Logout
|
||||
Auth::logout();
|
||||
} catch (\Exception $e) {
|
||||
$this->error('❌ ERROR: ' . $e->getMessage());
|
||||
$this->line('File: ' . $e->getFile() . ':' . $e->getLine());
|
||||
$this->newLine();
|
||||
$this->line('Stack Trace:');
|
||||
$this->line($e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -38,15 +38,91 @@ public function dashboard()
|
|||
->groupBy('kelompok_asal')
|
||||
->get();
|
||||
|
||||
// Rekomendasi per kelompok
|
||||
$rekomendasiPerKelompok = Recommendation::selectRaw(
|
||||
'users.kelompok_asal, COUNT(*) as count'
|
||||
)
|
||||
->join('users', 'rekomendasi.user_id', '=', 'users.id')
|
||||
->groupBy('users.kelompok_asal')
|
||||
->get();
|
||||
|
||||
$topMajors = Recommendation::selectRaw("
|
||||
JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') as major_name,
|
||||
COUNT(*) as count
|
||||
")
|
||||
->groupByRaw("JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan')")
|
||||
->orderBy('count', 'desc')
|
||||
->groupByRaw("JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan')") ->whereRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') IS NOT NULL")
|
||||
->whereRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') != 'null'") ->orderBy('count', 'desc')
|
||||
->take(5)
|
||||
->get();
|
||||
|
||||
// Data untuk chart - semua jurusan (filter out NULL values)
|
||||
$allMajorsChart = Recommendation::selectRaw("
|
||||
JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') as major_name,
|
||||
COUNT(*) as count
|
||||
")
|
||||
->groupByRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan')")
|
||||
->whereRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') IS NOT NULL")
|
||||
->whereRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') != 'null'")
|
||||
->orderBy('count', 'desc')
|
||||
->get();
|
||||
|
||||
// Persiapkan data untuk Chart.js - aggregate & fix major names
|
||||
$majorData = [];
|
||||
foreach ($allMajorsChart as $item) {
|
||||
$name = trim($item->major_name, '" ');
|
||||
|
||||
// Skip empty/null values
|
||||
if (empty($name) || $name === 'null') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Normalize: handle all variants
|
||||
$normalizedName = $name;
|
||||
if (stripos($name, 'Teknik Informatika') === 0 || stripos($name, 'Teknologi Informasi') === 0) {
|
||||
$normalizedName = 'Teknologi Informasi';
|
||||
}
|
||||
|
||||
if (!isset($majorData[$normalizedName])) {
|
||||
$majorData[$normalizedName] = 0;
|
||||
}
|
||||
$majorData[$normalizedName] += (int)$item->count;
|
||||
}
|
||||
|
||||
// Sort by count descending
|
||||
arsort($majorData);
|
||||
|
||||
$chartMajorNames = array_keys($majorData);
|
||||
$chartMajorCounts = array_values($majorData);
|
||||
|
||||
$chartKelompokNames = $kelompokStats->pluck('kelompok_asal')->toArray();
|
||||
$chartKelompokCounts = $kelompokStats->pluck('count')->toArray();
|
||||
|
||||
// Top majors untuk horizontal bar chart - aggregate & fix
|
||||
$topMajorData = [];
|
||||
foreach ($topMajors as $item) {
|
||||
$name = trim($item->major_name, '" ');
|
||||
|
||||
// Skip empty/null values
|
||||
if (empty($name) || $name === 'null') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Normalize: handle all variants
|
||||
$normalizedName = $name;
|
||||
if (stripos($name, 'Teknik Informatika') === 0 || stripos($name, 'Teknologi Informasi') === 0) {
|
||||
$normalizedName = 'Teknologi Informasi';
|
||||
}
|
||||
|
||||
if (!isset($topMajorData[$normalizedName])) {
|
||||
$topMajorData[$normalizedName] = 0;
|
||||
}
|
||||
$topMajorData[$normalizedName] += (int)$item->count;
|
||||
}
|
||||
|
||||
arsort($topMajorData);
|
||||
$topMajorsChart = array_keys($topMajorData);
|
||||
$topMajorsCounts = array_values($topMajorData);
|
||||
|
||||
return view('admin.dashboard', compact(
|
||||
'totalSiswa',
|
||||
'totalRekomendasi',
|
||||
|
|
@ -55,7 +131,14 @@ public function dashboard()
|
|||
'recentStudents',
|
||||
'recentRecommendations',
|
||||
'kelompokStats',
|
||||
'topMajors'
|
||||
'topMajors',
|
||||
'chartMajorNames',
|
||||
'chartMajorCounts',
|
||||
'chartKelompokNames',
|
||||
'chartKelompokCounts',
|
||||
'topMajorsChart',
|
||||
'topMajorsCounts',
|
||||
'rekomendasiPerKelompok'
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -86,7 +169,7 @@ public function students(Request $request)
|
|||
|
||||
public function studentDetail($id)
|
||||
{
|
||||
$student = User::findOrFail($id);
|
||||
$student = User::where('role', 'siswa')->findOrFail($id);
|
||||
$recommendations = Recommendation::where('user_id', $id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
|
@ -124,19 +207,22 @@ public function jurusanCreate()
|
|||
public function jurusanStore(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'nama_jurusan' => 'required|string|max:255|unique:polije_majors,nama_jurusan',
|
||||
'deskripsi' => 'nullable|string|max:1000',
|
||||
'nama_jurusan' => 'required|string|min:3|max:255|unique:jurusan_polije,nama_jurusan',
|
||||
'deskripsi' => 'nullable|string|max:10000',
|
||||
'keywords' => 'nullable|string',
|
||||
'preferensi_studi' => 'nullable|string',
|
||||
'prospek_kerja' => 'nullable|string|max:1000',
|
||||
'bobot_mtk' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_fisika' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_kimia' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_biologi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_ekonomi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_geografi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_sosiologi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_sejarah' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel' => 'nullable|array',
|
||||
'bobot_mapel.ipa' => 'nullable|array',
|
||||
'bobot_mapel.ipa.mtk' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ipa.fisika' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ipa.kimia' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ipa.biologi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ips' => 'nullable|array',
|
||||
'bobot_mapel.ips.ekonomi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ips.geografi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ips.sosiologi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ips.sejarah' => 'nullable|numeric|min:0|max:1',
|
||||
]);
|
||||
|
||||
PolijeMajor::create([
|
||||
|
|
@ -162,19 +248,22 @@ public function jurusanUpdate(Request $request, $id)
|
|||
$jurusan = PolijeMajor::findOrFail($id);
|
||||
|
||||
$request->validate([
|
||||
'nama_jurusan' => ['required', 'string', 'max:255', Rule::unique('polije_majors', 'nama_jurusan')->ignore($jurusan->id)],
|
||||
'deskripsi' => 'nullable|string|max:1000',
|
||||
'nama_jurusan' => ['required', 'string', 'min:3', 'max:255', Rule::unique('jurusan_polije', 'nama_jurusan')->ignore($jurusan->id)],
|
||||
'deskripsi' => 'nullable|string|max:10000',
|
||||
'keywords' => 'nullable|string',
|
||||
'preferensi_studi' => 'nullable|string',
|
||||
'prospek_kerja' => 'nullable|string|max:1000',
|
||||
'bobot_mtk' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_fisika' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_kimia' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_biologi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_ekonomi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_geografi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_sosiologi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_sejarah' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel' => 'nullable|array',
|
||||
'bobot_mapel.ipa' => 'nullable|array',
|
||||
'bobot_mapel.ipa.mtk' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ipa.fisika' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ipa.kimia' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ipa.biologi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ips' => 'nullable|array',
|
||||
'bobot_mapel.ips.ekonomi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ips.geografi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ips.sosiologi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ips.sejarah' => 'nullable|numeric|min:0|max:1',
|
||||
]);
|
||||
|
||||
$jurusan->update([
|
||||
|
|
@ -213,17 +302,45 @@ private function parseTagInput(?string $input): array
|
|||
*/
|
||||
private function parseBobotMapel(Request $request): array
|
||||
{
|
||||
$mapelList = ['mtk', 'fisika', 'kimia', 'biologi', 'ekonomi', 'geografi', 'sosiologi', 'sejarah'];
|
||||
$bobot = [];
|
||||
$ipaSubjects = ['mtk', 'fisika', 'kimia', 'biologi'];
|
||||
$ipsSubjects = ['ekonomi', 'geografi', 'sosiologi', 'sejarah'];
|
||||
|
||||
foreach ($mapelList as $mapel) {
|
||||
$value = $request->input("bobot_{$mapel}");
|
||||
if (!is_null($value) && $value !== '') {
|
||||
$bobot[$mapel] = floatval($value);
|
||||
}
|
||||
$ipaInput = $request->input('bobot_mapel.ipa');
|
||||
$ipsInput = $request->input('bobot_mapel.ips');
|
||||
|
||||
if (is_array($ipaInput) || is_array($ipsInput)) {
|
||||
return [
|
||||
'ipa' => $this->normalizeBobotGroup(is_array($ipaInput) ? $ipaInput : [], $ipaSubjects),
|
||||
'ips' => $this->normalizeBobotGroup(is_array($ipsInput) ? $ipsInput : [], $ipsSubjects),
|
||||
];
|
||||
}
|
||||
|
||||
return $bobot;
|
||||
return [
|
||||
'ipa' => $this->normalizeBobotGroup([
|
||||
'mtk' => $request->input('bobot_mtk'),
|
||||
'fisika' => $request->input('bobot_fisika'),
|
||||
'kimia' => $request->input('bobot_kimia'),
|
||||
'biologi' => $request->input('bobot_biologi'),
|
||||
], $ipaSubjects),
|
||||
'ips' => $this->normalizeBobotGroup([
|
||||
'ekonomi' => $request->input('bobot_ekonomi'),
|
||||
'geografi' => $request->input('bobot_geografi'),
|
||||
'sosiologi' => $request->input('bobot_sosiologi'),
|
||||
'sejarah' => $request->input('bobot_sejarah'),
|
||||
], $ipsSubjects),
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeBobotGroup(array $values, array $subjects): array
|
||||
{
|
||||
$normalized = [];
|
||||
|
||||
foreach ($subjects as $subject) {
|
||||
$value = $values[$subject] ?? null;
|
||||
$normalized[$subject] = is_numeric($value) ? (float) $value : 0.0;
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
|
@ -243,7 +360,7 @@ public function guruBKCreate()
|
|||
public function guruBKStore(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'name' => 'required|string|min:3|max:255',
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
]);
|
||||
|
|
@ -269,7 +386,7 @@ public function guruBKUpdate(Request $request, $id)
|
|||
$guruBK = User::where('role', 'bk')->findOrFail($id);
|
||||
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'name' => 'required|string|min:3|max:255',
|
||||
'email' => ['required', 'email', Rule::unique('users')->ignore($guruBK->id)],
|
||||
'password' => 'nullable|string|min:8|confirmed',
|
||||
]);
|
||||
|
|
@ -336,8 +453,8 @@ public function riwayatChatbot(Request $request)
|
|||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('prompt', 'like', "%{$search}%")
|
||||
->orWhere('response', 'like', "%{$search}%")
|
||||
$q->where('pertanyaan', 'like', "%{$search}%")
|
||||
->orWhere('jawaban', 'like', "%{$search}%")
|
||||
->orWhereHas('user', function ($q2) use ($search) {
|
||||
$q2->where('name', 'like', "%{$search}%");
|
||||
});
|
||||
|
|
@ -366,7 +483,7 @@ public function updateProfil(Request $request)
|
|||
$admin = Auth::user();
|
||||
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'name' => 'required|string|min:3|max:255',
|
||||
'email' => ['required', 'email', Rule::unique('users')->ignore($admin->id)],
|
||||
]);
|
||||
|
||||
|
|
@ -396,4 +513,17 @@ public function updatePassword(Request $request)
|
|||
return redirect()->route('admin.profil')->with('success', 'Password berhasil diubah!');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 8. LOGOUT SEMUA USER
|
||||
// ============================================
|
||||
public function logoutAllUsers()
|
||||
{
|
||||
// Logout semua session user
|
||||
\DB::table('sessions')->truncate();
|
||||
|
||||
Auth::logout();
|
||||
|
||||
return redirect()->route('login')->with('success', 'Semua user telah berhasil logout!');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,15 +7,45 @@
|
|||
|
||||
class AlumniController extends Controller
|
||||
{
|
||||
private const IPA_SUBJECTS = ['mtk', 'fisika', 'kimia', 'biologi'];
|
||||
private const IPS_SUBJECTS = ['ekonomi', 'geografi', 'sosiologi', 'sejarah'];
|
||||
|
||||
private const ALL_SUBJECTS = ['mtk', 'fisika', 'kimia', 'biologi', 'ekonomi', 'geografi', 'sosiologi', 'sejarah'];
|
||||
|
||||
private function normalizeScoreFields(array $validated, string $kelompokAsal): array
|
||||
{
|
||||
$activeSubjects = $kelompokAsal === 'IPA' ? self::IPA_SUBJECTS : self::IPS_SUBJECTS;
|
||||
|
||||
foreach (self::ALL_SUBJECTS as $subject) {
|
||||
if (!in_array($subject, $activeSubjects, true)) {
|
||||
$validated[$subject] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $validated;
|
||||
}
|
||||
|
||||
private function validateScoreByKelompok(Request $request): void
|
||||
{
|
||||
$requiredSubjects = $request->input('kelompok_asal') === 'IPA'
|
||||
? self::IPA_SUBJECTS
|
||||
: self::IPS_SUBJECTS;
|
||||
|
||||
foreach ($requiredSubjects as $subject) {
|
||||
$request->validate([
|
||||
$subject => 'required|numeric|min:0|max:100',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display alumni data list
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$alumni = Alumni::orderBy('tahun_masuk', 'desc')->paginate(20);
|
||||
$summary = $this->getAlumniSummary();
|
||||
|
||||
return view('alumni.index', compact('alumni', 'summary'));
|
||||
return view('admin.alumni.index', compact('alumni'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -32,7 +62,7 @@ public function create()
|
|||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'nama_alumni' => 'required|string|max:255',
|
||||
'nama_alumni' => 'required|string|min:3|max:255',
|
||||
'nis' => 'nullable|string|max:20',
|
||||
'kelompok_asal' => 'required|in:IPA,IPS',
|
||||
|
||||
|
|
@ -49,16 +79,18 @@ public function store(Request $request)
|
|||
// Non-akademik
|
||||
'minat' => 'nullable|string|max:255',
|
||||
'cita_cita' => 'nullable|string|max:255',
|
||||
'preferensi_studi' => 'nullable|in:Praktik_Langsung,DuDi,Project_Based,Blended',
|
||||
'preferensi_studi' => 'nullable|in:Praktik Langsung,Praktik_Langsung,DuDi,Project Based,Project_Based,Blended Learning,Blended',
|
||||
'prestasi' => 'nullable|string|max:255',
|
||||
|
||||
// Major & Outcome
|
||||
'major_masuk' => 'required|string|max:255',
|
||||
'ranking_saat_rekomendasi' => 'nullable|integer|min:1|max:9',
|
||||
'success_status' => 'nullable|in:sangat_sukses,sukses,cukup,kurang_sukses',
|
||||
// Major
|
||||
'major_masuk' => 'required|string|min:3|max:255',
|
||||
'tahun_lulus_polije' => 'nullable|integer|min:2020|max:' . date('Y'),
|
||||
'catatan' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$this->validateScoreByKelompok($request);
|
||||
$validated = $this->normalizeScoreFields($validated, $validated['kelompok_asal']);
|
||||
|
||||
Alumni::create($validated);
|
||||
|
||||
return redirect()->route('admin.alumni.index')->with('success', 'Alumni berhasil ditambahkan');
|
||||
|
|
@ -69,7 +101,7 @@ public function store(Request $request)
|
|||
*/
|
||||
public function show(Alumni $alumnus)
|
||||
{
|
||||
return view('alumni.show', compact('alumnus'));
|
||||
return view('admin.alumni.show', compact('alumnus'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -86,7 +118,7 @@ public function edit(Alumni $alumni)
|
|||
public function update(Request $request, Alumni $alumni)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'nama_alumni' => 'required|string|max:255',
|
||||
'nama_alumni' => 'required|string|min:3|max:255',
|
||||
'nis' => 'nullable|string|max:20',
|
||||
'kelompok_asal' => 'required|in:IPA,IPS',
|
||||
|
||||
|
|
@ -101,15 +133,17 @@ public function update(Request $request, Alumni $alumni)
|
|||
|
||||
'minat' => 'nullable|string|max:255',
|
||||
'cita_cita' => 'nullable|string|max:255',
|
||||
'preferensi_studi' => 'nullable|in:Praktik_Langsung,DuDi,Project_Based,Blended',
|
||||
'preferensi_studi' => 'nullable|in:Praktik Langsung,Praktik_Langsung,DuDi,Project Based,Project_Based,Blended Learning,Blended',
|
||||
'prestasi' => 'nullable|string|max:255',
|
||||
|
||||
'major_masuk' => 'required|string|max:255',
|
||||
'ranking_saat_rekomendasi' => 'nullable|integer|min:1|max:9',
|
||||
'success_status' => 'nullable|in:sangat_sukses,sukses,cukup,kurang_sukses',
|
||||
'major_masuk' => 'required|string|min:3|max:255',
|
||||
'tahun_lulus_polije' => 'nullable|integer|min:2020|max:' . date('Y'),
|
||||
'catatan' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$this->validateScoreByKelompok($request);
|
||||
$validated = $this->normalizeScoreFields($validated, $validated['kelompok_asal']);
|
||||
|
||||
$alumni->update($validated);
|
||||
|
||||
return redirect()->route('admin.alumni.index')->with('success', 'Alumni berhasil diupdate');
|
||||
|
|
@ -135,53 +169,15 @@ private function getAlumniSummary()
|
|||
->groupBy('major_masuk')
|
||||
->get();
|
||||
|
||||
$bySuccess = Alumni::selectRaw('success_status, COUNT(*) as count')
|
||||
->groupBy('success_status')
|
||||
// Statistics by kelompok asal (IPA/IPS)
|
||||
$byKelompok = Alumni::selectRaw('kelompok_asal, COUNT(*) as count')
|
||||
->groupBy('kelompok_asal')
|
||||
->get();
|
||||
|
||||
$prediction_accuracy = $this->calculatePredictionAccuracy();
|
||||
|
||||
return [
|
||||
'total' => $totalAlumni,
|
||||
'by_major' => $byMajor,
|
||||
'by_success' => $bySuccess,
|
||||
'prediction_accuracy' => $prediction_accuracy,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate how accurate was our algorithm prediction
|
||||
* vs actual major the alumni entered
|
||||
*/
|
||||
private function calculatePredictionAccuracy()
|
||||
{
|
||||
$alumni = Alumni::whereNotNull('ranking_saat_rekomendasi')->get();
|
||||
|
||||
if ($alumni->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$correctTop1 = 0;
|
||||
$correctTop3 = 0;
|
||||
$correctTop5 = 0;
|
||||
|
||||
foreach ($alumni as $a) {
|
||||
if ($a->ranking_saat_rekomendasi == 1) {
|
||||
$correctTop1++;
|
||||
}
|
||||
if ($a->ranking_saat_rekomendasi <= 3) {
|
||||
$correctTop3++;
|
||||
}
|
||||
if ($a->ranking_saat_rekomendasi <= 5) {
|
||||
$correctTop5++;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'top_1' => round(($correctTop1 / count($alumni)) * 100, 2),
|
||||
'top_3' => round(($correctTop3 / count($alumni)) * 100, 2),
|
||||
'top_5' => round(($correctTop5 / count($alumni)) * 100, 2),
|
||||
'total_alumni_analyzed' => count($alumni),
|
||||
'by_kelompok' => $byKelompok,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Alumni;
|
||||
use Illuminate\Http\Request;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class AlumniImportController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show upload form
|
||||
*/
|
||||
public function showForm()
|
||||
{
|
||||
return view('alumni.import-form');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle file upload & import
|
||||
*/
|
||||
public function import(Request $request)
|
||||
{
|
||||
// Validate file
|
||||
$validator = Validator::make($request->all(), [
|
||||
'file' => 'required|file|mimes:xlsx,xls,csv|max:10240', // max 10MB
|
||||
], [
|
||||
'file.required' => 'File harus dipilih',
|
||||
'file.mimes' => 'File harus format .xlsx, .xls, atau .csv',
|
||||
'file.max' => 'File tidak boleh lebih dari 10MB',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return back()
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
}
|
||||
|
||||
try {
|
||||
$file = $request->file('file');
|
||||
$filePath = $file->store('temp', 'local');
|
||||
$fullPath = storage_path('app/' . $filePath);
|
||||
|
||||
// Read Excel
|
||||
$spreadsheet = IOFactory::load($fullPath);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$data = $sheet->toArray();
|
||||
|
||||
if (count($data) < 2) {
|
||||
return back()->with('error', 'File Excel kosong atau hanya memiliki header');
|
||||
}
|
||||
|
||||
// Get headers
|
||||
$headers = array_map('strtolower', $data[0]);
|
||||
$headers = array_map(fn($h) => trim(str_replace([' ', '-', '_'], '_', $h)), $headers);
|
||||
|
||||
// Normalize column mapping
|
||||
$columnMap = $this->normalizeColumns();
|
||||
|
||||
// Process rows
|
||||
$rows = array_slice($data, 1);
|
||||
$successCount = 0;
|
||||
$errorCount = 0;
|
||||
$errors = [];
|
||||
|
||||
foreach ($rows as $idx => $row) {
|
||||
$record = $this->mapRow($row, $headers, $columnMap);
|
||||
|
||||
if ($record === null || empty($record['nama_alumni'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
$validator = Validator::make($record, [
|
||||
'nama_alumni' => 'required|string',
|
||||
'kelompok_asal' => 'nullable|string',
|
||||
'nilai_rata_rata' => 'nullable|numeric',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$errorCount++;
|
||||
$errors[] = "Baris " . ($idx + 2) . ": " . implode(', ', $validator->errors()->all());
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if already exists
|
||||
$existing = Alumni::where('nis', $record['nis'] ?? '')
|
||||
->where('nama_alumni', $record['nama_alumni'])
|
||||
->first();
|
||||
|
||||
if (!$existing) {
|
||||
Alumni::create($record);
|
||||
$successCount++;
|
||||
} else {
|
||||
// Update jika sudah ada
|
||||
$existing->update($record);
|
||||
$successCount++;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errorCount++;
|
||||
$errors[] = "Baris " . ($idx + 2) . ": " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up temp file
|
||||
@unlink($fullPath);
|
||||
|
||||
// Prepare response
|
||||
$message = "✓ Import Selesai! {$successCount} data berhasil diimport";
|
||||
if ($errorCount > 0) {
|
||||
$message .= " ({$errorCount} error/skip)";
|
||||
}
|
||||
|
||||
return back()
|
||||
->with('success', $message)
|
||||
->with('successCount', $successCount)
|
||||
->with('errorCount', $errorCount)
|
||||
->with('errors', count($errors) <= 10 ? $errors : array_slice($errors, 0, 10));
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return back()->with('error', 'Error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize column headers to database column names
|
||||
*/
|
||||
private function normalizeColumns()
|
||||
{
|
||||
return [
|
||||
'nama' => 'nama_alumni',
|
||||
'nama_alumni' => 'nama_alumni',
|
||||
'nis' => 'nis',
|
||||
'no_induk_siswa' => 'nis',
|
||||
'kelompok_asal' => 'kelompok_asal',
|
||||
'kelompok' => 'kelompok_asal',
|
||||
'nilai_(rata_rata)' => 'nilai_rata_rata',
|
||||
'nilai_rata_rata' => 'nilai_rata_rata',
|
||||
'rata_rata' => 'nilai_rata_rata',
|
||||
'average' => 'nilai_rata_rata',
|
||||
'minat' => 'minat',
|
||||
'interest' => 'minat',
|
||||
'cita_cita' => 'cita_cita',
|
||||
'cita' => 'cita_cita',
|
||||
'dream_job' => 'cita_cita',
|
||||
'preferensi_studi' => 'preferensi_studi',
|
||||
'preferensi' => 'preferensi_studi',
|
||||
'preference' => 'preferensi_studi',
|
||||
'prestasi' => 'prestasi',
|
||||
'achievement' => 'prestasi',
|
||||
'major_masuk' => 'major_masuk',
|
||||
'jurusan_masuk' => 'major_masuk',
|
||||
'jurusan' => 'major_masuk',
|
||||
'major' => 'major_masuk',
|
||||
'tahun_lulus_polije' => 'tahun_lulus_polije',
|
||||
'tahun_lulus' => 'tahun_lulus_polije',
|
||||
'graduation_year' => 'tahun_lulus_polije',
|
||||
'tahun' => 'tahun_lulus_polije',
|
||||
'catatan' => 'catatan',
|
||||
'keterangan' => 'catatan',
|
||||
'notes' => 'catatan',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Map row data to Alumni model
|
||||
*/
|
||||
private function mapRow($row, $headers, $columnMap)
|
||||
{
|
||||
$record = [];
|
||||
|
||||
foreach ($headers as $idx => $header) {
|
||||
$value = $row[$idx] ?? null;
|
||||
|
||||
if (!$value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Map column name
|
||||
$dbColumn = $columnMap[strtolower($header)] ?? null;
|
||||
|
||||
if (!$dbColumn) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Type conversion
|
||||
if (in_array($dbColumn, ['nilai_rata_rata', 'tahun_lulus_polije'])) {
|
||||
$record[$dbColumn] = (float) $value;
|
||||
} else {
|
||||
$record[$dbColumn] = trim((string) $value);
|
||||
}
|
||||
}
|
||||
|
||||
return empty($record) ? null : $record;
|
||||
}
|
||||
}
|
||||
|
|
@ -54,8 +54,8 @@ function ($user) use ($request) {
|
|||
// the application's home authenticated view. If there is an error we can
|
||||
// redirect them back to where they came from with their error message.
|
||||
return $status == Password::PASSWORD_RESET
|
||||
? redirect()->route('login')->with('status', __($status))
|
||||
? redirect()->route('login')->with('status', '✨ Password berhasil direset! Sekarang Anda bisa login dengan password baru.')
|
||||
: back()->withInput($request->only('email'))
|
||||
->withErrors(['email' => __($status)]);
|
||||
->withErrors(['token' => 'Token reset password telah kadaluarsa atau tidak valid. Silakan minta link reset password baru.']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class PasswordResetWithCodeController extends Controller
|
||||
{
|
||||
public function resetWithCode(Request $request)
|
||||
{
|
||||
$data = $request->only(['email','token','password','password_confirmation']);
|
||||
|
||||
$validator = Validator::make($data, [
|
||||
'email' => 'required|email',
|
||||
'token' => 'required|digits:6',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return back()->withErrors($validator)->withInput();
|
||||
}
|
||||
|
||||
$email = $data['email'];
|
||||
$code = $data['token'];
|
||||
|
||||
$expiryMinutes = config('auth.passwords.users.expire', 60);
|
||||
$cutoff = Carbon::now()->subMinutes($expiryMinutes);
|
||||
|
||||
$row = DB::table('password_reset_codes')
|
||||
->where('email', $email)
|
||||
->where('code', $code)
|
||||
->where('created_at', '>=', $cutoff)
|
||||
->first();
|
||||
|
||||
if (! $row) {
|
||||
return back()->withErrors(['token' => 'Token tidak valid atau sudah kadaluarsa.'])->withInput();
|
||||
}
|
||||
|
||||
$user = User::where('email', $email)->first();
|
||||
if (! $user) {
|
||||
return back()->withErrors(['email' => 'Akun dengan email ini tidak ditemukan.'])->withInput();
|
||||
}
|
||||
|
||||
$user->password = Hash::make($data['password']);
|
||||
$user->save();
|
||||
|
||||
// remove used codes
|
||||
DB::table('password_reset_codes')->where('email', $email)->delete();
|
||||
|
||||
return redirect()->route('login')->with('status', 'Password berhasil diubah. Silakan login dengan password baru.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify code for an email without changing password (used for two-step flow)
|
||||
*/
|
||||
public function verifyCode(Request $request)
|
||||
{
|
||||
$data = $request->only(['email','token']);
|
||||
|
||||
$validator = \Illuminate\Support\Facades\Validator::make($data, [
|
||||
'email' => 'required|email',
|
||||
'token' => 'required|digits:6',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json(['ok' => false, 'errors' => $validator->errors()->all()], 422);
|
||||
}
|
||||
|
||||
$email = $data['email'];
|
||||
$code = $data['token'];
|
||||
|
||||
$expiryMinutes = config('auth.passwords.users.expire', 60);
|
||||
$cutoff = Carbon::now()->subMinutes($expiryMinutes);
|
||||
|
||||
$row = DB::table('password_reset_codes')
|
||||
->where('email', $email)
|
||||
->where('code', $code)
|
||||
->where('created_at', '>=', $cutoff)
|
||||
->first();
|
||||
|
||||
if (! $row) {
|
||||
return response()->json(['ok' => false, 'message' => 'Token tidak valid atau sudah kadaluarsa.'], 404);
|
||||
}
|
||||
|
||||
return response()->json(['ok' => true, 'message' => 'Token valid. Silakan masukkan password baru.']);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Alumni;
|
||||
use App\Models\PolijeMajor;
|
||||
use App\Models\Recommendation;
|
||||
use App\Models\ChatHistory;
|
||||
|
|
@ -13,6 +14,37 @@
|
|||
|
||||
class BKController extends Controller
|
||||
{
|
||||
private const IPA_SUBJECTS = ['mtk', 'fisika', 'kimia', 'biologi'];
|
||||
private const IPS_SUBJECTS = ['ekonomi', 'geografi', 'sosiologi', 'sejarah'];
|
||||
|
||||
private const ALL_SUBJECTS = ['mtk', 'fisika', 'kimia', 'biologi', 'ekonomi', 'geografi', 'sosiologi', 'sejarah'];
|
||||
|
||||
private function normalizeScoreFields(array $validated, string $kelompokAsal): array
|
||||
{
|
||||
$activeSubjects = $kelompokAsal === 'IPA' ? self::IPA_SUBJECTS : self::IPS_SUBJECTS;
|
||||
|
||||
foreach (self::ALL_SUBJECTS as $subject) {
|
||||
if (!in_array($subject, $activeSubjects, true)) {
|
||||
$validated[$subject] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $validated;
|
||||
}
|
||||
|
||||
private function validateScoreByKelompok(Request $request): void
|
||||
{
|
||||
$requiredSubjects = $request->input('kelompok_asal') === 'IPA'
|
||||
? self::IPA_SUBJECTS
|
||||
: self::IPS_SUBJECTS;
|
||||
|
||||
foreach ($requiredSubjects as $subject) {
|
||||
$request->validate([
|
||||
$subject => 'required|numeric|min:0|max:100',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 1. DASHBOARD
|
||||
// ============================================
|
||||
|
|
@ -38,15 +70,91 @@ public function dashboard()
|
|||
->groupBy('kelompok_asal')
|
||||
->get();
|
||||
|
||||
// Rekomendasi per kelompok
|
||||
$rekomendasiPerKelompok = Recommendation::selectRaw(
|
||||
'users.kelompok_asal, COUNT(*) as count'
|
||||
)
|
||||
->join('users', 'rekomendasi.user_id', '=', 'users.id')
|
||||
->groupBy('users.kelompok_asal')
|
||||
->get();
|
||||
|
||||
$topMajors = Recommendation::selectRaw("
|
||||
JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') as major_name,
|
||||
COUNT(*) as count
|
||||
")
|
||||
->groupByRaw("JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan')")
|
||||
->orderBy('count', 'desc')
|
||||
->groupByRaw("JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan')") ->whereRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') IS NOT NULL")
|
||||
->whereRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') != 'null'") ->orderBy('count', 'desc')
|
||||
->take(5)
|
||||
->get();
|
||||
|
||||
// Data untuk chart - semua jurusan (filter out NULL values)
|
||||
$allMajorsChart = Recommendation::selectRaw("
|
||||
JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') as major_name,
|
||||
COUNT(*) as count
|
||||
")
|
||||
->groupByRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan')")
|
||||
->whereRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') IS NOT NULL")
|
||||
->whereRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') != 'null'")
|
||||
->orderBy('count', 'desc')
|
||||
->get();
|
||||
|
||||
// Persiapkan data untuk Chart.js - aggregate & fix major names
|
||||
$majorData = [];
|
||||
foreach ($allMajorsChart as $item) {
|
||||
$name = trim($item->major_name, '" ');
|
||||
|
||||
// Skip empty/null values
|
||||
if (empty($name) || $name === 'null') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Normalize: handle all variants
|
||||
$normalizedName = $name;
|
||||
if (stripos($name, 'Teknik Informatika') === 0 || stripos($name, 'Teknologi Informasi') === 0) {
|
||||
$normalizedName = 'Teknologi Informasi';
|
||||
}
|
||||
|
||||
if (!isset($majorData[$normalizedName])) {
|
||||
$majorData[$normalizedName] = 0;
|
||||
}
|
||||
$majorData[$normalizedName] += (int)$item->count;
|
||||
}
|
||||
|
||||
// Sort by count descending
|
||||
arsort($majorData);
|
||||
|
||||
$chartMajorNames = array_keys($majorData);
|
||||
$chartMajorCounts = array_values($majorData);
|
||||
|
||||
$chartKelompokNames = $kelompokStats->pluck('kelompok_asal')->toArray();
|
||||
$chartKelompokCounts = $kelompokStats->pluck('count')->toArray();
|
||||
|
||||
// Top majors untuk horizontal bar chart - aggregate & fix
|
||||
$topMajorData = [];
|
||||
foreach ($topMajors as $item) {
|
||||
$name = trim($item->major_name, '" ');
|
||||
|
||||
// Skip empty/null values
|
||||
if (empty($name) || $name === 'null') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Normalize: handle all variants
|
||||
$normalizedName = $name;
|
||||
if (stripos($name, 'Teknik Informatika') === 0 || stripos($name, 'Teknologi Informasi') === 0) {
|
||||
$normalizedName = 'Teknologi Informasi';
|
||||
}
|
||||
|
||||
if (!isset($topMajorData[$normalizedName])) {
|
||||
$topMajorData[$normalizedName] = 0;
|
||||
}
|
||||
$topMajorData[$normalizedName] += (int)$item->count;
|
||||
}
|
||||
|
||||
arsort($topMajorData);
|
||||
$topMajorsChart = array_keys($topMajorData);
|
||||
$topMajorsCounts = array_values($topMajorData);
|
||||
|
||||
return view('bk.dashboard', compact(
|
||||
'totalSiswa',
|
||||
'totalRekomendasi',
|
||||
|
|
@ -55,7 +163,14 @@ public function dashboard()
|
|||
'recentStudents',
|
||||
'recentRecommendations',
|
||||
'kelompokStats',
|
||||
'topMajors'
|
||||
'topMajors',
|
||||
'chartMajorNames',
|
||||
'chartMajorCounts',
|
||||
'chartKelompokNames',
|
||||
'chartKelompokCounts',
|
||||
'topMajorsChart',
|
||||
'topMajorsCounts',
|
||||
'rekomendasiPerKelompok'
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -148,8 +263,8 @@ public function riwayatChatbot(Request $request)
|
|||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('prompt', 'like', "%{$search}%")
|
||||
->orWhere('response', 'like', "%{$search}%")
|
||||
$q->where('pertanyaan', 'like', "%{$search}%")
|
||||
->orWhere('jawaban', 'like', "%{$search}%")
|
||||
->orWhereHas('user', function ($q2) use ($search) {
|
||||
$q2->where('name', 'like', "%{$search}%");
|
||||
});
|
||||
|
|
@ -181,19 +296,22 @@ public function jurusanCreate()
|
|||
public function jurusanStore(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'nama_jurusan' => 'required|string|max:255|unique:polije_majors,nama_jurusan',
|
||||
'deskripsi' => 'nullable|string|max:1000',
|
||||
'nama_jurusan' => 'required|string|min:3|max:255|unique:jurusan_polije,nama_jurusan',
|
||||
'deskripsi' => 'nullable|string|max:10000',
|
||||
'keywords' => 'nullable|string',
|
||||
'preferensi_studi' => 'nullable|string',
|
||||
'prospek_kerja' => 'nullable|string|max:1000',
|
||||
'bobot_mtk' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_fisika' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_kimia' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_biologi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_ekonomi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_geografi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_sosiologi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_sejarah' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel' => 'nullable|array',
|
||||
'bobot_mapel.ipa' => 'nullable|array',
|
||||
'bobot_mapel.ipa.mtk' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ipa.fisika' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ipa.kimia' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ipa.biologi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ips' => 'nullable|array',
|
||||
'bobot_mapel.ips.ekonomi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ips.geografi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ips.sosiologi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ips.sejarah' => 'nullable|numeric|min:0|max:1',
|
||||
]);
|
||||
|
||||
PolijeMajor::create([
|
||||
|
|
@ -219,19 +337,22 @@ public function jurusanUpdate(Request $request, $id)
|
|||
$jurusan = PolijeMajor::findOrFail($id);
|
||||
|
||||
$request->validate([
|
||||
'nama_jurusan' => ['required', 'string', 'max:255', Rule::unique('polije_majors', 'nama_jurusan')->ignore($jurusan->id)],
|
||||
'deskripsi' => 'nullable|string|max:1000',
|
||||
'nama_jurusan' => ['required', 'string', 'min:3', 'max:255', Rule::unique('jurusan_polije', 'nama_jurusan')->ignore($jurusan->id)],
|
||||
'deskripsi' => 'nullable|string|max:10000',
|
||||
'keywords' => 'nullable|string',
|
||||
'preferensi_studi' => 'nullable|string',
|
||||
'prospek_kerja' => 'nullable|string|max:1000',
|
||||
'bobot_mtk' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_fisika' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_kimia' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_biologi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_ekonomi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_geografi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_sosiologi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_sejarah' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel' => 'nullable|array',
|
||||
'bobot_mapel.ipa' => 'nullable|array',
|
||||
'bobot_mapel.ipa.mtk' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ipa.fisika' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ipa.kimia' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ipa.biologi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ips' => 'nullable|array',
|
||||
'bobot_mapel.ips.ekonomi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ips.geografi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ips.sosiologi' => 'nullable|numeric|min:0|max:1',
|
||||
'bobot_mapel.ips.sejarah' => 'nullable|numeric|min:0|max:1',
|
||||
]);
|
||||
|
||||
$jurusan->update([
|
||||
|
|
@ -262,19 +383,172 @@ private function parseTagInput(?string $input): array
|
|||
|
||||
private function parseBobotMapel(Request $request): array
|
||||
{
|
||||
$mapelList = ['mtk', 'fisika', 'kimia', 'biologi', 'ekonomi', 'geografi', 'sosiologi', 'sejarah'];
|
||||
$bobot = [];
|
||||
foreach ($mapelList as $mapel) {
|
||||
$value = $request->input("bobot_{$mapel}");
|
||||
if (!is_null($value) && $value !== '') {
|
||||
$bobot[$mapel] = floatval($value);
|
||||
}
|
||||
$ipaSubjects = ['mtk', 'fisika', 'kimia', 'biologi'];
|
||||
$ipsSubjects = ['ekonomi', 'geografi', 'sosiologi', 'sejarah'];
|
||||
|
||||
$ipaInput = $request->input('bobot_mapel.ipa');
|
||||
$ipsInput = $request->input('bobot_mapel.ips');
|
||||
|
||||
if (is_array($ipaInput) || is_array($ipsInput)) {
|
||||
return [
|
||||
'ipa' => $this->normalizeBobotGroup(is_array($ipaInput) ? $ipaInput : [], $ipaSubjects),
|
||||
'ips' => $this->normalizeBobotGroup(is_array($ipsInput) ? $ipsInput : [], $ipsSubjects),
|
||||
];
|
||||
}
|
||||
return $bobot;
|
||||
|
||||
return [
|
||||
'ipa' => $this->normalizeBobotGroup([
|
||||
'mtk' => $request->input('bobot_mtk'),
|
||||
'fisika' => $request->input('bobot_fisika'),
|
||||
'kimia' => $request->input('bobot_kimia'),
|
||||
'biologi' => $request->input('bobot_biologi'),
|
||||
], $ipaSubjects),
|
||||
'ips' => $this->normalizeBobotGroup([
|
||||
'ekonomi' => $request->input('bobot_ekonomi'),
|
||||
'geografi' => $request->input('bobot_geografi'),
|
||||
'sosiologi' => $request->input('bobot_sosiologi'),
|
||||
'sejarah' => $request->input('bobot_sejarah'),
|
||||
], $ipsSubjects),
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeBobotGroup(array $values, array $subjects): array
|
||||
{
|
||||
$normalized = [];
|
||||
|
||||
foreach ($subjects as $subject) {
|
||||
$value = $values[$subject] ?? null;
|
||||
$normalized[$subject] = is_numeric($value) ? (float) $value : 0.0;
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 6. PROFIL GURU BK
|
||||
// 6. MANAJEMEN ALUMNI
|
||||
// ============================================
|
||||
public function alumni()
|
||||
{
|
||||
$alumni = Alumni::orderBy('tahun_masuk', 'desc')->paginate(20);
|
||||
$summary = $this->getAlumniSummary();
|
||||
|
||||
return view('bk.alumni.index', compact('alumni', 'summary'));
|
||||
}
|
||||
|
||||
public function alumniCreate()
|
||||
{
|
||||
return view('bk.alumni.create');
|
||||
}
|
||||
|
||||
public function alumniStore(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'nama_alumni' => 'required|string|min:3|max:255',
|
||||
'nis' => 'nullable|string|max:20',
|
||||
'kelompok_asal' => 'required|in:IPA,IPS',
|
||||
|
||||
// Nilai
|
||||
'mtk' => 'nullable|numeric|min:0|max:100',
|
||||
'fisika' => 'nullable|numeric|min:0|max:100',
|
||||
'kimia' => 'nullable|numeric|min:0|max:100',
|
||||
'biologi' => 'nullable|numeric|min:0|max:100',
|
||||
'ekonomi' => 'nullable|numeric|min:0|max:100',
|
||||
'geografi' => 'nullable|numeric|min:0|max:100',
|
||||
'sosiologi' => 'nullable|numeric|min:0|max:100',
|
||||
'sejarah' => 'nullable|numeric|min:0|max:100',
|
||||
|
||||
// Non-akademik
|
||||
'minat' => 'nullable|string|max:255',
|
||||
'cita_cita' => 'nullable|string|max:255',
|
||||
'preferensi_studi' => 'nullable|in:Praktik Langsung,Praktik_Langsung,DuDi,Project Based,Project_Based,Blended Learning,Blended',
|
||||
'prestasi' => 'nullable|string|max:255',
|
||||
|
||||
// Major
|
||||
'major_masuk' => 'required|string|min:3|max:255',
|
||||
'tahun_lulus_polije' => 'nullable|integer|min:2020|max:' . date('Y'),
|
||||
'catatan' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$this->validateScoreByKelompok($request);
|
||||
$validated = $this->normalizeScoreFields($validated, $validated['kelompok_asal']);
|
||||
|
||||
Alumni::create($validated);
|
||||
|
||||
return redirect()->route('bk.alumni')->with('success', 'Alumni berhasil ditambahkan');
|
||||
}
|
||||
|
||||
public function alumniShow(Alumni $alumni)
|
||||
{
|
||||
return view('bk.alumni.show', compact('alumni'));
|
||||
}
|
||||
|
||||
public function alumniEdit(Alumni $alumni)
|
||||
{
|
||||
return view('bk.alumni.edit', compact('alumni'));
|
||||
}
|
||||
|
||||
public function alumniUpdate(Request $request, Alumni $alumni)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'nama_alumni' => 'required|string|min:3|max:255',
|
||||
'nis' => 'nullable|string|max:20',
|
||||
'kelompok_asal' => 'required|in:IPA,IPS',
|
||||
|
||||
'mtk' => 'nullable|numeric|min:0|max:100',
|
||||
'fisika' => 'nullable|numeric|min:0|max:100',
|
||||
'kimia' => 'nullable|numeric|min:0|max:100',
|
||||
'biologi' => 'nullable|numeric|min:0|max:100',
|
||||
'ekonomi' => 'nullable|numeric|min:0|max:100',
|
||||
'geografi' => 'nullable|numeric|min:0|max:100',
|
||||
'sosiologi' => 'nullable|numeric|min:0|max:100',
|
||||
'sejarah' => 'nullable|numeric|min:0|max:100',
|
||||
|
||||
'minat' => 'nullable|string|max:255',
|
||||
'cita_cita' => 'nullable|string|max:255',
|
||||
'preferensi_studi' => 'nullable|in:Praktik Langsung,Praktik_Langsung,DuDi,Project Based,Project_Based,Blended Learning,Blended',
|
||||
'prestasi' => 'nullable|string|max:255',
|
||||
|
||||
'major_masuk' => 'required|string|min:3|max:255',
|
||||
'tahun_lulus_polije' => 'nullable|integer|min:2020|max:' . date('Y'),
|
||||
'catatan' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$this->validateScoreByKelompok($request);
|
||||
$validated = $this->normalizeScoreFields($validated, $validated['kelompok_asal']);
|
||||
|
||||
$alumni->update($validated);
|
||||
|
||||
return redirect()->route('bk.alumni')->with('success', 'Alumni berhasil diupdate');
|
||||
}
|
||||
|
||||
public function alumniDestroy(Alumni $alumni)
|
||||
{
|
||||
$alumni->delete();
|
||||
return redirect()->route('bk.alumni')->with('success', 'Alumni berhasil dihapus');
|
||||
}
|
||||
|
||||
private function getAlumniSummary()
|
||||
{
|
||||
$totalAlumni = Alumni::count();
|
||||
|
||||
$byMajor = Alumni::selectRaw('major_masuk, COUNT(*) as count')
|
||||
->groupBy('major_masuk')
|
||||
->get();
|
||||
|
||||
// Statistics by kelompok asal (IPA/IPS)
|
||||
$byKelompok = Alumni::selectRaw('kelompok_asal, COUNT(*) as count')
|
||||
->groupBy('kelompok_asal')
|
||||
->get();
|
||||
|
||||
return [
|
||||
'total' => $totalAlumni,
|
||||
'by_major' => $byMajor,
|
||||
'by_kelompok' => $byKelompok,
|
||||
];
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 7. PROFIL GURU BK
|
||||
// ============================================
|
||||
public function profil()
|
||||
{
|
||||
|
|
@ -287,7 +561,7 @@ public function updateProfil(Request $request)
|
|||
$guru = Auth::user();
|
||||
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'name' => 'required|string|min:3|max:255',
|
||||
'email' => ['required', 'email', Rule::unique('users')->ignore($guru->id)],
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -30,13 +30,21 @@ public function index(Request $request)
|
|||
$user = Auth::user();
|
||||
$sessionId = $request->query('session');
|
||||
$recId = $request->query('rec');
|
||||
$isNew = $request->query('new'); // Deteksi jika langsung buka chatbot tanpa rekomendasi
|
||||
$previousMessages = [];
|
||||
$recommendationId = null;
|
||||
|
||||
if ($sessionId) {
|
||||
// Jika ?new=1, abaikan session dan rekomendasi lama - buat fresh session
|
||||
if ($isNew) {
|
||||
$sessionId = Str::uuid()->toString();
|
||||
$recommendationId = null;
|
||||
$previousMessages = [];
|
||||
// Clear session lama
|
||||
session()->forget('recomendation_data');
|
||||
} else if ($sessionId) {
|
||||
// Lanjutkan sesi lama — ambil semua chat dari sesi ini
|
||||
$chats = ChatHistory::where('user_id', $user->id)
|
||||
->where('session_id', $sessionId)
|
||||
->where('id_sesi', $sessionId)
|
||||
->orderBy('created_at', 'asc')
|
||||
->get();
|
||||
|
||||
|
|
@ -63,10 +71,7 @@ public function index(Request $request)
|
|||
$sessionId = Str::uuid()->toString();
|
||||
}
|
||||
|
||||
// Tentukan recommendation_id:
|
||||
// 1. Dari sesi lama (sudah diset di atas)
|
||||
// 2. Dari parameter ?rec= (klik dari hasil rekomendasi)
|
||||
// 3. Dari rekomendasi terbaru user
|
||||
// Tentukan recommendation_id (kecuali sudah diset oleh ?new=1 atau session lama):
|
||||
if (!$recommendationId && $recId) {
|
||||
$rec = Recommendation::where('id', $recId)
|
||||
->where('user_id', $user->id)
|
||||
|
|
@ -74,13 +79,14 @@ public function index(Request $request)
|
|||
$recommendationId = $rec ? $rec->id : null;
|
||||
}
|
||||
|
||||
if (!$recommendationId) {
|
||||
// Load rekomendasi terbaru jika tidak ada kondisi di atas
|
||||
if (!$recommendationId && !$isNew) {
|
||||
$latestRec = Recommendation::where('user_id', $user->id)->latest()->first();
|
||||
$recommendationId = $latestRec ? $latestRec->id : null;
|
||||
}
|
||||
|
||||
// Ambil konteks rekomendasi berdasarkan ID spesifik
|
||||
$recentRecommendation = $this->getRecommendationContext($user, $recommendationId);
|
||||
$recentRecommendation = $this->getRecommendationContext($user, $recommendationId, $isNew);
|
||||
|
||||
return view('chatbot.index', [
|
||||
'recommendation' => $recentRecommendation,
|
||||
|
|
@ -118,6 +124,7 @@ public function send(Request $request)
|
|||
'recommendation' => $recentRecommendation['jurusan'] ?? null,
|
||||
'score' => isset($recentRecommendation['skor']) ? number_format(($recentRecommendation['skor'] > 1 ? $recentRecommendation['skor'] : $recentRecommendation['skor'] * 100), 1) : null,
|
||||
'top3' => $recentRecommendation['top3'] ?? [],
|
||||
'intent' => $this->detectIntent($message),
|
||||
'profile' => [
|
||||
'nama' => $user->name,
|
||||
'kelompok' => $user->kelompok_asal ?? null,
|
||||
|
|
@ -139,14 +146,32 @@ public function send(Request $request)
|
|||
// Panggil Gemini API dengan conversation history
|
||||
$response = $this->geminiService->chat($message, $context, $chatHistory);
|
||||
|
||||
// Normalisasi respons agar error tetap memiliki pesan yang konsisten.
|
||||
$isSuccess = (bool) ($response['success'] ?? false);
|
||||
$errorCode = (string) ($response['error_code'] ?? 'CHAT_SERVICE_ERROR');
|
||||
$responseMessage = trim((string) ($response['message'] ?? ''));
|
||||
|
||||
if ($responseMessage === '') {
|
||||
$responseMessage = $isSuccess
|
||||
? 'Respons berhasil diproses.'
|
||||
: 'Maaf, layanan chatbot sedang mengalami gangguan. Silakan coba kembali beberapa saat lagi.';
|
||||
}
|
||||
|
||||
if (!$isSuccess) {
|
||||
$responseMessage = "[ERROR:{$errorCode}] {$responseMessage}";
|
||||
}
|
||||
|
||||
$response['message'] = $responseMessage;
|
||||
$response['error_code'] = $isSuccess ? null : $errorCode;
|
||||
|
||||
// Simpan chat ke database dengan session_id dan recommendation_id
|
||||
if ($user && isset($response['message'])) {
|
||||
if ($user) {
|
||||
ChatHistory::create([
|
||||
'user_id' => $user->id,
|
||||
'session_id' => $sessionId,
|
||||
'recommendation_id' => $recommendationId,
|
||||
'prompt' => $message,
|
||||
'response' => $response['message'],
|
||||
'id_sesi' => $sessionId,
|
||||
'id_rekomendasi' => $recommendationId,
|
||||
'pertanyaan' => $message,
|
||||
'jawaban' => $responseMessage,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -166,21 +191,31 @@ public function historyChat()
|
|||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
// Kelompokkan per session_id
|
||||
$sessions = $chatHistories->groupBy('session_id')->map(function ($chats, $sessionId) {
|
||||
// Kelompokkan per id_sesi
|
||||
$sessions = $chatHistories->groupBy('id_sesi')->map(function ($chats, $sessionId) {
|
||||
$first = $chats->last(); // oldest in group (karena desc)
|
||||
$last = $chats->first(); // newest in group
|
||||
$rec = $first->recommendation;
|
||||
$recInfo = null;
|
||||
if ($rec) {
|
||||
$hasil = is_array($rec->hasil_rekomendasi)
|
||||
? $rec->hasil_rekomendasi
|
||||
: json_decode($rec->hasil_rekomendasi, true);
|
||||
// Safely decode hasil_rekomendasi
|
||||
$hasil = [];
|
||||
if (!empty($rec->hasil_rekomendasi)) {
|
||||
$hasil = is_array($rec->hasil_rekomendasi)
|
||||
? $rec->hasil_rekomendasi
|
||||
: json_decode($rec->hasil_rekomendasi, true);
|
||||
|
||||
// Validate hasil is array
|
||||
if (!is_array($hasil)) {
|
||||
$hasil = [];
|
||||
}
|
||||
}
|
||||
|
||||
$topJurusan = $hasil[0] ?? null;
|
||||
$recInfo = [
|
||||
'id' => $rec->id,
|
||||
'jurusan' => $topJurusan['jurusan'] ?? '-',
|
||||
'skor' => $topJurusan['skor'] ?? 0,
|
||||
'jurusan' => is_array($topJurusan) ? ($topJurusan['jurusan'] ?? '-') : '-',
|
||||
'skor' => is_array($topJurusan) ? ($topJurusan['skor'] ?? 0) : 0,
|
||||
'tanggal' => $rec->created_at,
|
||||
];
|
||||
}
|
||||
|
|
@ -202,56 +237,79 @@ public function historyChat()
|
|||
* Ambil konteks rekomendasi berdasarkan ID spesifik.
|
||||
* Jika ID tidak ada, coba dari session, lalu dari DB (terbaru).
|
||||
*/
|
||||
private function getRecommendationContext($user, $recommendationId = null)
|
||||
private function getRecommendationContext($user, $recommendationId = null, $isNew = false)
|
||||
{
|
||||
// Jika ada recommendation_id spesifik, ambil langsung dari DB
|
||||
$lastRec = null;
|
||||
|
||||
// Jika ada recommendation_id spesifik, ambil langsung dari DB dan JANGAN fallback
|
||||
if ($recommendationId) {
|
||||
$lastRec = Recommendation::where('id', $recommendationId)
|
||||
->where('user_id', $user->id)
|
||||
->first();
|
||||
}
|
||||
|
||||
// Fallback: dari session (saat baru selesai rekomendasi)
|
||||
if (!$lastRec) {
|
||||
if (!$lastRec) {
|
||||
// Jika rec ID tidak ditemukan, jangan fallback - return null
|
||||
return null;
|
||||
}
|
||||
} else if ($isNew) {
|
||||
// Jika ?new=1, jangan load apapun dari session atau DB
|
||||
return null;
|
||||
} else {
|
||||
// Fallback: dari session (saat baru selesai rekomendasi)
|
||||
$sessionData = session('recomendation_data', null);
|
||||
if ($sessionData) {
|
||||
return $sessionData;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: rekomendasi terbaru dari DB
|
||||
if (!$lastRec) {
|
||||
// Fallback: rekomendasi terbaru dari DB
|
||||
$lastRec = Recommendation::where('user_id', $user->id)
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if (!$lastRec) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$lastRec) {
|
||||
return null;
|
||||
// Safely decode hasil_rekomendasi
|
||||
$hasil = [];
|
||||
if (!empty($lastRec->hasil_rekomendasi)) {
|
||||
$hasil = is_array($lastRec->hasil_rekomendasi)
|
||||
? $lastRec->hasil_rekomendasi
|
||||
: json_decode($lastRec->hasil_rekomendasi, true);
|
||||
|
||||
// Validate hasil is array
|
||||
if (!is_array($hasil)) {
|
||||
$hasil = [];
|
||||
}
|
||||
}
|
||||
|
||||
$hasil = is_array($lastRec->hasil_rekomendasi)
|
||||
? $lastRec->hasil_rekomendasi
|
||||
: json_decode($lastRec->hasil_rekomendasi, true);
|
||||
$topJurusan = $hasil[0] ?? null;
|
||||
$top3 = array_slice($hasil ?? [], 0, 3);
|
||||
|
||||
// Hitung rata-rata dari kolom nilai
|
||||
// Hitung rata-rata dari kolom nilai dengan safe access
|
||||
$nilaiCols = ['mtk', 'fisika', 'kimia', 'biologi', 'ekonomi', 'geografi', 'sosiologi', 'sejarah'];
|
||||
$validVals = array_filter(array_map(fn($c) => $lastRec->$c, $nilaiCols), fn($v) => $v !== null);
|
||||
$validVals = [];
|
||||
foreach ($nilaiCols as $col) {
|
||||
$val = $lastRec->getAttribute($col);
|
||||
if ($val !== null && is_numeric($val)) {
|
||||
$validVals[] = $val;
|
||||
}
|
||||
}
|
||||
$rataRata = count($validVals) > 0 ? round(array_sum($validVals) / count($validVals), 1) : null;
|
||||
|
||||
// Kategorisasi
|
||||
// Kategorisasi nilai
|
||||
$katNilai = 'Rendah';
|
||||
if ($rataRata >= 85) $katNilai = 'Tinggi';
|
||||
elseif ($rataRata >= 70) $katNilai = 'Sedang';
|
||||
if ($rataRata !== null) {
|
||||
if ($rataRata >= 85) {
|
||||
$katNilai = 'Tinggi';
|
||||
} elseif ($rataRata >= 70) {
|
||||
$katNilai = 'Sedang';
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'jurusan' => $topJurusan['jurusan'] ?? null,
|
||||
'skor' => $topJurusan['skor'] ?? null,
|
||||
'detail' => $topJurusan['detail'] ?? [],
|
||||
'detail' => is_array($topJurusan['detail'] ?? null) ? $topJurusan['detail'] : [],
|
||||
'nilai' => $katNilai,
|
||||
'rata_rata' => $rataRata,
|
||||
'minat' => $lastRec->minat,
|
||||
|
|
@ -259,8 +317,8 @@ private function getRecommendationContext($user, $recommendationId = null)
|
|||
'cita_cita' => $lastRec->cita_cita,
|
||||
'prestasi' => $lastRec->prestasi,
|
||||
'top3' => array_map(fn($r) => [
|
||||
'jurusan' => $r['jurusan'] ?? '',
|
||||
'skor' => $r['skor'] ?? 0,
|
||||
'jurusan' => is_array($r) ? ($r['jurusan'] ?? '') : '',
|
||||
'skor' => is_array($r) ? ($r['skor'] ?? 0) : 0,
|
||||
], $top3),
|
||||
];
|
||||
}
|
||||
|
|
@ -286,12 +344,12 @@ private function findSimilarQuestions(string $message, int $currentUserId): arra
|
|||
}
|
||||
|
||||
// Cari chat history yang mengandung kata kunci serupa
|
||||
$query = ChatHistory::select('prompt', 'response', 'created_at')
|
||||
$query = ChatHistory::select('pertanyaan', 'jawaban', 'created_at')
|
||||
->where('user_id', $currentUserId);
|
||||
|
||||
$query->where(function ($q) use ($keywords) {
|
||||
foreach ($keywords as $keyword) {
|
||||
$q->orWhere('prompt', 'like', "%{$keyword}%");
|
||||
$q->orWhere('pertanyaan', 'like', "%{$keyword}%");
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -306,7 +364,7 @@ private function findSimilarQuestions(string $message, int $currentUserId): arra
|
|||
// Scoring: hitung berapa keyword yang cocok
|
||||
$scored = [];
|
||||
foreach ($candidates as $chat) {
|
||||
$promptLower = strtolower($chat->prompt);
|
||||
$promptLower = strtolower($chat->pertanyaan);
|
||||
$matchCount = 0;
|
||||
foreach ($keywords as $kw) {
|
||||
if (stripos($promptLower, $kw) !== false) {
|
||||
|
|
@ -316,8 +374,8 @@ private function findSimilarQuestions(string $message, int $currentUserId): arra
|
|||
$ratio = $matchCount / count($keywords);
|
||||
if ($ratio >= 0.4) { // minimal 40% keyword cocok
|
||||
$scored[] = [
|
||||
'prompt' => $chat->prompt,
|
||||
'response' => Str::limit($chat->response, 300),
|
||||
'prompt' => $chat->pertanyaan,
|
||||
'response' => Str::limit($chat->jawaban, 300),
|
||||
'score' => $ratio,
|
||||
];
|
||||
}
|
||||
|
|
@ -339,4 +397,48 @@ private function stripMarkdown(string $text): string
|
|||
$text = preg_replace('/`(.*?)`/s', '$1', $text);
|
||||
return $text;
|
||||
}
|
||||
|
||||
private function detectIntent(string $message): string
|
||||
{
|
||||
$message = strtolower($message);
|
||||
|
||||
if (
|
||||
str_contains($message, 'banding') ||
|
||||
str_contains($message, 'beda') ||
|
||||
str_contains($message, 'vs') ||
|
||||
str_contains($message, 'dibanding')
|
||||
) {
|
||||
return 'compare_majors';
|
||||
}
|
||||
|
||||
if (
|
||||
str_contains($message, 'jelaskan semua') ||
|
||||
str_contains($message, 'semua jurusan')
|
||||
) {
|
||||
return 'explain_all_majors';
|
||||
}
|
||||
|
||||
if (
|
||||
str_contains($message, 'lanjut') ||
|
||||
str_contains($message, 'yang tadi') ||
|
||||
str_contains($message, 'yang sebelumnya') ||
|
||||
str_contains($message, 'maksudnya')
|
||||
) {
|
||||
return 'follow_up';
|
||||
}
|
||||
|
||||
if (str_contains($message, 'kenapa') || str_contains($message, 'mengapa')) {
|
||||
return 'ask_reason';
|
||||
}
|
||||
|
||||
if (
|
||||
str_contains($message, 'prospek') ||
|
||||
str_contains($message, 'karir') ||
|
||||
str_contains($message, 'kerja')
|
||||
) {
|
||||
return 'ask_career';
|
||||
}
|
||||
|
||||
return 'general';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,13 +28,30 @@ public function update(ProfileUpdateRequest $request): RedirectResponse
|
|||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
// Handle file upload
|
||||
// Handle file upload with error handling
|
||||
if ($request->hasFile('foto')) {
|
||||
// Simpan file foto
|
||||
$file = $request->file('foto');
|
||||
$filename = time() . '_' . $file->getClientOriginalName();
|
||||
$file->storeAs('public/profile', $filename);
|
||||
$validated['foto'] = 'storage/profile/' . $filename;
|
||||
try {
|
||||
$file = $request->file('foto');
|
||||
|
||||
// Validate file
|
||||
if (!$file->isValid()) {
|
||||
return Redirect::route('profile.edit')->withErrors(['foto' => 'File upload failed. Please try again.']);
|
||||
}
|
||||
|
||||
// Generate unique filename
|
||||
$filename = time() . '_' . preg_replace('/[^a-zA-Z0-9._-]/', '_', $file->getClientOriginalName());
|
||||
|
||||
// Store file
|
||||
$path = $file->storeAs('public/profile', $filename);
|
||||
|
||||
if ($path) {
|
||||
$validated['foto'] = 'storage/profile/' . $filename;
|
||||
} else {
|
||||
return Redirect::route('profile.edit')->withErrors(['foto' => 'Failed to save file.']);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return Redirect::route('profile.edit')->withErrors(['foto' => 'File upload error: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
$request->user()->fill($validated);
|
||||
|
|
|
|||
|
|
@ -25,65 +25,86 @@ public function index()
|
|||
];
|
||||
}
|
||||
|
||||
return view('rekomendasi.input', compact('student'));
|
||||
// Ambil riwayat rekomendasi user (limit 10 terakhir, diurutkan dari terbaru)
|
||||
$recommendations = Recommendation::where('user_id', $user->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit(10)
|
||||
->get();
|
||||
|
||||
return view('rekomendasi.input', compact('student', 'recommendations'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate textual explanation untuk setiap kriteria
|
||||
* Menjelaskan "mengapa jurusan ini cocok" berdasarkan scoring detail
|
||||
*/
|
||||
private function generateExplanation($jurusanNama, $detail, $katNilai, $kategoriMinat, $prefStudi, $prestasi)
|
||||
private function generateExplanation($jurusanNama, $detail, $katNilai, $kategoriMinat, $prefStudi, $prestasiRaw, array $prestasiAnalysis = [])
|
||||
{
|
||||
$explanations = [];
|
||||
|
||||
// 1. Penjelasan Nilai Akademik
|
||||
// 1. Penjelasan Nilai Akademik (Kriteria 1)
|
||||
$skorNilai = $detail['nilai'] ?? 0;
|
||||
if ($skorNilai >= 0.8) {
|
||||
$explanations['nilai'] = "✅ Nilai akademik Anda ($katNilai) sangat sesuai dengan jalur pendidikan yang dibutuhkan jurusan ini.";
|
||||
$explanations['nilai'] = "✅ Nilai akademik Anda ($katNilai: avg score tinggi) sangat sesuai dengan jalur pendidikan yang dibutuhkan jurusan ini.";
|
||||
} elseif ($skorNilai >= 0.6) {
|
||||
$explanations['nilai'] = "✓ Nilai akademik Anda ($katNilai) cukup sesuai dengan persyaratan jurusan ini.";
|
||||
} else {
|
||||
$explanations['nilai'] = "⚠️ Nilai akademik Anda ($katNilai) masih perlu ditingkatkan untuk optimal di jurusan ini, namun tetap relevan.";
|
||||
}
|
||||
|
||||
// 2. Penjelasan Minat
|
||||
// 2. Penjelasan Minat (Kriteria 2)
|
||||
$skorMinat = $detail['minat'] ?? 0;
|
||||
if ($skorMinat >= 0.8) {
|
||||
$explanations['minat'] = "✅ Minat Anda sangat sesuai dan cocok dengan fokus kurikulum $jurusanNama.";
|
||||
$explanations['minat'] = "✅ Minat Anda ($kategoriMinat) sangat sesuai dan cocok dengan fokus kurikulum $jurusanNama. Anda akan mempelajari hal-hal yang Anda sukai.";
|
||||
} elseif ($skorMinat >= 0.6) {
|
||||
$explanations['minat'] = "✓ Minat Anda cukup relevan dan sesuai dengan area pembelajaran di $jurusanNama.";
|
||||
$explanations['minat'] = "✓ Minat Anda ($kategoriMinat) cukup relevan dan sesuai dengan area pembelajaran di $jurusanNama.";
|
||||
} else {
|
||||
$explanations['minat'] = "ℹ️ Minat Anda memiliki kesamaan dan relevansi dengan aspek-aspek tertentu di $jurusanNama.";
|
||||
$explanations['minat'] = "ℹ️ Minat Anda ($kategoriMinat) memiliki kesamaan dan relevansi dengan aspek-aspek tertentu di $jurusanNama.";
|
||||
}
|
||||
|
||||
// 3. Penjelasan Preferensi Studi
|
||||
// 3. Penjelasan Preferensi Studi (Kriteria 3)
|
||||
$skorPref = $detail['pref'] ?? 0;
|
||||
if ($skorPref >= 0.8) {
|
||||
$explanations['pref'] = "✅ Metode pembelajaran \"$prefStudi\" yang Anda pilih sangat sesuai dengan pendekatan pembelajaran $jurusanNama.";
|
||||
$explanations['pref'] = "✅ Preferensi studi \"$prefStudi\" Anda sangat sesuai dengan karakter jurusan $jurusanNama.";
|
||||
} elseif ($skorPref >= 0.6) {
|
||||
$explanations['pref'] = "✓ Preferensi studi \"$prefStudi\" Anda cocok dengan sistem pembelajaran yang diterapkan.";
|
||||
$explanations['pref'] = "✓ Preferensi studi \"$prefStudi\" Anda cukup relevan dengan jurusan ini.";
|
||||
} else {
|
||||
$explanations['pref'] = "ℹ️ Jurusan ini menawarkan elemen pembelajaran \"$prefStudi\" yang relevan dengan preferensi Anda.";
|
||||
$explanations['pref'] = "ℹ️ Jurusan ini masih memiliki keterkaitan dengan preferensi studi \"$prefStudi\" Anda.";
|
||||
}
|
||||
|
||||
// 4. Penjelasan Cita-cita
|
||||
// 4. Penjelasan Cita-cita (Kriteria 4) - IMPROVED with more detail
|
||||
$skorCita = $detail['cita'] ?? 0;
|
||||
if ($skorCita >= 0.8) {
|
||||
$explanations['cita'] = "✅ Cita-cita karir Anda sangat sesuai dan aligned dengan standar lulusan bidang ini.";
|
||||
$explanations['cita'] = "✅ Cita-cita karir Anda sangat sesuai dan aligned dengan standar lulusan $jurusanNama. Jurusan ini secara langsung mempersiapkan Anda untuk mencapai cita-cita tersebut.";
|
||||
} elseif ($skorCita >= 0.6) {
|
||||
$explanations['cita'] = "✓ Cita-cita Anda memiliki potensi besar untuk dicapai melalui jurusan ini.";
|
||||
$explanations['cita'] = "✓ Cita-cita Anda memiliki potensi besar untuk dicapai melalui pendidikan di $jurusanNama. Kurikulum akan membekali skills yang relevan.";
|
||||
} else {
|
||||
$explanations['cita'] = "ℹ️ Jurusan ini membuka jalur karir yang sesuai dengan cita-cita dan aspirasi Anda.";
|
||||
$explanations['cita'] = "ℹ️ Jurusan ini membuka jalur karir yang sesuai dengan cita-cita dan aspirasi Anda, meski tidak secara langsung target-nya.";
|
||||
}
|
||||
|
||||
// 5. Penjelasan Prestasi
|
||||
// 5. Penjelasan Prestasi (Kriteria 5) - IMPROVED with more detail
|
||||
$skorPrestasi = $detail['prestasi'] ?? 0;
|
||||
if ($skorPrestasi >= 0.7) {
|
||||
$explanations['prestasi'] = "✅ Prestasi Anda mencerminkan potensi kuat untuk sukses dan berkembang di jurusan ini.";
|
||||
} elseif ($skorPrestasi >= 0.4) {
|
||||
$explanations['prestasi'] = "✓ Prestasi Anda menunjukkan kemampuan dasar yang memadai dan relevan.";
|
||||
if (!($prestasiAnalysis['provided'] ?? false)) {
|
||||
$explanations['prestasi'] = "ℹ️ Prestasi tidak diisi. Jika Anda memiliki prestasi atau achievement, itu dapat meningkatkan score untuk jurusan ini.";
|
||||
return $explanations;
|
||||
}
|
||||
|
||||
$levelPrestasi = $prestasiAnalysis['level'] ?? 'minimal';
|
||||
$rawPrestasi = $prestasiAnalysis['raw'] ?? '';
|
||||
|
||||
$labelLevel = [
|
||||
'tinggi' => 'TINGGI (Juara/Winner)',
|
||||
'sedang' => 'MENENGAH (Finalis/Medalist)',
|
||||
'cukup' => 'DASAR (Peserta/Sertifikat)',
|
||||
'minimal' => 'MINIMAL',
|
||||
];
|
||||
|
||||
if ($skorPrestasi >= 0.8) {
|
||||
$explanations['prestasi'] = "✅ Prestasi Anda ($labelLevel[$levelPrestasi]): \"$rawPrestasi\" sangat relevan dengan $jurusanNama. Ini menunjukkan Anda memiliki dedication dan capability.";
|
||||
} elseif ($skorPrestasi >= 0.6) {
|
||||
$explanations['prestasi'] = "✓ Prestasi Anda ($labelLevel[$levelPrestasi]): \"$rawPrestasi\" cukup relevan dan menunjukkan potensi di bidang ini.";
|
||||
} else {
|
||||
$explanations['prestasi'] = "ℹ️ Prestasi tidak menjadi hambatan untuk mengembangkan diri dan berkembang di jurusan ini.";
|
||||
$explanations['prestasi'] = "ℹ️ Prestasi Anda ($labelLevel[$levelPrestasi]): \"$rawPrestasi\" menunjukkan usaha yang dapat dikembangkan lebih lanjut di $jurusanNama.";
|
||||
}
|
||||
|
||||
return $explanations;
|
||||
|
|
@ -91,6 +112,52 @@ private function generateExplanation($jurusanNama, $detail, $katNilai, $kategori
|
|||
|
||||
public function proses(Request $request)
|
||||
{
|
||||
// Pastikan perhitungan Naive Bayes selalu berbasis 5 atribut input rekomendasi:
|
||||
// 1) Nilai akademik, 2) Minat, 3) Preferensi studi, 4) Cita-cita, 5) Prestasi.
|
||||
$user = Auth::user();
|
||||
$kelompokAsal = strtoupper($user->kelompok_asal ?? 'IPA');
|
||||
|
||||
// Enhanced validation rules dengan lebih strict untuk non-akademik fields
|
||||
$rules = [
|
||||
'mtk' => 'nullable|numeric|min:0|max:100',
|
||||
'fisika' => 'nullable|numeric|min:0|max:100',
|
||||
'kimia' => 'nullable|numeric|min:0|max:100',
|
||||
'biologi' => 'nullable|numeric|min:0|max:100',
|
||||
'ekonomi' => 'nullable|numeric|min:0|max:100',
|
||||
'geografi' => 'nullable|numeric|min:0|max:100',
|
||||
'sosiologi' => 'nullable|numeric|min:0|max:100',
|
||||
'sejarah' => 'nullable|numeric|min:0|max:100',
|
||||
'minat' => 'required|string|min:3|max:255',
|
||||
'pref_studi' => 'required|string|in:Sains & Teknologi,Pertanian & Lingkungan,Kesehatan & Ilmu Hayat,Bisnis & Manajemen,Sosial & Humaniora',
|
||||
'cita_cita' => 'required|string|min:3|max:255',
|
||||
'prestasi' => 'nullable|string|min:3|max:255',
|
||||
];
|
||||
|
||||
if ($kelompokAsal === 'IPA') {
|
||||
$rules['mtk'] = 'required|numeric|min:0|max:100';
|
||||
$rules['fisika'] = 'required|numeric|min:0|max:100';
|
||||
$rules['kimia'] = 'required|numeric|min:0|max:100';
|
||||
$rules['biologi'] = 'required|numeric|min:0|max:100';
|
||||
} else {
|
||||
$rules['ekonomi'] = 'required|numeric|min:0|max:100';
|
||||
$rules['geografi'] = 'required|numeric|min:0|max:100';
|
||||
$rules['sosiologi'] = 'required|numeric|min:0|max:100';
|
||||
$rules['sejarah'] = 'required|numeric|min:0|max:100';
|
||||
}
|
||||
|
||||
// Custom error messages untuk lebih informatif
|
||||
$messages = [
|
||||
'minat.required' => 'Minat harus diisi (minimal 3 karakter)',
|
||||
'minat.min' => 'Minat terlalu pendek, jelaskan lebih detail',
|
||||
'cita_cita.required' => 'Cita-cita harus diisi (minimal 3 karakter)',
|
||||
'cita_cita.min' => 'Cita-cita terlalu pendek, jelaskan lebih detail',
|
||||
'prestasi.min' => 'Prestasi terlalu pendek, jelaskan lebih detail',
|
||||
'pref_studi.required' => 'Pilih salah satu preferensi studi',
|
||||
'pref_studi.in' => 'Preferensi studi tidak valid',
|
||||
];
|
||||
|
||||
$validated = $request->validate($rules, $messages);
|
||||
|
||||
// --- 1. PREPROCESSING NILAI (Kriteria 1: Akademik) ---
|
||||
$scores = $request->only(['mtk', 'fisika', 'kimia', 'biologi', 'ekonomi', 'geografi', 'sosiologi', 'sejarah']);
|
||||
$validScores = array_filter($scores);
|
||||
|
|
@ -107,64 +174,157 @@ public function proses(Request $request)
|
|||
}
|
||||
|
||||
// --- 2. ANALISIS MINAT (Kriteria 2) ---
|
||||
$minatRaw = strtolower($request->minat ?? '');
|
||||
$minatInput = trim((string) ($validated['minat'] ?? ''));
|
||||
|
||||
// Validasi minat tidak hanya satu kata
|
||||
if (strlen($minatInput) < 3) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Minat harus diisi dengan minimal 3 karakter untuk analisis yang akurat',
|
||||
])->setStatusCode(422);
|
||||
}
|
||||
|
||||
$minatRaw = strtolower($minatInput);
|
||||
$minatMapped = $this->mapMinat($minatRaw);
|
||||
|
||||
// --- 3. ANALISIS CITA-CITA (Kriteria 3) ---
|
||||
$citaRaw = strtolower($request->cita_cita ?? '');
|
||||
// Log untuk audit trail
|
||||
\Log::debug('Minat Analysis', [
|
||||
'input' => $minatInput,
|
||||
'normalized' => $minatRaw,
|
||||
'mapped' => $minatMapped,
|
||||
]);
|
||||
|
||||
// --- 3. PREFERENSI STUDI LANJUTAN (Kriteria 3) ---
|
||||
$prefStudi = $validated['pref_studi'];
|
||||
|
||||
// --- 4. ANALISIS CITA-CITA (Kriteria 4) ---
|
||||
$citaInput = trim((string) ($validated['cita_cita'] ?? ''));
|
||||
|
||||
// Validasi cita-cita tidak hanya satu kata
|
||||
if (strlen($citaInput) < 3) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Cita-cita harus diisi dengan minimal 3 karakter untuk analisis yang akurat',
|
||||
])->setStatusCode(422);
|
||||
}
|
||||
|
||||
$citaRaw = strtolower($citaInput);
|
||||
$citaMapped = $this->mapCitaCita($citaRaw);
|
||||
|
||||
// --- 4. PEMETAAN PREFERENSI STUDI (Kriteria 4) ---
|
||||
$prefStudi = $request->pref_studi ?? 'Blended';
|
||||
$prefMapping = config('polije.pref_mapping', []);
|
||||
// Log untuk audit trail
|
||||
\Log::debug('Cita-cita Analysis', [
|
||||
'input' => $citaInput,
|
||||
'normalized' => $citaRaw,
|
||||
'mapped' => $citaMapped,
|
||||
]);
|
||||
|
||||
// --- 5. ANALISIS PRESTASI (Kriteria 5) ---
|
||||
$prestasiRaw = strtolower($request->prestasi ?? '');
|
||||
$prestasiScore = $this->scorePrestasiScore($prestasiRaw);
|
||||
$prestasiInput = trim((string) ($validated['prestasi'] ?? ''));
|
||||
$isPrestasiFilled = $prestasiInput !== '' && strlen($prestasiInput) >= 3;
|
||||
$prestasiRaw = strtolower($prestasiInput);
|
||||
$prestasiAnalysis = $this->analyzePrestasi($prestasiRaw);
|
||||
$prestasiScore = $prestasiAnalysis['score'];
|
||||
|
||||
// Log untuk audit trail
|
||||
\Log::debug('Prestasi Analysis', [
|
||||
'input' => $prestasiInput,
|
||||
'is_filled' => $isPrestasiFilled,
|
||||
'normalized' => $prestasiRaw,
|
||||
'level' => $prestasiAnalysis['level'] ?? 'not provided',
|
||||
'score' => $prestasiScore,
|
||||
]);
|
||||
|
||||
// --- 6. PERHITUNGAN NAIVE BAYES BERBOBOT ---
|
||||
$cfg = config('polije.criteria', []);
|
||||
$majorMap = PolijeMajor::all()->keyBy('nama_jurusan');
|
||||
$logPosteriors = [];
|
||||
$detailPerJurusan = [];
|
||||
$epsilon = 1e-9;
|
||||
|
||||
// Validate config exists
|
||||
if (empty($cfg)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Konfigurasi sistem rekomendasi tidak ditemukan',
|
||||
])->setStatusCode(500);
|
||||
}
|
||||
|
||||
foreach ($cfg as $jurusan => $c) {
|
||||
// Prior: uniform
|
||||
$prior = 1 / count($cfg);
|
||||
// Prior: uniform dengan safety check
|
||||
$cfgCount = max(1, count($cfg)); // Prevent division by zero
|
||||
$prior = 1 / $cfgCount;
|
||||
$logPrior = log(max($prior, $epsilon));
|
||||
|
||||
// Weights dan match probabilities
|
||||
$weights = $c['weights'] ?? ['nilai' => 0.40, 'minat' => 0.35, 'pref' => 0.15, 'prestasi' => 0.05, 'cita_cita' => 0.05];
|
||||
$matchProb = $c['match_prob'] ?? ['nilai' => 0.80, 'minat' => 0.90, 'pref' => 0.85, 'prestasi' => 0.65, 'cita_cita' => 0.85];
|
||||
// Use global ROC weights (override any per-jurusan editable weights)
|
||||
// ROC-based: nilai 15.6%, minat 45.6%, pref 25.6%, cita 9%, prestasi 4%
|
||||
$globalWeights = ['nilai' => 0.156, 'minat' => 0.456, 'pref' => 0.256, 'cita_cita' => 0.090, 'prestasi' => 0.040];
|
||||
$weights = $globalWeights;
|
||||
|
||||
// Jika prestasi kosong, atribut prestasi tidak dihitung dan lakukan normalisasi ulang pada atribut lain
|
||||
if (!$isPrestasiFilled) {
|
||||
$weights['prestasi'] = 0.0;
|
||||
$sumNonPrestasi = ($weights['nilai'] ?? 0) + ($weights['minat'] ?? 0) + ($weights['pref'] ?? 0) + ($weights['cita_cita'] ?? 0);
|
||||
|
||||
// Normalize weights dengan safety check
|
||||
if ($sumNonPrestasi > $epsilon) {
|
||||
$weights['nilai'] = ($weights['nilai'] ?? 0) / $sumNonPrestasi;
|
||||
$weights['minat'] = ($weights['minat'] ?? 0) / $sumNonPrestasi;
|
||||
$weights['pref'] = ($weights['pref'] ?? 0) / $sumNonPrestasi;
|
||||
$weights['cita_cita'] = ($weights['cita_cita'] ?? 0) / $sumNonPrestasi;
|
||||
} else {
|
||||
// Fallback ke global jika normalisasi gagal
|
||||
$weights = $globalWeights;
|
||||
}
|
||||
}
|
||||
|
||||
$matchProb = $c['match_prob'] ?? ['nilai' => 0.80, 'minat' => 0.90, 'prestasi' => 0.65, 'cita_cita' => 0.85];
|
||||
|
||||
// Ensure matchProb is array
|
||||
if (!is_array($matchProb)) {
|
||||
$matchProb = ['nilai' => 0.80, 'minat' => 0.90, 'prestasi' => 0.65, 'cita_cita' => 0.85];
|
||||
}
|
||||
|
||||
// 1. Likelihood untuk Nilai
|
||||
$p_nilai = ($katNilai == ($c['nilai'] ?? 'Sedang')) ? $matchProb['nilai'] : max(1 - $matchProb['nilai'], $epsilon);
|
||||
$p_nilai_category = ($katNilai == ($c['nilai'] ?? 'Sedang'))
|
||||
? ($matchProb['nilai'] ?? 0.80)
|
||||
: max(1 - ($matchProb['nilai'] ?? 0.80), $epsilon);
|
||||
|
||||
// Safe access to majorMap with null check
|
||||
$majorRecord = $majorMap[$jurusan] ?? null;
|
||||
$bobotMapel = $majorRecord ? $this->getBobotMapelForKelompok($majorRecord->bobot_mapel ?? [], $kelompokAsal) : [];
|
||||
|
||||
$p_nilai_subject = $this->scoreSubjectFitLikelihood(
|
||||
$bobotMapel,
|
||||
$scores,
|
||||
$p_nilai_category
|
||||
);
|
||||
$p_nilai = max(0.05, min(0.98, (0.6 * $p_nilai_category) + (0.4 * $p_nilai_subject)));
|
||||
|
||||
// 2. Likelihood untuk Minat
|
||||
$p_minat = ($minatMapped == ($c['minat'] ?? 'Umum')) ? $matchProb['minat'] : max(1 - $matchProb['minat'], $epsilon);
|
||||
$p_minat = $this->scoreMinatLikelihood(
|
||||
$minatRaw,
|
||||
$minatMapped,
|
||||
$c['minat'] ?? 'Umum',
|
||||
$matchProb['minat'] ?? 0.90
|
||||
);
|
||||
|
||||
// 3. Likelihood untuk Preferensi Studi
|
||||
$prefList = $c['pref'] ?? ['Praktik Langsung', 'DuDi', 'Project Based'];
|
||||
$prefList = $c['pref'] ?? ['Sains & Teknologi', 'Pertanian & Lingkungan', 'Kesehatan & Ilmu Hayat', 'Bisnis & Manajemen', 'Sosial & Humaniora'];
|
||||
if (!is_array($prefList)) {
|
||||
$prefList = [$prefList];
|
||||
}
|
||||
$p_pref = in_array($prefStudi, $prefList) ? $matchProb['pref'] : max(1 - $matchProb['pref'], $epsilon);
|
||||
$p_pref = in_array($prefStudi, $prefList) ? ($matchProb['pref'] ?? 0.85) : max(1 - ($matchProb['pref'] ?? 0.85), $epsilon);
|
||||
|
||||
// 4. Likelihood untuk Cita-cita
|
||||
$citaCitaKeywords = $c['cita_cita_keywords'] ?? [];
|
||||
$matchCitaCita = false;
|
||||
if (!empty($citaCitaKeywords)) {
|
||||
foreach ($citaCitaKeywords as $keyword) {
|
||||
if (stripos($citaMapped, $keyword) !== false) {
|
||||
$matchCitaCita = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$p_cita_cita = $matchCitaCita ? $matchProb['cita_cita'] : max(1 - $matchProb['cita_cita'], $epsilon);
|
||||
$p_cita_cita = $this->scoreKeywordLikelihood($citaMapped, $citaCitaKeywords, $matchProb['cita_cita'] ?? 0.85);
|
||||
|
||||
// 5. Likelihood untuk Prestasi (boost jika ada prestasi)
|
||||
$p_prestasi = ($prestasiScore > 0.5) ? $matchProb['prestasi'] : max(1 - $matchProb['prestasi'], $epsilon);
|
||||
// 5. Likelihood untuk Prestasi (bertingkat: tinggi/menengah/dasar/minimal)
|
||||
$p_prestasi = $this->scorePrestasiLikelihood(
|
||||
$prestasiAnalysis,
|
||||
$citaCitaKeywords,
|
||||
$matchProb['prestasi'] ?? 0.65
|
||||
);
|
||||
|
||||
// Simpan detail per kriteria untuk tampilan
|
||||
$detailPerJurusan[$jurusan] = [
|
||||
|
|
@ -172,7 +332,7 @@ public function proses(Request $request)
|
|||
'minat' => round($p_minat, 4),
|
||||
'pref' => round($p_pref, 4),
|
||||
'cita' => round($p_cita_cita, 4),
|
||||
'prestasi' => round($p_prestasi, 4),
|
||||
'prestasi' => $isPrestasiFilled ? round($p_prestasi, 4) : null,
|
||||
];
|
||||
|
||||
// Hitung log-likelihood dengan bobot
|
||||
|
|
@ -180,8 +340,11 @@ public function proses(Request $request)
|
|||
($weights['nilai'] ?? 0) * log(max($p_nilai, $epsilon)) +
|
||||
($weights['minat'] ?? 0) * log(max($p_minat, $epsilon)) +
|
||||
($weights['pref'] ?? 0) * log(max($p_pref, $epsilon)) +
|
||||
($weights['cita_cita'] ?? 0) * log(max($p_cita_cita, $epsilon)) +
|
||||
($weights['prestasi'] ?? 0) * log(max($p_prestasi, $epsilon));
|
||||
($weights['cita_cita'] ?? 0) * log(max($p_cita_cita, $epsilon));
|
||||
|
||||
if (($weights['prestasi'] ?? 0) > 0) {
|
||||
$logLikelihood += ($weights['prestasi'] ?? 0) * log(max($p_prestasi, $epsilon));
|
||||
}
|
||||
|
||||
$logPosteriors[$jurusan] = $logPrior + $logLikelihood;
|
||||
}
|
||||
|
|
@ -205,7 +368,8 @@ public function proses(Request $request)
|
|||
$katNilai,
|
||||
$minatMapped,
|
||||
$prefStudi,
|
||||
$prestasiRaw
|
||||
$prestasiRaw,
|
||||
$prestasiAnalysis
|
||||
);
|
||||
$hasilAkhir[] = [
|
||||
'jurusan' => $jurusan,
|
||||
|
|
@ -223,8 +387,9 @@ public function proses(Request $request)
|
|||
|
||||
// Simpan data rekomendasi ke database
|
||||
$user = Auth::user();
|
||||
$recommendationId = null;
|
||||
if ($user) {
|
||||
Recommendation::create([
|
||||
$recommendation = Recommendation::create([
|
||||
'user_id' => $user->id,
|
||||
'mtk' => $request->mtk ?? null,
|
||||
'fisika' => $request->fisika ?? null,
|
||||
|
|
@ -234,12 +399,14 @@ public function proses(Request $request)
|
|||
'geografi' => $request->geografi ?? null,
|
||||
'sosiologi' => $request->sosiologi ?? null,
|
||||
'sejarah' => $request->sejarah ?? null,
|
||||
'minat' => $request->minat ?? null,
|
||||
'preferensi_studi' => $request->pref_studi ?? null,
|
||||
'cita_cita' => $request->cita_cita ?? null,
|
||||
'prestasi' => $request->prestasi ?? null,
|
||||
'minat' => $minatInput,
|
||||
'preferensi_studi' => $prefStudi,
|
||||
'cita_cita' => $citaInput,
|
||||
// Kolom prestasi di DB bersifat NOT NULL, jadi simpan string kosong jika tidak diisi.
|
||||
'prestasi' => $prestasiInput,
|
||||
'hasil_rekomendasi' => $hasilAkhir,
|
||||
]);
|
||||
$recommendationId = $recommendation->id;
|
||||
}
|
||||
|
||||
// Simpan data rekomendasi ke session untuk chatbot
|
||||
|
|
@ -256,35 +423,127 @@ public function proses(Request $request)
|
|||
]);
|
||||
}
|
||||
|
||||
return view('rekomendasi.hasil', compact('hasilAkhir', 'katNilai', 'minatMapped', 'citaMapped', 'prefStudi', 'prestasiScore'));
|
||||
// Ambil data jurusan teratas dari database untuk deskripsi/prospek pada halaman hasil
|
||||
$topJurusan = null;
|
||||
if (count($hasilAkhir) > 0) {
|
||||
$topJurusan = PolijeMajor::where('nama_jurusan', $hasilAkhir[0]['jurusan'] ?? '')->first();
|
||||
}
|
||||
|
||||
return view('rekomendasi.hasil', compact('hasilAkhir', 'katNilai', 'average', 'minatRaw', 'minatMapped', 'citaRaw', 'citaMapped', 'prefStudi', 'prestasiScore', 'topJurusan', 'isPrestasiFilled', 'recommendationId'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pemetaan minat ke kategori yang dipahami sistem
|
||||
*/
|
||||
/**
|
||||
* Normalize text untuk better keyword matching
|
||||
* Menangani variasi kata seperti programmer/programming, coding/code
|
||||
*/
|
||||
private function normalizeText(string $text): string
|
||||
{
|
||||
$text = strtolower(trim($text));
|
||||
|
||||
// Simple stemming untuk common variations
|
||||
$replacements = [
|
||||
'programmer' => 'programming',
|
||||
'coder' => 'coding',
|
||||
'code' => 'coding',
|
||||
'codes' => 'coding',
|
||||
'develop' => 'development',
|
||||
'developer' => 'development',
|
||||
'develops' => 'development',
|
||||
'manager' => 'manajemen',
|
||||
'manages' => 'manajemen',
|
||||
'doctor' => 'dokter',
|
||||
'doctors' => 'dokter',
|
||||
'nurse' => 'perawat',
|
||||
'nurses' => 'perawat',
|
||||
'engineer' => 'teknik',
|
||||
'engineers' => 'teknik',
|
||||
'farming' => 'pertanian',
|
||||
'farmer' => 'pertanian',
|
||||
'farmers' => 'pertanian',
|
||||
'business' => 'bisnis',
|
||||
'businessmen' => 'bisnis',
|
||||
];
|
||||
|
||||
foreach ($replacements as $from => $to) {
|
||||
$text = str_replace($from, $to, $text);
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
private function mapMinat(string $minatRaw): string
|
||||
{
|
||||
if (preg_match('/(coding|komputer|laptop|web|aplikasi|logika|programming|software|development)/', $minatRaw)) {
|
||||
return 'Logika & Komputer';
|
||||
} elseif (preg_match('/(tanam|kebun|sawah|hewan|ternak|alam|pertanian|agri)/', $minatRaw)) {
|
||||
return 'Alam & Tanaman';
|
||||
} elseif (preg_match('/(obat|sakit|rawat|medis|gizi|sehat|kesehatan|perawat|dokter)/', $minatRaw)) {
|
||||
return 'Pelayanan & Kesehatan';
|
||||
} elseif (preg_match('/(bisnis|uang|jual|kantor|hitung|ekonomi|dagang|usaha|entrepreneur)/', $minatRaw)) {
|
||||
return 'Manajemen & Bisnis';
|
||||
} elseif (preg_match('/(mesin|bengkel|listrik|las|robot|motor|teknik|otomasi|elektronik)/', $minatRaw)) {
|
||||
return 'Mesin & Listrik';
|
||||
// Normalize text untuk better matching
|
||||
$minatNormalized = $this->normalizeText($minatRaw);
|
||||
|
||||
// Use coverage-based scoring untuk handle ambiguous inputs
|
||||
$categoryKeywords = [
|
||||
'Logika & Komputer' => ['coding', 'komputer', 'laptop', 'web', 'aplikasi', 'logika', 'programming', 'software', 'development', 'developer', 'it', 'data', 'ai', 'teknologi', 'sistem', 'cloud', 'database', 'network', 'cybersecurity', 'analyst', 'scientist', 'algorithm', 'machine learning', 'app', 'digital'],
|
||||
'Alam & Tanaman' => ['tanam', 'kebun', 'sawah', 'hewan', 'ternak', 'alam', 'pertanian', 'agri', 'panen', 'tani', 'hortikultura', 'lingkungan', 'berkelanjutan', 'farm', 'farming', 'plantation', 'crops', 'conservation', 'breeding', 'agribusiness', 'agroforestry', 'horticulture', 'cultivate', 'harvest', 'livestock management', 'animal husbandry', 'sustainable agriculture', 'crop science', 'soil', 'botanical'],
|
||||
'Pelayanan & Kesehatan' => ['obat', 'sakit', 'rawat', 'medis', 'gizi', 'sehat', 'kesehatan', 'perawat', 'dokter', 'rumah sakit', 'klinik', 'farmasi', 'keperawatan', 'terapis', 'nursing', 'therapy', 'wellness', 'nutrition', 'healing', 'caring', 'clinical', 'patient care', 'rehabilitation', 'surgery', 'diagnostic', 'laboratory', 'medical technician', 'health educator', 'public health', 'epidemiology', 'preventive care'],
|
||||
'Manajemen & Bisnis' => ['bisnis', 'uang', 'jual', 'kantor', 'hitung', 'ekonomi', 'dagang', 'usaha', 'entrepreneur', 'manager', 'marketing', 'akuntan', 'finance', 'keuangan', 'sales', 'trading', 'commerce', 'leadership', 'startup', 'corporate', 'organization', 'administration', 'strategic planning', 'operations', 'budget', 'investment', 'capital', 'supply chain', 'logistics', 'human resources'],
|
||||
'Mesin & Listrik' => ['mesin', 'bengkel', 'listrik', 'las', 'robot', 'motor', 'teknik', 'otomasi', 'elektronik', 'maintenance', 'industri', 'manufaktur', 'mechanical', 'electrical', 'automation', 'construction', 'repair', 'welding', 'hydraulic', 'pneumatic', 'power generation', 'circuit', 'transformer', 'machinery operation', 'fabrication', 'installation', 'troubleshooting'],
|
||||
];
|
||||
|
||||
// Score setiap kategori berdasarkan keyword coverage
|
||||
$scores = [];
|
||||
foreach ($categoryKeywords as $category => $keywords) {
|
||||
$scores[$category] = $this->keywordCoverage($minatNormalized, $keywords);
|
||||
}
|
||||
return 'Umum';
|
||||
|
||||
// Return kategori dengan coverage tertinggi
|
||||
$bestCategory = 'Umum';
|
||||
$maxScore = 0;
|
||||
foreach ($scores as $category => $score) {
|
||||
if ($score > $maxScore) {
|
||||
$maxScore = $score;
|
||||
$bestCategory = $category;
|
||||
}
|
||||
}
|
||||
|
||||
// Jika tidak ada keyword match, return Umum
|
||||
return $maxScore > 0 ? $bestCategory : 'Umum';
|
||||
}
|
||||
|
||||
/**
|
||||
* Pemetaan cita-cita ke kategori jurusan
|
||||
* Pemetaan cita-cita ke kategori jurusan yang relevan
|
||||
* Mengevaluasi input cita-cita dengan lebih detail
|
||||
*/
|
||||
private function mapCitaCita(string $citaRaw): string
|
||||
{
|
||||
// Return raw mapped text untuk matching dengan keywords
|
||||
return $citaRaw;
|
||||
// Normalize text untuk better matching
|
||||
$citaNormalized = $this->normalizeText($citaRaw);
|
||||
|
||||
// Map cita-cita ke category berdasarkan keywords
|
||||
$careerCategories = [
|
||||
'IT & Software' => ['programmer', 'developer', 'software', 'coding', 'web', 'database', 'it', 'scientist', 'analyst', 'data', 'cloud', 'architect', 'cybersecurity', 'security', 'devops', 'backend', 'frontend', 'fullstack', 'sysadmin', 'network admin', 'cto', 'tech lead', 'ai', 'machine learning'],
|
||||
'Agriculture' => ['petani', 'pertanian', 'agribisnis', 'kebun', 'ternak', 'peternak', 'agronomi', 'farming', 'livestock', 'agronomist', 'farmer', 'farm manager', 'plantation', 'crops specialist', 'agritech', 'horticultural', 'agricultural scientist', 'soil scientist', 'breeding specialist', 'extension officer', 'crop consultant', 'forestry', 'fishery manager'],
|
||||
'Healthcare' => ['dokter', 'perawat', 'medis', 'gizi', 'terapis', 'farmasi', 'kesehatan', 'nursing', 'therapist', 'pharmacist', 'nutritionist', 'clinician', 'public health', 'midwife', 'radiologist', 'dentist', 'nurse', 'surgeon', 'diagnostician', 'laboratory technician', 'paramedic', 'health educator', 'epidemiologist', 'wellness coach'],
|
||||
'Business' => ['entrepreneur', 'manager', 'marketing', 'sales', 'akuntan', 'keuangan', 'bisnis', 'accountant', 'consultant', 'finance', 'cfo', 'ceo', 'director', 'treasurer', 'auditor', 'trader', 'investor', 'controller', 'operations manager', 'strategic planner', 'business analyst', 'supply chain manager', 'hr manager', 'corporate executive'],
|
||||
'Engineering' => ['teknik', 'engineer', 'mesin', 'listrik', 'bengkel', 'maintenance', 'industri', 'technician', 'constructor', 'mechanical engineer', 'electrical engineer', 'automation', 'supervisor', 'foreman', 'technologist', 'specialist', 'civil engineer', 'welding specialist', 'hydraulics engineer', 'power engineer', 'manufacturing engineer', 'maintenance supervisor'],
|
||||
'Communication' => ['jurnalis', 'komunikator', 'presenter', 'content', 'pariwisata', 'hospitality', 'tour', 'guide', 'public relations', 'ambassador', 'interpreter', 'diplomat', 'broadcaster', 'event organizer', 'marketing specialist', 'pr specialist', 'copywriter', 'social media manager', 'travel consultant', 'hospitality manager', 'cultural ambassador', 'media producer'],
|
||||
];
|
||||
|
||||
// Score setiap kategori
|
||||
$scores = [];
|
||||
foreach ($careerCategories as $category => $keywords) {
|
||||
$scores[$category] = $this->keywordCoverage($citaNormalized, $keywords);
|
||||
}
|
||||
|
||||
// Return kategori dengan coverage tertinggi
|
||||
$bestCategory = 'Umum';
|
||||
$maxScore = 0;
|
||||
foreach ($scores as $category => $score) {
|
||||
if ($score > $maxScore) {
|
||||
$maxScore = $score;
|
||||
$bestCategory = $category;
|
||||
}
|
||||
}
|
||||
|
||||
return $maxScore > 0 ? $bestCategory : 'Umum';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -313,6 +572,196 @@ private function scorePrestasiScore(string $prestasiRaw): float
|
|||
return $prestasiScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ringkas level prestasi dari input teks agar lebih transparan.
|
||||
*/
|
||||
private function analyzePrestasi(string $prestasiRaw): array
|
||||
{
|
||||
if (empty(trim($prestasiRaw))) {
|
||||
return [
|
||||
'provided' => false,
|
||||
'level' => 'minimal',
|
||||
'score' => 0.0,
|
||||
'raw' => '',
|
||||
];
|
||||
}
|
||||
|
||||
$text = strtolower(trim($prestasiRaw));
|
||||
$level = 'minimal';
|
||||
|
||||
if (preg_match('/(juara|menang|champion|first|gold|emas|terbaik)/', $text)) {
|
||||
$level = 'tinggi';
|
||||
} elseif (preg_match('/(finalis|semifinal|peringkat|ranking|podium|medali|silver|perak)/', $text)) {
|
||||
$level = 'sedang';
|
||||
} elseif (preg_match('/(sertifikat|training|kursus|workshop|peserta|mengikuti)/', $text)) {
|
||||
$level = 'cukup';
|
||||
}
|
||||
|
||||
return [
|
||||
'provided' => true,
|
||||
'level' => $level,
|
||||
'score' => $this->scorePrestasiScore($text),
|
||||
'raw' => $text,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Skor atribut teks berdasarkan coverage keyword (0..1) lalu dipetakan menjadi likelihood.
|
||||
* Digunakan untuk cita-cita dan prestasi scoring.
|
||||
*/
|
||||
private function scoreKeywordLikelihood(string $text, array $keywords, float $matchProb): float
|
||||
{
|
||||
if (empty($keywords)) {
|
||||
return 0.50;
|
||||
}
|
||||
|
||||
$coverage = $this->keywordCoverage($text, $keywords);
|
||||
|
||||
// Log untuk debugging
|
||||
if ($coverage > 0) {
|
||||
\Log::debug('Keyword Coverage', [
|
||||
'text' => $text,
|
||||
'keywords_count' => count($keywords),
|
||||
'coverage' => $coverage,
|
||||
'match_prob' => $matchProb,
|
||||
]);
|
||||
}
|
||||
|
||||
// Base 0.2 agar tidak 0 total, lalu naik proporsional coverage.
|
||||
$likelihood = 0.20 + ($coverage * ($matchProb - 0.20));
|
||||
|
||||
return max(0.05, min(0.98, $likelihood));
|
||||
}
|
||||
|
||||
private function scoreMinatLikelihood(string $minatRaw, string $minatMapped, string $targetMinat, float $matchProb): float
|
||||
{
|
||||
// Expanded keyword bank dengan lebih banyak variasi
|
||||
$keywordBank = [
|
||||
'Logika & Komputer' => ['coding', 'programming', 'komputer', 'software', 'web', 'data', 'ai', 'digital', 'aplikasi', 'developer', 'coding', 'programer', 'it', 'database', 'network'],
|
||||
'Alam & Tanaman' => ['pertanian', 'tanaman', 'kebun', 'sawah', 'alam', 'peternakan', 'agribisnis', 'hewan', 'ternak', 'panen', 'tani', 'petani', 'hortikultura'],
|
||||
'Pelayanan & Kesehatan' => ['kesehatan', 'medis', 'gizi', 'perawat', 'dokter', 'klinik', 'rumah sakit', 'farmasi', 'terapis', 'kesehatan masyarakat', 'kesehatan', 'sehat', 'rawat'],
|
||||
'Manajemen & Bisnis' => ['bisnis', 'usaha', 'marketing', 'keuangan', 'manajemen', 'akuntansi', 'entrepreneur', 'sales', 'marketing', 'penjualan', 'perbankan', 'akuntan'],
|
||||
'Mesin & Listrik' => ['mesin', 'listrik', 'teknik', 'otomasi', 'elektronik', 'mekanik', 'industri', 'bengkel', 'las', 'motor', 'teknis'],
|
||||
];
|
||||
|
||||
$targetKeywords = $keywordBank[$targetMinat] ?? [];
|
||||
$coverage = $this->keywordCoverage($minatRaw, $targetKeywords);
|
||||
|
||||
// Perfect match jika mapped minat sama dengan target
|
||||
$categoryMatch = ($minatMapped === $targetMinat) ? 1.0 : 0.0;
|
||||
|
||||
// Weighted combination: kategori match lebih penting (60%) daripada coverage (40%)
|
||||
$combined = (0.6 * $categoryMatch) + (0.4 * $coverage);
|
||||
$likelihood = 0.20 + ($combined * ($matchProb - 0.20));
|
||||
|
||||
return max(0.05, min(0.98, $likelihood));
|
||||
}
|
||||
|
||||
private function scorePrestasiLikelihood(array $prestasiAnalysis, array $jurusanKeywords, float $matchProb): float
|
||||
{
|
||||
$baseScore = $prestasiAnalysis['score'] ?? 0.0; // 0..1
|
||||
|
||||
if ($baseScore <= 0.0) {
|
||||
return 0.20;
|
||||
}
|
||||
|
||||
// Relevansi prestasi terhadap konteks jurusan (dari teks prestasi vs keyword jurusan)
|
||||
$relevance = 0.0;
|
||||
if (!empty($jurusanKeywords)) {
|
||||
$relevance = $this->keywordCoverage($prestasiAnalysis['level'] . ' ' . ($prestasiAnalysis['raw'] ?? ''), $jurusanKeywords);
|
||||
}
|
||||
|
||||
// Prestasi level dominan, relevance sebagai penguat.
|
||||
$combined = (0.75 * $baseScore) + (0.25 * $relevance);
|
||||
$likelihood = 0.20 + ($combined * ($matchProb - 0.20));
|
||||
|
||||
return max(0.05, min(0.98, $likelihood));
|
||||
}
|
||||
|
||||
private function keywordCoverage(string $text, array $keywords): float
|
||||
{
|
||||
$text = strtolower(trim($text));
|
||||
if ($text === '' || empty($keywords)) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$matched = 0;
|
||||
foreach (array_unique($keywords) as $keyword) {
|
||||
if ($keyword !== '' && str_contains($text, strtolower($keyword))) {
|
||||
$matched++;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalisasi agar tidak terlalu menghukum list keyword yang panjang.
|
||||
$denominator = max(1, min(count(array_unique($keywords)), 6));
|
||||
|
||||
return min(1.0, $matched / $denominator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hitung kecocokan nilai mapel terhadap bobot mapel jurusan (0..1) lalu ubah ke likelihood.
|
||||
* Ini tetap bagian dari atribut "Nilai Akademik" agar tidak keluar dari 5 atribut.
|
||||
*/
|
||||
private function scoreSubjectFitLikelihood(array $bobotMapel, array $scores, float $fallback): float
|
||||
{
|
||||
if (empty($bobotMapel)) {
|
||||
return max(0.05, min(0.98, $fallback));
|
||||
}
|
||||
|
||||
$weighted = 0.0;
|
||||
$weightSum = 0.0;
|
||||
|
||||
foreach ($bobotMapel as $mapel => $w) {
|
||||
$nilai = $scores[$mapel] ?? null;
|
||||
if ($nilai === null || $nilai === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$num = (float) $nilai;
|
||||
$normalized = max(0.0, min(1.0, $num / 100));
|
||||
$weighted += $normalized * (float) $w;
|
||||
$weightSum += (float) $w;
|
||||
}
|
||||
|
||||
if ($weightSum <= 0) {
|
||||
return max(0.05, min(0.98, $fallback));
|
||||
}
|
||||
|
||||
$fitScore = $weighted / $weightSum; // 0..1
|
||||
|
||||
// Map ke likelihood agar tidak terlalu ekstrem.
|
||||
return max(0.05, min(0.98, 0.25 + (0.70 * $fitScore)));
|
||||
}
|
||||
|
||||
private function getBobotMapelForKelompok(array $bobotMapel, string $kelompokAsal): array
|
||||
{
|
||||
$kelompokKey = strtoupper($kelompokAsal) === 'IPS' ? 'ips' : 'ipa';
|
||||
$subjects = $kelompokKey === 'ipa'
|
||||
? ['mtk', 'fisika', 'kimia', 'biologi']
|
||||
: ['ekonomi', 'geografi', 'sosiologi', 'sejarah'];
|
||||
|
||||
if (isset($bobotMapel['ipa']) || isset($bobotMapel['ips'])) {
|
||||
$groupValues = $bobotMapel[$kelompokKey] ?? [];
|
||||
return $this->normalizeBobotGroup(is_array($groupValues) ? $groupValues : [], $subjects);
|
||||
}
|
||||
|
||||
$legacyValues = array_intersect_key($bobotMapel, array_flip($subjects));
|
||||
|
||||
return $this->normalizeBobotGroup($legacyValues, $subjects);
|
||||
}
|
||||
|
||||
private function normalizeBobotGroup(array $values, array $subjects): array
|
||||
{
|
||||
$normalized = [];
|
||||
|
||||
foreach ($subjects as $subject) {
|
||||
$value = $values[$subject] ?? null;
|
||||
$normalized[$subject] = is_numeric($value) ? (float) $value : 0.0;
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tampilkan history rekomendasi
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Requests\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
|
@ -54,17 +55,39 @@ public function authenticate(): void
|
|||
|
||||
/**
|
||||
* Ensure the login request is not rate limited.
|
||||
* Special handling: Students get 3 attempts, others get 5 attempts.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function ensureIsNotRateLimited(): void
|
||||
{
|
||||
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||
$email = $this->string('email');
|
||||
$user = User::where('email', $email)->first();
|
||||
|
||||
// Tentukan limit berdasarkan role (khusus siswa: 3x, lainnya: 5x)
|
||||
$maxAttempts = 5; // Default untuk BK, Admin, dan user lainnya
|
||||
$isStudent = false;
|
||||
|
||||
if ($user && $user->role === 'siswa') {
|
||||
$maxAttempts = 3; // Siswa hanya boleh 3x
|
||||
$isStudent = true;
|
||||
}
|
||||
|
||||
if (! RateLimiter::tooManyAttempts($this->throttleKey(), $maxAttempts)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event(new Lockout($this));
|
||||
|
||||
// Special message untuk siswa yang sudah 3x gagal
|
||||
if ($isStudent) {
|
||||
throw ValidationException::withMessages([
|
||||
'email' => '❌ Anda sudah salah password 3 kali. Silakan reset password melalui "Lupa Password" untuk keamanan akun Anda.',
|
||||
'forgot_password' => true, // Flag khusus untuk redirect
|
||||
'email_value' => $email, // Kirim email untuk auto-fill
|
||||
]);
|
||||
}
|
||||
|
||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class ProfileUpdateRequest extends FormRequest
|
|||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'name' => ['required', 'string', 'min:3', 'max:255'],
|
||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', Rule::unique(User::class)->ignore($this->user()->id)],
|
||||
'nis' => ['nullable', 'string', 'max:20'],
|
||||
'kelompok_asal' => ['nullable', 'string', 'in:IPA,IPS'],
|
||||
|
|
|
|||
|
|
@ -15,63 +15,17 @@ class Alumni extends Model
|
|||
'nama_alumni',
|
||||
'nis',
|
||||
'kelompok_asal',
|
||||
'mtk',
|
||||
'fisika',
|
||||
'kimia',
|
||||
'biologi',
|
||||
'ekonomi',
|
||||
'geografi',
|
||||
'sosiologi',
|
||||
'sejarah',
|
||||
'nilai_rata_rata',
|
||||
'minat',
|
||||
'cita_cita',
|
||||
'preferensi_studi',
|
||||
'prestasi',
|
||||
'major_masuk',
|
||||
'ranking_saat_rekomendasi',
|
||||
'success_status',
|
||||
'tahun_lulus_polije',
|
||||
'catatan',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'mtk' => 'float',
|
||||
'fisika' => 'float',
|
||||
'kimia' => 'float',
|
||||
'biologi' => 'float',
|
||||
'ekonomi' => 'float',
|
||||
'geografi' => 'float',
|
||||
'sosiologi' => 'float',
|
||||
'sejarah' => 'float',
|
||||
'nilai_rata_rata' => 'float',
|
||||
'ipk_lulus' => 'float',
|
||||
'predicted_score' => 'float',
|
||||
];
|
||||
|
||||
/**
|
||||
* Hitung nilai rata-rata otomatis
|
||||
*/
|
||||
public static function booted()
|
||||
{
|
||||
static::saving(function ($alumni) {
|
||||
// Gather nilai based on kelompok_asal
|
||||
$nilaiFields = ['mtk'];
|
||||
|
||||
if ($alumni->kelompok_asal == 'IPA') {
|
||||
$nilaiFields = ['mtk', 'fisika', 'kimia', 'biologi'];
|
||||
} else {
|
||||
$nilaiFields = ['mtk', 'ekonomi', 'geografi', 'sosiologi', 'sejarah'];
|
||||
}
|
||||
|
||||
$nilaiValues = [];
|
||||
foreach ($nilaiFields as $field) {
|
||||
if (!is_null($alumni->$field)) {
|
||||
$nilaiValues[] = $alumni->$field;
|
||||
}
|
||||
}
|
||||
|
||||
$alumni->nilai_rata_rata = count($nilaiValues) > 0
|
||||
? round(array_sum($nilaiValues) / count($nilaiValues), 2)
|
||||
: null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,16 +9,56 @@ class ChatHistory extends Model
|
|||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'chat_histories';
|
||||
protected $table = 'riwayat_chat';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'session_id',
|
||||
'recommendation_id',
|
||||
'prompt',
|
||||
'response',
|
||||
'id_sesi',
|
||||
'id_rekomendasi',
|
||||
'pertanyaan',
|
||||
'jawaban',
|
||||
];
|
||||
|
||||
public function getSessionIdAttribute()
|
||||
{
|
||||
return $this->attributes['id_sesi'] ?? null;
|
||||
}
|
||||
|
||||
public function setSessionIdAttribute($value): void
|
||||
{
|
||||
$this->attributes['id_sesi'] = $value;
|
||||
}
|
||||
|
||||
public function getPromptAttribute()
|
||||
{
|
||||
return $this->attributes['pertanyaan'] ?? null;
|
||||
}
|
||||
|
||||
public function setPromptAttribute($value): void
|
||||
{
|
||||
$this->attributes['pertanyaan'] = $value;
|
||||
}
|
||||
|
||||
public function getResponseAttribute()
|
||||
{
|
||||
return $this->attributes['jawaban'] ?? null;
|
||||
}
|
||||
|
||||
public function setResponseAttribute($value): void
|
||||
{
|
||||
$this->attributes['jawaban'] = $value;
|
||||
}
|
||||
|
||||
public function getRecommendationIdAttribute()
|
||||
{
|
||||
return $this->attributes['id_rekomendasi'] ?? null;
|
||||
}
|
||||
|
||||
public function setRecommendationIdAttribute($value): void
|
||||
{
|
||||
$this->attributes['id_rekomendasi'] = $value;
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
|
|
@ -26,6 +66,6 @@ public function user()
|
|||
|
||||
public function recommendation()
|
||||
{
|
||||
return $this->belongsTo(Recommendation::class);
|
||||
return $this->belongsTo(Recommendation::class, 'id_rekomendasi');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ class PolijeMajor extends Model
|
|||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'jurusan_polije';
|
||||
|
||||
protected $fillable = [
|
||||
'nama_jurusan',
|
||||
'deskripsi',
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ class Recommendation extends Model
|
|||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'rekomendasi';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'mtk',
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use App\Notifications\ResetPasswordNotification;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
|
@ -61,4 +62,12 @@ public function chatHistories()
|
|||
{
|
||||
return $this->hasMany(ChatHistory::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the password reset notification.
|
||||
*/
|
||||
public function sendPasswordResetNotification($token)
|
||||
{
|
||||
$this->notify(new ResetPasswordNotification($token));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class ResetPasswordNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* The password reset token.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $token;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct($token)
|
||||
{
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*/
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$resetUrl = url(route('password.reset', [
|
||||
'token' => $this->token,
|
||||
'email' => $notifiable->getEmailForPasswordReset(),
|
||||
], false));
|
||||
|
||||
// generate 6-digit numeric code and store it for inline token flow
|
||||
$code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
\Illuminate\Support\Facades\DB::table('password_reset_codes')->updateOrInsert(
|
||||
['email' => $notifiable->getEmailForPasswordReset()],
|
||||
['code' => $code, 'created_at' => now()]
|
||||
);
|
||||
|
||||
return (new MailMessage)
|
||||
->subject('🔑 Reset Password - Sistem Pemilihan Jurusan Polije')
|
||||
->view('emails.reset-password', [
|
||||
'user' => $notifiable,
|
||||
'resetUrl' => $resetUrl,
|
||||
'code' => $code,
|
||||
'token' => $this->token,
|
||||
'expiresIn' => config('auth.passwords.users.expire', 60),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,8 @@
|
|||
class GeminiService
|
||||
{
|
||||
protected $apiKey;
|
||||
protected $backendUrl;
|
||||
protected $backendToken;
|
||||
protected $baseUrl = 'https://generativelanguage.googleapis.com/v1beta/models/';
|
||||
|
||||
// Model priority list - try each if previous fails
|
||||
|
|
@ -21,6 +23,8 @@ class GeminiService
|
|||
public function __construct()
|
||||
{
|
||||
$this->apiKey = config('services.gemini.api_key');
|
||||
$this->backendUrl = rtrim((string) config('services.gemini.backend_url', ''), '/');
|
||||
$this->backendToken = (string) config('services.gemini.backend_token', '');
|
||||
}
|
||||
|
||||
public function chat($message, $context = [], $chatHistory = [])
|
||||
|
|
@ -29,10 +33,21 @@ public function chat($message, $context = [], $chatHistory = [])
|
|||
if (empty($this->apiKey)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'API Key tidak tersedia. Silakan konfigurasi GEMINI_API_KEY di .env'
|
||||
'message' => 'Layanan chatbot belum siap digunakan. Silakan hubungi pengelola sistem.'
|
||||
];
|
||||
}
|
||||
|
||||
// Intent router: mode perbandingan jurusan ditangani terstruktur agar konsisten.
|
||||
if (($context['intent'] ?? '') === 'compare_majors') {
|
||||
$comparison = $this->buildStructuredComparisonResponse($message, $context);
|
||||
if (!empty($comparison)) {
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => $comparison,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$systemPrompt = $this->buildSystemPrompt($context);
|
||||
|
||||
// Build multi-turn conversation for Gemini
|
||||
|
|
@ -68,41 +83,27 @@ public function chat($message, $context = [], $chatHistory = [])
|
|||
]
|
||||
];
|
||||
|
||||
// Try each model until one works
|
||||
foreach ($this->models as $model) {
|
||||
$url = $this->baseUrl . $model . ':generateContent?key=' . $this->apiKey;
|
||||
|
||||
Log::info('Trying Gemini model', ['model' => $model]);
|
||||
|
||||
$response = Http::timeout(30)
|
||||
->withHeaders(['Content-Type' => 'application/json'])
|
||||
->post($url, $payload);
|
||||
|
||||
if ($response->successful()) {
|
||||
$data = $response->json();
|
||||
|
||||
if (isset($data['candidates'][0]['content']['parts'][0]['text'])) {
|
||||
Log::info('Gemini API success', ['model' => $model]);
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => $data['candidates'][0]['content']['parts'][0]['text']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// If 429 (rate limit) or 404 (model not found), try next model
|
||||
$status = $response->status();
|
||||
Log::warning("Gemini model {$model} failed", ['status' => $status]);
|
||||
|
||||
if ($status === 429) {
|
||||
// Wait briefly before trying next model
|
||||
sleep(1);
|
||||
}
|
||||
// Mode Python-only: chatbot wajib melalui backend Python.
|
||||
if (empty($this->backendUrl)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Layanan chatbot belum siap digunakan saat ini. Silakan coba kembali beberapa saat lagi.',
|
||||
];
|
||||
}
|
||||
|
||||
// All models failed
|
||||
Log::error('All Gemini models failed, using fallback');
|
||||
return $this->getFallbackResponse($message, $context);
|
||||
$proxyResponse = $this->sendViaPythonBackend($payload);
|
||||
if (($proxyResponse['success'] ?? false) === true) {
|
||||
return $proxyResponse;
|
||||
}
|
||||
|
||||
Log::error('Python Gemini backend failed in Python-only mode', [
|
||||
'error' => $proxyResponse['message'] ?? 'unknown',
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $proxyResponse['message'] ?? 'Maaf, layanan chatbot sedang mengalami gangguan. Silakan coba lagi.',
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Gemini Service Exception', [
|
||||
|
|
@ -111,18 +112,130 @@ public function chat($message, $context = [], $chatHistory = [])
|
|||
'line' => $e->getLine()
|
||||
]);
|
||||
|
||||
return $this->getFallbackResponse($message, $context);
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Maaf, layanan chatbot sedang mengalami kendala. Silakan coba kembali beberapa saat lagi.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
protected function getFallbackResponse($message, $context = [])
|
||||
protected function sendViaPythonBackend(array $payload): array
|
||||
{
|
||||
try {
|
||||
$url = $this->backendUrl . '/api/chat';
|
||||
$request = Http::timeout(35)->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
]);
|
||||
|
||||
if (!empty($this->backendToken)) {
|
||||
$request = $request->withToken($this->backendToken);
|
||||
}
|
||||
|
||||
$response = $request->post($url, [
|
||||
'payload' => $payload,
|
||||
'models' => $this->models,
|
||||
]);
|
||||
|
||||
if (!$response->successful()) {
|
||||
$status = $response->status();
|
||||
|
||||
$mappedMessage = match ($status) {
|
||||
401, 403 => 'Maaf, layanan chatbot sedang dibatasi sementara. Silakan coba kembali nanti.',
|
||||
422 => 'Pesan belum dapat diproses. Silakan perjelas pertanyaan Anda dan coba lagi.',
|
||||
429 => 'Layanan chatbot sedang ramai digunakan. Silakan tunggu sebentar lalu coba lagi.',
|
||||
500, 502, 503, 504 => 'Maaf, layanan chatbot sedang mengalami gangguan. Silakan coba kembali beberapa saat lagi.',
|
||||
default => 'Layanan chatbot sedang tidak tersedia. Silakan coba kembali nanti.',
|
||||
};
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $mappedMessage,
|
||||
];
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
if (($data['success'] ?? false) === true && !empty($data['message'])) {
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => $data['message'],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $data['message'] ?? 'Maaf, layanan chatbot belum dapat memberikan jawaban saat ini.',
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Python backend exception', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Koneksi layanan chatbot sedang bermasalah. Silakan coba kembali beberapa saat lagi.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
protected function getFallbackResponse($message, $context = [], $chatHistory = [])
|
||||
{
|
||||
$jurusan = $context['recommendation'] ?? null;
|
||||
$score = isset($context['score']) ? floatval($context['score']) : 0;
|
||||
$hasRecommendation = !empty($jurusan);
|
||||
|
||||
if (($context['intent'] ?? '') === 'compare_majors') {
|
||||
$comparison = $this->buildStructuredComparisonResponse($message, $context);
|
||||
if (!empty($comparison)) {
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => $comparison,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$major = null;
|
||||
if ($hasRecommendation) {
|
||||
$major = PolijeMajor::where('nama_jurusan', $jurusan)->first();
|
||||
}
|
||||
$majorDesc = $major->deskripsi ?? null;
|
||||
$majorProspek = $major->prospek_kerja ?? null;
|
||||
|
||||
// Keyword-based responses
|
||||
$messageLower = strtolower($message);
|
||||
$lastAiMessage = $this->getLastAssistantMessage($chatHistory);
|
||||
|
||||
// Tangani pertanyaan lanjutan agar tetap nyambung saat fallback aktif
|
||||
if ($this->isFollowUpMessage($messageLower)) {
|
||||
if (
|
||||
strpos($messageLower, 'jelaskan semua') !== false ||
|
||||
strpos($messageLower, 'semua jurusan') !== false ||
|
||||
strpos($messageLower, 'satu satu') !== false ||
|
||||
strpos($messageLower, 'satu-satu') !== false
|
||||
) {
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => $this->buildAllMajorsResponse($hasRecommendation, $jurusan, $score),
|
||||
];
|
||||
}
|
||||
|
||||
if ($hasRecommendation) {
|
||||
$parts = [];
|
||||
$parts[] = "Menindaklanjuti pembahasan sebelumnya, fokus utama Anda saat ini tetap pada jurusan \"{$jurusan}\" dengan skor kesesuaian {$score}%.";
|
||||
if (!empty($majorDesc)) {
|
||||
$parts[] = "Fokus pembelajaran jurusan: {$majorDesc}";
|
||||
}
|
||||
if (!empty($majorProspek)) {
|
||||
$parts[] = "Prospek kerja yang relevan: {$majorProspek}";
|
||||
}
|
||||
$parts[] = "Jika Anda berkenan, saya dapat lanjutkan secara bertahap: (1) kompetensi yang harus dipersiapkan, (2) mata kuliah inti, dan (3) perbandingan dengan alternatif jurusan lain.";
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => implode(' ', $parts),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (strpos($messageLower, 'halo') !== false || strpos($messageLower, 'hai') !== false || strpos($messageLower, 'hallo') !== false || strpos($messageLower, 'hi') !== false) {
|
||||
$hour = (int) now()->format('H');
|
||||
|
|
@ -148,7 +261,7 @@ protected function getFallbackResponse($message, $context = [])
|
|||
if ($hasRecommendation) {
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Jurusan \"{$jurusan}\" direkomendasikan berdasarkan analisis komprehensif terhadap profil akademik, minat, serta preferensi studi Anda. Skor kesesuaian sebesar {$score}% menunjukkan tingkat kecocokan yang signifikan antara profil Anda dengan karakteristik jurusan tersebut. Sistem menghitung skor ini berdasarkan lima faktor utama, yaitu: nilai akademik, minat dan bakat, preferensi studi lanjutan, prestasi, dan cita-cita."
|
||||
'message' => "Jurusan \"{$jurusan}\" direkomendasikan karena paling sesuai dengan kombinasi profil Anda. Skor kesesuaian {$score}% diperoleh dari analisis lima atribut: nilai akademik, minat, preferensi studi lanjutan, cita-cita, dan prestasi (jika diisi). Berdasarkan data tersebut, jurusan ini memiliki kecocokan paling kuat dibanding alternatif lain pada sesi analisis Anda."
|
||||
];
|
||||
}
|
||||
return [
|
||||
|
|
@ -157,11 +270,40 @@ protected function getFallbackResponse($message, $context = [])
|
|||
];
|
||||
}
|
||||
|
||||
if (
|
||||
strpos($messageLower, 'apa itu') !== false ||
|
||||
strpos($messageLower, 'jurusan tersebut') !== false ||
|
||||
strpos($messageLower, 'jelaskan jurusan') !== false ||
|
||||
strpos($messageLower, 'maksud jurusan') !== false
|
||||
) {
|
||||
if ($hasRecommendation) {
|
||||
$parts = [];
|
||||
$parts[] = "Jurusan \"{$jurusan}\" adalah bidang yang berfokus pada kompetensi terapan sesuai kebutuhan dunia kerja.";
|
||||
if (!empty($majorDesc)) {
|
||||
$parts[] = "Gambaran jurusan: {$majorDesc}";
|
||||
}
|
||||
if (!empty($majorProspek)) {
|
||||
$parts[] = "Prospek kerja utama: {$majorProspek}";
|
||||
}
|
||||
$parts[] = "Jika Anda berkenan, saya dapat lanjutkan dengan mata kuliah inti, kemampuan yang perlu dipersiapkan, dan alasan kesesuaiannya dengan profil Anda.";
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => implode(' ', $parts),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Saya dapat menjelaskan jurusan secara rinci. Agar lebih tepat sasaran, sebutkan nama jurusan yang ingin Anda ketahui, atau jalankan Analisis Rekomendasi terlebih dahulu agar saya menjelaskan jurusan yang paling sesuai dengan profil Anda.",
|
||||
];
|
||||
}
|
||||
|
||||
if (strpos($messageLower, 'prospek') !== false || strpos($messageLower, 'karir') !== false || strpos($messageLower, 'kerja') !== false) {
|
||||
if ($hasRecommendation) {
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Jurusan \"{$jurusan}\" memiliki prospek karier yang menjanjikan. Lulusan dari jurusan ini dapat bekerja di berbagai sektor industri yang relevan dengan bidang keahliannya. Setiap jurusan di POLIJE dirancang untuk membekali lulusannya dengan kompetensi praktis yang dibutuhkan oleh dunia kerja. Apakah Anda ingin mengetahui lebih detail mengenai posisi pekerjaan spesifik yang dapat ditempuh?"
|
||||
'message' => "Jurusan \"{$jurusan}\" memiliki prospek karier yang baik. " . (!empty($majorProspek) ? "Contoh prospek kerja: {$majorProspek}. " : "Lulusan dapat bekerja pada bidang yang relevan dengan kompetensi jurusan. ") . "Jika Anda berkenan, saya dapat jelaskan jalur karier dari level awal sampai pengembangan jangka panjang."
|
||||
];
|
||||
}
|
||||
return [
|
||||
|
|
@ -197,11 +339,15 @@ protected function getFallbackResponse($message, $context = [])
|
|||
];
|
||||
}
|
||||
|
||||
// Default response
|
||||
// Default response: tetap menanggapi isi pertanyaan agar nyambung
|
||||
if ($hasRecommendation) {
|
||||
$lead = !empty($lastAiMessage)
|
||||
? "Menindaklanjuti percakapan sebelumnya, "
|
||||
: "";
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Saya adalah konselor BK virtual SMA Bima Ambulu. Berdasarkan hasil analisis, jurusan \"{$jurusan}\" memiliki kesesuaian tertinggi dengan profil Anda, yaitu sebesar {$score}%. Anda dapat berkonsultasi mengenai prospek karier, kompetensi yang dibutuhkan, perbandingan antar jurusan, atau hal lain terkait persiapan pendidikan tinggi. Silakan sampaikan pertanyaan Anda."
|
||||
'message' => $lead . "berdasarkan hasil analisis Anda saat ini, jurusan dengan kecocokan tertinggi adalah \"{$jurusan}\" ({$score}%). " . (!empty($majorDesc) ? "Ringkasan jurusan: {$majorDesc}. " : "") . "Silakan lanjutkan dengan pertanyaan yang lebih spesifik, misalnya perbandingan dengan jurusan lain, kompetensi yang harus dipersiapkan, atau prospek kariernya."
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -319,6 +465,7 @@ protected function buildSystemPrompt($context)
|
|||
}
|
||||
|
||||
$prompt .= "\n\nCara kamu merespons:";
|
||||
$prompt .= "\n0. WAJIB jawab inti pertanyaan pengguna terlebih dahulu secara langsung dalam 1-2 kalimat pertama. Jangan memutar atau memberi jawaban generik yang tidak menanggapi pertanyaan.";
|
||||
$prompt .= "\n1. INGAT seluruh percakapan sebelumnya. Jangan tanya ulang hal yang sudah dijawab siswa.";
|
||||
$prompt .= "\n2. Kalau siswa sudah bilang minat/kemampuan/kesukaan, LANGSUNG analisis dan arahkan ke jurusan yang cocok dengan ALASAN LOGIS (misal: 'kamu suka logika → TI cocok karena...')";
|
||||
$prompt .= "\n3. Berikan REKOMENDASI TEGAS, bukan cuma daftar pilihan. Contoh: 'Menurut Bapak, kamu paling cocok ke Teknologi Informasi. Alasannya: ...'";
|
||||
|
|
@ -327,6 +474,8 @@ protected function buildSystemPrompt($context)
|
|||
$prompt .= "\n6. Jawab RINGKAS (2-3 paragraf). Jangan terlalu panjang kecuali diminta detail.";
|
||||
$prompt .= "\n7. Boleh menjawab pertanyaan di luar topik jurusan secara singkat, lalu kembalikan ke konseling.";
|
||||
$prompt .= "\n8. JANGAN awali setiap respons dengan 'Halo' atau salam — langsung ke inti jawaban (kecuali percakapan baru dimulai).";
|
||||
$prompt .= "\n8a. Jika pengguna bertanya 'apa itu jurusan X' atau 'jelaskan jurusan tersebut', jelaskan definisi jurusan, fokus pembelajaran, dan prospek kerjanya secara ringkas dan nyambung dengan konteks pengguna.";
|
||||
$prompt .= "\n8b. Jika pengguna menilai jawaban tidak nyambung, lakukan klarifikasi singkat lalu jawab ulang secara spesifik sesuai pertanyaan terbaru.";
|
||||
|
||||
// Tambahkan referensi Q&A serupa dari riwayat
|
||||
if (!empty($context['similar_qa'])) {
|
||||
|
|
@ -339,7 +488,232 @@ protected function buildSystemPrompt($context)
|
|||
$prompt .= "\nGunakan referensi di atas untuk menjaga konsistensi jawaban, namun tetap sesuaikan dengan profil dan konteks percakapan siswa saat ini.";
|
||||
} $prompt .= "\n9. DILARANG KERAS menggunakan format markdown seperti **, *, #, ##, atau simbol formatting lainnya. Tulis teks biasa (plain text) saja tanpa formatting markdown.";
|
||||
$prompt .= "\n10. Gunakan bahasa Indonesia baku dan akademik. Hindari bahasa gaul seperti 'kek', 'banget', 'ngobrol', 'ngomongin', 'gampangnya'. Gunakan padanan formal seperti 'sangat', 'berbincang', 'membahas', 'secara sederhana'.";
|
||||
$prompt .= "\n11. Jika pertanyaan bersifat umum di luar jurusan (misalnya pengetahuan umum), jawab singkat namun benar, lalu tawarkan kaitan dengan rencana studi/karier pengguna.";
|
||||
|
||||
if (($context['intent'] ?? '') === 'compare_majors') {
|
||||
$prompt .= "\n12. Pengguna sedang meminta PERBANDINGAN JURUSAN. Gunakan format terstruktur dengan urutan tetap: (1) Fokus pembelajaran, (2) Kompetensi yang perlu dipersiapkan, (3) Prospek kerja, (4) Tantangan belajar, (5) Rekomendasi paling sesuai untuk profil pengguna beserta alasan.";
|
||||
}
|
||||
|
||||
return $prompt;
|
||||
}
|
||||
|
||||
protected function isFollowUpMessage(string $messageLower): bool
|
||||
{
|
||||
$followUpHints = [
|
||||
'jelaskan semua',
|
||||
'lanjut',
|
||||
'lanjutkan',
|
||||
'yang tadi',
|
||||
'yang sebelumnya',
|
||||
'maksudnya',
|
||||
'lebih detail',
|
||||
'perjelas',
|
||||
'itu gimana',
|
||||
'jurusan itu',
|
||||
'jurusan tersebut',
|
||||
'semua jurusan',
|
||||
'satu satu',
|
||||
'satu-satu',
|
||||
];
|
||||
|
||||
foreach ($followUpHints as $hint) {
|
||||
if (strpos($messageLower, $hint) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getLastAssistantMessage(array $chatHistory): ?string
|
||||
{
|
||||
for ($i = count($chatHistory) - 1; $i >= 0; $i--) {
|
||||
$item = $chatHistory[$i] ?? null;
|
||||
if (!$item || !isset($item['role'], $item['text'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($item['role'] === 'ai' && trim((string) $item['text']) !== '') {
|
||||
return (string) $item['text'];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function buildAllMajorsResponse(bool $hasRecommendation, ?string $jurusan, float $score): string
|
||||
{
|
||||
$majors = PolijeMajor::orderBy('nama_jurusan')->get();
|
||||
|
||||
if ($majors->isEmpty()) {
|
||||
if ($hasRecommendation) {
|
||||
return "Menindaklanjuti pertanyaan Anda, saat ini rekomendasi utama Anda tetap pada jurusan \"{$jurusan}\" dengan skor {$score}%. Jika Anda berkenan, saya dapat jelaskan detail jurusan ini terlebih dahulu.";
|
||||
}
|
||||
|
||||
return "Data jurusan saat ini belum tersedia. Silakan coba kembali beberapa saat lagi, atau ajukan satu jurusan yang ingin dibahas agar saya jelaskan secara umum.";
|
||||
}
|
||||
|
||||
$lines = [];
|
||||
$lines[] = "Berikut ringkasan seluruh jurusan di Politeknik Negeri Jember agar pembahasan kita tetap nyambung dengan pertanyaan Anda:";
|
||||
|
||||
foreach ($majors as $index => $m) {
|
||||
$desc = trim((string) ($m->deskripsi ?? ''));
|
||||
if ($desc === '') {
|
||||
$desc = 'Berorientasi pada kompetensi terapan yang relevan dengan kebutuhan dunia kerja.';
|
||||
}
|
||||
$num = $index + 1;
|
||||
$lines[] = "{$num}. {$m->nama_jurusan}: {$desc}";
|
||||
}
|
||||
|
||||
if ($hasRecommendation) {
|
||||
$lines[] = "Berdasarkan profil Anda, jurusan yang paling direkomendasikan tetap \"{$jurusan}\" ({$score}%). Jika Anda ingin, saya lanjutkan dengan perbandingan jurusan rekomendasi Anda terhadap 2 alternatif teratas.";
|
||||
} else {
|
||||
$lines[] = "Jika Anda berkenan, saya dapat bantu menyaring 2-3 jurusan paling relevan berdasarkan minat dan nilai Anda.";
|
||||
}
|
||||
|
||||
return implode(' ', $lines);
|
||||
}
|
||||
|
||||
protected function buildStructuredComparisonResponse(string $message, array $context = []): ?string
|
||||
{
|
||||
$majors = PolijeMajor::orderBy('nama_jurusan')->get();
|
||||
if ($majors->count() < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$selected = $this->resolveMajorsForComparison($message, $context, $majors);
|
||||
if (count($selected) < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$a = $selected[0];
|
||||
$b = $selected[1];
|
||||
|
||||
$aDesc = trim((string) ($a->deskripsi ?? ''));
|
||||
$bDesc = trim((string) ($b->deskripsi ?? ''));
|
||||
$aProspek = trim((string) ($a->prospek_kerja ?? ''));
|
||||
$bProspek = trim((string) ($b->prospek_kerja ?? ''));
|
||||
|
||||
if ($aDesc === '') {
|
||||
$aDesc = 'Berfokus pada kompetensi terapan sesuai kebutuhan industri.';
|
||||
}
|
||||
if ($bDesc === '') {
|
||||
$bDesc = 'Berfokus pada kompetensi terapan sesuai kebutuhan industri.';
|
||||
}
|
||||
if ($aProspek === '') {
|
||||
$aProspek = 'Lulusan berpeluang masuk pada bidang kerja yang relevan dengan kompetensi jurusan.';
|
||||
}
|
||||
if ($bProspek === '') {
|
||||
$bProspek = 'Lulusan berpeluang masuk pada bidang kerja yang relevan dengan kompetensi jurusan.';
|
||||
}
|
||||
|
||||
$recommended = $context['recommendation'] ?? null;
|
||||
$winner = $a->nama_jurusan;
|
||||
if (!empty($recommended)) {
|
||||
if (strcasecmp($recommended, $b->nama_jurusan) === 0) {
|
||||
$winner = $b->nama_jurusan;
|
||||
} elseif (strcasecmp($recommended, $a->nama_jurusan) !== 0) {
|
||||
$winner = $a->nama_jurusan;
|
||||
}
|
||||
}
|
||||
|
||||
$score = isset($context['score']) ? (float) $context['score'] : null;
|
||||
$scoreText = $score !== null && $score > 0 ? " dengan skor kesesuaian {$score}%" : "";
|
||||
|
||||
$lines = [];
|
||||
$lines[] = "Perbandingan terstruktur antara Jurusan {$a->nama_jurusan} dan Jurusan {$b->nama_jurusan}:";
|
||||
$lines[] = "1. Fokus pembelajaran";
|
||||
$lines[] = "- {$a->nama_jurusan}: {$aDesc}";
|
||||
$lines[] = "- {$b->nama_jurusan}: {$bDesc}";
|
||||
$lines[] = "2. Kompetensi yang perlu dipersiapkan";
|
||||
$lines[] = "- {$a->nama_jurusan}: Penguasaan konsep inti jurusan, kemampuan analitis, komunikasi profesional, dan disiplin praktik.";
|
||||
$lines[] = "- {$b->nama_jurusan}: Penguasaan konsep inti jurusan, kemampuan analitis, komunikasi profesional, dan disiplin praktik.";
|
||||
$lines[] = "3. Prospek kerja";
|
||||
$lines[] = "- {$a->nama_jurusan}: {$aProspek}";
|
||||
$lines[] = "- {$b->nama_jurusan}: {$bProspek}";
|
||||
$lines[] = "4. Tantangan belajar";
|
||||
$lines[] = "- {$a->nama_jurusan}: Menuntut konsistensi belajar, adaptasi pada praktik lapangan, dan ketekunan menyelesaikan tugas proyek.";
|
||||
$lines[] = "- {$b->nama_jurusan}: Menuntut konsistensi belajar, adaptasi pada praktik lapangan, dan ketekunan menyelesaikan tugas proyek.";
|
||||
$lines[] = "5. Rekomendasi untuk profil Anda";
|
||||
$lines[] = "- Berdasarkan konteks analisis Anda, jurusan yang lebih diprioritaskan adalah {$winner}{$scoreText}. Jika Anda berkenan, saya dapat lanjutkan dengan langkah persiapan 6 bulan pertama agar transisi belajar Anda lebih terarah.";
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
protected function resolveMajorsForComparison(string $message, array $context, $majors): array
|
||||
{
|
||||
$messageLower = mb_strtolower($message);
|
||||
$mentioned = [];
|
||||
|
||||
foreach ($majors as $major) {
|
||||
$name = mb_strtolower((string) $major->nama_jurusan);
|
||||
if ($name !== '' && str_contains($messageLower, $name)) {
|
||||
$mentioned[$major->nama_jurusan] = $major;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($mentioned) >= 2) {
|
||||
return array_slice(array_values($mentioned), 0, 2);
|
||||
}
|
||||
|
||||
$picked = array_values($mentioned);
|
||||
$recommendationName = $context['recommendation'] ?? null;
|
||||
|
||||
if (!empty($recommendationName)) {
|
||||
$recMajor = $majors->first(function ($m) use ($recommendationName) {
|
||||
return strcasecmp((string) $m->nama_jurusan, (string) $recommendationName) === 0;
|
||||
});
|
||||
|
||||
if ($recMajor && !$this->containsMajor($picked, $recMajor->nama_jurusan)) {
|
||||
$picked[] = $recMajor;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($context['top3']) && is_array($context['top3'])) {
|
||||
foreach ($context['top3'] as $candidate) {
|
||||
$name = $candidate['jurusan'] ?? null;
|
||||
if (empty($name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$candidateMajor = $majors->first(function ($m) use ($name) {
|
||||
return strcasecmp((string) $m->nama_jurusan, (string) $name) === 0;
|
||||
});
|
||||
|
||||
if ($candidateMajor && !$this->containsMajor($picked, $candidateMajor->nama_jurusan)) {
|
||||
$picked[] = $candidateMajor;
|
||||
}
|
||||
|
||||
if (count($picked) >= 2) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count($picked) < 2) {
|
||||
foreach ($majors as $major) {
|
||||
if (!$this->containsMajor($picked, $major->nama_jurusan)) {
|
||||
$picked[] = $major;
|
||||
}
|
||||
|
||||
if (count($picked) >= 2) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_slice($picked, 0, 2);
|
||||
}
|
||||
|
||||
protected function containsMajor(array $picked, string $name): bool
|
||||
{
|
||||
foreach ($picked as $item) {
|
||||
if (strcasecmp((string) $item->nama_jurusan, $name) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,15 @@
|
|||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"barryvdh/laravel-dompdf": "*",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"laravel/framework": "^10.0",
|
||||
"laravel/sanctum": "^3.2",
|
||||
"laravel/tinker": "^2.8"
|
||||
"laravel/tinker": "^2.8",
|
||||
"phpoffice/phpspreadsheet": "^5.7"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/dbal": "^3.10",
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
"laravel/breeze": "^1.29",
|
||||
"laravel/pint": "^1.0",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -94,7 +94,7 @@
|
|||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => 'password_reset_tokens',
|
||||
'expire' => 60,
|
||||
'expire' => 1440, // 24 hours (was 60 minutes)
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
|
|
|||
|
|
@ -4,98 +4,101 @@
|
|||
// Standar kriteria per jurusan dengan bobot dan probabilitas kecocokan
|
||||
// Dioptimalkan untuk Polije (Vocational Campus)
|
||||
// Weights: nilai, minat, pref, prestasi, cita_cita
|
||||
// Preference: Praktik Langsung, DuDi, Project Based, Blended
|
||||
// Preference diselaraskan dengan input form rekomendasi:
|
||||
// Sains & Teknologi, Pertanian & Lingkungan, Kesehatan & Ilmu Hayat,
|
||||
// Bisnis & Manajemen, Sosial & Humaniora
|
||||
|
||||
'criteria' => [
|
||||
'Produksi Pertanian' => [
|
||||
'nilai' => 'Sedang',
|
||||
'minat' => 'Alam & Tanaman',
|
||||
'pref' => ['Praktik Langsung', 'DuDi', 'Project Based'],
|
||||
'cita_cita_keywords' => ['pertanian', 'petani', 'kebun', 'sawah', 'panen', 'tanaman'],
|
||||
'pref' => ['Pertanian & Lingkungan'],
|
||||
'cita_cita_keywords' => ['pertanian', 'petani', 'kebun', 'sawah', 'panen', 'tanaman', 'agronomi', 'perkebunan', 'hortikultura', 'farmer', 'agronomist', 'cultivation'],
|
||||
'skills_required' => ['Observasi', 'Kerja Lapangan', 'Pemeliharaan Tanaman'],
|
||||
'weights' => ['nilai' => 0.40, 'minat' => 0.35, 'pref' => 0.15, 'prestasi' => 0.05, 'cita_cita' => 0.05],
|
||||
'weights' => ['nilai' => 0.156, 'minat' => 0.456, 'pref' => 0.256, 'cita_cita' => 0.090, 'prestasi' => 0.040],
|
||||
'match_prob' => ['nilai' => 0.80, 'minat' => 0.90, 'pref' => 0.85, 'prestasi' => 0.65, 'cita_cita' => 0.85],
|
||||
],
|
||||
'Teknologi Pertanian' => [
|
||||
'nilai' => 'Tinggi',
|
||||
'minat' => 'Alam & Tanaman',
|
||||
'pref' => ['Praktik Langsung', 'Project Based', 'DuDi'],
|
||||
'cita_cita_keywords' => ['teknologi', 'inovasi', 'otomasi', 'mesin pertanian'],
|
||||
'pref' => ['Sains & Teknologi', 'Pertanian & Lingkungan'],
|
||||
'cita_cita_keywords' => ['teknologi', 'inovasi', 'otomasi', 'mesin pertanian', 'smart farming', 'teknologi pangan', 'agritech', 'agricultural engineer'],
|
||||
'skills_required' => ['Problem Solving', 'Teknologi', 'Inovasi'],
|
||||
'weights' => ['nilai' => 0.50, 'minat' => 0.25, 'pref' => 0.15, 'prestasi' => 0.05, 'cita_cita' => 0.05],
|
||||
'weights' => ['nilai' => 0.156, 'minat' => 0.456, 'pref' => 0.256, 'cita_cita' => 0.090, 'prestasi' => 0.040],
|
||||
'match_prob' => ['nilai' => 0.85, 'minat' => 0.90, 'pref' => 0.85, 'prestasi' => 0.75, 'cita_cita' => 0.80],
|
||||
],
|
||||
'Peternakan' => [
|
||||
'nilai' => 'Sedang',
|
||||
'minat' => 'Alam & Tanaman',
|
||||
'pref' => ['Praktik Langsung', 'DuDi', 'Project Based'],
|
||||
'cita_cita_keywords' => ['ternak', 'hewan', 'peternakan', 'peeternak', 'sapi', 'ayam', 'unggas'],
|
||||
'pref' => ['Pertanian & Lingkungan', 'Kesehatan & Ilmu Hayat'],
|
||||
'cita_cita_keywords' => ['ternak', 'hewan', 'peternakan', 'peternak', 'sapi', 'ayam', 'unggas', 'veteriner', 'farm', 'livestock', 'husbandry', 'zootechnist'],
|
||||
'skills_required' => ['Perawatan Hewan', 'Kesabaran', 'Manajemen'],
|
||||
'weights' => ['nilai' => 0.40, 'minat' => 0.40, 'pref' => 0.10, 'prestasi' => 0.05, 'cita_cita' => 0.05],
|
||||
'weights' => ['nilai' => 0.156, 'minat' => 0.456, 'pref' => 0.256, 'cita_cita' => 0.090, 'prestasi' => 0.040],
|
||||
'match_prob' => ['nilai' => 0.80, 'minat' => 0.88, 'pref' => 0.80, 'prestasi' => 0.65, 'cita_cita' => 0.88],
|
||||
],
|
||||
'Manajemen Agribisnis' => [
|
||||
'nilai' => 'Sedang',
|
||||
'minat' => 'Manajemen & Bisnis',
|
||||
'pref' => ['Project Based', 'DuDi', 'Blended'],
|
||||
'cita_cita_keywords' => ['bisnis', 'agribisnis', 'usaha', 'entrepreneur', 'pengusaha'],
|
||||
'pref' => ['Bisnis & Manajemen', 'Pertanian & Lingkungan'],
|
||||
'cita_cita_keywords' => ['bisnis', 'agribisnis', 'usaha', 'entrepreneur', 'pengusaha', 'manajer', 'marketing', 'wirausaha', 'director', 'executive', 'manager'],
|
||||
'skills_required' => ['Manajemen', 'Bisnis Acumen', 'Komunikasi'],
|
||||
'weights' => ['nilai' => 0.35, 'minat' => 0.40, 'pref' => 0.15, 'prestasi' => 0.05, 'cita_cita' => 0.05],
|
||||
'weights' => ['nilai' => 0.156, 'minat' => 0.456, 'pref' => 0.256, 'cita_cita' => 0.090, 'prestasi' => 0.040],
|
||||
'match_prob' => ['nilai' => 0.75, 'minat' => 0.90, 'pref' => 0.80, 'prestasi' => 0.70, 'cita_cita' => 0.85],
|
||||
],
|
||||
'Teknologi Informasi' => [
|
||||
'nilai' => 'Tinggi',
|
||||
'minat' => 'Logika & Komputer',
|
||||
'pref' => ['Praktik Langsung', 'Project Based', 'DuDi'],
|
||||
'cita_cita_keywords' => ['programmer', 'developer', 'coding', 'software', 'web developer', 'hacker', 'it'],
|
||||
'pref' => ['Sains & Teknologi'],
|
||||
'cita_cita_keywords' => ['programmer', 'developer', 'coding', 'software', 'web developer', 'hacker', 'it', 'data analyst', 'ai engineer', 'mobile developer', 'devops', 'cloud engineer', 'scientist', 'architect'],
|
||||
'skills_required' => ['Coding', 'Problem Solving', 'Logika'],
|
||||
'weights' => ['nilai' => 0.45, 'minat' => 0.35, 'pref' => 0.12, 'prestasi' => 0.05, 'cita_cita' => 0.03],
|
||||
'weights' => ['nilai' => 0.156, 'minat' => 0.456, 'pref' => 0.256, 'cita_cita' => 0.090, 'prestasi' => 0.040],
|
||||
'match_prob' => ['nilai' => 0.90, 'minat' => 0.92, 'pref' => 0.85, 'prestasi' => 0.75, 'cita_cita' => 0.85],
|
||||
],
|
||||
'Teknik' => [
|
||||
'nilai' => 'Sedang',
|
||||
'minat' => 'Mesin & Listrik',
|
||||
'pref' => ['Praktik Langsung', 'DuDi', 'Project Based'],
|
||||
'cita_cita_keywords' => ['mesin', 'bengkel', 'teknisi', 'listrik', 'elektronik', 'automasi', 'instalasi', 'panel'],
|
||||
'pref' => ['Sains & Teknologi'],
|
||||
'cita_cita_keywords' => ['mesin', 'bengkel', 'teknisi', 'listrik', 'elektronik', 'otomasi', 'instalasi', 'panel', 'mekatronika', 'maintenance', 'engineer', 'technician', 'supervisor'],
|
||||
'skills_required' => ['Mekanik', 'Elektrik', 'Teknik', 'Presisi'],
|
||||
'weights' => ['nilai' => 0.42, 'minat' => 0.38, 'pref' => 0.12, 'prestasi' => 0.05, 'cita_cita' => 0.03],
|
||||
'weights' => ['nilai' => 0.156, 'minat' => 0.456, 'pref' => 0.256, 'cita_cita' => 0.090, 'prestasi' => 0.040],
|
||||
'match_prob' => ['nilai' => 0.82, 'minat' => 0.90, 'pref' => 0.85, 'prestasi' => 0.71, 'cita_cita' => 0.85],
|
||||
],
|
||||
'Kesehatan' => [
|
||||
'nilai' => 'Tinggi',
|
||||
'minat' => 'Pelayanan & Kesehatan',
|
||||
'pref' => ['Praktik Langsung', 'DuDi', 'Project Based'],
|
||||
'cita_cita_keywords' => ['dokter', 'perawat', 'medis', 'gizi', 'kesehatan', 'pelayanan', 'terapis'],
|
||||
'pref' => ['Kesehatan & Ilmu Hayat'],
|
||||
'cita_cita_keywords' => ['dokter', 'perawat', 'medis', 'gizi', 'kesehatan', 'pelayanan', 'terapis', 'farmasi', 'rekam medis', 'kesehatan masyarakat', 'nurse', 'pharmacist', 'nutritionist', 'clinician'],
|
||||
'skills_required' => ['Komunikasi', 'Empati', 'Presisi Medis'],
|
||||
'weights' => ['nilai' => 0.45, 'minat' => 0.35, 'pref' => 0.10, 'prestasi' => 0.05, 'cita_cita' => 0.05],
|
||||
'weights' => ['nilai' => 0.156, 'minat' => 0.456, 'pref' => 0.256, 'cita_cita' => 0.090, 'prestasi' => 0.040],
|
||||
'match_prob' => ['nilai' => 0.90, 'minat' => 0.90, 'pref' => 0.80, 'prestasi' => 0.75, 'cita_cita' => 0.90],
|
||||
],
|
||||
'Bahasa, Komunikasi, dan Pariwisata' => [
|
||||
'nilai' => 'Sedang',
|
||||
'minat' => 'Umum',
|
||||
'pref' => ['Project Based', 'DuDi', 'Praktik Langsung'],
|
||||
'cita_cita_keywords' => ['tour guide', 'pariwisata', 'bahasa', 'komunikasi', 'jurnalis', 'marketing'],
|
||||
'pref' => ['Sosial & Humaniora', 'Bisnis & Manajemen'],
|
||||
'cita_cita_keywords' => ['tour guide', 'pariwisata', 'bahasa', 'komunikasi', 'jurnalis', 'marketing', 'public relation', 'content creator', 'hospitality', 'ambassador', 'presenter', 'broadcaster'],
|
||||
'skills_required' => ['Komunikasi', 'Bahasa', 'Kepribadian'],
|
||||
'weights' => ['nilai' => 0.30, 'minat' => 0.40, 'pref' => 0.15, 'prestasi' => 0.08, 'cita_cita' => 0.07],
|
||||
'weights' => ['nilai' => 0.156, 'minat' => 0.456, 'pref' => 0.256, 'cita_cita' => 0.090, 'prestasi' => 0.040],
|
||||
'match_prob' => ['nilai' => 0.70, 'minat' => 0.85, 'pref' => 0.80, 'prestasi' => 0.70, 'cita_cita' => 0.85],
|
||||
],
|
||||
'Bisnis' => [
|
||||
'nilai' => 'Sedang',
|
||||
'minat' => 'Manajemen & Bisnis',
|
||||
'pref' => ['Project Based', 'DuDi', 'Blended'],
|
||||
'cita_cita_keywords' => ['manager', 'pimpinan', 'bisnis', 'accounting', 'marketing', 'sales'],
|
||||
'pref' => ['Bisnis & Manajemen'],
|
||||
'cita_cita_keywords' => ['manager', 'pimpinan', 'bisnis', 'accounting', 'marketing', 'sales', 'akuntan', 'keuangan', 'bank', 'finance', 'accountant', 'auditor'],
|
||||
'skills_required' => ['Manajemen', 'Leadership', 'Keuangan'],
|
||||
'weights' => ['nilai' => 0.35, 'minat' => 0.40, 'pref' => 0.15, 'prestasi' => 0.05, 'cita_cita' => 0.05],
|
||||
'weights' => ['nilai' => 0.156, 'minat' => 0.456, 'pref' => 0.256, 'cita_cita' => 0.090, 'prestasi' => 0.040],
|
||||
'match_prob' => ['nilai' => 0.75, 'minat' => 0.90, 'pref' => 0.80, 'prestasi' => 0.70, 'cita_cita' => 0.85],
|
||||
],
|
||||
],
|
||||
|
||||
// Vocational Learning Preference Mapping
|
||||
// Mapping preferensi studi sesuai opsi input form rekomendasi
|
||||
'pref_mapping' => [
|
||||
'Praktik Langsung' => ['weight' => 1.0, 'score' => 0.95],
|
||||
'DuDi' => ['weight' => 0.95, 'score' => 0.90],
|
||||
'Project Based' => ['weight' => 0.90, 'score' => 0.85],
|
||||
'Blended' => ['weight' => 0.80, 'score' => 0.75],
|
||||
'Sains & Teknologi' => ['weight' => 1.0, 'score' => 0.95],
|
||||
'Pertanian & Lingkungan' => ['weight' => 0.95, 'score' => 0.90],
|
||||
'Kesehatan & Ilmu Hayat' => ['weight' => 0.95, 'score' => 0.90],
|
||||
'Bisnis & Manajemen' => ['weight' => 0.90, 'score' => 0.85],
|
||||
'Sosial & Humaniora' => ['weight' => 0.85, 'score' => 0.80],
|
||||
],
|
||||
|
||||
// Category mapping untuk nilai
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@
|
|||
|
||||
'gemini' => [
|
||||
'api_key' => env('GEMINI_API_KEY'),
|
||||
'backend_url' => env('GEMINI_BACKEND_URL'),
|
||||
'backend_token' => env('GEMINI_BACKEND_TOKEN'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@
|
|||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
//
|
||||
$table->enum('role', ['admin', 'guru', 'siswa'])->default('siswa')->after('password');
|
||||
$table->enum('role', ['admin', 'guru', 'bk', 'siswa'])->default('siswa')->after('password');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -23,7 +22,7 @@ public function up(): void
|
|||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
//
|
||||
$table->dropColumn('role');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
|
|
@ -11,10 +12,10 @@
|
|||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// Modify role enum to include 'bk'
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->enum('role', ['admin', 'guru', 'bk', 'siswa'])->default('siswa')->change();
|
||||
});
|
||||
// Use raw SQL for MySQL to avoid Doctrine enum introspection issues in tests.
|
||||
if (DB::getDriverName() === 'mysql') {
|
||||
DB::statement("ALTER TABLE users MODIFY role ENUM('admin','guru','bk','siswa') NOT NULL DEFAULT 'siswa'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -22,8 +23,8 @@ public function up(): void
|
|||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->enum('role', ['admin', 'guru', 'siswa'])->default('siswa')->change();
|
||||
});
|
||||
if (DB::getDriverName() === 'mysql') {
|
||||
DB::statement("ALTER TABLE users MODIFY role ENUM('admin','guru','siswa') NOT NULL DEFAULT 'siswa'");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('recommendations') && !Schema::hasTable('rekomendasi')) {
|
||||
Schema::rename('recommendations', 'rekomendasi');
|
||||
}
|
||||
|
||||
if (Schema::hasTable('polije_majors') && !Schema::hasTable('jurusan_polije')) {
|
||||
Schema::rename('polije_majors', 'jurusan_polije');
|
||||
}
|
||||
|
||||
if (Schema::hasTable('chat_histories') && !Schema::hasTable('riwayat_chat')) {
|
||||
Schema::rename('chat_histories', 'riwayat_chat');
|
||||
}
|
||||
|
||||
if (Schema::hasTable('riwayat_chat') && Schema::hasColumn('riwayat_chat', 'session_id') && !Schema::hasColumn('riwayat_chat', 'id_sesi')) {
|
||||
Schema::table('riwayat_chat', function (Blueprint $table) {
|
||||
$table->renameColumn('session_id', 'id_sesi');
|
||||
});
|
||||
}
|
||||
|
||||
if (Schema::hasTable('riwayat_chat') && Schema::hasColumn('riwayat_chat', 'prompt') && !Schema::hasColumn('riwayat_chat', 'pertanyaan')) {
|
||||
Schema::table('riwayat_chat', function (Blueprint $table) {
|
||||
$table->renameColumn('prompt', 'pertanyaan');
|
||||
});
|
||||
}
|
||||
|
||||
if (Schema::hasTable('riwayat_chat') && Schema::hasColumn('riwayat_chat', 'response') && !Schema::hasColumn('riwayat_chat', 'jawaban')) {
|
||||
Schema::table('riwayat_chat', function (Blueprint $table) {
|
||||
$table->renameColumn('response', 'jawaban');
|
||||
});
|
||||
}
|
||||
|
||||
if (Schema::hasTable('riwayat_chat') && Schema::hasColumn('riwayat_chat', 'recommendation_id') && !Schema::hasColumn('riwayat_chat', 'id_rekomendasi')) {
|
||||
Schema::table('riwayat_chat', function (Blueprint $table) {
|
||||
$table->renameColumn('recommendation_id', 'id_rekomendasi');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasTable('riwayat_chat') && Schema::hasColumn('riwayat_chat', 'id_sesi') && !Schema::hasColumn('riwayat_chat', 'session_id')) {
|
||||
Schema::table('riwayat_chat', function (Blueprint $table) {
|
||||
$table->renameColumn('id_sesi', 'session_id');
|
||||
});
|
||||
}
|
||||
|
||||
if (Schema::hasTable('riwayat_chat') && Schema::hasColumn('riwayat_chat', 'pertanyaan') && !Schema::hasColumn('riwayat_chat', 'prompt')) {
|
||||
Schema::table('riwayat_chat', function (Blueprint $table) {
|
||||
$table->renameColumn('pertanyaan', 'prompt');
|
||||
});
|
||||
}
|
||||
|
||||
if (Schema::hasTable('riwayat_chat') && Schema::hasColumn('riwayat_chat', 'jawaban') && !Schema::hasColumn('riwayat_chat', 'response')) {
|
||||
Schema::table('riwayat_chat', function (Blueprint $table) {
|
||||
$table->renameColumn('jawaban', 'response');
|
||||
});
|
||||
}
|
||||
|
||||
if (Schema::hasTable('riwayat_chat') && Schema::hasColumn('riwayat_chat', 'id_rekomendasi') && !Schema::hasColumn('riwayat_chat', 'recommendation_id')) {
|
||||
Schema::table('riwayat_chat', function (Blueprint $table) {
|
||||
$table->renameColumn('id_rekomendasi', 'recommendation_id');
|
||||
});
|
||||
}
|
||||
|
||||
if (Schema::hasTable('riwayat_chat') && !Schema::hasTable('chat_histories')) {
|
||||
Schema::rename('riwayat_chat', 'chat_histories');
|
||||
}
|
||||
|
||||
if (Schema::hasTable('jurusan_polije') && !Schema::hasTable('polije_majors')) {
|
||||
Schema::rename('jurusan_polije', 'polije_majors');
|
||||
}
|
||||
|
||||
if (Schema::hasTable('rekomendasi') && !Schema::hasTable('recommendations')) {
|
||||
Schema::rename('rekomendasi', 'recommendations');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('alumni', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('alumni', 'preferensi_studi_lanjutan')) {
|
||||
$table->dropColumn('preferensi_studi_lanjutan');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('alumni', function (Blueprint $table) {
|
||||
//
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Simplify alumni table - keep only essential columns
|
||||
* Remove: success_status, ranking_saat_rekomendasi, predicted_score, ipk_lulus, karir_outcome
|
||||
* Update: preferensi_studi dengan 5 universal options
|
||||
* Keep: Essential academic & career data only
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('alumni', function (Blueprint $table) {
|
||||
// Drop unnecessary columns
|
||||
if (Schema::hasColumn('alumni', 'success_status')) {
|
||||
$table->dropColumn('success_status');
|
||||
}
|
||||
if (Schema::hasColumn('alumni', 'ranking_saat_rekomendasi')) {
|
||||
$table->dropColumn('ranking_saat_rekomendasi');
|
||||
}
|
||||
if (Schema::hasColumn('alumni', 'predicted_score')) {
|
||||
$table->dropColumn('predicted_score');
|
||||
}
|
||||
if (Schema::hasColumn('alumni', 'ipk_lulus')) {
|
||||
$table->dropColumn('ipk_lulus');
|
||||
}
|
||||
if (Schema::hasColumn('alumni', 'karir_outcome')) {
|
||||
$table->dropColumn('karir_outcome');
|
||||
}
|
||||
});
|
||||
|
||||
// Update preferensi_studi dengan raw SQL untuk menghindari Doctrine enum issue
|
||||
Schema::table('alumni', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('alumni', 'preferensi_studi')) {
|
||||
$table->dropColumn('preferensi_studi');
|
||||
}
|
||||
});
|
||||
|
||||
Schema::table('alumni', function (Blueprint $table) {
|
||||
// Add kembali preferensi_studi dengan enum values yang tepat
|
||||
$table->enum('preferensi_studi', [
|
||||
'Sains & Teknologi',
|
||||
'Pertanian & Lingkungan',
|
||||
'Kesehatan & Ilmu Hayat',
|
||||
'Bisnis & Manajemen',
|
||||
'Sosial & Humaniora'
|
||||
])->nullable()->after('cita_cita');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('alumni', function (Blueprint $table) {
|
||||
//
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
private const IPA_SUBJECTS = ['mtk', 'fisika', 'kimia', 'biologi'];
|
||||
private const IPS_SUBJECTS = ['ekonomi', 'geografi', 'sosiologi', 'sejarah'];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
$majors = DB::table('jurusan_polije')->select('id', 'bobot_mapel')->get();
|
||||
|
||||
foreach ($majors as $major) {
|
||||
$bobotMapel = $this->decodeBobotMapel($major->bobot_mapel);
|
||||
$nested = $this->toNestedStructure($bobotMapel);
|
||||
|
||||
DB::table('jurusan_polije')
|
||||
->where('id', $major->id)
|
||||
->update([
|
||||
'bobot_mapel' => json_encode($nested),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$majors = DB::table('jurusan_polije')->select('id', 'bobot_mapel')->get();
|
||||
|
||||
foreach ($majors as $major) {
|
||||
$bobotMapel = $this->decodeBobotMapel($major->bobot_mapel);
|
||||
$flat = $this->toFlatStructure($bobotMapel);
|
||||
|
||||
DB::table('jurusan_polije')
|
||||
->where('id', $major->id)
|
||||
->update([
|
||||
'bobot_mapel' => json_encode($flat),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function decodeBobotMapel(mixed $value): array
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (is_string($value) && $value !== '') {
|
||||
$decoded = json_decode($value, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function toNestedStructure(array $bobotMapel): array
|
||||
{
|
||||
if (isset($bobotMapel['ipa']) || isset($bobotMapel['ips'])) {
|
||||
return [
|
||||
'ipa' => $this->normalizeGroup($bobotMapel['ipa'] ?? [], self::IPA_SUBJECTS),
|
||||
'ips' => $this->normalizeGroup($bobotMapel['ips'] ?? [], self::IPS_SUBJECTS),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'ipa' => $this->normalizeGroup($bobotMapel, self::IPA_SUBJECTS),
|
||||
'ips' => $this->normalizeGroup($bobotMapel, self::IPS_SUBJECTS),
|
||||
];
|
||||
}
|
||||
|
||||
private function toFlatStructure(array $bobotMapel): array
|
||||
{
|
||||
if (!isset($bobotMapel['ipa']) && !isset($bobotMapel['ips'])) {
|
||||
return $this->normalizeFlatGroup($bobotMapel, array_merge(self::IPA_SUBJECTS, self::IPS_SUBJECTS));
|
||||
}
|
||||
|
||||
return array_merge(
|
||||
$this->normalizeGroup($bobotMapel['ipa'] ?? [], self::IPA_SUBJECTS),
|
||||
$this->normalizeGroup($bobotMapel['ips'] ?? [], self::IPS_SUBJECTS)
|
||||
);
|
||||
}
|
||||
|
||||
private function normalizeGroup(array $values, array $subjects): array
|
||||
{
|
||||
$normalized = [];
|
||||
|
||||
foreach ($subjects as $subject) {
|
||||
$value = $values[$subject] ?? null;
|
||||
$normalized[$subject] = is_numeric($value) ? (float) $value : 0.0;
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
private function normalizeFlatGroup(array $values, array $subjects): array
|
||||
{
|
||||
$normalized = [];
|
||||
|
||||
foreach ($subjects as $subject) {
|
||||
$value = $values[$subject] ?? null;
|
||||
if (is_numeric($value)) {
|
||||
$normalized[$subject] = (float) $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// Drop kolom nilai mapel individual, keep nilai_rata_rata
|
||||
Schema::table('alumni', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'mtk',
|
||||
'fisika',
|
||||
'kimia',
|
||||
'biologi',
|
||||
'ekonomi',
|
||||
'geografi',
|
||||
'sosiologi',
|
||||
'sejarah'
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('alumni', function (Blueprint $table) {
|
||||
$table->float('mtk')->nullable();
|
||||
$table->float('fisika')->nullable();
|
||||
$table->float('kimia')->nullable();
|
||||
$table->float('biologi')->nullable();
|
||||
$table->float('ekonomi')->nullable();
|
||||
$table->float('geografi')->nullable();
|
||||
$table->float('sosiologi')->nullable();
|
||||
$table->float('sejarah')->nullable();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('password_reset_codes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('email')->index();
|
||||
$table->string('code', 10)->index();
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('password_reset_codes');
|
||||
}
|
||||
};
|
||||
|
|
@ -28,10 +28,9 @@ public function run(): void
|
|||
'cita_cita' => 'Software Developer',
|
||||
'preferensi_studi' => 'Sains & Teknologi',
|
||||
'prestasi' => 'Juara 1 Olimpiade Komputer Nasional',
|
||||
'major_masuk' => 'Teknik Informatika', // Masuk jurusan ini
|
||||
'ranking_saat_rekomendasi' => 1, // Rekomendasi ranking 1 = COCOK! ✓✓
|
||||
'success_status' => 'sangat_sukses',
|
||||
'catatan' => 'Rekomendasi akurat - ranking 1 cocok dengan pilihan',
|
||||
'major_masuk' => 'Teknologi Informasi',
|
||||
'tahun_lulus_polije' => 2027,
|
||||
'catatan' => 'Alumni 2023 - Rekomendasi akurat',
|
||||
],
|
||||
[
|
||||
'nama_alumni' => 'Siti Nurhaliza',
|
||||
|
|
@ -46,9 +45,8 @@ public function run(): void
|
|||
'preferensi_studi' => 'Kesehatan & Ilmu Hayat',
|
||||
'prestasi' => 'Beasiswa Penuh Akademik',
|
||||
'major_masuk' => 'Teknik Biomedis',
|
||||
'ranking_saat_rekomendasi' => 2, // Cocok ✓
|
||||
'success_status' => 'sangat_sukses',
|
||||
'catatan' => 'Rekomendasi akurat - ranking 2 cocok',
|
||||
'tahun_lulus_polije' => 2027,
|
||||
'catatan' => 'Alumni 2023 - Rekomendasi akurat',
|
||||
],
|
||||
[
|
||||
'nama_alumni' => 'Ahmad Wijaya',
|
||||
|
|
@ -63,9 +61,8 @@ public function run(): void
|
|||
'preferensi_studi' => 'Sains & Teknologi',
|
||||
'prestasi' => 'Sertifikat Kompetisi Robotika',
|
||||
'major_masuk' => 'Teknik Mesin',
|
||||
'ranking_saat_rekomendasi' => 3, // Cocok ✓
|
||||
'success_status' => 'sukses',
|
||||
'catatan' => 'Rekomendasi cukup akurat - ranking 3 cocok',
|
||||
'tahun_lulus_polije' => 2027,
|
||||
'catatan' => 'Alumni 2023',
|
||||
],
|
||||
[
|
||||
'nama_alumni' => 'Lina Hartini',
|
||||
|
|
@ -80,9 +77,8 @@ public function run(): void
|
|||
'preferensi_studi' => 'Kesehatan & Ilmu Hayat',
|
||||
'prestasi' => 'Publikasi Paper Research',
|
||||
'major_masuk' => 'Teknik Biomedis',
|
||||
'ranking_saat_rekomendasi' => 1, // Cocok ✓✓
|
||||
'success_status' => 'sangat_sukses',
|
||||
'catatan' => 'Rekomendasi sangat akurat',
|
||||
'tahun_lulus_polije' => 2027,
|
||||
'catatan' => 'Alumni 2023 - Rekomendasi sangat akurat',
|
||||
],
|
||||
[
|
||||
'nama_alumni' => 'Fajar Maulana',
|
||||
|
|
@ -96,10 +92,9 @@ public function run(): void
|
|||
'cita_cita' => 'Electrical Engineer',
|
||||
'preferensi_studi' => 'Sains & Teknologi',
|
||||
'prestasi' => '-',
|
||||
'major_masuk' => 'Teknik Mesin', // BEDA dari rekomendasi ranking 1
|
||||
'ranking_saat_rekomendasi' => 6, // Ranking 6 = kurang cocok
|
||||
'success_status' => 'cukup',
|
||||
'catatan' => 'Rekomendasi kurang akurat - pilih jurusan berbeda',
|
||||
'major_masuk' => 'Teknik Mesin',
|
||||
'tahun_lulus_polije' => 2027,
|
||||
'catatan' => 'Alumni 2023',
|
||||
],
|
||||
|
||||
// === IPS ===
|
||||
|
|
@ -116,9 +111,8 @@ public function run(): void
|
|||
'preferensi_studi' => 'Bisnis & Manajemen',
|
||||
'prestasi' => 'Juara Debat Nasional',
|
||||
'major_masuk' => 'Manajemen Bisnis',
|
||||
'ranking_saat_rekomendasi' => 1, // Cocok ✓✓
|
||||
'success_status' => 'sangat_sukses',
|
||||
'catatan' => 'Rekomendasi sempurna',
|
||||
'tahun_lulus_polije' => 2027,
|
||||
'catatan' => 'Alumni 2023',
|
||||
],
|
||||
[
|
||||
'nama_alumni' => 'Dewi Prasetya',
|
||||
|
|
@ -133,9 +127,8 @@ public function run(): void
|
|||
'preferensi_studi' => 'Bisnis & Manajemen',
|
||||
'prestasi' => 'Sertifikasi ACCA',
|
||||
'major_masuk' => 'Akuntansi',
|
||||
'ranking_saat_rekomendasi' => 2, // Cocok ✓
|
||||
'success_status' => 'sukses',
|
||||
'catatan' => 'Rekomendasi akurat',
|
||||
'tahun_lulus_polije' => 2027,
|
||||
'catatan' => 'Alumni 2023',
|
||||
],
|
||||
[
|
||||
'nama_alumni' => 'Rudi Hermawan',
|
||||
|
|
@ -149,10 +142,9 @@ public function run(): void
|
|||
'cita_cita' => 'PNS',
|
||||
'preferensi_studi' => 'Sosial & Humaniora',
|
||||
'prestasi' => '-',
|
||||
'major_masuk' => 'Administrasi Publik', // RANKING JAUH dari pilihan
|
||||
'ranking_saat_rekomendasi' => 7, // Ranking 7 = TIDAK COCOK ✗
|
||||
'success_status' => 'kurang_sukses',
|
||||
'catatan' => 'Rekomendasi salah - siswa pilih jurusan lain',
|
||||
'major_masuk' => 'Administrasi Publik',
|
||||
'tahun_lulus_polije' => 2027,
|
||||
'catatan' => 'Alumni 2023',
|
||||
],
|
||||
[
|
||||
'nama_alumni' => 'Indra Setiawan',
|
||||
|
|
@ -167,9 +159,8 @@ public function run(): void
|
|||
'preferensi_studi' => 'Bisnis & Manajemen',
|
||||
'prestasi' => 'Kompetisi Business Plan',
|
||||
'major_masuk' => 'Manajemen Bisnis',
|
||||
'ranking_saat_rekomendasi' => 2, // Cocok ✓
|
||||
'success_status' => 'sukses',
|
||||
'catatan' => 'Rekomendasi akurat',
|
||||
'tahun_lulus_polije' => 2027,
|
||||
'catatan' => 'Alumni 2023',
|
||||
],
|
||||
[
|
||||
'nama_alumni' => 'Maya Suntari',
|
||||
|
|
@ -184,9 +175,8 @@ public function run(): void
|
|||
'preferensi_studi' => 'Bisnis & Manajemen',
|
||||
'prestasi' => 'Buku Tahunan Finance Club',
|
||||
'major_masuk' => 'Akuntansi',
|
||||
'ranking_saat_rekomendasi' => 3, // Cocok ✓
|
||||
'success_status' => 'sukses',
|
||||
'catatan' => 'Rekomendasi cukup akurat - ranking 3',
|
||||
'tahun_lulus_polije' => 2027,
|
||||
'catatan' => 'Alumni 2023',
|
||||
],
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,223 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Recommendation;
|
||||
use App\Models\PolijeMajor;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class BimaAmbulustudentSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* SEEDER UNTUK DATA SISWA SMK BIMA AMBULU
|
||||
*
|
||||
* Edit data siswa di bawah sesuai dengan data siswa dari SMK Bima Ambulu
|
||||
* Format:
|
||||
* - Kelompok: IPA atau IPS
|
||||
* - Nilai: 0-100
|
||||
* - Email: harus unik
|
||||
*
|
||||
* Run: php artisan db:seed --class=BimaAmbulustudentSeeder
|
||||
*/
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$studentsData = [
|
||||
// ========== SISWA SMK BIMA AMBULU - KELOMPOK IPA ==========
|
||||
|
||||
[
|
||||
'name' => 'Akmal Fiqri',
|
||||
'email' => 'akmal.fiqri@bima.student',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'values' => ['mtk' => 85, 'fisika' => 82, 'kimia' => 75, 'biologi' => 70],
|
||||
'minat' => 'Teknologi & Inovasi',
|
||||
'pref_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Software Engineer',
|
||||
'prestasi' => 'Aktif di klub robotika',
|
||||
],
|
||||
[
|
||||
'name' => 'Sasha Putri Aulia',
|
||||
'email' => 'sasha.putri@bima.student',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'values' => ['mtk' => 78, 'fisika' => 80, 'kimia' => 85, 'biologi' => 82],
|
||||
'minat' => 'Kesehatan & Biologi',
|
||||
'pref_studi' => 'Kesehatan & Ilmu Hayat',
|
||||
'cita_cita' => 'Dokter Umum',
|
||||
'prestasi' => 'Juara Olimpiade Biologi',
|
||||
],
|
||||
[
|
||||
'name' => 'Budi Kusuma',
|
||||
'email' => 'budi.kusuma@bima.student',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'values' => ['mtk' => 82, 'fisika' => 88, 'kimia' => 76, 'biologi' => 68],
|
||||
'minat' => 'Teknik & Mesin',
|
||||
'pref_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Insinyur Mesin',
|
||||
'prestasi' => 'Peserta Kompetisi Teknik',
|
||||
],
|
||||
[
|
||||
'name' => 'Dina Hartini',
|
||||
'email' => 'dina.hartini@bima.student',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'values' => ['mtk' => 75, 'fisika' => 72, 'kimia' => 88, 'biologi' => 90],
|
||||
'minat' => 'Farmasi & Kimia',
|
||||
'pref_studi' => 'Kesehatan & Ilmu Hayat',
|
||||
'cita_cita' => 'Apoteker',
|
||||
'prestasi' => 'Ranking 5 Besar',
|
||||
],
|
||||
[
|
||||
'name' => 'Eka Prasetyo',
|
||||
'email' => 'eka.prasetyo@bima.student',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'values' => ['mtk' => 88, 'fisika' => 85, 'kimia' => 72, 'biologi' => 75],
|
||||
'minat' => 'Pemrograman & Coding',
|
||||
'pref_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Web Developer',
|
||||
'prestasi' => 'Peserta Hackathon',
|
||||
],
|
||||
|
||||
// ========== SISWA SMK BIMA AMBULU - KELOMPOK IPS ==========
|
||||
|
||||
[
|
||||
'name' => 'Fahmi Rizki',
|
||||
'email' => 'fahmi.rizki@bima.student',
|
||||
'kelompok_asal' => 'IPS',
|
||||
'values' => ['ekonomi' => 88, 'geografi' => 80, 'sosiologi' => 78, 'sejarah' => 75],
|
||||
'minat' => 'Bisnis & Keuangan',
|
||||
'pref_studi' => 'Bisnis & Manajemen',
|
||||
'cita_cita' => 'Akuntan Profesional',
|
||||
'prestasi' => 'Aktif di organisasi bisnis',
|
||||
],
|
||||
[
|
||||
'name' => 'Gina Melani',
|
||||
'email' => 'gina.melani@bima.student',
|
||||
'kelompok_asal' => 'IPS',
|
||||
'values' => ['ekonomi' => 85, 'geografi' => 88, 'sosiologi' => 82, 'sejarah' => 80],
|
||||
'minat' => 'Pariwisata & Budaya',
|
||||
'pref_studi' => 'Sosial & Humaniora',
|
||||
'cita_cita' => 'Tour Guide Profesional',
|
||||
'prestasi' => 'Pelatihan Pariwisata',
|
||||
],
|
||||
[
|
||||
'name' => 'Hasan Wijaya',
|
||||
'email' => 'hasan.wijaya@bima.student',
|
||||
'kelompok_asal' => 'IPS',
|
||||
'values' => ['ekonomi' => 82, 'geografi' => 85, 'sosiologi' => 80, 'sejarah' => 78],
|
||||
'minat' => 'Manajemen & Administrasi',
|
||||
'pref_studi' => 'Bisnis & Manajemen',
|
||||
'cita_cita' => 'Manager Perusahaan',
|
||||
'prestasi' => 'Peserta Case Study',
|
||||
],
|
||||
[
|
||||
'name' => 'Irma Santika',
|
||||
'email' => 'irma.santika@bima.student',
|
||||
'kelompok_asal' => 'IPS',
|
||||
'values' => ['ekonomi' => 75, 'geografi' => 92, 'sosiologi' => 85, 'sejarah' => 88],
|
||||
'minat' => 'Budaya & Komunikasi',
|
||||
'pref_studi' => 'Sosial & Humaniora',
|
||||
'cita_cita' => 'Jurnalis',
|
||||
'prestasi' => 'Penulis artikel',
|
||||
],
|
||||
[
|
||||
'name' => 'Joko Supriyanto',
|
||||
'email' => 'joko.supriyanto@bima.student',
|
||||
'kelompok_asal' => 'IPS',
|
||||
'values' => ['ekonomi' => 80, 'geografi' => 75, 'sosiologi' => 88, 'sejarah' => 82],
|
||||
'minat' => 'Sosial & Masyarakat',
|
||||
'pref_studi' => 'Sosial & Humaniora',
|
||||
'cita_cita' => 'Pekerja Sosial',
|
||||
'prestasi' => 'Aktif di kegiatan sosial',
|
||||
],
|
||||
];
|
||||
|
||||
$majorNames = PolijeMajor::pluck('nama_jurusan')->toArray();
|
||||
|
||||
foreach ($studentsData as $data) {
|
||||
// Cek apakah siswa sudah ada
|
||||
$existingUser = User::where('email', $data['email'])->first();
|
||||
|
||||
if ($existingUser) {
|
||||
$this->command->warn("⚠ Siswa {$data['name']} ({$data['email']}) sudah ada di sistem, skip...");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Buat user siswa baru
|
||||
$user = User::create([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => bcrypt('password'), // Password default
|
||||
'role' => 'siswa',
|
||||
'kelompok_asal' => $data['kelompok_asal'],
|
||||
]);
|
||||
|
||||
// Hitung skor rekomendasi berdasarkan bobot mapel
|
||||
$scores = $this->calculateScores($data, $majorNames);
|
||||
|
||||
// Ambil top 3 dengan skor tertinggi
|
||||
arsort($scores);
|
||||
$topRecommendations = array_slice($scores, 0, 3, true);
|
||||
|
||||
// Buat data rekomendasi dengan ranking
|
||||
$recommendations = [];
|
||||
foreach ($topRecommendations as $majorName => $score) {
|
||||
$recommendations[] = [
|
||||
'jurusan' => $majorName,
|
||||
'skor' => round($score, 2),
|
||||
'detail' => "Rekomendasi berdasarkan nilai, minat, dan preferensi studi siswa.",
|
||||
];
|
||||
}
|
||||
|
||||
// Simpan rekomendasi ke database
|
||||
Recommendation::create([
|
||||
'user_id' => $user->id,
|
||||
'mtk' => $data['values']['mtk'] ?? 0,
|
||||
'fisika' => $data['values']['fisika'] ?? 0,
|
||||
'kimia' => $data['values']['kimia'] ?? 0,
|
||||
'biologi' => $data['values']['biologi'] ?? 0,
|
||||
'ekonomi' => $data['values']['ekonomi'] ?? 0,
|
||||
'sejarah' => $data['values']['sejarah'] ?? 0,
|
||||
'geografi' => $data['values']['geografi'] ?? 0,
|
||||
'sosiologi' => $data['values']['sosiologi'] ?? 0,
|
||||
'minat' => $data['minat'],
|
||||
'preferensi_studi' => $data['pref_studi'],
|
||||
'cita_cita' => $data['cita_cita'],
|
||||
'prestasi' => $data['prestasi'],
|
||||
'hasil_rekomendasi' => json_encode($recommendations),
|
||||
]);
|
||||
|
||||
$this->command->info("✅ Siswa {$data['name']} ({$data['email']}) ditambahkan dengan rekomendasi jurusan.");
|
||||
}
|
||||
|
||||
$this->command->info("\n✅ Impor data siswa SMK Bima Ambulu selesai!");
|
||||
}
|
||||
|
||||
private function calculateScores(array $studentData, array $majorNames): array
|
||||
{
|
||||
$scores = [];
|
||||
$majors = PolijeMajor::all()->keyBy('nama_jurusan');
|
||||
|
||||
foreach ($majorNames as $majorName) {
|
||||
if (!isset($majors[$majorName])) continue;
|
||||
|
||||
$major = $majors[$majorName];
|
||||
$bobot = $major->bobot_mapel;
|
||||
$score = 0;
|
||||
|
||||
// Hitung skor berdasarkan bobot dan nilai siswa
|
||||
foreach ($bobot as $subject => $weight) {
|
||||
$value = $studentData['values'][$subject] ?? 0;
|
||||
$score += ($value * $weight) / 100;
|
||||
}
|
||||
|
||||
// Bonus untuk preferensi studi yang sesuai
|
||||
if (in_array($studentData['pref_studi'], $major->preferensi_studi)) {
|
||||
$score += 5;
|
||||
}
|
||||
|
||||
$scores[$majorName] = $score;
|
||||
}
|
||||
|
||||
return $scores;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class BkSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$bkUsers = [
|
||||
['email' => 'gurubk1@polije.ac.id', 'name' => 'Konselor BK 1', 'password' => 'bk123456'],
|
||||
['email' => 'gurubk2@polije.ac.id', 'name' => 'Konselor BK 2', 'password' => 'bk234567'],
|
||||
['email' => 'gurubk3@polije.ac.id', 'name' => 'Konselor BK 3', 'password' => 'bk345678'],
|
||||
];
|
||||
|
||||
foreach ($bkUsers as $u) {
|
||||
User::firstOrCreate(
|
||||
['email' => $u['email']],
|
||||
[
|
||||
'name' => $u['name'],
|
||||
'password' => Hash::make($u['password']),
|
||||
'role' => 'bk',
|
||||
'email_verified_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
echo "Created or exists: {$u['email']} / {$u['password']}\n";
|
||||
}
|
||||
|
||||
echo "✅ 3 BK accounts seeded.\n";
|
||||
}
|
||||
}
|
||||
|
|
@ -12,11 +12,12 @@ class DatabaseSeeder extends Seeder
|
|||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// \App\Models\User::factory(10)->create();
|
||||
|
||||
// \App\Models\User::factory()->create([
|
||||
// 'name' => 'Test User',
|
||||
// 'email' => 'test@example.com',
|
||||
// ]);
|
||||
// Seeder inti untuk kebutuhan aplikasi
|
||||
// Jalankan otomatis saat migrate:fresh --seed
|
||||
$this->call([
|
||||
AdminSeeder::class,
|
||||
PolijeMajorSeeder::class,
|
||||
// AlumniSeeder::class, // Aktifkan jika butuh data evaluasi alumni
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Recommendation;
|
||||
use App\Models\ChatHistory;
|
||||
use App\Models\PolijeMajor;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Generate30StudentsSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Generate 30 students dengan data, recommendations, dan chat history
|
||||
* Run: php artisan db:seed --class=Generate30StudentsSeeder
|
||||
*/
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$studentsData = [
|
||||
// KELOMPOK IPA - 15 siswa
|
||||
['name' => 'Akmal Fiqri', 'email' => 'akmal.fiqri@school.id', 'kelompok' => 'IPA', 'mtk' => 85, 'fisika' => 82, 'kimia' => 75, 'biologi' => 70, 'minat' => 'Teknologi & Programming'],
|
||||
['name' => 'Sasha Putri Aulia', 'email' => 'sasha.putri@school.id', 'kelompok' => 'IPA', 'mtk' => 78, 'fisika' => 80, 'kimia' => 85, 'biologi' => 82, 'minat' => 'Kesehatan & Biologi'],
|
||||
['name' => 'Budi Kusuma', 'email' => 'budi.kusuma@school.id', 'kelompok' => 'IPA', 'mtk' => 82, 'fisika' => 88, 'kimia' => 76, 'biologi' => 68, 'minat' => 'Teknik & Mesin'],
|
||||
['name' => 'Dina Hartini', 'email' => 'dina.hartini@school.id', 'kelompok' => 'IPA', 'mtk' => 75, 'fisika' => 72, 'kimia' => 88, 'biologi' => 90, 'minat' => 'Farmasi & Kimia'],
|
||||
['name' => 'Eka Prasetyo', 'email' => 'eka.prasetyo@school.id', 'kelompok' => 'IPA', 'mtk' => 88, 'fisika' => 85, 'kimia' => 72, 'biologi' => 75, 'minat' => 'Pemrograman & Coding'],
|
||||
['name' => 'Fitra Handoko', 'email' => 'fitra.handoko@school.id', 'kelompok' => 'IPA', 'mtk' => 92, 'fisika' => 90, 'kimia' => 88, 'biologi' => 80, 'minat' => 'Sains & Penelitian'],
|
||||
['name' => 'Gita Pramesti', 'email' => 'gita.pramesti@school.id', 'kelompok' => 'IPA', 'mtk' => 80, 'fisika' => 78, 'kimia' => 82, 'biologi' => 85, 'minat' => 'Bioteknologi'],
|
||||
['name' => 'Hendra Wijaya', 'email' => 'hendra.wijaya@school.id', 'kelompok' => 'IPA', 'mtk' => 86, 'fisika' => 84, 'kimia' => 80, 'biologi' => 72, 'minat' => 'Fisika & Teknologi'],
|
||||
['name' => 'Intan Sari', 'email' => 'intan.sari@school.id', 'kelompok' => 'IPA', 'mtk' => 79, 'fisika' => 81, 'kimia' => 83, 'biologi' => 88, 'minat' => 'Kesehatan'],
|
||||
['name' => 'Jaka Surya', 'email' => 'jaka.surya@school.id', 'kelompok' => 'IPA', 'mtk' => 83, 'fisika' => 86, 'kimia' => 78, 'biologi' => 70, 'minat' => 'Teknik Elektro'],
|
||||
['name' => 'Kurniawan Adi', 'email' => 'kurniawan.adi@school.id', 'kelompok' => 'IPA', 'mtk' => 87, 'fisika' => 88, 'kimia' => 85, 'biologi' => 76, 'minat' => 'Teknik Sipil'],
|
||||
['name' => 'Latifa Zahra', 'email' => 'latifa.zahra@school.id', 'kelompok' => 'IPA', 'mtk' => 81, 'fisika' => 80, 'kimia' => 87, 'biologi' => 84, 'minat' => 'Kedokteran'],
|
||||
['name' => 'Maulana Rizki', 'email' => 'maulana.rizki@school.id', 'kelompok' => 'IPA', 'mtk' => 84, 'fisika' => 83, 'kimia' => 81, 'biologi' => 73, 'minat' => 'Teknologi Informasi'],
|
||||
['name' => 'Nisa Rahmawati', 'email' => 'nisa.rahmawati@school.id', 'kelompok' => 'IPA', 'mtk' => 77, 'fisika' => 79, 'kimia' => 84, 'biologi' => 89, 'minat' => 'Perawatan Kesehatan'],
|
||||
['name' => 'Oki Pratama', 'email' => 'oki.pratama@school.id', 'kelompok' => 'IPA', 'mtk' => 89, 'fisika' => 87, 'kimia' => 84, 'biologi' => 71, 'minat' => 'Energi Terbarukan'],
|
||||
|
||||
// KELOMPOK IPS - 15 siswa
|
||||
['name' => 'Fahmi Rizki', 'email' => 'fahmi.rizki@school.id', 'kelompok' => 'IPS', 'ekonomi' => 88, 'geografi' => 80, 'sosiologi' => 78, 'sejarah' => 75, 'minat' => 'Bisnis & Keuangan'],
|
||||
['name' => 'Gina Melani', 'email' => 'gina.melani@school.id', 'kelompok' => 'IPS', 'ekonomi' => 85, 'geografi' => 88, 'sosiologi' => 82, 'sejarah' => 80, 'minat' => 'Pariwisata & Budaya'],
|
||||
['name' => 'Hasan Wijaya', 'email' => 'hasan.wijaya@school.id', 'kelompok' => 'IPS', 'ekonomi' => 82, 'geografi' => 85, 'sosiologi' => 80, 'sejarah' => 78, 'minat' => 'Manajemen & Administrasi'],
|
||||
['name' => 'Irma Santika', 'email' => 'irma.santika@school.id', 'kelompok' => 'IPS', 'ekonomi' => 75, 'geografi' => 92, 'sosiologi' => 85, 'sejarah' => 88, 'minat' => 'Budaya & Komunikasi'],
|
||||
['name' => 'Joko Supriyanto', 'email' => 'joko.supriyanto@school.id', 'kelompok' => 'IPS', 'ekonomi' => 80, 'geografi' => 75, 'sosiologi' => 88, 'sejarah' => 82, 'minat' => 'Sosial & Masyarakat'],
|
||||
['name' => 'Kartika Dewi', 'email' => 'kartika.dewi@school.id', 'kelompok' => 'IPS', 'ekonomi' => 87, 'geografi' => 84, 'sosiologi' => 80, 'sejarah' => 82, 'minat' => 'Akuntansi & Keuangan'],
|
||||
['name' => 'Lucia Pratiwi', 'email' => 'lucia.pratiwi@school.id', 'kelompok' => 'IPS', 'ekonomi' => 83, 'geografi' => 86, 'sosiologi' => 84, 'sejarah' => 85, 'minat' => 'Ilmu Komunikasi'],
|
||||
['name' => 'Mahmud Ali', 'email' => 'mahmud.ali@school.id', 'kelompok' => 'IPS', 'ekonomi' => 81, 'geografi' => 78, 'sosiologi' => 85, 'sejarah' => 80, 'minat' => 'Hukum & Keadilan'],
|
||||
['name' => 'Nadya Salsabila', 'email' => 'nadya.salsabila@school.id', 'kelompok' => 'IPS', 'ekonomi' => 84, 'geografi' => 88, 'sosiologi' => 81, 'sejarah' => 79, 'minat' => 'Perhotelan'],
|
||||
['name' => 'Oman Sutrisno', 'email' => 'oman.sutrisno@school.id', 'kelompok' => 'IPS', 'ekonomi' => 86, 'geografi' => 82, 'sosiologi' => 79, 'sejarah' => 81, 'minat' => 'Bisnis Internasional'],
|
||||
['name' => 'Putri Handini', 'email' => 'putri.handini@school.id', 'kelompok' => 'IPS', 'ekonomi' => 80, 'geografi' => 87, 'sosiologi' => 83, 'sejarah' => 84, 'minat' => 'Pendidikan & Pengajaran'],
|
||||
['name' => 'Qohaf Ramadhan', 'email' => 'qohaf.ramadhan@school.id', 'kelompok' => 'IPS', 'ekonomi' => 82, 'geografi' => 79, 'sosiologi' => 86, 'sejarah' => 83, 'minat' => 'Pemerintahan'],
|
||||
['name' => 'Rita Kusuma', 'email' => 'rita.kusuma@school.id', 'kelompok' => 'IPS', 'ekonomi' => 85, 'geografi' => 86, 'sosiologi' => 82, 'sejarah' => 81, 'minat' => 'Diplomasi & Hubungan Internasional'],
|
||||
['name' => 'Satrio Budi', 'email' => 'satrio.budi@school.id', 'kelompok' => 'IPS', 'ekonomi' => 79, 'geografi' => 83, 'sosiologi' => 88, 'sejarah' => 85, 'minat' => 'Jurnalisme'],
|
||||
['name' => 'Tyas Merika', 'email' => 'tyas.merika@school.id', 'kelompok' => 'IPS', 'ekonomi' => 88, 'geografi' => 85, 'sosiologi' => 80, 'sejarah' => 78, 'minat' => 'Asuransi & Perbankan'],
|
||||
];
|
||||
|
||||
$chatMessages = [
|
||||
'Halo, apa yang harus saya lakukan untuk memilih jurusan yang tepat?',
|
||||
'Saya bingung antara dua pilihan jurusan yang berbeda.',
|
||||
'Bagaimana prospek kerja dari jurusan yang Anda rekomendasikan?',
|
||||
'Apakah ada beasiswa untuk jurusan ini?',
|
||||
'Apa saja persyaratan untuk masuk ke jurusan tersebut?',
|
||||
'Berapa lama program studi ini?',
|
||||
'Apa saja mata kuliah yang akan saya pelajari?',
|
||||
'Saya tertarik dengan teknologi, jurusan apa yang cocok?',
|
||||
'Bagaimana dengan kesempatan magang?',
|
||||
'Saya ingin bekerja di luar negeri, jurusan apa yang sesuai?',
|
||||
];
|
||||
|
||||
$majoritasNames = PolijeMajor::pluck('nama_jurusan')->toArray();
|
||||
|
||||
if (empty($majoritasNames)) {
|
||||
$this->command->error('❌ Tidak ada jurusan di database. Jalankan seeder jurusan terlebih dahulu!');
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($studentsData as $index => $data) {
|
||||
// Cek email unik
|
||||
if (User::where('email', $data['email'])->exists()) {
|
||||
$this->command->warn("⚠️ Email {$data['email']} sudah ada, skip...");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create Student User
|
||||
$user = User::create([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => bcrypt('student123'),
|
||||
'role' => 'siswa',
|
||||
'nis' => '20240' . str_pad($index + 1, 3, '0', STR_PAD_LEFT),
|
||||
'kelompok_asal' => $data['kelompok'],
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
$this->command->line("✅ Created student: {$data['name']} ({$data['email']})");
|
||||
|
||||
// Create Recommendation
|
||||
if ($data['kelompok'] === 'IPA') {
|
||||
$matematika = $data['mtk'];
|
||||
$fisika = $data['fisika'];
|
||||
$kimia = $data['kimia'];
|
||||
$biologi = $data['biologi'];
|
||||
} else {
|
||||
$ekonomi = $data['ekonomi'];
|
||||
$geografi = $data['geografi'];
|
||||
$sosiologi = $data['sosiologi'];
|
||||
$sejarah = $data['sejarah'];
|
||||
}
|
||||
|
||||
$hasRecommendation = Recommendation::where('user_id', $user->id)->exists();
|
||||
|
||||
if (!$hasRecommendation) {
|
||||
// Simulate recommendation algorithm scores
|
||||
$scores = array_fill(0, count($majoritasNames), 0);
|
||||
|
||||
// Random weighted scoring
|
||||
foreach ($scores as $key => $score) {
|
||||
$scores[$key] = rand(55, 95);
|
||||
}
|
||||
|
||||
// Top 9 recommendations
|
||||
arsort($scores);
|
||||
$topScores = array_slice($scores, 0, 9, true);
|
||||
|
||||
$hasilRekomendasi = [];
|
||||
$rank = 1;
|
||||
foreach ($topScores as $majorIndex => $score) {
|
||||
$hasilRekomendasi[] = [
|
||||
'ranking' => $rank,
|
||||
'jurusan' => $majoritasNames[$majorIndex] ?? 'Teknologi Informasi',
|
||||
'skor' => round($score),
|
||||
];
|
||||
$rank++;
|
||||
}
|
||||
|
||||
$recommendation = Recommendation::create([
|
||||
'user_id' => $user->id,
|
||||
'mtk' => $data['mtk'] ?? null,
|
||||
'fisika' => $data['fisika'] ?? null,
|
||||
'kimia' => $data['kimia'] ?? null,
|
||||
'biologi' => $data['biologi'] ?? null,
|
||||
'ekonomi' => $data['ekonomi'] ?? null,
|
||||
'geografi' => $data['geografi'] ?? null,
|
||||
'sosiologi' => $data['sosiologi'] ?? null,
|
||||
'sejarah' => $data['sejarah'] ?? null,
|
||||
'minat' => $data['minat'] ?? null,
|
||||
'preferensi_studi' => $data['pref_studi'] ?? ($data['kelompok'] === 'IPA' ? 'Sains & Teknologi' : 'Sosial & Humaniora'),
|
||||
'cita_cita' => $data['cita_cita'] ?? ($data['minat'] ?? null),
|
||||
'prestasi' => $data['prestasi'] ?? 'Aktif',
|
||||
'hasil_rekomendasi' => json_encode($hasilRekomendasi),
|
||||
]);
|
||||
|
||||
$this->command->info(" 📊 Recommendation created (id: {$recommendation->id})");
|
||||
}
|
||||
|
||||
// Create Chat Histories
|
||||
$existingChats = ChatHistory::where('user_id', $user->id)->count();
|
||||
|
||||
if ($existingChats === 0) {
|
||||
for ($i = 0; $i < rand(2, 5); $i++) {
|
||||
$randomMessage = $chatMessages[array_rand($chatMessages)];
|
||||
|
||||
ChatHistory::create([
|
||||
'user_id' => $user->id,
|
||||
'id_sesi' => null,
|
||||
'id_rekomendasi' => $recommendation->id ?? null,
|
||||
'pertanyaan' => $randomMessage,
|
||||
'jawaban' => 'Terima kasih atas pertanyaannya. Berdasarkan profil akademik dan minat Anda, saya merekomendasikan jurusan yang sesuai dengan kemampuan dan tujuan karir Anda.',
|
||||
]);
|
||||
}
|
||||
|
||||
$this->command->info(" 💬 Chat histories created (2-5 chats)");
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info("\n✨ Seeder selesai! 30 students + recommendations + chat histories berhasil dibuat.\n");
|
||||
}
|
||||
}
|
||||
|
|
@ -12,100 +12,190 @@ public function run(): void
|
|||
$jurusans = [
|
||||
[
|
||||
'nama_jurusan' => 'Produksi Pertanian',
|
||||
'deskripsi' => 'Jurusan yang mempelajari teknik budidaya tanaman, pengelolaan lahan pertanian, dan produksi hasil pertanian secara modern.',
|
||||
'keywords' => ['pertanian', 'petani', 'kebun', 'sawah', 'panen', 'tanaman', 'budidaya', 'agronomi', 'tanam', 'bercocok tanam', 'alam', 'hortikultura', 'pupuk', 'bibit'],
|
||||
'deskripsi' => 'Jurusan yang mempelajari budidaya tanaman, pengelolaan lahan, dan produksi hasil pertanian modern, termasuk bidang turunan yang terkait dengan agronomi, pangan, dan lingkungan.',
|
||||
'keywords' => ['pertanian', 'petani', 'kebun', 'sawah', 'panen', 'tanaman', 'budidaya', 'agronomi', 'tanam', 'bercocok tanam', 'alam', 'hortikultura', 'pupuk', 'bibit', 'agroteknologi', 'perkebunan', 'pangan', 'ketahanan pangan', 'hidroponik', 'organik'],
|
||||
'preferensi_studi' => ['Pertanian & Lingkungan'],
|
||||
'bobot_mapel' => [
|
||||
'biologi' => 0.40, 'kimia' => 0.30, 'fisika' => 0.15, 'mtk' => 0.15,
|
||||
'geografi' => 0.35, 'ekonomi' => 0.30, 'sosiologi' => 0.20, 'sejarah' => 0.15,
|
||||
'ipa' => [
|
||||
'mtk' => 0.15,
|
||||
'fisika' => 0.10,
|
||||
'kimia' => 0.30,
|
||||
'biologi' => 0.45,
|
||||
],
|
||||
'ips' => [
|
||||
'ekonomi' => 0.15,
|
||||
'geografi' => 0.35,
|
||||
'sosiologi' => 0.20,
|
||||
'sejarah' => 0.30,
|
||||
],
|
||||
],
|
||||
'prospek_kerja' => 'Petani modern, konsultan pertanian, pengelola perkebunan, peneliti pertanian, agronomis.',
|
||||
],
|
||||
[
|
||||
'nama_jurusan' => 'Teknologi Pertanian',
|
||||
'deskripsi' => 'Jurusan yang mengintegrasikan teknologi dengan pertanian, meliputi mekanisasi pertanian, pengolahan hasil pertanian, dan inovasi teknologi pangan.',
|
||||
'keywords' => ['teknologi pertanian', 'mesin pertanian', 'inovasi', 'otomasi', 'pengolahan pangan', 'pangan', 'mekanisasi', 'teknologi pangan', 'alat pertanian', 'rekayasa'],
|
||||
'deskripsi' => 'Jurusan yang mengintegrasikan teknologi dengan pertanian, meliputi mekanisasi, pengolahan hasil, otomasi, dan inovasi teknologi pangan serta sistem produksi modern.',
|
||||
'keywords' => ['teknologi pertanian', 'mesin pertanian', 'inovasi', 'otomasi', 'pengolahan pangan', 'pangan', 'mekanisasi', 'teknologi pangan', 'alat pertanian', 'rekayasa', 'iot pertanian', 'smart farming', 'digital farming', 'kontrol kualitas', 'proses produksi'],
|
||||
'preferensi_studi' => ['Sains & Teknologi', 'Pertanian & Lingkungan'],
|
||||
'bobot_mapel' => [
|
||||
'fisika' => 0.35, 'mtk' => 0.30, 'kimia' => 0.20, 'biologi' => 0.15,
|
||||
'ekonomi' => 0.30, 'geografi' => 0.30, 'sosiologi' => 0.20, 'sejarah' => 0.20,
|
||||
'ipa' => [
|
||||
'mtk' => 0.30,
|
||||
'fisika' => 0.25,
|
||||
'kimia' => 0.20,
|
||||
'biologi' => 0.25,
|
||||
],
|
||||
'ips' => [
|
||||
'ekonomi' => 0.20,
|
||||
'geografi' => 0.30,
|
||||
'sosiologi' => 0.25,
|
||||
'sejarah' => 0.25,
|
||||
],
|
||||
],
|
||||
'prospek_kerja' => 'Teknisi pertanian, ahli mekanisasi, quality control pangan, peneliti teknologi pangan.',
|
||||
],
|
||||
[
|
||||
'nama_jurusan' => 'Peternakan',
|
||||
'deskripsi' => 'Jurusan yang mempelajari pengelolaan dan pemeliharaan ternak, nutrisi hewan, reproduksi, dan pengolahan produk peternakan.',
|
||||
'keywords' => ['ternak', 'hewan', 'peternakan', 'peternak', 'sapi', 'ayam', 'unggas', 'kambing', 'susu', 'pakan', 'nutrisi hewan', 'veteriner', 'ikan', 'aquaculture'],
|
||||
'deskripsi' => 'Jurusan yang mempelajari pengelolaan ternak, nutrisi hewan, reproduksi, dan pengolahan produk peternakan, termasuk kewirausahaan dan teknologi peternakan terapan.',
|
||||
'keywords' => ['ternak', 'hewan', 'peternakan', 'peternak', 'sapi', 'ayam', 'unggas', 'kambing', 'susu', 'pakan', 'nutrisi hewan', 'veteriner', 'ikan', 'aquaculture', 'budidaya hewan', 'farm management', 'kesehatan hewan', 'produksi ternak'],
|
||||
'preferensi_studi' => ['Pertanian & Lingkungan', 'Kesehatan & Ilmu Hayat'],
|
||||
'bobot_mapel' => [
|
||||
'biologi' => 0.45, 'kimia' => 0.25, 'fisika' => 0.15, 'mtk' => 0.15,
|
||||
'geografi' => 0.30, 'ekonomi' => 0.30, 'sosiologi' => 0.20, 'sejarah' => 0.20,
|
||||
'ipa' => [
|
||||
'mtk' => 0.15,
|
||||
'fisika' => 0.10,
|
||||
'kimia' => 0.25,
|
||||
'biologi' => 0.50,
|
||||
],
|
||||
'ips' => [
|
||||
'ekonomi' => 0.15,
|
||||
'geografi' => 0.25,
|
||||
'sosiologi' => 0.20,
|
||||
'sejarah' => 0.40,
|
||||
],
|
||||
],
|
||||
'prospek_kerja' => 'Peternak profesional, konsultan peternakan, manajer peternakan, ahli nutrisi hewan.',
|
||||
],
|
||||
[
|
||||
'nama_jurusan' => 'Manajemen Agribisnis',
|
||||
'deskripsi' => 'Jurusan yang menggabungkan ilmu pertanian dan bisnis, meliputi pemasaran hasil pertanian, manajemen usaha tani, dan kewirausahaan agribisnis.',
|
||||
'keywords' => ['bisnis', 'agribisnis', 'usaha', 'entrepreneur', 'pengusaha', 'dagang', 'jual', 'pemasaran', 'kewirausahaan', 'manajemen', 'ekonomi pertanian', 'pasar'],
|
||||
'deskripsi' => 'Jurusan yang menggabungkan ilmu pertanian dan bisnis, meliputi manajemen usaha, pemasaran hasil, rantai pasok, dan kewirausahaan agribisnis.',
|
||||
'keywords' => ['bisnis', 'agribisnis', 'usaha', 'entrepreneur', 'pengusaha', 'dagang', 'jual', 'pemasaran', 'kewirausahaan', 'manajemen', 'ekonomi pertanian', 'pasar', 'supply chain', 'logistik', 'analisis pasar', 'branding produk'],
|
||||
'preferensi_studi' => ['Bisnis & Manajemen', 'Pertanian & Lingkungan'],
|
||||
'bobot_mapel' => [
|
||||
'mtk' => 0.35, 'biologi' => 0.25, 'kimia' => 0.20, 'fisika' => 0.20,
|
||||
'ekonomi' => 0.45, 'geografi' => 0.20, 'sosiologi' => 0.20, 'sejarah' => 0.15,
|
||||
'ipa' => [
|
||||
'mtk' => 0.30,
|
||||
'fisika' => 0.15,
|
||||
'kimia' => 0.15,
|
||||
'biologi' => 0.40,
|
||||
],
|
||||
'ips' => [
|
||||
'ekonomi' => 0.35,
|
||||
'geografi' => 0.20,
|
||||
'sosiologi' => 0.25,
|
||||
'sejarah' => 0.20,
|
||||
],
|
||||
],
|
||||
'prospek_kerja' => 'Manajer agribisnis, entrepreneur pertanian, konsultan pemasaran pertanian, analis pasar komoditas.',
|
||||
],
|
||||
[
|
||||
'nama_jurusan' => 'Teknologi Informasi',
|
||||
'deskripsi' => 'Jurusan yang mempelajari pengembangan perangkat lunak, jaringan komputer, keamanan siber, dan teknologi digital.',
|
||||
'keywords' => ['programmer', 'developer', 'coding', 'software', 'web', 'aplikasi', 'komputer', 'it', 'jaringan', 'hacker', 'game', 'data', 'ai', 'robot', 'ngoding', 'laptop', 'teknologi', 'digital', 'internet', 'programming', 'desain grafis'],
|
||||
'deskripsi' => 'Jurusan yang mempelajari pengembangan perangkat lunak, data, jaringan, keamanan siber, dan ekosistem teknologi digital, termasuk bidang turunan komputasi terapan.',
|
||||
'keywords' => ['programmer', 'developer', 'coding', 'software', 'web', 'aplikasi', 'komputer', 'it', 'jaringan', 'hacker', 'game', 'data', 'ai', 'robot', 'ngoding', 'laptop', 'teknologi', 'digital', 'internet', 'programming', 'desain grafis', 'ui ux', 'mobile app', 'cloud', 'database', 'machine learning'],
|
||||
'preferensi_studi' => ['Sains & Teknologi'],
|
||||
'bobot_mapel' => [
|
||||
'mtk' => 0.45, 'fisika' => 0.25, 'kimia' => 0.15, 'biologi' => 0.15,
|
||||
'ekonomi' => 0.25, 'geografi' => 0.20, 'sosiologi' => 0.25, 'sejarah' => 0.30,
|
||||
'ipa' => [
|
||||
'mtk' => 0.45,
|
||||
'fisika' => 0.30,
|
||||
'kimia' => 0.15,
|
||||
'biologi' => 0.10,
|
||||
],
|
||||
'ips' => [
|
||||
'ekonomi' => 0.40,
|
||||
'geografi' => 0.15,
|
||||
'sosiologi' => 0.20,
|
||||
'sejarah' => 0.25,
|
||||
],
|
||||
],
|
||||
'prospek_kerja' => 'Software developer, web developer, network engineer, data analyst, cybersecurity specialist.',
|
||||
],
|
||||
[
|
||||
'nama_jurusan' => 'Teknik',
|
||||
'deskripsi' => 'Jurusan yang mempelajari mesin, kelistrikan, elektronika, dan otomasi industri.',
|
||||
'keywords' => ['mesin', 'bengkel', 'listrik', 'las', 'robot', 'motor', 'teknik', 'otomasi', 'elektronik', 'instalasi', 'panel', 'mekanik', 'industri', 'manufaktur', 'pabrik', 'bangunan', 'konstruksi', 'sipil', 'energi'],
|
||||
'deskripsi' => 'Jurusan yang mempelajari mesin, kelistrikan, elektronika, otomasi, dan sistem teknik industri dengan pendekatan praktik dan pemecahan masalah teknis.',
|
||||
'keywords' => ['mesin', 'bengkel', 'listrik', 'las', 'robot', 'motor', 'teknik', 'otomasi', 'elektronik', 'instalasi', 'panel', 'mekanik', 'industri', 'manufaktur', 'pabrik', 'bangunan', 'konstruksi', 'sipil', 'energi', 'maintenance', 'mekatronika', 'instrumentasi', 'quality control'],
|
||||
'preferensi_studi' => ['Sains & Teknologi'],
|
||||
'bobot_mapel' => [
|
||||
'fisika' => 0.40, 'mtk' => 0.35, 'kimia' => 0.15, 'biologi' => 0.10,
|
||||
'ekonomi' => 0.25, 'geografi' => 0.25, 'sosiologi' => 0.20, 'sejarah' => 0.30,
|
||||
'ipa' => [
|
||||
'mtk' => 0.35,
|
||||
'fisika' => 0.40,
|
||||
'kimia' => 0.15,
|
||||
'biologi' => 0.10,
|
||||
],
|
||||
'ips' => [
|
||||
'ekonomi' => 0.30,
|
||||
'geografi' => 0.20,
|
||||
'sosiologi' => 0.20,
|
||||
'sejarah' => 0.30,
|
||||
],
|
||||
],
|
||||
'prospek_kerja' => 'Teknisi mesin, ahli listrik, engineer industri, maintenance engineer, kontraktor.',
|
||||
],
|
||||
[
|
||||
'nama_jurusan' => 'Kesehatan',
|
||||
'deskripsi' => 'Jurusan yang mempelajari ilmu kesehatan, gizi, rekam medis, dan pelayanan kesehatan masyarakat.',
|
||||
'keywords' => ['dokter', 'perawat', 'medis', 'gizi', 'kesehatan', 'pelayanan', 'terapis', 'obat', 'rumah sakit', 'klinik', 'farmasi', 'nutrisi', 'sanitasi', 'rawat', 'sehat'],
|
||||
'deskripsi' => 'Jurusan yang mempelajari ilmu kesehatan terapan, gizi, rekam medis, dan pelayanan kesehatan masyarakat, termasuk aspek promotif dan preventif.',
|
||||
'keywords' => ['dokter', 'perawat', 'medis', 'gizi', 'kesehatan', 'pelayanan', 'terapis', 'obat', 'rumah sakit', 'klinik', 'farmasi', 'nutrisi', 'sanitasi', 'rawat', 'sehat', 'kesehatan masyarakat', 'laboratorium', 'diagnostik', 'wellness'],
|
||||
'preferensi_studi' => ['Kesehatan & Ilmu Hayat'],
|
||||
'bobot_mapel' => [
|
||||
'biologi' => 0.40, 'kimia' => 0.35, 'mtk' => 0.15, 'fisika' => 0.10,
|
||||
'sosiologi' => 0.30, 'ekonomi' => 0.25, 'geografi' => 0.25, 'sejarah' => 0.20,
|
||||
'ipa' => [
|
||||
'mtk' => 0.15,
|
||||
'fisika' => 0.10,
|
||||
'kimia' => 0.35,
|
||||
'biologi' => 0.40,
|
||||
],
|
||||
'ips' => [
|
||||
'ekonomi' => 0.15,
|
||||
'geografi' => 0.25,
|
||||
'sosiologi' => 0.25,
|
||||
'sejarah' => 0.35,
|
||||
],
|
||||
],
|
||||
'prospek_kerja' => 'Ahli gizi, perekam medis, tenaga kesehatan, asisten apoteker, sanitarian.',
|
||||
],
|
||||
[
|
||||
'nama_jurusan' => 'Bahasa, Komunikasi, dan Pariwisata',
|
||||
'deskripsi' => 'Jurusan yang mempelajari bahasa asing, komunikasi, perhotelan, dan industri pariwisata.',
|
||||
'keywords' => ['bahasa', 'komunikasi', 'pariwisata', 'tour guide', 'hotel', 'jurnalis', 'marketing', 'inggris', 'penerjemah', 'travel', 'wisata', 'hospitality', 'public speaking', 'media', 'broadcasting'],
|
||||
'deskripsi' => 'Jurusan yang mempelajari bahasa, komunikasi, perhotelan, layanan publik, dan industri pariwisata dengan orientasi pada kompetensi komunikasi profesional.',
|
||||
'keywords' => ['bahasa', 'komunikasi', 'pariwisata', 'tour guide', 'hotel', 'jurnalis', 'marketing', 'inggris', 'penerjemah', 'travel', 'wisata', 'hospitality', 'public speaking', 'media', 'broadcasting', 'content creator', 'humas', 'event', 'pelayanan tamu'],
|
||||
'preferensi_studi' => ['Sosial & Humaniora', 'Bisnis & Manajemen'],
|
||||
'bobot_mapel' => [
|
||||
'biologi' => 0.20, 'kimia' => 0.20, 'fisika' => 0.20, 'mtk' => 0.40,
|
||||
'sosiologi' => 0.30, 'sejarah' => 0.30, 'geografi' => 0.25, 'ekonomi' => 0.15,
|
||||
'ipa' => [
|
||||
'mtk' => 0.35,
|
||||
'fisika' => 0.15,
|
||||
'kimia' => 0.10,
|
||||
'biologi' => 0.40,
|
||||
],
|
||||
'ips' => [
|
||||
'ekonomi' => 0.20,
|
||||
'geografi' => 0.20,
|
||||
'sosiologi' => 0.25,
|
||||
'sejarah' => 0.35,
|
||||
],
|
||||
],
|
||||
'prospek_kerja' => 'Tour guide, staf perhotelan, jurnalis, public relation, penerjemah, staf maskapai.',
|
||||
],
|
||||
[
|
||||
'nama_jurusan' => 'Bisnis',
|
||||
'deskripsi' => 'Jurusan yang mempelajari akuntansi, manajemen bisnis, perbankan, dan administrasi niaga.',
|
||||
'keywords' => ['manager', 'pimpinan', 'bisnis', 'accounting', 'marketing', 'sales', 'kantor', 'keuangan', 'bank', 'akuntansi', 'hitung', 'administrasi', 'perbankan', 'ekonomi', 'uang', 'investasi', 'pajak'],
|
||||
'deskripsi' => 'Jurusan yang mempelajari akuntansi, manajemen bisnis, perbankan, keuangan, dan administrasi niaga, termasuk analisis data bisnis dan pengambilan keputusan.',
|
||||
'keywords' => ['manager', 'pimpinan', 'bisnis', 'accounting', 'marketing', 'sales', 'kantor', 'keuangan', 'bank', 'akuntansi', 'hitung', 'administrasi', 'perbankan', 'ekonomi', 'uang', 'investasi', 'pajak', 'wirausaha', 'audit', 'finance', 'analisis bisnis'],
|
||||
'preferensi_studi' => ['Bisnis & Manajemen'],
|
||||
'bobot_mapel' => [
|
||||
'mtk' => 0.45, 'fisika' => 0.20, 'kimia' => 0.15, 'biologi' => 0.20,
|
||||
'ekonomi' => 0.45, 'sosiologi' => 0.20, 'geografi' => 0.15, 'sejarah' => 0.20,
|
||||
'ipa' => [
|
||||
'mtk' => 0.35,
|
||||
'fisika' => 0.20,
|
||||
'kimia' => 0.10,
|
||||
'biologi' => 0.35,
|
||||
],
|
||||
'ips' => [
|
||||
'ekonomi' => 0.40,
|
||||
'geografi' => 0.15,
|
||||
'sosiologi' => 0.20,
|
||||
'sejarah' => 0.25,
|
||||
],
|
||||
],
|
||||
'prospek_kerja' => 'Akuntan, staf perbankan, manajer bisnis, marketing executive, analis keuangan.',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,331 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Recommendation;
|
||||
use App\Models\ChatHistory;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class RecommendationAndChatSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
// Data rekomendasi detail dengan bobot ROC
|
||||
$recommendationData = [
|
||||
// Siswa IPA - Teknik & IT oriented
|
||||
[
|
||||
'user_id' => 2, // Siswa 1
|
||||
'mtk' => 85, 'fisika' => 88, 'kimia' => 82, 'biologi' => 75,
|
||||
'minat' => 'Suka coding dan bikin aplikasi, interested di cyber security juga',
|
||||
'preferensi_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Jadi software engineer di startup lokal, pengen develop aplikasi yang bermanfaat',
|
||||
'prestasi' => 'Juara 2 Kompetisi Programming tingkat provinsi tahun lalu',
|
||||
],
|
||||
[
|
||||
'user_id' => 3, // Siswa 2
|
||||
'mtk' => 92, 'fisika' => 90, 'kimia' => 88, 'biologi' => 85,
|
||||
'minat' => 'Sangat tertarik dengan robotika dan automation, suka ngutak-atik elektronik',
|
||||
'preferensi_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Ingin bekerja di industri manufaktur sebagai engineer untuk bikin sistem otomasi',
|
||||
'prestasi' => 'Finalis Olimpiade Fisika Nasional, sudah ada 3 paten design robotik',
|
||||
],
|
||||
[
|
||||
'user_id' => 4, // Siswa 3
|
||||
'mtk' => 78, 'fisika' => 81, 'kimia' => 79, 'biologi' => 92,
|
||||
'minat' => 'Passionate tentang bioteknologi dan kesehatan, sering baca tentang genomik',
|
||||
'preferensi_studi' => 'Kesehatan & Ilmu Hayat',
|
||||
'cita_cita' => 'Mau lanjut ke kedokteran atau jadi peneliti di bidang biomedis',
|
||||
'prestasi' => 'Presentasi paper biologi di seminar nasional, pernah volunteer di klinik',
|
||||
],
|
||||
[
|
||||
'user_id' => 5, // Siswa 4
|
||||
'mtk' => 88, 'fisika' => 85, 'kimia' => 90, 'biologi' => 80,
|
||||
'minat' => 'Suka kimia organik, proses manufaktur, dan industri pertanian',
|
||||
'preferensi_studi' => 'Pertanian & Lingkungan',
|
||||
'cita_cita' => 'Bikin startup agritech yang sustainable, pengen improve produktivitas petani lokal',
|
||||
'prestasi' => 'Juara Karya Ilmiah tentang pupuk organik, punya UKM pupuk sendiri',
|
||||
],
|
||||
[
|
||||
'user_id' => 6, // Siswa 5
|
||||
'mtk' => 82, 'fisika' => 84, 'kimia' => 86, 'biologi' => 88,
|
||||
'minat' => 'Selalu tertarik dengan peternakan modern dan nutrisi hewan',
|
||||
'preferensi_studi' => 'Pertanian & Lingkungan',
|
||||
'cita_cita' => 'Jadi manager farm modern atau ahli nutrisi ternak, pengen export hasil',
|
||||
'prestasi' => 'Punya kandang ternak sapi yang sudah operational, mengikuti workshop peternakan',
|
||||
],
|
||||
|
||||
// Siswa IPS - Bisnis & Sosial oriented
|
||||
[
|
||||
'user_id' => 7, // Siswa 6
|
||||
'ekonomi' => 85, 'geografi' => 88, 'sosiologi' => 80, 'sejarah' => 78,
|
||||
'minat' => 'Suka analisa bisnis dan strategi marketing, aktif di club entrepreneur',
|
||||
'preferensi_studi' => 'Bisnis & Manajemen',
|
||||
'cita_cita' => 'Mau founder perusahaan logistik yang bisa layani seluruh Indonesia',
|
||||
'prestasi' => 'Ketua OSIS 2 tahun, udah coba business plan shipping dengan teman',
|
||||
],
|
||||
[
|
||||
'user_id' => 8, // Siswa 7
|
||||
'ekonomi' => 90, 'geografi' => 85, 'sosiologi' => 88, 'sejarah' => 82,
|
||||
'minat' => 'Tertarik accounting dan finance, suka ngitung dan detail',
|
||||
'preferensi_studi' => 'Bisnis & Manajemen',
|
||||
'cita_cita' => 'Jadi akuntan profesional dan buka kantor akuntan sendiri',
|
||||
'prestasi' => 'Juara Lomba Akuntansi, sudah belajar MYOB dan software akuntansi',
|
||||
],
|
||||
[
|
||||
'user_id' => 9, // Siswa 8
|
||||
'ekonomi' => 78, 'geografi' => 82, 'sosiologi' => 92, 'sejarah' => 88,
|
||||
'minat' => 'Sangat peduli sosial dan keadilan, aktif di organisasi kemanusiaan',
|
||||
'preferensi_studi' => 'Sosial & Humaniora',
|
||||
'cita_cita' => 'Jadi social worker atau policy maker yang bisa bantu masyarakat kecil',
|
||||
'prestasi' => 'Koordinator program CSR, pernah design program beasiswa untuk anak kurang mampu',
|
||||
],
|
||||
[
|
||||
'user_id' => 10, // Siswa 9
|
||||
'ekonomi' => 88, 'geografi' => 90, 'sosiologi' => 85, 'sejarah' => 86,
|
||||
'minat' => 'Hobi pariwisata dan guide, suka cerita tentang budaya daerah',
|
||||
'preferensi_studi' => 'Sosial & Humaniora',
|
||||
'cita_cita' => 'Develop destinasi pariwisata lokal yang sustainable, buat kuliner branded',
|
||||
'prestasi' => 'Sudah jadi tour guide 6 bulan, punya grup social media wisata lokal',
|
||||
],
|
||||
[
|
||||
'user_id' => 11, // Siswa 10
|
||||
'ekonomi' => 82, 'geografi' => 88, 'sosiologi' => 80, 'sejarah' => 85,
|
||||
'minat' => 'Senang dengan komunikasi dan public speaking, aktif di debat club',
|
||||
'preferensi_studi' => 'Sosial & Humaniora',
|
||||
'cita_cita' => 'Jadi komunikasi profesional atau journalist yang investigatif',
|
||||
'prestasi' => 'Finalis Debat Nasional, pernah bikin dokumenter lokal, punya podcast',
|
||||
],
|
||||
|
||||
// Tambahan data diverse
|
||||
[
|
||||
'user_id' => 2,
|
||||
'mtk' => 79, 'fisika' => 75, 'kimia' => 80, 'biologi' => 88,
|
||||
'minat' => 'Biologi laut dan konservasi, sering ke pantai untuk research',
|
||||
'preferensi_studi' => 'Pertanian & Lingkungan',
|
||||
'cita_cita' => 'Jadi peneliti laut atau oceanographer, lindungi ekosistem terumbu karang',
|
||||
'prestasi' => 'Peserta program konservasi laut, pernah publikasi artikel tentang coral bleaching',
|
||||
],
|
||||
[
|
||||
'user_id' => 3,
|
||||
'mtk' => 86, 'fisika' => 92, 'kimia' => 85, 'biologi' => 78,
|
||||
'minat' => 'Tertarik mesin presisi dan manufacturing, suka dismantle dan assemble',
|
||||
'preferensi_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Jadi chief engineer di pabrik industri atau buka workshop manufaktur',
|
||||
'prestasi' => 'Juara kompetisi mesin, punya skill CAD dan CNC machining',
|
||||
],
|
||||
[
|
||||
'user_id' => 4,
|
||||
'mtk' => 81, 'fisika' => 80, 'kimia' => 88, 'biologi' => 90,
|
||||
'minat' => 'Farmasi dan obat-obatan, sering ikut diskusi medical science',
|
||||
'preferensi_studi' => 'Kesehatan & Ilmu Hayat',
|
||||
'cita_cita' => 'Jadi apoteker atau researcher farmasi, develop obat lokal berkualitas',
|
||||
'prestasi' => 'Internship di apotek besar, belajar formulation dan quality control',
|
||||
],
|
||||
[
|
||||
'user_id' => 5,
|
||||
'mtk' => 84, 'fisika' => 82, 'kimia' => 92, 'biologi' => 85,
|
||||
'minat' => 'Proses industri dan chemical engineering, suka tonton cara produksi',
|
||||
'preferensi_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Jadi chemical engineer, optimalkan proses produksi di industri tekstil',
|
||||
'prestasi' => 'Design proposal chemical processing untuk usaha keluarga',
|
||||
],
|
||||
[
|
||||
'user_id' => 6,
|
||||
'mtk' => 77, 'fisika' => 79, 'kimia' => 81, 'biologi' => 90,
|
||||
'minat' => 'Veteriner dan kesehatan hewan, punya pengalaman tangani hewan sakit',
|
||||
'preferensi_studi' => 'Kesehatan & Ilmu Hayat',
|
||||
'cita_cita' => 'Jadi dokter hewan dan buka klinik hewan modern di daerah',
|
||||
'prestasi' => 'Volunteer di animal shelter, treatment beberapa hewan terlantar',
|
||||
],
|
||||
[
|
||||
'user_id' => 7,
|
||||
'ekonomi' => 82, 'geografi' => 85, 'sosiologi' => 78, 'sejarah' => 80,
|
||||
'minat' => 'Digital marketing dan e-commerce, aktif buat konten di social media',
|
||||
'preferensi_studi' => 'Bisnis & Manajemen',
|
||||
'cita_cita' => 'Jadi digital marketing specialist atau content creator profesional',
|
||||
'prestasi' => 'Manage social media bisnis teman, follower tumbuh jadi 50 ribu',
|
||||
],
|
||||
[
|
||||
'user_id' => 8,
|
||||
'ekonomi' => 88, 'geografi' => 82, 'sosiologi' => 85, 'sejarah' => 84,
|
||||
'minat' => 'Banking dan investment, suka belajar tentang saham dan cryptocurrency',
|
||||
'preferensi_studi' => 'Bisnis & Manajemen',
|
||||
'cita_cita' => 'Jadi financial advisor atau investment manager di bank besar',
|
||||
'prestasi' => 'Peserta kompetisi investment game, udah trading saham sendiri',
|
||||
],
|
||||
[
|
||||
'user_id' => 9,
|
||||
'ekonomi' => 75, 'geografi' => 88, 'sosiologi' => 90, 'sejarah' => 85,
|
||||
'minat' => 'Pengembangan masyarakat dan pendidikan, pernah ajar anak-anak kurang mampu',
|
||||
'preferensi_studi' => 'Sosial & Humaniora',
|
||||
'cita_cita' => 'Jadi pendidik atau foundation director yang bisa rubah hidup banyak anak',
|
||||
'prestasi' => 'Founder program belajar gratis untuk anak pinggiran, sudah 50 peserta',
|
||||
],
|
||||
[
|
||||
'user_id' => 10,
|
||||
'ekonomi' => 85, 'geografi' => 92, 'sosiologi' => 88, 'sejarah' => 86,
|
||||
'minat' => 'Travel dan budaya lokal, suka explore setiap daerah dan documenting',
|
||||
'preferensi_studi' => 'Sosial & Humaniora',
|
||||
'cita_cita' => 'Jadi travel blogger profesional atau cultural ambassador',
|
||||
'prestasi' => 'Blog wisata udah 100k visitors, punya instagram tentang kuliner lokal',
|
||||
],
|
||||
[
|
||||
'user_id' => 11,
|
||||
'ekonomi' => 80, 'geografi' => 85, 'sosiologi' => 88, 'sejarah' => 90,
|
||||
'minat' => 'Sejarah dan warisan budaya, aktif di club heritage preservation',
|
||||
'preferensi_studi' => 'Sosial & Humaniora',
|
||||
'cita_cita' => 'Jadi historian atau kurator museum, lestarikan warisan budaya Indonesia',
|
||||
'prestasi' => 'Dokumentasi situs sejarah lokal, pernah present di forum budaya',
|
||||
],
|
||||
[
|
||||
'user_id' => 2,
|
||||
'mtk' => 88, 'fisika' => 86, 'kimia' => 84, 'biologi' => 82,
|
||||
'minat' => 'AI dan machine learning, ikut online course dan kaggle competition',
|
||||
'preferensi_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Jadi AI specialist atau data scientist, develop solution untuk Indonesia',
|
||||
'prestasi' => 'Rank 500 di kaggle, bikin model prediksi untuk project sekolah',
|
||||
],
|
||||
[
|
||||
'user_id' => 3,
|
||||
'mtk' => 89, 'fisika' => 88, 'kimia' => 87, 'biologi' => 75,
|
||||
'minat' => 'Game development dan creative coding, punya beberapa game project',
|
||||
'preferensi_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Jadi game developer di studio internasional atau bikin studio sendiri',
|
||||
'prestasi' => 'Game yang dibuat udah dimainkan ribuan orang, dapat award di expo',
|
||||
],
|
||||
[
|
||||
'user_id' => 4,
|
||||
'mtk' => 80, 'fisika' => 79, 'kimia' => 90, 'biologi' => 92,
|
||||
'minat' => 'Gizi dan kesehatan masyarakat, aktif di program kesehatan komunitas',
|
||||
'preferensi_studi' => 'Kesehatan & Ilmu Hayat',
|
||||
'cita_cita' => 'Jadi ahli gizi profesional, program nutrisi untuk anak Indonesia',
|
||||
'prestasi' => 'Design program gizi untuk sekolah di daerah tertinggal, pernah grant',
|
||||
],
|
||||
[
|
||||
'user_id' => 5,
|
||||
'mtk' => 83, 'fisika' => 84, 'kimia' => 89, 'biologi' => 86,
|
||||
'minat' => 'Sustainability dan green technology, interested di renewable energy',
|
||||
'preferensi_studi' => 'Pertanian & Lingkungan',
|
||||
'cita_cita' => 'Jadi engineer yang fokus sustainable development dan clean energy',
|
||||
'prestasi' => 'Project solar panel untuk sekolah, design water management system',
|
||||
],
|
||||
[
|
||||
'user_id' => 6,
|
||||
'mtk' => 81, 'fisika' => 80, 'kimia' => 82, 'biologi' => 89,
|
||||
'minat' => 'Agronomi modern dan precision farming, ikuti webinar tentang smart farm',
|
||||
'preferensi_studi' => 'Pertanian & Lingkungan',
|
||||
'cita_cita' => 'Jadi smart farmer yang pakai teknologi untuk maksimalkan hasil panen',
|
||||
'prestasi' => 'Pilot project smart farming dengan sensor IoT, hasil bagus',
|
||||
],
|
||||
[
|
||||
'user_id' => 7,
|
||||
'ekonomi' => 84, 'geografi' => 86, 'sosiologi' => 82, 'sejarah' => 76,
|
||||
'minat' => 'Supply chain dan logistik, suka pelajari efficient delivery systems',
|
||||
'preferensi_studi' => 'Bisnis & Manajemen',
|
||||
'cita_cita' => 'Jadi supply chain director atau logistics innovator untuk e-commerce',
|
||||
'prestasi' => 'Analyze supply chain usaha keluarga, improve efficiency 40 persen',
|
||||
],
|
||||
[
|
||||
'user_id' => 8,
|
||||
'ekonomi' => 87, 'geografi' => 84, 'sosiologi' => 86, 'sejarah' => 83,
|
||||
'minat' => 'Corporate finance dan management accounting, belajar SAP dan MYOB',
|
||||
'preferensi_studi' => 'Bisnis & Manajemen',
|
||||
'cita_cita' => 'Jadi CFO atau finance director di perusahaan multinational',
|
||||
'prestasi' => 'Intern di finance department perusahaan, handle reconciliation',
|
||||
],
|
||||
[
|
||||
'user_id' => 9,
|
||||
'ekonomi' => 76, 'geografi' => 87, 'sosiologi' => 92, 'sejarah' => 88,
|
||||
'minat' => 'Community development dan empowerment, pernah training leadership',
|
||||
'preferensi_studi' => 'Sosial & Humaniora',
|
||||
'cita_cita' => 'Jadi community development officer di NGO atau social enterprise',
|
||||
'prestasi' => 'Facilitate community meeting dan program design untuk 500 orang',
|
||||
],
|
||||
[
|
||||
'user_id' => 10,
|
||||
'ekonomi' => 86, 'geografi' => 89, 'sosiologi' => 87, 'sejarah' => 85,
|
||||
'minat' => 'Hospitality dan business management, experience internship di hotel',
|
||||
'preferensi_studi' => 'Bisnis & Manajemen',
|
||||
'cita_cita' => 'Jadi hotel manager atau develop hospitality brand lokal kelas dunia',
|
||||
'prestasi' => 'Manage event 200 orang, guest satisfaction score 95 persen',
|
||||
],
|
||||
];
|
||||
|
||||
// Insert recommendations
|
||||
foreach ($recommendationData as $data) {
|
||||
Recommendation::create(array_merge($data, [
|
||||
'hasil_rekomendasi' => [
|
||||
[
|
||||
'jurusan' => 'Teknologi Informasi',
|
||||
'skor' => rand(70, 95) / 100,
|
||||
'detail' => [
|
||||
'nilai' => rand(60, 95) / 100,
|
||||
'minat' => rand(70, 98) / 100,
|
||||
'pref' => rand(60, 95) / 100,
|
||||
'cita' => rand(75, 98) / 100,
|
||||
'prestasi' => rand(50, 90) / 100,
|
||||
]
|
||||
]
|
||||
]
|
||||
]));
|
||||
}
|
||||
|
||||
// Data chat history yang natural dan tidak template
|
||||
$chatData = [
|
||||
// Chat tentang rekomendasi
|
||||
['user_id' => 2, 'id_sesi' => 'session_1', 'pertanyaan' => 'Jadi menurut sistem saya cocok ambil jurusan apa ya?', 'jawaban' => 'Berdasarkan hasil analisis, untuk profile Anda yang nilai MTK-Fisika bagus dan minat coding kuat, Teknologi Informasi adalah pilihan yang selaras. Anda bisa fokus cybersecurity atau development.'],
|
||||
['user_id' => 2, 'id_sesi' => 'session_1', 'pertanyaan' => 'Cybersecurity lebih amankan karir atau development?', 'jawaban' => 'Dua-duanya prospek bagus. Cybersecurity ada demand dari government dan finance yang stabil. Development skills lebih flexible bisa startup atau big tech. Lihat passion Anda lebih ke yang mana.'],
|
||||
['user_id' => 3, 'id_sesi' => 'session_2', 'pertanyaan' => 'Saya pengen jurusan yang bisa langsung applicable untuk robotika', 'jawaban' => 'Untuk robotika, Teknik Elektro atau Teknik Mesin sangat applicable. Dengan score fisika 90 Anda, dua jurusan ini cocok. Teknik Elektro kalau fokus control system, Teknik Mesin kalau fokus mechanical design.'],
|
||||
['user_id' => 3, 'id_sesi' => 'session_2', 'pertanyaan' => 'Lebih pilih mana sih?', 'jawaban' => 'Lihat passion Anda lebih ke elektrik atau mekanik? Kalau suka coding dan control systems, Teknik Elektro. Kalau suka design fisik dan assemble-dismantle, Teknik Mesin. Keduanya relevant untuk robotika.'],
|
||||
['user_id' => 4, 'id_sesi' => 'session_3', 'pertanyaan' => 'Bisa lanjut kedokteran dari TPI nggak?', 'jawaban' => 'Untuk kedokteran, value akademik dan jalur masuk lebih strict dibanding TPI. Tapi calondidik bisa persiapan dari TPI, terutama biologi dan kimia. Fokus banget di olimpiade sains untuk booster nilai masuk.'],
|
||||
['user_id' => 4, 'id_sesi' => 'session_3', 'pertanyaan' => 'TPI itu apa ya singkatannya?', 'jawaban' => 'Teknik Pertanian dan Industri di Polije. Tapi kalau target kedokteran, mungkin bisa cek juga program kesehatan dulu untuk experience, atau aplikasi kedokteran pure dari outcome awal.'],
|
||||
|
||||
// Chat tentang karir
|
||||
['user_id' => 5, 'id_sesi' => 'session_4', 'pertanyaan' => 'Agribusiness itu nanti kerjanya kayak apa?', 'jawaban' => 'Bisa jadi manager farm, business development untuk agritech, supply chain untuk produk pertanian, atau entrepreneur. Banyak pilihan tergantung spesialisasi yang dipilih di kuliah.'],
|
||||
['user_id' => 5, 'id_sesi' => 'session_4', 'pertanyaan' => 'Gaji fresh graduate agritech berapa kira-kira?', 'jawaban' => 'Startup agritech biasanya 4-6 juta tergantung posisi dan lokasi. Corporate agribusiness bisa 5-8 juta. Tapi kalau entrepreneurship sendiri, income tergantung success rate usaha Anda.'],
|
||||
|
||||
// Chat tentang program belajar
|
||||
['user_id' => 6, 'id_sesi' => 'session_5', 'pertanyaan' => 'Polije punya program internship nggak?', 'jawaban' => 'Ada, biasanya di semester akhir. Untuk program Peternakan, ada partnership dengan farm modern dan feed company. Cek dengan prodi langsung untuk detail placement.'],
|
||||
['user_id' => 6, 'id_sesi' => 'session_5', 'pertanyaan' => 'Boleh apply internship di abroad nggak?', 'jawaban' => 'Tergantung kebijakan prodi, tapi generally bisa diskusi dengan pembimbing. Ada beberapa students yang udah internship di Malaysia atau Thailand. Usahakan komunikasi dengan prodi sejak early semester.'],
|
||||
|
||||
// Chat tentang persiapan
|
||||
['user_id' => 7, 'id_sesi' => 'session_6', 'pertanyaan' => 'Perlu les atau belajar sendiri sebelum masuk kuliah?', 'jawaban' => 'Tergantung kesiapan Anda. Kalau values academic udah solid, bisa self-study dengan referensi online. Tapi kalau mau jadi standout di semester pertama, bisa ambil kursus online tentang fondasi subject.'],
|
||||
['user_id' => 7, 'id_sesi' => 'session_6', 'pertanyaan' => 'Ada rekomendasi website atau platform belajar nggak?', 'jawaban' => 'Udah banyak. Coursera, Udemy, Khan Academy bagus untuk foundational. Indonesia ada Ruangguru atau Zenius. Tapi yang paling penting engagement sama dosen dan teman-teman di kelas nanti.'],
|
||||
|
||||
// Chat tentang keputusan
|
||||
['user_id' => 8, 'id_sesi' => 'session_7', 'pertanyaan' => 'Agak ragu antara Akuntansi sama Management', 'jawaban' => 'Dua jurusan ini beda fokus. Akuntansi lebih technical dan detail-oriented, Management lebih strategic dan leader-focused. Lihat strength Anda: detail dan angka atau big picture dan people? Itu bisa guide decision.'],
|
||||
['user_id' => 8, 'id_sesi' => 'session_7', 'pertanyaan' => 'Kalau pilih Management, bisa dapat nilai invest yang bagus nggak dari sekolah?', 'jawaban' => 'Bisa. Dengan nilai ekonomi 90 Anda, management akan mudah. Cuma akuntansi mungkin lebih certified langsung (JATS). Management lebih butuh experience dan soft skill yang dibangun di luar kelas.'],
|
||||
|
||||
// Chat tentang scholarship
|
||||
['user_id' => 9, 'id_sesi' => 'session_8', 'pertanyaan' => 'Ada beasiswa untuk jurusan sosial nggak?', 'jawaban' => 'Ada beberapa dari government dan foundation. Polije ada beasiswa akademik based on nilai. NGO juga sering sponsor untuk program kemanusiaan atau education. Tapi bisa juga dari corporate yang peduli CSR.'],
|
||||
['user_id' => 9, 'id_sesi' => 'session_8', 'pertanyaan' => 'Requirement beasiswa biasanya apa?', 'jawaban' => 'Tergantung sumber. Academic scholarship butuh nilai bagus. Need-based butuh dokumentasi ekonomi. Full scholarship biasanya maintain GPA minimal 3.0 dan aktif di organisasi sosial.'],
|
||||
|
||||
// Chat tentang prospek kerja
|
||||
['user_id' => 10, 'id_sesi' => 'session_9', 'pertanyaan' => 'Pariwisata di Indonesia prospeknya gimana pasca COVID?', 'jawaban' => 'Recovery udah bagus. Domestic tourism naik, international mulai balik. Tapi industry butuh profesional yang innovative, bukan hanya tour guide. Positioning diri Anda sebagai strategic player, bukan operational level.'],
|
||||
['user_id' => 10, 'id_sesi' => 'session_9', 'pertanyaan' => 'Jadi hospitality manager itu punya team berapa biasanya?', 'jawaban' => 'Tergantung size hotel. Small hotel (50 kamar) mungkin 5-10 staff. Mid-range (100-200 kamar) 20-50 staff. Besar (300+ kamar) bisa 100+. Semakin besar, semakin kompleks management-nya.'],
|
||||
|
||||
// Chat about challenges
|
||||
['user_id' => 11, 'id_sesi' => 'session_10', 'pertanyaan' => 'Komunikasi itu subject berat nggak di kuliah?', 'jawaban' => 'Tidak terlalu berat seperti teknik atau science. Tapi demanding dalam hal project, presentasi, dan banyak reading. Kalau Anda suka talking dan creative, ini cocok. Tantangan lebih ke academic rigor, bukan mathematical complexity.'],
|
||||
['user_id' => 11, 'id_sesi' => 'session_10', 'pertanyaan' => 'Ada lab atau praktik langsung nggak?', 'jawaban' => 'Ada. Biasanya ada lab communicative, media production, bahkan ada broadcast studio. Praktik langsung dengan project nyata untuk client atau media partner. Itu yang membuat komunikasi menarik.'],
|
||||
|
||||
// More natural chats
|
||||
['user_id' => 2, 'id_sesi' => 'session_11', 'pertanyaan' => 'Kenalin dong senior-senior di Teknologi Informasi', 'jawaban' => 'Udah ada program mentoring di Polije. Senior bisa di-connect melalui alumni network atau student organization. Mereka helpful banget share experience dan tips lolos interview di tech company.'],
|
||||
['user_id' => 3, 'id_sesi' => 'session_12', 'pertanyaan' => 'Lab di Teknik Elektro lengkap nggak untuk robotika?', 'jawaban' => 'Lumayan lengkap. Ada microcontroller lab, digital logic lab. Untuk advanced robotics, bisa kolaborasi dengan Teknik Mesin juga. Ada makerspace bersama yang membantu student projects.'],
|
||||
['user_id' => 4, 'id_sesi' => 'session_13', 'pertanyaan' => 'Kesehatan di Polije fokus apa ya?', 'jawaban' => 'Ada program Ilmu Gizi, Teknologi Laboratorium Medis, dan Kesehatan Masyarakat. Fokus ke applied health, tidak pure medical science. Cocok untuk Anda yang peduli community health.'],
|
||||
['user_id' => 5, 'id_sesi' => 'session_14', 'pertanyaan' => 'Mahasiswa agribusiness banyak nggak?', 'jawaban' => 'Cukup banyak, sekitar 100 per tahun. Intake lumayan tinggi karena demand industri. Networking sama peers bisa bagus untuk future business partnership.'],
|
||||
['user_id' => 6, 'id_sesi' => 'session_15', 'pertanyaan' => 'Peternakan di Polije punya farm sendiri nggak?', 'jawaban' => 'Ada. Farm sendiri untuk praktik langsung sama students. Facilities bagus, ada kandang modern, equipment hewan medis. Bisa langsung hands-on dari semester pertama.'],
|
||||
['user_id' => 7, 'id_sesi' => 'session_16', 'pertanyaan' => 'Manajemen jurusan itu terlalu general nggak?', 'jawaban' => 'Dalam kuliah ada specialization. Semester awal general foundation, nanti semester 5-6 bisa pilih tracks seperti operations, finance, marketing, atau digital business. Jadi nggak general-general amat.'],
|
||||
['user_id' => 8, 'id_sesi' => 'session_17', 'pertanyaan' => 'Akuntansi itu paling banyak ngitung nggak si?', 'jawaban' => 'Lumayan. Tapi sekarang accounting lebih ke understanding system, analysis, dan business advisory. Ngitung masih ada tapi banyak software yang bantu. Focus lebih ke thinking skills.'],
|
||||
['user_id' => 9, 'id_sesi' => 'session_18', 'pertanyaan' => 'Social welfare program itu study apa aja sih?', 'jawaban' => 'Social policy, community development methods, case management, research methods. Praktik langsung di komunitas juga. Banyak fieldwork yang meaningful.'],
|
||||
['user_id' => 10, 'id_sesi' => 'session_19', 'pertanyaan' => 'Tourism management itu enaknya atau challenging?', 'jawaban' => 'Enaklah karena hands-on banyak, networking luas. Challenging dari fast-paced industry yang selalu berubah. Tapi kalau Anda adaptable dan social, ini worth it.'],
|
||||
];
|
||||
|
||||
// Insert chat histories
|
||||
foreach ($chatData as $chat) {
|
||||
ChatHistory::create($chat);
|
||||
}
|
||||
|
||||
echo "✅ Created " . count($recommendationData) . " recommendations and " . count($chatData) . " chat histories\n";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Recommendation;
|
||||
use App\Models\PolijeMajor;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class RegenerateRecommendationsSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
// Cari siswa yang belum punya rekomendasi atau rekomendasi mereka kosong
|
||||
$studentsWithoutRecs = User::where('role', 'siswa')
|
||||
->whereNotIn('id', Recommendation::pluck('user_id')->toArray())
|
||||
->get();
|
||||
|
||||
$majorNames = PolijeMajor::pluck('nama_jurusan')->toArray();
|
||||
$count = 0;
|
||||
|
||||
foreach ($studentsWithoutRecs as $student) {
|
||||
// Jika siswa belum punya rekomendasi sama sekali, generate random recommendation
|
||||
if (!$student->hasRecommendation) {
|
||||
$scores = [];
|
||||
|
||||
// Generate random scores untuk setiap jurusan
|
||||
foreach ($majorNames as $majorName) {
|
||||
$major = PolijeMajor::where('nama_jurusan', $majorName)->first();
|
||||
if ($major) {
|
||||
$bobot = $major->bobot_mapel;
|
||||
$score = 0;
|
||||
|
||||
// Random scoring (untuk demo)
|
||||
foreach ($bobot as $subject => $weight) {
|
||||
$randomValue = rand(70, 95);
|
||||
$score += ($randomValue * $weight) / 100;
|
||||
}
|
||||
|
||||
// Random bonus untuk preferensi
|
||||
if (rand(0, 1) == 1) {
|
||||
$score += 5;
|
||||
}
|
||||
|
||||
$scores[$majorName] = $score;
|
||||
}
|
||||
}
|
||||
|
||||
arsort($scores);
|
||||
$topRecommendations = array_slice($scores, 0, 3, true);
|
||||
|
||||
$recommendations = [];
|
||||
foreach ($topRecommendations as $majorName => $score) {
|
||||
$recommendations[] = [
|
||||
'jurusan' => $majorName,
|
||||
'skor' => round($score, 2),
|
||||
'detail' => "Rekomendasi berdasarkan nilai dan minat siswa.",
|
||||
];
|
||||
}
|
||||
|
||||
Recommendation::create([
|
||||
'user_id' => $student->id,
|
||||
'mtk' => rand(60, 95),
|
||||
'fisika' => rand(60, 95),
|
||||
'kimia' => rand(60, 95),
|
||||
'biologi' => rand(60, 95),
|
||||
'ekonomi' => rand(60, 95),
|
||||
'geografi' => rand(60, 95),
|
||||
'sosiologi' => rand(60, 95),
|
||||
'sejarah' => rand(60, 95),
|
||||
'minat' => 'Umum',
|
||||
'preferensi_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Profesional',
|
||||
'prestasi' => 'Aktif',
|
||||
'hasil_rekomendasi' => $recommendations, // Pass array langsung, bukan json_encode
|
||||
]);
|
||||
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info("✅ Regenerasi rekomendasi untuk {$count} siswa selesai!");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Recommendation;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class StudentRecommendationSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
* Data siswa dan rekomendasi dengan logika yang akurat
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// Majors data mapping untuk rekomendasi
|
||||
$majorsWeights = [
|
||||
'teknologi_informasi' => ['mtk' => 0.30, 'fisika' => 0.25, 'kimia' => 0.15, 'biologi' => 0.05],
|
||||
'teknik' => ['mtk' => 0.25, 'fisika' => 0.35, 'kimia' => 0.20, 'biologi' => 0.05],
|
||||
'produksi_pertanian' => ['mtk' => 0.15, 'biologi' => 0.35, 'kimia' => 0.25, 'fisika' => 0.10],
|
||||
'peternakan' => ['biologi' => 0.40, 'kimia' => 0.20, 'mtk' => 0.15, 'fisika' => 0.05],
|
||||
'manajemen_agribisnis' => ['ekonomi' => 0.35, 'geografi' => 0.25, 'mtk' => 0.20, 'sosiologi' => 0.15],
|
||||
'akuntansi' => ['ekonomi' => 0.40, 'mtk' => 0.25, 'sejarah' => 0.15, 'sosiologi' => 0.10],
|
||||
'bahasa_komunikasi' => ['sosiologi' => 0.30, 'sejarah' => 0.25, 'ekonomi' => 0.20, 'geografi' => 0.15],
|
||||
];
|
||||
|
||||
$studentData = [
|
||||
// IPA Students
|
||||
[
|
||||
'name' => 'Adi Pratama',
|
||||
'nis' => '001',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'values' => ['mtk' => 92, 'fisika' => 88, 'kimia' => 85, 'biologi' => 78],
|
||||
'minat' => 'Logika Komputer',
|
||||
'preferensi_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Software Engineer',
|
||||
'prestasi' => 'Juara 2 Olimpiade Informatika',
|
||||
'expected_major' => 'Teknologi Informasi',
|
||||
],
|
||||
[
|
||||
'name' => 'Bella Maharani',
|
||||
'nis' => '002',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'values' => ['mtk' => 85, 'fisika' => 90, 'kimia' => 88, 'biologi' => 80],
|
||||
'minat' => 'Mesin & Otomasi',
|
||||
'preferensi_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Insinyur Mesin',
|
||||
'prestasi' => 'Juara Lomba Robotika Regional',
|
||||
'expected_major' => 'Teknik',
|
||||
],
|
||||
[
|
||||
'name' => 'Citra Dewi',
|
||||
'nis' => '003',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'values' => ['mtk' => 78, 'fisika' => 75, 'kimia' => 88, 'biologi' => 92],
|
||||
'minat' => 'Alam Tanaman',
|
||||
'preferensi_studi' => 'Pertanian & Lingkungan',
|
||||
'cita_cita' => 'Agronomis',
|
||||
'prestasi' => 'Juara Pameran Tanaman Hidroponik',
|
||||
'expected_major' => 'Produksi Pertanian',
|
||||
],
|
||||
[
|
||||
'name' => 'Doni Kusuma',
|
||||
'nis' => '004',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'values' => ['mtk' => 82, 'fisika' => 80, 'kimia' => 92, 'biologi' => 88],
|
||||
'minat' => 'Kimia & Biologi',
|
||||
'preferensi_studi' => 'Kesehatan & Ilmu Hayat',
|
||||
'cita_cita' => 'Peneliti Biologi',
|
||||
'prestasi' => 'Penulis Jurnal Ilmiah Tingkat Sekolah',
|
||||
'expected_major' => 'Peternakan',
|
||||
],
|
||||
|
||||
// IPS Students
|
||||
[
|
||||
'name' => 'Eka Prasetyo',
|
||||
'nis' => '005',
|
||||
'kelompok_asal' => 'IPS',
|
||||
'values' => ['ekonomi' => 90, 'geografi' => 87, 'sosiologi' => 85, 'sejarah' => 80],
|
||||
'minat' => 'Bisnis',
|
||||
'preferensi_studi' => 'Bisnis & Manajemen',
|
||||
'cita_cita' => 'Entrepreneur Muda',
|
||||
'prestasi' => 'Juara Kompetisi Bisnis Plan Nasional',
|
||||
'expected_major' => 'Manajemen Agribisnis',
|
||||
],
|
||||
[
|
||||
'name' => 'Fitri Handayani',
|
||||
'nis' => '006',
|
||||
'kelompok_asal' => 'IPS',
|
||||
'values' => ['ekonomi' => 88, 'mtk' => 85, 'sejarah' => 82, 'sosiologi' => 80],
|
||||
'minat' => 'Akuntansi',
|
||||
'preferensi_studi' => 'Bisnis & Manajemen',
|
||||
'cita_cita' => 'Akuntan Profesional',
|
||||
'prestasi' => 'Finalis Kompetisi Akuntansi Wilayah',
|
||||
'expected_major' => 'Akuntansi',
|
||||
],
|
||||
[
|
||||
'name' => 'Gita Salsabila',
|
||||
'nis' => '007',
|
||||
'kelompok_asal' => 'IPS',
|
||||
'values' => ['sosiologi' => 92, 'sejarah' => 88, 'ekonomi' => 80, 'geografi' => 85],
|
||||
'minat' => 'Komunikasi Sosial',
|
||||
'preferensi_studi' => 'Seni & Komunikasi',
|
||||
'cita_cita' => 'Presenter Berita',
|
||||
'prestasi' => 'Juara Debat Tingkat Propinsi',
|
||||
'expected_major' => 'Bahasa Komunikasi',
|
||||
],
|
||||
[
|
||||
'name' => 'Hendra Wijaya',
|
||||
'nis' => '008',
|
||||
'kelompok_asal' => 'IPS',
|
||||
'values' => ['ekonomi' => 85, 'geografi' => 88, 'mtk' => 80, 'sosiologi' => 82],
|
||||
'minat' => 'Bisnis',
|
||||
'preferensi_studi' => 'Bisnis & Manajemen',
|
||||
'cita_cita' => 'Manajer Bisnis',
|
||||
'prestasi' => 'Pengusaha Kuliner Muda',
|
||||
'expected_major' => 'Manajemen Agribisnis',
|
||||
],
|
||||
|
||||
// More diverse students
|
||||
[
|
||||
'name' => 'Ibu Musim',
|
||||
'nis' => '009',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'values' => ['mtk' => 88, 'fisika' => 85, 'kimia' => 86, 'biologi' => 84],
|
||||
'minat' => 'Teknologi & Inovasi',
|
||||
'preferensi_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Product Manager Tech',
|
||||
'prestasi' => 'Top 10 Innovation Challenge',
|
||||
'expected_major' => 'Teknologi Informasi',
|
||||
],
|
||||
[
|
||||
'name' => 'Joko Santoso',
|
||||
'nis' => '010',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'values' => ['fisika' => 92, 'mtk' => 90, 'kimia' => 84, 'biologi' => 76],
|
||||
'minat' => 'Listrik & Energi',
|
||||
'preferensi_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Insinyur Elektro',
|
||||
'prestasi' => 'Juara Kompetisi Energi Terbarukan',
|
||||
'expected_major' => 'Teknik',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($studentData as $data) {
|
||||
// Create student user
|
||||
$user = User::create([
|
||||
'name' => $data['name'],
|
||||
'email' => strtolower(str_replace(' ', '.', $data['name'])) . '@student.polije.ac.id',
|
||||
'password' => Hash::make('password123'),
|
||||
'role' => 'siswa',
|
||||
'nis' => $data['nis'],
|
||||
'kelompok_asal' => $data['kelompok_asal'],
|
||||
'foto' => null,
|
||||
]);
|
||||
|
||||
// Create recommendation with logical scoring
|
||||
$rekomendasiHasil = $this->generateRecommendationResult(
|
||||
$data['values'],
|
||||
$data['preferensi_studi'],
|
||||
$data['expected_major'],
|
||||
$majorsWeights
|
||||
);
|
||||
|
||||
$recommendation = Recommendation::create([
|
||||
'user_id' => $user->id,
|
||||
'mtk' => $data['values']['mtk'] ?? null,
|
||||
'fisika' => $data['values']['fisika'] ?? null,
|
||||
'kimia' => $data['values']['kimia'] ?? null,
|
||||
'biologi' => $data['values']['biologi'] ?? null,
|
||||
'ekonomi' => $data['values']['ekonomi'] ?? null,
|
||||
'geografi' => $data['values']['geografi'] ?? null,
|
||||
'sosiologi' => $data['values']['sosiologi'] ?? null,
|
||||
'sejarah' => $data['values']['sejarah'] ?? null,
|
||||
'minat' => $data['minat'],
|
||||
'preferensi_studi' => $data['preferensi_studi'],
|
||||
'cita_cita' => $data['cita_cita'],
|
||||
'prestasi' => $data['prestasi'],
|
||||
'hasil_rekomendasi' => $rekomendasiHasil,
|
||||
]);
|
||||
|
||||
// Create sample chat history
|
||||
$sessionId = Str::uuid()->toString();
|
||||
$this->createSampleChatHistory($user->id, $recommendation->id, $sessionId, $data);
|
||||
}
|
||||
|
||||
$this->command->info('Student dan Recommendation data berhasil diisi dengan ' . count($studentData) . ' siswa!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate logical recommendation result based on values
|
||||
*/
|
||||
private function generateRecommendationResult($values, $preferensi, $expectedMajor, $majorsWeights)
|
||||
{
|
||||
$majors = [
|
||||
[
|
||||
'jurusan' => 'Teknologi Informasi',
|
||||
'skor' => $this->calculateScore($values, ['mtk' => 0.30, 'fisika' => 0.25, 'kimia' => 0.15, 'biologi' => 0.05]),
|
||||
'detail' => 'Cocok untuk minat teknologi dan programming.'
|
||||
],
|
||||
[
|
||||
'jurusan' => 'Teknik',
|
||||
'skor' => $this->calculateScore($values, ['mtk' => 0.25, 'fisika' => 0.35, 'kimia' => 0.20, 'biologi' => 0.05]),
|
||||
'detail' => 'Sesuai dengan kemampuan fisika dan matematika tinggi.'
|
||||
],
|
||||
[
|
||||
'jurusan' => 'Produksi Pertanian',
|
||||
'skor' => $this->calculateScore($values, ['mtk' => 0.15, 'biologi' => 0.35, 'kimia' => 0.25, 'fisika' => 0.10]),
|
||||
'detail' => 'Cocok untuk minat di bidang pertanian dan lingkungan.'
|
||||
],
|
||||
[
|
||||
'jurusan' => 'Peternakan',
|
||||
'skor' => $this->calculateScore($values, ['biologi' => 0.40, 'kimia' => 0.20, 'mtk' => 0.15, 'fisika' => 0.05]),
|
||||
'detail' => 'Bidang peternakan dan manajemen hewan ternak.'
|
||||
],
|
||||
[
|
||||
'jurusan' => 'Manajemen Agribisnis',
|
||||
'skor' => $this->calculateScore($values, ['ekonomi' => 0.35, 'geografi' => 0.25, 'mtk' => 0.20, 'sosiologi' => 0.15]),
|
||||
'detail' => 'Kombinasi bisnis dan pertanian yang sempurna.'
|
||||
],
|
||||
[
|
||||
'jurusan' => 'Akuntansi',
|
||||
'skor' => $this->calculateScore($values, ['ekonomi' => 0.40, 'mtk' => 0.25, 'sejarah' => 0.15, 'sosiologi' => 0.10]),
|
||||
'detail' => 'Untuk yang tertarik dengan keuangan dan akuntansi.'
|
||||
],
|
||||
[
|
||||
'jurusan' => 'Bahasa Komunikasi',
|
||||
'skor' => $this->calculateScore($values, ['sosiologi' => 0.30, 'sejarah' => 0.25, 'ekonomi' => 0.20, 'geografi' => 0.15]),
|
||||
'detail' => 'Bidang komunikasi dan seni untuk yang ekspresif.'
|
||||
],
|
||||
];
|
||||
|
||||
// Sort by score descending
|
||||
usort($majors, fn($a, $b) => $b['skor'] <=> $a['skor']);
|
||||
|
||||
return array_slice($majors, 0, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate score based on weighted values
|
||||
*/
|
||||
private function calculateScore($values, $weights)
|
||||
{
|
||||
$score = 0;
|
||||
$totalWeight = 0;
|
||||
|
||||
foreach ($weights as $subject => $weight) {
|
||||
if (isset($values[$subject])) {
|
||||
$score += ($values[$subject] / 100) * $weight;
|
||||
$totalWeight += $weight;
|
||||
}
|
||||
}
|
||||
|
||||
return $totalWeight > 0 ? round(($score / $totalWeight) * 100, 1) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create sample chat history for a student
|
||||
*/
|
||||
private function createSampleChatHistory($userId, $recommendationId, $sessionId, $studentData)
|
||||
{
|
||||
$chatSamples = [
|
||||
[
|
||||
'pertanyaan' => 'Jurusan apa yang cocok untuk saya?',
|
||||
'jawaban' => 'Berdasarkan nilai dan minat Anda, ' . $studentData['expected_major'] . ' adalah pilihan terbaik. Dengan prestasi di bidang ' . strtolower($studentData['prestasi']) . ', Anda memiliki potensi besar untuk sukses.'
|
||||
],
|
||||
[
|
||||
'pertanyaan' => 'Mengapa jurusan tersebut cocok untuk saya?',
|
||||
'jawaban' => 'Karena nilai ' . implode(', ', array_keys($studentData['values'])) . ' Anda menunjukkan keunggulan di area yang relevan dengan jurusan tersebut.'
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($chatSamples as $index => $chat) {
|
||||
\App\Models\ChatHistory::create([
|
||||
'user_id' => $userId,
|
||||
'id_sesi' => $sessionId,
|
||||
'id_rekomendasi' => $recommendationId,
|
||||
'pertanyaan' => $chat['pertanyaan'],
|
||||
'jawaban' => $chat['jawaban'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Recommendation;
|
||||
use App\Models\PolijeMajor;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class StudentWithAccurateRecommendationSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
// Data siswa dengan nilai yang logis untuk setiap rekomendasi
|
||||
$studentsData = [
|
||||
// Siswa yang cocok untuk Teknologi Informasi
|
||||
[
|
||||
'name' => 'Rino Pratama',
|
||||
'email' => 'rino.pratama@student.edu',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'values' => ['mtk' => 92, 'fisika' => 88, 'kimia' => 75, 'biologi' => 70],
|
||||
'minat' => 'Logika Komputer',
|
||||
'pref_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Software Engineer',
|
||||
'prestasi' => 'Juara 1 Kompetisi Coding',
|
||||
],
|
||||
[
|
||||
'name' => 'Budi Santoso',
|
||||
'email' => 'budi.santoso@student.edu',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'values' => ['mtk' => 88, 'fisika' => 85, 'kimia' => 70, 'biologi' => 68],
|
||||
'minat' => 'Pemrograman',
|
||||
'pref_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Web Developer',
|
||||
'prestasi' => 'Peserta Hackathon 2025',
|
||||
],
|
||||
// Siswa yang cocok untuk Teknik
|
||||
[
|
||||
'name' => 'Adi Wijaya',
|
||||
'email' => 'adi.wijaya@student.edu',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'values' => ['mtk' => 85, 'fisika' => 90, 'kimia' => 70, 'biologi' => 65],
|
||||
'minat' => 'Mekanika & Energi',
|
||||
'pref_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Insinyur Mesin',
|
||||
'prestasi' => 'Juara 2 Robotika',
|
||||
],
|
||||
[
|
||||
'name' => 'Dani Pratama',
|
||||
'email' => 'dani.pratama@student.edu',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'values' => ['mtk' => 86, 'fisika' => 88, 'kimia' => 72, 'biologi' => 66],
|
||||
'minat' => 'Elektronika',
|
||||
'pref_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Teknisi Elektronik',
|
||||
'prestasi' => 'Aktif di klub robotika',
|
||||
],
|
||||
// Siswa yang cocok untuk Kesehatan
|
||||
[
|
||||
'name' => 'Siti Nurhaliza',
|
||||
'email' => 'siti.nurhaliza@student.edu',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'values' => ['mtk' => 80, 'fisika' => 70, 'kimia' => 88, 'biologi' => 92],
|
||||
'minat' => 'Ilmu Hayat & Pelayanan',
|
||||
'pref_studi' => 'Kesehatan & Ilmu Hayat',
|
||||
'cita_cita' => 'Perawat Profesional',
|
||||
'prestasi' => 'Juara Olimpiade Biologi',
|
||||
],
|
||||
[
|
||||
'name' => 'Tina Susanti',
|
||||
'email' => 'tina.susanti@student.edu',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'values' => ['mtk' => 78, 'fisika' => 68, 'kimia' => 90, 'biologi' => 89],
|
||||
'minat' => 'Farmasi & Kesehatan',
|
||||
'pref_studi' => 'Kesehatan & Ilmu Hayat',
|
||||
'cita_cita' => 'Apoteker',
|
||||
'prestasi' => 'Aktif di PMR',
|
||||
],
|
||||
// Siswa yang cocok untuk Pertanian
|
||||
[
|
||||
'name' => 'Wayan Suparta',
|
||||
'email' => 'wayan.suparta@student.edu',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'values' => ['mtk' => 75, 'fisika' => 72, 'kimia' => 82, 'biologi' => 88],
|
||||
'minat' => 'Alam & Pertanian',
|
||||
'pref_studi' => 'Pertanian & Lingkungan',
|
||||
'cita_cita' => 'Ahli Pertanian',
|
||||
'prestasi' => 'Peraih Medali Sains',
|
||||
],
|
||||
[
|
||||
'name' => 'Bambang Hermawan',
|
||||
'email' => 'bambang.hermawan@student.edu',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'values' => ['mtk' => 78, 'fisika' => 75, 'kimia' => 80, 'biologi' => 85],
|
||||
'minat' => 'Ternak & Hewan',
|
||||
'pref_studi' => 'Pertanian & Lingkungan',
|
||||
'cita_cita' => 'Peternak Modern',
|
||||
'prestasi' => 'Juara Pameran Ternak',
|
||||
],
|
||||
// Siswa cocok untuk Teknologi Pertanian
|
||||
[
|
||||
'name' => 'Hendra Sutrisno',
|
||||
'email' => 'hendra.sutrisno@student.edu',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'values' => ['mtk' => 87, 'fisika' => 86, 'kimia' => 76, 'biologi' => 78],
|
||||
'minat' => 'Inovasi & Teknologi Pertanian',
|
||||
'pref_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Engineer Pertanian',
|
||||
'prestasi' => 'Penemu Alat Pertanian',
|
||||
],
|
||||
// Siswa IPS yang cocok untuk Bisnis/Ekonomi
|
||||
[
|
||||
'name' => 'Rina Handayani',
|
||||
'email' => 'rina.handayani@student.edu',
|
||||
'kelompok_asal' => 'IPS',
|
||||
'values' => ['ekonomi' => 92, 'geografi' => 85, 'sosiologi' => 80, 'sejarah' => 78],
|
||||
'minat' => 'Bisnis & Kewirausahaan',
|
||||
'pref_studi' => 'Bisnis & Manajemen',
|
||||
'cita_cita' => 'Entrepreneur',
|
||||
'prestasi' => 'Juara Kompetisi Bisnis',
|
||||
],
|
||||
[
|
||||
'name' => 'Agus Suryanto',
|
||||
'email' => 'agus.suryanto@student.edu',
|
||||
'kelompok_asal' => 'IPS',
|
||||
'values' => ['ekonomi' => 88, 'geografi' => 82, 'sosiologi' => 85, 'sejarah' => 80],
|
||||
'minat' => 'Manajemen & Akuntansi',
|
||||
'pref_studi' => 'Bisnis & Manajemen',
|
||||
'cita_cita' => 'Akuntan Profesional',
|
||||
'prestasi' => 'Peserta Lomba Case Study',
|
||||
],
|
||||
// Siswa IPS untuk Agribisnis
|
||||
[
|
||||
'name' => 'Mardi Santoso',
|
||||
'email' => 'mardi.santoso@student.edu',
|
||||
'kelompok_asal' => 'IPS',
|
||||
'values' => ['ekonomi' => 85, 'geografi' => 88, 'sosiologi' => 78, 'sejarah' => 75],
|
||||
'minat' => 'Pertanian & Bisnis',
|
||||
'pref_studi' => 'Bisnis & Manajemen',
|
||||
'cita_cita' => 'Manager Agribisnis',
|
||||
'prestasi' => 'Aktif di OSIS',
|
||||
],
|
||||
// Siswa IPS untuk Bahasa & Komunikasi
|
||||
[
|
||||
'name' => 'Nadia Putri',
|
||||
'email' => 'nadia.putri@student.edu',
|
||||
'kelompok_asal' => 'IPS',
|
||||
'values' => ['sejarah' => 88, 'sosiologi' => 86, 'geografi' => 80, 'ekonomi' => 78],
|
||||
'minat' => 'Bahasa & Komunikasi',
|
||||
'pref_studi' => 'Sosial & Humaniora',
|
||||
'cita_cita' => 'Jurnalis Profesional',
|
||||
'prestasi' => 'Penulis Artikel Terpercaya',
|
||||
],
|
||||
[
|
||||
'name' => 'Lisa Maharani',
|
||||
'email' => 'lisa.maharani@student.edu',
|
||||
'kelompok_asal' => 'IPS',
|
||||
'values' => ['sejarah' => 85, 'sosiologi' => 82, 'geografi' => 85, 'ekonomi' => 76],
|
||||
'minat' => 'Pariwisata & Budaya',
|
||||
'pref_studi' => 'Sosial & Humaniora',
|
||||
'cita_cita' => 'Tour Guide Profesional',
|
||||
'prestasi' => 'Peserta Pelatihan Tour Guide',
|
||||
],
|
||||
];
|
||||
|
||||
$majorNames = PolijeMajor::pluck('nama_jurusan')->toArray();
|
||||
|
||||
foreach ($studentsData as $data) {
|
||||
// Buat user siswa
|
||||
$user = User::firstOrCreate(
|
||||
['email' => $data['email']],
|
||||
[
|
||||
'name' => $data['name'],
|
||||
'password' => bcrypt('password'),
|
||||
'role' => 'siswa',
|
||||
'kelompok_asal' => $data['kelompok_asal'],
|
||||
]
|
||||
);
|
||||
|
||||
// Hitung skor rekomendasi berdasarkan bobot mapel
|
||||
$scores = $this->calculateScores($data, $majorNames);
|
||||
|
||||
// Ambil top 3 dengan skor tertinggi
|
||||
arsort($scores);
|
||||
$topRecommendations = array_slice($scores, 0, 3, true);
|
||||
|
||||
// Buat data rekomendasi dengan ranking
|
||||
$recommendations = [];
|
||||
foreach ($topRecommendations as $majorName => $score) {
|
||||
$recommendations[] = [
|
||||
'jurusan' => $majorName,
|
||||
'skor' => round($score, 2),
|
||||
'detail' => "Rekomendasi berdasarkan nilai, minat, dan preferensi studi.",
|
||||
];
|
||||
}
|
||||
|
||||
// Simpan rekomendasi ke database
|
||||
Recommendation::create([
|
||||
'user_id' => $user->id,
|
||||
'mtk' => $data['values']['mtk'] ?? 0,
|
||||
'fisika' => $data['values']['fisika'] ?? 0,
|
||||
'kimia' => $data['values']['kimia'] ?? 0,
|
||||
'biologi' => $data['values']['biologi'] ?? 0,
|
||||
'ekonomi' => $data['values']['ekonomi'] ?? 0,
|
||||
'sejarah' => $data['values']['sejarah'] ?? 0,
|
||||
'geografi' => $data['values']['geografi'] ?? 0,
|
||||
'sosiologi' => $data['values']['sosiologi'] ?? 0,
|
||||
'minat' => $data['minat'],
|
||||
'preferensi_studi' => $data['pref_studi'],
|
||||
'cita_cita' => $data['cita_cita'],
|
||||
'prestasi' => $data['prestasi'],
|
||||
'hasil_rekomendasi' => json_encode($recommendations),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->command->info('✅ ' . count($studentsData) . ' siswa dengan rekomendasi akurat sudah ditambahkan!');
|
||||
}
|
||||
|
||||
private function calculateScores(array $studentData, array $majorNames): array
|
||||
{
|
||||
$scores = [];
|
||||
$majors = PolijeMajor::all()->keyBy('nama_jurusan');
|
||||
|
||||
foreach ($majorNames as $majorName) {
|
||||
if (!isset($majors[$majorName])) continue;
|
||||
|
||||
$major = $majors[$majorName];
|
||||
$bobot = $major->bobot_mapel;
|
||||
$score = 0;
|
||||
|
||||
// Hitung skor berdasarkan bobot dan nilai siswa
|
||||
foreach ($bobot as $subject => $weight) {
|
||||
$value = $studentData['values'][$subject] ?? 0;
|
||||
$score += ($value * $weight) / 100;
|
||||
}
|
||||
|
||||
// Bonus untuk preferensi studi yang sesuai
|
||||
if (in_array($studentData['pref_studi'], $major->preferensi_studi)) {
|
||||
$score += 5;
|
||||
}
|
||||
|
||||
$scores[$majorName] = $score;
|
||||
}
|
||||
|
||||
return $scores;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\PolijeMajor;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class UpdateMajorsAccurateBobotSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
// Data jurusan dengan bobot mapel yang akurat untuk Naive Bayes
|
||||
$majorsData = [
|
||||
[
|
||||
'nama_jurusan' => 'Produksi Pertanian',
|
||||
'bobot_mapel' => [
|
||||
'mtk' => 0.15,
|
||||
'fisika' => 0.15,
|
||||
'kimia' => 0.30,
|
||||
'biologi' => 0.40, // Tertinggi - biologi alam/tanaman
|
||||
'ekonomi' => 0.25,
|
||||
'geografi' => 0.35, // Tinggi - lokasi/iklim/lahan
|
||||
'sejarah' => 0.10,
|
||||
'sosiologi' => 0.15,
|
||||
],
|
||||
'keywords' => ['pertanian', 'petani', 'kebun', 'sawah', 'panen', 'tanaman', 'budidaya', 'agronomi', 'tanam', 'bercocok tanam', 'alam', 'hortikultura', 'pupuk', 'bibit', 'agroteknologi', 'perkebunan', 'pangan', 'ketahanan pangan', 'hidroponik', 'organik', 'lahan', 'irigasi', 'cuaca', 'musim'],
|
||||
'preferensi_studi' => ['Pertanian & Lingkungan', 'Sains & Teknologi', 'Inovasi & Teknologi', 'Sustainable Development', 'Agribisnis Modern'],
|
||||
],
|
||||
[
|
||||
'nama_jurusan' => 'Teknologi Pertanian',
|
||||
'bobot_mapel' => [
|
||||
'mtk' => 0.35, // Tinggi - rumus & hitungan
|
||||
'fisika' => 0.35, // Tertinggi - mesin/energi/gerakan
|
||||
'kimia' => 0.20,
|
||||
'biologi' => 0.15,
|
||||
'ekonomi' => 0.25,
|
||||
'geografi' => 0.25,
|
||||
'sejarah' => 0.15,
|
||||
'sosiologi' => 0.15,
|
||||
],
|
||||
'keywords' => ['teknologi pertanian', 'mesin pertanian', 'inovasi', 'otomasi', 'pengolahan pangan', 'pangan', 'mekanisasi', 'teknologi pangan', 'alat pertanian', 'rekayasa', 'iot pertanian', 'smart farming', 'digital farming', 'kontrol kualitas', 'proses produksi', 'efisiensi', 'presisi pertanian'],
|
||||
'preferensi_studi' => ['Sains & Teknologi', 'Inovasi & Teknologi', 'Pertanian & Lingkungan', 'Sustainable Development', 'Digital Transformation'],
|
||||
],
|
||||
[
|
||||
'nama_jurusan' => 'Peternakan',
|
||||
'bobot_mapel' => [
|
||||
'mtk' => 0.20,
|
||||
'fisika' => 0.15,
|
||||
'kimia' => 0.25,
|
||||
'biologi' => 0.45, // Tertinggi - ilmu hewan
|
||||
'ekonomi' => 0.30, // Tinggi - pasar/bisnis ternak
|
||||
'geografi' => 0.25,
|
||||
'sejarah' => 0.10,
|
||||
'sosiologi' => 0.15,
|
||||
],
|
||||
'keywords' => ['ternak', 'hewan', 'peternakan', 'peternak', 'sapi', 'ayam', 'unggas', 'kambing', 'susu', 'pakan', 'nutrisi hewan', 'veteriner', 'ikan', 'aquaculture', 'budidaya hewan', 'farm management', 'kesehatan hewan', 'produksi ternak', 'perikanan', 'kolam', 'perkembangbiakan', 'reproduksi'],
|
||||
'preferensi_studi' => ['Pertanian & Lingkungan', 'Kesehatan & Ilmu Hayat', 'Agribisnis Modern', 'Sustainable Development', 'Sains & Teknologi'],
|
||||
],
|
||||
[
|
||||
'nama_jurusan' => 'Manajemen Agribisnis',
|
||||
'bobot_mapel' => [
|
||||
'mtk' => 0.35, // Tinggi - akuntansi/perhitungan
|
||||
'fisika' => 0.15,
|
||||
'kimia' => 0.10,
|
||||
'biologi' => 0.15,
|
||||
'ekonomi' => 0.45, // Tertinggi - bisnis/pasar
|
||||
'geografi' => 0.20,
|
||||
'sejarah' => 0.15,
|
||||
'sosiologi' => 0.20,
|
||||
],
|
||||
'keywords' => ['bisnis', 'agribisnis', 'usaha', 'entrepreneur', 'pengusaha', 'dagang', 'jual', 'pemasaran', 'kewirausahaan', 'manajemen', 'ekonomi pertanian', 'pasar', 'supply chain', 'logistik', 'analisis pasar', 'branding produk', 'akuntansi', 'keuangan', 'investasi', 'ekspor', 'strategi bisnis'],
|
||||
'preferensi_studi' => ['Bisnis & Manajemen', 'Entrepreneurship & Inovasi', 'Pertanian & Lingkungan', 'Agribisnis Modern', 'Digital Transformation'],
|
||||
],
|
||||
[
|
||||
'nama_jurusan' => 'Teknologi Informasi',
|
||||
'bobot_mapel' => [
|
||||
'mtk' => 0.50, // Tertinggi - logika/algoritma
|
||||
'fisika' => 0.20,
|
||||
'kimia' => 0.10,
|
||||
'biologi' => 0.10,
|
||||
'ekonomi' => 0.20,
|
||||
'geografi' => 0.10,
|
||||
'sejarah' => 0.15,
|
||||
'sosiologi' => 0.15,
|
||||
],
|
||||
'keywords' => ['programmer', 'developer', 'coding', 'software', 'web', 'aplikasi', 'komputer', 'it', 'jaringan', 'hacker', 'game', 'data', 'ai', 'robot', 'ngoding', 'laptop', 'teknologi', 'digital', 'internet', 'programming', 'desain grafis', 'ui ux', 'mobile app', 'cloud', 'database', 'machine learning', 'cybersecurity', 'backend', 'frontend'],
|
||||
'preferensi_studi' => ['Sains & Teknologi', 'Inovasi & Teknologi', 'Digital Transformation', 'Entrepreneurship & Inovasi', 'Problem Solving & Logic'],
|
||||
],
|
||||
[
|
||||
'nama_jurusan' => 'Teknik',
|
||||
'bobot_mapel' => [
|
||||
'mtk' => 0.40, // Tinggi - perhitungan teknis
|
||||
'fisika' => 0.45, // Tertinggi - gaya/energi/mekanika
|
||||
'kimia' => 0.15,
|
||||
'biologi' => 0.10,
|
||||
'ekonomi' => 0.20,
|
||||
'geografi' => 0.15,
|
||||
'sejarah' => 0.15,
|
||||
'sosiologi' => 0.10,
|
||||
],
|
||||
'keywords' => ['mesin', 'bengkel', 'listrik', 'las', 'robot', 'motor', 'teknik', 'otomasi', 'elektronik', 'instalasi', 'panel', 'mekanik', 'industri', 'manufaktur', 'pabrik', 'bangunan', 'konstruksi', 'sipil', 'energi', 'maintenance', 'mekatronika', 'instrumentasi', 'quality control', 'produksi', 'assembly'],
|
||||
'preferensi_studi' => ['Sains & Teknologi', 'Industri & Manufaktur', 'Problem Solving & Logic', 'Inovasi & Teknologi', 'Infrastruktur & Pembangunan'],
|
||||
],
|
||||
[
|
||||
'nama_jurusan' => 'Kesehatan',
|
||||
'bobot_mapel' => [
|
||||
'mtk' => 0.20,
|
||||
'fisika' => 0.15,
|
||||
'kimia' => 0.35, // Tinggi - farmasi/reaksi
|
||||
'biologi' => 0.45, // Tertinggi - anatomi/fisiologi
|
||||
'ekonomi' => 0.15,
|
||||
'geografi' => 0.10,
|
||||
'sejarah' => 0.10,
|
||||
'sosiologi' => 0.30, // Tinggi - interaksi pasien
|
||||
],
|
||||
'keywords' => ['dokter', 'perawat', 'medis', 'gizi', 'kesehatan', 'pelayanan', 'terapis', 'obat', 'rumah sakit', 'klinik', 'farmasi', 'nutrisi', 'sanitasi', 'rawat', 'sehat', 'kesehatan masyarakat', 'laboratorium', 'diagnostik', 'wellness', 'vaksin', 'fisioterapi', 'psikologi', 'kebidanan'],
|
||||
'preferensi_studi' => ['Kesehatan & Ilmu Hayat', 'Pelayanan & Sosial', 'Sains & Teknologi', 'Inovasi Medis', 'Humanitarian & Community'],
|
||||
],
|
||||
[
|
||||
'nama_jurusan' => 'Bahasa, Komunikasi, dan Pariwisata',
|
||||
'bobot_mapel' => [
|
||||
'mtk' => 0.15, // Rendah - bukan fokus
|
||||
'fisika' => 0.10,
|
||||
'kimia' => 0.10,
|
||||
'biologi' => 0.10,
|
||||
'ekonomi' => 0.25, // Tinggi - pariwisata/bisnis
|
||||
'geografi' => 0.35, // Tinggi - destinasi/lokasi wisata
|
||||
'sejarah' => 0.35, // Tertinggi - konteks budaya/sejarah
|
||||
'sosiologi' => 0.35, // Tertinggi - interaksi sosial/komunikasi
|
||||
],
|
||||
'keywords' => ['bahasa', 'komunikasi', 'pariwisata', 'tour guide', 'hotel', 'jurnalis', 'marketing', 'inggris', 'penerjemah', 'travel', 'wisata', 'hospitality', 'public speaking', 'media', 'broadcasting', 'content creator', 'humas', 'event', 'pelayanan tamu', 'budaya', 'sejarah', 'linguistik', 'turis', 'destinasi'],
|
||||
'preferensi_studi' => ['Sosial & Humaniora', 'Bisnis & Manajemen', 'Kreativitas & Komunikasi', 'Entrepreneurship & Inovasi', 'Digital Marketing & Content'],
|
||||
],
|
||||
[
|
||||
'nama_jurusan' => 'Bisnis',
|
||||
'bobot_mapel' => [
|
||||
'mtk' => 0.45, // Tinggi - akuntansi/analisis
|
||||
'fisika' => 0.10,
|
||||
'kimia' => 0.10,
|
||||
'biologi' => 0.10,
|
||||
'ekonomi' => 0.50, // Tertinggi - bisnis/ekonomi
|
||||
'geografi' => 0.15,
|
||||
'sejarah' => 0.15,
|
||||
'sosiologi' => 0.20,
|
||||
],
|
||||
'keywords' => ['manager', 'pimpinan', 'bisnis', 'accounting', 'marketing', 'sales', 'kantor', 'keuangan', 'bank', 'akuntansi', 'hitung', 'administrasi', 'perbankan', 'ekonomi', 'uang', 'investasi', 'pajak', 'wirausaha', 'audit', 'finance', 'analisis bisnis', 'customer', 'strategi', 'leadership'],
|
||||
'preferensi_studi' => ['Bisnis & Manajemen', 'Entrepreneurship & Inovasi', 'Kepemimpinan & Manajemen', 'Digital Transformation', 'Analisis & Problem Solving'],
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($majorsData as $data) {
|
||||
PolijeMajor::where('nama_jurusan', $data['nama_jurusan'])
|
||||
->update([
|
||||
'bobot_mapel' => $data['bobot_mapel'],
|
||||
'keywords' => $data['keywords'],
|
||||
'preferensi_studi' => $data['preferensi_studi'],
|
||||
]);
|
||||
}
|
||||
|
||||
$this->command->info('✅ Bobot mapel semua jurusan sudah diperbarui dengan akurat!');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
<?php
|
||||
/**
|
||||
* DATABASE INTEGRITY TEST SCRIPT
|
||||
* Untuk memverifikasi data sebelum sidang
|
||||
*
|
||||
* Jalankan dari terminal:
|
||||
* php artisan tinker < database_test.php
|
||||
* atau copy-paste di tinker session
|
||||
*/
|
||||
|
||||
// ============================================
|
||||
// TEST 1: CHECK NULL VALUES IN RECOMMENDATIONS
|
||||
// ============================================
|
||||
echo "\n============================================\n";
|
||||
echo "✅ TEST 1: CHECK NULL VALUES IN RECOMMENDATIONS\n";
|
||||
echo "============================================\n";
|
||||
|
||||
$nullCount = \App\Models\Recommendation::whereRaw("JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') IS NULL")
|
||||
->orWhereRaw("JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') = 'null'")
|
||||
->count();
|
||||
|
||||
if ($nullCount === 0) {
|
||||
echo "✅ PASS: Tidak ada NULL values\n";
|
||||
echo " Total recommendations: " . \App\Models\Recommendation::count() . "\n";
|
||||
} else {
|
||||
echo "❌ FAIL: Ditemukan $nullCount NULL values!\n";
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TEST 2: CHECK MAJOR NAMES CONSISTENCY
|
||||
// ============================================
|
||||
echo "\n============================================\n";
|
||||
echo "✅ TEST 2: CHECK MAJOR NAMES CONSISTENCY\n";
|
||||
echo "============================================\n";
|
||||
|
||||
$majors = \App\Models\PolijeMajor::pluck('nama_jurusan')->unique();
|
||||
echo "Jurusan yang ada di database:\n";
|
||||
foreach ($majors as $major) {
|
||||
echo " - $major\n";
|
||||
}
|
||||
|
||||
// Check for wrong naming
|
||||
$wrongNaming = \App\Models\Recommendation::selectRaw("JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') as major_name")
|
||||
->distinct()
|
||||
->get();
|
||||
|
||||
echo "\nMajors dalam rekomendasi:\n";
|
||||
$hasWrongName = false;
|
||||
foreach ($wrongNaming as $item) {
|
||||
$name = trim($item->major_name, '\" ');
|
||||
if (strpos($name, 'Teknik Informatika') !== false) {
|
||||
echo " ⚠️ $name (SHOULD BE: Teknologi Informasi)\n";
|
||||
$hasWrongName = true;
|
||||
} elseif (!empty($name) && $name !== 'null') {
|
||||
echo " ✅ $name\n";
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasWrongName) {
|
||||
echo "\n❌ FAIL: Ada major name yang salah! (Teknik Informatika should be Teknologi Informasi)\n";
|
||||
} else {
|
||||
echo "\n✅ PASS: Semua major names konsisten\n";
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TEST 3: CHECK WEIGHTS STANDARDIZATION
|
||||
// ============================================
|
||||
echo "\n============================================\n";
|
||||
echo "✅ TEST 3: CHECK WEIGHTS STANDARDIZATION\n";
|
||||
echo "============================================\n";
|
||||
|
||||
$config = config('polije.jurusan');
|
||||
$expectedWeights = [
|
||||
'nilai' => 0.156,
|
||||
'minat' => 0.456,
|
||||
'pref' => 0.256,
|
||||
'cita_cita' => 0.090,
|
||||
'prestasi' => 0.040
|
||||
];
|
||||
|
||||
$weightsValid = true;
|
||||
echo "Expected weights:\n";
|
||||
foreach ($expectedWeights as $key => $weight) {
|
||||
echo " $key: $weight\n";
|
||||
}
|
||||
|
||||
echo "\nActual weights in config:\n";
|
||||
foreach ($config as $jurusan => $data) {
|
||||
$weights = $data['weights'];
|
||||
$total = array_sum($weights);
|
||||
|
||||
$isValid = true;
|
||||
foreach ($expectedWeights as $key => $expectedWeight) {
|
||||
if ($weights[$key] != $expectedWeight) {
|
||||
$isValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isValid && abs($total - 1.0) < 0.001) {
|
||||
echo " ✅ $jurusan (total: $total)\n";
|
||||
} else {
|
||||
echo " ❌ $jurusan - MISMATCH!\n";
|
||||
echo " Weights: " . json_encode($weights) . "\n";
|
||||
$weightsValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($weightsValid) {
|
||||
echo "\n✅ PASS: Semua jurusan memiliki weights yang sama dan valid\n";
|
||||
} else {
|
||||
echo "\n❌ FAIL: Ada weights yang tidak sesuai!\n";
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TEST 4: CHECK KEYWORDS BALANCE
|
||||
// ============================================
|
||||
echo "\n============================================\n";
|
||||
echo "✅ TEST 4: CHECK KEYWORDS BALANCE\n";
|
||||
echo "============================================\n";
|
||||
|
||||
$keywords = config('polije.keywords');
|
||||
|
||||
echo "Minat Keywords Balance:\n";
|
||||
$minatBalance = true;
|
||||
foreach ($keywords['minat'] as $category => $kwords) {
|
||||
$count = count($kwords);
|
||||
if ($count >= 24 && $count <= 28) {
|
||||
echo " ✅ $category: $count keywords\n";
|
||||
} else {
|
||||
echo " ⚠️ $category: $count keywords (ideal: 26)\n";
|
||||
$minatBalance = false;
|
||||
}
|
||||
}
|
||||
|
||||
echo "\nCita-Cita Keywords Balance:\n";
|
||||
$citaBalance = true;
|
||||
foreach ($keywords['cita_cita'] as $category => $kwords) {
|
||||
$count = count($kwords);
|
||||
if ($count >= 24 && $count <= 30) {
|
||||
echo " ✅ $category: $count keywords\n";
|
||||
} else {
|
||||
echo " ⚠️ $category: $count keywords (ideal: 24-30)\n";
|
||||
$citaBalance = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($minatBalance && $citaBalance) {
|
||||
echo "\n✅ PASS: Keywords balance OK\n";
|
||||
} else {
|
||||
echo "\n⚠️ WARNING: Ada keywords yang tidak balanced\n";
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TEST 5: CHECK TOTAL RECORDS
|
||||
// ============================================
|
||||
echo "\n============================================\n";
|
||||
echo "✅ TEST 5: TOTAL RECORDS CHECK\n";
|
||||
echo "============================================\n";
|
||||
|
||||
$stats = [
|
||||
'Total Users' => \App\Models\User::count(),
|
||||
'Total Siswa' => \App\Models\User::where('role', 'siswa')->count(),
|
||||
'Total BK' => \App\Models\User::where('role', 'bk')->count(),
|
||||
'Total Admin' => \App\Models\User::where('role', 'admin')->count(),
|
||||
'Total Recommendations' => \App\Models\Recommendation::count(),
|
||||
'Total Chat History' => \App\Models\ChatHistory::count(),
|
||||
'Total Jurusan' => \App\Models\PolijeMajor::count(),
|
||||
];
|
||||
|
||||
foreach ($stats as $label => $count) {
|
||||
echo " $label: $count\n";
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TEST 6: SAMPLE RECOMMENDATION CHECK
|
||||
// ============================================
|
||||
echo "\n============================================\n";
|
||||
echo "✅ TEST 6: SAMPLE RECOMMENDATION CHECK\n";
|
||||
echo "============================================\n";
|
||||
|
||||
$sample = \App\Models\Recommendation::with('user')->first();
|
||||
if ($sample) {
|
||||
echo "Sample Recommendation:\n";
|
||||
echo " User: " . $sample->user->name . "\n";
|
||||
echo " Input Data:\n";
|
||||
echo " - Nilai Rata-rata: " . ($sample->input_data['nilai_rata_rata'] ?? 'N/A') . "\n";
|
||||
echo " - Minat (Raw): " . ($sample->input_data['minat_raw'] ?? 'N/A') . "\n";
|
||||
echo " - Minat (Mapped): " . ($sample->input_data['minat_mapped'] ?? 'N/A') . "\n";
|
||||
echo " - Cita-cita (Raw): " . ($sample->input_data['cita_cita_raw'] ?? 'N/A') . "\n";
|
||||
echo " - Cita-cita (Mapped): " . ($sample->input_data['cita_cita_mapped'] ?? 'N/A') . "\n";
|
||||
echo " Top Recommendation:\n";
|
||||
$topRec = $sample->hasil_rekomendasi[0];
|
||||
echo " - Jurusan: " . $topRec['jurusan'] . "\n";
|
||||
echo " - Percentage: " . $topRec['persentase'] . "%\n";
|
||||
} else {
|
||||
echo "❌ Tidak ada rekomendasi ditemukan\n";
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SUMMARY
|
||||
// ============================================
|
||||
echo "\n============================================\n";
|
||||
echo "✅ TEST COMPLETE\n";
|
||||
echo "============================================\n";
|
||||
echo "\nJika semua test PASS ✅, sistem siap untuk sidang!\n";
|
||||
echo "Jika ada yang FAIL ❌, silahkan perbaiki terlebih dahulu.\n\n";
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,272 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script untuk Import Data Alumni dari Excel ke Database SPK Jurusan Polije
|
||||
Membaca file Excel dan memasukkan ke tabel alumni
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Add Laravel project to path
|
||||
project_path = Path(__file__).parent
|
||||
sys.path.insert(0, str(project_path))
|
||||
|
||||
# Import database connection
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
def read_excel_file(file_path):
|
||||
"""
|
||||
Membaca file Excel dan return dataframe
|
||||
"""
|
||||
try:
|
||||
print(f"📂 Membaca file: {file_path}")
|
||||
df = pd.read_excel(file_path)
|
||||
print(f"✓ File berhasil dibaca")
|
||||
print(f" Baris: {len(df)}")
|
||||
print(f" Kolom: {list(df.columns)}")
|
||||
return df
|
||||
except Exception as e:
|
||||
print(f"✗ Error membaca file: {e}")
|
||||
return None
|
||||
|
||||
def normalize_column_names(df):
|
||||
"""
|
||||
Normalize nama kolom Excel ke format database
|
||||
"""
|
||||
# Mapping kemungkinan nama kolom di Excel
|
||||
column_mapping = {
|
||||
'nama': 'nama_alumni',
|
||||
'nama alumni': 'nama_alumni',
|
||||
'nis': 'nis',
|
||||
'no. induk siswa': 'nis',
|
||||
'kelompok asal': 'kelompok_asal',
|
||||
'matematika': 'mtk',
|
||||
'mtk': 'mtk',
|
||||
'math': 'mtk',
|
||||
'fisika': 'fisika',
|
||||
'physics': 'fisika',
|
||||
'kimia': 'kimia',
|
||||
'chemistry': 'kimia',
|
||||
'biologi': 'biologi',
|
||||
'biology': 'biologi',
|
||||
'ekonomi': 'ekonomi',
|
||||
'economics': 'ekonomi',
|
||||
'geografi': 'geografi',
|
||||
'geography': 'geografi',
|
||||
'sosiologi': 'sosiologi',
|
||||
'sociology': 'sosiologi',
|
||||
'sejarah': 'sejarah',
|
||||
'history': 'sejarah',
|
||||
'minat': 'minat',
|
||||
'interest': 'minat',
|
||||
'cita cita': 'cita_cita',
|
||||
'cita-cita': 'cita_cita',
|
||||
'dream job': 'cita_cita',
|
||||
'preferensi studi': 'preferensi_studi',
|
||||
'preference': 'preferensi_studi',
|
||||
'prestasi': 'prestasi',
|
||||
'achievement': 'prestasi',
|
||||
'jurusan masuk': 'major_masuk',
|
||||
'major': 'major_masuk',
|
||||
'jurusan': 'major_masuk',
|
||||
'tahun lulus': 'tahun_lulus_polije',
|
||||
'tahun lulus polije': 'tahun_lulus_polije',
|
||||
'graduation year': 'tahun_lulus_polije',
|
||||
'catatan': 'catatan',
|
||||
'notes': 'catatan',
|
||||
'keterangan': 'catatan',
|
||||
}
|
||||
|
||||
# Normalize column names
|
||||
df.columns = [col.lower().strip() for col in df.columns]
|
||||
df = df.rename(columns=column_mapping, errors='ignore')
|
||||
|
||||
return df
|
||||
|
||||
def validate_data(df):
|
||||
"""
|
||||
Validasi data sebelum insert
|
||||
"""
|
||||
print("\n🔍 Validasi Data:")
|
||||
|
||||
# Cek kolom penting
|
||||
required_cols = ['nama_alumni', 'nis', 'kelompok_asal']
|
||||
missing_cols = [col for col in required_cols if col not in df.columns]
|
||||
|
||||
if missing_cols:
|
||||
print(f"✗ Kolom penting tidak ada: {missing_cols}")
|
||||
return False
|
||||
|
||||
print(f"✓ Kolom penting ditemukan: {required_cols}")
|
||||
|
||||
# Cek null values
|
||||
null_counts = df.isnull().sum()
|
||||
if null_counts.any():
|
||||
print(f"⚠ Null values ditemukan:")
|
||||
for col, count in null_counts[null_counts > 0].items():
|
||||
print(f" - {col}: {count} baris")
|
||||
|
||||
return True
|
||||
|
||||
def generate_insert_script(df):
|
||||
"""
|
||||
Generate Laravel seeder script untuk insert data
|
||||
"""
|
||||
print(f"\n📝 Generate Insert Script...")
|
||||
|
||||
# Prepare data
|
||||
records = []
|
||||
for idx, row in df.iterrows():
|
||||
record = {
|
||||
'nama_alumni': str(row.get('nama_alumni', '')).strip() or f"Alumni {idx+1}",
|
||||
'nis': str(row.get('nis', '')).strip() or None,
|
||||
'kelompok_asal': str(row.get('kelompok_asal', 'IPA')).strip(),
|
||||
'mtk': float(row.get('mtk', 0)) if pd.notna(row.get('mtk')) else 0,
|
||||
'fisika': float(row.get('fisika', 0)) if pd.notna(row.get('fisika')) else 0,
|
||||
'kimia': float(row.get('kimia', 0)) if pd.notna(row.get('kimia')) else 0,
|
||||
'biologi': float(row.get('biologi', 0)) if pd.notna(row.get('biologi')) else 0,
|
||||
'ekonomi': float(row.get('ekonomi', 0)) if pd.notna(row.get('ekonomi')) else 0,
|
||||
'geografi': float(row.get('geografi', 0)) if pd.notna(row.get('geografi')) else 0,
|
||||
'sosiologi': float(row.get('sosiologi', 0)) if pd.notna(row.get('sosiologi')) else 0,
|
||||
'sejarah': float(row.get('sejarah', 0)) if pd.notna(row.get('sejarah')) else 0,
|
||||
'minat': str(row.get('minat', 'IPA')).strip(),
|
||||
'cita_cita': str(row.get('cita_cita', '')).strip() or 'Profesional',
|
||||
'preferensi_studi': str(row.get('preferensi_studi', 'Sains & Teknologi')).strip(),
|
||||
'prestasi': str(row.get('prestasi', 'Tidak')).strip(),
|
||||
'major_masuk': str(row.get('major_masuk', '')).strip() or None,
|
||||
'tahun_lulus_polije': int(row.get('tahun_lulus_polije', 2024)) if pd.notna(row.get('tahun_lulus_polije')) else 2024,
|
||||
'catatan': str(row.get('catatan', '')).strip() or None,
|
||||
}
|
||||
records.append(record)
|
||||
|
||||
return records
|
||||
|
||||
def create_seeder_file(records):
|
||||
"""
|
||||
Create Laravel Seeder file
|
||||
"""
|
||||
seeder_content = '''<?php
|
||||
|
||||
namespace Database\\Seeders;
|
||||
|
||||
use Illuminate\\Database\\Console\\Seeds\\WithoutModelEvents;
|
||||
use Illuminate\\Database\\Seeder;
|
||||
use App\\Models\\Alumni;
|
||||
|
||||
class AlumniImportSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$alumni = [
|
||||
'''
|
||||
|
||||
for record in records:
|
||||
seeder_content += f''' [
|
||||
'nama_alumni' => '{record['nama_alumni']}',
|
||||
'nis' => {f"'{record['nis']}'" if record['nis'] else 'null'},
|
||||
'kelompok_asal' => '{record['kelompok_asal']}',
|
||||
'mtk' => {record['mtk']},
|
||||
'fisika' => {record['fisika']},
|
||||
'kimia' => {record['kimia']},
|
||||
'biologi' => {record['biologi']},
|
||||
'ekonomi' => {record['ekonomi']},
|
||||
'geografi' => {record['geografi']},
|
||||
'sosiologi' => {record['sosiologi']},
|
||||
'sejarah' => {record['sejarah']},
|
||||
'minat' => '{record['minat']}',
|
||||
'cita_cita' => '{record['cita_cita']}',
|
||||
'preferensi_studi' => '{record['preferensi_studi']}',
|
||||
'prestasi' => '{record['prestasi']}',
|
||||
'major_masuk' => {f"'{record['major_masuk']}'" if record['major_masuk'] else 'null'},
|
||||
'tahun_lulus_polije' => {record['tahun_lulus_polije']},
|
||||
'catatan' => {f"'{record['catatan']}'" if record['catatan'] else 'null'},
|
||||
],
|
||||
'''
|
||||
|
||||
seeder_content += ''' ];
|
||||
|
||||
foreach ($alumni as $data) {
|
||||
Alumni::create($data);
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
# Write to file
|
||||
seeder_file = Path(__file__).parent / 'database' / 'seeders' / 'AlumniImportSeeder.php'
|
||||
seeder_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(seeder_file, 'w', encoding='utf-8') as f:
|
||||
f.write(seeder_content)
|
||||
|
||||
print(f"✓ Seeder file created: {seeder_file}")
|
||||
return str(seeder_file)
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main function
|
||||
"""
|
||||
print("=" * 60)
|
||||
print("IMPORT DATA ALUMNI KE DATABASE SPK JURUSAN POLIJE")
|
||||
print("=" * 60)
|
||||
|
||||
# Find Excel file
|
||||
excel_files = list(Path(__file__).parent.glob('*.xlsx')) + \
|
||||
list(Path(__file__).parent.glob('DATA *.xlsx')) + \
|
||||
list(Path(__file__).parent.glob('*TAMATAN*.xlsx'))
|
||||
|
||||
if not excel_files:
|
||||
print("✗ File Excel tidak ditemukan di folder project")
|
||||
print(" Cari file dengan nama: DATA RESAPAN TAMATAN.xlsx")
|
||||
return False
|
||||
|
||||
excel_file = excel_files[0]
|
||||
print(f"📄 File ditemukan: {excel_file.name}\n")
|
||||
|
||||
# Read Excel
|
||||
df = read_excel_file(str(excel_file))
|
||||
if df is None:
|
||||
return False
|
||||
|
||||
# Display first few rows
|
||||
print("\n📊 Preview Data (5 baris pertama):")
|
||||
print(df.head().to_string())
|
||||
|
||||
# Normalize columns
|
||||
df = normalize_column_names(df)
|
||||
print(f"\n✓ Kolom di-normalize")
|
||||
|
||||
# Validate
|
||||
if not validate_data(df):
|
||||
return False
|
||||
|
||||
# Generate records
|
||||
records = generate_insert_script(df)
|
||||
print(f"✓ {len(records)} records siap untuk di-insert")
|
||||
|
||||
# Show sample
|
||||
print(f"\n📋 Sample Record (pertama):")
|
||||
print(json.dumps(records[0], indent=2, default=str))
|
||||
|
||||
# Create seeder
|
||||
seeder_file = create_seeder_file(records)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("✓ IMPORT DATA SIAP!")
|
||||
print("=" * 60)
|
||||
print("\n📝 Cara menggunakan Seeder:")
|
||||
print("1. Jalankan command: php artisan db:seed --class=AlumniImportSeeder")
|
||||
print("2. Atau jalankan tanpa argument untuk seed semua: php artisan db:seed")
|
||||
print("\n")
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == '__main__':
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
|
|
@ -21,8 +21,8 @@
|
|||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="CACHE_DRIVER" value="array"/>
|
||||
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
|
||||
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,257 @@
|
|||
import os
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import requests
|
||||
from flask import Flask, jsonify, request
|
||||
from dotenv import load_dotenv
|
||||
|
||||
BASE_DIR = os.path.dirname(__file__)
|
||||
ROOT_ENV = os.path.abspath(os.path.join(BASE_DIR, "..", "..", ".env"))
|
||||
load_dotenv(ROOT_ENV)
|
||||
load_dotenv(os.path.join(BASE_DIR, ".env"))
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
|
||||
BACKEND_TOKEN = os.getenv("BACKEND_TOKEN", "")
|
||||
GEMINI_BASE_URL = os.getenv("GEMINI_BASE_URL", "https://generativelanguage.googleapis.com/v1beta/models")
|
||||
TIMEOUT_SECONDS = int(os.getenv("GEMINI_TIMEOUT", "30"))
|
||||
MAJORS_FILE_PATH = os.getenv("MAJORS_FILE_PATH", os.path.join(BASE_DIR, "majors_data.json"))
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s %(message)s",
|
||||
handlers=[
|
||||
logging.StreamHandler(),
|
||||
],
|
||||
)
|
||||
logger = logging.getLogger("python_backend")
|
||||
|
||||
|
||||
def _log(message: str) -> None:
|
||||
logger.info(message)
|
||||
|
||||
|
||||
def _is_authorized() -> bool:
|
||||
if not BACKEND_TOKEN:
|
||||
return True
|
||||
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if not auth_header.startswith("Bearer "):
|
||||
return False
|
||||
|
||||
token = auth_header.replace("Bearer ", "", 1).strip()
|
||||
return token == BACKEND_TOKEN
|
||||
|
||||
|
||||
def _http_error(message: str, status_code: int, detail: str = "") -> Any:
|
||||
payload = {
|
||||
"success": False,
|
||||
"message": message,
|
||||
}
|
||||
if detail:
|
||||
payload["error"] = detail
|
||||
return jsonify(payload), status_code
|
||||
|
||||
|
||||
def _extract_text(data: Dict[str, Any]) -> str:
|
||||
return (
|
||||
data.get("candidates", [{}])[0]
|
||||
.get("content", {})
|
||||
.get("parts", [{}])[0]
|
||||
.get("text", "")
|
||||
)
|
||||
|
||||
|
||||
def _load_majors_file() -> Dict[str, Any]:
|
||||
try:
|
||||
with open(MAJORS_FILE_PATH, "r", encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
except FileNotFoundError:
|
||||
return {
|
||||
"ok": False,
|
||||
"message": "Data jurusan belum tersedia.",
|
||||
"data": {"majors": []},
|
||||
}
|
||||
except json.JSONDecodeError as exc:
|
||||
return {
|
||||
"ok": False,
|
||||
"message": "Data jurusan tidak dapat dibaca.",
|
||||
"data": {"majors": []},
|
||||
}
|
||||
|
||||
majors = data.get("majors") if isinstance(data, dict) else []
|
||||
if not isinstance(majors, list):
|
||||
majors = []
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"schema_version": data.get("schema_version", "unknown") if isinstance(data, dict) else "unknown",
|
||||
"last_updated": data.get("last_updated", "unknown") if isinstance(data, dict) else "unknown",
|
||||
"notes": data.get("notes", "") if isinstance(data, dict) else "",
|
||||
"majors": majors,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _build_majors_context_text(majors_data: Dict[str, Any]) -> str:
|
||||
majors = majors_data.get("majors", [])
|
||||
if not majors:
|
||||
return ""
|
||||
|
||||
lines = [
|
||||
"DATA JURUSAN RESMI (WAJIB JADI ACUAN UTAMA)",
|
||||
f"schema_version: {majors_data.get('schema_version', 'unknown')}",
|
||||
f"last_updated: {majors_data.get('last_updated', 'unknown')}",
|
||||
"Gunakan data berikut untuk menjawab informasi jurusan secara konsisten.",
|
||||
"Jika ada konflik dengan asumsi model, utamakan data ini.",
|
||||
]
|
||||
|
||||
for index, major in enumerate(majors, start=1):
|
||||
name = major.get("name", "-")
|
||||
description = major.get("description", "-")
|
||||
prospects = major.get("career_prospects", "-")
|
||||
preferences = ", ".join(major.get("study_preferences", [])) or "-"
|
||||
keywords = ", ".join(major.get("keywords", [])) or "-"
|
||||
|
||||
lines.append(
|
||||
f"{index}. {name} | deskripsi: {description} | preferensi studi: {preferences} | prospek: {prospects} | kata kunci: {keywords}"
|
||||
)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _inject_majors_context(payload: Dict[str, Any], majors_context: str) -> Dict[str, Any]:
|
||||
if not majors_context:
|
||||
return payload
|
||||
|
||||
contents = payload.get("contents", [])
|
||||
if not isinstance(contents, list) or not contents:
|
||||
return payload
|
||||
|
||||
first = contents[0]
|
||||
if isinstance(first, dict) and first.get("role") == "user":
|
||||
parts = first.get("parts", [])
|
||||
if isinstance(parts, list) and parts and isinstance(parts[0], dict):
|
||||
original_text = str(parts[0].get("text", ""))
|
||||
parts[0]["text"] = f"{majors_context}\n\n{original_text}"
|
||||
return payload
|
||||
|
||||
contents.insert(0, {"role": "user", "parts": [{"text": majors_context}]})
|
||||
return payload
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health() -> Any:
|
||||
majors_result = _load_majors_file()
|
||||
majors_count = len(majors_result["data"].get("majors", []))
|
||||
_log("[PY-BACKEND] GET /health")
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
"gemini_key_configured": bool(GEMINI_API_KEY),
|
||||
"majors_file": MAJORS_FILE_PATH,
|
||||
"majors_loaded": majors_count,
|
||||
"majors_valid": majors_result["ok"],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/majors")
|
||||
def majors() -> Any:
|
||||
result = _load_majors_file()
|
||||
status = 200 if result["ok"] else 500
|
||||
return jsonify(result), status
|
||||
|
||||
|
||||
@app.post("/api/chat")
|
||||
def chat() -> Any:
|
||||
_log("[PY-BACKEND] POST /api/chat")
|
||||
if not _is_authorized():
|
||||
_log("[PY-BACKEND] Unauthorized request")
|
||||
return _http_error(
|
||||
"Akses ke layanan chatbot ditolak.",
|
||||
401,
|
||||
)
|
||||
|
||||
if not GEMINI_API_KEY:
|
||||
return _http_error(
|
||||
"Layanan chatbot belum siap digunakan.",
|
||||
500,
|
||||
)
|
||||
|
||||
body = request.get_json(silent=True) or {}
|
||||
payload = body.get("payload")
|
||||
models = body.get("models", ["gemini-2.5-flash", "gemini-2.0-flash", "gemini-2.0-flash-lite"])
|
||||
|
||||
if not isinstance(payload, dict) or not payload.get("contents"):
|
||||
_log("[PY-BACKEND] Invalid payload")
|
||||
return _http_error(
|
||||
"Pesan belum dapat diproses. Silakan perjelas pertanyaan Anda.",
|
||||
422,
|
||||
)
|
||||
|
||||
majors_result = _load_majors_file()
|
||||
if majors_result["ok"]:
|
||||
majors_context = _build_majors_context_text(majors_result["data"])
|
||||
payload = _inject_majors_context(payload, majors_context)
|
||||
_log(f"[PY-BACKEND] Majors context loaded: {len(majors_result['data'].get('majors', []))} jurusan")
|
||||
else:
|
||||
_log(f"[PY-BACKEND] Warning: {majors_result['message']}")
|
||||
|
||||
last_error = ""
|
||||
for model in models:
|
||||
url = f"{GEMINI_BASE_URL}/{model}:generateContent?key={GEMINI_API_KEY}"
|
||||
try:
|
||||
resp = requests.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=TIMEOUT_SECONDS,
|
||||
)
|
||||
except requests.RequestException as exc:
|
||||
last_error = str(exc)
|
||||
continue
|
||||
|
||||
if resp.ok:
|
||||
data = resp.json()
|
||||
text = _extract_text(data)
|
||||
if text:
|
||||
_log(f"[PY-BACKEND] Success using model: {model}")
|
||||
return jsonify({"success": True, "message": text, "model": model})
|
||||
last_error = "Jawaban dari layanan tidak ditemukan."
|
||||
continue
|
||||
|
||||
if resp.status_code in (404, 429):
|
||||
if resp.status_code == 429:
|
||||
time.sleep(1)
|
||||
if resp.status_code == 429:
|
||||
last_error = (
|
||||
"Layanan chatbot sedang ramai digunakan. Silakan tunggu sebentar lalu coba lagi."
|
||||
)
|
||||
else:
|
||||
last_error = "Layanan chatbot sementara tidak tersedia."
|
||||
continue
|
||||
|
||||
last_error = (
|
||||
"Terjadi gangguan pada layanan chatbot."
|
||||
)
|
||||
|
||||
_log(f"[PY-BACKEND] Failed all models: {last_error}")
|
||||
return _http_error(
|
||||
"Maaf, layanan chatbot sedang mengalami gangguan. Silakan coba kembali beberapa saat lagi.",
|
||||
502,
|
||||
last_error,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
host = os.getenv("PY_BACKEND_HOST", "0.0.0.0")
|
||||
port = int(os.getenv("PY_BACKEND_PORT", "5000"))
|
||||
debug = os.getenv("PY_BACKEND_DEBUG", "true").lower() == "true"
|
||||
app.run(host=host, port=port, debug=debug)
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
2026-04-23 00:18:45,831 INFO [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* Running on all addresses (0.0.0.0)
|
||||
* Running on http://127.0.0.1:5000
|
||||
* Running on http://192.168.18.25:5000
|
||||
2026-04-23 00:18:45,878 INFO [33mPress CTRL+C to quit[0m
|
||||
2026-04-23 00:18:45,921 INFO * Restarting with stat
|
||||
2026-04-23 00:18:48,881 WARNING * Debugger is active!
|
||||
2026-04-23 00:18:48,891 INFO * Debugger PIN: 786-713-650
|
||||
2026-04-23 00:35:47,917 INFO [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* Running on all addresses (0.0.0.0)
|
||||
* Running on http://127.0.0.1:5000
|
||||
* Running on http://192.168.18.25:5000
|
||||
2026-04-23 00:35:48,031 INFO [33mPress CTRL+C to quit[0m
|
||||
2026-04-23 00:35:48,180 INFO * Restarting with stat
|
||||
2026-04-23 00:35:50,518 WARNING * Debugger is active!
|
||||
2026-04-23 00:35:50,529 INFO * Debugger PIN: 786-713-650
|
||||
2026-04-27 07:40:08,029 INFO [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* Running on all addresses (0.0.0.0)
|
||||
* Running on http://127.0.0.1:5000
|
||||
* Running on http://192.168.18.25:5000
|
||||
2026-04-27 07:40:08,057 INFO [33mPress CTRL+C to quit[0m
|
||||
2026-04-27 07:40:08,145 INFO * Restarting with stat
|
||||
2026-04-27 07:41:46,436 INFO [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* Running on all addresses (0.0.0.0)
|
||||
* Running on http://127.0.0.1:5000
|
||||
* Running on http://192.168.18.25:5000
|
||||
2026-04-27 07:41:46,446 INFO [33mPress CTRL+C to quit[0m
|
||||
2026-04-27 07:41:46,458 INFO * Restarting with stat
|
||||
2026-04-27 07:41:47,441 WARNING * Debugger is active!
|
||||
2026-04-27 07:41:47,445 INFO * Debugger PIN: 531-826-879
|
||||
2026-04-27 11:18:31,510 INFO [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* Running on all addresses (0.0.0.0)
|
||||
* Running on http://127.0.0.1:5000
|
||||
* Running on http://192.168.43.22:5000
|
||||
2026-04-27 11:18:31,511 INFO [33mPress CTRL+C to quit[0m
|
||||
2026-04-27 11:18:31,517 INFO * Restarting with stat
|
||||
2026-04-27 11:21:28,451 INFO [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* Running on all addresses (0.0.0.0)
|
||||
* Running on http://127.0.0.1:5000
|
||||
* Running on http://192.168.43.22:5000
|
||||
2026-04-27 11:21:28,452 INFO [33mPress CTRL+C to quit[0m
|
||||
2026-04-27 11:21:28,456 INFO * Restarting with stat
|
||||
2026-04-27 11:21:29,336 WARNING * Debugger is active!
|
||||
2026-04-27 11:21:29,347 INFO * Debugger PIN: 786-713-650
|
||||
2026-04-27 11:21:37,456 INFO [PY-BACKEND] POST /api/chat
|
||||
2026-04-27 11:21:37,480 INFO [PY-BACKEND] Majors context loaded: 9 jurusan
|
||||
2026-04-27 11:21:47,011 INFO [PY-BACKEND] Success using model: gemini-2.5-flash
|
||||
2026-04-27 11:21:47,021 INFO 127.0.0.1 - - [27/Apr/2026 11:21:47] "POST /api/chat HTTP/1.1" 200 -
|
||||
2026-04-28 09:21:46,306 INFO [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* Running on all addresses (0.0.0.0)
|
||||
* Running on http://127.0.0.1:5000
|
||||
* Running on http://192.168.18.25:5000
|
||||
2026-04-28 09:21:46,322 INFO [33mPress CTRL+C to quit[0m
|
||||
2026-04-28 09:21:46,346 INFO * Restarting with stat
|
||||
2026-04-28 09:21:47,094 WARNING * Debugger is active!
|
||||
2026-04-28 09:21:47,099 INFO * Debugger PIN: 786-713-650
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
{
|
||||
"schema_version": "1.0",
|
||||
"last_updated": "2026-04-21",
|
||||
"notes": "Edit file ini jika ada perubahan data jurusan. Backend Python akan membaca file ini untuk konteks chatbot.",
|
||||
"majors": [
|
||||
{
|
||||
"id": "produksi_pertanian",
|
||||
"name": "Produksi Pertanian",
|
||||
"description": "Mempelajari budidaya tanaman, pengelolaan lahan, dan produksi hasil pertanian modern.",
|
||||
"keywords": ["pertanian", "budidaya", "tanaman", "agronomi", "pangan"],
|
||||
"study_preferences": ["Pertanian & Lingkungan"],
|
||||
"career_prospects": "Petani modern, konsultan pertanian, pengelola perkebunan, peneliti pertanian, agronomis."
|
||||
},
|
||||
{
|
||||
"id": "teknologi_pertanian",
|
||||
"name": "Teknologi Pertanian",
|
||||
"description": "Mengintegrasikan teknologi dengan pertanian, termasuk mekanisasi, otomasi, dan pengolahan hasil pangan.",
|
||||
"keywords": ["teknologi pertanian", "mekanisasi", "otomasi", "smart farming", "teknologi pangan"],
|
||||
"study_preferences": ["Sains & Teknologi", "Pertanian & Lingkungan"],
|
||||
"career_prospects": "Teknisi pertanian, ahli mekanisasi, quality control pangan, peneliti teknologi pangan."
|
||||
},
|
||||
{
|
||||
"id": "peternakan",
|
||||
"name": "Peternakan",
|
||||
"description": "Mempelajari pengelolaan ternak, nutrisi hewan, reproduksi, dan pengolahan produk peternakan.",
|
||||
"keywords": ["peternakan", "ternak", "nutrisi hewan", "farm management", "kesehatan hewan"],
|
||||
"study_preferences": ["Pertanian & Lingkungan", "Kesehatan & Ilmu Hayat"],
|
||||
"career_prospects": "Peternak profesional, konsultan peternakan, manajer peternakan, ahli nutrisi hewan."
|
||||
},
|
||||
{
|
||||
"id": "manajemen_agribisnis",
|
||||
"name": "Manajemen Agribisnis",
|
||||
"description": "Menggabungkan ilmu pertanian dan bisnis, termasuk manajemen usaha, pemasaran, dan kewirausahaan agribisnis.",
|
||||
"keywords": ["agribisnis", "manajemen", "pemasaran", "wirausaha", "supply chain"],
|
||||
"study_preferences": ["Bisnis & Manajemen", "Pertanian & Lingkungan"],
|
||||
"career_prospects": "Manajer agribisnis, entrepreneur pertanian, konsultan pemasaran pertanian, analis pasar komoditas."
|
||||
},
|
||||
{
|
||||
"id": "teknologi_informasi",
|
||||
"name": "Teknologi Informasi",
|
||||
"description": "Mempelajari pengembangan software, data, jaringan, keamanan siber, dan teknologi digital terapan.",
|
||||
"keywords": ["programming", "software", "web", "data", "cybersecurity"],
|
||||
"study_preferences": ["Sains & Teknologi"],
|
||||
"career_prospects": "Software developer, web developer, network engineer, data analyst, cybersecurity specialist."
|
||||
},
|
||||
{
|
||||
"id": "teknik",
|
||||
"name": "Teknik",
|
||||
"description": "Mempelajari mesin, kelistrikan, elektronika, otomasi, dan sistem teknik industri.",
|
||||
"keywords": ["mesin", "listrik", "otomasi", "manufaktur", "mekatronika"],
|
||||
"study_preferences": ["Sains & Teknologi"],
|
||||
"career_prospects": "Teknisi mesin, ahli listrik, engineer industri, maintenance engineer, kontraktor."
|
||||
},
|
||||
{
|
||||
"id": "kesehatan",
|
||||
"name": "Kesehatan",
|
||||
"description": "Mempelajari ilmu kesehatan terapan, gizi, rekam medis, dan pelayanan kesehatan masyarakat.",
|
||||
"keywords": ["kesehatan", "gizi", "rekam medis", "farmasi", "kesehatan masyarakat"],
|
||||
"study_preferences": ["Kesehatan & Ilmu Hayat"],
|
||||
"career_prospects": "Ahli gizi, perekam medis, tenaga kesehatan, asisten apoteker, sanitarian."
|
||||
},
|
||||
{
|
||||
"id": "bahasa_komunikasi_pariwisata",
|
||||
"name": "Bahasa, Komunikasi, dan Pariwisata",
|
||||
"description": "Mempelajari bahasa, komunikasi profesional, perhotelan, layanan publik, dan industri pariwisata.",
|
||||
"keywords": ["bahasa", "komunikasi", "pariwisata", "hospitality", "media"],
|
||||
"study_preferences": ["Sosial & Humaniora", "Bisnis & Manajemen"],
|
||||
"career_prospects": "Tour guide, staf perhotelan, jurnalis, public relation, penerjemah, staf maskapai."
|
||||
},
|
||||
{
|
||||
"id": "bisnis",
|
||||
"name": "Bisnis",
|
||||
"description": "Mempelajari akuntansi, manajemen bisnis, perbankan, keuangan, dan administrasi niaga.",
|
||||
"keywords": ["akuntansi", "manajemen", "perbankan", "keuangan", "analisis bisnis"],
|
||||
"study_preferences": ["Bisnis & Manajemen"],
|
||||
"career_prospects": "Akuntan, staf perbankan, manajer bisnis, marketing executive, analis keuangan."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
flask==3.0.3
|
||||
requests==2.32.3
|
||||
python-dotenv==1.0.1
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
@extends('admin.layouts.app')
|
||||
|
||||
@section('title', 'Tambah Alumni')
|
||||
|
||||
@section('content')
|
||||
<div class="mb-6">
|
||||
<h2 class="text-2xl font-bold text-maroon">➕ Tambah Alumni</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">Input data alumni SMA Bima Ambulu</p>
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<div class="bg-red-50 border-l-4 border-red-400 p-4 rounded-lg mb-6">
|
||||
<p class="text-red-800 text-sm font-bold mb-2">❌ Validasi gagal:</p>
|
||||
<ul class="list-disc pl-5 text-sm text-red-700">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('admin.alumni.store') }}" method="POST" class="max-w-2xl">
|
||||
@csrf
|
||||
|
||||
<!-- Data Dasar -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-maroon">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📋 Data Dasar</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama Alumni *</label>
|
||||
<input type="text" name="nama_alumni" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('nama_alumni') }}" placeholder="Nama lengkap">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">NIS</label>
|
||||
<input type="text" name="nis" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('nis') }}" placeholder="NIS SMA">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Kelompok Asal *</label>
|
||||
<select name="kelompok_asal" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm">
|
||||
<option value="">-- Pilih --</option>
|
||||
<option value="IPA" {{ old('kelompok_asal') == 'IPA' ? 'selected' : '' }}>IPA</option>
|
||||
<option value="IPS" {{ old('kelompok_asal') == 'IPS' ? 'selected' : '' }}>IPS</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Minat / Bidang Studi</label>
|
||||
<input type="text" name="minat" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('minat') }}" placeholder="Minat siswa">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nilai Entry -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-yellow-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📊 Nilai Saat Entry (Rapor SMA)</h3>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Matematika</label>
|
||||
<input type="number" name="mtk" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('mtk') }}" placeholder="0-100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Fisika</label>
|
||||
<input type="number" name="fisika" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('fisika') }}" placeholder="0-100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Kimia</label>
|
||||
<input type="number" name="kimia" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('kimia') }}" placeholder="0-100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Biologi</label>
|
||||
<input type="number" name="biologi" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('biologi') }}" placeholder="0-100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Ekonomi</label>
|
||||
<input type="number" name="ekonomi" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('ekonomi') }}" placeholder="0-100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Geografi</label>
|
||||
<input type="number" name="geografi" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('geografi') }}" placeholder="0-100">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hasil -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-green-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">🎯 Hasil / Outcome</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Jurusan Masuk ke Polije *</label>
|
||||
<input type="text" name="major_masuk" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('major_masuk') }}" placeholder="Jurusan Polije">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Tahun Lulus Polije</label>
|
||||
<input type="number" name="tahun_lulus_polije" min="2020" max="{{ date('Y') }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('tahun_lulus_polije') }}" placeholder="Tahun lulus">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Catatan</label>
|
||||
<textarea name="catatan" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
placeholder="Catatan tambahan (opsional)">{{ old('catatan') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex gap-4 justify-end">
|
||||
<a href="{{ route('admin.alumni.index') }}" class="px-6 py-2 rounded-lg font-bold bg-gray-300 text-gray-700 hover:bg-gray-400 transition">
|
||||
Batal
|
||||
</a>
|
||||
<button type="submit" class="px-6 py-2 rounded-lg font-bold gradient-maroon text-white hover:opacity-90 transition">
|
||||
💾 Simpan
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
@extends('admin.layouts.app')
|
||||
|
||||
@section('title', 'Edit Alumni')
|
||||
|
||||
@section('content')
|
||||
<div class="mb-6">
|
||||
<h2 class="text-2xl font-bold text-maroon">✏ Edit Alumni</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ $alumni->nama_alumni }}</p>
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<div class="bg-red-50 border-l-4 border-red-400 p-4 rounded-lg mb-6">
|
||||
<p class="text-red-800 text-sm font-bold mb-2">❌ Validasi gagal:</p>
|
||||
<ul class="list-disc pl-5 text-sm text-red-700">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('admin.alumni.update', $alumni->id) }}" method="POST" class="max-w-2xl">
|
||||
@csrf @method('PUT')
|
||||
|
||||
<!-- Data Dasar -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-maroon">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📋 Data Dasar</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama Alumni *</label>
|
||||
<input type="text" name="nama_alumni" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('nama_alumni', $alumni->nama_alumni) }}" placeholder="Nama lengkap">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">NIS</label>
|
||||
<input type="text" name="nis" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('nis', $alumni->nis) }}" placeholder="NIS SMA">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Kelompok Asal *</label>
|
||||
<select name="kelompok_asal" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm">
|
||||
<option value="">-- Pilih --</option>
|
||||
<option value="IPA" {{ old('kelompok_asal', $alumni->kelompok_asal) == 'IPA' ? 'selected' : '' }}>IPA</option>
|
||||
<option value="IPS" {{ old('kelompok_asal', $alumni->kelompok_asal) == 'IPS' ? 'selected' : '' }}>IPS</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Minat / Bidang Studi</label>
|
||||
<input type="text" name="minat" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('minat', $alumni->minat) }}" placeholder="Minat siswa">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nilai Entry -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-yellow-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📊 Nilai Saat Entry (Rapor SMA)</h3>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Matematika</label>
|
||||
<input type="number" name="mtk" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('mtk', $alumni->mtk) }}" placeholder="0-100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Fisika</label>
|
||||
<input type="number" name="fisika" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('fisika', $alumni->fisika) }}" placeholder="0-100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Kimia</label>
|
||||
<input type="number" name="kimia" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('kimia', $alumni->kimia) }}" placeholder="0-100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Biologi</label>
|
||||
<input type="number" name="biologi" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('biologi', $alumni->biologi) }}" placeholder="0-100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Ekonomi</label>
|
||||
<input type="number" name="ekonomi" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('ekonomi', $alumni->ekonomi) }}" placeholder="0-100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Geografi</label>
|
||||
<input type="number" name="geografi" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('geografi', $alumni->geografi) }}" placeholder="0-100">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hasil -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-green-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">🎯 Hasil / Outcome</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Jurusan Masuk ke Polije *</label>
|
||||
<input type="text" name="major_masuk" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('major_masuk', $alumni->major_masuk) }}" placeholder="Jurusan Polije">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Tahun Lulus Polije</label>
|
||||
<input type="number" name="tahun_lulus_polije" min="2020" max="{{ date('Y') }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('tahun_lulus_polije', $alumni->tahun_lulus_polije) }}" placeholder="Tahun lulus">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Catatan</label>
|
||||
<textarea name="catatan" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
placeholder="Catatan tambahan (opsional)">{{ old('catatan', $alumni->catatan) }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex gap-4 justify-end">
|
||||
<a href="{{ route('admin.alumni.index') }}" class="px-6 py-2 rounded-lg font-bold bg-gray-300 text-gray-700 hover:bg-gray-400 transition">
|
||||
Batal
|
||||
</a>
|
||||
<button type="submit" class="px-6 py-2 rounded-lg font-bold gradient-maroon text-white hover:opacity-90 transition">
|
||||
💾 Update
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
@extends('admin.layouts.app')
|
||||
|
||||
@section('title', 'Data Alumni')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Header -->
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6 gap-3">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-maroon">🎓 Data Alumni</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">Kelola data alumni SMA Bima Ambulu yang masuk ke Polije</p>
|
||||
</div>
|
||||
<a href="{{ route('admin.alumni.create') }}" class="gradient-maroon text-white font-bold py-2 px-4 rounded-lg hover:opacity-90 transition text-sm">
|
||||
+ Tambah Alumni
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="bg-green-50 border-l-4 border-green-400 p-4 rounded-lg mb-6">
|
||||
<p class="text-green-800 text-sm font-semibold">✅ {{ session('success') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Alumni Table -->
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="gradient-maroon text-white">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold">Nama Alumni</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold">NIS</th>
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold">Kelompok</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold">Jurusan Masuk</th>
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold">Tahun Lulus</th>
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
@forelse($alumni as $a)
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
<td class="px-6 py-4 font-semibold text-gray-800">{{ $a->nama_alumni }}</td>
|
||||
<td class="px-6 py-4 text-gray-600">{{ $a->nis ?? '-' }}</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<span class="px-3 py-1 rounded text-xs font-bold"
|
||||
style="{{ $a->kelompok_asal == 'IPA' ? 'background-color: #E0F2FE; color: #0369A1;' : 'background-color: #FEF3C7; color: #92400E;' }}">
|
||||
{{ $a->kelompok_asal }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-gray-800">{{ $a->major_masuk }}</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
@if($a->tahun_lulus_polije)
|
||||
<span class="px-2 py-1 rounded bg-blue-100 text-blue-800 text-sm font-semibold">{{ $a->tahun_lulus_polije }}</span>
|
||||
@else
|
||||
<span class="text-gray-400 text-sm">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center gap-2 flex justify-center">
|
||||
<a href="{{ route('admin.alumni.show', $a->id) }}" class="text-blue-600 hover:text-blue-800 font-semibold text-sm">👁 Lihat</a>
|
||||
<a href="{{ route('admin.alumni.edit', $a->id) }}" class="text-yellow-600 hover:text-yellow-800 font-semibold text-sm">✏ Edit</a>
|
||||
<form action="{{ route('admin.alumni.destroy', $a->id) }}" method="POST" class="inline swal-confirm" data-confirm-message="Yakin hapus?">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="text-red-600 hover:text-red-800 font-semibold text-sm">🗑 Hapus</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-8 text-center text-gray-500">
|
||||
Belum ada data alumni. <a href="{{ route('admin.alumni.create') }}" class="text-maroon font-bold hover:underline">Tambah sekarang</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if($alumni->hasPages())
|
||||
<div class="mt-6">
|
||||
{{ $alumni->links() }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Info Box -->
|
||||
<div class="mt-8 p-4 bg-blue-50 border-l-4 border-blue-400 rounded">
|
||||
<p class="text-sm text-blue-800">
|
||||
<strong>📊 Catatan:</strong><br>
|
||||
Data alumni digunakan untuk tracking alumni SMA Bima Ambulu yang melanjutkan ke Polije, monitoring career development, dan referensi untuk siswa baru dalam memilih jurusan.
|
||||
</p>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
@extends('admin.layouts.app')
|
||||
|
||||
@section('title', 'Detail Alumni')
|
||||
|
||||
@section('content')
|
||||
<div class="mb-6 flex justify-between items-start">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-maroon">👁 Detail Alumni</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ $alumni->nama_alumni }}</p>
|
||||
</div>
|
||||
<a href="{{ route('admin.alumni.index') }}" class="px-4 py-2 rounded-lg font-bold bg-gray-300 text-gray-700 hover:bg-gray-400 transition">
|
||||
← Kembali
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Data Dasar -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-maroon">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📋 Data Dasar</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p class="text-xs text-gray-600 font-semibold">Nama Alumni</p>
|
||||
<p class="text-lg font-bold text-gray-800">{{ $alumni->nama_alumni }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-600 font-semibold">NIS</p>
|
||||
<p class="text-lg font-bold text-gray-800">{{ $alumni->nis ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-600 font-semibold">Kelompok Asal</p>
|
||||
<span class="inline-block px-3 py-1 rounded text-sm font-bold"
|
||||
style="{{ $alumni->kelompok_asal == 'IPA' ? 'background-color: #E0F2FE; color: #0369A1;' : 'background-color: #FEF3C7; color: #92400E;' }}">
|
||||
{{ $alumni->kelompok_asal }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-600 font-semibold">Minat</p>
|
||||
<p class="text-gray-800">{{ $alumni->minat ?? '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nilai Rata-rata -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-yellow-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📊 Nilai Rata-Rata (Rapor SMA)</h3>
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
@if($alumni->nilai_rata_rata)
|
||||
<div class="p-3 bg-blue-50 rounded-lg">
|
||||
<p class="text-xs text-gray-600">Nilai Rata-Rata</p>
|
||||
<p class="text-3xl font-bold text-blue-600">{{ $alumni->nilai_rata_rata }}</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="p-3 bg-gray-50 rounded-lg">
|
||||
<p class="text-xs text-gray-600">Nilai Rata-Rata</p>
|
||||
<p class="text-gray-500">-</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hasil -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-green-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">🎯 Hasil / Outcome</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p class="text-xs text-gray-600 font-semibold">Jurusan Masuk Polije</p>
|
||||
<p class="text-lg font-bold text-gray-800">{{ $alumni->major_masuk }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-600 font-semibold">Tahun Lulus SMA Bima Ambulu</p>
|
||||
<p class="text-lg font-bold text-gray-800">{{ $alumni->tahun_lulus_polije ?? '-' }}</p>
|
||||
</div>
|
||||
@if($alumni->catatan)
|
||||
<div class="md:col-span-2">
|
||||
<p class="text-xs text-gray-600 font-semibold">Catatan</p>
|
||||
<p class="text-gray-800">{{ $alumni->catatan }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex gap-4 justify-end">
|
||||
<a href="{{ route('admin.alumni.edit', $alumni->id) }}" class="px-6 py-2 rounded-lg font-bold bg-yellow-400 text-maroon hover:bg-yellow-300 transition">
|
||||
✏ Edit
|
||||
</a>
|
||||
<form action="{{ route('admin.alumni.destroy', $alumni->id) }}" method="POST" class="inline swal-confirm" data-confirm-message="Yakin hapus data alumni ini?">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="px-6 py-2 rounded-lg font-bold bg-red-500 text-white hover:bg-red-600 transition">
|
||||
🗑 Hapus
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -3,6 +3,13 @@
|
|||
@section('title', 'Dashboard')
|
||||
|
||||
@section('content')
|
||||
<!-- Logout All Users Button (hidden for safety during sidang) -->
|
||||
{{-- <div class="mb-6">
|
||||
<button onclick="confirmLogoutAllUsers()" class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition flex items-center gap-2 font-semibold">
|
||||
🚪 Logout Semua User
|
||||
</button>
|
||||
</div> --}}
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<div class="stat-card bg-white rounded-lg shadow p-6 border-t-4 border-maroon">
|
||||
|
|
@ -23,40 +30,41 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kelompok Distribution -->
|
||||
<!-- Charts Section -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-maroon">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📊 Siswa per Kelompok</h3>
|
||||
<div class="space-y-3">
|
||||
@foreach($kelompokStats as $stat)
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-semibold text-gray-700 w-20">{{ $stat->kelompok_asal ?? 'Tidak Ada' }}</span>
|
||||
<div class="flex-1 h-6 bg-gray-200 rounded">
|
||||
<div class="h-full rounded flex items-center justify-center text-white text-xs font-bold"
|
||||
style="width: {{ $totalSiswa > 0 ? ($stat->count / $totalSiswa) * 100 : 0 }}%; background-color: {{ $stat->kelompok_asal == 'IPA' ? '#0369A1' : '#D97706' }};">
|
||||
{{ $stat->count }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
<!-- Rekomendasi Distribution Chart -->
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-purple-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📊 Distribusi Rekomendasi per Jurusan</h3>
|
||||
<div style="position: relative; height: 300px;">
|
||||
<canvas id="chartRecommendations"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Recommended Majors -->
|
||||
<!-- Kelompok Distribution Chart -->
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-indigo-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📈 Distribusi Siswa per Kelompok</h3>
|
||||
<div style="position: relative; height: 300px;">
|
||||
<canvas id="chartKelompok"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rekomendasi & Top Majors -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<!-- Rekomendasi per Kelompok -->
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-maroon">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📊 Rekomendasi per Kelompok</h3>
|
||||
<div style="position: relative; height: 250px;">
|
||||
<canvas id="chartRekomendasiKelompok"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Recommended Majors Chart -->
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-green-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">🎯 Top Recommended Majors</h3>
|
||||
@if($topMajors->isNotEmpty())
|
||||
<div class="space-y-3">
|
||||
@foreach($topMajors as $major)
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-semibold text-gray-700 flex-1 truncate">{{ $major->major_name }}</span>
|
||||
<span class="px-3 py-1 rounded bg-green-100 text-green-800 font-bold text-sm">{{ $major->count }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<p class="text-gray-500 text-sm">Belum ada data rekomendasi</p>
|
||||
@endif
|
||||
<div style="position: relative; height: 250px;">
|
||||
<canvas id="chartTopMajors"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -66,7 +74,7 @@
|
|||
@if($recentStudents->isNotEmpty())
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="border-b-2 border-maroon">
|
||||
<thead class="border-b-2 border-purple-500">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-2 font-bold text-maroon">Nama</th>
|
||||
<th class="text-center px-4 py-2 font-bold text-maroon">NIS</th>
|
||||
|
|
@ -85,7 +93,7 @@
|
|||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-center">
|
||||
<a href="{{ route('admin.student.detail', $student->id) }}" class="text-blue-600 hover:text-blue-800 font-semibold text-xs">👁 Lihat</a>
|
||||
<a href="{{ route('admin.student.detail', $student->id) }}" class="text-purple-600 hover:text-purple-800 font-semibold text-xs">👁 Lihat</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
|
|
@ -135,3 +143,178 @@
|
|||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
|
||||
<script>
|
||||
// Chart 1: Rekomendasi Distribution
|
||||
const chartRecommendationsCtx = document.getElementById('chartRecommendations').getContext('2d');
|
||||
const chartRecommendations = new Chart(chartRecommendationsCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: @json($chartMajorNames),
|
||||
datasets: [{
|
||||
data: @json($chartMajorCounts),
|
||||
backgroundColor: [
|
||||
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF',
|
||||
'#FF9F40', '#FF6384', '#C9CBCF', '#4BC0C0'
|
||||
],
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
font: { size: 11 },
|
||||
padding: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Chart 2: Kelompok Distribution
|
||||
const chartKelompokCtx = document.getElementById('chartKelompok').getContext('2d');
|
||||
const chartKelompok = new Chart(chartKelompokCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: @json($chartKelompokNames),
|
||||
datasets: [{
|
||||
label: 'Jumlah Siswa',
|
||||
data: @json($chartKelompokCounts),
|
||||
backgroundColor: ['#0369A1', '#D97706'],
|
||||
borderColor: ['#0369A1', '#D97706'],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: {
|
||||
font: { size: 11 }
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
font: { size: 10 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Chart 3: Rekomendasi per Kelompok Bar Chart
|
||||
const chartRekomendasiKelompokCtx = document.getElementById('chartRekomendasiKelompok').getContext('2d');
|
||||
const chartRekomendasiKelompok = new Chart(chartRekomendasiKelompokCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: @json($rekomendasiPerKelompok->pluck('kelompok_asal')->toArray()),
|
||||
datasets: [{
|
||||
label: 'Jumlah Rekomendasi',
|
||||
data: @json($rekomendasiPerKelompok->pluck('count')->toArray()),
|
||||
backgroundColor: ['#0369A1', '#D97706'],
|
||||
borderColor: ['#0369A1', '#D97706'],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: {
|
||||
font: { size: 11 }
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
font: { size: 10 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Chart 4: Top Majors Horizontal Bar Chart
|
||||
const chartTopMajorsCtx = document.getElementById('chartTopMajors').getContext('2d');
|
||||
const chartTopMajors = new Chart(chartTopMajorsCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: @json($topMajorsChart),
|
||||
datasets: [{
|
||||
label: 'Jumlah Rekomendasi',
|
||||
data: @json($topMajorsCounts),
|
||||
backgroundColor: '#36A2EB',
|
||||
borderColor: '#36A2EB',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: {
|
||||
font: { size: 11 }
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
font: { size: 10 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Logout All Users Confirmation Script -->
|
||||
<script>
|
||||
function confirmLogoutAllUsers() {
|
||||
// First confirmation
|
||||
if (!confirm('⚠️ WARNING! Ini akan logout SEMUA user (Siswa, BK, Admin)!\\n\\nYakin ingin melanjutkan?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Second confirmation
|
||||
if (!confirm('🔔 Konfirmasi sekali lagi!\\n\\nIni tidak bisa dibatalkan. Lanjutkan?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Submit form via POST
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '{{ route("admin.logout-all-users") }}';
|
||||
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
const csrfInput = document.createElement('input');
|
||||
csrfInput.type = 'hidden';
|
||||
csrfInput.name = '_token';
|
||||
csrfInput.value = csrfToken;
|
||||
|
||||
form.appendChild(csrfInput);
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -13,37 +13,65 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Error Summary Alert -->
|
||||
@if($errors->any())
|
||||
<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-red-600 text-xl">⚠️</span>
|
||||
<span class="text-red-700 font-bold">Perbaiki kesalahan berikut:</span>
|
||||
</div>
|
||||
<ul class="text-red-600 text-sm space-y-1 ml-8">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>• {{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6 max-w-2xl">
|
||||
<form action="{{ route('admin.guru-bk.store') }}" method="POST" class="space-y-4">
|
||||
<form action="{{ route('admin.guru-bk.store') }}" method="POST" class="space-y-4" id="guruBKForm">
|
||||
@csrf
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama Lengkap *</label>
|
||||
<input type="text" name="name" required value="{{ old('name') }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-maroon" placeholder="Nama guru BK">
|
||||
@error('name') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama Lengkap * <span class="text-gray-400 text-xs">(minimal 3 karakter)</span></label>
|
||||
<input type="text" name="name" required minlength="3" maxlength="255" value="{{ old('name') }}" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 transition @error('name') border-red-500 focus:ring-red-400 @else border-gray-300 focus:ring-maroon @enderror" placeholder="Nama guru BK" oninput="validateGuruBKForm()">
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<span id="nameError" class="text-red-500 text-xs hidden">⚠️ Nama minimal 3 karakter</span>
|
||||
<span id="nameValid" class="text-green-500 text-xs hidden">✓ Nama valid</span>
|
||||
</div>
|
||||
@error('name') <span class="text-red-500 text-xs block mt-1">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Email *</label>
|
||||
<input type="email" name="email" required value="{{ old('email') }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-maroon" placeholder="email@sekolah.id">
|
||||
@error('email') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Email * <span class="text-gray-400 text-xs">(format email valid)</span></label>
|
||||
<input type="email" name="email" required value="{{ old('email') }}" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 transition @error('email') border-red-500 focus:ring-red-400 @else border-gray-300 focus:ring-maroon @enderror" placeholder="email@sekolah.id" oninput="validateGuruBKForm()">
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<span id="emailError" class="text-red-500 text-xs hidden">⚠️ Format email tidak valid</span>
|
||||
<span id="emailValid" class="text-green-500 text-xs hidden">✓ Email valid</span>
|
||||
</div>
|
||||
@error('email') <span class="text-red-500 text-xs block mt-1">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Password *</label>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Password * <span class="text-gray-400 text-xs">(minimal 8 karakter)</span></label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="password" id="guruBKPassword" name="password" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" placeholder="Minimal 8 karakter" style="padding-right: 45px;">
|
||||
<input type="password" id="guruBKPassword" name="password" required minlength="8" maxlength="255" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 transition @error('password') border-red-500 focus:ring-red-400 @else border-gray-300 focus:ring-blue-400 @enderror" placeholder="Minimal 8 karakter" style="padding-right: 45px;" oninput="validateGuruBKForm()">
|
||||
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('guruBKPassword', this)">👁️</button>
|
||||
</div>
|
||||
@error('password') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||
<div id="passwordStrength" class="text-xs mt-1 font-medium hidden">
|
||||
<span id="passwordStrengthText"></span>
|
||||
</div>
|
||||
@error('password') <span class="text-red-500 text-xs block mt-1">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Konfirmasi Password *</label>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Konfirmasi Password * <span class="text-gray-400 text-xs">(harus sama)</span></label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="password" id="guruBKPasswordConfirm" name="password_confirmation" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" placeholder="Ulangi password" style="padding-right: 45px;">
|
||||
<input type="password" id="guruBKPasswordConfirm" name="password_confirmation" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" placeholder="Ulangi password" style="padding-right: 45px;" oninput="validateGuruBKForm()">
|
||||
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('guruBKPasswordConfirm', this)">👁️</button>
|
||||
</div>
|
||||
<div id="confirmError" class="text-red-500 text-xs hidden mt-1">⚠️ Password konfirmasi tidak cocok</div>
|
||||
<div id="confirmValid" class="text-green-500 text-xs hidden mt-1">✓ Password cocok</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 pt-4">
|
||||
<button type="submit" class="flex-1 gradient-maroon text-white font-bold py-3 px-4 rounded-lg hover:opacity-90 transition">
|
||||
<button type="submit" class="flex-1 gradient-maroon text-white font-bold py-3 px-4 rounded-lg hover:opacity-90 transition disabled:opacity-50 disabled:cursor-not-allowed" id="submitButton">
|
||||
💾 Simpan
|
||||
</button>
|
||||
<a href="{{ route('admin.guru-bk') }}" class="flex-1 bg-gray-400 text-white font-bold py-3 px-4 rounded-lg hover:bg-gray-500 transition text-center">
|
||||
|
|
@ -60,6 +88,93 @@ function togglePasswordVisibility(inputId, buttonElement) {
|
|||
const isPassword = input.type === 'password';
|
||||
input.type = isPassword ? 'text' : 'password';
|
||||
}
|
||||
|
||||
function getPasswordStrength(password) {
|
||||
let strength = 0;
|
||||
if (password.length >= 8) strength++;
|
||||
if (password.length >= 12) strength++;
|
||||
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++;
|
||||
if (/\d/.test(password)) strength++;
|
||||
if (/[^a-zA-Z\d]/.test(password)) strength++;
|
||||
|
||||
if (strength <= 1) return { level: 'Lemah', color: 'text-red-500' };
|
||||
if (strength <= 2) return { level: 'Sedang', color: 'text-yellow-500' };
|
||||
if (strength <= 3) return { level: 'Baik', color: 'text-blue-500' };
|
||||
return { level: 'Kuat', color: 'text-green-500' };
|
||||
}
|
||||
|
||||
function validateGuruBKForm() {
|
||||
const name = document.querySelector('input[name="name"]');
|
||||
const email = document.querySelector('input[name="email"]');
|
||||
const password = document.getElementById('guruBKPassword');
|
||||
const passwordConfirm = document.getElementById('guruBKPasswordConfirm');
|
||||
const submitButton = document.getElementById('submitButton');
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
// Validate name
|
||||
if (name.value.trim().length >= 3) {
|
||||
name.classList.remove('border-red-500', 'focus:ring-red-400');
|
||||
name.classList.add('border-gray-300', 'focus:ring-maroon');
|
||||
document.getElementById('nameError').classList.add('hidden');
|
||||
document.getElementById('nameValid').classList.remove('hidden');
|
||||
} else if (name.value.trim().length > 0) {
|
||||
name.classList.remove('border-gray-300', 'focus:ring-maroon');
|
||||
name.classList.add('border-red-500', 'focus:ring-red-400');
|
||||
document.getElementById('nameError').classList.remove('hidden');
|
||||
document.getElementById('nameValid').classList.add('hidden');
|
||||
} else {
|
||||
document.getElementById('nameError').classList.add('hidden');
|
||||
document.getElementById('nameValid').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Validate email
|
||||
if (!emailRegex.test(email.value)) {
|
||||
email.classList.remove('border-gray-300', 'focus:ring-maroon');
|
||||
email.classList.add('border-red-500', 'focus:ring-red-400');
|
||||
document.getElementById('emailError').classList.remove('hidden');
|
||||
document.getElementById('emailValid').classList.add('hidden');
|
||||
} else {
|
||||
email.classList.remove('border-red-500', 'focus:ring-red-400');
|
||||
email.classList.add('border-gray-300', 'focus:ring-maroon');
|
||||
document.getElementById('emailError').classList.add('hidden');
|
||||
document.getElementById('emailValid').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Show password strength
|
||||
if (password.value.length > 0) {
|
||||
const strength = getPasswordStrength(password.value);
|
||||
document.getElementById('passwordStrengthText').textContent = `Kekuatan: ${strength.level}`;
|
||||
document.getElementById('passwordStrengthText').className = strength.color;
|
||||
document.getElementById('passwordStrength').classList.remove('hidden');
|
||||
} else {
|
||||
document.getElementById('passwordStrength').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Validate password confirmation
|
||||
if (passwordConfirm.value.length > 0) {
|
||||
if (password.value !== passwordConfirm.value) {
|
||||
document.getElementById('confirmError').classList.remove('hidden');
|
||||
document.getElementById('confirmValid').classList.add('hidden');
|
||||
} else if (password.value.length >= 8) {
|
||||
document.getElementById('confirmError').classList.add('hidden');
|
||||
document.getElementById('confirmValid').classList.remove('hidden');
|
||||
}
|
||||
} else {
|
||||
document.getElementById('confirmError').classList.add('hidden');
|
||||
document.getElementById('confirmValid').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Enable/disable submit
|
||||
const isValid = name.value.trim().length >= 3 &&
|
||||
emailRegex.test(email.value) &&
|
||||
password.value.length >= 8 &&
|
||||
password.value === passwordConfirm.value;
|
||||
submitButton.disabled = !isValid;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
validateGuruBKForm();
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -13,38 +13,64 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Error Summary Alert -->
|
||||
@if($errors->any())
|
||||
<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-red-600 text-xl">⚠️</span>
|
||||
<span class="text-red-700 font-bold">Perbaiki kesalahan berikut:</span>
|
||||
</div>
|
||||
<ul class="text-red-600 text-sm space-y-1 ml-8">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>• {{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6 max-w-2xl">
|
||||
<form action="{{ route('admin.guru-bk.update', $guruBK->id) }}" method="POST" class="space-y-4">
|
||||
<form action="{{ route('admin.guru-bk.update', $guruBK->id) }}" method="POST" class="space-y-4" id="guruBKForm">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama Lengkap *</label>
|
||||
<input type="text" name="name" required value="{{ old('name', $guruBK->name) }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-maroon">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama Lengkap * <span class="text-gray-400 text-xs">(minimal 3 karakter)</span></label>
|
||||
<input type="text" name="name" required minlength="3" maxlength="255" value="{{ old('name', $guruBK->name) }}" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 transition @error('name') border-red-500 focus:ring-red-400 @else border-gray-300 focus:ring-maroon @enderror" oninput="validateGuruBKForm()">
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<span id="nameError" class="text-red-500 text-xs hidden">⚠️ Nama minimal 3 karakter</span>
|
||||
<span id="nameValid" class="text-green-500 text-xs hidden">✓ Nama valid</span>
|
||||
</div>
|
||||
@error('name') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Email *</label>
|
||||
<input type="email" name="email" required value="{{ old('email', $guruBK->email) }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-maroon">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Email * <span class="text-gray-400 text-xs">(format email valid)</span></label>
|
||||
<input type="email" name="email" required value="{{ old('email', $guruBK->email) }}" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 transition @error('email') border-red-500 focus:ring-red-400 @else border-gray-300 focus:ring-maroon @enderror" oninput="validateGuruBKForm()">
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<span id="emailError" class="text-red-500 text-xs hidden">⚠️ Format email tidak valid</span>
|
||||
<span id="emailValid" class="text-green-500 text-xs hidden">✓ Email valid</span>
|
||||
</div>
|
||||
@error('email') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Password Baru <span class="text-gray-400 font-normal">(kosongkan jika tidak diubah)</span></label>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Password Baru <span class="text-gray-400 font-normal">(kosongkan jika tidak diubah, minimal 8 karakter)</span></label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="password" id="editGuruPassword" name="password" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" placeholder="Minimal 8 karakter" style="padding-right: 45px;">
|
||||
<input type="password" id="editGuruPassword" name="password" minlength="8" maxlength="255" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 transition @error('password') border-red-500 focus:ring-red-400 @else border-gray-300 focus:ring-blue-400 @enderror" placeholder="Minimal 8 karakter" style="padding-right: 45px;" oninput="validateGuruBKForm()">
|
||||
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('editGuruPassword', this)">👁️</button>
|
||||
</div>
|
||||
<div id="passwordStrength" class="text-xs mt-1 font-medium hidden"><span id="passwordStrengthText"></span></div>
|
||||
@error('password') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Konfirmasi Password Baru</label>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Konfirmasi Password Baru <span class="text-gray-400 text-xs">(harus sama)</span></label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="password" id="editGuruPasswordConfirm" name="password_confirmation" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" placeholder="Ulangi password baru" style="padding-right: 45px;">
|
||||
<input type="password" id="editGuruPasswordConfirm" name="password_confirmation" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" placeholder="Ulangi password baru" style="padding-right: 45px;" oninput="validateGuruBKForm()">
|
||||
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('editGuruPasswordConfirm', this)">👁️</button>
|
||||
</div>
|
||||
<div id="confirmError" class="text-red-500 text-xs hidden mt-1">⚠️ Password konfirmasi tidak cocok</div>
|
||||
<div id="confirmValid" class="text-green-500 text-xs hidden mt-1">✓ Password cocok</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 pt-4">
|
||||
<button type="submit" class="flex-1 gradient-maroon text-white font-bold py-3 px-4 rounded-lg hover:opacity-90 transition">
|
||||
<button type="submit" class="flex-1 gradient-maroon text-white font-bold py-3 px-4 rounded-lg hover:opacity-90 transition disabled:opacity-50 disabled:cursor-not-allowed" id="submitButton">
|
||||
💾 Update
|
||||
</button>
|
||||
<a href="{{ route('admin.guru-bk') }}" class="flex-1 bg-gray-400 text-white font-bold py-3 px-4 rounded-lg hover:bg-gray-500 transition text-center">
|
||||
|
|
@ -61,6 +87,84 @@ function togglePasswordVisibility(inputId, buttonElement) {
|
|||
const isPassword = input.type === 'password';
|
||||
input.type = isPassword ? 'text' : 'password';
|
||||
}
|
||||
|
||||
function getPasswordStrength(password) {
|
||||
let strength = 0;
|
||||
if (password.length >= 8) strength++;
|
||||
if (password.length >= 12) strength++;
|
||||
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++;
|
||||
if (/\d/.test(password)) strength++;
|
||||
if (/[^a-zA-Z\d]/.test(password)) strength++;
|
||||
if (strength <= 1) return { level: 'Lemah', color: 'text-red-500' };
|
||||
if (strength <= 2) return { level: 'Sedang', color: 'text-yellow-500' };
|
||||
if (strength <= 3) return { level: 'Baik', color: 'text-blue-500' };
|
||||
return { level: 'Kuat', color: 'text-green-500' };
|
||||
}
|
||||
|
||||
function validateGuruBKForm() {
|
||||
const name = document.querySelector('input[name="name"]');
|
||||
const email = document.querySelector('input[name="email"]');
|
||||
const password = document.getElementById('editGuruPassword');
|
||||
const passwordConfirm = document.getElementById('editGuruPasswordConfirm');
|
||||
const submitButton = document.getElementById('submitButton');
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
if (name.value.trim().length >= 3) {
|
||||
name.classList.remove('border-red-500', 'focus:ring-red-400');
|
||||
name.classList.add('border-gray-300', 'focus:ring-maroon');
|
||||
document.getElementById('nameError').classList.add('hidden');
|
||||
document.getElementById('nameValid').classList.remove('hidden');
|
||||
} else if (name.value.trim().length > 0) {
|
||||
name.classList.remove('border-gray-300', 'focus:ring-maroon');
|
||||
name.classList.add('border-red-500', 'focus:ring-red-400');
|
||||
document.getElementById('nameError').classList.remove('hidden');
|
||||
document.getElementById('nameValid').classList.add('hidden');
|
||||
} else {
|
||||
document.getElementById('nameError').classList.add('hidden');
|
||||
document.getElementById('nameValid').classList.add('hidden');
|
||||
}
|
||||
|
||||
if (!emailRegex.test(email.value)) {
|
||||
email.classList.remove('border-gray-300', 'focus:ring-maroon');
|
||||
email.classList.add('border-red-500', 'focus:ring-red-400');
|
||||
document.getElementById('emailError').classList.remove('hidden');
|
||||
document.getElementById('emailValid').classList.add('hidden');
|
||||
} else {
|
||||
email.classList.remove('border-red-500', 'focus:ring-red-400');
|
||||
email.classList.add('border-gray-300', 'focus:ring-maroon');
|
||||
document.getElementById('emailError').classList.add('hidden');
|
||||
document.getElementById('emailValid').classList.remove('hidden');
|
||||
}
|
||||
|
||||
if (password.value.length > 0) {
|
||||
const strength = getPasswordStrength(password.value);
|
||||
document.getElementById('passwordStrengthText').textContent = `Kekuatan: ${strength.level}`;
|
||||
document.getElementById('passwordStrengthText').className = strength.color;
|
||||
document.getElementById('passwordStrength').classList.remove('hidden');
|
||||
} else {
|
||||
document.getElementById('passwordStrength').classList.add('hidden');
|
||||
}
|
||||
|
||||
if (password.value.length > 0 || passwordConfirm.value.length > 0) {
|
||||
if (password.value.length >= 8 && password.value === passwordConfirm.value) {
|
||||
document.getElementById('confirmError').classList.add('hidden');
|
||||
document.getElementById('confirmValid').classList.remove('hidden');
|
||||
} else {
|
||||
document.getElementById('confirmError').classList.remove('hidden');
|
||||
document.getElementById('confirmValid').classList.add('hidden');
|
||||
}
|
||||
} else {
|
||||
document.getElementById('confirmError').classList.add('hidden');
|
||||
document.getElementById('confirmValid').classList.add('hidden');
|
||||
}
|
||||
|
||||
const passwordValid = password.value === '' || (password.value.length >= 8 && password.value === passwordConfirm.value);
|
||||
submitButton.disabled = !(name.value.trim().length >= 3 && emailRegex.test(email.value) && passwordValid);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
validateGuruBKForm();
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
<a href="{{ route('admin.guru-bk.edit', $guru->id) }}" class="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs font-semibold hover:bg-blue-200 transition inline-block">
|
||||
Edit
|
||||
</a>
|
||||
<form method="POST" action="{{ route('admin.guru-bk.destroy', $guru->id) }}" style="display:inline;" onsubmit="return confirm('Hapus akun guru BK ini?')">
|
||||
<form method="POST" action="{{ route('admin.guru-bk.destroy', $guru->id) }}" style="display:inline;" class="swal-confirm" data-confirm-message="Hapus akun guru BK ini?">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="px-2 py-1 bg-red-100 text-red-700 rounded text-xs font-semibold hover:bg-red-200 transition">
|
||||
|
|
|
|||
|
|
@ -13,104 +13,108 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Error Summary Alert -->
|
||||
@if($errors->any())
|
||||
<div class="bg-red-50 border-l-4 border-red-400 p-4 rounded-lg mb-6">
|
||||
<ul class="text-red-800 text-sm list-disc list-inside">
|
||||
<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-red-600 text-xl">⚠️</span>
|
||||
<span class="text-red-700 font-bold">Perbaiki kesalahan berikut:</span>
|
||||
</div>
|
||||
<ul class="text-red-600 text-sm space-y-1 ml-8">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
<li>• {{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('admin.jurusan.store') }}" method="POST" class="space-y-6">
|
||||
<form action="{{ route('admin.jurusan.store') }}" method="POST" class="space-y-6" id="jurusanForm">
|
||||
@csrf
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📋 Informasi Jurusan</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama Jurusan <span class="text-red-500">*</span></label>
|
||||
<input type="text" name="nama_jurusan" value="{{ old('nama_jurusan') }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon" placeholder="Contoh: Teknologi Informasi" required>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama Jurusan <span class="text-red-500">*</span> <span class="text-gray-400 text-xs">(minimal 3 karakter)</span></label>
|
||||
<input type="text" name="nama_jurusan" value="{{ old('nama_jurusan') }}" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 transition @error('nama_jurusan') border-red-500 focus:ring-red-400 @else border-gray-300 focus:ring-maroon @enderror" placeholder="Contoh: Teknologi Informasi" required minlength="3" maxlength="100" oninput="validateJurusanForm()">
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<span id="namaError" class="text-red-500 text-xs hidden">⚠️ Nama minimal 3 karakter</span>
|
||||
<span id="namaValid" class="text-green-500 text-xs hidden">✓ Nama valid</span>
|
||||
</div>
|
||||
@error('nama_jurusan') <span class="text-red-500 text-xs block mt-1">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Deskripsi</label>
|
||||
<textarea name="deskripsi" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon" placeholder="Jelaskan singkat tentang jurusan ini">{{ old('deskripsi') }}</textarea>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Deskripsi <span class="text-gray-400 text-xs">(disarankan detail untuk kebutuhan chatbot)</span></label>
|
||||
<textarea name="deskripsi" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-maroon" placeholder="Jelaskan detail tentang jurusan ini" oninput="updateCharCount()">{{ old('deskripsi') }}</textarea>
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<span class="text-gray-400 text-xs">Karakter:</span>
|
||||
<span id="charCount" class="text-gray-600 text-xs font-medium">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Keywords (Kata Kunci Minat & Cita-cita)</label>
|
||||
<textarea name="keywords" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon" placeholder="Pisahkan dengan koma, contoh: programmer, developer, coding, software, web">{{ old('keywords') }}</textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">Keywords digunakan untuk mencocokkan minat dan cita-cita siswa dengan jurusan ini. Pisahkan dengan koma.</p>
|
||||
<textarea name="keywords" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-maroon" placeholder="Pisahkan dengan koma, contoh: programmer, developer, coding, software, web" maxlength="500" oninput="updateCharCount('keywordsCount')">{{ old('keywords') }}</textarea>
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<p class="text-xs text-gray-500">Keywords digunakan untuk mencocokkan minat dan cita-cita siswa dengan jurusan ini. Pisahkan dengan koma.</p>
|
||||
<span id="keywordsCount" class="text-gray-600 text-xs font-medium">0/500</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Preferensi Studi</label>
|
||||
<textarea name="preferensi_studi" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon" placeholder="Pisahkan dengan koma, contoh: Sains & Teknologi, Pertanian & Lingkungan">{{ old('preferensi_studi') }}</textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">Rumpun bidang studi yang cocok untuk jurusan ini. Pilihan: Sains & Teknologi, Pertanian & Lingkungan, Kesehatan & Ilmu Hayat, Bisnis & Manajemen, Sosial & Humaniora</p>
|
||||
<textarea name="preferensi_studi" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-maroon" placeholder="Pisahkan dengan koma, contoh: Praktik Langsung, DuDi, Project Based, Blended Learning" maxlength="500" oninput="updateCharCount('preferensiCount')">{{ old('preferensi_studi') }}</textarea>
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<p class="text-xs text-gray-500">Pilihan preferensi studi: Praktik Langsung, DuDi, Project Based, Blended Learning</p>
|
||||
<span id="preferensiCount" class="text-gray-600 text-xs font-medium">0/500</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Prospek Kerja</label>
|
||||
<textarea name="prospek_kerja" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon" placeholder="Contoh: Software Developer, Web Developer, Data Analyst">{{ old('prospek_kerja') }}</textarea>
|
||||
<textarea name="prospek_kerja" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-maroon" placeholder="Contoh: Software Developer, Web Developer, Data Analyst" maxlength="500" oninput="updateCharCount('prospekCount')">{{ old('prospek_kerja') }}</textarea>
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<span class="text-xs text-gray-400">Prospek kerja yang terbuka</span>
|
||||
<span id="prospekCount" class="text-gray-600 text-xs font-medium">0/500</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bobot Mata Pelajaran -->
|
||||
<!-- ROC Weights Info (Fixed) -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">⚖️ Bobot Mata Pelajaran</h3>
|
||||
<p class="text-xs text-gray-500 mb-4">Tentukan bobot setiap mata pelajaran untuk jurusan ini (0.00 - 1.00). Mata pelajaran yang lebih relevan diberi bobot lebih tinggi. Jumlah total tidak harus 1.0.</p>
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">⚖️ Bobot Kriteria Penilaian (ROC-Validated)</h3>
|
||||
<p class="text-xs text-gray-600 mb-4">Semua jurusan menggunakan bobot yang sama dan telah divalidasi menggunakan ROC curve analysis:</p>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h4 class="text-sm font-bold text-gray-700 mb-3 border-b pb-2">📐 IPA</h4>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Matematika</label>
|
||||
<input type="number" name="bobot_mtk" value="{{ old('bobot_mtk', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Fisika</label>
|
||||
<input type="number" name="bobot_fisika" value="{{ old('bobot_fisika', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Kimia</label>
|
||||
<input type="number" name="bobot_kimia" value="{{ old('bobot_kimia', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Biologi</label>
|
||||
<input type="number" name="bobot_biologi" value="{{ old('bobot_biologi', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm">
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||||
<div class="p-3 bg-green-50 rounded-lg text-center border-l-4 border-green-400">
|
||||
<p class="text-xs font-bold text-green-800">💡 Minat</p>
|
||||
<p class="text-lg font-bold text-green-700">45.6%</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-bold text-gray-700 mb-3 border-b pb-2">📊 IPS</h4>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Ekonomi</label>
|
||||
<input type="number" name="bobot_ekonomi" value="{{ old('bobot_ekonomi', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Geografi</label>
|
||||
<input type="number" name="bobot_geografi" value="{{ old('bobot_geografi', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Sosiologi</label>
|
||||
<input type="number" name="bobot_sosiologi" value="{{ old('bobot_sosiologi', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Sejarah</label>
|
||||
<input type="number" name="bobot_sejarah" value="{{ old('bobot_sejarah', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm">
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 bg-yellow-50 rounded-lg text-center border-l-4 border-yellow-400">
|
||||
<p class="text-xs font-bold text-yellow-800">🎯 Preferensi</p>
|
||||
<p class="text-lg font-bold text-yellow-700">25.6%</p>
|
||||
</div>
|
||||
<div class="p-3 bg-blue-50 rounded-lg text-center border-l-4 border-blue-400">
|
||||
<p class="text-xs font-bold text-blue-800">📝 Nilai</p>
|
||||
<p class="text-lg font-bold text-blue-700">15.6%</p>
|
||||
</div>
|
||||
<div class="p-3 bg-red-50 rounded-lg text-center border-l-4 border-red-400">
|
||||
<p class="text-xs font-bold text-red-800">💼 Cita-cita</p>
|
||||
<p class="text-lg font-bold text-red-700">9.0%</p>
|
||||
</div>
|
||||
<div class="p-3 bg-purple-50 rounded-lg text-center border-l-4 border-purple-400">
|
||||
<p class="text-xs font-bold text-purple-800">🏆 Prestasi</p>
|
||||
<p class="text-lg font-bold text-purple-700">4.0%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-600 mt-4">ℹ️ Total: 100% | Bobot ini tidak dapat diubah per jurusan untuk konsistensi sistem</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<button type="submit" class="flex-1 gradient-maroon text-white font-bold py-3 px-4 rounded-lg hover:opacity-90 transition">
|
||||
<button type="submit" class="flex-1 gradient-maroon text-white font-bold py-3 px-4 rounded-lg hover:opacity-90 transition disabled:opacity-50 disabled:cursor-not-allowed" id="submitButton">
|
||||
💾 Simpan Jurusan
|
||||
</button>
|
||||
<a href="{{ route('admin.jurusan') }}" class="flex-1 bg-gray-400 text-white font-bold py-3 px-4 rounded-lg hover:bg-gray-500 transition text-center">
|
||||
|
|
@ -118,4 +122,60 @@
|
|||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
function updateCharCount(elementId = 'charCount') {
|
||||
const textarea = event.target;
|
||||
const count = textarea.value.length;
|
||||
const maxLength = parseInt(textarea.maxLength);
|
||||
document.getElementById(elementId).textContent = maxLength > 0 ? `${count}/${maxLength}` : `${count}`;
|
||||
validateJurusanForm();
|
||||
}
|
||||
|
||||
function validateJurusanForm() {
|
||||
const nama = document.querySelector('input[name="nama_jurusan"]');
|
||||
const submitBtn = document.getElementById('submitButton');
|
||||
|
||||
if (nama.value.trim().length >= 3) {
|
||||
nama.classList.remove('border-red-500', 'focus:ring-red-400');
|
||||
nama.classList.add('border-gray-300', 'focus:ring-maroon');
|
||||
document.getElementById('namaError').classList.add('hidden');
|
||||
document.getElementById('namaValid').classList.remove('hidden');
|
||||
} else if (nama.value.trim().length > 0) {
|
||||
nama.classList.remove('border-gray-300', 'focus:ring-maroon');
|
||||
nama.classList.add('border-red-500', 'focus:ring-red-400');
|
||||
document.getElementById('namaError').classList.remove('hidden');
|
||||
document.getElementById('namaValid').classList.add('hidden');
|
||||
} else {
|
||||
document.getElementById('namaError').classList.add('hidden');
|
||||
document.getElementById('namaValid').classList.add('hidden');
|
||||
}
|
||||
|
||||
const isValid = nama.value.trim().length >= 3;
|
||||
submitBtn.disabled = !isValid;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Update char counts on load
|
||||
document.querySelectorAll('textarea[maxlength]').forEach(textarea => {
|
||||
const id = textarea.name === 'deskripsi' ? 'charCount' :
|
||||
textarea.name === 'keywords' ? 'keywordsCount' :
|
||||
textarea.name === 'preferensi_studi' ? 'preferensiCount' :
|
||||
'prospekCount';
|
||||
const count = textarea.value.length;
|
||||
const maxLength = parseInt(textarea.maxLength);
|
||||
if (document.getElementById(id)) {
|
||||
document.getElementById(id).textContent = maxLength > 0 ? `${count}/${maxLength}` : `${count}`;
|
||||
}
|
||||
});
|
||||
|
||||
const desc = document.querySelector('textarea[name="deskripsi"]');
|
||||
if (desc && document.getElementById('charCount')) {
|
||||
document.getElementById('charCount').textContent = `${desc.value.length}`;
|
||||
}
|
||||
validateJurusanForm();
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -13,17 +13,22 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Error Summary Alert -->
|
||||
@if($errors->any())
|
||||
<div class="bg-red-50 border-l-4 border-red-400 p-4 rounded-lg mb-6">
|
||||
<ul class="text-red-800 text-sm list-disc list-inside">
|
||||
<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-red-600 text-xl">⚠️</span>
|
||||
<span class="text-red-700 font-bold">Perbaiki kesalahan berikut:</span>
|
||||
</div>
|
||||
<ul class="text-red-600 text-sm space-y-1 ml-8">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
<li>• {{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('admin.jurusan.update', $jurusan->id) }}" method="POST" class="space-y-6">
|
||||
<form action="{{ route('admin.jurusan.update', $jurusan->id) }}" method="POST" class="space-y-6" id="jurusanForm">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
|
|
@ -31,23 +36,35 @@
|
|||
<h3 class="text-lg font-bold text-maroon mb-4">📋 Informasi Jurusan</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama Jurusan <span class="text-red-500">*</span></label>
|
||||
<input type="text" name="nama_jurusan" value="{{ old('nama_jurusan', $jurusan->nama_jurusan) }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon" required>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama Jurusan <span class="text-red-500">*</span> <span class="text-gray-400 text-xs">(minimal 3 karakter)</span></label>
|
||||
<input type="text" name="nama_jurusan" value="{{ old('nama_jurusan', $jurusan->nama_jurusan) }}" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 transition @error('nama_jurusan') border-red-500 focus:ring-red-400 @else border-gray-300 focus:ring-maroon @enderror" required minlength="3" maxlength="100" oninput="validateJurusanForm()">
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<span id="namaError" class="text-red-500 text-xs hidden">⚠️ Nama minimal 3 karakter</span>
|
||||
<span id="namaValid" class="text-green-500 text-xs hidden">✓ Nama valid</span>
|
||||
</div>
|
||||
@error('nama_jurusan') <span class="text-red-500 text-xs block mt-1">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Deskripsi</label>
|
||||
<textarea name="deskripsi" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon" placeholder="Jelaskan singkat tentang jurusan ini">{{ old('deskripsi', $jurusan->deskripsi) }}</textarea>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Deskripsi <span class="text-gray-400 text-xs">(disarankan detail untuk kebutuhan chatbot)</span></label>
|
||||
<textarea name="deskripsi" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-maroon" placeholder="Jelaskan detail tentang jurusan ini" oninput="updateCharCount()">{{ old('deskripsi', $jurusan->deskripsi) }}</textarea>
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<span class="text-gray-400 text-xs">Karakter:</span>
|
||||
<span id="charCount" class="text-gray-600 text-xs font-medium">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Keywords (Kata Kunci Minat & Cita-cita)</label>
|
||||
<textarea name="keywords" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon" placeholder="Pisahkan dengan koma">{{ old('keywords', implode(', ', $jurusan->keywords ?? [])) }}</textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">Keywords digunakan untuk mencocokkan minat dan cita-cita siswa dengan jurusan ini. Pisahkan dengan koma.</p>
|
||||
<textarea name="keywords" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-maroon" placeholder="Pisahkan dengan koma, contoh: programmer, developer, coding, software, web" maxlength="500" oninput="updateCharCount('keywordsCount')">{{ old('keywords', implode(', ', $jurusan->keywords ?? [])) }}</textarea>
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<p class="text-xs text-gray-500">Keywords digunakan untuk mencocokkan minat dan cita-cita siswa dengan jurusan ini. Pisahkan dengan koma.</p>
|
||||
<span id="keywordsCount" class="text-gray-600 text-xs font-medium">0/500</span>
|
||||
</div>
|
||||
@if(!empty($jurusan->keywords))
|
||||
<div class="flex flex-wrap gap-1 mt-2">
|
||||
@foreach($jurusan->keywords as $kw)
|
||||
<span class="inline-block px-2 py-0.5 rounded bg-blue-100 text-blue-700 text-xs">{{ $kw }}</span>
|
||||
<span class="inline-block px-2 py-0.5 rounded bg-blue-100 text-blue-700 text-xs">{{ $kw }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
|
@ -55,73 +72,62 @@
|
|||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Preferensi Studi</label>
|
||||
<textarea name="preferensi_studi" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon" placeholder="Pisahkan dengan koma">{{ old('preferensi_studi', implode(', ', $jurusan->preferensi_studi ?? [])) }}</textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">Rumpun bidang studi yang cocok: Sains & Teknologi, Pertanian & Lingkungan, Kesehatan & Ilmu Hayat, Bisnis & Manajemen, Sosial & Humaniora</p>
|
||||
<textarea name="preferensi_studi" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-maroon" placeholder="Pisahkan dengan koma, contoh: Praktik Langsung, DuDi, Project Based, Blended Learning" maxlength="500" oninput="updateCharCount('preferensiCount')">{{ old('preferensi_studi', implode(', ', $jurusan->preferensi_studi ?? [])) }}</textarea>
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<p class="text-xs text-gray-500">Pilihan preferensi studi: Praktik Langsung, DuDi, Project Based, Blended Learning</p>
|
||||
<span id="preferensiCount" class="text-gray-600 text-xs font-medium">0/500</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Prospek Kerja</label>
|
||||
<textarea name="prospek_kerja" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon" placeholder="Contoh: Software Developer, Web Developer, Data Analyst">{{ old('prospek_kerja', $jurusan->prospek_kerja) }}</textarea>
|
||||
<textarea name="prospek_kerja" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-maroon" placeholder="Contoh: Software Developer, Web Developer, Data Analyst" maxlength="500" oninput="updateCharCount('prospekCount')">{{ old('prospek_kerja', $jurusan->prospek_kerja) }}</textarea>
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<span class="text-xs text-gray-400">Prospek kerja yang terbuka</span>
|
||||
<span id="prospekCount" class="text-gray-600 text-xs font-medium">0/500</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bobot Mata Pelajaran -->
|
||||
<!-- ROC Weights Info (Fixed) -->
|
||||
@php
|
||||
$bobot = $jurusan->bobot_mapel ?? [];
|
||||
$bobotIpa = data_get($bobot, 'ipa', $bobot);
|
||||
$bobotIps = data_get($bobot, 'ips', $bobot);
|
||||
@endphp
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">⚖️ Bobot Mata Pelajaran</h3>
|
||||
<p class="text-xs text-gray-500 mb-4">Tentukan bobot setiap mata pelajaran untuk jurusan ini (0.00 - 1.00). Mata pelajaran yang lebih relevan diberi bobot lebih tinggi. Jumlah total tidak harus 1.0.</p>
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">⚖️ Bobot Kriteria Penilaian (ROC-Validated)</h3>
|
||||
<p class="text-xs text-gray-600 mb-4">Semua jurusan menggunakan bobot yang sama dan telah divalidasi menggunakan ROC curve analysis:</p>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h4 class="text-sm font-bold text-gray-700 mb-3 border-b pb-2">📐 IPA</h4>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Matematika</label>
|
||||
<input type="number" name="bobot_mtk" value="{{ old('bobot_mtk', $bobot['mtk'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Fisika</label>
|
||||
<input type="number" name="bobot_fisika" value="{{ old('bobot_fisika', $bobot['fisika'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Kimia</label>
|
||||
<input type="number" name="bobot_kimia" value="{{ old('bobot_kimia', $bobot['kimia'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Biologi</label>
|
||||
<input type="number" name="bobot_biologi" value="{{ old('bobot_biologi', $bobot['biologi'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm">
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||||
<div class="p-3 bg-green-50 rounded-lg text-center border-l-4 border-green-400">
|
||||
<p class="text-xs font-bold text-green-800">💡 Minat</p>
|
||||
<p class="text-lg font-bold text-green-700">45.6%</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-bold text-gray-700 mb-3 border-b pb-2">📊 IPS</h4>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Ekonomi</label>
|
||||
<input type="number" name="bobot_ekonomi" value="{{ old('bobot_ekonomi', $bobot['ekonomi'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Geografi</label>
|
||||
<input type="number" name="bobot_geografi" value="{{ old('bobot_geografi', $bobot['geografi'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Sosiologi</label>
|
||||
<input type="number" name="bobot_sosiologi" value="{{ old('bobot_sosiologi', $bobot['sosiologi'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Sejarah</label>
|
||||
<input type="number" name="bobot_sejarah" value="{{ old('bobot_sejarah', $bobot['sejarah'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm">
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 bg-yellow-50 rounded-lg text-center border-l-4 border-yellow-400">
|
||||
<p class="text-xs font-bold text-yellow-800">🎯 Preferensi</p>
|
||||
<p class="text-lg font-bold text-yellow-700">25.6%</p>
|
||||
</div>
|
||||
<div class="p-3 bg-blue-50 rounded-lg text-center border-l-4 border-blue-400">
|
||||
<p class="text-xs font-bold text-blue-800">📝 Nilai</p>
|
||||
<p class="text-lg font-bold text-blue-700">15.6%</p>
|
||||
</div>
|
||||
<div class="p-3 bg-red-50 rounded-lg text-center border-l-4 border-red-400">
|
||||
<p class="text-xs font-bold text-red-800">💼 Cita-cita</p>
|
||||
<p class="text-lg font-bold text-red-700">9.0%</p>
|
||||
</div>
|
||||
<div class="p-3 bg-purple-50 rounded-lg text-center border-l-4 border-purple-400">
|
||||
<p class="text-xs font-bold text-purple-800">🏆 Prestasi</p>
|
||||
<p class="text-lg font-bold text-purple-700">4.0%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-600 mt-4">ℹ️ Total: 100% | Bobot ini tidak dapat diubah per jurusan untuk konsistensi sistem</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<button type="submit" class="flex-1 gradient-maroon text-white font-bold py-3 px-4 rounded-lg hover:opacity-90 transition">
|
||||
<button type="submit" class="flex-1 gradient-maroon text-white font-bold py-3 px-4 rounded-lg hover:opacity-90 transition disabled:opacity-50 disabled:cursor-not-allowed" id="submitButton">
|
||||
💾 Simpan Perubahan
|
||||
</button>
|
||||
<a href="{{ route('admin.jurusan') }}" class="flex-1 bg-gray-400 text-white font-bold py-3 px-4 rounded-lg hover:bg-gray-500 transition text-center">
|
||||
|
|
@ -129,4 +135,60 @@
|
|||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
function updateCharCount(elementId = 'charCount') {
|
||||
const textarea = event.target;
|
||||
const count = textarea.value.length;
|
||||
const maxLength = parseInt(textarea.maxLength);
|
||||
document.getElementById(elementId).textContent = maxLength > 0 ? `${count}/${maxLength}` : `${count}`;
|
||||
validateJurusanForm();
|
||||
}
|
||||
|
||||
function validateJurusanForm() {
|
||||
const nama = document.querySelector('input[name="nama_jurusan"]');
|
||||
const submitBtn = document.getElementById('submitButton');
|
||||
|
||||
if (nama.value.trim().length >= 3) {
|
||||
nama.classList.remove('border-red-500', 'focus:ring-red-400');
|
||||
nama.classList.add('border-gray-300', 'focus:ring-maroon');
|
||||
document.getElementById('namaError').classList.add('hidden');
|
||||
document.getElementById('namaValid').classList.remove('hidden');
|
||||
} else if (nama.value.trim().length > 0) {
|
||||
nama.classList.remove('border-gray-300', 'focus:ring-maroon');
|
||||
nama.classList.add('border-red-500', 'focus:ring-red-400');
|
||||
document.getElementById('namaError').classList.remove('hidden');
|
||||
document.getElementById('namaValid').classList.add('hidden');
|
||||
} else {
|
||||
document.getElementById('namaError').classList.add('hidden');
|
||||
document.getElementById('namaValid').classList.add('hidden');
|
||||
}
|
||||
|
||||
const isValid = nama.value.trim().length >= 3;
|
||||
submitBtn.disabled = !isValid;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Update char counts on load
|
||||
document.querySelectorAll('textarea[maxlength]').forEach(textarea => {
|
||||
const id = textarea.name === 'deskripsi' ? 'charCount' :
|
||||
textarea.name === 'keywords' ? 'keywordsCount' :
|
||||
textarea.name === 'preferensi_studi' ? 'preferensiCount' :
|
||||
'prospekCount';
|
||||
const count = textarea.value.length;
|
||||
const maxLength = parseInt(textarea.maxLength);
|
||||
if (document.getElementById(id)) {
|
||||
document.getElementById(id).textContent = maxLength > 0 ? `${count}/${maxLength}` : `${count}`;
|
||||
}
|
||||
});
|
||||
|
||||
const desc = document.querySelector('textarea[name="deskripsi"]');
|
||||
if (desc && document.getElementById('charCount')) {
|
||||
document.getElementById('charCount').textContent = `${desc.value.length}`;
|
||||
}
|
||||
validateJurusanForm();
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@
|
|||
<a href="{{ route('admin.jurusan.edit', $jurusan->id) }}" class="px-3 py-1 bg-blue-100 text-blue-700 rounded text-xs font-semibold hover:bg-blue-200 transition">
|
||||
✏️ Edit
|
||||
</a>
|
||||
<form action="{{ route('admin.jurusan.destroy', $jurusan->id) }}" method="POST" onsubmit="return confirm('Yakin ingin menghapus jurusan {{ $jurusan->nama_jurusan }}?')">
|
||||
<form action="{{ route('admin.jurusan.destroy', $jurusan->id) }}" method="POST" class="swal-confirm" data-confirm-message="Yakin ingin menghapus jurusan {{ $jurusan->nama_jurusan }}?">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="px-3 py-1 bg-red-100 text-red-700 rounded text-xs font-semibold hover:bg-red-200 transition">
|
||||
|
|
@ -97,23 +97,23 @@
|
|||
<p class="text-gray-700 text-sm mb-4">Sistem menggunakan 5 kriteria utama untuk memberikan rekomendasi jurusan yang tepat:</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div class="p-4 bg-blue-50 rounded-lg border-l-4 border-blue-400">
|
||||
<p class="font-bold text-blue-800 text-sm">📝 Nilai Akademik (40%)</p>
|
||||
<p class="font-bold text-blue-800 text-sm">📝 Nilai Akademik (15.6%)</p>
|
||||
<p class="text-xs text-blue-700 mt-1">IPA: MTK, Fisika, Kimia, Biologi<br>IPS: Ekonomi, Geografi, Sosiologi, Sejarah</p>
|
||||
</div>
|
||||
<div class="p-4 bg-green-50 rounded-lg border-l-4 border-green-400">
|
||||
<p class="font-bold text-green-800 text-sm">💡 Minat & Bakat (35%)</p>
|
||||
<p class="font-bold text-green-800 text-sm">💡 Minat & Bakat (45.6%)</p>
|
||||
<p class="text-xs text-green-700 mt-1">Dicocokkan dengan keywords jurusan secara graduated</p>
|
||||
</div>
|
||||
<div class="p-4 bg-yellow-50 rounded-lg border-l-4 border-yellow-400">
|
||||
<p class="font-bold text-yellow-800 text-sm">🎯 Preferensi Studi (15%)</p>
|
||||
<p class="font-bold text-yellow-800 text-sm">🎯 Preferensi Studi (25.6%)</p>
|
||||
<p class="text-xs text-yellow-700 mt-1">Praktik Langsung, DuDi, Project Based, Blended Learning</p>
|
||||
</div>
|
||||
<div class="p-4 bg-purple-50 rounded-lg border-l-4 border-purple-400">
|
||||
<p class="font-bold text-purple-800 text-sm">🏆 Prestasi (5%)</p>
|
||||
<p class="font-bold text-purple-800 text-sm">🏆 Prestasi (4%)</p>
|
||||
<p class="text-xs text-purple-700 mt-1">Prestasi akademik dan non-akademik siswa</p>
|
||||
</div>
|
||||
<div class="p-4 bg-red-50 rounded-lg border-l-4 border-red-400">
|
||||
<p class="font-bold text-red-800 text-sm">💼 Cita-cita (5%)</p>
|
||||
<p class="font-bold text-red-800 text-sm">💼 Cita-cita (9%)</p>
|
||||
<p class="text-xs text-red-700 mt-1">Dicocokkan dengan keywords jurusan secara graduated</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,17 +4,17 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>@yield('title', 'Admin Panel') - SPK Jurusan Kuliah</title>
|
||||
<title>@yield('title', 'Admin Panel') - SPK Jurusan Polije</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.gradient-maroon { background: linear-gradient(135deg, #5B7B89 0%, #7B9BA5 100%); }
|
||||
.text-maroon { color: #5B7B89; }
|
||||
.border-maroon { border-color: #5B7B89; }
|
||||
.bg-cream { background-color: #F8FAFC; }
|
||||
.bg-maroon { background-color: #5B7B89; }
|
||||
.hover\:bg-maroon:hover { background-color: #7B9BA5; }
|
||||
.gradient-maroon { background: linear-gradient(135deg, #7c3aed 0%, #6366f1 100%); }
|
||||
.text-maroon { color: #7c3aed; }
|
||||
.border-maroon { border-color: #7c3aed; }
|
||||
.bg-cream { background-color: #f8fafc; }
|
||||
.bg-maroon { background-color: #7c3aed; }
|
||||
.hover\:bg-maroon:hover { background-color: #6366f1; }
|
||||
.stat-card { transition: all 0.3s ease; }
|
||||
.stat-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(91, 123, 137, 0.1); }
|
||||
.stat-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(124, 58, 237, 0.1); }
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar-dark {
|
||||
|
|
@ -23,17 +23,18 @@
|
|||
.sidebar-link {
|
||||
transition: all 0.25s cubic-bezier(.4,0,.2,1);
|
||||
border-left: 3px solid transparent;
|
||||
color: #94a3b8;
|
||||
/* Higher contrast on dark sidebar */
|
||||
color: #cbd5e1;
|
||||
}
|
||||
.sidebar-link:hover {
|
||||
background: rgba(91, 123, 137, 0.15);
|
||||
color: #e2e8f0;
|
||||
border-left-color: rgba(91, 123, 137, 0.5);
|
||||
background: rgba(124, 58, 237, 0.12);
|
||||
color: #ffffff;
|
||||
border-left-color: rgba(124, 58, 237, 0.5);
|
||||
}
|
||||
.sidebar-link.active {
|
||||
background: linear-gradient(90deg, rgba(91,123,137,0.25) 0%, rgba(91,123,137,0.05) 100%);
|
||||
color: #7dd3fc !important;
|
||||
border-left-color: #7dd3fc;
|
||||
background: linear-gradient(90deg, rgba(124, 58, 237, 0.2) 0%, rgba(124, 58, 237, 0.03) 100%);
|
||||
color: #c4b5fd !important;
|
||||
border-left-color: #c4b5fd;
|
||||
}
|
||||
.sidebar-link .sidebar-icon {
|
||||
display: inline-flex;
|
||||
|
|
@ -48,18 +49,19 @@
|
|||
transition: all 0.25s ease;
|
||||
}
|
||||
.sidebar-link:hover .sidebar-icon {
|
||||
background: rgba(91, 123, 137, 0.3);
|
||||
background: rgba(124, 58, 237, 0.25);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.sidebar-link.active .sidebar-icon {
|
||||
background: rgba(125, 211, 252, 0.15);
|
||||
background: rgba(124, 58, 237, 0.15);
|
||||
}
|
||||
.sidebar-section-label {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
color: #475569;
|
||||
/* Keep section labels readable on dark background */
|
||||
color: #94a3b8;
|
||||
padding: 0 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
|
@ -71,13 +73,13 @@
|
|||
.sidebar-brand-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, #5B7B89 0%, #7B9BA5 100%);
|
||||
background: linear-gradient(135deg, #7c3aed 0%, #6366f1 100%);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 4px 12px rgba(91, 123, 137, 0.3);
|
||||
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);
|
||||
}
|
||||
.sidebar-footer {
|
||||
border-top: 1px solid rgba(255,255,255,0.06);
|
||||
|
|
@ -104,7 +106,7 @@
|
|||
</button>
|
||||
<div>
|
||||
<h1 class="text-lg sm:text-xl md:text-2xl font-bold">🔧 Admin Panel</h1>
|
||||
<p class="text-xs text-gray-200 font-semibold">Sistem Pemilihan Jurusan Kuliah</p>
|
||||
<p class="text-xs text-gray-200 font-semibold">Sistem Pemilihan Jurusan Politeknik Negeri Jember</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
|
|
@ -118,7 +120,7 @@
|
|||
<a href="{{ route('admin.profil') }}" class="block px-4 py-3 hover:bg-gray-50 text-xs sm:text-sm font-semibold border-b">
|
||||
👤 Profil Admin
|
||||
</a>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
<form method="POST" action="{{ route('logout') }}" class="confirm-logout">
|
||||
@csrf
|
||||
<button type="submit" class="block w-full text-left px-4 py-3 hover:bg-gray-50 text-xs sm:text-sm font-semibold text-red-600 rounded-b-lg">
|
||||
🚪 Logout
|
||||
|
|
@ -140,7 +142,7 @@
|
|||
<div class="sidebar-brand-icon">🎓</div>
|
||||
<div>
|
||||
<p class="text-white font-bold text-sm leading-tight">SPK Jurusan</p>
|
||||
<p class="text-xs text-slate-400">Admin Panel</p>
|
||||
<p class="text-xs text-white">Admin Panel</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -160,6 +162,9 @@
|
|||
<a href="{{ route('admin.guru-bk') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('admin.guru-bk*') ? 'active' : '' }}">
|
||||
<span class="sidebar-icon">👨🏫</span> Akun Guru BK
|
||||
</a>
|
||||
<a href="{{ route('admin.alumni.index') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('alumni*') ? 'active' : '' }}">
|
||||
<span class="sidebar-icon">🎓</span> Data Alumni
|
||||
</a>
|
||||
|
||||
<p class="sidebar-section-label mt-5">Riwayat</p>
|
||||
|
||||
|
|
@ -178,8 +183,8 @@
|
|||
{{ strtoupper(substr(Auth::user()->name, 0, 1)) }}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-xs font-medium text-slate-300 truncate">{{ Auth::user()->name }}</p>
|
||||
<p class="text-xs text-slate-500">{{ Auth::user()->role === 'admin' ? 'Administrator' : (Auth::user()->role === 'bk' ? 'Guru BK' : ucfirst(Auth::user()->role)) }}</p>
|
||||
<p class="text-xs font-medium text-white truncate">{{ Auth::user()->name }}</p>
|
||||
<p class="text-xs text-white">{{ Auth::user()->role === 'admin' ? 'Administrator' : (Auth::user()->role === 'bk' ? 'Guru BK' : ucfirst(Auth::user()->role)) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -195,7 +200,7 @@
|
|||
<div class="sidebar-brand-icon" style="width:36px;height:36px;font-size:18px;">🎓</div>
|
||||
<span class="font-bold text-white text-sm">SPK Jurusan</span>
|
||||
</div>
|
||||
<button id="closeMobileMenu" class="text-slate-400 hover:text-white transition text-xl">✕</button>
|
||||
<button id="closeMobileMenu" class="text-gray-400 hover:text-white transition text-xl">✕</button>
|
||||
</div>
|
||||
<nav class="px-3 py-3 space-y-1">
|
||||
<p class="sidebar-section-label mt-1">Menu Utama</p>
|
||||
|
|
@ -211,6 +216,9 @@
|
|||
<a href="{{ route('admin.guru-bk') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('admin.guru-bk*') ? 'active' : '' }}">
|
||||
<span class="sidebar-icon">👨🏫</span> Akun Guru BK
|
||||
</a>
|
||||
<a href="{{ route('admin.alumni.index') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('alumni*') ? 'active' : '' }}">
|
||||
<span class="sidebar-icon">🎓</span> Data Alumni
|
||||
</a>
|
||||
|
||||
<p class="sidebar-section-label mt-5">Riwayat</p>
|
||||
<a href="{{ route('admin.riwayat-rekomendasi') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('admin.riwayat-rekomendasi*') ? 'active' : '' }}">
|
||||
|
|
@ -271,6 +279,50 @@
|
|||
mobileOverlay.addEventListener('click', () => mobileSidebar.classList.add('hidden'));
|
||||
closeMobileMenu.addEventListener('click', () => mobileSidebar.classList.add('hidden'));
|
||||
</script>
|
||||
<!-- SweetAlert2 (admin) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.querySelectorAll('form.confirm-logout').forEach(function(form) {
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
Swal.fire({
|
||||
title: 'Konfirmasi Logout',
|
||||
text: 'Yakin ingin logout dari akun ini?',
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Ya, Logout',
|
||||
cancelButtonText: 'Batal',
|
||||
reverseButtons: true
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
// Generic SweetAlert2 confirmation for destructive actions in admin
|
||||
document.querySelectorAll('form.swal-confirm').forEach(function(form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const msg = form.getAttribute('data-confirm-message') || 'Yakin melanjutkan aksi ini?';
|
||||
Swal.fire({
|
||||
title: 'Konfirmasi',
|
||||
text: msg,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Ya',
|
||||
cancelButtonText: 'Batal',
|
||||
reverseButtons: true
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@yield('scripts')
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -8,22 +8,53 @@
|
|||
<p class="text-sm text-gray-500 mt-1">Kelola informasi akun administrator</p>
|
||||
</div>
|
||||
|
||||
<!-- Success Alert -->
|
||||
@if(session('success'))
|
||||
<div class="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg flex items-center gap-3">
|
||||
<span class="text-green-600 text-xl">✅</span>
|
||||
<span class="text-green-700 font-medium">{{ session('success') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Error Summary Alert -->
|
||||
@if($errors->any())
|
||||
<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-red-600 text-xl">⚠️</span>
|
||||
<span class="text-red-700 font-bold">Terjadi Kesalahan</span>
|
||||
</div>
|
||||
<ul class="text-red-600 text-sm space-y-1 ml-8">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>• {{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Update Profile -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">👤 Informasi Akun</h3>
|
||||
<form action="{{ route('admin.profil.update') }}" method="POST" class="space-y-4">
|
||||
<form action="{{ route('admin.profil.update') }}" method="POST" class="space-y-4" id="profileForm">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama *</label>
|
||||
<input type="text" name="name" required value="{{ old('name', $admin->name) }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-maroon">
|
||||
@error('name') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama * <span class="text-gray-400 text-xs">(minimal 3 karakter)</span></label>
|
||||
<input type="text" name="name" required value="{{ old('name', $admin->name) }}" minlength="3" maxlength="100" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 transition @error('name') border-red-500 focus:ring-red-400 @else border-gray-300 focus:ring-maroon @enderror" placeholder="Nama lengkap" oninput="validateName(this)">
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<span id="nameError" class="text-red-500 text-xs hidden">⚠️ Nama harus minimal 3 karakter</span>
|
||||
<span id="nameValid" class="text-green-500 text-xs hidden">✓ Nama valid</span>
|
||||
</div>
|
||||
@error('name') <span class="text-red-500 text-xs block mt-1">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Email *</label>
|
||||
<input type="email" name="email" required value="{{ old('email', $admin->email) }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-maroon">
|
||||
@error('email') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Email * <span class="text-gray-400 text-xs">(format email valid)</span></label>
|
||||
<input type="email" name="email" required value="{{ old('email', $admin->email) }}" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 transition @error('email') border-red-500 focus:ring-red-400 @else border-gray-300 focus:ring-maroon @enderror" placeholder="email@example.com" oninput="validateEmail(this)">
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<span id="emailError" class="text-red-500 text-xs hidden">⚠️ Format email tidak valid</span>
|
||||
<span id="emailValid" class="text-green-500 text-xs hidden">✓ Email valid</span>
|
||||
</div>
|
||||
@error('email') <span class="text-red-500 text-xs block mt-1">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Role</label>
|
||||
|
|
@ -33,7 +64,7 @@
|
|||
<label class="block text-sm font-semibold text-gray-700 mb-2">Terdaftar Sejak</label>
|
||||
<input type="text" value="{{ $admin->created_at->format('d M Y H:i') }}" readonly class="w-full px-4 py-2 border border-gray-200 rounded-lg bg-gray-100 text-gray-500 cursor-not-allowed">
|
||||
</div>
|
||||
<button type="submit" class="w-full gradient-maroon text-white font-bold py-3 px-4 rounded-lg hover:opacity-90 transition">
|
||||
<button type="submit" class="w-full gradient-maroon text-white font-bold py-3 px-4 rounded-lg hover:opacity-90 transition" id="profileSubmit">
|
||||
💾 Update Profil
|
||||
</button>
|
||||
</form>
|
||||
|
|
@ -42,33 +73,38 @@
|
|||
<!-- Change Password -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">🔒 Ubah Password</h3>
|
||||
<form action="{{ route('admin.profil.password') }}" method="POST" class="space-y-4">
|
||||
<form action="{{ route('admin.profil.password') }}" method="POST" class="space-y-4" id="passwordForm">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Password Lama *</label>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Password Lama * <span class="text-gray-400 text-xs">(harus benar)</span></label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="password" id="currentPass" name="current_password" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" placeholder="Masukkan password lama" style="padding-right: 45px;">
|
||||
<input type="password" id="currentPass" name="current_password" required class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 transition @error('current_password') border-red-500 focus:ring-red-400 @else border-gray-300 focus:ring-blue-400 @enderror" placeholder="Masukkan password lama" style="padding-right: 45px;" oninput="validatePasswordForm()">
|
||||
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('currentPass', this)">👁️</button>
|
||||
</div>
|
||||
@error('current_password') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||
@error('current_password') <span class="text-red-500 text-xs block mt-1">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Password Baru *</label>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Password Baru * <span class="text-gray-400 text-xs">(minimal 8 karakter)</span></label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="password" id="newPass" name="password" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" placeholder="Minimal 8 karakter" style="padding-right: 45px;">
|
||||
<input type="password" id="newPass" name="password" required minlength="8" maxlength="255" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 transition @error('password') border-red-500 focus:ring-red-400 @else border-gray-300 focus:ring-blue-400 @enderror" placeholder="Minimal 8 karakter" style="padding-right: 45px;" oninput="validatePasswordForm()">
|
||||
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('newPass', this)">👁️</button>
|
||||
</div>
|
||||
@error('password') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||
<div id="passwordStrength" class="text-xs mt-1 font-medium hidden">
|
||||
<span id="passwordStrengthText"></span>
|
||||
</div>
|
||||
@error('password') <span class="text-red-500 text-xs block mt-1">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Konfirmasi Password Baru *</label>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Konfirmasi Password Baru * <span class="text-gray-400 text-xs">(harus sama)</span></label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="password" id="newPassConfirm" name="password_confirmation" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" placeholder="Ulangi password baru" style="padding-right: 45px;">
|
||||
<input type="password" id="newPassConfirm" name="password_confirmation" required class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 transition border-gray-300 focus:ring-blue-400" placeholder="Ulangi password baru" style="padding-right: 45px;" oninput="validatePasswordForm()">
|
||||
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('newPassConfirm', this)">👁️</button>
|
||||
</div>
|
||||
<div id="confirmError" class="text-red-500 text-xs hidden mt-1">⚠️ Password konfirmasi tidak cocok</div>
|
||||
<div id="confirmValid" class="text-green-500 text-xs hidden mt-1">✓ Password cocok</div>
|
||||
</div>
|
||||
<button type="submit" class="w-full bg-blue-600 text-white font-bold py-3 px-4 rounded-lg hover:bg-blue-700 transition">
|
||||
<button type="submit" class="w-full bg-blue-600 text-white font-bold py-3 px-4 rounded-lg hover:bg-blue-700 transition disabled:opacity-50 disabled:cursor-not-allowed" id="passwordSubmit">
|
||||
🔑 Ubah Password
|
||||
</button>
|
||||
</form>
|
||||
|
|
@ -78,10 +114,160 @@
|
|||
|
||||
@section('scripts')
|
||||
<script>
|
||||
// Toggle password visibility
|
||||
function togglePasswordVisibility(inputId, buttonElement) {
|
||||
const input = document.getElementById(inputId);
|
||||
const isPassword = input.type === 'password';
|
||||
input.type = isPassword ? 'text' : 'password';
|
||||
}
|
||||
|
||||
// Validate name
|
||||
function validateName(input) {
|
||||
const nameError = document.getElementById('nameError');
|
||||
const nameValid = document.getElementById('nameValid');
|
||||
const value = input.value.trim();
|
||||
|
||||
if (value.length === 0) {
|
||||
nameError.classList.add('hidden');
|
||||
nameValid.classList.add('hidden');
|
||||
} else if (value.length < 3) {
|
||||
input.classList.remove('focus:ring-maroon');
|
||||
input.classList.add('border-red-500', 'focus:ring-red-400');
|
||||
nameError.classList.remove('hidden');
|
||||
nameValid.classList.add('hidden');
|
||||
} else {
|
||||
input.classList.remove('border-red-500', 'focus:ring-red-400');
|
||||
input.classList.add('border-gray-300', 'focus:ring-maroon');
|
||||
nameError.classList.add('hidden');
|
||||
nameValid.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Validate email
|
||||
function validateEmail(input) {
|
||||
const emailError = document.getElementById('emailError');
|
||||
const emailValid = document.getElementById('emailValid');
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const value = input.value.trim();
|
||||
|
||||
if (value.length === 0) {
|
||||
emailError.classList.add('hidden');
|
||||
emailValid.classList.add('hidden');
|
||||
} else if (!emailRegex.test(value)) {
|
||||
input.classList.remove('focus:ring-maroon');
|
||||
input.classList.add('border-red-500', 'focus:ring-red-400');
|
||||
emailError.classList.remove('hidden');
|
||||
emailValid.classList.add('hidden');
|
||||
} else {
|
||||
input.classList.remove('border-red-500', 'focus:ring-red-400');
|
||||
input.classList.add('border-gray-300', 'focus:ring-maroon');
|
||||
emailError.classList.add('hidden');
|
||||
emailValid.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Check password strength
|
||||
function getPasswordStrength(password) {
|
||||
let strength = 0;
|
||||
if (password.length >= 8) strength++;
|
||||
if (password.length >= 12) strength++;
|
||||
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++;
|
||||
if (/\d/.test(password)) strength++;
|
||||
if (/[^a-zA-Z\d]/.test(password)) strength++;
|
||||
|
||||
if (strength <= 1) return { level: 'Lemah', color: 'text-red-500' };
|
||||
if (strength <= 2) return { level: 'Sedang', color: 'text-yellow-500' };
|
||||
if (strength <= 3) return { level: 'Baik', color: 'text-blue-500' };
|
||||
return { level: 'Kuat', color: 'text-green-500' };
|
||||
}
|
||||
|
||||
// Validate password form
|
||||
function validatePasswordForm() {
|
||||
const currentPass = document.getElementById('currentPass').value;
|
||||
const newPass = document.getElementById('newPass').value;
|
||||
const confirmPass = document.getElementById('newPassConfirm').value;
|
||||
|
||||
const passwordStrength = document.getElementById('passwordStrength');
|
||||
const passwordStrengthText = document.getElementById('passwordStrengthText');
|
||||
const confirmError = document.getElementById('confirmError');
|
||||
const confirmValid = document.getElementById('confirmValid');
|
||||
const passwordSubmit = document.getElementById('passwordSubmit');
|
||||
|
||||
// Show password strength if new password is entered
|
||||
if (newPass.length > 0) {
|
||||
const strength = getPasswordStrength(newPass);
|
||||
passwordStrengthText.textContent = `Kekuatan: ${strength.level}`;
|
||||
passwordStrengthText.className = `${strength.color}`;
|
||||
passwordStrength.classList.remove('hidden');
|
||||
} else {
|
||||
passwordStrength.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Check password confirmation
|
||||
if (confirmPass.length > 0) {
|
||||
if (newPass !== confirmPass) {
|
||||
confirmError.classList.remove('hidden');
|
||||
confirmValid.classList.add('hidden');
|
||||
} else if (newPass.length >= 8) {
|
||||
confirmError.classList.add('hidden');
|
||||
confirmValid.classList.remove('hidden');
|
||||
} else {
|
||||
confirmError.classList.add('hidden');
|
||||
confirmValid.classList.add('hidden');
|
||||
}
|
||||
} else {
|
||||
confirmError.classList.add('hidden');
|
||||
confirmValid.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Enable/disable submit button
|
||||
const isValid = currentPass.length > 0 && newPass.length >= 8 && newPass === confirmPass;
|
||||
passwordSubmit.disabled = !isValid;
|
||||
}
|
||||
|
||||
// Validate profile form on submit
|
||||
document.getElementById('profileForm')?.addEventListener('submit', function(e) {
|
||||
const name = document.querySelector('input[name="name"]').value.trim();
|
||||
const email = document.querySelector('input[name="email"]').value.trim();
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
let errors = [];
|
||||
|
||||
if (name.length < 3) {
|
||||
errors.push('Nama harus minimal 3 karakter');
|
||||
}
|
||||
if (!emailRegex.test(email)) {
|
||||
errors.push('Format email tidak valid');
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
e.preventDefault();
|
||||
alert('Perbaiki kesalahan berikut:\n• ' + errors.join('\n• '));
|
||||
}
|
||||
});
|
||||
|
||||
// Validate password form on submit
|
||||
document.getElementById('passwordForm')?.addEventListener('submit', function(e) {
|
||||
const currentPass = document.getElementById('currentPass').value;
|
||||
const newPass = document.getElementById('newPass').value;
|
||||
const confirmPass = document.getElementById('newPassConfirm').value;
|
||||
|
||||
let errors = [];
|
||||
|
||||
if (currentPass.length === 0) {
|
||||
errors.push('Password lama harus diisi');
|
||||
}
|
||||
if (newPass.length < 8) {
|
||||
errors.push('Password baru harus minimal 8 karakter');
|
||||
}
|
||||
if (newPass !== confirmPass) {
|
||||
errors.push('Password konfirmasi tidak cocok dengan password baru');
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
e.preventDefault();
|
||||
alert('Perbaiki kesalahan berikut:\n• ' + errors.join('\n• '));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -13,120 +13,132 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Profile Card -->
|
||||
<!-- Profile Header Card -->
|
||||
<div class="bg-white rounded-lg shadow-lg p-6 mb-6 border-l-4 border-maroon">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">Nama</p>
|
||||
<p class="text-xl font-bold text-maroon mt-1">{{ $student->name }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">Email</p>
|
||||
<p class="text-gray-800 mt-1">{{ $student->email }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">NIS</p>
|
||||
<p class="text-gray-800 font-semibold mt-1">{{ $student->nis ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">Kelompok Asal</p>
|
||||
@if($student->kelompok_asal)
|
||||
<p class="mt-1">
|
||||
<span class="px-3 py-1 rounded text-sm font-bold" style="{{ $student->kelompok_asal == 'IPA' ? 'background-color: #E0F2FE; color: #0369A1;' : 'background-color: #FEF3C7; color: #92400E;' }}">
|
||||
{{ $student->kelompok_asal }}
|
||||
</span>
|
||||
</p>
|
||||
@else
|
||||
<p class="text-gray-500 mt-1">-</p>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">Foto Profil</p>
|
||||
<div class="flex flex-col md:flex-row gap-6 items-start">
|
||||
<!-- Avatar Section -->
|
||||
<div class="flex-shrink-0">
|
||||
@if($student->foto)
|
||||
<img src="{{ Storage::url($student->foto) }}" alt="{{ $student->name }}" class="w-24 h-24 rounded-lg object-cover mt-2 border-2 border-maroon">
|
||||
<img src="{{ Storage::url($student->foto) }}" alt="{{ $student->name }}" class="w-24 h-24 md:w-32 md:h-32 rounded-lg object-cover border-4 border-maroon shadow-md">
|
||||
@else
|
||||
<p class="text-gray-500 mt-1">-</p>
|
||||
<div class="w-24 h-24 md:w-32 md:h-32 rounded-lg bg-gradient-to-br from-maroon to-teal-600 flex items-center justify-center text-white text-2xl md:text-3xl font-bold shadow-md">
|
||||
{{ strtoupper(substr($student->name, 0, 1)) }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">Terdaftar</p>
|
||||
<p class="text-gray-800 mt-1">{{ $student->created_at->format('d M Y H:i') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rekomendasi -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-green-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">🎯 Riwayat Rekomendasi ({{ count($recommendations) }})</h3>
|
||||
<!-- Info Section -->
|
||||
<div class="flex-1">
|
||||
<h3 class="text-2xl md:text-3xl font-bold text-maroon mb-4">{{ $student->name }}</h3>
|
||||
|
||||
@if($recommendations->isNotEmpty())
|
||||
<div class="space-y-4">
|
||||
@foreach($recommendations as $rec)
|
||||
<div class="border border-gray-200 rounded-lg p-4 hover:bg-gray-50 transition">
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<p class="text-sm text-gray-500">{{ $rec->created_at->format('d M Y H:i') }}</p>
|
||||
<span class="px-2 py-1 rounded text-xs font-bold bg-blue-100 text-blue-800">Rekomendasi #{{ $loop->index + 1 }}</span>
|
||||
</div>
|
||||
|
||||
@if($rec->hasil_rekomendasi && is_array($rec->hasil_rekomendasi))
|
||||
<div class="mt-3">
|
||||
<p class="text-xs font-semibold text-gray-600 mb-2">Top 3 Rekomendasi:</p>
|
||||
<div class="space-y-2">
|
||||
@foreach(array_slice($rec->hasil_rekomendasi, 0, 3) as $idx => $hasil)
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-700">{{ $idx + 1 }}. {{ $hasil['jurusan'] ?? 'N/A' }}</span>
|
||||
@php $skorVal = $hasil['skor'] ?? 0; @endphp
|
||||
<span class="px-2 py-1 rounded text-xs font-bold" style="background-color: {{ $idx === 0 ? '#DBEAFE' : '#F3F4F6' }}; color: {{ $idx === 0 ? '#1e40af' : '#6B7280' }};">
|
||||
{{ round(($skorVal > 1 ? $skorVal : $skorVal * 100), 1) }}%
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500">NIS</p>
|
||||
<p class="text-lg font-bold text-gray-800">{{ $student->nis ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500">Kelompok</p>
|
||||
@if($student->kelompok_asal)
|
||||
<span class="inline-block px-3 py-1 rounded text-sm font-bold mt-1" style="{{ $student->kelompok_asal == 'IPA' ? 'background-color: #E0F2FE; color: #0369A1;' : 'background-color: #FEF3C7; color: #92400E;' }}">
|
||||
{{ $student->kelompok_asal }}
|
||||
</span>
|
||||
@else
|
||||
<p class="text-lg font-bold text-gray-500">-</p>
|
||||
@endif
|
||||
|
||||
<div class="mt-3 p-3 bg-gray-50 rounded text-xs text-gray-600">
|
||||
<p><strong>Minat:</strong> {{ $rec->minat ?? '-' }} | <strong>Cita-cita:</strong> {{ $rec->cita_cita ?? '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500">Email</p>
|
||||
<p class="text-sm font-semibold text-gray-800 break-all">{{ $student->email }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500">Terdaftar</p>
|
||||
<p class="text-sm font-semibold text-gray-800">{{ $student->created_at->format('d M Y') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-gray-500 text-sm">Siswa belum melakukan rekomendasi</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat History -->
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-blue-400">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-bold text-maroon">💬 Chat History ({{ count($chatHistories) }})</h3>
|
||||
@if(count($chatHistories) > 0)
|
||||
<a href="{{ route('admin.student.chat', $student->id) }}" class="bg-blue-500 text-white font-semibold py-2 px-3 rounded-lg hover:bg-blue-600 transition text-xs">
|
||||
Lihat Semua →
|
||||
</a>
|
||||
@endif
|
||||
<!-- Main Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
|
||||
<!-- Left Column: Rekomendasi (2/3 width) -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-green-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">🎯 Riwayat Rekomendasi ({{ count($recommendations) }})</h3>
|
||||
|
||||
@if($recommendations->isNotEmpty())
|
||||
<div class="space-y-4">
|
||||
@foreach($recommendations as $rec)
|
||||
<div class="border-2 border-green-100 rounded-lg p-4 hover:shadow-md hover:border-green-400 transition">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<p class="text-xs font-semibold text-gray-500">{{ $rec->created_at->format('d M Y H:i') }}</p>
|
||||
<span class="px-2 py-1 rounded-full text-xs font-bold bg-green-100 text-green-700">Rekomendasi #{{ $loop->index + 1 }}</span>
|
||||
</div>
|
||||
|
||||
@if($rec->hasil_rekomendasi && is_array($rec->hasil_rekomendasi))
|
||||
<div class="space-y-2">
|
||||
@foreach(array_slice($rec->hasil_rekomendasi, 0, 3) as $idx => $hasil)
|
||||
<div class="flex items-center justify-between bg-gradient-to-r {{ $idx === 0 ? 'from-green-50 to-transparent' : 'from-gray-50 to-transparent' }} p-3 rounded">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full {{ $idx === 0 ? 'bg-green-500 text-white' : 'bg-gray-300 text-white' }} text-xs font-bold">
|
||||
{{ $idx + 1 }}
|
||||
</span>
|
||||
<span class="text-sm font-semibold text-gray-800">{{ $hasil['jurusan'] ?? 'N/A' }}</span>
|
||||
</div>
|
||||
@php $skorVal = $hasil['skor'] ?? 0; @endphp
|
||||
<span class="px-3 py-1 rounded-full text-xs font-bold {{ $idx === 0 ? 'bg-green-200 text-green-800' : 'bg-gray-200 text-gray-800' }}">
|
||||
{{ round(($skorVal > 1 ? $skorVal : $skorVal * 100), 1) }}%
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if(isset($rec->minat) || isset($rec->cita_cita))
|
||||
<div class="mt-3 p-3 bg-blue-50 border-l-4 border-blue-300 rounded">
|
||||
<p class="text-xs text-gray-700"><strong>📝 Minat:</strong> {{ $rec->minat ?? '-' }} | <strong>🎓 Cita-cita:</strong> {{ $rec->cita_cita ?? '-' }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<p class="text-sm">Siswa belum melakukan rekomendasi</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($chatHistories->isNotEmpty())
|
||||
<div class="space-y-3 max-h-96 overflow-y-auto">
|
||||
@foreach($chatHistories as $chat)
|
||||
<div class="border-b pb-3 last:border-b-0">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<p class="text-xs font-semibold text-gray-600">{{ $chat->created_at->format('d M Y H:i') }}</p>
|
||||
</div>
|
||||
<div class="bg-blue-50 border-l-4 border-blue-400 p-3 rounded mb-2">
|
||||
<p class="text-xs font-semibold text-gray-700 mb-1">👤 Pertanyaan Siswa:</p>
|
||||
<p class="text-sm text-gray-800">{{ \Illuminate\Support\Str::limit($chat->prompt, 150) }}</p>
|
||||
</div>
|
||||
<div class="bg-green-50 border-l-4 border-green-400 p-3 rounded">
|
||||
<p class="text-xs font-semibold text-gray-700 mb-1">🤖 Jawaban AI:</p>
|
||||
<p class="text-sm text-gray-800">{{ \Illuminate\Support\Str::limit($chat->response, 150) }}</p>
|
||||
</div>
|
||||
<!-- Right Column: Chat History (1/3 width) -->
|
||||
<div class="lg:col-span-1">
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-blue-400 sticky top-20">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-bold text-maroon">💬 Chat</h3>
|
||||
<span class="text-xs font-bold bg-blue-100 text-blue-700 px-2 py-1 rounded-full">{{ count($chatHistories) }}</span>
|
||||
</div>
|
||||
|
||||
@if($chatHistories->isNotEmpty())
|
||||
<div class="space-y-3 max-h-96 overflow-y-auto pr-2">
|
||||
@foreach($chatHistories->take(5) as $chat)
|
||||
<div class="border border-blue-100 rounded-lg p-3 hover:bg-blue-50 transition">
|
||||
<p class="text-xs font-semibold text-gray-500 mb-2">{{ $chat->created_at->format('d M Y') }}</p>
|
||||
<p class="text-xs text-gray-700 mb-2"><strong>Q:</strong> {{ \Illuminate\Support\Str::limit($chat->prompt, 80) }}</p>
|
||||
<p class="text-xs text-gray-600"><strong>A:</strong> {{ \Illuminate\Support\Str::limit($chat->response, 80) }}</p>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
@if(count($chatHistories) > 5)
|
||||
<a href="{{ route('admin.student.chat', $student->id) }}" class="block mt-3 text-center text-xs text-blue-600 font-semibold hover:text-blue-800">
|
||||
Lihat Semua ({{ count($chatHistories) }}) →
|
||||
</a>
|
||||
@endif
|
||||
@else
|
||||
<p class="text-gray-500 text-xs text-center py-4">Belum ada chat</p>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<p class="text-gray-500 text-sm">Siswa belum melakukan chat dengan AI</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -1,236 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Input Alumni Baru - Sistem Pemilihan Jurusan</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.gradient-maroon {
|
||||
background: linear-gradient(135deg, #5B7B89 0%, #7B9BA5 100%);
|
||||
}
|
||||
.text-maroon {
|
||||
color: #5B7B89;
|
||||
}
|
||||
.border-maroon {
|
||||
border-color: #5B7B89;
|
||||
}
|
||||
.bg-cream {
|
||||
background-color: #F8FAFC;
|
||||
}
|
||||
.focus-maroon:focus {
|
||||
border-color: #5B7B89;
|
||||
box-shadow: 0 0 0 3px rgba(107, 44, 44, 0.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-cream">
|
||||
<!-- Header -->
|
||||
<header class="gradient-maroon text-white shadow-lg sticky top-0 z-50">
|
||||
<div class="container mx-auto px-4 sm:px-6 py-4 sm:py-6 flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-xl sm:text-2xl md:text-3xl font-bold">Input Alumni Baru</h1>
|
||||
<p class="text-xs sm:text-sm text-yellow-300 font-semibold mt-1">Tambah data alumni untuk validasi algoritma</p>
|
||||
</div>
|
||||
<a href="{{ route('alumni.index') }}" class="bg-yellow-400 text-maroon font-bold py-2 px-4 rounded-lg hover:bg-yellow-300 transition text-xs sm:text-sm">
|
||||
← Kembali
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="container mx-auto px-4 sm:px-6 py-6 sm:py-12">
|
||||
@if($errors->any())
|
||||
<div class="mb-6 p-4 bg-red-100 border-l-4 border-red-500 text-red-700 rounded">
|
||||
<p class="font-bold mb-2">❌ Ada kesalahan validasi:</p>
|
||||
<ul class="list-disc pl-5 text-sm">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('alumni.store') }}" method="POST" class="max-w-4xl mx-auto">
|
||||
@csrf
|
||||
|
||||
<!-- Section 1: Data Dasar Alumni -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-maroon">
|
||||
<h2 class="text-lg font-bold text-maroon mb-4">📋 Data Dasar Alumni</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama Alumni *</label>
|
||||
<input type="text" name="nama_alumni" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('nama_alumni') }}" placeholder="Nama lengkap">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">NIS</label>
|
||||
<input type="text" name="nis" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('nis') }}" placeholder="12345">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Kelompok Asal *</label>
|
||||
<select name="kelompok_asal" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm">
|
||||
<option value="">-- Pilih --</option>
|
||||
<option value="IPA" {{ old('kelompok_asal') == 'IPA' ? 'selected' : '' }}>IPA</option>
|
||||
<option value="IPS" {{ old('kelompok_asal') == 'IPS' ? 'selected' : '' }}>IPS</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Tahun Masuk *</label>
|
||||
<input type="number" name="tahun_masuk" required min="2020" max="{{ date('Y') }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('tahun_masuk') }}" placeholder="2023">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 2: Nilai Saat Entry -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-yellow-400">
|
||||
<h2 class="text-lg font-bold text-maroon mb-4">📊 Nilai Saat Entry (Rapor)</h2>
|
||||
<div id="nilaiFields" class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Matematika</label>
|
||||
<input type="number" name="mtk" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('mtk') }}" placeholder="85">
|
||||
</div>
|
||||
<div id="ipa-fields" style="display: none;" class="contents">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Fisika</label>
|
||||
<input type="number" name="fisika" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('fisika') }}" placeholder="78">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Kimia</label>
|
||||
<input type="number" name="kimia" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('kimia') }}" placeholder="72">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Biologi</label>
|
||||
<input type="number" name="biologi" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('biologi') }}" placeholder="80">
|
||||
</div>
|
||||
</div>
|
||||
<div id="ips-fields" style="display: none;" class="contents">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Ekonomi</label>
|
||||
<input type="number" name="ekonomi" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('ekonomi') }}" placeholder="82">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Geografi</label>
|
||||
<input type="number" name="geografi" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('geografi') }}" placeholder="76">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Sosiologi</label>
|
||||
<input type="number" name="sosiologi" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('sosiologi') }}" placeholder="74">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Sejarah</label>
|
||||
<input type="number" name="sejarah" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('sejarah') }}" placeholder="70">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 3: Variabel Non-Akademik -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-blue-400">
|
||||
<h2 class="text-lg font-bold text-maroon mb-4">💡 Variabel Non-Akademik</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Minat</label>
|
||||
<input type="text" name="minat" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('minat') }}" placeholder="Contoh: coding, bercocok tanam">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Cita-cita / Profesi</label>
|
||||
<input type="text" name="cita_cita" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('cita_cita') }}" placeholder="Contoh: Programmer, Dokter">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Preferensi Studi</label>
|
||||
<select name="preferensi_studi" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm">
|
||||
<option value="">-- Pilih --</option>
|
||||
<option value="Praktik Langsung" {{ old('preferensi_studi') == 'Praktik Langsung' ? 'selected' : '' }}>Praktik Langsung</option>
|
||||
<option value="DuDi" {{ old('preferensi_studi') == 'DuDi' ? 'selected' : '' }}>DuDi (Dunia Usaha & Industri)</option>
|
||||
<option value="Project Based" {{ old('preferensi_studi') == 'Project Based' ? 'selected' : '' }}>Project Based</option>
|
||||
<option value="Blended" {{ old('preferensi_studi') == 'Blended' ? 'selected' : '' }}>Blended (Teori + Praktik)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Prestasi (opsional)</label>
|
||||
<input type="text" name="prestasi" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('prestasi') }}" placeholder="Contoh: Juara lomba, sertifikat">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 4: Rekomendasi & Hasil -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-green-400">
|
||||
<h2 class="text-lg font-bold text-maroon mb-4">🎯 Rekomendasi & Hasil Algoritma</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Jurusan Masuk *</label>
|
||||
<input type="text" name="major_masuk" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('major_masuk') }}" placeholder="Contoh: Teknologi Informasi">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Ranking Rekomendasi (saat input)</label>
|
||||
<input type="number" name="ranking_saat_rekomendasi" min="1" max="9" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('ranking_saat_rekomendasi') }}" placeholder="1-9">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Score Prediksi</label>
|
||||
<input type="number" name="predicted_score" step="0.01" min="0" max="1" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('predicted_score') }}" placeholder="0.95">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 5: Outcome Alumni -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-purple-400">
|
||||
<h2 class="text-lg font-bold text-maroon mb-4">🎓 Outcome Alumni</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">IPK Lulus</label>
|
||||
<input type="number" name="ipk_lulus" step="0.01" min="0" max="4" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('ipk_lulus') }}" placeholder="3.65">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Tahun Lulus</label>
|
||||
<input type="number" name="tahun_lulus" min="2020" max="{{ date('Y') }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('tahun_lulus') }}" placeholder="2026">
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Karir Outcome (deskripsi)</label>
|
||||
<textarea name="karir_outcome" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" placeholder="Contoh: Bekerja di PT ABC sebagai Software Developer">{{ old('karir_outcome') }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Status Kesuksesan</label>
|
||||
<select name="success_status" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm">
|
||||
<option value="">-- Pilih --</option>
|
||||
<option value="sangat_sukses" {{ old('success_status') == 'sangat_sukses' ? 'selected' : '' }}>✓ Sangat Sukses</option>
|
||||
<option value="sukses" {{ old('success_status') == 'sukses' ? 'selected' : '' }}>✓ Sukses</option>
|
||||
<option value="cukup" {{ old('success_status') == 'cukup' ? 'selected' : '' }}>• Cukup</option>
|
||||
<option value="kurang_sukses" {{ old('success_status') == 'kurang_sukses' ? 'selected' : '' }}>✗ Kurang Sukses</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Catatan Tambahan</label>
|
||||
<textarea name="catatan" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" placeholder="Catatan / observasi tambahan">{{ old('catatan') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Buttons -->
|
||||
<div class="flex gap-4 justify-end">
|
||||
<a href="{{ route('alumni.index') }}" class="px-6 py-2 rounded-lg font-bold bg-gray-300 text-gray-700 hover:bg-gray-400 transition">
|
||||
Batal
|
||||
</a>
|
||||
<button type="submit" class="px-6 py-2 rounded-lg font-bold gradient-maroon text-white hover:opacity-90 transition">
|
||||
💾 Simpan Alumni
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Tampilkan field nilai berdasarkan kelompok asal
|
||||
const kelompokSelect = document.querySelector('select[name="kelompok_asal"]');
|
||||
const ipaFields = document.getElementById('ipa-fields');
|
||||
const ipsFields = document.getElementById('ips-fields');
|
||||
|
||||
function updateNilaiFields() {
|
||||
const kelompok = kelompokSelect.value;
|
||||
ipaFields.style.display = kelompok === 'IPA' ? 'contents' : 'none';
|
||||
ipsFields.style.display = kelompok === 'IPS' ? 'contents' : 'none';
|
||||
}
|
||||
|
||||
kelompokSelect.addEventListener('change', updateNilaiFields);
|
||||
updateNilaiFields(); // Call on load
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,235 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Edit Alumni - Sistem Pemilihan Jurusan</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.gradient-maroon {
|
||||
background: linear-gradient(135deg, #5B7B89 0%, #7B9BA5 100%);
|
||||
}
|
||||
.text-maroon {
|
||||
color: #5B7B89;
|
||||
}
|
||||
.border-maroon {
|
||||
border-color: #5B7B89;
|
||||
}
|
||||
.bg-cream {
|
||||
background-color: #F8FAFC;
|
||||
}
|
||||
.focus-maroon:focus {
|
||||
border-color: #5B7B89;
|
||||
box-shadow: 0 0 0 3px rgba(107, 44, 44, 0.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-cream">
|
||||
<!-- Header -->
|
||||
<header class="gradient-maroon text-white shadow-lg sticky top-0 z-50">
|
||||
<div class="container mx-auto px-4 sm:px-6 py-4 sm:py-6 flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-xl sm:text-2xl md:text-3xl font-bold">Edit Alumni</h1>
|
||||
<p class="text-xs sm:text-sm text-yellow-300 font-semibold mt-1">{{ $alumnus->nama_alumni }}</p>
|
||||
</div>
|
||||
<a href="{{ route('alumni.index') }}" class="bg-yellow-400 text-maroon font-bold py-2 px-4 rounded-lg hover:bg-yellow-300 transition text-xs sm:text-sm">
|
||||
← Kembali
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="container mx-auto px-4 sm:px-6 py-6 sm:py-12">
|
||||
@if($errors->any())
|
||||
<div class="mb-6 p-4 bg-red-100 border-l-4 border-red-500 text-red-700 rounded">
|
||||
<p class="font-bold mb-2">❌ Ada kesalahan validasi:</p>
|
||||
<ul class="list-disc pl-5 text-sm">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('alumni.update', $alumnus->id) }}" method="POST" class="max-w-4xl mx-auto">
|
||||
@csrf @method('PUT')
|
||||
|
||||
<!-- Section 1: Data Dasar Alumni -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-maroon">
|
||||
<h2 class="text-lg font-bold text-maroon mb-4">📋 Data Dasar Alumni</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama Alumni *</label>
|
||||
<input type="text" name="nama_alumni" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('nama_alumni', $alumnus->nama_alumni) }}" placeholder="Nama lengkap">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">NIS</label>
|
||||
<input type="text" name="nis" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('nis', $alumnus->nis) }}" placeholder="12345">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Kelompok Asal *</label>
|
||||
<select name="kelompok_asal" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm">
|
||||
<option value="">-- Pilih --</option>
|
||||
<option value="IPA" {{ old('kelompok_asal', $alumnus->kelompok_asal) == 'IPA' ? 'selected' : '' }}>IPA</option>
|
||||
<option value="IPS" {{ old('kelompok_asal', $alumnus->kelompok_asal) == 'IPS' ? 'selected' : '' }}>IPS</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Tahun Masuk *</label>
|
||||
<input type="number" name="tahun_masuk" required min="2020" max="{{ date('Y') }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('tahun_masuk', $alumnus->tahun_masuk) }}" placeholder="2023">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 2: Nilai Saat Entry -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-yellow-400">
|
||||
<h2 class="text-lg font-bold text-maroon mb-4">📊 Nilai Saat Entry (Rapor)</h2>
|
||||
<div id="nilaiFields" class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Matematika</label>
|
||||
<input type="number" name="mtk" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('mtk', $alumnus->mtk) }}" placeholder="85">
|
||||
</div>
|
||||
<div id="ipa-fields" style="display: none;" class="contents">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Fisika</label>
|
||||
<input type="number" name="fisika" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('fisika', $alumnus->fisika) }}" placeholder="78">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Kimia</label>
|
||||
<input type="number" name="kimia" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('kimia', $alumnus->kimia) }}" placeholder="72">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Biologi</label>
|
||||
<input type="number" name="biologi" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('biologi', $alumnus->biologi) }}" placeholder="80">
|
||||
</div>
|
||||
</div>
|
||||
<div id="ips-fields" style="display: none;" class="contents">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Ekonomi</label>
|
||||
<input type="number" name="ekonomi" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('ekonomi', $alumnus->ekonomi) }}" placeholder="82">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Geografi</label>
|
||||
<input type="number" name="geografi" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('geografi', $alumnus->geografi) }}" placeholder="76">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Sosiologi</label>
|
||||
<input type="number" name="sosiologi" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('sosiologi', $alumnus->sosiologi) }}" placeholder="74">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Sejarah</label>
|
||||
<input type="number" name="sejarah" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('sejarah', $alumnus->sejarah) }}" placeholder="70">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 3: Variabel Non-Akademik -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-blue-400">
|
||||
<h2 class="text-lg font-bold text-maroon mb-4">💡 Variabel Non-Akademik</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Minat</label>
|
||||
<input type="text" name="minat" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('minat', $alumnus->minat) }}" placeholder="Contoh: coding, bercocok tanam">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Cita-cita / Profesi</label>
|
||||
<input type="text" name="cita_cita" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('cita_cita', $alumnus->cita_cita) }}" placeholder="Contoh: Programmer, Dokter">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Preferensi Studi</label>
|
||||
<select name="preferensi_studi" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm">
|
||||
<option value="">-- Pilih --</option>
|
||||
<option value="Praktik Langsung" {{ old('preferensi_studi', $alumnus->preferensi_studi) == 'Praktik Langsung' ? 'selected' : '' }}>Praktik Langsung</option>
|
||||
<option value="DuDi" {{ old('preferensi_studi', $alumnus->preferensi_studi) == 'DuDi' ? 'selected' : '' }}>DuDi (Dunia Usaha & Industri)</option>
|
||||
<option value="Project Based" {{ old('preferensi_studi', $alumnus->preferensi_studi) == 'Project Based' ? 'selected' : '' }}>Project Based</option>
|
||||
<option value="Blended" {{ old('preferensi_studi', $alumnus->preferensi_studi) == 'Blended' ? 'selected' : '' }}>Blended (Teori + Praktik)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Prestasi (opsional)</label>
|
||||
<input type="text" name="prestasi" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('prestasi', $alumnus->prestasi) }}" placeholder="Contoh: Juara lomba, sertifikat">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 4: Rekomendasi & Hasil -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-green-400">
|
||||
<h2 class="text-lg font-bold text-maroon mb-4">🎯 Rekomendasi & Hasil Algoritma</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Jurusan Masuk *</label>
|
||||
<input type="text" name="major_masuk" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('major_masuk', $alumnus->major_masuk) }}" placeholder="Contoh: Teknologi Informasi">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Ranking Rekomendasi (saat input)</label>
|
||||
<input type="number" name="ranking_saat_rekomendasi" min="1" max="9" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('ranking_saat_rekomendasi', $alumnus->ranking_saat_rekomendasi) }}" placeholder="1-9">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Score Prediksi</label>
|
||||
<input type="number" name="predicted_score" step="0.01" min="0" max="1" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('predicted_score', $alumnus->predicted_score) }}" placeholder="0.95">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 5: Outcome Alumni -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-purple-400">
|
||||
<h2 class="text-lg font-bold text-maroon mb-4">🎓 Outcome Alumni</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">IPK Lulus</label>
|
||||
<input type="number" name="ipk_lulus" step="0.01" min="0" max="4" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('ipk_lulus', $alumnus->ipk_lulus) }}" placeholder="3.65">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Tahun Lulus</label>
|
||||
<input type="number" name="tahun_lulus" min="2020" max="{{ date('Y') }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" value="{{ old('tahun_lulus', $alumnus->tahun_lulus) }}" placeholder="2026">
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Karir Outcome (deskripsi)</label>
|
||||
<textarea name="karir_outcome" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" placeholder="Contoh: Bekerja di PT ABC sebagai Software Developer">{{ old('karir_outcome', $alumnus->karir_outcome) }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Status Kesuksesan</label>
|
||||
<select name="success_status" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm">
|
||||
<option value="">-- Pilih --</option>
|
||||
<option value="sangat_sukses" {{ old('success_status', $alumnus->success_status) == 'sangat_sukses' ? 'selected' : '' }}>✓ Sangat Sukses</option>
|
||||
<option value="sukses" {{ old('success_status', $alumnus->success_status) == 'sukses' ? 'selected' : '' }}>✓ Sukses</option>
|
||||
<option value="cukup" {{ old('success_status', $alumnus->success_status) == 'cukup' ? 'selected' : '' }}>• Cukup</option>
|
||||
<option value="kurang_sukses" {{ old('success_status', $alumnus->success_status) == 'kurang_sukses' ? 'selected' : '' }}>✗ Kurang Sukses</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Catatan Tambahan</label>
|
||||
<textarea name="catatan" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm" placeholder="Catatan / observasi tambahan">{{ old('catatan', $alumnus->catatan) }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Buttons -->
|
||||
<div class="flex gap-4 justify-end">
|
||||
<a href="{{ route('alumni.index') }}" class="px-6 py-2 rounded-lg font-bold bg-gray-300 text-gray-700 hover:bg-gray-400 transition">
|
||||
Batal
|
||||
</a>
|
||||
<button type="submit" class="px-6 py-2 rounded-lg font-bold gradient-maroon text-white hover:opacity-90 transition">
|
||||
💾 Update Alumni
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const kelompokSelect = document.querySelector('select[name="kelompok_asal"]');
|
||||
const ipaFields = document.getElementById('ipa-fields');
|
||||
const ipsFields = document.getElementById('ips-fields');
|
||||
|
||||
function updateNilaiFields() {
|
||||
const kelompok = kelompokSelect.value;
|
||||
ipaFields.style.display = kelompok === 'IPA' ? 'contents' : 'none';
|
||||
ipsFields.style.display = kelompok === 'IPS' ? 'contents' : 'none';
|
||||
}
|
||||
|
||||
kelompokSelect.addEventListener('change', updateNilaiFields);
|
||||
updateNilaiFields();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900">Import Data Alumni</h1>
|
||||
<p class="text-gray-600 mt-2">Unggah file Excel berisi data alumni untuk diimport ke database</p>
|
||||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
@if (session('success'))
|
||||
<div class="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-green-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-green-800">{{ session('success') }}</h3>
|
||||
@if (session('errors') && count(session('errors')) > 0)
|
||||
<div class="mt-2 text-sm text-green-700">
|
||||
<p class="font-medium">Detail Error (menampilkan max 10):</p>
|
||||
<ul class="list-disc ml-5 mt-1">
|
||||
@foreach (session('errors') as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Error Message -->
|
||||
@if ($errors->any())
|
||||
<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">Terjadi Error</h3>
|
||||
<ul class="list-disc ml-5 mt-2 text-sm text-red-700">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Upload Form -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<form action="{{ route('alumni.import') }}" method="POST" enctype="multipart/form-data" class="space-y-6">
|
||||
@csrf
|
||||
|
||||
<!-- File Input -->
|
||||
<div>
|
||||
<label for="file" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Pilih File Excel
|
||||
</label>
|
||||
<div class="relative border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-blue-500 hover:bg-blue-50 transition cursor-pointer">
|
||||
<input type="file" id="file" name="file" accept=".xlsx,.xls,.csv" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" required>
|
||||
<div class="pointer-events-none">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
|
||||
<path d="M28 8H12a4 4 0 00-4 4v20a4 4 0 004 4h24a4 4 0 004-4V20m-8-12l-4-4m0 0l-4 4m4-4v12" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<p class="mt-2 text-sm font-medium text-gray-700">
|
||||
<span class="text-blue-600">Klik untuk upload</span> atau drag & drop
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
File format: .xlsx, .xls, .csv (max 10MB)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p id="fileName" class="mt-2 text-sm text-gray-600"></p>
|
||||
</div>
|
||||
|
||||
<!-- Info Box -->
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<h4 class="font-semibold text-blue-900 mb-2">📝 Kolom yang Didukung:</h4>
|
||||
<ul class="text-sm text-blue-800 space-y-1">
|
||||
<li>✓ <strong>Wajib:</strong> Nama Alumni, Kelompok Asal</li>
|
||||
<li>✓ <strong>Optional:</strong> NIS, Matematika, Fisika, Kimia, Biologi, Ekonomi, Geografi, Sosiologi, Sejarah, Minat, Cita-Cita, Preferensi Studi, Prestasi, Jurusan Masuk, Tahun Lulus, Catatan</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="flex gap-4">
|
||||
<button type="submit" class="flex-1 bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition">
|
||||
📤 Upload & Import
|
||||
</button>
|
||||
<a href="{{ route('alumni.index') }}" class="flex-1 bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-2 px-4 rounded-lg transition text-center">
|
||||
Kembali
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Template Download -->
|
||||
<div class="mt-8 bg-gray-50 rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">📋 Template Excel</h3>
|
||||
<p class="text-gray-600 mb-4">Gunakan template berikut sebagai panduan struktur file Excel:</p>
|
||||
|
||||
<div class="bg-white rounded overflow-x-auto border border-gray-200">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left">Nama Alumni</th>
|
||||
<th class="px-4 py-2 text-left">NIS</th>
|
||||
<th class="px-4 py-2 text-left">Kelompok</th>
|
||||
<th class="px-4 py-2 text-left">MTK</th>
|
||||
<th class="px-4 py-2 text-left">Fisika</th>
|
||||
<th class="px-4 py-2 text-left">Jurusan</th>
|
||||
<th class="px-4 py-2 text-left">Tahun</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="border-t">
|
||||
<td class="px-4 py-2">Budi Santoso</td>
|
||||
<td class="px-4 py-2">001</td>
|
||||
<td class="px-4 py-2">IPA</td>
|
||||
<td class="px-4 py-2">85</td>
|
||||
<td class="px-4 py-2">82</td>
|
||||
<td class="px-4 py-2">TIF</td>
|
||||
<td class="px-4 py-2">2024</td>
|
||||
</tr>
|
||||
<tr class="border-t bg-gray-50">
|
||||
<td class="px-4 py-2">Siti Nurhaliza</td>
|
||||
<td class="px-4 py-2">002</td>
|
||||
<td class="px-4 py-2">IPS</td>
|
||||
<td class="px-4 py-2">75</td>
|
||||
<td class="px-4 py-2">65</td>
|
||||
<td class="px-4 py-2">AK</td>
|
||||
<td class="px-4 py-2">2024</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File input change handler -->
|
||||
<script>
|
||||
document.getElementById('file').addEventListener('change', function(e) {
|
||||
const fileName = e.target.files[0]?.name || '';
|
||||
document.getElementById('fileName').textContent = fileName ? `✓ File dipilih: ${fileName}` : '';
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Data Alumni - Sistem Pemilihan Jurusan</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.gradient-maroon {
|
||||
background: linear-gradient(135deg, #5B7B89 0%, #7B9BA5 100%);
|
||||
}
|
||||
.text-maroon {
|
||||
color: #5B7B89;
|
||||
}
|
||||
.border-maroon {
|
||||
border-color: #5B7B89;
|
||||
}
|
||||
.bg-cream {
|
||||
background-color: #F8FAFC;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-cream">
|
||||
<!-- Header -->
|
||||
<header class="gradient-maroon text-white shadow-lg sticky top-0 z-50">
|
||||
<div class="container mx-auto px-4 sm:px-6 py-4 sm:py-6">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 sm:gap-4">
|
||||
<div>
|
||||
<h1 class="text-xl sm:text-2xl md:text-3xl font-bold">Data Alumni</h1>
|
||||
<p class="text-xs sm:text-sm text-yellow-300 font-semibold mt-1">Validasi & Analisis Bobot Algoritma</p>
|
||||
</div>
|
||||
<a href="{{ route('alumni.create') }}" class="bg-yellow-400 text-maroon font-bold py-2 px-4 rounded-lg hover:bg-yellow-300 transition text-xs sm:text-sm text-center">
|
||||
➕ Input Alumni Baru
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="container mx-auto px-4 sm:px-6 py-6 sm:py-12">
|
||||
<!-- Success Alert -->
|
||||
@if(session('success'))
|
||||
<div class="mb-6 p-4 bg-green-100 border-l-4 border-green-500 text-green-700 rounded">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Summary Stats -->
|
||||
@if($summary)
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<div class="bg-white rounded-lg shadow p-6 border-t-4 border-maroon">
|
||||
<p class="text-gray-600 text-sm font-semibold">Total Alumni</p>
|
||||
<p class="text-3xl font-bold text-maroon mt-2">{{ $summary['total'] }}</p>
|
||||
</div>
|
||||
|
||||
@if($summary['prediction_accuracy'])
|
||||
<div class="bg-white rounded-lg shadow p-6 border-t-4 border-yellow-400">
|
||||
<p class="text-gray-600 text-sm font-semibold">Top-1 Accuracy</p>
|
||||
<p class="text-3xl font-bold mt-2" style="color: #EA580C;">{{ $summary['prediction_accuracy']['top_1'] }}%</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow p-6 border-t-4 border-blue-400">
|
||||
<p class="text-gray-600 text-sm font-semibold">Top-3 Accuracy</p>
|
||||
<p class="text-3xl font-bold text-blue-600 mt-2">{{ $summary['prediction_accuracy']['top_3'] }}%</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow p-6 border-t-4 border-green-400">
|
||||
<p class="text-gray-600 text-sm font-semibold">Top-5 Accuracy</p>
|
||||
<p class="text-3xl font-bold text-green-600 mt-2">{{ $summary['prediction_accuracy']['top_5'] }}%</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Distribution by Major -->
|
||||
@if($summary['by_major']->isNotEmpty())
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-8 border-l-4 border-maroon">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">Distribusi Alumni per Jurusan</h3>
|
||||
<div class="space-y-2">
|
||||
@foreach($summary['by_major'] as $major)
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-semibold text-gray-700 w-48">{{ $major->major_masuk }}</span>
|
||||
<div class="flex-1 h-6 bg-gray-200 rounded">
|
||||
<div class="h-full bg-gradient-to-r from-maroon to-yellow-400 rounded flex items-center justify-center text-white text-xs font-bold" style="width: {{ ($major->count / $summary['total']) * 100 }}%">
|
||||
{{ $major->count }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
<!-- Alumni Table -->
|
||||
<div class="bg-white rounded-lg shadow overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="gradient-maroon text-white">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left">Nama</th>
|
||||
<th class="px-4 py-3 text-left">NIS</th>
|
||||
<th class="px-4 py-3 text-center">Kelompok</th>
|
||||
<th class="px-4 py-3 text-center">Nilai Rata</th>
|
||||
<th class="px-4 py-3 text-left">Major</th>
|
||||
<th class="px-4 py-3 text-center">Ranking</th>
|
||||
<th class="px-4 py-3 text-center">Success</th>
|
||||
<th class="px-4 py-3 text-center">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
@forelse($alumni as $a)
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
<td class="px-4 py-3 font-semibold text-gray-800">{{ $a->nama_alumni }}</td>
|
||||
<td class="px-4 py-3 text-gray-600">{{ $a->nis ?? '-' }}</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<span class="px-2 py-1 rounded text-xs font-bold" style="{{ $a->kelompok_asal == 'IPA' ? 'background-color: #E0F2FE; color: #0369A1;' : 'background-color: #FEF3C7; color: #92400E;' }}">
|
||||
{{ $a->kelompok_asal }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center font-bold text-maroon">{{ $a->nilai_rata_rata ? number_format($a->nilai_rata_rata, 2) : '-' }}</td>
|
||||
<td class="px-4 py-3 text-gray-700">{{ $a->major_masuk }}</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
@if($a->ranking_saat_rekomendasi)
|
||||
<span class="px-2 py-1 rounded text-xs font-bold bg-blue-100 text-blue-800">#{{ $a->ranking_saat_rekomendasi }}</span>
|
||||
@else
|
||||
<span class="text-gray-400 text-xs">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center text-xs">
|
||||
@if($a->success_status)
|
||||
@switch($a->success_status)
|
||||
@case('sangat_sukses')
|
||||
<span class="px-2 py-1 rounded bg-green-100 text-green-800 font-bold">✓ Sangat</span>
|
||||
@break
|
||||
@case('sukses')
|
||||
<span class="px-2 py-1 rounded bg-green-50 text-green-700">✓ Sukses</span>
|
||||
@break
|
||||
@case('cukup')
|
||||
<span class="px-2 py-1 rounded bg-yellow-100 text-yellow-800">• Cukup</span>
|
||||
@break
|
||||
@case('kurang_sukses')
|
||||
<span class="px-2 py-1 rounded bg-red-100 text-red-800">✗ Kurang</span>
|
||||
@break
|
||||
@endswitch
|
||||
@else
|
||||
<span class="text-gray-400 text-xs">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center gap-2 flex justify-center">
|
||||
<a href="{{ route('alumni.show', $a->id) }}" class="text-blue-600 hover:text-blue-800 font-semibold text-xs">👁 Lihat</a>
|
||||
<a href="{{ route('alumni.edit', $a->id) }}" class="text-yellow-600 hover:text-yellow-800 font-semibold text-xs">✏ Edit</a>
|
||||
<form action="{{ route('alumni.destroy', $a->id) }}" method="POST" class="inline" onsubmit="return confirm('Yakin hapus?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="text-red-600 hover:text-red-800 font-semibold text-xs">🗑 Hapus</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8" class="px-4 py-6 text-center text-gray-500">
|
||||
Belum ada data alumni. <a href="{{ route('alumni.create') }}" class="text-maroon font-bold">Tambah sekarang</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="mt-6">
|
||||
{{ $alumni->links() }}
|
||||
</div>
|
||||
|
||||
<!-- Info Box -->
|
||||
<div class="mt-8 p-4 bg-blue-50 border-l-4 border-blue-400 rounded">
|
||||
<p class="text-sm text-blue-800">
|
||||
<strong>📊 Data Alumni digunakan untuk:</strong><br>
|
||||
1. Validasi akurasi algoritma Naive Bayes<br>
|
||||
2. Analisis faktor-faktor mana yang paling berpengaruh terhadap kesuksesan<br>
|
||||
3. Re-weighting: menyesuaikan bobot jika data menunjukkan faktor lain lebih penting
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,229 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Detail Alumni - {{ $alumnus->nama_alumni }}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.gradient-maroon {
|
||||
background: linear-gradient(135deg, #5B7B89 0%, #7B9BA5 100%);
|
||||
}
|
||||
.text-maroon {
|
||||
color: #5B7B89;
|
||||
}
|
||||
.bg-cream {
|
||||
background-color: #F8FAFC;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-cream">
|
||||
<!-- Header -->
|
||||
<header class="gradient-maroon text-white shadow-lg sticky top-0 z-50">
|
||||
<div class="container mx-auto px-4 sm:px-6 py-4 sm:py-6 flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-xl sm:text-2xl md:text-3xl font-bold">Detail Alumni</h1>
|
||||
<p class="text-xs sm:text-sm text-yellow-300 font-semibold mt-1">{{ $alumnus->nama_alumni }}</p>
|
||||
</div>
|
||||
<a href="{{ route('alumni.index') }}" class="bg-yellow-400 text-maroon font-bold py-2 px-4 rounded-lg hover:bg-yellow-300 transition text-xs sm:text-sm">
|
||||
← Kembali
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="container mx-auto px-4 sm:px-6 py-6 sm:py-12">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<!-- Profile Card -->
|
||||
<div class="bg-white rounded-lg shadow-lg p-6 mb-6 border-l-4 border-maroon">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Nama</p>
|
||||
<p class="text-xl font-bold text-maroon">{{ $alumnus->nama_alumni }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">NIS</p>
|
||||
<p class="text-lg font-semibold text-gray-800">{{ $alumnus->nis ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Kelompok Asal</p>
|
||||
<p class="text-lg font-semibold">
|
||||
<span class="px-3 py-1 rounded text-sm font-bold" style="{{ $alumnus->kelompok_asal == 'IPA' ? 'background-color: #E0F2FE; color: #0369A1;' : 'background-color: #FEF3C7; color: #92400E;' }}">
|
||||
{{ $alumnus->kelompok_asal }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Tahun Masuk</p>
|
||||
<p class="text-lg font-semibold text-gray-800">{{ $alumnus->tahun_masuk }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nilai & Variabel Input -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-yellow-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📊 Input Data Saat Entry</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
@if($alumnus->mtk)
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Matematika</p>
|
||||
<p class="text-lg font-bold text-maroon">{{ $alumnus->mtk }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@if($alumnus->fisika)
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Fisika</p>
|
||||
<p class="text-lg font-bold">{{ $alumnus->fisika }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@if($alumnus->kimia)
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Kimia</p>
|
||||
<p class="text-lg font-bold">{{ $alumnus->kimia }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@if($alumnus->biologi)
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Biologi</p>
|
||||
<p class="text-lg font-bold">{{ $alumnus->biologi }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@if($alumnus->ekonomi)
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Ekonomi</p>
|
||||
<p class="text-lg font-bold">{{ $alumnus->ekonomi }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@if($alumnus->geografi)
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Geografi</p>
|
||||
<p class="text-lg font-bold">{{ $alumnus->geografi }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@if($alumnus->sosiologi)
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Sosiologi</p>
|
||||
<p class="text-lg font-bold">{{ $alumnus->sosiologi }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@if($alumnus->sejarah)
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Sejarah</p>
|
||||
<p class="text-lg font-bold">{{ $alumnus->sejarah }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="mt-4 pt-4 border-t">
|
||||
<p class="text-gray-600 text-sm">Nilai Rata-rata</p>
|
||||
<p class="text-2xl font-bold text-maroon">{{ $alumnus->nilai_rata_rata ? number_format($alumnus->nilai_rata_rata, 2) : '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Non-Akademik -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-blue-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">💡 Variabel Non-Akademik</h3>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Minat</p>
|
||||
<p class="text-gray-800 font-semibold">{{ $alumnus->minat ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Cita-cita</p>
|
||||
<p class="text-gray-800 font-semibold">{{ $alumnus->cita_cita ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Preferensi Studi</p>
|
||||
<p class="text-gray-800 font-semibold">{{ $alumnus->preferensi_studi ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Prestasi</p>
|
||||
<p class="text-gray-800 font-semibold">{{ $alumnus->prestasi ?? '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hasil & Outcome -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-green-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">🎯 Hasil Rekomendasi & Outcome</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-4">
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Jurusan Masuk</p>
|
||||
<p class="text-lg font-bold text-maroon">{{ $alumnus->major_masuk }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Ranking Saat Rekomendasi</p>
|
||||
@if($alumnus->ranking_saat_rekomendasi)
|
||||
<p class="text-lg font-bold"><span class="px-3 py-1 rounded bg-blue-100 text-blue-800">{{ $alumnus->ranking_saat_rekomendasi }} / 9</span></p>
|
||||
@else
|
||||
<p class="text-gray-500">-</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Score Prediksi</p>
|
||||
<p class="text-lg font-bold">{{ $alumnus->predicted_score ? round($alumnus->predicted_score * 100) . '%' : '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Outcome Alumni -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-purple-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">🎓 Outcome Alumni</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">IPK Lulus</p>
|
||||
<p class="text-lg font-bold text-maroon">{{ $alumnus->ipk_lulus ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Tahun Lulus</p>
|
||||
<p class="text-lg font-bold">{{ $alumnus->tahun_lulus ?? '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Status Kesuksesan</p>
|
||||
@if($alumnus->success_status)
|
||||
@switch($alumnus->success_status)
|
||||
@case('sangat_sukses')
|
||||
<span class="inline-block px-3 py-1 rounded bg-green-100 text-green-800 font-bold">✓ Sangat Sukses</span>
|
||||
@break
|
||||
@case('sukses')
|
||||
<span class="inline-block px-3 py-1 rounded bg-green-50 text-green-700">✓ Sukses</span>
|
||||
@break
|
||||
@case('cukup')
|
||||
<span class="inline-block px-3 py-1 rounded bg-yellow-100 text-yellow-800">• Cukup</span>
|
||||
@break
|
||||
@case('kurang_sukses')
|
||||
<span class="inline-block px-3 py-1 rounded bg-red-100 text-red-800">✗ Kurang Sukses</span>
|
||||
@break
|
||||
@endswitch
|
||||
@else
|
||||
<p class="text-gray-500">-</p>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Karir Outcome</p>
|
||||
<p class="text-gray-800">{{ $alumnus->karir_outcome ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Catatan</p>
|
||||
<p class="text-gray-800">{{ $alumnus->catatan ?? '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex gap-4 justify-end">
|
||||
<a href="{{ route('alumni.edit', $alumnus->id) }}" class="px-6 py-2 rounded-lg font-bold bg-yellow-400 text-maroon hover:bg-yellow-300 transition">
|
||||
✏ Edit
|
||||
</a>
|
||||
<form action="{{ route('alumni.destroy', $alumnus->id) }}" method="POST" class="inline" onsubmit="return confirm('Yakin hapus alumni ini?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="px-6 py-2 rounded-lg font-bold bg-red-500 text-white hover:bg-red-600 transition">
|
||||
🗑 Hapus
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,25 +1,362 @@
|
|||
<x-guest-layout>
|
||||
<div class="mb-4 text-sm text-gray-600">
|
||||
{{ __('Lupa password Anda? Tidak apa-apa. Beri tahu kami alamat email Anda dan kami akan mengirimkan tautan pengaturan ulang password yang akan memungkinkan Anda memilih yang baru.') }}
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Lupa Password - SPK Jurusan Polije</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.gradient-purple {
|
||||
background: linear-gradient(135deg, #9333ea 0%, #6366f1 100%);
|
||||
}
|
||||
.bg-cream {
|
||||
background-color: #F8FAFC;
|
||||
}
|
||||
@keyframes pulse-soft {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.85; }
|
||||
}
|
||||
.animate-pulse-soft {
|
||||
animation: pulse-soft 3s ease-in-out infinite;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-cream">
|
||||
<div class="min-h-screen flex items-center justify-center px-4 py-8">
|
||||
<div class="w-full max-w-md">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-purple-100 mb-4">
|
||||
<span class="text-3xl">🔐</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-gray-900 mb-2">Lupa Password?</h1>
|
||||
<p class="text-gray-600 text-sm sm:text-base">Tenang, kami bantu kamu atur ulang password dengan mudah.</p>
|
||||
</div>
|
||||
|
||||
<!-- Info Box -->
|
||||
<div class="bg-purple-50 border-l-4 border-purple-600 rounded-lg p-4 mb-6">
|
||||
<p class="text-sm text-purple-900">
|
||||
Masukkan email kamu yang terdaftar, dan kami akan mengirimkan tautan untuk mengatur ulang password ke inbox kamu.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Session Status / Success Message -->
|
||||
@if (session('status'))
|
||||
<div class="mb-6 bg-gradient-to-r from-green-50 to-emerald-50 border-l-4 border-green-500 rounded-lg p-5 shadow-lg border-r border-green-300 animate-pulse-soft">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="text-2xl flex-shrink-0 mt-0.5">✅</span>
|
||||
<div>
|
||||
<h4 class="text-green-900 font-bold text-base">Berhasil!</h4>
|
||||
<p class="text-green-800 text-sm mt-1 leading-relaxed">{{ session('status') }}</p>
|
||||
<p class="text-green-700 text-xs mt-2 font-semibold">📧 Cek email Anda untuk tautan reset password (cek juga folder spam/promotions)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Show token input panel immediately after successful send -->
|
||||
@php
|
||||
$prefillCode = '';
|
||||
$emailForCode = old('email') ?? request()->query('email') ?? request()->input('email');
|
||||
if (!$emailForCode) {
|
||||
// try to retrieve email from old input stored in session
|
||||
$old = session()->get('_old_input', []);
|
||||
if (!empty($old['email'])) $emailForCode = $old['email'];
|
||||
}
|
||||
if ($emailForCode) {
|
||||
try {
|
||||
$expiryMinutes = config('auth.passwords.users.expire', 60);
|
||||
$cutoff = \Carbon\Carbon::now()->subMinutes($expiryMinutes);
|
||||
$row = \Illuminate\Support\Facades\DB::table('password_reset_codes')
|
||||
->where('email', $emailForCode)
|
||||
->where('created_at', '>=', $cutoff)
|
||||
->first();
|
||||
if ($row) $prefillCode = $row->code;
|
||||
} catch (\Throwable $e) {
|
||||
$prefillCode = '';
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div id="postSendTokenPanel" class="mb-6 bg-white rounded-xl shadow-lg p-6 border-t-4 border-gray-200">
|
||||
<h4 class="text-sm font-bold text-gray-700 mb-2">Sudah menerima kode 6-digit?</h4>
|
||||
<p class="text-xs text-gray-500 mb-3">Jika sudah, masukkan kode di bawah agar Anda bisa langsung mengganti password di halaman ini.</p>
|
||||
|
||||
<form method="POST" action="{{ route('password.reset.with_code') }}" onsubmit="return handleTokenSubmit(event)">
|
||||
@csrf
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Email</label>
|
||||
<input type="email" name="email" id="post_token_email" value="{{ old('email') ?? request()->query('email', '') }}" required class="w-full px-3 py-2 border rounded-md text-sm focus:ring-2 focus:ring-purple-200 focus:border-purple-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Kode 6-digit</label>
|
||||
<input type="text" name="token" id="post_token_input" inputmode="numeric" pattern="\d{6}" placeholder="123456" value="{{ $prefillCode }}" required class="w-full px-3 py-2 border rounded-md text-sm focus:ring-2 focus:ring-purple-200 focus:border-purple-500" />
|
||||
</div>
|
||||
|
||||
<div id="passwordFields" class="hidden">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Password Baru</label>
|
||||
<input type="password" name="password" id="post_token_password" class="w-full px-3 py-2 border rounded-md text-sm focus:ring-2 focus:ring-purple-200 focus:border-purple-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Konfirmasi Password</label>
|
||||
<input type="password" name="password_confirmation" id="post_token_password_confirmation" class="w-full px-3 py-2 border rounded-md text-sm focus:ring-2 focus:ring-purple-200 focus:border-purple-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex gap-2">
|
||||
<button type="button" id="verifyCodeBtn" onclick="verifyCode()" class="inline-flex items-center gap-2 bg-gray-200 text-gray-800 text-sm px-4 py-2 rounded-md font-semibold">🔎 Verifikasi Kode</button>
|
||||
<button type="submit" id="postTokenSubmitBtn" disabled class="inline-flex items-center gap-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white text-sm px-4 py-2 rounded-md font-semibold opacity-60 cursor-not-allowed">🔒 Reset dengan Kode</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Error Messages -->
|
||||
@if ($errors->any())
|
||||
<div class="mb-6 bg-gradient-to-r from-red-50 to-pink-50 border-l-4 border-red-500 rounded-lg p-5 shadow-lg border-r border-red-300">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="text-2xl flex-shrink-0 mt-0.5">❌</span>
|
||||
<div>
|
||||
<h4 class="text-red-900 font-bold text-base">Gagal!</h4>
|
||||
<ul class="text-red-800 text-sm mt-2 space-y-1.5">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="text-red-500 mt-0.5">•</span>
|
||||
<span>{{ $error }}</span>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Form -->
|
||||
<form method="POST" action="{{ route('password.email') }}" class="bg-white rounded-xl shadow-lg p-8 border-t-4 border-purple-600" onsubmit="handleForgotSubmit(event)">
|
||||
@csrf
|
||||
|
||||
<!-- Email Address -->
|
||||
<div class="mb-6">
|
||||
<label for="email" class="block text-sm font-semibold text-gray-700 mb-2">📧 Email</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
value="{{ old('email') ?? request()->query('email', '') }}"
|
||||
required
|
||||
autofocus
|
||||
placeholder="masukkan email kamu"
|
||||
class="w-full px-4 py-3 border-2 border-gray-300 rounded-lg focus:outline-none focus:border-purple-600 focus:ring-2 focus:ring-purple-200 transition text-sm @error('email') border-red-500 @enderror"
|
||||
/>
|
||||
@error('email')
|
||||
<span class="text-red-500 text-sm mt-1 block">⚠️ {{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button
|
||||
id="submitBtn"
|
||||
type="submit"
|
||||
class="w-full bg-gradient-to-r from-purple-600 to-indigo-600 text-white font-bold py-3 px-4 rounded-lg hover:shadow-lg active:scale-95 transition text-base mb-4 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
🔗 Kirim Tautan Reset Password
|
||||
</button>
|
||||
|
||||
<!-- Back to Login -->
|
||||
<div class="text-center">
|
||||
<p class="text-gray-600 text-sm">
|
||||
Ingat passwordnya?
|
||||
<a href="{{ route('login') }}" class="text-purple-600 hover:text-purple-700 font-semibold">Kembali ke Login</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Footer Info -->
|
||||
<div class="text-center mt-6 text-xs text-gray-500">
|
||||
<p>💡 Jika email tidak masuk, cek folder spam kamu.</p>
|
||||
</div>
|
||||
|
||||
<!-- (Token panel removed) -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="mb-4" :status="session('status')" />
|
||||
<script>
|
||||
function handleForgotSubmit(event) {
|
||||
const email = document.getElementById('email').value;
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
|
||||
<form method="POST" action="{{ route('password.email') }}">
|
||||
@csrf
|
||||
// Validasi email
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
event.preventDefault();
|
||||
alert('❌ Email tidak valid! Gunakan format yang benar (contoh: nama@gmail.com)');
|
||||
return false;
|
||||
}
|
||||
|
||||
<!-- Email Address -->
|
||||
<div>
|
||||
<x-input-label for="email" :value="__('Email')" />
|
||||
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus />
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||
</div>
|
||||
// Save email locally so we can prefill the post-send token panel
|
||||
try { localStorage.setItem('password_reset_email', email); } catch(e){}
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<x-primary-button>
|
||||
{{ __('Kirim Tautan Atur Ulang Password') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-guest-layout>
|
||||
// Disable button dan tampilkan loading state
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '⏳ Sedang Mengirim...';
|
||||
submitBtn.style.opacity = '0.7';
|
||||
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
function handleTokenSubmit(event) {
|
||||
const email = document.getElementById('post_token_email').value.trim();
|
||||
const token = document.getElementById('post_token_input').value.trim();
|
||||
const pass = document.getElementById('post_token_password').value;
|
||||
const pass2 = document.getElementById('post_token_password_confirmation').value;
|
||||
const submitBtn = document.getElementById('postTokenSubmitBtn');
|
||||
|
||||
if (!email || !token || !pass) {
|
||||
alert('Isi semua field: email, kode, dan password.');
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!/^\d{6}$/.test(token)) {
|
||||
alert('Kode harus 6 digit angka.');
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pass.length < 8) {
|
||||
alert('Password minimal 8 karakter.');
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pass !== pass2) {
|
||||
alert('Konfirmasi password tidak cocok.');
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '⏳ Memproses...';
|
||||
return true;
|
||||
}
|
||||
|
||||
// autofocus token input when panel is present
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
try {
|
||||
const panel = document.getElementById('postSendTokenPanel');
|
||||
// if there's a saved email from before submit, prefill and lock the email field
|
||||
const savedEmail = (function(){ try{ return localStorage.getItem('password_reset_email') }catch(e){return null} })();
|
||||
const emailInput = document.getElementById('post_token_email');
|
||||
if (savedEmail && emailInput) {
|
||||
emailInput.value = savedEmail;
|
||||
emailInput.readOnly = true;
|
||||
emailInput.classList.add('bg-gray-100');
|
||||
}
|
||||
if (panel) {
|
||||
const t = document.getElementById('post_token_input');
|
||||
if (t) t.focus();
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
async function verifyCode() {
|
||||
const email = document.getElementById('post_token_email').value.trim();
|
||||
const token = document.getElementById('post_token_input').value.trim();
|
||||
const verifyBtn = document.getElementById('verifyCodeBtn');
|
||||
const submitBtn = document.getElementById('postTokenSubmitBtn');
|
||||
const passwordFields = document.getElementById('passwordFields');
|
||||
|
||||
if (!email || !/^\d{6}$/.test(token)) {
|
||||
alert('Masukkan email dan kode 6 digit yang valid terlebih dahulu.');
|
||||
return;
|
||||
}
|
||||
|
||||
verifyBtn.disabled = true;
|
||||
verifyBtn.textContent = '⏳ Memeriksa...';
|
||||
|
||||
try {
|
||||
const resp = await fetch('{{ route('password.verify.code') }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('input[name="_token"]').value,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({email: email, token: token})
|
||||
});
|
||||
|
||||
const data = await resp.json();
|
||||
if (resp.ok && data.ok) {
|
||||
// reveal password fields and enable submit
|
||||
passwordFields.classList.remove('hidden');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.classList.remove('opacity-60','cursor-not-allowed');
|
||||
verifyBtn.textContent = '✔️ Kode Valid';
|
||||
verifyBtn.classList.add('bg-green-100');
|
||||
// focus password
|
||||
setTimeout(()=>{ document.getElementById('post_token_password').focus(); }, 50);
|
||||
// clear stored email after successful verification
|
||||
try { localStorage.removeItem('password_reset_email'); } catch(e){}
|
||||
} else {
|
||||
verifyBtn.disabled = false;
|
||||
verifyBtn.textContent = '🔎 Verifikasi Kode';
|
||||
alert(data.message || (data.errors || ['Token tidak valid'])[0]);
|
||||
}
|
||||
} catch (err) {
|
||||
verifyBtn.disabled = false;
|
||||
verifyBtn.textContent = '🔎 Verifikasi Kode';
|
||||
alert('Terjadi kesalahan saat memverifikasi kode.');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
function toggleTokenForm(e) {
|
||||
e.preventDefault();
|
||||
const panel = document.getElementById('tokenForm');
|
||||
panel.classList.toggle('hidden');
|
||||
const btn = document.getElementById('toggleTokenForm');
|
||||
btn.textContent = panel.classList.contains('hidden') ? 'Gunakan token' : 'Sembunyikan';
|
||||
}
|
||||
|
||||
function hideTokenForm() {
|
||||
const panel = document.getElementById('tokenForm');
|
||||
panel.classList.add('hidden');
|
||||
const btn = document.getElementById('toggleTokenForm');
|
||||
btn.textContent = 'Gunakan token';
|
||||
}
|
||||
|
||||
function handleTokenSubmit(event) {
|
||||
const email = document.getElementById('token_email').value.trim();
|
||||
const token = document.getElementById('token_input').value.trim();
|
||||
const pass = document.getElementById('token_password').value;
|
||||
const pass2 = document.getElementById('token_password_confirmation').value;
|
||||
const submitBtn = document.getElementById('tokenSubmitBtn');
|
||||
|
||||
if (!email || !token || !pass) {
|
||||
alert('Isi semua field token, email, dan password.');
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pass.length < 8) {
|
||||
alert('Password minimal 8 karakter.');
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pass !== pass2) {
|
||||
alert('Konfirmasi password tidak cocok.');
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '⏳ Memproses...';
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -13,59 +13,70 @@
|
|||
}
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container-wrapper {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
background-color: #5B7B89;
|
||||
background-color: #6B7280;
|
||||
}
|
||||
.left-section {
|
||||
width: 50%;
|
||||
background-color: #5B7B89;
|
||||
width: 100%;
|
||||
background-color: #6B7280;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
padding: 20px;
|
||||
min-height: 200px;
|
||||
order: -1;
|
||||
}
|
||||
.image-placeholder {
|
||||
text-align: center;
|
||||
}
|
||||
.image-area {
|
||||
width: 240px;
|
||||
height: 300px;
|
||||
width: 180px;
|
||||
height: 240px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border: 3px dashed #FCD34D;
|
||||
border: 2px dashed #C9A961;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 30px;
|
||||
color: #FCD34D;
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
color: #C9A961;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
padding: 15px;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
}
|
||||
.image-area img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.image-area span {
|
||||
font-size: 48px;
|
||||
font-size: 40px;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.brand-info h1 {
|
||||
font-size: 50px;
|
||||
font-size: 32px;
|
||||
font-weight: 900;
|
||||
color: #FFFFFF;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 3px;
|
||||
margin-bottom: 6px;
|
||||
letter-spacing: 2px;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.brand-info p {
|
||||
font-size: 22px;
|
||||
color: #FCD34D;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
}100%;
|
||||
background-color: #FFFFFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
flex: 1g: 0.5px;
|
||||
}
|
||||
.right-section {
|
||||
width: 50%;
|
||||
|
|
@ -80,18 +91,18 @@
|
|||
width: 100%;
|
||||
max-width: 420px;
|
||||
}
|
||||
.form-title {
|
||||
font-size: 48px;
|
||||
.form-title {32px;
|
||||
font-weight: 800;
|
||||
color: #111827;
|
||||
text-align: center;
|
||||
margin-bottom: 12px;
|
||||
letter-spacing: -1px;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
.form-subtitle {
|
||||
text-align: center;
|
||||
color: #6B7280;
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 1
|
||||
margin-bottom: 28px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
|
@ -179,12 +190,75 @@
|
|||
margin: 4px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
/* Tablet and Desktop */
|
||||
@media (min-width: 768px) {
|
||||
.container-wrapper {
|
||||
flex-direction: row;
|
||||
}
|
||||
.left-section {
|
||||
display: none;
|
||||
width: 50%;
|
||||
min-height: auto;
|
||||
padding: 40px;
|
||||
order: 0;
|
||||
}
|
||||
.image-area {
|
||||
width: 240px;
|
||||
height: 300px;
|
||||
font-size: 16px;
|
||||
padding: 0;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.image-area span {
|
||||
font-size: 48px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.brand-info h1 {
|
||||
font-size: 50px;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
.brand-info p {
|
||||
font-size: 22px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.right-section {
|
||||
width: 100%;
|
||||
width: 50%;
|
||||
padding: 40px;
|
||||
}
|
||||
.form-title {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
.form-subtitle {
|
||||
font-size: 16px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-group label {
|
||||
font-size: 15px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
padding: 13px 15px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.form-links {
|
||||
margin: 18px 0 24px 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
.btn-submit {
|
||||
padding: 15px 20px;
|
||||
font-size: 16px;
|
||||
letter-spacing: 1.5px;
|
||||
}
|
||||
.error-alert {
|
||||
padding: 14px 16px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -195,8 +269,7 @@
|
|||
<div class="left-section">
|
||||
<div class="image-placeholder">
|
||||
<div class="image-area">
|
||||
<span>🖼️</span>
|
||||
Ganti dengan Gambar Anda
|
||||
<img src="{{ asset('images/SMA%20BIMA.jpg') }}" alt="SMA BIMA" />
|
||||
</div>
|
||||
<div class="brand-info">
|
||||
<h1>POLIJE</h1>
|
||||
|
|
@ -208,7 +281,7 @@
|
|||
<!-- RIGHT SECTION - LOGIN FORM -->
|
||||
<div class="right-section">
|
||||
<div class="form-wrapper">
|
||||
<h2 class="form-title">WELCOME!</h2>
|
||||
<h2 class="form-title">Selamat Datang</h2>
|
||||
<p class="form-subtitle">Masuk untuk melanjutkan</p>
|
||||
|
||||
@if ($errors->any())
|
||||
|
|
|
|||
|
|
@ -19,11 +19,11 @@
|
|||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
background-color: #5B7B89;
|
||||
background-color: #6B7280;
|
||||
}
|
||||
.left-section {
|
||||
width: 100%;
|
||||
background-color: #5B7B89;
|
||||
background-color: #6B7280;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -38,18 +38,25 @@
|
|||
width: 180px;
|
||||
height: 240px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border: 2px dashed #FCD34D;
|
||||
border: 2px dashed #C9A961;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
color: #FCD34D;
|
||||
color: #C9A961;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
padding: 0;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
}
|
||||
.image-area img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.image-area span {
|
||||
font-size: 40px;
|
||||
|
|
@ -66,7 +73,7 @@
|
|||
}
|
||||
.brand-info p {
|
||||
font-size: 16px;
|
||||
color: #FCD34D;
|
||||
color: #C9A961;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
|
@ -184,6 +191,78 @@
|
|||
margin: 3px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
/* Forgot Password Lock Alert */
|
||||
.forgot-password-alert {
|
||||
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
|
||||
border: 3px solid #dc2626;
|
||||
border-radius: 12px;
|
||||
padding: 18px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 4px 15px rgba(220, 38, 38, 0.2);
|
||||
}
|
||||
.forgot-password-alert .alert-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.forgot-password-alert .alert-header span {
|
||||
font-size: 24px;
|
||||
}
|
||||
.forgot-password-alert .alert-header h3 {
|
||||
color: #7f1d1d;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
.forgot-password-alert .alert-message {
|
||||
color: #991b1b;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.forgot-password-alert .alert-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-direction: column;
|
||||
}
|
||||
.forgot-password-alert .btn-forgot {
|
||||
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||
color: white;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 14px;
|
||||
}
|
||||
.forgot-password-alert .btn-forgot:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.4);
|
||||
}
|
||||
.forgot-password-alert .btn-retry {
|
||||
background-color: #f3f4f6;
|
||||
color: #1f2937;
|
||||
padding: 10px 16px;
|
||||
border: 2px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 14px;
|
||||
}
|
||||
.forgot-password-alert .btn-retry:hover {
|
||||
background-color: #e5e7eb;
|
||||
border-color: #9ca3af;
|
||||
}
|
||||
/* Tablet and Desktop */
|
||||
@media (min-width: 768px) {
|
||||
.container-wrapper {
|
||||
|
|
@ -199,7 +278,7 @@
|
|||
width: 240px;
|
||||
height: 300px;
|
||||
font-size: 16px;
|
||||
padding: 20px;
|
||||
padding: 0;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.image-area span {
|
||||
|
|
@ -257,6 +336,23 @@
|
|||
font-size: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.forgot-password-alert {
|
||||
padding: 20px;
|
||||
margin-bottom: 24px;
|
||||
border: 3px solid #dc2626;
|
||||
}
|
||||
.forgot-password-alert .alert-header h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
.forgot-password-alert .alert-message {
|
||||
font-size: 15px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.forgot-password-alert .btn-forgot,
|
||||
.forgot-password-alert .btn-retry {
|
||||
padding: 12px 18px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
|
@ -266,8 +362,7 @@
|
|||
<div class="left-section">
|
||||
<div class="image-placeholder">
|
||||
<div class="image-area">
|
||||
<span>🖼️</span>
|
||||
Sistem Pemilihan Jurusan
|
||||
<img src="{{ asset('images/SMA%20BIMA.jpg') }}" alt="SMA BIMA" />
|
||||
</div>
|
||||
<div class="brand-info">
|
||||
<h1>POLIJE</h1>
|
||||
|
|
@ -282,7 +377,27 @@
|
|||
<h2 class="form-title">Selamat Datang</h2>
|
||||
<p class="form-subtitle">Masuk untuk melanjutkan</p>
|
||||
|
||||
@if ($errors->any())
|
||||
{{-- Special Alert: Siswa Forgot Password Lock --}}
|
||||
@if ($errors->has('forgot_password') && $errors->has('email'))
|
||||
<div class="forgot-password-alert">
|
||||
<div class="alert-header">
|
||||
<span>🔒</span>
|
||||
<h3>Akun Terkunci Sementara</h3>
|
||||
</div>
|
||||
<div class="alert-message">
|
||||
{{ $errors->first('email') }}
|
||||
</div>
|
||||
<div class="alert-buttons">
|
||||
<a href="{{ route('password.request') }}?email={{ old('email') }}" class="btn-forgot">
|
||||
🔑 Reset Password Sekarang
|
||||
</a>
|
||||
<button type="reset" class="btn-retry" onclick="document.getElementById('email').value=''; document.getElementById('password').value=''; document.getElementById('email').focus();">
|
||||
← Coba Email Lain
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@elseif ($errors->any())
|
||||
{{-- Regular Error Messages --}}
|
||||
<div class="error-alert">
|
||||
@foreach ($errors->all() as $error)
|
||||
<p>• {{ $error }}</p>
|
||||
|
|
@ -290,6 +405,20 @@
|
|||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Success Message: Password Reset Successfully --}}
|
||||
@if (session('status'))
|
||||
<div class="success-alert" style="background-color: #ECFDF5; border: 2px solid #A7F3D0; color: #065F46; padding: 12px 14px; border-radius: 8px; margin-bottom: 18px; font-size: 13px; font-weight: 500;">
|
||||
<div style="display: flex; align-items: flex-start; gap: 10px;">
|
||||
<span style="font-size: 20px;">✅</span>
|
||||
<div>
|
||||
<p style="margin: 0; font-weight: 700;">Password Berhasil Direset!</p>
|
||||
<p style="margin: 5px 0 0 0; line-height: 1.5;">{{ session('status') }}</p>
|
||||
<p style="margin: 8px 0 0 0; font-size: 12px; font-weight: 600;">Silakan login dengan password baru Anda.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('login') }}">
|
||||
@csrf
|
||||
|
||||
|
|
|
|||
|
|
@ -19,11 +19,11 @@
|
|||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
background-color: #5B7B89;
|
||||
background-color: #6B7280;
|
||||
}
|
||||
.left-section {
|
||||
width: 100%;
|
||||
background-color: #5B7B89;
|
||||
background-color: #6B7280;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -38,18 +38,25 @@
|
|||
width: 180px;
|
||||
height: 240px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border: 2px dashed #FCD34D;
|
||||
border: 2px dashed #C9A961;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
color: #FCD34D;
|
||||
color: #C9A961;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
padding: 0;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
}
|
||||
.image-area img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.image-area span {
|
||||
font-size: 40px;
|
||||
|
|
@ -66,7 +73,7 @@
|
|||
}
|
||||
.brand-info p {
|
||||
font-size: 16px;
|
||||
color: #FCD34D;
|
||||
color: #C9A961;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
|
@ -211,7 +218,7 @@
|
|||
width: 240px;
|
||||
height: 300px;
|
||||
font-size: 16px;
|
||||
padding: 20px;
|
||||
padding: 0;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.image-area span {
|
||||
|
|
@ -275,8 +282,7 @@
|
|||
<div class="left-section">
|
||||
<div class="image-placeholder">
|
||||
<div class="image-area">
|
||||
<span>🖼️</span>
|
||||
Sistem Pemilihan Jurusan
|
||||
<img src="{{ asset('images/SMA%20BIMA.jpg') }}" alt="SMA BIMA" />
|
||||
</div>
|
||||
<div class="brand-info">
|
||||
<h1>POLIJE</h1>
|
||||
|
|
|
|||
|
|
@ -1,51 +1,249 @@
|
|||
<x-guest-layout>
|
||||
<form method="POST" action="{{ route('password.store') }}">
|
||||
@csrf
|
||||
|
||||
<!-- Password Reset Token -->
|
||||
<input type="hidden" name="token" value="{{ $request->route('token') }}">
|
||||
|
||||
<!-- Email Address -->
|
||||
<div>
|
||||
<x-input-label for="email" :value="__('Email')" />
|
||||
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required autofocus autocomplete="username" />
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password" :value="__('Password')" />
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input id="password" class="block mt-1 w-full border border-gray-300 rounded-lg shadow-sm focus:border-indigo-500 focus:ring-indigo-500" type="password" name="password" required autocomplete="new-password" placeholder="Enter new password" style="padding-right: 45px;" />
|
||||
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('password', this)">👁️</button>
|
||||
</div>
|
||||
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input id="password_confirmation" class="block mt-1 w-full border border-gray-300 rounded-lg shadow-sm focus:border-indigo-500 focus:ring-indigo-500" type="password" name="password_confirmation" required autocomplete="new-password" placeholder="Confirm password" style="padding-right: 45px;" />
|
||||
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('password_confirmation', this)">👁️</button>
|
||||
</div>
|
||||
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<x-primary-button>
|
||||
{{ __('Atur Ulang Password') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function togglePasswordVisibility(inputId, buttonElement) {
|
||||
const input = document.getElementById(inputId);
|
||||
const isPassword = input.type === 'password';
|
||||
input.type = isPassword ? 'text' : 'password';
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Atur Ulang Password - SPK Jurusan Polije</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.gradient-purple {
|
||||
background: linear-gradient(135deg, #9333ea 0%, #6366f1 100%);
|
||||
}
|
||||
.bg-cream {
|
||||
background-color: #F8FAFC;
|
||||
}
|
||||
@keyframes pulse-soft {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.85; }
|
||||
}
|
||||
.animate-pulse-soft {
|
||||
animation: pulse-soft 3s ease-in-out infinite;
|
||||
}
|
||||
@keyframes slide-down {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
</x-guest-layout>
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.animate-slide-down {
|
||||
animation: slide-down 0.3s ease-out;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-cream">
|
||||
<div class="min-h-screen flex items-center justify-center px-4 py-8">
|
||||
<div class="w-full max-w-md">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-green-100 mb-4">
|
||||
<span class="text-3xl">🔄</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-gray-900 mb-2">Atur Ulang Password</h1>
|
||||
<p class="text-gray-600 text-sm sm:text-base">Buat password baru yang kuat untuk akun kamu.</p>
|
||||
</div>
|
||||
|
||||
<!-- Info Box -->
|
||||
<div class="bg-green-50 border-l-4 border-green-600 rounded-lg p-4 mb-6">
|
||||
<p class="text-sm text-green-900">
|
||||
Pilih password yang kuat (minimal 8 karakter) dan pastikan kamu ingat untuk login di lain waktu.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Success Message (if redirected back with message) -->
|
||||
@if (session('status'))
|
||||
<div class="mb-6 bg-gradient-to-r from-green-50 to-emerald-50 border-l-4 border-green-500 rounded-lg p-5 shadow-lg border-r border-green-300 animate-slide-down">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="text-2xl flex-shrink-0 mt-0.5">✅</span>
|
||||
<div>
|
||||
<h4 class="text-green-900 font-bold text-base">Berhasil!</h4>
|
||||
<p class="text-green-800 text-sm mt-1 leading-relaxed">{{ session('status') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Error Messages -->
|
||||
@if ($errors->any())
|
||||
<div class="mb-6 bg-gradient-to-r from-red-50 to-pink-50 border-l-4 border-red-500 rounded-lg p-5 shadow-lg border-r border-red-300 animate-slide-down">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="text-2xl flex-shrink-0 mt-0.5">❌</span>
|
||||
<div>
|
||||
<h4 class="text-red-900 font-bold text-base">Ada Kesalahan!</h4>
|
||||
<ul class="text-red-800 text-sm mt-2 space-y-1.5">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="text-red-500 mt-0.5">•</span>
|
||||
<span>{{ $error }}</span>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
|
||||
@if($errors->has('token'))
|
||||
<div class="mt-4 pt-3 border-t border-red-200">
|
||||
<p class="text-red-700 text-xs font-semibold mb-2">⏰ Token telah kadaluarsa?</p>
|
||||
<p class="text-red-700 text-xs mb-3">Minta tautan reset password yang baru:</p>
|
||||
<a href="{{ route('password.request') }}" class="inline-block px-4 py-2 bg-red-600 text-white text-xs font-semibold rounded hover:bg-red-700 transition">
|
||||
🔄 Minta Link Reset Baru
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(!$errors->has('token'))
|
||||
<p class="text-red-700 text-xs mt-3 font-semibold">💡 Pastikan:</p>
|
||||
<ul class="text-red-700 text-xs mt-1 space-y-0.5 ml-2">
|
||||
<li>✓ Password minimal 8 karakter</li>
|
||||
<li>✓ Kedua password sama persis</li>
|
||||
<li>✓ Tidak menggunakan password lama</li>
|
||||
</ul>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Form -->
|
||||
<form method="POST" action="{{ route('password.store') }}" class="bg-white rounded-xl shadow-lg p-8 border-t-4 border-green-600" onsubmit="handleSubmit(event)">
|
||||
@csrf
|
||||
|
||||
<!-- Password Reset Token -->
|
||||
<input type="hidden" name="token" value="{{ $request->route('token') }}">
|
||||
|
||||
<!-- Email Address (Read-only) -->
|
||||
<div class="mb-6">
|
||||
<label for="email" class="block text-sm font-semibold text-gray-700 mb-2">📧 Email</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value="{{ $request->email }}"
|
||||
disabled
|
||||
class="w-full px-4 py-3 border-2 border-gray-300 rounded-lg bg-gray-100 text-gray-600 text-sm cursor-not-allowed"
|
||||
/>
|
||||
<input type="hidden" name="email" value="{{ $request->email }}">
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="mb-6">
|
||||
<label for="password" class="block text-sm font-semibold text-gray-700 mb-2">🔐 Password Baru</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
name="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
placeholder="minimal 8 karakter"
|
||||
class="w-full px-4 py-3 border-2 border-gray-300 rounded-lg focus:outline-none focus:border-green-600 focus:ring-2 focus:ring-green-200 transition text-sm pr-12 @error('password') border-red-500 @enderror"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick="togglePassword('password', this)"
|
||||
class="absolute right-3 top-3 text-gray-500 hover:text-gray-700 text-xl"
|
||||
>
|
||||
👁️
|
||||
</button>
|
||||
</div>
|
||||
@error('password')
|
||||
<span class="text-red-500 text-sm mt-1 block">⚠️ {{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<div class="mb-6">
|
||||
<label for="password_confirmation" class="block text-sm font-semibold text-gray-700 mb-2">✅ Konfirmasi Password</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="password_confirmation"
|
||||
type="password"
|
||||
name="password_confirmation"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
placeholder="ulangi password yang sama"
|
||||
class="w-full px-4 py-3 border-2 border-gray-300 rounded-lg focus:outline-none focus:border-green-600 focus:ring-2 focus:ring-green-200 transition text-sm pr-12 @error('password_confirmation') border-red-500 @enderror"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick="togglePassword('password_confirmation', this)"
|
||||
class="absolute right-3 top-3 text-gray-500 hover:text-gray-700 text-xl"
|
||||
>
|
||||
👁️
|
||||
</button>
|
||||
</div>
|
||||
@error('password_confirmation')
|
||||
<span class="text-red-500 text-sm mt-1 block">⚠️ {{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button
|
||||
id="submitBtn"
|
||||
type="submit"
|
||||
class="w-full bg-gradient-to-r from-green-600 to-emerald-600 text-white font-bold py-3 px-4 rounded-lg hover:shadow-lg active:scale-95 transition text-base mb-4 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
✨ Simpan Password Baru
|
||||
</button>
|
||||
|
||||
<!-- Back to Login -->
|
||||
<div class="text-center">
|
||||
<p class="text-gray-600 text-sm">
|
||||
<a href="{{ route('login') }}" class="text-green-600 hover:text-green-700 font-semibold">Kembali ke Login</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Security Tips -->
|
||||
<div class="mt-6 text-sm text-gray-600">
|
||||
<p class="font-semibold mb-2">💡 Tips Password yang Aman:</p>
|
||||
<ul class="space-y-1 text-xs">
|
||||
<li>✓ Gunakan kombinasi huruf besar, kecil, angka, dan simbol</li>
|
||||
<li>✓ Hindari kata-kata yang mudah ditebak</li>
|
||||
<li>✓ Jangan gunakan informasi pribadi</li>
|
||||
<li>✓ Minimal 8 karakter, semakin panjang semakin aman</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function togglePassword(inputId, button) {
|
||||
const input = document.getElementById(inputId);
|
||||
if (input.type === 'password') {
|
||||
input.type = 'text';
|
||||
button.textContent = '🙈';
|
||||
} else {
|
||||
input.type = 'password';
|
||||
button.textContent = '👁️';
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit(event) {
|
||||
const password = document.getElementById('password').value;
|
||||
const passwordConfirmation = document.getElementById('password_confirmation').value;
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
|
||||
// Validasi client-side
|
||||
if (password !== passwordConfirmation) {
|
||||
event.preventDefault();
|
||||
alert('❌ Password tidak sama! Pastikan kedua password sama persis.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
event.preventDefault();
|
||||
alert('❌ Password minimal 8 karakter!');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Disable button dan tampilkan loading state
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '⏳ Sedang Menyimpan...';
|
||||
submitBtn.style.opacity = '0.7';
|
||||
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
@extends('bk.layouts.app')
|
||||
|
||||
@section('title', 'Tambah Alumni')
|
||||
|
||||
@section('content')
|
||||
<div class="mb-6">
|
||||
<h2 class="text-2xl font-bold text-maroon">➕ Tambah Alumni</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">Input data alumni SMA Bima Ambulu</p>
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<div class="bg-red-50 border-l-4 border-red-400 p-4 rounded-lg mb-6">
|
||||
<p class="text-red-800 text-sm font-bold mb-2">❌ Validasi gagal:</p>
|
||||
<ul class="list-disc pl-5 text-sm text-red-700">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('bk.alumni.store') }}" method="POST" class="max-w-2xl">
|
||||
@csrf
|
||||
|
||||
<!-- Data Dasar -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-maroon">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📋 Data Dasar</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama Alumni *</label>
|
||||
<input type="text" name="nama_alumni" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('nama_alumni') }}" placeholder="Nama lengkap">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">NIS</label>
|
||||
<input type="text" name="nis" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('nis') }}" placeholder="NIS SMA">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Kelompok Asal *</label>
|
||||
<select name="kelompok_asal" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm">
|
||||
<option value="">-- Pilih --</option>
|
||||
<option value="IPA" {{ old('kelompok_asal') == 'IPA' ? 'selected' : '' }}>IPA</option>
|
||||
<option value="IPS" {{ old('kelompok_asal') == 'IPS' ? 'selected' : '' }}>IPS</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Minat / Bidang Studi</label>
|
||||
<input type="text" name="minat" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('minat') }}" placeholder="Minat siswa">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nilai Entry -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-yellow-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📊 Nilai Saat Entry (Rapor SMA)</h3>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Matematika</label>
|
||||
<input type="number" name="mtk" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('mtk') }}" placeholder="0-100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Fisika</label>
|
||||
<input type="number" name="fisika" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('fisika') }}" placeholder="0-100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Kimia</label>
|
||||
<input type="number" name="kimia" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('kimia') }}" placeholder="0-100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Biologi</label>
|
||||
<input type="number" name="biologi" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('biologi') }}" placeholder="0-100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Ekonomi</label>
|
||||
<input type="number" name="ekonomi" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('ekonomi') }}" placeholder="0-100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Geografi</label>
|
||||
<input type="number" name="geografi" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('geografi') }}" placeholder="0-100">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hasil -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-green-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">🎯 Hasil / Outcome</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Jurusan Masuk ke Polije *</label>
|
||||
<input type="text" name="major_masuk" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('major_masuk') }}" placeholder="Jurusan Polije">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Tahun Lulus Polije</label>
|
||||
<input type="number" name="tahun_lulus_polije" min="2020" max="{{ date('Y') }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('tahun_lulus_polije') }}" placeholder="Tahun lulus">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Catatan</label>
|
||||
<textarea name="catatan" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
placeholder="Catatan tambahan (opsional)">{{ old('catatan') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex gap-4 justify-end">
|
||||
<a href="{{ route('bk.alumni') }}" class="px-6 py-2 rounded-lg font-bold bg-gray-300 text-gray-700 hover:bg-gray-400 transition">
|
||||
Batal
|
||||
</a>
|
||||
<button type="submit" class="px-6 py-2 rounded-lg font-bold gradient-maroon text-white hover:opacity-90 transition">
|
||||
💾 Simpan
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
@extends('bk.layouts.app')
|
||||
|
||||
@section('title', 'Edit Alumni')
|
||||
|
||||
@section('content')
|
||||
<div class="mb-6">
|
||||
<h2 class="text-2xl font-bold text-maroon">✏ Edit Alumni</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ $alumni->nama_alumni }}</p>
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<div class="bg-red-50 border-l-4 border-red-400 p-4 rounded-lg mb-6">
|
||||
<p class="text-red-800 text-sm font-bold mb-2">❌ Validasi gagal:</p>
|
||||
<ul class="list-disc pl-5 text-sm text-red-700">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('bk.alumni.update', $alumni->id) }}" method="POST" class="max-w-2xl">
|
||||
@csrf @method('PUT')
|
||||
|
||||
<!-- Data Dasar -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-maroon">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📋 Data Dasar</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama Alumni *</label>
|
||||
<input type="text" name="nama_alumni" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('nama_alumni', $alumni->nama_alumni) }}" placeholder="Nama lengkap">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">NIS</label>
|
||||
<input type="text" name="nis" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('nis', $alumni->nis) }}" placeholder="NIS SMA">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Kelompok Asal *</label>
|
||||
<select name="kelompok_asal" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm">
|
||||
<option value="">-- Pilih --</option>
|
||||
<option value="IPA" {{ old('kelompok_asal', $alumni->kelompok_asal) == 'IPA' ? 'selected' : '' }}>IPA</option>
|
||||
<option value="IPS" {{ old('kelompok_asal', $alumni->kelompok_asal) == 'IPS' ? 'selected' : '' }}>IPS</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Minat / Bidang Studi</label>
|
||||
<input type="text" name="minat" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('minat', $alumni->minat) }}" placeholder="Minat siswa">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nilai Entry -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-yellow-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📊 Nilai Saat Entry (Rapor SMA)</h3>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Matematika</label>
|
||||
<input type="number" name="mtk" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('mtk', $alumni->mtk) }}" placeholder="0-100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Fisika</label>
|
||||
<input type="number" name="fisika" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('fisika', $alumni->fisika) }}" placeholder="0-100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Kimia</label>
|
||||
<input type="number" name="kimia" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('kimia', $alumni->kimia) }}" placeholder="0-100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Biologi</label>
|
||||
<input type="number" name="biologi" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('biologi', $alumni->biologi) }}" placeholder="0-100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Ekonomi</label>
|
||||
<input type="number" name="ekonomi" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('ekonomi', $alumni->ekonomi) }}" placeholder="0-100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 mb-1">Geografi</label>
|
||||
<input type="number" name="geografi" step="0.01" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('geografi', $alumni->geografi) }}" placeholder="0-100">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hasil -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-green-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">🎯 Hasil / Outcome</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Jurusan Masuk ke Polije *</label>
|
||||
<input type="text" name="major_masuk" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('major_masuk', $alumni->major_masuk) }}" placeholder="Jurusan Polije">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Tahun Lulus Polije</label>
|
||||
<input type="number" name="tahun_lulus_polije" min="2020" max="{{ date('Y') }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
value="{{ old('tahun_lulus_polije', $alumni->tahun_lulus_polije) }}" placeholder="Tahun lulus">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Catatan</label>
|
||||
<textarea name="catatan" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
placeholder="Catatan tambahan (opsional)">{{ old('catatan', $alumni->catatan) }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex gap-4 justify-end">
|
||||
<a href="{{ route('bk.alumni') }}" class="px-6 py-2 rounded-lg font-bold bg-gray-300 text-gray-700 hover:bg-gray-400 transition">
|
||||
Batal
|
||||
</a>
|
||||
<button type="submit" class="px-6 py-2 rounded-lg font-bold gradient-maroon text-white hover:opacity-90 transition">
|
||||
💾 Update
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
@extends('bk.layouts.app')
|
||||
|
||||
@section('title', 'Data Alumni')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Header -->
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6 gap-3">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-maroon">🎓 Data Alumni</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">Kelola data alumni SMA Bima Ambulu yang masuk ke Polije</p>
|
||||
</div>
|
||||
<a href="{{ route('bk.alumni.create') }}" class="gradient-maroon text-white font-bold py-2 px-4 rounded-lg hover:opacity-90 transition text-sm">
|
||||
+ Tambah Alumni
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="mb-6 p-4 bg-green-100 border-l-4 border-green-500 text-green-700 rounded">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Alumni Table -->
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="gradient-maroon text-white">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold">Nama Alumni</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold">NIS</th>
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold">Kelompok</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold">Jurusan Masuk</th>
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold">Tahun Lulus</th>
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
@forelse($alumni as $a)
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
<td class="px-6 py-4 font-semibold text-gray-800">{{ $a->nama_alumni }}</td>
|
||||
<td class="px-6 py-4 text-gray-600">{{ $a->nis ?? '-' }}</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<span class="px-3 py-1 rounded text-xs font-bold"
|
||||
style="{{ $a->kelompok_asal == 'IPA' ? 'background-color: #E0F2FE; color: #0369A1;' : 'background-color: #FEF3C7; color: #92400E;' }}">
|
||||
{{ $a->kelompok_asal }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-gray-800">{{ $a->major_masuk }}</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
@if($a->tahun_lulus_polije)
|
||||
<span class="px-2 py-1 rounded bg-blue-100 text-blue-800 text-sm font-semibold">{{ $a->tahun_lulus_polije }}</span>
|
||||
@else
|
||||
<span class="text-gray-400 text-sm">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center gap-2 flex justify-center">
|
||||
<a href="{{ route('bk.alumni.show', $a->id) }}" class="text-blue-600 hover:text-blue-800 font-semibold text-sm">👁 Lihat</a>
|
||||
<a href="{{ route('bk.alumni.edit', $a->id) }}" class="text-yellow-600 hover:text-yellow-800 font-semibold text-sm">✏ Edit</a>
|
||||
<form action="{{ route('bk.alumni.destroy', $a->id) }}" method="POST" class="inline swal-confirm" data-confirm-message="Yakin hapus?">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="text-red-600 hover:text-red-800 font-semibold text-sm">🗑 Hapus</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-8 text-center text-gray-500">
|
||||
Belum ada data alumni. <a href="{{ route('bk.alumni.create') }}" class="text-maroon font-bold hover:underline">Tambah sekarang</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if($alumni->hasPages())
|
||||
<div class="mt-6">
|
||||
{{ $alumni->links() }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Info Box -->
|
||||
<div class="mt-8 p-4 bg-blue-50 border-l-4 border-blue-400 rounded">
|
||||
<p class="text-sm text-blue-800">
|
||||
<strong>📊 Catatan:</strong><br>
|
||||
Data alumni digunakan untuk tracking alumni SMA Bima Ambulu yang melanjutkan ke Polije, monitoring career development, dan referensi untuk siswa baru dalam memilih jurusan.
|
||||
</p>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
@extends('bk.layouts.app')
|
||||
|
||||
@section('title', 'Detail Alumni')
|
||||
|
||||
@section('content')
|
||||
<div class="mb-6 flex justify-between items-start">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-maroon">👁 Detail Alumni</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ $alumni->nama_alumni }}</p>
|
||||
</div>
|
||||
<a href="{{ route('bk.alumni') }}" class="px-4 py-2 rounded-lg font-bold bg-gray-300 text-gray-700 hover:bg-gray-400 transition">
|
||||
← Kembali
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Data Dasar -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-maroon">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📋 Data Dasar</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p class="text-xs text-gray-600 font-semibold">Nama Alumni</p>
|
||||
<p class="text-lg font-bold text-gray-800">{{ $alumni->nama_alumni }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-600 font-semibold">NIS</p>
|
||||
<p class="text-lg font-bold text-gray-800">{{ $alumni->nis ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-600 font-semibold">Kelompok Asal</p>
|
||||
<span class="inline-block px-3 py-1 rounded text-sm font-bold"
|
||||
style="{{ $alumni->kelompok_asal == 'IPA' ? 'background-color: #E0F2FE; color: #0369A1;' : 'background-color: #FEF3C7; color: #92400E;' }}">
|
||||
{{ $alumni->kelompok_asal }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-600 font-semibold">Minat</p>
|
||||
<p class="text-gray-800">{{ $alumni->minat ?? '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nilai Entry -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-yellow-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📊 Nilai Rata-Rata (Rapor SMA)</h3>
|
||||
@if($alumni->nilai_rata_rata)
|
||||
<div class="bg-blue-50 p-4 rounded-lg">
|
||||
<p class="text-3xl font-bold text-blue-600">{{ $alumni->nilai_rata_rata }}</p>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-gray-500">-</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Hasil -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-green-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">🎯 Hasil / Outcome</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p class="text-xs text-gray-600 font-semibold">Jurusan Masuk Polije</p>
|
||||
<p class="text-lg font-bold text-gray-800">{{ $alumni->major_masuk }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-600 font-semibold">Tahun Lulus SMA Bima Ambulu</p>
|
||||
<p class="text-lg font-bold text-gray-800">{{ $alumni->tahun_lulus_polije ?? '-' }}</p>
|
||||
</div>
|
||||
@if($alumni->catatan)
|
||||
<div class="md:col-span-2">
|
||||
<p class="text-xs text-gray-600 font-semibold">Catatan</p>
|
||||
<p class="text-gray-800">{{ $alumni->catatan }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex gap-4 justify-end">
|
||||
<a href="{{ route('bk.alumni.edit', $alumni->id) }}" class="px-6 py-2 rounded-lg font-bold bg-yellow-400 text-maroon hover:bg-yellow-300 transition">
|
||||
✏ Edit
|
||||
</a>
|
||||
<form action="{{ route('bk.alumni.destroy', $alumni->id) }}" method="POST" class="inline swal-confirm" data-confirm-message="Yakin hapus data alumni ini?">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="px-6 py-2 rounded-lg font-bold bg-red-500 text-white hover:bg-red-600 transition">
|
||||
🗑 Hapus
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<div class="stat-card bg-white rounded-lg shadow p-6 border-t-4 border-teal-500">
|
||||
<div class="stat-card bg-white rounded-lg shadow p-6 border-t-4 border-purple-500">
|
||||
<p class="text-gray-600 text-sm font-semibold">👥 Total Siswa</p>
|
||||
<p class="text-3xl font-bold text-bk mt-2">{{ $totalSiswa }}</p>
|
||||
</div>
|
||||
|
|
@ -28,39 +28,41 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kelompok Distribution & Top Majors -->
|
||||
<!-- Charts Section -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-teal-500">
|
||||
<h3 class="text-lg font-bold text-bk mb-4">📊 Siswa per Kelompok</h3>
|
||||
<div class="space-y-3">
|
||||
@foreach($kelompokStats as $stat)
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-semibold text-gray-700 w-20">{{ $stat->kelompok_asal ?? 'Tidak Ada' }}</span>
|
||||
<div class="flex-1 h-6 bg-gray-200 rounded">
|
||||
<div class="h-full rounded flex items-center justify-center text-white text-xs font-bold"
|
||||
style="width: {{ $totalSiswa > 0 ? ($stat->count / $totalSiswa) * 100 : 0 }}%; background-color: {{ $stat->kelompok_asal == 'IPA' ? '#0369A1' : '#D97706' }};">
|
||||
{{ $stat->count }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
<!-- Rekomendasi Distribution Chart -->
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-purple-400">
|
||||
<h3 class="text-lg font-bold text-bk mb-4">📊 Distribusi Rekomendasi per Jurusan</h3>
|
||||
<div style="position: relative; height: 300px;">
|
||||
<canvas id="chartRecommendations"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kelompok Distribution Chart -->
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-indigo-400">
|
||||
<h3 class="text-lg font-bold text-bk mb-4">📈 Distribusi Siswa per Kelompok</h3>
|
||||
<div style="position: relative; height: 300px;">
|
||||
<canvas id="chartKelompok"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rekomendasi & Top Majors -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<!-- Rekomendasi per Kelompok -->
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-purple-500">
|
||||
<h3 class="text-lg font-bold text-bk mb-4">📊 Rekomendasi per Kelompok</h3>
|
||||
<div style="position: relative; height: 250px;">
|
||||
<canvas id="chartRekomendasiKelompok"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Recommended Majors Chart -->
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-green-400">
|
||||
<h3 class="text-lg font-bold text-bk mb-4">🎯 Jurusan Terpopuler</h3>
|
||||
@if($topMajors->isNotEmpty())
|
||||
<div class="space-y-3">
|
||||
@foreach($topMajors as $major)
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-semibold text-gray-700 flex-1 truncate">{{ $major->major_name }}</span>
|
||||
<span class="px-3 py-1 rounded bg-green-100 text-green-800 font-bold text-sm">{{ $major->count }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<p class="text-gray-500 text-sm">Belum ada data rekomendasi</p>
|
||||
@endif
|
||||
<div style="position: relative; height: 250px;">
|
||||
<canvas id="chartTopMajors"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -70,7 +72,7 @@
|
|||
@if($recentStudents->isNotEmpty())
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="border-b-2 border-teal-500">
|
||||
<thead class="border-b-2 border-purple-500">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-2 font-bold text-bk">Nama</th>
|
||||
<th class="text-center px-4 py-2 font-bold text-bk">NIS</th>
|
||||
|
|
@ -89,7 +91,7 @@
|
|||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-center">
|
||||
<a href="{{ route('bk.student.detail', $student->id) }}" class="text-teal-600 hover:text-teal-800 font-semibold text-xs">👁 Lihat</a>
|
||||
<a href="{{ route('bk.student.detail', $student->id) }}" class="text-purple-600 hover:text-purple-800 font-semibold text-xs">👁 Lihat</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
|
|
@ -139,3 +141,148 @@
|
|||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
|
||||
<script>
|
||||
// Chart 1: Rekomendasi Distribution
|
||||
const chartRecommendationsCtx = document.getElementById('chartRecommendations').getContext('2d');
|
||||
const chartRecommendations = new Chart(chartRecommendationsCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: @json($chartMajorNames),
|
||||
datasets: [{
|
||||
data: @json($chartMajorCounts),
|
||||
backgroundColor: [
|
||||
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF',
|
||||
'#FF9F40', '#FF6384', '#C9CBCF', '#4BC0C0'
|
||||
],
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
font: { size: 11 },
|
||||
padding: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Chart 2: Kelompok Distribution Bar
|
||||
const chartKelompokCtx = document.getElementById('chartKelompok').getContext('2d');
|
||||
const chartKelompok = new Chart(chartKelompokCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: @json($chartKelompokNames),
|
||||
datasets: [{
|
||||
label: 'Jumlah Siswa',
|
||||
data: @json($chartKelompokCounts),
|
||||
backgroundColor: ['#0369A1', '#D97706'],
|
||||
borderColor: ['#0369A1', '#D97706'],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: {
|
||||
font: { size: 11 }
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
font: { size: 10 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Chart 3: Rekomendasi per Kelompok Bar Chart
|
||||
const chartRekomendasiKelompokCtx = document.getElementById('chartRekomendasiKelompok').getContext('2d');
|
||||
const chartRekomendasiKelompok = new Chart(chartRekomendasiKelompokCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: @json($rekomendasiPerKelompok->pluck('kelompok_asal')->toArray()),
|
||||
datasets: [{
|
||||
label: 'Jumlah Rekomendasi',
|
||||
data: @json($rekomendasiPerKelompok->pluck('count')->toArray()),
|
||||
backgroundColor: ['#0369A1', '#D97706'],
|
||||
borderColor: ['#0369A1', '#D97706'],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: {
|
||||
font: { size: 11 }
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
font: { size: 10 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Chart 4: Top Majors Horizontal Bar Chart
|
||||
const chartTopMajorsCtx = document.getElementById('chartTopMajors').getContext('2d');
|
||||
const chartTopMajors = new Chart(chartTopMajorsCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: @json($topMajorsChart),
|
||||
datasets: [{
|
||||
label: 'Jumlah Rekomendasi',
|
||||
data: @json($topMajorsCounts),
|
||||
backgroundColor: '#36A2EB',
|
||||
borderColor: '#36A2EB',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: {
|
||||
font: { size: 11 }
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
font: { size: 10 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -13,104 +13,81 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Error Summary Alert -->
|
||||
@if($errors->any())
|
||||
<div class="bg-red-50 border-l-4 border-red-400 p-4 rounded-lg mb-6">
|
||||
<ul class="text-red-800 text-sm list-disc list-inside">
|
||||
<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-red-600 text-xl">⚠️</span>
|
||||
<span class="text-red-700 font-bold">Perbaiki kesalahan berikut:</span>
|
||||
</div>
|
||||
<ul class="text-red-600 text-sm space-y-1 ml-8">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
<li>• {{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('bk.jurusan.store') }}" method="POST" class="space-y-6">
|
||||
<form action="{{ route('bk.jurusan.store') }}" method="POST" class="space-y-6" id="jurusanForm">
|
||||
@csrf
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-bold text-bk mb-4">📋 Informasi Jurusan</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama Jurusan <span class="text-red-500">*</span></label>
|
||||
<input type="text" name="nama_jurusan" value="{{ old('nama_jurusan') }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500" placeholder="Contoh: Teknologi Informasi" required>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama Jurusan <span class="text-red-500">*</span> <span class="text-gray-400 text-xs">(minimal 3 karakter)</span></label>
|
||||
<input type="text" name="nama_jurusan" value="{{ old('nama_jurusan') }}" minlength="3" maxlength="100" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 transition @error('nama_jurusan') border-red-500 focus:ring-red-400 @else border-gray-300 focus:ring-teal-400 @enderror" placeholder="Contoh: Teknologi Informasi" required oninput="validateJurusanForm()">
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<span id="namaError" class="text-red-500 text-xs hidden">⚠️ Nama harus minimal 3 karakter</span>
|
||||
<span id="namaValid" class="text-green-500 text-xs hidden">✓ Nama valid</span>
|
||||
</div>
|
||||
@error('nama_jurusan') <span class="text-red-500 text-xs block mt-1">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Deskripsi</label>
|
||||
<textarea name="deskripsi" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500" placeholder="Jelaskan singkat tentang jurusan ini">{{ old('deskripsi') }}</textarea>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Deskripsi <span class="text-gray-400 text-xs">(disarankan detail untuk kebutuhan chatbot)</span></label>
|
||||
<textarea name="deskripsi" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-400 transition" placeholder="Jelaskan detail tentang jurusan ini" oninput="updateCharCount('deskripsi'); validateJurusanForm()">{{ old('deskripsi') }}</textarea>
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<p class="text-xs text-gray-500">Jelaskan tentang program, fasilitas, dan prospek</p>
|
||||
<span id="descCount" class="text-gray-400 text-xs">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Keywords (Kata Kunci Minat & Cita-cita)</label>
|
||||
<textarea name="keywords" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500" placeholder="Pisahkan dengan koma, contoh: programmer, developer, coding, software, web">{{ old('keywords') }}</textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">Keywords digunakan untuk mencocokkan minat dan cita-cita siswa dengan jurusan ini. Pisahkan dengan koma.</p>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Keywords (Kata Kunci) <span class="text-gray-400 text-xs">(pisahkan dengan koma)</span></label>
|
||||
<textarea name="keywords" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-400 transition" placeholder="Contoh: programmer, developer, coding, software" oninput="validateJurusanForm()">{{ old('keywords') }}</textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">Gunakan keywords untuk mencocokkan minat & cita-cita siswa dengan jurusan</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Preferensi Studi</label>
|
||||
<textarea name="preferensi_studi" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500" placeholder="Pisahkan dengan koma, contoh: Sains & Teknologi, Pertanian & Lingkungan">{{ old('preferensi_studi') }}</textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">Rumpun bidang studi yang cocok untuk jurusan ini. Pilihan: Sains & Teknologi, Pertanian & Lingkungan, Kesehatan & Ilmu Hayat, Bisnis & Manajemen, Sosial & Humaniora</p>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Preferensi Studi <span class="text-gray-400 text-xs">(pisahkan dengan koma)</span></label>
|
||||
<textarea name="preferensi_studi" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-400 transition" placeholder="Contoh: Praktik Langsung, DuDi, Project Based, Blended Learning" oninput="validateJurusanForm()">{{ old('preferensi_studi') }}</textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">Pilihan preferensi studi: Praktik Langsung, DuDi, Project Based, Blended Learning</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Prospek Kerja</label>
|
||||
<textarea name="prospek_kerja" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500" placeholder="Contoh: Software Developer, Web Developer, Data Analyst">{{ old('prospek_kerja') }}</textarea>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Prospek Kerja <span class="text-gray-400 text-xs">(pisahkan dengan koma)</span></label>
|
||||
<textarea name="prospek_kerja" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-400 transition" placeholder="Contoh: Software Developer, Web Developer, Data Analyst" oninput="validateJurusanForm()">{{ old('prospek_kerja') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bobot Mata Pelajaran -->
|
||||
<!-- Bobot Mata Pelajaran dihapus: gunakan bobot ROC global -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-bold text-bk mb-4">⚖️ Bobot Mata Pelajaran</h3>
|
||||
<p class="text-xs text-gray-500 mb-4">Tentukan bobot setiap mata pelajaran untuk jurusan ini (0.00 - 1.00). Mata pelajaran yang lebih relevan diberi bobot lebih tinggi. Jumlah total tidak harus 1.0.</p>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h4 class="text-sm font-bold text-gray-700 mb-3 border-b pb-2">📐 IPA</h4>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Matematika</label>
|
||||
<input type="number" name="bobot_mtk" value="{{ old('bobot_mtk', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Fisika</label>
|
||||
<input type="number" name="bobot_fisika" value="{{ old('bobot_fisika', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Kimia</label>
|
||||
<input type="number" name="bobot_kimia" value="{{ old('bobot_kimia', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Biologi</label>
|
||||
<input type="number" name="bobot_biologi" value="{{ old('bobot_biologi', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-bold text-gray-700 mb-3 border-b pb-2">📊 IPS</h4>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Ekonomi</label>
|
||||
<input type="number" name="bobot_ekonomi" value="{{ old('bobot_ekonomi', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Geografi</label>
|
||||
<input type="number" name="bobot_geografi" value="{{ old('bobot_geografi', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Sosiologi</label>
|
||||
<input type="number" name="bobot_sosiologi" value="{{ old('bobot_sosiologi', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Sejarah</label>
|
||||
<input type="number" name="bobot_sejarah" value="{{ old('bobot_sejarah', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-bk mb-4">⚖️ Bobot Penilaian (ROC)</h3>
|
||||
<p class="text-sm text-gray-600 mb-3">Mulai sekarang, sistem menggunakan bobot ROC tetap untuk semua jurusan. Pengaturan bobot per-mata-pelajaran tidak lagi tersedia di form ini.</p>
|
||||
<ul class="list-disc pl-5 text-sm text-gray-700 space-y-1">
|
||||
<li><strong>Minat</strong>: 45.6% (0.456)</li>
|
||||
<li><strong>Preferensi</strong>: 25.6% (0.256)</li>
|
||||
<li><strong>Nilai</strong>: 15.6% (0.156)</li>
|
||||
<li><strong>Cita-cita</strong>: 9.0% (0.090)</li>
|
||||
<li><strong>Prestasi</strong>: 4.0% (0.040)</li>
|
||||
</ul>
|
||||
<p class="text-xs text-gray-500 mt-3">Catatan: Jika Anda tetap menyimpan nilai bobot di database, sistem akan mengabaikannya dan menggunakan bobot ROC global dalam perhitungan rekomendasi.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<button type="submit" class="flex-1 gradient-bk text-white font-bold py-3 px-4 rounded-lg hover:opacity-90 transition">
|
||||
<button type="submit" class="flex-1 gradient-bk text-white font-bold py-3 px-4 rounded-lg hover:opacity-90 transition disabled:opacity-50 disabled:cursor-not-allowed" id="submitButton">
|
||||
💾 Simpan Jurusan
|
||||
</button>
|
||||
<a href="{{ route('bk.jurusan') }}" class="flex-1 bg-gray-400 text-white font-bold py-3 px-4 rounded-lg hover:bg-gray-500 transition text-center">
|
||||
|
|
@ -119,3 +96,45 @@
|
|||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
function updateCharCount(textareaName) {
|
||||
const textarea = document.querySelector(`textarea[name="${textareaName}"]`);
|
||||
const countSpan = document.getElementById('descCount');
|
||||
const maxLength = parseInt(textarea.maxLength);
|
||||
countSpan.textContent = maxLength > 0 ? `${textarea.value.length}/${maxLength}` : `${textarea.value.length}`;
|
||||
}
|
||||
|
||||
function validateJurusanForm() {
|
||||
const namaJurusan = document.querySelector('input[name="nama_jurusan"]');
|
||||
const namaError = document.getElementById('namaError');
|
||||
const namaValid = document.getElementById('namaValid');
|
||||
const submitButton = document.getElementById('submitButton');
|
||||
|
||||
let isNamaValid = false;
|
||||
if (namaJurusan.value.trim().length >= 3) {
|
||||
namaJurusan.classList.remove('border-red-500', 'focus:ring-red-400');
|
||||
namaJurusan.classList.add('border-gray-300', 'focus:ring-teal-400');
|
||||
namaError.classList.add('hidden');
|
||||
namaValid.classList.remove('hidden');
|
||||
isNamaValid = true;
|
||||
} else if (namaJurusan.value.trim().length > 0) {
|
||||
namaJurusan.classList.remove('border-gray-300', 'focus:ring-teal-400');
|
||||
namaJurusan.classList.add('border-red-500', 'focus:ring-red-400');
|
||||
namaError.classList.remove('hidden');
|
||||
namaValid.classList.add('hidden');
|
||||
} else {
|
||||
namaError.classList.add('hidden');
|
||||
namaValid.classList.add('hidden');
|
||||
}
|
||||
|
||||
submitButton.disabled = !isNamaValid;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
validateJurusanForm();
|
||||
updateCharCount('deskripsi');
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -13,17 +13,22 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Error Summary Alert -->
|
||||
@if($errors->any())
|
||||
<div class="bg-red-50 border-l-4 border-red-400 p-4 rounded-lg mb-6">
|
||||
<ul class="text-red-800 text-sm list-disc list-inside">
|
||||
<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-red-600 text-xl">⚠️</span>
|
||||
<span class="text-red-700 font-bold">Perbaiki kesalahan berikut:</span>
|
||||
</div>
|
||||
<ul class="text-red-600 text-sm space-y-1 ml-8">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
<li>• {{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('bk.jurusan.update', $jurusan->id) }}" method="POST" class="space-y-6">
|
||||
<form action="{{ route('bk.jurusan.update', $jurusan->id) }}" method="POST" class="space-y-6" id="jurusanForm">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
|
|
@ -31,19 +36,28 @@
|
|||
<h3 class="text-lg font-bold text-bk mb-4">📋 Informasi Jurusan</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama Jurusan <span class="text-red-500">*</span></label>
|
||||
<input type="text" name="nama_jurusan" value="{{ old('nama_jurusan', $jurusan->nama_jurusan) }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500" required>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama Jurusan <span class="text-red-500">*</span> <span class="text-gray-400 text-xs">(minimal 3 karakter)</span></label>
|
||||
<input type="text" name="nama_jurusan" value="{{ old('nama_jurusan', $jurusan->nama_jurusan) }}" minlength="3" maxlength="100" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 transition @error('nama_jurusan') border-red-500 focus:ring-red-400 @else border-gray-300 focus:ring-teal-400 @enderror" placeholder="Contoh: Teknologi Informasi" required oninput="validateJurusanForm()">
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<span id="namaError" class="text-red-500 text-xs hidden">⚠️ Nama harus minimal 3 karakter</span>
|
||||
<span id="namaValid" class="text-green-500 text-xs hidden">✓ Nama valid</span>
|
||||
</div>
|
||||
@error('nama_jurusan') <span class="text-red-500 text-xs block mt-1">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Deskripsi</label>
|
||||
<textarea name="deskripsi" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500" placeholder="Jelaskan singkat tentang jurusan ini">{{ old('deskripsi', $jurusan->deskripsi) }}</textarea>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Deskripsi <span class="text-gray-400 text-xs">(disarankan detail untuk kebutuhan chatbot)</span></label>
|
||||
<textarea name="deskripsi" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-400 transition" placeholder="Jelaskan detail tentang jurusan ini" oninput="updateCharCount('deskripsi'); validateJurusanForm()">{{ old('deskripsi', $jurusan->deskripsi) }}</textarea>
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<p class="text-xs text-gray-500">Jelaskan tentang program, fasilitas, dan prospek</p>
|
||||
<span id="descCount" class="text-gray-400 text-xs">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Keywords (Kata Kunci Minat & Cita-cita)</label>
|
||||
<textarea name="keywords" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500" placeholder="Pisahkan dengan koma">{{ old('keywords', implode(', ', $jurusan->keywords ?? [])) }}</textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">Keywords digunakan untuk mencocokkan minat dan cita-cita siswa dengan jurusan ini. Pisahkan dengan koma.</p>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Keywords (Kata Kunci) <span class="text-gray-400 text-xs">(pisahkan dengan koma)</span></label>
|
||||
<textarea name="keywords" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-400 transition" placeholder="Contoh: programmer, developer, coding, software" oninput="validateJurusanForm()">{{ old('keywords', implode(', ', $jurusan->keywords ?? [])) }}</textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">Gunakan keywords untuk mencocokkan minat & cita-cita siswa dengan jurusan</p>
|
||||
@if(!empty($jurusan->keywords))
|
||||
<div class="flex flex-wrap gap-1 mt-2">
|
||||
@foreach($jurusan->keywords as $kw)
|
||||
|
|
@ -54,74 +68,34 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Preferensi Studi</label>
|
||||
<textarea name="preferensi_studi" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500" placeholder="Pisahkan dengan koma">{{ old('preferensi_studi', implode(', ', $jurusan->preferensi_studi ?? [])) }}</textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">Rumpun bidang studi yang cocok: Sains & Teknologi, Pertanian & Lingkungan, Kesehatan & Ilmu Hayat, Bisnis & Manajemen, Sosial & Humaniora</p>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Preferensi Studi <span class="text-gray-400 text-xs">(pisahkan dengan koma)</span></label>
|
||||
<textarea name="preferensi_studi" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-400 transition" placeholder="Contoh: Praktik Langsung, DuDi, Project Based, Blended Learning">{{ old('preferensi_studi', implode(', ', $jurusan->preferensi_studi ?? [])) }}</textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">Pilihan preferensi studi: Praktik Langsung, DuDi, Project Based, Blended Learning</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Prospek Kerja</label>
|
||||
<textarea name="prospek_kerja" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500" placeholder="Contoh: Software Developer, Web Developer, Data Analyst">{{ old('prospek_kerja', $jurusan->prospek_kerja) }}</textarea>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Prospek Kerja <span class="text-gray-400 text-xs">(pisahkan dengan koma)</span></label>
|
||||
<textarea name="prospek_kerja" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-400 transition" placeholder="Contoh: Software Developer, Web Developer, Data Analyst">{{ old('prospek_kerja', $jurusan->prospek_kerja) }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bobot Mata Pelajaran -->
|
||||
@php
|
||||
$bobot = $jurusan->bobot_mapel ?? [];
|
||||
@endphp
|
||||
<!-- Bobot Mata Pelajaran dihapus: gunakan bobot ROC global -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-bold text-bk mb-4">⚖️ Bobot Mata Pelajaran</h3>
|
||||
<p class="text-xs text-gray-500 mb-4">Tentukan bobot setiap mata pelajaran untuk jurusan ini (0.00 - 1.00). Mata pelajaran yang lebih relevan diberi bobot lebih tinggi. Jumlah total tidak harus 1.0.</p>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h4 class="text-sm font-bold text-gray-700 mb-3 border-b pb-2">📐 IPA</h4>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Matematika</label>
|
||||
<input type="number" name="bobot_mtk" value="{{ old('bobot_mtk', $bobot['mtk'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Fisika</label>
|
||||
<input type="number" name="bobot_fisika" value="{{ old('bobot_fisika', $bobot['fisika'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Kimia</label>
|
||||
<input type="number" name="bobot_kimia" value="{{ old('bobot_kimia', $bobot['kimia'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Biologi</label>
|
||||
<input type="number" name="bobot_biologi" value="{{ old('bobot_biologi', $bobot['biologi'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-bold text-gray-700 mb-3 border-b pb-2">📊 IPS</h4>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Ekonomi</label>
|
||||
<input type="number" name="bobot_ekonomi" value="{{ old('bobot_ekonomi', $bobot['ekonomi'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Geografi</label>
|
||||
<input type="number" name="bobot_geografi" value="{{ old('bobot_geografi', $bobot['geografi'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Sosiologi</label>
|
||||
<input type="number" name="bobot_sosiologi" value="{{ old('bobot_sosiologi', $bobot['sosiologi'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Sejarah</label>
|
||||
<input type="number" name="bobot_sejarah" value="{{ old('bobot_sejarah', $bobot['sejarah'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-bk mb-4">⚖️ Bobot Penilaian (ROC)</h3>
|
||||
<p class="text-sm text-gray-600 mb-3">Mulai sekarang, sistem menggunakan bobot ROC tetap untuk semua jurusan. Pengaturan bobot per-mata-pelajaran tidak lagi tersedia di form ini.</p>
|
||||
<ul class="list-disc pl-5 text-sm text-gray-700 space-y-1">
|
||||
<li><strong>Minat</strong>: 45.6% (0.456)</li>
|
||||
<li><strong>Preferensi</strong>: 25.6% (0.256)</li>
|
||||
<li><strong>Nilai</strong>: 15.6% (0.156)</li>
|
||||
<li><strong>Cita-cita</strong>: 9.0% (0.090)</li>
|
||||
<li><strong>Prestasi</strong>: 4.0% (0.040)</li>
|
||||
</ul>
|
||||
<p class="text-xs text-gray-500 mt-3">Catatan: Jika Anda tetap menyimpan nilai bobot di database, sistem akan mengabaikannya dan menggunakan bobot ROC global dalam perhitungan rekomendasi.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<button type="submit" class="flex-1 gradient-bk text-white font-bold py-3 px-4 rounded-lg hover:opacity-90 transition">
|
||||
<button type="submit" class="flex-1 gradient-bk text-white font-bold py-3 px-4 rounded-lg hover:opacity-90 transition disabled:opacity-50 disabled:cursor-not-allowed" id="submitButton">
|
||||
💾 Simpan Perubahan
|
||||
</button>
|
||||
<a href="{{ route('bk.jurusan') }}" class="flex-1 bg-gray-400 text-white font-bold py-3 px-4 rounded-lg hover:bg-gray-500 transition text-center">
|
||||
|
|
@ -130,3 +104,45 @@
|
|||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
function updateCharCount(textareaName) {
|
||||
const textarea = document.querySelector(`textarea[name="${textareaName}"]`);
|
||||
const countSpan = document.getElementById('descCount');
|
||||
const maxLength = parseInt(textarea.maxLength);
|
||||
countSpan.textContent = maxLength > 0 ? `${textarea.value.length}/${maxLength}` : `${textarea.value.length}`;
|
||||
}
|
||||
|
||||
function validateJurusanForm() {
|
||||
const namaJurusan = document.querySelector('input[name="nama_jurusan"]');
|
||||
const namaError = document.getElementById('namaError');
|
||||
const namaValid = document.getElementById('namaValid');
|
||||
const submitButton = document.getElementById('submitButton');
|
||||
|
||||
let isNamaValid = false;
|
||||
if (namaJurusan.value.trim().length >= 3) {
|
||||
namaJurusan.classList.remove('border-red-500', 'focus:ring-red-400');
|
||||
namaJurusan.classList.add('border-gray-300', 'focus:ring-teal-400');
|
||||
namaError.classList.add('hidden');
|
||||
namaValid.classList.remove('hidden');
|
||||
isNamaValid = true;
|
||||
} else if (namaJurusan.value.trim().length > 0) {
|
||||
namaJurusan.classList.remove('border-gray-300', 'focus:ring-teal-400');
|
||||
namaJurusan.classList.add('border-red-500', 'focus:ring-red-400');
|
||||
namaError.classList.remove('hidden');
|
||||
namaValid.classList.add('hidden');
|
||||
} else {
|
||||
namaError.classList.add('hidden');
|
||||
namaValid.classList.add('hidden');
|
||||
}
|
||||
|
||||
submitButton.disabled = !isNamaValid;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
validateJurusanForm();
|
||||
updateCharCount('deskripsi');
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@
|
|||
<a href="{{ route('bk.jurusan.edit', $jurusan->id) }}" class="px-3 py-1 bg-blue-100 text-blue-700 rounded text-xs font-semibold hover:bg-blue-200 transition">
|
||||
✏️ Edit
|
||||
</a>
|
||||
<form action="{{ route('bk.jurusan.destroy', $jurusan->id) }}" method="POST" onsubmit="return confirm('Yakin ingin menghapus jurusan {{ $jurusan->nama_jurusan }}?')">
|
||||
<form action="{{ route('bk.jurusan.destroy', $jurusan->id) }}" method="POST" class="swal-confirm" data-confirm-message="Yakin ingin menghapus jurusan {{ $jurusan->nama_jurusan }}?">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="px-3 py-1 bg-red-100 text-red-700 rounded text-xs font-semibold hover:bg-red-200 transition">
|
||||
|
|
@ -97,23 +97,23 @@
|
|||
<p class="text-gray-700 text-sm mb-4">Sistem menggunakan 5 kriteria utama untuk memberikan rekomendasi jurusan yang tepat:</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div class="p-4 bg-blue-50 rounded-lg border-l-4 border-blue-400">
|
||||
<p class="font-bold text-blue-800 text-sm">📝 Nilai Akademik (40%)</p>
|
||||
<p class="font-bold text-blue-800 text-sm">📝 Nilai Akademik (15.6%)</p>
|
||||
<p class="text-xs text-blue-700 mt-1">IPA: MTK, Fisika, Kimia, Biologi<br>IPS: Ekonomi, Geografi, Sosiologi, Sejarah</p>
|
||||
</div>
|
||||
<div class="p-4 bg-green-50 rounded-lg border-l-4 border-green-400">
|
||||
<p class="font-bold text-green-800 text-sm">💡 Minat & Bakat (35%)</p>
|
||||
<p class="font-bold text-green-800 text-sm">💡 Minat & Bakat (45.6%)</p>
|
||||
<p class="text-xs text-green-700 mt-1">Dicocokkan dengan keywords jurusan secara graduated</p>
|
||||
</div>
|
||||
<div class="p-4 bg-yellow-50 rounded-lg border-l-4 border-yellow-400">
|
||||
<p class="font-bold text-yellow-800 text-sm">🎯 Preferensi Studi (15%)</p>
|
||||
<p class="font-bold text-yellow-800 text-sm">🎯 Preferensi Studi (25.6%)</p>
|
||||
<p class="text-xs text-yellow-700 mt-1">Praktik Langsung, DuDi, Project Based, Blended Learning</p>
|
||||
</div>
|
||||
<div class="p-4 bg-purple-50 rounded-lg border-l-4 border-purple-400">
|
||||
<p class="font-bold text-purple-800 text-sm">🏆 Prestasi (5%)</p>
|
||||
<p class="font-bold text-purple-800 text-sm">🏆 Prestasi (4%)</p>
|
||||
<p class="text-xs text-purple-700 mt-1">Prestasi akademik dan non-akademik siswa</p>
|
||||
</div>
|
||||
<div class="p-4 bg-red-50 rounded-lg border-l-4 border-red-400">
|
||||
<p class="font-bold text-red-800 text-sm">💼 Cita-cita (5%)</p>
|
||||
<p class="font-bold text-red-800 text-sm">💼 Cita-cita (9%)</p>
|
||||
<p class="text-xs text-red-700 mt-1">Dicocokkan dengan keywords jurusan secara graduated</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,15 +4,21 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>@yield('title', 'Panel Guru BK') - SPK Jurusan Kuliah</title>
|
||||
<title>@yield('title', 'Panel Guru BK') - SPK Jurusan Polije</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.gradient-bk { background: linear-gradient(135deg, #0f766e 0%, #14b8a6 100%); }
|
||||
.text-bk { color: #0f766e; }
|
||||
.border-bk { border-color: #0f766e; }
|
||||
.bg-cream { background-color: #F8FAFC; }
|
||||
/* Updated BK theme to match modern purple-indigo gradient */
|
||||
.gradient-maroon { background: linear-gradient(135deg, #7c3aed 0%, #6366f1 100%); }
|
||||
.text-maroon { color: #7c3aed; }
|
||||
.border-maroon { border-color: #7c3aed; }
|
||||
.bg-maroon { background-color: #7c3aed; }
|
||||
|
||||
.gradient-bk { background: linear-gradient(135deg, #7c3aed 0%, #6366f1 100%); }
|
||||
.text-bk { color: #7c3aed; }
|
||||
.border-bk { border-color: #7c3aed; }
|
||||
.bg-cream { background-color: #f8fafc; }
|
||||
.stat-card { transition: all 0.3s ease; }
|
||||
.stat-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(15, 118, 110, 0.1); }
|
||||
.stat-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(124, 58, 237, 0.1); }
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar-dark {
|
||||
|
|
@ -21,17 +27,18 @@
|
|||
.sidebar-link {
|
||||
transition: all 0.25s cubic-bezier(.4,0,.2,1);
|
||||
border-left: 3px solid transparent;
|
||||
color: #94a3b8;
|
||||
/* Higher contrast on dark sidebar */
|
||||
color: #cbd5e1;
|
||||
}
|
||||
.sidebar-link:hover {
|
||||
background: rgba(20, 184, 166, 0.12);
|
||||
color: #e2e8f0;
|
||||
border-left-color: rgba(20, 184, 166, 0.5);
|
||||
background: rgba(124, 58, 237, 0.12);
|
||||
color: #ffffff;
|
||||
border-left-color: rgba(124, 58, 237, 0.5);
|
||||
}
|
||||
.sidebar-link.active {
|
||||
background: linear-gradient(90deg, rgba(20,184,166,0.2) 0%, rgba(20,184,166,0.03) 100%);
|
||||
color: #5eead4 !important;
|
||||
border-left-color: #5eead4;
|
||||
background: linear-gradient(90deg, rgba(124, 58, 237, 0.2) 0%, rgba(124, 58, 237, 0.03) 100%);
|
||||
color: #c4b5fd !important;
|
||||
border-left-color: #c4b5fd;
|
||||
}
|
||||
.sidebar-link .sidebar-icon {
|
||||
display: inline-flex;
|
||||
|
|
@ -46,18 +53,19 @@
|
|||
transition: all 0.25s ease;
|
||||
}
|
||||
.sidebar-link:hover .sidebar-icon {
|
||||
background: rgba(20, 184, 166, 0.25);
|
||||
background: rgba(124, 58, 237, 0.25);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.sidebar-link.active .sidebar-icon {
|
||||
background: rgba(94, 234, 212, 0.15);
|
||||
background: rgba(124, 58, 237, 0.15);
|
||||
}
|
||||
.sidebar-section-label {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
color: #475569;
|
||||
/* Keep section labels readable on dark background */
|
||||
color: #94a3b8;
|
||||
padding: 0 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
|
@ -69,13 +77,13 @@
|
|||
.sidebar-brand-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, #0f766e 0%, #14b8a6 100%);
|
||||
background: linear-gradient(135deg, #7c3aed 0%, #6366f1 100%);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 4px 12px rgba(15, 118, 110, 0.3);
|
||||
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);
|
||||
}
|
||||
.sidebar-footer {
|
||||
border-top: 1px solid rgba(255,255,255,0.06);
|
||||
|
|
@ -101,7 +109,7 @@
|
|||
</button>
|
||||
<div>
|
||||
<h1 class="text-lg sm:text-xl md:text-2xl font-bold">📋 Panel Guru BK</h1>
|
||||
<p class="text-xs text-teal-100 font-semibold">Sistem Pemilihan Jurusan Kuliah</p>
|
||||
<p class="text-xs text-teal-100 font-semibold">Sistem Pemilihan Jurusan Politeknik Negeri Jember</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
|
|
@ -115,7 +123,7 @@
|
|||
<a href="{{ route('bk.profil') }}" class="block px-4 py-3 hover:bg-gray-50 text-xs sm:text-sm font-semibold border-b">
|
||||
👤 Profil Saya
|
||||
</a>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
<form method="POST" action="{{ route('logout') }}" class="confirm-logout">
|
||||
@csrf
|
||||
<button type="submit" class="block w-full text-left px-4 py-3 hover:bg-gray-50 text-xs sm:text-sm font-semibold text-red-600 rounded-b-lg">
|
||||
🚪 Logout
|
||||
|
|
@ -136,7 +144,7 @@
|
|||
<div class="sidebar-brand-icon">📋</div>
|
||||
<div>
|
||||
<p class="text-white font-bold text-sm leading-tight">SPK Jurusan</p>
|
||||
<p class="text-xs text-slate-400">Panel Guru BK</p>
|
||||
<p class="text-xs text-gray-300">Panel Guru BK</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -159,6 +167,9 @@
|
|||
|
||||
<p class="sidebar-section-label mt-5">Kelola</p>
|
||||
|
||||
<a href="{{ route('bk.alumni') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('bk.alumni*') ? 'active' : '' }}">
|
||||
<span class="sidebar-icon">🎓</span> Data Alumni
|
||||
</a>
|
||||
<a href="{{ route('bk.jurusan') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('bk.jurusan*') ? 'active' : '' }}">
|
||||
<span class="sidebar-icon">🏛️</span> Manajemen Jurusan
|
||||
</a>
|
||||
|
|
@ -170,8 +181,8 @@
|
|||
{{ strtoupper(substr(Auth::user()->name, 0, 1)) }}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-xs font-medium text-slate-300 truncate">{{ Auth::user()->name }}</p>
|
||||
<p class="text-xs text-slate-500">Guru BK</p>
|
||||
<p class="text-xs font-medium text-gray-200 truncate">{{ Auth::user()->name }}</p>
|
||||
<p class="text-xs text-gray-400">Guru BK</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -187,7 +198,7 @@
|
|||
<div class="sidebar-brand-icon" style="width:36px;height:36px;font-size:18px;">📋</div>
|
||||
<span class="font-bold text-white text-sm">Panel Guru BK</span>
|
||||
</div>
|
||||
<button id="closeMobileMenu" class="text-slate-400 hover:text-white transition text-xl">✕</button>
|
||||
<button id="closeMobileMenu" class="text-gray-400 hover:text-white transition text-xl">✕</button>
|
||||
</div>
|
||||
<nav class="px-3 py-3 space-y-1">
|
||||
<p class="sidebar-section-label mt-1">Menu Utama</p>
|
||||
|
|
@ -205,6 +216,9 @@
|
|||
</a>
|
||||
|
||||
<p class="sidebar-section-label mt-5">Kelola</p>
|
||||
<a href="{{ route('bk.alumni') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('bk.alumni*') ? 'active' : '' }}">
|
||||
<span class="sidebar-icon">🎓</span> Data Alumni
|
||||
</a>
|
||||
<a href="{{ route('bk.jurusan') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('bk.jurusan*') ? 'active' : '' }}">
|
||||
<span class="sidebar-icon">🏛️</span> Manajemen Jurusan
|
||||
</a>
|
||||
|
|
@ -258,6 +272,50 @@
|
|||
mobileOverlay.addEventListener('click', () => mobileSidebar.classList.add('hidden'));
|
||||
closeMobileMenu.addEventListener('click', () => mobileSidebar.classList.add('hidden'));
|
||||
</script>
|
||||
<!-- SweetAlert2 (BK) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.querySelectorAll('form.confirm-logout').forEach(function(form) {
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
Swal.fire({
|
||||
title: 'Konfirmasi Logout',
|
||||
text: 'Yakin ingin logout dari akun ini?',
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Ya, Logout',
|
||||
cancelButtonText: 'Batal',
|
||||
reverseButtons: true
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
// Generic SweetAlert2 confirmation for destructive actions in BK
|
||||
document.querySelectorAll('form.swal-confirm').forEach(function(form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const msg = form.getAttribute('data-confirm-message') || 'Yakin melanjutkan aksi ini?';
|
||||
Swal.fire({
|
||||
title: 'Konfirmasi',
|
||||
text: msg,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Ya',
|
||||
cancelButtonText: 'Batal',
|
||||
reverseButtons: true
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@yield('scripts')
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
@section('content')
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-bk">👤 Detail Siswa</h2>
|
||||
<h2 class="text-2xl font-bold text-maroon">👤 Detail Siswa</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ $student->name }}</p>
|
||||
</div>
|
||||
<a href="{{ route('bk.students') }}" class="bg-gray-400 text-white font-bold py-2 px-4 rounded-lg hover:bg-gray-500 transition text-sm">
|
||||
|
|
@ -13,120 +13,132 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Profile Card -->
|
||||
<div class="bg-white rounded-lg shadow-lg p-6 mb-6 border-l-4 border-teal-500">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">Nama</p>
|
||||
<p class="text-xl font-bold text-bk mt-1">{{ $student->name }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">Email</p>
|
||||
<p class="text-gray-800 mt-1">{{ $student->email }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">NIS</p>
|
||||
<p class="text-gray-800 font-semibold mt-1">{{ $student->nis ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">Kelompok Asal</p>
|
||||
@if($student->kelompok_asal)
|
||||
<p class="mt-1">
|
||||
<span class="px-3 py-1 rounded text-sm font-bold" style="{{ $student->kelompok_asal == 'IPA' ? 'background-color: #E0F2FE; color: #0369A1;' : 'background-color: #FEF3C7; color: #92400E;' }}">
|
||||
{{ $student->kelompok_asal }}
|
||||
</span>
|
||||
</p>
|
||||
@else
|
||||
<p class="text-gray-500 mt-1">-</p>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">Foto Profil</p>
|
||||
<!-- Profile Header Card -->
|
||||
<div class="bg-white rounded-lg shadow-lg p-6 mb-6 border-l-4 border-maroon">
|
||||
<div class="flex flex-col md:flex-row gap-6 items-start">
|
||||
<!-- Avatar Section -->
|
||||
<div class="flex-shrink-0">
|
||||
@if($student->foto)
|
||||
<img src="{{ Storage::url($student->foto) }}" alt="{{ $student->name }}" class="w-24 h-24 rounded-lg object-cover mt-2 border-2 border-teal-500">
|
||||
<img src="{{ Storage::url($student->foto) }}" alt="{{ $student->name }}" class="w-24 h-24 md:w-32 md:h-32 rounded-lg object-cover border-4 border-maroon shadow-md">
|
||||
@else
|
||||
<p class="text-gray-500 mt-1">-</p>
|
||||
<div class="w-24 h-24 md:w-32 md:h-32 rounded-lg bg-gradient-to-br from-maroon to-teal-600 flex items-center justify-center text-white text-2xl md:text-3xl font-bold shadow-md">
|
||||
{{ strtoupper(substr($student->name, 0, 1)) }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">Terdaftar</p>
|
||||
<p class="text-gray-800 mt-1">{{ $student->created_at->format('d M Y H:i') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rekomendasi -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-green-400">
|
||||
<h3 class="text-lg font-bold text-bk mb-4">🎯 Riwayat Rekomendasi ({{ count($recommendations) }})</h3>
|
||||
<!-- Info Section -->
|
||||
<div class="flex-1">
|
||||
<h3 class="text-2xl md:text-3xl font-bold text-maroon mb-4">{{ $student->name }}</h3>
|
||||
|
||||
@if($recommendations->isNotEmpty())
|
||||
<div class="space-y-4">
|
||||
@foreach($recommendations as $rec)
|
||||
<div class="border border-gray-200 rounded-lg p-4 hover:bg-gray-50 transition">
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<p class="text-sm text-gray-500">{{ $rec->created_at->format('d M Y H:i') }}</p>
|
||||
<span class="px-2 py-1 rounded text-xs font-bold bg-teal-100 text-teal-800">Rekomendasi #{{ $loop->index + 1 }}</span>
|
||||
</div>
|
||||
|
||||
@if($rec->hasil_rekomendasi && is_array($rec->hasil_rekomendasi))
|
||||
<div class="mt-3">
|
||||
<p class="text-xs font-semibold text-gray-600 mb-2">Top 3 Rekomendasi:</p>
|
||||
<div class="space-y-2">
|
||||
@foreach(array_slice($rec->hasil_rekomendasi, 0, 3) as $idx => $hasil)
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-700">{{ $idx + 1 }}. {{ $hasil['jurusan'] ?? 'N/A' }}</span>
|
||||
@php $skorVal = $hasil['skor'] ?? 0; @endphp
|
||||
<span class="px-2 py-1 rounded text-xs font-bold" style="background-color: {{ $idx === 0 ? '#CCFBF1' : '#F3F4F6' }}; color: {{ $idx === 0 ? '#0f766e' : '#6B7280' }};">
|
||||
{{ round(($skorVal > 1 ? $skorVal : $skorVal * 100), 1) }}%
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500">NIS</p>
|
||||
<p class="text-lg font-bold text-gray-800">{{ $student->nis ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500">Kelompok</p>
|
||||
@if($student->kelompok_asal)
|
||||
<span class="inline-block px-3 py-1 rounded text-sm font-bold mt-1" style="{{ $student->kelompok_asal == 'IPA' ? 'background-color: #E0F2FE; color: #0369A1;' : 'background-color: #FEF3C7; color: #92400E;' }}">
|
||||
{{ $student->kelompok_asal }}
|
||||
</span>
|
||||
@else
|
||||
<p class="text-lg font-bold text-gray-500">-</p>
|
||||
@endif
|
||||
|
||||
<div class="mt-3 p-3 bg-gray-50 rounded text-xs text-gray-600">
|
||||
<p><strong>Minat:</strong> {{ $rec->minat ?? '-' }} | <strong>Cita-cita:</strong> {{ $rec->cita_cita ?? '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500">Email</p>
|
||||
<p class="text-sm font-semibold text-gray-800 break-all">{{ $student->email }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500">Terdaftar</p>
|
||||
<p class="text-sm font-semibold text-gray-800">{{ $student->created_at->format('d M Y') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-gray-500 text-sm">Siswa belum melakukan rekomendasi</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat History -->
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-blue-400">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-bold text-bk">💬 Chat History ({{ count($chatHistories) }})</h3>
|
||||
@if(count($chatHistories) > 0)
|
||||
<a href="{{ route('bk.student.chat', $student->id) }}" class="bg-teal-500 text-white font-semibold py-2 px-3 rounded-lg hover:bg-teal-600 transition text-xs">
|
||||
Lihat Semua →
|
||||
</a>
|
||||
@endif
|
||||
<!-- Main Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
|
||||
<!-- Left Column: Rekomendasi (2/3 width) -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-green-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">🎯 Riwayat Rekomendasi ({{ count($recommendations) }})</h3>
|
||||
|
||||
@if($recommendations->isNotEmpty())
|
||||
<div class="space-y-4">
|
||||
@foreach($recommendations as $rec)
|
||||
<div class="border-2 border-green-100 rounded-lg p-4 hover:shadow-md hover:border-green-400 transition">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<p class="text-xs font-semibold text-gray-500">{{ $rec->created_at->format('d M Y H:i') }}</p>
|
||||
<span class="px-2 py-1 rounded-full text-xs font-bold bg-green-100 text-green-700">Rekomendasi #{{ $loop->index + 1 }}</span>
|
||||
</div>
|
||||
|
||||
@if($rec->hasil_rekomendasi && is_array($rec->hasil_rekomendasi))
|
||||
<div class="space-y-2">
|
||||
@foreach(array_slice($rec->hasil_rekomendasi, 0, 3) as $idx => $hasil)
|
||||
<div class="flex items-center justify-between bg-gradient-to-r {{ $idx === 0 ? 'from-green-50 to-transparent' : 'from-gray-50 to-transparent' }} p-3 rounded">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full {{ $idx === 0 ? 'bg-green-500 text-white' : 'bg-gray-300 text-white' }} text-xs font-bold">
|
||||
{{ $idx + 1 }}
|
||||
</span>
|
||||
<span class="text-sm font-semibold text-gray-800">{{ $hasil['jurusan'] ?? 'N/A' }}</span>
|
||||
</div>
|
||||
@php $skorVal = $hasil['skor'] ?? 0; @endphp
|
||||
<span class="px-3 py-1 rounded-full text-xs font-bold {{ $idx === 0 ? 'bg-green-200 text-green-800' : 'bg-gray-200 text-gray-800' }}">
|
||||
{{ round(($skorVal > 1 ? $skorVal : $skorVal * 100), 1) }}%
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if(isset($rec->minat) || isset($rec->cita_cita))
|
||||
<div class="mt-3 p-3 bg-blue-50 border-l-4 border-blue-300 rounded">
|
||||
<p class="text-xs text-gray-700"><strong>📝 Minat:</strong> {{ $rec->minat ?? '-' }} | <strong>🎓 Cita-cita:</strong> {{ $rec->cita_cita ?? '-' }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<p class="text-sm">Siswa belum melakukan rekomendasi</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($chatHistories->isNotEmpty())
|
||||
<div class="space-y-3 max-h-96 overflow-y-auto">
|
||||
@foreach($chatHistories as $chat)
|
||||
<div class="border-b pb-3 last:border-b-0">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<p class="text-xs font-semibold text-gray-600">{{ $chat->created_at->format('d M Y H:i') }}</p>
|
||||
</div>
|
||||
<div class="bg-blue-50 border-l-4 border-blue-400 p-3 rounded mb-2">
|
||||
<p class="text-xs font-semibold text-gray-700 mb-1">👤 Pertanyaan Siswa:</p>
|
||||
<p class="text-sm text-gray-800">{{ \Illuminate\Support\Str::limit($chat->prompt, 150) }}</p>
|
||||
</div>
|
||||
<div class="bg-green-50 border-l-4 border-green-400 p-3 rounded">
|
||||
<p class="text-xs font-semibold text-gray-700 mb-1">🤖 Jawaban AI:</p>
|
||||
<p class="text-sm text-gray-800">{{ \Illuminate\Support\Str::limit($chat->response, 150) }}</p>
|
||||
</div>
|
||||
<!-- Right Column: Chat History (1/3 width) -->
|
||||
<div class="lg:col-span-1">
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-blue-400 sticky top-20">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-bold text-maroon">💬 Chat</h3>
|
||||
<span class="text-xs font-bold bg-blue-100 text-blue-700 px-2 py-1 rounded-full">{{ count($chatHistories) }}</span>
|
||||
</div>
|
||||
|
||||
@if($chatHistories->isNotEmpty())
|
||||
<div class="space-y-3 max-h-96 overflow-y-auto pr-2">
|
||||
@foreach($chatHistories->take(5) as $chat)
|
||||
<div class="border border-blue-100 rounded-lg p-3 hover:bg-blue-50 transition">
|
||||
<p class="text-xs font-semibold text-gray-500 mb-2">{{ $chat->created_at->format('d M Y') }}</p>
|
||||
<p class="text-xs text-gray-700 mb-2"><strong>Q:</strong> {{ \Illuminate\Support\Str::limit($chat->prompt, 80) }}</p>
|
||||
<p class="text-xs text-gray-600"><strong>A:</strong> {{ \Illuminate\Support\Str::limit($chat->response, 80) }}</p>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
@if(count($chatHistories) > 5)
|
||||
<a href="{{ route('bk.student.chat', $student->id) }}" class="block mt-3 text-center text-xs text-blue-600 font-semibold hover:text-blue-800">
|
||||
Lihat Semua ({{ count($chatHistories) }}) →
|
||||
</a>
|
||||
@endif
|
||||
@else
|
||||
<p class="text-gray-500 text-xs text-center py-4">Belum ada chat</p>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<p class="text-gray-500 text-sm">Siswa belum melakukan chat dengan AI</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
@extends('bk.layouts.app')
|
||||
|
||||
@section('title', 'Data Siswa')
|
||||
@section('title', 'Manajemen Data Siswa')
|
||||
|
||||
@section('content')
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-bk">👥 Data Siswa</h2>
|
||||
<h2 class="text-2xl font-bold text-maroon">👥 Manajemen Data Siswa</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">Total: {{ $students->total() }} Siswa</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search & Filter -->
|
||||
<div class="bg-white rounded-lg shadow p-4 mb-6 border-l-4 border-teal-500">
|
||||
<div class="bg-white rounded-lg shadow p-4 mb-6 border-l-4 border-maroon">
|
||||
<form method="GET" class="flex gap-3 flex-col sm:flex-row">
|
||||
<input type="text" name="search" placeholder="Cari nama atau NIS..." class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-400" value="{{ request('search') }}">
|
||||
<select name="kelompok" class="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-400">
|
||||
<input type="text" name="search" placeholder="Cari nama atau NIS..." class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-maroon" value="{{ request('search') }}">
|
||||
<select name="kelompok" class="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-maroon">
|
||||
<option value="">-- Semua Kelompok --</option>
|
||||
<option value="IPA" {{ request('kelompok') == 'IPA' ? 'selected' : '' }}>IPA</option>
|
||||
<option value="IPS" {{ request('kelompok') == 'IPS' ? 'selected' : '' }}>IPS</option>
|
||||
</select>
|
||||
<button type="submit" class="gradient-bk text-white font-bold px-6 py-2 rounded-lg hover:opacity-90 transition">
|
||||
<button type="submit" class="gradient-maroon text-white font-bold px-6 py-2 rounded-lg hover:opacity-90 transition">
|
||||
🔍 Cari
|
||||
</button>
|
||||
@if(request('search') || request('kelompok'))
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
<!-- Students Table -->
|
||||
<div class="bg-white rounded-lg shadow overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="gradient-bk text-white">
|
||||
<thead class="gradient-maroon text-white">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left">Nama</th>
|
||||
<th class="px-4 py-3 text-left">Email</th>
|
||||
|
|
@ -70,7 +70,7 @@
|
|||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<a href="{{ route('bk.student.detail', $student->id) }}" class="text-teal-600 hover:text-teal-800 font-semibold text-xs">👁 Lihat</a>
|
||||
<a href="{{ route('bk.student.detail', $student->id) }}" class="text-blue-600 hover:text-blue-800 font-semibold text-xs">👁 Lihat</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@
|
|||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.gradient-maroon {
|
||||
background: linear-gradient(135deg, #5B7B89 0%, #7B9BA5 100%);
|
||||
background: linear-gradient(135deg, #6B7280 0%, #8B95A5 100%);
|
||||
}
|
||||
.text-maroon {
|
||||
color: #5B7B89;
|
||||
color: #6B7280;
|
||||
}
|
||||
.border-maroon {
|
||||
border-color: #5B7B89;
|
||||
border-color: #6B7280;
|
||||
}
|
||||
.bg-cream {
|
||||
background-color: #F8FAFC;
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
.chat-container {
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
}}
|
||||
@media (min-width: 1024px) {
|
||||
.chat-container {
|
||||
height: 600px;
|
||||
|
|
@ -73,10 +73,26 @@
|
|||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
.input-error {
|
||||
border-color: #ef4444 !important;
|
||||
background-color: #fef2f2 !important;
|
||||
}
|
||||
.error-message {
|
||||
color: #dc2626;
|
||||
font-size: 13px;
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
.error-message::before {
|
||||
content: "⚠️";
|
||||
}
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
|
|
@ -91,10 +107,10 @@
|
|||
<div class="container mx-auto px-4 sm:px-6 py-4 sm:py-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4">
|
||||
<div>
|
||||
<h1 class="text-xl sm:text-2xl md:text-3xl font-bold">Konsultasi BK Virtual</h1>
|
||||
<p class="text-xs sm:text-sm text-yellow-300 font-semibold mt-1">Konseling Pemilihan Jurusan Kuliah</p>
|
||||
<p class="text-xs sm:text-sm text-yellow-300 font-semibold mt-1">Konseling Pemilihan Jurusan Politeknik Negeri Jember</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
|
||||
<a href="{{ route('chatbot.index') }}" class="block sm:inline-block flex-1 sm:flex-none text-center bg-white text-maroon font-bold py-2 px-3 sm:px-4 rounded-lg hover:bg-gray-100 transition text-xs sm:text-sm">
|
||||
<a href="{{ url('/chatbot?new=1') }}" class="block sm:inline-block flex-1 sm:flex-none text-center bg-white text-maroon font-bold py-2 px-3 sm:px-4 rounded-lg hover:bg-gray-100 transition text-xs sm:text-sm">
|
||||
Sesi Baru
|
||||
</a>
|
||||
<a href="{{ url('/dashboard') }}" class="block sm:inline-block flex-1 sm:flex-none text-center bg-yellow-400 text-maroon font-bold py-2 px-3 sm:px-4 rounded-lg hover:bg-yellow-300 transition text-xs sm:text-sm">
|
||||
|
|
@ -145,7 +161,7 @@
|
|||
<li class="text-gray-700">• Aku bingung pilih jurusan</li>
|
||||
<li class="text-gray-700">• Skill apa yang dibutuhkan?</li>
|
||||
<li class="text-gray-700">• Bedanya IPA dan IPS?</li>
|
||||
<li class="text-gray-700">• Tips sukses di kampus?</li>
|
||||
<li class="text-gray-700">• Tips sukses kuliah di Polije?</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -165,28 +181,63 @@
|
|||
|
||||
<!-- Input Area -->
|
||||
<div class="border-t border-gray-200 pt-3 sm:pt-4">
|
||||
<form id="chatForm" class="flex gap-2">
|
||||
<form id="chatForm" class="space-y-2">
|
||||
@csrf
|
||||
<input
|
||||
type="text"
|
||||
id="messageInput"
|
||||
name="message"
|
||||
placeholder="Ketik pertanyaan..."
|
||||
class="flex-1 px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
||||
required
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
class="gradient-maroon text-white font-bold py-2 px-4 sm:px-6 rounded-lg hover:opacity-90 transition text-sm sm:text-base whitespace-nowrap"
|
||||
id="sendBtn"
|
||||
>
|
||||
Kirim
|
||||
</button>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1 relative">
|
||||
<input
|
||||
type="text"
|
||||
id="messageInput"
|
||||
name="message"
|
||||
placeholder="Ketik pertanyaan..."
|
||||
class="w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm transition-colors"
|
||||
autocomplete="off"
|
||||
>
|
||||
<div id="errorMessage" class="error-message hidden"></div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="gradient-maroon text-white font-bold py-2 px-4 sm:px-6 rounded-lg hover:opacity-90 transition text-sm sm:text-base whitespace-nowrap disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
id="sendBtn"
|
||||
>
|
||||
Kirim
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History Chat Section -->
|
||||
@php
|
||||
$chatHistories = $chatHistories ?? [];
|
||||
@endphp
|
||||
|
||||
@if(count($chatHistories) > 0)
|
||||
<div class="mt-8 sm:mt-12">
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 border-l-4 border-orange-500">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="w-12 h-12 sm:w-14 sm:h-14 rounded-lg bg-orange-100 flex items-center justify-center text-2xl flex-shrink-0">💬</div>
|
||||
<div>
|
||||
<h2 class="text-lg sm:text-xl md:text-2xl font-bold text-maroon">Riwayat Chat</h2>
|
||||
<p class="text-xs sm:text-sm text-gray-600">Percakapan Anda dengan AI sebelumnya</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 sm:space-y-4 max-h-96 overflow-y-auto">
|
||||
@foreach($chatHistories as $chat)
|
||||
<a href="{{ route('chatbot.index', ['session' => $chat->id_sesi]) }}" class="block border border-gray-200 rounded-lg p-4 hover:shadow-md hover:border-maroon transition">
|
||||
<p class="text-xs sm:text-sm text-gray-500 mb-2">{{ $chat->created_at->format('d M Y - H:i') }}</p>
|
||||
<p class="text-xs sm:text-sm text-gray-700 line-clamp-2 leading-relaxed">
|
||||
<span class="font-semibold">Anda:</span> {{ $chat->prompt ?? 'Tidak ada pesan' }}
|
||||
</p>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
|
@ -218,7 +269,7 @@ class="gradient-maroon text-white font-bold py-2 px-4 sm:px-6 rounded-lg hover:o
|
|||
// Melanjutkan sesi lama — tampilkan info lanjutan
|
||||
document.getElementById('initialGreeting').textContent = sapaan + '. Anda melanjutkan sesi konsultasi sebelumnya. Silakan lanjutkan pertanyaan Anda.';
|
||||
} else {
|
||||
document.getElementById('initialGreeting').textContent = sapaan + '. Saya adalah konselor BK virtual SMA Bima Ambulu. Saya siap membantu Anda dalam pemilihan jurusan kuliah, informasi prospek karier, maupun konsultasi lainnya terkait pendidikan tinggi. Silakan sampaikan pertanyaan Anda.';
|
||||
document.getElementById('initialGreeting').textContent = sapaan + '. Saya adalah konselor BK virtual SMA Bima Ambulu. Saya siap membantu Anda dalam pemilihan jurusan Politeknik Negeri Jember, informasi prospek karier, maupun konsultasi lanjutan studi. Silakan sampaikan pertanyaan Anda.';
|
||||
}
|
||||
})();
|
||||
|
||||
|
|
@ -226,10 +277,47 @@ class="gradient-maroon text-white font-bold py-2 px-4 sm:px-6 rounded-lg hover:o
|
|||
const messageInput = document.getElementById('messageInput');
|
||||
const chatContainer = document.getElementById('chatContainer');
|
||||
const sendBtn = document.getElementById('sendBtn');
|
||||
const errorMessage = document.getElementById('errorMessage');
|
||||
|
||||
// Track conversation history for multi-turn context
|
||||
let conversationHistory = [];
|
||||
|
||||
// Validasi input saat user mengetik
|
||||
messageInput.addEventListener('input', function() {
|
||||
const message = this.value.trim();
|
||||
|
||||
if (message === '') {
|
||||
// Show error state
|
||||
this.classList.add('input-error');
|
||||
errorMessage.textContent = 'Pesan tidak boleh kosong';
|
||||
errorMessage.classList.remove('hidden');
|
||||
sendBtn.disabled = true;
|
||||
} else {
|
||||
// Clear error state
|
||||
this.classList.remove('input-error');
|
||||
errorMessage.classList.add('hidden');
|
||||
sendBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Validasi saat blur
|
||||
messageInput.addEventListener('blur', function() {
|
||||
if (this.value.trim() === '') {
|
||||
this.classList.add('input-error');
|
||||
errorMessage.textContent = 'Pesan tidak boleh kosong';
|
||||
errorMessage.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Clear error saat focus
|
||||
messageInput.addEventListener('focus', function() {
|
||||
if (this.value.trim() === '') {
|
||||
this.classList.add('input-error');
|
||||
errorMessage.textContent = 'Ketik sesuatu untuk melanjutkan';
|
||||
errorMessage.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Load previous messages if resuming session
|
||||
if (previousMessages.length > 0) {
|
||||
previousMessages.forEach(function(msg) {
|
||||
|
|
@ -238,11 +326,26 @@ class="gradient-maroon text-white font-bold py-2 px-4 sm:px-6 rounded-lg hover:o
|
|||
});
|
||||
}
|
||||
|
||||
// Initialize button state
|
||||
sendBtn.disabled = true;
|
||||
|
||||
chatForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const message = messageInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
// Validasi pesan tidak kosong
|
||||
if (!message) {
|
||||
messageInput.classList.add('input-error');
|
||||
errorMessage.textContent = 'Pesan tidak boleh kosong, ketik sesuatu';
|
||||
errorMessage.classList.remove('hidden');
|
||||
messageInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear error sebelum mengirim
|
||||
messageInput.classList.remove('input-error');
|
||||
errorMessage.classList.add('hidden');
|
||||
|
||||
// Add user message to UI
|
||||
addMessage(message, 'user');
|
||||
|
|
@ -306,8 +409,10 @@ class="gradient-maroon text-white font-bold py-2 px-4 sm:px-6 rounded-lg hover:o
|
|||
} catch (error) {
|
||||
addMessage('Terjadi kesalahan koneksi. Silakan coba lagi.', 'ai');
|
||||
} finally {
|
||||
sendBtn.disabled = false;
|
||||
sendBtn.textContent = 'Kirim';
|
||||
// Re-enable button only if input has text
|
||||
const currentMessage = messageInput.value.trim();
|
||||
sendBtn.disabled = currentMessage === '';
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@
|
|||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.gradient-maroon {
|
||||
background: linear-gradient(135deg, #5B7B89 0%, #7B9BA5 100%);
|
||||
background: linear-gradient(135deg, #6B7280 0%, #8B95A5 100%);
|
||||
}
|
||||
.text-maroon {
|
||||
color: #5B7B89;
|
||||
color: #6B7280;
|
||||
}
|
||||
.border-maroon {
|
||||
border-color: #5B7B89;
|
||||
border-color: #6B7280;
|
||||
}
|
||||
.bg-cream {
|
||||
background-color: #F8FAFC;
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
}
|
||||
.card-hover:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 30px rgba(107, 44, 44, 0.2);
|
||||
box-shadow: 0 10px 30px rgba(107, 114, 128, 0.15);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
<a href="{{ route('profile.edit') }}" class="block px-4 py-3 hover:bg-gray-50 text-xs sm:text-sm font-semibold border-b border-gray-100 rounded-t-lg">
|
||||
👤 Lihat Profil
|
||||
</a>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
<form method="POST" action="{{ route('logout') }}" class="confirm-logout">
|
||||
@csrf
|
||||
<button type="submit" class="block w-full text-left px-4 py-3 hover:bg-gray-50 text-xs sm:text-sm font-semibold text-red-600 rounded-b-lg">
|
||||
🚪 Logout
|
||||
|
|
@ -64,228 +64,173 @@
|
|||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="container mx-auto px-4 sm:px-6 py-6 sm:py-12">
|
||||
<!-- Info Box -->
|
||||
<div class="bg-white border-2 border-maroon rounded-lg p-4 sm:p-6 mb-6 sm:mb-8 shadow-md">
|
||||
<h2 class="text-lg sm:text-xl md:text-2xl font-bold text-maroon mb-2 sm:mb-3">Selamat Datang di Sistem Pemilihan Jurusan</h2>
|
||||
<p class="text-xs sm:text-sm md:text-base text-gray-700 mb-3 sm:mb-4">
|
||||
Memilih jurusan adalah keputusan penting yang akan mempengaruhi karir dan masa depan Anda. Sistem ini dirancang untuk membantu Anda menemukan jurusan kuliah yang paling sesuai dengan profil akademik, minat, gaya belajar, prestasi, dan cita-cita Anda.
|
||||
<div class="w-full px-4 sm:px-6 py-6 sm:py-8">
|
||||
<!-- Hero Section -->
|
||||
<div class="bg-gradient-to-r from-purple-600 to-indigo-600 rounded-xl shadow-xl p-8 sm:p-10 mb-8 text-white">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3">Temukan Jurusan Impianmu 🎯</h2>
|
||||
<p class="text-purple-100 text-lg mb-4 max-w-3xl">
|
||||
Memilih jurusan adalah salah satu keputusan terpenting dalam hidup. Kami ada di sini untuk membantu kamu menemukan program studi yang benar-benar sesuai dengan potensi, minat, dan impian karirmu. Dengan teknologi AI dan analisis mendalam, kami siap memberikan panduan terbaik.
|
||||
</p>
|
||||
|
||||
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-3 sm:p-4 mb-4 rounded">
|
||||
<p class="text-xs sm:text-sm md:text-base text-gray-800">
|
||||
<strong>Bagaimana Sistem Ini Bekerja?</strong> Kami menganalisis 5 faktor utama dalam diri Anda: nilai akademik (40%), minat dan passion (35%), preferensi gaya belajar (15%), prestasi dan pencapaian (5%), serta cita-cita dan rencana karir (5%). Dari analisis mendalam tersebut, sistem memberikan ranking 9 jurusan yang tersedia berdasarkan kesesuaian dengan profil Anda.
|
||||
</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-6">
|
||||
<div class="bg-white bg-opacity-20 rounded-lg p-4 backdrop-blur-sm">
|
||||
<p class="text-3xl font-bold">9</p>
|
||||
<p class="text-purple-100 text-sm">Jurusan</p>
|
||||
</div>
|
||||
<div class="bg-white bg-opacity-20 rounded-lg p-4 backdrop-blur-sm">
|
||||
<p class="text-3xl font-bold">24/7</p>
|
||||
<p class="text-purple-100 text-sm">Konsultasi AI</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs sm:text-sm md:text-base text-gray-700 mb-3 sm:mb-4">
|
||||
<strong>Fitur-Fitur yang Tersedia:</strong>
|
||||
</p>
|
||||
<ul class="text-xs sm:text-sm md:text-base text-gray-700 space-y-2 mb-4">
|
||||
<li class="flex gap-2">
|
||||
<span class="text-maroon">✓</span>
|
||||
<span><strong>Analisis Rekomendasi:</strong> Isi kuesioner singkat dan dapatkan rekomendasi 9 jurusan yang disesuaikan dengan profil Anda</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="text-maroon">✓</span>
|
||||
<span><strong>Konsultasi dengan AI:</strong> Chat dengan konselor BK virtual yang siap menjawab pertanyaan tentang jurusan, prospek karir, dan tips sukses kuliah</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="text-maroon">✓</span>
|
||||
<span><strong>Riwayat Analisis:</strong> Lihat kembali semua analisis dan chat history Anda kapan saja untuk referensi</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="text-maroon">✓</span>
|
||||
<span><strong>Profil Pribadi:</strong> Kelola data diri, foto profil, dan informasi akademik Anda</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p class="text-xs sm:text-sm md:text-base text-gray-700 text-italic">
|
||||
💡 <strong>Tips:</strong> Untuk hasil yang akurat, jawab semua pertanyaan dengan jujur dan detail. Semakin detail profil Anda, semakin akurat rekomendasi yang kami berikan.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Main Actions Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-8 mb-6 sm:mb-12">
|
||||
<!-- Quick Action Cards -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
<!-- Rekomendasi Card -->
|
||||
<div class="card-hover bg-white rounded-lg shadow-lg p-5 sm:p-8 border-l-4 border-blue-500 flex flex-col h-full">
|
||||
<div class="flex items-start gap-3 sm:gap-4 mb-3 sm:mb-4 flex-grow">
|
||||
<div class="text-3xl sm:text-4xl flex-shrink-0">📊</div>
|
||||
<div class="card-hover bg-gradient-to-br from-blue-50 to-blue-100 rounded-xl shadow-lg p-8 border-2 border-blue-300 hover:border-blue-400 transition">
|
||||
<div class="flex items-start gap-4 mb-6">
|
||||
<div class="w-16 h-16 bg-blue-500 rounded-lg flex items-center justify-center text-white text-3xl shadow-md">📊</div>
|
||||
<div>
|
||||
<h3 class="text-lg sm:text-lg md:text-2xl font-bold text-maroon mb-1 sm:mb-2">Analisis Rekomendasi</h3>
|
||||
<p class="text-xs sm:text-sm md:text-base text-gray-700">
|
||||
Isi formulir singkat tentang profil Anda. Sistem akan menganalisis dan memberikan rekomendasi dari 9 jurusan yang tersedia.
|
||||
</p>
|
||||
<h3 class="text-2xl font-bold text-blue-900 mb-1">Cari Jurusan Terbaikmu</h3>
|
||||
<p class="text-blue-700">Rekomendasi Khusus</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-6">
|
||||
<a href="{{ url('/rekomendasi') }}" class="block w-full text-center bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 sm:py-3 px-4 sm:px-6 rounded-lg transition duration-200 text-sm sm:text-base">
|
||||
Mulai Analisis Baru
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-3 sm:mt-4 p-2 sm:p-3 bg-blue-50 rounded-lg">
|
||||
<p class="text-xs sm:text-sm text-blue-800">
|
||||
<strong>Durasi:</strong> 3-5 menit | <strong>Metode:</strong> AI Analysis
|
||||
</p>
|
||||
<p class="text-blue-800 mb-6 leading-relaxed">
|
||||
Jawab beberapa pertanyaan tentang nilai akademik, minat, gaya belajar, dan cita-cita kamu. Sistem kami akan menganalisis profil lengkap kamu dan memberikan rekomendasi jurusan yang paling cocok, lengkap dengan penjelasan alasan kesesuaiannya.
|
||||
</p>
|
||||
<div class="flex items-center justify-between bg-white rounded-lg p-4 mb-6">
|
||||
<span class="text-sm font-semibold text-gray-600">⏱️ Waktu: 3-5 menit</span>
|
||||
<span class="text-sm font-semibold text-blue-600">📈 Akurasi Tinggi</span>
|
||||
</div>
|
||||
<a href="{{ url('/rekomendasi') }}" class="block w-full text-center bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-bold py-3 px-6 rounded-lg transition duration-300 shadow-md transform hover:scale-105">
|
||||
Mulai Analisis →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Chatbot Card -->
|
||||
<div class="card-hover bg-white rounded-lg shadow-lg p-5 sm:p-8 border-l-4 border-green-500 flex flex-col h-full">
|
||||
<div class="flex items-start gap-3 sm:gap-4 mb-3 sm:mb-4 flex-grow">
|
||||
<div class="text-3xl sm:text-4xl flex-shrink-0">💬</div>
|
||||
<div class="card-hover bg-gradient-to-br from-green-50 to-green-100 rounded-xl shadow-lg p-8 border-2 border-green-300 hover:border-green-400 transition">
|
||||
<div class="flex items-start gap-4 mb-6">
|
||||
<div class="w-16 h-16 bg-green-500 rounded-lg flex items-center justify-center text-white text-3xl shadow-md">💬</div>
|
||||
<div>
|
||||
<h3 class="text-lg sm:text-lg md:text-2xl font-bold text-maroon mb-1 sm:mb-2">Chat dengan AI</h3>
|
||||
<p class="text-xs sm:text-sm md:text-base text-gray-700">
|
||||
Tanya jawab tentang jurusan, kurikulum, prospek karir, dan pertanyaan seputar pemilihan jurusan.
|
||||
</p>
|
||||
<h3 class="text-2xl font-bold text-green-900 mb-1">Tanya Jawab dengan AI</h3>
|
||||
<p class="text-green-700">Konsultan Karir Virtual</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-6">
|
||||
<a href="{{ url('/chatbot') }}" class="block w-full text-center bg-green-600 hover:bg-green-700 text-white font-bold py-2 sm:py-3 px-4 sm:px-6 rounded-lg transition duration-200 text-sm sm:text-base">
|
||||
Mulai Chat
|
||||
</a>
|
||||
<p class="text-green-800 mb-6 leading-relaxed">
|
||||
Ada pertanyaan tentang jurusan tertentu? Penasaran dengan prospek karir? Atau butuh tips sukses masuk kuliah? Chat dengan konselor AI kami yang siap membantu kapan saja. Dapatkan jawaban detail dan rekomendasi personal untuk setiap pertanyaan kamu.
|
||||
</p>
|
||||
<div class="flex items-center justify-between bg-white rounded-lg p-4 mb-6">
|
||||
<span class="text-sm font-semibold text-gray-600">⭐ Respons Cepat</span>
|
||||
<span class="text-sm font-semibold text-green-600">🤖 AI Powered</span>
|
||||
</div>
|
||||
<div class="mt-3 sm:mt-4 p-2 sm:p-3 bg-green-50 rounded-lg">
|
||||
<p class="text-xs sm:text-sm text-green-800">
|
||||
<strong>Fitur:</strong> Tanya jawab dengan AI
|
||||
</p>
|
||||
<a href="{{ url('/chatbot?new=1') }}" class="block w-full text-center bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 text-white font-bold py-3 px-6 rounded-lg transition duration-300 shadow-md transform hover:scale-105">
|
||||
Mulai Konsultasi →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features Grid -->
|
||||
<div class="bg-white rounded-xl shadow-lg p-8 mb-8">
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-8">Fitur Lengkap untuk Pendampinganmu</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div class="text-center p-6 rounded-lg bg-gradient-to-br from-purple-50 to-purple-100 hover:shadow-md transition">
|
||||
<div class="text-4xl mb-3">📋</div>
|
||||
<h4 class="font-bold text-gray-900 mb-2">Rekomendasi Personal</h4>
|
||||
<p class="text-sm text-gray-700">Analisis mendalam berdasarkan profil unik kamu</p>
|
||||
</div>
|
||||
<div class="text-center p-6 rounded-lg bg-gradient-to-br from-blue-50 to-blue-100 hover:shadow-md transition">
|
||||
<div class="text-4xl mb-3">💬</div>
|
||||
<h4 class="font-bold text-gray-900 mb-2">Chat 24/7</h4>
|
||||
<p class="text-sm text-gray-700">Konsultasi kapan saja dengan AI konselor</p>
|
||||
</div>
|
||||
<div class="text-center p-6 rounded-lg bg-gradient-to-br from-green-50 to-green-100 hover:shadow-md transition">
|
||||
<div class="text-4xl mb-3">📚</div>
|
||||
<h4 class="font-bold text-gray-900 mb-2">Info Jurusan</h4>
|
||||
<p class="text-sm text-gray-700">Detail lengkap tentang setiap program studi</p>
|
||||
</div>
|
||||
<div class="text-center p-6 rounded-lg bg-gradient-to-br from-orange-50 to-orange-100 hover:shadow-md transition">
|
||||
<div class="text-4xl mb-3">📊</div>
|
||||
<h4 class="font-bold text-gray-900 mb-2">Riwayat</h4>
|
||||
<p class="text-sm text-gray-700">Simpan & bandingkan hasil analisis sebelumnya</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-8 mb-8 sm:mb-16">
|
||||
<!-- History Rekomendasi Card -->
|
||||
<div class="card-hover bg-white rounded-lg shadow-lg p-5 sm:p-8 border-l-4 border-purple-500 flex flex-col h-full">
|
||||
<div class="flex items-start gap-3 sm:gap-4 mb-3 sm:mb-4 flex-grow">
|
||||
<div class="text-3xl sm:text-4xl flex-shrink-0">📋</div>
|
||||
<div>
|
||||
<h3 class="text-lg sm:text-lg md:text-2xl font-bold text-maroon mb-1 sm:mb-2">History Rekomendasi</h3>
|
||||
<p class="text-xs sm:text-sm md:text-base text-gray-700">
|
||||
Lihat semua hasil analisis rekomendasi yang telah Anda lakukan sebelumnya.
|
||||
</p>
|
||||
</div>
|
||||
<!-- Process Section -->
|
||||
<div class="bg-gradient-to-r from-indigo-600 to-purple-600 rounded-xl p-8 text-white mb-8">
|
||||
<h3 class="text-2xl font-bold mb-8">Bagaimana Cara Kerjanya? 🔍</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<div class="text-center">
|
||||
<div class="w-12 h-12 rounded-full bg-white text-indigo-600 font-bold text-xl flex items-center justify-center mx-auto mb-4 shadow-lg">1</div>
|
||||
<h4 class="font-bold mb-2">Isi Kuesioner</h4>
|
||||
<p class="text-indigo-100 text-sm">Jawab pertanyaan tentang nilai, minat, dan impian kamu secara jujur</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-6">
|
||||
<a href="{{ url('/history/rekomendasi') }}" class="block w-full text-center bg-purple-600 hover:bg-purple-700 text-white font-bold py-2 sm:py-3 px-4 sm:px-6 rounded-lg transition duration-200 text-sm sm:text-base">
|
||||
Lihat History
|
||||
</a>
|
||||
<div class="text-center">
|
||||
<div class="w-12 h-12 rounded-full bg-white text-indigo-600 font-bold text-xl flex items-center justify-center mx-auto mb-4 shadow-lg">2</div>
|
||||
<h4 class="font-bold mb-2">Proses Data</h4>
|
||||
<p class="text-indigo-100 text-sm">AI menganalisis profil kamu dengan algoritma machine learning</p>
|
||||
</div>
|
||||
<div class="mt-3 sm:mt-4 p-2 sm:p-3 bg-purple-50 rounded-lg">
|
||||
<p class="text-xs sm:text-sm text-purple-800">
|
||||
<strong>Total:</strong> {{ $recommendationCount ?? 0 }} analisis
|
||||
</p>
|
||||
<div class="text-center">
|
||||
<div class="w-12 h-12 rounded-full bg-white text-indigo-600 font-bold text-xl flex items-center justify-center mx-auto mb-4 shadow-lg">3</div>
|
||||
<h4 class="font-bold mb-2">Dapatkan Hasil</h4>
|
||||
<p class="text-indigo-100 text-sm">Terima ranking 9 jurusan dengan skor kesesuaian detail</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History Chat Card -->
|
||||
<div class="card-hover bg-white rounded-lg shadow-lg p-5 sm:p-8 flex flex-col h-full" style="border-left: 4px solid #EA580C;">
|
||||
<div class="flex items-start gap-3 sm:gap-4 mb-3 sm:mb-4 flex-grow">
|
||||
<div class="text-3xl sm:text-4xl flex-shrink-0">💾</div>
|
||||
<div>
|
||||
<h3 class="text-lg sm:text-lg md:text-2xl font-bold text-maroon mb-1 sm:mb-2">History Chat</h3>
|
||||
<p class="text-xs sm:text-sm md:text-base text-gray-700">
|
||||
Lihat riwayat semua percakapan Anda dengan AI chatbot.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-6">
|
||||
<a href="{{ url('/history/chat') }}" class="block w-full text-center text-white font-bold py-2 sm:py-3 px-4 sm:px-6 rounded-lg transition duration-200 text-sm sm:text-base" style="background-color: #EA580C;">
|
||||
Lihat History
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-3 sm:mt-4 p-2 sm:p-3 rounded-lg" style="background-color: #FFF7ED;">
|
||||
<p class="text-xs sm:text-sm" style="color: #9A3412;">
|
||||
<strong>Total:</strong> {{ $chatCount ?? 0 }} chat
|
||||
</p>
|
||||
<div class="text-center">
|
||||
<div class="w-12 h-12 rounded-full bg-white text-indigo-600 font-bold text-xl flex items-center justify-center mx-auto mb-4 shadow-lg">4</div>
|
||||
<h4 class="font-bold mb-2">Konsultasi Lanjut</h4>
|
||||
<p class="text-indigo-100 text-sm">Diskusikan hasil & pertanyaan lanjut dengan AI konselor</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Section -->
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 sm:mb-8">
|
||||
<h3 class="text-lg sm:text-xl md:text-2xl font-bold text-maroon mb-4 sm:mb-6">9 Jurusan Tersedia</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-6">
|
||||
<div class="p-3 sm:p-4 bg-gradient-to-br from-yellow-50 to-orange-50 rounded-lg border border-maroon">
|
||||
<div class="text-2xl sm:text-3xl mb-1 sm:mb-2">🌾</div>
|
||||
<h4 class="font-bold text-sm sm:text-base text-maroon">Produksi Pertanian</h4>
|
||||
<p class="text-xs sm:text-sm text-gray-700 mt-1">Teknik budidaya tanaman modern</p>
|
||||
<!-- Programs Grid -->
|
||||
<div class="bg-white rounded-xl shadow-lg p-8">
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-2">Program Studi yang Tersedia</h3>
|
||||
<p class="text-gray-600 mb-8">Jelajahi 9 program studi unggulan kami di Politeknik Negeri Jember</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div class="group p-5 rounded-lg border-2 border-gray-200 hover:border-purple-500 hover:shadow-md transition bg-gradient-to-br from-gray-50 to-white">
|
||||
<div class="text-3xl mb-3">🌾</div>
|
||||
<h4 class="font-bold text-gray-900 group-hover:text-purple-600 transition">Produksi Pertanian</h4>
|
||||
<p class="text-sm text-gray-600 mt-2">Teknik budidaya tanaman modern & berkelanjutan</p>
|
||||
</div>
|
||||
<div class="p-3 sm:p-4 bg-gradient-to-br from-green-50 to-teal-50 rounded-lg border border-maroon">
|
||||
<div class="text-2xl sm:text-3xl mb-1 sm:mb-2">🔬</div>
|
||||
<h4 class="font-bold text-sm sm:text-base text-maroon">Teknologi Pertanian</h4>
|
||||
<p class="text-xs sm:text-sm text-gray-700 mt-1">Inovasi teknologi pertanian</p>
|
||||
<div class="group p-5 rounded-lg border-2 border-gray-200 hover:border-purple-500 hover:shadow-md transition bg-gradient-to-br from-gray-50 to-white">
|
||||
<div class="text-3xl mb-3">🔬</div>
|
||||
<h4 class="font-bold text-gray-900 group-hover:text-purple-600 transition">Teknologi Pertanian</h4>
|
||||
<p class="text-sm text-gray-600 mt-2">Inovasi teknologi untuk efisiensi pertanian</p>
|
||||
</div>
|
||||
<div class="p-3 sm:p-4 bg-gradient-to-br from-blue-50 to-purple-50 rounded-lg border border-maroon">
|
||||
<div class="text-2xl sm:text-3xl mb-1 sm:mb-2">🐄</div>
|
||||
<h4 class="font-bold text-sm sm:text-base text-maroon">Peternakan</h4>
|
||||
<p class="text-xs sm:text-sm text-gray-700 mt-1">Manajemen peternakan</p>
|
||||
<div class="group p-5 rounded-lg border-2 border-gray-200 hover:border-purple-500 hover:shadow-md transition bg-gradient-to-br from-gray-50 to-white">
|
||||
<div class="text-3xl mb-3">🐄</div>
|
||||
<h4 class="font-bold text-gray-900 group-hover:text-purple-600 transition">Peternakan</h4>
|
||||
<p class="text-sm text-gray-600 mt-2">Manajemen & teknologi peternakan terkini</p>
|
||||
</div>
|
||||
<div class="p-3 sm:p-4 bg-gradient-to-br from-red-50 to-pink-50 rounded-lg border border-maroon">
|
||||
<div class="text-2xl sm:text-3xl mb-1 sm:mb-2">💼</div>
|
||||
<h4 class="font-bold text-sm sm:text-base text-maroon">Manajemen Agribisnis</h4>
|
||||
<p class="text-xs sm:text-sm text-gray-700 mt-1">Bisnis pertanian</p>
|
||||
<div class="group p-5 rounded-lg border-2 border-gray-200 hover:border-purple-500 hover:shadow-md transition bg-gradient-to-br from-gray-50 to-white">
|
||||
<div class="text-3xl mb-3">💼</div>
|
||||
<h4 class="font-bold text-gray-900 group-hover:text-purple-600 transition">Manajemen Agribisnis</h4>
|
||||
<p class="text-sm text-gray-600 mt-2">Bisnis & manajemen di sektor pertanian</p>
|
||||
</div>
|
||||
<div class="p-3 sm:p-4 bg-gradient-to-br from-indigo-50 to-blue-50 rounded-lg border border-maroon">
|
||||
<div class="text-2xl sm:text-3xl mb-1 sm:mb-2">💻</div>
|
||||
<h4 class="font-bold text-sm sm:text-base text-maroon">Teknologi Informasi</h4>
|
||||
<p class="text-xs sm:text-sm text-gray-700 mt-1">Sistem digital</p>
|
||||
<div class="group p-5 rounded-lg border-2 border-gray-200 hover:border-purple-500 hover:shadow-md transition bg-gradient-to-br from-gray-50 to-white">
|
||||
<div class="text-3xl mb-3">💻</div>
|
||||
<h4 class="font-bold text-gray-900 group-hover:text-purple-600 transition">Teknologi Informasi</h4>
|
||||
<p class="text-sm text-gray-600 mt-2">Sistem digital & aplikasi web modern</p>
|
||||
</div>
|
||||
<div class="p-3 sm:p-4 bg-gradient-to-br from-amber-50 to-yellow-50 rounded-lg border border-maroon">
|
||||
<div class="text-2xl sm:text-3xl mb-1 sm:mb-2">⚙️</div>
|
||||
<h4 class="font-bold text-sm sm:text-base text-maroon">Teknik</h4>
|
||||
<p class="text-xs sm:text-sm text-gray-700 mt-1">Mesin & sistem teknik</p>
|
||||
<div class="group p-5 rounded-lg border-2 border-gray-200 hover:border-purple-500 hover:shadow-md transition bg-gradient-to-br from-gray-50 to-white">
|
||||
<div class="text-3xl mb-3">⚙️</div>
|
||||
<h4 class="font-bold text-gray-900 group-hover:text-purple-600 transition">Teknik Mesin</h4>
|
||||
<p class="text-sm text-gray-600 mt-2">Teknik mesin & sistem otomasi industri</p>
|
||||
</div>
|
||||
<div class="p-3 sm:p-4 bg-gradient-to-br from-rose-50 to-red-50 rounded-lg border border-maroon">
|
||||
<div class="text-2xl sm:text-3xl mb-1 sm:mb-2">⚕️</div>
|
||||
<h4 class="font-bold text-sm sm:text-base text-maroon">Kesehatan</h4>
|
||||
<p class="text-xs sm:text-sm text-gray-700 mt-1">Profesi kesehatan</p>
|
||||
<div class="group p-5 rounded-lg border-2 border-gray-200 hover:border-purple-500 hover:shadow-md transition bg-gradient-to-br from-gray-50 to-white">
|
||||
<div class="text-3xl mb-3">⚕️</div>
|
||||
<h4 class="font-bold text-gray-900 group-hover:text-purple-600 transition">Kesehatan</h4>
|
||||
<p class="text-sm text-gray-600 mt-2">Program kesehatan & keselamatan kerja</p>
|
||||
</div>
|
||||
<div class="p-3 sm:p-4 bg-gradient-to-br from-sky-50 to-cyan-50 rounded-lg border border-maroon">
|
||||
<div class="text-2xl sm:text-3xl mb-1 sm:mb-2">🗣️</div>
|
||||
<h4 class="font-bold text-sm sm:text-base text-maroon">Bahasa & Komunikasi</h4>
|
||||
<p class="text-xs sm:text-sm text-gray-700 mt-1">Komunasikan & wisata</p>
|
||||
<div class="group p-5 rounded-lg border-2 border-gray-200 hover:border-purple-500 hover:shadow-md transition bg-gradient-to-br from-gray-50 to-white">
|
||||
<div class="text-3xl mb-3">🗣️</div>
|
||||
<h4 class="font-bold text-gray-900 group-hover:text-purple-600 transition">Komunikasi & Pariwisata</h4>
|
||||
<p class="text-sm text-gray-600 mt-2">Program komunikasi & industri pariwisata</p>
|
||||
</div>
|
||||
<div class="p-3 sm:p-4 bg-gradient-to-br from-lime-50 to-green-50 rounded-lg border border-maroon">
|
||||
<div class="text-2xl sm:text-3xl mb-1 sm:mb-2">📊</div>
|
||||
<h4 class="font-bold text-sm sm:text-base text-maroon">Bisnis</h4>
|
||||
<p class="text-xs sm:text-sm text-gray-700 mt-1">Manajemen bisnis</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features Section -->
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8">
|
||||
<h3 class="text-lg sm:text-xl md:text-2xl font-bold text-maroon mb-4 sm:mb-6">Cara Kerja Sistem</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6">
|
||||
<div class="flex gap-3 sm:gap-4">
|
||||
<div class="text-2xl sm:text-3xl font-bold text-maroon flex-shrink-0">1</div>
|
||||
<div>
|
||||
<h4 class="font-bold text-sm sm:text-base text-maroon mb-1">Isi Data Diri</h4>
|
||||
<p class="text-xs sm:text-sm text-gray-700">Masukkan nilai akademik, minat, preferensi, prestasi, dan cita-cita</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 sm:gap-4">
|
||||
<div class="text-2xl sm:text-3xl font-bold text-maroon flex-shrink-0">2</div>
|
||||
<div>
|
||||
<h4 class="font-bold text-sm sm:text-base text-maroon mb-1">Analisis Sistem</h4>
|
||||
<p class="text-xs sm:text-sm text-gray-700">Sistem menganalisis data menggunakan algoritma canggih</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 sm:gap-4">
|
||||
<div class="text-2xl sm:text-3xl font-bold text-maroon flex-shrink-0">3</div>
|
||||
<div>
|
||||
<h4 class="font-bold text-sm sm:text-base text-maroon mb-1">Hasil Rekomendasi</h4>
|
||||
<p class="text-xs sm:text-sm text-gray-700">Dapatkan rekomendasi 9 jurusan dengan analisis detail</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 sm:gap-4">
|
||||
<div class="text-2xl sm:text-3xl font-bold text-maroon flex-shrink-0">4</div>
|
||||
<div>
|
||||
<h4 class="font-bold text-sm sm:text-base text-maroon mb-1">Konsultasi Lanjut</h4>
|
||||
<p class="text-xs sm:text-sm text-gray-700">Gunakan chatbot untuk tanya jawab lebih lanjut</p>
|
||||
</div>
|
||||
<div class="group p-5 rounded-lg border-2 border-gray-200 hover:border-purple-500 hover:shadow-md transition bg-gradient-to-br from-gray-50 to-white">
|
||||
<div class="text-3xl mb-3">📊</div>
|
||||
<h4 class="font-bold text-gray-900 group-hover:text-purple-600 transition">Akuntansi & Bisnis</h4>
|
||||
<p class="text-sm text-gray-600 mt-2">Akuntansi, keuangan & manajemen bisnis</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -327,5 +272,29 @@
|
|||
}
|
||||
});
|
||||
</script>
|
||||
<!-- SweetAlert2 for nicer logout modal on dashboard -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.querySelectorAll('form.confirm-logout').forEach(function(form) {
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
Swal.fire({
|
||||
title: 'Konfirmasi Logout',
|
||||
text: 'Yakin ingin logout dari akun ini?',
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Ya, Logout',
|
||||
cancelButtonText: 'Batal',
|
||||
reverseButtons: true
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Reset Password - Sistem Pemilihan Jurusan</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f3f4f6;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
color: #374151;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #6B7280 0%, #8B95A5 100%);
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
.header p {
|
||||
margin: 8px 0 0 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
color: #C9A961;
|
||||
}
|
||||
.content {
|
||||
padding: 40px;
|
||||
}
|
||||
.content h2 {
|
||||
color: #111827;
|
||||
font-size: 24px;
|
||||
margin: 0 0 16px 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
.greeting {
|
||||
color: #4B5563;
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.message {
|
||||
background-color: #f0f4f8;
|
||||
border-left: 4px solid #6B7280;
|
||||
padding: 16px;
|
||||
margin: 24px 0;
|
||||
border-radius: 6px;
|
||||
color: #374151;
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.cta-section {
|
||||
text-align: center;
|
||||
margin: 32px 0;
|
||||
}
|
||||
.cta-button {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #6B7280 0%, #8B95A5 100%);
|
||||
color: white;
|
||||
padding: 16px 48px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
letter-spacing: 1px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(107, 114, 128, 0.3);
|
||||
}
|
||||
.cta-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(107, 114, 128, 0.4);
|
||||
}
|
||||
.steps {
|
||||
background-color: #f9fafb;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 24px 0;
|
||||
}
|
||||
.steps h3 {
|
||||
color: #111827;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
.step {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin: 12px 0;
|
||||
color: #374151;
|
||||
font-size: 15px;
|
||||
}
|
||||
.step-number {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: linear-gradient(135deg, #6B7280 0%, #8B95A5 100%);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.step-text {
|
||||
flex: 1;
|
||||
}
|
||||
.warning {
|
||||
background-color: #fef3c7;
|
||||
border-left: 4px solid #f59e0b;
|
||||
padding: 14px;
|
||||
margin: 24px 0;
|
||||
border-radius: 6px;
|
||||
color: #78350f;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
font-weight: 500;
|
||||
}
|
||||
.warning strong {
|
||||
color: #b45309;
|
||||
}
|
||||
.footer {
|
||||
background-color: #f9fafb;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
padding: 24px 40px;
|
||||
color: #6B7280;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.footer p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
.footer strong {
|
||||
color: #374151;
|
||||
}
|
||||
.contact-info {
|
||||
background-color: #f0f4f8;
|
||||
border-radius: 6px;
|
||||
padding: 14px;
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
color: #374151;
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
.content {
|
||||
padding: 24px;
|
||||
}
|
||||
.header {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
.cta-button {
|
||||
padding: 14px 32px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<h1>🔐 POLIJE</h1>
|
||||
<p>Sistem Pemilihan Jurusan</p>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="content">
|
||||
<h2>Halo, {{ $user->name }}! 👋</h2>
|
||||
|
||||
<p class="greeting">
|
||||
Kami menerima permintaan untuk mengatur ulang password akunmu. Jika kamu yang melakukan ini, ikuti langkah-langkah di bawah untuk membuat password baru.
|
||||
</p>
|
||||
|
||||
<div class="message">
|
||||
<strong>⏰ Penting:</strong> Link reset password ini hanya berlaku selama <strong>{{ $expiresIn }} menit</strong>. Jika sudah kadaluarsa, silakan minta link baru di halaman "Lupa Password".
|
||||
</div>
|
||||
|
||||
<!-- CTA Button -->
|
||||
<div class="cta-section">
|
||||
<a href="{{ $resetUrl }}" class="cta-button">🔑 RESET PASSWORD</a>
|
||||
</div>
|
||||
|
||||
<!-- Code alternative -->
|
||||
@if(!empty($code))
|
||||
<div style="margin-top:18px;">
|
||||
<p style="font-size:14px;color:#374151;margin-bottom:8px;">Atau, jika Anda ingin melakukan reset tanpa membuka tautan, salin <strong>kode token 6 digit</strong> di bawah ini dan masukkan pada form "Gunakan token" di halaman Lupa Password:</p>
|
||||
<div style="background:#f3f4f6;border:1px dashed #e5e7eb;padding:14px;border-radius:6px;font-family:monospace;color:#111827;font-weight:700;word-break:break-all;font-size:20px;letter-spacing:4px;text-align:center;">{{ $code }}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Steps -->
|
||||
<div class="steps">
|
||||
<h3>📋 Langkah-Langkah:</h3>
|
||||
<div class="step">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-text">Klik tombol <strong>"RESET PASSWORD"</strong> di atas atau copy-paste link ke browser</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-text">Isi form dengan password baru yang kuat (minimal 8 karakter)</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-text">Konfirmasi password baru dengan memasukkan ulang</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-number">4</div>
|
||||
<div class="step-text">Klik tombol <strong>"Simpan Password"</strong></div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-number">5</div>
|
||||
<div class="step-text">Login dengan password baru kamu di Sistem Pemilihan Jurusan</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Warning -->
|
||||
<div class="warning">
|
||||
<strong>⚠️ Keamanan:</strong> Jika kamu tidak meminta reset password ini atau tidak mengenali aktivitas ini, abaikan email ini dan segera hubungi admin kami.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="footer">
|
||||
<p><strong>Informasi Penting:</strong></p>
|
||||
<p>
|
||||
• Jangan membagikan link ini kepada siapapun<br>
|
||||
• Link ini hanya untuk reset password, bukan untuk aktivitas lain<br>
|
||||
• Pastikan kamu menggunakan password yang kuat dan unik<br>
|
||||
• Jika ada masalah, hubungi tim dukungan kami
|
||||
</p>
|
||||
<div class="contact-info">
|
||||
<strong>💬 Butuh Bantuan?</strong><br>
|
||||
Jika link tidak berfungsi atau ada pertanyaan, hubungi BK (Bimbingan Konseling) atau admin Sistem Pemilihan Jurusan.
|
||||
</div>
|
||||
<p style="margin-top: 16px; border-top: 1px solid #e5e7eb; padding-top: 12px;">
|
||||
Terima kasih,<br>
|
||||
<strong>Tim Sistem Pemilihan Jurusan Polije</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -7,19 +7,19 @@
|
|||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.gradient-maroon {
|
||||
background: linear-gradient(135deg, #5B7B89 0%, #7B9BA5 100%);
|
||||
background: linear-gradient(135deg, #6B7280 0%, #8B95A5 100%);
|
||||
}
|
||||
.text-maroon {
|
||||
color: #5B7B89;
|
||||
color: #6B7280;
|
||||
}
|
||||
.border-maroon {
|
||||
border-color: #5B7B89;
|
||||
border-color: #6B7280;
|
||||
}
|
||||
.bg-cream {
|
||||
background-color: #F8FAFC;
|
||||
}
|
||||
.user-message {
|
||||
background-color: #5B7B89;
|
||||
background-color: #6B7280;
|
||||
color: white;
|
||||
border-radius: 12px 12px 0 12px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@
|
|||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.gradient-maroon {
|
||||
background: linear-gradient(135deg, #5B7B89 0%, #7B9BA5 100%);
|
||||
background: linear-gradient(135deg, #6B7280 0%, #8B95A5 100%);
|
||||
}
|
||||
.text-maroon {
|
||||
color: #5B7B89;
|
||||
color: #6B7280;
|
||||
}
|
||||
.border-maroon {
|
||||
border-color: #5B7B89;
|
||||
border-color: #6B7280;
|
||||
}
|
||||
.bg-cream {
|
||||
background-color: #F8FAFC;
|
||||
|
|
@ -25,85 +25,124 @@
|
|||
<header class="gradient-maroon text-white shadow-lg sticky top-0 z-50">
|
||||
<div class="container mx-auto px-4 sm:px-6 py-4 sm:py-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4">
|
||||
<div>
|
||||
<h1 class="text-xl sm:text-2xl md:text-3xl font-bold">History Rekomendasi</h1>
|
||||
<p class="text-xs sm:text-sm text-yellow-300 font-semibold mt-1">Semua Analisis Anda</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
|
||||
<a href="{{ url('/dashboard') }}" class="block sm:inline-block flex-1 sm:flex-none text-center bg-yellow-400 text-maroon font-bold py-2 px-3 sm:px-4 rounded-lg hover:bg-yellow-300 transition text-xs sm:text-sm">
|
||||
Kembali Dashboard
|
||||
</a>
|
||||
<h1 class="text-2xl md:text-3xl font-bold">📊 History Rekomendasi</h1>
|
||||
<p class="text-sm text-yellow-200 font-semibold mt-1">Semua Analisis Anda</p>
|
||||
</div>
|
||||
<a href="{{ url('/dashboard') }}" class="bg-yellow-400 text-maroon font-bold py-2 px-4 rounded-lg hover:bg-yellow-300 transition text-sm">
|
||||
← Kembali Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="container mx-auto px-4 sm:px-6 py-6 sm:py-12">
|
||||
<div class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
||||
@if($recommendations && $recommendations->count() > 0)
|
||||
<div class="space-y-4 sm:space-y-6">
|
||||
<div class="space-y-6">
|
||||
@foreach($recommendations as $rec)
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-6 border-l-4 border-maroon hover:shadow-xl transition">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4 mb-4 sm:mb-6">
|
||||
<div>
|
||||
<h3 class="text-lg sm:text-xl font-bold text-maroon mb-2">
|
||||
Analisis - {{ \Carbon\Carbon::parse($rec->created_at)->format('d M Y H:i') }}
|
||||
</h3>
|
||||
<p class="text-xs sm:text-sm text-gray-600">{{ $rec->created_at->diffForHumans() }}</p>
|
||||
<div class="bg-white rounded-lg shadow-lg overflow-hidden border-l-4 border-maroon hover:shadow-xl transition">
|
||||
<!-- Header Card -->
|
||||
<div class="bg-gradient-to-r from-yellow-400 to-yellow-300 p-6 text-gray-800 cursor-pointer hover:bg-gradient-to-r hover:from-yellow-300 hover:to-yellow-200 transition" onclick="toggleDetail({{ $loop->index }})">
|
||||
<div class="flex justify-between items-start gap-4">
|
||||
<div class="flex-1">
|
||||
<h3 class="text-2xl font-bold mb-2">📅 {{ \Carbon\Carbon::parse($rec->created_at)->format('d M Y H:i') }}</h3>
|
||||
<p class="text-sm text-gray-700 font-semibold">{{ $rec->created_at->diffForHumans() }}</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<span class="inline-block px-4 py-2 rounded-full bg-white text-maroon font-bold text-sm">
|
||||
Lihat Detail ↓
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="toggleDetail({{ $loop->index }})" class="w-full sm:w-auto bg-maroon text-white font-bold py-2 px-4 rounded-lg hover:opacity-90 transition text-xs sm:text-sm">
|
||||
Lihat Detail
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="detail-{{ $loop->index }}" class="hidden">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 sm:gap-4 mb-4 sm:mb-6">
|
||||
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg">
|
||||
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Minat</p>
|
||||
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ $rec->minat ?? '-' }}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg">
|
||||
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Pref. Belajar</p>
|
||||
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ $rec->preferensi_studi ?? '-' }}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg">
|
||||
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Cita-Cita</p>
|
||||
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ Str::limit($rec->cita_cita ?? '-', 15) }}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg">
|
||||
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Prestasi</p>
|
||||
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ Str::limit($rec->prestasi ?? '-', 15) }}</p>
|
||||
<!-- Detail Card (Hidden by default) -->
|
||||
<div id="detail-{{ $loop->index }}" class="hidden border-t-4 border-yellow-200 p-6">
|
||||
<!-- Input Data Summary -->
|
||||
<div class="mb-8">
|
||||
<h4 class="text-lg font-bold text-maroon mb-4">📝 Data Input</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="bg-blue-50 p-4 rounded-lg border-l-4 border-blue-500">
|
||||
<p class="text-xs text-blue-700 font-semibold mb-1">💭 Minat</p>
|
||||
<p class="text-sm font-bold text-gray-800">{{ $rec->minat ?? '-' }}</p>
|
||||
</div>
|
||||
<div class="bg-purple-50 p-4 rounded-lg border-l-4 border-purple-500">
|
||||
<p class="text-xs text-purple-700 font-semibold mb-1">🎓 Preferensi Studi</p>
|
||||
<p class="text-sm font-bold text-gray-800">{{ $rec->preferensi_studi ?? '-' }}</p>
|
||||
</div>
|
||||
<div class="bg-green-50 p-4 rounded-lg border-l-4 border-green-500">
|
||||
<p class="text-xs text-green-700 font-semibold mb-1">🎯 Cita-Cita</p>
|
||||
<p class="text-sm font-bold text-gray-800">{{ $rec->cita_cita ?? '-' }}</p>
|
||||
</div>
|
||||
<div class="bg-red-50 p-4 rounded-lg border-l-4 border-red-500">
|
||||
<p class="text-xs text-red-700 font-semibold mb-1">🏆 Prestasi</p>
|
||||
<p class="text-sm font-bold text-gray-800">{{ $rec->prestasi ?? '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-yellow-50 p-4 sm:p-6 rounded-lg">
|
||||
<h4 class="font-bold text-maroon mb-3 sm:mb-4">Top 3 Rekomendasi Jurusan</h4>
|
||||
<div class="space-y-2 sm:space-y-3">
|
||||
@if($rec->hasil_rekomendasi)
|
||||
<!-- Nilai Akademik -->
|
||||
<div class="mb-8">
|
||||
<h4 class="text-lg font-bold text-maroon mb-4">📚 Nilai Akademik</h4>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
@php
|
||||
$subjects = [
|
||||
['name' => 'Matematika', 'key' => 'mtk', 'icon' => '🔢'],
|
||||
['name' => 'Fisika', 'key' => 'fisika', 'icon' => '⚡'],
|
||||
['name' => 'Kimia', 'key' => 'kimia', 'icon' => '🧪'],
|
||||
['name' => 'Biologi', 'key' => 'biologi', 'icon' => '🔬'],
|
||||
['name' => 'Ekonomi', 'key' => 'ekonomi', 'icon' => '💰'],
|
||||
['name' => 'Geografi', 'key' => 'geografi', 'icon' => '🌍'],
|
||||
['name' => 'Sosiologi', 'key' => 'sosiologi', 'icon' => '👥'],
|
||||
['name' => 'Sejarah', 'key' => 'sejarah', 'icon' => '📜'],
|
||||
];
|
||||
@endphp
|
||||
@foreach($subjects as $subject)
|
||||
@if($rec->{$subject['key']} !== null)
|
||||
<div class="bg-gray-50 p-3 rounded-lg border border-gray-200 hover:border-maroon transition">
|
||||
<p class="text-xs text-gray-600 font-semibold mb-1">{{ $subject['icon'] }} {{ $subject['name'] }}</p>
|
||||
<p class="text-lg font-bold text-maroon">{{ $rec->{$subject['key']} }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top 3 Rekomendasi Jurusan -->
|
||||
<div>
|
||||
<h4 class="text-lg font-bold text-maroon mb-4">🎯 Top 3 Rekomendasi Jurusan</h4>
|
||||
@if($rec->hasil_rekomendasi && is_array($rec->hasil_rekomendasi))
|
||||
<div class="space-y-3">
|
||||
@foreach(array_slice($rec->hasil_rekomendasi, 0, 3) as $index => $hasil)
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 sm:gap-4 p-3 sm:p-4 bg-white rounded-lg border border-yellow-300">
|
||||
<div class="flex items-center gap-3 sm:gap-4">
|
||||
<span class="text-lg sm:text-xl font-bold text-yellow-500">{{ $index + 1 }}</span>
|
||||
<span class="text-sm sm:text-base font-bold text-maroon">{{ $hasil['jurusan'] ?? '-' }}</span>
|
||||
<div class="flex items-center justify-between p-4 bg-gradient-to-r {{ $index === 0 ? 'from-yellow-50 to-yellow-100' : ($index === 1 ? 'from-gray-50 to-gray-100' : 'from-orange-50 to-orange-100') }} rounded-lg border-l-4 {{ $index === 0 ? 'border-yellow-500' : ($index === 1 ? 'border-gray-500' : 'border-orange-500') }}">
|
||||
<div class="flex items-center gap-4 flex-1">
|
||||
<span class="inline-flex items-center justify-center w-10 h-10 rounded-full {{ $index === 0 ? 'bg-yellow-500 text-white' : ($index === 1 ? 'bg-gray-500 text-white' : 'bg-orange-500 text-white') }} font-bold text-lg">
|
||||
{{ $index + 1 }}
|
||||
</span>
|
||||
<div>
|
||||
<p class="text-sm font-bold text-gray-600">Rekomendasi {{ $index + 1 }}</p>
|
||||
<p class="text-lg font-bold text-gray-800">{{ $hasil['jurusan'] ?? '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs sm:text-sm bg-maroon text-white px-3 py-1 rounded-full font-bold">
|
||||
@php $skorVal = $hasil['skor'] ?? 0; @endphp
|
||||
@php $skorVal = $hasil['skor'] ?? 0; @endphp
|
||||
<span class="px-4 py-2 rounded-full font-bold text-white {{ $index === 0 ? 'bg-yellow-500' : ($index === 1 ? 'bg-gray-500' : 'bg-orange-500') }}">
|
||||
{{ number_format(($skorVal > 1 ? $skorVal : $skorVal * 100), 1) }}%
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="bg-white rounded-lg shadow-lg p-8 sm:p-12 text-center">
|
||||
<div class="text-5xl sm:text-6xl mb-4">📊</div>
|
||||
<h3 class="text-xl sm:text-2xl font-bold text-maroon mb-2">Belum Ada History</h3>
|
||||
<p class="text-xs sm:text-sm md:text-base text-gray-700 mb-6">Anda belum melakukan analisis rekomendasi. Mulai sekarang!</p>
|
||||
<a href="{{ url('/rekomendasi') }}" class="inline-block bg-maroon text-white font-bold py-2 sm:py-3 px-6 sm:px-8 rounded-lg hover:opacity-90 transition text-sm sm:text-base">
|
||||
Mulai Analisis
|
||||
<div class="bg-white rounded-lg shadow-lg p-12 text-center">
|
||||
<div class="text-6xl mb-4">📊</div>
|
||||
<h3 class="text-2xl font-bold text-maroon mb-2">Belum Ada History</h3>
|
||||
<p class="text-gray-700 mb-6">Anda belum melakukan analisis rekomendasi. Mulai sekarang untuk melihat history!</p>
|
||||
<a href="{{ url('/rekomendasi') }}" class="inline-block bg-maroon text-white font-bold py-3 px-8 rounded-lg hover:opacity-90 transition">
|
||||
🚀 Mulai Analisis
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
|
@ -112,7 +151,9 @@
|
|||
<script>
|
||||
function toggleDetail(index) {
|
||||
const detail = document.getElementById('detail-' + index);
|
||||
detail.classList.toggle('hidden');
|
||||
if (detail) {
|
||||
detail.classList.toggle('hidden');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -32,5 +32,49 @@
|
|||
{{ $slot }}
|
||||
</main>
|
||||
</div>
|
||||
<!-- SweetAlert2 for nicer confirmation modals -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.querySelectorAll('form.confirm-logout').forEach(function(form) {
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
Swal.fire({
|
||||
title: 'Konfirmasi Logout',
|
||||
text: 'Yakin ingin logout dari akun ini?',
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Ya, Logout',
|
||||
cancelButtonText: 'Batal',
|
||||
reverseButtons: true
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
// Generic SweetAlert2 confirmation for destructive actions
|
||||
document.querySelectorAll('form.swal-confirm').forEach(function(form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const msg = form.getAttribute('data-confirm-message') || 'Yakin melanjutkan aksi ini?';
|
||||
Swal.fire({
|
||||
title: 'Konfirmasi',
|
||||
text: msg,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Ya',
|
||||
cancelButtonText: 'Batal',
|
||||
reverseButtons: true
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
</x-dropdown-link>
|
||||
|
||||
<!-- Authentication -->
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
<form method="POST" action="{{ route('logout') }}" class="confirm-logout">
|
||||
@csrf
|
||||
|
||||
<x-dropdown-link :href="route('logout')"
|
||||
|
|
@ -85,7 +85,7 @@
|
|||
</x-responsive-nav-link>
|
||||
|
||||
<!-- Authentication -->
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
<form method="POST" action="{{ route('logout') }}" class="confirm-logout">
|
||||
@csrf
|
||||
|
||||
<x-responsive-nav-link :href="route('logout')"
|
||||
|
|
|
|||
|
|
@ -7,30 +7,30 @@
|
|||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.gradient-maroon {
|
||||
background: linear-gradient(135deg, #5B7B89 0%, #7B9BA5 100%);
|
||||
background: linear-gradient(135deg, #6B7280 0%, #8B95A5 100%);
|
||||
}
|
||||
.text-maroon {
|
||||
color: #5B7B89;
|
||||
color: #6B7280;
|
||||
}
|
||||
.border-maroon {
|
||||
border-color: #5B7B89;
|
||||
border-color: #6B7280;
|
||||
}
|
||||
.bg-cream {
|
||||
background-color: #F8FAFC;
|
||||
}
|
||||
.btn-maroon {
|
||||
background: linear-gradient(135deg, #5B7B89 0%, #7B9BA5 100%);
|
||||
background: linear-gradient(135deg, #6B7280 0%, #8B95A5 100%);
|
||||
color: #fff;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.btn-maroon:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(91, 123, 137, 0.3);
|
||||
box-shadow: 0 4px 12px rgba(107, 114, 128, 0.3);
|
||||
}
|
||||
.input-focus:focus {
|
||||
border-color: #5B7B89;
|
||||
box-shadow: 0 0 0 3px rgba(91, 123, 137, 0.15);
|
||||
border-color: #6B7280;
|
||||
box-shadow: 0 0 0 3px rgba(107, 114, 128, 0.15);
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -55,189 +55,192 @@
|
|||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="container mx-auto px-4 sm:px-6 py-6 sm:py-12 max-w-3xl">
|
||||
<div class="container mx-auto px-4 sm:px-6 py-4 sm:py-8 max-w-7xl">
|
||||
|
||||
{{-- Success Message --}}
|
||||
@if (session('status') === 'profile-updated')
|
||||
<div class="bg-green-50 border border-green-300 text-green-800 rounded-lg p-4 mb-6 text-sm">
|
||||
<div class="bg-green-50 border border-green-300 text-green-800 rounded-lg p-3 mb-4 text-sm">
|
||||
✅ Profil berhasil diperbarui!
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ========== INFORMASI PROFIL ========== --}}
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 border-l-4 border-blue-500">
|
||||
<h2 class="text-lg sm:text-xl font-bold text-maroon mb-1">Informasi Profil</h2>
|
||||
<p class="text-xs sm:text-sm text-gray-500 mb-6">Perbarui data diri dan foto profil Anda.</p>
|
||||
{{-- ========== PROFILE HEADER CARD - HORIZONTAL ========== --}}
|
||||
<div class="bg-gradient-to-r from-yellow-400 to-yellow-300 rounded-xl shadow-xl p-6 mb-6 text-gray-800">
|
||||
<div class="flex gap-6 items-center">
|
||||
<!-- Avatar Section -->
|
||||
<div class="flex-shrink-0">
|
||||
@if($user->foto)
|
||||
<img src="{{ asset($user->foto) }}" alt="Foto Profil" class="w-32 h-32 rounded-2xl object-cover border-4 border-white shadow-lg" id="foto-preview-header">
|
||||
@else
|
||||
<div class="w-32 h-32 rounded-2xl bg-white bg-opacity-30 flex items-center justify-center text-5xl font-bold text-gray-700" id="foto-placeholder-header">
|
||||
{{ strtoupper(substr($user->name, 0, 1)) }}
|
||||
</div>
|
||||
<img src="#" alt="Foto Profil" class="w-32 h-32 rounded-2xl object-cover border-4 border-white shadow-lg hidden" id="foto-preview-header">
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('profile.update') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@method('patch')
|
||||
|
||||
{{-- Foto Profil --}}
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-semibold text-maroon mb-2">Foto Profil</label>
|
||||
<div class="flex items-center gap-4">
|
||||
@if($user->foto)
|
||||
<img src="{{ asset($user->foto) }}" alt="Foto Profil" class="h-20 w-20 rounded-full object-cover border-2 border-maroon shadow" id="foto-preview">
|
||||
@else
|
||||
<div class="h-20 w-20 rounded-full bg-gray-200 flex items-center justify-center text-3xl border-2 border-maroon" id="foto-placeholder">
|
||||
👤
|
||||
</div>
|
||||
<img src="#" alt="Foto Profil" class="h-20 w-20 rounded-full object-cover border-2 border-maroon shadow hidden" id="foto-preview">
|
||||
@endif
|
||||
<!-- Info Section -->
|
||||
<div class="flex-1">
|
||||
<h2 class="text-3xl font-bold mb-4">{{ $user->name }}</h2>
|
||||
<div class="grid grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<input type="file" name="foto" id="foto" accept="image/*" class="text-sm text-gray-600 file:mr-3 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-yellow-100 file:text-yellow-800 hover:file:bg-yellow-200" onchange="previewFoto(this)">
|
||||
<p class="text-xs text-gray-400 mt-1">Format: JPG, PNG, GIF. Maks 2MB.</p>
|
||||
<p class="text-gray-700 text-xs font-semibold opacity-75">NIS</p>
|
||||
<p class="text-lg font-bold text-gray-800">{{ $user->nis ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-700 text-xs font-semibold opacity-75">Email</p>
|
||||
<p class="text-sm font-semibold text-gray-800 break-words">{{ $user->email }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-700 text-xs font-semibold opacity-75">Kelompok</p>
|
||||
@if($user->kelompok_asal)
|
||||
<span class="inline-block px-3 py-1 rounded-full text-xs font-bold text-white" style="background-color: {{ $user->kelompok_asal == 'IPA' ? '#0369A1' : '#B45309' }};">
|
||||
{{ $user->kelompok_asal }}
|
||||
</span>
|
||||
@else
|
||||
<p class="text-sm font-bold">-</p>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-700 text-xs font-semibold opacity-75">Terdaftar</p>
|
||||
<p class="text-sm font-semibold text-gray-800">{{ $user->created_at->format('d M Y') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@error('foto')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Nama --}}
|
||||
<div class="mb-4">
|
||||
<label for="name" class="block text-sm font-semibold text-maroon mb-1">Nama Lengkap</label>
|
||||
<input type="text" id="name" name="name" value="{{ old('name', $user->name) }}" required
|
||||
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0">
|
||||
@error('name')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Email --}}
|
||||
<div class="mb-4">
|
||||
<label for="email" class="block text-sm font-semibold text-maroon mb-1">Email</label>
|
||||
<input type="email" id="email" name="email" value="{{ old('email', $user->email) }}" required
|
||||
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0">
|
||||
@error('email')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- NIS --}}
|
||||
<div class="mb-4">
|
||||
<label for="nis" class="block text-sm font-semibold text-maroon mb-1">NIS (Nomor Induk Siswa)</label>
|
||||
<input type="text" id="nis" name="nis" value="{{ old('nis', $user->nis) }}" maxlength="20"
|
||||
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0" placeholder="Contoh: 123456">
|
||||
@error('nis')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Kelompok Asal --}}
|
||||
<div class="mb-6">
|
||||
<label for="kelompok_asal" class="block text-sm font-semibold text-maroon mb-1">Kelompok Asal</label>
|
||||
<select id="kelompok_asal" name="kelompok_asal"
|
||||
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0 bg-white">
|
||||
<option value="">-- Pilih Kelompok --</option>
|
||||
<option value="IPA" {{ old('kelompok_asal', $user->kelompok_asal) === 'IPA' ? 'selected' : '' }}>IPA</option>
|
||||
<option value="IPS" {{ old('kelompok_asal', $user->kelompok_asal) === 'IPS' ? 'selected' : '' }}>IPS</option>
|
||||
</select>
|
||||
@error('kelompok_asal')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-maroon font-bold py-2 px-6 rounded-lg text-sm">
|
||||
Simpan Perubahan
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ========== UBAH PASSWORD ========== --}}
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 border-l-4 border-green-500">
|
||||
<h2 class="text-lg sm:text-xl font-bold text-maroon mb-1">Ubah Password</h2>
|
||||
<p class="text-xs sm:text-sm text-gray-500 mb-6">Pastikan akun Anda menggunakan password yang kuat dan aman.</p>
|
||||
{{-- ========== FORMS CONTAINER - 2 KOLOM ========== --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
|
||||
@if (session('status') === 'password-updated')
|
||||
<div class="bg-green-50 border border-green-300 text-green-800 rounded-lg p-4 mb-4 text-sm">
|
||||
✅ Password berhasil diubah!
|
||||
</div>
|
||||
@endif
|
||||
{{-- ========== INFORMASI PROFIL ========== --}}
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 border-l-4 border-blue-500">
|
||||
<h2 class="text-lg font-bold text-maroon mb-1">📝 Edit Profil</h2>
|
||||
<p class="text-xs text-gray-500 mb-4">Perbarui data diri Anda.</p>
|
||||
|
||||
<form method="POST" action="{{ route('password.update') }}">
|
||||
@csrf
|
||||
@method('put')
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="current_password" class="block text-sm font-semibold text-maroon mb-1">Password Saat Ini</label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="password" id="current_password" name="current_password"
|
||||
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0" style="padding-right: 45px;">
|
||||
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('current_password', this)">👁️</button>
|
||||
</div>
|
||||
@error('current_password', 'updatePassword')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="password" class="block text-sm font-semibold text-maroon mb-1">Password Baru</label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="password" id="password" name="password"
|
||||
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0" style="padding-right: 45px;">
|
||||
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('password', this)">👁️</button>
|
||||
</div>
|
||||
@error('password', 'updatePassword')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="password_confirmation" class="block text-sm font-semibold text-maroon mb-1">Konfirmasi Password Baru</label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="password" id="password_confirmation" name="password_confirmation"
|
||||
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0" style="padding-right: 45px;">
|
||||
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('password_confirmation', this)">👁️</button>
|
||||
</div>
|
||||
@error('password_confirmation', 'updatePassword')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<button type="submit" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-6 rounded-lg text-sm transition">
|
||||
Ubah Password
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- ========== HAPUS AKUN ========== --}}
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 border-l-4 border-red-500">
|
||||
<h2 class="text-lg sm:text-xl font-bold text-red-700 mb-1">Hapus Akun</h2>
|
||||
<p class="text-xs sm:text-sm text-gray-500 mb-4">
|
||||
Setelah akun dihapus, semua data dan riwayat Anda akan dihapus secara permanen. Pastikan Anda sudah menyimpan data yang diperlukan.
|
||||
</p>
|
||||
|
||||
<button type="button" onclick="document.getElementById('delete-section').classList.toggle('hidden')" class="bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-6 rounded-lg text-sm transition">
|
||||
Hapus Akun Saya
|
||||
</button>
|
||||
|
||||
{{-- Konfirmasi Hapus --}}
|
||||
<div id="delete-section" class="hidden mt-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p class="text-sm text-red-800 mb-4 font-semibold">Apakah Anda yakin? Masukkan password untuk konfirmasi:</p>
|
||||
<form method="POST" action="{{ route('profile.destroy') }}">
|
||||
<form method="POST" action="{{ route('profile.update') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@method('delete')
|
||||
@method('patch')
|
||||
|
||||
{{-- Foto Profil Upload --}}
|
||||
<div class="mb-4">
|
||||
<input type="password" name="password" placeholder="Masukkan password Anda"
|
||||
class="input-focus w-full border border-red-300 rounded-lg px-4 py-2 text-sm focus:ring-0">
|
||||
@error('password', 'userDeletion')
|
||||
<label class="block text-xs font-semibold text-maroon mb-2">🖼️ Foto Profil</label>
|
||||
<input type="file" name="foto" id="foto" accept="image/*" class="text-xs text-gray-600 file:mr-2 file:py-1 file:px-2 file:rounded file:border-0 file:text-xs file:font-semibold file:bg-yellow-100 file:text-yellow-800 hover:file:bg-yellow-200 w-full" onchange="previewFoto(this)">
|
||||
<p class="text-xs text-gray-400 mt-1">JPG, PNG, GIF. Maks 2MB.</p>
|
||||
@error('foto')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button type="button" onclick="document.getElementById('delete-section').classList.add('hidden')" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded-lg text-sm transition">
|
||||
Batal
|
||||
</button>
|
||||
<button type="submit" class="bg-red-700 hover:bg-red-800 text-white font-bold py-2 px-4 rounded-lg text-sm transition">
|
||||
Ya, Hapus Akun
|
||||
</button>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
{{-- Nama --}}
|
||||
<div class="col-span-2">
|
||||
<label for="name" class="block text-xs font-semibold text-maroon mb-1">Nama Lengkap</label>
|
||||
<input type="text" id="name" name="name" value="{{ old('name', $user->name) }}" required
|
||||
class="input-focus w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-0">
|
||||
@error('name')
|
||||
<p class="text-red-600 text-xs mt-0.5">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Email --}}
|
||||
<div class="col-span-2">
|
||||
<label for="email" class="block text-xs font-semibold text-maroon mb-1">Email</label>
|
||||
<input type="email" id="email" name="email" value="{{ old('email', $user->email) }}" required
|
||||
class="input-focus w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-0">
|
||||
@error('email')
|
||||
<p class="text-red-600 text-xs mt-0.5">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- NIS --}}
|
||||
<div>
|
||||
<label for="nis" class="block text-xs font-semibold text-maroon mb-1">NIS</label>
|
||||
<input type="text" id="nis" name="nis" value="{{ old('nis', $user->nis) }}" maxlength="20"
|
||||
class="input-focus w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-0" placeholder="12345678">
|
||||
@error('nis')
|
||||
<p class="text-red-600 text-xs mt-0.5">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Kelompok Asal --}}
|
||||
<div>
|
||||
<label for="kelompok_asal" class="block text-xs font-semibold text-maroon mb-1">Kelompok</label>
|
||||
<select id="kelompok_asal" name="kelompok_asal"
|
||||
class="input-focus w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-0 bg-white">
|
||||
<option value="">Pilih</option>
|
||||
<option value="IPA" {{ old('kelompok_asal', $user->kelompok_asal) === 'IPA' ? 'selected' : '' }}>IPA</option>
|
||||
<option value="IPS" {{ old('kelompok_asal', $user->kelompok_asal) === 'IPS' ? 'selected' : '' }}>IPS</option>
|
||||
</select>
|
||||
@error('kelompok_asal')
|
||||
<p class="text-red-600 text-xs mt-0.5">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-maroon font-bold py-2 px-6 rounded text-xs mt-4 w-full">
|
||||
💾 Simpan
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- ========== UBAH PASSWORD ========== --}}
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 border-l-4 border-green-500">
|
||||
<h2 class="text-lg font-bold text-maroon mb-1">🔐 Ubah Password</h2>
|
||||
<p class="text-xs text-gray-500 mb-4">Gunakan password yang kuat dan aman.</p>
|
||||
|
||||
@if (session('status') === 'password-updated')
|
||||
<div class="bg-green-50 border border-green-300 text-green-800 rounded-lg p-3 mb-3 text-xs">
|
||||
✅ Password berhasil diubah!
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('password.update') }}">
|
||||
@csrf
|
||||
@method('put')
|
||||
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<div>
|
||||
<label for="current_password" class="block text-xs font-semibold text-maroon mb-1">Password Saat Ini</label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="password" id="current_password" name="current_password"
|
||||
class="input-focus w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-0" style="padding-right: 30px;">
|
||||
<button type="button" style="position: absolute; right: 6px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 14px;" onclick="togglePasswordVisibility('current_password', this)">👁️</button>
|
||||
</div>
|
||||
@error('current_password', 'updatePassword')
|
||||
<p class="text-red-600 text-xs mt-0.5">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-xs font-semibold text-maroon mb-1">Password Baru</label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="password" id="password" name="password"
|
||||
class="input-focus w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-0" style="padding-right: 30px;">
|
||||
<button type="button" style="position: absolute; right: 6px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 14px;" onclick="togglePasswordVisibility('password', this)">👁️</button>
|
||||
</div>
|
||||
@error('password', 'updatePassword')
|
||||
<p class="text-red-600 text-xs mt-0.5">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password_confirmation" class="block text-xs font-semibold text-maroon mb-1">Konfirmasi Password</label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="password" id="password_confirmation" name="password_confirmation"
|
||||
class="input-focus w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-0" style="padding-right: 30px;">
|
||||
<button type="button" style="position: absolute; right: 6px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 14px;" onclick="togglePasswordVisibility('password_confirmation', this)">👁️</button>
|
||||
</div>
|
||||
@error('password_confirmation', 'updatePassword')
|
||||
<p class="text-red-600 text-xs mt-0.5">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-6 rounded text-xs transition mt-4 w-full">
|
||||
✓ Ubah Password
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -253,11 +256,11 @@ function previewFoto(input) {
|
|||
if (input.files && input.files[0]) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
var preview = document.getElementById('foto-preview');
|
||||
var placeholder = document.getElementById('foto-placeholder');
|
||||
preview.src = e.target.result;
|
||||
preview.classList.remove('hidden');
|
||||
if (placeholder) placeholder.classList.add('hidden');
|
||||
var previewHeader = document.getElementById('foto-preview-header');
|
||||
var placeholderHeader = document.getElementById('foto-placeholder-header');
|
||||
previewHeader.src = e.target.result;
|
||||
previewHeader.classList.remove('hidden');
|
||||
if (placeholderHeader) placeholderHeader.classList.add('hidden');
|
||||
}
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,69 +6,183 @@
|
|||
<title>Hasil Rekomendasi - Sistem Pemilihan Jurusan</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.gradient-maroon {
|
||||
background: linear-gradient(135deg, #5B7B89 0%, #7B9BA5 100%);
|
||||
:root {
|
||||
--primary: #0f766e;
|
||||
--primary-light: #0d9488;
|
||||
--accent: #f59e0b;
|
||||
--success: #10b981;
|
||||
--error: #ef4444;
|
||||
--bg-soft: #f0f9ff;
|
||||
--bg-card: #ffffff;
|
||||
--text-main: #1f2937;
|
||||
--text-secondary: #6b7280;
|
||||
--border-light: #e5e7eb;
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.text-maroon {
|
||||
color: #5B7B89;
|
||||
|
||||
body {
|
||||
background-color: var(--bg-soft);
|
||||
color: var(--text-main);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
.border-maroon {
|
||||
border-color: #5B7B89;
|
||||
|
||||
.header-main {
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
|
||||
color: white;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.bg-cream {
|
||||
background-color: #F8FAFC;
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.bg-maroon-light {
|
||||
background-color: rgba(91, 123, 137, 0.1);
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-light);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-md);
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.badge-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 2.25rem;
|
||||
min-height: 2.25rem;
|
||||
background-color: var(--accent);
|
||||
color: white;
|
||||
border-radius: 9999px;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.profile-box {
|
||||
background: linear-gradient(135deg, rgba(15, 118, 110, 0.05) 0%, rgba(13, 148, 136, 0.05) 100%);
|
||||
border: 1px solid rgba(15, 118, 110, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.result-item:hover {
|
||||
box-shadow: var(--shadow-lg);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.result-item.top-1 {
|
||||
border-left: 4px solid var(--accent);
|
||||
background: linear-gradient(to right, rgba(245, 158, 11, 0.05), white);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background-color: #e5e7eb;
|
||||
border-radius: 9999px;
|
||||
height: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
background: linear-gradient(90deg, var(--primary) 0%, var(--primary-light) 100%);
|
||||
height: 100%;
|
||||
border-radius: 9999px;
|
||||
transition: width 0.6s ease;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-cream">
|
||||
<body>
|
||||
<!-- Header -->
|
||||
<header class="gradient-maroon text-white shadow-lg sticky top-0 z-50">
|
||||
<div class="container mx-auto px-4 sm:px-6 py-4 sm:py-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4">
|
||||
<div>
|
||||
<h1 class="text-xl sm:text-2xl md:text-3xl font-bold">Hasil Rekomendasi</h1>
|
||||
<p class="text-xs sm:text-sm text-yellow-300 font-semibold mt-1">Sistem Pemilihan Jurusan</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
|
||||
<a href="{{ url('/dashboard') }}" class="block sm:inline-block flex-1 sm:flex-none text-center bg-yellow-400 text-maroon font-bold py-2 px-3 sm:px-4 rounded-lg hover:bg-yellow-300 transition text-xs sm:text-sm">
|
||||
Kembali ke Dashboard
|
||||
</a>
|
||||
<header class="header-main py-4 md:py-6">
|
||||
<div class="container mx-auto px-4 md:px-6">
|
||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl md:text-3xl font-bold">Hasil Analisis Rekomendasi</h1>
|
||||
<p class="text-sm md:text-base text-teal-100 mt-1">Jurusan terbaik berdasarkan profil Anda</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<a href="{{ route('rekomendasi.input') }}" class="btn-primary px-4 py-2 rounded-lg font-medium text-sm hover:shadow-lg transition">
|
||||
🔄 Analisis Ulang
|
||||
</a>
|
||||
<a href="{{ url('/dashboard') }}" class="px-4 py-2 rounded-lg font-medium text-sm bg-white text-primary hover:bg-gray-50 transition">
|
||||
← Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="container mx-auto px-4 sm:px-6 py-6 sm:py-12">
|
||||
<!-- Info Box -->
|
||||
<div class="bg-white border-2 border-maroon rounded-lg p-4 sm:p-6 mb-6 sm:mb-8 shadow-md">
|
||||
<h2 class="text-lg sm:text-xl font-bold text-maroon mb-2 sm:mb-3">Hasil Analisis</h2>
|
||||
<p class="text-xs sm:text-sm md:text-base text-gray-700">
|
||||
Berikut adalah hasil analisis sistem terhadap profil Anda. Jurusan diurutkan berdasarkan skor kesesuaian dari yang tertinggi.
|
||||
</p>
|
||||
</div>
|
||||
<main class="container mx-auto px-4 md:px-6 py-8 md:py-12">
|
||||
<!-- Data Profil Summary -->
|
||||
<div class="card p-6 md:p-8 mb-8">
|
||||
<h2 class="text-2xl font-bold text-primary mb-6">Data Profil Anda</h2>
|
||||
|
||||
<!-- Ringkasan Input -->
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-6 mb-6 sm:mb-8 border-l-4 border-maroon">
|
||||
<h3 class="text-base sm:text-lg font-bold text-maroon mb-3 sm:mb-4">Data Profil Anda</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-2 sm:gap-4">
|
||||
<div class="bg-maroon-light p-3 sm:p-4 rounded-lg">
|
||||
<p class="text-xs sm:text-sm text-gray-600">Nilai Akademik</p>
|
||||
<p class="text-lg sm:text-xl font-bold text-maroon">{{ $katNilai }}</p>
|
||||
<p class="text-xs text-gray-500">Rata-rata: {{ number_format($average, 1) }}</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- Nilai Akademik -->
|
||||
<div class="profile-box">
|
||||
<p style="font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; font-weight: 600; margin-bottom: 0.5rem;">Nilai Akademik</p>
|
||||
<p style="font-size: 1.5rem; font-weight: 700; color: var(--primary); margin-bottom: 0.25rem;">{{ $katNilai }}</p>
|
||||
<p style="font-size: 0.875rem; color: var(--text-secondary);">Rata-rata: {{ number_format($average, 1) }}%</p>
|
||||
</div>
|
||||
<div class="bg-yellow-50 p-3 sm:p-4 rounded-lg">
|
||||
<p class="text-xs sm:text-sm text-gray-600">Preferensi Studi</p>
|
||||
<p class="text-sm sm:text-lg font-bold text-maroon">{{ $prefStudi }}</p>
|
||||
|
||||
<!-- Minat -->
|
||||
<div class="profile-box">
|
||||
<p style="font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; font-weight: 600; margin-bottom: 0.5rem;">Bidang Minat</p>
|
||||
<p style="font-size: 1.125rem; font-weight: 700; color: var(--primary);">{{ $minatMapped ?? '-' }}</p>
|
||||
<p style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.375rem;">Anda inputkan: <strong>{{ ucfirst($minatRaw ?? '-') }}</strong></p>
|
||||
</div>
|
||||
<div class="bg-maroon-light p-3 sm:p-4 rounded-lg">
|
||||
<p class="text-xs sm:text-sm text-gray-600">Prestasi</p>
|
||||
<p class="text-sm sm:text-lg font-bold text-maroon">
|
||||
@if($prestasiScore >= 0.8)
|
||||
Tinggi
|
||||
|
||||
<!-- Cita-cita -->
|
||||
<div class="profile-box">
|
||||
<p style="font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; font-weight: 600; margin-bottom: 0.5rem;">Cita-cita</p>
|
||||
<p style="font-size: 1.125rem; font-weight: 700; color: var(--primary);">{{ $citaMapped ?? '-' }}</p>
|
||||
<p style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.375rem;">Anda inputkan: <strong>{{ ucfirst($citaRaw ?? '-') }}</strong></p>
|
||||
</div>
|
||||
|
||||
<!-- Preferensi Studi -->
|
||||
<div class="profile-box">
|
||||
<p style="font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; font-weight: 600; margin-bottom: 0.5rem;">Preferensi Studi</p>
|
||||
<p style="font-size: 1.125rem; font-weight: 700; color: var(--primary);">{{ $prefStudi ?? '-' }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Prestasi -->
|
||||
<div class="profile-box">
|
||||
<p style="font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; font-weight: 600; margin-bottom: 0.5rem;">Prestasi</p>
|
||||
<p style="font-size: 1.125rem; font-weight: 700; color: var(--primary);">
|
||||
@if(!($isPrestasiFilled ?? true))
|
||||
Tidak Ada
|
||||
@elseif($prestasiScore >= 0.8)
|
||||
Tinggi ⭐
|
||||
@elseif($prestasiScore >= 0.6)
|
||||
Sedang
|
||||
Sedang 👍
|
||||
@elseif($prestasiScore > 0)
|
||||
Cukup
|
||||
@else
|
||||
|
|
@ -76,346 +190,108 @@
|
|||
@endif
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-yellow-50 p-3 sm:p-4 rounded-lg">
|
||||
<p class="text-xs sm:text-sm text-gray-600">Skor Nilai</p>
|
||||
<p class="text-sm sm:text-lg font-bold text-maroon">{{ number_format($average, 1) }}%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabel Peringkat Jurusan -->
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-6 mb-6 sm:mb-8">
|
||||
<h3 class="text-base sm:text-lg font-bold text-maroon mb-3 sm:mb-4">Peringkat Jurusan</h3>
|
||||
<!-- Top Recommendation -->
|
||||
@if(count($hasilAkhir) > 0)
|
||||
<div class="mb-8">
|
||||
<div class="result-item top-1 border-4">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<span style="display: inline-block; background: var(--accent); color: white; padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; margin-bottom: 0.75rem;">
|
||||
🏆 REKOMENDASI UTAMA
|
||||
</span>
|
||||
<h3 style="font-size: 2rem; font-weight: 700; color: var(--primary);">{{ $hasilAkhir[0]['jurusan'] ?? '-' }}</h3>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<p style="font-size: 2.5rem; font-weight: 700; color: var(--accent);">{{ number_format(($hasilAkhir[0]['skor'] ?? 0) * 100, 1) }}%</p>
|
||||
<p style="font-size: 0.875rem; color: var(--text-secondary);">Skor Kesesuaian</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="gradient-maroon text-white">
|
||||
<tr>
|
||||
<th class="px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-bold">#</th>
|
||||
<th class="px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-bold">Jurusan</th>
|
||||
<th class="px-3 sm:px-6 py-2 sm:py-3 text-right text-xs sm:text-sm font-bold">Skor</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
@foreach($hasilAkhir as $index => $res)
|
||||
<tr class="hover:bg-gray-50 transition {{ $index == 0 ? 'bg-yellow-50' : '' }}">
|
||||
<td class="px-3 sm:px-6 py-2 sm:py-4 text-center">
|
||||
<span class="inline-flex items-center justify-center w-6 h-6 sm:w-8 sm:h-8 rounded-full text-xs sm:text-sm font-bold {{ $index == 0 ? 'bg-yellow-400 text-white' : 'bg-gray-200 text-gray-700' }}">
|
||||
{{ $index + 1 }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm font-semibold text-gray-900">
|
||||
{{ $res['jurusan'] ?? '-' }}
|
||||
@if($index == 0)
|
||||
<span class="ml-1 sm:ml-2 inline-block px-2 py-0.5 rounded text-xs font-semibold bg-yellow-100 text-yellow-800">
|
||||
⭐ Utama
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-3 sm:px-6 py-2 sm:py-4 text-right text-xs sm:text-sm font-bold text-maroon">
|
||||
{{ number_format(($res['skor'] ?? 0) * 100, 1) }}%
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="progress-bar mb-4">
|
||||
<div class="progress-fill" style="width: {{ number_format(($hasilAkhir[0]['skor'] ?? 0) * 100, 1) }}%"></div>
|
||||
</div>
|
||||
|
||||
<p style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 1rem;">
|
||||
Berdasarkan analisis Weighted Naive Bayes, jurusan ini adalah pilihan terbaik untuk profil akademik dan minat Anda.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Visualisasi Progress Bars -->
|
||||
<div class="mt-4 sm:mt-6 space-y-2 sm:space-y-3">
|
||||
<!-- All Rankings -->
|
||||
<div class="card p-6 md:p-8">
|
||||
<h2 class="text-2xl font-bold text-primary mb-6">Peringkat Semua Jurusan</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
@foreach($hasilAkhir as $index => $res)
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs sm:text-sm font-semibold text-gray-700">{{ $res['jurusan'] ?? '-' }}</span>
|
||||
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format(($res['skor'] ?? 0) * 100, 1) }}%</span>
|
||||
<div class="result-item {{ $index == 0 ? 'top-1' : '' }}">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="badge-primary">{{ $index + 1 }}</div>
|
||||
<div>
|
||||
<p style="font-weight: 600; color: var(--text-main); margin-bottom: 0.125rem;">{{ $res['jurusan'] ?? '-' }}</p>
|
||||
<p style="font-size: 0.875rem; color: var(--text-secondary);">
|
||||
@if($index == 0)
|
||||
Pilihan terbaik untuk Anda
|
||||
@elseif($index < 3)
|
||||
Rekomendasi alternatif yang baik
|
||||
@else
|
||||
Pilihan lainnya
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<p style="font-size: 1.5rem; font-weight: 700; color: var(--primary);">
|
||||
{{ number_format(($res['skor'] ?? 0) * 100, 1) }}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div class="gradient-maroon h-2 rounded-full" style="width: {{ number_format(($res['skor'] ?? 0) * 100, 1) }}%"></div>
|
||||
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: {{ number_format(($res['skor'] ?? 0) * 100, 1) }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detail Rekomendasi Utama -->
|
||||
@if(count($hasilAkhir) > 0)
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 sm:mb-8 border-l-4 border-yellow-400">
|
||||
@php
|
||||
$topRecommendation = $hasilAkhir[0];
|
||||
$detail = $topRecommendation['detail'] ?? [];
|
||||
@endphp
|
||||
|
||||
<div class="flex flex-col sm:flex-row items-start gap-3 sm:gap-4 mb-4 sm:mb-6">
|
||||
<div class="w-12 h-12 sm:w-14 sm:h-14 rounded-lg bg-yellow-100 flex items-center justify-center text-xl sm:text-2xl flex-shrink-0">1</div>
|
||||
<div>
|
||||
<h3 class="text-lg sm:text-2xl font-bold text-maroon mb-1 sm:mb-2">{{ $topRecommendation['jurusan'] ?? '-' }}</h3>
|
||||
<p class="text-sm sm:text-lg text-gray-700">
|
||||
Skor Kesesuaian: <span class="font-bold text-maroon">{{ number_format(($topRecommendation['skor'] ?? 0) * 100, 1) }}%</span>
|
||||
</p>
|
||||
@if($topJurusan && $topJurusan->deskripsi)
|
||||
<p class="text-xs sm:text-sm text-gray-600 mt-2">{{ $topJurusan->deskripsi }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Analysis Breakdown -->
|
||||
<div class="mb-4 sm:mb-6 p-3 sm:p-4 bg-gray-50 rounded-lg">
|
||||
<h4 class="font-bold text-maroon text-sm sm:text-base mb-3 sm:mb-4">Likelihood per Kriteria (Weighted Naive Bayes):</h4>
|
||||
|
||||
<div class="space-y-2 sm:space-y-3">
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<p class="text-xs sm:text-sm font-semibold text-gray-700">Nilai Akademik — P(nilai|H) × w=0.40</p>
|
||||
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format(($detail['nilai'] ?? 0) * 100, 1) }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-300 rounded-full h-2">
|
||||
<div class="gradient-maroon h-2 rounded-full" style="width: {{ number_format(($detail['nilai'] ?? 0) * 100, 1) }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<p class="text-xs sm:text-sm font-semibold text-gray-700">Minat & Bakat — P(minat|H) × w=0.35</p>
|
||||
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format(($detail['minat'] ?? 0) * 100, 1) }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-300 rounded-full h-2">
|
||||
<div class="bg-yellow-400 h-2 rounded-full" style="width: {{ number_format(($detail['minat'] ?? 0) * 100, 1) }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<p class="text-xs sm:text-sm font-semibold text-gray-700">Preferensi Studi — P(pref|H) × w=0.15</p>
|
||||
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format(($detail['pref'] ?? 0) * 100, 1) }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-300 rounded-full h-2">
|
||||
<div class="gradient-maroon h-2 rounded-full" style="width: {{ number_format(($detail['pref'] ?? 0) * 100, 1) }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<p class="text-xs sm:text-sm font-semibold text-gray-700">Cita-cita — P(cita|H) × w=0.05</p>
|
||||
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format(($detail['cita'] ?? 0) * 100, 1) }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-300 rounded-full h-2">
|
||||
<div class="bg-yellow-400 h-2 rounded-full" style="width: {{ number_format(($detail['cita'] ?? 0) * 100, 1) }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<p class="text-xs sm:text-sm font-semibold text-gray-700">Prestasi — P(prestasi|H) × w=0.05</p>
|
||||
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format(($detail['prestasi'] ?? 0) * 100, 1) }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-300 rounded-full h-2">
|
||||
<div class="gradient-maroon h-2 rounded-full" style="width: {{ number_format(($detail['prestasi'] ?? 0) * 100, 1) }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Explanation Breakdown -->
|
||||
<div class="mb-4 sm:mb-6 p-3 sm:p-4 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<h4 class="font-bold text-blue-900 text-sm sm:text-base mb-3 sm:mb-4 flex items-center gap-2">
|
||||
<span class="text-lg">💡</span> Alasan Kenapa Jurusan Ini Cocok:
|
||||
</h4>
|
||||
|
||||
<div class="space-y-2 sm:space-y-3">
|
||||
<div class="flex gap-2 sm:gap-3">
|
||||
<span class="text-lg flex-shrink-0">📊</span>
|
||||
<p class="text-xs sm:text-sm text-gray-800">
|
||||
<strong>Nilai Akademik:</strong> {{ $topRecommendation['explanation']['nilai'] ?? '-' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2 sm:gap-3">
|
||||
<span class="text-lg flex-shrink-0">❤️</span>
|
||||
<p class="text-xs sm:text-sm text-gray-800">
|
||||
<strong>Minat & Bakat:</strong> {{ $topRecommendation['explanation']['minat'] ?? '-' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2 sm:gap-3">
|
||||
<span class="text-lg flex-shrink-0">🎓</span>
|
||||
<p class="text-xs sm:text-sm text-gray-800">
|
||||
<strong>Metode Pembelajaran:</strong> {{ $topRecommendation['explanation']['pref'] ?? '-' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2 sm:gap-3">
|
||||
<span class="text-lg flex-shrink-0">🎯</span>
|
||||
<p class="text-xs sm:text-sm text-gray-800">
|
||||
<strong>Cita-cita Karir:</strong> {{ $topRecommendation['explanation']['cita'] ?? '-' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2 sm:gap-3">
|
||||
<span class="text-lg flex-shrink-0">🏆</span>
|
||||
<p class="text-xs sm:text-sm text-gray-800">
|
||||
<strong>Prestasi Akademik:</strong> {{ $topRecommendation['explanation']['prestasi'] ?? '-' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kesimpulan -->
|
||||
<div class="p-3 sm:p-4 bg-yellow-50 rounded-lg border-l-4 border-yellow-400 mb-3 sm:mb-4">
|
||||
<h4 class="font-bold text-maroon mb-2 text-sm sm:text-base">📋 Kesimpulan:</h4>
|
||||
<p class="text-gray-700 text-xs sm:text-sm leading-relaxed">
|
||||
Berdasarkan profil Anda dengan <strong>nilai akademik {{ $katNilai }} (rata-rata {{ number_format($average, 1) }})</strong>
|
||||
dan <strong>preferensi studi {{ $prefStudi }}</strong>,
|
||||
sistem menganalisis bahwa <strong>{{ $topRecommendation['jurusan'] ?? '-' }}</strong>
|
||||
adalah pilihan yang paling sesuai dengan skor {{ number_format(($topRecommendation['skor'] ?? 0) * 100, 1) }}%.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Prospek Kerja -->
|
||||
@if($topJurusan && $topJurusan->prospek_kerja)
|
||||
<div class="mb-3 sm:mb-4">
|
||||
<p class="text-xs sm:text-sm font-bold text-maroon mb-2">Prospek Kerja:</p>
|
||||
<p class="text-xs sm:text-sm text-gray-700">{{ $topJurusan->prospek_kerja }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Rekomendasi Alternatif & Penjelasan Detail -->
|
||||
@if(count($hasilAkhir) > 1)
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 sm:mb-8">
|
||||
<h3 class="text-lg sm:text-xl font-bold text-maroon mb-4 sm:mb-6 flex items-center gap-2">
|
||||
<span class="text-2xl">🔍</span> Rekomendasi Alternatif & Penjelasan Detail
|
||||
</h3>
|
||||
|
||||
<div class="space-y-3 sm:space-y-4">
|
||||
@foreach($hasilAkhir as $index => $rec)
|
||||
@if($index > 0)
|
||||
<details class="border border-gray-300 rounded-lg overflow-hidden">
|
||||
<summary class="cursor-pointer p-4 hover:bg-gray-50 flex justify-between items-center">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-gray-200 text-gray-700 font-bold text-sm">
|
||||
{{ $index + 1 }}
|
||||
</span>
|
||||
<div>
|
||||
<p class="font-semibold text-gray-900 text-sm sm:text-base">{{ $rec['jurusan'] ?? '-' }}</p>
|
||||
<p class="text-xs sm:text-sm text-gray-600">Skor: {{ number_format(($rec['skor'] ?? 0) * 100, 1) }}%</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-gray-400">▸</span>
|
||||
</summary>
|
||||
|
||||
<div class="p-4 bg-gray-50 border-t border-gray-300 space-y-4">
|
||||
<!-- Score Breakdown -->
|
||||
<div class="bg-white p-3 rounded-lg">
|
||||
<p class="text-xs sm:text-sm font-bold text-gray-700 mb-3">Scoring per Kriteria:</p>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<span class="text-gray-600">📊 Nilai (40%)</span>
|
||||
<span class="font-semibold text-maroon">{{ number_format(($rec['detail']['nilai'] ?? 0) * 100, 1) }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded h-1.5">
|
||||
<div class="bg-red-500 rounded h-1.5 transition-all" style="width: {{ number_format(($rec['detail']['nilai'] ?? 0) * 100, 1) }}%"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center text-xs mt-2">
|
||||
<span class="text-gray-600">❤️ Minat (35%)</span>
|
||||
<span class="font-semibold text-maroon">{{ number_format(($rec['detail']['minat'] ?? 0) * 100, 1) }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded h-1.5">
|
||||
<div class="bg-pink-500 rounded h-1.5 transition-all" style="width: {{ number_format(($rec['detail']['minat'] ?? 0) * 100, 1) }}%"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center text-xs mt-2">
|
||||
<span class="text-gray-600">🎓 Preferensi (15%)</span>
|
||||
<span class="font-semibold text-maroon">{{ number_format(($rec['detail']['pref'] ?? 0) * 100, 1) }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded h-1.5">
|
||||
<div class="bg-yellow-500 rounded h-1.5 transition-all" style="width: {{ number_format(($rec['detail']['pref'] ?? 0) * 100, 1) }}%"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center text-xs mt-2">
|
||||
<span class="text-gray-600">🎯 Cita-cita (5%)</span>
|
||||
<span class="font-semibold text-maroon">{{ number_format(($rec['detail']['cita'] ?? 0) * 100, 1) }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded h-1.5">
|
||||
<div class="bg-blue-500 rounded h-1.5 transition-all" style="width: {{ number_format(($rec['detail']['cita'] ?? 0) * 100, 1) }}%"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center text-xs mt-2">
|
||||
<span class="text-gray-600">🏆 Prestasi (5%)</span>
|
||||
<span class="font-semibold text-maroon">{{ number_format(($rec['detail']['prestasi'] ?? 0) * 100, 1) }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded h-1.5">
|
||||
<div class="bg-green-500 rounded h-1.5 transition-all" style="width: {{ number_format(($rec['detail']['prestasi'] ?? 0) * 100, 1) }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Explanation -->
|
||||
@if(isset($rec['explanation']) && is_array($rec['explanation']))
|
||||
<div class="bg-blue-50 p-3 rounded-lg border-l-4 border-blue-400">
|
||||
<p class="text-xs sm:text-sm font-bold text-blue-900 mb-2">💡 Alasan Cocok:</p>
|
||||
<ul class="space-y-1.5 text-xs sm:text-sm text-gray-800">
|
||||
<li class="flex gap-2">
|
||||
<span class="flex-shrink-0">📊</span>
|
||||
<span>{{ $rec['explanation']['nilai'] ?? '-' }}</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="flex-shrink-0">❤️</span>
|
||||
<span>{{ $rec['explanation']['minat'] ?? '-' }}</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="flex-shrink-0">🎓</span>
|
||||
<span>{{ $rec['explanation']['pref'] ?? '-' }}</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="flex-shrink-0">🎯</span>
|
||||
<span>{{ $rec['explanation']['cita'] ?? '-' }}</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="flex-shrink-0">🏆</span>
|
||||
<span>{{ $rec['explanation']['prestasi'] ?? '-' }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</details>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Chatbot Confirmation Card -->
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 sm:mb-8 border-2 border-yellow-400">
|
||||
<div class="flex flex-col sm:flex-row items-start gap-3 sm:gap-4">
|
||||
<div class="w-12 h-12 sm:w-14 sm:h-14 rounded-lg bg-yellow-100 flex items-center justify-center text-xl sm:text-2xl flex-shrink-0">💬</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-base sm:text-xl font-bold text-maroon mb-1 sm:mb-2">Konsultasi Lebih Lanjut</h3>
|
||||
<p class="text-xs sm:text-sm md:text-base text-gray-700 mb-3 sm:mb-4">
|
||||
Ingin tahu mengapa jurusan <strong>{{ $hasilAkhir[0]['jurusan'] ?? '' }}</strong> direkomendasikan?
|
||||
Konsultasikan dengan AI Konselor BK Virtual untuk penjelasan detail berdasarkan profil Anda.
|
||||
</p>
|
||||
<a href="{{ route('chatbot.index', ['rec' => session('last_recommendation_id')]) }}" class="inline-block gradient-maroon text-white font-bold py-2 sm:py-3 px-4 sm:px-6 rounded-lg hover:opacity-90 transition duration-200 text-sm sm:text-base">
|
||||
💬 Tanya AI: "Mengapa jurusan ini cocok untukku?"
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tombol Aksi -->
|
||||
<div class="flex flex-col sm:flex-row gap-3 sm:gap-4 justify-between">
|
||||
<a href="{{ route('rekomendasi.index') }}" class="block sm:inline-flex items-center justify-center px-4 sm:px-6 py-2 sm:py-3 bg-gray-600 hover:bg-gray-700 text-white font-semibold rounded-lg transition duration-200 text-sm sm:text-base text-center">
|
||||
Kembali & Ubah Input
|
||||
</a>
|
||||
<a href="{{ url('/dashboard') }}" class="block sm:inline-flex items-center justify-center px-4 sm:px-6 py-2 sm:py-3 gradient-maroon text-white font-semibold rounded-lg hover:opacity-90 transition duration-200 text-sm sm:text-base text-center">
|
||||
Ke Dashboard
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Info Metode -->
|
||||
<div class="mt-6 sm:mt-8 p-3 sm:p-4 bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||
<p class="text-xs sm:text-sm text-gray-600">
|
||||
<strong>Metode:</strong> Sistem menggunakan algoritma Weighted Naive Bayes dengan 5 fitur berbobot: Nilai Akademik (w=0.40), Minat (w=0.35), Preferensi Studi (w=0.15), Cita-cita (w=0.05), Prestasi (w=0.05). Rumus: P(H|X) ∝ P(H) × ∏ P(Xi|H)<sup>wi</sup>, kemudian dinormalisasi menggunakan softmax.
|
||||
<!-- Method Explanation -->
|
||||
<div class="card p-6 md:p-8 mt-8 bg-gradient-to-r from-cyan-50 to-blue-50">
|
||||
<h3 class="text-lg font-bold text-primary mb-4 flex items-center gap-2">
|
||||
<span>🔬</span> Metode Analisis
|
||||
</h3>
|
||||
<p style="font-size: 0.875rem; color: var(--text-secondary); line-height: 1.6;">
|
||||
Sistem menggunakan <strong>Weighted Naive Bayes</strong> dengan 5 kriteria yang dibobotkan berdasarkan data historis siswa:
|
||||
<br><strong>Minat (45.6%)</strong> • Preferensi Studi (25.6%) • Nilai Akademik (15.6%) • Cita-cita (9%) • Prestasi (4%)
|
||||
<br>Jika prestasi tidak diisi, bobot distribusi ulang ke 4 kriteria lainnya untuk hasil yang akurat.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 mt-8">
|
||||
<a href="{{ route('rekomendasi.index') }}" class="btn-primary px-6 py-3 rounded-lg font-semibold text-center hover:shadow-lg transition">
|
||||
🔄 Analisis Ulang
|
||||
</a>
|
||||
<a href="{{ url('/dashboard') }}" class="px-6 py-3 rounded-lg font-semibold text-center bg-white text-primary border-2" style="border-color: var(--primary); transition: all 0.3s ease;" onmouseover="this.style.backgroundColor='var(--bg-soft)'" onmouseout="this.style.backgroundColor='white'">
|
||||
← Ke Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Smooth animations for progress bars
|
||||
window.addEventListener('load', () => {
|
||||
document.querySelectorAll('.progress-fill').forEach(el => {
|
||||
el.style.width = '0%';
|
||||
setTimeout(() => {
|
||||
el.style.width = el.parentElement.style.width;
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -6,261 +6,492 @@
|
|||
<title>Input Data - Sistem Pemilihan Jurusan</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.gradient-maroon {
|
||||
background: linear-gradient(135deg, #5B7B89 0%, #7B9BA5 100%);
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.text-maroon {
|
||||
color: #5B7B89;
|
||||
|
||||
:root {
|
||||
--primary: #0f766e;
|
||||
--primary-light: #0d9488;
|
||||
--secondary: #d97706;
|
||||
--accent: #f59e0b;
|
||||
--success: #10b981;
|
||||
--error: #ef4444;
|
||||
--bg-soft: #f0f9ff;
|
||||
--bg-card: #ffffff;
|
||||
--text-main: #1f2937;
|
||||
--text-secondary: #6b7280;
|
||||
--border-light: #e5e7eb;
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.border-maroon {
|
||||
border-color: #5B7B89;
|
||||
|
||||
body {
|
||||
background-color: var(--bg-soft);
|
||||
color: var(--text-main);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
.bg-cream {
|
||||
background-color: #F8FAFC;
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.focus-maroon:focus {
|
||||
border-color: #5B7B89;
|
||||
box-shadow: 0 0 0 3px rgba(91, 123, 137, 0.1);
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-light);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.border-primary {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.focus-primary:focus {
|
||||
border-color: var(--primary);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.1);
|
||||
}
|
||||
|
||||
.input-error {
|
||||
border-color: var(--error) !important;
|
||||
background-color: #fff5f5 !important;
|
||||
}
|
||||
|
||||
.input-valid {
|
||||
border-color: var(--success) !important;
|
||||
background-color: #f0fdf4 !important;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
font-size: 0.75rem;
|
||||
margin-top: 0.375rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-md);
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, rgba(15, 118, 110, 0.1) 0%, rgba(13, 148, 136, 0.1) 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-main {
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
|
||||
color: white;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.section-desc {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.input-field label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-main);
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.input-field input,
|
||||
.input-field select,
|
||||
.input-field textarea {
|
||||
padding: 0.625rem 0.875rem;
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.input-field input:focus,
|
||||
.input-field select:focus,
|
||||
.input-field textarea:focus {
|
||||
border-color: var(--primary);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.08);
|
||||
background-color: #f8feff;
|
||||
}
|
||||
|
||||
.input-hint {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 0.375rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-cream">
|
||||
<body>
|
||||
<!-- Header -->
|
||||
<header class="gradient-maroon text-white shadow-lg sticky top-0 z-50">
|
||||
<div class="container mx-auto px-4 sm:px-6 py-4 sm:py-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4">
|
||||
<div>
|
||||
<h1 class="text-xl sm:text-2xl md:text-3xl font-bold">Input Data Rekomendasi</h1>
|
||||
<p class="text-xs sm:text-sm text-yellow-300 font-semibold mt-1">Sistem Pemilihan Jurusan</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
|
||||
<a href="{{ url('/dashboard') }}" class="block sm:inline-block flex-1 sm:flex-none text-center bg-yellow-400 text-maroon font-bold py-2 px-3 sm:px-4 rounded-lg hover:bg-yellow-300 transition text-xs sm:text-sm">
|
||||
Kembali ke Dashboard
|
||||
<header class="header-main py-4 md:py-6">
|
||||
<div class="container mx-auto px-4 md:px-6">
|
||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl md:text-3xl font-bold">Form Analisis Jurusan</h1>
|
||||
<p class="text-sm md:text-base text-teal-100 mt-1">Temukan jurusan yang paling sesuai dengan minat dan potensi Anda</p>
|
||||
</div>
|
||||
<a href="{{ url('/dashboard') }}" class="btn-primary px-4 py-2 rounded-lg font-medium text-sm md:text-base hover:shadow-lg transition">
|
||||
← Kembali
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="container mx-auto px-4 sm:px-6 py-6 sm:py-12">
|
||||
<!-- Info Box -->
|
||||
<div class="bg-white border-2 border-maroon rounded-lg p-4 sm:p-6 mb-6 sm:mb-8 shadow-md">
|
||||
<h2 class="text-lg sm:text-xl font-bold text-maroon mb-2 sm:mb-3">Petunjuk Pengisian</h2>
|
||||
<p class="text-xs sm:text-sm md:text-base text-gray-700 mb-2">
|
||||
Silakan isi data berikut dengan jujur agar sistem dapat memberikan rekomendasi yang akurat.
|
||||
</p>
|
||||
<p class="text-xs sm:text-sm text-gray-600">
|
||||
Sistem akan menganalisis data Anda dan menampilkan ranking <strong>9 jurusan</strong> yang tersedia di Politeknik Negeri Jember.
|
||||
</p>
|
||||
<main class="container mx-auto px-4 md:px-6 py-8 md:py-12">
|
||||
<!-- Info Banner -->
|
||||
<div class="card p-5 md:p-6 mb-6 md:mb-8 bg-gradient-to-r from-blue-50 to-cyan-50 border-l-4" style="border-left-color: var(--primary);">
|
||||
<div style="display: flex; gap: 1rem;">
|
||||
<div style="font-size: 2rem; flex-shrink: 0;">🎯</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold mb-2 text-primary">Petunjuk Pengisian</h2>
|
||||
<p class="text-sm mb-2">Isi form berikut dengan data yang akurat. Sistem akan menganalisis profil Anda menggunakan algoritma Weighted Naive Bayes untuk merekomendasikan 9 jurusan terbaik.</p>
|
||||
<p class="text-xs" style="color: var(--text-secondary);">Bobot kriteria: Minat (45.6%) • Preferensi (25.6%) • Nilai (15.6%) • Cita-cita (9%) • Prestasi (4%)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Card -->
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 border-l-4 border-maroon">
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-3 sm:gap-4 mb-4 sm:mb-6">
|
||||
<div class="w-12 h-12 sm:w-14 sm:h-14 rounded-lg bg-yellow-100 flex items-center justify-center text-2xl flex-shrink-0">📝</div>
|
||||
<div>
|
||||
<h2 class="text-lg sm:text-xl md:text-2xl font-bold text-maroon">Formulir Data Profil</h2>
|
||||
<p class="text-xs sm:text-sm text-gray-600">Jawab pertanyaan berikut untuk mendapatkan rekomendasi jurusan.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-6 md:p-8">
|
||||
<h2 class="text-2xl font-bold mb-6 text-primary">Data Diri Siswa</h2>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="bg-red-50 border-l-4 border-red-500 p-4 sm:p-5 rounded-lg mb-6 shadow-sm">
|
||||
<h3 class="text-red-700 font-bold text-sm sm:text-base mb-3">❌ Kesalahan Validasi:</h3>
|
||||
<ul class="list-disc list-inside space-y-1 text-red-600 text-xs sm:text-sm">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<div class="mb-6 p-4 rounded-lg bg-red-50 border-l-4" style="border-left-color: var(--error);">
|
||||
<div style="display: flex; gap: 1rem;">
|
||||
<div style="font-size: 1.5rem; flex-shrink: 0;">❌</div>
|
||||
<div>
|
||||
<h3 style="color: var(--error); font-weight: 600; margin-bottom: 0.5rem;">Ada Kesalahan Pengisian</h3>
|
||||
<ul style="list-style: none; padding: 0; margin: 0;">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li style="font-size: 0.875rem; color: var(--error); margin-bottom: 0.375rem;">
|
||||
• {{ $error }}
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('rekomendasi.proses') }}" method="POST" class="space-y-4 sm:space-y-6">
|
||||
<form action="{{ route('rekomendasi.proses') }}" method="POST" class="space-y-8">
|
||||
@csrf
|
||||
|
||||
{{-- ============================================ --}}
|
||||
{{-- KRITERIA 1: NILAI MATA PELAJARAN --}}
|
||||
{{-- ============================================ --}}
|
||||
<div class="p-4 sm:p-5 rounded-lg border-2 border-gray-200 bg-gray-50">
|
||||
<h3 class="font-bold text-maroon text-base sm:text-lg mb-1 sm:mb-2">1. Nilai Mata Pelajaran <span class="text-red-500">*</span></h3>
|
||||
<p class="text-xs text-gray-600 mb-3 sm:mb-4">
|
||||
<!-- NILAI AKADEMIK -->
|
||||
<section>
|
||||
<h3 class="section-title">
|
||||
<span style="font-size: 1.25rem;">1️⃣</span> Nilai Akademik
|
||||
<span style="color: var(--error);">*</span>
|
||||
</h3>
|
||||
<p class="section-desc">
|
||||
@if(isset($student) && $student->kelompok_asal == 'IPA')
|
||||
Siswa <strong>IPA</strong> — Masukkan nilai rapor (0-100): <strong>Matematika, Fisika, Kimia, Biologi</strong>.
|
||||
Masukkan nilai rapor untuk IPA: <strong>Matematika, Fisika, Kimia, Biologi</strong>
|
||||
@else
|
||||
Siswa <strong>IPS</strong> — Masukkan nilai rapor (0-100): <strong>Ekonomi, Geografi, Sosiologi, Sejarah</strong>.
|
||||
Masukkan nilai rapor untuk IPS: <strong>Ekonomi, Geografi, Sosiologi, Sejarah</strong>
|
||||
@endif
|
||||
</p>
|
||||
|
||||
@if(isset($student) && $student->kelompok_asal == 'IPA')
|
||||
{{-- SISWA IPA: Matematika, Fisika, Kimia, Biologi --}}
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-2 sm:gap-3">
|
||||
<div>
|
||||
<label for="mtk" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Matematika <span class="text-red-500">*</span></label>
|
||||
<input id="mtk" type="number" name="mtk" min="0" max="100" value="{{ old('mtk') }}" placeholder="85" required
|
||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('mtk') border-red-500 @enderror">
|
||||
@error('mtk')
|
||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="fisika" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Fisika <span class="text-red-500">*</span></label>
|
||||
<input id="fisika" type="number" name="fisika" min="0" max="100" value="{{ old('fisika') }}" placeholder="78" required
|
||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('fisika') border-red-500 @enderror">
|
||||
@error('fisika')
|
||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="kimia" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Kimia <span class="text-red-500">*</span></label>
|
||||
<input id="kimia" type="number" name="kimia" min="0" max="100" value="{{ old('kimia') }}" placeholder="72" required
|
||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('kimia') border-red-500 @enderror">
|
||||
@error('kimia')
|
||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="biologi" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Biologi <span class="text-red-500">*</span></label>
|
||||
<input id="biologi" type="number" name="biologi" min="0" max="100" value="{{ old('biologi') }}" placeholder="80" required
|
||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('biologi') border-red-500 @enderror">
|
||||
@error('biologi')
|
||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
@foreach(['mtk' => 'Matematika', 'fisika' => 'Fisika', 'kimia' => 'Kimia', 'biologi' => 'Biologi'] as $field => $label)
|
||||
<div class="input-field">
|
||||
<label>{{ $label }}</label>
|
||||
<input type="number" name="{{ $field }}" min="0" max="100" value="{{ old($field) }}" placeholder="0-100" required
|
||||
class="focus-primary @error($field) input-error @enderror">
|
||||
@error($field)
|
||||
<span class="validation-message" style="color: var(--error);">⚠️ {{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
{{-- SISWA IPS: Ekonomi, Geografi, Sosiologi, Sejarah --}}
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-2 sm:gap-3">
|
||||
<div>
|
||||
<label for="ekonomi" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Ekonomi <span class="text-red-500">*</span></label>
|
||||
<input id="ekonomi" type="number" name="ekonomi" min="0" max="100" value="{{ old('ekonomi') }}" placeholder="82" required
|
||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('ekonomi') border-red-500 @enderror">
|
||||
@error('ekonomi')
|
||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="geografi" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Geografi <span class="text-red-500">*</span></label>
|
||||
<input id="geografi" type="number" name="geografi" min="0" max="100" value="{{ old('geografi') }}" placeholder="76" required
|
||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('geografi') border-red-500 @enderror">
|
||||
@error('geografi')
|
||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="sosiologi" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Sosiologi <span class="text-red-500">*</span></label>
|
||||
<input id="sosiologi" type="number" name="sosiologi" min="0" max="100" value="{{ old('sosiologi') }}" placeholder="74" required
|
||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('sosiologi') border-red-500 @enderror">
|
||||
@error('sosiologi')
|
||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="sejarah" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Sejarah <span class="text-red-500">*</span></label>
|
||||
<input id="sejarah" type="number" name="sejarah" min="0" max="100" value="{{ old('sejarah') }}" placeholder="70" required
|
||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('sejarah') border-red-500 @enderror">
|
||||
@error('sejarah')
|
||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
@foreach(['ekonomi' => 'Ekonomi', 'geografi' => 'Geografi', 'sosiologi' => 'Sosiologi', 'sejarah' => 'Sejarah'] as $field => $label)
|
||||
<div class="input-field">
|
||||
<label>{{ $label }}</label>
|
||||
<input type="number" name="{{ $field }}" min="0" max="100" value="{{ old($field) }}" placeholder="0-100" required
|
||||
class="focus-primary @error($field) input-error @enderror">
|
||||
@error($field)
|
||||
<span class="validation-message" style="color: var(--error);">⚠️ {{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6">
|
||||
{{-- ============================================ --}}
|
||||
{{-- KRITERIA 2: MINAT SISWA --}}
|
||||
{{-- ============================================ --}}
|
||||
<div class="p-4 sm:p-5 rounded-lg border-2 border-gray-200 bg-gray-50">
|
||||
<h3 class="font-bold text-maroon text-base sm:text-lg mb-1 sm:mb-2">2. Minat Siswa <span class="text-red-500">*</span></h3>
|
||||
<p class="text-xs text-gray-600 mb-3 sm:mb-4">Tuliskan bidang atau kegiatan yang Anda minati / sukai.</p>
|
||||
<div>
|
||||
<label for="minat" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Bidang Minat</label>
|
||||
<input id="minat" type="text" name="minat" value="{{ old('minat') }}" placeholder="Contoh: coding, komputer, bisnis, pertanian"
|
||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('minat') border-red-500 @enderror" required>
|
||||
<p class="text-xs text-gray-500 mt-1">Pisahkan dengan koma jika lebih dari satu minat</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<!-- MINAT -->
|
||||
<section>
|
||||
<h3 class="section-title">
|
||||
<span style="font-size: 1.25rem;">2️⃣</span> Bidang Minat
|
||||
<span style="color: var(--error);">*</span>
|
||||
</h3>
|
||||
<p class="section-desc">Tuliskan bidang atau kegiatan yang Anda minati. Contoh: coding, pertanian, seni, dll.</p>
|
||||
<div class="input-field">
|
||||
<input type="text" name="minat" value="{{ old('minat') }}" placeholder="Minat Anda..." required
|
||||
class="focus-primary @error('minat') input-error @enderror">
|
||||
<span class="input-hint">Minimal 3 karakter, pisahkan dengan koma jika lebih dari satu</span>
|
||||
@error('minat')
|
||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
||||
<span class="validation-message" style="color: var(--error);">⚠️ {{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{-- ============================================ --}}
|
||||
{{-- KRITERIA 3: PREFERENSI STUDI LANJUTAN --}}
|
||||
{{-- ============================================ --}}
|
||||
<div class="p-4 sm:p-5 rounded-lg border-2 border-gray-200 bg-gray-50">
|
||||
<h3 class="font-bold text-maroon text-base sm:text-lg mb-1 sm:mb-2">3. Preferensi Studi Lanjutan <span class="text-red-500">*</span></h3>
|
||||
<p class="text-xs text-gray-600 mb-3 sm:mb-4">
|
||||
Preferensi studi lanjutan adalah kecenderungan pilihan Anda terhadap rumpun bidang studi yang ingin ditempuh setelah lulus, berdasarkan pertimbangan minat, bakat, kemampuan, dan prospek karir di masa depan.
|
||||
</p>
|
||||
<div>
|
||||
<label for="pref_studi" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Rumpun Bidang Studi</label>
|
||||
<select id="pref_studi" name="pref_studi" class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('pref_studi') border-red-500 @enderror" required>
|
||||
<option value="">-- Pilih Rumpun Bidang Studi --</option>
|
||||
<!-- PREFERENSI STUDI -->
|
||||
<section>
|
||||
<h3 class="section-title">
|
||||
<span style="font-size: 1.25rem;">3️⃣</span> Preferensi Studi
|
||||
<span style="color: var(--error);">*</span>
|
||||
</h3>
|
||||
<p class="section-desc">Pilih rumpun jurusan yang paling Anda minati untuk studi lanjutan.</p>
|
||||
<div class="input-field">
|
||||
<select name="pref_studi" required class="focus-primary @error('pref_studi') input-error @enderror">
|
||||
<option value="">-- Pilih Preferensi --</option>
|
||||
<option value="Sains & Teknologi" {{ old('pref_studi') == 'Sains & Teknologi' ? 'selected' : '' }}>Sains & Teknologi</option>
|
||||
<option value="Pertanian & Lingkungan" {{ old('pref_studi') == 'Pertanian & Lingkungan' ? 'selected' : '' }}>Pertanian & Lingkungan</option>
|
||||
<option value="Kesehatan & Ilmu Hayat" {{ old('pref_studi') == 'Kesehatan & Ilmu Hayat' ? 'selected' : '' }}>Kesehatan & Ilmu Hayat</option>
|
||||
<option value="Bisnis & Manajemen" {{ old('pref_studi') == 'Bisnis & Manajemen' ? 'selected' : '' }}>Bisnis & Manajemen</option>
|
||||
<option value="Sosial & Humaniora" {{ old('pref_studi') == 'Sosial & Humaniora' ? 'selected' : '' }}>Sosial, Bahasa & Humaniora</option>
|
||||
<option value="Sosial & Humaniora" {{ old('pref_studi') == 'Sosial & Humaniora' ? 'selected' : '' }}>Sosial & Humaniora</option>
|
||||
</select>
|
||||
<p class="text-xs text-gray-500 mt-1">Pilih rumpun studi yang paling mendekati kecenderungan Anda</p>
|
||||
@error('pref_studi')
|
||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
||||
<span class="validation-message" style="color: var(--error);">⚠️ {{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ============================================ --}}
|
||||
{{-- KRITERIA 4: CITA-CITA / PREFERENSI KARIR --}}
|
||||
{{-- ============================================ --}}
|
||||
<div class="p-4 sm:p-5 rounded-lg border-2 border-gray-200 bg-gray-50">
|
||||
<h3 class="font-bold text-maroon text-base sm:text-lg mb-1 sm:mb-2">4. Cita-cita / Preferensi Karir <span class="text-red-500">*</span></h3>
|
||||
<p class="text-xs text-gray-600 mb-3 sm:mb-4">Tuliskan profesi atau karir yang Anda impikan.</p>
|
||||
<div>
|
||||
<label for="cita_cita" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Cita-cita</label>
|
||||
<input id="cita_cita" type="text" name="cita_cita" value="{{ old('cita_cita') }}" placeholder="Contoh: programmer, dokter, pengusaha"
|
||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('cita_cita') border-red-500 @enderror" required>
|
||||
<p class="text-xs text-gray-500 mt-1">Bisa lebih dari satu, pisahkan dengan koma</p>
|
||||
@error('cita_cita')
|
||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ============================================ --}}
|
||||
{{-- KRITERIA 5: PRESTASI AKADEMIK / NON-AKADEMIK --}}
|
||||
{{-- ============================================ --}}
|
||||
<div class="p-4 sm:p-5 rounded-lg border-2 border-gray-200 bg-gray-50">
|
||||
<h3 class="font-bold text-maroon text-base sm:text-lg mb-1 sm:mb-2">5. Prestasi Akademik / Non-Akademik</h3>
|
||||
<p class="text-xs text-gray-600 mb-3 sm:mb-4">Tuliskan prestasi yang pernah diraih (opsional).</p>
|
||||
<div>
|
||||
<label for="prestasi" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Prestasi</label>
|
||||
<input id="prestasi" type="text" name="prestasi" value="{{ old('prestasi') }}" placeholder="Contoh: Juara 1 olimpiade MTK, sertifikat web design"
|
||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('prestasi') border-red-500 @enderror">
|
||||
<p class="text-xs text-gray-500 mt-1">Kosongkan jika belum ada prestasi</p>
|
||||
@error('prestasi')
|
||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="mt-4 sm:mt-6 p-3 sm:p-4 rounded-lg bg-gray-50 border border-gray-200 text-center">
|
||||
<p class="text-xs sm:text-sm text-gray-600 mb-3 sm:mb-4">
|
||||
Setelah menekan tombol, sistem akan menganalisis data Anda dan menampilkan ranking 9 jurusan.
|
||||
</p>
|
||||
<button type="submit" class="w-full gradient-maroon text-white font-bold py-2 sm:py-3 px-4 sm:px-6 rounded-lg hover:opacity-90 transition duration-200 text-sm sm:text-base">
|
||||
Lihat Rekomendasi Jurusan
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<!-- CITA-CITA -->
|
||||
<section>
|
||||
<h3 class="section-title">
|
||||
<span style="font-size: 1.25rem;">4️⃣</span> Cita-cita / Karir Impian
|
||||
<span style="color: var(--error);">*</span>
|
||||
</h3>
|
||||
<p class="section-desc">Tuliskan profesi atau karir yang Anda impikan di masa depan.</p>
|
||||
<div class="input-field">
|
||||
<input type="text" name="cita_cita" value="{{ old('cita_cita') }}" placeholder="Contoh: programmer, dokter, pengusaha..." required
|
||||
class="focus-primary @error('cita_cita') input-error @enderror">
|
||||
<span class="input-hint">Minimal 3 karakter, pisahkan dengan koma jika lebih dari satu</span>
|
||||
@error('cita_cita')
|
||||
<span class="validation-message" style="color: var(--error);">⚠️ {{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PRESTASI -->
|
||||
<section>
|
||||
<h3 class="section-title">
|
||||
<span style="font-size: 1.25rem;">5️⃣</span> Prestasi (Opsional)
|
||||
</h3>
|
||||
<p class="section-desc">Tuliskan prestasi akademik atau non-akademik yang pernah Anda raih.</p>
|
||||
<div class="input-field">
|
||||
<input type="text" name="prestasi" value="{{ old('prestasi') }}" placeholder="Contoh: Juara olimpiade, sertifikat programming..."
|
||||
class="focus-primary @error('prestasi') input-error @enderror">
|
||||
<span class="input-hint">Kolom ini bersifat opsional</span>
|
||||
@error('prestasi')
|
||||
<span class="validation-message" style="color: var(--error);">⚠️ {{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- SUBMIT BUTTON -->
|
||||
<div class="pt-4">
|
||||
<button type="submit" class="btn-primary w-full py-3 rounded-lg font-semibold text-white text-base md:text-lg shadow-md hover:shadow-lg transition">
|
||||
✨ Analisis & Lihat Rekomendasi
|
||||
</button>
|
||||
<p style="font-size: 0.875rem; color: var(--text-secondary); margin-top: 1rem; text-align: center;">
|
||||
⏳ Proses analisis membutuhkan waktu 1-2 detik
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Info Metode -->
|
||||
<div class="mt-6 sm:mt-8 p-3 sm:p-4 bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||
<p class="text-xs sm:text-sm text-gray-600">
|
||||
<strong>Metode:</strong> Sistem menggunakan Graduated Scoring dengan 5 kriteria: Nilai Akademik (40%), Minat & Bakat (35%), Preferensi Studi (15%), Cita-cita (5%), Prestasi (5%).
|
||||
<!-- Riwayat Rekomendasi Section -->
|
||||
@php
|
||||
$recommendations = $recommendations ?? [];
|
||||
@endphp
|
||||
|
||||
@if(count($recommendations) > 0)
|
||||
<div class="mt-8 sm:mt-12">
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 border-l-4 border-purple-500">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="w-12 h-12 sm:w-14 sm:h-14 rounded-lg bg-purple-100 flex items-center justify-center text-2xl flex-shrink-0">📋</div>
|
||||
<div>
|
||||
<h2 class="text-lg sm:text-xl md:text-2xl font-bold text-maroon">Riwayat Rekomendasi</h2>
|
||||
<p class="text-xs sm:text-sm text-gray-600">Analisis yang sudah Anda lakukan sebelumnya</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 sm:space-y-4">
|
||||
@foreach($recommendations as $rec)
|
||||
<div class="border border-gray-200 rounded-lg p-4 hover:shadow-md hover:border-maroon transition cursor-pointer" onclick="this.classList.toggle('expanded')">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<div class="flex-1">
|
||||
<p class="text-xs sm:text-sm text-gray-500">{{ $rec->created_at->format('d M Y - H:i') }}</p>
|
||||
<p class="text-sm sm:text-base font-bold text-maroon mt-1">Rekomendasi Utama: <span class="text-lg">{{ $rec->hasil_rekomendasi[0]['jurusan'] ?? 'N/A' }}</span></p>
|
||||
</div>
|
||||
<span class="inline-block px-3 py-1 rounded-full text-xs font-bold bg-purple-100 text-purple-700 flex-shrink-0 ml-2">
|
||||
{{ number_format(($rec->hasil_rekomendasi[0]['skor'] ?? 0) * 100, 1) }}%
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 mb-3">Top 3 Rekomendasi:</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
|
||||
@foreach(array_slice($rec->hasil_rekomendasi, 0, 3) as $idx => $rec_item)
|
||||
<div class="bg-gradient-to-br from-purple-50 to-purple-100 p-3 rounded text-center">
|
||||
<p class="text-xs text-gray-600">{{ $idx + 1 }}. {{ $rec_item['jurusan'] }}</p>
|
||||
<p class="text-sm font-bold text-maroon">{{ number_format($rec_item['skor'] * 100, 1) }}%</p>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
<!-- Footer Info -->
|
||||
<div class="card p-4 md:p-5 mt-8 bg-gradient-to-r from-teal-50 to-cyan-50">
|
||||
<p style="font-size: 0.875rem; color: var(--text-secondary);">
|
||||
<strong>📊 Metode Analisis:</strong> Weighted Naive Bayes dengan 5 kriteria yang diseimbangkan berdasarkan data historis siswa Polije.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.querySelector('form');
|
||||
const submitBtn = form?.querySelector('button[type="submit"]');
|
||||
const inputs = form?.querySelectorAll('input, select, textarea');
|
||||
|
||||
inputs?.forEach(input => {
|
||||
input.addEventListener('change', () => validateField(input));
|
||||
input.addEventListener('blur', () => validateField(input));
|
||||
input.addEventListener('input', function() {
|
||||
if (this.classList.contains('input-error')) {
|
||||
validateField(this);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
form?.addEventListener('submit', function(e) {
|
||||
let isValid = true;
|
||||
inputs?.forEach(input => {
|
||||
if (!validateField(input)) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
e.preventDefault();
|
||||
const firstError = form.querySelector('.input-error');
|
||||
if (firstError) {
|
||||
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
firstError.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function validateField(field) {
|
||||
const value = field.value.trim();
|
||||
const isRequired = field.hasAttribute('required');
|
||||
const type = field.type;
|
||||
let isValid = true;
|
||||
|
||||
field.classList.remove('input-error', 'input-valid');
|
||||
const existingMsg = field.parentElement.querySelector('.validation-message');
|
||||
if (existingMsg) existingMsg.remove();
|
||||
|
||||
if (isRequired && !value) {
|
||||
isValid = false;
|
||||
} else if (type === 'number' && value) {
|
||||
const num = parseFloat(value);
|
||||
if (isNaN(num) || num < 0 || num > 100) {
|
||||
isValid = false;
|
||||
}
|
||||
} else if ((field.name === 'minat' || field.name === 'cita_cita') && value && value.length < 3) {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!isValid && (value || isRequired)) {
|
||||
field.classList.add('input-error');
|
||||
} else if (isValid && (isRequired || value)) {
|
||||
field.classList.add('input-valid');
|
||||
}
|
||||
|
||||
return isValid || !isRequired;
|
||||
}
|
||||
|
||||
form?.addEventListener('submit', function() {
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '⏳ Menganalisis...';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue