Compare commits

..

7 Commits

Author SHA1 Message Date
RMubarakzyanov 2ad2e06dc2 fixed columns bind 2026-06-17 21:14:45 +03:00
RMubarakzyanov 25c3990753 json bindings filter && order 2026-06-16 10:20:03 +03:00
RMubarakzyanov 9eb257d237 json bindings 2026-06-13 20:19:36 +03:00
RMubarakzyanov d623ee797c Merge branch 'main' into features/jsonintegration 2026-06-11 15:33:30 +03:00
solocla d088364a0d report search 2026-06-10 11:02:43 +02:00
solocla 6e43a178af added fixed field in json 2026-06-09 09:46:21 +02:00
solocla 25bd916221 import insert json 2026-06-08 12:45:37 +02:00
95 changed files with 6113 additions and 12075 deletions
+1 -2
View File
@@ -60,5 +60,4 @@ public/userarea/schemi_base_response.json
# Ignora tutti i log ovunque
*.log
public/userarea/cache/
public/userarea/error_log.txt
public/userarea/cache/
@@ -109,8 +109,6 @@ class LoginController extends Controller
// Reindirizza in base al ruolo
if ($user->hasRole('Admin')) {
return redirect()->to('userarea/import_dashboard.php');
} elseif ($user->hasRole('SuperUser')) {
return redirect()->to('userarea/import_dashboard.php');
} elseif ($user->hasRole('User')) {
return redirect()->to('userarea/import_dashboard.php');
}
-1
View File
@@ -44,7 +44,6 @@
"phpmailer/phpmailer": "^6.9",
"phpoffice/phpspreadsheet": "^4.1",
"proengsoft/laravel-jsvalidation": "^4.0.0",
"robmorgan/phinx": "^0.16.12",
"socialiteproviders/microsoft": "^4.7",
"spatie/laravel-query-builder": "^5.0",
"vanguardapp/activity-log": "^6.0",
Generated
+2 -646
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": "9c4f1e3bc3ee2180211c055e70635aef",
"packages": [
{
"name": "akaunting/laravel-setting",
@@ -251,330 +251,6 @@
],
"time": "2023-11-29T23:19:16+00:00"
},
{
"name": "cakephp/chronos",
"version": "3.5.0",
"source": {
"type": "git",
"url": "https://github.com/cakephp/chronos.git",
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/chronos/zipball/e6e777b534244911566face8a5dbdbd7f7bda5a6",
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/clock": "^1.0"
},
"provide": {
"psr/clock-implementation": "1.0"
},
"require-dev": {
"cakephp/cakephp-codesniffer": "^5.0",
"phpunit/phpunit": "^10.5.58 || ^11.5.3 || ^12.1.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Cake\\Chronos\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brian Nesbitt",
"email": "brian@nesbot.com",
"homepage": "http://nesbot.com"
},
{
"name": "The CakePHP Team",
"homepage": "https://cakephp.org"
}
],
"description": "A simple API extension for DateTime.",
"homepage": "https://cakephp.org",
"keywords": [
"date",
"datetime",
"time"
],
"support": {
"issues": "https://github.com/cakephp/chronos/issues",
"source": "https://github.com/cakephp/chronos"
},
"time": "2026-04-10T02:50:39+00:00"
},
{
"name": "cakephp/core",
"version": "5.3.6",
"source": {
"type": "git",
"url": "https://github.com/cakephp/core.git",
"reference": "9c458b0e9322ec88bc4c758b33cde6a0abf49d12"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/core/zipball/9c458b0e9322ec88bc4c758b33cde6a0abf49d12",
"reference": "9c458b0e9322ec88bc4c758b33cde6a0abf49d12",
"shasum": ""
},
"require": {
"cakephp/utility": "^5.3.0",
"league/container": "^5.1",
"php": ">=8.2",
"psr/container": "^1.1 || ^2.0"
},
"provide": {
"psr/container-implementation": "^2.0"
},
"suggest": {
"cakephp/cache": "To use Configure::store() and restore().",
"cakephp/event": "To use PluginApplicationInterface or plugin applications.",
"league/container": "To use Container and ServiceProvider classes"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"files": [
"functions.php"
],
"psr-4": {
"Cake\\Core\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/core/graphs/contributors"
}
],
"description": "CakePHP Framework Core classes",
"homepage": "https://cakephp.org",
"keywords": [
"cakephp",
"core",
"framework"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/core"
},
"time": "2026-05-15T03:31:14+00:00"
},
{
"name": "cakephp/database",
"version": "5.3.6",
"source": {
"type": "git",
"url": "https://github.com/cakephp/database.git",
"reference": "442d12bf0a1edeffc555a59de9f955cb0619c7ca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/database/zipball/442d12bf0a1edeffc555a59de9f955cb0619c7ca",
"reference": "442d12bf0a1edeffc555a59de9f955cb0619c7ca",
"shasum": ""
},
"require": {
"cakephp/chronos": "^3.3",
"cakephp/core": "^5.3.0",
"cakephp/datasource": "^5.3.0",
"php": ">=8.2",
"psr/log": "^3.0"
},
"require-dev": {
"cakephp/i18n": "^5.3.0",
"cakephp/log": "^5.3.0"
},
"suggest": {
"cakephp/i18n": "If you are using locale-aware datetime formats.",
"cakephp/log": "If you want to use query logging without providing a logger yourself."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"psr-4": {
"Cake\\Database\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/database/graphs/contributors"
}
],
"description": "Flexible and powerful Database abstraction library with a familiar PDO-like API",
"homepage": "https://cakephp.org",
"keywords": [
"abstraction",
"cakephp",
"database",
"database abstraction",
"pdo"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/database"
},
"time": "2026-05-21T19:38:13+00:00"
},
{
"name": "cakephp/datasource",
"version": "5.3.6",
"source": {
"type": "git",
"url": "https://github.com/cakephp/datasource.git",
"reference": "b768f0c0bf0fca815f83c4caef14e7fbb2409bf5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/datasource/zipball/b768f0c0bf0fca815f83c4caef14e7fbb2409bf5",
"reference": "b768f0c0bf0fca815f83c4caef14e7fbb2409bf5",
"shasum": ""
},
"require": {
"cakephp/core": "^5.3.0",
"php": ">=8.2",
"psr/simple-cache": "^2.0 || ^3.0"
},
"require-dev": {
"cakephp/cache": "^5.3.0",
"cakephp/collection": "^5.3.0",
"cakephp/utility": "^5.3.0"
},
"suggest": {
"cakephp/cache": "If you decide to use Query caching.",
"cakephp/collection": "If you decide to use ResultSetInterface.",
"cakephp/utility": "If you decide to use EntityTrait."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"psr-4": {
"Cake\\Datasource\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/datasource/graphs/contributors"
}
],
"description": "Provides connection managing and traits for Entities and Queries that can be reused for different datastores",
"homepage": "https://cakephp.org",
"keywords": [
"cakephp",
"connection management",
"datasource",
"entity",
"query"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/datasource"
},
"time": "2026-05-20T10:27:33+00:00"
},
{
"name": "cakephp/utility",
"version": "5.3.6",
"source": {
"type": "git",
"url": "https://github.com/cakephp/utility.git",
"reference": "4c703a010b9d955fed44731669e35d3043425cc7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/utility/zipball/4c703a010b9d955fed44731669e35d3043425cc7",
"reference": "4c703a010b9d955fed44731669e35d3043425cc7",
"shasum": ""
},
"require": {
"cakephp/core": "^5.3.0",
"php": ">=8.2"
},
"suggest": {
"ext-intl": "To use Text::transliterate() or Text::slug()",
"lib-ICU": "To use Text::transliterate() or Text::slug()"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Cake\\Utility\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/utility/graphs/contributors"
}
],
"description": "CakePHP Utility classes such as Inflector, String, Hash, and Security",
"homepage": "https://cakephp.org",
"keywords": [
"cakephp",
"hash",
"inflector",
"security",
"string",
"utility"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/utility"
},
"time": "2026-05-21T19:38:13+00:00"
},
{
"name": "carbonphp/carbon-doctrine-types",
"version": "3.2.0",
@@ -2951,90 +2627,6 @@
],
"time": "2022-12-11T20:36:23+00:00"
},
{
"name": "league/container",
"version": "5.2.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/container.git",
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/container/zipball/58accbc032f0090a9bd08326f93062c5a658b2c5",
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5",
"shasum": ""
},
"require": {
"php": "^8.1",
"psr/container": "^2.0.2",
"psr/event-dispatcher": "^1.0"
},
"provide": {
"psr/container-implementation": "^1.0"
},
"replace": {
"orno/di": "~2.0"
},
"require-dev": {
"nette/php-generator": "^4.1",
"nikic/php-parser": "^5.0",
"phpstan/phpstan": "^2.1.11",
"phpunit/phpunit": "^10.5.45|^11.5.15|^12.0",
"roave/security-advisories": "dev-latest",
"scrutinizer/ocular": "^1.9",
"squizlabs/php_codesniffer": "^3.9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-1.x": "1.x-dev",
"dev-2.x": "2.x-dev",
"dev-3.x": "3.x-dev",
"dev-4.x": "4.x-dev",
"dev-5.x": "5.x-dev",
"dev-master": "5.x-dev"
}
},
"autoload": {
"psr-4": {
"League\\Container\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Phil Bennett",
"email": "mail@philbennett.co.uk",
"role": "Developer"
}
],
"description": "A fast and intuitive dependency injection container.",
"homepage": "https://github.com/thephpleague/container",
"keywords": [
"container",
"dependency",
"di",
"injection",
"league",
"provider",
"service"
],
"support": {
"issues": "https://github.com/thephpleague/container/issues",
"source": "https://github.com/thephpleague/container/tree/5.2.0"
},
"funding": [
{
"url": "https://github.com/philipobenito",
"type": "github"
}
],
"time": "2026-03-19T18:52:39+00:00"
},
{
"name": "league/flysystem",
"version": "3.28.0",
@@ -5388,93 +4980,6 @@
],
"time": "2024-04-27T21:32:50+00:00"
},
{
"name": "robmorgan/phinx",
"version": "0.16.12",
"source": {
"type": "git",
"url": "https://github.com/cakephp/phinx.git",
"reference": "1cce44387a9146dc04c312e6a231b17fb30385cd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/phinx/zipball/1cce44387a9146dc04c312e6a231b17fb30385cd",
"reference": "1cce44387a9146dc04c312e6a231b17fb30385cd",
"shasum": ""
},
"require": {
"cakephp/database": "^5.0.2",
"composer-runtime-api": "^2.0",
"php-64bit": ">=8.1",
"psr/container": "^1.1|^2.0",
"symfony/config": "^4.0|^5.0|^6.0|^7.0|^8.0",
"symfony/console": "^6.0|^7.0|^8.0"
},
"require-dev": {
"cakephp/cakephp-codesniffer": "^5.0",
"cakephp/i18n": "^5.0",
"ext-json": "*",
"ext-pdo": "*",
"phpunit/phpunit": "^10.5",
"symfony/yaml": "^4.0|^5.0|^6.0|^7.0|^8.0"
},
"suggest": {
"ext-json": "Install if using JSON configuration format",
"ext-pdo": "PDO extension is needed",
"symfony/yaml": "Install if using YAML configuration format"
},
"bin": [
"bin/phinx"
],
"type": "library",
"autoload": {
"psr-4": {
"Phinx\\": "src/Phinx/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Rob Morgan",
"email": "robbym@gmail.com",
"homepage": "https://robmorgan.id.au",
"role": "Lead Developer"
},
{
"name": "Woody Gilk",
"email": "woody.gilk@gmail.com",
"homepage": "https://shadowhand.me",
"role": "Developer"
},
{
"name": "Richard Quadling",
"email": "rquadling@gmail.com",
"role": "Developer"
},
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/phinx/graphs/contributors",
"role": "Developer"
}
],
"description": "Phinx makes it ridiculously easy to manage the database migrations for your PHP app.",
"homepage": "https://phinx.org",
"keywords": [
"database",
"database migrations",
"db",
"migrations",
"phinx"
],
"support": {
"issues": "https://github.com/cakephp/phinx/issues",
"source": "https://github.com/cakephp/phinx/tree/0.16.12"
},
"time": "2026-07-03T07:27:17+00:00"
},
{
"name": "socialiteproviders/manager",
"version": "v4.8.1",
@@ -5807,85 +5312,6 @@
],
"time": "2024-05-31T14:57:53+00:00"
},
{
"name": "symfony/config",
"version": "v7.4.14",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
"reference": "7b665e443381ea7c4db03eb03b4bf79ea2b020eb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/7b665e443381ea7c4db03eb03b4bf79ea2b020eb",
"reference": "7b665e443381ea7c4db03eb03b4bf79ea2b020eb",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/filesystem": "^7.1|^8.0",
"symfony/polyfill-ctype": "~1.8"
},
"conflict": {
"symfony/finder": "<6.4",
"symfony/service-contracts": "<2.5"
},
"require-dev": {
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
"symfony/finder": "^6.4|^7.0|^8.0",
"symfony/messenger": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/yaml": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Config\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/config/tree/v7.4.14"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-06-09T07:51:57+00:00"
},
{
"name": "symfony/console",
"version": "v7.1.3",
@@ -6342,76 +5768,6 @@
],
"time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/filesystem",
"version": "v7.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.8"
},
"require-dev": {
"symfony/process": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Filesystem\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/filesystem/tree/v7.4.11"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-11T16:38:44+00:00"
},
{
"name": "symfony/finder",
"version": "v7.1.3",
@@ -11999,6 +11355,6 @@
"php": "^8.2.0",
"ext-json": "*"
},
"platform-dev": {},
"platform-dev": [],
"plugin-api-version": "2.6.0"
}
@@ -1,15 +0,0 @@
<?php
use Phinx\Migration\AbstractMigration;
final class CreatePhinxTestTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('phinx_test');
$table
->addColumn('name', 'string', ['limit' => 100, 'null' => false])
->addColumn('created_at', 'datetime', ['default' => 'CURRENT_TIMESTAMP'])
->create();
}
}
@@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddAddnoteToTemplateMapping extends AbstractMigration
{
public function change(): void
{
$table = $this->table('template_mapping');
$table->addColumn('is_addnote', 'boolean', [
'default' => 0,
'null' => false,
'after' => 'is_visible_parts',
'comment' => 'Campo utilizzabile come addnote (multi-selezione)',
])
->update();
}
}
@@ -1,48 +0,0 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateOptionsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('options');
$table
->addColumn('opt_key', 'string', ['limit' => 100])
->addColumn('opt_value', 'text', ['null' => true])
->addColumn('created_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP'])
->addColumn('updated_at', 'timestamp', [
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['opt_key'], ['unique' => true])
->create();
// Righe iniziali del popup
$this->table('options')->insert([
[
'opt_key' => 'popup_enabled',
'opt_value' => '1',
],
[
'opt_key' => 'popup_version',
'opt_value' => '2026-07-migrazione',
],
[
'opt_key' => 'popup',
'opt_value' =>
'<h2 style="margin-top:0;">Nuovo portale disponibile</h2>'
. '<p>Ci siamo spostati. Da ora usa il nuovo indirizzo:</p>'
. '<p style="text-align:center;">'
. '<a href="https://trfsmart-bv.cesoft.io/" '
. 'style="display:inline-block;padding:12px 24px;background:#2563eb;'
. 'color:#fff;text-decoration:none;border-radius:6px;">Vai al nuovo portale</a>'
. '</p>'
. '<p style="font-size:13px;color:#666;">Salva il nuovo indirizzo nei preferiti. '
. 'Se riscontri problemi puoi continuare a usare questo portale per ora.</p>',
],
])->saveData();
}
}
@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddExportDateToDatadb extends AbstractMigration
{
/**
* Aggiunge export_date su datadb: data/ora di esportazione verso il LIMS,
* valorizzata insieme al numero commessa quando status passa a 'l'.
*/
public function change(): void
{
$this->table('datadb')
->addColumn('export_date', 'datetime', [
'null' => true,
'default' => null,
'after' => 'commessaweb',
'comment' => 'Data/ora esportazione verso LIMS (valorizzata quando status = l)',
])
->addIndex(['export_date'], ['name' => 'idx_datadb_export_date'])
->update();
}
}
@@ -1,57 +0,0 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateLimsDailySamplesTable extends AbstractMigration
{
/**
* Tabella con un dato al giorno: numero totale di campioni inseriti
* nel LIMS (loro software) in quella giornata. Serve per calcolare
* la % di campioni esportati sul totale inserito nel LIMS.
*/
public function change(): void
{
$table = $this->table('lims_daily_samples', [
'id' => false,
'primary_key' => ['id'],
'comment' => 'Totale campioni inseriti nel LIMS per giornata',
]);
$table
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('sample_date', 'date', [
'null' => false,
'comment' => 'Giornata di riferimento',
])
->addColumn('total_samples', 'integer', [
'signed' => false,
'null' => false,
'default' => 0,
'comment' => 'Totale campioni inseriti nel LIMS in quella giornata',
])
->addColumn('note', 'string', [
'limit' => 255,
'null' => true,
'default' => null,
])
->addColumn('created_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => true,
'default' => null,
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['sample_date'], [
'unique' => true,
'name' => 'uq_lims_daily_samples_date',
])
->create();
}
}
@@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddTemplateNoteToExcelTemplates extends AbstractMigration
{
public function change(): void
{
$this->table('excel_templates')
->addColumn('template_note', 'text', [
'null' => true,
'default' => null,
'after' => 'idroutine',
'comment' => 'Nota opzionale del template, aggiunta in fondo alle note in esportazione',
])
->update();
}
}
@@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class DropUniqPartAnalysisIndex extends AbstractMigration
{
public function up(): void
{
$table = $this->table('identification_parts_analyses');
if ($table->hasIndexByName('uniq_part_analysis')) {
$table->removeIndexByName('uniq_part_analysis')->update();
}
}
public function down(): void
{
$table = $this->table('identification_parts_analyses');
if (!$table->hasIndexByName('uniq_part_analysis')) {
$table->addIndex(['part_id', 'analysis_recordkey'], [
'unique' => true,
'name' => 'uniq_part_analysis',
])->update();
}
}
}
-27
View File
@@ -1,27 +0,0 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->safeLoad();
return [
'paths' => [
'migrations' => '%%PHINX_CONFIG_DIR%%/db/migrations',
'seeds' => '%%PHINX_CONFIG_DIR%%/db/seeds',
],
'environments' => [
'default_migration_table' => 'phinxlog',
'default_environment' => 'production',
'production' => [
'adapter' => 'mysql',
'host' => $_ENV['DB_HOST'] ?? 'localhost',
'name' => $_ENV['DB_DATABASE'] ?? '',
'user' => $_ENV['DB_USERNAME'] ?? '',
'pass' => $_ENV['DB_PASSWORD'] ?? '',
'port' => $_ENV['DB_PORT'] ?? 3306,
'charset' => 'utf8mb4',
],
],
'version_order' => 'creation',
];
+39 -63
View File
@@ -7,7 +7,7 @@
function loadAnalysisMatrixNames() {
return $.ajax({
url: "search_matrici.php?full_list=1",
url: "get_matrici_db.php",
method: "GET",
dataType: "json",
})
@@ -106,9 +106,8 @@
}
container.innerHTML = items
.map(function (item, index) {
.map(function (item) {
const recordKey = item.analysis_recordkey || "";
const rowId = item.id != null ? item.id : "";
const title = item.analysis_name || "Unnamed analysis";
const method = item.analysis_method || "";
@@ -122,8 +121,6 @@
class="analysis-remove-btn"
data-part-id="${escapeHtml(partId)}"
data-recordkey="${escapeHtml(recordKey)}"
data-row-id="${escapeHtml(rowId)}"
data-index="${index}"
title="Remove analysis">×</button>
</div>
`;
@@ -169,21 +166,19 @@
};
}
function addAnalysisToLocalState(
partId,
payload,
iddatadb,
idmatrice,
newId,
) {
function addAnalysisToLocalState(partId, payload, iddatadb, idmatrice) {
const key = String(partId);
if (!Array.isArray(analysisAssignedState[key])) {
analysisAssignedState[key] = [];
}
{
const exists = analysisAssignedState[key].some(function (item) {
return item.analysis_recordkey === payload.analysis_recordkey;
});
if (!exists) {
analysisAssignedState[key].push({
id: newId || null,
id: null,
part_id: parseInt(partId, 10),
iddatadb: iddatadb || null,
idmatrice: idmatrice || null,
@@ -200,24 +195,17 @@
}
}
function removeAnalysisFromLocalState(partId, recordKey, index) {
function removeAnalysisFromLocalState(partId, recordKey) {
const key = String(partId);
if (!Array.isArray(analysisAssignedState[key])) {
return;
}
const i = parseInt(index, 10);
if (!isNaN(i) && i >= 0 && i < analysisAssignedState[key].length) {
analysisAssignedState[key].splice(i, 1);
return;
}
const found = analysisAssignedState[key].findIndex(function (item) {
return item.analysis_recordkey === recordKey;
});
if (found !== -1) {
analysisAssignedState[key].splice(found, 1);
}
analysisAssignedState[key] = analysisAssignedState[key].filter(
function (item) {
return item.analysis_recordkey !== recordKey;
},
);
}
function saveAnalysisAssociation(partId, payload, callback) {
@@ -249,13 +237,12 @@
is_accredited: payload.is_accredited,
},
})
.done(function (resp) {
.done(function () {
addAnalysisToLocalState(
partId,
payload,
iddatadb,
idmatrice !== "NO_MATRIX" ? idmatrice : null,
resp && resp.id ? resp.id : null,
);
renderAssignedAnalysesForPart(partId);
if (typeof callback === "function") callback(true);
@@ -270,13 +257,7 @@
});
}
function deleteAnalysisAssociation(
partId,
recordKey,
rowId,
index,
callback,
) {
function deleteAnalysisAssociation(partId, recordKey, callback) {
$.ajax({
url: "delete_part_analysis.php",
method: "POST",
@@ -284,11 +265,10 @@
data: {
part_id: partId,
analysis_recordkey: recordKey,
row_id: rowId || "",
},
})
.done(function () {
removeAnalysisFromLocalState(partId, recordKey, index);
removeAnalysisFromLocalState(partId, recordKey);
renderAssignedAnalysesForPart(partId);
if (typeof callback === "function") callback(true);
})
@@ -537,13 +517,6 @@
.done(function (response) {
const analyses = Array.isArray(response.value)
? response.value.filter(function (item) {
const name =
item.NomeAnalisiTraduzione ||
item.NomeAnalisi ||
"";
if (name.trim().startsWith("*")) {
return false;
}
return (
item.SelezionabileSuWeb === true ||
item.SelezionabileSuWeb === 1 ||
@@ -727,18 +700,10 @@
const partId = removeBtn.getAttribute("data-part-id");
const recordKey = removeBtn.getAttribute("data-recordkey");
const rowId = removeBtn.getAttribute("data-row-id");
const index = removeBtn.getAttribute("data-index");
deleteAnalysisAssociation(
partId,
recordKey,
rowId,
index,
function () {
const stillUsed = Object.keys(
analysisAssignedState,
).some(function (pid) {
deleteAnalysisAssociation(partId, recordKey, function () {
const stillUsed = Object.keys(analysisAssignedState).some(
function (pid) {
return (
Array.isArray(analysisAssignedState[pid]) &&
analysisAssignedState[pid].some(
@@ -750,15 +715,15 @@
},
)
);
});
},
);
if (!stillUsed) {
delete analysisSelectedState[recordKey];
}
if (!stillUsed) {
delete analysisSelectedState[recordKey];
}
syncSelectedAnalysisRows();
},
);
syncSelectedAnalysisRows();
});
return;
}
@@ -779,6 +744,17 @@
}
const recordKey = payload.analysis_recordkey;
const alreadySelected = !!analysisSelectedState[recordKey];
if (alreadySelected) {
selectedPartIds.forEach(function (partId) {
deleteAnalysisAssociation(partId, recordKey);
});
delete analysisSelectedState[recordKey];
syncSelectedAnalysisRows();
return;
}
let pending = selectedPartIds.length;
let atLeastOneSaved = false;
+2 -234
View File
@@ -19,9 +19,6 @@ $(document).ready(function () {
let descriptionTextbox = null;
let markerObjects = {};
let nextMarkerId = 1;
let freeTextMode = false;
let freeTextObjects = {};
let nextFreeTextId = 1;
let partsListData = [];
// DIMENSIONE GLOBALE MARKER
let globalMarkerSize = 16;
@@ -58,7 +55,7 @@ $(document).ready(function () {
.data("iddatadb", iddatadb || null)
.data("idquotations", idquotations || null);
// Parti (non legate al canvas) possono partire subito
loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations);
const modalElement = document.getElementById("annotationsModal");
@@ -77,18 +74,6 @@ $(document).ready(function () {
focus: true,
});
}
// IMPORTANTE: carica la foto SOLO dopo che il modale è completamente
// visibile (animazione fade terminata). Se l'immagine è già in cache
// del browser, l'evento "load" può scattare quasi subito, PRIMA che
// il contenitore del canvas abbia dimensioni reali (parent ancora
// display:none durante il fade) — questo rompeva il canvas Fabric.js
// per la prima foto mostrata (bug riprodotto solo con foto già in cache,
// tipico del server di test dove le foto erano già state visualizzate).
$(modalElement).one("shown.bs.modal", function () {
loadPhoto(iddatadb, idquotations);
});
modal.show();
// Inizializza slider dimensione marker
$("#markerSizeSlider").val(globalMarkerSize);
@@ -147,9 +132,6 @@ $(document).ready(function () {
selectedPartNumber = null;
unsavedChanges = false;
partsListData = [];
freeTextMode = false; // ← aggiungi
freeTextObjects = {}; // ← aggiungi
$("#addFreeTextBtnAnnotations").removeClass("active"); // ← aggiungi
if (fabricCanvas) {
fabricCanvas.off();
fabricCanvas.dispose();
@@ -198,29 +180,6 @@ $(document).ready(function () {
updateMarkers();
markUnsaved();
});
// ===================
// TESTO LIBERO — TOGGLE MODALITÀ
// ===================
$(document)
.off("click.freeTextToggle", "#addFreeTextBtnAnnotations")
.on("click.freeTextToggle", "#addFreeTextBtnAnnotations", function (e) {
e.preventDefault();
e.stopPropagation();
freeTextMode = !freeTextMode;
$(this).toggleClass("active", freeTextMode);
// Disattiva la selezione parte/marker mentre si aggiunge testo libero
if (freeTextMode) {
selectedPartNumber = null;
$("#partsListAnnotations .list-group-item").removeClass(
"active",
);
}
console.log("Free text mode:", freeTextMode);
});
// ===================
// COLORE LISTA DESCRIZIONI
// ===================
@@ -234,22 +193,9 @@ $(document).ready(function () {
if (descriptionTextbox && fabricCanvas) {
descriptionTextbox.set("fill", globalDescriptionColor);
fabricCanvas.renderAll();
}
// Aggiorna anche i testi liberi già piazzati sulla foto corrente
const currentPhoto = $("#samplePhotoAnnotations").attr("src");
const annotations = photoAnnotations[currentPhoto];
if (annotations && annotations.freeTexts) {
annotations.freeTexts.forEach((entry) => {
entry.color = globalDescriptionColor;
});
}
Object.values(freeTextObjects).forEach((obj) => {
obj.set("fill", globalDescriptionColor);
});
if (fabricCanvas) fabricCanvas.renderAll();
markUnsaved();
},
);
@@ -424,29 +370,7 @@ $(document).ready(function () {
console.log(
"Evento mouse:down su canvas, selectedPartNumber:",
selectedPartNumber,
"freeTextMode:",
freeTextMode,
);
// Modalità testo libero: ha priorità, un solo click e si disattiva da sola
if (freeTextMode) {
if (options.target) {
console.log(
"Click su un oggetto esistente, ignoro.",
);
return;
}
const pointer = fabricCanvas.getPointer(options.e);
const x = pointer.x / photoData.scale;
const y = pointer.y / photoData.scale;
addFreeText(x, y);
freeTextMode = false;
$("#addFreeTextBtnAnnotations").removeClass("active");
return;
}
if (selectedPartNumber === null) {
console.log(
"Nessuna parte selezionata, ignoro il click.",
@@ -501,7 +425,6 @@ $(document).ready(function () {
updateMarkers();
updateDescriptions();
updateFreeTexts();
}, 10);
});
}
@@ -1062,128 +985,7 @@ $(document).ready(function () {
},
});
}
// ===================
// TESTO LIBERO — CREAZIONE E RENDER
// ===================
function addFreeText(x, y) {
const currentPhoto = $("#samplePhotoAnnotations").attr("src");
if (!photoAnnotations[currentPhoto]) {
photoAnnotations[currentPhoto] = {
markers: [],
freeTexts: [],
hasDescriptions: false,
descriptionPosition: { x: 10, y: 10 },
descriptionSize: {
width: photoData.displayWidth * 0.3,
height: photoData.displayHeight * 0.3,
},
};
}
if (!photoAnnotations[currentPhoto].freeTexts) {
photoAnnotations[currentPhoto].freeTexts = [];
}
const entry = {
id: nextFreeTextId++,
x,
y,
text: "Testo",
color: globalDescriptionColor,
fontSize: Math.max(16, Math.round(globalMarkerSize * 0.8)),
};
photoAnnotations[currentPhoto].freeTexts.push(entry);
renderFreeText(entry, true);
markUnsaved();
}
function renderFreeText(entry, startEditing = false) {
const itext = new fabric.IText(entry.text, {
left: entry.x * photoData.scale,
top: entry.y * photoData.scale,
fontFamily: "Arial",
fontSize: entry.fontSize,
fill: entry.color,
selectable: true,
hasControls: true,
editable: true,
borderColor: "#333",
cornerColor: "#000",
cornerSize: 12,
transparentCorners: false,
cornerStyle: "rect",
borderScaleFactor: 2,
});
itext.freeTextId = entry.id;
// FIX: Fabric.js appende la <textarea> nascosta (usata per catturare la
// tastiera durante l'editing) a document.body, FUORI dal modale.
// Il focus trap di Bootstrap la considera "esterna" e ruba il focus
// ogni volta che Fabric lo assegna a quella textarea — così i tasti
// digitati non arrivano mai al testo. Spostiamo la textarea dentro
// il modale non appena l'editing viene avviato.
itext.on("editing:entered", function () {
const modalEl = document.getElementById("annotationsModal");
if (
this.hiddenTextarea &&
modalEl &&
this.hiddenTextarea.parentNode !== modalEl
) {
modalEl.appendChild(this.hiddenTextarea);
}
});
itext.on("moving", function () {
entry.x = this.left / photoData.scale;
entry.y = this.top / photoData.scale;
markUnsaved();
});
itext.on("editing:exited", function () {
entry.text = this.text;
markUnsaved();
});
itext.on("scaling", function () {
entry.fontSize = Math.round(this.fontSize * this.scaleY);
this.set({ fontSize: entry.fontSize, scaleX: 1, scaleY: 1 });
markUnsaved();
});
fabricCanvas.add(itext);
freeTextObjects[entry.id] = itext;
// IMPORTANTE: non attivare l'editing nello stesso evento mouse:down che
// ha creato l'oggetto. Fabric processa quel click come "click su canvas
// vuoto" (il target non esisteva ancora al momento del mousedown) e al
// successivo mouseup deseleziona tutto, interrompendo l'editing appena
// avviato — l'utente non riesce a scrivere né a spostare il testo.
// Rimandiamo l'attivazione al prossimo giro di event loop, così avviene
// DOPO che Fabric ha concluso la gestione nativa di quel click.
if (startEditing) {
setTimeout(() => {
fabricCanvas.setActiveObject(itext);
itext.enterEditing();
itext.selectAll();
fabricCanvas.renderAll();
}, 0);
}
fabricCanvas.renderAll();
}
function updateFreeTexts() {
for (let id in freeTextObjects) {
fabricCanvas.remove(freeTextObjects[id]);
delete freeTextObjects[id];
}
freeTextObjects = {};
const currentPhoto = $("#samplePhotoAnnotations").attr("src");
const annotations = photoAnnotations[currentPhoto];
if (!annotations || !annotations.freeTexts) return;
annotations.freeTexts.forEach((entry) => renderFreeText(entry, false));
}
// ===================
// MARKERS & DESCRIPTIONS
// ===================
@@ -1489,40 +1291,6 @@ $(document).ready(function () {
}
}
// ===================
// TESTO LIBERO — RIMOZIONE CON CANC/DELETE
// ===================
$(document).on("keydown.freeTextDelete", function (e) {
// Solo se il modale annotazioni è aperto
if (!$("#annotationsModal").hasClass("show")) return;
if (!fabricCanvas) return;
// Ignora se si sta scrivendo dentro un campo di editing testo
// (es. l'utente sta modificando il contenuto dell'IText stesso)
const active = fabricCanvas.getActiveObject();
if (active && active.isEditing) return;
if (e.key !== "Delete" && e.key !== "Backspace") return;
if (!active || active.freeTextId === undefined) return;
e.preventDefault();
const currentPhoto = $("#samplePhotoAnnotations").attr("src");
const annotations = photoAnnotations[currentPhoto];
if (annotations && annotations.freeTexts) {
annotations.freeTexts = annotations.freeTexts.filter(
(entry) => entry.id !== active.freeTextId,
);
}
fabricCanvas.remove(active);
delete freeTextObjects[active.freeTextId];
fabricCanvas.renderAll();
markUnsaved();
});
// Delegazione evento per il pulsante "Descrizioni"
$(document)
.off("click.addDescriptions", "#addDescriptionsBtnAnnotations")
+449
View File
@@ -0,0 +1,449 @@
<?php
include('include/headscript.php');
require_once(__DIR__ . '/class/binding-functions.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Filtri comuni.
$templateFilter = isset($_GET['template_id']) ? intval($_GET['template_id']) : 0;
$search = trim($_GET['q'] ?? '');
$perPage = 50;
$page = max(1, intval($_GET['page'] ?? 1));
$dir = (strtolower($_GET['dir'] ?? 'asc') === 'desc') ? 'DESC' : 'ASC';
// Modalita': overview (gruppi per campo) oppure detail (valori di un campo).
$focusTarget = trim($_GET['target'] ?? '');
$mode = $focusTarget !== '' ? 'detail' : 'overview';
$templates = $pdo->query("SELECT id, name FROM excel_templates ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
// Helper: URL che preserva i filtri correnti.
$bmUrl = function (array $ov) use ($templateFilter, $search, $dir, $page, $focusTarget) {
$base = [
'template_id' => $templateFilter ?: null,
'q' => $search !== '' ? $search : null,
'order' => $_GET['order'] ?? null,
'dir' => strtolower($dir),
'page' => $page,
'target' => $focusTarget !== '' ? $focusTarget : null,
];
$p = array_filter(array_merge($base, $ov), fn($v) => $v !== null && $v !== '');
return 'bindings_manage.php?' . http_build_query($p);
};
// Filtri WHERE comuni (template/kind/search).
$conds = [];
$params = [];
if ($templateFilter > 0) {
$conds[] = 'b.template_id = ?';
$params[] = $templateFilter;
}
if ($mode === 'detail') {
// ---- DETAIL: valori (json_value -> lims) di un singolo campo (target_key) ----
$conds[] = 'b.target_key = ?';
$params[] = $focusTarget;
if ($search !== '') {
$conds[] = '(b.json_value LIKE ? OR b.lims_value LIKE ?)';
$like = '%' . $search . '%';
array_push($params, $like, $like);
}
$where = 'WHERE ' . implode(' AND ', $conds);
$orderCols = ['json' => 'b.json_value', 'lims' => 'b.lims_value'];
$order = array_key_exists($_GET['order'] ?? '', $orderCols) ? $_GET['order'] : 'json';
$orderSql = $orderCols[$order] . ' ' . $dir;
$countStmt = $pdo->prepare("SELECT COUNT(*) FROM json_lims_binding b $where");
$countStmt->execute($params);
$total = (int) $countStmt->fetchColumn();
$totalPages = max(1, (int) ceil($total / $perPage));
if ($page > $totalPages) $page = $totalPages;
$offset = ($page - 1) * $perPage;
$sql = "SELECT b.id, b.template_id, b.binding_kind, b.mapping_id, b.fixed_field_key, b.field_id,
b.json_value, b.lims_value_id, b.lims_value,
t.name AS template_name, m.field_label
FROM json_lims_binding b
LEFT JOIN excel_templates t ON t.id = b.template_id
LEFT JOIN template_mapping m ON m.id = b.mapping_id
$where
ORDER BY $orderSql, b.json_value ASC
LIMIT $perPage OFFSET $offset";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Intestazione del gruppo (tutte le righe condividono campo/template/kind).
$head = $rows[0] ?? null;
if (!$head) {
// Target vuoto (es. dopo aver svuotato il campo): ricavo i meta dal target_key.
$head = ['binding_kind' => str_starts_with($focusTarget, 'fx:') ? 'fixed' : 'custom'];
}
$headKind = $head['binding_kind'] ?? 'custom';
if ($headKind === 'fixed') {
$headFixedKey = $head['fixed_field_key'] ?? (explode(':', $focusTarget)[2] ?? '');
$headLabel = binding_fixed_label((string) $headFixedKey);
$headTpl = (int) ($head['template_id'] ?? (explode(':', $focusTarget)[1] ?? 0));
} else {
$headLabel = $head['field_label'] ?? ('mapping ' . ($head['mapping_id'] ?? ''));
$headTpl = (int) ($head['template_id'] ?? 0);
}
$headTplName = $headTpl ? ($pdo->query("SELECT name FROM excel_templates WHERE id=" . $headTpl)->fetchColumn() ?: ('#' . $headTpl)) : '';
} else {
// ---- OVERVIEW: un gruppo per campo (target_key) con il conteggio ----
if ($search !== '') {
$conds[] = '(t.name LIKE ? OR m.field_label LIKE ? OR b.fixed_field_key LIKE ?)';
$like = '%' . $search . '%';
array_push($params, $like, $like, $like);
}
$where = $conds ? ('WHERE ' . implode(' AND ', $conds)) : '';
$orderCols = ['template' => 'template_name', 'field' => 'field_label', 'count' => 'cnt'];
$order = array_key_exists($_GET['order'] ?? '', $orderCols) ? $_GET['order'] : 'template';
$orderSql = $orderCols[$order] . ' ' . $dir;
$countStmt = $pdo->prepare("SELECT COUNT(DISTINCT b.target_key)
FROM json_lims_binding b
LEFT JOIN excel_templates t ON t.id = b.template_id
LEFT JOIN template_mapping m ON m.id = b.mapping_id $where");
$countStmt->execute($params);
$total = (int) $countStmt->fetchColumn();
$totalPages = max(1, (int) ceil($total / $perPage));
if ($page > $totalPages) $page = $totalPages;
$offset = ($page - 1) * $perPage;
$sql = "SELECT b.target_key, b.template_id, b.binding_kind, b.mapping_id, b.fixed_field_key,
MAX(t.name) AS template_name, MAX(m.field_label) AS field_label,
COUNT(*) AS cnt, MAX(b.updated_at) AS last_updated
FROM json_lims_binding b
LEFT JOIN excel_templates t ON t.id = b.template_id
LEFT JOIN template_mapping m ON m.id = b.mapping_id
$where
GROUP BY b.target_key, b.template_id, b.binding_kind, b.mapping_id, b.fixed_field_key
ORDER BY $orderSql, template_name ASC
LIMIT $perPage OFFSET $offset";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$groups = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
$sortLink = function (string $col, string $label) use ($bmUrl, $order, $dir) {
$nextDir = ($order === $col && $dir === 'ASC') ? 'desc' : 'asc';
$caret = $order === $col ? ($dir === 'ASC' ? ' &#9650;' : ' &#9660;') : '';
return '<a href="' . htmlspecialchars($bmUrl(['order' => $col, 'dir' => $nextDir, 'page' => 1]))
. '" class="text-decoration-none text-reset">' . $label . $caret . '</a>';
};
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
<style>
.json-value {
font-family: Consolas, Monaco, monospace;
font-weight: 600;
}
td .select2-container {
min-width: 220px;
}
.row-status {
font-size: 12px;
}
.group-row {
cursor: pointer;
}
.group-row:hover {
background-color: #f1f5ff;
}
</style>
<title>Gestione Binding JSON &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">
<h6 class="mb-0">Gestione Binding JSON &rarr; LIMS</h6>
<form method="GET" class="d-flex align-items-center gap-2 flex-wrap mb-0">
<?php if ($mode === 'detail'): ?>
<input type="hidden" name="target" value="<?= htmlspecialchars($focusTarget) ?>">
<?php endif; ?>
<input type="hidden" name="dir" value="<?= htmlspecialchars(strtolower($dir)) ?>">
<input type="text" name="q" class="form-control form-control-sm" style="width:220px;"
placeholder="<?= $mode === 'detail' ? 'Cerca valore...' : 'Cerca campo/template...' ?>"
value="<?= htmlspecialchars($search) ?>">
<?php if ($mode === 'overview'): ?>
<select name="template_id" class="form-select form-select-sm" style="width:auto;" onchange="this.form.submit()">
<option value="0">Tutti i template</option>
<?php foreach ($templates as $t): ?>
<option value="<?= (int) $t['id'] ?>" <?= $templateFilter === (int) $t['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($t['name']) ?>
</option>
<?php endforeach; ?>
</select>
<?php endif; ?>
<button type="submit" class="btn btn-sm btn-primary">Cerca</button>
</form>
</div>
</div>
<div class="card-body">
<div id="globalError" class="alert alert-danger" style="display:none;"></div>
<?php if ($mode === 'detail'): ?>
<div class="d-flex justify-content-between align-items-center mb-2 flex-wrap gap-2">
<div>
<a href="<?= htmlspecialchars($bmUrl(['target' => null, 'page' => 1, 'order' => null])) ?>" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Tutti i campi
</a>
<span class="ms-2">
<strong><?= htmlspecialchars($headLabel) ?></strong>
<?php if ($headKind === 'fixed'): ?><span class="badge bg-light text-dark border">fixed</span><?php endif; ?>
<span class="text-muted">&middot; <?= htmlspecialchars((string) $headTplName) ?></span>
</span>
</div>
<span class="small text-muted"><?= $total ?> valore/i &middot; pagina <?= $page ?>/<?= $totalPages ?></span>
</div>
<div class="table-responsive">
<table class="table table-striped table-bordered align-middle" id="bindingsTable">
<thead>
<tr>
<th><?= $sortLink('json', 'Valore JSON') ?></th>
<th><?= $sortLink('lims', 'Valore LIMS') ?></th>
<th style="width:170px;">Azioni</th>
</tr>
</thead>
<tbody>
<?php if (empty($rows)): ?>
<tr class="no-data-row">
<td colspan="3" class="text-center text-muted">Nessun valore per questo campo.</td>
</tr>
<?php else: ?>
<?php foreach ($rows as $b): ?>
<?php $bDangling = ($b['binding_kind'] === 'custom' && $b['field_label'] === null); ?>
<tr data-id="<?= (int) $b['id'] ?>"
data-kind="<?= $b['binding_kind'] ?>"
data-mapping-id="<?= (int) $b['mapping_id'] ?>"
data-field-id="<?= (int) $b['field_id'] ?>"
data-fixed-key="<?= htmlspecialchars((string) $b['fixed_field_key'], ENT_QUOTES) ?>"
data-template-id="<?= (int) $b['template_id'] ?>"
data-json-value="<?= htmlspecialchars($b['json_value'], ENT_QUOTES) ?>">
<td class="json-value"><?= htmlspecialchars($b['json_value']) ?></td>
<td>
<select class="form-select binding-select" <?= $bDangling ? 'disabled' : '' ?>>
<?php if ($b['lims_value_id']): ?>
<option value="<?= (int) $b['lims_value_id'] ?>" selected><?= htmlspecialchars($b['lims_value']) ?></option>
<?php endif; ?>
</select>
<span class="row-status text-muted"></span>
</td>
<td>
<button type="button" class="btn btn-sm btn-success save-binding-btn" disabled>
<i class="fas fa-save"></i> Salva
</button>
<button type="button" class="btn btn-sm btn-outline-danger delete-binding-btn">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="alert alert-secondary small">
Scegli un campo per gestirne i valori. Le modifiche valgono per le <strong>importazioni future</strong>.
</div>
<div class="small text-muted mb-2"><?= $total ?> camp<?= $total === 1 ? 'o' : 'i' ?> con binding &middot; pagina <?= $page ?>/<?= $totalPages ?></div>
<div class="table-responsive">
<table class="table table-striped table-bordered align-middle">
<thead>
<tr>
<th><?= $sortLink('template', 'Template') ?></th>
<th><?= $sortLink('field', 'Campo') ?></th>
<th style="width:120px;"><?= $sortLink('count', '# Binding') ?></th>
<th style="width:90px;"></th>
</tr>
</thead>
<tbody>
<?php if (empty($groups)): ?>
<tr>
<td colspan="4" class="text-center text-muted">Nessun binding presente.</td>
</tr>
<?php else: ?>
<?php foreach ($groups as $g): ?>
<?php
$gKind = $g['binding_kind'];
$gDangling = ($gKind === 'custom' && $g['field_label'] === null);
$gLabel = $gKind === 'fixed'
? binding_fixed_label((string) $g['fixed_field_key'])
: ($g['field_label'] ?? ('mapping ' . $g['mapping_id']));
$gUrl = $bmUrl(['target' => $g['target_key'], 'page' => 1, 'order' => null, 'q' => null]);
?>
<tr class="group-row" onclick="window.location.href='<?= htmlspecialchars($gUrl) ?>'">
<td><?= htmlspecialchars($g['template_name'] ?? ('#' . $g['template_id'])) ?></td>
<td>
<?= htmlspecialchars($gLabel) ?>
<?php if ($gKind === 'fixed'): ?><span class="badge bg-light text-dark border">fixed</span><?php endif; ?>
<?php if ($gDangling): ?><span class="badge bg-warning text-dark" title="Il campo mappato non esiste piu'">campo rimosso</span><?php endif; ?>
</td>
<td><span class="badge bg-primary"><?= (int) $g['cnt'] ?></span></td>
<td><a href="<?= htmlspecialchars($gUrl) ?>" class="btn btn-sm btn-outline-primary">Apri</a></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php if ($totalPages > 1): ?>
<nav class="d-flex justify-content-center mt-2">
<ul class="pagination pagination-sm mb-0">
<li class="page-item <?= $page <= 1 ? 'disabled' : '' ?>">
<a class="page-link" href="<?= htmlspecialchars($bmUrl(['page' => max(1, $page - 1)])) ?>">&laquo;</a>
</li>
<li class="page-item disabled"><span class="page-link"><?= $page ?> / <?= $totalPages ?></span></li>
<li class="page-item <?= $page >= $totalPages ? 'disabled' : '' ?>">
<a class="page-link" href="<?= htmlspecialchars($bmUrl(['page' => min($totalPages, $page + 1)])) ?>">&raquo;</a>
</li>
</ul>
</nav>
<?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 $globalError = $('#globalError');
// Select2 solo nelle righe valore della pagina corrente (max 50), saltando le disabilitate.
$('.binding-select').not(':disabled').each(function() {
const $row = $(this).closest('tr');
const isFixed = $row.data('kind') === 'fixed';
const initialVal = $(this).val();
$(this).select2({
width: '220px',
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
});
$(this).data('original-val', initialVal);
});
$('.binding-select').on('change', function() {
const $row = $(this).closest('tr');
const changed = String($(this).val()) !== String($(this).data('original-val'));
$row.find('.save-binding-btn').prop('disabled', !changed);
});
$('.save-binding-btn').on('click', function() {
const $row = $(this).closest('tr');
const $select = $row.find('.binding-select');
const selectedData = $select.select2('data')[0] || {};
const $status = $row.find('.row-status');
const $btn = $(this);
$globalError.hide();
$btn.prop('disabled', true);
$status.text('Salvataggio...').removeClass('text-success text-danger').addClass('text-muted');
const isFixed = $row.data('kind') === 'fixed';
const targetFields = isFixed
? { kind: 'fixed', fixed_field_key: $row.data('fixed-key') }
: { kind: 'custom', mapping_id: $row.data('mapping-id'), field_id: $row.data('field-id') };
$.post('save_binding.php', {
...targetFields,
template_id: $row.data('template-id'),
json_value: String($row.data('json-value')),
lims_value_id: $select.val(),
lims_value: selectedData.text || ''
}).then(resp => {
if (resp && resp.success) {
$status.text('Salvato').removeClass('text-muted').addClass('text-success');
$select.data('original-val', $select.val());
} else {
throw new Error((resp && resp.error) || 'Errore salvataggio');
}
}).catch(err => {
$status.text('Errore').removeClass('text-muted').addClass('text-danger');
$globalError.text(err.message || 'Errore durante il salvataggio.').show();
$btn.prop('disabled', false);
});
});
$('.delete-binding-btn').on('click', function() {
if (!confirm('Eliminare questo binding?')) return;
const $row = $(this).closest('tr');
const $btn = $(this);
$globalError.hide();
$btn.prop('disabled', true);
$.post('delete_binding.php', {
id: $row.data('id')
}).then(resp => {
if (resp && resp.success) {
$row.fadeOut(200, () => $row.remove());
} else {
throw new Error((resp && resp.error) || 'Errore eliminazione');
}
}).catch(err => {
$globalError.text(err.message || 'Errore durante l\'eliminazione.').show();
$btn.prop('disabled', false);
});
});
});
</script>
</body>
</html>
@@ -257,35 +257,56 @@ class VisualLimsApiClient
}
/**
* Recupera contenuto binario - Adattato per https://bvcpsitaly-elims.com/limsapi
* Get raw/binary content from VisualLims API.
* Used for PDF downloads from MediaFile/DownloadStream.
*/
public function getRaw($endpoint)
{
$token = $this->getToken();
// IMPORTANTE: usa /odata/ e NON /api/odata/
$url = "{$this->baseUrl}/odata/{$endpoint}";
/*
* Normal JSON OData calls use:
* {$this->baseUrl}/api/odata/...
*
* Media file downloads use:
* {$this->baseUrl}/api/MediaFile/DownloadStream...
*/
$url = rtrim($this->baseUrl, '/') . '/api/' . ltrim($endpoint, '/');
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer {$token}",
"Accept: */*"
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer {$token}",
"Accept: application/pdf,*/*"
],
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 60
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
$curlError = curl_error($ch);
curl_close($ch);
if ($response === false) {
throw new Exception("Errore cURL: " . $curl_error);
throw new Exception("Errore cURL download raw: " . $curlError);
}
if ($http_code !== 200) {
throw new Exception("HTTP {$http_code} su endpoint: " . $url);
if ($httpCode < 200 || $httpCode >= 300) {
throw new Exception(
"Errore HTTP {$httpCode} durante download raw. Content-Type: {$contentType}. Response: " .
substr($response, 0, 500)
);
}
if (empty($response)) {
throw new Exception("Risposta vuota dal download raw.");
}
return $response;
+448
View File
@@ -0,0 +1,448 @@
<?php
// Helpers for JSON -> LIMS value bindings (table json_lims_binding).
// Supports two kinds: 'custom' (template_mapping list fields, value -> import_data_details)
// and 'fixed' (template_fixed_mapping list fields, id -> datadb column).
// ---------------------------------------------------------------------------
// Fixed-field metadata
// ---------------------------------------------------------------------------
function binding_fixed_alias_map(): array
{
return [
'ClienteResponsabile' => 'cliente_responsabile_id',
'ClienteFornitore' => 'cliente_fornitore_id',
'ClienteAnalisi' => 'clienteAnalisi',
'MoltiplicatorePrezzo' => 'moltiplicatore_prezzo_id',
'AnagraficaCertestObject' => 'anagrafica_certest_object_id',
'AnagraficaCertestService' => 'anagrafica_certest_service_id',
'ConsegnaRichiesta' => 'consegna_richiesta',
];
}
// Fixed-field keys that are LIMS list dropdowns (bindable). ConsegnaRichiesta is a date.
function binding_fixed_is_list(string $key): bool
{
return in_array($key, [
'ClienteResponsabile',
'ClienteFornitore',
'ClienteAnalisi',
'MoltiplicatorePrezzo',
'AnagraficaCertestObject',
'AnagraficaCertestService',
], true);
}
function binding_fixed_column(string $key): ?string
{
return binding_fixed_alias_map()[$key] ?? null;
}
// Auto-match only the small global lists; the client-based ones are huge / client-specific.
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' => 'Cliente Analisi',
];
return $labels[$key] ?? $key;
}
// ---------------------------------------------------------------------------
// Bindable check (custom mapping row)
// ---------------------------------------------------------------------------
function binding_is_list_field(array $mapping): bool
{
$hasList = (int) ($mapping['has_list'] ?? 0) === 1;
$isMultiChoice = strcasecmp(trim((string) ($mapping['data_type'] ?? '')), 'SceltaMultipla') === 0;
return $hasList || $isMultiChoice;
}
// ---------------------------------------------------------------------------
// Target keys + lookup / upsert
// ---------------------------------------------------------------------------
function binding_target_custom(int $mappingId): string
{
return 'cf:' . $mappingId;
}
function binding_target_fixed(int $templateId, string $fixedKey): string
{
return 'fx:' . $templateId . ':' . $fixedKey;
}
function binding_lookup_target(PDO $pdo, string $targetKey, string $jsonValue): ?array
{
$stmt = $pdo->prepare(
"SELECT id, lims_value_id, lims_value
FROM json_lims_binding
WHERE target_key = ? AND json_value = ?
LIMIT 1"
);
$stmt->execute([$targetKey, $jsonValue]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
function binding_lookup(PDO $pdo, int $mappingId, string $jsonValue): ?array
{
return binding_lookup_target($pdo, binding_target_custom($mappingId), $jsonValue);
}
function binding_lookup_fixed(PDO $pdo, int $templateId, string $fixedKey, string $jsonValue): ?array
{
return binding_lookup_target($pdo, binding_target_fixed($templateId, $fixedKey), $jsonValue);
}
function binding_upsert_row(
PDO $pdo,
string $kind,
int $templateId,
?int $mappingId,
?string $fixedFieldKey,
string $targetKey,
int $fieldId,
string $jsonValue,
int $limsValueId,
string $limsValue,
?int $createdBy
): void {
$stmt = $pdo->prepare(
"INSERT INTO json_lims_binding
(template_id, binding_kind, mapping_id, fixed_field_key, target_key, field_id, json_value, lims_value_id, lims_value, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
lims_value_id = VALUES(lims_value_id),
lims_value = VALUES(lims_value),
field_id = VALUES(field_id),
template_id = VALUES(template_id),
binding_kind = VALUES(binding_kind),
mapping_id = VALUES(mapping_id),
fixed_field_key = VALUES(fixed_field_key)"
);
$stmt->execute([
$templateId, $kind, $mappingId, $fixedFieldKey, $targetKey,
$fieldId, $jsonValue, $limsValueId, $limsValue, $createdBy,
]);
}
function binding_upsert(
PDO $pdo,
int $templateId,
int $mappingId,
int $fieldId,
string $jsonValue,
int $limsValueId,
string $limsValue,
?int $createdBy
): void {
binding_upsert_row(
$pdo, 'custom', $templateId, $mappingId, null,
binding_target_custom($mappingId), $fieldId, $jsonValue, $limsValueId, $limsValue, $createdBy
);
}
function binding_upsert_fixed(
PDO $pdo,
int $templateId,
string $fixedKey,
string $jsonValue,
int $limsValueId,
string $limsValue,
?int $createdBy
): void {
binding_upsert_row(
$pdo, 'fixed', $templateId, null, $fixedKey,
binding_target_fixed($templateId, $fixedKey), 0, $jsonValue, $limsValueId, $limsValue, $createdBy
);
}
function binding_delete_target(PDO $pdo, string $targetKey, string $jsonValue): int
{
$stmt = $pdo->prepare("DELETE FROM json_lims_binding WHERE target_key = ? AND json_value = ?");
$stmt->execute([$targetKey, $jsonValue]);
return $stmt->rowCount();
}
// ---------------------------------------------------------------------------
// Custom-field LIMS values (cache/customfield_{id}.json) + auto-match
// ---------------------------------------------------------------------------
function binding_get_lims_values(int $fieldId): array
{
static $memo = [];
if ($fieldId <= 0) {
return [];
}
if (array_key_exists($fieldId, $memo)) {
return $memo[$fieldId];
}
$cacheDir = dirname(__DIR__) . '/cache';
$cacheFile = $cacheDir . '/customfield_' . $fieldId . '.json';
try {
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
$values = json_decode(file_get_contents($cacheFile), true);
} else {
require_once __DIR__ . '/VisualLimsApiClient.class.php';
$api = VisualLimsApiClient::getInstance();
$data = $api->get("CustomField($fieldId)?\$expand=CustomFieldsValues");
$values = $data['CustomFieldsValues'] ?? [];
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0777, true);
}
file_put_contents($cacheFile, json_encode($values));
}
} catch (Throwable $e) {
error_log("binding_get_lims_values failed for field $fieldId: " . $e->getMessage());
$values = [];
}
if (!is_array($values)) {
$values = [];
}
return $memo[$fieldId] = $values;
}
// Exactly one case-insensitive match by Valore -> that value, otherwise null.
function binding_auto_match(array $limsValues, string $jsonValue): ?array
{
$needle = mb_strtolower(trim($jsonValue));
if ($needle === '') {
return null;
}
$matches = [];
foreach ($limsValues as $v) {
$valore = (string) ($v['Valore'] ?? '');
if (mb_strtolower(trim($valore)) === $needle) {
$matches[] = $v;
}
}
return count($matches) === 1 ? $matches[0] : null;
}
// ---------------------------------------------------------------------------
// Fixed-field LIMS values (per-field source) + auto-match
// ---------------------------------------------------------------------------
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($client['Nominativo'] ?? '');
$id = trim((string) ($client['IdCliente'] ?? ''));
$code = trim((string) ($client['CodiceCliente'] ?? ''));
$parts = explode('_', $code);
$suffix = trim($parts[1] ?? '');
if ($suffix === '' && $code !== '') {
$suffix = substr($code, 0, 1);
}
if ($suffix === '') {
$suffix = '--';
}
return $name . ' - ' . $suffix . ' (ID: ' . $id . ')';
}
// Fetch an OData payload with a 1-hour file cache.
function binding_cached_get($api, string $cacheFile, string $endpoint): array
{
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
$data = json_decode(file_get_contents($cacheFile), true);
} else {
$data = $api->get($endpoint);
$dir = dirname($cacheFile);
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
file_put_contents($cacheFile, json_encode($data));
}
return is_array($data) ? $data : [];
}
// Normalized LIMS value list for a fixed field: [['id' => int, 'text' => string], ...].
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'] ?? ''));
$text = ($code !== '' ? $code . ' - ' : '') . (string) ($r['NomeAnagrafica'] ?? '');
$out[] = ['id' => (int) ($r['IdAnagrafica'] ?? 0), 'text' => $text];
}
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':
$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] = $out;
}
// Exactly one case-insensitive match by text on a [{id,text}] list, otherwise null.
function binding_auto_match_fixed(array $values, string $jsonValue): ?array
{
$needle = mb_strtolower(trim($jsonValue));
if ($needle === '') {
return null;
}
$matches = [];
foreach ($values as $v) {
if (mb_strtolower(trim((string) ($v['text'] ?? ''))) === $needle) {
$matches[] = $v;
}
}
return count($matches) === 1 ? $matches[0] : null;
}
// ---------------------------------------------------------------------------
// JSON node -> column matching (shared with import_insert custom logic)
// ---------------------------------------------------------------------------
function binding_find_column_index(string $sourceColumn, array $columns): int
{
$sourceColumn = trim($sourceColumn);
if ($sourceColumn === '') {
return -1;
}
$columnsTrimmed = array_map('trim', $columns);
$candidates = [
$sourceColumn,
preg_replace('/^data\[\]\./', '', $sourceColumn),
preg_replace('/^data\.0\./', '', $sourceColumn),
str_replace('data[].', 'data.0.', $sourceColumn),
str_replace('data.0.', 'data[].', $sourceColumn),
];
$candidates = array_values(array_unique(array_filter(array_map('trim', $candidates), function ($v) {
return $v !== '';
})));
foreach ($candidates as $c) {
$i = array_search($c, $columnsTrimmed, true);
if ($i !== false) {
return (int) $i;
}
}
return -1;
}
// ---------------------------------------------------------------------------
// Apply resolved values
// ---------------------------------------------------------------------------
// Custom: write the resolved LIMS text into import_data_details for the given datadb ids.
function binding_apply_to_details(
PDO $pdo,
int $mappingId,
string $limsValue,
array $datadbIds
): int {
$datadbIds = array_values(array_filter(array_map('intval', $datadbIds)));
if (empty($datadbIds)) {
return 0;
}
$placeholders = implode(',', array_fill(0, count($datadbIds), '?'));
$sql = "UPDATE import_data_details
SET field_value = ?
WHERE mapping_id = ?
AND id IN ($placeholders)";
$params = array_merge([$limsValue, $mappingId], $datadbIds);
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->rowCount();
}
// Fixed: write the resolved LIMS id into a whitelisted datadb column for the given ids.
// $limsValueId null clears the column.
function binding_apply_to_datadb(
PDO $pdo,
string $column,
?int $limsValueId,
array $datadbIds
): int {
if (!in_array($column, array_values(binding_fixed_alias_map()), true)) {
throw new InvalidArgumentException("Invalid fixed-field column: $column");
}
$datadbIds = array_values(array_filter(array_map('intval', $datadbIds)));
if (empty($datadbIds)) {
return 0;
}
$placeholders = implode(',', array_fill(0, count($datadbIds), '?'));
$sql = "UPDATE datadb SET `$column` = ? WHERE iddatadb IN ($placeholders)";
$stmt = $pdo->prepare($sql);
$stmt->execute(array_merge([$limsValueId], $datadbIds));
return $stmt->rowCount();
}
@@ -1,152 +0,0 @@
<?php
/**
* debug_odata_metadata.php TEMPORANEO, da rimuovere dopo il test.
* Scopo: leggere $metadata (XML standard OData) per vedere:
* - tutti gli EntitySet disponibili (non solo "Cliente")
* - tutti i campi (Property) definiti per l'EntityType Cliente
* Questo bypassa qualunque vista/proiezione ristretta lato server
* applicata sull'endpoint "Cliente" normale.
*/
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
include dirname(__DIR__) . '/../extra/auth.php';
if (!Auth::check()) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
use Dotenv\Dotenv;
header('Content-Type: text/plain; charset=utf-8');
ini_set('display_errors', '1');
error_reporting(E_ALL);
$dotenv = Dotenv::createImmutable(dirname(__DIR__, 2));
$dotenv->load();
$baseUrl = $_ENV['API_BASE_URL'];
$username = $_ENV['API_USERNAME'];
$password = $_ENV['API_PASSWORD'];
$scriptStart = microtime(true);
// ── 1. Autenticazione (stessa logica della classe originale) ──────────────
$ch = curl_init("{$baseUrl}/api/authentication/authenticate");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'Username' => $username,
'Password' => $password
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Accept: application/json'
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$authResponse = curl_exec($ch);
$authHttpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($authHttpCode !== 200) {
echo "AUTH FAILED: HTTP {$authHttpCode}\n{$authResponse}\n";
exit;
}
$tokenData = json_decode($authResponse, true);
$token = null;
if (is_array($tokenData) && isset($tokenData['token'])) {
$token = $tokenData['token'];
} elseif (is_string($tokenData)) {
$token = trim($tokenData, '"');
} else {
$token = trim($authResponse, '"');
}
if (empty($token)) {
echo "TOKEN NOT RECEIVED\n{$authResponse}\n";
exit;
}
$authElapsed = microtime(true) - $scriptStart;
// ── 2. Richiesta $metadata (XML, non JSON) ─────────────────────────────────
// Cache-buster nell'URL: alcuni server/reverse-proxy potrebbero cachare per URL esatto.
$cacheBuster = uniqid('cb_', true);
$metadataUrl = "{$baseUrl}/api/odata/\$metadata?_cb={$cacheBuster}";
$metadataStart = microtime(true);
$ch = curl_init($metadataUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer {$token}",
"Accept: application/xml"
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_HEADER, true); // includi header risposta per ispezione
$rawResponse = curl_exec($ch);
$curlInfo = curl_getinfo($ch);
$httpCode = $curlInfo['http_code'];
$curlTotalTime = $curlInfo['total_time'];
$curlError = curl_error($ch);
$headerSize = $curlInfo['header_size'];
curl_close($ch);
$metadataElapsed = microtime(true) - $metadataStart;
if ($rawResponse === false || $httpCode !== 200) {
echo "METADATA REQUEST FAILED: HTTP {$httpCode}\nError: {$curlError}\n{$rawResponse}\n";
exit;
}
$responseHeaders = substr($rawResponse, 0, $headerSize);
$xml = substr($rawResponse, $headerSize);
echo "==================== TIMING (prova che NON è cache) ====================\n";
echo "Tempo autenticazione: " . round($authElapsed * 1000, 1) . " ms\n";
echo "Tempo chiamata \$metadata (curl total_time): " . round($curlTotalTime * 1000, 1) . " ms\n";
echo "Tempo chiamata \$metadata (wall clock PHP): " . round($metadataElapsed * 1000, 1) . " ms\n";
echo "Cache-buster usato: {$cacheBuster}\n";
echo "Dimensione XML ricevuto: " . strlen($xml) . " bytes\n\n";
echo "==================== HEADER RISPOSTA (controlla se c'è Cache-Control/Age/X-Cache) ====================\n";
echo $responseHeaders . "\n";
// ── 3. Estrai tutti gli EntitySet disponibili ──────────────────────────────
echo "==================== ENTITY SETS DISPONIBILI ====================\n";
if (preg_match_all('/<EntitySet Name="([^"]+)"/', $xml, $matches)) {
foreach ($matches[1] as $name) {
echo "- {$name}\n";
}
} else {
echo "(nessun EntitySet trovato — controlla il formato XML grezzo sotto)\n";
}
// ── 4. Estrai tutte le Property dell'EntityType "Cliente" (e simili) ──────
echo "\n==================== CAMPI PER ENTITYTYPE CHE CONTENGONO 'Client' ====================\n";
if (preg_match_all('/<EntityType Name="([^"]*[Cc]lient[^"]*)"[^>]*>(.*?)<\/EntityType>/s', $xml, $entityMatches, PREG_SET_ORDER)) {
foreach ($entityMatches as $entity) {
$entityName = $entity[1];
$entityBody = $entity[2];
echo "\n--- EntityType: {$entityName} ---\n";
if (preg_match_all('/<Property Name="([^"]+)" Type="([^"]+)"/', $entityBody, $propMatches, PREG_SET_ORDER)) {
foreach ($propMatches as $prop) {
echo " {$prop[1]} ({$prop[2]})\n";
}
}
}
} else {
echo "(nessun EntityType con 'Client' nel nome trovato)\n";
}
// ── 5. Salva l'XML grezzo completo per ispezione manuale, se serve ────────
file_put_contents(__DIR__ . '/metadata_debug.xml', $xml);
echo "\n\nXML completo salvato in: metadata_debug.xml (rimuovilo dopo il test)\n";
+1 -68
View File
@@ -10,11 +10,6 @@ $data = json_decode(file_get_contents('php://input'), true);
$sourceIddatadb = isset($data['source_iddatadb']) ? (int)$data['source_iddatadb'] : 0;
$targetList = $data['target_iddatadb_list'] ?? [];
// Default true: se il client non manda il parametro, cloniamo comunque le note
// (comportamento di default richiesto, coerente con la checkbox pre-spuntata)
$cloneNotes = array_key_exists('clone_notes', $data) ? !empty($data['clone_notes']) : true;
$cloneAnalyses = !empty($data['clone_analyses']);
$overwrite = !empty($data['overwrite']);
$targetIds = array_values(array_unique(array_filter(array_map('intval', (array)$targetList), function ($v) use ($sourceIddatadb) {
return $v > 0 && $v !== $sourceIddatadb;
@@ -67,23 +62,6 @@ try {
ORDER BY id ASC
");
$stmtLoadAnalyses = $pdo->prepare("
SELECT idmatrice, analysis_recordkey, analysis_name, analysis_method,
analysis_level, is_web_selectable, is_accredited
FROM identification_parts_analyses
WHERE part_id = ?
ORDER BY id ASC
");
$stmtInsertAnalysis = $pdo->prepare("
INSERT INTO identification_parts_analyses
(part_id, iddatadb, idmatrice, analysis_recordkey, analysis_name, analysis_method,
analysis_level, is_web_selectable, is_accredited, created_at, updated_at)
VALUES
(:part_id, :iddatadb, :idmatrice, :analysis_recordkey, :analysis_name, :analysis_method,
:analysis_level, :is_web_selectable, :is_accredited, NOW(), NOW())
");
$stmtInsertCF = $pdo->prepare("
INSERT INTO identification_parts_customfields
(part_id, field_id, value_id, value_text, created_at, updated_at)
@@ -93,29 +71,6 @@ try {
$details = [];
$totalClonedParts = 0;
$totalClonedAnalyses = 0;
$totalDeletedParts = 0;
// 2b. Se overwrite: elimina le parti esistenti dei target (e i loro figli)
if ($overwrite) {
$stmtSelectExisting = $pdo->prepare("SELECT id FROM identification_parts WHERE iddatadb = ?");
$stmtDeleteCFByPart = $pdo->prepare("DELETE FROM identification_parts_customfields WHERE part_id = ?");
$stmtDeleteAnByPart = $pdo->prepare("DELETE FROM identification_parts_analyses WHERE part_id = ?");
$stmtDeletePart = $pdo->prepare("DELETE FROM identification_parts WHERE id = ?");
foreach ($targetIds as $targetIddatadb) {
$stmtSelectExisting->execute([$targetIddatadb]);
$existingIds = $stmtSelectExisting->fetchAll(PDO::FETCH_COLUMN);
foreach ($existingIds as $existingPartId) {
$existingPartId = (int)$existingPartId;
$stmtDeleteCFByPart->execute([$existingPartId]);
$stmtDeleteAnByPart->execute([$existingPartId]);
$stmtDeletePart->execute([$existingPartId]);
$totalDeletedParts++;
}
}
}
// 3. Clone source parts to each target record
foreach ($targetIds as $targetIddatadb) {
@@ -128,7 +83,7 @@ try {
':part_description' => $part['part_description'],
':mix' => $part['mix'] ?? 'N',
':idmatrice' => $part['idmatrice'] !== '' ? $part['idmatrice'] : null,
':note' => $cloneNotes ? ($part['note'] ?? null) : null,
':note' => $part['note'] ?? null,
':dateexpiry' => $part['dateexpiry'] ?? null,
]);
@@ -147,27 +102,6 @@ try {
]);
}
// Clone linked analyses, if requested
if ($cloneAnalyses) {
$stmtLoadAnalyses->execute([(int)$part['id']]);
$analyses = $stmtLoadAnalyses->fetchAll(PDO::FETCH_ASSOC);
foreach ($analyses as $an) {
$stmtInsertAnalysis->execute([
':part_id' => $newPartId,
':iddatadb' => $targetIddatadb,
':idmatrice' => ($an['idmatrice'] !== null && $an['idmatrice'] !== '') ? (int)$an['idmatrice'] : null,
':analysis_recordkey' => $an['analysis_recordkey'],
':analysis_name' => $an['analysis_name'] ?? null,
':analysis_method' => $an['analysis_method'] ?? null,
':analysis_level' => ($an['analysis_level'] !== null && $an['analysis_level'] !== '') ? (int)$an['analysis_level'] : null,
':is_web_selectable' => (int)($an['is_web_selectable'] ?? 0),
':is_accredited' => (int)($an['is_accredited'] ?? 0),
]);
$totalClonedAnalyses++;
}
}
$clonedCountForTarget++;
$totalClonedParts++;
}
@@ -185,7 +119,6 @@ try {
'source_iddatadb' => $sourceIddatadb,
'cloned_targets' => count($targetIds),
'total_cloned_parts' => $totalClonedParts,
'total_cloned_analyses' => $totalClonedAnalyses,
'details' => $details
]);
} catch (Throwable $e) {
+22 -18
View File
@@ -23,17 +23,19 @@ if ((int)$stmt->fetchColumn() > 0) {
}
/**
* FIXED FIELDS STANDARD
* FIXED FIELDS STANDARD (no UI selection)
* is_manual always 1
* is_visible_import default 1
*/
$fixedFields = [
['ClienteResponsabile', 'INT'],
['ClienteFornitore', 'INT'],
['ClienteAnalisi', 'INT'],
['ClienteFatturazione', 'INT'], // ← NUOVO
['MoltiplicatorePrezzo', 'INT'],
['AnagraficaCertestObject', 'INT'],
// fixed_field_key, data_type
['ClienteResponsabile', 'INT'],
['ClienteFornitore', 'INT'],
['ClienteAnalisi', 'INT'],
['MoltiplicatorePrezzo', 'INT'],
['AnagraficaCertestObject', 'INT'],
['AnagraficaCertestService', 'INT'],
['ConsegnaRichiesta', 'DATE'],
['ConsegnaRichiesta', 'DATE'],
];
@@ -41,11 +43,12 @@ try {
$pdo->beginTransaction();
$ins = $pdo->prepare("
INSERT INTO template_fixed_mapping
(template_id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import)
VALUES
(:template_id, :fixed_field_key, 1, :data_type, 1, NULL, 1)
");
INSERT INTO template_fixed_mapping
(template_id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import)
VALUES
(:template_id, :fixed_field_key, 1, :data_type, 1, NULL, 1)
");
foreach ($fixedFields as $f) {
$ins->execute([
@@ -55,15 +58,16 @@ try {
]);
}
$pdo->commit();
// Return rows (for client render)
$stmt = $pdo->prepare("
SELECT id, template_id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import
FROM template_fixed_mapping
WHERE template_id = ?
ORDER BY id ASC
");
SELECT id, template_id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import
FROM template_fixed_mapping
WHERE template_id = ?
ORDER BY id ASC
");
$stmt->execute([$templateId]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-152
View File
@@ -1,152 +0,0 @@
<?php
/**
* debug_odata_metadata.php TEMPORANEO, da rimuovere dopo il test.
* Scopo: leggere $metadata (XML standard OData) per vedere:
* - tutti gli EntitySet disponibili (non solo "Cliente")
* - tutti i campi (Property) definiti per l'EntityType Cliente
* Questo bypassa qualunque vista/proiezione ristretta lato server
* applicata sull'endpoint "Cliente" normale.
*/
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
include dirname(__DIR__) . '/../extra/auth.php';
if (!Auth::check()) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
use Dotenv\Dotenv;
header('Content-Type: text/plain; charset=utf-8');
ini_set('display_errors', '1');
error_reporting(E_ALL);
$dotenv = Dotenv::createImmutable(dirname(__DIR__, 2));
$dotenv->load();
$baseUrl = $_ENV['API_BASE_URL'];
$username = $_ENV['API_USERNAME'];
$password = $_ENV['API_PASSWORD'];
$scriptStart = microtime(true);
// ── 1. Autenticazione (stessa logica della classe originale) ──────────────
$ch = curl_init("{$baseUrl}/api/authentication/authenticate");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'Username' => $username,
'Password' => $password
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Accept: application/json'
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$authResponse = curl_exec($ch);
$authHttpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($authHttpCode !== 200) {
echo "AUTH FAILED: HTTP {$authHttpCode}\n{$authResponse}\n";
exit;
}
$tokenData = json_decode($authResponse, true);
$token = null;
if (is_array($tokenData) && isset($tokenData['token'])) {
$token = $tokenData['token'];
} elseif (is_string($tokenData)) {
$token = trim($tokenData, '"');
} else {
$token = trim($authResponse, '"');
}
if (empty($token)) {
echo "TOKEN NOT RECEIVED\n{$authResponse}\n";
exit;
}
$authElapsed = microtime(true) - $scriptStart;
// ── 2. Richiesta $metadata (XML, non JSON) ─────────────────────────────────
// Cache-buster nell'URL: alcuni server/reverse-proxy potrebbero cachare per URL esatto.
$cacheBuster = uniqid('cb_', true);
$metadataUrl = "{$baseUrl}/api/odata/\$metadata?_cb={$cacheBuster}";
$metadataStart = microtime(true);
$ch = curl_init($metadataUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer {$token}",
"Accept: application/xml"
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_HEADER, true); // includi header risposta per ispezione
$rawResponse = curl_exec($ch);
$curlInfo = curl_getinfo($ch);
$httpCode = $curlInfo['http_code'];
$curlTotalTime = $curlInfo['total_time'];
$curlError = curl_error($ch);
$headerSize = $curlInfo['header_size'];
curl_close($ch);
$metadataElapsed = microtime(true) - $metadataStart;
if ($rawResponse === false || $httpCode !== 200) {
echo "METADATA REQUEST FAILED: HTTP {$httpCode}\nError: {$curlError}\n{$rawResponse}\n";
exit;
}
$responseHeaders = substr($rawResponse, 0, $headerSize);
$xml = substr($rawResponse, $headerSize);
echo "==================== TIMING (prova che NON è cache) ====================\n";
echo "Tempo autenticazione: " . round($authElapsed * 1000, 1) . " ms\n";
echo "Tempo chiamata \$metadata (curl total_time): " . round($curlTotalTime * 1000, 1) . " ms\n";
echo "Tempo chiamata \$metadata (wall clock PHP): " . round($metadataElapsed * 1000, 1) . " ms\n";
echo "Cache-buster usato: {$cacheBuster}\n";
echo "Dimensione XML ricevuto: " . strlen($xml) . " bytes\n\n";
echo "==================== HEADER RISPOSTA (controlla se c'è Cache-Control/Age/X-Cache) ====================\n";
echo $responseHeaders . "\n";
// ── 3. Estrai tutti gli EntitySet disponibili ──────────────────────────────
echo "==================== ENTITY SETS DISPONIBILI ====================\n";
if (preg_match_all('/<EntitySet Name="([^"]+)"/', $xml, $matches)) {
foreach ($matches[1] as $name) {
echo "- {$name}\n";
}
} else {
echo "(nessun EntitySet trovato — controlla il formato XML grezzo sotto)\n";
}
// ── 4. Estrai tutte le Property dell'EntityType "Cliente" (e simili) ──────
echo "\n==================== CAMPI PER ENTITYTYPE CHE CONTENGONO 'Client' ====================\n";
if (preg_match_all('/<EntityType Name="([^"]*[Cc]lient[^"]*)"[^>]*>(.*?)<\/EntityType>/s', $xml, $entityMatches, PREG_SET_ORDER)) {
foreach ($entityMatches as $entity) {
$entityName = $entity[1];
$entityBody = $entity[2];
echo "\n--- EntityType: {$entityName} ---\n";
if (preg_match_all('/<Property Name="([^"]+)" Type="([^"]+)"/', $entityBody, $propMatches, PREG_SET_ORDER)) {
foreach ($propMatches as $prop) {
echo " {$prop[1]} ({$prop[2]})\n";
}
}
}
} else {
echo "(nessun EntityType con 'Client' nel nome trovato)\n";
}
// ── 5. Salva l'XML grezzo completo per ispezione manuale, se serve ────────
file_put_contents(__DIR__ . '/metadata_debug.xml', $xml);
echo "\n\nXML completo salvato in: metadata_debug.xml (rimuovilo dopo il test)\n";
+40
View File
@@ -0,0 +1,40 @@
<?php
// Elimina un binding JSON -> LIMS. Ritorna JSON.
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
include dirname(__DIR__) . '/../extra/auth.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
if (!Auth::check()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
exit;
}
$id = intval($_POST['id'] ?? 0);
if ($id <= 0) {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Missing id']);
exit;
}
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->prepare("DELETE FROM json_lims_binding WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(['success' => true, 'deleted' => $stmt->rowCount()]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
+10 -25
View File
@@ -15,9 +15,8 @@ try {
$partId = isset($_POST['part_id']) ? (int)$_POST['part_id'] : 0;
$analysisRecordkey = trim($_POST['analysis_recordkey'] ?? '');
$rowId = isset($_POST['row_id']) && $_POST['row_id'] !== '' ? (int)$_POST['row_id'] : 0;
if ($partId <= 0 || ($analysisRecordkey === '' && $rowId <= 0)) {
if ($partId <= 0 || $analysisRecordkey === '') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Missing required data']);
exit;
@@ -26,29 +25,15 @@ try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
if ($rowId > 0) {
$stmt = $pdo->prepare("
DELETE FROM identification_parts_analyses
WHERE id = :id
AND part_id = :part_id
LIMIT 1
");
$stmt->execute([
':id' => $rowId,
':part_id' => $partId,
]);
} else {
$stmt = $pdo->prepare("
DELETE FROM identification_parts_analyses
WHERE part_id = :part_id
AND analysis_recordkey = :analysis_recordkey
LIMIT 1
");
$stmt->execute([
':part_id' => $partId,
':analysis_recordkey' => $analysisRecordkey,
]);
}
$stmt = $pdo->prepare("
DELETE FROM identification_parts_analyses
WHERE part_id = :part_id
AND analysis_recordkey = :analysis_recordkey
");
$stmt->execute([
':part_id' => $partId,
':analysis_recordkey' => $analysisRecordkey,
]);
echo json_encode([
'success' => true,
+46
View File
@@ -0,0 +1,46 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$idRapportoFile = isset($_GET['id_rapporto_file']) ? (int)$_GET['id_rapporto_file'] : 0;
if ($idRapportoFile <= 0) {
throw new Exception("Parametro id_rapporto_file mancante o non valido.");
}
/*
* This endpoint returns the PDF binary stream.
* Do not call this with the normal get() method because get() expects JSON.
*/
$endpoint = "MediaFile/DownloadStream?objectType=RapportoFile&propertyName=FileContent&objectKey={$idRapportoFile}";
$api = VisualLimsApiClient::getInstance();
$pdfContent = $api->getRaw($endpoint);
if (empty($pdfContent)) {
throw new Exception("PDF vuoto o non ricevuto dal server.");
}
$fileName = "rapporto_{$idRapportoFile}.pdf";
header('Content-Type: application/pdf');
header('Content-Disposition: inline; filename="' . $fileName . '"');
header('Content-Length: ' . strlen($pdfContent));
header('Cache-Control: private, max-age=0, must-revalidate');
header('Pragma: public');
echo $pdfContent;
exit;
} catch (Exception $e) {
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+1 -5
View File
@@ -265,11 +265,7 @@ if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonText
<label class="form-label"><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></label>
<textarea name="description" class="form-control"><?php echo htmlspecialchars($template['description'] ?? ''); ?></textarea>
</div>
<div class="mb-3">
<label class="form-label">Template Note</label>
<textarea name="template_note" class="form-control" rows="3"><?php echo htmlspecialchars($template['template_note'] ?? ''); ?></textarea>
<small class="text-muted">If set, this note will be appended at the end of the notes during export.</small>
</div>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($desttable, ENT_QUOTES, 'UTF-8'); ?> *</label>
<input type="text" name="target_table" class="form-control" value="<?php echo htmlspecialchars($template['target_table'] ?? 'datadb'); ?>" readonly required>
-43
View File
@@ -331,46 +331,3 @@
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
+116 -301
View File
@@ -5,7 +5,7 @@
* Single export + batch export (Export All) with validation.
*/
(function () {
"use strict";
'use strict';
let pendingConfirmHandler = null;
let batchRunning = false;
@@ -17,7 +17,7 @@
// ── Helpers ──────────────────────────────────────────────────────────
function cleanupBackdrop() {
document.querySelectorAll(".modal-backdrop").forEach((b) => b.remove());
document.querySelectorAll(".modal-backdrop").forEach(b => b.remove());
document.body.classList.remove("modal-open");
document.body.style.paddingRight = "";
const overlay = document.querySelector(".overlay.toggle-icon");
@@ -29,9 +29,7 @@
}
function getRowIndexByIddatadb(iddatadb) {
return (window.gridData || []).findIndex(
(r) => String(r.iddatadb) === String(iddatadb),
);
return (window.gridData || []).findIndex(r => String(r.iddatadb) === String(iddatadb));
}
// ── Validation ──────────────────────────────────────────────────────
@@ -42,31 +40,21 @@
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rows: rowsToValidate }),
});
if (!response.ok)
throw new Error(`Validation HTTP error: ${response.status}`);
if (!response.ok) throw new Error(`Validation HTTP error: ${response.status}`);
return response.json();
}
function clearValidationErrors() {
// Clear from gridData
(window.gridData || []).forEach((row) => {
delete row._validationErrors;
delete row._exportError;
});
(window.gridData || []).forEach(row => { delete row._validationErrors; delete row._exportError; });
document
.querySelectorAll(".grid-cell.validation-error")
.forEach((cell) => {
cell.classList.remove("validation-error");
cell.querySelectorAll(".input-validation-error").forEach((el) =>
el.classList.remove("input-validation-error"),
);
const tooltip = cell.querySelector(".validation-tooltip");
if (tooltip) tooltip.remove();
});
document
.querySelectorAll(".grid-row.validation-row-error")
.forEach((row) => row.classList.remove("validation-row-error"));
document.querySelectorAll(".grid-cell.validation-error").forEach(cell => {
cell.classList.remove("validation-error");
cell.querySelectorAll(".input-validation-error").forEach(el => el.classList.remove("input-validation-error"));
const tooltip = cell.querySelector(".validation-tooltip");
if (tooltip) tooltip.remove();
});
document.querySelectorAll(".grid-row.validation-row-error").forEach(row => row.classList.remove("validation-row-error"));
clearAllRowErrors();
}
@@ -79,7 +67,7 @@
gridRow.classList.add("validation-row-error");
const messages = [];
errors.forEach((err) => {
errors.forEach(err => {
messages.push(err.message);
if (!err.field) return;
@@ -88,26 +76,19 @@
const label = err.field.substring("field_label:".length);
const headers = document.querySelectorAll(".grid-header");
let targetIndex = null;
headers.forEach((h) => {
if (h.textContent.trim() === label)
targetIndex = h.getAttribute("data-index");
headers.forEach(h => {
if (h.textContent.trim() === label) targetIndex = h.getAttribute("data-index");
});
if (targetIndex) {
cell = gridRow.querySelector(
`.grid-cell[data-index="${targetIndex}"]`,
);
cell = gridRow.querySelector(`.grid-cell[data-index="${targetIndex}"]`);
}
} else {
cell = gridRow.querySelector(
`.grid-cell[data-col="${err.field}"]`,
);
cell = gridRow.querySelector(`.grid-cell[data-col="${err.field}"]`);
}
if (cell) {
cell.classList.add("validation-error");
cell.querySelectorAll("input, select").forEach((el) =>
el.classList.add("input-validation-error"),
);
cell.querySelectorAll("input, select").forEach(el => el.classList.add("input-validation-error"));
let tooltip = cell.querySelector(".validation-tooltip");
if (!tooltip) {
tooltip = document.createElement("div");
@@ -128,43 +109,34 @@
formData.append("iddatadb", iddatadb);
if (batchUuid) formData.append("batch_uuid", batchUuid);
const response = await fetch("export_to_lims.php", {
method: "POST",
body: formData,
});
if (!response.ok)
throw new Error(`HTTP error! status: ${response.status}`);
const response = await fetch("export_to_lims.php", { method: "POST", body: formData });
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
if (data.success) {
// Update gridData
const idx = getRowIndexByIddatadb(iddatadb);
if (idx >= 0) {
window.gridData[idx].status = "l";
window.gridData[idx].status = 'l';
window.gridData[idx].commessaweb = data.commessaweb;
}
// Update visible DOM row
const gridRow = getGridRow(iddatadb);
if (gridRow) {
const statusBadge = gridRow.querySelector(
'.grid-cell[data-col="status"] .status-badge',
);
const statusBadge = gridRow.querySelector('.grid-cell[data-col="status"] .status-badge');
if (statusBadge) {
statusBadge.classList.remove("status-i", "status-P");
statusBadge.classList.add("status-l");
statusBadge.textContent = "To LIMS";
}
const statusCell = gridRow.querySelector(
'.grid-cell[data-col="status"]',
);
const statusCell = gridRow.querySelector('.grid-cell[data-col="status"]');
if (statusCell && data.commessaweb) {
let cwSpan = statusCell.querySelector(".commessaweb-code");
if (!cwSpan) {
cwSpan = document.createElement("span");
cwSpan.className = "commessaweb-code";
cwSpan.style.cssText =
"display:block; font-size:0.75em; color:#555; margin-top:2px;";
cwSpan.style.cssText = "display:block; font-size:0.75em; color:#555; margin-top:2px;";
statusCell.appendChild(cwSpan);
}
cwSpan.textContent = data.commessaweb;
@@ -192,8 +164,7 @@
const label = document.getElementById("exportResponseModalLabel");
if (data.success) {
msg.innerHTML =
`${data.message.replace(/\n/g, "<br>")}` +
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}` : "");
@@ -215,14 +186,10 @@
row.classList.remove("batch-disabled");
row.classList.add("batch-exporting");
if (btnCell) {
btnCell.querySelectorAll(".action-btn").forEach((b) => {
b.dataset.prevDisplay = b.style.display;
b.style.display = "none";
});
btnCell.querySelectorAll(".action-btn").forEach(b => { b.dataset.prevDisplay = b.style.display; b.style.display = "none"; });
const spinner = document.createElement("span");
spinner.className = "batch-row-spinner";
spinner.innerHTML =
'<i class="fas fa-spinner fa-spin"></i> Exporting...';
spinner.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Exporting...';
btnCell.appendChild(spinner);
}
} else {
@@ -230,10 +197,7 @@
if (btnCell) {
const spinner = btnCell.querySelector(".batch-row-spinner");
if (spinner) spinner.remove();
btnCell.querySelectorAll(".action-btn").forEach((b) => {
b.style.display = b.dataset.prevDisplay || "";
delete b.dataset.prevDisplay;
});
btnCell.querySelectorAll(".action-btn").forEach(b => { b.style.display = b.dataset.prevDisplay || ""; delete b.dataset.prevDisplay; });
}
}
}
@@ -249,56 +213,32 @@
errorEl.className = "batch-error-msg";
errorEl.textContent = "Warning — click for details";
errorEl.addEventListener("click", () => {
document.getElementById("exportResponseMessage").innerHTML =
message.replace(/\n/g, "<br>");
document.getElementById(
"exportResponseModalLabel",
).textContent = "Error (id: " + iddatadb + ")";
new bootstrap.Modal(
document.getElementById("exportResponseModal"),
{ keyboard: false },
).show();
document.getElementById("exportResponseMessage").innerHTML = message.replace(/\n/g, "<br>");
document.getElementById("exportResponseModalLabel").textContent = "Error (id: " + iddatadb + ")";
new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show();
});
btnCell.appendChild(errorEl);
}
}
function clearAllRowErrors() {
document
.querySelectorAll(".grid-row.batch-row-error")
.forEach((row) => {
row.classList.remove("batch-row-error");
const msg = row.querySelector(".batch-error-msg");
if (msg) msg.remove();
});
document.querySelectorAll(".grid-row.batch-row-error").forEach(row => {
row.classList.remove("batch-row-error");
const msg = row.querySelector(".batch-error-msg");
if (msg) msg.remove();
});
}
function disableAllRowButtons() {
document
.querySelectorAll(".grid-row[data-id]")
.forEach((row) => row.classList.add("batch-disabled"));
const toggle = document.querySelector(
".actions-dropdown .dropdown-toggle",
);
if (toggle) {
toggle.disabled = true;
toggle.style.opacity = "0.5";
toggle.style.pointerEvents = "none";
}
document.querySelectorAll(".grid-row[data-id]").forEach(row => row.classList.add("batch-disabled"));
const toggle = document.querySelector(".actions-dropdown .dropdown-toggle");
if (toggle) { toggle.disabled = true; toggle.style.opacity = "0.5"; toggle.style.pointerEvents = "none"; }
}
function enableAllRowButtons() {
document
.querySelectorAll(".grid-row[data-id]")
.forEach((row) => row.classList.remove("batch-disabled"));
const toggle = document.querySelector(
".actions-dropdown .dropdown-toggle",
);
if (toggle) {
toggle.disabled = false;
toggle.style.opacity = "";
toggle.style.pointerEvents = "";
}
document.querySelectorAll(".grid-row[data-id]").forEach(row => row.classList.remove("batch-disabled"));
const toggle = document.querySelector(".actions-dropdown .dropdown-toggle");
if (toggle) { toggle.disabled = false; toggle.style.opacity = ""; toggle.style.pointerEvents = ""; }
}
// ── Single row export: validate → confirm → send ────────────────────
@@ -310,61 +250,40 @@
if (gridRow) {
setRowExporting(gridRow, true);
const spinner = gridRow.querySelector(".batch-row-spinner");
if (spinner)
spinner.innerHTML =
'<i class="fas fa-spinner fa-spin"></i> Validating...';
if (spinner) spinner.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Validating...';
}
validateRows([{ iddatadb: parseInt(iddatadb), index: rowIndex }])
.then((validationData) => {
if (gridRow) {
setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
}
.then(validationData => {
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
if (!validationData.success) {
showExportResult({
success: false,
message: validationData.message || "Validation error",
});
showExportResult({ success: false, message: validationData.message || "Validation error" });
return;
}
const result = validationData.results[rowIndex];
if (result && !result.valid) {
if (gridRow)
showValidationErrors(gridRow, iddatadb, result.errors);
if (gridRow) showValidationErrors(gridRow, iddatadb, result.errors);
return;
}
showConfirmAndExport(iddatadb, rowIndex);
})
.catch((error) => {
if (gridRow) {
setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
}
showExportResult({
success: false,
message: "Validation error: " + error.message,
});
.catch(error => {
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
showExportResult({ success: false, message: "Validation error: " + error.message });
});
}
function showConfirmAndExport(iddatadb, rowIndex) {
const confirmModalElement =
document.getElementById("exportConfirmModal");
const confirmModalElement = document.getElementById("exportConfirmModal");
if (!confirmModalElement) return;
const confirmModal = new bootstrap.Modal(confirmModalElement, {
keyboard: false,
});
const confirmModal = new bootstrap.Modal(confirmModalElement, { keyboard: false });
document.getElementById("exportIddatadb").textContent = iddatadb;
confirmModal.show();
const confirmBtn = document.getElementById("exportConfirmBtn");
if (!confirmBtn) {
confirmModal.hide();
return;
}
if (!confirmBtn) { confirmModal.hide(); return; }
const confirmHandler = async () => {
pendingConfirmHandler = null;
@@ -375,36 +294,24 @@
try {
const data = await sendExport(iddatadb);
if (gridRow) {
setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
}
if (!data.success)
showRowError(
gridRow,
iddatadb,
data.message || "Unknown error",
);
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
if (!data.success) showRowError(gridRow, iddatadb, data.message || "Unknown error");
showExportResult(data);
} catch (error) {
if (gridRow) {
setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
}
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
showRowError(gridRow, iddatadb, error.message);
showExportResult({ success: false, message: error.message });
}
};
if (pendingConfirmHandler)
confirmBtn.removeEventListener("click", pendingConfirmHandler);
if (pendingConfirmHandler) confirmBtn.removeEventListener("click", pendingConfirmHandler);
pendingConfirmHandler = confirmHandler;
confirmBtn.addEventListener("click", confirmHandler, { once: true });
}
// ── Single row click (event delegation) ─────────────────────────────
$(document).on("click", ".export-lims-btn", function (e) {
$(document).on('click', '.export-lims-btn', function (e) {
e.preventDefault();
if (batchRunning) return;
@@ -415,34 +322,24 @@
// Check unsaved changes for this row
const dataRow = window.gridData?.[rowIndex];
if (dataRow && dataRow._dirty) {
const unsavedModal = new bootstrap.Modal(
document.getElementById("exportUnsavedModal"),
{ keyboard: false },
);
const unsavedModal = new bootstrap.Modal(document.getElementById("exportUnsavedModal"), { keyboard: false });
unsavedModal.show();
document.getElementById("saveAndExportBtn")?.addEventListener(
"click",
() => {
unsavedModal.hide();
// Save first, then export
const formData = window.buildSavePayload(rowIndex);
fetch("save_edited_row.php", {
method: "POST",
body: formData,
})
.then((r) => r.json())
.then((result) => {
if (result.success) {
dataRow._dirty = false;
startExportConfirmFlow(iddatadb, rowIndex);
} else {
alert("Save failed: " + result.message);
}
});
},
{ once: true },
);
document.getElementById("saveAndExportBtn")?.addEventListener("click", () => {
unsavedModal.hide();
// Save first, then export
const formData = window.buildSavePayload(rowIndex);
fetch('save_edited_row.php', { method: 'POST', body: formData })
.then(r => r.json())
.then(result => {
if (result.success) {
dataRow._dirty = false;
startExportConfirmFlow(iddatadb, rowIndex);
} else {
alert('Save failed: ' + result.message);
}
});
}, { once: true });
return;
}
@@ -452,33 +349,18 @@
// ── Batch export (Export All) ───────────────────────────────────────
function collectEligibleRows() {
// Read from gridData, not DOM. Se i filtri sono attivi, limita al target set.
const target =
typeof window.getTargetGridIds === "function"
? window.getTargetGridIds()
: null;
// Read from gridData, not DOM
const eligible = [];
(window.gridData || []).forEach((row, index) => {
if (row.status === "l") return;
if (target && !target.has(parseInt(row.iddatadb, 10))) return;
eligible.push({
iddatadb: row.iddatadb,
index,
row: getGridRow(row.iddatadb),
});
if (row.status !== 'l') {
eligible.push({ iddatadb: row.iddatadb, index, row: getGridRow(row.iddatadb) });
}
});
return eligible;
}
function hasUnsavedChanges() {
const target =
typeof window.getTargetGridIds === "function"
? window.getTargetGridIds()
: null;
return (window.gridData || []).some(
(r) =>
r._dirty && (!target || target.has(parseInt(r.iddatadb, 10))),
);
return (window.gridData || []).some(r => r._dirty);
}
async function validateAndFilter(eligibleRows) {
@@ -487,8 +369,7 @@
index,
}));
const validationData = await validateRows(rowsToValidate);
if (!validationData.success)
throw new Error(validationData.message || "Validation error");
if (!validationData.success) throw new Error(validationData.message || "Validation error");
const validRows = [];
let invalidCount = 0;
@@ -496,12 +377,7 @@
for (const item of eligibleRows) {
const result = validationData.results[item.index];
if (result && !result.valid) {
if (item.row)
showValidationErrors(
item.row,
item.iddatadb,
result.errors,
);
if (item.row) showValidationErrors(item.row, item.iddatadb, result.errors);
invalidCount++;
} else {
validRows.push(item);
@@ -534,92 +410,54 @@
if (validRows.length === 0) {
document.getElementById("exportResponseMessage").innerHTML =
`No valid rows for export.<br><strong>${invalidCount}</strong> rows with validation errors.`;
document.getElementById(
"exportResponseModalLabel",
).textContent = "Validation Failed";
new bootstrap.Modal(
document.getElementById("exportResponseModal"),
{ keyboard: false },
).show();
document.getElementById("exportResponseModalLabel").textContent = "Validation Failed";
new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show();
return;
}
const confirmModal = new bootstrap.Modal(
document.getElementById("exportBatchConfirmModal"),
{ keyboard: false },
);
const confirmModal = new bootstrap.Modal(document.getElementById("exportBatchConfirmModal"), { keyboard: false });
let countText = String(validRows.length);
if (invalidCount > 0)
countText += ` (${invalidCount} excluded due to errors)`;
document.getElementById("exportBatchCount").textContent =
countText;
if (invalidCount > 0) countText += ` (${invalidCount} excluded due to errors)`;
document.getElementById("exportBatchCount").textContent = countText;
confirmModal.show();
const confirmBtn = document.getElementById(
"exportBatchConfirmBtn",
);
if (pendingBatchConfirmHandler)
confirmBtn.removeEventListener(
"click",
pendingBatchConfirmHandler,
);
const confirmBtn = document.getElementById("exportBatchConfirmBtn");
if (pendingBatchConfirmHandler) confirmBtn.removeEventListener("click", pendingBatchConfirmHandler);
pendingBatchConfirmHandler = () => {
pendingBatchConfirmHandler = null;
confirmModal.hide();
startBatchExport(validRows);
};
confirmBtn.addEventListener(
"click",
pendingBatchConfirmHandler,
{ once: true },
);
confirmBtn.addEventListener("click", pendingBatchConfirmHandler, { once: true });
})
.catch((error) => {
.catch(error => {
showValidationSpinner(false);
document.getElementById("exportResponseMessage").textContent =
"Validation error: " + error.message;
document.getElementById(
"exportResponseModalLabel",
).textContent = "Validation Error";
new bootstrap.Modal(
document.getElementById("exportResponseModal"),
{ keyboard: false },
).show();
document.getElementById("exportResponseMessage").textContent = "Validation error: " + error.message;
document.getElementById("exportResponseModalLabel").textContent = "Validation Error";
new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show();
});
}
$(document).on("click", ".export-all-lims-btn", function (e) {
$(document).on('click', '.export-all-lims-btn', function (e) {
e.preventDefault();
if (batchRunning) return;
if (hasUnsavedChanges()) {
const unsavedModal = new bootstrap.Modal(
document.getElementById("exportBatchUnsavedModal"),
{ keyboard: false },
);
const unsavedModal = new bootstrap.Modal(document.getElementById("exportBatchUnsavedModal"), { keyboard: false });
unsavedModal.show();
document.getElementById("batchSaveAndExportBtn")?.addEventListener(
"click",
() => {
unsavedModal.hide();
// Trigger save all first — listen for completion
alert("Please Save All first, then Export All.");
},
{ once: true },
);
document.getElementById("batchSaveAndExportBtn")?.addEventListener("click", () => {
unsavedModal.hide();
// Trigger save all first — listen for completion
alert("Please Save All first, then Export All.");
}, { once: true });
return;
}
const eligibleRows = collectEligibleRows();
if (eligibleRows.length === 0) {
document.getElementById("exportResponseMessage").textContent =
"All rows already exported to LIMS.";
document.getElementById("exportResponseModalLabel").textContent =
"Export All";
new bootstrap.Modal(
document.getElementById("exportResponseModal"),
{ keyboard: false },
).show();
document.getElementById("exportResponseMessage").textContent = "All rows already exported to LIMS.";
document.getElementById("exportResponseModalLabel").textContent = "Export All";
new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show();
return;
}
showBatchConfirm(eligibleRows);
@@ -630,9 +468,7 @@
batchRunning = true;
const batchUuid = crypto.randomUUID();
const total = eligibleRows.length;
let processed = 0,
succeeded = 0,
failed = 0;
let processed = 0, succeeded = 0, failed = 0;
disableAllRowButtons();
@@ -643,25 +479,18 @@
if (cancelBtn) cancelBtn.disabled = false;
if (statusEl) statusEl.textContent = `Exporting 0 / ${total}...`;
cancelBtn?.addEventListener(
"click",
() => {
batchCancelled = true;
if (statusEl)
statusEl.textContent =
"Cancelling... (waiting for current row)";
if (cancelBtn) cancelBtn.disabled = true;
},
{ once: true },
);
cancelBtn?.addEventListener("click", () => {
batchCancelled = true;
if (statusEl) statusEl.textContent = "Cancelling... (waiting for current row)";
if (cancelBtn) cancelBtn.disabled = true;
}, { once: true });
(async () => {
for (let i = 0; i < eligibleRows.length; i++) {
if (batchCancelled) break;
const { iddatadb, row } = eligibleRows[i];
if (statusEl)
statusEl.textContent = `Exporting ${processed + 1} / ${total} (id: ${iddatadb})...`;
if (statusEl) statusEl.textContent = `Exporting ${processed + 1} / ${total} (id: ${iddatadb})...`;
const gridRow = row || getGridRow(iddatadb);
if (gridRow) setRowExporting(gridRow, true);
@@ -674,29 +503,18 @@
} else {
failed++;
const errIdx = getRowIndexByIddatadb(iddatadb);
if (errIdx >= 0)
window.gridData[errIdx]._exportError =
data.message || "Unknown error";
if (gridRow)
showRowError(
gridRow,
iddatadb,
data.message || "Unknown error",
);
if (errIdx >= 0) window.gridData[errIdx]._exportError = data.message || "Unknown error";
if (gridRow) showRowError(gridRow, iddatadb, data.message || "Unknown error");
}
} catch (error) {
processed++;
failed++;
const errIdx = getRowIndexByIddatadb(iddatadb);
if (errIdx >= 0)
window.gridData[errIdx]._exportError = error.message;
if (errIdx >= 0) window.gridData[errIdx]._exportError = error.message;
if (gridRow) showRowError(gridRow, iddatadb, error.message);
}
if (gridRow) {
setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
}
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
}
batchRunning = false;
@@ -711,8 +529,7 @@
(window.gridData || []).forEach((row, idx) => {
if (row._exportError) {
const gridRow = getGridRow(row.iddatadb);
if (gridRow)
showRowError(gridRow, row.iddatadb, row._exportError);
if (gridRow) showRowError(gridRow, row.iddatadb, row._exportError);
}
});
@@ -732,9 +549,7 @@
const modalEl = document.getElementById("exportResponseModal");
new bootstrap.Modal(modalEl, { keyboard: false }).show();
modalEl.addEventListener("hidden.bs.modal", cleanupBackdrop, {
once: true,
});
modalEl.addEventListener("hidden.bs.modal", cleanupBackdrop, { once: true });
})();
}
})();
+10 -100
View File
@@ -15,40 +15,6 @@ if (!is_dir($logDir)) {
$uploadDir = realpath(__DIR__ . '/../photostrf') . '/';
/**
* Resolve a datadb_photos.file_path to an absolute file path on disk.
*/
function resolvePhotoFullPath(string $filePath, string $uploadDir, string $userareaDir): ?string
{
$filePath = trim($filePath);
if ($filePath === '') {
return null;
}
$base = rtrim($uploadDir, '/');
$candidates = [];
// Userarea-relative form: "../photostrf/annotated/x.png" → resolve from __DIR__.
if (strncmp($filePath, '../', 3) === 0) {
$candidates[] = rtrim($userareaDir, '/') . '/' . $filePath;
}
// Bare filename or "annotated/x.png" subpath - resolve from photostrf/.
$candidates[] = $base . '/' . ltrim($filePath, './');
// Safety net for any odd/legacy value.
$candidates[] = $base . '/' . basename($filePath);
foreach ($candidates as $candidate) {
$real = realpath($candidate);
if ($real && file_exists($real)) {
return $real;
}
}
return null;
}
// 🔹 Base URL API
$apiBaseUrl = 'https://93.43.5.102/limsapi/api/odata/';
@@ -150,15 +116,13 @@ try {
// 🔹 STEP 1+2: Fetch Cliente ID from datadb and Schema ID from excel_templates
// Also fetch fixed fields stored in datadb
$stmt = $pdo->prepare("
SELECT d.idclient AS clienteId, et.idschema AS schemaId, et.id AS templateId,
et.template_note,
SELECT d.idclient AS clienteId, et.idschema AS schemaId,
d.cliente_responsabile_id,
d.moltiplicatore_prezzo_id,
d.anagrafica_certest_object_id,
d.anagrafica_certest_service_id,
d.cliente_fornitore_id,
d.clienteAnalisi,
d.ClienteFatturazione,
d.consegna_richiesta
FROM datadb as d
INNER JOIN excel_templates as et ON d.templateid = et.id
@@ -174,7 +138,6 @@ try {
$clienteId = (int) $result['clienteId'];
$schemaId = (int) $result['schemaId'];
$templateId = (int) $result['templateId'];
// Extract fixed fields (nullable INTs)
$clienteResponsabile = !empty($result['cliente_responsabile_id']) ? (int) $result['cliente_responsabile_id'] : null;
@@ -183,12 +146,11 @@ try {
$anagraficaService = !empty($result['anagrafica_certest_service_id']) ? (int) $result['anagrafica_certest_service_id'] : null;
$clienteFornitore = !empty($result['cliente_fornitore_id']) ? (int) $result['cliente_fornitore_id'] : null;
$clienteAnalisi = !empty($result['clienteAnalisi']) ? (int) $result['clienteAnalisi'] : null;
$clienteFatturazione = !empty($result['ClienteFatturazione']) ? (int) $result['ClienteFatturazione'] : null;
$consegnaRichiesta = !empty($result['consegna_richiesta']) ? $result['consegna_richiesta'] : null;
// 🔹 STEP 3: Fetch Parts (including idmatrice and part id for custom fields)
$stmt = $pdo->prepare("
SELECT id AS part_id, part_number, part_description, material, color, mix, idmatrice, dateexpiry, note
SELECT id AS part_id, part_number, part_description, material, color, mix, idmatrice, dateexpiry
FROM identification_parts
WHERE iddatadb = :iddatadb
AND part_description IS NOT NULL
@@ -214,7 +176,6 @@ try {
}
}
// 🔹 STEP 4a: Auto-populate export_date / export_time fields
$stmt = $pdo->prepare("
UPDATE import_data_details idd
@@ -238,9 +199,7 @@ try {
idd.field_value,
m.field_label,
m.schema_id,
m.field_id,
m.field_order,
m.is_addnote
m.field_id
FROM
import_data_details as idd
JOIN template_mapping m ON idd.mapping_id = m.id
@@ -260,35 +219,6 @@ try {
$valueMap[(int) $row['field_id']] = $row['field_value'];
}
// 🔹 Note aggiuntive: custom field flaggati is_addnote e valorizzati.
// Valori a livello di commessa → identici su tutte le parti.
$addnoteChunks = [];
$addnoteRows = array_filter($rows, fn($r) => (int)($r['is_addnote'] ?? 0) === 1);
usort($addnoteRows, function ($a, $b) {
$oa = (int)($a['field_order'] ?? 9999);
$ob = (int)($b['field_order'] ?? 9999);
if ($oa !== $ob) return $oa <=> $ob;
return strcmp((string)($a['field_label'] ?? ''), (string)($b['field_label'] ?? ''));
});
foreach ($addnoteRows as $r) {
$val = trim((string)($r['field_value'] ?? ''));
if ($val !== '') {
$addnoteChunks[] = $val;
}
}
$addnoteText = implode(' - ', $addnoteChunks);
// Nota del template: appesa in fondo alle note in esportazione
$templateNote = trim((string)($result['template_note'] ?? ''));
if ($templateNote !== '') {
$addnoteText = $addnoteText !== ''
? $addnoteText . ' - ' . $templateNote
: $templateNote;
}
// Logga i fieldValues in error_log
$logFieldValues = "FieldValues dal DB (iddatadb={$iddatadb}):\n" . json_encode($fieldValues, JSON_PRETTY_PRINT);
error_log($logFieldValues);
@@ -310,7 +240,6 @@ try {
"AnagraficaCertestService" => $anagraficaService,
"ClienteFornitore" => $clienteFornitore, // PLACEHOLDER — to be implemented
"ClienteAnalisi" => $clienteAnalisi, // PLACEHOLDER — to be implemented
// "ClienteFatturazione" => $clienteFatturazione, // ⚠️ TEMP DISABLED - problemi API lato LIMS
// DeliveryRequest goes to Campione, not CommessaWeb
];
@@ -343,33 +272,13 @@ try {
throw new Exception("Invalid or missing idmatrice for part: " . ($part["part_number"] ?? "Unknown"));
}
$partDescription = $part["part_description"] ?? "";
$partNote = trim((string)($part["note"] ?? ""));
// Note aggiuntive dai custom field flaggati is_addnote
$partDescription = $part["part_description"] ?? "";
$partNote = trim((string)($part["note"] ?? ""));
// Componi NoteWeb: descrizione [- note] [- addnote di commessa]
$noteParts = [];
if ($partDescription !== "") {
$noteParts[] = $partDescription;
}
if ($partNote !== "") {
$noteParts[] = $partNote;
}
if ($addnoteText !== "") {
$noteParts[] = $addnoteText;
}
$noteWebValue = implode(" - ", $noteParts);
$campionePayload = [
"Commessa" => $commessaId,
"Matrice" => $matriceId,
"SottoMatrice" => null,
"SchemaCustomField" => $schemaId,
// "Riferimento" => $part["part_description"] ?? "",
"NoteWeb" => $noteWebValue,
"NoteWeb" => $part["part_description"] ?? "",
"ConsegnaRichiesta" => !empty($part["dateexpiry"]) ? $part["dateexpiry"] : $consegnaRichiesta,
];
@@ -511,10 +420,11 @@ try {
$logContentPhotos .= "=== Campione {$campioneId} (main) ===\n";
foreach ($photos as $photo) {
$fullPath = resolvePhotoFullPath($photo['file_path'], $uploadDir, __DIR__);
$photoPath = $uploadDir . '/' . ltrim($photo['file_path'], './');
$fullPath = realpath($photoPath);
if ($fullPath === null) {
$logContentPhotos .= "SKIP (file not found): file_path='{$photo['file_path']}'\n";
if (!$fullPath || !file_exists($fullPath)) {
$logContentPhotos .= "SKIP (file not found): {$photoPath}\n";
continue;
}
@@ -684,10 +594,10 @@ try {
}
}
// 🔹 STEP 8: Update datadb with idcommessaweb, commessaweb, status and export date
// 🔹 STEP 8: Update datadb with idcommessaweb, commessaweb, and status
$stmt = $pdo->prepare("
UPDATE datadb
SET idcommessaweb = :idcommessaweb, commessaweb = :commessaweb, status = 'l', export_date = NOW()
SET idcommessaweb = :idcommessaweb, commessaweb = :commessaweb, status = 'l'
WHERE iddatadb = :iddatadb
");
$stmt->execute([
-394
View File
@@ -1,394 +0,0 @@
<?php
include('include/headscript.php');
require_once __DIR__ . '/include/grid_data_builder.php';
header('Content-Type: application/json');
$pdo = DBHandlerSelect::getInstance()->getConnection();
const FILTER_ALLOWED_PER_PAGE = [20, 40, 60, 100];
const FILTER_ALLIDS_CAP = 5000;
$body = json_decode(file_get_contents('php://input'), true) ?: [];
$templateId = (int)($body['template_id'] ?? 0);
if ($templateId <= 0) {
echo json_encode(['success' => false, 'message' => 'template_id mancante']);
exit;
}
$status = in_array($body['status'] ?? 'i', ['i', 'P', 'l'], true) ? $body['status'] : 'i';
$showAll = !empty($body['all_users']);
$importref = trim((string)($body['importref'] ?? '')); // scope su un singolo import
$filters = is_array($body['filters'] ?? null) ? $body['filters'] : [];
$userId = (int)($iduserlogin ?? 0);
$perPage = in_array((int)($body['per_page'] ?? 20), FILTER_ALLOWED_PER_PAGE, true) ? (int)$body['per_page'] : 20;
$page = max(1, (int)($body['page'] ?? 1));
$wantAllIds = !empty($body['want_all_ids']);
$cacheDir = __DIR__ . '/cache';
$explicitIds = [];
if (isset($body['ids']) && is_array($body['ids'])) {
$explicitIds = array_values(array_unique(array_filter(
array_map('intval', $body['ids']),
fn($v) => $v > 0
)));
}
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; }
$ph = implode(',', array_fill(0, count($explicitIds), '?'));
$conds[] = "d.iddatadb IN ($ph)";
foreach ($explicitIds as $id) $params[] = $id;
$whereSql = implode(' AND ', $conds);
try {
$countStmt = $pdo->prepare("SELECT COUNT(*) FROM datadb d WHERE {$whereSql}");
$countStmt->execute($params);
$total = (int)$countStmt->fetchColumn();
$totalPages = max(1, (int)ceil($total / $perPage));
if ($page > $totalPages) $page = $totalPages;
$offset = ($page - 1) * $perPage;
$idStmt = $pdo->prepare("
SELECT d.iddatadb FROM datadb d
WHERE {$whereSql}
ORDER BY d.excelrow ASC, d.iddatadb ASC
LIMIT {$perPage} OFFSET {$offset}
");
$idStmt->execute($params);
$pageIds = array_map('intval', $idStmt->fetchAll(PDO::FETCH_COLUMN));
$config = buildGridConfig($pdo, $templateId);
$rows = buildGridRows($pdo, $pageIds, $config);
echo json_encode([
'success' => true,
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'total_pages' => $totalPages,
'rows' => $rows,
'mode' => 'selection',
]);
} catch (Exception $e) {
error_log('filter_records (ids mode) error: ' . $e->getMessage());
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
exit;
}
$fixedAliasMap = [
'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',
];
$staticCols = ['importreferencecode', 'filename_import', 'importdate', 'commessaweb'];
// ── Risolutori "etichetta → lista di ID" via cache ────────────────────────
function cacheItems(string $file): array
{
if (!is_file($file)) return [];
$data = json_decode(file_get_contents($file), true);
if (!is_array($data)) return [];
return $data['value'] ?? $data ?? [];
}
/** Clienti: label "Nome - X - id" (come search_clienti.php). */
function resolveClientIdsByTerm(string $cacheDir, string $term): array
{
$term = mb_strtolower(trim($term));
if ($term === '') return [];
$ids = [];
foreach (cacheItems($cacheDir . '/clienti.json') as $c) {
$name = trim($c['Nominativo'] ?? '');
$id = trim((string)($c['IdCliente'] ?? ''));
$code = trim((string)($c['CodiceCliente'] ?? ''));
$parts = explode('_', $code);
$suffix = trim($parts[1] ?? '');
if ($suffix === '' && $code !== '') $suffix = substr($code, 0, 1);
if ($suffix === '') $suffix = '--';
$label = mb_strtolower($name . ' - ' . $suffix . ' - ' . $id);
if (mb_strpos($label, $term) !== false) $ids[] = (int)$c['IdCliente'];
}
return array_values(array_unique($ids));
}
/** Dropdown generico: matcha il termine su qualsiasi label field, ritorna gli id. */
function resolveIdsFromCache(string $file, string $idField, array $labelFields, string $term): array
{
$term = mb_strtolower(trim($term));
if ($term === '') return [];
$ids = [];
foreach (cacheItems($file) as $it) {
$hay = '';
foreach ($labelFields as $lf) $hay .= ' ' . ($it[$lf] ?? '');
if (mb_strpos(mb_strtolower($hay), $term) !== false && isset($it[$idField])) {
$ids[] = (int)$it[$idField];
}
}
return array_values(array_unique($ids));
}
/**
* Per un fixed field che memorizza un ID, risolve il termine in lista di id.
* Ritorna: array di id (IN), [] se il termine non matcha nulla, null se la colonna
* non è risolvibile ( il chiamante la salta invece di fare LIKE sbagliato).
*/
function resolveFixedIds(string $cacheDir, string $key, string $term): ?array
{
switch ($key) {
case 'ClienteAnalisi':
case 'ClienteFatturazione':
return resolveClientIdsByTerm($cacheDir, $term);
case 'MoltiplicatorePrezzo':
return resolveIdsFromCache($cacheDir . '/moltiplicatori_prezzo.json', 'IdMoltiplicatorePrezzo', ['Codice', 'Descrizione'], $term);
case 'AnagraficaCertestObject':
return resolveIdsFromCache($cacheDir . '/anagrafica_certest_object.json', 'IdAnagrafica', ['Codice', 'NomeAnagrafica'], $term);
case 'AnagraficaCertestService':
return resolveIdsFromCache($cacheDir . '/anagrafica_certest_service.json', 'IdAnagrafica', ['Codice', 'NomeAnagrafica'], $term);
default:
// ClienteResponsabile (per-cliente) e altri: non risolvibili globalmente
return null;
}
}
/**
* @return array<array{0:int,1:int}> lista di [idclient, idResponsabile]
*/
function resolveResponsabilePairs(string $cacheDir, string $term): array
{
$term = mb_strtolower(trim($term));
if ($term === '') return [];
$pairs = [];
foreach (glob($cacheDir . '/cliente_responsabili_*.json') ?: [] as $file) {
if (!preg_match('/cliente_responsabili_(\d+)\.json$/', $file, $m)) continue;
$cid = (int)$m[1];
$data = json_decode(file_get_contents($file), true);
foreach (($data['Responsabili'] ?? []) as $r) {
$name = mb_strtolower((string)($r['Nominativo'] ?? ''));
if ($name !== '' && mb_strpos($name, $term) !== false && isset($r['IdClienteResponsabile'])) {
$pairs[] = [$cid, (int)$r['IdClienteResponsabile']];
}
}
}
return $pairs;
}
/** Meta di un mapping (data_type, field_id), con cache statica. */
function mappingMeta(PDO $pdo, int $mappingId): array
{
static $cache = [];
if (!array_key_exists($mappingId, $cache)) {
$st = $pdo->prepare("SELECT data_type, field_id FROM template_mapping WHERE id = ?");
$st->execute([$mappingId]);
$cache[$mappingId] = $st->fetch(PDO::FETCH_ASSOC) ?: ['data_type' => 'Testo', 'field_id' => null];
}
return $cache[$mappingId];
}
function resolveCustomFieldValueIds(string $cacheDir, $fieldId, string $term): array
{
$fieldId = (int)$fieldId;
$term = mb_strtolower(trim($term));
if ($fieldId <= 0 || $term === '') return [];
$ids = [];
foreach (cacheItems($cacheDir . "/customfield_{$fieldId}.json") as $it) {
$label = mb_strtolower((string)($it['Valore'] ?? ''));
if (mb_strpos($label, $term) !== false && isset($it['IdCustomFieldsValue'])) {
$ids[] = (int)$it['IdCustomFieldsValue'];
}
}
return array_values(array_unique($ids));
}
// ── Costruzione WHERE ──────────────────────────────────────────────────────
$conds = ['d.templateid = ?', 'd.status = ?'];
$params = [$templateId, $status];
$needUserJoin = false;
if (!$showAll) {
$conds[] = 'd.user_id = ?';
$params[] = $userId;
}
// Scope su un singolo import (come imported.php ?importref=)
if ($importref !== '') {
$conds[] = 'd.importreferencecode = ?';
$params[] = $importref;
}
/** Aggiunge una condizione "colonna id IN (lista risolta)". */
function addIdInCondition(array &$conds, array &$params, string $col, array $ids): void
{
if (empty($ids)) {
$conds[] = '1 = 0'; // termine dato ma nessun match → zero risultati
return;
}
$ph = implode(',', array_fill(0, count($ids), '?'));
$conds[] = "d.`{$col}` IN ($ph)";
foreach ($ids as $id) $params[] = (int)$id;
}
foreach ($filters as $f) {
$term = trim((string)($f['term'] ?? ''));
if ($term === '') continue;
$type = (string)($f['type'] ?? '');
$key = (string)($f['key'] ?? '');
$like = '%' . $term . '%';
switch ($type) {
case 'detail':
case 'main_field':
$mappingId = (int)$key;
if ($mappingId <= 0) break;
$mm = mappingMeta($pdo, $mappingId);
if (($mm['data_type'] ?? '') === 'SceltaMultipla') {
// Dropdown: field_value = ID valore → risolvi label→id, match IN (= non LIKE)
$vids = resolveCustomFieldValueIds($cacheDir, $mm['field_id'], $term);
if (empty($vids)) {
$conds[] = '1 = 0';
break;
}
$inph = implode(',', array_fill(0, count($vids), '?'));
$conds[] = "EXISTS (SELECT 1 FROM import_data_details x
WHERE x.id = d.iddatadb AND x.mapping_id = ? AND x.field_value IN ($inph))";
$params[] = $mappingId;
foreach ($vids as $vid) $params[] = $vid;
} else {
// Testo / Data: match testuale
$conds[] = "EXISTS (SELECT 1 FROM import_data_details x
WHERE x.id = d.iddatadb AND x.mapping_id = ? AND x.field_value LIKE ?)";
$params[] = $mappingId;
$params[] = $like;
}
break;
case 'idclient':
addIdInCondition($conds, $params, 'idclient', resolveClientIdsByTerm($cacheDir, $term));
break;
case 'cliente_fornitore_id':
addIdInCondition($conds, $params, 'cliente_fornitore_id', resolveClientIdsByTerm($cacheDir, $term));
break;
case 'tested_component':
$conds[] = 'd.tested_component LIKE ?';
$params[] = $like;
break;
case 'status':
// Il dropdown invia direttamente il codice ('i'/'P'/'l').
if (in_array($term, ['i', 'P', 'l'], true)) {
$conds[] = 'd.status = ?';
$params[] = $term;
} else {
$conds[] = '1 = 0';
}
break;
case 'fixed':
$col = $fixedAliasMap[$key] ?? null;
if ($col === null) break;
if ($key === 'ConsegnaRichiesta') {
// Data: match testuale sul valore data (es. "2026-03").
$conds[] = "CAST(d.`{$col}` AS CHAR) LIKE ?";
$params[] = $like;
} elseif ($key === 'ClienteResponsabile') {
// Match sulla coppia (idclient, id responsabile).
$pairs = resolveResponsabilePairs($cacheDir, $term);
if (empty($pairs)) {
$conds[] = '1 = 0';
break;
}
$rowvals = implode(',', array_fill(0, count($pairs), '(?,?)'));
$conds[] = "(d.idclient, d.`{$col}`) IN ($rowvals)";
foreach ($pairs as $pr) {
$params[] = $pr[0];
$params[] = $pr[1];
}
} else {
$ids = resolveFixedIds($cacheDir, $key, $term);
if ($ids === null) break; // non risolvibile → non filtrare (niente LIKE su id)
addIdInCondition($conds, $params, $col, $ids);
}
break;
case 'static':
if ($key === 'user_name') {
$needUserJoin = true;
$conds[] = "CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,'')) LIKE ?";
$params[] = $like;
} elseif (in_array($key, $staticCols, true)) {
$conds[] = "d.`{$key}` LIKE ?";
$params[] = $like;
}
break;
default:
break; // tracking/awb ecc. → non filtrabili
}
}
$whereSql = implode(' AND ', $conds);
$joinSql = $needUserJoin ? 'LEFT JOIN auth_users u ON d.user_id = u.id' : '';
try {
// Totale + pagine
$countStmt = $pdo->prepare("SELECT COUNT(*) FROM datadb d {$joinSql} WHERE {$whereSql}");
$countStmt->execute($params);
$total = (int)$countStmt->fetchColumn();
$totalPages = max(1, (int)ceil($total / $perPage));
if ($page > $totalPages) $page = $totalPages;
$offset = ($page - 1) * $perPage;
// Pagina corrente
$pageStmt = $pdo->prepare("
SELECT d.iddatadb
FROM datadb d {$joinSql}
WHERE {$whereSql}
ORDER BY d.excelrow ASC, d.iddatadb ASC
LIMIT {$perPage} OFFSET {$offset}
");
$pageStmt->execute($params);
$pageIds = array_map('intval', $pageStmt->fetchAll(PDO::FETCH_COLUMN));
$config = buildGridConfig($pdo, $templateId);
$rows = buildGridRows($pdo, $pageIds, $config);
$out = [
'success' => true,
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'total_pages' => $totalPages,
'rows' => $rows,
];
// Tutti gli id del match (per "seleziona tutti"), solo su richiesta
if ($wantAllIds) {
$allStmt = $pdo->prepare("
SELECT d.iddatadb FROM datadb d {$joinSql}
WHERE {$whereSql}
ORDER BY d.excelrow ASC, d.iddatadb ASC
LIMIT " . FILTER_ALLIDS_CAP . "
");
$allStmt->execute($params);
$out['all_ids'] = array_map('intval', $allStmt->fetchAll(PDO::FETCH_COLUMN));
$out['all_ids_capped'] = $total > FILTER_ALLIDS_CAP;
}
echo json_encode($out);
} catch (Exception $e) {
error_log('filter_records error: ' . $e->getMessage());
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+7 -24
View File
@@ -16,11 +16,9 @@ try {
exit;
}
$api = VisualLimsApiClient::getInstance(); // also loads dotenv as a side-effect
$api = VisualLimsApiClient::getInstance();
$webOnly = isset($_GET['web_only']) ? (int)$_GET['web_only'] : 1;
$refresh = isset($_GET['refresh']) ? (int)$_GET['refresh'] : 0;
$simulate = ($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true';
$filterString = "Matrice/IdMatrice eq $idMatrice";
@@ -34,34 +32,19 @@ try {
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$full_url = $base_url . $endpoint;
// Cache file matches the one warm_cache.php writes (web_only=1 is what the analysis modal asks for).
// Caching skipped in simulate mode to avoid polluting real-data cache files.
$cache_file = __DIR__ . '/cache/analisi_matrice_' . $idMatrice . ($webOnly === 1 ? '' : '_all') . '.json';
$data = null;
$fromCache = false;
file_put_contents(__DIR__ . '/last_url_analisi.txt', $full_url . PHP_EOL, FILE_APPEND);
if (!$simulate && !$refresh && file_exists($cache_file) && (time() - filemtime($cache_file) < 3600)) { // 1 ora
$data = json_decode(file_get_contents($cache_file), true);
$fromCache = is_array($data);
}
$data = $api->get($endpoint, []);
if (!$fromCache) {
$data = $api->get($endpoint, []);
if (!$simulate) {
file_put_contents($cache_file, json_encode(['value' => $data['value'] ?? []], JSON_UNESCAPED_UNICODE));
}
}
file_put_contents(
__DIR__ . '/analisi_by_matrice_response.json',
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
if ($debug === 1) {
file_put_contents(__DIR__ . '/last_url_analisi.txt', $full_url . PHP_EOL, FILE_APPEND);
file_put_contents(
__DIR__ . '/analisi_by_matrice_response.json',
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
echo json_encode([
'endpoint' => $endpoint,
'full_url' => $full_url,
'from_cache' => $fromCache,
'raw_response' => $data
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
+1 -1
View File
@@ -30,7 +30,7 @@ try {
// Parametri OData
$params = [
'$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo',
'$select' => 'IdCliente,Nominativo,CodiceCliente',
'$orderby' => 'Nominativo asc'
];
-127
View File
@@ -1,127 +0,0 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
// Disable PHP error display
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance(); // also loads dotenv
// In simulate mode: return fake clients built from idclient values already in datadb.
// This ensures client dropdowns auto-select the correct row idclient, which in turn
// triggers the ClienteResponsabile select to populate via the mock.
if (($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true') {
require_once __DIR__ . '/class/db-functions.php';
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->query("SELECT DISTINCT idclient FROM datadb WHERE idclient IS NOT NULL AND idclient > 0 ORDER BY idclient ASC");
$ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
$fakeClients = array_map(fn($id) => [
'IdCliente' => (int) $id,
'Nominativo' => "Cliente Simulato {$id}",
'CodiceCliente' => "SIM_{$id}",
], $ids);
echo json_encode(['value' => $fakeClients]);
exit;
}
// ── DEBUG MODE ───────────────────────────────────────────────────────
// Chiamata: get_clienti.php?debug=1
// Bypassa cache e $select per vedere TUTTI i campi grezzi restituiti
// dall'API per l'entità Cliente. Usare solo per ispezione manuale,
// NON lasciare linkato/usato in produzione dal frontend.
if (($_GET['debug'] ?? '') === '1') {
$debugParams = [
'$top' => 5,
'$orderby' => 'Nominativo asc'
// Nessun $select: chiediamo tutti i campi disponibili
];
$debugEndpoint = "Cliente?" . http_build_query($debugParams);
$debugData = $api->get($debugEndpoint);
$debugClients = $debugData['value'] ?? $debugData;
echo json_encode([
'mode' => 'debug - no $select, cache bypassed',
'top_level_keys' => is_array($debugData) ? array_keys($debugData) : null,
'fields_found_in_first_record' => isset($debugClients[0]) ? array_keys($debugClients[0]) : null,
'sample_records' => $debugClients,
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
// ── FINE DEBUG MODE ──────────────────────────────────────────────────
// Parametri OData
$params = [
'$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo',
'$orderby' => 'Nominativo asc'
];
// Costruisce query string con encoding corretto
$queryString = http_build_query($params);
// Componi endpoint finale
$endpoint = "Cliente?$queryString";
// Funzione per eseguire la chiamata con retry
function makeApiRequest($api, $endpoint, $maxRetries = 3)
{
for ($retry = 0; $retry < $maxRetries; $retry++) {
try {
// Tenta la chiamata API
$data = $api->get($endpoint);
// Salva risposta per debug
file_put_contents(__DIR__ . '/clienti_response.json', json_encode($data, JSON_PRETTY_PRINT));
return $data;
} catch (Exception $e) {
$errorMessage = $e->getMessage();
// Controlla se l'errore è legato all'autenticazione (HTTP 400 con messaggio specifico)
if (strpos($errorMessage, 'HTTP 400') !== false && strpos($errorMessage, 'Cannot persist the object') !== false) {
// Forza il refresh del token
try {
// Assumi che VisualLimsApiClient abbia un metodo per il refresh del token
$api->refreshToken(); // Da implementare in VisualLimsApiClient se non esiste
error_log("Tentativo $retry: Refresh token eseguito per endpoint $endpoint");
} catch (Exception $refreshEx) {
error_log("Errore durante il refresh del token: " . $refreshEx->getMessage());
throw new Exception("Impossibile eseguire il refresh del token: " . $refreshEx->getMessage());
}
// Ritarda leggermente prima del retry
usleep(500000); // 500ms
continue;
}
// Altri errori non gestiti dal retry
throw $e;
}
}
throw new Exception("Massimo numero di tentativi raggiunto per $endpoint");
}
// Cache file (1 hour TTL)
$cacheFile = __DIR__ . '/cache/clienti.json';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
readfile($cacheFile);
exit;
}
// Esegui la chiamata con retry
$data = makeApiRequest($api, $endpoint);
$json = json_encode($data);
if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true);
file_put_contents($cacheFile, $json);
echo $json;
} catch (Exception $e) {
http_response_code(500);
$errorResponse = [
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
];
error_log("Errore in get_clienti.php: " . json_encode($errorResponse));
echo json_encode($errorResponse);
}
-102
View File
@@ -1,102 +0,0 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
// Disable PHP error display
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance(); // also loads dotenv
// In simulate mode: return fake clients built from idclient values already in datadb.
// This ensures client dropdowns auto-select the correct row idclient, which in turn
// triggers the ClienteResponsabile select to populate via the mock.
if (($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true') {
require_once __DIR__ . '/class/db-functions.php';
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->query("SELECT DISTINCT idclient FROM datadb WHERE idclient IS NOT NULL AND idclient > 0 ORDER BY idclient ASC");
$ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
$fakeClients = array_map(fn($id) => [
'IdCliente' => (int) $id,
'Nominativo' => "Cliente Simulato {$id}",
'CodiceCliente' => "SIM_{$id}",
], $ids);
echo json_encode(['value' => $fakeClients]);
exit;
}
// Parametri OData
$params = [
'$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo',
'$orderby' => 'Nominativo asc'
];
// Costruisce query string con encoding corretto
$queryString = http_build_query($params);
// Componi endpoint finale
$endpoint = "Cliente?$queryString";
// Funzione per eseguire la chiamata con retry
function makeApiRequest($api, $endpoint, $maxRetries = 3)
{
for ($retry = 0; $retry < $maxRetries; $retry++) {
try {
// Tenta la chiamata API
$data = $api->get($endpoint);
// Salva risposta per debug
file_put_contents(__DIR__ . '/clienti_response.json', json_encode($data, JSON_PRETTY_PRINT));
return $data;
} catch (Exception $e) {
$errorMessage = $e->getMessage();
// Controlla se l'errore è legato all'autenticazione (HTTP 400 con messaggio specifico)
if (strpos($errorMessage, 'HTTP 400') !== false && strpos($errorMessage, 'Cannot persist the object') !== false) {
// Forza il refresh del token
try {
// Assumi che VisualLimsApiClient abbia un metodo per il refresh del token
$api->refreshToken(); // Da implementare in VisualLimsApiClient se non esiste
error_log("Tentativo $retry: Refresh token eseguito per endpoint $endpoint");
} catch (Exception $refreshEx) {
error_log("Errore durante il refresh del token: " . $refreshEx->getMessage());
throw new Exception("Impossibile eseguire il refresh del token: " . $refreshEx->getMessage());
}
// Ritarda leggermente prima del retry
usleep(500000); // 500ms
continue;
}
// Altri errori non gestiti dal retry
throw $e;
}
}
throw new Exception("Massimo numero di tentativi raggiunto per $endpoint");
}
// Cache file (1 hour TTL)
$cacheFile = __DIR__ . '/cache/clienti.json';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
readfile($cacheFile);
exit;
}
// Esegui la chiamata con retry
$data = makeApiRequest($api, $endpoint);
$json = json_encode($data);
if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true);
file_put_contents($cacheFile, $json);
echo $json;
} catch (Exception $e) {
http_response_code(500);
$errorResponse = [
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
];
error_log("Errore in get_clienti.php: " . json_encode($errorResponse));
echo json_encode($errorResponse);
}
@@ -0,0 +1,63 @@
<?php
require_once 'include/headscript.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Invalid request method']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$template_id = isset($input['template_id']) ? (int)$input['template_id'] : 0;
$code = isset($input['code']) ? trim($input['code']) : '';
if ($template_id <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Missing or invalid template_id']);
exit;
}
if ($code === '') {
http_response_code(400);
echo json_encode(['error' => 'Missing code']);
exit;
}
/*
* TODO: Replace this block with your real API call.
* Expected response for import_json.php:
* {
* "success": true,
* "reference": "CODE123",
* "json": { ... real JSON payload ... }
* }
*/
$sampleJson = [
'data' => [[
'type' => 'trf_data_request',
'id' => $code,
'attributes' => [
'trf_type' => 'apparel',
'service_required' => 'regular',
'submitter_information' => [
'submitter_type' => 'supplier'
]
]
]]
];
echo json_encode([
'success' => true,
'reference' => $code,
'json' => $sampleJson
]);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
+143
View File
@@ -0,0 +1,143 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json; charset=utf-8');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
$idCliente = isset($_GET['id_cliente']) ? (int)$_GET['id_cliente'] : 0;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 3;
$signedStatus = trim($_GET['signed_status'] ?? 'all');
if ($idCliente <= 0) {
throw new Exception("Parametro id_cliente mancante o non valido.");
}
/*
* Allowed limits only.
* This prevents risky wide queries on the live LIMS.
*/
$allowedLimits = [1, 3, 5, 10];
if (!in_array($limit, $allowedLimits, true)) {
$limit = 3;
}
/*
* Allowed signature filters.
*/
$allowedSignedStatuses = ['all', 'signed', 'not_signed'];
if (!in_array($signedStatus, $allowedSignedStatuses, true)) {
$signedStatus = 'all';
}
/*
* Base filter by customer.
* We already verified that Rapporto can expand Cliente and returns Cliente.IdCliente.
*/
$filters = [
"Cliente/IdCliente eq {$idCliente}"
];
if ($signedStatus === 'signed') {
$filters[] = "Firmato eq true";
}
if ($signedStatus === 'not_signed') {
$filters[] = "Firmato eq false";
}
$filter = implode(' and ', $filters);
/*
* Important:
* - $top limits the number of reports.
* - $orderby=Data desc gets the latest reports first.
* - $expand=RapportiFiles retrieves only the PDF file metadata, not the binary PDF.
*/
$params = [
'$filter' => $filter,
'$select' => 'IdRapporto,CodiceRapporto,Data,Versione,Firmato,DataStampa',
'$expand' => 'RapportiFiles',
'$orderby' => 'Data desc',
'$top' => $limit
];
$endpoint = "Rapporto?" . http_build_query($params);
file_put_contents(
__DIR__ . '/last_rapporti_cliente_endpoint.txt',
'[' . date('Y-m-d H:i:s') . '] ' . $endpoint . PHP_EOL,
FILE_APPEND
);
$data = $api->get($endpoint);
$items = $data['value'] ?? [];
if (!is_array($items)) {
$items = [];
}
$reports = [];
foreach ($items as $item) {
$rapportiFiles = $item['RapportiFiles'] ?? [];
$pdfFiles = [];
if (is_array($rapportiFiles)) {
foreach ($rapportiFiles as $file) {
$idRapportoFile = intval($file['IdRapportoFile'] ?? 0);
if ($idRapportoFile > 0) {
$pdfFiles[] = [
'id_rapporto_file' => $idRapportoFile,
'file_name' => $file['FileName'] ?? null,
'categoria' => $file['Categoria'] ?? null,
'tipo_rapporto' => $file['TipoRapporto'] ?? null,
'download_url' => "download_rapporto_pdf.php?id_rapporto_file={$idRapportoFile}"
];
}
}
}
$reports[] = [
'id_rapporto' => $item['IdRapporto'] ?? null,
'codice_rapporto' => $item['CodiceRapporto'] ?? null,
'data' => $item['Data'] ?? null,
'data_stampa' => $item['DataStampa'] ?? null,
'versione' => $item['Versione'] ?? null,
'firmato' => $item['Firmato'] ?? null,
'pdf_files' => $pdfFiles
];
}
echo json_encode([
'success' => true,
'id_cliente' => $idCliente,
'limit' => $limit,
'signed_status' => $signedStatus,
'endpoint' => $endpoint,
'count' => count($reports),
'reports' => $reports
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log.txt',
date('Y-m-d H:i:s') . ' - get_rapporti_cliente.php - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+46 -7
View File
@@ -17,6 +17,7 @@ try {
// rapporto_by_codice_expand_step.php?codice=2541111&step=files_campioni
$codiceRapporto = trim($_GET['codice'] ?? '');
// Safe step mode: default is base, but allows controlled read-only steps
$step = trim($_GET['step'] ?? 'base');
if ($codiceRapporto === '') {
@@ -25,10 +26,9 @@ try {
$allowedSteps = [
'base' => '',
'files' => 'RapportiFiles',
'allegati' => 'RapportiAllegati',
'campioni' => 'CampioniDatiRapporto',
'files_campioni' => 'RapportiFiles,CampioniDatiRapporto'
'files' => 'RapportiFiles,Cliente',
'cliente' => 'Cliente'
];
if (!array_key_exists($step, $allowedSteps)) {
@@ -37,7 +37,8 @@ try {
// Escape OData per eventuali apostrofi
$codiceRapportoSafe = str_replace("'", "''", $codiceRapporto);
// Safe version of codice rapporto for filenames
$codiceRapportoFileSafe = preg_replace('/[^a-zA-Z0-9_-]/', '_', $codiceRapporto);
/*
* STEP 1 - Trova IdRapporto partendo da CodiceRapporto.
* Query leggera, con $select e $top=1.
@@ -107,15 +108,43 @@ try {
$detailData = $api->get($detailEndpoint);
$pdfFiles = [];
if ($step === 'files') {
$rapportiFiles = $detailData['RapportiFiles'] ?? [];
if (is_array($rapportiFiles)) {
foreach ($rapportiFiles as $file) {
$idRapportoFile = intval($file['IdRapportoFile'] ?? 0);
if ($idRapportoFile > 0) {
$pdfFiles[] = [
'id_rapporto_file' => $idRapportoFile,
'file_name' => $file['FileName'] ?? null,
'categoria' => $file['Categoria'] ?? null,
'tipo_rapporto' => $file['TipoRapporto'] ?? null,
'download_endpoint' => "MediaFile/DownloadStream?objectType=RapportoFile&propertyName=FileContent&objectKey={$idRapportoFile}"
];
}
}
}
}
$clienteData = null;
if ($step === 'cliente' || $step === 'files') {
$clienteData = $detailData['Cliente'] ?? null;
}
file_put_contents(
__DIR__ . "/rapporto_codice_{$codiceRapportoSafe}_{$step}.json",
__DIR__ . "/rapporto_codice_{$codiceRapportoFileSafe}_{$step}.json",
json_encode([
'search' => $searchData,
'detail' => $detailData
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
);
echo json_encode([
$response = [
'success' => true,
'codice_rapporto' => $codiceRapporto,
'id_rapporto' => $rapportoId,
@@ -124,7 +153,17 @@ try {
'detail_endpoint' => $detailEndpoint,
'rapporto_base' => $rapportoBase,
'data' => $detailData
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
];
if ($step === 'files') {
$response['pdf_files'] = $pdfFiles;
}
if ($step === 'cliente' || $step === 'files') {
$response['cliente'] = $clienteData;
}
echo json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log.txt',
-844
View File
@@ -1,844 +0,0 @@
/**
* gridFilter.js — Selezione righe + filtri colonna a scomparsa per imported.php
* Dipende da window.gridRenderer (gridRenderer.js). Includere DOPO gridRenderer.js.
*/
(function () {
"use strict";
let filtersActive = false;
let restricted = false;
const selected = new Set(); // iddatadb selezionati
const colFilters = {}; // colKey -> testo filtro
let restrictedIds = null; // Set di iddatadb visibili quando ristretto
function R() {
return window.gridRenderer;
}
function data() {
return R().getData();
}
function meta() {
return R().getMeta();
}
// ── Filtro SERVER-SIDE (paginato, come la lista normale ma filtrata) ──────
// Il filtro gira su TUTTO il template; il server restituisce UNA PAGINA di righe
// filtrate + il totale, e navighiamo le pagine del set filtrato via AJAX.
const TEMPLATE_ID =
new URLSearchParams(location.search).get("id") || (meta().templateId ?? "");
const SHOW_ALL =
new URLSearchParams(location.search).get("all_users") === "1";
const IMPORTREF =
new URLSearchParams(location.search).get("importref") || "";
const FILTER_PER_PAGE =
parseInt(new URLSearchParams(location.search).get("limit"), 10) || 20;
let serverFiltered = false;
let filterPage = 1;
let filterTotalPages = 1;
let filterTotal = 0;
let originalData = null; // snapshot della pagina originale (per ripristino)
let filterReqSeq = 0;
let showingSelection = false; // true quando mostriamo l'UNIONE dei selezionati
let pagerFn = null; // paginazione corrente (applyServerFilter | showSelectedRecords)
function collectActiveFilters() {
const cols = meta().columns || [];
const out = [];
for (const [key, term] of Object.entries(colFilters)) {
if (!term || !String(term).trim()) continue;
const col = cols.find((c) => String(c.key) === String(key));
if (!col) continue;
out.push({ key: String(key), type: col.type, term: String(term).trim() });
}
return out;
}
// Sostituisce il dataset del renderer in-place e ridisegna.
function swapData(rows) {
const arr = data();
arr.length = 0;
(rows || []).forEach((r) => arr.push(r));
R().renderVisibleRows();
}
function hidePagination(hide) {
// Durante il filtro la paginazione server (per pagina, via URL) è fuorviante.
document.querySelectorAll(".pager-bar").forEach((el) => {
el.style.display = hide ? "none" : "";
});
}
// Pager del set filtrato (Prec / pag X di Y / Succ) nella toolbar.
function updateFilterPager() {
const el = document.getElementById("filterPager");
if (!el) return;
if (!serverFiltered) {
el.style.display = "none";
el.innerHTML = "";
return;
}
el.style.display = "inline-flex";
const label = showingSelection ? "selezionati" : "trovati";
el.innerHTML =
`<button type="button" class="btn btn-outline-secondary btn-sm" id="filterPrevBtn" ${filterPage <= 1 ? "disabled" : ""}></button>` +
`<span style="font-size:12px;color:#333;">${filterTotal} ${label} · pag ${filterPage}/${filterTotalPages}</span>` +
`<button type="button" class="btn btn-outline-secondary btn-sm" id="filterNextBtn" ${filterPage >= filterTotalPages ? "disabled" : ""}></button>`;
const go = pagerFn || applyServerFilter;
const prev = document.getElementById("filterPrevBtn");
const next = document.getElementById("filterNextBtn");
if (prev) prev.addEventListener("click", () => go(filterPage - 1));
if (next) next.addEventListener("click", () => go(filterPage + 1));
}
function restoreOriginal() {
serverFiltered = false;
filterPage = 1;
filterTotalPages = 1;
filterTotal = 0;
if (originalData) swapData(originalData);
updateFilterPager();
hidePagination(false);
updateToolbar();
}
async function applyServerFilter(page = 1) {
const active = collectActiveFilters();
if (active.length === 0) {
if (serverFiltered) restoreOriginal();
return;
}
if (!originalData) originalData = [...data()]; // snapshot pagina 1 originale
const seq = ++filterReqSeq;
try {
const resp = await fetch("filter_records.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
template_id: parseInt(TEMPLATE_ID, 10),
all_users: SHOW_ALL,
importref: IMPORTREF,
status: "i",
filters: active,
page: Math.max(1, page),
per_page: FILTER_PER_PAGE,
}),
});
const json = await resp.json();
if (seq !== filterReqSeq) return; // risposta obsoleta: ignora
if (!json.success) {
console.error("[gridFilter] filtro:", json.message);
return;
}
serverFiltered = true;
showingSelection = false;
pagerFn = applyServerFilter;
filterPage = json.page;
filterTotalPages = json.total_pages;
filterTotal = json.total;
swapData(json.rows || []);
updateFilterPager();
hidePagination(true);
updateToolbar();
} catch (e) {
console.error("[gridFilter] fetch filtro fallita", e);
}
}
// ── Mostra i SELEZIONATI (unione tra filtri diversi) ──────────────────
// La selezione si accumula tra filtri; questo carica dal server TUTTI i record
// selezionati (per id) e li mostra, paginati, a prescindere dai filtri correnti.
async function showSelectedRecords(page = 1) {
const ids = [...selected];
if (!ids.length) {
showingSelection = false;
updateToolbar();
return;
}
pagerFn = showSelectedRecords;
try {
const resp = await fetch("filter_records.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
template_id: parseInt(TEMPLATE_ID, 10),
all_users: SHOW_ALL,
importref: IMPORTREF,
status: "i",
ids: ids,
page: Math.max(1, page),
per_page: FILTER_PER_PAGE,
}),
});
const json = await resp.json();
if (!json.success) {
console.error("[gridFilter] mostra selezionati:", json.message);
return;
}
if (!originalData) originalData = [...data()];
showingSelection = true;
serverFiltered = true;
filterPage = json.page;
filterTotalPages = json.total_pages;
filterTotal = json.total;
swapData(json.rows || []);
updateFilterPager();
hidePagination(true);
updateToolbar();
} catch (e) {
console.error("[gridFilter] mostra selezionati fallita", e);
}
}
// Tumbler "Mostra selezionati" / "Torna al filtro".
function toggleShowSelected() {
if (!showingSelection) {
if (!selected.size) return;
showSelectedRecords(1);
} else {
exitSelectionView();
}
}
// Torna dal "mostra selezionati" al filtro corrente (o alla pagina originale).
function exitSelectionView() {
showingSelection = false;
pagerFn = applyServerFilter;
if (collectActiveFilters().length) {
applyServerFilter(1);
} else {
restoreOriginal();
}
updateToolbar();
}
// Deseleziona tutto.
function clearSelection() {
selected.clear();
document
.querySelectorAll("#gridRowContainer .grid-row.row-selected")
.forEach((el) => el.classList.remove("row-selected"));
document
.querySelectorAll(".filter-row-checkbox")
.forEach((cb) => (cb.checked = false));
const sa = document.getElementById("filterSelectAll");
if (sa) sa.checked = false;
if (showingSelection) {
exitSelectionView();
} else {
updateToolbar();
}
}
// "Seleziona tutti i filtrati": chiede al server TUTTI gli id del match e li seleziona.
async function selectAllMatching(on) {
if (!serverFiltered) return;
const active = collectActiveFilters();
try {
const resp = await fetch("filter_records.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
template_id: parseInt(TEMPLATE_ID, 10),
all_users: SHOW_ALL,
importref: IMPORTREF,
status: "i",
filters: active,
page: filterPage,
per_page: FILTER_PER_PAGE,
want_all_ids: true,
}),
});
const json = await resp.json();
if (!json.success) return;
(json.all_ids || []).forEach((id) => (on ? selected.add(id) : selected.delete(id)));
document
.querySelectorAll("#gridRowContainer .grid-row")
.forEach((rowEl) => {
rowEl.classList.toggle("row-selected", on);
const cb = rowEl.querySelector(".filter-row-checkbox");
if (cb) cb.checked = on;
});
updateToolbar();
if (json.all_ids_capped) {
alert("Selezione limitata a " + (json.all_ids || []).length + " record.");
}
} catch (e) {
console.error("[gridFilter] select-all fallita", e);
}
}
function syncFilterRow() {
const fr = document.getElementById("gridFilterRow");
const header = document.getElementById("gridHeaderContainer");
if (!fr || !header) return;
const actHeader = header.querySelector(".button-header");
const actCell = fr.querySelector(".filter-actions-cell");
if (actHeader && actCell) {
const aw = actHeader.offsetWidth;
actCell.style.flex = `0 0 ${aw}px`;
actCell.style.minWidth = `${aw}px`;
}
const headerCells = header.querySelectorAll(
".grid-header:not(.button-header)",
);
const filterCells = fr.querySelectorAll(
".grid-cell:not(.filter-actions-cell)",
);
headerCells.forEach((hc, i) => {
const fc = filterCells[i];
if (fc) fc.style.flex = `0 0 ${hc.offsetWidth}px`;
});
}
function syncStickyLeft() {
const btnHeader = document.querySelector(
"#gridHeaderContainer .button-header",
);
if (!btnHeader) return;
const w1 = btnHeader.offsetWidth; // larghezza reale colonna Actions
const fixRow = (rowEl) => {
if (!rowEl) return;
const cells = rowEl.querySelectorAll(
":scope > .grid-cell, :scope > .grid-header",
);
const c2 = cells[1];
const c3 = cells[2];
if (c2 && getComputedStyle(c2).position === "sticky") {
c2.style.left = `${w1}px`;
}
if (c3 && getComputedStyle(c3).position === "sticky") {
c3.style.left = `${w1 + (c2 ? c2.offsetWidth : 0)}px`;
}
};
fixRow(document.getElementById("gridHeaderContainer"));
document.querySelectorAll(".grid-top").forEach(fixRow);
document
.querySelectorAll("#gridRowContainer .grid-row")
.forEach(fixRow);
fixRow(document.getElementById("gridFilterRow"));
}
let _headerObs = null;
let _rowObs = null;
let _syncRaf = null;
function reconnectObservers() {
const header = document.getElementById("gridHeaderContainer");
const rows = document.getElementById("gridRowContainer");
if (_headerObs && header)
_headerObs.observe(header, {
attributes: true,
attributeFilter: ["style"],
subtree: true,
});
if (_rowObs && rows) _rowObs.observe(rows, { childList: true });
}
function scheduleSync() {
if (_syncRaf) return;
_syncRaf = requestAnimationFrame(() => {
_syncRaf = null;
if (_headerObs) _headerObs.disconnect();
if (_rowObs) _rowObs.disconnect();
syncStickyLeft();
syncFilterRow();
reconnectObservers();
});
}
function hookGridSync() {
_headerObs = new MutationObserver(scheduleSync);
_rowObs = new MutationObserver(scheduleSync);
reconnectObservers();
scheduleSync(); // primo allineamento
}
function rowMatchesFilters(row) {
if (serverFiltered) return true;
for (const [key, term] of Object.entries(colFilters)) {
if (!term) continue;
const t = term.toLowerCase();
const val = getRowColValue(row, key);
if (!String(val).toLowerCase().includes(t)) return false;
}
return true;
}
function getRowColValue(row, key) {
const cols = meta().columns || [];
const col = cols.find((c) => String(c.key) === String(key));
if (!col) return "";
switch (col.type) {
case "detail":
case "main_field":
return row.details?.[String(key)] ?? "";
case "fixed":
return row.fixedFields?.[key] ?? "";
case "idclient":
return row.idclient ?? "";
case "cliente_fornitore_id":
return row.cliente_fornitore_id ?? "";
case "tested_component":
return row.tested_component ?? "";
case "static":
return row[key] ?? "";
case "status":
return row.status ?? "";
default:
return row[key] ?? "";
}
}
// ── Applica visibilità righe (filtri + restrizione) ──
function applyVisibility() {
const rows = document.querySelectorAll("#gridRowContainer .grid-row");
rows.forEach((rowEl) => {
const id = parseInt(rowEl.dataset.id, 10);
const row = data().find((r) => String(r.iddatadb) === String(id));
let show = true;
if (row && !rowMatchesFilters(row)) show = false;
if (restricted && restrictedIds && !restrictedIds.has(id))
show = false;
rowEl.style.display = show ? "" : "none";
});
window.dispatchEvent(new Event("resize")); // ricalcola scrollbar top
}
// ── Lista iddatadb attualmente visibili (per propagazione/export ecc.) ──
window.getVisibleGridIds = function () {
if (serverFiltered) {
return data()
.map((r) => parseInt(r.iddatadb, 10))
.filter(Boolean);
}
const ids = [];
document
.querySelectorAll("#gridRowContainer .grid-row")
.forEach((el) => {
if (el.style.display !== "none")
ids.push(parseInt(el.dataset.id, 10));
});
return ids.filter(Boolean);
};
// ── Righe target per operazioni bulk (propaga/export/clona/save) ──
// Regola: filtri attivi + selezione → solo selezionate;
// filtri attivi senza selezione → solo visibili;
// filtri non attivi → null (= tutte, comportamento originale).
window.getTargetGridIds = function () {
if (!filtersActive) return null; // null = nessun vincolo
if (selected.size > 0) return new Set([...selected]);
return new Set(window.getVisibleGridIds());
};
// Helper: true se l'id è tra i target (o se non c'è vincolo)
window.isTargetGridId = function (id) {
const t = window.getTargetGridIds();
if (t === null) return true;
return t.has(parseInt(id, 10));
};
// ── Toggle selezione riga ──
function toggleRow(id, on) {
id = parseInt(id, 10);
if (on) selected.add(id);
else selected.delete(id);
const rowEl = document.querySelector(
`#gridRowContainer .grid-row[data-id="${id}"]`,
);
if (rowEl) {
rowEl.classList.toggle("row-selected", selected.has(id));
const cb = rowEl.querySelector(".filter-row-checkbox");
if (cb) cb.checked = selected.has(id);
}
updateToolbar();
}
// ── Inietta checkbox nelle righe visibili ──
// ── Inietta checkbox dentro la cella Actions (già sticky) ──
function injectCheckboxes() {
document
.querySelectorAll("#gridRowContainer .grid-row")
.forEach((rowEl) => {
const btnCell = rowEl.querySelector(".button-cell");
if (!btnCell) return;
if (btnCell.querySelector(".filter-row-checkbox")) return;
const id = parseInt(rowEl.dataset.id, 10);
const wrap = document.createElement("label");
wrap.className = "filter-cb-wrap";
wrap.innerHTML = `<input type="checkbox" class="filter-row-checkbox" ${selected.has(id) ? "checked" : ""}>`;
btnCell.insertBefore(wrap, btnCell.firstChild);
if (selected.has(id)) rowEl.classList.add("row-selected");
});
}
function removeCheckboxes() {
document
.querySelectorAll(".filter-cb-wrap")
.forEach((el) => el.remove());
document
.querySelectorAll(".grid-row.row-selected")
.forEach((el) => el.classList.remove("row-selected"));
}
// ── Riga input filtri colonna (allineata alle larghezze reali) ──
// ── Riga input filtri colonna (allineata alle larghezze reali) ──
function injectFilterRow() {
if (document.getElementById("gridFilterRow")) return;
const top = document.getElementById("gridTopContainer");
const header = document.getElementById("gridHeaderContainer");
if (!top || !header) return;
const fr = document.createElement("div");
fr.className = "grid-row grid-filter-row";
fr.id = "gridFilterRow";
// cella allineata alla colonna Actions (sticky, legge larghezza reale)
const actHeader = header.querySelector(".button-header");
const actCell = document.createElement("div");
actCell.className = "grid-cell button-cell filter-actions-cell";
const aw = actHeader ? actHeader.offsetWidth : 220;
actCell.style.flex = `0 0 ${aw}px`;
actCell.style.minWidth = `${aw}px`;
actCell.innerHTML = `<label class="filter-cb-wrap" style="display:inline-flex;" title="Seleziona tutti i filtrati"><input type="checkbox" id="filterSelectAll"></label>`;
fr.appendChild(actCell);
// una cella per colonna, larghezza letta dall'header reale
const headerCells = header.querySelectorAll(
".grid-header:not(.button-header)",
);
(meta().columns || []).forEach((col, i) => {
const hc = headerCells[i];
const w = hc ? hc.offsetWidth : col.width || 150;
const cell = document.createElement("div");
cell.className = "grid-cell";
cell.style.flex = `0 0 ${w}px`;
if (col.type === "tracking" || col.type === "awb") {
// niente filtro
} else if (col.type === "status") {
// Status: dropdown (3 valori) invece del testo libero.
const cur = colFilters[col.key] || "";
const opt = (v, lbl) =>
`<option value="${v}" ${cur === v ? "selected" : ""}>${lbl}</option>`;
cell.innerHTML =
`<select class="filter-col-input" data-col-key="${col.key}" style="width:100%;padding:3px 6px;font-size:12px;border:1px solid #ced4da;border-radius:4px;">` +
opt("", "— tutti —") +
opt("i", "Imported") +
opt("P", "In Progress") +
opt("l", "To LIMS") +
`</select>`;
} else {
cell.innerHTML = `<input type="text" class="filter-col-input" data-col-key="${col.key}" placeholder="🔍 ${col.label || ""}" value="${colFilters[col.key] || ""}" style="width:100%;padding:3px 6px;font-size:12px;border:1px solid #ced4da;border-radius:4px;">`;
}
fr.appendChild(cell);
});
top.parentNode.insertBefore(fr, top);
scheduleSync(); // allinea larghezze filtri + offset sticky
}
function removeFilterRow() {
const fr = document.getElementById("gridFilterRow");
if (fr) fr.remove();
}
// ── Toolbar (bottoni contestuali) ──
function updateToolbar() {
const bar = document.getElementById("filterActionBar");
if (!bar) return;
const count = selected.size;
bar.style.display = filtersActive ? "inline-flex" : "none";
const cntEl = document.getElementById("filterSelCount");
if (cntEl) cntEl.textContent = count;
document.getElementById("filterDeleteBtn").disabled = count === 0;
const showBtn = document.getElementById("filterRestrictBtn");
showBtn.disabled = count === 0 && !showingSelection;
showBtn.innerHTML = showingSelection
? '<i class="fas fa-eye"></i> Torna al filtro'
: '<i class="fas fa-list-check"></i> Mostra selezionati';
const clearSelBtn = document.getElementById("filterClearSelBtn");
if (clearSelBtn) clearSelBtn.disabled = count === 0;
}
// ── Main toggle ──
function toggleFilters() {
filtersActive = !filtersActive;
const grid = document.getElementById("gridContainer");
const btn = document.getElementById("filtersToggleBtn");
if (filtersActive) {
grid.classList.add("filters-on");
btn.classList.add("active");
injectCheckboxes();
injectFilterRow();
} else {
grid.classList.remove("filters-on");
btn.classList.remove("active");
removeCheckboxes();
removeFilterRow();
// reset filtri colonna + ripristina la pagina originale
Object.keys(colFilters).forEach((k) => delete colFilters[k]);
if (serverFiltered) restoreOriginal();
}
updateToolbar();
}
// ── Batch delete ──
async function batchDelete() {
const ids = [...selected];
if (!ids.length) return;
if (
!confirm(
`Eliminare ${ids.length} righe dal database? Operazione irreversibile.`,
)
)
return;
const btn = document.getElementById("filterDeleteBtn");
btn.disabled = true;
const original = btn.innerHTML;
let ok = 0,
fail = 0;
for (const id of ids) {
try {
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${ok + fail + 1}/${ids.length}`;
const resp = await fetch("delete_record.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: id }),
});
const json = await resp.json();
if (json.success) {
ok++;
// rimuovi da gridData
const arr = data();
const idx = arr.findIndex(
(r) => String(r.iddatadb) === String(id),
);
if (idx !== -1) arr.splice(idx, 1);
selected.delete(id);
if (restrictedIds) restrictedIds.delete(id);
} else fail++;
} catch (e) {
fail++;
}
}
btn.innerHTML = original;
R().renderVisibleRows();
if (filtersActive) {
injectCheckboxes();
}
applyVisibility();
updateToolbar();
alert(`Eliminate: ${ok}${fail ? " — Falliti: " + fail : ""}`);
}
// ── Restringi / mostra tutti ──
function toggleRestrict() {
if (!restricted) {
if (!selected.size) return;
restrictedIds = new Set([...selected]);
restricted = true;
} else {
restricted = false;
restrictedIds = null;
}
applyVisibility();
updateToolbar();
}
// ── Costruzione UI toolbar (bottoni in cima) ──
function buildUI() {
const addBtn = document.getElementById("addRowBtn");
if (!addBtn) return;
// bottone Filters
const fBtn = document.createElement("button");
fBtn.type = "button";
fBtn.id = "filtersToggleBtn";
fBtn.className = "btn btn-outline-info btn-sm";
fBtn.style.flexShrink = "0";
fBtn.innerHTML = '<i class="fas fa-filter"></i> Filters';
addBtn.parentNode.insertBefore(fBtn, addBtn.nextSibling);
// barra azioni contestuali
const bar = document.createElement("span");
bar.id = "filterActionBar";
bar.style.cssText =
"display:none;align-items:center;gap:8px;flex-shrink:0;";
bar.innerHTML = `
<span id="filterPager" style="display:none;align-items:center;gap:6px;"></span>
<button type="button" id="filterClearBtn" class="btn btn-outline-secondary btn-sm"><i class="fas fa-times"></i> Pulisci filtri</button>
<span style="font-size:12px;color:#555;">Selezionate: <strong id="filterSelCount">0</strong></span>
<button type="button" id="filterRestrictBtn" class="btn btn-outline-primary btn-sm"><i class="fas fa-list-check"></i> Mostra selezionati</button>
<button type="button" id="filterClearSelBtn" class="btn btn-outline-secondary btn-sm"><i class="fas fa-square"></i> Deseleziona</button>
<button type="button" id="filterDeleteBtn" class="btn btn-outline-danger btn-sm"><i class="fas fa-trash"></i> Elimina selezionati</button>
`;
fBtn.parentNode.insertBefore(bar, fBtn.nextSibling);
fBtn.addEventListener("click", toggleFilters);
document
.getElementById("filterClearBtn")
.addEventListener("click", clearFilters);
document
.getElementById("filterDeleteBtn")
.addEventListener("click", batchDelete);
document
.getElementById("filterRestrictBtn")
.addEventListener("click", toggleShowSelected);
document
.getElementById("filterClearSelBtn")
.addEventListener("click", clearSelection);
}
// Svuota tutti i filtri di colonna (input + select) e torna alla pagina originale.
function clearFilters() {
Object.keys(colFilters).forEach((k) => delete colFilters[k]);
document
.querySelectorAll("#gridFilterRow .filter-col-input")
.forEach((el) => {
el.value = "";
});
if (serverFiltered) restoreOriginal();
}
// ── Event delegation ──
function attachEvents() {
// checkbox click
document.addEventListener("change", function (e) {
if (e.target.classList.contains("filter-row-checkbox")) {
const rowEl = e.target.closest(".grid-row");
toggleRow(rowEl.dataset.id, e.target.checked);
}
});
// Seleziona / deseleziona tutti i FILTRATI
document.addEventListener("change", function (e) {
if (e.target.id !== "filterSelectAll") return;
const on = e.target.checked;
// Con filtro server-side: seleziona TUTTI i match (anche altre pagine).
if (serverFiltered) {
selectAllMatching(on);
return;
}
document
.querySelectorAll("#gridRowContainer .grid-row")
.forEach((rowEl) => {
if (rowEl.style.display === "none") return;
const id = parseInt(rowEl.dataset.id, 10);
if (!id) return;
if (on) selected.add(id);
else selected.delete(id);
rowEl.classList.toggle("row-selected", on);
const cb = rowEl.querySelector(".filter-row-checkbox");
if (cb) cb.checked = on;
});
updateToolbar();
});
// click su QUALSIASI punto della riga (tranne campi interattivi) = seleziona
document.addEventListener("click", function (e) {
if (!filtersActive) return;
const rowEl = e.target.closest("#gridRowContainer .grid-row");
if (!rowEl) return;
// ignora click su elementi interattivi
if (
e.target.closest(
"input, select, textarea, button, a, .select2-container, .action-btn, .propagate-btn, .add-part-btn",
)
) {
// ma se ho cliccato proprio la checkbox, lascia fare al change
return;
}
const cb = rowEl.querySelector(".filter-row-checkbox");
if (!cb) return;
cb.checked = !cb.checked;
toggleRow(rowEl.dataset.id, cb.checked);
});
// filtri colonna testo (debounce) → filtro server-side su tutto il template
let t = null;
document.addEventListener("input", function (e) {
if (!e.target.classList.contains("filter-col-input")) return;
if (e.target.tagName === "SELECT") return; // i select li gestisce 'change'
const key = e.target.dataset.colKey;
colFilters[key] = e.target.value;
clearTimeout(t);
t = setTimeout(applyServerFilter, 250);
});
// dropdown (es. Status): applica subito, senza debounce
document.addEventListener("change", function (e) {
if (!e.target.classList.contains("filter-col-input")) return;
if (e.target.tagName !== "SELECT") return;
colFilters[e.target.dataset.colKey] = e.target.value;
applyServerFilter();
});
}
// ── Ripristina stato filtri dopo un re-render (checkbox + restrizione) ──
let _restoring = false;
function restoreFilterState() {
if (!filtersActive || _restoring) return;
_restoring = true; // evita loop con l'observer mentre iniettiamo/nascondiamo
try {
injectCheckboxes();
applyVisibility(); // ri-applica filtri colonna E restringi selezione
} finally {
// rilascia dopo il tick così le nostre mutazioni non ci ri-triggerano subito
setTimeout(() => {
_restoring = false;
}, 0);
}
}
// ── Observer: qualunque cosa ricrei le righe (propagazione, save, ecc.)
// ripristina automaticamente checkbox + restrizione. Robusto perché
// non dipende da chi chiama renderVisibleRows. ──
function hookRerender() {
const rowC = document.getElementById("gridRowContainer");
if (!rowC) return;
const obs = new MutationObserver(() => {
if (!filtersActive || _restoring) return;
// debounce leggero: aspetta che il render finisca
clearTimeout(hookRerender._t);
hookRerender._t = setTimeout(restoreFilterState, 30);
});
obs.observe(rowC, { childList: true });
}
function init() {
if (!window.gridRenderer) {
console.error("[gridFilter] gridRenderer non trovato");
return;
}
buildUI();
attachEvents();
hookRerender();
hookGridSync();
}
// parte dopo che gridRenderer ha finito init
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () =>
setTimeout(init, 300),
);
} else {
setTimeout(init, 300);
}
})();
+39 -337
View File
@@ -66,11 +66,7 @@
// ── Client data (AJAX Select2, no bulk loading) ──────────────────────
function formatClientLabel(client) {
const nome = (
client.Nominativo ||
client.text ||
"Nome non disponibile"
).trim();
const nome = client.Nominativo || client.text || "Nome non disponibile";
const id = client.IdCliente || client.id || "";
const codiceCliente = (
client.CodiceCliente ??
@@ -79,14 +75,11 @@
)
.toString()
.trim();
// Lettera estratta (parte dopo "_", altrimenti prima lettera del codice)
const suffix = (codiceCliente.split("_")[1] || "").trim();
const shortCode =
suffix || (codiceCliente ? codiceCliente.charAt(0) : "--");
// Nome - I - 123
return `${nome} - ${shortCode} - ${id}`;
return `${nome.trim()} - ${shortCode} (ID: ${id})`;
}
// Cache of resolved client names: id → name
@@ -117,9 +110,6 @@
ids.add(String(val));
}
}
if (row.fixedFields && row.fixedFields.ClienteFatturazione) {
ids.add(String(row.fixedFields.ClienteFatturazione));
}
});
// Batch resolve via single request per ID
@@ -143,54 +133,7 @@
}),
);
}
// Ranking inizio/centro/fine per select con dati già in memoria.
// Restituisce { matcher, sorter } da passare a select2().
function rankedLocalSelect2() {
return {
matcher: function (params, data) {
const term = $.trim(params.term || "").toLowerCase();
if (term === "") return data;
if (typeof data.text === "undefined") return null;
// AND multi-termine, posizione indipendente
const text = data.text.toLowerCase();
const terms = term.split(/\s+/).filter(Boolean);
const all = terms.every((t) => text.indexOf(t) !== -1);
return all ? data : null;
},
sorter: function (results) {
const term = $(
".select2-container--open .select2-search__field",
).val();
const search = (term || "").toLowerCase().trim();
if (!search) return results;
const first = search.split(/\s+/)[0];
function score(text) {
const t = (text || "").toLowerCase();
if (t === search) return 0; // match esatto
if (t.indexOf(first) === 0) return 1; // inizia col termine
if (new RegExp("(^|\\s)" + escapeRegExp(first)).test(t))
return 2; // inizio parola
return 3; // in mezzo
}
return results.slice().sort(function (a, b) {
const sa = score(a.text);
const sb = score(b.text);
if (sa !== sb) return sa - sb;
return (a.text || "").localeCompare(b.text || "", "it", {
sensitivity: "base",
});
});
},
};
}
function escapeRegExp(s) {
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function sortSelect2ResultsByStart(data) {
const term = $(".select2-container--open .select2-search__field").val();
@@ -213,49 +156,32 @@
return textA.localeCompare(textB, "it", { sensitivity: "base" });
});
}
// Mappa: quale filtro Inattivo applicare per ogni campo cliente
const clientFieldTipoMap = {
ClienteFornitore: "attivo",
ClienteAnalisi: "inattivo",
ClienteFatturazione: "attivo",
};
// Select2 AJAX config factory for client selects (parametrizzata su "tipo")
function clientSelect2ConfigFor(tipo) {
return {
placeholder: "Search client...",
allowClear: true,
width: "100%",
minimumInputLength: 0,
sorter: sortSelect2ResultsByStart,
dropdownCssClass: "select2-dropdown-smaller",
ajax: {
url: "search_clienti.php",
dataType: "json",
delay: 150,
data: function (params) {
const d = { q: params.term || "", limit: 20 };
if (tipo) d.tipo = tipo;
return d;
},
processResults: function (data) {
const results = (data.results || []).map((item) => ({
...item,
text: formatClientLabel(item),
}));
return { results: results };
},
cache: true,
// Select2 AJAX config for client selects
const clientSelect2Config = {
placeholder: "Search client...",
allowClear: true,
width: "100%",
minimumInputLength: 0,
sorter: sortSelect2ResultsByStart,
dropdownCssClass: "select2-dropdown-smaller",
ajax: {
url: "search_clienti.php",
dataType: "json",
delay: 150,
data: function (params) {
return { q: params.term || "", limit: 20 };
},
};
}
processResults: function (data) {
const results = (data.results || []).map((item) => ({
...item,
text: formatClientLabel(item),
}));
// Helper: inizializza Select2 su un elemento leggendo il tipo da data-client-tipo
function initClientSelect2(el) {
const tipo = el.dataset.clientTipo || "";
$(el).select2(clientSelect2ConfigFor(tipo));
}
return { results: results };
},
cache: true,
},
};
async function loadClientData() {
// No longer loads all clients — AJAX Select2 handles search
@@ -289,7 +215,6 @@
getParams: (cid) => ({ id_cliente: cid }),
},
ClienteFornitore: { source: "clients" },
ClienteFatturazione: { source: "clients" },
ClienteAnalisi: { source: "clients" },
};
@@ -439,6 +364,7 @@
allowClear: true,
width: "100%",
minimumInputLength: 0,
sorter: sortSelect2ResultsByStart,
ajax: {
url: "search_customfield_values.php",
dataType: "json",
@@ -610,7 +536,6 @@
sel.className =
"cell-input dropdown-select client-select searchable-client fornitore-select";
sel.dataset.currentValue = row.cliente_fornitore_id || "";
sel.dataset.clientTipo = "attivo";
sel.innerHTML = buildClientOptionsHTML(
row.cliente_fornitore_id,
);
@@ -706,13 +631,12 @@
if (config && config.source === "clients") {
const reqCls = col.isRequired ? " required-input" : "";
const req = col.isRequired ? " required" : "";
const tipo = clientFieldTipoMap[col.key] || "";
let opts = '<option value="">Select...</option>';
if (value) {
const label = clientNameCache[value] || value;
opts += `<option value="${esc(String(value))}" selected>${esc(String(label))}</option>`;
}
return `<select class="cell-input manual-input fixed-input searchable-client api-fixed-select${reqCls}" data-fixed-key="${escAttr(col.key)}" data-current-value="${escAttr(value)}" data-client-tipo="${escAttr(tipo)}"${req}>${opts}</select>`;
return `<select class="cell-input manual-input fixed-input searchable-client api-fixed-select${reqCls}" data-fixed-key="${escAttr(col.key)}" data-current-value="${escAttr(value)}"${req}>${opts}</select>`;
}
// Select — build from cache
@@ -730,7 +654,7 @@
const reqCls = col.isRequired ? " required-input" : "";
const req = col.isRequired ? " required" : "";
return `<select class="cell-input manual-input fixed-input searchable-fixed ${selectClass}${reqCls}" data-fixed-key="${escAttr(col.key)}" data-current-value="${escAttr(value)}"${req}>${options}</select>`;
return `<select class="cell-input manual-input fixed-input ${selectClass}${reqCls}" data-fixed-key="${escAttr(col.key)}" data-current-value="${escAttr(value)}"${req}>${options}</select>`;
}
function buildDropdownOptionsHTML(fieldId, selectedValue) {
@@ -755,7 +679,9 @@
div.style.position = "relative";
let html = "";
html += `<button type="button" class="export-lims-btn action-btn" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" title="${isExported ? "Already exported" : "Export to LIMS"}" style="background:${isExported ? "#ccc" : "#eb0b0b"}; color:white; border:none; border-radius:5px; cursor:${isExported ? "not-allowed" : "pointer"}; ${isExported ? "opacity:0.5;" : ""}" ${isExported ? "disabled" : ""}><i class="fas fa-upload"></i></button>`;
if (meta.isAdmin) {
html += `<button type="button" class="export-lims-btn action-btn" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" title="${isExported ? "Already exported" : "Export to LIMS"}" style="background:${isExported ? "#ccc" : "#eb0b0b"}; color:white; border:none; border-radius:5px; cursor:${isExported ? "not-allowed" : "pointer"}; ${isExported ? "opacity:0.5;" : ""}" ${isExported ? "disabled" : ""}><i class="fas fa-upload"></i></button>`;
}
html += `<button type="button" class="save-btn action-btn" data-row="${rowIndex}" title="Save" style="background:#28a745; color:white; border:none; border-radius:5px; cursor:pointer;"><i class="fas fa-save"></i></button>`;
html += `<button type="button" class="photos-btn action-btn" data-row="${rowIndex}" 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>`;
html += `<button type="button" class="parts-btn action-btn" data-row="${rowIndex}" 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>`;
@@ -1014,11 +940,6 @@
if (!row) return;
// Salta le righe nascoste (filtrate / non nella selezione ristretta):
// non devono essere sincronizzate né marcate dirty.
const rowEl = cell.closest(".grid-row");
if (rowEl && rowEl.style.display === "none") return;
const colType = cell.dataset.colType;
const colKey = cell.dataset.col;
const input = cell.querySelector(".cell-input");
@@ -1151,7 +1072,7 @@
`<button type="button" class="propagate-btn" data-column="idclient" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
} else if (col.type === "cliente_fornitore_id") {
cell.innerHTML =
`<select class="custom-field dropdown-select client-select searchable-client" data-column="cliente_fornitore_id" data-client-tipo="attivo" id="clienteFornitoreSelect"><option value="">Select a supplier...</option></select>` +
`<select class="custom-field dropdown-select client-select searchable-client" data-column="cliente_fornitore_id" id="clienteFornitoreSelect"><option value="">Select a supplier...</option></select>` +
`<button type="button" class="propagate-btn" data-column="cliente_fornitore_id" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
} else if (col.type === "fixed" && col.dataType === "DATE") {
cell.innerHTML =
@@ -1160,14 +1081,8 @@
} else if (col.type === "fixed") {
const isApiField = !!fixedFieldApiConfig[col.key];
if (isApiField) {
const tipo = clientFieldTipoMap[col.key] || "";
const isClientSourced =
fixedFieldApiConfig[col.key]?.source === "clients";
const clientCls = isClientSourced
? " client-select searchable-client"
: "";
cell.innerHTML =
`<select class="custom-field dropdown-select api-fixed-select fixed-top${clientCls}" data-column="fixed_${col.key}" data-fixed-key="${col.key}" data-client-tipo="${tipo}"><option value="">Seleziona...</option></select>` +
`<select class="custom-field dropdown-select api-fixed-select fixed-top" data-column="fixed_${col.key}" data-fixed-key="${col.key}"><option value="">Seleziona...</option></select>` +
`<button type="button" class="propagate-btn" data-column="fixed_${col.key}" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
} else {
cell.innerHTML =
@@ -1202,12 +1117,12 @@
const clientSel = document.getElementById("clientSelect");
if (clientSel) {
clientSel.innerHTML = buildClientOptionsHTML(meta.defaultIdclient);
initClientSelect2(clientSel);
$(clientSel).select2(clientSelect2Config);
}
const fornitSel = document.getElementById("clienteFornitoreSelect");
if (fornitSel) {
fornitSel.innerHTML = buildClientOptionsHTML("");
initClientSelect2(fornitSel);
$(fornitSel).select2(clientSelect2Config);
}
// Fixed field selects in top row
@@ -1217,7 +1132,7 @@
// Client-sourced → init as AJAX Select2
if (config && config.source === "clients") {
initClientSelect2(sel);
$(sel).select2(clientSelect2Config);
return;
}
@@ -1250,12 +1165,6 @@
items.forEach((item) =>
sel.add(new Option(item.text, item.id)),
);
$(sel).select2({
placeholder: "Seleziona...",
allowClear: true,
width: "100%",
...rankedLocalSelect2(),
});
}
});
@@ -1278,59 +1187,6 @@
function attachEvents() {
if (!rowContainer) return;
// ── Auto-copy: Sample Arrival date/time → Unlock date/time ──────────
// Coppie basate sui fieldId dei campi (source → target)
const AUTO_COPY_FIELD_PAIRS = [
{ from: 259, to: 260 }, // Sample Arrival date → Unlock Date
{ from: 348, to: 349 }, // Sample Arrival time → Unlock time
];
// Risolve fieldId → col.key (mapping id) usando le definizioni colonne
function colKeyByFieldId(fieldId) {
const col = columns.find(
(c) =>
(c.type === "detail" || c.type === "main_field") &&
String(c.fieldId) === String(fieldId),
);
return col ? String(col.key) : null;
}
// Precalcola le coppie come col.key (sourceKey → targetKey)
const AUTO_COPY_KEY_PAIRS = AUTO_COPY_FIELD_PAIRS.map((p) => ({
fromKey: colKeyByFieldId(p.from),
toKey: colKeyByFieldId(p.to),
})).filter((p) => p.fromKey && p.toKey);
function applyAutoCopy(rowIndex, changedColKey, value) {
const pair = AUTO_COPY_KEY_PAIRS.find(
(p) => p.fromKey === String(changedColKey),
);
if (!pair) return;
const row = data[rowIndex];
if (!row) return;
if (!row.details) row.details = {};
row.details[pair.toKey] = value;
row._dirty = true;
// Aggiorna la cella target visibile, se presente
const targetCell = rowContainer.querySelector(
`.grid-cell[data-row="${rowIndex}"][data-col="${pair.toKey}"]`,
);
if (targetCell) {
const input = targetCell.querySelector(".cell-input");
if (input) {
if ($(input).hasClass("select2-hidden-accessible")) {
$(input).val(value).trigger("change.select2");
} else {
input.value = value;
}
}
targetCell.classList.add("cell-changed");
}
}
// Cell value changes → write to gridData
rowContainer.addEventListener("change", function (e) {
const cell = e.target.closest(".grid-cell");
@@ -1345,7 +1201,6 @@
data[rowIndex].mainFieldValue = value;
}
setDetailValue(rowIndex, colKey, value);
applyAutoCopy(rowIndex, colKey, value);
} else if (colType === "fixed") {
setFixedValue(rowIndex, colKey, value);
} else if (colType === "idclient") {
@@ -1499,11 +1354,6 @@
if (column === "idclient") {
data.forEach((row) => {
if (
window.isTargetGridId &&
!window.isTargetGridId(row.iddatadb)
)
return;
const oldClientId = row.idclient || "";
row.idclient = value;
@@ -1537,11 +1387,6 @@
if (column === "cliente_fornitore_id") {
data.forEach((row) => {
if (
window.isTargetGridId &&
!window.isTargetGridId(row.iddatadb)
)
return;
row.cliente_fornitore_id = value;
row._dirty = true;
});
@@ -1555,11 +1400,6 @@
const fixedKey = column.replace("fixed_", "");
data.forEach((row) => {
if (
window.isTargetGridId &&
!window.isTargetGridId(row.iddatadb)
)
return;
if (!row.fixedFields) row.fixedFields = {};
row.fixedFields[fixedKey] = value;
row._dirty = true;
@@ -1572,11 +1412,6 @@
if (col && (col.type === "detail" || col.type === "main_field")) {
data.forEach((row) => {
if (
window.isTargetGridId &&
!window.isTargetGridId(row.iddatadb)
)
return;
if (!row.details) row.details = {};
row.details[col.key] = value;
@@ -1750,7 +1585,7 @@
$(this)
.find(".searchable-client:not(.select2-hidden-accessible)")
.each(function () {
initClientSelect2(this);
$(this).select2(clientSelect2Config);
});
$(this)
.find(".searchable-dropdown:not(.select2-hidden-accessible)")
@@ -1758,18 +1593,9 @@
const fieldId = this.dataset.fieldId;
if (fieldId) $(this).select2(sceltaSelect2Config(fieldId));
});
$(this)
.find(".searchable-fixed:not(.select2-hidden-accessible)")
.each(function () {
$(this).select2({
placeholder: "Seleziona...",
allowClear: true,
width: "100%",
...rankedLocalSelect2(),
});
});
});
}
// ── Column resize ──────────────────────────────────────────────────────
function initColumnResizers() {
@@ -1900,127 +1726,3 @@
},
};
})();
// ===================
// NAVIGAZIONE GRIGLIA CON FRECCE TASTIERA
// ===================
(function () {
// Naviga solo tra input di testo/numero/time e date-picker.
// I Select2 (client, fornitore, SceltaMultipla) sono esclusi:
// lì le frecce servono al dropdown.
function isNavigable(el) {
if (!el || !el.classList) return false;
if (!el.classList.contains("cell-input")) return false;
// Escludi i select (nativi o Select2)
if (el.tagName === "SELECT") return false;
if (el.classList.contains("select2-hidden-accessible")) return false;
return true;
}
// Tutte le celle navigabili della riga, ordinate per data-index
function navCellsInRow($row) {
return $row.find(".grid-cell[data-index]").filter(function () {
const input = this.querySelector(".cell-input");
return isNavigable(input);
});
}
function focusCellInput($cell) {
if (!$cell || !$cell.length) return;
const input = $cell.get(0).querySelector(".cell-input");
if (!input) return;
input.focus();
if (input.type !== "date" && typeof input.select === "function") {
try {
input.select();
} catch (e) {}
}
}
function atStart(el) {
if (el.type === "date" || el.type === "number" || el.type === "time")
return true;
return el.selectionStart === 0 && el.selectionEnd === 0;
}
function atEnd(el) {
if (el.type === "date" || el.type === "number" || el.type === "time")
return true;
const len = (el.value || "").length;
return el.selectionStart === len && el.selectionEnd === len;
}
$(document).on("keydown", "#gridRowContainer .cell-input", function (e) {
const el = this;
if (!isNavigable(el)) return;
// Ignora se un dropdown Select2 è aperto
if ($(".select2-container--open").length) return;
const key = e.key;
if (
key !== "ArrowUp" &&
key !== "ArrowDown" &&
key !== "ArrowLeft" &&
key !== "ArrowRight"
) {
return;
}
const $cell = $(el).closest(".grid-cell");
const $row = $cell.closest(".grid-row");
if (!$cell.length || !$row.length) return;
const colIndex = $cell.attr("data-index");
// --- SU / GIÙ: stessa colonna (data-index), riga adiacente ---
if (key === "ArrowUp" || key === "ArrowDown") {
const $targetRow =
key === "ArrowUp"
? $row.prev(".grid-row")
: $row.next(".grid-row");
if (!$targetRow.length) return;
e.preventDefault();
// Cella stessa colonna nella riga di destinazione
let $target = $targetRow.find(
`.grid-cell[data-index="${colIndex}"]`,
);
// Se quella colonna non è navigabile (es. è un select),
// cerca la più vicina navigabile nella riga
if (
!$target.length ||
!isNavigable($target.get(0).querySelector(".cell-input"))
) {
const $cells = navCellsInRow($targetRow);
if (!$cells.length) return;
$target = $cells.first();
}
focusCellInput($target);
return;
}
// --- SINISTRA / DESTRA: cella navigabile precedente/successiva nella riga ---
const $cells = navCellsInRow($row);
const curPos = $cells.index($cell);
if (curPos === -1) return;
if (key === "ArrowLeft") {
if (!atStart(el)) return;
if (curPos <= 0) return;
e.preventDefault();
focusCellInput($cells.eq(curPos - 1));
return;
}
if (key === "ArrowRight") {
if (!atEnd(el)) return;
if (curPos >= $cells.length - 1) return;
e.preventDefault();
focusCellInput($cells.eq(curPos + 1));
return;
}
});
})();
+24 -140
View File
@@ -130,60 +130,10 @@
color: #6c757d;
padding: 10px 0;
}
.section-title {
font-size: 13px;
font-weight: 600;
color: #6c757d;
text-transform: uppercase;
letter-spacing: 0.5px;
width: 100%;
margin-top: 6px;
margin-bottom: 2px;
padding-bottom: 4px;
border-bottom: 1px solid #eee;
}
.section-title:first-child {
margin-top: 0;
}
.template-btn-wrapper {
position: relative;
display: inline-flex;
}
.favorite-star {
position: absolute;
top: -6px;
right: -6px;
width: 22px;
height: 22px;
background: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
font-size: 13px;
color: #ccc;
z-index: 2;
transition: color 0.15s, transform 0.15s;
}
.favorite-star:hover {
transform: scale(1.15);
}
.favorite-star.active {
color: #ffc107;
}
</style>
</head>
<body>
<?php include('include/popup.php'); ?>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
@@ -281,23 +231,24 @@
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function createButtonWrapper(template) {
function createButton(template) {
const sizeClass = getSizeClass(template.button_size);
const sourceType = (template.source_type || '').toUpperCase();
const iconClass = getTemplateIcon(sourceType);
const isFavorite = !!parseInt(template.is_favorite);
const wrapper = document.createElement("div");
wrapper.className = "template-btn-wrapper";
const btn = document.createElement("a");
btn.href = `import_xls2.php?id=${template.id}`;
// Redirect based on template source type
if (sourceType === 'XLS') {
btn.href = `import_xls2.php?id=${template.id}`;
} else if (sourceType === 'API' || sourceType === 'JSON') {
btn.href = `import_json.php?id=${template.id}`;
} else if (sourceType === 'PDF') {
btn.href = `import_pdf.php?id=${template.id}`;
} else {
btn.href = `import_xls2.php?id=${template.id}`;
}
btn.className = `btn ${sizeClass}`;
btn.style.backgroundColor = template.button_bg_color || '#0d6efd';
btn.style.color = template.button_text_color || '#ffffff';
@@ -305,62 +256,17 @@
btn.setAttribute("data-source-type", sourceType);
btn.innerHTML = `
<i class="${iconClass} template-icon"></i>
<span>${escapeHtml(template.button_label || 'Unnamed')}</span>
`;
<i class="${iconClass} template-icon"></i>
<span>${escapeHtml(template.button_label || 'Unnamed')}</span>
`;
const star = document.createElement("span");
star.className = "favorite-star" + (isFavorite ? " active" : "");
star.innerHTML = `<i class="bx ${isFavorite ? 'bxs-star' : 'bx-star'}"></i>`;
star.setAttribute("data-template-id", template.id);
star.title = isFavorite ? "Rimuovi dai preferiti" : "Aggiungi ai preferiti";
star.addEventListener("click", function(e) {
e.preventDefault();
e.stopPropagation();
toggleFavorite(template.id);
});
wrapper.appendChild(btn);
wrapper.appendChild(star);
return wrapper;
return btn;
}
function toggleFavorite(templateId) {
const template = allTemplates.find(t => String(t.id) === String(templateId));
if (!template) return;
const newState = !parseInt(template.is_favorite);
// Optimistic update
template.is_favorite = newState ? 1 : 0;
renderTemplates(document.getElementById("searchInput").value.toLowerCase().trim());
fetch("toggle_template_favorite.php", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
template_id: templateId,
is_favorite: newState
})
})
.then(response => response.json())
.then(data => {
if (!data.success) {
console.error("Failed to toggle favorite:", data.message);
// Rollback in caso di errore
template.is_favorite = newState ? 0 : 1;
renderTemplates(document.getElementById("searchInput").value.toLowerCase().trim());
}
})
.catch(error => {
console.error("Fetch error toggling favorite:", error);
template.is_favorite = newState ? 0 : 1;
renderTemplates(document.getElementById("searchInput").value.toLowerCase().trim());
});
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function clearContainers() {
@@ -369,26 +275,6 @@
});
}
function renderSection(container, title, templates) {
if (templates.length === 0) return;
const heading = document.createElement("div");
heading.className = "section-title";
heading.textContent = title;
container.appendChild(heading);
const group = document.createElement("div");
group.className = "template-buttons";
group.style.marginTop = "0";
group.style.paddingTop = "0";
templates.forEach(template => {
group.appendChild(createButtonWrapper(template));
});
container.appendChild(group);
}
function renderTemplates(searchValue = '') {
clearContainers();
@@ -420,11 +306,9 @@
return;
}
const favorites = grouped[type].filter(t => parseInt(t.is_favorite) === 1);
const others = grouped[type].filter(t => parseInt(t.is_favorite) !== 1);
renderSection(container, "★ Preferiti", favorites);
renderSection(container, "Altri", others);
grouped[type].forEach(template => {
container.appendChild(createButton(template));
});
});
}
+318 -89
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"));
@@ -25,6 +26,7 @@ $rows = json_decode(urldecode($_POST['rows'] ?? '[]'), true);
$excelrows = json_decode(urldecode($_POST['excelrows'] ?? '[]'), true);
$newFilename = $_POST['filename'];
$source_type = strtolower(trim($_POST['source_type'] ?? 'xls'));
$_SESSION['template_id'] = $template_id;
$_SESSION['selected_rows'] = $selected_rows;
@@ -37,32 +39,10 @@ error_log("Received Data - Template ID: $template_id, Selected Rows: " . json_en
error_log("Columns: " . json_encode($columns));
error_log("Rows: " . json_encode($rows));
error_log("Excelrows: " . json_encode($excelrows));
error_log("Source type: " . $source_type);
$user_id = $iduserlogin ?? 1;
/**
* Normalizza un nome colonna per il confronto header Excel <-> mapping.
* Risolve alla radice i mismatch causati da caratteri invisibili:
* - non-breaking space (U+00A0), narrow NBSP (U+202F), figure space (U+2007), ecc. -> spazio normale
* - tab, newline e altri whitespace -> spazio normale
* - spazi multipli consecutivi -> un solo spazio
* - trim ai bordi
* NB: usata SOLO per il matching, non altera i valori importati.
*/
function normalizeColName($s): string
{
$s = (string)$s;
// Converte i principali spazi Unicode "insidiosi" in spazio normale
$s = str_replace(
["\xC2\xA0", "\xE2\x80\xAF", "\xE2\x80\x87", "\xE2\x80\x89", "\xE2\x80\x8A", "\xEF\xBB\xBF"],
' ',
$s
);
// Qualsiasi sequenza di whitespace (spazi, tab, newline) -> singolo spazio
$s = preg_replace('/\s+/u', ' ', $s);
return trim((string)$s);
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
@@ -70,7 +50,23 @@ $pdo = $db->getConnection();
$importReferenceCode = date('YmdHis') . '-' . uniqid();
// Recupera tutti i mapping dal template
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field, auto_value FROM template_mapping WHERE template_id = ?");
$stmt = $pdo->prepare("
SELECT
id,
excel_column,
json_node,
data_type,
is_required,
manual_default,
is_manual,
field_label,
field_id,
main_field,
auto_value,
has_list
FROM template_mapping
WHERE template_id = ?
");
$stmt->execute([$template_id]);
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -88,14 +84,41 @@ foreach ($allMappings as $mapping) {
}
}
// Campi fixed mappati da JSON (default_source='json') per questo template.
$fixedJsonFields = [];
if ($source_type === 'json') {
$fxStmt = $pdo->prepare("
SELECT fixed_field_key, json_node, data_type
FROM template_fixed_mapping
WHERE template_id = ? AND default_source = 'json' AND json_node IS NOT NULL AND json_node <> ''
");
$fxStmt->execute([$template_id]);
$fixedJsonFields = $fxStmt->fetchAll(PDO::FETCH_ASSOC);
}
// Inserisci le righe selezionate in datadb
$insertedIds = [];
foreach ($selected_rows as $rowIndex) {
$row = $rows[$rowIndex] ?? null;
$excelrow = $excelrows[$rowIndex] ?? null;
// Binding JSON -> LIMS senza corrispondenza salvata, per "kind:key|json_value".
$pendingBindings = [];
// Binding risolti in automatico durante questo import (solo per visualizzazione).
$autoBindings = [];
// Binding gia' salvati in precedenza, usati su questo import (visualizzazione + modifica).
$savedBindings = [];
foreach ($selected_rows as $loopIndex => $rowIndex) {
if ($source_type === 'json') {
// JSON import sends only selected rows in rows/excelrows
$row = $rows[$loopIndex] ?? null;
$excelrow = $excelrows[$loopIndex] ?? ('JSON-' . ($loopIndex + 1));
} else {
// XLS import keeps original row indexes
$row = $rows[$rowIndex] ?? null;
$excelrow = $excelrows[$rowIndex] ?? null;
}
if ($row === null || $excelrow === null) {
error_log("Errore: riga o excelrow mancante per rowIndex $rowIndex");
error_log("Errore: riga o excelrow mancante. Source type: $source_type, loopIndex: $loopIndex, rowIndex: $rowIndex");
continue;
}
@@ -105,6 +128,14 @@ foreach ($selected_rows as $rowIndex) {
$template = $template_stmt->fetch(PDO::FETCH_ASSOC);
$default_idclient = $template['idclient'] ?? null;
// excelrow e' INT: dal JSON arriva tipo 'JSON-1', tengo solo la parte numerica.
if (is_numeric($excelrow)) {
$excelrowDb = (int) $excelrow;
} else {
$digits = preg_replace('/\D+/', '', (string) $excelrow);
$excelrowDb = $digits !== '' ? (int) $digits : ($loopIndex + 1);
}
$values = [
$template_id,
$importReferenceCode,
@@ -113,7 +144,7 @@ foreach ($selected_rows as $rowIndex) {
$user_id,
null,
date('Y-m-d'),
$excelrow,
$excelrowDb,
$default_idclient // Aggiungi idclient
];
$sql = "INSERT INTO datadb (templateid, importreferencecode, filename_import, status, user_id, limscode, importdate, excelrow, idclient) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
@@ -127,19 +158,72 @@ foreach ($selected_rows as $rowIndex) {
foreach ($allMappings as $mapping) {
$fieldValue = null;
if (!$mapping['is_manual']) {
$excelColumn = trim($mapping['excel_column']);
// Match robusto: normalizza header e mapping (NBSP, spazi doppi, tab...)
// così un solo/doppio spazio o un non-breaking space non rompono il confronto.
$excelColumnIndex = array_search(
normalizeColName($excelColumn),
array_map('normalizeColName', $columns)
);
if ($excelColumnIndex !== false && isset($row[$excelColumnIndex]) && $row[$excelColumnIndex] !== '') {
$fieldValue = $row[$excelColumnIndex];
error_log("Found Excel column '$excelColumn' at index $excelColumnIndex, value: " . var_export($fieldValue, true));
$sourceColumn = '';
if ($source_type === 'json') {
$sourceColumn = trim($mapping['json_node'] ?? '');
} else {
$sourceColumn = trim($mapping['excel_column'] ?? '');
}
// Fallback: if JSON node is empty, try excel_column
if ($sourceColumn === '') {
$sourceColumn = trim($mapping['excel_column'] ?? '');
}
$columnsTrimmed = array_map('trim', $columns);
$candidateColumns = [];
if ($sourceColumn !== '') {
$candidateColumns[] = $sourceColumn;
if ($source_type === 'json') {
// Common JSON path variants
$candidateColumns[] = preg_replace('/^data\[\]\./', '', $sourceColumn);
$candidateColumns[] = preg_replace('/^data\.0\./', '', $sourceColumn);
$candidateColumns[] = str_replace('data[].', 'data.0.', $sourceColumn);
$candidateColumns[] = str_replace('data.0.', 'data[].', $sourceColumn);
}
}
// Remove empty and duplicate candidates
$candidateColumns = array_values(array_unique(array_filter($candidateColumns, function ($value) {
return trim((string)$value) !== '';
})));
$sourceColumnIndex = false;
$matchedColumn = '';
foreach ($candidateColumns as $candidateColumn) {
$candidateColumn = trim($candidateColumn);
$index = array_search($candidateColumn, $columnsTrimmed);
if ($index !== false) {
$sourceColumnIndex = $index;
$matchedColumn = $candidateColumn;
break;
}
}
if ($sourceColumnIndex !== false && isset($row[$sourceColumnIndex]) && $row[$sourceColumnIndex] !== '') {
$fieldValue = $row[$sourceColumnIndex];
error_log(
"Found source column. Original: '$sourceColumn', Matched: '$matchedColumn', Index: $sourceColumnIndex, Value: " .
var_export($fieldValue, true)
);
} else {
$fieldValue = $mapping['manual_default'] ?? '';
error_log("Excel column '$excelColumn' not found or empty, using default: " . var_export($fieldValue, true));
error_log(
"Source column not found or empty. Original: '$sourceColumn'. Candidates: " .
json_encode($candidateColumns) .
". Available columns: " .
json_encode($columnsTrimmed) .
". Using default: " .
var_export($fieldValue, true)
);
}
switch ($mapping['data_type']) {
case 'INT':
@@ -172,70 +256,215 @@ foreach ($selected_rows as $rowIndex) {
}
}
// Binding JSON -> LIMS solo per i campi a lista importati da JSON.
if (
$source_type === 'json'
&& !$mapping['is_manual']
&& binding_is_list_field($mapping)
&& $fieldValue !== null
&& $fieldValue !== ''
) {
$jsonValue = (string) $fieldValue;
$existing = binding_lookup($pdo, (int) $mapping['id'], $jsonValue);
if ($existing) {
$fieldValue = $existing['lims_value'];
$key = 'cf:' . $mapping['id'] . '|' . $jsonValue;
if (!isset($savedBindings[$key])) {
$savedBindings[$key] = [
'kind' => 'custom',
'mapping_id' => (int) $mapping['id'],
'field_id' => (int) $mapping['field_id'],
'field_label' => $mapping['field_label'],
'json_value' => $jsonValue,
'lims_value' => (string) $existing['lims_value'],
'lims_value_id' => (int) $existing['lims_value_id'],
'datadb_ids' => [],
];
}
$savedBindings[$key]['datadb_ids'][] = (int) $iddatadb;
} else {
// Nessun binding salvato: provo l'auto-match 1-a-1 sui valori LIMS.
$limsValues = binding_get_lims_values((int) $mapping['field_id']);
$autoMatch = binding_auto_match($limsValues, $jsonValue);
if ($autoMatch) {
binding_upsert(
$pdo,
(int) $template_id,
(int) $mapping['id'],
(int) $mapping['field_id'],
$jsonValue,
(int) $autoMatch['IdCustomFieldsValue'],
(string) $autoMatch['Valore'],
$user_id
);
$fieldValue = (string) $autoMatch['Valore'];
$key = 'cf:' . $mapping['id'] . '|' . $jsonValue;
if (!isset($autoBindings[$key])) {
$autoBindings[$key] = [
'kind' => 'custom',
'mapping_id' => (int) $mapping['id'],
'field_id' => (int) $mapping['field_id'],
'field_label' => $mapping['field_label'],
'json_value' => $jsonValue,
'lims_value' => (string) $autoMatch['Valore'],
'lims_value_id' => (int) $autoMatch['IdCustomFieldsValue'],
'datadb_ids' => [],
];
}
$autoBindings[$key]['datadb_ids'][] = (int) $iddatadb;
} else {
$key = 'cf:' . $mapping['id'] . '|' . $jsonValue;
if (!isset($pendingBindings[$key])) {
$pendingBindings[$key] = [
'kind' => 'custom',
'mapping_id' => (int) $mapping['id'],
'field_id' => (int) $mapping['field_id'],
'field_label' => $mapping['field_label'],
'json_value' => $jsonValue,
'datadb_ids' => [],
];
}
$pendingBindings[$key]['datadb_ids'][] = (int) $iddatadb;
}
}
}
if ($mapping['is_required'] && (is_null($fieldValue) || $fieldValue === '')) {
error_log("Required field missing for mapping ID: " . $mapping['id'] . ", field: " . $mapping['field_label']);
}
error_log("Inserting into import_data_details - Mapping ID: " . $mapping['id'] . ", Field Value: " . var_export($fieldValue, true) . ", Is Manual: " . $mapping['is_manual'] . ", Excel Column: " . ($mapping['excel_column'] ?? 'N/A') . ", Manual Default: " . ($mapping['manual_default'] ?? 'N/A'));
error_log("Inserting into import_data_details - Mapping ID: " . $mapping['id'] . ", Field Value: " . var_export($fieldValue, true) . ", Is Manual: " . $mapping['is_manual'] . ", Source Column: " . ($sourceColumn ?? 'N/A') . ", Source Type: " . $source_type . ", Manual Default: " . ($mapping['manual_default'] ?? 'N/A'));
$stmt = $pdo->prepare("INSERT INTO import_data_details (id, mapping_id, field_value) VALUES (?, ?, ?)");
$stmt->execute([$iddatadb, $mapping['id'], $fieldValue]);
error_log("Inserted into import_data_details for ID $iddatadb, Mapping ID: " . $mapping['id'] . ", Field Value: " . var_export($fieldValue, true));
}
}
// ------------------------------------------------------------
// AUTO SET: schema field_id = 244 (Accettatore) con auth_users.lims_user_id
// Spostato dal vecchio flusso (import_edit.php) al flusso nuovo.
// Autocompila il custom field 244 con il lims_user_id dell'utente loggato.
// ------------------------------------------------------------
try {
// 1) Leggi il lims_user_id dell'utente loggato
$stmtUser = $pdo->prepare("SELECT lims_user_id FROM auth_users WHERE id = ? LIMIT 1");
$stmtUser->execute([(int)$user_id]);
$limsUserId = $stmtUser->fetchColumn();
// ---- Fixed fields mappati da JSON (scrivono colonne datadb) ----
if ($source_type === 'json' && !empty($fixedJsonFields)) {
$fixedUpdates = []; // colonna datadb => valore (id LIMS o data)
// normalize
$limsUserId = ($limsUserId !== false && $limsUserId !== null && $limsUserId !== '') ? (string)$limsUserId : '';
if ($limsUserId !== '' && !empty($insertedIds)) {
// 2) Trova il mapping_id per field_id = 244 in questo template
$stmtMap = $pdo->prepare("SELECT id FROM template_mapping WHERE template_id = ? AND field_id = 244 LIMIT 1");
$stmtMap->execute([(int)$template_id]);
$mappingId244 = (int)$stmtMap->fetchColumn();
if ($mappingId244 > 0) {
// 3) Upsert del valore in import_data_details per ogni riga inserita
$pdo->beginTransaction();
$updStmt = $pdo->prepare("UPDATE import_data_details SET field_value = ? WHERE id = ? AND mapping_id = ?");
$insStmt = $pdo->prepare("INSERT INTO import_data_details (id, mapping_id, field_value) VALUES (?, ?, ?)");
foreach ($insertedIds as $iddatadb) {
$iddatadb = (int)$iddatadb;
if ($iddatadb <= 0) continue;
$updStmt->execute([$limsUserId, $iddatadb, $mappingId244]);
if ($updStmt->rowCount() === 0) {
$insStmt->execute([$iddatadb, $mappingId244, $limsUserId]);
}
foreach ($fixedJsonFields as $fx) {
$fixedKey = $fx['fixed_field_key'];
$column = binding_fixed_column($fixedKey);
if (!$column) {
continue;
}
$pdo->commit();
} else {
error_log("[AUTO FIELD 244] Nessun mapping field_id=244 per template $template_id");
$idx = binding_find_column_index((string) $fx['json_node'], $columns);
$raw = ($idx >= 0 && isset($row[$idx])) ? trim((string) $row[$idx]) : '';
if ($raw === '') {
continue;
}
// Campo non a lista (es. ConsegnaRichiesta DATE): scrivo il valore direttamente.
if (!binding_fixed_is_list($fixedKey)) {
if (($fx['data_type'] ?? '') === 'DATE') {
$fixedUpdates[$column] = date('Y-m-d', strtotime($raw));
}
continue;
}
// Binding gia' salvato.
$existing = binding_lookup_fixed($pdo, (int) $template_id, $fixedKey, $raw);
if ($existing) {
$fixedUpdates[$column] = (int) $existing['lims_value_id'];
$key = 'fx:' . $fixedKey . '|' . $raw;
if (!isset($savedBindings[$key])) {
$savedBindings[$key] = [
'kind' => 'fixed',
'fixed_field_key' => $fixedKey,
'field_label' => binding_fixed_label($fixedKey),
'json_value' => $raw,
'lims_value' => (string) $existing['lims_value'],
'lims_value_id' => (int) $existing['lims_value_id'],
'datadb_ids' => [],
];
}
$savedBindings[$key]['datadb_ids'][] = (int) $iddatadb;
continue;
}
// Auto-match 1-a-1 (solo per le liste globali piccole).
$autoMatch = null;
if (binding_fixed_auto_matchable($fixedKey)) {
$fixedValues = binding_get_fixed_values($pdo, $fixedKey, (int) $template_id);
$autoMatch = binding_auto_match_fixed($fixedValues, $raw);
}
if ($autoMatch) {
binding_upsert_fixed($pdo, (int) $template_id, $fixedKey, $raw, (int) $autoMatch['id'], (string) $autoMatch['text'], $user_id);
$fixedUpdates[$column] = (int) $autoMatch['id'];
$key = 'fx:' . $fixedKey . '|' . $raw;
if (!isset($autoBindings[$key])) {
$autoBindings[$key] = [
'kind' => 'fixed',
'fixed_field_key' => $fixedKey,
'field_label' => binding_fixed_label($fixedKey),
'json_value' => $raw,
'lims_value' => (string) $autoMatch['text'],
'lims_value_id' => (int) $autoMatch['id'],
'datadb_ids' => [],
];
}
$autoBindings[$key]['datadb_ids'][] = (int) $iddatadb;
continue;
}
// Nessuna corrispondenza: colonna lasciata vuota, segnalo come pending.
$key = 'fx:' . $fixedKey . '|' . $raw;
if (!isset($pendingBindings[$key])) {
$pendingBindings[$key] = [
'kind' => 'fixed',
'fixed_field_key' => $fixedKey,
'field_label' => binding_fixed_label($fixedKey),
'json_value' => $raw,
'datadb_ids' => [],
];
}
$pendingBindings[$key]['datadb_ids'][] = (int) $iddatadb;
}
if (!empty($fixedUpdates)) {
$setParts = [];
$params = [];
foreach ($fixedUpdates as $col => $val) {
$setParts[] = "`$col` = ?";
$params[] = $val;
}
$params[] = (int) $iddatadb;
$upd = $pdo->prepare("UPDATE datadb SET " . implode(', ', $setParts) . " WHERE iddatadb = ?");
$upd->execute($params);
}
} else {
error_log("[AUTO FIELD 244] limsUserId vuoto per user_id=$user_id o nessuna riga inserita");
}
} catch (Exception $e) {
// Non bloccare l'import se questo fallisce; logga soltanto
error_log("[AUTO FIELD 244] " . $e->getMessage());
if ($pdo->inTransaction()) $pdo->rollBack();
}
$_SESSION['inserted_ids'] = $insertedIds;
header("Location: imported.php?id=" . urlencode($template_id) . "&importref=" . urlencode($importReferenceCode));
$importedUrl = "imported.php?id=" . urlencode($template_id) . "&importref=" . urlencode($importReferenceCode);
// Solo se restano binding da risolvere mostro la pagina (con anche gli auto, modificabili).
if (!empty($pendingBindings)) {
$_SESSION['pending_bindings'] = [
'template_id' => $template_id,
'importref' => $importReferenceCode,
'items' => array_values($pendingBindings),
'auto' => array_values($autoBindings),
'saved' => array_values($savedBindings),
];
header("Location: resolve_bindings.php");
exit;
}
unset($_SESSION['pending_bindings']);
// Solo auto-collegati: vado diretto alla griglia, segnalando quanti.
if (!empty($autoBindings)) {
$importedUrl .= "&autobound=" . count($autoBindings);
}
header("Location: " . $importedUrl);
exit;
+829
View File
@@ -0,0 +1,829 @@
<?php
include('include/headscript.php');
// Check if a valid template ID has been provided
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Invalid ID"));
exit;
}
$id = intval($_GET['id']);
// Load template
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("SELECT * FROM excel_templates WHERE id = ?");
$stmt->execute([$id]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$template) {
header("Location: template_dashboard.php?status=error&message=" . urlencode("Template not found"));
exit;
}
// Check mappings
$stmt = $pdo->prepare("SELECT id FROM template_mapping WHERE template_id = ?");
$stmt->execute([$id]);
$hasMappings = $stmt->fetch(PDO::FETCH_ASSOC);
error_log("Loaded JSON import template: " . print_r($template, true));
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.top-scrollbar {
overflow-x: auto;
overflow-y: hidden;
width: 100%;
height: 18px;
margin-bottom: 8px;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
background: #f8f9fa;
display: none;
}
.top-scrollbar-inner {
height: 1px;
}
.table-container {
overflow-x: auto;
max-width: 100%;
margin-bottom: 20px;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 10px;
text-align: left;
border: 1px solid #dee2e6;
min-width: 120px;
max-width: 260px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
.table th:first-child,
.table td:first-child {
min-width: 50px;
max-width: 50px;
}
.table th {
background-color: #f8f9fa;
position: relative;
cursor: col-resize;
user-select: none;
}
.table th .resize-handle {
position: absolute;
top: 0;
right: 0;
width: 5px;
height: 100%;
cursor: col-resize;
background: transparent;
}
.table th .resize-handle:hover {
background: #007bff;
}
.loader {
display: none;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 10px auto;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.column-filters th {
background: #ffffff;
cursor: default;
}
.column-filters input {
width: 100%;
min-width: 80px;
}
.json-code-input {
font-size: 1.15rem;
min-height: 48px;
}
.json-paste-area {
min-height: 260px;
font-family: Consolas, Monaco, monospace;
font-size: 13px;
}
.json-help-box {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 12px 14px;
font-size: 13px;
}
.source-badge {
font-size: 12px;
}
</style>
<title><?= htmlspecialchars($template['name']) ?> - JSON Import - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<?php include('top_stat_widget.php'); ?>
<div class="mb-3 text">
<a href="imported.php?id=<?= $id ?>" class="btn btn-warning me-2">Imported (i)</a>
<a href="tolims.php?id=<?= $id ?>" class="btn btn-success me-2">To LIMS (l)</a>
<a href="bindings_manage.php?template_id=<?= $id ?>" class="btn btn-outline-secondary">Binding JSON &rarr; LIMS</a>
</div>
<div class="card radius-10">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<div>
<h6 class="mb-0"><?= htmlspecialchars($template['name']) ?> - JSON Import</h6>
<small>
Template ID: <?= $id ?>
</small>
</div>
<span class="badge bg-info text-dark">JSON mode</span>
</div>
</div>
<div class="card-body">
<?php if (!$hasMappings): ?>
<div class="alert alert-warning" role="alert">
Nessun mapping trovato per questo template. Configura i mapping prima di procedere.
</div>
<?php endif; ?>
<div class="json-help-box mb-3">
<strong>Flusso:</strong> inserisci/scansiona un codice per recuperare il JSON da API, oppure incolla manualmente un JSON nel secondo tab.
Ogni JSON aggiunto diventa una riga della tabella di preview. Quando hai finito, seleziona le righe e clicca <strong>Prosegui</strong>.
</div>
<ul class="nav nav-tabs" id="jsonImportTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="api-code-tab" data-bs-toggle="tab" data-bs-target="#api-code-pane" type="button" role="tab" aria-controls="api-code-pane" aria-selected="true">
Code / Barcode
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="paste-json-tab" data-bs-toggle="tab" data-bs-target="#paste-json-pane" type="button" role="tab" aria-controls="paste-json-pane" aria-selected="false">
Paste JSON
</button>
</li>
</ul>
<div class="tab-content border border-top-0 p-3 mb-3" id="jsonImportTabsContent">
<div class="tab-pane fade show active" id="api-code-pane" role="tabpanel" aria-labelledby="api-code-tab" tabindex="0">
<form id="jsonCodeForm" class="row g-3 align-items-end">
<div class="col-lg-8">
<label for="json_code" class="form-label">Code / Barcode</label>
<input type="text" class="form-control json-code-input" id="json_code" name="json_code" placeholder="Write or scan code" autocomplete="off" <?= !$hasMappings ? 'disabled' : '' ?>>
<small class="text-muted">Lo scanner barcode normalmente scrive qui il codice e invia Enter.</small>
</div>
<div class="col-lg-4 d-flex gap-2">
<button type="submit" class="btn btn-primary flex-fill" <?= !$hasMappings ? 'disabled' : '' ?>>Load JSON</button>
<button type="button" class="btn btn-outline-secondary" id="clearCodeBtn">Clear</button>
</div>
</form>
</div>
<div class="tab-pane fade" id="paste-json-pane" role="tabpanel" aria-labelledby="paste-json-tab" tabindex="0">
<form id="pasteJsonForm">
<div class="mb-3">
<label for="manual_json_reference" class="form-label">Reference / filename</label>
<input type="text" class="form-control" id="manual_json_reference" placeholder="Optional reference, e.g. manual-json-001">
</div>
<div class="mb-3">
<label for="manual_json" class="form-label">Paste JSON</label>
<textarea class="form-control json-paste-area" id="manual_json" placeholder='{"data":[{"id":"MM000620","attributes":{"trf_type":"apparel"}}]}' <?= !$hasMappings ? 'disabled' : '' ?>></textarea>
</div>
<button type="submit" class="btn btn-primary" <?= !$hasMappings ? 'disabled' : '' ?>>Add pasted JSON</button>
<button type="button" class="btn btn-outline-secondary" id="clearManualJsonBtn">Clear</button>
</form>
</div>
</div>
<div class="loader" id="loader"></div>
<div id="errorContainer" class="alert alert-danger mt-3" style="display:none;"></div>
<div id="successContainer" class="alert alert-success mt-3" style="display:none;"></div>
<div id="tableContainer"></div>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
</div>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.min.js"></script>
<?php include('jsinclude.php'); ?>
<script>
document.addEventListener("DOMContentLoaded", function() {
const TEMPLATE_ID = <?= (int)$id ?>;
const API_ENDPOINT = 'get_json_by_code.php';
const INCLUDE_SOURCE_CODE_COLUMN = true;
const UNWRAP_SINGLE_DATA_ITEM = true;
const jsonCodeForm = document.getElementById('jsonCodeForm');
const pasteJsonForm = document.getElementById('pasteJsonForm');
const jsonCodeInput = document.getElementById('json_code');
const manualJsonInput = document.getElementById('manual_json');
const manualJsonReferenceInput = document.getElementById('manual_json_reference');
const clearCodeBtn = document.getElementById('clearCodeBtn');
const clearManualJsonBtn = document.getElementById('clearManualJsonBtn');
const loader = document.getElementById('loader');
const errorContainer = document.getElementById('errorContainer');
const successContainer = document.getElementById('successContainer');
const tableContainer = document.getElementById('tableContainer');
let jsonRows = [];
let columns = [];
if (jsonCodeInput && !jsonCodeInput.disabled) {
jsonCodeInput.focus();
}
jsonCodeForm.addEventListener('submit', function(e) {
e.preventDefault();
const code = jsonCodeInput.value.trim();
if (!code) {
showError('Inserisci o scansiona un codice.');
return;
}
loadJsonFromApi(code);
});
pasteJsonForm.addEventListener('submit', function(e) {
e.preventDefault();
const rawJson = manualJsonInput.value.trim();
const reference = manualJsonReferenceInput.value.trim() || 'manual-json-' + (jsonRows.length + 1);
if (!rawJson) {
showError('Incolla un JSON prima di aggiungerlo.');
return;
}
try {
const parsedJson = JSON.parse(rawJson);
addJsonRow(parsedJson, reference, 'paste');
manualJsonInput.value = '';
manualJsonReferenceInput.value = '';
showSuccess('JSON incollato aggiunto correttamente.');
} catch (err) {
showError('JSON non valido: ' + err.message);
}
});
clearCodeBtn.addEventListener('click', function() {
jsonCodeInput.value = '';
jsonCodeInput.focus();
});
clearManualJsonBtn.addEventListener('click', function() {
manualJsonInput.value = '';
manualJsonReferenceInput.value = '';
});
function loadJsonFromApi(code) {
hideMessages();
loader.style.display = 'block';
fetch(API_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
template_id: TEMPLATE_ID,
code: code
})
})
.then(response => {
if (!response.ok) {
throw new Error('HTTP status ' + response.status);
}
return response.json();
})
.then(responseData => {
loader.style.display = 'none';
if (responseData.error) {
showError(responseData.error);
return;
}
let jsonPayload = responseData;
let reference = code;
// Supported endpoint response format:
// { success: true, json: {...}, reference: "..." }
if (responseData.json !== undefined) {
jsonPayload = responseData.json;
reference = responseData.reference || responseData.filename || responseData.code || code;
}
addJsonRow(jsonPayload, reference, 'api');
jsonCodeInput.value = '';
jsonCodeInput.focus();
showSuccess('JSON recuperato e aggiunto correttamente.');
})
.catch(error => {
loader.style.display = 'none';
showError('Errore durante il recupero del JSON: ' + error.message);
});
}
function addJsonRow(jsonPayload, reference, sourceType) {
// Ogni elemento di data[] diventa una riga (non colonne data.0.*, data.1.*).
const records = extractRecords(jsonPayload);
records.forEach((record, recordIndex) => {
const flattened = flattenJson(record);
const rowReference = records.length > 1 ?
reference + '#' + (recordIndex + 1) :
reference;
if (INCLUDE_SOURCE_CODE_COLUMN) {
flattened.source_code = rowReference;
flattened.source_type = sourceType;
}
const newColumns = Object.keys(flattened).filter(col => !columns.includes(col));
columns = columns.concat(newColumns);
jsonRows.push({
excelrow: 'JSON-' + (jsonRows.length + 1),
reference: rowReference,
sourceType: sourceType,
flat: flattened
});
});
renderTable();
}
// Record da trasformare in righe: gli oggetti in data[], altrimenti il payload stesso.
function extractRecords(payload) {
if (
payload &&
typeof payload === 'object' &&
!Array.isArray(payload) &&
Array.isArray(payload.data) &&
payload.data.length > 0
) {
const items = payload.data.filter(item => item && typeof item === 'object');
if (items.length > 0) {
return items;
}
}
return [payload];
}
function flattenJson(value, prefix = '', result = {}) {
if (value === null || value === undefined) {
result[prefix || 'value'] = '';
return result;
}
if (Array.isArray(value)) {
if (value.length === 0) {
result[prefix || 'value'] = '';
return result;
}
value.forEach((item, index) => {
const newPrefix = prefix ? prefix + '.' + index : String(index);
flattenJson(item, newPrefix, result);
});
return result;
}
if (typeof value === 'object') {
const keys = Object.keys(value);
if (keys.length === 0) {
result[prefix || 'value'] = '';
return result;
}
keys.forEach(key => {
const newPrefix = prefix ? prefix + '.' + key : key;
flattenJson(value[key], newPrefix, result);
});
return result;
}
result[prefix || 'value'] = String(value);
return result;
}
function buildImportData() {
const rows = jsonRows.map(row => columns.map(col => row.flat[col] ?? ''));
const excelData = rows.map((rowData, index) => ({
excelrow: jsonRows[index].excelrow,
data: rowData
}));
return {
template_id: TEMPLATE_ID,
columns: columns,
rows: rows,
excel_data: excelData,
filename: 'json_import_' + new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
};
}
function renderTable() {
if (jsonRows.length === 0) {
tableContainer.innerHTML = '';
return;
}
const data = buildImportData();
let html = `
<form id="selectRowsForm" action="import_insert.php" method="POST">
<input type="hidden" name="template_id" value="${escapeHtml(data.template_id)}">
<input type="hidden" name="columns" value="${encodeURIComponent(JSON.stringify(data.columns))}">
<input type="hidden" name="rows" id="selectedRowsData" value="">
<input type="hidden" name="excelrows" id="selectedExcelRowsData" value="">
<input type="hidden" name="filename" value="${escapeHtml(data.filename)}">
<input type="hidden" name="source_type" value="json">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
<div>
<strong>JSON rows loaded:</strong> ${jsonRows.length}
<span class="badge bg-secondary source-badge ms-2">Columns: ${data.columns.length}</span>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-danger btn-sm" id="clearAllRowsBtn">Clear all rows</button>
<button type="submit" class="btn btn-primary" id="proceedButtonTop" disabled>Prosegui</button>
</div>
</div>
<div class="top-scrollbar" id="topTableScrollbar">
<div class="top-scrollbar-inner" id="topTableScrollbarInner"></div>
</div>
<div class="table-container" id="mainTableContainer">
<table class="table table-striped table-bordered" id="importPreviewTable">
<thead>
<tr>
<th><input type="checkbox" id="selectAll"> Select</th>
${data.columns.map(col => `<th title="${escapeHtml(col)}">${escapeHtml(readableColumnLabel(col))}<div class="resize-handle"></div></th>`).join('')}
<th>Action</th>
</tr>
<tr class="column-filters">
<th></th>
${data.columns.map((col, i) => `
<th>
<input type="text"
class="form-control form-control-sm column-filter"
data-col-index="${i}"
placeholder="Filter...">
</th>
`).join('')}
<th></th>
</tr>
</thead>
<tbody>
${data.excel_data.map((row, index) => `
<tr data-row-index="${index}">
<td><input type="checkbox" class="row-checkbox" name="selected_rows[]" value="${index}" data-excelrow="${escapeHtml(row.excelrow)}"></td>
${row.data.map(cell => `<td title="${escapeHtml(cell)}">${escapeHtml(cell)}</td>`).join('')}
<td><button type="button" class="btn btn-sm btn-outline-danger remove-row-btn" data-row-index="${index}">Remove</button></td>
</tr>
`).join('')}
</tbody>
</table>
</div>
<button type="submit" class="btn btn-primary mt-3" id="proceedButtonBottom" disabled>Prosegui</button>
</form>
`;
tableContainer.innerHTML = html;
bindTableEvents(data);
}
function bindTableEvents(data) {
const selectRowsForm = document.getElementById('selectRowsForm');
const clearAllRowsBtn = document.getElementById('clearAllRowsBtn');
const removeRowButtons = document.querySelectorAll('.remove-row-btn');
selectRowsForm.addEventListener('submit', function(e) {
const checkedBoxes = Array.from(document.querySelectorAll('.row-checkbox:checked'));
if (checkedBoxes.length === 0) {
e.preventDefault();
alert('Seleziona almeno una riga.');
return;
}
const selectedRows = [];
const selectedExcelRows = [];
checkedBoxes.forEach((cb, newIndex) => {
const originalIndex = parseInt(cb.value, 10);
if (data.rows && data.rows[originalIndex]) {
selectedRows.push(data.rows[originalIndex]);
}
if (data.excel_data && data.excel_data[originalIndex]) {
selectedExcelRows.push(data.excel_data[originalIndex].excelrow);
}
// Reindex selected_rows so import_insert.php receives only the reduced rows array
cb.value = newIndex;
});
document.getElementById('selectedRowsData').value =
encodeURIComponent(JSON.stringify(selectedRows));
document.getElementById('selectedExcelRowsData').value =
encodeURIComponent(JSON.stringify(selectedExcelRows));
});
clearAllRowsBtn.addEventListener('click', function() {
if (!confirm('Vuoi rimuovere tutte le righe JSON caricate?')) return;
jsonRows = [];
columns = [];
renderTable();
showSuccess('Righe JSON rimosse.');
jsonCodeInput.focus();
});
removeRowButtons.forEach(button => {
button.addEventListener('click', function() {
const rowIndex = parseInt(this.dataset.rowIndex, 10);
jsonRows.splice(rowIndex, 1);
rebuildColumnsFromRows();
renderTable();
});
});
const topTableScrollbar = document.getElementById('topTableScrollbar');
const topTableScrollbarInner = document.getElementById('topTableScrollbarInner');
const mainTableContainer = document.getElementById('mainTableContainer');
const importPreviewTable = document.getElementById('importPreviewTable');
function updateTopTableScrollbar() {
if (!topTableScrollbar || !topTableScrollbarInner || !mainTableContainer || !importPreviewTable) return;
topTableScrollbarInner.style.width = importPreviewTable.scrollWidth + 'px';
if (mainTableContainer.scrollWidth > mainTableContainer.clientWidth) {
topTableScrollbar.style.display = 'block';
} else {
topTableScrollbar.style.display = 'none';
}
}
let syncingTop = false;
let syncingBottom = false;
if (topTableScrollbar && mainTableContainer) {
topTableScrollbar.addEventListener('scroll', function() {
if (syncingBottom) return;
syncingTop = true;
mainTableContainer.scrollLeft = topTableScrollbar.scrollLeft;
syncingTop = false;
});
mainTableContainer.addEventListener('scroll', function() {
if (syncingTop) return;
syncingBottom = true;
topTableScrollbar.scrollLeft = mainTableContainer.scrollLeft;
syncingBottom = false;
});
}
updateTopTableScrollbar();
setTimeout(updateTopTableScrollbar, 100);
setTimeout(updateTopTableScrollbar, 300);
window.addEventListener('resize', updateTopTableScrollbar);
const proceedButtonTop = document.getElementById('proceedButtonTop');
const proceedButtonBottom = document.getElementById('proceedButtonBottom');
const selectAllCheckbox = document.getElementById('selectAll');
const checkboxes = document.querySelectorAll('.row-checkbox');
function updateProceedButton() {
const enabled = Array.from(document.querySelectorAll('.row-checkbox')).some(cb => cb.checked);
if (proceedButtonTop) proceedButtonTop.disabled = !enabled;
if (proceedButtonBottom) proceedButtonBottom.disabled = !enabled;
}
selectAllCheckbox.addEventListener('change', function() {
const visibleRows = Array.from(document.querySelectorAll('#importPreviewTable tbody tr'))
.filter(row => row.style.display !== 'none');
visibleRows.forEach(row => {
const checkbox = row.querySelector('.row-checkbox');
if (checkbox) {
checkbox.checked = this.checked;
}
});
updateProceedButton();
});
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
const visibleCheckboxes = Array.from(document.querySelectorAll('#importPreviewTable tbody tr'))
.filter(row => row.style.display !== 'none')
.map(row => row.querySelector('.row-checkbox'))
.filter(cb => cb !== null);
selectAllCheckbox.checked =
visibleCheckboxes.length > 0 &&
visibleCheckboxes.every(cb => cb.checked);
updateProceedButton();
});
});
const thElements = document.querySelectorAll('#importPreviewTable th');
thElements.forEach((th, index) => {
if (index === 0) return;
const resizeHandle = th.querySelector('.resize-handle');
if (resizeHandle) {
resizeHandle.addEventListener('mousedown', (e) => {
e.preventDefault();
const startX = e.clientX;
const startWidth = th.offsetWidth;
const onMouseMove = (e) => {
const newWidth = Math.max(60, startWidth + (e.clientX - startX));
th.style.width = `${newWidth}px`;
th.style.minWidth = `${newWidth}px`;
th.style.maxWidth = `${newWidth}px`;
const cells = document.querySelectorAll(`#importPreviewTable td:nth-child(${index + 1})`);
cells.forEach(cell => {
cell.style.width = `${newWidth}px`;
cell.style.minWidth = `${newWidth}px`;
cell.style.maxWidth = `${newWidth}px`;
});
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
}
});
const rows = document.querySelectorAll('#importPreviewTable tbody tr');
const filterInputs = document.querySelectorAll('.column-filter');
const activeFilters = {};
function applyColumnFilters() {
rows.forEach(row => {
let visible = true;
for (const [colIndexStr, filterValue] of Object.entries(activeFilters)) {
const colIndex = parseInt(colIndexStr, 10);
const cell = row.cells[colIndex + 1];
const cellText = (cell?.textContent || '').toLowerCase();
const searchText = (filterValue || '').toLowerCase().trim();
if (searchText && !cellText.includes(searchText)) {
visible = false;
break;
}
}
row.style.display = visible ? '' : 'none';
});
const visibleCheckboxes = Array.from(document.querySelectorAll('#importPreviewTable tbody tr'))
.filter(row => row.style.display !== 'none')
.map(row => row.querySelector('.row-checkbox'))
.filter(cb => cb !== null);
selectAllCheckbox.checked =
visibleCheckboxes.length > 0 &&
visibleCheckboxes.every(cb => cb.checked);
updateProceedButton();
}
filterInputs.forEach(input => {
input.addEventListener('input', function() {
const colIndex = this.dataset.colIndex;
activeFilters[colIndex] = this.value;
applyColumnFilters();
});
});
updateProceedButton();
}
function rebuildColumnsFromRows() {
columns = [];
jsonRows.forEach(row => {
Object.keys(row.flat).forEach(col => {
if (!columns.includes(col)) {
columns.push(col);
}
});
});
}
function readableColumnLabel(columnName) {
if (!columnName) return 'Column without name';
return columnName;
}
function showError(message) {
successContainer.style.display = 'none';
errorContainer.textContent = message;
errorContainer.style.display = 'block';
}
function showSuccess(message) {
errorContainer.style.display = 'none';
successContainer.textContent = message;
successContainer.style.display = 'block';
setTimeout(() => {
successContainer.style.display = 'none';
}, 3500);
}
function hideMessages() {
errorContainer.style.display = 'none';
successContainer.style.display = 'none';
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
});
</script>
</body>
</html>
+21 -154
View File
@@ -147,7 +147,6 @@ $desiredOrder = [
'ClienteResponsabile',
'ClienteFornitore',
'ClienteAnalisi',
'ClienteFatturazione',
'AnagraficaCertestObject',
'AnagraficaCertestService',
'MoltiplicatorePrezzo',
@@ -182,7 +181,6 @@ $fixedAliasMap = [
'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',
@@ -287,21 +285,6 @@ $gridColumns[] = ['type' => 'idclient', 'key' => 'idclient', 'label' => 'Client'
// 4. Cliente Fornitore
$gridColumns[] = ['type' => 'cliente_fornitore_id', 'key' => 'cliente_fornitore_id', 'label' => $slugMapping['ClienteFornitore'] ?? 'ClienteFornitore', 'width' => 300];
// 4b. Fatturazione Avanzata — subito dopo Cliente Fornitore
foreach ($fixedFields as $f) {
if ($f['fixed_field_key'] === 'ClienteFatturazione') {
$gridColumns[] = [
'type' => 'fixed',
'key' => 'ClienteFatturazione',
'label' => 'Fatturazione avanzata',
'dataType' => $f['data_type'],
'isRequired' => (bool)$f['is_required'],
'width' => 180,
];
break;
}
}
// 5. Other custom fields in schema order
foreach ($allMappings as $mapping) {
if (
@@ -338,18 +321,7 @@ $gridColumns[] = ['type' => 'tracking', 'key' => 'tracking', 'label' => 'Trackin
// 10. Fixed fields (with importreferencecode/filename/importdate after ConsegnaRichiesta)
foreach ($fixedFields as $f) {
$key = $f['fixed_field_key'];
if ($key === 'ClienteFatturazione') continue; // già renderizzata al punto 4b
// Cambia solo il titolo della colonna ClienteAnalisi
if ($key === 'ClienteAnalisi') {
$label = 'Buyer';
} elseif ($key === 'ClienteFatturazione') {
$label = 'Fatturazione avanzata';
} else {
$label = $slugMapping[$key] ?? $key;
}
$label = $slugMapping[$key] ?? $key;
$gridColumns[] = [
'type' => 'fixed',
'key' => $key,
@@ -358,7 +330,6 @@ foreach ($fixedFields as $f) {
'isRequired' => (bool)$f['is_required'],
'width' => 180,
];
if ($key === 'ConsegnaRichiesta') {
$gridColumns[] = ['type' => 'static', 'key' => 'importreferencecode', 'label' => $slugMapping['importreferencecode'] ?? 'Import Reference Code', 'width' => 150, 'editable' => false];
$gridColumns[] = ['type' => 'static', 'key' => 'filename_import', 'label' => $slugMapping['filename_import'] ?? 'File', 'width' => 150, 'editable' => false];
@@ -497,16 +468,8 @@ $gridMeta = [
border: 1px solid #dee2e6;
border-radius: 0.25rem;
background: #f8f9fa;
position: sticky;
top: 70px;
/* regola sull'altezza reale della tua topbar fissa */
position: sticky;
top: 70px;
z-index: 30;
}
.top-scrollbar-inner {
height: 1px;
}
@@ -787,7 +750,6 @@ $gridMeta = [
padding: 10px 0;
min-height: 0;
flex-wrap: nowrap;
min-width: fit-content;
}
.grid-top .grid-cell {
@@ -1329,117 +1291,6 @@ $gridMeta = [
outline: 2px solid #dc3545 !important;
outline-offset: -2px;
}
/* ── Filtri / selezione righe ── */
#filtersToggleBtn.active {
background-color: #0dcaf0 !important;
color: #fff !important;
border-color: #0dcaf0 !important;
}
/* checkbox dentro la cella Actions */
.filter-cb-wrap {
display: none;
align-items: center;
justify-content: center;
margin-right: 6px;
flex-shrink: 0;
}
.grid-container.filters-on .filter-cb-wrap {
display: inline-flex;
}
.filter-cb-wrap input {
width: 18px !important;
height: 18px !important;
cursor: pointer;
}
.grid-container.filters-on .button-cell {
display: flex;
align-items: center;
}
.grid-row.row-selected {
background-color: #cfe9ff !important;
}
.grid-row.row-selected .button-cell,
.grid-row.row-selected .grid-cell:nth-child(2),
.grid-row.row-selected .grid-cell:nth-child(3) {
background-color: #cfe9ff !important;
}
.grid-filter-row {
background: #eef6f9 !important;
border-bottom: 2px solid #0dcaf0;
}
.grid-filter-row .grid-cell {
padding: 4px 8px;
}
.grid-filter-row .filter-actions-cell {
background: #eef6f9 !important;
}
.grid-container.filters-on .grid-row {
cursor: pointer;
}
.grid-container.filters-on .grid-row input,
.grid-container.filters-on .grid-row select,
.grid-container.filters-on .grid-row textarea,
.grid-container.filters-on .grid-row button,
.grid-container.filters-on .grid-row a {
cursor: auto;
}
/* Sticky su riga propagazione e filtri (fix scroll orizzontale) */
.grid-top .grid-cell.save-all-cell {
position: sticky !important;
left: 0;
z-index: 9;
background: #fff;
}
.grid-top .grid-cell:nth-child(2) {
position: sticky !important;
left: 210px;
z-index: 8;
background: #fff;
}
<?php if (isset($mainFieldMappings) && count($mainFieldMappings) >= 2): ?>.grid-top .grid-cell:nth-child(3) {
position: sticky !important;
left: 360px;
z-index: 7;
background: #fff;
}
<?php endif; ?>.grid-filter-row .filter-actions-cell {
position: sticky !important;
left: 0;
z-index: 9;
background: #eef6f9 !important;
}
.grid-filter-row .grid-cell:nth-child(2) {
position: sticky !important;
left: 210px;
z-index: 8;
background: #eef6f9 !important;
}
<?php if (isset($mainFieldMappings) && count($mainFieldMappings) >= 2): ?>.grid-filter-row .grid-cell:nth-child(3) {
position: sticky !important;
left: 360px;
z-index: 7;
background: #eef6f9 !important;
}
<?php endif; ?>
</style>
<title>Edit Imported Data - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
</head>
@@ -1450,9 +1301,17 @@ $gridMeta = [
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<?php $autoBoundCount = isset($_GET['autobound']) ? (int) $_GET['autobound'] : 0; ?>
<?php if ($autoBoundCount > 0): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?= $autoBoundCount ?> valore/i collegato/i automaticamente al LIMS durante l'import.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<div class="mb-3 text d-flex align-items-center gap-2">
<a href="imported.php?id=<?= $template_id ?>" class="btn btn-warning me-2">Imported (i)</a>
<a href="tolims.php?id=<?= $template_id ?>" class="btn btn-success">To LIMS (l)</a>
<a href="bindings_manage.php?template_id=<?= $template_id ?>" class="btn btn-outline-secondary">Binding JSON &rarr; LIMS</a>
<?php if ($importref === ''): ?>
<span class="ms-3">
<label class="form-check-label" style="font-size: 13px; cursor: pointer;">
@@ -1516,8 +1375,17 @@ $gridMeta = [
<div class="card radius-10">
<div class="card-header">
<div class="d-flex align-items-center" style="min-height: 42px; gap: 12px;">
<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>
<div class="dropdown actions-dropdown" style="flex-shrink: 0;">
<button class="dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-cogs"></i> Actions
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item export-all-lims-btn" href="#"><i class="fas fa-upload" style="color: #eb0b0b;"></i>Export All</a></li>
<li><a class="dropdown-item save-all-btn" href="#"><i class="fas fa-save" style="color: #28a745;"></i>Save All</a></li>
</ul>
</div>
<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>
<div id="unsavedChanges" style="display:none; color: red; font-weight: bold; font-size: 13px;">
⚠️ Unsaved changes detected!
@@ -1568,7 +1436,6 @@ $gridMeta = [
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script src="tracking.js"></script>
<script src="gridRenderer.js"></script>
<script src="gridFilter.js"></script>
<script src="saveAll.js"></script>
<script src="exportLims_gridData.js"></script>
<script src="modals_gridData.js"></script>
@@ -1807,4 +1674,4 @@ $gridMeta = [
</body>
</html>
</html>
@@ -1,213 +0,0 @@
<?php
if (!function_exists('gdbFixedDefaultValue')) {
/** Default di un fixed field (DATE 'today' → data odierna). */
function gdbFixedDefaultValue(array $f): string
{
$v = $f['default_value'] ?? '';
if (($f['data_type'] ?? '') === 'DATE' && $v === 'today') {
return date('Y-m-d');
}
return (string)$v;
}
}
if (!function_exists('buildGridConfig')) {
/**
* Config del template necessaria per costruire le righe: fixed fields (ordinati
* come in imported.php), main field mappings, alias map, default idclient.
*
* @return array{fixedFields:array, fixedAliasMap:array, mainFieldMappings:array, default_idclient:mixed}
*/
function buildGridConfig(PDO $pdo, int $templateId): array
{
// Mappa logica fixed_field_key → colonna reale su datadb (come imported.php)
$fixedAliasMap = [
'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',
];
// Fixed fields visibili, ordinati come imported.php
$fixedStmt = $pdo->prepare("
SELECT id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import
FROM template_fixed_mapping
WHERE template_id = ? AND is_visible_import = 1
ORDER BY id
");
$fixedStmt->execute([$templateId]);
$fixedFieldsRaw = $fixedStmt->fetchAll(PDO::FETCH_ASSOC);
$desiredOrder = [
'ClienteResponsabile', 'ClienteFornitore', 'ClienteAnalisi', 'ClienteFatturazione',
'AnagraficaCertestObject', 'AnagraficaCertestService', 'MoltiplicatorePrezzo', 'ConsegnaRichiesta',
];
$excludeFromFixed = ['ClienteFornitore']; // reso come colonna a sé
$fixedFields = [];
$tempMap = [];
foreach ($fixedFieldsRaw as $f) {
if (in_array($f['fixed_field_key'], $excludeFromFixed, true)) continue;
$tempMap[$f['fixed_field_key']] = $f;
}
foreach ($desiredOrder as $key) {
if (isset($tempMap[$key])) {
$fixedFields[] = $tempMap[$key];
unset($tempMap[$key]);
}
}
foreach ($tempMap as $f) {
$fixedFields[] = $f;
}
// Main field mappings (max 2), come imported.php
$mapStmt = $pdo->prepare("
SELECT id, field_label, data_type, is_required, field_id, field_order,
main_field, is_visible_import, manual_default
FROM template_mapping
WHERE template_id = ?
ORDER BY field_order ASC, id ASC
");
$mapStmt->execute([$templateId]);
$allMappings = $mapStmt->fetchAll(PDO::FETCH_ASSOC);
$mainFieldMappings = [];
foreach ($allMappings as $mapping) {
if ((string)$mapping['main_field'] === '1' && (int)$mapping['is_visible_import'] === 1) {
$mainFieldMappings[] = $mapping;
}
if (count($mainFieldMappings) >= 2) break;
}
// Default idclient dal template
$tplStmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
$tplStmt->execute([$templateId]);
$default_idclient = $tplStmt->fetchColumn();
$default_idclient = $default_idclient !== false ? $default_idclient : null;
return [
'fixedFields' => $fixedFields,
'fixedAliasMap' => $fixedAliasMap,
'mainFieldMappings' => $mainFieldMappings,
'default_idclient' => $default_idclient,
];
}
}
if (!function_exists('buildGridRows')) {
/**
* Costruisce le righe gridData per la lista di iddatadb data, nell'ordine passato.
*
* @param int[] $iddatadbList
* @param array $config output di buildGridConfig()
* @return array righe nella stessa forma di imported.php $gridDataArray
*/
function buildGridRows(PDO $pdo, array $iddatadbList, array $config): array
{
$iddatadbList = array_values(array_filter(array_map('intval', $iddatadbList), fn($v) => $v > 0));
if (empty($iddatadbList)) {
return [];
}
$fixedFields = $config['fixedFields'];
$fixedAliasMap = $config['fixedAliasMap'];
$mainFieldMappings = $config['mainFieldMappings'];
$default_idclient = $config['default_idclient'];
$ph = implode(',', array_fill(0, count($iddatadbList), '?'));
// Righe datadb + user_name
$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
WHERE d.iddatadb IN ($ph)
");
$stmt->execute($iddatadbList);
$byId = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
$byId[(int)$r['iddatadb']] = $r;
}
// Dettagli (import_data_details)
$detStmt = $pdo->prepare("
SELECT d.id AS datadb_id, d.mapping_id, d.field_value,
m.field_id, m.field_label, m.data_type, m.is_required, m.manual_default
FROM import_data_details d
JOIN template_mapping m ON d.mapping_id = m.id
WHERE d.id IN ($ph)
");
$detStmt->execute($iddatadbList);
$detailsByRow = [];
foreach ($detStmt->fetchAll(PDO::FETCH_ASSOC) as $d) {
$detailsByRow[(int)$d['datadb_id']][] = $d;
}
// Costruzione righe nell'ordine della lista passata
$rows = [];
foreach ($iddatadbList as $id) {
$row = $byId[$id] ?? null;
if ($row === null) continue;
$rowObj = [
'iddatadb' => (int)$row['iddatadb'],
'status' => $row['status'] ?? 'i',
'idclient' => $row['idclient'] ?? $default_idclient,
'cliente_fornitore_id' => $row['cliente_fornitore_id'] ?? null,
'tested_component' => $row['tested_component'] ?? '',
'commessaweb' => $row['commessaweb'] ?? null,
'user_name' => $row['user_name'] ?? '',
'importreferencecode' => $row['importreferencecode'] ?? '',
'filename_import' => $row['filename_import'] ?? '',
'importdate' => $row['importdate'] ?? '',
];
// Fixed fields
$rowObj['fixedFields'] = [];
foreach ($fixedFields as $f) {
$key = $f['fixed_field_key'];
$dbCol = $fixedAliasMap[$key] ?? $key;
$val = $row[$dbCol] ?? '';
if ($val === '' || $val === null) {
$val = gdbFixedDefaultValue($f);
}
$rowObj['fixedFields'][$key] = (string)$val;
}
// Details
$rowObj['details'] = [];
$rowDetails = $detailsByRow[$id] ?? [];
foreach ($rowDetails as $d) {
$rowObj['details'][(string)$d['mapping_id']] = $d['field_value'] ?? '';
}
// Main field values
foreach ($mainFieldMappings as $mainMapping) {
$found = null;
foreach ($rowDetails as $d) {
if ($d['mapping_id'] == $mainMapping['id']) {
$found = $d;
break;
}
}
$rowObj['details'][(string)$mainMapping['id']] =
($found['field_value'] ?? null) ?? ($mainMapping['manual_default'] ?? '');
}
if (!empty($mainFieldMappings)) {
$firstMain = $mainFieldMappings[0];
$rowObj['mainFieldValue'] = $rowObj['details'][(string)$firstMain['id']] ?? '';
}
$rowObj['_dirty'] = false;
$rows[] = $rowObj;
}
return $rows;
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ $db = DBHandlerSelect::getInstance()->getConnection();
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
error_reporting(E_ALL | E_STRICT);
// This should be equal to: PATH_TO_VANGUARD_FOLDER/extra/auth.php
include('../../extra/auth.php');
//require_once __DIR__ . '/extra/auth.php';
+55 -108
View File
@@ -6,163 +6,110 @@
<div>
<h4 class="logo-text"><?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></h4>
</div>
<div class="toggle-icon ms-auto">
<i class='bx bx-arrow-back'></i>
<div class="toggle-icon ms-auto"><i class='bx bx-arrow-back'></i>
</div>
</div>
<!--navigation-->
<ul class="metismenu" id="menu">
<?php
$currentUser = Auth::user();
$isAdmin = $currentUser->hasRole('Admin');
$isSuperUser = $currentUser->hasRole('SuperUser');
$isUser = $currentUser->hasRole('User');
$canAccessMainMenu = $isAdmin || $isSuperUser || $isUser;
$canAccessUserAdmin = $isAdmin || $isSuperUser;
$canAccessTemplates = $isAdmin || $isSuperUser;
?>
<!-- user, admin, superuser menu -->
<?php if ($canAccessMainMenu) : ?>
<!-- user, admin, superuser menù -->
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('User')) || (Auth::user()->hasRole('Superuser'))) : ?>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon">
<i class='bx bx-home-alt'></i>
<div class="parent-icon"><i class='bx bx-home-alt'></i>
</div>
<div class="menu-title">Dashboard</div>
</a>
<ul>
<li>
<a href="import_dashboard.php">
<i class='bx bx-radio-circle'></i>XLS Import
</a>
<!-- <li> <a href="index.php"><i class='bx bx-radio-circle'></i>Default</a>
</li> -->
<li> <a href="import_dashboard.php"><i class='bx bx-radio-circle'></i>Import AREA</a>
</li>
</ul>
</li>
<?php if ($canAccessTemplates) : ?>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon">
<i class="bx bx-category"></i>
</div>
<div class="menu-title">Templates</div>
</a>
<ul>
<li>
<a href="templates_dashboard.php">
<i class='bx bx-radio-circle'></i><?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?>
</a>
</li>
<li>
<a href="insert_template_xls.php">
<i class='bx bx-radio-circle'></i><?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?>
</a>
</li>
</ul>
</li>
<?php endif; ?>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon">
<i class="bx bx-category"></i>
<div class="parent-icon"><i class="bx bx-category"></i>
</div>
<div class="menu-title">Templates</div>
</a>
<ul>
<li> <a href="templates_dashboard.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?></a>
</li>
<li> <a href="insert_template_xls.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?></a>
</li>
</ul>
</li>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class="bx bx-category"></i>
</div>
<div class="menu-title">Other Functions</div>
</a>
<ul>
<li>
<a href="quotations.php">
<i class='bx bx-radio-circle'></i><?= htmlspecialchars($quotationstitle, ENT_QUOTES, 'UTF-8'); ?>
</a>
<li> <a href="quotations.php"><i class='bx bx-radio-circle'></i><?php echo $quotationstitle; ?></a>
</li>
<li> <a href="bindings_manage.php"><i class='bx bx-radio-circle'></i>Binding JSON &rarr; LIMS</a>
</li>
</ul>
</li>
<?php if ($canAccessUserAdmin) : ?>
<li class="menu-label">Administration</li>
<li>
<a href="user-admin.php">
<div class="parent-icon">
<i class="bx bx-user-plus"></i>
</div>
<div class="menu-title">User Admin</div>
</a>
</li>
<li>
<a href="stats_export_lims.php">
<div class="parent-icon">
<i class="bx bx-bar-chart-alt-2"></i>
</div>
<div class="menu-title">Stats</div>
</a>
</li>
<li>
<a href="warm_cache_run.php">
<div class="parent-icon">
<i class="bx bx-bar-chart-alt-2"></i>
</div>
<div class="menu-title">Warm Cache</div>
</a>
</li>
<?php endif; ?>
<li class="menu-label">Reports</li>
<li>
<a href="rapporti_cliente_lookup.php" target="">
<div class="parent-icon"><i class="bx bx-file-find"></i>
</div>
<div class="menu-title">Ricerca Reports</div>
</a>
</li>
<li class="menu-label">Others</li>
<li>
<a href="https://helpdesk.cesoft.io" target="_blank">
<div class="parent-icon">
<i class="bx bx-support"></i>
<div class="parent-icon"><i class="bx bx-support"></i>
</div>
<div class="menu-title">Support</div>
</a>
</li>
<?php endif; ?>
<!-- admin only menu -->
<?php if ($isAdmin) : ?>
<?php
endif; ?>
<!-- admin, superuser menù -->
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('Superuser'))) : ?>
<?php
endif; ?>
<!-- admin menù -->
<?php if (Auth::user()->hasRole('Admin')) : ?>
<li class="menu-label">Admin Menù</li>
<li>
<a href="../" target="_blank">
<div class="parent-icon">
<i class="bx bx-support"></i>
<div class="parent-icon"><i class="bx bx-support"></i>
</div>
<div class="menu-title">User Management</div>
</a>
</li>
<!--
<li>
<!-- <li>
<a href="template/index.html" target="_blank">
<div class="parent-icon">
<i class="bx bx-support"></i>
<div class="parent-icon"><i class="bx bx-support"></i>
</div>
<div class="menu-title">Template</div>
<div class="menu-title">Template</div>
</a>
</li>
<li>
<a href="https://codervent.com/rocker/documentation/index.html" target="_blank">
<div class="parent-icon">
<i class="bx bx-folder"></i>
<div class="parent-icon"><i class="bx bx-folder"></i>
</div>
<div class="menu-title">Documentation</div>
</a>
</li>
-->
<?php endif; ?>
</li> -->
<?php
endif; ?>
</ul>
<!--end navigation-->
</div>
-55
View File
@@ -1,55 +0,0 @@
<?php
$popupEnabled = (($_ENV['POPUP_ENABLED'] ?? getenv('POPUP_ENABLED')) === 'true');
if ($popupEnabled):
?>
<div id="app-popup-overlay" style="position:fixed;inset:0;background:rgba(0,0,0,.65);display:flex;align-items:center;justify-content:center;z-index:99999;padding:20px;">
<div style="background:#fff;max-width:640px;width:100%;max-height:90vh;overflow-y:auto;padding:40px;border-radius:14px;position:relative;box-shadow:0 20px 60px rgba(0,0,0,.4);text-align:center;">
<div style="font-size:46px;line-height:1;margin-bottom:10px;">🚀</div>
<h2 style="margin:0 0 8px;font-size:26px;color:#111;">Nuovo sito in produzione</h2>
<p style="font-size:16px;color:#444;margin:0 0 20px;">Il portale si è spostato su un nuovo indirizzo.</p>
<div style="background:#f1f5ff;border:1px solid #c9d8ff;border-radius:10px;padding:16px;margin-bottom:24px;">
<p style="margin:0 0 4px;font-size:13px;color:#667;text-transform:uppercase;letter-spacing:.5px;">Nuovo indirizzo</p>
<p style="margin:0;font-size:19px;font-weight:700;color:#1d4ed8;word-break:break-all;">https://trfsmart-bv.cesoft.io/</p>
</div>
<a href="https://trfsmart-bv.cesoft.io/"
style="display:inline-block;padding:16px 36px;background:#2563eb;color:#fff;text-decoration:none;border-radius:8px;font-size:18px;font-weight:600;box-shadow:0 4px 14px rgba(37,99,235,.35);">
Clicca qui per accedere al nuovo sito
</a>
<p style="font-size:13px;color:#888;margin:16px 0 0;">Ti consigliamo di salvare il nuovo indirizzo nei preferiti.</p>
<hr style="border:none;border-top:1px solid #eee;margin:28px 0 20px;">
<div style="background:#fff8e6;border:1px solid #ffe08a;border-radius:10px;padding:16px 18px;text-align:left;margin-bottom:20px;">
<p style="margin:0;font-size:14px;color:#8a6d00;">
<strong>⚠️ Attenzione:</strong> chiudi questo messaggio <u>solo</u> se riscontri problemi
con il nuovo sito. Continuando su questo portale userai la versione precedente.
</p>
</div>
<button onclick="chiudiPopupMigrazione()"
style="padding:11px 22px;background:#f0f0f0;color:#555;border:1px solid #ddd;border-radius:8px;cursor:pointer;font-size:14px;">
Ho problemi col nuovo sito, resto su questo portale
</button>
</div>
</div>
<script>
function chiudiPopupMigrazione() {
var conferma = confirm(
"Sei sicuro di voler restare sul portale precedente?\n\n" +
"Il nuovo sito è quello ufficiale in produzione. " +
"Chiudi questo avviso solo se hai riscontrato un problema reale sul nuovo sito."
);
if (conferma) {
document.getElementById('app-popup-overlay').remove();
}
}
</script>
<?php endif; ?>
+4 -24
View File
@@ -1,22 +1,8 @@
<header>
<div class="topbar d-flex align-items-center">
<?php
// Controllo ambiente TEST
$testEnv = strtolower($_ENV['TEST_ENVIRONMENT'] ?? getenv('TEST_ENVIRONMENT') ?? 'true');
if ($testEnv === 'true'):
?>
<div class="alert alert-danger border-0 rounded-0 mb-0 py-2 text-center fw-bold fs-5 w-100 shadow-sm"
style="margin: 0 -15px 0 -15px;">
<i class='bx bx-test-tube me-2'></i>
AMBIENTE DI TEST NON È PRODUZIONE
<nav class="navbar navbar-expand gap-3">
<div class="mobile-toggle-menu"><i class='bx bx-menu'></i>
</div>
<?php endif; ?>
<nav class="navbar navbar-expand gap-3 w-100">
<!-- Tutto il resto del tuo codice della topbar rimane identico -->
<div class="mobile-toggle-menu"><i class='bx bx-menu'></i></div>
<div class="search-bar d-lg-block d-none" data-bs-toggle="modal" data-bs-target="#SearchModal">
<!-- <a href="avascript:;" class="btn d-flex align-items-center"><i class='bx bx-search'></i>Search</a> -->
@@ -108,14 +94,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>
<?php if ($user->hasRole('Admin') || $user->hasRole('SuperUser')): ?>
<li>
<a class="dropdown-item d-flex align-items-center" href="user-admin.php">
<i class="bx bx-user-plus fs-5"></i>
<span>User Admin</span>
</a>
</li>
<?php endif; ?>
<li><a class="dropdown-item d-flex align-items-center" href="settings.php"><i class="bx bx-cog fs-5"></i><span>Settings</span></a>
</li>
<li>
<div class="dropdown-divider mb-0"></div>
</li>
+5 -20
View File
@@ -1,6 +1,5 @@
<?php
header('Content-Type: application/json');
include('include/headscript.php');
require_once 'class/db-functions.php';
$response = ["success" => false, "data" => [], "message" => ""];
@@ -13,25 +12,11 @@ try {
throw new Exception('Database connection failed.');
}
$userId = $iduserlogin ?? 0;
// Recupera i template attivi, con flag is_favorite per l'utente corrente
$stmt = $pdo->prepare("
SELECT
et.id,
et.button_label,
et.button_size,
et.button_bg_color,
et.button_text_color,
et.source_type,
CASE WHEN uf.id IS NOT NULL THEN 1 ELSE 0 END AS is_favorite
FROM excel_templates et
LEFT JOIN user_template_favorites uf
ON uf.template_id = et.id AND uf.user_id = :user_id
WHERE et.status = 'active'
ORDER BY et.button_label ASC
");
$stmt->execute(['user_id' => $userId]);
// Recupera solo i template attivi
$stmt = $pdo->query("SELECT id, button_label, button_size, button_bg_color, button_text_color, source_type
FROM excel_templates
WHERE status = 'active'
ORDER BY button_label ASC");
$templates = $stmt->fetchAll(PDO::FETCH_ASSOC);
$response["success"] = true;
+345 -160
View File
@@ -77,8 +77,7 @@ $stmt = $pdo->prepare("
field_label,
main_field,
is_visible_import,
is_visible_parts,
is_addnote
is_visible_parts
FROM template_mapping
WHERE template_id = ?
ORDER BY field_order ASC, id ASC
@@ -88,10 +87,19 @@ $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
FROM template_fixed_mapping
WHERE template_id = ?
ORDER BY id ASC
SELECT
id,
fixed_field_key,
is_manual,
data_type,
is_required,
default_value,
default_source,
json_node,
is_visible_import
FROM template_fixed_mapping
WHERE template_id = ?
ORDER BY id ASC
");
$stmt->execute([$id]);
$fixedMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -184,9 +192,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
#schemaFieldsTable th:nth-child(2),
#schemaFieldsTable td:nth-child(2),
#schemaFieldsTable th:nth-child(3),
#schemaFieldsTable td:nth-child(3),
#schemaFieldsTable th:nth-child(4),
#schemaFieldsTable td:nth-child(4) {
#schemaFieldsTable td:nth-child(3) {
width: 48px;
/* 🔥 più stretto */
text-align: center;
@@ -198,8 +204,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
/* Make checkbox itself not oversized */
#schemaFieldsTable td:nth-child(1) input[type="checkbox"],
#schemaFieldsTable td:nth-child(2) input[type="checkbox"],
#schemaFieldsTable td:nth-child(3) input[type="checkbox"],
#schemaFieldsTable td:nth-child(4) input[type="checkbox"] {
#schemaFieldsTable td:nth-child(3) input[type="checkbox"] {
transform: scale(0.95);
margin: 0;
}
@@ -402,7 +407,6 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
<th style="width:45px; text-align:center;">Main</th>
<th style="width:45px; text-align:center;">Import</th>
<th style="width:45px; text-align:center;">Parts</th>
<th style="width:45px; text-align:center;">Note</th>
<th style="width:320px;">Title</th>
<th style="width:70px; text-align:center;">Order</th>
<th style="width:120px;">Type</th>
@@ -433,12 +437,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
data-mapping-id="<?php echo (int)$mapping['id']; ?>"
<?php echo ((int)($mapping['is_visible_parts'] ?? 0) === 1) ? 'checked' : ''; ?>>
</td>
<td class="text-center">
<input type="checkbox"
class="addnote-checkbox"
data-mapping-id="<?php echo (int)$mapping['id']; ?>"
<?php echo ((int)($mapping['is_addnote'] ?? 0) === 1) ? 'checked' : ''; ?>>
</td>
<td class="title-col">
<?php echo htmlspecialchars($mapping['field_label'] ?? 'N/A'); ?>
<?php if ((int)$mapping['is_required'] === 1): ?>
@@ -452,22 +451,23 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
<td><?php echo htmlspecialchars($mapping['data_type'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($mapping['data_type'] ?? 'N/A'); ?></td>
<td>
<?php
$isSceltaMultipla = ($mapping['data_type'] === 'SceltaMultipla');
$isApiSceltaMultipla = ($sourceType === 'API' && $isSceltaMultipla);
$autoValue = $mapping['auto_value'] ?? 'none';
$hasAuto = ($autoValue && $autoValue !== 'none');
if ($isSceltaMultipla) {
$mappingValue = 'manual';
} elseif ($hasAuto) {
$mappingValue = 'auto';
} elseif ($sourceType === 'XLS' && !empty($mapping['excel_column'])) {
$mappingValue = 'xls';
} elseif ($sourceType === 'API' && !empty($mapping['json_node'])) {
if ($sourceType === 'API' && !empty($mapping['json_node'])) {
$mappingValue = 'json';
} elseif ((int)$mapping['is_manual'] === 1) {
} elseif ($sourceType === 'XLS' && !$isSceltaMultipla && !empty($mapping['excel_column'])) {
$mappingValue = 'xls';
} elseif (!$isSceltaMultipla && $hasAuto) {
$mappingValue = 'auto';
} elseif ((int)$mapping['is_manual'] === 1 || $isSceltaMultipla) {
$mappingValue = 'manual';
} else {
$mappingValue = '';
@@ -477,17 +477,18 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
<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-is-scelta-multipla="<?php echo $isSceltaMultipla ? '1' : '0'; ?>"
<?php echo ($isSceltaMultipla && $sourceType !== 'API') ? 'disabled' : ''; ?>>
<option value="">Select Option</option>
<?php if ($sourceType === 'XLS' && !$isSceltaMultipla): ?>
<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="">Select Option</option>
<?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; ?>
<option value="auto" <?php echo ($mappingValue === 'auto') ? 'selected' : ''; ?>>Auto value</option>
<?php endif; ?>
@@ -617,50 +618,74 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
</td>
<td>
<strong><?php
$fixedFieldLabels = ['ClienteAnalisi' => 'Buyer'];
$fixedKey = $fm['fixed_field_key'];
echo htmlspecialchars($fixedFieldLabels[$fixedKey] ?? $fixedKey);
?></strong>
<strong><?php echo htmlspecialchars($fm['fixed_field_key']); ?></strong>
</td>
<td><?php echo htmlspecialchars($fm['data_type']); ?></td>
<td>
<?php
$fixedDefaultSource = $fm['default_source'] ?? 'manual';
$fixedJsonNode = $fm['json_node'] ?? '';
$showFixedManual = ($fixedDefaultSource !== 'json');
$showFixedJson = ($fixedDefaultSource === 'json');
?>
<?php if ($fm['data_type'] === 'DATE'): ?>
<input type="date"
class="form-control fixed-default-input"
<?php if ($sourceType === 'API'): ?>
<select class="form-select fixed-source-select"
data-fixed-id="<?php echo (int)$fm['id']; ?>"
value="<?php echo htmlspecialchars($fm['default_value'] ?? ''); ?>">
<?php elseif (in_array($fm['fixed_field_key'], [
'ClienteResponsabile',
'ClienteFornitore',
'ClienteAnalisi',
'ClienteFatturazione',
'MoltiplicatorePrezzo',
'AnagraficaCertestObject',
'AnagraficaCertestService'
])): ?>
<select class="form-select fixed-default-select"
data-fixed-id="<?php echo (int)$fm['id']; ?>"
data-fixed-key="<?php echo htmlspecialchars($fm['fixed_field_key']); ?>"
data-current-value="<?php echo htmlspecialchars($fm['default_value'] ?? ''); ?>">
<option value="">Loading...</option>
style="margin-bottom:6px;">
<option value="manual" <?php echo ($fixedDefaultSource === 'manual') ? 'selected' : ''; ?>>
Manual value
</option>
<option value="json" <?php echo ($fixedDefaultSource === 'json') ? 'selected' : ''; ?>>
Map to JSON Node
</option>
</select>
<?php else: ?>
<input type="text"
class="form-control fixed-default-input"
<select class="form-select fixed-json-node-select"
data-fixed-id="<?php echo (int)$fm['id']; ?>"
value="<?php echo htmlspecialchars($fm['default_value'] ?? ''); ?>">
data-current-json="<?php echo htmlspecialchars($fixedJsonNode, ENT_QUOTES, 'UTF-8'); ?>"
style="display:<?php echo $showFixedJson ? 'block' : 'none'; ?>; margin-bottom:6px;">
</select>
<?php endif; ?>
<div class="fixed-manual-wrapper"
style="display:<?php echo $showFixedManual ? 'block' : 'none'; ?>;">
<?php if ($fm['data_type'] === 'DATE'): ?>
<input type="date"
class="form-control fixed-default-input"
data-fixed-id="<?php echo (int)$fm['id']; ?>"
value="<?php echo htmlspecialchars($fm['default_value'] ?? ''); ?>">
<?php elseif (in_array($fm['fixed_field_key'], [
'ClienteResponsabile',
'ClienteFornitore',
'ClienteAnalisi',
'MoltiplicatorePrezzo',
'AnagraficaCertestObject',
'AnagraficaCertestService'
])): ?>
<select class="form-select fixed-default-select"
data-fixed-id="<?php echo (int)$fm['id']; ?>"
data-fixed-key="<?php echo htmlspecialchars($fm['fixed_field_key']); ?>"
data-current-value="<?php echo htmlspecialchars($fm['default_value'] ?? ''); ?>">
<option value="">Loading...</option>
</select>
<?php else: ?>
<input type="text"
class="form-control fixed-default-input"
data-fixed-id="<?php echo (int)$fm['id']; ?>"
value="<?php echo htmlspecialchars($fm['default_value'] ?? ''); ?>">
<?php endif; ?>
</div>
</td>
</tr>
@@ -1196,6 +1221,46 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
initSelect2ForJsonDropdowns();
}
function updateFixedJsonDropdowns() {
document.querySelectorAll('select.fixed-json-node-select').forEach(select => {
let currentValue = select.value || select.dataset.currentJson || '';
let options = availableJsonNodes
.map(node => {
const clean = String(node || '').trim();
if (!clean) return '';
const info = jsonNodeLabels[clean] || {};
const shortName = info.shortName || getLastJsonNodeName(clean);
const sample = info.sample || '';
let label = shortName;
if (sample) {
label += ` — ${sample}`;
}
const isSelected = clean === currentValue ? 'selected' : '';
return `
<option
value="${escapeHtmlAttr(clean)}"
data-short-name="${escapeHtmlAttr(shortName)}"
data-sample="${escapeHtmlAttr(sample)}"
data-full-path="${escapeHtmlAttr(clean)}"
${isSelected}>
${escapeHtmlText(label)}
</option>
`;
})
.join('');
select.innerHTML = '<option value="">Select JSON Node</option>' + options;
select.dataset.currentJson = currentValue;
});
initSelect2ForFixedJsonDropdowns();
}
function initSelect2ForJsonDropdowns() {
if (!(window.jQuery && $.fn.select2)) return;
@@ -1279,7 +1344,81 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
});
}
function initSelect2ForFixedJsonDropdowns() {
if (!(window.jQuery && $.fn.select2)) return;
$('.fixed-json-node-select').each(function() {
const $el = $(this);
if (this.style.display === 'none') {
return;
}
if ($el.hasClass('select2-hidden-accessible')) {
$el.select2('destroy');
}
$el.select2({
width: '100%',
placeholder: 'Select JSON Node',
allowClear: true,
templateResult: function(data) {
if (!data.id) return data.text;
const option = data.element;
const shortName = option.getAttribute('data-short-name') || data.text;
const sample = option.getAttribute('data-sample') || '';
const $row = $('<span class="select2-json-row"></span>');
$row.append(`<span class="select2-json-node">${escapeHtmlText(shortName)}</span>`);
if (sample) {
$row.append(`<span class="select2-json-value">${escapeHtmlText(sample)}</span>`);
}
return $row;
},
templateSelection: function(data) {
if (!data.id) return data.text;
const option = data.element;
const shortName = option.getAttribute('data-short-name') || data.text;
const sample = option.getAttribute('data-sample') || '';
if (sample) {
return `${shortName} — ${sample}`;
}
return shortName;
},
matcher: function(params, data) {
if ($.trim(params.term) === '') {
return data;
}
const term = params.term.toLowerCase();
const option = data.element;
const fullPath = option?.getAttribute('data-full-path')?.toLowerCase() || '';
const shortName = option?.getAttribute('data-short-name')?.toLowerCase() || '';
const sample = option?.getAttribute('data-sample')?.toLowerCase() || '';
if (
fullPath.includes(term) ||
shortName.includes(term) ||
sample.includes(term)
) {
return data;
}
return null;
}
});
});
}
function saveJsonNodes(sampleJson, nodes) {
return fetch('update_api_json_nodes.php', {
@@ -1424,6 +1563,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
availableJsonNodes = nodes;
usedJsonNodesFromDB = [];
updateJsonDropdowns();
updateFixedJsonDropdowns();
try {
const data = await saveJsonNodes(rawJson, nodes);
@@ -1569,9 +1709,12 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
return;
}
const mappingSelect = tr?.querySelector('.mapping-select');
const currentMappingType = mappingSelect ? mappingSelect.value : 'manual';
saveMapping(
mappingId,
'manual',
currentMappingType || 'manual',
el.value,
xlsSelect ? xlsSelect.value : null,
null,
@@ -1601,9 +1744,12 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
return;
}
const mappingSelect = tr?.querySelector('.mapping-select');
const currentMappingType = mappingSelect ? mappingSelect.value : 'manual';
saveMapping(
mappingId,
'manual',
currentMappingType || 'manual',
el.value,
xlsSelect ? xlsSelect.value : null,
null,
@@ -1687,6 +1833,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
const removeBtn = tr.querySelector('.remove-xls');
const removeJsonBtn = tr.querySelector('.remove-json');
const isSceltaMultipla = mappingSelect.dataset.isSceltaMultipla === '1';
function destroyJsonSelect2() {
if (jsonSelect && window.jQuery && $(jsonSelect).hasClass('select2-hidden-accessible')) {
@@ -1729,8 +1876,12 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
if (autoSelect) autoSelect.style.display = 'none';
if (manualInput) {
manualInput.style.display = 'none';
manualInput.value = '';
if (isSceltaMultipla) {
manualInput.style.display = 'block';
} else {
manualInput.style.display = 'none';
manualInput.value = '';
}
}
if (mappedColumn) mappedColumn.style.display = 'none';
@@ -1887,74 +2038,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
document.querySelectorAll('#schemaFieldsBody .main-field-checkbox').forEach(cb => {
cb.dataset.originalChecked = cb.checked ? 'true' : 'false';
});
// =======================
// SAVE: Main / Import / Parts checkboxes
// =======================
function saveMappingFlag(checkboxEl, field) {
const mappingId = checkboxEl.getAttribute('data-mapping-id');
const checked = checkboxEl.checked;
fetch('update_mapping_flag.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: mappingId,
field: field,
value: checked ? 1 : 0
})
})
.then(response => response.json())
.then(data => {
if (!data.success) {
console.error("❌ Error saving flag:", field, data.message);
if (data.limit_reached) {
alert(data.message);
}
// Revert: il server ha rifiutato, riporto la checkbox allo stato precedente
checkboxEl.checked = !checked;
return;
}
console.log("✅ Flag saved:", field, "=", checked ? 1 : 0, "for id", mappingId);
// Se il server ha deselezionato un'altra riga (caso "parts"), rifletto anche in UI
if (data.unchecked_others && field === 'is_visible_parts') {
document.querySelectorAll('#schemaFieldsBody .visible-parts-checkbox').forEach(cb => {
if (cb !== checkboxEl) {
cb.checked = false;
}
});
}
})
.catch(error => {
console.error("❌ Fetch error saving flag:", field, error);
checkboxEl.checked = !checked;
});
}
document.getElementById('schemaFieldsBody').addEventListener('change', function(event) {
const el = event.target;
if (el.classList.contains('main-field-checkbox')) {
saveMappingFlag(el, 'main_field');
}
if (el.classList.contains('visible-import-checkbox')) {
saveMappingFlag(el, 'is_visible_import');
}
if (el.classList.contains('visible-parts-checkbox')) {
saveMappingFlag(el, 'is_visible_parts');
}
if (el.classList.contains('addnote-checkbox')) {
saveMappingFlag(el, 'is_addnote');
}
});
// AUTO VALUE select change -> save auto_value
document.getElementById('schemaFieldsBody').addEventListener('change', function(event) {
if (!event.target.classList.contains('auto-value-select')) return;
@@ -2038,13 +2122,19 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
let mappingId = event.target.getAttribute('data-id');
let xlsSelect = tr.querySelector('.xls-columns');
let jsonSelect = tr.querySelector('.json-nodes');
let mappingSelect = tr.querySelector('.mapping-select');
let currentMappingType = mappingSelect ? mappingSelect.value : 'manual';
console.log("Manual default dropdown changed:", {
id: mappingId,
value: event.target.value
value: event.target.value,
mappingType: currentMappingType
});
saveMapping(
mappingId,
'manual',
currentMappingType || 'manual',
event.target.value,
xlsSelect ? xlsSelect.value : null,
null,
@@ -2143,7 +2233,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
mapping_type: mappingType,
excel_column: mappingType === 'xls' ? excelColumn : null,
json_node: mappingType === 'json' ? jsonNode : null,
manual_default: mappingType === 'manual' ? defaultValue : null,
manual_default: defaultValue,
auto_value: mappingType === 'auto' ? (autoValue || 'none') : 'none',
tablename: "<?php echo $template['target_table']; ?>"
})
@@ -2172,6 +2262,115 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
// FIXED FIELDS (DB autosave)
// =======================
// =======================
// FIXED FIELDS: source manual/json autosave
// Only used for API/JSON templates
// =======================
document.addEventListener('change', function(e) {
if (!e.target.classList.contains('fixed-source-select')) return;
const sourceSelect = e.target;
const fixedId = sourceSelect.dataset.fixedId;
const tr = sourceSelect.closest('tr');
const manualWrapper = tr.querySelector('.fixed-manual-wrapper');
const jsonSelect = tr.querySelector('.fixed-json-node-select');
if (sourceSelect.value === 'json') {
if (manualWrapper) {
manualWrapper.style.display = 'none';
}
if (jsonSelect) {
jsonSelect.style.display = 'block';
updateFixedJsonDropdowns();
}
} else {
if (jsonSelect) {
if (window.jQuery && $(jsonSelect).hasClass('select2-hidden-accessible')) {
$(jsonSelect).select2('destroy');
}
jsonSelect.value = '';
jsonSelect.dataset.currentJson = '';
jsonSelect.style.display = 'none';
}
if (manualWrapper) {
manualWrapper.style.display = 'block';
}
}
fetch('update_fixed_field.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: fixedId,
field: 'default_source',
value: sourceSelect.value
})
})
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(data.message || 'Update failed');
}
fixedStatus('✅ Saved');
})
.catch(error => {
console.error(error);
fixedStatus('❌ Save error', true);
});
});
// =======================
// FIXED FIELDS: JSON node autosave
// =======================
function saveFixedJsonNode(selectEl) {
const fixedId = selectEl.dataset.fixedId;
const jsonNode = selectEl.value || '';
fetch('update_fixed_field.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: fixedId,
field: 'json_node',
value: jsonNode
})
})
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(data.message || 'Update failed');
}
selectEl.dataset.currentJson = jsonNode;
fixedStatus('✅ Saved');
})
.catch(error => {
console.error(error);
fixedStatus('❌ Save error', true);
});
}
document.addEventListener('change', function(e) {
if (!e.target.classList.contains('fixed-json-node-select')) return;
saveFixedJsonNode(e.target);
});
if (window.jQuery) {
$(document).on('select2:select select2:clear', '.fixed-json-node-select', function() {
saveFixedJsonNode(this);
});
}
function escapeHtml(str) {
return String(str ?? '')
.replaceAll('&', '&amp;')
@@ -2196,19 +2395,15 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
const tbody = document.querySelector('#fixedFieldsTable tbody');
if (!tbody) return;
const fixedFieldLabels = {
'ClienteAnalisi': 'Buyer'
};
const keysWithDropdown = [
'ClienteResponsabile',
'ClienteFornitore',
'ClienteAnalisi',
'ClienteFatturazione', // ← AGGIUNGI QUESTA RIGA
'MoltiplicatorePrezzo',
'AnagraficaCertestObject',
'AnagraficaCertestService'
];
tbody.innerHTML = rows.map(r => {
const type = String(r.data_type || '').toUpperCase();
const dv = r.default_value ?? '';
@@ -2231,7 +2426,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
${parseInt(r.is_required) === 1 ? 'checked' : ''}>
</td>
<td><strong>${escapeHtml(fixedFieldLabels[r.fixed_field_key] || r.fixed_field_key)}</strong></td>
<td><strong>${escapeHtml(r.fixed_field_key)}</strong></td>
<td>${escapeHtml(r.data_type || '')}</td>
@@ -2412,6 +2607,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
}
updateJsonDropdowns();
updateFixedJsonDropdowns();
}
fillFixedDropdowns().then(() => {
@@ -2512,22 +2708,11 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
})).filter(x => x.id != null && x.label);
}
if (key === 'ClienteFornitore' || key === 'ClienteAnalisi' || key === 'ClienteFatturazione') {
if (key === 'ClienteFornitore' || key === 'ClienteAnalisi') {
const j = await fetchJson('get_clienti.php');
const list = normalizeList(j, key);
const isInattivo = (x) => {
const v = x.Inattivo;
return v === true || v === 1 || v === '1' || v === 'true';
};
// Fornitore e Fatturazione: solo clienti ATTIVI (Inattivo = false)
// Analisi: solo clienti INATTIVI (Inattivo = true)
const filtered = (key === 'ClienteAnalisi') ?
list.filter(isInattivo) :
list.filter(x => !isInattivo(x));
return filtered.map(x => ({
return list.map(x => ({
id: x.IdCliente ?? x.Id ?? x.id,
label: formatClientLabel(x)
})).filter(x => x.id != null && x.label);
File diff suppressed because one or more lines are too long
@@ -1,106 +0,0 @@
<?php
/**
* ONE-OFF MIGRATION
* Aggiunge la riga ClienteFatturazione a tutti i template che hanno
* già dei fixed fields (quindi hanno già usato "Add Fixed Fields")
* ma non hanno ancora ClienteFatturazione.
*
* Idempotente: si può rilanciare più volte senza creare duplicati.
*
* Uso:
* - Dry run (default): migrate_add_fatturazione.php
* - Esecuzione reale: migrate_add_fatturazione.php?confirm=1
*/
header('Content-Type: application/json');
require_once(__DIR__ . '/include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$confirm = isset($_GET['confirm']) && $_GET['confirm'] == '1';
// Stessi default usati da create_fixed_fields.php per ClienteFatturazione
$FIELD_KEY = 'ClienteFatturazione';
$DATA_TYPE = 'INT';
$IS_MANUAL = 1;
$IS_REQUIRED = 1;
$DEFAULT_VAL = null;
$IS_VISIBLE = 1;
try {
// 1. Tutti i template_id distinti che hanno già fixed fields
$stmt = $pdo->query("SELECT DISTINCT template_id FROM template_fixed_mapping ORDER BY template_id ASC");
$allTemplateIds = $stmt->fetchAll(PDO::FETCH_COLUMN);
// 2. Tra questi, quelli che NON hanno già ClienteFatturazione
$placeholders = implode(',', array_fill(0, count($allTemplateIds), '?'));
$missingTemplateIds = $allTemplateIds;
if (!empty($allTemplateIds)) {
$stmt = $pdo->prepare("
SELECT DISTINCT template_id
FROM template_fixed_mapping
WHERE fixed_field_key = ?
AND template_id IN ({$placeholders})
");
$stmt->execute(array_merge([$FIELD_KEY], $allTemplateIds));
$alreadyHave = $stmt->fetchAll(PDO::FETCH_COLUMN);
$missingTemplateIds = array_values(array_diff($allTemplateIds, $alreadyHave));
}
$report = [
'total_templates_with_fixed_fields' => count($allTemplateIds),
'templates_missing_fatturazione' => count($missingTemplateIds),
'template_ids_missing' => $missingTemplateIds,
'dry_run' => !$confirm,
'inserted' => 0,
];
if (empty($missingTemplateIds)) {
echo json_encode(['success' => true] + $report, JSON_PRETTY_PRINT);
exit;
}
if (!$confirm) {
// Dry run: mostra solo cosa farebbe, non inserisce nulla
echo json_encode(['success' => true] + $report, JSON_PRETTY_PRINT);
exit;
}
// 3. Esecuzione reale
$pdo->beginTransaction();
$ins = $pdo->prepare("
INSERT INTO template_fixed_mapping
(template_id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import)
VALUES
(:template_id, :fixed_field_key, :is_manual, :data_type, :is_required, :default_value, :is_visible_import)
");
$insertedCount = 0;
foreach ($missingTemplateIds as $tid) {
$ins->execute([
':template_id' => $tid,
':fixed_field_key' => $FIELD_KEY,
':is_manual' => $IS_MANUAL,
':data_type' => $DATA_TYPE,
':is_required' => $IS_REQUIRED,
':default_value' => $DEFAULT_VAL,
':is_visible_import' => $IS_VISIBLE,
]);
$insertedCount++;
}
$pdo->commit();
$report['inserted'] = $insertedCount;
echo json_encode(['success' => true] + $report, JSON_PRETTY_PRINT);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+16 -6
View File
@@ -136,9 +136,14 @@ $matrixGroups = array_values($matrixGroups);
<button type="button"
class="list-group-item list-group-item-action analysis-matrix-item <?= $index === 0 ? 'active' : '' ?>"
data-matrix-id="<?= htmlspecialchars((string)($group['idmatrice'] ?? 'NO_MATRIX'), ENT_QUOTES, 'UTF-8') ?>">
<div class="fw-semibold analysis-matrix-name"
data-matrix-id="<?= htmlspecialchars((string)($group['idmatrice'] ?? 'NO_MATRIX'), ENT_QUOTES, 'UTF-8') ?>">
<?= !empty($group['idmatrice']) ? ('#' . (int)$group['idmatrice']) : 'No Matrix' ?>
<div class="fw-semibold">
<?= htmlspecialchars(
!empty($group['NomeMatriceTraduzione'])
? $group['NomeMatriceTraduzione']
: (!empty($group['idmatrice']) ? ('Matrix without translation') : 'No Matrix'),
ENT_QUOTES,
'UTF-8'
) ?>
</div>
<div class="small text-muted">
ID: <?= !empty($group['idmatrice']) ? (int)$group['idmatrice'] : '-' ?>
@@ -188,9 +193,14 @@ $matrixGroups = array_values($matrixGroups);
<div class="small text-muted mt-1">
Matrix:
<strong class="analysis-part-matrix-name"
data-matrix-id="<?= htmlspecialchars($matrixId, ENT_QUOTES, 'UTF-8') ?>">
<?= !empty($part['idmatrice']) ? ('#' . (int)$part['idmatrice']) : 'No Matrix' ?>
<strong>
<?= htmlspecialchars(
!empty($part['NomeMatriceTraduzione'])
? $part['NomeMatriceTraduzione']
: (!empty($part['idmatrice']) ? 'Matrix without translation' : 'No Matrix'),
ENT_QUOTES,
'UTF-8'
) ?>
</strong>
<?php if (!empty($part['idmatrice'])): ?>
<span class="small text-muted">(ID: <?= (int)$part['idmatrice'] ?>)</span>
-7
View File
@@ -57,7 +57,6 @@
style="width: 34px; height: 28px; padding: 1px; border: 1px solid #ccc; border-radius: 4px; cursor: pointer;">
</div>
<button type="button" class="btn btn-primary btn-sm" id="addDescriptionsBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Aggiungi Lista Descrizioni</button>
<button type="button" class="btn btn-outline-dark btn-sm" id="addFreeTextBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;"><i class="fas fa-font"></i> Testo Libero</button>
<button type="button" class="btn btn-danger btn-sm" id="removeAnnotationsBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Rimuovi Descrizioni</button>
<button type="button" class="btn btn-warning btn-sm" id="undoMarkerBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Undo Marker</button>
<button type="button" class="btn btn-success btn-sm" id="savePhotoBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Salva Foto con Nome</button>
@@ -281,11 +280,5 @@
margin: 2px 0;
border-radius: 4px;
}
#addFreeTextBtnAnnotations.active {
background-color: #343a40 !important;
border-color: #343a40 !important;
color: #fff !important;
}
</style>
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.min.js"></script>
-68
View File
@@ -29,9 +29,6 @@
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; min-width: 0;">
<h6 style="margin: 0; white-space: nowrap;">Elenco Parti</h6>
<div style="display: flex; align-items: center; min-width: 0; gap: 8px;">
<button type="button" id="savePartsBtn" class="btn btn-success btn-sm">
<i class="fas fa-save"></i> Salva
</button>
<button type="button"
class="btn btn-dark btn-sm open-analysis-modal-btn"
id="openAnalysisModalBtn"
@@ -48,9 +45,6 @@
<button type="button" class="btn btn-primary btn-sm" id="clonePartsBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
<i class="fas fa-clone"></i> Clona Parti
</button>
<button type="button" class="btn btn-warning btn-sm d-none" id="quotationeBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
<i class="fas fa-file-invoice-dollar"></i> Collega Quotazione
</button>
<button type="button" class="btn btn-secondary btn-sm ms-2" id="toggleVoiceBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
<i class="fas fa-microphone"></i> Voce
</button>
@@ -194,43 +188,6 @@
</div>
</div>
</div>
<!-- Modale di conferma per la clonazione delle parti -->
<div class="modal fade" id="cloneConfirmModal" tabindex="-1" aria-labelledby="cloneConfirmModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="cloneConfirmModalLabel">Conferma Clonazione</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="clone-confirm-message mb-3"></p>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="cloneNotesCheckbox" checked>
<label class="form-check-label" for="cloneNotesCheckbox">
Duplica note
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="cloneAnalysesCheckbox">
<label class="form-check-label" for="cloneAnalysesCheckbox">
Duplica anche le analisi collegate
</label>
</div>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="cloneOverwriteCheckbox" checked>
<label class="form-check-label" for="cloneOverwriteCheckbox">
<strong>Sovrascrivi parti esistenti</strong> (elimina le parti già presenti nei record di destinazione prima di inserire)
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Annulla</button>
<button type="button" class="btn btn-primary btn-sm" id="cloneConfirmBtn">Conferma</button>
</div>
</div>
</div>
</div>
<!-- Finestra modale "Aggiungi preventivo" -->
<div class="modal fade" id="addQuotationModal" tabindex="-1" aria-labelledby="addQuotationModalLabel" aria-hidden="true">
<div class="modal-dialog modal-md">
@@ -251,23 +208,6 @@
</div>
<style>
#partsModalLabel {
font-size: 1rem;
font-weight: 600;
color: #666;
}
#trfHeader {
font-size: 1rem;
font-weight: 500;
color: #666;
}
/* --- Base --- */
#partsModal {
z-index: 1060 !important
}
/* --- Base --- */
#partsModal {
z-index: 1060 !important
@@ -798,13 +738,5 @@
padding: 0.1rem 0.25rem !important;
}
#cloneConfirmModal {
z-index: 1070 !important
}
#cloneConfirmModal .modal-backdrop {
z-index: 1065 !important
}
/* rosso */
</style>
-10
View File
@@ -171,11 +171,6 @@
);
if (idx >= 0) window.gridData.splice(idx, 1);
// Aggiorna la lista dei record visibili (navigazione parti + clone)
window.visibleIddatadbList = window.gridData
.map((r) => parseInt(r.iddatadb, 10))
.filter(Boolean);
// Re-render
const gr = window.gridRenderer;
if (gr) gr.renderVisibleRows();
@@ -238,11 +233,6 @@
// Add to beginning of gridData
window.gridData.unshift(newRow);
// Aggiorna la lista dei record visibili (navigazione parti + clone)
window.visibleIddatadbList = window.gridData
.map((r) => parseInt(r.iddatadb, 10))
.filter(Boolean);
// Re-render
const gr = window.gridRenderer;
if (gr) gr.renderVisibleRows();
File diff suppressed because it is too large Load Diff
@@ -39,7 +39,6 @@ try {
$button_bg_color = trim($_POST['button_bg_color'] ?? '#007bff');
$button_text_color = trim($_POST['button_text_color'] ?? '#ffffff');
$button_label = trim($_POST['button_label'] ?? 'Click Me');
$template_note = trim($_POST['template_note'] ?? '');
// Allowed source types
if (!in_array($source_type, ['XLS', 'API', 'JSON', 'PDF'], true)) {
@@ -123,7 +122,6 @@ try {
button_bg_color = ?,
button_text_color = ?,
button_label = ?,
template_note = ?,
updated_at = NOW()
WHERE id = ?
");
@@ -146,7 +144,6 @@ try {
$button_bg_color,
$button_text_color,
$button_label,
$template_note,
$id
]);
+23 -192
View File
@@ -53,143 +53,7 @@ function normalizeColumnIndex($value): int
return 1;
}
/**
* Trim a cell value treating invisible/Unicode spaces as empty.
* PHP's native trim() strips only ASCII whitespace (" \t\n\r\0\x0B"), so a cell
* that contains only a non-breaking space (U+00A0), zero-width space (U+200B),
* BOM (U+FEFF) or another Unicode space looks blank to a human but would still
* count as "filled" pulling a ghost column into the import or retaining a
* visually-empty row. Normalize those to a real space before trimming.
*/
function cleanCellText($value): string
{
$raw = (string)$value;
$cleaned = preg_replace(
'/[\x{00A0}\x{200B}\x{FEFF}\x{2000}-\x{200A}\x{202F}\x{205F}\x{3000}]+/u',
' ',
$raw
);
// preg_replace returns null on malformed UTF-8; fall back to the raw value.
return trim($cleaned ?? $raw);
}
/**
* Pre-clean an .xlsx by streaming out "ghost" cells: empty, self-closing
* <c .../> (and <c ...></c>) elements that carry only leftover styling.
*/
function slimXlsxGhostCells(string $path): ?string
{
if (!class_exists('ZipArchive')) {
return null;
}
$slim = $path . '.slim.xlsx';
try {
if (!copy($path, $slim)) {
return null;
}
// Phase 1: stream-strip each worksheet to a temp file (low memory).
$zip = new ZipArchive();
if ($zip->open($slim) !== true) {
@unlink($slim);
return null;
}
$temps = [];
for ($i = 0; $i < $zip->numFiles; $i++) {
$name = $zip->getNameIndex($i);
if (!preg_match('#^xl/worksheets/sheet\d+\.xml$#', $name)) {
continue;
}
$in = $zip->getStream($name);
if (!$in) {
continue;
}
$tmp = tempnam(sys_get_temp_dir(), 'slim');
$out = fopen($tmp, 'w');
$carry = '';
while (!feof($in)) {
$chunk = fread($in, 4194304);
if ($chunk === '' || $chunk === false) {
break;
}
// Only process up to the last complete '>' so a cell tag is
// never split across a chunk boundary; carry the remainder.
$buf = $carry . $chunk;
$lastGt = strrpos($buf, '>');
if ($lastGt === false) {
$carry = $buf;
continue;
}
$proc = substr($buf, 0, $lastGt + 1);
$carry = substr($buf, $lastGt + 1);
$proc = preg_replace(['#<c [^>]*/>#', '#<c [^>]*></c>#'], '', $proc);
fwrite($out, $proc);
}
if ($carry !== '') {
fwrite($out, $carry);
}
fclose($in);
fclose($out);
$temps[$name] = $tmp;
}
$zip->close();
if (!$temps) {
@unlink($slim);
return null;
}
// Phase 2: swap the stripped worksheets back into the archive.
$zip = new ZipArchive();
if ($zip->open($slim) !== true) {
foreach ($temps as $t) {
@unlink($t);
}
@unlink($slim);
return null;
}
foreach ($temps as $name => $tmp) {
$zip->deleteName($name);
$zip->addFile($tmp, $name);
}
$zip->close(); // addFile streams from disk here, so unlink only after.
foreach ($temps as $t) {
@unlink($t);
}
return $slim;
} catch (\Throwable $e) {
error_log('slimXlsxGhostCells failed: ' . $e->getMessage());
@unlink($slim);
return null;
}
}
try {
// Quando il body POST supera post_max_size, PHP scarta $_POST e $_FILES
// (warning "Content-Length exceeds the limit ... in Unknown on line 0") e lo
// script riceve una richiesta vuota. Lo intercettiamo per dare un messaggio
// chiaro invece di "Richiesta non valida".
if (
$_SERVER['REQUEST_METHOD'] === 'POST'
&& empty($_POST) && empty($_FILES)
&& (int)($_SERVER['CONTENT_LENGTH'] ?? 0) > 0
) {
$postMax = ini_get('post_max_size');
throw new Exception(
"Il file caricato supera il limite di upload del server (post_max_size = {$postMax}). " .
"Chiedi all'amministratore di aumentare post_max_size e upload_max_filesize."
);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['excel_file'])) {
$template_id = isset($_POST['template_id']) ? intval($_POST['template_id']) : 0;
@@ -206,7 +70,7 @@ try {
* Così non dipendiamo solo dal form e siamo sicuri di usare i dati salvati.
*/
$stmt = $pdo->prepare("
SELECT
SELECT
id,
header_row,
start_column,
@@ -278,14 +142,14 @@ try {
// Recupera il mapping da template_mapping
$stmt = $pdo->prepare("
SELECT
field_id AS excel_column,
field_id AS mysql_column,
data_type,
is_required,
default_value,
is_manual
FROM template_mapping
SELECT
field_id AS excel_column,
field_id AS mysql_column,
data_type,
is_required,
default_value,
is_manual
FROM template_mapping
WHERE template_id = ?
");
$stmt->execute([$template_id]);
@@ -297,28 +161,8 @@ try {
if (empty($mappings)) {
$response['error'] = "Nessun mapping trovato per il template con ID $template_id";
} else {
// Pre-clean ghost cells for .xlsx so a bloated worksheet (millions
// of empty styled cells) doesn't make the load time out. Falls back
// to the original file if slimming fails for any reason.
$loadPath = $destination;
$slimPath = null;
if (preg_match('/\.xlsx$/i', $destination)) {
$slimPath = slimXlsxGhostCells($destination);
if ($slimPath !== null) {
$loadPath = $slimPath;
error_log("Ghost-cell pre-clean applied, loading slimmed copy: $slimPath");
}
}
// Carica il file con PHPSpreadsheet.
$reader = IOFactory::createReaderForFile($loadPath);
$reader->setReadEmptyCells(false);
$spreadsheet = $reader->load($loadPath);
// The slimmed copy is only needed for parsing; drop it now.
if ($slimPath !== null) {
@unlink($slimPath);
}
// Carica il file rinominato con PHPSpreadsheet
$spreadsheet = IOFactory::load($destination);
$sheetCount = $spreadsheet->getSheetCount();
$sheetNames = $spreadsheet->getSheetNames();
@@ -345,8 +189,8 @@ try {
error_log("Selected XLS sheet - index: {$xlsSheetIndex}, name: {$selectedSheetName}");
$highestRow = $worksheet->getHighestDataRow();
$highestColumn = $worksheet->getHighestDataColumn();
$highestRow = $worksheet->getHighestRow();
$highestColumn = $worksheet->getHighestColumn();
$highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
$startRow = max(1, $header_row);
@@ -355,7 +199,7 @@ try {
// Advance startColumn to first non-empty cell in header row, matching JS behavior
for ($sc = $startColumn; $sc <= $highestColumnIndex; $sc++) {
$cl = Coordinate::stringFromColumnIndex($sc);
$cv = cleanCellText($worksheet->getCell($cl . $header_row)->getCalculatedValue() ?? '');
$cv = trim((string)($worksheet->getCell($cl . $header_row)->getCalculatedValue() ?? ''));
if ($cv !== '') {
$startColumn = $sc;
@@ -363,19 +207,6 @@ try {
}
}
$lastHeaderCol = $startColumn;
for ($hc = $startColumn; $hc <= $highestColumnIndex; $hc++) {
$hl = Coordinate::stringFromColumnIndex($hc);
$hv = cleanCellText($worksheet->getCell($hl . $header_row)->getCalculatedValue() ?? '');
if ($hv !== '') {
$lastHeaderCol = $hc;
}
}
$highestColumnIndex = $lastHeaderCol;
$highestColumn = Coordinate::stringFromColumnIndex($highestColumnIndex);
// Debug dei parametri
error_log(
"Processing - template_id: $template_id, " .
@@ -438,7 +269,7 @@ try {
$columnLetter = Coordinate::stringFromColumnIndex($physCol);
$cell = $worksheet->getCell($columnLetter . $header_row);
$cellValue = cleanCellText($cell ? $cell->getCalculatedValue() : '');
$cellValue = trim((string)($cell ? $cell->getCalculatedValue() : ''));
$cellValue = preg_replace('/[\r\n\t]+/', ' ', $cellValue);
// Empty headers get __empty_N__ to match mapping page
@@ -475,7 +306,7 @@ try {
$filledCount = 0;
foreach ($headerFilledIndices as $idx) {
if (isset($rowData[$idx]) && cleanCellText($rowData[$idx]) !== '') {
if (isset($rowData[$idx]) && trim((string)$rowData[$idx]) !== '') {
$filledCount++;
}
}
@@ -491,13 +322,13 @@ try {
// Recupera routine dal template
if ($template && !empty($template['idroutine'])) {
$stmtRoutine = $pdo->prepare("
SELECT
idroutine,
name,
filename,
headerrow,
instruction
FROM routine
SELECT
idroutine,
name,
filename,
headerrow,
instruction
FROM routine
WHERE idroutine = ?
");
$stmtRoutine->execute([$template['idroutine']]);
+700
View File
@@ -0,0 +1,700 @@
<?php include('include/headscript.php'); ?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<link href="assets/plugins/datatable/css/dataTables.bootstrap5.min.css" rel="stylesheet" />
<title>TRF-Project - Customer Reports</title>
<style>
.lookup-wrapper {
width: 100%;
max-width: none;
margin: 0;
}
.lookup-title {
font-size: 18px;
font-weight: 700;
}
.lookup-subtitle {
font-size: 13px;
color: #6c757d;
}
.compact-card .card-body {
padding: 1rem;
}
.table-report th {
font-size: 12px;
text-transform: uppercase;
color: #6c757d;
white-space: nowrap;
}
.table-report td {
font-size: 13px;
vertical-align: middle;
}
.status-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.status-pill-success {
background: #e8fff1;
color: #198754;
}
.status-pill-warning {
background: #fff3cd;
color: #b58100;
}
.pdf-icon-link {
width: 36px;
height: 36px;
border-radius: 10px;
background: #dc3545;
color: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 20px;
text-decoration: none;
transition: all 0.15s ease-in-out;
}
.pdf-icon-link:hover {
color: #fff;
opacity: 0.85;
transform: translateY(-1px);
}
.no-pdf {
font-size: 12px;
color: #adb5bd;
font-weight: 600;
}
.empty-state {
border: 1px dashed #ced4da;
border-radius: 10px;
padding: 24px;
text-align: center;
color: #6c757d;
background: #fafafa;
}
.spinner-inline {
display: none;
margin-left: 8px;
}
.customer-box {
display: none;
border: 1px solid #e9ecef;
background: #fff;
border-radius: 10px;
padding: 12px 14px;
margin-bottom: 15px;
}
.customer-label {
font-size: 11px;
text-transform: uppercase;
color: #6c757d;
font-weight: 700;
}
.customer-value {
font-size: 14px;
color: #212529;
font-weight: 700;
}
.json-preview {
display: none;
background: #111827;
color: #e5e7eb;
border-radius: 10px;
padding: 14px;
font-size: 12px;
max-height: 420px;
overflow: auto;
white-space: pre-wrap;
}
@media (max-width: 768px) {
.table-responsive {
border-radius: 10px;
}
}
.select2-container {
width: 100% !important;
}
.select2-container--default .select2-selection--single {
height: 38px;
border: 1px solid #ced4da;
border-radius: 6px;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: 36px;
font-size: 14px;
color: #212529;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 36px;
}
.select2-dropdown {
z-index: 9999;
}
</style>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<?php include('top_stat_widget.php'); ?>
<div class="lookup-wrapper">
<div class="card radius-10 compact-card">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<div>
<div class="lookup-title">Customer Test Reports</div>
<div class="lookup-subtitle">
Select a VisualLims customer and retrieve the latest reports with PDF links.
</div>
</div>
</div>
</div>
<div class="card-body">
<form id="customerReportsForm" class="row g-3 align-items-end">
<div class="col-md-5">
<label for="idCliente" class="form-label fw-semibold">Customer</label>
<select id="idCliente" name="idCliente" class="form-select">
<option value="">Loading customers...</option>
</select>
</div>
<div class="col-md-2">
<label for="limitReports" class="form-label fw-semibold">Limit</label>
<select id="limitReports" name="limitReports" class="form-select">
<option value="1">Last 1</option>
<option value="3" selected>Last 3</option>
<option value="5">Last 5</option>
<option value="10">Last 10</option>
</select>
</div>
<div class="col-md-2">
<label for="signedStatus" class="form-label fw-semibold">Status</label>
<select id="signedStatus" name="signedStatus" class="form-select">
<option value="all" selected>All</option>
<option value="signed">Signed</option>
<option value="not_signed">Not signed</option>
</select>
</div>
<div class="col-md-3">
<button type="submit" id="btnSearchReports" class="btn btn-primary w-100">
<i class="bx bx-search"></i> Search Reports
<span class="spinner-border spinner-border-sm spinner-inline" id="searchSpinner" role="status" aria-hidden="true"></span>
</button>
</div>
</form>
</div>
</div>
<div id="customerBox" class="customer-box">
<div class="row g-3">
<div class="col-md-4">
<div class="customer-label">Customer Code</div>
<div class="customer-value" id="selectedCustomerCode">-</div>
</div>
<div class="col-md-8">
<div class="customer-label">Customer Name</div>
<div class="customer-value" id="selectedCustomerName">-</div>
</div>
</div>
</div>
<div id="resultContainer" style="display:none;">
<div class="card radius-10 compact-card">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<h6 class="mb-0">
Reports
<span class="badge bg-light text-dark ms-1" id="reportCountBadge">0</span>
</h6>
<button type="button" id="toggleJsonBtn" class="btn btn-sm btn-outline-secondary">
<i class="bx bx-code-alt"></i> Show JSON
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover table-report align-middle mb-0" id="reportsTable">
<thead>
<tr>
<th>Report Number</th>
<th>Report ID</th>
<th>Report Date</th>
<th>Print Date</th>
<th>Version</th>
<th>Status</th>
<th class="text-center">PDF</th>
</tr>
</thead>
<tbody id="reportsTableBody"></tbody>
</table>
</div>
<pre id="jsonPreview" class="json-preview mt-3"></pre>
</div>
</div>
</div>
<div id="emptyState" class="empty-state">
<i class="bx bx-file-find" style="font-size:34px;"></i>
<div class="mt-2 fw-semibold">No reports loaded</div>
<div class="small">Select a customer, choose the limit and click Search Reports.</div>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top">
<i class='bx bxs-up-arrow-alt'></i>
</a>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script src="assets/plugins/datatable/js/jquery.dataTables.min.js"></script>
<script src="assets/plugins/datatable/js/dataTables.bootstrap5.min.js"></script>
<script>
$(document).ready(function() {
let lastJsonResponse = null;
let loadedCustomers = [];
let reportsDataTable = null;
function escapeHtml(value) {
if (value === null || value === undefined) {
return '';
}
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function formatDate(value) {
if (!value) {
return '-';
}
const date = new Date(value);
if (isNaN(date.getTime())) {
return value;
}
return date.toLocaleString('it-IT', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
function setLoading(isLoading) {
$('#btnSearchReports').prop('disabled', isLoading);
$('#searchSpinner').toggle(isLoading);
}
function resetResults() {
lastJsonResponse = null;
if (reportsDataTable !== null) {
reportsDataTable.destroy();
reportsDataTable = null;
}
$('#resultContainer').hide();
$('#emptyState').show();
$('#reportsTableBody').html('');
$('#reportCountBadge').text('0');
$('#jsonPreview').hide().text('');
$('#toggleJsonBtn').html('<i class="bx bx-code-alt"></i> Show JSON');
}
function loadCustomers() {
$.ajax({
url: 'get_clienti.php',
method: 'GET',
dataType: 'json',
success: function(response) {
const select = $('#idCliente');
select.empty();
const customers = response.value || response || [];
loadedCustomers = Array.isArray(customers) ? customers : [];
select.append('<option value="">Select customer...</option>');
loadedCustomers.forEach(function(customer) {
const idCliente = customer.IdCliente || '';
const codiceCliente = customer.CodiceCliente || '';
const nominativo = customer.Nominativo || '';
const label = codiceCliente ?
codiceCliente + ' - ' + nominativo :
nominativo;
select.append(
'<option value="' + escapeHtml(idCliente) + '" ' +
'data-code="' + escapeHtml(codiceCliente) + '" ' +
'data-name="' + escapeHtml(nominativo) + '">' +
escapeHtml(label) +
'</option>'
);
});
if ($.fn.select2) {
select.select2({
width: '100%',
placeholder: 'Search customer...',
allowClear: true,
minimumInputLength: 0,
matcher: function(params, data) {
if ($.trim(params.term) === '') {
return data;
}
if (typeof data.text === 'undefined') {
return null;
}
const term = params.term.toLowerCase();
const text = data.text.toLowerCase();
if (text.indexOf(term) > -1) {
return data;
}
return null;
},
sorter: function(data) {
const term = $('.select2-search__field').val();
if (!term) {
return data;
}
const search = term.toLowerCase();
return data.sort(function(a, b) {
const aText = (a.text || '').toLowerCase();
const bText = (b.text || '').toLowerCase();
const aStarts = aText.startsWith(search);
const bStarts = bText.startsWith(search);
if (aStarts && !bStarts) return -1;
if (!aStarts && bStarts) return 1;
return aText.localeCompare(bText);
});
}
});
}
},
error: function(xhr) {
$('#idCliente').html('<option value="">Error loading customers</option>');
let message = 'Unable to load customers.';
if (xhr.responseJSON && xhr.responseJSON.error) {
message = xhr.responseJSON.error;
}
Swal.fire({
title: 'Customer loading error',
text: message,
icon: 'error',
confirmButtonText: 'OK'
});
}
});
}
function updateSelectedCustomerBox() {
const selectedOption = $('#idCliente option:selected');
const customerCode = selectedOption.data('code') || '-';
const customerName = selectedOption.data('name') || '-';
if (!$('#idCliente').val()) {
$('#customerBox').hide();
$('#selectedCustomerCode').text('-');
$('#selectedCustomerName').text('-');
return;
}
$('#selectedCustomerCode').text(customerCode);
$('#selectedCustomerName').text(customerName);
$('#customerBox').show();
}
function renderStatus(isSigned) {
if (isSigned === true) {
return '<span class="status-pill status-pill-success"><i class="bx bx-check-circle"></i> Signed</span>';
}
if (isSigned === false) {
return '<span class="status-pill status-pill-warning"><i class="bx bx-time"></i> Not signed</span>';
}
return '-';
}
function renderPdfCell(pdfFiles) {
if (!Array.isArray(pdfFiles) || pdfFiles.length === 0) {
return '<span class="no-pdf">No PDF</span>';
}
const firstPdf = pdfFiles[0];
if (!firstPdf.download_url) {
return '<span class="no-pdf">No PDF</span>';
}
return `
<a href="${escapeHtml(firstPdf.download_url)}"
target="_blank"
class="pdf-icon-link"
title="${escapeHtml(firstPdf.file_name || 'Download PDF')}">
<i class="bx bxs-file-pdf"></i>
</a>
`;
}
function initReportsDataTable() {
if (reportsDataTable !== null) {
reportsDataTable.destroy();
reportsDataTable = null;
}
reportsDataTable = $('#reportsTable').DataTable({
paging: false,
searching: false,
info: false,
ordering: true,
order: [
[2, 'desc']
],
autoWidth: false,
responsive: true,
columnDefs: [{
targets: 6,
orderable: false
}],
language: {
emptyTable: 'No reports found',
zeroRecords: 'No matching reports found'
}
});
}
function renderReports(response) {
lastJsonResponse = response;
const reports = response.reports || [];
const tbody = $('#reportsTableBody');
tbody.empty();
$('#reportCountBadge').text(reports.length);
if (reports.length > 0) {
reports.forEach(function(report) {
const reportDateOrder = report.data || '';
const printDateOrder = report.data_stampa || '';
const statusOrder = report.firmato === true ? 1 : 0;
const row = `
<tr>
<td>
<strong>${escapeHtml(report.codice_rapporto || '-')}</strong>
</td>
<td>${escapeHtml(report.id_rapporto || '-')}</td>
<td data-order="${escapeHtml(reportDateOrder)}">${escapeHtml(formatDate(report.data))}</td>
<td data-order="${escapeHtml(printDateOrder)}">${escapeHtml(formatDate(report.data_stampa))}</td>
<td>${escapeHtml(report.versione !== null && report.versione !== undefined ? report.versione : '-')}</td>
<td data-order="${statusOrder}">${renderStatus(report.firmato)}</td>
<td class="text-center">${renderPdfCell(report.pdf_files)}</td>
</tr>
`;
tbody.append(row);
});
}
$('#jsonPreview').text(JSON.stringify(response, null, 4));
$('#emptyState').hide();
$('#resultContainer').show();
setTimeout(function() {
initReportsDataTable();
}, 50);
}
$('#idCliente').on('change', function() {
updateSelectedCustomerBox();
resetResults();
});
$('#customerReportsForm').on('submit', function(event) {
event.preventDefault();
const idCliente = $('#idCliente').val();
const limit = $('#limitReports').val();
const signedStatus = $('#signedStatus').val();
if (!idCliente) {
Swal.fire({
title: 'Missing customer',
text: 'Please select a customer.',
icon: 'warning',
confirmButtonText: 'OK'
});
return;
}
resetResults();
updateSelectedCustomerBox();
setLoading(true);
$.ajax({
url: 'get_rapporti_cliente.php',
method: 'GET',
dataType: 'json',
data: {
id_cliente: idCliente,
limit: limit,
signed_status: signedStatus
},
success: function(response) {
if (!response || response.success !== true) {
Swal.fire({
title: 'No data',
text: response && response.error ? response.error : 'No reports were returned.',
icon: 'warning',
confirmButtonText: 'OK'
});
return;
}
renderReports(response);
},
error: function(xhr) {
let message = 'Unexpected error while loading reports.';
if (xhr.responseJSON && xhr.responseJSON.error) {
message = xhr.responseJSON.error;
} else if (xhr.responseText) {
message = xhr.responseText.substring(0, 500);
}
Swal.fire({
title: 'Error',
text: message,
icon: 'error',
confirmButtonText: 'OK'
});
},
complete: function() {
setLoading(false);
}
});
});
$('#toggleJsonBtn').on('click', function() {
const jsonPreview = $('#jsonPreview');
const isVisible = jsonPreview.is(':visible');
jsonPreview.toggle(!isVisible);
if (isVisible) {
$(this).html('<i class="bx bx-code-alt"></i> Show JSON');
} else {
$(this).html('<i class="bx bx-hide"></i> Hide JSON');
}
});
loadCustomers();
});
</script>
</body>
</html>
+580
View File
@@ -0,0 +1,580 @@
<?php include('include/headscript.php'); ?>
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--favicon-->
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>TRF-Project - Test Report Lookup</title>
<style>
.compact-card .card-body {
padding: 1rem;
}
.lookup-wrapper {
max-width: 1100px;
margin: 0 auto;
}
.lookup-title {
font-size: 18px;
font-weight: 700;
}
.lookup-subtitle {
font-size: 13px;
color: #6c757d;
}
.result-section-title {
font-size: 14px;
font-weight: 700;
margin-bottom: 10px;
color: #344767;
}
.info-box {
border: 1px solid #e9ecef;
border-radius: 10px;
padding: 14px;
background: #fff;
height: 100%;
}
.info-label {
font-size: 11px;
text-transform: uppercase;
color: #6c757d;
font-weight: 700;
margin-bottom: 3px;
}
.info-value {
font-size: 14px;
color: #212529;
font-weight: 600;
word-break: break-word;
}
.status-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
}
.status-pill-success {
background: #e8fff1;
color: #198754;
}
.status-pill-warning {
background: #fff3cd;
color: #b58100;
}
.pdf-card {
border: 1px solid #f1d1d1;
background: #fffafa;
border-radius: 10px;
padding: 14px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.pdf-icon {
width: 46px;
height: 46px;
border-radius: 12px;
background: #dc3545;
color: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 24px;
flex-shrink: 0;
}
.pdf-file-name {
font-weight: 700;
font-size: 14px;
margin-bottom: 2px;
}
.pdf-meta {
font-size: 12px;
color: #6c757d;
}
.json-preview {
display: none;
background: #111827;
color: #e5e7eb;
border-radius: 10px;
padding: 14px;
font-size: 12px;
max-height: 420px;
overflow: auto;
white-space: pre-wrap;
}
.empty-state {
border: 1px dashed #ced4da;
border-radius: 10px;
padding: 24px;
text-align: center;
color: #6c757d;
background: #fafafa;
}
.spinner-inline {
display: none;
margin-left: 8px;
}
.btn-download-pdf {
white-space: nowrap;
}
@media (max-width: 768px) {
.pdf-card {
flex-direction: column;
align-items: flex-start;
}
.btn-download-pdf {
width: 100%;
}
}
</style>
</head>
<body>
<!--wrapper-->
<div class="wrapper">
<!--sidebar wrapper -->
<?php include('include/navbar.php'); ?>
<!--end sidebar wrapper -->
<!--start header -->
<?php include('include/topbar.php'); ?>
<!--end header -->
<!--start page wrapper -->
<div class="page-wrapper">
<div class="page-content">
<?php include('top_stat_widget.php'); ?>
<div class="lookup-wrapper">
<div class="card radius-10 compact-card">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<div>
<div class="lookup-title">Test Report Lookup</div>
<div class="lookup-subtitle">
Search a test report from VisualLims by report number and download the PDF if available.
</div>
</div>
</div>
</div>
<div class="card-body">
<form id="reportSearchForm" class="row g-3 align-items-end">
<div class="col-md-8">
<label for="codiceRapporto" class="form-label fw-semibold">
Report Number
</label>
<input type="text"
class="form-control"
id="codiceRapporto"
name="codiceRapporto"
placeholder="Example: 2621521"
autocomplete="off">
</div>
<div class="col-md-4">
<button type="submit" id="btnSearchReport" class="btn btn-primary w-100">
<i class="bx bx-search"></i> Proceed
<span class="spinner-border spinner-border-sm spinner-inline" id="searchSpinner" role="status" aria-hidden="true"></span>
</button>
</div>
</form>
</div>
</div>
<div id="resultContainer" style="display:none;">
<div class="card radius-10 compact-card">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<h6 class="mb-0">Report Data</h6>
<button type="button" id="toggleJsonBtn" class="btn btn-sm btn-outline-secondary">
<i class="bx bx-code-alt"></i> Show JSON
</button>
</div>
</div>
<div class="card-body">
<div class="result-section-title">General Information</div>
<div class="row g-3 mb-4">
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Report Number</div>
<div class="info-value" id="resCodiceRapporto">-</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Report ID</div>
<div class="info-value" id="resIdRapporto">-</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Customer Code</div>
<div class="info-value" id="resCodiceCliente">-</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Customer Name</div>
<div class="info-value" id="resNominativoCliente">-</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Report Date</div>
<div class="info-value" id="resDataRapporto">-</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Print Date</div>
<div class="info-value" id="resDataStampa">-</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Version</div>
<div class="info-value" id="resVersione">-</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Signed</div>
<div class="info-value" id="resFirmato">-</div>
</div>
</div>
<div class="col-md-6">
<div class="info-box">
<div class="info-label">Detail Endpoint</div>
<div class="info-value small" id="resDetailEndpoint">-</div>
</div>
</div>
</div>
<div class="result-section-title">Available PDF Files</div>
<div id="pdfFilesContainer" class="mb-4"></div>
<pre id="jsonPreview" class="json-preview"></pre>
</div>
</div>
</div>
<div id="emptyState" class="empty-state">
<i class="bx bx-file-find" style="font-size:34px;"></i>
<div class="mt-2 fw-semibold">No report loaded</div>
<div class="small">Enter a report number and click Proceed.</div>
</div>
</div>
</div>
</div>
<!--end page wrapper -->
<!--start overlay-->
<div class="overlay toggle-icon"></div>
<!--end overlay-->
<!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top">
<i class='bx bxs-up-arrow-alt'></i>
</a>
<!--End Back To Top Button-->
<?php include('include/footer.php'); ?>
</div>
<!--end wrapper-->
<?php include('jsinclude.php'); ?>
<script>
$(document).ready(function() {
let lastJsonResponse = null;
function escapeHtml(value) {
if (value === null || value === undefined) {
return '';
}
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function formatDate(value) {
if (!value) {
return '-';
}
const date = new Date(value);
if (isNaN(date.getTime())) {
return value;
}
return date.toLocaleString('it-IT', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
function setLoading(isLoading) {
$('#btnSearchReport').prop('disabled', isLoading);
$('#searchSpinner').toggle(isLoading);
}
function resetResult() {
lastJsonResponse = null;
$('#resultContainer').hide();
$('#emptyState').show();
$('#jsonPreview').hide().text('');
$('#toggleJsonBtn').html('<i class="bx bx-code-alt"></i> Show JSON');
$('#resCodiceRapporto').text('-');
$('#resIdRapporto').text('-');
$('#resCodiceCliente').text('-');
$('#resNominativoCliente').text('-');
$('#resDataRapporto').text('-');
$('#resDataStampa').text('-');
$('#resVersione').text('-');
$('#resFirmato').text('-');
$('#resDetailEndpoint').text('-');
$('#pdfFilesContainer').html('');
}
function renderPdfFiles(pdfFiles) {
const container = $('#pdfFilesContainer');
container.empty();
if (!Array.isArray(pdfFiles) || pdfFiles.length === 0) {
container.html(`
<div class="empty-state">
<i class="bx bx-file-blank" style="font-size:28px;"></i>
<div class="mt-2 fw-semibold">No PDF available</div>
<div class="small">No report PDF file was returned by VisualLims.</div>
</div>
`);
return;
}
pdfFiles.forEach(function(file) {
const idRapportoFile = file.id_rapporto_file || '';
const fileName = file.file_name || 'report.pdf';
const categoria = file.categoria || '-';
const tipoRapporto = file.tipo_rapporto || '-';
const downloadUrl = 'download_rapporto_pdf.php?id_rapporto_file=' + encodeURIComponent(idRapportoFile);
const html = `
<div class="pdf-card mb-2">
<div class="d-flex align-items-center gap-3">
<div class="pdf-icon">
<i class="bx bxs-file-pdf"></i>
</div>
<div>
<div class="pdf-file-name">${escapeHtml(fileName)}</div>
<div class="pdf-meta">
ID File: ${escapeHtml(idRapportoFile)}
&nbsp;|&nbsp;
Category: ${escapeHtml(categoria)}
&nbsp;|&nbsp;
Type: ${escapeHtml(tipoRapporto)}
</div>
</div>
</div>
<a href="${downloadUrl}" target="_blank" class="btn btn-danger btn-sm btn-download-pdf">
<i class="bx bx-download"></i> Download PDF
</a>
</div>
`;
container.append(html);
});
}
function renderResult(response) {
lastJsonResponse = response;
const base = response.rapporto_base || {};
const data = response.data || {};
const cliente = response.cliente || data.Cliente || {};
$('#resCodiceRapporto').text(response.codice_rapporto || base.CodiceRapporto || '-');
$('#resIdRapporto').text(response.id_rapporto || base.IdRapporto || '-');
$('#resCodiceCliente').text(cliente.CodiceCliente || '-');
$('#resNominativoCliente').text(cliente.Nominativo || '-');
$('#resDataRapporto').text(formatDate(base.Data || data.Data));
$('#resDataStampa').text(formatDate(base.DataStampa || data.DataStampa));
$('#resVersione').text(base.Versione !== undefined ? base.Versione : (data.Versione !== undefined ? data.Versione : '-'));
$('#resDetailEndpoint').text(response.detail_endpoint || '-');
const isSigned = base.Firmato === true || data.Firmato === true;
if (isSigned) {
$('#resFirmato').html('<span class="status-pill status-pill-success"><i class="bx bx-check-circle"></i> Signed</span>');
} else {
$('#resFirmato').html('<span class="status-pill status-pill-warning"><i class="bx bx-time"></i> Not signed</span>');
}
renderPdfFiles(response.pdf_files || []);
$('#jsonPreview').text(JSON.stringify(response, null, 4));
$('#emptyState').hide();
$('#resultContainer').show();
}
$('#reportSearchForm').on('submit', function(event) {
event.preventDefault();
const codiceRapporto = $('#codiceRapporto').val().trim();
if (!codiceRapporto) {
Swal.fire({
title: 'Missing report number',
text: 'Please enter a report number.',
icon: 'warning',
confirmButtonText: 'OK'
});
return;
}
resetResult();
setLoading(true);
$.ajax({
url: 'get_rapporto_prova.php',
method: 'GET',
dataType: 'json',
data: {
codice: codiceRapporto,
step: 'files'
},
success: function(response) {
if (!response || response.success !== true) {
Swal.fire({
title: 'Report not found',
text: response && response.message ? response.message : 'No report was found for this number.',
icon: 'warning',
confirmButtonText: 'OK'
});
return;
}
renderResult(response);
},
error: function(xhr) {
let message = 'Unexpected error while loading the report.';
if (xhr.responseJSON && xhr.responseJSON.error) {
message = xhr.responseJSON.error;
} else if (xhr.responseJSON && xhr.responseJSON.message) {
message = xhr.responseJSON.message;
} else if (xhr.responseText) {
message = xhr.responseText.substring(0, 500);
}
Swal.fire({
title: 'Error',
text: message,
icon: 'error',
confirmButtonText: 'OK'
});
},
complete: function() {
setLoading(false);
}
});
});
$('#toggleJsonBtn').on('click', function() {
const jsonPreview = $('#jsonPreview');
const isVisible = jsonPreview.is(':visible');
jsonPreview.toggle(!isVisible);
if (isVisible) {
$(this).html('<i class="bx bx-code-alt"></i> Show JSON');
} else {
$(this).html('<i class="bx bx-hide"></i> Hide JSON');
}
});
const urlParams = new URLSearchParams(window.location.search);
const prefillCodice = urlParams.get('codice');
if (prefillCodice) {
$('#codiceRapporto').val(prefillCodice);
$('#reportSearchForm').trigger('submit');
}
});
</script>
</body>
</html>
+385
View File
@@ -0,0 +1,385 @@
<?php
include('include/headscript.php');
// Binding preparati da import_insert.php (da risolvere + collegati in automatico).
$pending = $_SESSION['pending_bindings'] ?? null;
if (empty($pending) || (empty($pending['items']) && empty($pending['auto']) && empty($pending['saved']))) {
// Niente da mostrare: vado alla griglia.
$tid = $pending['template_id'] ?? ($_SESSION['template_id'] ?? null);
$ref = $pending['importref'] ?? '';
unset($_SESSION['pending_bindings']);
if ($tid) {
header("Location: imported.php?id=" . urlencode($tid) . "&importref=" . urlencode($ref));
} else {
header("Location: xlstemplates_grid.php");
}
exit;
}
$templateId = (int) $pending['template_id'];
$importRef = (string) $pending['importref'];
$items = $pending['items'] ?? [];
$autoItems = $pending['auto'] ?? [];
$savedItems = $pending['saved'] ?? [];
// Righe gia' risolte (modificabili): auto-collegate + binding gia' salvati.
$resolvedItems = [];
foreach ($autoItems as $a) {
$a['badge'] = 'auto';
$a['badge_class'] = 'bg-success';
$resolvedItems[] = $a;
}
foreach ($savedItems as $s) {
$s['badge'] = 'salvato';
$s['badge_class'] = 'bg-secondary';
$resolvedItems[] = $s;
}
// Raggruppa le righe per campo (custom: mapping_id, fixed: fixed_field_key),
// mantenendo l'ordine di prima apparizione. Ogni gruppo elenca prima i pending.
$groups = [];
$groupOrder = [];
$pushToGroup = function (array $row, string $type, $idx = null) use (&$groups, &$groupOrder) {
$kind = $row['kind'] ?? 'custom';
$gkey = $kind === 'fixed'
? 'fx:' . ($row['fixed_field_key'] ?? '')
: 'cf:' . (int) ($row['mapping_id'] ?? 0);
if (!isset($groups[$gkey])) {
$groups[$gkey] = [
'label' => $row['field_label'] ?? $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');
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("SELECT name FROM excel_templates WHERE id = ?");
$stmt->execute([$templateId]);
$templateName = $stmt->fetchColumn() ?: ('Template ' . $templateId);
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
<style>
.json-value {
font-family: Consolas, Monaco, monospace;
font-weight: 600;
}
.binding-status {
font-size: 12px;
}
td .select2-container {
min-width: 240px;
}
/* Allinea l'altezza di select2 a form-select/btn di Bootstrap */
.select2-container--default .select2-selection--single {
height: 38px;
border-color: #ced4da;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: 36px;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 36px;
}
</style>
<title>Binding JSON &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 JSON &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">
<?php if (!empty($items)): ?>
<div class="alert alert-info">
Alcuni valori importati dal JSON non hanno ancora una corrispondenza con i valori del LIMS.
Seleziona il valore LIMS corretto per ciascuno e conferma. I binding verranno salvati e
riutilizzati nelle importazioni successive.
</div>
<?php endif; ?>
<?php if (!empty($autoItems)): ?>
<div class="alert alert-success">
<?= count($autoItems) ?> valore/i collegato/i automaticamente (corrispondenza esatta con il LIMS).
</div>
<?php endif; ?>
<div id="bindingError" class="alert alert-danger" style="display:none;"></div>
<form id="bindingForm">
<div class="table-responsive">
<table class="table table-bordered align-middle">
<thead>
<tr>
<th style="width:32px;"></th>
<th>Valore JSON</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="4">
<strong><?= htmlspecialchars($g['label']) ?></strong>
<?php if ($g['kind'] === 'fixed'): ?><span class="badge bg-light text-dark border">fixed</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']; $kind = $item['kind'] ?? 'custom'; ?>
<tr class="binding-row"
data-index="<?= $idx ?>"
data-kind="<?= $kind ?>"
<?php if ($kind === 'fixed'): ?>
data-fixed-key="<?= htmlspecialchars($item['fixed_field_key'], ENT_QUOTES) ?>"
<?php else: ?>
data-mapping-id="<?= (int) ($item['mapping_id'] ?? 0) ?>"
data-field-id="<?= (int) ($item['field_id'] ?? 0) ?>"
<?php endif; ?>
data-json-value="<?= htmlspecialchars($item['json_value'], ENT_QUOTES) ?>"
data-datadb-ids="<?= htmlspecialchars(json_encode($item['datadb_ids']), ENT_QUOTES) ?>">
<td class="text-muted ps-4">&rsaquo;</td>
<td class="json-value"><?= htmlspecialchars($item['json_value']) ?></td>
<td>
<select class="form-select binding-select">
<option value="">Seleziona valore LIMS...</option>
</select>
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-secondary skip-binding-btn">Nessuna corrispondenza</button>
<div class="binding-status text-muted mt-1">In attesa</div>
</td>
</tr>
<?php endforeach; ?>
<?php foreach ($g['resolved'] as $r): $res = $r['data']; $kind = $res['kind'] ?? 'custom'; ?>
<tr class="binding-row"
data-kind="<?= $kind ?>"
<?php if ($kind === 'fixed'): ?>
data-fixed-key="<?= htmlspecialchars($res['fixed_field_key'], ENT_QUOTES) ?>"
<?php else: ?>
data-mapping-id="<?= (int) ($res['mapping_id'] ?? 0) ?>"
data-field-id="<?= (int) ($res['field_id'] ?? 0) ?>"
<?php endif; ?>
data-json-value="<?= htmlspecialchars($res['json_value'], ENT_QUOTES) ?>"
data-datadb-ids="<?= htmlspecialchars(json_encode($res['datadb_ids']), ENT_QUOTES) ?>">
<td class="text-muted ps-4">&rsaquo;</td>
<td class="json-value"><?= htmlspecialchars($res['json_value']) ?></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="imported.php?id=<?= $templateId ?>&importref=<?= urlencode($importRef) ?>"
class="btn btn-outline-secondary">Salta per ora</a>
<button type="submit" class="btn btn-primary" id="confirmBindingsBtn" disabled>
Conferma e prosegui
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(function() {
const TEMPLATE_ID = <?= $templateId ?>;
const IMPORT_REF = <?= json_encode($importRef) ?>;
const CONTINUE_URL = 'imported.php?id=' + TEMPLATE_ID + '&importref=' + encodeURIComponent(IMPORT_REF);
const $form = $('#bindingForm');
const $btn = $('#confirmBindingsBtn');
const $error = $('#bindingError');
// Dropdown valori LIMS per riga: sorgente custom vs fixed.
$('.binding-select').each(function() {
const $row = $(this).closest('.binding-row');
const kind = $row.data('kind');
const isFixed = 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
});
});
// Una riga e' pronta se ha un valore scelto oppure e' marcata "nessuna corrispondenza".
function refreshButton() {
const allReady = $('.binding-row').toArray().every(row => {
const $row = $(row);
return $row.hasClass('is-skipped') || $row.find('.binding-select').val();
});
$btn.prop('disabled', !allReady);
}
$('.binding-select').on('change', refreshButton);
refreshButton();
// "Nessuna corrispondenza": azzera il valore importato, nessun binding salvato.
$('.skip-binding-btn').on('click', function() {
const $row = $(this).closest('.binding-row');
const $select = $row.find('.binding-select');
const $status = $row.find('.binding-status');
const skipped = $row.toggleClass('is-skipped').hasClass('is-skipped');
$(this).toggleClass('btn-outline-secondary', !skipped).toggleClass('btn-secondary', skipped);
$select.val(null).trigger('change').prop('disabled', skipped);
$status.text(skipped ? 'Nessuna corrispondenza' : 'In attesa');
refreshButton();
});
$form.on('submit', function(e) {
e.preventDefault();
$error.hide();
$btn.prop('disabled', true).text('Salvataggio...');
const tasks = $('.binding-row').toArray().map(row => {
const $row = $(row);
const $status = $row.find('.binding-status');
const kind = $row.data('kind');
const isFixed = kind === 'fixed';
const datadbIds = JSON.stringify($row.data('datadb-ids') || []);
const jsonValue = String($row.data('json-value'));
const targetFields = isFixed
? { kind: 'fixed', fixed_field_key: $row.data('fixed-key') }
: { kind: 'custom', mapping_id: $row.data('mapping-id'), field_id: $row.data('field-id') };
// Riga senza corrispondenza: azzera il valore, niente binding.
if ($row.hasClass('is-skipped')) {
return $.post('skip_binding.php', {
...targetFields,
template_id: TEMPLATE_ID,
json_value: jsonValue,
datadb_ids: datadbIds
}).then(resp => {
if (resp && resp.success) {
$status.text('Azzerato').removeClass('text-muted').addClass('text-success');
return true;
}
$status.text('Errore').addClass('text-danger');
throw new Error((resp && resp.error) || 'Errore azzeramento valore');
});
}
const $select = $row.find('.binding-select');
const selectedData = $select.select2('data')[0] || {};
return $.post('save_binding.php', {
...targetFields,
template_id: TEMPLATE_ID,
json_value: jsonValue,
lims_value_id: $select.val(),
lims_value: selectedData.text || '',
datadb_ids: datadbIds
}).then(resp => {
if (resp && resp.success) {
$status.text('Salvato').removeClass('text-muted').addClass('text-success');
return true;
}
$status.text('Errore').addClass('text-danger');
throw new Error((resp && resp.error) || 'Errore salvataggio binding');
});
});
Promise.all(tasks)
.then(() => {
window.location.href = CONTINUE_URL;
})
.catch(err => {
$error.text(err.message || 'Errore durante il salvataggio dei binding.').show();
$btn.prop('disabled', false).text('Conferma e prosegui');
});
});
});
</script>
</body>
</html>
-90
View File
@@ -1,90 +0,0 @@
<?php
ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/routine_debug.log');
function applyRoutine(&$excelData, $routineData)
{
try {
// Initial log
error_log("Inizio esecuzione routine Le Silla: " . date('Y-m-d H:i:s'));
error_log("Dati routine: " . print_r($routineData, true));
error_log("Dati excel_data: " . print_r($excelData, true));
// Check if excelData is empty
if (empty($excelData)) {
throw new Exception("excelData è vuoto o non valido.");
}
// Extract routine settings with default values
$articolo_header = trim($routineData['articolo'] ?? 'ARTICOLO');
$descrizione_header = trim($routineData['descrizione'] ?? 'DESCRIZIONE');
$headers = $routineData['xls_headers'] ?? [];
if (empty($headers)) {
throw new Exception("Nessun header trovato per la routine Le Silla.");
}
// If headers arrive as JSON string, decode them
if (!is_array($headers) && is_string($headers)) {
$decoded_headers = json_decode($headers, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded_headers)) {
$headers = $decoded_headers;
}
}
if (!is_array($headers) || empty($headers)) {
throw new Exception("Gli header della routine non sono validi.");
}
error_log("Header ricevuti: " . print_r($headers, true));
// Normalize headers
$normalized_headers = array_map(function ($header) {
return trim((string)$header);
}, $headers);
error_log("Header normalizzati: " . print_r($normalized_headers, true));
error_log("Header values - ARTICOLO: '$articolo_header', DESCRIZIONE: '$descrizione_header'");
// Find column indexes
$articolo_index = array_search($articolo_header, $normalized_headers, true);
$descrizione_index = array_search($descrizione_header, $normalized_headers, true);
if ($articolo_index === false) {
throw new Exception("Colonna ARTICOLO non trovata.");
}
if ($descrizione_index === false) {
throw new Exception("Colonna DESCRIZIONE non trovata.");
}
error_log("Indici colonne - ARTICOLO: $articolo_index, DESCRIZIONE: $descrizione_index");
/**
* Merge ARTICOLO + DESCRIZIONE into ARTICOLO separated by " - "
*/
foreach ($excelData as $index => $row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Riga non valida, manca 'data' all'indice $index");
continue;
}
$value_articolo = trim((string)($row['data'][$articolo_index] ?? ''));
$value_descrizione = trim((string)($row['data'][$descrizione_index] ?? ''));
$parts = [];
if ($value_articolo !== '') {
$parts[] = $value_articolo;
}
if ($value_descrizione !== '') {
$parts[] = $value_descrizione;
}
$excelData[$index]['data'][$articolo_index] = implode(' - ', $parts);
}
error_log("Routine Le Silla completata con successo - Merge applicato su " . count($excelData) . " righe");
} catch (Exception $e) {
error_log("Eccezione nella routine Le Silla: " . $e->getMessage());
throw $e;
}
}
+41 -77
View File
@@ -1,8 +1,8 @@
/**
* saveAll.js — Save All functionality using gridData
*/
(function () {
"use strict";
(function() {
'use strict';
let saveAllRunning = false;
@@ -11,7 +11,7 @@
}
// ── Save single row ──────────────────────────────────────────────────
$(document).on("click", ".save-btn", async function () {
$(document).on('click', '.save-btn', async function() {
const btn = this;
const rowIndex = parseInt(btn.dataset.row);
const row = window.gridData?.[rowIndex];
@@ -23,44 +23,29 @@
try {
const formData = window.buildSavePayload(rowIndex);
const resp = await fetch("save_edited_row.php", {
method: "POST",
body: formData,
});
const resp = await fetch('save_edited_row.php', { method: 'POST', body: formData });
const result = await resp.json();
if (result.success) {
row._dirty = false;
if (window.gridRenderer?.clearDirty)
window.gridRenderer.clearDirty(rowIndex);
if (window.gridRenderer?.clearDirty) window.gridRenderer.clearDirty(rowIndex);
// Flash success on row without re-rendering (preserves Select2 state)
const gridRow = document.querySelector(
`.grid-row[data-id="${row.iddatadb}"]`,
);
const gridRow = document.querySelector(`.grid-row[data-id="${row.iddatadb}"]`);
if (gridRow) {
gridRow.classList.remove("row-dirty");
gridRow.querySelectorAll(".grid-cell").forEach((cell) => {
cell.classList.remove("cell-changed");
cell.classList.add("flash-success");
gridRow.classList.remove('row-dirty');
gridRow.querySelectorAll('.grid-cell').forEach(cell => {
cell.classList.remove('cell-changed');
cell.classList.add('flash-success');
});
setTimeout(
() =>
gridRow
.querySelectorAll(".flash-success")
.forEach((c) =>
c.classList.remove("flash-success"),
),
500,
);
setTimeout(() => gridRow.querySelectorAll('.flash-success').forEach(c => c.classList.remove('flash-success')), 500);
}
const toastEl = document.getElementById("saveSuccessToast");
if (toastEl)
bootstrap.Toast.getOrCreateInstance(toastEl).show();
const toastEl = document.getElementById('saveSuccessToast');
if (toastEl) bootstrap.Toast.getOrCreateInstance(toastEl).show();
} else {
alert("Errore: " + result.message);
alert('Errore: ' + result.message);
}
} catch (e) {
alert("Errore: " + e.message);
alert('Errore: ' + e.message);
} finally {
btn.innerHTML = origHtml;
btn.disabled = false;
@@ -68,68 +53,49 @@
});
// ── Save All ─────────────────────────────────────────────────────────
$(document).on("click", ".save-all-btn", function (e) {
$(document).on('click', '.save-all-btn', function(e) {
e.preventDefault();
if (isBusy()) return;
const modalEl = document.getElementById("saveAllConfirmModal");
const modalEl = document.getElementById('saveAllConfirmModal');
if (!modalEl) return;
new bootstrap.Modal(modalEl, { keyboard: false }).show();
});
$(document).on("click", "#saveAllConfirmBtn", async function () {
const confirmModal = bootstrap.Modal.getInstance(
document.getElementById("saveAllConfirmModal"),
);
$(document).on('click', '#saveAllConfirmBtn', async function() {
const confirmModal = bootstrap.Modal.getInstance(document.getElementById('saveAllConfirmModal'));
if (confirmModal) confirmModal.hide();
saveAllRunning = true;
const bar = document.getElementById("batchExportBar");
const statusEl = document.getElementById("batchExportStatus");
const cancelBtn = document.getElementById("exportBatchCancelBtn");
if (bar) bar.style.display = "";
if (cancelBtn) cancelBtn.style.display = "none";
if (statusEl) statusEl.textContent = "Saving...";
const bar = document.getElementById('batchExportBar');
const statusEl = document.getElementById('batchExportStatus');
const cancelBtn = document.getElementById('exportBatchCancelBtn');
if (bar) bar.style.display = '';
if (cancelBtn) cancelBtn.style.display = 'none';
if (statusEl) statusEl.textContent = 'Saving...';
const data = window.gridData || [];
const target =
typeof window.getTargetGridIds === "function"
? window.getTargetGridIds()
: null;
const dirtyRows = data
.map((r, i) => (r._dirty ? i : -1))
.filter((i) => i >= 0)
.filter(
(i) => !target || target.has(parseInt(data[i].iddatadb, 10)),
);
const dirtyRows = data.map((r, i) => r._dirty ? i : -1).filter(i => i >= 0);
if (dirtyRows.length === 0) {
saveAllRunning = false;
if (bar) bar.style.display = "none";
const msgEl = document.getElementById("saveAllResultMessage");
if (msgEl) msgEl.textContent = "No changes to save.";
new bootstrap.Modal(
document.getElementById("saveAllResultModal"),
).show();
if (bar) bar.style.display = 'none';
const msgEl = document.getElementById('saveAllResultMessage');
if (msgEl) msgEl.textContent = 'No changes to save.';
new bootstrap.Modal(document.getElementById('saveAllResultModal')).show();
return;
}
let success = 0,
fail = 0;
let success = 0, fail = 0;
for (const idx of dirtyRows) {
if (statusEl)
statusEl.textContent = `Saving ${success + fail + 1} / ${dirtyRows.length}...`;
if (statusEl) statusEl.textContent = `Saving ${success + fail + 1} / ${dirtyRows.length}...`;
try {
const formData = window.buildSavePayload(idx);
const resp = await fetch("save_edited_row.php", {
method: "POST",
body: formData,
});
const resp = await fetch('save_edited_row.php', { method: 'POST', body: formData });
const result = await resp.json();
if (result.success) {
data[idx]._dirty = false;
if (window.gridRenderer?.clearDirty)
window.gridRenderer.clearDirty(idx);
if (window.gridRenderer?.clearDirty) window.gridRenderer.clearDirty(idx);
success++;
} else {
fail++;
@@ -140,24 +106,22 @@
}
saveAllRunning = false;
if (bar) bar.style.display = "none";
if (bar) bar.style.display = 'none';
const gr = window.gridRenderer;
if (gr) gr.renderVisibleRows();
const msg = `Saved: ${success}` + (fail > 0 ? `, Errors: ${fail}` : "");
const msgEl = document.getElementById("saveAllResultMessage");
const msg = `Saved: ${success}` + (fail > 0 ? `, Errors: ${fail}` : '');
const msgEl = document.getElementById('saveAllResultMessage');
if (msgEl) msgEl.textContent = msg;
new bootstrap.Modal(
document.getElementById("saveAllResultModal"),
).show();
new bootstrap.Modal(document.getElementById('saveAllResultModal')).show();
});
// ── beforeunload ─────────────────────────────────────────────────────
window.addEventListener("beforeunload", function (e) {
if (window.gridData && window.gridData.some((r) => r._dirty)) {
window.addEventListener('beforeunload', function(e) {
if (window.gridData && window.gridData.some(r => r._dirty)) {
e.preventDefault();
e.returnValue = "";
e.returnValue = '';
}
});
+91
View File
@@ -0,0 +1,91 @@
<?php
// Salva un binding JSON -> LIMS (custom o fixed) e lo applica ai record appena importati. Ritorna JSON.
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
require_once __DIR__ . '/class/binding-functions.php';
include dirname(__DIR__) . '/../extra/auth.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
if (!Auth::check()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
exit;
}
$kind = ($_POST['kind'] ?? 'custom') === 'fixed' ? 'fixed' : 'custom';
$templateId = intval($_POST['template_id'] ?? 0);
$jsonValue = (string) ($_POST['json_value'] ?? '');
$limsValueId = intval($_POST['lims_value_id'] ?? 0);
$limsValue = (string) ($_POST['lims_value'] ?? '');
$datadbIds = [];
if (isset($_POST['datadb_ids'])) {
$decoded = json_decode($_POST['datadb_ids'], true);
if (is_array($decoded)) {
$datadbIds = $decoded;
}
}
if ($templateId <= 0 || $jsonValue === '' || $limsValueId <= 0) {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Missing required parameters']);
exit;
}
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$createdBy = Auth::user() ? (int) Auth::user()->present()->id : null;
if ($kind === 'fixed') {
$fixedKey = trim($_POST['fixed_field_key'] ?? '');
$column = binding_fixed_column($fixedKey);
if ($fixedKey === '' || !binding_fixed_is_list($fixedKey) || !$column) {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Invalid fixed field']);
exit;
}
binding_upsert_fixed($pdo, $templateId, $fixedKey, $jsonValue, $limsValueId, $limsValue, $createdBy);
$applied = !empty($datadbIds) ? binding_apply_to_datadb($pdo, $column, $limsValueId, $datadbIds) : 0;
} else {
$mappingId = intval($_POST['mapping_id'] ?? 0);
$fieldId = intval($_POST['field_id'] ?? 0);
if ($mappingId <= 0) {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Missing mapping_id']);
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, $jsonValue, $limsValueId, $limsValue, $createdBy);
$applied = !empty($datadbIds) ? binding_apply_to_details($pdo, $mappingId, $limsValue, $datadbIds) : 0;
}
echo json_encode([
'success' => true,
'applied_rows' => $applied,
'kind' => $kind,
'json_value' => $jsonValue,
'lims_value' => $limsValue,
'lims_value_id' => $limsValueId,
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
+10 -2
View File
@@ -54,6 +54,15 @@ try {
:is_web_selectable,
:is_accredited
)
ON DUPLICATE KEY UPDATE
analysis_name = VALUES(analysis_name),
analysis_method = VALUES(analysis_method),
analysis_level = VALUES(analysis_level),
is_web_selectable = VALUES(is_web_selectable),
is_accredited = VALUES(is_accredited),
iddatadb = VALUES(iddatadb),
idmatrice = VALUES(idmatrice),
updated_at = CURRENT_TIMESTAMP
");
$stmt->execute([
@@ -70,8 +79,7 @@ try {
echo json_encode([
'success' => true,
'message' => 'Association saved',
'id' => (int)$pdo->lastInsertId()
'message' => 'Association saved'
]);
} catch (Throwable $e) {
http_response_code(500);
+1 -13
View File
@@ -815,7 +815,7 @@
"IdSchemaCustomFields": 191,
"ConteggioClienti": 0,
"Nome": "KIABI",
"Descrizione": "Schema per il cliente Kiabi\r\n\r\n"
"Descrizione": "Schema per il cliente Kiabi\r\n"
},
{
"IdSchemaCustomFields": 192,
@@ -900,18 +900,6 @@
"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"
}
]
}
+6 -24
View File
@@ -19,14 +19,6 @@ $q = mb_strtolower(trim($_GET['q'] ?? ''));
$limit = max(1, min(50, intval($_GET['limit'] ?? 20)));
$id = isset($_GET['id']) ? intval($_GET['id']) : null;
$tipo = $_GET['tipo'] ?? ''; // 'attivo' | 'inattivo' | '' (nessun filtro)
function isInattivo(array $c): bool
{
$v = $c['Inattivo'] ?? false;
return $v === true || $v === 1 || $v === '1' || $v === 'true';
}
function formatClientLabel(array $client): string
{
$name = trim($client['Nominativo'] ?? '');
@@ -44,7 +36,7 @@ function formatClientLabel(array $client): string
$suffix = '--';
}
return $name . ' - ' . $suffix . ' - ' . $id;
return $name . ' - ' . $suffix . ' (ID: ' . $id . ')';
}
try {
@@ -57,7 +49,7 @@ try {
$api = VisualLimsApiClient::getInstance();
$params = [
'$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo',
'$select' => 'IdCliente,Nominativo,CodiceCliente',
'$orderby' => 'Nominativo asc'
];
@@ -72,14 +64,6 @@ try {
$clients = $data['value'] ?? [];
// Filtro tipo, applicato SOLO alla ricerca, non al lookup per id
$searchClients = $clients;
if ($tipo === 'attivo') {
$searchClients = array_values(array_filter($clients, fn($c) => !isInattivo($c)));
} elseif ($tipo === 'inattivo') {
$searchClients = array_values(array_filter($clients, fn($c) => isInattivo($c)));
}
// If requesting by specific ID, used for loading selected value
if ($id !== null) {
foreach ($clients as $c) {
@@ -104,20 +88,18 @@ try {
// Search by query
$results = [];
foreach ($searchClients as $c) {
foreach ($clients as $c) {
$name = trim($c['Nominativo'] ?? '');
$code = trim($c['CodiceCliente'] ?? '');
// Cerca solo su ciò che viene visualizzato: Nome - Lettera - ID
$label = formatClientLabel($c);
if (
$q === '' ||
mb_strpos(mb_strtolower($label), $q) !== false
mb_strpos(mb_strtolower($name), $q) !== false ||
mb_strpos(mb_strtolower($code), $q) !== false
) {
$results[] = [
'id' => $c['IdCliente'],
'text' => $label,
'text' => formatClientLabel($c),
'IdCliente' => $c['IdCliente'],
'Nominativo' => $name,
'CodiceCliente' => $code
+7 -63
View File
@@ -51,74 +51,18 @@ try {
exit;
}
// Search — multi-term AND, position-independent, ranked
$terms = array_values(array_filter(
preg_split('/\s+/u', $q, -1, PREG_SPLIT_NO_EMPTY)
));
// Nessun termine digitato: alfabetico, taglio al limite
if (empty($terms)) {
$all = [];
foreach ($values as $v) {
$all[] = ['id' => $v['IdCustomFieldsValue'], 'text' => $v['Valore'] ?? ''];
}
usort($all, fn($a, $b) => strcasecmp($a['text'], $b['text']));
if ($limit > 0) $all = array_slice($all, 0, $limit);
echo json_encode(['results' => $all]);
exit;
}
$firstTerm = $terms[0];
$scored = [];
// Search by query
$results = [];
foreach ($values as $v) {
$text = $v['Valore'] ?? '';
$textLower = mb_strtolower($text);
// AND: tutti i termini devono essere presenti, in qualsiasi posizione
$matchAll = true;
foreach ($terms as $t) {
if (mb_strpos($textLower, $t) === false) {
$matchAll = false;
break;
}
if ($q === '' || mb_strpos(mb_strtolower($text), $q) !== false) {
$results[] = ['id' => $v['IdCustomFieldsValue'], 'text' => $text];
if ($limit > 0 && count($results) >= $limit) break;
}
if (!$matchAll) continue;
// Punteggio di pertinenza (più basso = più rilevante)
if ($textLower === $q) {
$score = 0; // match esatto
} elseif (mb_strpos($textLower, $firstTerm) === 0) {
$score = 1; // inizia col primo termine
} elseif (preg_match('/(^|\s)' . preg_quote($firstTerm, '/') . '/u', $textLower)) {
$score = 2; // primo termine a inizio di una parola
} else {
$score = 3; // presente ma non a inizio parola
}
$scored[] = [
'id' => $v['IdCustomFieldsValue'],
'text' => $text,
'_s' => $score,
'_n' => $textLower
];
}
// Ordina per punteggio, poi alfabetico
usort($scored, function ($a, $b) {
if ($a['_s'] !== $b['_s']) return $a['_s'] - $b['_s'];
return strcmp($a['_n'], $b['_n']);
});
// Taglia DOPO l'ordinamento e rimuovi i campi interni
if ($limit > 0) {
$scored = array_slice($scored, 0, $limit);
}
$results = array_map(
fn($r) => ['id' => $r['id'], 'text' => $r['text']],
$scored
);
// Sort alphabetically
usort($results, fn($a, $b) => strcasecmp($a['text'], $b['text']));
echo json_encode(['results' => $results]);
} catch (Exception $e) {
@@ -0,0 +1,65 @@
<?php
// Select2 source of LIMS values for a fixed field. Returns {results:[{id,text}]}.
// Params: field_key (required), template_id (required, drives client-dependent lists),
// q (search), id (resolve a single value for preselect), limit.
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 = intval($_GET['template_id'] ?? 0);
$q = mb_strtolower(trim($_GET['q'] ?? ''));
$id = isset($_GET['id']) ? intval($_GET['id']) : null;
$rawLimit = intval($_GET['limit'] ?? 30);
$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 ($id !== null) {
foreach ($values as $v) {
if ((int) $v['id'] === $id) {
echo json_encode(['results' => [['id' => $v['id'], 'text' => $v['text']]]]);
exit;
}
}
echo json_encode(['results' => []]);
exit;
}
$results = [];
foreach ($values as $v) {
$text = (string) $v['text'];
if ($q === '' || mb_strpos(mb_strtolower($text), $q) !== false) {
$results[] = ['id' => $v['id'], 'text' => $text];
if ($limit > 0 && count($results) >= $limit) {
break;
}
}
}
usort($results, fn($a, $b) => strcasecmp($a['text'], $b['text']));
echo json_encode(['results' => $results]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
+6 -65
View File
@@ -2,11 +2,7 @@
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; }
header('Content-Type: application/json');
ini_set('display_errors', '0');
@@ -62,71 +58,16 @@ if (isset($_GET['macro_list'])) {
echo json_encode(['success' => true, 'value' => $macros]);
exit;
}
// Return full matrix list (same shape as get_matrici_db.php)
if (isset($_GET['full_list'])) {
echo json_encode(['success' => true, 'value' => $matrici]);
exit;
}
// Search (with optional MacroMatrice filter) - multi-term AND, position-independent, ranked
$terms = array_values(array_filter(
preg_split('/\s+/u', $q, -1, PREG_SPLIT_NO_EMPTY)
));
// Nessun termine digitato: comportamento invariato (alfabetico, taglio al limite)
if (empty($terms)) {
$results = [];
foreach ($matrici as $m) {
if ($macro !== '' && ($m['MacroMatrice'] ?? '') !== $macro) continue;
$results[] = ['id' => $m['IdMatrice'], 'text' => $m['NomeMatrice'] ?? ''];
if (count($results) >= $limit) break;
}
echo json_encode(['results' => $results]);
exit;
}
$firstTerm = $terms[0];
$scored = [];
// Search (with optional MacroMatrice filter)
$results = [];
foreach ($matrici as $m) {
$nome = $m['NomeMatrice'] ?? '';
if ($macro !== '' && ($m['MacroMatrice'] ?? '') !== $macro) continue;
$nomeLower = mb_strtolower($nome);
// AND: tutti i termini devono essere presenti (in qualsiasi posizione)
$matchAll = true;
foreach ($terms as $t) {
if (mb_strpos($nomeLower, $t) === false) {
$matchAll = false;
break;
}
if ($q === '' || mb_strpos(mb_strtolower($nome), $q) !== false) {
$results[] = ['id' => $m['IdMatrice'], 'text' => $nome];
if (count($results) >= $limit) break;
}
if (!$matchAll) continue;
// Punteggio di pertinenza (più basso = più rilevante, per ordinamento crescente)
if ($nomeLower === $q) {
$score = 0; // match esatto
} elseif (mb_strpos($nomeLower, $firstTerm) === 0) {
$score = 1; // il nome inizia col primo termine
} elseif (preg_match('/(^|\s)' . preg_quote($firstTerm, '/') . '/u', $nomeLower)) {
$score = 2; // primo termine a inizio di una parola
} else {
$score = 3; // termine presente ma non a inizio parola
}
$scored[] = ['id' => $m['IdMatrice'], 'text' => $nome, '_s' => $score, '_n' => $nomeLower];
}
// Ordina per punteggio, poi alfabetico (stabile e prevedibile)
usort($scored, function ($a, $b) {
if ($a['_s'] !== $b['_s']) return $a['_s'] - $b['_s'];
return strcmp($a['_n'], $b['_n']);
});
// Taglia al limite DOPO l'ordinamento e rimuovi i campi interni
$results = array_map(
fn($r) => ['id' => $r['id'], 'text' => $r['text']],
array_slice($scored, 0, $limit)
);
echo json_encode(['results' => $results]);
+72
View File
@@ -0,0 +1,72 @@
<?php
// "Nessuna corrispondenza": azzera il valore importato (custom o fixed) e rimuove il binding. Ritorna JSON.
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
require_once __DIR__ . '/class/binding-functions.php';
include dirname(__DIR__) . '/../extra/auth.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
if (!Auth::check()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
exit;
}
$kind = ($_POST['kind'] ?? 'custom') === 'fixed' ? 'fixed' : 'custom';
$templateId = intval($_POST['template_id'] ?? 0);
$jsonValue = (string) ($_POST['json_value'] ?? '');
$datadbIds = [];
if (isset($_POST['datadb_ids'])) {
$decoded = json_decode($_POST['datadb_ids'], true);
if (is_array($decoded)) {
$datadbIds = $decoded;
}
}
if ($jsonValue === '') {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Missing json_value']);
exit;
}
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
if ($kind === 'fixed') {
$fixedKey = trim($_POST['fixed_field_key'] ?? '');
$column = binding_fixed_column($fixedKey);
if ($fixedKey === '' || !binding_fixed_is_list($fixedKey) || !$column || $templateId <= 0) {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Invalid fixed field']);
exit;
}
$cleared = !empty($datadbIds) ? binding_apply_to_datadb($pdo, $column, null, $datadbIds) : 0;
binding_delete_target($pdo, binding_target_fixed($templateId, $fixedKey), $jsonValue);
} else {
$mappingId = intval($_POST['mapping_id'] ?? 0);
if ($mappingId <= 0) {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Missing mapping_id']);
exit;
}
$cleared = !empty($datadbIds) ? binding_apply_to_details($pdo, $mappingId, '', $datadbIds) : 0;
binding_delete_target($pdo, binding_target_custom($mappingId), $jsonValue);
}
echo json_encode(['success' => true, 'cleared_rows' => $cleared]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
-546
View File
@@ -1,546 +0,0 @@
<?php include('include/headscript.php'); ?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>TRF-Project - Statistiche Export LIMS</title>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<style>
.stat-card .card-body {
padding: 1rem 1.25rem;
}
.stat-value {
font-size: 1.75rem;
font-weight: 700;
line-height: 1.1;
}
.stat-label {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: .03em;
color: #6c757d;
}
.chart-wrap {
position: relative;
height: 420px;
}
#tblClienti,
#tblUtenti {
font-size: 13px;
}
#tblClienti th,
#tblClienti td,
#tblUtenti th,
#tblUtenti td {
vertical-align: middle;
}
.filter-bar {
gap: .75rem;
}
.stat-card-extra {
background: linear-gradient(135deg, #021bff 0%, #010983 100%);
border: none;
}
.stat-card-extra .stat-label {
color: #d7d2f5;
}
.stat-card-extra .stat-value {
color: #ffffff !important;
}
.stat-card-extra small.text-muted {
color: #c9c2ec !important;
}
.kpi-row {
margin-bottom: 1rem;
}
</style>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<!-- ===== FILTRO GIORNATA ===== -->
<div class="card radius-10">
<div class="card-body">
<div class="d-flex align-items-center flex-wrap filter-bar">
<h6 class="mb-0 me-3"><i class="bx bx-bar-chart-alt-2"></i> Statistiche Export LIMS</h6>
<div class="d-flex align-items-center">
<label for="filterDate" class="me-2 mb-0 fw-semibold">Giornata:</label>
<input type="date" id="filterDate" class="form-control form-control-sm" style="width:auto;">
</div>
<div class="btn-group btn-group-sm ms-2" role="group">
<button class="btn btn-outline-secondary" id="btnToday">Oggi</button>
<button class="btn btn-outline-secondary" id="btnYesterday">Ieri</button>
</div>
<a href="stats_export_lims_monthly.php" class="btn btn-outline-primary btn-sm ms-auto">
<i class="bx bx-calendar-alt"></i> Vista mensile
</a>
<button class="btn btn-primary btn-sm ms-2" id="btnRefresh">
<i class="bx bx-refresh"></i> Aggiorna
</button>
</div>
</div>
</div>
<!-- ===== KPI ===== -->
<div class="row row-cols-2 row-cols-md-3 row-cols-lg-5 g-3 kpi-row">
<div class="col">
<div class="card radius-10 stat-card stat-card-extra h-100">
<div class="card-body">
<div class="stat-label">Campioni mese <span id="kpiMonthLabel"></span></div>
<div class="stat-value" id="kpiPartsMonth">0</div>
<small class="text-muted">Commesse: <span id="kpiExportMonth">0</span> · dal 16/07/2026</small>
</div>
</div>
</div>
<div class="col">
<div class="card radius-10 stat-card stat-card-extra h-100">
<div class="card-body">
<div class="stat-label">Campioni anno <span id="kpiYearLabel"></span></div>
<div class="stat-value" id="kpiPartsYear">0</div>
<small class="text-muted">Commesse: <span id="kpiExportYear">0</span> · dal 16/07/2026</small>
</div>
</div>
</div>
<div class="col">
<div class="card radius-10 stat-card h-100">
<div class="card-body">
<div class="stat-label">Campioni esportati (giorno)</div>
<div class="stat-value text-primary" id="kpiParts">0</div>
<small class="text-muted">Commesse: <span id="kpiExport">0</span></small>
</div>
</div>
</div>
<div class="col">
<div class="card radius-10 stat-card h-100">
<div class="card-body">
<div class="stat-label">Campioni inseriti LIMS</div>
<div class="stat-value text-info" id="kpiLimsSamples">0</div>
</div>
</div>
</div>
<div class="col">
<div class="card radius-10 stat-card h-100">
<div class="card-body">
<div class="stat-label">% Esportati / inseriti</div>
<div class="stat-value text-success" id="kpiPerc"></div>
</div>
</div>
</div>
</div>
<!-- ===== DATO GIORNALIERO CAMPIONI LIMS ===== -->
<div class="card radius-10">
<div class="card-body">
<div class="d-flex align-items-center flex-wrap" style="gap:.75rem;">
<span class="fw-semibold">Campioni totali inseriti nel LIMS per questa giornata:</span>
<input type="number" min="0" id="limsSamplesInput" class="form-control form-control-sm" style="width:140px;" placeholder="0">
<button class="btn btn-success btn-sm" id="btnSaveLimsSamples">
<i class="bx bx-save"></i> Salva dato
</button>
<small class="text-muted">Un valore al giorno. Usato per calcolare la % di export.</small>
</div>
</div>
</div>
<!-- ===== GRAFICO TORTA ESPORTATI vs TOTALE LIMS ===== -->
<div class="row" id="pieRow" style="display:none;">
<div class="col-12 col-lg-6">
<div class="card radius-10">
<div class="card-header">
<h6 class="mb-0">Esportati vs totale inserito nel LIMS</h6>
</div>
<div class="card-body">
<div class="chart-wrap" style="height:340px;"><canvas id="chartPie"></canvas></div>
</div>
</div>
</div>
</div>
<!-- ===== GRAFICI ===== -->
<div class="row">
<div class="col-12 col-lg-6">
<div class="card radius-10">
<div class="card-header">
<h6 class="mb-0">Export per cliente (idclient) <small class="text-muted"> top 15</small></h6>
</div>
<div class="card-body">
<div class="chart-wrap"><canvas id="chartClienti"></canvas></div>
</div>
</div>
</div>
<div class="col-12 col-lg-6">
<div class="card radius-10">
<div class="card-header">
<h6 class="mb-0">Export per utente (user_id) <small class="text-muted"> top 15</small></h6>
</div>
<div class="card-body">
<div class="chart-wrap"><canvas id="chartUtenti"></canvas></div>
</div>
</div>
</div>
</div>
<!-- ===== LISTE ===== -->
<div class="row">
<div class="col-12 col-lg-6">
<div class="card radius-10">
<div class="card-header">
<h6 class="mb-0">Dettaglio per cliente</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table id="tblClienti" class="table table-striped table-bordered table-sm w-100">
<thead>
<tr>
<th>ID Cliente</th>
<th>Cliente</th>
<th>Campioni</th>
<th>Commesse</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-12 col-lg-6">
<div class="card radius-10">
<div class="card-header">
<h6 class="mb-0">Dettaglio per utente</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table id="tblUtenti" class="table table-striped table-bordered table-sm w-100">
<thead>
<tr>
<th>ID Utente</th>
<th>Utente</th>
<th>Campioni</th>
<th>Commesse</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
let chartClienti = null;
let chartUtenti = null;
let chartPie = null;
let dtClienti = null;
let dtUtenti = null;
function todayStr() {
const d = new Date();
const off = d.getTimezoneOffset();
return new Date(d.getTime() - off * 60000).toISOString().slice(0, 10);
}
function initTables() {
dtClienti = $('#tblClienti').DataTable({
paging: false,
searching: false,
info: false,
order: [
[2, 'desc']
],
columns: [{
data: 'idclient'
},
{
data: 'clientname',
defaultContent: '—'
},
{
data: 'parts',
className: 'text-end'
},
{
data: 'tot',
className: 'text-end'
}
],
language: {
emptyTable: 'Nessun export in questa giornata'
}
});
dtUtenti = $('#tblUtenti').DataTable({
paging: false,
searching: false,
info: false,
order: [
[2, 'desc']
],
columns: [{
data: 'user_id'
},
{
data: 'username',
defaultContent: '—'
},
{
data: 'parts',
className: 'text-end'
},
{
data: 'tot',
className: 'text-end'
}
],
language: {
emptyTable: 'Nessun export in questa giornata'
}
});
}
function renderBarChart(existing, canvasId, labels, values, label) {
if (existing) existing.destroy();
const ctx = document.getElementById(canvasId).getContext('2d');
return new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: label,
data: values,
borderWidth: 1
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
beginAtZero: true,
ticks: {
precision: 0
}
}
}
}
});
}
// Torta: esportati vs non esportati (totale LIMS - esportati)
function renderPie(exported, limsTotal) {
const pieRow = document.getElementById('pieRow');
// Mostra solo se il totale è inserito e valido
if (!limsTotal || limsTotal <= 0) {
pieRow.style.display = 'none';
if (chartPie) {
chartPie.destroy();
chartPie = null;
}
return;
}
pieRow.style.display = '';
const notExported = Math.max(0, limsTotal - exported);
if (chartPie) chartPie.destroy();
const ctx = document.getElementById('chartPie').getContext('2d');
chartPie = new Chart(ctx, {
type: 'pie',
data: {
labels: ['Esportati', 'Totali LIMS'],
datasets: [{
data: [exported, notExported],
backgroundColor: ['#198754', '#dee2e6'],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
},
tooltip: {
callbacks: {
label: function(c) {
const perc = limsTotal > 0 ? ((c.parsed / limsTotal) * 100).toFixed(1) : 0;
return c.label + ': ' + c.parsed + ' (' + perc + '%)';
}
}
}
}
}
});
}
// Prende i primi N record (già ordinati desc dal backend) per il grafico
const CHART_TOP_N = 15;
function topN(arr) {
return arr.slice(0, CHART_TOP_N);
}
function loadStats() {
const date = $('#filterDate').val() || todayStr();
$.getJSON('stats_export_lims_data.php', {
date: date
}, function(res) {
if (!res || !res.success) {
Swal.fire('Errore', (res && res.message) || 'Errore nel caricamento', 'error');
return;
}
// KPI — dato principale = parti/campioni, sotto le commesse
$('#kpiParts').text(res.totals.parts ?? 0);
$('#kpiExport').text(res.totals.export ?? 0);
$('#kpiPartsMonth').text(res.totalsMonth ? (res.totalsMonth.parts ?? 0) : 0);
$('#kpiExportMonth').text(res.totalsMonth ? (res.totalsMonth.export ?? 0) : 0);
$('#kpiPartsYear').text(res.totalsYear ? (res.totalsYear.parts ?? 0) : 0);
$('#kpiExportYear').text(res.totalsYear ? (res.totalsYear.export ?? 0) : 0);
$('#kpiLimsSamples').text(res.limsSamples ?? 0);
$('#limsSamplesInput').val(res.limsSamples ?? '');
// Etichette mese/anno dei box totali
if (res.month) {
const MESI_IT = ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno',
'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'
];
const mParts = res.month.split('-');
const mIdx = parseInt(mParts[1], 10) - 1;
if (mIdx >= 0 && mIdx < 12) {
$('#kpiMonthLabel').text('(' + MESI_IT[mIdx] + ' ' + mParts[0] + ')');
}
}
if (res.year) {
$('#kpiYearLabel').text('(' + res.year + ')');
}
const limsTot = parseInt(res.limsSamples ?? 0, 10);
const partsTot = parseInt(res.totals.parts ?? 0, 10);
if (limsTot > 0) {
const perc = (partsTot / limsTot) * 100;
$('#kpiPerc').text(perc.toFixed(1) + '%');
} else {
$('#kpiPerc').text('—');
}
// Torta parti esportate vs totale inserito nel LIMS
renderPie(partsTot, limsTot);
// Grafici — valori = campioni/parti
const clientTop = topN(res.byClient);
const userTop = topN(res.byUser);
chartClienti = renderBarChart(chartClienti, 'chartClienti',
clientTop.map(r => r.clientname ? (r.clientname + ' (' + r.idclient + ')') : ('ID ' + r.idclient)),
clientTop.map(r => r.parts), 'Campioni');
chartUtenti = renderBarChart(chartUtenti, 'chartUtenti',
userTop.map(r => r.username ? (r.username + ' (' + r.user_id + ')') : ('ID ' + r.user_id)),
userTop.map(r => r.parts), 'Campioni');
// Tabelle
dtClienti.clear().rows.add(res.byClient).draw();
dtUtenti.clear().rows.add(res.byUser).draw();
}).fail(function(xhr) {
Swal.fire('Errore', 'Richiesta fallita: ' + xhr.status, 'error');
});
}
$(document).ready(function() {
$('#filterDate').val(todayStr());
initTables();
loadStats();
$('#btnRefresh').on('click', loadStats);
$('#filterDate').on('change', loadStats);
$('#btnToday').on('click', function() {
$('#filterDate').val(todayStr());
loadStats();
});
$('#btnYesterday').on('click', function() {
const d = new Date();
d.setDate(d.getDate() - 1);
const off = d.getTimezoneOffset();
$('#filterDate').val(new Date(d.getTime() - off * 60000).toISOString().slice(0, 10));
loadStats();
});
$('#btnSaveLimsSamples').on('click', function() {
const date = $('#filterDate').val() || todayStr();
const total = parseInt($('#limsSamplesInput').val(), 10);
if (isNaN(total) || total < 0) {
Swal.fire('Attenzione', 'Inserisci un numero valido', 'warning');
return;
}
$.ajax({
url: 'stats_save_lims_samples.php',
type: 'POST',
dataType: 'json',
data: {
date: date,
total_samples: total
},
success: function(res) {
if (res && res.success) {
Swal.fire({
title: 'Salvato',
icon: 'success',
timer: 1200,
showConfirmButton: false
});
loadStats();
} else {
Swal.fire('Errore', (res && res.message) || 'Salvataggio fallito', 'error');
}
},
error: function(xhr) {
Swal.fire('Errore', 'Richiesta fallita: ' + xhr.status, 'error');
}
});
});
});
</script>
</body>
</html>
-156
View File
@@ -1,156 +0,0 @@
<?php
include('include/headscript.php');
header('Content-Type: application/json');
$dbHandler = DBHandlerSelect::getInstance();
$pdo = $dbHandler->getConnection();
try {
// Data richiesta (default oggi). Formato atteso: Y-m-d
$date = $_GET['date'] ?? date('Y-m-d');
$d = DateTime::createFromFormat('Y-m-d', $date);
if (!$d || $d->format('Y-m-d') !== $date) {
throw new Exception('Data non valida');
}
// --- Export per cliente (status = 'l', giornata su export_date) ---
// tot = numero commesse, parts = numero parti/campioni
$stmtC = $pdo->prepare("
SELECT d.idclient AS idclient,
COUNT(DISTINCT d.iddatadb) AS tot,
COUNT(ip.id) AS parts
FROM datadb d
LEFT JOIN identification_parts ip ON ip.iddatadb = d.iddatadb
WHERE d.status = 'l'
AND DATE(d.export_date) = :d
GROUP BY d.idclient
ORDER BY parts DESC
");
$stmtC->execute(['d' => $date]);
$byClient = $stmtC->fetchAll(PDO::FETCH_ASSOC);
// --- Risolvi Nominativo cliente dalla cache locale (search_clienti.php) ---
$clientNames = [];
$cacheFile = __DIR__ . '/cache/clienti.json';
if (is_file($cacheFile)) {
$cache = json_decode(file_get_contents($cacheFile), true);
foreach (($cache['value'] ?? []) as $c) {
$clientNames[(int)($c['IdCliente'] ?? 0)] = trim($c['Nominativo'] ?? '');
}
}
// --- Export per utente (status = 'l', giornata su export_date) ---
// tot = numero commesse, parts = numero parti/campioni
$stmtU = $pdo->prepare("
SELECT d.user_id AS user_id,
TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS username,
COUNT(DISTINCT d.iddatadb) AS tot,
COUNT(ip.id) AS parts
FROM datadb d
LEFT JOIN auth_users u ON u.id = d.user_id
LEFT JOIN identification_parts ip ON ip.iddatadb = d.iddatadb
WHERE d.status = 'l'
AND DATE(d.export_date) = :d
GROUP BY d.user_id, username
ORDER BY parts DESC
");
$stmtU->execute(['d' => $date]);
$byUser = $stmtU->fetchAll(PDO::FETCH_ASSOC);
// Normalizza tipi (COUNT torna stringa in PDO)
foreach ($byClient as &$r) {
$r['idclient'] = $r['idclient'] !== null ? (int)$r['idclient'] : null;
$r['tot'] = (int)$r['tot'];
$r['parts'] = (int)$r['parts'];
$nm = $clientNames[$r['idclient']] ?? '';
$r['clientname'] = $nm !== '' ? $nm : null;
}
unset($r);
foreach ($byUser as &$r) {
$r['user_id'] = $r['user_id'] !== null ? (int)$r['user_id'] : null;
$r['tot'] = (int)$r['tot'];
$r['parts'] = (int)$r['parts'];
if (isset($r['username'])) $r['username'] = $r['username'] !== null && $r['username'] !== '' ? $r['username'] : null;
}
unset($r);
// --- Totale export della giornata ---
$totExport = array_sum(array_column($byClient, 'tot'));
// --- Totale PARTI/CAMPIONI della giornata (righe identification_parts legate
// ai datadb esportati status='l' in questa giornata) ---
$stmtP = $pdo->prepare("
SELECT COUNT(*)
FROM identification_parts ip
INNER JOIN datadb d ON d.iddatadb = ip.iddatadb
WHERE d.status = 'l'
AND DATE(d.export_date) = :d
");
$stmtP->execute(['d' => $date]);
$totParts = (int)$stmtP->fetchColumn();
// --- Totale COMMESSE del MESE (status = 'l') ---
$stmtM = $pdo->prepare("
SELECT COUNT(*)
FROM datadb d
WHERE d.status = 'l'
AND DATE_FORMAT(d.export_date, '%Y-%m') = :m
");
$stmtM->execute(['m' => $d->format('Y-m')]);
$totExportMonth = (int)$stmtM->fetchColumn();
// --- Totale PARTI del MESE (status = 'l') ---
$stmtMP = $pdo->prepare("
SELECT COUNT(*)
FROM identification_parts ip
INNER JOIN datadb d ON d.iddatadb = ip.iddatadb
WHERE d.status = 'l'
AND DATE_FORMAT(d.export_date, '%Y-%m') = :m
");
$stmtMP->execute(['m' => $d->format('Y-m')]);
$totPartsMonth = (int)$stmtMP->fetchColumn();
// --- Totale COMMESSE dell'ANNO (status = 'l') ---
$stmtY = $pdo->prepare("
SELECT COUNT(*)
FROM datadb d
WHERE d.status = 'l'
AND YEAR(d.export_date) = :y
");
$stmtY->execute(['y' => $d->format('Y')]);
$totExportYear = (int)$stmtY->fetchColumn();
// --- Totale PARTI dell'ANNO (status = 'l') ---
$stmtYP = $pdo->prepare("
SELECT COUNT(*)
FROM identification_parts ip
INNER JOIN datadb d ON d.iddatadb = ip.iddatadb
WHERE d.status = 'l'
AND YEAR(d.export_date) = :y
");
$stmtYP->execute(['y' => $d->format('Y')]);
$totPartsYear = (int)$stmtYP->fetchColumn();
// --- Campioni totali inseriti nel LIMS per la giornata (tabella lims_daily_samples) ---
$stmtL = $pdo->prepare("SELECT total_samples FROM lims_daily_samples WHERE sample_date = :d LIMIT 1");
$stmtL->execute(['d' => $date]);
$limsSamples = $stmtL->fetchColumn();
$limsSamples = ($limsSamples !== false) ? (int)$limsSamples : null;
echo json_encode([
'success' => true,
'date' => $date,
'month' => $d->format('Y-m'),
'year' => (int)$d->format('Y'),
'totals' => ['export' => (int)$totExport, 'parts' => $totParts],
'totalsMonth' => ['export' => $totExportMonth, 'parts' => $totPartsMonth],
'totalsYear' => ['export' => $totExportYear, 'parts' => $totPartsYear],
'byClient' => $byClient,
'byUser' => $byUser,
'limsSamples' => $limsSamples,
]);
} catch (Exception $e) {
error_log('Stats export LIMS error: ' . $e->getMessage());
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -1,440 +0,0 @@
<?php include('include/headscript.php'); ?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>TRF-Project - Statistiche Export LIMS Mensili</title>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<style>
.stat-card .card-body {
padding: 1rem 1.25rem;
}
.stat-value {
font-size: 1.75rem;
font-weight: 700;
line-height: 1.1;
}
.stat-label {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: .03em;
color: #6c757d;
}
.chart-wrap {
position: relative;
height: 420px;
}
#tblClienti,
#tblUtenti {
font-size: 13px;
}
#tblClienti th,
#tblClienti td,
#tblUtenti th,
#tblUtenti td {
vertical-align: middle;
}
.filter-bar {
gap: .75rem;
}
.month-label {
font-size: 1.05rem;
font-weight: 600;
}
.stat-card-extra {
background: linear-gradient(135deg, #021bff 0%, #010983 100%);
border: none;
}
.stat-card-extra .stat-label {
color: #c9cfff;
}
.stat-card-extra .stat-value {
color: #ffffff !important;
}
.stat-card-extra small.text-muted {
color: #b3bbff !important;
}
</style>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<!-- ===== FILTRO MESE ===== -->
<div class="card radius-10">
<div class="card-body">
<div class="d-flex align-items-center flex-wrap filter-bar">
<h6 class="mb-0 me-3"><i class="bx bx-bar-chart-alt-2"></i> Statistiche Export LIMS Mensili</h6>
<div class="d-flex align-items-center">
<label for="filterMonth" class="me-2 mb-0 fw-semibold">Mese:</label>
<input type="month" id="filterMonth" class="form-control form-control-sm" style="width:auto;">
</div>
<div class="btn-group btn-group-sm ms-2" role="group">
<button class="btn btn-outline-secondary" id="btnCurrentMonth">Mese corrente</button>
<button class="btn btn-outline-secondary" id="btnPrevMonth">Mese precedente</button>
</div>
<span class="month-label ms-2 text-primary" id="monthLabel"></span>
<a href="stats_export_lims.php" class="btn btn-outline-primary btn-sm ms-auto">
<i class="bx bx-calendar"></i> Vista giornaliera
</a>
<button class="btn btn-primary btn-sm ms-2" id="btnRefresh">
<i class="bx bx-refresh"></i> Aggiorna
</button>
</div>
</div>
</div>
<!-- ===== KPI (una sola riga) ===== -->
<div class="row row-cols-2 row-cols-md-3 row-cols-lg-5 g-3">
<div class="col">
<div class="card radius-10 stat-card h-100">
<div class="card-body">
<div class="stat-label">Campioni mese</div>
<div class="stat-value text-primary" id="kpiParts">0</div>
<small class="text-muted">Commesse: <span id="kpiExport">0</span></small>
</div>
</div>
</div>
<div class="col">
<div class="card radius-10 stat-card stat-card-extra h-100">
<div class="card-body">
<div class="stat-label">Campioni anno <span id="yearLabel"></span></div>
<div class="stat-value" id="kpiPartsYear">0</div>
<small class="text-muted">Commesse: <span id="kpiExportYear">0</span> · dal 16/07/2026</small>
</div>
</div>
</div>
<div class="col">
<div class="card radius-10 stat-card h-100">
<div class="card-body">
<div class="stat-label">Campioni inseriti LIMS (mese)</div>
<div class="stat-value text-info" id="kpiLimsSamples">0</div>
</div>
</div>
</div>
<div class="col">
<div class="card radius-10 stat-card h-100">
<div class="card-body">
<div class="stat-label">% Esportati / inseriti</div>
<div class="stat-value text-success" id="kpiPerc"></div>
</div>
</div>
</div>
<div class="col">
<div class="card radius-10 stat-card h-100">
<div class="card-body">
<div class="stat-label">Clienti distinti (mese)</div>
<div class="stat-value text-warning" id="kpiClienti">0</div>
</div>
</div>
</div>
</div>
<!-- ===== GRAFICI ===== -->
<div class="row mt-1">
<div class="col-12 col-lg-6">
<div class="card radius-10">
<div class="card-header">
<h6 class="mb-0">Campioni per cliente (idclient) <small class="text-muted"> top 15</small></h6>
</div>
<div class="card-body">
<div class="chart-wrap"><canvas id="chartClienti"></canvas></div>
</div>
</div>
</div>
<div class="col-12 col-lg-6">
<div class="card radius-10">
<div class="card-header">
<h6 class="mb-0">Campioni per utente (user_id) <small class="text-muted"> top 15</small></h6>
</div>
<div class="card-body">
<div class="chart-wrap"><canvas id="chartUtenti"></canvas></div>
</div>
</div>
</div>
</div>
<!-- ===== LISTE ===== -->
<div class="row">
<div class="col-12 col-lg-6">
<div class="card radius-10">
<div class="card-header">
<h6 class="mb-0">Dettaglio per cliente</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table id="tblClienti" class="table table-striped table-bordered table-sm w-100">
<thead>
<tr>
<th>ID Cliente</th>
<th>Cliente</th>
<th>Campioni</th>
<th>Commesse</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-12 col-lg-6">
<div class="card radius-10">
<div class="card-header">
<h6 class="mb-0">Dettaglio per utente</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table id="tblUtenti" class="table table-striped table-bordered table-sm w-100">
<thead>
<tr>
<th>ID Utente</th>
<th>Utente</th>
<th>Campioni</th>
<th>Commesse</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
let chartClienti = null;
let chartUtenti = null;
let dtClienti = null;
let dtUtenti = null;
const MESI_IT = ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno',
'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'
];
function currentMonthStr() {
const d = new Date();
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
return `${y}-${m}`;
}
function monthLabelFromStr(monthStr) {
const parts = (monthStr || '').split('-');
if (parts.length !== 2) return '—';
const y = parseInt(parts[0], 10);
const m = parseInt(parts[1], 10);
if (isNaN(y) || isNaN(m) || m < 1 || m > 12) return '—';
return `${MESI_IT[m - 1]} ${y}`;
}
function initTables() {
dtClienti = $('#tblClienti').DataTable({
paging: false,
searching: false,
info: false,
order: [
[2, 'desc']
],
columns: [{
data: 'idclient'
},
{
data: 'clientname',
defaultContent: '—'
},
{
data: 'parts',
className: 'text-end'
},
{
data: 'tot',
className: 'text-end'
}
],
language: {
emptyTable: 'Nessun export in questo mese'
}
});
dtUtenti = $('#tblUtenti').DataTable({
paging: false,
searching: false,
info: false,
order: [
[2, 'desc']
],
columns: [{
data: 'user_id'
},
{
data: 'username',
defaultContent: '—'
},
{
data: 'parts',
className: 'text-end'
},
{
data: 'tot',
className: 'text-end'
}
],
language: {
emptyTable: 'Nessun export in questo mese'
}
});
}
function renderBarChart(existing, canvasId, labels, values, label) {
if (existing) existing.destroy();
const ctx = document.getElementById(canvasId).getContext('2d');
return new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: label,
data: values,
borderWidth: 1
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
beginAtZero: true,
ticks: {
precision: 0
}
}
}
}
});
}
const CHART_TOP_N = 15;
function topN(arr) {
return arr.slice(0, CHART_TOP_N);
}
function loadStats() {
const month = $('#filterMonth').val() || currentMonthStr();
$('#monthLabel').text(monthLabelFromStr(month));
$.getJSON('stats_export_lims_monthly_data.php', {
month: month
}, function(res) {
if (!res || !res.success) {
Swal.fire('Errore', (res && res.message) || 'Errore nel caricamento', 'error');
return;
}
// KPI mese — dato principale = campioni/parti, sotto le commesse
$('#kpiParts').text(res.totals.parts ?? 0);
$('#kpiExport').text(res.totals.export ?? 0);
$('#kpiClienti').text(res.byClient.length);
$('#kpiLimsSamples').text(res.limsSamples ?? 0);
const limsTot = parseInt(res.limsSamples ?? 0, 10);
const partsTot = parseInt(res.totals.parts ?? 0, 10);
if (limsTot > 0) {
const perc = (partsTot / limsTot) * 100;
$('#kpiPerc').text(perc.toFixed(1) + '%');
} else {
$('#kpiPerc').text('—');
}
// Box annuale — campioni + commesse
$('#kpiPartsYear').text(res.totalsYear ? (res.totalsYear.parts ?? 0) : 0);
$('#kpiExportYear').text(res.totalsYear ? (res.totalsYear.export ?? 0) : 0);
$('#yearLabel').text(res.year);
// Grafici — valori = campioni/parti
const clientTop = topN(res.byClient);
const userTop = topN(res.byUser);
chartClienti = renderBarChart(chartClienti, 'chartClienti',
clientTop.map(r => r.clientname ? (r.clientname + ' (' + r.idclient + ')') : ('ID ' + r.idclient)),
clientTop.map(r => r.parts), 'Campioni');
chartUtenti = renderBarChart(chartUtenti, 'chartUtenti',
userTop.map(r => r.username ? (r.username + ' (' + r.user_id + ')') : ('ID ' + r.user_id)),
userTop.map(r => r.parts), 'Campioni');
// Tabelle
dtClienti.clear().rows.add(res.byClient).draw();
dtUtenti.clear().rows.add(res.byUser).draw();
}).fail(function(xhr) {
Swal.fire('Errore', 'Richiesta fallita: ' + xhr.status, 'error');
});
}
$(document).ready(function() {
$('#filterMonth').val(currentMonthStr());
initTables();
loadStats();
$('#btnRefresh').on('click', loadStats);
$('#filterMonth').on('change', loadStats);
$('#btnCurrentMonth').on('click', function() {
$('#filterMonth').val(currentMonthStr());
loadStats();
});
$('#btnPrevMonth').on('click', function() {
const cur = $('#filterMonth').val() || currentMonthStr();
const parts = cur.split('-');
let y = parseInt(parts[0], 10);
let m = parseInt(parts[1], 10);
m -= 1;
if (m < 1) {
m = 12;
y -= 1;
}
$('#filterMonth').val(`${y}-${String(m).padStart(2, '0')}`);
loadStats();
});
});
</script>
</body>
</html>
@@ -1,137 +0,0 @@
<?php
include('include/headscript.php');
header('Content-Type: application/json');
$dbHandler = DBHandlerSelect::getInstance();
$pdo = $dbHandler->getConnection();
try {
// Mese richiesto (default mese corrente). Formato atteso: Y-m
$month = $_GET['month'] ?? date('Y-m');
$dm = DateTime::createFromFormat('Y-m', $month);
if (!$dm || $dm->format('Y-m') !== $month) {
throw new Exception('Mese non valido');
}
$year = $dm->format('Y');
// --- Export per cliente (status = 'l', filtro mese) ---
// tot = numero commesse, parts = numero parti/campioni
$stmtC = $pdo->prepare("
SELECT d.idclient AS idclient,
COUNT(DISTINCT d.iddatadb) AS tot,
COUNT(ip.id) AS parts
FROM datadb d
LEFT JOIN identification_parts ip ON ip.iddatadb = d.iddatadb
WHERE d.status = 'l'
AND DATE_FORMAT(d.export_date, '%Y-%m') = :m
GROUP BY d.idclient
ORDER BY parts DESC
");
$stmtC->execute(['m' => $month]);
$byClient = $stmtC->fetchAll(PDO::FETCH_ASSOC);
// --- Risolvi Nominativo cliente dalla cache locale ---
$clientNames = [];
$cacheFile = __DIR__ . '/cache/clienti.json';
if (is_file($cacheFile)) {
$cache = json_decode(file_get_contents($cacheFile), true);
foreach (($cache['value'] ?? []) as $c) {
$clientNames[(int)($c['IdCliente'] ?? 0)] = trim($c['Nominativo'] ?? '');
}
}
// --- Export per utente (status = 'l', filtro mese) ---
// tot = numero commesse, parts = numero parti/campioni
$stmtU = $pdo->prepare("
SELECT d.user_id AS user_id,
TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS username,
COUNT(DISTINCT d.iddatadb) AS tot,
COUNT(ip.id) AS parts
FROM datadb d
LEFT JOIN auth_users u ON u.id = d.user_id
LEFT JOIN identification_parts ip ON ip.iddatadb = d.iddatadb
WHERE d.status = 'l'
AND DATE_FORMAT(d.export_date, '%Y-%m') = :m
GROUP BY d.user_id, username
ORDER BY parts DESC
");
$stmtU->execute(['m' => $month]);
$byUser = $stmtU->fetchAll(PDO::FETCH_ASSOC);
// Normalizza tipi
foreach ($byClient as &$r) {
$r['idclient'] = $r['idclient'] !== null ? (int)$r['idclient'] : null;
$r['tot'] = (int)$r['tot'];
$r['parts'] = (int)$r['parts'];
$nm = $clientNames[$r['idclient']] ?? '';
$r['clientname'] = $nm !== '' ? $nm : null;
}
unset($r);
foreach ($byUser as &$r) {
$r['user_id'] = $r['user_id'] !== null ? (int)$r['user_id'] : null;
$r['tot'] = (int)$r['tot'];
$r['parts'] = (int)$r['parts'];
if (isset($r['username'])) $r['username'] = $r['username'] !== null && $r['username'] !== '' ? $r['username'] : null;
}
unset($r);
// --- Totale COMMESSE del mese ---
$totExport = array_sum(array_column($byClient, 'tot'));
// --- Totale PARTI/CAMPIONI del mese (status = 'l') ---
$stmtP = $pdo->prepare("
SELECT COUNT(*)
FROM identification_parts ip
INNER JOIN datadb d ON d.iddatadb = ip.iddatadb
WHERE d.status = 'l'
AND DATE_FORMAT(d.export_date, '%Y-%m') = :m
");
$stmtP->execute(['m' => $month]);
$totParts = (int)$stmtP->fetchColumn();
// --- Totale COMMESSE dell'ANNO del mese selezionato ---
$stmtY = $pdo->prepare("
SELECT COUNT(*)
FROM datadb d
WHERE d.status = 'l'
AND YEAR(d.export_date) = :y
");
$stmtY->execute(['y' => $year]);
$totExportYear = (int)$stmtY->fetchColumn();
// --- Totale PARTI dell'ANNO del mese selezionato ---
$stmtYP = $pdo->prepare("
SELECT COUNT(*)
FROM identification_parts ip
INNER JOIN datadb d ON d.iddatadb = ip.iddatadb
WHERE d.status = 'l'
AND YEAR(d.export_date) = :y
");
$stmtYP->execute(['y' => $year]);
$totPartsYear = (int)$stmtYP->fetchColumn();
// --- Campioni LIMS: somma dei total_samples di tutti i giorni del mese ---
$stmtL = $pdo->prepare("
SELECT COALESCE(SUM(total_samples), 0)
FROM lims_daily_samples
WHERE DATE_FORMAT(sample_date, '%Y-%m') = :m
");
$stmtL->execute(['m' => $month]);
$limsSamples = (int)$stmtL->fetchColumn();
echo json_encode([
'success' => true,
'month' => $month,
'year' => (int)$year,
'totals' => ['export' => (int)$totExport, 'parts' => $totParts],
'totalsYear' => ['export' => $totExportYear, 'parts' => $totPartsYear],
'byClient' => $byClient,
'byUser' => $byUser,
'limsSamples' => $limsSamples,
]);
} catch (Exception $e) {
error_log('Stats export LIMS monthly error: ' . $e->getMessage());
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -1,36 +0,0 @@
<?php
include('include/headscript.php');
header('Content-Type: application/json');
// Usa la connessione in scrittura se disponibile nel tuo progetto;
// qui riuso il pattern del resto dell'app.
$dbHandler = DBHandlerSelect::getInstance();
$pdo = $dbHandler->getConnection();
try {
$date = $_POST['date'] ?? null;
$total = $_POST['total_samples'] ?? null;
$d = $date ? DateTime::createFromFormat('Y-m-d', $date) : null;
if (!$d || $d->format('Y-m-d') !== $date) {
throw new Exception('Data non valida');
}
if (!is_numeric($total) || (int)$total < 0) {
throw new Exception('Valore campioni non valido');
}
$total = (int)$total;
// Upsert: un solo record per giornata (unique su sample_date)
$stmt = $pdo->prepare("
INSERT INTO lims_daily_samples (sample_date, total_samples)
VALUES (:d, :t)
ON DUPLICATE KEY UPDATE total_samples = VALUES(total_samples)
");
$stmt->execute(['d' => $date, 't' => $total]);
echo json_encode(['success' => true, 'date' => $date, 'total_samples' => $total]);
} catch (Exception $e) {
error_log('Save LIMS samples error: ' . $e->getMessage());
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+298 -192
View File
@@ -78,6 +78,51 @@
color: #198754;
}
.badge-source-pdf {
background-color: #fff3cd;
color: #b58100;
}
.type-filter-bar {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 12px;
}
.type-filter-btn {
border: 0;
border-radius: 999px;
padding: 7px 14px;
font-size: 13px;
font-weight: 700;
color: #fff;
opacity: 0.35;
transition: all 0.15s ease-in-out;
}
.type-filter-btn.active {
opacity: 1;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
}
.type-filter-btn[data-type="XLS"] {
background-color: #0d6efd;
}
.type-filter-btn[data-type="API"] {
background-color: #198754;
}
.type-filter-btn[data-type="PDF"] {
background-color: #b58100;
}
.type-filter-btn:hover {
transform: translateY(-1px);
}
#xlsTemplatesTable {
font-size: 13px;
}
@@ -136,79 +181,92 @@
</div>
<div class="card-body">
<div class="type-filter-bar">
<span class="text-muted fw-semibold me-1">Filter by type:</span>
<button type="button" class="type-filter-btn active" data-type="XLS">
XLS
</button>
<button type="button" class="type-filter-btn active" data-type="API">
JSON/API
</button>
<button type="button" class="type-filter-btn active" data-type="PDF">
PDF
</button>
</div>
<div class="table-responsive">
<table id="xlsTemplatesTable" class="table table-striped table-bordered table-sm w-100">
<table id="xlsTemplatesTable" class="table table-striped table-bordered align-middle w-100">
<thead>
<tr>
<th><?= htmlspecialchars($action, ENT_QUOTES, 'UTF-8'); ?></th>
<th><?= htmlspecialchars($nametemplate, ENT_QUOTES, 'UTF-8'); ?></th>
<th>Actions</th>
<th>Template Name</th>
<th>Type</th>
<th>Row</th>
<th>Col</th>
<th><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></th>
<th>Description</th>
<th>Client</th>
<th>Button</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<!-- DataTables will populate this section automatically -->
</tbody>
<tbody></tbody>
</table>
</div>
</div>
</div>
<!--end page wrapper -->
<!--start overlay-->
<div class="overlay toggle-icon"></div>
<!--end overlay-->
<!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<!--End Back To Top Button-->
<?php include('include/footer.php'); ?>
</div>
</div>
<!--end page wrapper -->
<!--end wrapper-->
<!--start overlay-->
<div class="overlay toggle-icon"></div>
<!--end overlay-->
<?php include('jsinclude.php'); ?>
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<!--End Back To Top Button-->
<script>
$(document).ready(function() {
<?php include('include/footer.php'); ?>
</div>
<!--end wrapper-->
const urlParams = new URLSearchParams(window.location.search);
<?php include('jsinclude.php'); ?>
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
if (urlParams.get('cloned') === '1') {
Swal.fire({
title: "Template cloned",
text: "The template was cloned successfully.",
icon: "success",
confirmButtonText: "OK"
});
<script>
$(document).ready(function() {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cloned') === '1') {
Swal.fire({
title: "Template cloned",
text: "The template was cloned successfully.",
icon: "success",
confirmButtonText: "OK"
});
const cleanUrl = window.location.pathname;
window.history.replaceState({}, document.title, cleanUrl);
}
$('#xlsTemplatesTable').DataTable({
processing: true,
serverSide: false,
ajax: 'load_templates.php',
pageLength: 50,
autoWidth: false,
columns: [{
data: 'id',
orderable: false,
searchable: false,
title: "Actions",
className: "table-actions text-center",
render: function(data, type, row) {
return `
const cleanUrl = window.location.pathname;
window.history.replaceState({}, document.title, cleanUrl);
}
const templatesTable = $('#xlsTemplatesTable').DataTable({
processing: true,
serverSide: false,
ajax: 'load_templates.php',
pageLength: 50,
autoWidth: false,
columns: [{
data: 'id',
orderable: false,
searchable: false,
title: "Actions",
className: "table-actions text-center",
render: function(data, type, row) {
return `
<div class="d-flex justify-content-center gap-1">
<a href="edit_template_xls.php?id=${data}" class="btn btn-sm btn-primary" title="Edit">
<i class="bx bx-edit-alt"></i>
@@ -227,121 +285,169 @@
</button>
</div>
`;
}
},
{
data: 'name',
title: "Template Name",
className: "name-cell"
},
{
data: 'source_type',
title: "Type",
className: "text-center",
render: function(data, type, row) {
const sourceType = (data || 'XLS').toUpperCase();
if (type === 'display') {
if (sourceType === 'API') {
return '<span class="badge-source badge-source-api">API</span>';
}
return '<span class="badge-source badge-source-xls">XLS</span>';
}
},
{
data: 'name',
title: "Template Name",
className: "name-cell"
},
{
data: 'source_type',
title: "Type",
className: "text-center",
render: function(data, type, row) {
let sourceType = (data || 'XLS').toUpperCase();
return sourceType;
}
},
{
data: 'header_row',
title: "Row",
className: "text-center",
defaultContent: ''
},
{
data: 'start_column',
title: "Col",
className: "text-center",
defaultContent: ''
},
{
data: 'description',
title: "Description",
className: "description-cell",
defaultContent: 'No description'
},
{
data: null,
title: "Client",
className: "client-cell",
render: function(data, type, row) {
const clientName = row.clientname || "No client";
const clientId = row.idclient || "N/A";
return `${clientName} <small class="text-muted">(ID: ${clientId})</small>`;
}
},
{
data: 'button_label',
title: "Button",
className: "button-cell",
defaultContent: 'Click Me'
},
{
data: 'status',
title: "Status",
orderable: false,
searchable: false,
className: "text-center",
render: function(status, type, row) {
let checked = (status === "active") ? "checked" : "";
return `
// Treat JSON as API group for dashboard filter
if (sourceType === 'JSON') {
sourceType = 'API';
}
if (type === 'display') {
if (sourceType === 'API') {
return '<span class="badge-source badge-source-api">JSON/API</span>';
}
if (sourceType === 'PDF') {
return '<span class="badge-source badge-source-pdf">PDF</span>';
}
return '<span class="badge-source badge-source-xls">XLS</span>';
}
return sourceType;
}
},
{
data: 'header_row',
title: "Row",
className: "text-center",
defaultContent: ''
},
{
data: 'start_column',
title: "Col",
className: "text-center",
defaultContent: ''
},
{
data: 'description',
title: "Description",
className: "description-cell",
defaultContent: 'No description'
},
{
data: null,
title: "Client",
className: "client-cell",
render: function(data, type, row) {
const clientName = row.clientname || "No client";
const clientId = row.idclient || "N/A";
return `${clientName} <small class="text-muted">(ID: ${clientId})</small>`;
}
},
{
data: 'button_label',
title: "Button",
className: "button-cell",
defaultContent: 'Click Me'
},
{
data: 'status',
title: "Status",
orderable: false,
searchable: false,
className: "text-center",
render: function(status, type, row) {
let checked = (status === "active") ? "checked" : "";
return `
<label class="switch">
<input type="checkbox" class="toggle-status" data-id="${row.id}" ${checked}>
<span class="slider round"></span>
</label>
`;
}
}
],
dom: '<"card-header border-bottom p-3"<"d-flex align-items-center"<"card-title mb-0 flex-grow-1"f>>>rt<"card-footer border-top p-3"<"d-flex align-items-center"<"me-auto"l><"d-flex gap-2"ip>>>',
lengthMenu: [10, 25, 50, 100],
order: [
[1, 'asc']
],
language: {
search: "Cerca:",
lengthMenu: "Mostra _MENU_ elementi",
info: "Visualizzando da _START_ a _END_ di _TOTAL_ elementi",
paginate: {
first: "<?= isset($langdatatables['paginate_first']) ? $langdatatables['paginate_first'] : 'Primo' ?>",
last: "<?= isset($langdatatables['paginate_last']) ? $langdatatables['paginate_last'] : 'Ultimo' ?>",
next: "<?= isset($langdatatables['paginate_next']) ? $langdatatables['paginate_next'] : 'Successivo' ?>",
previous: "<?= isset($langdatatables['paginate_previous']) ? $langdatatables['paginate_previous'] : 'Precedente' ?>"
}
}
}
],
dom: '<"card-header border-bottom p-3"<"d-flex align-items-center"<"card-title mb-0 flex-grow-1"f>>>rt<"card-footer border-top p-3"<"d-flex align-items-center"<"me-auto"l><"d-flex gap-2"ip>>>',
lengthMenu: [10, 25, 50, 100],
order: [
[1, 'asc']
],
language: {
search: "Cerca:",
lengthMenu: "Mostra _MENU_ elementi",
info: "Visualizzando da _START_ a _END_ di _TOTAL_ elementi",
paginate: {
first: "<?= isset($langdatatables['paginate_first']) ? $langdatatables['paginate_first'] : 'Primo' ?>",
last: "<?= isset($langdatatables['paginate_last']) ? $langdatatables['paginate_last'] : 'Ultimo' ?>",
next: "<?= isset($langdatatables['paginate_next']) ? $langdatatables['paginate_next'] : 'Successivo' ?>",
previous: "<?= isset($langdatatables['paginate_previous']) ? $langdatatables['paginate_previous'] : 'Precedente' ?>"
}
}
});
});
});
const activeSourceTypes = {
XLS: true,
API: true,
PDF: true
};
function confirmDelete(id) {
Swal.fire({
title: "Are you sure?",
text: "This action cannot be undone!",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#d33",
cancelButtonColor: "#3085d6",
confirmButtonText: "Yes, delete it!",
cancelButtonText: "Cancel"
}).then((result) => {
if (result.isConfirmed) {
window.location.href = `delete_template_xls.php?id=${id}`;
}
});
}
$.fn.dataTable.ext.search.push(function(settings, data, dataIndex) {
if (settings.nTable.id !== 'xlsTemplatesTable') {
return true;
}
function confirmClone(id, templateName) {
Swal.fire({
title: "Clone template?",
html: `
const api = new $.fn.dataTable.Api(settings);
const rowData = api.row(dataIndex).data();
let sourceType = ((rowData && rowData.source_type) ? rowData.source_type : 'XLS').toUpperCase();
if (sourceType === 'JSON') {
sourceType = 'API';
}
return activeSourceTypes[sourceType] === true;
});
$('.type-filter-btn').on('click', function() {
const type = $(this).data('type');
activeSourceTypes[type] = !activeSourceTypes[type];
$(this).toggleClass('active', activeSourceTypes[type]);
const hasAtLeastOneActive = Object.values(activeSourceTypes).some(Boolean);
if (!hasAtLeastOneActive) {
activeSourceTypes[type] = true;
$(this).addClass('active');
}
$('#xlsTemplatesTable').DataTable().draw();
});
});
function confirmDelete(id) {
Swal.fire({
title: "Are you sure?",
text: "This action cannot be undone!",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#d33",
cancelButtonColor: "#3085d6",
confirmButtonText: "Yes, delete it!",
cancelButtonText: "Cancel"
}).then((result) => {
if (result.isConfirmed) {
window.location.href = `delete_template_xls.php?id=${id}`;
}
});
}
function confirmClone(id, templateName) {
Swal.fire({
title: "Clone template?",
html: `
<div class="text-start">
<p class="mb-2">You are about to clone this template:</p>
<strong>${templateName}</strong>
@@ -350,45 +456,45 @@
</p>
</div>
`,
icon: "question",
showCancelButton: true,
confirmButtonColor: "#ffc107",
cancelButtonColor: "#6c757d",
confirmButtonText: "Yes, clone it",
cancelButtonText: "Cancel"
}).then((result) => {
if (result.isConfirmed) {
window.location.href = `clone_template.php?id=${id}`;
icon: "question",
showCancelButton: true,
confirmButtonColor: "#ffc107",
cancelButtonColor: "#6c757d",
confirmButtonText: "Yes, clone it",
cancelButtonText: "Cancel"
}).then((result) => {
if (result.isConfirmed) {
window.location.href = `clone_template.php?id=${id}`;
}
});
}
});
}
$(document).on("change", ".toggle-status", function() {
let templateId = $(this).data("id");
let newStatus = $(this).is(":checked") ? "active" : "inactive";
$(document).on("change", ".toggle-status", function() {
let templateId = $(this).data("id");
let newStatus = $(this).is(":checked") ? "active" : "inactive";
$.ajax({
url: "update_template_status.php",
type: "POST",
dataType: "json",
data: {
id: templateId,
status: newStatus
},
success: function(response) {
if (response.success) {
console.log("Status updated successfully.");
} else {
console.error("Error updating status:", response.message);
alert("Error updating status: " + response.message);
}
},
error: function(xhr) {
console.error("AJAX error:", xhr.responseText);
}
});
});
</script>
$.ajax({
url: "update_template_status.php",
type: "POST",
dataType: "json",
data: {
id: templateId,
status: newStatus
},
success: function(response) {
if (response.success) {
console.log("Status updated successfully.");
} else {
console.error("Error updating status:", response.message);
alert("Error updating status: " + response.message);
}
},
error: function(xhr) {
console.error("AJAX error:", xhr.responseText);
}
});
});
</script>
</body>
@@ -1,53 +0,0 @@
<?php
header('Content-Type: application/json');
include('include/headscript.php');
require_once 'class/db-functions.php';
$response = ["success" => false, "message" => ""];
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
if (!$pdo) {
throw new Exception('Database connection failed.');
}
$userId = $iduserlogin ?? 0;
if (!$userId) {
throw new Exception('User not authenticated.');
}
$data = json_decode(file_get_contents('php://input'), true);
$templateId = isset($data['template_id']) ? (int)$data['template_id'] : 0;
$isFavorite = !empty($data['is_favorite']);
if ($templateId <= 0) {
throw new Exception('Invalid template_id.');
}
if ($isFavorite) {
// Aggiungi ai preferiti (ignora se già presente)
$stmt = $pdo->prepare("
INSERT INTO user_template_favorites (user_id, template_id)
VALUES (:user_id, :template_id)
ON DUPLICATE KEY UPDATE id = id
");
$stmt->execute(['user_id' => $userId, 'template_id' => $templateId]);
} else {
// Rimuovi dai preferiti
$stmt = $pdo->prepare("
DELETE FROM user_template_favorites
WHERE user_id = :user_id AND template_id = :template_id
");
$stmt->execute(['user_id' => $userId, 'template_id' => $templateId]);
}
$response["success"] = true;
} catch (PDOException $e) {
$response["message"] = "Database error: " . $e->getMessage();
} catch (Exception $e) {
$response["message"] = "Error: " . $e->getMessage();
}
echo json_encode($response);
+77 -26
View File
@@ -3,33 +3,84 @@ header('Content-Type: application/json');
require_once(__DIR__ . '/include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$input = json_decode(file_get_contents('php://input'), true);
$input = json_decode(file_get_contents('php://input'), true);
$id = (int)($input['id'] ?? 0);
$field = (string)($input['field'] ?? '');
$value = $input['value'] ?? null;
$id = (int)($input['id'] ?? 0);
$field = (string)($input['field'] ?? '');
$value = $input['value'] ?? null;
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid id']);
exit;
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid id']);
exit;
}
$allowed = [
'default_value',
'default_source',
'json_node',
'is_visible_import',
'is_required'
];
if (!in_array($field, $allowed, true)) {
echo json_encode(['success' => false, 'message' => 'Invalid field: ' . $field]);
exit;
}
if ($field === 'is_visible_import' || $field === 'is_required') {
$value = ((int)$value === 1) ? 1 : 0;
}
if ($field === 'default_source') {
$value = (string)$value;
if (!in_array($value, ['manual', 'json'], true)) {
echo json_encode(['success' => false, 'message' => 'Invalid default_source']);
exit;
}
// If the user goes back to manual, clear the JSON node
if ($value === 'manual') {
$sql = "
UPDATE template_fixed_mapping
SET default_source = :val, json_node = NULL
WHERE id = :id
";
$stmt = $pdo->prepare($sql);
$ok = $stmt->execute([
':val' => $value,
':id' => $id
]);
echo json_encode(['success' => (bool)$ok]);
exit;
}
}
if ($field === 'json_node') {
$value = trim((string)$value);
if ($value === '') {
$value = null;
}
}
$sql = "UPDATE template_fixed_mapping SET {$field} = :val WHERE id = :id";
$stmt = $pdo->prepare($sql);
$ok = $stmt->execute([
':val' => $value,
':id' => $id
]);
echo json_encode(['success' => (bool)$ok]);
} catch (Throwable $e) {
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
$allowed = ['default_value', 'is_visible_import', 'is_required'];
if (!in_array($field, $allowed, true)) {
echo json_encode(['success' => false, 'message' => 'Invalid field']);
exit;
}
if ($field === 'is_visible_import' || $field === 'is_required') {
$value = ((int)$value === 1) ? 1 : 0;
}
$sql = "UPDATE template_fixed_mapping SET {$field} = :val WHERE id = :id";
$stmt = $pdo->prepare($sql);
$ok = $stmt->execute([':val' => $value, ':id' => $id]);
echo json_encode(['success' => (bool)$ok]);
-108
View File
@@ -1,108 +0,0 @@
<?php
header('Content-Type: application/json');
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once(__DIR__ . '/class/db-functions.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$data = json_decode(file_get_contents("php://input"), true);
if (!$data || !isset($data['id'], $data['field'])) {
echo json_encode(["success" => false, "message" => "Invalid or missing parameters"]);
exit;
}
$mappingId = (int)$data['id'];
$field = $data['field'];
$value = isset($data['value']) && $data['value'] ? 1 : 0;
// Whitelist rigorosa dei campi aggiornabili: evita SQL injection sul nome colonna
$allowedFields = ['main_field', 'is_visible_import', 'is_visible_parts', 'is_addnote'];
if (!in_array($field, $allowedFields, true)) {
echo json_encode(["success" => false, "message" => "Invalid field"]);
exit;
}
try {
// Recupero il template_id della riga per poter applicare i vincoli a livello di template
$stmt = $pdo->prepare("SELECT template_id FROM template_mapping WHERE id = ?");
$stmt->execute([$mappingId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
echo json_encode(["success" => false, "message" => "Mapping not found"]);
exit;
}
$templateId = (int)$row['template_id'];
// Vincolo: max 2 righe con main_field = 1 per template
if ($field === 'main_field' && $value === 1) {
$stmt = $pdo->prepare("
SELECT COUNT(*)
FROM template_mapping
WHERE template_id = ? AND main_field = '1' AND id != ?
");
$stmt->execute([$templateId, $mappingId]);
$count = (int)$stmt->fetchColumn();
if ($count >= 2) {
echo json_encode([
"success" => false,
"limit_reached" => true,
"message" => "Massimo 2 campi Main consentiti per questo template"
]);
exit;
}
}
$pdo->beginTransaction();
$uncheckedOthers = false;
// Vincolo: solo 1 riga con is_visible_parts = 1 per template (comportamento radio)
if ($field === 'is_visible_parts' && $value === 1) {
$stmt = $pdo->prepare("
UPDATE template_mapping
SET is_visible_parts = 0
WHERE template_id = ? AND id != ? AND is_visible_parts = 1
");
$stmt->execute([$templateId, $mappingId]);
$uncheckedOthers = $stmt->rowCount() > 0;
}
$bindValue = ($field === 'main_field') ? (string)$value : $value;
$stmt = $pdo->prepare("UPDATE template_mapping SET {$field} = ? WHERE id = ?");
$result = $stmt->execute([$bindValue, $mappingId]);
if (!$result) {
$pdo->rollBack();
echo json_encode(["success" => false, "message" => "Database update failed"]);
exit;
}
$pdo->commit();
echo json_encode([
"success" => true,
"message" => "Flag updated successfully",
"unchecked_others" => $uncheckedOthers,
"saved" => [
"id" => $mappingId,
"field" => $field,
"value" => $value
]
]);
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
echo json_encode(["success" => false, "message" => "Error: " . $e->getMessage()]);
}
exit;
+17 -18
View File
@@ -135,26 +135,25 @@ try {
// Update existing field AND clear user-entered value if type changed
$updateChangedTypeStmt = $pdo->prepare("
UPDATE template_mapping
SET
schema_id = :schema_id,
field_order = :field_order,
data_type = :data_type,
is_required = :is_required,
default_value = :default_value,
has_list = :has_list,
length = :length,
decimals = :decimals,
min_value = :min_value,
max_value = :max_value,
default_curr_date = :default_curr_date,
tablename = :tablename,
field_label = :field_label,
UPDATE template_mapping
SET
schema_id = :schema_id,
data_type = :data_type,
is_required = :is_required,
default_value = :default_value,
has_list = :has_list,
length = :length,
decimals = :decimals,
min_value = :min_value,
max_value = :max_value,
default_curr_date = :default_curr_date,
tablename = :tablename,
field_label = :field_label,
manual_default = NULL,
auto_value = 'none'
manual_default = NULL,
auto_value = 'none'
WHERE id = :id
WHERE id = :id
");
$currentFieldIds = [];
File diff suppressed because it is too large Load Diff
+40 -621
View File
@@ -8,71 +8,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--favicon-->
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<!-- Select2 CSS -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<title>SmartTRF - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> - User Profile</title>
<style>
.profile-avatar-preview {
max-width: 100px;
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 50%;
border: 1px solid #e5e7eb;
background: #f8f9fa;
}
.small-muted {
font-size: 12px;
color: #6c757d;
}
.select2-container {
width: 100% !important;
}
.lims-loading {
font-size: 12px;
color: #6c757d;
}
.password-note {
background: #f8fafc;
border: 1px solid #e5e7eb;
color: #475569;
border-radius: 10px;
padding: 10px 12px;
font-size: 13px;
line-height: 1.5;
}
.password-error {
display: none;
background: #fff1f2;
border: 1px solid #fecdd3;
color: #be123c;
border-radius: 10px;
padding: 10px 12px;
font-size: 13px;
line-height: 1.5;
}
.password-success {
display: none;
background: #f0fdf4;
border: 1px solid #bbf7d0;
color: #166534;
border-radius: 10px;
padding: 10px 12px;
font-size: 13px;
line-height: 1.5;
}
</style>
<title>xxx - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> - User Profile</title>
</head>
<body>
@@ -81,11 +18,9 @@
<!--sidebar wrapper -->
<?php include('include/navbar.php'); ?>
<!--end sidebar wrapper -->
<!--start header -->
<?php include('include/topbar.php'); ?>
<!--end header -->
<!--start page wrapper -->
<div class="page-wrapper">
<div class="page-content">
@@ -96,195 +31,61 @@
<div class="d-flex align-items-center">
<div>
<h6 class="mb-0">Edit Profile</h6>
<small class="text-muted">Update your personal information and LIMS references.</small>
</div>
</div>
</div>
<div class="card-body">
<form action="update-profile.php" method="POST" enctype="multipart/form-data" id="profileForm">
<div class="row g-3">
<div class="col-md-6">
<label for="first_name" class="form-label">First Name</label>
<input
type="text"
class="form-control"
id="first_name"
name="first_name"
value="<?= htmlspecialchars($nameuser ?? '', ENT_QUOTES, 'UTF-8'); ?>"
required>
</div>
<div class="col-md-6">
<label for="last_name" class="form-label">Last Name</label>
<input
type="text"
class="form-control"
id="last_name"
name="last_name"
value="<?= htmlspecialchars($surnameuser ?? '', ENT_QUOTES, 'UTF-8'); ?>"
required>
</div>
<div class="col-md-12">
<label for="email" class="form-label">Email</label>
<input
type="email"
class="form-control"
id="email"
name="email"
value="<?= htmlspecialchars($emailuser ?? '', ENT_QUOTES, 'UTF-8'); ?>"
required>
</div>
<div class="col-md-6">
<label for="lims_user_id" class="form-label">Accettatore</label>
<select
class="form-select"
id="lims_user_id"
name="lims_user_id"
data-current-value="<?= htmlspecialchars($lims_user_id ?? '', ENT_QUOTES, 'UTF-8'); ?>">
<option value="">Select acceptor</option>
</select>
<div class="small-muted mt-1">
Values loaded from CustomField ID 244.
</div>
</div>
<div class="col-md-6">
<label for="lims_global_user_id" class="form-label">LIMS Global</label>
<select
class="form-select"
id="lims_global_user_id"
name="lims_global_user_id"
data-current-value="<?= htmlspecialchars($lims_global_user_id ?? '', ENT_QUOTES, 'UTF-8'); ?>">
<option value="">Select LIMS user</option>
</select>
<div class="small-muted mt-1">
Values loaded from LIMS users list.
</div>
</div>
<div class="col-md-12">
<div class="lims-loading" id="limsLoadingMessage">
Loading LIMS users and acceptors...
</div>
</div>
<div class="col-md-12">
<label for="avatar" class="form-label">Profile Picture</label>
<input
type="file"
class="form-control"
id="avatar"
name="avatar"
accept="image/jpeg,image/png,image/webp,image/gif">
<div class="mt-3 d-flex align-items-center gap-3">
<?php if (!empty($photousername)): ?>
<img
src="../upload/users/<?= htmlspecialchars($photousername, ENT_QUOTES, 'UTF-8'); ?>"
alt="Current Avatar"
class="profile-avatar-preview"
id="avatarPreview">
<?php else: ?>
<img
src="../upload/users/profile.png"
alt="Default Avatar"
class="profile-avatar-preview"
id="avatarPreview">
<?php endif; ?>
<div>
<div class="small-muted">Current avatar</div>
<div class="small-muted">Allowed formats: JPG, PNG, WEBP, GIF.</div>
</div>
</div>
</div>
<div class="col-md-6">
<label for="password" class="form-label">New Password</label>
<div class="input-group">
<input
type="password"
class="form-control"
id="password"
name="password"
autocomplete="new-password">
<button
class="btn btn-outline-secondary toggle-password"
type="button"
data-target="password"
aria-label="Show or hide password">
<i class="bx bx-show"></i>
</button>
</div>
</div>
<div class="col-md-6">
<label for="password_confirm" class="form-label">Confirm New Password</label>
<div class="input-group">
<input
type="password"
class="form-control"
id="password_confirm"
name="password_confirm"
autocomplete="new-password">
<button
class="btn btn-outline-secondary toggle-password"
type="button"
data-target="password_confirm"
aria-label="Show or hide password confirmation">
<i class="bx bx-show"></i>
</button>
</div>
</div>
<div class="col-md-12">
<div class="password-note" id="passwordNote">
<strong>Important:</strong> leave both password fields empty to keep your current password unchanged.
Type a new password only if you want to replace it.
</div>
<div class="password-error mt-2" id="passwordError">
The two password fields do not match. Please check them before saving.
</div>
<div class="password-success mt-2" id="passwordSuccess">
The two passwords match.
</div>
</div>
<input
type="hidden"
name="iduserlogin"
value="<?= htmlspecialchars($iduserlogin ?? '', ENT_QUOTES, 'UTF-8'); ?>">
<div class="col-md-12">
<button type="submit" class="btn btn-primary" id="submitProfileButton">
Update Profile
</button>
</div>
<form action="update-profile.php" method="POST" enctype="multipart/form-data">
<div class="mb-3">
<label for="first_name" class="form-label">First Name</label>
<input type="text" class="form-control" id="first_name" name="first_name" value="<?= htmlspecialchars($nameuser); ?>" required>
</div>
<div class="mb-3">
<label for="last_name" class="form-label">Last Name</label>
<input type="text" class="form-control" id="last_name" name="last_name" value="<?= htmlspecialchars($surnameuser); ?>" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" value="<?= htmlspecialchars($emailuser); ?>" required>
</div>
<div class="mb-3">
<label for="lims_user_id" class="form-label">Accettatore</label>
<input type="number" class="form-control" id="lims_user_id" name="lims_user_id" value="<?= htmlspecialchars($lims_user_id ?? ''); ?>">
</div>
<div class="mb-3">
<label for="lims_global_user_id" class="form-label">LIMS Global</label>
<input type="number" class="form-control" id="lims_global_user_id" name="lims_global_user_id" value="<?= htmlspecialchars($lims_global_user_id ?? ''); ?>">
</div>
<div class="mb-3">
<label for="avatar" class="form-label">Profile Picture</label>
<input type="file" class="form-control" id="avatar" name="avatar">
<?php if ($photousername): ?>
<p>Current avatar: <img src="..//upload/users/<?= htmlspecialchars($photousername); ?>" alt="Current Avatar" style="max-width: 100px;"></p>
<?php else: ?>
<p>Current avatar: <img src="..//upload/users/profile.png" alt="Default Avatar" style="max-width: 100px;"></p>
<?php endif; ?>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password (leave blank to keep current)</label>
<input type="password" class="form-control" id="password" name="password">
</div>
<input type="hidden" name="iduserlogin" value="<?= htmlspecialchars($iduserlogin); ?>">
<button type="submit" class="btn btn-primary">Update Profile</button>
</form>
</div>
</div>
</div>
</div>
<!--end page wrapper -->
<!--start overlay-->
<div class="overlay toggle-icon"></div>
<!--end overlay-->
<!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top">
<i class='bx bxs-up-arrow-alt'></i>
</a>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<!--End Back To Top Button-->
<?php include('include/footer.php'); ?>
</div>
<!--end wrapper-->
@@ -298,389 +99,7 @@
<?php //include('include/themeswitcher.php');
?>
<!--end switcher-->
<?php include('jsinclude.php'); ?>
<!-- Select2 JS -->
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const profileForm = document.getElementById('profileForm');
const submitProfileButton = document.getElementById('submitProfileButton');
const passwordInput = document.getElementById('password');
const passwordConfirmInput = document.getElementById('password_confirm');
const passwordError = document.getElementById('passwordError');
const passwordSuccess = document.getElementById('passwordSuccess');
const limsUserSelect = document.getElementById('lims_user_id');
const limsGlobalUserSelect = document.getElementById('lims_global_user_id');
const limsLoadingMessage = document.getElementById('limsLoadingMessage');
const avatarInput = document.getElementById('avatar');
const avatarPreview = document.getElementById('avatarPreview');
const currentLimsUserId = String(limsUserSelect.dataset.currentValue || '');
const currentLimsGlobalUserId = String(limsGlobalUserSelect.dataset.currentValue || '');
const limsGlobalUsersEndpoint = 'get_utenti.php';
const limsAcceptorsEndpoint = 'get_customfield_values.php?field_ids=244';
function validatePasswords(showEmptySuccess = false) {
const password = passwordInput.value;
const confirmPassword = passwordConfirmInput.value;
passwordError.style.display = 'none';
passwordSuccess.style.display = 'none';
passwordInput.classList.remove('is-invalid', 'is-valid');
passwordConfirmInput.classList.remove('is-invalid', 'is-valid');
/*
* Both empty means: keep current password.
*/
if (password === '' && confirmPassword === '') {
submitProfileButton.disabled = false;
return true;
}
/*
* One field filled and the other empty is not valid.
*/
if (password === '' || confirmPassword === '') {
passwordError.textContent = 'Please fill both password fields, or leave both empty to keep the current password.';
passwordError.style.display = 'block';
passwordInput.classList.add('is-invalid');
passwordConfirmInput.classList.add('is-invalid');
submitProfileButton.disabled = true;
return false;
}
/*
* Both filled but different.
*/
if (password !== confirmPassword) {
passwordError.textContent = 'The two password fields do not match. Please check them before saving.';
passwordError.style.display = 'block';
passwordInput.classList.add('is-invalid');
passwordConfirmInput.classList.add('is-invalid');
submitProfileButton.disabled = true;
return false;
}
/*
* Both filled and equal.
*/
passwordInput.classList.add('is-valid');
passwordConfirmInput.classList.add('is-valid');
passwordSuccess.style.display = showEmptySuccess ? 'block' : 'block';
submitProfileButton.disabled = false;
return true;
}
passwordInput.addEventListener('input', function() {
validatePasswords();
});
passwordConfirmInput.addEventListener('input', function() {
validatePasswords();
});
profileForm.addEventListener('submit', function(event) {
if (!validatePasswords(true)) {
event.preventDefault();
if (typeof Swal !== 'undefined') {
Swal.fire({
icon: 'error',
title: 'Password mismatch',
text: 'Please check the password fields before saving.'
});
}
return false;
}
return true;
});
function initSelect2() {
if (typeof $ === 'undefined') {
console.error('jQuery is not loaded. Select2 cannot start.');
return;
}
if (typeof $.fn.select2 === 'undefined') {
console.error('Select2 is not loaded. Check select2.min.js include.');
return;
}
$('#lims_user_id').select2({
width: '100%',
placeholder: 'Select acceptor',
allowClear: true,
minimumResultsForSearch: 0
});
$('#lims_global_user_id').select2({
width: '100%',
placeholder: 'Select LIMS user',
allowClear: true,
minimumResultsForSearch: 0
});
}
function destroySelect2IfActive(selector) {
if (typeof $ !== 'undefined' && $.fn.select2 && $(selector).hasClass('select2-hidden-accessible')) {
$(selector).select2('destroy');
}
}
function refreshSelect2() {
if (typeof $ === 'undefined' || typeof $.fn.select2 === 'undefined') {
return;
}
destroySelect2IfActive('#lims_user_id');
destroySelect2IfActive('#lims_global_user_id');
$('#lims_user_id').select2({
width: '100%',
placeholder: 'Select acceptor',
allowClear: true,
minimumResultsForSearch: 0
});
$('#lims_global_user_id').select2({
width: '100%',
placeholder: 'Select LIMS user',
allowClear: true,
minimumResultsForSearch: 0
});
$('#lims_user_id').trigger('change');
$('#lims_global_user_id').trigger('change');
}
function clearSelect(selectElement, placeholderText) {
selectElement.innerHTML = '';
const option = document.createElement('option');
option.value = '';
option.textContent = placeholderText;
selectElement.appendChild(option);
}
function appendOption(selectElement, value, text) {
if (value === null || value === undefined || value === '') {
return;
}
const option = document.createElement('option');
option.value = String(value);
option.textContent = text || String(value);
selectElement.appendChild(option);
}
function setSelectValue(selectElement, value) {
selectElement.value = value || '';
if (typeof $ !== 'undefined' && $.fn.select2) {
$(selectElement).trigger('change');
}
}
function normalizeLimsGlobalUsers(data) {
const list = Array.isArray(data) ? data : (data.value || []);
const normalized = [];
list.forEach(function(item) {
const id = item.IdUtente ?? item.id ?? item.ID ?? null;
const text = item.Nome || item.Nominativo || item.UserName || item.Email || id;
if (id !== null && id !== undefined && id !== '') {
normalized.push({
id: String(id),
text: String(text)
});
}
});
normalized.sort(function(a, b) {
return String(a.text).localeCompare(String(b.text), 'it', {
sensitivity: 'base'
});
});
return normalized;
}
function normalizeAcceptors(data) {
const list = data['244'] || data[244] || [];
const normalized = [];
list.forEach(function(item) {
const id =
item.IdCustomFieldValue ??
item.IdCustomFieldsValue ??
item.IdCustomFieldValues ??
item.Id ??
item.ID ??
item.id ??
item.ValueId ??
item.value_id ??
item.Codice ??
null;
const text =
item.Value ??
item.Valore ??
item.Nome ??
item.Name ??
item.Descrizione ??
item.Description ??
item.Text ??
item.text ??
item.Label ??
item.label ??
id;
if (id !== null && id !== undefined && id !== '') {
normalized.push({
id: String(id),
text: String(text)
});
}
});
normalized.sort(function(a, b) {
return String(a.text).localeCompare(String(b.text), 'it', {
sensitivity: 'base'
});
});
return normalized;
}
function populateLimsGlobalUsers(users) {
clearSelect(limsGlobalUserSelect, 'Select LIMS user');
users.forEach(function(user) {
appendOption(limsGlobalUserSelect, user.id, user.text);
});
if (currentLimsGlobalUserId !== '') {
setSelectValue(limsGlobalUserSelect, currentLimsGlobalUserId);
}
}
function populateAcceptors(acceptors) {
clearSelect(limsUserSelect, 'Select acceptor');
acceptors.forEach(function(item) {
appendOption(limsUserSelect, item.id, item.text);
});
if (currentLimsUserId !== '') {
setSelectValue(limsUserSelect, currentLimsUserId);
}
}
async function loadLimsDropdowns() {
try {
const [globalResponse, acceptorsResponse] = await Promise.all([
fetch(limsGlobalUsersEndpoint, {
cache: 'no-store'
}),
fetch(limsAcceptorsEndpoint, {
cache: 'no-store'
})
]);
if (!globalResponse.ok) {
throw new Error('Unable to load LIMS global users.');
}
if (!acceptorsResponse.ok) {
throw new Error('Unable to load LIMS acceptors.');
}
const globalData = await globalResponse.json();
const acceptorsData = await acceptorsResponse.json();
const globalUsers = normalizeLimsGlobalUsers(globalData);
const acceptors = normalizeAcceptors(acceptorsData);
populateLimsGlobalUsers(globalUsers);
populateAcceptors(acceptors);
refreshSelect2();
if (limsLoadingMessage) {
limsLoadingMessage.textContent = 'LIMS users and acceptors loaded.';
}
} catch (error) {
console.error(error);
if (limsLoadingMessage) {
limsLoadingMessage.textContent = 'Warning: unable to load LIMS users or acceptors.';
}
if (typeof Swal !== 'undefined') {
Swal.fire({
icon: 'warning',
title: 'LIMS lists not loaded',
text: error.message || 'Unable to load LIMS dropdown values.'
});
}
}
}
avatarInput.addEventListener('change', function(event) {
const file = event.target.files[0];
if (!file) {
return;
}
const reader = new FileReader();
reader.onload = function(e) {
avatarPreview.src = e.target.result;
};
reader.readAsDataURL(file);
});
document.querySelectorAll('.toggle-password').forEach(function(button) {
button.addEventListener('click', function() {
const targetId = button.dataset.target;
const input = document.getElementById(targetId);
const icon = button.querySelector('i');
if (!input) {
return;
}
if (input.type === 'password') {
input.type = 'text';
if (icon) {
icon.classList.remove('bx-show');
icon.classList.add('bx-hide');
}
} else {
input.type = 'password';
if (icon) {
icon.classList.remove('bx-hide');
icon.classList.add('bx-show');
}
}
});
});
initSelect2();
loadLimsDropdowns();
validatePasswords();
});
</script>
</body>
</html>
+9 -15
View File
@@ -1,5 +1,4 @@
<?php
/**
* Validate rows before export to LIMS.
*
@@ -129,7 +128,6 @@ $fixedAliasMap = [
'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',
@@ -138,11 +136,9 @@ $fixedAliasMap = [
// Fixed keys NOT enforced by the generic mandatory check above:
// - ConsegnaRichiesta: handled by its dedicated validator (also checks the date)
// - ClienteFornitore: nullable placeholder, sent as null on export and
// accepted by LIMS.
// - ClienteFatturazione: TEMP disabled - problemi API lato LIMS, non viene più
// inviato in save_export_lims.php, quindi non deve bloccare la validazione.
$skipRequiredFixed = ['ConsegnaRichiesta', 'ClienteFornitore', 'ClienteFatturazione'];
// - ClienteFornitore / ClienteAnalisi: nullable placeholders, sent as null on
// export and accepted by LIMS.
$skipRequiredFixed = ['ConsegnaRichiesta', 'ClienteFornitore', 'ClienteAnalisi'];
// ── Main ────────────────────────────────────────────────────────────────────
@@ -165,7 +161,7 @@ try {
SELECT iddatadb, templateid, consegna_richiesta,
cliente_responsabile_id, moltiplicatore_prezzo_id,
anagrafica_certest_object_id, anagrafica_certest_service_id,
cliente_fornitore_id, clienteAnalisi, ClienteFatturazione
cliente_fornitore_id, clienteAnalisi
FROM datadb
WHERE iddatadb IN ($placeholders)
");
@@ -180,8 +176,6 @@ try {
SELECT iddatadb, part_number, part_description, idmatrice
FROM identification_parts
WHERE iddatadb IN ($placeholders)
AND part_description IS NOT NULL
AND TRIM(part_description) <> ''
");
$stmt->execute($iddatadbList);
$partsInfo = [];
@@ -191,7 +185,7 @@ try {
// Mandatory-field config per template
$templateIds = array_values(array_unique(array_filter(array_map(
fn($r) => (int)($r['templateid'] ?? 0),
fn ($r) => (int)($r['templateid'] ?? 0),
$recordsInfo
))));
@@ -216,14 +210,13 @@ try {
$requiredFixedByTemplate[(int)$r['template_id']][$key] = $key;
}
// Required custom fields that are visible in the import grid (excluding filed_id = 189 Tested component)
// Required custom fields that are visible in the import grid
$stmt = $pdo->prepare("
SELECT id AS mapping_id, template_id, field_label
FROM template_mapping
WHERE template_id IN ($tplPlaceholders)
AND is_required = 1
AND is_visible_import = 1
AND id <> 189
AND is_required = 1
AND is_visible_import = 1
");
$stmt->execute($templateIds);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
@@ -279,6 +272,7 @@ try {
}
echo json_encode(['success' => true, 'results' => $results]);
} catch (Exception $e) {
error_log("Validation error: " . $e->getMessage());
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
+10 -76
View File
@@ -6,11 +6,6 @@
$startTime = microtime(true);
$isCli = (php_sapi_name() === 'cli');
$isStream = (!$isCli && isset($_GET['stream']));
// Analisi incluse? Default SÌ (il cron non passa il parametro → fa tutto).
// Solo una richiesta HTTP con analisi=0 le salta.
$includeAnalisi = !(!$isCli && isset($_GET['analisi']) && $_GET['analisi'] === '0');
if (!$isCli) {
// When called via HTTP, require auth
@@ -20,17 +15,7 @@ if (!$isCli) {
echo json_encode(['error' => 'Unauthorized']);
exit;
}
if ($isStream) {
// Streaming mode: send progress line-by-line (Server-Sent Events)
set_time_limit(0);
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no'); // disable nginx buffering
while (ob_get_level() > 0) ob_end_flush();
} else {
header('Content-Type: application/json');
}
header('Content-Type: application/json');
}
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
@@ -38,38 +23,14 @@ require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
require_once __DIR__ . '/class/db-functions.php';
$cacheDir = __DIR__ . '/cache';
if (!is_dir($cacheDir)) mkdir($cacheDir, 0755, true);
if (!is_dir($cacheDir)) mkdir($cacheDir, 0777, true);
$log = [];
// Prevent two runs at the same time (e.g. cron + manual click)
$lockHandle = fopen($cacheDir . '/.warm.lock', 'w');
if ($lockHandle === false || !flock($lockHandle, LOCK_EX | LOCK_NB)) {
$busyMsg = 'Un aggiornamento è già in corso in questo momento. Riprova tra poco.';
if ($isCli) {
echo $busyMsg . PHP_EOL;
} elseif ($isStream) {
header('Content-Type: text/event-stream');
echo "event: busy\ndata: " . $busyMsg . "\n\n";
@flush();
} else {
echo json_encode(['success' => false, 'busy' => true, 'message' => $busyMsg]);
}
exit;
}
function warmLog(string $msg, bool $isCli)
{
global $log, $isStream;
function warmLog(string $msg, bool $isCli) {
global $log;
$line = date('H:i:s') . " $msg";
$log[] = $line;
if ($isCli) {
echo $line . PHP_EOL;
} elseif (!empty($isStream)) {
echo "data: " . str_replace("\n", ' ', $line) . "\n\n";
@ob_flush();
@flush();
}
if ($isCli) echo $line . PHP_EOL;
}
try {
@@ -78,7 +39,7 @@ try {
// 1. Clients
warmLog('[clients] Fetching...', $isCli);
$params = ['$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo', '$orderby' => 'Nominativo asc'];
$params = ['$select' => 'IdCliente,Nominativo,CodiceCliente', '$orderby' => 'Nominativo asc'];
$clientData = $api->get("Cliente?" . http_build_query($params));
file_put_contents($cacheDir . '/clienti.json', json_encode($clientData));
$clientCount = count($clientData['value'] ?? []);
@@ -151,40 +112,13 @@ try {
file_put_contents($cacheDir . '/matrici.json', json_encode(['success' => true, 'value' => $matrici]));
warmLog("[matrici] Cached " . count($matrici) . " items", $isCli);
// 6. Analisi — only for matrici actually used by parts (warming all ~2500 matrici
// would mean one API call each; the used set is a few dozen).
if (!$includeAnalisi) {
warmLog('[analisi] Saltate su richiesta.', $isCli);
} else {
$stmt = $pdo->query("SELECT DISTINCT idmatrice FROM identification_parts WHERE idmatrice IS NOT NULL AND idmatrice > 0 ORDER BY idmatrice ASC");
$matriceIds = $stmt->fetchAll(PDO::FETCH_COLUMN);
warmLog("[analisi] Fetching for " . count($matriceIds) . " matrici in use...", $isCli);
foreach ($matriceIds as $mid) {
$mid = (int)$mid;
// Same query as get_analisi_matrice_filter.php with web_only=1 (what the analysis modal requests)
$filter = rawurlencode("Matrice/IdMatrice eq $mid and SelezionabileSuWeb eq true");
try {
$data = $api->get("Analisi?\$filter={$filter}");
$values = $data['value'] ?? [];
file_put_contents($cacheDir . '/analisi_matrice_' . $mid . '.json', json_encode(['value' => $values]));
warmLog("[analisi] Matrice $mid: " . count($values) . " analisi", $isCli);
} catch (Exception $e) {
warmLog("[analisi] Matrice $mid: ERROR " . $e->getMessage(), $isCli);
}
}
} // fine if includeAnalisi
$elapsed = round(microtime(true) - $startTime, 1);
warmLog("Aggiornamento completato in {$elapsed}s", $isCli);
warmLog("Done in {$elapsed}s", $isCli);
} catch (Exception $e) {
$elapsed = round(microtime(true) - $startTime, 1);
warmLog("ERRORE: " . $e->getMessage(), $isCli);
warmLog("FATAL: " . $e->getMessage(), $isCli);
}
if ($isStream) {
echo "event: done\ndata: " . json_encode(['elapsed' => $elapsed ?? 0]) . "\n\n";
@flush();
} elseif (!$isCli) {
if (!$isCli) {
echo json_encode(['success' => true, 'log' => $log]);
}
-519
View File
@@ -1,519 +0,0 @@
<?php
include('include/headscript.php');
/*
* NOTE PERMESSI:
* Il login è già gestito da headscript.php (Auth::check + redirect).
* Se vuoi limitare questa pagina a determinati ruoli, aggiungi qui il tuo
* controllo, esattamente come nelle altre pagine. Esempio:
*
* if (!$user->hasRole('Admin') && !$user->hasRole('SuperUser')) {
* header('Location: import_dashboard.php');
* exit;
* }
*/
?>
<!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>Aggiornamento dati - <?= htmlspecialchars($titlewebsite ?? 'SmartTRF', ENT_QUOTES, 'UTF-8'); ?></title>
<style>
.warm-hero {
display: flex;
align-items: center;
gap: 18px;
}
.warm-hero .warm-icon {
width: 64px;
height: 64px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 30px;
color: #fff;
background: linear-gradient(135deg, #0f172a, #1d4ed8);
flex-shrink: 0;
}
.warm-progress-wrap {
display: none;
}
.warm-progress-wrap.is-visible {
display: block;
}
.progress.warm-progress {
height: 22px;
border-radius: 12px;
background: #eef2f7;
}
.progress.warm-progress .progress-bar {
font-size: 12px;
font-weight: 600;
}
.warm-steps {
list-style: none;
padding: 0;
margin: 18px 0 0 0;
}
.warm-steps li {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 10px;
margin-bottom: 6px;
background: #f8fafc;
border: 1px solid #eef2f7;
transition: background .2s, border-color .2s;
}
.warm-steps li .step-icon {
width: 26px;
height: 26px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
background: #e5e7eb;
color: #94a3b8;
}
.warm-steps li.is-active {
background: #eff6ff;
border-color: #bfdbfe;
}
.warm-steps li.is-active .step-icon {
background: #2563eb;
color: #fff;
}
.warm-steps li.is-done {
background: #f0fdf4;
border-color: #bbf7d0;
}
.warm-steps li.is-done .step-icon {
background: #16a34a;
color: #fff;
}
.warm-steps li .step-label {
font-weight: 600;
color: #1f2937;
}
.warm-steps li .step-detail {
font-size: 12px;
color: #64748b;
margin-left: auto;
text-align: right;
}
.warm-log {
display: none;
margin-top: 18px;
}
.warm-log.is-visible {
display: block;
}
.warm-log pre {
max-height: 320px;
overflow: auto;
background: #0f172a;
color: #cbd5e1;
padding: 16px;
border-radius: 12px;
font-size: 12px;
line-height: 1.6;
margin: 0;
}
.warm-result {
display: none;
margin-top: 18px;
}
.warm-result.is-visible {
display: block;
}
</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-body p-4">
<div class="warm-hero">
<div class="warm-icon"><i class="bx bx-refresh"></i></div>
<div>
<h5 class="mb-1">Aggiornamento dati dal gestionale</h5>
<p class="mb-0 text-muted">
Ricarica clienti, matrici, analisi e le altre liste dal software di laboratorio.
Da usare quando sono state fatte modifiche sul gestionale e vuoi vederle subito
senza aspettare l'aggiornamento automatico.
</p>
</div>
</div>
<hr class="my-4">
<div class="form-check form-switch mb-3" id="analisiSwitchWrap">
<input class="form-check-input" type="checkbox" role="switch" id="includeAnalisi" checked>
<label class="form-check-label" for="includeAnalisi">
Aggiorna anche le <strong>analisi</strong>
<span class="text-muted small">(più lento &mdash; togli la spunta se hai modificato solo clienti o altre liste)</span>
</label>
</div>
<button type="button" class="btn btn-primary btn-lg px-4" id="btnStart">
<i class="bx bx-play-circle me-1"></i> Avvia aggiornamento
</button>
<div class="small text-muted mt-2" id="idleHint">
L'operazione può richiedere da qualche secondo a un paio di minuti.
</div>
<!-- Progress -->
<div class="warm-progress-wrap mt-4" id="progressWrap">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="fw-semibold" id="progressTitle">Aggiornamento in corso…</span>
<span class="text-muted small" id="progressElapsed">0s</span>
</div>
<div class="progress warm-progress">
<div class="progress-bar progress-bar-striped progress-bar-animated bg-primary"
id="progressBar" role="progressbar" style="width: 0%;">0%</div>
</div>
<ul class="warm-steps" id="stepsList">
<!-- steps injected by JS -->
</ul>
<div class="mt-3">
<a href="javascript:;" class="small text-decoration-none" id="toggleLog">
<i class="bx bx-code-alt"></i> Mostra dettaglio tecnico
</a>
</div>
<div class="warm-log" id="logBox">
<pre id="logPre"></pre>
</div>
</div>
<!-- Result -->
<div class="warm-result" id="resultBox">
<div class="alert d-flex align-items-center" id="resultAlert" role="alert">
<i class="bx me-2 fs-4" id="resultIcon"></i>
<div id="resultText"></div>
</div>
<button type="button" class="btn btn-outline-primary" id="btnAgain">
<i class="bx bx-refresh me-1"></i> Esegui di nuovo
</button>
</div>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
/*
* Se sposti warm_cache.php in un'altra cartella, cambia SOLO questa riga.
*/
const WARM_ENDPOINT = 'warm_cache.php';
const btnStart = document.getElementById('btnStart');
const btnAgain = document.getElementById('btnAgain');
const idleHint = document.getElementById('idleHint');
const includeAnalisi = document.getElementById('includeAnalisi');
const progressWrap = document.getElementById('progressWrap');
const progressBar = document.getElementById('progressBar');
const progressTitle = document.getElementById('progressTitle');
const progressElapsed = document.getElementById('progressElapsed');
const stepsList = document.getElementById('stepsList');
const toggleLog = document.getElementById('toggleLog');
const logBox = document.getElementById('logBox');
const logPre = document.getElementById('logPre');
const resultBox = document.getElementById('resultBox');
const resultAlert = document.getElementById('resultAlert');
const resultIcon = document.getElementById('resultIcon');
const resultText = document.getElementById('resultText');
/*
* I "passi" mostrati al cliente in modo leggibile.
* key = frammento che compare nel log tecnico (dentro le parentesi quadre)
* label = nome umano
* Il progresso avanza man mano che questi tag compaiono nel log.
*/
const STEPS = [{
key: 'clients',
label: 'Clienti'
},
{
key: 'MoltiplicatorePrezzo',
label: 'Listini prezzi'
},
{
key: 'AnagraficaCertest',
label: 'Anagrafiche Certest'
},
{
key: 'ClienteResponsabile',
label: 'Responsabili cliente'
},
{
key: 'CustomField',
label: 'Campi personalizzati'
},
{
key: 'matrici',
label: 'Matrici'
},
{
key: 'analisi',
label: 'Analisi'
}
];
let stepEls = {};
let currentStepIndex = -1;
let timerId = null;
let startedAt = 0;
let activeSteps = []; // i passi effettivamente in gioco per questa esecuzione
function buildSteps() {
stepsList.innerHTML = '';
stepEls = {};
// Se le analisi sono disattivate, escludo quel passo dalla lista.
const withAnalisi = includeAnalisi.checked;
activeSteps = STEPS.filter(function(s) {
return withAnalisi || s.key !== 'analisi';
});
activeSteps.forEach(function(step, i) {
const li = document.createElement('li');
li.dataset.index = i;
li.innerHTML =
'<span class="step-icon"><i class="bx bx-time"></i></span>' +
'<span class="step-label">' + step.label + '</span>' +
'<span class="step-detail"></span>';
stepsList.appendChild(li);
stepEls[i] = li;
});
}
function markStep(index, state, detail) {
const li = stepEls[index];
if (!li) return;
li.classList.remove('is-active', 'is-done');
const icon = li.querySelector('.step-icon i');
if (state === 'active') {
li.classList.add('is-active');
icon.className = 'bx bx-loader-alt bx-spin';
} else if (state === 'done') {
li.classList.add('is-done');
icon.className = 'bx bx-check';
}
if (detail !== undefined) {
li.querySelector('.step-detail').textContent = detail;
}
}
function advanceTo(index, detail) {
if (index <= currentStepIndex) {
// stesso passo, aggiorna solo il dettaglio (es. conteggio)
if (detail !== undefined) markStep(index, 'active', detail);
return;
}
// chiudi i passi precedenti
for (let i = 0; i <= currentStepIndex; i++) markStep(i, 'done');
currentStepIndex = index;
markStep(index, 'active', detail);
const pct = Math.round(((index) / activeSteps.length) * 100);
setProgress(Math.max(pct, 3));
}
function setProgress(pct) {
pct = Math.max(0, Math.min(100, pct));
progressBar.style.width = pct + '%';
progressBar.textContent = pct + '%';
}
function matchStep(line) {
// cerca [clients], [matrici], [analisi] ecc. nella riga di log
for (let i = 0; i < activeSteps.length; i++) {
if (line.indexOf('[' + activeSteps[i].key) !== -1 ||
line.indexOf(activeSteps[i].key + ']') !== -1) {
return i;
}
}
return -1;
}
function extractDetail(line) {
// prova a estrarre un conteggio tipo "Cached 128 clients" o "34 analisi"
const m = line.match(/(\d+)\s+(clients|items|values|responsabili|analisi)/i);
if (m) return m[1] + ' elementi';
return undefined;
}
function startTimer() {
startedAt = Date.now();
timerId = setInterval(function() {
const s = Math.round((Date.now() - startedAt) / 1000);
progressElapsed.textContent = s + 's';
}, 1000);
}
function stopTimer() {
if (timerId) clearInterval(timerId);
timerId = null;
}
function resetUI() {
buildSteps();
currentStepIndex = -1;
setProgress(0);
logPre.textContent = '';
progressTitle.textContent = 'Aggiornamento in corso…';
progressElapsed.textContent = '0s';
resultBox.classList.remove('is-visible');
progressWrap.classList.add('is-visible');
}
function showResult(type, message) {
stopTimer();
progressBar.classList.remove('progress-bar-animated', 'progress-bar-striped');
resultAlert.className = 'alert d-flex align-items-center';
if (type === 'success') {
resultAlert.classList.add('alert-success');
resultIcon.className = 'bx bx-check-circle me-2 fs-4';
progressBar.classList.add('bg-success');
} else if (type === 'warning') {
resultAlert.classList.add('alert-warning');
resultIcon.className = 'bx bx-error me-2 fs-4';
progressBar.classList.add('bg-warning');
} else {
resultAlert.classList.add('alert-danger');
resultIcon.className = 'bx bx-x-circle me-2 fs-4';
progressBar.classList.add('bg-danger');
}
resultText.textContent = message;
resultBox.classList.add('is-visible');
btnStart.disabled = false;
includeAnalisi.disabled = false;
}
function run() {
btnStart.disabled = true;
includeAnalisi.disabled = true;
idleHint.style.display = 'none';
resetUI();
startTimer();
const analisiParam = includeAnalisi.checked ? '1' : '0';
const es = new EventSource(WARM_ENDPOINT + '?stream=1&analisi=' + analisiParam);
es.onmessage = function(e) {
const line = e.data;
logPre.textContent += line + '\n';
logPre.scrollTop = logPre.scrollHeight;
const idx = matchStep(line);
if (idx !== -1) {
advanceTo(idx, extractDetail(line));
}
};
es.addEventListener('busy', function(e) {
es.close();
showResult('warning', e.data ||
'Un aggiornamento è già in corso. Attendi qualche istante e riprova.');
});
es.addEventListener('done', function(e) {
es.close();
// completa tutti i passi
for (let i = 0; i < activeSteps.length; i++) markStep(i, 'done');
setProgress(100);
let elapsed = '';
try {
elapsed = JSON.parse(e.data).elapsed;
} catch (err) {}
// se nel log è comparsa la parola ERRORE, segnala come warning
if (logPre.textContent.indexOf('ERRORE') !== -1) {
showResult('warning',
'Aggiornamento terminato, ma alcuni dati potrebbero non essere stati caricati. ' +
'Controlla il dettaglio tecnico.');
} else {
progressTitle.textContent = 'Completato';
showResult('success',
'Dati aggiornati correttamente' +
(elapsed ? ' in ' + elapsed + ' secondi.' : '.') +
' Le nuove informazioni sono ora disponibili.');
}
});
es.onerror = function() {
// EventSource tenta il reconnect da solo: lo blocchiamo.
es.close();
stopTimer();
// Se avevamo già finito, il done è già scattato: non sovrascrivere.
if (!resultBox.classList.contains('is-visible')) {
showResult('error',
'Connessione interrotta durante l\'aggiornamento. ' +
'Verifica la rete e riprova.');
}
};
}
btnStart.addEventListener('click', run);
btnAgain.addEventListener('click', run);
toggleLog.addEventListener('click', function() {
logBox.classList.toggle('is-visible');
toggleLog.innerHTML = logBox.classList.contains('is-visible') ?
'<i class="bx bx-hide"></i> Nascondi dettaglio tecnico' :
'<i class="bx bx-code-alt"></i> Mostra dettaglio tecnico';
});
});
</script>
</body>
</html>
+4 -1
View File
@@ -14,7 +14,10 @@ if (PHP_VERSION_ID < 50600) {
echo $err;
}
}
throw new RuntimeException($err);
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
+4 -41
View File
@@ -26,23 +26,12 @@ use Composer\Semver\VersionParser;
*/
class InstalledVersions
{
/**
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
* @internal
*/
private static $selfDir = null;
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/**
* @var bool|null
*/
@@ -320,24 +309,6 @@ class InstalledVersions
{
self::$installed = $data;
self::$installedByVendor = array();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
* @return string
*/
private static function getSelfDir()
{
if (self::$selfDir === null) {
self::$selfDir = strtr(__DIR__, '\\', '/');
}
return self::$selfDir;
}
/**
@@ -351,27 +322,19 @@ class InstalledVersions
}
$installed = array();
$copiedLocalDir = false;
if (self::$canGetVendors) {
$selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
self::$installedByVendor[$vendorDir] = $required;
$installed[] = $required;
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
self::$installed = $required;
self::$installedIsLocalDir = true;
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
}
}
@@ -387,7 +350,7 @@ class InstalledVersions
}
}
if (self::$installed !== array() && !$copiedLocalDir) {
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
-407
View File
@@ -184,196 +184,6 @@ return array(
'Brick\\Math\\Internal\\Calculator\\GmpCalculator' => $vendorDir . '/brick/math/src/Internal/Calculator/GmpCalculator.php',
'Brick\\Math\\Internal\\Calculator\\NativeCalculator' => $vendorDir . '/brick/math/src/Internal/Calculator/NativeCalculator.php',
'Brick\\Math\\RoundingMode' => $vendorDir . '/brick/math/src/RoundingMode.php',
'Cake\\Chronos\\Chronos' => $vendorDir . '/cakephp/chronos/src/Chronos.php',
'Cake\\Chronos\\ChronosDate' => $vendorDir . '/cakephp/chronos/src/ChronosDate.php',
'Cake\\Chronos\\ChronosDatePeriod' => $vendorDir . '/cakephp/chronos/src/ChronosDatePeriod.php',
'Cake\\Chronos\\ChronosInterval' => $vendorDir . '/cakephp/chronos/src/ChronosInterval.php',
'Cake\\Chronos\\ChronosPeriod' => $vendorDir . '/cakephp/chronos/src/ChronosPeriod.php',
'Cake\\Chronos\\ChronosTime' => $vendorDir . '/cakephp/chronos/src/ChronosTime.php',
'Cake\\Chronos\\ClockFactory' => $vendorDir . '/cakephp/chronos/src/ClockFactory.php',
'Cake\\Chronos\\DifferenceFormatter' => $vendorDir . '/cakephp/chronos/src/DifferenceFormatter.php',
'Cake\\Chronos\\DifferenceFormatterInterface' => $vendorDir . '/cakephp/chronos/src/DifferenceFormatterInterface.php',
'Cake\\Chronos\\FormattingTrait' => $vendorDir . '/cakephp/chronos/src/FormattingTrait.php',
'Cake\\Chronos\\Translator' => $vendorDir . '/cakephp/chronos/src/Translator.php',
'Cake\\Core\\App' => $vendorDir . '/cakephp/core/App.php',
'Cake\\Core\\Attribute\\Configure' => $vendorDir . '/cakephp/core/Attribute/Configure.php',
'Cake\\Core\\BasePlugin' => $vendorDir . '/cakephp/core/BasePlugin.php',
'Cake\\Core\\Configure' => $vendorDir . '/cakephp/core/Configure.php',
'Cake\\Core\\Configure\\ConfigEngineInterface' => $vendorDir . '/cakephp/core/Configure/ConfigEngineInterface.php',
'Cake\\Core\\Configure\\Engine\\IniConfig' => $vendorDir . '/cakephp/core/Configure/Engine/IniConfig.php',
'Cake\\Core\\Configure\\Engine\\JsonConfig' => $vendorDir . '/cakephp/core/Configure/Engine/JsonConfig.php',
'Cake\\Core\\Configure\\Engine\\PhpConfig' => $vendorDir . '/cakephp/core/Configure/Engine/PhpConfig.php',
'Cake\\Core\\Configure\\FileConfigTrait' => $vendorDir . '/cakephp/core/Configure/FileConfigTrait.php',
'Cake\\Core\\ConsoleApplicationInterface' => $vendorDir . '/cakephp/core/ConsoleApplicationInterface.php',
'Cake\\Core\\Container' => $vendorDir . '/cakephp/core/Container.php',
'Cake\\Core\\ContainerApplicationInterface' => $vendorDir . '/cakephp/core/ContainerApplicationInterface.php',
'Cake\\Core\\ContainerInterface' => $vendorDir . '/cakephp/core/ContainerInterface.php',
'Cake\\Core\\ConventionsTrait' => $vendorDir . '/cakephp/core/ConventionsTrait.php',
'Cake\\Core\\EventAwareApplicationInterface' => $vendorDir . '/cakephp/core/EventAwareApplicationInterface.php',
'Cake\\Core\\Exception\\CakeException' => $vendorDir . '/cakephp/core/Exception/CakeException.php',
'Cake\\Core\\Exception\\HttpErrorCodeInterface' => $vendorDir . '/cakephp/core/Exception/HttpErrorCodeInterface.php',
'Cake\\Core\\Exception\\MissingPluginException' => $vendorDir . '/cakephp/core/Exception/MissingPluginException.php',
'Cake\\Core\\HttpApplicationInterface' => $vendorDir . '/cakephp/core/HttpApplicationInterface.php',
'Cake\\Core\\InstanceConfigTrait' => $vendorDir . '/cakephp/core/InstanceConfigTrait.php',
'Cake\\Core\\ObjectRegistry' => $vendorDir . '/cakephp/core/ObjectRegistry.php',
'Cake\\Core\\Plugin' => $vendorDir . '/cakephp/core/Plugin.php',
'Cake\\Core\\PluginApplicationInterface' => $vendorDir . '/cakephp/core/PluginApplicationInterface.php',
'Cake\\Core\\PluginCollection' => $vendorDir . '/cakephp/core/PluginCollection.php',
'Cake\\Core\\PluginConfig' => $vendorDir . '/cakephp/core/PluginConfig.php',
'Cake\\Core\\PluginInterface' => $vendorDir . '/cakephp/core/PluginInterface.php',
'Cake\\Core\\Retry\\CommandRetry' => $vendorDir . '/cakephp/core/Retry/CommandRetry.php',
'Cake\\Core\\Retry\\RetryStrategyInterface' => $vendorDir . '/cakephp/core/Retry/RetryStrategyInterface.php',
'Cake\\Core\\ServiceConfig' => $vendorDir . '/cakephp/core/ServiceConfig.php',
'Cake\\Core\\ServiceProvider' => $vendorDir . '/cakephp/core/ServiceProvider.php',
'Cake\\Core\\StaticConfigTrait' => $vendorDir . '/cakephp/core/StaticConfigTrait.php',
'Cake\\Core\\TestSuite\\ContainerStubTrait' => $vendorDir . '/cakephp/core/TestSuite/ContainerStubTrait.php',
'Cake\\Database\\Connection' => $vendorDir . '/cakephp/database/Connection.php',
'Cake\\Database\\ConstraintsInterface' => $vendorDir . '/cakephp/database/ConstraintsInterface.php',
'Cake\\Database\\Driver' => $vendorDir . '/cakephp/database/Driver.php',
'Cake\\Database\\DriverFeatureEnum' => $vendorDir . '/cakephp/database/DriverFeatureEnum.php',
'Cake\\Database\\Driver\\Mysql' => $vendorDir . '/cakephp/database/Driver/Mysql.php',
'Cake\\Database\\Driver\\Postgres' => $vendorDir . '/cakephp/database/Driver/Postgres.php',
'Cake\\Database\\Driver\\Sqlite' => $vendorDir . '/cakephp/database/Driver/Sqlite.php',
'Cake\\Database\\Driver\\Sqlserver' => $vendorDir . '/cakephp/database/Driver/Sqlserver.php',
'Cake\\Database\\Driver\\TupleComparisonTranslatorTrait' => $vendorDir . '/cakephp/database/Driver/TupleComparisonTranslatorTrait.php',
'Cake\\Database\\Exception\\DatabaseException' => $vendorDir . '/cakephp/database/Exception/DatabaseException.php',
'Cake\\Database\\Exception\\MissingConnectionException' => $vendorDir . '/cakephp/database/Exception/MissingConnectionException.php',
'Cake\\Database\\Exception\\MissingDriverException' => $vendorDir . '/cakephp/database/Exception/MissingDriverException.php',
'Cake\\Database\\Exception\\MissingExtensionException' => $vendorDir . '/cakephp/database/Exception/MissingExtensionException.php',
'Cake\\Database\\Exception\\NestedTransactionRollbackException' => $vendorDir . '/cakephp/database/Exception/NestedTransactionRollbackException.php',
'Cake\\Database\\Exception\\QueryException' => $vendorDir . '/cakephp/database/Exception/QueryException.php',
'Cake\\Database\\ExpressionInterface' => $vendorDir . '/cakephp/database/ExpressionInterface.php',
'Cake\\Database\\Expression\\AggregateExpression' => $vendorDir . '/cakephp/database/Expression/AggregateExpression.php',
'Cake\\Database\\Expression\\BetweenExpression' => $vendorDir . '/cakephp/database/Expression/BetweenExpression.php',
'Cake\\Database\\Expression\\CaseExpressionTrait' => $vendorDir . '/cakephp/database/Expression/CaseExpressionTrait.php',
'Cake\\Database\\Expression\\CaseStatementExpression' => $vendorDir . '/cakephp/database/Expression/CaseStatementExpression.php',
'Cake\\Database\\Expression\\CommonTableExpression' => $vendorDir . '/cakephp/database/Expression/CommonTableExpression.php',
'Cake\\Database\\Expression\\ComparisonExpression' => $vendorDir . '/cakephp/database/Expression/ComparisonExpression.php',
'Cake\\Database\\Expression\\FieldInterface' => $vendorDir . '/cakephp/database/Expression/FieldInterface.php',
'Cake\\Database\\Expression\\FieldTrait' => $vendorDir . '/cakephp/database/Expression/FieldTrait.php',
'Cake\\Database\\Expression\\FunctionExpression' => $vendorDir . '/cakephp/database/Expression/FunctionExpression.php',
'Cake\\Database\\Expression\\IdentifierExpression' => $vendorDir . '/cakephp/database/Expression/IdentifierExpression.php',
'Cake\\Database\\Expression\\OrderByExpression' => $vendorDir . '/cakephp/database/Expression/OrderByExpression.php',
'Cake\\Database\\Expression\\OrderClauseExpression' => $vendorDir . '/cakephp/database/Expression/OrderClauseExpression.php',
'Cake\\Database\\Expression\\QueryExpression' => $vendorDir . '/cakephp/database/Expression/QueryExpression.php',
'Cake\\Database\\Expression\\StringExpression' => $vendorDir . '/cakephp/database/Expression/StringExpression.php',
'Cake\\Database\\Expression\\TupleComparison' => $vendorDir . '/cakephp/database/Expression/TupleComparison.php',
'Cake\\Database\\Expression\\UnaryExpression' => $vendorDir . '/cakephp/database/Expression/UnaryExpression.php',
'Cake\\Database\\Expression\\ValuesExpression' => $vendorDir . '/cakephp/database/Expression/ValuesExpression.php',
'Cake\\Database\\Expression\\WhenThenExpression' => $vendorDir . '/cakephp/database/Expression/WhenThenExpression.php',
'Cake\\Database\\Expression\\WindowExpression' => $vendorDir . '/cakephp/database/Expression/WindowExpression.php',
'Cake\\Database\\Expression\\WindowInterface' => $vendorDir . '/cakephp/database/Expression/WindowInterface.php',
'Cake\\Database\\FieldTypeConverter' => $vendorDir . '/cakephp/database/FieldTypeConverter.php',
'Cake\\Database\\FunctionsBuilder' => $vendorDir . '/cakephp/database/FunctionsBuilder.php',
'Cake\\Database\\IdentifierQuoter' => $vendorDir . '/cakephp/database/IdentifierQuoter.php',
'Cake\\Database\\Log\\LoggedQuery' => $vendorDir . '/cakephp/database/Log/LoggedQuery.php',
'Cake\\Database\\Log\\QueryLogger' => $vendorDir . '/cakephp/database/Log/QueryLogger.php',
'Cake\\Database\\PostgresCompiler' => $vendorDir . '/cakephp/database/PostgresCompiler.php',
'Cake\\Database\\Query' => $vendorDir . '/cakephp/database/Query.php',
'Cake\\Database\\QueryCompiler' => $vendorDir . '/cakephp/database/QueryCompiler.php',
'Cake\\Database\\Query\\DeleteQuery' => $vendorDir . '/cakephp/database/Query/DeleteQuery.php',
'Cake\\Database\\Query\\InsertQuery' => $vendorDir . '/cakephp/database/Query/InsertQuery.php',
'Cake\\Database\\Query\\QueryFactory' => $vendorDir . '/cakephp/database/Query/QueryFactory.php',
'Cake\\Database\\Query\\SelectQuery' => $vendorDir . '/cakephp/database/Query/SelectQuery.php',
'Cake\\Database\\Query\\UpdateQuery' => $vendorDir . '/cakephp/database/Query/UpdateQuery.php',
'Cake\\Database\\Retry\\ErrorCodeWaitStrategy' => $vendorDir . '/cakephp/database/Retry/ErrorCodeWaitStrategy.php',
'Cake\\Database\\Retry\\ReconnectStrategy' => $vendorDir . '/cakephp/database/Retry/ReconnectStrategy.php',
'Cake\\Database\\SchemaCache' => $vendorDir . '/cakephp/database/SchemaCache.php',
'Cake\\Database\\Schema\\CachedCollection' => $vendorDir . '/cakephp/database/Schema/CachedCollection.php',
'Cake\\Database\\Schema\\CheckConstraint' => $vendorDir . '/cakephp/database/Schema/CheckConstraint.php',
'Cake\\Database\\Schema\\Collection' => $vendorDir . '/cakephp/database/Schema/Collection.php',
'Cake\\Database\\Schema\\CollectionInterface' => $vendorDir . '/cakephp/database/Schema/CollectionInterface.php',
'Cake\\Database\\Schema\\Column' => $vendorDir . '/cakephp/database/Schema/Column.php',
'Cake\\Database\\Schema\\Constraint' => $vendorDir . '/cakephp/database/Schema/Constraint.php',
'Cake\\Database\\Schema\\ForeignKey' => $vendorDir . '/cakephp/database/Schema/ForeignKey.php',
'Cake\\Database\\Schema\\Index' => $vendorDir . '/cakephp/database/Schema/Index.php',
'Cake\\Database\\Schema\\MysqlSchemaDialect' => $vendorDir . '/cakephp/database/Schema/MysqlSchemaDialect.php',
'Cake\\Database\\Schema\\PostgresSchemaDialect' => $vendorDir . '/cakephp/database/Schema/PostgresSchemaDialect.php',
'Cake\\Database\\Schema\\SchemaDialect' => $vendorDir . '/cakephp/database/Schema/SchemaDialect.php',
'Cake\\Database\\Schema\\SqlGeneratorInterface' => $vendorDir . '/cakephp/database/Schema/SqlGeneratorInterface.php',
'Cake\\Database\\Schema\\SqliteSchemaDialect' => $vendorDir . '/cakephp/database/Schema/SqliteSchemaDialect.php',
'Cake\\Database\\Schema\\SqlserverSchemaDialect' => $vendorDir . '/cakephp/database/Schema/SqlserverSchemaDialect.php',
'Cake\\Database\\Schema\\TableSchema' => $vendorDir . '/cakephp/database/Schema/TableSchema.php',
'Cake\\Database\\Schema\\TableSchemaInterface' => $vendorDir . '/cakephp/database/Schema/TableSchemaInterface.php',
'Cake\\Database\\Schema\\UniqueKey' => $vendorDir . '/cakephp/database/Schema/UniqueKey.php',
'Cake\\Database\\SqlserverCompiler' => $vendorDir . '/cakephp/database/SqlserverCompiler.php',
'Cake\\Database\\StatementInterface' => $vendorDir . '/cakephp/database/StatementInterface.php',
'Cake\\Database\\Statement\\SqliteStatement' => $vendorDir . '/cakephp/database/Statement/SqliteStatement.php',
'Cake\\Database\\Statement\\SqlserverStatement' => $vendorDir . '/cakephp/database/Statement/SqlserverStatement.php',
'Cake\\Database\\Statement\\Statement' => $vendorDir . '/cakephp/database/Statement/Statement.php',
'Cake\\Database\\TypeFactory' => $vendorDir . '/cakephp/database/TypeFactory.php',
'Cake\\Database\\TypeInterface' => $vendorDir . '/cakephp/database/TypeInterface.php',
'Cake\\Database\\TypeMap' => $vendorDir . '/cakephp/database/TypeMap.php',
'Cake\\Database\\TypeMapTrait' => $vendorDir . '/cakephp/database/TypeMapTrait.php',
'Cake\\Database\\Type\\BaseType' => $vendorDir . '/cakephp/database/Type/BaseType.php',
'Cake\\Database\\Type\\BatchCastingInterface' => $vendorDir . '/cakephp/database/Type/BatchCastingInterface.php',
'Cake\\Database\\Type\\BinaryType' => $vendorDir . '/cakephp/database/Type/BinaryType.php',
'Cake\\Database\\Type\\BinaryUuidType' => $vendorDir . '/cakephp/database/Type/BinaryUuidType.php',
'Cake\\Database\\Type\\BoolType' => $vendorDir . '/cakephp/database/Type/BoolType.php',
'Cake\\Database\\Type\\ColumnSchemaAwareInterface' => $vendorDir . '/cakephp/database/Type/ColumnSchemaAwareInterface.php',
'Cake\\Database\\Type\\DateTimeFractionalType' => $vendorDir . '/cakephp/database/Type/DateTimeFractionalType.php',
'Cake\\Database\\Type\\DateTimeTimezoneType' => $vendorDir . '/cakephp/database/Type/DateTimeTimezoneType.php',
'Cake\\Database\\Type\\DateTimeType' => $vendorDir . '/cakephp/database/Type/DateTimeType.php',
'Cake\\Database\\Type\\DateType' => $vendorDir . '/cakephp/database/Type/DateType.php',
'Cake\\Database\\Type\\DecimalType' => $vendorDir . '/cakephp/database/Type/DecimalType.php',
'Cake\\Database\\Type\\EnumLabelInterface' => $vendorDir . '/cakephp/database/Type/EnumLabelInterface.php',
'Cake\\Database\\Type\\EnumType' => $vendorDir . '/cakephp/database/Type/EnumType.php',
'Cake\\Database\\Type\\ExpressionTypeCasterTrait' => $vendorDir . '/cakephp/database/Type/ExpressionTypeCasterTrait.php',
'Cake\\Database\\Type\\ExpressionTypeInterface' => $vendorDir . '/cakephp/database/Type/ExpressionTypeInterface.php',
'Cake\\Database\\Type\\FloatType' => $vendorDir . '/cakephp/database/Type/FloatType.php',
'Cake\\Database\\Type\\IntegerType' => $vendorDir . '/cakephp/database/Type/IntegerType.php',
'Cake\\Database\\Type\\JsonType' => $vendorDir . '/cakephp/database/Type/JsonType.php',
'Cake\\Database\\Type\\OptionalConvertInterface' => $vendorDir . '/cakephp/database/Type/OptionalConvertInterface.php',
'Cake\\Database\\Type\\StringType' => $vendorDir . '/cakephp/database/Type/StringType.php',
'Cake\\Database\\Type\\TimeType' => $vendorDir . '/cakephp/database/Type/TimeType.php',
'Cake\\Database\\Type\\UuidType' => $vendorDir . '/cakephp/database/Type/UuidType.php',
'Cake\\Database\\TypedResultInterface' => $vendorDir . '/cakephp/database/TypedResultInterface.php',
'Cake\\Database\\TypedResultTrait' => $vendorDir . '/cakephp/database/TypedResultTrait.php',
'Cake\\Database\\ValueBinder' => $vendorDir . '/cakephp/database/ValueBinder.php',
'Cake\\Datasource\\ConnectionInterface' => $vendorDir . '/cakephp/datasource/ConnectionInterface.php',
'Cake\\Datasource\\ConnectionManager' => $vendorDir . '/cakephp/datasource/ConnectionManager.php',
'Cake\\Datasource\\ConnectionRegistry' => $vendorDir . '/cakephp/datasource/ConnectionRegistry.php',
'Cake\\Datasource\\EntityInterface' => $vendorDir . '/cakephp/datasource/EntityInterface.php',
'Cake\\Datasource\\EntityTrait' => $vendorDir . '/cakephp/datasource/EntityTrait.php',
'Cake\\Datasource\\Exception\\InvalidPrimaryKeyException' => $vendorDir . '/cakephp/datasource/Exception/InvalidPrimaryKeyException.php',
'Cake\\Datasource\\Exception\\MissingDatasourceConfigException' => $vendorDir . '/cakephp/datasource/Exception/MissingDatasourceConfigException.php',
'Cake\\Datasource\\Exception\\MissingDatasourceException' => $vendorDir . '/cakephp/datasource/Exception/MissingDatasourceException.php',
'Cake\\Datasource\\Exception\\MissingModelException' => $vendorDir . '/cakephp/datasource/Exception/MissingModelException.php',
'Cake\\Datasource\\Exception\\MissingPropertyException' => $vendorDir . '/cakephp/datasource/Exception/MissingPropertyException.php',
'Cake\\Datasource\\Exception\\RecordNotFoundException' => $vendorDir . '/cakephp/datasource/Exception/RecordNotFoundException.php',
'Cake\\Datasource\\FactoryLocator' => $vendorDir . '/cakephp/datasource/FactoryLocator.php',
'Cake\\Datasource\\FixtureInterface' => $vendorDir . '/cakephp/datasource/FixtureInterface.php',
'Cake\\Datasource\\InvalidPropertyInterface' => $vendorDir . '/cakephp/datasource/InvalidPropertyInterface.php',
'Cake\\Datasource\\Locator\\AbstractLocator' => $vendorDir . '/cakephp/datasource/Locator/AbstractLocator.php',
'Cake\\Datasource\\Locator\\LocatorInterface' => $vendorDir . '/cakephp/datasource/Locator/LocatorInterface.php',
'Cake\\Datasource\\ModelAwareTrait' => $vendorDir . '/cakephp/datasource/ModelAwareTrait.php',
'Cake\\Datasource\\Paging\\Exception\\PageOutOfBoundsException' => $vendorDir . '/cakephp/datasource/Paging/Exception/PageOutOfBoundsException.php',
'Cake\\Datasource\\Paging\\NumericPaginator' => $vendorDir . '/cakephp/datasource/Paging/NumericPaginator.php',
'Cake\\Datasource\\Paging\\PaginatedInterface' => $vendorDir . '/cakephp/datasource/Paging/PaginatedInterface.php',
'Cake\\Datasource\\Paging\\PaginatedResultSet' => $vendorDir . '/cakephp/datasource/Paging/PaginatedResultSet.php',
'Cake\\Datasource\\Paging\\PaginatorInterface' => $vendorDir . '/cakephp/datasource/Paging/PaginatorInterface.php',
'Cake\\Datasource\\Paging\\SimplePaginator' => $vendorDir . '/cakephp/datasource/Paging/SimplePaginator.php',
'Cake\\Datasource\\Paging\\SortField' => $vendorDir . '/cakephp/datasource/Paging/SortField.php',
'Cake\\Datasource\\Paging\\SortableFieldsBuilder' => $vendorDir . '/cakephp/datasource/Paging/SortableFieldsBuilder.php',
'Cake\\Datasource\\QueryCacher' => $vendorDir . '/cakephp/datasource/QueryCacher.php',
'Cake\\Datasource\\QueryInterface' => $vendorDir . '/cakephp/datasource/QueryInterface.php',
'Cake\\Datasource\\RepositoryInterface' => $vendorDir . '/cakephp/datasource/RepositoryInterface.php',
'Cake\\Datasource\\ResultSetDecorator' => $vendorDir . '/cakephp/datasource/ResultSetDecorator.php',
'Cake\\Datasource\\ResultSetInterface' => $vendorDir . '/cakephp/datasource/ResultSetInterface.php',
'Cake\\Datasource\\RuleInvoker' => $vendorDir . '/cakephp/datasource/RuleInvoker.php',
'Cake\\Datasource\\RulesAwareTrait' => $vendorDir . '/cakephp/datasource/RulesAwareTrait.php',
'Cake\\Datasource\\RulesChecker' => $vendorDir . '/cakephp/datasource/RulesChecker.php',
'Cake\\Datasource\\SchemaInterface' => $vendorDir . '/cakephp/datasource/SchemaInterface.php',
'Cake\\Utility\\CookieCryptTrait' => $vendorDir . '/cakephp/utility/CookieCryptTrait.php',
'Cake\\Utility\\Crypto\\OpenSsl' => $vendorDir . '/cakephp/utility/Crypto/OpenSsl.php',
'Cake\\Utility\\Exception\\XmlException' => $vendorDir . '/cakephp/utility/Exception/XmlException.php',
'Cake\\Utility\\Filesystem' => $vendorDir . '/cakephp/utility/Filesystem.php',
'Cake\\Utility\\Hash' => $vendorDir . '/cakephp/utility/Hash.php',
'Cake\\Utility\\Inflector' => $vendorDir . '/cakephp/utility/Inflector.php',
'Cake\\Utility\\MergeVariablesTrait' => $vendorDir . '/cakephp/utility/MergeVariablesTrait.php',
'Cake\\Utility\\Security' => $vendorDir . '/cakephp/utility/Security.php',
'Cake\\Utility\\Text' => $vendorDir . '/cakephp/utility/Text.php',
'Cake\\Utility\\Xml' => $vendorDir . '/cakephp/utility/Xml.php',
'Carbon\\AbstractTranslator' => $vendorDir . '/nesbot/carbon/src/Carbon/AbstractTranslator.php',
'Carbon\\Callback' => $vendorDir . '/nesbot/carbon/src/Carbon/Callback.php',
'Carbon\\Carbon' => $vendorDir . '/nesbot/carbon/src/Carbon/Carbon.php',
@@ -3603,55 +3413,6 @@ return array(
'League\\Config\\MutableConfigurationInterface' => $vendorDir . '/league/config/src/MutableConfigurationInterface.php',
'League\\Config\\ReadOnlyConfiguration' => $vendorDir . '/league/config/src/ReadOnlyConfiguration.php',
'League\\Config\\SchemaBuilderInterface' => $vendorDir . '/league/config/src/SchemaBuilderInterface.php',
'League\\Container\\Argument\\ArgumentInterface' => $vendorDir . '/league/container/src/Argument/ArgumentInterface.php',
'League\\Container\\Argument\\ArgumentReflectorInterface' => $vendorDir . '/league/container/src/Argument/ArgumentReflectorInterface.php',
'League\\Container\\Argument\\ArgumentReflectorTrait' => $vendorDir . '/league/container/src/Argument/ArgumentReflectorTrait.php',
'League\\Container\\Argument\\ArgumentResolverInterface' => $vendorDir . '/league/container/src/Argument/ArgumentResolverInterface.php',
'League\\Container\\Argument\\ArgumentResolverTrait' => $vendorDir . '/league/container/src/Argument/ArgumentResolverTrait.php',
'League\\Container\\Argument\\DefaultValueArgument' => $vendorDir . '/league/container/src/Argument/DefaultValueArgument.php',
'League\\Container\\Argument\\DefaultValueInterface' => $vendorDir . '/league/container/src/Argument/DefaultValueInterface.php',
'League\\Container\\Argument\\LiteralArgument' => $vendorDir . '/league/container/src/Argument/LiteralArgument.php',
'League\\Container\\Argument\\LiteralArgumentInterface' => $vendorDir . '/league/container/src/Argument/LiteralArgumentInterface.php',
'League\\Container\\Argument\\Literal\\ArrayArgument' => $vendorDir . '/league/container/src/Argument/Literal/ArrayArgument.php',
'League\\Container\\Argument\\Literal\\BooleanArgument' => $vendorDir . '/league/container/src/Argument/Literal/BooleanArgument.php',
'League\\Container\\Argument\\Literal\\CallableArgument' => $vendorDir . '/league/container/src/Argument/Literal/CallableArgument.php',
'League\\Container\\Argument\\Literal\\FloatArgument' => $vendorDir . '/league/container/src/Argument/Literal/FloatArgument.php',
'League\\Container\\Argument\\Literal\\IntegerArgument' => $vendorDir . '/league/container/src/Argument/Literal/IntegerArgument.php',
'League\\Container\\Argument\\Literal\\ObjectArgument' => $vendorDir . '/league/container/src/Argument/Literal/ObjectArgument.php',
'League\\Container\\Argument\\Literal\\StringArgument' => $vendorDir . '/league/container/src/Argument/Literal/StringArgument.php',
'League\\Container\\Argument\\ResolvableArgument' => $vendorDir . '/league/container/src/Argument/ResolvableArgument.php',
'League\\Container\\Argument\\ResolvableArgumentInterface' => $vendorDir . '/league/container/src/Argument/ResolvableArgumentInterface.php',
'League\\Container\\Attribute\\AttributeInterface' => $vendorDir . '/league/container/src/Attribute/AttributeInterface.php',
'League\\Container\\Attribute\\Inject' => $vendorDir . '/league/container/src/Attribute/Inject.php',
'League\\Container\\Attribute\\Resolve' => $vendorDir . '/league/container/src/Attribute/Resolve.php',
'League\\Container\\Container' => $vendorDir . '/league/container/src/Container.php',
'League\\Container\\ContainerAwareInterface' => $vendorDir . '/league/container/src/ContainerAwareInterface.php',
'League\\Container\\ContainerAwareTrait' => $vendorDir . '/league/container/src/ContainerAwareTrait.php',
'League\\Container\\DefinitionContainerInterface' => $vendorDir . '/league/container/src/DefinitionContainerInterface.php',
'League\\Container\\Definition\\Definition' => $vendorDir . '/league/container/src/Definition/Definition.php',
'League\\Container\\Definition\\DefinitionAggregate' => $vendorDir . '/league/container/src/Definition/DefinitionAggregate.php',
'League\\Container\\Definition\\DefinitionAggregateInterface' => $vendorDir . '/league/container/src/Definition/DefinitionAggregateInterface.php',
'League\\Container\\Definition\\DefinitionInterface' => $vendorDir . '/league/container/src/Definition/DefinitionInterface.php',
'League\\Container\\Event\\BeforeResolveEvent' => $vendorDir . '/league/container/src/Event/BeforeResolveEvent.php',
'League\\Container\\Event\\ContainerEvent' => $vendorDir . '/league/container/src/Event/ContainerEvent.php',
'League\\Container\\Event\\DefinitionResolvedEvent' => $vendorDir . '/league/container/src/Event/DefinitionResolvedEvent.php',
'League\\Container\\Event\\EventAwareTrait' => $vendorDir . '/league/container/src/Event/EventAwareTrait.php',
'League\\Container\\Event\\EventDispatcher' => $vendorDir . '/league/container/src/Event/EventDispatcher.php',
'League\\Container\\Event\\EventFilter' => $vendorDir . '/league/container/src/Event/EventFilter.php',
'League\\Container\\Event\\OnDefineEvent' => $vendorDir . '/league/container/src/Event/OnDefineEvent.php',
'League\\Container\\Event\\ServiceResolvedEvent' => $vendorDir . '/league/container/src/Event/ServiceResolvedEvent.php',
'League\\Container\\Exception\\ContainerException' => $vendorDir . '/league/container/src/Exception/ContainerException.php',
'League\\Container\\Exception\\NotFoundException' => $vendorDir . '/league/container/src/Exception/NotFoundException.php',
'League\\Container\\Inflector\\Inflector' => $vendorDir . '/league/container/src/Inflector/Inflector.php',
'League\\Container\\Inflector\\InflectorAggregate' => $vendorDir . '/league/container/src/Inflector/InflectorAggregate.php',
'League\\Container\\Inflector\\InflectorAggregateInterface' => $vendorDir . '/league/container/src/Inflector/InflectorAggregateInterface.php',
'League\\Container\\Inflector\\InflectorInterface' => $vendorDir . '/league/container/src/Inflector/InflectorInterface.php',
'League\\Container\\ReflectionContainer' => $vendorDir . '/league/container/src/ReflectionContainer.php',
'League\\Container\\ServiceProvider\\AbstractServiceProvider' => $vendorDir . '/league/container/src/ServiceProvider/AbstractServiceProvider.php',
'League\\Container\\ServiceProvider\\BootableServiceProviderInterface' => $vendorDir . '/league/container/src/ServiceProvider/BootableServiceProviderInterface.php',
'League\\Container\\ServiceProvider\\ServiceProviderAggregate' => $vendorDir . '/league/container/src/ServiceProvider/ServiceProviderAggregate.php',
'League\\Container\\ServiceProvider\\ServiceProviderAggregateInterface' => $vendorDir . '/league/container/src/ServiceProvider/ServiceProviderAggregateInterface.php',
'League\\Container\\ServiceProvider\\ServiceProviderInterface' => $vendorDir . '/league/container/src/ServiceProvider/ServiceProviderInterface.php',
'League\\Flysystem\\CalculateChecksumFromStream' => $vendorDir . '/league/flysystem/src/CalculateChecksumFromStream.php',
'League\\Flysystem\\ChecksumAlgoIsNotSupported' => $vendorDir . '/league/flysystem/src/ChecksumAlgoIsNotSupported.php',
'League\\Flysystem\\ChecksumProvider' => $vendorDir . '/league/flysystem/src/ChecksumProvider.php',
@@ -5160,76 +4921,6 @@ return array(
'PharIo\\Version\\VersionConstraintParser' => $vendorDir . '/phar-io/version/src/VersionConstraintParser.php',
'PharIo\\Version\\VersionConstraintValue' => $vendorDir . '/phar-io/version/src/VersionConstraintValue.php',
'PharIo\\Version\\VersionNumber' => $vendorDir . '/phar-io/version/src/VersionNumber.php',
'Phinx\\Config\\Config' => $vendorDir . '/robmorgan/phinx/src/Phinx/Config/Config.php',
'Phinx\\Config\\ConfigInterface' => $vendorDir . '/robmorgan/phinx/src/Phinx/Config/ConfigInterface.php',
'Phinx\\Config\\FeatureFlags' => $vendorDir . '/robmorgan/phinx/src/Phinx/Config/FeatureFlags.php',
'Phinx\\Config\\NamespaceAwareInterface' => $vendorDir . '/robmorgan/phinx/src/Phinx/Config/NamespaceAwareInterface.php',
'Phinx\\Config\\NamespaceAwareTrait' => $vendorDir . '/robmorgan/phinx/src/Phinx/Config/NamespaceAwareTrait.php',
'Phinx\\Console\\Command\\AbstractCommand' => $vendorDir . '/robmorgan/phinx/src/Phinx/Console/Command/AbstractCommand.php',
'Phinx\\Console\\Command\\Breakpoint' => $vendorDir . '/robmorgan/phinx/src/Phinx/Console/Command/Breakpoint.php',
'Phinx\\Console\\Command\\Create' => $vendorDir . '/robmorgan/phinx/src/Phinx/Console/Command/Create.php',
'Phinx\\Console\\Command\\Init' => $vendorDir . '/robmorgan/phinx/src/Phinx/Console/Command/Init.php',
'Phinx\\Console\\Command\\ListAliases' => $vendorDir . '/robmorgan/phinx/src/Phinx/Console/Command/ListAliases.php',
'Phinx\\Console\\Command\\Migrate' => $vendorDir . '/robmorgan/phinx/src/Phinx/Console/Command/Migrate.php',
'Phinx\\Console\\Command\\Rollback' => $vendorDir . '/robmorgan/phinx/src/Phinx/Console/Command/Rollback.php',
'Phinx\\Console\\Command\\SeedCreate' => $vendorDir . '/robmorgan/phinx/src/Phinx/Console/Command/SeedCreate.php',
'Phinx\\Console\\Command\\SeedRun' => $vendorDir . '/robmorgan/phinx/src/Phinx/Console/Command/SeedRun.php',
'Phinx\\Console\\Command\\Status' => $vendorDir . '/robmorgan/phinx/src/Phinx/Console/Command/Status.php',
'Phinx\\Console\\Command\\Test' => $vendorDir . '/robmorgan/phinx/src/Phinx/Console/Command/Test.php',
'Phinx\\Console\\PhinxApplication' => $vendorDir . '/robmorgan/phinx/src/Phinx/Console/PhinxApplication.php',
'Phinx\\Db\\Action\\Action' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Action/Action.php',
'Phinx\\Db\\Action\\AddColumn' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Action/AddColumn.php',
'Phinx\\Db\\Action\\AddForeignKey' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Action/AddForeignKey.php',
'Phinx\\Db\\Action\\AddIndex' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Action/AddIndex.php',
'Phinx\\Db\\Action\\ChangeColumn' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Action/ChangeColumn.php',
'Phinx\\Db\\Action\\ChangeComment' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Action/ChangeComment.php',
'Phinx\\Db\\Action\\ChangePrimaryKey' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Action/ChangePrimaryKey.php',
'Phinx\\Db\\Action\\CreateTable' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Action/CreateTable.php',
'Phinx\\Db\\Action\\DropForeignKey' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Action/DropForeignKey.php',
'Phinx\\Db\\Action\\DropIndex' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Action/DropIndex.php',
'Phinx\\Db\\Action\\DropTable' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Action/DropTable.php',
'Phinx\\Db\\Action\\RemoveColumn' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Action/RemoveColumn.php',
'Phinx\\Db\\Action\\RenameColumn' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Action/RenameColumn.php',
'Phinx\\Db\\Action\\RenameTable' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Action/RenameTable.php',
'Phinx\\Db\\Adapter\\AbstractAdapter' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Adapter/AbstractAdapter.php',
'Phinx\\Db\\Adapter\\AdapterFactory' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Adapter/AdapterFactory.php',
'Phinx\\Db\\Adapter\\AdapterInterface' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Adapter/AdapterInterface.php',
'Phinx\\Db\\Adapter\\AdapterWrapper' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Adapter/AdapterWrapper.php',
'Phinx\\Db\\Adapter\\DirectActionInterface' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Adapter/DirectActionInterface.php',
'Phinx\\Db\\Adapter\\MysqlAdapter' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Adapter/MysqlAdapter.php',
'Phinx\\Db\\Adapter\\PdoAdapter' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Adapter/PdoAdapter.php',
'Phinx\\Db\\Adapter\\PostgresAdapter' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Adapter/PostgresAdapter.php',
'Phinx\\Db\\Adapter\\ProxyAdapter' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Adapter/ProxyAdapter.php',
'Phinx\\Db\\Adapter\\SQLiteAdapter' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Adapter/SQLiteAdapter.php',
'Phinx\\Db\\Adapter\\SqlServerAdapter' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Adapter/SqlServerAdapter.php',
'Phinx\\Db\\Adapter\\TablePrefixAdapter' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Adapter/TablePrefixAdapter.php',
'Phinx\\Db\\Adapter\\TimedOutputAdapter' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Adapter/TimedOutputAdapter.php',
'Phinx\\Db\\Adapter\\UnsupportedColumnTypeException' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Adapter/UnsupportedColumnTypeException.php',
'Phinx\\Db\\Adapter\\WrapperInterface' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Adapter/WrapperInterface.php',
'Phinx\\Db\\Plan\\AlterTable' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Plan/AlterTable.php',
'Phinx\\Db\\Plan\\Intent' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Plan/Intent.php',
'Phinx\\Db\\Plan\\NewTable' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Plan/NewTable.php',
'Phinx\\Db\\Plan\\Plan' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Plan/Plan.php',
'Phinx\\Db\\Plan\\Solver\\ActionSplitter' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Plan/Solver/ActionSplitter.php',
'Phinx\\Db\\Table' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Table.php',
'Phinx\\Db\\Table\\Column' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Table/Column.php',
'Phinx\\Db\\Table\\ForeignKey' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Table/ForeignKey.php',
'Phinx\\Db\\Table\\Index' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Table/Index.php',
'Phinx\\Db\\Table\\Table' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Table/Table.php',
'Phinx\\Db\\Util\\AlterInstructions' => $vendorDir . '/robmorgan/phinx/src/Phinx/Db/Util/AlterInstructions.php',
'Phinx\\Migration\\AbstractMigration' => $vendorDir . '/robmorgan/phinx/src/Phinx/Migration/AbstractMigration.php',
'Phinx\\Migration\\AbstractTemplateCreation' => $vendorDir . '/robmorgan/phinx/src/Phinx/Migration/AbstractTemplateCreation.php',
'Phinx\\Migration\\CreationInterface' => $vendorDir . '/robmorgan/phinx/src/Phinx/Migration/CreationInterface.php',
'Phinx\\Migration\\IrreversibleMigrationException' => $vendorDir . '/robmorgan/phinx/src/Phinx/Migration/IrreversibleMigrationException.php',
'Phinx\\Migration\\Manager' => $vendorDir . '/robmorgan/phinx/src/Phinx/Migration/Manager.php',
'Phinx\\Migration\\Manager\\Environment' => $vendorDir . '/robmorgan/phinx/src/Phinx/Migration/Manager/Environment.php',
'Phinx\\Migration\\MigrationInterface' => $vendorDir . '/robmorgan/phinx/src/Phinx/Migration/MigrationInterface.php',
'Phinx\\Seed\\AbstractSeed' => $vendorDir . '/robmorgan/phinx/src/Phinx/Seed/AbstractSeed.php',
'Phinx\\Seed\\SeedInterface' => $vendorDir . '/robmorgan/phinx/src/Phinx/Seed/SeedInterface.php',
'Phinx\\Util\\Expression' => $vendorDir . '/robmorgan/phinx/src/Phinx/Util/Expression.php',
'Phinx\\Util\\Literal' => $vendorDir . '/robmorgan/phinx/src/Phinx/Util/Literal.php',
'Phinx\\Util\\Util' => $vendorDir . '/robmorgan/phinx/src/Phinx/Util/Util.php',
'Phinx\\Wrapper\\TextWrapper' => $vendorDir . '/robmorgan/phinx/src/Phinx/Wrapper/TextWrapper.php',
'PhpOffice\\PhpSpreadsheet\\Calculation\\ArrayEnabled' => $vendorDir . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/ArrayEnabled.php',
'PhpOffice\\PhpSpreadsheet\\Calculation\\BinaryComparison' => $vendorDir . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/BinaryComparison.php',
'PhpOffice\\PhpSpreadsheet\\Calculation\\Calculation' => $vendorDir . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Calculation.php',
@@ -6970,96 +6661,6 @@ return array(
'Symfony\\Component\\Clock\\MonotonicClock' => $vendorDir . '/symfony/clock/MonotonicClock.php',
'Symfony\\Component\\Clock\\NativeClock' => $vendorDir . '/symfony/clock/NativeClock.php',
'Symfony\\Component\\Clock\\Test\\ClockSensitiveTrait' => $vendorDir . '/symfony/clock/Test/ClockSensitiveTrait.php',
'Symfony\\Component\\Config\\Builder\\ClassBuilder' => $vendorDir . '/symfony/config/Builder/ClassBuilder.php',
'Symfony\\Component\\Config\\Builder\\ConfigBuilderGenerator' => $vendorDir . '/symfony/config/Builder/ConfigBuilderGenerator.php',
'Symfony\\Component\\Config\\Builder\\ConfigBuilderGeneratorInterface' => $vendorDir . '/symfony/config/Builder/ConfigBuilderGeneratorInterface.php',
'Symfony\\Component\\Config\\Builder\\ConfigBuilderInterface' => $vendorDir . '/symfony/config/Builder/ConfigBuilderInterface.php',
'Symfony\\Component\\Config\\Builder\\Method' => $vendorDir . '/symfony/config/Builder/Method.php',
'Symfony\\Component\\Config\\Builder\\Property' => $vendorDir . '/symfony/config/Builder/Property.php',
'Symfony\\Component\\Config\\ConfigCache' => $vendorDir . '/symfony/config/ConfigCache.php',
'Symfony\\Component\\Config\\ConfigCacheFactory' => $vendorDir . '/symfony/config/ConfigCacheFactory.php',
'Symfony\\Component\\Config\\ConfigCacheFactoryInterface' => $vendorDir . '/symfony/config/ConfigCacheFactoryInterface.php',
'Symfony\\Component\\Config\\ConfigCacheInterface' => $vendorDir . '/symfony/config/ConfigCacheInterface.php',
'Symfony\\Component\\Config\\Definition\\ArrayNode' => $vendorDir . '/symfony/config/Definition/ArrayNode.php',
'Symfony\\Component\\Config\\Definition\\ArrayShapeGenerator' => $vendorDir . '/symfony/config/Definition/ArrayShapeGenerator.php',
'Symfony\\Component\\Config\\Definition\\BaseNode' => $vendorDir . '/symfony/config/Definition/BaseNode.php',
'Symfony\\Component\\Config\\Definition\\BooleanNode' => $vendorDir . '/symfony/config/Definition/BooleanNode.php',
'Symfony\\Component\\Config\\Definition\\Builder\\ArrayNodeDefinition' => $vendorDir . '/symfony/config/Definition/Builder/ArrayNodeDefinition.php',
'Symfony\\Component\\Config\\Definition\\Builder\\BooleanNodeDefinition' => $vendorDir . '/symfony/config/Definition/Builder/BooleanNodeDefinition.php',
'Symfony\\Component\\Config\\Definition\\Builder\\BuilderAwareInterface' => $vendorDir . '/symfony/config/Definition/Builder/BuilderAwareInterface.php',
'Symfony\\Component\\Config\\Definition\\Builder\\EnumNodeDefinition' => $vendorDir . '/symfony/config/Definition/Builder/EnumNodeDefinition.php',
'Symfony\\Component\\Config\\Definition\\Builder\\ExprBuilder' => $vendorDir . '/symfony/config/Definition/Builder/ExprBuilder.php',
'Symfony\\Component\\Config\\Definition\\Builder\\FloatNodeDefinition' => $vendorDir . '/symfony/config/Definition/Builder/FloatNodeDefinition.php',
'Symfony\\Component\\Config\\Definition\\Builder\\IntegerNodeDefinition' => $vendorDir . '/symfony/config/Definition/Builder/IntegerNodeDefinition.php',
'Symfony\\Component\\Config\\Definition\\Builder\\MergeBuilder' => $vendorDir . '/symfony/config/Definition/Builder/MergeBuilder.php',
'Symfony\\Component\\Config\\Definition\\Builder\\NodeBuilder' => $vendorDir . '/symfony/config/Definition/Builder/NodeBuilder.php',
'Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition' => $vendorDir . '/symfony/config/Definition/Builder/NodeDefinition.php',
'Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface' => $vendorDir . '/symfony/config/Definition/Builder/NodeParentInterface.php',
'Symfony\\Component\\Config\\Definition\\Builder\\NormalizationBuilder' => $vendorDir . '/symfony/config/Definition/Builder/NormalizationBuilder.php',
'Symfony\\Component\\Config\\Definition\\Builder\\NumericNodeDefinition' => $vendorDir . '/symfony/config/Definition/Builder/NumericNodeDefinition.php',
'Symfony\\Component\\Config\\Definition\\Builder\\ParentNodeDefinitionInterface' => $vendorDir . '/symfony/config/Definition/Builder/ParentNodeDefinitionInterface.php',
'Symfony\\Component\\Config\\Definition\\Builder\\ScalarNodeDefinition' => $vendorDir . '/symfony/config/Definition/Builder/ScalarNodeDefinition.php',
'Symfony\\Component\\Config\\Definition\\Builder\\StringNodeDefinition' => $vendorDir . '/symfony/config/Definition/Builder/StringNodeDefinition.php',
'Symfony\\Component\\Config\\Definition\\Builder\\TreeBuilder' => $vendorDir . '/symfony/config/Definition/Builder/TreeBuilder.php',
'Symfony\\Component\\Config\\Definition\\Builder\\ValidationBuilder' => $vendorDir . '/symfony/config/Definition/Builder/ValidationBuilder.php',
'Symfony\\Component\\Config\\Definition\\Builder\\VariableNodeDefinition' => $vendorDir . '/symfony/config/Definition/Builder/VariableNodeDefinition.php',
'Symfony\\Component\\Config\\Definition\\ConfigurableInterface' => $vendorDir . '/symfony/config/Definition/ConfigurableInterface.php',
'Symfony\\Component\\Config\\Definition\\Configuration' => $vendorDir . '/symfony/config/Definition/Configuration.php',
'Symfony\\Component\\Config\\Definition\\ConfigurationInterface' => $vendorDir . '/symfony/config/Definition/ConfigurationInterface.php',
'Symfony\\Component\\Config\\Definition\\Configurator\\DefinitionConfigurator' => $vendorDir . '/symfony/config/Definition/Configurator/DefinitionConfigurator.php',
'Symfony\\Component\\Config\\Definition\\Dumper\\XmlReferenceDumper' => $vendorDir . '/symfony/config/Definition/Dumper/XmlReferenceDumper.php',
'Symfony\\Component\\Config\\Definition\\Dumper\\YamlReferenceDumper' => $vendorDir . '/symfony/config/Definition/Dumper/YamlReferenceDumper.php',
'Symfony\\Component\\Config\\Definition\\EnumNode' => $vendorDir . '/symfony/config/Definition/EnumNode.php',
'Symfony\\Component\\Config\\Definition\\Exception\\DuplicateKeyException' => $vendorDir . '/symfony/config/Definition/Exception/DuplicateKeyException.php',
'Symfony\\Component\\Config\\Definition\\Exception\\Exception' => $vendorDir . '/symfony/config/Definition/Exception/Exception.php',
'Symfony\\Component\\Config\\Definition\\Exception\\ForbiddenOverwriteException' => $vendorDir . '/symfony/config/Definition/Exception/ForbiddenOverwriteException.php',
'Symfony\\Component\\Config\\Definition\\Exception\\InvalidConfigurationException' => $vendorDir . '/symfony/config/Definition/Exception/InvalidConfigurationException.php',
'Symfony\\Component\\Config\\Definition\\Exception\\InvalidDefinitionException' => $vendorDir . '/symfony/config/Definition/Exception/InvalidDefinitionException.php',
'Symfony\\Component\\Config\\Definition\\Exception\\InvalidTypeException' => $vendorDir . '/symfony/config/Definition/Exception/InvalidTypeException.php',
'Symfony\\Component\\Config\\Definition\\Exception\\UnsetKeyException' => $vendorDir . '/symfony/config/Definition/Exception/UnsetKeyException.php',
'Symfony\\Component\\Config\\Definition\\FloatNode' => $vendorDir . '/symfony/config/Definition/FloatNode.php',
'Symfony\\Component\\Config\\Definition\\IntegerNode' => $vendorDir . '/symfony/config/Definition/IntegerNode.php',
'Symfony\\Component\\Config\\Definition\\Loader\\DefinitionFileLoader' => $vendorDir . '/symfony/config/Definition/Loader/DefinitionFileLoader.php',
'Symfony\\Component\\Config\\Definition\\NodeInterface' => $vendorDir . '/symfony/config/Definition/NodeInterface.php',
'Symfony\\Component\\Config\\Definition\\NumericNode' => $vendorDir . '/symfony/config/Definition/NumericNode.php',
'Symfony\\Component\\Config\\Definition\\Processor' => $vendorDir . '/symfony/config/Definition/Processor.php',
'Symfony\\Component\\Config\\Definition\\PrototypeNodeInterface' => $vendorDir . '/symfony/config/Definition/PrototypeNodeInterface.php',
'Symfony\\Component\\Config\\Definition\\PrototypedArrayNode' => $vendorDir . '/symfony/config/Definition/PrototypedArrayNode.php',
'Symfony\\Component\\Config\\Definition\\ScalarNode' => $vendorDir . '/symfony/config/Definition/ScalarNode.php',
'Symfony\\Component\\Config\\Definition\\StringNode' => $vendorDir . '/symfony/config/Definition/StringNode.php',
'Symfony\\Component\\Config\\Definition\\VariableNode' => $vendorDir . '/symfony/config/Definition/VariableNode.php',
'Symfony\\Component\\Config\\Exception\\FileLoaderImportCircularReferenceException' => $vendorDir . '/symfony/config/Exception/FileLoaderImportCircularReferenceException.php',
'Symfony\\Component\\Config\\Exception\\FileLocatorFileNotFoundException' => $vendorDir . '/symfony/config/Exception/FileLocatorFileNotFoundException.php',
'Symfony\\Component\\Config\\Exception\\LoaderLoadException' => $vendorDir . '/symfony/config/Exception/LoaderLoadException.php',
'Symfony\\Component\\Config\\Exception\\LogicException' => $vendorDir . '/symfony/config/Exception/LogicException.php',
'Symfony\\Component\\Config\\FileLocator' => $vendorDir . '/symfony/config/FileLocator.php',
'Symfony\\Component\\Config\\FileLocatorInterface' => $vendorDir . '/symfony/config/FileLocatorInterface.php',
'Symfony\\Component\\Config\\Loader\\DelegatingLoader' => $vendorDir . '/symfony/config/Loader/DelegatingLoader.php',
'Symfony\\Component\\Config\\Loader\\DirectoryAwareLoaderInterface' => $vendorDir . '/symfony/config/Loader/DirectoryAwareLoaderInterface.php',
'Symfony\\Component\\Config\\Loader\\FileLoader' => $vendorDir . '/symfony/config/Loader/FileLoader.php',
'Symfony\\Component\\Config\\Loader\\GlobFileLoader' => $vendorDir . '/symfony/config/Loader/GlobFileLoader.php',
'Symfony\\Component\\Config\\Loader\\Loader' => $vendorDir . '/symfony/config/Loader/Loader.php',
'Symfony\\Component\\Config\\Loader\\LoaderInterface' => $vendorDir . '/symfony/config/Loader/LoaderInterface.php',
'Symfony\\Component\\Config\\Loader\\LoaderResolver' => $vendorDir . '/symfony/config/Loader/LoaderResolver.php',
'Symfony\\Component\\Config\\Loader\\LoaderResolverInterface' => $vendorDir . '/symfony/config/Loader/LoaderResolverInterface.php',
'Symfony\\Component\\Config\\Loader\\ParamConfigurator' => $vendorDir . '/symfony/config/Loader/ParamConfigurator.php',
'Symfony\\Component\\Config\\ResourceCheckerConfigCache' => $vendorDir . '/symfony/config/ResourceCheckerConfigCache.php',
'Symfony\\Component\\Config\\ResourceCheckerConfigCacheFactory' => $vendorDir . '/symfony/config/ResourceCheckerConfigCacheFactory.php',
'Symfony\\Component\\Config\\ResourceCheckerInterface' => $vendorDir . '/symfony/config/ResourceCheckerInterface.php',
'Symfony\\Component\\Config\\Resource\\ClassExistenceResource' => $vendorDir . '/symfony/config/Resource/ClassExistenceResource.php',
'Symfony\\Component\\Config\\Resource\\ComposerResource' => $vendorDir . '/symfony/config/Resource/ComposerResource.php',
'Symfony\\Component\\Config\\Resource\\DirectoryResource' => $vendorDir . '/symfony/config/Resource/DirectoryResource.php',
'Symfony\\Component\\Config\\Resource\\FileExistenceResource' => $vendorDir . '/symfony/config/Resource/FileExistenceResource.php',
'Symfony\\Component\\Config\\Resource\\FileResource' => $vendorDir . '/symfony/config/Resource/FileResource.php',
'Symfony\\Component\\Config\\Resource\\GlobResource' => $vendorDir . '/symfony/config/Resource/GlobResource.php',
'Symfony\\Component\\Config\\Resource\\ReflectionClassResource' => $vendorDir . '/symfony/config/Resource/ReflectionClassResource.php',
'Symfony\\Component\\Config\\Resource\\ResourceInterface' => $vendorDir . '/symfony/config/Resource/ResourceInterface.php',
'Symfony\\Component\\Config\\Resource\\SelfCheckingResourceChecker' => $vendorDir . '/symfony/config/Resource/SelfCheckingResourceChecker.php',
'Symfony\\Component\\Config\\Resource\\SelfCheckingResourceInterface' => $vendorDir . '/symfony/config/Resource/SelfCheckingResourceInterface.php',
'Symfony\\Component\\Config\\Resource\\SkippingResourceChecker' => $vendorDir . '/symfony/config/Resource/SkippingResourceChecker.php',
'Symfony\\Component\\Config\\Util\\Exception\\InvalidXmlException' => $vendorDir . '/symfony/config/Util/Exception/InvalidXmlException.php',
'Symfony\\Component\\Config\\Util\\Exception\\XmlParsingException' => $vendorDir . '/symfony/config/Util/Exception/XmlParsingException.php',
'Symfony\\Component\\Config\\Util\\XmlUtils' => $vendorDir . '/symfony/config/Util/XmlUtils.php',
'Symfony\\Component\\Console\\Application' => $vendorDir . '/symfony/console/Application.php',
'Symfony\\Component\\Console\\Attribute\\Argument' => $vendorDir . '/symfony/console/Attribute/Argument.php',
'Symfony\\Component\\Console\\Attribute\\AsCommand' => $vendorDir . '/symfony/console/Attribute/AsCommand.php',
@@ -7268,14 +6869,6 @@ return array(
'Symfony\\Component\\EventDispatcher\\EventSubscriberInterface' => $vendorDir . '/symfony/event-dispatcher/EventSubscriberInterface.php',
'Symfony\\Component\\EventDispatcher\\GenericEvent' => $vendorDir . '/symfony/event-dispatcher/GenericEvent.php',
'Symfony\\Component\\EventDispatcher\\ImmutableEventDispatcher' => $vendorDir . '/symfony/event-dispatcher/ImmutableEventDispatcher.php',
'Symfony\\Component\\Filesystem\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/filesystem/Exception/ExceptionInterface.php',
'Symfony\\Component\\Filesystem\\Exception\\FileNotFoundException' => $vendorDir . '/symfony/filesystem/Exception/FileNotFoundException.php',
'Symfony\\Component\\Filesystem\\Exception\\IOException' => $vendorDir . '/symfony/filesystem/Exception/IOException.php',
'Symfony\\Component\\Filesystem\\Exception\\IOExceptionInterface' => $vendorDir . '/symfony/filesystem/Exception/IOExceptionInterface.php',
'Symfony\\Component\\Filesystem\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/filesystem/Exception/InvalidArgumentException.php',
'Symfony\\Component\\Filesystem\\Exception\\RuntimeException' => $vendorDir . '/symfony/filesystem/Exception/RuntimeException.php',
'Symfony\\Component\\Filesystem\\Filesystem' => $vendorDir . '/symfony/filesystem/Filesystem.php',
'Symfony\\Component\\Filesystem\\Path' => $vendorDir . '/symfony/filesystem/Path.php',
'Symfony\\Component\\Finder\\Comparator\\Comparator' => $vendorDir . '/symfony/finder/Comparator/Comparator.php',
'Symfony\\Component\\Finder\\Comparator\\DateComparator' => $vendorDir . '/symfony/finder/Comparator/DateComparator.php',
'Symfony\\Component\\Finder\\Comparator\\NumberComparator' => $vendorDir . '/symfony/finder/Comparator/NumberComparator.php',
+1 -3
View File
@@ -11,15 +11,14 @@ return array(
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
'662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
'25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'72142d7b40a3a0b14e91825290b5ad82' => $vendorDir . '/cakephp/core/functions.php',
'35a6ad97d21e794e7e22a17d806652e4' => $vendorDir . '/nunomaduro/termwind/src/Functions.php',
'2203a247e6fda86070a5e4e07aed533a' => $vendorDir . '/symfony/clock/Resources/now.php',
'09f6b20656683369174dd6fa83b7e5fb' => $vendorDir . '/symfony/polyfill-uuid/bootstrap.php',
@@ -34,7 +33,6 @@ return array(
'58571171fd5812e6e447dce228f52f4d' => $vendorDir . '/laravel/framework/src/Illuminate/Support/helpers.php',
'6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
'3bd81c9b8fcc150b69d8b63b4d2ccf23' => $vendorDir . '/spatie/flare-client-php/src/helpers.php',
'948ad5488880985ff1c06721a4e447fe' => $vendorDir . '/cakephp/utility/bootstrap.php',
'ec07570ca5a812141189b1fa81503674' => $vendorDir . '/phpunit/phpunit/src/Framework/Assert/Functions.php',
'801c31d8ed748cfa537fa45402288c95' => $vendorDir . '/psy/psysh/src/functions.php',
'b84a5ba6d9a8ee8cf648b71c42c799b7' => $vendorDir . '/akaunting/laravel-setting/src/helpers.php',
+3 -12
View File
@@ -8,7 +8,7 @@ $baseDir = dirname($vendorDir);
return array(
'voku\\' => array($vendorDir . '/voku/portable-ascii/src/voku'),
'phpseclib3\\' => array($vendorDir . '/phpseclib/phpseclib/phpseclib'),
'phpDocumentor\\Reflection\\' => array($vendorDir . '/phpdocumentor/type-resolver/src', $vendorDir . '/phpdocumentor/reflection-common/src'),
'phpDocumentor\\Reflection\\' => array($vendorDir . '/phpdocumentor/reflection-common/src', $vendorDir . '/phpdocumentor/type-resolver/src'),
'ZipStream\\' => array($vendorDir . '/maennchen/zipstream-php/src'),
'Whoops\\' => array($vendorDir . '/filp/whoops/src/Whoops'),
'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'),
@@ -47,18 +47,16 @@ return array(
'Symfony\\Component\\HttpKernel\\' => array($vendorDir . '/symfony/http-kernel'),
'Symfony\\Component\\HttpFoundation\\' => array($vendorDir . '/symfony/http-foundation'),
'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'),
'Symfony\\Component\\Filesystem\\' => array($vendorDir . '/symfony/filesystem'),
'Symfony\\Component\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher'),
'Symfony\\Component\\ErrorHandler\\' => array($vendorDir . '/symfony/error-handler'),
'Symfony\\Component\\CssSelector\\' => array($vendorDir . '/symfony/css-selector'),
'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'),
'Symfony\\Component\\Config\\' => array($vendorDir . '/symfony/config'),
'Symfony\\Component\\Clock\\' => array($vendorDir . '/symfony/clock'),
'Spatie\\QueryBuilder\\Database\\Factories\\' => array($vendorDir . '/spatie/laravel-query-builder/database/factories'),
'Spatie\\QueryBuilder\\' => array($vendorDir . '/spatie/laravel-query-builder/src'),
'Spatie\\LaravelPackageTools\\' => array($vendorDir . '/spatie/laravel-package-tools/src'),
'Spatie\\LaravelIgnition\\' => array($vendorDir . '/spatie/laravel-ignition/src', $vendorDir . '/spatie/error-solutions/legacy/laravel-ignition'),
'Spatie\\Ignition\\' => array($vendorDir . '/spatie/ignition/src', $vendorDir . '/spatie/error-solutions/legacy/ignition'),
'Spatie\\LaravelIgnition\\' => array($vendorDir . '/spatie/error-solutions/legacy/laravel-ignition', $vendorDir . '/spatie/laravel-ignition/src'),
'Spatie\\Ignition\\' => array($vendorDir . '/spatie/error-solutions/legacy/ignition', $vendorDir . '/spatie/ignition/src'),
'Spatie\\FlareClient\\' => array($vendorDir . '/spatie/flare-client-php/src'),
'Spatie\\ErrorSolutions\\' => array($vendorDir . '/spatie/error-solutions/src'),
'Spatie\\Backtrace\\' => array($vendorDir . '/spatie/backtrace/src'),
@@ -79,7 +77,6 @@ return array(
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'),
'PhpOffice\\PhpSpreadsheet\\' => array($vendorDir . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet'),
'Phinx\\' => array($vendorDir . '/robmorgan/phinx/src/Phinx'),
'ParagonIE\\ConstantTime\\' => array($vendorDir . '/paragonie/constant_time_encoding/src'),
'ParaTest\\' => array($vendorDir . '/brianium/paratest/src'),
'PHPStan\\PhpDocParser\\' => array($vendorDir . '/phpstan/phpdoc-parser/src'),
@@ -92,7 +89,6 @@ return array(
'League\\MimeTypeDetection\\' => array($vendorDir . '/league/mime-type-detection/src'),
'League\\Flysystem\\Local\\' => array($vendorDir . '/league/flysystem-local'),
'League\\Flysystem\\' => array($vendorDir . '/league/flysystem/src'),
'League\\Container\\' => array($vendorDir . '/league/container/src'),
'League\\Config\\' => array($vendorDir . '/league/config/src'),
'League\\CommonMark\\' => array($vendorDir . '/league/commonmark/src'),
'Laravel\\Ui\\' => array($vendorDir . '/laravel/ui/src'),
@@ -138,11 +134,6 @@ return array(
'Complex\\' => array($vendorDir . '/markbaker/complex/classes/src'),
'Carbon\\Doctrine\\' => array($vendorDir . '/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine'),
'Carbon\\' => array($vendorDir . '/nesbot/carbon/src/Carbon'),
'Cake\\Utility\\' => array($vendorDir . '/cakephp/utility'),
'Cake\\Datasource\\' => array($vendorDir . '/cakephp/datasource'),
'Cake\\Database\\' => array($vendorDir . '/cakephp/database'),
'Cake\\Core\\' => array($vendorDir . '/cakephp/core'),
'Cake\\Chronos\\' => array($vendorDir . '/cakephp/chronos/src'),
'Brick\\Math\\' => array($vendorDir . '/brick/math/src'),
'Barryvdh\\LaravelIdeHelper\\' => array($vendorDir . '/barryvdh/laravel-ide-helper/src'),
'Barryvdh\\Debugbar\\' => array($vendorDir . '/barryvdh/laravel-debugbar/src'),
+7 -461
View File
@@ -12,15 +12,14 @@ class ComposerStaticInitc91cd9c5b1e6a9e8573a14b799ea9342
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
'8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php',
'662a729f963d39afe703c9d9b7ab4a8c' => __DIR__ . '/..' . '/symfony/polyfill-php83/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php',
'667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
'25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
'72142d7b40a3a0b14e91825290b5ad82' => __DIR__ . '/..' . '/cakephp/core/functions.php',
'35a6ad97d21e794e7e22a17d806652e4' => __DIR__ . '/..' . '/nunomaduro/termwind/src/Functions.php',
'2203a247e6fda86070a5e4e07aed533a' => __DIR__ . '/..' . '/symfony/clock/Resources/now.php',
'09f6b20656683369174dd6fa83b7e5fb' => __DIR__ . '/..' . '/symfony/polyfill-uuid/bootstrap.php',
@@ -35,7 +34,6 @@ class ComposerStaticInitc91cd9c5b1e6a9e8573a14b799ea9342
'58571171fd5812e6e447dce228f52f4d' => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Support/helpers.php',
'6124b4c8570aa390c21fafd04a26c69f' => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
'3bd81c9b8fcc150b69d8b63b4d2ccf23' => __DIR__ . '/..' . '/spatie/flare-client-php/src/helpers.php',
'948ad5488880985ff1c06721a4e447fe' => __DIR__ . '/..' . '/cakephp/utility/bootstrap.php',
'ec07570ca5a812141189b1fa81503674' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Assert/Functions.php',
'801c31d8ed748cfa537fa45402288c95' => __DIR__ . '/..' . '/psy/psysh/src/functions.php',
'b84a5ba6d9a8ee8cf648b71c42c799b7' => __DIR__ . '/..' . '/akaunting/laravel-setting/src/helpers.php',
@@ -109,12 +107,10 @@ class ComposerStaticInitc91cd9c5b1e6a9e8573a14b799ea9342
'Symfony\\Component\\HttpKernel\\' => 29,
'Symfony\\Component\\HttpFoundation\\' => 33,
'Symfony\\Component\\Finder\\' => 25,
'Symfony\\Component\\Filesystem\\' => 29,
'Symfony\\Component\\EventDispatcher\\' => 34,
'Symfony\\Component\\ErrorHandler\\' => 31,
'Symfony\\Component\\CssSelector\\' => 30,
'Symfony\\Component\\Console\\' => 26,
'Symfony\\Component\\Config\\' => 25,
'Symfony\\Component\\Clock\\' => 24,
'Spatie\\QueryBuilder\\Database\\Factories\\' => 39,
'Spatie\\QueryBuilder\\' => 20,
@@ -147,7 +143,6 @@ class ComposerStaticInitc91cd9c5b1e6a9e8573a14b799ea9342
'PhpParser\\' => 10,
'PhpOption\\' => 10,
'PhpOffice\\PhpSpreadsheet\\' => 25,
'Phinx\\' => 6,
'ParagonIE\\ConstantTime\\' => 23,
'ParaTest\\' => 9,
'PHPStan\\PhpDocParser\\' => 21,
@@ -169,7 +164,6 @@ class ComposerStaticInitc91cd9c5b1e6a9e8573a14b799ea9342
'League\\MimeTypeDetection\\' => 25,
'League\\Flysystem\\Local\\' => 23,
'League\\Flysystem\\' => 17,
'League\\Container\\' => 17,
'League\\Config\\' => 14,
'League\\CommonMark\\' => 18,
'Laravel\\Ui\\' => 11,
@@ -236,11 +230,6 @@ class ComposerStaticInitc91cd9c5b1e6a9e8573a14b799ea9342
'Complex\\' => 8,
'Carbon\\Doctrine\\' => 16,
'Carbon\\' => 7,
'Cake\\Utility\\' => 13,
'Cake\\Datasource\\' => 16,
'Cake\\Database\\' => 14,
'Cake\\Core\\' => 10,
'Cake\\Chronos\\' => 13,
),
'B' =>
array (
@@ -268,8 +257,8 @@ class ComposerStaticInitc91cd9c5b1e6a9e8573a14b799ea9342
),
'phpDocumentor\\Reflection\\' =>
array (
0 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',
1 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src',
0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src',
1 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',
),
'ZipStream\\' =>
array (
@@ -423,10 +412,6 @@ class ComposerStaticInitc91cd9c5b1e6a9e8573a14b799ea9342
array (
0 => __DIR__ . '/..' . '/symfony/finder',
),
'Symfony\\Component\\Filesystem\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/filesystem',
),
'Symfony\\Component\\EventDispatcher\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/event-dispatcher',
@@ -443,10 +428,6 @@ class ComposerStaticInitc91cd9c5b1e6a9e8573a14b799ea9342
array (
0 => __DIR__ . '/..' . '/symfony/console',
),
'Symfony\\Component\\Config\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/config',
),
'Symfony\\Component\\Clock\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/clock',
@@ -465,13 +446,13 @@ class ComposerStaticInitc91cd9c5b1e6a9e8573a14b799ea9342
),
'Spatie\\LaravelIgnition\\' =>
array (
0 => __DIR__ . '/..' . '/spatie/laravel-ignition/src',
1 => __DIR__ . '/..' . '/spatie/error-solutions/legacy/laravel-ignition',
0 => __DIR__ . '/..' . '/spatie/error-solutions/legacy/laravel-ignition',
1 => __DIR__ . '/..' . '/spatie/laravel-ignition/src',
),
'Spatie\\Ignition\\' =>
array (
0 => __DIR__ . '/..' . '/spatie/ignition/src',
1 => __DIR__ . '/..' . '/spatie/error-solutions/legacy/ignition',
0 => __DIR__ . '/..' . '/spatie/error-solutions/legacy/ignition',
1 => __DIR__ . '/..' . '/spatie/ignition/src',
),
'Spatie\\FlareClient\\' =>
array (
@@ -554,10 +535,6 @@ class ComposerStaticInitc91cd9c5b1e6a9e8573a14b799ea9342
array (
0 => __DIR__ . '/..' . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet',
),
'Phinx\\' =>
array (
0 => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx',
),
'ParagonIE\\ConstantTime\\' =>
array (
0 => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src',
@@ -606,10 +583,6 @@ class ComposerStaticInitc91cd9c5b1e6a9e8573a14b799ea9342
array (
0 => __DIR__ . '/..' . '/league/flysystem/src',
),
'League\\Container\\' =>
array (
0 => __DIR__ . '/..' . '/league/container/src',
),
'League\\Config\\' =>
array (
0 => __DIR__ . '/..' . '/league/config/src',
@@ -794,26 +767,6 @@ class ComposerStaticInitc91cd9c5b1e6a9e8573a14b799ea9342
array (
0 => __DIR__ . '/..' . '/nesbot/carbon/src/Carbon',
),
'Cake\\Utility\\' =>
array (
0 => __DIR__ . '/..' . '/cakephp/utility',
),
'Cake\\Datasource\\' =>
array (
0 => __DIR__ . '/..' . '/cakephp/datasource',
),
'Cake\\Database\\' =>
array (
0 => __DIR__ . '/..' . '/cakephp/database',
),
'Cake\\Core\\' =>
array (
0 => __DIR__ . '/..' . '/cakephp/core',
),
'Cake\\Chronos\\' =>
array (
0 => __DIR__ . '/..' . '/cakephp/chronos/src',
),
'Brick\\Math\\' =>
array (
0 => __DIR__ . '/..' . '/brick/math/src',
@@ -1047,196 +1000,6 @@ class ComposerStaticInitc91cd9c5b1e6a9e8573a14b799ea9342
'Brick\\Math\\Internal\\Calculator\\GmpCalculator' => __DIR__ . '/..' . '/brick/math/src/Internal/Calculator/GmpCalculator.php',
'Brick\\Math\\Internal\\Calculator\\NativeCalculator' => __DIR__ . '/..' . '/brick/math/src/Internal/Calculator/NativeCalculator.php',
'Brick\\Math\\RoundingMode' => __DIR__ . '/..' . '/brick/math/src/RoundingMode.php',
'Cake\\Chronos\\Chronos' => __DIR__ . '/..' . '/cakephp/chronos/src/Chronos.php',
'Cake\\Chronos\\ChronosDate' => __DIR__ . '/..' . '/cakephp/chronos/src/ChronosDate.php',
'Cake\\Chronos\\ChronosDatePeriod' => __DIR__ . '/..' . '/cakephp/chronos/src/ChronosDatePeriod.php',
'Cake\\Chronos\\ChronosInterval' => __DIR__ . '/..' . '/cakephp/chronos/src/ChronosInterval.php',
'Cake\\Chronos\\ChronosPeriod' => __DIR__ . '/..' . '/cakephp/chronos/src/ChronosPeriod.php',
'Cake\\Chronos\\ChronosTime' => __DIR__ . '/..' . '/cakephp/chronos/src/ChronosTime.php',
'Cake\\Chronos\\ClockFactory' => __DIR__ . '/..' . '/cakephp/chronos/src/ClockFactory.php',
'Cake\\Chronos\\DifferenceFormatter' => __DIR__ . '/..' . '/cakephp/chronos/src/DifferenceFormatter.php',
'Cake\\Chronos\\DifferenceFormatterInterface' => __DIR__ . '/..' . '/cakephp/chronos/src/DifferenceFormatterInterface.php',
'Cake\\Chronos\\FormattingTrait' => __DIR__ . '/..' . '/cakephp/chronos/src/FormattingTrait.php',
'Cake\\Chronos\\Translator' => __DIR__ . '/..' . '/cakephp/chronos/src/Translator.php',
'Cake\\Core\\App' => __DIR__ . '/..' . '/cakephp/core/App.php',
'Cake\\Core\\Attribute\\Configure' => __DIR__ . '/..' . '/cakephp/core/Attribute/Configure.php',
'Cake\\Core\\BasePlugin' => __DIR__ . '/..' . '/cakephp/core/BasePlugin.php',
'Cake\\Core\\Configure' => __DIR__ . '/..' . '/cakephp/core/Configure.php',
'Cake\\Core\\Configure\\ConfigEngineInterface' => __DIR__ . '/..' . '/cakephp/core/Configure/ConfigEngineInterface.php',
'Cake\\Core\\Configure\\Engine\\IniConfig' => __DIR__ . '/..' . '/cakephp/core/Configure/Engine/IniConfig.php',
'Cake\\Core\\Configure\\Engine\\JsonConfig' => __DIR__ . '/..' . '/cakephp/core/Configure/Engine/JsonConfig.php',
'Cake\\Core\\Configure\\Engine\\PhpConfig' => __DIR__ . '/..' . '/cakephp/core/Configure/Engine/PhpConfig.php',
'Cake\\Core\\Configure\\FileConfigTrait' => __DIR__ . '/..' . '/cakephp/core/Configure/FileConfigTrait.php',
'Cake\\Core\\ConsoleApplicationInterface' => __DIR__ . '/..' . '/cakephp/core/ConsoleApplicationInterface.php',
'Cake\\Core\\Container' => __DIR__ . '/..' . '/cakephp/core/Container.php',
'Cake\\Core\\ContainerApplicationInterface' => __DIR__ . '/..' . '/cakephp/core/ContainerApplicationInterface.php',
'Cake\\Core\\ContainerInterface' => __DIR__ . '/..' . '/cakephp/core/ContainerInterface.php',
'Cake\\Core\\ConventionsTrait' => __DIR__ . '/..' . '/cakephp/core/ConventionsTrait.php',
'Cake\\Core\\EventAwareApplicationInterface' => __DIR__ . '/..' . '/cakephp/core/EventAwareApplicationInterface.php',
'Cake\\Core\\Exception\\CakeException' => __DIR__ . '/..' . '/cakephp/core/Exception/CakeException.php',
'Cake\\Core\\Exception\\HttpErrorCodeInterface' => __DIR__ . '/..' . '/cakephp/core/Exception/HttpErrorCodeInterface.php',
'Cake\\Core\\Exception\\MissingPluginException' => __DIR__ . '/..' . '/cakephp/core/Exception/MissingPluginException.php',
'Cake\\Core\\HttpApplicationInterface' => __DIR__ . '/..' . '/cakephp/core/HttpApplicationInterface.php',
'Cake\\Core\\InstanceConfigTrait' => __DIR__ . '/..' . '/cakephp/core/InstanceConfigTrait.php',
'Cake\\Core\\ObjectRegistry' => __DIR__ . '/..' . '/cakephp/core/ObjectRegistry.php',
'Cake\\Core\\Plugin' => __DIR__ . '/..' . '/cakephp/core/Plugin.php',
'Cake\\Core\\PluginApplicationInterface' => __DIR__ . '/..' . '/cakephp/core/PluginApplicationInterface.php',
'Cake\\Core\\PluginCollection' => __DIR__ . '/..' . '/cakephp/core/PluginCollection.php',
'Cake\\Core\\PluginConfig' => __DIR__ . '/..' . '/cakephp/core/PluginConfig.php',
'Cake\\Core\\PluginInterface' => __DIR__ . '/..' . '/cakephp/core/PluginInterface.php',
'Cake\\Core\\Retry\\CommandRetry' => __DIR__ . '/..' . '/cakephp/core/Retry/CommandRetry.php',
'Cake\\Core\\Retry\\RetryStrategyInterface' => __DIR__ . '/..' . '/cakephp/core/Retry/RetryStrategyInterface.php',
'Cake\\Core\\ServiceConfig' => __DIR__ . '/..' . '/cakephp/core/ServiceConfig.php',
'Cake\\Core\\ServiceProvider' => __DIR__ . '/..' . '/cakephp/core/ServiceProvider.php',
'Cake\\Core\\StaticConfigTrait' => __DIR__ . '/..' . '/cakephp/core/StaticConfigTrait.php',
'Cake\\Core\\TestSuite\\ContainerStubTrait' => __DIR__ . '/..' . '/cakephp/core/TestSuite/ContainerStubTrait.php',
'Cake\\Database\\Connection' => __DIR__ . '/..' . '/cakephp/database/Connection.php',
'Cake\\Database\\ConstraintsInterface' => __DIR__ . '/..' . '/cakephp/database/ConstraintsInterface.php',
'Cake\\Database\\Driver' => __DIR__ . '/..' . '/cakephp/database/Driver.php',
'Cake\\Database\\DriverFeatureEnum' => __DIR__ . '/..' . '/cakephp/database/DriverFeatureEnum.php',
'Cake\\Database\\Driver\\Mysql' => __DIR__ . '/..' . '/cakephp/database/Driver/Mysql.php',
'Cake\\Database\\Driver\\Postgres' => __DIR__ . '/..' . '/cakephp/database/Driver/Postgres.php',
'Cake\\Database\\Driver\\Sqlite' => __DIR__ . '/..' . '/cakephp/database/Driver/Sqlite.php',
'Cake\\Database\\Driver\\Sqlserver' => __DIR__ . '/..' . '/cakephp/database/Driver/Sqlserver.php',
'Cake\\Database\\Driver\\TupleComparisonTranslatorTrait' => __DIR__ . '/..' . '/cakephp/database/Driver/TupleComparisonTranslatorTrait.php',
'Cake\\Database\\Exception\\DatabaseException' => __DIR__ . '/..' . '/cakephp/database/Exception/DatabaseException.php',
'Cake\\Database\\Exception\\MissingConnectionException' => __DIR__ . '/..' . '/cakephp/database/Exception/MissingConnectionException.php',
'Cake\\Database\\Exception\\MissingDriverException' => __DIR__ . '/..' . '/cakephp/database/Exception/MissingDriverException.php',
'Cake\\Database\\Exception\\MissingExtensionException' => __DIR__ . '/..' . '/cakephp/database/Exception/MissingExtensionException.php',
'Cake\\Database\\Exception\\NestedTransactionRollbackException' => __DIR__ . '/..' . '/cakephp/database/Exception/NestedTransactionRollbackException.php',
'Cake\\Database\\Exception\\QueryException' => __DIR__ . '/..' . '/cakephp/database/Exception/QueryException.php',
'Cake\\Database\\ExpressionInterface' => __DIR__ . '/..' . '/cakephp/database/ExpressionInterface.php',
'Cake\\Database\\Expression\\AggregateExpression' => __DIR__ . '/..' . '/cakephp/database/Expression/AggregateExpression.php',
'Cake\\Database\\Expression\\BetweenExpression' => __DIR__ . '/..' . '/cakephp/database/Expression/BetweenExpression.php',
'Cake\\Database\\Expression\\CaseExpressionTrait' => __DIR__ . '/..' . '/cakephp/database/Expression/CaseExpressionTrait.php',
'Cake\\Database\\Expression\\CaseStatementExpression' => __DIR__ . '/..' . '/cakephp/database/Expression/CaseStatementExpression.php',
'Cake\\Database\\Expression\\CommonTableExpression' => __DIR__ . '/..' . '/cakephp/database/Expression/CommonTableExpression.php',
'Cake\\Database\\Expression\\ComparisonExpression' => __DIR__ . '/..' . '/cakephp/database/Expression/ComparisonExpression.php',
'Cake\\Database\\Expression\\FieldInterface' => __DIR__ . '/..' . '/cakephp/database/Expression/FieldInterface.php',
'Cake\\Database\\Expression\\FieldTrait' => __DIR__ . '/..' . '/cakephp/database/Expression/FieldTrait.php',
'Cake\\Database\\Expression\\FunctionExpression' => __DIR__ . '/..' . '/cakephp/database/Expression/FunctionExpression.php',
'Cake\\Database\\Expression\\IdentifierExpression' => __DIR__ . '/..' . '/cakephp/database/Expression/IdentifierExpression.php',
'Cake\\Database\\Expression\\OrderByExpression' => __DIR__ . '/..' . '/cakephp/database/Expression/OrderByExpression.php',
'Cake\\Database\\Expression\\OrderClauseExpression' => __DIR__ . '/..' . '/cakephp/database/Expression/OrderClauseExpression.php',
'Cake\\Database\\Expression\\QueryExpression' => __DIR__ . '/..' . '/cakephp/database/Expression/QueryExpression.php',
'Cake\\Database\\Expression\\StringExpression' => __DIR__ . '/..' . '/cakephp/database/Expression/StringExpression.php',
'Cake\\Database\\Expression\\TupleComparison' => __DIR__ . '/..' . '/cakephp/database/Expression/TupleComparison.php',
'Cake\\Database\\Expression\\UnaryExpression' => __DIR__ . '/..' . '/cakephp/database/Expression/UnaryExpression.php',
'Cake\\Database\\Expression\\ValuesExpression' => __DIR__ . '/..' . '/cakephp/database/Expression/ValuesExpression.php',
'Cake\\Database\\Expression\\WhenThenExpression' => __DIR__ . '/..' . '/cakephp/database/Expression/WhenThenExpression.php',
'Cake\\Database\\Expression\\WindowExpression' => __DIR__ . '/..' . '/cakephp/database/Expression/WindowExpression.php',
'Cake\\Database\\Expression\\WindowInterface' => __DIR__ . '/..' . '/cakephp/database/Expression/WindowInterface.php',
'Cake\\Database\\FieldTypeConverter' => __DIR__ . '/..' . '/cakephp/database/FieldTypeConverter.php',
'Cake\\Database\\FunctionsBuilder' => __DIR__ . '/..' . '/cakephp/database/FunctionsBuilder.php',
'Cake\\Database\\IdentifierQuoter' => __DIR__ . '/..' . '/cakephp/database/IdentifierQuoter.php',
'Cake\\Database\\Log\\LoggedQuery' => __DIR__ . '/..' . '/cakephp/database/Log/LoggedQuery.php',
'Cake\\Database\\Log\\QueryLogger' => __DIR__ . '/..' . '/cakephp/database/Log/QueryLogger.php',
'Cake\\Database\\PostgresCompiler' => __DIR__ . '/..' . '/cakephp/database/PostgresCompiler.php',
'Cake\\Database\\Query' => __DIR__ . '/..' . '/cakephp/database/Query.php',
'Cake\\Database\\QueryCompiler' => __DIR__ . '/..' . '/cakephp/database/QueryCompiler.php',
'Cake\\Database\\Query\\DeleteQuery' => __DIR__ . '/..' . '/cakephp/database/Query/DeleteQuery.php',
'Cake\\Database\\Query\\InsertQuery' => __DIR__ . '/..' . '/cakephp/database/Query/InsertQuery.php',
'Cake\\Database\\Query\\QueryFactory' => __DIR__ . '/..' . '/cakephp/database/Query/QueryFactory.php',
'Cake\\Database\\Query\\SelectQuery' => __DIR__ . '/..' . '/cakephp/database/Query/SelectQuery.php',
'Cake\\Database\\Query\\UpdateQuery' => __DIR__ . '/..' . '/cakephp/database/Query/UpdateQuery.php',
'Cake\\Database\\Retry\\ErrorCodeWaitStrategy' => __DIR__ . '/..' . '/cakephp/database/Retry/ErrorCodeWaitStrategy.php',
'Cake\\Database\\Retry\\ReconnectStrategy' => __DIR__ . '/..' . '/cakephp/database/Retry/ReconnectStrategy.php',
'Cake\\Database\\SchemaCache' => __DIR__ . '/..' . '/cakephp/database/SchemaCache.php',
'Cake\\Database\\Schema\\CachedCollection' => __DIR__ . '/..' . '/cakephp/database/Schema/CachedCollection.php',
'Cake\\Database\\Schema\\CheckConstraint' => __DIR__ . '/..' . '/cakephp/database/Schema/CheckConstraint.php',
'Cake\\Database\\Schema\\Collection' => __DIR__ . '/..' . '/cakephp/database/Schema/Collection.php',
'Cake\\Database\\Schema\\CollectionInterface' => __DIR__ . '/..' . '/cakephp/database/Schema/CollectionInterface.php',
'Cake\\Database\\Schema\\Column' => __DIR__ . '/..' . '/cakephp/database/Schema/Column.php',
'Cake\\Database\\Schema\\Constraint' => __DIR__ . '/..' . '/cakephp/database/Schema/Constraint.php',
'Cake\\Database\\Schema\\ForeignKey' => __DIR__ . '/..' . '/cakephp/database/Schema/ForeignKey.php',
'Cake\\Database\\Schema\\Index' => __DIR__ . '/..' . '/cakephp/database/Schema/Index.php',
'Cake\\Database\\Schema\\MysqlSchemaDialect' => __DIR__ . '/..' . '/cakephp/database/Schema/MysqlSchemaDialect.php',
'Cake\\Database\\Schema\\PostgresSchemaDialect' => __DIR__ . '/..' . '/cakephp/database/Schema/PostgresSchemaDialect.php',
'Cake\\Database\\Schema\\SchemaDialect' => __DIR__ . '/..' . '/cakephp/database/Schema/SchemaDialect.php',
'Cake\\Database\\Schema\\SqlGeneratorInterface' => __DIR__ . '/..' . '/cakephp/database/Schema/SqlGeneratorInterface.php',
'Cake\\Database\\Schema\\SqliteSchemaDialect' => __DIR__ . '/..' . '/cakephp/database/Schema/SqliteSchemaDialect.php',
'Cake\\Database\\Schema\\SqlserverSchemaDialect' => __DIR__ . '/..' . '/cakephp/database/Schema/SqlserverSchemaDialect.php',
'Cake\\Database\\Schema\\TableSchema' => __DIR__ . '/..' . '/cakephp/database/Schema/TableSchema.php',
'Cake\\Database\\Schema\\TableSchemaInterface' => __DIR__ . '/..' . '/cakephp/database/Schema/TableSchemaInterface.php',
'Cake\\Database\\Schema\\UniqueKey' => __DIR__ . '/..' . '/cakephp/database/Schema/UniqueKey.php',
'Cake\\Database\\SqlserverCompiler' => __DIR__ . '/..' . '/cakephp/database/SqlserverCompiler.php',
'Cake\\Database\\StatementInterface' => __DIR__ . '/..' . '/cakephp/database/StatementInterface.php',
'Cake\\Database\\Statement\\SqliteStatement' => __DIR__ . '/..' . '/cakephp/database/Statement/SqliteStatement.php',
'Cake\\Database\\Statement\\SqlserverStatement' => __DIR__ . '/..' . '/cakephp/database/Statement/SqlserverStatement.php',
'Cake\\Database\\Statement\\Statement' => __DIR__ . '/..' . '/cakephp/database/Statement/Statement.php',
'Cake\\Database\\TypeFactory' => __DIR__ . '/..' . '/cakephp/database/TypeFactory.php',
'Cake\\Database\\TypeInterface' => __DIR__ . '/..' . '/cakephp/database/TypeInterface.php',
'Cake\\Database\\TypeMap' => __DIR__ . '/..' . '/cakephp/database/TypeMap.php',
'Cake\\Database\\TypeMapTrait' => __DIR__ . '/..' . '/cakephp/database/TypeMapTrait.php',
'Cake\\Database\\Type\\BaseType' => __DIR__ . '/..' . '/cakephp/database/Type/BaseType.php',
'Cake\\Database\\Type\\BatchCastingInterface' => __DIR__ . '/..' . '/cakephp/database/Type/BatchCastingInterface.php',
'Cake\\Database\\Type\\BinaryType' => __DIR__ . '/..' . '/cakephp/database/Type/BinaryType.php',
'Cake\\Database\\Type\\BinaryUuidType' => __DIR__ . '/..' . '/cakephp/database/Type/BinaryUuidType.php',
'Cake\\Database\\Type\\BoolType' => __DIR__ . '/..' . '/cakephp/database/Type/BoolType.php',
'Cake\\Database\\Type\\ColumnSchemaAwareInterface' => __DIR__ . '/..' . '/cakephp/database/Type/ColumnSchemaAwareInterface.php',
'Cake\\Database\\Type\\DateTimeFractionalType' => __DIR__ . '/..' . '/cakephp/database/Type/DateTimeFractionalType.php',
'Cake\\Database\\Type\\DateTimeTimezoneType' => __DIR__ . '/..' . '/cakephp/database/Type/DateTimeTimezoneType.php',
'Cake\\Database\\Type\\DateTimeType' => __DIR__ . '/..' . '/cakephp/database/Type/DateTimeType.php',
'Cake\\Database\\Type\\DateType' => __DIR__ . '/..' . '/cakephp/database/Type/DateType.php',
'Cake\\Database\\Type\\DecimalType' => __DIR__ . '/..' . '/cakephp/database/Type/DecimalType.php',
'Cake\\Database\\Type\\EnumLabelInterface' => __DIR__ . '/..' . '/cakephp/database/Type/EnumLabelInterface.php',
'Cake\\Database\\Type\\EnumType' => __DIR__ . '/..' . '/cakephp/database/Type/EnumType.php',
'Cake\\Database\\Type\\ExpressionTypeCasterTrait' => __DIR__ . '/..' . '/cakephp/database/Type/ExpressionTypeCasterTrait.php',
'Cake\\Database\\Type\\ExpressionTypeInterface' => __DIR__ . '/..' . '/cakephp/database/Type/ExpressionTypeInterface.php',
'Cake\\Database\\Type\\FloatType' => __DIR__ . '/..' . '/cakephp/database/Type/FloatType.php',
'Cake\\Database\\Type\\IntegerType' => __DIR__ . '/..' . '/cakephp/database/Type/IntegerType.php',
'Cake\\Database\\Type\\JsonType' => __DIR__ . '/..' . '/cakephp/database/Type/JsonType.php',
'Cake\\Database\\Type\\OptionalConvertInterface' => __DIR__ . '/..' . '/cakephp/database/Type/OptionalConvertInterface.php',
'Cake\\Database\\Type\\StringType' => __DIR__ . '/..' . '/cakephp/database/Type/StringType.php',
'Cake\\Database\\Type\\TimeType' => __DIR__ . '/..' . '/cakephp/database/Type/TimeType.php',
'Cake\\Database\\Type\\UuidType' => __DIR__ . '/..' . '/cakephp/database/Type/UuidType.php',
'Cake\\Database\\TypedResultInterface' => __DIR__ . '/..' . '/cakephp/database/TypedResultInterface.php',
'Cake\\Database\\TypedResultTrait' => __DIR__ . '/..' . '/cakephp/database/TypedResultTrait.php',
'Cake\\Database\\ValueBinder' => __DIR__ . '/..' . '/cakephp/database/ValueBinder.php',
'Cake\\Datasource\\ConnectionInterface' => __DIR__ . '/..' . '/cakephp/datasource/ConnectionInterface.php',
'Cake\\Datasource\\ConnectionManager' => __DIR__ . '/..' . '/cakephp/datasource/ConnectionManager.php',
'Cake\\Datasource\\ConnectionRegistry' => __DIR__ . '/..' . '/cakephp/datasource/ConnectionRegistry.php',
'Cake\\Datasource\\EntityInterface' => __DIR__ . '/..' . '/cakephp/datasource/EntityInterface.php',
'Cake\\Datasource\\EntityTrait' => __DIR__ . '/..' . '/cakephp/datasource/EntityTrait.php',
'Cake\\Datasource\\Exception\\InvalidPrimaryKeyException' => __DIR__ . '/..' . '/cakephp/datasource/Exception/InvalidPrimaryKeyException.php',
'Cake\\Datasource\\Exception\\MissingDatasourceConfigException' => __DIR__ . '/..' . '/cakephp/datasource/Exception/MissingDatasourceConfigException.php',
'Cake\\Datasource\\Exception\\MissingDatasourceException' => __DIR__ . '/..' . '/cakephp/datasource/Exception/MissingDatasourceException.php',
'Cake\\Datasource\\Exception\\MissingModelException' => __DIR__ . '/..' . '/cakephp/datasource/Exception/MissingModelException.php',
'Cake\\Datasource\\Exception\\MissingPropertyException' => __DIR__ . '/..' . '/cakephp/datasource/Exception/MissingPropertyException.php',
'Cake\\Datasource\\Exception\\RecordNotFoundException' => __DIR__ . '/..' . '/cakephp/datasource/Exception/RecordNotFoundException.php',
'Cake\\Datasource\\FactoryLocator' => __DIR__ . '/..' . '/cakephp/datasource/FactoryLocator.php',
'Cake\\Datasource\\FixtureInterface' => __DIR__ . '/..' . '/cakephp/datasource/FixtureInterface.php',
'Cake\\Datasource\\InvalidPropertyInterface' => __DIR__ . '/..' . '/cakephp/datasource/InvalidPropertyInterface.php',
'Cake\\Datasource\\Locator\\AbstractLocator' => __DIR__ . '/..' . '/cakephp/datasource/Locator/AbstractLocator.php',
'Cake\\Datasource\\Locator\\LocatorInterface' => __DIR__ . '/..' . '/cakephp/datasource/Locator/LocatorInterface.php',
'Cake\\Datasource\\ModelAwareTrait' => __DIR__ . '/..' . '/cakephp/datasource/ModelAwareTrait.php',
'Cake\\Datasource\\Paging\\Exception\\PageOutOfBoundsException' => __DIR__ . '/..' . '/cakephp/datasource/Paging/Exception/PageOutOfBoundsException.php',
'Cake\\Datasource\\Paging\\NumericPaginator' => __DIR__ . '/..' . '/cakephp/datasource/Paging/NumericPaginator.php',
'Cake\\Datasource\\Paging\\PaginatedInterface' => __DIR__ . '/..' . '/cakephp/datasource/Paging/PaginatedInterface.php',
'Cake\\Datasource\\Paging\\PaginatedResultSet' => __DIR__ . '/..' . '/cakephp/datasource/Paging/PaginatedResultSet.php',
'Cake\\Datasource\\Paging\\PaginatorInterface' => __DIR__ . '/..' . '/cakephp/datasource/Paging/PaginatorInterface.php',
'Cake\\Datasource\\Paging\\SimplePaginator' => __DIR__ . '/..' . '/cakephp/datasource/Paging/SimplePaginator.php',
'Cake\\Datasource\\Paging\\SortField' => __DIR__ . '/..' . '/cakephp/datasource/Paging/SortField.php',
'Cake\\Datasource\\Paging\\SortableFieldsBuilder' => __DIR__ . '/..' . '/cakephp/datasource/Paging/SortableFieldsBuilder.php',
'Cake\\Datasource\\QueryCacher' => __DIR__ . '/..' . '/cakephp/datasource/QueryCacher.php',
'Cake\\Datasource\\QueryInterface' => __DIR__ . '/..' . '/cakephp/datasource/QueryInterface.php',
'Cake\\Datasource\\RepositoryInterface' => __DIR__ . '/..' . '/cakephp/datasource/RepositoryInterface.php',
'Cake\\Datasource\\ResultSetDecorator' => __DIR__ . '/..' . '/cakephp/datasource/ResultSetDecorator.php',
'Cake\\Datasource\\ResultSetInterface' => __DIR__ . '/..' . '/cakephp/datasource/ResultSetInterface.php',
'Cake\\Datasource\\RuleInvoker' => __DIR__ . '/..' . '/cakephp/datasource/RuleInvoker.php',
'Cake\\Datasource\\RulesAwareTrait' => __DIR__ . '/..' . '/cakephp/datasource/RulesAwareTrait.php',
'Cake\\Datasource\\RulesChecker' => __DIR__ . '/..' . '/cakephp/datasource/RulesChecker.php',
'Cake\\Datasource\\SchemaInterface' => __DIR__ . '/..' . '/cakephp/datasource/SchemaInterface.php',
'Cake\\Utility\\CookieCryptTrait' => __DIR__ . '/..' . '/cakephp/utility/CookieCryptTrait.php',
'Cake\\Utility\\Crypto\\OpenSsl' => __DIR__ . '/..' . '/cakephp/utility/Crypto/OpenSsl.php',
'Cake\\Utility\\Exception\\XmlException' => __DIR__ . '/..' . '/cakephp/utility/Exception/XmlException.php',
'Cake\\Utility\\Filesystem' => __DIR__ . '/..' . '/cakephp/utility/Filesystem.php',
'Cake\\Utility\\Hash' => __DIR__ . '/..' . '/cakephp/utility/Hash.php',
'Cake\\Utility\\Inflector' => __DIR__ . '/..' . '/cakephp/utility/Inflector.php',
'Cake\\Utility\\MergeVariablesTrait' => __DIR__ . '/..' . '/cakephp/utility/MergeVariablesTrait.php',
'Cake\\Utility\\Security' => __DIR__ . '/..' . '/cakephp/utility/Security.php',
'Cake\\Utility\\Text' => __DIR__ . '/..' . '/cakephp/utility/Text.php',
'Cake\\Utility\\Xml' => __DIR__ . '/..' . '/cakephp/utility/Xml.php',
'Carbon\\AbstractTranslator' => __DIR__ . '/..' . '/nesbot/carbon/src/Carbon/AbstractTranslator.php',
'Carbon\\Callback' => __DIR__ . '/..' . '/nesbot/carbon/src/Carbon/Callback.php',
'Carbon\\Carbon' => __DIR__ . '/..' . '/nesbot/carbon/src/Carbon/Carbon.php',
@@ -4466,55 +4229,6 @@ class ComposerStaticInitc91cd9c5b1e6a9e8573a14b799ea9342
'League\\Config\\MutableConfigurationInterface' => __DIR__ . '/..' . '/league/config/src/MutableConfigurationInterface.php',
'League\\Config\\ReadOnlyConfiguration' => __DIR__ . '/..' . '/league/config/src/ReadOnlyConfiguration.php',
'League\\Config\\SchemaBuilderInterface' => __DIR__ . '/..' . '/league/config/src/SchemaBuilderInterface.php',
'League\\Container\\Argument\\ArgumentInterface' => __DIR__ . '/..' . '/league/container/src/Argument/ArgumentInterface.php',
'League\\Container\\Argument\\ArgumentReflectorInterface' => __DIR__ . '/..' . '/league/container/src/Argument/ArgumentReflectorInterface.php',
'League\\Container\\Argument\\ArgumentReflectorTrait' => __DIR__ . '/..' . '/league/container/src/Argument/ArgumentReflectorTrait.php',
'League\\Container\\Argument\\ArgumentResolverInterface' => __DIR__ . '/..' . '/league/container/src/Argument/ArgumentResolverInterface.php',
'League\\Container\\Argument\\ArgumentResolverTrait' => __DIR__ . '/..' . '/league/container/src/Argument/ArgumentResolverTrait.php',
'League\\Container\\Argument\\DefaultValueArgument' => __DIR__ . '/..' . '/league/container/src/Argument/DefaultValueArgument.php',
'League\\Container\\Argument\\DefaultValueInterface' => __DIR__ . '/..' . '/league/container/src/Argument/DefaultValueInterface.php',
'League\\Container\\Argument\\LiteralArgument' => __DIR__ . '/..' . '/league/container/src/Argument/LiteralArgument.php',
'League\\Container\\Argument\\LiteralArgumentInterface' => __DIR__ . '/..' . '/league/container/src/Argument/LiteralArgumentInterface.php',
'League\\Container\\Argument\\Literal\\ArrayArgument' => __DIR__ . '/..' . '/league/container/src/Argument/Literal/ArrayArgument.php',
'League\\Container\\Argument\\Literal\\BooleanArgument' => __DIR__ . '/..' . '/league/container/src/Argument/Literal/BooleanArgument.php',
'League\\Container\\Argument\\Literal\\CallableArgument' => __DIR__ . '/..' . '/league/container/src/Argument/Literal/CallableArgument.php',
'League\\Container\\Argument\\Literal\\FloatArgument' => __DIR__ . '/..' . '/league/container/src/Argument/Literal/FloatArgument.php',
'League\\Container\\Argument\\Literal\\IntegerArgument' => __DIR__ . '/..' . '/league/container/src/Argument/Literal/IntegerArgument.php',
'League\\Container\\Argument\\Literal\\ObjectArgument' => __DIR__ . '/..' . '/league/container/src/Argument/Literal/ObjectArgument.php',
'League\\Container\\Argument\\Literal\\StringArgument' => __DIR__ . '/..' . '/league/container/src/Argument/Literal/StringArgument.php',
'League\\Container\\Argument\\ResolvableArgument' => __DIR__ . '/..' . '/league/container/src/Argument/ResolvableArgument.php',
'League\\Container\\Argument\\ResolvableArgumentInterface' => __DIR__ . '/..' . '/league/container/src/Argument/ResolvableArgumentInterface.php',
'League\\Container\\Attribute\\AttributeInterface' => __DIR__ . '/..' . '/league/container/src/Attribute/AttributeInterface.php',
'League\\Container\\Attribute\\Inject' => __DIR__ . '/..' . '/league/container/src/Attribute/Inject.php',
'League\\Container\\Attribute\\Resolve' => __DIR__ . '/..' . '/league/container/src/Attribute/Resolve.php',
'League\\Container\\Container' => __DIR__ . '/..' . '/league/container/src/Container.php',
'League\\Container\\ContainerAwareInterface' => __DIR__ . '/..' . '/league/container/src/ContainerAwareInterface.php',
'League\\Container\\ContainerAwareTrait' => __DIR__ . '/..' . '/league/container/src/ContainerAwareTrait.php',
'League\\Container\\DefinitionContainerInterface' => __DIR__ . '/..' . '/league/container/src/DefinitionContainerInterface.php',
'League\\Container\\Definition\\Definition' => __DIR__ . '/..' . '/league/container/src/Definition/Definition.php',
'League\\Container\\Definition\\DefinitionAggregate' => __DIR__ . '/..' . '/league/container/src/Definition/DefinitionAggregate.php',
'League\\Container\\Definition\\DefinitionAggregateInterface' => __DIR__ . '/..' . '/league/container/src/Definition/DefinitionAggregateInterface.php',
'League\\Container\\Definition\\DefinitionInterface' => __DIR__ . '/..' . '/league/container/src/Definition/DefinitionInterface.php',
'League\\Container\\Event\\BeforeResolveEvent' => __DIR__ . '/..' . '/league/container/src/Event/BeforeResolveEvent.php',
'League\\Container\\Event\\ContainerEvent' => __DIR__ . '/..' . '/league/container/src/Event/ContainerEvent.php',
'League\\Container\\Event\\DefinitionResolvedEvent' => __DIR__ . '/..' . '/league/container/src/Event/DefinitionResolvedEvent.php',
'League\\Container\\Event\\EventAwareTrait' => __DIR__ . '/..' . '/league/container/src/Event/EventAwareTrait.php',
'League\\Container\\Event\\EventDispatcher' => __DIR__ . '/..' . '/league/container/src/Event/EventDispatcher.php',
'League\\Container\\Event\\EventFilter' => __DIR__ . '/..' . '/league/container/src/Event/EventFilter.php',
'League\\Container\\Event\\OnDefineEvent' => __DIR__ . '/..' . '/league/container/src/Event/OnDefineEvent.php',
'League\\Container\\Event\\ServiceResolvedEvent' => __DIR__ . '/..' . '/league/container/src/Event/ServiceResolvedEvent.php',
'League\\Container\\Exception\\ContainerException' => __DIR__ . '/..' . '/league/container/src/Exception/ContainerException.php',
'League\\Container\\Exception\\NotFoundException' => __DIR__ . '/..' . '/league/container/src/Exception/NotFoundException.php',
'League\\Container\\Inflector\\Inflector' => __DIR__ . '/..' . '/league/container/src/Inflector/Inflector.php',
'League\\Container\\Inflector\\InflectorAggregate' => __DIR__ . '/..' . '/league/container/src/Inflector/InflectorAggregate.php',
'League\\Container\\Inflector\\InflectorAggregateInterface' => __DIR__ . '/..' . '/league/container/src/Inflector/InflectorAggregateInterface.php',
'League\\Container\\Inflector\\InflectorInterface' => __DIR__ . '/..' . '/league/container/src/Inflector/InflectorInterface.php',
'League\\Container\\ReflectionContainer' => __DIR__ . '/..' . '/league/container/src/ReflectionContainer.php',
'League\\Container\\ServiceProvider\\AbstractServiceProvider' => __DIR__ . '/..' . '/league/container/src/ServiceProvider/AbstractServiceProvider.php',
'League\\Container\\ServiceProvider\\BootableServiceProviderInterface' => __DIR__ . '/..' . '/league/container/src/ServiceProvider/BootableServiceProviderInterface.php',
'League\\Container\\ServiceProvider\\ServiceProviderAggregate' => __DIR__ . '/..' . '/league/container/src/ServiceProvider/ServiceProviderAggregate.php',
'League\\Container\\ServiceProvider\\ServiceProviderAggregateInterface' => __DIR__ . '/..' . '/league/container/src/ServiceProvider/ServiceProviderAggregateInterface.php',
'League\\Container\\ServiceProvider\\ServiceProviderInterface' => __DIR__ . '/..' . '/league/container/src/ServiceProvider/ServiceProviderInterface.php',
'League\\Flysystem\\CalculateChecksumFromStream' => __DIR__ . '/..' . '/league/flysystem/src/CalculateChecksumFromStream.php',
'League\\Flysystem\\ChecksumAlgoIsNotSupported' => __DIR__ . '/..' . '/league/flysystem/src/ChecksumAlgoIsNotSupported.php',
'League\\Flysystem\\ChecksumProvider' => __DIR__ . '/..' . '/league/flysystem/src/ChecksumProvider.php',
@@ -6023,76 +5737,6 @@ class ComposerStaticInitc91cd9c5b1e6a9e8573a14b799ea9342
'PharIo\\Version\\VersionConstraintParser' => __DIR__ . '/..' . '/phar-io/version/src/VersionConstraintParser.php',
'PharIo\\Version\\VersionConstraintValue' => __DIR__ . '/..' . '/phar-io/version/src/VersionConstraintValue.php',
'PharIo\\Version\\VersionNumber' => __DIR__ . '/..' . '/phar-io/version/src/VersionNumber.php',
'Phinx\\Config\\Config' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Config/Config.php',
'Phinx\\Config\\ConfigInterface' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Config/ConfigInterface.php',
'Phinx\\Config\\FeatureFlags' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Config/FeatureFlags.php',
'Phinx\\Config\\NamespaceAwareInterface' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Config/NamespaceAwareInterface.php',
'Phinx\\Config\\NamespaceAwareTrait' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Config/NamespaceAwareTrait.php',
'Phinx\\Console\\Command\\AbstractCommand' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Console/Command/AbstractCommand.php',
'Phinx\\Console\\Command\\Breakpoint' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Console/Command/Breakpoint.php',
'Phinx\\Console\\Command\\Create' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Console/Command/Create.php',
'Phinx\\Console\\Command\\Init' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Console/Command/Init.php',
'Phinx\\Console\\Command\\ListAliases' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Console/Command/ListAliases.php',
'Phinx\\Console\\Command\\Migrate' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Console/Command/Migrate.php',
'Phinx\\Console\\Command\\Rollback' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Console/Command/Rollback.php',
'Phinx\\Console\\Command\\SeedCreate' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Console/Command/SeedCreate.php',
'Phinx\\Console\\Command\\SeedRun' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Console/Command/SeedRun.php',
'Phinx\\Console\\Command\\Status' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Console/Command/Status.php',
'Phinx\\Console\\Command\\Test' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Console/Command/Test.php',
'Phinx\\Console\\PhinxApplication' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Console/PhinxApplication.php',
'Phinx\\Db\\Action\\Action' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Action/Action.php',
'Phinx\\Db\\Action\\AddColumn' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Action/AddColumn.php',
'Phinx\\Db\\Action\\AddForeignKey' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Action/AddForeignKey.php',
'Phinx\\Db\\Action\\AddIndex' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Action/AddIndex.php',
'Phinx\\Db\\Action\\ChangeColumn' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Action/ChangeColumn.php',
'Phinx\\Db\\Action\\ChangeComment' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Action/ChangeComment.php',
'Phinx\\Db\\Action\\ChangePrimaryKey' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Action/ChangePrimaryKey.php',
'Phinx\\Db\\Action\\CreateTable' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Action/CreateTable.php',
'Phinx\\Db\\Action\\DropForeignKey' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Action/DropForeignKey.php',
'Phinx\\Db\\Action\\DropIndex' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Action/DropIndex.php',
'Phinx\\Db\\Action\\DropTable' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Action/DropTable.php',
'Phinx\\Db\\Action\\RemoveColumn' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Action/RemoveColumn.php',
'Phinx\\Db\\Action\\RenameColumn' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Action/RenameColumn.php',
'Phinx\\Db\\Action\\RenameTable' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Action/RenameTable.php',
'Phinx\\Db\\Adapter\\AbstractAdapter' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Adapter/AbstractAdapter.php',
'Phinx\\Db\\Adapter\\AdapterFactory' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Adapter/AdapterFactory.php',
'Phinx\\Db\\Adapter\\AdapterInterface' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Adapter/AdapterInterface.php',
'Phinx\\Db\\Adapter\\AdapterWrapper' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Adapter/AdapterWrapper.php',
'Phinx\\Db\\Adapter\\DirectActionInterface' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Adapter/DirectActionInterface.php',
'Phinx\\Db\\Adapter\\MysqlAdapter' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Adapter/MysqlAdapter.php',
'Phinx\\Db\\Adapter\\PdoAdapter' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Adapter/PdoAdapter.php',
'Phinx\\Db\\Adapter\\PostgresAdapter' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Adapter/PostgresAdapter.php',
'Phinx\\Db\\Adapter\\ProxyAdapter' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Adapter/ProxyAdapter.php',
'Phinx\\Db\\Adapter\\SQLiteAdapter' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Adapter/SQLiteAdapter.php',
'Phinx\\Db\\Adapter\\SqlServerAdapter' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Adapter/SqlServerAdapter.php',
'Phinx\\Db\\Adapter\\TablePrefixAdapter' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Adapter/TablePrefixAdapter.php',
'Phinx\\Db\\Adapter\\TimedOutputAdapter' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Adapter/TimedOutputAdapter.php',
'Phinx\\Db\\Adapter\\UnsupportedColumnTypeException' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Adapter/UnsupportedColumnTypeException.php',
'Phinx\\Db\\Adapter\\WrapperInterface' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Adapter/WrapperInterface.php',
'Phinx\\Db\\Plan\\AlterTable' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Plan/AlterTable.php',
'Phinx\\Db\\Plan\\Intent' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Plan/Intent.php',
'Phinx\\Db\\Plan\\NewTable' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Plan/NewTable.php',
'Phinx\\Db\\Plan\\Plan' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Plan/Plan.php',
'Phinx\\Db\\Plan\\Solver\\ActionSplitter' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Plan/Solver/ActionSplitter.php',
'Phinx\\Db\\Table' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Table.php',
'Phinx\\Db\\Table\\Column' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Table/Column.php',
'Phinx\\Db\\Table\\ForeignKey' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Table/ForeignKey.php',
'Phinx\\Db\\Table\\Index' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Table/Index.php',
'Phinx\\Db\\Table\\Table' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Table/Table.php',
'Phinx\\Db\\Util\\AlterInstructions' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Db/Util/AlterInstructions.php',
'Phinx\\Migration\\AbstractMigration' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Migration/AbstractMigration.php',
'Phinx\\Migration\\AbstractTemplateCreation' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Migration/AbstractTemplateCreation.php',
'Phinx\\Migration\\CreationInterface' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Migration/CreationInterface.php',
'Phinx\\Migration\\IrreversibleMigrationException' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Migration/IrreversibleMigrationException.php',
'Phinx\\Migration\\Manager' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Migration/Manager.php',
'Phinx\\Migration\\Manager\\Environment' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Migration/Manager/Environment.php',
'Phinx\\Migration\\MigrationInterface' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Migration/MigrationInterface.php',
'Phinx\\Seed\\AbstractSeed' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Seed/AbstractSeed.php',
'Phinx\\Seed\\SeedInterface' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Seed/SeedInterface.php',
'Phinx\\Util\\Expression' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Util/Expression.php',
'Phinx\\Util\\Literal' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Util/Literal.php',
'Phinx\\Util\\Util' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Util/Util.php',
'Phinx\\Wrapper\\TextWrapper' => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx/Wrapper/TextWrapper.php',
'PhpOffice\\PhpSpreadsheet\\Calculation\\ArrayEnabled' => __DIR__ . '/..' . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/ArrayEnabled.php',
'PhpOffice\\PhpSpreadsheet\\Calculation\\BinaryComparison' => __DIR__ . '/..' . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/BinaryComparison.php',
'PhpOffice\\PhpSpreadsheet\\Calculation\\Calculation' => __DIR__ . '/..' . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Calculation.php',
@@ -7833,96 +7477,6 @@ class ComposerStaticInitc91cd9c5b1e6a9e8573a14b799ea9342
'Symfony\\Component\\Clock\\MonotonicClock' => __DIR__ . '/..' . '/symfony/clock/MonotonicClock.php',
'Symfony\\Component\\Clock\\NativeClock' => __DIR__ . '/..' . '/symfony/clock/NativeClock.php',
'Symfony\\Component\\Clock\\Test\\ClockSensitiveTrait' => __DIR__ . '/..' . '/symfony/clock/Test/ClockSensitiveTrait.php',
'Symfony\\Component\\Config\\Builder\\ClassBuilder' => __DIR__ . '/..' . '/symfony/config/Builder/ClassBuilder.php',
'Symfony\\Component\\Config\\Builder\\ConfigBuilderGenerator' => __DIR__ . '/..' . '/symfony/config/Builder/ConfigBuilderGenerator.php',
'Symfony\\Component\\Config\\Builder\\ConfigBuilderGeneratorInterface' => __DIR__ . '/..' . '/symfony/config/Builder/ConfigBuilderGeneratorInterface.php',
'Symfony\\Component\\Config\\Builder\\ConfigBuilderInterface' => __DIR__ . '/..' . '/symfony/config/Builder/ConfigBuilderInterface.php',
'Symfony\\Component\\Config\\Builder\\Method' => __DIR__ . '/..' . '/symfony/config/Builder/Method.php',
'Symfony\\Component\\Config\\Builder\\Property' => __DIR__ . '/..' . '/symfony/config/Builder/Property.php',
'Symfony\\Component\\Config\\ConfigCache' => __DIR__ . '/..' . '/symfony/config/ConfigCache.php',
'Symfony\\Component\\Config\\ConfigCacheFactory' => __DIR__ . '/..' . '/symfony/config/ConfigCacheFactory.php',
'Symfony\\Component\\Config\\ConfigCacheFactoryInterface' => __DIR__ . '/..' . '/symfony/config/ConfigCacheFactoryInterface.php',
'Symfony\\Component\\Config\\ConfigCacheInterface' => __DIR__ . '/..' . '/symfony/config/ConfigCacheInterface.php',
'Symfony\\Component\\Config\\Definition\\ArrayNode' => __DIR__ . '/..' . '/symfony/config/Definition/ArrayNode.php',
'Symfony\\Component\\Config\\Definition\\ArrayShapeGenerator' => __DIR__ . '/..' . '/symfony/config/Definition/ArrayShapeGenerator.php',
'Symfony\\Component\\Config\\Definition\\BaseNode' => __DIR__ . '/..' . '/symfony/config/Definition/BaseNode.php',
'Symfony\\Component\\Config\\Definition\\BooleanNode' => __DIR__ . '/..' . '/symfony/config/Definition/BooleanNode.php',
'Symfony\\Component\\Config\\Definition\\Builder\\ArrayNodeDefinition' => __DIR__ . '/..' . '/symfony/config/Definition/Builder/ArrayNodeDefinition.php',
'Symfony\\Component\\Config\\Definition\\Builder\\BooleanNodeDefinition' => __DIR__ . '/..' . '/symfony/config/Definition/Builder/BooleanNodeDefinition.php',
'Symfony\\Component\\Config\\Definition\\Builder\\BuilderAwareInterface' => __DIR__ . '/..' . '/symfony/config/Definition/Builder/BuilderAwareInterface.php',
'Symfony\\Component\\Config\\Definition\\Builder\\EnumNodeDefinition' => __DIR__ . '/..' . '/symfony/config/Definition/Builder/EnumNodeDefinition.php',
'Symfony\\Component\\Config\\Definition\\Builder\\ExprBuilder' => __DIR__ . '/..' . '/symfony/config/Definition/Builder/ExprBuilder.php',
'Symfony\\Component\\Config\\Definition\\Builder\\FloatNodeDefinition' => __DIR__ . '/..' . '/symfony/config/Definition/Builder/FloatNodeDefinition.php',
'Symfony\\Component\\Config\\Definition\\Builder\\IntegerNodeDefinition' => __DIR__ . '/..' . '/symfony/config/Definition/Builder/IntegerNodeDefinition.php',
'Symfony\\Component\\Config\\Definition\\Builder\\MergeBuilder' => __DIR__ . '/..' . '/symfony/config/Definition/Builder/MergeBuilder.php',
'Symfony\\Component\\Config\\Definition\\Builder\\NodeBuilder' => __DIR__ . '/..' . '/symfony/config/Definition/Builder/NodeBuilder.php',
'Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition' => __DIR__ . '/..' . '/symfony/config/Definition/Builder/NodeDefinition.php',
'Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface' => __DIR__ . '/..' . '/symfony/config/Definition/Builder/NodeParentInterface.php',
'Symfony\\Component\\Config\\Definition\\Builder\\NormalizationBuilder' => __DIR__ . '/..' . '/symfony/config/Definition/Builder/NormalizationBuilder.php',
'Symfony\\Component\\Config\\Definition\\Builder\\NumericNodeDefinition' => __DIR__ . '/..' . '/symfony/config/Definition/Builder/NumericNodeDefinition.php',
'Symfony\\Component\\Config\\Definition\\Builder\\ParentNodeDefinitionInterface' => __DIR__ . '/..' . '/symfony/config/Definition/Builder/ParentNodeDefinitionInterface.php',
'Symfony\\Component\\Config\\Definition\\Builder\\ScalarNodeDefinition' => __DIR__ . '/..' . '/symfony/config/Definition/Builder/ScalarNodeDefinition.php',
'Symfony\\Component\\Config\\Definition\\Builder\\StringNodeDefinition' => __DIR__ . '/..' . '/symfony/config/Definition/Builder/StringNodeDefinition.php',
'Symfony\\Component\\Config\\Definition\\Builder\\TreeBuilder' => __DIR__ . '/..' . '/symfony/config/Definition/Builder/TreeBuilder.php',
'Symfony\\Component\\Config\\Definition\\Builder\\ValidationBuilder' => __DIR__ . '/..' . '/symfony/config/Definition/Builder/ValidationBuilder.php',
'Symfony\\Component\\Config\\Definition\\Builder\\VariableNodeDefinition' => __DIR__ . '/..' . '/symfony/config/Definition/Builder/VariableNodeDefinition.php',
'Symfony\\Component\\Config\\Definition\\ConfigurableInterface' => __DIR__ . '/..' . '/symfony/config/Definition/ConfigurableInterface.php',
'Symfony\\Component\\Config\\Definition\\Configuration' => __DIR__ . '/..' . '/symfony/config/Definition/Configuration.php',
'Symfony\\Component\\Config\\Definition\\ConfigurationInterface' => __DIR__ . '/..' . '/symfony/config/Definition/ConfigurationInterface.php',
'Symfony\\Component\\Config\\Definition\\Configurator\\DefinitionConfigurator' => __DIR__ . '/..' . '/symfony/config/Definition/Configurator/DefinitionConfigurator.php',
'Symfony\\Component\\Config\\Definition\\Dumper\\XmlReferenceDumper' => __DIR__ . '/..' . '/symfony/config/Definition/Dumper/XmlReferenceDumper.php',
'Symfony\\Component\\Config\\Definition\\Dumper\\YamlReferenceDumper' => __DIR__ . '/..' . '/symfony/config/Definition/Dumper/YamlReferenceDumper.php',
'Symfony\\Component\\Config\\Definition\\EnumNode' => __DIR__ . '/..' . '/symfony/config/Definition/EnumNode.php',
'Symfony\\Component\\Config\\Definition\\Exception\\DuplicateKeyException' => __DIR__ . '/..' . '/symfony/config/Definition/Exception/DuplicateKeyException.php',
'Symfony\\Component\\Config\\Definition\\Exception\\Exception' => __DIR__ . '/..' . '/symfony/config/Definition/Exception/Exception.php',
'Symfony\\Component\\Config\\Definition\\Exception\\ForbiddenOverwriteException' => __DIR__ . '/..' . '/symfony/config/Definition/Exception/ForbiddenOverwriteException.php',
'Symfony\\Component\\Config\\Definition\\Exception\\InvalidConfigurationException' => __DIR__ . '/..' . '/symfony/config/Definition/Exception/InvalidConfigurationException.php',
'Symfony\\Component\\Config\\Definition\\Exception\\InvalidDefinitionException' => __DIR__ . '/..' . '/symfony/config/Definition/Exception/InvalidDefinitionException.php',
'Symfony\\Component\\Config\\Definition\\Exception\\InvalidTypeException' => __DIR__ . '/..' . '/symfony/config/Definition/Exception/InvalidTypeException.php',
'Symfony\\Component\\Config\\Definition\\Exception\\UnsetKeyException' => __DIR__ . '/..' . '/symfony/config/Definition/Exception/UnsetKeyException.php',
'Symfony\\Component\\Config\\Definition\\FloatNode' => __DIR__ . '/..' . '/symfony/config/Definition/FloatNode.php',
'Symfony\\Component\\Config\\Definition\\IntegerNode' => __DIR__ . '/..' . '/symfony/config/Definition/IntegerNode.php',
'Symfony\\Component\\Config\\Definition\\Loader\\DefinitionFileLoader' => __DIR__ . '/..' . '/symfony/config/Definition/Loader/DefinitionFileLoader.php',
'Symfony\\Component\\Config\\Definition\\NodeInterface' => __DIR__ . '/..' . '/symfony/config/Definition/NodeInterface.php',
'Symfony\\Component\\Config\\Definition\\NumericNode' => __DIR__ . '/..' . '/symfony/config/Definition/NumericNode.php',
'Symfony\\Component\\Config\\Definition\\Processor' => __DIR__ . '/..' . '/symfony/config/Definition/Processor.php',
'Symfony\\Component\\Config\\Definition\\PrototypeNodeInterface' => __DIR__ . '/..' . '/symfony/config/Definition/PrototypeNodeInterface.php',
'Symfony\\Component\\Config\\Definition\\PrototypedArrayNode' => __DIR__ . '/..' . '/symfony/config/Definition/PrototypedArrayNode.php',
'Symfony\\Component\\Config\\Definition\\ScalarNode' => __DIR__ . '/..' . '/symfony/config/Definition/ScalarNode.php',
'Symfony\\Component\\Config\\Definition\\StringNode' => __DIR__ . '/..' . '/symfony/config/Definition/StringNode.php',
'Symfony\\Component\\Config\\Definition\\VariableNode' => __DIR__ . '/..' . '/symfony/config/Definition/VariableNode.php',
'Symfony\\Component\\Config\\Exception\\FileLoaderImportCircularReferenceException' => __DIR__ . '/..' . '/symfony/config/Exception/FileLoaderImportCircularReferenceException.php',
'Symfony\\Component\\Config\\Exception\\FileLocatorFileNotFoundException' => __DIR__ . '/..' . '/symfony/config/Exception/FileLocatorFileNotFoundException.php',
'Symfony\\Component\\Config\\Exception\\LoaderLoadException' => __DIR__ . '/..' . '/symfony/config/Exception/LoaderLoadException.php',
'Symfony\\Component\\Config\\Exception\\LogicException' => __DIR__ . '/..' . '/symfony/config/Exception/LogicException.php',
'Symfony\\Component\\Config\\FileLocator' => __DIR__ . '/..' . '/symfony/config/FileLocator.php',
'Symfony\\Component\\Config\\FileLocatorInterface' => __DIR__ . '/..' . '/symfony/config/FileLocatorInterface.php',
'Symfony\\Component\\Config\\Loader\\DelegatingLoader' => __DIR__ . '/..' . '/symfony/config/Loader/DelegatingLoader.php',
'Symfony\\Component\\Config\\Loader\\DirectoryAwareLoaderInterface' => __DIR__ . '/..' . '/symfony/config/Loader/DirectoryAwareLoaderInterface.php',
'Symfony\\Component\\Config\\Loader\\FileLoader' => __DIR__ . '/..' . '/symfony/config/Loader/FileLoader.php',
'Symfony\\Component\\Config\\Loader\\GlobFileLoader' => __DIR__ . '/..' . '/symfony/config/Loader/GlobFileLoader.php',
'Symfony\\Component\\Config\\Loader\\Loader' => __DIR__ . '/..' . '/symfony/config/Loader/Loader.php',
'Symfony\\Component\\Config\\Loader\\LoaderInterface' => __DIR__ . '/..' . '/symfony/config/Loader/LoaderInterface.php',
'Symfony\\Component\\Config\\Loader\\LoaderResolver' => __DIR__ . '/..' . '/symfony/config/Loader/LoaderResolver.php',
'Symfony\\Component\\Config\\Loader\\LoaderResolverInterface' => __DIR__ . '/..' . '/symfony/config/Loader/LoaderResolverInterface.php',
'Symfony\\Component\\Config\\Loader\\ParamConfigurator' => __DIR__ . '/..' . '/symfony/config/Loader/ParamConfigurator.php',
'Symfony\\Component\\Config\\ResourceCheckerConfigCache' => __DIR__ . '/..' . '/symfony/config/ResourceCheckerConfigCache.php',
'Symfony\\Component\\Config\\ResourceCheckerConfigCacheFactory' => __DIR__ . '/..' . '/symfony/config/ResourceCheckerConfigCacheFactory.php',
'Symfony\\Component\\Config\\ResourceCheckerInterface' => __DIR__ . '/..' . '/symfony/config/ResourceCheckerInterface.php',
'Symfony\\Component\\Config\\Resource\\ClassExistenceResource' => __DIR__ . '/..' . '/symfony/config/Resource/ClassExistenceResource.php',
'Symfony\\Component\\Config\\Resource\\ComposerResource' => __DIR__ . '/..' . '/symfony/config/Resource/ComposerResource.php',
'Symfony\\Component\\Config\\Resource\\DirectoryResource' => __DIR__ . '/..' . '/symfony/config/Resource/DirectoryResource.php',
'Symfony\\Component\\Config\\Resource\\FileExistenceResource' => __DIR__ . '/..' . '/symfony/config/Resource/FileExistenceResource.php',
'Symfony\\Component\\Config\\Resource\\FileResource' => __DIR__ . '/..' . '/symfony/config/Resource/FileResource.php',
'Symfony\\Component\\Config\\Resource\\GlobResource' => __DIR__ . '/..' . '/symfony/config/Resource/GlobResource.php',
'Symfony\\Component\\Config\\Resource\\ReflectionClassResource' => __DIR__ . '/..' . '/symfony/config/Resource/ReflectionClassResource.php',
'Symfony\\Component\\Config\\Resource\\ResourceInterface' => __DIR__ . '/..' . '/symfony/config/Resource/ResourceInterface.php',
'Symfony\\Component\\Config\\Resource\\SelfCheckingResourceChecker' => __DIR__ . '/..' . '/symfony/config/Resource/SelfCheckingResourceChecker.php',
'Symfony\\Component\\Config\\Resource\\SelfCheckingResourceInterface' => __DIR__ . '/..' . '/symfony/config/Resource/SelfCheckingResourceInterface.php',
'Symfony\\Component\\Config\\Resource\\SkippingResourceChecker' => __DIR__ . '/..' . '/symfony/config/Resource/SkippingResourceChecker.php',
'Symfony\\Component\\Config\\Util\\Exception\\InvalidXmlException' => __DIR__ . '/..' . '/symfony/config/Util/Exception/InvalidXmlException.php',
'Symfony\\Component\\Config\\Util\\Exception\\XmlParsingException' => __DIR__ . '/..' . '/symfony/config/Util/Exception/XmlParsingException.php',
'Symfony\\Component\\Config\\Util\\XmlUtils' => __DIR__ . '/..' . '/symfony/config/Util/XmlUtils.php',
'Symfony\\Component\\Console\\Application' => __DIR__ . '/..' . '/symfony/console/Application.php',
'Symfony\\Component\\Console\\Attribute\\Argument' => __DIR__ . '/..' . '/symfony/console/Attribute/Argument.php',
'Symfony\\Component\\Console\\Attribute\\AsCommand' => __DIR__ . '/..' . '/symfony/console/Attribute/AsCommand.php',
@@ -8131,14 +7685,6 @@ class ComposerStaticInitc91cd9c5b1e6a9e8573a14b799ea9342
'Symfony\\Component\\EventDispatcher\\EventSubscriberInterface' => __DIR__ . '/..' . '/symfony/event-dispatcher/EventSubscriberInterface.php',
'Symfony\\Component\\EventDispatcher\\GenericEvent' => __DIR__ . '/..' . '/symfony/event-dispatcher/GenericEvent.php',
'Symfony\\Component\\EventDispatcher\\ImmutableEventDispatcher' => __DIR__ . '/..' . '/symfony/event-dispatcher/ImmutableEventDispatcher.php',
'Symfony\\Component\\Filesystem\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/filesystem/Exception/ExceptionInterface.php',
'Symfony\\Component\\Filesystem\\Exception\\FileNotFoundException' => __DIR__ . '/..' . '/symfony/filesystem/Exception/FileNotFoundException.php',
'Symfony\\Component\\Filesystem\\Exception\\IOException' => __DIR__ . '/..' . '/symfony/filesystem/Exception/IOException.php',
'Symfony\\Component\\Filesystem\\Exception\\IOExceptionInterface' => __DIR__ . '/..' . '/symfony/filesystem/Exception/IOExceptionInterface.php',
'Symfony\\Component\\Filesystem\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/filesystem/Exception/InvalidArgumentException.php',
'Symfony\\Component\\Filesystem\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/filesystem/Exception/RuntimeException.php',
'Symfony\\Component\\Filesystem\\Filesystem' => __DIR__ . '/..' . '/symfony/filesystem/Filesystem.php',
'Symfony\\Component\\Filesystem\\Path' => __DIR__ . '/..' . '/symfony/filesystem/Path.php',
'Symfony\\Component\\Finder\\Comparator\\Comparator' => __DIR__ . '/..' . '/symfony/finder/Comparator/Comparator.php',
'Symfony\\Component\\Finder\\Comparator\\DateComparator' => __DIR__ . '/..' . '/symfony/finder/Comparator/DateComparator.php',
'Symfony\\Component\\Finder\\Comparator\\NumberComparator' => __DIR__ . '/..' . '/symfony/finder/Comparator/NumberComparator.php',
-671
View File
@@ -593,345 +593,6 @@
],
"install-path": "../brick/math"
},
{
"name": "cakephp/chronos",
"version": "3.5.0",
"version_normalized": "3.5.0.0",
"source": {
"type": "git",
"url": "https://github.com/cakephp/chronos.git",
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/chronos/zipball/e6e777b534244911566face8a5dbdbd7f7bda5a6",
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/clock": "^1.0"
},
"provide": {
"psr/clock-implementation": "1.0"
},
"require-dev": {
"cakephp/cakephp-codesniffer": "^5.0",
"phpunit/phpunit": "^10.5.58 || ^11.5.3 || ^12.1.3"
},
"time": "2026-04-10T02:50:39+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Cake\\Chronos\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brian Nesbitt",
"email": "brian@nesbot.com",
"homepage": "http://nesbot.com"
},
{
"name": "The CakePHP Team",
"homepage": "https://cakephp.org"
}
],
"description": "A simple API extension for DateTime.",
"homepage": "https://cakephp.org",
"keywords": [
"date",
"datetime",
"time"
],
"support": {
"issues": "https://github.com/cakephp/chronos/issues",
"source": "https://github.com/cakephp/chronos"
},
"install-path": "../cakephp/chronos"
},
{
"name": "cakephp/core",
"version": "5.3.6",
"version_normalized": "5.3.6.0",
"source": {
"type": "git",
"url": "https://github.com/cakephp/core.git",
"reference": "9c458b0e9322ec88bc4c758b33cde6a0abf49d12"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/core/zipball/9c458b0e9322ec88bc4c758b33cde6a0abf49d12",
"reference": "9c458b0e9322ec88bc4c758b33cde6a0abf49d12",
"shasum": ""
},
"require": {
"cakephp/utility": "^5.3.0",
"league/container": "^5.1",
"php": ">=8.2",
"psr/container": "^1.1 || ^2.0"
},
"provide": {
"psr/container-implementation": "^2.0"
},
"suggest": {
"cakephp/cache": "To use Configure::store() and restore().",
"cakephp/event": "To use PluginApplicationInterface or plugin applications.",
"league/container": "To use Container and ServiceProvider classes"
},
"time": "2026-05-15T03:31:14+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"functions.php"
],
"psr-4": {
"Cake\\Core\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/core/graphs/contributors"
}
],
"description": "CakePHP Framework Core classes",
"homepage": "https://cakephp.org",
"keywords": [
"cakephp",
"core",
"framework"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/core"
},
"install-path": "../cakephp/core"
},
{
"name": "cakephp/database",
"version": "5.3.6",
"version_normalized": "5.3.6.0",
"source": {
"type": "git",
"url": "https://github.com/cakephp/database.git",
"reference": "442d12bf0a1edeffc555a59de9f955cb0619c7ca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/database/zipball/442d12bf0a1edeffc555a59de9f955cb0619c7ca",
"reference": "442d12bf0a1edeffc555a59de9f955cb0619c7ca",
"shasum": ""
},
"require": {
"cakephp/chronos": "^3.3",
"cakephp/core": "^5.3.0",
"cakephp/datasource": "^5.3.0",
"php": ">=8.2",
"psr/log": "^3.0"
},
"require-dev": {
"cakephp/i18n": "^5.3.0",
"cakephp/log": "^5.3.0"
},
"suggest": {
"cakephp/i18n": "If you are using locale-aware datetime formats.",
"cakephp/log": "If you want to use query logging without providing a logger yourself."
},
"time": "2026-05-21T19:38:13+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Cake\\Database\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/database/graphs/contributors"
}
],
"description": "Flexible and powerful Database abstraction library with a familiar PDO-like API",
"homepage": "https://cakephp.org",
"keywords": [
"abstraction",
"cakephp",
"database",
"database abstraction",
"pdo"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/database"
},
"install-path": "../cakephp/database"
},
{
"name": "cakephp/datasource",
"version": "5.3.6",
"version_normalized": "5.3.6.0",
"source": {
"type": "git",
"url": "https://github.com/cakephp/datasource.git",
"reference": "b768f0c0bf0fca815f83c4caef14e7fbb2409bf5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/datasource/zipball/b768f0c0bf0fca815f83c4caef14e7fbb2409bf5",
"reference": "b768f0c0bf0fca815f83c4caef14e7fbb2409bf5",
"shasum": ""
},
"require": {
"cakephp/core": "^5.3.0",
"php": ">=8.2",
"psr/simple-cache": "^2.0 || ^3.0"
},
"require-dev": {
"cakephp/cache": "^5.3.0",
"cakephp/collection": "^5.3.0",
"cakephp/utility": "^5.3.0"
},
"suggest": {
"cakephp/cache": "If you decide to use Query caching.",
"cakephp/collection": "If you decide to use ResultSetInterface.",
"cakephp/utility": "If you decide to use EntityTrait."
},
"time": "2026-05-20T10:27:33+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Cake\\Datasource\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/datasource/graphs/contributors"
}
],
"description": "Provides connection managing and traits for Entities and Queries that can be reused for different datastores",
"homepage": "https://cakephp.org",
"keywords": [
"cakephp",
"connection management",
"datasource",
"entity",
"query"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/datasource"
},
"install-path": "../cakephp/datasource"
},
{
"name": "cakephp/utility",
"version": "5.3.6",
"version_normalized": "5.3.6.0",
"source": {
"type": "git",
"url": "https://github.com/cakephp/utility.git",
"reference": "4c703a010b9d955fed44731669e35d3043425cc7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/utility/zipball/4c703a010b9d955fed44731669e35d3043425cc7",
"reference": "4c703a010b9d955fed44731669e35d3043425cc7",
"shasum": ""
},
"require": {
"cakephp/core": "^5.3.0",
"php": ">=8.2"
},
"suggest": {
"ext-intl": "To use Text::transliterate() or Text::slug()",
"lib-ICU": "To use Text::transliterate() or Text::slug()"
},
"time": "2026-05-21T19:38:13+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Cake\\Utility\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/utility/graphs/contributors"
}
],
"description": "CakePHP Utility classes such as Inflector, String, Hash, and Security",
"homepage": "https://cakephp.org",
"keywords": [
"cakephp",
"hash",
"inflector",
"security",
"string",
"utility"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/utility"
},
"install-path": "../cakephp/utility"
},
{
"name": "carbonphp/carbon-doctrine-types",
"version": "3.2.0",
@@ -3979,93 +3640,6 @@
],
"install-path": "../league/config"
},
{
"name": "league/container",
"version": "5.2.0",
"version_normalized": "5.2.0.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/container.git",
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/container/zipball/58accbc032f0090a9bd08326f93062c5a658b2c5",
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5",
"shasum": ""
},
"require": {
"php": "^8.1",
"psr/container": "^2.0.2",
"psr/event-dispatcher": "^1.0"
},
"provide": {
"psr/container-implementation": "^1.0"
},
"replace": {
"orno/di": "~2.0"
},
"require-dev": {
"nette/php-generator": "^4.1",
"nikic/php-parser": "^5.0",
"phpstan/phpstan": "^2.1.11",
"phpunit/phpunit": "^10.5.45|^11.5.15|^12.0",
"roave/security-advisories": "dev-latest",
"scrutinizer/ocular": "^1.9",
"squizlabs/php_codesniffer": "^3.9"
},
"time": "2026-03-19T18:52:39+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-1.x": "1.x-dev",
"dev-2.x": "2.x-dev",
"dev-3.x": "3.x-dev",
"dev-4.x": "4.x-dev",
"dev-5.x": "5.x-dev",
"dev-master": "5.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"League\\Container\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Phil Bennett",
"email": "mail@philbennett.co.uk",
"role": "Developer"
}
],
"description": "A fast and intuitive dependency injection container.",
"homepage": "https://github.com/thephpleague/container",
"keywords": [
"container",
"dependency",
"di",
"injection",
"league",
"provider",
"service"
],
"support": {
"issues": "https://github.com/thephpleague/container/issues",
"source": "https://github.com/thephpleague/container/tree/5.2.0"
},
"funding": [
{
"url": "https://github.com/philipobenito",
"type": "github"
}
],
"install-path": "../league/container"
},
{
"name": "league/flysystem",
"version": "3.28.0",
@@ -7572,96 +7146,6 @@
],
"install-path": "../ramsey/uuid"
},
{
"name": "robmorgan/phinx",
"version": "0.16.12",
"version_normalized": "0.16.12.0",
"source": {
"type": "git",
"url": "https://github.com/cakephp/phinx.git",
"reference": "1cce44387a9146dc04c312e6a231b17fb30385cd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/phinx/zipball/1cce44387a9146dc04c312e6a231b17fb30385cd",
"reference": "1cce44387a9146dc04c312e6a231b17fb30385cd",
"shasum": ""
},
"require": {
"cakephp/database": "^5.0.2",
"composer-runtime-api": "^2.0",
"php-64bit": ">=8.1",
"psr/container": "^1.1|^2.0",
"symfony/config": "^4.0|^5.0|^6.0|^7.0|^8.0",
"symfony/console": "^6.0|^7.0|^8.0"
},
"require-dev": {
"cakephp/cakephp-codesniffer": "^5.0",
"cakephp/i18n": "^5.0",
"ext-json": "*",
"ext-pdo": "*",
"phpunit/phpunit": "^10.5",
"symfony/yaml": "^4.0|^5.0|^6.0|^7.0|^8.0"
},
"suggest": {
"ext-json": "Install if using JSON configuration format",
"ext-pdo": "PDO extension is needed",
"symfony/yaml": "Install if using YAML configuration format"
},
"time": "2026-07-03T07:27:17+00:00",
"bin": [
"bin/phinx"
],
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Phinx\\": "src/Phinx/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Rob Morgan",
"email": "robbym@gmail.com",
"homepage": "https://robmorgan.id.au",
"role": "Lead Developer"
},
{
"name": "Woody Gilk",
"email": "woody.gilk@gmail.com",
"homepage": "https://shadowhand.me",
"role": "Developer"
},
{
"name": "Richard Quadling",
"email": "rquadling@gmail.com",
"role": "Developer"
},
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/phinx/graphs/contributors",
"role": "Developer"
}
],
"description": "Phinx makes it ridiculously easy to manage the database migrations for your PHP app.",
"homepage": "https://phinx.org",
"keywords": [
"database",
"database migrations",
"db",
"migrations",
"phinx"
],
"support": {
"issues": "https://github.com/cakephp/phinx/issues",
"source": "https://github.com/cakephp/phinx/tree/0.16.12"
},
"install-path": "../robmorgan/phinx"
},
{
"name": "sebastian/cli-parser",
"version": "2.0.1",
@@ -9365,88 +8849,6 @@
],
"install-path": "../symfony/clock"
},
{
"name": "symfony/config",
"version": "v7.4.14",
"version_normalized": "7.4.14.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
"reference": "7b665e443381ea7c4db03eb03b4bf79ea2b020eb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/7b665e443381ea7c4db03eb03b4bf79ea2b020eb",
"reference": "7b665e443381ea7c4db03eb03b4bf79ea2b020eb",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/filesystem": "^7.1|^8.0",
"symfony/polyfill-ctype": "~1.8"
},
"conflict": {
"symfony/finder": "<6.4",
"symfony/service-contracts": "<2.5"
},
"require-dev": {
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
"symfony/finder": "^6.4|^7.0|^8.0",
"symfony/messenger": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/yaml": "^6.4|^7.0|^8.0"
},
"time": "2026-06-09T07:51:57+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Symfony\\Component\\Config\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/config/tree/v7.4.14"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/config"
},
{
"name": "symfony/console",
"version": "v7.1.3",
@@ -9921,79 +9323,6 @@
],
"install-path": "../symfony/event-dispatcher-contracts"
},
{
"name": "symfony/filesystem",
"version": "v7.4.11",
"version_normalized": "7.4.11.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.8"
},
"require-dev": {
"symfony/process": "^6.4|^7.0|^8.0"
},
"time": "2026-05-11T16:38:44+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Symfony\\Component\\Filesystem\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/filesystem/tree/v7.4.11"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/filesystem"
},
{
"name": "symfony/finder",
"version": "v7.1.3",
+2 -91
View File
@@ -3,7 +3,7 @@
'name' => 'loshmis/vanguard',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'b19963808e5545f0b99bf0eba214b0c85b2f4b7b',
'reference' => 'baf3f6da32fd5bc669ac7360970be21407d1d384',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -82,51 +82,6 @@
'aliases' => array(),
'dev_requirement' => false,
),
'cakephp/chronos' => array(
'pretty_version' => '3.5.0',
'version' => '3.5.0.0',
'reference' => 'e6e777b534244911566face8a5dbdbd7f7bda5a6',
'type' => 'library',
'install_path' => __DIR__ . '/../cakephp/chronos',
'aliases' => array(),
'dev_requirement' => false,
),
'cakephp/core' => array(
'pretty_version' => '5.3.6',
'version' => '5.3.6.0',
'reference' => '9c458b0e9322ec88bc4c758b33cde6a0abf49d12',
'type' => 'library',
'install_path' => __DIR__ . '/../cakephp/core',
'aliases' => array(),
'dev_requirement' => false,
),
'cakephp/database' => array(
'pretty_version' => '5.3.6',
'version' => '5.3.6.0',
'reference' => '442d12bf0a1edeffc555a59de9f955cb0619c7ca',
'type' => 'library',
'install_path' => __DIR__ . '/../cakephp/database',
'aliases' => array(),
'dev_requirement' => false,
),
'cakephp/datasource' => array(
'pretty_version' => '5.3.6',
'version' => '5.3.6.0',
'reference' => 'b768f0c0bf0fca815f83c4caef14e7fbb2409bf5',
'type' => 'library',
'install_path' => __DIR__ . '/../cakephp/datasource',
'aliases' => array(),
'dev_requirement' => false,
),
'cakephp/utility' => array(
'pretty_version' => '5.3.6',
'version' => '5.3.6.0',
'reference' => '4c703a010b9d955fed44731669e35d3043425cc7',
'type' => 'library',
'install_path' => __DIR__ . '/../cakephp/utility',
'aliases' => array(),
'dev_requirement' => false,
),
'carbonphp/carbon-doctrine-types' => array(
'pretty_version' => '3.2.0',
'version' => '3.2.0.0',
@@ -694,15 +649,6 @@
'aliases' => array(),
'dev_requirement' => false,
),
'league/container' => array(
'pretty_version' => '5.2.0',
'version' => '5.2.0.0',
'reference' => '58accbc032f0090a9bd08326f93062c5a658b2c5',
'type' => 'library',
'install_path' => __DIR__ . '/../league/container',
'aliases' => array(),
'dev_requirement' => false,
),
'league/flysystem' => array(
'pretty_version' => '3.28.0',
'version' => '3.28.0.0',
@@ -742,7 +688,7 @@
'loshmis/vanguard' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'b19963808e5545f0b99bf0eba214b0c85b2f4b7b',
'reference' => 'baf3f6da32fd5bc669ac7360970be21407d1d384',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -880,12 +826,6 @@
'aliases' => array(),
'dev_requirement' => false,
),
'orno/di' => array(
'dev_requirement' => false,
'replaced' => array(
0 => '~2.0',
),
),
'paragonie/constant_time_encoding' => array(
'pretty_version' => 'v2.7.0',
'version' => '2.7.0.0',
@@ -1085,8 +1025,6 @@
'dev_requirement' => false,
'provided' => array(
0 => '1.1|2.0',
1 => '^1.0',
2 => '^2.0',
),
),
'psr/event-dispatcher' => array(
@@ -1222,15 +1160,6 @@
0 => '4.7.6',
),
),
'robmorgan/phinx' => array(
'pretty_version' => '0.16.12',
'version' => '0.16.12.0',
'reference' => '1cce44387a9146dc04c312e6a231b17fb30385cd',
'type' => 'library',
'install_path' => __DIR__ . '/../robmorgan/phinx',
'aliases' => array(),
'dev_requirement' => false,
),
'sebastian/cli-parser' => array(
'pretty_version' => '2.0.1',
'version' => '2.0.1.0',
@@ -1462,15 +1391,6 @@
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/config' => array(
'pretty_version' => 'v7.4.14',
'version' => '7.4.14.0',
'reference' => '7b665e443381ea7c4db03eb03b4bf79ea2b020eb',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/config',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/console' => array(
'pretty_version' => 'v7.1.3',
'version' => '7.1.3.0',
@@ -1531,15 +1451,6 @@
0 => '2.0|3.0',
),
),
'symfony/filesystem' => array(
'pretty_version' => 'v7.4.11',
'version' => '7.4.11.0',
'reference' => 'd721ea61b4a5fba8c5b6e7c1feda19efea144b50',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/filesystem',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/finder' => array(
'pretty_version' => 'v7.1.3',
'version' => '7.1.3.0',
+3 -2
View File
@@ -23,7 +23,8 @@ if ($issues) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
throw new \RuntimeException(
'Composer detected issues in your platform: ' . implode(' ', $issues)
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}