Files
controls-web/controls-rework/reports/milldata-dashboard.php
2026-02-17 09:29:34 -06:00

836 lines
25 KiB
PHP

<?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'; ?>