flaskService = $flaskService; $this->calculator = $calculator; } /** * Tampilkan form input aktivitas */ public function create() { // Cek apakah sudah input hari ini $todayActivity = ActivityLog::where('user_id', Auth::id()) ->whereDate('activity_date', Carbon::today()) ->first(); if ($todayActivity) { return redirect()->route('siswa.input.edit-today') ->with('info', 'Kamu sudah menginput aktivitas hari ini. Silakan edit jika ada perubahan.'); } return view('siswa.input'); } /** * Tampilkan form edit aktivitas hari ini */ public function editToday() { $activity = ActivityLog::where('user_id', Auth::id()) ->whereDate('activity_date', Carbon::today()) ->first(); if (!$activity) { return redirect()->route('siswa.input') ->with('info', 'Belum ada aktivitas hari ini. Silakan input aktivitas dulu.'); } return view('siswa.input', [ 'activity' => $activity, 'isEdit' => true, ]); } /** * Simpan aktivitas dan generate rekomendasi */ public function store(Request $request) { // Log request data Log::info('=== MEMULAI PROSES INPUT ==='); Log::info('User ID: ' . Auth::id()); Log::info('Request data: ', $request->all()); // Validasi input try { $validated = $request->validate([ 'start_time' => 'required', 'end_time' => 'required|after:start_time', 'mood' => 'required|in:Bagus,Lumayan,Biasa Saja,Cukup Jenuh,Jenuh', 'sleep_hours' => 'required|numeric|min:1|max:10', ]); Log::info('Validasi berhasil'); } catch (\Exception $e) { Log::error('Validasi gagal: ' . $e->getMessage()); return redirect()->back() ->withErrors(['error' => 'Validasi gagal: ' . $e->getMessage()]) ->withInput(); } try { DB::beginTransaction(); $user = Auth::user(); $todayActivity = ActivityLog::where('user_id', $user->id) ->whereDate('activity_date', Carbon::today()) ->first(); if ($todayActivity) { DB::rollBack(); return redirect()->route('siswa.input.edit-today') ->with('info', 'Aktivitas hari ini sudah ada. Gunakan form edit untuk memperbarui data.'); } // Hitung durasi belajar dalam menit $start = Carbon::parse($request->start_time); $end = Carbon::parse($request->end_time); $durationMinutes = $end->diffInMinutes($start); Log::info("Durasi belajar: {$durationMinutes} menit"); // Ambil nilai sleep hours $sleepHours = floatval($request->sleep_hours); Log::info("Durasi tidur: {$sleepHours} jam"); // Simpan activity $activity = ActivityLog::create([ 'user_id' => $user->id, 'start_time' => $request->start_time, 'end_time' => $request->end_time, 'duration_minutes' => $durationMinutes, 'mood' => $request->mood, 'sleep_hours' => $sleepHours, 'activity_date' => Carbon::today(), ]); Log::info('Activity berhasil disimpan dengan ID: ' . $activity->id); // Hitung total hari user sudah input $totalDays = ActivityLog::where('user_id', $user->id)->count(); Log::info("Total hari input: {$totalDays}"); // Generate rekomendasi hybrid Log::info('Mulai generate rekomendasi...'); $recommendation = $this->generateHybridRecommendation($user->id, $activity, $totalDays); Log::info('Rekomendasi berhasil digenerate dengan ID: ' . $recommendation->id); DB::commit(); $message = $totalDays <= 7 ? "Aktivitas berhasil disimpan! (Mode: Model Harian - Hari ke-{$totalDays}/7)" : "Aktivitas berhasil disimpan! (Mode: Pola Siswa - Hari ke-{$totalDays})"; Log::info('=== PROSES SELESAI ==='); // Redirect ke halaman rekomendasi return redirect()->route('siswa.recommendations') ->with('success', $message); } catch (\Exception $e) { DB::rollBack(); Log::error('ERROR di store: ' . $e->getMessage()); Log::error('Stack trace: ' . $e->getTraceAsString()); return redirect()->back() ->with('error', 'Gagal menyimpan aktivitas: ' . $e->getMessage()) ->withInput(); } } /** * Update aktivitas hari ini dan hitung ulang rekomendasi */ public function updateToday(Request $request) { Log::info('=== MEMULAI PROSES UPDATE AKTIVITAS HARI INI ==='); Log::info('User ID: ' . Auth::id()); Log::info('Request data: ', $request->all()); try { $validated = $request->validate([ 'start_time' => 'required', 'end_time' => 'required|after:start_time', 'mood' => 'required|in:Bagus,Lumayan,Biasa Saja,Cukup Jenuh,Jenuh', 'sleep_hours' => 'required|numeric|min:1|max:10', ]); Log::info('Validasi update berhasil'); } catch (\Exception $e) { Log::error('Validasi update gagal: ' . $e->getMessage()); return redirect()->back() ->withErrors(['error' => 'Validasi gagal: ' . $e->getMessage()]) ->withInput(); } try { DB::beginTransaction(); $user = Auth::user(); $activity = ActivityLog::where('user_id', $user->id) ->whereDate('activity_date', Carbon::today()) ->firstOrFail(); $start = Carbon::parse($request->start_time); $end = Carbon::parse($request->end_time); $lastEnd = Carbon::parse($activity->end_time); if ($start->lt($lastEnd)) { DB::rollBack(); return redirect()->back() ->withErrors(['start_time' => 'Jam mulai tambahan tidak boleh sebelum jam selesai aktivitas sebelumnya.']) ->withInput(); } $additionalMinutes = $end->diffInMinutes($start); $durationMinutes = $activity->duration_minutes + $additionalMinutes; $sleepHours = floatval($request->sleep_hours); $activity->update([ 'start_time' => $activity->start_time, 'end_time' => $request->end_time, 'duration_minutes' => $durationMinutes, 'mood' => $request->mood, 'sleep_hours' => $sleepHours, 'activity_date' => Carbon::today(), ]); $totalDays = ActivityLog::where('user_id', $user->id)->count(); Log::info("Total hari input setelah update: {$totalDays}"); $recommendation = $this->generateHybridRecommendation($user->id, $activity, $totalDays); Log::info('Rekomendasi berhasil diperbarui dengan ID: ' . $recommendation->id); DB::commit(); return redirect()->route('siswa.recommendations') ->with('success', 'Aktivitas hari ini berhasil diperbarui dan rekomendasi sudah dihitung ulang.'); } catch (\Exception $e) { DB::rollBack(); Log::error('ERROR di updateToday: ' . $e->getMessage()); Log::error('Stack trace: ' . $e->getTraceAsString()); return redirect()->back() ->with('error', 'Gagal memperbarui aktivitas: ' . $e->getMessage()) ->withInput(); } } /** * Generate rekomendasi hybrid berdasarkan jumlah hari */ private function generateHybridRecommendation($userId, $activity, $totalDays) { $recommendation = []; $message = ''; if ($totalDays <= 7) { // 7 HARI PERTAMA: Gunakan model harian (Flask) Log::info("Hari ke-{$totalDays}: Menggunakan model harian"); $result = $this->flaskService->predictDaily( $activity->mood, $activity->duration_minutes, $activity->sleep_hours ); if ($result['success']) { // ✅ FLASK SUKSES - dapat kategori dari ML $category = $result['category']; // Hitung durasi personal dengan calculator $durationResult = $this->calculator->calculateDuration( $userId, $category, $activity->mood, $activity->sleep_hours ); $recommendation = [ 'category' => $category, 'recommended_minutes' => $durationResult['minutes'], 'notes' => $durationResult['notes'], 'based_on' => 'daily_model', 'confidence' => $result['confidence'] ?? null, 'factors' => $durationResult['factors'] ?? [], 'adjustment' => $durationResult['adjustment'] ?? 0 ]; $message = 'Rekomendasi berdasarkan model harian dengan penyesuaian personal.'; Log::info("ML Prediction: {$category}, Durasi personal: {$durationResult['minutes']} menit"); } else { // ❌ FLASK GAGAL - fallback ke aturan manual Log::warning('Flask API gagal, menggunakan aturan manual'); $manualResult = $this->generateManualRecommendation($activity); // Hitung durasi personal untuk manual result $durationResult = $this->calculator->calculateDuration( $userId, $manualResult['category'], $activity->mood, $activity->sleep_hours ); $recommendation = [ 'category' => $manualResult['category'], 'recommended_minutes' => $durationResult['minutes'], 'notes' => $durationResult['notes'] . ' (Fallback manual)', 'based_on' => 'manual_rules', 'confidence' => null, 'factors' => $durationResult['factors'] ?? [], 'adjustment' => $durationResult['adjustment'] ?? 0 ]; $message = 'Rekomendasi berdasarkan aturan manual (model server sedang maintenance).'; } } else { // HARI KE-8 dst: Gunakan pola siswa Log::info("Hari ke-{$totalDays}: Menggunakan pola siswa"); $result = $this->flaskService->predictBasedOnPattern($userId); if ($result['success']) { // ✅ ANALISIS POLA SUKSES $category = $result['category']; // Hitung durasi personal dengan calculator (dengan data pola) $durationResult = $this->calculator->calculateDuration( $userId, $category, $activity->mood, $activity->sleep_hours ); $recommendation = [ 'category' => $category, 'recommended_minutes' => $durationResult['minutes'], 'notes' => $durationResult['notes'], 'based_on' => 'student_pattern', 'pattern_analysis' => $result['pattern_analysis'] ?? null, 'factors' => $durationResult['factors'] ?? [], 'adjustment' => $durationResult['adjustment'] ?? 0 ]; $message = 'Rekomendasi berdasarkan pola belajarmu selama 7 hari terakhir.'; } else { // ❌ ANALISIS POLA GAGAL - fallback ke model harian Log::warning('Pattern analysis gagal, fallback ke model harian'); $dailyResult = $this->flaskService->predictDaily( $activity->mood, $activity->duration_minutes, $activity->sleep_hours ); if ($dailyResult['success']) { $category = $dailyResult['category']; $durationResult = $this->calculator->calculateDuration( $userId, $category, $activity->mood, $activity->sleep_hours ); $recommendation = [ 'category' => $category, 'recommended_minutes' => $durationResult['minutes'], 'notes' => $durationResult['notes'] . ' (Fallback dari pola)', 'based_on' => 'daily_model_fallback', 'confidence' => $dailyResult['confidence'] ?? null, 'factors' => $durationResult['factors'] ?? [], 'adjustment' => $durationResult['adjustment'] ?? 0 ]; $message = 'Rekomendasi berdasarkan model harian (analisis pola belum tersedia).'; } else { // 🚨 FALLBACK TERAKHIR - aturan manual Log::warning('Semua fallback gagal, menggunakan aturan manual'); $manualResult = $this->generateManualRecommendation($activity); $durationResult = $this->calculator->calculateDuration( $userId, $manualResult['category'], $activity->mood, $activity->sleep_hours ); $recommendation = [ 'category' => $manualResult['category'], 'recommended_minutes' => $durationResult['minutes'], 'notes' => $durationResult['notes'] . ' (Fallback darurat)', 'based_on' => 'emergency_fallback', 'confidence' => null, 'factors' => $durationResult['factors'] ?? [], 'adjustment' => $durationResult['adjustment'] ?? 0 ]; $message = 'Rekomendasi berdasarkan aturan dasar.'; } } } // PASTIKAN ARRAY $recommendation PUNYA SEMUA KEY YANG DIPERLUKAN if (!isset($recommendation['based_on'])) { $recommendation['based_on'] = 'unknown'; Log::warning('based_on tidak diset, menggunakan default unknown'); } // Simpan atau perbarui rekomendasi untuk aktivitas ini $savedRec = Recommendation::updateOrCreate([ 'activity_log_id' => $activity->id, ], [ 'user_id' => $userId, 'category' => $recommendation['category'], 'recommended_minutes' => $recommendation['recommended_minutes'], 'notes' => $recommendation['notes'], 'based_on' => $recommendation['based_on'], 'confidence' => $recommendation['confidence'] ?? null, 'model_input' => json_encode([ 'mood' => $activity->mood, 'duration_minutes' => $activity->duration_minutes, 'sleep_hours' => $activity->sleep_hours, 'total_days' => $totalDays, 'factors' => $recommendation['factors'] ?? [], 'adjustment' => $recommendation['adjustment'] ?? 0, 'pattern_analysis' => $recommendation['pattern_analysis'] ?? null ]), 'recommendation_date' => Carbon::today(), ]); Log::info('Rekomendasi tersimpan:', [ 'id' => $savedRec->id, 'category' => $savedRec->category, 'minutes' => $savedRec->recommended_minutes, 'based_on' => $savedRec->based_on ]); return $savedRec; } /** * Generate rekomendasi manual (fallback) */ private function generateManualRecommendation($activity) { // Hitung skor manual berdasarkan proposal $moodScore = match($activity->mood) { 'Bagus' => 2.0, 'Lumayan' => 1.5, 'Biasa Saja' => 1.0, 'Cukup Jenuh' => 0.5, 'Jenuh' => 0.0, default => 1.0 }; $durationScore = $activity->duration_minutes > 60 ? 2.0 : ($activity->duration_minutes >= 31 ? 1.0 : 0.0); $sleepScore = $activity->sleep_hours >= 7 ? 2.0 : ($activity->sleep_hours >= 4 ? 1.0 : 0.0); $totalScore = $moodScore + $durationScore + $sleepScore; // Tentukan kategori berdasarkan total skor if ($totalScore > 4.5) { $category = 'Intensif'; } elseif ($totalScore > 3) { $category = 'Sedang'; } else { $category = 'Ringan'; } Log::info('Manual recommendation:', [ 'scores' => [$moodScore, $durationScore, $sleepScore], 'total' => $totalScore, 'category' => $category ]); return [ 'category' => $category, 'based_on' => 'manual_rules' ]; } /** * Helper untuk mendapatkan durasi base dari kategori */ private function getMinutesFromCategory($category) { return match($category) { 'Ringan' => 30, 'Sedang' => 45, 'Intensif' => 90, default => 30 }; } /** * Generate notes untuk daily model (tidak dipakai langsung karena calculator sudah generate notes) */ private function generateDailyNotes($result, $day, $activity) { $category = $result['category']; $confidence = $result['confidence'] ?? 0; $notes = "Hari ke-{$day} belajar. "; $sleepQuality = $this->getSleepQuality($activity->sleep_hours); $notes .= match($category) { 'Ringan' => "Kondisi kamu kurang fit (tidur {$activity->sleep_hours} jam - {$sleepQuality}).", 'Sedang' => "Kamu dalam kondisi cukup baik (tidur {$activity->sleep_hours} jam - {$sleepQuality}).", 'Intensif' => "Kondisi kamu sangat baik (tidur {$activity->sleep_hours} jam - {$sleepQuality})!", default => 'Tetap semangat belajar!' }; if ($confidence > 0.9) { $notes .= ' Prediksi dengan keyakinan tinggi.'; } return $notes; } /** * Helper untuk kualitas tidur */ private function getSleepQuality($hours) { if ($hours < 5) return 'kurang tidur'; if ($hours < 7) return 'kurang ideal'; if ($hours <= 9) return 'ideal'; return 'berlebih'; } /** * Tampilkan history aktivitas */ public function history(Request $request) { $user = Auth::user(); // Ambil parameter filter $period = $request->get('period', 'all'); // all, 7, 30, 90 $mood = $request->get('mood', 'all'); $search = $request->get('search', ''); // Query dasar $query = ActivityLog::where('user_id', $user->id) ->with('recommendation') ->orderBy('activity_date', 'desc'); // Filter periode if ($period == '7') { $query->whereDate('activity_date', '>=', Carbon::today()->subDays(7)); } elseif ($period == '30') { $query->whereDate('activity_date', '>=', Carbon::today()->subDays(30)); } elseif ($period == '90') { $query->whereDate('activity_date', '>=', Carbon::today()->subDays(90)); } // Filter mood if ($mood != 'all') { $query->where('mood', $mood); } // Pencarian (di notes) if (!empty($search)) { $query->where('notes', 'like', '%' . $search . '%'); } // Ambil data dengan pagination $activities = $query->paginate(15)->withQueryString(); // Statistik untuk header $stats = [ 'total' => ActivityLog::where('user_id', $user->id)->count(), 'total_duration' => ActivityLog::where('user_id', $user->id)->sum('duration_minutes'), 'avg_duration' => round(ActivityLog::where('user_id', $user->id)->avg('duration_minutes') ?? 0), 'consistency' => $this->calculateConsistency($user->id), ]; // Daftar mood untuk filter dropdown $moodList = ['Bagus', 'Lumayan', 'Biasa Saja', 'Cukup Jenuh', 'Jenuh']; return view('siswa.history', compact('activities', 'stats', 'period', 'mood', 'search', 'moodList')); } private function calculateConsistency($userId) { $total = ActivityLog::where('user_id', $userId)->count(); if ($total == 0) return 0; $firstDate = ActivityLog::where('user_id', $userId)->min('activity_date'); if (!$firstDate) return 0; $daysDiff = Carbon::parse($firstDate)->diffInDays(Carbon::today()) + 1; return round(($total / $daysDiff) * 100); } }