add all files

This commit is contained in:
Rucus
2026-02-17 09:29:34 -06:00
parent b8c8d67c67
commit 782d203799
21925 changed files with 2433086 additions and 0 deletions

View File

@@ -0,0 +1,171 @@
<?php // phpcs:ignoreFile
/**
* Report Data API
*
* Fetches aggregated data from SQL Server archive for reporting
*/
header('Content-Type: application/json');
header('Cache-Control: no-cache, must-revalidate');
require __DIR__ . '/../config.php';
$response = ['success' => false, 'error' => null, 'data' => []];
try {
$pdo = getReportDbConnection();
if (!$pdo) {
throw new Exception('Database connection failed');
}
// Parameters
$tags = isset($_GET['tags']) ? explode(',', $_GET['tags']) : [];
$startDate = $_GET['start_date'] ?? null;
$endDate = $_GET['end_date'] ?? null;
$aggregation = $_GET['aggregation'] ?? 'hourly'; // hourly, daily, raw
$statsOnly = isset($_GET['stats_only']); // Return only min/max/avg
if (empty($tags)) {
throw new Exception('No tags specified');
}
if (!$startDate || !$endDate) {
throw new Exception('Start and end dates required');
}
// Validate aggregation
$validAggregations = ['hourly', 'daily', 'raw'];
if (!in_array($aggregation, $validAggregations)) {
$aggregation = 'hourly';
}
// Build results for each tag
$results = [];
foreach ($tags as $tagName) {
$tagName = trim($tagName);
if (empty($tagName)) continue;
// Use JOIN pattern like trends/live/realtime_data.php
// No separate lookup - join archive with id_names directly
if ($statsOnly) {
// Return summary statistics only (fast)
// CAST Value to FLOAT since it's stored as VARCHAR
$sql = "SELECT
MIN(CAST(a.Value AS FLOAT)) as min_val,
MAX(CAST(a.Value AS FLOAT)) as max_val,
AVG(CAST(a.Value AS FLOAT)) as avg_val,
COUNT(*) as sample_count,
MIN(a.TimeStamp) as first_sample,
MAX(a.TimeStamp) as last_sample
FROM dbo.archive a
INNER JOIN dbo.id_names n ON a.ID = n.idnumber
WHERE n.name = :tag_name
AND a.TimeStamp BETWEEN :start_date AND :end_date";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':tag_name' => $tagName,
':start_date' => $startDate,
':end_date' => $endDate
]);
$stats = $stmt->fetch();
if (!$stats || $stats['sample_count'] == 0) {
$results[$tagName] = ['error' => 'No data found for this tag/date range'];
continue;
}
$results[$tagName] = [
'min' => $stats['min_val'] !== null ? round((float)$stats['min_val'], 2) : null,
'max' => $stats['max_val'] !== null ? round((float)$stats['max_val'], 2) : null,
'avg' => $stats['avg_val'] !== null ? round((float)$stats['avg_val'], 2) : null,
'samples' => (int)$stats['sample_count'],
'first' => $stats['first_sample'],
'last' => $stats['last_sample']
];
} elseif ($aggregation === 'hourly') {
// Hourly aggregation
$sql = "SELECT
DATEADD(HOUR, DATEDIFF(HOUR, 0, a.TimeStamp), 0) as time_bucket,
MIN(CAST(a.Value AS FLOAT)) as min_val,
MAX(CAST(a.Value AS FLOAT)) as max_val,
AVG(CAST(a.Value AS FLOAT)) as avg_val,
COUNT(*) as samples
FROM dbo.archive a
INNER JOIN dbo.id_names n ON a.ID = n.idnumber
WHERE n.name = :tag_name
AND a.TimeStamp BETWEEN :start_date AND :end_date
GROUP BY DATEADD(HOUR, DATEDIFF(HOUR, 0, a.TimeStamp), 0)
ORDER BY time_bucket";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':tag_name' => $tagName,
':start_date' => $startDate,
':end_date' => $endDate
]);
$results[$tagName] = $stmt->fetchAll();
} elseif ($aggregation === 'daily') {
// Daily aggregation
$sql = "SELECT
CAST(a.TimeStamp AS DATE) as time_bucket,
MIN(CAST(a.Value AS FLOAT)) as min_val,
MAX(CAST(a.Value AS FLOAT)) as max_val,
AVG(CAST(a.Value AS FLOAT)) as avg_val,
COUNT(*) as samples
FROM dbo.archive a
INNER JOIN dbo.id_names n ON a.ID = n.idnumber
WHERE n.name = :tag_name
AND a.TimeStamp BETWEEN :start_date AND :end_date
GROUP BY CAST(a.TimeStamp AS DATE)
ORDER BY time_bucket";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':tag_name' => $tagName,
':start_date' => $startDate,
':end_date' => $endDate
]);
$results[$tagName] = $stmt->fetchAll();
} else {
// Raw data (limited)
$sql = "SELECT a.TimeStamp, a.Value
FROM dbo.archive a
INNER JOIN dbo.id_names n ON a.ID = n.idnumber
WHERE n.name = :tag_name
AND a.TimeStamp BETWEEN :start_date AND :end_date
ORDER BY a.TimeStamp
OFFSET 0 ROWS FETCH NEXT :limit ROWS ONLY";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':tag_name', $tagName);
$stmt->bindValue(':start_date', $startDate);
$stmt->bindValue(':end_date', $endDate);
$stmt->bindValue(':limit', REPORT_MAX_ROWS, PDO::PARAM_INT);
$stmt->execute();
$results[$tagName] = $stmt->fetchAll();
}
}
$response['success'] = true;
$response['data'] = $results;
$response['params'] = [
'start_date' => $startDate,
'end_date' => $endDate,
'aggregation' => $aggregation,
'stats_only' => $statsOnly
];
} catch (Exception $e) {
$response['error'] = $e->getMessage();
}
echo json_encode($response, JSON_PRETTY_PRINT);

View File

@@ -0,0 +1,42 @@
<?php // phpcs:ignoreFile
/**
* Debug endpoint - list tags from id_names table
*/
header('Content-Type: application/json');
require __DIR__ . '/../config.php';
$pdo = getReportDbConnection();
if (!$pdo) {
echo json_encode(['error' => 'Connection failed']);
exit;
}
$search = $_GET['search'] ?? '';
if ($search) {
// Search for tags containing the search term
$stmt = $pdo->prepare("
SELECT TOP 50 idnumber, name, description
FROM dbo.id_names
WHERE name LIKE :search OR description LIKE :search
ORDER BY name
");
$stmt->execute([':search' => '%' . $search . '%']);
} else {
// Return first 100 tags
$stmt = $pdo->prepare("
SELECT TOP 100 idnumber, name, description
FROM dbo.id_names
ORDER BY name
");
$stmt->execute();
}
$tags = $stmt->fetchAll();
echo json_encode([
'count' => count($tags),
'tags' => $tags
], JSON_PRETTY_PRINT);

View File

@@ -0,0 +1,165 @@
<?php
/**
* Report Configuration
*
* Defines factory areas and their associated tags for reporting
*/
// SQL Server connection (history database)
define('REPORT_DB_SERVER', '192.168.0.13\\SQLEXPRESS');
define('REPORT_DB_NAME', 'history');
define('REPORT_DB_USER', 'opce');
define('REPORT_DB_PASS', 'opcelasuca');
// Maximum rows to return in a single query (safety limit)
define('REPORT_MAX_ROWS', 10000);
// Factory areas and their associated tags
$factoryAreas = [
'milling' => [
'name' => 'Milling',
'description' => 'Mill grinding operations - East and West',
'sections' => [
'production' => [
'name' => 'Production Totals',
'tags' => [
'CANETOT' => ['label' => 'East Tons Ground', 'unit' => 'Tons'],
'W TONS GROUND' => ['label' => 'West Tons Ground', 'unit' => 'Tons'],
'TotalGround' => ['label' => 'Combined Total', 'unit' => 'Tons'],
],
],
'rates' => [
'name' => 'Grinding Rates',
'tags' => [
'RATE' => ['label' => 'West Tons/Hr', 'unit' => 'Tons/Hr'],
'MAINSPD' => ['label' => 'East MCC Speed', 'unit' => 'FPM'],
'Feet/Minute' => ['label' => 'West BC-09 Speed', 'unit' => 'FPM'],
],
],
'tanks' => [
'name' => 'Tank Levels',
'tags' => [
'Juice Tank1' => ['label' => 'Juice Tank 1', 'unit' => '%'],
'Juice Tank2' => ['label' => 'Juice Tank 2', 'unit' => '%'],
'Syrup Overflow Lvl' => ['label' => 'Syrup Overflow', 'unit' => '%'],
'Syrup RCVR' => ['label' => 'Syrup Receiver', 'unit' => '%'],
],
],
'steam' => [
'name' => 'Steam',
'tags' => [
'PT_001' => ['label' => 'Live Steam Pressure', 'unit' => 'PSI'],
],
],
'imbibition' => [
'name' => 'Imbibition',
'tags' => [
'IMB5Flow' => ['label' => 'IMB 5 Flow', 'unit' => 'GPM'],
'IMB6Flow' => ['label' => 'IMB 6 Flow', 'unit' => 'GPM'],
'WMillIMBFlow' => ['label' => 'West IMB Flow', 'unit' => 'GPM'],
],
],
'runtime' => [
'name' => 'Run Time',
'tags' => [
'RUNHRSTODAY' => ['label' => 'East Run Hours', 'unit' => 'Hours'],
'todayrun' => ['label' => 'West Run Hours', 'unit' => 'Hours'],
'LOSSTIME' => ['label' => 'East Loss Time', 'unit' => 'Hours'],
'todayloss' => ['label' => 'West Loss Time', 'unit' => 'Hours'],
],
],
],
],
'boilers' => [
'name' => 'Boilers',
'description' => 'Boiler operations and steam generation',
'sections' => [
'steam_totals' => [
'name' => 'Steam Totals',
'tags' => [
'B1TotSteam' => ['label' => 'Boiler 1 Steam', 'unit' => 'PPH'],
'B2TotSteam' => ['label' => 'Boiler 2 Steam', 'unit' => 'PPH'],
'B3TotSteam' => ['label' => 'Boiler 3 Steam', 'unit' => 'PPH'],
'B4TotSteam' => ['label' => 'Boiler 4 Steam', 'unit' => 'PPH'],
],
],
'pressures' => [
'name' => 'Pressures',
'tags' => [
'HouseAirPressure' => ['label' => 'House Air', 'unit' => 'PSI'],
],
],
],
],
'fabrication' => [
'name' => 'Fabrication',
'description' => 'Sugar fabrication and dryers',
'sections' => [
'tanks' => [
'name' => 'Tank Levels',
'tags' => [
'BBTankLvl' => ['label' => 'Broadbent Tank', 'unit' => '%'],
'ACVPMassecuiteLVL' => ['label' => 'A CVP Massecuite', 'unit' => '%'],
],
],
'dryers' => [
'name' => 'Dryers',
'tags' => [
'A1Running' => ['label' => 'Dryer A1', 'unit' => 'On/Off'],
'A2Running' => ['label' => 'Dryer A2', 'unit' => 'On/Off'],
'A3Running' => ['label' => 'Dryer A3', 'unit' => 'On/Off'],
'A4Running' => ['label' => 'Dryer A4', 'unit' => 'On/Off'],
'A5Running' => ['label' => 'Dryer A5', 'unit' => 'On/Off'],
'A6Running' => ['label' => 'Dryer A6', 'unit' => 'On/Off'],
'A7Running' => ['label' => 'Dryer A7', 'unit' => 'On/Off'],
],
],
],
],
];
/**
* Get database connection
*/
function getReportDbConnection(): ?PDO
{
static $pdo = null;
if ($pdo === null) {
try {
$pdo = new PDO(
"sqlsrv:Server=" . REPORT_DB_SERVER . ";Database=" . REPORT_DB_NAME,
REPORT_DB_USER,
REPORT_DB_PASS,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]
);
} catch (PDOException $e) {
error_log('Report DB connection failed: ' . $e->getMessage());
return null;
}
}
return $pdo;
}
/**
* Get tag ID from name (cached)
*/
function getTagId(PDO $pdo, string $tagName): ?string
{
static $cache = [];
if (isset($cache[$tagName])) {
return $cache[$tagName];
}
$stmt = $pdo->prepare("SELECT idnumber FROM dbo.id_names WHERE name = ?");
$stmt->execute([$tagName]);
$row = $stmt->fetch();
$cache[$tagName] = $row ? $row['idnumber'] : null;
return $cache[$tagName];
}

View File

@@ -0,0 +1,512 @@
<?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'; ?>

View File

@@ -0,0 +1,373 @@
<?php // phpcs:ignoreFile
/**
* Mill Names Admin - Manage mill code to display name mappings
*/
require __DIR__ . '/../session.php';
require __DIR__ . '/../userAccess.php';
// Only allow admin or controls
if ($_SESSION['SESS_MEMBER_LEVEL'] < 4) {
header("Location: ../access-denied.php");
exit();
}
$config = [
'server' => '192.168.0.16',
'database' => 'lasucaai',
'username' => 'lasucaai',
'password' => 'is413#dfslw',
];
$connectionOptions = [
"Database" => $config['database'],
"Uid" => $config['username'],
"PWD" => $config['password'],
"TrustServerCertificate" => true,
"Encrypt" => false,
];
$conn = sqlsrv_connect($config['server'], $connectionOptions);
if ($conn === false) {
die("Connection failed: " . print_r(sqlsrv_errors(), true));
}
$message = '';
$messageType = '';
// Handle form submissions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['action'])) {
switch ($_POST['action']) {
case 'update':
$millId = intval($_POST['mill_id']);
$displayName = trim($_POST['display_name']);
$sortOrder = intval($_POST['sort_order']);
$isActive = isset($_POST['is_active']) ? 1 : 0;
$sql = "UPDATE mill_names SET display_name = ?, sort_order = ?, is_active = ? WHERE mill_id = ?";
$result = sqlsrv_query($conn, $sql, [$displayName, $sortOrder, $isActive, $millId]);
if ($result) {
$message = "Mill name updated successfully!";
$messageType = 'success';
} else {
$message = "Error updating: " . print_r(sqlsrv_errors(), true);
$messageType = 'error';
}
break;
case 'add':
$millCode = trim($_POST['mill_code']);
$displayName = trim($_POST['display_name']);
$sortOrder = intval($_POST['sort_order']);
$sql = "INSERT INTO mill_names (mill_code, display_name, sort_order) VALUES (?, ?, ?)";
$result = sqlsrv_query($conn, $sql, [$millCode, $displayName, $sortOrder]);
if ($result) {
$message = "Mill name added successfully!";
$messageType = 'success';
} else {
$message = "Error adding: " . print_r(sqlsrv_errors(), true);
$messageType = 'error';
}
break;
case 'delete':
$millId = intval($_POST['mill_id']);
$sql = "DELETE FROM mill_names WHERE mill_id = ?";
$result = sqlsrv_query($conn, $sql, [$millId]);
if ($result) {
$message = "Mill name deleted!";
$messageType = 'success';
} else {
$message = "Error deleting: " . print_r(sqlsrv_errors(), true);
$messageType = 'error';
}
break;
}
}
}
// Fetch all mill names
$sql = "SELECT * FROM mill_names ORDER BY sort_order, mill_code";
$result = sqlsrv_query($conn, $sql);
$millNames = [];
while ($row = sqlsrv_fetch_array($result, SQLSRV_FETCH_ASSOC)) {
$millNames[] = $row;
}
// Layout config
$layoutWithoutSidebar = true;
$layoutReturnUrl = 'milldata-dashboard.php';
$layoutCloseWindowLabel = 'Back to Dashboard';
$assetBasePath = '../';
include __DIR__ . '/../includes/layout/header.php';
?>
<style>
.admin-container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
h1 {
color: var(--text);
margin-bottom: 20px;
font-size: 1.5rem;
}
.message {
padding: 12px 16px;
border-radius: 6px;
margin-bottom: 20px;
}
.message.success {
background: rgba(76, 175, 80, 0.2);
border: 1px solid #4caf50;
color: #4caf50;
}
.message.error {
background: rgba(244, 67, 54, 0.2);
border: 1px solid #f44336;
color: #f44336;
}
.mill-table {
width: 100%;
border-collapse: collapse;
background: var(--surface);
border-radius: 8px;
overflow: hidden;
}
.mill-table th,
.mill-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid var(--border);
}
.mill-table th {
background: var(--surface-alt, #2a2a4a);
color: var(--text);
font-weight: 600;
font-size: 0.85rem;
text-transform: uppercase;
}
.mill-table tr:hover {
background: rgba(255, 255, 255, 0.05);
}
.mill-table input[type="text"],
.mill-table input[type="number"] {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 4px;
padding: 6px 10px;
color: var(--text);
width: 100%;
box-sizing: border-box;
}
.mill-table input[type="text"]:focus,
.mill-table input[type="number"]:focus {
outline: none;
border-color: var(--accent);
}
.btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
transition: opacity 0.2s;
}
.btn:hover {
opacity: 0.8;
}
.btn-primary {
background: var(--accent);
color: #fff;
}
.btn-danger {
background: #f44336;
color: #fff;
}
.btn-sm {
padding: 4px 8px;
font-size: 0.75rem;
}
.actions {
display: flex;
gap: 6px;
}
.add-form {
background: var(--surface);
border-radius: 8px;
padding: 20px;
margin-top: 30px;
}
.add-form h2 {
color: var(--text);
font-size: 1.1rem;
margin-bottom: 15px;
}
.form-row {
display: flex;
gap: 15px;
align-items: flex-end;
flex-wrap: wrap;
}
.form-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.form-group label {
font-size: 0.8rem;
color: var(--text-muted, #888);
}
.form-group input {
background: var(--surface-alt, #2a2a4a);
border: 1px solid var(--border);
border-radius: 4px;
padding: 8px 12px;
color: var(--text);
}
.status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 0.75rem;
font-weight: 600;
}
.status-badge.active {
background: rgba(76, 175, 80, 0.2);
color: #4caf50;
}
.status-badge.inactive {
background: rgba(158, 158, 158, 0.2);
color: #9e9e9e;
}
.code-cell {
font-family: monospace;
background: var(--surface-alt, #2a2a4a);
padding: 4px 8px;
border-radius: 4px;
font-size: 0.85rem;
}
</style>
<div class="admin-container">
<h1>🏭 Mill Names Admin</h1>
<?php if ($message): ?>
<div class="message <?= $messageType ?>"><?= htmlspecialchars($message) ?></div>
<?php endif; ?>
<table class="mill-table">
<thead>
<tr>
<th>Mill Code</th>
<th>Display Name</th>
<th>Sort Order</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($millNames as $mill): ?>
<tr>
<form method="POST">
<input type="hidden" name="action" value="update">
<input type="hidden" name="mill_id" value="<?= $mill['mill_id'] ?>">
<td><span class="code-cell"><?= htmlspecialchars($mill['mill_code']) ?></span></td>
<td>
<input type="text" name="display_name"
value="<?= htmlspecialchars($mill['display_name']) ?>"
placeholder="Display name...">
</td>
<td style="width: 80px;">
<input type="number" name="sort_order"
value="<?= $mill['sort_order'] ?>"
style="width: 60px;">
</td>
<td>
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer;">
<input type="checkbox" name="is_active" <?= $mill['is_active'] ? 'checked' : '' ?>>
<span class="status-badge <?= $mill['is_active'] ? 'active' : 'inactive' ?>">
<?= $mill['is_active'] ? 'Active' : 'Inactive' ?>
</span>
</label>
</td>
<td>
<div class="actions">
<button type="submit" class="btn btn-primary btn-sm">Save</button>
</form>
<form method="POST" style="display: inline;"
onsubmit="return confirm('Delete this mill name?');">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="mill_id" value="<?= $mill['mill_id'] ?>">
<button type="submit" class="btn btn-danger btn-sm">×</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div class="add-form">
<h2>Add New Mill Name</h2>
<form method="POST">
<input type="hidden" name="action" value="add">
<div class="form-row">
<div class="form-group">
<label>Mill Code (exact match from data)</label>
<input type="text" name="mill_code" required placeholder="e.g., EastMill">
</div>
<div class="form-group">
<label>Display Name</label>
<input type="text" name="display_name" required placeholder="e.g., East Mill">
</div>
<div class="form-group">
<label>Sort Order</label>
<input type="number" name="sort_order" value="<?= count($millNames) + 1 ?>" style="width: 80px;">
</div>
<button type="submit" class="btn btn-primary">Add Mill</button>
</div>
</form>
</div>
</div>
<?php
sqlsrv_close($conn);
include __DIR__ . '/../includes/layout/footer.php';
?>

View File

@@ -0,0 +1,546 @@
<?php // phpcs:ignoreFile
/**
* Mill Data Comparison - Compare categories across mills
*/
require __DIR__ . '/../session.php';
require __DIR__ . '/../userAccess.php';
require __DIR__ . '/../includes/millnames.php';
// ============================================================================
// Database Configuration
// ============================================================================
$config = [
'server' => '192.168.0.16',
'database' => 'lasucaai',
'username' => 'lasucaai',
'password' => 'is413#dfslw',
];
// ============================================================================
// Database Connection
// ============================================================================
function getCompareConnection($config) {
$connectionOptions = [
"Database" => $config['database'],
"Uid" => $config['username'],
"PWD" => $config['password'],
"TrustServerCertificate" => true,
"Encrypt" => false,
];
$conn = sqlsrv_connect($config['server'], $connectionOptions);
return $conn ?: null;
}
// ============================================================================
// Data Queries
// ============================================================================
function getCompareSourceFiles($conn) {
$sql = "SELECT DISTINCT SourceFileName,
MIN(BeginningDate) as BeginningDate,
MAX(EndingDate) as EndingDate,
MAX(ProcessedAt) as ProcessedAt
FROM dbo.MillDataReports
GROUP BY SourceFileName
ORDER BY MAX(ProcessedAt) DESC";
$stmt = sqlsrv_query($conn, $sql);
$files = [];
if ($stmt) {
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$files[] = $row;
}
}
return $files;
}
function getCompareCategories($conn, $sourceFileName = null) {
// Get all categories - simpler query that matches working pattern
$sql = "SELECT DISTINCT Category FROM dbo.MillDataMetrics WHERE Category IS NOT NULL ORDER BY Category";
$stmt = sqlsrv_query($conn, $sql);
$categories = [];
if ($stmt) {
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$categories[] = $row['Category'];
}
}
return $categories;
}
function getMillsForCompare($conn, $sourceFileName) {
$sql = "SELECT ReportId, MillName
FROM dbo.MillDataReports
WHERE SourceFileName = ?
ORDER BY MillName";
$stmt = sqlsrv_query($conn, $sql, [$sourceFileName]);
$mills = [];
if ($stmt) {
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$mills[] = $row;
}
}
return $mills;
}
function getComparisonData($conn, $sourceFileName, $category) {
// Get all metrics for this category across all mills in this file
$sql = "SELECT r.MillName, r.ReportId, m.ItemNumber, m.MetricName,
m.RunValue, m.RunValueNumeric, m.ToDateValue, m.ToDateValueNumeric
FROM dbo.MillDataMetrics m
JOIN dbo.MillDataReports r ON m.ReportId = r.ReportId
WHERE r.SourceFileName = ? AND m.Category = ?
ORDER BY m.ItemNumber, m.MetricName, r.MillName";
$stmt = sqlsrv_query($conn, $sql, [$sourceFileName, $category]);
$data = [];
if ($stmt) {
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$data[] = $row;
}
}
return $data;
}
function formatCompareDate($date) {
if ($date instanceof DateTime) {
return $date->format('m/d/Y');
}
return $date ?? '-';
}
// ============================================================================
// Main Logic
// ============================================================================
$conn = getCompareConnection($config);
$connectionError = $conn === null;
$sourceFiles = [];
$categories = [];
$mills = [];
$comparisonData = [];
$pivotedData = [];
$millNameLookup = [];
$selectedFile = isset($_GET['file']) ? trim($_GET['file']) : '';
$selectedCategory = isset($_GET['category']) ? trim($_GET['category']) : '';
if (!$connectionError) {
// Load mill name mappings
$millNameLookup = getMillNames($conn);
$sourceFiles = getCompareSourceFiles($conn);
$categories = getCompareCategories($conn);
// Default to first file
if (empty($selectedFile) && !empty($sourceFiles)) {
$selectedFile = $sourceFiles[0]['SourceFileName'];
}
if (!empty($selectedFile)) {
$mills = getMillsForCompare($conn, $selectedFile);
// Default to first category
if (empty($selectedCategory) && !empty($categories)) {
$selectedCategory = $categories[0];
}
if (!empty($selectedCategory)) {
$comparisonData = getComparisonData($conn, $selectedFile, $selectedCategory);
// Pivot data: group by metric, columns are mills
foreach ($comparisonData as $row) {
$metricKey = $row['ItemNumber'] . '|' . $row['MetricName'];
if (!isset($pivotedData[$metricKey])) {
$pivotedData[$metricKey] = [
'ItemNumber' => $row['ItemNumber'],
'MetricName' => $row['MetricName'],
'mills' => []
];
}
$pivotedData[$metricKey]['mills'][$row['MillName']] = [
'RunValue' => $row['RunValue'],
'RunValueNumeric' => $row['RunValueNumeric'],
'ToDateValue' => $row['ToDateValue'],
'ToDateValueNumeric' => $row['ToDateValueNumeric']
];
}
}
}
sqlsrv_close($conn);
}
// Get unique mill codes for column headers (raw codes used as data keys)
$millCodes = [];
foreach ($mills as $m) {
$millCodes[] = $m['MillName'];
}
// Build display names map for headers
$millDisplayNames = [];
foreach ($millCodes as $code) {
$millDisplayNames[$code] = getMillDisplayName($millNameLookup, $code);
}
// ============================================================================
// Page Layout
// ============================================================================
$pageTitle = 'Mill Comparison';
$pageSubtitle = 'Compare metrics across mills';
$pageDescription = 'Side-by-side comparison of mill production data.';
$layoutWithoutSidebar = true;
$layoutReturnUrl = 'milldata.php';
$layoutReturnLabel = 'Back to Mill Data';
$assetBasePath = '../';
require __DIR__ . '/../includes/layout/header.php';
?>
<style>
.compare-controls {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
align-items: flex-end;
}
.compare-control {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.compare-control label {
font-size: 0.8rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.compare-control select {
padding: 0.6rem 1rem;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 0.9rem;
background: var(--surface);
color: var(--text);
min-width: 200px;
}
.compare-table-container {
background: var(--surface);
border-radius: 8px;
border: 1px solid var(--border);
overflow-x: auto;
max-height: 600px;
overflow-y: auto;
}
.compare-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: 0.85rem;
}
.compare-table th,
.compare-table td {
padding: 0.5rem 0.6rem;
border-bottom: 1px solid var(--border);
}
.compare-table th {
background: var(--surface-alt);
text-align: left;
font-weight: 600;
color: var(--text-muted);
border-bottom: 2px solid var(--border);
position: sticky;
top: 0;
white-space: nowrap;
z-index: 20;
}
/* Sticky first two columns */
.compare-table th.sticky-col,
.compare-table td.sticky-col {
position: sticky;
background: var(--surface);
z-index: 10;
}
.compare-table th.sticky-col {
background: var(--surface-alt);
z-index: 30;
}
.compare-table .sticky-col-1 {
left: 0;
min-width: 60px;
max-width: 60px;
}
.compare-table .sticky-col-2 {
left: 60px;
min-width: 200px;
max-width: 250px;
border-right: 2px solid var(--border);
}
.compare-table th.mill-header {
text-align: center;
min-width: 120px;
}
.compare-table th.mill-header .mill-name {
font-weight: 600;
color: var(--accent);
}
/* Mill column shading - alternating backgrounds */
.compare-table .mill-col-0 { background-color: rgba(33, 150, 243, 0.08); }
.compare-table .mill-col-1 { background-color: rgba(76, 175, 80, 0.08); }
.compare-table .mill-col-2 { background-color: rgba(156, 39, 176, 0.08); }
.compare-table .mill-col-3 { background-color: rgba(255, 152, 0, 0.08); }
.compare-table .mill-col-4 { background-color: rgba(0, 188, 212, 0.08); }
.compare-table .mill-col-5 { background-color: rgba(233, 30, 99, 0.08); }
.compare-table .mill-col-6 { background-color: rgba(139, 195, 74, 0.08); }
.compare-table .mill-col-7 { background-color: rgba(121, 85, 72, 0.08); }
/* Header shading matches columns */
.compare-table th.mill-col-0 { background-color: rgba(33, 150, 243, 0.15); }
.compare-table th.mill-col-1 { background-color: rgba(76, 175, 80, 0.15); }
.compare-table th.mill-col-2 { background-color: rgba(156, 39, 176, 0.15); }
.compare-table th.mill-col-3 { background-color: rgba(255, 152, 0, 0.15); }
.compare-table th.mill-col-4 { background-color: rgba(0, 188, 212, 0.15); }
.compare-table th.mill-col-5 { background-color: rgba(233, 30, 99, 0.15); }
.compare-table th.mill-col-6 { background-color: rgba(139, 195, 74, 0.15); }
.compare-table th.mill-col-7 { background-color: rgba(121, 85, 72, 0.15); }
.compare-table th.sub-header {
font-size: 0.75rem;
font-weight: 500;
top: 35px;
}
.compare-table tr:hover td {
background: var(--hover);
}
.compare-table tr:hover td.sticky-col {
background: var(--hover);
}
.compare-table .item-num {
font-weight: 600;
color: var(--accent);
white-space: nowrap;
}
.compare-table .metric-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.compare-table .value-cell {
text-align: right;
font-family: 'Consolas', 'Monaco', monospace;
white-space: nowrap;
}
.compare-table .value-cell.negative {
color: #ef5350;
}
.compare-table .value-cell.best {
background: rgba(76, 175, 80, 0.15);
font-weight: 600;
}
.compare-table .value-cell.worst {
background: rgba(244, 67, 54, 0.1);
}
.no-data {
text-align: center;
padding: 3rem;
color: var(--text-muted);
background: var(--surface);
border-radius: 8px;
border: 1px solid var(--border);
}
.compare-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.compare-stat {
background: var(--surface);
padding: 1rem;
border-radius: 8px;
text-align: center;
border: 1px solid var(--border);
}
.compare-stat .value {
font-size: 1.5rem;
font-weight: 600;
color: var(--accent);
}
.compare-stat .label {
font-size: 0.8rem;
color: var(--text-muted);
}
.view-toggle {
display: flex;
gap: 0.5rem;
margin-left: auto;
}
.view-toggle button {
padding: 0.5rem 1rem;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
}
.view-toggle button.active {
background: var(--accent);
color: white;
border-color: var(--accent);
}
</style>
<div class="app-content">
<section class="data-panel">
<?php if ($connectionError): ?>
<div class="no-data">Unable to connect to the database.</div>
<?php else: ?>
<form class="compare-controls" method="get">
<div class="compare-control">
<label>Report File</label>
<select name="file" onchange="this.form.submit()">
<?php if (empty($sourceFiles)): ?>
<option value="">No reports available</option>
<?php else: ?>
<?php foreach ($sourceFiles as $file): ?>
<option value="<?= htmlspecialchars($file['SourceFileName']) ?>"
<?= $file['SourceFileName'] === $selectedFile ? 'selected' : '' ?>>
<?= htmlspecialchars($file['SourceFileName']) ?>
(<?= formatCompareDate($file['BeginningDate']) ?> - <?= formatCompareDate($file['EndingDate']) ?>)
</option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</div>
<div class="compare-control">
<label>Category</label>
<select name="category" onchange="this.form.submit()">
<?php if (empty($categories)): ?>
<option value="">No categories available</option>
<?php else: ?>
<?php foreach ($categories as $cat): ?>
<option value="<?= htmlspecialchars($cat) ?>"
<?= $cat === $selectedCategory ? 'selected' : '' ?>>
<?= htmlspecialchars($cat) ?>
</option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</div>
</form>
<div class="compare-summary">
<div class="compare-stat">
<div class="value"><?= count($millCodes) ?></div>
<div class="label">Mills</div>
</div>
<div class="compare-stat">
<div class="value"><?= count($pivotedData) ?></div>
<div class="label">Metrics</div>
</div>
<div class="compare-stat">
<div class="value"><?= htmlspecialchars($selectedCategory ?: '-') ?></div>
<div class="label">Category</div>
</div>
</div>
<?php if (!empty($pivotedData) && !empty($millCodes)): ?>
<div class="compare-table-container">
<table class="compare-table">
<thead>
<tr>
<th rowspan="2" class="sticky-col sticky-col-1">Item</th>
<th rowspan="2" class="sticky-col sticky-col-2">Metric</th>
<?php foreach ($millCodes as $idx => $millCode): ?>
<th colspan="2" class="mill-header mill-col-<?= $idx % 8 ?>">
<span class="mill-name"><?= htmlspecialchars($millDisplayNames[$millCode]) ?></span>
</th>
<?php endforeach; ?>
</tr>
<tr>
<?php foreach ($millCodes as $idx => $millCode): ?>
<th class="sub-header mill-col-<?= $idx % 8 ?>">RUN</th>
<th class="sub-header mill-col-<?= $idx % 8 ?>">TO DATE</th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php foreach ($pivotedData as $metric): ?>
<tr>
<td class="item-num sticky-col sticky-col-1"><?= htmlspecialchars($metric['ItemNumber'] ?? '') ?></td>
<td class="metric-name sticky-col sticky-col-2"><?= htmlspecialchars($metric['MetricName'] ?? '') ?></td>
<?php foreach ($millCodes as $idx => $millCode): ?>
<?php
$millData = $metric['mills'][$millCode] ?? null;
$runValue = $millData['RunValue'] ?? '-';
$toDateValue = $millData['ToDateValue'] ?? '-';
$runNumeric = $millData['RunValueNumeric'] ?? null;
$toDateNumeric = $millData['ToDateValueNumeric'] ?? null;
$colClass = 'mill-col-' . ($idx % 8);
?>
<td class="value-cell <?= $colClass ?> <?= ($runNumeric ?? 0) < 0 ? 'negative' : '' ?>">
<?= htmlspecialchars($runValue) ?>
</td>
<td class="value-cell <?= $colClass ?> <?= ($toDateNumeric ?? 0) < 0 ? 'negative' : '' ?>">
<?= htmlspecialchars($toDateValue) ?>
</td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="no-data">
<p>No comparison data available. Select a file and category above.</p>
</div>
<?php endif; ?>
<?php endif; ?>
</section>
</div>
<?php require __DIR__ . '/../includes/layout/footer.php'; ?>

View File

@@ -0,0 +1,835 @@
<?php // phpcs:ignoreFile
/**
* Mill Data Dashboard - Visual analytics and trends
*/
require __DIR__ . '/../session.php';
require __DIR__ . '/../userAccess.php';
require __DIR__ . '/../includes/millnames.php';
// ============================================================================
// Database Configuration
// ============================================================================
$config = [
'server' => '192.168.0.16',
'database' => 'lasucaai',
'username' => 'lasucaai',
'password' => 'is413#dfslw',
];
// ============================================================================
// Database Connection
// ============================================================================
function getDashboardConnection($config) {
$connectionOptions = [
"Database" => $config['database'],
"Uid" => $config['username'],
"PWD" => $config['password'],
"TrustServerCertificate" => true,
"Encrypt" => false,
];
$conn = sqlsrv_connect($config['server'], $connectionOptions);
return $conn ?: null;
}
// ============================================================================
// Data Queries
// ============================================================================
function getDashboardStats($conn) {
$sql = "SELECT
COUNT(DISTINCT r.ReportId) as TotalReports,
COUNT(DISTINCT r.SourceFileName) as TotalFiles,
COUNT(DISTINCT r.MillName) as TotalMills,
COUNT(m.MetricId) as TotalMetrics,
MIN(r.BeginningDate) as EarliestDate,
MAX(r.EndingDate) as LatestDate
FROM dbo.MillDataReports r
LEFT JOIN dbo.MillDataMetrics m ON r.ReportId = m.ReportId";
$stmt = sqlsrv_query($conn, $sql);
return sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
}
function getRecentReports($conn, $limit = 10) {
$sql = "SELECT TOP(?) r.ReportId, r.SourceFileName, r.MillName,
r.BeginningDate, r.EndingDate, r.CropDays, r.ProcessedAt
FROM dbo.MillDataReports r
ORDER BY r.ProcessedAt DESC";
$stmt = sqlsrv_query($conn, $sql, [$limit]);
$reports = [];
if ($stmt) {
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$reports[] = $row;
}
}
return $reports;
}
function getKeyMetricTrends($conn) {
// Get key grinding/production metrics across all reports with dates
$sql = "SELECT r.MillName, r.BeginningDate, r.EndingDate, r.SourceFileName,
m.MetricName, m.RunValueNumeric, m.ToDateValueNumeric, m.Category
FROM dbo.MillDataMetrics m
JOIN dbo.MillDataReports r ON m.ReportId = r.ReportId
WHERE m.MetricName IN (
'TC Ground This Week',
'TC Ground To Date',
'Hours This Week',
'Lost Time This Week',
'TC/GH This Week',
'Efficiency This Week'
)
AND m.RunValueNumeric IS NOT NULL
ORDER BY r.BeginningDate, r.MillName";
$stmt = sqlsrv_query($conn, $sql);
$trends = [];
if ($stmt) {
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$trends[] = $row;
}
}
return $trends;
}
function getMillPerformanceSummary($conn) {
// Get latest performance for each mill
$sql = "WITH LatestReports AS (
SELECT MillName, MAX(ProcessedAt) as LatestProcessed
FROM dbo.MillDataReports
GROUP BY MillName
)
SELECT r.MillName, r.BeginningDate, r.EndingDate, r.SourceFileName,
MAX(CASE WHEN m.MetricName = 'TC Ground This Week' THEN m.RunValueNumeric END) as TCGround,
MAX(CASE WHEN m.MetricName = 'Hours This Week' THEN m.RunValueNumeric END) as HoursRun,
MAX(CASE WHEN m.MetricName = 'Lost Time This Week' THEN m.RunValueNumeric END) as LostTime,
MAX(CASE WHEN m.MetricName = 'TC/GH This Week' THEN m.RunValueNumeric END) as TCPerHour,
MAX(CASE WHEN m.MetricName = 'TC Ground To Date' THEN m.ToDateValueNumeric END) as TCToDate
FROM dbo.MillDataReports r
JOIN LatestReports lr ON r.MillName = lr.MillName AND r.ProcessedAt = lr.LatestProcessed
LEFT JOIN dbo.MillDataMetrics m ON r.ReportId = m.ReportId
GROUP BY r.MillName, r.BeginningDate, r.EndingDate, r.SourceFileName
ORDER BY r.MillName";
$stmt = sqlsrv_query($conn, $sql);
$summary = [];
if ($stmt) {
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$summary[] = $row;
}
}
return $summary;
}
function getCategoryBreakdown($conn) {
$sql = "SELECT m.Category, COUNT(*) as MetricCount,
COUNT(DISTINCT r.ReportId) as ReportCount
FROM dbo.MillDataMetrics m
JOIN dbo.MillDataReports r ON m.ReportId = r.ReportId
WHERE m.Category IS NOT NULL
GROUP BY m.Category
ORDER BY COUNT(*) DESC";
$stmt = sqlsrv_query($conn, $sql);
$breakdown = [];
if ($stmt) {
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$breakdown[] = $row;
}
}
return $breakdown;
}
function getWeeklyTrendData($conn) {
// Get TC Ground per week for charting
$sql = "SELECT r.MillName, r.BeginningDate,
SUM(CASE WHEN m.MetricName = 'TC Ground This Week' THEN m.RunValueNumeric END) as TCGround,
SUM(CASE WHEN m.MetricName = 'Hours This Week' THEN m.RunValueNumeric END) as HoursRun
FROM dbo.MillDataReports r
JOIN dbo.MillDataMetrics m ON r.ReportId = m.ReportId
WHERE m.MetricName IN ('TC Ground This Week', 'Hours This Week')
GROUP BY r.MillName, r.BeginningDate
ORDER BY r.BeginningDate, r.MillName";
$stmt = sqlsrv_query($conn, $sql);
$data = [];
if ($stmt) {
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$data[] = $row;
}
}
return $data;
}
function formatDate($date) {
if ($date instanceof DateTime) {
return $date->format('m/d/Y');
}
return $date ?? '-';
}
function formatShortDate($date) {
if ($date instanceof DateTime) {
return $date->format('M j');
}
return $date ?? '-';
}
// ============================================================================
// Main Logic
// ============================================================================
$conn = getDashboardConnection($config);
$connectionError = $conn === null;
$stats = ['TotalReports' => 0, 'TotalFiles' => 0, 'TotalMills' => 0, 'TotalMetrics' => 0];
$recentReports = [];
$keyTrends = [];
$millPerformance = [];
$categoryBreakdown = [];
$weeklyTrends = [];
$millNameLookup = [];
if (!$connectionError) {
// Load mill name mappings
$millNameLookup = getMillNames($conn);
$stats = getDashboardStats($conn);
$recentReports = getRecentReports($conn, 10);
$keyTrends = getKeyMetricTrends($conn);
$millPerformance = getMillPerformanceSummary($conn);
$categoryBreakdown = getCategoryBreakdown($conn);
$weeklyTrends = getWeeklyTrendData($conn);
sqlsrv_close($conn);
}
// Prepare chart data
$chartLabels = [];
$chartDatasets = [];
$millColors = [
'East Mill' => 'rgba(59, 130, 246, 1)',
'West Mill' => 'rgba(16, 185, 129, 1)',
'Mill 1' => 'rgba(245, 158, 11, 1)',
'Mill 2' => 'rgba(239, 68, 68, 1)',
'Mill 3' => 'rgba(139, 92, 246, 1)',
'Mill 4' => 'rgba(236, 72, 153, 1)',
];
// Group weekly trends by mill
$millData = [];
foreach ($weeklyTrends as $row) {
$millCode = $row['MillName'];
$date = formatShortDate($row['BeginningDate']);
if (!isset($millData[$millCode])) {
$millData[$millCode] = [];
}
$millData[$millCode][$date] = $row['TCGround'];
if (!in_array($date, $chartLabels)) {
$chartLabels[] = $date;
}
}
foreach ($millData as $millCode => $data) {
$millDisplayName = getMillDisplayName($millNameLookup, $millCode);
$color = $millColors[$millCode] ?? $millColors[$millDisplayName] ?? 'rgba(107, 114, 128, 1)';
$bgColor = str_replace(', 1)', ', 0.2)', $color);
$values = [];
foreach ($chartLabels as $label) {
$values[] = $data[$label] ?? null;
}
$chartDatasets[] = [
'label' => $millDisplayName,
'data' => $values,
'borderColor' => $color,
'backgroundColor' => $bgColor,
'tension' => 0.3,
'fill' => false,
];
}
// ============================================================================
// Page Layout
// ============================================================================
$pageTitle = 'Mill Data Dashboard';
$pageSubtitle = 'Analytics & Performance Trends';
$pageDescription = 'Visual analytics dashboard for mill production data.';
$layoutWithoutSidebar = true;
$layoutReturnUrl = '../overview.php';
$layoutReturnLabel = 'Back to overview';
$assetBasePath = '../';
require __DIR__ . '/../includes/layout/header.php';
?>
<style>
/* Dashboard Grid */
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
/* KPI Cards */
.kpi-card {
background: var(--surface, #1a1a2e);
border: 1px solid var(--border, #333);
border-radius: 12px;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.kpi-card__label {
font-size: 0.875rem;
color: var(--text-muted, #888);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.kpi-card__value {
font-size: 2.5rem;
font-weight: 700;
color: var(--text, #fff);
line-height: 1;
}
.kpi-card__sub {
font-size: 0.8rem;
color: var(--text-muted, #888);
}
.kpi-card--accent {
border-color: var(--accent, #3b82f6);
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, var(--surface, #1a1a2e) 100%);
}
.kpi-card--success {
border-color: #10b981;
background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, var(--surface, #1a1a2e) 100%);
}
.kpi-card--warning {
border-color: #f59e0b;
background: linear-gradient(135deg, rgba(245, 158, 11, 0.1) 0%, var(--surface, #1a1a2e) 100%);
}
/* Chart Container */
.chart-container {
background: var(--surface, #1a1a2e);
border: 1px solid var(--border, #333);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 2rem;
}
.chart-container__title {
font-size: 1.125rem;
font-weight: 600;
color: var(--text, #fff);
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.chart-wrapper {
position: relative;
height: 300px;
}
/* Mill Performance Cards */
.mill-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.mill-card {
background: var(--surface, #1a1a2e);
border: 1px solid var(--border, #333);
border-radius: 12px;
padding: 1.5rem;
position: relative;
overflow: hidden;
}
.mill-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
}
.mill-card--east::before { background: #3b82f6; }
.mill-card--west::before { background: #10b981; }
.mill-card--default::before { background: #6b7280; }
.mill-card__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.mill-card__name {
font-size: 1.25rem;
font-weight: 700;
color: var(--text, #fff);
}
.mill-card__date {
font-size: 0.75rem;
color: var(--text-muted, #888);
text-align: right;
}
.mill-card__stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.mill-stat {
text-align: center;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 8px;
}
.mill-stat__value {
font-size: 1.5rem;
font-weight: 700;
color: var(--text, #fff);
}
.mill-stat__label {
font-size: 0.7rem;
color: var(--text-muted, #888);
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Sparkline */
.sparkline-row {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem 0;
border-bottom: 1px solid var(--border, #333);
}
.sparkline-row:last-child {
border-bottom: none;
}
.sparkline-label {
flex: 0 0 120px;
font-size: 0.875rem;
color: var(--text, #fff);
}
.sparkline-canvas {
flex: 1;
height: 30px;
}
.sparkline-value {
flex: 0 0 80px;
text-align: right;
font-weight: 600;
color: var(--text, #fff);
}
/* Category Pills */
.category-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.category-pill {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border, #333);
border-radius: 20px;
font-size: 0.8rem;
color: var(--text, #fff);
}
.category-pill__count {
background: var(--accent, #3b82f6);
color: white;
padding: 0.125rem 0.5rem;
border-radius: 10px;
font-size: 0.7rem;
font-weight: 600;
}
/* Recent Reports Table */
.recent-table {
width: 100%;
border-collapse: collapse;
}
.recent-table th,
.recent-table td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid var(--border, #333);
}
.recent-table th {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted, #888);
font-weight: 600;
}
.recent-table td {
font-size: 0.875rem;
color: var(--text, #fff);
}
.recent-table tr:hover td {
background: rgba(255, 255, 255, 0.03);
}
/* Navigation Links */
.dashboard-nav {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.dashboard-nav__link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.25rem;
background: var(--surface, #1a1a2e);
border: 1px solid var(--border, #333);
border-radius: 8px;
color: var(--text, #fff);
text-decoration: none;
font-size: 0.875rem;
transition: all 0.2s;
}
.dashboard-nav__link:hover {
border-color: var(--accent, #3b82f6);
background: rgba(59, 130, 246, 0.1);
}
.section-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--text, #fff);
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.two-column {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 1.5rem;
margin-bottom: 2rem;
}
@media (max-width: 1024px) {
.two-column {
grid-template-columns: 1fr;
}
}
</style>
<div class="dashboard-nav">
<a href="milldata.php" class="dashboard-nav__link">
📊 Data Viewer
</a>
<a href="milldata-compare.php" class="dashboard-nav__link">
🔀 Compare Mills
</a>
</div>
<?php if ($connectionError): ?>
<div class="kpi-card" style="border-color: #ef4444;">
<div class="kpi-card__label">Connection Error</div>
<div class="kpi-card__value" style="font-size: 1rem;">Unable to connect to the mill data database.</div>
</div>
<?php else: ?>
<!-- KPI Cards -->
<div class="dashboard-grid">
<div class="kpi-card kpi-card--accent">
<div class="kpi-card__label">Total Reports</div>
<div class="kpi-card__value"><?php echo number_format($stats['TotalReports'] ?? 0); ?></div>
<div class="kpi-card__sub">Across <?php echo number_format($stats['TotalFiles'] ?? 0); ?> files</div>
</div>
<div class="kpi-card kpi-card--success">
<div class="kpi-card__label">Mills Tracked</div>
<div class="kpi-card__value"><?php echo number_format($stats['TotalMills'] ?? 0); ?></div>
<div class="kpi-card__sub">Active production units</div>
</div>
<div class="kpi-card kpi-card--warning">
<div class="kpi-card__label">Total Metrics</div>
<div class="kpi-card__value"><?php echo number_format($stats['TotalMetrics'] ?? 0); ?></div>
<div class="kpi-card__sub">Data points captured</div>
</div>
<div class="kpi-card">
<div class="kpi-card__label">Date Range</div>
<div class="kpi-card__value" style="font-size: 1.25rem;">
<?php echo formatDate($stats['EarliestDate']); ?> - <?php echo formatDate($stats['LatestDate']); ?>
</div>
<div class="kpi-card__sub">Report coverage period</div>
</div>
</div>
<!-- Mill Performance Cards -->
<?php if (!empty($millPerformance)): ?>
<h2 class="section-title">📈 Latest Mill Performance</h2>
<div class="mill-cards">
<?php foreach ($millPerformance as $mill): ?>
<?php
$cardClass = 'mill-card--default';
$millCode = $mill['MillName'];
$millDisplayName = getMillDisplayName($millNameLookup, $millCode);
if (stripos($millCode, 'east') !== false) $cardClass = 'mill-card--east';
if (stripos($millCode, 'west') !== false) $cardClass = 'mill-card--west';
?>
<div class="mill-card <?php echo $cardClass; ?>">
<div class="mill-card__header">
<div class="mill-card__name"><?php echo htmlspecialchars($millDisplayName); ?></div>
<div class="mill-card__date">
Week of<br>
<?php echo formatDate($mill['BeginningDate']); ?>
</div>
</div>
<div class="mill-card__stats">
<div class="mill-stat">
<div class="mill-stat__value">
<?php echo $mill['TCGround'] !== null ? number_format($mill['TCGround'], 0) : '-'; ?>
</div>
<div class="mill-stat__label">TC Ground</div>
</div>
<div class="mill-stat">
<div class="mill-stat__value">
<?php echo $mill['HoursRun'] !== null ? number_format($mill['HoursRun'], 1) : '-'; ?>
</div>
<div class="mill-stat__label">Hours Run</div>
</div>
<div class="mill-stat">
<div class="mill-stat__value">
<?php echo $mill['LostTime'] !== null ? number_format($mill['LostTime'], 1) : '-'; ?>
</div>
<div class="mill-stat__label">Lost Time</div>
</div>
<div class="mill-stat">
<div class="mill-stat__value">
<?php echo $mill['TCPerHour'] !== null ? number_format($mill['TCPerHour'], 1) : '-'; ?>
</div>
<div class="mill-stat__label">TC/Hour</div>
</div>
</div>
<?php if ($mill['TCToDate'] !== null): ?>
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border, #333); text-align: center;">
<span style="color: var(--text-muted, #888); font-size: 0.75rem;">Season Total:</span>
<span style="font-weight: 700; font-size: 1.25rem; margin-left: 0.5rem;">
<?php echo number_format($mill['TCToDate'], 0); ?> TC
</span>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Charts Section -->
<div class="two-column">
<div class="chart-container">
<div class="chart-container__title">📊 Weekly TC Ground Trend</div>
<div class="chart-wrapper">
<canvas id="tcTrendChart"></canvas>
</div>
</div>
<div class="chart-container">
<div class="chart-container__title">📁 Data Categories</div>
<?php if (!empty($categoryBreakdown)): ?>
<div class="category-list">
<?php foreach ($categoryBreakdown as $cat): ?>
<div class="category-pill">
<?php echo htmlspecialchars($cat['Category']); ?>
<span class="category-pill__count"><?php echo $cat['MetricCount']; ?></span>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p style="color: var(--text-muted, #888);">No category data available.</p>
<?php endif; ?>
</div>
</div>
<!-- Recent Reports -->
<div class="chart-container">
<div class="chart-container__title">🕐 Recent Reports</div>
<?php if (!empty($recentReports)): ?>
<div style="overflow-x: auto;">
<table class="recent-table">
<thead>
<tr>
<th>Mill</th>
<th>Source File</th>
<th>Begin Date</th>
<th>End Date</th>
<th>Crop Days</th>
<th>Processed</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($recentReports as $report): ?>
<tr>
<td><strong><?php echo htmlspecialchars(getMillDisplayName($millNameLookup, $report['MillName'])); ?></strong></td>
<td style="font-size: 0.75rem; color: var(--text-muted, #888);">
<?php echo htmlspecialchars($report['SourceFileName']); ?>
</td>
<td><?php echo formatDate($report['BeginningDate']); ?></td>
<td><?php echo formatDate($report['EndingDate']); ?></td>
<td><?php echo $report['CropDays'] ?? '-'; ?></td>
<td style="font-size: 0.75rem; color: var(--text-muted, #888);">
<?php echo formatDate($report['ProcessedAt']); ?>
</td>
<td>
<a href="milldata.php?report=<?php echo $report['ReportId']; ?>&file=<?php echo urlencode($report['SourceFileName']); ?>"
style="color: var(--accent, #3b82f6); text-decoration: none; font-size: 0.8rem;">
View →
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<p style="color: var(--text-muted, #888);">No reports available.</p>
<?php endif; ?>
</div>
<?php endif; ?>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const chartLabels = <?php echo json_encode($chartLabels); ?>;
const chartDatasets = <?php echo json_encode($chartDatasets); ?>;
if (chartLabels.length > 0 && chartDatasets.length > 0) {
const ctx = document.getElementById('tcTrendChart');
if (ctx) {
new Chart(ctx, {
type: 'line',
data: {
labels: chartLabels,
datasets: chartDatasets
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index'
},
plugins: {
legend: {
position: 'top',
labels: {
color: '#888',
usePointStyle: true,
padding: 20
}
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleColor: '#fff',
bodyColor: '#fff',
borderColor: '#333',
borderWidth: 1,
padding: 12,
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += new Intl.NumberFormat().format(context.parsed.y) + ' TC';
}
return label;
}
}
}
},
scales: {
x: {
grid: {
color: 'rgba(255, 255, 255, 0.05)'
},
ticks: {
color: '#888'
}
},
y: {
beginAtZero: true,
grid: {
color: 'rgba(255, 255, 255, 0.05)'
},
ticks: {
color: '#888',
callback: function(value) {
return new Intl.NumberFormat().format(value);
}
}
}
}
}
});
}
}
});
</script>
<?php require __DIR__ . '/../includes/layout/footer.php'; ?>

View File

@@ -0,0 +1,706 @@
<?php // phpcs:ignoreFile
require __DIR__ . '/../session.php';
require __DIR__ . '/../userAccess.php';
require __DIR__ . '/../includes/millnames.php';
// ============================================================================
// Database Configuration
// ============================================================================
$config = [
'server' => '192.168.0.16',
'database' => 'lasucaai',
'username' => 'lasucaai',
'password' => 'is413#dfslw',
];
// ============================================================================
// Database Connection
// ============================================================================
function getMillDataConnection($config) {
$connectionOptions = [
"Database" => $config['database'],
"Uid" => $config['username'],
"PWD" => $config['password'],
"TrustServerCertificate" => true,
"Encrypt" => false,
];
$conn = sqlsrv_connect($config['server'], $connectionOptions);
if ($conn === false) {
return null;
}
return $conn;
}
// ============================================================================
// Data Queries
// ============================================================================
function getMillReports($conn) {
$sql = "SELECT ReportId, SourceFileName, MillName, ReportTitle,
BeginningDate, EndingDate, CropDays, ProcessedAt
FROM dbo.MillDataReports
ORDER BY ProcessedAt DESC";
$stmt = sqlsrv_query($conn, $sql);
$reports = [];
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$reports[] = $row;
}
return $reports;
}
function getMillSourceFiles($conn) {
$sql = "SELECT DISTINCT SourceFileName,
MIN(BeginningDate) as BeginningDate,
MAX(EndingDate) as EndingDate,
MAX(ProcessedAt) as ProcessedAt
FROM dbo.MillDataReports
GROUP BY SourceFileName
ORDER BY MAX(ProcessedAt) DESC";
$stmt = sqlsrv_query($conn, $sql);
$files = [];
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$files[] = $row;
}
return $files;
}
function getMillsForSourceFile($conn, $sourceFileName) {
$sql = "SELECT ReportId, MillName
FROM dbo.MillDataReports
WHERE SourceFileName = ?
ORDER BY MillName";
$stmt = sqlsrv_query($conn, $sql, [$sourceFileName]);
if ($stmt === false) {
return [];
}
$mills = [];
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$mills[] = $row;
}
return $mills;
}
function getMillMetrics($conn, $reportId, $search = '', $category = '') {
$params = [$reportId];
$sql = "SELECT MetricId, ItemNumber, MetricName, RunValue, RunValueNumeric,
ToDateValue, ToDateValueNumeric, Unit, Category
FROM dbo.MillDataMetrics
WHERE ReportId = ?";
if (!empty($search)) {
$sql .= " AND MetricName LIKE ?";
$params[] = '%' . $search . '%';
}
if (!empty($category)) {
$sql .= " AND Category = ?";
$params[] = $category;
}
$sql .= " ORDER BY MetricId";
$stmt = sqlsrv_query($conn, $sql, $params);
$metrics = [];
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$metrics[] = $row;
}
return $metrics;
}
function getMillCategories($conn) {
$sql = "SELECT DISTINCT Category FROM dbo.MillDataMetrics WHERE Category IS NOT NULL ORDER BY Category";
$stmt = sqlsrv_query($conn, $sql);
$categories = [];
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$categories[] = $row['Category'];
}
return $categories;
}
function getMillReportStats($conn) {
$sql = "SELECT
COUNT(DISTINCT r.ReportId) as TotalReports,
COUNT(m.MetricId) as TotalMetrics,
MIN(r.BeginningDate) as EarliestDate,
MAX(r.EndingDate) as LatestDate
FROM dbo.MillDataReports r
LEFT JOIN dbo.MillDataMetrics m ON r.ReportId = m.ReportId";
$stmt = sqlsrv_query($conn, $sql);
return sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
}
function formatMillDate($date) {
if ($date instanceof DateTime) {
return $date->format('m/d/Y');
}
return $date ?? '-';
}
// ============================================================================
// Main Logic
// ============================================================================
$conn = getMillDataConnection($config);
$connectionError = $conn === null;
$reports = [];
$sourceFiles = [];
$millsForFile = [];
$categories = [];
$stats = ['TotalReports' => 0, 'TotalMetrics' => 0];
$metrics = [];
$selectedReport = null;
$selectedFile = '';
$selectedFileInfo = null;
$millNameLookup = [];
if (!$connectionError) {
// Load mill name mappings
$millNameLookup = getMillNames($conn);
$selectedFile = isset($_GET['file']) ? trim($_GET['file']) : '';
$selectedReportId = isset($_GET['report']) ? intval($_GET['report']) : 0;
$searchTerm = isset($_GET['search']) ? trim($_GET['search']) : '';
$selectedCategory = isset($_GET['category']) ? trim($_GET['category']) : '';
$reports = getMillReports($conn);
$sourceFiles = getMillSourceFiles($conn);
$categories = getMillCategories($conn);
$stats = getMillReportStats($conn);
// Default to first file if none selected
if (empty($selectedFile) && !empty($sourceFiles)) {
$selectedFile = $sourceFiles[0]['SourceFileName'];
}
// Get mills for selected file
$millsForFile = !empty($selectedFile) ? getMillsForSourceFile($conn, $selectedFile) : [];
// Default to first mill if no report selected
if ($selectedReportId === 0 && !empty($millsForFile)) {
$selectedReportId = $millsForFile[0]['ReportId'];
}
// Validate report belongs to selected file
$validReport = false;
foreach ($millsForFile as $m) {
if ($m['ReportId'] == $selectedReportId) {
$validReport = true;
break;
}
}
if (!$validReport && !empty($millsForFile)) {
$selectedReportId = $millsForFile[0]['ReportId'];
}
$metrics = $selectedReportId ? getMillMetrics($conn, $selectedReportId, $searchTerm, $selectedCategory) : [];
// Find selected report details
foreach ($reports as $r) {
if ($r['ReportId'] == $selectedReportId) {
$selectedReport = $r;
break;
}
}
// Find selected file details
foreach ($sourceFiles as $f) {
if ($f['SourceFileName'] == $selectedFile) {
$selectedFileInfo = $f;
break;
}
}
sqlsrv_close($conn);
} else {
$selectedReportId = 0;
$searchTerm = '';
$selectedCategory = '';
}
// ============================================================================
// Page Layout
// ============================================================================
$pageTitle = 'Mill Data Viewer';
$pageSubtitle = 'Sugar Mill Production Report Data';
$pageDescription = 'View extracted mill production data from daily reports.';
$layoutWithoutSidebar = true;
$layoutReturnUrl = '../overview.php';
$layoutReturnLabel = 'Back to overview';
$assetBasePath = '../';
require __DIR__ . '/../includes/layout/header.php';
?>
<style>
.milldata-stats {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 1.5rem;
align-items: center;
}
.milldata-stat {
background: var(--surface);
padding: 1rem;
border-radius: 8px;
text-align: center;
border: 1px solid var(--border);
min-width: 100px;
}
.milldata-stat .value {
font-size: 1.5rem;
font-weight: 600;
color: var(--accent);
}
.milldata-stat .label {
font-size: 0.8rem;
color: var(--text-muted);
}
.compare-mills-btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.6rem 1rem;
background: var(--accent);
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 500;
transition: background 0.2s, transform 0.1s;
margin-left: auto;
}
.compare-mills-btn:hover {
background: var(--accent-hover, #1976d2);
transform: translateY(-1px);
}
.milldata-layout {
display: grid;
grid-template-columns: 280px 1fr;
gap: 1.5rem;
}
@media (max-width: 900px) {
.milldata-layout {
grid-template-columns: 1fr;
}
}
.milldata-sidebar {
background: var(--surface);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--border);
height: fit-content;
max-height: 500px;
overflow-y: auto;
}
.milldata-sidebar h2 {
font-size: 0.85rem;
color: var(--text-muted);
margin-bottom: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.report-list {
list-style: none;
padding: 0;
margin: 0;
}
.report-list li {
margin-bottom: 0.5rem;
}
.report-list a {
display: block;
padding: 0.6rem 0.75rem;
background: var(--surface-alt);
border-radius: 6px;
text-decoration: none;
color: var(--text);
transition: all 0.2s;
border-left: 3px solid transparent;
font-size: 0.9rem;
}
.report-list a:hover {
background: var(--hover);
border-left-color: var(--accent);
}
.report-list a.active {
background: var(--hover);
border-left-color: var(--accent);
font-weight: 500;
}
.report-list .date {
font-size: 0.75rem;
color: var(--text-muted);
}
.milldata-main {
background: var(--surface);
border-radius: 8px;
padding: 1.25rem;
border: 1px solid var(--border);
}
.milldata-report-header {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border);
}
.milldata-report-header h2 {
color: var(--accent);
margin-bottom: 0.5rem;
font-size: 1.1rem;
}
.report-meta {
display: flex;
gap: 1.25rem;
flex-wrap: wrap;
font-size: 0.85rem;
color: var(--text-muted);
}
.milldata-filters {
display: flex;
gap: 0.75rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.milldata-filters input,
.milldata-filters select {
padding: 0.5rem 0.75rem;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 0.85rem;
background: var(--surface);
color: var(--text);
}
.milldata-filters input {
flex: 1;
min-width: 180px;
}
.milldata-filters button {
padding: 0.5rem 1rem;
background: var(--accent);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
}
.milldata-filters button:hover {
opacity: 0.9;
}
.milldata-filters .clear-link {
padding: 0.5rem;
color: var(--text-muted);
text-decoration: none;
font-size: 0.85rem;
}
.results-count {
font-size: 0.8rem;
color: var(--text-muted);
margin-bottom: 0.75rem;
}
.milldata-table-wrapper {
border: 1px solid var(--border);
border-radius: 6px;
overflow: hidden;
}
.milldata-table-header {
background: var(--surface-alt);
}
.milldata-table-header table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: 0.85rem;
table-layout: fixed;
}
.milldata-table-header th {
background: var(--surface-alt);
padding: 0.75rem 0.6rem;
text-align: center;
font-weight: 600;
color: var(--text-muted);
border-bottom: 2px solid var(--border);
}
.milldata-table-header th:nth-child(1) { width: 60px; text-align: left; }
.milldata-table-header th:nth-child(2) { text-align: left; }
.milldata-table-header th:nth-child(3) { width: 100px; }
.milldata-table-header th:nth-child(4) { width: 100px; }
.milldata-table-header th:nth-child(5) { width: 120px; }
.milldata-table-container {
max-height: 450px;
overflow-y: auto;
}
.milldata-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: 0.85rem;
table-layout: fixed;
}
.milldata-table td:nth-child(1) { width: 60px; }
.milldata-table td:nth-child(3) { width: 100px; }
.milldata-table td:nth-child(4) { width: 100px; }
.milldata-table td:nth-child(5) { width: 120px; }
.milldata-table td {
padding: 0.6rem;
border-bottom: 1px solid var(--border);
}
.milldata-table tr:hover {
background: var(--hover);
}
.item-num {
font-weight: 600;
color: var(--accent);
white-space: nowrap;
}
.metric-name {
max-width: 280px;
}
.value-cell {
text-align: right;
font-family: 'Consolas', 'Monaco', monospace;
white-space: nowrap;
}
.value-cell.negative {
color: #ef5350;
}
.category-badge {
display: inline-block;
padding: 2px 8px;
background: var(--hover);
color: var(--accent);
border-radius: 12px;
font-size: 0.7rem;
white-space: nowrap;
}
.no-data {
text-align: center;
padding: 2rem;
color: var(--text-muted);
}
.connection-error {
background: #ffebee;
color: #c62828;
padding: 1rem;
border-radius: 8px;
text-align: center;
margin-bottom: 1rem;
}
</style>
<div class="app-content">
<section class="data-panel">
<?php if ($connectionError): ?>
<div class="connection-error">
Unable to connect to the database. Please check the connection settings.
</div>
<?php endif; ?>
<div class="milldata-stats">
<div class="milldata-stat">
<div class="value"><?= $stats['TotalReports'] ?? 0 ?></div>
<div class="label">Mills</div>
</div>
<div class="milldata-stat">
<div class="value"><?= number_format($stats['TotalMetrics'] ?? 0) ?></div>
<div class="label">Metrics</div>
</div>
<div class="milldata-stat">
<div class="value"><?= formatMillDate($stats['EarliestDate'] ?? null) ?></div>
<div class="label">Earliest</div>
</div>
<div class="milldata-stat">
<div class="value"><?= formatMillDate($stats['LatestDate'] ?? null) ?></div>
<div class="label">Latest</div>
</div>
<a href="milldata-compare.php" class="compare-mills-btn">🔀 Compare Mills</a>
<a href="milldata-dashboard.php" class="compare-mills-btn" style="background: #10b981;">📊 Dashboard</a>
</div>
<div class="milldata-layout">
<aside class="milldata-sidebar">
<h2>Reports</h2>
<ul class="report-list">
<?php foreach ($sourceFiles as $file): ?>
<li>
<a href="?file=<?= urlencode($file['SourceFileName']) ?>"
class="<?= $file['SourceFileName'] == $selectedFile ? 'active' : '' ?>">
<?= htmlspecialchars($file['SourceFileName']) ?>
<div class="date">
<?= formatMillDate($file['BeginningDate']) ?> - <?= formatMillDate($file['EndingDate']) ?>
</div>
</a>
</li>
<?php endforeach; ?>
<?php if (empty($sourceFiles)): ?>
<li class="no-data">No reports found</li>
<?php endif; ?>
</ul>
</aside>
<main class="milldata-main">
<?php if ($selectedReport): ?>
<div class="milldata-report-header">
<h2><?= htmlspecialchars($selectedReport['ReportTitle'] ?? $selectedReport['SourceFileName']) ?></h2>
<div class="report-meta">
<span>📅 <?= formatMillDate($selectedReport['BeginningDate']) ?> - <?= formatMillDate($selectedReport['EndingDate']) ?></span>
<span>🏭
<select onchange="window.location.href='?file=<?= urlencode($selectedFile) ?>&report='+this.value" style="padding: 4px 8px; border-radius: 4px; border: 1px solid var(--border); background: var(--surface); color: var(--text);">
<?php foreach ($millsForFile as $mill):
$displayName = getMillDisplayName($millNameLookup, $mill['MillName']);
?>
<option value="<?= $mill['ReportId'] ?>" <?= $mill['ReportId'] == $selectedReportId ? 'selected' : '' ?>>
<?= htmlspecialchars($displayName) ?>
</option>
<?php endforeach; ?>
</select>
</span>
</div>
</div>
<form class="milldata-filters" method="get">
<input type="hidden" name="file" value="<?= htmlspecialchars($selectedFile) ?>">
<input type="hidden" name="report" value="<?= $selectedReportId ?>">
<input type="text" name="search" placeholder="Search metrics..."
value="<?= htmlspecialchars($searchTerm) ?>">
<select name="category">
<option value="">All Categories</option>
<?php foreach ($categories as $cat): ?>
<option value="<?= htmlspecialchars($cat) ?>"
<?= $cat === $selectedCategory ? 'selected' : '' ?>>
<?= htmlspecialchars($cat) ?>
</option>
<?php endforeach; ?>
</select>
<button type="submit">Filter</button>
<?php if ($searchTerm || $selectedCategory): ?>
<a href="?file=<?= urlencode($selectedFile) ?>&report=<?= $selectedReportId ?>" class="clear-link">Clear</a>
<?php endif; ?>
</form>
<div class="results-count">
Showing <?= count($metrics) ?> metrics
<?php if ($searchTerm): ?>
matching "<?= htmlspecialchars($searchTerm) ?>"
<?php endif; ?>
</div>
<div class="milldata-table-wrapper">
<div class="milldata-table-header">
<table>
<thead>
<tr>
<th>Item</th>
<th>Metric</th>
<th>RUN</th>
<th>TO DATE</th>
<th>Category</th>
</tr>
</thead>
</table>
</div>
<div class="milldata-table-container">
<table class="milldata-table">
<tbody>
<?php foreach ($metrics as $m): ?>
<tr>
<td class="item-num"><?= htmlspecialchars($m['ItemNumber'] ?? '') ?></td>
<td class="metric-name"><?= htmlspecialchars($m['MetricName'] ?? '') ?></td>
<td class="value-cell <?= ($m['RunValueNumeric'] ?? 0) < 0 ? 'negative' : '' ?>">
<?= htmlspecialchars($m['RunValue'] ?? '') ?>
</td>
<td class="value-cell <?= ($m['ToDateValueNumeric'] ?? 0) < 0 ? 'negative' : '' ?>">
<?= htmlspecialchars($m['ToDateValue'] ?? '') ?>
</td>
<td>
<?php if ($m['Category']): ?>
<span class="category-badge"><?= htmlspecialchars($m['Category']) ?></span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($metrics)): ?>
<tr>
<td colspan="5" class="no-data">No metrics found</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php else: ?>
<div class="no-data">
<p>Select a report from the sidebar to view metrics</p>
</div>
<?php endif; ?>
</main>
</div>
</section>
</div>
<?php require __DIR__ . '/../includes/layout/footer.php'; ?>

View File

@@ -0,0 +1,152 @@
<?php // phpcs:ignoreFile
/**
* Setup script to create mill_names lookup table
* Run this once via browser to create the table and populate with existing mill names
* Access via: http://server/reports/setup_mill_names.php
*/
require __DIR__ . '/../session.php';
require __DIR__ . '/../userAccess.php';
// Only allow admin or controls
if ($_SESSION['SESS_MEMBER_LEVEL'] < 4) {
header("Location: ../access-denied.php");
exit();
}
$config = [
'server' => '192.168.0.16',
'database' => 'lasucaai',
'username' => 'lasucaai',
'password' => 'is413#dfslw',
];
$connectionOptions = [
"Database" => $config['database'],
"Uid" => $config['username'],
"PWD" => $config['password'],
"TrustServerCertificate" => true,
"Encrypt" => false,
];
$conn = sqlsrv_connect($config['server'], $connectionOptions);
if ($conn === false) {
die("Connection failed: " . print_r(sqlsrv_errors(), true));
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Mill Names Setup</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background: #1a1a2e; color: #eee; }
table { border-collapse: collapse; margin: 10px 0; }
th, td { border: 1px solid #444; padding: 8px 12px; text-align: left; }
th { background: #333; }
.success { color: #4caf50; }
.error { color: #f44336; }
code { background: #333; padding: 2px 6px; border-radius: 3px; }
a { color: #64b5f6; }
</style>
</head>
<body>
<?php
echo "<h2>Mill Names Setup</h2>";
// Step 1: Create the mill_names table
$createTableSql = "
IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='mill_names' AND xtype='U')
CREATE TABLE mill_names (
mill_id INT IDENTITY(1,1) PRIMARY KEY,
mill_code NVARCHAR(100) NOT NULL UNIQUE,
display_name NVARCHAR(200) NOT NULL,
sort_order INT DEFAULT 0,
is_active BIT DEFAULT 1,
created_at DATETIME DEFAULT GETDATE()
)
";
$result = sqlsrv_query($conn, $createTableSql);
if ($result === false) {
echo "<p style='color:red'>Error creating table: " . print_r(sqlsrv_errors(), true) . "</p>";
} else {
echo "<p style='color:green'>✓ mill_names table created (or already exists)</p>";
}
// Step 2: Get existing mill names from reports
$existingMillsSql = "SELECT DISTINCT MillName FROM dbo.MillDataReports ORDER BY MillName";
$existingResult = sqlsrv_query($conn, $existingMillsSql);
$existingMills = [];
if ($existingResult) {
while ($row = sqlsrv_fetch_array($existingResult, SQLSRV_FETCH_ASSOC)) {
$existingMills[] = $row['MillName'];
}
}
echo "<h3>Existing Mill Codes in Reports:</h3>";
echo "<ul>";
foreach ($existingMills as $mill) {
echo "<li>" . htmlspecialchars($mill) . "</li>";
}
echo "</ul>";
// Step 3: Insert existing mills into lookup table (if not already there)
$insertCount = 0;
foreach ($existingMills as $index => $millCode) {
$checkSql = "SELECT mill_id FROM mill_names WHERE mill_code = ?";
$checkResult = sqlsrv_query($conn, $checkSql, [$millCode]);
if ($checkResult && !sqlsrv_fetch_array($checkResult)) {
// Not in table yet, insert with mill_code as default display_name
$insertSql = "INSERT INTO mill_names (mill_code, display_name, sort_order) VALUES (?, ?, ?)";
$insertResult = sqlsrv_query($conn, $insertSql, [$millCode, $millCode, $index + 1]);
if ($insertResult) {
$insertCount++;
}
}
}
if ($insertCount > 0) {
echo "<p class='success'>✓ Inserted $insertCount new mill codes</p>";
} else {
echo "<p>No new mill codes to insert</p>";
}
// Step 4: Show current table contents
echo "<h3>Current mill_names Table:</h3>";
$selectSql = "SELECT * FROM mill_names ORDER BY sort_order";
$selectResult = sqlsrv_query($conn, $selectSql);
if ($selectResult) {
echo "<table>";
echo "<tr><th>ID</th><th>Mill Code</th><th>Display Name</th><th>Sort Order</th><th>Active</th></tr>";
while ($row = sqlsrv_fetch_array($selectResult, SQLSRV_FETCH_ASSOC)) {
echo "<tr>";
echo "<td>" . $row['mill_id'] . "</td>";
echo "<td>" . htmlspecialchars($row['mill_code']) . "</td>";
echo "<td>" . htmlspecialchars($row['display_name']) . "</td>";
echo "<td>" . $row['sort_order'] . "</td>";
echo "<td>" . ($row['is_active'] ? 'Yes' : 'No') . "</td>";
echo "</tr>";
}
echo "</table>";
}
sqlsrv_close($conn);
echo "<hr>";
echo "<p><strong>Next steps:</strong></p>";
echo "<ol>";
echo "<li>Go to <a href='mill-names-admin.php'>Mill Names Admin</a> to update display names</li>";
echo "<li>Or run SQL like: <code>UPDATE mill_names SET display_name = 'East Mill' WHERE mill_code = 'EastMill'</code></li>";
echo "<li>Delete this setup script when done</li>";
echo "</ol>";
echo "<p><a href='mill-names-admin.php'>→ Open Mill Names Admin</a></p>";
?>
</body>
</html>";
echo "</ol>";
?>