json bindings

This commit is contained in:
2026-06-13 20:19:36 +03:00
parent d623ee797c
commit 9eb257d237
10 changed files with 1024 additions and 24 deletions
+246
View File
@@ -0,0 +1,246 @@
<?php
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Filtro opzionale per template.
$templateFilter = isset($_GET['template_id']) ? intval($_GET['template_id']) : 0;
$templates = $pdo->query("SELECT id, name FROM excel_templates ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
$where = '';
$params = [];
if ($templateFilter > 0) {
$where = 'WHERE b.template_id = ?';
$params[] = $templateFilter;
}
$sql = "SELECT b.id, b.template_id, b.mapping_id, b.field_id, b.json_value,
b.lims_value_id, b.lims_value, b.updated_at,
t.name AS template_name,
m.field_label
FROM json_lims_binding b
LEFT JOIN excel_templates t ON t.id = b.template_id
LEFT JOIN template_mapping m ON m.id = b.mapping_id
$where
ORDER BY t.name, m.field_label, b.json_value";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$bindings = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!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/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
<style>
.json-value {
font-family: Consolas, Monaco, monospace;
font-weight: 600;
}
td .select2-container {
min-width: 220px;
}
.row-status {
font-size: 12px;
}
</style>
<title>Gestione Binding JSON -> LIMS - <?= 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">
<div class="card radius-10">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<h6 class="mb-0">Gestione Binding JSON -> LIMS</h6>
<form method="GET" class="d-flex align-items-center gap-2">
<label class="small text-muted mb-0">Template</label>
<select name="template_id" class="form-select form-select-sm" onchange="this.form.submit()">
<option value="0">Tutti</option>
<?php foreach ($templates as $t): ?>
<option value="<?= (int) $t['id'] ?>" <?= $templateFilter === (int) $t['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($t['name']) ?>
</option>
<?php endforeach; ?>
</select>
</form>
</div>
</div>
<div class="card-body">
<div id="globalError" class="alert alert-danger" style="display:none;"></div>
<div class="alert alert-secondary small">
Le modifiche ai binding si applicano alle <strong>importazioni future</strong>.
I record gia' importati non vengono ricalcolati.
</div>
<div class="table-responsive">
<table class="table table-striped table-bordered align-middle">
<thead>
<tr>
<th>Template</th>
<th>Campo (template_mapping)</th>
<th>Valore JSON</th>
<th>Valore LIMS</th>
<th style="width:170px;">Azioni</th>
</tr>
</thead>
<tbody>
<?php if (empty($bindings)): ?>
<tr>
<td colspan="5" class="text-center text-muted">Nessun binding presente.</td>
</tr>
<?php else: ?>
<?php foreach ($bindings as $b): ?>
<tr data-id="<?= (int) $b['id'] ?>"
data-mapping-id="<?= (int) $b['mapping_id'] ?>"
data-field-id="<?= (int) $b['field_id'] ?>"
data-template-id="<?= (int) $b['template_id'] ?>"
data-json-value="<?= htmlspecialchars($b['json_value'], ENT_QUOTES) ?>">
<td><?= htmlspecialchars($b['template_name'] ?? ('#' . $b['template_id'])) ?></td>
<td><?= htmlspecialchars($b['field_label'] ?? ('mapping ' . $b['mapping_id'])) ?></td>
<td class="json-value"><?= htmlspecialchars($b['json_value']) ?></td>
<td>
<select class="form-select binding-select" data-field-id="<?= (int) $b['field_id'] ?>">
<?php if ($b['lims_value_id']): ?>
<option value="<?= (int) $b['lims_value_id'] ?>" selected>
<?= htmlspecialchars($b['lims_value']) ?>
</option>
<?php endif; ?>
</select>
<span class="row-status text-muted"></span>
</td>
<td>
<button type="button" class="btn btn-sm btn-success save-binding-btn" disabled>
<i class="fas fa-save"></i> Salva
</button>
<button type="button" class="btn btn-sm btn-outline-danger delete-binding-btn">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</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>
<?php include('jsinclude.php'); ?>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(function() {
const $globalError = $('#globalError');
$('.binding-select').each(function() {
const fieldId = $(this).data('field-id');
const initialVal = $(this).val();
$(this).select2({
width: '220px',
ajax: {
url: 'search_customfield_values.php',
dataType: 'json',
delay: 200,
data: params => ({
field_id: fieldId,
q: params.term || '',
limit: 50
}),
processResults: data => ({
results: data.results || []
})
},
minimumInputLength: 0
});
// Abilito Salva solo quando il valore cambia davvero.
$(this).data('original-val', initialVal);
});
$('.binding-select').on('change', function() {
const $row = $(this).closest('tr');
const changed = String($(this).val()) !== String($(this).data('original-val'));
$row.find('.save-binding-btn').prop('disabled', !changed);
});
$('.save-binding-btn').on('click', function() {
const $row = $(this).closest('tr');
const $select = $row.find('.binding-select');
const selectedData = $select.select2('data')[0] || {};
const $status = $row.find('.row-status');
const $btn = $(this);
$globalError.hide();
$btn.prop('disabled', true);
$status.text('Salvataggio...').removeClass('text-success text-danger').addClass('text-muted');
$.post('save_binding.php', {
mapping_id: $row.data('mapping-id'),
field_id: $row.data('field-id'),
template_id: $row.data('template-id'),
json_value: String($row.data('json-value')),
lims_value_id: $select.val(),
lims_value: selectedData.text || ''
}).then(resp => {
if (resp && resp.success) {
$status.text('Salvato').removeClass('text-muted').addClass('text-success');
$select.data('original-val', $select.val());
} else {
throw new Error((resp && resp.error) || 'Errore salvataggio');
}
}).catch(err => {
$status.text('Errore').removeClass('text-muted').addClass('text-danger');
$globalError.text(err.message || 'Errore durante il salvataggio.').show();
$btn.prop('disabled', false);
});
});
$('.delete-binding-btn').on('click', function() {
if (!confirm('Eliminare questo binding?')) return;
const $row = $(this).closest('tr');
const $btn = $(this);
$globalError.hide();
$btn.prop('disabled', true);
$.post('delete_binding.php', {
id: $row.data('id')
}).then(resp => {
if (resp && resp.success) {
$row.fadeOut(200, () => $row.remove());
} else {
throw new Error((resp && resp.error) || 'Errore eliminazione');
}
}).catch(err => {
$globalError.text(err.message || 'Errore durante l\'eliminazione.').show();
$btn.prop('disabled', false);
});
});
});
</script>
</body>
</html>
+128
View File
@@ -0,0 +1,128 @@
<?php
// Helpers for JSON -> LIMS value bindings (table json_lims_binding).
function binding_is_list_field(array $mapping): bool
{
$hasList = (int) ($mapping['has_list'] ?? 0) === 1;
$isMultiChoice = strcasecmp(trim((string) ($mapping['data_type'] ?? '')), 'SceltaMultipla') === 0;
return $hasList || $isMultiChoice;
}
function binding_lookup(PDO $pdo, int $mappingId, string $jsonValue): ?array
{
$stmt = $pdo->prepare(
"SELECT id, lims_value_id, lims_value
FROM json_lims_binding
WHERE mapping_id = ? AND json_value = ?
LIMIT 1"
);
$stmt->execute([$mappingId, $jsonValue]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
function binding_upsert(
PDO $pdo,
int $templateId,
int $mappingId,
int $fieldId,
string $jsonValue,
int $limsValueId,
string $limsValue,
?int $createdBy
): void {
$stmt = $pdo->prepare(
"INSERT INTO json_lims_binding
(template_id, mapping_id, field_id, json_value, lims_value_id, lims_value, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
lims_value_id = VALUES(lims_value_id),
lims_value = VALUES(lims_value),
field_id = VALUES(field_id),
template_id = VALUES(template_id)"
);
$stmt->execute([$templateId, $mappingId, $fieldId, $jsonValue, $limsValueId, $limsValue, $createdBy]);
}
// LIMS list values for a field, reusing the cache/customfield_{id}.json cache (1h).
function binding_get_lims_values(int $fieldId): array
{
static $memo = [];
if ($fieldId <= 0) {
return [];
}
if (array_key_exists($fieldId, $memo)) {
return $memo[$fieldId];
}
$cacheDir = dirname(__DIR__) . '/cache';
$cacheFile = $cacheDir . '/customfield_' . $fieldId . '.json';
try {
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
$values = json_decode(file_get_contents($cacheFile), true);
} else {
require_once __DIR__ . '/VisualLimsApiClient.class.php';
$api = VisualLimsApiClient::getInstance();
$data = $api->get("CustomField($fieldId)?\$expand=CustomFieldsValues");
$values = $data['CustomFieldsValues'] ?? [];
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0777, true);
}
file_put_contents($cacheFile, json_encode($values));
}
} catch (Throwable $e) {
error_log("binding_get_lims_values failed for field $fieldId: " . $e->getMessage());
$values = [];
}
if (!is_array($values)) {
$values = [];
}
return $memo[$fieldId] = $values;
}
// Exactly one case-insensitive match by Valore -> that value, otherwise null.
function binding_auto_match(array $limsValues, string $jsonValue): ?array
{
$needle = mb_strtolower(trim($jsonValue));
if ($needle === '') {
return null;
}
$matches = [];
foreach ($limsValues as $v) {
$valore = (string) ($v['Valore'] ?? '');
if (mb_strtolower(trim($valore)) === $needle) {
$matches[] = $v;
}
}
return count($matches) === 1 ? $matches[0] : null;
}
// Scrive il valore risolto sui record indicati (datadb_ids gia' delimita l'import corrente).
function binding_apply_to_details(
PDO $pdo,
int $mappingId,
string $limsValue,
array $datadbIds
): int {
$datadbIds = array_values(array_filter(array_map('intval', $datadbIds)));
if (empty($datadbIds)) {
return 0;
}
$placeholders = implode(',', array_fill(0, count($datadbIds), '?'));
$sql = "UPDATE import_data_details
SET field_value = ?
WHERE mapping_id = ?
AND id IN ($placeholders)";
$params = array_merge([$limsValue, $mappingId], $datadbIds);
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->rowCount();
}
+40
View File
@@ -0,0 +1,40 @@
<?php
// Elimina un binding JSON -> LIMS. Ritorna JSON.
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
include dirname(__DIR__) . '/../extra/auth.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
if (!Auth::check()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
exit;
}
$id = intval($_POST['id'] ?? 0);
if ($id <= 0) {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Missing id']);
exit;
}
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->prepare("DELETE FROM json_lims_binding WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(['success' => true, 'deleted' => $stmt->rowCount()]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
+117 -3
View File
@@ -12,6 +12,7 @@ if (!file_exists(__DIR__ . '/import_debug.log')) {
error_log("Inizio importazione alle " . date('Y-m-d H:i:s'));
include('include/headscript.php');
require_once(__DIR__ . '/class/binding-functions.php');
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['template_id']) || !isset($_POST['selected_rows']) || !isset($_POST['filename'])) {
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Richiesta non valida"));
@@ -61,7 +62,8 @@ $stmt = $pdo->prepare("
field_label,
field_id,
main_field,
auto_value
auto_value,
has_list
FROM template_mapping
WHERE template_id = ?
");
@@ -84,6 +86,13 @@ foreach ($allMappings as $mapping) {
// Inserisci le righe selezionate in datadb
$insertedIds = [];
// Binding JSON -> LIMS senza corrispondenza salvata, per "mapping_id|json_value".
$pendingBindings = [];
// Binding risolti in automatico durante questo import (solo per visualizzazione).
$autoBindings = [];
// Binding gia' salvati in precedenza, usati su questo import (visualizzazione + modifica).
$savedBindings = [];
foreach ($selected_rows as $loopIndex => $rowIndex) {
if ($source_type === 'json') {
@@ -107,6 +116,14 @@ foreach ($selected_rows as $loopIndex => $rowIndex) {
$template = $template_stmt->fetch(PDO::FETCH_ASSOC);
$default_idclient = $template['idclient'] ?? null;
// excelrow e' INT: dal JSON arriva tipo 'JSON-1', tengo solo la parte numerica.
if (is_numeric($excelrow)) {
$excelrowDb = (int) $excelrow;
} else {
$digits = preg_replace('/\D+/', '', (string) $excelrow);
$excelrowDb = $digits !== '' ? (int) $digits : ($loopIndex + 1);
}
$values = [
$template_id,
$importReferenceCode,
@@ -115,7 +132,7 @@ foreach ($selected_rows as $loopIndex => $rowIndex) {
$user_id,
null,
date('Y-m-d'),
$excelrow,
$excelrowDb,
$default_idclient // Aggiungi idclient
];
$sql = "INSERT INTO datadb (templateid, importreferencecode, filename_import, status, user_id, limscode, importdate, excelrow, idclient) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
@@ -227,6 +244,80 @@ foreach ($selected_rows as $loopIndex => $rowIndex) {
}
}
// Binding JSON -> LIMS solo per i campi a lista importati da JSON.
if (
$source_type === 'json'
&& !$mapping['is_manual']
&& binding_is_list_field($mapping)
&& $fieldValue !== null
&& $fieldValue !== ''
) {
$jsonValue = (string) $fieldValue;
$existing = binding_lookup($pdo, (int) $mapping['id'], $jsonValue);
if ($existing) {
$fieldValue = $existing['lims_value'];
$key = $mapping['id'] . '|' . $jsonValue;
if (!isset($savedBindings[$key])) {
$savedBindings[$key] = [
'mapping_id' => (int) $mapping['id'],
'field_id' => (int) $mapping['field_id'],
'field_label' => $mapping['field_label'],
'json_value' => $jsonValue,
'lims_value' => (string) $existing['lims_value'],
'lims_value_id' => (int) $existing['lims_value_id'],
'datadb_ids' => [],
];
}
$savedBindings[$key]['datadb_ids'][] = (int) $iddatadb;
} else {
// Nessun binding salvato: provo l'auto-match 1-a-1 sui valori LIMS.
$limsValues = binding_get_lims_values((int) $mapping['field_id']);
$autoMatch = binding_auto_match($limsValues, $jsonValue);
if ($autoMatch) {
binding_upsert(
$pdo,
(int) $template_id,
(int) $mapping['id'],
(int) $mapping['field_id'],
$jsonValue,
(int) $autoMatch['IdCustomFieldsValue'],
(string) $autoMatch['Valore'],
$user_id
);
$fieldValue = (string) $autoMatch['Valore'];
$key = $mapping['id'] . '|' . $jsonValue;
if (!isset($autoBindings[$key])) {
$autoBindings[$key] = [
'mapping_id' => (int) $mapping['id'],
'field_id' => (int) $mapping['field_id'],
'field_label' => $mapping['field_label'],
'json_value' => $jsonValue,
'lims_value' => (string) $autoMatch['Valore'],
'lims_value_id' => (int) $autoMatch['IdCustomFieldsValue'],
'datadb_ids' => [],
];
}
$autoBindings[$key]['datadb_ids'][] = (int) $iddatadb;
} else {
$key = $mapping['id'] . '|' . $jsonValue;
if (!isset($pendingBindings[$key])) {
$pendingBindings[$key] = [
'mapping_id' => (int) $mapping['id'],
'field_id' => (int) $mapping['field_id'],
'field_label' => $mapping['field_label'],
'json_value' => $jsonValue,
'datadb_ids' => [],
];
}
$pendingBindings[$key]['datadb_ids'][] = (int) $iddatadb;
}
}
}
if ($mapping['is_required'] && (is_null($fieldValue) || $fieldValue === '')) {
error_log("Required field missing for mapping ID: " . $mapping['id'] . ", field: " . $mapping['field_label']);
}
@@ -239,5 +330,28 @@ foreach ($selected_rows as $loopIndex => $rowIndex) {
$_SESSION['inserted_ids'] = $insertedIds;
header("Location: imported.php?id=" . urlencode($template_id) . "&importref=" . urlencode($importReferenceCode));
$importedUrl = "imported.php?id=" . urlencode($template_id) . "&importref=" . urlencode($importReferenceCode);
// Solo se restano binding da risolvere mostro la pagina (con anche gli auto, modificabili).
if (!empty($pendingBindings)) {
$_SESSION['pending_bindings'] = [
'template_id' => $template_id,
'importref' => $importReferenceCode,
'items' => array_values($pendingBindings),
'auto' => array_values($autoBindings),
'saved' => array_values($savedBindings),
];
header("Location: resolve_bindings.php");
exit;
}
unset($_SESSION['pending_bindings']);
// Solo auto-collegati: vado diretto alla griglia, segnalando quanti.
if (!empty($autoBindings)) {
$importedUrl .= "&autobound=" . count($autoBindings);
}
header("Location: " . $importedUrl);
exit;
+31 -21
View File
@@ -173,7 +173,8 @@ error_log("Loaded JSON import template: " . print_r($template, true));
<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">To LIMS (l)</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">
@@ -384,42 +385,51 @@ error_log("Loaded JSON import template: " . print_r($template, true));
}
function addJsonRow(jsonPayload, reference, sourceType) {
const normalizedPayload = normalizeJsonPayload(jsonPayload);
const flattened = flattenJson(normalizedPayload);
// Ogni elemento di data[] diventa una riga (non colonne data.0.*, data.1.*).
const records = extractRecords(jsonPayload);
if (INCLUDE_SOURCE_CODE_COLUMN) {
flattened.source_code = reference;
flattened.source_type = sourceType;
}
records.forEach((record, recordIndex) => {
const flattened = flattenJson(record);
const newColumns = Object.keys(flattened).filter(col => !columns.includes(col));
columns = columns.concat(newColumns);
const rowReference = records.length > 1 ?
reference + '#' + (recordIndex + 1) :
reference;
jsonRows.push({
excelrow: 'JSON-' + (jsonRows.length + 1),
reference: reference,
sourceType: sourceType,
flat: flattened
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();
}
function normalizeJsonPayload(payload) {
// Record da trasformare in righe: gli oggetti in data[], altrimenti il payload stesso.
function extractRecords(payload) {
if (
UNWRAP_SINGLE_DATA_ITEM &&
payload &&
typeof payload === 'object' &&
!Array.isArray(payload) &&
Array.isArray(payload.data) &&
payload.data.length === 1 &&
payload.data[0] &&
typeof payload.data[0] === 'object'
payload.data.length > 0
) {
return payload.data[0];
const items = payload.data.filter(item => item && typeof item === 'object');
if (items.length > 0) {
return items;
}
}
return payload;
return [payload];
}
function flattenJson(value, prefix = '', result = {}) {
+8
View File
@@ -1301,9 +1301,17 @@ $gridMeta = [
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<?php $autoBoundCount = isset($_GET['autobound']) ? (int) $_GET['autobound'] : 0; ?>
<?php if ($autoBoundCount > 0): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?= $autoBoundCount ?> valore/i collegato/i automaticamente al LIMS durante l'import.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<div class="mb-3 text d-flex align-items-center gap-2">
<a href="imported.php?id=<?= $template_id ?>" class="btn btn-warning me-2">Imported (i)</a>
<a href="tolims.php?id=<?= $template_id ?>" class="btn btn-success">To LIMS (l)</a>
<a href="bindings_manage.php?template_id=<?= $template_id ?>" class="btn btn-outline-secondary">Binding JSON -> LIMS</a>
<?php if ($importref === ''): ?>
<span class="ms-3">
<label class="form-check-label" style="font-size: 13px; cursor: pointer;">
+2
View File
@@ -51,6 +51,8 @@
<ul>
<li> <a href="quotations.php"><i class='bx bx-radio-circle'></i><?php echo $quotationstitle; ?></a>
</li>
<li> <a href="bindings_manage.php"><i class='bx bx-radio-circle'></i>Binding JSON -> LIMS</a>
</li>
</ul>
+318
View File
@@ -0,0 +1,318 @@
<?php
include('include/headscript.php');
// Binding preparati da import_insert.php (da risolvere + collegati in automatico).
$pending = $_SESSION['pending_bindings'] ?? null;
if (empty($pending) || (empty($pending['items']) && empty($pending['auto']) && empty($pending['saved']))) {
// Niente da mostrare: vado alla griglia.
$tid = $pending['template_id'] ?? ($_SESSION['template_id'] ?? null);
$ref = $pending['importref'] ?? '';
unset($_SESSION['pending_bindings']);
if ($tid) {
header("Location: imported.php?id=" . urlencode($tid) . "&importref=" . urlencode($ref));
} else {
header("Location: xlstemplates_grid.php");
}
exit;
}
$templateId = (int) $pending['template_id'];
$importRef = (string) $pending['importref'];
$items = $pending['items'] ?? [];
$autoItems = $pending['auto'] ?? [];
$savedItems = $pending['saved'] ?? [];
// Righe gia' risolte (modificabili): auto-collegate + binding gia' salvati.
$resolvedItems = [];
foreach ($autoItems as $a) {
$a['badge'] = 'auto';
$a['badge_class'] = 'bg-success';
$resolvedItems[] = $a;
}
foreach ($savedItems as $s) {
$s['badge'] = 'salvato';
$s['badge_class'] = 'bg-secondary';
$resolvedItems[] = $s;
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("SELECT name FROM excel_templates WHERE id = ?");
$stmt->execute([$templateId]);
$templateName = $stmt->fetchColumn() ?: ('Template ' . $templateId);
?>
<!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/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
<style>
.json-value {
font-family: Consolas, Monaco, monospace;
font-weight: 600;
}
.binding-status {
font-size: 12px;
}
td .select2-container {
min-width: 240px;
}
/* Allinea l'altezza di select2 a form-select/btn di Bootstrap */
.select2-container--default .select2-selection--single {
height: 38px;
border-color: #ced4da;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: 36px;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 36px;
}
</style>
<title>Binding JSON -> LIMS - <?= 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">
<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">Binding JSON -> LIMS</h6>
<small><?= htmlspecialchars($templateName) ?> - Template ID: <?= $templateId ?></small>
</div>
<a href="bindings_manage.php?template_id=<?= $templateId ?>" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-cog"></i> Gestione binding
</a>
</div>
</div>
<div class="card-body">
<?php if (!empty($items)): ?>
<div class="alert alert-info">
Alcuni valori importati dal JSON non hanno ancora una corrispondenza con i valori del LIMS.
Seleziona il valore LIMS corretto per ciascuno e conferma. I binding verranno salvati e
riutilizzati nelle importazioni successive.
</div>
<?php endif; ?>
<?php if (!empty($autoItems)): ?>
<div class="alert alert-success">
<?= count($autoItems) ?> valore/i collegato/i automaticamente (corrispondenza esatta con il LIMS).
</div>
<?php endif; ?>
<div id="bindingError" class="alert alert-danger" style="display:none;"></div>
<form id="bindingForm">
<div class="table-responsive">
<table class="table table-bordered align-middle">
<thead>
<tr>
<th>Campo (template_mapping)</th>
<th>Valore JSON</th>
<th>Valore LIMS</th>
<th style="width:210px;">Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $idx => $item): ?>
<tr class="binding-row"
data-index="<?= $idx ?>"
data-mapping-id="<?= (int) $item['mapping_id'] ?>"
data-field-id="<?= (int) $item['field_id'] ?>"
data-json-value="<?= htmlspecialchars($item['json_value'], ENT_QUOTES) ?>"
data-datadb-ids="<?= htmlspecialchars(json_encode($item['datadb_ids']), ENT_QUOTES) ?>">
<td class="text-muted"><?= htmlspecialchars($item['field_label']) ?></td>
<td class="json-value"><?= htmlspecialchars($item['json_value']) ?></td>
<td>
<select class="form-select binding-select" data-field-id="<?= (int) $item['field_id'] ?>">
<option value="">Seleziona valore LIMS...</option>
</select>
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-secondary skip-binding-btn">Nessuna corrispondenza</button>
<div class="binding-status text-muted mt-1">In attesa</div>
</td>
</tr>
<?php endforeach; ?>
<?php foreach ($resolvedItems as $res): ?>
<tr class="binding-row"
data-mapping-id="<?= (int) $res['mapping_id'] ?>"
data-field-id="<?= (int) $res['field_id'] ?>"
data-json-value="<?= htmlspecialchars($res['json_value'], ENT_QUOTES) ?>"
data-datadb-ids="<?= htmlspecialchars(json_encode($res['datadb_ids']), ENT_QUOTES) ?>">
<td class="text-muted"><?= htmlspecialchars($res['field_label']) ?></td>
<td class="json-value"><?= htmlspecialchars($res['json_value']) ?></td>
<td>
<select class="form-select binding-select" data-field-id="<?= (int) $res['field_id'] ?>">
<option value="<?= (int) $res['lims_value_id'] ?>" selected><?= htmlspecialchars($res['lims_value']) ?></option>
</select>
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-secondary skip-binding-btn">Nessuna corrispondenza</button>
<div class="binding-status mt-1"><span class="badge <?= $res['badge_class'] ?>"><?= $res['badge'] ?></span></div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="d-flex justify-content-end gap-2 mt-3">
<a href="imported.php?id=<?= $templateId ?>&importref=<?= urlencode($importRef) ?>"
class="btn btn-outline-secondary">Salta per ora</a>
<button type="submit" class="btn btn-primary" id="confirmBindingsBtn" disabled>
Conferma e prosegui
</button>
</div>
</form>
</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>
<?php include('jsinclude.php'); ?>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(function() {
const TEMPLATE_ID = <?= $templateId ?>;
const IMPORT_REF = <?= json_encode($importRef) ?>;
const CONTINUE_URL = 'imported.php?id=' + TEMPLATE_ID + '&importref=' + encodeURIComponent(IMPORT_REF);
const $form = $('#bindingForm');
const $btn = $('#confirmBindingsBtn');
const $error = $('#bindingError');
// Dropdown valori LIMS per riga (search_customfield_values.php).
$('.binding-select').each(function() {
const fieldId = $(this).data('field-id');
$(this).select2({
placeholder: 'Seleziona valore LIMS...',
width: '100%',
ajax: {
url: 'search_customfield_values.php',
dataType: 'json',
delay: 200,
data: params => ({
field_id: fieldId,
q: params.term || '',
limit: 50
}),
processResults: data => ({
results: data.results || []
})
},
minimumInputLength: 0
});
});
// Una riga e' pronta se ha un valore scelto oppure e' marcata "nessuna corrispondenza".
function refreshButton() {
const allReady = $('.binding-row').toArray().every(row => {
const $row = $(row);
return $row.hasClass('is-skipped') || $row.find('.binding-select').val();
});
$btn.prop('disabled', !allReady);
}
$('.binding-select').on('change', refreshButton);
refreshButton();
// "Nessuna corrispondenza": azzera il valore importato, nessun binding salvato.
$('.skip-binding-btn').on('click', function() {
const $row = $(this).closest('.binding-row');
const $select = $row.find('.binding-select');
const $status = $row.find('.binding-status');
const skipped = $row.toggleClass('is-skipped').hasClass('is-skipped');
$(this).toggleClass('btn-outline-secondary', !skipped).toggleClass('btn-secondary', skipped);
$select.val(null).trigger('change').prop('disabled', skipped);
$status.text(skipped ? 'Nessuna corrispondenza' : 'In attesa');
refreshButton();
});
$form.on('submit', function(e) {
e.preventDefault();
$error.hide();
$btn.prop('disabled', true).text('Salvataggio...');
const tasks = $('.binding-row').toArray().map(row => {
const $row = $(row);
const $status = $row.find('.binding-status');
const datadbIds = JSON.stringify($row.data('datadb-ids') || []);
const jsonValue = String($row.data('json-value'));
// Riga senza corrispondenza: azzera il valore, niente binding.
if ($row.hasClass('is-skipped')) {
return $.post('skip_binding.php', {
mapping_id: $row.data('mapping-id'),
json_value: jsonValue,
datadb_ids: datadbIds
}).then(resp => {
if (resp && resp.success) {
$status.text('Azzerato').removeClass('text-muted').addClass('text-success');
return true;
}
$status.text('Errore').addClass('text-danger');
throw new Error((resp && resp.error) || 'Errore azzeramento valore');
});
}
const $select = $row.find('.binding-select');
const selectedData = $select.select2('data')[0] || {};
return $.post('save_binding.php', {
mapping_id: $row.data('mapping-id'),
field_id: $row.data('field-id'),
template_id: TEMPLATE_ID,
json_value: jsonValue,
lims_value_id: $select.val(),
lims_value: selectedData.text || '',
datadb_ids: datadbIds
}).then(resp => {
if (resp && resp.success) {
$status.text('Salvato').removeClass('text-muted').addClass('text-success');
return true;
}
$status.text('Errore').addClass('text-danger');
throw new Error((resp && resp.error) || 'Errore salvataggio binding');
});
});
Promise.all(tasks)
.then(() => {
window.location.href = CONTINUE_URL;
})
.catch(err => {
$error.text(err.message || 'Errore durante il salvataggio dei binding.').show();
$btn.prop('disabled', false).text('Conferma e prosegui');
});
});
});
</script>
</body>
</html>
+79
View File
@@ -0,0 +1,79 @@
<?php
// Salva un binding JSON -> LIMS e lo applica ai record appena importati. Ritorna JSON.
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
require_once __DIR__ . '/class/binding-functions.php';
include dirname(__DIR__) . '/../extra/auth.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
if (!Auth::check()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
exit;
}
$mappingId = intval($_POST['mapping_id'] ?? 0);
$fieldId = intval($_POST['field_id'] ?? 0);
$templateId = intval($_POST['template_id'] ?? 0);
$jsonValue = (string) ($_POST['json_value'] ?? '');
$limsValueId = intval($_POST['lims_value_id'] ?? 0);
$limsValue = (string) ($_POST['lims_value'] ?? '');
$datadbIds = [];
if (isset($_POST['datadb_ids'])) {
$decoded = json_decode($_POST['datadb_ids'], true);
if (is_array($decoded)) {
$datadbIds = $decoded;
}
}
if ($mappingId <= 0 || $templateId <= 0 || $jsonValue === '' || $limsValueId <= 0) {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Missing required parameters']);
exit;
}
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$createdBy = null;
if (Auth::user()) {
$createdBy = (int) Auth::user()->present()->id;
}
if ($fieldId <= 0) {
$stmt = $pdo->prepare("SELECT field_id FROM template_mapping WHERE id = ?");
$stmt->execute([$mappingId]);
$fieldId = (int) ($stmt->fetchColumn() ?: 0);
}
binding_upsert($pdo, $templateId, $mappingId, $fieldId, $jsonValue, $limsValueId, $limsValue, $createdBy);
$applied = 0;
if (!empty($datadbIds)) {
$applied = binding_apply_to_details($pdo, $mappingId, $limsValue, $datadbIds);
}
echo json_encode([
'success' => true,
'applied_rows' => $applied,
'mapping_id' => $mappingId,
'json_value' => $jsonValue,
'lims_value' => $limsValue,
'lims_value_id' => $limsValueId,
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
+55
View File
@@ -0,0 +1,55 @@
<?php
// "Nessuna corrispondenza": azzera il valore grezzo nei record importati, senza salvare binding.
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
require_once __DIR__ . '/class/binding-functions.php';
include dirname(__DIR__) . '/../extra/auth.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
if (!Auth::check()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
exit;
}
$mappingId = intval($_POST['mapping_id'] ?? 0);
$jsonValue = (string) ($_POST['json_value'] ?? '');
$datadbIds = [];
if (isset($_POST['datadb_ids'])) {
$decoded = json_decode($_POST['datadb_ids'], true);
if (is_array($decoded)) {
$datadbIds = $decoded;
}
}
if ($mappingId <= 0 || $jsonValue === '') {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Missing required parameters']);
exit;
}
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$cleared = binding_apply_to_details($pdo, $mappingId, '', $datadbIds);
// Rimuovo un eventuale binding gia' salvato (es. auto-collegato) per coerenza.
$del = $pdo->prepare("DELETE FROM json_lims_binding WHERE mapping_id = ? AND json_value = ?");
$del->execute([$mappingId, $jsonValue]);
echo json_encode(['success' => true, 'cleared_rows' => $cleared]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}