diff --git a/public/userarea/bindings_manage.php b/public/userarea/bindings_manage.php index a5db220..032a13e 100644 --- a/public/userarea/bindings_manage.php +++ b/public/userarea/bindings_manage.php @@ -1,33 +1,140 @@ getConnection(); -// Filtro opzionale per template. +// 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); -$where = ''; +// 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) { - $where = 'WHERE b.template_id = ?'; + $conds[] = '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 +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 - ORDER BY t.name, m.field_label, b.json_value"; -$stmt = $pdo->prepare($sql); -$stmt->execute($params); -$bindings = $stmt->fetchAll(PDO::FETCH_ASSOC); + 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 '' . $label . $caret . ''; +}; ?> @@ -51,6 +158,14 @@ $bindings = $stmt->fetchAll(PDO::FETCH_ASSOC); .row-status { font-size: 12px; } + + .group-row { + cursor: pointer; + } + + .group-row:hover { + background-color: #f1f5ff; + } Gestione Binding JSON → LIMS - <?= htmlspecialchars($titlewebsite ?? '', ENT_QUOTES, 'UTF-8'); ?> @@ -66,82 +181,156 @@ $bindings = $stmt->fetchAll(PDO::FETCH_ASSOC);
Gestione Binding JSON → LIMS
-
- -
- - + + + + + -
-
+ + +
-
- Le modifiche ai binding si applicano alle importazioni future. - I record già importati non vengono ricalcolati. -
+ +
+
+ + Tutti i campi + + + + fixed + · + +
+ valore/i · pagina / +
-
- - - - - - - - - - - - - - +
+
Template Campo (template_mapping) Valore JSON Valore LIMS Azioni
Nessun binding presente.
+ + + + + - - - - - - - - + + + + + - - - -
Azioni
- - - - - -
Nessun valore per questo campo.
-
+ + + + + + + + + + + + + + + + + + +
+ + +
+ Scegli un campo per gestirne i valori. Le modifiche valgono per le importazioni future. +
+
camp con binding · pagina /
+ +
+ + + + + + + + + + + + + + + + + $g['target_key'], 'page' => 1, 'order' => null, 'q' => null]); + ?> + + + + + + + + + +
Nessun binding presente.
+ + fixed + campo rimosso + Apri
+
+ + + 1): ?> + + @@ -159,48 +348,28 @@ $bindings = $stmt->fetchAll(PDO::FETCH_ASSOC); $(function() { const $globalError = $('#globalError'); - // Filtro testuale generale su tutte le colonne. - const $rows = () => $('#bindingsTable tbody tr').not('.no-data-row'); - $('#bindingSearch').on('input', function() { - const q = $(this).val().toLowerCase().trim(); - $rows().each(function() { - const text = $(this).text().toLowerCase(); - $(this).toggle(q === '' || text.indexOf(q) !== -1); - }); - }); - - // Ordinamento per colonna (click sull'intestazione). - $('#bindingsTable thead .sortable').on('click', function() { - const col = +$(this).data('col'); - const asc = !($(this).data('asc')); - $('#bindingsTable thead .sortable').data('asc', null).find('.sort-caret').html(''); - $(this).data('asc', asc).find('.sort-caret').html(asc ? ' ▲' : ' ▼'); - - const $tbody = $('#bindingsTable tbody'); - const rows = $tbody.find('tr').not('.no-data-row').get(); - rows.sort((a, b) => { - const x = $(a).find('td').eq(col).text().trim().toLowerCase(); - const y = $(b).find('td').eq(col).text().trim().toLowerCase(); - return asc ? x.localeCompare(y) : y.localeCompare(x); - }); - rows.forEach(r => $tbody.append(r)); - }); - - $('.binding-select').each(function() { - const fieldId = $(this).data('field-id'); + // 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: 'search_customfield_values.php', + url: isFixed ? 'search_fixed_field_values.php' : 'search_customfield_values.php', dataType: 'json', delay: 200, - data: params => ({ - field_id: fieldId, + 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 || [] }) @@ -208,7 +377,6 @@ $bindings = $stmt->fetchAll(PDO::FETCH_ASSOC); minimumInputLength: 0 }); - // Abilito Salva solo quando il valore cambia davvero. $(this).data('original-val', initialVal); }); @@ -229,9 +397,13 @@ $bindings = $stmt->fetchAll(PDO::FETCH_ASSOC); $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', { - mapping_id: $row.data('mapping-id'), - field_id: $row.data('field-id'), + ...targetFields, template_id: $row.data('template-id'), json_value: String($row.data('json-value')), lims_value_id: $select.val(), diff --git a/public/userarea/class/binding-functions.php b/public/userarea/class/binding-functions.php index faaa9cf..06930a9 100644 --- a/public/userarea/class/binding-functions.php +++ b/public/userarea/class/binding-functions.php @@ -1,6 +1,70 @@ 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 { @@ -9,19 +73,75 @@ function binding_is_list_field(array $mapping): bool return $hasList || $isMultiChoice; } -function binding_lookup(PDO $pdo, int $mappingId, string $jsonValue): ?array +// --------------------------------------------------------------------------- +// 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 mapping_id = ? AND json_value = ? + WHERE target_key = ? AND json_value = ? LIMIT 1" ); - $stmt->execute([$mappingId, $jsonValue]); + $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, @@ -32,20 +152,38 @@ function binding_upsert( 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)" + binding_upsert_row( + $pdo, 'custom', $templateId, $mappingId, null, + binding_target_custom($mappingId), $fieldId, $jsonValue, $limsValueId, $limsValue, $createdBy ); - $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_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 = []; @@ -103,7 +241,166 @@ function binding_auto_match(array $limsValues, string $jsonValue): ?array return count($matches) === 1 ? $matches[0] : null; } -// Scrive il valore risolto sui record indicati (datadb_ids gia' delimita l'import corrente). +// --------------------------------------------------------------------------- +// 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, @@ -126,3 +423,26 @@ function binding_apply_to_details( $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(); +} diff --git a/public/userarea/import_insert.php b/public/userarea/import_insert.php index a0fb58a..0766926 100644 --- a/public/userarea/import_insert.php +++ b/public/userarea/import_insert.php @@ -84,10 +84,22 @@ 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 "mapping_id|json_value". +// Binding JSON -> LIMS senza corrispondenza salvata, per "kind:key|json_value". $pendingBindings = []; // Binding risolti in automatico durante questo import (solo per visualizzazione). $autoBindings = []; @@ -258,9 +270,10 @@ foreach ($selected_rows as $loopIndex => $rowIndex) { if ($existing) { $fieldValue = $existing['lims_value']; - $key = $mapping['id'] . '|' . $jsonValue; + $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'], @@ -289,9 +302,10 @@ foreach ($selected_rows as $loopIndex => $rowIndex) { ); $fieldValue = (string) $autoMatch['Valore']; - $key = $mapping['id'] . '|' . $jsonValue; + $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'], @@ -303,9 +317,10 @@ foreach ($selected_rows as $loopIndex => $rowIndex) { } $autoBindings[$key]['datadb_ids'][] = (int) $iddatadb; } else { - $key = $mapping['id'] . '|' . $jsonValue; + $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'], @@ -326,6 +341,104 @@ 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; diff --git a/public/userarea/resolve_bindings.php b/public/userarea/resolve_bindings.php index 5178881..834274c 100644 --- a/public/userarea/resolve_bindings.php +++ b/public/userarea/resolve_bindings.php @@ -37,6 +37,37 @@ foreach ($savedItems as $s) { $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 = ?"); @@ -124,52 +155,74 @@ $templateName = $stmt->fetchColumn() ?: ('Template ' . $templateId); - + - $item): ?> - - - - - + - - - - - - - - + + + data-fixed-key="" + + data-mapping-id="" + data-field-id="" + + data-json-value="" + data-datadb-ids=""> + + + + + + + + + + data-fixed-key="" + + data-mapping-id="" + data-field-id="" + + data-json-value="" + data-datadb-ids=""> + + + + + +
Campo (template_mapping) Valore JSON Valore LIMS Azioni
- - - -
In attesa
+ +
+ + fixed + · valore/i, da risolvere
- - - -
-
+ + + +
In attesa
+
+ + + +
+
@@ -206,21 +259,29 @@ $templateName = $stmt->fetchColumn() ?: ('Template ' . $templateId); const $btn = $('#confirmBindingsBtn'); const $error = $('#bindingError'); - // Dropdown valori LIMS per riga (search_customfield_values.php). + // Dropdown valori LIMS per riga: sorgente custom vs fixed. $('.binding-select').each(function() { - const fieldId = $(this).data('field-id'); + 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: 'search_customfield_values.php', + url: isFixed ? 'search_fixed_field_values.php' : 'search_customfield_values.php', dataType: 'json', delay: 200, - data: params => ({ - field_id: fieldId, + 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 || [] }) @@ -262,13 +323,20 @@ $templateName = $stmt->fetchColumn() ?: ('Template ' . $templateId); 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', { - mapping_id: $row.data('mapping-id'), + ...targetFields, + template_id: TEMPLATE_ID, json_value: jsonValue, datadb_ids: datadbIds }).then(resp => { @@ -285,8 +353,7 @@ $templateName = $stmt->fetchColumn() ?: ('Template ' . $templateId); const selectedData = $select.select2('data')[0] || {}; return $.post('save_binding.php', { - mapping_id: $row.data('mapping-id'), - field_id: $row.data('field-id'), + ...targetFields, template_id: TEMPLATE_ID, json_value: jsonValue, lims_value_id: $select.val(), diff --git a/public/userarea/save_binding.php b/public/userarea/save_binding.php index 1ec6a9c..2de9055 100644 --- a/public/userarea/save_binding.php +++ b/public/userarea/save_binding.php @@ -1,6 +1,6 @@ LIMS e lo applica ai record appena importati. Ritorna JSON. +// 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'; @@ -23,8 +23,7 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') { exit; } -$mappingId = intval($_POST['mapping_id'] ?? 0); -$fieldId = intval($_POST['field_id'] ?? 0); +$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); @@ -38,7 +37,7 @@ if (isset($_POST['datadb_ids'])) { } } -if ($mappingId <= 0 || $templateId <= 0 || $jsonValue === '' || $limsValueId <= 0) { +if ($templateId <= 0 || $jsonValue === '' || $limsValueId <= 0) { http_response_code(422); echo json_encode(['success' => false, 'error' => 'Missing required parameters']); exit; @@ -47,31 +46,44 @@ if ($mappingId <= 0 || $templateId <= 0 || $jsonValue === '' || $limsValueId <= try { $pdo = DBHandlerSelect::getInstance()->getConnection(); - $createdBy = null; - if (Auth::user()) { - $createdBy = (int) Auth::user()->present()->id; - } + $createdBy = Auth::user() ? (int) Auth::user()->present()->id : null; - if ($fieldId <= 0) { - $stmt = $pdo->prepare("SELECT field_id FROM template_mapping WHERE id = ?"); - $stmt->execute([$mappingId]); - $fieldId = (int) ($stmt->fetchColumn() ?: 0); - } + 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($pdo, $templateId, $mappingId, $fieldId, $jsonValue, $limsValueId, $limsValue, $createdBy); + 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); + } - $applied = 0; - if (!empty($datadbIds)) { - $applied = binding_apply_to_details($pdo, $mappingId, $limsValue, $datadbIds); + 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, - 'mapping_id' => $mappingId, - 'json_value' => $jsonValue, - 'lims_value' => $limsValue, - 'lims_value_id' => $limsValueId, + 'success' => true, + 'applied_rows' => $applied, + 'kind' => $kind, + 'json_value' => $jsonValue, + 'lims_value' => $limsValue, + 'lims_value_id' => $limsValueId, ]); } catch (Exception $e) { http_response_code(500); diff --git a/public/userarea/search_fixed_field_values.php b/public/userarea/search_fixed_field_values.php new file mode 100644 index 0000000..5eea49a --- /dev/null +++ b/public/userarea/search_fixed_field_values.php @@ -0,0 +1,65 @@ + '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()]); +} diff --git a/public/userarea/skip_binding.php b/public/userarea/skip_binding.php index 53efe2e..c33d06b 100644 --- a/public/userarea/skip_binding.php +++ b/public/userarea/skip_binding.php @@ -1,6 +1,6 @@ false, 'error' => 'Missing required parameters']); + echo json_encode(['success' => false, 'error' => 'Missing json_value']); 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]); + 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) {