Files
trf_certest/public/userarea/import_json.php
T
2026-06-13 20:19:36 +03:00

829 lines
34 KiB
PHP

<?php
include('include/headscript.php');
// Check if a valid template ID has been provided
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Invalid ID"));
exit;
}
$id = intval($_GET['id']);
// Load template
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("SELECT * FROM excel_templates WHERE id = ?");
$stmt->execute([$id]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$template) {
header("Location: template_dashboard.php?status=error&message=" . urlencode("Template not found"));
exit;
}
// Check mappings
$stmt = $pdo->prepare("SELECT id FROM template_mapping WHERE template_id = ?");
$stmt->execute([$id]);
$hasMappings = $stmt->fetch(PDO::FETCH_ASSOC);
error_log("Loaded JSON import template: " . print_r($template, true));
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.top-scrollbar {
overflow-x: auto;
overflow-y: hidden;
width: 100%;
height: 18px;
margin-bottom: 8px;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
background: #f8f9fa;
display: none;
}
.top-scrollbar-inner {
height: 1px;
}
.table-container {
overflow-x: auto;
max-width: 100%;
margin-bottom: 20px;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 10px;
text-align: left;
border: 1px solid #dee2e6;
min-width: 120px;
max-width: 260px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
.table th:first-child,
.table td:first-child {
min-width: 50px;
max-width: 50px;
}
.table th {
background-color: #f8f9fa;
position: relative;
cursor: col-resize;
user-select: none;
}
.table th .resize-handle {
position: absolute;
top: 0;
right: 0;
width: 5px;
height: 100%;
cursor: col-resize;
background: transparent;
}
.table th .resize-handle:hover {
background: #007bff;
}
.loader {
display: none;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 10px auto;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.column-filters th {
background: #ffffff;
cursor: default;
}
.column-filters input {
width: 100%;
min-width: 80px;
}
.json-code-input {
font-size: 1.15rem;
min-height: 48px;
}
.json-paste-area {
min-height: 260px;
font-family: Consolas, Monaco, monospace;
font-size: 13px;
}
.json-help-box {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 12px 14px;
font-size: 13px;
}
.source-badge {
font-size: 12px;
}
</style>
<title><?= htmlspecialchars($template['name']) ?> - JSON Import - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<?php include('top_stat_widget.php'); ?>
<div class="mb-3 text">
<a href="imported.php?id=<?= $id ?>" class="btn btn-warning me-2">Imported (i)</a>
<a href="tolims.php?id=<?= $id ?>" class="btn btn-success me-2">To LIMS (l)</a>
<a href="bindings_manage.php?template_id=<?= $id ?>" class="btn btn-outline-secondary">Binding JSON -> LIMS</a>
</div>
<div class="card radius-10">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<div>
<h6 class="mb-0"><?= htmlspecialchars($template['name']) ?> - JSON Import</h6>
<small>
Template ID: <?= $id ?>
</small>
</div>
<span class="badge bg-info text-dark">JSON mode</span>
</div>
</div>
<div class="card-body">
<?php if (!$hasMappings): ?>
<div class="alert alert-warning" role="alert">
Nessun mapping trovato per questo template. Configura i mapping prima di procedere.
</div>
<?php endif; ?>
<div class="json-help-box mb-3">
<strong>Flusso:</strong> inserisci/scansiona un codice per recuperare il JSON da API, oppure incolla manualmente un JSON nel secondo tab.
Ogni JSON aggiunto diventa una riga della tabella di preview. Quando hai finito, seleziona le righe e clicca <strong>Prosegui</strong>.
</div>
<ul class="nav nav-tabs" id="jsonImportTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="api-code-tab" data-bs-toggle="tab" data-bs-target="#api-code-pane" type="button" role="tab" aria-controls="api-code-pane" aria-selected="true">
Code / Barcode
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="paste-json-tab" data-bs-toggle="tab" data-bs-target="#paste-json-pane" type="button" role="tab" aria-controls="paste-json-pane" aria-selected="false">
Paste JSON
</button>
</li>
</ul>
<div class="tab-content border border-top-0 p-3 mb-3" id="jsonImportTabsContent">
<div class="tab-pane fade show active" id="api-code-pane" role="tabpanel" aria-labelledby="api-code-tab" tabindex="0">
<form id="jsonCodeForm" class="row g-3 align-items-end">
<div class="col-lg-8">
<label for="json_code" class="form-label">Code / Barcode</label>
<input type="text" class="form-control json-code-input" id="json_code" name="json_code" placeholder="Write or scan code" autocomplete="off" <?= !$hasMappings ? 'disabled' : '' ?>>
<small class="text-muted">Lo scanner barcode normalmente scrive qui il codice e invia Enter.</small>
</div>
<div class="col-lg-4 d-flex gap-2">
<button type="submit" class="btn btn-primary flex-fill" <?= !$hasMappings ? 'disabled' : '' ?>>Load JSON</button>
<button type="button" class="btn btn-outline-secondary" id="clearCodeBtn">Clear</button>
</div>
</form>
</div>
<div class="tab-pane fade" id="paste-json-pane" role="tabpanel" aria-labelledby="paste-json-tab" tabindex="0">
<form id="pasteJsonForm">
<div class="mb-3">
<label for="manual_json_reference" class="form-label">Reference / filename</label>
<input type="text" class="form-control" id="manual_json_reference" placeholder="Optional reference, e.g. manual-json-001">
</div>
<div class="mb-3">
<label for="manual_json" class="form-label">Paste JSON</label>
<textarea class="form-control json-paste-area" id="manual_json" placeholder='{"data":[{"id":"MM000620","attributes":{"trf_type":"apparel"}}]}' <?= !$hasMappings ? 'disabled' : '' ?>></textarea>
</div>
<button type="submit" class="btn btn-primary" <?= !$hasMappings ? 'disabled' : '' ?>>Add pasted JSON</button>
<button type="button" class="btn btn-outline-secondary" id="clearManualJsonBtn">Clear</button>
</form>
</div>
</div>
<div class="loader" id="loader"></div>
<div id="errorContainer" class="alert alert-danger mt-3" style="display:none;"></div>
<div id="successContainer" class="alert alert-success mt-3" style="display:none;"></div>
<div id="tableContainer"></div>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
</div>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.min.js"></script>
<?php include('jsinclude.php'); ?>
<script>
document.addEventListener("DOMContentLoaded", function() {
const TEMPLATE_ID = <?= (int)$id ?>;
const API_ENDPOINT = 'get_json_by_code.php';
const INCLUDE_SOURCE_CODE_COLUMN = true;
const UNWRAP_SINGLE_DATA_ITEM = true;
const jsonCodeForm = document.getElementById('jsonCodeForm');
const pasteJsonForm = document.getElementById('pasteJsonForm');
const jsonCodeInput = document.getElementById('json_code');
const manualJsonInput = document.getElementById('manual_json');
const manualJsonReferenceInput = document.getElementById('manual_json_reference');
const clearCodeBtn = document.getElementById('clearCodeBtn');
const clearManualJsonBtn = document.getElementById('clearManualJsonBtn');
const loader = document.getElementById('loader');
const errorContainer = document.getElementById('errorContainer');
const successContainer = document.getElementById('successContainer');
const tableContainer = document.getElementById('tableContainer');
let jsonRows = [];
let columns = [];
if (jsonCodeInput && !jsonCodeInput.disabled) {
jsonCodeInput.focus();
}
jsonCodeForm.addEventListener('submit', function(e) {
e.preventDefault();
const code = jsonCodeInput.value.trim();
if (!code) {
showError('Inserisci o scansiona un codice.');
return;
}
loadJsonFromApi(code);
});
pasteJsonForm.addEventListener('submit', function(e) {
e.preventDefault();
const rawJson = manualJsonInput.value.trim();
const reference = manualJsonReferenceInput.value.trim() || 'manual-json-' + (jsonRows.length + 1);
if (!rawJson) {
showError('Incolla un JSON prima di aggiungerlo.');
return;
}
try {
const parsedJson = JSON.parse(rawJson);
addJsonRow(parsedJson, reference, 'paste');
manualJsonInput.value = '';
manualJsonReferenceInput.value = '';
showSuccess('JSON incollato aggiunto correttamente.');
} catch (err) {
showError('JSON non valido: ' + err.message);
}
});
clearCodeBtn.addEventListener('click', function() {
jsonCodeInput.value = '';
jsonCodeInput.focus();
});
clearManualJsonBtn.addEventListener('click', function() {
manualJsonInput.value = '';
manualJsonReferenceInput.value = '';
});
function loadJsonFromApi(code) {
hideMessages();
loader.style.display = 'block';
fetch(API_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
template_id: TEMPLATE_ID,
code: code
})
})
.then(response => {
if (!response.ok) {
throw new Error('HTTP status ' + response.status);
}
return response.json();
})
.then(responseData => {
loader.style.display = 'none';
if (responseData.error) {
showError(responseData.error);
return;
}
let jsonPayload = responseData;
let reference = code;
// Supported endpoint response format:
// { success: true, json: {...}, reference: "..." }
if (responseData.json !== undefined) {
jsonPayload = responseData.json;
reference = responseData.reference || responseData.filename || responseData.code || code;
}
addJsonRow(jsonPayload, reference, 'api');
jsonCodeInput.value = '';
jsonCodeInput.focus();
showSuccess('JSON recuperato e aggiunto correttamente.');
})
.catch(error => {
loader.style.display = 'none';
showError('Errore durante il recupero del JSON: ' + error.message);
});
}
function addJsonRow(jsonPayload, reference, sourceType) {
// Ogni elemento di data[] diventa una riga (non colonne data.0.*, data.1.*).
const records = extractRecords(jsonPayload);
records.forEach((record, recordIndex) => {
const flattened = flattenJson(record);
const rowReference = records.length > 1 ?
reference + '#' + (recordIndex + 1) :
reference;
if (INCLUDE_SOURCE_CODE_COLUMN) {
flattened.source_code = rowReference;
flattened.source_type = sourceType;
}
const newColumns = Object.keys(flattened).filter(col => !columns.includes(col));
columns = columns.concat(newColumns);
jsonRows.push({
excelrow: 'JSON-' + (jsonRows.length + 1),
reference: rowReference,
sourceType: sourceType,
flat: flattened
});
});
renderTable();
}
// Record da trasformare in righe: gli oggetti in data[], altrimenti il payload stesso.
function extractRecords(payload) {
if (
payload &&
typeof payload === 'object' &&
!Array.isArray(payload) &&
Array.isArray(payload.data) &&
payload.data.length > 0
) {
const items = payload.data.filter(item => item && typeof item === 'object');
if (items.length > 0) {
return items;
}
}
return [payload];
}
function flattenJson(value, prefix = '', result = {}) {
if (value === null || value === undefined) {
result[prefix || 'value'] = '';
return result;
}
if (Array.isArray(value)) {
if (value.length === 0) {
result[prefix || 'value'] = '';
return result;
}
value.forEach((item, index) => {
const newPrefix = prefix ? prefix + '.' + index : String(index);
flattenJson(item, newPrefix, result);
});
return result;
}
if (typeof value === 'object') {
const keys = Object.keys(value);
if (keys.length === 0) {
result[prefix || 'value'] = '';
return result;
}
keys.forEach(key => {
const newPrefix = prefix ? prefix + '.' + key : key;
flattenJson(value[key], newPrefix, result);
});
return result;
}
result[prefix || 'value'] = String(value);
return result;
}
function buildImportData() {
const rows = jsonRows.map(row => columns.map(col => row.flat[col] ?? ''));
const excelData = rows.map((rowData, index) => ({
excelrow: jsonRows[index].excelrow,
data: rowData
}));
return {
template_id: TEMPLATE_ID,
columns: columns,
rows: rows,
excel_data: excelData,
filename: 'json_import_' + new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
};
}
function renderTable() {
if (jsonRows.length === 0) {
tableContainer.innerHTML = '';
return;
}
const data = buildImportData();
let html = `
<form id="selectRowsForm" action="import_insert.php" method="POST">
<input type="hidden" name="template_id" value="${escapeHtml(data.template_id)}">
<input type="hidden" name="columns" value="${encodeURIComponent(JSON.stringify(data.columns))}">
<input type="hidden" name="rows" id="selectedRowsData" value="">
<input type="hidden" name="excelrows" id="selectedExcelRowsData" value="">
<input type="hidden" name="filename" value="${escapeHtml(data.filename)}">
<input type="hidden" name="source_type" value="json">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
<div>
<strong>JSON rows loaded:</strong> ${jsonRows.length}
<span class="badge bg-secondary source-badge ms-2">Columns: ${data.columns.length}</span>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-danger btn-sm" id="clearAllRowsBtn">Clear all rows</button>
<button type="submit" class="btn btn-primary" id="proceedButtonTop" disabled>Prosegui</button>
</div>
</div>
<div class="top-scrollbar" id="topTableScrollbar">
<div class="top-scrollbar-inner" id="topTableScrollbarInner"></div>
</div>
<div class="table-container" id="mainTableContainer">
<table class="table table-striped table-bordered" id="importPreviewTable">
<thead>
<tr>
<th><input type="checkbox" id="selectAll"> Select</th>
${data.columns.map(col => `<th title="${escapeHtml(col)}">${escapeHtml(readableColumnLabel(col))}<div class="resize-handle"></div></th>`).join('')}
<th>Action</th>
</tr>
<tr class="column-filters">
<th></th>
${data.columns.map((col, i) => `
<th>
<input type="text"
class="form-control form-control-sm column-filter"
data-col-index="${i}"
placeholder="Filter...">
</th>
`).join('')}
<th></th>
</tr>
</thead>
<tbody>
${data.excel_data.map((row, index) => `
<tr data-row-index="${index}">
<td><input type="checkbox" class="row-checkbox" name="selected_rows[]" value="${index}" data-excelrow="${escapeHtml(row.excelrow)}"></td>
${row.data.map(cell => `<td title="${escapeHtml(cell)}">${escapeHtml(cell)}</td>`).join('')}
<td><button type="button" class="btn btn-sm btn-outline-danger remove-row-btn" data-row-index="${index}">Remove</button></td>
</tr>
`).join('')}
</tbody>
</table>
</div>
<button type="submit" class="btn btn-primary mt-3" id="proceedButtonBottom" disabled>Prosegui</button>
</form>
`;
tableContainer.innerHTML = html;
bindTableEvents(data);
}
function bindTableEvents(data) {
const selectRowsForm = document.getElementById('selectRowsForm');
const clearAllRowsBtn = document.getElementById('clearAllRowsBtn');
const removeRowButtons = document.querySelectorAll('.remove-row-btn');
selectRowsForm.addEventListener('submit', function(e) {
const checkedBoxes = Array.from(document.querySelectorAll('.row-checkbox:checked'));
if (checkedBoxes.length === 0) {
e.preventDefault();
alert('Seleziona almeno una riga.');
return;
}
const selectedRows = [];
const selectedExcelRows = [];
checkedBoxes.forEach((cb, newIndex) => {
const originalIndex = parseInt(cb.value, 10);
if (data.rows && data.rows[originalIndex]) {
selectedRows.push(data.rows[originalIndex]);
}
if (data.excel_data && data.excel_data[originalIndex]) {
selectedExcelRows.push(data.excel_data[originalIndex].excelrow);
}
// Reindex selected_rows so import_insert.php receives only the reduced rows array
cb.value = newIndex;
});
document.getElementById('selectedRowsData').value =
encodeURIComponent(JSON.stringify(selectedRows));
document.getElementById('selectedExcelRowsData').value =
encodeURIComponent(JSON.stringify(selectedExcelRows));
});
clearAllRowsBtn.addEventListener('click', function() {
if (!confirm('Vuoi rimuovere tutte le righe JSON caricate?')) return;
jsonRows = [];
columns = [];
renderTable();
showSuccess('Righe JSON rimosse.');
jsonCodeInput.focus();
});
removeRowButtons.forEach(button => {
button.addEventListener('click', function() {
const rowIndex = parseInt(this.dataset.rowIndex, 10);
jsonRows.splice(rowIndex, 1);
rebuildColumnsFromRows();
renderTable();
});
});
const topTableScrollbar = document.getElementById('topTableScrollbar');
const topTableScrollbarInner = document.getElementById('topTableScrollbarInner');
const mainTableContainer = document.getElementById('mainTableContainer');
const importPreviewTable = document.getElementById('importPreviewTable');
function updateTopTableScrollbar() {
if (!topTableScrollbar || !topTableScrollbarInner || !mainTableContainer || !importPreviewTable) return;
topTableScrollbarInner.style.width = importPreviewTable.scrollWidth + 'px';
if (mainTableContainer.scrollWidth > mainTableContainer.clientWidth) {
topTableScrollbar.style.display = 'block';
} else {
topTableScrollbar.style.display = 'none';
}
}
let syncingTop = false;
let syncingBottom = false;
if (topTableScrollbar && mainTableContainer) {
topTableScrollbar.addEventListener('scroll', function() {
if (syncingBottom) return;
syncingTop = true;
mainTableContainer.scrollLeft = topTableScrollbar.scrollLeft;
syncingTop = false;
});
mainTableContainer.addEventListener('scroll', function() {
if (syncingTop) return;
syncingBottom = true;
topTableScrollbar.scrollLeft = mainTableContainer.scrollLeft;
syncingBottom = false;
});
}
updateTopTableScrollbar();
setTimeout(updateTopTableScrollbar, 100);
setTimeout(updateTopTableScrollbar, 300);
window.addEventListener('resize', updateTopTableScrollbar);
const proceedButtonTop = document.getElementById('proceedButtonTop');
const proceedButtonBottom = document.getElementById('proceedButtonBottom');
const selectAllCheckbox = document.getElementById('selectAll');
const checkboxes = document.querySelectorAll('.row-checkbox');
function updateProceedButton() {
const enabled = Array.from(document.querySelectorAll('.row-checkbox')).some(cb => cb.checked);
if (proceedButtonTop) proceedButtonTop.disabled = !enabled;
if (proceedButtonBottom) proceedButtonBottom.disabled = !enabled;
}
selectAllCheckbox.addEventListener('change', function() {
const visibleRows = Array.from(document.querySelectorAll('#importPreviewTable tbody tr'))
.filter(row => row.style.display !== 'none');
visibleRows.forEach(row => {
const checkbox = row.querySelector('.row-checkbox');
if (checkbox) {
checkbox.checked = this.checked;
}
});
updateProceedButton();
});
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
const visibleCheckboxes = Array.from(document.querySelectorAll('#importPreviewTable tbody tr'))
.filter(row => row.style.display !== 'none')
.map(row => row.querySelector('.row-checkbox'))
.filter(cb => cb !== null);
selectAllCheckbox.checked =
visibleCheckboxes.length > 0 &&
visibleCheckboxes.every(cb => cb.checked);
updateProceedButton();
});
});
const thElements = document.querySelectorAll('#importPreviewTable th');
thElements.forEach((th, index) => {
if (index === 0) return;
const resizeHandle = th.querySelector('.resize-handle');
if (resizeHandle) {
resizeHandle.addEventListener('mousedown', (e) => {
e.preventDefault();
const startX = e.clientX;
const startWidth = th.offsetWidth;
const onMouseMove = (e) => {
const newWidth = Math.max(60, startWidth + (e.clientX - startX));
th.style.width = `${newWidth}px`;
th.style.minWidth = `${newWidth}px`;
th.style.maxWidth = `${newWidth}px`;
const cells = document.querySelectorAll(`#importPreviewTable td:nth-child(${index + 1})`);
cells.forEach(cell => {
cell.style.width = `${newWidth}px`;
cell.style.minWidth = `${newWidth}px`;
cell.style.maxWidth = `${newWidth}px`;
});
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
}
});
const rows = document.querySelectorAll('#importPreviewTable tbody tr');
const filterInputs = document.querySelectorAll('.column-filter');
const activeFilters = {};
function applyColumnFilters() {
rows.forEach(row => {
let visible = true;
for (const [colIndexStr, filterValue] of Object.entries(activeFilters)) {
const colIndex = parseInt(colIndexStr, 10);
const cell = row.cells[colIndex + 1];
const cellText = (cell?.textContent || '').toLowerCase();
const searchText = (filterValue || '').toLowerCase().trim();
if (searchText && !cellText.includes(searchText)) {
visible = false;
break;
}
}
row.style.display = visible ? '' : 'none';
});
const visibleCheckboxes = Array.from(document.querySelectorAll('#importPreviewTable tbody tr'))
.filter(row => row.style.display !== 'none')
.map(row => row.querySelector('.row-checkbox'))
.filter(cb => cb !== null);
selectAllCheckbox.checked =
visibleCheckboxes.length > 0 &&
visibleCheckboxes.every(cb => cb.checked);
updateProceedButton();
}
filterInputs.forEach(input => {
input.addEventListener('input', function() {
const colIndex = this.dataset.colIndex;
activeFilters[colIndex] = this.value;
applyColumnFilters();
});
});
updateProceedButton();
}
function rebuildColumnsFromRows() {
columns = [];
jsonRows.forEach(row => {
Object.keys(row.flat).forEach(col => {
if (!columns.includes(col)) {
columns.push(col);
}
});
});
}
function readableColumnLabel(columnName) {
if (!columnName) return 'Column without name';
return columnName;
}
function showError(message) {
successContainer.style.display = 'none';
errorContainer.textContent = message;
errorContainer.style.display = 'block';
}
function showSuccess(message) {
errorContainer.style.display = 'none';
successContainer.textContent = message;
successContainer.style.display = 'block';
setTimeout(() => {
successContainer.style.display = 'none';
}, 3500);
}
function hideMessages() {
errorContainer.style.display = 'none';
successContainer.style.display = 'none';
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
});
</script>
</body>
</html>