tinyqc/app/Views/report/view.php
mahdahar ff90e0eb29 Initial commit: Add CodeIgniter 4 QC application with full MVC structure
- 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
2026-01-14 16:49:27 +07:00

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(); ?>