# Heat User - Study Session Tracking System

## 📋 Tổng quan

Hệ thống tracking thời gian học của học sinh với các tính năng:
- ⏱️ Tracking thời gian học chính xác theo từng session
- 💓 Heartbeat mechanism để detect active/inactive sessions
- 🔄 Continue session khi đóng browser
- 🛑 Stop session để kết thúc hẳn
- 📊 Thống kê thời gian học theo ngày
- ⚙️ Auto-end inactive sessions bằng cron job

---

## 🗄️ Database Schema

### **Bảng 1: student_study_sessions**
Lưu thông tin các session học của học sinh

| Column | Type | Description |
|--------|------|-------------|
| id | bigint | Primary key |
| student_id | bigint | ID học sinh |
| id_history_contest | bigint | ID history contest từ EMS |
| id_history | bigint | ID history của từng phần |
| id_bai_kiem_tra | bigint | ID bài kiểm tra |
| session_start_at | datetime | Thời gian bắt đầu |
| session_end_at | datetime | Thời gian kết thúc |
| last_heartbeat_at | datetime | Heartbeat cuối cùng |
| status | enum | active/auto_ended/paused/completed/stopped |
| duration_seconds | int | Thời gian thực tế (giây) |
| adjusted_duration_seconds | int | Thời gian sau rule 15 phút (giây) |
| ip_address | varchar(45) | IP address |
| user_agent | text | User agent |

### **Bảng 2: student_daily_study_time**
Tổng hợp thời gian học theo ngày

| Column | Type | Description |
|--------|------|-------------|
| id | bigint | Primary key |
| student_id | bigint | ID học sinh |
| study_date | date | Ngày học |
| total_minutes | decimal(10,2) | Tổng phút học |
| session_count | int | Số lượng sessions |

---

## 🚀 Cài đặt

### **1. Chạy Migration**

```bash
# Chạy migrations
php artisan migrate

# Hoặc chạy file cụ thể
php artisan migrate --path=/database/migrations/2025_12_04_100000_create_student_study_sessions_table.php
php artisan migrate --path=/database/migrations/2025_12_04_100001_create_student_daily_study_time_table.php
```

### **2. Thiết lập Cron Job**

```bash
# Kiểm tra scheduler đã hoạt động chưa
php artisan schedule:list

# Test chạy command thủ công
php artisan sessions:auto-end

# Thiết lập cron trên server (nếu chưa có)
# Thêm vào crontab
crontab -e

# Thêm dòng sau:
* * * * * cd /var/www/html/lms_hocmai && php artisan schedule:run >> /dev/null 2>&1
```

### **3. Kiểm tra Cache Driver**

Hệ thống sử dụng Cache Lock để tránh race condition. Đảm bảo cache driver đã được config:

```bash
# .env
CACHE_DRIVER=redis  # hoặc memcached
```

---

## 📡 API Endpoints

### **Base URL:** `/api/heat-user/study-session`

### **1. Start Session**

**POST** `/api/heat-user/study-session/start`

Bắt đầu session học mới.

**Request:**
```json
{
  "id_history_contest": 100,
  "id_bai_kiem_tra": 456,
  "id_history": 200
}
```

**Response - Success (New):**
```json
{
  "success": true,
  "type": "new",
  "session_id": 1,
  "message": "Bắt đầu session mới"
}
```

**Response - Need Continue:**
```json
{
  "success": false,
  "type": "need_continue",
  "session_id": 1,
  "status": "active",
  "message": "Có bài làm chưa hoàn thành. Vui lòng tiếp tục."
}
```

---

### **2. Continue Session**

**POST** `/api/heat-user/study-session/continue`

Tiếp tục session đã tạm dừng.

**Request:**
```json
{
  "session_id": 1
}
```

**Response:**
```json
{
  "success": true,
  "session_id": 2,
  "old_session_id": 1,
  "old_session_duration_minutes": 15.0,
  "message": "Tiếp tục làm bài"
}
```

---

### **3. Heartbeat**

**POST** `/api/heat-user/study-session/heartbeat`

Gửi heartbeat để báo session còn active. **Gọi mỗi 5 phút.**

**Request:**
```json
{
  "session_id": 1
}
```

**Response:**
```json
{
  "success": true,
  "message": "Heartbeat updated"
}
```

---

### **4. Submit Session**

**POST** `/api/heat-user/study-session/submit`

Nộp bài và kết thúc session.

**Request:**
```json
{
  "session_id": 1
}
```

**Response:**
```json
{
  "success": true,
  "duration_minutes": 25.5,
  "message": "Đã nộp bài thành công"
}
```

---

### **5. Stop Session**

**POST** `/api/heat-user/study-session/stop`

Dừng hẳn session (không thể continue, phải start mới).

**Request:**
```json
{
  "session_id": 1
}
```

**Response:**
```json
{
  "success": true,
  "session_id": 1,
  "duration_minutes": 18.0,
  "message": "Đã dừng session thành công. Lần sau cần start mới."
}
```

---

### **6. Get Statistics**

**GET** `/api/heat-user/study-session/statistics`

Lấy thống kê thời gian học theo ngày.

**Query Parameters:**
```
from_date=2025-01-01&to_date=2025-12-31&student_id=123
```

**Response:**
```json
{
  "success": true,
  "data": {
    "student_id": 123,
    "from_date": "2025-01-01",
    "to_date": "2025-12-31",
    "total_days": 180,
    "total_minutes": 5400,
    "total_hours": 90,
    "total_sessions": 250,
    "daily_breakdown": [
      {
        "date": "2025-01-01",
        "total_minutes": 45,
        "total_hours": 0.75,
        "formatted_time": "00:45",
        "session_count": 2
      }
    ]
  }
}
```

---

### **7. Get Active Session**

**GET** `/api/heat-user/study-session/active`

Kiểm tra xem có session active không.

**Query Parameters:**
```
id_history=200
```

**Response:**
```json
{
  "success": true,
  "data": {
    "session_id": 1,
    "status": "active",
    "started_at": "2025-12-04 10:00:00",
    "last_heartbeat_at": "2025-12-04 10:25:00"
  }
}
```

---

## 💻 Frontend Integration

### **Example: Vue.js/React Implementation**

```javascript
class StudySessionManager {
  constructor() {
    this.sessionId = null;
    this.idHistory = null;
    this.heartbeatInterval = null;
  }

  // 1. Start session
  async start(idHistoryContest, idBaiKiemTra, idHistory) {
    try {
      const response = await fetch('/api/heat-user/study-session/start', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          id_history_contest: idHistoryContest,
          id_bai_kiem_tra: idBaiKiemTra,
          id_history: idHistory
        })
      });

      const data = await response.json();

      if (data.type === 'need_continue') {
        // Show continue modal
        this.showContinueModal(data.session_id);
      } else if (data.success) {
        // Start success
        this.sessionId = data.session_id;
        this.idHistory = idHistory;
        
        // Save to localStorage
        localStorage.setItem('study_session', JSON.stringify({
          sessionId: data.session_id,
          idHistory: idHistory,
          startedAt: new Date().toISOString()
        }));

        // Start heartbeat
        this.startHeartbeat();
      }

      return data;
    } catch (error) {
      console.error('Start session failed:', error);
    }
  }

  // 2. Continue session
  async continue(sessionId) {
    const response = await fetch('/api/heat-user/study-session/continue', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ session_id: sessionId })
    });

    const data = await response.json();

    if (data.success) {
      this.sessionId = data.session_id;
      
      // Update localStorage
      const stored = JSON.parse(localStorage.getItem('study_session'));
      stored.sessionId = data.session_id;
      localStorage.setItem('study_session', JSON.stringify(stored));

      // Start heartbeat
      this.startHeartbeat();
    }

    return data;
  }

  // 3. Heartbeat - call every 5 minutes
  startHeartbeat() {
    // Clear existing interval
    if (this.heartbeatInterval) {
      clearInterval(this.heartbeatInterval);
    }

    // Start new interval - every 5 minutes
    this.heartbeatInterval = setInterval(async () => {
      if (!this.sessionId) return;

      try {
        await fetch('/api/heat-user/study-session/heartbeat', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ session_id: this.sessionId })
        });
      } catch (error) {
        console.error('Heartbeat failed:', error);
      }
    }, 5 * 60 * 1000); // 5 minutes
  }

  // 4. Submit
  async submit() {
    if (!this.sessionId) return;

    const response = await fetch('/api/heat-user/study-session/submit', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ session_id: this.sessionId })
    });

    const data = await response.json();

    if (data.success) {
      // Clean up
      this.cleanup();
    }

    return data;
  }

  // 5. Stop
  async stop() {
    if (!this.sessionId) return;

    const confirmed = confirm('Bạn có chắc muốn dừng? Bạn sẽ phải bắt đầu lại từ đầu.');
    if (!confirmed) return;

    const response = await fetch('/api/heat-user/study-session/stop', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ session_id: this.sessionId })
    });

    const data = await response.json();

    if (data.success) {
      // Clean up
      this.cleanup();
      // Redirect
      window.location.href = '/exams';
    }

    return data;
  }

  // 6. Check on page load
  checkExistingSession() {
    const stored = localStorage.getItem('study_session');
    if (!stored) return null;

    const session = JSON.parse(stored);
    
    // Show continue button in UI
    return session;
  }

  // 7. Cleanup
  cleanup() {
    clearInterval(this.heartbeatInterval);
    this.heartbeatInterval = null;
    this.sessionId = null;
    this.idHistory = null;
    localStorage.removeItem('study_session');
  }

  // 8. Auto-submit on idle (optional)
  setupIdleDetection(idleMinutes = 10) {
    let idleTimer;
    const resetTimer = () => {
      clearTimeout(idleTimer);
      idleTimer = setTimeout(() => {
        // Auto submit after idle
        this.submit();
        alert('Đã tự động nộp bài do không có hoạt động.');
      }, idleMinutes * 60 * 1000);
    };

    // Reset timer on user activity
    ['mousedown', 'keypress', 'scroll', 'touchstart'].forEach(event => {
      document.addEventListener(event, resetTimer, true);
    });

    resetTimer();
  }
}

// Usage
const sessionManager = new StudySessionManager();

// On page load
const existingSession = sessionManager.checkExistingSession();
if (existingSession) {
  // Show continue button
  document.getElementById('continueBtn').style.display = 'block';
}

// Start button click
document.getElementById('startBtn').addEventListener('click', async () => {
  await sessionManager.start(100, 456, 200);
});

// Continue button click
document.getElementById('continueBtn').addEventListener('click', async () => {
  await sessionManager.continue(existingSession.sessionId);
});

// Submit button click
document.getElementById('submitBtn').addEventListener('click', async () => {
  await sessionManager.submit();
});

// Stop button click
document.getElementById('stopBtn').addEventListener('click', async () => {
  await sessionManager.stop();
});
```

---

## ⚙️ Cron Job & Maintenance

### **Auto-end Command**

```bash
# Chạy thủ công để test
php artisan sessions:auto-end

# Xem log
tail -f storage/logs/laravel.log | grep "Auto ended"

# Check metrics
php artisan tinker
>>> Cache::get('auto_end_metrics')
```

### **Monitoring**

```php
// Check active sessions count
$activeCount = \App\Models\HeatUser\StudentStudySession::where('status', 'active')->count();

// Check sessions cần auto-end
$needEndCount = \App\Models\HeatUser\StudentStudySession::inactive(10)->count();

// Check metrics
$metrics = Cache::get('auto_end_metrics');
```

---

## 🧪 Testing

### **Test các API endpoints**

```bash
# 1. Start session
curl -X POST http://localhost/api/heat-user/study-session/start \
  -H "Content-Type: application/json" \
  -d '{
    "id_history_contest": 100,
    "id_bai_kiem_tra": 456,
    "id_history": 200
  }'

# 2. Heartbeat
curl -X POST http://localhost/api/heat-user/study-session/heartbeat \
  -H "Content-Type: application/json" \
  -d '{"session_id": 1}'

# 3. Submit
curl -X POST http://localhost/api/heat-user/study-session/submit \
  -H "Content-Type: application/json" \
  -d '{"session_id": 1}'

# 4. Get statistics
curl "http://localhost/api/heat-user/study-session/statistics?from_date=2025-01-01&to_date=2025-12-31&student_id=123"
```

### **Test cron job**

```bash
# Run manually
php artisan sessions:auto-end

# Should see output:
# Active sessions before: 5
# ✓ Auto ended 3 sessions in 125.50ms
```

---

## 📝 Business Rules

### **1. Minimum 15 Minutes Rule**
- Mỗi session có thời gian tối thiểu 15 phút
- Nếu thực tế < 15 phút → tính là 15 phút
- Nếu thực tế ≥ 15 phút → tính thời gian thực

### **2. Heartbeat Timeout**
- FE gửi heartbeat mỗi 5 phút
- Timeout = 10 phút (2 lần heartbeat miss)
- Cron job chạy mỗi 5 phút để auto-end

### **3. Session States**
- **active**: Đang làm bài
- **auto_ended**: Tự động end do timeout
- **paused**: Tạm dừng (từ continue)
- **completed**: Hoàn thành (submit)
- **stopped**: Dừng hẳn (không thể continue)

### **4. Continue vs Stop**
- **Close browser**: Có thể continue (same id_history)
- **Click Stop**: Không thể continue (need new id_history)

---

## 🔧 Troubleshooting

### **Issue 1: Cron job không chạy**

```bash
# Check scheduler
php artisan schedule:list

# Check crontab
crontab -l

# Test manually
php artisan sessions:auto-end
```

### **Issue 2: Session không auto-end**

```bash
# Check active sessions
php artisan tinker
>>> \App\Models\HeatUser\StudentStudySession::inactive(10)->count()

# Check last heartbeat
>>> \App\Models\HeatUser\StudentStudySession::find(1)->last_heartbeat_at
```

### **Issue 3: Cache lock failed**

```bash
# Clear cache
php artisan cache:clear

# Check Redis
redis-cli
> keys auto_end_sessions
> del auto_end_sessions
```

---

## 📊 Performance Tips

1. **Indexes**: Đã tạo sẵn indexes phù hợp
2. **Chunk Processing**: Xử lý 500 records mỗi lần
3. **Cache Lock**: Tránh race condition
4. **Background Job**: Cân nhắc dùng Queue cho > 10K users

---

## 📞 Support

Nếu có vấn đề, check logs:
```bash
tail -f storage/logs/laravel.log
```

---

**Version:** 1.0.0  
**Last Updated:** 2025-12-04

