diff --git a/public/languages/en/general.php b/public/languages/en/general.php
index c0ad6c6b..582ba962 100644
--- a/public/languages/en/general.php
+++ b/public/languages/en/general.php
@@ -25,6 +25,7 @@ $templatedashboard = "Template Dashboard";
$newtemplate = "New Template";
$dashtemplate = "Dashboard Templates";
$insertnewtemplatexls = "Insert New Template XLS";
+$bindingsmanage = "Value Bindings";
// Datatables
$langdatatables = [
"paginate_first" => "First",
diff --git a/public/userarea/bindings_manage.php b/public/userarea/bindings_manage.php
new file mode 100644
index 00000000..692176b2
--- /dev/null
+++ b/public/userarea/bindings_manage.php
@@ -0,0 +1,299 @@
+getConnection();
+
+$templateFilter = isset($_GET['template_id']) ? (int) $_GET['template_id'] : 0;
+$search = trim($_GET['q'] ?? '');
+
+$templates = $pdo->query("SELECT id, name FROM excel_templates ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
+
+$conds = [];
+$params = [];
+
+if ($templateFilter > 0) {
+ $conds[] = 'b.template_id = ?';
+ $params[] = $templateFilter;
+}
+if ($search !== '') {
+ $conds[] = '(b.source_value LIKE ? OR b.lims_value LIKE ? OR m.field_label LIKE ? OR f.fixed_field_key LIKE ?)';
+ $like = '%' . $search . '%';
+ array_push($params, $like, $like, $like, $like);
+}
+$where = $conds ? ('WHERE ' . implode(' AND ', $conds)) : '';
+
+$stmt = $pdo->prepare(
+ "SELECT b.id, b.template_id, b.binding_kind, b.mapping_id, b.field_id, b.source_value,
+ b.lims_value_id, b.lims_value, b.updated_at,
+ t.name AS template_name,
+ COALESCE(m.field_label, f.fixed_field_key) AS field_label,
+ COALESCE(m.excel_column, f.excel_column) AS excel_column,
+ f.fixed_field_key
+ FROM lims_value_binding b
+ LEFT JOIN excel_templates t ON t.id = b.template_id
+ LEFT JOIN template_mapping m ON m.id = b.mapping_id AND b.binding_kind = 'custom'
+ LEFT JOIN template_fixed_mapping f ON f.id = b.mapping_id AND b.binding_kind = 'fixed'
+ $where
+ ORDER BY t.name ASC, field_label ASC, b.source_value ASC"
+);
+$stmt->execute($params);
+$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+$groups = [];
+foreach ($rows as $r) {
+ $kind = $r['binding_kind'] ?? 'custom';
+ $gkey = $kind . ':' . (int) $r['mapping_id'];
+ if (!isset($groups[$gkey])) {
+ $groups[$gkey] = [
+ 'kind' => $kind,
+ 'label' => $kind === 'fixed'
+ ? binding_fixed_label((string) $r['fixed_field_key'])
+ : ($r['field_label'] ?: ('Mapping ' . $gkey)),
+ 'excel_column' => $r['excel_column'] ?: '',
+ 'template_name' => $r['template_name'] ?: ('Template ' . $r['template_id']),
+ 'template_id' => (int) $r['template_id'],
+ 'rows' => [],
+ ];
+ }
+ $groups[$gkey]['rows'][] = $r;
+}
+?>
+
+
+
+
+
+
+
+
+
+
+ Gestione binding - = htmlspecialchars($titlewebsite ?? '', ENT_QUOTES, 'UTF-8'); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Nessun binding salvato per i filtri selezionati.
+
+
+
+
+
+
+ Valore importato
+ Valore LIMS
+ Aggiornato
+ Azioni
+
+
+
+ $g): ?>
+
+
+ = htmlspecialchars($g['label']) ?>
+ campo fisso
+
+ · = htmlspecialchars($g['template_name']) ?>
+
+ · colonna «= htmlspecialchars($g['excel_column']) ?>»
+
+ · = count($g['rows']) ?> valore/i
+
+
+
+
+
+ ›
+ = htmlspecialchars($r['source_value']) ?>
+
+
+
+ = htmlspecialchars($r['lims_value'] ?: ('#' . $r['lims_value_id'])) ?>
+
+
+
+
+ = htmlspecialchars((string) $r['updated_at']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/userarea/class/binding-functions.php b/public/userarea/class/binding-functions.php
new file mode 100644
index 00000000..c6cc41a0
--- /dev/null
+++ b/public/userarea/class/binding-functions.php
@@ -0,0 +1,716 @@
+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;
+}
+
+function binding_lims_value_index(int $fieldId): array
+{
+ static $memo = [];
+
+ if (array_key_exists($fieldId, $memo)) {
+ return $memo[$fieldId];
+ }
+
+ $index = [];
+ foreach (binding_get_lims_values($fieldId) as $v) {
+ $id = (int) ($v['IdCustomFieldsValue'] ?? 0);
+ if ($id > 0) {
+ $index[$id] = (string) ($v['Valore'] ?? '');
+ }
+ }
+
+ return $memo[$fieldId] = $index;
+}
+
+function binding_has_lims_values(int $fieldId): bool
+{
+ return binding_get_lims_values($fieldId) !== [];
+}
+
+function binding_is_lims_id(int $fieldId, $value): bool
+{
+ $raw = trim((string) $value);
+ if ($raw === '' || !ctype_digit($raw)) {
+ return false;
+ }
+
+ $index = binding_lims_value_index($fieldId);
+
+ return $index === [] ? false : isset($index[(int) $raw]);
+}
+
+function binding_auto_match(int $fieldId, string $sourceValue): ?array
+{
+ $needle = binding_normalize_value($sourceValue);
+ if ($needle === '') {
+ return null;
+ }
+
+ $matches = [];
+ foreach (binding_get_lims_values($fieldId) as $v) {
+ if (binding_normalize_value($v['Valore'] ?? '') === $needle) {
+ $matches[] = [
+ 'id' => (int) ($v['IdCustomFieldsValue'] ?? 0),
+ 'text' => (string) ($v['Valore'] ?? ''),
+ ];
+ }
+ }
+
+ return count($matches) === 1 && $matches[0]['id'] > 0 ? $matches[0] : null;
+}
+
+function binding_lookup(PDO $pdo, int $mappingId, string $sourceValue, string $kind = 'custom'): ?array
+{
+ $stmt = $pdo->prepare(
+ "SELECT id, lims_value_id, lims_value, source_value
+ FROM lims_value_binding
+ WHERE binding_kind = ? AND mapping_id = ? AND source_key = ?
+ LIMIT 1"
+ );
+ $stmt->execute([$kind, $mappingId, binding_normalize_value($sourceValue)]);
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ return $row ?: null;
+}
+
+function binding_upsert(
+ PDO $pdo,
+ int $templateId,
+ int $mappingId,
+ int $fieldId,
+ string $sourceValue,
+ int $limsValueId,
+ string $limsValue,
+ ?int $createdBy,
+ string $kind = 'custom'
+): void {
+ $stmt = $pdo->prepare(
+ "INSERT INTO lims_value_binding
+ (template_id, binding_kind, mapping_id, field_id, source_value, source_key, lims_value_id, lims_value, created_by)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ON DUPLICATE KEY UPDATE
+ lims_value_id = VALUES(lims_value_id),
+ lims_value = VALUES(lims_value),
+ field_id = VALUES(field_id),
+ template_id = VALUES(template_id)"
+ );
+ $stmt->execute([
+ $templateId,
+ $kind,
+ $mappingId,
+ $fieldId,
+ mb_substr(trim($sourceValue), 0, 500),
+ mb_substr(binding_normalize_value($sourceValue), 0, 500),
+ $limsValueId,
+ mb_substr($limsValue, 0, 500),
+ $createdBy,
+ ]);
+}
+
+function binding_delete(PDO $pdo, int $mappingId, string $sourceValue, string $kind = 'custom'): int
+{
+ $stmt = $pdo->prepare("DELETE FROM lims_value_binding WHERE binding_kind = ? AND mapping_id = ? AND source_key = ?");
+ $stmt->execute([$kind, $mappingId, binding_normalize_value($sourceValue)]);
+
+ return $stmt->rowCount();
+}
+
+function binding_delete_by_id(PDO $pdo, int $bindingId): int
+{
+ $stmt = $pdo->prepare("DELETE FROM lims_value_binding WHERE id = ?");
+ $stmt->execute([$bindingId]);
+
+ return $stmt->rowCount();
+}
+
+function binding_suggest(PDO $pdo, int $fieldId, string $sourceValue, int $excludeMappingId): ?array
+{
+ if ($fieldId <= 0) {
+ return null;
+ }
+
+ $stmt = $pdo->prepare(
+ "SELECT b.lims_value_id, b.lims_value, b.template_id, t.name AS template_name
+ FROM lims_value_binding b
+ LEFT JOIN excel_templates t ON t.id = b.template_id
+ WHERE b.binding_kind = 'custom' AND b.field_id = ? AND b.source_key = ? AND b.mapping_id <> ?
+ ORDER BY b.updated_at DESC
+ LIMIT 1"
+ );
+ $stmt->execute([$fieldId, binding_normalize_value($sourceValue), $excludeMappingId]);
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ return $row ?: null;
+}
+
+function binding_apply_to_details(PDO $pdo, int $mappingId, string $value, array $datadbIds): int
+{
+ $datadbIds = array_values(array_filter(array_map('intval', $datadbIds)));
+ if (empty($datadbIds)) {
+ return 0;
+ }
+
+ $placeholders = implode(',', array_fill(0, count($datadbIds), '?'));
+ $stmt = $pdo->prepare(
+ "UPDATE import_data_details
+ SET field_value = ?
+ WHERE mapping_id = ? AND id IN ($placeholders)"
+ );
+ $stmt->execute(array_merge([$value, $mappingId], $datadbIds));
+
+ return $stmt->rowCount();
+}
+
+function binding_find_column_index(string $column, array $columns)
+{
+ $needle = binding_normalize_value($column);
+ if ($needle === '') {
+ return false;
+ }
+
+ foreach ($columns as $i => $c) {
+ if (binding_normalize_value($c) === $needle) {
+ return $i;
+ }
+ }
+
+ return false;
+}
+
+function binding_fixed_alias_map(): array
+{
+ return [
+ 'ClienteResponsabile' => 'cliente_responsabile_id',
+ 'ClienteFornitore' => 'cliente_fornitore_id',
+ 'ClienteAnalisi' => 'clienteAnalisi',
+ 'ClienteFatturazione' => 'ClienteFatturazione',
+ 'MoltiplicatorePrezzo' => 'moltiplicatore_prezzo_id',
+ 'AnagraficaCertestObject' => 'anagrafica_certest_object_id',
+ 'AnagraficaCertestService' => 'anagrafica_certest_service_id',
+ 'ConsegnaRichiesta' => 'consegna_richiesta',
+ ];
+}
+
+function binding_fixed_column(string $key): ?string
+{
+ return binding_fixed_alias_map()[$key] ?? null;
+}
+
+function binding_fixed_is_list(string $key): bool
+{
+ return in_array($key, [
+ 'ClienteResponsabile',
+ 'ClienteFornitore',
+ 'ClienteAnalisi',
+ 'ClienteFatturazione',
+ 'MoltiplicatorePrezzo',
+ 'AnagraficaCertestObject',
+ 'AnagraficaCertestService',
+ ], true);
+}
+
+function binding_fixed_is_date(string $key): bool
+{
+ return $key === 'ConsegnaRichiesta';
+}
+
+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' => 'Buyer',
+ 'ClienteFatturazione' => 'Cliente Fatturazione',
+ 'ConsegnaRichiesta' => 'Consegna Richiesta',
+ ];
+
+ return $labels[$key] ?? $key;
+}
+
+function binding_parse_date($value): ?string
+{
+ $raw = trim((string) $value);
+ if ($raw === '') {
+ return null;
+ }
+
+ foreach (['Y-m-d', 'd/m/Y', 'd-m-Y', 'd.m.Y', 'Y/m/d', 'd/m/y', 'Y-m-d H:i:s'] as $format) {
+ $date = DateTime::createFromFormat($format, $raw);
+ if ($date && $date->format($format) === $raw) {
+ return $date->format('Y-m-d');
+ }
+ }
+
+ $ts = strtotime($raw);
+
+ return $ts ? date('Y-m-d', $ts) : null;
+}
+
+function binding_fixed_default(array $fixedMapping): string
+{
+ $value = trim((string) ($fixedMapping['default_value'] ?? ''));
+ if ($value === '') {
+ return '';
+ }
+
+ $isDate = strcasecmp((string) ($fixedMapping['data_type'] ?? ''), 'DATE') === 0
+ || binding_fixed_is_date((string) ($fixedMapping['fixed_field_key'] ?? ''));
+
+ if ($isDate && preg_match('/^today\s*(?:\+\s*(\d+))?$/i', $value, $m)) {
+ return date('Y-m-d', strtotime('+' . (int) ($m[1] ?? 0) . ' days'));
+ }
+
+ return $value;
+}
+
+function binding_cached_get($api, string $cacheFile, string $endpoint, int $ttl = 3600): array
+{
+ if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < $ttl)) {
+ $data = json_decode(file_get_contents($cacheFile), true);
+ if (is_array($data)) {
+ return $data;
+ }
+ }
+
+ $data = $api->get($endpoint);
+ if (is_array($data)) {
+ $dir = dirname($cacheFile);
+ if (!is_dir($dir)) {
+ mkdir($dir, 0777, true);
+ }
+ file_put_contents($cacheFile, json_encode($data));
+ }
+
+ return is_array($data) ? $data : [];
+}
+
+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((string) ($client['Nominativo'] ?? ''));
+ $code = trim((string) ($client['CodiceCliente'] ?? ''));
+
+ return $code !== '' ? ($name . ' - ' . $code) : $name;
+}
+
+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'] ?? ''));
+ $name = (string) ($r['NomeAnagrafica'] ?? '');
+ $out[] = [
+ 'id' => (int) ($r['IdAnagrafica'] ?? 0),
+ 'text' => $name,
+ 'aliases' => array_values(array_filter([$code, $code !== '' ? "$code - $name" : ''])),
+ ];
+ }
+ 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':
+ case 'ClienteFatturazione':
+ $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] = array_values(array_filter($out, fn($v) => $v['id'] > 0));
+}
+
+function binding_fixed_has_values(PDO $pdo, string $fieldKey, int $templateId): bool
+{
+ return binding_get_fixed_values($pdo, $fieldKey, $templateId) !== [];
+}
+
+function binding_fixed_is_lims_id(PDO $pdo, string $fieldKey, int $templateId, $value): bool
+{
+ $raw = trim((string) $value);
+ if ($raw === '' || !ctype_digit($raw)) {
+ return false;
+ }
+
+ foreach (binding_get_fixed_values($pdo, $fieldKey, $templateId) as $v) {
+ if ((int) $v['id'] === (int) $raw) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function binding_auto_match_fixed(array $values, string $sourceValue): ?array
+{
+ $needle = binding_normalize_value($sourceValue);
+ if ($needle === '') {
+ return null;
+ }
+
+ $matches = [];
+ foreach ($values as $v) {
+ $candidates = array_merge([$v['text'] ?? ''], $v['aliases'] ?? []);
+ foreach ($candidates as $candidate) {
+ if (binding_normalize_value($candidate) === $needle) {
+ $matches[] = $v;
+ break;
+ }
+ }
+ }
+
+ return count($matches) === 1 ? $matches[0] : null;
+}
+
+function binding_suggest_fixed(PDO $pdo, string $fieldKey, string $sourceValue, int $excludeMappingId): ?array
+{
+ $stmt = $pdo->prepare(
+ "SELECT b.lims_value_id, b.lims_value, b.template_id, t.name AS template_name
+ FROM lims_value_binding b
+ JOIN template_fixed_mapping f ON f.id = b.mapping_id
+ LEFT JOIN excel_templates t ON t.id = b.template_id
+ WHERE b.binding_kind = 'fixed' AND f.fixed_field_key = ?
+ AND b.source_key = ? AND b.mapping_id <> ?
+ ORDER BY b.updated_at DESC
+ LIMIT 1"
+ );
+ $stmt->execute([$fieldKey, binding_normalize_value($sourceValue), $excludeMappingId]);
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ return $row ?: null;
+}
+
+function binding_apply_to_datadb(PDO $pdo, string $fieldKey, $value, array $datadbIds): int
+{
+ $column = binding_fixed_column($fieldKey);
+ if ($column === null) {
+ throw new InvalidArgumentException("Campo fisso sconosciuto: $fieldKey");
+ }
+
+ $datadbIds = array_values(array_filter(array_map('intval', $datadbIds)));
+ if (empty($datadbIds)) {
+ return 0;
+ }
+
+ $placeholders = implode(',', array_fill(0, count($datadbIds), '?'));
+ $stmt = $pdo->prepare("UPDATE datadb SET `$column` = ? WHERE iddatadb IN ($placeholders)");
+ $stmt->execute(array_merge([$value === '' ? null : $value], $datadbIds));
+
+ return $stmt->rowCount();
+}
+
+function binding_write_fixed_columns(PDO $pdo, int $iddatadb, array $valuesByKey): int
+{
+ $map = binding_fixed_alias_map();
+ $sets = [];
+ $params = [];
+
+ foreach ($valuesByKey as $key => $value) {
+ if (!isset($map[$key]) || $value === null || $value === '') {
+ continue;
+ }
+ $sets[] = "`{$map[$key]}` = ?";
+ $params[] = $value;
+ }
+
+ if (empty($sets)) {
+ return 0;
+ }
+
+ $params[] = $iddatadb;
+ $stmt = $pdo->prepare("UPDATE datadb SET " . implode(', ', $sets) . " WHERE iddatadb = ?");
+ $stmt->execute($params);
+
+ return $stmt->rowCount();
+}
+
+function binding_resolve_for_import(
+ PDO $pdo,
+ array $mapping,
+ int $templateId,
+ string $sourceValue,
+ int $iddatadb,
+ ?int $userId,
+ array &$saved,
+ array &$auto,
+ array &$pending
+): string {
+ $mappingId = (int) $mapping['id'];
+ $fieldId = (int) $mapping['field_id'];
+ $key = 'cf:' . $mappingId . '|' . binding_normalize_value($sourceValue);
+
+ $collect = function (array &$bucket, array $extra = []) use ($key, $mapping, $mappingId, $fieldId, $sourceValue, $iddatadb) {
+ if (!isset($bucket[$key])) {
+ $bucket[$key] = array_merge([
+ 'kind' => 'custom',
+ 'mapping_id' => $mappingId,
+ 'field_id' => $fieldId,
+ 'field_label' => $mapping['field_label'] ?? ('Mapping ' . $mappingId),
+ 'source_value' => $sourceValue,
+ 'datadb_ids' => [],
+ ], $extra);
+ }
+ $bucket[$key]['datadb_ids'][] = $iddatadb;
+ };
+
+ if (!binding_has_lims_values($fieldId)) {
+ return $sourceValue;
+ }
+
+ if (binding_is_lims_id($fieldId, $sourceValue)) {
+ return $sourceValue;
+ }
+
+ if ($existing = binding_lookup($pdo, $mappingId, $sourceValue)) {
+ $collect($saved, [
+ 'lims_value_id' => (int) $existing['lims_value_id'],
+ 'lims_value' => (string) $existing['lims_value'],
+ ]);
+
+ return (string) $existing['lims_value_id'];
+ }
+
+ if ($match = binding_auto_match($fieldId, $sourceValue)) {
+ binding_upsert($pdo, $templateId, $mappingId, $fieldId, $sourceValue, $match['id'], $match['text'], $userId);
+ $collect($auto, [
+ 'lims_value_id' => $match['id'],
+ 'lims_value' => $match['text'],
+ ]);
+
+ return (string) $match['id'];
+ }
+
+ $collect($pending);
+
+ return $sourceValue;
+}
+
+function binding_resolve_fixed_for_import(
+ PDO $pdo,
+ array $fixedMapping,
+ int $templateId,
+ string $sourceValue,
+ int $iddatadb,
+ ?int $userId,
+ array &$saved,
+ array &$auto,
+ array &$pending
+): ?string {
+ $fieldKey = (string) $fixedMapping['fixed_field_key'];
+ $mappingId = (int) $fixedMapping['id'];
+ $key = 'fx:' . $mappingId . '|' . binding_normalize_value($sourceValue);
+
+ $collect = function (array &$bucket, array $extra = []) use ($key, $fieldKey, $mappingId, $sourceValue, $iddatadb) {
+ if (!isset($bucket[$key])) {
+ $bucket[$key] = array_merge([
+ 'kind' => 'fixed',
+ 'mapping_id' => $mappingId,
+ 'fixed_field_key' => $fieldKey,
+ 'field_id' => 0,
+ 'field_label' => binding_fixed_label($fieldKey),
+ 'source_value' => $sourceValue,
+ 'datadb_ids' => [],
+ ], $extra);
+ }
+ $bucket[$key]['datadb_ids'][] = $iddatadb;
+ };
+
+ if (binding_fixed_is_date($fieldKey)) {
+ return binding_parse_date($sourceValue);
+ }
+
+ if (!binding_fixed_is_list($fieldKey)) {
+ return $sourceValue;
+ }
+
+ if (!binding_fixed_has_values($pdo, $fieldKey, $templateId)) {
+ error_log("[BINDING] campo fisso $fieldKey: lista LIMS non disponibile, valore non scritto");
+
+ return null;
+ }
+
+ if (binding_fixed_is_lims_id($pdo, $fieldKey, $templateId, $sourceValue)) {
+ return (string) (int) $sourceValue;
+ }
+
+ if ($existing = binding_lookup($pdo, $mappingId, $sourceValue, 'fixed')) {
+ $collect($saved, [
+ 'lims_value_id' => (int) $existing['lims_value_id'],
+ 'lims_value' => (string) $existing['lims_value'],
+ ]);
+
+ return (string) $existing['lims_value_id'];
+ }
+
+ if (binding_fixed_auto_matchable($fieldKey)) {
+ $match = binding_auto_match_fixed(binding_get_fixed_values($pdo, $fieldKey, $templateId), $sourceValue);
+ if ($match) {
+ binding_upsert($pdo, $templateId, $mappingId, 0, $sourceValue, (int) $match['id'], (string) $match['text'], $userId, 'fixed');
+ $collect($auto, [
+ 'lims_value_id' => (int) $match['id'],
+ 'lims_value' => (string) $match['text'],
+ ]);
+
+ return (string) $match['id'];
+ }
+ }
+
+ $collect($pending);
+
+ return null;
+}
+
+function binding_merge_buckets(array $bucket, array $items): array
+{
+ foreach ($items as $key => $item) {
+ if (!isset($bucket[$key])) {
+ $bucket[$key] = $item;
+ continue;
+ }
+ $bucket[$key]['datadb_ids'] = array_values(array_unique(array_merge(
+ $bucket[$key]['datadb_ids'] ?? [],
+ $item['datadb_ids'] ?? []
+ )));
+ }
+
+ return $bucket;
+}
+
+function binding_list_for_template(PDO $pdo, int $templateId): array
+{
+ $stmt = $pdo->prepare(
+ "SELECT b.*,
+ COALESCE(m.field_label, f.fixed_field_key) AS field_label,
+ COALESCE(m.excel_column, f.excel_column) AS excel_column,
+ f.fixed_field_key
+ FROM lims_value_binding b
+ LEFT JOIN template_mapping m ON m.id = b.mapping_id AND b.binding_kind = 'custom'
+ LEFT JOIN template_fixed_mapping f ON f.id = b.mapping_id AND b.binding_kind = 'fixed'
+ WHERE b.template_id = ?
+ ORDER BY field_label ASC, b.source_value ASC"
+ );
+ $stmt->execute([$templateId]);
+
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+}
diff --git a/public/userarea/delete_binding.php b/public/userarea/delete_binding.php
new file mode 100644
index 00000000..008b2caa
--- /dev/null
+++ b/public/userarea/delete_binding.php
@@ -0,0 +1,40 @@
+ false, 'error' => 'Unauthorized']);
+ exit;
+}
+
+if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+ http_response_code(405);
+ echo json_encode(['success' => false, 'error' => 'Method not allowed']);
+ exit;
+}
+
+$bindingId = intval($_POST['binding_id'] ?? 0);
+
+if ($bindingId <= 0) {
+ http_response_code(422);
+ echo json_encode(['success' => false, 'error' => 'Missing binding_id']);
+ exit;
+}
+
+try {
+ $pdo = DBHandlerSelect::getInstance()->getConnection();
+ $deleted = binding_delete_by_id($pdo, $bindingId);
+
+ echo json_encode(['success' => $deleted > 0, 'deleted' => $deleted]);
+} catch (Exception $e) {
+ http_response_code(500);
+ echo json_encode(['success' => false, 'error' => $e->getMessage()]);
+}
diff --git a/public/userarea/import_insert.php b/public/userarea/import_insert.php
index f861f0f7..2582cb07 100644
--- a/public/userarea/import_insert.php
+++ b/public/userarea/import_insert.php
@@ -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"));
@@ -70,7 +71,7 @@ $pdo = $db->getConnection();
$importReferenceCode = date('YmdHis') . '-' . uniqid();
// Recupera tutti i mapping dal template
-$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field, auto_value FROM template_mapping WHERE template_id = ?");
+$stmt = $pdo->prepare("SELECT id, excel_column, data_type, has_list, is_required, manual_default, is_manual, field_label, field_id, main_field, auto_value FROM template_mapping WHERE template_id = ?");
$stmt->execute([$template_id]);
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -88,6 +89,23 @@ foreach ($allMappings as $mapping) {
}
}
+$fixedMappings = [];
+try {
+ $fxStmt = $pdo->prepare(
+ "SELECT id, fixed_field_key, data_type, default_value, default_source, excel_column
+ FROM template_fixed_mapping
+ WHERE template_id = ?"
+ );
+ $fxStmt->execute([$template_id]);
+ $fixedMappings = $fxStmt->fetchAll(PDO::FETCH_ASSOC);
+} catch (Exception $e) {
+ error_log("[FIXED] lettura template_fixed_mapping fallita: " . $e->getMessage());
+}
+
+$savedBindings = []; // risolti con un binding gia' salvato
+$autoBindings = []; // collegati in automatico (corrispondenza esatta)
+$pendingBindings = []; // senza corrispondenza: da risolvere a mano
+
// Inserisci le righe selezionate in datadb
$insertedIds = [];
foreach ($selected_rows as $rowIndex) {
@@ -172,6 +190,24 @@ foreach ($selected_rows as $rowIndex) {
}
}
+ if (
+ binding_is_list_field($mapping)
+ && $fieldValue !== null
+ && trim((string) $fieldValue) !== ''
+ ) {
+ $fieldValue = binding_resolve_for_import(
+ $pdo,
+ $mapping,
+ (int) $template_id,
+ (string) $fieldValue,
+ (int) $iddatadb,
+ (int) $user_id,
+ $savedBindings,
+ $autoBindings,
+ $pendingBindings
+ );
+ }
+
if ($mapping['is_required'] && (is_null($fieldValue) || $fieldValue === '')) {
error_log("Required field missing for mapping ID: " . $mapping['id'] . ", field: " . $mapping['field_label']);
}
@@ -180,6 +216,43 @@ foreach ($selected_rows as $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));
}
+ $fixedValues = [];
+ foreach ($fixedMappings as $fm) {
+ $fixedKey = (string) $fm['fixed_field_key'];
+ $value = null;
+
+ if (($fm['default_source'] ?? 'manual') === 'xls' && trim((string) ($fm['excel_column'] ?? '')) !== '') {
+ $idx = binding_find_column_index((string) $fm['excel_column'], $columns);
+ $raw = ($idx !== false && isset($row[$idx])) ? trim((string) $row[$idx]) : '';
+
+ if ($raw !== '') {
+ $value = binding_resolve_fixed_for_import(
+ $pdo,
+ $fm,
+ (int) $template_id,
+ $raw,
+ (int) $iddatadb,
+ (int) $user_id,
+ $savedBindings,
+ $autoBindings,
+ $pendingBindings
+ );
+ }
+ }
+
+ if ($value === null || $value === '') {
+ $default = binding_fixed_default($fm);
+ $value = $default !== '' ? $default : null;
+ }
+
+ if ($value !== null && $value !== '') {
+ $fixedValues[$fixedKey] = $value;
+ }
+ }
+
+ if (!empty($fixedValues)) {
+ binding_write_fixed_columns($pdo, (int) $iddatadb, $fixedValues);
+ }
}
// ------------------------------------------------------------
@@ -237,5 +310,20 @@ try {
$_SESSION['inserted_ids'] = $insertedIds;
+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']);
+
header("Location: imported.php?id=" . urlencode($template_id) . "&importref=" . urlencode($importReferenceCode));
exit;
diff --git a/public/userarea/import_insert_batch.php b/public/userarea/import_insert_batch.php
index 5c6c0873..4557c246 100644
--- a/public/userarea/import_insert_batch.php
+++ b/public/userarea/import_insert_batch.php
@@ -5,6 +5,7 @@ ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/import_debug.log');
include('include/headscript.php');
+require_once __DIR__ . '/class/binding-functions.php';
header('Content-Type: application/json');
@@ -49,7 +50,7 @@ $db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Recupera i mapping
-$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field, auto_value FROM template_mapping WHERE template_id = ?");
+$stmt = $pdo->prepare("SELECT id, excel_column, data_type, has_list, is_required, manual_default, is_manual, field_label, field_id, main_field, auto_value FROM template_mapping WHERE template_id = ?");
$stmt->execute([$template_id]);
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -76,6 +77,23 @@ $mappingId244 = (int)$stmtMap->fetchColumn();
$insertedIds = [];
+$fixedMappings = [];
+try {
+ $fxStmt = $pdo->prepare(
+ "SELECT id, fixed_field_key, data_type, default_value, default_source, excel_column
+ FROM template_fixed_mapping
+ WHERE template_id = ?"
+ );
+ $fxStmt->execute([$template_id]);
+ $fixedMappings = $fxStmt->fetchAll(PDO::FETCH_ASSOC);
+} catch (Exception $e) {
+ error_log("[FIXED] lettura template_fixed_mapping fallita: " . $e->getMessage());
+}
+
+$savedBindings = [];
+$autoBindings = [];
+$pendingBindings = [];
+
try {
$pdo->beginTransaction();
@@ -146,9 +164,65 @@ try {
}
}
+ if (
+ binding_is_list_field($mapping)
+ && $fieldValue !== null
+ && trim((string) $fieldValue) !== ''
+ ) {
+ $fieldValue = binding_resolve_for_import(
+ $pdo,
+ $mapping,
+ $template_id,
+ (string) $fieldValue,
+ (int) $iddatadb,
+ (int) $user_id,
+ $savedBindings,
+ $autoBindings,
+ $pendingBindings
+ );
+ }
+
$insDetail->execute([$iddatadb, $mapping['id'], $fieldValue]);
}
+ $fixedValues = [];
+ foreach ($fixedMappings as $fm) {
+ $fixedKey = (string) $fm['fixed_field_key'];
+ $value = null;
+
+ if (($fm['default_source'] ?? 'manual') === 'xls' && trim((string) ($fm['excel_column'] ?? '')) !== '') {
+ $idx = binding_find_column_index((string) $fm['excel_column'], $columns);
+ $raw = ($idx !== false && isset($row[$idx])) ? trim((string) $row[$idx]) : '';
+
+ if ($raw !== '') {
+ $value = binding_resolve_fixed_for_import(
+ $pdo,
+ $fm,
+ (int) $template_id,
+ $raw,
+ (int) $iddatadb,
+ (int) $user_id,
+ $savedBindings,
+ $autoBindings,
+ $pendingBindings
+ );
+ }
+ }
+
+ if ($value === null || $value === '') {
+ $default = binding_fixed_default($fm);
+ $value = $default !== '' ? $default : null;
+ }
+
+ if ($value !== null && $value !== '') {
+ $fixedValues[$fixedKey] = $value;
+ }
+ }
+
+ if (!empty($fixedValues)) {
+ binding_write_fixed_columns($pdo, (int) $iddatadb, $fixedValues);
+ }
+
// Campo 244 (Accettatore) con lims_user_id
if ($limsUserId !== '' && $mappingId244 > 0) {
$insDetail->execute([$iddatadb, $mappingId244, $limsUserId]);
@@ -157,9 +231,27 @@ try {
$pdo->commit();
+ $state = $_SESSION['pending_bindings'] ?? null;
+ if (!is_array($state) || ($state['importref'] ?? null) !== $importReferenceCode) {
+ $state = [
+ 'template_id' => $template_id,
+ 'importref' => $importReferenceCode,
+ 'items' => [],
+ 'auto' => [],
+ 'saved' => [],
+ ];
+ }
+
+ $state['items'] = binding_merge_buckets($state['items'], $pendingBindings);
+ $state['auto'] = binding_merge_buckets($state['auto'], $autoBindings);
+ $state['saved'] = binding_merge_buckets($state['saved'], $savedBindings);
+
+ $_SESSION['pending_bindings'] = $state;
+
echo json_encode([
- 'ok' => true,
- 'inserted' => count($insertedIds)
+ 'ok' => true,
+ 'inserted' => count($insertedIds),
+ 'pending_bindings' => count($state['items'])
]);
exit;
} catch (Exception $e) {
diff --git a/public/userarea/import_xls2.php b/public/userarea/import_xls2.php
index b3d94d03..4b795d78 100644
--- a/public/userarea/import_xls2.php
+++ b/public/userarea/import_xls2.php
@@ -492,6 +492,7 @@ error_log("Loaded template: " . print_r($template, true));
const progressErr = document.getElementById('importProgressError');
let insertedTotal = 0;
+ let pendingBindings = 0; // valori a lista senza corrispondenza LIMS
// Invia un batch e ritorna la risposta JSON
async function sendBatch(startIndex) {
@@ -550,6 +551,7 @@ error_log("Loaded template: " . print_r($template, true));
}
insertedTotal += (result.inserted || 0);
+ pendingBindings = result.pending_bindings || pendingBindings;
const done = Math.min(start + BATCH_SIZE, total);
const pct = Math.round((done / total) * 100);
progressBar.style.width = pct + '%';
@@ -565,7 +567,9 @@ error_log("Loaded template: " . print_r($template, true));
// PAUSA temporanea di 2 secondi per vedere la barra completa (da rimuovere dopo il test)
await new Promise(r => setTimeout(r, 2000));
- window.location.href = 'imported.php?id=' +
+ window.location.href = pendingBindings > 0
+ ? 'resolve_bindings.php'
+ : 'imported.php?id=' +
encodeURIComponent(data.template_id) +
'&importref=' + encodeURIComponent(importReferenceCode);
});
diff --git a/public/userarea/include/navbar.php b/public/userarea/include/navbar.php
index 5f7d63cd..6151e185 100644
--- a/public/userarea/include/navbar.php
+++ b/public/userarea/include/navbar.php
@@ -64,6 +64,11 @@
= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?>
+
+
+ = htmlspecialchars($bindingsmanage ?? 'Value Bindings', ENT_QUOTES, 'UTF-8'); ?>
+
+
diff --git a/public/userarea/mapping_template_xls_scheme2.php b/public/userarea/mapping_template_xls_scheme2.php
index 07a68ce8..a3b74de6 100644
--- a/public/userarea/mapping_template_xls_scheme2.php
+++ b/public/userarea/mapping_template_xls_scheme2.php
@@ -88,7 +88,8 @@ $mappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Recupera i fixed fields dalla tabella template_fixed_mapping
$stmt = $pdo->prepare("
- SELECT id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import
+ SELECT id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import,
+ default_source, excel_column
FROM template_fixed_mapping
WHERE template_id = ?
ORDER BY id ASC
@@ -594,6 +595,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
Required
Field
Type
+ Source
Default Value
@@ -626,6 +628,28 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
+
+
+
+ >Default value
+
+ >Map to XLS Column
+
+
+ >Map to JSON Node
+
+
+
+
+ >
+
+
+
+
@@ -1279,8 +1303,6 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
});
}
-
-
function saveJsonNodes(sampleJson, nodes) {
return fetch('update_api_json_nodes.php', {
method: 'POST',
@@ -1363,6 +1385,41 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
});
// fallback (nel caso cambi senza select2)
+ $(document).on('change', '.fixed-source-select', function() {
+ const id = this.dataset.fixedId;
+ const value = this.value;
+ const $colSelect = $(this).closest('td').find('.fixed-xls-column');
+
+ $colSelect.toggle(value === 'xls');
+ if (value !== 'xls') {
+ $colSelect.val('');
+ }
+
+ saveFixedField(id, 'default_source', value);
+ });
+
+ $(document).on('change', '.fixed-xls-column', function() {
+ saveFixedField(this.dataset.fixedId, 'excel_column', this.value || null);
+ });
+
+ function saveFixedField(id, field, value) {
+ const status = document.getElementById('fixedSaveStatus');
+ if (status) status.textContent = 'Salvataggio...';
+
+ fetch('update_fixed_field.php', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ id: parseInt(id, 10), field: field, value: value })
+ })
+ .then(r => r.json())
+ .then(d => {
+ if (status) status.textContent = d.success ? 'Salvato' : ('Errore: ' + (d.message || ''));
+ })
+ .catch(() => {
+ if (status) status.textContent = 'Errore di rete';
+ });
+ }
+
$(document).on('change', '.fixed-default-select', function() {
saveFixedDefaultSelect(this);
});
@@ -1882,7 +1939,6 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
});
}
-
// Save original Main checkbox state
document.querySelectorAll('#schemaFieldsBody .main-field-checkbox').forEach(cb => {
cb.dataset.originalChecked = cb.checked ? 'true' : 'false';
@@ -2031,7 +2087,6 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
}
});
-
document.getElementById('schemaFieldsBody').addEventListener('change', function(event) {
if (event.target.classList.contains('manual-default') && event.target.tagName === 'SELECT') {
let tr = event.target.closest('tr');
@@ -2233,7 +2288,6 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
${escapeHtml(fixedFieldLabels[r.fixed_field_key] || r.fixed_field_key)}
-
${escapeHtml(r.data_type || '')}
diff --git a/public/userarea/resolve_bindings.php b/public/userarea/resolve_bindings.php
new file mode 100644
index 00000000..8c4e2385
--- /dev/null
+++ b/public/userarea/resolve_bindings.php
@@ -0,0 +1,417 @@
+getConnection();
+
+$stmt = $pdo->prepare("SELECT name FROM excel_templates WHERE id = ?");
+$stmt->execute([$templateId]);
+$templateName = $stmt->fetchColumn() ?: ('Template ' . $templateId);
+
+foreach ($items as $i => $item) {
+ $items[$i]['suggestion'] = ($item['kind'] ?? 'custom') === 'fixed'
+ ? binding_suggest_fixed(
+ $pdo,
+ (string) ($item['fixed_field_key'] ?? ''),
+ (string) $item['source_value'],
+ (int) $item['mapping_id']
+ )
+ : binding_suggest(
+ $pdo,
+ (int) $item['field_id'],
+ (string) $item['source_value'],
+ (int) $item['mapping_id']
+ );
+}
+
+$resolvedItems = [];
+foreach ($autoItems as $a) {
+ $a['badge'] = 'automatico';
+ $a['badge_class'] = 'bg-success';
+ $resolvedItems[] = $a;
+}
+foreach ($savedItems as $s) {
+ $s['badge'] = 'binding salvato';
+ $s['badge_class'] = 'bg-secondary';
+ $resolvedItems[] = $s;
+}
+
+$groups = [];
+$groupOrder = [];
+$pushToGroup = function (array $row, string $type, $idx = null) use (&$groups, &$groupOrder) {
+ $kind = $row['kind'] ?? 'custom';
+ $gkey = $kind . ':' . (int) ($row['mapping_id'] ?? 0);
+ if (!isset($groups[$gkey])) {
+ $groups[$gkey] = [
+ 'label' => $row['field_label'] ?? ('Mapping ' . $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');
+}
+
+$pendingRecords = 0;
+foreach ($items as $item) {
+ $pendingRecords += count($item['datadb_ids'] ?? []);
+}
+?>
+
+
+
+
+
+
+
+
+
+
+ Binding valori → LIMS - = htmlspecialchars($titlewebsite ?? '', ENT_QUOTES, 'UTF-8'); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Alcuni valori importati dal file non corrispondono a nessun valore delle liste LIMS.
+ Finche' non vengono collegati, questi campi non possono essere esportati.
+ Scegli il valore LIMS corretto per ciascuno: il binding viene salvato e
+ riutilizzato nelle importazioni successive.
+
+ = count($items) ?> valore/i da risolvere su
+ = $pendingRecords ?> record importati.
+
+
+
+
+ = count($autoItems) ?> valore/i collegato/i automaticamente (corrispondenza esatta con il LIMS).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/userarea/save_binding.php b/public/userarea/save_binding.php
new file mode 100644
index 00000000..a6fe8e9b
--- /dev/null
+++ b/public/userarea/save_binding.php
@@ -0,0 +1,101 @@
+ 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);
+$mappingId = intval($_POST['mapping_id'] ?? 0);
+$fieldId = intval($_POST['field_id'] ?? 0);
+$sourceValue = (string) ($_POST['source_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 || $mappingId <= 0 || trim($sourceValue) === '' || $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') {
+ $stmt = $pdo->prepare("SELECT fixed_field_key FROM template_fixed_mapping WHERE id = ?");
+ $stmt->execute([$mappingId]);
+ $fixedKey = (string) ($stmt->fetchColumn() ?: '');
+
+ if ($fixedKey === '' || !binding_fixed_is_list($fixedKey)) {
+ http_response_code(422);
+ echo json_encode(['success' => false, 'error' => 'Invalid fixed field']);
+ exit;
+ }
+
+ binding_upsert($pdo, $templateId, $mappingId, 0, $sourceValue, $limsValueId, $limsValue, $createdBy, 'fixed');
+
+ $applied = !empty($datadbIds)
+ ? binding_apply_to_datadb($pdo, $fixedKey, (string) $limsValueId, $datadbIds)
+ : 0;
+
+ echo json_encode([
+ 'success' => true,
+ 'applied_rows' => $applied,
+ 'kind' => 'fixed',
+ 'source_value' => $sourceValue,
+ 'lims_value' => $limsValue,
+ 'lims_value_id' => $limsValueId,
+ ]);
+ 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, $sourceValue, $limsValueId, $limsValue, $createdBy);
+
+ $applied = !empty($datadbIds)
+ ? binding_apply_to_details($pdo, $mappingId, (string) $limsValueId, $datadbIds)
+ : 0;
+
+ echo json_encode([
+ 'success' => true,
+ 'applied_rows' => $applied,
+ 'kind' => 'custom',
+ 'source_value' => $sourceValue,
+ 'lims_value' => $limsValue,
+ 'lims_value_id' => $limsValueId,
+ ]);
+} catch (Exception $e) {
+ http_response_code(500);
+ echo json_encode(['success' => false, 'error' => $e->getMessage()]);
+}
diff --git a/public/userarea/search_fixed_field_values.php b/public/userarea/search_fixed_field_values.php
new file mode 100644
index 00000000..b06b536d
--- /dev/null
+++ b/public/userarea/search_fixed_field_values.php
@@ -0,0 +1,54 @@
+ 'Unauthorized']);
+ exit;
+}
+
+$fieldKey = trim($_GET['field_key'] ?? '');
+$templateId = (int) ($_GET['template_id'] ?? 0);
+$q = binding_normalize_value($_GET['q'] ?? '');
+$rawLimit = (int) ($_GET['limit'] ?? 50);
+$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 ($q !== '') {
+ $terms = array_values(array_filter(preg_split('/\s+/u', $q, -1, PREG_SPLIT_NO_EMPTY)));
+ $values = array_values(array_filter($values, function ($v) use ($terms) {
+ $hay = binding_normalize_value($v['text'] ?? '');
+ foreach ($terms as $t) {
+ if (mb_strpos($hay, $t) === false) {
+ return false;
+ }
+ }
+ return true;
+ }));
+ }
+
+ if ($limit > 0) {
+ $values = array_slice($values, 0, $limit);
+ }
+
+ echo json_encode(['results' => $values]);
+} catch (Exception $e) {
+ http_response_code(500);
+ echo json_encode(['error' => $e->getMessage(), 'results' => []]);
+}
diff --git a/public/userarea/skip_binding.php b/public/userarea/skip_binding.php
new file mode 100644
index 00000000..509d5173
--- /dev/null
+++ b/public/userarea/skip_binding.php
@@ -0,0 +1,71 @@
+ 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';
+$mappingId = intval($_POST['mapping_id'] ?? 0);
+$sourceValue = (string) ($_POST['source_value'] ?? '');
+
+$datadbIds = [];
+if (isset($_POST['datadb_ids'])) {
+ $decoded = json_decode($_POST['datadb_ids'], true);
+ if (is_array($decoded)) {
+ $datadbIds = $decoded;
+ }
+}
+
+if ($mappingId <= 0 || trim($sourceValue) === '') {
+ http_response_code(422);
+ echo json_encode(['success' => false, 'error' => 'Missing required parameters']);
+ exit;
+}
+
+try {
+ $pdo = DBHandlerSelect::getInstance()->getConnection();
+
+ if ($kind === 'fixed') {
+ $stmt = $pdo->prepare("SELECT fixed_field_key FROM template_fixed_mapping WHERE id = ?");
+ $stmt->execute([$mappingId]);
+ $fixedKey = (string) ($stmt->fetchColumn() ?: '');
+
+ if ($fixedKey === '') {
+ http_response_code(422);
+ echo json_encode(['success' => false, 'error' => 'Invalid fixed field']);
+ exit;
+ }
+
+ $cleared = !empty($datadbIds)
+ ? binding_apply_to_datadb($pdo, $fixedKey, null, $datadbIds)
+ : 0;
+ } else {
+ $cleared = !empty($datadbIds)
+ ? binding_apply_to_details($pdo, $mappingId, '', $datadbIds)
+ : 0;
+ }
+
+ binding_delete($pdo, $mappingId, $sourceValue, $kind);
+
+ echo json_encode(['success' => true, 'cleared_rows' => $cleared]);
+} catch (Exception $e) {
+ http_response_code(500);
+ echo json_encode(['success' => false, 'error' => $e->getMessage()]);
+}
diff --git a/public/userarea/sql/fixed_field_binding.sql b/public/userarea/sql/fixed_field_binding.sql
new file mode 100644
index 00000000..a56e147c
--- /dev/null
+++ b/public/userarea/sql/fixed_field_binding.sql
@@ -0,0 +1,19 @@
+-- json_node e default_source possono gia' esistere: in quel caso saltare le ADD COLUMN.
+
+ALTER TABLE `template_fixed_mapping`
+ ADD COLUMN `json_node` varchar(500) DEFAULT NULL,
+ ADD COLUMN `default_source` enum('manual','xls','json') NOT NULL DEFAULT 'manual';
+
+ALTER TABLE `template_fixed_mapping`
+ ADD COLUMN `excel_column` varchar(255) DEFAULT NULL AFTER `json_node`;
+
+ALTER TABLE `template_fixed_mapping`
+ MODIFY COLUMN `default_source` enum('manual','xls','json') NOT NULL DEFAULT 'manual';
+
+ALTER TABLE `lims_value_binding`
+ ADD COLUMN `binding_kind` enum('custom','fixed') NOT NULL DEFAULT 'custom' AFTER `template_id`,
+ MODIFY COLUMN `field_id` int(11) NOT NULL DEFAULT 0;
+
+ALTER TABLE `lims_value_binding` DROP INDEX `uq_mapping_source`;
+ALTER TABLE `lims_value_binding`
+ ADD UNIQUE KEY `uq_kind_mapping_source` (`binding_kind`, `mapping_id`, `source_key`);
diff --git a/public/userarea/sql/lims_value_binding.sql b/public/userarea/sql/lims_value_binding.sql
new file mode 100644
index 00000000..06cc8a4e
--- /dev/null
+++ b/public/userarea/sql/lims_value_binding.sql
@@ -0,0 +1,17 @@
+CREATE TABLE IF NOT EXISTS `lims_value_binding` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `template_id` int(11) NOT NULL,
+ `mapping_id` int(11) NOT NULL,
+ `field_id` int(11) NOT NULL,
+ `source_value` varchar(500) NOT NULL,
+ `source_key` varchar(500) NOT NULL,
+ `lims_value_id` int(11) NOT NULL,
+ `lims_value` varchar(500) DEFAULT NULL,
+ `created_by` int(11) DEFAULT NULL,
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uq_mapping_source` (`mapping_id`, `source_key`),
+ KEY `idx_template` (`template_id`),
+ KEY `idx_field_source` (`field_id`, `source_key`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
diff --git a/public/userarea/update_fixed_field.php b/public/userarea/update_fixed_field.php
index 71b6c97e..3b0a7132 100644
--- a/public/userarea/update_fixed_field.php
+++ b/public/userarea/update_fixed_field.php
@@ -17,7 +17,7 @@ if ($id <= 0) {
exit;
}
-$allowed = ['default_value', 'is_visible_import', 'is_required'];
+$allowed = ['default_value', 'is_visible_import', 'is_required', 'default_source', 'excel_column'];
if (!in_array($field, $allowed, true)) {
echo json_encode(['success' => false, 'message' => 'Invalid field']);
exit;
@@ -27,6 +27,21 @@ if ($field === 'is_visible_import' || $field === 'is_required') {
$value = ((int)$value === 1) ? 1 : 0;
}
+if ($field === 'default_source') {
+ $value = in_array($value, ['manual', 'xls', 'json'], true) ? $value : 'manual';
+
+ if ($value !== 'xls') {
+ $pdo->prepare("UPDATE template_fixed_mapping SET excel_column = NULL WHERE id = :id")
+ ->execute([':id' => $id]);
+ }
+}
+
+if ($field === 'excel_column') {
+ $value = trim((string)$value);
+ if ($value === '') {
+ $value = null;
+ }
+}
$sql = "UPDATE template_fixed_mapping SET {$field} = :val WHERE id = :id";
$stmt = $pdo->prepare($sql);
diff --git a/public/userarea/validate_export.php b/public/userarea/validate_export.php
index 76b48f92..e039dcac 100644
--- a/public/userarea/validate_export.php
+++ b/public/userarea/validate_export.php
@@ -22,6 +22,7 @@
* To add a new rule — just append another closure to $validators.
*/
include('include/headscript.php');
+require_once __DIR__ . '/class/binding-functions.php';
$dbHandler = DBHandlerSelect::getInstance();
$pdo = $dbHandler->getConnection();
@@ -124,6 +125,29 @@ $validators[] = function (int $iddatadb, array $ctx): array {
return $errors;
};
+$validators[] = function (int $iddatadb, array $ctx): array {
+ $errors = [];
+
+ foreach (($ctx['listCustom'] ?? []) as $cf) {
+ $val = trim((string) ($ctx['customValues'][(int) $cf['mapping_id']] ?? ''));
+ if ($val === '' || ctype_digit($val)) {
+ continue;
+ }
+ if (!binding_has_lims_values((int) $cf['field_id'])) {
+ continue;
+ }
+
+ $label = rtrim($cf['field_label'], ': ');
+ $errors[] = [
+ 'field' => 'field_label:' . $cf['field_label'],
+ 'message' => $label . ': il valore importato "' . $val . '" non e\' collegato a nessun valore '
+ . 'della lista LIMS. Aprire Gestione binding o ri-importare per collegarlo.',
+ ];
+ }
+
+ return $errors;
+};
+
// Logical fixed_field_key - real datadb column (mirrors imported.php $fixedAliasMap)
$fixedAliasMap = [
'ClienteResponsabile' => 'cliente_responsabile_id',
@@ -197,6 +221,7 @@ try {
$requiredFixedByTemplate = []; // template_id => [ fixed_field_key => label ]
$requiredCustomByTemplate = []; // template_id => [ { mapping_id, field_label }, ... ]
+ $listCustomByTemplate = []; // template_id => [ { mapping_id, field_label }, ... ]
if (!empty($templateIds)) {
$tplPlaceholders = implode(',', array_fill(0, count($templateIds), '?'));
@@ -237,6 +262,21 @@ try {
'field_label' => $r['field_label'],
];
}
+
+ $stmt = $pdo->prepare("
+ SELECT id AS mapping_id, template_id, field_id, field_label
+ FROM template_mapping
+ WHERE template_id IN ($tplPlaceholders)
+ AND data_type = 'SceltaMultipla'
+ ");
+ $stmt->execute($templateIds);
+ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
+ $listCustomByTemplate[(int)$r['template_id']][] = [
+ 'mapping_id' => (int)$r['mapping_id'],
+ 'field_id' => (int)$r['field_id'],
+ 'field_label' => $r['field_label'],
+ ];
+ }
}
// Custom field values per record (import_data_details.id is the FK to datadb)
@@ -268,6 +308,7 @@ try {
'fixedAliasMap' => $fixedAliasMap,
'requiredFixed' => $requiredFixedByTemplate[$templateId] ?? [],
'requiredCustom' => $requiredCustomByTemplate[$templateId] ?? [],
+ 'listCustom' => $listCustomByTemplate[$templateId] ?? [],
'customValues' => $customValuesByRecord[$iddatadb] ?? [],
];