Compare commits

...

39 Commits

Author SHA1 Message Date
RMubarakzyanov c3a6dd73b6 import backoff 2026-05-28 23:59:15 +03:00
solocla 44ed1186e0 added richmento pelletteria routine 2026-05-28 13:50:52 +02:00
solocla 9050cb1006 routine burberry 2026-05-26 12:15:45 +02:00
solocla e6820fdb62 added order column 2026-05-25 10:59:58 +02:00
solocla 5da37a7836 paulshark routine 2026-05-22 12:27:53 +02:00
solocla c5f27cb69a routine fendi 2026-05-21 10:11:53 +02:00
solocla 1d81d6c996 fixed import update 2026-05-20 18:47:41 +02:00
solocla 0c72dbf5ae sort first with exactly phrase 2026-05-20 17:01:07 +02:00
solocla 8455be04e1 remove hidden from xls import to avoid big file dimension 2026-05-20 16:47:18 +02:00
solocla e42d1b9c51 fix limit dropdown 2026-05-19 16:20:55 +02:00
solocla 3e69e3c322 fixed routine valentino 2026-05-19 11:23:26 +02:00
solocla 0eb4f7a2ad valentino routine 2026-05-19 11:12:18 +02:00
solocla 4f2cfc1930 fix render gird for doppi apici 2026-05-18 15:38:43 +02:00
solocla df075dd76a main field show in imported 2026-05-14 10:14:59 +02:00
solocla 6460454201 fixed main field 2026-05-14 10:00:47 +02:00
solocla 574ddbbd32 fixed propagation 2026-05-13 15:12:13 +02:00
solocla 41f414db5c 2 main 2026-05-13 14:58:13 +02:00
solocla 4a863e8c16 fixed double scroll bar parts dropdown 2026-05-13 14:26:11 +02:00
solocla b431f1d4e9 added top scrollbar imported 2026-05-13 14:22:43 +02:00
solocla f97b52f158 skip empty parts during clone and LIMS export 2026-05-13 11:54:28 +02:00
solocla 836fc055ec cliente reponsabile change based on client 2026-05-12 15:48:35 +02:00
solocla e8dd585df4 added arrows to jump parts 2026-05-12 09:37:52 +02:00
solocla 198b8c08ad Revert "update scheme different obbligatorioweb"
This reverts commit a3eb0f0a57.
2026-05-11 14:47:42 +02:00
solocla 28c467d55e fixed order import 2026-05-11 14:24:35 +02:00
solocla 56eee99a67 change template and import o use also sheet number of XLS 2026-05-09 15:39:43 +02:00
solocla f514b3d2c7 added sheet and config API in insert edit template 2026-05-09 09:44:52 +02:00
solocla a3eb0f0a57 update scheme different obbligatorioweb 2026-05-09 09:30:59 +02:00
solocla aa355905d7 update schema 2026-05-08 11:10:21 +02:00
solocla b38f3e1240 Stop tracking generated schema response file 2026-05-06 16:52:53 +02:00
solocla cbd0c5b68a annotation color and clone templates 2026-05-06 16:44:42 +02:00
solocla f3e5cb4ffd fixed capitolato riferimento 2026-05-05 09:37:18 +02:00
solocla 0f0c6a04b7 fixed Mix 2026-05-05 09:07:14 +02:00
solocla 813bd66f96 scroll table parts 2026-05-04 12:10:12 +02:00
solocla ce00247d1c edit templtae lingue e colori 2026-05-04 11:21:30 +02:00
solocla 3a7dd266c8 fixed quotation 2026-04-29 12:16:44 +02:00
solocla ba8dc4c721 added get 2026-04-29 08:38:16 +02:00
RMubarakzyanov cfbbc36116 Fix TZ Issue 2026-04-28 12:53:21 +03:00
solocla 50d578eea1 commesssa on commessaweb 2026-04-27 16:02:50 +02:00
solocla 6b0d2aa9b9 fixed color and import dahboard 2026-04-27 14:34:45 +02:00
58 changed files with 6435 additions and 941 deletions
+3
View File
@@ -40,10 +40,13 @@ 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
# File XLSX temporanei importati
/public/userarea/imported_trf/*.xlsx
+38 -26
View File
@@ -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
View File
@@ -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 {
@@ -255,4 +255,39 @@ class VisualLimsApiClient
{
return $this->baseUrl;
}
/**
* Recupera contenuto binario - Adattato per https://bvcpsitaly-elims.com/limsapi
*/
public function getRaw($endpoint)
{
$token = $this->getToken();
// IMPORTANTE: usa /odata/ e NON /api/odata/
$url = "{$this->baseUrl}/odata/{$endpoint}";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer {$token}",
"Accept: */*"
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_error($ch);
curl_close($ch);
if ($response === false) {
throw new Exception("Errore cURL: " . $curl_error);
}
if ($http_code !== 200) {
throw new Exception("HTTP {$http_code} su endpoint: " . $url);
}
return $response;
}
}
+3
View File
@@ -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;
+7 -5
View File
@@ -28,11 +28,13 @@ try {
// 1. Load source parts
$stmtParts = $pdo->prepare("
SELECT id, part_number, part_description, mix, idmatrice, note, dateexpiry
FROM identification_parts
WHERE iddatadb = ?
ORDER BY part_number ASC, id ASC
");
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);
+228
View File
@@ -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'];
+112
View File
@@ -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);
}
+273 -15
View File
@@ -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
+3
View File
@@ -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
+79 -13
View File
@@ -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);
@@ -510,9 +555,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 +564,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 +592,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,7 +623,7 @@ try {
echo json_encode([
"success" => true,
"idcommessaweb" => $commessaId,
"commessaweb" => $commessaWebCode,
"commessaweb" => $finalCodiceCommessa ?: $commessaWebCode,
"commessaWeb" => $finalCommessa,
"commessaWebApiResponse" => $commessaWeb, // Incluso per debug
"totalCampioni" => count($campioni),
+95
View File
@@ -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);
}
+88
View File
@@ -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,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);
}
+118
View File
@@ -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);
}
+125 -10
View File
@@ -2,25 +2,140 @@
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'] ?? '');
$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' => '',
'files' => 'RapportiFiles',
'allegati' => 'RapportiAllegati',
'campioni' => 'CampioniDatiRapporto',
'files_campioni' => 'RapportiFiles,CampioniDatiRapporto'
];
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);
/*
* 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);
file_put_contents(
__DIR__ . "/rapporto_codice_{$codiceRapportoSafe}_{$step}.json",
json_encode([
'search' => $searchData,
'detail' => $detailData
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
);
echo json_encode([
'success' => true,
'codice_rapporto' => $codiceRapporto,
'id_rapporto' => $rapportoId,
'step' => $step,
'search_endpoint' => $searchEndpoint,
'detail_endpoint' => $detailEndpoint,
'rapporto_base' => $rapportoBase,
'data' => $detailData
], 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);
}
+511 -62
View File
@@ -36,6 +36,15 @@
return d.innerHTML;
}
function escAttr(str) {
if (str === null || str === undefined) return "";
return String(str)
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
function getDetailValue(rowIndex, mappingId) {
return data[rowIndex].details[String(mappingId)] ?? "";
}
@@ -57,7 +66,20 @@
// ── Client data (AJAX Select2, no bulk loading) ──────────────────────
function formatClientLabel(client) {
return (client.Nominativo || "").trim();
const nome = client.Nominativo || client.text || "Nome non disponibile";
const id = client.IdCliente || client.id || "";
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})`;
}
// Cache of resolved client names: id → name
@@ -102,7 +124,9 @@
);
const json = await resp.json();
const item = (json.results || [])[0];
if (item) clientNameCache[id] = item.text;
if (item) {
clientNameCache[id] = formatClientLabel(item);
}
} catch (e) {
/* ignore */
}
@@ -110,12 +134,35 @@
);
}
function sortSelect2ResultsByStart(data) {
const term = $(".select2-container--open .select2-search__field").val();
if (!term) {
return data;
}
const search = term.toLowerCase().trim();
return data.sort(function (a, b) {
const textA = (a.text || "").toLowerCase().trim();
const textB = (b.text || "").toLowerCase().trim();
const aStarts = textA.startsWith(search);
const bStarts = textB.startsWith(search);
if (aStarts && !bStarts) return -1;
if (!aStarts && bStarts) return 1;
return textA.localeCompare(textB, "it", { sensitivity: "base" });
});
}
// Select2 AJAX config for client selects
const clientSelect2Config = {
placeholder: "Search client...",
allowClear: true,
width: "100%",
minimumInputLength: 0,
sorter: sortSelect2ResultsByStart,
dropdownCssClass: "select2-dropdown-smaller",
ajax: {
url: "search_clienti.php",
@@ -125,7 +172,12 @@
return { q: params.term || "", limit: 20 };
},
processResults: function (data) {
return { results: data.results || [] };
const results = (data.results || []).map((item) => ({
...item,
text: formatClientLabel(item),
}));
return { results: results };
},
cache: true,
},
@@ -222,7 +274,88 @@
return _pendingFixed[cacheKey];
}
async function refreshDependentFixedFieldsForRow(rowIndex) {
const row = data[rowIndex];
if (!row) return;
const clientId = row.idclient || "";
// Find fixed fields that depend on idclient
const dependentFields = Object.keys(fixedFieldApiConfig).filter(
(key) => {
return fixedFieldApiConfig[key].dependsOn === "idclient";
},
);
if (dependentFields.length === 0) return;
for (const fieldKey of dependentFields) {
// When client changes, the old responsible is no longer reliable
if (
row.fixedFields &&
Object.prototype.hasOwnProperty.call(row.fixedFields, fieldKey)
) {
row.fixedFields[fieldKey] = "";
row._dirty = true;
}
// Reload options for the new client
if (clientId) {
await loadFixedFieldOptions(fieldKey, clientId);
}
}
// Re-render only this row so ClienteResponsabile select is rebuilt with the new options
renderSingleRow(rowIndex);
// If the first row client changes, update the top propagation select too
if (rowIndex === 0) {
await refreshTopDependentFixedSelect(
"ClienteResponsabile",
clientId,
);
}
updateDirtyIndicator();
}
// ── Custom field dropdown data loading ─────────────────────────────────
async function refreshTopDependentFixedSelect(fieldKey, clientId) {
if (!topContainer || !fieldKey) return;
const sel = topContainer.querySelector(
`.api-fixed-select[data-fixed-key="${fieldKey}"]`,
);
if (!sel) return;
// Destroy Select2 if already initialized
if ($(sel).hasClass("select2-hidden-accessible")) {
$(sel).select2("destroy");
}
sel.innerHTML = '<option value="">Seleziona...</option>';
if (!clientId) {
$(sel).select2({
placeholder: "Seleziona...",
allowClear: true,
width: "100%",
});
return;
}
const items = await loadFixedFieldOptions(fieldKey, clientId);
items.forEach((item) => {
sel.add(new Option(item.text, item.id));
});
$(sel).select2({
placeholder: "Seleziona...",
allowClear: true,
width: "100%",
});
}
// Select2 AJAX config factory for SceltaMultipla
function sceltaSelect2Config(fieldId) {
@@ -231,6 +364,7 @@
allowClear: true,
width: "100%",
minimumInputLength: 0,
sorter: sortSelect2ResultsByStart,
ajax: {
url: "search_customfield_values.php",
dataType: "json",
@@ -239,7 +373,7 @@
return {
field_id: fieldId,
q: params.term || "",
limit: 10,
limit: 0, // 0 = no limit for custom field values
};
},
processResults: function (data) {
@@ -331,6 +465,28 @@
function createCell(col, rowIndex, cellIndex) {
const div = document.createElement("div");
div.className = "grid-cell editable-cell";
// Field color classification
// Schema/customfield fields are green.
// Fixed and standard fields stay white.
if (col.type === "detail" || col.type === "main_field") {
div.classList.add("schema-field");
} else if (col.type === "fixed") {
div.classList.add("fixed-field");
} else {
div.classList.add("standard-field");
}
// Required field classification.
// This comes from template_mapping.is_required or template_fixed_mapping.is_required.
if (
col.isRequired === true ||
col.isRequired === 1 ||
col.isRequired === "1"
) {
div.classList.add("required-field");
}
div.dataset.col = col.key;
div.dataset.colType = col.type;
div.dataset.row = rowIndex;
@@ -340,13 +496,13 @@
const row = data[rowIndex];
switch (col.type) {
case "main_field":
div.innerHTML = createInputHTML(
col,
row.mainFieldValue || "",
rowIndex,
);
case "main_field": {
const val = getDetailValue(rowIndex, col.key);
div.innerHTML = createInputHTML(col, val || "", rowIndex);
break;
}
case "status": {
const st = row.status || "i";
@@ -396,7 +552,7 @@
case "tested_component":
div.style.overflow = "visible";
div.innerHTML = `<div style="display:flex; align-items:center; gap:4px; width:100%; height:100%;">
<input type="text" class="cell-input manual-input tested-component-input" value="${esc(row.tested_component || "")}" style="flex:1; min-width:0; height:28px;">
<input type="text" class="cell-input manual-input tested-component-input" value="${escAttr(row.tested_component || "")}" style="flex:1; min-width:0; height:28px;">
<button type="button" class="add-part-btn btn btn-sm btn-primary" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" style="display:inline-flex; align-items:center; justify-content:center; min-width:28px; width:28px; height:28px; padding:0; font-size:12px; flex-shrink:0; text-align:center;">
<i class="fas fa-plus" style="margin:0; padding:0;"></i>
</button>
@@ -442,7 +598,7 @@
const cls = col.isManual ? "manual-input" : "auto-input";
const reqCls = col.isRequired ? " required-input" : "";
const req = col.isRequired ? " required" : "";
const v = esc(value);
const v = escAttr(value);
if (col.dataType === "SceltaMultipla") {
const options = buildDropdownOptionsHTML(col.fieldId, value);
@@ -467,7 +623,7 @@
if (col.dataType === "DATE") {
const reqCls = col.isRequired ? " required-input" : "";
const req = col.isRequired ? " required" : "";
return `<input type="text" class="cell-input date-picker manual-input${reqCls} fixed-input" data-fixed-key="${col.key}" value="${esc(value)}"${req}>`;
return `<input type="text" class="cell-input date-picker manual-input${reqCls} fixed-input" data-fixed-key="${escAttr(col.key)}" value="${escAttr(value)}"${req}>`;
}
// Client-sourced fields → AJAX Select2 (like idclient)
@@ -480,7 +636,7 @@
const label = clientNameCache[value] || value;
opts += `<option value="${esc(String(value))}" selected>${esc(String(label))}</option>`;
}
return `<select class="cell-input manual-input fixed-input searchable-client api-fixed-select${reqCls}" data-fixed-key="${col.key}" data-current-value="${esc(value)}"${req}>${opts}</select>`;
return `<select class="cell-input manual-input fixed-input searchable-client api-fixed-select${reqCls}" data-fixed-key="${escAttr(col.key)}" data-current-value="${escAttr(value)}"${req}>${opts}</select>`;
}
// Select — build from cache
@@ -498,7 +654,7 @@
const reqCls = col.isRequired ? " required-input" : "";
const req = col.isRequired ? " required" : "";
return `<select class="cell-input manual-input fixed-input ${selectClass}${reqCls}" data-fixed-key="${col.key}" data-current-value="${esc(value)}"${req}>${options}</select>`;
return `<select class="cell-input manual-input fixed-input ${selectClass}${reqCls}" data-fixed-key="${escAttr(col.key)}" data-current-value="${escAttr(value)}"${req}>${options}</select>`;
}
function buildDropdownOptionsHTML(fieldId, selectedValue) {
@@ -703,6 +859,143 @@
flatpickr(this, { dateFormat: "Y-m-d", allowInput: true });
});
}
function getInputTextWidth(input) {
const span = document.createElement("span");
const style = window.getComputedStyle(input);
span.style.position = "absolute";
span.style.visibility = "hidden";
span.style.whiteSpace = "pre";
span.style.font = style.font;
span.style.fontSize = style.fontSize;
span.style.fontFamily = style.fontFamily;
span.style.fontWeight = style.fontWeight;
span.textContent = input.value || input.placeholder || "";
document.body.appendChild(span);
const width = span.offsetWidth + 60;
document.body.removeChild(span);
return width;
}
function autoExpandColumnFromInput(input) {
if (!input) return;
const cell = input.closest(".grid-cell");
if (!cell || !cell.dataset.index) return;
const columnIndex = parseInt(cell.dataset.index, 10);
if (!columnIndex) return;
const wantedWidth = Math.max(120, getInputTextWidth(input));
const currentWidth = cell.offsetWidth || 0;
// Only expand, do not shrink automatically
if (wantedWidth <= currentWidth) return;
const newWidth = Math.min(wantedWidth, 900);
const header = document.querySelector(
`.grid-header[data-index="${columnIndex}"]`,
);
if (header) {
header.style.flex = `0 0 ${newWidth}px`;
}
const topCell = document.querySelector(
`.grid-top .grid-cell:nth-child(${columnIndex + 1})`,
);
if (topCell) {
topCell.style.flex = `0 0 ${newWidth}px`;
}
const cells = document.querySelectorAll(
`.grid-row .grid-cell[data-index="${columnIndex}"]`,
);
cells.forEach((c) => {
c.style.flex = `0 0 ${newWidth}px`;
});
const colPos = columnIndex - 1;
if (columns[colPos]) {
columns[colPos].width = newWidth;
}
}
function syncVisibleRowsToGridData() {
if (!rowContainer) return;
rowContainer
.querySelectorAll(".grid-cell[data-row]")
.forEach((cell) => {
const rowIndex = parseInt(cell.dataset.row, 10);
const row = data[rowIndex];
if (!row) return;
const colType = cell.dataset.colType;
const colKey = cell.dataset.col;
const input = cell.querySelector(".cell-input");
if (!input) return;
const value = $(input).hasClass("select2-hidden-accessible")
? $(input).val() || ""
: input.value || "";
if (colType === "detail" || colType === "main_field") {
if (!row.details) row.details = {};
if (
String(row.details[String(colKey)] ?? "") !==
String(value)
) {
row.details[String(colKey)] = value;
if (colType === "main_field") {
row.mainFieldValue = value;
}
row._dirty = true;
}
} else if (colType === "fixed") {
if (!row.fixedFields) row.fixedFields = {};
if (
String(row.fixedFields[colKey] ?? "") !== String(value)
) {
row.fixedFields[colKey] = value;
row._dirty = true;
}
} else if (colType === "idclient") {
if (String(row.idclient ?? "") !== String(value)) {
row.idclient = value;
row._dirty = true;
}
} else if (colType === "cliente_fornitore_id") {
if (
String(row.cliente_fornitore_id ?? "") !== String(value)
) {
row.cliente_fornitore_id = value;
row._dirty = true;
}
} else if (colType === "tested_component") {
if (String(row.tested_component ?? "") !== String(value)) {
row.tested_component = value;
row._dirty = true;
}
}
});
updateDirtyIndicator();
}
// ── Headers & Propagate row ────────────────────────────────────────────
@@ -744,6 +1037,27 @@
columns.forEach((col, colIdx) => {
const cell = document.createElement("div");
cell.className = "grid-cell grid-top-cell";
// Field color classification for top propagation row
// Schema/customfield fields are green.
// Fixed and standard fields stay white.
if (col.type === "detail" || col.type === "main_field") {
cell.classList.add("schema-field");
} else if (col.type === "fixed") {
cell.classList.add("fixed-field");
} else {
cell.classList.add("standard-field");
}
// Required field classification also for the top propagation row.
if (
col.isRequired === true ||
col.isRequired === 1 ||
col.isRequired === "1"
) {
cell.classList.add("required-field");
}
cell.style.flex = `0 0 ${col.width}px`;
if (
@@ -823,23 +1137,28 @@
}
if (config && config.dependsOn) {
// For dependent fields: merge all cached values across all clientIds
const allItems = new Map();
for (const [key, items] of Object.entries(fixedFieldCache)) {
if (key.startsWith(fieldKey + "_")) {
items.forEach((item) =>
allItems.set(String(item.id), item),
);
}
}
// Dependent fixed fields, for example ClienteResponsabile:
// use the first row client, not all cached clients.
const firstClientId =
data[0]?.idclient || meta.defaultIdclient || "";
sel.innerHTML = '<option value="">Seleziona...</option>';
[...allItems.values()]
.sort((a, b) =>
String(a.text).localeCompare(String(b.text), "it", {
sensitivity: "base",
}),
)
.forEach((item) => sel.add(new Option(item.text, item.id)));
if (firstClientId) {
const items =
fixedFieldCache[fieldKey + "_" + firstClientId] || [];
items.forEach((item) => {
sel.add(new Option(item.text, item.id));
});
}
$(sel).select2({
placeholder: "Seleziona...",
allowClear: true,
width: "100%",
sorter: sortSelect2ResultsByStart,
});
} else {
const items = fixedFieldCache[fieldKey] || [];
sel.innerHTML = '<option value="">Seleziona...</option>';
@@ -887,6 +1206,8 @@
} else if (colType === "idclient") {
data[rowIndex].idclient = value;
data[rowIndex]._dirty = true;
refreshDependentFixedFieldsForRow(rowIndex);
} else if (colType === "cliente_fornitore_id") {
data[rowIndex].cliente_fornitore_id = value;
data[rowIndex]._dirty = true;
@@ -909,6 +1230,10 @@
const cell = e.target.closest(".grid-cell");
if (!cell || !cell.dataset.row) return;
if (e.target.classList.contains("cell-input")) {
autoExpandColumnFromInput(e.target);
}
const rowIndex = parseInt(cell.dataset.row, 10);
const colType = cell.dataset.colType;
@@ -920,6 +1245,16 @@
}
});
rowContainer.addEventListener("focusin", function (e) {
if (!e.target.classList.contains("cell-input")) return;
autoExpandColumnFromInput(e.target);
setTimeout(() => {
autoExpandColumnFromInput(e.target);
}, 50);
});
// Persist tested_component before clicking +
document.addEventListener("mousedown", function (e) {
const btn = e.target.closest(".add-part-btn");
@@ -948,62 +1283,149 @@
const btn = e.target.closest(".propagate-btn");
if (!btn) return;
const colIndex = parseInt(btn.dataset.colIndex);
const column = btn.dataset.column;
if (isNaN(colIndex) && !column) return;
// Before propagating and re-rendering, persist current visible row values into gridData.
syncVisibleRowsToGridData();
// Get value from the input/select in the same cell
const cell =
btn.closest(".grid-cell") || btn.closest(".grid-top-cell");
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const column = btn.dataset.column || "";
const colIndex = Number.isNaN(parseInt(btn.dataset.colIndex, 10))
? null
: parseInt(btn.dataset.colIndex, 10);
if (!column && colIndex === null) return;
// IMPORTANT:
// Read ONLY the input/select inside the same top propagation cell.
// Do not scan other top fields.
const cell = btn.closest(".grid-top-cell");
if (!cell) return;
const input = cell.querySelector("select, input");
if (!input) return;
const value = $(input).hasClass("select2-hidden-accessible")
? $(input).val()
: input.value;
// Cache Select2 label so re-render shows name not ID
const input = cell.querySelector(".custom-field");
if (!input) return;
const value = $(input).hasClass("select2-hidden-accessible")
? $(input).val() || ""
: input.value || "";
// Do not propagate empty dropdown values.
// This prevents wiping rows when a top Select2 is empty/not fully initialized.
if (input.tagName === "SELECT" && value === "") {
console.warn(
"[gridRenderer] Empty select propagation blocked:",
column,
);
return;
}
// Cache selected label so re-render can show text instead of only ID.
if (value && $(input).hasClass("select2-hidden-accessible")) {
const label = $(input).find("option:selected").text();
if (label && label !== value) {
clientNameCache[value] = label;
// Also cache for SceltaMultipla
if (
column === "idclient" ||
column === "cliente_fornitore_id" ||
input.classList.contains("searchable-client")
) {
clientNameCache[value] = label;
}
const fieldId = input.dataset?.fieldId;
if (fieldId)
if (fieldId) {
dropdownNameCache[fieldId + "_" + value] = label;
}
}
}
const col = columns[colIndex] || null;
const col = colIndex !== null ? columns[colIndex] : null;
console.log("[gridRenderer] Propagating ONLY:", {
column: column,
colIndex: colIndex,
value: value,
label:
input.tagName === "SELECT"
? $(input).find("option:selected").text()
: value,
});
if (column === "idclient") {
data.forEach((row) => {
const oldClientId = row.idclient || "";
row.idclient = value;
// Clear ClienteResponsabile only if client really changed.
if (
String(oldClientId) !== String(value) &&
row.fixedFields &&
Object.prototype.hasOwnProperty.call(
row.fixedFields,
"ClienteResponsabile",
)
) {
row.fixedFields["ClienteResponsabile"] = "";
}
row._dirty = true;
});
} else if (column === "cliente_fornitore_id") {
loadFixedFieldOptions("ClienteResponsabile", value).then(() => {
refreshTopDependentFixedSelect(
"ClienteResponsabile",
value,
);
renderVisibleRows();
updateDirtyIndicator();
});
return;
}
if (column === "cliente_fornitore_id") {
data.forEach((row) => {
row.cliente_fornitore_id = value;
row._dirty = true;
});
} else if (column && column.startsWith("fixed_")) {
renderVisibleRows();
updateDirtyIndicator();
return;
}
if (column && column.startsWith("fixed_")) {
const fixedKey = column.replace("fixed_", "");
data.forEach((row) => {
if (!row.fixedFields) row.fixedFields = {};
row.fixedFields[fixedKey] = value;
row._dirty = true;
});
} else if (col) {
if (col.type === "detail" || col.type === "main_field") {
data.forEach((row) => {
row.details[col.key] = value;
if (col.type === "main_field")
row.mainFieldValue = value;
row._dirty = true;
});
}
renderVisibleRows();
updateDirtyIndicator();
return;
}
renderVisibleRows();
if (col && (col.type === "detail" || col.type === "main_field")) {
data.forEach((row) => {
if (!row.details) row.details = {};
row.details[col.key] = value;
if (col.type === "main_field") {
row.mainFieldValue = value;
}
row._dirty = true;
});
renderVisibleRows();
updateDirtyIndicator();
return;
}
});
// Select2 change events (don't bubble via native addEventListener)
@@ -1023,6 +1445,8 @@
if (colType === "idclient") {
data[rowIndex].idclient = value;
data[rowIndex]._dirty = true;
refreshDependentFixedFieldsForRow(rowIndex);
} else if (colType === "cliente_fornitore_id") {
data[rowIndex].cliente_fornitore_id = value;
data[rowIndex]._dirty = true;
@@ -1031,14 +1455,39 @@
updateDirtyIndicator();
});
// Cache labels on SceltaMultipla change
// Handle SceltaMultipla changes and persist them into gridData.
// Without this, a later renderVisibleRows() can rebuild the row with the old empty value.
$(rowContainer).on("change", ".searchable-dropdown", function () {
const val = $(this).val();
const cell = this.closest(".grid-cell");
if (!cell || !cell.dataset.row) return;
const rowIndex = parseInt(cell.dataset.row, 10);
const colType = cell.dataset.colType;
const colKey = cell.dataset.col;
const val = $(this).val() || "";
const fieldId = this.dataset.fieldId;
// Cache label so re-render shows the text instead of only the ID.
if (val && fieldId) {
const label = $(this).find("option:selected").text();
if (label && label !== val)
if (label && label !== val) {
dropdownNameCache[fieldId + "_" + val] = label;
}
}
// Persist value into gridData.
if (colType === "detail" || colType === "main_field") {
if (!data[rowIndex].details) data[rowIndex].details = {};
data[rowIndex].details[String(colKey)] = val;
if (colType === "main_field") {
data[rowIndex].mainFieldValue = val;
}
data[rowIndex]._dirty = true;
cell.classList.add("cell-changed");
updateDirtyIndicator();
}
});
+252 -61
View File
@@ -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,137 @@
<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");
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>
-4
View File
@@ -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;
+47 -5
View File
@@ -167,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>
@@ -244,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',
@@ -331,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 -->
@@ -383,6 +389,42 @@ 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');
+219 -58
View File
@@ -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"));
@@ -59,15 +55,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 +98,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);
@@ -228,11 +231,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,16 +252,16 @@ foreach ($importedData as $index => $row) {
// Build columns in display order
$gridColumns = [];
// 1. Main field
if ($mainFieldMapping) {
// 1. Main fields, maximum 2
foreach ($mainFieldMappings as $mainMapping) {
$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,
'key' => (string)$mainMapping['id'],
'label' => $mainMapping['field_label'],
'dataType' => $mainMapping['data_type'],
'isManual' => (bool)$mainMapping['is_manual'],
'isRequired' => (bool)$mainMapping['is_required'],
'fieldId' => $mainMapping['field_id'] ?? null,
'width' => 150,
];
}
@@ -346,7 +356,8 @@ $gridMeta = [
'slugMapping' => $slugMapping,
'timeLabels' => $timeLabels,
'columns' => $gridColumns,
'mainFieldMapping' => $mainFieldMapping,
'mainFieldMapping' => $mainFieldMapping,
'mainFieldMappings' => $mainFieldMappings,
'totalRows' => count($gridDataArray),
];
@@ -354,6 +365,9 @@ $gridMeta = [
<script>
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>
@@ -374,20 +388,10 @@ $gridMeta = [
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 {
@@ -426,17 +430,9 @@ $gridMeta = [
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;
@@ -688,7 +684,33 @@ $gridMeta = [
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;
}
@@ -1044,11 +1066,7 @@ $gridMeta = [
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 {
@@ -1161,6 +1179,128 @@ $gridMeta = [
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>
@@ -1311,6 +1451,9 @@ $gridMeta = [
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;
@@ -1318,14 +1461,22 @@ $gridMeta = [
let syncingFromGrid = false;
function updateTopScrollbarWidth() {
topScrollbarInner.style.width = gridContainer.scrollWidth + 'px';
const realWidth = Math.max(
gridContainer.scrollWidth,
gridHeaderContainer ? gridHeaderContainer.scrollWidth : 0,
gridTopContainer ? gridTopContainer.scrollWidth : 0,
gridRowContainer ? gridRowContainer.scrollWidth : 0
);
// Mostra la barra solo se serve davvero
if (gridContainer.scrollWidth > gridContainer.clientWidth) {
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() {
@@ -1342,14 +1493,24 @@ $gridMeta = [
syncingFromGrid = false;
});
updateTopScrollbarWidth();
window.addEventListener('resize', updateTopScrollbarWidth);
// Ritarda un attimo per sicurezza, visto che la griglia viene renderizzata via JS
setTimeout(updateTopScrollbarWidth, 200);
setTimeout(updateTopScrollbarWidth, 600);
setTimeout(updateTopScrollbarWidth, 1200);
// 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>
+3
View File
@@ -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.
+142 -14
View File
@@ -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];
+4 -1
View File
@@ -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;
+5 -5
View File
@@ -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
+10
View File
@@ -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>
+279 -58
View File
@@ -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,8 +37,7 @@
</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
@@ -32,6 +48,10 @@
<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 -->
@@ -44,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>
@@ -290,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;
@@ -342,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;
}
@@ -516,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>
+511 -35
View File
@@ -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(
@@ -1883,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,
@@ -1964,6 +2300,8 @@ $(document).ready(function () {
});
function markUnsaved() {
if (isLoadingPartsRecord) return;
if (!unsavedChanges) {
unsavedChanges = true;
}
@@ -2238,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;
@@ -2349,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;
@@ -2463,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;
})();
+36
View File
@@ -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);
}
+56 -4
View File
@@ -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,
+3
View File
@@ -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 {
+190 -31
View File
@@ -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,
File diff suppressed because it is too large Load Diff
+71
View File
@@ -0,0 +1,71 @@
<?php
/**
* Routine: burberry
*
* Purpose:
* For each imported XLS row:
* - read the value from column S
* - read the value from column T
* - merge the values
* - save the final value into column S
*
* Target:
* Column S must be mapped to the destination field in the template mapping.
*/
function applyRoutine(&$excelData, $routineData = [])
{
/*
* This routine does not require external routine data.
* Columns are fixed.
*
* Excel column indexes are zero-based:
*
* S = 18
* T = 19
*/
$targetColumnIndex = 18; // S
$columnSIndex = 18; // S
$columnTIndex = 19; // T
foreach ($excelData as $rowIndex => &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Routine burberry: invalid row structure at index {$rowIndex}.");
continue;
}
$valueS = trim((string)($row['data'][$columnSIndex] ?? ''));
$valueT = trim((string)($row['data'][$columnTIndex] ?? ''));
/*
* Merge values, ignoring empty values.
*/
$mergedValues = [];
if ($valueS !== '') {
$mergedValues[] = $valueS;
}
if ($valueT !== '') {
$mergedValues[] = $valueT;
}
/*
* Save final value into column S.
*/
$row['data'][$targetColumnIndex] = implode(' ', $mergedValues);
error_log(
"Routine burberry: row " .
($row['excelrow'] ?? $rowIndex) .
" generated value in column S: " .
$row['data'][$targetColumnIndex]
);
}
unset($row);
error_log("Routine burberry completed.");
}
+67
View File
@@ -0,0 +1,67 @@
<?php
/**
* Routine: merge_column_T_and_U_into_T
*
* Purpose:
* For each imported XLS row:
* - read the value from column T
* - read the value from column U
* - merge both values
* - save the final value into column T
*
* Target:
* Column T must be mapped to the destination field in the template mapping.
*/
function applyRoutine(&$excelData, $routineData = [])
{
/*
* Excel column indexes are zero-based:
*
* T = 19
* U = 20
*/
$targetColumnIndex = 19; // T
$firstColumnIndex = 19; // T
$secondColumnIndex = 20; // U
foreach ($excelData as $rowIndex => &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Routine merge T+U: invalid row structure at index {$rowIndex}.");
continue;
}
$valueT = trim((string)($row['data'][$firstColumnIndex] ?? ''));
$valueU = trim((string)($row['data'][$secondColumnIndex] ?? ''));
/*
* Merge values, ignoring empty values.
*/
$mergedValues = [];
if ($valueT !== '') {
$mergedValues[] = $valueT;
}
if ($valueU !== '') {
$mergedValues[] = $valueU;
}
/*
* Save final value into column T.
*/
$row['data'][$targetColumnIndex] = implode(' ', $mergedValues);
error_log(
"Routine merge T+U: row " .
($row['excelrow'] ?? $rowIndex) .
" generated value in column T: " .
$row['data'][$targetColumnIndex]
);
}
unset($row);
error_log("Routine merge T+U completed.");
}
+76
View File
@@ -0,0 +1,76 @@
<?php
/**
* Routine: paulshark
*
* Purpose:
* For each imported XLS row:
* - read the value from column D
* - read the value from column E
* - read the value from column J
* - merge the values
* - save the final value into column D
*
* Target:
* Column D must be mapped to the destination field in the template mapping.
*/
function applyRoutine(&$excelData, $routineData = [])
{
/*
* Excel column indexes are zero-based:
*
* D = 3
* E = 4
* J = 9
*/
$targetColumnIndex = 3; // D
$columnDIndex = 3; // D
$columnEIndex = 4; // E
$columnJIndex = 9; // J
foreach ($excelData as $rowIndex => &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Routine paulshark: invalid row structure at index {$rowIndex}.");
continue;
}
$valueD = trim((string)($row['data'][$columnDIndex] ?? ''));
$valueE = trim((string)($row['data'][$columnEIndex] ?? ''));
$valueJ = trim((string)($row['data'][$columnJIndex] ?? ''));
/*
* Merge values, ignoring empty values.
*/
$mergedValues = [];
if ($valueD !== '') {
$mergedValues[] = $valueD;
}
if ($valueE !== '') {
$mergedValues[] = $valueE;
}
if ($valueJ !== '') {
$mergedValues[] = $valueJ;
}
/*
* Save final value into column D.
*/
$row['data'][$targetColumnIndex] = implode(' ', $mergedValues);
error_log(
"Routine paulshark: row " .
($row['excelrow'] ?? $rowIndex) .
" generated value in column D: " .
$row['data'][$targetColumnIndex]
);
}
unset($row);
error_log("Routine paulshark completed.");
}
@@ -0,0 +1,71 @@
<?php
/**
* Routine: Richemont Pelletteria
*
* Purpose:
* For each imported XLS row:
* - read the value from column D
* - read the value from column E
* - merge the values
* - save the final value into column D
*
* Target:
* Column D must be mapped to the destination field in the template mapping.
*/
function applyRoutine(&$excelData, $routineData = [])
{
/*
* This routine does not require external routine data.
* Columns are fixed.
*
* Excel column indexes are zero-based:
*
* D = 3
* E = 4
*/
$targetColumnIndex = 3; // D
$columnDIndex = 3; // D
$columnEIndex = 4; // E
foreach ($excelData as $rowIndex => &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Routine Richemont Pelletteria: invalid row structure at index {$rowIndex}.");
continue;
}
$valueD = trim((string)($row['data'][$columnDIndex] ?? ''));
$valueE = trim((string)($row['data'][$columnEIndex] ?? ''));
/*
* Merge values, ignoring empty values.
*/
$mergedValues = [];
if ($valueD !== '') {
$mergedValues[] = $valueD;
}
if ($valueE !== '') {
$mergedValues[] = $valueE;
}
/*
* Save final value into column D.
*/
$row['data'][$targetColumnIndex] = implode(' ', $mergedValues);
error_log(
"Routine Richemont Pelletteria: row " .
($row['excelrow'] ?? $rowIndex) .
" generated value in column D: " .
$row['data'][$targetColumnIndex]
);
}
unset($row);
error_log("Routine Richemont Pelletteria completed.");
}
@@ -0,0 +1,114 @@
<?php
/**
* Routine: build_field_347_from_x_columns
*
* Purpose:
* For each imported XLS row:
* - check columns P to AT
* - when a cell contains "x", take the related column title from row 6
* - append the free text value from column AU
* - save the final comma-separated text into column P
*
* Target:
* Column P must be mapped to field_id 347.
*/
function applyRoutine(&$excelData, $routineData = [])
{
/*
* Excel column indexes are zero-based:
*
* P = 15
* AT = 45
* AU = 46
*/
$targetColumnIndex = 15; // P
$startColumnIndex = 15; // P
$endColumnIndex = 45; // AT
$extraColumnIndex = 46; // AU
/*
* Headers must come from XLS row 6.
* Usually they are passed inside $routineData['xls_headers'].
*/
$headers = $routineData['xls_headers'] ?? [];
if (empty($headers) || !is_array($headers)) {
error_log("Routine field_id 347: missing XLS headers from row 6.");
return;
}
foreach ($excelData as $rowIndex => &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Routine field_id 347: invalid row structure at index {$rowIndex}.");
continue;
}
$selectedValues = [];
/*
* Check columns from P to AT.
* If the cell contains x, take the related column header.
*/
for ($columnIndex = $startColumnIndex; $columnIndex <= $endColumnIndex; $columnIndex++) {
$cellValue = strtolower(trim((string)($row['data'][$columnIndex] ?? '')));
if ($cellValue === 'x') {
$headerTitle = trim((string)($headers[$columnIndex] ?? ''));
if ($headerTitle !== '') {
$selectedValues[] = $headerTitle;
}
}
}
/*
* Add free text from column AU.
*/
$extraText = '';
if (isset($row['data'][$extraColumnIndex])) {
$extraText = trim((string)$row['data'][$extraColumnIndex]);
} elseif (isset($row['data']['AU'])) {
$extraText = trim((string)$row['data']['AU']);
}
error_log(
"Routine field_id 347: row " .
($row['excelrow'] ?? $rowIndex) .
" AU index {$extraColumnIndex} value: " .
print_r($row['data'][$extraColumnIndex] ?? null, true) .
" | AU key value: " .
print_r($row['data']['AU'] ?? null, true)
);
if ($extraText !== '') {
$selectedValues[] = $extraText;
}
/*
* Remove empty and duplicate values.
*/
$selectedValues = array_values(array_unique(array_filter($selectedValues, function ($value) {
return trim((string)$value) !== '';
})));
/*
* Save final value into column P.
* Column P must be mapped to field_id 347 in the template mapping.
*/
$row['data'][$targetColumnIndex] = implode(', ', $selectedValues);
error_log(
"Routine field_id 347: row " .
($row['excelrow'] ?? $rowIndex) .
" generated value: " .
$row['data'][$targetColumnIndex]
);
}
unset($row);
error_log("Routine field_id 347 completed.");
}
+42 -14
View File
@@ -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
+1 -1
View File
@@ -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,
+63 -7
View File
@@ -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;
}
}
+73
View File
@@ -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);
}
+57 -12
View File
@@ -181,6 +181,20 @@
<script>
$(document).ready(function() {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cloned') === '1') {
Swal.fire({
title: "Template cloned",
text: "The template was cloned successfully.",
icon: "success",
confirmButtonText: "OK"
});
const cleanUrl = window.location.pathname;
window.history.replaceState({}, document.title, cleanUrl);
}
$('#xlsTemplatesTable').DataTable({
processing: true,
serverSide: false,
@@ -195,18 +209,24 @@
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>
`;
<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-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>
`;
}
},
{
@@ -318,6 +338,31 @@
});
}
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";
+3
View File
@@ -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 {
+117
View File
@@ -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);
}
-4
View File
@@ -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"));
+23
View File
@@ -0,0 +1,23 @@
<?php
require_once "class/VisualLimsApiClient.class.php";
include('include/headscript.php');
header("Content-Type: application/json; charset=utf-8");
try {
$api = VisualLimsApiClient::getInstance();
$commessaId = 577818;
$endpoint = "CommessaWeb({$commessaId})?\$expand=CommesseCustomFields(\$expand=CustomField)";
$result = $api->get($endpoint);
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
"success" => false,
"error" => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
@@ -0,0 +1,107 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
require_once dirname(__FILE__) . '/include/headscript.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$api = VisualLimsApiClient::getInstance();
// Get all schemas currently used in template_mapping
$stmtSchemas = $pdo->query("
SELECT DISTINCT schema_id
FROM template_mapping
WHERE schema_id IS NOT NULL
AND schema_id > 0
ORDER BY schema_id ASC
");
$schemaIds = $stmtSchemas->fetchAll(PDO::FETCH_COLUMN);
if (empty($schemaIds)) {
throw new Exception('No schema_id found in template_mapping');
}
$stmtUpdate = $pdo->prepare("
UPDATE template_mapping
SET field_order = ?
WHERE schema_id = ?
AND field_id = ?
");
$summary = [];
$totalUpdated = 0;
foreach ($schemaIds as $schemaId) {
$schemaId = (int)$schemaId;
$endpoint = "SchemaCustomField($schemaId)?\$expand=SchemiCustomFieldsDettagli(\$expand=CustomField)";
$data = $api->get($endpoint);
if (empty($data['SchemiCustomFieldsDettagli']) || !is_array($data['SchemiCustomFieldsDettagli'])) {
$summary[] = [
'schema_id' => $schemaId,
'success' => false,
'message' => 'No SchemiCustomFieldsDettagli found'
];
continue;
}
$schemaUpdated = 0;
$notFound = [];
foreach ($data['SchemiCustomFieldsDettagli'] as $detail) {
$order = intval($detail['Ordine'] ?? 9999);
$fieldId = intval($detail['CustomField']['IdCustomField'] ?? 0);
if ($fieldId <= 0) {
continue;
}
$stmtUpdate->execute([
$order,
$schemaId,
$fieldId
]);
if ($stmtUpdate->rowCount() > 0) {
$schemaUpdated++;
$totalUpdated++;
} else {
$notFound[] = [
'field_id' => $fieldId,
'order' => $order,
'label' => $detail['CustomField']['Titolo'] ?? ''
];
}
}
$summary[] = [
'schema_id' => $schemaId,
'success' => true,
'updated' => $schemaUpdated,
'not_found_count' => count($notFound),
'not_found' => $notFound
];
}
echo json_encode([
'success' => true,
'schemas_processed' => count($schemaIds),
'total_updated' => $totalUpdated,
'summary' => $summary
], JSON_PRETTY_PRINT);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => $e->getMessage()
], JSON_PRETTY_PRINT);
}
+63
View File
@@ -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()
]);
}
+46 -14
View File
@@ -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()
]);
}
+180 -54
View File
@@ -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,235 @@ 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,
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,
: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,
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,
':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);
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()
]);
}
+68 -16
View File
@@ -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;