Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9eb257d237 | |||
| d623ee797c | |||
| 4dd7b89c22 | |||
| dec42b4442 | |||
| d088364a0d | |||
| 6e43a178af | |||
| 25bd916221 | |||
| dab8d9aebf | |||
| 375a10a678 | |||
| 15990be884 | |||
| c3a6dd73b6 | |||
| 44ed1186e0 | |||
| 9050cb1006 | |||
| e6820fdb62 | |||
| 5da37a7836 | |||
| c5f27cb69a | |||
| 1d81d6c996 | |||
| 0c72dbf5ae | |||
| 8455be04e1 | |||
| e42d1b9c51 | |||
| 3e69e3c322 | |||
| 0eb4f7a2ad | |||
| 4f2cfc1930 | |||
| df075dd76a | |||
| 6460454201 | |||
| 574ddbbd32 | |||
| 41f414db5c | |||
| 4a863e8c16 | |||
| b431f1d4e9 | |||
| f97b52f158 | |||
| 836fc055ec | |||
| e8dd585df4 | |||
| 198b8c08ad | |||
| 28c467d55e | |||
| 56eee99a67 | |||
| f514b3d2c7 | |||
| a3eb0f0a57 | |||
| aa355905d7 | |||
| b38f3e1240 | |||
| cbd0c5b68a | |||
| f3e5cb4ffd | |||
| 0f0c6a04b7 | |||
| 813bd66f96 | |||
| ce00247d1c | |||
| 3a7dd266c8 | |||
| ba8dc4c721 | |||
| cfbbc36116 | |||
| 50d578eea1 | |||
| 6b0d2aa9b9 | |||
| 67bbd9bbbb | |||
| fa7997c980 | |||
| 66be442eb6 | |||
| 19a2d6e3f7 | |||
| a15ab08576 | |||
| f71e8a56b5 | |||
| cb38bfb75a | |||
| 28a708dad3 | |||
| 6b9cf20ab9 | |||
| d40fc7d177 |
+7
-1
@@ -40,10 +40,14 @@ yarn-error.log
|
||||
/public/userarea/logaspi/
|
||||
/public/userarea/logs/api/
|
||||
/public/userarea/logs/api/**
|
||||
/public/userarea/logs/
|
||||
/public/userarea/logs/**
|
||||
/public/userarea/photostrf/
|
||||
/public/userarea/class/*.log
|
||||
/public/userarea/class/curl_auth_debug.log
|
||||
/public/userarea/class/curl_request_debug.log
|
||||
/public/userarea/schema_dettagli_response.json
|
||||
public/userarea/schemi_base_response.json
|
||||
|
||||
# File XLSX temporanei importati
|
||||
/public/userarea/imported_trf/*.xlsx
|
||||
@@ -54,4 +58,6 @@ yarn-error.log
|
||||
/public/photostrf/qrcodes/
|
||||
|
||||
# Ignora tutti i log ovunque
|
||||
*.log
|
||||
*.log
|
||||
|
||||
public/userarea/cache/
|
||||
@@ -431,7 +431,7 @@
|
||||
const emptyEl = modal.querySelector("#analysisEmptyBox");
|
||||
const errorEl = modal.querySelector("#analysisErrorBox");
|
||||
|
||||
const webOnly = webOnlyEl ? webOnlyEl.checked : false;
|
||||
const webOnly = true;
|
||||
const searchValue = searchEl ? searchEl.value.trim().toLowerCase() : "";
|
||||
|
||||
let visibleCount = 0;
|
||||
@@ -496,8 +496,10 @@
|
||||
emptyEl.classList.add("d-none");
|
||||
}
|
||||
|
||||
if (analysisLoadedCache[String(matrixId)]) {
|
||||
renderAnalysesList(analysisLoadedCache[String(matrixId)]);
|
||||
const cacheKey = String(matrixId) + "_WEB_ONLY";
|
||||
|
||||
if (analysisLoadedCache[cacheKey]) {
|
||||
renderAnalysesList(analysisLoadedCache[cacheKey]);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -509,13 +511,21 @@
|
||||
dataType: "json",
|
||||
data: {
|
||||
id_matrice: matrixId,
|
||||
web_only: 1,
|
||||
},
|
||||
})
|
||||
.done(function (response) {
|
||||
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);
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
@@ -674,12 +684,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
const webOnlyEl = modal.querySelector("#analysisWebOnly");
|
||||
if (webOnlyEl) {
|
||||
webOnlyEl.addEventListener("change", function () {
|
||||
filterAnalysisList();
|
||||
});
|
||||
}
|
||||
// WEB only is now fixed by default
|
||||
|
||||
const searchEl = modal.querySelector("#analysisSearchInput");
|
||||
if (searchEl) {
|
||||
|
||||
@@ -22,6 +22,8 @@ $(document).ready(function () {
|
||||
let partsListData = [];
|
||||
// DIMENSIONE GLOBALE MARKER
|
||||
let globalMarkerSize = 16;
|
||||
// COLORE TESTO LISTA DESCRIZIONI
|
||||
let globalDescriptionColor = "#000000";
|
||||
|
||||
// ===================
|
||||
// MODAL INITIALIZATION
|
||||
@@ -138,6 +140,7 @@ $(document).ready(function () {
|
||||
descriptionTextbox = null;
|
||||
markerObjects = {};
|
||||
globalMarkerSize = 16;
|
||||
globalDescriptionColor = "#000000";
|
||||
$("#photoSelectorContainerAnnotations").empty().hide();
|
||||
$("#samplePhotoAnnotations").attr("src", "");
|
||||
$("#partsListAnnotations").empty();
|
||||
@@ -177,7 +180,25 @@ $(document).ready(function () {
|
||||
updateMarkers();
|
||||
markUnsaved();
|
||||
});
|
||||
// ===================
|
||||
// COLORE LISTA DESCRIZIONI
|
||||
// ===================
|
||||
$(document)
|
||||
.off("input.descriptionColor", "#descriptionColorPickerAnnotations")
|
||||
.on(
|
||||
"input.descriptionColor",
|
||||
"#descriptionColorPickerAnnotations",
|
||||
function () {
|
||||
globalDescriptionColor = $(this).val();
|
||||
|
||||
if (descriptionTextbox && fabricCanvas) {
|
||||
descriptionTextbox.set("fill", globalDescriptionColor);
|
||||
fabricCanvas.renderAll();
|
||||
}
|
||||
|
||||
markUnsaved();
|
||||
},
|
||||
);
|
||||
// ===================
|
||||
// PHOTO LOADERS
|
||||
// ===================
|
||||
@@ -614,17 +635,11 @@ $(document).ready(function () {
|
||||
partsListData.forEach((part) => {
|
||||
const partNumber = part.part_number;
|
||||
const partDescription = part.part_description;
|
||||
const isMixPart = String(part.mix || "N").toUpperCase() === "Y";
|
||||
|
||||
const partColor =
|
||||
partColors[partNumber] ||
|
||||
(partDescription.toLowerCase().startsWith("mix")
|
||||
? "#0000ff"
|
||||
: "#ff0000");
|
||||
if (
|
||||
partNumber &&
|
||||
partDescription &&
|
||||
(showMixParts ||
|
||||
!partDescription.toLowerCase().startsWith("mix"))
|
||||
) {
|
||||
partColors[partNumber] || (isMixPart ? "#0000ff" : "#ff0000");
|
||||
if (partNumber && partDescription && (showMixParts || !isMixPart)) {
|
||||
const colorOptions = predefinedColors
|
||||
.map(
|
||||
(color) =>
|
||||
@@ -922,11 +937,10 @@ $(document).ready(function () {
|
||||
) {
|
||||
partsListData = response.parts;
|
||||
response.parts.forEach((part) => {
|
||||
const defaultColor = part.part_description
|
||||
.toLowerCase()
|
||||
.startsWith("mix")
|
||||
? "#0000ff"
|
||||
: "#ff0000";
|
||||
const isMixPart =
|
||||
String(part.mix || "N").toUpperCase() === "Y";
|
||||
const defaultColor = isMixPart ? "#0000ff" : "#ff0000";
|
||||
|
||||
partColors[part.part_number] = defaultColor;
|
||||
});
|
||||
updatePartsList();
|
||||
@@ -1003,11 +1017,10 @@ $(document).ready(function () {
|
||||
(p) => p.part_number == marker.partNumber,
|
||||
);
|
||||
const partDescription = part ? part.part_description : "";
|
||||
if (
|
||||
!showMixParts &&
|
||||
partDescription &&
|
||||
partDescription.toLowerCase().startsWith("mix")
|
||||
) {
|
||||
const isMixPart =
|
||||
part && String(part.mix || "N").toUpperCase() === "Y";
|
||||
|
||||
if (!showMixParts && isMixPart) {
|
||||
console.log("Ignoro marker per parte Mix:", marker.partNumber);
|
||||
return;
|
||||
}
|
||||
@@ -1115,11 +1128,10 @@ $(document).ready(function () {
|
||||
}
|
||||
|
||||
const partsList = partsListData
|
||||
.filter(
|
||||
(part) =>
|
||||
showMixParts ||
|
||||
!part.part_description.toLowerCase().startsWith("mix"),
|
||||
)
|
||||
.filter((part) => {
|
||||
const isMixPart = String(part.mix || "N").toUpperCase() === "Y";
|
||||
return showMixParts || !isMixPart;
|
||||
})
|
||||
.map((part) => `${part.part_number} ${part.part_description}`);
|
||||
|
||||
const text = partsList.join("\n");
|
||||
@@ -1139,7 +1151,7 @@ $(document).ready(function () {
|
||||
backgroundColor: "transparent",
|
||||
fontFamily: "Arial",
|
||||
fontSize: Math.max(16, Math.round(globalMarkerSize * 0.8)),
|
||||
fill: "#000000",
|
||||
fill: globalDescriptionColor,
|
||||
padding: 10,
|
||||
editable: false,
|
||||
selectable: true,
|
||||
|
||||
@@ -3,6 +3,9 @@ ob_start();
|
||||
session_start();
|
||||
require_once '../../vendor/autoload.php';
|
||||
|
||||
Dotenv\Dotenv::createImmutable(dirname(__DIR__, 2))->safeLoad();
|
||||
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
|
||||
|
||||
$response = ['error' => '', 'rows' => [], 'columns' => [], 'template_id' => 0, 'filename' => '', 'excel_data' => []];
|
||||
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
// Filtro opzionale per template.
|
||||
$templateFilter = isset($_GET['template_id']) ? intval($_GET['template_id']) : 0;
|
||||
|
||||
$templates = $pdo->query("SELECT id, name FROM excel_templates ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$where = '';
|
||||
$params = [];
|
||||
if ($templateFilter > 0) {
|
||||
$where = 'WHERE b.template_id = ?';
|
||||
$params[] = $templateFilter;
|
||||
}
|
||||
|
||||
$sql = "SELECT b.id, b.template_id, b.mapping_id, b.field_id, b.json_value,
|
||||
b.lims_value_id, b.lims_value, b.updated_at,
|
||||
t.name AS template_name,
|
||||
m.field_label
|
||||
FROM json_lims_binding b
|
||||
LEFT JOIN excel_templates t ON t.id = b.template_id
|
||||
LEFT JOIN template_mapping m ON m.id = b.mapping_id
|
||||
$where
|
||||
ORDER BY t.name, m.field_label, b.json_value";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$bindings = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
<?php include('cssinclude.php'); ?>
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.json-value {
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
td .select2-container {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.row-status {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
<title>Gestione Binding JSON -> LIMS - <?= htmlspecialchars($titlewebsite ?? '', ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<h6 class="mb-0">Gestione Binding JSON -> LIMS</h6>
|
||||
<form method="GET" class="d-flex align-items-center gap-2">
|
||||
<label class="small text-muted mb-0">Template</label>
|
||||
<select name="template_id" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||
<option value="0">Tutti</option>
|
||||
<?php foreach ($templates as $t): ?>
|
||||
<option value="<?= (int) $t['id'] ?>" <?= $templateFilter === (int) $t['id'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($t['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div id="globalError" class="alert alert-danger" style="display:none;"></div>
|
||||
|
||||
<div class="alert alert-secondary small">
|
||||
Le modifiche ai binding si applicano alle <strong>importazioni future</strong>.
|
||||
I record gia' importati non vengono ricalcolati.
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Template</th>
|
||||
<th>Campo (template_mapping)</th>
|
||||
<th>Valore JSON</th>
|
||||
<th>Valore LIMS</th>
|
||||
<th style="width:170px;">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($bindings)): ?>
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted">Nessun binding presente.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($bindings as $b): ?>
|
||||
<tr data-id="<?= (int) $b['id'] ?>"
|
||||
data-mapping-id="<?= (int) $b['mapping_id'] ?>"
|
||||
data-field-id="<?= (int) $b['field_id'] ?>"
|
||||
data-template-id="<?= (int) $b['template_id'] ?>"
|
||||
data-json-value="<?= htmlspecialchars($b['json_value'], ENT_QUOTES) ?>">
|
||||
<td><?= htmlspecialchars($b['template_name'] ?? ('#' . $b['template_id'])) ?></td>
|
||||
<td><?= htmlspecialchars($b['field_label'] ?? ('mapping ' . $b['mapping_id'])) ?></td>
|
||||
<td class="json-value"><?= htmlspecialchars($b['json_value']) ?></td>
|
||||
<td>
|
||||
<select class="form-select binding-select" data-field-id="<?= (int) $b['field_id'] ?>">
|
||||
<?php if ($b['lims_value_id']): ?>
|
||||
<option value="<?= (int) $b['lims_value_id'] ?>" selected>
|
||||
<?= htmlspecialchars($b['lims_value']) ?>
|
||||
</option>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
<span class="row-status text-muted"></span>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-success save-binding-btn" disabled>
|
||||
<i class="fas fa-save"></i> Salva
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger delete-binding-btn">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay toggle-icon"></div>
|
||||
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
const $globalError = $('#globalError');
|
||||
|
||||
$('.binding-select').each(function() {
|
||||
const fieldId = $(this).data('field-id');
|
||||
const initialVal = $(this).val();
|
||||
|
||||
$(this).select2({
|
||||
width: '220px',
|
||||
ajax: {
|
||||
url: 'search_customfield_values.php',
|
||||
dataType: 'json',
|
||||
delay: 200,
|
||||
data: params => ({
|
||||
field_id: fieldId,
|
||||
q: params.term || '',
|
||||
limit: 50
|
||||
}),
|
||||
processResults: data => ({
|
||||
results: data.results || []
|
||||
})
|
||||
},
|
||||
minimumInputLength: 0
|
||||
});
|
||||
|
||||
// Abilito Salva solo quando il valore cambia davvero.
|
||||
$(this).data('original-val', initialVal);
|
||||
});
|
||||
|
||||
$('.binding-select').on('change', function() {
|
||||
const $row = $(this).closest('tr');
|
||||
const changed = String($(this).val()) !== String($(this).data('original-val'));
|
||||
$row.find('.save-binding-btn').prop('disabled', !changed);
|
||||
});
|
||||
|
||||
$('.save-binding-btn').on('click', function() {
|
||||
const $row = $(this).closest('tr');
|
||||
const $select = $row.find('.binding-select');
|
||||
const selectedData = $select.select2('data')[0] || {};
|
||||
const $status = $row.find('.row-status');
|
||||
const $btn = $(this);
|
||||
|
||||
$globalError.hide();
|
||||
$btn.prop('disabled', true);
|
||||
$status.text('Salvataggio...').removeClass('text-success text-danger').addClass('text-muted');
|
||||
|
||||
$.post('save_binding.php', {
|
||||
mapping_id: $row.data('mapping-id'),
|
||||
field_id: $row.data('field-id'),
|
||||
template_id: $row.data('template-id'),
|
||||
json_value: String($row.data('json-value')),
|
||||
lims_value_id: $select.val(),
|
||||
lims_value: selectedData.text || ''
|
||||
}).then(resp => {
|
||||
if (resp && resp.success) {
|
||||
$status.text('Salvato').removeClass('text-muted').addClass('text-success');
|
||||
$select.data('original-val', $select.val());
|
||||
} else {
|
||||
throw new Error((resp && resp.error) || 'Errore salvataggio');
|
||||
}
|
||||
}).catch(err => {
|
||||
$status.text('Errore').removeClass('text-muted').addClass('text-danger');
|
||||
$globalError.text(err.message || 'Errore durante il salvataggio.').show();
|
||||
$btn.prop('disabled', false);
|
||||
});
|
||||
});
|
||||
|
||||
$('.delete-binding-btn').on('click', function() {
|
||||
if (!confirm('Eliminare questo binding?')) return;
|
||||
const $row = $(this).closest('tr');
|
||||
const $btn = $(this);
|
||||
$globalError.hide();
|
||||
$btn.prop('disabled', true);
|
||||
|
||||
$.post('delete_binding.php', {
|
||||
id: $row.data('id')
|
||||
}).then(resp => {
|
||||
if (resp && resp.success) {
|
||||
$row.fadeOut(200, () => $row.remove());
|
||||
} else {
|
||||
throw new Error((resp && resp.error) || 'Errore eliminazione');
|
||||
}
|
||||
}).catch(err => {
|
||||
$globalError.text(err.message || 'Errore durante l\'eliminazione.').show();
|
||||
$btn.prop('disabled', false);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -255,4 +255,60 @@ class VisualLimsApiClient
|
||||
{
|
||||
return $this->baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get raw/binary content from VisualLims API.
|
||||
* Used for PDF downloads from MediaFile/DownloadStream.
|
||||
*/
|
||||
public function getRaw($endpoint)
|
||||
{
|
||||
$token = $this->getToken();
|
||||
|
||||
/*
|
||||
* Normal JSON OData calls use:
|
||||
* {$this->baseUrl}/api/odata/...
|
||||
*
|
||||
* Media file downloads use:
|
||||
* {$this->baseUrl}/api/MediaFile/DownloadStream...
|
||||
*/
|
||||
$url = rtrim($this->baseUrl, '/') . '/api/' . ltrim($endpoint, '/');
|
||||
|
||||
$ch = curl_init($url);
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
"Authorization: Bearer {$token}",
|
||||
"Accept: application/pdf,*/*"
|
||||
],
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_TIMEOUT => 60
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
$curlError = curl_error($ch);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
if ($response === false) {
|
||||
throw new Exception("Errore cURL download raw: " . $curlError);
|
||||
}
|
||||
|
||||
if ($httpCode < 200 || $httpCode >= 300) {
|
||||
throw new Exception(
|
||||
"Errore HTTP {$httpCode} durante download raw. Content-Type: {$contentType}. Response: " .
|
||||
substr($response, 0, 500)
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($response)) {
|
||||
throw new Exception("Risposta vuota dal download raw.");
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
// Helpers for JSON -> LIMS value bindings (table json_lims_binding).
|
||||
|
||||
function binding_is_list_field(array $mapping): bool
|
||||
{
|
||||
$hasList = (int) ($mapping['has_list'] ?? 0) === 1;
|
||||
$isMultiChoice = strcasecmp(trim((string) ($mapping['data_type'] ?? '')), 'SceltaMultipla') === 0;
|
||||
return $hasList || $isMultiChoice;
|
||||
}
|
||||
|
||||
function binding_lookup(PDO $pdo, int $mappingId, string $jsonValue): ?array
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT id, lims_value_id, lims_value
|
||||
FROM json_lims_binding
|
||||
WHERE mapping_id = ? AND json_value = ?
|
||||
LIMIT 1"
|
||||
);
|
||||
$stmt->execute([$mappingId, $jsonValue]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
function binding_upsert(
|
||||
PDO $pdo,
|
||||
int $templateId,
|
||||
int $mappingId,
|
||||
int $fieldId,
|
||||
string $jsonValue,
|
||||
int $limsValueId,
|
||||
string $limsValue,
|
||||
?int $createdBy
|
||||
): void {
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO json_lims_binding
|
||||
(template_id, mapping_id, field_id, json_value, lims_value_id, lims_value, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
lims_value_id = VALUES(lims_value_id),
|
||||
lims_value = VALUES(lims_value),
|
||||
field_id = VALUES(field_id),
|
||||
template_id = VALUES(template_id)"
|
||||
);
|
||||
$stmt->execute([$templateId, $mappingId, $fieldId, $jsonValue, $limsValueId, $limsValue, $createdBy]);
|
||||
}
|
||||
|
||||
// LIMS list values for a field, reusing the cache/customfield_{id}.json cache (1h).
|
||||
function binding_get_lims_values(int $fieldId): array
|
||||
{
|
||||
static $memo = [];
|
||||
if ($fieldId <= 0) {
|
||||
return [];
|
||||
}
|
||||
if (array_key_exists($fieldId, $memo)) {
|
||||
return $memo[$fieldId];
|
||||
}
|
||||
|
||||
$cacheDir = dirname(__DIR__) . '/cache';
|
||||
$cacheFile = $cacheDir . '/customfield_' . $fieldId . '.json';
|
||||
|
||||
try {
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
|
||||
$values = json_decode(file_get_contents($cacheFile), true);
|
||||
} else {
|
||||
require_once __DIR__ . '/VisualLimsApiClient.class.php';
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
$data = $api->get("CustomField($fieldId)?\$expand=CustomFieldsValues");
|
||||
$values = $data['CustomFieldsValues'] ?? [];
|
||||
if (!is_dir($cacheDir)) {
|
||||
mkdir($cacheDir, 0777, true);
|
||||
}
|
||||
file_put_contents($cacheFile, json_encode($values));
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log("binding_get_lims_values failed for field $fieldId: " . $e->getMessage());
|
||||
$values = [];
|
||||
}
|
||||
|
||||
if (!is_array($values)) {
|
||||
$values = [];
|
||||
}
|
||||
|
||||
return $memo[$fieldId] = $values;
|
||||
}
|
||||
|
||||
// Exactly one case-insensitive match by Valore -> that value, otherwise null.
|
||||
function binding_auto_match(array $limsValues, string $jsonValue): ?array
|
||||
{
|
||||
$needle = mb_strtolower(trim($jsonValue));
|
||||
if ($needle === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$matches = [];
|
||||
foreach ($limsValues as $v) {
|
||||
$valore = (string) ($v['Valore'] ?? '');
|
||||
if (mb_strtolower(trim($valore)) === $needle) {
|
||||
$matches[] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
return count($matches) === 1 ? $matches[0] : null;
|
||||
}
|
||||
|
||||
// Scrive il valore risolto sui record indicati (datadb_ids gia' delimita l'import corrente).
|
||||
function binding_apply_to_details(
|
||||
PDO $pdo,
|
||||
int $mappingId,
|
||||
string $limsValue,
|
||||
array $datadbIds
|
||||
): int {
|
||||
$datadbIds = array_values(array_filter(array_map('intval', $datadbIds)));
|
||||
if (empty($datadbIds)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($datadbIds), '?'));
|
||||
$sql = "UPDATE import_data_details
|
||||
SET field_value = ?
|
||||
WHERE mapping_id = ?
|
||||
AND id IN ($placeholders)";
|
||||
|
||||
$params = array_merge([$limsValue, $mappingId], $datadbIds);
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt->rowCount();
|
||||
}
|
||||
@@ -3,6 +3,9 @@ require_once dirname(__DIR__, 3) . '/vendor/autoload.php';
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
Dotenv::createImmutable(dirname(__DIR__, 3))->safeLoad();
|
||||
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
|
||||
|
||||
class DBHandlerSelect
|
||||
{
|
||||
private static $instance = null;
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include('include/headscript.php');
|
||||
|
||||
$dbHandler = DBHandlerSelect::getInstance();
|
||||
$pdo = $dbHandler->getConnection();
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$sourceIddatadb = isset($data['source_iddatadb']) ? (int)$data['source_iddatadb'] : 0;
|
||||
$targetList = $data['target_iddatadb_list'] ?? [];
|
||||
|
||||
$targetIds = array_values(array_unique(array_filter(array_map('intval', (array)$targetList), function ($v) use ($sourceIddatadb) {
|
||||
return $v > 0 && $v !== $sourceIddatadb;
|
||||
})));
|
||||
|
||||
if ($sourceIddatadb <= 0 || empty($targetIds)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Missing source or target records'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// 1. Load source parts
|
||||
$stmtParts = $pdo->prepare("
|
||||
SELECT id, part_number, part_description, mix, idmatrice, note, dateexpiry
|
||||
FROM identification_parts
|
||||
WHERE iddatadb = ?
|
||||
AND part_description IS NOT NULL
|
||||
AND TRIM(part_description) <> ''
|
||||
ORDER BY part_number ASC, id ASC
|
||||
");
|
||||
$stmtParts->execute([$sourceIddatadb]);
|
||||
$sourceParts = $stmtParts->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($sourceParts)) {
|
||||
$pdo->rollBack();
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'No parts found for source record'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. Prepare statements
|
||||
$stmtInsertPart = $pdo->prepare("
|
||||
INSERT INTO identification_parts
|
||||
(iddatadb, part_number, part_description, mix, idmatrice, note, dateexpiry, created_at, updated_at)
|
||||
VALUES
|
||||
(:iddatadb, :part_number, :part_description, :mix, :idmatrice, :note, :dateexpiry, NOW(), NOW())
|
||||
");
|
||||
|
||||
$stmtLoadCF = $pdo->prepare("
|
||||
SELECT field_id, value_id, value_text
|
||||
FROM identification_parts_customfields
|
||||
WHERE part_id = ?
|
||||
ORDER BY id ASC
|
||||
");
|
||||
|
||||
$stmtInsertCF = $pdo->prepare("
|
||||
INSERT INTO identification_parts_customfields
|
||||
(part_id, field_id, value_id, value_text, created_at, updated_at)
|
||||
VALUES
|
||||
(:part_id, :field_id, :value_id, :value_text, NOW(), NOW())
|
||||
");
|
||||
|
||||
$details = [];
|
||||
$totalClonedParts = 0;
|
||||
|
||||
// 3. Clone source parts to each target record
|
||||
foreach ($targetIds as $targetIddatadb) {
|
||||
$clonedCountForTarget = 0;
|
||||
|
||||
foreach ($sourceParts as $part) {
|
||||
$stmtInsertPart->execute([
|
||||
':iddatadb' => $targetIddatadb,
|
||||
':part_number' => $part['part_number'],
|
||||
':part_description' => $part['part_description'],
|
||||
':mix' => $part['mix'] ?? 'N',
|
||||
':idmatrice' => $part['idmatrice'] !== '' ? $part['idmatrice'] : null,
|
||||
':note' => $part['note'] ?? null,
|
||||
':dateexpiry' => $part['dateexpiry'] ?? null,
|
||||
]);
|
||||
|
||||
$newPartId = (int)$pdo->lastInsertId();
|
||||
|
||||
// Load source custom fields for this part
|
||||
$stmtLoadCF->execute([(int)$part['id']]);
|
||||
$customFields = $stmtLoadCF->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($customFields as $cf) {
|
||||
$stmtInsertCF->execute([
|
||||
':part_id' => $newPartId,
|
||||
':field_id' => (int)$cf['field_id'],
|
||||
':value_id' => ($cf['value_id'] !== null && $cf['value_id'] !== '') ? (int)$cf['value_id'] : null,
|
||||
':value_text' => $cf['value_text'] !== '' ? $cf['value_text'] : null,
|
||||
]);
|
||||
}
|
||||
|
||||
$clonedCountForTarget++;
|
||||
$totalClonedParts++;
|
||||
}
|
||||
|
||||
$details[] = [
|
||||
'target_iddatadb' => $targetIddatadb,
|
||||
'cloned_parts' => $clonedCountForTarget
|
||||
];
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'source_iddatadb' => $sourceIddatadb,
|
||||
'cloned_targets' => count($targetIds),
|
||||
'total_cloned_parts' => $totalClonedParts,
|
||||
'details' => $details
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Clone failed: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
require_once 'class/db-functions.php';
|
||||
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
die("Invalid template ID");
|
||||
}
|
||||
|
||||
$sourceTemplateId = (int)$_GET['id'];
|
||||
|
||||
try {
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// 1. Get source template
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
name,
|
||||
source_type,
|
||||
header_row,
|
||||
start_column,
|
||||
description,
|
||||
target_table,
|
||||
sample_xlsx,
|
||||
button_size,
|
||||
button_bg_color,
|
||||
button_text_color,
|
||||
button_label,
|
||||
status,
|
||||
client_specific_fields,
|
||||
idclient,
|
||||
clientname,
|
||||
schemaname,
|
||||
idschema,
|
||||
schemajson,
|
||||
xls_headers,
|
||||
api_sample_json,
|
||||
json_nodes,
|
||||
idroutine
|
||||
FROM excel_templates
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$sourceTemplateId]);
|
||||
$template = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$template) {
|
||||
throw new Exception("Template not found.");
|
||||
}
|
||||
|
||||
// 2. Generate unique clone name
|
||||
$baseName = 'Copia di ' . $template['name'];
|
||||
$newName = $baseName;
|
||||
|
||||
$checkStmt = $pdo->prepare("SELECT COUNT(*) FROM excel_templates WHERE name = ?");
|
||||
$checkStmt->execute([$newName]);
|
||||
|
||||
if ((int)$checkStmt->fetchColumn() > 0) {
|
||||
$counter = 2;
|
||||
|
||||
do {
|
||||
$newName = $baseName . ' (' . $counter . ')';
|
||||
$checkStmt->execute([$newName]);
|
||||
$exists = (int)$checkStmt->fetchColumn() > 0;
|
||||
$counter++;
|
||||
} while ($exists);
|
||||
}
|
||||
|
||||
// 3. Insert cloned template
|
||||
$insertTemplate = $pdo->prepare("
|
||||
INSERT INTO excel_templates (
|
||||
name,
|
||||
source_type,
|
||||
created_at,
|
||||
updated_at,
|
||||
header_row,
|
||||
start_column,
|
||||
description,
|
||||
target_table,
|
||||
sample_xlsx,
|
||||
button_size,
|
||||
button_bg_color,
|
||||
button_text_color,
|
||||
button_label,
|
||||
status,
|
||||
client_specific_fields,
|
||||
idclient,
|
||||
clientname,
|
||||
schemaname,
|
||||
idschema,
|
||||
schemajson,
|
||||
xls_headers,
|
||||
api_sample_json,
|
||||
json_nodes,
|
||||
idroutine
|
||||
) VALUES (
|
||||
:name,
|
||||
:source_type,
|
||||
NOW(),
|
||||
NOW(),
|
||||
:header_row,
|
||||
:start_column,
|
||||
:description,
|
||||
:target_table,
|
||||
:sample_xlsx,
|
||||
:button_size,
|
||||
:button_bg_color,
|
||||
:button_text_color,
|
||||
:button_label,
|
||||
:status,
|
||||
:client_specific_fields,
|
||||
:idclient,
|
||||
:clientname,
|
||||
:schemaname,
|
||||
:idschema,
|
||||
:schemajson,
|
||||
:xls_headers,
|
||||
:api_sample_json,
|
||||
:json_nodes,
|
||||
:idroutine
|
||||
)
|
||||
");
|
||||
|
||||
$insertTemplate->execute([
|
||||
':name' => $newName,
|
||||
':source_type' => $template['source_type'],
|
||||
':header_row' => $template['header_row'],
|
||||
':start_column' => $template['start_column'],
|
||||
':description' => $template['description'],
|
||||
':target_table' => $template['target_table'],
|
||||
':sample_xlsx' => $template['sample_xlsx'],
|
||||
':button_size' => $template['button_size'],
|
||||
':button_bg_color' => $template['button_bg_color'],
|
||||
':button_text_color' => $template['button_text_color'],
|
||||
':button_label' => $template['button_label'],
|
||||
':status' => $template['status'],
|
||||
':client_specific_fields' => $template['client_specific_fields'],
|
||||
':idclient' => $template['idclient'],
|
||||
':clientname' => $template['clientname'],
|
||||
':schemaname' => $template['schemaname'],
|
||||
':idschema' => $template['idschema'],
|
||||
':schemajson' => $template['schemajson'],
|
||||
':xls_headers' => $template['xls_headers'],
|
||||
':api_sample_json' => $template['api_sample_json'],
|
||||
':json_nodes' => $template['json_nodes'],
|
||||
':idroutine' => $template['idroutine']
|
||||
]);
|
||||
|
||||
$newTemplateId = (int)$pdo->lastInsertId();
|
||||
|
||||
if ($newTemplateId <= 0) {
|
||||
throw new Exception("Unable to create cloned template.");
|
||||
}
|
||||
|
||||
// 4. Clone template_mapping rows
|
||||
$cloneMappings = $pdo->prepare("
|
||||
INSERT INTO template_mapping (
|
||||
template_id,
|
||||
schema_id,
|
||||
field_id,
|
||||
excel_column,
|
||||
json_node,
|
||||
is_manual,
|
||||
manual_default,
|
||||
auto_value,
|
||||
data_type,
|
||||
is_required,
|
||||
default_value,
|
||||
has_list,
|
||||
length,
|
||||
decimals,
|
||||
min_value,
|
||||
max_value,
|
||||
default_curr_date,
|
||||
tablename,
|
||||
field_label,
|
||||
main_field,
|
||||
is_visible_import,
|
||||
is_visible_parts
|
||||
)
|
||||
SELECT
|
||||
:new_template_id,
|
||||
schema_id,
|
||||
field_id,
|
||||
excel_column,
|
||||
json_node,
|
||||
is_manual,
|
||||
manual_default,
|
||||
auto_value,
|
||||
data_type,
|
||||
is_required,
|
||||
default_value,
|
||||
has_list,
|
||||
length,
|
||||
decimals,
|
||||
min_value,
|
||||
max_value,
|
||||
default_curr_date,
|
||||
tablename,
|
||||
field_label,
|
||||
main_field,
|
||||
is_visible_import,
|
||||
is_visible_parts
|
||||
FROM template_mapping
|
||||
WHERE template_id = :source_template_id
|
||||
");
|
||||
|
||||
$cloneMappings->execute([
|
||||
':new_template_id' => $newTemplateId,
|
||||
':source_template_id' => $sourceTemplateId
|
||||
]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
header("Location: templates_dashboard.php?cloned=1&new_id=" . $newTemplateId);
|
||||
exit;
|
||||
} catch (Exception $e) {
|
||||
if (isset($pdo) && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
die("Clone error: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'));
|
||||
}
|
||||
@@ -17,6 +17,8 @@ try {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
|
||||
|
||||
// Recupera le variabili d'ambiente
|
||||
$dbHost = $_ENV['DB_HOST'];
|
||||
$dbName = $_ENV['DB_DATABASE'];
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
// Elimina un binding JSON -> LIMS. Ritorna JSON.
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
include dirname(__DIR__) . '/../extra/auth.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
if (!Auth::check()) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = intval($_POST['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
http_response_code(422);
|
||||
echo json_encode(['success' => false, 'error' => 'Missing id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
$stmt = $pdo->prepare("DELETE FROM json_lims_binding WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
echo json_encode(['success' => true, 'deleted' => $stmt->rowCount()]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
/**
|
||||
* Uso:
|
||||
* download_rapporto_file.php?id=123
|
||||
*/
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
$id = isset($_GET['id']) ? intval($_GET['id']) : 0;
|
||||
|
||||
if (!$id) {
|
||||
throw new Exception("ID RapportoFile mancante");
|
||||
}
|
||||
|
||||
$debugDir = __DIR__ . '/logs/lims_get_commessa/';
|
||||
|
||||
if (!is_dir($debugDir)) {
|
||||
mkdir($debugDir, 0755, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recupero il record RapportoFile.
|
||||
*/
|
||||
$data = $api->get("RapportoFile({$id})", []);
|
||||
|
||||
file_put_contents(
|
||||
$debugDir . "rapporto_file_{$id}.json",
|
||||
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
|
||||
/**
|
||||
* Provo vari nomi campo possibili per il contenuto PDF.
|
||||
* Dopo il primo test potremo adattarlo al nome esatto.
|
||||
*/
|
||||
$possibleContentFields = [
|
||||
'File',
|
||||
'file',
|
||||
'Content',
|
||||
'content',
|
||||
'FileContent',
|
||||
'fileContent',
|
||||
'Contenuto',
|
||||
'contenuto',
|
||||
'Bytes',
|
||||
'bytes'
|
||||
];
|
||||
|
||||
$base64 = null;
|
||||
|
||||
foreach ($possibleContentFields as $field) {
|
||||
if (!empty($data[$field])) {
|
||||
$base64 = $data[$field];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$base64) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Record RapportoFile recuperato, ma non ho trovato un campo base64 del PDF.',
|
||||
'hint' => 'Apri logs/lims_get_commessa/rapporto_file_' . $id . '.json e controlla il nome reale del campo file.',
|
||||
'data' => $data
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Se il contenuto ha prefisso data URI, lo pulisco.
|
||||
*/
|
||||
if (strpos($base64, 'base64,') !== false) {
|
||||
$parts = explode('base64,', $base64);
|
||||
$base64 = end($parts);
|
||||
}
|
||||
|
||||
$pdfBinary = base64_decode($base64, true);
|
||||
|
||||
if ($pdfBinary === false) {
|
||||
throw new Exception("Il contenuto trovato non è un base64 valido");
|
||||
}
|
||||
|
||||
$filename = $data['NomeFile']
|
||||
?? $data['nomeFile']
|
||||
?? $data['FileName']
|
||||
?? $data['fileName']
|
||||
?? "rapporto_{$id}.pdf";
|
||||
|
||||
if (!str_ends_with(strtolower($filename), '.pdf')) {
|
||||
$filename .= '.pdf';
|
||||
}
|
||||
|
||||
header('Content-Type: application/pdf');
|
||||
header('Content-Disposition: inline; filename="' . basename($filename) . '"');
|
||||
header('Content-Length: ' . strlen($pdfBinary));
|
||||
|
||||
echo $pdfBinary;
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$idRapportoFile = isset($_GET['id_rapporto_file']) ? (int)$_GET['id_rapporto_file'] : 0;
|
||||
|
||||
if ($idRapportoFile <= 0) {
|
||||
throw new Exception("Parametro id_rapporto_file mancante o non valido.");
|
||||
}
|
||||
|
||||
/*
|
||||
* This endpoint returns the PDF binary stream.
|
||||
* Do not call this with the normal get() method because get() expects JSON.
|
||||
*/
|
||||
$endpoint = "MediaFile/DownloadStream?objectType=RapportoFile&propertyName=FileContent&objectKey={$idRapportoFile}";
|
||||
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
$pdfContent = $api->getRaw($endpoint);
|
||||
|
||||
if (empty($pdfContent)) {
|
||||
throw new Exception("PDF vuoto o non ricevuto dal server.");
|
||||
}
|
||||
|
||||
$fileName = "rapporto_{$idRapportoFile}.pdf";
|
||||
|
||||
header('Content-Type: application/pdf');
|
||||
header('Content-Disposition: inline; filename="' . $fileName . '"');
|
||||
header('Content-Length: ' . strlen($pdfContent));
|
||||
header('Cache-Control: private, max-age=0, must-revalidate');
|
||||
header('Pragma: public');
|
||||
|
||||
echo $pdfContent;
|
||||
exit;
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
@@ -25,6 +25,51 @@ if (!$template) {
|
||||
$stmt = $pdo->prepare("SELECT * FROM routine");
|
||||
$stmt->execute();
|
||||
$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 = [
|
||||
'#0d6efd' => 'Blue',
|
||||
'#6610f2' => 'Indigo',
|
||||
'#6f42c1' => 'Purple',
|
||||
'#d63384' => 'Pink',
|
||||
'#dc3545' => 'Red',
|
||||
'#fd7e14' => 'Orange',
|
||||
'#ffc107' => 'Yellow',
|
||||
'#198754' => 'Green',
|
||||
'#20c997' => 'Teal',
|
||||
'#0dcaf0' => 'Cyan',
|
||||
'#212529' => 'Dark',
|
||||
'#6c757d' => 'Gray',
|
||||
'#495057' => 'Slate',
|
||||
'#795548' => 'Brown',
|
||||
'#2f5d50' => 'Sage Green',
|
||||
];
|
||||
|
||||
$buttonTextPalette = [
|
||||
'#ffffff' => 'White',
|
||||
'#6c757d' => 'Gray',
|
||||
'#000000' => 'Black',
|
||||
];
|
||||
|
||||
$currentButtonBgColor = strtolower($template['button_bg_color'] ?? '#007bff');
|
||||
$currentButtonTextColor = strtolower($template['button_text_color'] ?? '#ffffff');
|
||||
|
||||
if (!array_key_exists($currentButtonBgColor, array_change_key_case($buttonBgPalette, CASE_LOWER))) {
|
||||
$currentButtonBgColor = '#0d6efd';
|
||||
}
|
||||
|
||||
if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonTextPalette, CASE_LOWER))) {
|
||||
$currentButtonTextColor = '#ffffff';
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
@@ -35,6 +80,64 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<?php include('cssinclude.php'); ?>
|
||||
<style>
|
||||
.color-palette {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.color-option {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.color-option input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.color-swatch {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #d6dbe0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
|
||||
transition: all 0.18s ease;
|
||||
}
|
||||
|
||||
.color-option input:checked+.color-swatch {
|
||||
border: 3px solid #111827;
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 0 0 4px rgba(13, 110, 253, 0.18);
|
||||
}
|
||||
|
||||
.color-swatch .check-mark {
|
||||
display: none;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.color-option input:checked+.color-swatch .check-mark {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.color-name {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
margin-top: 4px;
|
||||
color: #555;
|
||||
max-width: 55px;
|
||||
}
|
||||
</style>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<title>Edit Template <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
@@ -88,6 +191,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
<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="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>
|
||||
<small class="text-muted">Choose the source used by this template</small>
|
||||
</div>
|
||||
@@ -102,6 +207,60 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
<input type="text" name="start_column" id="startColumn" class="form-control" value="<?php echo htmlspecialchars($template['start_column'] ?? ''); ?>">
|
||||
</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">
|
||||
<label class="form-label"><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></label>
|
||||
<textarea name="description" class="form-control"><?php echo htmlspecialchars($template['description'] ?? ''); ?></textarea>
|
||||
@@ -123,12 +282,46 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Button Background Color</label>
|
||||
<input type="color" name="button_bg_color" class="form-control form-control-color" value="<?php echo htmlspecialchars($template['button_bg_color'] ?? '#007bff'); ?>">
|
||||
|
||||
<div class="color-palette">
|
||||
<?php foreach ($buttonBgPalette as $colorValue => $colorLabel): ?>
|
||||
<?php $isChecked = strtolower($colorValue) === $currentButtonBgColor; ?>
|
||||
|
||||
<label class="color-option" title="<?= htmlspecialchars($colorLabel, ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<input
|
||||
type="radio"
|
||||
name="button_bg_color"
|
||||
value="<?= htmlspecialchars($colorValue, ENT_QUOTES, 'UTF-8'); ?>"
|
||||
<?= $isChecked ? 'checked' : ''; ?>>
|
||||
<span class="color-swatch" style="background-color: <?= htmlspecialchars($colorValue, ENT_QUOTES, 'UTF-8'); ?>;">
|
||||
<span class="check-mark">✓</span>
|
||||
</span>
|
||||
<span class="color-name"><?= htmlspecialchars($colorLabel, ENT_QUOTES, 'UTF-8'); ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Button Text Color</label>
|
||||
<input type="color" name="button_text_color" class="form-control form-control-color" value="<?php echo htmlspecialchars($template['button_text_color'] ?? '#ffffff'); ?>">
|
||||
|
||||
<div class="color-palette">
|
||||
<?php foreach ($buttonTextPalette as $colorValue => $colorLabel): ?>
|
||||
<?php $isChecked = strtolower($colorValue) === $currentButtonTextColor; ?>
|
||||
|
||||
<label class="color-option" title="<?= htmlspecialchars($colorLabel, ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<input
|
||||
type="radio"
|
||||
name="button_text_color"
|
||||
value="<?= htmlspecialchars($colorValue, ENT_QUOTES, 'UTF-8'); ?>"
|
||||
<?= $isChecked ? 'checked' : ''; ?>>
|
||||
<span class="color-swatch" style="background-color: <?= htmlspecialchars($colorValue, ENT_QUOTES, 'UTF-8'); ?>;">
|
||||
<span class="check-mark">✓</span>
|
||||
</span>
|
||||
<span class="color-name"><?= htmlspecialchars($colorLabel, ENT_QUOTES, 'UTF-8'); ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
@@ -208,10 +401,16 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
const routineAction3 = document.getElementById("routineAction3");
|
||||
|
||||
const sourceType = document.getElementById("sourceType");
|
||||
|
||||
const headerRowWrapper = document.getElementById("headerRowWrapper");
|
||||
const startColumnWrapper = document.getElementById("startColumnWrapper");
|
||||
const xlsSheetNumberWrapper = document.getElementById("xlsSheetNumberWrapper");
|
||||
const apiConfigWrapper = document.getElementById("apiConfigWrapper");
|
||||
|
||||
const headerRow = document.getElementById("headerRow");
|
||||
const startColumn = document.getElementById("startColumn");
|
||||
const xlsSheetIndex = document.getElementById("xlsSheetIndex");
|
||||
const apiConfigSelect = document.getElementById("apiConfigSelect");
|
||||
|
||||
const selectedClientId = <?php echo json_encode((int)($template['idclient'] ?? 0)); ?>;
|
||||
const selectedSchemaId = <?php echo json_encode((int)($template['idschema'] ?? 0)); ?>;
|
||||
@@ -231,33 +430,71 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
allowClear: true
|
||||
});
|
||||
|
||||
$('#apiConfigSelect').select2({
|
||||
placeholder: "Select an API configuration...",
|
||||
allowClear: true
|
||||
});
|
||||
|
||||
function updateSourceFields() {
|
||||
const selectedSource = sourceType.value;
|
||||
|
||||
if (selectedSource === 'API') {
|
||||
headerRowWrapper.style.opacity = '0.6';
|
||||
startColumnWrapper.style.opacity = '0.6';
|
||||
const isXls = selectedSource === 'XLS';
|
||||
const isApiOrJson = selectedSource === 'API' || selectedSource === 'JSON';
|
||||
|
||||
headerRow.required = false;
|
||||
startColumn.required = false;
|
||||
|
||||
headerRow.disabled = true;
|
||||
startColumn.disabled = true;
|
||||
} else {
|
||||
headerRowWrapper.style.opacity = '1';
|
||||
startColumnWrapper.style.opacity = '1';
|
||||
if (isXls) {
|
||||
headerRowWrapper.style.display = 'block';
|
||||
startColumnWrapper.style.display = 'block';
|
||||
xlsSheetNumberWrapper.style.display = 'block';
|
||||
|
||||
headerRow.required = true;
|
||||
startColumn.required = true;
|
||||
|
||||
headerRow.disabled = false;
|
||||
startColumn.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';
|
||||
xlsSheetNumberWrapper.style.display = 'none';
|
||||
|
||||
headerRow.required = false;
|
||||
startColumn.required = false;
|
||||
|
||||
headerRow.disabled = true;
|
||||
startColumn.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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceType.addEventListener('change', updateSourceFields);
|
||||
updateSourceFields();
|
||||
|
||||
function formatClientLabel(client) {
|
||||
const nome = client.Nominativo || "Name not available";
|
||||
const id = client.IdCliente || "";
|
||||
const codiceCliente = (client.CodiceCliente ?? client.codiceCliente ?? "").toString().trim();
|
||||
const suffix = (codiceCliente.split("_")[1] || "").trim();
|
||||
const shortCode = suffix || (codiceCliente ? codiceCliente.charAt(0) : "--");
|
||||
|
||||
return `${nome.trim()} - ${shortCode} (ID: ${id})`;
|
||||
}
|
||||
|
||||
async function loadClients() {
|
||||
try {
|
||||
clientLoadingStatus.style.display = 'inline';
|
||||
@@ -280,9 +517,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
select.innerHTML = '<option value="">Select a client...</option>';
|
||||
|
||||
data.value.forEach(client => {
|
||||
const nome = client.Nominativo || "Name not available";
|
||||
const id = client.IdCliente || "";
|
||||
const option = new Option(`${nome.trim()} (ID: ${id})`, id);
|
||||
const option = new Option(formatClientLabel(client), id);
|
||||
|
||||
if (parseInt(id) === parseInt(selectedClientId)) {
|
||||
option.selected = true;
|
||||
@@ -468,6 +704,28 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
const routineId = routineSelect.value;
|
||||
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;
|
||||
}
|
||||
|
||||
fetch("process_edit_template_xls.php", {
|
||||
method: "POST",
|
||||
body: formData
|
||||
|
||||
@@ -328,3 +328,6 @@
|
||||
2026-03-26 10:57:41 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
|
||||
2026-03-26 10:57:56 [AnagraficaCertestObject] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
|
||||
2026-03-26 10:58:11 [AnagraficaCertestService] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
|
||||
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:03:19 - Errore nella richiesta: URL rejected: Malformed input to a URL function
|
||||
|
||||
@@ -59,6 +59,49 @@ function formatDateToExport($value)
|
||||
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 {
|
||||
$iddatadb = $_POST['iddatadb'] ?? null;
|
||||
if (!$iddatadb) {
|
||||
@@ -107,11 +150,13 @@ try {
|
||||
|
||||
// 🔹 STEP 3: Fetch Parts (including idmatrice and part id for custom fields)
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id AS part_id, part_number, part_description, material, color, mix, idmatrice, dateexpiry
|
||||
FROM identification_parts
|
||||
WHERE iddatadb = :iddatadb
|
||||
ORDER BY CAST(part_number AS UNSIGNED) ASC, part_number ASC
|
||||
");
|
||||
SELECT id AS part_id, part_number, part_description, material, color, mix, idmatrice, dateexpiry
|
||||
FROM identification_parts
|
||||
WHERE iddatadb = :iddatadb
|
||||
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]);
|
||||
$parts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
@@ -432,6 +477,71 @@ try {
|
||||
$logFilePhotos = $logDir . "commessa_{$commessaId}_photos_step5_2_" . time() . ".txt";
|
||||
$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
|
||||
if (!empty($fieldValues)) {
|
||||
// GET con espansione per CustomField
|
||||
@@ -510,9 +620,8 @@ try {
|
||||
$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
|
||||
|
||||
$importUserId = (!empty($lims_global_user_id) && is_numeric($lims_global_user_id))
|
||||
? (int) $lims_global_user_id
|
||||
: 285;
|
||||
@@ -520,17 +629,23 @@ try {
|
||||
$importPayload = [
|
||||
"IdUtente" => $importUserId
|
||||
];
|
||||
$importResult = $api->post("CommessaWeb({$commessaId})/ImportaCommessa", $importPayload);
|
||||
|
||||
$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" .
|
||||
"--header 'Content-Type: application/json' \\\n" .
|
||||
"--header 'Authorization: Bearer ••••••' \\\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";
|
||||
$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
|
||||
$expand = "CommesseCustomFields(\$expand=CustomField)";
|
||||
@@ -542,7 +657,23 @@ try {
|
||||
"RESPONSE:\n" . json_encode($commessaAfterPatch, JSON_PRETTY_PRINT);
|
||||
$logFileStep10 = $logDir . "commessa_{$commessaId}_get_step10_" . time() . ".txt";
|
||||
$writeLog($logFileStep10, $logContentStep10, "STEP 10 - GET verify (commessa={$commessaId})");
|
||||
// 🔹 STEP 10.1: Save final CodiceCommessa into datadb.commessaweb
|
||||
// After ImportaCommessa, the API returns the final LIMS job code in CodiceCommessa.
|
||||
// Example: CodiceCommessa = 2614795, CodiceCommessaWeb = 26C0103.
|
||||
$finalCodiceCommessa = trim((string)($commessaAfterPatch['CodiceCommessa'] ?? ''));
|
||||
|
||||
if ($finalCodiceCommessa !== '') {
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE datadb
|
||||
SET commessaweb = :commessaweb,
|
||||
status = 'l'
|
||||
WHERE iddatadb = :iddatadb
|
||||
");
|
||||
$stmt->execute([
|
||||
'commessaweb' => substr($finalCodiceCommessa, 0, 30),
|
||||
'iddatadb' => $iddatadb
|
||||
]);
|
||||
}
|
||||
// 🔹 STEP 11: Prepare final response
|
||||
$finalCommessa = [
|
||||
"Cliente" => $clienteId,
|
||||
@@ -557,17 +688,21 @@ try {
|
||||
echo json_encode([
|
||||
"success" => true,
|
||||
"idcommessaweb" => $commessaId,
|
||||
"commessaweb" => $commessaWebCode,
|
||||
"commessaweb" => $finalCodiceCommessa ?: $commessaWebCode,
|
||||
"commessaWeb" => $finalCommessa,
|
||||
"commessaWebApiResponse" => $commessaWeb, // Incluso per debug
|
||||
"totalCampioni" => count($campioni),
|
||||
"totalCustomFields" => count($commessaAfterPatch["CommesseCustomFields"] ?? []),
|
||||
"totalPhotos" => count($photos),
|
||||
"totalAnalyses" => $totalAnalyses,
|
||||
"addedAnalyses" => $addedAnalyses,
|
||||
"failedAnalyses" => $failedAnalyses,
|
||||
"message" => "Export successful",
|
||||
"logFiles" => [
|
||||
"step5_create" => $logFileStep5,
|
||||
"step5_2_photos" => $logFilePhotos,
|
||||
"step6_campioni" => $logFileStep6,
|
||||
"step63_analyses" => $logFileStep63Analisi,
|
||||
"step7_patch" => $logFileStep7 ?? null,
|
||||
"step9_1_importa" => $logFileStep91,
|
||||
"step10_get" => $logFileStep10
|
||||
@@ -583,6 +718,7 @@ try {
|
||||
"step5_create" => $logFileStep5 ?? null,
|
||||
"step5_2_photos" => $logFilePhotos ?? null,
|
||||
"step6_campioni" => $logFileStep6 ?? null,
|
||||
"step63_analyses" => $logFileStep63Analisi ?? null,
|
||||
"step7_patch" => $logFileStep7 ?? null,
|
||||
"step9_1_importa" => $logFileStep91 ?? null,
|
||||
"step10_get" => $logFileStep10 ?? null
|
||||
|
||||
@@ -18,7 +18,15 @@ try {
|
||||
|
||||
$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}";
|
||||
|
||||
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
// Campione hardcoded del tuo esempio
|
||||
$idCampione = 749027;
|
||||
|
||||
// Analisi che stai provando ad assegnare
|
||||
$targetRecordKey = '11;1218320';
|
||||
$targetIdAnalisi = '1218320';
|
||||
|
||||
// Endpoint: recupera il campione espandendo le analisi abilitate
|
||||
$endpoint = "Campione($idCampione)";
|
||||
|
||||
// ATTENZIONE:
|
||||
// nella tua classe normalmente le opzioni vengono trasformate in query string.
|
||||
// Qui serve: ?$expand=AnalisiAbilitate
|
||||
$options = [
|
||||
'$expand' => 'AnalisiAbilitate'
|
||||
];
|
||||
|
||||
// Debug: salva URL usato
|
||||
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
|
||||
$query = http_build_query($options);
|
||||
$query = urldecode($query); // rende leggibile $expand invece di %24expand
|
||||
|
||||
$full_url = $base_url . $endpoint . ($query ? '?' . $query : '');
|
||||
file_put_contents(__DIR__ . '/last_url_analisi_abilitate.txt', $full_url . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Chiamata API
|
||||
$data = $api->get($endpoint, $options);
|
||||
|
||||
// Recupera AnalisiAbilitate dalla risposta
|
||||
$analisiAbilitate = $data['AnalisiAbilitate'] ?? [];
|
||||
|
||||
// Alcune API OData possono restituire collection dentro "value"
|
||||
if (isset($analisiAbilitate['value']) && is_array($analisiAbilitate['value'])) {
|
||||
$analisiAbilitate = $analisiAbilitate['value'];
|
||||
}
|
||||
|
||||
// Cerca se il RecordKey / IdAnalisi che stai usando è effettivamente assegnabile
|
||||
$matches = [];
|
||||
|
||||
foreach ($analisiAbilitate as $analisi) {
|
||||
$recordKey = isset($analisi['RecordKey']) ? (string)$analisi['RecordKey'] : '';
|
||||
$idAnalisi = isset($analisi['IdAnalisi']) ? (string)$analisi['IdAnalisi'] : '';
|
||||
|
||||
if ($recordKey === $targetRecordKey || $idAnalisi === $targetIdAnalisi) {
|
||||
$matches[] = $analisi;
|
||||
}
|
||||
}
|
||||
|
||||
// Output diagnostico
|
||||
$output = [
|
||||
'success' => true,
|
||||
'idCampione' => $idCampione,
|
||||
'request_url' => $full_url,
|
||||
'targetRecordKey' => $targetRecordKey,
|
||||
'targetIdAnalisi' => $targetIdAnalisi,
|
||||
'enabled_analyses_count' => is_array($analisiAbilitate) ? count($analisiAbilitate) : 0,
|
||||
'target_found' => count($matches) > 0,
|
||||
'target_matches' => $matches,
|
||||
'analisi_abilitate' => $analisiAbilitate,
|
||||
'raw_response' => $data
|
||||
];
|
||||
|
||||
// Salva il JSON in locale
|
||||
file_put_contents(
|
||||
__DIR__ . '/analisi_abilitate_campione_749027_response.json',
|
||||
json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
|
||||
echo json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
} catch (Exception $e) {
|
||||
file_put_contents(
|
||||
__DIR__ . '/error_log_analisi_abilitate.txt',
|
||||
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
|
||||
FILE_APPEND
|
||||
);
|
||||
|
||||
http_response_code(500);
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
// Campione hardcoded del tuo esempio
|
||||
$idCampione = 749027;
|
||||
|
||||
// Matrice attesa dal log STEP 6
|
||||
$expectedMatriceId = 6430;
|
||||
|
||||
// Endpoint con expand della Matrice
|
||||
$endpoint = "Campione($idCampione)";
|
||||
$options = [
|
||||
'$expand' => 'Matrice'
|
||||
];
|
||||
|
||||
// Debug URL
|
||||
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
|
||||
$query = http_build_query($options);
|
||||
$queryReadable = urldecode($query);
|
||||
|
||||
$full_url = $base_url . $endpoint . ($queryReadable ? '?' . $queryReadable : '');
|
||||
|
||||
file_put_contents(
|
||||
__DIR__ . '/last_url_check_matrice.txt',
|
||||
$full_url . PHP_EOL,
|
||||
FILE_APPEND
|
||||
);
|
||||
|
||||
// Chiamata API
|
||||
$data = $api->get($endpoint, $options);
|
||||
|
||||
// Recupero Matrice dalla response
|
||||
$matrice = $data['Matrice'] ?? null;
|
||||
|
||||
$actualMatriceId = null;
|
||||
|
||||
if (is_array($matrice)) {
|
||||
// Provo i nomi più probabili
|
||||
$actualMatriceId = $matrice['IdMatrice']
|
||||
?? $matrice['idMatrice']
|
||||
?? $matrice['Id']
|
||||
?? $matrice['ID']
|
||||
?? null;
|
||||
}
|
||||
|
||||
$matrice_ok = ((int)$actualMatriceId === (int)$expectedMatriceId);
|
||||
|
||||
$output = [
|
||||
'success' => true,
|
||||
'idCampione' => $idCampione,
|
||||
'expectedMatriceId' => $expectedMatriceId,
|
||||
'actualMatriceId' => $actualMatriceId,
|
||||
'matrice_ok' => $matrice_ok,
|
||||
'request_url' => $full_url,
|
||||
'matrice' => $matrice,
|
||||
'raw_response' => $data
|
||||
];
|
||||
|
||||
// Salva JSON completo
|
||||
file_put_contents(
|
||||
__DIR__ . '/check_matrice_campione_749027_response.json',
|
||||
json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
|
||||
echo json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
} catch (Exception $e) {
|
||||
file_put_contents(
|
||||
__DIR__ . '/error_log_check_matrice.txt',
|
||||
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
|
||||
FILE_APPEND
|
||||
);
|
||||
|
||||
http_response_code(500);
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
require_once 'include/headscript.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Invalid request method']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$template_id = isset($input['template_id']) ? (int)$input['template_id'] : 0;
|
||||
$code = isset($input['code']) ? trim($input['code']) : '';
|
||||
|
||||
if ($template_id <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Missing or invalid template_id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($code === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Missing code']);
|
||||
exit;
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO: Replace this block with your real API call.
|
||||
* Expected response for import_json.php:
|
||||
* {
|
||||
* "success": true,
|
||||
* "reference": "CODE123",
|
||||
* "json": { ... real JSON payload ... }
|
||||
* }
|
||||
*/
|
||||
|
||||
$sampleJson = [
|
||||
'data' => [[
|
||||
'type' => 'trf_data_request',
|
||||
'id' => $code,
|
||||
'attributes' => [
|
||||
'trf_type' => 'apparel',
|
||||
'service_required' => 'regular',
|
||||
'submitter_information' => [
|
||||
'submitter_type' => 'supplier'
|
||||
]
|
||||
]
|
||||
]]
|
||||
];
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'reference' => $code,
|
||||
'json' => $sampleJson
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
$idCliente = isset($_GET['id_cliente']) ? (int)$_GET['id_cliente'] : 0;
|
||||
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 3;
|
||||
$signedStatus = trim($_GET['signed_status'] ?? 'all');
|
||||
|
||||
if ($idCliente <= 0) {
|
||||
throw new Exception("Parametro id_cliente mancante o non valido.");
|
||||
}
|
||||
|
||||
/*
|
||||
* Allowed limits only.
|
||||
* This prevents risky wide queries on the live LIMS.
|
||||
*/
|
||||
$allowedLimits = [1, 3, 5, 10];
|
||||
|
||||
if (!in_array($limit, $allowedLimits, true)) {
|
||||
$limit = 3;
|
||||
}
|
||||
|
||||
/*
|
||||
* Allowed signature filters.
|
||||
*/
|
||||
$allowedSignedStatuses = ['all', 'signed', 'not_signed'];
|
||||
|
||||
if (!in_array($signedStatus, $allowedSignedStatuses, true)) {
|
||||
$signedStatus = 'all';
|
||||
}
|
||||
|
||||
/*
|
||||
* Base filter by customer.
|
||||
* We already verified that Rapporto can expand Cliente and returns Cliente.IdCliente.
|
||||
*/
|
||||
$filters = [
|
||||
"Cliente/IdCliente eq {$idCliente}"
|
||||
];
|
||||
|
||||
if ($signedStatus === 'signed') {
|
||||
$filters[] = "Firmato eq true";
|
||||
}
|
||||
|
||||
if ($signedStatus === 'not_signed') {
|
||||
$filters[] = "Firmato eq false";
|
||||
}
|
||||
|
||||
$filter = implode(' and ', $filters);
|
||||
|
||||
/*
|
||||
* Important:
|
||||
* - $top limits the number of reports.
|
||||
* - $orderby=Data desc gets the latest reports first.
|
||||
* - $expand=RapportiFiles retrieves only the PDF file metadata, not the binary PDF.
|
||||
*/
|
||||
$params = [
|
||||
'$filter' => $filter,
|
||||
'$select' => 'IdRapporto,CodiceRapporto,Data,Versione,Firmato,DataStampa',
|
||||
'$expand' => 'RapportiFiles',
|
||||
'$orderby' => 'Data desc',
|
||||
'$top' => $limit
|
||||
];
|
||||
|
||||
$endpoint = "Rapporto?" . http_build_query($params);
|
||||
|
||||
file_put_contents(
|
||||
__DIR__ . '/last_rapporti_cliente_endpoint.txt',
|
||||
'[' . date('Y-m-d H:i:s') . '] ' . $endpoint . PHP_EOL,
|
||||
FILE_APPEND
|
||||
);
|
||||
|
||||
$data = $api->get($endpoint);
|
||||
|
||||
$items = $data['value'] ?? [];
|
||||
|
||||
if (!is_array($items)) {
|
||||
$items = [];
|
||||
}
|
||||
|
||||
$reports = [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
$rapportiFiles = $item['RapportiFiles'] ?? [];
|
||||
$pdfFiles = [];
|
||||
|
||||
if (is_array($rapportiFiles)) {
|
||||
foreach ($rapportiFiles as $file) {
|
||||
$idRapportoFile = intval($file['IdRapportoFile'] ?? 0);
|
||||
|
||||
if ($idRapportoFile > 0) {
|
||||
$pdfFiles[] = [
|
||||
'id_rapporto_file' => $idRapportoFile,
|
||||
'file_name' => $file['FileName'] ?? null,
|
||||
'categoria' => $file['Categoria'] ?? null,
|
||||
'tipo_rapporto' => $file['TipoRapporto'] ?? null,
|
||||
'download_url' => "download_rapporto_pdf.php?id_rapporto_file={$idRapportoFile}"
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$reports[] = [
|
||||
'id_rapporto' => $item['IdRapporto'] ?? null,
|
||||
'codice_rapporto' => $item['CodiceRapporto'] ?? null,
|
||||
'data' => $item['Data'] ?? null,
|
||||
'data_stampa' => $item['DataStampa'] ?? null,
|
||||
'versione' => $item['Versione'] ?? null,
|
||||
'firmato' => $item['Firmato'] ?? null,
|
||||
'pdf_files' => $pdfFiles
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'id_cliente' => $idCliente,
|
||||
'limit' => $limit,
|
||||
'signed_status' => $signedStatus,
|
||||
'endpoint' => $endpoint,
|
||||
'count' => count($reports),
|
||||
'reports' => $reports
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
} catch (Exception $e) {
|
||||
file_put_contents(
|
||||
__DIR__ . '/error_log.txt',
|
||||
date('Y-m-d H:i:s') . ' - get_rapporti_cliente.php - ' . $e->getMessage() . PHP_EOL,
|
||||
FILE_APPEND
|
||||
);
|
||||
|
||||
http_response_code(500);
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
/**
|
||||
* Uso:
|
||||
* rapporto_full_by_codice.php?codice=2621521
|
||||
* rapporto_full_by_codice.php?codice=2621521&download=1
|
||||
*/
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
$codiceRapporto = trim($_GET['codice'] ?? '');
|
||||
$downloadPdf = isset($_GET['download']) && $_GET['download'] == '1';
|
||||
|
||||
if ($codiceRapporto === '') {
|
||||
throw new Exception("Parametro codice mancante. Usa ?codice=2621521");
|
||||
}
|
||||
|
||||
$debugDir = __DIR__ . '/logs/lims_rapporti/';
|
||||
$pdfDir = __DIR__ . '/pdf/rapporti/';
|
||||
|
||||
if (!is_dir($debugDir)) mkdir($debugDir, 0755, true);
|
||||
if (!is_dir($pdfDir)) mkdir($pdfDir, 0755, true);
|
||||
|
||||
$codiceRapportoSafe = str_replace("'", "''", $codiceRapporto);
|
||||
|
||||
/**
|
||||
* STEP 1 - Search IdRapporto
|
||||
*/
|
||||
$searchParams = [
|
||||
'$filter' => "CodiceRapporto eq '{$codiceRapportoSafe}'",
|
||||
'$select' => 'IdRapporto,CodiceRapporto,Data,Versione,Firmato,DataStampa',
|
||||
'$top' => 1
|
||||
];
|
||||
|
||||
$searchEndpoint = "Rapporto?" . http_build_query($searchParams);
|
||||
$searchData = $api->get($searchEndpoint);
|
||||
|
||||
$rapporti = $searchData['value'] ?? [];
|
||||
|
||||
if (empty($rapporti)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Nessun rapporto trovato per questo CodiceRapporto.',
|
||||
'codice_rapporto' => $codiceRapporto
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$rapportoBase = $rapporti[0];
|
||||
$idRapporto = intval($rapportoBase['IdRapporto'] ?? 0);
|
||||
|
||||
if (!$idRapporto) {
|
||||
throw new Exception("IdRapporto non trovato nella risposta.");
|
||||
}
|
||||
|
||||
/**
|
||||
* STEP 2 - Dettaglio Rapporto
|
||||
*/
|
||||
$clienteExpandError = null;
|
||||
$detailEndpoint = "Rapporto({$idRapporto})?\$expand=Cliente,RapportiFiles,CampioniDatiRapporto";
|
||||
|
||||
try {
|
||||
$detailData = $api->get($detailEndpoint);
|
||||
} catch (Exception $e) {
|
||||
$clienteExpandError = $e->getMessage();
|
||||
$detailEndpoint = "Rapporto({$idRapporto})?\$expand=RapportiFiles,CampioniDatiRapporto";
|
||||
$detailData = $api->get($detailEndpoint);
|
||||
}
|
||||
|
||||
file_put_contents(
|
||||
$debugDir . "rapporto_{$codiceRapportoSafe}_light.json",
|
||||
json_encode($detailData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
|
||||
$cliente = $detailData['Cliente'] ?? null;
|
||||
$rapportiFiles = $detailData['RapportiFiles'] ?? [];
|
||||
$campioniDatiRapporto = $detailData['CampioniDatiRapporto'] ?? [];
|
||||
|
||||
// ====================== CLIENTE ======================
|
||||
$clienteInfo = [
|
||||
'found' => is_array($cliente),
|
||||
'id_cliente' => $cliente['IdCliente'] ?? $cliente['Id'] ?? null,
|
||||
'codice_cliente' => $cliente['CodiceCliente'] ?? $cliente['Codice'] ?? null,
|
||||
'nome_cliente' => $cliente['Nominativo'] ?? $cliente['RagioneSociale'] ?? $cliente['Nome'] ?? $cliente['Descrizione'] ?? null,
|
||||
'partita_iva' => $cliente['PartitaIva'] ?? null,
|
||||
'codice_fiscale' => $cliente['CodiceFiscale'] ?? null
|
||||
];
|
||||
|
||||
// ====================== FILE PDF ======================
|
||||
$selectedRapportoFile = null;
|
||||
$idRapportoFile = null;
|
||||
$pdfFileName = $codiceRapporto . '.pdf';
|
||||
$pdfFullPath = $pdfDir . $pdfFileName;
|
||||
|
||||
if (is_array($rapportiFiles) && count($rapportiFiles) > 0) {
|
||||
foreach ($rapportiFiles as $file) {
|
||||
$fileName = $file['FileName'] ?? '';
|
||||
$tipoRapporto = $file['TipoRapporto'] ?? '';
|
||||
$categoria = $file['Categoria'] ?? '';
|
||||
|
||||
if (
|
||||
stripos($fileName, '.pdf') !== false ||
|
||||
stripos($tipoRapporto, 'Rapporto') !== false ||
|
||||
stripos($categoria, 'Rapporti') !== false
|
||||
) {
|
||||
$selectedRapportoFile = $file;
|
||||
$idRapportoFile = $file['IdRapportoFile'] ?? null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$selectedRapportoFile && count($rapportiFiles) > 0) {
|
||||
$selectedRapportoFile = $rapportiFiles[0];
|
||||
$idRapportoFile = $rapportiFiles[0]['IdRapportoFile'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== DOWNLOAD PDF - DEBUG AVANZATO ======================
|
||||
$pdfDownloaded = false;
|
||||
$pdfError = null;
|
||||
$endpointUsato = null;
|
||||
|
||||
if ($downloadPdf && $idRapportoFile) {
|
||||
|
||||
$tentativi = [
|
||||
"RapportoFile({$idRapportoFile})/\$value",
|
||||
"RapportoFile({$idRapportoFile})/Contenuto/\$value",
|
||||
"RapportoFile({$idRapportoFile})/File/\$value",
|
||||
"Rapporto({$idRapporto})/RapportiFiles({$idRapportoFile})/\$value",
|
||||
"Rapporto({$idRapporto})/RapportoFile/\$value",
|
||||
"GetRapportoFile?IdRapportoFile={$idRapportoFile}",
|
||||
"RapportoFile/Download?Id={$idRapportoFile}",
|
||||
"Rapporto/DownloadFile?IdRapporto={$idRapporto}"
|
||||
];
|
||||
|
||||
foreach ($tentativi as $ep) {
|
||||
try {
|
||||
$pdfContent = $api->getRaw($ep);
|
||||
|
||||
if (strlen($pdfContent) > 2000) { // PDF decente deve essere più grande
|
||||
file_put_contents($pdfFullPath, $pdfContent);
|
||||
$pdfDownloaded = true;
|
||||
$endpointUsato = $ep;
|
||||
$pdfError = "SUCCESSO con: " . $ep;
|
||||
break;
|
||||
} else {
|
||||
$pdfError = "Contenuto troppo piccolo con: " . $ep;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$pdfError = "Fallito con " . $ep . " → " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== CAMPIONI + RATING (ripristinato dal tuo originale) ======================
|
||||
$campioniSintesi = [];
|
||||
$ratingFinale = null;
|
||||
$ratingSource = null;
|
||||
$hasIrregolare = false;
|
||||
$hasPositiveResults = false;
|
||||
|
||||
if (is_array($campioniDatiRapporto)) {
|
||||
foreach ($campioniDatiRapporto as $campione) {
|
||||
$giudizioRapporto = $campione['GiudizioRapporto'] ?? null;
|
||||
$giudizioCertificato = $campione['GiudizioCertificato'] ?? null;
|
||||
$esitoGiudizioLMR = $campione['EsitoGiudizioLMR'] ?? null;
|
||||
$esitoGiudizioImpiego = $campione['EsitoGiudizioImpiego'] ?? null;
|
||||
$risultatiIrregolari = ($campione['RisultatiIrregolari'] ?? false) === true;
|
||||
$risultatiPositivi = ($campione['RisultatiPositivi'] ?? false) === true;
|
||||
|
||||
$campioniSintesi[] = [
|
||||
'id_campione_dati_rapporto' => $campione['IdCampioneDatiRapporto'] ?? null,
|
||||
'codice_campione' => $campione['CodiceCampione'] ?? null,
|
||||
'stato_campione' => $campione['StatoCampione'] ?? null,
|
||||
'stato_campione_web' => $campione['StatoCampioneWeb'] ?? null,
|
||||
'matrice' => $campione['Matrice'] ?? null,
|
||||
'macro_matrice' => $campione['MacroMatrice'] ?? null,
|
||||
'giudizio_rapporto' => $giudizioRapporto,
|
||||
'giudizio_certificato' => $giudizioCertificato,
|
||||
'esito_giudizio_lmr' => $esitoGiudizioLMR,
|
||||
'esito_giudizio_impiego' => $esitoGiudizioImpiego,
|
||||
'risultati_positivi' => $risultatiPositivi,
|
||||
'risultati_irregolari' => $risultatiIrregolari
|
||||
];
|
||||
|
||||
if (!$ratingFinale && !empty($giudizioRapporto)) {
|
||||
$ratingFinale = $giudizioRapporto;
|
||||
$ratingSource = 'CampioniDatiRapporto.GiudizioRapporto';
|
||||
}
|
||||
|
||||
if (!$ratingFinale && !empty($giudizioCertificato)) {
|
||||
$ratingFinale = $giudizioCertificato;
|
||||
$ratingSource = 'CampioniDatiRapporto.GiudizioCertificato';
|
||||
}
|
||||
|
||||
if ($risultatiIrregolari) $hasIrregolare = true;
|
||||
if ($risultatiPositivi) $hasPositiveResults = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback rating
|
||||
if (!$ratingFinale) {
|
||||
if ($hasIrregolare) {
|
||||
$ratingFinale = 'Irregolare';
|
||||
$ratingSource = 'RisultatiIrregolari';
|
||||
} elseif ($hasPositiveResults || count($campioniSintesi) > 0) {
|
||||
$ratingFinale = 'Regolare';
|
||||
$ratingSource = 'fallback';
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== RISPOSTA FINALE ======================
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
|
||||
'codice_rapporto' => $codiceRapporto,
|
||||
'id_rapporto' => $idRapporto,
|
||||
|
||||
'search_endpoint' => $searchEndpoint,
|
||||
'detail_endpoint' => $detailEndpoint,
|
||||
'cliente_expand_error' => $clienteExpandError,
|
||||
|
||||
'rapporto_base' => $rapportoBase,
|
||||
'cliente' => $clienteInfo,
|
||||
|
||||
'rating_finale' => $ratingFinale,
|
||||
'rating_source' => $ratingSource,
|
||||
|
||||
'rapporti_files_count' => count($rapportiFiles),
|
||||
'selected_rapporto_file' => $selectedRapportoFile,
|
||||
|
||||
'pdf_file_name' => $pdfFileName,
|
||||
'pdf_path' => $pdfDownloaded ? $pdfFullPath : null,
|
||||
'pdf_downloaded' => $pdfDownloaded,
|
||||
'pdf_error' => $pdfError,
|
||||
|
||||
'campioni_count' => count($campioniSintesi),
|
||||
'campioni_sintesi' => $campioniSintesi,
|
||||
|
||||
'download_requested' => $downloadPdf
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
$commessa = trim($_GET['commessa'] ?? '');
|
||||
|
||||
if ($commessa === '') {
|
||||
throw new Exception("Parametro commessa mancante");
|
||||
}
|
||||
|
||||
$debugDir = __DIR__ . '/logs/lims_rapporto/';
|
||||
if (!is_dir($debugDir)) {
|
||||
mkdir($debugDir, 0755, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* STEP 1
|
||||
* Provo a cercare il rapporto con filtri leggeri.
|
||||
* NIENTE expand pesanti.
|
||||
*/
|
||||
$attempts = [];
|
||||
|
||||
$filters = [
|
||||
'CodiceCommessa' => "CodiceCommessa eq '{$commessa}'",
|
||||
'Commessa_CodiceCommessa' => "Commessa/CodiceCommessa eq '{$commessa}'",
|
||||
'Commessa_IdCommessa' => is_numeric($commessa) ? "Commessa/IdCommessa eq {$commessa}" : null,
|
||||
'Codice' => "Codice eq '{$commessa}'"
|
||||
];
|
||||
|
||||
foreach ($filters as $label => $filter) {
|
||||
if (!$filter) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$options = [
|
||||
'$filter' => $filter,
|
||||
'$top' => 10
|
||||
];
|
||||
|
||||
$data = $api->get('Rapporto', $options);
|
||||
|
||||
$attempts[$label] = [
|
||||
'success' => true,
|
||||
'filter' => $filter,
|
||||
'records' => isset($data['value']) && is_array($data['value']) ? count($data['value']) : null,
|
||||
'data' => $data
|
||||
];
|
||||
|
||||
file_put_contents(
|
||||
$debugDir . "rapporto_{$commessa}_{$label}.json",
|
||||
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
$attempts[$label] = [
|
||||
'success' => false,
|
||||
'filter' => $filter,
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* STEP 2
|
||||
* Prendo solo eventuali rapporti trovati.
|
||||
*/
|
||||
$rapportiFound = [];
|
||||
|
||||
foreach ($attempts as $label => $attempt) {
|
||||
if (!$attempt['success']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$items = $attempt['data']['value'] ?? [];
|
||||
|
||||
if (!is_array($items)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($items as $item) {
|
||||
$rapportiFound[] = [
|
||||
'matched_by' => $label,
|
||||
'rapporto' => $item
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'input_commessa' => $commessa,
|
||||
'message' => 'Ricerca leggera su Rapporto completata. Se trovi un rapporto, poi recuperiamo RapportiFiles solo per quello.',
|
||||
'rapporti_found_count' => count($rapportiFound),
|
||||
'rapporti_found' => $rapportiFound,
|
||||
'attempts_summary' => array_map(function ($a) {
|
||||
return [
|
||||
'success' => $a['success'],
|
||||
'filter' => $a['filter'],
|
||||
'records' => $a['records'] ?? null,
|
||||
'error' => $a['error'] ?? null
|
||||
];
|
||||
}, $attempts)
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
@@ -2,25 +2,179 @@
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
$rapporto_id = 533329;
|
||||
|
||||
// Costruzione manuale dell'endpoint con espansione annidata
|
||||
$endpoint = "Rapporto($rapporto_id)?\$expand=CampioniDatiRapporto(\$expand=AnalisiDatiRapporto,CustomFieldsDatiRapporto)";
|
||||
// Esempi:
|
||||
// rapporto_by_codice_expand_step.php?codice=2541111&step=base
|
||||
// rapporto_by_codice_expand_step.php?codice=2541111&step=files
|
||||
// rapporto_by_codice_expand_step.php?codice=2541111&step=campioni
|
||||
// rapporto_by_codice_expand_step.php?codice=2541111&step=files_campioni
|
||||
|
||||
// Non passiamo options, già incluso nell'endpoint
|
||||
$data = $api->get($endpoint);
|
||||
$codiceRapporto = trim($_GET['codice'] ?? '');
|
||||
// Safe step mode: default is base, but allows controlled read-only steps
|
||||
$step = trim($_GET['step'] ?? 'base');
|
||||
|
||||
file_put_contents(__DIR__ . '/rapporto_expanded.json', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
echo json_encode($data);
|
||||
if ($codiceRapporto === '') {
|
||||
throw new Exception("Parametro codice mancante. Usa ?codice=2541111");
|
||||
}
|
||||
|
||||
$allowedSteps = [
|
||||
'base' => '',
|
||||
'campioni' => 'CampioniDatiRapporto',
|
||||
'files' => 'RapportiFiles,Cliente',
|
||||
'cliente' => 'Cliente'
|
||||
];
|
||||
|
||||
if (!array_key_exists($step, $allowedSteps)) {
|
||||
throw new Exception("Step non valido. Usa: " . implode(', ', array_keys($allowedSteps)));
|
||||
}
|
||||
|
||||
// Escape OData per eventuali apostrofi
|
||||
$codiceRapportoSafe = str_replace("'", "''", $codiceRapporto);
|
||||
// Safe version of codice rapporto for filenames
|
||||
$codiceRapportoFileSafe = preg_replace('/[^a-zA-Z0-9_-]/', '_', $codiceRapporto);
|
||||
/*
|
||||
* STEP 1 - Trova IdRapporto partendo da CodiceRapporto.
|
||||
* Query leggera, con $select e $top=1.
|
||||
*/
|
||||
$searchParams = [
|
||||
'$filter' => "CodiceRapporto eq '{$codiceRapportoSafe}'",
|
||||
'$select' => 'IdRapporto,CodiceRapporto,Data,Versione,Firmato,DataStampa',
|
||||
'$top' => 1
|
||||
];
|
||||
|
||||
$searchEndpoint = "Rapporto?" . http_build_query($searchParams);
|
||||
|
||||
$searchData = $api->get($searchEndpoint);
|
||||
|
||||
$items = $searchData['value'] ?? [];
|
||||
|
||||
if (!is_array($items) || count($items) === 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Nessun rapporto trovato per questo CodiceRapporto.',
|
||||
'codice_rapporto' => $codiceRapporto,
|
||||
'search_endpoint' => $searchEndpoint,
|
||||
'search_data' => $searchData
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$rapportoBase = $items[0];
|
||||
$rapportoId = intval($rapportoBase['IdRapporto'] ?? 0);
|
||||
|
||||
if (!$rapportoId) {
|
||||
throw new Exception("IdRapporto non trovato nella risposta.");
|
||||
}
|
||||
|
||||
/*
|
||||
* STEP 2 - Se step=base, restituisco solo la ricerca base.
|
||||
*/
|
||||
if ($step === 'base') {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'codice_rapporto' => $codiceRapporto,
|
||||
'id_rapporto' => $rapportoId,
|
||||
'step' => $step,
|
||||
'search_endpoint' => $searchEndpoint,
|
||||
'rapporto_base' => $rapportoBase
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
/*
|
||||
* STEP 3 - Espande SOLO il rapporto trovato.
|
||||
*/
|
||||
$expandValue = $allowedSteps[$step];
|
||||
|
||||
$detailParams = [
|
||||
'$expand' => $expandValue
|
||||
];
|
||||
|
||||
$detailEndpoint = "Rapporto({$rapportoId})?" . http_build_query($detailParams);
|
||||
|
||||
file_put_contents(
|
||||
__DIR__ . '/last_rapporto_by_codice_expand_endpoint.txt',
|
||||
'[' . date('Y-m-d H:i:s') . '] SEARCH: ' . $searchEndpoint . PHP_EOL .
|
||||
'[' . date('Y-m-d H:i:s') . '] DETAIL: ' . $detailEndpoint . PHP_EOL,
|
||||
FILE_APPEND
|
||||
);
|
||||
|
||||
$detailData = $api->get($detailEndpoint);
|
||||
|
||||
$pdfFiles = [];
|
||||
|
||||
if ($step === 'files') {
|
||||
$rapportiFiles = $detailData['RapportiFiles'] ?? [];
|
||||
|
||||
if (is_array($rapportiFiles)) {
|
||||
foreach ($rapportiFiles as $file) {
|
||||
$idRapportoFile = intval($file['IdRapportoFile'] ?? 0);
|
||||
|
||||
if ($idRapportoFile > 0) {
|
||||
$pdfFiles[] = [
|
||||
'id_rapporto_file' => $idRapportoFile,
|
||||
'file_name' => $file['FileName'] ?? null,
|
||||
'categoria' => $file['Categoria'] ?? null,
|
||||
'tipo_rapporto' => $file['TipoRapporto'] ?? null,
|
||||
'download_endpoint' => "MediaFile/DownloadStream?objectType=RapportoFile&propertyName=FileContent&objectKey={$idRapportoFile}"
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$clienteData = null;
|
||||
|
||||
if ($step === 'cliente' || $step === 'files') {
|
||||
$clienteData = $detailData['Cliente'] ?? null;
|
||||
}
|
||||
|
||||
file_put_contents(
|
||||
__DIR__ . "/rapporto_codice_{$codiceRapportoFileSafe}_{$step}.json",
|
||||
json_encode([
|
||||
'search' => $searchData,
|
||||
'detail' => $detailData
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
|
||||
$response = [
|
||||
'success' => true,
|
||||
'codice_rapporto' => $codiceRapporto,
|
||||
'id_rapporto' => $rapportoId,
|
||||
'step' => $step,
|
||||
'search_endpoint' => $searchEndpoint,
|
||||
'detail_endpoint' => $detailEndpoint,
|
||||
'rapporto_base' => $rapportoBase,
|
||||
'data' => $detailData
|
||||
];
|
||||
|
||||
if ($step === 'files') {
|
||||
$response['pdf_files'] = $pdfFiles;
|
||||
}
|
||||
|
||||
if ($step === 'cliente' || $step === 'files') {
|
||||
$response['cliente'] = $clienteData;
|
||||
}
|
||||
|
||||
echo json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
} catch (Exception $e) {
|
||||
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
|
||||
file_put_contents(
|
||||
__DIR__ . '/error_log.txt',
|
||||
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
|
||||
FILE_APPEND
|
||||
);
|
||||
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
+1058
-309
File diff suppressed because it is too large
Load Diff
@@ -10,65 +10,125 @@
|
||||
<title>Template Buttons - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
|
||||
<style>
|
||||
/* Layout flessibile per gestire dimensioni diverse */
|
||||
#templateButtons {
|
||||
/* Main buttons container */
|
||||
.template-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
justify-content: flex-start;
|
||||
/* Allinea a sinistra */
|
||||
padding: 20px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
/* Definizione delle dimensioni */
|
||||
/* Definizione delle dimensioni */
|
||||
/* Button sizes */
|
||||
.btn-small {
|
||||
font-size: 12px;
|
||||
padding: 6px 12px;
|
||||
min-width: 100px;
|
||||
min-height: 30px;
|
||||
display: flex;
|
||||
/* Aggiunto */
|
||||
min-height: 36px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
/* Aggiunto */
|
||||
align-items: center;
|
||||
/* Aggiunto */
|
||||
gap: 8px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-medium {
|
||||
font-size: 16px;
|
||||
padding: 10px 20px;
|
||||
min-width: 130px;
|
||||
min-height: 45px;
|
||||
display: flex;
|
||||
/* Aggiunto */
|
||||
min-width: 140px;
|
||||
min-height: 48px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
/* Aggiunto */
|
||||
align-items: center;
|
||||
/* Aggiunto */
|
||||
gap: 8px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-large {
|
||||
font-size: 20px;
|
||||
padding: 14px 28px;
|
||||
min-width: 180px;
|
||||
min-height: 60px;
|
||||
display: flex;
|
||||
/* Aggiunto */
|
||||
min-width: 190px;
|
||||
min-height: 64px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
/* Aggiunto */
|
||||
align-items: center;
|
||||
/* Aggiunto */
|
||||
gap: 10px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Stile della barra di ricerca */
|
||||
.template-icon {
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.btn-large .template-icon {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.btn-small .template-icon {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* Search box */
|
||||
#searchInput {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
padding: 10px 14px;
|
||||
font-size: 16px;
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 18px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#searchInput:focus {
|
||||
border-color: #0d6efd;
|
||||
box-shadow: 0 0 0 0.15rem rgba(13, 110, 253, 0.15);
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.custom-tabs {
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
margin-bottom: 20px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.custom-tabs .nav-link {
|
||||
border: none;
|
||||
border-radius: 10px 10px 0 0;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 18px;
|
||||
}
|
||||
|
||||
.custom-tabs .nav-link:hover {
|
||||
background: #f8f9fa;
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
.custom-tabs .nav-link.active {
|
||||
background: #0d6efd;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
min-height: 140px;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.loading-message {
|
||||
color: #6c757d;
|
||||
padding: 10px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -87,14 +147,54 @@
|
||||
<h6 class="mb-0">Active Templates</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Barra di ricerca -->
|
||||
<input type="text" id="searchInput" placeholder="Search template...">
|
||||
<div id="templateButtons"></div>
|
||||
|
||||
<ul class="nav nav-tabs custom-tabs" id="templateTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="xls-tab" data-bs-toggle="tab" data-bs-target="#xls-pane" type="button" role="tab" aria-controls="xls-pane" aria-selected="true">
|
||||
<i class="bx bx-spreadsheet template-icon"></i>
|
||||
XLS
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="api-tab" data-bs-toggle="tab" data-bs-target="#api-pane" type="button" role="tab" aria-controls="api-pane" aria-selected="false">
|
||||
<i class="bx bx-transfer-alt template-icon"></i>
|
||||
JSON/API
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="pdf-tab" data-bs-toggle="tab" data-bs-target="#pdf-pane" type="button" role="tab" aria-controls="pdf-pane" aria-selected="false">
|
||||
<i class="bx bx-file-pdf template-icon"></i>
|
||||
PDF
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="templateTabsContent">
|
||||
<div class="tab-pane fade show active" id="xls-pane" role="tabpanel" aria-labelledby="xls-tab">
|
||||
<div id="templateButtonsXLS" class="template-buttons">
|
||||
<div class="loading-message">Loading XLS templates...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="api-pane" role="tabpanel" aria-labelledby="api-tab">
|
||||
<div id="templateButtonsAPI" class="template-buttons">
|
||||
<div class="loading-message">Loading API templates...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pdf-pane" role="tabpanel" aria-labelledby="pdf-tab">
|
||||
<div id="templateButtonsPDF" class="template-buttons">
|
||||
<div class="loading-message">Loading PDF templates...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overlay toggle-icon"></div>
|
||||
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
||||
<?php include('include/footer.php'); ?>
|
||||
@@ -104,46 +204,148 @@
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const allTemplates = [];
|
||||
|
||||
const containers = {
|
||||
XLS: document.getElementById("templateButtonsXLS"),
|
||||
API: document.getElementById("templateButtonsAPI"),
|
||||
PDF: document.getElementById("templateButtonsPDF")
|
||||
};
|
||||
|
||||
function getSizeClass(buttonSize) {
|
||||
if (buttonSize === "small") return "btn-small";
|
||||
if (buttonSize === "large") return "btn-large";
|
||||
return "btn-medium";
|
||||
}
|
||||
|
||||
function getTemplateIcon(sourceType) {
|
||||
switch ((sourceType || '').toUpperCase()) {
|
||||
case 'XLS':
|
||||
return 'bx bx-spreadsheet';
|
||||
case 'API':
|
||||
return 'bx bx-transfer-alt';
|
||||
case 'PDF':
|
||||
return 'bx bx-file-pdf';
|
||||
default:
|
||||
return 'bx bx-file';
|
||||
}
|
||||
}
|
||||
|
||||
function createButton(template) {
|
||||
const sizeClass = getSizeClass(template.button_size);
|
||||
const sourceType = (template.source_type || '').toUpperCase();
|
||||
const iconClass = getTemplateIcon(sourceType);
|
||||
|
||||
const btn = document.createElement("a");
|
||||
|
||||
// Redirect based on template source type
|
||||
if (sourceType === 'XLS') {
|
||||
btn.href = `import_xls2.php?id=${template.id}`;
|
||||
} else if (sourceType === 'API' || sourceType === 'JSON') {
|
||||
btn.href = `import_json.php?id=${template.id}`;
|
||||
} else if (sourceType === 'PDF') {
|
||||
btn.href = `import_pdf.php?id=${template.id}`;
|
||||
} else {
|
||||
btn.href = `import_xls2.php?id=${template.id}`;
|
||||
}
|
||||
|
||||
btn.className = `btn ${sizeClass}`;
|
||||
btn.style.backgroundColor = template.button_bg_color || '#0d6efd';
|
||||
btn.style.color = template.button_text_color || '#ffffff';
|
||||
btn.setAttribute("data-label", (template.button_label || '').toLowerCase());
|
||||
btn.setAttribute("data-source-type", sourceType);
|
||||
|
||||
btn.innerHTML = `
|
||||
<i class="${iconClass} template-icon"></i>
|
||||
<span>${escapeHtml(template.button_label || 'Unnamed')}</span>
|
||||
`;
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function clearContainers() {
|
||||
Object.values(containers).forEach(container => {
|
||||
container.innerHTML = '';
|
||||
});
|
||||
}
|
||||
|
||||
function renderTemplates(searchValue = '') {
|
||||
clearContainers();
|
||||
|
||||
const grouped = {
|
||||
XLS: [],
|
||||
API: [],
|
||||
PDF: []
|
||||
};
|
||||
|
||||
allTemplates.forEach(template => {
|
||||
const sourceType = (template.source_type || '').toUpperCase();
|
||||
const label = (template.button_label || '').toLowerCase();
|
||||
|
||||
if (searchValue && !label.includes(searchValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (grouped[sourceType]) {
|
||||
grouped[sourceType].push(template);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(grouped).forEach(type => {
|
||||
const container = containers[type];
|
||||
if (!container) return;
|
||||
|
||||
if (grouped[type].length === 0) {
|
||||
container.innerHTML = `<div class="empty-message">No templates found in this section.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
grouped[type].forEach(template => {
|
||||
container.appendChild(createButton(template));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fetch("load_active_templates.php")
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (!data.success) {
|
||||
console.error("Error loading templates:", data.message);
|
||||
clearContainers();
|
||||
Object.values(containers).forEach(container => {
|
||||
container.innerHTML = `<div class="empty-message">Error loading templates.</div>`;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let templateButtons = document.getElementById("templateButtons");
|
||||
templateButtons.innerHTML = '';
|
||||
|
||||
data.data.forEach(template => {
|
||||
let sizeClass = "btn-medium"; // Default
|
||||
if (template.button_size === "small") sizeClass = "btn-small";
|
||||
if (template.button_size === "large") sizeClass = "btn-large";
|
||||
|
||||
let btn = document.createElement("a");
|
||||
btn.href = `import_xls2.php?id=${template.id}`;
|
||||
btn.className = `btn ${sizeClass}`;
|
||||
btn.style.backgroundColor = template.button_bg_color;
|
||||
btn.style.color = template.button_text_color;
|
||||
btn.textContent = template.button_label;
|
||||
btn.setAttribute("data-label", template.button_label.toLowerCase()); // Attributo per ricerca
|
||||
|
||||
templateButtons.appendChild(btn);
|
||||
});
|
||||
})
|
||||
.catch(error => console.error("Fetch error:", error));
|
||||
|
||||
// Funzione per la ricerca live
|
||||
document.getElementById("searchInput").addEventListener("input", function() {
|
||||
let searchValue = this.value.toLowerCase();
|
||||
document.querySelectorAll("#templateButtons a").forEach(btn => {
|
||||
let label = btn.getAttribute("data-label");
|
||||
if (label.includes(searchValue)) {
|
||||
btn.style.display = "inline-block";
|
||||
} else {
|
||||
btn.style.display = "none";
|
||||
if (!Array.isArray(data.data)) {
|
||||
clearContainers();
|
||||
Object.values(containers).forEach(container => {
|
||||
container.innerHTML = `<div class="empty-message">Invalid response format.</div>`;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
allTemplates.push(...data.data);
|
||||
renderTemplates();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Fetch error:", error);
|
||||
clearContainers();
|
||||
Object.values(containers).forEach(container => {
|
||||
container.innerHTML = `<div class="empty-message">Fetch error while loading templates.</div>`;
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("searchInput").addEventListener("input", function() {
|
||||
const searchValue = this.value.toLowerCase().trim();
|
||||
renderTemplates(searchValue);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
|
||||
// ✅ FIX timezone (Rome)
|
||||
ini_set('date.timezone', 'Europe/Rome');
|
||||
date_default_timezone_set('Europe/Rome');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['template_id']) || !isset($_POST['selected_rows']) || !isset($_POST['filename'])) {
|
||||
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Richiesta non valida"));
|
||||
exit;
|
||||
|
||||
@@ -12,6 +12,7 @@ if (!file_exists(__DIR__ . '/import_debug.log')) {
|
||||
error_log("Inizio importazione alle " . date('Y-m-d H:i:s'));
|
||||
|
||||
include('include/headscript.php');
|
||||
require_once(__DIR__ . '/class/binding-functions.php');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['template_id']) || !isset($_POST['selected_rows']) || !isset($_POST['filename'])) {
|
||||
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Richiesta non valida"));
|
||||
@@ -25,6 +26,7 @@ $rows = json_decode(urldecode($_POST['rows'] ?? '[]'), true);
|
||||
$excelrows = json_decode(urldecode($_POST['excelrows'] ?? '[]'), true);
|
||||
|
||||
$newFilename = $_POST['filename'];
|
||||
$source_type = strtolower(trim($_POST['source_type'] ?? 'xls'));
|
||||
|
||||
$_SESSION['template_id'] = $template_id;
|
||||
$_SESSION['selected_rows'] = $selected_rows;
|
||||
@@ -37,6 +39,7 @@ error_log("Received Data - Template ID: $template_id, Selected Rows: " . json_en
|
||||
error_log("Columns: " . json_encode($columns));
|
||||
error_log("Rows: " . json_encode($rows));
|
||||
error_log("Excelrows: " . json_encode($excelrows));
|
||||
error_log("Source type: " . $source_type);
|
||||
|
||||
$user_id = $iduserlogin ?? 1;
|
||||
|
||||
@@ -47,7 +50,23 @@ $pdo = $db->getConnection();
|
||||
$importReferenceCode = date('YmdHis') . '-' . uniqid();
|
||||
|
||||
// Recupera tutti i mapping dal template
|
||||
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field, auto_value FROM template_mapping WHERE template_id = ?");
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
id,
|
||||
excel_column,
|
||||
json_node,
|
||||
data_type,
|
||||
is_required,
|
||||
manual_default,
|
||||
is_manual,
|
||||
field_label,
|
||||
field_id,
|
||||
main_field,
|
||||
auto_value,
|
||||
has_list
|
||||
FROM template_mapping
|
||||
WHERE template_id = ?
|
||||
");
|
||||
$stmt->execute([$template_id]);
|
||||
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
@@ -67,12 +86,27 @@ foreach ($allMappings as $mapping) {
|
||||
|
||||
// Inserisci le righe selezionate in datadb
|
||||
$insertedIds = [];
|
||||
foreach ($selected_rows as $rowIndex) {
|
||||
$row = $rows[$rowIndex] ?? null;
|
||||
$excelrow = $excelrows[$rowIndex] ?? null;
|
||||
|
||||
// Binding JSON -> LIMS senza corrispondenza salvata, per "mapping_id|json_value".
|
||||
$pendingBindings = [];
|
||||
// Binding risolti in automatico durante questo import (solo per visualizzazione).
|
||||
$autoBindings = [];
|
||||
// Binding gia' salvati in precedenza, usati su questo import (visualizzazione + modifica).
|
||||
$savedBindings = [];
|
||||
foreach ($selected_rows as $loopIndex => $rowIndex) {
|
||||
|
||||
if ($source_type === 'json') {
|
||||
// JSON import sends only selected rows in rows/excelrows
|
||||
$row = $rows[$loopIndex] ?? null;
|
||||
$excelrow = $excelrows[$loopIndex] ?? ('JSON-' . ($loopIndex + 1));
|
||||
} else {
|
||||
// XLS import keeps original row indexes
|
||||
$row = $rows[$rowIndex] ?? null;
|
||||
$excelrow = $excelrows[$rowIndex] ?? null;
|
||||
}
|
||||
|
||||
if ($row === null || $excelrow === null) {
|
||||
error_log("Errore: riga o excelrow mancante per rowIndex $rowIndex");
|
||||
error_log("Errore: riga o excelrow mancante. Source type: $source_type, loopIndex: $loopIndex, rowIndex: $rowIndex");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -82,6 +116,14 @@ foreach ($selected_rows as $rowIndex) {
|
||||
$template = $template_stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$default_idclient = $template['idclient'] ?? null;
|
||||
|
||||
// excelrow e' INT: dal JSON arriva tipo 'JSON-1', tengo solo la parte numerica.
|
||||
if (is_numeric($excelrow)) {
|
||||
$excelrowDb = (int) $excelrow;
|
||||
} else {
|
||||
$digits = preg_replace('/\D+/', '', (string) $excelrow);
|
||||
$excelrowDb = $digits !== '' ? (int) $digits : ($loopIndex + 1);
|
||||
}
|
||||
|
||||
$values = [
|
||||
$template_id,
|
||||
$importReferenceCode,
|
||||
@@ -90,7 +132,7 @@ foreach ($selected_rows as $rowIndex) {
|
||||
$user_id,
|
||||
null,
|
||||
date('Y-m-d'),
|
||||
$excelrow,
|
||||
$excelrowDb,
|
||||
$default_idclient // Aggiungi idclient
|
||||
];
|
||||
$sql = "INSERT INTO datadb (templateid, importreferencecode, filename_import, status, user_id, limscode, importdate, excelrow, idclient) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
@@ -104,14 +146,72 @@ foreach ($selected_rows as $rowIndex) {
|
||||
foreach ($allMappings as $mapping) {
|
||||
$fieldValue = null;
|
||||
if (!$mapping['is_manual']) {
|
||||
$excelColumn = trim($mapping['excel_column']);
|
||||
$excelColumnIndex = array_search($excelColumn, array_map('trim', $columns));
|
||||
if ($excelColumnIndex !== false && isset($row[$excelColumnIndex]) && $row[$excelColumnIndex] !== '') {
|
||||
$fieldValue = $row[$excelColumnIndex];
|
||||
error_log("Found Excel column '$excelColumn' at index $excelColumnIndex, value: " . var_export($fieldValue, true));
|
||||
$sourceColumn = '';
|
||||
|
||||
if ($source_type === 'json') {
|
||||
$sourceColumn = trim($mapping['json_node'] ?? '');
|
||||
} else {
|
||||
$sourceColumn = trim($mapping['excel_column'] ?? '');
|
||||
}
|
||||
|
||||
// Fallback: if JSON node is empty, try excel_column
|
||||
if ($sourceColumn === '') {
|
||||
$sourceColumn = trim($mapping['excel_column'] ?? '');
|
||||
}
|
||||
|
||||
$columnsTrimmed = array_map('trim', $columns);
|
||||
|
||||
$candidateColumns = [];
|
||||
|
||||
if ($sourceColumn !== '') {
|
||||
$candidateColumns[] = $sourceColumn;
|
||||
|
||||
if ($source_type === 'json') {
|
||||
// Common JSON path variants
|
||||
$candidateColumns[] = preg_replace('/^data\[\]\./', '', $sourceColumn);
|
||||
$candidateColumns[] = preg_replace('/^data\.0\./', '', $sourceColumn);
|
||||
$candidateColumns[] = str_replace('data[].', 'data.0.', $sourceColumn);
|
||||
$candidateColumns[] = str_replace('data.0.', 'data[].', $sourceColumn);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove empty and duplicate candidates
|
||||
$candidateColumns = array_values(array_unique(array_filter($candidateColumns, function ($value) {
|
||||
return trim((string)$value) !== '';
|
||||
})));
|
||||
|
||||
$sourceColumnIndex = false;
|
||||
$matchedColumn = '';
|
||||
|
||||
foreach ($candidateColumns as $candidateColumn) {
|
||||
$candidateColumn = trim($candidateColumn);
|
||||
$index = array_search($candidateColumn, $columnsTrimmed);
|
||||
|
||||
if ($index !== false) {
|
||||
$sourceColumnIndex = $index;
|
||||
$matchedColumn = $candidateColumn;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($sourceColumnIndex !== false && isset($row[$sourceColumnIndex]) && $row[$sourceColumnIndex] !== '') {
|
||||
$fieldValue = $row[$sourceColumnIndex];
|
||||
|
||||
error_log(
|
||||
"Found source column. Original: '$sourceColumn', Matched: '$matchedColumn', Index: $sourceColumnIndex, Value: " .
|
||||
var_export($fieldValue, true)
|
||||
);
|
||||
} else {
|
||||
$fieldValue = $mapping['manual_default'] ?? '';
|
||||
error_log("Excel column '$excelColumn' not found or empty, using default: " . var_export($fieldValue, true));
|
||||
|
||||
error_log(
|
||||
"Source column not found or empty. Original: '$sourceColumn'. Candidates: " .
|
||||
json_encode($candidateColumns) .
|
||||
". Available columns: " .
|
||||
json_encode($columnsTrimmed) .
|
||||
". Using default: " .
|
||||
var_export($fieldValue, true)
|
||||
);
|
||||
}
|
||||
switch ($mapping['data_type']) {
|
||||
case 'INT':
|
||||
@@ -144,10 +244,84 @@ foreach ($selected_rows as $rowIndex) {
|
||||
}
|
||||
}
|
||||
|
||||
// Binding JSON -> LIMS solo per i campi a lista importati da JSON.
|
||||
if (
|
||||
$source_type === 'json'
|
||||
&& !$mapping['is_manual']
|
||||
&& binding_is_list_field($mapping)
|
||||
&& $fieldValue !== null
|
||||
&& $fieldValue !== ''
|
||||
) {
|
||||
$jsonValue = (string) $fieldValue;
|
||||
$existing = binding_lookup($pdo, (int) $mapping['id'], $jsonValue);
|
||||
|
||||
if ($existing) {
|
||||
$fieldValue = $existing['lims_value'];
|
||||
|
||||
$key = $mapping['id'] . '|' . $jsonValue;
|
||||
if (!isset($savedBindings[$key])) {
|
||||
$savedBindings[$key] = [
|
||||
'mapping_id' => (int) $mapping['id'],
|
||||
'field_id' => (int) $mapping['field_id'],
|
||||
'field_label' => $mapping['field_label'],
|
||||
'json_value' => $jsonValue,
|
||||
'lims_value' => (string) $existing['lims_value'],
|
||||
'lims_value_id' => (int) $existing['lims_value_id'],
|
||||
'datadb_ids' => [],
|
||||
];
|
||||
}
|
||||
$savedBindings[$key]['datadb_ids'][] = (int) $iddatadb;
|
||||
} else {
|
||||
// Nessun binding salvato: provo l'auto-match 1-a-1 sui valori LIMS.
|
||||
$limsValues = binding_get_lims_values((int) $mapping['field_id']);
|
||||
$autoMatch = binding_auto_match($limsValues, $jsonValue);
|
||||
|
||||
if ($autoMatch) {
|
||||
binding_upsert(
|
||||
$pdo,
|
||||
(int) $template_id,
|
||||
(int) $mapping['id'],
|
||||
(int) $mapping['field_id'],
|
||||
$jsonValue,
|
||||
(int) $autoMatch['IdCustomFieldsValue'],
|
||||
(string) $autoMatch['Valore'],
|
||||
$user_id
|
||||
);
|
||||
$fieldValue = (string) $autoMatch['Valore'];
|
||||
|
||||
$key = $mapping['id'] . '|' . $jsonValue;
|
||||
if (!isset($autoBindings[$key])) {
|
||||
$autoBindings[$key] = [
|
||||
'mapping_id' => (int) $mapping['id'],
|
||||
'field_id' => (int) $mapping['field_id'],
|
||||
'field_label' => $mapping['field_label'],
|
||||
'json_value' => $jsonValue,
|
||||
'lims_value' => (string) $autoMatch['Valore'],
|
||||
'lims_value_id' => (int) $autoMatch['IdCustomFieldsValue'],
|
||||
'datadb_ids' => [],
|
||||
];
|
||||
}
|
||||
$autoBindings[$key]['datadb_ids'][] = (int) $iddatadb;
|
||||
} else {
|
||||
$key = $mapping['id'] . '|' . $jsonValue;
|
||||
if (!isset($pendingBindings[$key])) {
|
||||
$pendingBindings[$key] = [
|
||||
'mapping_id' => (int) $mapping['id'],
|
||||
'field_id' => (int) $mapping['field_id'],
|
||||
'field_label' => $mapping['field_label'],
|
||||
'json_value' => $jsonValue,
|
||||
'datadb_ids' => [],
|
||||
];
|
||||
}
|
||||
$pendingBindings[$key]['datadb_ids'][] = (int) $iddatadb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($mapping['is_required'] && (is_null($fieldValue) || $fieldValue === '')) {
|
||||
error_log("Required field missing for mapping ID: " . $mapping['id'] . ", field: " . $mapping['field_label']);
|
||||
}
|
||||
error_log("Inserting into import_data_details - Mapping ID: " . $mapping['id'] . ", Field Value: " . var_export($fieldValue, true) . ", Is Manual: " . $mapping['is_manual'] . ", Excel Column: " . ($mapping['excel_column'] ?? 'N/A') . ", Manual Default: " . ($mapping['manual_default'] ?? 'N/A'));
|
||||
error_log("Inserting into import_data_details - Mapping ID: " . $mapping['id'] . ", Field Value: " . var_export($fieldValue, true) . ", Is Manual: " . $mapping['is_manual'] . ", Source Column: " . ($sourceColumn ?? 'N/A') . ", Source Type: " . $source_type . ", Manual Default: " . ($mapping['manual_default'] ?? 'N/A'));
|
||||
$stmt = $pdo->prepare("INSERT INTO import_data_details (id, mapping_id, field_value) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$iddatadb, $mapping['id'], $fieldValue]);
|
||||
error_log("Inserted into import_data_details for ID $iddatadb, Mapping ID: " . $mapping['id'] . ", Field Value: " . var_export($fieldValue, true));
|
||||
@@ -156,6 +330,28 @@ foreach ($selected_rows as $rowIndex) {
|
||||
|
||||
$_SESSION['inserted_ids'] = $insertedIds;
|
||||
|
||||
header("Location: imported.php?id=" . urlencode($template_id) . "&importref=" . urlencode($importReferenceCode));
|
||||
$importedUrl = "imported.php?id=" . urlencode($template_id) . "&importref=" . urlencode($importReferenceCode);
|
||||
|
||||
// Solo se restano binding da risolvere mostro la pagina (con anche gli auto, modificabili).
|
||||
if (!empty($pendingBindings)) {
|
||||
$_SESSION['pending_bindings'] = [
|
||||
'template_id' => $template_id,
|
||||
'importref' => $importReferenceCode,
|
||||
'items' => array_values($pendingBindings),
|
||||
'auto' => array_values($autoBindings),
|
||||
'saved' => array_values($savedBindings),
|
||||
];
|
||||
|
||||
header("Location: resolve_bindings.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
unset($_SESSION['pending_bindings']);
|
||||
|
||||
// Solo auto-collegati: vado diretto alla griglia, segnalando quanti.
|
||||
if (!empty($autoBindings)) {
|
||||
$importedUrl .= "&autobound=" . count($autoBindings);
|
||||
}
|
||||
|
||||
header("Location: " . $importedUrl);
|
||||
exit;
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,829 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
|
||||
// Check if a valid template ID has been provided
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Invalid ID"));
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = intval($_GET['id']);
|
||||
|
||||
// Load template
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
$stmt = $pdo->prepare("SELECT * FROM excel_templates WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$template = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$template) {
|
||||
header("Location: template_dashboard.php?status=error&message=" . urlencode("Template not found"));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check mappings
|
||||
$stmt = $pdo->prepare("SELECT id FROM template_mapping WHERE template_id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$hasMappings = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
error_log("Loaded JSON import template: " . print_r($template, true));
|
||||
?>
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
<?php include('cssinclude.php'); ?>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.top-scrollbar {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
width: 100%;
|
||||
height: 18px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
background: #f8f9fa;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.top-scrollbar-inner {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
border: 1px solid #dee2e6;
|
||||
min-width: 120px;
|
||||
max-width: 260px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table th:first-child,
|
||||
.table td:first-child {
|
||||
min-width: 50px;
|
||||
max-width: 50px;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background-color: #f8f9fa;
|
||||
position: relative;
|
||||
cursor: col-resize;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.table th .resize-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 5px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.table th .resize-handle:hover {
|
||||
background: #007bff;
|
||||
}
|
||||
|
||||
.loader {
|
||||
display: none;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #3498db;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.column-filters th {
|
||||
background: #ffffff;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.column-filters input {
|
||||
width: 100%;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.json-code-input {
|
||||
font-size: 1.15rem;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.json-paste-area {
|
||||
min-height: 260px;
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.json-help-box {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.source-badge {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
<title><?= htmlspecialchars($template['name']) ?> - JSON Import - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
<?php include('top_stat_widget.php'); ?>
|
||||
|
||||
<div class="mb-3 text">
|
||||
<a href="imported.php?id=<?= $id ?>" class="btn btn-warning me-2">Imported (i)</a>
|
||||
<a href="tolims.php?id=<?= $id ?>" class="btn btn-success me-2">To LIMS (l)</a>
|
||||
<a href="bindings_manage.php?template_id=<?= $id ?>" class="btn btn-outline-secondary">Binding JSON -> LIMS</a>
|
||||
</div>
|
||||
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<div>
|
||||
<h6 class="mb-0"><?= htmlspecialchars($template['name']) ?> - JSON Import</h6>
|
||||
<small>
|
||||
Template ID: <?= $id ?>
|
||||
</small>
|
||||
</div>
|
||||
<span class="badge bg-info text-dark">JSON mode</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<?php if (!$hasMappings): ?>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
Nessun mapping trovato per questo template. Configura i mapping prima di procedere.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="json-help-box mb-3">
|
||||
<strong>Flusso:</strong> inserisci/scansiona un codice per recuperare il JSON da API, oppure incolla manualmente un JSON nel secondo tab.
|
||||
Ogni JSON aggiunto diventa una riga della tabella di preview. Quando hai finito, seleziona le righe e clicca <strong>Prosegui</strong>.
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs" id="jsonImportTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="api-code-tab" data-bs-toggle="tab" data-bs-target="#api-code-pane" type="button" role="tab" aria-controls="api-code-pane" aria-selected="true">
|
||||
Code / Barcode
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="paste-json-tab" data-bs-toggle="tab" data-bs-target="#paste-json-pane" type="button" role="tab" aria-controls="paste-json-pane" aria-selected="false">
|
||||
Paste JSON
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content border border-top-0 p-3 mb-3" id="jsonImportTabsContent">
|
||||
<div class="tab-pane fade show active" id="api-code-pane" role="tabpanel" aria-labelledby="api-code-tab" tabindex="0">
|
||||
<form id="jsonCodeForm" class="row g-3 align-items-end">
|
||||
<div class="col-lg-8">
|
||||
<label for="json_code" class="form-label">Code / Barcode</label>
|
||||
<input type="text" class="form-control json-code-input" id="json_code" name="json_code" placeholder="Write or scan code" autocomplete="off" <?= !$hasMappings ? 'disabled' : '' ?>>
|
||||
<small class="text-muted">Lo scanner barcode normalmente scrive qui il codice e invia Enter.</small>
|
||||
</div>
|
||||
<div class="col-lg-4 d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary flex-fill" <?= !$hasMappings ? 'disabled' : '' ?>>Load JSON</button>
|
||||
<button type="button" class="btn btn-outline-secondary" id="clearCodeBtn">Clear</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="paste-json-pane" role="tabpanel" aria-labelledby="paste-json-tab" tabindex="0">
|
||||
<form id="pasteJsonForm">
|
||||
<div class="mb-3">
|
||||
<label for="manual_json_reference" class="form-label">Reference / filename</label>
|
||||
<input type="text" class="form-control" id="manual_json_reference" placeholder="Optional reference, e.g. manual-json-001">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="manual_json" class="form-label">Paste JSON</label>
|
||||
<textarea class="form-control json-paste-area" id="manual_json" placeholder='{"data":[{"id":"MM000620","attributes":{"trf_type":"apparel"}}]}' <?= !$hasMappings ? 'disabled' : '' ?>></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" <?= !$hasMappings ? 'disabled' : '' ?>>Add pasted JSON</button>
|
||||
<button type="button" class="btn btn-outline-secondary" id="clearManualJsonBtn">Clear</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="loader" id="loader"></div>
|
||||
<div id="errorContainer" class="alert alert-danger mt-3" style="display:none;"></div>
|
||||
<div id="successContainer" class="alert alert-success mt-3" style="display:none;"></div>
|
||||
|
||||
<div id="tableContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overlay toggle-icon"></div>
|
||||
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.min.js"></script>
|
||||
<?php include('jsinclude.php'); ?>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const TEMPLATE_ID = <?= (int)$id ?>;
|
||||
const API_ENDPOINT = 'get_json_by_code.php';
|
||||
const INCLUDE_SOURCE_CODE_COLUMN = true;
|
||||
const UNWRAP_SINGLE_DATA_ITEM = true;
|
||||
|
||||
const jsonCodeForm = document.getElementById('jsonCodeForm');
|
||||
const pasteJsonForm = document.getElementById('pasteJsonForm');
|
||||
const jsonCodeInput = document.getElementById('json_code');
|
||||
const manualJsonInput = document.getElementById('manual_json');
|
||||
const manualJsonReferenceInput = document.getElementById('manual_json_reference');
|
||||
const clearCodeBtn = document.getElementById('clearCodeBtn');
|
||||
const clearManualJsonBtn = document.getElementById('clearManualJsonBtn');
|
||||
const loader = document.getElementById('loader');
|
||||
const errorContainer = document.getElementById('errorContainer');
|
||||
const successContainer = document.getElementById('successContainer');
|
||||
const tableContainer = document.getElementById('tableContainer');
|
||||
|
||||
let jsonRows = [];
|
||||
let columns = [];
|
||||
|
||||
if (jsonCodeInput && !jsonCodeInput.disabled) {
|
||||
jsonCodeInput.focus();
|
||||
}
|
||||
|
||||
jsonCodeForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const code = jsonCodeInput.value.trim();
|
||||
|
||||
if (!code) {
|
||||
showError('Inserisci o scansiona un codice.');
|
||||
return;
|
||||
}
|
||||
|
||||
loadJsonFromApi(code);
|
||||
});
|
||||
|
||||
pasteJsonForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const rawJson = manualJsonInput.value.trim();
|
||||
const reference = manualJsonReferenceInput.value.trim() || 'manual-json-' + (jsonRows.length + 1);
|
||||
|
||||
if (!rawJson) {
|
||||
showError('Incolla un JSON prima di aggiungerlo.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedJson = JSON.parse(rawJson);
|
||||
addJsonRow(parsedJson, reference, 'paste');
|
||||
manualJsonInput.value = '';
|
||||
manualJsonReferenceInput.value = '';
|
||||
showSuccess('JSON incollato aggiunto correttamente.');
|
||||
} catch (err) {
|
||||
showError('JSON non valido: ' + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
clearCodeBtn.addEventListener('click', function() {
|
||||
jsonCodeInput.value = '';
|
||||
jsonCodeInput.focus();
|
||||
});
|
||||
|
||||
clearManualJsonBtn.addEventListener('click', function() {
|
||||
manualJsonInput.value = '';
|
||||
manualJsonReferenceInput.value = '';
|
||||
});
|
||||
|
||||
function loadJsonFromApi(code) {
|
||||
hideMessages();
|
||||
loader.style.display = 'block';
|
||||
|
||||
fetch(API_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
template_id: TEMPLATE_ID,
|
||||
code: code
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('HTTP status ' + response.status);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(responseData => {
|
||||
loader.style.display = 'none';
|
||||
|
||||
if (responseData.error) {
|
||||
showError(responseData.error);
|
||||
return;
|
||||
}
|
||||
|
||||
let jsonPayload = responseData;
|
||||
let reference = code;
|
||||
|
||||
// Supported endpoint response format:
|
||||
// { success: true, json: {...}, reference: "..." }
|
||||
if (responseData.json !== undefined) {
|
||||
jsonPayload = responseData.json;
|
||||
reference = responseData.reference || responseData.filename || responseData.code || code;
|
||||
}
|
||||
|
||||
addJsonRow(jsonPayload, reference, 'api');
|
||||
jsonCodeInput.value = '';
|
||||
jsonCodeInput.focus();
|
||||
showSuccess('JSON recuperato e aggiunto correttamente.');
|
||||
})
|
||||
.catch(error => {
|
||||
loader.style.display = 'none';
|
||||
showError('Errore durante il recupero del JSON: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
function addJsonRow(jsonPayload, reference, sourceType) {
|
||||
// Ogni elemento di data[] diventa una riga (non colonne data.0.*, data.1.*).
|
||||
const records = extractRecords(jsonPayload);
|
||||
|
||||
records.forEach((record, recordIndex) => {
|
||||
const flattened = flattenJson(record);
|
||||
|
||||
const rowReference = records.length > 1 ?
|
||||
reference + '#' + (recordIndex + 1) :
|
||||
reference;
|
||||
|
||||
if (INCLUDE_SOURCE_CODE_COLUMN) {
|
||||
flattened.source_code = rowReference;
|
||||
flattened.source_type = sourceType;
|
||||
}
|
||||
|
||||
const newColumns = Object.keys(flattened).filter(col => !columns.includes(col));
|
||||
columns = columns.concat(newColumns);
|
||||
|
||||
jsonRows.push({
|
||||
excelrow: 'JSON-' + (jsonRows.length + 1),
|
||||
reference: rowReference,
|
||||
sourceType: sourceType,
|
||||
flat: flattened
|
||||
});
|
||||
});
|
||||
|
||||
renderTable();
|
||||
}
|
||||
|
||||
// Record da trasformare in righe: gli oggetti in data[], altrimenti il payload stesso.
|
||||
function extractRecords(payload) {
|
||||
if (
|
||||
payload &&
|
||||
typeof payload === 'object' &&
|
||||
!Array.isArray(payload) &&
|
||||
Array.isArray(payload.data) &&
|
||||
payload.data.length > 0
|
||||
) {
|
||||
const items = payload.data.filter(item => item && typeof item === 'object');
|
||||
if (items.length > 0) {
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
return [payload];
|
||||
}
|
||||
|
||||
function flattenJson(value, prefix = '', result = {}) {
|
||||
if (value === null || value === undefined) {
|
||||
result[prefix || 'value'] = '';
|
||||
return result;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) {
|
||||
result[prefix || 'value'] = '';
|
||||
return result;
|
||||
}
|
||||
|
||||
value.forEach((item, index) => {
|
||||
const newPrefix = prefix ? prefix + '.' + index : String(index);
|
||||
flattenJson(item, newPrefix, result);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
const keys = Object.keys(value);
|
||||
if (keys.length === 0) {
|
||||
result[prefix || 'value'] = '';
|
||||
return result;
|
||||
}
|
||||
|
||||
keys.forEach(key => {
|
||||
const newPrefix = prefix ? prefix + '.' + key : key;
|
||||
flattenJson(value[key], newPrefix, result);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
result[prefix || 'value'] = String(value);
|
||||
return result;
|
||||
}
|
||||
|
||||
function buildImportData() {
|
||||
const rows = jsonRows.map(row => columns.map(col => row.flat[col] ?? ''));
|
||||
const excelData = rows.map((rowData, index) => ({
|
||||
excelrow: jsonRows[index].excelrow,
|
||||
data: rowData
|
||||
}));
|
||||
|
||||
return {
|
||||
template_id: TEMPLATE_ID,
|
||||
columns: columns,
|
||||
rows: rows,
|
||||
excel_data: excelData,
|
||||
filename: 'json_import_' + new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
||||
};
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
if (jsonRows.length === 0) {
|
||||
tableContainer.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const data = buildImportData();
|
||||
|
||||
let html = `
|
||||
<form id="selectRowsForm" action="import_insert.php" method="POST">
|
||||
<input type="hidden" name="template_id" value="${escapeHtml(data.template_id)}">
|
||||
<input type="hidden" name="columns" value="${encodeURIComponent(JSON.stringify(data.columns))}">
|
||||
<input type="hidden" name="rows" id="selectedRowsData" value="">
|
||||
<input type="hidden" name="excelrows" id="selectedExcelRowsData" value="">
|
||||
<input type="hidden" name="filename" value="${escapeHtml(data.filename)}">
|
||||
<input type="hidden" name="source_type" value="json">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
|
||||
<div>
|
||||
<strong>JSON rows loaded:</strong> ${jsonRows.length}
|
||||
<span class="badge bg-secondary source-badge ms-2">Columns: ${data.columns.length}</span>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" id="clearAllRowsBtn">Clear all rows</button>
|
||||
<button type="submit" class="btn btn-primary" id="proceedButtonTop" disabled>Prosegui</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="top-scrollbar" id="topTableScrollbar">
|
||||
<div class="top-scrollbar-inner" id="topTableScrollbarInner"></div>
|
||||
</div>
|
||||
|
||||
<div class="table-container" id="mainTableContainer">
|
||||
<table class="table table-striped table-bordered" id="importPreviewTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="selectAll"> Select</th>
|
||||
${data.columns.map(col => `<th title="${escapeHtml(col)}">${escapeHtml(readableColumnLabel(col))}<div class="resize-handle"></div></th>`).join('')}
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
<tr class="column-filters">
|
||||
<th></th>
|
||||
${data.columns.map((col, i) => `
|
||||
<th>
|
||||
<input type="text"
|
||||
class="form-control form-control-sm column-filter"
|
||||
data-col-index="${i}"
|
||||
placeholder="Filter...">
|
||||
</th>
|
||||
`).join('')}
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${data.excel_data.map((row, index) => `
|
||||
<tr data-row-index="${index}">
|
||||
<td><input type="checkbox" class="row-checkbox" name="selected_rows[]" value="${index}" data-excelrow="${escapeHtml(row.excelrow)}"></td>
|
||||
${row.data.map(cell => `<td title="${escapeHtml(cell)}">${escapeHtml(cell)}</td>`).join('')}
|
||||
<td><button type="button" class="btn btn-sm btn-outline-danger remove-row-btn" data-row-index="${index}">Remove</button></td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mt-3" id="proceedButtonBottom" disabled>Prosegui</button>
|
||||
</form>
|
||||
`;
|
||||
|
||||
tableContainer.innerHTML = html;
|
||||
bindTableEvents(data);
|
||||
}
|
||||
|
||||
function bindTableEvents(data) {
|
||||
const selectRowsForm = document.getElementById('selectRowsForm');
|
||||
const clearAllRowsBtn = document.getElementById('clearAllRowsBtn');
|
||||
const removeRowButtons = document.querySelectorAll('.remove-row-btn');
|
||||
|
||||
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));
|
||||
});
|
||||
|
||||
clearAllRowsBtn.addEventListener('click', function() {
|
||||
if (!confirm('Vuoi rimuovere tutte le righe JSON caricate?')) return;
|
||||
jsonRows = [];
|
||||
columns = [];
|
||||
renderTable();
|
||||
showSuccess('Righe JSON rimosse.');
|
||||
jsonCodeInput.focus();
|
||||
});
|
||||
|
||||
removeRowButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const rowIndex = parseInt(this.dataset.rowIndex, 10);
|
||||
jsonRows.splice(rowIndex, 1);
|
||||
rebuildColumnsFromRows();
|
||||
renderTable();
|
||||
});
|
||||
});
|
||||
|
||||
const topTableScrollbar = document.getElementById('topTableScrollbar');
|
||||
const topTableScrollbarInner = document.getElementById('topTableScrollbarInner');
|
||||
const mainTableContainer = document.getElementById('mainTableContainer');
|
||||
const importPreviewTable = document.getElementById('importPreviewTable');
|
||||
|
||||
function updateTopTableScrollbar() {
|
||||
if (!topTableScrollbar || !topTableScrollbarInner || !mainTableContainer || !importPreviewTable) return;
|
||||
|
||||
topTableScrollbarInner.style.width = importPreviewTable.scrollWidth + 'px';
|
||||
|
||||
if (mainTableContainer.scrollWidth > mainTableContainer.clientWidth) {
|
||||
topTableScrollbar.style.display = 'block';
|
||||
} else {
|
||||
topTableScrollbar.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
let syncingTop = false;
|
||||
let syncingBottom = false;
|
||||
|
||||
if (topTableScrollbar && mainTableContainer) {
|
||||
topTableScrollbar.addEventListener('scroll', function() {
|
||||
if (syncingBottom) return;
|
||||
syncingTop = true;
|
||||
mainTableContainer.scrollLeft = topTableScrollbar.scrollLeft;
|
||||
syncingTop = false;
|
||||
});
|
||||
|
||||
mainTableContainer.addEventListener('scroll', function() {
|
||||
if (syncingTop) return;
|
||||
syncingBottom = true;
|
||||
topTableScrollbar.scrollLeft = mainTableContainer.scrollLeft;
|
||||
syncingBottom = false;
|
||||
});
|
||||
}
|
||||
|
||||
updateTopTableScrollbar();
|
||||
setTimeout(updateTopTableScrollbar, 100);
|
||||
setTimeout(updateTopTableScrollbar, 300);
|
||||
window.addEventListener('resize', updateTopTableScrollbar);
|
||||
|
||||
const proceedButtonTop = document.getElementById('proceedButtonTop');
|
||||
const proceedButtonBottom = document.getElementById('proceedButtonBottom');
|
||||
const selectAllCheckbox = document.getElementById('selectAll');
|
||||
const checkboxes = document.querySelectorAll('.row-checkbox');
|
||||
|
||||
function updateProceedButton() {
|
||||
const enabled = Array.from(document.querySelectorAll('.row-checkbox')).some(cb => cb.checked);
|
||||
|
||||
if (proceedButtonTop) proceedButtonTop.disabled = !enabled;
|
||||
if (proceedButtonBottom) proceedButtonBottom.disabled = !enabled;
|
||||
}
|
||||
|
||||
selectAllCheckbox.addEventListener('change', function() {
|
||||
const visibleRows = Array.from(document.querySelectorAll('#importPreviewTable tbody tr'))
|
||||
.filter(row => row.style.display !== 'none');
|
||||
|
||||
visibleRows.forEach(row => {
|
||||
const checkbox = row.querySelector('.row-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.checked = this.checked;
|
||||
}
|
||||
});
|
||||
|
||||
updateProceedButton();
|
||||
});
|
||||
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
const visibleCheckboxes = Array.from(document.querySelectorAll('#importPreviewTable tbody tr'))
|
||||
.filter(row => row.style.display !== 'none')
|
||||
.map(row => row.querySelector('.row-checkbox'))
|
||||
.filter(cb => cb !== null);
|
||||
|
||||
selectAllCheckbox.checked =
|
||||
visibleCheckboxes.length > 0 &&
|
||||
visibleCheckboxes.every(cb => cb.checked);
|
||||
|
||||
updateProceedButton();
|
||||
});
|
||||
});
|
||||
|
||||
const thElements = document.querySelectorAll('#importPreviewTable th');
|
||||
thElements.forEach((th, index) => {
|
||||
if (index === 0) return;
|
||||
const resizeHandle = th.querySelector('.resize-handle');
|
||||
if (resizeHandle) {
|
||||
resizeHandle.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
const startX = e.clientX;
|
||||
const startWidth = th.offsetWidth;
|
||||
|
||||
const onMouseMove = (e) => {
|
||||
const newWidth = Math.max(60, startWidth + (e.clientX - startX));
|
||||
th.style.width = `${newWidth}px`;
|
||||
th.style.minWidth = `${newWidth}px`;
|
||||
th.style.maxWidth = `${newWidth}px`;
|
||||
|
||||
const cells = document.querySelectorAll(`#importPreviewTable td:nth-child(${index + 1})`);
|
||||
cells.forEach(cell => {
|
||||
cell.style.width = `${newWidth}px`;
|
||||
cell.style.minWidth = `${newWidth}px`;
|
||||
cell.style.maxWidth = `${newWidth}px`;
|
||||
});
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const rows = document.querySelectorAll('#importPreviewTable tbody tr');
|
||||
const filterInputs = document.querySelectorAll('.column-filter');
|
||||
const activeFilters = {};
|
||||
|
||||
function applyColumnFilters() {
|
||||
rows.forEach(row => {
|
||||
let visible = true;
|
||||
|
||||
for (const [colIndexStr, filterValue] of Object.entries(activeFilters)) {
|
||||
const colIndex = parseInt(colIndexStr, 10);
|
||||
const cell = row.cells[colIndex + 1];
|
||||
const cellText = (cell?.textContent || '').toLowerCase();
|
||||
const searchText = (filterValue || '').toLowerCase().trim();
|
||||
|
||||
if (searchText && !cellText.includes(searchText)) {
|
||||
visible = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
row.style.display = visible ? '' : 'none';
|
||||
});
|
||||
|
||||
const visibleCheckboxes = Array.from(document.querySelectorAll('#importPreviewTable tbody tr'))
|
||||
.filter(row => row.style.display !== 'none')
|
||||
.map(row => row.querySelector('.row-checkbox'))
|
||||
.filter(cb => cb !== null);
|
||||
|
||||
selectAllCheckbox.checked =
|
||||
visibleCheckboxes.length > 0 &&
|
||||
visibleCheckboxes.every(cb => cb.checked);
|
||||
|
||||
updateProceedButton();
|
||||
}
|
||||
|
||||
filterInputs.forEach(input => {
|
||||
input.addEventListener('input', function() {
|
||||
const colIndex = this.dataset.colIndex;
|
||||
activeFilters[colIndex] = this.value;
|
||||
applyColumnFilters();
|
||||
});
|
||||
});
|
||||
|
||||
updateProceedButton();
|
||||
}
|
||||
|
||||
function rebuildColumnsFromRows() {
|
||||
columns = [];
|
||||
jsonRows.forEach(row => {
|
||||
Object.keys(row.flat).forEach(col => {
|
||||
if (!columns.includes(col)) {
|
||||
columns.push(col);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function readableColumnLabel(columnName) {
|
||||
if (!columnName) return 'Column without name';
|
||||
return columnName;
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
successContainer.style.display = 'none';
|
||||
errorContainer.textContent = message;
|
||||
errorContainer.style.display = 'block';
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
errorContainer.style.display = 'none';
|
||||
successContainer.textContent = message;
|
||||
successContainer.style.display = 'block';
|
||||
setTimeout(() => {
|
||||
successContainer.style.display = 'none';
|
||||
}, 3500);
|
||||
}
|
||||
|
||||
function hideMessages() {
|
||||
errorContainer.style.display = 'none';
|
||||
successContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -40,6 +40,22 @@ error_log("Loaded template: " . print_r($template, true));
|
||||
<?php include('cssinclude.php'); ?>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.top-scrollbar {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
width: 100%;
|
||||
height: 18px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
background: #f8f9fa;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.top-scrollbar-inner {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
@@ -151,7 +167,12 @@ error_log("Loaded template: " . print_r($template, true));
|
||||
<div class="d-flex align-items-center">
|
||||
<div>
|
||||
<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>
|
||||
@@ -228,8 +249,9 @@ error_log("Loaded template: " . print_r($template, true));
|
||||
const templateId = <?= $id ?>;
|
||||
console.log('Template ID passed to formData:', templateId);
|
||||
formData.append('template_id', templateId);
|
||||
formData.append('header_row', <?= $template['header_row'] ?>);
|
||||
formData.append('start_column', <?= $template['start_column'] ?>);
|
||||
formData.append('header_row', <?= (int)$template['header_row'] ?>);
|
||||
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', {
|
||||
method: 'POST',
|
||||
@@ -315,8 +337,8 @@ error_log("Loaded template: " . print_r($template, true));
|
||||
<form id="selectRowsForm" action="import_insert.php" method="POST">
|
||||
<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="rows" value="${encodeURIComponent(JSON.stringify(data.rows))}">
|
||||
<input type="hidden" name="excelrows" value="${encodeURIComponent(JSON.stringify(data.excel_data.map(r => r.excelrow)))}">
|
||||
<input type="hidden" name="rows" id="selectedRowsData" value="">
|
||||
<input type="hidden" name="excelrows" id="selectedExcelRowsData" value="">
|
||||
<input type="hidden" name="filename" value="${data.filename}">
|
||||
|
||||
<!-- TOP BUTTON -->
|
||||
@@ -324,8 +346,12 @@ error_log("Loaded template: " . print_r($template, true));
|
||||
<button type="submit" class="btn btn-primary" id="proceedButtonTop" disabled>Prosegui</button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="table table-striped table-bordered">
|
||||
<div class="top-scrollbar" id="topTableScrollbar">
|
||||
<div class="top-scrollbar-inner" id="topTableScrollbarInner"></div>
|
||||
</div>
|
||||
|
||||
<div class="table-container" id="mainTableContainer">
|
||||
<table class="table table-striped table-bordered" id="importPreviewTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="selectAll"> Seleziona</th>
|
||||
@@ -363,6 +389,83 @@ error_log("Loaded template: " . print_r($template, true));
|
||||
`;
|
||||
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 topTableScrollbarInner = document.getElementById('topTableScrollbarInner');
|
||||
const mainTableContainer = document.getElementById('mainTableContainer');
|
||||
const importPreviewTable = document.getElementById('importPreviewTable');
|
||||
|
||||
function updateTopTableScrollbar() {
|
||||
if (!topTableScrollbar || !topTableScrollbarInner || !mainTableContainer || !importPreviewTable) return;
|
||||
|
||||
topTableScrollbarInner.style.width = importPreviewTable.scrollWidth + 'px';
|
||||
|
||||
if (mainTableContainer.scrollWidth > mainTableContainer.clientWidth) {
|
||||
topTableScrollbar.style.display = 'block';
|
||||
} else {
|
||||
topTableScrollbar.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
let syncingTop = false;
|
||||
let syncingBottom = false;
|
||||
|
||||
if (topTableScrollbar && mainTableContainer) {
|
||||
topTableScrollbar.addEventListener('scroll', function() {
|
||||
if (syncingBottom) return;
|
||||
syncingTop = true;
|
||||
mainTableContainer.scrollLeft = topTableScrollbar.scrollLeft;
|
||||
syncingTop = false;
|
||||
});
|
||||
|
||||
mainTableContainer.addEventListener('scroll', function() {
|
||||
if (syncingTop) return;
|
||||
syncingBottom = true;
|
||||
topTableScrollbar.scrollLeft = mainTableContainer.scrollLeft;
|
||||
syncingBottom = false;
|
||||
});
|
||||
}
|
||||
|
||||
updateTopTableScrollbar();
|
||||
setTimeout(updateTopTableScrollbar, 100);
|
||||
setTimeout(updateTopTableScrollbar, 300);
|
||||
window.addEventListener('resize', updateTopTableScrollbar);
|
||||
|
||||
const proceedButtonTop = document.getElementById('proceedButtonTop');
|
||||
const proceedButtonBottom = document.getElementById('proceedButtonBottom');
|
||||
const selectAllCheckbox = document.getElementById('selectAll');
|
||||
@@ -493,4 +596,4 @@ error_log("Loaded template: " . print_r($template, true));
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
+419
-139
@@ -1,10 +1,6 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
|
||||
// ✅ FIX timezone (Rome)
|
||||
ini_set('date.timezone', 'Europe/Rome');
|
||||
date_default_timezone_set('Europe/Rome');
|
||||
|
||||
$template_id = intval($_GET['id'] ?? 0);
|
||||
if (!$template_id) {
|
||||
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Template ID mancante"));
|
||||
@@ -24,9 +20,10 @@ $db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
// 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
|
||||
WHERE template_id = ?");
|
||||
WHERE template_id = ?
|
||||
ORDER BY field_order ASC, id ASC");
|
||||
$stmt->execute([$template_id]);
|
||||
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
@@ -59,15 +56,22 @@ if (empty($allMappings)) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Trova il campo main_field
|
||||
$mainFieldMapping = null;
|
||||
// Find up to 2 main fields
|
||||
$mainFieldMappings = [];
|
||||
|
||||
foreach ($allMappings as $mapping) {
|
||||
if ($mapping['main_field'] == 1 && $mapping['is_visible_import'] == 1) {
|
||||
$mainFieldMapping = $mapping;
|
||||
if ((string)$mapping['main_field'] === '1' && (int)$mapping['is_visible_import'] === 1) {
|
||||
$mainFieldMappings[] = $mapping;
|
||||
}
|
||||
|
||||
if (count($mainFieldMappings) >= 2) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Backward compatibility: first main field
|
||||
$mainFieldMapping = $mainFieldMappings[0] ?? null;
|
||||
|
||||
// Recupera l'idclient di default dal template (se presente)
|
||||
$template_stmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
|
||||
$template_stmt->execute([$template_id]);
|
||||
@@ -95,7 +99,7 @@ $stmt = $pdo->prepare("
|
||||
FROM datadb d
|
||||
LEFT JOIN auth_users u ON d.user_id = u.id
|
||||
{$baseWhere}
|
||||
ORDER BY d.iddatadb DESC
|
||||
ORDER BY d.excelrow ASC, d.iddatadb ASC
|
||||
{$limitClause}
|
||||
");
|
||||
$stmt->execute($baseParams);
|
||||
@@ -202,13 +206,13 @@ foreach ($importedData as $index => $row) {
|
||||
'status' => $row['status'] ?? 'i',
|
||||
'idclient' => $row['idclient'] ?? $default_idclient,
|
||||
'cliente_fornitore_id' => $row['cliente_fornitore_id'] ?? null,
|
||||
'tested_component' => $row['tested_component'] ?? '',
|
||||
'commessaweb' => $row['commessaweb'] ?? null,
|
||||
'user_name' => $row['user_name'] ?? '',
|
||||
'importreferencecode' => $row['importreferencecode'] ?? '',
|
||||
'filename_import' => $row['filename_import'] ?? '',
|
||||
'importdate' => $row['importdate'] ?? '',
|
||||
];
|
||||
|
||||
// Fixed fields
|
||||
$rowObj['fixedFields'] = [];
|
||||
foreach ($fixedFields as $f) {
|
||||
@@ -228,11 +232,18 @@ foreach ($importedData as $index => $row) {
|
||||
$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) {
|
||||
$mainDetail = array_filter($rowDetails, fn($d) => $d['mapping_id'] == $mainFieldMapping['id']);
|
||||
$mainDetail = reset($mainDetail) ?: ['field_value' => $mainFieldMapping['manual_default'] ?? ''];
|
||||
$rowObj['mainFieldValue'] = $mainDetail['field_value'] ?? $mainFieldMapping['manual_default'] ?? '';
|
||||
$rowObj['mainFieldValue'] = $rowObj['details'][(string)$mainFieldMapping['id']] ?? '';
|
||||
}
|
||||
|
||||
$rowObj['_dirty'] = false;
|
||||
@@ -242,18 +253,27 @@ foreach ($importedData as $index => $row) {
|
||||
// Build columns in display order
|
||||
$gridColumns = [];
|
||||
|
||||
// 1. Main field
|
||||
if ($mainFieldMapping) {
|
||||
$gridColumns[] = [
|
||||
'type' => 'main_field',
|
||||
'key' => (string)$mainFieldMapping['id'],
|
||||
'label' => $mainFieldMapping['field_label'],
|
||||
'dataType' => $mainFieldMapping['data_type'],
|
||||
'isManual' => (bool)$mainFieldMapping['is_manual'],
|
||||
'isRequired' => (bool)$mainFieldMapping['is_required'],
|
||||
'fieldId' => $mainFieldMapping['field_id'] ?? null,
|
||||
'width' => 150,
|
||||
];
|
||||
// 1. Main fields first, immediately after buttons
|
||||
foreach ($allMappings as $mapping) {
|
||||
if (
|
||||
(int)$mapping['is_visible_import'] === 1
|
||||
&& (string)$mapping['main_field'] === '1'
|
||||
&& trim((string)$mapping['field_label']) !== 'Tested Component:'
|
||||
) {
|
||||
$gridColumns[] = [
|
||||
'type' => 'main_field',
|
||||
'key' => (string)$mapping['id'],
|
||||
'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
|
||||
@@ -265,40 +285,30 @@ $gridColumns[] = ['type' => 'idclient', 'key' => 'idclient', 'label' => 'Client'
|
||||
// 4. Cliente Fornitore
|
||||
$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) {
|
||||
if (!$mapping['is_manual'] && $mapping['main_field'] != 1 && $mapping['is_visible_import'] == 1) {
|
||||
if (
|
||||
(int)$mapping['is_visible_import'] === 1
|
||||
&& (string)$mapping['main_field'] !== '1'
|
||||
&& trim((string)$mapping['field_label']) !== 'Tested Component:'
|
||||
) {
|
||||
$isMainField = ((string)$mapping['main_field'] === '1');
|
||||
|
||||
$gridColumns[] = [
|
||||
'type' => 'detail',
|
||||
'type' => $isMainField ? 'main_field' : 'detail',
|
||||
'key' => (string)$mapping['id'],
|
||||
'label' => $mapping['field_label'],
|
||||
'dataType' => $mapping['data_type'],
|
||||
'isManual' => false,
|
||||
'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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Manual fields
|
||||
foreach ($allMappings as $mapping) {
|
||||
if ($mapping['is_manual'] && $mapping['main_field'] != 1 && $mapping['is_visible_import'] == 1) {
|
||||
$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
|
||||
$gridColumns[] = ['type' => 'tested_component', 'key' => 'tested_component', 'label' => 'Tested Component', 'width' => 150];
|
||||
|
||||
@@ -336,14 +346,18 @@ $gridMeta = [
|
||||
'slugMapping' => $slugMapping,
|
||||
'timeLabels' => $timeLabels,
|
||||
'columns' => $gridColumns,
|
||||
'mainFieldMapping' => $mainFieldMapping,
|
||||
'mainFieldMapping' => $mainFieldMapping,
|
||||
'mainFieldMappings' => $mainFieldMappings,
|
||||
'totalRows' => count($gridDataArray),
|
||||
];
|
||||
|
||||
?>
|
||||
<script>
|
||||
window.gridData = <?= json_encode($gridDataArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||
window.gridMeta = <?= json_encode($gridMeta, 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) ?>;
|
||||
|
||||
// Visible records in the current imported.php page
|
||||
window.visibleIddatadbList = window.gridData.map(row => parseInt(row.iddatadb, 10)).filter(Boolean);
|
||||
</script>
|
||||
|
||||
<!doctype html>
|
||||
@@ -364,20 +378,10 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
input.auto-input,
|
||||
select.auto-input {
|
||||
background-color: #d4edda;
|
||||
}
|
||||
|
||||
input.manual-input,
|
||||
select.manual-input {
|
||||
background-color: #fff3cd;
|
||||
}
|
||||
|
||||
input.required-input,
|
||||
select.required-input {
|
||||
background-color: #f8d7da;
|
||||
}
|
||||
|
||||
|
||||
|
||||
input.required-input,
|
||||
select.required-input {
|
||||
@@ -416,17 +420,9 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
textarea.auto-input {
|
||||
background-color: #d4edda;
|
||||
}
|
||||
|
||||
textarea.manual-input {
|
||||
background-color: #fff3cd;
|
||||
}
|
||||
|
||||
textarea.required-input {
|
||||
background-color: #f8d7da;
|
||||
}
|
||||
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
@@ -463,6 +459,21 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.top-scrollbar {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
width: 100%;
|
||||
height: 18px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.top-scrollbar-inner {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.grid-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -663,7 +674,33 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -797,7 +834,7 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#photosModal > .modal-content {
|
||||
#photosModal>.modal-content {
|
||||
background-color: #fff;
|
||||
margin: 5% auto;
|
||||
padding: 20px;
|
||||
@@ -892,9 +929,17 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
|
||||
}
|
||||
|
||||
@keyframes new-row-pulse {
|
||||
0%, 100% { background-color: transparent; }
|
||||
50% { background-color: #cce5ff; }
|
||||
|
||||
0%,
|
||||
100% {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-color: #cce5ff;
|
||||
}
|
||||
}
|
||||
|
||||
.row-just-created {
|
||||
animation: new-row-pulse 1s ease-in-out 3;
|
||||
}
|
||||
@@ -1011,11 +1056,7 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.api-fixed-select.required-input:invalid,
|
||||
.api-fixed-select[required]:not([value]):not([data-select2-id]) {
|
||||
background-color: #f8d7da !important;
|
||||
border-color: #dc3545 !important;
|
||||
}
|
||||
|
||||
|
||||
/* ── Pagination bar ── */
|
||||
.pager-bar {
|
||||
@@ -1029,21 +1070,25 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.pager-rows-per-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pager-label {
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pager-limit-group {
|
||||
display: inline-flex;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pager-limit-btn {
|
||||
padding: 3px 10px;
|
||||
text-decoration: none;
|
||||
@@ -1052,24 +1097,41 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
|
||||
transition: background .15s, color .15s;
|
||||
font-weight: 500;
|
||||
}
|
||||
.pager-limit-btn:last-child { border-right: none; }
|
||||
.pager-limit-btn:hover { background: #e9ecef; text-decoration: none; color: #212529; }
|
||||
|
||||
.pager-limit-btn:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.pager-limit-btn:hover {
|
||||
background: #e9ecef;
|
||||
text-decoration: none;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.pager-limit-btn.active {
|
||||
background: #0d6efd;
|
||||
color: #fff;
|
||||
}
|
||||
.pager-limit-btn.active:hover { background: #0b5ed7; color: #fff; }
|
||||
|
||||
.pager-limit-btn.active:hover {
|
||||
background: #0b5ed7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.pager-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.pager-info {
|
||||
margin-right: 8px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pager-btn, .pager-num {
|
||||
|
||||
.pager-btn,
|
||||
.pager-num {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -1083,21 +1145,152 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
|
||||
font-weight: 500;
|
||||
transition: background .15s, color .15s, border-color .15s;
|
||||
}
|
||||
.pager-btn:hover, .pager-num:hover { background: #e9ecef; text-decoration: none; color: #212529; }
|
||||
|
||||
.pager-btn:hover,
|
||||
.pager-num:hover {
|
||||
background: #e9ecef;
|
||||
text-decoration: none;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.pager-num.active {
|
||||
background: #0d6efd;
|
||||
color: #fff;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
|
||||
.pager-btn.disabled {
|
||||
pointer-events: none;
|
||||
opacity: .4;
|
||||
}
|
||||
|
||||
.pager-dots {
|
||||
padding: 0 2px;
|
||||
color: #adb5bd;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
FINAL GRID COLORS
|
||||
Schema/customfield fields = green
|
||||
Fixed/standard fields = white
|
||||
Required fields = red border only
|
||||
========================================================= */
|
||||
|
||||
/* Default: all fields white */
|
||||
.grid-container input,
|
||||
.grid-container select,
|
||||
.grid-container textarea,
|
||||
.grid-top input,
|
||||
.grid-top select,
|
||||
.grid-top textarea {
|
||||
background-color: #ffffff !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
/* Schema/customfield fields: green background */
|
||||
.grid-container .schema-field input,
|
||||
.grid-container .schema-field select,
|
||||
.grid-container .schema-field textarea,
|
||||
.grid-top .schema-field input,
|
||||
.grid-top .schema-field select,
|
||||
.grid-top .schema-field textarea {
|
||||
background-color: #dff3e3 !important;
|
||||
}
|
||||
|
||||
/* Fixed and standard fields: white background */
|
||||
.grid-container .fixed-field input,
|
||||
.grid-container .fixed-field select,
|
||||
.grid-container .fixed-field textarea,
|
||||
.grid-container .standard-field input,
|
||||
.grid-container .standard-field select,
|
||||
.grid-container .standard-field textarea,
|
||||
.grid-top .fixed-field input,
|
||||
.grid-top .fixed-field select,
|
||||
.grid-top .fixed-field textarea,
|
||||
.grid-top .standard-field input,
|
||||
.grid-top .standard-field select,
|
||||
.grid-top .standard-field textarea {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Required fields: red border only */
|
||||
.grid-container .required-field input,
|
||||
.grid-container .required-field select,
|
||||
.grid-container .required-field textarea,
|
||||
.grid-top .required-field input,
|
||||
.grid-top .required-field select,
|
||||
.grid-top .required-field textarea {
|
||||
border: 2px solid #dc3545 !important;
|
||||
box-shadow: 0 0 0 1px rgba(220, 53, 69, 0.15) !important;
|
||||
}
|
||||
|
||||
/* Required schema/customfield fields: green + red border */
|
||||
.grid-container .schema-field.required-field input,
|
||||
.grid-container .schema-field.required-field select,
|
||||
.grid-container .schema-field.required-field textarea,
|
||||
.grid-top .schema-field.required-field input,
|
||||
.grid-top .schema-field.required-field select,
|
||||
.grid-top .schema-field.required-field textarea {
|
||||
background-color: #dff3e3 !important;
|
||||
border: 2px solid #dc3545 !important;
|
||||
}
|
||||
|
||||
/* Required fixed/standard fields: white + red border */
|
||||
.grid-container .fixed-field.required-field input,
|
||||
.grid-container .fixed-field.required-field select,
|
||||
.grid-container .fixed-field.required-field textarea,
|
||||
.grid-container .standard-field.required-field input,
|
||||
.grid-container .standard-field.required-field select,
|
||||
.grid-container .standard-field.required-field textarea,
|
||||
.grid-top .fixed-field.required-field input,
|
||||
.grid-top .fixed-field.required-field select,
|
||||
.grid-top .fixed-field.required-field textarea,
|
||||
.grid-top .standard-field.required-field input,
|
||||
.grid-top .standard-field.required-field select,
|
||||
.grid-top .standard-field.required-field textarea {
|
||||
background-color: #ffffff !important;
|
||||
border: 2px solid #dc3545 !important;
|
||||
}
|
||||
|
||||
/* Select2 - schema/customfield fields: green */
|
||||
.grid-container .schema-field .select2-container--default .select2-selection--single,
|
||||
.grid-top .schema-field .select2-container--default .select2-selection--single {
|
||||
background-color: #dff3e3 !important;
|
||||
}
|
||||
|
||||
/* Select2 - fixed/standard fields: white */
|
||||
.grid-container .fixed-field .select2-container--default .select2-selection--single,
|
||||
.grid-container .standard-field .select2-container--default .select2-selection--single,
|
||||
.grid-top .fixed-field .select2-container--default .select2-selection--single,
|
||||
.grid-top .standard-field .select2-container--default .select2-selection--single {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Select2 - required fields: red border only */
|
||||
.grid-container .required-field .select2-container--default .select2-selection--single,
|
||||
.grid-top .required-field .select2-container--default .select2-selection--single {
|
||||
border: 2px solid #dc3545 !important;
|
||||
box-shadow: 0 0 0 1px rgba(220, 53, 69, 0.15) !important;
|
||||
}
|
||||
|
||||
/* Remove old red background from required classes */
|
||||
.grid-container input.required-input,
|
||||
.grid-container select.required-input,
|
||||
.grid-container textarea.required-input,
|
||||
.grid-top input.required-input,
|
||||
.grid-top select.required-input,
|
||||
.grid-top textarea.required-input {
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
/* Missing required cell: red outline only */
|
||||
.grid-cell.missing-required {
|
||||
background-color: inherit !important;
|
||||
border-right: 1px solid #dee2e6 !important;
|
||||
outline: 2px solid #dc3545 !important;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
</style>
|
||||
<title>Edit Imported Data - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
</head>
|
||||
@@ -1108,24 +1301,32 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
|
||||
<?php include('include/topbar.php'); ?>
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
<?php $autoBoundCount = isset($_GET['autobound']) ? (int) $_GET['autobound'] : 0; ?>
|
||||
<?php if ($autoBoundCount > 0): ?>
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<?= $autoBoundCount ?> valore/i collegato/i automaticamente al LIMS durante l'import.
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="mb-3 text d-flex align-items-center gap-2">
|
||||
<a href="imported.php?id=<?= $template_id ?>" class="btn btn-warning me-2">Imported (i)</a>
|
||||
<a href="tolims.php?id=<?= $template_id ?>" class="btn btn-success">To LIMS (l)</a>
|
||||
<a href="bindings_manage.php?template_id=<?= $template_id ?>" class="btn btn-outline-secondary">Binding JSON -> LIMS</a>
|
||||
<?php if ($importref === ''): ?>
|
||||
<span class="ms-3">
|
||||
<label class="form-check-label" style="font-size: 13px; cursor: pointer;">
|
||||
<input type="checkbox" class="form-check-input" id="showAllUsers" <?= $show_all_users ? 'checked' : '' ?>
|
||||
onchange="window.location.href='imported.php?id=<?= $template_id ?>' + (this.checked ? '&all_users=1' : '')">
|
||||
Show all users
|
||||
</label>
|
||||
</span>
|
||||
<span class="text-muted" style="font-size: 12px;">
|
||||
(<?= $usePagination ? count($importedData) . " of {$totalRows}" : count($importedData) ?> records<?= !$show_all_users ? ' — my records only' : '' ?>)
|
||||
</span>
|
||||
<span class="ms-3">
|
||||
<label class="form-check-label" style="font-size: 13px; cursor: pointer;">
|
||||
<input type="checkbox" class="form-check-input" id="showAllUsers" <?= $show_all_users ? 'checked' : '' ?>
|
||||
onchange="window.location.href='imported.php?id=<?= $template_id ?>' + (this.checked ? '&all_users=1' : '')">
|
||||
Show all users
|
||||
</label>
|
||||
</span>
|
||||
<span class="text-muted" style="font-size: 12px;">
|
||||
(<?= $usePagination ? count($importedData) . " of {$totalRows}" : count($importedData) ?> records<?= !$show_all_users ? ' — my records only' : '' ?>)
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if ($usePagination): ?>
|
||||
<?php
|
||||
<?php
|
||||
$baseQuery = $_GET;
|
||||
unset($baseQuery['limit'], $baseQuery['page']);
|
||||
$pageQuery = $_GET;
|
||||
@@ -1133,43 +1334,43 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
|
||||
$pageBase = 'imported.php?' . http_build_query($pageQuery);
|
||||
$fromRow = ($page - 1) * $perPage + 1;
|
||||
$toRow = min($page * $perPage, $totalRows);
|
||||
?>
|
||||
<div class="pager-bar mb-2">
|
||||
<div class="pager-rows-per-page">
|
||||
<span class="pager-label">Rows per page</span>
|
||||
<div class="pager-limit-group">
|
||||
<?php foreach ($allowedLimits as $lim):
|
||||
$isActive = ($perPage === $lim);
|
||||
$url = 'imported.php?' . http_build_query(array_merge($baseQuery, ['limit' => $lim]));
|
||||
?>
|
||||
<a href="<?= htmlspecialchars($url) ?>" class="pager-limit-btn <?= $isActive ? 'active' : '' ?>"><?= $lim ?></a>
|
||||
<?php endforeach; ?>
|
||||
?>
|
||||
<div class="pager-bar mb-2">
|
||||
<div class="pager-rows-per-page">
|
||||
<span class="pager-label">Rows per page</span>
|
||||
<div class="pager-limit-group">
|
||||
<?php foreach ($allowedLimits as $lim):
|
||||
$isActive = ($perPage === $lim);
|
||||
$url = 'imported.php?' . http_build_query(array_merge($baseQuery, ['limit' => $lim]));
|
||||
?>
|
||||
<a href="<?= htmlspecialchars($url) ?>" class="pager-limit-btn <?= $isActive ? 'active' : '' ?>"><?= $lim ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<div class="pager-nav">
|
||||
<span class="pager-info"><?= $fromRow ?>–<?= $toRow ?> of <?= $totalRows ?></span>
|
||||
<a href="<?= htmlspecialchars($pageBase . '&page=1') ?>" class="pager-btn <?= $page <= 1 ? 'disabled' : '' ?>" title="First"><i class="fas fa-angle-double-left"></i></a>
|
||||
<a href="<?= htmlspecialchars($pageBase . '&page=' . ($page - 1)) ?>" class="pager-btn <?= $page <= 1 ? 'disabled' : '' ?>" title="Previous"><i class="fas fa-angle-left"></i></a>
|
||||
<?php
|
||||
$startPage = max(1, $page - 2);
|
||||
$endPage = min($totalPages, $page + 2);
|
||||
if ($startPage > 1): ?>
|
||||
<a href="<?= htmlspecialchars($pageBase . '&page=1') ?>" class="pager-num">1</a>
|
||||
<?php if ($startPage > 2): ?><span class="pager-dots">...</span><?php endif; ?>
|
||||
<?php endif;
|
||||
for ($p = $startPage; $p <= $endPage; $p++): ?>
|
||||
<a href="<?= htmlspecialchars($pageBase . '&page=' . $p) ?>" class="pager-num <?= $p === $page ? 'active' : '' ?>"><?= $p ?></a>
|
||||
<?php endfor;
|
||||
if ($endPage < $totalPages): ?>
|
||||
<?php if ($endPage < $totalPages - 1): ?><span class="pager-dots">...</span><?php endif; ?>
|
||||
<a href="<?= htmlspecialchars($pageBase . '&page=' . $totalPages) ?>" class="pager-num"><?= $totalPages ?></a>
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<div class="pager-nav">
|
||||
<span class="pager-info"><?= $fromRow ?>–<?= $toRow ?> of <?= $totalRows ?></span>
|
||||
<a href="<?= htmlspecialchars($pageBase . '&page=1') ?>" class="pager-btn <?= $page <= 1 ? 'disabled' : '' ?>" title="First"><i class="fas fa-angle-double-left"></i></a>
|
||||
<a href="<?= htmlspecialchars($pageBase . '&page=' . ($page - 1)) ?>" class="pager-btn <?= $page <= 1 ? 'disabled' : '' ?>" title="Previous"><i class="fas fa-angle-left"></i></a>
|
||||
<?php
|
||||
$startPage = max(1, $page - 2);
|
||||
$endPage = min($totalPages, $page + 2);
|
||||
if ($startPage > 1): ?>
|
||||
<a href="<?= htmlspecialchars($pageBase . '&page=1') ?>" class="pager-num">1</a>
|
||||
<?php if ($startPage > 2): ?><span class="pager-dots">...</span><?php endif; ?>
|
||||
<?php endif;
|
||||
for ($p = $startPage; $p <= $endPage; $p++): ?>
|
||||
<a href="<?= htmlspecialchars($pageBase . '&page=' . $p) ?>" class="pager-num <?= $p === $page ? 'active' : '' ?>"><?= $p ?></a>
|
||||
<?php endfor;
|
||||
if ($endPage < $totalPages): ?>
|
||||
<?php if ($endPage < $totalPages - 1): ?><span class="pager-dots">...</span><?php endif; ?>
|
||||
<a href="<?= htmlspecialchars($pageBase . '&page=' . $totalPages) ?>" class="pager-num"><?= $totalPages ?></a>
|
||||
<?php endif; ?>
|
||||
<a href="<?= htmlspecialchars($pageBase . '&page=' . ($page + 1)) ?>" class="pager-btn <?= $page >= $totalPages ? 'disabled' : '' ?>" title="Next"><i class="fas fa-angle-right"></i></a>
|
||||
<a href="<?= htmlspecialchars($pageBase . '&page=' . $totalPages) ?>" class="pager-btn <?= $page >= $totalPages ? 'disabled' : '' ?>" title="Last"><i class="fas fa-angle-double-right"></i></a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<a href="<?= htmlspecialchars($pageBase . '&page=' . ($page + 1)) ?>" class="pager-btn <?= $page >= $totalPages ? 'disabled' : '' ?>" title="Next"><i class="fas fa-angle-right"></i></a>
|
||||
<a href="<?= htmlspecialchars($pageBase . '&page=' . $totalPages) ?>" class="pager-btn <?= $page >= $totalPages ? 'disabled' : '' ?>" title="Last"><i class="fas fa-angle-double-right"></i></a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
@@ -1179,9 +1380,9 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
|
||||
<i class="fas fa-cogs"></i> Actions
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<?php if ((Auth::user()->hasRole('Admin'))) : ?>
|
||||
<li><a class="dropdown-item export-all-lims-btn" href="#"><i class="fas fa-upload" style="color: #eb0b0b;"></i>Export All</a></li>
|
||||
<?php endif; ?>
|
||||
|
||||
<li><a class="dropdown-item export-all-lims-btn" href="#"><i class="fas fa-upload" style="color: #eb0b0b;"></i>Export All</a></li>
|
||||
|
||||
<li><a class="dropdown-item save-all-btn" href="#"><i class="fas fa-save" style="color: #28a745;"></i>Save All</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -1201,13 +1402,18 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="editForm">
|
||||
<div class="grid-container">
|
||||
<div class="top-scrollbar" id="topScrollbar">
|
||||
<div class="top-scrollbar-inner" id="topScrollbarInner"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid-container" id="gridContainer">
|
||||
<div class="grid-top" id="gridTopContainer"></div>
|
||||
<div class="grid-row" id="gridHeaderContainer" style="font-weight:600;"></div>
|
||||
<div id="gridRowContainer"></div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="partsModalContainer"></div>
|
||||
<div id="analysisModalContainer"></div>
|
||||
<div id="annotationsModalContainer"></div>
|
||||
<?php include 'photos_functions.php'; ?>
|
||||
</div>
|
||||
@@ -1218,7 +1424,12 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
|
||||
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
<style>.btn i { margin-top: 0 !important; margin-bottom: 0 !important; }</style>
|
||||
<style>
|
||||
.btn i {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
</style>
|
||||
<?php include('jsinclude.php'); ?>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js"></script>
|
||||
@@ -1231,6 +1442,75 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
|
||||
<script src="photos.js"></script>
|
||||
<script src="annotationsModal.js"></script>
|
||||
<script src="partsTable.js"></script>
|
||||
<script src="analysisModal.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const topScrollbar = document.getElementById('topScrollbar');
|
||||
const topScrollbarInner = document.getElementById('topScrollbarInner');
|
||||
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;
|
||||
|
||||
let syncingFromTop = false;
|
||||
let syncingFromGrid = false;
|
||||
|
||||
function updateTopScrollbarWidth() {
|
||||
const realWidth = Math.max(
|
||||
gridContainer.scrollWidth,
|
||||
gridHeaderContainer ? gridHeaderContainer.scrollWidth : 0,
|
||||
gridTopContainer ? gridTopContainer.scrollWidth : 0,
|
||||
gridRowContainer ? gridRowContainer.scrollWidth : 0
|
||||
);
|
||||
|
||||
topScrollbarInner.style.width = realWidth + 'px';
|
||||
|
||||
if (realWidth > gridContainer.clientWidth) {
|
||||
topScrollbar.style.display = 'block';
|
||||
} else {
|
||||
topScrollbar.style.display = 'none';
|
||||
}
|
||||
|
||||
topScrollbar.scrollLeft = gridContainer.scrollLeft;
|
||||
}
|
||||
|
||||
topScrollbar.addEventListener('scroll', function() {
|
||||
if (syncingFromGrid) return;
|
||||
syncingFromTop = true;
|
||||
gridContainer.scrollLeft = topScrollbar.scrollLeft;
|
||||
syncingFromTop = false;
|
||||
});
|
||||
|
||||
gridContainer.addEventListener('scroll', function() {
|
||||
if (syncingFromTop) return;
|
||||
syncingFromGrid = true;
|
||||
topScrollbar.scrollLeft = gridContainer.scrollLeft;
|
||||
syncingFromGrid = false;
|
||||
});
|
||||
|
||||
window.addEventListener('resize', updateTopScrollbarWidth);
|
||||
|
||||
// Recalculate after JS grid rendering
|
||||
setTimeout(updateTopScrollbarWidth, 100);
|
||||
setTimeout(updateTopScrollbarWidth, 300);
|
||||
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>
|
||||
|
||||
<div class="modal fade" id="exportConfirmModal" tabindex="-1" aria-labelledby="exportConfirmModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
@@ -1394,4 +1674,4 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -10,6 +10,9 @@ error_reporting(E_ALL | E_STRICT);
|
||||
include('../../extra/auth.php');
|
||||
//require_once __DIR__ . '/extra/auth.php';
|
||||
|
||||
// Laravel bootstrap (loaded by auth.php) forces UTC via config/app.php — re-apply our TZ
|
||||
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
|
||||
|
||||
// Here we just check if user is not
|
||||
// logged in, and in that case we redirect
|
||||
// the user to vanguard login page.
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<ul>
|
||||
<!-- <li> <a href="index.php"><i class='bx bx-radio-circle'></i>Default</a>
|
||||
</li> -->
|
||||
<li> <a href="import_dashboard.php"><i class='bx bx-radio-circle'></i>XLS Import</a>
|
||||
<li> <a href="import_dashboard.php"><i class='bx bx-radio-circle'></i>Import AREA</a>
|
||||
</li>
|
||||
|
||||
|
||||
@@ -51,14 +51,22 @@
|
||||
<ul>
|
||||
<li> <a href="quotations.php"><i class='bx bx-radio-circle'></i><?php echo $quotationstitle; ?></a>
|
||||
</li>
|
||||
<li> <a href="bindings_manage.php"><i class='bx bx-radio-circle'></i>Binding JSON -> LIMS</a>
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="menu-label">Reports</li>
|
||||
<li>
|
||||
<a href="rapporti_cliente_lookup.php" target="">
|
||||
<div class="parent-icon"><i class="bx bx-file-find"></i>
|
||||
</div>
|
||||
<div class="menu-title">Ricerca Reports</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="menu-label">Others</li>
|
||||
|
||||
|
||||
@@ -3,9 +3,20 @@
|
||||
// Retrieve all routines from database
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM routine");
|
||||
$stmt->execute();
|
||||
$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>
|
||||
<html lang="en">
|
||||
@@ -40,7 +51,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
<li>Template Name</li>
|
||||
<li>Source Type</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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,7 +79,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
<label class="form-label">Source Type *</label>
|
||||
<select name="source_type" id="sourceType" class="form-control" required>
|
||||
<option value="XLS" selected>XLS</option>
|
||||
<option value="API">API</option>
|
||||
<option value="API">API / JSON</option>
|
||||
<option value="PDF">PDF</option>
|
||||
</select>
|
||||
<small class="text-muted">Choose the source used by this template</small>
|
||||
</div>
|
||||
@@ -82,6 +95,58 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
<input type="text" name="start_column" id="startColumn" class="form-control" value="A" required>
|
||||
</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">
|
||||
<label class="form-label"><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></label>
|
||||
<textarea name="description" class="form-control"></textarea>
|
||||
@@ -185,10 +250,16 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
const routineAction3 = document.getElementById("routineAction3");
|
||||
|
||||
const sourceType = document.getElementById("sourceType");
|
||||
|
||||
const headerRowWrapper = document.getElementById("headerRowWrapper");
|
||||
const startColumnWrapper = document.getElementById("startColumnWrapper");
|
||||
const xlsSheetNumberWrapper = document.getElementById("xlsSheetNumberWrapper");
|
||||
const apiConfigWrapper = document.getElementById("apiConfigWrapper");
|
||||
|
||||
const headerRow = document.getElementById("headerRow");
|
||||
const startColumn = document.getElementById("startColumn");
|
||||
const xlsSheetIndex = document.getElementById("xlsSheetIndex");
|
||||
const apiConfigSelect = document.getElementById("apiConfigSelect");
|
||||
|
||||
if (!form || !clientLoadingStatus || !schemaLoadingStatus || !routineSelect || !routineDetails) {
|
||||
alert("Errore: Uno o più elementi della pagina non sono stati trovati. Contatta l'amministratore.");
|
||||
@@ -210,27 +281,57 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
allowClear: true
|
||||
});
|
||||
|
||||
$('#apiConfigSelect').select2({
|
||||
placeholder: "Select an API configuration...",
|
||||
allowClear: true
|
||||
});
|
||||
|
||||
function updateSourceFields() {
|
||||
const selectedSource = sourceType.value;
|
||||
|
||||
if (selectedSource === 'API') {
|
||||
headerRowWrapper.style.opacity = '0.6';
|
||||
startColumnWrapper.style.opacity = '0.6';
|
||||
const isXls = selectedSource === 'XLS';
|
||||
const isApiJson = selectedSource === 'API';
|
||||
|
||||
headerRow.required = false;
|
||||
startColumn.required = false;
|
||||
|
||||
headerRow.disabled = true;
|
||||
startColumn.disabled = true;
|
||||
} else {
|
||||
headerRowWrapper.style.opacity = '1';
|
||||
startColumnWrapper.style.opacity = '1';
|
||||
if (isXls) {
|
||||
headerRowWrapper.style.display = 'block';
|
||||
startColumnWrapper.style.display = 'block';
|
||||
xlsSheetNumberWrapper.style.display = 'block';
|
||||
|
||||
headerRow.required = true;
|
||||
startColumn.required = true;
|
||||
xlsSheetIndex.required = true;
|
||||
|
||||
headerRow.disabled = false;
|
||||
startColumn.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';
|
||||
xlsSheetNumberWrapper.style.display = 'none';
|
||||
|
||||
headerRow.required = false;
|
||||
startColumn.required = false;
|
||||
xlsSheetIndex.required = false;
|
||||
|
||||
headerRow.disabled = true;
|
||||
startColumn.disabled = true;
|
||||
xlsSheetIndex.disabled = true;
|
||||
|
||||
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 +362,12 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
data.value.forEach(client => {
|
||||
const nome = client.Nominativo || "Nome 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);
|
||||
});
|
||||
|
||||
@@ -388,6 +494,28 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
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 clientId = clientSelect.value;
|
||||
const selectedClientOption = clientSelect.options[clientSelect.selectedIndex];
|
||||
|
||||
@@ -13,7 +13,10 @@ try {
|
||||
}
|
||||
|
||||
// Recupera solo i template attivi
|
||||
$stmt = $pdo->query("SELECT id, button_label, button_bg_color, button_text_color, button_size FROM excel_templates WHERE status = 'active'");
|
||||
$stmt = $pdo->query("SELECT id, button_label, button_size, button_bg_color, button_text_color, source_type
|
||||
FROM excel_templates
|
||||
WHERE status = 'active'
|
||||
ORDER BY button_label ASC");
|
||||
$templates = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$response["success"] = true;
|
||||
|
||||
@@ -33,9 +33,9 @@ try {
|
||||
if ($extraFieldId) {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
p.id, p.iddatadb, p.part_number, p.part_description, p.idmatrice, p.note, p.dateexpiry,
|
||||
cf.value_id AS extra_value_id,
|
||||
cf.value_text AS extra_value_text
|
||||
p.id, p.iddatadb, p.part_number, p.part_description, p.mix, p.idmatrice, p.note, p.dateexpiry,
|
||||
cf.value_id AS extra_value_id,
|
||||
cf.value_text AS extra_value_text
|
||||
FROM identification_parts p
|
||||
LEFT JOIN identification_parts_customfields cf
|
||||
ON cf.part_id = p.id AND cf.field_id = :extraFieldId
|
||||
@@ -48,8 +48,8 @@ try {
|
||||
]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, iddatadb, part_number, part_description, idmatrice, note, dateexpiry,
|
||||
NULL AS extra_value_id, NULL AS extra_value_text
|
||||
SELECT id, iddatadb, part_number, part_description, mix, idmatrice, note, dateexpiry,
|
||||
NULL AS extra_value_id, NULL AS extra_value_text
|
||||
FROM identification_parts
|
||||
WHERE iddatadb = :iddatadb
|
||||
ORDER BY part_number ASC
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -259,11 +259,9 @@ $matrixGroups = array_values($matrixGroups);
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mb-3">
|
||||
<div class="form-check m-0">
|
||||
<input class="form-check-input" type="checkbox" id="analysisWebOnly">
|
||||
<label class="form-check-label small" for="analysisWebOnly">
|
||||
Web only
|
||||
</label>
|
||||
<input type="hidden" id="analysisWebOnly" value="1">
|
||||
<div class="small text-success fw-semibold">
|
||||
Showing WEB analyses only
|
||||
</div>
|
||||
|
||||
<div class="flex-grow-1" style="min-width: 220px;">
|
||||
|
||||
@@ -46,6 +46,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div style="display: flex; align-items: center; gap: 6px; margin-right: auto;">
|
||||
<label for="descriptionColorPickerAnnotations" style="font-size: 0.8rem; margin: 0; white-space: nowrap;">
|
||||
Colore annotazioni:
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
id="descriptionColorPickerAnnotations"
|
||||
value="#000000"
|
||||
style="width: 34px; height: 28px; padding: 1px; border: 1px solid #ccc; border-radius: 4px; cursor: pointer;">
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary btn-sm" id="addDescriptionsBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Aggiungi Lista Descrizioni</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" id="removeAnnotationsBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Rimuovi Descrizioni</button>
|
||||
<button type="button" class="btn btn-warning btn-sm" id="undoMarkerBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Undo Marker</button>
|
||||
|
||||
@@ -2,7 +2,24 @@
|
||||
<div class="modal-dialog modal-xl" style="max-width: 95vw !important;">
|
||||
<div class="modal-content">
|
||||
<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>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -20,16 +37,21 @@
|
||||
</button>
|
||||
|
||||
|
||||
<input type="checkbox" id="showMixParts" name="showMixParts" style="margin-right: 5px; margin-left: 10px;">
|
||||
<label for="showMixParts" style="font-size: 0.9rem; margin-right: 10px;">Mix</label>
|
||||
|
||||
|
||||
<button type="button" class="btn btn-info btn-sm" id="renumberPartsBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
|
||||
Rinumera Parti
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-primary btn-sm" id="clonePartsBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
|
||||
<i class="fas fa-clone"></i> Clona Parti
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm ms-2" id="toggleVoiceBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
|
||||
<i class="fas fa-microphone"></i> Voce
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm ms-2" id="showHideImageBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
|
||||
<i class="fas fa-eye-slash" style="font-size: 0.8rem;"></i>
|
||||
<i class="fas fa-image ms-1" style="font-size: 0.8rem;"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Seconda riga: +, M, MacroMatrice, Matrice globale, Propaga -->
|
||||
@@ -42,45 +64,54 @@
|
||||
<select id="global-matrice" class="form-control form-control-sm" style="width: 350px !important; margin-right: 10px;"></select>
|
||||
<button type="button" class="btn btn-primary btn-sm propagate-all-btn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;"><i class="fas fa-arrow-right"></i> Propaga a tutte</button>
|
||||
</div>
|
||||
<table class="table table-striped table-sm" id="partsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 55px;">Num</th>
|
||||
<th>Descrizione</th>
|
||||
<th style="width: 200px;">Matrice</th>
|
||||
<th style="width: 150px;">
|
||||
<input type="date" class="form-control form-control-sm propagate-date-input" style="width: 130px; margin-left: 5px; display: inline-block;" title="Propaga data a tutte le parti">
|
||||
</th>
|
||||
<th style="width: 200px;">
|
||||
<button type="button" class="btn btn-light btn-sm propagate-note-btn" style="padding: 0.2rem 0.4rem; font-size: 0.9rem; margin-left: 5px;" title="Propaga nota a tutte le parti">
|
||||
<i class="fas fa-sticky-note"></i>
|
||||
</button>
|
||||
Azioni
|
||||
<div class="parts-table-scroll">
|
||||
<table class="table table-striped table-sm" id="partsTable">
|
||||
<colgroup id="partsTableColgroup">
|
||||
<col class="parts-col-num">
|
||||
<col class="parts-col-description">
|
||||
<col class="parts-col-matrice">
|
||||
<col class="parts-col-date">
|
||||
<col class="parts-col-actions">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 55px;">Num</th>
|
||||
<th>Descrizione</th>
|
||||
<th style="width: 200px;">Matrice</th>
|
||||
<th style="width: 150px;">
|
||||
<input type="date" class="form-control form-control-sm propagate-date-input" style="width: 130px; margin-left: 5px; display: inline-block;" title="Propaga data a tutte le parti">
|
||||
</th>
|
||||
<th style="width: 200px;">
|
||||
<button type="button" class="btn btn-light btn-sm propagate-note-btn" style="padding: 0.2rem 0.4rem; font-size: 0.9rem; margin-left: 5px;" title="Propaga nota a tutte le parti">
|
||||
<i class="fas fa-sticky-note"></i>
|
||||
</button>
|
||||
Azioni
|
||||
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="partsTableBody">
|
||||
<tr data-part-id="new">
|
||||
<td><input type="number" class="form-control form-control-sm part-number" value="1" style="width: 55px;"></td>
|
||||
<td><input type="text" class="form-control form-control-sm part-description" placeholder="Inserisci descrizione"></td>
|
||||
<td>
|
||||
<div style="display: flex; align-items: center;">
|
||||
<button type="button" class="btn btn-primary btn-sm propagate-matrice-btn" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; margin-right: 3px;"><i class="fas fa-arrow-right"></i></button>
|
||||
<select class="part-matrice form-control form-control-sm" style="width: 150px;"></select>
|
||||
</div>
|
||||
</td>
|
||||
<td><input type="date" class="form-control form-control-sm part-dateexpiry" style="width: 130px;"></td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-light btn-sm note-btn" style="padding: 0.2rem 0.4rem; font-size: 0.9rem;" title="Aggiungi/Modifica nota"><i class="fas fa-sticky-note"></i></button>
|
||||
<button type="button" class="btn btn-warning btn-sm add-mix-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;">M+</button>
|
||||
<button type="button" class="btn btn-danger btn-sm remove-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; display: none;"><i class="fas fa-trash"></i></button>
|
||||
<span class="save-status text-success" style="display: none; margin-left: 5px;"><i class="fas fa-check"></i></span>
|
||||
<span class="save-loading text-warning" style="display: none; margin-left: 5px;"><i class="fas fa-spinner fa-spin"></i></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="partsTableBody">
|
||||
<tr data-part-id="new">
|
||||
<td><input type="number" class="form-control form-control-sm part-number" value="1" style="width: 55px;"></td>
|
||||
<td><input type="text" class="form-control form-control-sm part-description" placeholder="Inserisci descrizione"></td>
|
||||
<td>
|
||||
<div style="display: flex; align-items: center;">
|
||||
<button type="button" class="btn btn-primary btn-sm propagate-matrice-btn" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; margin-right: 3px;"><i class="fas fa-arrow-right"></i></button>
|
||||
<select class="part-matrice form-control form-control-sm" style="width: 150px;"></select>
|
||||
</div>
|
||||
</td>
|
||||
<td><input type="date" class="form-control form-control-sm part-dateexpiry" style="width: 130px;"></td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-light btn-sm note-btn" style="padding: 0.2rem 0.4rem; font-size: 0.9rem;" title="Aggiungi/Modifica nota"><i class="fas fa-sticky-note"></i></button>
|
||||
<button type="button" class="btn btn-warning btn-sm add-mix-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;">M+</button>
|
||||
<button type="button" class="btn btn-danger btn-sm remove-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; display: none;"><i class="fas fa-trash"></i></button>
|
||||
<span class="save-status text-success" style="display: none; margin-left: 5px;"><i class="fas fa-check"></i></span>
|
||||
<span class="save-loading text-warning" style="display: none; margin-left: 5px;"><i class="fas fa-spinner fa-spin"></i></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h6>Foto del Campione</h6>
|
||||
@@ -288,28 +319,20 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Select delle righe (colonna Matrice) = 150px */
|
||||
/* Select delle righe Matrice: si adatta alla colonna */
|
||||
.part-matrice {
|
||||
width: 300px !important;
|
||||
min-width: 300px !important;
|
||||
max-width: 300px !important;
|
||||
flex: 0 0 300px !important;
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.part-matrice.select2-hidden-accessible+.select2 {
|
||||
width: 300px !important;
|
||||
min-width: 300px !important;
|
||||
max-width: 300px !important;
|
||||
flex: 0 0 300px !important;
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
/* Colonna Descrizione (2ª colonna) = 420px */
|
||||
#partsTable th:nth-child(2),
|
||||
#partsTable td:nth-child(2) {
|
||||
width: 300px !important;
|
||||
min-width: 300px !important;
|
||||
max-width: 300px !important;
|
||||
}
|
||||
|
||||
|
||||
#partsTable .part-number {
|
||||
text-align: center;
|
||||
@@ -340,7 +363,11 @@
|
||||
border: 1px solid #aaa !important;
|
||||
border-radius: 4px !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;
|
||||
}
|
||||
|
||||
@@ -514,6 +541,202 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Resizable parts table - stable */
|
||||
.parts-table-scroll {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
contain: inline-size;
|
||||
}
|
||||
|
||||
#partsTable {
|
||||
table-layout: fixed !important;
|
||||
width: max-content !important;
|
||||
min-width: unset !important;
|
||||
}
|
||||
|
||||
#partsTable col.parts-col-num {
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
#partsTable col.parts-col-description {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
#partsTable col.parts-col-matrice {
|
||||
width: 360px;
|
||||
}
|
||||
|
||||
#partsTable col.parts-col-date {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
#partsTable col.parts-col-actions {
|
||||
width: 230px;
|
||||
}
|
||||
|
||||
#partsTable th,
|
||||
#partsTable td {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#partsTable th {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#partsTable th .parts-resizer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 9px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
z-index: 50;
|
||||
background: rgba(108, 117, 125, 0.12);
|
||||
border-right: 1px solid rgba(108, 117, 125, 0.45);
|
||||
}
|
||||
|
||||
#partsTable th .parts-resizer:hover {
|
||||
background: rgba(108, 117, 125, 0.25);
|
||||
border-right: 2px solid rgba(73, 80, 87, 0.7);
|
||||
}
|
||||
|
||||
#partsTable td {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#partsTable td input,
|
||||
#partsTable td select,
|
||||
#partsTable td .select2-container {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
/* Propagation control for dynamic extra field column */
|
||||
#partsTable th.extra-field-th {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#partsTable th.extra-field-th .extra-propagate-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#partsTable th.extra-field-th .extra-propagate-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 95px;
|
||||
}
|
||||
|
||||
#partsTable th.extra-field-th .extra-propagate-input,
|
||||
#partsTable th.extra-field-th .extra-propagate-select {
|
||||
height: 24px !important;
|
||||
font-size: 0.75rem !important;
|
||||
padding: 0.1rem 0.25rem !important;
|
||||
min-width: 90px;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
#partsTable th.extra-field-th .extra-propagate-btn {
|
||||
height: 24px;
|
||||
min-width: 26px;
|
||||
padding: 0.1rem 0.3rem !important;
|
||||
font-size: 0.75rem !important;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
#partsTable th.extra-field-th .select2-container {
|
||||
flex: 1 1 auto;
|
||||
min-width: 90px;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
#partsTable th.extra-field-th .select2-selection--single {
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
#partsTable th.extra-field-th .select2-selection__rendered {
|
||||
line-height: 22px !important;
|
||||
font-size: 0.75rem !important;
|
||||
padding-left: 4px !important;
|
||||
}
|
||||
|
||||
#partsTable th.extra-field-th .select2-selection__arrow {
|
||||
height: 22px !important;
|
||||
}
|
||||
|
||||
/* Extra field propagation header - full column width */
|
||||
#partsTable th.extra-field-th {
|
||||
width: auto !important;
|
||||
min-width: 220px !important;
|
||||
max-width: none !important;
|
||||
padding: 0.25rem !important;
|
||||
}
|
||||
|
||||
#partsTable .extra-propagate-wrapper {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#partsTable .extra-propagate-label {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
display: block;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
color: #222;
|
||||
white-space: nowrap !important;
|
||||
overflow: visible !important;
|
||||
text-overflow: unset !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#partsTable .extra-propagate-control {
|
||||
width: 100% !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#partsTable .extra-propagate-control .extra-propagate-input,
|
||||
#partsTable .extra-propagate-control .extra-propagate-select {
|
||||
flex: 1 1 auto !important;
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
#partsTable .extra-propagate-control .select2-container {
|
||||
flex: 1 1 auto !important;
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
#partsTable .extra-propagate-control .select2-selection {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
#partsTable .extra-propagate-btn {
|
||||
flex: 0 0 30px !important;
|
||||
width: 30px !important;
|
||||
height: 24px !important;
|
||||
min-width: 30px !important;
|
||||
padding: 0.1rem 0.25rem !important;
|
||||
}
|
||||
|
||||
/* rosso */
|
||||
</style>
|
||||
@@ -2,93 +2,111 @@
|
||||
* modals_gridData.js — Photos, Parts, Tested Component handlers for gridData pages
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
"use strict";
|
||||
|
||||
// ── Photos — use photos.js loadPopupContent (exported to window) ──
|
||||
$(document).on('click', '.photos-btn', function () {
|
||||
const iddatadb = $(this).data('iddatadb') || null;
|
||||
const idquotations = $(this).data('idquotations') || null;
|
||||
const modal = document.getElementById('photosModal');
|
||||
$(document).on("click", ".photos-btn", function () {
|
||||
const iddatadb = $(this).data("iddatadb") || null;
|
||||
const idquotations = $(this).data("idquotations") || null;
|
||||
const modal = document.getElementById("photosModal");
|
||||
if (!modal) return;
|
||||
|
||||
modal.style.display = 'block';
|
||||
modal.style.display = "block";
|
||||
|
||||
if (typeof window.loadPopupContent === 'function') {
|
||||
if (typeof window.loadPopupContent === "function") {
|
||||
window.loadPopupContent(iddatadb, idquotations);
|
||||
}
|
||||
});
|
||||
|
||||
// Close photos modal
|
||||
$(document).on('click', '.close-btn', function () {
|
||||
const modal = document.getElementById('photosModal');
|
||||
if (modal) modal.style.display = 'none';
|
||||
$(document).on("click", ".close-btn", function () {
|
||||
const modal = document.getElementById("photosModal");
|
||||
if (modal) modal.style.display = "none";
|
||||
});
|
||||
|
||||
// Close on backdrop click
|
||||
$(document).on('click', '#photosModal', function (e) {
|
||||
$(document).on("click", "#photosModal", function (e) {
|
||||
if (e.target === this) {
|
||||
this.style.display = 'none';
|
||||
this.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
// ── Parts (matching import_edit2.php behavior) ─────────────────────
|
||||
$(document).on('click', '.parts-btn', function () {
|
||||
const iddatadb = $(this).data('iddatadb') || null;
|
||||
const idquotations = $(this).data('idquotations') || null;
|
||||
$(document).on("click", ".parts-btn", function () {
|
||||
const iddatadb = $(this).data("iddatadb") || null;
|
||||
const idquotations = $(this).data("idquotations") || null;
|
||||
|
||||
$.ajax({
|
||||
url: 'modal_partsTable.php',
|
||||
method: 'GET',
|
||||
url: "modal_partsTable.php",
|
||||
method: "GET",
|
||||
data: { iddatadb: iddatadb },
|
||||
success: function (response) {
|
||||
$('#partsModalContainer').html(response);
|
||||
const modalElement = document.getElementById('partsModal');
|
||||
$("#partsModalContainer").html(response);
|
||||
const modalElement = document.getElementById("partsModal");
|
||||
if (!modalElement) return;
|
||||
|
||||
$("#trfHeader").text(iddatadb || idquotations || '');
|
||||
$("#partsModal").data("iddatadb", iddatadb).data("idquotations", idquotations);
|
||||
$("#trfHeader").text(iddatadb || idquotations || "");
|
||||
|
||||
const visibleIddatadbList = Array.isArray(window.gridData)
|
||||
? window.gridData
|
||||
.map((r) => parseInt(r.iddatadb, 10))
|
||||
.filter((v) => !isNaN(v) && v > 0)
|
||||
: [];
|
||||
|
||||
$("#partsModal")
|
||||
.data("iddatadb", iddatadb)
|
||||
.data("idquotations", idquotations)
|
||||
.data("visible-iddatadb-list", visibleIddatadbList);
|
||||
|
||||
let modal = bootstrap.Modal.getInstance(modalElement);
|
||||
if (!modal) modal = new bootstrap.Modal(modalElement, { backdrop: true, keyboard: true, focus: true });
|
||||
if (!modal)
|
||||
modal = new bootstrap.Modal(modalElement, {
|
||||
backdrop: true,
|
||||
keyboard: true,
|
||||
focus: true,
|
||||
});
|
||||
modal.show();
|
||||
|
||||
if (typeof window.loadParts === 'function') {
|
||||
if (typeof window.loadParts === "function") {
|
||||
window.loadParts(iddatadb, idquotations);
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
console.error('Error loading parts:', error);
|
||||
}
|
||||
console.error("Error loading parts:", error);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on('hidden.bs.modal', '#partsModal', function () {
|
||||
const modalElement = document.getElementById('partsModal');
|
||||
$(document).on("hidden.bs.modal", "#partsModal", function () {
|
||||
const modalElement = document.getElementById("partsModal");
|
||||
if (modalElement) {
|
||||
const modal = bootstrap.Modal.getInstance(modalElement);
|
||||
if (modal) modal.dispose();
|
||||
}
|
||||
$('#partsModalContainer').empty();
|
||||
$('.modal-backdrop').remove();
|
||||
$('body').removeClass('modal-open').css('padding-right', '');
|
||||
$("#partsModalContainer").empty();
|
||||
$(".modal-backdrop").remove();
|
||||
$("body").removeClass("modal-open").css("padding-right", "");
|
||||
});
|
||||
|
||||
// ── Tested Component quick add ───────────────────────────────────────
|
||||
$(document).on('click', '.add-part-btn', async function () {
|
||||
const iddatadb = $(this).data('iddatadb') || null;
|
||||
const rowIndex = parseInt($(this).data('row'));
|
||||
$(document).on("click", ".add-part-btn", async function () {
|
||||
const iddatadb = $(this).data("iddatadb") || null;
|
||||
const rowIndex = parseInt($(this).data("row"));
|
||||
const row = window.gridData?.[rowIndex];
|
||||
const id = iddatadb || (row ? row.iddatadb : null);
|
||||
if (!id) return;
|
||||
|
||||
const $cell = $(this).closest('.grid-cell, div');
|
||||
const $input = $cell.find('input');
|
||||
const raw = ($input.val() || '').trim();
|
||||
const parts = raw.split('|').map(s => s.trim()).filter(s => s.length > 0);
|
||||
const $cell = $(this).closest(".grid-cell, div");
|
||||
const $input = $cell.find("input");
|
||||
const raw = ($input.val() || "").trim();
|
||||
const parts = raw
|
||||
.split("|")
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
const uniqueParts = [...new Set(parts)];
|
||||
|
||||
if (!uniqueParts.length) {
|
||||
alert('Insert a description first.');
|
||||
alert("Insert a description first.");
|
||||
$input.focus();
|
||||
return;
|
||||
}
|
||||
@@ -96,14 +114,23 @@
|
||||
try {
|
||||
for (const p of uniqueParts) {
|
||||
const formData = new FormData();
|
||||
formData.append('iddatadb', id);
|
||||
formData.append('part_description', p);
|
||||
await fetch('add_part_quick.php', { method: 'POST', body: formData });
|
||||
formData.append("iddatadb", id);
|
||||
formData.append("part_description", p);
|
||||
await fetch("add_part_quick.php", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
}
|
||||
|
||||
if (row) {
|
||||
row.tested_component = raw;
|
||||
row._dirty = true;
|
||||
}
|
||||
|
||||
alert(`Added ${uniqueParts.length} part(s).`);
|
||||
$input.val('');
|
||||
$input.val(raw);
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
alert("Error: " + e.message);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -111,44 +138,47 @@
|
||||
let deleteIddatadb = null;
|
||||
let deleteRowIndex = null;
|
||||
|
||||
$(document).on('click', '.delete-btn', function () {
|
||||
deleteIddatadb = $(this).data('iddatadb');
|
||||
deleteRowIndex = parseInt($(this).data('row'));
|
||||
const modalEl = document.getElementById('deleteConfirmModal');
|
||||
$(document).on("click", ".delete-btn", function () {
|
||||
deleteIddatadb = $(this).data("iddatadb");
|
||||
deleteRowIndex = parseInt($(this).data("row"));
|
||||
const modalEl = document.getElementById("deleteConfirmModal");
|
||||
if (modalEl) {
|
||||
document.getElementById('deleteIddatadbText').textContent = deleteIddatadb;
|
||||
document.getElementById("deleteIddatadbText").textContent =
|
||||
deleteIddatadb;
|
||||
new bootstrap.Modal(modalEl).show();
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '#deleteConfirmBtn', async function () {
|
||||
const modalEl = document.getElementById('deleteConfirmModal');
|
||||
$(document).on("click", "#deleteConfirmBtn", async function () {
|
||||
const modalEl = document.getElementById("deleteConfirmModal");
|
||||
const modal = bootstrap.Modal.getInstance(modalEl);
|
||||
if (modal) modal.hide();
|
||||
|
||||
if (!deleteIddatadb) return;
|
||||
|
||||
try {
|
||||
const resp = await fetch('delete_record.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: deleteIddatadb })
|
||||
const resp = await fetch("delete_record.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id: deleteIddatadb }),
|
||||
});
|
||||
const result = await resp.json();
|
||||
|
||||
if (result.success) {
|
||||
// Remove from gridData
|
||||
const idx = window.gridData.findIndex(r => r.iddatadb === deleteIddatadb);
|
||||
const idx = window.gridData.findIndex(
|
||||
(r) => r.iddatadb === deleteIddatadb,
|
||||
);
|
||||
if (idx >= 0) window.gridData.splice(idx, 1);
|
||||
|
||||
// Re-render
|
||||
const gr = window.gridRenderer;
|
||||
if (gr) gr.renderVisibleRows();
|
||||
} else {
|
||||
alert('Error: ' + result.message);
|
||||
alert("Error: " + result.message);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
alert("Error: " + e.message);
|
||||
}
|
||||
|
||||
deleteIddatadb = null;
|
||||
@@ -156,20 +186,26 @@
|
||||
});
|
||||
|
||||
// ── Add new row ──────────────────────────────────────────────────────
|
||||
$(document).on('click', '#addRowBtn', async function () {
|
||||
$(document).on("click", "#addRowBtn", async function () {
|
||||
const btn = this;
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const templateId = window.gridMeta?.templateId;
|
||||
if (!templateId) { alert('Template ID missing'); return; }
|
||||
if (!templateId) {
|
||||
alert("Template ID missing");
|
||||
return;
|
||||
}
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const importref = urlParams.get('importref') || '';
|
||||
const resp = await fetch('add_record.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ template_id: templateId, importreferencecode: importref })
|
||||
const importref = urlParams.get("importref") || "";
|
||||
const resp = await fetch("add_record.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
template_id: templateId,
|
||||
importreferencecode: importref,
|
||||
}),
|
||||
});
|
||||
const result = await resp.json();
|
||||
|
||||
@@ -177,17 +213,20 @@
|
||||
// Build new row object
|
||||
const newRow = {
|
||||
iddatadb: result.iddatadb,
|
||||
status: 'i',
|
||||
idclient: window.gridMeta?.defaultIdclient || '',
|
||||
status: "i",
|
||||
idclient: window.gridMeta?.defaultIdclient || "",
|
||||
cliente_fornitore_id: null,
|
||||
commessaweb: null,
|
||||
user_name: result.user_name || '',
|
||||
importreferencecode: result.importreferencecode || '',
|
||||
filename_import: '',
|
||||
importdate: new Date().toISOString().slice(0, 19).replace('T', ' '),
|
||||
user_name: result.user_name || "",
|
||||
importreferencecode: result.importreferencecode || "",
|
||||
filename_import: "",
|
||||
importdate: new Date()
|
||||
.toISOString()
|
||||
.slice(0, 19)
|
||||
.replace("T", " "),
|
||||
fixedFields: {},
|
||||
details: {},
|
||||
mainFieldValue: '',
|
||||
mainFieldValue: "",
|
||||
_dirty: false,
|
||||
};
|
||||
|
||||
@@ -199,20 +238,27 @@
|
||||
if (gr) gr.renderVisibleRows();
|
||||
|
||||
// Highlight new row briefly
|
||||
const newGridRow = document.querySelector(`.grid-row[data-id="${result.iddatadb}"]`);
|
||||
const newGridRow = document.querySelector(
|
||||
`.grid-row[data-id="${result.iddatadb}"]`,
|
||||
);
|
||||
if (newGridRow) {
|
||||
newGridRow.classList.add('row-just-created');
|
||||
newGridRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
setTimeout(() => newGridRow.classList.remove('row-just-created'), 4000);
|
||||
newGridRow.classList.add("row-just-created");
|
||||
newGridRow.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
setTimeout(
|
||||
() => newGridRow.classList.remove("row-just-created"),
|
||||
4000,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
alert('Error: ' + (result.message || 'Unknown error'));
|
||||
alert("Error: " + (result.message || "Unknown error"));
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
alert("Error: " + e.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
+626
-38
@@ -9,6 +9,7 @@ $(document).ready(function () {
|
||||
let quotations = [];
|
||||
let partsExtraField = null; // {field_id, field_label} oppure null
|
||||
let extraFieldOptions = []; // [{id,label}]
|
||||
let isLoadingPartsRecord = false;
|
||||
|
||||
// --- ROW ID helpers: niente più cache impazzita di jQuery .data() ---
|
||||
function getPartId($row) {
|
||||
@@ -107,8 +108,45 @@ $(document).ready(function () {
|
||||
if (!partsExtraField) return "";
|
||||
|
||||
const selectedValueId = $row ? $row.data("extra-value-id") || "" : "";
|
||||
const selectedValueText = $row
|
||||
? $row.data("extra-value-text") || ""
|
||||
: "";
|
||||
|
||||
// SceltaMultipla -> select + hidden value id
|
||||
if (
|
||||
(partsExtraField.data_type || "").toLowerCase() === "sceltamultipla"
|
||||
) {
|
||||
const opts = [`<option value="">Select…</option>`]
|
||||
.concat(
|
||||
extraFieldOptions.map(
|
||||
(o) =>
|
||||
`<option value="${o.id}" ${String(o.id) === String(selectedValueId) ? "selected" : ""}>${o.label}</option>`,
|
||||
),
|
||||
)
|
||||
.join("");
|
||||
|
||||
return `
|
||||
<td class="extra-field-td" style="width:200px;">
|
||||
<input type="hidden" class="part-extra-field-id" value="${partsExtraField.field_id}">
|
||||
<input type="hidden" class="part-extra-value-id" value="${selectedValueId || ""}">
|
||||
<select class="form-control form-control-sm part-extra-select" data-selected="${selectedValueId || ""}">${opts}</select>
|
||||
</td>`;
|
||||
}
|
||||
|
||||
// Testo -> input + hidden field_id
|
||||
return `
|
||||
<td class="extra-field-td" style="width:200px;">
|
||||
<input type="hidden" class="part-extra-field-id" value="${partsExtraField.field_id}">
|
||||
<input type="text" class="form-control form-control-sm part-extra-field" data-type="${partsExtraField.data_type || ""}" value="${selectedValueText || ""}">
|
||||
</td>`;
|
||||
}
|
||||
|
||||
function buildExtraFieldHeaderHtml() {
|
||||
if (!partsExtraField) return "";
|
||||
|
||||
const label = partsExtraField.field_label || "Extra";
|
||||
|
||||
// SceltaMultipla: titolo sopra, select + bottone sotto
|
||||
if (
|
||||
(partsExtraField.data_type || "").toLowerCase() === "sceltamultipla"
|
||||
) {
|
||||
@@ -121,19 +159,38 @@ $(document).ready(function () {
|
||||
.join("");
|
||||
|
||||
return `
|
||||
<td class="extra-field-td" style="width:200px;">
|
||||
<input type="hidden" class="part-extra-field-id" value="${partsExtraField.field_id}">
|
||||
<input type="hidden" class="part-extra-value-id" value="">
|
||||
<select class="form-control form-control-sm part-extra-select" data-selected="${selectedValueId || ""}">${opts}</select>
|
||||
</td>`;
|
||||
<th class="extra-field-th">
|
||||
<div class="extra-propagate-wrapper">
|
||||
<div class="extra-propagate-label" title="${label}">
|
||||
${label}
|
||||
</div>
|
||||
<div class="extra-propagate-control">
|
||||
<select class="form-control form-control-sm extra-propagate-select">
|
||||
${opts}
|
||||
</select>
|
||||
<button type="button" class="btn btn-primary btn-sm extra-propagate-btn" title="Propaga a tutte le parti">
|
||||
<i class="fas fa-arrow-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</th>`;
|
||||
}
|
||||
|
||||
// Testo -> input + hidden field_id
|
||||
// Campo testo: titolo sopra, input + bottone sotto
|
||||
return `
|
||||
<td class="extra-field-td" style="width:200px;">
|
||||
<input type="hidden" class="part-extra-field-id" value="${partsExtraField.field_id}">
|
||||
<input type="text" class="form-control form-control-sm part-extra-field" data-type="${partsExtraField.data_type || ""}">
|
||||
</td>`;
|
||||
<th class="extra-field-th">
|
||||
<div class="extra-propagate-wrapper">
|
||||
<div class="extra-propagate-label" title="${label}">
|
||||
${label}
|
||||
</div>
|
||||
<div class="extra-propagate-control">
|
||||
<input type="text" class="form-control form-control-sm extra-propagate-input" placeholder="Value">
|
||||
<button type="button" class="btn btn-primary btn-sm extra-propagate-btn" title="Propaga a tutte le parti">
|
||||
<i class="fas fa-arrow-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</th>`;
|
||||
}
|
||||
|
||||
function initializeExtraFieldSelect2($context) {
|
||||
@@ -191,6 +248,79 @@ $(document).ready(function () {
|
||||
saveRow($row);
|
||||
});
|
||||
|
||||
$(document).on("click", ".extra-propagate-btn", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!partsExtraField) return;
|
||||
|
||||
const isSelect =
|
||||
(partsExtraField.data_type || "").toLowerCase() ===
|
||||
"sceltamultipla";
|
||||
|
||||
let propagateValueId = null;
|
||||
let propagateValueText = null;
|
||||
|
||||
if (isSelect) {
|
||||
propagateValueId =
|
||||
$("#partsTable thead .extra-propagate-select").val() || "";
|
||||
|
||||
if (!propagateValueId) {
|
||||
const errorMsg = $(
|
||||
'<div class="alert alert-warning temp-alert" role="alert">Seleziona un valore da propagare.</div>',
|
||||
);
|
||||
$("#partsModal .modal-body").prepend(errorMsg);
|
||||
setTimeout(() => {
|
||||
errorMsg.fadeOut(500, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 3500);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
propagateValueText = (
|
||||
$("#partsTable thead .extra-propagate-input").val() || ""
|
||||
).trim();
|
||||
|
||||
if (!propagateValueText) {
|
||||
const errorMsg = $(
|
||||
'<div class="alert alert-warning temp-alert" role="alert">Inserisci un valore da propagare.</div>',
|
||||
);
|
||||
$("#partsModal .modal-body").prepend(errorMsg);
|
||||
setTimeout(() => {
|
||||
errorMsg.fadeOut(500, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 3500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$("#partsTableBody tr").each(function () {
|
||||
const $row = $(this);
|
||||
|
||||
if (isSelect) {
|
||||
const $select = $row.find(".part-extra-select");
|
||||
|
||||
if ($select.length) {
|
||||
$select.val(propagateValueId).trigger("change.select2");
|
||||
|
||||
$row.data("extra-value-id", propagateValueId);
|
||||
$row.find(".part-extra-value-id").val(propagateValueId);
|
||||
}
|
||||
} else {
|
||||
const $input = $row.find(".part-extra-field");
|
||||
|
||||
if ($input.length) {
|
||||
$input.val(propagateValueText);
|
||||
|
||||
$row.data("extra-value-text", propagateValueText);
|
||||
}
|
||||
}
|
||||
|
||||
saveRow($row);
|
||||
});
|
||||
});
|
||||
|
||||
function applyExtraFieldColumn() {
|
||||
const $theadRow = $("#partsTable thead tr");
|
||||
|
||||
@@ -203,16 +333,17 @@ $(document).ready(function () {
|
||||
});
|
||||
|
||||
// 3) se non c'è campo extra -> fine
|
||||
if (!partsExtraField) return;
|
||||
if (!partsExtraField) {
|
||||
if (typeof window.applyPartsColumnWidths === "function") {
|
||||
setTimeout(window.applyPartsColumnWidths, 100);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 4) inserisci header prima di "Azioni" (ultima colonna)
|
||||
$theadRow
|
||||
.find("th:last")
|
||||
.before(
|
||||
`<th class="extra-field-th" style="width:200px;">${partsExtraField.field_label}</th>`,
|
||||
);
|
||||
// 4) inserisci header propagabile prima di "Azioni" ultima colonna
|
||||
$theadRow.find("th:last").before(buildExtraFieldHeaderHtml());
|
||||
|
||||
// 5) aggiungi cella a ogni riga già presente (una sola volta)
|
||||
// 5) aggiungi cella a ogni riga già presente
|
||||
$("#partsTableBody tr").each(function () {
|
||||
const $row = $(this);
|
||||
$row.find("td:last").before(buildExtraFieldCellHtml($row));
|
||||
@@ -225,6 +356,33 @@ $(document).ready(function () {
|
||||
});
|
||||
|
||||
initializeExtraFieldSelect2($("#partsTableBody"));
|
||||
|
||||
// Select2 anche sul campo di propagazione in testata
|
||||
if (
|
||||
(partsExtraField.data_type || "").toLowerCase() === "sceltamultipla"
|
||||
) {
|
||||
const $headerSelect = $(
|
||||
"#partsTable thead .extra-propagate-select",
|
||||
);
|
||||
|
||||
if ($headerSelect.length && typeof $.fn.select2 !== "undefined") {
|
||||
if ($headerSelect.hasClass("select2-hidden-accessible")) {
|
||||
$headerSelect.select2("destroy");
|
||||
}
|
||||
|
||||
$headerSelect.select2({
|
||||
placeholder: "Select…",
|
||||
allowClear: true,
|
||||
width: "100%",
|
||||
dropdownParent: $("#partsModal"),
|
||||
minimumResultsForSearch: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window.applyPartsColumnWidths === "function") {
|
||||
setTimeout(window.applyPartsColumnWidths, 100);
|
||||
}
|
||||
}
|
||||
|
||||
function setPartId($row, id) {
|
||||
@@ -235,6 +393,17 @@ $(document).ready(function () {
|
||||
$row.data("__pid", id);
|
||||
}
|
||||
|
||||
function setRowMix($row, isMix) {
|
||||
const mixValue = isMix === true || isMix === "Y" ? "Y" : "N";
|
||||
$row.attr("data-is-mix", mixValue);
|
||||
$row.data("is-mix", mixValue);
|
||||
}
|
||||
|
||||
function getRowMix($row) {
|
||||
const value = $row.attr("data-is-mix") || $row.data("is-mix");
|
||||
return value === "Y" ? "Y" : "N";
|
||||
}
|
||||
|
||||
// ===================
|
||||
// VOICE RECOGNITION SETUP
|
||||
// ===================
|
||||
@@ -341,21 +510,175 @@ $(document).ready(function () {
|
||||
// MODAL HANDLING
|
||||
// ===================
|
||||
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) {
|
||||
loadMacroMatrici();
|
||||
initializeGlobalSelect2();
|
||||
loadPartsExtraField(iddatadb, function () {
|
||||
loadPhoto(iddatadb, idquotations);
|
||||
loadExistingParts(iddatadb, idquotations, callback);
|
||||
loadExistingParts(iddatadb, idquotations, finishLoading);
|
||||
});
|
||||
} else {
|
||||
loadPartsExtraField(iddatadb, function () {
|
||||
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
|
||||
$(document).on("click", "#openAnnotationsBtn", function () {
|
||||
console.log("Clic su Apri Annotazioni...");
|
||||
@@ -698,7 +1021,7 @@ $(document).ready(function () {
|
||||
const $saveLoading = $row.find(".save-loading");
|
||||
const iddatadb = $("#partsModal").data("iddatadb");
|
||||
const idquotations = $("#partsModal").data("idquotations");
|
||||
const isMix = partDescription.startsWith("Mix") ? "Y" : "N";
|
||||
const isMix = getRowMix($row);
|
||||
|
||||
// EXTRA FIELD (0/1)
|
||||
const extra_field_id = $row.find(".part-extra-field-id").val() || null;
|
||||
@@ -837,11 +1160,7 @@ $(document).ready(function () {
|
||||
|
||||
let $mixRow = $("#partsTableBody tr")
|
||||
.filter(function () {
|
||||
return $(this)
|
||||
.find(".part-description")
|
||||
.val()
|
||||
.trim()
|
||||
.startsWith("Mix");
|
||||
return getRowMix($(this)) === "Y";
|
||||
})
|
||||
.last();
|
||||
|
||||
@@ -897,7 +1216,7 @@ $(document).ready(function () {
|
||||
function addNewRow(nextPartNumber, isMix = false) {
|
||||
const description = isMix ? "Mix" : "";
|
||||
const newRow = `
|
||||
<tr data-part-id="new">
|
||||
<tr data-part-id="new" data-is-mix="${isMix ? "Y" : "N"}">
|
||||
<td><input type="number" class="form-control form-control-sm part-number" value="${nextPartNumber || 1}" style="width: 80px;"></td>
|
||||
<td><input type="text" class="form-control form-control-sm part-description" value="${description}" placeholder="Inserisci descrizione"></td>
|
||||
<td>
|
||||
@@ -918,6 +1237,7 @@ $(document).ready(function () {
|
||||
</tr>`;
|
||||
$("#partsTableBody").append(newRow);
|
||||
const $newRow = $("#partsTableBody tr:last");
|
||||
setRowMix($newRow, isMix ? "Y" : "N");
|
||||
const $select = $newRow.find(".part-matrice");
|
||||
const selectedMacro = $("#macro-matrice-filter").val() || "";
|
||||
|
||||
@@ -925,7 +1245,10 @@ $(document).ready(function () {
|
||||
initializeExtraFieldSelect2($newRow);
|
||||
|
||||
updateRowButtons();
|
||||
markUnsaved();
|
||||
|
||||
if (!isLoadingPartsRecord) {
|
||||
markUnsaved();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================
|
||||
@@ -963,7 +1286,7 @@ $(document).ready(function () {
|
||||
// Raccogli tutti i dati della riga per evitare sovrascritture
|
||||
const partNumber = $row.find(".part-number").val();
|
||||
const partDescription = $row.find(".part-description").val().trim();
|
||||
const mix = partDescription.startsWith("Mix") ? "Y" : "N";
|
||||
const mix = getRowMix($row);
|
||||
const idmatrice = $row.find(".part-matrice").val() || null;
|
||||
const dateexpiry = $row.find(".part-dateexpiry").val() || null;
|
||||
|
||||
@@ -1062,7 +1385,7 @@ $(document).ready(function () {
|
||||
// Raccogli tutti i dati della riga per evitare sovrascritture
|
||||
const partNumber = $row.find(".part-number").val();
|
||||
const partDescription = $row.find(".part-description").val().trim();
|
||||
const mix = partDescription.startsWith("Mix") ? "Y" : "N";
|
||||
const mix = getRowMix($row);
|
||||
const idmatrice = $row.find(".part-matrice").val() || null;
|
||||
const note = $row.data("note") || null;
|
||||
|
||||
@@ -1394,7 +1717,7 @@ $(document).ready(function () {
|
||||
? $("<div>").text(part.note).html()
|
||||
: "";
|
||||
const newRow = `
|
||||
<tr data-part-id="${part.id}" data-note="${escapedNote}">
|
||||
<tr data-part-id="${part.id}" data-note="${escapedNote}" data-is-mix="${part.mix === "Y" ? "Y" : "N"}">
|
||||
<td><input type="number" class="form-control form-control-sm part-number" value="${part.part_number || ""}" style="width: 80px;"></td>
|
||||
<td><input type="text" class="form-control form-control-sm part-description" value="${escapedDescription}" placeholder="Inserisci descrizione"></td>
|
||||
<td>
|
||||
@@ -1418,7 +1741,7 @@ $(document).ready(function () {
|
||||
const $row = $(
|
||||
`#partsTableBody tr[data-part-id="${part.id}"]`,
|
||||
);
|
||||
|
||||
setRowMix($row, part.mix === "Y" ? "Y" : "N");
|
||||
if (
|
||||
part.extra_value_id !== undefined &&
|
||||
part.extra_value_id !== null
|
||||
@@ -1432,6 +1755,19 @@ $(document).ready(function () {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
part.extra_value_text !== undefined &&
|
||||
part.extra_value_text !== null
|
||||
) {
|
||||
$row.data(
|
||||
"extra-value-text",
|
||||
part.extra_value_text,
|
||||
);
|
||||
$row.find(".part-extra-field").val(
|
||||
part.extra_value_text,
|
||||
);
|
||||
}
|
||||
|
||||
initializeExtraFieldSelect2($row);
|
||||
|
||||
const $select = $("#partsTableBody").find(
|
||||
@@ -1595,7 +1931,11 @@ $(document).ready(function () {
|
||||
dataType: "json",
|
||||
delay: 150,
|
||||
data: function (params) {
|
||||
return { q: params.term || "", limit: 20, macro: selectedMacro || "" };
|
||||
return {
|
||||
q: params.term || "",
|
||||
limit: 20,
|
||||
macro: selectedMacro || "",
|
||||
};
|
||||
},
|
||||
processResults: function (data) {
|
||||
return { results: data.results || [] };
|
||||
@@ -1654,7 +1994,11 @@ $(document).ready(function () {
|
||||
dataType: "json",
|
||||
delay: 150,
|
||||
data: function (params) {
|
||||
return { q: params.term || "", limit: 20, macro: selectedMacro || "" };
|
||||
return {
|
||||
q: params.term || "",
|
||||
limit: 20,
|
||||
macro: selectedMacro || "",
|
||||
};
|
||||
},
|
||||
processResults: function (data) {
|
||||
return { results: data.results || [] };
|
||||
@@ -1800,7 +2144,9 @@ $(document).ready(function () {
|
||||
$("#partsTableBody .part-matrice").each(function () {
|
||||
const $target = $(this);
|
||||
if (!$target.find(`option[value="${globalVal}"]`).length) {
|
||||
$target.append(new Option(globalText, globalVal, true, true));
|
||||
$target.append(
|
||||
new Option(globalText, globalVal, true, true),
|
||||
);
|
||||
}
|
||||
$target.val(globalVal).trigger("change");
|
||||
});
|
||||
@@ -1873,7 +2219,7 @@ $(document).ready(function () {
|
||||
id: part.partId,
|
||||
part_number: index + 1,
|
||||
part_description: part.partDescription,
|
||||
mix: part.partDescription.startsWith("Mix") ? "Y" : "N",
|
||||
mix: getRowMix($rows.eq(index)),
|
||||
idmatrice: partMatrice[index + 1] || null,
|
||||
note: part.note,
|
||||
dateexpiry: part.dateexpiry,
|
||||
@@ -1954,6 +2300,8 @@ $(document).ready(function () {
|
||||
});
|
||||
|
||||
function markUnsaved() {
|
||||
if (isLoadingPartsRecord) return;
|
||||
|
||||
if (!unsavedChanges) {
|
||||
unsavedChanges = true;
|
||||
}
|
||||
@@ -1969,7 +2317,109 @@ $(document).ready(function () {
|
||||
".add-row-global, .add-mix-global, .add-mix-row, .remove-row, .propagate-matrice-btn, .propagate-all-btn, .note-btn",
|
||||
markUnsaved,
|
||||
);
|
||||
$(document).on("click", "#clonePartsBtn", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const sourceIddatadb =
|
||||
parseInt($("#partsModal").data("iddatadb"), 10) || null;
|
||||
const visibleListRaw =
|
||||
$("#partsModal").data("visible-iddatadb-list") || [];
|
||||
|
||||
if (!sourceIddatadb) {
|
||||
const errorMsg = $(
|
||||
'<div class="alert alert-danger temp-alert" role="alert">Source record not found.</div>',
|
||||
);
|
||||
$("#partsModal .modal-body").prepend(errorMsg);
|
||||
setTimeout(() => {
|
||||
errorMsg.fadeOut(500, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetIds = (Array.isArray(visibleListRaw) ? visibleListRaw : [])
|
||||
.map((v) => parseInt(v, 10))
|
||||
.filter((v) => !isNaN(v) && v > 0 && v !== sourceIddatadb);
|
||||
|
||||
if (!targetIds.length) {
|
||||
const errorMsg = $(
|
||||
'<div class="alert alert-warning temp-alert" role="alert">No other visible records available for clone.</div>',
|
||||
);
|
||||
$("#partsModal .modal-body").prepend(errorMsg);
|
||||
setTimeout(() => {
|
||||
errorMsg.fadeOut(500, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!confirm(
|
||||
`Confermi il clone delle parti del record ${sourceIddatadb} negli altri ${targetIds.length} record visibili?`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $btn = $(this);
|
||||
const originalHtml = $btn.html();
|
||||
$btn.prop("disabled", true).html(
|
||||
'<i class="fas fa-spinner fa-spin"></i> Clonazione...',
|
||||
);
|
||||
|
||||
$.ajax({
|
||||
url: "clone_parts_to_visible.php",
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
dataType: "json",
|
||||
data: JSON.stringify({
|
||||
source_iddatadb: sourceIddatadb,
|
||||
target_iddatadb_list: targetIds,
|
||||
}),
|
||||
success: function (response) {
|
||||
$btn.prop("disabled", false).html(originalHtml);
|
||||
|
||||
if (response.success) {
|
||||
const successMsg = $(
|
||||
`<div class="alert alert-success temp-alert" role="alert">
|
||||
Clone completed. Source parts copied to ${response.cloned_targets || 0} record(s), total cloned parts: ${response.total_cloned_parts || 0}.
|
||||
</div>`,
|
||||
);
|
||||
$("#partsModal .modal-body").prepend(successMsg);
|
||||
setTimeout(() => {
|
||||
successMsg.fadeOut(500, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 5000);
|
||||
} else {
|
||||
const errorMsg = $(
|
||||
`<div class="alert alert-danger temp-alert" role="alert">Clone error: ${response.message || "Unknown error"}</div>`,
|
||||
);
|
||||
$("#partsModal .modal-body").prepend(errorMsg);
|
||||
setTimeout(() => {
|
||||
errorMsg.fadeOut(500, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
$btn.prop("disabled", false).html(originalHtml);
|
||||
|
||||
const errorMsg = $(
|
||||
`<div class="alert alert-danger temp-alert" role="alert">Clone error: ${error} (${xhr.status})</div>`,
|
||||
);
|
||||
$("#partsModal .modal-body").prepend(errorMsg);
|
||||
setTimeout(() => {
|
||||
errorMsg.fadeOut(500, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 5000);
|
||||
},
|
||||
});
|
||||
});
|
||||
// Esporta la funzione loadParts per essere usata da import_Edit2.php
|
||||
window.loadParts = loadParts;
|
||||
|
||||
@@ -2126,7 +2576,7 @@ $(document).on("change", ".propagate-date-input", function () {
|
||||
const partId = $row.data("part-id");
|
||||
const partNumber = $row.find(".part-number").val();
|
||||
const partDescription = $row.find(".part-description").val().trim();
|
||||
const mix = partDescription.startsWith("Mix") ? "Y" : "N";
|
||||
const mix = $row.attr("data-is-mix") === "Y" ? "Y" : "N";
|
||||
const idmatrice = $row.find(".part-matrice").val() || null;
|
||||
const note = $row.data("note") || null;
|
||||
|
||||
@@ -2237,7 +2687,7 @@ $(document).on("click", ".save-common-note-btn", function () {
|
||||
const partId = $row.data("part-id");
|
||||
const partNumber = $row.find(".part-number").val();
|
||||
const partDescription = $row.find(".part-description").val().trim();
|
||||
const mix = partDescription.startsWith("Mix") ? "Y" : "N";
|
||||
const mix = $row.attr("data-is-mix") === "Y" ? "Y" : "N";
|
||||
const idmatrice = $row.find(".part-matrice").val() || null;
|
||||
const dateexpiry = $row.find(".part-dateexpiry").val() || null;
|
||||
|
||||
@@ -2351,4 +2801,142 @@ $(document).on("click", "#showHideImageBtn", function () {
|
||||
"<i class='fas fa-eye' style='font-size: 0.8rem;'></i><i class='fas fa-image ms-1' style='font-size: 0.8rem;'></i>",
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof window.applyPartsColumnWidths === "function") {
|
||||
setTimeout(window.applyPartsColumnWidths, 150);
|
||||
}
|
||||
});
|
||||
// ===================
|
||||
// RESIZABLE PARTS TABLE COLUMNS - FIXED COLGROUP VERSION
|
||||
// ===================
|
||||
(function () {
|
||||
// Larghezze default per indice colonna (0-based)
|
||||
const defaultWidths = [55, 320, 360, 150, 220, 230];
|
||||
const savedWidths = [...defaultWidths];
|
||||
|
||||
function getColgroup() {
|
||||
return $("#partsTableColgroup");
|
||||
}
|
||||
|
||||
function syncColgroupToHeaders() {
|
||||
const $table = $("#partsTable");
|
||||
const $ths = $table.find("thead tr:first th");
|
||||
const $colgroup = getColgroup();
|
||||
const thCount = $ths.length;
|
||||
|
||||
// Assicura che ci siano esattamente tante <col> quante <th>
|
||||
while ($colgroup.find("col").length < thCount) {
|
||||
$colgroup.append("<col>");
|
||||
}
|
||||
while ($colgroup.find("col").length > thCount) {
|
||||
$colgroup.find("col:last").remove();
|
||||
}
|
||||
|
||||
// Applica le larghezze salvate
|
||||
$colgroup.find("col").each(function (i) {
|
||||
const w = savedWidths[i] !== undefined ? savedWidths[i] : 150;
|
||||
$(this).css("width", w + "px");
|
||||
});
|
||||
|
||||
// Imposta larghezza totale della tabella = somma colonne (evita reflow)
|
||||
const total = savedWidths.slice(0, thCount).reduce((a, b) => a + b, 0);
|
||||
$table.css("width", total + "px");
|
||||
}
|
||||
|
||||
function applyColumnWidth(colIndex, newWidth) {
|
||||
const w = Math.max(40, Math.round(newWidth));
|
||||
savedWidths[colIndex] = w;
|
||||
|
||||
const $col = getColgroup().find("col").eq(colIndex);
|
||||
if ($col.length) {
|
||||
$col.css("width", w + "px");
|
||||
}
|
||||
|
||||
// Aggiorna larghezza totale tabella senza toccare le altre colonne
|
||||
const thCount = $("#partsTable thead tr:first th").length;
|
||||
const total = savedWidths.slice(0, thCount).reduce((a, b) => a + b, 0);
|
||||
$("#partsTable").css("width", total + "px");
|
||||
}
|
||||
|
||||
function addResizers() {
|
||||
const $table = $("#partsTable");
|
||||
if (!$table.length) return;
|
||||
|
||||
$table.find("thead tr:first th").each(function (colIndex) {
|
||||
const $th = $(this);
|
||||
|
||||
// Salta colonna Num (indice 0) — non ridimensionabile
|
||||
if (colIndex === 0) return;
|
||||
|
||||
// Non aggiungere due volte
|
||||
if ($th.find(".parts-resizer").length) return;
|
||||
|
||||
const $resizer = $("<span class='parts-resizer'></span>");
|
||||
$th.css("position", "relative"); // necessario per il posizionamento assoluto
|
||||
$th.append($resizer);
|
||||
|
||||
$resizer.on("mousedown", function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const startX = e.pageX;
|
||||
// Leggi la larghezza ESATTA dalla <col>, non dal <th>
|
||||
const startWidth =
|
||||
savedWidths[colIndex] !== undefined
|
||||
? savedWidths[colIndex]
|
||||
: parseInt(
|
||||
getColgroup()
|
||||
.find("col")
|
||||
.eq(colIndex)
|
||||
.css("width"),
|
||||
10,
|
||||
) || 150;
|
||||
|
||||
$("body")
|
||||
.css("user-select", "none")
|
||||
.css("cursor", "col-resize");
|
||||
|
||||
$(document).on("mousemove.partsResize", function (ev) {
|
||||
const delta = ev.pageX - startX;
|
||||
applyColumnWidth(colIndex, startWidth + delta);
|
||||
});
|
||||
|
||||
$(document).on("mouseup.partsResize", function () {
|
||||
$("body").css("user-select", "").css("cursor", "");
|
||||
$(document).off(".partsResize");
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
syncColgroupToHeaders();
|
||||
addResizers();
|
||||
}
|
||||
|
||||
// Init al primo show del modale
|
||||
$(document).on("shown.bs.modal", "#partsModal", function () {
|
||||
setTimeout(init, 120);
|
||||
});
|
||||
|
||||
// Re-init dopo ogni AJAX (nuove righe, caricamenti)
|
||||
$(document).ajaxComplete(function () {
|
||||
if ($("#partsModal").hasClass("show")) {
|
||||
setTimeout(syncColgroupToHeaders, 200);
|
||||
setTimeout(addResizers, 220);
|
||||
}
|
||||
});
|
||||
|
||||
// Re-init dopo aggiunta/rimozione righe
|
||||
$(document).on(
|
||||
"click",
|
||||
".add-row-global, .add-mix-global, .add-mix-row, .remove-row, #renumberPartsBtn, #clonePartsBtn",
|
||||
function () {
|
||||
setTimeout(syncColgroupToHeaders, 120);
|
||||
setTimeout(addResizers, 140);
|
||||
},
|
||||
);
|
||||
|
||||
window.initPartsResizableColumns = init;
|
||||
window.applyPartsColumnWidths = syncColgroupToHeaders;
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
set_time_limit(20);
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
$start = microtime(true);
|
||||
|
||||
// Chiamata minima: 1 solo record, 1 solo campo
|
||||
$data = $api->get('Rapporto', [
|
||||
'$top' => 1,
|
||||
'$select' => 'IdRapporto'
|
||||
]);
|
||||
|
||||
$elapsed = round(microtime(true) - $start, 3);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'elapsed_seconds' => $elapsed,
|
||||
'data' => $data
|
||||
], 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);
|
||||
}
|
||||
@@ -13,8 +13,21 @@ try {
|
||||
$id = intval($_POST['id'] ?? 0);
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$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'] ?? '');
|
||||
|
||||
$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'] ?? '');
|
||||
$target_table = trim($_POST['target_table'] ?? 'datadb');
|
||||
$idclient = intval($_POST['client_id'] ?? 0);
|
||||
@@ -27,7 +40,8 @@ try {
|
||||
$button_text_color = trim($_POST['button_text_color'] ?? '#ffffff');
|
||||
$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';
|
||||
}
|
||||
|
||||
@@ -41,18 +55,52 @@ try {
|
||||
if ($header_row === null || $header_row <= 0 || $start_column === '') {
|
||||
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.");
|
||||
}
|
||||
|
||||
$api_config_id = null;
|
||||
}
|
||||
|
||||
// API templates do not require XLS coordinates
|
||||
if ($source_type === 'API') {
|
||||
// API/JSON validation
|
||||
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;
|
||||
$start_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_sheet_index = null;
|
||||
$api_config_id = null;
|
||||
}
|
||||
|
||||
// Database connection
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$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
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE excel_templates
|
||||
@@ -61,6 +109,8 @@ try {
|
||||
source_type = ?,
|
||||
header_row = ?,
|
||||
start_column = ?,
|
||||
xls_sheet_index = ?,
|
||||
api_config_id = ?,
|
||||
description = ?,
|
||||
target_table = ?,
|
||||
idclient = ?,
|
||||
@@ -81,6 +131,8 @@ try {
|
||||
$source_type,
|
||||
$header_row,
|
||||
$start_column,
|
||||
$xls_sheet_index,
|
||||
$api_config_id,
|
||||
$description,
|
||||
$target_table,
|
||||
$idclient,
|
||||
|
||||
@@ -10,6 +10,9 @@ session_start();
|
||||
// Includi PHPSpreadsheet
|
||||
require_once '../../vendor/autoload.php';
|
||||
|
||||
Dotenv\Dotenv::createImmutable(dirname(__DIR__, 2))->safeLoad();
|
||||
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
|
||||
|
||||
$response = ['error' => '', 'rows' => [], 'columns' => [], 'template_id' => 0, 'filename' => ''];
|
||||
|
||||
try {
|
||||
|
||||
@@ -11,17 +11,105 @@ session_start();
|
||||
require_once '../../vendor/autoload.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 {
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['excel_file'])) {
|
||||
$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_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);
|
||||
|
||||
$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
|
||||
error_log("Received template_id from POST: " . print_r($_POST['template_id'], true));
|
||||
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_sheet_index: $xlsSheetIndex");
|
||||
|
||||
$file = $_FILES['excel_file'];
|
||||
$fileError = $file['error'];
|
||||
@@ -38,23 +126,32 @@ try {
|
||||
$originalFilename = basename($file['name']);
|
||||
$newFilename = "{$iduserlogin}-{$timestamp}-{$originalFilename}";
|
||||
$importFolder = __DIR__ . '/imported_trf/';
|
||||
|
||||
if (!file_exists($importFolder)) {
|
||||
mkdir($importFolder, 0777, true);
|
||||
}
|
||||
|
||||
$destination = $importFolder . $newFilename;
|
||||
|
||||
// Sposta il file
|
||||
if (!move_uploaded_file($file['tmp_name'], $destination)) {
|
||||
throw new Exception("Errore durante lo spostamento del file 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
|
||||
$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]);
|
||||
$mappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
@@ -65,19 +162,45 @@ try {
|
||||
$response['error'] = "Nessun mapping trovato per il template con ID $template_id";
|
||||
} else {
|
||||
// Carica il file rinominato con PHPSpreadsheet
|
||||
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($destination);
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
$spreadsheet = IOFactory::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();
|
||||
$highestColumn = $worksheet->getHighestColumn();
|
||||
$highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn);
|
||||
$highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
|
||||
|
||||
$startRow = max(1, $header_row);
|
||||
$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++) {
|
||||
$cl = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($sc);
|
||||
$cl = Coordinate::stringFromColumnIndex($sc);
|
||||
$cv = trim((string)($worksheet->getCell($cl . $header_row)->getCalculatedValue() ?? ''));
|
||||
|
||||
if ($cv !== '') {
|
||||
$startColumn = $sc;
|
||||
break;
|
||||
@@ -85,24 +208,32 @@ try {
|
||||
}
|
||||
|
||||
// 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
|
||||
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) {
|
||||
$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 {
|
||||
$excelData = [];
|
||||
|
||||
// Build merge map for header row: physCol -> mergeStartCol
|
||||
$mergeStartMap = [];
|
||||
|
||||
foreach ($worksheet->getMergeCells() as $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);
|
||||
$mEndRow = (int)preg_replace('/[A-Z]+/i', '', $endCell);
|
||||
|
||||
if ($header_row >= $mStartRow && $header_row <= $mEndRow) {
|
||||
for ($c = $mStartCol; $c <= $mEndCol; $c++) {
|
||||
$mergeStartMap[$c] = $mStartCol;
|
||||
@@ -111,12 +242,17 @@ try {
|
||||
}
|
||||
|
||||
// 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 = [];
|
||||
|
||||
for ($col = $startColumn; $col <= $highestColumnIndex; $col++) {
|
||||
if (isset($mergeStartMap[$col])) {
|
||||
$ms = $mergeStartMap[$col];
|
||||
if (in_array($ms, $seen, true)) continue;
|
||||
|
||||
if (in_array($ms, $seen, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$seen[] = $ms;
|
||||
$logicalCols[] = $ms;
|
||||
} else {
|
||||
@@ -127,38 +263,48 @@ try {
|
||||
// Build header row using logical columns
|
||||
$headerRowData = [];
|
||||
$logicalNum = 0;
|
||||
|
||||
foreach ($logicalCols as $physCol) {
|
||||
$logicalNum++;
|
||||
$columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($physCol);
|
||||
|
||||
$columnLetter = Coordinate::stringFromColumnIndex($physCol);
|
||||
$cell = $worksheet->getCell($columnLetter . $header_row);
|
||||
$cellValue = trim((string)($cell ? $cell->getCalculatedValue() : ''));
|
||||
$cellValue = preg_replace('/[\r\n\t]+/', ' ', $cellValue);
|
||||
|
||||
// Empty headers get __empty_N__ to match mapping page
|
||||
$headerRowData[] = ($cellValue !== '') ? $cellValue : '__empty_' . $logicalNum . '__';
|
||||
}
|
||||
|
||||
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
|
||||
$headerFilledIndices = [];
|
||||
|
||||
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)));
|
||||
|
||||
// Extract data rows using logical columns
|
||||
for ($row = $startRow + 1; $row <= $highestRow; $row++) {
|
||||
$rowData = [];
|
||||
|
||||
foreach ($logicalCols as $physCol) {
|
||||
$columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($physCol);
|
||||
$columnLetter = Coordinate::stringFromColumnIndex($physCol);
|
||||
$cell = $worksheet->getCell($columnLetter . $row);
|
||||
$cellValue = $cell ? $cell->getCalculatedValue() : '';
|
||||
|
||||
$rowData[] = $cellValue ?: '';
|
||||
}
|
||||
|
||||
// Count how many header columns have data in this row
|
||||
$filledCount = 0;
|
||||
|
||||
foreach ($headerFilledIndices as $idx) {
|
||||
if (isset($rowData[$idx]) && trim((string)$rowData[$idx]) !== '') {
|
||||
$filledCount++;
|
||||
@@ -166,17 +312,25 @@ try {
|
||||
}
|
||||
|
||||
if ($filledCount >= $minFilled) {
|
||||
$excelData[] = ['data' => $rowData, 'excelrow' => $row];
|
||||
$excelData[] = [
|
||||
'data' => $rowData,
|
||||
'excelrow' => $row
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Recupera routine dal template
|
||||
$stmt = $pdo->prepare("SELECT idroutine, idclient FROM excel_templates WHERE id = ?");
|
||||
$stmt->execute([$template_id]);
|
||||
$template = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($template && $template['idroutine']) {
|
||||
$stmtRoutine = $pdo->prepare("SELECT idroutine, name, filename, headerrow, instruction FROM routine WHERE idroutine = ?");
|
||||
if ($template && !empty($template['idroutine'])) {
|
||||
$stmtRoutine = $pdo->prepare("
|
||||
SELECT
|
||||
idroutine,
|
||||
name,
|
||||
filename,
|
||||
headerrow,
|
||||
instruction
|
||||
FROM routine
|
||||
WHERE idroutine = ?
|
||||
");
|
||||
$stmtRoutine->execute([$template['idroutine']]);
|
||||
$routineData = $stmtRoutine->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
@@ -188,6 +342,7 @@ try {
|
||||
'filename' => $routineData['filename'] ?? '',
|
||||
'headerrow' => $routineData['headerrow'] ?? $header_row
|
||||
];
|
||||
|
||||
error_log("Routine rilevata per template {$template_id}: " . print_r($routineData, true));
|
||||
} else {
|
||||
error_log("Errore: Nessuna routine trovata per idroutine {$template['idroutine']}");
|
||||
@@ -204,6 +359,8 @@ try {
|
||||
$_SESSION['template_id'] = $template_id;
|
||||
$_SESSION['headers'] = $headerRowData;
|
||||
$_SESSION['mappings'] = $mappings;
|
||||
$_SESSION['xls_sheet_index'] = $xlsSheetIndex;
|
||||
$_SESSION['xls_sheet_name'] = $selectedSheetName;
|
||||
|
||||
// Includi excel_data nella risposta JSON in ogni caso
|
||||
$response['excel_data'] = $excelData;
|
||||
@@ -211,6 +368,8 @@ try {
|
||||
$response['columns'] = $headerRowData;
|
||||
$response['template_id'] = $template_id;
|
||||
$response['filename'] = $newFilename;
|
||||
$response['xls_sheet_index'] = $xlsSheetIndex;
|
||||
$response['xls_sheet_name'] = $selectedSheetName;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -12,22 +12,39 @@ try {
|
||||
// Retrieve and sanitize form data
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$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'] ?? '');
|
||||
|
||||
$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'] ?? '');
|
||||
$target_table = trim($_POST['target_table'] ?? 'datadb');
|
||||
$idclient = intval($_POST['client_id'] ?? 0);
|
||||
$clientname = trim($_POST['client_name'] ?? '');
|
||||
$idschema = intval($_POST['idschema'] ?? 0);
|
||||
$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_bg_color = trim($_POST['button_bg_color'] ?? '#007bff');
|
||||
$button_text_color = trim($_POST['button_text_color'] ?? '#ffffff');
|
||||
$button_label = trim($_POST['button_label'] ?? 'Click Me');
|
||||
|
||||
// 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';
|
||||
}
|
||||
|
||||
@@ -41,26 +58,62 @@ try {
|
||||
if ($header_row === null || $header_row <= 0 || $start_column === '') {
|
||||
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.");
|
||||
}
|
||||
|
||||
$api_config_id = null;
|
||||
}
|
||||
|
||||
// API templates do not require XLS coordinates
|
||||
// API / JSON validation
|
||||
if ($source_type === 'API') {
|
||||
if (empty($api_config_id)) {
|
||||
throw new Exception("API / JSON configuration is required for API / JSON templates.");
|
||||
}
|
||||
|
||||
$header_row = null;
|
||||
$start_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_sheet_index = null;
|
||||
$api_config_id = null;
|
||||
}
|
||||
|
||||
// Database connection
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$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
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO excel_templates
|
||||
INSERT INTO excel_templates
|
||||
(
|
||||
name,
|
||||
source_type,
|
||||
header_row,
|
||||
start_column,
|
||||
xls_sheet_index,
|
||||
api_config_id,
|
||||
description,
|
||||
target_table,
|
||||
idclient,
|
||||
@@ -75,7 +128,13 @@ try {
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
VALUES
|
||||
(
|
||||
?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?,
|
||||
NOW(), NOW()
|
||||
)
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
@@ -83,6 +142,8 @@ try {
|
||||
$source_type,
|
||||
$header_row,
|
||||
$start_column,
|
||||
$xls_sheet_index,
|
||||
$api_config_id,
|
||||
$description,
|
||||
$target_table,
|
||||
$idclient,
|
||||
|
||||
+555
-242
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,700 @@
|
||||
<?php include('include/headscript.php'); ?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
|
||||
<?php include('cssinclude.php'); ?>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<link href="assets/plugins/datatable/css/dataTables.bootstrap5.min.css" rel="stylesheet" />
|
||||
|
||||
<title>TRF-Project - Customer Reports</title>
|
||||
|
||||
<style>
|
||||
.lookup-wrapper {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.lookup-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lookup-subtitle {
|
||||
font-size: 13px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.compact-card .card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.table-report th {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
color: #6c757d;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table-report td {
|
||||
font-size: 13px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-pill-success {
|
||||
background: #e8fff1;
|
||||
color: #198754;
|
||||
}
|
||||
|
||||
.status-pill-warning {
|
||||
background: #fff3cd;
|
||||
color: #b58100;
|
||||
}
|
||||
|
||||
.pdf-icon-link {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
background: #dc3545;
|
||||
color: #fff;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
text-decoration: none;
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.pdf-icon-link:hover {
|
||||
color: #fff;
|
||||
opacity: 0.85;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.no-pdf {
|
||||
font-size: 12px;
|
||||
color: #adb5bd;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
border: 1px dashed #ced4da;
|
||||
border-radius: 10px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.spinner-inline {
|
||||
display: none;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.customer-box {
|
||||
display: none;
|
||||
border: 1px solid #e9ecef;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.customer-label {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
color: #6c757d;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.customer-value {
|
||||
font-size: 14px;
|
||||
color: #212529;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.json-preview {
|
||||
display: none;
|
||||
background: #111827;
|
||||
color: #e5e7eb;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
font-size: 12px;
|
||||
max-height: 420px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.table-responsive {
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.select2-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single {
|
||||
height: 38px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||
line-height: 36px;
|
||||
font-size: 14px;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.select2-dropdown {
|
||||
z-index: 9999;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
|
||||
<?php include('include/navbar.php'); ?>
|
||||
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<?php include('top_stat_widget.php'); ?>
|
||||
|
||||
<div class="lookup-wrapper">
|
||||
|
||||
<div class="card radius-10 compact-card">
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<div>
|
||||
<div class="lookup-title">Customer Test Reports</div>
|
||||
<div class="lookup-subtitle">
|
||||
Select a VisualLims customer and retrieve the latest reports with PDF links.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form id="customerReportsForm" class="row g-3 align-items-end">
|
||||
|
||||
<div class="col-md-5">
|
||||
<label for="idCliente" class="form-label fw-semibold">Customer</label>
|
||||
<select id="idCliente" name="idCliente" class="form-select">
|
||||
<option value="">Loading customers...</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<label for="limitReports" class="form-label fw-semibold">Limit</label>
|
||||
<select id="limitReports" name="limitReports" class="form-select">
|
||||
<option value="1">Last 1</option>
|
||||
<option value="3" selected>Last 3</option>
|
||||
<option value="5">Last 5</option>
|
||||
<option value="10">Last 10</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<label for="signedStatus" class="form-label fw-semibold">Status</label>
|
||||
<select id="signedStatus" name="signedStatus" class="form-select">
|
||||
<option value="all" selected>All</option>
|
||||
<option value="signed">Signed</option>
|
||||
<option value="not_signed">Not signed</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<button type="submit" id="btnSearchReports" class="btn btn-primary w-100">
|
||||
<i class="bx bx-search"></i> Search Reports
|
||||
<span class="spinner-border spinner-border-sm spinner-inline" id="searchSpinner" role="status" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="customerBox" class="customer-box">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="customer-label">Customer Code</div>
|
||||
<div class="customer-value" id="selectedCustomerCode">-</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<div class="customer-label">Customer Name</div>
|
||||
<div class="customer-value" id="selectedCustomerName">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="resultContainer" style="display:none;">
|
||||
|
||||
<div class="card radius-10 compact-card">
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<h6 class="mb-0">
|
||||
Reports
|
||||
<span class="badge bg-light text-dark ms-1" id="reportCountBadge">0</span>
|
||||
</h6>
|
||||
|
||||
<button type="button" id="toggleJsonBtn" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bx bx-code-alt"></i> Show JSON
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover table-report align-middle mb-0" id="reportsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Report Number</th>
|
||||
<th>Report ID</th>
|
||||
<th>Report Date</th>
|
||||
<th>Print Date</th>
|
||||
<th>Version</th>
|
||||
<th>Status</th>
|
||||
<th class="text-center">PDF</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="reportsTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<pre id="jsonPreview" class="json-preview mt-3"></pre>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="emptyState" class="empty-state">
|
||||
<i class="bx bx-file-find" style="font-size:34px;"></i>
|
||||
<div class="mt-2 fw-semibold">No reports loaded</div>
|
||||
<div class="small">Select a customer, choose the limit and click Search Reports.</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overlay toggle-icon"></div>
|
||||
|
||||
<a href="javaScript:;" class="back-to-top">
|
||||
<i class='bx bxs-up-arrow-alt'></i>
|
||||
</a>
|
||||
|
||||
<?php include('include/footer.php'); ?>
|
||||
|
||||
</div>
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
|
||||
<script src="assets/plugins/datatable/js/jquery.dataTables.min.js"></script>
|
||||
<script src="assets/plugins/datatable/js/dataTables.bootstrap5.min.js"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
|
||||
let lastJsonResponse = null;
|
||||
let loadedCustomers = [];
|
||||
let reportsDataTable = null;
|
||||
|
||||
function escapeHtml(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const date = new Date(value);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return date.toLocaleString('it-IT', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
$('#btnSearchReports').prop('disabled', isLoading);
|
||||
$('#searchSpinner').toggle(isLoading);
|
||||
}
|
||||
|
||||
function resetResults() {
|
||||
lastJsonResponse = null;
|
||||
|
||||
if (reportsDataTable !== null) {
|
||||
reportsDataTable.destroy();
|
||||
reportsDataTable = null;
|
||||
}
|
||||
|
||||
$('#resultContainer').hide();
|
||||
$('#emptyState').show();
|
||||
$('#reportsTableBody').html('');
|
||||
$('#reportCountBadge').text('0');
|
||||
$('#jsonPreview').hide().text('');
|
||||
$('#toggleJsonBtn').html('<i class="bx bx-code-alt"></i> Show JSON');
|
||||
}
|
||||
|
||||
function loadCustomers() {
|
||||
$.ajax({
|
||||
url: 'get_clienti.php',
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
const select = $('#idCliente');
|
||||
select.empty();
|
||||
|
||||
const customers = response.value || response || [];
|
||||
loadedCustomers = Array.isArray(customers) ? customers : [];
|
||||
|
||||
select.append('<option value="">Select customer...</option>');
|
||||
|
||||
loadedCustomers.forEach(function(customer) {
|
||||
const idCliente = customer.IdCliente || '';
|
||||
const codiceCliente = customer.CodiceCliente || '';
|
||||
const nominativo = customer.Nominativo || '';
|
||||
|
||||
const label = codiceCliente ?
|
||||
codiceCliente + ' - ' + nominativo :
|
||||
nominativo;
|
||||
|
||||
select.append(
|
||||
'<option value="' + escapeHtml(idCliente) + '" ' +
|
||||
'data-code="' + escapeHtml(codiceCliente) + '" ' +
|
||||
'data-name="' + escapeHtml(nominativo) + '">' +
|
||||
escapeHtml(label) +
|
||||
'</option>'
|
||||
);
|
||||
});
|
||||
|
||||
if ($.fn.select2) {
|
||||
select.select2({
|
||||
width: '100%',
|
||||
placeholder: 'Search customer...',
|
||||
allowClear: true,
|
||||
minimumInputLength: 0,
|
||||
matcher: function(params, data) {
|
||||
if ($.trim(params.term) === '') {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (typeof data.text === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const term = params.term.toLowerCase();
|
||||
const text = data.text.toLowerCase();
|
||||
|
||||
if (text.indexOf(term) > -1) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
sorter: function(data) {
|
||||
const term = $('.select2-search__field').val();
|
||||
|
||||
if (!term) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const search = term.toLowerCase();
|
||||
|
||||
return data.sort(function(a, b) {
|
||||
const aText = (a.text || '').toLowerCase();
|
||||
const bText = (b.text || '').toLowerCase();
|
||||
|
||||
const aStarts = aText.startsWith(search);
|
||||
const bStarts = bText.startsWith(search);
|
||||
|
||||
if (aStarts && !bStarts) return -1;
|
||||
if (!aStarts && bStarts) return 1;
|
||||
|
||||
return aText.localeCompare(bText);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
$('#idCliente').html('<option value="">Error loading customers</option>');
|
||||
|
||||
let message = 'Unable to load customers.';
|
||||
|
||||
if (xhr.responseJSON && xhr.responseJSON.error) {
|
||||
message = xhr.responseJSON.error;
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: 'Customer loading error',
|
||||
text: message,
|
||||
icon: 'error',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateSelectedCustomerBox() {
|
||||
const selectedOption = $('#idCliente option:selected');
|
||||
|
||||
const customerCode = selectedOption.data('code') || '-';
|
||||
const customerName = selectedOption.data('name') || '-';
|
||||
|
||||
if (!$('#idCliente').val()) {
|
||||
$('#customerBox').hide();
|
||||
$('#selectedCustomerCode').text('-');
|
||||
$('#selectedCustomerName').text('-');
|
||||
return;
|
||||
}
|
||||
|
||||
$('#selectedCustomerCode').text(customerCode);
|
||||
$('#selectedCustomerName').text(customerName);
|
||||
$('#customerBox').show();
|
||||
}
|
||||
|
||||
function renderStatus(isSigned) {
|
||||
if (isSigned === true) {
|
||||
return '<span class="status-pill status-pill-success"><i class="bx bx-check-circle"></i> Signed</span>';
|
||||
}
|
||||
|
||||
if (isSigned === false) {
|
||||
return '<span class="status-pill status-pill-warning"><i class="bx bx-time"></i> Not signed</span>';
|
||||
}
|
||||
|
||||
return '-';
|
||||
}
|
||||
|
||||
function renderPdfCell(pdfFiles) {
|
||||
if (!Array.isArray(pdfFiles) || pdfFiles.length === 0) {
|
||||
return '<span class="no-pdf">No PDF</span>';
|
||||
}
|
||||
|
||||
const firstPdf = pdfFiles[0];
|
||||
|
||||
if (!firstPdf.download_url) {
|
||||
return '<span class="no-pdf">No PDF</span>';
|
||||
}
|
||||
|
||||
return `
|
||||
<a href="${escapeHtml(firstPdf.download_url)}"
|
||||
target="_blank"
|
||||
class="pdf-icon-link"
|
||||
title="${escapeHtml(firstPdf.file_name || 'Download PDF')}">
|
||||
<i class="bx bxs-file-pdf"></i>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
function initReportsDataTable() {
|
||||
if (reportsDataTable !== null) {
|
||||
reportsDataTable.destroy();
|
||||
reportsDataTable = null;
|
||||
}
|
||||
|
||||
reportsDataTable = $('#reportsTable').DataTable({
|
||||
paging: false,
|
||||
searching: false,
|
||||
info: false,
|
||||
ordering: true,
|
||||
order: [
|
||||
[2, 'desc']
|
||||
],
|
||||
autoWidth: false,
|
||||
responsive: true,
|
||||
columnDefs: [{
|
||||
targets: 6,
|
||||
orderable: false
|
||||
}],
|
||||
language: {
|
||||
emptyTable: 'No reports found',
|
||||
zeroRecords: 'No matching reports found'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderReports(response) {
|
||||
lastJsonResponse = response;
|
||||
|
||||
const reports = response.reports || [];
|
||||
const tbody = $('#reportsTableBody');
|
||||
|
||||
tbody.empty();
|
||||
|
||||
$('#reportCountBadge').text(reports.length);
|
||||
|
||||
if (reports.length > 0) {
|
||||
reports.forEach(function(report) {
|
||||
const reportDateOrder = report.data || '';
|
||||
const printDateOrder = report.data_stampa || '';
|
||||
const statusOrder = report.firmato === true ? 1 : 0;
|
||||
|
||||
const row = `
|
||||
<tr>
|
||||
<td>
|
||||
<strong>${escapeHtml(report.codice_rapporto || '-')}</strong>
|
||||
</td>
|
||||
<td>${escapeHtml(report.id_rapporto || '-')}</td>
|
||||
<td data-order="${escapeHtml(reportDateOrder)}">${escapeHtml(formatDate(report.data))}</td>
|
||||
<td data-order="${escapeHtml(printDateOrder)}">${escapeHtml(formatDate(report.data_stampa))}</td>
|
||||
<td>${escapeHtml(report.versione !== null && report.versione !== undefined ? report.versione : '-')}</td>
|
||||
<td data-order="${statusOrder}">${renderStatus(report.firmato)}</td>
|
||||
<td class="text-center">${renderPdfCell(report.pdf_files)}</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
tbody.append(row);
|
||||
});
|
||||
}
|
||||
|
||||
$('#jsonPreview').text(JSON.stringify(response, null, 4));
|
||||
|
||||
$('#emptyState').hide();
|
||||
$('#resultContainer').show();
|
||||
|
||||
setTimeout(function() {
|
||||
initReportsDataTable();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
$('#idCliente').on('change', function() {
|
||||
updateSelectedCustomerBox();
|
||||
resetResults();
|
||||
});
|
||||
|
||||
$('#customerReportsForm').on('submit', function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const idCliente = $('#idCliente').val();
|
||||
const limit = $('#limitReports').val();
|
||||
const signedStatus = $('#signedStatus').val();
|
||||
|
||||
if (!idCliente) {
|
||||
Swal.fire({
|
||||
title: 'Missing customer',
|
||||
text: 'Please select a customer.',
|
||||
icon: 'warning',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
resetResults();
|
||||
updateSelectedCustomerBox();
|
||||
setLoading(true);
|
||||
|
||||
$.ajax({
|
||||
url: 'get_rapporti_cliente.php',
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
id_cliente: idCliente,
|
||||
limit: limit,
|
||||
signed_status: signedStatus
|
||||
},
|
||||
success: function(response) {
|
||||
if (!response || response.success !== true) {
|
||||
Swal.fire({
|
||||
title: 'No data',
|
||||
text: response && response.error ? response.error : 'No reports were returned.',
|
||||
icon: 'warning',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
renderReports(response);
|
||||
},
|
||||
error: function(xhr) {
|
||||
let message = 'Unexpected error while loading reports.';
|
||||
|
||||
if (xhr.responseJSON && xhr.responseJSON.error) {
|
||||
message = xhr.responseJSON.error;
|
||||
} else if (xhr.responseText) {
|
||||
message = xhr.responseText.substring(0, 500);
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: 'Error',
|
||||
text: message,
|
||||
icon: 'error',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
},
|
||||
complete: function() {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#toggleJsonBtn').on('click', function() {
|
||||
const jsonPreview = $('#jsonPreview');
|
||||
const isVisible = jsonPreview.is(':visible');
|
||||
|
||||
jsonPreview.toggle(!isVisible);
|
||||
|
||||
if (isVisible) {
|
||||
$(this).html('<i class="bx bx-code-alt"></i> Show JSON');
|
||||
} else {
|
||||
$(this).html('<i class="bx bx-hide"></i> Hide JSON');
|
||||
}
|
||||
});
|
||||
|
||||
loadCustomers();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,580 @@
|
||||
<?php include('include/headscript.php'); ?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<!--favicon-->
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
|
||||
<?php include('cssinclude.php'); ?>
|
||||
|
||||
<title>TRF-Project - Test Report Lookup</title>
|
||||
|
||||
<style>
|
||||
.compact-card .card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.lookup-wrapper {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.lookup-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lookup-subtitle {
|
||||
font-size: 13px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.result-section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
color: #344767;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
background: #fff;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
color: #6c757d;
|
||||
font-weight: 700;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
color: #212529;
|
||||
font-weight: 600;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.status-pill-success {
|
||||
background: #e8fff1;
|
||||
color: #198754;
|
||||
}
|
||||
|
||||
.status-pill-warning {
|
||||
background: #fff3cd;
|
||||
color: #b58100;
|
||||
}
|
||||
|
||||
.pdf-card {
|
||||
border: 1px solid #f1d1d1;
|
||||
background: #fffafa;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pdf-icon {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 12px;
|
||||
background: #dc3545;
|
||||
color: #fff;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pdf-file-name {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.pdf-meta {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.json-preview {
|
||||
display: none;
|
||||
background: #111827;
|
||||
color: #e5e7eb;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
font-size: 12px;
|
||||
max-height: 420px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
border: 1px dashed #ced4da;
|
||||
border-radius: 10px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.spinner-inline {
|
||||
display: none;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.btn-download-pdf {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pdf-card {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.btn-download-pdf {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!--wrapper-->
|
||||
<div class="wrapper">
|
||||
|
||||
<!--sidebar wrapper -->
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<!--end sidebar wrapper -->
|
||||
|
||||
<!--start header -->
|
||||
<?php include('include/topbar.php'); ?>
|
||||
<!--end header -->
|
||||
|
||||
<!--start page wrapper -->
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<?php include('top_stat_widget.php'); ?>
|
||||
|
||||
<div class="lookup-wrapper">
|
||||
|
||||
<div class="card radius-10 compact-card">
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<div>
|
||||
<div class="lookup-title">Test Report Lookup</div>
|
||||
<div class="lookup-subtitle">
|
||||
Search a test report from VisualLims by report number and download the PDF if available.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<form id="reportSearchForm" class="row g-3 align-items-end">
|
||||
<div class="col-md-8">
|
||||
<label for="codiceRapporto" class="form-label fw-semibold">
|
||||
Report Number
|
||||
</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="codiceRapporto"
|
||||
name="codiceRapporto"
|
||||
placeholder="Example: 2621521"
|
||||
autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<button type="submit" id="btnSearchReport" class="btn btn-primary w-100">
|
||||
<i class="bx bx-search"></i> Proceed
|
||||
<span class="spinner-border spinner-border-sm spinner-inline" id="searchSpinner" role="status" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="resultContainer" style="display:none;">
|
||||
|
||||
<div class="card radius-10 compact-card">
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<h6 class="mb-0">Report Data</h6>
|
||||
|
||||
<button type="button" id="toggleJsonBtn" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bx bx-code-alt"></i> Show JSON
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<div class="result-section-title">General Information</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="info-box">
|
||||
<div class="info-label">Report Number</div>
|
||||
<div class="info-value" id="resCodiceRapporto">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="info-box">
|
||||
<div class="info-label">Report ID</div>
|
||||
<div class="info-value" id="resIdRapporto">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="info-box">
|
||||
<div class="info-label">Customer Code</div>
|
||||
<div class="info-value" id="resCodiceCliente">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="info-box">
|
||||
<div class="info-label">Customer Name</div>
|
||||
<div class="info-value" id="resNominativoCliente">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="info-box">
|
||||
<div class="info-label">Report Date</div>
|
||||
<div class="info-value" id="resDataRapporto">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="info-box">
|
||||
<div class="info-label">Print Date</div>
|
||||
<div class="info-value" id="resDataStampa">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="info-box">
|
||||
<div class="info-label">Version</div>
|
||||
<div class="info-value" id="resVersione">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="info-box">
|
||||
<div class="info-label">Signed</div>
|
||||
<div class="info-value" id="resFirmato">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="info-box">
|
||||
<div class="info-label">Detail Endpoint</div>
|
||||
<div class="info-value small" id="resDetailEndpoint">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-section-title">Available PDF Files</div>
|
||||
|
||||
<div id="pdfFilesContainer" class="mb-4"></div>
|
||||
|
||||
<pre id="jsonPreview" class="json-preview"></pre>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="emptyState" class="empty-state">
|
||||
<i class="bx bx-file-find" style="font-size:34px;"></i>
|
||||
<div class="mt-2 fw-semibold">No report loaded</div>
|
||||
<div class="small">Enter a report number and click Proceed.</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!--end page wrapper -->
|
||||
|
||||
<!--start overlay-->
|
||||
<div class="overlay toggle-icon"></div>
|
||||
<!--end overlay-->
|
||||
|
||||
<!--Start Back To Top Button-->
|
||||
<a href="javaScript:;" class="back-to-top">
|
||||
<i class='bx bxs-up-arrow-alt'></i>
|
||||
</a>
|
||||
<!--End Back To Top Button-->
|
||||
|
||||
<?php include('include/footer.php'); ?>
|
||||
|
||||
</div>
|
||||
<!--end wrapper-->
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
|
||||
let lastJsonResponse = null;
|
||||
|
||||
function escapeHtml(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const date = new Date(value);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return date.toLocaleString('it-IT', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
$('#btnSearchReport').prop('disabled', isLoading);
|
||||
$('#searchSpinner').toggle(isLoading);
|
||||
}
|
||||
|
||||
function resetResult() {
|
||||
lastJsonResponse = null;
|
||||
|
||||
$('#resultContainer').hide();
|
||||
$('#emptyState').show();
|
||||
$('#jsonPreview').hide().text('');
|
||||
$('#toggleJsonBtn').html('<i class="bx bx-code-alt"></i> Show JSON');
|
||||
|
||||
$('#resCodiceRapporto').text('-');
|
||||
$('#resIdRapporto').text('-');
|
||||
$('#resCodiceCliente').text('-');
|
||||
$('#resNominativoCliente').text('-');
|
||||
$('#resDataRapporto').text('-');
|
||||
$('#resDataStampa').text('-');
|
||||
$('#resVersione').text('-');
|
||||
$('#resFirmato').text('-');
|
||||
$('#resDetailEndpoint').text('-');
|
||||
$('#pdfFilesContainer').html('');
|
||||
}
|
||||
|
||||
function renderPdfFiles(pdfFiles) {
|
||||
const container = $('#pdfFilesContainer');
|
||||
container.empty();
|
||||
|
||||
if (!Array.isArray(pdfFiles) || pdfFiles.length === 0) {
|
||||
container.html(`
|
||||
<div class="empty-state">
|
||||
<i class="bx bx-file-blank" style="font-size:28px;"></i>
|
||||
<div class="mt-2 fw-semibold">No PDF available</div>
|
||||
<div class="small">No report PDF file was returned by VisualLims.</div>
|
||||
</div>
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
pdfFiles.forEach(function(file) {
|
||||
const idRapportoFile = file.id_rapporto_file || '';
|
||||
const fileName = file.file_name || 'report.pdf';
|
||||
const categoria = file.categoria || '-';
|
||||
const tipoRapporto = file.tipo_rapporto || '-';
|
||||
|
||||
const downloadUrl = 'download_rapporto_pdf.php?id_rapporto_file=' + encodeURIComponent(idRapportoFile);
|
||||
|
||||
const html = `
|
||||
<div class="pdf-card mb-2">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="pdf-icon">
|
||||
<i class="bx bxs-file-pdf"></i>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="pdf-file-name">${escapeHtml(fileName)}</div>
|
||||
<div class="pdf-meta">
|
||||
ID File: ${escapeHtml(idRapportoFile)}
|
||||
|
|
||||
Category: ${escapeHtml(categoria)}
|
||||
|
|
||||
Type: ${escapeHtml(tipoRapporto)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="${downloadUrl}" target="_blank" class="btn btn-danger btn-sm btn-download-pdf">
|
||||
<i class="bx bx-download"></i> Download PDF
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.append(html);
|
||||
});
|
||||
}
|
||||
|
||||
function renderResult(response) {
|
||||
lastJsonResponse = response;
|
||||
|
||||
const base = response.rapporto_base || {};
|
||||
const data = response.data || {};
|
||||
const cliente = response.cliente || data.Cliente || {};
|
||||
|
||||
$('#resCodiceRapporto').text(response.codice_rapporto || base.CodiceRapporto || '-');
|
||||
$('#resIdRapporto').text(response.id_rapporto || base.IdRapporto || '-');
|
||||
$('#resCodiceCliente').text(cliente.CodiceCliente || '-');
|
||||
$('#resNominativoCliente').text(cliente.Nominativo || '-');
|
||||
$('#resDataRapporto').text(formatDate(base.Data || data.Data));
|
||||
$('#resDataStampa').text(formatDate(base.DataStampa || data.DataStampa));
|
||||
$('#resVersione').text(base.Versione !== undefined ? base.Versione : (data.Versione !== undefined ? data.Versione : '-'));
|
||||
$('#resDetailEndpoint').text(response.detail_endpoint || '-');
|
||||
|
||||
const isSigned = base.Firmato === true || data.Firmato === true;
|
||||
|
||||
if (isSigned) {
|
||||
$('#resFirmato').html('<span class="status-pill status-pill-success"><i class="bx bx-check-circle"></i> Signed</span>');
|
||||
} else {
|
||||
$('#resFirmato').html('<span class="status-pill status-pill-warning"><i class="bx bx-time"></i> Not signed</span>');
|
||||
}
|
||||
|
||||
renderPdfFiles(response.pdf_files || []);
|
||||
|
||||
$('#jsonPreview').text(JSON.stringify(response, null, 4));
|
||||
|
||||
$('#emptyState').hide();
|
||||
$('#resultContainer').show();
|
||||
}
|
||||
|
||||
$('#reportSearchForm').on('submit', function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const codiceRapporto = $('#codiceRapporto').val().trim();
|
||||
|
||||
if (!codiceRapporto) {
|
||||
Swal.fire({
|
||||
title: 'Missing report number',
|
||||
text: 'Please enter a report number.',
|
||||
icon: 'warning',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
resetResult();
|
||||
setLoading(true);
|
||||
|
||||
$.ajax({
|
||||
url: 'get_rapporto_prova.php',
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
codice: codiceRapporto,
|
||||
step: 'files'
|
||||
},
|
||||
success: function(response) {
|
||||
if (!response || response.success !== true) {
|
||||
Swal.fire({
|
||||
title: 'Report not found',
|
||||
text: response && response.message ? response.message : 'No report was found for this number.',
|
||||
icon: 'warning',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
renderResult(response);
|
||||
},
|
||||
error: function(xhr) {
|
||||
let message = 'Unexpected error while loading the report.';
|
||||
|
||||
if (xhr.responseJSON && xhr.responseJSON.error) {
|
||||
message = xhr.responseJSON.error;
|
||||
} else if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
message = xhr.responseJSON.message;
|
||||
} else if (xhr.responseText) {
|
||||
message = xhr.responseText.substring(0, 500);
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: 'Error',
|
||||
text: message,
|
||||
icon: 'error',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
},
|
||||
complete: function() {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#toggleJsonBtn').on('click', function() {
|
||||
const jsonPreview = $('#jsonPreview');
|
||||
const isVisible = jsonPreview.is(':visible');
|
||||
|
||||
jsonPreview.toggle(!isVisible);
|
||||
|
||||
if (isVisible) {
|
||||
$(this).html('<i class="bx bx-code-alt"></i> Show JSON');
|
||||
} else {
|
||||
$(this).html('<i class="bx bx-hide"></i> Hide JSON');
|
||||
}
|
||||
});
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const prefillCodice = urlParams.get('codice');
|
||||
|
||||
if (prefillCodice) {
|
||||
$('#codiceRapporto').val(prefillCodice);
|
||||
$('#reportSearchForm').trigger('submit');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,318 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
|
||||
// Binding preparati da import_insert.php (da risolvere + collegati in automatico).
|
||||
$pending = $_SESSION['pending_bindings'] ?? null;
|
||||
|
||||
if (empty($pending) || (empty($pending['items']) && empty($pending['auto']) && empty($pending['saved']))) {
|
||||
// Niente da mostrare: vado alla griglia.
|
||||
$tid = $pending['template_id'] ?? ($_SESSION['template_id'] ?? null);
|
||||
$ref = $pending['importref'] ?? '';
|
||||
unset($_SESSION['pending_bindings']);
|
||||
|
||||
if ($tid) {
|
||||
header("Location: imported.php?id=" . urlencode($tid) . "&importref=" . urlencode($ref));
|
||||
} else {
|
||||
header("Location: xlstemplates_grid.php");
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
$templateId = (int) $pending['template_id'];
|
||||
$importRef = (string) $pending['importref'];
|
||||
$items = $pending['items'] ?? [];
|
||||
$autoItems = $pending['auto'] ?? [];
|
||||
$savedItems = $pending['saved'] ?? [];
|
||||
|
||||
// Righe gia' risolte (modificabili): auto-collegate + binding gia' salvati.
|
||||
$resolvedItems = [];
|
||||
foreach ($autoItems as $a) {
|
||||
$a['badge'] = 'auto';
|
||||
$a['badge_class'] = 'bg-success';
|
||||
$resolvedItems[] = $a;
|
||||
}
|
||||
foreach ($savedItems as $s) {
|
||||
$s['badge'] = 'salvato';
|
||||
$s['badge_class'] = 'bg-secondary';
|
||||
$resolvedItems[] = $s;
|
||||
}
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
$stmt = $pdo->prepare("SELECT name FROM excel_templates WHERE id = ?");
|
||||
$stmt->execute([$templateId]);
|
||||
$templateName = $stmt->fetchColumn() ?: ('Template ' . $templateId);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
<?php include('cssinclude.php'); ?>
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.json-value {
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.binding-status {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
td .select2-container {
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
/* Allinea l'altezza di select2 a form-select/btn di Bootstrap */
|
||||
.select2-container--default .select2-selection--single {
|
||||
height: 38px;
|
||||
border-color: #ced4da;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||
height: 36px;
|
||||
}
|
||||
</style>
|
||||
<title>Binding JSON -> LIMS - <?= htmlspecialchars($titlewebsite ?? '', ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<div>
|
||||
<h6 class="mb-0">Binding JSON -> LIMS</h6>
|
||||
<small><?= htmlspecialchars($templateName) ?> - Template ID: <?= $templateId ?></small>
|
||||
</div>
|
||||
<a href="bindings_manage.php?template_id=<?= $templateId ?>" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-cog"></i> Gestione binding
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<?php if (!empty($items)): ?>
|
||||
<div class="alert alert-info">
|
||||
Alcuni valori importati dal JSON non hanno ancora una corrispondenza con i valori del LIMS.
|
||||
Seleziona il valore LIMS corretto per ciascuno e conferma. I binding verranno salvati e
|
||||
riutilizzati nelle importazioni successive.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($autoItems)): ?>
|
||||
<div class="alert alert-success">
|
||||
<?= count($autoItems) ?> valore/i collegato/i automaticamente (corrispondenza esatta con il LIMS).
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div id="bindingError" class="alert alert-danger" style="display:none;"></div>
|
||||
|
||||
<form id="bindingForm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Campo (template_mapping)</th>
|
||||
<th>Valore JSON</th>
|
||||
<th>Valore LIMS</th>
|
||||
<th style="width:210px;">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($items as $idx => $item): ?>
|
||||
<tr class="binding-row"
|
||||
data-index="<?= $idx ?>"
|
||||
data-mapping-id="<?= (int) $item['mapping_id'] ?>"
|
||||
data-field-id="<?= (int) $item['field_id'] ?>"
|
||||
data-json-value="<?= htmlspecialchars($item['json_value'], ENT_QUOTES) ?>"
|
||||
data-datadb-ids="<?= htmlspecialchars(json_encode($item['datadb_ids']), ENT_QUOTES) ?>">
|
||||
<td class="text-muted"><?= htmlspecialchars($item['field_label']) ?></td>
|
||||
<td class="json-value"><?= htmlspecialchars($item['json_value']) ?></td>
|
||||
<td>
|
||||
<select class="form-select binding-select" data-field-id="<?= (int) $item['field_id'] ?>">
|
||||
<option value="">Seleziona valore LIMS...</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary skip-binding-btn">Nessuna corrispondenza</button>
|
||||
<div class="binding-status text-muted mt-1">In attesa</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php foreach ($resolvedItems as $res): ?>
|
||||
<tr class="binding-row"
|
||||
data-mapping-id="<?= (int) $res['mapping_id'] ?>"
|
||||
data-field-id="<?= (int) $res['field_id'] ?>"
|
||||
data-json-value="<?= htmlspecialchars($res['json_value'], ENT_QUOTES) ?>"
|
||||
data-datadb-ids="<?= htmlspecialchars(json_encode($res['datadb_ids']), ENT_QUOTES) ?>">
|
||||
<td class="text-muted"><?= htmlspecialchars($res['field_label']) ?></td>
|
||||
<td class="json-value"><?= htmlspecialchars($res['json_value']) ?></td>
|
||||
<td>
|
||||
<select class="form-select binding-select" data-field-id="<?= (int) $res['field_id'] ?>">
|
||||
<option value="<?= (int) $res['lims_value_id'] ?>" selected><?= htmlspecialchars($res['lims_value']) ?></option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary skip-binding-btn">Nessuna corrispondenza</button>
|
||||
<div class="binding-status mt-1"><span class="badge <?= $res['badge_class'] ?>"><?= $res['badge'] ?></span></div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end gap-2 mt-3">
|
||||
<a href="imported.php?id=<?= $templateId ?>&importref=<?= urlencode($importRef) ?>"
|
||||
class="btn btn-outline-secondary">Salta per ora</a>
|
||||
<button type="submit" class="btn btn-primary" id="confirmBindingsBtn" disabled>
|
||||
Conferma e prosegui
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay toggle-icon"></div>
|
||||
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
const TEMPLATE_ID = <?= $templateId ?>;
|
||||
const IMPORT_REF = <?= json_encode($importRef) ?>;
|
||||
const CONTINUE_URL = 'imported.php?id=' + TEMPLATE_ID + '&importref=' + encodeURIComponent(IMPORT_REF);
|
||||
|
||||
const $form = $('#bindingForm');
|
||||
const $btn = $('#confirmBindingsBtn');
|
||||
const $error = $('#bindingError');
|
||||
|
||||
// Dropdown valori LIMS per riga (search_customfield_values.php).
|
||||
$('.binding-select').each(function() {
|
||||
const fieldId = $(this).data('field-id');
|
||||
$(this).select2({
|
||||
placeholder: 'Seleziona valore LIMS...',
|
||||
width: '100%',
|
||||
ajax: {
|
||||
url: 'search_customfield_values.php',
|
||||
dataType: 'json',
|
||||
delay: 200,
|
||||
data: params => ({
|
||||
field_id: fieldId,
|
||||
q: params.term || '',
|
||||
limit: 50
|
||||
}),
|
||||
processResults: data => ({
|
||||
results: data.results || []
|
||||
})
|
||||
},
|
||||
minimumInputLength: 0
|
||||
});
|
||||
});
|
||||
|
||||
// Una riga e' pronta se ha un valore scelto oppure e' marcata "nessuna corrispondenza".
|
||||
function refreshButton() {
|
||||
const allReady = $('.binding-row').toArray().every(row => {
|
||||
const $row = $(row);
|
||||
return $row.hasClass('is-skipped') || $row.find('.binding-select').val();
|
||||
});
|
||||
$btn.prop('disabled', !allReady);
|
||||
}
|
||||
|
||||
$('.binding-select').on('change', refreshButton);
|
||||
refreshButton();
|
||||
|
||||
// "Nessuna corrispondenza": azzera il valore importato, nessun binding salvato.
|
||||
$('.skip-binding-btn').on('click', function() {
|
||||
const $row = $(this).closest('.binding-row');
|
||||
const $select = $row.find('.binding-select');
|
||||
const $status = $row.find('.binding-status');
|
||||
const skipped = $row.toggleClass('is-skipped').hasClass('is-skipped');
|
||||
|
||||
$(this).toggleClass('btn-outline-secondary', !skipped).toggleClass('btn-secondary', skipped);
|
||||
$select.val(null).trigger('change').prop('disabled', skipped);
|
||||
$status.text(skipped ? 'Nessuna corrispondenza' : 'In attesa');
|
||||
refreshButton();
|
||||
});
|
||||
|
||||
$form.on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
$error.hide();
|
||||
$btn.prop('disabled', true).text('Salvataggio...');
|
||||
|
||||
const tasks = $('.binding-row').toArray().map(row => {
|
||||
const $row = $(row);
|
||||
const $status = $row.find('.binding-status');
|
||||
const datadbIds = JSON.stringify($row.data('datadb-ids') || []);
|
||||
const jsonValue = String($row.data('json-value'));
|
||||
|
||||
// Riga senza corrispondenza: azzera il valore, niente binding.
|
||||
if ($row.hasClass('is-skipped')) {
|
||||
return $.post('skip_binding.php', {
|
||||
mapping_id: $row.data('mapping-id'),
|
||||
json_value: jsonValue,
|
||||
datadb_ids: datadbIds
|
||||
}).then(resp => {
|
||||
if (resp && resp.success) {
|
||||
$status.text('Azzerato').removeClass('text-muted').addClass('text-success');
|
||||
return true;
|
||||
}
|
||||
$status.text('Errore').addClass('text-danger');
|
||||
throw new Error((resp && resp.error) || 'Errore azzeramento valore');
|
||||
});
|
||||
}
|
||||
|
||||
const $select = $row.find('.binding-select');
|
||||
const selectedData = $select.select2('data')[0] || {};
|
||||
|
||||
return $.post('save_binding.php', {
|
||||
mapping_id: $row.data('mapping-id'),
|
||||
field_id: $row.data('field-id'),
|
||||
template_id: TEMPLATE_ID,
|
||||
json_value: jsonValue,
|
||||
lims_value_id: $select.val(),
|
||||
lims_value: selectedData.text || '',
|
||||
datadb_ids: datadbIds
|
||||
}).then(resp => {
|
||||
if (resp && resp.success) {
|
||||
$status.text('Salvato').removeClass('text-muted').addClass('text-success');
|
||||
return true;
|
||||
}
|
||||
$status.text('Errore').addClass('text-danger');
|
||||
throw new Error((resp && resp.error) || 'Errore salvataggio binding');
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all(tasks)
|
||||
.then(() => {
|
||||
window.location.href = CONTINUE_URL;
|
||||
})
|
||||
.catch(err => {
|
||||
$error.text(err.message || 'Errore durante il salvataggio dei binding.').show();
|
||||
$btn.prop('disabled', false).text('Conferma e prosegui');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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.");
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
// Salva un binding JSON -> LIMS e lo applica ai record appena importati. Ritorna JSON.
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
require_once __DIR__ . '/class/binding-functions.php';
|
||||
include dirname(__DIR__) . '/../extra/auth.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
if (!Auth::check()) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$mappingId = intval($_POST['mapping_id'] ?? 0);
|
||||
$fieldId = intval($_POST['field_id'] ?? 0);
|
||||
$templateId = intval($_POST['template_id'] ?? 0);
|
||||
$jsonValue = (string) ($_POST['json_value'] ?? '');
|
||||
$limsValueId = intval($_POST['lims_value_id'] ?? 0);
|
||||
$limsValue = (string) ($_POST['lims_value'] ?? '');
|
||||
|
||||
$datadbIds = [];
|
||||
if (isset($_POST['datadb_ids'])) {
|
||||
$decoded = json_decode($_POST['datadb_ids'], true);
|
||||
if (is_array($decoded)) {
|
||||
$datadbIds = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
if ($mappingId <= 0 || $templateId <= 0 || $jsonValue === '' || $limsValueId <= 0) {
|
||||
http_response_code(422);
|
||||
echo json_encode(['success' => false, 'error' => 'Missing required parameters']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$createdBy = null;
|
||||
if (Auth::user()) {
|
||||
$createdBy = (int) Auth::user()->present()->id;
|
||||
}
|
||||
|
||||
if ($fieldId <= 0) {
|
||||
$stmt = $pdo->prepare("SELECT field_id FROM template_mapping WHERE id = ?");
|
||||
$stmt->execute([$mappingId]);
|
||||
$fieldId = (int) ($stmt->fetchColumn() ?: 0);
|
||||
}
|
||||
|
||||
binding_upsert($pdo, $templateId, $mappingId, $fieldId, $jsonValue, $limsValueId, $limsValue, $createdBy);
|
||||
|
||||
$applied = 0;
|
||||
if (!empty($datadbIds)) {
|
||||
$applied = binding_apply_to_details($pdo, $mappingId, $limsValue, $datadbIds);
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'applied_rows' => $applied,
|
||||
'mapping_id' => $mappingId,
|
||||
'json_value' => $jsonValue,
|
||||
'lims_value' => $limsValue,
|
||||
'lims_value_id' => $limsValueId,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -13,7 +13,7 @@ try {
|
||||
$iddatadb = intval($_POST['iddatadb']);
|
||||
$idclient = isset($_POST['idclient']) ? (is_numeric($_POST['idclient']) ? intval($_POST['idclient']) : null) : null;
|
||||
$clienteFornitoreId = isset($_POST['cliente_fornitore_id']) ? (is_numeric($_POST['cliente_fornitore_id']) ? intval($_POST['cliente_fornitore_id']) : null) : null;
|
||||
|
||||
$testedComponent = isset($_POST['tested_component']) ? trim((string)$_POST['tested_component']) : null;
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
@@ -144,7 +144,11 @@ try {
|
||||
$setParts[] = "cliente_fornitore_id = ?";
|
||||
$params[] = $clienteFornitoreId;
|
||||
}
|
||||
|
||||
// 5a3) tested_component (se presente)
|
||||
if (isset($testedComponent)) {
|
||||
$setParts[] = "tested_component = ?";
|
||||
$params[] = ($testedComponent === '') ? null : $testedComponent;
|
||||
}
|
||||
// 5b) fixed fields dal POST
|
||||
// QUI è il punto chiave: accettiamo SOLO colonne reali (realWhitelist),
|
||||
// ma se dal JS arrivassero ancora key logiche, funzionerebbe uguale
|
||||
|
||||
@@ -16,9 +16,11 @@ if (!$data || !isset($data['id'])) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$mappingId = $data['id'];
|
||||
$mappingId = (int)$data['id'];
|
||||
$mappingType = $data['mapping_type'] ?? '';
|
||||
|
||||
$excelColumn = $data['excel_column'] ?? null;
|
||||
$jsonNode = $data['json_node'] ?? null;
|
||||
$manualDefault = $data['manual_default'] ?? null;
|
||||
|
||||
$autoValue = $data['auto_value'] ?? 'none';
|
||||
@@ -26,7 +28,7 @@ $tablename = $data['tablename'] ?? '';
|
||||
|
||||
try {
|
||||
// Normalize mapping type
|
||||
$allowedTypes = ['', 'xls', 'manual', 'auto'];
|
||||
$allowedTypes = ['', 'xls', 'json', 'manual', 'auto'];
|
||||
if (!in_array($mappingType, $allowedTypes, true)) {
|
||||
echo json_encode(["success" => false, "message" => "Invalid mapping_type"]);
|
||||
exit;
|
||||
@@ -41,43 +43,67 @@ try {
|
||||
// Decide what to persist based on mapping_type
|
||||
$isManual = 0;
|
||||
$excelToSave = null;
|
||||
$jsonNodeToSave = null;
|
||||
$manualToSave = null;
|
||||
$autoToSave = 'none';
|
||||
|
||||
if ($mappingType === 'xls') {
|
||||
|
||||
$isManual = 0;
|
||||
$excelToSave = $excelColumn ?: null;
|
||||
$jsonNodeToSave = null;
|
||||
$manualToSave = null;
|
||||
$autoToSave = 'none';
|
||||
} elseif ($mappingType === 'json') {
|
||||
|
||||
$isManual = 0;
|
||||
$excelToSave = null;
|
||||
$jsonNodeToSave = $jsonNode ?: null;
|
||||
$manualToSave = null;
|
||||
$autoToSave = 'none';
|
||||
} elseif ($mappingType === 'manual') {
|
||||
|
||||
$isManual = 1;
|
||||
$excelToSave = null;
|
||||
$jsonNodeToSave = null;
|
||||
$manualToSave = $manualDefault;
|
||||
$autoToSave = 'none';
|
||||
} elseif ($mappingType === 'auto') {
|
||||
|
||||
$isManual = 0;
|
||||
$excelToSave = null;
|
||||
$jsonNodeToSave = null;
|
||||
$manualToSave = null;
|
||||
$autoToSave = $autoValue ?: 'none';
|
||||
} else {
|
||||
// reset
|
||||
|
||||
// Reset mapping
|
||||
$isManual = 0;
|
||||
$excelToSave = null;
|
||||
$jsonNodeToSave = null;
|
||||
$manualToSave = null;
|
||||
$autoToSave = 'none';
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE template_mapping
|
||||
SET
|
||||
is_manual = ?,
|
||||
excel_column = ?,
|
||||
manual_default = ?,
|
||||
auto_value = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
UPDATE template_mapping
|
||||
SET
|
||||
is_manual = ?,
|
||||
excel_column = ?,
|
||||
json_node = ?,
|
||||
manual_default = ?,
|
||||
auto_value = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
|
||||
$result = $stmt->execute([$isManual, $excelToSave, $manualToSave, $autoToSave, $mappingId]);
|
||||
$result = $stmt->execute([
|
||||
$isManual,
|
||||
$excelToSave,
|
||||
$jsonNodeToSave,
|
||||
$manualToSave,
|
||||
$autoToSave,
|
||||
$mappingId
|
||||
]);
|
||||
|
||||
if (!$result) {
|
||||
echo json_encode(["success" => false, "message" => "Database update failed"]);
|
||||
@@ -88,15 +114,17 @@ try {
|
||||
"success" => true,
|
||||
"message" => "Mapping updated successfully",
|
||||
"saved" => [
|
||||
"id" => (int)$mappingId,
|
||||
"id" => $mappingId,
|
||||
"mapping_type" => $mappingType,
|
||||
"is_manual" => $isManual,
|
||||
"excel_column" => $excelToSave,
|
||||
"json_node" => $jsonNodeToSave,
|
||||
"manual_default" => $manualToSave,
|
||||
"auto_value" => $autoToSave
|
||||
]
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
} catch (Throwable $e) {
|
||||
echo json_encode(["success" => false, "message" => "Error: " . $e->getMessage()]);
|
||||
}
|
||||
|
||||
exit;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -46,8 +46,8 @@
|
||||
{
|
||||
"IdSchemaCustomFields": 48,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Standard Generico \/ Generic Standard",
|
||||
"Descrizione": "Schema per tutti i campioni di qualsiasi matrice escluso cuoio\/pelle\r\n\r\n"
|
||||
"Nome": "Standard \/ Generico",
|
||||
"Descrizione": "\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 49,
|
||||
@@ -653,7 +653,7 @@
|
||||
"IdSchemaCustomFields": 163,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Devred",
|
||||
"Descrizione": "Schema creato per cliente DEVRED\r\nGR 18\/03\/2024\r\n\r\n"
|
||||
"Descrizione": "Schema creato per cliente DEVRED\r\nGR 18\/03\/2024 aggiornato il 23\/04\/2026\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 164,
|
||||
@@ -730,8 +730,8 @@
|
||||
{
|
||||
"IdSchemaCustomFields": 177,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Phoebe philo ACC",
|
||||
"Descrizione": "(scarpe, borse, cinture, occhiali, gioielleria)\r\n"
|
||||
"Nome": "Phoebe Philo ",
|
||||
"Descrizione": "\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 178,
|
||||
@@ -760,7 +760,7 @@
|
||||
{
|
||||
"IdSchemaCustomFields": 182,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Ralph Lauren - All testing V.6",
|
||||
"Nome": "Ralph Lauren - All testing V.10",
|
||||
"Descrizione": "AGGIORNAMENTO AL 16\/03\/2026 per estrazione fatturazione"
|
||||
},
|
||||
{
|
||||
@@ -876,6 +876,30 @@
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "ROSSIMODA",
|
||||
"Descrizione": "Per tutti i campioni di ROSSIMODA\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 202,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "LIMS-CIM - MAX MARA",
|
||||
"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,12 @@
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/db-functions.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';
|
||||
|
||||
@@ -14,44 +19,95 @@ $q = mb_strtolower(trim($_GET['q'] ?? ''));
|
||||
$limit = max(1, min(50, intval($_GET['limit'] ?? 20)));
|
||||
$id = isset($_GET['id']) ? intval($_GET['id']) : null;
|
||||
|
||||
function formatClientLabel(array $client): string
|
||||
{
|
||||
$name = trim($client['Nominativo'] ?? '');
|
||||
$id = trim((string)($client['IdCliente'] ?? ''));
|
||||
$code = trim((string)($client['CodiceCliente'] ?? ''));
|
||||
|
||||
$parts = explode('_', $code);
|
||||
$suffix = trim($parts[1] ?? '');
|
||||
|
||||
if ($suffix === '' && $code !== '') {
|
||||
$suffix = substr($code, 0, 1);
|
||||
}
|
||||
|
||||
if ($suffix === '') {
|
||||
$suffix = '--';
|
||||
}
|
||||
|
||||
return $name . ' - ' . $suffix . ' (ID: ' . $id . ')';
|
||||
}
|
||||
|
||||
try {
|
||||
// Load from cache or API
|
||||
$cacheFile = __DIR__ . '/cache/clienti.json';
|
||||
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
|
||||
$data = json_decode(file_get_contents($cacheFile), true);
|
||||
} else {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
$params = [
|
||||
'$select' => 'IdCliente,Nominativo,CodiceCliente',
|
||||
'$orderby' => 'Nominativo asc'
|
||||
];
|
||||
|
||||
$data = $api->get("Cliente?" . http_build_query($params));
|
||||
if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true);
|
||||
|
||||
if (!is_dir(__DIR__ . '/cache')) {
|
||||
mkdir(__DIR__ . '/cache', 0777, true);
|
||||
}
|
||||
|
||||
file_put_contents($cacheFile, json_encode($data));
|
||||
}
|
||||
|
||||
$clients = $data['value'] ?? [];
|
||||
|
||||
// If requesting by specific ID (for loading selected value)
|
||||
// If requesting by specific ID, used for loading selected value
|
||||
if ($id !== null) {
|
||||
foreach ($clients as $c) {
|
||||
if ((int)$c['IdCliente'] === $id) {
|
||||
echo json_encode(['results' => [['id' => $c['IdCliente'], 'text' => trim($c['Nominativo'] ?? '')]]]);
|
||||
echo json_encode([
|
||||
'results' => [[
|
||||
'id' => $c['IdCliente'],
|
||||
'text' => formatClientLabel($c),
|
||||
'IdCliente' => $c['IdCliente'],
|
||||
'Nominativo' => trim($c['Nominativo'] ?? ''),
|
||||
'CodiceCliente' => trim($c['CodiceCliente'] ?? '')
|
||||
]]
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['results' => []]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Search by query
|
||||
$results = [];
|
||||
|
||||
foreach ($clients as $c) {
|
||||
$name = trim($c['Nominativo'] ?? '');
|
||||
$code = trim($c['CodiceCliente'] ?? '');
|
||||
if ($q === '' || mb_strpos(mb_strtolower($name), $q) !== false || mb_strpos(mb_strtolower($code), $q) !== false) {
|
||||
$results[] = ['id' => $c['IdCliente'], 'text' => $name];
|
||||
if (count($results) >= $limit) break;
|
||||
|
||||
if (
|
||||
$q === '' ||
|
||||
mb_strpos(mb_strtolower($name), $q) !== false ||
|
||||
mb_strpos(mb_strtolower($code), $q) !== false
|
||||
) {
|
||||
$results[] = [
|
||||
'id' => $c['IdCliente'],
|
||||
'text' => formatClientLabel($c),
|
||||
'IdCliente' => $c['IdCliente'],
|
||||
'Nominativo' => $name,
|
||||
'CodiceCliente' => $code
|
||||
];
|
||||
|
||||
if (count($results) >= $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/db-functions.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';
|
||||
|
||||
@@ -13,7 +17,8 @@ error_reporting(E_ALL);
|
||||
$fieldId = intval($_GET['field_id'] ?? 0);
|
||||
$q = mb_strtolower(trim($_GET['q'] ?? ''));
|
||||
$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) {
|
||||
echo json_encode(['results' => []]);
|
||||
@@ -52,7 +57,7 @@ try {
|
||||
$text = $v['Valore'] ?? '';
|
||||
if ($q === '' || mb_strpos(mb_strtolower($text), $q) !== false) {
|
||||
$results[] = ['id' => $v['IdCustomFieldsValue'], 'text' => $text];
|
||||
if (count($results) >= $limit) break;
|
||||
if ($limit > 0 && count($results) >= $limit) break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
// Esempio: search_rapporto_min.php?codice=2621716
|
||||
$codice = trim($_GET['codice'] ?? '');
|
||||
|
||||
if ($codice === '') {
|
||||
throw new Exception("Parametro codice mancante. Usa ?codice=2621716");
|
||||
}
|
||||
|
||||
/*
|
||||
* Minimal:
|
||||
* - niente expand
|
||||
* - max 5 record
|
||||
* - pochi campi
|
||||
*
|
||||
* Se il campo Codice non è quello giusto, sotto proviamo anche CodiceRapporto.
|
||||
*/
|
||||
$attempts = [];
|
||||
|
||||
$filters = [
|
||||
'Codice' => "Codice eq '{$codice}'",
|
||||
'CodiceRapporto' => "CodiceRapporto eq '{$codice}'",
|
||||
'Numero' => "Numero eq '{$codice}'"
|
||||
];
|
||||
|
||||
foreach ($filters as $label => $filter) {
|
||||
try {
|
||||
$data = $api->get('Rapporto', [
|
||||
'$filter' => $filter,
|
||||
'$top' => 5,
|
||||
'$select' => 'IdRapporto,Codice,CodiceRapporto,Numero,Data,Versione,Cliente'
|
||||
]);
|
||||
|
||||
$attempts[$label] = [
|
||||
'success' => true,
|
||||
'filter' => $filter,
|
||||
'records' => isset($data['value']) && is_array($data['value']) ? count($data['value']) : null,
|
||||
'data' => $data
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
$attempts[$label] = [
|
||||
'success' => false,
|
||||
'filter' => $filter,
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'input_codice' => $codice,
|
||||
'attempts' => $attempts
|
||||
], 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,55 @@
|
||||
<?php
|
||||
|
||||
// "Nessuna corrispondenza": azzera il valore grezzo nei record importati, senza salvare binding.
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
require_once __DIR__ . '/class/binding-functions.php';
|
||||
include dirname(__DIR__) . '/../extra/auth.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
if (!Auth::check()) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$mappingId = intval($_POST['mapping_id'] ?? 0);
|
||||
$jsonValue = (string) ($_POST['json_value'] ?? '');
|
||||
|
||||
$datadbIds = [];
|
||||
if (isset($_POST['datadb_ids'])) {
|
||||
$decoded = json_decode($_POST['datadb_ids'], true);
|
||||
if (is_array($decoded)) {
|
||||
$datadbIds = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
if ($mappingId <= 0 || $jsonValue === '') {
|
||||
http_response_code(422);
|
||||
echo json_encode(['success' => false, 'error' => 'Missing required parameters']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
$cleared = binding_apply_to_details($pdo, $mappingId, '', $datadbIds);
|
||||
|
||||
// Rimuovo un eventuale binding gia' salvato (es. auto-collegato) per coerenza.
|
||||
$del = $pdo->prepare("DELETE FROM json_lims_binding WHERE mapping_id = ? AND json_value = ?");
|
||||
$del->execute([$mappingId, $jsonValue]);
|
||||
|
||||
echo json_encode(['success' => true, 'cleared_rows' => $cleared]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -78,6 +78,51 @@
|
||||
color: #198754;
|
||||
}
|
||||
|
||||
.badge-source-pdf {
|
||||
background-color: #fff3cd;
|
||||
color: #b58100;
|
||||
}
|
||||
|
||||
.type-filter-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.type-filter-btn {
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
padding: 7px 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
opacity: 0.35;
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.type-filter-btn.active {
|
||||
opacity: 1;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.type-filter-btn[data-type="XLS"] {
|
||||
background-color: #0d6efd;
|
||||
}
|
||||
|
||||
.type-filter-btn[data-type="API"] {
|
||||
background-color: #198754;
|
||||
}
|
||||
|
||||
.type-filter-btn[data-type="PDF"] {
|
||||
background-color: #b58100;
|
||||
}
|
||||
|
||||
.type-filter-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
#xlsTemplatesTable {
|
||||
font-size: 13px;
|
||||
}
|
||||
@@ -136,214 +181,320 @@
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<div class="type-filter-bar">
|
||||
<span class="text-muted fw-semibold me-1">Filter by type:</span>
|
||||
|
||||
<button type="button" class="type-filter-btn active" data-type="XLS">
|
||||
XLS
|
||||
</button>
|
||||
|
||||
<button type="button" class="type-filter-btn active" data-type="API">
|
||||
JSON/API
|
||||
</button>
|
||||
|
||||
<button type="button" class="type-filter-btn active" data-type="PDF">
|
||||
PDF
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="xlsTemplatesTable" class="table table-striped table-bordered table-sm w-100">
|
||||
<table id="xlsTemplatesTable" class="table table-striped table-bordered align-middle w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= htmlspecialchars($action, ENT_QUOTES, 'UTF-8'); ?></th>
|
||||
<th><?= htmlspecialchars($nametemplate, ENT_QUOTES, 'UTF-8'); ?></th>
|
||||
<th>Actions</th>
|
||||
<th>Template Name</th>
|
||||
<th>Type</th>
|
||||
<th>Row</th>
|
||||
<th>Col</th>
|
||||
<th><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></th>
|
||||
<th>Description</th>
|
||||
<th>Client</th>
|
||||
<th>Button</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- DataTables will populate this section automatically -->
|
||||
</tbody>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!--end page wrapper -->
|
||||
|
||||
<!--start overlay-->
|
||||
<div class="overlay toggle-icon"></div>
|
||||
<!--end overlay-->
|
||||
|
||||
<!--Start Back To Top Button-->
|
||||
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
||||
<!--End Back To Top Button-->
|
||||
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
</div>
|
||||
<!--end page wrapper -->
|
||||
<!--end wrapper-->
|
||||
|
||||
<!--start overlay-->
|
||||
<div class="overlay toggle-icon"></div>
|
||||
<!--end overlay-->
|
||||
<?php include('jsinclude.php'); ?>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||
|
||||
<!--Start Back To Top Button-->
|
||||
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
||||
<!--End Back To Top Button-->
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
<!--end wrapper-->
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||
if (urlParams.get('cloned') === '1') {
|
||||
Swal.fire({
|
||||
title: "Template cloned",
|
||||
text: "The template was cloned successfully.",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK"
|
||||
});
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#xlsTemplatesTable').DataTable({
|
||||
processing: true,
|
||||
serverSide: false,
|
||||
ajax: 'load_templates.php',
|
||||
pageLength: 50,
|
||||
autoWidth: false,
|
||||
columns: [{
|
||||
data: 'id',
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
title: "Actions",
|
||||
className: "table-actions text-center",
|
||||
render: function(data, type, row) {
|
||||
return `
|
||||
<div class="d-flex justify-content-center gap-1">
|
||||
<a href="edit_template_xls.php?id=${data}" class="btn btn-sm btn-primary" title="Edit">
|
||||
<i class="bx bx-edit-alt"></i>
|
||||
</a>
|
||||
<a href="mapping_template_xls_scheme2.php?id=${data}" class="btn btn-sm btn-success" title="Mapping">
|
||||
<i class="bx bx-link-alt"></i>
|
||||
</a>
|
||||
<button class="btn btn-sm btn-danger" onclick="confirmDelete(${data})" title="Delete">
|
||||
<i class="bx bx-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'name',
|
||||
title: "Template Name",
|
||||
className: "name-cell"
|
||||
},
|
||||
{
|
||||
data: 'source_type',
|
||||
title: "Type",
|
||||
className: "text-center",
|
||||
render: function(data, type, row) {
|
||||
const sourceType = (data || 'XLS').toUpperCase();
|
||||
const cleanUrl = window.location.pathname;
|
||||
window.history.replaceState({}, document.title, cleanUrl);
|
||||
}
|
||||
const templatesTable = $('#xlsTemplatesTable').DataTable({
|
||||
processing: true,
|
||||
serverSide: false,
|
||||
ajax: 'load_templates.php',
|
||||
pageLength: 50,
|
||||
autoWidth: false,
|
||||
columns: [{
|
||||
data: 'id',
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
title: "Actions",
|
||||
className: "table-actions text-center",
|
||||
render: function(data, type, row) {
|
||||
return `
|
||||
<div class="d-flex justify-content-center gap-1">
|
||||
<a href="edit_template_xls.php?id=${data}" class="btn btn-sm btn-primary" title="Edit">
|
||||
<i class="bx bx-edit-alt"></i>
|
||||
</a>
|
||||
|
||||
if (type === 'display') {
|
||||
if (sourceType === 'API') {
|
||||
return '<span class="badge-source badge-source-api">API</span>';
|
||||
<a href="mapping_template_xls_scheme2.php?id=${data}" class="btn btn-sm btn-success" title="Mapping">
|
||||
<i class="bx bx-link-alt"></i>
|
||||
</a>
|
||||
|
||||
<button class="btn btn-sm btn-warning" onclick="confirmClone(${data}, '${String(row.name || '').replace(/'/g, "\\'")}')" title="Clone">
|
||||
<i class="bx bx-copy"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-sm btn-danger" onclick="confirmDelete(${data})" title="Delete">
|
||||
<i class="bx bx-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return '<span class="badge-source badge-source-xls">XLS</span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'name',
|
||||
title: "Template Name",
|
||||
className: "name-cell"
|
||||
},
|
||||
{
|
||||
data: 'source_type',
|
||||
title: "Type",
|
||||
className: "text-center",
|
||||
render: function(data, type, row) {
|
||||
let sourceType = (data || 'XLS').toUpperCase();
|
||||
|
||||
return sourceType;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'header_row',
|
||||
title: "Row",
|
||||
className: "text-center",
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
data: 'start_column',
|
||||
title: "Col",
|
||||
className: "text-center",
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
data: 'description',
|
||||
title: "Description",
|
||||
className: "description-cell",
|
||||
defaultContent: 'No description'
|
||||
},
|
||||
{
|
||||
data: null,
|
||||
title: "Client",
|
||||
className: "client-cell",
|
||||
render: function(data, type, row) {
|
||||
const clientName = row.clientname || "No client";
|
||||
const clientId = row.idclient || "N/A";
|
||||
return `${clientName} <small class="text-muted">(ID: ${clientId})</small>`;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'button_label',
|
||||
title: "Button",
|
||||
className: "button-cell",
|
||||
defaultContent: 'Click Me'
|
||||
},
|
||||
{
|
||||
data: 'status',
|
||||
title: "Status",
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
className: "text-center",
|
||||
render: function(status, type, row) {
|
||||
let checked = (status === "active") ? "checked" : "";
|
||||
return `
|
||||
// Treat JSON as API group for dashboard filter
|
||||
if (sourceType === 'JSON') {
|
||||
sourceType = 'API';
|
||||
}
|
||||
|
||||
if (type === 'display') {
|
||||
if (sourceType === 'API') {
|
||||
return '<span class="badge-source badge-source-api">JSON/API</span>';
|
||||
}
|
||||
|
||||
if (sourceType === 'PDF') {
|
||||
return '<span class="badge-source badge-source-pdf">PDF</span>';
|
||||
}
|
||||
|
||||
return '<span class="badge-source badge-source-xls">XLS</span>';
|
||||
}
|
||||
|
||||
return sourceType;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'header_row',
|
||||
title: "Row",
|
||||
className: "text-center",
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
data: 'start_column',
|
||||
title: "Col",
|
||||
className: "text-center",
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
data: 'description',
|
||||
title: "Description",
|
||||
className: "description-cell",
|
||||
defaultContent: 'No description'
|
||||
},
|
||||
{
|
||||
data: null,
|
||||
title: "Client",
|
||||
className: "client-cell",
|
||||
render: function(data, type, row) {
|
||||
const clientName = row.clientname || "No client";
|
||||
const clientId = row.idclient || "N/A";
|
||||
return `${clientName} <small class="text-muted">(ID: ${clientId})</small>`;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'button_label',
|
||||
title: "Button",
|
||||
className: "button-cell",
|
||||
defaultContent: 'Click Me'
|
||||
},
|
||||
{
|
||||
data: 'status',
|
||||
title: "Status",
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
className: "text-center",
|
||||
render: function(status, type, row) {
|
||||
let checked = (status === "active") ? "checked" : "";
|
||||
return `
|
||||
<label class="switch">
|
||||
<input type="checkbox" class="toggle-status" data-id="${row.id}" ${checked}>
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
`;
|
||||
}
|
||||
}
|
||||
],
|
||||
dom: '<"card-header border-bottom p-3"<"d-flex align-items-center"<"card-title mb-0 flex-grow-1"f>>>rt<"card-footer border-top p-3"<"d-flex align-items-center"<"me-auto"l><"d-flex gap-2"ip>>>',
|
||||
lengthMenu: [10, 25, 50, 100],
|
||||
order: [
|
||||
[1, 'asc']
|
||||
],
|
||||
language: {
|
||||
search: "Cerca:",
|
||||
lengthMenu: "Mostra _MENU_ elementi",
|
||||
info: "Visualizzando da _START_ a _END_ di _TOTAL_ elementi",
|
||||
paginate: {
|
||||
first: "<?= isset($langdatatables['paginate_first']) ? $langdatatables['paginate_first'] : 'Primo' ?>",
|
||||
last: "<?= isset($langdatatables['paginate_last']) ? $langdatatables['paginate_last'] : 'Ultimo' ?>",
|
||||
next: "<?= isset($langdatatables['paginate_next']) ? $langdatatables['paginate_next'] : 'Successivo' ?>",
|
||||
previous: "<?= isset($langdatatables['paginate_previous']) ? $langdatatables['paginate_previous'] : 'Precedente' ?>"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
dom: '<"card-header border-bottom p-3"<"d-flex align-items-center"<"card-title mb-0 flex-grow-1"f>>>rt<"card-footer border-top p-3"<"d-flex align-items-center"<"me-auto"l><"d-flex gap-2"ip>>>',
|
||||
lengthMenu: [10, 25, 50, 100],
|
||||
order: [
|
||||
[1, 'asc']
|
||||
],
|
||||
language: {
|
||||
search: "Cerca:",
|
||||
lengthMenu: "Mostra _MENU_ elementi",
|
||||
info: "Visualizzando da _START_ a _END_ di _TOTAL_ elementi",
|
||||
paginate: {
|
||||
first: "<?= isset($langdatatables['paginate_first']) ? $langdatatables['paginate_first'] : 'Primo' ?>",
|
||||
last: "<?= isset($langdatatables['paginate_last']) ? $langdatatables['paginate_last'] : 'Ultimo' ?>",
|
||||
next: "<?= isset($langdatatables['paginate_next']) ? $langdatatables['paginate_next'] : 'Successivo' ?>",
|
||||
previous: "<?= isset($langdatatables['paginate_previous']) ? $langdatatables['paginate_previous'] : 'Precedente' ?>"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
const activeSourceTypes = {
|
||||
XLS: true,
|
||||
API: true,
|
||||
PDF: true
|
||||
};
|
||||
|
||||
function confirmDelete(id) {
|
||||
Swal.fire({
|
||||
title: "Are you sure?",
|
||||
text: "This action cannot be undone!",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#d33",
|
||||
cancelButtonColor: "#3085d6",
|
||||
confirmButtonText: "Yes, delete it!",
|
||||
cancelButtonText: "Cancel"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = `delete_template_xls.php?id=${id}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
$.fn.dataTable.ext.search.push(function(settings, data, dataIndex) {
|
||||
if (settings.nTable.id !== 'xlsTemplatesTable') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$(document).on("change", ".toggle-status", function() {
|
||||
let templateId = $(this).data("id");
|
||||
let newStatus = $(this).is(":checked") ? "active" : "inactive";
|
||||
const api = new $.fn.dataTable.Api(settings);
|
||||
const rowData = api.row(dataIndex).data();
|
||||
|
||||
$.ajax({
|
||||
url: "update_template_status.php",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
data: {
|
||||
id: templateId,
|
||||
status: newStatus
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
console.log("Status updated successfully.");
|
||||
} else {
|
||||
console.error("Error updating status:", response.message);
|
||||
alert("Error updating status: " + response.message);
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
console.error("AJAX error:", xhr.responseText);
|
||||
let sourceType = ((rowData && rowData.source_type) ? rowData.source_type : 'XLS').toUpperCase();
|
||||
|
||||
if (sourceType === 'JSON') {
|
||||
sourceType = 'API';
|
||||
}
|
||||
|
||||
return activeSourceTypes[sourceType] === true;
|
||||
});
|
||||
|
||||
$('.type-filter-btn').on('click', function() {
|
||||
const type = $(this).data('type');
|
||||
|
||||
activeSourceTypes[type] = !activeSourceTypes[type];
|
||||
$(this).toggleClass('active', activeSourceTypes[type]);
|
||||
|
||||
const hasAtLeastOneActive = Object.values(activeSourceTypes).some(Boolean);
|
||||
|
||||
if (!hasAtLeastOneActive) {
|
||||
activeSourceTypes[type] = true;
|
||||
$(this).addClass('active');
|
||||
}
|
||||
|
||||
$('#xlsTemplatesTable').DataTable().draw();
|
||||
});
|
||||
});
|
||||
|
||||
function confirmDelete(id) {
|
||||
Swal.fire({
|
||||
title: "Are you sure?",
|
||||
text: "This action cannot be undone!",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#d33",
|
||||
cancelButtonColor: "#3085d6",
|
||||
confirmButtonText: "Yes, delete it!",
|
||||
cancelButtonText: "Cancel"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = `delete_template_xls.php?id=${id}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
function confirmClone(id, templateName) {
|
||||
Swal.fire({
|
||||
title: "Clone template?",
|
||||
html: `
|
||||
<div class="text-start">
|
||||
<p class="mb-2">You are about to clone this template:</p>
|
||||
<strong>${templateName}</strong>
|
||||
<p class="mt-3 mb-0 text-muted">
|
||||
The new template will be created as <strong>Copia di ${templateName}</strong> and all mappings will be copied.
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
icon: "question",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#ffc107",
|
||||
cancelButtonColor: "#6c757d",
|
||||
confirmButtonText: "Yes, clone it",
|
||||
cancelButtonText: "Cancel"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = `clone_template.php?id=${id}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).on("change", ".toggle-status", function() {
|
||||
let templateId = $(this).data("id");
|
||||
let newStatus = $(this).is(":checked") ? "active" : "inactive";
|
||||
|
||||
$.ajax({
|
||||
url: "update_template_status.php",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
data: {
|
||||
id: templateId,
|
||||
status: newStatus
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
console.log("Status updated successfully.");
|
||||
} else {
|
||||
console.error("Error updating status:", response.message);
|
||||
alert("Error updating status: " + response.message);
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
console.error("AJAX error:", xhr.responseText);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ ini_set('log_errors', 1);
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
Dotenv\Dotenv::createImmutable(dirname(__DIR__, 2))->safeLoad();
|
||||
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
ini_set('display_errors', '1');
|
||||
error_reporting(E_ALL);
|
||||
set_time_limit(60);
|
||||
|
||||
/**
|
||||
* Uso: get_pdf.php?id=591749
|
||||
*/
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
$idRapportoFile = isset($_GET['id']) ? intval($_GET['id']) : 0;
|
||||
|
||||
if (!$idRapportoFile) {
|
||||
throw new Exception("Parametro id mancante. Usa ?id=591749");
|
||||
}
|
||||
|
||||
// STEP 1: leggi metadati entita
|
||||
$entityData = $api->get("RapportoFile(" . $idRapportoFile . ")");
|
||||
|
||||
$pdfBody = null;
|
||||
$strategyUsed = null;
|
||||
$debugLog = array();
|
||||
|
||||
// Strategia A - campo FileContent base64
|
||||
if (!empty($entityData['FileContent'])) {
|
||||
$decoded = base64_decode($entityData['FileContent'], true);
|
||||
if ($decoded !== false && substr($decoded, 0, 4) === '%PDF') {
|
||||
$pdfBody = $decoded;
|
||||
$strategyUsed = 'A: FileContent base64';
|
||||
}
|
||||
}
|
||||
|
||||
// Strategia B - campo Contenuto base64
|
||||
if (!$pdfBody && !empty($entityData['Contenuto'])) {
|
||||
$decoded = base64_decode($entityData['Contenuto'], true);
|
||||
if ($decoded !== false && substr($decoded, 0, 4) === '%PDF') {
|
||||
$pdfBody = $decoded;
|
||||
$strategyUsed = 'B: Contenuto base64';
|
||||
}
|
||||
}
|
||||
|
||||
// Recupera token e baseUrl dalla classe
|
||||
$token = $api->getToken();
|
||||
$baseUrl = rtrim($api->getBaseUrl(), '/');
|
||||
|
||||
$rawEndpoints = array(
|
||||
'C: $value' => $baseUrl . '/api/odata/RapportoFile(' . $idRapportoFile . ')/$value',
|
||||
'D: FileContent/$value' => $baseUrl . '/api/odata/RapportoFile(' . $idRapportoFile . ')/FileContent/$value',
|
||||
'E: Contenuto/$value' => $baseUrl . '/api/odata/RapportoFile(' . $idRapportoFile . ')/Contenuto/$value',
|
||||
);
|
||||
|
||||
foreach ($rawEndpoints as $label => $url) {
|
||||
if ($pdfBody) break;
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
|
||||
'Authorization: Bearer ' . $token,
|
||||
'Accept: application/pdf, application/octet-stream, */*',
|
||||
));
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
|
||||
$body = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$ct = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
curl_close($ch);
|
||||
|
||||
$debugLog[$label] = array(
|
||||
'url' => $url,
|
||||
'http_code' => $httpCode,
|
||||
'content_type' => $ct,
|
||||
'body_start' => substr((string)$body, 0, 300),
|
||||
);
|
||||
|
||||
if ($httpCode === 200 && is_string($body) && substr($body, 0, 4) === '%PDF') {
|
||||
$pdfBody = $body;
|
||||
$strategyUsed = $label;
|
||||
}
|
||||
}
|
||||
|
||||
// RISPOSTA
|
||||
if ($pdfBody) {
|
||||
$fileName = isset($entityData['FileName']) ? $entityData['FileName'] : 'rapporto_' . $idRapportoFile . '.pdf';
|
||||
header('Content-Type: application/pdf');
|
||||
header('Content-Disposition: inline; filename="' . $fileName . '"');
|
||||
header('Content-Length: ' . strlen($pdfBody));
|
||||
header('Cache-Control: private, max-age=0, must-revalidate');
|
||||
echo $pdfBody;
|
||||
exit;
|
||||
}
|
||||
|
||||
// Nessuna strategia ha funzionato
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(array(
|
||||
'success' => false,
|
||||
'message' => 'Nessuna strategia ha restituito un PDF valido.',
|
||||
'id_rapporto_file' => $idRapportoFile,
|
||||
'entity_fields' => array_keys($entityData ? $entityData : array()),
|
||||
'entity_data' => $entityData,
|
||||
'strategies_debug' => $debugLog,
|
||||
), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(array(
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
|
||||
// ✅ FIX timezone (Rome)
|
||||
ini_set('date.timezone', 'Europe/Rome');
|
||||
date_default_timezone_set('Europe/Rome');
|
||||
|
||||
$template_id = intval($_GET['id'] ?? 0);
|
||||
if (!$template_id) {
|
||||
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Template ID mancante"));
|
||||
@@ -2016,6 +2012,51 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
|
||||
el.addEventListener('change', () => el.classList.toggle('has-value', el.value.trim() !== ''));
|
||||
});
|
||||
})();
|
||||
|
||||
// ── Column resize ──
|
||||
(function() {
|
||||
const resizers = document.querySelectorAll('.resizer');
|
||||
let currentResizer = null, startX = 0, startWidth = 0, columnIndex = null;
|
||||
|
||||
function resize(e) {
|
||||
if (!currentResizer || columnIndex === null) return;
|
||||
const newWidth = Math.max(80, startWidth + (e.pageX - startX));
|
||||
const sel = '[data-index="' + columnIndex + '"]';
|
||||
const header = document.querySelector('.grid-header' + sel);
|
||||
if (header) header.style.flex = '0 0 ' + newWidth + 'px';
|
||||
const topCell = document.querySelector('.grid-top .grid-cell' + sel);
|
||||
if (topCell) topCell.style.flex = '0 0 ' + newWidth + 'px';
|
||||
document.querySelectorAll('.grid-row .grid-cell' + sel).forEach(function(cell) {
|
||||
cell.style.flex = '0 0 ' + newWidth + 'px';
|
||||
});
|
||||
var filterCell = document.querySelector('#gridFilterRow > .grid-cell:nth-child(' + (parseInt(columnIndex, 10) + 1) + ')');
|
||||
if (filterCell) filterCell.style.flex = '0 0 ' + newWidth + 'px';
|
||||
}
|
||||
|
||||
function stopResize() {
|
||||
if (currentResizer) {
|
||||
document.removeEventListener('mousemove', resize);
|
||||
document.removeEventListener('mouseup', stopResize);
|
||||
currentResizer = null;
|
||||
columnIndex = null;
|
||||
}
|
||||
}
|
||||
|
||||
resizers.forEach(function(resizer) {
|
||||
resizer.addEventListener('mousedown', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
currentResizer = this;
|
||||
var header = this.closest('.grid-header');
|
||||
if (!header) return;
|
||||
columnIndex = header.getAttribute('data-index');
|
||||
startX = e.pageX;
|
||||
startWidth = header.offsetWidth;
|
||||
document.addEventListener('mousemove', resize);
|
||||
document.addEventListener('mouseup', stopResize);
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$input) {
|
||||
throw new Exception('Invalid JSON payload');
|
||||
}
|
||||
|
||||
$templateId = isset($input['template_id']) ? (int)$input['template_id'] : 0;
|
||||
$apiSampleJson = $input['api_sample_json'] ?? '';
|
||||
$jsonNodes = $input['json_nodes'] ?? '';
|
||||
|
||||
if ($templateId <= 0) {
|
||||
throw new Exception('Invalid template ID');
|
||||
}
|
||||
|
||||
if (trim($apiSampleJson) === '') {
|
||||
throw new Exception('API sample JSON is empty');
|
||||
}
|
||||
|
||||
json_decode($apiSampleJson, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new Exception('Invalid API sample JSON: ' . json_last_error_msg());
|
||||
}
|
||||
|
||||
$decodedNodes = json_decode($jsonNodes, true);
|
||||
|
||||
if (!is_array($decodedNodes)) {
|
||||
throw new Exception('Invalid JSON nodes array');
|
||||
}
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE excel_templates
|
||||
SET
|
||||
api_sample_json = ?,
|
||||
json_nodes = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
$apiSampleJson,
|
||||
json_encode($decodedNodes, JSON_UNESCAPED_UNICODE),
|
||||
$templateId
|
||||
]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'nodes_count' => count($decodedNodes)
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
@@ -3,33 +3,84 @@ header('Content-Type: application/json');
|
||||
|
||||
require_once(__DIR__ . '/include/headscript.php');
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
try {
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$id = (int)($input['id'] ?? 0);
|
||||
$field = (string)($input['field'] ?? '');
|
||||
$value = $input['value'] ?? null;
|
||||
$id = (int)($input['id'] ?? 0);
|
||||
$field = (string)($input['field'] ?? '');
|
||||
$value = $input['value'] ?? null;
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid id']);
|
||||
exit;
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$allowed = [
|
||||
'default_value',
|
||||
'default_source',
|
||||
'json_node',
|
||||
'is_visible_import',
|
||||
'is_required'
|
||||
];
|
||||
|
||||
if (!in_array($field, $allowed, true)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid field: ' . $field]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($field === 'is_visible_import' || $field === 'is_required') {
|
||||
$value = ((int)$value === 1) ? 1 : 0;
|
||||
}
|
||||
|
||||
if ($field === 'default_source') {
|
||||
$value = (string)$value;
|
||||
|
||||
if (!in_array($value, ['manual', 'json'], true)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid default_source']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// If the user goes back to manual, clear the JSON node
|
||||
if ($value === 'manual') {
|
||||
$sql = "
|
||||
UPDATE template_fixed_mapping
|
||||
SET default_source = :val, json_node = NULL
|
||||
WHERE id = :id
|
||||
";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$ok = $stmt->execute([
|
||||
':val' => $value,
|
||||
':id' => $id
|
||||
]);
|
||||
|
||||
echo json_encode(['success' => (bool)$ok]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if ($field === 'json_node') {
|
||||
$value = trim((string)$value);
|
||||
|
||||
if ($value === '') {
|
||||
$value = null;
|
||||
}
|
||||
}
|
||||
|
||||
$sql = "UPDATE template_fixed_mapping SET {$field} = :val WHERE id = :id";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
|
||||
$ok = $stmt->execute([
|
||||
':val' => $value,
|
||||
':id' => $id
|
||||
]);
|
||||
|
||||
echo json_encode(['success' => (bool)$ok]);
|
||||
} catch (Throwable $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
$allowed = ['default_value', 'is_visible_import', 'is_required'];
|
||||
if (!in_array($field, $allowed, true)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid field']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($field === 'is_visible_import' || $field === 'is_required') {
|
||||
$value = ((int)$value === 1) ? 1 : 0;
|
||||
}
|
||||
|
||||
|
||||
$sql = "UPDATE template_fixed_mapping SET {$field} = :val WHERE id = :id";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$ok = $stmt->execute([':val' => $value, ':id' => $id]);
|
||||
|
||||
echo json_encode(['success' => (bool)$ok]);
|
||||
|
||||
@@ -3,14 +3,18 @@ ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
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);
|
||||
|
||||
$template_id = intval($data['template_id']);
|
||||
$mapping_id = intval($data['mapping_id']);
|
||||
$value = intval($data['value']);
|
||||
$template_id = intval($data['template_id'] ?? 0);
|
||||
$mapping_id = intval($data['mapping_id'] ?? 0);
|
||||
$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) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid IDs']);
|
||||
@@ -23,19 +27,47 @@ $pdo = $db->getConnection();
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
if ($value === 1) {
|
||||
// Setta tutti main_field a 0 per questo template
|
||||
$stmt = $pdo->prepare("UPDATE template_mapping SET main_field = 0 WHERE template_id = ?");
|
||||
$stmt->execute([$template_id]);
|
||||
if ($mainValue === '1') {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT COUNT(*)
|
||||
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("UPDATE template_mapping SET main_field = ? WHERE id = ? AND template_id = ?");
|
||||
$stmt->execute([$value, $mapping_id, $template_id]);
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE template_mapping
|
||||
SET main_field = ?
|
||||
WHERE id = ?
|
||||
AND template_id = ?
|
||||
");
|
||||
$stmt->execute([$mainValue, $mapping_id, $template_id]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (Exception $e) {
|
||||
$pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
header('Content-Type: application/json');
|
||||
require_once 'class/db-functions.php';
|
||||
|
||||
// Disabilita l'output di errori HTML
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
@@ -13,108 +12,238 @@ try {
|
||||
throw new Exception("Invalid request method.");
|
||||
}
|
||||
|
||||
// Recupera i dati dal body JSON
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$template_id = isset($input['template_id']) ? intval($input['template_id']) : null;
|
||||
$schemajson = isset($input['schemajson']) ? trim($input['schemajson']) : '';
|
||||
|
||||
// Controllo sui campi obbligatori
|
||||
$template_id = isset($input['template_id']) ? intval($input['template_id']) : null;
|
||||
$schemajson = isset($input['schemajson']) ? trim($input['schemajson']) : '';
|
||||
|
||||
if (empty($template_id) || empty($schemajson)) {
|
||||
throw new Exception("All fields marked with * are required, including schemajson.");
|
||||
}
|
||||
|
||||
// Validazione del JSON
|
||||
$decoded_json = json_decode($schemajson, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new Exception("Invalid JSON format for schemajson.");
|
||||
}
|
||||
|
||||
// Connessione al database
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
// Recupera il target_table e verifica l'esistenza del template
|
||||
$stmt = $pdo->prepare("SELECT target_table FROM excel_templates WHERE id = ?");
|
||||
$stmt->execute([$template_id]);
|
||||
$template = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$template) {
|
||||
throw new Exception("Template not found.");
|
||||
}
|
||||
|
||||
$target_table = $template['target_table'];
|
||||
|
||||
// Inizia una transazione
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Aggiorna il schemajson in excel_templates
|
||||
$stmt = $pdo->prepare("UPDATE excel_templates
|
||||
SET schemajson = ?, updated_at = NOW()
|
||||
WHERE id = ?");
|
||||
$stmt->execute([$schemajson, $template_id]);
|
||||
|
||||
// Estrai i campi dallo schema
|
||||
$schema_id = $decoded_json['IdSchemaCustomFields'] ?? null;
|
||||
$schema_fields = $decoded_json['SchemiCustomFieldsDettagli'] ?? [];
|
||||
|
||||
if (empty($schema_id)) {
|
||||
throw new Exception("Schema ID not found in schemajson.");
|
||||
}
|
||||
|
||||
if (empty($schema_fields)) {
|
||||
throw new Exception("No fields found in schema.");
|
||||
}
|
||||
|
||||
// Prepara la query per l'inserimento condizionato in template_mapping
|
||||
$insert_stmt = $pdo->prepare("
|
||||
$pdo->beginTransaction();
|
||||
|
||||
/*
|
||||
* 1) Update raw schema JSON in excel_templates
|
||||
*/
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE excel_templates
|
||||
SET schemajson = ?, updated_at = NOW()
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$schemajson, $template_id]);
|
||||
|
||||
/*
|
||||
* Prepare reusable statements
|
||||
*/
|
||||
|
||||
// Check if mapping already exists
|
||||
$checkStmt = $pdo->prepare("
|
||||
SELECT id, data_type
|
||||
FROM template_mapping
|
||||
WHERE template_id = ?
|
||||
AND field_id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
|
||||
// Insert new field
|
||||
$insertStmt = $pdo->prepare("
|
||||
INSERT INTO template_mapping (
|
||||
template_id, schema_id, field_id, data_type, is_required, default_value,
|
||||
has_list, length, decimals, min_value, max_value, default_curr_date,
|
||||
tablename, field_label
|
||||
)
|
||||
SELECT :template_id, :schema_id, :field_id, :data_type, :is_required, :default_value,
|
||||
:has_list, :length, :decimals, :min_value, :max_value, :default_curr_date,
|
||||
:tablename, :field_label
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM template_mapping
|
||||
WHERE template_id = :template_id_check AND field_id = :field_id_check
|
||||
template_id,
|
||||
schema_id,
|
||||
field_id,
|
||||
field_order,
|
||||
data_type,
|
||||
is_required,
|
||||
default_value,
|
||||
has_list,
|
||||
length,
|
||||
decimals,
|
||||
min_value,
|
||||
max_value,
|
||||
default_curr_date,
|
||||
tablename,
|
||||
field_label
|
||||
) VALUES (
|
||||
:template_id,
|
||||
:schema_id,
|
||||
:field_id,
|
||||
:field_order,
|
||||
:data_type,
|
||||
:is_required,
|
||||
:default_value,
|
||||
:has_list,
|
||||
:length,
|
||||
:decimals,
|
||||
:min_value,
|
||||
:max_value,
|
||||
:default_curr_date,
|
||||
:tablename,
|
||||
:field_label
|
||||
)
|
||||
");
|
||||
|
||||
// Itera sui campi dello schema e inserisci quelli mancanti
|
||||
// Update existing field WITHOUT clearing manual_default
|
||||
$updateSameTypeStmt = $pdo->prepare("
|
||||
UPDATE template_mapping
|
||||
SET
|
||||
schema_id = :schema_id,
|
||||
field_order = :field_order,
|
||||
data_type = :data_type,
|
||||
is_required = :is_required,
|
||||
default_value = :default_value,
|
||||
has_list = :has_list,
|
||||
length = :length,
|
||||
decimals = :decimals,
|
||||
min_value = :min_value,
|
||||
max_value = :max_value,
|
||||
default_curr_date = :default_curr_date,
|
||||
tablename = :tablename,
|
||||
field_label = :field_label
|
||||
WHERE id = :id
|
||||
");
|
||||
|
||||
// Update existing field AND clear user-entered value if type changed
|
||||
$updateChangedTypeStmt = $pdo->prepare("
|
||||
UPDATE template_mapping
|
||||
SET
|
||||
schema_id = :schema_id,
|
||||
data_type = :data_type,
|
||||
is_required = :is_required,
|
||||
default_value = :default_value,
|
||||
has_list = :has_list,
|
||||
length = :length,
|
||||
decimals = :decimals,
|
||||
min_value = :min_value,
|
||||
max_value = :max_value,
|
||||
default_curr_date = :default_curr_date,
|
||||
tablename = :tablename,
|
||||
field_label = :field_label,
|
||||
|
||||
manual_default = NULL,
|
||||
auto_value = 'none'
|
||||
|
||||
WHERE id = :id
|
||||
");
|
||||
|
||||
$currentFieldIds = [];
|
||||
|
||||
/*
|
||||
* 2) Insert new fields and update existing fields
|
||||
*/
|
||||
foreach ($schema_fields as $field) {
|
||||
$custom_field = $field['CustomField'] ?? [];
|
||||
|
||||
if (empty($custom_field['IdCustomField'])) {
|
||||
continue; // Salta se manca l'ID del campo
|
||||
continue;
|
||||
}
|
||||
|
||||
$insert_stmt->execute([
|
||||
':template_id' => $template_id,
|
||||
':schema_id' => $schema_id,
|
||||
':field_id' => $custom_field['IdCustomField'],
|
||||
':data_type' => $custom_field['Tipo'] ?? 'Testo',
|
||||
':is_required' => $custom_field['ObbligatorioWeb'] ? 1 : 0,
|
||||
':default_value' => $custom_field['ValoreDefault'] ?? null,
|
||||
':has_list' => $custom_field['Elenco'] ? 1 : 0,
|
||||
':length' => $custom_field['Lunghezza'] ?? 0,
|
||||
':decimals' => $custom_field['Decimali'] ?? 0,
|
||||
':min_value' => $custom_field['Minimo'] ?? null,
|
||||
':max_value' => $custom_field['Massimo'] ?? null,
|
||||
':default_curr_date' => $custom_field['DefaultCurrDate'] ? 1 : 0,
|
||||
':tablename' => $target_table,
|
||||
':field_label' => $custom_field['TitoloTraduzione'] ?? '',
|
||||
':template_id_check' => $template_id,
|
||||
':field_id_check' => $custom_field['IdCustomField']
|
||||
]);
|
||||
$fieldId = (int)$custom_field['IdCustomField'];
|
||||
$currentFieldIds[] = $fieldId;
|
||||
|
||||
$newDataType = $custom_field['Tipo'] ?? 'Testo';
|
||||
|
||||
$data = [
|
||||
':schema_id' => $schema_id,
|
||||
':field_order' => (int)($field['Ordine'] ?? 9999),
|
||||
':data_type' => $newDataType,
|
||||
':is_required' => !empty($custom_field['ObbligatorioWeb']) ? 1 : 0,
|
||||
':default_value' => $custom_field['ValoreDefault'] ?? null,
|
||||
':has_list' => !empty($custom_field['Elenco']) ? 1 : 0,
|
||||
':length' => $custom_field['Lunghezza'] ?? 0,
|
||||
':decimals' => $custom_field['Decimali'] ?? 0,
|
||||
':min_value' => $custom_field['Minimo'] ?? null,
|
||||
':max_value' => $custom_field['Massimo'] ?? null,
|
||||
':default_curr_date' => !empty($custom_field['DefaultCurrDate']) ? 1 : 0,
|
||||
':tablename' => $target_table,
|
||||
':field_label' => $custom_field['TitoloTraduzione'] ?? ''
|
||||
];
|
||||
|
||||
$checkStmt->execute([$template_id, $fieldId]);
|
||||
$existing = $checkStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$existing) {
|
||||
// New field: insert it
|
||||
$insertData = $data;
|
||||
$insertData[':template_id'] = $template_id;
|
||||
$insertData[':field_id'] = $fieldId;
|
||||
|
||||
$insertStmt->execute($insertData);
|
||||
} else {
|
||||
// Existing field: update schema details
|
||||
$oldDataType = $existing['data_type'] ?? '';
|
||||
|
||||
$updateData = $data;
|
||||
$updateData[':id'] = (int)$existing['id'];
|
||||
|
||||
if ($oldDataType !== $newDataType) {
|
||||
// Type changed: clear manual value because it may no longer be valid
|
||||
$updateChangedTypeStmt->execute($updateData);
|
||||
} else {
|
||||
// Same type: keep manual_default, excel/json mapping, visibility, main field, etc.
|
||||
$updateSameTypeStmt->execute($updateData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 3) Remove fields no longer present in the schema
|
||||
*/
|
||||
$currentFieldIds = array_values(array_unique(array_filter($currentFieldIds)));
|
||||
|
||||
if (!empty($currentFieldIds)) {
|
||||
$placeholders = implode(',', array_fill(0, count($currentFieldIds), '?'));
|
||||
|
||||
$deleteParams = array_merge([$template_id], $currentFieldIds);
|
||||
|
||||
$deleteStmt = $pdo->prepare("
|
||||
DELETE FROM template_mapping
|
||||
WHERE template_id = ?
|
||||
AND field_id NOT IN ($placeholders)
|
||||
");
|
||||
$deleteStmt->execute($deleteParams);
|
||||
}
|
||||
|
||||
// Commit della transazione
|
||||
$pdo->commit();
|
||||
|
||||
$response["success"] = true;
|
||||
$response["message"] = "Schema JSON updated and template mappings created successfully!";
|
||||
$response["message"] = "Schema JSON updated, mappings synchronized, removed fields deleted, and changed fields updated successfully.";
|
||||
} catch (Exception $e) {
|
||||
if (isset($pdo) && $pdo->inTransaction()) {
|
||||
$pdo->rollback();
|
||||
}
|
||||
|
||||
$response["message"] = $e->getMessage();
|
||||
}
|
||||
|
||||
// Restituisce un JSON per il fetch
|
||||
echo json_encode($response);
|
||||
|
||||
@@ -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()
|
||||
]);
|
||||
}
|
||||
@@ -6,22 +6,57 @@ error_reporting(E_ALL);
|
||||
|
||||
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 {
|
||||
$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 = ?";
|
||||
$params = [$xlsHeaders];
|
||||
|
||||
@@ -29,11 +64,18 @@ try {
|
||||
$sql .= ", header_row = ?";
|
||||
$params[] = $headerRow;
|
||||
}
|
||||
|
||||
if ($startColumn !== null) {
|
||||
$sql .= ", start_column = ?";
|
||||
$params[] = $startColumn;
|
||||
}
|
||||
|
||||
if ($xlsSheetIndex !== null) {
|
||||
$sql .= ", xls_sheet_index = ?";
|
||||
$params[] = $xlsSheetIndex;
|
||||
}
|
||||
|
||||
$sql .= ", updated_at = NOW()";
|
||||
$sql .= " WHERE id = ?";
|
||||
$params[] = $templateId;
|
||||
|
||||
@@ -45,8 +87,18 @@ try {
|
||||
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) {
|
||||
echo json_encode(["success" => false, "message" => "Error: " . $e->getMessage()]);
|
||||
}
|
||||
|
||||
exit;
|
||||
|
||||
@@ -88,6 +88,58 @@ $validators[] = function (int $iddatadb, array $ctx): array {
|
||||
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 ────────────────────────────────────────────────────────────────────
|
||||
|
||||
try {
|
||||
@@ -104,9 +156,12 @@ try {
|
||||
$iddatadbList = array_column($rows, 'iddatadb');
|
||||
$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("
|
||||
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
|
||||
WHERE iddatadb IN ($placeholders)
|
||||
");
|
||||
@@ -128,6 +183,62 @@ try {
|
||||
$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
|
||||
$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
|
||||
");
|
||||
$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 ──────────────────────────────────────────────
|
||||
|
||||
$results = [];
|
||||
@@ -137,9 +248,15 @@ try {
|
||||
$index = $rowInfo['index'];
|
||||
|
||||
// Build context for validators
|
||||
$record = $recordsInfo[$iddatadb] ?? null;
|
||||
$templateId = (int)($record['templateid'] ?? 0);
|
||||
$ctx = [
|
||||
'record' => $recordsInfo[$iddatadb] ?? null,
|
||||
'parts' => $partsInfo[$iddatadb] ?? [],
|
||||
'record' => $record,
|
||||
'parts' => $partsInfo[$iddatadb] ?? [],
|
||||
'fixedAliasMap' => $fixedAliasMap,
|
||||
'requiredFixed' => $requiredFixedByTemplate[$templateId] ?? [],
|
||||
'requiredCustom' => $requiredCustomByTemplate[$templateId] ?? [],
|
||||
'customValues' => $customValuesByRecord[$iddatadb] ?? [],
|
||||
];
|
||||
|
||||
$errors = [];
|
||||
|
||||
Reference in New Issue
Block a user