Files
assetmgt/report_builder.php
“VeLiTi” 782050c3ca Add user role management functionality with CRUD operations and permissions handling
- Created user_role.php for viewing and editing user roles and their permissions.
- Implemented inline editing for role details and permissions.
- Added user_role_manage.php for creating and managing user roles.
- Introduced user_roles.php for listing all user roles with pagination and filtering options.
- Integrated API calls for fetching and updating role data and permissions.
- Enhanced user interface with success messages and navigation controls.
2026-01-19 11:16:54 +01:00

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['profile'], $_SESSION['permission'], 'R') === 0) {
header('location: index.php');
exit;
}
// Create bearer token for API calls
$bearertoken = createCommunicationToken($_SESSION['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=">">&gt;</option>
<option value="<">&lt;</option>
<option value=">=">&gt;=</option>
<option value="<=">&lt;=</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();