450 lines
24 KiB
PHP
450 lines
24 KiB
PHP
<?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' ? ' ▲' : ' ▼') : '';
|
|
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 → 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 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">· <?= htmlspecialchars((string) $headTplName) ?></span>
|
|
</span>
|
|
</div>
|
|
<span class="small text-muted"><?= $total ?> valore/i · 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 · 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)])) ?>">«</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)])) ?>">»</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>
|