LearnMood/app/Http/Controllers/Siswa/ActivityController.php

595 lines
22 KiB
PHP

<?php
// app/Http/Controllers/Siswa/ActivityController.php
namespace App\Http\Controllers\Siswa;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Models\ActivityLog;
use App\Models\Recommendation;
use App\Services\FlaskModelService;
use App\Services\RecommendationCalculator;
use Carbon\Carbon;
class ActivityController extends Controller
{
protected $flaskService;
protected $calculator;
public function __construct(FlaskModelService $flaskService, RecommendationCalculator $calculator)
{
$this->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);
}
}