init backend presensi
This commit is contained in:
185
app/Modules/Academic/Controllers/DapodikSyncController.php
Normal file
185
app/Modules/Academic/Controllers/DapodikSyncController.php
Normal file
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Academic\Controllers;
|
||||
|
||||
use App\Core\BaseApiController;
|
||||
use App\Modules\Academic\Models\DapodikSyncJobModel;
|
||||
use App\Modules\Academic\Models\DapodikRombelMappingModel;
|
||||
use App\Modules\Academic\Services\DapodikSyncService;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Dapodik sync and rombel mapping API (ADMIN only).
|
||||
*/
|
||||
class DapodikSyncController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* POST /api/academic/dapodik/sync/students
|
||||
* Starts sync job, returns job_id immediately, processes in background (same request after response sent).
|
||||
* If a students-sync job is already running, returns existing job_id (job locking).
|
||||
* Body/Query: limit, max_pages (optional).
|
||||
*/
|
||||
public function syncStudents(): ResponseInterface
|
||||
{
|
||||
$body = $this->request->getJSON(true) ?? [];
|
||||
$limit = (int) ($body['limit'] ?? $this->request->getGet('limit') ?? 100);
|
||||
$maxPages = (int) ($body['max_pages'] ?? $this->request->getGet('max_pages') ?? 50);
|
||||
|
||||
$limit = max(1, min(500, $limit));
|
||||
$maxPages = max(1, min(100, $maxPages));
|
||||
|
||||
$jobModel = new DapodikSyncJobModel();
|
||||
|
||||
// Job locking: if there is already a running students-sync job, reuse it.
|
||||
$existing = $jobModel
|
||||
->where('type', DapodikSyncJobModel::TYPE_STUDENTS)
|
||||
->where('status', DapodikSyncJobModel::STATUS_RUNNING)
|
||||
->orderBy('started_at', 'DESC')
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
return $this->successResponse([
|
||||
'job_id' => (int) $existing['id'],
|
||||
'total_rows' => (int) ($existing['total_rows'] ?? 0),
|
||||
'status' => $existing['status'],
|
||||
], 'Sync already running');
|
||||
}
|
||||
|
||||
$service = new DapodikSyncService();
|
||||
$estimate = $service->estimateTotalStudents($limit);
|
||||
$totalRows = $estimate['total'];
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$jobId = $jobModel->insert([
|
||||
'type' => DapodikSyncJobModel::TYPE_STUDENTS,
|
||||
'total_rows' => $totalRows,
|
||||
'processed_rows'=> 0,
|
||||
'status' => DapodikSyncJobModel::STATUS_RUNNING,
|
||||
'message' => null,
|
||||
'started_at' => $now,
|
||||
'finished_at' => null,
|
||||
]);
|
||||
|
||||
$payload = [
|
||||
'job_id' => (int) $jobId,
|
||||
'total_rows' => $totalRows,
|
||||
'status' => 'running',
|
||||
];
|
||||
|
||||
$this->successResponse($payload, 'Sync started')->send();
|
||||
|
||||
if (function_exists('fastcgi_finish_request')) {
|
||||
fastcgi_finish_request();
|
||||
}
|
||||
ignore_user_abort(true);
|
||||
set_time_limit(0);
|
||||
|
||||
$service->syncStudentsWithJob((int) $jobId, $limit, $maxPages);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/academic/dapodik/sync/status/{job_id}
|
||||
* Returns job progress for polling.
|
||||
*/
|
||||
public function status(int $jobId): ResponseInterface
|
||||
{
|
||||
$jobModel = new DapodikSyncJobModel();
|
||||
$job = $jobModel->find($jobId);
|
||||
if (! $job) {
|
||||
return $this->errorResponse('Job not found', null, null, 404);
|
||||
}
|
||||
|
||||
$total = (int) ($job['total_rows'] ?? 0);
|
||||
$processed = (int) ($job['processed_rows'] ?? 0);
|
||||
$percent = $total > 0 ? min(100, (int) round(($processed / $total) * 100)) : ($processed > 0 ? 1 : 0);
|
||||
|
||||
return $this->successResponse([
|
||||
'total_rows' => $total,
|
||||
'processed_rows' => $processed,
|
||||
'percent' => $percent,
|
||||
'status' => $job['status'],
|
||||
'message' => $job['message'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/academic/dapodik/rombels
|
||||
* Query: unmapped_only=1 to filter.
|
||||
*/
|
||||
public function rombels(): ResponseInterface
|
||||
{
|
||||
$unmappedOnly = $this->request->getGet('unmapped_only') === '1' || $this->request->getGet('unmapped_only') === 'true';
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$builder = $db->table('dapodik_rombel_mappings AS m')
|
||||
->select('m.id, m.dapodik_rombel, m.class_id, m.last_seen_at, m.updated_at, c.grade, c.major, c.name AS class_name')
|
||||
->join('classes AS c', 'c.id = m.class_id', 'left')
|
||||
->orderBy('m.dapodik_rombel', 'ASC');
|
||||
|
||||
if ($unmappedOnly) {
|
||||
$builder->where('m.class_id', null);
|
||||
}
|
||||
|
||||
$rows = $builder->get()->getResultArray();
|
||||
$data = array_map(static function ($r) {
|
||||
$classLabel = null;
|
||||
if ($r['class_id'] !== null && isset($r['grade'])) {
|
||||
$classLabel = trim(($r['grade'] ?? '') . ' ' . ($r['major'] ?? '') . ' ' . ($r['class_name'] ?? ''));
|
||||
}
|
||||
return [
|
||||
'id' => (int) $r['id'],
|
||||
'dapodik_rombel' => (string) $r['dapodik_rombel'],
|
||||
'class_id' => $r['class_id'] !== null ? (int) $r['class_id'] : null,
|
||||
'class_label' => $classLabel,
|
||||
'last_seen_at' => $r['last_seen_at'],
|
||||
'updated_at' => $r['updated_at'],
|
||||
];
|
||||
}, $rows);
|
||||
|
||||
return $this->successResponse($data, 'Rombel mappings');
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/academic/dapodik/rombels/{id}
|
||||
* Body: class_id (nullable)
|
||||
*/
|
||||
public function updateRombel(int $id): ResponseInterface
|
||||
{
|
||||
$payload = $this->request->getJSON(true) ?? $this->request->getPost();
|
||||
$classId = null;
|
||||
if (isset($payload['class_id']) && $payload['class_id'] !== '' && $payload['class_id'] !== null) {
|
||||
$classId = (int) $payload['class_id'];
|
||||
$db = \Config\Database::connect();
|
||||
$exists = $db->table('classes')->where('id', $classId)->countAllResults();
|
||||
if ($exists < 1) {
|
||||
return $this->errorResponse('class_id must exist in classes', null, null, 422);
|
||||
}
|
||||
}
|
||||
|
||||
$model = new DapodikRombelMappingModel();
|
||||
$row = $model->find($id);
|
||||
if (! $row) {
|
||||
return $this->errorResponse('Mapping not found', null, null, 404);
|
||||
}
|
||||
|
||||
$model->update($id, ['class_id' => $classId]);
|
||||
|
||||
$updated = $model->find($id);
|
||||
$classLabel = null;
|
||||
if ($updated['class_id']) {
|
||||
$db = \Config\Database::connect();
|
||||
$c = $db->table('classes')->select('grade, major, name')->where('id', $updated['class_id'])->get()->getRowArray();
|
||||
if ($c) {
|
||||
$classLabel = trim(($c['grade'] ?? '') . ' ' . ($c['major'] ?? '') . ' ' . ($c['name'] ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->successResponse([
|
||||
'id' => (int) $updated['id'],
|
||||
'dapodik_rombel' => (string) $updated['dapodik_rombel'],
|
||||
'class_id' => $updated['class_id'] !== null ? (int) $updated['class_id'] : null,
|
||||
'class_label' => $classLabel,
|
||||
], 'Mapping updated');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user