gdc_cmod/app/Views/v2/index.php
2025-12-05 07:11:43 +07:00

313 lines
11 KiB
PHP

<!DOCTYPE html>
<html lang="en" data-theme="corporate">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CMOD</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css" rel="stylesheet" type="text/css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/js/all.min.js"></script>
<style>
body {
margin: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
font-size: 0.71rem;
}
.navbar {
padding: 0.2rem 1rem;
min-height: 0rem;
}
.card-body {
font-size: 0.71rem !important;
}
</style>
</head>
<body class="bg-base-200 min-h-screen flex flex-col">
<nav class="navbar bg-secondary shadow-sm text-white">
<div class='flex-1 font-bold'>
<a class=''>CMOD</a>
</div>
<div class="mr-2">
<a>Hi, lisfse</a>
</div>
<div class="dropdown dropdown-end p-0">
<div tabindex="0" role="button" class="btn btn-sm btn-secondary">Menu</div>
<ul tabindex="-1" class="dropdown-content menu bg-base-100 rounded-box z-1 w-46 p-2 shadow-sm text-black">
<li><a>Item 1</a></li>
<li><a>Item 2</a></li>
</ul>
</div>
</nav>
<main class="p-4 flex-1 flex flex-col gap-2" x-data="dashboard">
<div class="card bg-base-100">
<div class="card-body p-3 max-h-full overflow-y-auto">
<div class="flex gap-1">
<div class="flex-1 font-bold text-lg">Dashboard</div>
<div class="flex gap-1">
<button @click="filterKey = 'Pend'" :class="filterKey === 'Pend' ? 'btn-active' : ''" class="btn btn-outline btn-sm"><span x-text="counters.Pend"></span> Pending</button>
<button @click="filterKey = 'Coll'" :class="filterKey === 'Coll' ? 'btn-active' : ''" class="btn btn-outline btn-sm btn-secondary"><span x-text="counters.Coll"></span> Collected</button>
<button @click="filterKey = 'Recv'" :class="filterKey === 'Recv' ? 'btn-active' : ''" class="btn btn-outline btn-sm btn-primary"><span x-text="counters.Recv"></span> Received</button>
<button @click="filterKey = 'Inc'" :class="filterKey === 'Inc' ? 'btn-active' : ''" class="btn btn-outline btn-sm btn-warning"><span x-text="counters.Inc"></span> Incomplete</button>
<button @click="filterKey = 'Fin'" :class="filterKey === 'Fin' ? 'btn-active' : ''" class="btn btn-outline btn-sm btn-success"><span x-text="counters.Fin"></span> Final</button>
<button @click="filterKey = 'Total'" :class="filterKey === 'Total' ? 'btn-active' : ''" class="btn btn-outline btn-sm"><span x-text="counters.Total"></span> Total</button>
<button @click="filterKey = 'Validated'" :class="filterKey === 'Validated' ? 'btn-active' : ''" class="btn btn-outline btn-sm btn-info"><span x-text="validatedCount"></span> Val</button>
</div>
</div>
<div class="flex gap-3 mb-2">
<div class="flex-1 flex gap-2 items-center">
<div>Date</div>
<input type="date" class="input input-sm w-39" x-model="filter.date1"/>-
<input type="date" class="input input-sm w-39" x-model="filter.date2"/>
<button class="btn btn-sm btn-primary" @click='fetchList()'><i class='fa fa-search'></i>Search</button>
<button class="btn btn-sm btn-secondary" @click='reset()'><i class='fa fa-refresh'></i>Reset</button>
</div>
<div class="flex gap-2 items-center">
<div>Filter</div>
<input type="text" class="input input-sm w-39" x-model="filterTable" />
</div>
</div>
<template x-if="list.length">
<table class="table table-xs table-zebra w-full">
<thead class="bg-base-100 sticky top-0 z-10">
<tr>
<th style='width:7%;'>Order Datetime</th>
<th style='width:15%;'>Patient Name</th>
<th style='width:7%;'>No Lab</th>
<th style='width:7%;'>No Register</th>
<th style='width:8%;'>Reff</th>
<th style='width:8%;'>Doctor</th>
<th style='width:15%;'>Tests</th>
<th style='width:5%;'>Result To</th>
<th style='width:5%;'>Validation</th>
<th style='width:4%;'>Status</th>
</tr>
</thead>
<tbody>
<template x-for="req in filtered" :key="req.SP_ACCESSNUMBER">
<tr class="hover:bg-base-300">
<td x-text="req.REQDATE"></td>
<td x-text="req.Name"></td>
<td x-text="req.SP_ACCESSNUMBER"></td>
<td x-text="req.HOSTORDERNUMBER"></td>
<td x-text="req.REFF"></td>
<td x-text="req.DOC"></td>
<td x-text="req.TESTS"></td>
<td x-text="req.ODR_CRESULT_TO"></td>
<td>
<div>1: <span x-text="req.val1user"></span></div>
<div>2: <span x-text="req.val2user"></span></div>
<template x-if="req.ISVAL == 1">
<div>
<button class="btn btn-xs btn-outline btn-secondary" @click="unval(req.SP_ACCESSNUMBER)"><i class="fa-solid fa-rotate-right"></i></button>
<button class="btn btn-xs btn-outline btn-success" @click="openValDialog(req.SP_ACCESSNUMBER)"><i class="fa-solid fa-check"></i></button>
</div>
</template>
</td>
<td><button x-text="req.STATS === 'Fin' ? 'Final' : req.STATS" class="btn btn-xs"
:class="statusColor[req.STATS]" @click="openSampleDialog(req.SP_ACCESSNUMBER)"></button></td>
</tr>
</template>
</tbody>
</table>
</template>
</div>
</div>
<?php echo $this->include('v2/dialog_sample'); ?>
<?php echo $this->include('v2/dialog_val'); ?>
</main>
<footer class='bg-base-100 p-1'>&copy; 2025 - 5Panda</footer>
<script>
window.BASEURL = "<?=base_url();?>";
</script>
<script type="module">
import Alpine from '<?=base_url("js/app.js");?>';
document.addEventListener('alpine:init', () => {
Alpine.data("dashboard", ()=> ({
// dashboard
today: "",
filter: { date1: "", date2: "" },
list: [],
counters: { Pend: 0, Coll: 0, Recv: 0, Inc: 0, Fin: 0, Total: 0 },
statusColor: {
Pend: 'bg-white text-black font-bold',
PartColl: 'bg-orange-300 text-white font-bold',
Coll: 'bg-orange-500 text-white font-bold',
PartRecv: 'bg-blue-200 text-black font-bold',
Recv: 'bg-blue-500 text-white font-bold',
Inc: 'bg-yellow-500 text-white font-bold',
Fin: 'bg-green-500 text-white font-bold',
},
filterTable :"",
filterKey: 'Total',
statusMap: {
Total: [],
Pend: ['Pend'],
Coll: ['Coll', 'PartColl'],
Recv: ['Recv'],
Inc: ['Inc'],
Fin: ['Fin'],
},
init() {
this.today = new Date().toISOString().slice(0, 10);
this.filter.date1 = "2025-03-03";
this.filter.date2 = "2025-03-03";
//this.filter.date1 = this.today;
//this.filter.date2 = this.today;
this.fetchList();
},
fetchList(){
this.list = [];
let statusOrder = { Pend: 1, PartColl: 2, Coll: 3, PartRecv: 4, Recv: 5, Inc: 6, Fin: 7 };
let param = new URLSearchParams(this.filter).toString();
// reset counters before processing
for (let k in this.counters) { this.counters[k] = 0; }
fetch(`${BASEURL}/api/request?${param}`, {
method: 'GET',
headers: {'Content-Type': 'application/json'},
}).then(res => res.json()).then(data => {
this.list = data.data ?? [];
this.filterKey = 'Total';
// count + sort in a single loop
this.list.forEach(item => {
if (this.counters[item.STATS] !== undefined) { this.counters[item.STATS]++; this.counters.Total++; }
else {
if(item.STATS == 'PartColl') { this.counters.Coll++; }
else if(item.STATS == 'PartRecv') { this.counters.Recv++; }
this.counters.Total++;
}
});
this.list.sort((a, b) => {
let codeA = statusOrder[a.STATS] ?? 0;
let codeB = statusOrder[b.STATS] ?? 0;
return codeA - codeB;
});
});
},
reset() {
this.filter.date1 = this.today;
this.filter.date2 = this.today;
this.fetchList();
},
get filtered() {
let data = this.list;
if (this.filterKey === 'Validated') {
data = data.filter(i => i.ISVAL == 1);
} else {
const valid = this.statusMap[this.filterKey]
if (valid.length > 0) {
data = data.filter(i => valid.includes(i.STATS));
}
}
if (this.filterTable) {
const s = this.filterTable.toLowerCase();
data = data.filter(i =>
Object.values(i).some(v =>
String(v).toLowerCase().includes(s)
)
);
}
return data;
},
get validatedCount() {
return this.list.filter(r => r.ISVAL == 1).length;
},
/*
sample dialog
*/
item : '',
isDialogSampleOpen : false,
openSampleDialog (accessnumber) {
this.isDialogSampleOpen = true;
this.fetchItem(accessnumber)
},
closeSampleDialog () {
this.isDialogSampleOpen = false;
},
fetchItem(accessnumber){
this.item = [];
fetch(`${BASEURL}/api/sample/${accessnumber}`, { method: 'GET', headers: {'Content-Type': 'application/json'}})
.then(res => res.json()).then(data => {
this.item = data.data ?? {};
if (!Array.isArray(this.item.samples)) this.item.samples = [];
});
},
/*
validate dialog
*/
isDialogValOpen : false,
isValidateEnabled: false,
valAccessnumber : null,
openValDialog (accessnumber) {
this.isDialogValOpen = true;
this.valAccessnumber = accessnumber;
this.$nextTick(() => {
// refs will be available after render
const iframe = this.$root.querySelector('#result-iframe') || (this.$refs && this.$refs.resultIframe);
const validateBtn = this.$root.querySelector('#validate-btn') || (this.$refs && this.$refs.validateBtn);
if (!iframe || !validateBtn) return;
const setup = () => {
try {
const doc = iframe.contentDocument || iframe.contentWindow.document;
const scrollable = doc.documentElement || doc.body;
const checkScroll = () => {
try {
const atBottom = (scrollable.scrollHeight - scrollable.scrollTop - scrollable.clientHeight) < 2;
this.isValidateEnabled = atBottom;
validateBtn.disabled = !atBottom;
} catch (e) { /* cross-origin or not ready */ }
};
iframe.contentWindow.removeEventListener('scroll', checkScroll);
iframe.contentWindow.addEventListener('scroll', checkScroll);
checkScroll();
} catch (e) { /* ignore cross-origin */ }
};
// If iframe already loaded, setup immediately; otherwise wait for load
if (iframe.contentWindow && (iframe.contentDocument && iframe.contentDocument.readyState === 'complete')) {
setup();
} else {
iframe.addEventListener('load', setup);
}
});
},
closeValDialog () {
this.isDialogValOpen = false;
},
unval(accessnumber) {
console.log("Unvalidate access number:", accessnumber);
},
validate(accessnumber, userid) {
fetch(`${BASE_URL}/api/request/validate/${accessnumber}`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({ userid:`${userid}` })
}).then(response => {
this.closeValDialog()
console.log('Validate clicked for', this.valAccessnumber);
});
}
}));
});
Alpine.start();
</script>
</body>
</html>