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:
parent
dcb09804f5
commit
9e374103fa
10
TODO.md
10
TODO.md
@ -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)
|
||||
@ -36,3 +40,5 @@ Completed:
|
||||
- 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)
|
||||
- Test and fix Reprint label
|
||||
- fasten the load of val page
|
||||
@ -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 & 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 & 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>
|
||||
|
||||
@ -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(); ?>
|
||||
|
||||
|
||||
@ -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(); ?>
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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">
|
||||
|
||||
41
app/Views/shared/dialog_preview.php
Normal file
41
app/Views/shared/dialog_preview.php
Normal 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>
|
||||
@ -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`;
|
||||
|
||||
@ -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}`);
|
||||
}
|
||||
|
||||
@ -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(); ?>
|
||||
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>403 Forbidden</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p>Directory access is forbidden.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user