Files
controls-web/reports/index.php
2026-02-17 12:44:37 -06:00

513 lines
14 KiB
PHP

<?php // phpcs:ignoreFile
/**
* Factory Reports - Main Page
*
* Generate reports from historical data by factory area
*/
require __DIR__ . '/../session.php';
require __DIR__ . '/config.php';
$pageTitle = 'Factory Reports';
$pageSubtitle = 'Historical data analysis by area';
$assetBasePath = '../';
require __DIR__ . '/../includes/layout/header.php';
?>
<style>
.report-grid {
display: grid;
grid-template-columns: 300px 1fr;
gap: 1.5rem;
min-height: 70vh;
}
@media (max-width: 900px) {
.report-grid {
grid-template-columns: 1fr;
}
}
.report-sidebar {
background: var(--bg-secondary);
border-radius: 8px;
padding: 1.5rem;
height: fit-content;
}
.report-content {
background: var(--bg-secondary);
border-radius: 8px;
padding: 1.5rem;
}
.report-section {
margin-bottom: 1.5rem;
}
.report-section h3 {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 0 0 0.75rem 0;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border-color);
}
.report-section label {
display: block;
font-size: 0.9rem;
color: var(--text-primary);
margin-bottom: 0.25rem;
}
.report-section select,
.report-section input[type="date"],
.report-section input[type="datetime-local"] {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 0.9rem;
margin-bottom: 0.75rem;
}
.area-card {
background: var(--bg-tertiary);
border-radius: 6px;
padding: 1rem;
margin-bottom: 0.75rem;
cursor: pointer;
transition: all 0.2s;
border: 2px solid transparent;
}
.area-card:hover {
background: var(--bg-primary);
}
.area-card.selected {
border-color: var(--accent-primary);
background: var(--bg-primary);
}
.area-card h4 {
margin: 0 0 0.25rem 0;
font-size: 1rem;
color: var(--text-primary);
}
.area-card p {
margin: 0;
font-size: 0.8rem;
color: var(--text-secondary);
}
.tag-list {
max-height: 200px;
overflow-y: auto;
background: var(--bg-primary);
border-radius: 4px;
padding: 0.5rem;
}
.tag-item {
display: flex;
align-items: center;
padding: 0.35rem 0.5rem;
border-radius: 4px;
cursor: pointer;
}
.tag-item:hover {
background: var(--bg-tertiary);
}
.tag-item input {
margin-right: 0.5rem;
}
.tag-item label {
margin: 0;
cursor: pointer;
font-size: 0.85rem;
}
.btn-generate {
width: 100%;
padding: 0.75rem 1rem;
background: var(--accent-primary);
color: #fff;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.btn-generate:hover {
background: var(--accent-hover);
}
.btn-generate:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.results-table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
.results-table th,
.results-table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.results-table th {
background: var(--bg-tertiary);
font-weight: 600;
font-size: 0.85rem;
color: var(--text-secondary);
}
.results-table tr:hover td {
background: var(--bg-tertiary);
}
.stat-value {
font-weight: 600;
color: var(--accent-primary);
}
.loading {
text-align: center;
padding: 2rem;
color: var(--text-secondary);
}
.no-data {
text-align: center;
padding: 3rem;
color: var(--text-tertiary);
}
.export-buttons {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
.btn-export {
padding: 0.5rem 1rem;
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
}
.btn-export:hover {
background: var(--bg-primary);
}
</style>
<div class="report-grid">
<aside class="report-sidebar">
<div class="report-section">
<h3>Factory Area</h3>
<?php foreach ($factoryAreas as $areaKey => $area): ?>
<div class="area-card" data-area="<?php echo htmlspecialchars($areaKey); ?>">
<h4><?php echo htmlspecialchars($area['name']); ?></h4>
<p><?php echo htmlspecialchars($area['description']); ?></p>
</div>
<?php endforeach; ?>
</div>
<div class="report-section" id="section-picker" style="display: none;">
<h3>Section</h3>
<select id="section-select">
<option value="">Select a section...</option>
</select>
</div>
<div class="report-section" id="tag-picker" style="display: none;">
<h3>Tags</h3>
<div class="tag-list" id="tag-list"></div>
</div>
<div class="report-section">
<h3>Date Range</h3>
<label>Start Date</label>
<input type="datetime-local" id="start-date">
<label>End Date</label>
<input type="datetime-local" id="end-date">
</div>
<div class="report-section">
<h3>Aggregation</h3>
<select id="aggregation">
<option value="stats_only">Summary Statistics Only</option>
<option value="daily">Daily Averages</option>
<option value="hourly">Hourly Averages</option>
<option value="raw">Raw Data (limited)</option>
</select>
</div>
<button class="btn-generate" id="btn-generate">Generate Report</button>
</aside>
<main class="report-content">
<div id="report-placeholder" class="no-data">
<h3>📊 Select an area and date range</h3>
<p>Choose a factory area from the sidebar, select the tags you want to analyze, and set your date range.</p>
</div>
<div id="report-loading" class="loading" style="display: none;">
<p>⏳ Generating report...</p>
</div>
<div id="report-results" style="display: none;"></div>
</main>
</div>
<script>
const factoryAreas = <?php echo json_encode($factoryAreas); ?>;
let selectedArea = null;
let selectedTags = [];
// Set default dates (last 24 hours)
const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
document.getElementById('end-date').value = now.toISOString().slice(0, 16);
document.getElementById('start-date').value = yesterday.toISOString().slice(0, 16);
// Area selection
document.querySelectorAll('.area-card').forEach(card => {
card.addEventListener('click', () => {
document.querySelectorAll('.area-card').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
selectedArea = card.dataset.area;
loadSections(selectedArea);
});
});
function loadSections(areaKey) {
const area = factoryAreas[areaKey];
const sectionSelect = document.getElementById('section-select');
sectionSelect.innerHTML = '<option value="">Select a section...</option>';
for (const [sectionKey, section] of Object.entries(area.sections)) {
sectionSelect.innerHTML += `<option value="${sectionKey}">${section.name}</option>`;
}
document.getElementById('section-picker').style.display = 'block';
document.getElementById('tag-picker').style.display = 'none';
}
document.getElementById('section-select').addEventListener('change', function() {
const sectionKey = this.value;
if (!sectionKey || !selectedArea) return;
const section = factoryAreas[selectedArea].sections[sectionKey];
const tagList = document.getElementById('tag-list');
tagList.innerHTML = '';
for (const [tagName, tagInfo] of Object.entries(section.tags)) {
tagList.innerHTML += `
<div class="tag-item">
<input type="checkbox" id="tag-${tagName}" value="${tagName}" checked>
<label for="tag-${tagName}">${tagInfo.label} (${tagInfo.unit})</label>
</div>
`;
}
document.getElementById('tag-picker').style.display = 'block';
});
// Generate report
document.getElementById('btn-generate').addEventListener('click', async () => {
const checkedTags = document.querySelectorAll('#tag-list input:checked');
if (checkedTags.length === 0) {
alert('Please select at least one tag');
return;
}
const tags = Array.from(checkedTags).map(cb => cb.value);
const startDate = document.getElementById('start-date').value;
const endDate = document.getElementById('end-date').value;
const aggregation = document.getElementById('aggregation').value;
if (!startDate || !endDate) {
alert('Please select a date range');
return;
}
document.getElementById('report-placeholder').style.display = 'none';
document.getElementById('report-results').style.display = 'none';
document.getElementById('report-loading').style.display = 'block';
try {
const params = new URLSearchParams({
tags: tags.join(','),
start_date: startDate.replace('T', ' '),
end_date: endDate.replace('T', ' '),
aggregation: aggregation === 'stats_only' ? 'daily' : aggregation
});
if (aggregation === 'stats_only') {
params.append('stats_only', '1');
}
const response = await fetch('api/data.php?' + params);
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Unknown error');
}
renderResults(result.data, aggregation, tags);
} catch (error) {
document.getElementById('report-results').innerHTML = `
<div class="no-data">
<h3>❌ Error</h3>
<p>${error.message}</p>
</div>
`;
document.getElementById('report-results').style.display = 'block';
} finally {
document.getElementById('report-loading').style.display = 'none';
}
});
function renderResults(data, aggregation, tags) {
const container = document.getElementById('report-results');
let html = '';
if (aggregation === 'stats_only') {
// Summary table
html = `
<h3>Summary Statistics</h3>
<table class="results-table">
<thead>
<tr>
<th>Tag</th>
<th>Min</th>
<th>Max</th>
<th>Average</th>
<th>Samples</th>
</tr>
</thead>
<tbody>
`;
for (const tagName of tags) {
const stats = data[tagName];
if (stats && !stats.error) {
html += `
<tr>
<td>${tagName}</td>
<td class="stat-value">${stats.min !== null ? stats.min.toLocaleString() : '—'}</td>
<td class="stat-value">${stats.max !== null ? stats.max.toLocaleString() : '—'}</td>
<td class="stat-value">${stats.avg !== null ? stats.avg.toLocaleString() : '—'}</td>
<td>${stats.samples.toLocaleString()}</td>
</tr>
`;
} else {
html += `
<tr>
<td>${tagName}</td>
<td colspan="4" style="color: var(--text-tertiary);">${stats?.error || 'No data'}</td>
</tr>
`;
}
}
html += '</tbody></table>';
} else {
// Time series table
for (const tagName of tags) {
const rows = data[tagName];
if (!rows || rows.error) {
html += `<p>${tagName}: ${rows?.error || 'No data'}</p>`;
continue;
}
html += `
<h3>${tagName}</h3>
<table class="results-table" data-tag="${tagName}">
<thead>
<tr>
<th>Time</th>
<th>Min</th>
<th>Max</th>
<th>Average</th>
<th>Samples</th>
</tr>
</thead>
<tbody>
`;
for (const row of rows.slice(0, 500)) {
const time = row.time_bucket || row.TimeStamp;
html += `
<tr>
<td>${time}</td>
<td>${row.min_val !== undefined ? Number(row.min_val).toFixed(2) : row.Value}</td>
<td>${row.max_val !== undefined ? Number(row.max_val).toFixed(2) : '—'}</td>
<td>${row.avg_val !== undefined ? Number(row.avg_val).toFixed(2) : '—'}</td>
<td>${row.samples || 1}</td>
</tr>
`;
}
html += '</tbody></table>';
}
}
html += `
<div class="export-buttons">
<button class="btn-export" onclick="exportCSV()">📥 Export CSV</button>
</div>
`;
container.innerHTML = html;
container.style.display = 'block';
}
function exportCSV() {
const tables = document.querySelectorAll('.results-table');
let csv = '';
tables.forEach(table => {
const tagName = table.dataset.tag || 'Summary';
csv += `\n${tagName}\n`;
table.querySelectorAll('tr').forEach(row => {
const cells = row.querySelectorAll('th, td');
csv += Array.from(cells).map(c => `"${c.textContent.trim()}"`).join(',') + '\n';
});
});
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `factory-report-${new Date().toISOString().slice(0,10)}.csv`;
a.click();
}
</script>
<?php require __DIR__ . '/../includes/layout/footer.php'; ?>