Compare commits

...

96 Commits

Author SHA1 Message Date
solocla 66ab44142b fixed save parts with color button 2026-07-16 15:01:40 +02:00
solocla be450f9d85 parts with save button 2026-07-15 16:59:27 +02:00
solocla 2a6eddeb44 fixed import insert 2026-07-15 15:36:30 +02:00
solocla 4d447a5c0d fixed accettatore 2026-07-15 12:08:26 +02:00
solocla c6168dd5d4 Ripristina search_clienti.php sovrascritto per errore 2026-07-15 11:12:59 +02:00
solocla 1c2ea459ca navbar update 2026-07-15 10:29:17 +02:00
solocla 7183189343 Pagina statistiche export LIMS 2026-07-15 10:24:04 +02:00
RMubarakzyanov 1bb9928b24 warm cache for analysis 2026-07-13 19:57:28 +03:00
solocla 173aef51c6 fixed popup 2026-07-11 19:58:10 +02:00
solocla a94eac9fb4 popup dashboard 2026-07-11 19:49:14 +02:00
solocla 51cfd0a434 scroll bar blocked imported 2026-07-11 09:03:17 +02:00
solocla def2715d38 lesillaroutine 2026-07-10 16:38:06 +02:00
solocla 68f344bc06 ranking search matrici 2026-07-10 11:02:45 +02:00
solocla e3f7637b64 changed search matrici strategy 2026-07-10 09:10:06 +02:00
solocla f4ea06fb93 added note field on template and export 2026-07-10 08:28:46 +02:00
solocla 2ab667df22 added fkag clone parts note 2026-07-06 17:06:17 +02:00
solocla 485748859e fixed search clients 2026-07-06 15:54:20 +02:00
solocla 7b961d5818 fixed search client 2026-07-06 15:32:15 +02:00
solocla 500e40782d phinx install 2026-07-04 18:13:21 +02:00
solocla b19963808e favourites button 2026-07-04 15:54:34 +02:00
solocla 8a5859ce46 added free text 2026-07-04 15:46:11 +02:00
solocla 3d2c0c9799 fixed marker 2026-07-04 15:23:49 +02:00
solocla 0130231c64 various fixing 2026-07-04 15:18:48 +02:00
solocla a99810d61b during clone pars ask to clone analysis related 2026-07-04 14:46:44 +02:00
solocla 7473973a34 disabled ClienteFatturazione temporary 2026-07-04 14:28:03 +02:00
solocla 1947730e34 fixed warm cache 2026-07-03 16:48:16 +02:00
solocla e3b1a46482 clienteAnalisi added to validation 2026-07-03 15:52:37 +02:00
solocla 0e16812e9a added Fatturazione in export and fixed attivo inattivo, added desxription to parts 2026-07-03 14:46:04 +02:00
solocla 40dd23e658 fixed headscript 2026-07-02 15:53:38 +02:00
solocla bd47f4f715 fixed topbar2 2026-07-02 15:38:26 +02:00
solocla 042b4698fb fixed topbar 2026-07-02 15:37:56 +02:00
solocla 614bc4feba migration fatturazione script 2026-07-02 15:37:11 +02:00
solocla bef600ae11 added fatturazione avanzata to template and imported 2026-07-02 15:20:55 +02:00
solocla 1476160a38 test enivornment badge 2026-07-02 10:27:28 +02:00
solocla 2c7ddbcfeb fixed flag main 2026-07-01 12:19:11 +02:00
solocla 6ad10c4d8f fixed validation per dup0licate parts, client inactive, fixed flags scheme, update schema json 2026-07-01 11:43:17 +02:00
RMubarakzyanov e0654b4847 fix annotated photos 2026-06-29 22:06:27 +03:00
RMubarakzyanov fda3d66d1f fix ghost columns v2 2026-06-22 22:30:02 +03:00
RMubarakzyanov ff61456d91 fix ghost columns 2026-06-22 20:43:52 +03:00
solocla 089886cb9a fixed button 2026-06-22 15:39:28 +02:00
solocla 4dc010d80e added users admin and roles on menu 2026-06-18 10:54:44 +02:00
solocla 83c6157b10 validation excldugin id 189 tested component 2026-06-16 08:33:32 +02:00
RMubarakzyanov 4dd7b89c22 added mandatory field checking 2026-06-11 13:41:31 +03:00
RMubarakzyanov dec42b4442 Merge branch 'main' into feature/analysis 2026-06-11 09:37:01 +03:00
solocla dab8d9aebf fixed mapping for json 2026-06-08 08:47:17 +02:00
solocla 375a10a678 filter analysis web 2026-06-08 07:40:05 +02:00
solocla 15990be884 fxied column order LIMS 2026-06-04 16:48:22 +02:00
RMubarakzyanov c3a6dd73b6 import backoff 2026-05-28 23:59:15 +03:00
solocla 44ed1186e0 added richmento pelletteria routine 2026-05-28 13:50:52 +02:00
solocla 9050cb1006 routine burberry 2026-05-26 12:15:45 +02:00
solocla e6820fdb62 added order column 2026-05-25 10:59:58 +02:00
solocla 5da37a7836 paulshark routine 2026-05-22 12:27:53 +02:00
solocla c5f27cb69a routine fendi 2026-05-21 10:11:53 +02:00
solocla 1d81d6c996 fixed import update 2026-05-20 18:47:41 +02:00
solocla 0c72dbf5ae sort first with exactly phrase 2026-05-20 17:01:07 +02:00
solocla 8455be04e1 remove hidden from xls import to avoid big file dimension 2026-05-20 16:47:18 +02:00
solocla e42d1b9c51 fix limit dropdown 2026-05-19 16:20:55 +02:00
solocla 3e69e3c322 fixed routine valentino 2026-05-19 11:23:26 +02:00
solocla 0eb4f7a2ad valentino routine 2026-05-19 11:12:18 +02:00
solocla 4f2cfc1930 fix render gird for doppi apici 2026-05-18 15:38:43 +02:00
solocla df075dd76a main field show in imported 2026-05-14 10:14:59 +02:00
solocla 6460454201 fixed main field 2026-05-14 10:00:47 +02:00
solocla 574ddbbd32 fixed propagation 2026-05-13 15:12:13 +02:00
solocla 41f414db5c 2 main 2026-05-13 14:58:13 +02:00
solocla 4a863e8c16 fixed double scroll bar parts dropdown 2026-05-13 14:26:11 +02:00
solocla b431f1d4e9 added top scrollbar imported 2026-05-13 14:22:43 +02:00
solocla f97b52f158 skip empty parts during clone and LIMS export 2026-05-13 11:54:28 +02:00
solocla 836fc055ec cliente reponsabile change based on client 2026-05-12 15:48:35 +02:00
solocla e8dd585df4 added arrows to jump parts 2026-05-12 09:37:52 +02:00
solocla 198b8c08ad Revert "update scheme different obbligatorioweb"
This reverts commit a3eb0f0a57.
2026-05-11 14:47:42 +02:00
solocla 28c467d55e fixed order import 2026-05-11 14:24:35 +02:00
solocla 56eee99a67 change template and import o use also sheet number of XLS 2026-05-09 15:39:43 +02:00
solocla f514b3d2c7 added sheet and config API in insert edit template 2026-05-09 09:44:52 +02:00
solocla a3eb0f0a57 update scheme different obbligatorioweb 2026-05-09 09:30:59 +02:00
solocla aa355905d7 update schema 2026-05-08 11:10:21 +02:00
solocla b38f3e1240 Stop tracking generated schema response file 2026-05-06 16:52:53 +02:00
solocla cbd0c5b68a annotation color and clone templates 2026-05-06 16:44:42 +02:00
solocla f3e5cb4ffd fixed capitolato riferimento 2026-05-05 09:37:18 +02:00
solocla 0f0c6a04b7 fixed Mix 2026-05-05 09:07:14 +02:00
solocla 813bd66f96 scroll table parts 2026-05-04 12:10:12 +02:00
solocla ce00247d1c edit templtae lingue e colori 2026-05-04 11:21:30 +02:00
solocla 3a7dd266c8 fixed quotation 2026-04-29 12:16:44 +02:00
solocla ba8dc4c721 added get 2026-04-29 08:38:16 +02:00
RMubarakzyanov cfbbc36116 Fix TZ Issue 2026-04-28 12:53:21 +03:00
solocla 50d578eea1 commesssa on commessaweb 2026-04-27 16:02:50 +02:00
solocla 6b0d2aa9b9 fixed color and import dahboard 2026-04-27 14:34:45 +02:00
RMubarakzyanov 67bbd9bbbb export analyses 2026-04-21 00:09:59 +03:00
solocla fa7997c980 tested component fixed 2026-04-19 17:38:35 +02:00
solocla 66be442eb6 scrollbar import page 2026-04-17 15:00:17 +02:00
solocla 19a2d6e3f7 scrollbar also top 2026-04-17 10:47:41 +02:00
solocla a15ab08576 Merge branch 'main' of http://192.168.1.93:8418/solocla/trf_certest 2026-04-17 08:43:32 +02:00
solocla f71e8a56b5 cloneparts 2026-04-17 08:43:05 +02:00
RMubarakzyanov cb38bfb75a fix resize on tolims page 2026-04-16 11:39:00 +03:00
solocla 28a708dad3 removed limit admin 2026-04-15 16:19:39 +02:00
solocla 6b9cf20ab9 fixed resize and analysis 2026-04-15 14:37:49 +02:00
solocla d40fc7d177 update gitignore 2026-04-15 08:49:06 +02:00
104 changed files with 15962 additions and 2164 deletions
+8 -1
View File
@@ -40,10 +40,14 @@ yarn-error.log
/public/userarea/logaspi/
/public/userarea/logs/api/
/public/userarea/logs/api/**
/public/userarea/logs/
/public/userarea/logs/**
/public/userarea/photostrf/
/public/userarea/class/*.log
/public/userarea/class/curl_auth_debug.log
/public/userarea/class/curl_request_debug.log
/public/userarea/schema_dettagli_response.json
public/userarea/schemi_base_response.json
# File XLSX temporanei importati
/public/userarea/imported_trf/*.xlsx
@@ -54,4 +58,7 @@ yarn-error.log
/public/photostrf/qrcodes/
# Ignora tutti i log ovunque
*.log
*.log
public/userarea/cache/
public/userarea/error_log.txt
@@ -109,6 +109,8 @@ 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,6 +44,7 @@
"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
+646 -2
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": "9c4f1e3bc3ee2180211c055e70635aef",
"content-hash": "557df76fdde549210349be369d479f5d",
"packages": [
{
"name": "akaunting/laravel-setting",
@@ -251,6 +251,330 @@
],
"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",
@@ -2627,6 +2951,90 @@
],
"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",
@@ -4980,6 +5388,93 @@
],
"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",
@@ -5312,6 +5807,85 @@
],
"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",
@@ -5768,6 +6342,76 @@
],
"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",
@@ -11355,6 +11999,6 @@
"php": "^8.2.0",
"ext-json": "*"
},
"platform-dev": [],
"platform-dev": {},
"plugin-api-version": "2.6.0"
}
@@ -0,0 +1,15 @@
<?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();
}
}
@@ -0,0 +1,20 @@
<?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();
}
}
@@ -0,0 +1,48 @@
<?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();
}
}
@@ -0,0 +1,25 @@
<?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();
}
}
@@ -0,0 +1,57 @@
<?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();
}
}
+27
View File
@@ -0,0 +1,27 @@
<?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',
];
+16 -11
View File
@@ -431,7 +431,7 @@
const emptyEl = modal.querySelector("#analysisEmptyBox");
const errorEl = modal.querySelector("#analysisErrorBox");
const webOnly = webOnlyEl ? webOnlyEl.checked : false;
const webOnly = true;
const searchValue = searchEl ? searchEl.value.trim().toLowerCase() : "";
let visibleCount = 0;
@@ -496,8 +496,10 @@
emptyEl.classList.add("d-none");
}
if (analysisLoadedCache[String(matrixId)]) {
renderAnalysesList(analysisLoadedCache[String(matrixId)]);
const cacheKey = String(matrixId) + "_WEB_ONLY";
if (analysisLoadedCache[cacheKey]) {
renderAnalysesList(analysisLoadedCache[cacheKey]);
return;
}
@@ -509,13 +511,21 @@
dataType: "json",
data: {
id_matrice: matrixId,
web_only: 1,
},
})
.done(function (response) {
const analyses = Array.isArray(response.value)
? response.value
? response.value.filter(function (item) {
return (
item.SelezionabileSuWeb === true ||
item.SelezionabileSuWeb === 1 ||
item.SelezionabileSuWeb === "1"
);
})
: [];
analysisLoadedCache[String(matrixId)] = analyses;
analysisLoadedCache[cacheKey] = analyses;
renderAnalysesList(analyses);
})
.fail(function (xhr) {
@@ -674,12 +684,7 @@
});
}
const webOnlyEl = modal.querySelector("#analysisWebOnly");
if (webOnlyEl) {
webOnlyEl.addEventListener("change", function () {
filterAnalysisList();
});
}
// WEB only is now fixed by default
const searchEl = modal.querySelector("#analysisSearchInput");
if (searchEl) {
+271 -27
View File
@@ -19,9 +19,14 @@ $(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;
// COLORE TESTO LISTA DESCRIZIONI
let globalDescriptionColor = "#000000";
// ===================
// MODAL INITIALIZATION
@@ -53,7 +58,7 @@ $(document).ready(function () {
.data("iddatadb", iddatadb || null)
.data("idquotations", idquotations || null);
loadPhoto(iddatadb, idquotations);
// Parti (non legate al canvas) possono partire subito
loadExistingParts(iddatadb, idquotations);
const modalElement = document.getElementById("annotationsModal");
@@ -72,6 +77,18 @@ $(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);
@@ -130,6 +147,9 @@ $(document).ready(function () {
selectedPartNumber = null;
unsavedChanges = false;
partsListData = [];
freeTextMode = false; // ← aggiungi
freeTextObjects = {}; // ← aggiungi
$("#addFreeTextBtnAnnotations").removeClass("active"); // ← aggiungi
if (fabricCanvas) {
fabricCanvas.off();
fabricCanvas.dispose();
@@ -138,6 +158,7 @@ $(document).ready(function () {
descriptionTextbox = null;
markerObjects = {};
globalMarkerSize = 16;
globalDescriptionColor = "#000000";
$("#photoSelectorContainerAnnotations").empty().hide();
$("#samplePhotoAnnotations").attr("src", "");
$("#partsListAnnotations").empty();
@@ -179,6 +200,60 @@ $(document).ready(function () {
});
// ===================
// 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
// ===================
$(document)
.off("input.descriptionColor", "#descriptionColorPickerAnnotations")
.on(
"input.descriptionColor",
"#descriptionColorPickerAnnotations",
function () {
globalDescriptionColor = $(this).val();
if (descriptionTextbox && fabricCanvas) {
descriptionTextbox.set("fill", globalDescriptionColor);
}
// 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();
},
);
// ===================
// PHOTO LOADERS
// ===================
function loadPhoto(iddatadb, idquotations) {
@@ -349,7 +424,29 @@ $(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.",
@@ -404,6 +501,7 @@ $(document).ready(function () {
updateMarkers();
updateDescriptions();
updateFreeTexts();
}, 10);
});
}
@@ -614,17 +712,11 @@ $(document).ready(function () {
partsListData.forEach((part) => {
const partNumber = part.part_number;
const partDescription = part.part_description;
const isMixPart = String(part.mix || "N").toUpperCase() === "Y";
const partColor =
partColors[partNumber] ||
(partDescription.toLowerCase().startsWith("mix")
? "#0000ff"
: "#ff0000");
if (
partNumber &&
partDescription &&
(showMixParts ||
!partDescription.toLowerCase().startsWith("mix"))
) {
partColors[partNumber] || (isMixPart ? "#0000ff" : "#ff0000");
if (partNumber && partDescription && (showMixParts || !isMixPart)) {
const colorOptions = predefinedColors
.map(
(color) =>
@@ -922,11 +1014,10 @@ $(document).ready(function () {
) {
partsListData = response.parts;
response.parts.forEach((part) => {
const defaultColor = part.part_description
.toLowerCase()
.startsWith("mix")
? "#0000ff"
: "#ff0000";
const isMixPart =
String(part.mix || "N").toUpperCase() === "Y";
const defaultColor = isMixPart ? "#0000ff" : "#ff0000";
partColors[part.part_number] = defaultColor;
});
updatePartsList();
@@ -971,7 +1062,128 @@ $(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
// ===================
@@ -1003,11 +1215,10 @@ $(document).ready(function () {
(p) => p.part_number == marker.partNumber,
);
const partDescription = part ? part.part_description : "";
if (
!showMixParts &&
partDescription &&
partDescription.toLowerCase().startsWith("mix")
) {
const isMixPart =
part && String(part.mix || "N").toUpperCase() === "Y";
if (!showMixParts && isMixPart) {
console.log("Ignoro marker per parte Mix:", marker.partNumber);
return;
}
@@ -1115,11 +1326,10 @@ $(document).ready(function () {
}
const partsList = partsListData
.filter(
(part) =>
showMixParts ||
!part.part_description.toLowerCase().startsWith("mix"),
)
.filter((part) => {
const isMixPart = String(part.mix || "N").toUpperCase() === "Y";
return showMixParts || !isMixPart;
})
.map((part) => `${part.part_number} ${part.part_description}`);
const text = partsList.join("\n");
@@ -1139,7 +1349,7 @@ $(document).ready(function () {
backgroundColor: "transparent",
fontFamily: "Arial",
fontSize: Math.max(16, Math.round(globalMarkerSize * 0.8)),
fill: "#000000",
fill: globalDescriptionColor,
padding: 10,
editable: false,
selectable: true,
@@ -1279,6 +1489,40 @@ $(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")
+3
View File
@@ -3,6 +3,9 @@ ob_start();
session_start();
require_once '../../vendor/autoload.php';
Dotenv\Dotenv::createImmutable(dirname(__DIR__, 2))->safeLoad();
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
$response = ['error' => '', 'rows' => [], 'columns' => [], 'template_id' => 0, 'filename' => '', 'excel_data' => []];
try {
@@ -255,4 +255,39 @@ class VisualLimsApiClient
{
return $this->baseUrl;
}
/**
* Recupera contenuto binario - Adattato per https://bvcpsitaly-elims.com/limsapi
*/
public function getRaw($endpoint)
{
$token = $this->getToken();
// IMPORTANTE: usa /odata/ e NON /api/odata/
$url = "{$this->baseUrl}/odata/{$endpoint}";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer {$token}",
"Accept: */*"
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_error($ch);
curl_close($ch);
if ($response === false) {
throw new Exception("Errore cURL: " . $curl_error);
}
if ($http_code !== 200) {
throw new Exception("HTTP {$http_code} su endpoint: " . $url);
}
return $response;
}
}
+3
View File
@@ -3,6 +3,9 @@ require_once dirname(__DIR__, 3) . '/vendor/autoload.php';
use Dotenv\Dotenv;
Dotenv::createImmutable(dirname(__DIR__, 3))->safeLoad();
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
class DBHandlerSelect
{
private static $instance = null;
@@ -0,0 +1,152 @@
<?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";
+177
View File
@@ -0,0 +1,177 @@
<?php
header('Content-Type: application/json');
include('include/headscript.php');
$dbHandler = DBHandlerSelect::getInstance();
$pdo = $dbHandler->getConnection();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$data = json_decode(file_get_contents('php://input'), true);
$sourceIddatadb = isset($data['source_iddatadb']) ? (int)$data['source_iddatadb'] : 0;
$targetList = $data['target_iddatadb_list'] ?? [];
// 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']);
$targetIds = array_values(array_unique(array_filter(array_map('intval', (array)$targetList), function ($v) use ($sourceIddatadb) {
return $v > 0 && $v !== $sourceIddatadb;
})));
if ($sourceIddatadb <= 0 || empty($targetIds)) {
echo json_encode([
'success' => false,
'message' => 'Missing source or target records'
]);
exit;
}
try {
$pdo->beginTransaction();
// 1. Load source parts
$stmtParts = $pdo->prepare("
SELECT id, part_number, part_description, mix, idmatrice, note, dateexpiry
FROM identification_parts
WHERE iddatadb = ?
AND part_description IS NOT NULL
AND TRIM(part_description) <> ''
ORDER BY part_number ASC, id ASC
");
$stmtParts->execute([$sourceIddatadb]);
$sourceParts = $stmtParts->fetchAll(PDO::FETCH_ASSOC);
if (empty($sourceParts)) {
$pdo->rollBack();
echo json_encode([
'success' => false,
'message' => 'No parts found for source record'
]);
exit;
}
// 2. Prepare statements
$stmtInsertPart = $pdo->prepare("
INSERT INTO identification_parts
(iddatadb, part_number, part_description, mix, idmatrice, note, dateexpiry, created_at, updated_at)
VALUES
(:iddatadb, :part_number, :part_description, :mix, :idmatrice, :note, :dateexpiry, NOW(), NOW())
");
$stmtLoadCF = $pdo->prepare("
SELECT field_id, value_id, value_text
FROM identification_parts_customfields
WHERE part_id = ?
ORDER BY id ASC
");
$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)
VALUES
(:part_id, :field_id, :value_id, :value_text, NOW(), NOW())
");
$details = [];
$totalClonedParts = 0;
$totalClonedAnalyses = 0;
// 3. Clone source parts to each target record
foreach ($targetIds as $targetIddatadb) {
$clonedCountForTarget = 0;
foreach ($sourceParts as $part) {
$stmtInsertPart->execute([
':iddatadb' => $targetIddatadb,
':part_number' => $part['part_number'],
':part_description' => $part['part_description'],
':mix' => $part['mix'] ?? 'N',
':idmatrice' => $part['idmatrice'] !== '' ? $part['idmatrice'] : null,
':note' => $cloneNotes ? ($part['note'] ?? null) : null,
':dateexpiry' => $part['dateexpiry'] ?? null,
]);
$newPartId = (int)$pdo->lastInsertId();
// Load source custom fields for this part
$stmtLoadCF->execute([(int)$part['id']]);
$customFields = $stmtLoadCF->fetchAll(PDO::FETCH_ASSOC);
foreach ($customFields as $cf) {
$stmtInsertCF->execute([
':part_id' => $newPartId,
':field_id' => (int)$cf['field_id'],
':value_id' => ($cf['value_id'] !== null && $cf['value_id'] !== '') ? (int)$cf['value_id'] : null,
':value_text' => $cf['value_text'] !== '' ? $cf['value_text'] : null,
]);
}
// 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++;
}
$details[] = [
'target_iddatadb' => $targetIddatadb,
'cloned_parts' => $clonedCountForTarget
];
}
$pdo->commit();
echo json_encode([
'success' => true,
'source_iddatadb' => $sourceIddatadb,
'cloned_targets' => count($targetIds),
'total_cloned_parts' => $totalClonedParts,
'total_cloned_analyses' => $totalClonedAnalyses,
'details' => $details
]);
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
echo json_encode([
'success' => false,
'message' => 'Clone failed: ' . $e->getMessage()
]);
}
+228
View File
@@ -0,0 +1,228 @@
<?php
require_once 'class/db-functions.php';
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
die("Invalid template ID");
}
$sourceTemplateId = (int)$_GET['id'];
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$pdo->beginTransaction();
// 1. Get source template
$stmt = $pdo->prepare("
SELECT
name,
source_type,
header_row,
start_column,
description,
target_table,
sample_xlsx,
button_size,
button_bg_color,
button_text_color,
button_label,
status,
client_specific_fields,
idclient,
clientname,
schemaname,
idschema,
schemajson,
xls_headers,
api_sample_json,
json_nodes,
idroutine
FROM excel_templates
WHERE id = ?
LIMIT 1
");
$stmt->execute([$sourceTemplateId]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$template) {
throw new Exception("Template not found.");
}
// 2. Generate unique clone name
$baseName = 'Copia di ' . $template['name'];
$newName = $baseName;
$checkStmt = $pdo->prepare("SELECT COUNT(*) FROM excel_templates WHERE name = ?");
$checkStmt->execute([$newName]);
if ((int)$checkStmt->fetchColumn() > 0) {
$counter = 2;
do {
$newName = $baseName . ' (' . $counter . ')';
$checkStmt->execute([$newName]);
$exists = (int)$checkStmt->fetchColumn() > 0;
$counter++;
} while ($exists);
}
// 3. Insert cloned template
$insertTemplate = $pdo->prepare("
INSERT INTO excel_templates (
name,
source_type,
created_at,
updated_at,
header_row,
start_column,
description,
target_table,
sample_xlsx,
button_size,
button_bg_color,
button_text_color,
button_label,
status,
client_specific_fields,
idclient,
clientname,
schemaname,
idschema,
schemajson,
xls_headers,
api_sample_json,
json_nodes,
idroutine
) VALUES (
:name,
:source_type,
NOW(),
NOW(),
:header_row,
:start_column,
:description,
:target_table,
:sample_xlsx,
:button_size,
:button_bg_color,
:button_text_color,
:button_label,
:status,
:client_specific_fields,
:idclient,
:clientname,
:schemaname,
:idschema,
:schemajson,
:xls_headers,
:api_sample_json,
:json_nodes,
:idroutine
)
");
$insertTemplate->execute([
':name' => $newName,
':source_type' => $template['source_type'],
':header_row' => $template['header_row'],
':start_column' => $template['start_column'],
':description' => $template['description'],
':target_table' => $template['target_table'],
':sample_xlsx' => $template['sample_xlsx'],
':button_size' => $template['button_size'],
':button_bg_color' => $template['button_bg_color'],
':button_text_color' => $template['button_text_color'],
':button_label' => $template['button_label'],
':status' => $template['status'],
':client_specific_fields' => $template['client_specific_fields'],
':idclient' => $template['idclient'],
':clientname' => $template['clientname'],
':schemaname' => $template['schemaname'],
':idschema' => $template['idschema'],
':schemajson' => $template['schemajson'],
':xls_headers' => $template['xls_headers'],
':api_sample_json' => $template['api_sample_json'],
':json_nodes' => $template['json_nodes'],
':idroutine' => $template['idroutine']
]);
$newTemplateId = (int)$pdo->lastInsertId();
if ($newTemplateId <= 0) {
throw new Exception("Unable to create cloned template.");
}
// 4. Clone template_mapping rows
$cloneMappings = $pdo->prepare("
INSERT INTO template_mapping (
template_id,
schema_id,
field_id,
excel_column,
json_node,
is_manual,
manual_default,
auto_value,
data_type,
is_required,
default_value,
has_list,
length,
decimals,
min_value,
max_value,
default_curr_date,
tablename,
field_label,
main_field,
is_visible_import,
is_visible_parts
)
SELECT
:new_template_id,
schema_id,
field_id,
excel_column,
json_node,
is_manual,
manual_default,
auto_value,
data_type,
is_required,
default_value,
has_list,
length,
decimals,
min_value,
max_value,
default_curr_date,
tablename,
field_label,
main_field,
is_visible_import,
is_visible_parts
FROM template_mapping
WHERE template_id = :source_template_id
");
$cloneMappings->execute([
':new_template_id' => $newTemplateId,
':source_template_id' => $sourceTemplateId
]);
$pdo->commit();
header("Location: templates_dashboard.php?cloned=1&new_id=" . $newTemplateId);
exit;
} catch (Exception $e) {
if (isset($pdo) && $pdo->inTransaction()) {
$pdo->rollBack();
}
die("Clone error: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'));
}
+18 -22
View File
@@ -23,19 +23,17 @@ if ((int)$stmt->fetchColumn() > 0) {
}
/**
* FIXED FIELDS STANDARD (no UI selection)
* is_manual always 1
* is_visible_import default 1
* FIXED FIELDS STANDARD
*/
$fixedFields = [
// fixed_field_key, data_type
['ClienteResponsabile', 'INT'],
['ClienteFornitore', 'INT'],
['ClienteAnalisi', 'INT'],
['MoltiplicatorePrezzo', 'INT'],
['AnagraficaCertestObject', 'INT'],
['ClienteResponsabile', 'INT'],
['ClienteFornitore', 'INT'],
['ClienteAnalisi', 'INT'],
['ClienteFatturazione', 'INT'], // ← NUOVO
['MoltiplicatorePrezzo', 'INT'],
['AnagraficaCertestObject', 'INT'],
['AnagraficaCertestService', 'INT'],
['ConsegnaRichiesta', 'DATE'],
['ConsegnaRichiesta', 'DATE'],
];
@@ -43,12 +41,11 @@ 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([
@@ -58,16 +55,15 @@ 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);
@@ -17,6 +17,8 @@ try {
exit(1);
}
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
// Recupera le variabili d'ambiente
$dbHost = $_ENV['DB_HOST'];
$dbName = $_ENV['DB_DATABASE'];
+152
View File
@@ -0,0 +1,152 @@
<?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";
+112
View File
@@ -0,0 +1,112 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
ini_set('display_errors', '0');
error_reporting(E_ALL);
/**
* Uso:
* download_rapporto_file.php?id=123
*/
try {
$api = VisualLimsApiClient::getInstance();
$id = isset($_GET['id']) ? intval($_GET['id']) : 0;
if (!$id) {
throw new Exception("ID RapportoFile mancante");
}
$debugDir = __DIR__ . '/logs/lims_get_commessa/';
if (!is_dir($debugDir)) {
mkdir($debugDir, 0755, true);
}
/**
* Recupero il record RapportoFile.
*/
$data = $api->get("RapportoFile({$id})", []);
file_put_contents(
$debugDir . "rapporto_file_{$id}.json",
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
/**
* Provo vari nomi campo possibili per il contenuto PDF.
* Dopo il primo test potremo adattarlo al nome esatto.
*/
$possibleContentFields = [
'File',
'file',
'Content',
'content',
'FileContent',
'fileContent',
'Contenuto',
'contenuto',
'Bytes',
'bytes'
];
$base64 = null;
foreach ($possibleContentFields as $field) {
if (!empty($data[$field])) {
$base64 = $data[$field];
break;
}
}
if (!$base64) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => false,
'message' => 'Record RapportoFile recuperato, ma non ho trovato un campo base64 del PDF.',
'hint' => 'Apri logs/lims_get_commessa/rapporto_file_' . $id . '.json e controlla il nome reale del campo file.',
'data' => $data
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
/**
* Se il contenuto ha prefisso data URI, lo pulisco.
*/
if (strpos($base64, 'base64,') !== false) {
$parts = explode('base64,', $base64);
$base64 = end($parts);
}
$pdfBinary = base64_decode($base64, true);
if ($pdfBinary === false) {
throw new Exception("Il contenuto trovato non è un base64 valido");
}
$filename = $data['NomeFile']
?? $data['nomeFile']
?? $data['FileName']
?? $data['fileName']
?? "rapporto_{$id}.pdf";
if (!str_ends_with(strtolower($filename), '.pdf')) {
$filename .= '.pdf';
}
header('Content-Type: application/pdf');
header('Content-Disposition: inline; filename="' . basename($filename) . '"');
header('Content-Length: ' . strlen($pdfBinary));
echo $pdfBinary;
} catch (Exception $e) {
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+273 -15
View File
@@ -25,6 +25,51 @@ if (!$template) {
$stmt = $pdo->prepare("SELECT * FROM routine");
$stmt->execute();
$routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Retrieve active API/JSON configurations
$stmt = $pdo->prepare("
SELECT id, name, provider_code, api_type, php_class_name
FROM api_configurations
WHERE is_active = 1
ORDER BY name ASC
");
$stmt->execute();
$apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
$buttonBgPalette = [
'#0d6efd' => 'Blue',
'#6610f2' => 'Indigo',
'#6f42c1' => 'Purple',
'#d63384' => 'Pink',
'#dc3545' => 'Red',
'#fd7e14' => 'Orange',
'#ffc107' => 'Yellow',
'#198754' => 'Green',
'#20c997' => 'Teal',
'#0dcaf0' => 'Cyan',
'#212529' => 'Dark',
'#6c757d' => 'Gray',
'#495057' => 'Slate',
'#795548' => 'Brown',
'#2f5d50' => 'Sage Green',
];
$buttonTextPalette = [
'#ffffff' => 'White',
'#6c757d' => 'Gray',
'#000000' => 'Black',
];
$currentButtonBgColor = strtolower($template['button_bg_color'] ?? '#007bff');
$currentButtonTextColor = strtolower($template['button_text_color'] ?? '#ffffff');
if (!array_key_exists($currentButtonBgColor, array_change_key_case($buttonBgPalette, CASE_LOWER))) {
$currentButtonBgColor = '#0d6efd';
}
if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonTextPalette, CASE_LOWER))) {
$currentButtonTextColor = '#ffffff';
}
?>
<!doctype html>
<html lang="en">
@@ -35,6 +80,64 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<?php include('cssinclude.php'); ?>
<style>
.color-palette {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 8px;
}
.color-option {
position: relative;
cursor: pointer;
}
.color-option input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.color-swatch {
width: 42px;
height: 42px;
border-radius: 10px;
border: 2px solid #d6dbe0;
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
transition: all 0.18s ease;
}
.color-option input:checked+.color-swatch {
border: 3px solid #111827;
transform: scale(1.08);
box-shadow: 0 0 0 4px rgba(13, 110, 253, 0.18);
}
.color-swatch .check-mark {
display: none;
font-size: 18px;
font-weight: 700;
color: #ffffff;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
}
.color-option input:checked+.color-swatch .check-mark {
display: inline-block;
}
.color-name {
display: block;
font-size: 11px;
text-align: center;
margin-top: 4px;
color: #555;
max-width: 55px;
}
</style>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<title>Edit Template <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
@@ -88,6 +191,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<select name="source_type" id="sourceType" class="form-control" required>
<option value="XLS" <?php echo (($template['source_type'] ?? 'XLS') === 'XLS') ? 'selected' : ''; ?>>XLS</option>
<option value="API" <?php echo (($template['source_type'] ?? 'XLS') === 'API') ? 'selected' : ''; ?>>API</option>
<option value="JSON" <?php echo (($template['source_type'] ?? 'XLS') === 'JSON') ? 'selected' : ''; ?>>JSON</option>
<option value="PDF" <?php echo (($template['source_type'] ?? 'XLS') === 'PDF') ? 'selected' : ''; ?>>PDF</option>
</select>
<small class="text-muted">Choose the source used by this template</small>
</div>
@@ -102,6 +207,60 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<input type="text" name="start_column" id="startColumn" class="form-control" value="<?php echo htmlspecialchars($template['start_column'] ?? ''); ?>">
</div>
<div class="mb-3" id="xlsSheetNumberWrapper">
<label class="form-label">XLS Sheet Number</label>
<input
type="number"
name="xls_sheet_index"
id="xlsSheetIndex"
class="form-control"
min="0"
value="<?php echo htmlspecialchars($template['xls_sheet_index'] ?? 0); ?>">
<small class="text-muted">
Use 0 for the first sheet, 1 for the second sheet, 2 for the third sheet, and so on.
</small>
</div>
<div class="mb-3" id="apiConfigWrapper" style="display: none;">
<label class="form-label">API / JSON Configuration *</label>
<select name="api_config_id" id="apiConfigSelect" class="form-control">
<option value="">Select an API configuration...</option>
<?php foreach ($apiConfigurations as $apiConfig): ?>
<?php
$apiLabelParts = [];
if (!empty($apiConfig['name'])) {
$apiLabelParts[] = $apiConfig['name'];
}
if (!empty($apiConfig['provider_code'])) {
$apiLabelParts[] = '[' . $apiConfig['provider_code'] . ']';
}
if (!empty($apiConfig['api_type'])) {
$apiLabelParts[] = '(' . $apiConfig['api_type'] . ')';
}
if (!empty($apiConfig['php_class_name'])) {
$apiLabelParts[] = '- ' . $apiConfig['php_class_name'];
}
$apiLabel = implode(' ', $apiLabelParts);
?>
<option
value="<?php echo (int)$apiConfig['id']; ?>"
<?php echo ((int)($template['api_config_id'] ?? 0) === (int)$apiConfig['id']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($apiLabel, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
<small class="text-muted">
Select the API/JSON configuration linked to this template.
</small>
</div>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></label>
<textarea name="description" class="form-control"><?php echo htmlspecialchars($template['description'] ?? ''); ?></textarea>
@@ -123,12 +282,46 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<div class="mb-3">
<label class="form-label">Button Background Color</label>
<input type="color" name="button_bg_color" class="form-control form-control-color" value="<?php echo htmlspecialchars($template['button_bg_color'] ?? '#007bff'); ?>">
<div class="color-palette">
<?php foreach ($buttonBgPalette as $colorValue => $colorLabel): ?>
<?php $isChecked = strtolower($colorValue) === $currentButtonBgColor; ?>
<label class="color-option" title="<?= htmlspecialchars($colorLabel, ENT_QUOTES, 'UTF-8'); ?>">
<input
type="radio"
name="button_bg_color"
value="<?= htmlspecialchars($colorValue, ENT_QUOTES, 'UTF-8'); ?>"
<?= $isChecked ? 'checked' : ''; ?>>
<span class="color-swatch" style="background-color: <?= htmlspecialchars($colorValue, ENT_QUOTES, 'UTF-8'); ?>;">
<span class="check-mark"></span>
</span>
<span class="color-name"><?= htmlspecialchars($colorLabel, ENT_QUOTES, 'UTF-8'); ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="mb-3">
<label class="form-label">Button Text Color</label>
<input type="color" name="button_text_color" class="form-control form-control-color" value="<?php echo htmlspecialchars($template['button_text_color'] ?? '#ffffff'); ?>">
<div class="color-palette">
<?php foreach ($buttonTextPalette as $colorValue => $colorLabel): ?>
<?php $isChecked = strtolower($colorValue) === $currentButtonTextColor; ?>
<label class="color-option" title="<?= htmlspecialchars($colorLabel, ENT_QUOTES, 'UTF-8'); ?>">
<input
type="radio"
name="button_text_color"
value="<?= htmlspecialchars($colorValue, ENT_QUOTES, 'UTF-8'); ?>"
<?= $isChecked ? 'checked' : ''; ?>>
<span class="color-swatch" style="background-color: <?= htmlspecialchars($colorValue, ENT_QUOTES, 'UTF-8'); ?>;">
<span class="check-mark"></span>
</span>
<span class="color-name"><?= htmlspecialchars($colorLabel, ENT_QUOTES, 'UTF-8'); ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="mb-3">
@@ -208,10 +401,16 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
const routineAction3 = document.getElementById("routineAction3");
const sourceType = document.getElementById("sourceType");
const headerRowWrapper = document.getElementById("headerRowWrapper");
const startColumnWrapper = document.getElementById("startColumnWrapper");
const xlsSheetNumberWrapper = document.getElementById("xlsSheetNumberWrapper");
const apiConfigWrapper = document.getElementById("apiConfigWrapper");
const headerRow = document.getElementById("headerRow");
const startColumn = document.getElementById("startColumn");
const xlsSheetIndex = document.getElementById("xlsSheetIndex");
const apiConfigSelect = document.getElementById("apiConfigSelect");
const selectedClientId = <?php echo json_encode((int)($template['idclient'] ?? 0)); ?>;
const selectedSchemaId = <?php echo json_encode((int)($template['idschema'] ?? 0)); ?>;
@@ -231,33 +430,71 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
allowClear: true
});
$('#apiConfigSelect').select2({
placeholder: "Select an API configuration...",
allowClear: true
});
function updateSourceFields() {
const selectedSource = sourceType.value;
if (selectedSource === 'API') {
headerRowWrapper.style.opacity = '0.6';
startColumnWrapper.style.opacity = '0.6';
const isXls = selectedSource === 'XLS';
const isApiOrJson = selectedSource === 'API' || selectedSource === 'JSON';
headerRow.required = false;
startColumn.required = false;
headerRow.disabled = true;
startColumn.disabled = true;
} else {
headerRowWrapper.style.opacity = '1';
startColumnWrapper.style.opacity = '1';
if (isXls) {
headerRowWrapper.style.display = 'block';
startColumnWrapper.style.display = 'block';
xlsSheetNumberWrapper.style.display = 'block';
headerRow.required = true;
startColumn.required = true;
headerRow.disabled = false;
startColumn.disabled = false;
xlsSheetIndex.disabled = false;
apiConfigWrapper.style.display = 'none';
apiConfigSelect.required = false;
apiConfigSelect.disabled = true;
$('#apiConfigSelect').val(null).trigger('change');
} else {
headerRowWrapper.style.display = 'none';
startColumnWrapper.style.display = 'none';
xlsSheetNumberWrapper.style.display = 'none';
headerRow.required = false;
startColumn.required = false;
headerRow.disabled = true;
startColumn.disabled = true;
xlsSheetIndex.disabled = true;
if (isApiOrJson) {
apiConfigWrapper.style.display = 'block';
apiConfigSelect.required = true;
apiConfigSelect.disabled = false;
} else {
apiConfigWrapper.style.display = 'none';
apiConfigSelect.required = false;
apiConfigSelect.disabled = true;
$('#apiConfigSelect').val(null).trigger('change');
}
}
}
sourceType.addEventListener('change', updateSourceFields);
updateSourceFields();
function formatClientLabel(client) {
const nome = client.Nominativo || "Name not available";
const id = client.IdCliente || "";
const codiceCliente = (client.CodiceCliente ?? client.codiceCliente ?? "").toString().trim();
const suffix = (codiceCliente.split("_")[1] || "").trim();
const shortCode = suffix || (codiceCliente ? codiceCliente.charAt(0) : "--");
return `${nome.trim()} - ${shortCode} (ID: ${id})`;
}
async function loadClients() {
try {
clientLoadingStatus.style.display = 'inline';
@@ -280,9 +517,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
select.innerHTML = '<option value="">Select a client...</option>';
data.value.forEach(client => {
const nome = client.Nominativo || "Name not available";
const id = client.IdCliente || "";
const option = new Option(`${nome.trim()} (ID: ${id})`, id);
const option = new Option(formatClientLabel(client), id);
if (parseInt(id) === parseInt(selectedClientId)) {
option.selected = true;
@@ -468,6 +704,28 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
const routineId = routineSelect.value;
formData.append("idroutine", routineId);
const selectedSource = sourceType.value;
if ((selectedSource === 'API' || selectedSource === 'JSON') && !apiConfigSelect.value) {
Swal.fire({
title: "Error!",
text: "Please select an API/JSON configuration.",
icon: "error",
confirmButtonText: "OK"
});
return;
}
if (selectedSource === 'XLS' && xlsSheetIndex.value === '') {
Swal.fire({
title: "Error!",
text: "Please enter the XLS sheet number.",
icon: "error",
confirmButtonText: "OK"
});
return;
}
fetch("process_edit_template_xls.php", {
method: "POST",
body: formData
+46
View File
@@ -328,3 +328,49 @@
2026-03-26 10:57:41 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:57:56 [AnagraficaCertestObject] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:58:11 [AnagraficaCertestService] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-04-30 15:01:25 - Errore nel recupero dati: HTTP 404, Risposta: {"title":"Not Found","status":404,"detail":"Not Found","instance":"GET /api/odata/Rapporto(2621521)","errorCode":"d25cbd678"}
2026-04-30 15:02:04 - Errore nel recupero dati: HTTP 404, Risposta:
2026-04-30 15:03:19 - Errore nella richiesta: URL rejected: Malformed input to a URL function
2026-06-16 14:29:15 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21037 ms: Couldn't connect to server, Risposta:
2026-06-16 14:29:36 [ClienteResponsabile] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21099 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:03 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21076 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:03 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21083 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:03 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21083 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:34 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21043 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:55 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21053 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:55 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21050 ms: Couldn't connect to server, Risposta:
2026-06-16 14:31:46 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21048 ms: Couldn't connect to server, Risposta:
2026-06-16 14:31:46 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21043 ms: Couldn't connect to server, Risposta:
2026-06-16 14:31:46 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21042 ms: Couldn't connect to server, Risposta:
2026-06-16 14:32:07 [ClienteResponsabile] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21047 ms: Couldn't connect to server, Risposta:
2026-06-16 14:33:09 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21058 ms: Couldn't connect to server, Risposta:
2026-06-16 14:33:09 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21057 ms: Couldn't connect to server, Risposta:
2026-06-16 14:33:09 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21057 ms: Couldn't connect to server, Risposta:
2026-06-16 14:33:30 [ClienteResponsabile] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21047 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:11 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21062 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:11 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21064 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:11 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21065 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:33 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21049 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:33 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21057 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:33 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21053 ms: Couldn't connect to server, Risposta:
2026-06-16 14:37:13 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21032 ms: Couldn't connect to server, Risposta:
2026-06-16 14:37:14 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21031 ms: Couldn't connect to server, Risposta:
2026-06-16 14:37:14 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21027 ms: Couldn't connect to server, Risposta:
2026-06-16 14:37:35 [ClienteResponsabile] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21054 ms: Couldn't connect to server, Risposta:
2026-06-16 17:13:55 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21067 ms: Couldn't connect to server, Risposta:
2026-06-16 17:14:25 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21065 ms: Couldn't connect to server, Risposta:
2026-06-16 17:14:46 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21071 ms: Couldn't connect to server, Risposta:
2026-06-16 17:14:46 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21078 ms: Couldn't connect to server, Risposta:
2026-07-09 08:06:33 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:33 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:33 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:33 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:33 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:41 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:41 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:41 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:41 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:41 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-10 08:12:24 - Errore nella richiesta: SSL connection timeout
+239 -22
View File
@@ -15,6 +15,40 @@ 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/';
@@ -59,6 +93,49 @@ function formatDateToExport($value)
return null; // Imposta null se non è una data valida
}
// ImportaCommessa con retry: la chiamata è asincrona lato LIMS e a volte
// risponde 200 senza importare (StatoCommessaWeb resta "Inviata"/"Nuova").
// Riprova con backoff esponenziale finché non passa a "Elaborata".
function importaCommessaWithRetry($api, $commessaId, array $payload, $maxRetries = 3, $initialBackoff = 1)
{
$result = null;
$stato = null;
$succeeded = false;
$log = "";
$backoff = $initialBackoff;
set_time_limit(120); // i backoff non devono far scadere il timeout della richiesta
for ($attempt = 1; $attempt <= $maxRetries + 1; $attempt++) {
try {
$result = $api->post("CommessaWeb({$commessaId})/ImportaCommessa", $payload);
$stato = $result['StatoCommessaWeb'] ?? null;
$log .= "Attempt {$attempt}: HTTP 200, StatoCommessaWeb=" . ($stato ?? 'null') . "\n";
} catch (Exception $e) {
$stato = null;
$log .= "Attempt {$attempt}: EXCEPTION " . $e->getMessage() . "\n";
}
if ($stato === 'Elaborata') {
$succeeded = true;
break;
}
if ($attempt <= $maxRetries) {
$log .= " -> not Elaborata, waiting {$backoff}s before retry\n";
sleep($backoff);
$backoff *= 2;
}
}
return [
'succeeded' => $succeeded,
'stato' => $stato,
'result' => $result,
'log' => $log,
];
}
try {
$iddatadb = $_POST['iddatadb'] ?? null;
if (!$iddatadb) {
@@ -73,13 +150,14 @@ 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,
SELECT d.idclient AS clienteId, et.idschema AS schemaId, et.id AS templateId,
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
@@ -95,6 +173,7 @@ 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;
@@ -103,15 +182,18 @@ 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
FROM identification_parts
WHERE iddatadb = :iddatadb
ORDER BY CAST(part_number AS UNSIGNED) ASC, part_number ASC
");
SELECT id AS part_id, part_number, part_description, material, color, mix, idmatrice, dateexpiry, note
FROM identification_parts
WHERE iddatadb = :iddatadb
AND part_description IS NOT NULL
AND TRIM(part_description) <> ''
ORDER BY CAST(part_number AS UNSIGNED) ASC, part_number ASC
");
$stmt->execute(['iddatadb' => $iddatadb]);
$parts = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -131,6 +213,7 @@ try {
}
}
// 🔹 STEP 4a: Auto-populate export_date / export_time fields
$stmt = $pdo->prepare("
UPDATE import_data_details idd
@@ -154,7 +237,9 @@ try {
idd.field_value,
m.field_label,
m.schema_id,
m.field_id
m.field_id,
m.field_order,
m.is_addnote
FROM
import_data_details as idd
JOIN template_mapping m ON idd.mapping_id = m.id
@@ -174,6 +259,27 @@ 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);
// Logga i fieldValues in error_log
$logFieldValues = "FieldValues dal DB (iddatadb={$iddatadb}):\n" . json_encode($fieldValues, JSON_PRETTY_PRINT);
error_log($logFieldValues);
@@ -195,6 +301,7 @@ 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
];
@@ -227,13 +334,33 @@ 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" => $part["part_description"] ?? "",
"NoteWeb" => $noteWebValue,
"ConsegnaRichiesta" => !empty($part["dateexpiry"]) ? $part["dateexpiry"] : $consegnaRichiesta,
];
@@ -375,11 +502,10 @@ try {
$logContentPhotos .= "=== Campione {$campioneId} (main) ===\n";
foreach ($photos as $photo) {
$photoPath = $uploadDir . '/' . ltrim($photo['file_path'], './');
$fullPath = realpath($photoPath);
$fullPath = resolvePhotoFullPath($photo['file_path'], $uploadDir, __DIR__);
if (!$fullPath || !file_exists($fullPath)) {
$logContentPhotos .= "SKIP (file not found): {$photoPath}\n";
if ($fullPath === null) {
$logContentPhotos .= "SKIP (file not found): file_path='{$photo['file_path']}'\n";
continue;
}
@@ -432,6 +558,71 @@ try {
$logFilePhotos = $logDir . "commessa_{$commessaId}_photos_step5_2_" . time() . ".txt";
$writeLog($logFilePhotos, $logContentPhotos, "STEP 6.2 - Photos (commessa={$commessaId})");
// 🔹 STEP 6.3: Add Analyses (AnalisiCampione) via Campione({id})/AddAnalisi bound action
$stmt = $pdo->prepare("
SELECT part_id, analysis_recordkey, analysis_name, analysis_method
FROM identification_parts_analyses
WHERE iddatadb = :iddatadb
ORDER BY part_id, id
");
$stmt->execute(['iddatadb' => $iddatadb]);
$analysesRows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$partIdToIndex = [];
foreach ($parts as $idx => $part) {
$partIdToIndex[(int)$part['part_id']] = $idx;
}
$totalAnalyses = count($analysesRows);
$addedAnalyses = 0;
$failedAnalyses = [];
$logContentStep63Analisi = "Analyses for iddatadb={$iddatadb}: total={$totalAnalyses}\n\n";
foreach ($analysesRows as $a) {
$partId = (int)$a['part_id'];
$recordKey = trim((string)($a['analysis_recordkey'] ?? ''));
$idx = $partIdToIndex[$partId] ?? null;
if ($idx === null || !isset($campioni[$idx]) || $recordKey === '') {
$logContentStep63Analisi .= "SKIP (no campione for part_id={$partId} / empty recordkey): '{$recordKey}'\n";
continue;
}
$campioneId = (int)($campioni[$idx]['IdCampione'] ?? 0);
if ($campioneId <= 0) {
$logContentStep63Analisi .= "SKIP (invalid IdCampione for part_id={$partId}): '{$recordKey}'\n";
continue;
}
$payload = ['RecordKey' => $recordKey];
$jsonPayload = json_encode($payload, JSON_UNESCAPED_SLASHES);
$logContentStep63Analisi .= "curl --location --request POST '{$apiBaseUrl}Campione({$campioneId})/AddAnalisi' \\\n" .
"--header 'Content-Type: application/json' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" .
"--data '{$jsonPayload}'\n";
try {
$result = $api->post("Campione({$campioneId})/AddAnalisi", $payload);
$logContentStep63Analisi .= "OK (part_id={$partId}, campione={$campioneId}): " .
($a['analysis_name'] ?? '') . "\n---\n";
$addedAnalyses++;
} catch (Exception $e) {
$errMsg = $e->getMessage();
$logContentStep63Analisi .= "FAIL: {$errMsg}\n---\n";
$failedAnalyses[] = [
'part_id' => $partId,
'campione_id' => $campioneId,
'analysis_recordkey' => $recordKey,
'analysis_name' => $a['analysis_name'] ?? '',
'error' => $errMsg,
];
}
}
$logFileStep63Analisi = $logDir . "commessa_{$commessaId}_analyses_step63_" . time() . ".txt";
$writeLog($logFileStep63Analisi, $logContentStep63Analisi, "STEP 6.3 - AddAnalisi (commessa={$commessaId})");
// 🔹 STEP 7: Update Custom Fields for CommessaWeb
if (!empty($fieldValues)) {
// GET con espansione per CustomField
@@ -484,10 +675,10 @@ try {
}
}
// 🔹 STEP 8: Update datadb with idcommessaweb, commessaweb, and status
// 🔹 STEP 8: Update datadb with idcommessaweb, commessaweb, status and export date
$stmt = $pdo->prepare("
UPDATE datadb
SET idcommessaweb = :idcommessaweb, commessaweb = :commessaweb, status = 'l'
SET idcommessaweb = :idcommessaweb, commessaweb = :commessaweb, status = 'l', export_date = NOW()
WHERE iddatadb = :iddatadb
");
$stmt->execute([
@@ -510,9 +701,8 @@ try {
$writeLog($logFileStep9, $logContentStep9, "STEP 9 - InviaCommessa (commessa={$commessaId})");
// 🔹 STEP 9.5: Importazione da CommessaWeb a Commessa (commentato come richiesto)
// 🔹 STEP 9.5: Importazione da CommessaWeb a Commessa (con retry)
// Supplier call: POST api/odata/CommessaWeb(XXX)/ImportaCommessa
$importUserId = (!empty($lims_global_user_id) && is_numeric($lims_global_user_id))
? (int) $lims_global_user_id
: 285;
@@ -520,17 +710,23 @@ try {
$importPayload = [
"IdUtente" => $importUserId
];
$importResult = $api->post("CommessaWeb({$commessaId})/ImportaCommessa", $importPayload);
$importPayloadLog = json_encode($importPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
// Logga il POST
$importOutcome = importaCommessaWithRetry($api, $commessaId, $importPayload);
$importResult = $importOutcome['result'];
$importStato = $importOutcome['stato'];
$importSucceeded = $importOutcome['succeeded'];
// Logga il POST (tutti i tentativi)
$logContentStep91 = "curl --location --request POST '{$apiBaseUrl}CommessaWeb({$commessaId})/ImportaCommessa' \\\n" .
"--header 'Content-Type: application/json' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" .
"--data '{$importPayloadLog}'\n\n" .
"RESPONSE:\n" . json_encode($importResult, JSON_PRETTY_PRINT);
"ATTEMPTS:\n" . $importOutcome['log'] . "\n" .
"SUCCEEDED: " . ($importSucceeded ? 'yes' : 'NO') . "\n\n" .
"LAST RESPONSE:\n" . json_encode($importResult, JSON_PRETTY_PRINT);
$logFileStep91 = $logDir . "commessa_{$commessaId}_importa_step91_" . time() . ".txt";
$writeLog($logFileStep91, $logContentStep91, "STEP 9.5 - ImportaCommessa (commessa={$commessaId})");
$writeLog($logFileStep91, $logContentStep91, "STEP 9.5 - ImportaCommessa (commessa={$commessaId}, succeeded=" . ($importSucceeded ? '1' : '0') . ")");
// 🔹 STEP 10: GET di controllo post-PATCH
$expand = "CommesseCustomFields(\$expand=CustomField)";
@@ -542,7 +738,23 @@ try {
"RESPONSE:\n" . json_encode($commessaAfterPatch, JSON_PRETTY_PRINT);
$logFileStep10 = $logDir . "commessa_{$commessaId}_get_step10_" . time() . ".txt";
$writeLog($logFileStep10, $logContentStep10, "STEP 10 - GET verify (commessa={$commessaId})");
// 🔹 STEP 10.1: Save final CodiceCommessa into datadb.commessaweb
// After ImportaCommessa, the API returns the final LIMS job code in CodiceCommessa.
// Example: CodiceCommessa = 2614795, CodiceCommessaWeb = 26C0103.
$finalCodiceCommessa = trim((string)($commessaAfterPatch['CodiceCommessa'] ?? ''));
if ($finalCodiceCommessa !== '') {
$stmt = $pdo->prepare("
UPDATE datadb
SET commessaweb = :commessaweb,
status = 'l'
WHERE iddatadb = :iddatadb
");
$stmt->execute([
'commessaweb' => substr($finalCodiceCommessa, 0, 30),
'iddatadb' => $iddatadb
]);
}
// 🔹 STEP 11: Prepare final response
$finalCommessa = [
"Cliente" => $clienteId,
@@ -557,17 +769,21 @@ try {
echo json_encode([
"success" => true,
"idcommessaweb" => $commessaId,
"commessaweb" => $commessaWebCode,
"commessaweb" => $finalCodiceCommessa ?: $commessaWebCode,
"commessaWeb" => $finalCommessa,
"commessaWebApiResponse" => $commessaWeb, // Incluso per debug
"totalCampioni" => count($campioni),
"totalCustomFields" => count($commessaAfterPatch["CommesseCustomFields"] ?? []),
"totalPhotos" => count($photos),
"totalAnalyses" => $totalAnalyses,
"addedAnalyses" => $addedAnalyses,
"failedAnalyses" => $failedAnalyses,
"message" => "Export successful",
"logFiles" => [
"step5_create" => $logFileStep5,
"step5_2_photos" => $logFilePhotos,
"step6_campioni" => $logFileStep6,
"step63_analyses" => $logFileStep63Analisi,
"step7_patch" => $logFileStep7 ?? null,
"step9_1_importa" => $logFileStep91,
"step10_get" => $logFileStep10
@@ -583,6 +799,7 @@ try {
"step5_create" => $logFileStep5 ?? null,
"step5_2_photos" => $logFilePhotos ?? null,
"step6_campioni" => $logFileStep6 ?? null,
"step63_analyses" => $logFileStep63Analisi ?? null,
"step7_patch" => $logFileStep7 ?? null,
"step9_1_importa" => $logFileStep91 ?? null,
"step10_get" => $logFileStep10 ?? null
+33 -8
View File
@@ -16,27 +16,52 @@ try {
exit;
}
$api = VisualLimsApiClient::getInstance();
$api = VisualLimsApiClient::getInstance(); // also loads dotenv as a side-effect
$filter = rawurlencode("Matrice/IdMatrice eq $idMatrice");
$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";
if ($webOnly === 1) {
$filterString .= " and SelezionabileSuWeb eq true";
}
$filter = rawurlencode($filterString);
$endpoint = "Analisi?\$filter={$filter}";
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$full_url = $base_url . $endpoint;
file_put_contents(__DIR__ . '/last_url_analisi.txt', $full_url . PHP_EOL, FILE_APPEND);
// 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;
$data = $api->get($endpoint, []);
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);
}
file_put_contents(
__DIR__ . '/analisi_by_matrice_response.json',
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
if (!$fromCache) {
$data = $api->get($endpoint, []);
if (!$simulate) {
file_put_contents($cache_file, json_encode(['value' => $data['value'] ?? []], 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;
+95
View File
@@ -0,0 +1,95 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
// Campione hardcoded del tuo esempio
$idCampione = 749027;
// Analisi che stai provando ad assegnare
$targetRecordKey = '11;1218320';
$targetIdAnalisi = '1218320';
// Endpoint: recupera il campione espandendo le analisi abilitate
$endpoint = "Campione($idCampione)";
// ATTENZIONE:
// nella tua classe normalmente le opzioni vengono trasformate in query string.
// Qui serve: ?$expand=AnalisiAbilitate
$options = [
'$expand' => 'AnalisiAbilitate'
];
// Debug: salva URL usato
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$query = http_build_query($options);
$query = urldecode($query); // rende leggibile $expand invece di %24expand
$full_url = $base_url . $endpoint . ($query ? '?' . $query : '');
file_put_contents(__DIR__ . '/last_url_analisi_abilitate.txt', $full_url . PHP_EOL, FILE_APPEND);
// Chiamata API
$data = $api->get($endpoint, $options);
// Recupera AnalisiAbilitate dalla risposta
$analisiAbilitate = $data['AnalisiAbilitate'] ?? [];
// Alcune API OData possono restituire collection dentro "value"
if (isset($analisiAbilitate['value']) && is_array($analisiAbilitate['value'])) {
$analisiAbilitate = $analisiAbilitate['value'];
}
// Cerca se il RecordKey / IdAnalisi che stai usando è effettivamente assegnabile
$matches = [];
foreach ($analisiAbilitate as $analisi) {
$recordKey = isset($analisi['RecordKey']) ? (string)$analisi['RecordKey'] : '';
$idAnalisi = isset($analisi['IdAnalisi']) ? (string)$analisi['IdAnalisi'] : '';
if ($recordKey === $targetRecordKey || $idAnalisi === $targetIdAnalisi) {
$matches[] = $analisi;
}
}
// Output diagnostico
$output = [
'success' => true,
'idCampione' => $idCampione,
'request_url' => $full_url,
'targetRecordKey' => $targetRecordKey,
'targetIdAnalisi' => $targetIdAnalisi,
'enabled_analyses_count' => is_array($analisiAbilitate) ? count($analisiAbilitate) : 0,
'target_found' => count($matches) > 0,
'target_matches' => $matches,
'analisi_abilitate' => $analisiAbilitate,
'raw_response' => $data
];
// Salva il JSON in locale
file_put_contents(
__DIR__ . '/analisi_abilitate_campione_749027_response.json',
json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
echo json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log_analisi_abilitate.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+88
View File
@@ -0,0 +1,88 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
// Campione hardcoded del tuo esempio
$idCampione = 749027;
// Matrice attesa dal log STEP 6
$expectedMatriceId = 6430;
// Endpoint con expand della Matrice
$endpoint = "Campione($idCampione)";
$options = [
'$expand' => 'Matrice'
];
// Debug URL
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$query = http_build_query($options);
$queryReadable = urldecode($query);
$full_url = $base_url . $endpoint . ($queryReadable ? '?' . $queryReadable : '');
file_put_contents(
__DIR__ . '/last_url_check_matrice.txt',
$full_url . PHP_EOL,
FILE_APPEND
);
// Chiamata API
$data = $api->get($endpoint, $options);
// Recupero Matrice dalla response
$matrice = $data['Matrice'] ?? null;
$actualMatriceId = null;
if (is_array($matrice)) {
// Provo i nomi più probabili
$actualMatriceId = $matrice['IdMatrice']
?? $matrice['idMatrice']
?? $matrice['Id']
?? $matrice['ID']
?? null;
}
$matrice_ok = ((int)$actualMatriceId === (int)$expectedMatriceId);
$output = [
'success' => true,
'idCampione' => $idCampione,
'expectedMatriceId' => $expectedMatriceId,
'actualMatriceId' => $actualMatriceId,
'matrice_ok' => $matrice_ok,
'request_url' => $full_url,
'matrice' => $matrice,
'raw_response' => $data
];
// Salva JSON completo
file_put_contents(
__DIR__ . '/check_matrice_campione_749027_response.json',
json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
echo json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log_check_matrice.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+1 -1
View File
@@ -30,7 +30,7 @@ try {
// Parametri OData
$params = [
'$select' => 'IdCliente,Nominativo,CodiceCliente',
'$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo',
'$orderby' => 'Nominativo asc'
];
+127
View File
@@ -0,0 +1,127 @@
<?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
@@ -0,0 +1,102 @@
<?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,256 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json; charset=utf-8');
ini_set('display_errors', '0');
error_reporting(E_ALL);
/**
* Uso:
* rapporto_full_by_codice.php?codice=2621521
* rapporto_full_by_codice.php?codice=2621521&download=1
*/
try {
$api = VisualLimsApiClient::getInstance();
$codiceRapporto = trim($_GET['codice'] ?? '');
$downloadPdf = isset($_GET['download']) && $_GET['download'] == '1';
if ($codiceRapporto === '') {
throw new Exception("Parametro codice mancante. Usa ?codice=2621521");
}
$debugDir = __DIR__ . '/logs/lims_rapporti/';
$pdfDir = __DIR__ . '/pdf/rapporti/';
if (!is_dir($debugDir)) mkdir($debugDir, 0755, true);
if (!is_dir($pdfDir)) mkdir($pdfDir, 0755, true);
$codiceRapportoSafe = str_replace("'", "''", $codiceRapporto);
/**
* STEP 1 - Search IdRapporto
*/
$searchParams = [
'$filter' => "CodiceRapporto eq '{$codiceRapportoSafe}'",
'$select' => 'IdRapporto,CodiceRapporto,Data,Versione,Firmato,DataStampa',
'$top' => 1
];
$searchEndpoint = "Rapporto?" . http_build_query($searchParams);
$searchData = $api->get($searchEndpoint);
$rapporti = $searchData['value'] ?? [];
if (empty($rapporti)) {
echo json_encode([
'success' => false,
'message' => 'Nessun rapporto trovato per questo CodiceRapporto.',
'codice_rapporto' => $codiceRapporto
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
$rapportoBase = $rapporti[0];
$idRapporto = intval($rapportoBase['IdRapporto'] ?? 0);
if (!$idRapporto) {
throw new Exception("IdRapporto non trovato nella risposta.");
}
/**
* STEP 2 - Dettaglio Rapporto
*/
$clienteExpandError = null;
$detailEndpoint = "Rapporto({$idRapporto})?\$expand=Cliente,RapportiFiles,CampioniDatiRapporto";
try {
$detailData = $api->get($detailEndpoint);
} catch (Exception $e) {
$clienteExpandError = $e->getMessage();
$detailEndpoint = "Rapporto({$idRapporto})?\$expand=RapportiFiles,CampioniDatiRapporto";
$detailData = $api->get($detailEndpoint);
}
file_put_contents(
$debugDir . "rapporto_{$codiceRapportoSafe}_light.json",
json_encode($detailData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
);
$cliente = $detailData['Cliente'] ?? null;
$rapportiFiles = $detailData['RapportiFiles'] ?? [];
$campioniDatiRapporto = $detailData['CampioniDatiRapporto'] ?? [];
// ====================== CLIENTE ======================
$clienteInfo = [
'found' => is_array($cliente),
'id_cliente' => $cliente['IdCliente'] ?? $cliente['Id'] ?? null,
'codice_cliente' => $cliente['CodiceCliente'] ?? $cliente['Codice'] ?? null,
'nome_cliente' => $cliente['Nominativo'] ?? $cliente['RagioneSociale'] ?? $cliente['Nome'] ?? $cliente['Descrizione'] ?? null,
'partita_iva' => $cliente['PartitaIva'] ?? null,
'codice_fiscale' => $cliente['CodiceFiscale'] ?? null
];
// ====================== FILE PDF ======================
$selectedRapportoFile = null;
$idRapportoFile = null;
$pdfFileName = $codiceRapporto . '.pdf';
$pdfFullPath = $pdfDir . $pdfFileName;
if (is_array($rapportiFiles) && count($rapportiFiles) > 0) {
foreach ($rapportiFiles as $file) {
$fileName = $file['FileName'] ?? '';
$tipoRapporto = $file['TipoRapporto'] ?? '';
$categoria = $file['Categoria'] ?? '';
if (
stripos($fileName, '.pdf') !== false ||
stripos($tipoRapporto, 'Rapporto') !== false ||
stripos($categoria, 'Rapporti') !== false
) {
$selectedRapportoFile = $file;
$idRapportoFile = $file['IdRapportoFile'] ?? null;
break;
}
}
if (!$selectedRapportoFile && count($rapportiFiles) > 0) {
$selectedRapportoFile = $rapportiFiles[0];
$idRapportoFile = $rapportiFiles[0]['IdRapportoFile'] ?? null;
}
}
// ====================== DOWNLOAD PDF - DEBUG AVANZATO ======================
$pdfDownloaded = false;
$pdfError = null;
$endpointUsato = null;
if ($downloadPdf && $idRapportoFile) {
$tentativi = [
"RapportoFile({$idRapportoFile})/\$value",
"RapportoFile({$idRapportoFile})/Contenuto/\$value",
"RapportoFile({$idRapportoFile})/File/\$value",
"Rapporto({$idRapporto})/RapportiFiles({$idRapportoFile})/\$value",
"Rapporto({$idRapporto})/RapportoFile/\$value",
"GetRapportoFile?IdRapportoFile={$idRapportoFile}",
"RapportoFile/Download?Id={$idRapportoFile}",
"Rapporto/DownloadFile?IdRapporto={$idRapporto}"
];
foreach ($tentativi as $ep) {
try {
$pdfContent = $api->getRaw($ep);
if (strlen($pdfContent) > 2000) { // PDF decente deve essere più grande
file_put_contents($pdfFullPath, $pdfContent);
$pdfDownloaded = true;
$endpointUsato = $ep;
$pdfError = "SUCCESSO con: " . $ep;
break;
} else {
$pdfError = "Contenuto troppo piccolo con: " . $ep;
}
} catch (Exception $e) {
$pdfError = "Fallito con " . $ep . "" . $e->getMessage();
}
}
}
// ====================== CAMPIONI + RATING (ripristinato dal tuo originale) ======================
$campioniSintesi = [];
$ratingFinale = null;
$ratingSource = null;
$hasIrregolare = false;
$hasPositiveResults = false;
if (is_array($campioniDatiRapporto)) {
foreach ($campioniDatiRapporto as $campione) {
$giudizioRapporto = $campione['GiudizioRapporto'] ?? null;
$giudizioCertificato = $campione['GiudizioCertificato'] ?? null;
$esitoGiudizioLMR = $campione['EsitoGiudizioLMR'] ?? null;
$esitoGiudizioImpiego = $campione['EsitoGiudizioImpiego'] ?? null;
$risultatiIrregolari = ($campione['RisultatiIrregolari'] ?? false) === true;
$risultatiPositivi = ($campione['RisultatiPositivi'] ?? false) === true;
$campioniSintesi[] = [
'id_campione_dati_rapporto' => $campione['IdCampioneDatiRapporto'] ?? null,
'codice_campione' => $campione['CodiceCampione'] ?? null,
'stato_campione' => $campione['StatoCampione'] ?? null,
'stato_campione_web' => $campione['StatoCampioneWeb'] ?? null,
'matrice' => $campione['Matrice'] ?? null,
'macro_matrice' => $campione['MacroMatrice'] ?? null,
'giudizio_rapporto' => $giudizioRapporto,
'giudizio_certificato' => $giudizioCertificato,
'esito_giudizio_lmr' => $esitoGiudizioLMR,
'esito_giudizio_impiego' => $esitoGiudizioImpiego,
'risultati_positivi' => $risultatiPositivi,
'risultati_irregolari' => $risultatiIrregolari
];
if (!$ratingFinale && !empty($giudizioRapporto)) {
$ratingFinale = $giudizioRapporto;
$ratingSource = 'CampioniDatiRapporto.GiudizioRapporto';
}
if (!$ratingFinale && !empty($giudizioCertificato)) {
$ratingFinale = $giudizioCertificato;
$ratingSource = 'CampioniDatiRapporto.GiudizioCertificato';
}
if ($risultatiIrregolari) $hasIrregolare = true;
if ($risultatiPositivi) $hasPositiveResults = true;
}
}
// Fallback rating
if (!$ratingFinale) {
if ($hasIrregolare) {
$ratingFinale = 'Irregolare';
$ratingSource = 'RisultatiIrregolari';
} elseif ($hasPositiveResults || count($campioniSintesi) > 0) {
$ratingFinale = 'Regolare';
$ratingSource = 'fallback';
}
}
// ====================== RISPOSTA FINALE ======================
echo json_encode([
'success' => true,
'codice_rapporto' => $codiceRapporto,
'id_rapporto' => $idRapporto,
'search_endpoint' => $searchEndpoint,
'detail_endpoint' => $detailEndpoint,
'cliente_expand_error' => $clienteExpandError,
'rapporto_base' => $rapportoBase,
'cliente' => $clienteInfo,
'rating_finale' => $ratingFinale,
'rating_source' => $ratingSource,
'rapporti_files_count' => count($rapportiFiles),
'selected_rapporto_file' => $selectedRapportoFile,
'pdf_file_name' => $pdfFileName,
'pdf_path' => $pdfDownloaded ? $pdfFullPath : null,
'pdf_downloaded' => $pdfDownloaded,
'pdf_error' => $pdfError,
'campioni_count' => count($campioniSintesi),
'campioni_sintesi' => $campioniSintesi,
'download_requested' => $downloadPdf
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+118
View File
@@ -0,0 +1,118 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json; charset=utf-8');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
$commessa = trim($_GET['commessa'] ?? '');
if ($commessa === '') {
throw new Exception("Parametro commessa mancante");
}
$debugDir = __DIR__ . '/logs/lims_rapporto/';
if (!is_dir($debugDir)) {
mkdir($debugDir, 0755, true);
}
/**
* STEP 1
* Provo a cercare il rapporto con filtri leggeri.
* NIENTE expand pesanti.
*/
$attempts = [];
$filters = [
'CodiceCommessa' => "CodiceCommessa eq '{$commessa}'",
'Commessa_CodiceCommessa' => "Commessa/CodiceCommessa eq '{$commessa}'",
'Commessa_IdCommessa' => is_numeric($commessa) ? "Commessa/IdCommessa eq {$commessa}" : null,
'Codice' => "Codice eq '{$commessa}'"
];
foreach ($filters as $label => $filter) {
if (!$filter) {
continue;
}
try {
$options = [
'$filter' => $filter,
'$top' => 10
];
$data = $api->get('Rapporto', $options);
$attempts[$label] = [
'success' => true,
'filter' => $filter,
'records' => isset($data['value']) && is_array($data['value']) ? count($data['value']) : null,
'data' => $data
];
file_put_contents(
$debugDir . "rapporto_{$commessa}_{$label}.json",
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
} catch (Exception $e) {
$attempts[$label] = [
'success' => false,
'filter' => $filter,
'error' => $e->getMessage()
];
}
}
/**
* STEP 2
* Prendo solo eventuali rapporti trovati.
*/
$rapportiFound = [];
foreach ($attempts as $label => $attempt) {
if (!$attempt['success']) {
continue;
}
$items = $attempt['data']['value'] ?? [];
if (!is_array($items)) {
continue;
}
foreach ($items as $item) {
$rapportiFound[] = [
'matched_by' => $label,
'rapporto' => $item
];
}
}
echo json_encode([
'success' => true,
'input_commessa' => $commessa,
'message' => 'Ricerca leggera su Rapporto completata. Se trovi un rapporto, poi recuperiamo RapportiFiles solo per quello.',
'rapporti_found_count' => count($rapportiFound),
'rapporti_found' => $rapportiFound,
'attempts_summary' => array_map(function ($a) {
return [
'success' => $a['success'],
'filter' => $a['filter'],
'records' => $a['records'] ?? null,
'error' => $a['error'] ?? null
];
}, $attempts)
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+125 -10
View File
@@ -2,25 +2,140 @@
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
header('Content-Type: application/json; charset=utf-8');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
$rapporto_id = 533329;
// Costruzione manuale dell'endpoint con espansione annidata
$endpoint = "Rapporto($rapporto_id)?\$expand=CampioniDatiRapporto(\$expand=AnalisiDatiRapporto,CustomFieldsDatiRapporto)";
// Esempi:
// rapporto_by_codice_expand_step.php?codice=2541111&step=base
// rapporto_by_codice_expand_step.php?codice=2541111&step=files
// rapporto_by_codice_expand_step.php?codice=2541111&step=campioni
// rapporto_by_codice_expand_step.php?codice=2541111&step=files_campioni
// Non passiamo options, già incluso nell'endpoint
$data = $api->get($endpoint);
$codiceRapporto = trim($_GET['codice'] ?? '');
$step = trim($_GET['step'] ?? 'base');
file_put_contents(__DIR__ . '/rapporto_expanded.json', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
echo json_encode($data);
if ($codiceRapporto === '') {
throw new Exception("Parametro codice mancante. Usa ?codice=2541111");
}
$allowedSteps = [
'base' => '',
'files' => 'RapportiFiles',
'allegati' => 'RapportiAllegati',
'campioni' => 'CampioniDatiRapporto',
'files_campioni' => 'RapportiFiles,CampioniDatiRapporto'
];
if (!array_key_exists($step, $allowedSteps)) {
throw new Exception("Step non valido. Usa: " . implode(', ', array_keys($allowedSteps)));
}
// Escape OData per eventuali apostrofi
$codiceRapportoSafe = str_replace("'", "''", $codiceRapporto);
/*
* STEP 1 - Trova IdRapporto partendo da CodiceRapporto.
* Query leggera, con $select e $top=1.
*/
$searchParams = [
'$filter' => "CodiceRapporto eq '{$codiceRapportoSafe}'",
'$select' => 'IdRapporto,CodiceRapporto,Data,Versione,Firmato,DataStampa',
'$top' => 1
];
$searchEndpoint = "Rapporto?" . http_build_query($searchParams);
$searchData = $api->get($searchEndpoint);
$items = $searchData['value'] ?? [];
if (!is_array($items) || count($items) === 0) {
echo json_encode([
'success' => false,
'message' => 'Nessun rapporto trovato per questo CodiceRapporto.',
'codice_rapporto' => $codiceRapporto,
'search_endpoint' => $searchEndpoint,
'search_data' => $searchData
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
$rapportoBase = $items[0];
$rapportoId = intval($rapportoBase['IdRapporto'] ?? 0);
if (!$rapportoId) {
throw new Exception("IdRapporto non trovato nella risposta.");
}
/*
* STEP 2 - Se step=base, restituisco solo la ricerca base.
*/
if ($step === 'base') {
echo json_encode([
'success' => true,
'codice_rapporto' => $codiceRapporto,
'id_rapporto' => $rapportoId,
'step' => $step,
'search_endpoint' => $searchEndpoint,
'rapporto_base' => $rapportoBase
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
/*
* STEP 3 - Espande SOLO il rapporto trovato.
*/
$expandValue = $allowedSteps[$step];
$detailParams = [
'$expand' => $expandValue
];
$detailEndpoint = "Rapporto({$rapportoId})?" . http_build_query($detailParams);
file_put_contents(
__DIR__ . '/last_rapporto_by_codice_expand_endpoint.txt',
'[' . date('Y-m-d H:i:s') . '] SEARCH: ' . $searchEndpoint . PHP_EOL .
'[' . date('Y-m-d H:i:s') . '] DETAIL: ' . $detailEndpoint . PHP_EOL,
FILE_APPEND
);
$detailData = $api->get($detailEndpoint);
file_put_contents(
__DIR__ . "/rapporto_codice_{$codiceRapportoSafe}_{$step}.json",
json_encode([
'search' => $searchData,
'detail' => $detailData
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
);
echo json_encode([
'success' => true,
'codice_rapporto' => $codiceRapporto,
'id_rapporto' => $rapportoId,
'step' => $step,
'search_endpoint' => $searchEndpoint,
'detail_endpoint' => $detailEndpoint,
'rapporto_base' => $rapportoBase,
'data' => $detailData
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
file_put_contents(
__DIR__ . '/error_log.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
File diff suppressed because it is too large Load Diff
+379 -61
View File
@@ -10,70 +10,180 @@
<title>Template Buttons - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<style>
/* Layout flessibile per gestire dimensioni diverse */
#templateButtons {
/* Main buttons container */
.template-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
gap: 12px;
justify-content: flex-start;
/* Allinea a sinistra */
padding: 20px;
padding: 10px 0;
}
/* Definizione delle dimensioni */
/* Definizione delle dimensioni */
/* Button sizes */
.btn-small {
font-size: 12px;
padding: 6px 12px;
min-width: 100px;
min-height: 30px;
display: flex;
/* Aggiunto */
min-height: 36px;
display: inline-flex;
justify-content: center;
/* Aggiunto */
align-items: center;
/* Aggiunto */
gap: 8px;
border-radius: 8px;
text-align: center;
}
.btn-medium {
font-size: 16px;
padding: 10px 20px;
min-width: 130px;
min-height: 45px;
display: flex;
/* Aggiunto */
min-width: 140px;
min-height: 48px;
display: inline-flex;
justify-content: center;
/* Aggiunto */
align-items: center;
/* Aggiunto */
gap: 8px;
border-radius: 10px;
text-align: center;
}
.btn-large {
font-size: 20px;
padding: 14px 28px;
min-width: 180px;
min-height: 60px;
display: flex;
/* Aggiunto */
min-width: 190px;
min-height: 64px;
display: inline-flex;
justify-content: center;
/* Aggiunto */
align-items: center;
/* Aggiunto */
gap: 10px;
border-radius: 12px;
text-align: center;
}
/* Stile della barra di ricerca */
.template-icon {
font-size: 18px;
line-height: 1;
}
.btn-large .template-icon {
font-size: 22px;
}
.btn-small .template-icon {
font-size: 15px;
}
/* Search box */
#searchInput {
width: 100%;
padding: 10px;
padding: 10px 14px;
font-size: 16px;
margin-bottom: 15px;
border: 1px solid #ccc;
border-radius: 5px;
margin-bottom: 18px;
border: 1px solid #d9d9d9;
border-radius: 8px;
outline: none;
}
#searchInput:focus {
border-color: #0d6efd;
box-shadow: 0 0 0 0.15rem rgba(13, 110, 253, 0.15);
}
/* Tabs */
.custom-tabs {
border-bottom: 1px solid #e5e5e5;
margin-bottom: 20px;
gap: 6px;
}
.custom-tabs .nav-link {
border: none;
border-radius: 10px 10px 0 0;
color: #555;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 18px;
}
.custom-tabs .nav-link:hover {
background: #f8f9fa;
color: #0d6efd;
}
.custom-tabs .nav-link.active {
background: #0d6efd;
color: #fff;
}
.tab-pane {
min-height: 140px;
}
.empty-message {
color: #6c757d;
font-style: italic;
padding: 10px 0;
}
.loading-message {
color: #6c757d;
padding: 10px 0;
}
.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'); ?>
@@ -87,14 +197,54 @@
<h6 class="mb-0">Active Templates</h6>
</div>
<div class="card-body">
<!-- Barra di ricerca -->
<input type="text" id="searchInput" placeholder="Search template...">
<div id="templateButtons"></div>
<ul class="nav nav-tabs custom-tabs" id="templateTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="xls-tab" data-bs-toggle="tab" data-bs-target="#xls-pane" type="button" role="tab" aria-controls="xls-pane" aria-selected="true">
<i class="bx bx-spreadsheet template-icon"></i>
XLS
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="api-tab" data-bs-toggle="tab" data-bs-target="#api-pane" type="button" role="tab" aria-controls="api-pane" aria-selected="false">
<i class="bx bx-transfer-alt template-icon"></i>
JSON/API
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="pdf-tab" data-bs-toggle="tab" data-bs-target="#pdf-pane" type="button" role="tab" aria-controls="pdf-pane" aria-selected="false">
<i class="bx bx-file-pdf template-icon"></i>
PDF
</button>
</li>
</ul>
<div class="tab-content" id="templateTabsContent">
<div class="tab-pane fade show active" id="xls-pane" role="tabpanel" aria-labelledby="xls-tab">
<div id="templateButtonsXLS" class="template-buttons">
<div class="loading-message">Loading XLS templates...</div>
</div>
</div>
<div class="tab-pane fade" id="api-pane" role="tabpanel" aria-labelledby="api-tab">
<div id="templateButtonsAPI" class="template-buttons">
<div class="loading-message">Loading API templates...</div>
</div>
</div>
<div class="tab-pane fade" id="pdf-pane" role="tabpanel" aria-labelledby="pdf-tab">
<div id="templateButtonsPDF" class="template-buttons">
<div class="loading-message">Loading PDF templates...</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
@@ -104,46 +254,214 @@
<script>
document.addEventListener("DOMContentLoaded", function() {
const allTemplates = [];
const containers = {
XLS: document.getElementById("templateButtonsXLS"),
API: document.getElementById("templateButtonsAPI"),
PDF: document.getElementById("templateButtonsPDF")
};
function getSizeClass(buttonSize) {
if (buttonSize === "small") return "btn-small";
if (buttonSize === "large") return "btn-large";
return "btn-medium";
}
function getTemplateIcon(sourceType) {
switch ((sourceType || '').toUpperCase()) {
case 'XLS':
return 'bx bx-spreadsheet';
case 'API':
return 'bx bx-transfer-alt';
case 'PDF':
return 'bx bx-file-pdf';
default:
return 'bx bx-file';
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function createButtonWrapper(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}`;
btn.className = `btn ${sizeClass}`;
btn.style.backgroundColor = template.button_bg_color || '#0d6efd';
btn.style.color = template.button_text_color || '#ffffff';
btn.setAttribute("data-label", (template.button_label || '').toLowerCase());
btn.setAttribute("data-source-type", sourceType);
btn.innerHTML = `
<i class="${iconClass} template-icon"></i>
<span>${escapeHtml(template.button_label || 'Unnamed')}</span>
`;
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;
}
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 clearContainers() {
Object.values(containers).forEach(container => {
container.innerHTML = '';
});
}
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();
const grouped = {
XLS: [],
API: [],
PDF: []
};
allTemplates.forEach(template => {
const sourceType = (template.source_type || '').toUpperCase();
const label = (template.button_label || '').toLowerCase();
if (searchValue && !label.includes(searchValue)) {
return;
}
if (grouped[sourceType]) {
grouped[sourceType].push(template);
}
});
Object.keys(grouped).forEach(type => {
const container = containers[type];
if (!container) return;
if (grouped[type].length === 0) {
container.innerHTML = `<div class="empty-message">No templates found in this section.</div>`;
return;
}
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);
});
}
fetch("load_active_templates.php")
.then(response => response.json())
.then(data => {
if (!data.success) {
console.error("Error loading templates:", data.message);
clearContainers();
Object.values(containers).forEach(container => {
container.innerHTML = `<div class="empty-message">Error loading templates.</div>`;
});
return;
}
let templateButtons = document.getElementById("templateButtons");
templateButtons.innerHTML = '';
data.data.forEach(template => {
let sizeClass = "btn-medium"; // Default
if (template.button_size === "small") sizeClass = "btn-small";
if (template.button_size === "large") sizeClass = "btn-large";
let btn = document.createElement("a");
btn.href = `import_xls2.php?id=${template.id}`;
btn.className = `btn ${sizeClass}`;
btn.style.backgroundColor = template.button_bg_color;
btn.style.color = template.button_text_color;
btn.textContent = template.button_label;
btn.setAttribute("data-label", template.button_label.toLowerCase()); // Attributo per ricerca
templateButtons.appendChild(btn);
});
})
.catch(error => console.error("Fetch error:", error));
// Funzione per la ricerca live
document.getElementById("searchInput").addEventListener("input", function() {
let searchValue = this.value.toLowerCase();
document.querySelectorAll("#templateButtons a").forEach(btn => {
let label = btn.getAttribute("data-label");
if (label.includes(searchValue)) {
btn.style.display = "inline-block";
} else {
btn.style.display = "none";
if (!Array.isArray(data.data)) {
clearContainers();
Object.values(containers).forEach(container => {
container.innerHTML = `<div class="empty-message">Invalid response format.</div>`;
});
return;
}
allTemplates.push(...data.data);
renderTemplates();
})
.catch(error => {
console.error("Fetch error:", error);
clearContainers();
Object.values(containers).forEach(container => {
container.innerHTML = `<div class="empty-message">Fetch error while loading templates.</div>`;
});
});
document.getElementById("searchInput").addEventListener("input", function() {
const searchValue = this.value.toLowerCase().trim();
renderTemplates(searchValue);
});
});
</script>
-4
View File
@@ -1,10 +1,6 @@
<?php
include('include/headscript.php');
// ✅ FIX timezone (Rome)
ini_set('date.timezone', 'Europe/Rome');
date_default_timezone_set('Europe/Rome');
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['template_id']) || !isset($_POST['selected_rows']) || !isset($_POST['filename'])) {
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Richiesta non valida"));
exit;
+82 -2
View File
@@ -40,6 +40,29 @@ error_log("Excelrows: " . json_encode($excelrows));
$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();
@@ -105,7 +128,12 @@ foreach ($selected_rows as $rowIndex) {
$fieldValue = null;
if (!$mapping['is_manual']) {
$excelColumn = trim($mapping['excel_column']);
$excelColumnIndex = array_search($excelColumn, array_map('trim', $columns));
// 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));
@@ -154,8 +182,60 @@ foreach ($selected_rows as $rowIndex) {
}
}
// ------------------------------------------------------------
// 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();
// 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]);
}
}
$pdo->commit();
} else {
error_log("[AUTO FIELD 244] Nessun mapping field_id=244 per template $template_id");
}
} 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));
exit;
?>
+111 -8
View File
@@ -40,6 +40,22 @@ error_log("Loaded template: " . print_r($template, true));
<?php include('cssinclude.php'); ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.top-scrollbar {
overflow-x: auto;
overflow-y: hidden;
width: 100%;
height: 18px;
margin-bottom: 8px;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
background: #f8f9fa;
display: none;
}
.top-scrollbar-inner {
height: 1px;
}
.table-container {
overflow-x: auto;
max-width: 100%;
@@ -151,7 +167,12 @@ error_log("Loaded template: " . print_r($template, true));
<div class="d-flex align-items-center">
<div>
<h6 class="mb-0"><?= htmlspecialchars($template['name']) ?></h6>
<small>Template ID: <?= $id ?>, Start Row: <?= $template['header_row'] ?>, Start Column: <?= $template['start_column'] ?></small>
<small>
Template ID: <?= $id ?>,
Sheet Number: <?= (int)($template['xls_sheet_index'] ?? 0) ?>,
Start Row: <?= $template['header_row'] ?>,
Start Column: <?= $template['start_column'] ?>
</small>
</div>
</div>
</div>
@@ -228,8 +249,9 @@ error_log("Loaded template: " . print_r($template, true));
const templateId = <?= $id ?>;
console.log('Template ID passed to formData:', templateId);
formData.append('template_id', templateId);
formData.append('header_row', <?= $template['header_row'] ?>);
formData.append('start_column', <?= $template['start_column'] ?>);
formData.append('header_row', <?= (int)$template['header_row'] ?>);
formData.append('start_column', <?= json_encode($template['start_column']) ?>);
formData.append('xls_sheet_index', <?= (int)($template['xls_sheet_index'] ?? 0) ?>);
fetch('process_import_xls2.php', {
method: 'POST',
@@ -315,8 +337,8 @@ error_log("Loaded template: " . print_r($template, true));
<form id="selectRowsForm" action="import_insert.php" method="POST">
<input type="hidden" name="template_id" value="${data.template_id}">
<input type="hidden" name="columns" value="${encodeURIComponent(JSON.stringify(data.columns))}">
<input type="hidden" name="rows" value="${encodeURIComponent(JSON.stringify(data.rows))}">
<input type="hidden" name="excelrows" value="${encodeURIComponent(JSON.stringify(data.excel_data.map(r => r.excelrow)))}">
<input type="hidden" name="rows" id="selectedRowsData" value="">
<input type="hidden" name="excelrows" id="selectedExcelRowsData" value="">
<input type="hidden" name="filename" value="${data.filename}">
<!-- TOP BUTTON -->
@@ -324,8 +346,12 @@ error_log("Loaded template: " . print_r($template, true));
<button type="submit" class="btn btn-primary" id="proceedButtonTop" disabled>Prosegui</button>
</div>
<div class="table-container">
<table class="table table-striped table-bordered">
<div class="top-scrollbar" id="topTableScrollbar">
<div class="top-scrollbar-inner" id="topTableScrollbarInner"></div>
</div>
<div class="table-container" id="mainTableContainer">
<table class="table table-striped table-bordered" id="importPreviewTable">
<thead>
<tr>
<th><input type="checkbox" id="selectAll"> Seleziona</th>
@@ -363,6 +389,83 @@ error_log("Loaded template: " . print_r($template, true));
`;
tableContainer.innerHTML = html;
const selectRowsForm = document.getElementById('selectRowsForm');
selectRowsForm.addEventListener('submit', function(e) {
const checkedBoxes = Array.from(document.querySelectorAll('.row-checkbox:checked'));
if (checkedBoxes.length === 0) {
e.preventDefault();
alert('Seleziona almeno una riga.');
return;
}
const selectedRows = [];
const selectedExcelRows = [];
checkedBoxes.forEach((cb, newIndex) => {
const originalIndex = parseInt(cb.value, 10);
if (data.rows && data.rows[originalIndex]) {
selectedRows.push(data.rows[originalIndex]);
}
if (data.excel_data && data.excel_data[originalIndex]) {
selectedExcelRows.push(data.excel_data[originalIndex].excelrow);
}
// Reindex selected_rows so import_insert.php receives only the reduced rows array
cb.value = newIndex;
});
document.getElementById('selectedRowsData').value =
encodeURIComponent(JSON.stringify(selectedRows));
document.getElementById('selectedExcelRowsData').value =
encodeURIComponent(JSON.stringify(selectedExcelRows));
});
const topTableScrollbar = document.getElementById('topTableScrollbar');
const topTableScrollbarInner = document.getElementById('topTableScrollbarInner');
const mainTableContainer = document.getElementById('mainTableContainer');
const importPreviewTable = document.getElementById('importPreviewTable');
function updateTopTableScrollbar() {
if (!topTableScrollbar || !topTableScrollbarInner || !mainTableContainer || !importPreviewTable) return;
topTableScrollbarInner.style.width = importPreviewTable.scrollWidth + 'px';
if (mainTableContainer.scrollWidth > mainTableContainer.clientWidth) {
topTableScrollbar.style.display = 'block';
} else {
topTableScrollbar.style.display = 'none';
}
}
let syncingTop = false;
let syncingBottom = false;
if (topTableScrollbar && mainTableContainer) {
topTableScrollbar.addEventListener('scroll', function() {
if (syncingBottom) return;
syncingTop = true;
mainTableContainer.scrollLeft = topTableScrollbar.scrollLeft;
syncingTop = false;
});
mainTableContainer.addEventListener('scroll', function() {
if (syncingTop) return;
syncingBottom = true;
topTableScrollbar.scrollLeft = mainTableContainer.scrollLeft;
syncingBottom = false;
});
}
updateTopTableScrollbar();
setTimeout(updateTopTableScrollbar, 100);
setTimeout(updateTopTableScrollbar, 300);
window.addEventListener('resize', updateTopTableScrollbar);
const proceedButtonTop = document.getElementById('proceedButtonTop');
const proceedButtonBottom = document.getElementById('proceedButtonBottom');
const selectAllCheckbox = document.getElementById('selectAll');
@@ -493,4 +596,4 @@ error_log("Loaded template: " . print_r($template, true));
</script>
</body>
</html>
</html>
+449 -149
View File
@@ -1,10 +1,6 @@
<?php
include('include/headscript.php');
// ✅ FIX timezone (Rome)
ini_set('date.timezone', 'Europe/Rome');
date_default_timezone_set('Europe/Rome');
$template_id = intval($_GET['id'] ?? 0);
if (!$template_id) {
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Template ID mancante"));
@@ -24,9 +20,10 @@ $db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Recupera tutti i mapping dal template, includendo is_visible_import
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field, is_visible_import, auto_value
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, field_order, main_field, is_visible_import, auto_value
FROM template_mapping
WHERE template_id = ?");
WHERE template_id = ?
ORDER BY field_order ASC, id ASC");
$stmt->execute([$template_id]);
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -59,15 +56,22 @@ if (empty($allMappings)) {
exit;
}
// Trova il campo main_field
$mainFieldMapping = null;
// Find up to 2 main fields
$mainFieldMappings = [];
foreach ($allMappings as $mapping) {
if ($mapping['main_field'] == 1 && $mapping['is_visible_import'] == 1) {
$mainFieldMapping = $mapping;
if ((string)$mapping['main_field'] === '1' && (int)$mapping['is_visible_import'] === 1) {
$mainFieldMappings[] = $mapping;
}
if (count($mainFieldMappings) >= 2) {
break;
}
}
// Backward compatibility: first main field
$mainFieldMapping = $mainFieldMappings[0] ?? null;
// Recupera l'idclient di default dal template (se presente)
$template_stmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
$template_stmt->execute([$template_id]);
@@ -95,7 +99,7 @@ $stmt = $pdo->prepare("
FROM datadb d
LEFT JOIN auth_users u ON d.user_id = u.id
{$baseWhere}
ORDER BY d.iddatadb DESC
ORDER BY d.excelrow ASC, d.iddatadb ASC
{$limitClause}
");
$stmt->execute($baseParams);
@@ -143,6 +147,7 @@ $desiredOrder = [
'ClienteResponsabile',
'ClienteFornitore',
'ClienteAnalisi',
'ClienteFatturazione',
'AnagraficaCertestObject',
'AnagraficaCertestService',
'MoltiplicatorePrezzo',
@@ -177,6 +182,7 @@ $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',
@@ -202,13 +208,13 @@ foreach ($importedData as $index => $row) {
'status' => $row['status'] ?? 'i',
'idclient' => $row['idclient'] ?? $default_idclient,
'cliente_fornitore_id' => $row['cliente_fornitore_id'] ?? null,
'tested_component' => $row['tested_component'] ?? '',
'commessaweb' => $row['commessaweb'] ?? null,
'user_name' => $row['user_name'] ?? '',
'importreferencecode' => $row['importreferencecode'] ?? '',
'filename_import' => $row['filename_import'] ?? '',
'importdate' => $row['importdate'] ?? '',
];
// Fixed fields
$rowObj['fixedFields'] = [];
foreach ($fixedFields as $f) {
@@ -228,11 +234,18 @@ foreach ($importedData as $index => $row) {
$rowObj['details'][(string)$d['mapping_id']] = $d['field_value'] ?? '';
}
// Main field value
// Main field values
foreach ($mainFieldMappings as $mainMapping) {
$mainDetail = array_filter($rowDetails, fn($d) => $d['mapping_id'] == $mainMapping['id']);
$mainDetail = reset($mainDetail) ?: ['field_value' => $mainMapping['manual_default'] ?? ''];
$rowObj['details'][(string)$mainMapping['id']] =
$mainDetail['field_value'] ?? $mainMapping['manual_default'] ?? '';
}
// Backward compatibility: first main value
if ($mainFieldMapping) {
$mainDetail = array_filter($rowDetails, fn($d) => $d['mapping_id'] == $mainFieldMapping['id']);
$mainDetail = reset($mainDetail) ?: ['field_value' => $mainFieldMapping['manual_default'] ?? ''];
$rowObj['mainFieldValue'] = $mainDetail['field_value'] ?? $mainFieldMapping['manual_default'] ?? '';
$rowObj['mainFieldValue'] = $rowObj['details'][(string)$mainFieldMapping['id']] ?? '';
}
$rowObj['_dirty'] = false;
@@ -242,18 +255,27 @@ foreach ($importedData as $index => $row) {
// Build columns in display order
$gridColumns = [];
// 1. Main field
if ($mainFieldMapping) {
$gridColumns[] = [
'type' => 'main_field',
'key' => (string)$mainFieldMapping['id'],
'label' => $mainFieldMapping['field_label'],
'dataType' => $mainFieldMapping['data_type'],
'isManual' => (bool)$mainFieldMapping['is_manual'],
'isRequired' => (bool)$mainFieldMapping['is_required'],
'fieldId' => $mainFieldMapping['field_id'] ?? null,
'width' => 150,
];
// 1. Main fields first, immediately after buttons
foreach ($allMappings as $mapping) {
if (
(int)$mapping['is_visible_import'] === 1
&& (string)$mapping['main_field'] === '1'
&& trim((string)$mapping['field_label']) !== 'Tested Component:'
) {
$gridColumns[] = [
'type' => 'main_field',
'key' => (string)$mapping['id'],
'label' => $mapping['field_label'],
'dataType' => $mapping['data_type'],
'isManual' => (bool)$mapping['is_manual'],
'isRequired' => (bool)$mapping['is_required'],
'fieldId' => $mapping['field_id'] ?? null,
'fieldOrder' => (int)($mapping['field_order'] ?? 9999),
'manualDefault' => $mapping['manual_default'] ?? '',
'autoValue' => $mapping['auto_value'] ?? 'none',
'width' => 150,
];
}
}
// 2. Status
@@ -265,40 +287,45 @@ $gridColumns[] = ['type' => 'idclient', 'key' => 'idclient', 'label' => 'Client'
// 4. Cliente Fornitore
$gridColumns[] = ['type' => 'cliente_fornitore_id', 'key' => 'cliente_fornitore_id', 'label' => $slugMapping['ClienteFornitore'] ?? 'ClienteFornitore', 'width' => 300];
// 5. Auto fields
foreach ($allMappings as $mapping) {
if (!$mapping['is_manual'] && $mapping['main_field'] != 1 && $mapping['is_visible_import'] == 1) {
// 4b. Fatturazione Avanzata — subito dopo Cliente Fornitore
foreach ($fixedFields as $f) {
if ($f['fixed_field_key'] === 'ClienteFatturazione') {
$gridColumns[] = [
'type' => 'detail',
'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 (
(int)$mapping['is_visible_import'] === 1
&& (string)$mapping['main_field'] !== '1'
&& trim((string)$mapping['field_label']) !== 'Tested Component:'
) {
$isMainField = ((string)$mapping['main_field'] === '1');
$gridColumns[] = [
'type' => $isMainField ? 'main_field' : 'detail',
'key' => (string)$mapping['id'],
'label' => $mapping['field_label'],
'dataType' => $mapping['data_type'],
'isManual' => false,
'isManual' => (bool)$mapping['is_manual'],
'isRequired' => (bool)$mapping['is_required'],
'fieldId' => $mapping['field_id'] ?? null,
'fieldOrder' => (int)($mapping['field_order'] ?? 9999),
'manualDefault' => $mapping['manual_default'] ?? '',
'autoValue' => $mapping['auto_value'] ?? 'none',
'width' => 150,
];
}
}
// 6. Manual fields
foreach ($allMappings as $mapping) {
if ($mapping['is_manual'] && $mapping['main_field'] != 1 && $mapping['is_visible_import'] == 1) {
$gridColumns[] = [
'type' => 'detail',
'key' => (string)$mapping['id'],
'label' => $mapping['field_label'],
'dataType' => $mapping['data_type'],
'isManual' => true,
'isRequired' => (bool)$mapping['is_required'],
'fieldId' => $mapping['field_id'] ?? null,
'manualDefault' => $mapping['manual_default'] ?? '',
'width' => 150,
];
}
}
// 7. Tested Component
$gridColumns[] = ['type' => 'tested_component', 'key' => 'tested_component', 'label' => 'Tested Component', 'width' => 150];
@@ -311,7 +338,18 @@ $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'];
$label = $slugMapping[$key] ?? $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;
}
$gridColumns[] = [
'type' => 'fixed',
'key' => $key,
@@ -320,6 +358,7 @@ 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];
@@ -336,14 +375,18 @@ $gridMeta = [
'slugMapping' => $slugMapping,
'timeLabels' => $timeLabels,
'columns' => $gridColumns,
'mainFieldMapping' => $mainFieldMapping,
'mainFieldMapping' => $mainFieldMapping,
'mainFieldMappings' => $mainFieldMappings,
'totalRows' => count($gridDataArray),
];
?>
<script>
window.gridData = <?= json_encode($gridDataArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
window.gridData = <?= json_encode($gridDataArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
// Visible records in the current imported.php page
window.visibleIddatadbList = window.gridData.map(row => parseInt(row.iddatadb, 10)).filter(Boolean);
</script>
<!doctype html>
@@ -364,20 +407,10 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
transition: background-color 0.3s ease;
}
input.auto-input,
select.auto-input {
background-color: #d4edda;
}
input.manual-input,
select.manual-input {
background-color: #fff3cd;
}
input.required-input,
select.required-input {
background-color: #f8d7da;
}
input.required-input,
select.required-input {
@@ -416,17 +449,9 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
outline: none !important;
}
textarea.auto-input {
background-color: #d4edda;
}
textarea.manual-input {
background-color: #fff3cd;
}
textarea.required-input {
background-color: #f8d7da;
}
.status-badge {
display: inline-block;
@@ -463,6 +488,29 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
position: relative;
}
.top-scrollbar {
overflow-x: auto;
overflow-y: hidden;
width: 100%;
height: 18px;
margin-bottom: 8px;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
background: #f8f9fa;
position: sticky;
top: 70px;
/* regola sull'altezza reale della tua topbar fissa */
position: sticky;
top: 70px;
z-index: 30;
}
.top-scrollbar-inner {
height: 1px;
}
.grid-row {
display: flex;
align-items: center;
@@ -663,7 +711,33 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
flex-shrink: 0;
}
.grid-row .grid-header:nth-child(2) {
<?php if (isset($mainFieldMappings) && count($mainFieldMappings) >= 2): ?>
/* Sticky second Main column - only when the template has 2 Main fields */
.grid-top .grid-cell:nth-child(3),
#gridHeaderContainer .grid-header:nth-child(3),
.grid-row .grid-cell:nth-child(3) {
position: sticky !important;
left: 360px;
z-index: 7;
background: white;
overflow: visible;
flex-shrink: 0;
}
#gridHeaderContainer .grid-header:nth-child(3) {
background-color: #e9ecef;
}
.grid-row:nth-child(even) .grid-cell:nth-child(3) {
background-color: #f8f9fa;
}
.grid-row:hover .grid-cell:nth-child(3) {
background-color: #e9ecef;
}
<?php endif; ?>.grid-row .grid-header:nth-child(2) {
background-color: #e9ecef;
}
@@ -797,7 +871,7 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
background-color: rgba(0, 0, 0, 0.5);
}
#photosModal > .modal-content {
#photosModal>.modal-content {
background-color: #fff;
margin: 5% auto;
padding: 20px;
@@ -892,9 +966,17 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
}
@keyframes new-row-pulse {
0%, 100% { background-color: transparent; }
50% { background-color: #cce5ff; }
0%,
100% {
background-color: transparent;
}
50% {
background-color: #cce5ff;
}
}
.row-just-created {
animation: new-row-pulse 1s ease-in-out 3;
}
@@ -1011,11 +1093,7 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
font-style: italic;
}
.api-fixed-select.required-input:invalid,
.api-fixed-select[required]:not([value]):not([data-select2-id]) {
background-color: #f8d7da !important;
border-color: #dc3545 !important;
}
/* ── Pagination bar ── */
.pager-bar {
@@ -1029,21 +1107,25 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
font-size: 13px;
color: #495057;
}
.pager-rows-per-page {
display: flex;
align-items: center;
gap: 8px;
}
.pager-label {
font-weight: 500;
white-space: nowrap;
}
.pager-limit-group {
display: inline-flex;
border: 1px solid #ced4da;
border-radius: 4px;
overflow: hidden;
}
.pager-limit-btn {
padding: 3px 10px;
text-decoration: none;
@@ -1052,24 +1134,41 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
transition: background .15s, color .15s;
font-weight: 500;
}
.pager-limit-btn:last-child { border-right: none; }
.pager-limit-btn:hover { background: #e9ecef; text-decoration: none; color: #212529; }
.pager-limit-btn:last-child {
border-right: none;
}
.pager-limit-btn:hover {
background: #e9ecef;
text-decoration: none;
color: #212529;
}
.pager-limit-btn.active {
background: #0d6efd;
color: #fff;
}
.pager-limit-btn.active:hover { background: #0b5ed7; color: #fff; }
.pager-limit-btn.active:hover {
background: #0b5ed7;
color: #fff;
}
.pager-nav {
display: flex;
align-items: center;
gap: 4px;
}
.pager-info {
margin-right: 8px;
font-weight: 500;
white-space: nowrap;
}
.pager-btn, .pager-num {
.pager-btn,
.pager-num {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -1083,21 +1182,152 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
font-weight: 500;
transition: background .15s, color .15s, border-color .15s;
}
.pager-btn:hover, .pager-num:hover { background: #e9ecef; text-decoration: none; color: #212529; }
.pager-btn:hover,
.pager-num:hover {
background: #e9ecef;
text-decoration: none;
color: #212529;
}
.pager-num.active {
background: #0d6efd;
color: #fff;
border-color: #0d6efd;
}
.pager-btn.disabled {
pointer-events: none;
opacity: .4;
}
.pager-dots {
padding: 0 2px;
color: #adb5bd;
user-select: none;
}
/* =========================================================
FINAL GRID COLORS
Schema/customfield fields = green
Fixed/standard fields = white
Required fields = red border only
========================================================= */
/* Default: all fields white */
.grid-container input,
.grid-container select,
.grid-container textarea,
.grid-top input,
.grid-top select,
.grid-top textarea {
background-color: #ffffff !important;
color: #333 !important;
}
/* Schema/customfield fields: green background */
.grid-container .schema-field input,
.grid-container .schema-field select,
.grid-container .schema-field textarea,
.grid-top .schema-field input,
.grid-top .schema-field select,
.grid-top .schema-field textarea {
background-color: #dff3e3 !important;
}
/* Fixed and standard fields: white background */
.grid-container .fixed-field input,
.grid-container .fixed-field select,
.grid-container .fixed-field textarea,
.grid-container .standard-field input,
.grid-container .standard-field select,
.grid-container .standard-field textarea,
.grid-top .fixed-field input,
.grid-top .fixed-field select,
.grid-top .fixed-field textarea,
.grid-top .standard-field input,
.grid-top .standard-field select,
.grid-top .standard-field textarea {
background-color: #ffffff !important;
}
/* Required fields: red border only */
.grid-container .required-field input,
.grid-container .required-field select,
.grid-container .required-field textarea,
.grid-top .required-field input,
.grid-top .required-field select,
.grid-top .required-field textarea {
border: 2px solid #dc3545 !important;
box-shadow: 0 0 0 1px rgba(220, 53, 69, 0.15) !important;
}
/* Required schema/customfield fields: green + red border */
.grid-container .schema-field.required-field input,
.grid-container .schema-field.required-field select,
.grid-container .schema-field.required-field textarea,
.grid-top .schema-field.required-field input,
.grid-top .schema-field.required-field select,
.grid-top .schema-field.required-field textarea {
background-color: #dff3e3 !important;
border: 2px solid #dc3545 !important;
}
/* Required fixed/standard fields: white + red border */
.grid-container .fixed-field.required-field input,
.grid-container .fixed-field.required-field select,
.grid-container .fixed-field.required-field textarea,
.grid-container .standard-field.required-field input,
.grid-container .standard-field.required-field select,
.grid-container .standard-field.required-field textarea,
.grid-top .fixed-field.required-field input,
.grid-top .fixed-field.required-field select,
.grid-top .fixed-field.required-field textarea,
.grid-top .standard-field.required-field input,
.grid-top .standard-field.required-field select,
.grid-top .standard-field.required-field textarea {
background-color: #ffffff !important;
border: 2px solid #dc3545 !important;
}
/* Select2 - schema/customfield fields: green */
.grid-container .schema-field .select2-container--default .select2-selection--single,
.grid-top .schema-field .select2-container--default .select2-selection--single {
background-color: #dff3e3 !important;
}
/* Select2 - fixed/standard fields: white */
.grid-container .fixed-field .select2-container--default .select2-selection--single,
.grid-container .standard-field .select2-container--default .select2-selection--single,
.grid-top .fixed-field .select2-container--default .select2-selection--single,
.grid-top .standard-field .select2-container--default .select2-selection--single {
background-color: #ffffff !important;
}
/* Select2 - required fields: red border only */
.grid-container .required-field .select2-container--default .select2-selection--single,
.grid-top .required-field .select2-container--default .select2-selection--single {
border: 2px solid #dc3545 !important;
box-shadow: 0 0 0 1px rgba(220, 53, 69, 0.15) !important;
}
/* Remove old red background from required classes */
.grid-container input.required-input,
.grid-container select.required-input,
.grid-container textarea.required-input,
.grid-top input.required-input,
.grid-top select.required-input,
.grid-top textarea.required-input {
background-color: inherit !important;
}
/* Missing required cell: red outline only */
.grid-cell.missing-required {
background-color: inherit !important;
border-right: 1px solid #dee2e6 !important;
outline: 2px solid #dc3545 !important;
outline-offset: -2px;
}
</style>
<title>Edit Imported Data - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
</head>
@@ -1112,20 +1342,20 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
<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>
<?php if ($importref === ''): ?>
<span class="ms-3">
<label class="form-check-label" style="font-size: 13px; cursor: pointer;">
<input type="checkbox" class="form-check-input" id="showAllUsers" <?= $show_all_users ? 'checked' : '' ?>
onchange="window.location.href='imported.php?id=<?= $template_id ?>' + (this.checked ? '&all_users=1' : '')">
Show all users
</label>
</span>
<span class="text-muted" style="font-size: 12px;">
(<?= $usePagination ? count($importedData) . " of {$totalRows}" : count($importedData) ?> records<?= !$show_all_users ? ' — my records only' : '' ?>)
</span>
<span class="ms-3">
<label class="form-check-label" style="font-size: 13px; cursor: pointer;">
<input type="checkbox" class="form-check-input" id="showAllUsers" <?= $show_all_users ? 'checked' : '' ?>
onchange="window.location.href='imported.php?id=<?= $template_id ?>' + (this.checked ? '&all_users=1' : '')">
Show all users
</label>
</span>
<span class="text-muted" style="font-size: 12px;">
(<?= $usePagination ? count($importedData) . " of {$totalRows}" : count($importedData) ?> records<?= !$show_all_users ? ' — my records only' : '' ?>)
</span>
<?php endif; ?>
</div>
<?php if ($usePagination): ?>
<?php
<?php
$baseQuery = $_GET;
unset($baseQuery['limit'], $baseQuery['page']);
$pageQuery = $_GET;
@@ -1133,58 +1363,49 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
$pageBase = 'imported.php?' . http_build_query($pageQuery);
$fromRow = ($page - 1) * $perPage + 1;
$toRow = min($page * $perPage, $totalRows);
?>
<div class="pager-bar mb-2">
<div class="pager-rows-per-page">
<span class="pager-label">Rows per page</span>
<div class="pager-limit-group">
<?php foreach ($allowedLimits as $lim):
$isActive = ($perPage === $lim);
$url = 'imported.php?' . http_build_query(array_merge($baseQuery, ['limit' => $lim]));
?>
<a href="<?= htmlspecialchars($url) ?>" class="pager-limit-btn <?= $isActive ? 'active' : '' ?>"><?= $lim ?></a>
<?php endforeach; ?>
?>
<div class="pager-bar mb-2">
<div class="pager-rows-per-page">
<span class="pager-label">Rows per page</span>
<div class="pager-limit-group">
<?php foreach ($allowedLimits as $lim):
$isActive = ($perPage === $lim);
$url = 'imported.php?' . http_build_query(array_merge($baseQuery, ['limit' => $lim]));
?>
<a href="<?= htmlspecialchars($url) ?>" class="pager-limit-btn <?= $isActive ? 'active' : '' ?>"><?= $lim ?></a>
<?php endforeach; ?>
</div>
</div>
</div>
<?php if ($totalPages > 1): ?>
<div class="pager-nav">
<span class="pager-info"><?= $fromRow ?><?= $toRow ?> of <?= $totalRows ?></span>
<a href="<?= htmlspecialchars($pageBase . '&page=1') ?>" class="pager-btn <?= $page <= 1 ? 'disabled' : '' ?>" title="First"><i class="fas fa-angle-double-left"></i></a>
<a href="<?= htmlspecialchars($pageBase . '&page=' . ($page - 1)) ?>" class="pager-btn <?= $page <= 1 ? 'disabled' : '' ?>" title="Previous"><i class="fas fa-angle-left"></i></a>
<?php
$startPage = max(1, $page - 2);
$endPage = min($totalPages, $page + 2);
if ($startPage > 1): ?>
<a href="<?= htmlspecialchars($pageBase . '&page=1') ?>" class="pager-num">1</a>
<?php if ($startPage > 2): ?><span class="pager-dots">...</span><?php endif; ?>
<?php endif;
for ($p = $startPage; $p <= $endPage; $p++): ?>
<a href="<?= htmlspecialchars($pageBase . '&page=' . $p) ?>" class="pager-num <?= $p === $page ? 'active' : '' ?>"><?= $p ?></a>
<?php endfor;
if ($endPage < $totalPages): ?>
<?php if ($endPage < $totalPages - 1): ?><span class="pager-dots">...</span><?php endif; ?>
<a href="<?= htmlspecialchars($pageBase . '&page=' . $totalPages) ?>" class="pager-num"><?= $totalPages ?></a>
<?php if ($totalPages > 1): ?>
<div class="pager-nav">
<span class="pager-info"><?= $fromRow ?><?= $toRow ?> of <?= $totalRows ?></span>
<a href="<?= htmlspecialchars($pageBase . '&page=1') ?>" class="pager-btn <?= $page <= 1 ? 'disabled' : '' ?>" title="First"><i class="fas fa-angle-double-left"></i></a>
<a href="<?= htmlspecialchars($pageBase . '&page=' . ($page - 1)) ?>" class="pager-btn <?= $page <= 1 ? 'disabled' : '' ?>" title="Previous"><i class="fas fa-angle-left"></i></a>
<?php
$startPage = max(1, $page - 2);
$endPage = min($totalPages, $page + 2);
if ($startPage > 1): ?>
<a href="<?= htmlspecialchars($pageBase . '&page=1') ?>" class="pager-num">1</a>
<?php if ($startPage > 2): ?><span class="pager-dots">...</span><?php endif; ?>
<?php endif;
for ($p = $startPage; $p <= $endPage; $p++): ?>
<a href="<?= htmlspecialchars($pageBase . '&page=' . $p) ?>" class="pager-num <?= $p === $page ? 'active' : '' ?>"><?= $p ?></a>
<?php endfor;
if ($endPage < $totalPages): ?>
<?php if ($endPage < $totalPages - 1): ?><span class="pager-dots">...</span><?php endif; ?>
<a href="<?= htmlspecialchars($pageBase . '&page=' . $totalPages) ?>" class="pager-num"><?= $totalPages ?></a>
<?php endif; ?>
<a href="<?= htmlspecialchars($pageBase . '&page=' . ($page + 1)) ?>" class="pager-btn <?= $page >= $totalPages ? 'disabled' : '' ?>" title="Next"><i class="fas fa-angle-right"></i></a>
<a href="<?= htmlspecialchars($pageBase . '&page=' . $totalPages) ?>" class="pager-btn <?= $page >= $totalPages ? 'disabled' : '' ?>" title="Last"><i class="fas fa-angle-double-right"></i></a>
</div>
<?php endif; ?>
<a href="<?= htmlspecialchars($pageBase . '&page=' . ($page + 1)) ?>" class="pager-btn <?= $page >= $totalPages ? 'disabled' : '' ?>" title="Next"><i class="fas fa-angle-right"></i></a>
<a href="<?= htmlspecialchars($pageBase . '&page=' . $totalPages) ?>" class="pager-btn <?= $page >= $totalPages ? 'disabled' : '' ?>" title="Last"><i class="fas fa-angle-double-right"></i></a>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="card radius-10">
<div class="card-header">
<div class="d-flex align-items-center" style="min-height: 42px; gap: 12px;">
<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">
<?php if ((Auth::user()->hasRole('Admin'))) : ?>
<li><a class="dropdown-item export-all-lims-btn" href="#"><i class="fas fa-upload" style="color: #eb0b0b;"></i>Export All</a></li>
<?php endif; ?>
<li><a class="dropdown-item save-all-btn" href="#"><i class="fas fa-save" style="color: #28a745;"></i>Save All</a></li>
</ul>
</div>
<a href="#" class="btn btn-outline-danger btn-sm export-all-lims-btn" style="flex-shrink:0;"><i class="fas fa-upload"></i> Export All</a>
<a href="#" class="btn btn-outline-success btn-sm save-all-btn" style="flex-shrink:0;"><i class="fas fa-save"></i> Save All</a>
<button type="button" class="btn btn-outline-success btn-sm" id="addRowBtn" style="flex-shrink:0;"><i class="fas fa-plus"></i> Add Row</button>
<div id="unsavedChanges" style="display:none; color: red; font-weight: bold; font-size: 13px;">
⚠️ Unsaved changes detected!
@@ -1201,13 +1422,18 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
</div>
<div class="card-body">
<form id="editForm">
<div class="grid-container">
<div class="top-scrollbar" id="topScrollbar">
<div class="top-scrollbar-inner" id="topScrollbarInner"></div>
</div>
<div class="grid-container" id="gridContainer">
<div class="grid-top" id="gridTopContainer"></div>
<div class="grid-row" id="gridHeaderContainer" style="font-weight:600;"></div>
<div id="gridRowContainer"></div>
</div>
</form>
<div id="partsModalContainer"></div>
<div id="analysisModalContainer"></div>
<div id="annotationsModalContainer"></div>
<?php include 'photos_functions.php'; ?>
</div>
@@ -1218,7 +1444,12 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
</div>
<style>.btn i { margin-top: 0 !important; margin-bottom: 0 !important; }</style>
<style>
.btn i {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
</style>
<?php include('jsinclude.php'); ?>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js"></script>
@@ -1231,6 +1462,75 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
<script src="photos.js"></script>
<script src="annotationsModal.js"></script>
<script src="partsTable.js"></script>
<script src="analysisModal.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const topScrollbar = document.getElementById('topScrollbar');
const topScrollbarInner = document.getElementById('topScrollbarInner');
const gridContainer = document.getElementById('gridContainer');
const gridRowContainer = document.getElementById('gridRowContainer');
const gridHeaderContainer = document.getElementById('gridHeaderContainer');
const gridTopContainer = document.getElementById('gridTopContainer');
if (!topScrollbar || !topScrollbarInner || !gridContainer) return;
let syncingFromTop = false;
let syncingFromGrid = false;
function updateTopScrollbarWidth() {
const realWidth = Math.max(
gridContainer.scrollWidth,
gridHeaderContainer ? gridHeaderContainer.scrollWidth : 0,
gridTopContainer ? gridTopContainer.scrollWidth : 0,
gridRowContainer ? gridRowContainer.scrollWidth : 0
);
topScrollbarInner.style.width = realWidth + 'px';
if (realWidth > gridContainer.clientWidth) {
topScrollbar.style.display = 'block';
} else {
topScrollbar.style.display = 'none';
}
topScrollbar.scrollLeft = gridContainer.scrollLeft;
}
topScrollbar.addEventListener('scroll', function() {
if (syncingFromGrid) return;
syncingFromTop = true;
gridContainer.scrollLeft = topScrollbar.scrollLeft;
syncingFromTop = false;
});
gridContainer.addEventListener('scroll', function() {
if (syncingFromTop) return;
syncingFromGrid = true;
topScrollbar.scrollLeft = gridContainer.scrollLeft;
syncingFromGrid = false;
});
window.addEventListener('resize', updateTopScrollbarWidth);
// Recalculate after JS grid rendering
setTimeout(updateTopScrollbarWidth, 100);
setTimeout(updateTopScrollbarWidth, 300);
setTimeout(updateTopScrollbarWidth, 700);
setTimeout(updateTopScrollbarWidth, 1500);
// Recalculate automatically when rows/header/top controls are rendered or changed
const observer = new MutationObserver(updateTopScrollbarWidth);
if (gridContainer) {
observer.observe(gridContainer, {
childList: true,
subtree: true,
attributes: true
});
}
});
</script>
<div class="modal fade" id="exportConfirmModal" tabindex="-1" aria-labelledby="exportConfirmModalLabel" aria-hidden="true">
<div class="modal-dialog">
@@ -1394,4 +1694,4 @@ window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESC
</body>
</html>
</html>
+4 -1
View File
@@ -5,11 +5,14 @@ $db = DBHandlerSelect::getInstance()->getConnection();
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL | E_STRICT);
error_reporting(E_ALL);
// This should be equal to: PATH_TO_VANGUARD_FOLDER/extra/auth.php
include('../../extra/auth.php');
//require_once __DIR__ . '/extra/auth.php';
// Laravel bootstrap (loaded by auth.php) forces UTC via config/app.php — re-apply our TZ
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
// Here we just check if user is not
// logged in, and in that case we redirect
// the user to vanguard login page.
+99 -46
View File
@@ -6,102 +6,155 @@
<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">
<!-- user, admin, superuser menù -->
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('User')) || (Auth::user()->hasRole('Superuser'))) : ?>
<?php
$currentUser = Auth::user();
$isAdmin = $currentUser->hasRole('Admin');
$isSuperUser = $currentUser->hasRole('SuperUser');
$isUser = $currentUser->hasRole('User');
$canAccessMainMenu = $isAdmin || $isSuperUser || $isUser;
$canAccessUserAdmin = $isAdmin || $isSuperUser;
$canAccessTemplates = $isAdmin || $isSuperUser;
?>
<!-- user, admin, superuser menu -->
<?php if ($canAccessMainMenu) : ?>
<li>
<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="index.php"><i class='bx bx-radio-circle'></i>Default</a>
</li> -->
<li> <a href="import_dashboard.php"><i class='bx bx-radio-circle'></i>XLS Import</a>
<li>
<a href="import_dashboard.php">
<i class='bx bx-radio-circle'></i>XLS Import
</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">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 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">Other Functions</div>
</a>
<ul>
<li> <a href="quotations.php"><i class='bx bx-radio-circle'></i><?php echo $quotationstitle; ?></a>
<li>
<a href="quotations.php">
<i class='bx bx-radio-circle'></i><?= htmlspecialchars($quotationstitle, ENT_QUOTES, 'UTF-8'); ?>
</a>
</li>
</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-user-plus"></i>
</div>
<div class="menu-title">Stats</div>
</a>
</li>
<?php endif; ?>
<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, superuser menù -->
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('Superuser'))) : ?>
<?php
endif; ?>
<!-- admin menù -->
<?php if (Auth::user()->hasRole('Admin')) : ?>
<?php endif; ?>
<!-- admin only menu -->
<?php if ($isAdmin) : ?>
<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
@@ -0,0 +1,55 @@
<?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; ?>
+24 -4
View File
@@ -1,8 +1,22 @@
<header>
<div class="topbar d-flex align-items-center">
<nav class="navbar navbar-expand gap-3">
<div class="mobile-toggle-menu"><i class='bx bx-menu'></i>
<?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
</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> -->
@@ -94,8 +108,14 @@
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item d-flex align-items-center" href="user-profile.php"><i class="bx bx-user fs-5"></i><span>Profile</span></a>
</li>
<li><a class="dropdown-item d-flex align-items-center" href="settings.php"><i class="bx bx-cog fs-5"></i><span>Settings</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>
<div class="dropdown-divider mb-0"></div>
</li>
+142 -14
View File
@@ -3,9 +3,20 @@
// Retrieve all routines from database
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("SELECT * FROM routine");
$stmt->execute();
$routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Retrieve active API/JSON configurations
$stmt = $pdo->prepare("
SELECT id, name, provider_code, api_type, php_class_name
FROM api_configurations
WHERE is_active = 1
ORDER BY name ASC
");
$stmt->execute();
$apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!doctype html>
<html lang="en">
@@ -40,7 +51,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<li>Template Name</li>
<li>Source Type</li>
<li>Schema and Client</li>
<li>Row Header and Column Header only for XLS templates</li>
<li>Row Header, Column Header and Sheet Number only for XLS templates</li>
<li>API / JSON Configuration only for API / JSON templates</li>
</ul>
</div>
</div>
@@ -67,7 +79,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<label class="form-label">Source Type *</label>
<select name="source_type" id="sourceType" class="form-control" required>
<option value="XLS" selected>XLS</option>
<option value="API">API</option>
<option value="API">API / JSON</option>
<option value="PDF">PDF</option>
</select>
<small class="text-muted">Choose the source used by this template</small>
</div>
@@ -82,6 +95,58 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<input type="text" name="start_column" id="startColumn" class="form-control" value="A" required>
</div>
<div class="mb-3" id="xlsSheetNumberWrapper">
<label class="form-label">XLS Sheet Number</label>
<input
type="number"
name="xls_sheet_index"
id="xlsSheetIndex"
class="form-control"
min="0"
value="0">
<small class="text-muted">
Use 0 for the first sheet, 1 for the second sheet, 2 for the third sheet, and so on.
</small>
</div>
<div class="mb-3" id="apiConfigWrapper" style="display: none;">
<label class="form-label">API / JSON Configuration *</label>
<select name="api_config_id" id="apiConfigSelect" class="form-control">
<option value="">Select an API configuration...</option>
<?php foreach ($apiConfigurations as $apiConfig): ?>
<?php
$apiLabelParts = [];
if (!empty($apiConfig['name'])) {
$apiLabelParts[] = $apiConfig['name'];
}
if (!empty($apiConfig['provider_code'])) {
$apiLabelParts[] = '[' . $apiConfig['provider_code'] . ']';
}
if (!empty($apiConfig['api_type'])) {
$apiLabelParts[] = '(' . $apiConfig['api_type'] . ')';
}
if (!empty($apiConfig['php_class_name'])) {
$apiLabelParts[] = '- ' . $apiConfig['php_class_name'];
}
$apiLabel = implode(' ', $apiLabelParts);
?>
<option value="<?php echo (int)$apiConfig['id']; ?>">
<?php echo htmlspecialchars($apiLabel, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
<small class="text-muted">
Select the API/JSON configuration linked to this template.
</small>
</div>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></label>
<textarea name="description" class="form-control"></textarea>
@@ -185,10 +250,16 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
const routineAction3 = document.getElementById("routineAction3");
const sourceType = document.getElementById("sourceType");
const headerRowWrapper = document.getElementById("headerRowWrapper");
const startColumnWrapper = document.getElementById("startColumnWrapper");
const xlsSheetNumberWrapper = document.getElementById("xlsSheetNumberWrapper");
const apiConfigWrapper = document.getElementById("apiConfigWrapper");
const headerRow = document.getElementById("headerRow");
const startColumn = document.getElementById("startColumn");
const xlsSheetIndex = document.getElementById("xlsSheetIndex");
const apiConfigSelect = document.getElementById("apiConfigSelect");
if (!form || !clientLoadingStatus || !schemaLoadingStatus || !routineSelect || !routineDetails) {
alert("Errore: Uno o più elementi della pagina non sono stati trovati. Contatta l'amministratore.");
@@ -210,27 +281,57 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
allowClear: true
});
$('#apiConfigSelect').select2({
placeholder: "Select an API configuration...",
allowClear: true
});
function updateSourceFields() {
const selectedSource = sourceType.value;
if (selectedSource === 'API') {
headerRowWrapper.style.opacity = '0.6';
startColumnWrapper.style.opacity = '0.6';
const isXls = selectedSource === 'XLS';
const isApiJson = selectedSource === 'API';
headerRow.required = false;
startColumn.required = false;
headerRow.disabled = true;
startColumn.disabled = true;
} else {
headerRowWrapper.style.opacity = '1';
startColumnWrapper.style.opacity = '1';
if (isXls) {
headerRowWrapper.style.display = 'block';
startColumnWrapper.style.display = 'block';
xlsSheetNumberWrapper.style.display = 'block';
headerRow.required = true;
startColumn.required = true;
xlsSheetIndex.required = true;
headerRow.disabled = false;
startColumn.disabled = false;
xlsSheetIndex.disabled = false;
apiConfigWrapper.style.display = 'none';
apiConfigSelect.required = false;
apiConfigSelect.disabled = true;
$('#apiConfigSelect').val(null).trigger('change');
} else {
headerRowWrapper.style.display = 'none';
startColumnWrapper.style.display = 'none';
xlsSheetNumberWrapper.style.display = 'none';
headerRow.required = false;
startColumn.required = false;
xlsSheetIndex.required = false;
headerRow.disabled = true;
startColumn.disabled = true;
xlsSheetIndex.disabled = true;
if (isApiJson) {
apiConfigWrapper.style.display = 'block';
apiConfigSelect.required = true;
apiConfigSelect.disabled = false;
} else {
apiConfigWrapper.style.display = 'none';
apiConfigSelect.required = false;
apiConfigSelect.disabled = true;
$('#apiConfigSelect').val(null).trigger('change');
}
}
}
@@ -261,7 +362,12 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
data.value.forEach(client => {
const nome = client.Nominativo || "Nome non disponibile";
const id = client.IdCliente || "ID non disponibile";
const option = new Option(`${nome.trim()} (ID: ${id})`, id);
const codiceCliente = (client.CodiceCliente ?? client.codiceCliente ?? "").toString().trim();
const suffix = (codiceCliente.split("_")[1] || "").trim();
const shortCode = suffix || (codiceCliente ? codiceCliente.charAt(0) : "--");
const option = new Option(`${nome.trim()} - ${shortCode} (ID: ${id})`, id);
select.add(option);
});
@@ -388,6 +494,28 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
let formData = new FormData(this);
const selectedSource = sourceType.value;
if (selectedSource === 'XLS' && xlsSheetIndex.value === '') {
Swal.fire({
title: "Errore!",
text: "Inserisci il numero del foglio XLS.",
icon: "error",
confirmButtonText: "OK"
});
return;
}
if (selectedSource === 'API' && !apiConfigSelect.value) {
Swal.fire({
title: "Errore!",
text: "Seleziona una configurazione API / JSON.",
icon: "error",
confirmButtonText: "OK"
});
return;
}
const clientSelect = document.getElementById("clientSelect");
const clientId = clientSelect.value;
const selectedClientOption = clientSelect.options[clientSelect.selectedIndex];
+20 -2
View File
@@ -1,5 +1,6 @@
<?php
header('Content-Type: application/json');
include('include/headscript.php');
require_once 'class/db-functions.php';
$response = ["success" => false, "data" => [], "message" => ""];
@@ -12,8 +13,25 @@ try {
throw new Exception('Database connection failed.');
}
// Recupera solo i template attivi
$stmt = $pdo->query("SELECT id, button_label, button_bg_color, button_text_color, button_size FROM excel_templates WHERE status = 'active'");
$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]);
$templates = $stmt->fetchAll(PDO::FETCH_ASSOC);
$response["success"] = true;
+5 -5
View File
@@ -33,9 +33,9 @@ try {
if ($extraFieldId) {
$stmt = $pdo->prepare("
SELECT
p.id, p.iddatadb, p.part_number, p.part_description, p.idmatrice, p.note, p.dateexpiry,
cf.value_id AS extra_value_id,
cf.value_text AS extra_value_text
p.id, p.iddatadb, p.part_number, p.part_description, p.mix, p.idmatrice, p.note, p.dateexpiry,
cf.value_id AS extra_value_id,
cf.value_text AS extra_value_text
FROM identification_parts p
LEFT JOIN identification_parts_customfields cf
ON cf.part_id = p.id AND cf.field_id = :extraFieldId
@@ -48,8 +48,8 @@ try {
]);
} else {
$stmt = $pdo->prepare("
SELECT id, iddatadb, part_number, part_description, idmatrice, note, dateexpiry,
NULL AS extra_value_id, NULL AS extra_value_text
SELECT id, iddatadb, part_number, part_description, mix, idmatrice, note, dateexpiry,
NULL AS extra_value_id, NULL AS extra_value_text
FROM identification_parts
WHERE iddatadb = :iddatadb
ORDER BY part_number ASC
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,106 @@
<?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()]);
}
+3 -5
View File
@@ -259,11 +259,9 @@ $matrixGroups = array_values($matrixGroups);
</div>
<div class="d-flex flex-wrap align-items-center gap-2 mb-3">
<div class="form-check m-0">
<input class="form-check-input" type="checkbox" id="analysisWebOnly">
<label class="form-check-label small" for="analysisWebOnly">
Web only
</label>
<input type="hidden" id="analysisWebOnly" value="1">
<div class="small text-success fw-semibold">
Showing WEB analyses only
</div>
<div class="flex-grow-1" style="min-width: 220px;">
+17
View File
@@ -46,7 +46,18 @@
</div>
</div>
<div class="modal-footer">
<div style="display: flex; align-items: center; gap: 6px; margin-right: auto;">
<label for="descriptionColorPickerAnnotations" style="font-size: 0.8rem; margin: 0; white-space: nowrap;">
Colore annotazioni:
</label>
<input
type="color"
id="descriptionColorPickerAnnotations"
value="#000000"
style="width: 34px; height: 28px; padding: 1px; border: 1px solid #ccc; border-radius: 4px; cursor: pointer;">
</div>
<button type="button" class="btn btn-primary btn-sm" id="addDescriptionsBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Aggiungi Lista Descrizioni</button>
<button type="button" class="btn btn-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>
@@ -270,5 +281,11 @@
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>
+341 -59
View File
@@ -2,7 +2,24 @@
<div class="modal-dialog modal-xl" style="max-width: 95vw !important;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="partsModalLabel">Parti per TRF: <span id="trfHeader"></span></h5>
<h5 class="modal-title" id="partsModalLabel">
Parti per TRF:
<span id="trfHeader"></span>
</h5>
<div class="ms-auto me-3 d-flex align-items-center gap-2">
<button type="button" class="btn btn-outline-secondary btn-sm" id="prevPartsRecordBtn" title="Record precedente">
<i class="fas fa-chevron-left"></i>
</button>
<span id="partsRecordCounter" class="text-muted" style="font-size: 12px; min-width: 70px; text-align: center;">
-
</span>
<button type="button" class="btn btn-outline-secondary btn-sm" id="nextPartsRecordBtn" title="Record successivo">
<i class="fas fa-chevron-right"></i>
</button>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
@@ -12,6 +29,9 @@
<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"
@@ -20,16 +40,21 @@
</button>
<input type="checkbox" id="showMixParts" name="showMixParts" style="margin-right: 5px; margin-left: 10px;">
<label for="showMixParts" style="font-size: 0.9rem; margin-right: 10px;">Mix</label>
<button type="button" class="btn btn-info btn-sm" id="renumberPartsBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
Rinumera Parti
</button>
<button type="button" class="btn btn-primary btn-sm" id="clonePartsBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
<i class="fas fa-clone"></i> Clona Parti
</button>
<button type="button" class="btn btn-secondary btn-sm ms-2" id="toggleVoiceBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
<i class="fas fa-microphone"></i> Voce
</button>
<button type="button" class="btn btn-outline-primary btn-sm ms-2" id="showHideImageBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
<i class="fas fa-eye-slash" style="font-size: 0.8rem;"></i>
<i class="fas fa-image ms-1" style="font-size: 0.8rem;"></i>
</button>
</div>
</div>
<!-- Seconda riga: +, M, MacroMatrice, Matrice globale, Propaga -->
@@ -42,45 +67,54 @@
<select id="global-matrice" class="form-control form-control-sm" style="width: 350px !important; margin-right: 10px;"></select>
<button type="button" class="btn btn-primary btn-sm propagate-all-btn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;"><i class="fas fa-arrow-right"></i> Propaga a tutte</button>
</div>
<table class="table table-striped table-sm" id="partsTable">
<thead>
<tr>
<th style="width: 55px;">Num</th>
<th>Descrizione</th>
<th style="width: 200px;">Matrice</th>
<th style="width: 150px;">
<input type="date" class="form-control form-control-sm propagate-date-input" style="width: 130px; margin-left: 5px; display: inline-block;" title="Propaga data a tutte le parti">
</th>
<th style="width: 200px;">
<button type="button" class="btn btn-light btn-sm propagate-note-btn" style="padding: 0.2rem 0.4rem; font-size: 0.9rem; margin-left: 5px;" title="Propaga nota a tutte le parti">
<i class="fas fa-sticky-note"></i>
</button>
Azioni
<div class="parts-table-scroll">
<table class="table table-striped table-sm" id="partsTable">
<colgroup id="partsTableColgroup">
<col class="parts-col-num">
<col class="parts-col-description">
<col class="parts-col-matrice">
<col class="parts-col-date">
<col class="parts-col-actions">
</colgroup>
<thead>
<tr>
<th style="width: 55px;">Num</th>
<th>Descrizione</th>
<th style="width: 200px;">Matrice</th>
<th style="width: 150px;">
<input type="date" class="form-control form-control-sm propagate-date-input" style="width: 130px; margin-left: 5px; display: inline-block;" title="Propaga data a tutte le parti">
</th>
<th style="width: 200px;">
<button type="button" class="btn btn-light btn-sm propagate-note-btn" style="padding: 0.2rem 0.4rem; font-size: 0.9rem; margin-left: 5px;" title="Propaga nota a tutte le parti">
<i class="fas fa-sticky-note"></i>
</button>
Azioni
</th>
</tr>
</thead>
<tbody id="partsTableBody">
<tr data-part-id="new">
<td><input type="number" class="form-control form-control-sm part-number" value="1" style="width: 55px;"></td>
<td><input type="text" class="form-control form-control-sm part-description" placeholder="Inserisci descrizione"></td>
<td>
<div style="display: flex; align-items: center;">
<button type="button" class="btn btn-primary btn-sm propagate-matrice-btn" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; margin-right: 3px;"><i class="fas fa-arrow-right"></i></button>
<select class="part-matrice form-control form-control-sm" style="width: 150px;"></select>
</div>
</td>
<td><input type="date" class="form-control form-control-sm part-dateexpiry" style="width: 130px;"></td>
<td>
<button type="button" class="btn btn-light btn-sm note-btn" style="padding: 0.2rem 0.4rem; font-size: 0.9rem;" title="Aggiungi/Modifica nota"><i class="fas fa-sticky-note"></i></button>
<button type="button" class="btn btn-warning btn-sm add-mix-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;">M+</button>
<button type="button" class="btn btn-danger btn-sm remove-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; display: none;"><i class="fas fa-trash"></i></button>
<span class="save-status text-success" style="display: none; margin-left: 5px;"><i class="fas fa-check"></i></span>
<span class="save-loading text-warning" style="display: none; margin-left: 5px;"><i class="fas fa-spinner fa-spin"></i></span>
</td>
</tr>
</tbody>
</table>
</th>
</tr>
</thead>
<tbody id="partsTableBody">
<tr data-part-id="new">
<td><input type="number" class="form-control form-control-sm part-number" value="1" style="width: 55px;"></td>
<td><input type="text" class="form-control form-control-sm part-description" placeholder="Inserisci descrizione"></td>
<td>
<div style="display: flex; align-items: center;">
<button type="button" class="btn btn-primary btn-sm propagate-matrice-btn" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; margin-right: 3px;"><i class="fas fa-arrow-right"></i></button>
<select class="part-matrice form-control form-control-sm" style="width: 150px;"></select>
</div>
</td>
<td><input type="date" class="form-control form-control-sm part-dateexpiry" style="width: 130px;"></td>
<td>
<button type="button" class="btn btn-light btn-sm note-btn" style="padding: 0.2rem 0.4rem; font-size: 0.9rem;" title="Aggiungi/Modifica nota"><i class="fas fa-sticky-note"></i></button>
<button type="button" class="btn btn-warning btn-sm add-mix-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;">M+</button>
<button type="button" class="btn btn-danger btn-sm remove-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; display: none;"><i class="fas fa-trash"></i></button>
<span class="save-status text-success" style="display: none; margin-left: 5px;"><i class="fas fa-check"></i></span>
<span class="save-loading text-warning" style="display: none; margin-left: 5px;"><i class="fas fa-spinner fa-spin"></i></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-3">
<h6>Foto del Campione</h6>
@@ -157,6 +191,37 @@
</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>
<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">
@@ -177,6 +242,23 @@
</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
@@ -288,28 +370,20 @@
white-space: nowrap;
}
/* Select delle righe (colonna Matrice) = 150px */
/* Select delle righe Matrice: si adatta alla colonna */
.part-matrice {
width: 300px !important;
min-width: 300px !important;
max-width: 300px !important;
flex: 0 0 300px !important;
width: 100% !important;
min-width: 0 !important;
max-width: 100% !important;
}
.part-matrice.select2-hidden-accessible+.select2 {
width: 300px !important;
min-width: 300px !important;
max-width: 300px !important;
flex: 0 0 300px !important;
width: 100% !important;
min-width: 0 !important;
max-width: 100% !important;
}
/* Colonna Descrizione (2ª colonna) = 420px */
#partsTable th:nth-child(2),
#partsTable td:nth-child(2) {
width: 300px !important;
min-width: 300px !important;
max-width: 300px !important;
}
#partsTable .part-number {
text-align: center;
@@ -340,7 +414,11 @@
border: 1px solid #aaa !important;
border-radius: 4px !important;
background: #fff !important;
max-height: 200px !important;
overflow: visible !important;
}
.select2-container--open .select2-results__options {
max-height: 220px !important;
overflow-y: auto !important;
}
@@ -514,6 +592,210 @@
}
}
/* Resizable parts table - stable */
.parts-table-scroll {
width: 100%;
overflow-x: auto;
overflow-y: visible;
contain: inline-size;
}
#partsTable {
table-layout: fixed !important;
width: max-content !important;
min-width: unset !important;
}
#partsTable col.parts-col-num {
width: 55px;
}
#partsTable col.parts-col-description {
width: 320px;
}
#partsTable col.parts-col-matrice {
width: 360px;
}
#partsTable col.parts-col-date {
width: 150px;
}
#partsTable col.parts-col-actions {
width: 230px;
}
#partsTable th,
#partsTable td {
position: relative;
box-sizing: border-box;
}
#partsTable th {
user-select: none;
}
#partsTable th .parts-resizer {
position: absolute;
top: 0;
right: 0;
width: 9px;
height: 100%;
cursor: col-resize;
z-index: 50;
background: rgba(108, 117, 125, 0.12);
border-right: 1px solid rgba(108, 117, 125, 0.45);
}
#partsTable th .parts-resizer:hover {
background: rgba(108, 117, 125, 0.25);
border-right: 2px solid rgba(73, 80, 87, 0.7);
}
#partsTable td {
overflow: hidden;
text-overflow: ellipsis;
}
#partsTable td input,
#partsTable td select,
#partsTable td .select2-container {
max-width: 100% !important;
}
/* Propagation control for dynamic extra field column */
#partsTable th.extra-field-th {
vertical-align: middle;
}
#partsTable th.extra-field-th .extra-propagate-wrapper {
display: flex;
align-items: center;
gap: 4px;
width: 100%;
}
#partsTable th.extra-field-th .extra-propagate-label {
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 95px;
}
#partsTable th.extra-field-th .extra-propagate-input,
#partsTable th.extra-field-th .extra-propagate-select {
height: 24px !important;
font-size: 0.75rem !important;
padding: 0.1rem 0.25rem !important;
min-width: 90px;
flex: 1 1 auto;
}
#partsTable th.extra-field-th .extra-propagate-btn {
height: 24px;
min-width: 26px;
padding: 0.1rem 0.3rem !important;
font-size: 0.75rem !important;
flex: 0 0 auto;
}
#partsTable th.extra-field-th .select2-container {
flex: 1 1 auto;
min-width: 90px;
max-width: 100% !important;
}
#partsTable th.extra-field-th .select2-selection--single {
height: 24px !important;
}
#partsTable th.extra-field-th .select2-selection__rendered {
line-height: 22px !important;
font-size: 0.75rem !important;
padding-left: 4px !important;
}
#partsTable th.extra-field-th .select2-selection__arrow {
height: 22px !important;
}
/* Extra field propagation header - full column width */
#partsTable th.extra-field-th {
width: auto !important;
min-width: 220px !important;
max-width: none !important;
padding: 0.25rem !important;
}
#partsTable .extra-propagate-wrapper {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
display: flex;
flex-direction: column;
gap: 4px;
box-sizing: border-box;
}
#partsTable .extra-propagate-label {
width: 100% !important;
max-width: 100% !important;
display: block;
font-size: 0.82rem;
font-weight: 700;
line-height: 1.15;
color: #222;
white-space: nowrap !important;
overflow: visible !important;
text-overflow: unset !important;
text-align: center;
}
#partsTable .extra-propagate-control {
width: 100% !important;
display: flex;
align-items: center;
gap: 5px;
box-sizing: border-box;
}
#partsTable .extra-propagate-control .extra-propagate-input,
#partsTable .extra-propagate-control .extra-propagate-select {
flex: 1 1 auto !important;
width: 100% !important;
min-width: 0 !important;
max-width: none !important;
}
#partsTable .extra-propagate-control .select2-container {
flex: 1 1 auto !important;
width: 100% !important;
min-width: 0 !important;
max-width: none !important;
}
#partsTable .extra-propagate-control .select2-selection {
width: 100% !important;
}
#partsTable .extra-propagate-btn {
flex: 0 0 30px !important;
width: 30px !important;
height: 24px !important;
min-width: 30px !important;
padding: 0.1rem 0.25rem !important;
}
#cloneConfirmModal {
z-index: 1070 !important
}
#cloneConfirmModal .modal-backdrop {
z-index: 1065 !important
}
/* rosso */
</style>
+124 -78
View File
@@ -2,93 +2,111 @@
* modals_gridData.js — Photos, Parts, Tested Component handlers for gridData pages
*/
(function () {
'use strict';
"use strict";
// ── Photos — use photos.js loadPopupContent (exported to window) ──
$(document).on('click', '.photos-btn', function () {
const iddatadb = $(this).data('iddatadb') || null;
const idquotations = $(this).data('idquotations') || null;
const modal = document.getElementById('photosModal');
$(document).on("click", ".photos-btn", function () {
const iddatadb = $(this).data("iddatadb") || null;
const idquotations = $(this).data("idquotations") || null;
const modal = document.getElementById("photosModal");
if (!modal) return;
modal.style.display = 'block';
modal.style.display = "block";
if (typeof window.loadPopupContent === 'function') {
if (typeof window.loadPopupContent === "function") {
window.loadPopupContent(iddatadb, idquotations);
}
});
// Close photos modal
$(document).on('click', '.close-btn', function () {
const modal = document.getElementById('photosModal');
if (modal) modal.style.display = 'none';
$(document).on("click", ".close-btn", function () {
const modal = document.getElementById("photosModal");
if (modal) modal.style.display = "none";
});
// Close on backdrop click
$(document).on('click', '#photosModal', function (e) {
$(document).on("click", "#photosModal", function (e) {
if (e.target === this) {
this.style.display = 'none';
this.style.display = "none";
}
});
// ── Parts (matching import_edit2.php behavior) ─────────────────────
$(document).on('click', '.parts-btn', function () {
const iddatadb = $(this).data('iddatadb') || null;
const idquotations = $(this).data('idquotations') || null;
$(document).on("click", ".parts-btn", function () {
const iddatadb = $(this).data("iddatadb") || null;
const idquotations = $(this).data("idquotations") || null;
$.ajax({
url: 'modal_partsTable.php',
method: 'GET',
url: "modal_partsTable.php",
method: "GET",
data: { iddatadb: iddatadb },
success: function (response) {
$('#partsModalContainer').html(response);
const modalElement = document.getElementById('partsModal');
$("#partsModalContainer").html(response);
const modalElement = document.getElementById("partsModal");
if (!modalElement) return;
$("#trfHeader").text(iddatadb || idquotations || '');
$("#partsModal").data("iddatadb", iddatadb).data("idquotations", idquotations);
$("#trfHeader").text(iddatadb || idquotations || "");
const visibleIddatadbList = Array.isArray(window.gridData)
? window.gridData
.map((r) => parseInt(r.iddatadb, 10))
.filter((v) => !isNaN(v) && v > 0)
: [];
$("#partsModal")
.data("iddatadb", iddatadb)
.data("idquotations", idquotations)
.data("visible-iddatadb-list", visibleIddatadbList);
let modal = bootstrap.Modal.getInstance(modalElement);
if (!modal) modal = new bootstrap.Modal(modalElement, { backdrop: true, keyboard: true, focus: true });
if (!modal)
modal = new bootstrap.Modal(modalElement, {
backdrop: true,
keyboard: true,
focus: true,
});
modal.show();
if (typeof window.loadParts === 'function') {
if (typeof window.loadParts === "function") {
window.loadParts(iddatadb, idquotations);
}
},
error: function (xhr, status, error) {
console.error('Error loading parts:', error);
}
console.error("Error loading parts:", error);
},
});
});
$(document).on('hidden.bs.modal', '#partsModal', function () {
const modalElement = document.getElementById('partsModal');
$(document).on("hidden.bs.modal", "#partsModal", function () {
const modalElement = document.getElementById("partsModal");
if (modalElement) {
const modal = bootstrap.Modal.getInstance(modalElement);
if (modal) modal.dispose();
}
$('#partsModalContainer').empty();
$('.modal-backdrop').remove();
$('body').removeClass('modal-open').css('padding-right', '');
$("#partsModalContainer").empty();
$(".modal-backdrop").remove();
$("body").removeClass("modal-open").css("padding-right", "");
});
// ── Tested Component quick add ───────────────────────────────────────
$(document).on('click', '.add-part-btn', async function () {
const iddatadb = $(this).data('iddatadb') || null;
const rowIndex = parseInt($(this).data('row'));
$(document).on("click", ".add-part-btn", async function () {
const iddatadb = $(this).data("iddatadb") || null;
const rowIndex = parseInt($(this).data("row"));
const row = window.gridData?.[rowIndex];
const id = iddatadb || (row ? row.iddatadb : null);
if (!id) return;
const $cell = $(this).closest('.grid-cell, div');
const $input = $cell.find('input');
const raw = ($input.val() || '').trim();
const parts = raw.split('|').map(s => s.trim()).filter(s => s.length > 0);
const $cell = $(this).closest(".grid-cell, div");
const $input = $cell.find("input");
const raw = ($input.val() || "").trim();
const parts = raw
.split("|")
.map((s) => s.trim())
.filter((s) => s.length > 0);
const uniqueParts = [...new Set(parts)];
if (!uniqueParts.length) {
alert('Insert a description first.');
alert("Insert a description first.");
$input.focus();
return;
}
@@ -96,14 +114,23 @@
try {
for (const p of uniqueParts) {
const formData = new FormData();
formData.append('iddatadb', id);
formData.append('part_description', p);
await fetch('add_part_quick.php', { method: 'POST', body: formData });
formData.append("iddatadb", id);
formData.append("part_description", p);
await fetch("add_part_quick.php", {
method: "POST",
body: formData,
});
}
if (row) {
row.tested_component = raw;
row._dirty = true;
}
alert(`Added ${uniqueParts.length} part(s).`);
$input.val('');
$input.val(raw);
} catch (e) {
alert('Error: ' + e.message);
alert("Error: " + e.message);
}
});
@@ -111,44 +138,47 @@
let deleteIddatadb = null;
let deleteRowIndex = null;
$(document).on('click', '.delete-btn', function () {
deleteIddatadb = $(this).data('iddatadb');
deleteRowIndex = parseInt($(this).data('row'));
const modalEl = document.getElementById('deleteConfirmModal');
$(document).on("click", ".delete-btn", function () {
deleteIddatadb = $(this).data("iddatadb");
deleteRowIndex = parseInt($(this).data("row"));
const modalEl = document.getElementById("deleteConfirmModal");
if (modalEl) {
document.getElementById('deleteIddatadbText').textContent = deleteIddatadb;
document.getElementById("deleteIddatadbText").textContent =
deleteIddatadb;
new bootstrap.Modal(modalEl).show();
}
});
$(document).on('click', '#deleteConfirmBtn', async function () {
const modalEl = document.getElementById('deleteConfirmModal');
$(document).on("click", "#deleteConfirmBtn", async function () {
const modalEl = document.getElementById("deleteConfirmModal");
const modal = bootstrap.Modal.getInstance(modalEl);
if (modal) modal.hide();
if (!deleteIddatadb) return;
try {
const resp = await fetch('delete_record.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: deleteIddatadb })
const resp = await fetch("delete_record.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: deleteIddatadb }),
});
const result = await resp.json();
if (result.success) {
// Remove from gridData
const idx = window.gridData.findIndex(r => r.iddatadb === deleteIddatadb);
const idx = window.gridData.findIndex(
(r) => r.iddatadb === deleteIddatadb,
);
if (idx >= 0) window.gridData.splice(idx, 1);
// Re-render
const gr = window.gridRenderer;
if (gr) gr.renderVisibleRows();
} else {
alert('Error: ' + result.message);
alert("Error: " + result.message);
}
} catch (e) {
alert('Error: ' + e.message);
alert("Error: " + e.message);
}
deleteIddatadb = null;
@@ -156,20 +186,26 @@
});
// ── Add new row ──────────────────────────────────────────────────────
$(document).on('click', '#addRowBtn', async function () {
$(document).on("click", "#addRowBtn", async function () {
const btn = this;
btn.disabled = true;
try {
const templateId = window.gridMeta?.templateId;
if (!templateId) { alert('Template ID missing'); return; }
if (!templateId) {
alert("Template ID missing");
return;
}
const urlParams = new URLSearchParams(window.location.search);
const importref = urlParams.get('importref') || '';
const resp = await fetch('add_record.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ template_id: templateId, importreferencecode: importref })
const importref = urlParams.get("importref") || "";
const resp = await fetch("add_record.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
template_id: templateId,
importreferencecode: importref,
}),
});
const result = await resp.json();
@@ -177,17 +213,20 @@
// Build new row object
const newRow = {
iddatadb: result.iddatadb,
status: 'i',
idclient: window.gridMeta?.defaultIdclient || '',
status: "i",
idclient: window.gridMeta?.defaultIdclient || "",
cliente_fornitore_id: null,
commessaweb: null,
user_name: result.user_name || '',
importreferencecode: result.importreferencecode || '',
filename_import: '',
importdate: new Date().toISOString().slice(0, 19).replace('T', ' '),
user_name: result.user_name || "",
importreferencecode: result.importreferencecode || "",
filename_import: "",
importdate: new Date()
.toISOString()
.slice(0, 19)
.replace("T", " "),
fixedFields: {},
details: {},
mainFieldValue: '',
mainFieldValue: "",
_dirty: false,
};
@@ -199,20 +238,27 @@
if (gr) gr.renderVisibleRows();
// Highlight new row briefly
const newGridRow = document.querySelector(`.grid-row[data-id="${result.iddatadb}"]`);
const newGridRow = document.querySelector(
`.grid-row[data-id="${result.iddatadb}"]`,
);
if (newGridRow) {
newGridRow.classList.add('row-just-created');
newGridRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => newGridRow.classList.remove('row-just-created'), 4000);
newGridRow.classList.add("row-just-created");
newGridRow.scrollIntoView({
behavior: "smooth",
block: "center",
});
setTimeout(
() => newGridRow.classList.remove("row-just-created"),
4000,
);
}
} else {
alert('Error: ' + (result.message || 'Unknown error'));
alert("Error: " + (result.message || "Unknown error"));
}
} catch (e) {
alert('Error: ' + e.message);
alert("Error: " + e.message);
} finally {
btn.disabled = false;
}
});
})();
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json; charset=utf-8');
ini_set('display_errors', '0');
error_reporting(E_ALL);
set_time_limit(20);
try {
$api = VisualLimsApiClient::getInstance();
$start = microtime(true);
// Chiamata minima: 1 solo record, 1 solo campo
$data = $api->get('Rapporto', [
'$top' => 1,
'$select' => 'IdRapporto'
]);
$elapsed = round(microtime(true) - $start, 3);
echo json_encode([
'success' => true,
'elapsed_seconds' => $elapsed,
'data' => $data
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+56 -4
View File
@@ -13,8 +13,21 @@ try {
$id = intval($_POST['id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$source_type = strtoupper(trim($_POST['source_type'] ?? 'XLS'));
$header_row = isset($_POST['header_row']) && $_POST['header_row'] !== '' ? intval($_POST['header_row']) : null;
$header_row = isset($_POST['header_row']) && $_POST['header_row'] !== ''
? intval($_POST['header_row'])
: null;
$start_column = trim($_POST['start_column'] ?? '');
$xls_sheet_index = isset($_POST['xls_sheet_index']) && $_POST['xls_sheet_index'] !== ''
? intval($_POST['xls_sheet_index'])
: 0;
$api_config_id = isset($_POST['api_config_id']) && $_POST['api_config_id'] !== ''
? intval($_POST['api_config_id'])
: null;
$description = trim($_POST['description'] ?? '');
$target_table = trim($_POST['target_table'] ?? 'datadb');
$idclient = intval($_POST['client_id'] ?? 0);
@@ -27,7 +40,8 @@ try {
$button_text_color = trim($_POST['button_text_color'] ?? '#ffffff');
$button_label = trim($_POST['button_label'] ?? 'Click Me');
if (!in_array($source_type, ['XLS', 'API'], true)) {
// Allowed source types
if (!in_array($source_type, ['XLS', 'API', 'JSON', 'PDF'], true)) {
$source_type = 'XLS';
}
@@ -41,18 +55,52 @@ try {
if ($header_row === null || $header_row <= 0 || $start_column === '') {
throw new Exception("Header Row and Start Column are required for XLS templates.");
}
if ($xls_sheet_index < 0) {
throw new Exception("XLS Sheet Number cannot be negative.");
}
$api_config_id = null;
}
// API templates do not require XLS coordinates
if ($source_type === 'API') {
// API/JSON validation
if ($source_type === 'API' || $source_type === 'JSON') {
if (empty($api_config_id)) {
throw new Exception("API/JSON configuration is required for API or JSON templates.");
}
$header_row = null;
$start_column = null;
$xls_sheet_index = null;
}
// PDF currently does not require XLS coordinates or API configuration
if ($source_type === 'PDF') {
$header_row = null;
$start_column = null;
$xls_sheet_index = null;
$api_config_id = null;
}
// Database connection
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Optional check: verify API configuration exists and is active
if ($api_config_id !== null) {
$stmt = $pdo->prepare("
SELECT COUNT(*)
FROM api_configurations
WHERE id = ?
AND is_active = 1
");
$stmt->execute([$api_config_id]);
if ((int)$stmt->fetchColumn() === 0) {
throw new Exception("Selected API/JSON configuration does not exist or is not active.");
}
}
// Update template
$stmt = $pdo->prepare("
UPDATE excel_templates
@@ -61,6 +109,8 @@ try {
source_type = ?,
header_row = ?,
start_column = ?,
xls_sheet_index = ?,
api_config_id = ?,
description = ?,
target_table = ?,
idclient = ?,
@@ -81,6 +131,8 @@ try {
$source_type,
$header_row,
$start_column,
$xls_sheet_index,
$api_config_id,
$description,
$target_table,
$idclient,
+3
View File
@@ -10,6 +10,9 @@ session_start();
// Includi PHPSpreadsheet
require_once '../../vendor/autoload.php';
Dotenv\Dotenv::createImmutable(dirname(__DIR__, 2))->safeLoad();
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
$response = ['error' => '', 'rows' => [], 'columns' => [], 'template_id' => 0, 'filename' => ''];
try {
+365 -37
View File
@@ -11,17 +11,241 @@ session_start();
require_once '../../vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
$response = ['error' => '', 'rows' => [], 'columns' => [], 'template_id' => 0, 'filename' => '', 'apply_routine' => false];
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
$response = [
'error' => '',
'rows' => [],
'columns' => [],
'template_id' => 0,
'filename' => '',
'apply_routine' => false
];
/**
* Converts a column value to a PhpSpreadsheet 1-based column index.
* Accepted values:
* - "A" => 1
* - "B" => 2
* - "AA" => 27
* - "1" => 1
* - 1 => 1
*/
function normalizeColumnIndex($value): int
{
$value = trim((string)$value);
if ($value === '') {
return 1;
}
if (ctype_digit($value)) {
return max(1, (int)$value);
}
$value = strtoupper($value);
if (preg_match('/^[A-Z]+$/', $value)) {
return Coordinate::columnIndexFromString($value);
}
return 1;
}
/**
* 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;
$header_row = isset($_POST['header_row']) ? intval($_POST['header_row']) : 1;
$start_column = isset($_POST['start_column']) ? intval($_POST['start_column']) : 1;
if ($template_id <= 0) {
throw new Exception("Template ID non valido.");
}
// Connessione al database
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
/*
* Recuperiamo i parametri direttamente dal template.
* Così non dipendiamo solo dal form e siamo sicuri di usare i dati salvati.
*/
$stmt = $pdo->prepare("
SELECT
id,
header_row,
start_column,
xls_sheet_index,
idroutine,
idclient
FROM excel_templates
WHERE id = ?
");
$stmt->execute([$template_id]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$template) {
throw new Exception("Template non trovato.");
}
$header_row = isset($template['header_row']) && $template['header_row'] !== null
? (int)$template['header_row']
: 1;
$start_column_raw = $template['start_column'] ?? 'A';
$start_column = normalizeColumnIndex($start_column_raw);
$xlsSheetIndex = isset($template['xls_sheet_index']) && $template['xls_sheet_index'] !== null
? (int)$template['xls_sheet_index']
: 0;
if ($header_row <= 0) {
$header_row = 1;
}
if ($xlsSheetIndex < 0) {
$xlsSheetIndex = 0;
}
// Debug del template_id ricevuto
error_log("Received template_id from POST: " . print_r($_POST['template_id'], true));
error_log("Converted template_id: $template_id");
error_log("Template XLS settings - header_row: $header_row, start_column_raw: $start_column_raw, start_column_index: $start_column, xls_sheet_index: $xlsSheetIndex");
$file = $_FILES['excel_file'];
$fileError = $file['error'];
@@ -38,23 +262,32 @@ try {
$originalFilename = basename($file['name']);
$newFilename = "{$iduserlogin}-{$timestamp}-{$originalFilename}";
$importFolder = __DIR__ . '/imported_trf/';
if (!file_exists($importFolder)) {
mkdir($importFolder, 0777, true);
}
$destination = $importFolder . $newFilename;
// Sposta il file
if (!move_uploaded_file($file['tmp_name'], $destination)) {
throw new Exception("Errore durante lo spostamento del file in $destination");
}
error_log("File spostato con successo in: $destination");
// Connessione al database
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Recupera il mapping da template_mapping
$stmt = $pdo->prepare("SELECT field_id AS excel_column, field_id AS mysql_column, data_type, is_required, default_value, is_manual FROM template_mapping WHERE template_id = ?");
$stmt = $pdo->prepare("
SELECT
field_id AS excel_column,
field_id AS mysql_column,
data_type,
is_required,
default_value,
is_manual
FROM template_mapping
WHERE template_id = ?
");
$stmt->execute([$template_id]);
$mappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -64,45 +297,112 @@ try {
if (empty($mappings)) {
$response['error'] = "Nessun mapping trovato per il template con ID $template_id";
} else {
// Carica il file rinominato con PHPSpreadsheet
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($destination);
$worksheet = $spreadsheet->getActiveSheet();
$highestRow = $worksheet->getHighestRow();
$highestColumn = $worksheet->getHighestColumn();
$highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn);
// 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);
}
$sheetCount = $spreadsheet->getSheetCount();
$sheetNames = $spreadsheet->getSheetNames();
if ($sheetCount <= 0) {
throw new Exception("Il file XLS non contiene fogli.");
}
if ($xlsSheetIndex >= $sheetCount) {
throw new Exception(
"Il foglio XLS selezionato non esiste. " .
"Sheet Number selezionato: {$xlsSheetIndex}. " .
"Fogli disponibili: " . implode(", ", array_map(
fn($name, $index) => "{$index}={$name}",
$sheetNames,
array_keys($sheetNames)
))
);
}
// Usa il foglio configurato nel template
$worksheet = $spreadsheet->getSheet($xlsSheetIndex);
$selectedSheetName = $worksheet->getTitle();
error_log("Selected XLS sheet - index: {$xlsSheetIndex}, name: {$selectedSheetName}");
$highestRow = $worksheet->getHighestDataRow();
$highestColumn = $worksheet->getHighestDataColumn();
$highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
$startRow = max(1, $header_row);
$startColumn = max(1, $start_column);
// Advance startColumn to first non-empty cell in header row (match JS behavior)
// Advance startColumn to first non-empty cell in header row, matching JS behavior
for ($sc = $startColumn; $sc <= $highestColumnIndex; $sc++) {
$cl = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($sc);
$cv = trim((string)($worksheet->getCell($cl . $header_row)->getCalculatedValue() ?? ''));
$cl = Coordinate::stringFromColumnIndex($sc);
$cv = cleanCellText($worksheet->getCell($cl . $header_row)->getCalculatedValue() ?? '');
if ($cv !== '') {
$startColumn = $sc;
break;
}
}
$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, startRow: $startRow, startColumn: $startColumn, highestRow: $highestRow, highestColumn: $highestColumn, highestColumnIndex: $highestColumnIndex");
error_log(
"Processing - template_id: $template_id, " .
"sheetIndex: $xlsSheetIndex, sheetName: $selectedSheetName, " .
"startRow: $startRow, startColumn: $startColumn, " .
"highestRow: $highestRow, highestColumn: $highestColumn, highestColumnIndex: $highestColumnIndex"
);
// Validazione degli indici
if ($startRow > $highestRow) {
$response['error'] = "La riga di partenza ($startRow) supera il numero totale di righe ($highestRow).";
$response['error'] = "La riga di partenza ($startRow) supera il numero totale di righe ($highestRow) del foglio '$selectedSheetName'.";
} elseif ($startColumn > $highestColumnIndex) {
$response['error'] = "La colonna di partenza ($startColumn) supera il numero totale di colonne ($highestColumnIndex).";
$response['error'] = "La colonna di partenza ($startColumn) supera il numero totale di colonne ($highestColumnIndex) del foglio '$selectedSheetName'.";
} else {
$excelData = [];
// Build merge map for header row: physCol -> mergeStartCol
$mergeStartMap = [];
foreach ($worksheet->getMergeCells() as $range) {
[$startCell, $endCell] = explode(':', $range);
$mStartCol = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString(preg_replace('/\d+/', '', $startCell));
$mEndCol = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString(preg_replace('/\d+/', '', $endCell));
$mStartCol = Coordinate::columnIndexFromString(preg_replace('/\d+/', '', $startCell));
$mEndCol = Coordinate::columnIndexFromString(preg_replace('/\d+/', '', $endCell));
$mStartRow = (int)preg_replace('/[A-Z]+/i', '', $startCell);
$mEndRow = (int)preg_replace('/[A-Z]+/i', '', $endCell);
if ($header_row >= $mStartRow && $header_row <= $mEndRow) {
for ($c = $mStartCol; $c <= $mEndCol; $c++) {
$mergeStartMap[$c] = $mStartCol;
@@ -111,12 +411,17 @@ try {
}
// Build logical columns: each merge = one column
$logicalCols = []; // array of physical column indices (one per logical column)
$logicalCols = []; // array of physical column indices, one per logical column
$seen = [];
for ($col = $startColumn; $col <= $highestColumnIndex; $col++) {
if (isset($mergeStartMap[$col])) {
$ms = $mergeStartMap[$col];
if (in_array($ms, $seen, true)) continue;
if (in_array($ms, $seen, true)) {
continue;
}
$seen[] = $ms;
$logicalCols[] = $ms;
} else {
@@ -127,56 +432,74 @@ try {
// Build header row using logical columns
$headerRowData = [];
$logicalNum = 0;
foreach ($logicalCols as $physCol) {
$logicalNum++;
$columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($physCol);
$columnLetter = Coordinate::stringFromColumnIndex($physCol);
$cell = $worksheet->getCell($columnLetter . $header_row);
$cellValue = trim((string)($cell ? $cell->getCalculatedValue() : ''));
$cellValue = cleanCellText($cell ? $cell->getCalculatedValue() : '');
$cellValue = preg_replace('/[\r\n\t]+/', ' ', $cellValue);
// Empty headers get __empty_N__ to match mapping page
$headerRowData[] = ($cellValue !== '') ? $cellValue : '__empty_' . $logicalNum . '__';
}
error_log("Logical headers: " . json_encode($headerRowData));
error_log("Logical cols (physical indices): " . json_encode($logicalCols));
error_log("Logical cols physical indices: " . json_encode($logicalCols));
// Find which logical columns have real headers
$headerFilledIndices = [];
foreach ($headerRowData as $idx => $hVal) {
if (!str_starts_with($hVal, '__empty_')) $headerFilledIndices[] = $idx;
if (!str_starts_with($hVal, '__empty_')) {
$headerFilledIndices[] = $idx;
}
}
$minFilled = max(1, min(2, count($headerFilledIndices)));
// Extract data rows using logical columns
for ($row = $startRow + 1; $row <= $highestRow; $row++) {
$rowData = [];
foreach ($logicalCols as $physCol) {
$columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($physCol);
$columnLetter = Coordinate::stringFromColumnIndex($physCol);
$cell = $worksheet->getCell($columnLetter . $row);
$cellValue = $cell ? $cell->getCalculatedValue() : '';
$rowData[] = $cellValue ?: '';
}
// Count how many header columns have data in this row
$filledCount = 0;
foreach ($headerFilledIndices as $idx) {
if (isset($rowData[$idx]) && trim((string)$rowData[$idx]) !== '') {
if (isset($rowData[$idx]) && cleanCellText($rowData[$idx]) !== '') {
$filledCount++;
}
}
if ($filledCount >= $minFilled) {
$excelData[] = ['data' => $rowData, 'excelrow' => $row];
$excelData[] = [
'data' => $rowData,
'excelrow' => $row
];
}
}
// Recupera routine dal template
$stmt = $pdo->prepare("SELECT idroutine, idclient FROM excel_templates WHERE id = ?");
$stmt->execute([$template_id]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
if ($template && $template['idroutine']) {
$stmtRoutine = $pdo->prepare("SELECT idroutine, name, filename, headerrow, instruction FROM routine WHERE idroutine = ?");
if ($template && !empty($template['idroutine'])) {
$stmtRoutine = $pdo->prepare("
SELECT
idroutine,
name,
filename,
headerrow,
instruction
FROM routine
WHERE idroutine = ?
");
$stmtRoutine->execute([$template['idroutine']]);
$routineData = $stmtRoutine->fetch(PDO::FETCH_ASSOC);
@@ -188,6 +511,7 @@ try {
'filename' => $routineData['filename'] ?? '',
'headerrow' => $routineData['headerrow'] ?? $header_row
];
error_log("Routine rilevata per template {$template_id}: " . print_r($routineData, true));
} else {
error_log("Errore: Nessuna routine trovata per idroutine {$template['idroutine']}");
@@ -204,6 +528,8 @@ try {
$_SESSION['template_id'] = $template_id;
$_SESSION['headers'] = $headerRowData;
$_SESSION['mappings'] = $mappings;
$_SESSION['xls_sheet_index'] = $xlsSheetIndex;
$_SESSION['xls_sheet_name'] = $selectedSheetName;
// Includi excel_data nella risposta JSON in ogni caso
$response['excel_data'] = $excelData;
@@ -211,6 +537,8 @@ try {
$response['columns'] = $headerRowData;
$response['template_id'] = $template_id;
$response['filename'] = $newFilename;
$response['xls_sheet_index'] = $xlsSheetIndex;
$response['xls_sheet_name'] = $selectedSheetName;
}
}
} else {
@@ -12,22 +12,39 @@ try {
// Retrieve and sanitize form data
$name = trim($_POST['name'] ?? '');
$source_type = strtoupper(trim($_POST['source_type'] ?? 'XLS'));
$header_row = isset($_POST['header_row']) && $_POST['header_row'] !== '' ? intval($_POST['header_row']) : null;
$header_row = isset($_POST['header_row']) && $_POST['header_row'] !== ''
? intval($_POST['header_row'])
: null;
$start_column = trim($_POST['start_column'] ?? '');
$xls_sheet_index = isset($_POST['xls_sheet_index']) && $_POST['xls_sheet_index'] !== ''
? intval($_POST['xls_sheet_index'])
: 0;
$api_config_id = isset($_POST['api_config_id']) && $_POST['api_config_id'] !== ''
? intval($_POST['api_config_id'])
: null;
$description = trim($_POST['description'] ?? '');
$target_table = trim($_POST['target_table'] ?? 'datadb');
$idclient = intval($_POST['client_id'] ?? 0);
$clientname = trim($_POST['client_name'] ?? '');
$idschema = intval($_POST['idschema'] ?? 0);
$schemaname = trim($_POST['schemaname'] ?? '');
$idroutine = isset($_POST['idroutine']) && $_POST['idroutine'] !== '' ? intval($_POST['idroutine']) : null;
$idroutine = isset($_POST['idroutine']) && $_POST['idroutine'] !== ''
? intval($_POST['idroutine'])
: null;
$button_size = trim($_POST['button_size'] ?? 'medium');
$button_bg_color = trim($_POST['button_bg_color'] ?? '#007bff');
$button_text_color = trim($_POST['button_text_color'] ?? '#ffffff');
$button_label = trim($_POST['button_label'] ?? 'Click Me');
// Normalize source type
if (!in_array($source_type, ['XLS', 'API'], true)) {
// API / JSON is saved as API
if (!in_array($source_type, ['XLS', 'API', 'PDF'], true)) {
$source_type = 'XLS';
}
@@ -41,26 +58,62 @@ try {
if ($header_row === null || $header_row <= 0 || $start_column === '') {
throw new Exception("Header Row and Start Column are required for XLS templates.");
}
if ($xls_sheet_index < 0) {
throw new Exception("XLS Sheet Number cannot be negative.");
}
$api_config_id = null;
}
// API templates do not require XLS coordinates
// API / JSON validation
if ($source_type === 'API') {
if (empty($api_config_id)) {
throw new Exception("API / JSON configuration is required for API / JSON templates.");
}
$header_row = null;
$start_column = null;
$xls_sheet_index = null;
}
// PDF currently does not require XLS coordinates or API configuration
if ($source_type === 'PDF') {
$header_row = null;
$start_column = null;
$xls_sheet_index = null;
$api_config_id = null;
}
// Database connection
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Optional check: verify API configuration exists and is active
if ($api_config_id !== null) {
$stmt = $pdo->prepare("
SELECT COUNT(*)
FROM api_configurations
WHERE id = ?
AND is_active = 1
");
$stmt->execute([$api_config_id]);
if ((int)$stmt->fetchColumn() === 0) {
throw new Exception("Selected API / JSON configuration does not exist or is not active.");
}
}
// Insert the new template
$stmt = $pdo->prepare("
INSERT INTO excel_templates
INSERT INTO excel_templates
(
name,
source_type,
header_row,
start_column,
xls_sheet_index,
api_config_id,
description,
target_table,
idclient,
@@ -75,7 +128,13 @@ try {
created_at,
updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
VALUES
(
?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?,
NOW(), NOW()
)
");
$stmt->execute([
@@ -83,6 +142,8 @@ try {
$source_type,
$header_row,
$start_column,
$xls_sheet_index,
$api_config_id,
$description,
$target_table,
$idclient,
File diff suppressed because it is too large Load Diff
+71
View File
@@ -0,0 +1,71 @@
<?php
/**
* Routine: burberry
*
* Purpose:
* For each imported XLS row:
* - read the value from column S
* - read the value from column T
* - merge the values
* - save the final value into column S
*
* Target:
* Column S must be mapped to the destination field in the template mapping.
*/
function applyRoutine(&$excelData, $routineData = [])
{
/*
* This routine does not require external routine data.
* Columns are fixed.
*
* Excel column indexes are zero-based:
*
* S = 18
* T = 19
*/
$targetColumnIndex = 18; // S
$columnSIndex = 18; // S
$columnTIndex = 19; // T
foreach ($excelData as $rowIndex => &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Routine burberry: invalid row structure at index {$rowIndex}.");
continue;
}
$valueS = trim((string)($row['data'][$columnSIndex] ?? ''));
$valueT = trim((string)($row['data'][$columnTIndex] ?? ''));
/*
* Merge values, ignoring empty values.
*/
$mergedValues = [];
if ($valueS !== '') {
$mergedValues[] = $valueS;
}
if ($valueT !== '') {
$mergedValues[] = $valueT;
}
/*
* Save final value into column S.
*/
$row['data'][$targetColumnIndex] = implode(' ', $mergedValues);
error_log(
"Routine burberry: row " .
($row['excelrow'] ?? $rowIndex) .
" generated value in column S: " .
$row['data'][$targetColumnIndex]
);
}
unset($row);
error_log("Routine burberry completed.");
}
+67
View File
@@ -0,0 +1,67 @@
<?php
/**
* Routine: merge_column_T_and_U_into_T
*
* Purpose:
* For each imported XLS row:
* - read the value from column T
* - read the value from column U
* - merge both values
* - save the final value into column T
*
* Target:
* Column T must be mapped to the destination field in the template mapping.
*/
function applyRoutine(&$excelData, $routineData = [])
{
/*
* Excel column indexes are zero-based:
*
* T = 19
* U = 20
*/
$targetColumnIndex = 19; // T
$firstColumnIndex = 19; // T
$secondColumnIndex = 20; // U
foreach ($excelData as $rowIndex => &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Routine merge T+U: invalid row structure at index {$rowIndex}.");
continue;
}
$valueT = trim((string)($row['data'][$firstColumnIndex] ?? ''));
$valueU = trim((string)($row['data'][$secondColumnIndex] ?? ''));
/*
* Merge values, ignoring empty values.
*/
$mergedValues = [];
if ($valueT !== '') {
$mergedValues[] = $valueT;
}
if ($valueU !== '') {
$mergedValues[] = $valueU;
}
/*
* Save final value into column T.
*/
$row['data'][$targetColumnIndex] = implode(' ', $mergedValues);
error_log(
"Routine merge T+U: row " .
($row['excelrow'] ?? $rowIndex) .
" generated value in column T: " .
$row['data'][$targetColumnIndex]
);
}
unset($row);
error_log("Routine merge T+U completed.");
}
+90
View File
@@ -0,0 +1,90 @@
<?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;
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
/**
* Routine: paulshark
*
* Purpose:
* For each imported XLS row:
* - read the value from column D
* - read the value from column E
* - read the value from column J
* - merge the values
* - save the final value into column D
*
* Target:
* Column D must be mapped to the destination field in the template mapping.
*/
function applyRoutine(&$excelData, $routineData = [])
{
/*
* Excel column indexes are zero-based:
*
* D = 3
* E = 4
* J = 9
*/
$targetColumnIndex = 3; // D
$columnDIndex = 3; // D
$columnEIndex = 4; // E
$columnJIndex = 9; // J
foreach ($excelData as $rowIndex => &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Routine paulshark: invalid row structure at index {$rowIndex}.");
continue;
}
$valueD = trim((string)($row['data'][$columnDIndex] ?? ''));
$valueE = trim((string)($row['data'][$columnEIndex] ?? ''));
$valueJ = trim((string)($row['data'][$columnJIndex] ?? ''));
/*
* Merge values, ignoring empty values.
*/
$mergedValues = [];
if ($valueD !== '') {
$mergedValues[] = $valueD;
}
if ($valueE !== '') {
$mergedValues[] = $valueE;
}
if ($valueJ !== '') {
$mergedValues[] = $valueJ;
}
/*
* Save final value into column D.
*/
$row['data'][$targetColumnIndex] = implode(' ', $mergedValues);
error_log(
"Routine paulshark: row " .
($row['excelrow'] ?? $rowIndex) .
" generated value in column D: " .
$row['data'][$targetColumnIndex]
);
}
unset($row);
error_log("Routine paulshark completed.");
}
@@ -0,0 +1,71 @@
<?php
/**
* Routine: Richemont Pelletteria
*
* Purpose:
* For each imported XLS row:
* - read the value from column D
* - read the value from column E
* - merge the values
* - save the final value into column D
*
* Target:
* Column D must be mapped to the destination field in the template mapping.
*/
function applyRoutine(&$excelData, $routineData = [])
{
/*
* This routine does not require external routine data.
* Columns are fixed.
*
* Excel column indexes are zero-based:
*
* D = 3
* E = 4
*/
$targetColumnIndex = 3; // D
$columnDIndex = 3; // D
$columnEIndex = 4; // E
foreach ($excelData as $rowIndex => &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Routine Richemont Pelletteria: invalid row structure at index {$rowIndex}.");
continue;
}
$valueD = trim((string)($row['data'][$columnDIndex] ?? ''));
$valueE = trim((string)($row['data'][$columnEIndex] ?? ''));
/*
* Merge values, ignoring empty values.
*/
$mergedValues = [];
if ($valueD !== '') {
$mergedValues[] = $valueD;
}
if ($valueE !== '') {
$mergedValues[] = $valueE;
}
/*
* Save final value into column D.
*/
$row['data'][$targetColumnIndex] = implode(' ', $mergedValues);
error_log(
"Routine Richemont Pelletteria: row " .
($row['excelrow'] ?? $rowIndex) .
" generated value in column D: " .
$row['data'][$targetColumnIndex]
);
}
unset($row);
error_log("Routine Richemont Pelletteria completed.");
}
@@ -0,0 +1,114 @@
<?php
/**
* Routine: build_field_347_from_x_columns
*
* Purpose:
* For each imported XLS row:
* - check columns P to AT
* - when a cell contains "x", take the related column title from row 6
* - append the free text value from column AU
* - save the final comma-separated text into column P
*
* Target:
* Column P must be mapped to field_id 347.
*/
function applyRoutine(&$excelData, $routineData = [])
{
/*
* Excel column indexes are zero-based:
*
* P = 15
* AT = 45
* AU = 46
*/
$targetColumnIndex = 15; // P
$startColumnIndex = 15; // P
$endColumnIndex = 45; // AT
$extraColumnIndex = 46; // AU
/*
* Headers must come from XLS row 6.
* Usually they are passed inside $routineData['xls_headers'].
*/
$headers = $routineData['xls_headers'] ?? [];
if (empty($headers) || !is_array($headers)) {
error_log("Routine field_id 347: missing XLS headers from row 6.");
return;
}
foreach ($excelData as $rowIndex => &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Routine field_id 347: invalid row structure at index {$rowIndex}.");
continue;
}
$selectedValues = [];
/*
* Check columns from P to AT.
* If the cell contains x, take the related column header.
*/
for ($columnIndex = $startColumnIndex; $columnIndex <= $endColumnIndex; $columnIndex++) {
$cellValue = strtolower(trim((string)($row['data'][$columnIndex] ?? '')));
if ($cellValue === 'x') {
$headerTitle = trim((string)($headers[$columnIndex] ?? ''));
if ($headerTitle !== '') {
$selectedValues[] = $headerTitle;
}
}
}
/*
* Add free text from column AU.
*/
$extraText = '';
if (isset($row['data'][$extraColumnIndex])) {
$extraText = trim((string)$row['data'][$extraColumnIndex]);
} elseif (isset($row['data']['AU'])) {
$extraText = trim((string)$row['data']['AU']);
}
error_log(
"Routine field_id 347: row " .
($row['excelrow'] ?? $rowIndex) .
" AU index {$extraColumnIndex} value: " .
print_r($row['data'][$extraColumnIndex] ?? null, true) .
" | AU key value: " .
print_r($row['data']['AU'] ?? null, true)
);
if ($extraText !== '') {
$selectedValues[] = $extraText;
}
/*
* Remove empty and duplicate values.
*/
$selectedValues = array_values(array_unique(array_filter($selectedValues, function ($value) {
return trim((string)$value) !== '';
})));
/*
* Save final value into column P.
* Column P must be mapped to field_id 347 in the template mapping.
*/
$row['data'][$targetColumnIndex] = implode(', ', $selectedValues);
error_log(
"Routine field_id 347: row " .
($row['excelrow'] ?? $rowIndex) .
" generated value: " .
$row['data'][$targetColumnIndex]
);
}
unset($row);
error_log("Routine field_id 347 completed.");
}
+6 -2
View File
@@ -13,7 +13,7 @@ try {
$iddatadb = intval($_POST['iddatadb']);
$idclient = isset($_POST['idclient']) ? (is_numeric($_POST['idclient']) ? intval($_POST['idclient']) : null) : null;
$clienteFornitoreId = isset($_POST['cliente_fornitore_id']) ? (is_numeric($_POST['cliente_fornitore_id']) ? intval($_POST['cliente_fornitore_id']) : null) : null;
$testedComponent = isset($_POST['tested_component']) ? trim((string)$_POST['tested_component']) : null;
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
@@ -144,7 +144,11 @@ try {
$setParts[] = "cliente_fornitore_id = ?";
$params[] = $clienteFornitoreId;
}
// 5a3) tested_component (se presente)
if (isset($testedComponent)) {
$setParts[] = "tested_component = ?";
$params[] = ($testedComponent === '') ? null : $testedComponent;
}
// 5b) fixed fields dal POST
// QUI è il punto chiave: accettiamo SOLO colonne reali (realWhitelist),
// ma se dal JS arrivassero ancora key logiche, funzionerebbe uguale
+42 -14
View File
@@ -16,9 +16,11 @@ if (!$data || !isset($data['id'])) {
exit;
}
$mappingId = $data['id'];
$mappingId = (int)$data['id'];
$mappingType = $data['mapping_type'] ?? '';
$excelColumn = $data['excel_column'] ?? null;
$jsonNode = $data['json_node'] ?? null;
$manualDefault = $data['manual_default'] ?? null;
$autoValue = $data['auto_value'] ?? 'none';
@@ -26,7 +28,7 @@ $tablename = $data['tablename'] ?? '';
try {
// Normalize mapping type
$allowedTypes = ['', 'xls', 'manual', 'auto'];
$allowedTypes = ['', 'xls', 'json', 'manual', 'auto'];
if (!in_array($mappingType, $allowedTypes, true)) {
echo json_encode(["success" => false, "message" => "Invalid mapping_type"]);
exit;
@@ -41,43 +43,67 @@ try {
// Decide what to persist based on mapping_type
$isManual = 0;
$excelToSave = null;
$jsonNodeToSave = null;
$manualToSave = null;
$autoToSave = 'none';
if ($mappingType === 'xls') {
$isManual = 0;
$excelToSave = $excelColumn ?: null;
$jsonNodeToSave = null;
$manualToSave = null;
$autoToSave = 'none';
} elseif ($mappingType === 'json') {
$isManual = 0;
$excelToSave = null;
$jsonNodeToSave = $jsonNode ?: null;
$manualToSave = null;
$autoToSave = 'none';
} elseif ($mappingType === 'manual') {
$isManual = 1;
$excelToSave = null;
$jsonNodeToSave = null;
$manualToSave = $manualDefault;
$autoToSave = 'none';
} elseif ($mappingType === 'auto') {
$isManual = 0;
$excelToSave = null;
$jsonNodeToSave = null;
$manualToSave = null;
$autoToSave = $autoValue ?: 'none';
} else {
// reset
// Reset mapping
$isManual = 0;
$excelToSave = null;
$jsonNodeToSave = null;
$manualToSave = null;
$autoToSave = 'none';
}
$stmt = $pdo->prepare("
UPDATE template_mapping
SET
is_manual = ?,
excel_column = ?,
manual_default = ?,
auto_value = ?
WHERE id = ?
");
UPDATE template_mapping
SET
is_manual = ?,
excel_column = ?,
json_node = ?,
manual_default = ?,
auto_value = ?
WHERE id = ?
");
$result = $stmt->execute([$isManual, $excelToSave, $manualToSave, $autoToSave, $mappingId]);
$result = $stmt->execute([
$isManual,
$excelToSave,
$jsonNodeToSave,
$manualToSave,
$autoToSave,
$mappingId
]);
if (!$result) {
echo json_encode(["success" => false, "message" => "Database update failed"]);
@@ -88,15 +114,17 @@ try {
"success" => true,
"message" => "Mapping updated successfully",
"saved" => [
"id" => (int)$mappingId,
"id" => $mappingId,
"mapping_type" => $mappingType,
"is_manual" => $isManual,
"excel_column" => $excelToSave,
"json_node" => $jsonNodeToSave,
"manual_default" => $manualToSave,
"auto_value" => $autoToSave
]
]);
} catch (Exception $e) {
} catch (Throwable $e) {
echo json_encode(["success" => false, "message" => "Error: " . $e->getMessage()]);
}
exit;
File diff suppressed because one or more lines are too long
+43 -7
View File
@@ -46,8 +46,8 @@
{
"IdSchemaCustomFields": 48,
"ConteggioClienti": 0,
"Nome": "Standard Generico \/ Generic Standard",
"Descrizione": "Schema per tutti i campioni di qualsiasi matrice escluso cuoio\/pelle\r\n\r\n"
"Nome": "Standard \/ Generico",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 49,
@@ -653,7 +653,7 @@
"IdSchemaCustomFields": 163,
"ConteggioClienti": 0,
"Nome": "Devred",
"Descrizione": "Schema creato per cliente DEVRED\r\nGR 18\/03\/2024\r\n\r\n"
"Descrizione": "Schema creato per cliente DEVRED\r\nGR 18\/03\/2024 aggiornato il 23\/04\/2026\r\n\r\n"
},
{
"IdSchemaCustomFields": 164,
@@ -730,8 +730,8 @@
{
"IdSchemaCustomFields": 177,
"ConteggioClienti": 0,
"Nome": "Phoebe philo ACC",
"Descrizione": "(scarpe, borse, cinture, occhiali, gioielleria)\r\n"
"Nome": "Phoebe Philo ",
"Descrizione": "\r\n\r\n"
},
{
"IdSchemaCustomFields": 178,
@@ -760,7 +760,7 @@
{
"IdSchemaCustomFields": 182,
"ConteggioClienti": 0,
"Nome": "Ralph Lauren - All testing V.6",
"Nome": "Ralph Lauren - All testing V.10",
"Descrizione": "AGGIORNAMENTO AL 16\/03\/2026 per estrazione fatturazione"
},
{
@@ -815,7 +815,7 @@
"IdSchemaCustomFields": 191,
"ConteggioClienti": 0,
"Nome": "KIABI",
"Descrizione": "Schema per il cliente Kiabi\r\n"
"Descrizione": "Schema per il cliente Kiabi\r\n\r\n"
},
{
"IdSchemaCustomFields": 192,
@@ -876,6 +876,42 @@
"ConteggioClienti": 0,
"Nome": "ROSSIMODA",
"Descrizione": "Per tutti i campioni di ROSSIMODA\r\n\r\n"
},
{
"IdSchemaCustomFields": 202,
"ConteggioClienti": 0,
"Nome": "LIMS-CIM - MAX MARA",
"Descrizione": "Schema per MAX MARA scambio dati Database"
},
{
"IdSchemaCustomFields": 203,
"ConteggioClienti": 0,
"Nome": "Vince",
"Descrizione": "Schema per tutti i campioni di VINCE\r\n\r\n"
},
{
"IdSchemaCustomFields": 204,
"ConteggioClienti": 0,
"Nome": "Max Mara",
"Descrizione": "Schema da usare per Max Mara\r\n"
},
{
"IdSchemaCustomFields": 205,
"ConteggioClienti": 0,
"Nome": "Chanel Flammability",
"Descrizione": "Schema per Chanel Flammability\r\n"
},
{
"IdSchemaCustomFields": 206,
"ConteggioClienti": 0,
"Nome": "COLDWATER CREEK",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 208,
"ConteggioClienti": 0,
"Nome": "ETON",
"Descrizione": "Schema da usare per ETON\r\n"
}
]
}
+83 -9
View File
@@ -2,7 +2,12 @@
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
include dirname(__DIR__) . '/../extra/auth.php';
if (!Auth::check()) { http_response_code(401); echo json_encode(['error' => 'Unauthorized']); exit; }
if (!Auth::check()) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
@@ -14,44 +19,113 @@ $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'] ?? '');
$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;
}
try {
// Load from cache or API
$cacheFile = __DIR__ . '/cache/clienti.json';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
$data = json_decode(file_get_contents($cacheFile), true);
} else {
$api = VisualLimsApiClient::getInstance();
$params = [
'$select' => 'IdCliente,Nominativo,CodiceCliente',
'$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo',
'$orderby' => 'Nominativo asc'
];
$data = $api->get("Cliente?" . http_build_query($params));
if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true);
if (!is_dir(__DIR__ . '/cache')) {
mkdir(__DIR__ . '/cache', 0777, true);
}
file_put_contents($cacheFile, json_encode($data));
}
$clients = $data['value'] ?? [];
// If requesting by specific ID (for loading selected value)
// 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) {
if ((int)$c['IdCliente'] === $id) {
echo json_encode(['results' => [['id' => $c['IdCliente'], 'text' => trim($c['Nominativo'] ?? '')]]]);
echo json_encode([
'results' => [[
'id' => $c['IdCliente'],
'text' => formatClientLabel($c),
'IdCliente' => $c['IdCliente'],
'Nominativo' => trim($c['Nominativo'] ?? ''),
'CodiceCliente' => trim($c['CodiceCliente'] ?? '')
]]
]);
exit;
}
}
echo json_encode(['results' => []]);
exit;
}
// Search by query
$results = [];
foreach ($clients as $c) {
foreach ($searchClients as $c) {
$name = trim($c['Nominativo'] ?? '');
$code = trim($c['CodiceCliente'] ?? '');
if ($q === '' || mb_strpos(mb_strtolower($name), $q) !== false || mb_strpos(mb_strtolower($code), $q) !== false) {
$results[] = ['id' => $c['IdCliente'], 'text' => $name];
if (count($results) >= $limit) break;
// Cerca solo su ciò che viene visualizzato: Nome - Lettera - ID
$label = formatClientLabel($c);
if (
$q === '' ||
mb_strpos(mb_strtolower($label), $q) !== false
) {
$results[] = [
'id' => $c['IdCliente'],
'text' => $label,
'IdCliente' => $c['IdCliente'],
'Nominativo' => $name,
'CodiceCliente' => $code
];
if (count($results) >= $limit) {
break;
}
}
}
@@ -2,7 +2,11 @@
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
include dirname(__DIR__) . '/../extra/auth.php';
if (!Auth::check()) { http_response_code(401); echo json_encode(['error' => 'Unauthorized']); exit; }
if (!Auth::check()) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
@@ -13,7 +17,8 @@ error_reporting(E_ALL);
$fieldId = intval($_GET['field_id'] ?? 0);
$q = mb_strtolower(trim($_GET['q'] ?? ''));
$id = isset($_GET['id']) ? intval($_GET['id']) : null;
$limit = max(1, min(50, intval($_GET['limit'] ?? 20)));
$rawLimit = intval($_GET['limit'] ?? 20);
$limit = $rawLimit <= 0 ? 0 : max(1, min(500, $rawLimit));
if (!$fieldId) {
echo json_encode(['results' => []]);
@@ -52,7 +57,7 @@ try {
$text = $v['Valore'] ?? '';
if ($q === '' || mb_strpos(mb_strtolower($text), $q) !== false) {
$results[] = ['id' => $v['IdCustomFieldsValue'], 'text' => $text];
if (count($results) >= $limit) break;
if ($limit > 0 && count($results) >= $limit) break;
}
}
+61 -6
View File
@@ -2,7 +2,11 @@
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
include dirname(__DIR__) . '/../extra/auth.php';
if (!Auth::check()) { http_response_code(401); echo json_encode(['error' => 'Unauthorized']); exit; }
if (!Auth::check()) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
header('Content-Type: application/json');
ini_set('display_errors', '0');
@@ -59,15 +63,66 @@ if (isset($_GET['macro_list'])) {
exit;
}
// Search (with optional MacroMatrice filter)
$results = [];
// 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 = [];
foreach ($matrici as $m) {
$nome = $m['NomeMatrice'] ?? '';
if ($macro !== '' && ($m['MacroMatrice'] ?? '') !== $macro) continue;
if ($q === '' || mb_strpos(mb_strtolower($nome), $q) !== false) {
$results[] = ['id' => $m['IdMatrice'], 'text' => $nome];
if (count($results) >= $limit) break;
$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 (!$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]);
+73
View File
@@ -0,0 +1,73 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json; charset=utf-8');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
// Esempio: search_rapporto_min.php?codice=2621716
$codice = trim($_GET['codice'] ?? '');
if ($codice === '') {
throw new Exception("Parametro codice mancante. Usa ?codice=2621716");
}
/*
* Minimal:
* - niente expand
* - max 5 record
* - pochi campi
*
* Se il campo Codice non è quello giusto, sotto proviamo anche CodiceRapporto.
*/
$attempts = [];
$filters = [
'Codice' => "Codice eq '{$codice}'",
'CodiceRapporto' => "CodiceRapporto eq '{$codice}'",
'Numero' => "Numero eq '{$codice}'"
];
foreach ($filters as $label => $filter) {
try {
$data = $api->get('Rapporto', [
'$filter' => $filter,
'$top' => 5,
'$select' => 'IdRapporto,Codice,CodiceRapporto,Numero,Data,Versione,Cliente'
]);
$attempts[$label] = [
'success' => true,
'filter' => $filter,
'records' => isset($data['value']) && is_array($data['value']) ? count($data['value']) : null,
'data' => $data
];
} catch (Exception $e) {
$attempts[$label] = [
'success' => false,
'filter' => $filter,
'error' => $e->getMessage()
];
}
}
echo json_encode([
'success' => true,
'input_codice' => $codice,
'attempts' => $attempts
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+481
View File
@@ -0,0 +1,481 @@
<?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;
}
</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>
<button class="btn btn-primary btn-sm ms-auto" id="btnRefresh">
<i class="bx bx-refresh"></i> Aggiorna
</button>
</div>
</div>
</div>
<!-- ===== KPI ===== -->
<div class="row">
<div class="col-12 col-lg-3 col-md-6">
<div class="card radius-10 stat-card">
<div class="card-body">
<div class="stat-label">Export (status L)</div>
<div class="stat-value text-primary" id="kpiExport">0</div>
</div>
</div>
</div>
<div class="col-12 col-lg-3 col-md-6">
<div class="card radius-10 stat-card">
<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-12 col-lg-3 col-md-6">
<div class="card radius-10 stat-card">
<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-12 col-lg-3 col-md-6">
<div class="card radius-10 stat-card">
<div class="card-body">
<div class="stat-label">Clienti distinti</div>
<div class="stat-value text-warning" id="kpiClienti">0</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>Export</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>Export</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: '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: '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
$('#kpiExport').text(res.totals.export);
$('#kpiClienti').text(res.byClient.length);
$('#kpiLimsSamples').text(res.limsSamples ?? 0);
$('#limsSamplesInput').val(res.limsSamples ?? '');
const limsTot = parseInt(res.limsSamples ?? 0, 10);
if (limsTot > 0) {
const perc = (res.totals.export / limsTot) * 100;
$('#kpiPerc').text(perc.toFixed(1) + '%');
} else {
$('#kpiPerc').text('—');
}
// Torta esportati vs totale
renderPie(res.totals.export, limsTot);
// Grafici
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.tot), 'Export');
chartUtenti = renderBarChart(chartUtenti, 'chartUtenti',
userTop.map(r => r.username ? (r.username + ' (' + r.user_id + ')') : ('ID ' + r.user_id)),
userTop.map(r => r.tot), 'Export');
// 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>
@@ -0,0 +1,90 @@
<?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) ---
$stmtC = $pdo->prepare("
SELECT d.idclient AS idclient,
COUNT(*) AS tot
FROM datadb d
WHERE d.status = 'l'
AND DATE(d.export_date) = :d
GROUP BY d.idclient
ORDER BY tot 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) ---
$stmtU = $pdo->prepare("
SELECT d.user_id AS user_id,
TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS username,
COUNT(*) AS tot
FROM datadb d
LEFT JOIN auth_users u ON u.id = d.user_id
WHERE d.status = 'l'
AND DATE(d.export_date) = :d
GROUP BY d.user_id, username
ORDER BY tot 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'];
$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'];
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'));
// --- 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,
'totals' => ['export' => (int)$totExport],
'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()]);
}
@@ -0,0 +1,36 @@
<?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()]);
}
+57 -12
View File
@@ -181,6 +181,20 @@
<script>
$(document).ready(function() {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cloned') === '1') {
Swal.fire({
title: "Template cloned",
text: "The template was cloned successfully.",
icon: "success",
confirmButtonText: "OK"
});
const cleanUrl = window.location.pathname;
window.history.replaceState({}, document.title, cleanUrl);
}
$('#xlsTemplatesTable').DataTable({
processing: true,
serverSide: false,
@@ -195,18 +209,24 @@
className: "table-actions text-center",
render: function(data, type, row) {
return `
<div class="d-flex justify-content-center gap-1">
<a href="edit_template_xls.php?id=${data}" class="btn btn-sm btn-primary" title="Edit">
<i class="bx bx-edit-alt"></i>
</a>
<a href="mapping_template_xls_scheme2.php?id=${data}" class="btn btn-sm btn-success" title="Mapping">
<i class="bx bx-link-alt"></i>
</a>
<button class="btn btn-sm btn-danger" onclick="confirmDelete(${data})" title="Delete">
<i class="bx bx-trash"></i>
</button>
</div>
`;
<div class="d-flex justify-content-center gap-1">
<a href="edit_template_xls.php?id=${data}" class="btn btn-sm btn-primary" title="Edit">
<i class="bx bx-edit-alt"></i>
</a>
<a href="mapping_template_xls_scheme2.php?id=${data}" class="btn btn-sm btn-success" title="Mapping">
<i class="bx bx-link-alt"></i>
</a>
<button class="btn btn-sm btn-warning" onclick="confirmClone(${data}, '${String(row.name || '').replace(/'/g, "\\'")}')" title="Clone">
<i class="bx bx-copy"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="confirmDelete(${data})" title="Delete">
<i class="bx bx-trash"></i>
</button>
</div>
`;
}
},
{
@@ -318,6 +338,31 @@
});
}
function confirmClone(id, templateName) {
Swal.fire({
title: "Clone template?",
html: `
<div class="text-start">
<p class="mb-2">You are about to clone this template:</p>
<strong>${templateName}</strong>
<p class="mt-3 mb-0 text-muted">
The new template will be created as <strong>Copia di ${templateName}</strong> and all mappings will be copied.
</p>
</div>
`,
icon: "question",
showCancelButton: true,
confirmButtonColor: "#ffc107",
cancelButtonColor: "#6c757d",
confirmButtonText: "Yes, clone it",
cancelButtonText: "Cancel"
}).then((result) => {
if (result.isConfirmed) {
window.location.href = `clone_template.php?id=${id}`;
}
});
}
$(document).on("change", ".toggle-status", function() {
let templateId = $(this).data("id");
let newStatus = $(this).is(":checked") ? "active" : "inactive";
+3
View File
@@ -8,6 +8,9 @@ ini_set('log_errors', 1);
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
Dotenv\Dotenv::createImmutable(dirname(__DIR__, 2))->safeLoad();
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
header('Content-Type: application/json');
try {
+117
View File
@@ -0,0 +1,117 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
ini_set('display_errors', '1');
error_reporting(E_ALL);
set_time_limit(60);
/**
* Uso: get_pdf.php?id=591749
*/
try {
$api = VisualLimsApiClient::getInstance();
$idRapportoFile = isset($_GET['id']) ? intval($_GET['id']) : 0;
if (!$idRapportoFile) {
throw new Exception("Parametro id mancante. Usa ?id=591749");
}
// STEP 1: leggi metadati entita
$entityData = $api->get("RapportoFile(" . $idRapportoFile . ")");
$pdfBody = null;
$strategyUsed = null;
$debugLog = array();
// Strategia A - campo FileContent base64
if (!empty($entityData['FileContent'])) {
$decoded = base64_decode($entityData['FileContent'], true);
if ($decoded !== false && substr($decoded, 0, 4) === '%PDF') {
$pdfBody = $decoded;
$strategyUsed = 'A: FileContent base64';
}
}
// Strategia B - campo Contenuto base64
if (!$pdfBody && !empty($entityData['Contenuto'])) {
$decoded = base64_decode($entityData['Contenuto'], true);
if ($decoded !== false && substr($decoded, 0, 4) === '%PDF') {
$pdfBody = $decoded;
$strategyUsed = 'B: Contenuto base64';
}
}
// Recupera token e baseUrl dalla classe
$token = $api->getToken();
$baseUrl = rtrim($api->getBaseUrl(), '/');
$rawEndpoints = array(
'C: $value' => $baseUrl . '/api/odata/RapportoFile(' . $idRapportoFile . ')/$value',
'D: FileContent/$value' => $baseUrl . '/api/odata/RapportoFile(' . $idRapportoFile . ')/FileContent/$value',
'E: Contenuto/$value' => $baseUrl . '/api/odata/RapportoFile(' . $idRapportoFile . ')/Contenuto/$value',
);
foreach ($rawEndpoints as $label => $url) {
if ($pdfBody) break;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Authorization: Bearer ' . $token,
'Accept: application/pdf, application/octet-stream, */*',
));
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$body = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$ct = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
curl_close($ch);
$debugLog[$label] = array(
'url' => $url,
'http_code' => $httpCode,
'content_type' => $ct,
'body_start' => substr((string)$body, 0, 300),
);
if ($httpCode === 200 && is_string($body) && substr($body, 0, 4) === '%PDF') {
$pdfBody = $body;
$strategyUsed = $label;
}
}
// RISPOSTA
if ($pdfBody) {
$fileName = isset($entityData['FileName']) ? $entityData['FileName'] : 'rapporto_' . $idRapportoFile . '.pdf';
header('Content-Type: application/pdf');
header('Content-Disposition: inline; filename="' . $fileName . '"');
header('Content-Length: ' . strlen($pdfBody));
header('Cache-Control: private, max-age=0, must-revalidate');
echo $pdfBody;
exit;
}
// Nessuna strategia ha funzionato
header('Content-Type: application/json; charset=utf-8');
echo json_encode(array(
'success' => false,
'message' => 'Nessuna strategia ha restituito un PDF valido.',
'id_rapporto_file' => $idRapportoFile,
'entity_fields' => array_keys($entityData ? $entityData : array()),
'entity_data' => $entityData,
'strategies_debug' => $debugLog,
), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(array(
'success' => false,
'error' => $e->getMessage(),
), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
@@ -0,0 +1,53 @@
<?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);
+45 -4
View File
@@ -1,10 +1,6 @@
<?php
include('include/headscript.php');
// ✅ FIX timezone (Rome)
ini_set('date.timezone', 'Europe/Rome');
date_default_timezone_set('Europe/Rome');
$template_id = intval($_GET['id'] ?? 0);
if (!$template_id) {
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Template ID mancante"));
@@ -2016,6 +2012,51 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
el.addEventListener('change', () => el.classList.toggle('has-value', el.value.trim() !== ''));
});
})();
// ── Column resize ──
(function() {
const resizers = document.querySelectorAll('.resizer');
let currentResizer = null, startX = 0, startWidth = 0, columnIndex = null;
function resize(e) {
if (!currentResizer || columnIndex === null) return;
const newWidth = Math.max(80, startWidth + (e.pageX - startX));
const sel = '[data-index="' + columnIndex + '"]';
const header = document.querySelector('.grid-header' + sel);
if (header) header.style.flex = '0 0 ' + newWidth + 'px';
const topCell = document.querySelector('.grid-top .grid-cell' + sel);
if (topCell) topCell.style.flex = '0 0 ' + newWidth + 'px';
document.querySelectorAll('.grid-row .grid-cell' + sel).forEach(function(cell) {
cell.style.flex = '0 0 ' + newWidth + 'px';
});
var filterCell = document.querySelector('#gridFilterRow > .grid-cell:nth-child(' + (parseInt(columnIndex, 10) + 1) + ')');
if (filterCell) filterCell.style.flex = '0 0 ' + newWidth + 'px';
}
function stopResize() {
if (currentResizer) {
document.removeEventListener('mousemove', resize);
document.removeEventListener('mouseup', stopResize);
currentResizer = null;
columnIndex = null;
}
}
resizers.forEach(function(resizer) {
resizer.addEventListener('mousedown', function(e) {
e.preventDefault();
e.stopPropagation();
currentResizer = this;
var header = this.closest('.grid-header');
if (!header) return;
columnIndex = header.getAttribute('data-index');
startX = e.pageX;
startWidth = header.offsetWidth;
document.addEventListener('mousemove', resize);
document.addEventListener('mouseup', stopResize);
});
});
})();
</script>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
<?php
require_once "class/VisualLimsApiClient.class.php";
include('include/headscript.php');
header("Content-Type: application/json; charset=utf-8");
try {
$api = VisualLimsApiClient::getInstance();
$commessaId = 577818;
$endpoint = "CommessaWeb({$commessaId})?\$expand=CommesseCustomFields(\$expand=CustomField)";
$result = $api->get($endpoint);
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
"success" => false,
"error" => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
@@ -0,0 +1,107 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
require_once dirname(__FILE__) . '/include/headscript.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$api = VisualLimsApiClient::getInstance();
// Get all schemas currently used in template_mapping
$stmtSchemas = $pdo->query("
SELECT DISTINCT schema_id
FROM template_mapping
WHERE schema_id IS NOT NULL
AND schema_id > 0
ORDER BY schema_id ASC
");
$schemaIds = $stmtSchemas->fetchAll(PDO::FETCH_COLUMN);
if (empty($schemaIds)) {
throw new Exception('No schema_id found in template_mapping');
}
$stmtUpdate = $pdo->prepare("
UPDATE template_mapping
SET field_order = ?
WHERE schema_id = ?
AND field_id = ?
");
$summary = [];
$totalUpdated = 0;
foreach ($schemaIds as $schemaId) {
$schemaId = (int)$schemaId;
$endpoint = "SchemaCustomField($schemaId)?\$expand=SchemiCustomFieldsDettagli(\$expand=CustomField)";
$data = $api->get($endpoint);
if (empty($data['SchemiCustomFieldsDettagli']) || !is_array($data['SchemiCustomFieldsDettagli'])) {
$summary[] = [
'schema_id' => $schemaId,
'success' => false,
'message' => 'No SchemiCustomFieldsDettagli found'
];
continue;
}
$schemaUpdated = 0;
$notFound = [];
foreach ($data['SchemiCustomFieldsDettagli'] as $detail) {
$order = intval($detail['Ordine'] ?? 9999);
$fieldId = intval($detail['CustomField']['IdCustomField'] ?? 0);
if ($fieldId <= 0) {
continue;
}
$stmtUpdate->execute([
$order,
$schemaId,
$fieldId
]);
if ($stmtUpdate->rowCount() > 0) {
$schemaUpdated++;
$totalUpdated++;
} else {
$notFound[] = [
'field_id' => $fieldId,
'order' => $order,
'label' => $detail['CustomField']['Titolo'] ?? ''
];
}
}
$summary[] = [
'schema_id' => $schemaId,
'success' => true,
'updated' => $schemaUpdated,
'not_found_count' => count($notFound),
'not_found' => $notFound
];
}
echo json_encode([
'success' => true,
'schemas_processed' => count($schemaIds),
'total_updated' => $totalUpdated,
'summary' => $summary
], JSON_PRETTY_PRINT);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => $e->getMessage()
], JSON_PRETTY_PRINT);
}
+63
View File
@@ -0,0 +1,63 @@
<?php
include('include/headscript.php');
header('Content-Type: application/json');
try {
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
throw new Exception('Invalid JSON payload');
}
$templateId = isset($input['template_id']) ? (int)$input['template_id'] : 0;
$apiSampleJson = $input['api_sample_json'] ?? '';
$jsonNodes = $input['json_nodes'] ?? '';
if ($templateId <= 0) {
throw new Exception('Invalid template ID');
}
if (trim($apiSampleJson) === '') {
throw new Exception('API sample JSON is empty');
}
json_decode($apiSampleJson, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception('Invalid API sample JSON: ' . json_last_error_msg());
}
$decodedNodes = json_decode($jsonNodes, true);
if (!is_array($decodedNodes)) {
throw new Exception('Invalid JSON nodes array');
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("
UPDATE excel_templates
SET
api_sample_json = ?,
json_nodes = ?
WHERE id = ?
");
$stmt->execute([
$apiSampleJson,
json_encode($decodedNodes, JSON_UNESCAPED_UNICODE),
$templateId
]);
echo json_encode([
'success' => true,
'nodes_count' => count($decodedNodes)
]);
} catch (Throwable $e) {
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
+46 -14
View File
@@ -3,14 +3,18 @@ ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
include('include/headscript.php'); // Assumi che questo includa la connessione DB
include('include/headscript.php');
header('Content-Type: application/json');
// Recupera il payload JSON
$data = json_decode(file_get_contents('php://input'), true);
$template_id = intval($data['template_id']);
$mapping_id = intval($data['mapping_id']);
$value = intval($data['value']);
$template_id = intval($data['template_id'] ?? 0);
$mapping_id = intval($data['mapping_id'] ?? 0);
$value = intval($data['value'] ?? 0);
// IMPORTANT: main_field may be ENUM('0','1'), so use string values
$mainValue = ($value === 1) ? '1' : '0';
if ($template_id <= 0 || $mapping_id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid IDs']);
@@ -23,19 +27,47 @@ $pdo = $db->getConnection();
try {
$pdo->beginTransaction();
if ($value === 1) {
// Setta tutti main_field a 0 per questo template
$stmt = $pdo->prepare("UPDATE template_mapping SET main_field = 0 WHERE template_id = ?");
$stmt->execute([$template_id]);
if ($mainValue === '1') {
$stmt = $pdo->prepare("
SELECT COUNT(*)
FROM template_mapping
WHERE template_id = ?
AND main_field = '1'
AND id <> ?
");
$stmt->execute([$template_id, $mapping_id]);
$currentMainCount = (int)$stmt->fetchColumn();
if ($currentMainCount >= 2) {
$pdo->rollBack();
echo json_encode([
'success' => false,
'message' => 'Maximum 2 Main fields allowed',
'currentMainCount' => $currentMainCount
]);
exit;
}
}
// Setta il valore per questo mapping
$stmt = $pdo->prepare("UPDATE template_mapping SET main_field = ? WHERE id = ? AND template_id = ?");
$stmt->execute([$value, $mapping_id, $template_id]);
$stmt = $pdo->prepare("
UPDATE template_mapping
SET main_field = ?
WHERE id = ?
AND template_id = ?
");
$stmt->execute([$mainValue, $mapping_id, $template_id]);
$pdo->commit();
echo json_encode(['success' => true]);
} catch (Exception $e) {
$pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
+108
View File
@@ -0,0 +1,108 @@
<?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;
+183 -53
View File
@@ -2,7 +2,6 @@
header('Content-Type: application/json');
require_once 'class/db-functions.php';
// Disabilita l'output di errori HTML
ini_set('display_errors', '0');
error_reporting(E_ALL);
@@ -13,108 +12,239 @@ try {
throw new Exception("Invalid request method.");
}
// Recupera i dati dal body JSON
$input = json_decode(file_get_contents('php://input'), true);
$template_id = isset($input['template_id']) ? intval($input['template_id']) : null;
$schemajson = isset($input['schemajson']) ? trim($input['schemajson']) : '';
// Controllo sui campi obbligatori
$template_id = isset($input['template_id']) ? intval($input['template_id']) : null;
$schemajson = isset($input['schemajson']) ? trim($input['schemajson']) : '';
if (empty($template_id) || empty($schemajson)) {
throw new Exception("All fields marked with * are required, including schemajson.");
}
// Validazione del JSON
$decoded_json = json_decode($schemajson, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception("Invalid JSON format for schemajson.");
}
// Connessione al database
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Recupera il target_table e verifica l'esistenza del template
$stmt = $pdo->prepare("SELECT target_table FROM excel_templates WHERE id = ?");
$stmt->execute([$template_id]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$template) {
throw new Exception("Template not found.");
}
$target_table = $template['target_table'];
// Inizia una transazione
$pdo->beginTransaction();
// Aggiorna il schemajson in excel_templates
$stmt = $pdo->prepare("UPDATE excel_templates
SET schemajson = ?, updated_at = NOW()
WHERE id = ?");
$stmt->execute([$schemajson, $template_id]);
// Estrai i campi dallo schema
$schema_id = $decoded_json['IdSchemaCustomFields'] ?? null;
$schema_fields = $decoded_json['SchemiCustomFieldsDettagli'] ?? [];
if (empty($schema_id)) {
throw new Exception("Schema ID not found in schemajson.");
}
if (empty($schema_fields)) {
throw new Exception("No fields found in schema.");
}
// Prepara la query per l'inserimento condizionato in template_mapping
$insert_stmt = $pdo->prepare("
$pdo->beginTransaction();
/*
* 1) Update raw schema JSON in excel_templates
*/
$stmt = $pdo->prepare("
UPDATE excel_templates
SET schemajson = ?, updated_at = NOW()
WHERE id = ?
");
$stmt->execute([$schemajson, $template_id]);
/*
* Prepare reusable statements
*/
// Check if mapping already exists
$checkStmt = $pdo->prepare("
SELECT id, data_type
FROM template_mapping
WHERE template_id = ?
AND field_id = ?
LIMIT 1
");
// Insert new field
$insertStmt = $pdo->prepare("
INSERT INTO template_mapping (
template_id, schema_id, field_id, data_type, is_required, default_value,
has_list, length, decimals, min_value, max_value, default_curr_date,
tablename, field_label
)
SELECT :template_id, :schema_id, :field_id, :data_type, :is_required, :default_value,
:has_list, :length, :decimals, :min_value, :max_value, :default_curr_date,
:tablename, :field_label
WHERE NOT EXISTS (
SELECT 1 FROM template_mapping
WHERE template_id = :template_id_check AND field_id = :field_id_check
template_id,
schema_id,
field_id,
field_order,
data_type,
is_required,
default_value,
has_list,
length,
decimals,
min_value,
max_value,
default_curr_date,
tablename,
field_label
) VALUES (
:template_id,
:schema_id,
:field_id,
:field_order,
:data_type,
:is_required,
:default_value,
:has_list,
:length,
:decimals,
:min_value,
:max_value,
:default_curr_date,
:tablename,
:field_label
)
");
// Itera sui campi dello schema e inserisci quelli mancanti
// Update existing field WITHOUT clearing manual_default
$updateSameTypeStmt = $pdo->prepare("
UPDATE template_mapping
SET
schema_id = :schema_id,
field_order = :field_order,
data_type = :data_type,
is_required = :is_required,
default_value = :default_value,
has_list = :has_list,
length = :length,
decimals = :decimals,
min_value = :min_value,
max_value = :max_value,
default_curr_date = :default_curr_date,
tablename = :tablename,
field_label = :field_label
WHERE id = :id
");
// Update existing field AND clear user-entered value if type changed
$updateChangedTypeStmt = $pdo->prepare("
UPDATE template_mapping
SET
schema_id = :schema_id,
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,
manual_default = NULL,
auto_value = 'none'
WHERE id = :id
");
$currentFieldIds = [];
/*
* 2) Insert new fields and update existing fields
*/
foreach ($schema_fields as $field) {
$custom_field = $field['CustomField'] ?? [];
if (empty($custom_field['IdCustomField'])) {
continue; // Salta se manca l'ID del campo
continue;
}
$insert_stmt->execute([
':template_id' => $template_id,
':schema_id' => $schema_id,
':field_id' => $custom_field['IdCustomField'],
':data_type' => $custom_field['Tipo'] ?? 'Testo',
':is_required' => $custom_field['ObbligatorioWeb'] ? 1 : 0,
':default_value' => $custom_field['ValoreDefault'] ?? null,
':has_list' => $custom_field['Elenco'] ? 1 : 0,
':length' => $custom_field['Lunghezza'] ?? 0,
':decimals' => $custom_field['Decimali'] ?? 0,
':min_value' => $custom_field['Minimo'] ?? null,
':max_value' => $custom_field['Massimo'] ?? null,
':default_curr_date' => $custom_field['DefaultCurrDate'] ? 1 : 0,
':tablename' => $target_table,
':field_label' => $custom_field['TitoloTraduzione'] ?? '',
':template_id_check' => $template_id,
':field_id_check' => $custom_field['IdCustomField']
]);
$fieldId = (int)$custom_field['IdCustomField'];
$currentFieldIds[] = $fieldId;
$newDataType = $custom_field['Tipo'] ?? 'Testo';
$data = [
':schema_id' => $schema_id,
':field_order' => (int)($field['Ordine'] ?? 9999),
':data_type' => $newDataType,
':is_required' => !empty($custom_field['ObbligatorioWeb']) ? 1 : 0,
':default_value' => $custom_field['ValoreDefault'] ?? null,
':has_list' => !empty($custom_field['Elenco']) ? 1 : 0,
':length' => $custom_field['Lunghezza'] ?? 0,
':decimals' => $custom_field['Decimali'] ?? 0,
':min_value' => $custom_field['Minimo'] ?? null,
':max_value' => $custom_field['Massimo'] ?? null,
':default_curr_date' => !empty($custom_field['DefaultCurrDate']) ? 1 : 0,
':tablename' => $target_table,
':field_label' => $custom_field['TitoloTraduzione'] ?? ''
];
$checkStmt->execute([$template_id, $fieldId]);
$existing = $checkStmt->fetch(PDO::FETCH_ASSOC);
if (!$existing) {
// New field: insert it
$insertData = $data;
$insertData[':template_id'] = $template_id;
$insertData[':field_id'] = $fieldId;
$insertStmt->execute($insertData);
} else {
// Existing field: update schema details
$oldDataType = $existing['data_type'] ?? '';
$updateData = $data;
$updateData[':id'] = (int)$existing['id'];
if ($oldDataType !== $newDataType) {
// Type changed: clear manual value because it may no longer be valid
$updateChangedTypeStmt->execute($updateData);
} else {
// Same type: keep manual_default, excel/json mapping, visibility, main field, etc.
$updateSameTypeStmt->execute($updateData);
}
}
}
/*
* 3) Remove fields no longer present in the schema
*/
$currentFieldIds = array_values(array_unique(array_filter($currentFieldIds)));
if (!empty($currentFieldIds)) {
$placeholders = implode(',', array_fill(0, count($currentFieldIds), '?'));
$deleteParams = array_merge([$template_id], $currentFieldIds);
$deleteStmt = $pdo->prepare("
DELETE FROM template_mapping
WHERE template_id = ?
AND field_id NOT IN ($placeholders)
");
$deleteStmt->execute($deleteParams);
}
// Commit della transazione
$pdo->commit();
$response["success"] = true;
$response["message"] = "Schema JSON updated and template mappings created successfully!";
$response["message"] = "Schema JSON updated, mappings synchronized, removed fields deleted, and changed fields updated successfully.";
} catch (Exception $e) {
if (isset($pdo) && $pdo->inTransaction()) {
$pdo->rollback();
}
$response["message"] = $e->getMessage();
}
// Restituisce un JSON per il fetch
echo json_encode($response);
@@ -0,0 +1,79 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
require_once dirname(__FILE__) . '/include/headscript.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$schemaId = isset($_GET['schema_id']) && is_numeric($_GET['schema_id'])
? intval($_GET['schema_id'])
: 0;
if ($schemaId <= 0) {
throw new Exception('Missing or invalid schema_id');
}
$api = VisualLimsApiClient::getInstance();
$endpoint = "SchemaCustomField($schemaId)?\$expand=SchemiCustomFieldsDettagli(\$expand=CustomField)";
$data = $api->get($endpoint);
if (empty($data['SchemiCustomFieldsDettagli']) || !is_array($data['SchemiCustomFieldsDettagli'])) {
throw new Exception('No SchemiCustomFieldsDettagli found');
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$updated = 0;
$notFound = [];
$stmt = $pdo->prepare("
UPDATE template_mapping
SET field_order = ?
WHERE schema_id = ?
AND field_id = ?
");
foreach ($data['SchemiCustomFieldsDettagli'] as $detail) {
$order = intval($detail['Ordine'] ?? 9999);
$fieldId = intval($detail['CustomField']['IdCustomField'] ?? 0);
if ($fieldId <= 0) {
continue;
}
$stmt->execute([
$order,
$schemaId,
$fieldId
]);
if ($stmt->rowCount() > 0) {
$updated++;
} else {
$notFound[] = [
'field_id' => $fieldId,
'order' => $order,
'label' => $detail['CustomField']['Titolo'] ?? ''
];
}
}
echo json_encode([
'success' => true,
'schema_id' => $schemaId,
'updated' => $updated,
'not_found' => $notFound
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
+68 -16
View File
@@ -6,22 +6,57 @@ error_reporting(E_ALL);
require_once(__DIR__ . '/class/db-functions.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$data = json_decode(file_get_contents("php://input"), true);
if (!$data || !isset($data['template_id'], $data['xls_headers'])) {
echo json_encode(["success" => false, "message" => "Invalid or missing data"]);
exit;
}
$templateId = $data['template_id'];
$xlsHeaders = $data['xls_headers'];
$headerRow = isset($data['header_row']) ? (int)$data['header_row'] : null;
$startColumn = isset($data['start_column']) ? (int)$data['start_column'] : null;
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$data = json_decode(file_get_contents("php://input"), true);
if (!$data || !isset($data['template_id'], $data['xls_headers'])) {
echo json_encode(["success" => false, "message" => "Invalid or missing data"]);
exit;
}
$templateId = (int)$data['template_id'];
$xlsHeaders = $data['xls_headers'];
$headerRow = isset($data['header_row']) && $data['header_row'] !== ''
? (int)$data['header_row']
: null;
$startColumn = isset($data['start_column']) && $data['start_column'] !== ''
? (int)$data['start_column']
: null;
$xlsSheetIndex = isset($data['xls_sheet_index']) && $data['xls_sheet_index'] !== ''
? (int)$data['xls_sheet_index']
: null;
if ($templateId <= 0) {
echo json_encode(["success" => false, "message" => "Invalid template ID"]);
exit;
}
if ($xlsHeaders === '') {
echo json_encode(["success" => false, "message" => "XLS headers cannot be empty"]);
exit;
}
if ($headerRow !== null && $headerRow <= 0) {
echo json_encode(["success" => false, "message" => "Header row must be greater than 0"]);
exit;
}
if ($startColumn !== null && $startColumn <= 0) {
echo json_encode(["success" => false, "message" => "Start column must be greater than 0"]);
exit;
}
if ($xlsSheetIndex !== null && $xlsSheetIndex < 0) {
echo json_encode(["success" => false, "message" => "XLS sheet number cannot be negative"]);
exit;
}
$sql = "UPDATE excel_templates SET xls_headers = ?";
$params = [$xlsHeaders];
@@ -29,11 +64,18 @@ try {
$sql .= ", header_row = ?";
$params[] = $headerRow;
}
if ($startColumn !== null) {
$sql .= ", start_column = ?";
$params[] = $startColumn;
}
if ($xlsSheetIndex !== null) {
$sql .= ", xls_sheet_index = ?";
$params[] = $xlsSheetIndex;
}
$sql .= ", updated_at = NOW()";
$sql .= " WHERE id = ?";
$params[] = $templateId;
@@ -45,8 +87,18 @@ try {
exit;
}
echo json_encode(["success" => true, "message" => "XLS headers saved successfully"]);
echo json_encode([
"success" => true,
"message" => "XLS headers saved successfully",
"debug" => [
"template_id" => $templateId,
"header_row" => $headerRow,
"start_column" => $startColumn,
"xls_sheet_index" => $xlsSheetIndex
]
]);
} catch (Exception $e) {
echo json_encode(["success" => false, "message" => "Error: " . $e->getMessage()]);
}
exit;
File diff suppressed because it is too large Load Diff
+618 -37
View File
@@ -8,8 +8,71 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--favicon-->
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>xxx - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> - User Profile</title>
<!-- Select2 CSS -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<title>SmartTRF - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> - User Profile</title>
<style>
.profile-avatar-preview {
max-width: 100px;
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 50%;
border: 1px solid #e5e7eb;
background: #f8f9fa;
}
.small-muted {
font-size: 12px;
color: #6c757d;
}
.select2-container {
width: 100% !important;
}
.lims-loading {
font-size: 12px;
color: #6c757d;
}
.password-note {
background: #f8fafc;
border: 1px solid #e5e7eb;
color: #475569;
border-radius: 10px;
padding: 10px 12px;
font-size: 13px;
line-height: 1.5;
}
.password-error {
display: none;
background: #fff1f2;
border: 1px solid #fecdd3;
color: #be123c;
border-radius: 10px;
padding: 10px 12px;
font-size: 13px;
line-height: 1.5;
}
.password-success {
display: none;
background: #f0fdf4;
border: 1px solid #bbf7d0;
color: #166534;
border-radius: 10px;
padding: 10px 12px;
font-size: 13px;
line-height: 1.5;
}
</style>
</head>
<body>
@@ -18,9 +81,11 @@
<!--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">
@@ -31,61 +96,195 @@
<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">
<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>
<form action="update-profile.php" method="POST" enctype="multipart/form-data" id="profileForm">
<div class="row g-3">
<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="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="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="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>
<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-->
@@ -99,7 +298,389 @@
<?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>
+128 -5
View File
@@ -1,4 +1,5 @@
<?php
/**
* Validate rows before export to LIMS.
*
@@ -88,6 +89,61 @@ $validators[] = function (int $iddatadb, array $ctx): array {
return [];
};
// 3. All LIMS-mandatory fields must be filled.
$validators[] = function (int $iddatadb, array $ctx): array {
$record = $ctx['record'] ?? null;
if (!$record) {
return [];
}
$errors = [];
// Fixed fields (stored as columns in datadb)
foreach (($ctx['requiredFixed'] ?? []) as $key => $label) {
$col = $ctx['fixedAliasMap'][$key] ?? null;
$val = $col !== null ? ($record[$col] ?? null) : null;
if ($val === null || $val === '' || (int) $val === 0) {
$errors[] = [
'field' => $key,
'message' => $label . ' è obbligatorio.',
];
}
}
// Custom fields (values stored in import_data_details, keyed by mapping_id)
foreach (($ctx['requiredCustom'] ?? []) as $cf) {
$val = $ctx['customValues'][(int) $cf['mapping_id']] ?? null;
if ($val === null || trim((string) $val) === '') {
$errors[] = [
'field' => 'field_label:' . $cf['field_label'],
'message' => rtrim($cf['field_label'], ': ') . ' è obbligatorio.',
];
}
}
return $errors;
};
// Logical fixed_field_key - real datadb column (mirrors imported.php $fixedAliasMap)
$fixedAliasMap = [
'ClienteResponsabile' => 'cliente_responsabile_id',
'ClienteFornitore' => 'cliente_fornitore_id',
'ClienteAnalisi' => 'clienteAnalisi',
'ClienteFatturazione' => 'ClienteFatturazione',
'MoltiplicatorePrezzo' => 'moltiplicatore_prezzo_id',
'AnagraficaCertestObject' => 'anagrafica_certest_object_id',
'AnagraficaCertestService' => 'anagrafica_certest_service_id',
'ConsegnaRichiesta' => 'consegna_richiesta',
];
// Fixed keys NOT enforced by the generic mandatory check above:
// - ConsegnaRichiesta: handled by its dedicated validator (also checks the date)
// - ClienteFornitore: 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'];
// ── Main ────────────────────────────────────────────────────────────────────
try {
@@ -104,9 +160,12 @@ try {
$iddatadbList = array_column($rows, 'iddatadb');
$placeholders = implode(',', array_fill(0, count($iddatadbList), '?'));
// Records (datadb) for fixed field validation
// Records (datadb) — templateid + fixed-field columns for mandatory validation
$stmt = $pdo->prepare("
SELECT iddatadb, consegna_richiesta
SELECT iddatadb, templateid, consegna_richiesta,
cliente_responsabile_id, moltiplicatore_prezzo_id,
anagrafica_certest_object_id, anagrafica_certest_service_id,
cliente_fornitore_id, clienteAnalisi, ClienteFatturazione
FROM datadb
WHERE iddatadb IN ($placeholders)
");
@@ -121,6 +180,8 @@ 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 = [];
@@ -128,6 +189,63 @@ try {
$partsInfo[(int)$r['iddatadb']][] = $r;
}
// Mandatory-field config per template
$templateIds = array_values(array_unique(array_filter(array_map(
fn($r) => (int)($r['templateid'] ?? 0),
$recordsInfo
))));
$requiredFixedByTemplate = []; // template_id => [ fixed_field_key => label ]
$requiredCustomByTemplate = []; // template_id => [ { mapping_id, field_label }, ... ]
if (!empty($templateIds)) {
$tplPlaceholders = implode(',', array_fill(0, count($templateIds), '?'));
// Required fixed fields (is_required synced from LIMS ObbligatorioWeb)
$stmt = $pdo->prepare("
SELECT template_id, fixed_field_key
FROM template_fixed_mapping
WHERE template_id IN ($tplPlaceholders) AND is_required = 1
");
$stmt->execute($templateIds);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
$key = $r['fixed_field_key'];
if (in_array($key, $skipRequiredFixed, true)) {
continue;
}
$requiredFixedByTemplate[(int)$r['template_id']][$key] = $key;
}
// Required custom fields that are visible in the import grid (excluding filed_id = 189 Tested component)
$stmt = $pdo->prepare("
SELECT id AS mapping_id, template_id, field_label
FROM template_mapping
WHERE template_id IN ($tplPlaceholders)
AND is_required = 1
AND is_visible_import = 1
AND id <> 189
");
$stmt->execute($templateIds);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
$requiredCustomByTemplate[(int)$r['template_id']][] = [
'mapping_id' => (int)$r['mapping_id'],
'field_label' => $r['field_label'],
];
}
}
// Custom field values per record (import_data_details.id is the FK to datadb)
$stmt = $pdo->prepare("
SELECT id AS iddatadb, mapping_id, field_value
FROM import_data_details
WHERE id IN ($placeholders)
");
$stmt->execute($iddatadbList);
$customValuesByRecord = []; // iddatadb => [ mapping_id => field_value ]
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
$customValuesByRecord[(int)$r['iddatadb']][(int)$r['mapping_id']] = $r['field_value'];
}
// ── Run validators per row ──────────────────────────────────────────────
$results = [];
@@ -137,9 +255,15 @@ try {
$index = $rowInfo['index'];
// Build context for validators
$record = $recordsInfo[$iddatadb] ?? null;
$templateId = (int)($record['templateid'] ?? 0);
$ctx = [
'record' => $recordsInfo[$iddatadb] ?? null,
'parts' => $partsInfo[$iddatadb] ?? [],
'record' => $record,
'parts' => $partsInfo[$iddatadb] ?? [],
'fixedAliasMap' => $fixedAliasMap,
'requiredFixed' => $requiredFixedByTemplate[$templateId] ?? [],
'requiredCustom' => $requiredCustomByTemplate[$templateId] ?? [],
'customValues' => $customValuesByRecord[$iddatadb] ?? [],
];
$errors = [];
@@ -155,7 +279,6 @@ 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()]);
+23 -3
View File
@@ -26,7 +26,8 @@ $cacheDir = __DIR__ . '/cache';
if (!is_dir($cacheDir)) mkdir($cacheDir, 0777, true);
$log = [];
function warmLog(string $msg, bool $isCli) {
function warmLog(string $msg, bool $isCli)
{
global $log;
$line = date('H:i:s') . " $msg";
$log[] = $line;
@@ -39,7 +40,7 @@ try {
// 1. Clients
warmLog('[clients] Fetching...', $isCli);
$params = ['$select' => 'IdCliente,Nominativo,CodiceCliente', '$orderby' => 'Nominativo asc'];
$params = ['$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo', '$orderby' => 'Nominativo asc'];
$clientData = $api->get("Cliente?" . http_build_query($params));
file_put_contents($cacheDir . '/clienti.json', json_encode($clientData));
$clientCount = count($clientData['value'] ?? []);
@@ -112,9 +113,28 @@ 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).
$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);
}
}
$elapsed = round(microtime(true) - $startTime, 1);
warmLog("Done in {$elapsed}s", $isCli);
} catch (Exception $e) {
warmLog("FATAL: " . $e->getMessage(), $isCli);
}
+1 -4
View File
@@ -14,10 +14,7 @@ if (PHP_VERSION_ID < 50600) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
throw new RuntimeException($err);
}
require_once __DIR__ . '/composer/autoload_real.php';
+41 -4
View File
@@ -26,12 +26,23 @@ 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
*/
@@ -309,6 +320,24 @@ 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;
}
/**
@@ -322,19 +351,27 @@ 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';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
self::$installedByVendor[$vendorDir] = $required;
$installed[] = $required;
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
self::$installed = $required;
self::$installedIsLocalDir = true;
}
}
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
}
}
@@ -350,7 +387,7 @@ class InstalledVersions
}
}
if (self::$installed !== array()) {
if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed;
}
+407
View File
@@ -184,6 +184,196 @@ 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',
@@ -3413,6 +3603,55 @@ 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',
@@ -4921,6 +5160,76 @@ 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',
@@ -6661,6 +6970,96 @@ 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',
@@ -6869,6 +7268,14 @@ 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',
+3 -1
View File
@@ -11,14 +11,15 @@ 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',
'662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
'662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.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',
@@ -33,6 +34,7 @@ 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',
+12 -3
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/reflection-common/src', $vendorDir . '/phpdocumentor/type-resolver/src'),
'phpDocumentor\\Reflection\\' => array($vendorDir . '/phpdocumentor/type-resolver/src', $vendorDir . '/phpdocumentor/reflection-common/src'),
'ZipStream\\' => array($vendorDir . '/maennchen/zipstream-php/src'),
'Whoops\\' => array($vendorDir . '/filp/whoops/src/Whoops'),
'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'),
@@ -47,16 +47,18 @@ 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/error-solutions/legacy/laravel-ignition', $vendorDir . '/spatie/laravel-ignition/src'),
'Spatie\\Ignition\\' => array($vendorDir . '/spatie/error-solutions/legacy/ignition', $vendorDir . '/spatie/ignition/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\\FlareClient\\' => array($vendorDir . '/spatie/flare-client-php/src'),
'Spatie\\ErrorSolutions\\' => array($vendorDir . '/spatie/error-solutions/src'),
'Spatie\\Backtrace\\' => array($vendorDir . '/spatie/backtrace/src'),
@@ -77,6 +79,7 @@ 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'),
@@ -89,6 +92,7 @@ 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'),
@@ -134,6 +138,11 @@ 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'),

Some files were not shown because too many files have changed in this diff Show More