Compare commits

...

3 Commits

Author SHA1 Message Date
RMubarakzyanov 2ad2e06dc2 fixed columns bind 2026-06-17 21:14:45 +03:00
RMubarakzyanov 25c3990753 json bindings filter && order 2026-06-16 10:20:03 +03:00
RMubarakzyanov 9eb257d237 json bindings 2026-06-13 20:19:36 +03:00
11 changed files with 1821 additions and 24 deletions
+449
View File
@@ -0,0 +1,449 @@
<?php
include('include/headscript.php');
require_once(__DIR__ . '/class/binding-functions.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Filtri comuni.
$templateFilter = isset($_GET['template_id']) ? intval($_GET['template_id']) : 0;
$search = trim($_GET['q'] ?? '');
$perPage = 50;
$page = max(1, intval($_GET['page'] ?? 1));
$dir = (strtolower($_GET['dir'] ?? 'asc') === 'desc') ? 'DESC' : 'ASC';
// Modalita': overview (gruppi per campo) oppure detail (valori di un campo).
$focusTarget = trim($_GET['target'] ?? '');
$mode = $focusTarget !== '' ? 'detail' : 'overview';
$templates = $pdo->query("SELECT id, name FROM excel_templates ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
// Helper: URL che preserva i filtri correnti.
$bmUrl = function (array $ov) use ($templateFilter, $search, $dir, $page, $focusTarget) {
$base = [
'template_id' => $templateFilter ?: null,
'q' => $search !== '' ? $search : null,
'order' => $_GET['order'] ?? null,
'dir' => strtolower($dir),
'page' => $page,
'target' => $focusTarget !== '' ? $focusTarget : null,
];
$p = array_filter(array_merge($base, $ov), fn($v) => $v !== null && $v !== '');
return 'bindings_manage.php?' . http_build_query($p);
};
// Filtri WHERE comuni (template/kind/search).
$conds = [];
$params = [];
if ($templateFilter > 0) {
$conds[] = 'b.template_id = ?';
$params[] = $templateFilter;
}
if ($mode === 'detail') {
// ---- DETAIL: valori (json_value -> lims) di un singolo campo (target_key) ----
$conds[] = 'b.target_key = ?';
$params[] = $focusTarget;
if ($search !== '') {
$conds[] = '(b.json_value LIKE ? OR b.lims_value LIKE ?)';
$like = '%' . $search . '%';
array_push($params, $like, $like);
}
$where = 'WHERE ' . implode(' AND ', $conds);
$orderCols = ['json' => 'b.json_value', 'lims' => 'b.lims_value'];
$order = array_key_exists($_GET['order'] ?? '', $orderCols) ? $_GET['order'] : 'json';
$orderSql = $orderCols[$order] . ' ' . $dir;
$countStmt = $pdo->prepare("SELECT COUNT(*) FROM json_lims_binding b $where");
$countStmt->execute($params);
$total = (int) $countStmt->fetchColumn();
$totalPages = max(1, (int) ceil($total / $perPage));
if ($page > $totalPages) $page = $totalPages;
$offset = ($page - 1) * $perPage;
$sql = "SELECT b.id, b.template_id, b.binding_kind, b.mapping_id, b.fixed_field_key, b.field_id,
b.json_value, b.lims_value_id, b.lims_value,
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 $orderSql, b.json_value ASC
LIMIT $perPage OFFSET $offset";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Intestazione del gruppo (tutte le righe condividono campo/template/kind).
$head = $rows[0] ?? null;
if (!$head) {
// Target vuoto (es. dopo aver svuotato il campo): ricavo i meta dal target_key.
$head = ['binding_kind' => str_starts_with($focusTarget, 'fx:') ? 'fixed' : 'custom'];
}
$headKind = $head['binding_kind'] ?? 'custom';
if ($headKind === 'fixed') {
$headFixedKey = $head['fixed_field_key'] ?? (explode(':', $focusTarget)[2] ?? '');
$headLabel = binding_fixed_label((string) $headFixedKey);
$headTpl = (int) ($head['template_id'] ?? (explode(':', $focusTarget)[1] ?? 0));
} else {
$headLabel = $head['field_label'] ?? ('mapping ' . ($head['mapping_id'] ?? ''));
$headTpl = (int) ($head['template_id'] ?? 0);
}
$headTplName = $headTpl ? ($pdo->query("SELECT name FROM excel_templates WHERE id=" . $headTpl)->fetchColumn() ?: ('#' . $headTpl)) : '';
} else {
// ---- OVERVIEW: un gruppo per campo (target_key) con il conteggio ----
if ($search !== '') {
$conds[] = '(t.name LIKE ? OR m.field_label LIKE ? OR b.fixed_field_key LIKE ?)';
$like = '%' . $search . '%';
array_push($params, $like, $like, $like);
}
$where = $conds ? ('WHERE ' . implode(' AND ', $conds)) : '';
$orderCols = ['template' => 'template_name', 'field' => 'field_label', 'count' => 'cnt'];
$order = array_key_exists($_GET['order'] ?? '', $orderCols) ? $_GET['order'] : 'template';
$orderSql = $orderCols[$order] . ' ' . $dir;
$countStmt = $pdo->prepare("SELECT COUNT(DISTINCT b.target_key)
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");
$countStmt->execute($params);
$total = (int) $countStmt->fetchColumn();
$totalPages = max(1, (int) ceil($total / $perPage));
if ($page > $totalPages) $page = $totalPages;
$offset = ($page - 1) * $perPage;
$sql = "SELECT b.target_key, b.template_id, b.binding_kind, b.mapping_id, b.fixed_field_key,
MAX(t.name) AS template_name, MAX(m.field_label) AS field_label,
COUNT(*) AS cnt, MAX(b.updated_at) AS last_updated
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
GROUP BY b.target_key, b.template_id, b.binding_kind, b.mapping_id, b.fixed_field_key
ORDER BY $orderSql, template_name ASC
LIMIT $perPage OFFSET $offset";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$groups = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
$sortLink = function (string $col, string $label) use ($bmUrl, $order, $dir) {
$nextDir = ($order === $col && $dir === 'ASC') ? 'desc' : 'asc';
$caret = $order === $col ? ($dir === 'ASC' ? ' &#9650;' : ' &#9660;') : '';
return '<a href="' . htmlspecialchars($bmUrl(['order' => $col, 'dir' => $nextDir, 'page' => 1]))
. '" class="text-decoration-none text-reset">' . $label . $caret . '</a>';
};
?>
<!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;
}
.group-row {
cursor: pointer;
}
.group-row:hover {
background-color: #f1f5ff;
}
</style>
<title>Gestione Binding JSON &rarr; 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 &rarr; LIMS</h6>
<form method="GET" class="d-flex align-items-center gap-2 flex-wrap mb-0">
<?php if ($mode === 'detail'): ?>
<input type="hidden" name="target" value="<?= htmlspecialchars($focusTarget) ?>">
<?php endif; ?>
<input type="hidden" name="dir" value="<?= htmlspecialchars(strtolower($dir)) ?>">
<input type="text" name="q" class="form-control form-control-sm" style="width:220px;"
placeholder="<?= $mode === 'detail' ? 'Cerca valore...' : 'Cerca campo/template...' ?>"
value="<?= htmlspecialchars($search) ?>">
<?php if ($mode === 'overview'): ?>
<select name="template_id" class="form-select form-select-sm" style="width:auto;" onchange="this.form.submit()">
<option value="0">Tutti i template</option>
<?php foreach ($templates as $t): ?>
<option value="<?= (int) $t['id'] ?>" <?= $templateFilter === (int) $t['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($t['name']) ?>
</option>
<?php endforeach; ?>
</select>
<?php endif; ?>
<button type="submit" class="btn btn-sm btn-primary">Cerca</button>
</form>
</div>
</div>
<div class="card-body">
<div id="globalError" class="alert alert-danger" style="display:none;"></div>
<?php if ($mode === 'detail'): ?>
<div class="d-flex justify-content-between align-items-center mb-2 flex-wrap gap-2">
<div>
<a href="<?= htmlspecialchars($bmUrl(['target' => null, 'page' => 1, 'order' => null])) ?>" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Tutti i campi
</a>
<span class="ms-2">
<strong><?= htmlspecialchars($headLabel) ?></strong>
<?php if ($headKind === 'fixed'): ?><span class="badge bg-light text-dark border">fixed</span><?php endif; ?>
<span class="text-muted">&middot; <?= htmlspecialchars((string) $headTplName) ?></span>
</span>
</div>
<span class="small text-muted"><?= $total ?> valore/i &middot; pagina <?= $page ?>/<?= $totalPages ?></span>
</div>
<div class="table-responsive">
<table class="table table-striped table-bordered align-middle" id="bindingsTable">
<thead>
<tr>
<th><?= $sortLink('json', 'Valore JSON') ?></th>
<th><?= $sortLink('lims', 'Valore LIMS') ?></th>
<th style="width:170px;">Azioni</th>
</tr>
</thead>
<tbody>
<?php if (empty($rows)): ?>
<tr class="no-data-row">
<td colspan="3" class="text-center text-muted">Nessun valore per questo campo.</td>
</tr>
<?php else: ?>
<?php foreach ($rows as $b): ?>
<?php $bDangling = ($b['binding_kind'] === 'custom' && $b['field_label'] === null); ?>
<tr data-id="<?= (int) $b['id'] ?>"
data-kind="<?= $b['binding_kind'] ?>"
data-mapping-id="<?= (int) $b['mapping_id'] ?>"
data-field-id="<?= (int) $b['field_id'] ?>"
data-fixed-key="<?= htmlspecialchars((string) $b['fixed_field_key'], ENT_QUOTES) ?>"
data-template-id="<?= (int) $b['template_id'] ?>"
data-json-value="<?= htmlspecialchars($b['json_value'], ENT_QUOTES) ?>">
<td class="json-value"><?= htmlspecialchars($b['json_value']) ?></td>
<td>
<select class="form-select binding-select" <?= $bDangling ? 'disabled' : '' ?>>
<?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>
<?php else: ?>
<div class="alert alert-secondary small">
Scegli un campo per gestirne i valori. Le modifiche valgono per le <strong>importazioni future</strong>.
</div>
<div class="small text-muted mb-2"><?= $total ?> camp<?= $total === 1 ? 'o' : 'i' ?> con binding &middot; pagina <?= $page ?>/<?= $totalPages ?></div>
<div class="table-responsive">
<table class="table table-striped table-bordered align-middle">
<thead>
<tr>
<th><?= $sortLink('template', 'Template') ?></th>
<th><?= $sortLink('field', 'Campo') ?></th>
<th style="width:120px;"><?= $sortLink('count', '# Binding') ?></th>
<th style="width:90px;"></th>
</tr>
</thead>
<tbody>
<?php if (empty($groups)): ?>
<tr>
<td colspan="4" class="text-center text-muted">Nessun binding presente.</td>
</tr>
<?php else: ?>
<?php foreach ($groups as $g): ?>
<?php
$gKind = $g['binding_kind'];
$gDangling = ($gKind === 'custom' && $g['field_label'] === null);
$gLabel = $gKind === 'fixed'
? binding_fixed_label((string) $g['fixed_field_key'])
: ($g['field_label'] ?? ('mapping ' . $g['mapping_id']));
$gUrl = $bmUrl(['target' => $g['target_key'], 'page' => 1, 'order' => null, 'q' => null]);
?>
<tr class="group-row" onclick="window.location.href='<?= htmlspecialchars($gUrl) ?>'">
<td><?= htmlspecialchars($g['template_name'] ?? ('#' . $g['template_id'])) ?></td>
<td>
<?= htmlspecialchars($gLabel) ?>
<?php if ($gKind === 'fixed'): ?><span class="badge bg-light text-dark border">fixed</span><?php endif; ?>
<?php if ($gDangling): ?><span class="badge bg-warning text-dark" title="Il campo mappato non esiste piu'">campo rimosso</span><?php endif; ?>
</td>
<td><span class="badge bg-primary"><?= (int) $g['cnt'] ?></span></td>
<td><a href="<?= htmlspecialchars($gUrl) ?>" class="btn btn-sm btn-outline-primary">Apri</a></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php if ($totalPages > 1): ?>
<nav class="d-flex justify-content-center mt-2">
<ul class="pagination pagination-sm mb-0">
<li class="page-item <?= $page <= 1 ? 'disabled' : '' ?>">
<a class="page-link" href="<?= htmlspecialchars($bmUrl(['page' => max(1, $page - 1)])) ?>">&laquo;</a>
</li>
<li class="page-item disabled"><span class="page-link"><?= $page ?> / <?= $totalPages ?></span></li>
<li class="page-item <?= $page >= $totalPages ? 'disabled' : '' ?>">
<a class="page-link" href="<?= htmlspecialchars($bmUrl(['page' => min($totalPages, $page + 1)])) ?>">&raquo;</a>
</li>
</ul>
</nav>
<?php endif; ?>
</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');
// Select2 solo nelle righe valore della pagina corrente (max 50), saltando le disabilitate.
$('.binding-select').not(':disabled').each(function() {
const $row = $(this).closest('tr');
const isFixed = $row.data('kind') === 'fixed';
const initialVal = $(this).val();
$(this).select2({
width: '220px',
ajax: {
url: isFixed ? 'search_fixed_field_values.php' : 'search_customfield_values.php',
dataType: 'json',
delay: 200,
data: params => isFixed ? {
field_key: $row.data('fixed-key'),
template_id: $row.data('template-id'),
q: params.term || '',
limit: 50
} : {
field_id: $row.data('field-id'),
q: params.term || '',
limit: 50
},
processResults: data => ({
results: data.results || []
})
},
minimumInputLength: 0
});
$(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');
const isFixed = $row.data('kind') === 'fixed';
const targetFields = isFixed
? { kind: 'fixed', fixed_field_key: $row.data('fixed-key') }
: { kind: 'custom', mapping_id: $row.data('mapping-id'), field_id: $row.data('field-id') };
$.post('save_binding.php', {
...targetFields,
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>
+448
View File
@@ -0,0 +1,448 @@
<?php
// Helpers for JSON -> LIMS value bindings (table json_lims_binding).
// Supports two kinds: 'custom' (template_mapping list fields, value -> import_data_details)
// and 'fixed' (template_fixed_mapping list fields, id -> datadb column).
// ---------------------------------------------------------------------------
// Fixed-field metadata
// ---------------------------------------------------------------------------
function binding_fixed_alias_map(): array
{
return [
'ClienteResponsabile' => 'cliente_responsabile_id',
'ClienteFornitore' => 'cliente_fornitore_id',
'ClienteAnalisi' => 'clienteAnalisi',
'MoltiplicatorePrezzo' => 'moltiplicatore_prezzo_id',
'AnagraficaCertestObject' => 'anagrafica_certest_object_id',
'AnagraficaCertestService' => 'anagrafica_certest_service_id',
'ConsegnaRichiesta' => 'consegna_richiesta',
];
}
// Fixed-field keys that are LIMS list dropdowns (bindable). ConsegnaRichiesta is a date.
function binding_fixed_is_list(string $key): bool
{
return in_array($key, [
'ClienteResponsabile',
'ClienteFornitore',
'ClienteAnalisi',
'MoltiplicatorePrezzo',
'AnagraficaCertestObject',
'AnagraficaCertestService',
], true);
}
function binding_fixed_column(string $key): ?string
{
return binding_fixed_alias_map()[$key] ?? null;
}
// Auto-match only the small global lists; the client-based ones are huge / client-specific.
function binding_fixed_auto_matchable(string $key): bool
{
return in_array($key, [
'MoltiplicatorePrezzo',
'AnagraficaCertestObject',
'AnagraficaCertestService',
], true);
}
function binding_fixed_label(string $key): string
{
$labels = [
'AnagraficaCertestObject' => 'Anagrafica Certest Object',
'AnagraficaCertestService' => 'Anagrafica Certest Service',
'MoltiplicatorePrezzo' => 'Moltiplicatore Prezzo',
'ClienteResponsabile' => 'Cliente Responsabile',
'ClienteFornitore' => 'Cliente Fornitore',
'ClienteAnalisi' => 'Cliente Analisi',
];
return $labels[$key] ?? $key;
}
// ---------------------------------------------------------------------------
// Bindable check (custom mapping row)
// ---------------------------------------------------------------------------
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;
}
// ---------------------------------------------------------------------------
// Target keys + lookup / upsert
// ---------------------------------------------------------------------------
function binding_target_custom(int $mappingId): string
{
return 'cf:' . $mappingId;
}
function binding_target_fixed(int $templateId, string $fixedKey): string
{
return 'fx:' . $templateId . ':' . $fixedKey;
}
function binding_lookup_target(PDO $pdo, string $targetKey, string $jsonValue): ?array
{
$stmt = $pdo->prepare(
"SELECT id, lims_value_id, lims_value
FROM json_lims_binding
WHERE target_key = ? AND json_value = ?
LIMIT 1"
);
$stmt->execute([$targetKey, $jsonValue]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
function binding_lookup(PDO $pdo, int $mappingId, string $jsonValue): ?array
{
return binding_lookup_target($pdo, binding_target_custom($mappingId), $jsonValue);
}
function binding_lookup_fixed(PDO $pdo, int $templateId, string $fixedKey, string $jsonValue): ?array
{
return binding_lookup_target($pdo, binding_target_fixed($templateId, $fixedKey), $jsonValue);
}
function binding_upsert_row(
PDO $pdo,
string $kind,
int $templateId,
?int $mappingId,
?string $fixedFieldKey,
string $targetKey,
int $fieldId,
string $jsonValue,
int $limsValueId,
string $limsValue,
?int $createdBy
): void {
$stmt = $pdo->prepare(
"INSERT INTO json_lims_binding
(template_id, binding_kind, mapping_id, fixed_field_key, target_key, 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),
binding_kind = VALUES(binding_kind),
mapping_id = VALUES(mapping_id),
fixed_field_key = VALUES(fixed_field_key)"
);
$stmt->execute([
$templateId, $kind, $mappingId, $fixedFieldKey, $targetKey,
$fieldId, $jsonValue, $limsValueId, $limsValue, $createdBy,
]);
}
function binding_upsert(
PDO $pdo,
int $templateId,
int $mappingId,
int $fieldId,
string $jsonValue,
int $limsValueId,
string $limsValue,
?int $createdBy
): void {
binding_upsert_row(
$pdo, 'custom', $templateId, $mappingId, null,
binding_target_custom($mappingId), $fieldId, $jsonValue, $limsValueId, $limsValue, $createdBy
);
}
function binding_upsert_fixed(
PDO $pdo,
int $templateId,
string $fixedKey,
string $jsonValue,
int $limsValueId,
string $limsValue,
?int $createdBy
): void {
binding_upsert_row(
$pdo, 'fixed', $templateId, null, $fixedKey,
binding_target_fixed($templateId, $fixedKey), 0, $jsonValue, $limsValueId, $limsValue, $createdBy
);
}
function binding_delete_target(PDO $pdo, string $targetKey, string $jsonValue): int
{
$stmt = $pdo->prepare("DELETE FROM json_lims_binding WHERE target_key = ? AND json_value = ?");
$stmt->execute([$targetKey, $jsonValue]);
return $stmt->rowCount();
}
// ---------------------------------------------------------------------------
// Custom-field LIMS values (cache/customfield_{id}.json) + auto-match
// ---------------------------------------------------------------------------
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;
}
// ---------------------------------------------------------------------------
// Fixed-field LIMS values (per-field source) + auto-match
// ---------------------------------------------------------------------------
function binding_template_idclient(PDO $pdo, int $templateId): int
{
$stmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
$stmt->execute([$templateId]);
return (int) ($stmt->fetchColumn() ?: 0);
}
function binding_client_label(array $client): string
{
$name = trim($client['Nominativo'] ?? '');
$id = trim((string) ($client['IdCliente'] ?? ''));
$code = trim((string) ($client['CodiceCliente'] ?? ''));
$parts = explode('_', $code);
$suffix = trim($parts[1] ?? '');
if ($suffix === '' && $code !== '') {
$suffix = substr($code, 0, 1);
}
if ($suffix === '') {
$suffix = '--';
}
return $name . ' - ' . $suffix . ' (ID: ' . $id . ')';
}
// Fetch an OData payload with a 1-hour file cache.
function binding_cached_get($api, string $cacheFile, string $endpoint): array
{
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
$data = json_decode(file_get_contents($cacheFile), true);
} else {
$data = $api->get($endpoint);
$dir = dirname($cacheFile);
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
file_put_contents($cacheFile, json_encode($data));
}
return is_array($data) ? $data : [];
}
// Normalized LIMS value list for a fixed field: [['id' => int, 'text' => string], ...].
function binding_get_fixed_values(PDO $pdo, string $fieldKey, int $templateId): array
{
static $memo = [];
$memoKey = $fieldKey . '|' . $templateId;
if (isset($memo[$memoKey])) {
return $memo[$memoKey];
}
$cacheDir = dirname(__DIR__) . '/cache';
$out = [];
try {
require_once __DIR__ . '/VisualLimsApiClient.class.php';
$api = VisualLimsApiClient::getInstance();
switch ($fieldKey) {
case 'MoltiplicatorePrezzo':
$data = binding_cached_get($api, "$cacheDir/moltiplicatori_prezzo.json", 'MoltiplicatorePrezzi');
foreach (($data['value'] ?? []) as $r) {
$out[] = ['id' => (int) ($r['IdMoltiplicatorePrezzo'] ?? 0), 'text' => (string) ($r['Descrizione'] ?? '')];
}
break;
case 'AnagraficaCertestObject':
case 'AnagraficaCertestService':
$file = $fieldKey === 'AnagraficaCertestObject' ? 'anagrafica_certest_object.json' : 'anagrafica_certest_service.json';
$data = binding_cached_get($api, "$cacheDir/$file", $fieldKey);
foreach (($data['value'] ?? []) as $r) {
$code = trim((string) ($r['Codice'] ?? ''));
$text = ($code !== '' ? $code . ' - ' : '') . (string) ($r['NomeAnagrafica'] ?? '');
$out[] = ['id' => (int) ($r['IdAnagrafica'] ?? 0), 'text' => $text];
}
break;
case 'ClienteResponsabile':
$idCliente = binding_template_idclient($pdo, $templateId);
if ($idCliente > 0) {
$data = binding_cached_get($api, "$cacheDir/cliente_responsabili_$idCliente.json", "Cliente($idCliente)?\$expand=Responsabili");
foreach (($data['Responsabili'] ?? []) as $r) {
$out[] = ['id' => (int) ($r['IdClienteResponsabile'] ?? 0), 'text' => (string) ($r['Nominativo'] ?? '')];
}
}
break;
case 'ClienteFornitore':
case 'ClienteAnalisi':
$endpoint = 'Cliente?' . http_build_query(['$select' => 'IdCliente,Nominativo,CodiceCliente', '$orderby' => 'Nominativo asc']);
$data = binding_cached_get($api, "$cacheDir/clienti.json", $endpoint);
foreach (($data['value'] ?? []) as $r) {
$out[] = ['id' => (int) ($r['IdCliente'] ?? 0), 'text' => binding_client_label($r)];
}
break;
}
} catch (Throwable $e) {
error_log("binding_get_fixed_values($fieldKey) failed: " . $e->getMessage());
$out = [];
}
return $memo[$memoKey] = $out;
}
// Exactly one case-insensitive match by text on a [{id,text}] list, otherwise null.
function binding_auto_match_fixed(array $values, string $jsonValue): ?array
{
$needle = mb_strtolower(trim($jsonValue));
if ($needle === '') {
return null;
}
$matches = [];
foreach ($values as $v) {
if (mb_strtolower(trim((string) ($v['text'] ?? ''))) === $needle) {
$matches[] = $v;
}
}
return count($matches) === 1 ? $matches[0] : null;
}
// ---------------------------------------------------------------------------
// JSON node -> column matching (shared with import_insert custom logic)
// ---------------------------------------------------------------------------
function binding_find_column_index(string $sourceColumn, array $columns): int
{
$sourceColumn = trim($sourceColumn);
if ($sourceColumn === '') {
return -1;
}
$columnsTrimmed = array_map('trim', $columns);
$candidates = [
$sourceColumn,
preg_replace('/^data\[\]\./', '', $sourceColumn),
preg_replace('/^data\.0\./', '', $sourceColumn),
str_replace('data[].', 'data.0.', $sourceColumn),
str_replace('data.0.', 'data[].', $sourceColumn),
];
$candidates = array_values(array_unique(array_filter(array_map('trim', $candidates), function ($v) {
return $v !== '';
})));
foreach ($candidates as $c) {
$i = array_search($c, $columnsTrimmed, true);
if ($i !== false) {
return (int) $i;
}
}
return -1;
}
// ---------------------------------------------------------------------------
// Apply resolved values
// ---------------------------------------------------------------------------
// Custom: write the resolved LIMS text into import_data_details for the given datadb ids.
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();
}
// Fixed: write the resolved LIMS id into a whitelisted datadb column for the given ids.
// $limsValueId null clears the column.
function binding_apply_to_datadb(
PDO $pdo,
string $column,
?int $limsValueId,
array $datadbIds
): int {
if (!in_array($column, array_values(binding_fixed_alias_map()), true)) {
throw new InvalidArgumentException("Invalid fixed-field column: $column");
}
$datadbIds = array_values(array_filter(array_map('intval', $datadbIds)));
if (empty($datadbIds)) {
return 0;
}
$placeholders = implode(',', array_fill(0, count($datadbIds), '?'));
$sql = "UPDATE datadb SET `$column` = ? WHERE iddatadb IN ($placeholders)";
$stmt = $pdo->prepare($sql);
$stmt->execute(array_merge([$limsValueId], $datadbIds));
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()]);
}
+230 -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 = ?
");
@@ -82,8 +84,27 @@ foreach ($allMappings as $mapping) {
}
}
// Campi fixed mappati da JSON (default_source='json') per questo template.
$fixedJsonFields = [];
if ($source_type === 'json') {
$fxStmt = $pdo->prepare("
SELECT fixed_field_key, json_node, data_type
FROM template_fixed_mapping
WHERE template_id = ? AND default_source = 'json' AND json_node IS NOT NULL AND json_node <> ''
");
$fxStmt->execute([$template_id]);
$fixedJsonFields = $fxStmt->fetchAll(PDO::FETCH_ASSOC);
}
// Inserisci le righe selezionate in datadb
$insertedIds = [];
// Binding JSON -> LIMS senza corrispondenza salvata, per "kind:key|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 +128,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 +144,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 +256,83 @@ 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 = 'cf:' . $mapping['id'] . '|' . $jsonValue;
if (!isset($savedBindings[$key])) {
$savedBindings[$key] = [
'kind' => 'custom',
'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 = 'cf:' . $mapping['id'] . '|' . $jsonValue;
if (!isset($autoBindings[$key])) {
$autoBindings[$key] = [
'kind' => 'custom',
'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 = 'cf:' . $mapping['id'] . '|' . $jsonValue;
if (!isset($pendingBindings[$key])) {
$pendingBindings[$key] = [
'kind' => 'custom',
'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']);
}
@@ -235,9 +341,130 @@ foreach ($selected_rows as $loopIndex => $rowIndex) {
$stmt->execute([$iddatadb, $mapping['id'], $fieldValue]);
error_log("Inserted into import_data_details for ID $iddatadb, Mapping ID: " . $mapping['id'] . ", Field Value: " . var_export($fieldValue, true));
}
// ---- Fixed fields mappati da JSON (scrivono colonne datadb) ----
if ($source_type === 'json' && !empty($fixedJsonFields)) {
$fixedUpdates = []; // colonna datadb => valore (id LIMS o data)
foreach ($fixedJsonFields as $fx) {
$fixedKey = $fx['fixed_field_key'];
$column = binding_fixed_column($fixedKey);
if (!$column) {
continue;
}
$idx = binding_find_column_index((string) $fx['json_node'], $columns);
$raw = ($idx >= 0 && isset($row[$idx])) ? trim((string) $row[$idx]) : '';
if ($raw === '') {
continue;
}
// Campo non a lista (es. ConsegnaRichiesta DATE): scrivo il valore direttamente.
if (!binding_fixed_is_list($fixedKey)) {
if (($fx['data_type'] ?? '') === 'DATE') {
$fixedUpdates[$column] = date('Y-m-d', strtotime($raw));
}
continue;
}
// Binding gia' salvato.
$existing = binding_lookup_fixed($pdo, (int) $template_id, $fixedKey, $raw);
if ($existing) {
$fixedUpdates[$column] = (int) $existing['lims_value_id'];
$key = 'fx:' . $fixedKey . '|' . $raw;
if (!isset($savedBindings[$key])) {
$savedBindings[$key] = [
'kind' => 'fixed',
'fixed_field_key' => $fixedKey,
'field_label' => binding_fixed_label($fixedKey),
'json_value' => $raw,
'lims_value' => (string) $existing['lims_value'],
'lims_value_id' => (int) $existing['lims_value_id'],
'datadb_ids' => [],
];
}
$savedBindings[$key]['datadb_ids'][] = (int) $iddatadb;
continue;
}
// Auto-match 1-a-1 (solo per le liste globali piccole).
$autoMatch = null;
if (binding_fixed_auto_matchable($fixedKey)) {
$fixedValues = binding_get_fixed_values($pdo, $fixedKey, (int) $template_id);
$autoMatch = binding_auto_match_fixed($fixedValues, $raw);
}
if ($autoMatch) {
binding_upsert_fixed($pdo, (int) $template_id, $fixedKey, $raw, (int) $autoMatch['id'], (string) $autoMatch['text'], $user_id);
$fixedUpdates[$column] = (int) $autoMatch['id'];
$key = 'fx:' . $fixedKey . '|' . $raw;
if (!isset($autoBindings[$key])) {
$autoBindings[$key] = [
'kind' => 'fixed',
'fixed_field_key' => $fixedKey,
'field_label' => binding_fixed_label($fixedKey),
'json_value' => $raw,
'lims_value' => (string) $autoMatch['text'],
'lims_value_id' => (int) $autoMatch['id'],
'datadb_ids' => [],
];
}
$autoBindings[$key]['datadb_ids'][] = (int) $iddatadb;
continue;
}
// Nessuna corrispondenza: colonna lasciata vuota, segnalo come pending.
$key = 'fx:' . $fixedKey . '|' . $raw;
if (!isset($pendingBindings[$key])) {
$pendingBindings[$key] = [
'kind' => 'fixed',
'fixed_field_key' => $fixedKey,
'field_label' => binding_fixed_label($fixedKey),
'json_value' => $raw,
'datadb_ids' => [],
];
}
$pendingBindings[$key]['datadb_ids'][] = (int) $iddatadb;
}
if (!empty($fixedUpdates)) {
$setParts = [];
$params = [];
foreach ($fixedUpdates as $col => $val) {
$setParts[] = "`$col` = ?";
$params[] = $val;
}
$params[] = (int) $iddatadb;
$upd = $pdo->prepare("UPDATE datadb SET " . implode(', ', $setParts) . " WHERE iddatadb = ?");
$upd->execute($params);
}
}
}
$_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;
+22 -12
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 &rarr; LIMS</a>
</div>
<div class="card radius-10">
@@ -384,11 +385,18 @@ 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);
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 = reference;
flattened.source_code = rowReference;
flattened.source_type = sourceType;
}
@@ -397,29 +405,31 @@ error_log("Loaded JSON import template: " . print_r($template, true));
jsonRows.push({
excelrow: 'JSON-' + (jsonRows.length + 1),
reference: reference,
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 &rarr; 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 &rarr; LIMS</a>
</li>
</ul>
+385
View File
@@ -0,0 +1,385 @@
<?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;
}
// Raggruppa le righe per campo (custom: mapping_id, fixed: fixed_field_key),
// mantenendo l'ordine di prima apparizione. Ogni gruppo elenca prima i pending.
$groups = [];
$groupOrder = [];
$pushToGroup = function (array $row, string $type, $idx = null) use (&$groups, &$groupOrder) {
$kind = $row['kind'] ?? 'custom';
$gkey = $kind === 'fixed'
? 'fx:' . ($row['fixed_field_key'] ?? '')
: 'cf:' . (int) ($row['mapping_id'] ?? 0);
if (!isset($groups[$gkey])) {
$groups[$gkey] = [
'label' => $row['field_label'] ?? $gkey,
'kind' => $kind,
'pending' => [],
'resolved' => [],
];
$groupOrder[] = $gkey;
}
if ($type === 'pending') {
$groups[$gkey]['pending'][] = ['idx' => $idx, 'data' => $row];
} else {
$groups[$gkey]['resolved'][] = ['data' => $row];
}
};
foreach ($items as $idx => $item) {
$pushToGroup($item, 'pending', $idx);
}
foreach ($resolvedItems as $res) {
$pushToGroup($res, 'resolved');
}
$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 &rarr; 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 &rarr; LIMS</h6>
<small><?= htmlspecialchars($templateName) ?> &middot; 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 style="width:32px;"></th>
<th>Valore JSON</th>
<th>Valore LIMS</th>
<th style="width:210px;">Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($groupOrder as $gkey): $g = $groups[$gkey];
$pendCount = count($g['pending']);
$totalCount = $pendCount + count($g['resolved']); ?>
<tr class="binding-group-header table-light">
<td colspan="4">
<strong><?= htmlspecialchars($g['label']) ?></strong>
<?php if ($g['kind'] === 'fixed'): ?><span class="badge bg-light text-dark border">fixed</span><?php endif; ?>
<span class="text-muted">&middot; <?= $totalCount ?> valore/i<?php if ($pendCount): ?>, <?= $pendCount ?> da risolvere<?php endif; ?></span>
</td>
</tr>
<?php foreach ($g['pending'] as $p): $idx = $p['idx']; $item = $p['data']; $kind = $item['kind'] ?? 'custom'; ?>
<tr class="binding-row"
data-index="<?= $idx ?>"
data-kind="<?= $kind ?>"
<?php if ($kind === 'fixed'): ?>
data-fixed-key="<?= htmlspecialchars($item['fixed_field_key'], ENT_QUOTES) ?>"
<?php else: ?>
data-mapping-id="<?= (int) ($item['mapping_id'] ?? 0) ?>"
data-field-id="<?= (int) ($item['field_id'] ?? 0) ?>"
<?php endif; ?>
data-json-value="<?= htmlspecialchars($item['json_value'], ENT_QUOTES) ?>"
data-datadb-ids="<?= htmlspecialchars(json_encode($item['datadb_ids']), ENT_QUOTES) ?>">
<td class="text-muted ps-4">&rsaquo;</td>
<td class="json-value"><?= htmlspecialchars($item['json_value']) ?></td>
<td>
<select class="form-select binding-select">
<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 ($g['resolved'] as $r): $res = $r['data']; $kind = $res['kind'] ?? 'custom'; ?>
<tr class="binding-row"
data-kind="<?= $kind ?>"
<?php if ($kind === 'fixed'): ?>
data-fixed-key="<?= htmlspecialchars($res['fixed_field_key'], ENT_QUOTES) ?>"
<?php else: ?>
data-mapping-id="<?= (int) ($res['mapping_id'] ?? 0) ?>"
data-field-id="<?= (int) ($res['field_id'] ?? 0) ?>"
<?php endif; ?>
data-json-value="<?= htmlspecialchars($res['json_value'], ENT_QUOTES) ?>"
data-datadb-ids="<?= htmlspecialchars(json_encode($res['datadb_ids']), ENT_QUOTES) ?>">
<td class="text-muted ps-4">&rsaquo;</td>
<td class="json-value"><?= htmlspecialchars($res['json_value']) ?></td>
<td>
<select class="form-select binding-select">
<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; ?>
<?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: sorgente custom vs fixed.
$('.binding-select').each(function() {
const $row = $(this).closest('.binding-row');
const kind = $row.data('kind');
const isFixed = kind === 'fixed';
$(this).select2({
placeholder: 'Seleziona valore LIMS...',
width: '100%',
ajax: {
url: isFixed ? 'search_fixed_field_values.php' : 'search_customfield_values.php',
dataType: 'json',
delay: 200,
data: params => isFixed ? {
field_key: $row.data('fixed-key'),
template_id: TEMPLATE_ID,
q: params.term || '',
limit: 50
} : {
field_id: $row.data('field-id'),
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 kind = $row.data('kind');
const isFixed = kind === 'fixed';
const datadbIds = JSON.stringify($row.data('datadb-ids') || []);
const jsonValue = String($row.data('json-value'));
const targetFields = isFixed
? { kind: 'fixed', fixed_field_key: $row.data('fixed-key') }
: { kind: 'custom', mapping_id: $row.data('mapping-id'), field_id: $row.data('field-id') };
// Riga senza corrispondenza: azzera il valore, niente binding.
if ($row.hasClass('is-skipped')) {
return $.post('skip_binding.php', {
...targetFields,
template_id: TEMPLATE_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', {
...targetFields,
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>
+91
View File
@@ -0,0 +1,91 @@
<?php
// Salva un binding JSON -> LIMS (custom o fixed) 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;
}
$kind = ($_POST['kind'] ?? 'custom') === 'fixed' ? 'fixed' : 'custom';
$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 ($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 = Auth::user() ? (int) Auth::user()->present()->id : null;
if ($kind === 'fixed') {
$fixedKey = trim($_POST['fixed_field_key'] ?? '');
$column = binding_fixed_column($fixedKey);
if ($fixedKey === '' || !binding_fixed_is_list($fixedKey) || !$column) {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Invalid fixed field']);
exit;
}
binding_upsert_fixed($pdo, $templateId, $fixedKey, $jsonValue, $limsValueId, $limsValue, $createdBy);
$applied = !empty($datadbIds) ? binding_apply_to_datadb($pdo, $column, $limsValueId, $datadbIds) : 0;
} else {
$mappingId = intval($_POST['mapping_id'] ?? 0);
$fieldId = intval($_POST['field_id'] ?? 0);
if ($mappingId <= 0) {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Missing mapping_id']);
exit;
}
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 = !empty($datadbIds) ? binding_apply_to_details($pdo, $mappingId, $limsValue, $datadbIds) : 0;
}
echo json_encode([
'success' => true,
'applied_rows' => $applied,
'kind' => $kind,
'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()]);
}
@@ -0,0 +1,65 @@
<?php
// Select2 source of LIMS values for a fixed field. Returns {results:[{id,text}]}.
// Params: field_key (required), template_id (required, drives client-dependent lists),
// q (search), id (resolve a single value for preselect), limit.
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(['error' => 'Unauthorized']);
exit;
}
$fieldKey = trim($_GET['field_key'] ?? '');
$templateId = intval($_GET['template_id'] ?? 0);
$q = mb_strtolower(trim($_GET['q'] ?? ''));
$id = isset($_GET['id']) ? intval($_GET['id']) : null;
$rawLimit = intval($_GET['limit'] ?? 30);
$limit = $rawLimit <= 0 ? 0 : max(1, min(500, $rawLimit));
if ($fieldKey === '' || !binding_fixed_is_list($fieldKey)) {
echo json_encode(['results' => []]);
exit;
}
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$values = binding_get_fixed_values($pdo, $fieldKey, $templateId);
if ($id !== null) {
foreach ($values as $v) {
if ((int) $v['id'] === $id) {
echo json_encode(['results' => [['id' => $v['id'], 'text' => $v['text']]]]);
exit;
}
}
echo json_encode(['results' => []]);
exit;
}
$results = [];
foreach ($values as $v) {
$text = (string) $v['text'];
if ($q === '' || mb_strpos(mb_strtolower($text), $q) !== false) {
$results[] = ['id' => $v['id'], 'text' => $text];
if ($limit > 0 && count($results) >= $limit) {
break;
}
}
}
usort($results, fn($a, $b) => strcasecmp($a['text'], $b['text']));
echo json_encode(['results' => $results]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
+72
View File
@@ -0,0 +1,72 @@
<?php
// "Nessuna corrispondenza": azzera il valore importato (custom o fixed) e rimuove il binding. 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;
}
$kind = ($_POST['kind'] ?? 'custom') === 'fixed' ? 'fixed' : 'custom';
$templateId = intval($_POST['template_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 ($jsonValue === '') {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Missing json_value']);
exit;
}
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
if ($kind === 'fixed') {
$fixedKey = trim($_POST['fixed_field_key'] ?? '');
$column = binding_fixed_column($fixedKey);
if ($fixedKey === '' || !binding_fixed_is_list($fixedKey) || !$column || $templateId <= 0) {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Invalid fixed field']);
exit;
}
$cleared = !empty($datadbIds) ? binding_apply_to_datadb($pdo, $column, null, $datadbIds) : 0;
binding_delete_target($pdo, binding_target_fixed($templateId, $fixedKey), $jsonValue);
} else {
$mappingId = intval($_POST['mapping_id'] ?? 0);
if ($mappingId <= 0) {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Missing mapping_id']);
exit;
}
$cleared = !empty($datadbIds) ? binding_apply_to_details($pdo, $mappingId, '', $datadbIds) : 0;
binding_delete_target($pdo, binding_target_custom($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()]);
}