Compare commits

..

7 Commits

Author SHA1 Message Date
solocla 83c6157b10 validation excldugin id 189 tested component 2026-06-16 08:33:32 +02:00
RMubarakzyanov 4dd7b89c22 added mandatory field checking 2026-06-11 13:41:31 +03:00
RMubarakzyanov dec42b4442 Merge branch 'main' into feature/analysis 2026-06-11 09:37:01 +03:00
solocla dab8d9aebf fixed mapping for json 2026-06-08 08:47:17 +02:00
solocla 375a10a678 filter analysis web 2026-06-08 07:40:05 +02:00
solocla 15990be884 fxied column order LIMS 2026-06-04 16:48:22 +02:00
RMubarakzyanov 67bbd9bbbb export analyses 2026-04-21 00:09:59 +03:00
10 changed files with 683 additions and 350 deletions
+1
View File
@@ -47,6 +47,7 @@ yarn-error.log
/public/userarea/class/curl_auth_debug.log /public/userarea/class/curl_auth_debug.log
/public/userarea/class/curl_request_debug.log /public/userarea/class/curl_request_debug.log
/public/userarea/schema_dettagli_response.json /public/userarea/schema_dettagli_response.json
public/userarea/schemi_base_response.json
# File XLSX temporanei importati # File XLSX temporanei importati
/public/userarea/imported_trf/*.xlsx /public/userarea/imported_trf/*.xlsx
+16 -11
View File
@@ -431,7 +431,7 @@
const emptyEl = modal.querySelector("#analysisEmptyBox"); const emptyEl = modal.querySelector("#analysisEmptyBox");
const errorEl = modal.querySelector("#analysisErrorBox"); const errorEl = modal.querySelector("#analysisErrorBox");
const webOnly = webOnlyEl ? webOnlyEl.checked : false; const webOnly = true;
const searchValue = searchEl ? searchEl.value.trim().toLowerCase() : ""; const searchValue = searchEl ? searchEl.value.trim().toLowerCase() : "";
let visibleCount = 0; let visibleCount = 0;
@@ -496,8 +496,10 @@
emptyEl.classList.add("d-none"); emptyEl.classList.add("d-none");
} }
if (analysisLoadedCache[String(matrixId)]) { const cacheKey = String(matrixId) + "_WEB_ONLY";
renderAnalysesList(analysisLoadedCache[String(matrixId)]);
if (analysisLoadedCache[cacheKey]) {
renderAnalysesList(analysisLoadedCache[cacheKey]);
return; return;
} }
@@ -509,13 +511,21 @@
dataType: "json", dataType: "json",
data: { data: {
id_matrice: matrixId, id_matrice: matrixId,
web_only: 1,
}, },
}) })
.done(function (response) { .done(function (response) {
const analyses = Array.isArray(response.value) const analyses = Array.isArray(response.value)
? response.value ? response.value.filter(function (item) {
return (
item.SelezionabileSuWeb === true ||
item.SelezionabileSuWeb === 1 ||
item.SelezionabileSuWeb === "1"
);
})
: []; : [];
analysisLoadedCache[String(matrixId)] = analyses;
analysisLoadedCache[cacheKey] = analyses;
renderAnalysesList(analyses); renderAnalysesList(analyses);
}) })
.fail(function (xhr) { .fail(function (xhr) {
@@ -674,12 +684,7 @@
}); });
} }
const webOnlyEl = modal.querySelector("#analysisWebOnly"); // WEB only is now fixed by default
if (webOnlyEl) {
webOnlyEl.addEventListener("change", function () {
filterAnalysisList();
});
}
const searchEl = modal.querySelector("#analysisSearchInput"); const searchEl = modal.querySelector("#analysisSearchInput");
if (searchEl) { if (searchEl) {
+70
View File
@@ -477,6 +477,71 @@ try {
$logFilePhotos = $logDir . "commessa_{$commessaId}_photos_step5_2_" . time() . ".txt"; $logFilePhotos = $logDir . "commessa_{$commessaId}_photos_step5_2_" . time() . ".txt";
$writeLog($logFilePhotos, $logContentPhotos, "STEP 6.2 - Photos (commessa={$commessaId})"); $writeLog($logFilePhotos, $logContentPhotos, "STEP 6.2 - Photos (commessa={$commessaId})");
// 🔹 STEP 6.3: Add Analyses (AnalisiCampione) via Campione({id})/AddAnalisi bound action
$stmt = $pdo->prepare("
SELECT part_id, analysis_recordkey, analysis_name, analysis_method
FROM identification_parts_analyses
WHERE iddatadb = :iddatadb
ORDER BY part_id, id
");
$stmt->execute(['iddatadb' => $iddatadb]);
$analysesRows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$partIdToIndex = [];
foreach ($parts as $idx => $part) {
$partIdToIndex[(int)$part['part_id']] = $idx;
}
$totalAnalyses = count($analysesRows);
$addedAnalyses = 0;
$failedAnalyses = [];
$logContentStep63Analisi = "Analyses for iddatadb={$iddatadb}: total={$totalAnalyses}\n\n";
foreach ($analysesRows as $a) {
$partId = (int)$a['part_id'];
$recordKey = trim((string)($a['analysis_recordkey'] ?? ''));
$idx = $partIdToIndex[$partId] ?? null;
if ($idx === null || !isset($campioni[$idx]) || $recordKey === '') {
$logContentStep63Analisi .= "SKIP (no campione for part_id={$partId} / empty recordkey): '{$recordKey}'\n";
continue;
}
$campioneId = (int)($campioni[$idx]['IdCampione'] ?? 0);
if ($campioneId <= 0) {
$logContentStep63Analisi .= "SKIP (invalid IdCampione for part_id={$partId}): '{$recordKey}'\n";
continue;
}
$payload = ['RecordKey' => $recordKey];
$jsonPayload = json_encode($payload, JSON_UNESCAPED_SLASHES);
$logContentStep63Analisi .= "curl --location --request POST '{$apiBaseUrl}Campione({$campioneId})/AddAnalisi' \\\n" .
"--header 'Content-Type: application/json' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" .
"--data '{$jsonPayload}'\n";
try {
$result = $api->post("Campione({$campioneId})/AddAnalisi", $payload);
$logContentStep63Analisi .= "OK (part_id={$partId}, campione={$campioneId}): " .
($a['analysis_name'] ?? '') . "\n---\n";
$addedAnalyses++;
} catch (Exception $e) {
$errMsg = $e->getMessage();
$logContentStep63Analisi .= "FAIL: {$errMsg}\n---\n";
$failedAnalyses[] = [
'part_id' => $partId,
'campione_id' => $campioneId,
'analysis_recordkey' => $recordKey,
'analysis_name' => $a['analysis_name'] ?? '',
'error' => $errMsg,
];
}
}
$logFileStep63Analisi = $logDir . "commessa_{$commessaId}_analyses_step63_" . time() . ".txt";
$writeLog($logFileStep63Analisi, $logContentStep63Analisi, "STEP 6.3 - AddAnalisi (commessa={$commessaId})");
// 🔹 STEP 7: Update Custom Fields for CommessaWeb // 🔹 STEP 7: Update Custom Fields for CommessaWeb
if (!empty($fieldValues)) { if (!empty($fieldValues)) {
// GET con espansione per CustomField // GET con espansione per CustomField
@@ -629,11 +694,15 @@ try {
"totalCampioni" => count($campioni), "totalCampioni" => count($campioni),
"totalCustomFields" => count($commessaAfterPatch["CommesseCustomFields"] ?? []), "totalCustomFields" => count($commessaAfterPatch["CommesseCustomFields"] ?? []),
"totalPhotos" => count($photos), "totalPhotos" => count($photos),
"totalAnalyses" => $totalAnalyses,
"addedAnalyses" => $addedAnalyses,
"failedAnalyses" => $failedAnalyses,
"message" => "Export successful", "message" => "Export successful",
"logFiles" => [ "logFiles" => [
"step5_create" => $logFileStep5, "step5_create" => $logFileStep5,
"step5_2_photos" => $logFilePhotos, "step5_2_photos" => $logFilePhotos,
"step6_campioni" => $logFileStep6, "step6_campioni" => $logFileStep6,
"step63_analyses" => $logFileStep63Analisi,
"step7_patch" => $logFileStep7 ?? null, "step7_patch" => $logFileStep7 ?? null,
"step9_1_importa" => $logFileStep91, "step9_1_importa" => $logFileStep91,
"step10_get" => $logFileStep10 "step10_get" => $logFileStep10
@@ -649,6 +718,7 @@ try {
"step5_create" => $logFileStep5 ?? null, "step5_create" => $logFileStep5 ?? null,
"step5_2_photos" => $logFilePhotos ?? null, "step5_2_photos" => $logFilePhotos ?? null,
"step6_campioni" => $logFileStep6 ?? null, "step6_campioni" => $logFileStep6 ?? null,
"step63_analyses" => $logFileStep63Analisi ?? null,
"step7_patch" => $logFileStep7 ?? null, "step7_patch" => $logFileStep7 ?? null,
"step9_1_importa" => $logFileStep91 ?? null, "step9_1_importa" => $logFileStep91 ?? null,
"step10_get" => $logFileStep10 ?? null "step10_get" => $logFileStep10 ?? null
@@ -18,7 +18,15 @@ try {
$api = VisualLimsApiClient::getInstance(); $api = VisualLimsApiClient::getInstance();
$filter = rawurlencode("Matrice/IdMatrice eq $idMatrice"); $webOnly = isset($_GET['web_only']) ? (int)$_GET['web_only'] : 1;
$filterString = "Matrice/IdMatrice eq $idMatrice";
if ($webOnly === 1) {
$filterString .= " and SelezionabileSuWeb eq true";
}
$filter = rawurlencode($filterString);
$endpoint = "Analisi?\$filter={$filter}"; $endpoint = "Analisi?\$filter={$filter}";
$base_url = 'https://93.43.5.102/limsapi/api/odata/'; $base_url = 'https://93.43.5.102/limsapi/api/odata/';
+29 -39
View File
@@ -20,9 +20,10 @@ $db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection(); $pdo = $db->getConnection();
// Recupera tutti i mapping dal template, includendo is_visible_import // Recupera tutti i mapping dal template, includendo is_visible_import
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field, is_visible_import, auto_value $stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, field_order, main_field, is_visible_import, auto_value
FROM template_mapping FROM template_mapping
WHERE template_id = ?"); WHERE template_id = ?
ORDER BY field_order ASC, id ASC");
$stmt->execute([$template_id]); $stmt->execute([$template_id]);
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC); $allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -252,18 +253,27 @@ foreach ($importedData as $index => $row) {
// Build columns in display order // Build columns in display order
$gridColumns = []; $gridColumns = [];
// 1. Main fields, maximum 2 // 1. Main fields first, immediately after buttons
foreach ($mainFieldMappings as $mainMapping) { foreach ($allMappings as $mapping) {
if (
(int)$mapping['is_visible_import'] === 1
&& (string)$mapping['main_field'] === '1'
&& trim((string)$mapping['field_label']) !== 'Tested Component:'
) {
$gridColumns[] = [ $gridColumns[] = [
'type' => 'main_field', 'type' => 'main_field',
'key' => (string)$mainMapping['id'], 'key' => (string)$mapping['id'],
'label' => $mainMapping['field_label'], 'label' => $mapping['field_label'],
'dataType' => $mainMapping['data_type'], 'dataType' => $mapping['data_type'],
'isManual' => (bool)$mainMapping['is_manual'], 'isManual' => (bool)$mapping['is_manual'],
'isRequired' => (bool)$mainMapping['is_required'], 'isRequired' => (bool)$mapping['is_required'],
'fieldId' => $mainMapping['field_id'] ?? null, 'fieldId' => $mapping['field_id'] ?? null,
'fieldOrder' => (int)($mapping['field_order'] ?? 9999),
'manualDefault' => $mapping['manual_default'] ?? '',
'autoValue' => $mapping['auto_value'] ?? 'none',
'width' => 150, 'width' => 150,
]; ];
}
} }
// 2. Status // 2. Status
@@ -275,50 +285,30 @@ $gridColumns[] = ['type' => 'idclient', 'key' => 'idclient', 'label' => 'Client'
// 4. Cliente Fornitore // 4. Cliente Fornitore
$gridColumns[] = ['type' => 'cliente_fornitore_id', 'key' => 'cliente_fornitore_id', 'label' => $slugMapping['ClienteFornitore'] ?? 'ClienteFornitore', 'width' => 300]; $gridColumns[] = ['type' => 'cliente_fornitore_id', 'key' => 'cliente_fornitore_id', 'label' => $slugMapping['ClienteFornitore'] ?? 'ClienteFornitore', 'width' => 300];
// 5. Auto fields // 5. Other custom fields in schema order
foreach ($allMappings as $mapping) { foreach ($allMappings as $mapping) {
if ( if (
!$mapping['is_manual'] (int)$mapping['is_visible_import'] === 1
&& $mapping['main_field'] != 1 && (string)$mapping['main_field'] !== '1'
&& $mapping['is_visible_import'] == 1
&& trim((string)$mapping['field_label']) !== 'Tested Component:' && trim((string)$mapping['field_label']) !== 'Tested Component:'
) { ) {
$isMainField = ((string)$mapping['main_field'] === '1');
$gridColumns[] = [ $gridColumns[] = [
'type' => 'detail', 'type' => $isMainField ? 'main_field' : 'detail',
'key' => (string)$mapping['id'], 'key' => (string)$mapping['id'],
'label' => $mapping['field_label'], 'label' => $mapping['field_label'],
'dataType' => $mapping['data_type'], 'dataType' => $mapping['data_type'],
'isManual' => false, 'isManual' => (bool)$mapping['is_manual'],
'isRequired' => (bool)$mapping['is_required'], 'isRequired' => (bool)$mapping['is_required'],
'fieldId' => $mapping['field_id'] ?? null, 'fieldId' => $mapping['field_id'] ?? null,
'fieldOrder' => (int)($mapping['field_order'] ?? 9999),
'manualDefault' => $mapping['manual_default'] ?? '',
'autoValue' => $mapping['auto_value'] ?? 'none', 'autoValue' => $mapping['auto_value'] ?? 'none',
'width' => 150, 'width' => 150,
]; ];
} }
} }
// 6. Manual fields
foreach ($allMappings as $mapping) {
if (
$mapping['is_manual']
&& $mapping['main_field'] != 1
&& $mapping['is_visible_import'] == 1
&& trim((string)$mapping['field_label']) !== 'Tested Component:'
) {
$gridColumns[] = [
'type' => 'detail',
'key' => (string)$mapping['id'],
'label' => $mapping['field_label'],
'dataType' => $mapping['data_type'],
'isManual' => true,
'isRequired' => (bool)$mapping['is_required'],
'fieldId' => $mapping['field_id'] ?? null,
'manualDefault' => $mapping['manual_default'] ?? '',
'width' => 150,
];
}
}
// 7. Tested Component // 7. Tested Component
$gridColumns[] = ['type' => 'tested_component', 'key' => 'tested_component', 'label' => 'Tested Component', 'width' => 150]; $gridColumns[] = ['type' => 'tested_component', 'key' => 'tested_component', 'label' => 'Tested Component', 'width' => 150];
+314 -192
View File
@@ -265,6 +265,47 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
font-family: Consolas, Monaco, monospace; font-family: Consolas, Monaco, monospace;
font-size: 13px; font-size: 13px;
} }
.select2-container--default .select2-results__option {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 520px;
}
.select2-json-node {
font-weight: 600;
}
.select2-json-value {
color: #6c757d;
font-size: 12px;
margin-left: 8px;
}
.select2-json-row {
display: block;
margin: -6px;
padding: 6px;
}
.select2-results__option:has(.select2-json-row.used-option) {
background-color: #fff3cd !important;
color: #856404 !important;
font-weight: 600;
}
.select2-results__option:has(.select2-json-row.used-option).select2-results__option--highlighted {
background-color: #ffe69c !important;
color: #856404 !important;
}
.select2-json-row.used-option .select2-json-node::after {
content: " (already used)";
color: #856404;
font-size: 12px;
font-weight: 600;
}
</style> </style>
</head> </head>
@@ -1018,6 +1059,42 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
return escapeHtmlText(str); return escapeHtmlText(str);
} }
let jsonNodeLabels = {};
function getLastJsonNodeName(path) {
return String(path || '')
.replace(/\[\]/g, '')
.split('.')
.pop();
}
function formatJsonSampleValue(value) {
if (value === null || value === undefined) return '';
let text = '';
if (Array.isArray(value)) {
text = '[Array]';
} else if (typeof value === 'object') {
text = '{Object}';
} else {
text = String(value);
}
text = text
.replace(/[\r\n\t]+/g, ' ')
.replace(/\s+/g, ' ')
.trim();
if (text === '/' || text === '\\/') return '';
if (text.length > 38) {
text = text.substring(0, 38) + '...';
}
return text;
}
function flattenJsonNodes(obj, prefix = '') { function flattenJsonNodes(obj, prefix = '') {
let nodes = []; let nodes = [];
@@ -1036,6 +1113,14 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
if (value !== null && typeof value === 'object') { if (value !== null && typeof value === 'object') {
nodes = nodes.concat(flattenJsonNodes(value, path)); nodes = nodes.concat(flattenJsonNodes(value, path));
} else { } else {
const sample = formatJsonSampleValue(value);
const shortName = getLastJsonNodeName(path);
jsonNodeLabels[path] = {
shortName: shortName,
sample: sample
};
nodes.push(path); nodes.push(path);
} }
}); });
@@ -1061,18 +1146,128 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
if (!clean) return ''; if (!clean) return '';
const isUsed = uniqueUsedNodes.includes(clean) && clean !== currentValue; const isUsed = uniqueUsedNodes.includes(clean) && clean !== currentValue;
const label = isUsed ? `⚠ ${clean} (already used)` : clean; const info = jsonNodeLabels[clean] || {};
const shortName = info.shortName || getLastJsonNodeName(clean);
const sample = info.sample || '';
let label = shortName;
if (sample) {
label += ` — ${sample}`;
}
if (isUsed) {
label = `⚠ ${label} (already used)`;
}
const isSelected = clean === currentValue ? 'selected' : ''; const isSelected = clean === currentValue ? 'selected' : '';
return `<option value="${escapeHtmlAttr(clean)}" class="${isUsed ? 'used-option' : ''}" ${isSelected}>${escapeHtmlText(label)}</option>`; return `
<option
value="${escapeHtmlAttr(clean)}"
data-short-name="${escapeHtmlAttr(shortName)}"
data-sample="${escapeHtmlAttr(sample)}"
data-full-path="${escapeHtmlAttr(clean)}"
class="${isUsed ? 'used-option' : ''}"
data-used="${isUsed ? '1' : '0'}"
${isSelected}>
${escapeHtmlText(label)}
</option>
`;
}) })
.join(''); .join('');
select.innerHTML = '<option value="">Select JSON Node</option>' + options; select.innerHTML = '<option value="">Select JSON Node</option>' + options;
select.dataset.currentJson = currentValue; select.dataset.currentJson = currentValue;
}); });
initSelect2ForJsonDropdowns();
} }
function initSelect2ForJsonDropdowns() {
if (!(window.jQuery && $.fn.select2)) return;
$('.json-nodes').filter(function() {
const tr = this.closest('tr');
const mappingSelect = tr ? tr.querySelector('.mapping-select') : null;
return mappingSelect && mappingSelect.value === 'json';
}).each(function() {
const $el = $(this);
if (this.style.display === 'none') {
return;
}
if ($el.hasClass('select2-hidden-accessible')) {
$el.select2('destroy');
}
$el.select2({
width: '100%',
placeholder: 'Select JSON Node',
allowClear: true,
dropdownAutoWidth: false,
templateResult: function(data) {
if (!data.id) return data.text;
const option = data.element;
const shortName = option.getAttribute('data-short-name') || data.text;
const sample = option.getAttribute('data-sample') || '';
const isUsed = option.getAttribute('data-used') === '1';
const $row = $('<span class="select2-json-row"></span>');
if (isUsed) {
$row.addClass('used-option');
}
$row.append(`<span class="select2-json-node">${escapeHtmlText(shortName)}</span>`);
if (sample) {
$row.append(`<span class="select2-json-value">${escapeHtmlText(sample)}</span>`);
}
return $row;
},
templateSelection: function(data) {
if (!data.id) return data.text;
const option = data.element;
const shortName = option.getAttribute('data-short-name') || data.text;
const sample = option.getAttribute('data-sample') || '';
if (sample) {
return `${shortName} — ${sample}`;
}
return shortName;
},
matcher: function(params, data) {
if ($.trim(params.term) === '') {
return data;
}
const term = params.term.toLowerCase();
const option = data.element;
const fullPath = option?.getAttribute('data-full-path')?.toLowerCase() || '';
const shortName = option?.getAttribute('data-short-name')?.toLowerCase() || '';
const sample = option?.getAttribute('data-sample')?.toLowerCase() || '';
if (
fullPath.includes(term) ||
shortName.includes(term) ||
sample.includes(term)
) {
return data;
}
return null;
}
});
});
}
function saveJsonNodes(sampleJson, nodes) { function saveJsonNodes(sampleJson, nodes) {
return fetch('update_api_json_nodes.php', { return fetch('update_api_json_nodes.php', {
method: 'POST', method: 'POST',
@@ -1463,25 +1658,38 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
document.getElementById('updateSchemaButton').addEventListener('click', updateSchemaDetails); document.getElementById('updateSchemaButton').addEventListener('click', updateSchemaDetails);
document.getElementById('schemaFieldsBody').addEventListener('change', function(event) { document.getElementById('schemaFieldsBody').addEventListener('change', function(event) {
if (!event.target.classList.contains('mapping-select')) return;
if (event.target.classList.contains('mapping-select')) { const mappingSelect = event.target;
let tr = event.target.closest('tr'); const tr = mappingSelect.closest('tr');
let mappingId = event.target.getAttribute('data-id'); const mappingId = mappingSelect.getAttribute('data-id');
let xlsSelect = tr.querySelector('.xls-columns'); const xlsSelect = tr.querySelector('.xls-columns');
let jsonSelect = tr.querySelector('.json-nodes'); const jsonSelect = tr.querySelector('.json-nodes');
let manualInput = tr.querySelector('.manual-default'); const manualInput = tr.querySelector('.manual-default');
let autoSelect = tr.querySelector('.auto-value-select'); const autoSelect = tr.querySelector('.auto-value-select');
let mappedColumn = tr.querySelector('.mapped-column'); const mappedColumn = tr.querySelector('.mapped-column');
let mappedJsonNode = tr.querySelector('.mapped-json-node'); const mappedJsonNode = tr.querySelector('.mapped-json-node');
let removeBtn = tr.querySelector('.remove-xls'); const removeBtn = tr.querySelector('.remove-xls');
let removeJsonBtn = tr.querySelector('.remove-json'); const removeJsonBtn = tr.querySelector('.remove-json');
if (event.target.value === 'xls') { function destroyJsonSelect2() {
if (jsonSelect && window.jQuery && $(jsonSelect).hasClass('select2-hidden-accessible')) {
$(jsonSelect).select2('destroy');
}
}
if (mappingSelect.value === 'xls') {
if (xlsSelect) xlsSelect.style.display = 'block'; if (xlsSelect) xlsSelect.style.display = 'block';
if (jsonSelect) jsonSelect.style.display = 'none';
destroyJsonSelect2();
if (jsonSelect) {
jsonSelect.style.display = 'none';
jsonSelect.value = '';
}
if (autoSelect) autoSelect.style.display = 'none'; if (autoSelect) autoSelect.style.display = 'none';
if (manualInput) { if (manualInput) {
@@ -1495,9 +1703,16 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
if (removeBtn) removeBtn.style.display = xlsSelect && xlsSelect.value ? 'inline-block' : 'none'; if (removeBtn) removeBtn.style.display = xlsSelect && xlsSelect.value ? 'inline-block' : 'none';
if (removeJsonBtn) removeJsonBtn.style.display = 'none'; if (removeJsonBtn) removeJsonBtn.style.display = 'none';
} else if (event.target.value === 'json') { } else if (mappingSelect.value === 'json') {
if (xlsSelect) xlsSelect.style.display = 'none'; if (xlsSelect) {
if (jsonSelect) jsonSelect.style.display = 'block'; xlsSelect.style.display = 'none';
xlsSelect.value = '';
}
if (jsonSelect) {
jsonSelect.style.display = 'block';
}
if (autoSelect) autoSelect.style.display = 'none'; if (autoSelect) autoSelect.style.display = 'none';
if (manualInput) { if (manualInput) {
@@ -1511,9 +1726,17 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
if (removeBtn) removeBtn.style.display = 'none'; if (removeBtn) removeBtn.style.display = 'none';
if (removeJsonBtn) removeJsonBtn.style.display = jsonSelect && jsonSelect.value ? 'inline-block' : 'none'; if (removeJsonBtn) removeJsonBtn.style.display = jsonSelect && jsonSelect.value ? 'inline-block' : 'none';
} else if (event.target.value === 'manual') { updateJsonDropdowns();
} else if (mappingSelect.value === 'manual') {
if (xlsSelect) xlsSelect.style.display = 'none'; if (xlsSelect) xlsSelect.style.display = 'none';
if (jsonSelect) jsonSelect.style.display = 'none';
destroyJsonSelect2();
if (jsonSelect) {
jsonSelect.style.display = 'none';
jsonSelect.value = '';
}
if (autoSelect) autoSelect.style.display = 'none'; if (autoSelect) autoSelect.style.display = 'none';
if (manualInput) { if (manualInput) {
@@ -1526,12 +1749,13 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
if (removeBtn) removeBtn.style.display = 'none'; if (removeBtn) removeBtn.style.display = 'none';
if (removeJsonBtn) removeJsonBtn.style.display = 'none'; if (removeJsonBtn) removeJsonBtn.style.display = 'none';
} else if (event.target.value === 'auto') { } else if (mappingSelect.value === 'auto') {
if (xlsSelect) { if (xlsSelect) {
xlsSelect.style.display = 'none'; xlsSelect.style.display = 'none';
xlsSelect.value = ''; xlsSelect.value = '';
} }
destroyJsonSelect2();
if (jsonSelect) { if (jsonSelect) {
jsonSelect.style.display = 'none'; jsonSelect.style.display = 'none';
jsonSelect.value = ''; jsonSelect.value = '';
@@ -1552,7 +1776,13 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
} else { } else {
if (xlsSelect) xlsSelect.style.display = 'none'; if (xlsSelect) xlsSelect.style.display = 'none';
if (jsonSelect) jsonSelect.style.display = 'none';
destroyJsonSelect2();
if (jsonSelect) {
jsonSelect.style.display = 'none';
jsonSelect.value = '';
}
if (autoSelect) autoSelect.style.display = 'none'; if (autoSelect) autoSelect.style.display = 'none';
if (manualInput) { if (manualInput) {
@@ -1569,7 +1799,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
saveMapping( saveMapping(
mappingId, mappingId,
event.target.value, mappingSelect.value,
manualInput ? manualInput.value : '', manualInput ? manualInput.value : '',
xlsSelect ? xlsSelect.value : null, xlsSelect ? xlsSelect.value : null,
autoSelect ? autoSelect.value : null, autoSelect ? autoSelect.value : null,
@@ -1577,143 +1807,67 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
); );
if (sourceType === 'XLS') updateXlsDropdowns(); if (sourceType === 'XLS') updateXlsDropdowns();
if (sourceType === 'API') updateJsonDropdowns(); });
function saveJsonNodeSelection(jsonSelect) {
if (!jsonSelect) return;
let tr = jsonSelect.closest('tr');
let mappingId = jsonSelect.getAttribute('data-id');
let manualInput = tr.querySelector('.manual-default');
let mappedJsonNode = tr.querySelector('.mapped-json-node');
let removeJsonBtn = tr.querySelector('.remove-json');
let mappingSelect = tr.querySelector('.mapping-select');
if (mappingSelect && mappingSelect.value !== 'json') {
return; return;
} }
if (event.target.classList.contains('main-field-checkbox')) { if (!mappedJsonNode) {
const checkbox = event.target; mappedJsonNode = document.createElement('span');
const mappingId = checkbox.dataset.mappingId; mappedJsonNode.className = 'mapped-json-node';
const value = checkbox.checked ? 1 : 0; mappedJsonNode.style.marginLeft = '5px';
tr.querySelector('td:nth-child(7)').appendChild(mappedJsonNode);
// Count only the other Main fields already checked in this table
const otherCheckedMainFields = Array.from(
document.querySelectorAll('#schemaFieldsBody .main-field-checkbox')
).filter(cb => cb !== checkbox && cb.checked);
// If I am checking this one, I can have max 2 total:
// this checkbox + max 1 other already checked
if (checkbox.checked && otherCheckedMainFields.length >= 2) {
checkbox.checked = false;
alert('Puoi selezionare al massimo 2 campi Main.');
return;
} }
fetch('update_main_field.php', { if (!removeJsonBtn) {
method: 'POST', removeJsonBtn = document.createElement('button');
headers: { removeJsonBtn.className = 'btn btn-danger btn-sm remove-json';
'Content-Type': 'application/json' removeJsonBtn.textContent = 'X';
}, removeJsonBtn.style.marginLeft = '5px';
body: JSON.stringify({ removeJsonBtn.setAttribute('data-id', mappingId);
template_id: <?php echo $id; ?>, tr.querySelector('td:nth-child(7)').appendChild(removeJsonBtn);
mapping_id: mappingId,
value: value
})
})
.then(response => response.json())
.then(data => {
if (!data.success) {
console.error("❌ Error updating main_field:", data.message);
document.querySelectorAll('#schemaFieldsBody .main-field-checkbox').forEach(cb => {
cb.checked = cb.dataset.originalChecked === 'true';
});
alert(data.message || 'Errore durante il salvataggio del campo Main.');
return;
} }
document.querySelectorAll('#schemaFieldsBody .main-field-checkbox').forEach(cb => { mappedJsonNode.textContent = jsonSelect.value ? `(${jsonSelect.value})` : '';
cb.dataset.originalChecked = cb.checked ? 'true' : 'false'; mappedJsonNode.style.display = jsonSelect.value ? 'inline' : 'none';
}); removeJsonBtn.style.display = jsonSelect.value ? 'inline-block' : 'none';
})
.catch(error => {
console.error("❌ Fetch error:", error);
document.querySelectorAll('#schemaFieldsBody .main-field-checkbox').forEach(cb => { console.log('[JSON NODE SAVE]', {
cb.checked = cb.dataset.originalChecked === 'true'; mappingId: mappingId,
value: jsonSelect.value
}); });
alert('Errore di comunicazione durante il salvataggio del campo Main.'); saveMapping(
}); mappingId,
'json',
return; manualInput ? manualInput.value : '',
} null,
if (event.target.classList.contains('visible-import-checkbox')) { null,
const checkbox = event.target; jsonSelect.value
const mappingId = checkbox.dataset.mappingId; );
const value = checkbox.checked ? 1 : 0;
const prevChecked = checkbox.checked;
fetch('update_visible_import.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
template_id: <?php echo $id; ?>,
mapping_id: mappingId,
value: value
})
})
.then(r => r.json())
.then(data => {
if (!data.success) {
console.error("❌ Error updating is_visible_import:", data.message);
checkbox.checked = !prevChecked;
alert(data.message || 'Errore durante il salvataggio del campo Import.');
}
})
.catch(error => {
console.error("❌ Fetch error:", error);
checkbox.checked = !prevChecked;
alert('Errore di comunicazione durante il salvataggio del campo Import.');
});
return;
}
if (event.target.classList.contains('visible-parts-checkbox')) {
const checkbox = event.target;
const mappingId = checkbox.dataset.mappingId;
const value = checkbox.checked ? 1 : 0;
const prevChecked = checkbox.checked;
if (value === 1) {
document.querySelectorAll('.visible-parts-checkbox').forEach(cb => {
if (cb !== checkbox) cb.checked = false;
});
} }
fetch('update_visible_parts.php', { document.getElementById('schemaFieldsBody').addEventListener('change', function(event) {
method: 'POST', if (!event.target.classList.contains('json-nodes')) return;
headers: { saveJsonNodeSelection(event.target);
'Content-Type': 'application/json'
},
body: JSON.stringify({
template_id: <?php echo $id; ?>,
mapping_id: mappingId,
value: value
})
})
.then(r => r.json())
.then(data => {
if (!data.success) {
console.error("❌ Error updating is_visible_parts:", data.message);
checkbox.checked = !prevChecked;
location.reload();
}
})
.catch(error => {
console.error("❌ Fetch error:", error);
checkbox.checked = !prevChecked;
location.reload();
}); });
return; if (window.jQuery) {
} $(document).on('select2:select select2:clear', 'select.json-nodes', function() {
saveJsonNodeSelection(this);
}); });
}
// Save original Main checkbox state // Save original Main checkbox state
@@ -1797,51 +1951,6 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
} }
}); });
document.getElementById('schemaFieldsBody').addEventListener('change', function(event) {
if (event.target.classList.contains('json-nodes')) {
let tr = event.target.closest('tr');
let mappingId = event.target.getAttribute('data-id');
let manualInput = tr.querySelector('.manual-default');
let mappedJsonNode = tr.querySelector('.mapped-json-node');
let removeJsonBtn = tr.querySelector('.remove-json');
if (!mappedJsonNode) {
mappedJsonNode = document.createElement('span');
mappedJsonNode.className = 'mapped-json-node';
mappedJsonNode.style.marginLeft = '5px';
tr.querySelector('td:nth-child(7)').appendChild(mappedJsonNode);
}
if (!removeJsonBtn) {
removeJsonBtn = document.createElement('button');
removeJsonBtn.className = 'btn btn-danger btn-sm remove-json';
removeJsonBtn.textContent = 'X';
removeJsonBtn.style.marginLeft = '5px';
removeJsonBtn.setAttribute('data-id', mappingId);
tr.querySelector('td:nth-child(7)').appendChild(removeJsonBtn);
}
mappedJsonNode.textContent = event.target.value ? `(${event.target.value})` : '';
mappedJsonNode.style.display = event.target.value ? 'inline' : 'none';
removeJsonBtn.style.display = event.target.value ? 'inline-block' : 'none';
const mappingSelect = tr.querySelector('.mapping-select');
if (mappingSelect && mappingSelect.value !== 'json') {
return;
}
saveMapping(
mappingId,
'json',
manualInput ? manualInput.value : '',
null,
null,
event.target.value
);
updateJsonDropdowns();
}
});
document.getElementById('schemaFieldsBody').addEventListener('change', function(event) { document.getElementById('schemaFieldsBody').addEventListener('change', function(event) {
if (event.target.classList.contains('manual-default') && event.target.tagName === 'SELECT') { if (event.target.classList.contains('manual-default') && event.target.tagName === 'SELECT') {
@@ -1968,9 +2077,11 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
updateXlsDropdowns(); updateXlsDropdowns();
} }
if (data.success && mappingType === 'json' && jsonNode) { if (data.success && mappingType === 'json') {
usedJsonNodesFromDB = usedJsonNodesFromDB.filter(node => node !== jsonNode); usedJsonNodesFromDB = Array.from(document.querySelectorAll('select.json-nodes'))
usedJsonNodesFromDB.push(jsonNode); .map(select => select.value || select.dataset.currentJson || '')
.filter(Boolean);
updateJsonDropdowns(); updateJsonDropdowns();
} }
}) })
@@ -2205,6 +2316,17 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
} }
if (sourceType === 'API' && availableJsonNodes.length) { if (sourceType === 'API' && availableJsonNodes.length) {
const rawJson = document.getElementById('apiJsonExample')?.value || '';
if (rawJson.trim()) {
try {
jsonNodeLabels = {};
flattenJsonNodes(JSON.parse(rawJson));
} catch (e) {
console.warn('Cannot rebuild JSON labels from sample JSON', e);
}
}
updateJsonDropdowns(); updateJsonDropdowns();
} }
+3 -5
View File
@@ -259,11 +259,9 @@ $matrixGroups = array_values($matrixGroups);
</div> </div>
<div class="d-flex flex-wrap align-items-center gap-2 mb-3"> <div class="d-flex flex-wrap align-items-center gap-2 mb-3">
<div class="form-check m-0"> <input type="hidden" id="analysisWebOnly" value="1">
<input class="form-check-input" type="checkbox" id="analysisWebOnly"> <div class="small text-success fw-semibold">
<label class="form-check-label small" for="analysisWebOnly"> Showing WEB analyses only
Web only
</label>
</div> </div>
<div class="flex-grow-1" style="min-width: 220px;"> <div class="flex-grow-1" style="min-width: 220px;">
+20 -2
View File
@@ -46,8 +46,8 @@
{ {
"IdSchemaCustomFields": 48, "IdSchemaCustomFields": 48,
"ConteggioClienti": 0, "ConteggioClienti": 0,
"Nome": "Standard Generico \/ Generic Standard", "Nome": "Standard \/ Generico",
"Descrizione": "Schema per tutti i campioni di qualsiasi matrice escluso cuoio\/pelle\r\n\r\n" "Descrizione": "\r\n"
}, },
{ {
"IdSchemaCustomFields": 49, "IdSchemaCustomFields": 49,
@@ -882,6 +882,24 @@
"ConteggioClienti": 0, "ConteggioClienti": 0,
"Nome": "LIMS-CIM - MAX MARA", "Nome": "LIMS-CIM - MAX MARA",
"Descrizione": "Schema per MAX MARA scambio dati Database" "Descrizione": "Schema per MAX MARA scambio dati Database"
},
{
"IdSchemaCustomFields": 203,
"ConteggioClienti": 0,
"Nome": "Vince",
"Descrizione": "Schema per tutti i campioni di VINCE\r\n\r\n"
},
{
"IdSchemaCustomFields": 204,
"ConteggioClienti": 0,
"Nome": "Max Mara",
"Descrizione": "Schema da usare per Max Mara\r\n"
},
{
"IdSchemaCustomFields": 205,
"ConteggioClienti": 0,
"Nome": "Chanel Flammability",
"Descrizione": "Schema per Chanel Flammability\r\n"
} }
] ]
} }
+4 -1
View File
@@ -82,6 +82,7 @@ try {
template_id, template_id,
schema_id, schema_id,
field_id, field_id,
field_order,
data_type, data_type,
is_required, is_required,
default_value, default_value,
@@ -97,6 +98,7 @@ try {
:template_id, :template_id,
:schema_id, :schema_id,
:field_id, :field_id,
:field_order,
:data_type, :data_type,
:is_required, :is_required,
:default_value, :default_value,
@@ -116,6 +118,7 @@ try {
UPDATE template_mapping UPDATE template_mapping
SET SET
schema_id = :schema_id, schema_id = :schema_id,
field_order = :field_order,
data_type = :data_type, data_type = :data_type,
is_required = :is_required, is_required = :is_required,
default_value = :default_value, default_value = :default_value,
@@ -172,6 +175,7 @@ try {
$data = [ $data = [
':schema_id' => $schema_id, ':schema_id' => $schema_id,
':field_order' => (int)($field['Ordine'] ?? 9999),
':data_type' => $newDataType, ':data_type' => $newDataType,
':is_required' => !empty($custom_field['ObbligatorioWeb']) ? 1 : 0, ':is_required' => !empty($custom_field['ObbligatorioWeb']) ? 1 : 0,
':default_value' => $custom_field['ValoreDefault'] ?? null, ':default_value' => $custom_field['ValoreDefault'] ?? null,
@@ -234,7 +238,6 @@ try {
$response["success"] = true; $response["success"] = true;
$response["message"] = "Schema JSON updated, mappings synchronized, removed fields deleted, and changed fields updated successfully."; $response["message"] = "Schema JSON updated, mappings synchronized, removed fields deleted, and changed fields updated successfully.";
} catch (Exception $e) { } catch (Exception $e) {
if (isset($pdo) && $pdo->inTransaction()) { if (isset($pdo) && $pdo->inTransaction()) {
$pdo->rollback(); $pdo->rollback();
+122 -4
View File
@@ -1,4 +1,5 @@
<?php <?php
/** /**
* Validate rows before export to LIMS. * Validate rows before export to LIMS.
* *
@@ -88,6 +89,58 @@ $validators[] = function (int $iddatadb, array $ctx): array {
return []; return [];
}; };
// 3. All LIMS-mandatory fields must be filled.
$validators[] = function (int $iddatadb, array $ctx): array {
$record = $ctx['record'] ?? null;
if (!$record) {
return [];
}
$errors = [];
// Fixed fields (stored as columns in datadb)
foreach (($ctx['requiredFixed'] ?? []) as $key => $label) {
$col = $ctx['fixedAliasMap'][$key] ?? null;
$val = $col !== null ? ($record[$col] ?? null) : null;
if ($val === null || $val === '' || (int) $val === 0) {
$errors[] = [
'field' => $key,
'message' => $label . ' è obbligatorio.',
];
}
}
// Custom fields (values stored in import_data_details, keyed by mapping_id)
foreach (($ctx['requiredCustom'] ?? []) as $cf) {
$val = $ctx['customValues'][(int) $cf['mapping_id']] ?? null;
if ($val === null || trim((string) $val) === '') {
$errors[] = [
'field' => 'field_label:' . $cf['field_label'],
'message' => rtrim($cf['field_label'], ': ') . ' è obbligatorio.',
];
}
}
return $errors;
};
// Logical fixed_field_key - real datadb column (mirrors imported.php $fixedAliasMap)
$fixedAliasMap = [
'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 keys NOT enforced by the generic mandatory check above:
// - ConsegnaRichiesta: handled by its dedicated validator (also checks the date)
// - ClienteFornitore / ClienteAnalisi: nullable placeholders, sent as null on
// export and accepted by LIMS.
$skipRequiredFixed = ['ConsegnaRichiesta', 'ClienteFornitore', 'ClienteAnalisi'];
// ── Main ──────────────────────────────────────────────────────────────────── // ── Main ────────────────────────────────────────────────────────────────────
try { try {
@@ -104,9 +157,12 @@ try {
$iddatadbList = array_column($rows, 'iddatadb'); $iddatadbList = array_column($rows, 'iddatadb');
$placeholders = implode(',', array_fill(0, count($iddatadbList), '?')); $placeholders = implode(',', array_fill(0, count($iddatadbList), '?'));
// Records (datadb) for fixed field validation // Records (datadb) — templateid + fixed-field columns for mandatory validation
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
SELECT iddatadb, consegna_richiesta SELECT iddatadb, templateid, consegna_richiesta,
cliente_responsabile_id, moltiplicatore_prezzo_id,
anagrafica_certest_object_id, anagrafica_certest_service_id,
cliente_fornitore_id, clienteAnalisi
FROM datadb FROM datadb
WHERE iddatadb IN ($placeholders) WHERE iddatadb IN ($placeholders)
"); ");
@@ -128,6 +184,63 @@ try {
$partsInfo[(int)$r['iddatadb']][] = $r; $partsInfo[(int)$r['iddatadb']][] = $r;
} }
// Mandatory-field config per template
$templateIds = array_values(array_unique(array_filter(array_map(
fn($r) => (int)($r['templateid'] ?? 0),
$recordsInfo
))));
$requiredFixedByTemplate = []; // template_id => [ fixed_field_key => label ]
$requiredCustomByTemplate = []; // template_id => [ { mapping_id, field_label }, ... ]
if (!empty($templateIds)) {
$tplPlaceholders = implode(',', array_fill(0, count($templateIds), '?'));
// Required fixed fields (is_required synced from LIMS ObbligatorioWeb)
$stmt = $pdo->prepare("
SELECT template_id, fixed_field_key
FROM template_fixed_mapping
WHERE template_id IN ($tplPlaceholders) AND is_required = 1
");
$stmt->execute($templateIds);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
$key = $r['fixed_field_key'];
if (in_array($key, $skipRequiredFixed, true)) {
continue;
}
$requiredFixedByTemplate[(int)$r['template_id']][$key] = $key;
}
// Required custom fields that are visible in the import grid (excluding filed_id = 189 Tested component)
$stmt = $pdo->prepare("
SELECT id AS mapping_id, template_id, field_label
FROM template_mapping
WHERE template_id IN ($tplPlaceholders)
AND is_required = 1
AND is_visible_import = 1
AND id <> 189
");
$stmt->execute($templateIds);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
$requiredCustomByTemplate[(int)$r['template_id']][] = [
'mapping_id' => (int)$r['mapping_id'],
'field_label' => $r['field_label'],
];
}
}
// Custom field values per record (import_data_details.id is the FK to datadb)
$stmt = $pdo->prepare("
SELECT id AS iddatadb, mapping_id, field_value
FROM import_data_details
WHERE id IN ($placeholders)
");
$stmt->execute($iddatadbList);
$customValuesByRecord = []; // iddatadb => [ mapping_id => field_value ]
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
$customValuesByRecord[(int)$r['iddatadb']][(int)$r['mapping_id']] = $r['field_value'];
}
// ── Run validators per row ────────────────────────────────────────────── // ── Run validators per row ──────────────────────────────────────────────
$results = []; $results = [];
@@ -137,9 +250,15 @@ try {
$index = $rowInfo['index']; $index = $rowInfo['index'];
// Build context for validators // Build context for validators
$record = $recordsInfo[$iddatadb] ?? null;
$templateId = (int)($record['templateid'] ?? 0);
$ctx = [ $ctx = [
'record' => $recordsInfo[$iddatadb] ?? null, 'record' => $record,
'parts' => $partsInfo[$iddatadb] ?? [], 'parts' => $partsInfo[$iddatadb] ?? [],
'fixedAliasMap' => $fixedAliasMap,
'requiredFixed' => $requiredFixedByTemplate[$templateId] ?? [],
'requiredCustom' => $requiredCustomByTemplate[$templateId] ?? [],
'customValues' => $customValuesByRecord[$iddatadb] ?? [],
]; ];
$errors = []; $errors = [];
@@ -155,7 +274,6 @@ try {
} }
echo json_encode(['success' => true, 'results' => $results]); echo json_encode(['success' => true, 'results' => $results]);
} catch (Exception $e) { } catch (Exception $e) {
error_log("Validation error: " . $e->getMessage()); error_log("Validation error: " . $e->getMessage());
echo json_encode(['success' => false, 'message' => $e->getMessage()]); echo json_encode(['success' => false, 'message' => $e->getMessage()]);