595 lines
22 KiB
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);
|
|
}
|
|
}
|