186 lines
6.8 KiB
PHP
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');
|
|
}
|
|
}
|