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.
This commit is contained in:
511
report_builder.php
Normal file
511
report_builder.php
Normal file
@@ -0,0 +1,511 @@
|
||||
<?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=">">></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();
|
||||
Reference in New Issue
Block a user