458 lines
15 KiB
PHP
458 lines
15 KiB
PHP
<?php
|
|
// app/Services/RecommendationCalculator.php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\ActivityLog;
|
|
use App\Models\Recommendation;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class RecommendationCalculator
|
|
{
|
|
/**
|
|
* Hitung durasi rekomendasi berdasarkan kategori dan data historis
|
|
*/
|
|
public function calculateDuration($userId, $category, $currentMood, $currentSleep)
|
|
{
|
|
// Base duration per kategori (dibuat variatif)
|
|
$baseDurations = [
|
|
'Ringan' => 30,
|
|
'Sedang' => 45,
|
|
'Intensif' => 80 // Base 80, bukan 90, agar lebih bervariasi
|
|
];
|
|
|
|
$baseDuration = $baseDurations[$category];
|
|
|
|
// Ambil data 7 hari terakhir untuk analisis
|
|
$recentActivities = ActivityLog::where('user_id', $userId)
|
|
->with('recommendation')
|
|
->orderBy('activity_date', 'desc')
|
|
->take(7)
|
|
->get();
|
|
|
|
// Jika belum punya history, return base duration dengan variasi ringan
|
|
if ($recentActivities->isEmpty()) {
|
|
$initialVariation = $this->getInitialVariation($category);
|
|
$finalDuration = $baseDuration + $initialVariation;
|
|
$finalDuration = $this->ensureInRange($finalDuration, $category);
|
|
|
|
return [
|
|
'minutes' => $finalDuration,
|
|
'notes' => $this->generateSimpleNotes($category, $finalDuration),
|
|
'factors' => ['new_user'],
|
|
'adjustment' => $initialVariation,
|
|
'base_duration' => $baseDuration
|
|
];
|
|
}
|
|
|
|
// ===========================================
|
|
// FAKTOR-FAKTOR PENYESUAIAN
|
|
// ===========================================
|
|
|
|
$adjustment = 0;
|
|
$factors = [];
|
|
|
|
// 1. FAKTOR KONSISTENSI
|
|
$consistencyScore = $this->calculateConsistency($recentActivities);
|
|
if ($consistencyScore > 0.7) {
|
|
$adjustment += 5;
|
|
$factors[] = 'konsisten';
|
|
} elseif ($consistencyScore < 0.3) {
|
|
$adjustment -= 5;
|
|
$factors[] = 'tidak konsisten';
|
|
}
|
|
|
|
// 2. FAKTOR TREND
|
|
$trend = $this->calculateTrend($recentActivities);
|
|
if ($trend == 'meningkat') {
|
|
$adjustment += 4;
|
|
$factors[] = 'trend meningkat';
|
|
} elseif ($trend == 'menurun') {
|
|
$adjustment -= 4;
|
|
$factors[] = 'trend menurun';
|
|
}
|
|
|
|
// 3. FAKTOR MOOD
|
|
$moodScore = $this->getMoodWeight($currentMood);
|
|
if ($moodScore >= 1.5) {
|
|
$adjustment += 3;
|
|
$factors[] = 'mood positif';
|
|
} elseif ($moodScore <= 0.5) {
|
|
$adjustment -= 5;
|
|
$factors[] = 'mood negatif';
|
|
}
|
|
|
|
// 4. FAKTOR TIDUR
|
|
if ($currentSleep >= 7 && $currentSleep <= 9) {
|
|
$adjustment += 3;
|
|
$factors[] = 'tidur ideal';
|
|
} elseif ($currentSleep < 5) {
|
|
$adjustment -= 5;
|
|
$factors[] = 'kurang tidur';
|
|
} elseif ($currentSleep > 9) {
|
|
$adjustment += 1;
|
|
$factors[] = 'tidur panjang';
|
|
}
|
|
|
|
// 5. FAKTOR BEBAN KOGNITIF
|
|
$intensiveDays = $recentActivities->take(3)
|
|
->filter(function($activity) {
|
|
return $activity->recommendation && $activity->recommendation->category == 'Intensif';
|
|
})->count();
|
|
|
|
if ($intensiveDays >= 2) {
|
|
$adjustment -= 5;
|
|
$factors[] = 'perlu recovery';
|
|
}
|
|
|
|
// 6. FAKTOR ADAPTASI (Individual Differences)
|
|
$avgActual = $recentActivities->avg('duration_minutes');
|
|
$avgRecommended = $recentActivities->filter(function($a) {
|
|
return $a->recommendation;
|
|
})->avg(function($a) {
|
|
return $a->recommendation->recommended_minutes ?? 0;
|
|
});
|
|
|
|
if ($avgActual > $avgRecommended * 1.2) {
|
|
$adjustment += 5;
|
|
$factors[] = 'kapasitas tinggi';
|
|
} elseif ($avgActual < $avgRecommended * 0.7) {
|
|
$adjustment -= 5;
|
|
$factors[] = 'target terlalu tinggi';
|
|
}
|
|
|
|
// 7. FAKTOR STREAK (Hari beruntun)
|
|
$streak = $this->calculateStreak($recentActivities);
|
|
if ($streak >= 5) {
|
|
$adjustment += 5;
|
|
$factors[] = 'streak 5+ hari';
|
|
} elseif ($streak >= 3) {
|
|
$adjustment += 3;
|
|
$factors[] = 'streak 3+ hari';
|
|
}
|
|
|
|
// 8. FAKTOR KESESUAIAN KATEGORI
|
|
$sameCategory = $recentActivities->take(5)
|
|
->filter(function($a) use ($category) {
|
|
return $a->recommendation && $a->recommendation->category == $category;
|
|
})->count();
|
|
|
|
if ($sameCategory >= 4) {
|
|
$adjustment += 3;
|
|
$factors[] = 'konsisten di kategori';
|
|
}
|
|
|
|
// ===========================================
|
|
// VARIASI KHUSUS PER KATEGORI
|
|
// ===========================================
|
|
|
|
if ($category == 'Intensif') {
|
|
// Variasi untuk Intensif (75-115)
|
|
|
|
// Berdasarkan durasi aktual
|
|
if ($avgActual > 100) {
|
|
$adjustment += rand(5, 10);
|
|
$factors[] = 'biasa belajar panjang';
|
|
} elseif ($avgActual > 80) {
|
|
$adjustment += rand(0, 5);
|
|
$factors[] = 'cukup terbiasa';
|
|
} else {
|
|
$adjustment -= rand(0, 5);
|
|
$factors[] = 'baru masuk intensif';
|
|
}
|
|
|
|
// Variasi berdasarkan mood
|
|
if ($currentMood == 'Bagus') {
|
|
$adjustment += rand(2, 5);
|
|
} elseif ($currentMood == 'Lumayan') {
|
|
$adjustment += rand(0, 3);
|
|
}
|
|
|
|
// Random factor untuk variasi alami (±5)
|
|
$adjustment += rand(-5, 5);
|
|
|
|
} elseif ($category == 'Sedang') {
|
|
// Variasi untuk Sedang (35-65)
|
|
|
|
// Berdasarkan trend
|
|
if ($trend == 'meningkat') {
|
|
$adjustment += rand(3, 7);
|
|
$factors[] = 'menuju intensif';
|
|
} elseif ($trend == 'menurun') {
|
|
$adjustment -= rand(3, 7);
|
|
$factors[] = 'menuju ringan';
|
|
}
|
|
|
|
// Random factor kecil
|
|
$adjustment += rand(-3, 3);
|
|
|
|
} elseif ($category == 'Ringan') {
|
|
// Variasi untuk Ringan (20-40)
|
|
|
|
// Jika mood jelek, kurangi
|
|
if ($currentMood == 'Jenuh' || $currentMood == 'Cukup Jenuh') {
|
|
$adjustment -= rand(0, 5);
|
|
$factors[] = 'butuh istirahat';
|
|
}
|
|
|
|
// Random factor kecil
|
|
$adjustment += rand(-2, 2);
|
|
}
|
|
|
|
// ===========================================
|
|
// HITUNG DURASI FINAL
|
|
// ===========================================
|
|
|
|
// Batasi adjustment agar tidak terlalu ekstrim
|
|
$maxAdjustment = $this->getMaxAdjustment($category);
|
|
$adjustment = max(min($adjustment, $maxAdjustment), -$maxAdjustment);
|
|
|
|
$finalDuration = $baseDuration + $adjustment;
|
|
|
|
// Bulatkan ke 5 menit terdekat
|
|
$finalDuration = round($finalDuration / 5) * 5;
|
|
|
|
// Pastikan dalam range yang wajar per kategori
|
|
$finalDuration = $this->ensureInRange($finalDuration, $category);
|
|
|
|
// Generate notes yang personal
|
|
$notes = $this->generatePersonalNotes($category, $factors, $adjustment, $finalDuration, $streak);
|
|
|
|
Log::info('Recommendation calculated:', [
|
|
'user_id' => $userId,
|
|
'category' => $category,
|
|
'base' => $baseDuration,
|
|
'adjustment' => $adjustment,
|
|
'final' => $finalDuration,
|
|
'factors' => $factors
|
|
]);
|
|
|
|
return [
|
|
'minutes' => $finalDuration,
|
|
'notes' => $notes,
|
|
'factors' => $factors,
|
|
'adjustment' => $adjustment,
|
|
'base_duration' => $baseDuration
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Variasi awal untuk user baru
|
|
*/
|
|
private function getInitialVariation($category)
|
|
{
|
|
$variations = [
|
|
'Ringan' => rand(-2, 5),
|
|
'Sedang' => rand(-3, 8),
|
|
'Intensif' => rand(-5, 15)
|
|
];
|
|
|
|
return $variations[$category];
|
|
}
|
|
|
|
/**
|
|
* Hitung skor konsistensi (0-1)
|
|
*/
|
|
private function calculateConsistency($activities)
|
|
{
|
|
if ($activities->count() < 3) return 0.5;
|
|
|
|
$durations = $activities->pluck('duration_minutes')->toArray();
|
|
$avg = array_sum($durations) / count($durations);
|
|
|
|
// Hitung standar deviasi
|
|
$variance = 0;
|
|
foreach ($durations as $dur) {
|
|
$variance += pow($dur - $avg, 2);
|
|
}
|
|
$stdDev = sqrt($variance / count($durations));
|
|
|
|
// Konsistensi = kebalikan dari koefisien variasi
|
|
$cv = $stdDev / $avg;
|
|
$consistency = max(0, min(1, 1 - $cv));
|
|
|
|
return $consistency;
|
|
}
|
|
|
|
/**
|
|
* Hitung trend (meningkat/menurun/stabil)
|
|
*/
|
|
private function calculateTrend($activities)
|
|
{
|
|
if ($activities->count() < 4) return 'stabil';
|
|
|
|
$recent = $activities->take(3)->avg('duration_minutes');
|
|
$older = $activities->slice(3, 3)->avg('duration_minutes');
|
|
|
|
$diff = $recent - $older;
|
|
|
|
if ($diff > 10) return 'meningkat';
|
|
if ($diff < -10) return 'menurun';
|
|
return 'stabil';
|
|
}
|
|
|
|
/**
|
|
* Hitung streak hari beruntun
|
|
*/
|
|
private function calculateStreak($activities)
|
|
{
|
|
if ($activities->isEmpty()) return 0;
|
|
|
|
$streak = 1;
|
|
$prevDate = null;
|
|
|
|
foreach ($activities as $activity) {
|
|
if ($prevDate) {
|
|
$diff = $prevDate->diffInDays($activity->activity_date);
|
|
if ($diff == 1) {
|
|
$streak++;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
$prevDate = $activity->activity_date;
|
|
}
|
|
|
|
return $streak;
|
|
}
|
|
|
|
/**
|
|
* Bobot mood (sesuai skema proposal)
|
|
*/
|
|
private function getMoodWeight($mood)
|
|
{
|
|
return match($mood) {
|
|
'Bagus' => 2.0,
|
|
'Lumayan' => 1.5,
|
|
'Biasa Saja' => 1.0,
|
|
'Cukup Jenuh' => 0.5,
|
|
'Jenuh' => 0.0,
|
|
default => 1.0
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Maksimum adjustment per kategori
|
|
*/
|
|
private function getMaxAdjustment($category)
|
|
{
|
|
return match($category) {
|
|
'Ringan' => 10, // 20-40
|
|
'Sedang' => 20, // 25-65 (lebih lebar)
|
|
'Intensif' => 35, // 45-115 (sangat lebar)
|
|
default => 15
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Pastikan durasi dalam range wajar
|
|
*/
|
|
private function ensureInRange($duration, $category)
|
|
{
|
|
$ranges = [
|
|
'Ringan' => ['min' => 20, 'max' => 40],
|
|
'Sedang' => ['min' => 30, 'max' => 70], // Diperlebar
|
|
'Intensif' => ['min' => 60, 'max' => 120] // 60-120
|
|
];
|
|
|
|
$range = $ranges[$category];
|
|
|
|
return max($range['min'], min($duration, $range['max']));
|
|
}
|
|
|
|
/**
|
|
* Generate notes sederhana untuk user baru
|
|
*/
|
|
private function generateSimpleNotes($category, $duration)
|
|
{
|
|
return match($category) {
|
|
'Ringan' => "📚 Belajar ringan {$duration} menit. Selamat datang di LearnMood!",
|
|
'Sedang' => "🎯 Belajar {$duration} menit dengan fokus. Selamat datang!",
|
|
'Intensif' => "🚀 Sesi intensif {$duration} menit. Selamat datang!",
|
|
default => "Selamat datang di LearnMood!"
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate notes personal berdasarkan faktor-faktor
|
|
*/
|
|
private function generatePersonalNotes($category, $factors, $adjustment, $duration, $streak)
|
|
{
|
|
$notes = [];
|
|
|
|
// Base note
|
|
$notes[] = match($category) {
|
|
'Ringan' => "📚 Belajar ringan {$duration} menit",
|
|
'Sedang' => "🎯 Belajar {$duration} menit dengan fokus",
|
|
'Intensif' => "🚀 Sesi intensif {$duration} menit"
|
|
};
|
|
|
|
// Notes berdasarkan streak
|
|
if ($streak >= 7) {
|
|
$notes[] = "Luar biasa! Streak 7 hari! 🌟";
|
|
} elseif ($streak >= 5) {
|
|
$notes[] = "Hebat! Streak 5 hari! 🔥";
|
|
} elseif ($streak >= 3) {
|
|
$notes[] = "Mantap! Streak 3 hari! ⭐";
|
|
}
|
|
|
|
// Notes berdasarkan faktor
|
|
if (in_array('konsisten', $factors)) {
|
|
$notes[] = "Konsistensimu luar biasa!";
|
|
}
|
|
|
|
if (in_array('trend meningkat', $factors)) {
|
|
$notes[] = "Durasi belajarmu terus meningkat!";
|
|
} elseif (in_array('trend menurun', $factors)) {
|
|
$notes[] = "Semangat! Coba tingkatkan lagi.";
|
|
}
|
|
|
|
if (in_array('mood positif', $factors)) {
|
|
$notes[] = "Mood positif, belajar jadi lebih mudah!";
|
|
} elseif (in_array('mood negatif', $factors)) {
|
|
$notes[] = "Mood sedang kurang, jangan paksakan diri.";
|
|
}
|
|
|
|
if (in_array('tidur ideal', $factors)) {
|
|
$notes[] = "Tidur cukup mendukung konsentrasi.";
|
|
} elseif (in_array('kurang tidur', $factors)) {
|
|
$notes[] = "Coba tidur lebih awal nanti malam.";
|
|
}
|
|
|
|
if (in_array('perlu recovery', $factors)) {
|
|
$notes[] = "Beberapa hari ini intensif, jaga keseimbangan.";
|
|
}
|
|
|
|
if (in_array('kapasitas tinggi', $factors)) {
|
|
$notes[] = "Kamu mampu lebih dari target!";
|
|
} elseif (in_array('target terlalu tinggi', $factors)) {
|
|
$notes[] = "Target mungkin terlalu tinggi, fokus ke kualitas.";
|
|
}
|
|
|
|
// Khusus Intensif
|
|
if ($category == 'Intensif') {
|
|
if ($duration >= 110) {
|
|
$notes[] = "Wow, super intensif! Pastikan istirahat cukup.";
|
|
} elseif ($duration >= 90) {
|
|
$notes[] = "Sesi intensif optimal! Pertahankan.";
|
|
} elseif ($duration <= 70) {
|
|
$notes[] = "Intensif ringan, bagus untuk adaptasi.";
|
|
}
|
|
}
|
|
|
|
// Khusus Sedang
|
|
if ($category == 'Sedang' && $duration >= 60) {
|
|
$notes[] = "Hampir masuk intensif! Tingkatkan sedikit lagi.";
|
|
}
|
|
|
|
// Info adjustment
|
|
if ($adjustment > 5) {
|
|
$notes[] = "(+{$adjustment} menit dari baseline)";
|
|
} elseif ($adjustment < -5) {
|
|
$notes[] = "({$adjustment} menit dari baseline - fokus kualitas)";
|
|
}
|
|
|
|
return implode(' ', $notes);
|
|
}
|
|
} |