- Updated session variables to use 'authorization' array instead of 'username' for user identification across multiple files. - Introduced a new function `getUserPermissions` to consolidate user permissions retrieval based on assigned roles. - Modified API calls to use the new authorization structure and updated endpoints to v2. - Enhanced language support by adding 'PL' to the list of supported languages. - Cleaned up redundant code and improved session management during user login and registration processes. - Added a new API endpoint for fetching user permissions based on user ID.
512 lines
21 KiB
PHP
512 lines
21 KiB
PHP
<?php
|
|
defined(page_security_key) or exit;
|
|
|
|
//SET ORIGIN FOR NAVIGATION
|
|
$_SESSION['prev_origin'] = $_SERVER['REQUEST_URI'];
|
|
$page = $_SESSION['origin'] = 'report_builder';
|
|
|
|
//Check if allowed
|
|
if (isAllowed($page, $_SESSION['authorization']['profile'], $_SESSION['authorization']['permission'], 'R') === 0) {
|
|
header('location: index.php');
|
|
exit;
|
|
}
|
|
|
|
// Create bearer token for API calls
|
|
$bearertoken = createCommunicationToken($_SESSION['authorization']['userkey']);
|
|
|
|
// Include settings for baseurl
|
|
include './settings/settings_redirector.php';
|
|
|
|
template_header('Report Builder', 'report_builder', 'view');
|
|
|
|
$view = '
|
|
<p id="servicetoken" hidden>'.$bearertoken.'</p>
|
|
|
|
<div class="content-title responsive-flex-wrap responsive-pad-bot-3">
|
|
<h2 class="responsive-width-100"><i class="fa-solid fa-chart-bar"></i> ' . ($report_builder_title ?? 'Report Builder') . '</h2>
|
|
</div>
|
|
|
|
<!-- Query Builder Section -->
|
|
<div class="content-block" id="query-builder">
|
|
<div id="query-builder-header" style="display:none;padding:15px 20px;cursor:pointer;border-bottom:1px solid #f0f1f2;" onclick="toggleQueryBuilder()">
|
|
<span style="font-weight:600;color:#555;"><i class="fa-solid fa-chevron-down" id="query-builder-icon"></i> ' . ($report_builder_title ?? 'Query Builder') . '</span>
|
|
<span id="current-query-info" style="margin-left:15px;color:#6b788c;font-size:13px;"></span>
|
|
</div>
|
|
<div id="query-builder-content" style="display:flex;flex-wrap:wrap;gap:20px;padding:20px;">
|
|
<!-- Left Column: Table & Columns -->
|
|
<div style="flex:1;min-width:250px;">
|
|
<label for="query-table" style="display:block;margin-bottom:8px;font-weight:600;">' . ($report_builder_select_table ?? 'Select Table') . '</label>
|
|
<select id="query-table" style="width:100%;padding:10px;border:0;border-bottom:1px solid #dedfe1;margin-bottom:20px;">
|
|
<option value="">' . ($report_builder_choose_table ?? 'Choose a table...') . '</option>
|
|
</select>
|
|
|
|
<label for="query-columns" style="display:block;margin-bottom:8px;font-weight:600;">' . ($report_builder_select_columns ?? 'Select Columns') . ' <span style="font-weight:normal;color:#6b788c;font-size:12px;">(Ctrl/Cmd + click)</span></label>
|
|
<select id="query-columns" multiple style="width:100%;height:180px;padding:5px;border:1px solid #dedfe1;border-radius:4px;">
|
|
<option value="">' . ($report_builder_select_table_first ?? 'Select a table first...') . '</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Middle Column: Filters -->
|
|
<div style="flex:1;min-width:300px;">
|
|
<label style="display:block;margin-bottom:8px;font-weight:600;">' . ($report_builder_filters ?? 'Filters') . ' <span style="font-weight:normal;color:#6b788c;font-size:12px;">(WHERE)</span></label>
|
|
<div id="filters-container" style="min-height:100px;background:#f9fafb;border-radius:4px;padding:10px;margin-bottom:10px;"></div>
|
|
<button class="btn green small" onclick="addFilter()"><i class="fa-solid fa-plus"></i> ' . ($report_builder_add_filter ?? 'Add Filter') . '</button>
|
|
</div>
|
|
|
|
<!-- Right Column: Options -->
|
|
<div style="flex:0 0 200px;min-width:150px;">
|
|
<label for="query-order" style="display:block;margin-bottom:8px;font-weight:600;">' . ($report_builder_order_by ?? 'Order By') . '</label>
|
|
<select id="query-order" style="width:100%;padding:10px;border:0;border-bottom:1px solid #dedfe1;margin-bottom:15px;">
|
|
<option value="">' . ($report_builder_none ?? 'None') . '</option>
|
|
</select>
|
|
|
|
<label for="query-sort" style="display:block;margin-bottom:8px;font-weight:600;">' . ($report_builder_sort_direction ?? 'Sort') . '</label>
|
|
<select id="query-sort" style="width:100%;padding:10px;border:0;border-bottom:1px solid #dedfe1;margin-bottom:15px;">
|
|
<option value="ASC">' . ($report_builder_ascending ?? 'Ascending') . '</option>
|
|
<option value="DESC">' . ($report_builder_descending ?? 'Descending') . '</option>
|
|
</select>
|
|
|
|
<label for="query-limit" style="display:block;margin-bottom:8px;font-weight:600;">' . ($report_builder_limit ?? 'Limit') . '</label>
|
|
<input type="number" id="query-limit" value="100" min="1" max="5000" style="width:100%;padding:10px;border:0;border-bottom:1px solid #dedfe1;">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div id="query-builder-actions" style="padding:0 20px 20px 20px;display:flex;gap:10px;flex-wrap:wrap;align-items:center;">
|
|
<button class="btn" onclick="executeBuilderQuery()"><i class="fa-solid fa-play"></i> ' . ($report_builder_run ?? 'Run Query') . '</button>
|
|
<button class="btn alt" onclick="clearAll()"><i class="fa-solid fa-eraser"></i> ' . ($report_builder_clear ?? 'Clear') . '</button>
|
|
<span style="flex:1;"></span>
|
|
<a href="#" onclick="toggleAdvanced(event)" style="color:#6b788c;font-size:13px;"><i class="fa-solid fa-code"></i> ' . ($report_builder_advanced ?? 'Advanced SQL') . '</a>
|
|
</div>
|
|
|
|
<!-- Advanced SQL (hidden by default) -->
|
|
<div id="advanced-sql" style="display:none;border-top:1px solid #f0f1f2;padding:20px;">
|
|
<label for="sql-query" style="display:block;margin-bottom:8px;font-weight:600;">' . ($report_builder_sql_query ?? 'SQL Query') . '</label>
|
|
<textarea id="sql-query" placeholder="SELECT * FROM table_name WHERE condition LIMIT 100" style="width:100%;font-family:monospace;min-height:100px;padding:10px;border:1px solid #dedfe1;border-radius:4px;margin-bottom:10px;"></textarea>
|
|
<button class="btn" onclick="executeAdvancedQuery()"><i class="fa-solid fa-play"></i> ' . ($report_builder_execute ?? 'Execute') . '</button>
|
|
<span style="margin-left:15px;color:#6b788c;font-size:12px;"><i class="fa-solid fa-info-circle"></i> ' . ($report_builder_tip1 ?? 'Only SELECT queries are allowed') . '</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading Indicator -->
|
|
<div id="query-loading" style="display:none;text-align:center;padding:30px;">
|
|
<div class="loader" style="margin:0 auto 15px;"></div>
|
|
<p>' . ($report_builder_loading ?? 'Loading...') . '</p>
|
|
</div>
|
|
|
|
<!-- Results Container -->
|
|
<div id="results"></div>
|
|
|
|
<script>
|
|
// API base URL (from PHP settings)
|
|
var link = "'.$baseurl.'";
|
|
|
|
// Get bearer token from hidden element
|
|
function getAuthHeader() {
|
|
const token = document.getElementById("servicetoken").textContent;
|
|
return {
|
|
"Authorization": "Bearer " + token,
|
|
"Content-Type": "application/json"
|
|
};
|
|
}
|
|
|
|
// Toggle advanced SQL section
|
|
function toggleAdvanced(e) {
|
|
e.preventDefault();
|
|
const adv = document.getElementById("advanced-sql");
|
|
adv.style.display = adv.style.display === "none" ? "block" : "none";
|
|
}
|
|
|
|
// Toggle query builder collapse/expand
|
|
function toggleQueryBuilder() {
|
|
const content = document.getElementById("query-builder-content");
|
|
const actions = document.getElementById("query-builder-actions");
|
|
const advanced = document.getElementById("advanced-sql");
|
|
const icon = document.getElementById("query-builder-icon");
|
|
|
|
if (content.style.display === "none") {
|
|
content.style.display = "flex";
|
|
actions.style.display = "flex";
|
|
icon.className = "fa-solid fa-chevron-down";
|
|
} else {
|
|
content.style.display = "none";
|
|
actions.style.display = "none";
|
|
advanced.style.display = "none";
|
|
icon.className = "fa-solid fa-chevron-right";
|
|
}
|
|
}
|
|
|
|
// Collapse query builder and show header
|
|
function collapseQueryBuilder(query) {
|
|
const header = document.getElementById("query-builder-header");
|
|
const content = document.getElementById("query-builder-content");
|
|
const actions = document.getElementById("query-builder-actions");
|
|
const advanced = document.getElementById("advanced-sql");
|
|
const icon = document.getElementById("query-builder-icon");
|
|
const info = document.getElementById("current-query-info");
|
|
|
|
header.style.display = "block";
|
|
content.style.display = "none";
|
|
actions.style.display = "none";
|
|
advanced.style.display = "none";
|
|
icon.className = "fa-solid fa-chevron-right";
|
|
|
|
// Show truncated query info
|
|
const table = document.getElementById("query-table").value;
|
|
info.textContent = table ? "Table: " + table : "";
|
|
}
|
|
|
|
// Initialize the application
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
loadTables();
|
|
|
|
document.getElementById("query-table").addEventListener("change", function() {
|
|
loadColumns();
|
|
});
|
|
});
|
|
|
|
// Load tables from database
|
|
async function loadTables() {
|
|
try {
|
|
const response = await fetch(link + "/v2/report_builder/action=getTables", {
|
|
method: "GET",
|
|
headers: getAuthHeader()
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
const select = document.getElementById("query-table");
|
|
select.innerHTML = \'<option value="">' . ($report_builder_choose_table ?? 'Choose a table...') . '</option>\';
|
|
data.tables.forEach(table => {
|
|
const option = document.createElement("option");
|
|
option.value = table;
|
|
option.textContent = table;
|
|
select.appendChild(option);
|
|
});
|
|
} else {
|
|
showError(data.message || "Failed to load tables");
|
|
}
|
|
} catch (error) {
|
|
showError("' . ($report_builder_error_load_tables ?? 'Failed to load tables') . ': " + error.message);
|
|
}
|
|
}
|
|
|
|
// Load columns for selected table
|
|
async function loadColumns() {
|
|
const table = document.getElementById("query-table").value;
|
|
const columnsSelect = document.getElementById("query-columns");
|
|
const orderSelect = document.getElementById("query-order");
|
|
|
|
if (!table) {
|
|
columnsSelect.innerHTML = \'<option value="">' . ($report_builder_select_table_first ?? 'Select a table first...') . '</option>\';
|
|
orderSelect.innerHTML = \'<option value="">' . ($report_builder_none ?? 'None') . '</option>\';
|
|
document.getElementById("filters-container").innerHTML = "";
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(link + "/v2/report_builder/action=getColumns&table=" + encodeURIComponent(table), {
|
|
method: "GET",
|
|
headers: getAuthHeader()
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
// Populate columns select
|
|
columnsSelect.innerHTML = "";
|
|
const allOption = document.createElement("option");
|
|
allOption.value = "*";
|
|
allOption.textContent = "* (' . ($report_builder_all_columns ?? 'All Columns') . ')";
|
|
allOption.selected = true;
|
|
columnsSelect.appendChild(allOption);
|
|
|
|
data.columns.forEach(column => {
|
|
const option = document.createElement("option");
|
|
option.value = column;
|
|
option.textContent = column;
|
|
columnsSelect.appendChild(option);
|
|
});
|
|
|
|
// Populate order by select
|
|
orderSelect.innerHTML = \'<option value="">' . ($report_builder_none ?? 'None') . '</option>\';
|
|
data.columns.forEach(column => {
|
|
const option = document.createElement("option");
|
|
option.value = column;
|
|
option.textContent = column;
|
|
orderSelect.appendChild(option);
|
|
});
|
|
|
|
// Clear filters
|
|
document.getElementById("filters-container").innerHTML = "";
|
|
} else {
|
|
showError(data.message || "Failed to load columns");
|
|
}
|
|
} catch (error) {
|
|
showError("' . ($report_builder_error_load_columns ?? 'Failed to load columns') . ': " + error.message);
|
|
}
|
|
}
|
|
|
|
// Execute query builder query
|
|
function executeBuilderQuery() {
|
|
const table = document.getElementById("query-table").value;
|
|
const columnsSelect = document.getElementById("query-columns");
|
|
const selectedColumns = Array.from(columnsSelect.selectedOptions).map(opt => opt.value);
|
|
const orderBy = document.getElementById("query-order").value;
|
|
const sortDir = document.getElementById("query-sort").value;
|
|
const limit = document.getElementById("query-limit").value;
|
|
|
|
if (!table) {
|
|
showError("' . ($report_builder_error_select_table ?? 'Please select a table') . '");
|
|
return;
|
|
}
|
|
|
|
if (selectedColumns.length === 0) {
|
|
showError("' . ($report_builder_error_select_column ?? 'Please select at least one column') . '");
|
|
return;
|
|
}
|
|
|
|
const columns = selectedColumns.join(", ");
|
|
let query = `SELECT ${columns} FROM ${table}`;
|
|
|
|
// Add filters
|
|
const filters = getFilters();
|
|
if (filters.length > 0) {
|
|
query += " WHERE " + filters.join(" AND ");
|
|
}
|
|
|
|
// Add order by
|
|
if (orderBy) {
|
|
query += ` ORDER BY ${orderBy} ${sortDir}`;
|
|
}
|
|
|
|
// Add limit
|
|
query += ` LIMIT ${limit || 100}`;
|
|
|
|
executeQuery(query);
|
|
}
|
|
|
|
// Execute advanced query
|
|
function executeAdvancedQuery() {
|
|
const query = document.getElementById("sql-query").value.trim();
|
|
|
|
if (!query) {
|
|
showError("' . ($report_builder_error_enter_query ?? 'Please enter a SQL query') . '");
|
|
return;
|
|
}
|
|
|
|
if (!query.toLowerCase().startsWith("select")) {
|
|
showError("' . ($report_builder_error_select_only ?? 'Only SELECT queries are allowed') . '");
|
|
return;
|
|
}
|
|
|
|
executeQuery(query);
|
|
}
|
|
|
|
// Get filters from query builder
|
|
function getFilters() {
|
|
const filters = [];
|
|
const filterRows = document.querySelectorAll(".filter-row");
|
|
|
|
filterRows.forEach(row => {
|
|
const column = row.querySelector(".filter-column").value;
|
|
const operator = row.querySelector(".filter-operator").value;
|
|
const value = row.querySelector(".filter-value").value;
|
|
|
|
if (column && value) {
|
|
if (operator === "LIKE") {
|
|
filters.push(`${column} LIKE \'%${value}%\'`);
|
|
} else {
|
|
filters.push(`${column} ${operator} \'${value}\'`);
|
|
}
|
|
}
|
|
});
|
|
|
|
return filters;
|
|
}
|
|
|
|
// Add filter row
|
|
function addFilter() {
|
|
const table = document.getElementById("query-table").value;
|
|
if (!table) {
|
|
showError("' . ($report_builder_error_select_table_first ?? 'Please select a table first') . '");
|
|
return;
|
|
}
|
|
|
|
const container = document.getElementById("filters-container");
|
|
const filterRow = document.createElement("div");
|
|
filterRow.className = "filter-row";
|
|
filterRow.style.cssText = "display:flex;gap:8px;margin-bottom:8px;align-items:center;flex-wrap:wrap;";
|
|
|
|
const columnsSelect = document.getElementById("query-columns");
|
|
const columns = Array.from(columnsSelect.options).map(opt => opt.value).filter(v => v && v !== "*");
|
|
|
|
filterRow.innerHTML = `
|
|
<select class="filter-column" style="flex:1;min-width:100px;padding:8px;border:1px solid #dedfe1;border-radius:4px;">
|
|
${columns.map(col => `<option value="${col}">${col}</option>`).join("")}
|
|
</select>
|
|
<select class="filter-operator" style="width:70px;padding:8px;border:1px solid #dedfe1;border-radius:4px;">
|
|
<option value="=">=</option>
|
|
<option value="!=">!=</option>
|
|
<option value=">">></option>
|
|
<option value="<"><</option>
|
|
<option value=">=">>=</option>
|
|
<option value="<="><=</option>
|
|
<option value="LIKE">LIKE</option>
|
|
</select>
|
|
<input type="text" class="filter-value" placeholder="Value" style="flex:1;min-width:100px;padding:8px;border:1px solid #dedfe1;border-radius:4px;">
|
|
<button class="btn red small" onclick="this.parentElement.remove()" style="padding:8px 12px;"><i class="fa-solid fa-times"></i></button>
|
|
`;
|
|
|
|
container.appendChild(filterRow);
|
|
}
|
|
|
|
// Execute query via POST
|
|
async function executeQuery(query) {
|
|
showLoading();
|
|
|
|
try {
|
|
const response = await fetch(link + "/v2/report_builder", {
|
|
method: "POST",
|
|
headers: getAuthHeader(),
|
|
body: JSON.stringify({
|
|
action: "executeQuery",
|
|
query: query
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
hideLoading();
|
|
|
|
if (data.success) {
|
|
displayResults(data.results, query, data.rowCount);
|
|
} else {
|
|
showError(data.message || "Query execution failed");
|
|
}
|
|
} catch (error) {
|
|
hideLoading();
|
|
showError("' . ($report_builder_error_execute ?? 'Failed to execute query') . ': " + error.message);
|
|
}
|
|
}
|
|
|
|
// Display results
|
|
function displayResults(results, query, rowCount) {
|
|
const resultsDiv = document.getElementById("results");
|
|
|
|
if (!results || results.length === 0) {
|
|
resultsDiv.innerHTML = \'<div class="msg" style="margin-top:20px;background-color:#d1ecf1;border-left:4px solid #0c5460;color:#0c5460;"><i class="fa-solid fa-info-circle"></i><p>' . ($report_builder_no_results ?? 'No results found') . '</p></div>\';
|
|
return;
|
|
}
|
|
|
|
// Table
|
|
const columns = Object.keys(results[0]);
|
|
let tableHtml = `
|
|
<div class="content-block" style="margin-top:20px;">
|
|
<div style="padding:15px;border-bottom:1px solid #f0f1f2;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;">
|
|
<div style="flex:1;min-width:200px;">
|
|
<strong>' . ($report_builder_query ?? 'Query') . ':</strong> <code style="font-family:monospace;background:#f8fafc;padding:5px 10px;border-radius:3px;display:inline-block;margin-top:5px;word-break:break-all;">${escapeHtml(query)}</code>
|
|
</div>
|
|
<button class="btn green" onclick="exportToCSV()"><i class="fa-solid fa-download"></i> </button>
|
|
</div>
|
|
<div class="table">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
${columns.map(col => `<th>${escapeHtml(col)}</th>`).join("")}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
`;
|
|
|
|
results.forEach(row => {
|
|
tableHtml += "<tr>";
|
|
columns.forEach(col => {
|
|
tableHtml += `<td>${escapeHtml(String(row[col] ?? ""))}</td>`;
|
|
});
|
|
tableHtml += "</tr>";
|
|
});
|
|
|
|
tableHtml += `
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
resultsDiv.innerHTML = tableHtml;
|
|
window.currentResults = results;
|
|
|
|
// Collapse query builder after showing results
|
|
collapseQueryBuilder(query);
|
|
}
|
|
|
|
// Export to CSV
|
|
function exportToCSV() {
|
|
if (!window.currentResults || window.currentResults.length === 0) {
|
|
showError("' . ($report_builder_error_no_export ?? 'No results to export') . '");
|
|
return;
|
|
}
|
|
|
|
const results = window.currentResults;
|
|
const columns = Object.keys(results[0]);
|
|
|
|
let csv = columns.join(",") + "\\n";
|
|
|
|
results.forEach(row => {
|
|
const values = columns.map(col => {
|
|
const value = String(row[col] ?? "");
|
|
return \'"\' + value.replace(/"/g, \'"""\') + \'"\';
|
|
});
|
|
csv += values.join(",") + "\\n";
|
|
});
|
|
|
|
const blob = new Blob([csv], { type: "text/csv" });
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = "report_" + new Date().toISOString().slice(0,10) + ".csv";
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
}
|
|
|
|
// Utility functions
|
|
function showLoading() {
|
|
document.getElementById("query-loading").style.display = "block";
|
|
document.getElementById("results").innerHTML = "";
|
|
}
|
|
|
|
function hideLoading() {
|
|
document.getElementById("query-loading").style.display = "none";
|
|
}
|
|
|
|
function showError(message) {
|
|
document.getElementById("results").innerHTML = `<div class="msg error" style="margin-top:20px;"><i class="fa-solid fa-exclamation-circle"></i><p><strong>' . ($report_builder_error ?? 'Error') . ':</strong> ${escapeHtml(message)}</p></div>`;
|
|
}
|
|
|
|
function clearAll() {
|
|
document.getElementById("query-table").value = "";
|
|
document.getElementById("query-columns").innerHTML = \'<option value="">' . ($report_builder_select_table_first ?? 'Select a table first...') . '</option>\';
|
|
document.getElementById("query-order").innerHTML = \'<option value="">' . ($report_builder_none ?? 'None') . '</option>\';
|
|
document.getElementById("query-limit").value = "100";
|
|
document.getElementById("filters-container").innerHTML = "";
|
|
document.getElementById("sql-query").value = "";
|
|
document.getElementById("results").innerHTML = "";
|
|
window.currentResults = null;
|
|
|
|
// Expand query builder
|
|
document.getElementById("query-builder-header").style.display = "none";
|
|
document.getElementById("query-builder-content").style.display = "flex";
|
|
document.getElementById("query-builder-actions").style.display = "flex";
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement("div");
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
</script>
|
|
';
|
|
|
|
echo $view;
|
|
template_footer();
|