Files
presensi/app/Modules/Academic/Controllers/DapodikSyncController.php
2026-03-05 14:37:36 +07:00

186 lines
6.8 KiB
PHP

<?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');
}
}