1635 lines
60 KiB
PHP
1635 lines
60 KiB
PHP
<?php include "session.php";
|
|
include "userAccess.php"; ?>
|
|
<?php
|
|
// Database connection
|
|
$servername = "192.168.0.13\\SQLEXPRESS";
|
|
$username = "opce";
|
|
$password = "opcelasuca";
|
|
$dbname = "history";
|
|
|
|
try {
|
|
$pdo = new PDO(
|
|
"sqlsrv:Server=$servername;Database=$dbname",
|
|
$username,
|
|
$password,
|
|
[
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
|
|
]
|
|
);
|
|
} catch (PDOException $e) {
|
|
die("Connection failed: " . $e->getMessage());
|
|
}
|
|
|
|
// Handle AJAX request for autocomplete
|
|
if (isset($_GET['action']) && $_GET['action'] === 'autocomplete') {
|
|
$query = isset($_GET['query']) ? trim($_GET['query']) : '';
|
|
$suggestions = [];
|
|
|
|
if (!empty($query)) {
|
|
try {
|
|
// Make sure we're using the same column name as the main query
|
|
$stmt = $pdo->prepare("
|
|
SELECT TOP 20 name
|
|
FROM dbo.id_names
|
|
WHERE name LIKE :query
|
|
ORDER BY name
|
|
");
|
|
$stmt->execute([':query' => '%' . $query . '%']);
|
|
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
foreach ($results as $row) {
|
|
$suggestions[] = $row['name'];
|
|
}
|
|
} catch (PDOException $e) {
|
|
// Handle error silently for AJAX
|
|
error_log("Autocomplete error: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
header('Content-Type: application/json');
|
|
echo json_encode($suggestions);
|
|
exit;
|
|
}
|
|
|
|
// Handle search parameters
|
|
$searchType = isset($_GET['search_type']) ? $_GET['search_type'] : 'single_name';
|
|
$searchName = isset($_GET['name']) ? trim($_GET['name']) : '';
|
|
|
|
// Handle multiple tag dropdowns
|
|
$selectedTags = [];
|
|
for ($i = 1; $i <= 5; $i++) {
|
|
$tagParam = "tag{$i}";
|
|
if (isset($_GET[$tagParam]) && !empty(trim($_GET[$tagParam]))) {
|
|
$selectedTags[] = trim($_GET[$tagParam]);
|
|
}
|
|
}
|
|
$multipleNames = implode(', ', $selectedTags); // For backward compatibility
|
|
|
|
$startDate = isset($_GET['start_date']) ? $_GET['start_date'] : '';
|
|
$endDate = isset($_GET['end_date']) ? $_GET['end_date'] : '';
|
|
|
|
// Normalize HTML datetime-local (YYYY-MM-DDTHH:MM[/SS]) to SQL Server format
|
|
$normalizeDateTime = function ($value) {
|
|
if (empty($value)) {
|
|
return '';
|
|
}
|
|
|
|
$patterns = [
|
|
'Y-m-d\TH:i:s',
|
|
'Y-m-d\TH:i'
|
|
];
|
|
|
|
foreach ($patterns as $pattern) {
|
|
$dt = DateTime::createFromFormat($pattern, $value);
|
|
if ($dt instanceof DateTime) {
|
|
return $dt->format('Y-m-d H:i:s');
|
|
}
|
|
}
|
|
|
|
// Fallback: let SQL Server attempt conversion unchanged
|
|
return $value;
|
|
};
|
|
|
|
$startDate = $normalizeDateTime($startDate);
|
|
$endDate = $normalizeDateTime($endDate);
|
|
|
|
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 100;
|
|
$showTrend = isset($_GET['show_trend']) ? true : false;
|
|
|
|
// Change from fixed value to variable with default
|
|
$timeInterval = isset($_GET['time_interval']) ? (int)$_GET['time_interval'] : 30; // Default 30 seconds
|
|
|
|
// Add validation
|
|
if ($timeInterval < 1) {
|
|
$timeInterval = 1; // Minimum 1 second
|
|
}
|
|
|
|
$results = [];
|
|
$searchTitle = '';
|
|
$totalCount = 0;
|
|
$tagNames = [];
|
|
$organizedResults = [];
|
|
|
|
// Only search if required fields are provided
|
|
$canSearch = false;
|
|
switch ($searchType) {
|
|
case 'single_name':
|
|
$canSearch = !empty($searchName);
|
|
break;
|
|
case 'multiple_names':
|
|
$canSearch = !empty($selectedTags); // Use selectedTags instead of multipleNames
|
|
break;
|
|
}
|
|
|
|
if ($canSearch) {
|
|
try {
|
|
// Base queries - no time interval filtering
|
|
$sql = "
|
|
SELECT
|
|
h.ID,
|
|
h.Value,
|
|
h.TimeStamp,
|
|
COALESCE(n.name, CONCAT('ID_', h.ID)) AS name
|
|
FROM dbo.historicaldata h
|
|
LEFT JOIN dbo.id_names n ON h.ID = n.idnumber
|
|
WHERE 1 = 1
|
|
";
|
|
$countSql = "
|
|
SELECT COUNT(*)
|
|
FROM dbo.historicaldata h
|
|
LEFT JOIN dbo.id_names n ON h.ID = n.idnumber
|
|
WHERE 1 = 1
|
|
";
|
|
$params = [];
|
|
|
|
// Build query based on search type
|
|
switch ($searchType) {
|
|
case 'single_name':
|
|
$sql .= " AND n.name = :name";
|
|
$countSql .= " AND n.name = :name";
|
|
$params[':name'] = $searchName;
|
|
$searchTitle = $searchName;
|
|
break;
|
|
|
|
case 'multiple_names':
|
|
if (!empty($selectedTags)) {
|
|
$placeholders = [];
|
|
for ($i = 0; $i < count($selectedTags); $i++) {
|
|
$placeholder = ':name' . $i;
|
|
$placeholders[] = $placeholder;
|
|
$params[$placeholder] = $selectedTags[$i];
|
|
}
|
|
$placeholderString = implode(',', $placeholders);
|
|
$sql .= " AND n.name IN ($placeholderString)";
|
|
$countSql .= " AND n.name IN ($placeholderString)";
|
|
$searchTitle = "Multiple Tags (" . count($selectedTags) . "): " . implode(', ', array_slice($selectedTags, 0, 3)) . (count($selectedTags) > 3 ? '...' : '');
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Add date filters
|
|
if (!empty($startDate)) {
|
|
$sql .= " AND h.TimeStamp >= :start_date";
|
|
$countSql .= " AND h.TimeStamp >= :start_date";
|
|
$params[':start_date'] = $startDate;
|
|
}
|
|
|
|
if (!empty($endDate)) {
|
|
$sql .= " AND h.TimeStamp <= :end_date";
|
|
$countSql .= " AND h.TimeStamp <= :end_date";
|
|
$params[':end_date'] = $endDate;
|
|
}
|
|
|
|
// Add time interval filtering
|
|
if ($timeInterval > 1) {
|
|
if ($timeInterval >= 60) {
|
|
// For minute intervals, align to minute boundaries
|
|
$minutes = $timeInterval / 60;
|
|
$sql .= "
|
|
AND DATEPART(MINUTE, h.TimeStamp) % :minute_interval = 0
|
|
AND DATEPART(SECOND, h.TimeStamp) = 0
|
|
";
|
|
$countSql .= "
|
|
AND DATEPART(MINUTE, h.TimeStamp) % :minute_interval = 0
|
|
AND DATEPART(SECOND, h.TimeStamp) = 0
|
|
";
|
|
$params[':minute_interval'] = (int)$minutes;
|
|
} else {
|
|
$sql .= " AND DATEPART(SECOND, h.TimeStamp) % :time_interval = 0";
|
|
$countSql .= " AND DATEPART(SECOND, h.TimeStamp) % :time_interval = 0";
|
|
$params[':time_interval'] = $timeInterval;
|
|
}
|
|
}
|
|
|
|
// Get total count
|
|
$countStmt = $pdo->prepare($countSql);
|
|
$countStmt->execute($params);
|
|
$totalCount = $countStmt->fetchColumn();
|
|
|
|
// Add ordering and limit
|
|
if ($showTrend) {
|
|
$sql .= "
|
|
ORDER BY h.TimeStamp ASC
|
|
OFFSET 0 ROWS FETCH NEXT :limit ROWS ONLY
|
|
";
|
|
} else {
|
|
$sql .= "
|
|
ORDER BY h.TimeStamp DESC
|
|
OFFSET 0 ROWS FETCH NEXT :limit ROWS ONLY
|
|
";
|
|
}
|
|
$params[':limit'] = $limit;
|
|
|
|
$stmt = $pdo->prepare($sql);
|
|
|
|
// Bind all parameters
|
|
foreach ($params as $key => $value) {
|
|
if ($key === ':limit') {
|
|
$stmt->bindValue($key, $value, PDO::PARAM_INT);
|
|
} else {
|
|
$stmt->bindValue($key, $value);
|
|
}
|
|
}
|
|
|
|
$stmt->execute();
|
|
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Organize results by timestamp
|
|
if (!empty($results)) {
|
|
foreach ($results as $row) {
|
|
$timestamp = $row['TimeStamp'];
|
|
$tagName = $row['name'];
|
|
$value = $row['Value'];
|
|
|
|
if (!isset($organizedResults[$timestamp])) {
|
|
$organizedResults[$timestamp] = ['timestamp' => $timestamp];
|
|
}
|
|
|
|
$organizedResults[$timestamp][$tagName] = $value;
|
|
|
|
if (!in_array($tagName, $tagNames)) {
|
|
$tagNames[] = $tagName;
|
|
}
|
|
}
|
|
|
|
// Sort timestamps
|
|
if ($showTrend && $searchType == 'single_name') {
|
|
ksort($organizedResults);
|
|
} else {
|
|
krsort($organizedResults);
|
|
}
|
|
}
|
|
} catch (PDOException $e) {
|
|
echo "<div style='color: red; padding: 10px; border: 1px solid red; margin: 10px 0;'>Query error: " . htmlspecialchars($e->getMessage()) . "</div>";
|
|
}
|
|
}
|
|
|
|
// Functions
|
|
function formatTimestamp($timestamp)
|
|
{
|
|
return date('Y-m-d H:i:s', strtotime($timestamp));
|
|
}
|
|
|
|
function formatValue($value)
|
|
{
|
|
if (is_numeric($value)) {
|
|
return number_format($value, 2);
|
|
}
|
|
return $value;
|
|
}
|
|
|
|
// Prepare chart data for single tag charts only
|
|
$chartData = [];
|
|
$chartLabels = [];
|
|
|
|
if (!empty($results) && $showTrend && $searchType == 'single_name') {
|
|
// Single tag chart data only
|
|
foreach ($results as $row) {
|
|
$chartLabels[] = date('M j, Y H:i', strtotime($row['TimeStamp']));
|
|
$chartData[] = is_numeric($row['Value']) ? (float)$row['Value'] : 0;
|
|
}
|
|
}
|
|
?>
|
|
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>LASUCA Controls - Historical Data Search & Trend</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
background: #2c3e50; /* Chart page background */
|
|
color: #fff;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
/* Chart-style header */
|
|
.dashboard-header {
|
|
background: #34495e;
|
|
padding: 20px;
|
|
text-align: center;
|
|
border-bottom: 3px solid #3498db;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
|
}
|
|
|
|
.dashboard-header h1 {
|
|
color: #3498db;
|
|
font-size: 2rem;
|
|
margin: 0;
|
|
}
|
|
|
|
.dashboard-header .subtitle {
|
|
color: #bdc3c7;
|
|
font-size: 1rem;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 30px;
|
|
}
|
|
|
|
/* Keep existing layout but update colors */
|
|
.search-form {
|
|
background: #ecf0f1; /* Keep light background for form */
|
|
color: #2c3e50;
|
|
border-radius: 5px;
|
|
box-shadow: 0 8px 25px rgba(0,0,0,0.3);
|
|
overflow: hidden;
|
|
margin: 0px 0px 10px 0px;
|
|
}
|
|
|
|
.search-type-tabs {
|
|
display: flex;
|
|
background: #34495e; /* Chart header color */
|
|
}
|
|
|
|
.tab {
|
|
flex: 1;
|
|
padding: 15px 20px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
color: #bdc3c7;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.tab.active {
|
|
background: #3498db; /* Chart accent color */
|
|
color: white;
|
|
}
|
|
|
|
.tab:hover:not(.active) {
|
|
background: #2c3e50;
|
|
color: #3498db;
|
|
}
|
|
|
|
.search-option {
|
|
display: none;
|
|
padding: 30px;
|
|
}
|
|
|
|
.search-option.active {
|
|
display: block;
|
|
}
|
|
|
|
.form-row {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
gap: 20px;
|
|
padding: 0px 20px 0px 20px;
|
|
}
|
|
|
|
.form-row label {
|
|
min-width: 140px;
|
|
font-weight: bold;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.form-row input,
|
|
.form-row select {
|
|
flex: 1;
|
|
padding: 12px 18px;
|
|
border: 2px solid #bdc3c7;
|
|
border-radius: 6px;
|
|
font-size: 0.9rem;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.form-row input:focus,
|
|
.form-row select:focus {
|
|
outline: none;
|
|
border-color: #3498db; /* Chart accent color */
|
|
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
|
}
|
|
|
|
button {
|
|
background: #3498db; /* Chart accent color */
|
|
color: white;
|
|
border: none;
|
|
padding: 12px 25px;
|
|
border-radius: 6px;
|
|
font-size: 1rem;
|
|
font-weight: bold;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
margin-right: 10px;
|
|
}
|
|
|
|
button:hover:not(:disabled) {
|
|
background: #2980b9;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 20px rgba(52, 152, 219, 0.3);
|
|
}
|
|
|
|
button[type="button"] {
|
|
background: #e74c3c;
|
|
}
|
|
|
|
button[type="button"]:hover {
|
|
background: #c0392b;
|
|
}
|
|
|
|
/* Keep existing autocomplete styling but update colors */
|
|
.autocomplete-container {
|
|
position: relative;
|
|
flex: 1;
|
|
}
|
|
|
|
.autocomplete-suggestions {
|
|
display: none;
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
background: white;
|
|
border: 2px solid #3498db; /* Chart accent color */
|
|
border-top: none;
|
|
border-radius: 0 0 6px 6px;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
z-index: 1000;
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
|
}
|
|
|
|
.autocomplete-suggestion:hover,
|
|
.autocomplete-suggestion.active {
|
|
background: #3498db; /* Chart accent color */
|
|
color: white;
|
|
}
|
|
|
|
/* Results styling with chart colors */
|
|
.results-info {
|
|
background: #ecf0f1;
|
|
color: #2c3e50;
|
|
padding: 20px 25px;
|
|
border-radius: 5px;
|
|
margin-bottom: 10px;
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
|
}
|
|
|
|
.results-count {
|
|
font-weight: bold;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.stats-panel {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
gap: 20px;
|
|
padding: 25px;
|
|
background: #ecf0f1;
|
|
border-radius: 15px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
|
}
|
|
|
|
.stat-box {
|
|
background: #fff;
|
|
padding: 25px;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
border: 2px solid #bdc3c7;
|
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
}
|
|
|
|
.stat-box:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 6px 15px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.8rem;
|
|
font-weight: bold;
|
|
color: #3498db; /* Chart accent color */
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.85rem;
|
|
color: #7f8c8d;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.chart-container {
|
|
background: #ecf0f1;
|
|
border-radius: 15px;
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
|
overflow: hidden;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.chart-container h3 {
|
|
background: #3498db; /* Chart header color */
|
|
color: white;
|
|
padding: 20px;
|
|
margin: 0 0 20px 0;
|
|
font-size: 1.3rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.chart-wrapper {
|
|
position: relative;
|
|
height: 400px;
|
|
margin: 25px;
|
|
background: white;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.export-buttons {
|
|
display: flex;
|
|
gap: 15px;
|
|
justify-content: center;
|
|
margin: 20px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.export-buttons button {
|
|
background: #27ae60;
|
|
}
|
|
|
|
.export-buttons button:hover {
|
|
background: #219a52;
|
|
}
|
|
|
|
.trend-controls {
|
|
display: flex;
|
|
gap: 25px;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
margin: 20px;
|
|
padding: 20px;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.table-scroll {
|
|
background: #ecf0f1;
|
|
border-radius: 5px;
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
|
overflow: hidden;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 0.9rem;
|
|
background: white;
|
|
}
|
|
|
|
th {
|
|
background: #34495e; /* Chart header color */
|
|
color: white;
|
|
padding: 18px 15px;
|
|
text-align: left;
|
|
font-weight: bold;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 10;
|
|
}
|
|
|
|
td {
|
|
padding: 15px 15px;
|
|
border-bottom: 1px solid #ecf0f1;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
tr:nth-child(even) {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
tr:hover {
|
|
background: rgba(52, 152, 219, 0.1); /* Chart color with transparency */
|
|
}
|
|
|
|
.no-results {
|
|
background: #ecf0f1;
|
|
color: #2c3e50;
|
|
padding: 50px;
|
|
text-align: center;
|
|
border-radius: 15px;
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
|
margin: 0 15px;
|
|
}
|
|
|
|
.no-results h3 {
|
|
color: #e74c3c;
|
|
margin-bottom: 15px;
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
/* Loading overlay with chart colors */
|
|
.loading-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(44, 62, 80, 0.95); /* Chart background with transparency */
|
|
display: none;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 9999;
|
|
}
|
|
|
|
.loading-container {
|
|
text-align: center;
|
|
color: white;
|
|
max-width: 400px;
|
|
padding: 50px;
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 60px;
|
|
height: 60px;
|
|
border: 4px solid #34495e;
|
|
border-top: 4px solid #3498db; /* Chart accent color */
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto 20px;
|
|
}
|
|
|
|
.loading-text {
|
|
font-size: 1.5rem;
|
|
font-weight: bold;
|
|
margin-bottom: 10px;
|
|
color: #3498db; /* Chart accent color */
|
|
}
|
|
|
|
.loading-subtext {
|
|
font-size: 1rem;
|
|
color: #bdc3c7;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.progress-bar {
|
|
width: 100%;
|
|
height: 8px;
|
|
background: #34495e;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: #3498db; /* Chart accent color */
|
|
width: 0%;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.help-text {
|
|
font-size: 0.8rem;
|
|
color: #7f8c8d;
|
|
margin-top: 8px;
|
|
margin-left: 140px;
|
|
font-style: italic;
|
|
}
|
|
|
|
/* Responsive design */
|
|
@media (max-width: 768px) {
|
|
.dashboard-header h1 {
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.container {
|
|
padding: 20px;
|
|
}
|
|
|
|
.form-row {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: 10px;
|
|
}
|
|
|
|
.form-row label {
|
|
min-width: auto;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.help-text {
|
|
margin-left: 0;
|
|
margin-top: 10px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<!-- Loading Overlay -->
|
|
<div class="loading-overlay" id="loadingOverlay">
|
|
<div class="loading-container">
|
|
<div class="loading-spinner"></div>
|
|
<div class="loading-text" id="loadingText">Please wait...</div>
|
|
<div class="loading-subtext" id="loadingSubtext">Searching database for historical data</div>
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" id="progressFill"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Chart-style header -->
|
|
<div class="dashboard-header">
|
|
<h1>LASUCA Controls - Historical Data Search & Trend</h1>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<!-- Rest of your existing HTML content stays the same -->
|
|
<div class="search-form">
|
|
<!-- Search Type Tabs -->
|
|
<div class="search-type-tabs">
|
|
<div class="tab <?php echo $searchType == 'single_name' ? 'active' : ''; ?>" onclick="switchSearchType('single_name')">Single Tag</div>
|
|
<div class="tab <?php echo $searchType == 'multiple_names' ? 'active' : ''; ?>" onclick="switchSearchType('multiple_names')">Multiple Tags</div>
|
|
</div>
|
|
|
|
<form method="GET" id="searchForm" onsubmit="showLoading()">
|
|
<input type="hidden" id="search_type" name="search_type" value="<?php echo $searchType; ?>">
|
|
|
|
<!-- Single Name Search -->
|
|
<div id="single_name" class="search-option <?php echo $searchType == 'single_name' ? 'active' : ''; ?>">
|
|
<div class="form-row">
|
|
<label for="name">Tag Name:</label>
|
|
<div class="autocomplete-container">
|
|
<input type="text" id="name" name="name" value="<?php echo htmlspecialchars($searchName); ?>" placeholder="Start typing tag name..." autocomplete="off">
|
|
<div class="autocomplete-suggestions" id="name-suggestions"></div>
|
|
</div>
|
|
</div>
|
|
<div class="help-text">Search for historical data by tag name (with auto-complete)</div>
|
|
</div>
|
|
|
|
<!-- Multiple Names Search -->
|
|
<div id="multiple_names" class="search-option <?php echo $searchType == 'multiple_names' ? 'active' : ''; ?>">
|
|
<div class="form-row">
|
|
<label for="tag1">Tag 1:</label>
|
|
<select id="tag1" name="tag1">
|
|
<option value="">Select a tag...</option>
|
|
<?php
|
|
// Get all available tag names for dropdowns
|
|
try {
|
|
$tagStmt = $pdo->prepare("
|
|
SELECT DISTINCT name
|
|
FROM dbo.id_names
|
|
ORDER BY name
|
|
");
|
|
$tagStmt->execute();
|
|
$allTags = $tagStmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Parse existing multiple_names for pre-selection
|
|
$selectedTags = [];
|
|
if (!empty($multipleNames)) {
|
|
$selectedTags = array_map('trim', explode(',', $multipleNames));
|
|
}
|
|
|
|
foreach ($allTags as $tag) {
|
|
$tagName = htmlspecialchars($tag['name']);
|
|
$selected = isset($selectedTags[0]) && $selectedTags[0] === $tag['name'] ? 'selected' : '';
|
|
echo "<option value=\"{$tagName}\" {$selected}>{$tagName}</option>";
|
|
}
|
|
} catch (PDOException $e) {
|
|
echo "<option value=\"\">Error loading tags</option>";
|
|
}
|
|
?>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<label for="tag2">Tag 2:</label>
|
|
<select id="tag2" name="tag2">
|
|
<option value="">Select a tag...</option>
|
|
<?php
|
|
foreach ($allTags as $tag) {
|
|
$tagName = htmlspecialchars($tag['name']);
|
|
$selected = isset($selectedTags[1]) && $selectedTags[1] === $tag['name'] ? 'selected' : '';
|
|
echo "<option value=\"{$tagName}\" {$selected}>{$tagName}</option>";
|
|
}
|
|
?>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<label for="tag3">Tag 3:</label>
|
|
<select id="tag3" name="tag3">
|
|
<option value="">Select a tag...</option>
|
|
<?php
|
|
foreach ($allTags as $tag) {
|
|
$tagName = htmlspecialchars($tag['name']);
|
|
$selected = isset($selectedTags[2]) && $selectedTags[2] === $tag['name'] ? 'selected' : '';
|
|
echo "<option value=\"{$tagName}\" {$selected}>{$tagName}</option>";
|
|
}
|
|
?>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<label for="tag4">Tag 4:</label>
|
|
<select id="tag4" name="tag4">
|
|
<option value="">Select a tag...</option>
|
|
<?php
|
|
foreach ($allTags as $tag) {
|
|
$tagName = htmlspecialchars($tag['name']);
|
|
$selected = isset($selectedTags[3]) && $selectedTags[3] === $tag['name'] ? 'selected' : '';
|
|
echo "<option value=\"{$tagName}\" {$selected}>{$tagName}</option>";
|
|
}
|
|
?>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<label for="tag5">Tag 5:</label>
|
|
<select id="tag5" name="tag5">
|
|
<option value="">Select a tag...</option>
|
|
<?php
|
|
foreach ($allTags as $tag) {
|
|
$tagName = htmlspecialchars($tag['name']);
|
|
$selected = isset($selectedTags[4]) && $selectedTags[4] === $tag['name'] ? 'selected' : '';
|
|
echo "<option value=\"{$tagName}\" {$selected}>{$tagName}</option>";
|
|
}
|
|
?>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="help-text">Select up to 5 different tags to compare their historical data</div>
|
|
</div>
|
|
|
|
<!-- Common Date Filters -->
|
|
<div class="form-row">
|
|
<label for="start_date">Start Date:</label>
|
|
<input type="datetime-local" id="start_date" name="start_date" value="<?php echo htmlspecialchars($startDate); ?>">
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<label for="end_date">End Date:</label>
|
|
<input type="datetime-local" id="end_date" name="end_date" value="<?php echo htmlspecialchars($endDate); ?>">
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<label for="limit">Results Limit:</label>
|
|
<select id="limit" name="limit">
|
|
<option value="50" <?php echo $limit == 50 ? 'selected' : ''; ?>>50</option>
|
|
<option value="100" <?php echo $limit == 100 ? 'selected' : ''; ?>>100</option>
|
|
<option value="500" <?php echo $limit == 500 ? 'selected' : ''; ?>>500</option>
|
|
<option value="1000" <?php echo $limit == 1000 ? 'selected' : ''; ?>>1000</option>
|
|
<option value="5000" <?php echo $limit == 5000 ? 'selected' : ''; ?>>5000</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<label for="time_interval">Data Interval:</label>
|
|
<select id="time_interval" name="time_interval">
|
|
<option value="1" <?php echo $timeInterval == 1 ? 'selected' : ''; ?>>Every 1 Second</option>
|
|
<option value="10" <?php echo $timeInterval == 10 ? 'selected' : ''; ?>>Every 10 Seconds</option>
|
|
<option value="30" <?php echo $timeInterval == 30 ? 'selected' : ''; ?>>Every 30 Seconds (Default)</option>
|
|
<option value="60" <?php echo $timeInterval == 60 ? 'selected' : ''; ?>>Every 1 Minute</option>
|
|
</select>
|
|
</div>
|
|
<div class="help-text">Choose data sampling interval. 30 seconds shows data every 30 seconds for balanced detail and performance.</div>
|
|
|
|
<div class="form-row checkbox" id="trend_option" style="display: <?php echo $searchType == 'single_name' ? 'flex' : 'none'; ?>;">
|
|
<input type="checkbox" id="show_trend" name="show_trend" <?php echo $showTrend ? 'checked' : ''; ?>>
|
|
<label for="show_trend" class="checkbox">Show Trend Chart (Single Tag only)</label>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<button type="submit">Search</button>
|
|
<a href="opcsearch.php"><button type="button">Clear</button></a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<?php if ($canSearch) : ?>
|
|
<?php if (!empty($results)) : ?>
|
|
<div class="results-info">
|
|
<div class="results-count">
|
|
Showing <?php echo count($organizedResults); ?> timestamp(s) with <?php echo count($results); ?> data points for
|
|
<strong><?php echo htmlspecialchars($searchTitle); ?></strong>
|
|
<?php if ($timeInterval > 1): ?>
|
|
<br><small>Data interval: Every <?php
|
|
if ($timeInterval >= 60) {
|
|
echo ($timeInterval / 60) . ' minute' . ($timeInterval > 60 ? 's' : '');
|
|
} else {
|
|
echo $timeInterval . ' second' . ($timeInterval > 1 ? 's' : '');
|
|
}
|
|
?></small>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php if ($totalCount > $limit): ?>
|
|
<div style="color: #856404; background-color: #fff3cd; padding: 5px 10px; border-radius: 3px;">
|
|
Note: Only showing first <?php echo $limit; ?> results. Total: <?php echo $totalCount; ?>. Use date filters to narrow search.
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Export buttons for multiple tags - MOVED TO TOP -->
|
|
<?php if ($searchType == 'multiple_names' && !empty($results)) : ?>
|
|
<div class="export-buttons">
|
|
<button onclick="exportMultipleTagsCSV()">Export Multiple Tags as CSV</button>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($showTrend && !empty($chartLabels) && $searchType == 'single_name') : ?>
|
|
<!-- Statistics Panel -->
|
|
<?php
|
|
$numericValues = array_filter($chartData, 'is_numeric');
|
|
if (!empty($numericValues)) {
|
|
$min = min($numericValues);
|
|
$max = max($numericValues);
|
|
$avg = array_sum($numericValues) / count($numericValues);
|
|
$firstValue = reset($numericValues);
|
|
$lastValue = end($numericValues);
|
|
$change = $lastValue - $firstValue;
|
|
$changePercent = $firstValue != 0 ? ($change / $firstValue) * 100 : 0;
|
|
}
|
|
?>
|
|
|
|
<?php if (!empty($numericValues)) : ?>
|
|
<div class="stats-panel">
|
|
<div class="stat-box">
|
|
<div class="stat-value"><?php echo number_format($avg, 2); ?></div>
|
|
<div class="stat-label">Average</div>
|
|
</div>
|
|
<div class="stat-box">
|
|
<div class="stat-value"><?php echo number_format($min, 2); ?></div>
|
|
<div class="stat-label">Minimum</div>
|
|
</div>
|
|
<div class="stat-box">
|
|
<div class="stat-value"><?php echo number_format($max, 2); ?></div>
|
|
<div class="stat-label">Maximum</div>
|
|
</div>
|
|
<div class="stat-box">
|
|
<div class="stat-value" style="color: <?php echo $change >= 0 ? '#28a745' : '#dc3545'; ?>">
|
|
<?php echo ($change >= 0 ? '+' : '') . number_format($change, 2); ?>
|
|
(<?php echo ($changePercent >= 0 ? '+' : '') . number_format($changePercent, 1); ?>%)
|
|
</div>
|
|
<div class="stat-label">Total Change</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Chart Container -->
|
|
<div class="chart-container">
|
|
<h3>Trend Chart for <?php echo htmlspecialchars($searchTitle); ?></h3>
|
|
|
|
<div class="export-buttons">
|
|
<button onclick="exportChart('png')">Export as PNG</button>
|
|
<button onclick="exportChart('csv')">Export as CSV</button>
|
|
</div>
|
|
|
|
<div class="chart-wrapper">
|
|
<canvas id="trendChart"></canvas>
|
|
</div>
|
|
|
|
<div class="trend-controls">
|
|
<label><input type="checkbox" id="showDataPoints" checked> Show Data Points</label>
|
|
<label><input type="checkbox" id="showGrid" checked> Show Grid</label>
|
|
<label><input type="checkbox" id="smoothLines"> Smooth Lines</label>
|
|
<label><input type="checkbox" id="showAverage" checked> Show Average Line</label>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="table-scroll">
|
|
<table id="dataTable">
|
|
<thead>
|
|
<tr>
|
|
<th class="timestamp-header">Timestamp</th>
|
|
<?php foreach ($tagNames as $tagName): ?>
|
|
<th><?php echo htmlspecialchars($tagName); ?></th>
|
|
<?php endforeach; ?>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($organizedResults as $timestampData): ?>
|
|
<tr>
|
|
<td class="timestamp"><?php echo htmlspecialchars(formatTimestamp($timestampData['timestamp'])); ?></td>
|
|
<?php foreach ($tagNames as $tagName): ?>
|
|
<td class="numeric-value">
|
|
<?php
|
|
if (isset($timestampData[$tagName])) {
|
|
echo htmlspecialchars(formatValue($timestampData[$tagName]));
|
|
} else {
|
|
echo '-';
|
|
}
|
|
?>
|
|
</td>
|
|
<?php endforeach; ?>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="no-results">
|
|
<h3>No results found</h3>
|
|
<p>No data found for the specified search criteria.</p>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Inline Status Indicator (alternative/additional to overlay) -->
|
|
<div class="search-status" id="searchStatus" style="display: none;">
|
|
<div class="status-content">
|
|
<div class="mini-spinner"></div>
|
|
<span class="status-text">Searching database...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Autocomplete functionality
|
|
class AutoComplete {
|
|
constructor(inputElement, suggestionsElement) {
|
|
this.input = inputElement;
|
|
this.suggestions = suggestionsElement;
|
|
this.selectedIndex = -1;
|
|
this.currentSuggestions = [];
|
|
this.isSelecting = false; // Flag to prevent conflicts
|
|
|
|
this.input.addEventListener('input', this.handleInput.bind(this));
|
|
this.input.addEventListener('keydown', this.handleKeyDown.bind(this));
|
|
this.input.addEventListener('focus', this.handleFocus.bind(this));
|
|
document.addEventListener('click', this.handleDocumentClick.bind(this));
|
|
}
|
|
|
|
async handleInput(e) {
|
|
const query = e.target.value.trim();
|
|
|
|
if (query.length < 2) {
|
|
this.hideSuggestions();
|
|
return;
|
|
}
|
|
|
|
let cursorPos = 0;
|
|
let currentWord = '';
|
|
|
|
// Get cursor position safely
|
|
try {
|
|
if (typeof e.target.selectionStart === 'number') {
|
|
cursorPos = e.target.selectionStart;
|
|
} else {
|
|
cursorPos = query.length;
|
|
}
|
|
} catch (error) {
|
|
cursorPos = query.length;
|
|
}
|
|
|
|
// Get the current word being typed (for multiple names)
|
|
const textBeforeCursor = query.substring(0, cursorPos);
|
|
const lastCommaIndex = textBeforeCursor.lastIndexOf(',');
|
|
|
|
if (lastCommaIndex >= 0) {
|
|
currentWord = textBeforeCursor.substring(lastCommaIndex + 1).trim();
|
|
} else {
|
|
currentWord = textBeforeCursor.trim();
|
|
}
|
|
|
|
if (currentWord.length < 2) {
|
|
this.hideSuggestions();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`?action=autocomplete&query=${encodeURIComponent(currentWord)}`);
|
|
const suggestions = await response.json();
|
|
this.showSuggestions(suggestions, currentWord);
|
|
} catch (error) {
|
|
console.error('Autocomplete error:', error);
|
|
this.hideSuggestions();
|
|
}
|
|
}
|
|
|
|
showSuggestions(suggestions, currentWord) {
|
|
this.currentSuggestions = suggestions;
|
|
this.selectedIndex = -1;
|
|
|
|
if (suggestions.length === 0) {
|
|
this.hideSuggestions();
|
|
return;
|
|
}
|
|
|
|
this.suggestions.innerHTML = '';
|
|
suggestions.forEach((suggestion, index) => {
|
|
const div = document.createElement('div');
|
|
div.className = 'autocomplete-suggestion';
|
|
div.textContent = suggestion;
|
|
|
|
// Use both mousedown and click for better compatibility
|
|
div.addEventListener('mousedown', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.isSelecting = true;
|
|
this.selectSuggestion(suggestion);
|
|
});
|
|
|
|
div.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (!this.isSelecting) {
|
|
this.selectSuggestion(suggestion);
|
|
}
|
|
});
|
|
|
|
this.suggestions.appendChild(div);
|
|
});
|
|
|
|
this.suggestions.style.display = 'block';
|
|
}
|
|
|
|
hideSuggestions() {
|
|
this.suggestions.style.display = 'none';
|
|
this.selectedIndex = -1;
|
|
this.isSelecting = false;
|
|
}
|
|
|
|
handleFocus(e) {
|
|
// Reset selection flag when input gains focus
|
|
this.isSelecting = false;
|
|
}
|
|
|
|
handleKeyDown(e) {
|
|
if (this.suggestions.style.display === 'none') return;
|
|
|
|
switch (e.key) {
|
|
case 'ArrowDown':
|
|
e.preventDefault();
|
|
this.selectedIndex = Math.min(this.selectedIndex + 1, this.currentSuggestions.length - 1);
|
|
this.updateSelection();
|
|
break;
|
|
case 'ArrowUp':
|
|
e.preventDefault();
|
|
this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
|
|
this.updateSelection();
|
|
break;
|
|
case 'Enter':
|
|
e.preventDefault();
|
|
if (this.selectedIndex >= 0) {
|
|
this.selectSuggestion(this.currentSuggestions[this.selectedIndex]);
|
|
}
|
|
break;
|
|
case 'Escape':
|
|
this.hideSuggestions();
|
|
break;
|
|
}
|
|
}
|
|
|
|
updateSelection() {
|
|
const suggestions = this.suggestions.querySelectorAll('.autocomplete-suggestion');
|
|
suggestions.forEach((suggestion, index) => {
|
|
suggestion.classList.toggle('active', index === this.selectedIndex);
|
|
});
|
|
}
|
|
|
|
selectSuggestion(suggestion) {
|
|
console.log('Selecting suggestion:', suggestion); // Debug log
|
|
|
|
let cursorPos = 0;
|
|
let currentValue = this.input.value || '';
|
|
|
|
// Handle cursor position differently for textarea vs input
|
|
if (this.input.tagName.toLowerCase() === 'textarea') {
|
|
// For textarea, use different approach
|
|
try {
|
|
if (typeof this.input.selectionStart === 'number') {
|
|
cursorPos = this.input.selectionStart;
|
|
} else {
|
|
// Fallback for older browsers
|
|
cursorPos = currentValue.length;
|
|
}
|
|
} catch (e) {
|
|
cursorPos = currentValue.length;
|
|
}
|
|
} else {
|
|
// For regular input elements
|
|
try {
|
|
cursorPos = this.input.selectionStart || currentValue.length;
|
|
} catch (e) {
|
|
cursorPos = currentValue.length;
|
|
}
|
|
}
|
|
|
|
const textBeforeCursor = currentValue.substring(0, cursorPos);
|
|
const textAfterCursor = currentValue.substring(cursorPos);
|
|
const lastCommaIndex = textBeforeCursor.lastIndexOf(',');
|
|
|
|
let newValue;
|
|
let newCursorPos;
|
|
|
|
if (lastCommaIndex >= 0) {
|
|
// Replace the current word in a comma-separated list
|
|
const beforeComma = textBeforeCursor.substring(0, lastCommaIndex + 1);
|
|
newValue = beforeComma + ' ' + suggestion + textAfterCursor;
|
|
newCursorPos = beforeComma.length + 1 + suggestion.length;
|
|
} else {
|
|
// Replace the entire input
|
|
newValue = suggestion + textAfterCursor;
|
|
newCursorPos = suggestion.length;
|
|
}
|
|
|
|
console.log('New value:', newValue); // Debug log
|
|
console.log('New cursor position:', newCursorPos); // Debug log
|
|
|
|
// Update the value
|
|
this.input.value = newValue;
|
|
this.hideSuggestions();
|
|
|
|
// Set cursor position and focus
|
|
setTimeout(() => {
|
|
try {
|
|
// Focus first
|
|
this.input.focus();
|
|
|
|
// Then try to set cursor position
|
|
if (this.input.setSelectionRange && typeof newCursorPos === 'number') {
|
|
this.input.setSelectionRange(newCursorPos, newCursorPos);
|
|
}
|
|
} catch (e) {
|
|
console.log('Cursor positioning failed:', e);
|
|
// Just ensure focus if cursor positioning fails
|
|
this.input.focus();
|
|
}
|
|
|
|
this.isSelecting = false;
|
|
}, 50); // Increased timeout for better compatibility
|
|
}
|
|
|
|
handleDocumentClick(e) {
|
|
// Don't hide if we're in the process of selecting
|
|
if (this.isSelecting) {
|
|
return;
|
|
}
|
|
|
|
// Only hide if clicking outside both input and suggestions
|
|
if (!this.input.contains(e.target) && !this.suggestions.contains(e.target)) {
|
|
this.hideSuggestions();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Only initialize autocomplete for single name search
|
|
const nameInput = document.getElementById('name');
|
|
const nameSuggestions = document.getElementById('name-suggestions');
|
|
|
|
if (nameInput && nameSuggestions) {
|
|
new AutoComplete(nameInput, nameSuggestions);
|
|
}
|
|
|
|
// Add change event handlers for the tag dropdowns to prevent duplicates
|
|
const tagSelects = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5'];
|
|
|
|
tagSelects.forEach(tagId => {
|
|
const select = document.getElementById(tagId);
|
|
if (select) {
|
|
select.addEventListener('change', function() {
|
|
updateTagOptions();
|
|
});
|
|
}
|
|
});
|
|
|
|
function updateTagOptions() {
|
|
// Get all currently selected values
|
|
const selectedValues = [];
|
|
tagSelects.forEach(tagId => {
|
|
const select = document.getElementById(tagId);
|
|
if (select && select.value) {
|
|
selectedValues.push(select.value);
|
|
}
|
|
});
|
|
|
|
// Update each dropdown to disable already selected options
|
|
tagSelects.forEach(tagId => {
|
|
const select = document.getElementById(tagId);
|
|
if (select) {
|
|
Array.from(select.options).forEach(option => {
|
|
if (option.value === '') {
|
|
option.disabled = false; // Always allow "Select a tag..."
|
|
} else {
|
|
// Disable if selected in another dropdown, but not in this one
|
|
option.disabled = selectedValues.includes(option.value) && option.value !== select.value;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
updateTagOptions();
|
|
});
|
|
|
|
// Tab switching functionality
|
|
function switchSearchType(type) {
|
|
document.getElementById('search_type').value = type;
|
|
|
|
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
|
|
document.querySelector(`.tab[onclick="switchSearchType('${type}')"]`).classList.add('active');
|
|
|
|
document.querySelectorAll('.search-option').forEach(option => option.classList.remove('active'));
|
|
document.getElementById(type).classList.add('active');
|
|
|
|
// Show trend option only for single tag search
|
|
const trendOption = document.getElementById('trend_option');
|
|
const showTrendCheckbox = document.getElementById('show_trend');
|
|
|
|
if (type === 'single_name') {
|
|
trendOption.style.display = 'flex';
|
|
document.querySelector('label[for="show_trend"]').textContent = 'Show Trend Chart (Single Tag only)';
|
|
} else {
|
|
trendOption.style.display = 'none';
|
|
showTrendCheckbox.checked = false; // Uncheck when hidden
|
|
}
|
|
}
|
|
|
|
// Loading indicator functions
|
|
function showLoading() {
|
|
const overlay = document.getElementById('loadingOverlay');
|
|
const loadingText = document.getElementById('loadingText');
|
|
const loadingSubtext = document.getElementById('loadingSubtext');
|
|
const progressFill = document.getElementById('progressFill');
|
|
const submitButton = document.querySelector('button[type="submit"]');
|
|
|
|
// Disable submit button
|
|
submitButton.disabled = true;
|
|
submitButton.textContent = 'Searching...';
|
|
|
|
// Show overlay
|
|
overlay.style.display = 'flex';
|
|
|
|
// Animate progress bar
|
|
let progress = 0;
|
|
const progressInterval = setInterval(() => {
|
|
progress += Math.random() * 15;
|
|
if (progress > 90) progress = 90;
|
|
progressFill.style.width = progress + '%';
|
|
}, 200);
|
|
|
|
// Update loading messages
|
|
const messages = [{
|
|
text: "Please wait...",
|
|
subtext: "Connecting to database"
|
|
},
|
|
{
|
|
text: "Searching...",
|
|
subtext: "Querying historical data"
|
|
},
|
|
{
|
|
text: "Processing...",
|
|
subtext: "Organizing results"
|
|
},
|
|
{
|
|
text: "Almost done...",
|
|
subtext: "Preparing data display"
|
|
}
|
|
];
|
|
|
|
let messageIndex = 0;
|
|
const messageInterval = setInterval(() => {
|
|
if (messageIndex < messages.length) {
|
|
loadingText.textContent = messages[messageIndex].text;
|
|
loadingSubtext.textContent = messages[messageIndex].subtext;
|
|
messageIndex++;
|
|
}
|
|
}, 1000);
|
|
|
|
// Store intervals for cleanup
|
|
window.loadingIntervals = {
|
|
progressInterval,
|
|
messageInterval
|
|
};
|
|
}
|
|
|
|
function hideLoading() {
|
|
const overlay = document.getElementById('loadingOverlay');
|
|
const submitButton = document.querySelector('button[type="submit"]');
|
|
const progressFill = document.getElementById('progressFill');
|
|
|
|
// Clear intervals
|
|
if (window.loadingIntervals) {
|
|
clearInterval(window.loadingIntervals.progressInterval);
|
|
clearInterval(window.loadingIntervals.messageInterval);
|
|
}
|
|
|
|
// Complete progress bar
|
|
progressFill.style.width = '100%';
|
|
|
|
// Hide overlay after a short delay
|
|
setTimeout(() => {
|
|
overlay.style.display = 'none';
|
|
submitButton.disabled = false;
|
|
submitButton.textContent = 'Search';
|
|
progressFill.style.width = '0%';
|
|
}, 500);
|
|
}
|
|
|
|
// Auto-hide loading on page load (for when search completes)
|
|
window.addEventListener('load', function() {
|
|
// Small delay to show completion
|
|
setTimeout(hideLoading, 100);
|
|
});
|
|
|
|
// Also hide loading if user navigates back
|
|
window.addEventListener('pageshow', function(event) {
|
|
if (event.persisted) {
|
|
hideLoading();
|
|
}
|
|
});
|
|
|
|
<?php if ($showTrend && !empty($chartLabels) && $searchType == 'single_name') : ?>
|
|
// Single tag chart functionality only
|
|
const chartLabels = <?php echo json_encode($chartLabels); ?>;
|
|
const chartData = <?php echo json_encode($chartData); ?>;
|
|
const searchTitle = <?php echo json_encode($searchTitle); ?>;
|
|
|
|
const numericValues = chartData.filter(val => !isNaN(val) && val !== null);
|
|
const average = numericValues.length > 0 ? numericValues.reduce((a, b) => a + b, 0) / numericValues.length : 0;
|
|
const averageData = new Array(chartLabels.length).fill(average);
|
|
|
|
let chartConfig = {
|
|
type: 'line',
|
|
data: {
|
|
labels: chartLabels,
|
|
datasets: [{
|
|
label: searchTitle,
|
|
data: chartData,
|
|
borderColor: 'rgb(0, 124, 186)',
|
|
backgroundColor: 'rgba(0, 124, 186, 0.1)',
|
|
borderWidth: 2,
|
|
pointRadius: 3,
|
|
pointHoverRadius: 6,
|
|
fill: true,
|
|
tension: 0.1,
|
|
order: 1
|
|
}, {
|
|
label: 'Average (' + average.toFixed(2) + ')',
|
|
data: averageData,
|
|
borderColor: 'rgb(220, 53, 69)',
|
|
backgroundColor: 'transparent',
|
|
borderWidth: 2,
|
|
borderDash: [5, 5],
|
|
pointRadius: 0,
|
|
pointHoverRadius: 0,
|
|
fill: false,
|
|
tension: 0,
|
|
order: 2
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
x: {
|
|
display: true,
|
|
title: {
|
|
display: true,
|
|
text: 'Time'
|
|
},
|
|
ticks: {
|
|
maxTicksLimit: 10
|
|
}
|
|
},
|
|
y: {
|
|
display: true,
|
|
title: {
|
|
display: true,
|
|
text: 'Value'
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'top'
|
|
},
|
|
tooltip: {
|
|
mode: 'index',
|
|
intersect: false,
|
|
}
|
|
},
|
|
interaction: {
|
|
mode: 'nearest',
|
|
axis: 'x',
|
|
intersect: false
|
|
}
|
|
}
|
|
};
|
|
|
|
const ctx = document.getElementById('trendChart').getContext('2d');
|
|
let chart = new Chart(ctx, chartConfig);
|
|
|
|
// Chart controls
|
|
document.getElementById('showDataPoints').addEventListener('change', function() {
|
|
chart.data.datasets.forEach(dataset => {
|
|
if (dataset.label.indexOf('Average') === -1) {
|
|
dataset.pointRadius = this.checked ? 3 : 0;
|
|
}
|
|
});
|
|
chart.update();
|
|
});
|
|
|
|
document.getElementById('showGrid').addEventListener('change', function() {
|
|
chart.options.scales.x.grid = { display: this.checked };
|
|
chart.options.scales.y.grid = { display: this.checked };
|
|
chart.update();
|
|
});
|
|
|
|
document.getElementById('smoothLines').addEventListener('change', function() {
|
|
chart.data.datasets.forEach(dataset => {
|
|
if (dataset.label.indexOf('Average') === -1) {
|
|
dataset.tension = this.checked ? 0.4 : 0.1;
|
|
}
|
|
});
|
|
chart.update();
|
|
});
|
|
|
|
document.getElementById('showAverage').addEventListener('change', function() {
|
|
chart.data.datasets[1].hidden = !this.checked;
|
|
chart.update();
|
|
});
|
|
|
|
// Export functions
|
|
function exportChart(format) {
|
|
if (format === 'png') {
|
|
const link = document.createElement('a');
|
|
const timestamp = new Date().getTime();
|
|
link.download = 'trend_chart_' + timestamp + '.png';
|
|
link.href = chart.toBase64Image();
|
|
link.click();
|
|
} else if (format === 'csv') {
|
|
exportToCSV();
|
|
}
|
|
}
|
|
|
|
function exportToCSV() {
|
|
let csv = 'Timestamp,Value,Average\n';
|
|
for (let i = 0; i < chartLabels.length; i++) {
|
|
csv += chartLabels[i] + ',' + chartData[i] + ',' + average.toFixed(2) + '\n';
|
|
}
|
|
|
|
const blob = new Blob([csv], { type: 'text/csv' });
|
|
const link = document.createElement('a');
|
|
const timestamp = new Date().getTime();
|
|
link.download = 'trend_data_' + timestamp + '.csv';
|
|
link.href = window.URL.createObjectURL(blob);
|
|
link.click();
|
|
}
|
|
<?php endif; ?>
|
|
|
|
// Export functions for multiple tags - MOVED TO MAIN SCRIPT AREA
|
|
function exportMultipleTagsCSV() {
|
|
const table = document.getElementById('dataTable');
|
|
if (!table) return;
|
|
|
|
let csv = '';
|
|
|
|
// Export headers
|
|
const headers = [];
|
|
table.querySelectorAll('thead th').forEach(th => {
|
|
headers.push(th.textContent.trim());
|
|
});
|
|
csv += headers.join(',') + '\n';
|
|
|
|
// Export data rows
|
|
table.querySelectorAll('tbody tr').forEach(tr => {
|
|
const row = [];
|
|
tr.querySelectorAll('td').forEach(td => {
|
|
let cellData = td.textContent.trim();
|
|
// Escape commas and quotes in CSV
|
|
if (cellData.includes(',') || cellData.includes('"') || cellData.includes('\n')) {
|
|
cellData = '"' + cellData.replace(/"/g, '""') + '"';
|
|
}
|
|
row.push(cellData);
|
|
});
|
|
csv += row.join(',') + '\n';
|
|
});
|
|
|
|
// Download the CSV
|
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
const link = document.createElement('a');
|
|
const timestamp = new Date().getTime();
|
|
link.download = 'multiple_tags_data_' + timestamp + '.csv';
|
|
link.href = window.URL.createObjectURL(blob);
|
|
link.style.display = 'none';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|