feat: auto-generate PDF after second validation - Trigger PDF generation via /report/{accessnumber}/pdf endpoint after VAL2 - Add preview dialog for admin/lab/superuser roles - Update role configs with previewEnabled flag

This commit is contained in:
mahdahar 2026-02-12 13:02:31 +07:00
parent dcb09804f5
commit 9e374103fa
11 changed files with 170 additions and 17 deletions

12
TODO.md
View File

@ -1,9 +1,13 @@
# Project Checklist: Glen RME & Lab Management System
**Last Updated:** 20260205
**Last Updated:** 20260212
Pending:
- Test and fix Reprint label
- preview result for validate for su adm lab
- auto generate pdf after 2 val
- add datetime val1 val2
- sample other for MCU is annoying
- report2 go to cmod
Completed:
- Update User Role levels (Standardize roles: Superuser, Admin, Lab, Phlebo, CS)
@ -35,4 +39,6 @@ Completed:
- Reprint Label (Add functionality to reprint labels)
- Create Eng Result UI UX on request dashboard
- Test and fix PDF Generation
- Print Result Audit (Track when result reports are printed/exported, log user and timestamp)
- Print Result Audit (Track when result reports are printed/exported, log user and timestamp)
- Test and fix Reprint label
- fasten the load of val page

View File

@ -234,8 +234,17 @@
<tr><td>TC-071</td><td>Concurrent Validation ⚡</td><td>2 user buka validation dialog bersamaan</td><td>Validasi berhasil, tidak conflict ✅</td><td>FITUR CROSS-ROLE 🤝</td><td><input type="checkbox"><br> <input type="checkbox"></td><td><input type="text" value="___________"></td></tr>
<tr><td>TC-072</td><td>Concurrent Sample Collection ⚡</td><td>2 user collect tube berbeda bersamaan</td><td>Semua berhasil tercatat ✅</td><td>FITUR CROSS-ROLE 🧪</td><td><input type="checkbox"><br> <input type="checkbox"></td><td><input type="text" value="___________"></td></tr>
<tr><td>TC-073</td><td>Session Timeout ⏱️</td><td>Tunggu session timeout</td><td>Redirect ke login 🔄</td><td>FITUR CROSS-ROLE 🔐</td><td><input type="checkbox"><br> <input type="checkbox"></td><td><input type="text" value="___________"></td></tr>
<tr><td>TC-074</td><td>Print Labels 🏷️</td><td>Print individual/collection/all</td><td>Semua labels tercetak 🖨️</td><td>ADM, LAB, PHLEB</td><td><input type="checkbox"><br> <input type="checkbox"></td><td><input type="text" value="___________"></td></tr>
<tr><td>TC-075</td><td>Edit Comment ✏️</td><td>Edit comment di dashboard</td><td>Comment berubah tersimpan 💾</td><td>SU</td><td><input type="checkbox"><br> <input type="checkbox"></td><td><input type="text" value="___________"></td></tr>
<tr><td>TC-074</td><td>Password Hashing Security 🔒</td><td>Buat user → cek database</td><td>Password dalam HASH bukan plain 🛡️</td><td>FITUR CROSS-ROLE 🔐</td><td><input type="checkbox"><br> <input type="checkbox"></td><td><input type="text" value="___________"></td></tr>
<tr><td>TC-075</td><td>Legacy Read-Only 👀</td><td>Cek koneksi &amp; fungsi Firebird</td><td>Hanya READ dari Firebird, TIDAK WRITE 🚫✍️</td><td>FITUR CROSS-ROLE 🗄️</td><td><input type="checkbox"><br> <input type="checkbox"></td><td><input type="text" value="___________"></td></tr>
<tr><td>TC-076</td><td>Print Labels 🏷️</td><td>Print individual/collection/all</td><td>Semua labels tercetak 🖨️</td><td>ADM, LAB, PHLEB</td><td><input type="checkbox"><br> <input type="checkbox"></td><td><input type="text" value="___________"></td></tr>
<tr><td>TC-077</td><td>Edit Comment ✏️</td><td>Edit comment di dashboard</td><td>Comment berubah tersimpan 💾</td><td>SU</td><td><input type="checkbox"><br> <input type="checkbox"></td><td><input type="text" value="___________"></td></tr>
</table>
<h2>📋 Next Plan 📋</h2>
<table class="data-table">
<tr><th>ID</th><th>Judul Test Case</th><th>Langkah Utama</th><th>Expected Result</th><th>Role</th><th>Hasil</th><th>Issue/Jawaban</th></tr>
<tr><td>NP-001</td><td>Collect Sample 🧪</td><td>Buka dialog sample → Collect</td><td>STATUS=1, COLLECTIONDATE &amp; USERID diset ✅</td><td>SU, ADM, LAB</td><td><input type="checkbox"><br> <input type="checkbox"></td><td><input type="text" value="___________"></td></tr>
<tr><td>NP-002</td><td>Un-Collect Sample ↩️</td><td>Buka dialog sample → Un-Collect</td><td>STATUS di-reset, audit log tercatat 📝</td><td>SU, ADM, LAB</td><td><input type="checkbox"><br> <input type="checkbox"></td><td><input type="text" value="___________"></td></tr>
<tr><td>NP-004</td><td>Sample Collection Buttons Enabled ✅</td><td>Buka dialog sample</td><td>Tombol Collect/Un-Coll/Un-Recv enabled 🔘</td><td>ADMIN</td><td><input type="checkbox"><br> <input type="checkbox"></td><td><input type="text" value="___________"></td></tr>
</table>
</div>
</body>

View File

@ -11,6 +11,7 @@ $roleConfig = $config['admin'];
<?= $this->include('shared/dialog_unval'); ?>
<?= $this->include('shared/dialog_audit'); ?>
<?= $this->include('shared/dialog_results_generate'); ?>
<?= $this->include('shared/dialog_preview'); ?>
</main>
<?= $this->endSection(); ?>

View File

@ -11,6 +11,7 @@ $roleConfig = $config['lab'];
<?= $this->include('shared/dialog_unval'); ?>
<?= $this->include('shared/dialog_audit'); ?>
<?= $this->include('shared/dialog_results_generate'); ?>
<?= $this->include('shared/dialog_preview'); ?>
</main>
<?= $this->endSection(); ?>

View File

@ -19,6 +19,7 @@
return [
'admin' => [
'title' => 'Admin Dashboard',
'previewEnabled' => true,
'sampleDialog' => [
'commentEditable' => true,
'showCollectButtons' => true,
@ -31,6 +32,7 @@ return [
],
'cs' => [
'title' => 'CS Dashboard',
'previewEnabled' => false,
'sampleDialog' => [
'commentEditable' => false,
'showCollectButtons' => false,
@ -42,6 +44,7 @@ return [
],
'lab' => [
'title' => 'Lab Analyst Dashboard',
'previewEnabled' => true,
'sampleDialog' => [
'commentEditable' => true,
'showCollectButtons' => true,
@ -54,6 +57,7 @@ return [
],
'phlebo' => [
'title' => 'Phlebotomist Dashboard',
'previewEnabled' => false,
'sampleDialog' => [
'commentEditable' => false,
'showCollectButtons' => true,
@ -66,6 +70,7 @@ return [
],
'superuser' => [
'title' => 'Superuser Dashboard',
'previewEnabled' => true,
'sampleDialog' => [
'commentEditable' => true,
'showCollectButtons' => true,

View File

@ -206,7 +206,32 @@
<p>2: <span x-text="req.VAL2USER"></span></p>
</div>
</td>
<?php
$configFile = include __DIR__ . '/config.php';
$roleMap = ['superuser' => 'superuser', 'admin' => 'admin', 'lab analyst' => 'lab', 'phlebotomist' => 'phlebo', 'customer service' => 'cs'];
$userRole = strtolower(session('userrole') ?? '');
$configKey = $roleMap[$userRole] ?? '';
$previewEnabled = $configFile[$configKey]['previewEnabled'] ?? false;
?>
<td>
<?php if ($previewEnabled): ?>
<template x-if="!req.VAL1USER || !req.VAL2USER">
<button @click="openPreviewDialog(req)"
class="btn btn-xs w-full btn-warning">
<i class="fa fa-clock mr-1"></i>
<span class="text-xs">Preview</span>
</button>
</template>
<template x-if="req.VAL1USER && req.VAL2USER">
<div class="dropdown dropdown-end dropdown-hover">
<div tabindex="0" role="button"
class="btn btn-xs w-full btn-success text-white">
<i class="fa fa-clipboard-check mr-1"></i>
<span class="text-xs">Ready</span>
</div>
<ul tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-40 p-2 shadow-lg border border-base-300">
<?php else: ?>
<div class="dropdown dropdown-end dropdown-hover">
<div tabindex="0" role="button"
class="btn btn-xs w-full"
@ -216,6 +241,7 @@
</div>
<ul tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-40 p-2 shadow-lg border border-base-300">
<?php endif; ?>
<template x-if="req.VAL1USER && req.VAL2USER">
<div>
<li>
@ -236,6 +262,7 @@
</li>
</div>
</template>
<?php if (!$previewEnabled): ?>
<template x-if="!req.VAL1USER || !req.VAL2USER">
<div>
<li class="disabled opacity-50 cursor-not-allowed">
@ -245,8 +272,13 @@
</li>
</div>
</template>
<?php endif; ?>
</ul>
<?php if ($previewEnabled): ?>
</template>
<?php else: ?>
</div>
<?php endif; ?>
</td>
<td>
<div class="dropdown dropdown-end dropdown-hover">

View File

@ -0,0 +1,41 @@
<dialog class="modal" :open="isDialogPreviewOpen" @keydown.escape="closePreviewDialog()">
<div class="modal-box w-2/3 max-w-5xl" x-trap.noreturn="isDialogPreviewOpen">
<!-- Request info header -->
<div class="bg-base-200 p-3 rounded mb-3">
<div class="grid grid-cols-4 gap-2 text-sm">
<div>Access#: <span x-text="previewAccessnumber" class="font-mono font-bold"></span></div>
<div>Patient: <span x-text="previewItem?.PATNAME || previewItem?.Name"></span></div>
<div>MRN: <span x-text="previewItem?.PATNUMBER?.substring(14) || previewItem?.PATNUMBER"></span></div>
<div>Tests: <span x-text="(previewItem?.TESTS || previewItem?.TESTNAMES || '').substring(0,40) + '...'"></span></div>
</div>
</div>
<div class="flex justify-between items-center mb-2">
<h3 class="font-bold text-lg">Preview Result</h3>
<button class="btn btn-sm btn-ghost" @click="closePreviewDialog()" aria-label="Close">
<i class="fa fa-times"></i>
</button>
</div>
<p class="mb-2 flex gap-2">
<button id="preview-validate-btn" class="btn btn-sm btn-success"
@click="validateFromPreview(previewAccessnumber, '<?=session('userid');?>')"
:disabled="!isPreviewIframeLoaded || isPreviewValidating">
<span x-text="isPreviewValidating ? 'Validating...' : 'Validate'"></span>
<span x-show="isPreviewValidating" class="loading loading-spinner loading-xs ml-1"></span>
</button>
<button class="btn btn-sm btn-ghost" @click="closePreviewDialog()">
Close (Esc)
</button>
</p>
<iframe id="preview-iframe" x-ref="previewIframe" :src="getPreviewUrl()" @load="onPreviewIframeLoad()" width="100%" height="500px"
class="border border-base-300 rounded"></iframe>
<!-- Loading overlay -->
<template x-if="isPreviewValidating">
<div class="absolute inset-0 bg-base-100/80 flex items-center justify-center z-10 rounded-box">
<span class="loading loading-spinner loading-lg text-success"></span>
</div>
</template>
</div>
</dialog>

View File

@ -414,6 +414,60 @@ document.addEventListener('alpine:init', () => {
.catch(() => this.showToast('Print failed', 'error'));
},
/*
preview dialog methods
*/
isDialogPreviewOpen: false,
previewAccessnumber: null,
previewItem: null,
isPreviewIframeLoaded: false,
isPreviewValidating: false,
openPreviewDialog(item) {
this.previewItem = item;
this.previewAccessnumber = item.SP_ACCESSNUMBER;
this.isPreviewIframeLoaded = false;
this.isPreviewValidating = false;
this.isDialogPreviewOpen = true;
},
closePreviewDialog() {
this.isDialogPreviewOpen = false;
this.previewItem = null;
this.previewAccessnumber = null;
this.isPreviewIframeLoaded = false;
},
getPreviewUrl() {
return `${BASEURL}/report/${this.previewAccessnumber}`;
},
onPreviewIframeLoad() {
this.isPreviewIframeLoaded = true;
},
validateFromPreview(accessnumber, userid) {
if (!this.isPreviewIframeLoaded || this.isPreviewValidating) return;
this.isPreviewValidating = true;
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
.then(res => res.json())
.then(data => {
this.isPreviewValidating = false;
if (data.val) {
this.showToast(`Validated (val${data.val}): ${accessnumber}`, 'success');
this.fetchList();
this.closePreviewDialog();
} else if (data.message && data.message.includes('already validate')) {
this.showToast('You have already validated this request', 'error');
} else {
this.showToast(data.message || 'Validation failed', 'error');
}
})
.catch(() => {
this.isPreviewValidating = false;
this.showToast('Validation failed', 'error');
});
},
showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = `alert alert-${type} fixed top-4 right-4 z-50`;

View File

@ -212,6 +212,16 @@ document.addEventListener('alpine:init', () => {
}).then(response => response.json()).then(data => {
if (data.val === 2) {
this.showToast(`Validated (val2): ${accessnumber} - PDF queued`);
// Trigger PDF auto-generation after val2
fetch(`${BASEURL}/report/${accessnumber}/pdf`).then(res => res.json()).then(pdfData => {
if (pdfData.success) {
console.log('PDF generation queued:', pdfData.jobId);
} else {
console.error('PDF generation failed:', pdfData.error);
}
}).catch(err => {
console.error('PDF generation request failed:', err);
});
} else {
this.showToast(`Validated: ${accessnumber}`);
}

View File

@ -1,4 +1,8 @@
<?= $this->extend('shared/layout'); ?>
<?php
$config = include __DIR__ . '/../shared/config.php';
$roleConfig = $config['superuser'];
?>
<?= $this->extend('shared/layout', ['roleConfig' => $roleConfig]); ?>
<?= $this->section('content'); ?>
<main class="p-4 flex-1 flex flex-col gap-2 overflow-hidden min-h-0" x-data="dashboard">
@ -7,6 +11,7 @@
<?= $this->include('shared/dialog_unval'); ?>
<?= $this->include('shared/dialog_audit'); ?>
<?= $this->include('shared/dialog_results_generate'); ?>
<?= $this->include('shared/dialog_preview'); ?>
</main>
<?= $this->endSection(); ?>

View File

@ -1,11 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>403 Forbidden</title>
</head>
<body>
<p>Directory access is forbidden.</p>
</body>
</html>