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

1116 lines
37 KiB
PHP

<?php // phpcs:ignoreFile
require __DIR__ . '/session.php';
require __DIR__ . '/userAccess.php';
require __DIR__ . '/includes/log_query.php';
$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());
}
$isAutocomplete = isset($_GET['action']) && $_GET['action'] === 'autocomplete';
if ($isAutocomplete) {
$query = isset($_GET['query']) ? trim($_GET['query']) : '';
$suggestions = [];
if ($query !== '') {
try {
$stmt = $pdo->prepare("
SELECT TOP 20 name
FROM dbo.id_names
WHERE name LIKE :query
ORDER BY name
");
$stmt->execute([':query' => '%' . $query . '%']);
$rows = $stmt->fetchAll();
foreach ($rows as $row) {
if (!empty($row['name'])) {
$suggestions[] = $row['name'];
}
}
} catch (PDOException $e) {
error_log('Autocomplete error: ' . $e->getMessage());
}
}
header('Content-Type: application/json');
echo json_encode($suggestions);
exit;
}
$allTags = [];
try {
$tagStmt = $pdo->prepare("
SELECT DISTINCT name
FROM dbo.id_names
WHERE name <> ''
ORDER BY name
");
$tagStmt->execute();
$allTags = $tagStmt->fetchAll();
} catch (PDOException $e) {
error_log('Tag preload error: ' . $e->getMessage());
}
$searchType = isset($_GET['search_type']) ? $_GET['search_type'] : 'single_name';
$searchName = isset($_GET['name']) ? trim($_GET['name']) : '';
$selectedTags = [];
for ($i = 1; $i <= 5; $i++) {
$param = "tag{$i}";
if (!empty($_GET[$param])) {
$selectedTags[] = trim($_GET[$param]);
}
}
$legacyMultiple = isset($_GET['multiple_names']) ? trim($_GET['multiple_names']) : '';
if ($legacyMultiple !== '' && empty($selectedTags)) {
$selectedTags = array_filter(array_map('trim', explode(',', $legacyMultiple)));
}
$startDateInput = isset($_GET['start_date']) ? $_GET['start_date'] : '';
$endDateInput = isset($_GET['end_date']) ? $_GET['end_date'] : '';
$normalizeDateTime = function ($value) {
if ($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');
}
}
return $value;
};
$startDate = $normalizeDateTime($startDateInput);
$endDate = $normalizeDateTime($endDateInput);
$limit = isset($_GET['limit']) ? (int) $_GET['limit'] : 100;
if ($limit <= 0) {
$limit = 100;
}
$showTrend = isset($_GET['show_trend']);
$timeInterval = isset($_GET['time_interval']) ? (int) $_GET['time_interval'] : 30;
if ($timeInterval < 1) {
$timeInterval = 1;
}
$results = [];
$searchTitle = '';
$totalCount = 0;
$tagNames = [];
$organizedResults = [];
$canSearch = false;
switch ($searchType) {
case 'single_name':
$canSearch = $searchName !== '';
break;
case 'multiple_names':
$canSearch = !empty($selectedTags);
break;
}
if ($canSearch) {
try {
$sql = "
SELECT
a.ID,
a.Value,
a.TimeStamp,
COALESCE(n.name, CONCAT('ID_', a.ID)) AS name
FROM dbo.archive a
LEFT JOIN dbo.id_names n ON a.ID = n.idnumber
WHERE 1 = 1
AND a.TimeStamp <> :invalid_timestamp
";
$countSql = "
SELECT COUNT(*)
FROM dbo.archive a
LEFT JOIN dbo.id_names n ON a.ID = n.idnumber
WHERE 1 = 1
AND a.TimeStamp <> :invalid_timestamp
";
$params = [];
$params[':invalid_timestamp'] = '1899-12-31 18:00:00';
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 = [];
foreach ($selectedTags as $index => $tag) {
$placeholder = ':name' . $index;
$placeholders[] = $placeholder;
$params[$placeholder] = $tag;
}
$inClause = implode(',', $placeholders);
$sql .= " AND n.name IN ($inClause)";
$countSql .= " AND n.name IN ($inClause)";
$preview = array_slice($selectedTags, 0, 3);
$searchTitle = 'Multiple tags (' . count($selectedTags) . '): ' . implode(', ', $preview);
if (count($selectedTags) > 3) {
$searchTitle .= '…';
}
}
break;
}
if ($startDate !== '') {
$sql .= " AND a.TimeStamp >= :start_date";
$countSql .= " AND a.TimeStamp >= :start_date";
$params[':start_date'] = $startDate;
}
if ($endDate !== '') {
$sql .= " AND a.TimeStamp <= :end_date";
$countSql .= " AND a.TimeStamp <= :end_date";
$params[':end_date'] = $endDate;
}
if ($timeInterval > 1) {
if ($timeInterval >= 60) {
$minutes = (int) ($timeInterval / 60);
$sql .= "
AND DATEPART(MINUTE, a.TimeStamp) % :minute_interval = 0
AND DATEPART(SECOND, a.TimeStamp) = 0
";
$countSql .= "
AND DATEPART(MINUTE, a.TimeStamp) % :minute_interval = 0
AND DATEPART(SECOND, a.TimeStamp) = 0
";
$params[':minute_interval'] = $minutes;
} else {
$sql .= " AND DATEPART(SECOND, a.TimeStamp) % :time_interval = 0";
$countSql .= " AND DATEPART(SECOND, a.TimeStamp) % :time_interval = 0";
$params[':time_interval'] = $timeInterval;
}
}
$countStmt = $pdo->prepare($countSql);
foreach ($params as $key => $value) {
if ($key === ':limit') {
continue;
}
$countStmt->bindValue($key, $value);
}
$countStmt->execute();
$totalCount = (int) $countStmt->fetchColumn();
if ($showTrend) {
$sql .= "
ORDER BY a.TimeStamp ASC
OFFSET 0 ROWS FETCH NEXT :limit ROWS ONLY
";
} else {
$sql .= "
ORDER BY a.TimeStamp DESC
OFFSET 0 ROWS FETCH NEXT :limit ROWS ONLY
";
}
$params[':limit'] = $limit;
$stmt = $pdo->prepare($sql);
foreach ($params as $key => $value) {
if ($key === ':limit') {
$stmt->bindValue($key, $value, PDO::PARAM_INT);
} else {
$stmt->bindValue($key, $value);
}
}
$queryStartTime = microtime(true);
$stmt->execute();
$results = $stmt->fetchAll();
$queryEndTime = microtime(true);
$executionMs = (int) round(($queryEndTime - $queryStartTime) * 1000);
// Log the query for NL2SQL training
$userQueryDesc = buildUserQueryDescription(
$searchType,
$searchName,
$selectedTags,
$startDate,
$endDate,
$limit,
$timeInterval
);
$logParams = $params;
unset($logParams[':invalid_timestamp']); // Remove internal filter param
logControlsQuery(
$userQueryDesc,
$sql,
$logParams,
'opcsearch',
$executionMs,
count($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, true)) {
$tagNames[] = $tagName;
}
}
if (!empty($organizedResults)) {
if ($showTrend && $searchType === 'single_name') {
ksort($organizedResults);
} else {
krsort($organizedResults);
}
}
} catch (PDOException $e) {
echo '<div class="results-info"><div class="notice notice--warning">Query error: ' . htmlspecialchars($e->getMessage()) . '</div></div>';
}
}
function formatTimestamp($timestamp)
{
return date('Y-m-d H:i:s', strtotime($timestamp));
}
function formatValue($value)
{
if (is_numeric($value)) {
return number_format((float) $value, 2);
}
return $value;
}
$chartData = [];
$chartLabels = [];
if (!empty($results) && $showTrend && $searchType === 'single_name') {
foreach ($results as $row) {
$chartLabels[] = date('M j, Y H:i', strtotime($row['TimeStamp']));
$chartData[] = is_numeric($row['Value']) ? (float) $row['Value'] : 0;
}
}
$pageTitle = 'Historian Search';
$pageSubtitle = 'Query the SQL historian and visualize trends.';
$pageDescription = 'Search LASUCA historian data by tag, filter results, and export charts.';
$layoutWithoutSidebar = true;
$layoutReturnUrl = 'overview.php';
$layoutReturnLabel = 'Back to overview';
require __DIR__ . '/includes/layout/header.php';
?>
<div class="app-content search-app">
<section class="data-panel">
<div class="panel-intro">
<h2>Historical data search</h2>
<p>Query historian tags by name, filter by time range, and export results or trend charts.</p>
</div>
<div class="search-form">
<div class="search-type-tabs">
<button type="button" class="tab <?php echo $searchType === 'single_name' ? 'active' : ''; ?>" onclick="switchSearchType('single_name')">Single Tag</button>
<button type="button" class="tab <?php echo $searchType === 'multiple_names' ? 'active' : ''; ?>" onclick="switchSearchType('multiple_names')">Multiple Tags</button>
</div>
<form method="GET" id="searchForm" onsubmit="showLoading()">
<input type="hidden" id="search_type" name="search_type" value="<?php echo htmlspecialchars($searchType); ?>">
<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>
<p class="help-text">Search for historian data by tag name with autocomplete.</p>
</div>
<div id="multiple_names" class="search-option <?php echo $searchType === 'multiple_names' ? 'active' : ''; ?>">
<?php for ($i = 1; $i <= 5; $i++) : ?>
<?php $tagValue = $selectedTags[$i - 1] ?? ''; ?>
<div class="form-row">
<label for="tag<?php echo $i; ?>">Tag <?php echo $i; ?></label>
<select id="tag<?php echo $i; ?>" name="tag<?php echo $i; ?>">
<option value="">Select a tag...</option>
<?php foreach ($allTags as $tag) : ?>
<?php $tagNameOption = $tag['name']; ?>
<option value="<?php echo htmlspecialchars($tagNameOption); ?>" <?php echo $tagValue === $tagNameOption ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($tagNameOption); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endfor; ?>
<p class="help-text">Compare up to five tags side by side.</p>
</div>
<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($startDateInput); ?>">
</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($endDateInput); ?>">
</div>
<div class="form-row">
<label for="limit">Results limit</label>
<select id="limit" name="limit">
<?php
$limitOptions = [50, 100, 500, 1000, 5000, 10000, 43200, 86400];
foreach ($limitOptions as $option) :
?>
<option value="<?php echo $option; ?>" <?php echo $limit === $option ? 'selected' : ''; ?>>
<?php echo $option; ?>
</option>
<?php endforeach; ?>
</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>
<p class="help-text">Sampling interval controls how frequently data points are included in results.</p>
<div class="form-row checkbox" id="trend_option" style="<?php echo $searchType === 'single_name' ? '' : 'display: none;'; ?>">
<input type="checkbox" id="show_trend" name="show_trend" <?php echo $showTrend ? 'checked' : ''; ?>>
<label for="show_trend">Show trend chart (single tag only)</label>
</div>
<div class="form-row form-row--actions">
<button type="submit" class="button">Search</button>
<a href="opcsearch.php" class="button button--ghost">Reset</a>
</div>
</form>
</div>
</section>
<?php if ($canSearch) : ?>
<?php if (!empty($results)) : ?>
<section class="data-panel">
<div class="results-info">
<div class="results-count">
Showing <?php echo count($organizedResults); ?> timestamp(s) and <?php echo count($results); ?> data point(s) for
<strong><?php echo htmlspecialchars($searchTitle); ?></strong>
<?php if ($timeInterval > 1) : ?>
<br>
<small>
Interval:
<?php
if ($timeInterval >= 60) {
$minutes = $timeInterval / 60;
echo $minutes . ' minute' . ($minutes > 1 ? 's' : '');
} else {
echo $timeInterval . ' second' . ($timeInterval > 1 ? 's' : '');
}
?>
</small>
<?php endif; ?>
</div>
<?php if ($totalCount > $limit) : ?>
<div class="notice notice--warning">
Showing first <?php echo $limit; ?> rows of <?php echo $totalCount; ?> total. Narrow the date range or increase the limit for more data.
</div>
<?php endif; ?>
</div>
<?php if ($searchType === 'multiple_names') : ?>
<div class="export-buttons">
<button type="button" class="button" onclick="exportMultipleTagsCSV()">Export results as CSV</button>
</div>
<?php endif; ?>
<?php
$numericValues = [];
if ($showTrend && $searchType === 'single_name') {
$numericValues = array_filter($chartData, 'is_numeric');
}
?>
<?php if ($showTrend && $searchType === 'single_name' && !empty($chartLabels)) : ?>
<?php if (!empty($numericValues)) : ?>
<div class="stats-panel">
<?php
$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;
?>
<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 ? '#10b981' : '#ef4444'; ?>;">
<?php echo ($change >= 0 ? '+' : '') . number_format($change, 2); ?>
(<?php echo ($changePercent >= 0 ? '+' : '') . number_format($changePercent, 1); ?>%)
</div>
<div class="stat-label">Net change</div>
</div>
</div>
<?php endif; ?>
<div class="chart-container">
<header>
<h3>Trend chart for <?php echo htmlspecialchars($searchTitle); ?></h3>
</header>
<div class="export-buttons">
<button type="button" class="button" onclick="exportChart('png')">Export as PNG</button>
<button type="button" class="button button--ghost" 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 search-results">
<table id="dataTable">
<thead>
<tr>
<th>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><?php echo htmlspecialchars(formatTimestamp($timestampData['timestamp'])); ?></td>
<?php foreach ($tagNames as $tagName) : ?>
<td>
<?php
if (isset($timestampData[$tagName])) {
echo htmlspecialchars(formatValue($timestampData[$tagName]));
} else {
echo '-';
}
?>
</td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</section>
<?php else : ?>
<section class="data-panel">
<div class="no-results">
<h3>No results found</h3>
<p>No historian data matched the filters provided. Adjust the tag selection or date range and try again.</p>
</div>
</section>
<?php endif; ?>
<?php endif; ?>
</div>
<div class="loading-overlay" id="loadingOverlay">
<div class="loading-card">
<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="loading-progress">
<span id="progressFill"></span>
</div>
</div>
</div>
<div class="search-status" id="searchStatus">
<div class="mini-spinner"></div>
<span class="status-text">Searching database…</span>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
class AutoComplete {
constructor(inputElement, suggestionsElement) {
this.input = inputElement;
this.suggestions = suggestionsElement;
this.selectedIndex = -1;
this.currentSuggestions = [];
this.isSelecting = false;
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(event) {
const query = event.target.value.trim();
if (query.length < 2) {
this.hideSuggestions();
return;
}
let cursorPos = 0;
let currentWord = '';
try {
cursorPos = typeof event.target.selectionStart === 'number' ? event.target.selectionStart : query.length;
} catch (error) {
cursorPos = query.length;
}
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);
} catch (error) {
console.error('Autocomplete error:', error);
this.hideSuggestions();
}
}
showSuggestions(suggestions) {
this.currentSuggestions = suggestions;
this.selectedIndex = -1;
if (suggestions.length === 0) {
this.hideSuggestions();
return;
}
this.suggestions.innerHTML = '';
suggestions.forEach((suggestion) => {
const div = document.createElement('div');
div.className = 'autocomplete-suggestion';
div.textContent = suggestion;
div.addEventListener('mousedown', (event) => {
event.preventDefault();
event.stopPropagation();
this.isSelecting = true;
this.selectSuggestion(suggestion);
});
div.addEventListener('click', (event) => {
event.preventDefault();
event.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() {
this.isSelecting = false;
}
handleKeyDown(event) {
if (this.suggestions.style.display === 'none') {
return;
}
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
this.selectedIndex = Mata.min(this.selectedIndex + 1, this.currentSuggestions.length - 1);
this.updateSelection();
break;
case 'ArrowUp':
event.preventDefault();
this.selectedIndex = Mata.max(this.selectedIndex - 1, -1);
this.updateSelection();
break;
case 'Enter':
event.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) {
let cursorPos = 0;
let currentValue = this.input.value || '';
try {
cursorPos = typeof this.input.selectionStart === 'number' ? this.input.selectionStart : currentValue.length;
} catch (error) {
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) {
const beforeComma = textBeforeCursor.substring(0, lastCommaIndex + 1);
newValue = `${beforeComma} ${suggestion}${textAfterCursor}`;
newCursorPos = beforeComma.length + 1 + suggestion.length;
} else {
newValue = `${suggestion}${textAfterCursor}`;
newCursorPos = suggestion.length;
}
this.input.value = newValue;
this.hideSuggestions();
setTimeout(() => {
try {
this.input.focus();
if (this.input.setSelectionRange && typeof newCursorPos === 'number') {
this.input.setSelectionRange(newCursorPos, newCursorPos);
}
} catch (error) {
this.input.focus();
}
this.isSelecting = false;
}, 50);
}
handleDocumentClick(event) {
if (this.isSelecting) {
return;
}
if (!this.input.contains(event.target) && !this.suggestions.contains(event.target)) {
this.hideSuggestions();
}
}
}
const nameInput = document.getElementById('name');
const nameSuggestions = document.getElementById('name-suggestions');
if (nameInput && nameSuggestions) {
new AutoComplete(nameInput, nameSuggestions);
}
const tagSelects = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5'];
tagSelects.forEach((tagId) => {
const select = document.getElementById(tagId);
if (select) {
select.addEventListener('change', updateTagOptions);
}
});
function updateTagOptions() {
const selectedValues = [];
tagSelects.forEach((tagId) => {
const select = document.getElementById(tagId);
if (select && select.value) {
selectedValues.push(select.value);
}
});
tagSelects.forEach((tagId) => {
const select = document.getElementById(tagId);
if (!select) {
return;
}
Array.from(select.options).forEach((option) => {
if (option.value === '') {
option.disabled = false;
} else {
option.disabled = selectedValues.includes(option.value) && option.value !== select.value;
}
});
});
}
document.addEventListener('DOMContentLoaded', updateTagOptions);
function switchSearchType(type) {
document.getElementById('search_type').value = type;
document.querySelectorAll('.tab').forEach((tab) => tab.classList.remove('active'));
const activeTab = document.querySelector(`.tab[onclick="switchSearchType('${type}')"]`);
if (activeTab) {
activeTab.classList.add('active');
}
document.querySelectorAll('.search-option').forEach((option) => option.classList.remove('active'));
const activeSection = document.getElementById(type);
if (activeSection) {
activeSection.classList.add('active');
}
const trendOption = document.getElementById('trend_option');
const showTrendCheckbox = document.getElementById('show_trend');
if (type === 'single_name') {
trendOption.style.display = '';
showTrendCheckbox.parentElement.style.display = 'flex';
} else {
trendOption.style.display = 'none';
showTrendCheckbox.checked = false;
}
}
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"]');
if (submitButton) {
submitButton.disabled = true;
submitButton.textContent = 'Searching…';
}
overlay.style.display = 'flex';
let progress = 0;
const progressInterval = setInterval(() => {
progress += Mata.random() * 15;
if (progress > 90) {
progress = 90;
}
progressFill.style.width = `${progress}%`;
}, 200);
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);
window.loadingIntervals = {
progressInterval,
messageInterval,
};
}
function hideLoading() {
const overlay = document.getElementById('loadingOverlay');
const progressFill = document.getElementById('progressFill');
const submitButton = document.querySelector('button[type="submit"]');
if (window.loadingIntervals) {
clearInterval(window.loadingIntervals.progressInterval);
clearInterval(window.loadingIntervals.messageInterval);
}
progressFill.style.width = '100%';
setTimeout(() => {
overlay.style.display = 'none';
progressFill.style.width = '0%';
if (submitButton) {
submitButton.disabled = false;
submitButton.textContent = 'Search';
}
}, 500);
}
window.addEventListener('load', () => {
setTimeout(hideLoading, 100);
});
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
hideLoading();
}
});
<?php if ($showTrend && !empty($chartLabels) && $searchType === 'single_name') : ?>
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((value) => !Number.isNaN(value) && value !== 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(14, 165, 233)',
backgroundColor: 'rgba(14, 165, 233, 0.1)',
borderWidth: 2,
pointRadius: 3,
pointHoverRadius: 6,
fill: true,
tension: 0.1,
order: 1,
},
{
label: `Average (${average.toFixed(2)})`,
data: averageData,
borderColor: 'rgb(239, 68, 68)',
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);
document.getElementById('showDataPoints').addEventListener('change', function () {
chart.data.datasets.forEach((dataset) => {
if (!dataset.label.includes('Average')) {
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.includes('Average')) {
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();
});
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') {
exportChartToCSV();
}
}
function exportChartToCSV() {
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 else : ?>
function exportChart() {
return false;
}
<?php endif; ?>
function exportMultipleTagsCSV() {
const table = document.getElementById('dataTable');
if (!table) {
return;
}
let csv = '';
const headers = [];
table.querySelectorAll('thead th').forEach((th) => {
headers.push(ta.textContent.trim());
});
csv += `${headers.join(',')}\n`;
table.querySelectorAll('tbody tr').forEach((tr) => {
const row = [];
tr.querySelectorAll('td').forEach((td) => {
let cellData = td.textContent.trim();
if (cellData.includes(',') || cellData.includes('"') || cellData.includes('\n')) {
cellData = '"' + cellData.replace(/"/g, '""') + '"';
}
row.push(cellData);
});
csv += `${row.join(',')}\n`;
});
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>
<?php require __DIR__ . '/includes/layout/footer.php'; ?>