Compare commits

...

36 Commits

Author SHA1 Message Date
solocla 955b663322 added end column for XLS in template and import 2026-06-18 16:50:38 +02:00
solocla 4dc010d80e added users admin and roles on menu 2026-06-18 10:54:44 +02:00
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 c3a6dd73b6 import backoff 2026-05-28 23:59:15 +03:00
solocla 44ed1186e0 added richmento pelletteria routine 2026-05-28 13:50:52 +02:00
solocla 9050cb1006 routine burberry 2026-05-26 12:15:45 +02:00
solocla e6820fdb62 added order column 2026-05-25 10:59:58 +02:00
solocla 5da37a7836 paulshark routine 2026-05-22 12:27:53 +02:00
solocla c5f27cb69a routine fendi 2026-05-21 10:11:53 +02:00
solocla 1d81d6c996 fixed import update 2026-05-20 18:47:41 +02:00
solocla 0c72dbf5ae sort first with exactly phrase 2026-05-20 17:01:07 +02:00
solocla 8455be04e1 remove hidden from xls import to avoid big file dimension 2026-05-20 16:47:18 +02:00
solocla e42d1b9c51 fix limit dropdown 2026-05-19 16:20:55 +02:00
solocla 3e69e3c322 fixed routine valentino 2026-05-19 11:23:26 +02:00
solocla 0eb4f7a2ad valentino routine 2026-05-19 11:12:18 +02:00
solocla 4f2cfc1930 fix render gird for doppi apici 2026-05-18 15:38:43 +02:00
solocla df075dd76a main field show in imported 2026-05-14 10:14:59 +02:00
solocla 6460454201 fixed main field 2026-05-14 10:00:47 +02:00
solocla 574ddbbd32 fixed propagation 2026-05-13 15:12:13 +02:00
solocla 41f414db5c 2 main 2026-05-13 14:58:13 +02:00
solocla 4a863e8c16 fixed double scroll bar parts dropdown 2026-05-13 14:26:11 +02:00
solocla b431f1d4e9 added top scrollbar imported 2026-05-13 14:22:43 +02:00
solocla f97b52f158 skip empty parts during clone and LIMS export 2026-05-13 11:54:28 +02:00
solocla 836fc055ec cliente reponsabile change based on client 2026-05-12 15:48:35 +02:00
solocla e8dd585df4 added arrows to jump parts 2026-05-12 09:37:52 +02:00
solocla 198b8c08ad Revert "update scheme different obbligatorioweb"
This reverts commit a3eb0f0a57.
2026-05-11 14:47:42 +02:00
solocla 28c467d55e fixed order import 2026-05-11 14:24:35 +02:00
solocla 56eee99a67 change template and import o use also sheet number of XLS 2026-05-09 15:39:43 +02:00
solocla f514b3d2c7 added sheet and config API in insert edit template 2026-05-09 09:44:52 +02:00
solocla a3eb0f0a57 update scheme different obbligatorioweb 2026-05-09 09:30:59 +02:00
RMubarakzyanov 67bbd9bbbb export analyses 2026-04-21 00:09:59 +03:00
37 changed files with 5321 additions and 632 deletions
+2
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
@@ -60,3 +61,4 @@ yarn-error.log
*.log *.log
public/userarea/cache/ public/userarea/cache/
public/userarea/error_log.txt
@@ -109,6 +109,8 @@ class LoginController extends Controller
// Reindirizza in base al ruolo // Reindirizza in base al ruolo
if ($user->hasRole('Admin')) { if ($user->hasRole('Admin')) {
return redirect()->to('userarea/import_dashboard.php'); return redirect()->to('userarea/import_dashboard.php');
} elseif ($user->hasRole('SuperUser')) {
return redirect()->to('userarea/import_dashboard.php');
} elseif ($user->hasRole('User')) { } elseif ($user->hasRole('User')) {
return redirect()->to('userarea/import_dashboard.php'); return redirect()->to('userarea/import_dashboard.php');
} }
+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) {
+7 -5
View File
@@ -28,11 +28,13 @@ try {
// 1. Load source parts // 1. Load source parts
$stmtParts = $pdo->prepare(" $stmtParts = $pdo->prepare("
SELECT id, part_number, part_description, mix, idmatrice, note, dateexpiry SELECT id, part_number, part_description, mix, idmatrice, note, dateexpiry
FROM identification_parts FROM identification_parts
WHERE iddatadb = ? WHERE iddatadb = ?
ORDER BY part_number ASC, id ASC AND part_description IS NOT NULL
"); AND TRIM(part_description) <> ''
ORDER BY part_number ASC, id ASC
");
$stmtParts->execute([$sourceIddatadb]); $stmtParts->execute([$sourceIddatadb]);
$sourceParts = $stmtParts->fetchAll(PDO::FETCH_ASSOC); $sourceParts = $stmtParts->fetchAll(PDO::FETCH_ASSOC);
+172 -11
View File
@@ -26,6 +26,16 @@ $stmt = $pdo->prepare("SELECT * FROM routine");
$stmt->execute(); $stmt->execute();
$routines = $stmt->fetchAll(PDO::FETCH_ASSOC); $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Retrieve active API/JSON configurations
$stmt = $pdo->prepare("
SELECT id, name, provider_code, api_type, php_class_name
FROM api_configurations
WHERE is_active = 1
ORDER BY name ASC
");
$stmt->execute();
$apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
$buttonBgPalette = [ $buttonBgPalette = [
'#0d6efd' => 'Blue', '#0d6efd' => 'Blue',
'#6610f2' => 'Indigo', '#6610f2' => 'Indigo',
@@ -181,6 +191,8 @@ if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonText
<select name="source_type" id="sourceType" class="form-control" required> <select name="source_type" id="sourceType" class="form-control" required>
<option value="XLS" <?php echo (($template['source_type'] ?? 'XLS') === 'XLS') ? 'selected' : ''; ?>>XLS</option> <option value="XLS" <?php echo (($template['source_type'] ?? 'XLS') === 'XLS') ? 'selected' : ''; ?>>XLS</option>
<option value="API" <?php echo (($template['source_type'] ?? 'XLS') === 'API') ? 'selected' : ''; ?>>API</option> <option value="API" <?php echo (($template['source_type'] ?? 'XLS') === 'API') ? 'selected' : ''; ?>>API</option>
<option value="JSON" <?php echo (($template['source_type'] ?? 'XLS') === 'JSON') ? 'selected' : ''; ?>>JSON</option>
<option value="PDF" <?php echo (($template['source_type'] ?? 'XLS') === 'PDF') ? 'selected' : ''; ?>>PDF</option>
</select> </select>
<small class="text-muted">Choose the source used by this template</small> <small class="text-muted">Choose the source used by this template</small>
</div> </div>
@@ -195,6 +207,75 @@ if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonText
<input type="text" name="start_column" id="startColumn" class="form-control" value="<?php echo htmlspecialchars($template['start_column'] ?? ''); ?>"> <input type="text" name="start_column" id="startColumn" class="form-control" value="<?php echo htmlspecialchars($template['start_column'] ?? ''); ?>">
</div> </div>
<div class="mb-3" id="xlsEndColumnWrapper">
<label class="form-label">Last Column included</label>
<input
type="text"
name="xls_end_column"
id="xlsEndColumn"
class="form-control"
value="<?php echo htmlspecialchars($template['xls_end_column'] ?? '', ENT_QUOTES, 'UTF-8'); ?>"
placeholder="Example: AN">
<small class="text-danger fw-semibold">
Attention: if left empty, the system will read the entire XLS/XLSX sheet.
Dirty Excel files may cause memory errors or timeout.
</small>
</div>
<div class="mb-3" id="xlsSheetNumberWrapper">
<label class="form-label">XLS Sheet Number</label>
<input
type="number"
name="xls_sheet_index"
id="xlsSheetIndex"
class="form-control"
min="0"
value="<?php echo htmlspecialchars($template['xls_sheet_index'] ?? 0); ?>">
<small class="text-muted">
Use 0 for the first sheet, 1 for the second sheet, 2 for the third sheet, and so on.
</small>
</div>
<div class="mb-3" id="apiConfigWrapper" style="display: none;">
<label class="form-label">API / JSON Configuration *</label>
<select name="api_config_id" id="apiConfigSelect" class="form-control">
<option value="">Select an API configuration...</option>
<?php foreach ($apiConfigurations as $apiConfig): ?>
<?php
$apiLabelParts = [];
if (!empty($apiConfig['name'])) {
$apiLabelParts[] = $apiConfig['name'];
}
if (!empty($apiConfig['provider_code'])) {
$apiLabelParts[] = '[' . $apiConfig['provider_code'] . ']';
}
if (!empty($apiConfig['api_type'])) {
$apiLabelParts[] = '(' . $apiConfig['api_type'] . ')';
}
if (!empty($apiConfig['php_class_name'])) {
$apiLabelParts[] = '- ' . $apiConfig['php_class_name'];
}
$apiLabel = implode(' ', $apiLabelParts);
?>
<option
value="<?php echo (int)$apiConfig['id']; ?>"
<?php echo ((int)($template['api_config_id'] ?? 0) === (int)$apiConfig['id']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($apiLabel, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
<small class="text-muted">
Select the API/JSON configuration linked to this template.
</small>
</div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label"><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></label> <label class="form-label"><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></label>
<textarea name="description" class="form-control"><?php echo htmlspecialchars($template['description'] ?? ''); ?></textarea> <textarea name="description" class="form-control"><?php echo htmlspecialchars($template['description'] ?? ''); ?></textarea>
@@ -335,10 +416,18 @@ if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonText
const routineAction3 = document.getElementById("routineAction3"); const routineAction3 = document.getElementById("routineAction3");
const sourceType = document.getElementById("sourceType"); const sourceType = document.getElementById("sourceType");
const headerRowWrapper = document.getElementById("headerRowWrapper"); const headerRowWrapper = document.getElementById("headerRowWrapper");
const startColumnWrapper = document.getElementById("startColumnWrapper"); const startColumnWrapper = document.getElementById("startColumnWrapper");
const xlsEndColumnWrapper = document.getElementById("xlsEndColumnWrapper");
const xlsSheetNumberWrapper = document.getElementById("xlsSheetNumberWrapper");
const apiConfigWrapper = document.getElementById("apiConfigWrapper");
const headerRow = document.getElementById("headerRow"); const headerRow = document.getElementById("headerRow");
const startColumn = document.getElementById("startColumn"); const startColumn = document.getElementById("startColumn");
const xlsEndColumn = document.getElementById("xlsEndColumn");
const xlsSheetIndex = document.getElementById("xlsSheetIndex");
const apiConfigSelect = document.getElementById("apiConfigSelect");
const selectedClientId = <?php echo json_encode((int)($template['idclient'] ?? 0)); ?>; const selectedClientId = <?php echo json_encode((int)($template['idclient'] ?? 0)); ?>;
const selectedSchemaId = <?php echo json_encode((int)($template['idschema'] ?? 0)); ?>; const selectedSchemaId = <?php echo json_encode((int)($template['idschema'] ?? 0)); ?>;
@@ -358,27 +447,61 @@ if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonText
allowClear: true allowClear: true
}); });
$('#apiConfigSelect').select2({
placeholder: "Select an API configuration...",
allowClear: true
});
function updateSourceFields() { function updateSourceFields() {
const selectedSource = sourceType.value; const selectedSource = sourceType.value;
if (selectedSource === 'API') { const isXls = selectedSource === 'XLS';
headerRowWrapper.style.opacity = '0.6'; const isApiOrJson = selectedSource === 'API' || selectedSource === 'JSON';
startColumnWrapper.style.opacity = '0.6';
headerRow.required = false; if (isXls) {
startColumn.required = false; headerRowWrapper.style.display = 'block';
startColumnWrapper.style.display = 'block';
headerRow.disabled = true; xlsEndColumnWrapper.style.display = 'block';
startColumn.disabled = true; xlsSheetNumberWrapper.style.display = 'block';
} else {
headerRowWrapper.style.opacity = '1';
startColumnWrapper.style.opacity = '1';
headerRow.required = true; headerRow.required = true;
startColumn.required = true; startColumn.required = true;
xlsEndColumn.required = false;
headerRow.disabled = false; headerRow.disabled = false;
startColumn.disabled = false; startColumn.disabled = false;
xlsEndColumn.disabled = false;
xlsSheetIndex.disabled = false;
apiConfigWrapper.style.display = 'none';
apiConfigSelect.required = false;
apiConfigSelect.disabled = true;
$('#apiConfigSelect').val(null).trigger('change');
} else {
headerRowWrapper.style.display = 'none';
startColumnWrapper.style.display = 'none';
xlsEndColumnWrapper.style.display = 'none';
xlsSheetNumberWrapper.style.display = 'none';
headerRow.required = false;
startColumn.required = false;
xlsEndColumn.required = false;
headerRow.disabled = true;
startColumn.disabled = true;
xlsEndColumn.disabled = true;
xlsSheetIndex.disabled = true;
if (isApiOrJson) {
apiConfigWrapper.style.display = 'block';
apiConfigSelect.required = true;
apiConfigSelect.disabled = false;
} else {
apiConfigWrapper.style.display = 'none';
apiConfigSelect.required = false;
apiConfigSelect.disabled = true;
$('#apiConfigSelect').val(null).trigger('change');
}
} }
} }
@@ -604,6 +727,44 @@ if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonText
const routineId = routineSelect.value; const routineId = routineSelect.value;
formData.append("idroutine", routineId); formData.append("idroutine", routineId);
const selectedSource = sourceType.value;
if ((selectedSource === 'API' || selectedSource === 'JSON') && !apiConfigSelect.value) {
Swal.fire({
title: "Error!",
text: "Please select an API/JSON configuration.",
icon: "error",
confirmButtonText: "OK"
});
return;
}
if (selectedSource === 'XLS' && xlsSheetIndex.value === '') {
Swal.fire({
title: "Error!",
text: "Please enter the XLS sheet number.",
icon: "error",
confirmButtonText: "OK"
});
return;
}
if (selectedSource === 'XLS' && xlsEndColumn.value.trim() !== '') {
const lastColumnValue = xlsEndColumn.value.trim().toUpperCase();
if (!/^[A-Z]+$|^[1-9][0-9]*$/.test(lastColumnValue)) {
Swal.fire({
title: "Error!",
text: "Last Column must be an Excel column like AN or a positive number like 40.",
icon: "error",
confirmButtonText: "OK"
});
return;
}
xlsEndColumn.value = lastColumnValue;
}
fetch("process_edit_template_xls.php", { fetch("process_edit_template_xls.php", {
method: "POST", method: "POST",
body: formData body: formData
+32
View File
@@ -331,3 +331,35 @@
2026-04-30 15:01:25 - Errore nel recupero dati: HTTP 404, Risposta: {"title":"Not Found","status":404,"detail":"Not Found","instance":"GET /api/odata/Rapporto(2621521)","errorCode":"d25cbd678"} 2026-04-30 15:01:25 - Errore nel recupero dati: HTTP 404, Risposta: {"title":"Not Found","status":404,"detail":"Not Found","instance":"GET /api/odata/Rapporto(2621521)","errorCode":"d25cbd678"}
2026-04-30 15:02:04 - Errore nel recupero dati: HTTP 404, Risposta: 2026-04-30 15:02:04 - Errore nel recupero dati: HTTP 404, Risposta:
2026-04-30 15:03:19 - Errore nella richiesta: URL rejected: Malformed input to a URL function 2026-04-30 15:03:19 - Errore nella richiesta: URL rejected: Malformed input to a URL function
2026-06-16 14:29:15 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21037 ms: Couldn't connect to server, Risposta:
2026-06-16 14:29:36 [ClienteResponsabile] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21099 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:03 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21076 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:03 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21083 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:03 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21083 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:34 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21043 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:55 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21053 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:55 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21050 ms: Couldn't connect to server, Risposta:
2026-06-16 14:31:46 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21048 ms: Couldn't connect to server, Risposta:
2026-06-16 14:31:46 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21043 ms: Couldn't connect to server, Risposta:
2026-06-16 14:31:46 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21042 ms: Couldn't connect to server, Risposta:
2026-06-16 14:32:07 [ClienteResponsabile] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21047 ms: Couldn't connect to server, Risposta:
2026-06-16 14:33:09 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21058 ms: Couldn't connect to server, Risposta:
2026-06-16 14:33:09 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21057 ms: Couldn't connect to server, Risposta:
2026-06-16 14:33:09 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21057 ms: Couldn't connect to server, Risposta:
2026-06-16 14:33:30 [ClienteResponsabile] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21047 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:11 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21062 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:11 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21064 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:11 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21065 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:33 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21049 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:33 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21057 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:33 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21053 ms: Couldn't connect to server, Risposta:
2026-06-16 14:37:13 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21032 ms: Couldn't connect to server, Risposta:
2026-06-16 14:37:14 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21031 ms: Couldn't connect to server, Risposta:
2026-06-16 14:37:14 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21027 ms: Couldn't connect to server, Risposta:
2026-06-16 14:37:35 [ClienteResponsabile] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21054 ms: Couldn't connect to server, Risposta:
2026-06-16 17:13:55 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21067 ms: Couldn't connect to server, Risposta:
2026-06-16 17:14:25 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21065 ms: Couldn't connect to server, Risposta:
2026-06-16 17:14:46 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21071 ms: Couldn't connect to server, Risposta:
2026-06-16 17:14:46 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21078 ms: Couldn't connect to server, Risposta:
+132 -12
View File
@@ -59,6 +59,49 @@ function formatDateToExport($value)
return null; // Imposta null se non è una data valida return null; // Imposta null se non è una data valida
} }
// ImportaCommessa con retry: la chiamata è asincrona lato LIMS e a volte
// risponde 200 senza importare (StatoCommessaWeb resta "Inviata"/"Nuova").
// Riprova con backoff esponenziale finché non passa a "Elaborata".
function importaCommessaWithRetry($api, $commessaId, array $payload, $maxRetries = 3, $initialBackoff = 1)
{
$result = null;
$stato = null;
$succeeded = false;
$log = "";
$backoff = $initialBackoff;
set_time_limit(120); // i backoff non devono far scadere il timeout della richiesta
for ($attempt = 1; $attempt <= $maxRetries + 1; $attempt++) {
try {
$result = $api->post("CommessaWeb({$commessaId})/ImportaCommessa", $payload);
$stato = $result['StatoCommessaWeb'] ?? null;
$log .= "Attempt {$attempt}: HTTP 200, StatoCommessaWeb=" . ($stato ?? 'null') . "\n";
} catch (Exception $e) {
$stato = null;
$log .= "Attempt {$attempt}: EXCEPTION " . $e->getMessage() . "\n";
}
if ($stato === 'Elaborata') {
$succeeded = true;
break;
}
if ($attempt <= $maxRetries) {
$log .= " -> not Elaborata, waiting {$backoff}s before retry\n";
sleep($backoff);
$backoff *= 2;
}
}
return [
'succeeded' => $succeeded,
'stato' => $stato,
'result' => $result,
'log' => $log,
];
}
try { try {
$iddatadb = $_POST['iddatadb'] ?? null; $iddatadb = $_POST['iddatadb'] ?? null;
if (!$iddatadb) { if (!$iddatadb) {
@@ -107,11 +150,13 @@ try {
// 🔹 STEP 3: Fetch Parts (including idmatrice and part id for custom fields) // 🔹 STEP 3: Fetch Parts (including idmatrice and part id for custom fields)
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
SELECT id AS part_id, part_number, part_description, material, color, mix, idmatrice, dateexpiry SELECT id AS part_id, part_number, part_description, material, color, mix, idmatrice, dateexpiry
FROM identification_parts FROM identification_parts
WHERE iddatadb = :iddatadb WHERE iddatadb = :iddatadb
ORDER BY CAST(part_number AS UNSIGNED) ASC, part_number ASC AND part_description IS NOT NULL
"); AND TRIM(part_description) <> ''
ORDER BY CAST(part_number AS UNSIGNED) ASC, part_number ASC
");
$stmt->execute(['iddatadb' => $iddatadb]); $stmt->execute(['iddatadb' => $iddatadb]);
$parts = $stmt->fetchAll(PDO::FETCH_ASSOC); $parts = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -432,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
@@ -510,9 +620,8 @@ try {
$writeLog($logFileStep9, $logContentStep9, "STEP 9 - InviaCommessa (commessa={$commessaId})"); $writeLog($logFileStep9, $logContentStep9, "STEP 9 - InviaCommessa (commessa={$commessaId})");
// 🔹 STEP 9.5: Importazione da CommessaWeb a Commessa (commentato come richiesto) // 🔹 STEP 9.5: Importazione da CommessaWeb a Commessa (con retry)
// Supplier call: POST api/odata/CommessaWeb(XXX)/ImportaCommessa // Supplier call: POST api/odata/CommessaWeb(XXX)/ImportaCommessa
$importUserId = (!empty($lims_global_user_id) && is_numeric($lims_global_user_id)) $importUserId = (!empty($lims_global_user_id) && is_numeric($lims_global_user_id))
? (int) $lims_global_user_id ? (int) $lims_global_user_id
: 285; : 285;
@@ -520,17 +629,23 @@ try {
$importPayload = [ $importPayload = [
"IdUtente" => $importUserId "IdUtente" => $importUserId
]; ];
$importResult = $api->post("CommessaWeb({$commessaId})/ImportaCommessa", $importPayload);
$importPayloadLog = json_encode($importPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); $importPayloadLog = json_encode($importPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
// Logga il POST
$importOutcome = importaCommessaWithRetry($api, $commessaId, $importPayload);
$importResult = $importOutcome['result'];
$importStato = $importOutcome['stato'];
$importSucceeded = $importOutcome['succeeded'];
// Logga il POST (tutti i tentativi)
$logContentStep91 = "curl --location --request POST '{$apiBaseUrl}CommessaWeb({$commessaId})/ImportaCommessa' \\\n" . $logContentStep91 = "curl --location --request POST '{$apiBaseUrl}CommessaWeb({$commessaId})/ImportaCommessa' \\\n" .
"--header 'Content-Type: application/json' \\\n" . "--header 'Content-Type: application/json' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" . "--header 'Authorization: Bearer ••••••' \\\n" .
"--data '{$importPayloadLog}'\n\n" . "--data '{$importPayloadLog}'\n\n" .
"RESPONSE:\n" . json_encode($importResult, JSON_PRETTY_PRINT); "ATTEMPTS:\n" . $importOutcome['log'] . "\n" .
"SUCCEEDED: " . ($importSucceeded ? 'yes' : 'NO') . "\n\n" .
"LAST RESPONSE:\n" . json_encode($importResult, JSON_PRETTY_PRINT);
$logFileStep91 = $logDir . "commessa_{$commessaId}_importa_step91_" . time() . ".txt"; $logFileStep91 = $logDir . "commessa_{$commessaId}_importa_step91_" . time() . ".txt";
$writeLog($logFileStep91, $logContentStep91, "STEP 9.5 - ImportaCommessa (commessa={$commessaId})"); $writeLog($logFileStep91, $logContentStep91, "STEP 9.5 - ImportaCommessa (commessa={$commessaId}, succeeded=" . ($importSucceeded ? '1' : '0') . ")");
// 🔹 STEP 10: GET di controllo post-PATCH // 🔹 STEP 10: GET di controllo post-PATCH
$expand = "CommesseCustomFields(\$expand=CustomField)"; $expand = "CommesseCustomFields(\$expand=CustomField)";
@@ -579,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
@@ -599,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/';
+445 -59
View File
@@ -36,6 +36,15 @@
return d.innerHTML; return d.innerHTML;
} }
function escAttr(str) {
if (str === null || str === undefined) return "";
return String(str)
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
function getDetailValue(rowIndex, mappingId) { function getDetailValue(rowIndex, mappingId) {
return data[rowIndex].details[String(mappingId)] ?? ""; return data[rowIndex].details[String(mappingId)] ?? "";
} }
@@ -125,12 +134,35 @@
); );
} }
function sortSelect2ResultsByStart(data) {
const term = $(".select2-container--open .select2-search__field").val();
if (!term) {
return data;
}
const search = term.toLowerCase().trim();
return data.sort(function (a, b) {
const textA = (a.text || "").toLowerCase().trim();
const textB = (b.text || "").toLowerCase().trim();
const aStarts = textA.startsWith(search);
const bStarts = textB.startsWith(search);
if (aStarts && !bStarts) return -1;
if (!aStarts && bStarts) return 1;
return textA.localeCompare(textB, "it", { sensitivity: "base" });
});
}
// Select2 AJAX config for client selects // Select2 AJAX config for client selects
const clientSelect2Config = { const clientSelect2Config = {
placeholder: "Search client...", placeholder: "Search client...",
allowClear: true, allowClear: true,
width: "100%", width: "100%",
minimumInputLength: 0, minimumInputLength: 0,
sorter: sortSelect2ResultsByStart,
dropdownCssClass: "select2-dropdown-smaller", dropdownCssClass: "select2-dropdown-smaller",
ajax: { ajax: {
url: "search_clienti.php", url: "search_clienti.php",
@@ -242,7 +274,88 @@
return _pendingFixed[cacheKey]; return _pendingFixed[cacheKey];
} }
async function refreshDependentFixedFieldsForRow(rowIndex) {
const row = data[rowIndex];
if (!row) return;
const clientId = row.idclient || "";
// Find fixed fields that depend on idclient
const dependentFields = Object.keys(fixedFieldApiConfig).filter(
(key) => {
return fixedFieldApiConfig[key].dependsOn === "idclient";
},
);
if (dependentFields.length === 0) return;
for (const fieldKey of dependentFields) {
// When client changes, the old responsible is no longer reliable
if (
row.fixedFields &&
Object.prototype.hasOwnProperty.call(row.fixedFields, fieldKey)
) {
row.fixedFields[fieldKey] = "";
row._dirty = true;
}
// Reload options for the new client
if (clientId) {
await loadFixedFieldOptions(fieldKey, clientId);
}
}
// Re-render only this row so ClienteResponsabile select is rebuilt with the new options
renderSingleRow(rowIndex);
// If the first row client changes, update the top propagation select too
if (rowIndex === 0) {
await refreshTopDependentFixedSelect(
"ClienteResponsabile",
clientId,
);
}
updateDirtyIndicator();
}
// ── Custom field dropdown data loading ───────────────────────────────── // ── Custom field dropdown data loading ─────────────────────────────────
async function refreshTopDependentFixedSelect(fieldKey, clientId) {
if (!topContainer || !fieldKey) return;
const sel = topContainer.querySelector(
`.api-fixed-select[data-fixed-key="${fieldKey}"]`,
);
if (!sel) return;
// Destroy Select2 if already initialized
if ($(sel).hasClass("select2-hidden-accessible")) {
$(sel).select2("destroy");
}
sel.innerHTML = '<option value="">Seleziona...</option>';
if (!clientId) {
$(sel).select2({
placeholder: "Seleziona...",
allowClear: true,
width: "100%",
});
return;
}
const items = await loadFixedFieldOptions(fieldKey, clientId);
items.forEach((item) => {
sel.add(new Option(item.text, item.id));
});
$(sel).select2({
placeholder: "Seleziona...",
allowClear: true,
width: "100%",
});
}
// Select2 AJAX config factory for SceltaMultipla // Select2 AJAX config factory for SceltaMultipla
function sceltaSelect2Config(fieldId) { function sceltaSelect2Config(fieldId) {
@@ -251,6 +364,7 @@
allowClear: true, allowClear: true,
width: "100%", width: "100%",
minimumInputLength: 0, minimumInputLength: 0,
sorter: sortSelect2ResultsByStart,
ajax: { ajax: {
url: "search_customfield_values.php", url: "search_customfield_values.php",
dataType: "json", dataType: "json",
@@ -259,7 +373,7 @@
return { return {
field_id: fieldId, field_id: fieldId,
q: params.term || "", q: params.term || "",
limit: 10, limit: 0, // 0 = no limit for custom field values
}; };
}, },
processResults: function (data) { processResults: function (data) {
@@ -382,13 +496,13 @@
const row = data[rowIndex]; const row = data[rowIndex];
switch (col.type) { switch (col.type) {
case "main_field": case "main_field": {
div.innerHTML = createInputHTML( const val = getDetailValue(rowIndex, col.key);
col,
row.mainFieldValue || "", div.innerHTML = createInputHTML(col, val || "", rowIndex);
rowIndex,
);
break; break;
}
case "status": { case "status": {
const st = row.status || "i"; const st = row.status || "i";
@@ -438,7 +552,7 @@
case "tested_component": case "tested_component":
div.style.overflow = "visible"; div.style.overflow = "visible";
div.innerHTML = `<div style="display:flex; align-items:center; gap:4px; width:100%; height:100%;"> div.innerHTML = `<div style="display:flex; align-items:center; gap:4px; width:100%; height:100%;">
<input type="text" class="cell-input manual-input tested-component-input" value="${esc(row.tested_component || "")}" style="flex:1; min-width:0; height:28px;"> <input type="text" class="cell-input manual-input tested-component-input" value="${escAttr(row.tested_component || "")}" style="flex:1; min-width:0; height:28px;">
<button type="button" class="add-part-btn btn btn-sm btn-primary" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" style="display:inline-flex; align-items:center; justify-content:center; min-width:28px; width:28px; height:28px; padding:0; font-size:12px; flex-shrink:0; text-align:center;"> <button type="button" class="add-part-btn btn btn-sm btn-primary" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" style="display:inline-flex; align-items:center; justify-content:center; min-width:28px; width:28px; height:28px; padding:0; font-size:12px; flex-shrink:0; text-align:center;">
<i class="fas fa-plus" style="margin:0; padding:0;"></i> <i class="fas fa-plus" style="margin:0; padding:0;"></i>
</button> </button>
@@ -484,7 +598,7 @@
const cls = col.isManual ? "manual-input" : "auto-input"; const cls = col.isManual ? "manual-input" : "auto-input";
const reqCls = col.isRequired ? " required-input" : ""; const reqCls = col.isRequired ? " required-input" : "";
const req = col.isRequired ? " required" : ""; const req = col.isRequired ? " required" : "";
const v = esc(value); const v = escAttr(value);
if (col.dataType === "SceltaMultipla") { if (col.dataType === "SceltaMultipla") {
const options = buildDropdownOptionsHTML(col.fieldId, value); const options = buildDropdownOptionsHTML(col.fieldId, value);
@@ -509,7 +623,7 @@
if (col.dataType === "DATE") { if (col.dataType === "DATE") {
const reqCls = col.isRequired ? " required-input" : ""; const reqCls = col.isRequired ? " required-input" : "";
const req = col.isRequired ? " required" : ""; const req = col.isRequired ? " required" : "";
return `<input type="text" class="cell-input date-picker manual-input${reqCls} fixed-input" data-fixed-key="${col.key}" value="${esc(value)}"${req}>`; return `<input type="text" class="cell-input date-picker manual-input${reqCls} fixed-input" data-fixed-key="${escAttr(col.key)}" value="${escAttr(value)}"${req}>`;
} }
// Client-sourced fields → AJAX Select2 (like idclient) // Client-sourced fields → AJAX Select2 (like idclient)
@@ -522,7 +636,7 @@
const label = clientNameCache[value] || value; const label = clientNameCache[value] || value;
opts += `<option value="${esc(String(value))}" selected>${esc(String(label))}</option>`; opts += `<option value="${esc(String(value))}" selected>${esc(String(label))}</option>`;
} }
return `<select class="cell-input manual-input fixed-input searchable-client api-fixed-select${reqCls}" data-fixed-key="${col.key}" data-current-value="${esc(value)}"${req}>${opts}</select>`; return `<select class="cell-input manual-input fixed-input searchable-client api-fixed-select${reqCls}" data-fixed-key="${escAttr(col.key)}" data-current-value="${escAttr(value)}"${req}>${opts}</select>`;
} }
// Select — build from cache // Select — build from cache
@@ -540,7 +654,7 @@
const reqCls = col.isRequired ? " required-input" : ""; const reqCls = col.isRequired ? " required-input" : "";
const req = col.isRequired ? " required" : ""; const req = col.isRequired ? " required" : "";
return `<select class="cell-input manual-input fixed-input ${selectClass}${reqCls}" data-fixed-key="${col.key}" data-current-value="${esc(value)}"${req}>${options}</select>`; return `<select class="cell-input manual-input fixed-input ${selectClass}${reqCls}" data-fixed-key="${escAttr(col.key)}" data-current-value="${escAttr(value)}"${req}>${options}</select>`;
} }
function buildDropdownOptionsHTML(fieldId, selectedValue) { function buildDropdownOptionsHTML(fieldId, selectedValue) {
@@ -745,6 +859,143 @@
flatpickr(this, { dateFormat: "Y-m-d", allowInput: true }); flatpickr(this, { dateFormat: "Y-m-d", allowInput: true });
}); });
} }
function getInputTextWidth(input) {
const span = document.createElement("span");
const style = window.getComputedStyle(input);
span.style.position = "absolute";
span.style.visibility = "hidden";
span.style.whiteSpace = "pre";
span.style.font = style.font;
span.style.fontSize = style.fontSize;
span.style.fontFamily = style.fontFamily;
span.style.fontWeight = style.fontWeight;
span.textContent = input.value || input.placeholder || "";
document.body.appendChild(span);
const width = span.offsetWidth + 60;
document.body.removeChild(span);
return width;
}
function autoExpandColumnFromInput(input) {
if (!input) return;
const cell = input.closest(".grid-cell");
if (!cell || !cell.dataset.index) return;
const columnIndex = parseInt(cell.dataset.index, 10);
if (!columnIndex) return;
const wantedWidth = Math.max(120, getInputTextWidth(input));
const currentWidth = cell.offsetWidth || 0;
// Only expand, do not shrink automatically
if (wantedWidth <= currentWidth) return;
const newWidth = Math.min(wantedWidth, 900);
const header = document.querySelector(
`.grid-header[data-index="${columnIndex}"]`,
);
if (header) {
header.style.flex = `0 0 ${newWidth}px`;
}
const topCell = document.querySelector(
`.grid-top .grid-cell:nth-child(${columnIndex + 1})`,
);
if (topCell) {
topCell.style.flex = `0 0 ${newWidth}px`;
}
const cells = document.querySelectorAll(
`.grid-row .grid-cell[data-index="${columnIndex}"]`,
);
cells.forEach((c) => {
c.style.flex = `0 0 ${newWidth}px`;
});
const colPos = columnIndex - 1;
if (columns[colPos]) {
columns[colPos].width = newWidth;
}
}
function syncVisibleRowsToGridData() {
if (!rowContainer) return;
rowContainer
.querySelectorAll(".grid-cell[data-row]")
.forEach((cell) => {
const rowIndex = parseInt(cell.dataset.row, 10);
const row = data[rowIndex];
if (!row) return;
const colType = cell.dataset.colType;
const colKey = cell.dataset.col;
const input = cell.querySelector(".cell-input");
if (!input) return;
const value = $(input).hasClass("select2-hidden-accessible")
? $(input).val() || ""
: input.value || "";
if (colType === "detail" || colType === "main_field") {
if (!row.details) row.details = {};
if (
String(row.details[String(colKey)] ?? "") !==
String(value)
) {
row.details[String(colKey)] = value;
if (colType === "main_field") {
row.mainFieldValue = value;
}
row._dirty = true;
}
} else if (colType === "fixed") {
if (!row.fixedFields) row.fixedFields = {};
if (
String(row.fixedFields[colKey] ?? "") !== String(value)
) {
row.fixedFields[colKey] = value;
row._dirty = true;
}
} else if (colType === "idclient") {
if (String(row.idclient ?? "") !== String(value)) {
row.idclient = value;
row._dirty = true;
}
} else if (colType === "cliente_fornitore_id") {
if (
String(row.cliente_fornitore_id ?? "") !== String(value)
) {
row.cliente_fornitore_id = value;
row._dirty = true;
}
} else if (colType === "tested_component") {
if (String(row.tested_component ?? "") !== String(value)) {
row.tested_component = value;
row._dirty = true;
}
}
});
updateDirtyIndicator();
}
// ── Headers & Propagate row ──────────────────────────────────────────── // ── Headers & Propagate row ────────────────────────────────────────────
@@ -886,23 +1137,28 @@
} }
if (config && config.dependsOn) { if (config && config.dependsOn) {
// For dependent fields: merge all cached values across all clientIds // Dependent fixed fields, for example ClienteResponsabile:
const allItems = new Map(); // use the first row client, not all cached clients.
for (const [key, items] of Object.entries(fixedFieldCache)) { const firstClientId =
if (key.startsWith(fieldKey + "_")) { data[0]?.idclient || meta.defaultIdclient || "";
items.forEach((item) =>
allItems.set(String(item.id), item),
);
}
}
sel.innerHTML = '<option value="">Seleziona...</option>'; sel.innerHTML = '<option value="">Seleziona...</option>';
[...allItems.values()]
.sort((a, b) => if (firstClientId) {
String(a.text).localeCompare(String(b.text), "it", { const items =
sensitivity: "base", fixedFieldCache[fieldKey + "_" + firstClientId] || [];
}),
) items.forEach((item) => {
.forEach((item) => sel.add(new Option(item.text, item.id))); sel.add(new Option(item.text, item.id));
});
}
$(sel).select2({
placeholder: "Seleziona...",
allowClear: true,
width: "100%",
sorter: sortSelect2ResultsByStart,
});
} else { } else {
const items = fixedFieldCache[fieldKey] || []; const items = fixedFieldCache[fieldKey] || [];
sel.innerHTML = '<option value="">Seleziona...</option>'; sel.innerHTML = '<option value="">Seleziona...</option>';
@@ -950,6 +1206,8 @@
} else if (colType === "idclient") { } else if (colType === "idclient") {
data[rowIndex].idclient = value; data[rowIndex].idclient = value;
data[rowIndex]._dirty = true; data[rowIndex]._dirty = true;
refreshDependentFixedFieldsForRow(rowIndex);
} else if (colType === "cliente_fornitore_id") { } else if (colType === "cliente_fornitore_id") {
data[rowIndex].cliente_fornitore_id = value; data[rowIndex].cliente_fornitore_id = value;
data[rowIndex]._dirty = true; data[rowIndex]._dirty = true;
@@ -972,6 +1230,10 @@
const cell = e.target.closest(".grid-cell"); const cell = e.target.closest(".grid-cell");
if (!cell || !cell.dataset.row) return; if (!cell || !cell.dataset.row) return;
if (e.target.classList.contains("cell-input")) {
autoExpandColumnFromInput(e.target);
}
const rowIndex = parseInt(cell.dataset.row, 10); const rowIndex = parseInt(cell.dataset.row, 10);
const colType = cell.dataset.colType; const colType = cell.dataset.colType;
@@ -983,6 +1245,16 @@
} }
}); });
rowContainer.addEventListener("focusin", function (e) {
if (!e.target.classList.contains("cell-input")) return;
autoExpandColumnFromInput(e.target);
setTimeout(() => {
autoExpandColumnFromInput(e.target);
}, 50);
});
// Persist tested_component before clicking + // Persist tested_component before clicking +
document.addEventListener("mousedown", function (e) { document.addEventListener("mousedown", function (e) {
const btn = e.target.closest(".add-part-btn"); const btn = e.target.closest(".add-part-btn");
@@ -1011,62 +1283,149 @@
const btn = e.target.closest(".propagate-btn"); const btn = e.target.closest(".propagate-btn");
if (!btn) return; if (!btn) return;
const colIndex = parseInt(btn.dataset.colIndex); // Before propagating and re-rendering, persist current visible row values into gridData.
const column = btn.dataset.column; syncVisibleRowsToGridData();
if (isNaN(colIndex) && !column) return;
// Get value from the input/select in the same cell e.preventDefault();
const cell = e.stopPropagation();
btn.closest(".grid-cell") || btn.closest(".grid-top-cell"); e.stopImmediatePropagation();
const column = btn.dataset.column || "";
const colIndex = Number.isNaN(parseInt(btn.dataset.colIndex, 10))
? null
: parseInt(btn.dataset.colIndex, 10);
if (!column && colIndex === null) return;
// IMPORTANT:
// Read ONLY the input/select inside the same top propagation cell.
// Do not scan other top fields.
const cell = btn.closest(".grid-top-cell");
if (!cell) return; if (!cell) return;
const input = cell.querySelector("select, input");
if (!input) return;
const value = $(input).hasClass("select2-hidden-accessible")
? $(input).val()
: input.value;
// Cache Select2 label so re-render shows name not ID const input = cell.querySelector(".custom-field");
if (!input) return;
const value = $(input).hasClass("select2-hidden-accessible")
? $(input).val() || ""
: input.value || "";
// Do not propagate empty dropdown values.
// This prevents wiping rows when a top Select2 is empty/not fully initialized.
if (input.tagName === "SELECT" && value === "") {
console.warn(
"[gridRenderer] Empty select propagation blocked:",
column,
);
return;
}
// Cache selected label so re-render can show text instead of only ID.
if (value && $(input).hasClass("select2-hidden-accessible")) { if (value && $(input).hasClass("select2-hidden-accessible")) {
const label = $(input).find("option:selected").text(); const label = $(input).find("option:selected").text();
if (label && label !== value) { if (label && label !== value) {
clientNameCache[value] = label; if (
// Also cache for SceltaMultipla column === "idclient" ||
column === "cliente_fornitore_id" ||
input.classList.contains("searchable-client")
) {
clientNameCache[value] = label;
}
const fieldId = input.dataset?.fieldId; const fieldId = input.dataset?.fieldId;
if (fieldId) if (fieldId) {
dropdownNameCache[fieldId + "_" + value] = label; dropdownNameCache[fieldId + "_" + value] = label;
}
} }
} }
const col = columns[colIndex] || null; const col = colIndex !== null ? columns[colIndex] : null;
console.log("[gridRenderer] Propagating ONLY:", {
column: column,
colIndex: colIndex,
value: value,
label:
input.tagName === "SELECT"
? $(input).find("option:selected").text()
: value,
});
if (column === "idclient") { if (column === "idclient") {
data.forEach((row) => { data.forEach((row) => {
const oldClientId = row.idclient || "";
row.idclient = value; row.idclient = value;
// Clear ClienteResponsabile only if client really changed.
if (
String(oldClientId) !== String(value) &&
row.fixedFields &&
Object.prototype.hasOwnProperty.call(
row.fixedFields,
"ClienteResponsabile",
)
) {
row.fixedFields["ClienteResponsabile"] = "";
}
row._dirty = true; row._dirty = true;
}); });
} else if (column === "cliente_fornitore_id") {
loadFixedFieldOptions("ClienteResponsabile", value).then(() => {
refreshTopDependentFixedSelect(
"ClienteResponsabile",
value,
);
renderVisibleRows();
updateDirtyIndicator();
});
return;
}
if (column === "cliente_fornitore_id") {
data.forEach((row) => { data.forEach((row) => {
row.cliente_fornitore_id = value; row.cliente_fornitore_id = value;
row._dirty = true; row._dirty = true;
}); });
} else if (column && column.startsWith("fixed_")) {
renderVisibleRows();
updateDirtyIndicator();
return;
}
if (column && column.startsWith("fixed_")) {
const fixedKey = column.replace("fixed_", ""); const fixedKey = column.replace("fixed_", "");
data.forEach((row) => { data.forEach((row) => {
if (!row.fixedFields) row.fixedFields = {};
row.fixedFields[fixedKey] = value; row.fixedFields[fixedKey] = value;
row._dirty = true; row._dirty = true;
}); });
} else if (col) {
if (col.type === "detail" || col.type === "main_field") { renderVisibleRows();
data.forEach((row) => { updateDirtyIndicator();
row.details[col.key] = value; return;
if (col.type === "main_field")
row.mainFieldValue = value;
row._dirty = true;
});
}
} }
renderVisibleRows(); if (col && (col.type === "detail" || col.type === "main_field")) {
data.forEach((row) => {
if (!row.details) row.details = {};
row.details[col.key] = value;
if (col.type === "main_field") {
row.mainFieldValue = value;
}
row._dirty = true;
});
renderVisibleRows();
updateDirtyIndicator();
return;
}
}); });
// Select2 change events (don't bubble via native addEventListener) // Select2 change events (don't bubble via native addEventListener)
@@ -1086,6 +1445,8 @@
if (colType === "idclient") { if (colType === "idclient") {
data[rowIndex].idclient = value; data[rowIndex].idclient = value;
data[rowIndex]._dirty = true; data[rowIndex]._dirty = true;
refreshDependentFixedFieldsForRow(rowIndex);
} else if (colType === "cliente_fornitore_id") { } else if (colType === "cliente_fornitore_id") {
data[rowIndex].cliente_fornitore_id = value; data[rowIndex].cliente_fornitore_id = value;
data[rowIndex]._dirty = true; data[rowIndex]._dirty = true;
@@ -1094,14 +1455,39 @@
updateDirtyIndicator(); updateDirtyIndicator();
}); });
// Cache labels on SceltaMultipla change // Handle SceltaMultipla changes and persist them into gridData.
// Without this, a later renderVisibleRows() can rebuild the row with the old empty value.
$(rowContainer).on("change", ".searchable-dropdown", function () { $(rowContainer).on("change", ".searchable-dropdown", function () {
const val = $(this).val(); const cell = this.closest(".grid-cell");
if (!cell || !cell.dataset.row) return;
const rowIndex = parseInt(cell.dataset.row, 10);
const colType = cell.dataset.colType;
const colKey = cell.dataset.col;
const val = $(this).val() || "";
const fieldId = this.dataset.fieldId; const fieldId = this.dataset.fieldId;
// Cache label so re-render shows the text instead of only the ID.
if (val && fieldId) { if (val && fieldId) {
const label = $(this).find("option:selected").text(); const label = $(this).find("option:selected").text();
if (label && label !== val) if (label && label !== val) {
dropdownNameCache[fieldId + "_" + val] = label; dropdownNameCache[fieldId + "_" + val] = label;
}
}
// Persist value into gridData.
if (colType === "detail" || colType === "main_field") {
if (!data[rowIndex].details) data[rowIndex].details = {};
data[rowIndex].details[String(colKey)] = val;
if (colType === "main_field") {
data[rowIndex].mainFieldValue = val;
}
data[rowIndex]._dirty = true;
cell.classList.add("cell-changed");
updateDirtyIndicator();
} }
}); });
+47 -5
View File
@@ -167,7 +167,12 @@ error_log("Loaded template: " . print_r($template, true));
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div> <div>
<h6 class="mb-0"><?= htmlspecialchars($template['name']) ?></h6> <h6 class="mb-0"><?= htmlspecialchars($template['name']) ?></h6>
<small>Template ID: <?= $id ?>, Start Row: <?= $template['header_row'] ?>, Start Column: <?= $template['start_column'] ?></small> <small>
Template ID: <?= $id ?>,
Sheet Number: <?= (int)($template['xls_sheet_index'] ?? 0) ?>,
Start Row: <?= $template['header_row'] ?>,
Start Column: <?= $template['start_column'] ?>
</small>
</div> </div>
</div> </div>
</div> </div>
@@ -244,8 +249,9 @@ error_log("Loaded template: " . print_r($template, true));
const templateId = <?= $id ?>; const templateId = <?= $id ?>;
console.log('Template ID passed to formData:', templateId); console.log('Template ID passed to formData:', templateId);
formData.append('template_id', templateId); formData.append('template_id', templateId);
formData.append('header_row', <?= $template['header_row'] ?>); formData.append('header_row', <?= (int)$template['header_row'] ?>);
formData.append('start_column', <?= $template['start_column'] ?>); formData.append('start_column', <?= json_encode($template['start_column']) ?>);
formData.append('xls_sheet_index', <?= (int)($template['xls_sheet_index'] ?? 0) ?>);
fetch('process_import_xls2.php', { fetch('process_import_xls2.php', {
method: 'POST', method: 'POST',
@@ -331,8 +337,8 @@ error_log("Loaded template: " . print_r($template, true));
<form id="selectRowsForm" action="import_insert.php" method="POST"> <form id="selectRowsForm" action="import_insert.php" method="POST">
<input type="hidden" name="template_id" value="${data.template_id}"> <input type="hidden" name="template_id" value="${data.template_id}">
<input type="hidden" name="columns" value="${encodeURIComponent(JSON.stringify(data.columns))}"> <input type="hidden" name="columns" value="${encodeURIComponent(JSON.stringify(data.columns))}">
<input type="hidden" name="rows" value="${encodeURIComponent(JSON.stringify(data.rows))}"> <input type="hidden" name="rows" id="selectedRowsData" value="">
<input type="hidden" name="excelrows" value="${encodeURIComponent(JSON.stringify(data.excel_data.map(r => r.excelrow)))}"> <input type="hidden" name="excelrows" id="selectedExcelRowsData" value="">
<input type="hidden" name="filename" value="${data.filename}"> <input type="hidden" name="filename" value="${data.filename}">
<!-- TOP BUTTON --> <!-- TOP BUTTON -->
@@ -383,6 +389,42 @@ error_log("Loaded template: " . print_r($template, true));
`; `;
tableContainer.innerHTML = html; tableContainer.innerHTML = html;
const selectRowsForm = document.getElementById('selectRowsForm');
selectRowsForm.addEventListener('submit', function(e) {
const checkedBoxes = Array.from(document.querySelectorAll('.row-checkbox:checked'));
if (checkedBoxes.length === 0) {
e.preventDefault();
alert('Seleziona almeno una riga.');
return;
}
const selectedRows = [];
const selectedExcelRows = [];
checkedBoxes.forEach((cb, newIndex) => {
const originalIndex = parseInt(cb.value, 10);
if (data.rows && data.rows[originalIndex]) {
selectedRows.push(data.rows[originalIndex]);
}
if (data.excel_data && data.excel_data[originalIndex]) {
selectedExcelRows.push(data.excel_data[originalIndex].excelrow);
}
// Reindex selected_rows so import_insert.php receives only the reduced rows array
cb.value = newIndex;
});
document.getElementById('selectedRowsData').value =
encodeURIComponent(JSON.stringify(selectedRows));
document.getElementById('selectedExcelRowsData').value =
encodeURIComponent(JSON.stringify(selectedExcelRows));
});
const topTableScrollbar = document.getElementById('topTableScrollbar'); const topTableScrollbar = document.getElementById('topTableScrollbar');
const topTableScrollbarInner = document.getElementById('topTableScrollbarInner'); const topTableScrollbarInner = document.getElementById('topTableScrollbarInner');
const mainTableContainer = document.getElementById('mainTableContainer'); const mainTableContainer = document.getElementById('mainTableContainer');
+118 -63
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);
@@ -55,15 +56,22 @@ if (empty($allMappings)) {
exit; exit;
} }
// Trova il campo main_field // Find up to 2 main fields
$mainFieldMapping = null; $mainFieldMappings = [];
foreach ($allMappings as $mapping) { foreach ($allMappings as $mapping) {
if ($mapping['main_field'] == 1 && $mapping['is_visible_import'] == 1) { if ((string)$mapping['main_field'] === '1' && (int)$mapping['is_visible_import'] === 1) {
$mainFieldMapping = $mapping; $mainFieldMappings[] = $mapping;
}
if (count($mainFieldMappings) >= 2) {
break; break;
} }
} }
// Backward compatibility: first main field
$mainFieldMapping = $mainFieldMappings[0] ?? null;
// Recupera l'idclient di default dal template (se presente) // Recupera l'idclient di default dal template (se presente)
$template_stmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?"); $template_stmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
$template_stmt->execute([$template_id]); $template_stmt->execute([$template_id]);
@@ -91,7 +99,7 @@ $stmt = $pdo->prepare("
FROM datadb d FROM datadb d
LEFT JOIN auth_users u ON d.user_id = u.id LEFT JOIN auth_users u ON d.user_id = u.id
{$baseWhere} {$baseWhere}
ORDER BY d.iddatadb DESC ORDER BY d.excelrow ASC, d.iddatadb ASC
{$limitClause} {$limitClause}
"); ");
$stmt->execute($baseParams); $stmt->execute($baseParams);
@@ -224,11 +232,18 @@ foreach ($importedData as $index => $row) {
$rowObj['details'][(string)$d['mapping_id']] = $d['field_value'] ?? ''; $rowObj['details'][(string)$d['mapping_id']] = $d['field_value'] ?? '';
} }
// Main field value // Main field values
foreach ($mainFieldMappings as $mainMapping) {
$mainDetail = array_filter($rowDetails, fn($d) => $d['mapping_id'] == $mainMapping['id']);
$mainDetail = reset($mainDetail) ?: ['field_value' => $mainMapping['manual_default'] ?? ''];
$rowObj['details'][(string)$mainMapping['id']] =
$mainDetail['field_value'] ?? $mainMapping['manual_default'] ?? '';
}
// Backward compatibility: first main value
if ($mainFieldMapping) { if ($mainFieldMapping) {
$mainDetail = array_filter($rowDetails, fn($d) => $d['mapping_id'] == $mainFieldMapping['id']); $rowObj['mainFieldValue'] = $rowObj['details'][(string)$mainFieldMapping['id']] ?? '';
$mainDetail = reset($mainDetail) ?: ['field_value' => $mainFieldMapping['manual_default'] ?? ''];
$rowObj['mainFieldValue'] = $mainDetail['field_value'] ?? $mainFieldMapping['manual_default'] ?? '';
} }
$rowObj['_dirty'] = false; $rowObj['_dirty'] = false;
@@ -238,18 +253,27 @@ foreach ($importedData as $index => $row) {
// Build columns in display order // Build columns in display order
$gridColumns = []; $gridColumns = [];
// 1. Main field // 1. Main fields first, immediately after buttons
if ($mainFieldMapping) { foreach ($allMappings as $mapping) {
$gridColumns[] = [ if (
'type' => 'main_field', (int)$mapping['is_visible_import'] === 1
'key' => (string)$mainFieldMapping['id'], && (string)$mapping['main_field'] === '1'
'label' => $mainFieldMapping['field_label'], && trim((string)$mapping['field_label']) !== 'Tested Component:'
'dataType' => $mainFieldMapping['data_type'], ) {
'isManual' => (bool)$mainFieldMapping['is_manual'], $gridColumns[] = [
'isRequired' => (bool)$mainFieldMapping['is_required'], 'type' => 'main_field',
'fieldId' => $mainFieldMapping['field_id'] ?? null, 'key' => (string)$mapping['id'],
'width' => 150, 'label' => $mapping['field_label'],
]; 'dataType' => $mapping['data_type'],
'isManual' => (bool)$mapping['is_manual'],
'isRequired' => (bool)$mapping['is_required'],
'fieldId' => $mapping['field_id'] ?? null,
'fieldOrder' => (int)($mapping['field_order'] ?? 9999),
'manualDefault' => $mapping['manual_default'] ?? '',
'autoValue' => $mapping['auto_value'] ?? 'none',
'width' => 150,
];
}
} }
// 2. Status // 2. Status
@@ -261,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];
@@ -342,7 +346,8 @@ $gridMeta = [
'slugMapping' => $slugMapping, 'slugMapping' => $slugMapping,
'timeLabels' => $timeLabels, 'timeLabels' => $timeLabels,
'columns' => $gridColumns, 'columns' => $gridColumns,
'mainFieldMapping' => $mainFieldMapping, 'mainFieldMapping' => $mainFieldMapping,
'mainFieldMappings' => $mainFieldMappings,
'totalRows' => count($gridDataArray), 'totalRows' => count($gridDataArray),
]; ];
@@ -350,6 +355,9 @@ $gridMeta = [
<script> <script>
window.gridData = <?= json_encode($gridDataArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>; window.gridData = <?= json_encode($gridDataArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>; window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
// Visible records in the current imported.php page
window.visibleIddatadbList = window.gridData.map(row => parseInt(row.iddatadb, 10)).filter(Boolean);
</script> </script>
<!doctype html> <!doctype html>
@@ -666,7 +674,33 @@ $gridMeta = [
flex-shrink: 0; flex-shrink: 0;
} }
.grid-row .grid-header:nth-child(2) { <?php if (isset($mainFieldMappings) && count($mainFieldMappings) >= 2): ?>
/* Sticky second Main column - only when the template has 2 Main fields */
.grid-top .grid-cell:nth-child(3),
#gridHeaderContainer .grid-header:nth-child(3),
.grid-row .grid-cell:nth-child(3) {
position: sticky !important;
left: 360px;
z-index: 7;
background: white;
overflow: visible;
flex-shrink: 0;
}
#gridHeaderContainer .grid-header:nth-child(3) {
background-color: #e9ecef;
}
.grid-row:nth-child(even) .grid-cell:nth-child(3) {
background-color: #f8f9fa;
}
.grid-row:hover .grid-cell:nth-child(3) {
background-color: #e9ecef;
}
<?php endif; ?>.grid-row .grid-header:nth-child(2) {
background-color: #e9ecef; background-color: #e9ecef;
} }
@@ -1407,6 +1441,9 @@ $gridMeta = [
const topScrollbar = document.getElementById('topScrollbar'); const topScrollbar = document.getElementById('topScrollbar');
const topScrollbarInner = document.getElementById('topScrollbarInner'); const topScrollbarInner = document.getElementById('topScrollbarInner');
const gridContainer = document.getElementById('gridContainer'); const gridContainer = document.getElementById('gridContainer');
const gridRowContainer = document.getElementById('gridRowContainer');
const gridHeaderContainer = document.getElementById('gridHeaderContainer');
const gridTopContainer = document.getElementById('gridTopContainer');
if (!topScrollbar || !topScrollbarInner || !gridContainer) return; if (!topScrollbar || !topScrollbarInner || !gridContainer) return;
@@ -1414,14 +1451,22 @@ $gridMeta = [
let syncingFromGrid = false; let syncingFromGrid = false;
function updateTopScrollbarWidth() { function updateTopScrollbarWidth() {
topScrollbarInner.style.width = gridContainer.scrollWidth + 'px'; const realWidth = Math.max(
gridContainer.scrollWidth,
gridHeaderContainer ? gridHeaderContainer.scrollWidth : 0,
gridTopContainer ? gridTopContainer.scrollWidth : 0,
gridRowContainer ? gridRowContainer.scrollWidth : 0
);
// Mostra la barra solo se serve davvero topScrollbarInner.style.width = realWidth + 'px';
if (gridContainer.scrollWidth > gridContainer.clientWidth) {
if (realWidth > gridContainer.clientWidth) {
topScrollbar.style.display = 'block'; topScrollbar.style.display = 'block';
} else { } else {
topScrollbar.style.display = 'none'; topScrollbar.style.display = 'none';
} }
topScrollbar.scrollLeft = gridContainer.scrollLeft;
} }
topScrollbar.addEventListener('scroll', function() { topScrollbar.addEventListener('scroll', function() {
@@ -1438,14 +1483,24 @@ $gridMeta = [
syncingFromGrid = false; syncingFromGrid = false;
}); });
updateTopScrollbarWidth();
window.addEventListener('resize', updateTopScrollbarWidth); window.addEventListener('resize', updateTopScrollbarWidth);
// Ritarda un attimo per sicurezza, visto che la griglia viene renderizzata via JS // Recalculate after JS grid rendering
setTimeout(updateTopScrollbarWidth, 200); setTimeout(updateTopScrollbarWidth, 100);
setTimeout(updateTopScrollbarWidth, 600); setTimeout(updateTopScrollbarWidth, 300);
setTimeout(updateTopScrollbarWidth, 1200); setTimeout(updateTopScrollbarWidth, 700);
setTimeout(updateTopScrollbarWidth, 1500);
// Recalculate automatically when rows/header/top controls are rendered or changed
const observer = new MutationObserver(updateTopScrollbarWidth);
if (gridContainer) {
observer.observe(gridContainer, {
childList: true,
subtree: true,
attributes: true
});
}
}); });
</script> </script>
+91 -46
View File
@@ -6,102 +6,147 @@
<div> <div>
<h4 class="logo-text"><?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></h4> <h4 class="logo-text"><?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></h4>
</div> </div>
<div class="toggle-icon ms-auto"><i class='bx bx-arrow-back'></i> <div class="toggle-icon ms-auto">
<i class='bx bx-arrow-back'></i>
</div> </div>
</div> </div>
<!--navigation--> <!--navigation-->
<ul class="metismenu" id="menu"> <ul class="metismenu" id="menu">
<!-- user, admin, superuser menù -->
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('User')) || (Auth::user()->hasRole('Superuser'))) : ?> <?php
$currentUser = Auth::user();
$isAdmin = $currentUser->hasRole('Admin');
$isSuperUser = $currentUser->hasRole('SuperUser');
$isUser = $currentUser->hasRole('User');
$canAccessMainMenu = $isAdmin || $isSuperUser || $isUser;
$canAccessUserAdmin = $isAdmin || $isSuperUser;
$canAccessTemplates = $isAdmin || $isSuperUser;
?>
<!-- user, admin, superuser menu -->
<?php if ($canAccessMainMenu) : ?>
<li> <li>
<a href="javascript:;" class="has-arrow"> <a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class='bx bx-home-alt'></i> <div class="parent-icon">
<i class='bx bx-home-alt'></i>
</div> </div>
<div class="menu-title">Dashboard</div> <div class="menu-title">Dashboard</div>
</a> </a>
<ul> <ul>
<!-- <li> <a href="index.php"><i class='bx bx-radio-circle'></i>Default</a> <li>
</li> --> <a href="import_dashboard.php">
<li> <a href="import_dashboard.php"><i class='bx bx-radio-circle'></i>XLS Import</a> <i class='bx bx-radio-circle'></i>XLS Import
</a>
</li> </li>
</ul> </ul>
</li> </li>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class="bx bx-category"></i>
</div>
<div class="menu-title">Templates</div>
</a>
<ul>
<li> <a href="templates_dashboard.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?></a>
</li>
<li> <a href="insert_template_xls.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?></a>
</li>
</ul> <?php if ($canAccessTemplates) : ?>
</li> <li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon">
<i class="bx bx-category"></i>
</div>
<div class="menu-title">Templates</div>
</a>
<ul>
<li>
<a href="templates_dashboard.php">
<i class='bx bx-radio-circle'></i><?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?>
</a>
</li>
<li>
<a href="insert_template_xls.php">
<i class='bx bx-radio-circle'></i><?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?>
</a>
</li>
</ul>
</li>
<?php endif; ?>
<li> <li>
<a href="javascript:;" class="has-arrow"> <a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class="bx bx-category"></i> <div class="parent-icon">
<i class="bx bx-category"></i>
</div> </div>
<div class="menu-title">Other Functions</div> <div class="menu-title">Other Functions</div>
</a> </a>
<ul> <ul>
<li> <a href="quotations.php"><i class='bx bx-radio-circle'></i><?php echo $quotationstitle; ?></a> <li>
<a href="quotations.php">
<i class='bx bx-radio-circle'></i><?= htmlspecialchars($quotationstitle, ENT_QUOTES, 'UTF-8'); ?>
</a>
</li> </li>
</ul> </ul>
</li> </li>
<?php if ($canAccessUserAdmin) : ?>
<li class="menu-label">Administration</li>
<li>
<a href="user-admin.php">
<div class="parent-icon">
<i class="bx bx-user-plus"></i>
</div>
<div class="menu-title">User Admin</div>
</a>
</li>
<?php endif; ?>
<li class="menu-label">Others</li> <li class="menu-label">Others</li>
<li> <li>
<a href="https://helpdesk.cesoft.io" target="_blank"> <a href="https://helpdesk.cesoft.io" target="_blank">
<div class="parent-icon"><i class="bx bx-support"></i> <div class="parent-icon">
<i class="bx bx-support"></i>
</div> </div>
<div class="menu-title">Support</div> <div class="menu-title">Support</div>
</a> </a>
</li> </li>
<?php
endif; ?> <?php endif; ?>
<!-- admin, superuser menù -->
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('Superuser'))) : ?>
<?php <!-- admin only menu -->
endif; ?> <?php if ($isAdmin) : ?>
<!-- admin menù -->
<?php if (Auth::user()->hasRole('Admin')) : ?>
<li class="menu-label">Admin Menù</li> <li class="menu-label">Admin Menù</li>
<li> <li>
<a href="../" target="_blank"> <a href="../" target="_blank">
<div class="parent-icon"><i class="bx bx-support"></i> <div class="parent-icon">
<i class="bx bx-support"></i>
</div> </div>
<div class="menu-title">User Management</div> <div class="menu-title">User Management</div>
</a> </a>
</li> </li>
<!-- <li>
<!--
<li>
<a href="template/index.html" target="_blank"> <a href="template/index.html" target="_blank">
<div class="parent-icon"><i class="bx bx-support"></i> <div class="parent-icon">
<i class="bx bx-support"></i>
</div> </div>
<div class="menu-title">Template</div> <div class="menu-title">Template</div>
</a> </a>
</li> </li>
<li> <li>
<a href="https://codervent.com/rocker/documentation/index.html" target="_blank"> <a href="https://codervent.com/rocker/documentation/index.html" target="_blank">
<div class="parent-icon"><i class="bx bx-folder"></i> <div class="parent-icon">
<i class="bx bx-folder"></i>
</div> </div>
<div class="menu-title">Documentation</div> <div class="menu-title">Documentation</div>
</a> </a>
</li> --> </li>
<?php -->
endif; ?>
<?php endif; ?>
</ul> </ul>
<!--end navigation--> <!--end navigation-->
</div> </div>
+8 -2
View File
@@ -94,8 +94,14 @@
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item d-flex align-items-center" href="user-profile.php"><i class="bx bx-user fs-5"></i><span>Profile</span></a> <li><a class="dropdown-item d-flex align-items-center" href="user-profile.php"><i class="bx bx-user fs-5"></i><span>Profile</span></a>
</li> </li>
<li><a class="dropdown-item d-flex align-items-center" href="settings.php"><i class="bx bx-cog fs-5"></i><span>Settings</span></a> <?php if ($user->hasRole('Admin') || $user->hasRole('SuperUser')): ?>
</li> <li>
<a class="dropdown-item d-flex align-items-center" href="user-admin.php">
<i class="bx bx-user-plus fs-5"></i>
<span>User Admin</span>
</a>
</li>
<?php endif; ?>
<li> <li>
<div class="dropdown-divider mb-0"></div> <div class="dropdown-divider mb-0"></div>
</li> </li>
+170 -14
View File
@@ -3,9 +3,20 @@
// Retrieve all routines from database // Retrieve all routines from database
$db = DBHandlerSelect::getInstance(); $db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection(); $pdo = $db->getConnection();
$stmt = $pdo->prepare("SELECT * FROM routine"); $stmt = $pdo->prepare("SELECT * FROM routine");
$stmt->execute(); $stmt->execute();
$routines = $stmt->fetchAll(PDO::FETCH_ASSOC); $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Retrieve active API/JSON configurations
$stmt = $pdo->prepare("
SELECT id, name, provider_code, api_type, php_class_name
FROM api_configurations
WHERE is_active = 1
ORDER BY name ASC
");
$stmt->execute();
$apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
?> ?>
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
@@ -40,7 +51,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<li>Template Name</li> <li>Template Name</li>
<li>Source Type</li> <li>Source Type</li>
<li>Schema and Client</li> <li>Schema and Client</li>
<li>Row Header and Column Header only for XLS templates</li> <li>Row Header, Column Header and Sheet Number only for XLS templates</li>
<li>API / JSON Configuration only for API / JSON templates</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -67,7 +79,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<label class="form-label">Source Type *</label> <label class="form-label">Source Type *</label>
<select name="source_type" id="sourceType" class="form-control" required> <select name="source_type" id="sourceType" class="form-control" required>
<option value="XLS" selected>XLS</option> <option value="XLS" selected>XLS</option>
<option value="API">API</option> <option value="API">API / JSON</option>
<option value="PDF">PDF</option>
</select> </select>
<small class="text-muted">Choose the source used by this template</small> <small class="text-muted">Choose the source used by this template</small>
</div> </div>
@@ -82,6 +95,73 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<input type="text" name="start_column" id="startColumn" class="form-control" value="A" required> <input type="text" name="start_column" id="startColumn" class="form-control" value="A" required>
</div> </div>
<div class="mb-3" id="xlsEndColumnWrapper">
<label class="form-label">Last Column included</label>
<input
type="text"
name="xls_end_column"
id="xlsEndColumn"
class="form-control"
value=""
placeholder="Example: AN">
<small class="text-danger fw-semibold">
Attention: if left empty, the system will read the entire XLS/XLSX sheet.
Dirty Excel files may cause memory errors or timeout.
</small>
</div>
<div class="mb-3" id="xlsSheetNumberWrapper">
<label class="form-label">XLS Sheet Number</label>
<input
type="number"
name="xls_sheet_index"
id="xlsSheetIndex"
class="form-control"
min="0"
value="0">
<small class="text-muted">
Use 0 for the first sheet, 1 for the second sheet, 2 for the third sheet, and so on.
</small>
</div>
<div class="mb-3" id="apiConfigWrapper" style="display: none;">
<label class="form-label">API / JSON Configuration *</label>
<select name="api_config_id" id="apiConfigSelect" class="form-control">
<option value="">Select an API configuration...</option>
<?php foreach ($apiConfigurations as $apiConfig): ?>
<?php
$apiLabelParts = [];
if (!empty($apiConfig['name'])) {
$apiLabelParts[] = $apiConfig['name'];
}
if (!empty($apiConfig['provider_code'])) {
$apiLabelParts[] = '[' . $apiConfig['provider_code'] . ']';
}
if (!empty($apiConfig['api_type'])) {
$apiLabelParts[] = '(' . $apiConfig['api_type'] . ')';
}
if (!empty($apiConfig['php_class_name'])) {
$apiLabelParts[] = '- ' . $apiConfig['php_class_name'];
}
$apiLabel = implode(' ', $apiLabelParts);
?>
<option value="<?php echo (int)$apiConfig['id']; ?>">
<?php echo htmlspecialchars($apiLabel, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
<small class="text-muted">
Select the API/JSON configuration linked to this template.
</small>
</div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label"><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></label> <label class="form-label"><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></label>
<textarea name="description" class="form-control"></textarea> <textarea name="description" class="form-control"></textarea>
@@ -185,10 +265,18 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
const routineAction3 = document.getElementById("routineAction3"); const routineAction3 = document.getElementById("routineAction3");
const sourceType = document.getElementById("sourceType"); const sourceType = document.getElementById("sourceType");
const headerRowWrapper = document.getElementById("headerRowWrapper"); const headerRowWrapper = document.getElementById("headerRowWrapper");
const startColumnWrapper = document.getElementById("startColumnWrapper"); const startColumnWrapper = document.getElementById("startColumnWrapper");
const xlsEndColumnWrapper = document.getElementById("xlsEndColumnWrapper");
const xlsSheetNumberWrapper = document.getElementById("xlsSheetNumberWrapper");
const apiConfigWrapper = document.getElementById("apiConfigWrapper");
const headerRow = document.getElementById("headerRow"); const headerRow = document.getElementById("headerRow");
const startColumn = document.getElementById("startColumn"); const startColumn = document.getElementById("startColumn");
const xlsEndColumn = document.getElementById("xlsEndColumn");
const xlsSheetIndex = document.getElementById("xlsSheetIndex");
const apiConfigSelect = document.getElementById("apiConfigSelect");
if (!form || !clientLoadingStatus || !schemaLoadingStatus || !routineSelect || !routineDetails) { if (!form || !clientLoadingStatus || !schemaLoadingStatus || !routineSelect || !routineDetails) {
alert("Errore: Uno o più elementi della pagina non sono stati trovati. Contatta l'amministratore."); alert("Errore: Uno o più elementi della pagina non sono stati trovati. Contatta l'amministratore.");
@@ -210,27 +298,68 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
allowClear: true allowClear: true
}); });
$('#apiConfigSelect').select2({
placeholder: "Select an API configuration...",
allowClear: true
});
function updateSourceFields() { function updateSourceFields() {
const selectedSource = sourceType.value; const selectedSource = sourceType.value;
if (selectedSource === 'API') { const isXls = selectedSource === 'XLS';
headerRowWrapper.style.opacity = '0.6'; const isApiJson = selectedSource === 'API';
startColumnWrapper.style.opacity = '0.6';
headerRow.required = false; if (isXls) {
startColumn.required = false; headerRowWrapper.style.display = 'block';
startColumnWrapper.style.display = 'block';
headerRow.disabled = true; xlsEndColumnWrapper.style.display = 'block';
startColumn.disabled = true; xlsSheetNumberWrapper.style.display = 'block';
} else {
headerRowWrapper.style.opacity = '1';
startColumnWrapper.style.opacity = '1';
headerRow.required = true; headerRow.required = true;
startColumn.required = true; startColumn.required = true;
xlsSheetIndex.required = true;
// Last Column is optional.
// If empty, the import will read the entire sheet.
xlsEndColumn.required = false;
headerRow.disabled = false; headerRow.disabled = false;
startColumn.disabled = false; startColumn.disabled = false;
xlsEndColumn.disabled = false;
xlsSheetIndex.disabled = false;
apiConfigWrapper.style.display = 'none';
apiConfigSelect.required = false;
apiConfigSelect.disabled = true;
$('#apiConfigSelect').val(null).trigger('change');
} else {
headerRowWrapper.style.display = 'none';
startColumnWrapper.style.display = 'none';
xlsEndColumnWrapper.style.display = 'none';
xlsSheetNumberWrapper.style.display = 'none';
headerRow.required = false;
startColumn.required = false;
xlsEndColumn.required = false;
xlsSheetIndex.required = false;
headerRow.disabled = true;
startColumn.disabled = true;
xlsEndColumn.disabled = true;
xlsSheetIndex.disabled = true;
xlsEndColumn.value = '';
if (isApiJson) {
apiConfigWrapper.style.display = 'block';
apiConfigSelect.required = true;
apiConfigSelect.disabled = false;
} else {
apiConfigWrapper.style.display = 'none';
apiConfigSelect.required = false;
apiConfigSelect.disabled = true;
$('#apiConfigSelect').val(null).trigger('change');
}
} }
} }
@@ -261,7 +390,12 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
data.value.forEach(client => { data.value.forEach(client => {
const nome = client.Nominativo || "Nome non disponibile"; const nome = client.Nominativo || "Nome non disponibile";
const id = client.IdCliente || "ID non disponibile"; const id = client.IdCliente || "ID non disponibile";
const option = new Option(`${nome.trim()} (ID: ${id})`, id);
const codiceCliente = (client.CodiceCliente ?? client.codiceCliente ?? "").toString().trim();
const suffix = (codiceCliente.split("_")[1] || "").trim();
const shortCode = suffix || (codiceCliente ? codiceCliente.charAt(0) : "--");
const option = new Option(`${nome.trim()} - ${shortCode} (ID: ${id})`, id);
select.add(option); select.add(option);
}); });
@@ -388,6 +522,28 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
let formData = new FormData(this); let formData = new FormData(this);
const selectedSource = sourceType.value;
if (selectedSource === 'XLS' && xlsSheetIndex.value === '') {
Swal.fire({
title: "Errore!",
text: "Inserisci il numero del foglio XLS.",
icon: "error",
confirmButtonText: "OK"
});
return;
}
if (selectedSource === 'API' && !apiConfigSelect.value) {
Swal.fire({
title: "Errore!",
text: "Seleziona una configurazione API / JSON.",
icon: "error",
confirmButtonText: "OK"
});
return;
}
const clientSelect = document.getElementById("clientSelect"); const clientSelect = document.getElementById("clientSelect");
const clientId = clientSelect.value; const clientId = clientSelect.value;
const selectedClientOption = clientSelect.options[clientSelect.selectedIndex]; const selectedClientOption = clientSelect.options[clientSelect.selectedIndex];
+497 -267
View File
@@ -15,6 +15,7 @@ $stmt = $pdo->prepare("
start_column, start_column,
target_table, target_table,
source_type, source_type,
xls_sheet_index,
sample_xlsx, sample_xlsx,
idclient, idclient,
clientname, clientname,
@@ -39,6 +40,14 @@ if (!in_array($sourceType, ['XLS', 'API', 'PDF'], true)) {
$sourceType = 'XLS'; $sourceType = 'XLS';
} }
$xlsSheetIndex = isset($template['xls_sheet_index']) && $template['xls_sheet_index'] !== null
? (int)$template['xls_sheet_index']
: 0;
if ($xlsSheetIndex < 0) {
$xlsSheetIndex = 0;
}
$clientName = $template['clientname'] ?: ''; $clientName = $template['clientname'] ?: '';
$schemaName = $template['schemaname'] ?: ''; $schemaName = $template['schemaname'] ?: '';
$schemajson = $template['schemajson'] ? json_decode($template['schemajson'], true) : []; $schemajson = $template['schemajson'] ? json_decode($template['schemajson'], true) : [];
@@ -49,6 +58,7 @@ $stmt = $pdo->prepare("
SELECT SELECT
id, id,
field_id, field_id,
field_order,
excel_column, excel_column,
json_node, json_node,
is_manual, is_manual,
@@ -70,6 +80,7 @@ $stmt = $pdo->prepare("
is_visible_parts is_visible_parts
FROM template_mapping FROM template_mapping
WHERE template_id = ? WHERE template_id = ?
ORDER BY field_order ASC, id ASC
"); ");
$stmt->execute([$id]); $stmt->execute([$id]);
$mappings = $stmt->fetchAll(PDO::FETCH_ASSOC); $mappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -196,28 +207,37 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
} }
/* Type */ /* Type */
/* Order */
#schemaFieldsTable th:nth-child(5), #schemaFieldsTable th:nth-child(5),
#schemaFieldsTable td:nth-child(5) { #schemaFieldsTable td:nth-child(5) {
width: 70px;
text-align: center;
white-space: nowrap;
}
/* Type */
#schemaFieldsTable th:nth-child(6),
#schemaFieldsTable td:nth-child(6) {
width: 120px; width: 120px;
white-space: nowrap; white-space: nowrap;
} }
/* Mapping = wide but NOT insane */ /* Mapping = wide but NOT insane */
#schemaFieldsTable th:nth-child(6), #schemaFieldsTable th:nth-child(7),
#schemaFieldsTable td:nth-child(6) { #schemaFieldsTable td:nth-child(7) {
width: 380px; width: 380px;
} }
/* Default Value = wider */ /* Default Value = wider */
#schemaFieldsTable th:nth-child(7), #schemaFieldsTable th:nth-child(8),
#schemaFieldsTable td:nth-child(7) { #schemaFieldsTable td:nth-child(8) {
width: 320px; width: 320px;
} }
/* selects fill the cell */ /* selects fill the cell */
#schemaFieldsTable td:nth-child(6) .form-select, #schemaFieldsTable td:nth-child(7) .form-select,
#schemaFieldsTable td:nth-child(7) .form-control, #schemaFieldsTable td:nth-child(8) .form-control,
#schemaFieldsTable td:nth-child(7) .form-select { #schemaFieldsTable td:nth-child(8) .form-select {
width: 100% !important; width: 100% !important;
} }
@@ -245,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>
@@ -267,7 +328,8 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
Source: <strong><?php echo htmlspecialchars($sourceType); ?></strong> Source: <strong><?php echo htmlspecialchars($sourceType); ?></strong>
<?php if ($sourceType === 'XLS'): ?> <?php if ($sourceType === 'XLS'): ?>
| |
Header Row: <span id="headerRow"><?php echo $template['header_row']; ?></span> | Sheet Number: <span id="xlsSheetIndex"><?php echo (int)$xlsSheetIndex; ?></span> |
Header Row: <span id="headerRow"><?php echo (int)$template['header_row']; ?></span> |
Start Column: <span id="startColumn"><?php echo htmlspecialchars($template['start_column']); ?></span> Start Column: <span id="startColumn"><?php echo htmlspecialchars($template['start_column']); ?></span>
<?php endif; ?> <?php endif; ?>
</p> </p>
@@ -337,6 +399,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
<th style="width:45px; text-align:center;">Import</th> <th style="width:45px; text-align:center;">Import</th>
<th style="width:45px; text-align:center;">Parts</th> <th style="width:45px; text-align:center;">Parts</th>
<th style="width:320px;">Title</th> <th style="width:320px;">Title</th>
<th style="width:70px; text-align:center;">Order</th>
<th style="width:120px;">Type</th> <th style="width:120px;">Type</th>
<th><?php echo $sourceType === 'API' ? 'JSON Mapping' : 'Mapping'; ?></th> <th><?php echo $sourceType === 'API' ? 'JSON Mapping' : 'Mapping'; ?></th>
<th>Default Value</th> <th>Default Value</th>
@@ -373,6 +436,12 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
<?php endif; ?> <?php endif; ?>
</td> </td>
<td class="text-center">
<?php echo (int)($mapping['field_order'] ?? 9999); ?>
</td>
<td><?php echo htmlspecialchars($mapping['data_type'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($mapping['data_type'] ?? 'N/A'); ?></td> <td><?php echo htmlspecialchars($mapping['data_type'] ?? 'N/A'); ?></td>
<td> <td>
@@ -656,7 +725,30 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
let workbook = XLSX.read(data, { let workbook = XLSX.read(data, {
type: 'array' type: 'array'
}); });
let sheet = workbook.Sheets[workbook.SheetNames[0]];
const selectedSheetIndex = <?php echo (int)$xlsSheetIndex; ?>;
if (!workbook.SheetNames || workbook.SheetNames.length === 0) {
alert("No sheets found in this XLS file.");
return;
}
if (!workbook.SheetNames[selectedSheetIndex]) {
alert(
"The selected sheet number " + selectedSheetIndex +
" does not exist in this XLS file. Available sheets: " +
workbook.SheetNames.map((name, index) => index + " = " + name).join(", ")
);
return;
}
const selectedSheetName = workbook.SheetNames[selectedSheetIndex];
let sheet = workbook.Sheets[selectedSheetName];
console.log("Selected XLS sheet:", {
index: selectedSheetIndex,
name: selectedSheetName
});
// Read sheet range to determine column offset // Read sheet range to determine column offset
const sheetRange = XLSX.utils.decode_range(sheet['!ref'] || 'A1'); const sheetRange = XLSX.utils.decode_range(sheet['!ref'] || 'A1');
@@ -745,7 +837,8 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
const uniqueLabels = [...new Set(knownLabels)]; const uniqueLabels = [...new Set(knownLabels)];
console.group('🔍 Auto-detect header row'); console.group('🔍 Auto-detect header row');
console.log('Sheet name:', workbook.SheetNames[0]); console.log('Sheet index:', selectedSheetIndex);
console.log('Sheet name:', selectedSheetName);
console.log('Total rows in sheet:', sheetData.length); console.log('Total rows in sheet:', sheetData.length);
console.log('Labels from schema field titles:', knownLabels); console.log('Labels from schema field titles:', knownLabels);
console.log('Unique labels to match against:', uniqueLabels); console.log('Unique labels to match against:', uniqueLabels);
@@ -884,8 +977,10 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
function saveXlsHeaders(headers, headerRow, startColumn) { function saveXlsHeaders(headers, headerRow, startColumn) {
const payload = { const payload = {
template_id: <?php echo $id; ?>, template_id: <?php echo $id; ?>,
xls_headers: JSON.stringify(headers) xls_headers: JSON.stringify(headers),
xls_sheet_index: <?php echo (int)$xlsSheetIndex; ?>
}; };
if (headerRow !== undefined) payload.header_row = headerRow; if (headerRow !== undefined) payload.header_row = headerRow;
if (startColumn !== undefined) payload.start_column = startColumn; if (startColumn !== undefined) payload.start_column = startColumn;
@@ -897,8 +992,18 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
body: JSON.stringify(payload) body: JSON.stringify(payload)
}).then(response => response.json()) }).then(response => response.json())
.then(data => { .then(data => {
if (!data.success) console.error("❌ Error saving XLS headers:", data.message); if (!data.success) {
else console.log("✅ Saved headers, header_row:", headerRow, "start_column:", startColumn); console.error("❌ Error saving XLS headers:", data.message);
} else {
console.log(
"✅ Saved headers, header_row:",
headerRow,
"start_column:",
startColumn,
"xls_sheet_index:",
<?php echo (int)$xlsSheetIndex; ?>
);
}
}) })
.catch(error => console.error("❌ Fetch error:", error)); .catch(error => console.error("❌ Fetch error:", error));
} }
@@ -954,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 = [];
@@ -972,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);
} }
}); });
@@ -997,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',
@@ -1399,218 +1658,221 @@ $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')) { if (!event.target.classList.contains('mapping-select')) return;
let tr = event.target.closest('tr');
let mappingId = event.target.getAttribute('data-id');
let xlsSelect = tr.querySelector('.xls-columns'); const mappingSelect = event.target;
let jsonSelect = tr.querySelector('.json-nodes'); const tr = mappingSelect.closest('tr');
let manualInput = tr.querySelector('.manual-default'); const mappingId = mappingSelect.getAttribute('data-id');
let autoSelect = tr.querySelector('.auto-value-select');
let mappedColumn = tr.querySelector('.mapped-column'); const xlsSelect = tr.querySelector('.xls-columns');
let mappedJsonNode = tr.querySelector('.mapped-json-node'); const jsonSelect = tr.querySelector('.json-nodes');
const manualInput = tr.querySelector('.manual-default');
const autoSelect = tr.querySelector('.auto-value-select');
let removeBtn = tr.querySelector('.remove-xls'); const mappedColumn = tr.querySelector('.mapped-column');
let removeJsonBtn = tr.querySelector('.remove-json'); const mappedJsonNode = tr.querySelector('.mapped-json-node');
if (event.target.value === 'xls') { const removeBtn = tr.querySelector('.remove-xls');
if (xlsSelect) xlsSelect.style.display = 'block'; const removeJsonBtn = tr.querySelector('.remove-json');
if (jsonSelect) jsonSelect.style.display = 'none';
if (autoSelect) autoSelect.style.display = 'none';
if (manualInput) { function destroyJsonSelect2() {
manualInput.style.display = 'none'; if (jsonSelect && window.jQuery && $(jsonSelect).hasClass('select2-hidden-accessible')) {
manualInput.value = ''; $(jsonSelect).select2('destroy');
}
if (mappedColumn) mappedColumn.style.display = 'none';
if (mappedJsonNode) mappedJsonNode.style.display = 'none';
if (removeBtn) removeBtn.style.display = xlsSelect && xlsSelect.value ? 'inline-block' : 'none';
if (removeJsonBtn) removeJsonBtn.style.display = 'none';
} else if (event.target.value === 'json') {
if (xlsSelect) xlsSelect.style.display = 'none';
if (jsonSelect) jsonSelect.style.display = 'block';
if (autoSelect) autoSelect.style.display = 'none';
if (manualInput) {
manualInput.style.display = 'none';
manualInput.value = '';
}
if (mappedColumn) mappedColumn.style.display = 'none';
if (mappedJsonNode) mappedJsonNode.style.display = 'none';
if (removeBtn) removeBtn.style.display = 'none';
if (removeJsonBtn) removeJsonBtn.style.display = jsonSelect && jsonSelect.value ? 'inline-block' : 'none';
} else if (event.target.value === 'manual') {
if (xlsSelect) xlsSelect.style.display = 'none';
if (jsonSelect) jsonSelect.style.display = 'none';
if (autoSelect) autoSelect.style.display = 'none';
if (manualInput) {
manualInput.style.display = 'block';
}
if (mappedColumn) mappedColumn.style.display = 'none';
if (mappedJsonNode) mappedJsonNode.style.display = 'none';
if (removeBtn) removeBtn.style.display = 'none';
if (removeJsonBtn) removeJsonBtn.style.display = 'none';
} else if (event.target.value === 'auto') {
if (xlsSelect) {
xlsSelect.style.display = 'none';
xlsSelect.value = '';
}
if (jsonSelect) {
jsonSelect.style.display = 'none';
jsonSelect.value = '';
}
if (manualInput) {
manualInput.style.display = 'none';
manualInput.value = '';
}
if (mappedColumn) mappedColumn.style.display = 'none';
if (mappedJsonNode) mappedJsonNode.style.display = 'none';
if (removeBtn) removeBtn.style.display = 'none';
if (removeJsonBtn) removeJsonBtn.style.display = 'none';
if (autoSelect) autoSelect.style.display = 'block';
} else {
if (xlsSelect) xlsSelect.style.display = 'none';
if (jsonSelect) jsonSelect.style.display = 'none';
if (autoSelect) autoSelect.style.display = 'none';
if (manualInput) {
manualInput.style.display = 'none';
manualInput.value = '';
}
if (mappedColumn) mappedColumn.style.display = 'none';
if (mappedJsonNode) mappedJsonNode.style.display = 'none';
if (removeBtn) removeBtn.style.display = 'none';
if (removeJsonBtn) removeJsonBtn.style.display = 'none';
} }
saveMapping(
mappingId,
event.target.value,
manualInput ? manualInput.value : '',
xlsSelect ? xlsSelect.value : null,
autoSelect ? autoSelect.value : null,
jsonSelect ? jsonSelect.value : null
);
if (sourceType === 'XLS') updateXlsDropdowns();
if (sourceType === 'API') updateJsonDropdowns();
} else if (event.target.classList.contains('main-field-checkbox')) {
const checkbox = event.target;
const mappingId = checkbox.dataset.mappingId;
const value = checkbox.checked ? 1 : 0;
// Se checked, deseleziona tutti gli altri visivamente
if (checkbox.checked) {
document.querySelectorAll('.main-field-checkbox').forEach(cb => {
if (cb !== checkbox) cb.checked = false;
});
}
// Salva l'aggiornamento
fetch('update_main_field.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
template_id: <?php echo $id; ?>,
mapping_id: mappingId,
value: value
})
}).then(response => response.json())
.then(data => {
if (!data.success) {
console.error("❌ Error updating main_field:", data.message);
checkbox.checked = !checkbox.checked;
document.querySelectorAll('.main-field-checkbox').forEach(cb => {
cb.checked = cb.dataset.originalChecked === 'true';
});
} else {
document.querySelectorAll('.main-field-checkbox').forEach(cb => {
cb.dataset.originalChecked = cb.checked;
});
}
})
.catch(error => {
console.error("❌ Fetch error:", error);
checkbox.checked = !checkbox.checked;
document.querySelectorAll('.main-field-checkbox').forEach(cb => {
cb.checked = cb.dataset.originalChecked === 'true';
});
});
} else if (event.target.classList.contains('visible-parts-checkbox')) {
const checkbox = event.target;
const mappingId = checkbox.dataset.mappingId;
const value = checkbox.checked ? 1 : 0;
// salva stato per rollback
const prevChecked = checkbox.checked;
// ✅ UI: se sto mettendo a 1, tolgo la spunta a tutti gli altri SUBITO
if (value === 1) {
document.querySelectorAll('.visible-parts-checkbox').forEach(cb => {
if (cb !== checkbox) cb.checked = false;
});
}
fetch('update_visible_parts.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_parts:", data.message);
// rollback UI
checkbox.checked = !prevChecked;
// se avevo tolto le spunte agli altri, ricarico per riallineare la UI al DB
// (semplice e safe)
location.reload();
}
})
.catch(error => {
console.error("❌ Fetch error:", error);
// rollback UI
checkbox.checked = !prevChecked;
location.reload();
});
} }
if (mappingSelect.value === 'xls') {
if (xlsSelect) xlsSelect.style.display = 'block';
destroyJsonSelect2();
if (jsonSelect) {
jsonSelect.style.display = 'none';
jsonSelect.value = '';
}
if (autoSelect) autoSelect.style.display = 'none';
if (manualInput) {
manualInput.style.display = 'none';
manualInput.value = '';
}
if (mappedColumn) mappedColumn.style.display = 'none';
if (mappedJsonNode) mappedJsonNode.style.display = 'none';
if (removeBtn) removeBtn.style.display = xlsSelect && xlsSelect.value ? 'inline-block' : 'none';
if (removeJsonBtn) removeJsonBtn.style.display = 'none';
} else if (mappingSelect.value === 'json') {
if (xlsSelect) {
xlsSelect.style.display = 'none';
xlsSelect.value = '';
}
if (jsonSelect) {
jsonSelect.style.display = 'block';
}
if (autoSelect) autoSelect.style.display = 'none';
if (manualInput) {
manualInput.style.display = 'none';
manualInput.value = '';
}
if (mappedColumn) mappedColumn.style.display = 'none';
if (mappedJsonNode) mappedJsonNode.style.display = 'none';
if (removeBtn) removeBtn.style.display = 'none';
if (removeJsonBtn) removeJsonBtn.style.display = jsonSelect && jsonSelect.value ? 'inline-block' : 'none';
updateJsonDropdowns();
} else if (mappingSelect.value === 'manual') {
if (xlsSelect) xlsSelect.style.display = 'none';
destroyJsonSelect2();
if (jsonSelect) {
jsonSelect.style.display = 'none';
jsonSelect.value = '';
}
if (autoSelect) autoSelect.style.display = 'none';
if (manualInput) {
manualInput.style.display = 'block';
}
if (mappedColumn) mappedColumn.style.display = 'none';
if (mappedJsonNode) mappedJsonNode.style.display = 'none';
if (removeBtn) removeBtn.style.display = 'none';
if (removeJsonBtn) removeJsonBtn.style.display = 'none';
} else if (mappingSelect.value === 'auto') {
if (xlsSelect) {
xlsSelect.style.display = 'none';
xlsSelect.value = '';
}
destroyJsonSelect2();
if (jsonSelect) {
jsonSelect.style.display = 'none';
jsonSelect.value = '';
}
if (manualInput) {
manualInput.style.display = 'none';
manualInput.value = '';
}
if (mappedColumn) mappedColumn.style.display = 'none';
if (mappedJsonNode) mappedJsonNode.style.display = 'none';
if (removeBtn) removeBtn.style.display = 'none';
if (removeJsonBtn) removeJsonBtn.style.display = 'none';
if (autoSelect) autoSelect.style.display = 'block';
} else {
if (xlsSelect) xlsSelect.style.display = 'none';
destroyJsonSelect2();
if (jsonSelect) {
jsonSelect.style.display = 'none';
jsonSelect.value = '';
}
if (autoSelect) autoSelect.style.display = 'none';
if (manualInput) {
manualInput.style.display = 'none';
manualInput.value = '';
}
if (mappedColumn) mappedColumn.style.display = 'none';
if (mappedJsonNode) mappedJsonNode.style.display = 'none';
if (removeBtn) removeBtn.style.display = 'none';
if (removeJsonBtn) removeJsonBtn.style.display = 'none';
}
saveMapping(
mappingId,
mappingSelect.value,
manualInput ? manualInput.value : '',
xlsSelect ? xlsSelect.value : null,
autoSelect ? autoSelect.value : null,
jsonSelect ? jsonSelect.value : null
);
if (sourceType === 'XLS') updateXlsDropdowns();
}); });
// Salva lo stato originale dei checkbox al caricamento function saveJsonNodeSelection(jsonSelect) {
document.querySelectorAll('.main-field-checkbox').forEach(cb => { if (!jsonSelect) return;
cb.dataset.originalChecked = cb.checked;
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;
}
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 = jsonSelect.value ? `(${jsonSelect.value})` : '';
mappedJsonNode.style.display = jsonSelect.value ? 'inline' : 'none';
removeJsonBtn.style.display = jsonSelect.value ? 'inline-block' : 'none';
console.log('[JSON NODE SAVE]', {
mappingId: mappingId,
value: jsonSelect.value
});
saveMapping(
mappingId,
'json',
manualInput ? manualInput.value : '',
null,
null,
jsonSelect.value
);
}
document.getElementById('schemaFieldsBody').addEventListener('change', function(event) {
if (!event.target.classList.contains('json-nodes')) return;
saveJsonNodeSelection(event.target);
});
if (window.jQuery) {
$(document).on('select2:select select2:clear', 'select.json-nodes', function() {
saveJsonNodeSelection(this);
});
}
// Save original Main checkbox state
document.querySelectorAll('#schemaFieldsBody .main-field-checkbox').forEach(cb => {
cb.dataset.originalChecked = cb.checked ? 'true' : 'false';
}); });
// AUTO VALUE select change -> save auto_value // AUTO VALUE select change -> save auto_value
@@ -1647,7 +1909,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
mappedColumn = document.createElement('span'); mappedColumn = document.createElement('span');
mappedColumn.className = 'mapped-column'; mappedColumn.className = 'mapped-column';
mappedColumn.style.marginLeft = '5px'; mappedColumn.style.marginLeft = '5px';
tr.querySelector('td:nth-child(6)').appendChild(mappedColumn); tr.querySelector('td:nth-child(7)').appendChild(mappedColumn);
} }
if (!removeBtn) { if (!removeBtn) {
removeBtn = document.createElement('button'); removeBtn = document.createElement('button');
@@ -1655,7 +1917,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
removeBtn.textContent = 'X'; removeBtn.textContent = 'X';
removeBtn.style.marginLeft = '5px'; removeBtn.style.marginLeft = '5px';
removeBtn.setAttribute('data-id', mappingId); removeBtn.setAttribute('data-id', mappingId);
tr.querySelector('td:nth-child(6)').appendChild(removeBtn); tr.querySelector('td:nth-child(7)').appendChild(removeBtn);
removeBtn.addEventListener('click', function(e) { removeBtn.addEventListener('click', function(e) {
let tr = e.target.closest('tr'); let tr = e.target.closest('tr');
@@ -1689,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(6)').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(6)').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') {
@@ -1860,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();
} }
}) })
@@ -2097,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;">
+23 -2
View File
@@ -2,7 +2,24 @@
<div class="modal-dialog modal-xl" style="max-width: 95vw !important;"> <div class="modal-dialog modal-xl" style="max-width: 95vw !important;">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="partsModalLabel">Parti per TRF: <span id="trfHeader"></span></h5> <h5 class="modal-title" id="partsModalLabel">
Parti per TRF:
<span id="trfHeader"></span>
</h5>
<div class="ms-auto me-3 d-flex align-items-center gap-2">
<button type="button" class="btn btn-outline-secondary btn-sm" id="prevPartsRecordBtn" title="Record precedente">
<i class="fas fa-chevron-left"></i>
</button>
<span id="partsRecordCounter" class="text-muted" style="font-size: 12px; min-width: 70px; text-align: center;">
-
</span>
<button type="button" class="btn btn-outline-secondary btn-sm" id="nextPartsRecordBtn" title="Record successivo">
<i class="fas fa-chevron-right"></i>
</button>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -346,7 +363,11 @@
border: 1px solid #aaa !important; border: 1px solid #aaa !important;
border-radius: 4px !important; border-radius: 4px !important;
background: #fff !important; background: #fff !important;
max-height: 200px !important; overflow: visible !important;
}
.select2-container--open .select2-results__options {
max-height: 220px !important;
overflow-y: auto !important; overflow-y: auto !important;
} }
+163 -3
View File
@@ -9,6 +9,7 @@ $(document).ready(function () {
let quotations = []; let quotations = [];
let partsExtraField = null; // {field_id, field_label} oppure null let partsExtraField = null; // {field_id, field_label} oppure null
let extraFieldOptions = []; // [{id,label}] let extraFieldOptions = []; // [{id,label}]
let isLoadingPartsRecord = false;
// --- ROW ID helpers: niente più cache impazzita di jQuery .data() --- // --- ROW ID helpers: niente più cache impazzita di jQuery .data() ---
function getPartId($row) { function getPartId($row) {
@@ -509,21 +510,175 @@ $(document).ready(function () {
// MODAL HANDLING // MODAL HANDLING
// =================== // ===================
function loadParts(iddatadb, idquotations, callback = null) { function loadParts(iddatadb, idquotations, callback = null) {
isLoadingPartsRecord = true;
unsavedChanges = false;
// Store current modal context
$("#partsModal").data("iddatadb", iddatadb || null);
$("#partsModal").data("idquotations", idquotations || null);
// Store the visible record list from the main grid
if (Array.isArray(window.visibleIddatadbList)) {
$("#partsModal").data(
"visible-iddatadb-list",
window.visibleIddatadbList,
);
}
updatePartsRecordHeader(iddatadb);
const finishLoading = function () {
unsavedChanges = false;
isLoadingPartsRecord = false;
if (callback) callback();
};
if (iddatadb) { if (iddatadb) {
loadMacroMatrici(); loadMacroMatrici();
initializeGlobalSelect2(); initializeGlobalSelect2();
loadPartsExtraField(iddatadb, function () { loadPartsExtraField(iddatadb, function () {
loadPhoto(iddatadb, idquotations); loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations, callback); loadExistingParts(iddatadb, idquotations, finishLoading);
}); });
} else { } else {
loadPartsExtraField(iddatadb, function () { loadPartsExtraField(iddatadb, function () {
loadPhoto(iddatadb, idquotations); loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations, callback); loadExistingParts(iddatadb, idquotations, finishLoading);
}); });
} }
} }
// ===================
// PARTS MODAL RECORD NAVIGATION
// ===================
function getVisiblePartsRecordList() {
const listFromModal = $("#partsModal").data("visible-iddatadb-list");
if (Array.isArray(listFromModal) && listFromModal.length > 0) {
return listFromModal.map((v) => parseInt(v, 10)).filter(Boolean);
}
if (
Array.isArray(window.visibleIddatadbList) &&
window.visibleIddatadbList.length > 0
) {
return window.visibleIddatadbList
.map((v) => parseInt(v, 10))
.filter(Boolean);
}
if (Array.isArray(window.gridData) && window.gridData.length > 0) {
return window.gridData
.map((row) => parseInt(row.iddatadb, 10))
.filter(Boolean);
}
return [];
}
function getGridRecordById(iddatadb) {
if (!Array.isArray(window.gridData)) return null;
return (
window.gridData.find((row) => {
return parseInt(row.iddatadb, 10) === parseInt(iddatadb, 10);
}) || null
);
}
function getRecordHeaderLabel(iddatadb) {
const record = getGridRecordById(iddatadb);
if (!record) {
return iddatadb ? "#" + iddatadb : "";
}
// Prefer main field value if available
if (record.mainFieldValue) {
return record.mainFieldValue;
}
// Fallbacks
if (record.importreferencecode) {
return record.importreferencecode;
}
if (record.filename_import) {
return record.filename_import;
}
return "#" + iddatadb;
}
function updatePartsRecordHeader(iddatadb) {
const list = getVisiblePartsRecordList();
const currentId = parseInt(iddatadb, 10);
const currentIndex = list.indexOf(currentId);
$("#trfHeader").text(getRecordHeaderLabel(currentId));
if (list.length <= 1 || currentIndex === -1) {
$("#partsRecordCounter").text("-");
$("#prevPartsRecordBtn").prop("disabled", true);
$("#nextPartsRecordBtn").prop("disabled", true);
return;
}
$("#partsRecordCounter").text(currentIndex + 1 + " / " + list.length);
$("#prevPartsRecordBtn").prop("disabled", currentIndex <= 0);
$("#nextPartsRecordBtn").prop(
"disabled",
currentIndex >= list.length - 1,
);
}
function goToAdjacentPartsRecord(direction) {
const list = getVisiblePartsRecordList();
const currentId = parseInt($("#partsModal").data("iddatadb"), 10);
const currentIndex = list.indexOf(currentId);
if (currentIndex === -1) return;
const nextIndex = currentIndex + direction;
if (nextIndex < 0 || nextIndex >= list.length) return;
if (
!isLoadingPartsRecord &&
unsavedChanges &&
!confirm(
"Hai modifiche non salvate. Vuoi cambiare record senza salvare?",
)
) {
return;
}
const nextIddatadb = list[nextIndex];
// Reset local modal state before loading the next record
partMatrice = {};
unsavedChanges = false;
$("#partsTableBody").empty();
$("#photoSelectorContainer").empty().hide();
$("#samplePhoto").attr("src", "");
$(".temp-alert").remove();
loadParts(nextIddatadb, null);
}
$(document).on("click", "#prevPartsRecordBtn", function (e) {
e.preventDefault();
goToAdjacentPartsRecord(-1);
});
$(document).on("click", "#nextPartsRecordBtn", function (e) {
e.preventDefault();
goToAdjacentPartsRecord(1);
});
// EVENTO PER APRIRE IL SECONDO MODALE // EVENTO PER APRIRE IL SECONDO MODALE
$(document).on("click", "#openAnnotationsBtn", function () { $(document).on("click", "#openAnnotationsBtn", function () {
console.log("Clic su Apri Annotazioni..."); console.log("Clic su Apri Annotazioni...");
@@ -1090,7 +1245,10 @@ $(document).ready(function () {
initializeExtraFieldSelect2($newRow); initializeExtraFieldSelect2($newRow);
updateRowButtons(); updateRowButtons();
markUnsaved();
if (!isLoadingPartsRecord) {
markUnsaved();
}
} }
// =================== // ===================
@@ -2142,6 +2300,8 @@ $(document).ready(function () {
}); });
function markUnsaved() { function markUnsaved() {
if (isLoadingPartsRecord) return;
if (!unsavedChanges) { if (!unsavedChanges) {
unsavedChanges = true; unsavedChanges = true;
} }
+109 -4
View File
@@ -4,6 +4,35 @@ require_once 'class/db-functions.php';
$response = ["success" => false, "message" => ""]; $response = ["success" => false, "message" => ""];
function excelColumnToIndex($column)
{
$column = strtoupper(trim((string)$column));
if ($column === '') {
return null;
}
// Numeric column index, example: 40
if (ctype_digit($column)) {
$index = (int)$column;
return $index > 0 ? $index : null;
}
// Excel column letters, example: A, AN, XFC
if (!preg_match('/^[A-Z]+$/', $column)) {
return null;
}
$index = 0;
$length = strlen($column);
for ($i = 0; $i < $length; $i++) {
$index = ($index * 26) + (ord($column[$i]) - ord('A') + 1);
}
return $index;
}
try { try {
if ($_SERVER["REQUEST_METHOD"] !== "POST") { if ($_SERVER["REQUEST_METHOD"] !== "POST") {
throw new Exception("Invalid request method."); throw new Exception("Invalid request method.");
@@ -13,8 +42,23 @@ try {
$id = intval($_POST['id'] ?? 0); $id = intval($_POST['id'] ?? 0);
$name = trim($_POST['name'] ?? ''); $name = trim($_POST['name'] ?? '');
$source_type = strtoupper(trim($_POST['source_type'] ?? 'XLS')); $source_type = strtoupper(trim($_POST['source_type'] ?? 'XLS'));
$header_row = isset($_POST['header_row']) && $_POST['header_row'] !== '' ? intval($_POST['header_row']) : null;
$header_row = isset($_POST['header_row']) && $_POST['header_row'] !== ''
? intval($_POST['header_row'])
: null;
$start_column = trim($_POST['start_column'] ?? ''); $start_column = trim($_POST['start_column'] ?? '');
$xls_end_column = strtoupper(trim($_POST['xls_end_column'] ?? ''));
$xls_end_column = $xls_end_column !== '' ? $xls_end_column : null;
$xls_sheet_index = isset($_POST['xls_sheet_index']) && $_POST['xls_sheet_index'] !== ''
? intval($_POST['xls_sheet_index'])
: 0;
$api_config_id = isset($_POST['api_config_id']) && $_POST['api_config_id'] !== ''
? intval($_POST['api_config_id'])
: null;
$description = trim($_POST['description'] ?? ''); $description = trim($_POST['description'] ?? '');
$target_table = trim($_POST['target_table'] ?? 'datadb'); $target_table = trim($_POST['target_table'] ?? 'datadb');
$idclient = intval($_POST['client_id'] ?? 0); $idclient = intval($_POST['client_id'] ?? 0);
@@ -27,7 +71,8 @@ try {
$button_text_color = trim($_POST['button_text_color'] ?? '#ffffff'); $button_text_color = trim($_POST['button_text_color'] ?? '#ffffff');
$button_label = trim($_POST['button_label'] ?? 'Click Me'); $button_label = trim($_POST['button_label'] ?? 'Click Me');
if (!in_array($source_type, ['XLS', 'API'], true)) { // Allowed source types
if (!in_array($source_type, ['XLS', 'API', 'JSON', 'PDF'], true)) {
$source_type = 'XLS'; $source_type = 'XLS';
} }
@@ -41,18 +86,72 @@ try {
if ($header_row === null || $header_row <= 0 || $start_column === '') { if ($header_row === null || $header_row <= 0 || $start_column === '') {
throw new Exception("Header Row and Start Column are required for XLS templates."); throw new Exception("Header Row and Start Column are required for XLS templates.");
} }
if ($xls_sheet_index < 0) {
throw new Exception("XLS Sheet Number cannot be negative.");
}
$startColumnIndex = excelColumnToIndex($start_column);
if ($startColumnIndex === null) {
throw new Exception("Start Column is not valid. Use Excel column letters like A, AN or a positive number.");
}
if ($xls_end_column !== null) {
$endColumnIndex = excelColumnToIndex($xls_end_column);
if ($endColumnIndex === null) {
throw new Exception("Last Column is not valid. Use Excel column letters like AN or a positive number.");
}
if ($endColumnIndex < $startColumnIndex) {
throw new Exception("Last Column cannot be before Start Column.");
}
}
$api_config_id = null;
} }
// API templates do not require XLS coordinates // API/JSON validation
if ($source_type === 'API') { if ($source_type === 'API' || $source_type === 'JSON') {
if (empty($api_config_id)) {
throw new Exception("API/JSON configuration is required for API or JSON templates.");
}
$header_row = null; $header_row = null;
$start_column = null; $start_column = null;
$xls_end_column = null;
$xls_sheet_index = null;
}
// PDF currently does not require XLS coordinates or API configuration
if ($source_type === 'PDF') {
$header_row = null;
$start_column = null;
$xls_end_column = null;
$xls_sheet_index = null;
$api_config_id = null;
} }
// Database connection // Database connection
$db = DBHandlerSelect::getInstance(); $db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection(); $pdo = $db->getConnection();
// Optional check: verify API configuration exists and is active
if ($api_config_id !== null) {
$stmt = $pdo->prepare("
SELECT COUNT(*)
FROM api_configurations
WHERE id = ?
AND is_active = 1
");
$stmt->execute([$api_config_id]);
if ((int)$stmt->fetchColumn() === 0) {
throw new Exception("Selected API/JSON configuration does not exist or is not active.");
}
}
// Update template // Update template
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
UPDATE excel_templates UPDATE excel_templates
@@ -61,6 +160,9 @@ try {
source_type = ?, source_type = ?,
header_row = ?, header_row = ?,
start_column = ?, start_column = ?,
xls_end_column = ?,
xls_sheet_index = ?,
api_config_id = ?,
description = ?, description = ?,
target_table = ?, target_table = ?,
idclient = ?, idclient = ?,
@@ -81,6 +183,9 @@ try {
$source_type, $source_type,
$header_row, $header_row,
$start_column, $start_column,
$xls_end_column,
$xls_sheet_index,
$api_config_id,
$description, $description,
$target_table, $target_table,
$idclient, $idclient,
+237 -32
View File
@@ -11,17 +11,120 @@ session_start();
require_once '../../vendor/autoload.php'; require_once '../../vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php'; require_once __DIR__ . '/class/db-functions.php';
$response = ['error' => '', 'rows' => [], 'columns' => [], 'template_id' => 0, 'filename' => '', 'apply_routine' => false]; use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
$response = [
'error' => '',
'rows' => [],
'columns' => [],
'template_id' => 0,
'filename' => '',
'apply_routine' => false
];
/**
* Converts a column value to a PhpSpreadsheet 1-based column index.
* Accepted values:
* - "A" => 1
* - "B" => 2
* - "AA" => 27
* - "1" => 1
* - 1 => 1
*/
function normalizeColumnIndex($value): int
{
$value = trim((string)$value);
if ($value === '') {
return 1;
}
if (ctype_digit($value)) {
return max(1, (int)$value);
}
$value = strtoupper($value);
if (preg_match('/^[A-Z]+$/', $value)) {
return Coordinate::columnIndexFromString($value);
}
return 1;
}
try { try {
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['excel_file'])) { if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['excel_file'])) {
$template_id = isset($_POST['template_id']) ? intval($_POST['template_id']) : 0; $template_id = isset($_POST['template_id']) ? intval($_POST['template_id']) : 0;
$header_row = isset($_POST['header_row']) ? intval($_POST['header_row']) : 1;
$start_column = isset($_POST['start_column']) ? intval($_POST['start_column']) : 1; if ($template_id <= 0) {
throw new Exception("Template ID non valido.");
}
// Connessione al database
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
/*
* Recuperiamo i parametri direttamente dal template.
* Così non dipendiamo solo dal form e siamo sicuri di usare i dati salvati.
*/
$stmt = $pdo->prepare("
SELECT
id,
header_row,
start_column,
xls_end_column,
xls_sheet_index,
idroutine,
idclient
FROM excel_templates
WHERE id = ?
");
$stmt->execute([$template_id]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$template) {
throw new Exception("Template non trovato.");
}
$header_row = isset($template['header_row']) && $template['header_row'] !== null
? (int)$template['header_row']
: 1;
$start_column_raw = $template['start_column'] ?? 'A';
$start_column = normalizeColumnIndex($start_column_raw);
$xls_end_column_raw = trim((string)($template['xls_end_column'] ?? ''));
$xls_end_column = $xls_end_column_raw !== '' ? normalizeColumnIndex($xls_end_column_raw) : 0;
if ($xls_end_column > 0 && $xls_end_column < $start_column) {
throw new Exception("Last Column cannot be before Start Column.");
}
$xlsSheetIndex = isset($template['xls_sheet_index']) && $template['xls_sheet_index'] !== null
? (int)$template['xls_sheet_index']
: 0;
if ($header_row <= 0) {
$header_row = 1;
}
if ($xlsSheetIndex < 0) {
$xlsSheetIndex = 0;
}
// Debug del template_id ricevuto // Debug del template_id ricevuto
error_log("Received template_id from POST: " . print_r($_POST['template_id'], true)); error_log("Received template_id from POST: " . print_r($_POST['template_id'], true));
error_log("Converted template_id: $template_id"); error_log("Converted template_id: $template_id");
error_log(
"Template XLS settings - " .
"header_row: $header_row, " .
"start_column_raw: $start_column_raw, " .
"start_column_index: $start_column, " .
"xls_end_column_raw: " . ($xls_end_column_raw !== '' ? $xls_end_column_raw : '[empty = read all]') . ", " .
"xls_end_column_index: " . ($xls_end_column > 0 ? $xls_end_column : '[no limit]') . ", " .
"xls_sheet_index: $xlsSheetIndex"
);
$file = $_FILES['excel_file']; $file = $_FILES['excel_file'];
$fileError = $file['error']; $fileError = $file['error'];
@@ -38,23 +141,32 @@ try {
$originalFilename = basename($file['name']); $originalFilename = basename($file['name']);
$newFilename = "{$iduserlogin}-{$timestamp}-{$originalFilename}"; $newFilename = "{$iduserlogin}-{$timestamp}-{$originalFilename}";
$importFolder = __DIR__ . '/imported_trf/'; $importFolder = __DIR__ . '/imported_trf/';
if (!file_exists($importFolder)) { if (!file_exists($importFolder)) {
mkdir($importFolder, 0777, true); mkdir($importFolder, 0777, true);
} }
$destination = $importFolder . $newFilename; $destination = $importFolder . $newFilename;
// Sposta il file // Sposta il file
if (!move_uploaded_file($file['tmp_name'], $destination)) { if (!move_uploaded_file($file['tmp_name'], $destination)) {
throw new Exception("Errore durante lo spostamento del file in $destination"); throw new Exception("Errore durante lo spostamento del file in $destination");
} }
error_log("File spostato con successo in: $destination"); error_log("File spostato con successo in: $destination");
// Connessione al database
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Recupera il mapping da template_mapping // Recupera il mapping da template_mapping
$stmt = $pdo->prepare("SELECT field_id AS excel_column, field_id AS mysql_column, data_type, is_required, default_value, is_manual FROM template_mapping WHERE template_id = ?"); $stmt = $pdo->prepare("
SELECT
field_id AS excel_column,
field_id AS mysql_column,
data_type,
is_required,
default_value,
is_manual
FROM template_mapping
WHERE template_id = ?
");
$stmt->execute([$template_id]); $stmt->execute([$template_id]);
$mappings = $stmt->fetchAll(PDO::FETCH_ASSOC); $mappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -64,20 +176,77 @@ try {
if (empty($mappings)) { if (empty($mappings)) {
$response['error'] = "Nessun mapping trovato per il template con ID $template_id"; $response['error'] = "Nessun mapping trovato per il template con ID $template_id";
} else { } else {
// Carica il file rinominato con PHPSpreadsheet // Load the XLS/XLSX file.
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($destination); // If Last Column is configured in the template, load only the configured column range.
$worksheet = $spreadsheet->getActiveSheet(); // If Last Column is empty, keep the original behavior and read the entire sheet.
$reader = IOFactory::createReaderForFile($destination);
$reader->setReadDataOnly(true);
if ($xls_end_column > 0) {
$reader->setReadFilter(new class($start_column, $xls_end_column) implements \PhpOffice\PhpSpreadsheet\Reader\IReadFilter {
private int $startColumn;
private int $endColumn;
public function __construct(int $startColumn, int $endColumn)
{
$this->startColumn = $startColumn;
$this->endColumn = $endColumn;
}
public function readCell($columnAddress, $row, $worksheetName = ''): bool
{
$columnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($columnAddress);
return $columnIndex >= $this->startColumn && $columnIndex <= $this->endColumn;
}
});
}
$spreadsheet = $reader->load($destination);
$sheetCount = $spreadsheet->getSheetCount();
$sheetNames = $spreadsheet->getSheetNames();
if ($sheetCount <= 0) {
throw new Exception("Il file XLS non contiene fogli.");
}
if ($xlsSheetIndex >= $sheetCount) {
throw new Exception(
"Il foglio XLS selezionato non esiste. " .
"Sheet Number selezionato: {$xlsSheetIndex}. " .
"Fogli disponibili: " . implode(", ", array_map(
fn($name, $index) => "{$index}={$name}",
$sheetNames,
array_keys($sheetNames)
))
);
}
// Usa il foglio configurato nel template
$worksheet = $spreadsheet->getSheet($xlsSheetIndex);
$selectedSheetName = $worksheet->getTitle();
error_log("Selected XLS sheet - index: {$xlsSheetIndex}, name: {$selectedSheetName}");
$highestRow = $worksheet->getHighestRow(); $highestRow = $worksheet->getHighestRow();
$highestColumn = $worksheet->getHighestColumn(); $highestColumn = $worksheet->getHighestColumn();
$highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn); $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
// Force the effective highest column to Last Column, if configured.
if ($xls_end_column > 0) {
$highestColumnIndex = $xls_end_column;
$highestColumn = Coordinate::stringFromColumnIndex($highestColumnIndex);
}
$startRow = max(1, $header_row); $startRow = max(1, $header_row);
$startColumn = max(1, $start_column); $startColumn = max(1, $start_column);
// Advance startColumn to first non-empty cell in header row (match JS behavior) // Advance startColumn to first non-empty cell in header row, matching JS behavior
for ($sc = $startColumn; $sc <= $highestColumnIndex; $sc++) { for ($sc = $startColumn; $sc <= $highestColumnIndex; $sc++) {
$cl = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($sc); $cl = Coordinate::stringFromColumnIndex($sc);
$cv = trim((string)($worksheet->getCell($cl . $header_row)->getCalculatedValue() ?? '')); $cv = trim((string)($worksheet->getCell($cl . $header_row)->getCalculatedValue() ?? ''));
if ($cv !== '') { if ($cv !== '') {
$startColumn = $sc; $startColumn = $sc;
break; break;
@@ -85,24 +254,32 @@ try {
} }
// Debug dei parametri // Debug dei parametri
error_log("Processing - template_id: $template_id, startRow: $startRow, startColumn: $startColumn, highestRow: $highestRow, highestColumn: $highestColumn, highestColumnIndex: $highestColumnIndex"); error_log(
"Processing - template_id: $template_id, " .
"sheetIndex: $xlsSheetIndex, sheetName: $selectedSheetName, " .
"startRow: $startRow, startColumn: $startColumn, " .
"highestRow: $highestRow, highestColumn: $highestColumn, highestColumnIndex: $highestColumnIndex"
);
// Validazione degli indici // Validazione degli indici
if ($startRow > $highestRow) { if ($startRow > $highestRow) {
$response['error'] = "La riga di partenza ($startRow) supera il numero totale di righe ($highestRow)."; $response['error'] = "La riga di partenza ($startRow) supera il numero totale di righe ($highestRow) del foglio '$selectedSheetName'.";
} elseif ($startColumn > $highestColumnIndex) { } elseif ($startColumn > $highestColumnIndex) {
$response['error'] = "La colonna di partenza ($startColumn) supera il numero totale di colonne ($highestColumnIndex)."; $response['error'] = "La colonna di partenza ($startColumn) supera il numero totale di colonne ($highestColumnIndex) del foglio '$selectedSheetName'.";
} else { } else {
$excelData = []; $excelData = [];
// Build merge map for header row: physCol -> mergeStartCol // Build merge map for header row: physCol -> mergeStartCol
$mergeStartMap = []; $mergeStartMap = [];
foreach ($worksheet->getMergeCells() as $range) { foreach ($worksheet->getMergeCells() as $range) {
[$startCell, $endCell] = explode(':', $range); [$startCell, $endCell] = explode(':', $range);
$mStartCol = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString(preg_replace('/\d+/', '', $startCell));
$mEndCol = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString(preg_replace('/\d+/', '', $endCell)); $mStartCol = Coordinate::columnIndexFromString(preg_replace('/\d+/', '', $startCell));
$mEndCol = Coordinate::columnIndexFromString(preg_replace('/\d+/', '', $endCell));
$mStartRow = (int)preg_replace('/[A-Z]+/i', '', $startCell); $mStartRow = (int)preg_replace('/[A-Z]+/i', '', $startCell);
$mEndRow = (int)preg_replace('/[A-Z]+/i', '', $endCell); $mEndRow = (int)preg_replace('/[A-Z]+/i', '', $endCell);
if ($header_row >= $mStartRow && $header_row <= $mEndRow) { if ($header_row >= $mStartRow && $header_row <= $mEndRow) {
for ($c = $mStartCol; $c <= $mEndCol; $c++) { for ($c = $mStartCol; $c <= $mEndCol; $c++) {
$mergeStartMap[$c] = $mStartCol; $mergeStartMap[$c] = $mStartCol;
@@ -111,12 +288,17 @@ try {
} }
// Build logical columns: each merge = one column // Build logical columns: each merge = one column
$logicalCols = []; // array of physical column indices (one per logical column) $logicalCols = []; // array of physical column indices, one per logical column
$seen = []; $seen = [];
for ($col = $startColumn; $col <= $highestColumnIndex; $col++) { for ($col = $startColumn; $col <= $highestColumnIndex; $col++) {
if (isset($mergeStartMap[$col])) { if (isset($mergeStartMap[$col])) {
$ms = $mergeStartMap[$col]; $ms = $mergeStartMap[$col];
if (in_array($ms, $seen, true)) continue;
if (in_array($ms, $seen, true)) {
continue;
}
$seen[] = $ms; $seen[] = $ms;
$logicalCols[] = $ms; $logicalCols[] = $ms;
} else { } else {
@@ -127,38 +309,48 @@ try {
// Build header row using logical columns // Build header row using logical columns
$headerRowData = []; $headerRowData = [];
$logicalNum = 0; $logicalNum = 0;
foreach ($logicalCols as $physCol) { foreach ($logicalCols as $physCol) {
$logicalNum++; $logicalNum++;
$columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($physCol);
$columnLetter = Coordinate::stringFromColumnIndex($physCol);
$cell = $worksheet->getCell($columnLetter . $header_row); $cell = $worksheet->getCell($columnLetter . $header_row);
$cellValue = trim((string)($cell ? $cell->getCalculatedValue() : '')); $cellValue = trim((string)($cell ? $cell->getCalculatedValue() : ''));
$cellValue = preg_replace('/[\r\n\t]+/', ' ', $cellValue); $cellValue = preg_replace('/[\r\n\t]+/', ' ', $cellValue);
// Empty headers get __empty_N__ to match mapping page // Empty headers get __empty_N__ to match mapping page
$headerRowData[] = ($cellValue !== '') ? $cellValue : '__empty_' . $logicalNum . '__'; $headerRowData[] = ($cellValue !== '') ? $cellValue : '__empty_' . $logicalNum . '__';
} }
error_log("Logical headers: " . json_encode($headerRowData)); error_log("Logical headers: " . json_encode($headerRowData));
error_log("Logical cols (physical indices): " . json_encode($logicalCols)); error_log("Logical cols physical indices: " . json_encode($logicalCols));
// Find which logical columns have real headers // Find which logical columns have real headers
$headerFilledIndices = []; $headerFilledIndices = [];
foreach ($headerRowData as $idx => $hVal) { foreach ($headerRowData as $idx => $hVal) {
if (!str_starts_with($hVal, '__empty_')) $headerFilledIndices[] = $idx; if (!str_starts_with($hVal, '__empty_')) {
$headerFilledIndices[] = $idx;
}
} }
$minFilled = max(1, min(2, count($headerFilledIndices))); $minFilled = max(1, min(2, count($headerFilledIndices)));
// Extract data rows using logical columns // Extract data rows using logical columns
for ($row = $startRow + 1; $row <= $highestRow; $row++) { for ($row = $startRow + 1; $row <= $highestRow; $row++) {
$rowData = []; $rowData = [];
foreach ($logicalCols as $physCol) { foreach ($logicalCols as $physCol) {
$columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($physCol); $columnLetter = Coordinate::stringFromColumnIndex($physCol);
$cell = $worksheet->getCell($columnLetter . $row); $cell = $worksheet->getCell($columnLetter . $row);
$cellValue = $cell ? $cell->getCalculatedValue() : ''; $cellValue = $cell ? $cell->getCalculatedValue() : '';
$rowData[] = $cellValue ?: ''; $rowData[] = $cellValue ?: '';
} }
// Count how many header columns have data in this row // Count how many header columns have data in this row
$filledCount = 0; $filledCount = 0;
foreach ($headerFilledIndices as $idx) { foreach ($headerFilledIndices as $idx) {
if (isset($rowData[$idx]) && trim((string)$rowData[$idx]) !== '') { if (isset($rowData[$idx]) && trim((string)$rowData[$idx]) !== '') {
$filledCount++; $filledCount++;
@@ -166,17 +358,25 @@ try {
} }
if ($filledCount >= $minFilled) { if ($filledCount >= $minFilled) {
$excelData[] = ['data' => $rowData, 'excelrow' => $row]; $excelData[] = [
'data' => $rowData,
'excelrow' => $row
];
} }
} }
// Recupera routine dal template // Recupera routine dal template
$stmt = $pdo->prepare("SELECT idroutine, idclient FROM excel_templates WHERE id = ?"); if ($template && !empty($template['idroutine'])) {
$stmt->execute([$template_id]); $stmtRoutine = $pdo->prepare("
$template = $stmt->fetch(PDO::FETCH_ASSOC); SELECT
idroutine,
if ($template && $template['idroutine']) { name,
$stmtRoutine = $pdo->prepare("SELECT idroutine, name, filename, headerrow, instruction FROM routine WHERE idroutine = ?"); filename,
headerrow,
instruction
FROM routine
WHERE idroutine = ?
");
$stmtRoutine->execute([$template['idroutine']]); $stmtRoutine->execute([$template['idroutine']]);
$routineData = $stmtRoutine->fetch(PDO::FETCH_ASSOC); $routineData = $stmtRoutine->fetch(PDO::FETCH_ASSOC);
@@ -188,6 +388,7 @@ try {
'filename' => $routineData['filename'] ?? '', 'filename' => $routineData['filename'] ?? '',
'headerrow' => $routineData['headerrow'] ?? $header_row 'headerrow' => $routineData['headerrow'] ?? $header_row
]; ];
error_log("Routine rilevata per template {$template_id}: " . print_r($routineData, true)); error_log("Routine rilevata per template {$template_id}: " . print_r($routineData, true));
} else { } else {
error_log("Errore: Nessuna routine trovata per idroutine {$template['idroutine']}"); error_log("Errore: Nessuna routine trovata per idroutine {$template['idroutine']}");
@@ -204,6 +405,8 @@ try {
$_SESSION['template_id'] = $template_id; $_SESSION['template_id'] = $template_id;
$_SESSION['headers'] = $headerRowData; $_SESSION['headers'] = $headerRowData;
$_SESSION['mappings'] = $mappings; $_SESSION['mappings'] = $mappings;
$_SESSION['xls_sheet_index'] = $xlsSheetIndex;
$_SESSION['xls_sheet_name'] = $selectedSheetName;
// Includi excel_data nella risposta JSON in ogni caso // Includi excel_data nella risposta JSON in ogni caso
$response['excel_data'] = $excelData; $response['excel_data'] = $excelData;
@@ -211,6 +414,8 @@ try {
$response['columns'] = $headerRowData; $response['columns'] = $headerRowData;
$response['template_id'] = $template_id; $response['template_id'] = $template_id;
$response['filename'] = $newFilename; $response['filename'] = $newFilename;
$response['xls_sheet_index'] = $xlsSheetIndex;
$response['xls_sheet_name'] = $selectedSheetName;
} }
} }
} else { } else {
+119 -5
View File
@@ -4,6 +4,35 @@ require_once 'class/db-functions.php';
$response = ["success" => false, "message" => ""]; $response = ["success" => false, "message" => ""];
function excelColumnToIndex($column)
{
$column = strtoupper(trim((string)$column));
if ($column === '') {
return null;
}
// Numeric column index, example: 40
if (ctype_digit($column)) {
$index = (int)$column;
return $index > 0 ? $index : null;
}
// Excel column letters, example: A, AN, XFC
if (!preg_match('/^[A-Z]+$/', $column)) {
return null;
}
$index = 0;
$length = strlen($column);
for ($i = 0; $i < $length; $i++) {
$index = ($index * 26) + (ord($column[$i]) - ord('A') + 1);
}
return $index;
}
try { try {
if ($_SERVER["REQUEST_METHOD"] !== "POST") { if ($_SERVER["REQUEST_METHOD"] !== "POST") {
throw new Exception("Invalid request method."); throw new Exception("Invalid request method.");
@@ -12,22 +41,41 @@ try {
// Retrieve and sanitize form data // Retrieve and sanitize form data
$name = trim($_POST['name'] ?? ''); $name = trim($_POST['name'] ?? '');
$source_type = strtoupper(trim($_POST['source_type'] ?? 'XLS')); $source_type = strtoupper(trim($_POST['source_type'] ?? 'XLS'));
$header_row = isset($_POST['header_row']) && $_POST['header_row'] !== '' ? intval($_POST['header_row']) : null;
$header_row = isset($_POST['header_row']) && $_POST['header_row'] !== ''
? intval($_POST['header_row'])
: null;
$start_column = trim($_POST['start_column'] ?? ''); $start_column = trim($_POST['start_column'] ?? '');
$xls_end_column = strtoupper(trim($_POST['xls_end_column'] ?? ''));
$xls_end_column = $xls_end_column !== '' ? $xls_end_column : null;
$xls_sheet_index = isset($_POST['xls_sheet_index']) && $_POST['xls_sheet_index'] !== ''
? intval($_POST['xls_sheet_index'])
: 0;
$api_config_id = isset($_POST['api_config_id']) && $_POST['api_config_id'] !== ''
? intval($_POST['api_config_id'])
: null;
$description = trim($_POST['description'] ?? ''); $description = trim($_POST['description'] ?? '');
$target_table = trim($_POST['target_table'] ?? 'datadb'); $target_table = trim($_POST['target_table'] ?? 'datadb');
$idclient = intval($_POST['client_id'] ?? 0); $idclient = intval($_POST['client_id'] ?? 0);
$clientname = trim($_POST['client_name'] ?? ''); $clientname = trim($_POST['client_name'] ?? '');
$idschema = intval($_POST['idschema'] ?? 0); $idschema = intval($_POST['idschema'] ?? 0);
$schemaname = trim($_POST['schemaname'] ?? ''); $schemaname = trim($_POST['schemaname'] ?? '');
$idroutine = isset($_POST['idroutine']) && $_POST['idroutine'] !== '' ? intval($_POST['idroutine']) : null; $idroutine = isset($_POST['idroutine']) && $_POST['idroutine'] !== ''
? intval($_POST['idroutine'])
: null;
$button_size = trim($_POST['button_size'] ?? 'medium'); $button_size = trim($_POST['button_size'] ?? 'medium');
$button_bg_color = trim($_POST['button_bg_color'] ?? '#007bff'); $button_bg_color = trim($_POST['button_bg_color'] ?? '#007bff');
$button_text_color = trim($_POST['button_text_color'] ?? '#ffffff'); $button_text_color = trim($_POST['button_text_color'] ?? '#ffffff');
$button_label = trim($_POST['button_label'] ?? 'Click Me'); $button_label = trim($_POST['button_label'] ?? 'Click Me');
// Normalize source type // Normalize source type
if (!in_array($source_type, ['XLS', 'API'], true)) { // API / JSON is saved as API
if (!in_array($source_type, ['XLS', 'API', 'PDF'], true)) {
$source_type = 'XLS'; $source_type = 'XLS';
} }
@@ -41,18 +89,72 @@ try {
if ($header_row === null || $header_row <= 0 || $start_column === '') { if ($header_row === null || $header_row <= 0 || $start_column === '') {
throw new Exception("Header Row and Start Column are required for XLS templates."); throw new Exception("Header Row and Start Column are required for XLS templates.");
} }
if ($xls_sheet_index < 0) {
throw new Exception("XLS Sheet Number cannot be negative.");
}
$startColumnIndex = excelColumnToIndex($start_column);
if ($startColumnIndex === null) {
throw new Exception("Start Column is not valid. Use Excel column letters like A, AN or a positive number.");
}
if ($xls_end_column !== null) {
$endColumnIndex = excelColumnToIndex($xls_end_column);
if ($endColumnIndex === null) {
throw new Exception("Last Column is not valid. Use Excel column letters like AN or a positive number.");
}
if ($endColumnIndex < $startColumnIndex) {
throw new Exception("Last Column cannot be before Start Column.");
}
}
$api_config_id = null;
} }
// API templates do not require XLS coordinates // API / JSON validation
if ($source_type === 'API') { if ($source_type === 'API') {
if (empty($api_config_id)) {
throw new Exception("API / JSON configuration is required for API / JSON templates.");
}
$header_row = null; $header_row = null;
$start_column = null; $start_column = null;
$xls_sheet_index = null;
$xls_end_column = null;
}
// PDF currently does not require XLS coordinates or API configuration
if ($source_type === 'PDF') {
$header_row = null;
$start_column = null;
$xls_sheet_index = null;
$xls_end_column = null;
$api_config_id = null;
} }
// Database connection // Database connection
$db = DBHandlerSelect::getInstance(); $db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection(); $pdo = $db->getConnection();
// Optional check: verify API configuration exists and is active
if ($api_config_id !== null) {
$stmt = $pdo->prepare("
SELECT COUNT(*)
FROM api_configurations
WHERE id = ?
AND is_active = 1
");
$stmt->execute([$api_config_id]);
if ((int)$stmt->fetchColumn() === 0) {
throw new Exception("Selected API / JSON configuration does not exist or is not active.");
}
}
// Insert the new template // Insert the new template
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO excel_templates INSERT INTO excel_templates
@@ -61,6 +163,9 @@ try {
source_type, source_type,
header_row, header_row,
start_column, start_column,
xls_end_column,
xls_sheet_index,
api_config_id,
description, description,
target_table, target_table,
idclient, idclient,
@@ -75,7 +180,13 @@ try {
created_at, created_at,
updated_at updated_at
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) VALUES
(
?, ?, ?, ?, ?, ?,?,
?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?,
NOW(), NOW()
)
"); ");
$stmt->execute([ $stmt->execute([
@@ -83,6 +194,9 @@ try {
$source_type, $source_type,
$header_row, $header_row,
$start_column, $start_column,
$xls_end_column,
$xls_sheet_index,
$api_config_id,
$description, $description,
$target_table, $target_table,
$idclient, $idclient,
+71
View File
@@ -0,0 +1,71 @@
<?php
/**
* Routine: burberry
*
* Purpose:
* For each imported XLS row:
* - read the value from column S
* - read the value from column T
* - merge the values
* - save the final value into column S
*
* Target:
* Column S must be mapped to the destination field in the template mapping.
*/
function applyRoutine(&$excelData, $routineData = [])
{
/*
* This routine does not require external routine data.
* Columns are fixed.
*
* Excel column indexes are zero-based:
*
* S = 18
* T = 19
*/
$targetColumnIndex = 18; // S
$columnSIndex = 18; // S
$columnTIndex = 19; // T
foreach ($excelData as $rowIndex => &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Routine burberry: invalid row structure at index {$rowIndex}.");
continue;
}
$valueS = trim((string)($row['data'][$columnSIndex] ?? ''));
$valueT = trim((string)($row['data'][$columnTIndex] ?? ''));
/*
* Merge values, ignoring empty values.
*/
$mergedValues = [];
if ($valueS !== '') {
$mergedValues[] = $valueS;
}
if ($valueT !== '') {
$mergedValues[] = $valueT;
}
/*
* Save final value into column S.
*/
$row['data'][$targetColumnIndex] = implode(' ', $mergedValues);
error_log(
"Routine burberry: row " .
($row['excelrow'] ?? $rowIndex) .
" generated value in column S: " .
$row['data'][$targetColumnIndex]
);
}
unset($row);
error_log("Routine burberry completed.");
}
+67
View File
@@ -0,0 +1,67 @@
<?php
/**
* Routine: merge_column_T_and_U_into_T
*
* Purpose:
* For each imported XLS row:
* - read the value from column T
* - read the value from column U
* - merge both values
* - save the final value into column T
*
* Target:
* Column T must be mapped to the destination field in the template mapping.
*/
function applyRoutine(&$excelData, $routineData = [])
{
/*
* Excel column indexes are zero-based:
*
* T = 19
* U = 20
*/
$targetColumnIndex = 19; // T
$firstColumnIndex = 19; // T
$secondColumnIndex = 20; // U
foreach ($excelData as $rowIndex => &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Routine merge T+U: invalid row structure at index {$rowIndex}.");
continue;
}
$valueT = trim((string)($row['data'][$firstColumnIndex] ?? ''));
$valueU = trim((string)($row['data'][$secondColumnIndex] ?? ''));
/*
* Merge values, ignoring empty values.
*/
$mergedValues = [];
if ($valueT !== '') {
$mergedValues[] = $valueT;
}
if ($valueU !== '') {
$mergedValues[] = $valueU;
}
/*
* Save final value into column T.
*/
$row['data'][$targetColumnIndex] = implode(' ', $mergedValues);
error_log(
"Routine merge T+U: row " .
($row['excelrow'] ?? $rowIndex) .
" generated value in column T: " .
$row['data'][$targetColumnIndex]
);
}
unset($row);
error_log("Routine merge T+U completed.");
}
+76
View File
@@ -0,0 +1,76 @@
<?php
/**
* Routine: paulshark
*
* Purpose:
* For each imported XLS row:
* - read the value from column D
* - read the value from column E
* - read the value from column J
* - merge the values
* - save the final value into column D
*
* Target:
* Column D must be mapped to the destination field in the template mapping.
*/
function applyRoutine(&$excelData, $routineData = [])
{
/*
* Excel column indexes are zero-based:
*
* D = 3
* E = 4
* J = 9
*/
$targetColumnIndex = 3; // D
$columnDIndex = 3; // D
$columnEIndex = 4; // E
$columnJIndex = 9; // J
foreach ($excelData as $rowIndex => &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Routine paulshark: invalid row structure at index {$rowIndex}.");
continue;
}
$valueD = trim((string)($row['data'][$columnDIndex] ?? ''));
$valueE = trim((string)($row['data'][$columnEIndex] ?? ''));
$valueJ = trim((string)($row['data'][$columnJIndex] ?? ''));
/*
* Merge values, ignoring empty values.
*/
$mergedValues = [];
if ($valueD !== '') {
$mergedValues[] = $valueD;
}
if ($valueE !== '') {
$mergedValues[] = $valueE;
}
if ($valueJ !== '') {
$mergedValues[] = $valueJ;
}
/*
* Save final value into column D.
*/
$row['data'][$targetColumnIndex] = implode(' ', $mergedValues);
error_log(
"Routine paulshark: row " .
($row['excelrow'] ?? $rowIndex) .
" generated value in column D: " .
$row['data'][$targetColumnIndex]
);
}
unset($row);
error_log("Routine paulshark completed.");
}
@@ -0,0 +1,71 @@
<?php
/**
* Routine: Richemont Pelletteria
*
* Purpose:
* For each imported XLS row:
* - read the value from column D
* - read the value from column E
* - merge the values
* - save the final value into column D
*
* Target:
* Column D must be mapped to the destination field in the template mapping.
*/
function applyRoutine(&$excelData, $routineData = [])
{
/*
* This routine does not require external routine data.
* Columns are fixed.
*
* Excel column indexes are zero-based:
*
* D = 3
* E = 4
*/
$targetColumnIndex = 3; // D
$columnDIndex = 3; // D
$columnEIndex = 4; // E
foreach ($excelData as $rowIndex => &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Routine Richemont Pelletteria: invalid row structure at index {$rowIndex}.");
continue;
}
$valueD = trim((string)($row['data'][$columnDIndex] ?? ''));
$valueE = trim((string)($row['data'][$columnEIndex] ?? ''));
/*
* Merge values, ignoring empty values.
*/
$mergedValues = [];
if ($valueD !== '') {
$mergedValues[] = $valueD;
}
if ($valueE !== '') {
$mergedValues[] = $valueE;
}
/*
* Save final value into column D.
*/
$row['data'][$targetColumnIndex] = implode(' ', $mergedValues);
error_log(
"Routine Richemont Pelletteria: row " .
($row['excelrow'] ?? $rowIndex) .
" generated value in column D: " .
$row['data'][$targetColumnIndex]
);
}
unset($row);
error_log("Routine Richemont Pelletteria completed.");
}
@@ -0,0 +1,114 @@
<?php
/**
* Routine: build_field_347_from_x_columns
*
* Purpose:
* For each imported XLS row:
* - check columns P to AT
* - when a cell contains "x", take the related column title from row 6
* - append the free text value from column AU
* - save the final comma-separated text into column P
*
* Target:
* Column P must be mapped to field_id 347.
*/
function applyRoutine(&$excelData, $routineData = [])
{
/*
* Excel column indexes are zero-based:
*
* P = 15
* AT = 45
* AU = 46
*/
$targetColumnIndex = 15; // P
$startColumnIndex = 15; // P
$endColumnIndex = 45; // AT
$extraColumnIndex = 46; // AU
/*
* Headers must come from XLS row 6.
* Usually they are passed inside $routineData['xls_headers'].
*/
$headers = $routineData['xls_headers'] ?? [];
if (empty($headers) || !is_array($headers)) {
error_log("Routine field_id 347: missing XLS headers from row 6.");
return;
}
foreach ($excelData as $rowIndex => &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Routine field_id 347: invalid row structure at index {$rowIndex}.");
continue;
}
$selectedValues = [];
/*
* Check columns from P to AT.
* If the cell contains x, take the related column header.
*/
for ($columnIndex = $startColumnIndex; $columnIndex <= $endColumnIndex; $columnIndex++) {
$cellValue = strtolower(trim((string)($row['data'][$columnIndex] ?? '')));
if ($cellValue === 'x') {
$headerTitle = trim((string)($headers[$columnIndex] ?? ''));
if ($headerTitle !== '') {
$selectedValues[] = $headerTitle;
}
}
}
/*
* Add free text from column AU.
*/
$extraText = '';
if (isset($row['data'][$extraColumnIndex])) {
$extraText = trim((string)$row['data'][$extraColumnIndex]);
} elseif (isset($row['data']['AU'])) {
$extraText = trim((string)$row['data']['AU']);
}
error_log(
"Routine field_id 347: row " .
($row['excelrow'] ?? $rowIndex) .
" AU index {$extraColumnIndex} value: " .
print_r($row['data'][$extraColumnIndex] ?? null, true) .
" | AU key value: " .
print_r($row['data']['AU'] ?? null, true)
);
if ($extraText !== '') {
$selectedValues[] = $extraText;
}
/*
* Remove empty and duplicate values.
*/
$selectedValues = array_values(array_unique(array_filter($selectedValues, function ($value) {
return trim((string)$value) !== '';
})));
/*
* Save final value into column P.
* Column P must be mapped to field_id 347 in the template mapping.
*/
$row['data'][$targetColumnIndex] = implode(', ', $selectedValues);
error_log(
"Routine field_id 347: row " .
($row['excelrow'] ?? $rowIndex) .
" generated value: " .
$row['data'][$targetColumnIndex]
);
}
unset($row);
error_log("Routine field_id 347 completed.");
}
+22 -4
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,
@@ -730,8 +730,8 @@
{ {
"IdSchemaCustomFields": 177, "IdSchemaCustomFields": 177,
"ConteggioClienti": 0, "ConteggioClienti": 0,
"Nome": "Phoebe philo ACC", "Nome": "Phoebe Philo ",
"Descrizione": "(scarpe, borse, cinture, occhiali, gioielleria)\r\n" "Descrizione": "\r\n\r\n"
}, },
{ {
"IdSchemaCustomFields": 178, "IdSchemaCustomFields": 178,
@@ -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"
} }
] ]
} }
@@ -2,7 +2,11 @@
require_once dirname(__DIR__, 2) . '/vendor/autoload.php'; require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php'; require_once __DIR__ . '/class/db-functions.php';
include dirname(__DIR__) . '/../extra/auth.php'; include dirname(__DIR__) . '/../extra/auth.php';
if (!Auth::check()) { http_response_code(401); echo json_encode(['error' => 'Unauthorized']); exit; } if (!Auth::check()) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
require_once __DIR__ . '/class/VisualLimsApiClient.class.php'; require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
@@ -13,7 +17,8 @@ error_reporting(E_ALL);
$fieldId = intval($_GET['field_id'] ?? 0); $fieldId = intval($_GET['field_id'] ?? 0);
$q = mb_strtolower(trim($_GET['q'] ?? '')); $q = mb_strtolower(trim($_GET['q'] ?? ''));
$id = isset($_GET['id']) ? intval($_GET['id']) : null; $id = isset($_GET['id']) ? intval($_GET['id']) : null;
$limit = max(1, min(50, intval($_GET['limit'] ?? 20))); $rawLimit = intval($_GET['limit'] ?? 20);
$limit = $rawLimit <= 0 ? 0 : max(1, min(500, $rawLimit));
if (!$fieldId) { if (!$fieldId) {
echo json_encode(['results' => []]); echo json_encode(['results' => []]);
@@ -52,7 +57,7 @@ try {
$text = $v['Valore'] ?? ''; $text = $v['Valore'] ?? '';
if ($q === '' || mb_strpos(mb_strtolower($text), $q) !== false) { if ($q === '' || mb_strpos(mb_strtolower($text), $q) !== false) {
$results[] = ['id' => $v['IdCustomFieldsValue'], 'text' => $text]; $results[] = ['id' => $v['IdCustomFieldsValue'], 'text' => $text];
if (count($results) >= $limit) break; if ($limit > 0 && count($results) >= $limit) break;
} }
} }
+23
View File
@@ -0,0 +1,23 @@
<?php
require_once "class/VisualLimsApiClient.class.php";
include('include/headscript.php');
header("Content-Type: application/json; charset=utf-8");
try {
$api = VisualLimsApiClient::getInstance();
$commessaId = 577818;
$endpoint = "CommessaWeb({$commessaId})?\$expand=CommesseCustomFields(\$expand=CustomField)";
$result = $api->get($endpoint);
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
"success" => false,
"error" => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
@@ -0,0 +1,107 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
require_once dirname(__FILE__) . '/include/headscript.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$api = VisualLimsApiClient::getInstance();
// Get all schemas currently used in template_mapping
$stmtSchemas = $pdo->query("
SELECT DISTINCT schema_id
FROM template_mapping
WHERE schema_id IS NOT NULL
AND schema_id > 0
ORDER BY schema_id ASC
");
$schemaIds = $stmtSchemas->fetchAll(PDO::FETCH_COLUMN);
if (empty($schemaIds)) {
throw new Exception('No schema_id found in template_mapping');
}
$stmtUpdate = $pdo->prepare("
UPDATE template_mapping
SET field_order = ?
WHERE schema_id = ?
AND field_id = ?
");
$summary = [];
$totalUpdated = 0;
foreach ($schemaIds as $schemaId) {
$schemaId = (int)$schemaId;
$endpoint = "SchemaCustomField($schemaId)?\$expand=SchemiCustomFieldsDettagli(\$expand=CustomField)";
$data = $api->get($endpoint);
if (empty($data['SchemiCustomFieldsDettagli']) || !is_array($data['SchemiCustomFieldsDettagli'])) {
$summary[] = [
'schema_id' => $schemaId,
'success' => false,
'message' => 'No SchemiCustomFieldsDettagli found'
];
continue;
}
$schemaUpdated = 0;
$notFound = [];
foreach ($data['SchemiCustomFieldsDettagli'] as $detail) {
$order = intval($detail['Ordine'] ?? 9999);
$fieldId = intval($detail['CustomField']['IdCustomField'] ?? 0);
if ($fieldId <= 0) {
continue;
}
$stmtUpdate->execute([
$order,
$schemaId,
$fieldId
]);
if ($stmtUpdate->rowCount() > 0) {
$schemaUpdated++;
$totalUpdated++;
} else {
$notFound[] = [
'field_id' => $fieldId,
'order' => $order,
'label' => $detail['CustomField']['Titolo'] ?? ''
];
}
}
$summary[] = [
'schema_id' => $schemaId,
'success' => true,
'updated' => $schemaUpdated,
'not_found_count' => count($notFound),
'not_found' => $notFound
];
}
echo json_encode([
'success' => true,
'schemas_processed' => count($schemaIds),
'total_updated' => $totalUpdated,
'summary' => $summary
], JSON_PRETTY_PRINT);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => $e->getMessage()
], JSON_PRETTY_PRINT);
}
+46 -14
View File
@@ -3,14 +3,18 @@ ini_set('display_errors', 1);
ini_set('display_startup_errors', 1); ini_set('display_startup_errors', 1);
error_reporting(E_ALL); error_reporting(E_ALL);
include('include/headscript.php'); // Assumi che questo includa la connessione DB include('include/headscript.php');
header('Content-Type: application/json');
// Recupera il payload JSON
$data = json_decode(file_get_contents('php://input'), true); $data = json_decode(file_get_contents('php://input'), true);
$template_id = intval($data['template_id']); $template_id = intval($data['template_id'] ?? 0);
$mapping_id = intval($data['mapping_id']); $mapping_id = intval($data['mapping_id'] ?? 0);
$value = intval($data['value']); $value = intval($data['value'] ?? 0);
// IMPORTANT: main_field may be ENUM('0','1'), so use string values
$mainValue = ($value === 1) ? '1' : '0';
if ($template_id <= 0 || $mapping_id <= 0) { if ($template_id <= 0 || $mapping_id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid IDs']); echo json_encode(['success' => false, 'message' => 'Invalid IDs']);
@@ -23,19 +27,47 @@ $pdo = $db->getConnection();
try { try {
$pdo->beginTransaction(); $pdo->beginTransaction();
if ($value === 1) { if ($mainValue === '1') {
// Setta tutti main_field a 0 per questo template $stmt = $pdo->prepare("
$stmt = $pdo->prepare("UPDATE template_mapping SET main_field = 0 WHERE template_id = ?"); SELECT COUNT(*)
$stmt->execute([$template_id]); FROM template_mapping
WHERE template_id = ?
AND main_field = '1'
AND id <> ?
");
$stmt->execute([$template_id, $mapping_id]);
$currentMainCount = (int)$stmt->fetchColumn();
if ($currentMainCount >= 2) {
$pdo->rollBack();
echo json_encode([
'success' => false,
'message' => 'Maximum 2 Main fields allowed',
'currentMainCount' => $currentMainCount
]);
exit;
}
} }
// Setta il valore per questo mapping $stmt = $pdo->prepare("
$stmt = $pdo->prepare("UPDATE template_mapping SET main_field = ? WHERE id = ? AND template_id = ?"); UPDATE template_mapping
$stmt->execute([$value, $mapping_id, $template_id]); SET main_field = ?
WHERE id = ?
AND template_id = ?
");
$stmt->execute([$mainValue, $mapping_id, $template_id]);
$pdo->commit(); $pdo->commit();
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
} catch (Exception $e) { } catch (Exception $e) {
$pdo->rollBack(); if ($pdo->inTransaction()) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]); $pdo->rollBack();
}
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
} }
+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();
@@ -0,0 +1,79 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
require_once dirname(__FILE__) . '/include/headscript.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$schemaId = isset($_GET['schema_id']) && is_numeric($_GET['schema_id'])
? intval($_GET['schema_id'])
: 0;
if ($schemaId <= 0) {
throw new Exception('Missing or invalid schema_id');
}
$api = VisualLimsApiClient::getInstance();
$endpoint = "SchemaCustomField($schemaId)?\$expand=SchemiCustomFieldsDettagli(\$expand=CustomField)";
$data = $api->get($endpoint);
if (empty($data['SchemiCustomFieldsDettagli']) || !is_array($data['SchemiCustomFieldsDettagli'])) {
throw new Exception('No SchemiCustomFieldsDettagli found');
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$updated = 0;
$notFound = [];
$stmt = $pdo->prepare("
UPDATE template_mapping
SET field_order = ?
WHERE schema_id = ?
AND field_id = ?
");
foreach ($data['SchemiCustomFieldsDettagli'] as $detail) {
$order = intval($detail['Ordine'] ?? 9999);
$fieldId = intval($detail['CustomField']['IdCustomField'] ?? 0);
if ($fieldId <= 0) {
continue;
}
$stmt->execute([
$order,
$schemaId,
$fieldId
]);
if ($stmt->rowCount() > 0) {
$updated++;
} else {
$notFound[] = [
'field_id' => $fieldId,
'order' => $order,
'label' => $detail['CustomField']['Titolo'] ?? ''
];
}
}
echo json_encode([
'success' => true,
'schema_id' => $schemaId,
'updated' => $updated,
'not_found' => $notFound
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
+68 -16
View File
@@ -6,22 +6,57 @@ error_reporting(E_ALL);
require_once(__DIR__ . '/class/db-functions.php'); require_once(__DIR__ . '/class/db-functions.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$data = json_decode(file_get_contents("php://input"), true);
if (!$data || !isset($data['template_id'], $data['xls_headers'])) {
echo json_encode(["success" => false, "message" => "Invalid or missing data"]);
exit;
}
$templateId = $data['template_id'];
$xlsHeaders = $data['xls_headers'];
$headerRow = isset($data['header_row']) ? (int)$data['header_row'] : null;
$startColumn = isset($data['start_column']) ? (int)$data['start_column'] : null;
try { try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$data = json_decode(file_get_contents("php://input"), true);
if (!$data || !isset($data['template_id'], $data['xls_headers'])) {
echo json_encode(["success" => false, "message" => "Invalid or missing data"]);
exit;
}
$templateId = (int)$data['template_id'];
$xlsHeaders = $data['xls_headers'];
$headerRow = isset($data['header_row']) && $data['header_row'] !== ''
? (int)$data['header_row']
: null;
$startColumn = isset($data['start_column']) && $data['start_column'] !== ''
? (int)$data['start_column']
: null;
$xlsSheetIndex = isset($data['xls_sheet_index']) && $data['xls_sheet_index'] !== ''
? (int)$data['xls_sheet_index']
: null;
if ($templateId <= 0) {
echo json_encode(["success" => false, "message" => "Invalid template ID"]);
exit;
}
if ($xlsHeaders === '') {
echo json_encode(["success" => false, "message" => "XLS headers cannot be empty"]);
exit;
}
if ($headerRow !== null && $headerRow <= 0) {
echo json_encode(["success" => false, "message" => "Header row must be greater than 0"]);
exit;
}
if ($startColumn !== null && $startColumn <= 0) {
echo json_encode(["success" => false, "message" => "Start column must be greater than 0"]);
exit;
}
if ($xlsSheetIndex !== null && $xlsSheetIndex < 0) {
echo json_encode(["success" => false, "message" => "XLS sheet number cannot be negative"]);
exit;
}
$sql = "UPDATE excel_templates SET xls_headers = ?"; $sql = "UPDATE excel_templates SET xls_headers = ?";
$params = [$xlsHeaders]; $params = [$xlsHeaders];
@@ -29,11 +64,18 @@ try {
$sql .= ", header_row = ?"; $sql .= ", header_row = ?";
$params[] = $headerRow; $params[] = $headerRow;
} }
if ($startColumn !== null) { if ($startColumn !== null) {
$sql .= ", start_column = ?"; $sql .= ", start_column = ?";
$params[] = $startColumn; $params[] = $startColumn;
} }
if ($xlsSheetIndex !== null) {
$sql .= ", xls_sheet_index = ?";
$params[] = $xlsSheetIndex;
}
$sql .= ", updated_at = NOW()";
$sql .= " WHERE id = ?"; $sql .= " WHERE id = ?";
$params[] = $templateId; $params[] = $templateId;
@@ -45,8 +87,18 @@ try {
exit; exit;
} }
echo json_encode(["success" => true, "message" => "XLS headers saved successfully"]); echo json_encode([
"success" => true,
"message" => "XLS headers saved successfully",
"debug" => [
"template_id" => $templateId,
"header_row" => $headerRow,
"start_column" => $startColumn,
"xls_sheet_index" => $xlsSheetIndex
]
]);
} catch (Exception $e) { } catch (Exception $e) {
echo json_encode(["success" => false, "message" => "Error: " . $e->getMessage()]); echo json_encode(["success" => false, "message" => "Error: " . $e->getMessage()]);
} }
exit; exit;
File diff suppressed because it is too large Load Diff
+618 -37
View File
@@ -8,8 +8,71 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!--favicon--> <!--favicon-->
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" /> <link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?> <?php include('cssinclude.php'); ?>
<title>xxx - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> - User Profile</title>
<!-- Select2 CSS -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<title>SmartTRF - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> - User Profile</title>
<style>
.profile-avatar-preview {
max-width: 100px;
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 50%;
border: 1px solid #e5e7eb;
background: #f8f9fa;
}
.small-muted {
font-size: 12px;
color: #6c757d;
}
.select2-container {
width: 100% !important;
}
.lims-loading {
font-size: 12px;
color: #6c757d;
}
.password-note {
background: #f8fafc;
border: 1px solid #e5e7eb;
color: #475569;
border-radius: 10px;
padding: 10px 12px;
font-size: 13px;
line-height: 1.5;
}
.password-error {
display: none;
background: #fff1f2;
border: 1px solid #fecdd3;
color: #be123c;
border-radius: 10px;
padding: 10px 12px;
font-size: 13px;
line-height: 1.5;
}
.password-success {
display: none;
background: #f0fdf4;
border: 1px solid #bbf7d0;
color: #166534;
border-radius: 10px;
padding: 10px 12px;
font-size: 13px;
line-height: 1.5;
}
</style>
</head> </head>
<body> <body>
@@ -18,9 +81,11 @@
<!--sidebar wrapper --> <!--sidebar wrapper -->
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<!--end sidebar wrapper --> <!--end sidebar wrapper -->
<!--start header --> <!--start header -->
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
<!--end header --> <!--end header -->
<!--start page wrapper --> <!--start page wrapper -->
<div class="page-wrapper"> <div class="page-wrapper">
<div class="page-content"> <div class="page-content">
@@ -31,61 +96,195 @@
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div> <div>
<h6 class="mb-0">Edit Profile</h6> <h6 class="mb-0">Edit Profile</h6>
<small class="text-muted">Update your personal information and LIMS references.</small>
</div> </div>
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<form action="update-profile.php" method="POST" enctype="multipart/form-data"> <form action="update-profile.php" method="POST" enctype="multipart/form-data" id="profileForm">
<div class="mb-3"> <div class="row g-3">
<label for="first_name" class="form-label">First Name</label>
<input type="text" class="form-control" id="first_name" name="first_name" value="<?= htmlspecialchars($nameuser); ?>" required>
</div>
<div class="mb-3">
<label for="last_name" class="form-label">Last Name</label>
<input type="text" class="form-control" id="last_name" name="last_name" value="<?= htmlspecialchars($surnameuser); ?>" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" value="<?= htmlspecialchars($emailuser); ?>" required>
</div>
<div class="mb-3"> <div class="col-md-6">
<label for="lims_user_id" class="form-label">Accettatore</label> <label for="first_name" class="form-label">First Name</label>
<input type="number" class="form-control" id="lims_user_id" name="lims_user_id" value="<?= htmlspecialchars($lims_user_id ?? ''); ?>"> <input
</div> type="text"
class="form-control"
id="first_name"
name="first_name"
value="<?= htmlspecialchars($nameuser ?? '', ENT_QUOTES, 'UTF-8'); ?>"
required>
</div>
<div class="mb-3"> <div class="col-md-6">
<label for="lims_global_user_id" class="form-label">LIMS Global</label> <label for="last_name" class="form-label">Last Name</label>
<input type="number" class="form-control" id="lims_global_user_id" name="lims_global_user_id" value="<?= htmlspecialchars($lims_global_user_id ?? ''); ?>"> <input
</div> type="text"
class="form-control"
id="last_name"
name="last_name"
value="<?= htmlspecialchars($surnameuser ?? '', ENT_QUOTES, 'UTF-8'); ?>"
required>
</div>
<div class="col-md-12">
<label for="email" class="form-label">Email</label>
<input
type="email"
class="form-control"
id="email"
name="email"
value="<?= htmlspecialchars($emailuser ?? '', ENT_QUOTES, 'UTF-8'); ?>"
required>
</div>
<div class="col-md-6">
<label for="lims_user_id" class="form-label">Accettatore</label>
<select
class="form-select"
id="lims_user_id"
name="lims_user_id"
data-current-value="<?= htmlspecialchars($lims_user_id ?? '', ENT_QUOTES, 'UTF-8'); ?>">
<option value="">Select acceptor</option>
</select>
<div class="small-muted mt-1">
Values loaded from CustomField ID 244.
</div>
</div>
<div class="col-md-6">
<label for="lims_global_user_id" class="form-label">LIMS Global</label>
<select
class="form-select"
id="lims_global_user_id"
name="lims_global_user_id"
data-current-value="<?= htmlspecialchars($lims_global_user_id ?? '', ENT_QUOTES, 'UTF-8'); ?>">
<option value="">Select LIMS user</option>
</select>
<div class="small-muted mt-1">
Values loaded from LIMS users list.
</div>
</div>
<div class="col-md-12">
<div class="lims-loading" id="limsLoadingMessage">
Loading LIMS users and acceptors...
</div>
</div>
<div class="col-md-12">
<label for="avatar" class="form-label">Profile Picture</label>
<input
type="file"
class="form-control"
id="avatar"
name="avatar"
accept="image/jpeg,image/png,image/webp,image/gif">
<div class="mt-3 d-flex align-items-center gap-3">
<?php if (!empty($photousername)): ?>
<img
src="../upload/users/<?= htmlspecialchars($photousername, ENT_QUOTES, 'UTF-8'); ?>"
alt="Current Avatar"
class="profile-avatar-preview"
id="avatarPreview">
<?php else: ?>
<img
src="../upload/users/profile.png"
alt="Default Avatar"
class="profile-avatar-preview"
id="avatarPreview">
<?php endif; ?>
<div>
<div class="small-muted">Current avatar</div>
<div class="small-muted">Allowed formats: JPG, PNG, WEBP, GIF.</div>
</div>
</div>
</div>
<div class="col-md-6">
<label for="password" class="form-label">New Password</label>
<div class="input-group">
<input
type="password"
class="form-control"
id="password"
name="password"
autocomplete="new-password">
<button
class="btn btn-outline-secondary toggle-password"
type="button"
data-target="password"
aria-label="Show or hide password">
<i class="bx bx-show"></i>
</button>
</div>
</div>
<div class="col-md-6">
<label for="password_confirm" class="form-label">Confirm New Password</label>
<div class="input-group">
<input
type="password"
class="form-control"
id="password_confirm"
name="password_confirm"
autocomplete="new-password">
<button
class="btn btn-outline-secondary toggle-password"
type="button"
data-target="password_confirm"
aria-label="Show or hide password confirmation">
<i class="bx bx-show"></i>
</button>
</div>
</div>
<div class="col-md-12">
<div class="password-note" id="passwordNote">
<strong>Important:</strong> leave both password fields empty to keep your current password unchanged.
Type a new password only if you want to replace it.
</div>
<div class="password-error mt-2" id="passwordError">
The two password fields do not match. Please check them before saving.
</div>
<div class="password-success mt-2" id="passwordSuccess">
The two passwords match.
</div>
</div>
<input
type="hidden"
name="iduserlogin"
value="<?= htmlspecialchars($iduserlogin ?? '', ENT_QUOTES, 'UTF-8'); ?>">
<div class="col-md-12">
<button type="submit" class="btn btn-primary" id="submitProfileButton">
Update Profile
</button>
</div>
<div class="mb-3">
<label for="avatar" class="form-label">Profile Picture</label>
<input type="file" class="form-control" id="avatar" name="avatar">
<?php if ($photousername): ?>
<p>Current avatar: <img src="..//upload/users/<?= htmlspecialchars($photousername); ?>" alt="Current Avatar" style="max-width: 100px;"></p>
<?php else: ?>
<p>Current avatar: <img src="..//upload/users/profile.png" alt="Default Avatar" style="max-width: 100px;"></p>
<?php endif; ?>
</div> </div>
<div class="mb-3">
<label for="password" class="form-label">Password (leave blank to keep current)</label>
<input type="password" class="form-control" id="password" name="password">
</div>
<input type="hidden" name="iduserlogin" value="<?= htmlspecialchars($iduserlogin); ?>">
<button type="submit" class="btn btn-primary">Update Profile</button>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!--end page wrapper --> <!--end page wrapper -->
<!--start overlay--> <!--start overlay-->
<div class="overlay toggle-icon"></div> <div class="overlay toggle-icon"></div>
<!--end overlay--> <!--end overlay-->
<!--Start Back To Top Button--> <!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a> <a href="javaScript:;" class="back-to-top">
<i class='bx bxs-up-arrow-alt'></i>
</a>
<!--End Back To Top Button--> <!--End Back To Top Button-->
<?php include('include/footer.php'); ?> <?php include('include/footer.php'); ?>
</div> </div>
<!--end wrapper--> <!--end wrapper-->
@@ -99,7 +298,389 @@
<?php //include('include/themeswitcher.php'); <?php //include('include/themeswitcher.php');
?> ?>
<!--end switcher--> <!--end switcher-->
<?php include('jsinclude.php'); ?> <?php include('jsinclude.php'); ?>
<!-- Select2 JS -->
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const profileForm = document.getElementById('profileForm');
const submitProfileButton = document.getElementById('submitProfileButton');
const passwordInput = document.getElementById('password');
const passwordConfirmInput = document.getElementById('password_confirm');
const passwordError = document.getElementById('passwordError');
const passwordSuccess = document.getElementById('passwordSuccess');
const limsUserSelect = document.getElementById('lims_user_id');
const limsGlobalUserSelect = document.getElementById('lims_global_user_id');
const limsLoadingMessage = document.getElementById('limsLoadingMessage');
const avatarInput = document.getElementById('avatar');
const avatarPreview = document.getElementById('avatarPreview');
const currentLimsUserId = String(limsUserSelect.dataset.currentValue || '');
const currentLimsGlobalUserId = String(limsGlobalUserSelect.dataset.currentValue || '');
const limsGlobalUsersEndpoint = 'get_utenti.php';
const limsAcceptorsEndpoint = 'get_customfield_values.php?field_ids=244';
function validatePasswords(showEmptySuccess = false) {
const password = passwordInput.value;
const confirmPassword = passwordConfirmInput.value;
passwordError.style.display = 'none';
passwordSuccess.style.display = 'none';
passwordInput.classList.remove('is-invalid', 'is-valid');
passwordConfirmInput.classList.remove('is-invalid', 'is-valid');
/*
* Both empty means: keep current password.
*/
if (password === '' && confirmPassword === '') {
submitProfileButton.disabled = false;
return true;
}
/*
* One field filled and the other empty is not valid.
*/
if (password === '' || confirmPassword === '') {
passwordError.textContent = 'Please fill both password fields, or leave both empty to keep the current password.';
passwordError.style.display = 'block';
passwordInput.classList.add('is-invalid');
passwordConfirmInput.classList.add('is-invalid');
submitProfileButton.disabled = true;
return false;
}
/*
* Both filled but different.
*/
if (password !== confirmPassword) {
passwordError.textContent = 'The two password fields do not match. Please check them before saving.';
passwordError.style.display = 'block';
passwordInput.classList.add('is-invalid');
passwordConfirmInput.classList.add('is-invalid');
submitProfileButton.disabled = true;
return false;
}
/*
* Both filled and equal.
*/
passwordInput.classList.add('is-valid');
passwordConfirmInput.classList.add('is-valid');
passwordSuccess.style.display = showEmptySuccess ? 'block' : 'block';
submitProfileButton.disabled = false;
return true;
}
passwordInput.addEventListener('input', function() {
validatePasswords();
});
passwordConfirmInput.addEventListener('input', function() {
validatePasswords();
});
profileForm.addEventListener('submit', function(event) {
if (!validatePasswords(true)) {
event.preventDefault();
if (typeof Swal !== 'undefined') {
Swal.fire({
icon: 'error',
title: 'Password mismatch',
text: 'Please check the password fields before saving.'
});
}
return false;
}
return true;
});
function initSelect2() {
if (typeof $ === 'undefined') {
console.error('jQuery is not loaded. Select2 cannot start.');
return;
}
if (typeof $.fn.select2 === 'undefined') {
console.error('Select2 is not loaded. Check select2.min.js include.');
return;
}
$('#lims_user_id').select2({
width: '100%',
placeholder: 'Select acceptor',
allowClear: true,
minimumResultsForSearch: 0
});
$('#lims_global_user_id').select2({
width: '100%',
placeholder: 'Select LIMS user',
allowClear: true,
minimumResultsForSearch: 0
});
}
function destroySelect2IfActive(selector) {
if (typeof $ !== 'undefined' && $.fn.select2 && $(selector).hasClass('select2-hidden-accessible')) {
$(selector).select2('destroy');
}
}
function refreshSelect2() {
if (typeof $ === 'undefined' || typeof $.fn.select2 === 'undefined') {
return;
}
destroySelect2IfActive('#lims_user_id');
destroySelect2IfActive('#lims_global_user_id');
$('#lims_user_id').select2({
width: '100%',
placeholder: 'Select acceptor',
allowClear: true,
minimumResultsForSearch: 0
});
$('#lims_global_user_id').select2({
width: '100%',
placeholder: 'Select LIMS user',
allowClear: true,
minimumResultsForSearch: 0
});
$('#lims_user_id').trigger('change');
$('#lims_global_user_id').trigger('change');
}
function clearSelect(selectElement, placeholderText) {
selectElement.innerHTML = '';
const option = document.createElement('option');
option.value = '';
option.textContent = placeholderText;
selectElement.appendChild(option);
}
function appendOption(selectElement, value, text) {
if (value === null || value === undefined || value === '') {
return;
}
const option = document.createElement('option');
option.value = String(value);
option.textContent = text || String(value);
selectElement.appendChild(option);
}
function setSelectValue(selectElement, value) {
selectElement.value = value || '';
if (typeof $ !== 'undefined' && $.fn.select2) {
$(selectElement).trigger('change');
}
}
function normalizeLimsGlobalUsers(data) {
const list = Array.isArray(data) ? data : (data.value || []);
const normalized = [];
list.forEach(function(item) {
const id = item.IdUtente ?? item.id ?? item.ID ?? null;
const text = item.Nome || item.Nominativo || item.UserName || item.Email || id;
if (id !== null && id !== undefined && id !== '') {
normalized.push({
id: String(id),
text: String(text)
});
}
});
normalized.sort(function(a, b) {
return String(a.text).localeCompare(String(b.text), 'it', {
sensitivity: 'base'
});
});
return normalized;
}
function normalizeAcceptors(data) {
const list = data['244'] || data[244] || [];
const normalized = [];
list.forEach(function(item) {
const id =
item.IdCustomFieldValue ??
item.IdCustomFieldsValue ??
item.IdCustomFieldValues ??
item.Id ??
item.ID ??
item.id ??
item.ValueId ??
item.value_id ??
item.Codice ??
null;
const text =
item.Value ??
item.Valore ??
item.Nome ??
item.Name ??
item.Descrizione ??
item.Description ??
item.Text ??
item.text ??
item.Label ??
item.label ??
id;
if (id !== null && id !== undefined && id !== '') {
normalized.push({
id: String(id),
text: String(text)
});
}
});
normalized.sort(function(a, b) {
return String(a.text).localeCompare(String(b.text), 'it', {
sensitivity: 'base'
});
});
return normalized;
}
function populateLimsGlobalUsers(users) {
clearSelect(limsGlobalUserSelect, 'Select LIMS user');
users.forEach(function(user) {
appendOption(limsGlobalUserSelect, user.id, user.text);
});
if (currentLimsGlobalUserId !== '') {
setSelectValue(limsGlobalUserSelect, currentLimsGlobalUserId);
}
}
function populateAcceptors(acceptors) {
clearSelect(limsUserSelect, 'Select acceptor');
acceptors.forEach(function(item) {
appendOption(limsUserSelect, item.id, item.text);
});
if (currentLimsUserId !== '') {
setSelectValue(limsUserSelect, currentLimsUserId);
}
}
async function loadLimsDropdowns() {
try {
const [globalResponse, acceptorsResponse] = await Promise.all([
fetch(limsGlobalUsersEndpoint, {
cache: 'no-store'
}),
fetch(limsAcceptorsEndpoint, {
cache: 'no-store'
})
]);
if (!globalResponse.ok) {
throw new Error('Unable to load LIMS global users.');
}
if (!acceptorsResponse.ok) {
throw new Error('Unable to load LIMS acceptors.');
}
const globalData = await globalResponse.json();
const acceptorsData = await acceptorsResponse.json();
const globalUsers = normalizeLimsGlobalUsers(globalData);
const acceptors = normalizeAcceptors(acceptorsData);
populateLimsGlobalUsers(globalUsers);
populateAcceptors(acceptors);
refreshSelect2();
if (limsLoadingMessage) {
limsLoadingMessage.textContent = 'LIMS users and acceptors loaded.';
}
} catch (error) {
console.error(error);
if (limsLoadingMessage) {
limsLoadingMessage.textContent = 'Warning: unable to load LIMS users or acceptors.';
}
if (typeof Swal !== 'undefined') {
Swal.fire({
icon: 'warning',
title: 'LIMS lists not loaded',
text: error.message || 'Unable to load LIMS dropdown values.'
});
}
}
}
avatarInput.addEventListener('change', function(event) {
const file = event.target.files[0];
if (!file) {
return;
}
const reader = new FileReader();
reader.onload = function(e) {
avatarPreview.src = e.target.result;
};
reader.readAsDataURL(file);
});
document.querySelectorAll('.toggle-password').forEach(function(button) {
button.addEventListener('click', function() {
const targetId = button.dataset.target;
const input = document.getElementById(targetId);
const icon = button.querySelector('i');
if (!input) {
return;
}
if (input.type === 'password') {
input.type = 'text';
if (icon) {
icon.classList.remove('bx-show');
icon.classList.add('bx-hide');
}
} else {
input.type = 'password';
if (icon) {
icon.classList.remove('bx-hide');
icon.classList.add('bx-show');
}
}
});
});
initSelect2();
loadLimsDropdowns();
validatePasswords();
});
</script>
</body> </body>
</html> </html>
+123 -5
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()]);