feat: add PDF batch generation page for superuser

- Add /superuser/pdf-batch page with textarea input for access numbers
- Create API endpoint /api/superuser/pdf-batch to process batch PDF generation
- Use report/{id}/pdf endpoint for authenticated PDF generation
- Show results table with success/failed status, language, and type (new/regen)
- Remove deprecated /batch/pdf endpoint
- Add PDF Batch menu item to superuser navigation
This commit is contained in:
mahdahar 2026-03-11 09:34:11 +07:00
parent d8dc4e95f2
commit 0fad3baab7
4 changed files with 299 additions and 3 deletions

View File

@ -71,6 +71,11 @@ $routes->group('superuser', ['filter' => 'role:0'], function ($routes) {
$routes->get('', 'Pages\SuperuserController::index');
$routes->get('users', 'Pages\SuperuserController::users');
$routes->get('validate', 'Pages\SuperuserController::validatePage');
$routes->get('pdf-batch', 'Pages\SuperuserController::pdfBatch');
});
$routes->group('api', ['filter' => 'role:0'], function ($routes) {
$routes->post('superuser/pdf-batch', 'Pages\SuperuserController::processPdfBatch');
});
$routes->group('admin', ['filter' => 'role:1'], function ($routes) {
@ -108,9 +113,6 @@ $routes->group('report', ['filter' => 'role:0,1,2,4'], function ($routes) {
$routes->get('report/status/(:any)', 'ReportController::checkPdfStatus/$1');
// Batch PDF endpoint - no auth required (internal use only)
$routes->get('batch/pdf/(:num)', 'ReportController::generatePdfNoAuth/$1');
// External PDF generator endpoint - no auth required
$routes->post('api/requests/(:any)/pdf', 'RequestsController::setPdfFlag/$1');

View File

@ -30,4 +30,127 @@ class SuperuserController extends BaseController
return view('superuser/validate', ['roleConfig' => $config['superuser']]);
}
public function pdfBatch()
{
$config = require APPPATH . 'Views/shared/config.php';
return view('superuser/pdf_batch', ['roleConfig' => $config['superuser']]);
}
public function processPdfBatch()
{
$input = $this->request->getJSON(true);
$accessNumbers = $input['accessNumbers'] ?? [];
if (empty($accessNumbers)) {
return $this->response->setStatusCode(400)->setJSON([
'success' => false,
'message' => 'No access numbers provided'
]);
}
$results = [];
$db = \Config\Database::connect();
foreach ($accessNumbers as $accessnumber) {
$accessnumber = trim($accessnumber);
if (empty($accessnumber)) {
continue;
}
try {
// Check if request exists
$sql = "SELECT ACCESSNUMBER, REPORT_LANG FROM GDC_CMOD.dbo.CM_REQUESTS WHERE ACCESSNUMBER = ?";
$row = $db->query($sql, [$accessnumber])->getRowArray();
if (!$row) {
$results[] = [
'accessnumber' => $accessnumber,
'success' => false,
'error' => 'Access number not found',
'lang' => null,
'isRegen' => false
];
continue;
}
// Call report/{accessnumber}/pdf endpoint internally
$response = $this->callReportPdfEndpoint($accessnumber);
if ($response['success']) {
$results[] = [
'accessnumber' => $accessnumber,
'success' => true,
'error' => null,
'lang' => $response['lang'] ?? 'Unknown',
'isRegen' => $response['isRegen'] ?? false
];
} else {
$results[] = [
'accessnumber' => $accessnumber,
'success' => false,
'error' => $response['error'] ?? 'PDF generation failed',
'lang' => null,
'isRegen' => false
];
}
} catch (\Throwable $e) {
$results[] = [
'accessnumber' => $accessnumber,
'success' => false,
'error' => $e->getMessage(),
'lang' => null,
'isRegen' => false
];
}
}
return $this->response->setJSON([
'success' => true,
'results' => $results,
'total' => count($results),
'successful' => count(array_filter($results, fn($r) => $r['success'])),
'failed' => count(array_filter($results, fn($r) => !$r['success']))
]);
}
private function callReportPdfEndpoint($accessnumber)
{
$db = \Config\Database::connect();
// Get language preference
$sql = "SELECT REPORT_LANG FROM GDC_CMOD.dbo.CM_REQUESTS WHERE ACCESSNUMBER = ?";
$row = $db->query($sql, [$accessnumber])->getRowArray();
$eng = (int) ($row['REPORT_LANG'] ?? 0);
// Load ReportController and call generatePdf
$reportController = new \App\Controllers\ReportController();
try {
// Temporarily override the response to capture it
$response = $reportController->generatePdf($accessnumber);
// Parse the response
$body = $response->getBody();
$data = json_decode($body, true);
if ($data && isset($data['success']) && $data['success']) {
return [
'success' => true,
'lang' => $data['lang'] ?? ($eng == 1 ? 'English' : 'Indonesian'),
'isRegen' => $data['isRegen'] ?? false
];
}
return [
'success' => false,
'error' => $data['error'] ?? 'Unknown error'
];
} catch (\Throwable $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
}

View File

@ -82,6 +82,7 @@ return [
['label' => 'Dashboard', 'href' => 'superuser', 'icon' => 'chart-bar'],
['label' => 'Validate', 'href' => 'superuser/validate', 'icon' => 'check-circle'],
['label' => 'Users', 'href' => 'superuser/users', 'icon' => 'users'],
['label' => 'PDF Batch', 'href' => 'superuser/pdf-batch', 'icon' => 'file-pdf'],
['label' => 'Reports', 'href' => 'http://glenlis/report2/', 'icon' => 'book'],
],
],

View File

@ -0,0 +1,170 @@
<?php $config = require APPPATH . 'Views/shared/config.php'; ?>
<?= $this->extend('shared/layout'); ?>
<?= $this->section('content') ?>
<div x-data="pdfBatch" class="contents">
<main class="p-4 flex-1 flex flex-col gap-4 max-w-4xl w-full mx-auto overflow-hidden min-h-0">
<div class="card bg-base-100 shadow-xl border border-base-200 h-full">
<div class="card-body p-4 overflow-y-auto">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold flex items-center gap-2 text-base-content">
<i class="fa fa-file-pdf text-primary"></i> PDF Batch Generation
</h2>
</div>
<div class="form-control w-full mb-4">
<label class="label">
<span class="label-text font-medium">Access Numbers</span>
<span class="label-text-alt">One per line or comma-separated</span>
</label>
<textarea
x-model="accessNumbersInput"
class="textarea textarea-bordered w-full font-mono text-sm h-48"
placeholder="24000001&#10;24000002&#10;24000003"
:disabled="isProcessing"
></textarea>
</div>
<div class="flex justify-between items-center mb-4">
<div class="text-sm text-base-content/60">
<span x-show="!isProcessing" x-text="`${getAccessNumberCount()} access numbers`"></span>
<span x-show="isProcessing" x-text="`Processing ${currentIndex} of ${totalCount}...`"></span>
</div>
<button
class="btn btn-primary"
@click="processBatch()"
:disabled="isProcessing || getAccessNumberCount() === 0"
>
<span x-show="isProcessing" class="loading loading-spinner loading-sm"></span>
<i x-show="!isProcessing" class="fa fa-play mr-2"></i>
<span x-text="isProcessing ? 'Processing...' : 'Generate PDFs'"></span>
</button>
</div>
<div x-show="results.length > 0" class="mt-4">
<div class="flex justify-between items-center mb-2">
<h3 class="text-lg font-semibold">Results</h3>
<div class="flex gap-4 text-sm">
<span class="text-success">
<i class="fa fa-check-circle mr-1"></i>
<span x-text="successfulCount"></span> Success
</span>
<span class="text-error">
<i class="fa fa-times-circle mr-1"></i>
<span x-text="failedCount"></span> Failed
</span>
</div>
</div>
<div class="overflow-x-auto border rounded-lg">
<table class="table table-zebra w-full">
<thead>
<tr>
<th class="w-16">Status</th>
<th>Access Number</th>
<th>Language</th>
<th>Type</th>
<th>Error</th>
</tr>
</thead>
<tbody>
<template x-for="(result, index) in results" :key="index">
<tr :class="result.success ? 'bg-success/5' : 'bg-error/5'">
<td>
<i
:class="result.success ? 'fa fa-check-circle text-success' : 'fa fa-times-circle text-error'"
class="text-lg"
></i>
</td>
<td class="font-mono font-bold" x-text="result.accessnumber"></td>
<td x-text="result.lang || '-'" :class="result.success ? 'text-success' : ''"></td>
<td>
<span
x-show="result.success"
class="badge badge-sm"
:class="result.isRegen ? 'badge-warning' : 'badge-success'"
x-text="result.isRegen ? 'Regenerated' : 'New'"
></span>
<span x-show="!result.success" class="text-error">-</span>
</td>
<td class="text-error text-sm" x-text="result.error || '-'" :class="result.error ? '' : 'text-base-content/30'"></td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
</div>
</main>
</div>
<?= $this->endSection(); ?>
<?= $this->section('script') ?>
<script type="module">
import Alpine from '<?= base_url("js/app.js"); ?>';
document.addEventListener('alpine:init', () => {
Alpine.data("pdfBatch", () => ({
accessNumbersInput: '',
isProcessing: false,
results: [],
currentIndex: 0,
totalCount: 0,
successfulCount: 0,
failedCount: 0,
getAccessNumberCount() {
const numbers = this.parseAccessNumbers();
return numbers.length;
},
parseAccessNumbers() {
return this.accessNumbersInput
.split(/[\n,]+/)
.map(n => n.trim())
.filter(n => n.length > 0);
},
async processBatch() {
const accessNumbers = this.parseAccessNumbers();
if (accessNumbers.length === 0) {
alert('Please enter at least one access number');
return;
}
this.isProcessing = true;
this.results = [];
this.totalCount = accessNumbers.length;
this.currentIndex = 0;
this.successfulCount = 0;
this.failedCount = 0;
try {
const res = await fetch(`${BASEURL}/api/superuser/pdf-batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accessNumbers })
});
const data = await res.json();
if (data.success) {
this.results = data.results;
this.successfulCount = data.successful;
this.failedCount = data.failed;
} else {
alert(data.message || 'Failed to process batch');
}
} catch (err) {
alert('Error processing batch: ' + err.message);
} finally {
this.isProcessing = false;
}
}
}));
});
Alpine.start();
</script>
<?= $this->endSection(); ?>