Compare commits

...
Author SHA1 Message Date
RMubarakzyanov 1249ad3079 fix SceltaMultipla mapping 2026-10-26 19:45:10 +03:00
solocla 48ee511166 fixed filter order 2026-10-26 16:46:45 +01:00
RMubarakzyanov 6718e08d1f Merge branch 'main' into feature/xls-mapping 2026-09-23 16:03:43 +03:00
RMubarakzyanov b8b01cd5d5 xls binding 2026-09-23 15:50:03 +03:00
solocla f31ba24008 email send afetr export routine 2026-09-23 11:40:57 +02:00
solocla bb2ebcc122 fixed export to lims 2026-09-22 15:27:35 +02:00
solocla 955c760756 Stop tracking runtime files (error_log, schemi_base_response, logs, uploads) 2026-09-22 14:08:27 +02:00
solocla 5c12d97d0e added error page and logs linked 2026-09-22 13:58:14 +02:00
solocla 9bed740223 added log step 9 2026-09-22 12:13:02 +02:00
solocla 8be7ea0b85 fixed2 clientefatturazione 2026-09-18 07:59:49 +02:00
solocla dc0209dcf1 removed clientefatturazione on export 2026-09-16 10:12:58 +02:00
solocla 9cce361b90 added clientefatturazione 2026-09-16 09:22:08 +02:00
solocla c966096060 analysis in alphabetical order 2026-09-16 08:20:26 +02:00
solocla 55da10cea4 fixed routine error 2026-09-14 16:58:27 +02:00
solocla 0d3ed2505f fixed note with slash 2026-09-08 10:29:45 +02:00
solocla def7377447 fixed sticky bars with toggle 2026-09-08 09:18:17 +02:00
solocla 9453e7a01a sticky action button on topbar 2026-09-08 08:48:26 +02:00
solocla 8f4ac3d53e added routine engine 2026-09-05 13:41:26 +02:00
solocla 34f7490e94 session keep alive 2026-09-05 13:41:26 +02:00
solocla d9d6a7ab58 cambiato ttl warm cache 2026-09-05 13:41:26 +02:00
83 changed files with 7173 additions and 1808 deletions
+10
View File
@@ -62,3 +62,13 @@ public/userarea/schemi_base_response.json
public/userarea/cache/
public/userarea/error_log.txt
# PDF/QR generati dai template (userarea)
/public/userarea/pdftemplates/
# Runtime / rigenerati - non tracciare
public/userarea/error_log.txt
public/userarea/schemi_base_response.json
public/userarea/logs/
public/userarea/imported_trf/
public/userarea/xlstemplates/
.well-known/
+1 -1
View File
@@ -41,7 +41,7 @@
"laravel/socialite": "^5.16",
"laravel/tinker": "^2.7",
"laravel/ui": "^4.0",
"phpmailer/phpmailer": "^6.9",
"phpmailer/phpmailer": "^7.1",
"phpoffice/phpspreadsheet": "^4.1",
"proengsoft/laravel-jsvalidation": "^4.0.0",
"robmorgan/phinx": "^0.16.12",
Generated
+11 -10
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "557df76fdde549210349be369d479f5d",
"content-hash": "e54cf3e0bbc04b94b563b01620ac3416",
"packages": [
{
"name": "akaunting/laravel-setting",
@@ -4166,16 +4166,16 @@
},
{
"name": "phpmailer/phpmailer",
"version": "v6.9.3",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e"
"reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/2f5c94fe7493efc213f643c23b1b1c249d40f47e",
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
"reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
"shasum": ""
},
"require": {
@@ -4189,13 +4189,14 @@
"doctrine/annotations": "^1.2.6 || ^1.13.3",
"php-parallel-lint/php-console-highlighter": "^1.0.0",
"php-parallel-lint/php-parallel-lint": "^1.3.2",
"phpcompatibility/php-compatibility": "^9.3.5",
"roave/security-advisories": "dev-latest",
"squizlabs/php_codesniffer": "^3.7.2",
"phpcompatibility/php-compatibility": "^10.0.0@dev",
"squizlabs/php_codesniffer": "^3.13.5",
"yoast/phpunit-polyfills": "^1.0.4"
},
"suggest": {
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
"directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example",
"ext-imap": "Needed to support advanced email address parsing according to RFC822",
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
@@ -4235,7 +4236,7 @@
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"support": {
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.9.3"
"source": "https://github.com/PHPMailer/PHPMailer/tree/v7.1.1"
},
"funding": [
{
@@ -4243,7 +4244,7 @@
"type": "github"
}
],
"time": "2024-11-24T18:04:13+00:00"
"time": "2026-05-18T08:06:14+00:00"
},
{
"name": "phpoffice/phpspreadsheet",
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
/**
* Aggiunge il flag `engine` alla tabella `routine` esistente (0 = legacy su file,
* 1 = motore generico basato su routine_step) e crea la tabella `routine_step`.
*
* Migrazione puramente additiva: le routine legacy esistenti (id 1..4) restano
* intatte e con engine = 0 continuano a funzionare come prima.
*/
final class AddEngineAndRoutineStep extends AbstractMigration
{
public function change(): void
{
// 1) Flag engine sulla tabella esistente `routine`
$routine = $this->table('routine');
if (!$routine->hasColumn('engine')) {
$routine
->addColumn('engine', 'boolean', [
'default' => 0,
'null' => false,
'comment' => '0 = routine legacy su file, 1 = motore generico (routine_step)',
'after' => 'filename',
])
->update();
}
// 2) Nuova tabella routine_step
if (!$this->hasTable('routine_step')) {
$table = $this->table('routine_step', [
'id' => 'idstep',
'signed' => false,
'engine' => 'InnoDB',
'collation' => 'utf8mb4_general_ci',
]);
$table
->addColumn('idroutine', 'integer', [
'null' => false,
'comment' => 'FK verso routine.idroutine',
])
->addColumn('step_order', 'integer', [
'default' => 1,
'null' => false,
'comment' => 'Ordine di esecuzione dello step nella routine',
])
->addColumn('operation', 'string', [
'limit' => 50,
'null' => false,
'comment' => 'Verbo operazione: merge, replace, trim, copy, clear, uppercase, lowercase',
])
->addColumn('params', 'text', [
'null' => true,
'comment' => 'Parametri operazione in formato JSON',
])
->addColumn('active', 'boolean', [
'default' => 1,
'null' => false,
'comment' => 'Se 0 lo step viene saltato',
])
->addIndex(['idroutine', 'step_order'], ['name' => 'idx_routine_order'])
->create();
}
}
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddLogFileToDatadb extends AbstractMigration
{
public function change(): void
{
$table = $this->table('datadb');
if (!$table->hasColumn('log_file')) {
$table->addColumn('log_file', 'string', [
'limit' => 255,
'null' => true,
'default' => null,
'after' => 'commessaweb', // aggiusta se la colonna non esiste
])->update();
}
}
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddErrorStatusToDatadb extends AbstractMigration
{
public function up(): void
{
// Aggiunge 'x' (errore export) all'enum status, preservando i valori esistenti
$this->execute("ALTER TABLE `datadb`
MODIFY `status` ENUM('i','P','l','x') NULL DEFAULT NULL");
}
public function down(): void
{
// Rollback: riporta le eventuali 'x' a NULL prima di togliere il valore
$this->execute("UPDATE `datadb` SET `status` = NULL WHERE `status` = 'x'");
$this->execute("ALTER TABLE `datadb`
MODIFY `status` ENUM('i','P','l') NULL DEFAULT NULL");
}
}
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddNotifyExportEmailToAuthUsers extends AbstractMigration
{
public function change(): void
{
$table = $this->table('auth_users');
if (!$table->hasColumn('notify_export_email')) {
$table->addColumn('notify_export_email', 'boolean', [
'default' => true, // di default ricevono la mail
'null' => false,
'after' => 'lims_global_user_id',
])->update();
}
}
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddAnalysisPriceToPartAnalyses extends AbstractMigration
{
public function change(): void
{
$this->table('identification_parts_analyses')
->addColumn('analysis_price', 'decimal', [
'precision' => 10,
'scale' => 2,
'null' => true,
'default' => null,
'after' => 'analysis_level',
])
->update();
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
/**
* Ex public/userarea/sql/lims_value_binding.sql (feature/xls-mapping)
*/
final class CreateLimsValueBinding extends AbstractMigration
{
public function up(): void
{
if ($this->hasTable('lims_value_binding')) {
return;
}
$this->execute("
CREATE TABLE `lims_value_binding` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`template_id` int(11) NOT NULL,
`mapping_id` int(11) NOT NULL,
`field_id` int(11) NOT NULL,
`source_value` varchar(500) NOT NULL,
`source_key` varchar(500) NOT NULL,
`lims_value_id` int(11) NOT NULL,
`lims_value` varchar(500) DEFAULT NULL,
`created_by` int(11) DEFAULT NULL,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_mapping_source` (`mapping_id`, `source_key`),
KEY `idx_template` (`template_id`),
KEY `idx_field_source` (`field_id`, `source_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
");
}
public function down(): void
{
$this->execute("DROP TABLE IF EXISTS `lims_value_binding`");
}
}
@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
/**
* Ex public/userarea/sql/fixed_field_binding.sql (feature/xls-mapping)
* Idempotente: salta colonne/indici gia' presenti.
*/
final class XlsFixedFieldBinding extends AbstractMigration
{
public function up(): void
{
// --- template_fixed_mapping ---
$tfm = $this->table('template_fixed_mapping');
if (!$tfm->hasColumn('json_node')) {
$this->execute("ALTER TABLE `template_fixed_mapping`
ADD COLUMN `json_node` varchar(500) DEFAULT NULL");
}
if (!$tfm->hasColumn('default_source')) {
$this->execute("ALTER TABLE `template_fixed_mapping`
ADD COLUMN `default_source` enum('manual','xls','json') NOT NULL DEFAULT 'manual'");
} else {
$this->execute("ALTER TABLE `template_fixed_mapping`
MODIFY COLUMN `default_source` enum('manual','xls','json') NOT NULL DEFAULT 'manual'");
}
if (!$tfm->hasColumn('excel_column')) {
$this->execute("ALTER TABLE `template_fixed_mapping`
ADD COLUMN `excel_column` varchar(255) DEFAULT NULL AFTER `json_node`");
}
// --- lims_value_binding ---
$lvb = $this->table('lims_value_binding');
if (!$lvb->hasColumn('binding_kind')) {
$this->execute("ALTER TABLE `lims_value_binding`
ADD COLUMN `binding_kind` enum('custom','fixed') NOT NULL DEFAULT 'custom' AFTER `template_id`");
}
$this->execute("ALTER TABLE `lims_value_binding`
MODIFY COLUMN `field_id` int(11) NOT NULL DEFAULT 0");
if ($lvb->hasIndexByName('uq_mapping_source')) {
$this->execute("ALTER TABLE `lims_value_binding` DROP INDEX `uq_mapping_source`");
}
if (!$lvb->hasIndexByName('uq_kind_mapping_source')) {
$this->execute("ALTER TABLE `lims_value_binding`
ADD UNIQUE KEY `uq_kind_mapping_source` (`binding_kind`, `mapping_id`, `source_key`)");
}
}
public function down(): void
{
$lvb = $this->table('lims_value_binding');
if ($lvb->hasIndexByName('uq_kind_mapping_source')) {
$this->execute("ALTER TABLE `lims_value_binding` DROP INDEX `uq_kind_mapping_source`");
}
if (!$lvb->hasIndexByName('uq_mapping_source')) {
$this->execute("ALTER TABLE `lims_value_binding`
ADD UNIQUE KEY `uq_mapping_source` (`mapping_id`, `source_key`)");
}
if ($lvb->hasColumn('binding_kind')) {
$this->execute("ALTER TABLE `lims_value_binding` DROP COLUMN `binding_kind`");
}
$this->execute("ALTER TABLE `lims_value_binding`
MODIFY COLUMN `field_id` int(11) NOT NULL");
$tfm = $this->table('template_fixed_mapping');
foreach (['excel_column', 'default_source', 'json_node'] as $col) {
if ($tfm->hasColumn($col)) {
$this->execute("ALTER TABLE `template_fixed_mapping` DROP COLUMN `$col`");
}
}
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
use Phinx\Seed\AbstractSeed;
/**
* Seed dimostrativo del motore generico.
*
* - Assicura che le routine legacy esistenti abbiano engine = 0 (nessun impatto).
* - Crea una routine di esempio "merge_D_F_H_into_J" con engine = 1
* e il relativo step: unisce le colonne D, F, H nella colonna J.
*
* Nota: filename per le routine engine punta al file motore condiviso _engine.php,
* così apply_routine.php lo risolve correttamente anche se il flusso invia il filename.
*/
final class RoutineEngineSeed extends AbstractSeed
{
public function run(): void
{
$pdo = $this->getAdapter()->getConnection();
// 1) Le legacy restano engine = 0 (idempotente).
$pdo->exec("UPDATE routine SET engine = 0 WHERE engine IS NULL");
// 2) Crea/aggiorna la routine engine di esempio.
$name = 'merge_D_F_H_into_J';
$description = 'Unisce le colonne D, F, H nella colonna J durante l\'importazione';
$filename = '_engine.php';
$check = $pdo->prepare("SELECT idroutine FROM routine WHERE name = :name LIMIT 1");
$check->execute([':name' => $name]);
$existing = $check->fetch(PDO::FETCH_ASSOC);
if ($existing) {
$idroutine = (int)$existing['idroutine'];
$upd = $pdo->prepare(
"UPDATE routine
SET description = :descr, filename = :fn, engine = 1, instruction = :instr
WHERE idroutine = :id"
);
$upd->execute([
':descr' => $description,
':fn' => $filename,
':instr' => 'Routine gestita dal motore generico (routine_step).',
':id' => $idroutine,
]);
} else {
$ins = $pdo->prepare(
"INSERT INTO routine (name, description, instruction, filename, engine, headerrow)
VALUES (:name, :descr, :instr, :fn, 1, 1)"
);
$ins->execute([
':name' => $name,
':descr' => $description,
':instr' => 'Routine gestita dal motore generico (routine_step).',
':fn' => $filename,
]);
$idroutine = (int)$pdo->lastInsertId();
}
// 3) Ripulisci eventuali step precedenti di questa routine e reinserisci.
$del = $pdo->prepare("DELETE FROM routine_step WHERE idroutine = :id");
$del->execute([':id' => $idroutine]);
// Step unico: merge D,F,H -> J. Colonne espresse in lettere per leggibilità.
$params = json_encode([
'source_columns' => ['D', 'F', 'H'],
'target_column' => 'J',
'separator' => ' ',
'skip_empty' => true,
], JSON_UNESCAPED_UNICODE);
$insStep = $pdo->prepare(
"INSERT INTO routine_step (idroutine, step_order, operation, params, active)
VALUES (:id, 1, 'merge', :params, 1)"
);
$insStep->execute([':id' => $idroutine, ':params' => $params]);
}
}
+1
View File
@@ -25,6 +25,7 @@ $templatedashboard = "Template Dashboard";
$newtemplate = "New Template";
$dashtemplate = "Dashboard Templates";
$insertnewtemplatexls = "Insert New Template XLS";
$bindingsmanage = "Value Bindings";
// Datatables
$langdatatables = [
"paginate_first" => "First",
+36 -1
View File
@@ -166,6 +166,7 @@
is_web_selectable: rowEl.getAttribute("data-web") === "1" ? 1 : 0,
is_accredited:
rowEl.getAttribute("data-accredited") === "1" ? 1 : 0,
analysis_price: rowEl.getAttribute("data-price") || "0",
};
}
@@ -196,6 +197,10 @@
: null,
is_web_selectable: payload.is_web_selectable,
is_accredited: payload.is_accredited,
analysis_price:
payload.analysis_price !== ""
? parseFloat(payload.analysis_price)
: null,
});
}
}
@@ -247,6 +252,7 @@
analysis_level: payload.analysis_level,
is_web_selectable: payload.is_web_selectable,
is_accredited: payload.is_accredited,
analysis_price: payload.analysis_price,
},
})
.done(function (resp) {
@@ -389,6 +395,17 @@
const selectable = item.SelezionabileSuWeb === true;
const accredited = item.Accreditato === true;
const level = item.Livello ?? "";
const rawPrice = item.Prezzo;
const priceNum =
rawPrice === null ||
rawPrice === undefined ||
rawPrice === ""
? 0
: parseFloat(String(rawPrice).replace(",", "."));
const price = isNaN(priceNum) ? 0 : priceNum;
const hasPrice = price > 0;
const searchText = (
analysisName +
" " +
@@ -414,6 +431,10 @@
"</span>";
}
if (!hasPrice) {
badges += '<span class="badge bg-danger">No price</span>';
}
return `
<div
class="list-group-item analysis-analysis-item"
@@ -423,6 +444,7 @@
data-analysis-level="${escapeHtml(level)}"
data-web="${selectable ? "1" : "0"}"
data-accredited="${accredited ? "1" : "0"}"
data-price="${escapeHtml(price)}"
data-search="${escapeHtml(searchText)}"
>
<div class="fw-semibold">${escapeHtml(analysisName)}</div>
@@ -440,7 +462,6 @@
filterAnalysisList();
syncSelectedAnalysisRows();
}
function filterAnalysisList() {
const modal = document.getElementById("analysisModal");
if (!modal) return;
@@ -552,6 +573,20 @@
})
: [];
analyses.sort(function (a, b) {
const nameA = (
a.NomeAnalisiTraduzione ||
a.NomeAnalisi ||
""
).toLowerCase();
const nameB = (
b.NomeAnalisiTraduzione ||
b.NomeAnalisi ||
""
).toLowerCase();
return nameA.localeCompare(nameB);
});
analysisLoadedCache[cacheKey] = analyses;
renderAnalysesList(analyses);
})
+40 -2
View File
@@ -21,12 +21,50 @@ try {
throw new Exception("Dati della routine mancanti.");
}
$routineFile = __DIR__ . '/routines/' . $filename;
// --- NUOVO: risolvi la routine dal DB per decidere legacy vs motore ---
// Riusa la connessione PDO del progetto.
require_once __DIR__ . '/class/db-functions.php';
$pdo = DBHandlerSelect::getInstance()->getConnection();
// Identifica la routine: preferisci idroutine se passato, altrimenti risali dal filename.
$idroutine = isset($input['routine_data']['idroutine'])
? (int)$input['routine_data']['idroutine']
: 0;
if ($idroutine > 0) {
$stmt = $pdo->prepare("SELECT idroutine, name, filename, engine FROM routine WHERE idroutine = :id LIMIT 1");
$stmt->execute([':id' => $idroutine]);
} else {
// Il matching per filename è ambiguo per le routine a motore: tutte
// condividono _engine.php. Senza idroutine non possiamo sapere quale
// routine engine applicare -> meglio fermarsi che eseguire quella sbagliata.
if ($filename === '_engine.php') {
throw new Exception("Routine a motore senza idroutine: impossibile determinare quale routine applicare. Verifica che il template inoltri routine_data.idroutine.");
}
$stmt = $pdo->prepare("SELECT idroutine, name, filename, engine FROM routine WHERE filename = :fn LIMIT 1");
$stmt->execute([':fn' => $filename]);
}
$routineRow = $stmt->fetch();
// engine = 1 -> motore generico (_engine.php); 0 o record assente -> legacy come oggi.
$useEngine = $routineRow && (int)$routineRow['engine'] === 1;
if ($useEngine) {
// Il motore ha bisogno di sapere quale routine caricare dagli step.
$routineData['idroutine'] = (int)$routineRow['idroutine'];
$routineData['name'] = $routineData['name'] ?? ($routineRow['name'] ?? '');
$routineFile = __DIR__ . '/routines/_engine.php';
} else {
// Percorso legacy: il vecchio file della routine, identico a prima.
$legacyFilename = $routineRow['filename'] ?? $filename;
$routineFile = __DIR__ . '/routines/' . $legacyFilename;
}
if (file_exists($routineFile)) {
include_once $routineFile;
$routineData['xls_headers'] = $_SESSION['headers'] ?? [];
applyRoutine($excelData, $routineData); // Modifica $excelData in place
error_log("Routine {$routineData['name']} applicata (file: {$filename}) per template {$template_id}, header row: {$headerrow}");
error_log("Routine " . ($routineData['name'] ?? '') . " applicata (" . ($useEngine ? "motore" : "file: {$filename}") . ") per template {$template_id}, header row: {$headerrow}");
} else {
throw new Exception("File della routine non trovato: $routineFile");
}
+299
View File
@@ -0,0 +1,299 @@
<?php
include('include/headscript.php');
require_once __DIR__ . '/class/binding-functions.php';
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$templateFilter = isset($_GET['template_id']) ? (int) $_GET['template_id'] : 0;
$search = trim($_GET['q'] ?? '');
$templates = $pdo->query("SELECT id, name FROM excel_templates ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
$conds = [];
$params = [];
if ($templateFilter > 0) {
$conds[] = 'b.template_id = ?';
$params[] = $templateFilter;
}
if ($search !== '') {
$conds[] = '(b.source_value LIKE ? OR b.lims_value LIKE ? OR m.field_label LIKE ? OR f.fixed_field_key LIKE ?)';
$like = '%' . $search . '%';
array_push($params, $like, $like, $like, $like);
}
$where = $conds ? ('WHERE ' . implode(' AND ', $conds)) : '';
$stmt = $pdo->prepare(
"SELECT b.id, b.template_id, b.binding_kind, b.mapping_id, b.field_id, b.source_value,
b.lims_value_id, b.lims_value, b.updated_at,
t.name AS template_name,
COALESCE(m.field_label, f.fixed_field_key) AS field_label,
COALESCE(m.excel_column, f.excel_column) AS excel_column,
f.fixed_field_key
FROM lims_value_binding b
LEFT JOIN excel_templates t ON t.id = b.template_id
LEFT JOIN template_mapping m ON m.id = b.mapping_id AND b.binding_kind = 'custom'
LEFT JOIN template_fixed_mapping f ON f.id = b.mapping_id AND b.binding_kind = 'fixed'
$where
ORDER BY t.name ASC, field_label ASC, b.source_value ASC"
);
$stmt->execute($params);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$groups = [];
foreach ($rows as $r) {
$kind = $r['binding_kind'] ?? 'custom';
$gkey = $kind . ':' . (int) $r['mapping_id'];
if (!isset($groups[$gkey])) {
$groups[$gkey] = [
'kind' => $kind,
'label' => $kind === 'fixed'
? binding_fixed_label((string) $r['fixed_field_key'])
: ($r['field_label'] ?: ('Mapping ' . $gkey)),
'excel_column' => $r['excel_column'] ?: '',
'template_name' => $r['template_name'] ?: ('Template ' . $r['template_id']),
'template_id' => (int) $r['template_id'],
'rows' => [],
];
}
$groups[$gkey]['rows'][] = $r;
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
<style>
.source-value {
font-family: Consolas, Monaco, monospace;
font-weight: 600;
}
td .select2-container {
min-width: 240px;
}
.select2-container--default .select2-selection--single {
height: 38px;
border-color: #ced4da;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: 36px;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 36px;
}
</style>
<title>Gestione binding - <?= htmlspecialchars($titlewebsite ?? '', ENT_QUOTES, 'UTF-8'); ?></title>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card radius-10">
<div class="card-header">
<h6 class="mb-0">Binding valori importati &rarr; LIMS</h6>
<small class="text-muted">
Le modifiche valgono dalla prossima importazione: i record gia' importati non vengono aggiornati.
</small>
</div>
<div class="card-body">
<form class="row g-2 align-items-end mb-3" method="get">
<div class="col-md-4">
<label class="form-label mb-1">Template</label>
<select class="form-select" name="template_id" onchange="this.form.submit()">
<option value="">Tutti</option>
<?php foreach ($templates as $t): ?>
<option value="<?= (int) $t['id'] ?>" <?= $templateFilter === (int) $t['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($t['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label class="form-label mb-1">Cerca</label>
<input type="text" class="form-control" name="q" value="<?= htmlspecialchars($search) ?>"
placeholder="valore importato, valore LIMS, campo...">
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-primary">Filtra</button>
<a href="bindings_manage.php" class="btn btn-outline-secondary">Azzera</a>
</div>
</form>
<div id="bindingError" class="alert alert-danger" style="display:none;"></div>
<?php if (empty($rows)): ?>
<div class="alert alert-info mb-0">Nessun binding salvato per i filtri selezionati.</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-bordered align-middle">
<thead>
<tr>
<th style="width:32px;"></th>
<th>Valore importato</th>
<th>Valore LIMS</th>
<th style="width:150px;">Aggiornato</th>
<th style="width:110px;">Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($groups as $gkey => $g): ?>
<tr class="table-light">
<td colspan="5">
<strong><?= htmlspecialchars($g['label']) ?></strong>
<?php if (($g['kind'] ?? 'custom') === 'fixed'): ?><span class="badge bg-light text-dark border">campo fisso</span><?php endif; ?>
<span class="text-muted">
&middot; <?= htmlspecialchars($g['template_name']) ?>
<?php if ($g['excel_column'] !== ''): ?>
&middot; colonna &laquo;<?= htmlspecialchars($g['excel_column']) ?>&raquo;
<?php endif; ?>
&middot; <?= count($g['rows']) ?> valore/i
</span>
</td>
</tr>
<?php foreach ($g['rows'] as $r): ?>
<tr class="binding-row"
data-binding-id="<?= (int) $r['id'] ?>"
data-kind="<?= htmlspecialchars($r['binding_kind'] ?? 'custom', ENT_QUOTES) ?>"
data-fixed-key="<?= htmlspecialchars($r['fixed_field_key'] ?? '', ENT_QUOTES) ?>"
data-template-id="<?= (int) $r['template_id'] ?>"
data-mapping-id="<?= (int) $r['mapping_id'] ?>"
data-field-id="<?= (int) $r['field_id'] ?>"
data-source-value="<?= htmlspecialchars($r['source_value'], ENT_QUOTES) ?>">
<td class="text-muted ps-4">&rsaquo;</td>
<td class="source-value"><?= htmlspecialchars($r['source_value']) ?></td>
<td>
<select class="form-select binding-select">
<option value="<?= (int) $r['lims_value_id'] ?>" selected>
<?= htmlspecialchars($r['lims_value'] ?: ('#' . $r['lims_value_id'])) ?>
</option>
</select>
<div class="binding-status small mt-1"></div>
</td>
<td class="small text-muted"><?= htmlspecialchars((string) $r['updated_at']) ?></td>
<td>
<button type="button" class="btn btn-sm btn-outline-danger delete-binding-btn">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(function() {
const $error = $('#bindingError');
$('.binding-select').each(function() {
const $row = $(this).closest('.binding-row');
const isFixed = $row.data('kind') === 'fixed';
$(this).select2({
placeholder: 'Seleziona valore LIMS...',
width: '100%',
ajax: {
url: isFixed ? 'search_fixed_field_values.php' : 'search_customfield_values.php',
dataType: 'json',
delay: 200,
data: params => isFixed ? {
field_key: $row.data('fixed-key'),
template_id: $row.data('template-id'),
q: params.term || '',
limit: 50
} : {
field_id: $row.data('field-id'),
q: params.term || '',
limit: 50
},
processResults: data => ({
results: data.results || []
})
},
minimumInputLength: 0
});
});
$('.binding-select').on('change', function() {
const $select = $(this);
const $row = $select.closest('.binding-row');
const $status = $row.find('.binding-status');
const selected = $select.select2('data')[0] || {};
if (!$select.val()) {
return;
}
$status.removeClass('text-success text-danger').addClass('text-muted').text('Salvataggio...');
$.post('save_binding.php', {
kind: $row.data('kind') || 'custom',
template_id: $row.data('template-id'),
mapping_id: $row.data('mapping-id'),
field_id: $row.data('field-id'),
source_value: String($row.data('source-value')),
lims_value_id: $select.val(),
lims_value: selected.text || ''
}).then(resp => {
if (resp && resp.success) {
$status.removeClass('text-muted').addClass('text-success').text('Salvato');
} else {
$status.removeClass('text-muted').addClass('text-danger').text((resp && resp.error) || 'Errore');
}
}).fail(() => {
$status.removeClass('text-muted').addClass('text-danger').text('Errore di rete');
});
});
$('.delete-binding-btn').on('click', function() {
const $row = $(this).closest('.binding-row');
$error.hide();
$.post('delete_binding.php', {
binding_id: $row.data('binding-id')
}).then(resp => {
if (resp && resp.success) {
$row.remove();
} else {
$error.text((resp && resp.error) || 'Errore durante l\'eliminazione.').show();
}
}).fail(() => {
$error.text('Errore di rete durante l\'eliminazione.').show();
});
});
});
</script>
</body>
</html>
+50
View File
@@ -0,0 +1,50 @@
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception as PHPMailerException;
require_once __DIR__ . '/../../../vendor/autoload.php'; // aggiusta il path se serve
class Mailer
{
public static function send(array $to, string $subject, string $htmlBody, ?string $altBody = null): array
{
$mail = new PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = env('MAIL_HOST');
$mail->SMTPAuth = true;
$mail->Username = env('MAIL_USERNAME');
$mail->Password = env('MAIL_PASSWORD');
$mail->Port = (int) env('MAIL_PORT', 465);
$enc = env('MAIL_ENCRYPTION', 'ssl');
$mail->SMTPSecure = ($enc === 'tls')
? PHPMailer::ENCRYPTION_STARTTLS
: PHPMailer::ENCRYPTION_SMTPS;
$mail->CharSet = 'UTF-8';
$mail->setFrom(env('MAIL_FROM_ADDRESS'), env('MAIL_FROM_NAME', 'SmartTRF'));
foreach ($to as $addr) {
$addr = trim((string)$addr);
if ($addr !== '' && filter_var($addr, FILTER_VALIDATE_EMAIL)) {
$mail->addAddress($addr);
}
}
if (!$mail->getToAddresses()) {
return ['ok' => false, 'error' => 'Nessun destinatario valido'];
}
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $htmlBody;
$mail->AltBody = $altBody ?: strip_tags($htmlBody);
$mail->send();
return ['ok' => true, 'error' => null];
} catch (PHPMailerException $e) {
error_log('Mailer error: ' . $mail->ErrorInfo);
return ['ok' => false, 'error' => $mail->ErrorInfo];
}
}
}
+716
View File
@@ -0,0 +1,716 @@
<?php
function binding_normalize_value($value): string
{
$s = (string) $value;
$s = str_replace(
["\xC2\xA0", "\xE2\x80\xAF", "\xE2\x80\x87", "\xE2\x80\x89", "\xE2\x80\x8A", "\xEF\xBB\xBF"],
' ',
$s
);
$s = preg_replace('/\s+/u', ' ', $s);
return mb_strtolower(trim((string) $s));
}
function binding_is_list_field(array $mapping): bool
{
return strcasecmp(trim((string) ($mapping['data_type'] ?? '')), 'SceltaMultipla') === 0;
}
function binding_get_lims_values(int $fieldId): array
{
static $memo = [];
if ($fieldId <= 0) {
return [];
}
if (array_key_exists($fieldId, $memo)) {
return $memo[$fieldId];
}
$cacheDir = dirname(__DIR__) . '/cache';
$cacheFile = $cacheDir . '/customfield_' . $fieldId . '.json';
try {
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
$values = json_decode(file_get_contents($cacheFile), true);
} else {
require_once __DIR__ . '/VisualLimsApiClient.class.php';
$api = VisualLimsApiClient::getInstance();
$data = $api->get("CustomField($fieldId)?\$expand=CustomFieldsValues");
$values = $data['CustomFieldsValues'] ?? [];
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0777, true);
}
file_put_contents($cacheFile, json_encode($values));
}
} catch (Throwable $e) {
error_log("binding_get_lims_values failed for field $fieldId: " . $e->getMessage());
$values = [];
}
if (!is_array($values)) {
$values = [];
}
return $memo[$fieldId] = $values;
}
function binding_lims_value_index(int $fieldId): array
{
static $memo = [];
if (array_key_exists($fieldId, $memo)) {
return $memo[$fieldId];
}
$index = [];
foreach (binding_get_lims_values($fieldId) as $v) {
$id = (int) ($v['IdCustomFieldsValue'] ?? 0);
if ($id > 0) {
$index[$id] = (string) ($v['Valore'] ?? '');
}
}
return $memo[$fieldId] = $index;
}
function binding_has_lims_values(int $fieldId): bool
{
return binding_get_lims_values($fieldId) !== [];
}
function binding_is_lims_id(int $fieldId, $value): bool
{
$raw = trim((string) $value);
if ($raw === '' || !ctype_digit($raw)) {
return false;
}
$index = binding_lims_value_index($fieldId);
return $index === [] ? false : isset($index[(int) $raw]);
}
function binding_auto_match(int $fieldId, string $sourceValue): ?array
{
$needle = binding_normalize_value($sourceValue);
if ($needle === '') {
return null;
}
$matches = [];
foreach (binding_get_lims_values($fieldId) as $v) {
if (binding_normalize_value($v['Valore'] ?? '') === $needle) {
$matches[] = [
'id' => (int) ($v['IdCustomFieldsValue'] ?? 0),
'text' => (string) ($v['Valore'] ?? ''),
];
}
}
return count($matches) === 1 && $matches[0]['id'] > 0 ? $matches[0] : null;
}
function binding_lookup(PDO $pdo, int $mappingId, string $sourceValue, string $kind = 'custom'): ?array
{
$stmt = $pdo->prepare(
"SELECT id, lims_value_id, lims_value, source_value
FROM lims_value_binding
WHERE binding_kind = ? AND mapping_id = ? AND source_key = ?
LIMIT 1"
);
$stmt->execute([$kind, $mappingId, binding_normalize_value($sourceValue)]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
function binding_upsert(
PDO $pdo,
int $templateId,
int $mappingId,
int $fieldId,
string $sourceValue,
int $limsValueId,
string $limsValue,
?int $createdBy,
string $kind = 'custom'
): void {
$stmt = $pdo->prepare(
"INSERT INTO lims_value_binding
(template_id, binding_kind, mapping_id, field_id, source_value, source_key, lims_value_id, lims_value, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
lims_value_id = VALUES(lims_value_id),
lims_value = VALUES(lims_value),
field_id = VALUES(field_id),
template_id = VALUES(template_id)"
);
$stmt->execute([
$templateId,
$kind,
$mappingId,
$fieldId,
mb_substr(trim($sourceValue), 0, 500),
mb_substr(binding_normalize_value($sourceValue), 0, 500),
$limsValueId,
mb_substr($limsValue, 0, 500),
$createdBy,
]);
}
function binding_delete(PDO $pdo, int $mappingId, string $sourceValue, string $kind = 'custom'): int
{
$stmt = $pdo->prepare("DELETE FROM lims_value_binding WHERE binding_kind = ? AND mapping_id = ? AND source_key = ?");
$stmt->execute([$kind, $mappingId, binding_normalize_value($sourceValue)]);
return $stmt->rowCount();
}
function binding_delete_by_id(PDO $pdo, int $bindingId): int
{
$stmt = $pdo->prepare("DELETE FROM lims_value_binding WHERE id = ?");
$stmt->execute([$bindingId]);
return $stmt->rowCount();
}
function binding_suggest(PDO $pdo, int $fieldId, string $sourceValue, int $excludeMappingId): ?array
{
if ($fieldId <= 0) {
return null;
}
$stmt = $pdo->prepare(
"SELECT b.lims_value_id, b.lims_value, b.template_id, t.name AS template_name
FROM lims_value_binding b
LEFT JOIN excel_templates t ON t.id = b.template_id
WHERE b.binding_kind = 'custom' AND b.field_id = ? AND b.source_key = ? AND b.mapping_id <> ?
ORDER BY b.updated_at DESC
LIMIT 1"
);
$stmt->execute([$fieldId, binding_normalize_value($sourceValue), $excludeMappingId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
function binding_apply_to_details(PDO $pdo, int $mappingId, string $value, array $datadbIds): int
{
$datadbIds = array_values(array_filter(array_map('intval', $datadbIds)));
if (empty($datadbIds)) {
return 0;
}
$placeholders = implode(',', array_fill(0, count($datadbIds), '?'));
$stmt = $pdo->prepare(
"UPDATE import_data_details
SET field_value = ?
WHERE mapping_id = ? AND id IN ($placeholders)"
);
$stmt->execute(array_merge([$value, $mappingId], $datadbIds));
return $stmt->rowCount();
}
function binding_find_column_index(string $column, array $columns)
{
$needle = binding_normalize_value($column);
if ($needle === '') {
return false;
}
foreach ($columns as $i => $c) {
if (binding_normalize_value($c) === $needle) {
return $i;
}
}
return false;
}
function binding_fixed_alias_map(): array
{
return [
'ClienteResponsabile' => 'cliente_responsabile_id',
'ClienteFornitore' => 'cliente_fornitore_id',
'ClienteAnalisi' => 'clienteAnalisi',
'ClienteFatturazione' => 'ClienteFatturazione',
'MoltiplicatorePrezzo' => 'moltiplicatore_prezzo_id',
'AnagraficaCertestObject' => 'anagrafica_certest_object_id',
'AnagraficaCertestService' => 'anagrafica_certest_service_id',
'ConsegnaRichiesta' => 'consegna_richiesta',
];
}
function binding_fixed_column(string $key): ?string
{
return binding_fixed_alias_map()[$key] ?? null;
}
function binding_fixed_is_list(string $key): bool
{
return in_array($key, [
'ClienteResponsabile',
'ClienteFornitore',
'ClienteAnalisi',
'ClienteFatturazione',
'MoltiplicatorePrezzo',
'AnagraficaCertestObject',
'AnagraficaCertestService',
], true);
}
function binding_fixed_is_date(string $key): bool
{
return $key === 'ConsegnaRichiesta';
}
function binding_fixed_auto_matchable(string $key): bool
{
return in_array($key, [
'MoltiplicatorePrezzo',
'AnagraficaCertestObject',
'AnagraficaCertestService',
], true);
}
function binding_fixed_label(string $key): string
{
$labels = [
'AnagraficaCertestObject' => 'Anagrafica Certest Object',
'AnagraficaCertestService' => 'Anagrafica Certest Service',
'MoltiplicatorePrezzo' => 'Moltiplicatore Prezzo',
'ClienteResponsabile' => 'Cliente Responsabile',
'ClienteFornitore' => 'Cliente Fornitore',
'ClienteAnalisi' => 'Buyer',
'ClienteFatturazione' => 'Cliente Fatturazione',
'ConsegnaRichiesta' => 'Consegna Richiesta',
];
return $labels[$key] ?? $key;
}
function binding_parse_date($value): ?string
{
$raw = trim((string) $value);
if ($raw === '') {
return null;
}
foreach (['Y-m-d', 'd/m/Y', 'd-m-Y', 'd.m.Y', 'Y/m/d', 'd/m/y', 'Y-m-d H:i:s'] as $format) {
$date = DateTime::createFromFormat($format, $raw);
if ($date && $date->format($format) === $raw) {
return $date->format('Y-m-d');
}
}
$ts = strtotime($raw);
return $ts ? date('Y-m-d', $ts) : null;
}
function binding_fixed_default(array $fixedMapping): string
{
$value = trim((string) ($fixedMapping['default_value'] ?? ''));
if ($value === '') {
return '';
}
$isDate = strcasecmp((string) ($fixedMapping['data_type'] ?? ''), 'DATE') === 0
|| binding_fixed_is_date((string) ($fixedMapping['fixed_field_key'] ?? ''));
if ($isDate && preg_match('/^today\s*(?:\+\s*(\d+))?$/i', $value, $m)) {
return date('Y-m-d', strtotime('+' . (int) ($m[1] ?? 0) . ' days'));
}
return $value;
}
function binding_cached_get($api, string $cacheFile, string $endpoint, int $ttl = 3600): array
{
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < $ttl)) {
$data = json_decode(file_get_contents($cacheFile), true);
if (is_array($data)) {
return $data;
}
}
$data = $api->get($endpoint);
if (is_array($data)) {
$dir = dirname($cacheFile);
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
file_put_contents($cacheFile, json_encode($data));
}
return is_array($data) ? $data : [];
}
function binding_template_idclient(PDO $pdo, int $templateId): int
{
$stmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
$stmt->execute([$templateId]);
return (int) ($stmt->fetchColumn() ?: 0);
}
function binding_client_label(array $client): string
{
$name = trim((string) ($client['Nominativo'] ?? ''));
$code = trim((string) ($client['CodiceCliente'] ?? ''));
return $code !== '' ? ($name . ' - ' . $code) : $name;
}
function binding_get_fixed_values(PDO $pdo, string $fieldKey, int $templateId): array
{
static $memo = [];
$memoKey = $fieldKey . '|' . $templateId;
if (isset($memo[$memoKey])) {
return $memo[$memoKey];
}
$cacheDir = dirname(__DIR__) . '/cache';
$out = [];
try {
require_once __DIR__ . '/VisualLimsApiClient.class.php';
$api = VisualLimsApiClient::getInstance();
switch ($fieldKey) {
case 'MoltiplicatorePrezzo':
$data = binding_cached_get($api, "$cacheDir/moltiplicatori_prezzo.json", 'MoltiplicatorePrezzi');
foreach (($data['value'] ?? []) as $r) {
$out[] = ['id' => (int) ($r['IdMoltiplicatorePrezzo'] ?? 0), 'text' => (string) ($r['Descrizione'] ?? '')];
}
break;
case 'AnagraficaCertestObject':
case 'AnagraficaCertestService':
$file = $fieldKey === 'AnagraficaCertestObject'
? 'anagrafica_certest_object.json'
: 'anagrafica_certest_service.json';
$data = binding_cached_get($api, "$cacheDir/$file", $fieldKey);
foreach (($data['value'] ?? []) as $r) {
$code = trim((string) ($r['Codice'] ?? ''));
$name = (string) ($r['NomeAnagrafica'] ?? '');
$out[] = [
'id' => (int) ($r['IdAnagrafica'] ?? 0),
'text' => $name,
'aliases' => array_values(array_filter([$code, $code !== '' ? "$code - $name" : ''])),
];
}
break;
case 'ClienteResponsabile':
$idCliente = binding_template_idclient($pdo, $templateId);
if ($idCliente > 0) {
$data = binding_cached_get(
$api,
"$cacheDir/cliente_responsabili_$idCliente.json",
"Cliente($idCliente)?\$expand=Responsabili"
);
foreach (($data['Responsabili'] ?? []) as $r) {
$out[] = ['id' => (int) ($r['IdClienteResponsabile'] ?? 0), 'text' => (string) ($r['Nominativo'] ?? '')];
}
}
break;
case 'ClienteFornitore':
case 'ClienteAnalisi':
case 'ClienteFatturazione':
$endpoint = 'Cliente?' . http_build_query([
'$select' => 'IdCliente,Nominativo,CodiceCliente',
'$orderby' => 'Nominativo asc',
]);
$data = binding_cached_get($api, "$cacheDir/clienti.json", $endpoint);
foreach (($data['value'] ?? []) as $r) {
$out[] = ['id' => (int) ($r['IdCliente'] ?? 0), 'text' => binding_client_label($r)];
}
break;
}
} catch (Throwable $e) {
error_log("binding_get_fixed_values($fieldKey) failed: " . $e->getMessage());
$out = [];
}
return $memo[$memoKey] = array_values(array_filter($out, fn($v) => $v['id'] > 0));
}
function binding_fixed_has_values(PDO $pdo, string $fieldKey, int $templateId): bool
{
return binding_get_fixed_values($pdo, $fieldKey, $templateId) !== [];
}
function binding_fixed_is_lims_id(PDO $pdo, string $fieldKey, int $templateId, $value): bool
{
$raw = trim((string) $value);
if ($raw === '' || !ctype_digit($raw)) {
return false;
}
foreach (binding_get_fixed_values($pdo, $fieldKey, $templateId) as $v) {
if ((int) $v['id'] === (int) $raw) {
return true;
}
}
return false;
}
function binding_auto_match_fixed(array $values, string $sourceValue): ?array
{
$needle = binding_normalize_value($sourceValue);
if ($needle === '') {
return null;
}
$matches = [];
foreach ($values as $v) {
$candidates = array_merge([$v['text'] ?? ''], $v['aliases'] ?? []);
foreach ($candidates as $candidate) {
if (binding_normalize_value($candidate) === $needle) {
$matches[] = $v;
break;
}
}
}
return count($matches) === 1 ? $matches[0] : null;
}
function binding_suggest_fixed(PDO $pdo, string $fieldKey, string $sourceValue, int $excludeMappingId): ?array
{
$stmt = $pdo->prepare(
"SELECT b.lims_value_id, b.lims_value, b.template_id, t.name AS template_name
FROM lims_value_binding b
JOIN template_fixed_mapping f ON f.id = b.mapping_id
LEFT JOIN excel_templates t ON t.id = b.template_id
WHERE b.binding_kind = 'fixed' AND f.fixed_field_key = ?
AND b.source_key = ? AND b.mapping_id <> ?
ORDER BY b.updated_at DESC
LIMIT 1"
);
$stmt->execute([$fieldKey, binding_normalize_value($sourceValue), $excludeMappingId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
function binding_apply_to_datadb(PDO $pdo, string $fieldKey, $value, array $datadbIds): int
{
$column = binding_fixed_column($fieldKey);
if ($column === null) {
throw new InvalidArgumentException("Campo fisso sconosciuto: $fieldKey");
}
$datadbIds = array_values(array_filter(array_map('intval', $datadbIds)));
if (empty($datadbIds)) {
return 0;
}
$placeholders = implode(',', array_fill(0, count($datadbIds), '?'));
$stmt = $pdo->prepare("UPDATE datadb SET `$column` = ? WHERE iddatadb IN ($placeholders)");
$stmt->execute(array_merge([$value === '' ? null : $value], $datadbIds));
return $stmt->rowCount();
}
function binding_write_fixed_columns(PDO $pdo, int $iddatadb, array $valuesByKey): int
{
$map = binding_fixed_alias_map();
$sets = [];
$params = [];
foreach ($valuesByKey as $key => $value) {
if (!isset($map[$key]) || $value === null || $value === '') {
continue;
}
$sets[] = "`{$map[$key]}` = ?";
$params[] = $value;
}
if (empty($sets)) {
return 0;
}
$params[] = $iddatadb;
$stmt = $pdo->prepare("UPDATE datadb SET " . implode(', ', $sets) . " WHERE iddatadb = ?");
$stmt->execute($params);
return $stmt->rowCount();
}
function binding_resolve_for_import(
PDO $pdo,
array $mapping,
int $templateId,
string $sourceValue,
int $iddatadb,
?int $userId,
array &$saved,
array &$auto,
array &$pending
): string {
$mappingId = (int) $mapping['id'];
$fieldId = (int) $mapping['field_id'];
$key = 'cf:' . $mappingId . '|' . binding_normalize_value($sourceValue);
$collect = function (array &$bucket, array $extra = []) use ($key, $mapping, $mappingId, $fieldId, $sourceValue, $iddatadb) {
if (!isset($bucket[$key])) {
$bucket[$key] = array_merge([
'kind' => 'custom',
'mapping_id' => $mappingId,
'field_id' => $fieldId,
'field_label' => $mapping['field_label'] ?? ('Mapping ' . $mappingId),
'source_value' => $sourceValue,
'datadb_ids' => [],
], $extra);
}
$bucket[$key]['datadb_ids'][] = $iddatadb;
};
if (!binding_has_lims_values($fieldId)) {
return $sourceValue;
}
if (binding_is_lims_id($fieldId, $sourceValue)) {
return $sourceValue;
}
if ($existing = binding_lookup($pdo, $mappingId, $sourceValue)) {
$collect($saved, [
'lims_value_id' => (int) $existing['lims_value_id'],
'lims_value' => (string) $existing['lims_value'],
]);
return (string) $existing['lims_value_id'];
}
if ($match = binding_auto_match($fieldId, $sourceValue)) {
binding_upsert($pdo, $templateId, $mappingId, $fieldId, $sourceValue, $match['id'], $match['text'], $userId);
$collect($auto, [
'lims_value_id' => $match['id'],
'lims_value' => $match['text'],
]);
return (string) $match['id'];
}
$collect($pending);
return $sourceValue;
}
function binding_resolve_fixed_for_import(
PDO $pdo,
array $fixedMapping,
int $templateId,
string $sourceValue,
int $iddatadb,
?int $userId,
array &$saved,
array &$auto,
array &$pending
): ?string {
$fieldKey = (string) $fixedMapping['fixed_field_key'];
$mappingId = (int) $fixedMapping['id'];
$key = 'fx:' . $mappingId . '|' . binding_normalize_value($sourceValue);
$collect = function (array &$bucket, array $extra = []) use ($key, $fieldKey, $mappingId, $sourceValue, $iddatadb) {
if (!isset($bucket[$key])) {
$bucket[$key] = array_merge([
'kind' => 'fixed',
'mapping_id' => $mappingId,
'fixed_field_key' => $fieldKey,
'field_id' => 0,
'field_label' => binding_fixed_label($fieldKey),
'source_value' => $sourceValue,
'datadb_ids' => [],
], $extra);
}
$bucket[$key]['datadb_ids'][] = $iddatadb;
};
if (binding_fixed_is_date($fieldKey)) {
return binding_parse_date($sourceValue);
}
if (!binding_fixed_is_list($fieldKey)) {
return $sourceValue;
}
if (!binding_fixed_has_values($pdo, $fieldKey, $templateId)) {
error_log("[BINDING] campo fisso $fieldKey: lista LIMS non disponibile, valore non scritto");
return null;
}
if (binding_fixed_is_lims_id($pdo, $fieldKey, $templateId, $sourceValue)) {
return (string) (int) $sourceValue;
}
if ($existing = binding_lookup($pdo, $mappingId, $sourceValue, 'fixed')) {
$collect($saved, [
'lims_value_id' => (int) $existing['lims_value_id'],
'lims_value' => (string) $existing['lims_value'],
]);
return (string) $existing['lims_value_id'];
}
if (binding_fixed_auto_matchable($fieldKey)) {
$match = binding_auto_match_fixed(binding_get_fixed_values($pdo, $fieldKey, $templateId), $sourceValue);
if ($match) {
binding_upsert($pdo, $templateId, $mappingId, 0, $sourceValue, (int) $match['id'], (string) $match['text'], $userId, 'fixed');
$collect($auto, [
'lims_value_id' => (int) $match['id'],
'lims_value' => (string) $match['text'],
]);
return (string) $match['id'];
}
}
$collect($pending);
return null;
}
function binding_merge_buckets(array $bucket, array $items): array
{
foreach ($items as $key => $item) {
if (!isset($bucket[$key])) {
$bucket[$key] = $item;
continue;
}
$bucket[$key]['datadb_ids'] = array_values(array_unique(array_merge(
$bucket[$key]['datadb_ids'] ?? [],
$item['datadb_ids'] ?? []
)));
}
return $bucket;
}
function binding_list_for_template(PDO $pdo, int $templateId): array
{
$stmt = $pdo->prepare(
"SELECT b.*,
COALESCE(m.field_label, f.fixed_field_key) AS field_label,
COALESCE(m.excel_column, f.excel_column) AS excel_column,
f.fixed_field_key
FROM lims_value_binding b
LEFT JOIN template_mapping m ON m.id = b.mapping_id AND b.binding_kind = 'custom'
LEFT JOIN template_fixed_mapping f ON f.id = b.mapping_id AND b.binding_kind = 'fixed'
WHERE b.template_id = ?
ORDER BY field_label ASC, b.source_value ASC"
);
$stmt->execute([$templateId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
+40
View File
@@ -0,0 +1,40 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
require_once __DIR__ . '/class/binding-functions.php';
include dirname(__DIR__) . '/../extra/auth.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
if (!Auth::check()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
exit;
}
$bindingId = intval($_POST['binding_id'] ?? 0);
if ($bindingId <= 0) {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Missing binding_id']);
exit;
}
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$deleted = binding_delete_by_id($pdo, $bindingId);
echo json_encode(['success' => $deleted > 0, 'deleted' => $deleted]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
+37
View File
@@ -0,0 +1,37 @@
<?php
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$logDir = __DIR__ . '/logs/api/';
$iddatadb = (int)($_GET['iddatadb'] ?? 0);
if ($iddatadb <= 0) {
http_response_code(400);
exit('iddatadb mancante');
}
$stmt = $pdo->prepare("SELECT log_file, commessaweb FROM datadb WHERE iddatadb = :id LIMIT 1");
$stmt->execute(['id' => $iddatadb]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row || empty($row['log_file'])) {
http_response_code(404);
exit('Nessun log per questa commessa');
}
// solo basename: niente path traversal
$path = $logDir . basename($row['log_file']);
if (!is_file($path)) {
http_response_code(404);
exit('File di log non trovato: ' . basename($row['log_file']));
}
while (ob_get_level() > 0) {
ob_end_clean();
}
$label = preg_replace('/[^A-Za-z0-9_\-]/', '_', (string)($row['commessaweb'] ?: $iddatadb));
header('Content-Type: text/plain; charset=utf-8');
header('Content-Disposition: attachment; filename="log_' . $label . '_' . $iddatadb . '.log"');
header('Content-Length: ' . filesize($path));
readfile($path);
exit;
-376
View File
@@ -1,376 +0,0 @@
2025-06-10 12:21:44 - Errore nel recupero dati: HTTP 404, Risposta:
2025-06-16 11:15:30 - Errore nel recupero dati: HTTP 404, Risposta: {"title":"Not Found","status":404,"detail":"Not Found","instance":"GET /api/odata/Rapporto(2523026)","errorCode":"d25cbd678"}
2025-06-16 11:34:59 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to 93.43.5.102 port 443 after 21033 ms: Couldn't connect to server, Risposta:
2025-06-16 11:42:03 - Errore nella richiesta: Recv failure: Connection was reset
2025-07-04 10:42:49 - Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2025-07-04 10:44:13 - Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2025-07-04 10:48:07 - Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2025-08-19 16:29:25 - Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2025-08-26 16:47:19 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
2025-08-26 16:48:15 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
2025-08-26 16:48:44 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
2025-08-26 16:49:24 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
2025-08-26 16:50:23 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
2025-09-08 08:39:17 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
2026-01-27 15:33:53 - Errore nel recupero dati: HTTP 404, Risposta:
2026-01-27 15:34:06 - Errore nel recupero dati: HTTP 404, Risposta:
2026-01-27 15:34:10 - Errore nel recupero dati: HTTP 404, Risposta:
2026-01-27 15:35:13 - Errore nel recupero dati: HTTP 404, Risposta:
2026-01-29 14:33:38 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
2026-01-29 14:33:39 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-01-29 14:34:04 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
2026-01-29 14:37:29 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
2026-01-29 14:41:55 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
2026-01-29 14:42:03 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
2026-01-29 14:42:52 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
2026-01-29 14:43:00 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
2026-01-30 10:50:43 [AnagraficaCertestService] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-01-30 10:50:43 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-01-30 11:09:22 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-19 10:00:13 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-19 10:00:42 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-19 10:00:44 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-23 10:44:04 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:43 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:43 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:43 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:53 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:48:26 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:48:26 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:48:27 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:48:27 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:48:27 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:49:04 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:49:04 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:49:04 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:49:05 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:49:05 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 14:26:52 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 14:26:59 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 15:54:16 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 16:22:45 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 16:32:17 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-25 15:23:12 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-26 15:01:32 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-28 21:01:27 [AnagraficaCertestService] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-03-01 19:11:58 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-03-18 15:51:45 [AnagraficaCertestService] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-03-19 09:50:34 [AnagraficaCertestService] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-03-25 14:13:34 - Autenticazione fallita: HTTP 503, Errore cURL: , Risposta: The service is unavailable.
2026-03-25 14:13:34 - Autenticazione fallita: HTTP 503, Errore cURL: , Risposta: The service is unavailable.
2026-03-26 10:57:18 [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:57:21 [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:57:21 [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-03-26 10:57:21 [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:36 [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:57:36 [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-03-26 10:57:36 [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: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
2026-06-16 14:29:15 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21037 ms: Couldn't connect to server, Risposta:
2026-06-16 14:29:36 [ClienteResponsabile] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21099 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:03 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21076 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:03 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21083 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:03 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21083 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:34 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21043 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:55 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21053 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:55 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21050 ms: Couldn't connect to server, Risposta:
2026-06-16 14:31:46 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21048 ms: Couldn't connect to server, Risposta:
2026-06-16 14:31:46 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21043 ms: Couldn't connect to server, Risposta:
2026-06-16 14:31:46 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21042 ms: Couldn't connect to server, Risposta:
2026-06-16 14:32:07 [ClienteResponsabile] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21047 ms: Couldn't connect to server, Risposta:
2026-06-16 14:33:09 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21058 ms: Couldn't connect to server, Risposta:
2026-06-16 14:33:09 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21057 ms: Couldn't connect to server, Risposta:
2026-06-16 14:33:09 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21057 ms: Couldn't connect to server, Risposta:
2026-06-16 14:33:30 [ClienteResponsabile] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21047 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:11 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21062 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:11 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21064 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:11 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21065 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:33 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21049 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:33 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21057 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:33 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21053 ms: Couldn't connect to server, Risposta:
2026-06-16 14:37:13 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21032 ms: Couldn't connect to server, Risposta:
2026-06-16 14:37:14 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21031 ms: Couldn't connect to server, Risposta:
2026-06-16 14:37:14 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21027 ms: Couldn't connect to server, Risposta:
2026-06-16 14:37:35 [ClienteResponsabile] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21054 ms: Couldn't connect to server, Risposta:
2026-06-16 17:13:55 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21067 ms: Couldn't connect to server, Risposta:
2026-06-16 17:14:25 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21065 ms: Couldn't connect to server, Risposta:
2026-06-16 17:14:46 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21071 ms: Couldn't connect to server, Risposta:
2026-06-16 17:14:46 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21078 ms: Couldn't connect to server, Risposta:
2026-07-09 08:06:33 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:33 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:33 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:33 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:33 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:41 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:41 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:41 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:41 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:41 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-10 08:12:24 - Errore nella richiesta: SSL connection timeout
+363
View File
@@ -0,0 +1,363 @@
<?php
include('include/headscript.php');
$template_id = intval($_GET['id'] ?? 0);
if (!$template_id) {
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Template ID mancante"));
exit;
}
$user_id = $iduserlogin ?? 1;
$show_all_users = isset($_GET['all_users']) && $_GET['all_users'] == '1';
$allowedLimits = [20, 40, 60, 100];
$rawLimit = (int)($_GET['limit'] ?? 20);
$perPage = in_array($rawLimit, $allowedLimits) ? $rawLimit : 20;
$page = max(1, (int)($_GET['page'] ?? 1));
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$logDir = __DIR__ . '/logs/api/';
// Estrae un riassunto breve dell'errore dal file di log della commessa
function extractErrorSummary(string $logDir, ?string $logFile): string
{
if (empty($logFile)) return '';
$path = $logDir . basename($logFile);
if (!is_file($path)) return '';
$content = @file_get_contents($path);
if ($content === false || $content === '') return '';
// 1) prova col "detail" del JSON del 500
if (preg_match('/"detail"\s*:\s*"([^"]+)"/', $content, $m)) {
return trim(stripcslashes($m[1]));
}
// 2) fallback: ultima riga MESSAGE:
if (preg_match_all('/^MESSAGE:\s*(.+)$/m', $content, $m) && !empty($m[1])) {
return trim(end($m[1]));
}
return '';
}
// Query: solo le righe in errore per questo template
$userFilter = $show_all_users ? '' : 'AND d.user_id = ?';
$baseWhere = "WHERE d.templateid = ? AND d.status = 'x' {$userFilter}";
$baseParams = [$template_id];
if (!$show_all_users) $baseParams[] = $user_id;
$countStmt = $pdo->prepare("SELECT COUNT(*) FROM datadb d {$baseWhere}");
$countStmt->execute($baseParams);
$totalRows = (int)$countStmt->fetchColumn();
$totalPages = max(1, (int)ceil($totalRows / $perPage));
if ($page > $totalPages) $page = $totalPages;
$stmt = $pdo->prepare("
SELECT d.*, CONCAT(u.first_name, ' ', u.last_name) AS user_name
FROM datadb d
LEFT JOIN auth_users u ON d.user_id = u.id
{$baseWhere}
ORDER BY d.export_date DESC, d.iddatadb DESC
LIMIT {$perPage} OFFSET " . (($page - 1) * $perPage) . "
");
$stmt->execute($baseParams);
$rowsData = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
.err-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.err-table th,
.err-table td {
padding: 8px 10px;
border-bottom: 1px solid #dee2e6;
vertical-align: top;
}
.err-table th {
background: #f8f9fa;
text-align: left;
font-weight: 600;
color: #495057;
}
.err-table tr:hover {
background: #fff8f8;
}
.status-badge {
display: inline-block;
padding: 2px 8px;
font-size: 12px;
font-weight: 500;
border-radius: 12px;
}
.status-x {
background: #dc3545;
color: #fff;
}
.err-msg {
color: #b02a37;
max-width: 420px;
white-space: normal;
word-break: break-word;
}
.commessa-code {
font-size: 0.8em;
color: #555;
display: block;
margin-top: 2px;
}
.action-btn {
padding: 5px 8px;
margin-right: 4px;
border: none;
border-radius: 5px;
cursor: pointer;
color: #fff;
text-decoration: none;
display: inline-flex;
align-items: center;
}
.btn-resend {
background: #28a745;
}
.btn-log {
background: #17a2b8;
}
.btn-revert {
background: #fd7e14;
}
.pager-bar {
display: flex;
align-items: center;
justify-content: space-between;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 6px 14px;
font-size: 13px;
margin-bottom: 10px;
}
.pager-num,
.pager-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 28px;
height: 28px;
padding: 0 6px;
border: 1px solid #ced4da;
border-radius: 4px;
text-decoration: none;
color: #495057;
margin-left: 4px;
}
.pager-num.active {
background: #0d6efd;
color: #fff;
border-color: #0d6efd;
}
</style>
<title>Export Errors - <?= htmlspecialchars($titlewebsite ?? '', ENT_QUOTES, 'UTF-8'); ?></title>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="mb-3 d-flex align-items-center gap-2">
<a href="imported.php?id=<?= $template_id ?>" class="btn btn-warning me-2">Imported (i)</a>
<a href="tolims.php?id=<?= $template_id ?>" class="btn btn-success me-2">To LIMS (l)</a>
<a href="errors.php?id=<?= $template_id ?>" class="btn btn-danger">Errors (x)</a>
<span class="ms-3">
<label class="form-check-label" style="font-size:13px; cursor:pointer;">
<?php $tp = $_GET;
unset($tp['all_users']);
$toggleBase = 'errors.php?' . http_build_query($tp); ?>
<input type="checkbox" class="form-check-input" <?= $show_all_users ? 'checked' : '' ?>
onchange="window.location.href='<?= htmlspecialchars($toggleBase) ?>' + (this.checked ? '&all_users=1' : '')">
Show all users
</label>
</span>
<span class="text-muted" style="font-size:12px;">(<?= count($rowsData) ?> of <?= $totalRows ?> errori<?= !$show_all_users ? ' — my records' : '' ?>)</span>
</div>
<?php if ($totalPages > 1):
$pageQuery = $_GET;
unset($pageQuery['page']);
$pageBase = 'errors.php?' . http_build_query($pageQuery); ?>
<div class="pager-bar">
<span>Pagina <?= $page ?> di <?= $totalPages ?> — <?= $totalRows ?> righe</span>
<span>
<?php for ($p = 1; $p <= $totalPages; $p++): ?>
<a href="<?= htmlspecialchars($pageBase . '&page=' . $p) ?>" class="pager-num <?= $p === $page ? 'active' : '' ?>"><?= $p ?></a>
<?php endfor; ?>
</span>
</div>
<?php endif; ?>
<div class="card radius-10">
<div class="card-header">
<span style="font-weight:600;"><i class="fas fa-triangle-exclamation" style="color:#dc3545;"></i> Export falliti verso il LIMS</span>
</div>
<div class="card-body">
<?php if (empty($rowsData)): ?>
<p class="text-muted mb-0">Nessun errore di export. 🎉</p>
<?php else: ?>
<div style="overflow-x:auto;">
<table class="err-table">
<thead>
<tr>
<th style="width:170px;">Azioni</th>
<th>Status</th>
<th>Client</th>
<th>Errore</th>
<th style="width:150px;">Data export</th>
<th>File import</th>
</tr>
</thead>
<tbody>
<?php foreach ($rowsData as $row):
$errMsg = extractErrorSummary($logDir, $row['log_file'] ?? null); ?>
<tr data-id="<?= (int)$row['iddatadb'] ?>">
<td>
<button type="button" class="action-btn btn-resend resend-btn" data-iddatadb="<?= (int)$row['iddatadb'] ?>" title="Reinvia al LIMS"><i class="fas fa-paper-plane"></i></button>
<?php if (!empty($row['log_file'])): ?>
<a href="download_logs.php?iddatadb=<?= (int)$row['iddatadb'] ?>" class="action-btn btn-log" title="Scarica log"><i class="fas fa-file-download"></i></a>
<?php endif; ?>
<button type="button" class="action-btn btn-revert revert-btn" data-iddatadb="<?= (int)$row['iddatadb'] ?>" title="Riporta a Imported"><i class="fas fa-rotate-left"></i></button>
</td>
<td>
<span class="status-badge status-x">Error</span>
<?php if (!empty($row['commessaweb'])): ?>
<span class="commessa-code"><?= htmlspecialchars($row['commessaweb']) ?></span>
<?php endif; ?>
</td>
<td><span class="client-text" data-client-id="<?= htmlspecialchars((string)($row['idclient'] ?? '')) ?>"><?= htmlspecialchars((string)($row['idclient'] ?? '')) ?></span></td>
<td class="err-msg"><?= htmlspecialchars($errMsg ?: '—') ?></td>
<td><?= htmlspecialchars((string)($row['export_date'] ?? '')) ?></td>
<td><?php if (!empty($row['filename_import'])): ?><a href="imported_trf/<?= htmlspecialchars($row['filename_import']) ?>" target="_blank">File</a><?php endif; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(function() {
// Risoluzione nomi cliente (come in tolims.php)
(function() {
const ids = new Set();
document.querySelectorAll('.client-text').forEach(el => {
const id = el.dataset.clientId;
if (id) ids.add(id);
});
ids.forEach(id => {
fetch('search_clienti.php?id=' + encodeURIComponent(id))
.then(r => r.json())
.then(j => {
const it = (j.results || [])[0];
if (it) document.querySelectorAll('.client-text[data-client-id="' + id + '"]').forEach(el => el.textContent = it.text);
})
.catch(() => {});
});
})();
// Reinvia: richiama export_to_lims.php da capo (create incluso)
$(document).on('click', '.resend-btn', function() {
const $btn = $(this);
const iddatadb = $btn.data('iddatadb');
if (!confirm('Reinviare questa commessa al LIMS? Verrà creata una nuova commessa nel LIMS.')) return;
const original = $btn.html();
$btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
$.ajax({
url: 'export_to_lims.php',
method: 'POST',
dataType: 'json',
data: {
iddatadb: iddatadb
},
success: function(res) {
if (res && res.success) {
const $tr = $('tr[data-id="' + iddatadb + '"]');
$tr.css('background', '#d4edda');
setTimeout(() => $tr.fadeOut(300, () => $tr.remove()), 500);
} else {
alert('Ancora errore: ' + ((res && res.message) ? res.message : 'invio non riuscito'));
$btn.prop('disabled', false).html(original);
}
},
error: function() {
alert('Errore di rete durante il reinvio.');
$btn.prop('disabled', false).html(original);
}
});
});
// Revert a Imported (i)
$(document).on('click', '.revert-btn', function() {
const $btn = $(this);
const iddatadb = $btn.data('iddatadb');
if (!confirm('Riportare questo record a Imported (i)?')) return;
$.ajax({
url: 'revert_status.php',
method: 'POST',
dataType: 'json',
data: {
iddatadb: iddatadb,
status: 'i'
},
success: function(res) {
if (res && res.success) {
const $tr = $('tr[data-id="' + iddatadb + '"]');
$tr.fadeOut(300, () => $tr.remove());
} else {
alert('Errore: ' + ((res && res.message) ? res.message : 'operazione non riuscita'));
}
},
error: function() {
alert('Errore di rete.');
}
});
});
});
</script>
</body>
</html>
+98 -2
View File
@@ -24,6 +24,24 @@
if (overlay) overlay.style.display = "none";
}
function escHtml(v) {
return String(v ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// Badge "No price" con tooltip dei nomi analisi (vuoto se count = 0)
function noPriceBadgeHTML(count, names) {
const n = parseInt(count, 10) || 0;
if (n <= 0) return "";
const title = (names || []).join("\n");
return `<span class="badge bg-danger no-price-badge" title="${escHtml(title)}" style="margin-left:6px; font-size:0.85em; cursor:help; vertical-align:middle;"><i class="fas fa-exclamation-triangle"></i> No price (${n})</span>`;
}
window.noPriceBadgeHTML = noPriceBadgeHTML;
function getGridRow(iddatadb) {
return document.querySelector(`.grid-row[data-id="${iddatadb}"]`);
}
@@ -34,6 +52,29 @@
);
}
// Invia la mail di riepilogo per gli iddatadb processati (fire-and-forget).
// Una sola chiamata a fine export (singolo o batch).
function sendExportSummaryMail(iddatadbList) {
const ids = (iddatadbList || [])
.map((v) => parseInt(v, 10))
.filter(Boolean);
if (!ids.length) return;
const body = new URLSearchParams();
ids.forEach((id) => body.append("ids[]", id));
const tid = window.gridMeta && window.gridMeta.templateId;
if (tid) body.append("template_id", tid);
fetch("send_export_summary.php", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: body.toString(),
})
.then((r) => r.json())
.then((res) => console.log("Summary mail:", res))
.catch((err) => console.warn("Summary mail failed:", err));
}
// ── Validation ──────────────────────────────────────────────────────
async function validateRows(rowsToValidate) {
@@ -142,6 +183,9 @@
if (idx >= 0) {
window.gridData[idx].status = "l";
window.gridData[idx].commessaweb = data.commessaweb;
window.gridData[idx].noPriceCount = data.noPriceCount || 0;
window.gridData[idx].noPriceAnalyses =
data.noPriceAnalyses || [];
}
// Update visible DOM row
@@ -168,6 +212,11 @@
statusCell.appendChild(cwSpan);
}
cwSpan.textContent = data.commessaweb;
const npHtml = noPriceBadgeHTML(
data.noPriceCount,
data.noPriceAnalyses,
);
if (npHtml) cwSpan.insertAdjacentHTML("beforeend", npHtml);
}
const exportBtn = gridRow.querySelector(".export-lims-btn");
if (exportBtn) {
@@ -195,8 +244,17 @@
msg.innerHTML =
`${data.message.replace(/\n/g, "<br>")}` +
`<br>ID CommessaWeb: ${data.idcommessaweb}` +
`<br>Codice CommessaWeb: ${data.commessaweb}` +
(data.totalPhotos > 0 ? `<br>Foto: ${data.totalPhotos}` : "");
`<br>Codice CommessaWeb: ${escHtml(data.commessaweb)}` +
noPriceBadgeHTML(data.noPriceCount, data.noPriceAnalyses) +
(data.totalPhotos > 0 ? `<br>Foto: ${data.totalPhotos}` : "") +
(data.noPriceCount > 0
? `<div class="mt-2 small text-danger fw-semibold">Analisi senza prezzo:</div>` +
`<ul class="mb-0 small text-danger">` +
(data.noPriceAnalyses || [])
.map((n) => `<li>${escHtml(n)}</li>`)
.join("") +
`</ul>`
: "");
label.textContent = "Esportazione Completata";
} else {
msg.textContent = `Errore: ${data.message}`;
@@ -386,6 +444,8 @@
data.message || "Unknown error",
);
showExportResult(data);
// Mail di riepilogo per la singola commessa
sendExportSummaryMail([iddatadb]);
} catch (error) {
if (gridRow) {
setRowExporting(gridRow, false);
@@ -633,6 +693,8 @@
let processed = 0,
succeeded = 0,
failed = 0;
const exportedList = []; // { commessaweb, noPriceCount, noPriceAnalyses }
const processedIds = []; // iddatadb processati (ok o errore) per la mail
disableAllRowButtons();
@@ -666,11 +728,17 @@
const gridRow = row || getGridRow(iddatadb);
if (gridRow) setRowExporting(gridRow, true);
processedIds.push(iddatadb);
try {
const data = await sendExport(iddatadb, batchUuid);
processed++;
if (data.success) {
succeeded++;
exportedList.push({
commessaweb: data.commessaweb,
noPriceCount: data.noPriceCount || 0,
noPriceAnalyses: data.noPriceAnalyses || [],
});
} else {
failed++;
const errIdx = getRowIndexByIddatadb(iddatadb);
@@ -703,6 +771,9 @@
enableAllRowButtons();
if (bar) bar.style.display = "none";
// Mail di riepilogo unica per tutto il batch processato
sendExportSummaryMail(processedIds);
// Re-render to reflect status changes, then restore error indicators
const gr = window.gridRenderer;
if (gr) gr.renderVisibleRows();
@@ -730,6 +801,31 @@
msgEl.innerHTML = `Exported: <strong>${succeeded}</strong><br>Errors: <strong>${failed}</strong>`;
}
if (exportedList.length) {
const npCommesse = exportedList.filter(
(x) => x.noPriceCount > 0,
).length;
msgEl.innerHTML +=
`<hr class="my-2">` +
(npCommesse > 0
? `<div class="small text-danger fw-semibold mb-1"><i class="fas fa-exclamation-triangle"></i> ${npCommesse} commesse con analisi senza prezzo</div>`
: "") +
`<div class="small fw-semibold mb-1">Commesse esportate:</div>` +
`<ul class="mb-0 ps-3">` +
exportedList
.map(
(x) =>
`<li><span style="font-family:monospace;">${escHtml(x.commessaweb || "—")}</span>` +
noPriceBadgeHTML(
x.noPriceCount,
x.noPriceAnalyses,
) +
`</li>`,
)
.join("") +
`</ul>`;
}
const modalEl = document.getElementById("exportResponseModal");
new bootstrap.Modal(modalEl, { keyboard: false }).show();
modalEl.addEventListener("hidden.bs.modal", cleanupBackdrop, {
+100 -26
View File
@@ -55,22 +55,29 @@ $apiBaseUrl = 'https://93.43.5.102/limsapi/api/odata/';
// 🔹 Batch UUID — if present, all logs go to a single file
$batchUuid = $_POST['batch_uuid'] ?? null;
$writeLog = (function () use ($batchUuid, $logDir) {
$batchLogFile = $batchUuid ? $logDir . "batch_export_{$batchUuid}.log" : null;
// File unico per questa commessa (nominato per iddatadb, che conosciamo sempre)
$iddatadbForLog = $_POST['iddatadb'] ?? 'unknown';
$commessaLogFile = $logDir . "commessa_{$iddatadbForLog}_" . time() . ".log";
return function ($individualPath, $content, $stepLabel = null) use ($batchLogFile) {
if ($batchLogFile) {
$header = "\n" . str_repeat("=", 60) . "\n";
if ($stepLabel) {
$header .= "[{$stepLabel}] " . date('Y-m-d H:i:s') . "\n";
}
$header .= str_repeat("=", 60) . "\n";
file_put_contents($batchLogFile, $header . $content . "\n", FILE_APPEND);
} else {
file_put_contents($individualPath, $content);
}
};
})();
// File di lotto: resta come INDICE (quali commesse nel lotto), non più tutti gli step
$batchIndexFile = $batchUuid ? $logDir . "batch_export_{$batchUuid}.log" : null;
// $writeLog ora appende sempre sul file unico della commessa, con header di step
$writeLog = function ($individualPath, $content, $stepLabel = null) use ($commessaLogFile) {
$header = "\n" . str_repeat("=", 60) . "\n";
if ($stepLabel) {
$header .= "[{$stepLabel}] " . date('Y-m-d H:i:s') . "\n";
}
$header .= str_repeat("=", 60) . "\n";
file_put_contents($commessaLogFile, $header . $content . "\n", FILE_APPEND);
};
// Helper per registrare una riga nell'indice di lotto
$writeBatchIndex = function ($line) use ($batchIndexFile) {
if ($batchIndexFile) {
file_put_contents($batchIndexFile, "[" . date('Y-m-d H:i:s') . "] " . $line . "\n", FILE_APPEND);
}
};
// 🔹 Funzione per validare e convertire date
function validateDate($value)
@@ -272,11 +279,20 @@ try {
return strcmp((string)($a['field_label'] ?? ''), (string)($b['field_label'] ?? ''));
});
// Valori "vuoti convenzionali" da NON includere nelle note.
// Il cliente usa "/" (e simili) come placeholder di campo vuoto.
$addnotePlaceholders = ['/', '-', 'n/a', 'na', 'n.a.', '//'];
foreach ($addnoteRows as $r) {
$val = trim((string)($r['field_value'] ?? ''));
if ($val !== '') {
$addnoteChunks[] = $val;
if ($val === '') {
continue;
}
// Scarta i placeholder (confronto case-insensitive)
if (in_array(mb_strtolower($val), $addnotePlaceholders, true)) {
continue;
}
$addnoteChunks[] = $val;
}
$addnoteText = implode(' - ', $addnoteChunks);
@@ -310,7 +326,7 @@ try {
"AnagraficaCertestService" => $anagraficaService,
"ClienteFornitore" => $clienteFornitore, // PLACEHOLDER — to be implemented
"ClienteAnalisi" => $clienteAnalisi, // PLACEHOLDER — to be implemented
// "ClienteFatturazione" => $clienteFatturazione, // ⚠️ TEMP DISABLED - problemi API lato LIMS
"ClienteFatturazione" => $clienteFatturazione, // ⚠️ TEMP DISABLED - problemi API lato LIMS
// DeliveryRequest goes to Campione, not CommessaWeb
];
@@ -569,7 +585,7 @@ try {
// 🔹 STEP 6.3: Add Analyses (AnalisiCampione) via Campione({id})/AddAnalisi bound action
$stmt = $pdo->prepare("
SELECT part_id, analysis_recordkey, analysis_name, analysis_method
SELECT part_id, analysis_recordkey, analysis_name, analysis_method, analysis_price
FROM identification_parts_analyses
WHERE iddatadb = :iddatadb
ORDER BY part_id, id
@@ -577,6 +593,17 @@ try {
$stmt->execute(['iddatadb' => $iddatadb]);
$analysesRows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Analisi senza prezzo (NULL o 0), una sola volta per recordkey
$noPriceAnalyses = [];
foreach ($analysesRows as $a) {
$price = $a['analysis_price'];
if ($price === null || (float)$price <= 0) {
$rk = (string)($a['analysis_recordkey'] ?? '');
$noPriceAnalyses[$rk] = $a['analysis_name'] ?: 'Unnamed analysis';
}
}
$noPriceAnalyses = array_values($noPriceAnalyses);
$partIdToIndex = [];
foreach ($parts as $idx => $part) {
$partIdToIndex[(int)$part['part_id']] = $idx;
@@ -684,16 +711,20 @@ try {
}
}
// 🔹 STEP 8: Update datadb with idcommessaweb, commessaweb, status and export date
// 🔹 STEP 8: Update datadb with idcommessaweb, commessaweb, log_file and export date
// NB: lo status 'l' viene impostato SOLO dopo invio+import ok (vedi più sotto),
// così una riga rimasta indietro non risulta "To LIMS" per errore.
$stmt = $pdo->prepare("
UPDATE datadb
SET idcommessaweb = :idcommessaweb, commessaweb = :commessaweb, status = 'l', export_date = NOW()
SET idcommessaweb = :idcommessaweb, commessaweb = :commessaweb,
log_file = :log_file, export_date = NOW()
WHERE iddatadb = :iddatadb
");
$stmt->execute([
'idcommessaweb' => $commessaId,
'commessaweb' => $commessaWebCode,
'iddatadb' => $iddatadb
'commessaweb' => $commessaWebCode,
'log_file' => basename($commessaLogFile),
'iddatadb' => $iddatadb
]);
// 🔹 STEP 9: Send CommessaWeb to laboratory (commentato come richiesto)
@@ -761,9 +792,12 @@ try {
");
$stmt->execute([
'commessaweb' => substr($finalCodiceCommessa, 0, 30),
'iddatadb' => $iddatadb
'iddatadb' => $iddatadb
]);
}
// Indice di lotto: esito OK di questa commessa
$writeBatchIndex("OK iddatadb={$iddatadb} commessa={$commessaId} codice={$finalCodiceCommessa} log=" . basename($commessaLogFile));
// 🔹 STEP 11: Prepare final response
$finalCommessa = [
"Cliente" => $clienteId,
@@ -787,6 +821,8 @@ try {
"totalAnalyses" => $totalAnalyses,
"addedAnalyses" => $addedAnalyses,
"failedAnalyses" => $failedAnalyses,
"noPriceCount" => count($noPriceAnalyses),
"noPriceAnalyses" => $noPriceAnalyses,
"message" => "Export successful",
"logFiles" => [
"step5_create" => $logFileStep5,
@@ -799,11 +835,49 @@ try {
]
]);
} catch (Exception $e) {
// --- LOG ERRORE DEDICATO (accanto agli altri log in logs/api/) ---
$errIddatadb = $iddatadb ?? 'unknown';
$errCommessaId = $commessaId ?? null;
$errContent = "TIMESTAMP: " . date('Y-m-d H:i:s') . "\n";
$errContent .= "iddatadb: {$errIddatadb}\n";
$errContent .= "IdCommessa: " . ($errCommessaId ?? 'non ancora creata') . "\n";
$errContent .= "MESSAGE: " . $e->getMessage() . "\n\n";
$errContent .= "TRACE:\n" . $e->getTraceAsString() . "\n";
// Appendi l'errore in coda al file unico della commessa (se esiste),
// così resta tutto in un posto solo.
if (isset($commessaLogFile)) {
$header = "\n" . str_repeat("=", 60) . "\n[ERROR] " . date('Y-m-d H:i:s') . "\n" . str_repeat("=", 60) . "\n";
@file_put_contents($commessaLogFile, $header . $errContent . "\n", FILE_APPEND);
$errFileToSave = basename($commessaLogFile);
} else {
// fallback: file d'errore dedicato
$errName = "commessa_ERROR_{$errIddatadb}_" . time() . ".txt";
@file_put_contents($logDir . $errName, $errContent);
$errFileToSave = $errName;
}
// Segna la riga come errore ('x') e registra il log_file, senza lasciarla 'l'
if ($errIddatadb !== 'unknown') {
try {
$up = $pdo->prepare("UPDATE datadb SET status = 'x', log_file = :log_file WHERE iddatadb = :iddatadb");
$up->execute(['log_file' => $errFileToSave, 'iddatadb' => $errIddatadb]);
} catch (Exception $ignore) { /* non bloccare la risposta d'errore */
}
}
// Indice di lotto: esito KO di questa commessa
if (isset($writeBatchIndex)) {
$writeBatchIndex("FAIL iddatadb={$errIddatadb} commessa=" . ($errCommessaId ?? '-') . " log={$errFileToSave} :: " . $e->getMessage());
}
error_log("LIMS Export Error: " . $e->getMessage() . "\nTrace: " . $e->getTraceAsString());
echo json_encode([
"success" => false,
"message" => "Export failed: " . $e->getMessage(),
"success" => false,
"message" => "Export failed: " . $e->getMessage(),
"errorLog" => $logDir . $errFileToSave,
"logFiles" => [
"step5_create" => $logFileStep5 ?? null,
"step5_2_photos" => $logFilePhotos ?? null,
+9 -3
View File
@@ -39,8 +39,14 @@ if (!empty($explicitIds)) {
// Scoping di sicurezza: solo record del template/utente/import corretti.
$conds = ['d.templateid = ?', 'd.status = ?'];
$params = [$templateId, $status];
if (!$showAll) { $conds[] = 'd.user_id = ?'; $params[] = $userId; }
if ($importref !== '') { $conds[] = 'd.importreferencecode = ?'; $params[] = $importref; }
if (!$showAll) {
$conds[] = 'd.user_id = ?';
$params[] = $userId;
}
if ($importref !== '') {
$conds[] = 'd.importreferencecode = ?';
$params[] = $importref;
}
$ph = implode(',', array_fill(0, count($explicitIds), '?'));
$conds[] = "d.iddatadb IN ($ph)";
foreach ($explicitIds as $id) $params[] = $id;
@@ -57,7 +63,7 @@ if (!empty($explicitIds)) {
$idStmt = $pdo->prepare("
SELECT d.iddatadb FROM datadb d
WHERE {$whereSql}
ORDER BY d.excelrow ASC, d.iddatadb ASC
ORDER BY CAST(d.excelrow AS UNSIGNED) ASC, d.excelrow ASC, d.iddatadb ASC
LIMIT {$perPage} OFFSET {$offset}
");
$idStmt->execute($params);
@@ -40,7 +40,7 @@ try {
$data = null;
$fromCache = false;
if (!$simulate && !$refresh && file_exists($cache_file) && (time() - filemtime($cache_file) < 3600)) { // 1 ora
if (!$simulate && !$refresh && file_exists($cache_file) && (time() - filemtime($cache_file) < 7200)) { // 1 ora
$data = json_decode(file_get_contents($cache_file), true);
$fromCache = is_array($data);
}
+1 -1
View File
@@ -76,7 +76,7 @@ try {
// Cache file (1 hour TTL)
$cacheFile = __DIR__ . '/cache/clienti.json';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 7200)) {
readfile($cacheFile);
exit;
}
+1 -1
View File
@@ -101,7 +101,7 @@ try {
// Cache file (1 hour TTL)
$cacheFile = __DIR__ . '/cache/clienti.json';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 7200)) {
readfile($cacheFile);
exit;
}
+1 -1
View File
@@ -76,7 +76,7 @@ try {
// Cache file (1 hour TTL)
$cacheFile = __DIR__ . '/cache/clienti.json';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 7200)) {
readfile($cacheFile);
exit;
}
+1 -1
View File
@@ -28,7 +28,7 @@ try {
$cacheFile = $cacheDir . '/customfield_' . $customFieldId . '.json';
// Use cache if fresh (1 hour)
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 7200)) {
$results[$customFieldId] = json_decode(file_get_contents($cacheFile), true);
continue;
}
+1 -1
View File
@@ -67,7 +67,7 @@ try {
}
// Caching skipped in simulate mode to avoid polluting real-data cache files
if (!$simulate && $cache_file && file_exists($cache_file) && (time() - filemtime($cache_file) < 3600)) { // 1 ora
if (!$simulate && $cache_file && file_exists($cache_file) && (time() - filemtime($cache_file) < 7200)) { // 1 ora
$data = json_decode(file_get_contents($cache_file), true);
} else {
$data = $api->get($endpoint, $options);
+6 -2
View File
@@ -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,7 @@ error_reporting(E_ALL);
try {
// Cache file (1 hour TTL)
$cacheFile = __DIR__ . '/cache/matrici.json';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 7200)) {
readfile($cacheFile);
exit;
}
+92 -4
View File
@@ -588,7 +588,14 @@
: "To LIMS";
let html = `<span class="status-badge status-${esc(st)}">${label}</span>`;
if (row.commessaweb) {
html += `<span class="commessaweb-code" style="display:block; font-size:0.75em; color:#555; margin-top:2px;">${esc(row.commessaweb)}</span>`;
const npBadge =
typeof window.noPriceBadgeHTML === "function"
? window.noPriceBadgeHTML(
row.noPriceCount,
row.noPriceAnalyses,
)
: "";
html += `<span class="commessaweb-code" style="display:block; font-size:0.75em; color:#555; margin-top:2px;">${esc(row.commessaweb)}${npBadge}</span>`;
}
div.innerHTML = html;
div.classList.remove("editable-cell");
@@ -895,6 +902,76 @@
updateDirtyIndicator();
}
// Aggiorna in-place SOLO le celle di una colonna in tutte le righe visibili,
// senza distruggere/ricostruire l'intera griglia (evita il freeze in propagazione).
function updateColumnCells(colKey, colType) {
if (!rowContainer) return;
const cells = rowContainer.querySelectorAll(
`.grid-cell[data-col="${CSS.escape(String(colKey))}"][data-col-type="${colType}"]`,
);
cells.forEach((cell) => {
const rowIndex = parseInt(cell.dataset.row, 10);
if (Number.isNaN(rowIndex) || !data[rowIndex]) return;
// Ricava il valore corrente dal modello dati
let value = "";
if (colType === "detail" || colType === "main_field") {
value = data[rowIndex].details?.[String(colKey)] ?? "";
} else if (colType === "fixed") {
value = data[rowIndex].fixedFields?.[colKey] ?? "";
} else if (colType === "cliente_fornitore_id") {
value = data[rowIndex].cliente_fornitore_id ?? "";
}
const input = cell.querySelector(".cell-input");
if (!input) return;
if ($(input).hasClass("select2-hidden-accessible")) {
// Assicura che l'opzione selezionata esista, poi imposta il valore
if (
value &&
!input.querySelector(
`option[value="${CSS.escape(String(value))}"]`,
)
) {
const label =
clientNameCache[value] ||
(input.dataset?.fieldId
? dropdownNameCache[
input.dataset.fieldId + "_" + value
]
: null) ||
value;
input.add(new Option(label, value, true, true));
}
$(input)
.val(value ? String(value) : "")
.trigger("change.select2");
} else if (input.tagName === "SELECT") {
// Select nativo: aggiungi opzione se manca
if (
value &&
!input.querySelector(
`option[value="${CSS.escape(String(value))}"]`,
)
) {
const label =
(input.dataset?.fieldId
? dropdownNameCache[
input.dataset.fieldId + "_" + value
]
: null) || value;
input.add(new Option(label, value, true, true));
}
input.value = value;
} else {
input.value = value;
}
cell.classList.add("cell-changed");
});
}
function renderSingleRow(rowIndex) {
const existing = rowContainer.querySelector(
`.grid-row[data-id="${data[rowIndex].iddatadb}"]`,
@@ -1546,7 +1623,10 @@
row._dirty = true;
});
renderVisibleRows();
updateColumnCells(
"cliente_fornitore_id",
"cliente_fornitore_id",
);
updateDirtyIndicator();
return;
}
@@ -1565,7 +1645,7 @@
row._dirty = true;
});
renderVisibleRows();
updateColumnCells(fixedKey, "fixed");
updateDirtyIndicator();
return;
}
@@ -1595,7 +1675,15 @@
row._dirty = true;
});
renderVisibleRows();
updateColumnCells(col.key, col.type);
// Se esiste una colonna auto-copiata (es. Arrival→Unlock), aggiorna anche quella
if (autoPair) {
const pairCol = columns.find(
(c) => String(c.key) === String(autoPair.toKey),
);
if (pairCol) updateColumnCells(pairCol.key, pairCol.type);
}
updateDirtyIndicator();
return;
}
+89 -1
View File
@@ -12,6 +12,7 @@ if (!file_exists(__DIR__ . '/import_debug.log')) {
error_log("Inizio importazione alle " . date('Y-m-d H:i:s'));
include('include/headscript.php');
require_once __DIR__ . '/class/binding-functions.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['template_id']) || !isset($_POST['selected_rows']) || !isset($_POST['filename'])) {
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Richiesta non valida"));
@@ -70,7 +71,7 @@ $pdo = $db->getConnection();
$importReferenceCode = date('YmdHis') . '-' . uniqid();
// Recupera tutti i mapping dal template
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field, auto_value FROM template_mapping WHERE template_id = ?");
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, has_list, is_required, manual_default, is_manual, field_label, field_id, main_field, auto_value FROM template_mapping WHERE template_id = ?");
$stmt->execute([$template_id]);
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -88,6 +89,23 @@ foreach ($allMappings as $mapping) {
}
}
$fixedMappings = [];
try {
$fxStmt = $pdo->prepare(
"SELECT id, fixed_field_key, data_type, default_value, default_source, excel_column
FROM template_fixed_mapping
WHERE template_id = ?"
);
$fxStmt->execute([$template_id]);
$fixedMappings = $fxStmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
error_log("[FIXED] lettura template_fixed_mapping fallita: " . $e->getMessage());
}
$savedBindings = []; // risolti con un binding gia' salvato
$autoBindings = []; // collegati in automatico (corrispondenza esatta)
$pendingBindings = []; // senza corrispondenza: da risolvere a mano
// Inserisci le righe selezionate in datadb
$insertedIds = [];
foreach ($selected_rows as $rowIndex) {
@@ -172,6 +190,24 @@ foreach ($selected_rows as $rowIndex) {
}
}
if (
binding_is_list_field($mapping)
&& $fieldValue !== null
&& trim((string) $fieldValue) !== ''
) {
$fieldValue = binding_resolve_for_import(
$pdo,
$mapping,
(int) $template_id,
(string) $fieldValue,
(int) $iddatadb,
(int) $user_id,
$savedBindings,
$autoBindings,
$pendingBindings
);
}
if ($mapping['is_required'] && (is_null($fieldValue) || $fieldValue === '')) {
error_log("Required field missing for mapping ID: " . $mapping['id'] . ", field: " . $mapping['field_label']);
}
@@ -180,6 +216,43 @@ foreach ($selected_rows as $rowIndex) {
$stmt->execute([$iddatadb, $mapping['id'], $fieldValue]);
error_log("Inserted into import_data_details for ID $iddatadb, Mapping ID: " . $mapping['id'] . ", Field Value: " . var_export($fieldValue, true));
}
$fixedValues = [];
foreach ($fixedMappings as $fm) {
$fixedKey = (string) $fm['fixed_field_key'];
$value = null;
if (($fm['default_source'] ?? 'manual') === 'xls' && trim((string) ($fm['excel_column'] ?? '')) !== '') {
$idx = binding_find_column_index((string) $fm['excel_column'], $columns);
$raw = ($idx !== false && isset($row[$idx])) ? trim((string) $row[$idx]) : '';
if ($raw !== '') {
$value = binding_resolve_fixed_for_import(
$pdo,
$fm,
(int) $template_id,
$raw,
(int) $iddatadb,
(int) $user_id,
$savedBindings,
$autoBindings,
$pendingBindings
);
}
}
if ($value === null || $value === '') {
$default = binding_fixed_default($fm);
$value = $default !== '' ? $default : null;
}
if ($value !== null && $value !== '') {
$fixedValues[$fixedKey] = $value;
}
}
if (!empty($fixedValues)) {
binding_write_fixed_columns($pdo, (int) $iddatadb, $fixedValues);
}
}
// ------------------------------------------------------------
@@ -237,5 +310,20 @@ try {
$_SESSION['inserted_ids'] = $insertedIds;
if (!empty($pendingBindings)) {
$_SESSION['pending_bindings'] = [
'template_id' => $template_id,
'importref' => $importReferenceCode,
'items' => array_values($pendingBindings),
'auto' => array_values($autoBindings),
'saved' => array_values($savedBindings),
];
header("Location: resolve_bindings.php");
exit;
}
unset($_SESSION['pending_bindings']);
header("Location: imported.php?id=" . urlencode($template_id) . "&importref=" . urlencode($importReferenceCode));
exit;
+95 -3
View File
@@ -5,6 +5,7 @@ ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/import_debug.log');
include('include/headscript.php');
require_once __DIR__ . '/class/binding-functions.php';
header('Content-Type: application/json');
@@ -49,7 +50,7 @@ $db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Recupera i mapping
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field, auto_value FROM template_mapping WHERE template_id = ?");
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, has_list, is_required, manual_default, is_manual, field_label, field_id, main_field, auto_value FROM template_mapping WHERE template_id = ?");
$stmt->execute([$template_id]);
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -76,6 +77,23 @@ $mappingId244 = (int)$stmtMap->fetchColumn();
$insertedIds = [];
$fixedMappings = [];
try {
$fxStmt = $pdo->prepare(
"SELECT id, fixed_field_key, data_type, default_value, default_source, excel_column
FROM template_fixed_mapping
WHERE template_id = ?"
);
$fxStmt->execute([$template_id]);
$fixedMappings = $fxStmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
error_log("[FIXED] lettura template_fixed_mapping fallita: " . $e->getMessage());
}
$savedBindings = [];
$autoBindings = [];
$pendingBindings = [];
try {
$pdo->beginTransaction();
@@ -146,9 +164,65 @@ try {
}
}
if (
binding_is_list_field($mapping)
&& $fieldValue !== null
&& trim((string) $fieldValue) !== ''
) {
$fieldValue = binding_resolve_for_import(
$pdo,
$mapping,
$template_id,
(string) $fieldValue,
(int) $iddatadb,
(int) $user_id,
$savedBindings,
$autoBindings,
$pendingBindings
);
}
$insDetail->execute([$iddatadb, $mapping['id'], $fieldValue]);
}
$fixedValues = [];
foreach ($fixedMappings as $fm) {
$fixedKey = (string) $fm['fixed_field_key'];
$value = null;
if (($fm['default_source'] ?? 'manual') === 'xls' && trim((string) ($fm['excel_column'] ?? '')) !== '') {
$idx = binding_find_column_index((string) $fm['excel_column'], $columns);
$raw = ($idx !== false && isset($row[$idx])) ? trim((string) $row[$idx]) : '';
if ($raw !== '') {
$value = binding_resolve_fixed_for_import(
$pdo,
$fm,
(int) $template_id,
$raw,
(int) $iddatadb,
(int) $user_id,
$savedBindings,
$autoBindings,
$pendingBindings
);
}
}
if ($value === null || $value === '') {
$default = binding_fixed_default($fm);
$value = $default !== '' ? $default : null;
}
if ($value !== null && $value !== '') {
$fixedValues[$fixedKey] = $value;
}
}
if (!empty($fixedValues)) {
binding_write_fixed_columns($pdo, (int) $iddatadb, $fixedValues);
}
// Campo 244 (Accettatore) con lims_user_id
if ($limsUserId !== '' && $mappingId244 > 0) {
$insDetail->execute([$iddatadb, $mappingId244, $limsUserId]);
@@ -157,9 +231,27 @@ try {
$pdo->commit();
$state = $_SESSION['pending_bindings'] ?? null;
if (!is_array($state) || ($state['importref'] ?? null) !== $importReferenceCode) {
$state = [
'template_id' => $template_id,
'importref' => $importReferenceCode,
'items' => [],
'auto' => [],
'saved' => [],
];
}
$state['items'] = binding_merge_buckets($state['items'], $pendingBindings);
$state['auto'] = binding_merge_buckets($state['auto'], $autoBindings);
$state['saved'] = binding_merge_buckets($state['saved'], $savedBindings);
$_SESSION['pending_bindings'] = $state;
echo json_encode([
'ok' => true,
'inserted' => count($insertedIds)
'ok' => true,
'inserted' => count($insertedIds),
'pending_bindings' => count($state['items'])
]);
exit;
} catch (Exception $e) {
+6 -1
View File
@@ -164,6 +164,7 @@ error_log("Loaded template: " . print_r($template, true));
<div class="mb-3 text">
<a href="imported.php?id=<?= $id ?>" class="btn btn-warning me-2">Imported (i)</a>
<a href="tolims.php?id=<?= $id ?>" class="btn btn-success">To LIMS (l)</a>
<a href="errors.php?id=<?= $id ?>" class="btn btn-danger">Errors (x)</a>
</div>
<div class="card radius-10">
<div class="card-header">
@@ -492,6 +493,7 @@ error_log("Loaded template: " . print_r($template, true));
const progressErr = document.getElementById('importProgressError');
let insertedTotal = 0;
let pendingBindings = 0; // valori a lista senza corrispondenza LIMS
// Invia un batch e ritorna la risposta JSON
async function sendBatch(startIndex) {
@@ -550,6 +552,7 @@ error_log("Loaded template: " . print_r($template, true));
}
insertedTotal += (result.inserted || 0);
pendingBindings = result.pending_bindings || pendingBindings;
const done = Math.min(start + BATCH_SIZE, total);
const pct = Math.round((done / total) * 100);
progressBar.style.width = pct + '%';
@@ -565,7 +568,9 @@ error_log("Loaded template: " . print_r($template, true));
// PAUSA temporanea di 2 secondi per vedere la barra completa (da rimuovere dopo il test)
await new Promise(r => setTimeout(r, 2000));
window.location.href = 'imported.php?id=' +
window.location.href = pendingBindings > 0
? 'resolve_bindings.php'
: 'imported.php?id=' +
encodeURIComponent(data.template_id) +
'&importref=' + encodeURIComponent(importReferenceCode);
});
+209 -15
View File
@@ -1411,6 +1411,84 @@ $gridMeta = [
background: #fff;
}
/* Card-header azioni sticky sotto la topbar */
:root {
--actions-top: 0px;
/* posizione verticale barra azioni: abbassa/alza questo */
--actions-height: 58px;
/* altezza reale del card-header azioni */
--scrollbar-height: 18px;
/* altezza della .top-scrollbar */
}
.card-header.sticky-actions {
position: sticky;
top: var(--actions-top);
z-index: 40;
background: #fff;
}
/* La barra di scroll superiore segue sempre il card-header sticky */
.top-scrollbar {
top: calc(var(--actions-top) + var(--actions-height)) !important;
}
/* Wrapper del grid-top: sticky verticale + scroll orizzontale proprio */
.grid-top-scroller {
position: sticky;
top: calc(var(--actions-top) + var(--actions-height) + var(--scrollbar-height));
z-index: 25;
background: #fff;
overflow-x: auto;
overflow-y: hidden;
/* nasconde la barra di scroll di questo wrapper: usiamo la top-scrollbar e la griglia */
scrollbar-width: none;
}
.grid-top-scroller::-webkit-scrollbar {
display: none;
}
/* Tastino toggle sticky */
.sticky-toggle-btn {
border: 1px solid #ced4da;
background: #fff;
color: #adb5bd;
transition: all .15s;
}
.sticky-toggle-btn:hover {
background: #f1f3f5;
color: #6c757d;
}
.sticky-toggle-btn.active {
background: #0d6efd;
border-color: #0d6efd;
color: #fff;
}
.sticky-toggle-btn.active:hover {
background: #0b5ed7;
color: #fff;
}
/* Icona ruotata quando sbloccato (puntina "storta") */
.sticky-toggle-btn:not(.active) i {
transform: rotate(45deg);
}
/* Quando sbloccato, card-header e grid-top non sono più sticky verticalmente */
.card-header.actions-unpinned {
position: static !important;
top: auto !important;
}
.grid-top-scroller.actions-unpinned {
position: static !important;
top: auto !important;
}
<?php if (isset($mainFieldMappings) && count($mainFieldMappings) >= 2): ?>.grid-top .grid-cell:nth-child(3) {
position: sticky !important;
left: 360px;
@@ -1452,7 +1530,8 @@ $gridMeta = [
<div class="page-content">
<div class="mb-3 text d-flex align-items-center gap-2">
<a href="imported.php?id=<?= $template_id ?>" class="btn btn-warning me-2">Imported (i)</a>
<a href="tolims.php?id=<?= $template_id ?>" class="btn btn-success">To LIMS (l)</a>
<a href="tolims.php?id=<?= $template_id ?>" class="btn btn-success me-2">To LIMS (l)</a>
<a href="errors.php?id=<?= $template_id ?>" class="btn btn-danger">Errors (x)</a>
<?php if ($importref === ''): ?>
<span class="ms-3">
<label class="form-check-label" style="font-size: 13px; cursor: pointer;">
@@ -1514,8 +1593,9 @@ $gridMeta = [
</div>
<?php endif; ?>
<div class="card radius-10">
<div class="card-header">
<div class="card-header sticky-actions">
<div class="d-flex align-items-center" style="min-height: 42px; gap: 12px;">
<button type="button" id="toggleStickyBtn" class="btn btn-sm sticky-toggle-btn active" style="flex-shrink:0;" title="Blocca/sblocca barra azioni"><i class="fas fa-thumbtack"></i></button>
<a href="#" class="btn btn-outline-danger btn-sm export-all-lims-btn" style="flex-shrink:0;"><i class="fas fa-upload"></i> Export All</a>
<a href="#" class="btn btn-outline-success btn-sm save-all-btn" style="flex-shrink:0;"><i class="fas fa-save"></i> Save All</a>
<button type="button" class="btn btn-outline-success btn-sm" id="addRowBtn" style="flex-shrink:0;"><i class="fas fa-plus"></i> Add Row</button>
@@ -1538,8 +1618,11 @@ $gridMeta = [
<div class="top-scrollbar-inner" id="topScrollbarInner"></div>
</div>
<div class="grid-container" id="gridContainer">
<div class="grid-top-scroller" id="gridTopScroller">
<div class="grid-top" id="gridTopContainer"></div>
</div>
<div class="grid-container" id="gridContainer">
<div class="grid-row" id="gridHeaderContainer" style="font-weight:600;"></div>
<div id="gridRowContainer"></div>
</div>
@@ -1610,21 +1693,43 @@ $gridMeta = [
topScrollbar.scrollLeft = gridContainer.scrollLeft;
}
const gridTopScroller = document.getElementById('gridTopScroller');
let syncing = false;
function syncScroll(source) {
if (syncing) return;
syncing = true;
const x = source.scrollLeft;
if (source !== topScrollbar) topScrollbar.scrollLeft = x;
if (source !== gridContainer) gridContainer.scrollLeft = x;
if (gridTopScroller && source !== gridTopScroller) gridTopScroller.scrollLeft = x;
syncing = false;
}
topScrollbar.addEventListener('scroll', function() {
if (syncingFromGrid) return;
syncingFromTop = true;
gridContainer.scrollLeft = topScrollbar.scrollLeft;
syncingFromTop = false;
syncScroll(topScrollbar);
});
gridContainer.addEventListener('scroll', function() {
if (syncingFromTop) return;
syncingFromGrid = true;
topScrollbar.scrollLeft = gridContainer.scrollLeft;
syncingFromGrid = false;
syncScroll(gridContainer);
});
if (gridTopScroller) {
gridTopScroller.addEventListener('scroll', function() {
syncScroll(gridTopScroller);
});
}
window.addEventListener('resize', updateTopScrollbarWidth);
let rafPending = false;
function scheduleUpdate() {
if (rafPending) return;
rafPending = true;
requestAnimationFrame(function() {
rafPending = false;
updateTopScrollbarWidth();
});
}
window.addEventListener('resize', scheduleUpdate);
// Recalculate after JS grid rendering
setTimeout(updateTopScrollbarWidth, 100);
@@ -1633,13 +1738,13 @@ $gridMeta = [
setTimeout(updateTopScrollbarWidth, 1500);
// Recalculate automatically when rows/header/top controls are rendered or changed
const observer = new MutationObserver(updateTopScrollbarWidth);
const observer = new MutationObserver(scheduleUpdate);
if (gridContainer) {
observer.observe(gridContainer, {
childList: true,
subtree: true,
attributes: true
attributes: false
});
}
});
@@ -1804,7 +1909,96 @@ $gridMeta = [
</div>
</div>
</div>
<div id="sessionModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:99999;align-items:center;justify-content:center">
<div style="background:#fff;padding:28px 32px;border-radius:14px;max-width:420px;text-align:center;box-shadow:0 20px 60px rgba(0,0,0,.3)">
<h5 style="margin:0 0 10px">La sessione sta per scadere</h5>
<p style="color:#64748b;margin:0 0 6px">Per la tua sicurezza verrai disconnesso tra</p>
<div id="sessCountdownBig" style="font:700 34px monospace;color:#dc2626;margin:8px 0 18px">00:30</div>
<button id="sessStayBtn" class="btn btn-primary btn-lg px-4">Resta connesso</button>
</div>
</div>
<script>
(function() {
// ── TEST: tempi compressi. In produzione: SESSION_MINUTES=120, WARN_SECONDS=120
const SESSION_MINUTES = 110; // durata simulata
const WARN_SECONDS = 30; // preavviso prima della scadenza
const LOGIN_URL = '../login'; // adatta al tuo path reale
const sessionMs = SESSION_MINUTES * 60 * 1000;
let lastActivity = Date.now();
let modalShown = false;
// ogni attività reale resetta il timer locale
['mousemove', 'keydown', 'click', 'scroll', 'touchstart'].forEach(ev =>
document.addEventListener(ev, () => {
lastActivity = Date.now();
if (!modalShown) return; // se il modal è aperto, l'attività NON conta: serve il click esplicito
}, {
passive: true
})
);
const modal = document.getElementById('sessionModal');
const bigCd = document.getElementById('sessCountdownBig');
document.getElementById('sessStayBtn').addEventListener('click', async () => {
try {
const r = await fetch('session_keepalive.php', {
cache: 'no-store'
});
// se la sessione era già morta, headscript reindirizza: la risposta non sarà il JSON atteso
const ct = r.headers.get('content-type') || '';
if (!ct.includes('application/json')) {
window.location.href = LOGIN_URL;
return;
}
await r.json();
lastActivity = Date.now(); // timer riparte
modalShown = false;
modal.style.display = 'none';
} catch (e) {
window.location.href = LOGIN_URL;
}
});
setInterval(() => {
const elapsed = Date.now() - lastActivity;
const remaining = Math.ceil((sessionMs - elapsed) / 1000);
if (remaining <= 0) {
window.location.href = LOGIN_URL; // scaduta
return;
}
if (remaining <= WARN_SECONDS && !modalShown) {
modalShown = true;
modal.style.display = 'flex';
}
if (modalShown) {
const m = Math.floor(remaining / 60),
s = remaining % 60;
bigCd.textContent = String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
}
}, 1000);
})();
</script>
<script>
(function() {
const btn = document.getElementById('toggleStickyBtn');
const cardHeader = document.querySelector('.card-header.sticky-actions');
if (!btn || !cardHeader) return;
const gridTopScroller = document.getElementById('gridTopScroller');
btn.addEventListener('click', function() {
const pinned = btn.classList.toggle('active');
cardHeader.classList.toggle('actions-unpinned', !pinned);
if (gridTopScroller) gridTopScroller.classList.toggle('actions-unpinned', !pinned);
btn.title = pinned ?
'Sblocca barra azioni' :
'Blocca barra azioni';
});
})();
</script>
</body>
</html>
+9 -1
View File
@@ -56,4 +56,12 @@ $photousername = basename($avatar);
//include files
require_once(__DIR__ . '/../../languages/en/general.php');
//include("generalsettings.php");
//include("generalsettings.php");
// Timbra la sessione dell'utente a ogni pagina (rolling manuale)
try {
$pdoSess = DBHandlerSelect::getInstance()->getConnection();
$pdoSess->prepare("UPDATE auth_sessions SET last_activity = ? WHERE user_id = ? ORDER BY last_activity DESC LIMIT 1")
->execute([time(), $iduserlogin]);
} catch (\Throwable $e) { /* non bloccare la pagina */
}
+13
View File
@@ -64,6 +64,11 @@
<i class='bx bx-radio-circle'></i><?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?>
</a>
</li>
<li>
<a href="bindings_manage.php">
<i class='bx bx-radio-circle'></i><?= htmlspecialchars($bindingsmanage ?? 'Value Bindings', ENT_QUOTES, 'UTF-8'); ?>
</a>
</li>
</ul>
</li>
<?php endif; ?>
@@ -103,6 +108,14 @@
<div class="menu-title">Stats</div>
</a>
</li>
<li>
<a href="routine_admin.php">
<div class="parent-icon">
<i class="bx bx-bar-chart-alt-2"></i>
</div>
<div class="menu-title">Routines</div>
</a>
</li>
<li>
<a href="warm_cache_run.php">
<div class="parent-icon">
+2
View File
@@ -108,6 +108,8 @@
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item d-flex align-items-center" href="user-profile.php"><i class="bx bx-user fs-5"></i><span>Profile</span></a>
</li>
<li><a class="dropdown-item d-flex align-items-center" href="notification_settings.php"><i class="bx bx-user fs-5"></i><span>Notifications</span></a>
</li>
<?php if ($user->hasRole('Admin') || $user->hasRole('SuperUser')): ?>
<li>
<a class="dropdown-item d-flex align-items-center" href="user-admin.php">
+109 -24
View File
@@ -88,7 +88,8 @@ $mappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Recupera i fixed fields dalla tabella template_fixed_mapping
$stmt = $pdo->prepare("
SELECT id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import
SELECT id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import,
default_source, excel_column
FROM template_fixed_mapping
WHERE template_id = ?
ORDER BY id ASC
@@ -459,9 +460,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
$autoValue = $mapping['auto_value'] ?? 'none';
$hasAuto = ($autoValue && $autoValue !== 'none');
if ($isSceltaMultipla) {
$mappingValue = 'manual';
} elseif ($hasAuto) {
if ($hasAuto && !$isSceltaMultipla) {
$mappingValue = 'auto';
} elseif ($sourceType === 'XLS' && !empty($mapping['excel_column'])) {
$mappingValue = 'xls';
@@ -470,24 +469,26 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
} elseif ((int)$mapping['is_manual'] === 1) {
$mappingValue = 'manual';
} else {
$mappingValue = '';
// SceltaMultipla senza colonna: valore di default dal dropdown
$mappingValue = $isSceltaMultipla ? 'manual' : '';
}
?>
<select class="form-select mapping-select"
data-id="<?php echo (int)$mapping['id']; ?>"
data-field-id="<?php echo (int)$mapping['field_id']; ?>"
<?php echo $isSceltaMultipla ? 'disabled' : ''; ?>>
data-field-id="<?php echo (int)$mapping['field_id']; ?>">
<?php if (!$isSceltaMultipla): ?>
<option value="">Select Option</option>
<?php endif; ?>
<?php if ($sourceType === 'XLS'): ?>
<option value="xls" <?php echo ($mappingValue === 'xls') ? 'selected' : ''; ?>>Map to XLS Column</option>
<?php elseif ($sourceType === 'API'): ?>
<option value="json" <?php echo ($mappingValue === 'json') ? 'selected' : ''; ?>>Map to JSON Node</option>
<?php endif; ?>
<?php if ($sourceType === 'XLS'): ?>
<option value="xls" <?php echo ($mappingValue === 'xls') ? 'selected' : ''; ?>>Map to XLS Column</option>
<?php elseif ($sourceType === 'API'): ?>
<option value="json" <?php echo ($mappingValue === 'json') ? 'selected' : ''; ?>>Map to JSON Node</option>
<?php endif; ?>
<?php if (!$isSceltaMultipla): ?>
<option value="auto" <?php echo ($mappingValue === 'auto') ? 'selected' : ''; ?>>Auto value</option>
<?php endif; ?>
@@ -543,6 +544,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
<td>
<?php if ($mapping['data_type'] === 'SceltaMultipla'): ?>
<div class="scelta-default-wrap" style="display:<?php echo ($mappingValue === 'xls' || $mappingValue === 'json') ? 'none' : 'block'; ?>;">
<select class="form-select dropdown-select manual-default"
data-id="<?php echo (int)$mapping['id']; ?>"
data-field-id="<?php echo (int)$mapping['field_id']; ?>"
@@ -550,6 +552,10 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
style="display:block;">
<option value="">Seleziona...</option>
</select>
</div>
<small class="text-muted scelta-binding-hint" style="display:<?php echo ($mappingValue === 'xls' || $mappingValue === 'json') ? 'block' : 'none'; ?>;">
Values from the file are linked to the LIMS list via Value Bindings
</small>
<?php else: ?>
<input type="text"
class="form-control manual-default"
@@ -594,6 +600,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
<th style="width:120px; text-align:center;">Required</th>
<th>Field</th>
<th style="width:140px;">Type</th>
<th style="width:230px;">Source</th>
<th>Default Value</th>
</tr>
</thead>
@@ -626,6 +633,28 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
<td><?php echo htmlspecialchars($fm['data_type']); ?></td>
<td>
<?php $fixedSource = $fm['default_source'] ?? 'manual'; ?>
<select class="form-select fixed-source-select"
data-fixed-id="<?php echo (int)$fm['id']; ?>">
<option value="manual" <?php echo $fixedSource === 'manual' ? 'selected' : ''; ?>>Default value</option>
<?php if ($sourceType === 'XLS'): ?>
<option value="xls" <?php echo $fixedSource === 'xls' ? 'selected' : ''; ?>>Map to XLS Column</option>
<?php endif; ?>
<?php if ($sourceType === 'API'): ?>
<option value="json" <?php echo $fixedSource === 'json' ? 'selected' : ''; ?>>Map to JSON Node</option>
<?php endif; ?>
</select>
<?php if ($sourceType === 'XLS'): ?>
<select class="form-select xls-columns fixed-xls-column mt-1"
style="display:<?php echo $fixedSource === 'xls' ? 'block' : 'none'; ?>"
data-fixed-id="<?php echo (int)$fm['id']; ?>"
<?php echo !empty($fm['excel_column']) ? 'data-current-xls="' . htmlspecialchars($fm['excel_column'], ENT_QUOTES, 'UTF-8') . '"' : ''; ?>>
</select>
<?php endif; ?>
</td>
<td>
<?php if ($fm['data_type'] === 'DATE'): ?>
@@ -1279,8 +1308,6 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
});
}
function saveJsonNodes(sampleJson, nodes) {
return fetch('update_api_json_nodes.php', {
method: 'POST',
@@ -1363,6 +1390,41 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
});
// fallback (nel caso cambi senza select2)
$(document).on('change', '.fixed-source-select', function() {
const id = this.dataset.fixedId;
const value = this.value;
const $colSelect = $(this).closest('td').find('.fixed-xls-column');
$colSelect.toggle(value === 'xls');
if (value !== 'xls') {
$colSelect.val('');
}
saveFixedField(id, 'default_source', value);
});
$(document).on('change', '.fixed-xls-column', function() {
saveFixedField(this.dataset.fixedId, 'excel_column', this.value || null);
});
function saveFixedField(id, field, value) {
const status = document.getElementById('fixedSaveStatus');
if (status) status.textContent = 'Salvataggio...';
fetch('update_fixed_field.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: parseInt(id, 10), field: field, value: value })
})
.then(r => r.json())
.then(d => {
if (status) status.textContent = d.success ? 'Salvato' : ('Errore: ' + (d.message || ''));
})
.catch(() => {
if (status) status.textContent = 'Errore di rete';
});
}
$(document).on('change', '.fixed-default-select', function() {
saveFixedDefaultSelect(this);
});
@@ -1694,6 +1756,17 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
}
}
// SceltaMultipla: il default è un dropdown (Select2) nel wrapper; con colonna/nodo
// mappato si nasconde (il valore arriva dal file e passa dal binding).
const sceltaWrap = tr.querySelector('.scelta-default-wrap');
const sceltaHint = tr.querySelector('.scelta-binding-hint');
if (sceltaWrap) {
const fromFile = mappingSelect.value === 'xls' || mappingSelect.value === 'json';
sceltaWrap.style.display = fromFile ? 'none' : 'block';
if (sceltaHint) sceltaHint.style.display = fromFile ? 'block' : 'none';
}
const isScelta = !!sceltaWrap;
if (mappingSelect.value === 'xls') {
if (xlsSelect) xlsSelect.style.display = 'block';
@@ -1705,7 +1778,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
if (autoSelect) autoSelect.style.display = 'none';
if (manualInput) {
if (manualInput && !isScelta) {
manualInput.style.display = 'none';
manualInput.value = '';
}
@@ -1728,7 +1801,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
if (autoSelect) autoSelect.style.display = 'none';
if (manualInput) {
if (manualInput && !isScelta) {
manualInput.style.display = 'none';
manualInput.value = '';
}
@@ -1774,7 +1847,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
jsonSelect.value = '';
}
if (manualInput) {
if (manualInput && !isScelta) {
manualInput.style.display = 'none';
manualInput.value = '';
}
@@ -1798,7 +1871,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
if (autoSelect) autoSelect.style.display = 'none';
if (manualInput) {
if (manualInput && !isScelta) {
manualInput.style.display = 'none';
manualInput.value = '';
}
@@ -1882,7 +1955,6 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
});
}
// Save original Main checkbox state
document.querySelectorAll('#schemaFieldsBody .main-field-checkbox').forEach(cb => {
cb.dataset.originalChecked = cb.checked ? 'true' : 'false';
@@ -2008,7 +2080,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
mappingSelect.value = '';
if (mappedColumn) mappedColumn.style.display = 'none';
e.target.style.display = 'none';
saveMapping(mappingId, '', '', null, null, null);
if (!resetSceltaToManual(tr, mappingId)) saveMapping(mappingId, '', '', null, null, null);
updateXlsDropdowns();
});
}
@@ -2031,7 +2103,6 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
}
});
document.getElementById('schemaFieldsBody').addEventListener('change', function(event) {
if (event.target.classList.contains('manual-default') && event.target.tagName === 'SELECT') {
let tr = event.target.closest('tr');
@@ -2089,7 +2160,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
console.log("Removing XLS mapping:", {
id: mappingId
});
saveMapping(mappingId, '', '', null, null, null);
if (!resetSceltaToManual(tr, mappingId)) saveMapping(mappingId, '', '', null, null, null);
updateXlsDropdowns();
}
});
@@ -2119,11 +2190,26 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
event.target.style.display = 'none';
saveMapping(mappingId, '', '', null, null, null);
if (!resetSceltaToManual(tr, mappingId)) saveMapping(mappingId, '', '', null, null, null);
updateJsonDropdowns();
}
});
// Rimozione colonna/nodo: un campo SceltaMultipla torna a "Manual Entry" col suo dropdown default
// (non ha l'opzione vuota). Ritorna true se gestito.
function resetSceltaToManual(tr, mappingId) {
const wrap = tr.querySelector('.scelta-default-wrap');
if (!wrap) return false;
const sel = tr.querySelector('.mapping-select');
if (sel) sel.value = 'manual';
wrap.style.display = 'block';
const hint = tr.querySelector('.scelta-binding-hint');
if (hint) hint.style.display = 'none';
const dd = tr.querySelector('select.manual-default.dropdown-select');
saveMapping(mappingId, 'manual', dd ? dd.value : '', null, null, null);
return true;
}
function saveMapping(mappingId, mappingType, defaultValue, excelColumn, autoValue, jsonNode) {
console.log("Saving mapping:", {
id: mappingId,
@@ -2233,7 +2319,6 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
<td><strong>${escapeHtml(fixedFieldLabels[r.fixed_field_key] || r.fixed_field_key)}</strong></td>
<td>${escapeHtml(r.data_type || '')}</td>
<td>
+127
View File
@@ -0,0 +1,127 @@
<?php
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$userId = (int)($iduserlogin ?? 0);
// Carica le preferenze correnti dell'utente
$prefs = ['notify_export_email' => 1];
if ($userId > 0) {
$s = $pdo->prepare("SELECT notify_export_email FROM auth_users WHERE id = ? LIMIT 1");
$s->execute([$userId]);
$row = $s->fetch(PDO::FETCH_ASSOC);
if ($row) {
$prefs['notify_export_email'] = (int)($row['notify_export_email'] ?? 1);
}
}
// Flash message post-save (via querystring)
$saved = isset($_GET['saved']) && $_GET['saved'] === '1';
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>SmartTRF - Notifiche</title>
<style>
.notif-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
padding: 16px 0;
border-bottom: 1px solid #eef0f2;
}
.notif-row:last-child {
border-bottom: none;
}
.notif-text {
max-width: 640px;
}
.notif-text h6 {
margin: 0 0 4px;
font-size: 15px;
}
.notif-text p {
margin: 0;
font-size: 13px;
color: #6c757d;
}
.form-switch .form-check-input {
width: 2.6em;
height: 1.4em;
cursor: pointer;
}
</style>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card radius-10">
<div class="card-header">
<div>
<h6 class="mb-0">Preferenze notifiche</h6>
<small class="text-muted">Scegli quali email vuoi ricevere da SmartTRF.</small>
</div>
</div>
<div class="card-body">
<?php if ($saved): ?>
<div class="alert alert-success py-2" role="alert">
<i class="fas fa-check-circle me-1"></i> Preferenze salvate.
</div>
<?php endif; ?>
<form action="update_notification_prefs.php" method="POST">
<input type="hidden" name="iduserlogin" value="<?= htmlspecialchars((string)$userId, ENT_QUOTES, 'UTF-8') ?>">
<!-- Notifica: commesse esportate -->
<div class="notif-row">
<div class="notif-text">
<h6>Ricezione email commesse esportate</h6>
<p>Ricevi un riepilogo via email al termine di ogni esportazione verso il LIMS (singola o in blocco), con l'esito di ciascuna commessa.</p>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch"
id="notify_export_email" name="notify_export_email" value="1"
<?= $prefs['notify_export_email'] === 1 ? 'checked' : '' ?>>
</div>
</div>
<!-- Qui in futuro altre notifiche, stessa struttura .notif-row -->
<div class="mt-4">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i> Salva preferenze
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
</body>
</html>
+1
View File
@@ -506,6 +506,7 @@ try {
if ($routineData) {
$response['apply_routine'] = true;
$response['routine_data'] = [
'idroutine' => (int)$routineData['idroutine'],
'name' => $routineData['name'] ?? 'Routine Sconosciuta',
'instruction' => $routineData['instruction'] ?? 'Nessuna descrizione disponibile',
'filename' => $routineData['filename'] ?? '',
+417
View File
@@ -0,0 +1,417 @@
<?php
include('include/headscript.php');
require_once __DIR__ . '/class/binding-functions.php';
$pending = $_SESSION['pending_bindings'] ?? null;
if (isset($_GET['done'])) {
$tid = $pending['template_id'] ?? ($_SESSION['template_id'] ?? null);
$ref = $pending['importref'] ?? '';
unset($_SESSION['pending_bindings']);
header($tid
? "Location: imported.php?id=" . urlencode($tid) . "&importref=" . urlencode($ref)
: "Location: xlstemplates_grid.php");
exit;
}
if (empty($pending) || empty($pending['items'])) {
$tid = $pending['template_id'] ?? ($_SESSION['template_id'] ?? null);
$ref = $pending['importref'] ?? '';
unset($_SESSION['pending_bindings']);
if ($tid) {
header("Location: imported.php?id=" . urlencode($tid) . "&importref=" . urlencode($ref));
} else {
header("Location: xlstemplates_grid.php");
}
exit;
}
$templateId = (int) $pending['template_id'];
$importRef = (string) $pending['importref'];
$items = array_values($pending['items'] ?? []);
$autoItems = array_values($pending['auto'] ?? []);
$savedItems = array_values($pending['saved'] ?? []);
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("SELECT name FROM excel_templates WHERE id = ?");
$stmt->execute([$templateId]);
$templateName = $stmt->fetchColumn() ?: ('Template ' . $templateId);
foreach ($items as $i => $item) {
$items[$i]['suggestion'] = ($item['kind'] ?? 'custom') === 'fixed'
? binding_suggest_fixed(
$pdo,
(string) ($item['fixed_field_key'] ?? ''),
(string) $item['source_value'],
(int) $item['mapping_id']
)
: binding_suggest(
$pdo,
(int) $item['field_id'],
(string) $item['source_value'],
(int) $item['mapping_id']
);
}
$resolvedItems = [];
foreach ($autoItems as $a) {
$a['badge'] = 'automatico';
$a['badge_class'] = 'bg-success';
$resolvedItems[] = $a;
}
foreach ($savedItems as $s) {
$s['badge'] = 'binding salvato';
$s['badge_class'] = 'bg-secondary';
$resolvedItems[] = $s;
}
$groups = [];
$groupOrder = [];
$pushToGroup = function (array $row, string $type, $idx = null) use (&$groups, &$groupOrder) {
$kind = $row['kind'] ?? 'custom';
$gkey = $kind . ':' . (int) ($row['mapping_id'] ?? 0);
if (!isset($groups[$gkey])) {
$groups[$gkey] = [
'label' => $row['field_label'] ?? ('Mapping ' . $gkey),
'kind' => $kind,
'pending' => [],
'resolved' => [],
];
$groupOrder[] = $gkey;
}
if ($type === 'pending') {
$groups[$gkey]['pending'][] = ['idx' => $idx, 'data' => $row];
} else {
$groups[$gkey]['resolved'][] = ['data' => $row];
}
};
foreach ($items as $idx => $item) {
$pushToGroup($item, 'pending', $idx);
}
foreach ($resolvedItems as $res) {
$pushToGroup($res, 'resolved');
}
$pendingRecords = 0;
foreach ($items as $item) {
$pendingRecords += count($item['datadb_ids'] ?? []);
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
<style>
.source-value {
font-family: Consolas, Monaco, monospace;
font-weight: 600;
}
.binding-status {
font-size: 12px;
}
td .select2-container {
min-width: 240px;
}
/* Allinea l'altezza di select2 a form-select/btn di Bootstrap */
.select2-container--default .select2-selection--single {
height: 38px;
border-color: #ced4da;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: 36px;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 36px;
}
</style>
<title>Binding valori &rarr; LIMS - <?= htmlspecialchars($titlewebsite ?? '', ENT_QUOTES, 'UTF-8'); ?></title>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card radius-10">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<div>
<h6 class="mb-0">Binding valori importati &rarr; LIMS</h6>
<small><?= htmlspecialchars($templateName) ?> &middot; Template ID: <?= $templateId ?></small>
</div>
<a href="bindings_manage.php?template_id=<?= $templateId ?>" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-cog"></i> Gestione binding
</a>
</div>
</div>
<div class="card-body">
<div class="alert alert-info">
Alcuni valori importati dal file non corrispondono a nessun valore delle liste LIMS.
Finche' non vengono collegati, questi campi non possono essere esportati.
Scegli il valore LIMS corretto per ciascuno: il binding viene salvato e
riutilizzato nelle importazioni successive.
<div class="mt-1">
<strong><?= count($items) ?></strong> valore/i da risolvere su
<strong><?= $pendingRecords ?></strong> record importati.
</div>
</div>
<?php if (!empty($autoItems)): ?>
<div class="alert alert-success">
<?= count($autoItems) ?> valore/i collegato/i automaticamente (corrispondenza esatta con il LIMS).
</div>
<?php endif; ?>
<div id="bindingError" class="alert alert-danger" style="display:none;"></div>
<form id="bindingForm">
<div class="table-responsive">
<table class="table table-bordered align-middle">
<thead>
<tr>
<th style="width:32px;"></th>
<th>Valore importato</th>
<th style="width:90px;">Record</th>
<th>Valore LIMS</th>
<th style="width:210px;">Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($groupOrder as $gkey): $g = $groups[$gkey];
$pendCount = count($g['pending']);
$totalCount = $pendCount + count($g['resolved']); ?>
<tr class="binding-group-header table-light">
<td colspan="5">
<strong><?= htmlspecialchars($g['label']) ?></strong>
<?php if (($g['kind'] ?? 'custom') === 'fixed'): ?><span class="badge bg-light text-dark border">campo fisso</span><?php endif; ?>
<span class="text-muted">&middot; <?= $totalCount ?> valore/i<?php if ($pendCount): ?>, <?= $pendCount ?> da risolvere<?php endif; ?></span>
</td>
</tr>
<?php foreach ($g['pending'] as $p): $idx = $p['idx']; $item = $p['data'];
$suggestion = $item['suggestion'] ?? null; ?>
<tr class="binding-row"
data-index="<?= $idx ?>"
data-kind="<?= htmlspecialchars($item['kind'] ?? 'custom', ENT_QUOTES) ?>"
data-fixed-key="<?= htmlspecialchars($item['fixed_field_key'] ?? '', ENT_QUOTES) ?>"
data-mapping-id="<?= (int) $item['mapping_id'] ?>"
data-field-id="<?= (int) $item['field_id'] ?>"
data-source-value="<?= htmlspecialchars($item['source_value'], ENT_QUOTES) ?>"
data-datadb-ids="<?= htmlspecialchars(json_encode($item['datadb_ids']), ENT_QUOTES) ?>">
<td class="text-muted ps-4">&rsaquo;</td>
<td class="source-value"><?= htmlspecialchars($item['source_value']) ?></td>
<td><?= count($item['datadb_ids'] ?? []) ?></td>
<td>
<select class="form-select binding-select">
<?php if ($suggestion): ?>
<option value="<?= (int) $suggestion['lims_value_id'] ?>" selected><?= htmlspecialchars($suggestion['lims_value']) ?></option>
<?php else: ?>
<option value="">Seleziona valore LIMS...</option>
<?php endif; ?>
</select>
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-secondary skip-binding-btn">Nessuna corrispondenza</button>
<div class="binding-status mt-1">
<?php if ($suggestion): ?>
<span class="badge bg-warning text-dark" title="Stesso valore gia' collegato in un altro template">
suggerito &middot; <?= htmlspecialchars($suggestion['template_name'] ?: ('template ' . $suggestion['template_id'])) ?>
</span>
<?php else: ?>
<span class="text-muted">In attesa</span>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php foreach ($g['resolved'] as $r): $res = $r['data']; ?>
<tr class="binding-row"
data-kind="<?= htmlspecialchars($res['kind'] ?? 'custom', ENT_QUOTES) ?>"
data-fixed-key="<?= htmlspecialchars($res['fixed_field_key'] ?? '', ENT_QUOTES) ?>"
data-mapping-id="<?= (int) $res['mapping_id'] ?>"
data-field-id="<?= (int) $res['field_id'] ?>"
data-source-value="<?= htmlspecialchars($res['source_value'], ENT_QUOTES) ?>"
data-datadb-ids="<?= htmlspecialchars(json_encode($res['datadb_ids']), ENT_QUOTES) ?>">
<td class="text-muted ps-4">&rsaquo;</td>
<td class="source-value"><?= htmlspecialchars($res['source_value']) ?></td>
<td><?= count($res['datadb_ids'] ?? []) ?></td>
<td>
<select class="form-select binding-select">
<option value="<?= (int) $res['lims_value_id'] ?>" selected><?= htmlspecialchars($res['lims_value']) ?></option>
</select>
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-secondary skip-binding-btn">Nessuna corrispondenza</button>
<div class="binding-status mt-1"><span class="badge <?= $res['badge_class'] ?>"><?= $res['badge'] ?></span></div>
</td>
</tr>
<?php endforeach; ?>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="d-flex justify-content-end gap-2 mt-3">
<a href="resolve_bindings.php?done=1"
class="btn btn-outline-secondary">Salta per ora</a>
<button type="submit" class="btn btn-primary" id="confirmBindingsBtn" disabled>
Conferma e prosegui
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(function() {
const TEMPLATE_ID = <?= $templateId ?>;
const IMPORT_REF = <?= json_encode($importRef) ?>;
const CONTINUE_URL = 'resolve_bindings.php?done=1';
const $form = $('#bindingForm');
const $btn = $('#confirmBindingsBtn');
const $error = $('#bindingError');
$('.binding-select').each(function() {
const $row = $(this).closest('.binding-row');
const isFixed = $row.data('kind') === 'fixed';
$(this).select2({
placeholder: 'Seleziona valore LIMS...',
width: '100%',
ajax: {
url: isFixed ? 'search_fixed_field_values.php' : 'search_customfield_values.php',
dataType: 'json',
delay: 200,
data: params => isFixed ? {
field_key: $row.data('fixed-key'),
template_id: TEMPLATE_ID,
q: params.term || '',
limit: 50
} : {
field_id: $row.data('field-id'),
q: params.term || '',
limit: 50
},
processResults: data => ({
results: data.results || []
})
},
minimumInputLength: 0
});
});
function refreshButton() {
const allReady = $('.binding-row').toArray().every(row => {
const $row = $(row);
return $row.hasClass('is-skipped') || $row.find('.binding-select').val();
});
$btn.prop('disabled', !allReady);
}
$('.binding-select').on('change', refreshButton);
refreshButton();
$('.skip-binding-btn').on('click', function() {
const $row = $(this).closest('.binding-row');
const $select = $row.find('.binding-select');
const $status = $row.find('.binding-status');
const skipped = $row.toggleClass('is-skipped').hasClass('is-skipped');
$(this).toggleClass('btn-outline-secondary', !skipped).toggleClass('btn-secondary', skipped);
$select.val(null).trigger('change').prop('disabled', skipped);
$status.html(skipped ? 'Nessuna corrispondenza' : '<span class="text-muted">In attesa</span>');
refreshButton();
});
$form.on('submit', function(e) {
e.preventDefault();
$error.hide();
$btn.prop('disabled', true).text('Salvataggio...');
const tasks = $('.binding-row').toArray().map(row => {
const $row = $(row);
const $status = $row.find('.binding-status');
const datadbIds = JSON.stringify($row.data('datadb-ids') || []);
const sourceValue = String($row.data('source-value'));
const mappingId = $row.data('mapping-id');
if ($row.hasClass('is-skipped')) {
return $.post('skip_binding.php', {
kind: $row.data('kind') || 'custom',
mapping_id: mappingId,
template_id: TEMPLATE_ID,
source_value: sourceValue,
datadb_ids: datadbIds
}).then(resp => {
if (resp && resp.success) {
$status.html('<span class="text-success">Azzerato</span>');
return true;
}
throw new Error((resp && resp.error) || 'Errore azzeramento valore');
});
}
const $select = $row.find('.binding-select');
const selectedData = $select.select2('data')[0] || {};
return $.post('save_binding.php', {
kind: $row.data('kind') || 'custom',
mapping_id: mappingId,
field_id: $row.data('field-id'),
template_id: TEMPLATE_ID,
source_value: sourceValue,
lims_value_id: $select.val(),
lims_value: selectedData.text || '',
datadb_ids: datadbIds
}).then(resp => {
if (resp && resp.success) {
$status.html('<span class="text-success">Salvato</span>');
return true;
}
throw new Error((resp && resp.error) || 'Errore salvataggio binding');
});
});
Promise.all(tasks)
.then(() => {
window.location.href = CONTINUE_URL;
})
.catch(err => {
$error.text(err.message || 'Errore durante il salvataggio dei binding.').show();
$btn.prop('disabled', false).text('Conferma e prosegui');
});
});
});
</script>
</body>
</html>
+38
View File
@@ -0,0 +1,38 @@
<?php
// Evita che eventuale output di headscript rompa il JSON
ob_start();
include('include/headscript.php');
ob_end_clean();
header('Content-Type: application/json');
$iddatadb = (int)($_POST['iddatadb'] ?? 0);
$newStatus = $_POST['status'] ?? 'i';
if ($iddatadb <= 0) {
echo json_encode(['success' => false, 'message' => 'ID record mancante']);
exit;
}
// Da questa azione consentiamo solo il ritorno a 'i'
if ($newStatus !== 'i') {
echo json_encode(['success' => false, 'message' => 'Stato non consentito']);
exit;
}
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Aggiorna solo se attualmente è in stato 'l' (To LIMS)
$stmt = $pdo->prepare("UPDATE datadb SET status = ? WHERE iddatadb = ? AND status = 'l'");
$stmt->execute([$newStatus, $iddatadb]);
if ($stmt->rowCount() > 0) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'message' => 'Record non trovato o già modificato']);
}
} catch (Throwable $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+549
View File
@@ -0,0 +1,549 @@
<?php
include('include/headscript.php');
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!$user->hasRole('Admin') && !$user->hasRole('SuperUser')) {
header('Location: import_dashboard.php');
exit;
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
/*
* Nome file del motore generico condiviso da tutte le routine engine.
* Deve corrispondere al file presente in routines/.
*/
$engineFilename = '_engine.php';
/*
* CSRF token.
*/
if (empty($_SESSION['routine_admin_csrf'])) {
$_SESSION['routine_admin_csrf'] = bin2hex(random_bytes(32));
}
$csrfToken = $_SESSION['routine_admin_csrf'];
function cleanInput($value)
{
return trim((string)($value ?? ''));
}
function redirectWithMessage($type, $message)
{
$_SESSION['routine_admin_flash'] = [
'type' => $type,
'message' => $message
];
header('Location: ' . $_SERVER['PHP_SELF']);
exit;
}
/*
* Gestione azioni POST.
*/
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$postedToken = $_POST['csrf_token'] ?? '';
if (!hash_equals($csrfToken, $postedToken)) {
throw new Exception('Token di sicurezza non valido.');
}
$action = cleanInput($_POST['action'] ?? '');
if ($action === 'create' || $action === 'update') {
$name = cleanInput($_POST['name'] ?? '');
$description = cleanInput($_POST['description'] ?? '');
$engine = (int)($_POST['engine'] ?? 0) === 1 ? 1 : 0;
$headerrow = (int)($_POST['headerrow'] ?? 1);
$filename = cleanInput($_POST['filename'] ?? '');
if ($name === '') {
throw new Exception('Il nome della routine è obbligatorio.');
}
if ($headerrow < 1) {
$headerrow = 1;
}
/*
* Se è una routine a motore, il filename è forzato al file del motore.
* Se è legacy, serve un filename di file reale.
*/
if ($engine === 1) {
$filename = $GLOBALS['engineFilename'];
} else {
if ($filename === '') {
throw new Exception('Per una routine di tipo File indica il nome del file (es. fendi.php).');
}
}
if ($action === 'create') {
// instruction è NOT NULL nello schema: valorizziamo sempre.
$instruction = $engine === 1
? 'Routine gestita dal motore generico (routine_step).'
: ('Routine su file: ' . $filename);
$stmt = $pdo->prepare("
INSERT INTO routine (name, description, instruction, filename, engine, headerrow)
VALUES (:name, :descr, :instr, :fn, :engine, :hr)
");
$stmt->execute([
':name' => $name,
':descr' => ($description !== '' ? $description : null),
':instr' => $instruction,
':fn' => $filename,
':engine' => $engine,
':hr' => $headerrow,
]);
redirectWithMessage('success', 'Routine creata correttamente.');
} else {
$idroutine = (int)($_POST['idroutine'] ?? 0);
if ($idroutine <= 0) {
throw new Exception('ID routine non valido.');
}
$stmt = $pdo->prepare("
UPDATE routine
SET name = :name,
description = :descr,
filename = :fn,
engine = :engine,
headerrow = :hr
WHERE idroutine = :id
");
$stmt->execute([
':name' => $name,
':descr' => ($description !== '' ? $description : null),
':fn' => $filename,
':engine' => $engine,
':hr' => $headerrow,
':id' => $idroutine,
]);
redirectWithMessage('success', 'Routine aggiornata correttamente.');
}
}
if ($action === 'delete') {
$idroutine = (int)($_POST['idroutine'] ?? 0);
if ($idroutine <= 0) {
throw new Exception('ID routine non valido.');
}
// Rimuove prima gli eventuali step collegati, poi la routine.
$stmt = $pdo->prepare("DELETE FROM routine_step WHERE idroutine = :id");
$stmt->execute([':id' => $idroutine]);
$stmt = $pdo->prepare("DELETE FROM routine WHERE idroutine = :id");
$stmt->execute([':id' => $idroutine]);
redirectWithMessage('success', 'Routine eliminata correttamente.');
}
throw new Exception('Azione non valida.');
} catch (Exception $e) {
redirectWithMessage('error', $e->getMessage());
}
}
/*
* Caricamento routine + conteggio step.
*/
$stmt = $pdo->query("
SELECT
r.idroutine,
r.name,
r.description,
r.filename,
r.engine,
r.headerrow,
(SELECT COUNT(*) FROM routine_step s WHERE s.idroutine = r.idroutine) AS step_count
FROM routine r
ORDER BY r.name ASC
");
$routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
$flash = $_SESSION['routine_admin_flash'] ?? null;
unset($_SESSION['routine_admin_flash']);
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>Gestione Routine - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<style>
.small-muted {
font-size: 12px;
color: #6c757d;
}
/* La tabella non deve sforare la pagina */
#routinesTable {
table-layout: fixed;
width: 100%;
}
#routinesTable td,
#routinesTable th {
overflow-wrap: break-word;
word-break: break-word;
white-space: normal;
vertical-align: middle;
}
/* Colonna descrizione: va a capo invece di allargare la tabella */
#routinesTable td.desc-cell {
white-space: normal;
overflow-wrap: break-word;
word-break: break-word;
max-width: 320px;
}
/* Le azioni restano su una riga */
#routinesTable td.table-actions {
white-space: nowrap;
}
.badge-engine {
background: #6f42c1;
color: #fff;
}
.badge-file {
background: #6c757d;
color: #fff;
}
.table-actions {
white-space: nowrap;
}
.filename-chip {
font-family: Consolas, Monaco, monospace;
font-size: 12px;
background: #f1f5f9;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 2px 8px;
color: #334155;
}
.step-count-pill {
display: inline-block;
min-width: 26px;
text-align: center;
font-weight: 600;
border-radius: 999px;
padding: 2px 10px;
background: #eef2ff;
color: #4338ca;
border: 1px solid #e0e7ff;
}
</style>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<?php include('top_stat_widget.php'); ?>
<div class="card radius-10">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between">
<div>
<h6 class="mb-0">Gestione Routine</h6>
<small class="text-muted">Crea e configura le routine di importazione. Le routine di tipo Motore sono configurabili a step senza codice.</small>
</div>
<button type="button" class="btn btn-primary" id="btnAddRoutine">
<i class="bx bx-plus"></i> Nuova Routine
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table id="routinesTable" class="table table-striped table-bordered align-middle">
<thead>
<tr>
<th>Nome</th>
<th>Descrizione</th>
<th style="width: 120px;">Tipo</th>
<th style="width: 160px;">File</th>
<th style="width: 90px;">Header row</th>
<th style="width: 80px;">Step</th>
<th style="width: 130px;">Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($routines as $row): ?>
<?php $isEngine = ((int)$row['engine'] === 1); ?>
<tr>
<td><?= htmlspecialchars($row['name'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
<td class="small-muted desc-cell"><?= htmlspecialchars($row['description'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
<td>
<?php if ($isEngine): ?>
<span class="badge badge-engine">Motore</span>
<?php else: ?>
<span class="badge badge-file">File</span>
<?php endif; ?>
</td>
<td><span class="filename-chip"><?= htmlspecialchars($row['filename'] ?? '', ENT_QUOTES, 'UTF-8'); ?></span></td>
<td class="text-center"><?= (int)$row['headerrow']; ?></td>
<td class="text-center">
<?php if ($isEngine): ?>
<span class="step-count-pill"><?= (int)$row['step_count']; ?></span>
<?php else: ?>
<span class="small-muted">—</span>
<?php endif; ?>
</td>
<td class="table-actions">
<?php if ($isEngine): ?>
<a class="btn btn-sm btn-outline-success"
href="routine_steps.php?idroutine=<?= (int)$row['idroutine']; ?>"
title="Gestisci step" data-bs-toggle="tooltip">
<i class="bx bx-list-ul"></i>
</a>
<?php endif; ?>
<button
type="button"
class="btn btn-sm btn-outline-primary btnEditRoutine"
data-id="<?= (int)$row['idroutine']; ?>"
data-name="<?= htmlspecialchars($row['name'] ?? '', ENT_QUOTES, 'UTF-8'); ?>"
data-description="<?= htmlspecialchars($row['description'] ?? '', ENT_QUOTES, 'UTF-8'); ?>"
data-engine="<?= (int)$row['engine']; ?>"
data-filename="<?= htmlspecialchars($row['filename'] ?? '', ENT_QUOTES, 'UTF-8'); ?>"
data-headerrow="<?= (int)$row['headerrow']; ?>"
title="Modifica" data-bs-toggle="tooltip">
<i class="bx bx-edit"></i>
</button>
<button
type="button"
class="btn btn-sm btn-outline-danger btnDeleteRoutine"
data-id="<?= (int)$row['idroutine']; ?>"
data-name="<?= htmlspecialchars($row['name'] ?? '', ENT_QUOTES, 'UTF-8'); ?>"
title="Elimina" data-bs-toggle="tooltip">
<i class="bx bx-trash"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
</div>
<!-- Modal Routine -->
<div class="modal fade" id="routineModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<form method="POST" class="modal-content" id="routineForm">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" id="formAction" value="create">
<input type="hidden" name="idroutine" id="idroutine" value="">
<div class="modal-header">
<h5 class="modal-title" id="routineModalTitle">Nuova Routine</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<div class="modal-body">
<div class="row g-3">
<div class="col-md-12">
<label for="name" class="form-label">Nome *</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="col-md-12">
<label for="description" class="form-label">Descrizione</label>
<input type="text" class="form-control" id="description" name="description" maxlength="150">
</div>
<div class="col-md-6">
<label for="engine" class="form-label">Tipo routine *</label>
<select class="form-select" id="engine" name="engine" required>
<option value="1">Motore (step configurabili)</option>
<option value="0">File (legacy)</option>
</select>
<div class="small-muted mt-1">
Motore: configurabile a step da questa interfaccia. File: logica in un file PHP dedicato.
</div>
</div>
<div class="col-md-6">
<label for="headerrow" class="form-label">Riga intestazione (header row) *</label>
<input type="number" class="form-control" id="headerrow" name="headerrow" min="1" value="1" required>
</div>
<div class="col-md-12" id="filenameWrapper">
<label for="filename" class="form-label">Nome file</label>
<input type="text" class="form-control" id="filename" name="filename" placeholder="es. fendi.php">
<div class="small-muted mt-1" id="filenameHelp">
Per le routine di tipo File indica il nome del file PHP presente nella cartella routines/.
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-primary" id="saveRoutineButton">Salva Routine</button>
</div>
</form>
</div>
</div>
<!-- Delete Form -->
<form method="POST" id="deleteRoutineForm" style="display:none;">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="idroutine" id="deleteIdRoutine" value="">
</form>
<?php include('jsinclude.php'); ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
const routineModal = new bootstrap.Modal(document.getElementById('routineModal'));
const formAction = document.getElementById('formAction');
const idroutine = document.getElementById('idroutine');
const routineModalTitle = document.getElementById('routineModalTitle');
const saveRoutineButton = document.getElementById('saveRoutineButton');
const name = document.getElementById('name');
const description = document.getElementById('description');
const engine = document.getElementById('engine');
const headerrow = document.getElementById('headerrow');
const filename = document.getElementById('filename');
const filenameWrapper = document.getElementById('filenameWrapper');
const filenameHelp = document.getElementById('filenameHelp');
const engineFilename = "<?= htmlspecialchars($engineFilename, ENT_QUOTES, 'UTF-8'); ?>";
/*
* Mostra/nasconde il campo filename in base al tipo di routine.
* Motore -> filename forzato al file del motore e campo di sola lettura.
*/
function applyEngineUi() {
if (engine.value === '1') {
filename.value = engineFilename;
filename.readOnly = true;
filenameHelp.textContent = 'Le routine di tipo Motore usano automaticamente il file ' + engineFilename + '.';
} else {
if (filename.value === engineFilename) {
filename.value = '';
}
filename.readOnly = false;
filenameHelp.textContent = 'Per le routine di tipo File indica il nome del file PHP presente nella cartella routines/.';
}
}
engine.addEventListener('change', applyEngineUi);
function resetModalForCreate() {
formAction.value = 'create';
idroutine.value = '';
routineModalTitle.textContent = 'Nuova Routine';
saveRoutineButton.textContent = 'Crea Routine';
name.value = '';
description.value = '';
engine.value = '1';
headerrow.value = '1';
filename.value = '';
applyEngineUi();
}
function setModalForEdit(button) {
formAction.value = 'update';
idroutine.value = button.dataset.id || '';
routineModalTitle.textContent = 'Modifica Routine';
saveRoutineButton.textContent = 'Aggiorna Routine';
name.value = button.dataset.name || '';
description.value = button.dataset.description || '';
engine.value = button.dataset.engine === '1' ? '1' : '0';
headerrow.value = button.dataset.headerrow || '1';
filename.value = button.dataset.filename || '';
applyEngineUi();
}
document.getElementById('btnAddRoutine').addEventListener('click', function() {
resetModalForCreate();
routineModal.show();
});
document.querySelectorAll('.btnEditRoutine').forEach(function(button) {
button.addEventListener('click', function() {
setModalForEdit(button);
routineModal.show();
});
});
document.querySelectorAll('.btnDeleteRoutine').forEach(function(button) {
button.addEventListener('click', function() {
const id = button.dataset.id;
const nm = button.dataset.name || 'questa routine';
Swal.fire({
title: 'Eliminare la routine?',
text: 'Vuoi eliminare "' + nm + '"? Verranno rimossi anche gli step collegati. Operazione irreversibile.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Sì, elimina',
cancelButtonText: 'Annulla',
confirmButtonColor: '#d33'
}).then(function(result) {
if (result.isConfirmed) {
document.getElementById('deleteIdRoutine').value = id;
document.getElementById('deleteRoutineForm').submit();
}
});
});
});
if (typeof $ !== 'undefined' && $.fn.DataTable) {
$('#routinesTable').DataTable({
pageLength: 25,
order: [
[0, 'asc']
]
});
}
<?php if ($flash): ?>
Swal.fire({
icon: '<?= htmlspecialchars($flash['type'], ENT_QUOTES, 'UTF-8'); ?>',
title: '<?= $flash['type'] === 'success' ? 'Fatto' : ($flash['type'] === 'warning' ? 'Attenzione' : 'Errore'); ?>',
text: '<?= htmlspecialchars($flash['message'], ENT_QUOTES, 'UTF-8'); ?>'
});
<?php endif; ?>
});
</script>
</body>
</html>
+184
View File
@@ -0,0 +1,184 @@
<?php
/**
* routine_preview.php
* -------------------
* Anteprima di una routine a motore su un XLS caricato temporaneamente.
*
* - Riceve via multipart/form-data: `file` (XLS/XLSX) e `idroutine`.
* - NON salva nulla: il file viene letto dal tmp di PHP ed elaborato in RAM.
* - Legge la PRIMA riga dati subito dopo l'header row (routine.headerrow).
* - Impacchetta la riga nel formato excelData atteso dal motore:
* [ ['excelrow' => N, 'data' => [0=>.., 1=>.., ...]] ]
* - Include _engine.php e applica applyRoutine().
* - Risponde JSON con la riga elaborata + intestazioni + lettere colonna.
*/
ob_start();
session_start();
require_once '../../vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
Dotenv\Dotenv::createImmutable(dirname(__DIR__, 2))->safeLoad();
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
header('Content-Type: application/json');
$response = [
'error' => '',
'headers' => [], // intestazioni prese dalla header row
'letters' => [], // lettere colonna (A, B, C, ...) allineate ai valori
'row' => [], // valori della riga DOPO l'applicazione del motore
'excelrow' => 0, // numero riga excel elaborata
];
/**
* Indice zero-based -> lettera colonna Excel (0 -> A, 25 -> Z, 26 -> AA...).
*/
function previewIndexToCol($index)
{
$index = (int)$index;
if ($index < 0) {
return '';
}
$letters = '';
$index += 1;
while ($index > 0) {
$rem = ($index - 1) % 26;
$letters = chr(ord('A') + $rem) . $letters;
$index = intdiv($index - 1, 26);
}
return $letters;
}
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
throw new Exception('Richiesta non valida.');
}
$idroutine = (int)($_POST['idroutine'] ?? 0);
if ($idroutine <= 0) {
throw new Exception('ID routine mancante.');
}
if (
empty($_FILES['file']) ||
!isset($_FILES['file']['tmp_name']) ||
$_FILES['file']['error'] !== UPLOAD_ERR_OK
) {
throw new Exception('Nessun file caricato o upload non riuscito.');
}
// --- Recupera la routine e verifica che sia a motore ---
require_once __DIR__ . '/class/db-functions.php';
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->prepare("SELECT idroutine, name, engine, headerrow FROM routine WHERE idroutine = :id LIMIT 1");
$stmt->execute([':id' => $idroutine]);
$routine = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$routine) {
throw new Exception('Routine non trovata.');
}
if ((int)$routine['engine'] !== 1) {
throw new Exception('Questa routine non è di tipo Motore: anteprima non disponibile.');
}
$headerRow = max(1, (int)$routine['headerrow']); // riga 1-based dell'intestazione
// --- Legge l'XLS dal file temporaneo (nessun salvataggio) ---
$tmpPath = $_FILES['file']['tmp_name'];
$reader = IOFactory::createReaderForFile($tmpPath);
$reader->setReadDataOnly(true);
$spreadsheet = $reader->load($tmpPath);
$sheet = $spreadsheet->getActiveSheet();
$highestColIndex = Coordinate::columnIndexFromString($sheet->getHighestDataColumn()); // 1-based
$highestRow = $sheet->getHighestDataRow();
// --- Intestazioni dalla header row ---
$headers = [];
$letters = [];
for ($c = 1; $c <= $highestColIndex; $c++) {
$val = $sheet->getCell([$c, $headerRow])->getValue();
$headers[] = ($val === null ? '' : (string)$val);
$letters[] = previewIndexToCol($c - 1); // zero-based -> lettera
}
// --- Trova la prima riga dati valorizzata dopo l'header ---
$dataRowNumber = null;
$rowValues = null;
for ($r = $headerRow + 1; $r <= $highestRow; $r++) {
$vals = [];
$hasValue = false;
for ($c = 1; $c <= $highestColIndex; $c++) {
$v = $sheet->getCell([$c, $r])->getValue();
$v = ($v === null ? '' : (string)$v);
if ($v !== '') {
$hasValue = true;
}
$vals[$c - 1] = $v; // indice zero-based, allineato alle colonne
}
if ($hasValue) {
$dataRowNumber = $r;
$rowValues = $vals;
break;
}
}
if ($dataRowNumber === null) {
throw new Exception('Nessuna riga dati valorizzata trovata dopo la riga di intestazione (' . $headerRow . ').');
}
// --- Impacchetta nel formato excelData e applica il motore ---
$excelData = [
[
'excelrow' => $dataRowNumber,
'data' => $rowValues,
],
];
// Il motore legge gli step dal DB in base a idroutine.
$routineData = [
'idroutine' => $idroutine,
'name' => $routine['name'],
'headerrow' => $headerRow,
'xls_headers' => $headers,
];
require_once __DIR__ . '/routines/_engine.php';
applyRoutine($excelData, $routineData); // modifica in place
$processed = $excelData[0]['data'] ?? [];
// Riallinea l'output all'ampiezza delle intestazioni (il merge può creare indici nuovi).
$maxIndex = max(
count($headers) - 1,
empty($processed) ? 0 : max(array_keys($processed))
);
$outRow = [];
$outLetters = [];
$outHeaders = [];
for ($i = 0; $i <= $maxIndex; $i++) {
$outRow[] = array_key_exists($i, $processed) ? (string)$processed[$i] : '';
$outLetters[] = previewIndexToCol($i);
$outHeaders[] = array_key_exists($i, $headers) ? $headers[$i] : '';
}
$response['headers'] = $outHeaders;
$response['letters'] = $outLetters;
$response['row'] = $outRow;
$response['excelrow'] = $dataRowNumber;
} catch (Throwable $e) {
$response['error'] = 'Errore anteprima: ' . $e->getMessage();
error_log('routine_preview.php: ' . $e->getMessage());
}
ob_end_clean();
echo json_encode($response, JSON_UNESCAPED_UNICODE);
exit;
+976
View File
@@ -0,0 +1,976 @@
<?php
include('include/headscript.php');
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!$user->hasRole('Admin') && !$user->hasRole('SuperUser')) {
header('Location: import_dashboard.php');
exit;
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
/*
* CSRF token.
*/
if (empty($_SESSION['routine_steps_csrf'])) {
$_SESSION['routine_steps_csrf'] = bin2hex(random_bytes(32));
}
$csrfToken = $_SESSION['routine_steps_csrf'];
function cleanInput($value)
{
return trim((string)($value ?? ''));
}
/*
* Determina l'id routine dalla querystring (GET) o dal POST.
*/
$idroutine = 0;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$idroutine = (int)($_POST['idroutine'] ?? 0);
} else {
$idroutine = (int)($_GET['idroutine'] ?? 0);
}
function redirectToSteps($idroutine, $type, $message)
{
$_SESSION['routine_steps_flash'] = [
'type' => $type,
'message' => $message
];
header('Location: routine_steps.php?idroutine=' . (int)$idroutine);
exit;
}
/*
* Converte lettera colonna Excel in indice zero-based. Accetta anche numeri.
* A=0, B=1, ... Z=25, AA=26, ...
*/
function colToIndex($ref)
{
$ref = strtoupper(trim((string)$ref));
if ($ref === '') {
return null;
}
if (ctype_digit($ref)) {
return (int)$ref;
}
$index = 0;
$len = strlen($ref);
for ($i = 0; $i < $len; $i++) {
$c = $ref[$i];
if ($c < 'A' || $c > 'Z') {
return null;
}
$index = $index * 26 + (ord($c) - ord('A') + 1);
}
return $index - 1;
}
/*
* Converte indice zero-based in lettera Excel (per mostrare gli step salvati).
*/
function indexToCol($index)
{
$index = (int)$index;
if ($index < 0) {
return '';
}
$letters = '';
$index += 1;
while ($index > 0) {
$rem = ($index - 1) % 26;
$letters = chr(ord('A') + $rem) . $letters;
$index = intdiv($index - 1, 26);
}
return $letters;
}
/*
* Normalizza una stringa di colonne separate da virgola ("D, F, H") in
* un array di indici zero-based, validando ogni riferimento.
*/
function parseColumnList($raw)
{
$raw = trim((string)$raw);
if ($raw === '') {
return [];
}
$parts = preg_split('/[\s,;]+/', $raw);
$out = [];
foreach ($parts as $p) {
if ($p === '') {
continue;
}
$idx = colToIndex($p);
if ($idx === null) {
throw new Exception('Riferimento colonna non valido: "' . $p . '". Usa lettere come D, F, H.');
}
$out[] = $idx;
}
return $out;
}
/*
* Verifica esistenza routine ed engine = 1.
*/
$routine = null;
if ($idroutine > 0) {
$stmt = $pdo->prepare("SELECT idroutine, name, description, engine, headerrow FROM routine WHERE idroutine = :id LIMIT 1");
$stmt->execute([':id' => $idroutine]);
$routine = $stmt->fetch(PDO::FETCH_ASSOC);
}
if (!$routine) {
$_SESSION['routine_admin_flash'] = ['type' => 'error', 'message' => 'Routine non trovata.'];
header('Location: routine_admin.php');
exit;
}
if ((int)$routine['engine'] !== 1) {
$_SESSION['routine_admin_flash'] = ['type' => 'error', 'message' => 'Questa routine è di tipo File: gli step non sono configurabili da qui.'];
header('Location: routine_admin.php');
exit;
}
/*
* Vocabolario operazioni disponibili (deve combaciare con gli handler di _engine.php).
*/
$operations = ['merge', 'copy', 'clear', 'trim', 'replace', 'uppercase', 'lowercase'];
/*
* Gestione azioni POST.
*/
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$postedToken = $_POST['csrf_token'] ?? '';
if (!hash_equals($csrfToken, $postedToken)) {
throw new Exception('Token di sicurezza non valido.');
}
$action = cleanInput($_POST['action'] ?? '');
if ($action === 'create' || $action === 'update') {
$operation = strtolower(cleanInput($_POST['operation'] ?? ''));
$active = (int)($_POST['active'] ?? 1) === 1 ? 1 : 0;
if (!in_array($operation, $operations, true)) {
throw new Exception('Operazione non valida.');
}
/*
* Costruzione params in base all'operazione.
* Le colonne arrivano come lettere e vengono convertite in indici.
*/
$params = [];
if ($operation === 'merge') {
$sources = parseColumnList($_POST['source_columns'] ?? '');
$target = colToIndex($_POST['target_column'] ?? '');
if (empty($sources)) {
throw new Exception('Indica almeno una colonna sorgente (es. D, F, H).');
}
if ($target === null) {
throw new Exception('Indica la colonna di destinazione (es. J).');
}
$params = [
'source_columns' => $sources,
'target_column' => $target,
'separator' => (string)($_POST['separator'] ?? ' '),
'skip_empty' => isset($_POST['skip_empty']) ? true : false,
];
} elseif ($operation === 'copy') {
$src = colToIndex($_POST['source_column'] ?? '');
$dst = colToIndex($_POST['target_column'] ?? '');
if ($src === null || $dst === null) {
throw new Exception('Indica colonna sorgente e destinazione (es. D e J).');
}
$params = ['source_column' => $src, 'target_column' => $dst];
} elseif ($operation === 'clear' || $operation === 'trim' || $operation === 'uppercase' || $operation === 'lowercase') {
$cols = parseColumnList($_POST['columns'] ?? '');
if (empty($cols)) {
throw new Exception('Indica almeno una colonna (es. D, F).');
}
$params = ['columns' => $cols];
if ($operation === 'trim') {
$chars = (string)($_POST['chars'] ?? '');
if ($chars !== '') {
$params['chars'] = $chars;
}
}
} elseif ($operation === 'replace') {
$cols = parseColumnList($_POST['columns'] ?? '');
if (empty($cols)) {
throw new Exception('Indica almeno una colonna (es. D, F).');
}
$params = [
'columns' => $cols,
'search' => (string)($_POST['search'] ?? ''),
'replace' => (string)($_POST['replace'] ?? ''),
'regex' => isset($_POST['regex']) ? true : false,
];
}
$paramsJson = json_encode($params, JSON_UNESCAPED_UNICODE);
if ($action === 'create') {
// step_order = max + 1
$stmt = $pdo->prepare("SELECT COALESCE(MAX(step_order), 0) + 1 AS next_order FROM routine_step WHERE idroutine = :id");
$stmt->execute([':id' => $idroutine]);
$nextOrder = (int)$stmt->fetchColumn();
$stmt = $pdo->prepare("
INSERT INTO routine_step (idroutine, step_order, operation, params, active)
VALUES (:id, :ord, :op, :params, :active)
");
$stmt->execute([
':id' => $idroutine,
':ord' => $nextOrder,
':op' => $operation,
':params' => $paramsJson,
':active' => $active,
]);
redirectToSteps($idroutine, 'success', 'Step aggiunto correttamente.');
} else {
$idstep = (int)($_POST['idstep'] ?? 0);
if ($idstep <= 0) {
throw new Exception('ID step non valido.');
}
$stmt = $pdo->prepare("
UPDATE routine_step
SET operation = :op, params = :params, active = :active
WHERE idstep = :idstep AND idroutine = :id
");
$stmt->execute([
':op' => $operation,
':params' => $paramsJson,
':active' => $active,
':idstep' => $idstep,
':id' => $idroutine,
]);
redirectToSteps($idroutine, 'success', 'Step aggiornato correttamente.');
}
}
if ($action === 'delete') {
$idstep = (int)($_POST['idstep'] ?? 0);
if ($idstep <= 0) {
throw new Exception('ID step non valido.');
}
$stmt = $pdo->prepare("DELETE FROM routine_step WHERE idstep = :idstep AND idroutine = :id");
$stmt->execute([':idstep' => $idstep, ':id' => $idroutine]);
redirectToSteps($idroutine, 'success', 'Step eliminato.');
}
if ($action === 'move') {
$idstep = (int)($_POST['idstep'] ?? 0);
$dir = cleanInput($_POST['direction'] ?? '');
if ($idstep <= 0 || !in_array($dir, ['up', 'down'], true)) {
throw new Exception('Parametri di spostamento non validi.');
}
// Carica step correnti ordinati.
$stmt = $pdo->prepare("SELECT idstep, step_order FROM routine_step WHERE idroutine = :id ORDER BY step_order ASC, idstep ASC");
$stmt->execute([':id' => $idroutine]);
$all = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Trova posizione corrente.
$pos = null;
foreach ($all as $i => $s) {
if ((int)$s['idstep'] === $idstep) {
$pos = $i;
break;
}
}
if ($pos !== null) {
$swapWith = $dir === 'up' ? $pos - 1 : $pos + 1;
if ($swapWith >= 0 && $swapWith < count($all)) {
$a = $all[$pos];
$b = $all[$swapWith];
// Scambia gli step_order dei due record.
$upd = $pdo->prepare("UPDATE routine_step SET step_order = :ord WHERE idstep = :idstep AND idroutine = :id");
$upd->execute([':ord' => (int)$b['step_order'], ':idstep' => (int)$a['idstep'], ':id' => $idroutine]);
$upd->execute([':ord' => (int)$a['step_order'], ':idstep' => (int)$b['idstep'], ':id' => $idroutine]);
}
}
redirectToSteps($idroutine, 'success', 'Ordine aggiornato.');
}
throw new Exception('Azione non valida.');
} catch (Exception $e) {
redirectToSteps($idroutine, 'error', $e->getMessage());
}
}
/*
* Caricamento step della routine.
*/
$stmt = $pdo->prepare("SELECT idstep, step_order, operation, params, active FROM routine_step WHERE idroutine = :id ORDER BY step_order ASC, idstep ASC");
$stmt->execute([':id' => $idroutine]);
$steps = $stmt->fetchAll(PDO::FETCH_ASSOC);
/*
* Costruisce una descrizione leggibile dei params per la tabella.
*/
function describeStep($operation, array $p)
{
$colsToLetters = function ($arr) {
return implode(', ', array_map('indexToCol', $arr));
};
switch ($operation) {
case 'merge':
return 'Unisce ' . $colsToLetters($p['source_columns'] ?? []) .
' → ' . indexToCol($p['target_column'] ?? 0) .
' (sep: "' . ($p['separator'] ?? ' ') . '")';
case 'copy':
return 'Copia ' . indexToCol($p['source_column'] ?? 0) . ' → ' . indexToCol($p['target_column'] ?? 0);
case 'clear':
return 'Svuota ' . $colsToLetters($p['columns'] ?? []);
case 'trim':
return 'Trim ' . $colsToLetters($p['columns'] ?? []) . (isset($p['chars']) ? ' (chars: "' . $p['chars'] . '")' : '');
case 'uppercase':
return 'Maiuscolo ' . $colsToLetters($p['columns'] ?? []);
case 'lowercase':
return 'Minuscolo ' . $colsToLetters($p['columns'] ?? []);
case 'replace':
return 'Sostituisci in ' . $colsToLetters($p['columns'] ?? []) .
': "' . ($p['search'] ?? '') . '" → "' . ($p['replace'] ?? '') . '"' .
(!empty($p['regex']) ? ' (regex)' : '');
default:
return $operation;
}
}
/*
* Prepara i dati step per il JS (params già in lettere per il form di modifica).
*/
$stepsForJs = [];
foreach ($steps as $s) {
$p = json_decode($s['params'] ?? '[]', true);
if (!is_array($p)) {
$p = [];
}
// Converte indici -> lettere per il form.
$letters = function ($arr) {
return implode(', ', array_map('indexToCol', $arr));
};
$formData = ['operation' => $s['operation'], 'active' => (int)$s['active']];
if ($s['operation'] === 'merge') {
$formData['source_columns'] = $letters($p['source_columns'] ?? []);
$formData['target_column'] = indexToCol($p['target_column'] ?? 0);
$formData['separator'] = $p['separator'] ?? ' ';
$formData['skip_empty'] = !empty($p['skip_empty']);
} elseif ($s['operation'] === 'copy') {
$formData['source_column'] = indexToCol($p['source_column'] ?? 0);
$formData['target_column'] = indexToCol($p['target_column'] ?? 0);
} elseif (in_array($s['operation'], ['clear', 'trim', 'uppercase', 'lowercase'], true)) {
$formData['columns'] = $letters($p['columns'] ?? []);
if ($s['operation'] === 'trim') {
$formData['chars'] = $p['chars'] ?? '';
}
} elseif ($s['operation'] === 'replace') {
$formData['columns'] = $letters($p['columns'] ?? []);
$formData['search'] = $p['search'] ?? '';
$formData['replace'] = $p['replace'] ?? '';
$formData['regex'] = !empty($p['regex']);
}
$stepsForJs[(int)$s['idstep']] = $formData;
}
$flash = $_SESSION['routine_steps_flash'] ?? null;
unset($_SESSION['routine_steps_flash']);
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>Step Routine - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<style>
.small-muted {
font-size: 12px;
color: #6c757d;
}
.table-actions {
white-space: nowrap;
}
.op-badge {
font-family: Consolas, Monaco, monospace;
font-size: 12px;
background: #eef2ff;
color: #4338ca;
border: 1px solid #e0e7ff;
border-radius: 6px;
padding: 2px 8px;
text-transform: uppercase;
}
.step-inactive {
opacity: 0.55;
}
.col-hint {
background: #f0fdf4;
border: 1px solid #bbf7d0;
color: #166534;
border-radius: 8px;
padding: 8px 12px;
font-size: 13px;
}
.order-btns .btn {
padding: 2px 8px;
}
</style>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<?php include('top_stat_widget.php'); ?>
<div class="card radius-10">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between">
<div>
<h6 class="mb-0">Step routine: <?= htmlspecialchars($routine['name'], ENT_QUOTES, 'UTF-8'); ?></h6>
<small class="text-muted">
Configura le operazioni applicate all'XLS. Vengono eseguite dall'alto verso il basso.
</small>
</div>
<div>
<a href="routine_admin.php" class="btn btn-light">
<i class="bx bx-arrow-back"></i> Torna alle routine
</a>
<button type="button" class="btn btn-primary" id="btnAddStep">
<i class="bx bx-plus"></i> Aggiungi step
</button>
</div>
</div>
</div>
<div class="card-body">
<div class="col-hint mb-3">
Le colonne si indicano con le <strong>lettere dell'Excel</strong> (A, B, C, …). Per più colonne separale con la virgola: <strong>D, F, H</strong>.
</div>
<div class="table-responsive">
<table class="table table-bordered align-middle">
<thead>
<tr>
<th style="width: 70px;">Ordine</th>
<th style="width: 120px;">Operazione</th>
<th>Dettaglio</th>
<th style="width: 90px;">Stato</th>
<th style="width: 240px;">Azioni</th>
</tr>
</thead>
<tbody>
<?php if (empty($steps)): ?>
<tr>
<td colspan="5" class="text-center small-muted py-4">
Nessuno step configurato. Usa "Aggiungi step" per iniziare.
</td>
</tr>
<?php else: ?>
<?php foreach ($steps as $i => $s): ?>
<?php
$p = json_decode($s['params'] ?? '[]', true);
if (!is_array($p)) {
$p = [];
}
$isFirst = ($i === 0);
$isLast = ($i === count($steps) - 1);
$rowClass = ((int)$s['active'] === 1) ? '' : 'step-inactive';
?>
<tr class="<?= $rowClass; ?>">
<td class="text-center">
<div class="order-btns d-inline-flex flex-column gap-1">
<button type="button" class="btn btn-sm btn-outline-secondary btnMove"
data-idstep="<?= (int)$s['idstep']; ?>" data-direction="up"
<?= $isFirst ? 'disabled' : ''; ?>>
<i class="bx bx-chevron-up"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-secondary btnMove"
data-idstep="<?= (int)$s['idstep']; ?>" data-direction="down"
<?= $isLast ? 'disabled' : ''; ?>>
<i class="bx bx-chevron-down"></i>
</button>
</div>
</td>
<td><span class="op-badge"><?= htmlspecialchars($s['operation'], ENT_QUOTES, 'UTF-8'); ?></span></td>
<td><?= htmlspecialchars(describeStep($s['operation'], $p), ENT_QUOTES, 'UTF-8'); ?></td>
<td>
<?php if ((int)$s['active'] === 1): ?>
<span class="badge bg-success">Attivo</span>
<?php else: ?>
<span class="badge bg-secondary">Disattivo</span>
<?php endif; ?>
</td>
<td class="table-actions">
<button type="button" class="btn btn-sm btn-outline-primary btnEditStep"
data-idstep="<?= (int)$s['idstep']; ?>">
Modifica
</button>
<button type="button" class="btn btn-sm btn-outline-danger btnDeleteStep"
data-idstep="<?= (int)$s['idstep']; ?>">
Elimina
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<hr class="my-4">
<div class="d-flex align-items-center justify-content-between mb-2">
<div>
<h6 class="mb-0">Prova la routine</h6>
<small class="text-muted">Carica un XLS di esempio: viene mostrata la prima riga dati dopo l'intestazione, elaborata con gli step qui sopra. Il file non viene salvato.</small>
</div>
</div>
<div class="row g-2 align-items-end">
<div class="col-md-8">
<label for="previewFile" class="form-label">File XLS / XLSX di prova</label>
<input type="file" class="form-control" id="previewFile" accept=".xls,.xlsx">
</div>
<div class="col-md-4">
<button type="button" class="btn btn-outline-primary w-100" id="btnRunPreview">
<i class="bx bx-play-circle"></i> Esegui anteprima
</button>
</div>
</div>
<div id="previewStatus" class="small-muted mt-2"></div>
<div class="table-responsive mt-3" id="previewResultWrapper" style="display:none;">
<table class="table table-bordered table-sm align-middle" id="previewTable">
<thead>
<tr>
<th style="width: 90px;">Colonna</th>
<th style="width: 40%;">Intestazione</th>
<th>Valore dopo la routine</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
</div>
<!-- Modal Step -->
<div class="modal fade" id="stepModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<form method="POST" class="modal-content" id="stepForm">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="idroutine" value="<?= (int)$idroutine; ?>">
<input type="hidden" name="action" id="stepFormAction" value="create">
<input type="hidden" name="idstep" id="idstep" value="">
<div class="modal-header">
<h5 class="modal-title" id="stepModalTitle">Aggiungi step</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<div class="modal-body">
<div class="row g-3">
<div class="col-md-8">
<label for="operation" class="form-label">Operazione *</label>
<select class="form-select" id="operation" name="operation" required>
<option value="merge">merge — unisci più colonne in una</option>
<option value="copy">copy — copia una colonna in un'altra</option>
<option value="clear">clear — svuota colonne</option>
<option value="trim">trim — rimuovi spazi</option>
<option value="replace">replace — sostituisci testo</option>
<option value="uppercase">uppercase — MAIUSCOLO</option>
<option value="lowercase">lowercase — minuscolo</option>
</select>
</div>
<div class="col-md-4">
<label for="active" class="form-label">Stato *</label>
<select class="form-select" id="active" name="active" required>
<option value="1">Attivo</option>
<option value="0">Disattivo</option>
</select>
</div>
<!-- Campi MERGE -->
<div class="col-md-8 op-field op-merge">
<label class="form-label">Colonne sorgente *</label>
<input type="text" class="form-control" name="source_columns" id="merge_source" placeholder="es. D, F, H">
<div class="small-muted mt-1">Lettere separate da virgola.</div>
</div>
<div class="col-md-4 op-field op-merge">
<label class="form-label">Colonna destinazione *</label>
<input type="text" class="form-control" name="target_column" id="merge_target" placeholder="es. J">
</div>
<div class="col-md-8 op-field op-merge">
<label class="form-label">Separatore</label>
<input type="text" class="form-control" name="separator" id="merge_sep" value=" " placeholder="spazio">
<div class="small-muted mt-1">Carattere inserito tra i valori uniti. Default: spazio.</div>
</div>
<div class="col-md-4 op-field op-merge d-flex align-items-end">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="skip_empty" id="merge_skip" checked>
<label class="form-check-label" for="merge_skip">Ignora valori vuoti</label>
</div>
</div>
<!-- Campi COPY -->
<div class="col-md-6 op-field op-copy">
<label class="form-label">Colonna sorgente *</label>
<input type="text" class="form-control" name="source_column" id="copy_source" placeholder="es. D">
</div>
<div class="col-md-6 op-field op-copy">
<label class="form-label">Colonna destinazione *</label>
<input type="text" class="form-control" name="target_column_copy" id="copy_target" placeholder="es. J">
</div>
<!-- Campi COLONNE (clear/trim/uppercase/lowercase/replace) -->
<div class="col-md-12 op-field op-columns">
<label class="form-label">Colonne *</label>
<input type="text" class="form-control" name="columns" id="columns_input" placeholder="es. D, F">
<div class="small-muted mt-1">Una o più lettere separate da virgola.</div>
</div>
<!-- Campo TRIM chars -->
<div class="col-md-12 op-field op-trim">
<label class="form-label">Caratteri da rimuovere (opzionale)</label>
<input type="text" class="form-control" name="chars" id="trim_chars" placeholder="lascia vuoto per spazi">
</div>
<!-- Campi REPLACE -->
<div class="col-md-6 op-field op-replace">
<label class="form-label">Testo da cercare</label>
<input type="text" class="form-control" name="search" id="replace_search">
</div>
<div class="col-md-6 op-field op-replace">
<label class="form-label">Testo sostitutivo</label>
<input type="text" class="form-control" name="replace" id="replace_replace">
</div>
<div class="col-md-12 op-field op-replace">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="regex" id="replace_regex">
<label class="form-check-label" for="replace_regex">
Usa espressione regolare (regex) — solo se sai cosa stai facendo
</label>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-primary" id="saveStepButton">Salva step</button>
</div>
</form>
</div>
</div>
<!-- Form nascosti -->
<form method="POST" id="deleteStepForm" style="display:none;">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="idroutine" value="<?= (int)$idroutine; ?>">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="idstep" id="deleteIdStep" value="">
</form>
<form method="POST" id="moveStepForm" style="display:none;">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="idroutine" value="<?= (int)$idroutine; ?>">
<input type="hidden" name="action" value="move">
<input type="hidden" name="idstep" id="moveIdStep" value="">
<input type="hidden" name="direction" id="moveDirection" value="">
</form>
<?php include('jsinclude.php'); ?>
<script>
const stepsData = <?= json_encode($stepsForJs, JSON_UNESCAPED_UNICODE); ?>;
document.addEventListener('DOMContentLoaded', function() {
const stepModal = new bootstrap.Modal(document.getElementById('stepModal'));
const stepFormAction = document.getElementById('stepFormAction');
const idstep = document.getElementById('idstep');
const stepModalTitle = document.getElementById('stepModalTitle');
const saveStepButton = document.getElementById('saveStepButton');
const operation = document.getElementById('operation');
const active = document.getElementById('active');
// Mappa quali gruppi di campi mostrare per ogni operazione.
const fieldGroups = {
merge: ['op-merge'],
copy: ['op-copy'],
clear: ['op-columns'],
trim: ['op-columns', 'op-trim'],
replace: ['op-columns', 'op-replace'],
uppercase: ['op-columns'],
lowercase: ['op-columns']
};
function showFieldsForOperation(op) {
// Nasconde tutti i gruppi.
document.querySelectorAll('.op-field').forEach(function(el) {
el.style.display = 'none';
});
// Mostra i gruppi dell'operazione scelta.
(fieldGroups[op] || []).forEach(function(cls) {
document.querySelectorAll('.' + cls).forEach(function(el) {
el.style.display = '';
});
});
}
operation.addEventListener('change', function() {
showFieldsForOperation(operation.value);
});
function resetStepForm() {
document.getElementById('stepForm').reset();
// reset non ripristina i checkbox "checked" di default in modo affidabile: li forziamo
document.getElementById('merge_skip').checked = true;
document.getElementById('replace_regex').checked = false;
document.getElementById('merge_sep').value = ' ';
}
function openForCreate() {
stepFormAction.value = 'create';
idstep.value = '';
stepModalTitle.textContent = 'Aggiungi step';
saveStepButton.textContent = 'Aggiungi step';
resetStepForm();
operation.value = 'merge';
active.value = '1';
showFieldsForOperation('merge');
stepModal.show();
}
function openForEdit(id) {
const data = stepsData[id];
if (!data) {
return;
}
stepFormAction.value = 'update';
idstep.value = id;
stepModalTitle.textContent = 'Modifica step';
saveStepButton.textContent = 'Aggiorna step';
resetStepForm();
operation.value = data.operation;
active.value = String(data.active);
// Popola i campi in base all'operazione.
if (data.operation === 'merge') {
document.getElementById('merge_source').value = data.source_columns || '';
document.getElementById('merge_target').value = data.target_column || '';
document.getElementById('merge_sep').value = (data.separator !== undefined ? data.separator : ' ');
document.getElementById('merge_skip').checked = !!data.skip_empty;
} else if (data.operation === 'copy') {
document.getElementById('copy_source').value = data.source_column || '';
document.getElementById('copy_target').value = data.target_column || '';
} else if (['clear', 'trim', 'uppercase', 'lowercase'].includes(data.operation)) {
document.getElementById('columns_input').value = data.columns || '';
if (data.operation === 'trim') {
document.getElementById('trim_chars').value = data.chars || '';
}
} else if (data.operation === 'replace') {
document.getElementById('columns_input').value = data.columns || '';
document.getElementById('replace_search').value = data.search || '';
document.getElementById('replace_replace').value = data.replace || '';
document.getElementById('replace_regex').checked = !!data.regex;
}
showFieldsForOperation(data.operation);
stepModal.show();
}
/*
* NOTA sul campo destinazione di copy:
* l'input HTML si chiama target_column_copy per non collidere con merge,
* ma il backend si aspetta target_column. Rinominiamo al submit.
*/
document.getElementById('stepForm').addEventListener('submit', function(e) {
if (operation.value === 'copy') {
const copyTarget = document.getElementById('copy_target');
// Crea/aggiorna un campo hidden target_column col valore di copy.
let hidden = document.getElementById('copy_target_hidden');
if (!hidden) {
hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.name = 'target_column';
hidden.id = 'copy_target_hidden';
this.appendChild(hidden);
}
hidden.value = copyTarget.value;
}
});
document.getElementById('btnAddStep').addEventListener('click', openForCreate);
document.querySelectorAll('.btnEditStep').forEach(function(btn) {
btn.addEventListener('click', function() {
openForEdit(btn.dataset.idstep);
});
});
document.querySelectorAll('.btnDeleteStep').forEach(function(btn) {
btn.addEventListener('click', function() {
Swal.fire({
title: 'Eliminare lo step?',
text: 'Operazione irreversibile.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Sì, elimina',
cancelButtonText: 'Annulla',
confirmButtonColor: '#d33'
}).then(function(result) {
if (result.isConfirmed) {
document.getElementById('deleteIdStep').value = btn.dataset.idstep;
document.getElementById('deleteStepForm').submit();
}
});
});
});
document.querySelectorAll('.btnMove').forEach(function(btn) {
btn.addEventListener('click', function() {
document.getElementById('moveIdStep').value = btn.dataset.idstep;
document.getElementById('moveDirection').value = btn.dataset.direction;
document.getElementById('moveStepForm').submit();
});
});
// --- Anteprima routine su XLS caricato (nessun salvataggio) ---
const previewFile = document.getElementById('previewFile');
const btnRunPreview = document.getElementById('btnRunPreview');
const previewStatus = document.getElementById('previewStatus');
const previewResultWrapper = document.getElementById('previewResultWrapper');
const previewTableBody = document.querySelector('#previewTable tbody');
const currentIdRoutine = "<?= (int)$idroutine; ?>";
btnRunPreview.addEventListener('click', function() {
if (!previewFile.files || previewFile.files.length === 0) {
Swal.fire({
icon: 'info',
title: 'Nessun file',
text: 'Seleziona un file XLS di prova.'
});
return;
}
const formData = new FormData();
formData.append('idroutine', currentIdRoutine);
formData.append('file', previewFile.files[0]);
previewStatus.textContent = 'Elaborazione in corso...';
previewResultWrapper.style.display = 'none';
previewTableBody.innerHTML = '';
btnRunPreview.disabled = true;
fetch('routine_preview.php', {
method: 'POST',
body: formData,
cache: 'no-store'
})
.then(function(res) {
return res.json();
})
.then(function(data) {
btnRunPreview.disabled = false;
if (data.error) {
previewStatus.textContent = '';
Swal.fire({
icon: 'warning',
title: 'Anteprima non riuscita',
text: data.error
});
return;
}
previewStatus.textContent = 'Riga elaborata: riga Excel ' + data.excelrow + '.';
const letters = data.letters || [];
const headers = data.headers || [];
const row = data.row || [];
for (let i = 0; i < row.length; i++) {
const tr = document.createElement('tr');
const tdCol = document.createElement('td');
tdCol.innerHTML = '<strong>' + (letters[i] || '') + '</strong>';
const tdHead = document.createElement('td');
tdHead.className = 'small-muted';
tdHead.textContent = headers[i] || '';
const tdVal = document.createElement('td');
tdVal.textContent = row[i] || '';
tr.appendChild(tdCol);
tr.appendChild(tdHead);
tr.appendChild(tdVal);
previewTableBody.appendChild(tr);
}
previewResultWrapper.style.display = '';
})
.catch(function(err) {
btnRunPreview.disabled = false;
previewStatus.textContent = '';
Swal.fire({
icon: 'error',
title: 'Errore',
text: 'Impossibile eseguire l\'anteprima.'
});
console.error(err);
});
});
<?php if ($flash): ?>
Swal.fire({
icon: '<?= htmlspecialchars($flash['type'], ENT_QUOTES, 'UTF-8'); ?>',
title: '<?= $flash['type'] === 'success' ? 'Fatto' : ($flash['type'] === 'warning' ? 'Attenzione' : 'Errore'); ?>',
text: '<?= htmlspecialchars($flash['message'], ENT_QUOTES, 'UTF-8'); ?>'
});
<?php endif; ?>
});
</script>
</body>
</html>
+296
View File
@@ -0,0 +1,296 @@
<?php
/**
* Routine motore generico (_engine.php)
* -------------------------------------
* File UNICO condiviso da tutte le routine con engine = 1.
*
* Espone la stessa firma delle routine legacy:
*
* applyRoutine(&$excelData, $routineData)
*
* così apply_routine.php lo include allo stesso modo. La differenza è che qui
* NON c'è logica hardcoded: gli step vengono letti dalla tabella routine_step
* (in base a routineData['idroutine']) ed eseguiti in ordine.
*
* Ogni step ha una `operation` (verbo del vocabolario) e `params` JSON.
* Le colonne nei params possono essere indici zero-based (3) o lettere Excel ("D").
*/
if (!function_exists('applyRoutine')) {
/**
* Converte un riferimento colonna in indice zero-based.
* Accetta int/indici numerici (3) o lettere Excel ("D", "AA", ...).
*/
function routineEngineColToIndex($ref): int
{
if (is_int($ref)) {
return $ref;
}
$ref = trim((string)$ref);
if ($ref === '') {
return 0;
}
// Numerico puro -> indice diretto
if (ctype_digit($ref)) {
return (int)$ref;
}
// Lettere Excel -> indice zero-based (A=0, B=1, ..., Z=25, AA=26, ...)
$ref = strtoupper($ref);
$index = 0;
$len = strlen($ref);
for ($i = 0; $i < $len; $i++) {
$c = $ref[$i];
if ($c < 'A' || $c > 'Z') {
continue;
}
$index = $index * 26 + (ord($c) - ord('A') + 1);
}
return $index - 1;
}
/** Normalizza una lista di riferimenti colonna in indici zero-based. */
function routineEngineColsToIndexes(array $refs): array
{
return array_map('routineEngineColToIndex', $refs);
}
/**
* Carica gli step attivi di una routine dal DB, ordinati per step_order.
* Ritorna array di ['operation' => string, 'params' => array].
*/
function routineEngineLoadSteps(int $idroutine): array
{
// Riusa la connessione PDO del progetto.
// db-functions.php sta in .../class/ rispetto alla cartella routines/.
// Aggiusta il require se nel tuo albero il path differisce.
$dbFile = dirname(__DIR__) . '/class/db-functions.php';
if (!class_exists('DBHandlerSelect') && file_exists($dbFile)) {
require_once $dbFile;
}
if (!class_exists('DBHandlerSelect')) {
throw new Exception("Motore routine: classe DBHandlerSelect non trovata (path db-functions.php da verificare).");
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->prepare(
"SELECT operation, params
FROM routine_step
WHERE idroutine = :id AND active = 1
ORDER BY step_order ASC, idstep ASC"
);
$stmt->execute([':id' => $idroutine]);
$rows = $stmt->fetchAll();
$steps = [];
foreach ($rows as $r) {
$params = [];
if (!empty($r['params'])) {
$decoded = json_decode($r['params'], true);
if (is_array($decoded)) {
$params = $decoded;
} else {
error_log("Motore routine: params JSON non valido per operazione {$r['operation']} (routine {$idroutine}).");
}
}
$steps[] = ['operation' => strtolower(trim($r['operation'])), 'params' => $params];
}
return $steps;
}
/* ------------------------------------------------------------------ *
* HANDLERS – ogni operazione modifica $excelData in place.
* Aggiungere una capacità nuova = aggiungere un handler qui, una volta.
* ------------------------------------------------------------------ */
/**
* merge: unisce N colonne sorgente in una destinazione.
* params: source_columns [], target_column, separator=" ", skip_empty=true
*/
function routineOpMerge(array &$excelData, array $params): void
{
$sources = routineEngineColsToIndexes($params['source_columns'] ?? []);
$target = routineEngineColToIndex($params['target_column'] ?? 0);
$sep = array_key_exists('separator', $params) ? (string)$params['separator'] : ' ';
$skip = $params['skip_empty'] ?? true;
foreach ($excelData as $rowIndex => &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("merge: struttura riga non valida all'indice {$rowIndex}.");
continue;
}
$parts = [];
foreach ($sources as $col) {
$val = trim((string)($row['data'][$col] ?? ''));
if ($skip && $val === '') {
continue;
}
$parts[] = $val;
}
$row['data'][$target] = implode($sep, $parts);
}
unset($row);
}
/**
* copy: copia una colonna sorgente in una destinazione.
* params: source_column, target_column
*/
function routineOpCopy(array &$excelData, array $params): void
{
$src = routineEngineColToIndex($params['source_column'] ?? 0);
$dst = routineEngineColToIndex($params['target_column'] ?? 0);
foreach ($excelData as &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
continue;
}
$row['data'][$dst] = $row['data'][$src] ?? '';
}
unset($row);
}
/**
* clear: svuota una o più colonne.
* params: columns [] (oppure column singola)
*/
function routineOpClear(array &$excelData, array $params): void
{
$cols = $params['columns'] ?? (isset($params['column']) ? [$params['column']] : []);
$cols = routineEngineColsToIndexes($cols);
foreach ($excelData as &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
continue;
}
foreach ($cols as $c) {
$row['data'][$c] = '';
}
}
unset($row);
}
/**
* trim: rimuove spazi (o caratteri indicati) da una o più colonne.
* params: columns [] (o column), chars (opzionale)
*/
function routineOpTrim(array &$excelData, array $params): void
{
$cols = $params['columns'] ?? (isset($params['column']) ? [$params['column']] : []);
$cols = routineEngineColsToIndexes($cols);
$chars = $params['chars'] ?? null;
foreach ($excelData as &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
continue;
}
foreach ($cols as $c) {
$v = (string)($row['data'][$c] ?? '');
$row['data'][$c] = $chars === null ? trim($v) : trim($v, $chars);
}
}
unset($row);
}
/**
* replace: sostituzione testo in una o più colonne.
* params: columns [] (o column), search, replace="", regex=false
*/
function routineOpReplace(array &$excelData, array $params): void
{
$cols = $params['columns'] ?? (isset($params['column']) ? [$params['column']] : []);
$cols = routineEngineColsToIndexes($cols);
$search = $params['search'] ?? '';
$replace = $params['replace'] ?? '';
$regex = $params['regex'] ?? false;
foreach ($excelData as &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
continue;
}
foreach ($cols as $c) {
$v = (string)($row['data'][$c] ?? '');
if ($regex) {
$out = @preg_replace($search, $replace, $v);
$row['data'][$c] = $out === null ? $v : $out;
} else {
$row['data'][$c] = str_replace($search, $replace, $v);
}
}
}
unset($row);
}
/** uppercase: maiuscolo su una o più colonne. params: columns [] (o column) */
function routineOpUppercase(array &$excelData, array $params): void
{
$cols = $params['columns'] ?? (isset($params['column']) ? [$params['column']] : []);
$cols = routineEngineColsToIndexes($cols);
foreach ($excelData as &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
continue;
}
foreach ($cols as $c) {
$row['data'][$c] = mb_strtoupper((string)($row['data'][$c] ?? ''), 'UTF-8');
}
}
unset($row);
}
/** lowercase: minuscolo su una o più colonne. params: columns [] (o column) */
function routineOpLowercase(array &$excelData, array $params): void
{
$cols = $params['columns'] ?? (isset($params['column']) ? [$params['column']] : []);
$cols = routineEngineColsToIndexes($cols);
foreach ($excelData as &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
continue;
}
foreach ($cols as $c) {
$row['data'][$c] = mb_strtolower((string)($row['data'][$c] ?? ''), 'UTF-8');
}
}
unset($row);
}
/**
* Entry point richiamato da apply_routine.php.
* Stessa firma delle routine legacy.
*/
function applyRoutine(&$excelData, $routineData = [])
{
$idroutine = isset($routineData['idroutine']) ? (int)$routineData['idroutine'] : 0;
if ($idroutine <= 0) {
throw new Exception("Motore routine: idroutine mancante in routineData.");
}
$steps = routineEngineLoadSteps($idroutine);
if (empty($steps)) {
error_log("Motore routine: nessuno step attivo per routine {$idroutine}.");
return;
}
// Dispatch table verbo -> handler
$handlers = [
'merge' => 'routineOpMerge',
'copy' => 'routineOpCopy',
'clear' => 'routineOpClear',
'trim' => 'routineOpTrim',
'replace' => 'routineOpReplace',
'uppercase' => 'routineOpUppercase',
'lowercase' => 'routineOpLowercase',
];
foreach ($steps as $i => $step) {
$op = $step['operation'];
if (!isset($handlers[$op])) {
error_log("Motore routine: operazione sconosciuta '{$op}' (routine {$idroutine}, step " . ($i + 1) . ") — saltata.");
continue;
}
$handlers[$op]($excelData, $step['params']);
error_log("Motore routine: applicata operazione '{$op}' (routine {$idroutine}, step " . ($i + 1) . ").");
}
error_log("Motore routine {$idroutine} completato (" . count($steps) . " step).");
}
}
+101
View File
@@ -0,0 +1,101 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
require_once __DIR__ . '/class/binding-functions.php';
include dirname(__DIR__) . '/../extra/auth.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
if (!Auth::check()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
exit;
}
$kind = ($_POST['kind'] ?? 'custom') === 'fixed' ? 'fixed' : 'custom';
$templateId = intval($_POST['template_id'] ?? 0);
$mappingId = intval($_POST['mapping_id'] ?? 0);
$fieldId = intval($_POST['field_id'] ?? 0);
$sourceValue = (string) ($_POST['source_value'] ?? '');
$limsValueId = intval($_POST['lims_value_id'] ?? 0);
$limsValue = (string) ($_POST['lims_value'] ?? '');
$datadbIds = [];
if (isset($_POST['datadb_ids'])) {
$decoded = json_decode($_POST['datadb_ids'], true);
if (is_array($decoded)) {
$datadbIds = $decoded;
}
}
if ($templateId <= 0 || $mappingId <= 0 || trim($sourceValue) === '' || $limsValueId <= 0) {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Missing required parameters']);
exit;
}
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$createdBy = Auth::user() ? (int) Auth::user()->present()->id : null;
if ($kind === 'fixed') {
$stmt = $pdo->prepare("SELECT fixed_field_key FROM template_fixed_mapping WHERE id = ?");
$stmt->execute([$mappingId]);
$fixedKey = (string) ($stmt->fetchColumn() ?: '');
if ($fixedKey === '' || !binding_fixed_is_list($fixedKey)) {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Invalid fixed field']);
exit;
}
binding_upsert($pdo, $templateId, $mappingId, 0, $sourceValue, $limsValueId, $limsValue, $createdBy, 'fixed');
$applied = !empty($datadbIds)
? binding_apply_to_datadb($pdo, $fixedKey, (string) $limsValueId, $datadbIds)
: 0;
echo json_encode([
'success' => true,
'applied_rows' => $applied,
'kind' => 'fixed',
'source_value' => $sourceValue,
'lims_value' => $limsValue,
'lims_value_id' => $limsValueId,
]);
exit;
}
if ($fieldId <= 0) {
$stmt = $pdo->prepare("SELECT field_id FROM template_mapping WHERE id = ?");
$stmt->execute([$mappingId]);
$fieldId = (int) ($stmt->fetchColumn() ?: 0);
}
binding_upsert($pdo, $templateId, $mappingId, $fieldId, $sourceValue, $limsValueId, $limsValue, $createdBy);
$applied = !empty($datadbIds)
? binding_apply_to_details($pdo, $mappingId, (string) $limsValueId, $datadbIds)
: 0;
echo json_encode([
'success' => true,
'applied_rows' => $applied,
'kind' => 'custom',
'source_value' => $sourceValue,
'lims_value' => $limsValue,
'lims_value_id' => $limsValueId,
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
+7 -2
View File
@@ -22,6 +22,8 @@ try {
$analysisLevel = isset($_POST['analysis_level']) && $_POST['analysis_level'] !== '' ? (int)$_POST['analysis_level'] : null;
$isWebSelectable = isset($_POST['is_web_selectable']) ? (int)$_POST['is_web_selectable'] : 0;
$isAccredited = isset($_POST['is_accredited']) ? (int)$_POST['is_accredited'] : 0;
$analysisPriceRaw = str_replace(',', '.', trim((string)($_POST['analysis_price'] ?? '')));
$analysisPrice = is_numeric($analysisPriceRaw) ? round((float)$analysisPriceRaw, 2) : null;
if ($partId <= 0 || $analysisRecordkey === '') {
http_response_code(400);
@@ -42,7 +44,8 @@ try {
analysis_method,
analysis_level,
is_web_selectable,
is_accredited
is_accredited,
analysis_price
) VALUES (
:part_id,
:iddatadb,
@@ -52,7 +55,8 @@ try {
:analysis_method,
:analysis_level,
:is_web_selectable,
:is_accredited
:is_accredited,
:analysis_price
)
");
@@ -66,6 +70,7 @@ try {
':analysis_level' => $analysisLevel,
':is_web_selectable' => $isWebSelectable,
':is_accredited' => $isAccredited,
':analysis_price' => $analysisPrice,
]);
echo json_encode([
-917
View File
@@ -1,917 +0,0 @@
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#SchemaCustomField",
"value": [
{
"IdSchemaCustomFields": 41,
"ConteggioClienti": 0,
"Nome": "Labostudio",
"Descrizione": null
},
{
"IdSchemaCustomFields": 42,
"ConteggioClienti": 0,
"Nome": "Schema 1",
"Descrizione": null
},
{
"IdSchemaCustomFields": 43,
"ConteggioClienti": 0,
"Nome": "Schema1",
"Descrizione": null
},
{
"IdSchemaCustomFields": 44,
"ConteggioClienti": 0,
"Nome": "Cuoio\/Pelle Standard \/ Leather",
"Descrizione": "Schema per tutti i campioni in cuoio\/pelle per i quali non \u00e8 specificato un destinatario.\r\n"
},
{
"IdSchemaCustomFields": 45,
"ConteggioClienti": 0,
"Nome": "Borse",
"Descrizione": null
},
{
"IdSchemaCustomFields": 46,
"ConteggioClienti": 0,
"Nome": "Borse Burberry",
"Descrizione": null
},
{
"IdSchemaCustomFields": 47,
"ConteggioClienti": 0,
"Nome": "pelle calzatura",
"Descrizione": null
},
{
"IdSchemaCustomFields": 48,
"ConteggioClienti": 0,
"Nome": "Standard \/ Generico",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 49,
"ConteggioClienti": 0,
"Nome": "Accessori Metallici \/ Metallic Accessories",
"Descrizione": "Schema per tutti gli Accessori Metallici\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 50,
"ConteggioClienti": 0,
"Nome": "Tessile \/ Textiles",
"Descrizione": "Schema per tutti i Tessuti\r\n"
},
{
"IdSchemaCustomFields": 52,
"ConteggioClienti": 0,
"Nome": "Brasport",
"Descrizione": null
},
{
"IdSchemaCustomFields": 54,
"ConteggioClienti": 0,
"Nome": "Chanel Calzatura",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 55,
"ConteggioClienti": 0,
"Nome": "Burberry",
"Descrizione": null
},
{
"IdSchemaCustomFields": 57,
"ConteggioClienti": 0,
"Nome": "Z_Ralph Lauren - Tessile",
"Descrizione": "Schema per tutti i campioni in tessuto per Ralph Lauren.\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 58,
"ConteggioClienti": 0,
"Nome": "LIU JO",
"Descrizione": null
},
{
"IdSchemaCustomFields": 59,
"ConteggioClienti": 0,
"Nome": "Migrazione tra Componenti",
"Descrizione": null
},
{
"IdSchemaCustomFields": 60,
"ConteggioClienti": 0,
"Nome": "Prova Outsourcing",
"Descrizione": null
},
{
"IdSchemaCustomFields": 61,
"ConteggioClienti": 0,
"Nome": "Marc Jacobs",
"Descrizione": null
},
{
"IdSchemaCustomFields": 62,
"ConteggioClienti": 0,
"Nome": "A. TESTONI",
"Descrizione": null
},
{
"IdSchemaCustomFields": 63,
"ConteggioClienti": 0,
"Nome": "Chanel CHINA PROJECT",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 66,
"ConteggioClienti": 0,
"Nome": "GUESS",
"Descrizione": null
},
{
"IdSchemaCustomFields": 68,
"ConteggioClienti": 0,
"Nome": "Valentino",
"Descrizione": null
},
{
"IdSchemaCustomFields": 69,
"ConteggioClienti": 0,
"Nome": "DESIGUAL",
"Descrizione": null
},
{
"IdSchemaCustomFields": 70,
"ConteggioClienti": 0,
"Nome": "FENDI PELLETTERIA",
"Descrizione": "Solo per FENDI Pelletteria_INGLESE\r\n"
},
{
"IdSchemaCustomFields": 71,
"ConteggioClienti": 0,
"Nome": "Z_Cuoio\/Pelle Standard ACCREDIA",
"Descrizione": "Schema per tutti i campioni in cuoio\/pelle per i quali non \u00e8 specificato un destinatario.\r\n"
},
{
"IdSchemaCustomFields": 72,
"ConteggioClienti": 0,
"Nome": "Z_BALLY",
"Descrizione": "sia per calzatura che pelletteria. Aggiunto campo TEST DESCRIPTION, compreso nella fatturazione per le divisioni pelletteria.\r\n\r\n"
},
{
"IdSchemaCustomFields": 73,
"ConteggioClienti": 0,
"Nome": "Z_Ralph Lauren - Leather",
"Descrizione": null
},
{
"IdSchemaCustomFields": 74,
"ConteggioClienti": 0,
"Nome": "Z_Ralph Lauren - Fabric",
"Descrizione": null
},
{
"IdSchemaCustomFields": 76,
"ConteggioClienti": 0,
"Nome": "NORDSTROM",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 77,
"ConteggioClienti": 0,
"Nome": "ONWARD LUXURY GROUP",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 78,
"ConteggioClienti": 0,
"Nome": "P&G SRL_Progetto Durability",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 79,
"ConteggioClienti": 0,
"Nome": "Bally qualsiasi divisione",
"Descrizione": null
},
{
"IdSchemaCustomFields": 80,
"ConteggioClienti": 0,
"Nome": "FENDI CALZATURA",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 81,
"ConteggioClienti": 0,
"Nome": "LouBoutin",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 82,
"ConteggioClienti": 0,
"Nome": "MONCLER Brand",
"Descrizione": "Da utilizzare solo per il Brand Diretto\r\nGR 19\/03\/2024"
},
{
"IdSchemaCustomFields": 83,
"ConteggioClienti": 0,
"Nome": "JIMMY CHOO",
"Descrizione": null
},
{
"IdSchemaCustomFields": 84,
"ConteggioClienti": 0,
"Nome": "Yves Saint Laurent",
"Descrizione": null
},
{
"IdSchemaCustomFields": 85,
"ConteggioClienti": 0,
"Nome": "BENETTON",
"Descrizione": null
},
{
"IdSchemaCustomFields": 86,
"ConteggioClienti": 0,
"Nome": "UPLOAD REPORT",
"Descrizione": null
},
{
"IdSchemaCustomFields": 87,
"ConteggioClienti": 0,
"Nome": "Hermes Calzatura",
"Descrizione": "\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 88,
"ConteggioClienti": 0,
"Nome": "C&A",
"Descrizione": "Schema per fornitori e C&A diretto\r\n"
},
{
"IdSchemaCustomFields": 89,
"ConteggioClienti": 0,
"Nome": "AMERICAN EAGLE",
"Descrizione": "Schema per American Eagle\/Todd snyder\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 91,
"ConteggioClienti": 0,
"Nome": "Ferragamo",
"Descrizione": "\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 93,
"ConteggioClienti": 0,
"Nome": "Z_Alexander McQueen",
"Descrizione": "\r\n\r\n"
},
{
"IdSchemaCustomFields": 95,
"ConteggioClienti": 0,
"Nome": "Balenciaga Pelletteria",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 96,
"ConteggioClienti": 0,
"Nome": "DUNHILL",
"Descrizione": "SCHEMA DUNHILL tutti i tipi di campioni\r\n"
},
{
"IdSchemaCustomFields": 97,
"ConteggioClienti": 0,
"Nome": "PRELIEVI ACQUE",
"Descrizione": null
},
{
"IdSchemaCustomFields": 98,
"ConteggioClienti": 0,
"Nome": "LouBoutin_Tossicologico",
"Descrizione": null
},
{
"IdSchemaCustomFields": 99,
"ConteggioClienti": 0,
"Nome": "ROBAN'S ",
"Descrizione": "tutte le divisioni\r\n\r\n"
},
{
"IdSchemaCustomFields": 100,
"ConteggioClienti": 0,
"Nome": "PROGETTO LUGGAGE",
"Descrizione": "Progetto Luggage Hermes\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 102,
"ConteggioClienti": 0,
"Nome": "Balmain",
"Descrizione": "Schema per tutti i campioni Balmain"
},
{
"IdSchemaCustomFields": 103,
"ConteggioClienti": 0,
"Nome": "ZDHC",
"Descrizione": "Schema di Riferimento ZDHC Wastewaters"
},
{
"IdSchemaCustomFields": 104,
"ConteggioClienti": 0,
"Nome": "GUCCI CDC 4172",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 105,
"ConteggioClienti": 0,
"Nome": "Z_ASOS",
"Descrizione": null
},
{
"IdSchemaCustomFields": 106,
"ConteggioClienti": 0,
"Nome": "Z_BOUX AVENUE",
"Descrizione": "28\/09 no indicazioni su esigenze di fatturazione (detto da Frosini)\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 107,
"ConteggioClienti": 0,
"Nome": "BONPOINT",
"Descrizione": "05\/10 creazione\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 108,
"ConteggioClienti": 0,
"Nome": "Canada Goose",
"Descrizione": null
},
{
"IdSchemaCustomFields": 109,
"ConteggioClienti": 0,
"Nome": "DIESEL",
"Descrizione": null
},
{
"IdSchemaCustomFields": 110,
"ConteggioClienti": 0,
"Nome": "AQC_Leather",
"Descrizione": null
},
{
"IdSchemaCustomFields": 111,
"ConteggioClienti": 0,
"Nome": "BEAUMANOIR (CC-Mor-Bon-Breal)",
"Descrizione": "04\/02\/21_ AGGIORNATO 05\/08\r\n"
},
{
"IdSchemaCustomFields": 112,
"ConteggioClienti": 0,
"Nome": "BooHoo",
"Descrizione": null
},
{
"IdSchemaCustomFields": 114,
"ConteggioClienti": 0,
"Nome": "Golden Goose",
"Descrizione": "Golden goose\r\n"
},
{
"IdSchemaCustomFields": 115,
"ConteggioClienti": 0,
"Nome": "Z_ZLABEL",
"Descrizione": "Schema per ZLABEL"
},
{
"IdSchemaCustomFields": 116,
"ConteggioClienti": 0,
"Nome": "Zadig & Voltaire",
"Descrizione": "Schema per Zadig & Voltaire"
},
{
"IdSchemaCustomFields": 117,
"ConteggioClienti": 0,
"Nome": "Gemo",
"Descrizione": "Schema solo per Gemo\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 118,
"ConteggioClienti": 0,
"Nome": "PROMOD",
"Descrizione": "CHF 06\/08\r\n\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 119,
"ConteggioClienti": 0,
"Nome": "LA HALLE (BEAUMANOIR)",
"Descrizione": "13\/08\/2021 CHF\r\n\r\n"
},
{
"IdSchemaCustomFields": 120,
"ConteggioClienti": 0,
"Nome": "LA REDOUTE",
"Descrizione": "14\/09\/2021 AD\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 121,
"ConteggioClienti": 0,
"Nome": "TOD'S",
"Descrizione": "Schema per materiale gb18401 e GB20400\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 122,
"ConteggioClienti": 0,
"Nome": "ASOS rev 1",
"Descrizione": null
},
{
"IdSchemaCustomFields": 123,
"ConteggioClienti": 0,
"Nome": "Monoprix",
"Descrizione": "Schema per Monoprix\r\n\r\n"
},
{
"IdSchemaCustomFields": 124,
"ConteggioClienti": 0,
"Nome": "DIOR Ita",
"Descrizione": "CHF 21\/03\r\n"
},
{
"IdSchemaCustomFields": 125,
"ConteggioClienti": 0,
"Nome": "GEOX - Raw Material",
"Descrizione": "SCHEMA DA USARE SOLO PER GEOX - SOLO PER LE SUOLE E MATERIALI DENOMINATI RPU \/ RAW MATERIAL"
},
{
"IdSchemaCustomFields": 126,
"ConteggioClienti": 0,
"Nome": "CAMAIEU",
"Descrizione": "SCHEMA DA USARE SOLO PER CAMAIEU\r\n"
},
{
"IdSchemaCustomFields": 127,
"ConteggioClienti": 0,
"Nome": "Z_Golden Goose FTW",
"Descrizione": "Golden goose\r\n"
},
{
"IdSchemaCustomFields": 128,
"ConteggioClienti": 0,
"Nome": "STEVE MADDEN - FOOTWEAR",
"Descrizione": "DA UTILIZZARE SOLO PER DESTINAZIONE D'USO CALZATURA\r\n"
},
{
"IdSchemaCustomFields": 129,
"ConteggioClienti": 0,
"Nome": "Z_Carhartt",
"Descrizione": "SCHEMA DA UTILIZZARE SOLO PER RICHIESTE UFFICIALI CARHARTT\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 131,
"ConteggioClienti": 0,
"Nome": "DAMART",
"Descrizione": "\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 132,
"ConteggioClienti": 0,
"Nome": "Gruppo OTB",
"Descrizione": "Schema per Staff International, Diesel e Brave kid\r\n\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 133,
"ConteggioClienti": 0,
"Nome": "FERRARI",
"Descrizione": "Schema da usare per Ferrari e fornitori\r\n\r\n"
},
{
"IdSchemaCustomFields": 134,
"ConteggioClienti": 0,
"Nome": "GAP - ABBIGLIAMENTO",
"Descrizione": "Schema GAP da utilizzare solo per destinazione abbigliamento\r\n"
},
{
"IdSchemaCustomFields": 135,
"ConteggioClienti": 0,
"Nome": "GAP - CALZATURA",
"Descrizione": "Schema GAP da utilizzare solo per destinazione calzatura\r\n\r\n"
},
{
"IdSchemaCustomFields": 136,
"ConteggioClienti": 0,
"Nome": "SALLING GROUP",
"Descrizione": "Schema per Salling Group\r\n\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 137,
"ConteggioClienti": 0,
"Nome": "SOFT SURROUNDINGS",
"Descrizione": "Schema da utilizzare per Soft Surroundings\r\n\r\n"
},
{
"IdSchemaCustomFields": 138,
"ConteggioClienti": 0,
"Nome": "E-LIMS - Cuoio\/Pelle Standard \/ Leather",
"Descrizione": "Schema da utilizzare solo per accettazioni online\r\n"
},
{
"IdSchemaCustomFields": 139,
"ConteggioClienti": 0,
"Nome": "Z_ZALANDO",
"Descrizione": "Schema per ZALANDO ------ (NON per ZLABEL che ha il suo)\r\n"
},
{
"IdSchemaCustomFields": 140,
"ConteggioClienti": 0,
"Nome": "LVMH",
"Descrizione": "gennaio 2023\r\n"
},
{
"IdSchemaCustomFields": 141,
"ConteggioClienti": 0,
"Nome": "Valentino - eLims",
"Descrizione": "SCHEMA DA UTILIZZARE SOLO PER L'ACCETTAZIONE ONLINE DI VALENTINO"
},
{
"IdSchemaCustomFields": 142,
"ConteggioClienti": 0,
"Nome": "FENDI PELLETTERIA COMPLIANCE",
"Descrizione": "Solo per FENDI Pelletteria_INGLESE 04\/05\/2023\r\n"
},
{
"IdSchemaCustomFields": 143,
"ConteggioClienti": 0,
"Nome": "TAPESTRY",
"Descrizione": "04\/05\/2023, agg secondo procedura 08\/11\/23\r\n"
},
{
"IdSchemaCustomFields": 144,
"ConteggioClienti": 0,
"Nome": "ALEXANDER MC QUEEN",
"Descrizione": "ALEXANDER MC QUEEN 15\/05\/2023\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 145,
"ConteggioClienti": 0,
"Nome": "Hermes_Chimici_Parigi",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 146,
"ConteggioClienti": 0,
"Nome": "CELINE ABBIGLIAMENTO_Progetto Speciale",
"Descrizione": "IO 202 CELINE ABBIGLIAMENTO_06\/06\/203_CHF\r\n\r\n"
},
{
"IdSchemaCustomFields": 147,
"ConteggioClienti": 0,
"Nome": "GEOX - Prodotto Finito",
"Descrizione": "SCHEMA DA USARE SOLO PER GEOX - SOLO PER PRODOTTO FINITO"
},
{
"IdSchemaCustomFields": 149,
"ConteggioClienti": 0,
"Nome": "Hermes\/IDO",
"Descrizione": "creato il 05\/07\/2023 CHF\r\n"
},
{
"IdSchemaCustomFields": 150,
"ConteggioClienti": 0,
"Nome": "Tessile \/ Textiles PER CLIENTI CON OBLO",
"Descrizione": "Schema per tutti i Tessuti QUANDO IL CLIENTE NECESSITA DI NR OBLO\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 151,
"ConteggioClienti": 0,
"Nome": "J JIll",
"Descrizione": "Schema per cliente J JIll, necessario riportare la recommended fiber content in schema verde\r\n\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 152,
"ConteggioClienti": 0,
"Nome": "AUCHAN - TAPE A L'OEIL",
"Descrizione": "Schema AUCHAN da utilizzare solo per la divisione TAPE A L'OEIL SA, successivamente se uguale da usare anche per le altre divisioni ed uniformarlo come generico"
},
{
"IdSchemaCustomFields": 153,
"ConteggioClienti": 0,
"Nome": "ALL SAINTS - BUSCEMI",
"Descrizione": "SCHE DA USARE SOLO PER ALL SAINTS E BUSCEMI\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 154,
"ConteggioClienti": 0,
"Nome": "CELINE - LVMH",
"Descrizione": "Schema con aggiunta campi LVMH per estrazioni\r\nAD-29\/03\/2024"
},
{
"IdSchemaCustomFields": 155,
"ConteggioClienti": 0,
"Nome": "SeyMeChamLou",
"Descrizione": "DA USARE SOLO PER SEYMECHAMLOU (LOUBOUTIN)\r\n"
},
{
"IdSchemaCustomFields": 156,
"ConteggioClienti": 0,
"Nome": "PRADA",
"Descrizione": "PER ADESSO DA USARE PER ABBIGLIAMENTO E CALZATURA\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 157,
"ConteggioClienti": 0,
"Nome": "Hermes_HCP",
"Descrizione": "Schema per unicamente HCP"
},
{
"IdSchemaCustomFields": 158,
"ConteggioClienti": 0,
"Nome": "STONE ISLAND",
"Descrizione": "Schema per tutti i campioni in cuoio\/pelle per i quali non \u00e8 specificato un destinatario.\r\n"
},
{
"IdSchemaCustomFields": 159,
"ConteggioClienti": 0,
"Nome": "Chanel_Pierre Damien",
"Descrizione": "Schema da usare per Campioni ricevuti da Pierre-Damien Verine e suo staff CHANEL.\r\n"
},
{
"IdSchemaCustomFields": 160,
"ConteggioClienti": 0,
"Nome": "Z_ACNE",
"Descrizione": "\r\n.\r\n"
},
{
"IdSchemaCustomFields": 161,
"ConteggioClienti": 0,
"Nome": "OBERALP GROUP",
"Descrizione": "Schema da usare solo per Oberalp Group\r\n"
},
{
"IdSchemaCustomFields": 162,
"ConteggioClienti": 0,
"Nome": "Club Monaco",
"Descrizione": "da usare solo per Club Monaco GR 20\/02\/2024"
},
{
"IdSchemaCustomFields": 163,
"ConteggioClienti": 0,
"Nome": "Devred",
"Descrizione": "Schema creato per cliente DEVRED\r\nGR 18\/03\/2024 aggiornato il 23\/04\/2026\r\n\r\n"
},
{
"IdSchemaCustomFields": 164,
"ConteggioClienti": 0,
"Nome": "MONCLER Supplier",
"Descrizione": "Da utilizzare per fornitori Moncler \r\nGR 19\/03\/2024"
},
{
"IdSchemaCustomFields": 165,
"ConteggioClienti": 0,
"Nome": "Kering Sunglasses",
"Descrizione": "Schema per tutti i campioni di qualsiasi matrice escluso cuoio\/pelle\r\n\r\n"
},
{
"IdSchemaCustomFields": 166,
"ConteggioClienti": 0,
"Nome": "Giorgio Armani",
"Descrizione": "\r\n\r\n"
},
{
"IdSchemaCustomFields": 167,
"ConteggioClienti": 0,
"Nome": "Chanel USA",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 168,
"ConteggioClienti": 0,
"Nome": "Staff Calzatura",
"Descrizione": "Schema per tutti i campioni di qualsiasi matrice escluso cuoio\/pelle\r\n\r\n"
},
{
"IdSchemaCustomFields": 169,
"ConteggioClienti": 0,
"Nome": "P&G SRL",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 170,
"ConteggioClienti": 0,
"Nome": "INCOM",
"Descrizione": "Schema da utilizzare per INCOM"
},
{
"IdSchemaCustomFields": 171,
"ConteggioClienti": 0,
"Nome": "RIVER ISLAND",
"Descrizione": "SCHEDA DA USARE SOLO PER RIVER ISLAND - NO CALZATURA\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 172,
"ConteggioClienti": 0,
"Nome": "Longchamp",
"Descrizione": "Schema per tutti i campioni con RIchiesta Longchamp ufficiale\r\n"
},
{
"IdSchemaCustomFields": 173,
"ConteggioClienti": 0,
"Nome": "Bottega Veneta",
"Descrizione": "Schema per tutti i campioni di Bottega Veneta di Stella"
},
{
"IdSchemaCustomFields": 174,
"ConteggioClienti": 0,
"Nome": "Mint Velvet",
"Descrizione": "Schema per tutti i campioni di Mint Velvel con richiesta ufficiale.\r\n\r\n"
},
{
"IdSchemaCustomFields": 176,
"ConteggioClienti": 0,
"Nome": "RIVER ISLAND FOOTWEAR",
"Descrizione": "SCHEDA DA USARE SOLO PER RIVER ISLAND - USARE SOLO PER\r\n CALZATURA\r\n"
},
{
"IdSchemaCustomFields": 177,
"ConteggioClienti": 0,
"Nome": "Phoebe Philo ",
"Descrizione": "\r\n\r\n"
},
{
"IdSchemaCustomFields": 178,
"ConteggioClienti": 0,
"Nome": "New Guards Group",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 179,
"ConteggioClienti": 0,
"Nome": "Giorgio Armani Operation",
"Descrizione": "\r\n\r\n"
},
{
"IdSchemaCustomFields": 180,
"ConteggioClienti": 0,
"Nome": "Chloe'",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 181,
"ConteggioClienti": 0,
"Nome": "LVMH non diretti",
"Descrizione": "utilizzato ogni qualvolta siano caricati pacchetti \/ prove singole che non rientrano nel programma LVMH \r\n"
},
{
"IdSchemaCustomFields": 182,
"ConteggioClienti": 0,
"Nome": "Ralph Lauren - All testing V.10",
"Descrizione": "AGGIORNAMENTO AL 16\/03\/2026 per estrazione fatturazione"
},
{
"IdSchemaCustomFields": 183,
"ConteggioClienti": 0,
"Nome": "TWINSET",
"Descrizione": "Schema da usare per Twinset - come da mail di EP del 20\/05\/2025\r\n\r\n"
},
{
"IdSchemaCustomFields": 184,
"ConteggioClienti": 0,
"Nome": "ACNE STUDIOS rev 01",
"Descrizione": null
},
{
"IdSchemaCustomFields": 185,
"ConteggioClienti": 0,
"Nome": "Brunello cucinelli",
"Descrizione": null
},
{
"IdSchemaCustomFields": 186,
"ConteggioClienti": 0,
"Nome": "TWINSET",
"Descrizione": "Schema per tutti i campioni di qualsiasi matrice escluso cuoio\/pelle\r\n\r\n"
},
{
"IdSchemaCustomFields": 187,
"ConteggioClienti": 0,
"Nome": "LORO PIANA",
"Descrizione": "Schema per tutti i campioni Loro Piana\r\n"
},
{
"IdSchemaCustomFields": 188,
"ConteggioClienti": 0,
"Nome": "LEMAIRE",
"Descrizione": "Schema per tutti i campioni in cuoio\/pelle per i quali non \u00e8 specificato un destinatario.\r\n"
},
{
"IdSchemaCustomFields": 189,
"ConteggioClienti": 0,
"Nome": "TEST_IT_SCHEMA",
"Descrizione": "Test per IT"
},
{
"IdSchemaCustomFields": 190,
"ConteggioClienti": 0,
"Nome": "LBS Interscambio",
"Descrizione": "Schema per tutti i campioni interscambio LBS\r\n"
},
{
"IdSchemaCustomFields": 191,
"ConteggioClienti": 0,
"Nome": "KIABI",
"Descrizione": "Schema per il cliente Kiabi\r\n\r\n"
},
{
"IdSchemaCustomFields": 192,
"ConteggioClienti": 0,
"Nome": "INDITEX",
"Descrizione": "Schema da usare solo per richieste INDITEX"
},
{
"IdSchemaCustomFields": 193,
"ConteggioClienti": 0,
"Nome": "AQC_Other materials",
"Descrizione": null
},
{
"IdSchemaCustomFields": 194,
"ConteggioClienti": 0,
"Nome": "Hermes ADM & Silk",
"Descrizione": "SCHEMA DA USARE SOLO PER LA DIVISIONE ADM & SILK\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 195,
"ConteggioClienti": 0,
"Nome": "Chloe'_APPAREL",
"Descrizione": "Schema aggiornato il 18\/12\/25 secondo modulo excel x codifica Elims\r\nGR"
},
{
"IdSchemaCustomFields": 196,
"ConteggioClienti": 0,
"Nome": "DUNHILL_DRAFT NON USARE",
"Descrizione": "SCHEMA DUNHILL NON USARE STUDIO NUOVA PROCEDURA NOVEMBRE 2025\r\n\r\n"
},
{
"IdSchemaCustomFields": 197,
"ConteggioClienti": 0,
"Nome": "BERLUTI - LVMH",
"Descrizione": "Creato per accettazione Elims\r\n"
},
{
"IdSchemaCustomFields": 198,
"ConteggioClienti": 0,
"Nome": "PIAGET (gruppo Cartier)",
"Descrizione": "Schema per tutti i campioni PIAGET (gruppo Cartier)\r\n\r\n"
},
{
"IdSchemaCustomFields": 199,
"ConteggioClienti": 0,
"Nome": "HERMES PAP FEMME",
"Descrizione": "Schema per tutti i campioni di HERMES PAP FEMME \r\n15\/01\/2025\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 200,
"ConteggioClienti": 0,
"Nome": "CARHARTT ( provvisiorio in revisione)",
"Descrizione": "Schema per tutti i campioni CARHARTT provvissorio\r\n\r\n"
},
{
"IdSchemaCustomFields": 201,
"ConteggioClienti": 0,
"Nome": "ROSSIMODA",
"Descrizione": "Per tutti i campioni di ROSSIMODA\r\n\r\n"
},
{
"IdSchemaCustomFields": 202,
"ConteggioClienti": 0,
"Nome": "LIMS-CIM - MAX MARA",
"Descrizione": "Schema per MAX MARA scambio dati Database"
},
{
"IdSchemaCustomFields": 203,
"ConteggioClienti": 0,
"Nome": "Vince",
"Descrizione": "Schema per tutti i campioni di VINCE\r\n\r\n"
},
{
"IdSchemaCustomFields": 204,
"ConteggioClienti": 0,
"Nome": "Max Mara",
"Descrizione": "Schema da usare per Max Mara\r\n"
},
{
"IdSchemaCustomFields": 205,
"ConteggioClienti": 0,
"Nome": "Chanel Flammability",
"Descrizione": "Schema per Chanel Flammability\r\n"
},
{
"IdSchemaCustomFields": 206,
"ConteggioClienti": 0,
"Nome": "COLDWATER CREEK",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 208,
"ConteggioClienti": 0,
"Nome": "ETON",
"Descrizione": "Schema da usare per ETON\r\n"
}
]
}
+1 -1
View File
@@ -51,7 +51,7 @@ try {
// Load from cache or API
$cacheFile = __DIR__ . '/cache/clienti.json';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 7200)) {
$data = json_decode(file_get_contents($cacheFile), true);
} else {
$api = VisualLimsApiClient::getInstance();
@@ -29,7 +29,7 @@ try {
$cacheDir = __DIR__ . '/cache';
$cacheFile = $cacheDir . '/customfield_' . $fieldId . '.json';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 7200)) {
$values = json_decode(file_get_contents($cacheFile), true);
} else {
$api = VisualLimsApiClient::getInstance();
@@ -0,0 +1,54 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
require_once __DIR__ . '/class/binding-functions.php';
include dirname(__DIR__) . '/../extra/auth.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
if (!Auth::check()) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
$fieldKey = trim($_GET['field_key'] ?? '');
$templateId = (int) ($_GET['template_id'] ?? 0);
$q = binding_normalize_value($_GET['q'] ?? '');
$rawLimit = (int) ($_GET['limit'] ?? 50);
$limit = $rawLimit <= 0 ? 0 : max(1, min(500, $rawLimit));
if ($fieldKey === '' || !binding_fixed_is_list($fieldKey)) {
echo json_encode(['results' => []]);
exit;
}
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$values = binding_get_fixed_values($pdo, $fieldKey, $templateId);
if ($q !== '') {
$terms = array_values(array_filter(preg_split('/\s+/u', $q, -1, PREG_SPLIT_NO_EMPTY)));
$values = array_values(array_filter($values, function ($v) use ($terms) {
$hay = binding_normalize_value($v['text'] ?? '');
foreach ($terms as $t) {
if (mb_strpos($hay, $t) === false) {
return false;
}
}
return true;
}));
}
if ($limit > 0) {
$values = array_slice($values, 0, $limit);
}
echo json_encode(['results' => $values]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage(), 'results' => []]);
}
+294
View File
@@ -0,0 +1,294 @@
<?php
include('include/headscript.php');
require_once __DIR__ . '/class/Mailer.class.php';
header('Content-Type: application/json');
/**
* Risolve [IdCliente => Nominativo] leggendo la stessa cache di search_clienti.php.
* Non chiama l'API: usa cache/clienti.json (già mantenuto aggiornato dalla ricerca clienti).
*/
function resolveClientNames(array $ids): array
{
$ids = array_values(array_filter(array_map('intval', $ids), fn($v) => $v > 0));
if (empty($ids)) return [];
$cacheFile = __DIR__ . '/cache/clienti.json';
if (!file_exists($cacheFile)) return [];
$data = json_decode((string)@file_get_contents($cacheFile), true);
$clients = $data['value'] ?? [];
if (empty($clients)) return [];
// indicizza per IdCliente
$byId = [];
foreach ($clients as $c) {
$cid = (int)($c['IdCliente'] ?? 0);
if ($cid > 0) {
$byId[$cid] = trim((string)($c['Nominativo'] ?? ''));
}
}
$out = [];
foreach ($ids as $id) {
if (isset($byId[$id]) && $byId[$id] !== '') {
$out[$id] = $byId[$id];
}
}
return $out;
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// input: solo POST (endpoint che invia email — niente attivazione via URL)
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito']);
exit;
}
$ids = $_POST['ids'] ?? [];
if (!is_array($ids)) $ids = explode(',', (string)$ids);
$ids = array_values(array_filter(array_map('intval', $ids), fn($v) => $v > 0));
$templateId = (int)($_POST['template_id'] ?? 0);
if (empty($ids)) {
echo json_encode(['success' => false, 'message' => 'Nessun id']);
exit;
}
// Fallback: se il template_id non arriva, ricavalo dalla prima commessa
if ($templateId <= 0) {
$tq = $pdo->prepare("SELECT templateid FROM datadb WHERE iddatadb = ? LIMIT 1");
$tq->execute([$ids[0]]);
$templateId = (int)$tq->fetchColumn();
}
// tutti i main field del template (possono essere fino a 2), in ordine
$mainMappingIds = [];
$templateName = '';
if ($templateId > 0) {
$s = $pdo->prepare("
SELECT id FROM template_mapping
WHERE template_id = ? AND main_field = '1' AND is_visible_import = 1
ORDER BY field_order ASC, id ASC
");
$s->execute([$templateId]);
$mainMappingIds = array_map('intval', $s->fetchAll(PDO::FETCH_COLUMN));
// nome del template per l'intestazione mail
$t = $pdo->prepare("SELECT name FROM excel_templates WHERE id = ? LIMIT 1");
$t->execute([$templateId]);
$templateName = (string)$t->fetchColumn();
}
// esiti delle commesse
$ph = implode(',', array_fill(0, count($ids), '?'));
$sql = "
SELECT d.iddatadb, d.commessaweb, d.status, d.user_id,
d.idclient, d.ClienteFatturazione,
CONCAT(u.first_name,' ',u.last_name) AS user_name,
u.email AS user_email, u.notify_export_email
FROM datadb d
LEFT JOIN auth_users u ON d.user_id = u.id
WHERE d.iddatadb IN ($ph)
";
$stmt = $pdo->prepare($sql);
$stmt->execute($ids);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($rows)) {
echo json_encode(['success' => false, 'message' => 'Righe non trovate']);
exit;
}
// --- Risoluzione nomi cliente (idclient + ClienteFatturazione) ---
$clientIdsToResolve = [];
foreach ($rows as $r) {
if (!empty($r['idclient'])) $clientIdsToResolve[(int)$r['idclient']] = true;
if (!empty($r['ClienteFatturazione'])) $clientIdsToResolve[(int)$r['ClienteFatturazione']] = true;
}
$clientNames = resolveClientNames(array_keys($clientIdsToResolve)); // [id => nome]
// helper: id cliente → nome, con fallback all'id se non risolto
$clientLabel = function ($id) use ($clientNames) {
$id = (int)$id;
if ($id <= 0) return '—';
return htmlspecialchars($clientNames[$id] ?? (string)$id);
};
// valori dei main field per ogni iddatadb — concatena i main valorizzati
$mainValues = [];
if (!empty($mainMappingIds)) {
$mph = implode(',', array_fill(0, count($mainMappingIds), '?'));
$s = $pdo->prepare("
SELECT id AS iddatadb, mapping_id, field_value
FROM import_data_details
WHERE id IN ($ph) AND mapping_id IN ($mph)
");
$s->execute(array_merge($ids, $mainMappingIds));
// raggruppa per iddatadb, mantenendo l'ordine dei main
$byRow = []; // iddatadb => [mapping_id => value]
foreach ($s->fetchAll(PDO::FETCH_ASSOC) as $r) {
$byRow[(int)$r['iddatadb']][(int)$r['mapping_id']] = $r['field_value'];
}
foreach ($byRow as $idd => $vals) {
$parts = [];
foreach ($mainMappingIds as $mid) { // ordine dei main
$v = trim((string)($vals[$mid] ?? ''));
if ($v !== '') $parts[] = $v;
}
$mainValues[$idd] = implode(' — ', $parts); // es. "descrizione — style code"
}
}
// analisi senza prezzo (NULL o 0) per ogni iddatadb
$noPriceByRow = []; // iddatadb => [ [part_number, part_description, analysis_name], ... ]
$s = $pdo->prepare("
SELECT a.iddatadb, MAX(a.analysis_name) AS analysis_name
FROM identification_parts_analyses a
WHERE a.iddatadb IN ($ph)
AND (a.analysis_price IS NULL OR a.analysis_price <= 0)
GROUP BY a.iddatadb, a.analysis_recordkey
ORDER BY a.iddatadb, analysis_name
");
$s->execute($ids);
foreach ($s->fetchAll(PDO::FETCH_ASSOC) as $r) {
$noPriceByRow[(int)$r['iddatadb']][] = $r;
}
$noPriceTotal = array_sum(array_map('count', $noPriceByRow));
// destinatari: utente/i dell'export che hanno la notifica attiva + admin fisso
$recipients = [];
foreach ($rows as $r) {
if (!empty($r['user_email']) && (int)($r['notify_export_email'] ?? 1) === 1) {
$recipients[$r['user_email']] = true;
}
}
$adminEmail = env('EXPORT_SUMMARY_ADMIN', 'admin@cesoft.io'); // sempre, indipendente dalla preferenza
if ($adminEmail) $recipients[$adminEmail] = true;
$recipients = array_keys($recipients);
// Se dopo il filtro non resta nessuno, non inviare
if (empty($recipients)) {
echo json_encode(['success' => true, 'message' => 'Nessun destinatario (notifiche disattivate)', 'recipients' => []]);
exit;
}
// conteggi
$okCount = 0;
$errCount = 0;
foreach ($rows as $r) {
if ($r['status'] === 'l') $okCount++;
elseif ($r['status'] === 'x') $errCount++;
}
$hasError = $errCount > 0;
// --- tabella righe ---
$rowsHtml = '';
foreach ($rows as $r) {
$st = $r['status'];
if ($st === 'l') {
$label = 'Inviata';
$bg = '#e6f4ea';
$col = '#1e7e34';
} elseif ($st === 'x') {
$label = 'ERRORE';
$bg = '#fdecea';
$col = '#c0392b';
} else {
$label = htmlspecialchars((string)$st);
$bg = '#fff8e1';
$col = '#8a6d3b';
}
$commessa = htmlspecialchars($r['commessaweb'] ?: '—');
$main = htmlspecialchars($mainValues[(int)$r['iddatadb']] ?? '—');
$committente = $clientLabel($r['idclient'] ?? 0);
$fatturazione = $clientLabel($r['ClienteFatturazione'] ?? 0);
$rowsHtml .= "<tr>
<td style='padding:10px 10px;border-bottom:1px solid #eee;font-family:monospace;white-space:nowrap;vertical-align:top;'>{$commessa}</td>
<td style='padding:10px 10px;border-bottom:1px solid #eee;word-break:break-word;vertical-align:top;'>{$committente}</td>
<td style='padding:10px 10px;border-bottom:1px solid #eee;word-break:break-word;vertical-align:top;'>{$fatturazione}</td>
<td style='padding:10px 10px;border-bottom:1px solid #eee;word-break:break-word;vertical-align:top;'>{$main}</td>
<td style='padding:10px 10px;border-bottom:1px solid #eee;text-align:center;white-space:nowrap;vertical-align:top;'>
<span style='display:inline-block;padding:3px 10px;border-radius:12px;background:{$bg};color:{$col};font-weight:600;font-size:13px;'>{$label}</span>
</td>
</tr>";
// sotto-riga: solo le analisi No Price di questa commessa
$np = $noPriceByRow[(int)$r['iddatadb']] ?? [];
if (!empty($np)) {
$items = '';
foreach ($np as $n) {
$analysisName = htmlspecialchars((string)($n['analysis_name'] ?: 'Unnamed analysis'));
$items .= "<li style='margin:2px 0;'>{$analysisName}</li>";
}
$npCount = count($np);
$rowsHtml .= "<tr>
<td colspan='5' style='padding:8px 12px 12px 24px;border-bottom:1px solid #eee;background:#fff5f5;'>
<div style='margin-bottom:4px;font-size:12px;color:#c0392b;font-weight:600;'>
<span style='display:inline-block;padding:2px 8px;border-radius:10px;background:#c0392b;color:#fff;font-size:11px;'>No price</span>
&nbsp;Analisi senza prezzo ({$npCount})
</div>
<ul style='margin:0;padding-left:18px;font-size:13px;color:#555;'>{$items}</ul>
</td>
</tr>";
}
}
$headerColor = $hasError ? '#c0392b' : '#1e7e34';
$headerText = $hasError ? 'Export completato con errori' : 'Export completato con successo';
$total = count($rows);
$html = "
<div style='font-family:Arial,Helvetica,sans-serif;max-width:820px;margin:0 auto;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden;'>
<div style='background:{$headerColor};color:#fff;padding:18px 24px;'>
<h2 style='margin:0;font-size:18px;'>{$headerText}</h2>
<p style='margin:6px 0 0;font-size:13px;opacity:.9;'>Riepilogo esportazione verso LIMS" .
($templateName !== '' ? " &middot; Template: <strong>" . htmlspecialchars($templateName) . "</strong>" : "") . "</p>
<p style='margin:6px 0 0;font-size:13px;opacity:.95;'>
Committente: <strong>" . $clientLabel($rows[0]['idclient'] ?? 0) . "</strong>
&nbsp;&middot;&nbsp;
FatturazioneAvanzata: <strong>" . $clientLabel($rows[0]['ClienteFatturazione'] ?? 0) . "</strong>
</p>
</div>
<div style='padding:20px 24px;'>
<p style='margin:0 0 14px;font-size:14px;color:#333;'>
Totale commesse: <strong>{$total}</strong> &nbsp;•&nbsp;
<span style='color:#1e7e34;'>Inviate: {$okCount}</span> &nbsp;•&nbsp;
<span style='color:#c0392b;'>Errori: {$errCount}</span>" .
($noPriceTotal > 0 ? " &nbsp;•&nbsp; <span style='color:#c0392b;'>Analisi No price: {$noPriceTotal}</span>" : "") . "
</p>
<table style='width:100%;border-collapse:collapse;font-size:14px;table-layout:fixed;'>
<thead>
<tr style='background:#f5f5f5;'>
<th style='padding:10px 10px;text-align:left;border-bottom:2px solid #ddd;width:14%;'>Commessa</th>
<th style='padding:10px 10px;text-align:left;border-bottom:2px solid #ddd;width:24%;'>Committente</th>
<th style='padding:10px 10px;text-align:left;border-bottom:2px solid #ddd;width:24%;'>Fatturazione</th>
<th style='padding:10px 10px;text-align:left;border-bottom:2px solid #ddd;width:24%;'>Riferimento</th>
<th style='padding:10px 10px;text-align:center;border-bottom:2px solid #ddd;width:14%;'>Status</th>
</tr>
</thead>
<tbody>{$rowsHtml}</tbody>
</table>
" . ($hasError ? "<p style='margin:18px 0 0;padding:12px;background:#fdecea;border-left:4px solid #c0392b;color:#c0392b;font-size:13px;border-radius:4px;'>⚠️ Alcune commesse non sono state inviate al LIMS. Controlla la sezione <strong>Errors</strong> nell'applicazione per i dettagli e per rilanciarle.</p>" : "") . "
</div>
<div style='background:#fafafa;padding:14px 24px;font-size:12px;color:#999;border-top:1px solid #eee;'>
Messaggio automatico da SmartTRF — non rispondere a questa email.
</div>
</div>";
$subjectPrefix = $hasError ? '⚠️ Errori Export LIMS' : '✅ Export LIMS OK';
$subject = "{$subjectPrefix} — {$total} commesse ({$okCount} ok, {$errCount} err)";
$res = Mailer::send($recipients, $subject, $html);
echo json_encode([
'success' => $res['ok'],
'message' => $res['ok'] ? 'Mail inviata' : ('Mail non inviata: ' . $res['error']),
'recipients' => $recipients,
'counts' => ['ok' => $okCount, 'err' => $errCount, 'total' => $total],
]);
+16
View File
@@ -0,0 +1,16 @@
<?php
require_once __DIR__ . '/include/headscript.php'; // autentica e dà $iduserlogin
require_once __DIR__ . '/class/db-functions.php';
header('Content-Type: application/json');
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->prepare(
"UPDATE auth_sessions SET last_activity = ? WHERE user_id = ? ORDER BY last_activity DESC LIMIT 1"
);
$stmt->execute([time(), $iduserlogin]);
echo json_encode(['ok' => true, 'rows' => $stmt->rowCount(), 'ts' => time()]);
} catch (\Throwable $e) {
echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
}
+71
View File
@@ -0,0 +1,71 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
require_once __DIR__ . '/class/binding-functions.php';
include dirname(__DIR__) . '/../extra/auth.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
if (!Auth::check()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
exit;
}
$kind = ($_POST['kind'] ?? 'custom') === 'fixed' ? 'fixed' : 'custom';
$mappingId = intval($_POST['mapping_id'] ?? 0);
$sourceValue = (string) ($_POST['source_value'] ?? '');
$datadbIds = [];
if (isset($_POST['datadb_ids'])) {
$decoded = json_decode($_POST['datadb_ids'], true);
if (is_array($decoded)) {
$datadbIds = $decoded;
}
}
if ($mappingId <= 0 || trim($sourceValue) === '') {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Missing required parameters']);
exit;
}
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
if ($kind === 'fixed') {
$stmt = $pdo->prepare("SELECT fixed_field_key FROM template_fixed_mapping WHERE id = ?");
$stmt->execute([$mappingId]);
$fixedKey = (string) ($stmt->fetchColumn() ?: '');
if ($fixedKey === '') {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Invalid fixed field']);
exit;
}
$cleared = !empty($datadbIds)
? binding_apply_to_datadb($pdo, $fixedKey, null, $datadbIds)
: 0;
} else {
$cleared = !empty($datadbIds)
? binding_apply_to_details($pdo, $mappingId, '', $datadbIds)
: 0;
}
binding_delete($pdo, $mappingId, $sourceValue, $kind);
echo json_encode(['success' => true, 'cleared_rows' => $cleared]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
@@ -0,0 +1,19 @@
-- json_node e default_source possono gia' esistere: in quel caso saltare le ADD COLUMN.
ALTER TABLE `template_fixed_mapping`
ADD COLUMN `json_node` varchar(500) DEFAULT NULL,
ADD COLUMN `default_source` enum('manual','xls','json') NOT NULL DEFAULT 'manual';
ALTER TABLE `template_fixed_mapping`
ADD COLUMN `excel_column` varchar(255) DEFAULT NULL AFTER `json_node`;
ALTER TABLE `template_fixed_mapping`
MODIFY COLUMN `default_source` enum('manual','xls','json') NOT NULL DEFAULT 'manual';
ALTER TABLE `lims_value_binding`
ADD COLUMN `binding_kind` enum('custom','fixed') NOT NULL DEFAULT 'custom' AFTER `template_id`,
MODIFY COLUMN `field_id` int(11) NOT NULL DEFAULT 0;
ALTER TABLE `lims_value_binding` DROP INDEX `uq_mapping_source`;
ALTER TABLE `lims_value_binding`
ADD UNIQUE KEY `uq_kind_mapping_source` (`binding_kind`, `mapping_id`, `source_key`);
@@ -0,0 +1,17 @@
CREATE TABLE IF NOT EXISTS `lims_value_binding` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`template_id` int(11) NOT NULL,
`mapping_id` int(11) NOT NULL,
`field_id` int(11) NOT NULL,
`source_value` varchar(500) NOT NULL,
`source_key` varchar(500) NOT NULL,
`lims_value_id` int(11) NOT NULL,
`lims_value` varchar(500) DEFAULT NULL,
`created_by` int(11) DEFAULT NULL,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_mapping_source` (`mapping_id`, `source_key`),
KEY `idx_template` (`template_id`),
KEY `idx_field_source` (`field_id`, `source_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+284 -82
View File
@@ -227,8 +227,9 @@ function fixedDefaultValue(array $f): string
// ── Build lookup maps: id → label for fixed fields ──
$fixedLookup = []; // e.g. $fixedLookup['MoltiplicatorePrezzo'][2] = 'Urgente (1.5x)'
function loadCacheJson(string $path): ?array {
if (file_exists($path) && (time() - filemtime($path) < 3600)) {
function loadCacheJson(string $path): ?array
{
if (file_exists($path) && (time() - filemtime($path) < 7200)) {
return json_decode(file_get_contents($path), true);
}
return null;
@@ -278,7 +279,8 @@ foreach ($clienteIds as $cid) {
}
// Helper: resolve fixed field id → label
function resolveFixedValue(string $key, $val, array $fixedLookup): string {
function resolveFixedValue(string $key, $val, array $fixedLookup): string
{
if ($val === '' || $val === null) return '';
if (isset($fixedLookup[$key][(int)$val])) return $fixedLookup[$key][(int)$val];
return (string)$val;
@@ -649,6 +651,7 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
.grid-top {
display: none !important;
}
.grid-top-original {
display: flex;
align-items: stretch;
@@ -819,12 +822,12 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
}
.action-btn {
padding: 6px 8px;
margin-right: 5px;
padding: 6px 4px;
margin-right: 3px;
border: none;
border-radius: 5px;
cursor: pointer;
width: 35px;
width: 26px;
box-sizing: border-box;
}
@@ -952,15 +955,35 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
}
/* Hide grid-top (propagate row) */
.grid-top { display: none !important; }
.grid-top {
display: none !important;
}
/* Text cells styling */
.grid-row .grid-cell span { font-size: 12px; padding: 2px 4px; }
.propagate-btn { display: none !important; }
.button-cell { flex: 0 0 120px !important; }
.grid-row .grid-cell span {
font-size: 12px;
padding: 2px 4px;
}
.propagate-btn {
display: none !important;
}
.button-cell {
flex: 0 0 120px !important;
}
/* View modals — ensure they sit above backdrop */
#partsViewModal, #photosViewModal { z-index: 1060 !important; }
.modal-backdrop { z-index: 1050 !important; }
#partsViewModal,
#photosViewModal,
#revertStatusModal {
z-index: 1060 !important;
}
.modal-backdrop {
z-index: 1050 !important;
}
/* ── Grid filter row ── */
.grid-filter-row {
background: #f0f4f8 !important;
@@ -969,12 +992,15 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
top: 0;
z-index: 5;
}
.grid-filter-row:hover {
background: #f0f4f8 !important;
}
.grid-filter-row .grid-cell {
padding: 4px 6px !important;
}
.grid-filter-input {
width: 100% !important;
padding: 3px 6px !important;
@@ -985,30 +1011,36 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
color: #333 !important;
box-sizing: border-box;
}
.grid-filter-input:focus {
border-color: #80bdff !important;
box-shadow: 0 0 0 2px rgba(0,123,255,.15) !important;
box-shadow: 0 0 0 2px rgba(0, 123, 255, .15) !important;
outline: none !important;
}
.grid-filter-input::placeholder {
color: #adb5bd;
font-style: italic;
}
.grid-filter-row .grid-filter-input.has-value {
border-color: #0d6efd !important;
background: #f0f7ff !important;
}
select.grid-filter-input {
cursor: pointer;
padding-right: 18px !important;
appearance: auto;
}
/* Select2 inside filter row */
.grid-filter-row .select2-container {
width: 100% !important;
min-width: 0;
flex: 1;
}
.grid-filter-row .select2-container .select2-selection--single {
height: 26px !important;
min-height: 26px !important;
@@ -1020,6 +1052,7 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
border-radius: 3px;
position: relative;
}
.grid-filter-row .select2-container .select2-selection__rendered {
line-height: 1 !important;
font-size: 11px;
@@ -1029,32 +1062,39 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
text-overflow: ellipsis;
white-space: nowrap;
}
.grid-filter-row .select2-container .select2-selection__arrow {
height: 26px !important;
position: absolute;
right: 2px;
top: 0;
}
.grid-filter-row .filter-wrap {
overflow: visible;
}
/* Hide our clear button when Select2 is active — Select2 has its own allowClear */
.filter-wrap:has(.select2-container) .filter-clear-btn {
display: none;
}
.grid-filter-row .grid-cell {
overflow: visible !important;
}
.filter-wrap {
display: flex;
align-items: center;
gap: 2px;
width: 100%;
}
.filter-wrap .grid-filter-input {
flex: 1;
min-width: 0;
}
.filter-clear-btn {
flex-shrink: 0;
border: none;
@@ -1066,12 +1106,15 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
line-height: 1;
border-radius: 3px;
}
.filter-clear-btn:hover {
background: #c82333;
}
.grid-filter-row .grid-cell {
background: #f0f4f8 !important;
}
/* First sticky cell fills full row height */
.grid-filter-row .grid-cell.button-cell {
align-self: stretch;
@@ -1089,21 +1132,25 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
font-size: 13px;
color: #495057;
}
.pager-rows-per-page {
display: flex;
align-items: center;
gap: 8px;
}
.pager-label {
font-weight: 500;
white-space: nowrap;
}
.pager-limit-group {
display: inline-flex;
border: 1px solid #ced4da;
border-radius: 4px;
overflow: hidden;
}
.pager-limit-btn {
padding: 3px 10px;
text-decoration: none;
@@ -1112,24 +1159,41 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
transition: background .15s, color .15s;
font-weight: 500;
}
.pager-limit-btn:last-child { border-right: none; }
.pager-limit-btn:hover { background: #e9ecef; text-decoration: none; color: #212529; }
.pager-limit-btn:last-child {
border-right: none;
}
.pager-limit-btn:hover {
background: #e9ecef;
text-decoration: none;
color: #212529;
}
.pager-limit-btn.active {
background: #0d6efd;
color: #fff;
}
.pager-limit-btn.active:hover { background: #0b5ed7; color: #fff; }
.pager-limit-btn.active:hover {
background: #0b5ed7;
color: #fff;
}
.pager-nav {
display: flex;
align-items: center;
gap: 4px;
}
.pager-info {
margin-right: 8px;
font-weight: 500;
white-space: nowrap;
}
.pager-btn, .pager-num {
.pager-btn,
.pager-num {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -1143,16 +1207,25 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
font-weight: 500;
transition: background .15s, color .15s, border-color .15s;
}
.pager-btn:hover, .pager-num:hover { background: #e9ecef; text-decoration: none; color: #212529; }
.pager-btn:hover,
.pager-num:hover {
background: #e9ecef;
text-decoration: none;
color: #212529;
}
.pager-num.active {
background: #0d6efd;
color: #fff;
border-color: #0d6efd;
}
.pager-btn.disabled {
pointer-events: none;
opacity: .4;
}
.pager-dots {
padding: 0 2px;
color: #adb5bd;
@@ -1170,7 +1243,8 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
<div class="page-content">
<div class="mb-3 text d-flex align-items-center gap-2">
<a href="imported.php?id=<?= $template_id ?>" class="btn btn-warning me-2">Imported (i)</a>
<a href="tolims.php?id=<?= $template_id ?>" class="btn btn-success">To LIMS (l)</a>
<a href="tolims.php?id=<?= $template_id ?>" class="btn btn-success me-2">To LIMS (l)</a>
<a href="errors.php?id=<?= $template_id ?>" class="btn btn-danger">Errors (x)</a>
<span class="ms-3">
<label class="form-check-label" style="font-size: 13px; cursor: pointer;">
<?php
@@ -1188,48 +1262,48 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
</span>
</div>
<?php
$baseQuery = $_GET;
unset($baseQuery['limit'], $baseQuery['page']);
$pageQuery = $_GET;
unset($pageQuery['page']);
$pageBase = 'tolims.php?' . http_build_query($pageQuery);
$fromRow = ($page - 1) * $perPage + 1;
$toRow = min($page * $perPage, $totalRows);
$baseQuery = $_GET;
unset($baseQuery['limit'], $baseQuery['page']);
$pageQuery = $_GET;
unset($pageQuery['page']);
$pageBase = 'tolims.php?' . http_build_query($pageQuery);
$fromRow = ($page - 1) * $perPage + 1;
$toRow = min($page * $perPage, $totalRows);
?>
<div class="pager-bar mb-2">
<div class="pager-rows-per-page">
<span class="pager-label">Rows per page</span>
<div class="pager-limit-group">
<?php foreach ($allowedLimits as $lim):
$isActive = ($perPage === $lim);
$url = 'tolims.php?' . http_build_query(array_merge($baseQuery, ['limit' => $lim]));
?>
<a href="<?= htmlspecialchars($url) ?>" class="pager-limit-btn <?= $isActive ? 'active' : '' ?>"><?= $lim ?></a>
<?php endforeach; ?>
<?php foreach ($allowedLimits as $lim):
$isActive = ($perPage === $lim);
$url = 'tolims.php?' . http_build_query(array_merge($baseQuery, ['limit' => $lim]));
?>
<a href="<?= htmlspecialchars($url) ?>" class="pager-limit-btn <?= $isActive ? 'active' : '' ?>"><?= $lim ?></a>
<?php endforeach; ?>
</div>
</div>
<?php if ($totalPages > 1): ?>
<div class="pager-nav">
<span class="pager-info"><?= $fromRow ?>–<?= $toRow ?> of <?= $totalRows ?></span>
<a href="<?= htmlspecialchars($pageBase . '&page=1') ?>" class="pager-btn <?= $page <= 1 ? 'disabled' : '' ?>" title="First"><i class="fas fa-angle-double-left"></i></a>
<a href="<?= htmlspecialchars($pageBase . '&page=' . ($page - 1)) ?>" class="pager-btn <?= $page <= 1 ? 'disabled' : '' ?>" title="Previous"><i class="fas fa-angle-left"></i></a>
<?php
$startPage = max(1, $page - 2);
$endPage = min($totalPages, $page + 2);
if ($startPage > 1): ?>
<a href="<?= htmlspecialchars($pageBase . '&page=1') ?>" class="pager-num">1</a>
<?php if ($startPage > 2): ?><span class="pager-dots">...</span><?php endif; ?>
<?php endif;
for ($p = $startPage; $p <= $endPage; $p++): ?>
<a href="<?= htmlspecialchars($pageBase . '&page=' . $p) ?>" class="pager-num <?= $p === $page ? 'active' : '' ?>"><?= $p ?></a>
<?php endfor;
if ($endPage < $totalPages): ?>
<?php if ($endPage < $totalPages - 1): ?><span class="pager-dots">...</span><?php endif; ?>
<a href="<?= htmlspecialchars($pageBase . '&page=' . $totalPages) ?>" class="pager-num"><?= $totalPages ?></a>
<?php endif; ?>
<a href="<?= htmlspecialchars($pageBase . '&page=' . ($page + 1)) ?>" class="pager-btn <?= $page >= $totalPages ? 'disabled' : '' ?>" title="Next"><i class="fas fa-angle-right"></i></a>
<a href="<?= htmlspecialchars($pageBase . '&page=' . $totalPages) ?>" class="pager-btn <?= $page >= $totalPages ? 'disabled' : '' ?>" title="Last"><i class="fas fa-angle-double-right"></i></a>
</div>
<div class="pager-nav">
<span class="pager-info"><?= $fromRow ?>–<?= $toRow ?> of <?= $totalRows ?></span>
<a href="<?= htmlspecialchars($pageBase . '&page=1') ?>" class="pager-btn <?= $page <= 1 ? 'disabled' : '' ?>" title="First"><i class="fas fa-angle-double-left"></i></a>
<a href="<?= htmlspecialchars($pageBase . '&page=' . ($page - 1)) ?>" class="pager-btn <?= $page <= 1 ? 'disabled' : '' ?>" title="Previous"><i class="fas fa-angle-left"></i></a>
<?php
$startPage = max(1, $page - 2);
$endPage = min($totalPages, $page + 2);
if ($startPage > 1): ?>
<a href="<?= htmlspecialchars($pageBase . '&page=1') ?>" class="pager-num">1</a>
<?php if ($startPage > 2): ?><span class="pager-dots">...</span><?php endif; ?>
<?php endif;
for ($p = $startPage; $p <= $endPage; $p++): ?>
<a href="<?= htmlspecialchars($pageBase . '&page=' . $p) ?>" class="pager-num <?= $p === $page ? 'active' : '' ?>"><?= $p ?></a>
<?php endfor;
if ($endPage < $totalPages): ?>
<?php if ($endPage < $totalPages - 1): ?><span class="pager-dots">...</span><?php endif; ?>
<a href="<?= htmlspecialchars($pageBase . '&page=' . $totalPages) ?>" class="pager-num"><?= $totalPages ?></a>
<?php endif; ?>
<a href="<?= htmlspecialchars($pageBase . '&page=' . ($page + 1)) ?>" class="pager-btn <?= $page >= $totalPages ? 'disabled' : '' ?>" title="Next"><i class="fas fa-angle-right"></i></a>
<a href="<?= htmlspecialchars($pageBase . '&page=' . $totalPages) ?>" class="pager-btn <?= $page >= $totalPages ? 'disabled' : '' ?>" title="Last"><i class="fas fa-angle-double-right"></i></a>
</div>
<?php endif; ?>
</div>
<div class="card radius-10">
@@ -1512,14 +1586,16 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
<!-- Filter row (server-side) -->
<?php
// Helper: wrap filter element with clear button
function wrapFilter(string $inner, bool $hasValue): string {
function wrapFilter(string $inner, bool $hasValue): string
{
$btn = $hasValue
? "<button type='button' class='filter-clear-btn' onclick='clearOneFilter(this)' title='Clear'>&times;</button>"
: '';
return "<div class='filter-wrap'>{$inner}{$btn}</div>";
}
// Helper: render a select filter from a lookup array
function renderFilterSelect(string $name, array $options, string $currentVal, string $style = ''): string {
function renderFilterSelect(string $name, array $options, string $currentVal, string $style = ''): string
{
$html = "<select class='grid-filter-input' name='" . htmlspecialchars($name) . "' style='" . htmlspecialchars($style) . "' onchange='submitFilters()'>";
$html .= "<option value=''>All</option>";
foreach ($options as $id => $label) {
@@ -1530,12 +1606,14 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
return wrapFilter($html, $currentVal !== '');
}
// Helper: render a text filter
function renderFilterInput(string $name, string $currentVal): string {
function renderFilterInput(string $name, string $currentVal): string
{
$inner = "<input type='text' class='grid-filter-input' name='" . htmlspecialchars($name) . "' value='" . htmlspecialchars($currentVal) . "' placeholder='&#8981;'>";
return wrapFilter($inner, $currentVal !== '');
}
// Helper: render a select filter populated by JS
function renderFilterSelectJS(string $name, string $currentVal, string $jsClass, string $dataAttr = ''): string {
function renderFilterSelectJS(string $name, string $currentVal, string $jsClass, string $dataAttr = ''): string
{
$html = "<select class='grid-filter-input {$jsClass}' name='" . htmlspecialchars($name) . "' {$dataAttr} onchange='submitFilters()'>";
$html .= "<option value=''>All</option>";
if ($currentVal !== '') {
@@ -1639,6 +1717,8 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
<div class="grid-cell button-cell" style="flex: 0 0 120px; position: relative;">
<button type="button" class="photos-btn action-btn" data-row="<?= $index ?>" data-iddatadb="<?= $row['iddatadb'] ?>" title="Photos" style="background: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer;"><i class="fas fa-camera"></i></button>
<button type="button" class="parts-btn action-btn" data-row="<?= $index ?>" data-iddatadb="<?= $row['iddatadb'] ?>" title="Parts" style="background: #ffc107; color: white; border: none; border-radius: 5px; cursor: pointer;"><i class="fas fa-puzzle-piece"></i></button>
<button type="button" class="revert-btn action-btn" data-row="<?= $index ?>" data-iddatadb="<?= $row['iddatadb'] ?>" data-commessaweb="<?= htmlspecialchars($row['commessaweb'] ?? '', ENT_QUOTES) ?>" title="Riporta a Imported (i)" style="background: #fd7e14; color: white; border: none; border-radius: 5px; cursor: pointer;"><i class="fas fa-rotate-left"></i></button>
<a href="download_logs.php?iddatadb=<?= (int)$row['iddatadb'] ?>" class="logs-btn action-btn" title="Scarica log LIMS" style="background:#17a2b8; color:white; border:none; border-radius:5px; cursor:pointer; display:inline-flex; align-items:center; justify-content:center; text-decoration:none;"><i class="fas fa-file-download"></i></a>
</div>
<?php if ($mainFieldMapping):
$detail = array_filter($manualDetails, fn($d) => $d['mapping_id'] == $mainFieldMapping['id'] && $d['datadb_id'] == $row['iddatadb']);
@@ -1775,6 +1855,25 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
</div>
</div>
</div>
<!-- Revert to Imported Modal -->
<div class="modal fade" id="revertStatusModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-rotate-left" style="color:#fd7e14;"></i> Riporta a Imported</h5>
<button type="button" class="btn-close close-revert-modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Vuoi riportare questo record allo stato <strong>Imported (i)</strong>?</p>
<p class="text-muted" id="revertRecordInfo" style="font-size:13px;"></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm close-revert-modal">Annulla</button>
<button type="button" class="btn btn-warning btn-sm" id="confirmRevertBtn"><i class="fas fa-check"></i> Conferma</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -1792,10 +1891,14 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
(function() {
const sceltaEls = document.querySelectorAll('.scelta-value');
const fieldIds = new Set();
sceltaEls.forEach(el => { if (el.dataset.fieldId) fieldIds.add(el.dataset.fieldId); });
sceltaEls.forEach(el => {
if (el.dataset.fieldId) fieldIds.add(el.dataset.fieldId);
});
if (!fieldIds.size) return;
$.getJSON('get_customfield_values.php', { field_ids: [...fieldIds].join(',') }, function(data) {
$.getJSON('get_customfield_values.php', {
field_ids: [...fieldIds].join(',')
}, function(data) {
const lookup = {};
for (const [fid, values] of Object.entries(data)) {
lookup[fid] = {};
@@ -1836,8 +1939,18 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
url: 'search_customfield_values.php',
dataType: 'json',
delay: 150,
data: function(params) { return { field_id: fieldId, q: params.term || '', limit: 20 }; },
processResults: function(data) { return { results: data.results || [] }; },
data: function(params) {
return {
field_id: fieldId,
q: params.term || '',
limit: 20
};
},
processResults: function(data) {
return {
results: data.results || []
};
},
cache: true
}
});
@@ -1859,16 +1972,16 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
const promises = [...ids].map(id =>
fetch('search_clienti.php?id=' + encodeURIComponent(id))
.then(r => r.json())
.then(json => {
const item = (json.results || [])[0];
if (item) {
document.querySelectorAll(`.client-text[data-client-id="${id}"]`).forEach(el => {
el.textContent = item.text;
});
}
})
.catch(() => {})
.then(r => r.json())
.then(json => {
const item = (json.results || [])[0];
if (item) {
document.querySelectorAll(`.client-text[data-client-id="${id}"]`).forEach(el => {
el.textContent = item.text;
});
}
})
.catch(() => {})
);
Promise.all(promises);
})();
@@ -1893,8 +2006,17 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
url: 'search_clienti.php',
dataType: 'json',
delay: 150,
data: function(params) { return { q: params.term || '', limit: 20 }; },
processResults: function(data) { return { results: data.results || [] }; },
data: function(params) {
return {
q: params.term || '',
limit: 20
};
},
processResults: function(data) {
return {
results: data.results || []
};
},
cache: true
}
});
@@ -1909,8 +2031,17 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
url: 'search_clienti.php',
dataType: 'json',
delay: 150,
data: function(params) { return { q: params.term || '', limit: 20 }; },
processResults: function(data) { return { results: data.results || [] }; },
data: function(params) {
return {
q: params.term || '',
limit: 20
};
},
processResults: function(data) {
return {
results: data.results || []
};
},
cache: true
}
});
@@ -1942,14 +2073,16 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
$('#partsViewContent').html('<i class="fas fa-spinner fa-spin"></i> Loading...');
new bootstrap.Modal(document.getElementById('partsViewModal')).show();
$.getJSON('load_parts.php', { iddatadb: iddatadb }, function(data) {
$.getJSON('load_parts.php', {
iddatadb: iddatadb
}, function(data) {
if (!data.parts || data.parts.length === 0) {
$('#partsViewContent').html('<p class="text-muted">No parts found.</p>');
return;
}
let html = '<table class="table table-sm table-bordered"><thead><tr><th>#</th><th>Description</th><th>Material</th><th>Color</th><th>Matrice</th><th>Note</th></tr></thead><tbody>';
data.parts.forEach(p => {
html += '<tr><td>' + (p.part_number||'') + '</td><td>' + (p.part_description||'') + '</td><td>' + (p.material||'') + '</td><td>' + (p.color||'') + '</td><td>' + (p.idmatrice||'') + '</td><td>' + (p.note||'') + '</td></tr>';
html += '<tr><td>' + (p.part_number || '') + '</td><td>' + (p.part_description || '') + '</td><td>' + (p.material || '') + '</td><td>' + (p.color || '') + '</td><td>' + (p.idmatrice || '') + '</td><td>' + (p.note || '') + '</td></tr>';
});
html += '</tbody></table>';
$('#partsViewContent').html(html);
@@ -1964,7 +2097,9 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
$('#photosViewContent').html('<i class="fas fa-spinner fa-spin"></i> Loading...');
new bootstrap.Modal(document.getElementById('photosViewModal')).show();
$.getJSON('load_photo.php', { iddatadb: iddatadb }, function(data) {
$.getJSON('load_photo.php', {
iddatadb: iddatadb
}, function(data) {
if (!data.success || !data.photos || data.photos.length === 0) {
$('#photosViewContent').html('<p class="text-muted">No photos found.</p>');
return;
@@ -1979,14 +2114,74 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
$('#photosViewContent').html('<p class="text-danger">Error loading photos.</p>');
});
});
// ── Revert status to Imported (i) ──
let revertIddatadb = null;
$(document).on('click', '.revert-btn', function() {
revertIddatadb = $(this).data('iddatadb');
const cw = $(this).data('commessaweb') || '';
$('#revertRecordInfo').text(cw ? ('Commessa: ' + cw) : ('ID: ' + revertIddatadb));
new bootstrap.Modal(document.getElementById('revertStatusModal')).show();
});
$(document).on('click', '.close-revert-modal', function() {
const modal = bootstrap.Modal.getInstance(document.getElementById('revertStatusModal'));
if (modal) modal.hide();
});
$(document).on('hidden.bs.modal', '#revertStatusModal', function() {
$('.modal-backdrop').remove();
$('body').removeClass('modal-open').css('padding-right', '');
});
$(document).on('click', '#confirmRevertBtn', function() {
if (!revertIddatadb) return;
const $btn = $(this);
$btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Attendere...');
$.ajax({
url: 'revert_status.php',
method: 'POST',
dataType: 'json',
data: {
iddatadb: revertIddatadb,
status: 'i'
},
success: function(res) {
if (res && res.success) {
const modal = bootstrap.Modal.getInstance(document.getElementById('revertStatusModal'));
if (modal) modal.hide();
const $row = $('.grid-row[data-id="' + revertIddatadb + '"]');
$row.addClass('flash-success');
setTimeout(function() {
$row.fadeOut(300, function() {
$row.remove();
});
}, 400);
} else {
alert('Errore: ' + ((res && res.message) ? res.message : 'operazione non riuscita'));
}
},
error: function() {
alert('Errore di rete durante l\'operazione.');
},
complete: function() {
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Conferma');
}
});
});
});
// ── Column filters ──
function clearOneFilter(btn) {
const wrap = btn.closest('.filter-wrap');
const el = wrap.querySelector('.grid-filter-input');
if (el) { el.value = ''; submitFilters(); }
if (el) {
el.value = '';
submitFilters();
}
}
function submitFilters() {
const filterRow = document.getElementById('gridFilterRow');
if (!filterRow) return;
@@ -2004,7 +2199,10 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
const filterRow = document.getElementById('gridFilterRow');
if (!filterRow) return;
filterRow.addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); submitFilters(); }
if (e.key === 'Enter') {
e.preventDefault();
submitFilters();
}
});
filterRow.querySelectorAll('.grid-filter-input').forEach(el => {
el.classList.toggle('has-value', el.value.trim() !== '');
@@ -2016,7 +2214,10 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
// ── Column resize ──
(function() {
const resizers = document.querySelectorAll('.resizer');
let currentResizer = null, startX = 0, startWidth = 0, columnIndex = null;
let currentResizer = null,
startX = 0,
startWidth = 0,
columnIndex = null;
function resize(e) {
if (!currentResizer || columnIndex === null) return;
@@ -2059,4 +2260,5 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
})();
</script>
</body>
</html>
</html>
+8
View File
@@ -0,0 +1,8 @@
<?php
session_start();
require_once __DIR__ . '/class/db-functions.php';
$sid = session_id(); // oppure l'id Laravel, da verificare
$pdo = DBHandlerSelect::getInstance()->getConnection();
$pdo->prepare("UPDATE auth_sessions SET last_activity = ? WHERE id = ?")
->execute([time(), $sid]);
echo json_encode(['ok' => true, 'sid' => $sid, 'ts' => time()]);
+16 -1
View File
@@ -17,7 +17,7 @@ if ($id <= 0) {
exit;
}
$allowed = ['default_value', 'is_visible_import', 'is_required'];
$allowed = ['default_value', 'is_visible_import', 'is_required', 'default_source', 'excel_column'];
if (!in_array($field, $allowed, true)) {
echo json_encode(['success' => false, 'message' => 'Invalid field']);
exit;
@@ -27,6 +27,21 @@ if ($field === 'is_visible_import' || $field === 'is_required') {
$value = ((int)$value === 1) ? 1 : 0;
}
if ($field === 'default_source') {
$value = in_array($value, ['manual', 'xls', 'json'], true) ? $value : 'manual';
if ($value !== 'xls') {
$pdo->prepare("UPDATE template_fixed_mapping SET excel_column = NULL WHERE id = :id")
->execute([':id' => $id]);
}
}
if ($field === 'excel_column') {
$value = trim((string)$value);
if ($value === '') {
$value = null;
}
}
$sql = "UPDATE template_fixed_mapping SET {$field} = :val WHERE id = :id";
$stmt = $pdo->prepare($sql);
@@ -0,0 +1,38 @@
<?php
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$userId = (int)($iduserlogin ?? 0);
if ($userId <= 0) {
header("Location: login.php");
exit;
}
// Ogni preferenza: colonna DB => 1 se checkbox spuntata, 0 altrimenti
$prefsMap = [
'notify_export_email' => isset($_POST['notify_export_email']) ? 1 : 0,
// in futuro: 'notify_altra_cosa' => isset($_POST['notify_altra_cosa']) ? 1 : 0,
];
try {
$sets = [];
$params = [];
foreach ($prefsMap as $col => $val) {
$sets[] = "`{$col}` = :{$col}";
$params[$col] = $val;
}
$params['id'] = $userId;
$sql = "UPDATE auth_users SET " . implode(', ', $sets) . " WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
header("Location: notification_settings.php?saved=1");
exit;
} catch (Exception $e) {
error_log("Notification prefs update error: " . $e->getMessage());
header("Location: notification_settings.php?saved=0");
exit;
}
+41
View File
@@ -22,6 +22,7 @@
* To add a new rule — just append another closure to $validators.
*/
include('include/headscript.php');
require_once __DIR__ . '/class/binding-functions.php';
$dbHandler = DBHandlerSelect::getInstance();
$pdo = $dbHandler->getConnection();
@@ -124,6 +125,29 @@ $validators[] = function (int $iddatadb, array $ctx): array {
return $errors;
};
$validators[] = function (int $iddatadb, array $ctx): array {
$errors = [];
foreach (($ctx['listCustom'] ?? []) as $cf) {
$val = trim((string) ($ctx['customValues'][(int) $cf['mapping_id']] ?? ''));
if ($val === '' || ctype_digit($val)) {
continue;
}
if (!binding_has_lims_values((int) $cf['field_id'])) {
continue;
}
$label = rtrim($cf['field_label'], ': ');
$errors[] = [
'field' => 'field_label:' . $cf['field_label'],
'message' => $label . ': il valore importato "' . $val . '" non e\' collegato a nessun valore '
. 'della lista LIMS. Aprire Gestione binding o ri-importare per collegarlo.',
];
}
return $errors;
};
// Logical fixed_field_key - real datadb column (mirrors imported.php $fixedAliasMap)
$fixedAliasMap = [
'ClienteResponsabile' => 'cliente_responsabile_id',
@@ -197,6 +221,7 @@ try {
$requiredFixedByTemplate = []; // template_id => [ fixed_field_key => label ]
$requiredCustomByTemplate = []; // template_id => [ { mapping_id, field_label }, ... ]
$listCustomByTemplate = []; // template_id => [ { mapping_id, field_label }, ... ]
if (!empty($templateIds)) {
$tplPlaceholders = implode(',', array_fill(0, count($templateIds), '?'));
@@ -237,6 +262,21 @@ try {
'field_label' => $r['field_label'],
];
}
$stmt = $pdo->prepare("
SELECT id AS mapping_id, template_id, field_id, field_label
FROM template_mapping
WHERE template_id IN ($tplPlaceholders)
AND data_type = 'SceltaMultipla'
");
$stmt->execute($templateIds);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
$listCustomByTemplate[(int)$r['template_id']][] = [
'mapping_id' => (int)$r['mapping_id'],
'field_id' => (int)$r['field_id'],
'field_label' => $r['field_label'],
];
}
}
// Custom field values per record (import_data_details.id is the FK to datadb)
@@ -268,6 +308,7 @@ try {
'fixedAliasMap' => $fixedAliasMap,
'requiredFixed' => $requiredFixedByTemplate[$templateId] ?? [],
'requiredCustom' => $requiredCustomByTemplate[$templateId] ?? [],
'listCustom' => $listCustomByTemplate[$templateId] ?? [],
'customValues' => $customValuesByRecord[$iddatadb] ?? [],
];
+11 -10
View File
@@ -5806,17 +5806,17 @@
},
{
"name": "phpmailer/phpmailer",
"version": "v6.9.3",
"version_normalized": "6.9.3.0",
"version": "v7.1.1",
"version_normalized": "7.1.1.0",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e"
"reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/2f5c94fe7493efc213f643c23b1b1c249d40f47e",
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
"reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
"shasum": ""
},
"require": {
@@ -5830,13 +5830,14 @@
"doctrine/annotations": "^1.2.6 || ^1.13.3",
"php-parallel-lint/php-console-highlighter": "^1.0.0",
"php-parallel-lint/php-parallel-lint": "^1.3.2",
"phpcompatibility/php-compatibility": "^9.3.5",
"roave/security-advisories": "dev-latest",
"squizlabs/php_codesniffer": "^3.7.2",
"phpcompatibility/php-compatibility": "^10.0.0@dev",
"squizlabs/php_codesniffer": "^3.13.5",
"yoast/phpunit-polyfills": "^1.0.4"
},
"suggest": {
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
"directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example",
"ext-imap": "Needed to support advanced email address parsing according to RFC822",
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
@@ -5846,7 +5847,7 @@
"symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
"thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
},
"time": "2024-11-24T18:04:13+00:00",
"time": "2026-05-18T08:06:14+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -5878,7 +5879,7 @@
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"support": {
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.9.3"
"source": "https://github.com/PHPMailer/PHPMailer/tree/v7.1.1"
},
"funding": [
{
+5 -5
View File
@@ -3,7 +3,7 @@
'name' => 'loshmis/vanguard',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'b19963808e5545f0b99bf0eba214b0c85b2f4b7b',
'reference' => 'bb2ebcc1223dd084e59588774fecbca9875d9daf',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -742,7 +742,7 @@
'loshmis/vanguard' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'b19963808e5545f0b99bf0eba214b0c85b2f4b7b',
'reference' => 'bb2ebcc1223dd084e59588774fecbca9875d9daf',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -941,9 +941,9 @@
'dev_requirement' => true,
),
'phpmailer/phpmailer' => array(
'pretty_version' => 'v6.9.3',
'version' => '6.9.3.0',
'reference' => '2f5c94fe7493efc213f643c23b1b1c249d40f47e',
'pretty_version' => 'v7.1.1',
'version' => '7.1.1.0',
'reference' => '1bc1716a507a65e039d4ac9d9adebbbd0d346e15',
'type' => 'library',
'install_path' => __DIR__ . '/../phpmailer/phpmailer',
'aliases' => array(),
-15
View File
@@ -1,15 +0,0 @@
root = true
[*]
charset = utf-8
indent_size = 4
indent_style = space
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
+13 -12
View File
@@ -6,9 +6,9 @@
[![Test status](https://github.com/PHPMailer/PHPMailer/workflows/Tests/badge.svg)](https://github.com/PHPMailer/PHPMailer/actions)
[![codecov.io](https://codecov.io/gh/PHPMailer/PHPMailer/branch/master/graph/badge.svg?token=iORZpwmYmM)](https://codecov.io/gh/PHPMailer/PHPMailer)
[![Latest Stable Version](https://poser.pugx.org/phpmailer/phpmailer/v/stable.svg)](https://packagist.org/packages/phpmailer/phpmailer)
[![Total Downloads](https://poser.pugx.org/phpmailer/phpmailer/downloads)](https://packagist.org/packages/phpmailer/phpmailer)
[![License](https://poser.pugx.org/phpmailer/phpmailer/license.svg)](https://packagist.org/packages/phpmailer/phpmailer)
[![Latest Stable Version](https://img.shields.io/packagist/v/phpmailer/phpmailer?label=stable)](https://packagist.org/packages/phpmailer/phpmailer)
[![Total Downloads](https://img.shields.io/packagist/dt/phpmailer/phpmailer)](https://packagist.org/packages/phpmailer/phpmailer)
[![License](https://img.shields.io/github/license/PHPMailer/PHPMailer)](https://packagist.org/packages/phpmailer/phpmailer)
[![API Docs](https://github.com/phpmailer/phpmailer/workflows/Docs/badge.svg)](https://phpmailer.github.io/PHPMailer/)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/PHPMailer/PHPMailer/badge)](https://api.securityscorecards.dev/projects/github.com/PHPMailer/PHPMailer)
@@ -20,25 +20,26 @@
- Multipart/alternative emails for mail clients that do not read HTML email
- Add attachments, including inline
- Support for UTF-8 content and 8bit, base64, binary, and quoted-printable encodings
- SMTP authentication with LOGIN, PLAIN, CRAM-MD5, and XOAUTH2 mechanisms over SMTPS and SMTP+STARTTLS transports
- Full UTF-8 support when using servers that support `SMTPUTF8`.
- Support for iCal events in multiparts and attachments
- SMTP authentication with `LOGIN`, `PLAIN`, `CRAM-MD5`, and `XOAUTH2` mechanisms over SMTPS and SMTP+STARTTLS transports
- Validates email addresses automatically
- Protects against header injection attacks
- Error messages in over 50 languages!
- DKIM and S/MIME signing support
- Compatible with PHP 5.5 and later, including PHP 8.2
- Compatible with PHP 5.5 and later, including PHP 8.5
- Namespaced to prevent name clashes
- Much more!
## Why you might need it
Many PHP developers need to send email from their code. The only PHP function that supports this directly is [`mail()`](https://www.php.net/manual/en/function.mail.php). However, it does not provide any assistance for making use of popular features such as encryption, authentication, HTML messages, and attachments.
Many PHP developers need to send email from their code. The only PHP function that supports this directly is [`mail()`](https://www.php.net/manual/en/function.mail.php). However, it does not provide any assistance for making use of popular features such as authentication, HTML messages, and attachments.
Formatting email correctly is surprisingly difficult. There are myriad overlapping (and conflicting) standards, requiring tight adherence to horribly complicated formatting and encoding rules – the vast majority of code that you'll find online that uses the `mail()` function directly is just plain wrong, if not unsafe!
The PHP `mail()` function usually sends via a local mail server, typically fronted by a `sendmail` binary on Linux, BSD, and macOS platforms, however, Windows usually doesn't include a local mail server; PHPMailer's integrated SMTP client allows email sending on all platforms without needing a local mail server. Be aware though, that the `mail()` function should be avoided when possible; it's both faster and [safer](https://exploitbox.io/paper/Pwning-PHP-Mail-Function-For-Fun-And-RCE.html) to use SMTP to localhost.
*Please* don't be tempted to do it yourself – if you don't use PHPMailer, there are many other excellent libraries that
you should look at before rolling your own. Try [SwiftMailer](https://swiftmailer.symfony.com/)
, [Laminas/Mail](https://docs.laminas.dev/laminas-mail/), [ZetaComponents](https://github.com/zetacomponents/Mail), etc.
you should look at before rolling your own. Try [Symfony Mailer](https://symfony.com/doc/current/mailer.html), [Laminas/Mail](https://docs.laminas.dev/laminas-mail/), [ZetaComponents](https://github.com/zetacomponents/Mail), etc.
## License
This software is distributed under the [LGPL 2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) license, along with the [GPL Cooperation Commitment](https://gplcc.github.io/gplcc/). Please read [LICENSE](https://github.com/PHPMailer/PHPMailer/blob/master/LICENSE) for information on the software availability and distribution.
@@ -47,7 +48,7 @@ This software is distributed under the [LGPL 2.1](https://www.gnu.org/licenses/o
PHPMailer is available on [Packagist](https://packagist.org/packages/phpmailer/phpmailer) (using semantic versioning), and installation via [Composer](https://getcomposer.org) is the recommended way to install PHPMailer. Just add this line to your `composer.json` file:
```json
"phpmailer/phpmailer": "^6.9.2"
"phpmailer/phpmailer": "^7.0.0"
```
or run
@@ -74,10 +75,10 @@ require 'path/to/PHPMailer/src/PHPMailer.php';
require 'path/to/PHPMailer/src/SMTP.php';
```
If you're not using the `SMTP` class explicitly (you're probably not), you don't need a `use` line for the SMTP class. Even if you're not using exceptions, you do still need to load the `Exception` class as it is used internally.
If you're not using the `SMTP` class explicitly (you're probably not), you don't need a `use` line for it. Even if you're not using exceptions, you do still need to load the `Exception` class as it is used internally.
## Legacy versions
PHPMailer 5.2 (which is compatible with PHP 5.0 — 7.0) is no longer supported, even for security updates. You will find the latest version of 5.2 in the [5.2-stable branch](https://github.com/PHPMailer/PHPMailer/tree/5.2-stable). If you're using PHP 5.5 or later (which you should be), switch to the 6.x releases.
PHPMailer 5.2 (which is compatible with PHP 5.0 — 7.0) is no longer supported, even for security updates. You will find the latest version of 5.2 in the [5.2-stable branch](https://github.com/PHPMailer/PHPMailer/tree/5.2-stable). If you're using PHP 5.5 or later (which you should be), upgrade to a later release.
### Upgrading from 5.2
The biggest changes are that source files are now in the `src/` folder, and PHPMailer now declares the namespace `PHPMailer\PHPMailer`. This has several important effects – [read the upgrade guide](https://github.com/PHPMailer/PHPMailer/tree/master/UPGRADING.md) for more details.
@@ -95,7 +96,7 @@ use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;
//Load Composer's autoloader
//Load Composer's autoloader (created by composer, not included with PHPMailer)
require 'vendor/autoload.php';
//Create an instance; passing `true` enables exceptions
+1 -1
View File
@@ -1 +1 @@
6.9.3
7.1.1
+8 -4
View File
@@ -42,22 +42,25 @@
"doctrine/annotations": "^1.2.6 || ^1.13.3",
"php-parallel-lint/php-console-highlighter": "^1.0.0",
"php-parallel-lint/php-parallel-lint": "^1.3.2",
"phpcompatibility/php-compatibility": "^9.3.5",
"roave/security-advisories": "dev-latest",
"squizlabs/php_codesniffer": "^3.7.2",
"phpcompatibility/php-compatibility": "^10.0.0@dev",
"squizlabs/php_codesniffer": "^3.13.5",
"yoast/phpunit-polyfills": "^1.0.4"
},
"suggest": {
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
"ext-imap": "Needed to support advanced email address parsing according to RFC822",
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
"hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
"league/oauth2-google": "Needed for Google XOAUTH2 authentication",
"psr/log": "For optional PSR-3 debug logging",
"symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
"thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication",
"symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)"
"directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example"
},
"minimum-stability": "dev",
"prefer-stable": true,
"autoload": {
"psr-4": {
"PHPMailer\\PHPMailer\\": "src/"
@@ -71,6 +74,7 @@
"license": "LGPL-2.1-only",
"scripts": {
"check": "./vendor/bin/phpcs",
"style": "./vendor/bin/phpcbf",
"test": "./vendor/bin/phpunit --no-coverage",
"coverage": "./vendor/bin/phpunit",
"lint": [
+1 -1
View File
@@ -178,5 +178,5 @@ if (!isset($_GET['code'])) {
);
//Use this to interact with an API on the users behalf
//Use this to get a new access token if the old one expires
echo 'Refresh Token: ', htmlspecialchars($token->getRefreshToken());
echo 'Refresh Token: ', htmlspecialchars($token->getRefreshToken(), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401);
}
@@ -34,3 +34,6 @@ $PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() fejlede.';
$PHPMAILER_LANG['smtp_detail'] = 'Detalje: ';
$PHPMAILER_LANG['smtp_error'] = 'SMTP server fejl: ';
$PHPMAILER_LANG['variable_set'] = 'Kunne ikke definere eller nulstille variablen: ';
$PHPMAILER_LANG['no_smtputf8'] = 'Serveren understøtter ikke SMTPUTF8 som påkrævet for at sende til Unicode adresser';
$PHPMAILER_LANG['imap_recommended'] = 'Brug af forenklet adresseparser anbefales ikke. Installer PHP IMAP udvidelsen for fuld RFC822 parsing.';
$PHPMAILER_LANG['deprecated_argument'] = 'Udfaset argument: ';
+29 -18
View File
@@ -3,24 +3,35 @@
/**
* Esperanto PHPMailer language file: refer to English translation for definitive list
* @package PHPMailer
* @author Robin van der Vliet <info@robinvandervliet.com>
*/
$PHPMAILER_LANG['authenticate'] = 'Eraro de servilo SMTP : aŭtentigo malsukcesis.';
$PHPMAILER_LANG['connect_host'] = 'Eraro de servilo SMTP : konektado al servilo malsukcesis.';
$PHPMAILER_LANG['data_not_accepted'] = 'Eraro de servilo SMTP : neĝustaj datumoj.';
$PHPMAILER_LANG['empty_message'] = 'Teksto de mesaĝo mankas.';
$PHPMAILER_LANG['authenticate'] = 'SMTP-eraro: Ne eblis aŭtentigi.';
$PHPMAILER_LANG['buggy_php'] = 'Via versio de PHP estas trafita de cimo, kiu povas kaŭzi difektitajn mesaĝojn. Por ripari tion, ŝanĝu al sendado per SMTP, malŝaltu la opcion mail.add_x_header en via php.ini, ŝanĝu al MacOS aŭ Linux, aŭ ĝisdatigu vian PHP al versio 7.0.17+ aŭ 7.1.3+.';
$PHPMAILER_LANG['connect_host'] = 'SMTP-eraro: Ne eblis konektiĝi al la SMTP-gastiganto.';
$PHPMAILER_LANG['data_not_accepted'] = 'SMTP-eraro: Datumoj ne akceptitaj.';
$PHPMAILER_LANG['empty_message'] = 'Mesaĝokorpo malplena';
$PHPMAILER_LANG['encoding'] = 'Nekonata kodoprezento: ';
$PHPMAILER_LANG['execute'] = 'Lanĉi rulumadon ne eblis: ';
$PHPMAILER_LANG['file_access'] = 'Aliro al dosiero ne sukcesis: ';
$PHPMAILER_LANG['file_open'] = 'Eraro de dosiero: malfermo neeblas: ';
$PHPMAILER_LANG['from_failed'] = 'Jena adreso de sendinto malsukcesis: ';
$PHPMAILER_LANG['instantiate'] = 'Genero de retmesaĝa funkcio neeblis.';
$PHPMAILER_LANG['invalid_address'] = 'Retadreso ne validas: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' mesaĝilo ne subtenata.';
$PHPMAILER_LANG['provide_address'] = 'Vi devas tajpi almenaŭ unu recevontan retadreson.';
$PHPMAILER_LANG['recipients_failed'] = 'Eraro de servilo SMTP : la jenaj poŝtrecivuloj kaŭzis eraron: ';
$PHPMAILER_LANG['signing'] = 'Eraro de subskribo: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP konektado malsukcesis.';
$PHPMAILER_LANG['smtp_error'] = 'Eraro de servilo SMTP : ';
$PHPMAILER_LANG['variable_set'] = 'Variablo ne pravalorizeblas aŭ ne repravalorizeblas: ';
$PHPMAILER_LANG['extension_missing'] = 'Mankas etendo: ';
$PHPMAILER_LANG['execute'] = 'Ne eblis plenumi: ';
$PHPMAILER_LANG['extension_missing'] = 'Kromprogramo mankas: ';
$PHPMAILER_LANG['file_access'] = 'Ne eblis aliri la dosieron: ';
$PHPMAILER_LANG['file_open'] = 'Dosiera eraro: Ne eblis malfermi la dosieron: ';
$PHPMAILER_LANG['from_failed'] = 'La sekva(j) sendinto(j) malsukcesis: ';
$PHPMAILER_LANG['instantiate'] = 'Ne eblis funkciigi la retpoŝtan funkcion.';
$PHPMAILER_LANG['invalid_address'] = 'Nevalida adreso: ';
$PHPMAILER_LANG['invalid_header'] = 'Nevalida kaplinia nomo aŭ valoro';
$PHPMAILER_LANG['invalid_hostentry'] = 'Nevalida enigo de gastiganto: ';
$PHPMAILER_LANG['invalid_host'] = 'Nevalida gastiganto: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' retpoŝtilo ne estas subtenata.';
$PHPMAILER_LANG['provide_address'] = 'Vi devas provizi almenaŭ unu retpoŝtadreson de ricevonto.';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP-eraro: La sekva(j) ricevonto(j) malsukcesis: ';
$PHPMAILER_LANG['signing'] = 'Subskriba eraro: ';
$PHPMAILER_LANG['smtp_code'] = 'SMTP-kodo: ';
$PHPMAILER_LANG['smtp_code_ex'] = 'Pliaj SMTP-informoj: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'La SMTP-konektiĝo malsukcesis.';
$PHPMAILER_LANG['smtp_detail'] = 'Informoj: ';
$PHPMAILER_LANG['smtp_error'] = 'Eraro de SMTP-servilo: ';
$PHPMAILER_LANG['variable_set'] = 'Ne eblas agordi aŭ reagordi la variablon: ';
$PHPMAILER_LANG['no_smtputf8'] = 'La servilo ne subtenas SMTPUTF8, kiu estas bezonata por sendi al Unicode-adresoj.';
$PHPMAILER_LANG['imap_recommended'] = 'Uzado de la simpligita adresanalizilo ne estas rekomendita. Instalu la IMAP-kromprogramon por PHP por plena RFC822-analizado.';
$PHPMAILER_LANG['deprecated_argument'] = 'Malrekomendita argumento: ';
+4 -2
View File
@@ -9,7 +9,7 @@
*/
$PHPMAILER_LANG['authenticate'] = 'Error SMTP: Imposible autentificar.';
$PHPMAILER_LANG['buggy_php'] = 'Tu versión de PHP está afectada por un bug que puede resultar en mensajes corruptos. Para arreglarlo, cambia a enviar usando SMTP, deshabilita la opción mail.add_x_header en tu php.ini, cambia a MacOS o Linux, o actualiza tu PHP a la versión 7.0.17+ o 7.1.3+.';
$PHPMAILER_LANG['buggy_php'] = 'Tu versión de PHP ha sido afectada por un bug que puede resultar en mensajes corruptos. Para arreglarlo, cambia a enviar usando SMTP, deshabilita la opción mail.add_x_header en tu php.ini, cambia a MacOS o Linux, o actualiza tu PHP a la versión 7.0.17+ o 7.1.3+.';
$PHPMAILER_LANG['connect_host'] = 'Error SMTP: Imposible conectar al servidor SMTP.';
$PHPMAILER_LANG['data_not_accepted'] = 'Error SMTP: Datos no aceptados.';
$PHPMAILER_LANG['empty_message'] = 'El cuerpo del mensaje está vacío.';
@@ -18,7 +18,7 @@ $PHPMAILER_LANG['execute'] = 'Imposible ejecutar: ';
$PHPMAILER_LANG['extension_missing'] = 'Extensión faltante: ';
$PHPMAILER_LANG['file_access'] = 'Imposible acceder al archivo: ';
$PHPMAILER_LANG['file_open'] = 'Error de Archivo: Imposible abrir el archivo: ';
$PHPMAILER_LANG['from_failed'] = 'La(s) siguiente(s) direcciones de remitente fallaron: ';
$PHPMAILER_LANG['from_failed'] = 'La siguiente dirección de remitente falló: ';
$PHPMAILER_LANG['instantiate'] = 'Imposible crear una instancia de la función Mail.';
$PHPMAILER_LANG['invalid_address'] = 'Imposible enviar: dirección de email inválido: ';
$PHPMAILER_LANG['invalid_header'] = 'Nombre o valor de encabezado no válido';
@@ -34,3 +34,5 @@ $PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() falló.';
$PHPMAILER_LANG['smtp_detail'] = 'Detalle: ';
$PHPMAILER_LANG['smtp_error'] = 'Error del servidor SMTP: ';
$PHPMAILER_LANG['variable_set'] = 'No se pudo configurar la variable: ';
$PHPMAILER_LANG['imap_recommended'] = 'No se recomienda usar el analizador de direcciones simplificado. Instala la extensión IMAP de PHP para un análisis RFC822 más completo.';
$PHPMAILER_LANG['deprecated_argument'] = 'Argumento obsoleto: ';
+10 -6
View File
@@ -3,27 +3,28 @@
/**
* Norwegian Bokmål PHPMailer language file: refer to English translation for definitive list
* @package PHPMailer
* @author Wera AS <wordpress@wera.no>
*/
$PHPMAILER_LANG['authenticate'] = 'SMTP-feil: Kunne ikke autentiseres.';
$PHPMAILER_LANG['buggy_php'] = 'Din versjon av PHP er berørt av en feil som kan føre til ødelagte meldinger. For å løse problemet kan du bytte til SMTP, deaktivere alternativet mail.add_x_header i php.ini, bytte til MacOS eller Linux eller oppgradere PHP til versjon 7.0.17+ eller 7.1.3+.';
$PHPMAILER_LANG['authenticate'] = 'SMTP-feil: Kunne ikke autentisere.';
$PHPMAILER_LANG['buggy_php'] = 'Din versjon av PHP er påvirket av en feil som kan føre til ødelagte meldinger. For å løse problemet kan du bytte til sending via SMTP, deaktivere mail.add_x_header-alternativet i php.ini, bytte til MacOS eller Linux, eller oppgradere PHP til versjon 7.0.17+ eller 7.1.3+.';
$PHPMAILER_LANG['connect_host'] = 'SMTP-feil: Kunne ikke koble til SMTP-vert.';
$PHPMAILER_LANG['data_not_accepted'] = 'SMTP-feil: data ikke akseptert.';
$PHPMAILER_LANG['empty_message'] = 'Meldingstekst mangler';
$PHPMAILER_LANG['empty_message'] = 'Meldingsinnholdet er tomt';
$PHPMAILER_LANG['encoding'] = 'Ukjent koding: ';
$PHPMAILER_LANG['execute'] = 'Kunne ikke utføres: ';
$PHPMAILER_LANG['extension_missing'] = 'Utvidelse mangler: ';
$PHPMAILER_LANG['file_access'] = 'Kunne ikke få tilgang til filen: ';
$PHPMAILER_LANG['file_open'] = 'Feil i fil: Kunne ikke åpne filen: ';
$PHPMAILER_LANG['from_failed'] = 'Følgende Fra-adresse mislyktes: ';
$PHPMAILER_LANG['instantiate'] = 'Kunne ikke instansiere e-postfunksjonen.';
$PHPMAILER_LANG['from_failed'] = 'Følgende avsenderadresse mislyktes: ';
$PHPMAILER_LANG['instantiate'] = 'Kunne ikke starte e-postfunksjonen.';
$PHPMAILER_LANG['invalid_address'] = 'Ugyldig adresse: ';
$PHPMAILER_LANG['invalid_header'] = 'Ugyldig headernavn eller verdi';
$PHPMAILER_LANG['invalid_hostentry'] = 'Ugyldig vertsinngang: ';
$PHPMAILER_LANG['invalid_host'] = 'Ugyldig vert: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' sender er ikke støttet.';
$PHPMAILER_LANG['provide_address'] = 'Du må oppgi minst én mottaker-e-postadresse.';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP Feil: Følgende mottakeradresse feilet: ';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP-feil: Følgende mottakeradresser feilet: ';
$PHPMAILER_LANG['signing'] = 'Signeringsfeil: ';
$PHPMAILER_LANG['smtp_code'] = 'SMTP-kode: ';
$PHPMAILER_LANG['smtp_code_ex'] = 'Ytterligere SMTP-info: ';
@@ -31,3 +32,6 @@
$PHPMAILER_LANG['smtp_detail'] = 'Detaljer: ';
$PHPMAILER_LANG['smtp_error'] = 'SMTP-serverfeil: ';
$PHPMAILER_LANG['variable_set'] = 'Kan ikke angi eller tilbakestille variabel: ';
$PHPMAILER_LANG['no_smtputf8'] = 'Serveren støtter ikke SMTPUTF8, som er nødvendig for å sende til Unicode-adresser.';
$PHPMAILER_LANG['imap_recommended'] = 'Det anbefales ikke å bruke forenklet adresseanalyse. Installer PHP IMAP-utvidelsen for full RFC822-analyse.';
$PHPMAILER_LANG['deprecated_argument'] = 'Avviklet argument: ';
+11 -7
View File
@@ -4,10 +4,11 @@
* Dutch PHPMailer language file: refer to PHPMailer.php for definitive list.
* @package PHPMailer
* @author Tuxion <team@tuxion.nl>
* @author Robin van der Vliet <info@robinvandervliet.com>
*/
$PHPMAILER_LANG['authenticate'] = 'SMTP-fout: authenticatie mislukt.';
$PHPMAILER_LANG['buggy_php'] = 'PHP versie gededecteerd die onderhavig is aan een bug die kan resulteren in gecorrumpeerde berichten. Om dit te voorkomen, gebruik SMTP voor het verzenden van berichten, zet de mail.add_x_header optie in uw php.ini file uit, gebruik MacOS of Linux, of pas de gebruikte PHP versie aan naar versie 7.0.17+ or 7.1.3+.';
$PHPMAILER_LANG['buggy_php'] = 'PHP-versie gedetecteerd die onderhevig is aan een bug die kan resulteren in gecorrumpeerde berichten. Om dit te voorkomen, gebruik SMTP voor het verzenden van berichten, zet de optie mail.add_x_header in uw php.ini uit, gebruik MacOS of Linux, of pas de gebruikte PHP-versie aan naar versie 7.0.17+ or 7.1.3+.';
$PHPMAILER_LANG['connect_host'] = 'SMTP-fout: kon niet verbinden met SMTP-host.';
$PHPMAILER_LANG['data_not_accepted'] = 'SMTP-fout: data niet geaccepteerd.';
$PHPMAILER_LANG['empty_message'] = 'Berichttekst is leeg';
@@ -16,19 +17,22 @@ $PHPMAILER_LANG['execute'] = 'Kon niet uitvoeren: ';
$PHPMAILER_LANG['extension_missing'] = 'Extensie afwezig: ';
$PHPMAILER_LANG['file_access'] = 'Kreeg geen toegang tot bestand: ';
$PHPMAILER_LANG['file_open'] = 'Bestandsfout: kon bestand niet openen: ';
$PHPMAILER_LANG['from_failed'] = 'Het volgende afzendersadres is mislukt: ';
$PHPMAILER_LANG['from_failed'] = 'Het volgende afzenderadres is mislukt: ';
$PHPMAILER_LANG['instantiate'] = 'Kon mailfunctie niet initialiseren.';
$PHPMAILER_LANG['invalid_address'] = 'Ongeldig adres: ';
$PHPMAILER_LANG['invalid_header'] = 'Ongeldige header naam of waarde';
$PHPMAILER_LANG['invalid_header'] = 'Ongeldige headernaam of -waarde';
$PHPMAILER_LANG['invalid_hostentry'] = 'Ongeldige hostentry: ';
$PHPMAILER_LANG['invalid_host'] = 'Ongeldige host: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' mailer wordt niet ondersteund.';
$PHPMAILER_LANG['provide_address'] = 'Er moet minstens één ontvanger worden opgegeven.';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP-fout: de volgende ontvangers zijn mislukt: ';
$PHPMAILER_LANG['signing'] = 'Signeerfout: ';
$PHPMAILER_LANG['smtp_code'] = 'SMTP code: ';
$PHPMAILER_LANG['smtp_code_ex'] = 'Aanvullende SMTP informatie: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Verbinding mislukt.';
$PHPMAILER_LANG['smtp_code'] = 'SMTP-code: ';
$PHPMAILER_LANG['smtp_code_ex'] = 'Aanvullende SMTP-informatie: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP-verbinding mislukt.';
$PHPMAILER_LANG['smtp_detail'] = 'Detail: ';
$PHPMAILER_LANG['smtp_error'] = 'SMTP-serverfout: ';
$PHPMAILER_LANG['variable_set'] = 'Kan de volgende variabele niet instellen of resetten: ';
$PHPMAILER_LANG['variable_set'] = 'Kan de volgende variabele niet instellen of herstellen: ';
$PHPMAILER_LANG['no_smtputf8'] = 'De server ondersteunt geen SMTPUTF8 dat nodig is om naar Unicode-adressen te sturen.';
$PHPMAILER_LANG['imap_recommended'] = 'Het gebruik van de vereenvoudigde adresparser is niet aanbevolen. Installeer de IMAP-extensie voor PHP voor volledige RFC822-ondersteuning.';
$PHPMAILER_LANG['deprecated_argument'] = 'Verouderd argument: ';
+22 -22
View File
@@ -5,29 +5,29 @@
* @package PHPMailer
*/
$PHPMAILER_LANG['authenticate'] = 'Błąd SMTP: Nie można przeprowadzić uwierzytelnienia.';
$PHPMAILER_LANG['buggy_php'] = 'Twoja wersja PHP zawiera błąd, który może powodować uszkodzenie wiadomości. Aby go naprawić, przełącz się na wysyłanie za pomocą SMTP, wyłącz opcję mail.add_x_header w php.ini, przełącz się na MacOS lub Linux lub zaktualizuj PHP do wersji 7.0.17+ lub 7.1.3+.';
$PHPMAILER_LANG['connect_host'] = 'Błąd SMTP: Nie można połączyć się z wybranym hostem.';
$PHPMAILER_LANG['data_not_accepted'] = 'Błąd SMTP: Dane nie zostały przyjęte.';
$PHPMAILER_LANG['empty_message'] = 'Wiadomość jest pusta.';
$PHPMAILER_LANG['encoding'] = 'Błędny sposób kodowania znaków: ';
$PHPMAILER_LANG['execute'] = 'Nie można uruchomić: ';
$PHPMAILER_LANG['extension_missing'] = 'Brakujące rozszerzenie: ';
$PHPMAILER_LANG['authenticate'] = 'Błąd SMTP: nie udało się przeprowadzić uwierzytelnienia.';
$PHPMAILER_LANG['buggy_php'] = 'Używana wersja PHP zawiera błąd, który może powodować uszkodzenie wiadomości. Aby temu zapobiec, użyj wysyłki przez SMTP, wyłącz opcję mail.add_x_header w php.ini, przejdź na macOS lub Linux, lub zaktualizuj PHP do wersji 7.0.17+ albo 7.1.3+.';
$PHPMAILER_LANG['connect_host'] = 'Błąd SMTP: nie udało się połączyć z serwerem (hostem).';
$PHPMAILER_LANG['data_not_accepted'] = 'Błąd SMTP: dane wiadomości nie zostały przyjęte przez serwer.';
$PHPMAILER_LANG['empty_message'] = 'Nie można wysłać pustej wiadomości.';
$PHPMAILER_LANG['encoding'] = 'Nieobsługiwane kodowanie znaków: ';
$PHPMAILER_LANG['execute'] = 'Nie udało się uruchomić polecenia: ';
$PHPMAILER_LANG['extension_missing'] = 'Brak wymaganego rozszerzenia PHP: ';
$PHPMAILER_LANG['file_access'] = 'Brak dostępu do pliku: ';
$PHPMAILER_LANG['file_open'] = 'Nie można otworzyć pliku: ';
$PHPMAILER_LANG['from_failed'] = 'Następujący adres nadawcy jest nieprawidłowy lub nie istnieje: ';
$PHPMAILER_LANG['instantiate'] = 'Nie można wywołać funkcji mail(). Sprawdź konfigurację serwera.';
$PHPMAILER_LANG['invalid_address'] = 'Nie można wysłać wiadomości, ' . 'następujący adres odbiorcy jest nieprawidłowy lub nie istnieje: ';
$PHPMAILER_LANG['invalid_header'] = 'Nieprawidłowa nazwa lub wartość nagłówka';
$PHPMAILER_LANG['file_open'] = 'Nie udało się otworzyć pliku: ';
$PHPMAILER_LANG['from_failed'] = 'Nieprawidłowy adres nadawcy: ';
$PHPMAILER_LANG['instantiate'] = 'Nie można zainicjować funkcji mail(). Sprawdź konfigurację serwera.';
$PHPMAILER_LANG['invalid_address'] = 'Nie można wysłać wiadomości. Nieprawidłowy adres odbiorcy: ';
$PHPMAILER_LANG['invalid_header'] = 'Nieprawidłowa nazwa lub wartość nagłówka.';
$PHPMAILER_LANG['invalid_hostentry'] = 'Nieprawidłowy wpis hosta: ';
$PHPMAILER_LANG['invalid_host'] = 'Nieprawidłowy host: ';
$PHPMAILER_LANG['provide_address'] = 'Należy podać prawidłowy adres email odbiorcy.';
$PHPMAILER_LANG['invalid_host'] = 'Nieprawidłowa nazwa hosta: ';
$PHPMAILER_LANG['provide_address'] = 'Musisz podać co najmniej jeden prawidłowy adres e-mail odbiorcy.';
$PHPMAILER_LANG['mailer_not_supported'] = 'Wybrana metoda wysyłki wiadomości nie jest obsługiwana.';
$PHPMAILER_LANG['recipients_failed'] = 'Błąd SMTP: Następujący odbiorcy są nieprawidłowi lub nie istnieją: ';
$PHPMAILER_LANG['signing'] = 'Błąd podpisywania wiadomości: ';
$PHPMAILER_LANG['smtp_code'] = 'Kod SMTP: ';
$PHPMAILER_LANG['smtp_code_ex'] = 'Dodatkowe informacje SMTP: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'Wywołanie funkcji SMTP Connect() zostało zakończone niepowodzeniem.';
$PHPMAILER_LANG['smtp_detail'] = 'Szczegóły: ';
$PHPMAILER_LANG['smtp_error'] = 'Błąd SMTP: ';
$PHPMAILER_LANG['recipients_failed'] = 'Błąd SMTP: nie udało się wysłać do następujących odbiorców: ';
$PHPMAILER_LANG['signing'] = 'Błąd podpisywania wiadomości cyfrowo: ';
$PHPMAILER_LANG['smtp_code'] = 'Kod odpowiedzi SMTP: ';
$PHPMAILER_LANG['smtp_code_ex'] = 'Dodatkowe informacje serwera SMTP: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'Nie udało się nawiązać połączenia za pomocą SMTP Connect().';
$PHPMAILER_LANG['smtp_detail'] = 'Szczegóły błędu: ';
$PHPMAILER_LANG['smtp_error'] = 'Błąd serwera SMTP: ';
$PHPMAILER_LANG['variable_set'] = 'Nie można ustawić lub zmodyfikować zmiennej: ';
+24 -17
View File
@@ -3,25 +3,32 @@
/**
* Portuguese (European) PHPMailer language file: refer to English translation for definitive list
* @package PHPMailer
* @author Jonadabe <jonadabe@hotmail.com>
* @author João Vieira <mail@joaovieira.eu>
*/
$PHPMAILER_LANG['authenticate'] = 'Erro do SMTP: Não foi possível realizar a autenticação.';
$PHPMAILER_LANG['connect_host'] = 'Erro do SMTP: Não foi possível realizar ligação com o servidor SMTP.';
$PHPMAILER_LANG['data_not_accepted'] = 'Erro do SMTP: Os dados foram rejeitados.';
$PHPMAILER_LANG['empty_message'] = 'A mensagem no e-mail está vazia.';
$PHPMAILER_LANG['authenticate'] = 'Erro SMTP: Falha na autenticação.';
$PHPMAILER_LANG['buggy_php'] = 'A sua versão do PHP tem um bug que pode causar mensagens corrompidas. Para resolver, utilize o envio por SMTP, desative a opção mail.add_x_header no ficheiro php.ini, mude para MacOS ou Linux, ou atualize o PHP para a versão 7.0.17+ ou 7.1.3+.';
$PHPMAILER_LANG['connect_host'] = 'Erro SMTP: Não foi possível ligar ao servidor SMTP.';
$PHPMAILER_LANG['data_not_accepted'] = 'Erro SMTP: Dados não aceites.';
$PHPMAILER_LANG['empty_message'] = 'A mensagem de e-mail está vazia.';
$PHPMAILER_LANG['encoding'] = 'Codificação desconhecida: ';
$PHPMAILER_LANG['execute'] = 'Não foi possível executar: ';
$PHPMAILER_LANG['file_access'] = 'Não foi possível aceder o ficheiro: ';
$PHPMAILER_LANG['file_open'] = 'Abertura do ficheiro: Não foi possível abrir o ficheiro: ';
$PHPMAILER_LANG['from_failed'] = 'Ocorreram falhas nos endereços dos seguintes remententes: ';
$PHPMAILER_LANG['instantiate'] = 'Não foi possível iniciar uma instância da função mail.';
$PHPMAILER_LANG['invalid_address'] = 'Não foi enviado nenhum e-mail para o endereço de e-mail inválido: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' mailer não é suportado.';
$PHPMAILER_LANG['provide_address'] = 'Tem de fornecer pelo menos um endereço como destinatário do e-mail.';
$PHPMAILER_LANG['recipients_failed'] = 'Erro do SMTP: O endereço do seguinte destinatário falhou: ';
$PHPMAILER_LANG['signing'] = 'Erro ao assinar: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() falhou.';
$PHPMAILER_LANG['smtp_error'] = 'Erro de servidor SMTP: ';
$PHPMAILER_LANG['variable_set'] = 'Não foi possível definir ou redefinir a variável: ';
$PHPMAILER_LANG['extension_missing'] = 'Extensão em falta: ';
$PHPMAILER_LANG['file_access'] = 'Não foi possível aceder ao ficheiro: ';
$PHPMAILER_LANG['file_open'] = 'Erro ao abrir o ficheiro: ';
$PHPMAILER_LANG['from_failed'] = 'O envio falhou para o seguinte endereço do remetente: ';
$PHPMAILER_LANG['instantiate'] = 'Não foi possível instanciar a função mail.';
$PHPMAILER_LANG['invalid_address'] = 'Endereço de e-mail inválido: ';
$PHPMAILER_LANG['invalid_header'] = 'Nome ou valor do cabeçalho inválido.';
$PHPMAILER_LANG['invalid_hostentry'] = 'Entrada de host inválida: ';
$PHPMAILER_LANG['invalid_host'] = 'Host inválido: ';
$PHPMAILER_LANG['mailer_not_supported'] = 'O cliente de e-mail não é suportado.';
$PHPMAILER_LANG['provide_address'] = 'Deve fornecer pelo menos um endereço de destinatário.';
$PHPMAILER_LANG['recipients_failed'] = 'Erro SMTP: Falha no envio para os seguintes destinatários: ';
$PHPMAILER_LANG['signing'] = 'Erro ao assinar: ';
$PHPMAILER_LANG['smtp_code'] = 'Código SMTP: ';
$PHPMAILER_LANG['smtp_code_ex'] = 'Informações adicionais SMTP: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'Falha na função SMTP connect().';
$PHPMAILER_LANG['smtp_detail'] = 'Detalhes: ';
$PHPMAILER_LANG['smtp_error'] = 'Erro do servidor SMTP: ';
$PHPMAILER_LANG['variable_set'] = 'Não foi possível definir ou redefinir a variável: ';
@@ -8,6 +8,7 @@
* @author Mehmet Benlioğlu
* @author @yasinaydin
* @author Ogün Karakuş
* @author Mustafa Deniz Buksur
*/
$PHPMAILER_LANG['authenticate'] = 'SMTP Hatası: Oturum açılamadı.';
@@ -36,3 +37,7 @@ $PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP connect() fonksiyonu başarısı
$PHPMAILER_LANG['smtp_detail'] = 'SMTP SMTP Detayı: ';
$PHPMAILER_LANG['smtp_error'] = 'SMTP sunucu hatası: ';
$PHPMAILER_LANG['variable_set'] = 'Değişken ayarlanamadı ya da sıfırlanamadı: ';
$PHPMAILER_LANG['no_smtputf8'] = 'Unicode adreslere gönderim için gereken SMTPUTF8 desteği sunucu tarafından desteklenmiyor.';
$PHPMAILER_LANG['imap_recommended'] = 'Basitleştirilmiş adres ayrıştırıcısını kullanmanız önerilmez. ' .
'Tam RFC822 ayrıştırma için PHP IMAP eklentisini yükleyin.';
$PHPMAILER_LANG['deprecated_argument'] = 'Kullanımdan kaldırılmış argüman: ';
File diff suppressed because it is too large Load Diff
+20 -6
View File
@@ -45,8 +45,9 @@ class POP3
* The POP3 PHPMailer Version number.
*
* @var string
* @deprecated This constant will be removed in PHPMailer 8.0. Use `PHPMailer::VERSION` instead.
*/
const VERSION = '6.9.3';
const VERSION = '7.1.1';
/**
* Default POP3 port number.
@@ -211,9 +212,9 @@ class POP3
} else {
$this->tval = (int) $timeout;
}
$this->do_debug = $debug_level;
$this->username = $username;
$this->password = $password;
$this->do_debug = (int) $debug_level;
$this->username = self::stripControls($username);
$this->password = self::stripControls($password);
//Reset the error log
$this->errors = [];
//Connect
@@ -318,7 +319,8 @@ class POP3
if (empty($password)) {
$password = $this->password;
}
$username = self::stripControls($username);
$password = self::stripControls($password);
//Send the Username
$this->sendString("USER $username" . static::LE);
$pop3_response = $this->getResponse();
@@ -406,7 +408,7 @@ class POP3
/**
* Checks the POP3 server response.
* Looks for for +OK or -ERR.
* Looks for +OK or -ERR.
*
* @param string $string
*
@@ -466,4 +468,16 @@ class POP3
"errno: $errno errstr: $errstr; errfile: $errfile; errline: $errline"
);
}
/**
* Strip all control chars from a string.
*
* @param $string
*
* @return string
*/
protected static function stripControls($string)
{
return preg_replace('/[\x00-\x1F\x7F]/u', '', $string);
}
}
+121 -13
View File
@@ -34,8 +34,9 @@ class SMTP
* The PHPMailer SMTP version number.
*
* @var string
* @deprecated This constant will be removed in PHPMailer 8.0. Use `PHPMailer::VERSION` instead.
*/
const VERSION = '6.9.3';
const VERSION = '7.1.1';
/**
* SMTP line break constant.
@@ -159,6 +160,15 @@ class SMTP
*/
public $do_verp = false;
/**
* Whether to use SMTPUTF8.
*
* @see https://www.rfc-editor.org/rfc/rfc6531
*
* @var bool
*/
public $do_smtputf8 = false;
/**
* The timeout value for connection, in seconds.
* Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
@@ -196,6 +206,7 @@ class SMTP
'Haraka' => '/[\d]{3} Message Queued \((.*)\)/',
'ZoneMTA' => '/[\d]{3} Message queued as (.*)/',
'Mailjet' => '/[\d]{3} OK queued as (.*)/',
'Gsmtp' => '/[\d]{3} 2\.0\.0 OK (.*) - gsmtp/',
];
/**
@@ -484,7 +495,9 @@ class SMTP
//PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT
//so add them back in manually if we can
if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
// phpcs:ignore PHPCompatibility.Constants.NewConstants.stream_crypto_method_tlsv1_2_clientFound
$crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
// phpcs:ignore PHPCompatibility.Constants.NewConstants.stream_crypto_method_tlsv1_1_clientFound
$crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
}
@@ -623,11 +636,48 @@ class SMTP
if (null === $OAuth) {
return false;
}
$oauth = $OAuth->getOauth64();
//Start authentication
if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
return false;
try {
$oauth = $OAuth->getOauth64();
} catch (\Exception $e) {
// We catch all exceptions and convert them to PHPMailer exceptions to be able to
// handle them correctly later
throw new Exception("SMTP authentication error", 0, $e);
}
/*
* An SMTP command line can have a maximum length of 512 bytes, including the command name,
* so the base64-encoded OAUTH token has a maximum length of:
* 512 - 13 (AUTH XOAUTH2) - 2 (CRLF) = 497 bytes
* If the token is longer than that, the command and the token must be sent separately as described in
* https://www.rfc-editor.org/rfc/rfc4954#section-4
*/
if ($oauth === '') {
//Sending an empty auth token is legitimate, but it must be encoded as '='
//to indicate it's not a 2-part command
if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 =', 235)) {
return false;
}
} elseif (strlen($oauth) <= 497) {
//Authenticate using a token in the initial-response part
if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
return false;
}
} else {
//The token is too long, so we need to send it in two parts.
//Send the auth command without a token and expect a 334
if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2', 334)) {
return false;
}
//Send the token
if (!$this->sendCommand('OAuth TOKEN', $oauth, [235, 334])) {
return false;
}
//If the server answers with 334, send an empty line and wait for a 235
if (
substr($this->last_reply, 0, 3) === '334'
&& $this->sendCommand('AUTH End', '', 235)
) {
return false;
}
}
break;
default:
@@ -720,6 +770,25 @@ class SMTP
}
}
private function iterateLines($s)
{
$start = 0;
$length = strlen($s);
for ($i = 0; $i < $length; $i++) {
$c = $s[$i];
if ($c === "\n" || $c === "\r") {
yield substr($s, $start, $i - $start);
if ($c === "\r" && $i + 1 < $length && $s[$i + 1] === "\n") {
$i++;
}
$start = $i + 1;
}
}
yield substr($s, $start);
}
/**
* Send an SMTP DATA command.
* Issues a data command and sends the msg_data to the server,
@@ -748,15 +817,16 @@ class SMTP
* NOTE: this does not count towards line-length limit.
*/
//Normalize line breaks before exploding
$lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $msg_data));
//Iterate over lines with normalized line breaks
$lines = $this->iterateLines($msg_data);
/* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
* of the first line (':' separated) does not contain a space then it _should_ be a header, and we will
* process all lines before a blank line as headers.
*/
$field = substr($lines[0], 0, strpos($lines[0], ':'));
$first_line = $lines->current();
$field = substr($first_line, 0, strpos($first_line, ':'));
$in_headers = false;
if (!empty($field) && strpos($field, ' ') === false) {
$in_headers = true;
@@ -913,7 +983,15 @@ class SMTP
* $from. Returns true if successful or false otherwise. If True
* the mail transaction is started and then one or more recipient
* commands may be called followed by a data command.
* Implements RFC 821: MAIL <SP> FROM:<reverse-path> <CRLF>.
* Implements RFC 821: MAIL <SP> FROM:<reverse-path> <CRLF> and
* two extensions, namely XVERP and SMTPUTF8.
*
* The server's EHLO response is not checked. If use of either
* extensions is enabled even though the server does not support
* that, mail submission will fail.
*
* XVERP is documented at https://www.postfix.org/VERP_README.html
* and SMTPUTF8 is specified in RFC 6531.
*
* @param string $from Source address of this message
*
@@ -922,10 +1000,11 @@ class SMTP
public function mail($from)
{
$useVerp = ($this->do_verp ? ' XVERP' : '');
$useSmtputf8 = ($this->do_smtputf8 ? ' SMTPUTF8' : '');
return $this->sendCommand(
'MAIL FROM',
'MAIL FROM:<' . $from . '>' . $useVerp,
'MAIL FROM:<' . $from . '>' . $useSmtputf8 . $useVerp,
250
);
}
@@ -1210,7 +1289,7 @@ class SMTP
* 3. EHLO has been sent -
* $name == 'HELO'|'EHLO': returns the server name
* $name == any other string: if extension $name exists, returns True
* or its options (e.g. AUTH mechanisms supported). Otherwise returns False.
* or its options (e.g. AUTH mechanisms supported). Otherwise, returns False.
*
* @param string $name Name of SMTP extension or 'HELO'|'EHLO'
*
@@ -1291,7 +1370,16 @@ class SMTP
//stream_select returns false when the `select` system call is interrupted
//by an incoming signal, try the select again
if (stripos($message, 'interrupted system call') !== false) {
if (
stripos($message, 'interrupted system call') !== false ||
(
// on applications with a different locale than english, the message above is not found because
// it's translated. So we also check for the SOCKET_EINTR constant which is defined under
// Windows and UNIX-like platforms (if available on the platform).
defined('SOCKET_EINTR') &&
stripos($message, 'stream_select(): Unable to select [' . SOCKET_EINTR . ']') !== false
)
) {
$this->edebug(
'SMTP -> get_lines(): retrying stream_select',
self::DEBUG_LOWLEVEL
@@ -1364,6 +1452,26 @@ class SMTP
return $this->do_verp;
}
/**
* Enable or disable use of SMTPUTF8.
*
* @param bool $enabled
*/
public function setSMTPUTF8($enabled = false)
{
$this->do_smtputf8 = $enabled;
}
/**
* Get SMTPUTF8 use.
*
* @return bool
*/
public function getSMTPUTF8()
{
return $this->do_smtputf8;
}
/**
* Set error messages and codes.
*