Files
controls-web/controls-classic/opcsearch.php
2026-02-17 09:29:34 -06:00

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>