Folder reorganize 1
This commit is contained in:
171
reports/api/data.php
Normal file
171
reports/api/data.php
Normal 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);
|
||||
42
reports/api/list-tags.php
Normal file
42
reports/api/list-tags.php
Normal 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);
|
||||
165
reports/config.php
Normal file
165
reports/config.php
Normal 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];
|
||||
}
|
||||
512
reports/index.php
Normal file
512
reports/index.php
Normal 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'; ?>
|
||||
373
reports/mill-names-admin.php
Normal file
373
reports/mill-names-admin.php
Normal 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';
|
||||
?>
|
||||
546
reports/milldata-compare.php
Normal file
546
reports/milldata-compare.php
Normal 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'; ?>
|
||||
835
reports/milldata-dashboard.php
Normal file
835
reports/milldata-dashboard.php
Normal 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'; ?>
|
||||
706
reports/milldata.php
Normal file
706
reports/milldata.php
Normal 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'; ?>
|
||||
152
reports/setup_mill_names.php
Normal file
152
reports/setup_mill_names.php
Normal 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>";
|
||||
?>
|
||||
Reference in New Issue
Block a user