513 lines
14 KiB
PHP
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'; ?>
|