request->getJSON(true) ?? []; $nisn = trim((string) ($payload['nisn'] ?? '')); $pin = (string) ($payload['pin'] ?? ''); $qrToken = trim((string) ($payload['qr_token'] ?? '')); if ($nisn === '' || $pin === '' || $qrToken === '') { return $this->errorResponse('NISN, PIN, dan qr_token wajib diisi', null, null, ResponseInterface::HTTP_UNPROCESSABLE_ENTITY); } $studentModel = new StudentModel(); $student = $studentModel->findByNisn($nisn); if (!$student) { return $this->errorResponse('Siswa tidak ditemukan', null, null, ResponseInterface::HTTP_NOT_FOUND); } $accountModel = new StudentMobileAccountModel(); $accountRow = $accountModel->where('student_id', (int) $student->id)->first(); if (!$accountRow || empty($accountRow['pin_hash']) || !password_verify($pin, $accountRow['pin_hash'])) { return $this->errorResponse('PIN salah', null, null, ResponseInterface::HTTP_UNAUTHORIZED); } $studentId = (int) $student->id; $checkinPayload = [ 'student_id' => $studentId, 'qr_token' => $qrToken, 'lat' => $payload['lat'] ?? 0, 'lng' => $payload['lng'] ?? 0, ]; $checkinService = new AttendanceCheckinService(); $result = $checkinService->checkinByQr($checkinPayload); $messages = [ 'PRESENT' => 'Absensi berhasil (Hadir)', 'LATE' => 'Absensi berhasil (Terlambat)', 'INVALID_QR_TOKEN' => 'QR tidak valid atau sudah kadaluarsa', 'STUDENT_NOT_IN_CLASS' => 'Siswa tidak termasuk kelas untuk mapel ini', 'ALREADY_CHECKED_IN' => 'Sudah absen untuk mapel ini hari ini', 'NO_SCHEDULE' => 'Jadwal tidak ditemukan', 'INVALID_DEVICE' => 'Kesalahan sistem', ]; $message = $messages[$result['status']] ?? $result['status']; return $this->successResponse($result, $message); } /** * POST /api/mobile/checkin-masuk-pulang * Body: { nisn, pin, type: 'masuk'|'pulang', lat, lng } * Absen masuk atau pulang pakai jam dari Pengaturan Presensi, auth NISN+PIN. */ public function checkinMasukPulang(): ResponseInterface { $payload = $this->request->getJSON(true) ?? []; $nisn = trim((string) ($payload['nisn'] ?? '')); $pin = (string) ($payload['pin'] ?? ''); $type = strtolower(trim((string) ($payload['type'] ?? ''))); $lat = (float) ($payload['lat'] ?? 0); $lng = (float) ($payload['lng'] ?? 0); if ($nisn === '' || $pin === '') { return $this->errorResponse('NISN dan PIN wajib diisi', null, null, ResponseInterface::HTTP_UNPROCESSABLE_ENTITY); } if ($type !== 'masuk' && $type !== 'pulang') { return $this->errorResponse('type harus masuk atau pulang', null, null, ResponseInterface::HTTP_UNPROCESSABLE_ENTITY); } $studentModel = new StudentModel(); $student = $studentModel->findByNisn($nisn); if (! $student) { return $this->errorResponse('Siswa tidak ditemukan', null, null, ResponseInterface::HTTP_NOT_FOUND); } $accountModel = new StudentMobileAccountModel(); $accountRow = $accountModel->where('student_id', (int) $student->id)->first(); if (! $accountRow || empty($accountRow['pin_hash']) || ! password_verify($pin, $accountRow['pin_hash'])) { return $this->errorResponse('PIN salah', null, null, ResponseInterface::HTTP_UNAUTHORIZED); } $checkinService = new AttendanceCheckinService(); $result = $checkinService->checkinMasukPulang([ 'student_id' => (int) $student->id, 'type' => $type, 'lat' => $lat, 'lng' => $lng, ]); $messages = [ 'PRESENT' => $type === 'masuk' ? 'Absen masuk berhasil.' : 'Absen pulang berhasil.', 'LATE' => 'Absensi tercatat.', 'OUTSIDE_ZONE' => 'Anda di luar jangkauan sekolah.', 'NO_SCHEDULE' => 'Data tidak valid.', 'INVALID_DEVICE' => 'Device aplikasi mobile belum dikonfigurasi. Hubungi admin untuk menambah device dengan kode MOBILE_APP.', 'ALREADY_CHECKED_IN' => $type === 'masuk' ? 'Sudah absen masuk hari ini.' : 'Sudah absen pulang hari ini.', 'ABSENCE_WINDOW_CLOSED' => 'Di luar jam absen. Cek jam masuk/pulang di pengaturan sekolah.', ]; $message = $messages[$result['status']] ?? $result['status']; return $this->successResponse($result, $message); } /** * GET /api/mobile/attendance/today?student_id=123 * Returns today's attendance status for the student (untuk UI tombol Masuk/Pulang). */ public function todayStatus(): ResponseInterface { $studentId = (int) $this->request->getGet('student_id'); if ($studentId < 1) { return $this->errorResponse('student_id wajib', null, null, ResponseInterface::HTTP_BAD_REQUEST); } $db = \Config\Database::connect(); $today = date('Y-m-d'); $rows = $db->table('attendance_sessions') ->select('checkin_at, checkin_type') ->where('student_id', $studentId) ->where('attendance_date', $today) ->orderBy('checkin_at', 'ASC') ->get() ->getResultArray(); $has_masuk = false; $has_pulang = false; $first_at = null; $last_at = null; foreach ($rows as $r) { $t = $r['checkin_at'] ?? ''; $ct = $r['checkin_type'] ?? 'mapel'; if ($t === '') { continue; } if (!$first_at) { $first_at = $t; } $last_at = $t; if ($ct === 'masuk') { $has_masuk = true; } elseif ($ct === 'pulang') { $has_pulang = true; } else { $timeOnly = date('H:i:s', strtotime($t)); if ($timeOnly < '12:00:00') { $has_masuk = true; } if ($timeOnly >= '13:00:00') { $has_pulang = true; } } } $data = [ 'has_masuk' => $has_masuk, 'has_pulang' => $has_pulang, 'first_at' => $first_at, 'last_at' => $last_at, 'date' => $today, ]; return $this->successResponse($data, 'OK'); } }