- CodeIgniter 4 framework setup with SQL Server database config - Models: Control, Test, Dept, Result, Daily/ Monthly entry models - Controllers: Dashboard, Control, Test, Dept, Entry, Report, API endpoints - Views: CRUD pages with modal dialogs, dashboard, reports - Database: Migrations for control test and daily/monthly result tables - Legacy v1 PHP application preserved in /v1 directory - Documentation: AGENTS.md, VIEWS_RULES.md for development guidelines
287 lines
11 KiB
PHP
287 lines
11 KiB
PHP
<?= $this->extend("layout/main_layout"); ?>
|
|
<?= $this->section("content") ?>
|
|
<div class="space-y-6">
|
|
<div class="flex justify-between items-center">
|
|
<h2 class="text-2xl font-bold text-slate-800">QC Report - <?= $dates ?></h2>
|
|
<div class="flex items-center space-x-4">
|
|
<button onclick="window.print()" class="btn btn-secondary">
|
|
<i class="fa-solid fa-print mr-2"></i>
|
|
Print Report
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if (count($reportData) > 1): ?>
|
|
<div class="bg-white rounded-xl border border-slate-100 shadow-sm p-6">
|
|
<h3 class="text-lg font-semibold text-slate-800 mb-4">QC Trend Overview</h3>
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<div class="h-80">
|
|
<canvas id="trend-chart"></canvas>
|
|
</div>
|
|
<div class="h-80">
|
|
<canvas id="comparison-chart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php foreach ($reportData as $index => $data): ?>
|
|
<div class="bg-white rounded-xl border border-slate-100 shadow-sm overflow-hidden page-break" id="report-<?= $index ?>">
|
|
<div class="px-6 py-4 bg-slate-50 border-b border-slate-100 flex items-center justify-between">
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-slate-800"><?= $data['control']['name'] ?> (Lot: <?= $data['control']['lot'] ?? 'N/A' ?>)</h3>
|
|
<p class="text-sm text-slate-500"><?= $data['test']['name'] ?? 'N/A' ?></p>
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<span class="px-3 py-1 text-sm rounded-full <?= $data['outOfRange'] > 0 ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800' ?>">
|
|
<?= $data['outOfRange'] ?> Out of Range
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($data['controlTest']): ?>
|
|
<?php
|
|
$mean = $data['controlTest']['mean'];
|
|
$sd = $data['controlTest']['sd'];
|
|
$results = $data['results'];
|
|
$daysInMonth = date('t', strtotime($dates . '-01'));
|
|
$chartLabels = [];
|
|
$chartValues = [];
|
|
$upperLimit = $mean + 2 * $sd;
|
|
$lowerLimit = $mean - 2 * $sd;
|
|
|
|
for ($day = 1; $day <= $daysInMonth; $day++) {
|
|
$chartLabels[] = $day;
|
|
$value = null;
|
|
foreach ($results as $result) {
|
|
if (date('j', strtotime($result['resdate'])) == $day) {
|
|
$value = $result['resvalue'];
|
|
break;
|
|
}
|
|
}
|
|
$chartValues[] = $value !== null ? $value : 'null';
|
|
}
|
|
?>
|
|
<div class="p-6">
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|
<div class="text-center p-4 bg-slate-50 rounded-xl">
|
|
<p class="text-sm text-slate-500">Mean</p>
|
|
<p class="text-xl font-bold text-slate-800"><?= number_format($mean, 3) ?></p>
|
|
</div>
|
|
<div class="text-center p-4 bg-slate-50 rounded-xl">
|
|
<p class="text-sm text-slate-500">SD</p>
|
|
<p class="text-xl font-bold text-slate-800"><?= number_format($sd, 3) ?></p>
|
|
</div>
|
|
<div class="text-center p-4 bg-green-50 rounded-xl">
|
|
<p class="text-sm text-green-600">+2SD</p>
|
|
<p class="text-xl font-bold text-green-700"><?= number_format($upperLimit, 3) ?></p>
|
|
</div>
|
|
<div class="text-center p-4 bg-red-50 rounded-xl">
|
|
<p class="text-sm text-red-600">-2SD</p>
|
|
<p class="text-xl font-bold text-red-700"><?= number_format($lowerLimit, 3) ?></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-6">
|
|
<h4 class="text-sm font-medium text-slate-700 mb-2">Trend Chart</h4>
|
|
<div class="h-64">
|
|
<canvas id="chart-<?= $index ?>"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-sm text-left">
|
|
<thead class="bg-slate-50 text-slate-500 text-xs uppercase tracking-wider">
|
|
<tr>
|
|
<th class="py-3 px-4 font-semibold">Day</th>
|
|
<th class="py-3 px-4 font-semibold">Value</th>
|
|
<th class="py-3 px-4 font-semibold">Z-Score</th>
|
|
<th class="py-3 px-4 font-semibold">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100">
|
|
<?php
|
|
for ($day = 1; $day <= $daysInMonth; $day++):
|
|
$value = null;
|
|
foreach ($results as $result) {
|
|
if (date('j', strtotime($result['resdate'])) == $day) {
|
|
$value = $result['resvalue'];
|
|
break;
|
|
}
|
|
}
|
|
|
|
$zScore = ($value && $sd > 0) ? ($value - $mean) / $sd : null;
|
|
$status = $zScore !== null ? (abs($zScore) > 2 ? 'Out' : (abs($zScore) > 1 ? 'Warn' : 'OK')) : '-';
|
|
$statusClass = $status == 'Out' ? 'text-red-600 font-bold bg-red-50' : ($status == 'Warn' ? 'text-yellow-600 bg-yellow-50' : 'text-green-600');
|
|
?>
|
|
<tr class="hover:bg-slate-50/50">
|
|
<td class="py-3 px-4 text-slate-800"><?= $day ?></td>
|
|
<td class="py-3 px-4 font-medium text-slate-800"><?= $value ?? '-' ?></td>
|
|
<td class="py-3 px-4 text-slate-600"><?= $zScore !== null ? number_format($zScore, 2) : '-' ?></td>
|
|
<td class="py-3 px-4 <?= $statusClass ?>"><?= $status ?></td>
|
|
</tr>
|
|
<?php endfor; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<?php if ($data['comment']): ?>
|
|
<div class="mt-4 p-4 bg-amber-50 rounded-xl">
|
|
<p class="text-sm font-medium text-amber-800">Monthly Comment:</p>
|
|
<p class="text-slate-700"><?= $data['comment']['comtext'] ?></p>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="p-6 text-center text-slate-500">
|
|
No test data found for this control.
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?= $this->endSection(); ?>
|
|
|
|
<?= $this->section("script") ?>
|
|
<script>
|
|
const reportData = <?= json_encode(array_map(function($data) use ($dates) {
|
|
$mean = $data['controlTest']['mean'] ?? 0;
|
|
$sd = $data['controlTest']['sd'] ?? 1;
|
|
$daysInMonth = date('t', strtotime($dates . '-01'));
|
|
$values = [];
|
|
for ($day = 1; $day <= $daysInMonth; $day++) {
|
|
$value = null;
|
|
foreach ($data['results'] as $result) {
|
|
if (date('j', strtotime($result['resdate'])) == $day) {
|
|
$value = $result['resvalue'];
|
|
break;
|
|
}
|
|
}
|
|
$values[] = $value;
|
|
}
|
|
return [
|
|
'name' => $data['control']['name'],
|
|
'lot' => $data['control']['lot'],
|
|
'mean' => $mean,
|
|
'sd' => $sd,
|
|
'values' => $values
|
|
];
|
|
}, $reportData)) ?>;
|
|
|
|
const dates = '<?= $dates ?>';
|
|
const daysInMonth = Array.from({length: 31}, (_, i) => i + 1);
|
|
|
|
function initReportCharts() {
|
|
if (typeof ChartManager === 'undefined') {
|
|
setTimeout(initReportCharts, 100);
|
|
return;
|
|
}
|
|
|
|
if (reportData.length > 1) {
|
|
const trendDatasets = reportData.map((data, i) => ({
|
|
label: data.name,
|
|
data: data.values,
|
|
color: ChartManager.getColor(i)
|
|
}));
|
|
ChartManager.createTrendChart('trend-chart', daysInMonth, trendDatasets);
|
|
|
|
const comparisonDatasets = reportData.map((data, i) => ({
|
|
label: data.name,
|
|
data: data.values,
|
|
color: ChartManager.getColor(i)
|
|
}));
|
|
ChartManager.createComparisonChart('comparison-chart', daysInMonth, comparisonDatasets);
|
|
}
|
|
|
|
reportData.forEach((data, index) => {
|
|
const ctx = document.getElementById(`chart-${index}`);
|
|
if (ctx) {
|
|
new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: daysInMonth,
|
|
datasets: [{
|
|
label: data.name,
|
|
data: data.values,
|
|
borderColor: ChartManager.getColor(index),
|
|
backgroundColor: ChartManager.getColor(index, 0.1),
|
|
tension: 0.3,
|
|
fill: true,
|
|
pointRadius: 4,
|
|
pointBackgroundColor: data.values.map(v => {
|
|
if (v === null) return 'transparent';
|
|
if (data.sd === 0) return ChartManager.getColor(index);
|
|
const z = (v - data.mean) / data.sd;
|
|
return Math.abs(z) > 2 ? '#ef4444' : (Math.abs(z) > 1 ? '#f59e0b' : ChartManager.getColor(index));
|
|
})
|
|
}, {
|
|
label: '+2SD',
|
|
data: Array(31).fill(data.mean + 2 * data.sd),
|
|
borderColor: '#22c55e',
|
|
borderDash: [5, 5],
|
|
pointRadius: 0,
|
|
fill: false
|
|
}, {
|
|
label: '-2SD',
|
|
data: Array(31).fill(data.mean - 2 * data.sd),
|
|
borderColor: '#ef4444',
|
|
borderDash: [5, 5],
|
|
pointRadius: 0,
|
|
fill: false
|
|
}, {
|
|
label: 'Mean',
|
|
data: Array(31).fill(data.mean),
|
|
borderColor: '#3b82f6',
|
|
borderDash: [2, 2],
|
|
pointRadius: 0,
|
|
fill: false
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'top'
|
|
},
|
|
title: {
|
|
display: true,
|
|
text: `${data.name} (Mean: ${data.mean.toFixed(3)}, SD: ${data.sd.toFixed(3)})`
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
title: {
|
|
display: true,
|
|
text: 'Value'
|
|
}
|
|
},
|
|
x: {
|
|
title: {
|
|
display: true,
|
|
text: 'Day'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', initReportCharts);
|
|
</script>
|
|
|
|
<style>
|
|
@media print {
|
|
.page-break {
|
|
page-break-inside: avoid;
|
|
}
|
|
.bg-white {
|
|
box-shadow: none !important;
|
|
}
|
|
}
|
|
</style>
|
|
<?= $this->endSection(); ?>
|
|
|