188 lines
7.4 KiB
PHP
188 lines
7.4 KiB
PHP
<?php
|
|
|
|
namespace App\Modules\Mobile\Controllers;
|
|
|
|
use App\Core\BaseApiController;
|
|
use App\Modules\Academic\Models\StudentModel;
|
|
use App\Modules\Attendance\Services\AttendanceCheckinService;
|
|
use App\Modules\Mobile\Models\StudentMobileAccountModel;
|
|
use CodeIgniter\HTTP\ResponseInterface;
|
|
|
|
/**
|
|
* Check-in via QR (guru generate QR di dashboard, siswa scan di app).
|
|
* POST /api/mobile/checkin-qr
|
|
* Body: { nisn, pin, qr_token [, lat, lng ] }
|
|
*
|
|
* Absen masuk/pulang: POST /api/mobile/checkin-masuk-pulang
|
|
* Body: { nisn, pin, type: 'masuk'|'pulang', lat, lng }
|
|
*/
|
|
class CheckinController extends BaseApiController
|
|
{
|
|
/**
|
|
* Absen mapel via scan QR yang ditampilkan guru.
|
|
*/
|
|
public function checkinQr(): ResponseInterface
|
|
{
|
|
$payload = $this->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');
|
|
}
|
|
}
|