Compare commits
106
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1249ad3079 | ||
|
|
48ee511166 | ||
|
|
6718e08d1f | ||
|
|
b8b01cd5d5 | ||
|
|
f31ba24008 | ||
|
|
bb2ebcc122 | ||
|
|
955c760756 | ||
|
|
5c12d97d0e | ||
|
|
9bed740223 | ||
|
|
8be7ea0b85 | ||
|
|
dc0209dcf1 | ||
|
|
9cce361b90 | ||
|
|
c966096060 | ||
|
|
55da10cea4 | ||
|
|
0d3ed2505f | ||
|
|
def7377447 | ||
|
|
9453e7a01a | ||
|
|
8f4ac3d53e | ||
|
|
34f7490e94 | ||
|
|
d9d6a7ab58 | ||
|
|
14a7cc2738 | ||
|
|
601e72ca61 | ||
|
|
54c164478c | ||
|
|
965d2fa5bb | ||
|
|
a1e91f8d76 | ||
|
|
153cdbca98 | ||
|
|
2603c86c5b | ||
|
|
e7d733a136 | ||
|
|
eb34cba7bb | ||
|
|
f01bfa06c9 | ||
|
|
71d6fe1d98 | ||
|
|
33615f65d7 | ||
|
|
7b6d85766d | ||
|
|
b2a79aec1b | ||
|
|
a300932421 | ||
|
|
df1ddce0a6 | ||
|
|
0277fb5262 | ||
|
|
0407bcdcc4 | ||
|
|
b1810211d5 | ||
|
|
48d775f4d5 | ||
|
|
bd9bd52c10 | ||
|
|
9dda4630a4 | ||
|
|
6d0b98bd41 | ||
|
|
7db96a1f9c | ||
|
|
699bffaa78 | ||
|
|
e92e107a3c | ||
|
|
9330a49008 | ||
|
|
80dbd26929 | ||
|
|
69bb029d9d | ||
|
|
c3f3e824dc | ||
|
|
05356fc98b | ||
|
|
232f8244bf | ||
|
|
1107e003f8 | ||
|
|
c2fd2dc470 | ||
|
|
86b1c8420d | ||
|
|
1b5dd04740 | ||
|
|
ff676376ab | ||
|
|
1a47d5649a | ||
|
|
66ab44142b | ||
|
|
05f5722ebd | ||
|
|
546b4ced69 | ||
|
|
d256370712 | ||
|
|
be450f9d85 | ||
|
|
2a6eddeb44 | ||
|
|
4d447a5c0d | ||
|
|
c6168dd5d4 | ||
|
|
1c2ea459ca | ||
|
|
7183189343 | ||
|
|
1bb9928b24 | ||
|
|
173aef51c6 | ||
|
|
a94eac9fb4 | ||
|
|
51cfd0a434 | ||
|
|
def2715d38 | ||
|
|
68f344bc06 | ||
|
|
e3f7637b64 | ||
|
|
f4ea06fb93 | ||
|
|
2ab667df22 | ||
|
|
485748859e | ||
|
|
7b961d5818 | ||
|
|
500e40782d | ||
|
|
b19963808e | ||
|
|
8a5859ce46 | ||
|
|
3d2c0c9799 | ||
|
|
0130231c64 | ||
|
|
a99810d61b | ||
|
|
7473973a34 | ||
|
|
1947730e34 | ||
|
|
e3b1a46482 | ||
|
|
0e16812e9a | ||
|
|
40dd23e658 | ||
|
|
bd47f4f715 | ||
|
|
042b4698fb | ||
|
|
614bc4feba | ||
|
|
bef600ae11 | ||
|
|
1476160a38 | ||
|
|
2c7ddbcfeb | ||
|
|
6ad10c4d8f | ||
|
|
e0654b4847 | ||
|
|
fda3d66d1f | ||
|
|
ff61456d91 | ||
|
|
089886cb9a | ||
|
|
4dc010d80e | ||
|
|
83c6157b10 | ||
|
|
4dd7b89c22 | ||
|
|
dec42b4442 | ||
|
|
67bbd9bbbb |
+12
-1
@@ -60,4 +60,15 @@ public/userarea/schemi_base_response.json
|
||||
# Ignora tutti i log ovunque
|
||||
*.log
|
||||
|
||||
public/userarea/cache/
|
||||
public/userarea/cache/
|
||||
public/userarea/error_log.txt
|
||||
|
||||
# PDF/QR generati dai template (userarea)
|
||||
/public/userarea/pdftemplates/
|
||||
# Runtime / rigenerati - non tracciare
|
||||
public/userarea/error_log.txt
|
||||
public/userarea/schemi_base_response.json
|
||||
public/userarea/logs/
|
||||
public/userarea/imported_trf/
|
||||
public/userarea/xlstemplates/
|
||||
.well-known/
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
+2
-1
@@ -41,9 +41,10 @@
|
||||
"laravel/socialite": "^5.16",
|
||||
"laravel/tinker": "^2.7",
|
||||
"laravel/ui": "^4.0",
|
||||
"phpmailer/phpmailer": "^6.9",
|
||||
"phpmailer/phpmailer": "^7.1",
|
||||
"phpoffice/phpspreadsheet": "^4.1",
|
||||
"proengsoft/laravel-jsvalidation": "^4.0.0",
|
||||
"robmorgan/phinx": "^0.16.12",
|
||||
"socialiteproviders/microsoft": "^4.7",
|
||||
"spatie/laravel-query-builder": "^5.0",
|
||||
"vanguardapp/activity-log": "^6.0",
|
||||
|
||||
Generated
+656
-11
@@ -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": "e54cf3e0bbc04b94b563b01620ac3416",
|
||||
"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",
|
||||
@@ -3758,16 +4166,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpmailer/phpmailer",
|
||||
"version": "v6.9.3",
|
||||
"version": "v7.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
||||
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e"
|
||||
"reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/2f5c94fe7493efc213f643c23b1b1c249d40f47e",
|
||||
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e",
|
||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
|
||||
"reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3781,13 +4189,14 @@
|
||||
"doctrine/annotations": "^1.2.6 || ^1.13.3",
|
||||
"php-parallel-lint/php-console-highlighter": "^1.0.0",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.3.2",
|
||||
"phpcompatibility/php-compatibility": "^9.3.5",
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"squizlabs/php_codesniffer": "^3.7.2",
|
||||
"phpcompatibility/php-compatibility": "^10.0.0@dev",
|
||||
"squizlabs/php_codesniffer": "^3.13.5",
|
||||
"yoast/phpunit-polyfills": "^1.0.4"
|
||||
},
|
||||
"suggest": {
|
||||
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
|
||||
"directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example",
|
||||
"ext-imap": "Needed to support advanced email address parsing according to RFC822",
|
||||
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
|
||||
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
||||
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
|
||||
@@ -3827,7 +4236,7 @@
|
||||
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
|
||||
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.9.3"
|
||||
"source": "https://github.com/PHPMailer/PHPMailer/tree/v7.1.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -3835,7 +4244,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-11-24T18:04:13+00:00"
|
||||
"time": "2026-05-18T08:06:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpspreadsheet",
|
||||
@@ -4980,6 +5389,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 +5808,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 +6343,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 +12000,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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddTemplateNoteToExcelTemplates extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('excel_templates')
|
||||
->addColumn('template_note', 'text', [
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
'after' => 'idroutine',
|
||||
'comment' => 'Nota opzionale del template, aggiunta in fondo alle note in esportazione',
|
||||
])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class DropUniqPartAnalysisIndex extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$table = $this->table('identification_parts_analyses');
|
||||
|
||||
if ($table->hasIndexByName('uniq_part_analysis')) {
|
||||
$table->removeIndexByName('uniq_part_analysis')->update();
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$table = $this->table('identification_parts_analyses');
|
||||
|
||||
if (!$table->hasIndexByName('uniq_part_analysis')) {
|
||||
$table->addIndex(['part_id', 'analysis_recordkey'], [
|
||||
'unique' => true,
|
||||
'name' => 'uniq_part_analysis',
|
||||
])->update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class ChangeExcelrowToVarchar extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->table('datadb')
|
||||
->changeColumn('excelrow', 'string', [
|
||||
'limit' => 100,
|
||||
'null' => true,
|
||||
])
|
||||
->update();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->table('datadb')
|
||||
->changeColumn('excelrow', 'integer', [
|
||||
'null' => true,
|
||||
])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Aggiunge il flag `engine` alla tabella `routine` esistente (0 = legacy su file,
|
||||
* 1 = motore generico basato su routine_step) e crea la tabella `routine_step`.
|
||||
*
|
||||
* Migrazione puramente additiva: le routine legacy esistenti (id 1..4) restano
|
||||
* intatte e con engine = 0 continuano a funzionare come prima.
|
||||
*/
|
||||
final class AddEngineAndRoutineStep extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
// 1) Flag engine sulla tabella esistente `routine`
|
||||
$routine = $this->table('routine');
|
||||
if (!$routine->hasColumn('engine')) {
|
||||
$routine
|
||||
->addColumn('engine', 'boolean', [
|
||||
'default' => 0,
|
||||
'null' => false,
|
||||
'comment' => '0 = routine legacy su file, 1 = motore generico (routine_step)',
|
||||
'after' => 'filename',
|
||||
])
|
||||
->update();
|
||||
}
|
||||
|
||||
// 2) Nuova tabella routine_step
|
||||
if (!$this->hasTable('routine_step')) {
|
||||
$table = $this->table('routine_step', [
|
||||
'id' => 'idstep',
|
||||
'signed' => false,
|
||||
'engine' => 'InnoDB',
|
||||
'collation' => 'utf8mb4_general_ci',
|
||||
]);
|
||||
|
||||
$table
|
||||
->addColumn('idroutine', 'integer', [
|
||||
'null' => false,
|
||||
'comment' => 'FK verso routine.idroutine',
|
||||
])
|
||||
->addColumn('step_order', 'integer', [
|
||||
'default' => 1,
|
||||
'null' => false,
|
||||
'comment' => 'Ordine di esecuzione dello step nella routine',
|
||||
])
|
||||
->addColumn('operation', 'string', [
|
||||
'limit' => 50,
|
||||
'null' => false,
|
||||
'comment' => 'Verbo operazione: merge, replace, trim, copy, clear, uppercase, lowercase',
|
||||
])
|
||||
->addColumn('params', 'text', [
|
||||
'null' => true,
|
||||
'comment' => 'Parametri operazione in formato JSON',
|
||||
])
|
||||
->addColumn('active', 'boolean', [
|
||||
'default' => 1,
|
||||
'null' => false,
|
||||
'comment' => 'Se 0 lo step viene saltato',
|
||||
])
|
||||
->addIndex(['idroutine', 'step_order'], ['name' => 'idx_routine_order'])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddLogFileToDatadb extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$table = $this->table('datadb');
|
||||
if (!$table->hasColumn('log_file')) {
|
||||
$table->addColumn('log_file', 'string', [
|
||||
'limit' => 255,
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
'after' => 'commessaweb', // aggiusta se la colonna non esiste
|
||||
])->update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddErrorStatusToDatadb extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// Aggiunge 'x' (errore export) all'enum status, preservando i valori esistenti
|
||||
$this->execute("ALTER TABLE `datadb`
|
||||
MODIFY `status` ENUM('i','P','l','x') NULL DEFAULT NULL");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Rollback: riporta le eventuali 'x' a NULL prima di togliere il valore
|
||||
$this->execute("UPDATE `datadb` SET `status` = NULL WHERE `status` = 'x'");
|
||||
$this->execute("ALTER TABLE `datadb`
|
||||
MODIFY `status` ENUM('i','P','l') NULL DEFAULT NULL");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddNotifyExportEmailToAuthUsers extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$table = $this->table('auth_users');
|
||||
if (!$table->hasColumn('notify_export_email')) {
|
||||
$table->addColumn('notify_export_email', 'boolean', [
|
||||
'default' => true, // di default ricevono la mail
|
||||
'null' => false,
|
||||
'after' => 'lims_global_user_id',
|
||||
])->update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddAnalysisPriceToPartAnalyses extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('identification_parts_analyses')
|
||||
->addColumn('analysis_price', 'decimal', [
|
||||
'precision' => 10,
|
||||
'scale' => 2,
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
'after' => 'analysis_level',
|
||||
])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Ex public/userarea/sql/lims_value_binding.sql (feature/xls-mapping)
|
||||
*/
|
||||
final class CreateLimsValueBinding extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if ($this->hasTable('lims_value_binding')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->execute("
|
||||
CREATE TABLE `lims_value_binding` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`template_id` int(11) NOT NULL,
|
||||
`mapping_id` int(11) NOT NULL,
|
||||
`field_id` int(11) NOT NULL,
|
||||
`source_value` varchar(500) NOT NULL,
|
||||
`source_key` varchar(500) NOT NULL,
|
||||
`lims_value_id` int(11) NOT NULL,
|
||||
`lims_value` varchar(500) DEFAULT NULL,
|
||||
`created_by` int(11) DEFAULT NULL,
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uq_mapping_source` (`mapping_id`, `source_key`),
|
||||
KEY `idx_template` (`template_id`),
|
||||
KEY `idx_field_source` (`field_id`, `source_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
|
||||
");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->execute("DROP TABLE IF EXISTS `lims_value_binding`");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Ex public/userarea/sql/fixed_field_binding.sql (feature/xls-mapping)
|
||||
* Idempotente: salta colonne/indici gia' presenti.
|
||||
*/
|
||||
final class XlsFixedFieldBinding extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// --- template_fixed_mapping ---
|
||||
$tfm = $this->table('template_fixed_mapping');
|
||||
|
||||
if (!$tfm->hasColumn('json_node')) {
|
||||
$this->execute("ALTER TABLE `template_fixed_mapping`
|
||||
ADD COLUMN `json_node` varchar(500) DEFAULT NULL");
|
||||
}
|
||||
|
||||
if (!$tfm->hasColumn('default_source')) {
|
||||
$this->execute("ALTER TABLE `template_fixed_mapping`
|
||||
ADD COLUMN `default_source` enum('manual','xls','json') NOT NULL DEFAULT 'manual'");
|
||||
} else {
|
||||
$this->execute("ALTER TABLE `template_fixed_mapping`
|
||||
MODIFY COLUMN `default_source` enum('manual','xls','json') NOT NULL DEFAULT 'manual'");
|
||||
}
|
||||
|
||||
if (!$tfm->hasColumn('excel_column')) {
|
||||
$this->execute("ALTER TABLE `template_fixed_mapping`
|
||||
ADD COLUMN `excel_column` varchar(255) DEFAULT NULL AFTER `json_node`");
|
||||
}
|
||||
|
||||
// --- lims_value_binding ---
|
||||
$lvb = $this->table('lims_value_binding');
|
||||
|
||||
if (!$lvb->hasColumn('binding_kind')) {
|
||||
$this->execute("ALTER TABLE `lims_value_binding`
|
||||
ADD COLUMN `binding_kind` enum('custom','fixed') NOT NULL DEFAULT 'custom' AFTER `template_id`");
|
||||
}
|
||||
|
||||
$this->execute("ALTER TABLE `lims_value_binding`
|
||||
MODIFY COLUMN `field_id` int(11) NOT NULL DEFAULT 0");
|
||||
|
||||
if ($lvb->hasIndexByName('uq_mapping_source')) {
|
||||
$this->execute("ALTER TABLE `lims_value_binding` DROP INDEX `uq_mapping_source`");
|
||||
}
|
||||
|
||||
if (!$lvb->hasIndexByName('uq_kind_mapping_source')) {
|
||||
$this->execute("ALTER TABLE `lims_value_binding`
|
||||
ADD UNIQUE KEY `uq_kind_mapping_source` (`binding_kind`, `mapping_id`, `source_key`)");
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$lvb = $this->table('lims_value_binding');
|
||||
|
||||
if ($lvb->hasIndexByName('uq_kind_mapping_source')) {
|
||||
$this->execute("ALTER TABLE `lims_value_binding` DROP INDEX `uq_kind_mapping_source`");
|
||||
}
|
||||
if (!$lvb->hasIndexByName('uq_mapping_source')) {
|
||||
$this->execute("ALTER TABLE `lims_value_binding`
|
||||
ADD UNIQUE KEY `uq_mapping_source` (`mapping_id`, `source_key`)");
|
||||
}
|
||||
if ($lvb->hasColumn('binding_kind')) {
|
||||
$this->execute("ALTER TABLE `lims_value_binding` DROP COLUMN `binding_kind`");
|
||||
}
|
||||
$this->execute("ALTER TABLE `lims_value_binding`
|
||||
MODIFY COLUMN `field_id` int(11) NOT NULL");
|
||||
|
||||
$tfm = $this->table('template_fixed_mapping');
|
||||
foreach (['excel_column', 'default_source', 'json_node'] as $col) {
|
||||
if ($tfm->hasColumn($col)) {
|
||||
$this->execute("ALTER TABLE `template_fixed_mapping` DROP COLUMN `$col`");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Seed\AbstractSeed;
|
||||
|
||||
/**
|
||||
* Seed dimostrativo del motore generico.
|
||||
*
|
||||
* - Assicura che le routine legacy esistenti abbiano engine = 0 (nessun impatto).
|
||||
* - Crea una routine di esempio "merge_D_F_H_into_J" con engine = 1
|
||||
* e il relativo step: unisce le colonne D, F, H nella colonna J.
|
||||
*
|
||||
* Nota: filename per le routine engine punta al file motore condiviso _engine.php,
|
||||
* così apply_routine.php lo risolve correttamente anche se il flusso invia il filename.
|
||||
*/
|
||||
final class RoutineEngineSeed extends AbstractSeed
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$pdo = $this->getAdapter()->getConnection();
|
||||
|
||||
// 1) Le legacy restano engine = 0 (idempotente).
|
||||
$pdo->exec("UPDATE routine SET engine = 0 WHERE engine IS NULL");
|
||||
|
||||
// 2) Crea/aggiorna la routine engine di esempio.
|
||||
$name = 'merge_D_F_H_into_J';
|
||||
$description = 'Unisce le colonne D, F, H nella colonna J durante l\'importazione';
|
||||
$filename = '_engine.php';
|
||||
|
||||
$check = $pdo->prepare("SELECT idroutine FROM routine WHERE name = :name LIMIT 1");
|
||||
$check->execute([':name' => $name]);
|
||||
$existing = $check->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($existing) {
|
||||
$idroutine = (int)$existing['idroutine'];
|
||||
$upd = $pdo->prepare(
|
||||
"UPDATE routine
|
||||
SET description = :descr, filename = :fn, engine = 1, instruction = :instr
|
||||
WHERE idroutine = :id"
|
||||
);
|
||||
$upd->execute([
|
||||
':descr' => $description,
|
||||
':fn' => $filename,
|
||||
':instr' => 'Routine gestita dal motore generico (routine_step).',
|
||||
':id' => $idroutine,
|
||||
]);
|
||||
} else {
|
||||
$ins = $pdo->prepare(
|
||||
"INSERT INTO routine (name, description, instruction, filename, engine, headerrow)
|
||||
VALUES (:name, :descr, :instr, :fn, 1, 1)"
|
||||
);
|
||||
$ins->execute([
|
||||
':name' => $name,
|
||||
':descr' => $description,
|
||||
':instr' => 'Routine gestita dal motore generico (routine_step).',
|
||||
':fn' => $filename,
|
||||
]);
|
||||
$idroutine = (int)$pdo->lastInsertId();
|
||||
}
|
||||
|
||||
// 3) Ripulisci eventuali step precedenti di questa routine e reinserisci.
|
||||
$del = $pdo->prepare("DELETE FROM routine_step WHERE idroutine = :id");
|
||||
$del->execute([':id' => $idroutine]);
|
||||
|
||||
// Step unico: merge D,F,H -> J. Colonne espresse in lettere per leggibilità.
|
||||
$params = json_encode([
|
||||
'source_columns' => ['D', 'F', 'H'],
|
||||
'target_column' => 'J',
|
||||
'separator' => ' ',
|
||||
'skip_empty' => true,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
$insStep = $pdo->prepare(
|
||||
"INSERT INTO routine_step (idroutine, step_order, operation, params, active)
|
||||
VALUES (:id, 1, 'merge', :params, 1)"
|
||||
);
|
||||
$insStep->execute([':id' => $idroutine, ':params' => $params]);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
@@ -25,6 +25,7 @@ $templatedashboard = "Template Dashboard";
|
||||
$newtemplate = "New Template";
|
||||
$dashtemplate = "Dashboard Templates";
|
||||
$insertnewtemplatexls = "Insert New Template XLS";
|
||||
$bindingsmanage = "Value Bindings";
|
||||
// Datatables
|
||||
$langdatatables = [
|
||||
"paginate_first" => "First",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
function loadAnalysisMatrixNames() {
|
||||
return $.ajax({
|
||||
url: "get_matrici_db.php",
|
||||
url: "search_matrici.php?full_list=1",
|
||||
method: "GET",
|
||||
dataType: "json",
|
||||
})
|
||||
@@ -106,8 +106,9 @@
|
||||
}
|
||||
|
||||
container.innerHTML = items
|
||||
.map(function (item) {
|
||||
.map(function (item, index) {
|
||||
const recordKey = item.analysis_recordkey || "";
|
||||
const rowId = item.id != null ? item.id : "";
|
||||
const title = item.analysis_name || "Unnamed analysis";
|
||||
const method = item.analysis_method || "";
|
||||
|
||||
@@ -121,6 +122,8 @@
|
||||
class="analysis-remove-btn"
|
||||
data-part-id="${escapeHtml(partId)}"
|
||||
data-recordkey="${escapeHtml(recordKey)}"
|
||||
data-row-id="${escapeHtml(rowId)}"
|
||||
data-index="${index}"
|
||||
title="Remove analysis">×</button>
|
||||
</div>
|
||||
`;
|
||||
@@ -163,22 +166,25 @@
|
||||
is_web_selectable: rowEl.getAttribute("data-web") === "1" ? 1 : 0,
|
||||
is_accredited:
|
||||
rowEl.getAttribute("data-accredited") === "1" ? 1 : 0,
|
||||
analysis_price: rowEl.getAttribute("data-price") || "0",
|
||||
};
|
||||
}
|
||||
|
||||
function addAnalysisToLocalState(partId, payload, iddatadb, idmatrice) {
|
||||
function addAnalysisToLocalState(
|
||||
partId,
|
||||
payload,
|
||||
iddatadb,
|
||||
idmatrice,
|
||||
newId,
|
||||
) {
|
||||
const key = String(partId);
|
||||
if (!Array.isArray(analysisAssignedState[key])) {
|
||||
analysisAssignedState[key] = [];
|
||||
}
|
||||
|
||||
const exists = analysisAssignedState[key].some(function (item) {
|
||||
return item.analysis_recordkey === payload.analysis_recordkey;
|
||||
});
|
||||
|
||||
if (!exists) {
|
||||
{
|
||||
analysisAssignedState[key].push({
|
||||
id: null,
|
||||
id: newId || null,
|
||||
part_id: parseInt(partId, 10),
|
||||
iddatadb: iddatadb || null,
|
||||
idmatrice: idmatrice || null,
|
||||
@@ -191,21 +197,32 @@
|
||||
: null,
|
||||
is_web_selectable: payload.is_web_selectable,
|
||||
is_accredited: payload.is_accredited,
|
||||
analysis_price:
|
||||
payload.analysis_price !== ""
|
||||
? parseFloat(payload.analysis_price)
|
||||
: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function removeAnalysisFromLocalState(partId, recordKey) {
|
||||
function removeAnalysisFromLocalState(partId, recordKey, index) {
|
||||
const key = String(partId);
|
||||
if (!Array.isArray(analysisAssignedState[key])) {
|
||||
return;
|
||||
}
|
||||
|
||||
analysisAssignedState[key] = analysisAssignedState[key].filter(
|
||||
function (item) {
|
||||
return item.analysis_recordkey !== recordKey;
|
||||
},
|
||||
);
|
||||
const i = parseInt(index, 10);
|
||||
if (!isNaN(i) && i >= 0 && i < analysisAssignedState[key].length) {
|
||||
analysisAssignedState[key].splice(i, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
const found = analysisAssignedState[key].findIndex(function (item) {
|
||||
return item.analysis_recordkey === recordKey;
|
||||
});
|
||||
if (found !== -1) {
|
||||
analysisAssignedState[key].splice(found, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function saveAnalysisAssociation(partId, payload, callback) {
|
||||
@@ -235,14 +252,16 @@
|
||||
analysis_level: payload.analysis_level,
|
||||
is_web_selectable: payload.is_web_selectable,
|
||||
is_accredited: payload.is_accredited,
|
||||
analysis_price: payload.analysis_price,
|
||||
},
|
||||
})
|
||||
.done(function () {
|
||||
.done(function (resp) {
|
||||
addAnalysisToLocalState(
|
||||
partId,
|
||||
payload,
|
||||
iddatadb,
|
||||
idmatrice !== "NO_MATRIX" ? idmatrice : null,
|
||||
resp && resp.id ? resp.id : null,
|
||||
);
|
||||
renderAssignedAnalysesForPart(partId);
|
||||
if (typeof callback === "function") callback(true);
|
||||
@@ -257,7 +276,13 @@
|
||||
});
|
||||
}
|
||||
|
||||
function deleteAnalysisAssociation(partId, recordKey, callback) {
|
||||
function deleteAnalysisAssociation(
|
||||
partId,
|
||||
recordKey,
|
||||
rowId,
|
||||
index,
|
||||
callback,
|
||||
) {
|
||||
$.ajax({
|
||||
url: "delete_part_analysis.php",
|
||||
method: "POST",
|
||||
@@ -265,10 +290,11 @@
|
||||
data: {
|
||||
part_id: partId,
|
||||
analysis_recordkey: recordKey,
|
||||
row_id: rowId || "",
|
||||
},
|
||||
})
|
||||
.done(function () {
|
||||
removeAnalysisFromLocalState(partId, recordKey);
|
||||
removeAnalysisFromLocalState(partId, recordKey, index);
|
||||
renderAssignedAnalysesForPart(partId);
|
||||
if (typeof callback === "function") callback(true);
|
||||
})
|
||||
@@ -369,6 +395,17 @@
|
||||
const selectable = item.SelezionabileSuWeb === true;
|
||||
const accredited = item.Accreditato === true;
|
||||
const level = item.Livello ?? "";
|
||||
|
||||
const rawPrice = item.Prezzo;
|
||||
const priceNum =
|
||||
rawPrice === null ||
|
||||
rawPrice === undefined ||
|
||||
rawPrice === ""
|
||||
? 0
|
||||
: parseFloat(String(rawPrice).replace(",", "."));
|
||||
const price = isNaN(priceNum) ? 0 : priceNum;
|
||||
const hasPrice = price > 0;
|
||||
|
||||
const searchText = (
|
||||
analysisName +
|
||||
" " +
|
||||
@@ -394,6 +431,10 @@
|
||||
"</span>";
|
||||
}
|
||||
|
||||
if (!hasPrice) {
|
||||
badges += '<span class="badge bg-danger">No price</span>';
|
||||
}
|
||||
|
||||
return `
|
||||
<div
|
||||
class="list-group-item analysis-analysis-item"
|
||||
@@ -403,6 +444,7 @@
|
||||
data-analysis-level="${escapeHtml(level)}"
|
||||
data-web="${selectable ? "1" : "0"}"
|
||||
data-accredited="${accredited ? "1" : "0"}"
|
||||
data-price="${escapeHtml(price)}"
|
||||
data-search="${escapeHtml(searchText)}"
|
||||
>
|
||||
<div class="fw-semibold">${escapeHtml(analysisName)}</div>
|
||||
@@ -420,7 +462,6 @@
|
||||
filterAnalysisList();
|
||||
syncSelectedAnalysisRows();
|
||||
}
|
||||
|
||||
function filterAnalysisList() {
|
||||
const modal = document.getElementById("analysisModal");
|
||||
if (!modal) return;
|
||||
@@ -517,6 +558,13 @@
|
||||
.done(function (response) {
|
||||
const analyses = Array.isArray(response.value)
|
||||
? response.value.filter(function (item) {
|
||||
const name =
|
||||
item.NomeAnalisiTraduzione ||
|
||||
item.NomeAnalisi ||
|
||||
"";
|
||||
if (name.trim().startsWith("*")) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
item.SelezionabileSuWeb === true ||
|
||||
item.SelezionabileSuWeb === 1 ||
|
||||
@@ -525,6 +573,20 @@
|
||||
})
|
||||
: [];
|
||||
|
||||
analyses.sort(function (a, b) {
|
||||
const nameA = (
|
||||
a.NomeAnalisiTraduzione ||
|
||||
a.NomeAnalisi ||
|
||||
""
|
||||
).toLowerCase();
|
||||
const nameB = (
|
||||
b.NomeAnalisiTraduzione ||
|
||||
b.NomeAnalisi ||
|
||||
""
|
||||
).toLowerCase();
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
|
||||
analysisLoadedCache[cacheKey] = analyses;
|
||||
renderAnalysesList(analyses);
|
||||
})
|
||||
@@ -700,10 +762,18 @@
|
||||
|
||||
const partId = removeBtn.getAttribute("data-part-id");
|
||||
const recordKey = removeBtn.getAttribute("data-recordkey");
|
||||
const rowId = removeBtn.getAttribute("data-row-id");
|
||||
const index = removeBtn.getAttribute("data-index");
|
||||
|
||||
deleteAnalysisAssociation(partId, recordKey, function () {
|
||||
const stillUsed = Object.keys(analysisAssignedState).some(
|
||||
function (pid) {
|
||||
deleteAnalysisAssociation(
|
||||
partId,
|
||||
recordKey,
|
||||
rowId,
|
||||
index,
|
||||
function () {
|
||||
const stillUsed = Object.keys(
|
||||
analysisAssignedState,
|
||||
).some(function (pid) {
|
||||
return (
|
||||
Array.isArray(analysisAssignedState[pid]) &&
|
||||
analysisAssignedState[pid].some(
|
||||
@@ -715,15 +785,15 @@
|
||||
},
|
||||
)
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (!stillUsed) {
|
||||
delete analysisSelectedState[recordKey];
|
||||
}
|
||||
if (!stillUsed) {
|
||||
delete analysisSelectedState[recordKey];
|
||||
}
|
||||
|
||||
syncSelectedAnalysisRows();
|
||||
});
|
||||
syncSelectedAnalysisRows();
|
||||
},
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -744,17 +814,6 @@
|
||||
}
|
||||
|
||||
const recordKey = payload.analysis_recordkey;
|
||||
const alreadySelected = !!analysisSelectedState[recordKey];
|
||||
|
||||
if (alreadySelected) {
|
||||
selectedPartIds.forEach(function (partId) {
|
||||
deleteAnalysisAssociation(partId, recordKey);
|
||||
});
|
||||
|
||||
delete analysisSelectedState[recordKey];
|
||||
syncSelectedAnalysisRows();
|
||||
return;
|
||||
}
|
||||
|
||||
let pending = selectedPartIds.length;
|
||||
let atLeastOneSaved = false;
|
||||
|
||||
@@ -19,6 +19,9 @@ $(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;
|
||||
@@ -55,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");
|
||||
@@ -74,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);
|
||||
@@ -132,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();
|
||||
@@ -180,6 +198,29 @@ $(document).ready(function () {
|
||||
updateMarkers();
|
||||
markUnsaved();
|
||||
});
|
||||
|
||||
// ===================
|
||||
// TESTO LIBERO — TOGGLE MODALITÀ
|
||||
// ===================
|
||||
$(document)
|
||||
.off("click.freeTextToggle", "#addFreeTextBtnAnnotations")
|
||||
.on("click.freeTextToggle", "#addFreeTextBtnAnnotations", function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
freeTextMode = !freeTextMode;
|
||||
$(this).toggleClass("active", freeTextMode);
|
||||
|
||||
// Disattiva la selezione parte/marker mentre si aggiunge testo libero
|
||||
if (freeTextMode) {
|
||||
selectedPartNumber = null;
|
||||
$("#partsListAnnotations .list-group-item").removeClass(
|
||||
"active",
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Free text mode:", freeTextMode);
|
||||
});
|
||||
// ===================
|
||||
// COLORE LISTA DESCRIZIONI
|
||||
// ===================
|
||||
@@ -193,9 +234,22 @@ $(document).ready(function () {
|
||||
|
||||
if (descriptionTextbox && fabricCanvas) {
|
||||
descriptionTextbox.set("fill", globalDescriptionColor);
|
||||
fabricCanvas.renderAll();
|
||||
}
|
||||
|
||||
// Aggiorna anche i testi liberi già piazzati sulla foto corrente
|
||||
const currentPhoto = $("#samplePhotoAnnotations").attr("src");
|
||||
const annotations = photoAnnotations[currentPhoto];
|
||||
if (annotations && annotations.freeTexts) {
|
||||
annotations.freeTexts.forEach((entry) => {
|
||||
entry.color = globalDescriptionColor;
|
||||
});
|
||||
}
|
||||
Object.values(freeTextObjects).forEach((obj) => {
|
||||
obj.set("fill", globalDescriptionColor);
|
||||
});
|
||||
|
||||
if (fabricCanvas) fabricCanvas.renderAll();
|
||||
|
||||
markUnsaved();
|
||||
},
|
||||
);
|
||||
@@ -370,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.",
|
||||
@@ -425,6 +501,7 @@ $(document).ready(function () {
|
||||
|
||||
updateMarkers();
|
||||
updateDescriptions();
|
||||
updateFreeTexts();
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
@@ -985,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
|
||||
// ===================
|
||||
@@ -1291,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")
|
||||
|
||||
@@ -21,12 +21,50 @@ try {
|
||||
throw new Exception("Dati della routine mancanti.");
|
||||
}
|
||||
|
||||
$routineFile = __DIR__ . '/routines/' . $filename;
|
||||
// --- NUOVO: risolvi la routine dal DB per decidere legacy vs motore ---
|
||||
// Riusa la connessione PDO del progetto.
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
// Identifica la routine: preferisci idroutine se passato, altrimenti risali dal filename.
|
||||
$idroutine = isset($input['routine_data']['idroutine'])
|
||||
? (int)$input['routine_data']['idroutine']
|
||||
: 0;
|
||||
|
||||
if ($idroutine > 0) {
|
||||
$stmt = $pdo->prepare("SELECT idroutine, name, filename, engine FROM routine WHERE idroutine = :id LIMIT 1");
|
||||
$stmt->execute([':id' => $idroutine]);
|
||||
} else {
|
||||
// Il matching per filename è ambiguo per le routine a motore: tutte
|
||||
// condividono _engine.php. Senza idroutine non possiamo sapere quale
|
||||
// routine engine applicare -> meglio fermarsi che eseguire quella sbagliata.
|
||||
if ($filename === '_engine.php') {
|
||||
throw new Exception("Routine a motore senza idroutine: impossibile determinare quale routine applicare. Verifica che il template inoltri routine_data.idroutine.");
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT idroutine, name, filename, engine FROM routine WHERE filename = :fn LIMIT 1");
|
||||
$stmt->execute([':fn' => $filename]);
|
||||
}
|
||||
$routineRow = $stmt->fetch();
|
||||
|
||||
// engine = 1 -> motore generico (_engine.php); 0 o record assente -> legacy come oggi.
|
||||
$useEngine = $routineRow && (int)$routineRow['engine'] === 1;
|
||||
|
||||
if ($useEngine) {
|
||||
// Il motore ha bisogno di sapere quale routine caricare dagli step.
|
||||
$routineData['idroutine'] = (int)$routineRow['idroutine'];
|
||||
$routineData['name'] = $routineData['name'] ?? ($routineRow['name'] ?? '');
|
||||
$routineFile = __DIR__ . '/routines/_engine.php';
|
||||
} else {
|
||||
// Percorso legacy: il vecchio file della routine, identico a prima.
|
||||
$legacyFilename = $routineRow['filename'] ?? $filename;
|
||||
$routineFile = __DIR__ . '/routines/' . $legacyFilename;
|
||||
}
|
||||
|
||||
if (file_exists($routineFile)) {
|
||||
include_once $routineFile;
|
||||
$routineData['xls_headers'] = $_SESSION['headers'] ?? [];
|
||||
applyRoutine($excelData, $routineData); // Modifica $excelData in place
|
||||
error_log("Routine {$routineData['name']} applicata (file: {$filename}) per template {$template_id}, header row: {$headerrow}");
|
||||
error_log("Routine " . ($routineData['name'] ?? '') . " applicata (" . ($useEngine ? "motore" : "file: {$filename}") . ") per template {$template_id}, header row: {$headerrow}");
|
||||
} else {
|
||||
throw new Exception("File della routine non trovato: $routineFile");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
<?php
|
||||
|
||||
include('include/headscript.php');
|
||||
require_once __DIR__ . '/class/binding-functions.php';
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$templateFilter = isset($_GET['template_id']) ? (int) $_GET['template_id'] : 0;
|
||||
$search = trim($_GET['q'] ?? '');
|
||||
|
||||
$templates = $pdo->query("SELECT id, name FROM excel_templates ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$conds = [];
|
||||
$params = [];
|
||||
|
||||
if ($templateFilter > 0) {
|
||||
$conds[] = 'b.template_id = ?';
|
||||
$params[] = $templateFilter;
|
||||
}
|
||||
if ($search !== '') {
|
||||
$conds[] = '(b.source_value LIKE ? OR b.lims_value LIKE ? OR m.field_label LIKE ? OR f.fixed_field_key LIKE ?)';
|
||||
$like = '%' . $search . '%';
|
||||
array_push($params, $like, $like, $like, $like);
|
||||
}
|
||||
$where = $conds ? ('WHERE ' . implode(' AND ', $conds)) : '';
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT b.id, b.template_id, b.binding_kind, b.mapping_id, b.field_id, b.source_value,
|
||||
b.lims_value_id, b.lims_value, b.updated_at,
|
||||
t.name AS template_name,
|
||||
COALESCE(m.field_label, f.fixed_field_key) AS field_label,
|
||||
COALESCE(m.excel_column, f.excel_column) AS excel_column,
|
||||
f.fixed_field_key
|
||||
FROM lims_value_binding b
|
||||
LEFT JOIN excel_templates t ON t.id = b.template_id
|
||||
LEFT JOIN template_mapping m ON m.id = b.mapping_id AND b.binding_kind = 'custom'
|
||||
LEFT JOIN template_fixed_mapping f ON f.id = b.mapping_id AND b.binding_kind = 'fixed'
|
||||
$where
|
||||
ORDER BY t.name ASC, field_label ASC, b.source_value ASC"
|
||||
);
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$groups = [];
|
||||
foreach ($rows as $r) {
|
||||
$kind = $r['binding_kind'] ?? 'custom';
|
||||
$gkey = $kind . ':' . (int) $r['mapping_id'];
|
||||
if (!isset($groups[$gkey])) {
|
||||
$groups[$gkey] = [
|
||||
'kind' => $kind,
|
||||
'label' => $kind === 'fixed'
|
||||
? binding_fixed_label((string) $r['fixed_field_key'])
|
||||
: ($r['field_label'] ?: ('Mapping ' . $gkey)),
|
||||
'excel_column' => $r['excel_column'] ?: '',
|
||||
'template_name' => $r['template_name'] ?: ('Template ' . $r['template_id']),
|
||||
'template_id' => (int) $r['template_id'],
|
||||
'rows' => [],
|
||||
];
|
||||
}
|
||||
$groups[$gkey]['rows'][] = $r;
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
<?php include('cssinclude.php'); ?>
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.source-value {
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
td .select2-container {
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single {
|
||||
height: 38px;
|
||||
border-color: #ced4da;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||
height: 36px;
|
||||
}
|
||||
</style>
|
||||
<title>Gestione binding - <?= htmlspecialchars($titlewebsite ?? '', ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Binding valori importati → LIMS</h6>
|
||||
<small class="text-muted">
|
||||
Le modifiche valgono dalla prossima importazione: i record gia' importati non vengono aggiornati.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form class="row g-2 align-items-end mb-3" method="get">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label mb-1">Template</label>
|
||||
<select class="form-select" name="template_id" onchange="this.form.submit()">
|
||||
<option value="">Tutti</option>
|
||||
<?php foreach ($templates as $t): ?>
|
||||
<option value="<?= (int) $t['id'] ?>" <?= $templateFilter === (int) $t['id'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($t['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label mb-1">Cerca</label>
|
||||
<input type="text" class="form-control" name="q" value="<?= htmlspecialchars($search) ?>"
|
||||
placeholder="valore importato, valore LIMS, campo...">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button type="submit" class="btn btn-primary">Filtra</button>
|
||||
<a href="bindings_manage.php" class="btn btn-outline-secondary">Azzera</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="bindingError" class="alert alert-danger" style="display:none;"></div>
|
||||
|
||||
<?php if (empty($rows)): ?>
|
||||
<div class="alert alert-info mb-0">Nessun binding salvato per i filtri selezionati.</div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:32px;"></th>
|
||||
<th>Valore importato</th>
|
||||
<th>Valore LIMS</th>
|
||||
<th style="width:150px;">Aggiornato</th>
|
||||
<th style="width:110px;">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($groups as $gkey => $g): ?>
|
||||
<tr class="table-light">
|
||||
<td colspan="5">
|
||||
<strong><?= htmlspecialchars($g['label']) ?></strong>
|
||||
<?php if (($g['kind'] ?? 'custom') === 'fixed'): ?><span class="badge bg-light text-dark border">campo fisso</span><?php endif; ?>
|
||||
<span class="text-muted">
|
||||
· <?= htmlspecialchars($g['template_name']) ?>
|
||||
<?php if ($g['excel_column'] !== ''): ?>
|
||||
· colonna «<?= htmlspecialchars($g['excel_column']) ?>»
|
||||
<?php endif; ?>
|
||||
· <?= count($g['rows']) ?> valore/i
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php foreach ($g['rows'] as $r): ?>
|
||||
<tr class="binding-row"
|
||||
data-binding-id="<?= (int) $r['id'] ?>"
|
||||
data-kind="<?= htmlspecialchars($r['binding_kind'] ?? 'custom', ENT_QUOTES) ?>"
|
||||
data-fixed-key="<?= htmlspecialchars($r['fixed_field_key'] ?? '', ENT_QUOTES) ?>"
|
||||
data-template-id="<?= (int) $r['template_id'] ?>"
|
||||
data-mapping-id="<?= (int) $r['mapping_id'] ?>"
|
||||
data-field-id="<?= (int) $r['field_id'] ?>"
|
||||
data-source-value="<?= htmlspecialchars($r['source_value'], ENT_QUOTES) ?>">
|
||||
<td class="text-muted ps-4">›</td>
|
||||
<td class="source-value"><?= htmlspecialchars($r['source_value']) ?></td>
|
||||
<td>
|
||||
<select class="form-select binding-select">
|
||||
<option value="<?= (int) $r['lims_value_id'] ?>" selected>
|
||||
<?= htmlspecialchars($r['lims_value'] ?: ('#' . $r['lims_value_id'])) ?>
|
||||
</option>
|
||||
</select>
|
||||
<div class="binding-status small mt-1"></div>
|
||||
</td>
|
||||
<td class="small text-muted"><?= htmlspecialchars((string) $r['updated_at']) ?></td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger delete-binding-btn">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay toggle-icon"></div>
|
||||
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
const $error = $('#bindingError');
|
||||
|
||||
$('.binding-select').each(function() {
|
||||
const $row = $(this).closest('.binding-row');
|
||||
const isFixed = $row.data('kind') === 'fixed';
|
||||
|
||||
$(this).select2({
|
||||
placeholder: 'Seleziona valore LIMS...',
|
||||
width: '100%',
|
||||
ajax: {
|
||||
url: isFixed ? 'search_fixed_field_values.php' : 'search_customfield_values.php',
|
||||
dataType: 'json',
|
||||
delay: 200,
|
||||
data: params => isFixed ? {
|
||||
field_key: $row.data('fixed-key'),
|
||||
template_id: $row.data('template-id'),
|
||||
q: params.term || '',
|
||||
limit: 50
|
||||
} : {
|
||||
field_id: $row.data('field-id'),
|
||||
q: params.term || '',
|
||||
limit: 50
|
||||
},
|
||||
processResults: data => ({
|
||||
results: data.results || []
|
||||
})
|
||||
},
|
||||
minimumInputLength: 0
|
||||
});
|
||||
});
|
||||
|
||||
$('.binding-select').on('change', function() {
|
||||
const $select = $(this);
|
||||
const $row = $select.closest('.binding-row');
|
||||
const $status = $row.find('.binding-status');
|
||||
const selected = $select.select2('data')[0] || {};
|
||||
|
||||
if (!$select.val()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$status.removeClass('text-success text-danger').addClass('text-muted').text('Salvataggio...');
|
||||
|
||||
$.post('save_binding.php', {
|
||||
kind: $row.data('kind') || 'custom',
|
||||
template_id: $row.data('template-id'),
|
||||
mapping_id: $row.data('mapping-id'),
|
||||
field_id: $row.data('field-id'),
|
||||
source_value: String($row.data('source-value')),
|
||||
lims_value_id: $select.val(),
|
||||
lims_value: selected.text || ''
|
||||
}).then(resp => {
|
||||
if (resp && resp.success) {
|
||||
$status.removeClass('text-muted').addClass('text-success').text('Salvato');
|
||||
} else {
|
||||
$status.removeClass('text-muted').addClass('text-danger').text((resp && resp.error) || 'Errore');
|
||||
}
|
||||
}).fail(() => {
|
||||
$status.removeClass('text-muted').addClass('text-danger').text('Errore di rete');
|
||||
});
|
||||
});
|
||||
|
||||
$('.delete-binding-btn').on('click', function() {
|
||||
const $row = $(this).closest('.binding-row');
|
||||
|
||||
$error.hide();
|
||||
|
||||
$.post('delete_binding.php', {
|
||||
binding_id: $row.data('binding-id')
|
||||
}).then(resp => {
|
||||
if (resp && resp.success) {
|
||||
$row.remove();
|
||||
} else {
|
||||
$error.text((resp && resp.error) || 'Errore durante l\'eliminazione.').show();
|
||||
}
|
||||
}).fail(() => {
|
||||
$error.text('Errore di rete durante l\'eliminazione.').show();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception as PHPMailerException;
|
||||
|
||||
require_once __DIR__ . '/../../../vendor/autoload.php'; // aggiusta il path se serve
|
||||
|
||||
class Mailer
|
||||
{
|
||||
public static function send(array $to, string $subject, string $htmlBody, ?string $altBody = null): array
|
||||
{
|
||||
$mail = new PHPMailer(true);
|
||||
try {
|
||||
$mail->isSMTP();
|
||||
$mail->Host = env('MAIL_HOST');
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = env('MAIL_USERNAME');
|
||||
$mail->Password = env('MAIL_PASSWORD');
|
||||
$mail->Port = (int) env('MAIL_PORT', 465);
|
||||
$enc = env('MAIL_ENCRYPTION', 'ssl');
|
||||
$mail->SMTPSecure = ($enc === 'tls')
|
||||
? PHPMailer::ENCRYPTION_STARTTLS
|
||||
: PHPMailer::ENCRYPTION_SMTPS;
|
||||
$mail->CharSet = 'UTF-8';
|
||||
|
||||
$mail->setFrom(env('MAIL_FROM_ADDRESS'), env('MAIL_FROM_NAME', 'SmartTRF'));
|
||||
|
||||
foreach ($to as $addr) {
|
||||
$addr = trim((string)$addr);
|
||||
if ($addr !== '' && filter_var($addr, FILTER_VALIDATE_EMAIL)) {
|
||||
$mail->addAddress($addr);
|
||||
}
|
||||
}
|
||||
if (!$mail->getToAddresses()) {
|
||||
return ['ok' => false, 'error' => 'Nessun destinatario valido'];
|
||||
}
|
||||
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = $htmlBody;
|
||||
$mail->AltBody = $altBody ?: strip_tags($htmlBody);
|
||||
|
||||
$mail->send();
|
||||
return ['ok' => true, 'error' => null];
|
||||
} catch (PHPMailerException $e) {
|
||||
error_log('Mailer error: ' . $mail->ErrorInfo);
|
||||
return ['ok' => false, 'error' => $mail->ErrorInfo];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,716 @@
|
||||
<?php
|
||||
|
||||
function binding_normalize_value($value): string
|
||||
{
|
||||
$s = (string) $value;
|
||||
$s = str_replace(
|
||||
["\xC2\xA0", "\xE2\x80\xAF", "\xE2\x80\x87", "\xE2\x80\x89", "\xE2\x80\x8A", "\xEF\xBB\xBF"],
|
||||
' ',
|
||||
$s
|
||||
);
|
||||
$s = preg_replace('/\s+/u', ' ', $s);
|
||||
|
||||
return mb_strtolower(trim((string) $s));
|
||||
}
|
||||
|
||||
function binding_is_list_field(array $mapping): bool
|
||||
{
|
||||
return strcasecmp(trim((string) ($mapping['data_type'] ?? '')), 'SceltaMultipla') === 0;
|
||||
}
|
||||
|
||||
function binding_get_lims_values(int $fieldId): array
|
||||
{
|
||||
static $memo = [];
|
||||
|
||||
if ($fieldId <= 0) {
|
||||
return [];
|
||||
}
|
||||
if (array_key_exists($fieldId, $memo)) {
|
||||
return $memo[$fieldId];
|
||||
}
|
||||
|
||||
$cacheDir = dirname(__DIR__) . '/cache';
|
||||
$cacheFile = $cacheDir . '/customfield_' . $fieldId . '.json';
|
||||
|
||||
try {
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
|
||||
$values = json_decode(file_get_contents($cacheFile), true);
|
||||
} else {
|
||||
require_once __DIR__ . '/VisualLimsApiClient.class.php';
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
$data = $api->get("CustomField($fieldId)?\$expand=CustomFieldsValues");
|
||||
$values = $data['CustomFieldsValues'] ?? [];
|
||||
if (!is_dir($cacheDir)) {
|
||||
mkdir($cacheDir, 0777, true);
|
||||
}
|
||||
file_put_contents($cacheFile, json_encode($values));
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log("binding_get_lims_values failed for field $fieldId: " . $e->getMessage());
|
||||
$values = [];
|
||||
}
|
||||
|
||||
if (!is_array($values)) {
|
||||
$values = [];
|
||||
}
|
||||
|
||||
return $memo[$fieldId] = $values;
|
||||
}
|
||||
|
||||
function binding_lims_value_index(int $fieldId): array
|
||||
{
|
||||
static $memo = [];
|
||||
|
||||
if (array_key_exists($fieldId, $memo)) {
|
||||
return $memo[$fieldId];
|
||||
}
|
||||
|
||||
$index = [];
|
||||
foreach (binding_get_lims_values($fieldId) as $v) {
|
||||
$id = (int) ($v['IdCustomFieldsValue'] ?? 0);
|
||||
if ($id > 0) {
|
||||
$index[$id] = (string) ($v['Valore'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
return $memo[$fieldId] = $index;
|
||||
}
|
||||
|
||||
function binding_has_lims_values(int $fieldId): bool
|
||||
{
|
||||
return binding_get_lims_values($fieldId) !== [];
|
||||
}
|
||||
|
||||
function binding_is_lims_id(int $fieldId, $value): bool
|
||||
{
|
||||
$raw = trim((string) $value);
|
||||
if ($raw === '' || !ctype_digit($raw)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$index = binding_lims_value_index($fieldId);
|
||||
|
||||
return $index === [] ? false : isset($index[(int) $raw]);
|
||||
}
|
||||
|
||||
function binding_auto_match(int $fieldId, string $sourceValue): ?array
|
||||
{
|
||||
$needle = binding_normalize_value($sourceValue);
|
||||
if ($needle === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$matches = [];
|
||||
foreach (binding_get_lims_values($fieldId) as $v) {
|
||||
if (binding_normalize_value($v['Valore'] ?? '') === $needle) {
|
||||
$matches[] = [
|
||||
'id' => (int) ($v['IdCustomFieldsValue'] ?? 0),
|
||||
'text' => (string) ($v['Valore'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return count($matches) === 1 && $matches[0]['id'] > 0 ? $matches[0] : null;
|
||||
}
|
||||
|
||||
function binding_lookup(PDO $pdo, int $mappingId, string $sourceValue, string $kind = 'custom'): ?array
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT id, lims_value_id, lims_value, source_value
|
||||
FROM lims_value_binding
|
||||
WHERE binding_kind = ? AND mapping_id = ? AND source_key = ?
|
||||
LIMIT 1"
|
||||
);
|
||||
$stmt->execute([$kind, $mappingId, binding_normalize_value($sourceValue)]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
function binding_upsert(
|
||||
PDO $pdo,
|
||||
int $templateId,
|
||||
int $mappingId,
|
||||
int $fieldId,
|
||||
string $sourceValue,
|
||||
int $limsValueId,
|
||||
string $limsValue,
|
||||
?int $createdBy,
|
||||
string $kind = 'custom'
|
||||
): void {
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO lims_value_binding
|
||||
(template_id, binding_kind, mapping_id, field_id, source_value, source_key, lims_value_id, lims_value, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
lims_value_id = VALUES(lims_value_id),
|
||||
lims_value = VALUES(lims_value),
|
||||
field_id = VALUES(field_id),
|
||||
template_id = VALUES(template_id)"
|
||||
);
|
||||
$stmt->execute([
|
||||
$templateId,
|
||||
$kind,
|
||||
$mappingId,
|
||||
$fieldId,
|
||||
mb_substr(trim($sourceValue), 0, 500),
|
||||
mb_substr(binding_normalize_value($sourceValue), 0, 500),
|
||||
$limsValueId,
|
||||
mb_substr($limsValue, 0, 500),
|
||||
$createdBy,
|
||||
]);
|
||||
}
|
||||
|
||||
function binding_delete(PDO $pdo, int $mappingId, string $sourceValue, string $kind = 'custom'): int
|
||||
{
|
||||
$stmt = $pdo->prepare("DELETE FROM lims_value_binding WHERE binding_kind = ? AND mapping_id = ? AND source_key = ?");
|
||||
$stmt->execute([$kind, $mappingId, binding_normalize_value($sourceValue)]);
|
||||
|
||||
return $stmt->rowCount();
|
||||
}
|
||||
|
||||
function binding_delete_by_id(PDO $pdo, int $bindingId): int
|
||||
{
|
||||
$stmt = $pdo->prepare("DELETE FROM lims_value_binding WHERE id = ?");
|
||||
$stmt->execute([$bindingId]);
|
||||
|
||||
return $stmt->rowCount();
|
||||
}
|
||||
|
||||
function binding_suggest(PDO $pdo, int $fieldId, string $sourceValue, int $excludeMappingId): ?array
|
||||
{
|
||||
if ($fieldId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT b.lims_value_id, b.lims_value, b.template_id, t.name AS template_name
|
||||
FROM lims_value_binding b
|
||||
LEFT JOIN excel_templates t ON t.id = b.template_id
|
||||
WHERE b.binding_kind = 'custom' AND b.field_id = ? AND b.source_key = ? AND b.mapping_id <> ?
|
||||
ORDER BY b.updated_at DESC
|
||||
LIMIT 1"
|
||||
);
|
||||
$stmt->execute([$fieldId, binding_normalize_value($sourceValue), $excludeMappingId]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
function binding_apply_to_details(PDO $pdo, int $mappingId, string $value, array $datadbIds): int
|
||||
{
|
||||
$datadbIds = array_values(array_filter(array_map('intval', $datadbIds)));
|
||||
if (empty($datadbIds)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($datadbIds), '?'));
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE import_data_details
|
||||
SET field_value = ?
|
||||
WHERE mapping_id = ? AND id IN ($placeholders)"
|
||||
);
|
||||
$stmt->execute(array_merge([$value, $mappingId], $datadbIds));
|
||||
|
||||
return $stmt->rowCount();
|
||||
}
|
||||
|
||||
function binding_find_column_index(string $column, array $columns)
|
||||
{
|
||||
$needle = binding_normalize_value($column);
|
||||
if ($needle === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($columns as $i => $c) {
|
||||
if (binding_normalize_value($c) === $needle) {
|
||||
return $i;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function binding_fixed_alias_map(): array
|
||||
{
|
||||
return [
|
||||
'ClienteResponsabile' => 'cliente_responsabile_id',
|
||||
'ClienteFornitore' => 'cliente_fornitore_id',
|
||||
'ClienteAnalisi' => 'clienteAnalisi',
|
||||
'ClienteFatturazione' => 'ClienteFatturazione',
|
||||
'MoltiplicatorePrezzo' => 'moltiplicatore_prezzo_id',
|
||||
'AnagraficaCertestObject' => 'anagrafica_certest_object_id',
|
||||
'AnagraficaCertestService' => 'anagrafica_certest_service_id',
|
||||
'ConsegnaRichiesta' => 'consegna_richiesta',
|
||||
];
|
||||
}
|
||||
|
||||
function binding_fixed_column(string $key): ?string
|
||||
{
|
||||
return binding_fixed_alias_map()[$key] ?? null;
|
||||
}
|
||||
|
||||
function binding_fixed_is_list(string $key): bool
|
||||
{
|
||||
return in_array($key, [
|
||||
'ClienteResponsabile',
|
||||
'ClienteFornitore',
|
||||
'ClienteAnalisi',
|
||||
'ClienteFatturazione',
|
||||
'MoltiplicatorePrezzo',
|
||||
'AnagraficaCertestObject',
|
||||
'AnagraficaCertestService',
|
||||
], true);
|
||||
}
|
||||
|
||||
function binding_fixed_is_date(string $key): bool
|
||||
{
|
||||
return $key === 'ConsegnaRichiesta';
|
||||
}
|
||||
|
||||
function binding_fixed_auto_matchable(string $key): bool
|
||||
{
|
||||
return in_array($key, [
|
||||
'MoltiplicatorePrezzo',
|
||||
'AnagraficaCertestObject',
|
||||
'AnagraficaCertestService',
|
||||
], true);
|
||||
}
|
||||
|
||||
function binding_fixed_label(string $key): string
|
||||
{
|
||||
$labels = [
|
||||
'AnagraficaCertestObject' => 'Anagrafica Certest Object',
|
||||
'AnagraficaCertestService' => 'Anagrafica Certest Service',
|
||||
'MoltiplicatorePrezzo' => 'Moltiplicatore Prezzo',
|
||||
'ClienteResponsabile' => 'Cliente Responsabile',
|
||||
'ClienteFornitore' => 'Cliente Fornitore',
|
||||
'ClienteAnalisi' => 'Buyer',
|
||||
'ClienteFatturazione' => 'Cliente Fatturazione',
|
||||
'ConsegnaRichiesta' => 'Consegna Richiesta',
|
||||
];
|
||||
|
||||
return $labels[$key] ?? $key;
|
||||
}
|
||||
|
||||
function binding_parse_date($value): ?string
|
||||
{
|
||||
$raw = trim((string) $value);
|
||||
if ($raw === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (['Y-m-d', 'd/m/Y', 'd-m-Y', 'd.m.Y', 'Y/m/d', 'd/m/y', 'Y-m-d H:i:s'] as $format) {
|
||||
$date = DateTime::createFromFormat($format, $raw);
|
||||
if ($date && $date->format($format) === $raw) {
|
||||
return $date->format('Y-m-d');
|
||||
}
|
||||
}
|
||||
|
||||
$ts = strtotime($raw);
|
||||
|
||||
return $ts ? date('Y-m-d', $ts) : null;
|
||||
}
|
||||
|
||||
function binding_fixed_default(array $fixedMapping): string
|
||||
{
|
||||
$value = trim((string) ($fixedMapping['default_value'] ?? ''));
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$isDate = strcasecmp((string) ($fixedMapping['data_type'] ?? ''), 'DATE') === 0
|
||||
|| binding_fixed_is_date((string) ($fixedMapping['fixed_field_key'] ?? ''));
|
||||
|
||||
if ($isDate && preg_match('/^today\s*(?:\+\s*(\d+))?$/i', $value, $m)) {
|
||||
return date('Y-m-d', strtotime('+' . (int) ($m[1] ?? 0) . ' days'));
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
function binding_cached_get($api, string $cacheFile, string $endpoint, int $ttl = 3600): array
|
||||
{
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < $ttl)) {
|
||||
$data = json_decode(file_get_contents($cacheFile), true);
|
||||
if (is_array($data)) {
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
$data = $api->get($endpoint);
|
||||
if (is_array($data)) {
|
||||
$dir = dirname($cacheFile);
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
file_put_contents($cacheFile, json_encode($data));
|
||||
}
|
||||
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
function binding_template_idclient(PDO $pdo, int $templateId): int
|
||||
{
|
||||
$stmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
|
||||
$stmt->execute([$templateId]);
|
||||
|
||||
return (int) ($stmt->fetchColumn() ?: 0);
|
||||
}
|
||||
|
||||
function binding_client_label(array $client): string
|
||||
{
|
||||
$name = trim((string) ($client['Nominativo'] ?? ''));
|
||||
$code = trim((string) ($client['CodiceCliente'] ?? ''));
|
||||
|
||||
return $code !== '' ? ($name . ' - ' . $code) : $name;
|
||||
}
|
||||
|
||||
function binding_get_fixed_values(PDO $pdo, string $fieldKey, int $templateId): array
|
||||
{
|
||||
static $memo = [];
|
||||
$memoKey = $fieldKey . '|' . $templateId;
|
||||
if (isset($memo[$memoKey])) {
|
||||
return $memo[$memoKey];
|
||||
}
|
||||
|
||||
$cacheDir = dirname(__DIR__) . '/cache';
|
||||
$out = [];
|
||||
|
||||
try {
|
||||
require_once __DIR__ . '/VisualLimsApiClient.class.php';
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
switch ($fieldKey) {
|
||||
case 'MoltiplicatorePrezzo':
|
||||
$data = binding_cached_get($api, "$cacheDir/moltiplicatori_prezzo.json", 'MoltiplicatorePrezzi');
|
||||
foreach (($data['value'] ?? []) as $r) {
|
||||
$out[] = ['id' => (int) ($r['IdMoltiplicatorePrezzo'] ?? 0), 'text' => (string) ($r['Descrizione'] ?? '')];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'AnagraficaCertestObject':
|
||||
case 'AnagraficaCertestService':
|
||||
$file = $fieldKey === 'AnagraficaCertestObject'
|
||||
? 'anagrafica_certest_object.json'
|
||||
: 'anagrafica_certest_service.json';
|
||||
$data = binding_cached_get($api, "$cacheDir/$file", $fieldKey);
|
||||
foreach (($data['value'] ?? []) as $r) {
|
||||
$code = trim((string) ($r['Codice'] ?? ''));
|
||||
$name = (string) ($r['NomeAnagrafica'] ?? '');
|
||||
$out[] = [
|
||||
'id' => (int) ($r['IdAnagrafica'] ?? 0),
|
||||
'text' => $name,
|
||||
'aliases' => array_values(array_filter([$code, $code !== '' ? "$code - $name" : ''])),
|
||||
];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ClienteResponsabile':
|
||||
$idCliente = binding_template_idclient($pdo, $templateId);
|
||||
if ($idCliente > 0) {
|
||||
$data = binding_cached_get(
|
||||
$api,
|
||||
"$cacheDir/cliente_responsabili_$idCliente.json",
|
||||
"Cliente($idCliente)?\$expand=Responsabili"
|
||||
);
|
||||
foreach (($data['Responsabili'] ?? []) as $r) {
|
||||
$out[] = ['id' => (int) ($r['IdClienteResponsabile'] ?? 0), 'text' => (string) ($r['Nominativo'] ?? '')];
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ClienteFornitore':
|
||||
case 'ClienteAnalisi':
|
||||
case 'ClienteFatturazione':
|
||||
$endpoint = 'Cliente?' . http_build_query([
|
||||
'$select' => 'IdCliente,Nominativo,CodiceCliente',
|
||||
'$orderby' => 'Nominativo asc',
|
||||
]);
|
||||
$data = binding_cached_get($api, "$cacheDir/clienti.json", $endpoint);
|
||||
foreach (($data['value'] ?? []) as $r) {
|
||||
$out[] = ['id' => (int) ($r['IdCliente'] ?? 0), 'text' => binding_client_label($r)];
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log("binding_get_fixed_values($fieldKey) failed: " . $e->getMessage());
|
||||
$out = [];
|
||||
}
|
||||
|
||||
return $memo[$memoKey] = array_values(array_filter($out, fn($v) => $v['id'] > 0));
|
||||
}
|
||||
|
||||
function binding_fixed_has_values(PDO $pdo, string $fieldKey, int $templateId): bool
|
||||
{
|
||||
return binding_get_fixed_values($pdo, $fieldKey, $templateId) !== [];
|
||||
}
|
||||
|
||||
function binding_fixed_is_lims_id(PDO $pdo, string $fieldKey, int $templateId, $value): bool
|
||||
{
|
||||
$raw = trim((string) $value);
|
||||
if ($raw === '' || !ctype_digit($raw)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (binding_get_fixed_values($pdo, $fieldKey, $templateId) as $v) {
|
||||
if ((int) $v['id'] === (int) $raw) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function binding_auto_match_fixed(array $values, string $sourceValue): ?array
|
||||
{
|
||||
$needle = binding_normalize_value($sourceValue);
|
||||
if ($needle === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$matches = [];
|
||||
foreach ($values as $v) {
|
||||
$candidates = array_merge([$v['text'] ?? ''], $v['aliases'] ?? []);
|
||||
foreach ($candidates as $candidate) {
|
||||
if (binding_normalize_value($candidate) === $needle) {
|
||||
$matches[] = $v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count($matches) === 1 ? $matches[0] : null;
|
||||
}
|
||||
|
||||
function binding_suggest_fixed(PDO $pdo, string $fieldKey, string $sourceValue, int $excludeMappingId): ?array
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT b.lims_value_id, b.lims_value, b.template_id, t.name AS template_name
|
||||
FROM lims_value_binding b
|
||||
JOIN template_fixed_mapping f ON f.id = b.mapping_id
|
||||
LEFT JOIN excel_templates t ON t.id = b.template_id
|
||||
WHERE b.binding_kind = 'fixed' AND f.fixed_field_key = ?
|
||||
AND b.source_key = ? AND b.mapping_id <> ?
|
||||
ORDER BY b.updated_at DESC
|
||||
LIMIT 1"
|
||||
);
|
||||
$stmt->execute([$fieldKey, binding_normalize_value($sourceValue), $excludeMappingId]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
function binding_apply_to_datadb(PDO $pdo, string $fieldKey, $value, array $datadbIds): int
|
||||
{
|
||||
$column = binding_fixed_column($fieldKey);
|
||||
if ($column === null) {
|
||||
throw new InvalidArgumentException("Campo fisso sconosciuto: $fieldKey");
|
||||
}
|
||||
|
||||
$datadbIds = array_values(array_filter(array_map('intval', $datadbIds)));
|
||||
if (empty($datadbIds)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($datadbIds), '?'));
|
||||
$stmt = $pdo->prepare("UPDATE datadb SET `$column` = ? WHERE iddatadb IN ($placeholders)");
|
||||
$stmt->execute(array_merge([$value === '' ? null : $value], $datadbIds));
|
||||
|
||||
return $stmt->rowCount();
|
||||
}
|
||||
|
||||
function binding_write_fixed_columns(PDO $pdo, int $iddatadb, array $valuesByKey): int
|
||||
{
|
||||
$map = binding_fixed_alias_map();
|
||||
$sets = [];
|
||||
$params = [];
|
||||
|
||||
foreach ($valuesByKey as $key => $value) {
|
||||
if (!isset($map[$key]) || $value === null || $value === '') {
|
||||
continue;
|
||||
}
|
||||
$sets[] = "`{$map[$key]}` = ?";
|
||||
$params[] = $value;
|
||||
}
|
||||
|
||||
if (empty($sets)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$params[] = $iddatadb;
|
||||
$stmt = $pdo->prepare("UPDATE datadb SET " . implode(', ', $sets) . " WHERE iddatadb = ?");
|
||||
$stmt->execute($params);
|
||||
|
||||
return $stmt->rowCount();
|
||||
}
|
||||
|
||||
function binding_resolve_for_import(
|
||||
PDO $pdo,
|
||||
array $mapping,
|
||||
int $templateId,
|
||||
string $sourceValue,
|
||||
int $iddatadb,
|
||||
?int $userId,
|
||||
array &$saved,
|
||||
array &$auto,
|
||||
array &$pending
|
||||
): string {
|
||||
$mappingId = (int) $mapping['id'];
|
||||
$fieldId = (int) $mapping['field_id'];
|
||||
$key = 'cf:' . $mappingId . '|' . binding_normalize_value($sourceValue);
|
||||
|
||||
$collect = function (array &$bucket, array $extra = []) use ($key, $mapping, $mappingId, $fieldId, $sourceValue, $iddatadb) {
|
||||
if (!isset($bucket[$key])) {
|
||||
$bucket[$key] = array_merge([
|
||||
'kind' => 'custom',
|
||||
'mapping_id' => $mappingId,
|
||||
'field_id' => $fieldId,
|
||||
'field_label' => $mapping['field_label'] ?? ('Mapping ' . $mappingId),
|
||||
'source_value' => $sourceValue,
|
||||
'datadb_ids' => [],
|
||||
], $extra);
|
||||
}
|
||||
$bucket[$key]['datadb_ids'][] = $iddatadb;
|
||||
};
|
||||
|
||||
if (!binding_has_lims_values($fieldId)) {
|
||||
return $sourceValue;
|
||||
}
|
||||
|
||||
if (binding_is_lims_id($fieldId, $sourceValue)) {
|
||||
return $sourceValue;
|
||||
}
|
||||
|
||||
if ($existing = binding_lookup($pdo, $mappingId, $sourceValue)) {
|
||||
$collect($saved, [
|
||||
'lims_value_id' => (int) $existing['lims_value_id'],
|
||||
'lims_value' => (string) $existing['lims_value'],
|
||||
]);
|
||||
|
||||
return (string) $existing['lims_value_id'];
|
||||
}
|
||||
|
||||
if ($match = binding_auto_match($fieldId, $sourceValue)) {
|
||||
binding_upsert($pdo, $templateId, $mappingId, $fieldId, $sourceValue, $match['id'], $match['text'], $userId);
|
||||
$collect($auto, [
|
||||
'lims_value_id' => $match['id'],
|
||||
'lims_value' => $match['text'],
|
||||
]);
|
||||
|
||||
return (string) $match['id'];
|
||||
}
|
||||
|
||||
$collect($pending);
|
||||
|
||||
return $sourceValue;
|
||||
}
|
||||
|
||||
function binding_resolve_fixed_for_import(
|
||||
PDO $pdo,
|
||||
array $fixedMapping,
|
||||
int $templateId,
|
||||
string $sourceValue,
|
||||
int $iddatadb,
|
||||
?int $userId,
|
||||
array &$saved,
|
||||
array &$auto,
|
||||
array &$pending
|
||||
): ?string {
|
||||
$fieldKey = (string) $fixedMapping['fixed_field_key'];
|
||||
$mappingId = (int) $fixedMapping['id'];
|
||||
$key = 'fx:' . $mappingId . '|' . binding_normalize_value($sourceValue);
|
||||
|
||||
$collect = function (array &$bucket, array $extra = []) use ($key, $fieldKey, $mappingId, $sourceValue, $iddatadb) {
|
||||
if (!isset($bucket[$key])) {
|
||||
$bucket[$key] = array_merge([
|
||||
'kind' => 'fixed',
|
||||
'mapping_id' => $mappingId,
|
||||
'fixed_field_key' => $fieldKey,
|
||||
'field_id' => 0,
|
||||
'field_label' => binding_fixed_label($fieldKey),
|
||||
'source_value' => $sourceValue,
|
||||
'datadb_ids' => [],
|
||||
], $extra);
|
||||
}
|
||||
$bucket[$key]['datadb_ids'][] = $iddatadb;
|
||||
};
|
||||
|
||||
if (binding_fixed_is_date($fieldKey)) {
|
||||
return binding_parse_date($sourceValue);
|
||||
}
|
||||
|
||||
if (!binding_fixed_is_list($fieldKey)) {
|
||||
return $sourceValue;
|
||||
}
|
||||
|
||||
if (!binding_fixed_has_values($pdo, $fieldKey, $templateId)) {
|
||||
error_log("[BINDING] campo fisso $fieldKey: lista LIMS non disponibile, valore non scritto");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (binding_fixed_is_lims_id($pdo, $fieldKey, $templateId, $sourceValue)) {
|
||||
return (string) (int) $sourceValue;
|
||||
}
|
||||
|
||||
if ($existing = binding_lookup($pdo, $mappingId, $sourceValue, 'fixed')) {
|
||||
$collect($saved, [
|
||||
'lims_value_id' => (int) $existing['lims_value_id'],
|
||||
'lims_value' => (string) $existing['lims_value'],
|
||||
]);
|
||||
|
||||
return (string) $existing['lims_value_id'];
|
||||
}
|
||||
|
||||
if (binding_fixed_auto_matchable($fieldKey)) {
|
||||
$match = binding_auto_match_fixed(binding_get_fixed_values($pdo, $fieldKey, $templateId), $sourceValue);
|
||||
if ($match) {
|
||||
binding_upsert($pdo, $templateId, $mappingId, 0, $sourceValue, (int) $match['id'], (string) $match['text'], $userId, 'fixed');
|
||||
$collect($auto, [
|
||||
'lims_value_id' => (int) $match['id'],
|
||||
'lims_value' => (string) $match['text'],
|
||||
]);
|
||||
|
||||
return (string) $match['id'];
|
||||
}
|
||||
}
|
||||
|
||||
$collect($pending);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function binding_merge_buckets(array $bucket, array $items): array
|
||||
{
|
||||
foreach ($items as $key => $item) {
|
||||
if (!isset($bucket[$key])) {
|
||||
$bucket[$key] = $item;
|
||||
continue;
|
||||
}
|
||||
$bucket[$key]['datadb_ids'] = array_values(array_unique(array_merge(
|
||||
$bucket[$key]['datadb_ids'] ?? [],
|
||||
$item['datadb_ids'] ?? []
|
||||
)));
|
||||
}
|
||||
|
||||
return $bucket;
|
||||
}
|
||||
|
||||
function binding_list_for_template(PDO $pdo, int $templateId): array
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT b.*,
|
||||
COALESCE(m.field_label, f.fixed_field_key) AS field_label,
|
||||
COALESCE(m.excel_column, f.excel_column) AS excel_column,
|
||||
f.fixed_field_key
|
||||
FROM lims_value_binding b
|
||||
LEFT JOIN template_mapping m ON m.id = b.mapping_id AND b.binding_kind = 'custom'
|
||||
LEFT JOIN template_fixed_mapping f ON f.id = b.mapping_id AND b.binding_kind = 'fixed'
|
||||
WHERE b.template_id = ?
|
||||
ORDER BY field_label ASC, b.source_value ASC"
|
||||
);
|
||||
$stmt->execute([$templateId]);
|
||||
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
@@ -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";
|
||||
@@ -10,6 +10,11 @@ $data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$sourceIddatadb = isset($data['source_iddatadb']) ? (int)$data['source_iddatadb'] : 0;
|
||||
$targetList = $data['target_iddatadb_list'] ?? [];
|
||||
// Default true: se il client non manda il parametro, cloniamo comunque le note
|
||||
// (comportamento di default richiesto, coerente con la checkbox pre-spuntata)
|
||||
$cloneNotes = array_key_exists('clone_notes', $data) ? !empty($data['clone_notes']) : true;
|
||||
$cloneAnalyses = !empty($data['clone_analyses']);
|
||||
$overwrite = !empty($data['overwrite']);
|
||||
|
||||
$targetIds = array_values(array_unique(array_filter(array_map('intval', (array)$targetList), function ($v) use ($sourceIddatadb) {
|
||||
return $v > 0 && $v !== $sourceIddatadb;
|
||||
@@ -62,6 +67,23 @@ try {
|
||||
ORDER BY id ASC
|
||||
");
|
||||
|
||||
$stmtLoadAnalyses = $pdo->prepare("
|
||||
SELECT idmatrice, analysis_recordkey, analysis_name, analysis_method,
|
||||
analysis_level, is_web_selectable, is_accredited
|
||||
FROM identification_parts_analyses
|
||||
WHERE part_id = ?
|
||||
ORDER BY id ASC
|
||||
");
|
||||
|
||||
$stmtInsertAnalysis = $pdo->prepare("
|
||||
INSERT INTO identification_parts_analyses
|
||||
(part_id, iddatadb, idmatrice, analysis_recordkey, analysis_name, analysis_method,
|
||||
analysis_level, is_web_selectable, is_accredited, created_at, updated_at)
|
||||
VALUES
|
||||
(:part_id, :iddatadb, :idmatrice, :analysis_recordkey, :analysis_name, :analysis_method,
|
||||
:analysis_level, :is_web_selectable, :is_accredited, NOW(), NOW())
|
||||
");
|
||||
|
||||
$stmtInsertCF = $pdo->prepare("
|
||||
INSERT INTO identification_parts_customfields
|
||||
(part_id, field_id, value_id, value_text, created_at, updated_at)
|
||||
@@ -71,6 +93,29 @@ try {
|
||||
|
||||
$details = [];
|
||||
$totalClonedParts = 0;
|
||||
$totalClonedAnalyses = 0;
|
||||
$totalDeletedParts = 0;
|
||||
|
||||
// 2b. Se overwrite: elimina le parti esistenti dei target (e i loro figli)
|
||||
if ($overwrite) {
|
||||
$stmtSelectExisting = $pdo->prepare("SELECT id FROM identification_parts WHERE iddatadb = ?");
|
||||
$stmtDeleteCFByPart = $pdo->prepare("DELETE FROM identification_parts_customfields WHERE part_id = ?");
|
||||
$stmtDeleteAnByPart = $pdo->prepare("DELETE FROM identification_parts_analyses WHERE part_id = ?");
|
||||
$stmtDeletePart = $pdo->prepare("DELETE FROM identification_parts WHERE id = ?");
|
||||
|
||||
foreach ($targetIds as $targetIddatadb) {
|
||||
$stmtSelectExisting->execute([$targetIddatadb]);
|
||||
$existingIds = $stmtSelectExisting->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
foreach ($existingIds as $existingPartId) {
|
||||
$existingPartId = (int)$existingPartId;
|
||||
$stmtDeleteCFByPart->execute([$existingPartId]);
|
||||
$stmtDeleteAnByPart->execute([$existingPartId]);
|
||||
$stmtDeletePart->execute([$existingPartId]);
|
||||
$totalDeletedParts++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Clone source parts to each target record
|
||||
foreach ($targetIds as $targetIddatadb) {
|
||||
@@ -83,7 +128,7 @@ try {
|
||||
':part_description' => $part['part_description'],
|
||||
':mix' => $part['mix'] ?? 'N',
|
||||
':idmatrice' => $part['idmatrice'] !== '' ? $part['idmatrice'] : null,
|
||||
':note' => $part['note'] ?? null,
|
||||
':note' => $cloneNotes ? ($part['note'] ?? null) : null,
|
||||
':dateexpiry' => $part['dateexpiry'] ?? null,
|
||||
]);
|
||||
|
||||
@@ -102,6 +147,27 @@ try {
|
||||
]);
|
||||
}
|
||||
|
||||
// Clone linked analyses, if requested
|
||||
if ($cloneAnalyses) {
|
||||
$stmtLoadAnalyses->execute([(int)$part['id']]);
|
||||
$analyses = $stmtLoadAnalyses->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($analyses as $an) {
|
||||
$stmtInsertAnalysis->execute([
|
||||
':part_id' => $newPartId,
|
||||
':iddatadb' => $targetIddatadb,
|
||||
':idmatrice' => ($an['idmatrice'] !== null && $an['idmatrice'] !== '') ? (int)$an['idmatrice'] : null,
|
||||
':analysis_recordkey' => $an['analysis_recordkey'],
|
||||
':analysis_name' => $an['analysis_name'] ?? null,
|
||||
':analysis_method' => $an['analysis_method'] ?? null,
|
||||
':analysis_level' => ($an['analysis_level'] !== null && $an['analysis_level'] !== '') ? (int)$an['analysis_level'] : null,
|
||||
':is_web_selectable' => (int)($an['is_web_selectable'] ?? 0),
|
||||
':is_accredited' => (int)($an['is_accredited'] ?? 0),
|
||||
]);
|
||||
$totalClonedAnalyses++;
|
||||
}
|
||||
}
|
||||
|
||||
$clonedCountForTarget++;
|
||||
$totalClonedParts++;
|
||||
}
|
||||
@@ -119,6 +185,7 @@ try {
|
||||
'source_iddatadb' => $sourceIddatadb,
|
||||
'cloned_targets' => count($targetIds),
|
||||
'total_cloned_parts' => $totalClonedParts,
|
||||
'total_cloned_analyses' => $totalClonedAnalyses,
|
||||
'details' => $details
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
require_once __DIR__ . '/class/binding-functions.php';
|
||||
include dirname(__DIR__) . '/../extra/auth.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
if (!Auth::check()) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$bindingId = intval($_POST['binding_id'] ?? 0);
|
||||
|
||||
if ($bindingId <= 0) {
|
||||
http_response_code(422);
|
||||
echo json_encode(['success' => false, 'error' => 'Missing binding_id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
$deleted = binding_delete_by_id($pdo, $bindingId);
|
||||
|
||||
echo json_encode(['success' => $deleted > 0, 'deleted' => $deleted]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -15,8 +15,9 @@ try {
|
||||
|
||||
$partId = isset($_POST['part_id']) ? (int)$_POST['part_id'] : 0;
|
||||
$analysisRecordkey = trim($_POST['analysis_recordkey'] ?? '');
|
||||
$rowId = isset($_POST['row_id']) && $_POST['row_id'] !== '' ? (int)$_POST['row_id'] : 0;
|
||||
|
||||
if ($partId <= 0 || $analysisRecordkey === '') {
|
||||
if ($partId <= 0 || ($analysisRecordkey === '' && $rowId <= 0)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Missing required data']);
|
||||
exit;
|
||||
@@ -25,15 +26,29 @@ try {
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
DELETE FROM identification_parts_analyses
|
||||
WHERE part_id = :part_id
|
||||
AND analysis_recordkey = :analysis_recordkey
|
||||
");
|
||||
$stmt->execute([
|
||||
':part_id' => $partId,
|
||||
':analysis_recordkey' => $analysisRecordkey,
|
||||
]);
|
||||
if ($rowId > 0) {
|
||||
$stmt = $pdo->prepare("
|
||||
DELETE FROM identification_parts_analyses
|
||||
WHERE id = :id
|
||||
AND part_id = :part_id
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([
|
||||
':id' => $rowId,
|
||||
':part_id' => $partId,
|
||||
]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("
|
||||
DELETE FROM identification_parts_analyses
|
||||
WHERE part_id = :part_id
|
||||
AND analysis_recordkey = :analysis_recordkey
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([
|
||||
':part_id' => $partId,
|
||||
':analysis_recordkey' => $analysisRecordkey,
|
||||
]);
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
$logDir = __DIR__ . '/logs/api/';
|
||||
|
||||
$iddatadb = (int)($_GET['iddatadb'] ?? 0);
|
||||
if ($iddatadb <= 0) {
|
||||
http_response_code(400);
|
||||
exit('iddatadb mancante');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT log_file, commessaweb FROM datadb WHERE iddatadb = :id LIMIT 1");
|
||||
$stmt->execute(['id' => $iddatadb]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row || empty($row['log_file'])) {
|
||||
http_response_code(404);
|
||||
exit('Nessun log per questa commessa');
|
||||
}
|
||||
|
||||
// solo basename: niente path traversal
|
||||
$path = $logDir . basename($row['log_file']);
|
||||
if (!is_file($path)) {
|
||||
http_response_code(404);
|
||||
exit('File di log non trovato: ' . basename($row['log_file']));
|
||||
}
|
||||
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
$label = preg_replace('/[^A-Za-z0-9_\-]/', '_', (string)($row['commessaweb'] ?: $iddatadb));
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="log_' . $label . '_' . $iddatadb . '.log"');
|
||||
header('Content-Length: ' . filesize($path));
|
||||
readfile($path);
|
||||
exit;
|
||||
@@ -265,7 +265,11 @@ if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonText
|
||||
<label class="form-label"><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></label>
|
||||
<textarea name="description" class="form-control"><?php echo htmlspecialchars($template['description'] ?? ''); ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Template Note</label>
|
||||
<textarea name="template_note" class="form-control" rows="3"><?php echo htmlspecialchars($template['template_note'] ?? ''); ?></textarea>
|
||||
<small class="text-muted">If set, this note will be appended at the end of the notes during export.</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($desttable, ENT_QUOTES, 'UTF-8'); ?> *</label>
|
||||
<input type="text" name="target_table" class="form-control" value="<?php echo htmlspecialchars($template['target_table'] ?? 'datadb'); ?>" readonly required>
|
||||
|
||||
@@ -1,333 +0,0 @@
|
||||
2025-06-10 12:21:44 - Errore nel recupero dati: HTTP 404, Risposta:
|
||||
2025-06-16 11:15:30 - Errore nel recupero dati: HTTP 404, Risposta: {"title":"Not Found","status":404,"detail":"Not Found","instance":"GET /api/odata/Rapporto(2523026)","errorCode":"d25cbd678"}
|
||||
2025-06-16 11:34:59 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to 93.43.5.102 port 443 after 21033 ms: Couldn't connect to server, Risposta:
|
||||
2025-06-16 11:42:03 - Errore nella richiesta: Recv failure: Connection was reset
|
||||
2025-07-04 10:42:49 - Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
|
||||
2025-07-04 10:44:13 - Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
|
||||
2025-07-04 10:48:07 - Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
|
||||
2025-08-19 16:29:25 - Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
|
||||
2025-08-26 16:47:19 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
|
||||
2025-08-26 16:48:15 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
|
||||
2025-08-26 16:48:44 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
|
||||
2025-08-26 16:49:24 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
|
||||
2025-08-26 16:50:23 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
|
||||
2025-09-08 08:39:17 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
|
||||
2026-01-27 15:33:53 - Errore nel recupero dati: HTTP 404, Risposta:
|
||||
2026-01-27 15:34:06 - Errore nel recupero dati: HTTP 404, Risposta:
|
||||
2026-01-27 15:34:10 - Errore nel recupero dati: HTTP 404, Risposta:
|
||||
2026-01-27 15:35:13 - Errore nel recupero dati: HTTP 404, Risposta:
|
||||
2026-01-29 14:33:38 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
|
||||
2026-01-29 14:33:39 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
|
||||
2026-01-29 14:34:04 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
|
||||
2026-01-29 14:37:29 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
|
||||
2026-01-29 14:41:55 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
|
||||
2026-01-29 14:42:03 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
|
||||
2026-01-29 14:42:52 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
|
||||
2026-01-29 14:43:00 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
|
||||
2026-01-30 10:50:43 [AnagraficaCertestService] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
|
||||
2026-01-30 10:50:43 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
|
||||
2026-01-30 11:09:22 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
|
||||
2026-02-19 10:00:13 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
|
||||
2026-02-19 10:00:42 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
|
||||
2026-02-19 10:00:44 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
|
||||
2026-02-23 10:44:04 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:42 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:42 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:42 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:42 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:42 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:42 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:42 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:42 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:42 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:43 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:43 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:43 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:52 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:52 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:52 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:52 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:52 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:52 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:52 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:52 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:52 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:52 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:52 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:44:53 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:48:26 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:48:26 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:48:27 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:48:27 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:48:27 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:49:04 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:49:04 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:49:04 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:49:05 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 10:49:05 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 14:26:52 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 14:26:59 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 15:54:16 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 16:22:45 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-23 16:32:17 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
|
||||
<HTML><HEAD><TITLE>Not Found</TITLE>
|
||||
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
|
||||
<BODY><h2>Not Found</h2>
|
||||
<hr><p>HTTP Error 404. The requested resource is not found.</p>
|
||||
</BODY></HTML>
|
||||
|
||||
2026-02-25 15:23:12 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
|
||||
2026-02-26 15:01:32 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
|
||||
2026-02-28 21:01:27 [AnagraficaCertestService] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
|
||||
2026-03-01 19:11:58 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
|
||||
2026-03-18 15:51:45 [AnagraficaCertestService] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
|
||||
2026-03-19 09:50:34 [AnagraficaCertestService] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
|
||||
2026-03-25 14:13:34 - Autenticazione fallita: HTTP 503, Errore cURL: , Risposta: The service is unavailable.
|
||||
2026-03-25 14:13:34 - Autenticazione fallita: HTTP 503, Errore cURL: , Risposta: The service is unavailable.
|
||||
2026-03-26 10:57:18 [AnagraficaCertestObject] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
|
||||
2026-03-26 10:57:21 [AnagraficaCertestObject] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
|
||||
2026-03-26 10:57:21 [AnagraficaCertestService] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
|
||||
2026-03-26 10:57:21 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
|
||||
2026-03-26 10:57:36 [AnagraficaCertestObject] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
|
||||
2026-03-26 10:57:36 [AnagraficaCertestService] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
|
||||
2026-03-26 10:57:36 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
|
||||
2026-03-26 10:57:41 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
|
||||
2026-03-26 10:57:56 [AnagraficaCertestObject] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
|
||||
2026-03-26 10:58:11 [AnagraficaCertestService] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
|
||||
2026-04-30 15:01:25 - Errore nel recupero dati: HTTP 404, Risposta: {"title":"Not Found","status":404,"detail":"Not Found","instance":"GET /api/odata/Rapporto(2621521)","errorCode":"d25cbd678"}
|
||||
2026-04-30 15:02:04 - Errore nel recupero dati: HTTP 404, Risposta:
|
||||
2026-04-30 15:03:19 - Errore nella richiesta: URL rejected: Malformed input to a URL function
|
||||
@@ -0,0 +1,363 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
|
||||
$template_id = intval($_GET['id'] ?? 0);
|
||||
if (!$template_id) {
|
||||
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Template ID mancante"));
|
||||
exit;
|
||||
}
|
||||
|
||||
$user_id = $iduserlogin ?? 1;
|
||||
$show_all_users = isset($_GET['all_users']) && $_GET['all_users'] == '1';
|
||||
$allowedLimits = [20, 40, 60, 100];
|
||||
$rawLimit = (int)($_GET['limit'] ?? 20);
|
||||
$perPage = in_array($rawLimit, $allowedLimits) ? $rawLimit : 20;
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
$logDir = __DIR__ . '/logs/api/';
|
||||
|
||||
// Estrae un riassunto breve dell'errore dal file di log della commessa
|
||||
function extractErrorSummary(string $logDir, ?string $logFile): string
|
||||
{
|
||||
if (empty($logFile)) return '';
|
||||
$path = $logDir . basename($logFile);
|
||||
if (!is_file($path)) return '';
|
||||
$content = @file_get_contents($path);
|
||||
if ($content === false || $content === '') return '';
|
||||
|
||||
// 1) prova col "detail" del JSON del 500
|
||||
if (preg_match('/"detail"\s*:\s*"([^"]+)"/', $content, $m)) {
|
||||
return trim(stripcslashes($m[1]));
|
||||
}
|
||||
// 2) fallback: ultima riga MESSAGE:
|
||||
if (preg_match_all('/^MESSAGE:\s*(.+)$/m', $content, $m) && !empty($m[1])) {
|
||||
return trim(end($m[1]));
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// Query: solo le righe in errore per questo template
|
||||
$userFilter = $show_all_users ? '' : 'AND d.user_id = ?';
|
||||
$baseWhere = "WHERE d.templateid = ? AND d.status = 'x' {$userFilter}";
|
||||
$baseParams = [$template_id];
|
||||
if (!$show_all_users) $baseParams[] = $user_id;
|
||||
|
||||
$countStmt = $pdo->prepare("SELECT COUNT(*) FROM datadb d {$baseWhere}");
|
||||
$countStmt->execute($baseParams);
|
||||
$totalRows = (int)$countStmt->fetchColumn();
|
||||
$totalPages = max(1, (int)ceil($totalRows / $perPage));
|
||||
if ($page > $totalPages) $page = $totalPages;
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT d.*, CONCAT(u.first_name, ' ', u.last_name) AS user_name
|
||||
FROM datadb d
|
||||
LEFT JOIN auth_users u ON d.user_id = u.id
|
||||
{$baseWhere}
|
||||
ORDER BY d.export_date DESC, d.iddatadb DESC
|
||||
LIMIT {$perPage} OFFSET " . (($page - 1) * $perPage) . "
|
||||
");
|
||||
$stmt->execute($baseParams);
|
||||
$rowsData = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
<?php include('cssinclude.php'); ?>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<style>
|
||||
.err-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.err-table th,
|
||||
.err-table td {
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.err-table th {
|
||||
background: #f8f9fa;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.err-table tr:hover {
|
||||
background: #fff8f8;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.status-x {
|
||||
background: #dc3545;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.err-msg {
|
||||
color: #b02a37;
|
||||
max-width: 420px;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.commessa-code {
|
||||
font-size: 0.8em;
|
||||
color: #555;
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 5px 8px;
|
||||
margin-right: 4px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-resend {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.btn-log {
|
||||
background: #17a2b8;
|
||||
}
|
||||
|
||||
.btn-revert {
|
||||
background: #fd7e14;
|
||||
}
|
||||
|
||||
.pager-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.pager-num,
|
||||
.pager-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
padding: 0 6px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
color: #495057;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.pager-num.active {
|
||||
background: #0d6efd;
|
||||
color: #fff;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
</style>
|
||||
<title>Export Errors - <?= htmlspecialchars($titlewebsite ?? '', ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<div class="mb-3 d-flex align-items-center gap-2">
|
||||
<a href="imported.php?id=<?= $template_id ?>" class="btn btn-warning me-2">Imported (i)</a>
|
||||
<a href="tolims.php?id=<?= $template_id ?>" class="btn btn-success me-2">To LIMS (l)</a>
|
||||
<a href="errors.php?id=<?= $template_id ?>" class="btn btn-danger">Errors (x)</a>
|
||||
<span class="ms-3">
|
||||
<label class="form-check-label" style="font-size:13px; cursor:pointer;">
|
||||
<?php $tp = $_GET;
|
||||
unset($tp['all_users']);
|
||||
$toggleBase = 'errors.php?' . http_build_query($tp); ?>
|
||||
<input type="checkbox" class="form-check-input" <?= $show_all_users ? 'checked' : '' ?>
|
||||
onchange="window.location.href='<?= htmlspecialchars($toggleBase) ?>' + (this.checked ? '&all_users=1' : '')">
|
||||
Show all users
|
||||
</label>
|
||||
</span>
|
||||
<span class="text-muted" style="font-size:12px;">(<?= count($rowsData) ?> of <?= $totalRows ?> errori<?= !$show_all_users ? ' — my records' : '' ?>)</span>
|
||||
</div>
|
||||
|
||||
<?php if ($totalPages > 1):
|
||||
$pageQuery = $_GET;
|
||||
unset($pageQuery['page']);
|
||||
$pageBase = 'errors.php?' . http_build_query($pageQuery); ?>
|
||||
<div class="pager-bar">
|
||||
<span>Pagina <?= $page ?> di <?= $totalPages ?> — <?= $totalRows ?> righe</span>
|
||||
<span>
|
||||
<?php for ($p = 1; $p <= $totalPages; $p++): ?>
|
||||
<a href="<?= htmlspecialchars($pageBase . '&page=' . $p) ?>" class="pager-num <?= $p === $page ? 'active' : '' ?>"><?= $p ?></a>
|
||||
<?php endfor; ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<span style="font-weight:600;"><i class="fas fa-triangle-exclamation" style="color:#dc3545;"></i> Export falliti verso il LIMS</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (empty($rowsData)): ?>
|
||||
<p class="text-muted mb-0">Nessun errore di export. 🎉</p>
|
||||
<?php else: ?>
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="err-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:170px;">Azioni</th>
|
||||
<th>Status</th>
|
||||
<th>Client</th>
|
||||
<th>Errore</th>
|
||||
<th style="width:150px;">Data export</th>
|
||||
<th>File import</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rowsData as $row):
|
||||
$errMsg = extractErrorSummary($logDir, $row['log_file'] ?? null); ?>
|
||||
<tr data-id="<?= (int)$row['iddatadb'] ?>">
|
||||
<td>
|
||||
<button type="button" class="action-btn btn-resend resend-btn" data-iddatadb="<?= (int)$row['iddatadb'] ?>" title="Reinvia al LIMS"><i class="fas fa-paper-plane"></i></button>
|
||||
<?php if (!empty($row['log_file'])): ?>
|
||||
<a href="download_logs.php?iddatadb=<?= (int)$row['iddatadb'] ?>" class="action-btn btn-log" title="Scarica log"><i class="fas fa-file-download"></i></a>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="action-btn btn-revert revert-btn" data-iddatadb="<?= (int)$row['iddatadb'] ?>" title="Riporta a Imported"><i class="fas fa-rotate-left"></i></button>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge status-x">Error</span>
|
||||
<?php if (!empty($row['commessaweb'])): ?>
|
||||
<span class="commessa-code"><?= htmlspecialchars($row['commessaweb']) ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><span class="client-text" data-client-id="<?= htmlspecialchars((string)($row['idclient'] ?? '')) ?>"><?= htmlspecialchars((string)($row['idclient'] ?? '')) ?></span></td>
|
||||
<td class="err-msg"><?= htmlspecialchars($errMsg ?: '—') ?></td>
|
||||
<td><?= htmlspecialchars((string)($row['export_date'] ?? '')) ?></td>
|
||||
<td><?php if (!empty($row['filename_import'])): ?><a href="imported_trf/<?= htmlspecialchars($row['filename_import']) ?>" target="_blank">File</a><?php endif; ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay toggle-icon"></div>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
<?php include('jsinclude.php'); ?>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script>
|
||||
$(function() {
|
||||
// Risoluzione nomi cliente (come in tolims.php)
|
||||
(function() {
|
||||
const ids = new Set();
|
||||
document.querySelectorAll('.client-text').forEach(el => {
|
||||
const id = el.dataset.clientId;
|
||||
if (id) ids.add(id);
|
||||
});
|
||||
ids.forEach(id => {
|
||||
fetch('search_clienti.php?id=' + encodeURIComponent(id))
|
||||
.then(r => r.json())
|
||||
.then(j => {
|
||||
const it = (j.results || [])[0];
|
||||
if (it) document.querySelectorAll('.client-text[data-client-id="' + id + '"]').forEach(el => el.textContent = it.text);
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
})();
|
||||
|
||||
// Reinvia: richiama export_to_lims.php da capo (create incluso)
|
||||
$(document).on('click', '.resend-btn', function() {
|
||||
const $btn = $(this);
|
||||
const iddatadb = $btn.data('iddatadb');
|
||||
if (!confirm('Reinviare questa commessa al LIMS? Verrà creata una nuova commessa nel LIMS.')) return;
|
||||
|
||||
const original = $btn.html();
|
||||
$btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
|
||||
|
||||
$.ajax({
|
||||
url: 'export_to_lims.php',
|
||||
method: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
iddatadb: iddatadb
|
||||
},
|
||||
success: function(res) {
|
||||
if (res && res.success) {
|
||||
const $tr = $('tr[data-id="' + iddatadb + '"]');
|
||||
$tr.css('background', '#d4edda');
|
||||
setTimeout(() => $tr.fadeOut(300, () => $tr.remove()), 500);
|
||||
} else {
|
||||
alert('Ancora errore: ' + ((res && res.message) ? res.message : 'invio non riuscito'));
|
||||
$btn.prop('disabled', false).html(original);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Errore di rete durante il reinvio.');
|
||||
$btn.prop('disabled', false).html(original);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Revert a Imported (i)
|
||||
$(document).on('click', '.revert-btn', function() {
|
||||
const $btn = $(this);
|
||||
const iddatadb = $btn.data('iddatadb');
|
||||
if (!confirm('Riportare questo record a Imported (i)?')) return;
|
||||
$.ajax({
|
||||
url: 'revert_status.php',
|
||||
method: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
iddatadb: iddatadb,
|
||||
status: 'i'
|
||||
},
|
||||
success: function(res) {
|
||||
if (res && res.success) {
|
||||
const $tr = $('tr[data-id="' + iddatadb + '"]');
|
||||
$tr.fadeOut(300, () => $tr.remove());
|
||||
} else {
|
||||
alert('Errore: ' + ((res && res.message) ? res.message : 'operazione non riuscita'));
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Errore di rete.');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -5,7 +5,7 @@
|
||||
* Single export + batch export (Export All) with validation.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
"use strict";
|
||||
|
||||
let pendingConfirmHandler = null;
|
||||
let batchRunning = false;
|
||||
@@ -17,19 +17,62 @@
|
||||
// ── Helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
function cleanupBackdrop() {
|
||||
document.querySelectorAll(".modal-backdrop").forEach(b => b.remove());
|
||||
document.querySelectorAll(".modal-backdrop").forEach((b) => b.remove());
|
||||
document.body.classList.remove("modal-open");
|
||||
document.body.style.paddingRight = "";
|
||||
const overlay = document.querySelector(".overlay.toggle-icon");
|
||||
if (overlay) overlay.style.display = "none";
|
||||
}
|
||||
|
||||
function escHtml(v) {
|
||||
return String(v ?? "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
// Badge "No price" con tooltip dei nomi analisi (vuoto se count = 0)
|
||||
function noPriceBadgeHTML(count, names) {
|
||||
const n = parseInt(count, 10) || 0;
|
||||
if (n <= 0) return "";
|
||||
const title = (names || []).join("\n");
|
||||
return `<span class="badge bg-danger no-price-badge" title="${escHtml(title)}" style="margin-left:6px; font-size:0.85em; cursor:help; vertical-align:middle;"><i class="fas fa-exclamation-triangle"></i> No price (${n})</span>`;
|
||||
}
|
||||
window.noPriceBadgeHTML = noPriceBadgeHTML;
|
||||
|
||||
function getGridRow(iddatadb) {
|
||||
return document.querySelector(`.grid-row[data-id="${iddatadb}"]`);
|
||||
}
|
||||
|
||||
function getRowIndexByIddatadb(iddatadb) {
|
||||
return (window.gridData || []).findIndex(r => String(r.iddatadb) === String(iddatadb));
|
||||
return (window.gridData || []).findIndex(
|
||||
(r) => String(r.iddatadb) === String(iddatadb),
|
||||
);
|
||||
}
|
||||
|
||||
// Invia la mail di riepilogo per gli iddatadb processati (fire-and-forget).
|
||||
// Una sola chiamata a fine export (singolo o batch).
|
||||
function sendExportSummaryMail(iddatadbList) {
|
||||
const ids = (iddatadbList || [])
|
||||
.map((v) => parseInt(v, 10))
|
||||
.filter(Boolean);
|
||||
if (!ids.length) return;
|
||||
|
||||
const body = new URLSearchParams();
|
||||
ids.forEach((id) => body.append("ids[]", id));
|
||||
const tid = window.gridMeta && window.gridMeta.templateId;
|
||||
if (tid) body.append("template_id", tid);
|
||||
|
||||
fetch("send_export_summary.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: body.toString(),
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((res) => console.log("Summary mail:", res))
|
||||
.catch((err) => console.warn("Summary mail failed:", err));
|
||||
}
|
||||
|
||||
// ── Validation ──────────────────────────────────────────────────────
|
||||
@@ -40,21 +83,31 @@
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rows: rowsToValidate }),
|
||||
});
|
||||
if (!response.ok) throw new Error(`Validation HTTP error: ${response.status}`);
|
||||
if (!response.ok)
|
||||
throw new Error(`Validation HTTP error: ${response.status}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function clearValidationErrors() {
|
||||
// Clear from gridData
|
||||
(window.gridData || []).forEach(row => { delete row._validationErrors; delete row._exportError; });
|
||||
|
||||
document.querySelectorAll(".grid-cell.validation-error").forEach(cell => {
|
||||
cell.classList.remove("validation-error");
|
||||
cell.querySelectorAll(".input-validation-error").forEach(el => el.classList.remove("input-validation-error"));
|
||||
const tooltip = cell.querySelector(".validation-tooltip");
|
||||
if (tooltip) tooltip.remove();
|
||||
(window.gridData || []).forEach((row) => {
|
||||
delete row._validationErrors;
|
||||
delete row._exportError;
|
||||
});
|
||||
document.querySelectorAll(".grid-row.validation-row-error").forEach(row => row.classList.remove("validation-row-error"));
|
||||
|
||||
document
|
||||
.querySelectorAll(".grid-cell.validation-error")
|
||||
.forEach((cell) => {
|
||||
cell.classList.remove("validation-error");
|
||||
cell.querySelectorAll(".input-validation-error").forEach((el) =>
|
||||
el.classList.remove("input-validation-error"),
|
||||
);
|
||||
const tooltip = cell.querySelector(".validation-tooltip");
|
||||
if (tooltip) tooltip.remove();
|
||||
});
|
||||
document
|
||||
.querySelectorAll(".grid-row.validation-row-error")
|
||||
.forEach((row) => row.classList.remove("validation-row-error"));
|
||||
clearAllRowErrors();
|
||||
}
|
||||
|
||||
@@ -67,7 +120,7 @@
|
||||
gridRow.classList.add("validation-row-error");
|
||||
const messages = [];
|
||||
|
||||
errors.forEach(err => {
|
||||
errors.forEach((err) => {
|
||||
messages.push(err.message);
|
||||
if (!err.field) return;
|
||||
|
||||
@@ -76,19 +129,26 @@
|
||||
const label = err.field.substring("field_label:".length);
|
||||
const headers = document.querySelectorAll(".grid-header");
|
||||
let targetIndex = null;
|
||||
headers.forEach(h => {
|
||||
if (h.textContent.trim() === label) targetIndex = h.getAttribute("data-index");
|
||||
headers.forEach((h) => {
|
||||
if (h.textContent.trim() === label)
|
||||
targetIndex = h.getAttribute("data-index");
|
||||
});
|
||||
if (targetIndex) {
|
||||
cell = gridRow.querySelector(`.grid-cell[data-index="${targetIndex}"]`);
|
||||
cell = gridRow.querySelector(
|
||||
`.grid-cell[data-index="${targetIndex}"]`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
cell = gridRow.querySelector(`.grid-cell[data-col="${err.field}"]`);
|
||||
cell = gridRow.querySelector(
|
||||
`.grid-cell[data-col="${err.field}"]`,
|
||||
);
|
||||
}
|
||||
|
||||
if (cell) {
|
||||
cell.classList.add("validation-error");
|
||||
cell.querySelectorAll("input, select").forEach(el => el.classList.add("input-validation-error"));
|
||||
cell.querySelectorAll("input, select").forEach((el) =>
|
||||
el.classList.add("input-validation-error"),
|
||||
);
|
||||
let tooltip = cell.querySelector(".validation-tooltip");
|
||||
if (!tooltip) {
|
||||
tooltip = document.createElement("div");
|
||||
@@ -109,37 +169,54 @@
|
||||
formData.append("iddatadb", iddatadb);
|
||||
if (batchUuid) formData.append("batch_uuid", batchUuid);
|
||||
|
||||
const response = await fetch("export_to_lims.php", { method: "POST", body: formData });
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
const response = await fetch("export_to_lims.php", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
if (!response.ok)
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Update gridData
|
||||
const idx = getRowIndexByIddatadb(iddatadb);
|
||||
if (idx >= 0) {
|
||||
window.gridData[idx].status = 'l';
|
||||
window.gridData[idx].status = "l";
|
||||
window.gridData[idx].commessaweb = data.commessaweb;
|
||||
window.gridData[idx].noPriceCount = data.noPriceCount || 0;
|
||||
window.gridData[idx].noPriceAnalyses =
|
||||
data.noPriceAnalyses || [];
|
||||
}
|
||||
|
||||
// Update visible DOM row
|
||||
const gridRow = getGridRow(iddatadb);
|
||||
if (gridRow) {
|
||||
const statusBadge = gridRow.querySelector('.grid-cell[data-col="status"] .status-badge');
|
||||
const statusBadge = gridRow.querySelector(
|
||||
'.grid-cell[data-col="status"] .status-badge',
|
||||
);
|
||||
if (statusBadge) {
|
||||
statusBadge.classList.remove("status-i", "status-P");
|
||||
statusBadge.classList.add("status-l");
|
||||
statusBadge.textContent = "To LIMS";
|
||||
}
|
||||
const statusCell = gridRow.querySelector('.grid-cell[data-col="status"]');
|
||||
const statusCell = gridRow.querySelector(
|
||||
'.grid-cell[data-col="status"]',
|
||||
);
|
||||
if (statusCell && data.commessaweb) {
|
||||
let cwSpan = statusCell.querySelector(".commessaweb-code");
|
||||
if (!cwSpan) {
|
||||
cwSpan = document.createElement("span");
|
||||
cwSpan.className = "commessaweb-code";
|
||||
cwSpan.style.cssText = "display:block; font-size:0.75em; color:#555; margin-top:2px;";
|
||||
cwSpan.style.cssText =
|
||||
"display:block; font-size:0.75em; color:#555; margin-top:2px;";
|
||||
statusCell.appendChild(cwSpan);
|
||||
}
|
||||
cwSpan.textContent = data.commessaweb;
|
||||
const npHtml = noPriceBadgeHTML(
|
||||
data.noPriceCount,
|
||||
data.noPriceAnalyses,
|
||||
);
|
||||
if (npHtml) cwSpan.insertAdjacentHTML("beforeend", npHtml);
|
||||
}
|
||||
const exportBtn = gridRow.querySelector(".export-lims-btn");
|
||||
if (exportBtn) {
|
||||
@@ -164,10 +241,20 @@
|
||||
const label = document.getElementById("exportResponseModalLabel");
|
||||
|
||||
if (data.success) {
|
||||
msg.innerHTML = `${data.message.replace(/\n/g, "<br>")}` +
|
||||
msg.innerHTML =
|
||||
`${data.message.replace(/\n/g, "<br>")}` +
|
||||
`<br>ID CommessaWeb: ${data.idcommessaweb}` +
|
||||
`<br>Codice CommessaWeb: ${data.commessaweb}` +
|
||||
(data.totalPhotos > 0 ? `<br>Foto: ${data.totalPhotos}` : "");
|
||||
`<br>Codice CommessaWeb: ${escHtml(data.commessaweb)}` +
|
||||
noPriceBadgeHTML(data.noPriceCount, data.noPriceAnalyses) +
|
||||
(data.totalPhotos > 0 ? `<br>Foto: ${data.totalPhotos}` : "") +
|
||||
(data.noPriceCount > 0
|
||||
? `<div class="mt-2 small text-danger fw-semibold">Analisi senza prezzo:</div>` +
|
||||
`<ul class="mb-0 small text-danger">` +
|
||||
(data.noPriceAnalyses || [])
|
||||
.map((n) => `<li>${escHtml(n)}</li>`)
|
||||
.join("") +
|
||||
`</ul>`
|
||||
: "");
|
||||
label.textContent = "Esportazione Completata";
|
||||
} else {
|
||||
msg.textContent = `Errore: ${data.message}`;
|
||||
@@ -186,10 +273,14 @@
|
||||
row.classList.remove("batch-disabled");
|
||||
row.classList.add("batch-exporting");
|
||||
if (btnCell) {
|
||||
btnCell.querySelectorAll(".action-btn").forEach(b => { b.dataset.prevDisplay = b.style.display; b.style.display = "none"; });
|
||||
btnCell.querySelectorAll(".action-btn").forEach((b) => {
|
||||
b.dataset.prevDisplay = b.style.display;
|
||||
b.style.display = "none";
|
||||
});
|
||||
const spinner = document.createElement("span");
|
||||
spinner.className = "batch-row-spinner";
|
||||
spinner.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Exporting...';
|
||||
spinner.innerHTML =
|
||||
'<i class="fas fa-spinner fa-spin"></i> Exporting...';
|
||||
btnCell.appendChild(spinner);
|
||||
}
|
||||
} else {
|
||||
@@ -197,7 +288,10 @@
|
||||
if (btnCell) {
|
||||
const spinner = btnCell.querySelector(".batch-row-spinner");
|
||||
if (spinner) spinner.remove();
|
||||
btnCell.querySelectorAll(".action-btn").forEach(b => { b.style.display = b.dataset.prevDisplay || ""; delete b.dataset.prevDisplay; });
|
||||
btnCell.querySelectorAll(".action-btn").forEach((b) => {
|
||||
b.style.display = b.dataset.prevDisplay || "";
|
||||
delete b.dataset.prevDisplay;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,32 +307,56 @@
|
||||
errorEl.className = "batch-error-msg";
|
||||
errorEl.textContent = "Warning — click for details";
|
||||
errorEl.addEventListener("click", () => {
|
||||
document.getElementById("exportResponseMessage").innerHTML = message.replace(/\n/g, "<br>");
|
||||
document.getElementById("exportResponseModalLabel").textContent = "Error (id: " + iddatadb + ")";
|
||||
new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show();
|
||||
document.getElementById("exportResponseMessage").innerHTML =
|
||||
message.replace(/\n/g, "<br>");
|
||||
document.getElementById(
|
||||
"exportResponseModalLabel",
|
||||
).textContent = "Error (id: " + iddatadb + ")";
|
||||
new bootstrap.Modal(
|
||||
document.getElementById("exportResponseModal"),
|
||||
{ keyboard: false },
|
||||
).show();
|
||||
});
|
||||
btnCell.appendChild(errorEl);
|
||||
}
|
||||
}
|
||||
|
||||
function clearAllRowErrors() {
|
||||
document.querySelectorAll(".grid-row.batch-row-error").forEach(row => {
|
||||
row.classList.remove("batch-row-error");
|
||||
const msg = row.querySelector(".batch-error-msg");
|
||||
if (msg) msg.remove();
|
||||
});
|
||||
document
|
||||
.querySelectorAll(".grid-row.batch-row-error")
|
||||
.forEach((row) => {
|
||||
row.classList.remove("batch-row-error");
|
||||
const msg = row.querySelector(".batch-error-msg");
|
||||
if (msg) msg.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function disableAllRowButtons() {
|
||||
document.querySelectorAll(".grid-row[data-id]").forEach(row => row.classList.add("batch-disabled"));
|
||||
const toggle = document.querySelector(".actions-dropdown .dropdown-toggle");
|
||||
if (toggle) { toggle.disabled = true; toggle.style.opacity = "0.5"; toggle.style.pointerEvents = "none"; }
|
||||
document
|
||||
.querySelectorAll(".grid-row[data-id]")
|
||||
.forEach((row) => row.classList.add("batch-disabled"));
|
||||
const toggle = document.querySelector(
|
||||
".actions-dropdown .dropdown-toggle",
|
||||
);
|
||||
if (toggle) {
|
||||
toggle.disabled = true;
|
||||
toggle.style.opacity = "0.5";
|
||||
toggle.style.pointerEvents = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function enableAllRowButtons() {
|
||||
document.querySelectorAll(".grid-row[data-id]").forEach(row => row.classList.remove("batch-disabled"));
|
||||
const toggle = document.querySelector(".actions-dropdown .dropdown-toggle");
|
||||
if (toggle) { toggle.disabled = false; toggle.style.opacity = ""; toggle.style.pointerEvents = ""; }
|
||||
document
|
||||
.querySelectorAll(".grid-row[data-id]")
|
||||
.forEach((row) => row.classList.remove("batch-disabled"));
|
||||
const toggle = document.querySelector(
|
||||
".actions-dropdown .dropdown-toggle",
|
||||
);
|
||||
if (toggle) {
|
||||
toggle.disabled = false;
|
||||
toggle.style.opacity = "";
|
||||
toggle.style.pointerEvents = "";
|
||||
}
|
||||
}
|
||||
|
||||
// ── Single row export: validate → confirm → send ────────────────────
|
||||
@@ -250,40 +368,61 @@
|
||||
if (gridRow) {
|
||||
setRowExporting(gridRow, true);
|
||||
const spinner = gridRow.querySelector(".batch-row-spinner");
|
||||
if (spinner) spinner.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Validating...';
|
||||
if (spinner)
|
||||
spinner.innerHTML =
|
||||
'<i class="fas fa-spinner fa-spin"></i> Validating...';
|
||||
}
|
||||
|
||||
validateRows([{ iddatadb: parseInt(iddatadb), index: rowIndex }])
|
||||
.then(validationData => {
|
||||
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
|
||||
.then((validationData) => {
|
||||
if (gridRow) {
|
||||
setRowExporting(gridRow, false);
|
||||
gridRow.classList.remove("batch-disabled");
|
||||
}
|
||||
|
||||
if (!validationData.success) {
|
||||
showExportResult({ success: false, message: validationData.message || "Validation error" });
|
||||
showExportResult({
|
||||
success: false,
|
||||
message: validationData.message || "Validation error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const result = validationData.results[rowIndex];
|
||||
if (result && !result.valid) {
|
||||
if (gridRow) showValidationErrors(gridRow, iddatadb, result.errors);
|
||||
if (gridRow)
|
||||
showValidationErrors(gridRow, iddatadb, result.errors);
|
||||
return;
|
||||
}
|
||||
showConfirmAndExport(iddatadb, rowIndex);
|
||||
})
|
||||
.catch(error => {
|
||||
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
|
||||
showExportResult({ success: false, message: "Validation error: " + error.message });
|
||||
.catch((error) => {
|
||||
if (gridRow) {
|
||||
setRowExporting(gridRow, false);
|
||||
gridRow.classList.remove("batch-disabled");
|
||||
}
|
||||
showExportResult({
|
||||
success: false,
|
||||
message: "Validation error: " + error.message,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showConfirmAndExport(iddatadb, rowIndex) {
|
||||
const confirmModalElement = document.getElementById("exportConfirmModal");
|
||||
const confirmModalElement =
|
||||
document.getElementById("exportConfirmModal");
|
||||
if (!confirmModalElement) return;
|
||||
|
||||
const confirmModal = new bootstrap.Modal(confirmModalElement, { keyboard: false });
|
||||
const confirmModal = new bootstrap.Modal(confirmModalElement, {
|
||||
keyboard: false,
|
||||
});
|
||||
document.getElementById("exportIddatadb").textContent = iddatadb;
|
||||
confirmModal.show();
|
||||
|
||||
const confirmBtn = document.getElementById("exportConfirmBtn");
|
||||
if (!confirmBtn) { confirmModal.hide(); return; }
|
||||
if (!confirmBtn) {
|
||||
confirmModal.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmHandler = async () => {
|
||||
pendingConfirmHandler = null;
|
||||
@@ -294,24 +433,38 @@
|
||||
|
||||
try {
|
||||
const data = await sendExport(iddatadb);
|
||||
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
|
||||
if (!data.success) showRowError(gridRow, iddatadb, data.message || "Unknown error");
|
||||
if (gridRow) {
|
||||
setRowExporting(gridRow, false);
|
||||
gridRow.classList.remove("batch-disabled");
|
||||
}
|
||||
if (!data.success)
|
||||
showRowError(
|
||||
gridRow,
|
||||
iddatadb,
|
||||
data.message || "Unknown error",
|
||||
);
|
||||
showExportResult(data);
|
||||
// Mail di riepilogo per la singola commessa
|
||||
sendExportSummaryMail([iddatadb]);
|
||||
} catch (error) {
|
||||
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
|
||||
if (gridRow) {
|
||||
setRowExporting(gridRow, false);
|
||||
gridRow.classList.remove("batch-disabled");
|
||||
}
|
||||
showRowError(gridRow, iddatadb, error.message);
|
||||
showExportResult({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
if (pendingConfirmHandler) confirmBtn.removeEventListener("click", pendingConfirmHandler);
|
||||
if (pendingConfirmHandler)
|
||||
confirmBtn.removeEventListener("click", pendingConfirmHandler);
|
||||
pendingConfirmHandler = confirmHandler;
|
||||
confirmBtn.addEventListener("click", confirmHandler, { once: true });
|
||||
}
|
||||
|
||||
// ── Single row click (event delegation) ─────────────────────────────
|
||||
|
||||
$(document).on('click', '.export-lims-btn', function (e) {
|
||||
$(document).on("click", ".export-lims-btn", function (e) {
|
||||
e.preventDefault();
|
||||
if (batchRunning) return;
|
||||
|
||||
@@ -322,24 +475,34 @@
|
||||
// Check unsaved changes for this row
|
||||
const dataRow = window.gridData?.[rowIndex];
|
||||
if (dataRow && dataRow._dirty) {
|
||||
const unsavedModal = new bootstrap.Modal(document.getElementById("exportUnsavedModal"), { keyboard: false });
|
||||
const unsavedModal = new bootstrap.Modal(
|
||||
document.getElementById("exportUnsavedModal"),
|
||||
{ keyboard: false },
|
||||
);
|
||||
unsavedModal.show();
|
||||
|
||||
document.getElementById("saveAndExportBtn")?.addEventListener("click", () => {
|
||||
unsavedModal.hide();
|
||||
// Save first, then export
|
||||
const formData = window.buildSavePayload(rowIndex);
|
||||
fetch('save_edited_row.php', { method: 'POST', body: formData })
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
dataRow._dirty = false;
|
||||
startExportConfirmFlow(iddatadb, rowIndex);
|
||||
} else {
|
||||
alert('Save failed: ' + result.message);
|
||||
}
|
||||
});
|
||||
}, { once: true });
|
||||
document.getElementById("saveAndExportBtn")?.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
unsavedModal.hide();
|
||||
// Save first, then export
|
||||
const formData = window.buildSavePayload(rowIndex);
|
||||
fetch("save_edited_row.php", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((result) => {
|
||||
if (result.success) {
|
||||
dataRow._dirty = false;
|
||||
startExportConfirmFlow(iddatadb, rowIndex);
|
||||
} else {
|
||||
alert("Save failed: " + result.message);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -349,18 +512,33 @@
|
||||
// ── Batch export (Export All) ───────────────────────────────────────
|
||||
|
||||
function collectEligibleRows() {
|
||||
// Read from gridData, not DOM
|
||||
// Read from gridData, not DOM. Se i filtri sono attivi, limita al target set.
|
||||
const target =
|
||||
typeof window.getTargetGridIds === "function"
|
||||
? window.getTargetGridIds()
|
||||
: null;
|
||||
const eligible = [];
|
||||
(window.gridData || []).forEach((row, index) => {
|
||||
if (row.status !== 'l') {
|
||||
eligible.push({ iddatadb: row.iddatadb, index, row: getGridRow(row.iddatadb) });
|
||||
}
|
||||
if (row.status === "l") return;
|
||||
if (target && !target.has(parseInt(row.iddatadb, 10))) return;
|
||||
eligible.push({
|
||||
iddatadb: row.iddatadb,
|
||||
index,
|
||||
row: getGridRow(row.iddatadb),
|
||||
});
|
||||
});
|
||||
return eligible;
|
||||
}
|
||||
|
||||
function hasUnsavedChanges() {
|
||||
return (window.gridData || []).some(r => r._dirty);
|
||||
const target =
|
||||
typeof window.getTargetGridIds === "function"
|
||||
? window.getTargetGridIds()
|
||||
: null;
|
||||
return (window.gridData || []).some(
|
||||
(r) =>
|
||||
r._dirty && (!target || target.has(parseInt(r.iddatadb, 10))),
|
||||
);
|
||||
}
|
||||
|
||||
async function validateAndFilter(eligibleRows) {
|
||||
@@ -369,7 +547,8 @@
|
||||
index,
|
||||
}));
|
||||
const validationData = await validateRows(rowsToValidate);
|
||||
if (!validationData.success) throw new Error(validationData.message || "Validation error");
|
||||
if (!validationData.success)
|
||||
throw new Error(validationData.message || "Validation error");
|
||||
|
||||
const validRows = [];
|
||||
let invalidCount = 0;
|
||||
@@ -377,7 +556,12 @@
|
||||
for (const item of eligibleRows) {
|
||||
const result = validationData.results[item.index];
|
||||
if (result && !result.valid) {
|
||||
if (item.row) showValidationErrors(item.row, item.iddatadb, result.errors);
|
||||
if (item.row)
|
||||
showValidationErrors(
|
||||
item.row,
|
||||
item.iddatadb,
|
||||
result.errors,
|
||||
);
|
||||
invalidCount++;
|
||||
} else {
|
||||
validRows.push(item);
|
||||
@@ -410,54 +594,92 @@
|
||||
if (validRows.length === 0) {
|
||||
document.getElementById("exportResponseMessage").innerHTML =
|
||||
`No valid rows for export.<br><strong>${invalidCount}</strong> rows with validation errors.`;
|
||||
document.getElementById("exportResponseModalLabel").textContent = "Validation Failed";
|
||||
new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show();
|
||||
document.getElementById(
|
||||
"exportResponseModalLabel",
|
||||
).textContent = "Validation Failed";
|
||||
new bootstrap.Modal(
|
||||
document.getElementById("exportResponseModal"),
|
||||
{ keyboard: false },
|
||||
).show();
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmModal = new bootstrap.Modal(document.getElementById("exportBatchConfirmModal"), { keyboard: false });
|
||||
const confirmModal = new bootstrap.Modal(
|
||||
document.getElementById("exportBatchConfirmModal"),
|
||||
{ keyboard: false },
|
||||
);
|
||||
let countText = String(validRows.length);
|
||||
if (invalidCount > 0) countText += ` (${invalidCount} excluded due to errors)`;
|
||||
document.getElementById("exportBatchCount").textContent = countText;
|
||||
if (invalidCount > 0)
|
||||
countText += ` (${invalidCount} excluded due to errors)`;
|
||||
document.getElementById("exportBatchCount").textContent =
|
||||
countText;
|
||||
confirmModal.show();
|
||||
|
||||
const confirmBtn = document.getElementById("exportBatchConfirmBtn");
|
||||
if (pendingBatchConfirmHandler) confirmBtn.removeEventListener("click", pendingBatchConfirmHandler);
|
||||
const confirmBtn = document.getElementById(
|
||||
"exportBatchConfirmBtn",
|
||||
);
|
||||
if (pendingBatchConfirmHandler)
|
||||
confirmBtn.removeEventListener(
|
||||
"click",
|
||||
pendingBatchConfirmHandler,
|
||||
);
|
||||
pendingBatchConfirmHandler = () => {
|
||||
pendingBatchConfirmHandler = null;
|
||||
confirmModal.hide();
|
||||
startBatchExport(validRows);
|
||||
};
|
||||
confirmBtn.addEventListener("click", pendingBatchConfirmHandler, { once: true });
|
||||
confirmBtn.addEventListener(
|
||||
"click",
|
||||
pendingBatchConfirmHandler,
|
||||
{ once: true },
|
||||
);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
showValidationSpinner(false);
|
||||
document.getElementById("exportResponseMessage").textContent = "Validation error: " + error.message;
|
||||
document.getElementById("exportResponseModalLabel").textContent = "Validation Error";
|
||||
new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show();
|
||||
document.getElementById("exportResponseMessage").textContent =
|
||||
"Validation error: " + error.message;
|
||||
document.getElementById(
|
||||
"exportResponseModalLabel",
|
||||
).textContent = "Validation Error";
|
||||
new bootstrap.Modal(
|
||||
document.getElementById("exportResponseModal"),
|
||||
{ keyboard: false },
|
||||
).show();
|
||||
});
|
||||
}
|
||||
|
||||
$(document).on('click', '.export-all-lims-btn', function (e) {
|
||||
$(document).on("click", ".export-all-lims-btn", function (e) {
|
||||
e.preventDefault();
|
||||
if (batchRunning) return;
|
||||
|
||||
if (hasUnsavedChanges()) {
|
||||
const unsavedModal = new bootstrap.Modal(document.getElementById("exportBatchUnsavedModal"), { keyboard: false });
|
||||
const unsavedModal = new bootstrap.Modal(
|
||||
document.getElementById("exportBatchUnsavedModal"),
|
||||
{ keyboard: false },
|
||||
);
|
||||
unsavedModal.show();
|
||||
document.getElementById("batchSaveAndExportBtn")?.addEventListener("click", () => {
|
||||
unsavedModal.hide();
|
||||
// Trigger save all first — listen for completion
|
||||
alert("Please Save All first, then Export All.");
|
||||
}, { once: true });
|
||||
document.getElementById("batchSaveAndExportBtn")?.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
unsavedModal.hide();
|
||||
// Trigger save all first — listen for completion
|
||||
alert("Please Save All first, then Export All.");
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const eligibleRows = collectEligibleRows();
|
||||
if (eligibleRows.length === 0) {
|
||||
document.getElementById("exportResponseMessage").textContent = "All rows already exported to LIMS.";
|
||||
document.getElementById("exportResponseModalLabel").textContent = "Export All";
|
||||
new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show();
|
||||
document.getElementById("exportResponseMessage").textContent =
|
||||
"All rows already exported to LIMS.";
|
||||
document.getElementById("exportResponseModalLabel").textContent =
|
||||
"Export All";
|
||||
new bootstrap.Modal(
|
||||
document.getElementById("exportResponseModal"),
|
||||
{ keyboard: false },
|
||||
).show();
|
||||
return;
|
||||
}
|
||||
showBatchConfirm(eligibleRows);
|
||||
@@ -468,7 +690,11 @@
|
||||
batchRunning = true;
|
||||
const batchUuid = crypto.randomUUID();
|
||||
const total = eligibleRows.length;
|
||||
let processed = 0, succeeded = 0, failed = 0;
|
||||
let processed = 0,
|
||||
succeeded = 0,
|
||||
failed = 0;
|
||||
const exportedList = []; // { commessaweb, noPriceCount, noPriceAnalyses }
|
||||
const processedIds = []; // iddatadb processati (ok o errore) per la mail
|
||||
|
||||
disableAllRowButtons();
|
||||
|
||||
@@ -479,48 +705,75 @@
|
||||
if (cancelBtn) cancelBtn.disabled = false;
|
||||
if (statusEl) statusEl.textContent = `Exporting 0 / ${total}...`;
|
||||
|
||||
cancelBtn?.addEventListener("click", () => {
|
||||
batchCancelled = true;
|
||||
if (statusEl) statusEl.textContent = "Cancelling... (waiting for current row)";
|
||||
if (cancelBtn) cancelBtn.disabled = true;
|
||||
}, { once: true });
|
||||
cancelBtn?.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
batchCancelled = true;
|
||||
if (statusEl)
|
||||
statusEl.textContent =
|
||||
"Cancelling... (waiting for current row)";
|
||||
if (cancelBtn) cancelBtn.disabled = true;
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
|
||||
(async () => {
|
||||
for (let i = 0; i < eligibleRows.length; i++) {
|
||||
if (batchCancelled) break;
|
||||
|
||||
const { iddatadb, row } = eligibleRows[i];
|
||||
if (statusEl) statusEl.textContent = `Exporting ${processed + 1} / ${total} (id: ${iddatadb})...`;
|
||||
if (statusEl)
|
||||
statusEl.textContent = `Exporting ${processed + 1} / ${total} (id: ${iddatadb})...`;
|
||||
|
||||
const gridRow = row || getGridRow(iddatadb);
|
||||
if (gridRow) setRowExporting(gridRow, true);
|
||||
|
||||
processedIds.push(iddatadb);
|
||||
try {
|
||||
const data = await sendExport(iddatadb, batchUuid);
|
||||
processed++;
|
||||
if (data.success) {
|
||||
succeeded++;
|
||||
exportedList.push({
|
||||
commessaweb: data.commessaweb,
|
||||
noPriceCount: data.noPriceCount || 0,
|
||||
noPriceAnalyses: data.noPriceAnalyses || [],
|
||||
});
|
||||
} else {
|
||||
failed++;
|
||||
const errIdx = getRowIndexByIddatadb(iddatadb);
|
||||
if (errIdx >= 0) window.gridData[errIdx]._exportError = data.message || "Unknown error";
|
||||
if (gridRow) showRowError(gridRow, iddatadb, data.message || "Unknown error");
|
||||
if (errIdx >= 0)
|
||||
window.gridData[errIdx]._exportError =
|
||||
data.message || "Unknown error";
|
||||
if (gridRow)
|
||||
showRowError(
|
||||
gridRow,
|
||||
iddatadb,
|
||||
data.message || "Unknown error",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
processed++;
|
||||
failed++;
|
||||
const errIdx = getRowIndexByIddatadb(iddatadb);
|
||||
if (errIdx >= 0) window.gridData[errIdx]._exportError = error.message;
|
||||
if (errIdx >= 0)
|
||||
window.gridData[errIdx]._exportError = error.message;
|
||||
if (gridRow) showRowError(gridRow, iddatadb, error.message);
|
||||
}
|
||||
|
||||
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
|
||||
if (gridRow) {
|
||||
setRowExporting(gridRow, false);
|
||||
gridRow.classList.remove("batch-disabled");
|
||||
}
|
||||
}
|
||||
|
||||
batchRunning = false;
|
||||
enableAllRowButtons();
|
||||
if (bar) bar.style.display = "none";
|
||||
|
||||
// Mail di riepilogo unica per tutto il batch processato
|
||||
sendExportSummaryMail(processedIds);
|
||||
|
||||
// Re-render to reflect status changes, then restore error indicators
|
||||
const gr = window.gridRenderer;
|
||||
if (gr) gr.renderVisibleRows();
|
||||
@@ -529,7 +782,8 @@
|
||||
(window.gridData || []).forEach((row, idx) => {
|
||||
if (row._exportError) {
|
||||
const gridRow = getGridRow(row.iddatadb);
|
||||
if (gridRow) showRowError(gridRow, row.iddatadb, row._exportError);
|
||||
if (gridRow)
|
||||
showRowError(gridRow, row.iddatadb, row._exportError);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -547,9 +801,36 @@
|
||||
msgEl.innerHTML = `Exported: <strong>${succeeded}</strong><br>Errors: <strong>${failed}</strong>`;
|
||||
}
|
||||
|
||||
if (exportedList.length) {
|
||||
const npCommesse = exportedList.filter(
|
||||
(x) => x.noPriceCount > 0,
|
||||
).length;
|
||||
msgEl.innerHTML +=
|
||||
`<hr class="my-2">` +
|
||||
(npCommesse > 0
|
||||
? `<div class="small text-danger fw-semibold mb-1"><i class="fas fa-exclamation-triangle"></i> ${npCommesse} commesse con analisi senza prezzo</div>`
|
||||
: "") +
|
||||
`<div class="small fw-semibold mb-1">Commesse esportate:</div>` +
|
||||
`<ul class="mb-0 ps-3">` +
|
||||
exportedList
|
||||
.map(
|
||||
(x) =>
|
||||
`<li><span style="font-family:monospace;">${escHtml(x.commessaweb || "—")}</span>` +
|
||||
noPriceBadgeHTML(
|
||||
x.noPriceCount,
|
||||
x.noPriceAnalyses,
|
||||
) +
|
||||
`</li>`,
|
||||
)
|
||||
.join("") +
|
||||
`</ul>`;
|
||||
}
|
||||
|
||||
const modalEl = document.getElementById("exportResponseModal");
|
||||
new bootstrap.Modal(modalEl, { keyboard: false }).show();
|
||||
modalEl.addEventListener("hidden.bs.modal", cleanupBackdrop, { once: true });
|
||||
modalEl.addEventListener("hidden.bs.modal", cleanupBackdrop, {
|
||||
once: true,
|
||||
});
|
||||
})();
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -15,28 +15,69 @@ 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/';
|
||||
|
||||
// 🔹 Batch UUID — if present, all logs go to a single file
|
||||
$batchUuid = $_POST['batch_uuid'] ?? null;
|
||||
|
||||
$writeLog = (function () use ($batchUuid, $logDir) {
|
||||
$batchLogFile = $batchUuid ? $logDir . "batch_export_{$batchUuid}.log" : null;
|
||||
// File unico per questa commessa (nominato per iddatadb, che conosciamo sempre)
|
||||
$iddatadbForLog = $_POST['iddatadb'] ?? 'unknown';
|
||||
$commessaLogFile = $logDir . "commessa_{$iddatadbForLog}_" . time() . ".log";
|
||||
|
||||
return function ($individualPath, $content, $stepLabel = null) use ($batchLogFile) {
|
||||
if ($batchLogFile) {
|
||||
$header = "\n" . str_repeat("=", 60) . "\n";
|
||||
if ($stepLabel) {
|
||||
$header .= "[{$stepLabel}] " . date('Y-m-d H:i:s') . "\n";
|
||||
}
|
||||
$header .= str_repeat("=", 60) . "\n";
|
||||
file_put_contents($batchLogFile, $header . $content . "\n", FILE_APPEND);
|
||||
} else {
|
||||
file_put_contents($individualPath, $content);
|
||||
}
|
||||
};
|
||||
})();
|
||||
// File di lotto: resta come INDICE (quali commesse nel lotto), non più tutti gli step
|
||||
$batchIndexFile = $batchUuid ? $logDir . "batch_export_{$batchUuid}.log" : null;
|
||||
|
||||
// $writeLog ora appende sempre sul file unico della commessa, con header di step
|
||||
$writeLog = function ($individualPath, $content, $stepLabel = null) use ($commessaLogFile) {
|
||||
$header = "\n" . str_repeat("=", 60) . "\n";
|
||||
if ($stepLabel) {
|
||||
$header .= "[{$stepLabel}] " . date('Y-m-d H:i:s') . "\n";
|
||||
}
|
||||
$header .= str_repeat("=", 60) . "\n";
|
||||
file_put_contents($commessaLogFile, $header . $content . "\n", FILE_APPEND);
|
||||
};
|
||||
|
||||
// Helper per registrare una riga nell'indice di lotto
|
||||
$writeBatchIndex = function ($line) use ($batchIndexFile) {
|
||||
if ($batchIndexFile) {
|
||||
file_put_contents($batchIndexFile, "[" . date('Y-m-d H:i:s') . "] " . $line . "\n", FILE_APPEND);
|
||||
}
|
||||
};
|
||||
|
||||
// 🔹 Funzione per validare e convertire date
|
||||
function validateDate($value)
|
||||
@@ -116,13 +157,15 @@ 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,
|
||||
et.template_note,
|
||||
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
|
||||
@@ -138,6 +181,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;
|
||||
@@ -146,11 +190,12 @@ 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
|
||||
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
|
||||
@@ -176,6 +221,7 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 🔹 STEP 4a: Auto-populate export_date / export_time fields
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE import_data_details idd
|
||||
@@ -199,7 +245,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
|
||||
@@ -219,6 +267,44 @@ 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'] ?? ''));
|
||||
});
|
||||
|
||||
// Valori "vuoti convenzionali" da NON includere nelle note.
|
||||
// Il cliente usa "/" (e simili) come placeholder di campo vuoto.
|
||||
$addnotePlaceholders = ['/', '-', 'n/a', 'na', 'n.a.', '//'];
|
||||
|
||||
foreach ($addnoteRows as $r) {
|
||||
$val = trim((string)($r['field_value'] ?? ''));
|
||||
if ($val === '') {
|
||||
continue;
|
||||
}
|
||||
// Scarta i placeholder (confronto case-insensitive)
|
||||
if (in_array(mb_strtolower($val), $addnotePlaceholders, true)) {
|
||||
continue;
|
||||
}
|
||||
$addnoteChunks[] = $val;
|
||||
}
|
||||
|
||||
$addnoteText = implode(' - ', $addnoteChunks);
|
||||
|
||||
// Nota del template: appesa in fondo alle note in esportazione
|
||||
$templateNote = trim((string)($result['template_note'] ?? ''));
|
||||
if ($templateNote !== '') {
|
||||
$addnoteText = $addnoteText !== ''
|
||||
? $addnoteText . ' - ' . $templateNote
|
||||
: $templateNote;
|
||||
}
|
||||
|
||||
// Logga i fieldValues in error_log
|
||||
$logFieldValues = "FieldValues dal DB (iddatadb={$iddatadb}):\n" . json_encode($fieldValues, JSON_PRETTY_PRINT);
|
||||
error_log($logFieldValues);
|
||||
@@ -240,6 +326,7 @@ try {
|
||||
"AnagraficaCertestService" => $anagraficaService,
|
||||
"ClienteFornitore" => $clienteFornitore, // PLACEHOLDER — to be implemented
|
||||
"ClienteAnalisi" => $clienteAnalisi, // PLACEHOLDER — to be implemented
|
||||
"ClienteFatturazione" => $clienteFatturazione, // ⚠️ TEMP DISABLED - problemi API lato LIMS
|
||||
// DeliveryRequest goes to Campione, not CommessaWeb
|
||||
];
|
||||
|
||||
@@ -272,13 +359,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,
|
||||
];
|
||||
|
||||
@@ -420,11 +527,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;
|
||||
}
|
||||
|
||||
@@ -477,6 +583,82 @@ 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, analysis_price
|
||||
FROM identification_parts_analyses
|
||||
WHERE iddatadb = :iddatadb
|
||||
ORDER BY part_id, id
|
||||
");
|
||||
$stmt->execute(['iddatadb' => $iddatadb]);
|
||||
$analysesRows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Analisi senza prezzo (NULL o 0), una sola volta per recordkey
|
||||
$noPriceAnalyses = [];
|
||||
foreach ($analysesRows as $a) {
|
||||
$price = $a['analysis_price'];
|
||||
if ($price === null || (float)$price <= 0) {
|
||||
$rk = (string)($a['analysis_recordkey'] ?? '');
|
||||
$noPriceAnalyses[$rk] = $a['analysis_name'] ?: 'Unnamed analysis';
|
||||
}
|
||||
}
|
||||
$noPriceAnalyses = array_values($noPriceAnalyses);
|
||||
|
||||
$partIdToIndex = [];
|
||||
foreach ($parts as $idx => $part) {
|
||||
$partIdToIndex[(int)$part['part_id']] = $idx;
|
||||
}
|
||||
|
||||
$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
|
||||
@@ -529,16 +711,20 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
// 🔹 STEP 8: Update datadb with idcommessaweb, commessaweb, and status
|
||||
// 🔹 STEP 8: Update datadb with idcommessaweb, commessaweb, log_file and export date
|
||||
// NB: lo status 'l' viene impostato SOLO dopo invio+import ok (vedi più sotto),
|
||||
// così una riga rimasta indietro non risulta "To LIMS" per errore.
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE datadb
|
||||
SET idcommessaweb = :idcommessaweb, commessaweb = :commessaweb, status = 'l'
|
||||
SET idcommessaweb = :idcommessaweb, commessaweb = :commessaweb,
|
||||
log_file = :log_file, export_date = NOW()
|
||||
WHERE iddatadb = :iddatadb
|
||||
");
|
||||
$stmt->execute([
|
||||
'idcommessaweb' => $commessaId,
|
||||
'commessaweb' => $commessaWebCode,
|
||||
'iddatadb' => $iddatadb
|
||||
'commessaweb' => $commessaWebCode,
|
||||
'log_file' => basename($commessaLogFile),
|
||||
'iddatadb' => $iddatadb
|
||||
]);
|
||||
|
||||
// 🔹 STEP 9: Send CommessaWeb to laboratory (commentato come richiesto)
|
||||
@@ -606,9 +792,12 @@ try {
|
||||
");
|
||||
$stmt->execute([
|
||||
'commessaweb' => substr($finalCodiceCommessa, 0, 30),
|
||||
'iddatadb' => $iddatadb
|
||||
'iddatadb' => $iddatadb
|
||||
]);
|
||||
}
|
||||
|
||||
// Indice di lotto: esito OK di questa commessa
|
||||
$writeBatchIndex("OK iddatadb={$iddatadb} commessa={$commessaId} codice={$finalCodiceCommessa} log=" . basename($commessaLogFile));
|
||||
// 🔹 STEP 11: Prepare final response
|
||||
$finalCommessa = [
|
||||
"Cliente" => $clienteId,
|
||||
@@ -629,26 +818,71 @@ try {
|
||||
"totalCampioni" => count($campioni),
|
||||
"totalCustomFields" => count($commessaAfterPatch["CommesseCustomFields"] ?? []),
|
||||
"totalPhotos" => count($photos),
|
||||
"totalAnalyses" => $totalAnalyses,
|
||||
"addedAnalyses" => $addedAnalyses,
|
||||
"failedAnalyses" => $failedAnalyses,
|
||||
"noPriceCount" => count($noPriceAnalyses),
|
||||
"noPriceAnalyses" => $noPriceAnalyses,
|
||||
"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
|
||||
]
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
// --- LOG ERRORE DEDICATO (accanto agli altri log in logs/api/) ---
|
||||
$errIddatadb = $iddatadb ?? 'unknown';
|
||||
$errCommessaId = $commessaId ?? null;
|
||||
|
||||
$errContent = "TIMESTAMP: " . date('Y-m-d H:i:s') . "\n";
|
||||
$errContent .= "iddatadb: {$errIddatadb}\n";
|
||||
$errContent .= "IdCommessa: " . ($errCommessaId ?? 'non ancora creata') . "\n";
|
||||
$errContent .= "MESSAGE: " . $e->getMessage() . "\n\n";
|
||||
$errContent .= "TRACE:\n" . $e->getTraceAsString() . "\n";
|
||||
|
||||
// Appendi l'errore in coda al file unico della commessa (se esiste),
|
||||
// così resta tutto in un posto solo.
|
||||
if (isset($commessaLogFile)) {
|
||||
$header = "\n" . str_repeat("=", 60) . "\n[ERROR] " . date('Y-m-d H:i:s') . "\n" . str_repeat("=", 60) . "\n";
|
||||
@file_put_contents($commessaLogFile, $header . $errContent . "\n", FILE_APPEND);
|
||||
$errFileToSave = basename($commessaLogFile);
|
||||
} else {
|
||||
// fallback: file d'errore dedicato
|
||||
$errName = "commessa_ERROR_{$errIddatadb}_" . time() . ".txt";
|
||||
@file_put_contents($logDir . $errName, $errContent);
|
||||
$errFileToSave = $errName;
|
||||
}
|
||||
|
||||
// Segna la riga come errore ('x') e registra il log_file, senza lasciarla 'l'
|
||||
if ($errIddatadb !== 'unknown') {
|
||||
try {
|
||||
$up = $pdo->prepare("UPDATE datadb SET status = 'x', log_file = :log_file WHERE iddatadb = :iddatadb");
|
||||
$up->execute(['log_file' => $errFileToSave, 'iddatadb' => $errIddatadb]);
|
||||
} catch (Exception $ignore) { /* non bloccare la risposta d'errore */
|
||||
}
|
||||
}
|
||||
|
||||
// Indice di lotto: esito KO di questa commessa
|
||||
if (isset($writeBatchIndex)) {
|
||||
$writeBatchIndex("FAIL iddatadb={$errIddatadb} commessa=" . ($errCommessaId ?? '-') . " log={$errFileToSave} :: " . $e->getMessage());
|
||||
}
|
||||
|
||||
error_log("LIMS Export Error: " . $e->getMessage() . "\nTrace: " . $e->getTraceAsString());
|
||||
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"message" => "Export failed: " . $e->getMessage(),
|
||||
"success" => false,
|
||||
"message" => "Export failed: " . $e->getMessage(),
|
||||
"errorLog" => $logDir . $errFileToSave,
|
||||
"logFiles" => [
|
||||
"step5_create" => $logFileStep5 ?? null,
|
||||
"step5_2_photos" => $logFilePhotos ?? null,
|
||||
"step6_campioni" => $logFileStep6 ?? null,
|
||||
"step63_analyses" => $logFileStep63Analisi ?? null,
|
||||
"step7_patch" => $logFileStep7 ?? null,
|
||||
"step9_1_importa" => $logFileStep91 ?? null,
|
||||
"step10_get" => $logFileStep10 ?? null
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
<?php
|
||||
|
||||
include('include/headscript.php');
|
||||
require_once __DIR__ . '/include/grid_data_builder.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
const FILTER_ALLOWED_PER_PAGE = [20, 40, 60, 100];
|
||||
const FILTER_ALLIDS_CAP = 5000;
|
||||
|
||||
$body = json_decode(file_get_contents('php://input'), true) ?: [];
|
||||
|
||||
$templateId = (int)($body['template_id'] ?? 0);
|
||||
if ($templateId <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'template_id mancante']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$status = in_array($body['status'] ?? 'i', ['i', 'P', 'l'], true) ? $body['status'] : 'i';
|
||||
$showAll = !empty($body['all_users']);
|
||||
$importref = trim((string)($body['importref'] ?? '')); // scope su un singolo import
|
||||
$filters = is_array($body['filters'] ?? null) ? $body['filters'] : [];
|
||||
$userId = (int)($iduserlogin ?? 0);
|
||||
$perPage = in_array((int)($body['per_page'] ?? 20), FILTER_ALLOWED_PER_PAGE, true) ? (int)$body['per_page'] : 20;
|
||||
$page = max(1, (int)($body['page'] ?? 1));
|
||||
$wantAllIds = !empty($body['want_all_ids']);
|
||||
|
||||
$cacheDir = __DIR__ . '/cache';
|
||||
|
||||
$explicitIds = [];
|
||||
if (isset($body['ids']) && is_array($body['ids'])) {
|
||||
$explicitIds = array_values(array_unique(array_filter(
|
||||
array_map('intval', $body['ids']),
|
||||
fn($v) => $v > 0
|
||||
)));
|
||||
}
|
||||
if (!empty($explicitIds)) {
|
||||
// Scoping di sicurezza: solo record del template/utente/import corretti.
|
||||
$conds = ['d.templateid = ?', 'd.status = ?'];
|
||||
$params = [$templateId, $status];
|
||||
if (!$showAll) {
|
||||
$conds[] = 'd.user_id = ?';
|
||||
$params[] = $userId;
|
||||
}
|
||||
if ($importref !== '') {
|
||||
$conds[] = 'd.importreferencecode = ?';
|
||||
$params[] = $importref;
|
||||
}
|
||||
$ph = implode(',', array_fill(0, count($explicitIds), '?'));
|
||||
$conds[] = "d.iddatadb IN ($ph)";
|
||||
foreach ($explicitIds as $id) $params[] = $id;
|
||||
$whereSql = implode(' AND ', $conds);
|
||||
|
||||
try {
|
||||
$countStmt = $pdo->prepare("SELECT COUNT(*) FROM datadb d WHERE {$whereSql}");
|
||||
$countStmt->execute($params);
|
||||
$total = (int)$countStmt->fetchColumn();
|
||||
$totalPages = max(1, (int)ceil($total / $perPage));
|
||||
if ($page > $totalPages) $page = $totalPages;
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$idStmt = $pdo->prepare("
|
||||
SELECT d.iddatadb FROM datadb d
|
||||
WHERE {$whereSql}
|
||||
ORDER BY CAST(d.excelrow AS UNSIGNED) ASC, d.excelrow ASC, d.iddatadb ASC
|
||||
LIMIT {$perPage} OFFSET {$offset}
|
||||
");
|
||||
$idStmt->execute($params);
|
||||
$pageIds = array_map('intval', $idStmt->fetchAll(PDO::FETCH_COLUMN));
|
||||
|
||||
$config = buildGridConfig($pdo, $templateId);
|
||||
$rows = buildGridRows($pdo, $pageIds, $config);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'total_pages' => $totalPages,
|
||||
'rows' => $rows,
|
||||
'mode' => 'selection',
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
error_log('filter_records (ids mode) error: ' . $e->getMessage());
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
$fixedAliasMap = [
|
||||
'ClienteResponsabile' => 'cliente_responsabile_id',
|
||||
'ClienteFornitore' => 'cliente_fornitore_id',
|
||||
'ClienteAnalisi' => 'clienteAnalisi',
|
||||
'ClienteFatturazione' => 'ClienteFatturazione',
|
||||
'MoltiplicatorePrezzo' => 'moltiplicatore_prezzo_id',
|
||||
'AnagraficaCertestObject' => 'anagrafica_certest_object_id',
|
||||
'AnagraficaCertestService' => 'anagrafica_certest_service_id',
|
||||
'ConsegnaRichiesta' => 'consegna_richiesta',
|
||||
];
|
||||
$staticCols = ['importreferencecode', 'filename_import', 'importdate', 'commessaweb'];
|
||||
|
||||
// ── Risolutori "etichetta → lista di ID" via cache ────────────────────────
|
||||
function cacheItems(string $file): array
|
||||
{
|
||||
if (!is_file($file)) return [];
|
||||
$data = json_decode(file_get_contents($file), true);
|
||||
if (!is_array($data)) return [];
|
||||
return $data['value'] ?? $data ?? [];
|
||||
}
|
||||
|
||||
/** Clienti: label "Nome - X - id" (come search_clienti.php). */
|
||||
function resolveClientIdsByTerm(string $cacheDir, string $term): array
|
||||
{
|
||||
$term = mb_strtolower(trim($term));
|
||||
if ($term === '') return [];
|
||||
$ids = [];
|
||||
foreach (cacheItems($cacheDir . '/clienti.json') as $c) {
|
||||
$name = trim($c['Nominativo'] ?? '');
|
||||
$id = trim((string)($c['IdCliente'] ?? ''));
|
||||
$code = trim((string)($c['CodiceCliente'] ?? ''));
|
||||
$parts = explode('_', $code);
|
||||
$suffix = trim($parts[1] ?? '');
|
||||
if ($suffix === '' && $code !== '') $suffix = substr($code, 0, 1);
|
||||
if ($suffix === '') $suffix = '--';
|
||||
$label = mb_strtolower($name . ' - ' . $suffix . ' - ' . $id);
|
||||
if (mb_strpos($label, $term) !== false) $ids[] = (int)$c['IdCliente'];
|
||||
}
|
||||
return array_values(array_unique($ids));
|
||||
}
|
||||
|
||||
/** Dropdown generico: matcha il termine su qualsiasi label field, ritorna gli id. */
|
||||
function resolveIdsFromCache(string $file, string $idField, array $labelFields, string $term): array
|
||||
{
|
||||
$term = mb_strtolower(trim($term));
|
||||
if ($term === '') return [];
|
||||
$ids = [];
|
||||
foreach (cacheItems($file) as $it) {
|
||||
$hay = '';
|
||||
foreach ($labelFields as $lf) $hay .= ' ' . ($it[$lf] ?? '');
|
||||
if (mb_strpos(mb_strtolower($hay), $term) !== false && isset($it[$idField])) {
|
||||
$ids[] = (int)$it[$idField];
|
||||
}
|
||||
}
|
||||
return array_values(array_unique($ids));
|
||||
}
|
||||
|
||||
/**
|
||||
* Per un fixed field che memorizza un ID, risolve il termine in lista di id.
|
||||
* Ritorna: array di id (IN), [] se il termine non matcha nulla, null se la colonna
|
||||
* non è risolvibile (→ il chiamante la salta invece di fare LIKE sbagliato).
|
||||
*/
|
||||
function resolveFixedIds(string $cacheDir, string $key, string $term): ?array
|
||||
{
|
||||
switch ($key) {
|
||||
case 'ClienteAnalisi':
|
||||
case 'ClienteFatturazione':
|
||||
return resolveClientIdsByTerm($cacheDir, $term);
|
||||
case 'MoltiplicatorePrezzo':
|
||||
return resolveIdsFromCache($cacheDir . '/moltiplicatori_prezzo.json', 'IdMoltiplicatorePrezzo', ['Codice', 'Descrizione'], $term);
|
||||
case 'AnagraficaCertestObject':
|
||||
return resolveIdsFromCache($cacheDir . '/anagrafica_certest_object.json', 'IdAnagrafica', ['Codice', 'NomeAnagrafica'], $term);
|
||||
case 'AnagraficaCertestService':
|
||||
return resolveIdsFromCache($cacheDir . '/anagrafica_certest_service.json', 'IdAnagrafica', ['Codice', 'NomeAnagrafica'], $term);
|
||||
default:
|
||||
// ClienteResponsabile (per-cliente) e altri: non risolvibili globalmente
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array{0:int,1:int}> lista di [idclient, idResponsabile]
|
||||
*/
|
||||
function resolveResponsabilePairs(string $cacheDir, string $term): array
|
||||
{
|
||||
$term = mb_strtolower(trim($term));
|
||||
if ($term === '') return [];
|
||||
$pairs = [];
|
||||
foreach (glob($cacheDir . '/cliente_responsabili_*.json') ?: [] as $file) {
|
||||
if (!preg_match('/cliente_responsabili_(\d+)\.json$/', $file, $m)) continue;
|
||||
$cid = (int)$m[1];
|
||||
$data = json_decode(file_get_contents($file), true);
|
||||
foreach (($data['Responsabili'] ?? []) as $r) {
|
||||
$name = mb_strtolower((string)($r['Nominativo'] ?? ''));
|
||||
if ($name !== '' && mb_strpos($name, $term) !== false && isset($r['IdClienteResponsabile'])) {
|
||||
$pairs[] = [$cid, (int)$r['IdClienteResponsabile']];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $pairs;
|
||||
}
|
||||
|
||||
/** Meta di un mapping (data_type, field_id), con cache statica. */
|
||||
function mappingMeta(PDO $pdo, int $mappingId): array
|
||||
{
|
||||
static $cache = [];
|
||||
if (!array_key_exists($mappingId, $cache)) {
|
||||
$st = $pdo->prepare("SELECT data_type, field_id FROM template_mapping WHERE id = ?");
|
||||
$st->execute([$mappingId]);
|
||||
$cache[$mappingId] = $st->fetch(PDO::FETCH_ASSOC) ?: ['data_type' => 'Testo', 'field_id' => null];
|
||||
}
|
||||
return $cache[$mappingId];
|
||||
}
|
||||
|
||||
function resolveCustomFieldValueIds(string $cacheDir, $fieldId, string $term): array
|
||||
{
|
||||
$fieldId = (int)$fieldId;
|
||||
$term = mb_strtolower(trim($term));
|
||||
if ($fieldId <= 0 || $term === '') return [];
|
||||
$ids = [];
|
||||
foreach (cacheItems($cacheDir . "/customfield_{$fieldId}.json") as $it) {
|
||||
$label = mb_strtolower((string)($it['Valore'] ?? ''));
|
||||
if (mb_strpos($label, $term) !== false && isset($it['IdCustomFieldsValue'])) {
|
||||
$ids[] = (int)$it['IdCustomFieldsValue'];
|
||||
}
|
||||
}
|
||||
return array_values(array_unique($ids));
|
||||
}
|
||||
|
||||
// ── Costruzione WHERE ──────────────────────────────────────────────────────
|
||||
$conds = ['d.templateid = ?', 'd.status = ?'];
|
||||
$params = [$templateId, $status];
|
||||
$needUserJoin = false;
|
||||
|
||||
if (!$showAll) {
|
||||
$conds[] = 'd.user_id = ?';
|
||||
$params[] = $userId;
|
||||
}
|
||||
|
||||
// Scope su un singolo import (come imported.php ?importref=)
|
||||
if ($importref !== '') {
|
||||
$conds[] = 'd.importreferencecode = ?';
|
||||
$params[] = $importref;
|
||||
}
|
||||
|
||||
/** Aggiunge una condizione "colonna id IN (lista risolta)". */
|
||||
function addIdInCondition(array &$conds, array &$params, string $col, array $ids): void
|
||||
{
|
||||
if (empty($ids)) {
|
||||
$conds[] = '1 = 0'; // termine dato ma nessun match → zero risultati
|
||||
return;
|
||||
}
|
||||
$ph = implode(',', array_fill(0, count($ids), '?'));
|
||||
$conds[] = "d.`{$col}` IN ($ph)";
|
||||
foreach ($ids as $id) $params[] = (int)$id;
|
||||
}
|
||||
|
||||
foreach ($filters as $f) {
|
||||
$term = trim((string)($f['term'] ?? ''));
|
||||
if ($term === '') continue;
|
||||
$type = (string)($f['type'] ?? '');
|
||||
$key = (string)($f['key'] ?? '');
|
||||
$like = '%' . $term . '%';
|
||||
|
||||
switch ($type) {
|
||||
case 'detail':
|
||||
case 'main_field':
|
||||
$mappingId = (int)$key;
|
||||
if ($mappingId <= 0) break;
|
||||
$mm = mappingMeta($pdo, $mappingId);
|
||||
if (($mm['data_type'] ?? '') === 'SceltaMultipla') {
|
||||
// Dropdown: field_value = ID valore → risolvi label→id, match IN (= non LIKE)
|
||||
$vids = resolveCustomFieldValueIds($cacheDir, $mm['field_id'], $term);
|
||||
if (empty($vids)) {
|
||||
$conds[] = '1 = 0';
|
||||
break;
|
||||
}
|
||||
$inph = implode(',', array_fill(0, count($vids), '?'));
|
||||
$conds[] = "EXISTS (SELECT 1 FROM import_data_details x
|
||||
WHERE x.id = d.iddatadb AND x.mapping_id = ? AND x.field_value IN ($inph))";
|
||||
$params[] = $mappingId;
|
||||
foreach ($vids as $vid) $params[] = $vid;
|
||||
} else {
|
||||
// Testo / Data: match testuale
|
||||
$conds[] = "EXISTS (SELECT 1 FROM import_data_details x
|
||||
WHERE x.id = d.iddatadb AND x.mapping_id = ? AND x.field_value LIKE ?)";
|
||||
$params[] = $mappingId;
|
||||
$params[] = $like;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'idclient':
|
||||
addIdInCondition($conds, $params, 'idclient', resolveClientIdsByTerm($cacheDir, $term));
|
||||
break;
|
||||
|
||||
case 'cliente_fornitore_id':
|
||||
addIdInCondition($conds, $params, 'cliente_fornitore_id', resolveClientIdsByTerm($cacheDir, $term));
|
||||
break;
|
||||
|
||||
case 'tested_component':
|
||||
$conds[] = 'd.tested_component LIKE ?';
|
||||
$params[] = $like;
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
// Il dropdown invia direttamente il codice ('i'/'P'/'l').
|
||||
if (in_array($term, ['i', 'P', 'l'], true)) {
|
||||
$conds[] = 'd.status = ?';
|
||||
$params[] = $term;
|
||||
} else {
|
||||
$conds[] = '1 = 0';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'fixed':
|
||||
$col = $fixedAliasMap[$key] ?? null;
|
||||
if ($col === null) break;
|
||||
if ($key === 'ConsegnaRichiesta') {
|
||||
// Data: match testuale sul valore data (es. "2026-03").
|
||||
$conds[] = "CAST(d.`{$col}` AS CHAR) LIKE ?";
|
||||
$params[] = $like;
|
||||
} elseif ($key === 'ClienteResponsabile') {
|
||||
// Match sulla coppia (idclient, id responsabile).
|
||||
$pairs = resolveResponsabilePairs($cacheDir, $term);
|
||||
if (empty($pairs)) {
|
||||
$conds[] = '1 = 0';
|
||||
break;
|
||||
}
|
||||
$rowvals = implode(',', array_fill(0, count($pairs), '(?,?)'));
|
||||
$conds[] = "(d.idclient, d.`{$col}`) IN ($rowvals)";
|
||||
foreach ($pairs as $pr) {
|
||||
$params[] = $pr[0];
|
||||
$params[] = $pr[1];
|
||||
}
|
||||
} else {
|
||||
$ids = resolveFixedIds($cacheDir, $key, $term);
|
||||
if ($ids === null) break; // non risolvibile → non filtrare (niente LIKE su id)
|
||||
addIdInCondition($conds, $params, $col, $ids);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'static':
|
||||
if ($key === 'user_name') {
|
||||
$needUserJoin = true;
|
||||
$conds[] = "CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,'')) LIKE ?";
|
||||
$params[] = $like;
|
||||
} elseif (in_array($key, $staticCols, true)) {
|
||||
$conds[] = "d.`{$key}` LIKE ?";
|
||||
$params[] = $like;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break; // tracking/awb ecc. → non filtrabili
|
||||
}
|
||||
}
|
||||
|
||||
$whereSql = implode(' AND ', $conds);
|
||||
$joinSql = $needUserJoin ? 'LEFT JOIN auth_users u ON d.user_id = u.id' : '';
|
||||
|
||||
try {
|
||||
// Totale + pagine
|
||||
$countStmt = $pdo->prepare("SELECT COUNT(*) FROM datadb d {$joinSql} WHERE {$whereSql}");
|
||||
$countStmt->execute($params);
|
||||
$total = (int)$countStmt->fetchColumn();
|
||||
$totalPages = max(1, (int)ceil($total / $perPage));
|
||||
if ($page > $totalPages) $page = $totalPages;
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
// Pagina corrente
|
||||
$pageStmt = $pdo->prepare("
|
||||
SELECT d.iddatadb
|
||||
FROM datadb d {$joinSql}
|
||||
WHERE {$whereSql}
|
||||
ORDER BY d.excelrow ASC, d.iddatadb ASC
|
||||
LIMIT {$perPage} OFFSET {$offset}
|
||||
");
|
||||
$pageStmt->execute($params);
|
||||
$pageIds = array_map('intval', $pageStmt->fetchAll(PDO::FETCH_COLUMN));
|
||||
|
||||
$config = buildGridConfig($pdo, $templateId);
|
||||
$rows = buildGridRows($pdo, $pageIds, $config);
|
||||
|
||||
$out = [
|
||||
'success' => true,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'total_pages' => $totalPages,
|
||||
'rows' => $rows,
|
||||
];
|
||||
|
||||
// Tutti gli id del match (per "seleziona tutti"), solo su richiesta
|
||||
if ($wantAllIds) {
|
||||
$allStmt = $pdo->prepare("
|
||||
SELECT d.iddatadb FROM datadb d {$joinSql}
|
||||
WHERE {$whereSql}
|
||||
ORDER BY d.excelrow ASC, d.iddatadb ASC
|
||||
LIMIT " . FILTER_ALLIDS_CAP . "
|
||||
");
|
||||
$allStmt->execute($params);
|
||||
$out['all_ids'] = array_map('intval', $allStmt->fetchAll(PDO::FETCH_COLUMN));
|
||||
$out['all_ids_capped'] = $total > FILTER_ALLIDS_CAP;
|
||||
}
|
||||
|
||||
echo json_encode($out);
|
||||
} catch (Exception $e) {
|
||||
error_log('filter_records error: ' . $e->getMessage());
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -16,9 +16,11 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
$api = VisualLimsApiClient::getInstance(); // also loads dotenv as a side-effect
|
||||
|
||||
$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";
|
||||
|
||||
@@ -32,19 +34,34 @@ try {
|
||||
$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) < 7200)) { // 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;
|
||||
@@ -52,6 +69,11 @@ try {
|
||||
|
||||
$analisi = $data['value'] ?? [];
|
||||
|
||||
// Escludi le analisi con NominativoCliente compilato
|
||||
$analisi = array_values(array_filter($analisi, function ($item) {
|
||||
return empty(trim((string)($item['NominativoCliente'] ?? '')));
|
||||
}));
|
||||
|
||||
echo json_encode([
|
||||
'value' => $analisi,
|
||||
'count' => is_array($analisi) ? count($analisi) : 0
|
||||
|
||||
@@ -30,7 +30,7 @@ try {
|
||||
|
||||
// Parametri OData
|
||||
$params = [
|
||||
'$select' => 'IdCliente,Nominativo,CodiceCliente',
|
||||
'$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo',
|
||||
'$orderby' => 'Nominativo asc'
|
||||
];
|
||||
|
||||
@@ -76,7 +76,7 @@ try {
|
||||
|
||||
// Cache file (1 hour TTL)
|
||||
$cacheFile = __DIR__ . '/cache/clienti.json';
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 7200)) {
|
||||
readfile($cacheFile);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -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) < 7200)) {
|
||||
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,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) < 7200)) {
|
||||
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);
|
||||
}
|
||||
@@ -28,7 +28,7 @@ try {
|
||||
$cacheFile = $cacheDir . '/customfield_' . $customFieldId . '.json';
|
||||
|
||||
// Use cache if fresh (1 hour)
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 7200)) {
|
||||
$results[$customFieldId] = json_decode(file_get_contents($cacheFile), true);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ try {
|
||||
}
|
||||
|
||||
// Caching skipped in simulate mode to avoid polluting real-data cache files
|
||||
if (!$simulate && $cache_file && file_exists($cache_file) && (time() - filemtime($cache_file) < 3600)) { // 1 ora
|
||||
if (!$simulate && $cache_file && file_exists($cache_file) && (time() - filemtime($cache_file) < 7200)) { // 1 ora
|
||||
$data = json_decode(file_get_contents($cache_file), true);
|
||||
} else {
|
||||
$data = $api->get($endpoint, $options);
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
include dirname(__DIR__) . '/../extra/auth.php';
|
||||
if (!Auth::check()) { http_response_code(401); echo json_encode(['error' => 'Unauthorized']); exit; }
|
||||
if (!Auth::check()) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
@@ -13,7 +17,7 @@ error_reporting(E_ALL);
|
||||
try {
|
||||
// Cache file (1 hour TTL)
|
||||
$cacheFile = __DIR__ . '/cache/matrici.json';
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 7200)) {
|
||||
readfile($cacheFile);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,844 @@
|
||||
/**
|
||||
* gridFilter.js — Selezione righe + filtri colonna a scomparsa per imported.php
|
||||
* Dipende da window.gridRenderer (gridRenderer.js). Includere DOPO gridRenderer.js.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
let filtersActive = false;
|
||||
let restricted = false;
|
||||
const selected = new Set(); // iddatadb selezionati
|
||||
const colFilters = {}; // colKey -> testo filtro
|
||||
let restrictedIds = null; // Set di iddatadb visibili quando ristretto
|
||||
|
||||
function R() {
|
||||
return window.gridRenderer;
|
||||
}
|
||||
function data() {
|
||||
return R().getData();
|
||||
}
|
||||
function meta() {
|
||||
return R().getMeta();
|
||||
}
|
||||
|
||||
// ── Filtro SERVER-SIDE (paginato, come la lista normale ma filtrata) ──────
|
||||
// Il filtro gira su TUTTO il template; il server restituisce UNA PAGINA di righe
|
||||
// filtrate + il totale, e navighiamo le pagine del set filtrato via AJAX.
|
||||
const TEMPLATE_ID =
|
||||
new URLSearchParams(location.search).get("id") || (meta().templateId ?? "");
|
||||
const SHOW_ALL =
|
||||
new URLSearchParams(location.search).get("all_users") === "1";
|
||||
const IMPORTREF =
|
||||
new URLSearchParams(location.search).get("importref") || "";
|
||||
const FILTER_PER_PAGE =
|
||||
parseInt(new URLSearchParams(location.search).get("limit"), 10) || 20;
|
||||
|
||||
let serverFiltered = false;
|
||||
let filterPage = 1;
|
||||
let filterTotalPages = 1;
|
||||
let filterTotal = 0;
|
||||
let originalData = null; // snapshot della pagina originale (per ripristino)
|
||||
let filterReqSeq = 0;
|
||||
let showingSelection = false; // true quando mostriamo l'UNIONE dei selezionati
|
||||
let pagerFn = null; // paginazione corrente (applyServerFilter | showSelectedRecords)
|
||||
|
||||
function collectActiveFilters() {
|
||||
const cols = meta().columns || [];
|
||||
const out = [];
|
||||
for (const [key, term] of Object.entries(colFilters)) {
|
||||
if (!term || !String(term).trim()) continue;
|
||||
const col = cols.find((c) => String(c.key) === String(key));
|
||||
if (!col) continue;
|
||||
out.push({ key: String(key), type: col.type, term: String(term).trim() });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Sostituisce il dataset del renderer in-place e ridisegna.
|
||||
function swapData(rows) {
|
||||
const arr = data();
|
||||
arr.length = 0;
|
||||
(rows || []).forEach((r) => arr.push(r));
|
||||
R().renderVisibleRows();
|
||||
}
|
||||
|
||||
function hidePagination(hide) {
|
||||
// Durante il filtro la paginazione server (per pagina, via URL) è fuorviante.
|
||||
document.querySelectorAll(".pager-bar").forEach((el) => {
|
||||
el.style.display = hide ? "none" : "";
|
||||
});
|
||||
}
|
||||
|
||||
// Pager del set filtrato (Prec / pag X di Y / Succ) nella toolbar.
|
||||
function updateFilterPager() {
|
||||
const el = document.getElementById("filterPager");
|
||||
if (!el) return;
|
||||
if (!serverFiltered) {
|
||||
el.style.display = "none";
|
||||
el.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
el.style.display = "inline-flex";
|
||||
const label = showingSelection ? "selezionati" : "trovati";
|
||||
el.innerHTML =
|
||||
`<button type="button" class="btn btn-outline-secondary btn-sm" id="filterPrevBtn" ${filterPage <= 1 ? "disabled" : ""}>‹</button>` +
|
||||
`<span style="font-size:12px;color:#333;">${filterTotal} ${label} · pag ${filterPage}/${filterTotalPages}</span>` +
|
||||
`<button type="button" class="btn btn-outline-secondary btn-sm" id="filterNextBtn" ${filterPage >= filterTotalPages ? "disabled" : ""}>›</button>`;
|
||||
const go = pagerFn || applyServerFilter;
|
||||
const prev = document.getElementById("filterPrevBtn");
|
||||
const next = document.getElementById("filterNextBtn");
|
||||
if (prev) prev.addEventListener("click", () => go(filterPage - 1));
|
||||
if (next) next.addEventListener("click", () => go(filterPage + 1));
|
||||
}
|
||||
|
||||
function restoreOriginal() {
|
||||
serverFiltered = false;
|
||||
filterPage = 1;
|
||||
filterTotalPages = 1;
|
||||
filterTotal = 0;
|
||||
if (originalData) swapData(originalData);
|
||||
updateFilterPager();
|
||||
hidePagination(false);
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
async function applyServerFilter(page = 1) {
|
||||
const active = collectActiveFilters();
|
||||
if (active.length === 0) {
|
||||
if (serverFiltered) restoreOriginal();
|
||||
return;
|
||||
}
|
||||
if (!originalData) originalData = [...data()]; // snapshot pagina 1 originale
|
||||
|
||||
const seq = ++filterReqSeq;
|
||||
try {
|
||||
const resp = await fetch("filter_records.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
template_id: parseInt(TEMPLATE_ID, 10),
|
||||
all_users: SHOW_ALL,
|
||||
importref: IMPORTREF,
|
||||
status: "i",
|
||||
filters: active,
|
||||
page: Math.max(1, page),
|
||||
per_page: FILTER_PER_PAGE,
|
||||
}),
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (seq !== filterReqSeq) return; // risposta obsoleta: ignora
|
||||
if (!json.success) {
|
||||
console.error("[gridFilter] filtro:", json.message);
|
||||
return;
|
||||
}
|
||||
serverFiltered = true;
|
||||
showingSelection = false;
|
||||
pagerFn = applyServerFilter;
|
||||
filterPage = json.page;
|
||||
filterTotalPages = json.total_pages;
|
||||
filterTotal = json.total;
|
||||
swapData(json.rows || []);
|
||||
updateFilterPager();
|
||||
hidePagination(true);
|
||||
updateToolbar();
|
||||
} catch (e) {
|
||||
console.error("[gridFilter] fetch filtro fallita", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mostra i SELEZIONATI (unione tra filtri diversi) ──────────────────
|
||||
// La selezione si accumula tra filtri; questo carica dal server TUTTI i record
|
||||
// selezionati (per id) e li mostra, paginati, a prescindere dai filtri correnti.
|
||||
async function showSelectedRecords(page = 1) {
|
||||
const ids = [...selected];
|
||||
if (!ids.length) {
|
||||
showingSelection = false;
|
||||
updateToolbar();
|
||||
return;
|
||||
}
|
||||
pagerFn = showSelectedRecords;
|
||||
try {
|
||||
const resp = await fetch("filter_records.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
template_id: parseInt(TEMPLATE_ID, 10),
|
||||
all_users: SHOW_ALL,
|
||||
importref: IMPORTREF,
|
||||
status: "i",
|
||||
ids: ids,
|
||||
page: Math.max(1, page),
|
||||
per_page: FILTER_PER_PAGE,
|
||||
}),
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (!json.success) {
|
||||
console.error("[gridFilter] mostra selezionati:", json.message);
|
||||
return;
|
||||
}
|
||||
if (!originalData) originalData = [...data()];
|
||||
showingSelection = true;
|
||||
serverFiltered = true;
|
||||
filterPage = json.page;
|
||||
filterTotalPages = json.total_pages;
|
||||
filterTotal = json.total;
|
||||
swapData(json.rows || []);
|
||||
updateFilterPager();
|
||||
hidePagination(true);
|
||||
updateToolbar();
|
||||
} catch (e) {
|
||||
console.error("[gridFilter] mostra selezionati fallita", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Tumbler "Mostra selezionati" / "Torna al filtro".
|
||||
function toggleShowSelected() {
|
||||
if (!showingSelection) {
|
||||
if (!selected.size) return;
|
||||
showSelectedRecords(1);
|
||||
} else {
|
||||
exitSelectionView();
|
||||
}
|
||||
}
|
||||
|
||||
// Torna dal "mostra selezionati" al filtro corrente (o alla pagina originale).
|
||||
function exitSelectionView() {
|
||||
showingSelection = false;
|
||||
pagerFn = applyServerFilter;
|
||||
if (collectActiveFilters().length) {
|
||||
applyServerFilter(1);
|
||||
} else {
|
||||
restoreOriginal();
|
||||
}
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
// Deseleziona tutto.
|
||||
function clearSelection() {
|
||||
selected.clear();
|
||||
document
|
||||
.querySelectorAll("#gridRowContainer .grid-row.row-selected")
|
||||
.forEach((el) => el.classList.remove("row-selected"));
|
||||
document
|
||||
.querySelectorAll(".filter-row-checkbox")
|
||||
.forEach((cb) => (cb.checked = false));
|
||||
const sa = document.getElementById("filterSelectAll");
|
||||
if (sa) sa.checked = false;
|
||||
if (showingSelection) {
|
||||
exitSelectionView();
|
||||
} else {
|
||||
updateToolbar();
|
||||
}
|
||||
}
|
||||
|
||||
// "Seleziona tutti i filtrati": chiede al server TUTTI gli id del match e li seleziona.
|
||||
async function selectAllMatching(on) {
|
||||
if (!serverFiltered) return;
|
||||
const active = collectActiveFilters();
|
||||
try {
|
||||
const resp = await fetch("filter_records.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
template_id: parseInt(TEMPLATE_ID, 10),
|
||||
all_users: SHOW_ALL,
|
||||
importref: IMPORTREF,
|
||||
status: "i",
|
||||
filters: active,
|
||||
page: filterPage,
|
||||
per_page: FILTER_PER_PAGE,
|
||||
want_all_ids: true,
|
||||
}),
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (!json.success) return;
|
||||
(json.all_ids || []).forEach((id) => (on ? selected.add(id) : selected.delete(id)));
|
||||
document
|
||||
.querySelectorAll("#gridRowContainer .grid-row")
|
||||
.forEach((rowEl) => {
|
||||
rowEl.classList.toggle("row-selected", on);
|
||||
const cb = rowEl.querySelector(".filter-row-checkbox");
|
||||
if (cb) cb.checked = on;
|
||||
});
|
||||
updateToolbar();
|
||||
if (json.all_ids_capped) {
|
||||
alert("Selezione limitata a " + (json.all_ids || []).length + " record.");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[gridFilter] select-all fallita", e);
|
||||
}
|
||||
}
|
||||
|
||||
function syncFilterRow() {
|
||||
const fr = document.getElementById("gridFilterRow");
|
||||
const header = document.getElementById("gridHeaderContainer");
|
||||
if (!fr || !header) return;
|
||||
|
||||
const actHeader = header.querySelector(".button-header");
|
||||
const actCell = fr.querySelector(".filter-actions-cell");
|
||||
if (actHeader && actCell) {
|
||||
const aw = actHeader.offsetWidth;
|
||||
actCell.style.flex = `0 0 ${aw}px`;
|
||||
actCell.style.minWidth = `${aw}px`;
|
||||
}
|
||||
|
||||
const headerCells = header.querySelectorAll(
|
||||
".grid-header:not(.button-header)",
|
||||
);
|
||||
const filterCells = fr.querySelectorAll(
|
||||
".grid-cell:not(.filter-actions-cell)",
|
||||
);
|
||||
headerCells.forEach((hc, i) => {
|
||||
const fc = filterCells[i];
|
||||
if (fc) fc.style.flex = `0 0 ${hc.offsetWidth}px`;
|
||||
});
|
||||
}
|
||||
|
||||
function syncStickyLeft() {
|
||||
const btnHeader = document.querySelector(
|
||||
"#gridHeaderContainer .button-header",
|
||||
);
|
||||
if (!btnHeader) return;
|
||||
const w1 = btnHeader.offsetWidth; // larghezza reale colonna Actions
|
||||
|
||||
const fixRow = (rowEl) => {
|
||||
if (!rowEl) return;
|
||||
const cells = rowEl.querySelectorAll(
|
||||
":scope > .grid-cell, :scope > .grid-header",
|
||||
);
|
||||
const c2 = cells[1];
|
||||
const c3 = cells[2];
|
||||
if (c2 && getComputedStyle(c2).position === "sticky") {
|
||||
c2.style.left = `${w1}px`;
|
||||
}
|
||||
if (c3 && getComputedStyle(c3).position === "sticky") {
|
||||
c3.style.left = `${w1 + (c2 ? c2.offsetWidth : 0)}px`;
|
||||
}
|
||||
};
|
||||
|
||||
fixRow(document.getElementById("gridHeaderContainer"));
|
||||
document.querySelectorAll(".grid-top").forEach(fixRow);
|
||||
document
|
||||
.querySelectorAll("#gridRowContainer .grid-row")
|
||||
.forEach(fixRow);
|
||||
fixRow(document.getElementById("gridFilterRow"));
|
||||
}
|
||||
|
||||
let _headerObs = null;
|
||||
let _rowObs = null;
|
||||
let _syncRaf = null;
|
||||
|
||||
function reconnectObservers() {
|
||||
const header = document.getElementById("gridHeaderContainer");
|
||||
const rows = document.getElementById("gridRowContainer");
|
||||
if (_headerObs && header)
|
||||
_headerObs.observe(header, {
|
||||
attributes: true,
|
||||
attributeFilter: ["style"],
|
||||
subtree: true,
|
||||
});
|
||||
if (_rowObs && rows) _rowObs.observe(rows, { childList: true });
|
||||
}
|
||||
|
||||
function scheduleSync() {
|
||||
if (_syncRaf) return;
|
||||
_syncRaf = requestAnimationFrame(() => {
|
||||
_syncRaf = null;
|
||||
if (_headerObs) _headerObs.disconnect();
|
||||
if (_rowObs) _rowObs.disconnect();
|
||||
syncStickyLeft();
|
||||
syncFilterRow();
|
||||
reconnectObservers();
|
||||
});
|
||||
}
|
||||
|
||||
function hookGridSync() {
|
||||
_headerObs = new MutationObserver(scheduleSync);
|
||||
_rowObs = new MutationObserver(scheduleSync);
|
||||
reconnectObservers();
|
||||
scheduleSync(); // primo allineamento
|
||||
}
|
||||
|
||||
function rowMatchesFilters(row) {
|
||||
if (serverFiltered) return true;
|
||||
for (const [key, term] of Object.entries(colFilters)) {
|
||||
if (!term) continue;
|
||||
const t = term.toLowerCase();
|
||||
const val = getRowColValue(row, key);
|
||||
if (!String(val).toLowerCase().includes(t)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function getRowColValue(row, key) {
|
||||
const cols = meta().columns || [];
|
||||
const col = cols.find((c) => String(c.key) === String(key));
|
||||
if (!col) return "";
|
||||
switch (col.type) {
|
||||
case "detail":
|
||||
case "main_field":
|
||||
return row.details?.[String(key)] ?? "";
|
||||
case "fixed":
|
||||
return row.fixedFields?.[key] ?? "";
|
||||
case "idclient":
|
||||
return row.idclient ?? "";
|
||||
case "cliente_fornitore_id":
|
||||
return row.cliente_fornitore_id ?? "";
|
||||
case "tested_component":
|
||||
return row.tested_component ?? "";
|
||||
case "static":
|
||||
return row[key] ?? "";
|
||||
case "status":
|
||||
return row.status ?? "";
|
||||
default:
|
||||
return row[key] ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
// ── Applica visibilità righe (filtri + restrizione) ──
|
||||
function applyVisibility() {
|
||||
const rows = document.querySelectorAll("#gridRowContainer .grid-row");
|
||||
rows.forEach((rowEl) => {
|
||||
const id = parseInt(rowEl.dataset.id, 10);
|
||||
const row = data().find((r) => String(r.iddatadb) === String(id));
|
||||
let show = true;
|
||||
if (row && !rowMatchesFilters(row)) show = false;
|
||||
if (restricted && restrictedIds && !restrictedIds.has(id))
|
||||
show = false;
|
||||
rowEl.style.display = show ? "" : "none";
|
||||
});
|
||||
window.dispatchEvent(new Event("resize")); // ricalcola scrollbar top
|
||||
}
|
||||
|
||||
// ── Lista iddatadb attualmente visibili (per propagazione/export ecc.) ──
|
||||
window.getVisibleGridIds = function () {
|
||||
if (serverFiltered) {
|
||||
return data()
|
||||
.map((r) => parseInt(r.iddatadb, 10))
|
||||
.filter(Boolean);
|
||||
}
|
||||
const ids = [];
|
||||
document
|
||||
.querySelectorAll("#gridRowContainer .grid-row")
|
||||
.forEach((el) => {
|
||||
if (el.style.display !== "none")
|
||||
ids.push(parseInt(el.dataset.id, 10));
|
||||
});
|
||||
return ids.filter(Boolean);
|
||||
};
|
||||
|
||||
// ── Righe target per operazioni bulk (propaga/export/clona/save) ──
|
||||
// Regola: filtri attivi + selezione → solo selezionate;
|
||||
// filtri attivi senza selezione → solo visibili;
|
||||
// filtri non attivi → null (= tutte, comportamento originale).
|
||||
window.getTargetGridIds = function () {
|
||||
if (!filtersActive) return null; // null = nessun vincolo
|
||||
if (selected.size > 0) return new Set([...selected]);
|
||||
return new Set(window.getVisibleGridIds());
|
||||
};
|
||||
|
||||
// Helper: true se l'id è tra i target (o se non c'è vincolo)
|
||||
window.isTargetGridId = function (id) {
|
||||
const t = window.getTargetGridIds();
|
||||
if (t === null) return true;
|
||||
return t.has(parseInt(id, 10));
|
||||
};
|
||||
|
||||
// ── Toggle selezione riga ──
|
||||
function toggleRow(id, on) {
|
||||
id = parseInt(id, 10);
|
||||
if (on) selected.add(id);
|
||||
else selected.delete(id);
|
||||
const rowEl = document.querySelector(
|
||||
`#gridRowContainer .grid-row[data-id="${id}"]`,
|
||||
);
|
||||
if (rowEl) {
|
||||
rowEl.classList.toggle("row-selected", selected.has(id));
|
||||
const cb = rowEl.querySelector(".filter-row-checkbox");
|
||||
if (cb) cb.checked = selected.has(id);
|
||||
}
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
// ── Inietta checkbox nelle righe visibili ──
|
||||
// ── Inietta checkbox dentro la cella Actions (già sticky) ──
|
||||
function injectCheckboxes() {
|
||||
document
|
||||
.querySelectorAll("#gridRowContainer .grid-row")
|
||||
.forEach((rowEl) => {
|
||||
const btnCell = rowEl.querySelector(".button-cell");
|
||||
if (!btnCell) return;
|
||||
if (btnCell.querySelector(".filter-row-checkbox")) return;
|
||||
const id = parseInt(rowEl.dataset.id, 10);
|
||||
const wrap = document.createElement("label");
|
||||
wrap.className = "filter-cb-wrap";
|
||||
wrap.innerHTML = `<input type="checkbox" class="filter-row-checkbox" ${selected.has(id) ? "checked" : ""}>`;
|
||||
btnCell.insertBefore(wrap, btnCell.firstChild);
|
||||
if (selected.has(id)) rowEl.classList.add("row-selected");
|
||||
});
|
||||
}
|
||||
|
||||
function removeCheckboxes() {
|
||||
document
|
||||
.querySelectorAll(".filter-cb-wrap")
|
||||
.forEach((el) => el.remove());
|
||||
document
|
||||
.querySelectorAll(".grid-row.row-selected")
|
||||
.forEach((el) => el.classList.remove("row-selected"));
|
||||
}
|
||||
|
||||
// ── Riga input filtri colonna (allineata alle larghezze reali) ──
|
||||
// ── Riga input filtri colonna (allineata alle larghezze reali) ──
|
||||
function injectFilterRow() {
|
||||
if (document.getElementById("gridFilterRow")) return;
|
||||
const top = document.getElementById("gridTopContainer");
|
||||
const header = document.getElementById("gridHeaderContainer");
|
||||
if (!top || !header) return;
|
||||
|
||||
const fr = document.createElement("div");
|
||||
fr.className = "grid-row grid-filter-row";
|
||||
fr.id = "gridFilterRow";
|
||||
|
||||
// cella allineata alla colonna Actions (sticky, legge larghezza reale)
|
||||
const actHeader = header.querySelector(".button-header");
|
||||
const actCell = document.createElement("div");
|
||||
actCell.className = "grid-cell button-cell filter-actions-cell";
|
||||
const aw = actHeader ? actHeader.offsetWidth : 220;
|
||||
actCell.style.flex = `0 0 ${aw}px`;
|
||||
actCell.style.minWidth = `${aw}px`;
|
||||
actCell.innerHTML = `<label class="filter-cb-wrap" style="display:inline-flex;" title="Seleziona tutti i filtrati"><input type="checkbox" id="filterSelectAll"></label>`;
|
||||
fr.appendChild(actCell);
|
||||
|
||||
// una cella per colonna, larghezza letta dall'header reale
|
||||
const headerCells = header.querySelectorAll(
|
||||
".grid-header:not(.button-header)",
|
||||
);
|
||||
(meta().columns || []).forEach((col, i) => {
|
||||
const hc = headerCells[i];
|
||||
const w = hc ? hc.offsetWidth : col.width || 150;
|
||||
const cell = document.createElement("div");
|
||||
cell.className = "grid-cell";
|
||||
cell.style.flex = `0 0 ${w}px`;
|
||||
if (col.type === "tracking" || col.type === "awb") {
|
||||
// niente filtro
|
||||
} else if (col.type === "status") {
|
||||
// Status: dropdown (3 valori) invece del testo libero.
|
||||
const cur = colFilters[col.key] || "";
|
||||
const opt = (v, lbl) =>
|
||||
`<option value="${v}" ${cur === v ? "selected" : ""}>${lbl}</option>`;
|
||||
cell.innerHTML =
|
||||
`<select class="filter-col-input" data-col-key="${col.key}" style="width:100%;padding:3px 6px;font-size:12px;border:1px solid #ced4da;border-radius:4px;">` +
|
||||
opt("", "— tutti —") +
|
||||
opt("i", "Imported") +
|
||||
opt("P", "In Progress") +
|
||||
opt("l", "To LIMS") +
|
||||
`</select>`;
|
||||
} else {
|
||||
cell.innerHTML = `<input type="text" class="filter-col-input" data-col-key="${col.key}" placeholder="🔍 ${col.label || ""}" value="${colFilters[col.key] || ""}" style="width:100%;padding:3px 6px;font-size:12px;border:1px solid #ced4da;border-radius:4px;">`;
|
||||
}
|
||||
fr.appendChild(cell);
|
||||
});
|
||||
|
||||
top.parentNode.insertBefore(fr, top);
|
||||
scheduleSync(); // allinea larghezze filtri + offset sticky
|
||||
}
|
||||
|
||||
function removeFilterRow() {
|
||||
const fr = document.getElementById("gridFilterRow");
|
||||
if (fr) fr.remove();
|
||||
}
|
||||
|
||||
// ── Toolbar (bottoni contestuali) ──
|
||||
function updateToolbar() {
|
||||
const bar = document.getElementById("filterActionBar");
|
||||
if (!bar) return;
|
||||
const count = selected.size;
|
||||
bar.style.display = filtersActive ? "inline-flex" : "none";
|
||||
const cntEl = document.getElementById("filterSelCount");
|
||||
if (cntEl) cntEl.textContent = count;
|
||||
document.getElementById("filterDeleteBtn").disabled = count === 0;
|
||||
|
||||
const showBtn = document.getElementById("filterRestrictBtn");
|
||||
showBtn.disabled = count === 0 && !showingSelection;
|
||||
showBtn.innerHTML = showingSelection
|
||||
? '<i class="fas fa-eye"></i> Torna al filtro'
|
||||
: '<i class="fas fa-list-check"></i> Mostra selezionati';
|
||||
|
||||
const clearSelBtn = document.getElementById("filterClearSelBtn");
|
||||
if (clearSelBtn) clearSelBtn.disabled = count === 0;
|
||||
}
|
||||
|
||||
// ── Main toggle ──
|
||||
function toggleFilters() {
|
||||
filtersActive = !filtersActive;
|
||||
const grid = document.getElementById("gridContainer");
|
||||
const btn = document.getElementById("filtersToggleBtn");
|
||||
if (filtersActive) {
|
||||
grid.classList.add("filters-on");
|
||||
btn.classList.add("active");
|
||||
injectCheckboxes();
|
||||
injectFilterRow();
|
||||
} else {
|
||||
grid.classList.remove("filters-on");
|
||||
btn.classList.remove("active");
|
||||
removeCheckboxes();
|
||||
removeFilterRow();
|
||||
// reset filtri colonna + ripristina la pagina originale
|
||||
Object.keys(colFilters).forEach((k) => delete colFilters[k]);
|
||||
if (serverFiltered) restoreOriginal();
|
||||
}
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
// ── Batch delete ──
|
||||
async function batchDelete() {
|
||||
const ids = [...selected];
|
||||
if (!ids.length) return;
|
||||
if (
|
||||
!confirm(
|
||||
`Eliminare ${ids.length} righe dal database? Operazione irreversibile.`,
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
const btn = document.getElementById("filterDeleteBtn");
|
||||
btn.disabled = true;
|
||||
const original = btn.innerHTML;
|
||||
|
||||
let ok = 0,
|
||||
fail = 0;
|
||||
for (const id of ids) {
|
||||
try {
|
||||
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${ok + fail + 1}/${ids.length}`;
|
||||
const resp = await fetch("delete_record.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id: id }),
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (json.success) {
|
||||
ok++;
|
||||
// rimuovi da gridData
|
||||
const arr = data();
|
||||
const idx = arr.findIndex(
|
||||
(r) => String(r.iddatadb) === String(id),
|
||||
);
|
||||
if (idx !== -1) arr.splice(idx, 1);
|
||||
selected.delete(id);
|
||||
if (restrictedIds) restrictedIds.delete(id);
|
||||
} else fail++;
|
||||
} catch (e) {
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
|
||||
btn.innerHTML = original;
|
||||
R().renderVisibleRows();
|
||||
if (filtersActive) {
|
||||
injectCheckboxes();
|
||||
}
|
||||
applyVisibility();
|
||||
updateToolbar();
|
||||
alert(`Eliminate: ${ok}${fail ? " — Falliti: " + fail : ""}`);
|
||||
}
|
||||
|
||||
// ── Restringi / mostra tutti ──
|
||||
function toggleRestrict() {
|
||||
if (!restricted) {
|
||||
if (!selected.size) return;
|
||||
restrictedIds = new Set([...selected]);
|
||||
restricted = true;
|
||||
} else {
|
||||
restricted = false;
|
||||
restrictedIds = null;
|
||||
}
|
||||
applyVisibility();
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
// ── Costruzione UI toolbar (bottoni in cima) ──
|
||||
function buildUI() {
|
||||
const addBtn = document.getElementById("addRowBtn");
|
||||
if (!addBtn) return;
|
||||
|
||||
// bottone Filters
|
||||
const fBtn = document.createElement("button");
|
||||
fBtn.type = "button";
|
||||
fBtn.id = "filtersToggleBtn";
|
||||
fBtn.className = "btn btn-outline-info btn-sm";
|
||||
fBtn.style.flexShrink = "0";
|
||||
fBtn.innerHTML = '<i class="fas fa-filter"></i> Filters';
|
||||
addBtn.parentNode.insertBefore(fBtn, addBtn.nextSibling);
|
||||
|
||||
// barra azioni contestuali
|
||||
const bar = document.createElement("span");
|
||||
bar.id = "filterActionBar";
|
||||
bar.style.cssText =
|
||||
"display:none;align-items:center;gap:8px;flex-shrink:0;";
|
||||
bar.innerHTML = `
|
||||
<span id="filterPager" style="display:none;align-items:center;gap:6px;"></span>
|
||||
<button type="button" id="filterClearBtn" class="btn btn-outline-secondary btn-sm"><i class="fas fa-times"></i> Pulisci filtri</button>
|
||||
<span style="font-size:12px;color:#555;">Selezionate: <strong id="filterSelCount">0</strong></span>
|
||||
<button type="button" id="filterRestrictBtn" class="btn btn-outline-primary btn-sm"><i class="fas fa-list-check"></i> Mostra selezionati</button>
|
||||
<button type="button" id="filterClearSelBtn" class="btn btn-outline-secondary btn-sm"><i class="fas fa-square"></i> Deseleziona</button>
|
||||
<button type="button" id="filterDeleteBtn" class="btn btn-outline-danger btn-sm"><i class="fas fa-trash"></i> Elimina selezionati</button>
|
||||
`;
|
||||
fBtn.parentNode.insertBefore(bar, fBtn.nextSibling);
|
||||
|
||||
fBtn.addEventListener("click", toggleFilters);
|
||||
document
|
||||
.getElementById("filterClearBtn")
|
||||
.addEventListener("click", clearFilters);
|
||||
document
|
||||
.getElementById("filterDeleteBtn")
|
||||
.addEventListener("click", batchDelete);
|
||||
document
|
||||
.getElementById("filterRestrictBtn")
|
||||
.addEventListener("click", toggleShowSelected);
|
||||
document
|
||||
.getElementById("filterClearSelBtn")
|
||||
.addEventListener("click", clearSelection);
|
||||
}
|
||||
|
||||
// Svuota tutti i filtri di colonna (input + select) e torna alla pagina originale.
|
||||
function clearFilters() {
|
||||
Object.keys(colFilters).forEach((k) => delete colFilters[k]);
|
||||
document
|
||||
.querySelectorAll("#gridFilterRow .filter-col-input")
|
||||
.forEach((el) => {
|
||||
el.value = "";
|
||||
});
|
||||
if (serverFiltered) restoreOriginal();
|
||||
}
|
||||
|
||||
// ── Event delegation ──
|
||||
function attachEvents() {
|
||||
// checkbox click
|
||||
document.addEventListener("change", function (e) {
|
||||
if (e.target.classList.contains("filter-row-checkbox")) {
|
||||
const rowEl = e.target.closest(".grid-row");
|
||||
toggleRow(rowEl.dataset.id, e.target.checked);
|
||||
}
|
||||
});
|
||||
|
||||
// Seleziona / deseleziona tutti i FILTRATI
|
||||
document.addEventListener("change", function (e) {
|
||||
if (e.target.id !== "filterSelectAll") return;
|
||||
const on = e.target.checked;
|
||||
|
||||
// Con filtro server-side: seleziona TUTTI i match (anche altre pagine).
|
||||
if (serverFiltered) {
|
||||
selectAllMatching(on);
|
||||
return;
|
||||
}
|
||||
|
||||
document
|
||||
.querySelectorAll("#gridRowContainer .grid-row")
|
||||
.forEach((rowEl) => {
|
||||
if (rowEl.style.display === "none") return;
|
||||
const id = parseInt(rowEl.dataset.id, 10);
|
||||
if (!id) return;
|
||||
if (on) selected.add(id);
|
||||
else selected.delete(id);
|
||||
rowEl.classList.toggle("row-selected", on);
|
||||
const cb = rowEl.querySelector(".filter-row-checkbox");
|
||||
if (cb) cb.checked = on;
|
||||
});
|
||||
updateToolbar();
|
||||
});
|
||||
|
||||
// click su QUALSIASI punto della riga (tranne campi interattivi) = seleziona
|
||||
document.addEventListener("click", function (e) {
|
||||
if (!filtersActive) return;
|
||||
const rowEl = e.target.closest("#gridRowContainer .grid-row");
|
||||
if (!rowEl) return;
|
||||
|
||||
// ignora click su elementi interattivi
|
||||
if (
|
||||
e.target.closest(
|
||||
"input, select, textarea, button, a, .select2-container, .action-btn, .propagate-btn, .add-part-btn",
|
||||
)
|
||||
) {
|
||||
// ma se ho cliccato proprio la checkbox, lascia fare al change
|
||||
return;
|
||||
}
|
||||
|
||||
const cb = rowEl.querySelector(".filter-row-checkbox");
|
||||
if (!cb) return;
|
||||
cb.checked = !cb.checked;
|
||||
toggleRow(rowEl.dataset.id, cb.checked);
|
||||
});
|
||||
|
||||
// filtri colonna testo (debounce) → filtro server-side su tutto il template
|
||||
let t = null;
|
||||
document.addEventListener("input", function (e) {
|
||||
if (!e.target.classList.contains("filter-col-input")) return;
|
||||
if (e.target.tagName === "SELECT") return; // i select li gestisce 'change'
|
||||
const key = e.target.dataset.colKey;
|
||||
colFilters[key] = e.target.value;
|
||||
clearTimeout(t);
|
||||
t = setTimeout(applyServerFilter, 250);
|
||||
});
|
||||
|
||||
// dropdown (es. Status): applica subito, senza debounce
|
||||
document.addEventListener("change", function (e) {
|
||||
if (!e.target.classList.contains("filter-col-input")) return;
|
||||
if (e.target.tagName !== "SELECT") return;
|
||||
colFilters[e.target.dataset.colKey] = e.target.value;
|
||||
applyServerFilter();
|
||||
});
|
||||
}
|
||||
|
||||
// ── Ripristina stato filtri dopo un re-render (checkbox + restrizione) ──
|
||||
let _restoring = false;
|
||||
function restoreFilterState() {
|
||||
if (!filtersActive || _restoring) return;
|
||||
_restoring = true; // evita loop con l'observer mentre iniettiamo/nascondiamo
|
||||
try {
|
||||
injectCheckboxes();
|
||||
applyVisibility(); // ri-applica filtri colonna E restringi selezione
|
||||
} finally {
|
||||
// rilascia dopo il tick così le nostre mutazioni non ci ri-triggerano subito
|
||||
setTimeout(() => {
|
||||
_restoring = false;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Observer: qualunque cosa ricrei le righe (propagazione, save, ecc.)
|
||||
// ripristina automaticamente checkbox + restrizione. Robusto perché
|
||||
// non dipende da chi chiama renderVisibleRows. ──
|
||||
function hookRerender() {
|
||||
const rowC = document.getElementById("gridRowContainer");
|
||||
if (!rowC) return;
|
||||
|
||||
const obs = new MutationObserver(() => {
|
||||
if (!filtersActive || _restoring) return;
|
||||
// debounce leggero: aspetta che il render finisca
|
||||
clearTimeout(hookRerender._t);
|
||||
hookRerender._t = setTimeout(restoreFilterState, 30);
|
||||
});
|
||||
|
||||
obs.observe(rowC, { childList: true });
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (!window.gridRenderer) {
|
||||
console.error("[gridFilter] gridRenderer non trovato");
|
||||
return;
|
||||
}
|
||||
buildUI();
|
||||
attachEvents();
|
||||
hookRerender();
|
||||
hookGridSync();
|
||||
}
|
||||
|
||||
// parte dopo che gridRenderer ha finito init
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () =>
|
||||
setTimeout(init, 300),
|
||||
);
|
||||
} else {
|
||||
setTimeout(init, 300);
|
||||
}
|
||||
})();
|
||||
+438
-44
@@ -66,7 +66,11 @@
|
||||
// ── Client data (AJAX Select2, no bulk loading) ──────────────────────
|
||||
|
||||
function formatClientLabel(client) {
|
||||
const nome = client.Nominativo || client.text || "Nome non disponibile";
|
||||
const nome = (
|
||||
client.Nominativo ||
|
||||
client.text ||
|
||||
"Nome non disponibile"
|
||||
).trim();
|
||||
const id = client.IdCliente || client.id || "";
|
||||
const codiceCliente = (
|
||||
client.CodiceCliente ??
|
||||
@@ -75,11 +79,14 @@
|
||||
)
|
||||
.toString()
|
||||
.trim();
|
||||
|
||||
// Lettera estratta (parte dopo "_", altrimenti prima lettera del codice)
|
||||
const suffix = (codiceCliente.split("_")[1] || "").trim();
|
||||
const shortCode =
|
||||
suffix || (codiceCliente ? codiceCliente.charAt(0) : "--");
|
||||
|
||||
return `${nome.trim()} - ${shortCode} (ID: ${id})`;
|
||||
// Nome - I - 123
|
||||
return `${nome} - ${shortCode} - ${id}`;
|
||||
}
|
||||
|
||||
// Cache of resolved client names: id → name
|
||||
@@ -110,6 +117,9 @@
|
||||
ids.add(String(val));
|
||||
}
|
||||
}
|
||||
if (row.fixedFields && row.fixedFields.ClienteFatturazione) {
|
||||
ids.add(String(row.fixedFields.ClienteFatturazione));
|
||||
}
|
||||
});
|
||||
|
||||
// Batch resolve via single request per ID
|
||||
@@ -133,7 +143,54 @@
|
||||
}),
|
||||
);
|
||||
}
|
||||
// Ranking inizio/centro/fine per select con dati già in memoria.
|
||||
// Restituisce { matcher, sorter } da passare a select2().
|
||||
function rankedLocalSelect2() {
|
||||
return {
|
||||
matcher: function (params, data) {
|
||||
const term = $.trim(params.term || "").toLowerCase();
|
||||
if (term === "") return data;
|
||||
if (typeof data.text === "undefined") return null;
|
||||
|
||||
// AND multi-termine, posizione indipendente
|
||||
const text = data.text.toLowerCase();
|
||||
const terms = term.split(/\s+/).filter(Boolean);
|
||||
const all = terms.every((t) => text.indexOf(t) !== -1);
|
||||
return all ? data : null;
|
||||
},
|
||||
sorter: function (results) {
|
||||
const term = $(
|
||||
".select2-container--open .select2-search__field",
|
||||
).val();
|
||||
const search = (term || "").toLowerCase().trim();
|
||||
if (!search) return results;
|
||||
|
||||
const first = search.split(/\s+/)[0];
|
||||
|
||||
function score(text) {
|
||||
const t = (text || "").toLowerCase();
|
||||
if (t === search) return 0; // match esatto
|
||||
if (t.indexOf(first) === 0) return 1; // inizia col termine
|
||||
if (new RegExp("(^|\\s)" + escapeRegExp(first)).test(t))
|
||||
return 2; // inizio parola
|
||||
return 3; // in mezzo
|
||||
}
|
||||
|
||||
return results.slice().sort(function (a, b) {
|
||||
const sa = score(a.text);
|
||||
const sb = score(b.text);
|
||||
if (sa !== sb) return sa - sb;
|
||||
return (a.text || "").localeCompare(b.text || "", "it", {
|
||||
sensitivity: "base",
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function escapeRegExp(s) {
|
||||
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
function sortSelect2ResultsByStart(data) {
|
||||
const term = $(".select2-container--open .select2-search__field").val();
|
||||
|
||||
@@ -156,33 +213,50 @@
|
||||
return textA.localeCompare(textB, "it", { sensitivity: "base" });
|
||||
});
|
||||
}
|
||||
// Select2 AJAX config for client selects
|
||||
const clientSelect2Config = {
|
||||
placeholder: "Search client...",
|
||||
allowClear: true,
|
||||
width: "100%",
|
||||
minimumInputLength: 0,
|
||||
sorter: sortSelect2ResultsByStart,
|
||||
dropdownCssClass: "select2-dropdown-smaller",
|
||||
ajax: {
|
||||
url: "search_clienti.php",
|
||||
dataType: "json",
|
||||
delay: 150,
|
||||
data: function (params) {
|
||||
return { q: params.term || "", limit: 20 };
|
||||
},
|
||||
processResults: function (data) {
|
||||
const results = (data.results || []).map((item) => ({
|
||||
...item,
|
||||
text: formatClientLabel(item),
|
||||
}));
|
||||
|
||||
return { results: results };
|
||||
},
|
||||
cache: true,
|
||||
},
|
||||
// Mappa: quale filtro Inattivo applicare per ogni campo cliente
|
||||
const clientFieldTipoMap = {
|
||||
ClienteFornitore: "attivo",
|
||||
ClienteAnalisi: "inattivo",
|
||||
ClienteFatturazione: "attivo",
|
||||
};
|
||||
|
||||
// Select2 AJAX config factory for client selects (parametrizzata su "tipo")
|
||||
function clientSelect2ConfigFor(tipo) {
|
||||
return {
|
||||
placeholder: "Search client...",
|
||||
allowClear: true,
|
||||
width: "100%",
|
||||
minimumInputLength: 0,
|
||||
sorter: sortSelect2ResultsByStart,
|
||||
dropdownCssClass: "select2-dropdown-smaller",
|
||||
ajax: {
|
||||
url: "search_clienti.php",
|
||||
dataType: "json",
|
||||
delay: 150,
|
||||
data: function (params) {
|
||||
const d = { q: params.term || "", limit: 20 };
|
||||
if (tipo) d.tipo = tipo;
|
||||
return d;
|
||||
},
|
||||
processResults: function (data) {
|
||||
const results = (data.results || []).map((item) => ({
|
||||
...item,
|
||||
text: formatClientLabel(item),
|
||||
}));
|
||||
|
||||
return { results: results };
|
||||
},
|
||||
cache: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Helper: inizializza Select2 su un elemento leggendo il tipo da data-client-tipo
|
||||
function initClientSelect2(el) {
|
||||
const tipo = el.dataset.clientTipo || "";
|
||||
$(el).select2(clientSelect2ConfigFor(tipo));
|
||||
}
|
||||
|
||||
async function loadClientData() {
|
||||
// No longer loads all clients — AJAX Select2 handles search
|
||||
// Just resolve names for pre-selected values
|
||||
@@ -215,6 +289,7 @@
|
||||
getParams: (cid) => ({ id_cliente: cid }),
|
||||
},
|
||||
ClienteFornitore: { source: "clients" },
|
||||
ClienteFatturazione: { source: "clients" },
|
||||
ClienteAnalisi: { source: "clients" },
|
||||
};
|
||||
|
||||
@@ -364,7 +439,6 @@
|
||||
allowClear: true,
|
||||
width: "100%",
|
||||
minimumInputLength: 0,
|
||||
sorter: sortSelect2ResultsByStart,
|
||||
ajax: {
|
||||
url: "search_customfield_values.php",
|
||||
dataType: "json",
|
||||
@@ -514,7 +588,14 @@
|
||||
: "To LIMS";
|
||||
let html = `<span class="status-badge status-${esc(st)}">${label}</span>`;
|
||||
if (row.commessaweb) {
|
||||
html += `<span class="commessaweb-code" style="display:block; font-size:0.75em; color:#555; margin-top:2px;">${esc(row.commessaweb)}</span>`;
|
||||
const npBadge =
|
||||
typeof window.noPriceBadgeHTML === "function"
|
||||
? window.noPriceBadgeHTML(
|
||||
row.noPriceCount,
|
||||
row.noPriceAnalyses,
|
||||
)
|
||||
: "";
|
||||
html += `<span class="commessaweb-code" style="display:block; font-size:0.75em; color:#555; margin-top:2px;">${esc(row.commessaweb)}${npBadge}</span>`;
|
||||
}
|
||||
div.innerHTML = html;
|
||||
div.classList.remove("editable-cell");
|
||||
@@ -536,6 +617,7 @@
|
||||
sel.className =
|
||||
"cell-input dropdown-select client-select searchable-client fornitore-select";
|
||||
sel.dataset.currentValue = row.cliente_fornitore_id || "";
|
||||
sel.dataset.clientTipo = "attivo";
|
||||
sel.innerHTML = buildClientOptionsHTML(
|
||||
row.cliente_fornitore_id,
|
||||
);
|
||||
@@ -631,12 +713,13 @@
|
||||
if (config && config.source === "clients") {
|
||||
const reqCls = col.isRequired ? " required-input" : "";
|
||||
const req = col.isRequired ? " required" : "";
|
||||
const tipo = clientFieldTipoMap[col.key] || "";
|
||||
let opts = '<option value="">Select...</option>';
|
||||
if (value) {
|
||||
const label = clientNameCache[value] || value;
|
||||
opts += `<option value="${esc(String(value))}" selected>${esc(String(label))}</option>`;
|
||||
}
|
||||
return `<select class="cell-input manual-input fixed-input searchable-client api-fixed-select${reqCls}" data-fixed-key="${escAttr(col.key)}" data-current-value="${escAttr(value)}"${req}>${opts}</select>`;
|
||||
return `<select class="cell-input manual-input fixed-input searchable-client api-fixed-select${reqCls}" data-fixed-key="${escAttr(col.key)}" data-current-value="${escAttr(value)}" data-client-tipo="${escAttr(tipo)}"${req}>${opts}</select>`;
|
||||
}
|
||||
|
||||
// Select — build from cache
|
||||
@@ -654,7 +737,7 @@
|
||||
|
||||
const reqCls = col.isRequired ? " required-input" : "";
|
||||
const req = col.isRequired ? " required" : "";
|
||||
return `<select class="cell-input manual-input fixed-input ${selectClass}${reqCls}" data-fixed-key="${escAttr(col.key)}" data-current-value="${escAttr(value)}"${req}>${options}</select>`;
|
||||
return `<select class="cell-input manual-input fixed-input searchable-fixed ${selectClass}${reqCls}" data-fixed-key="${escAttr(col.key)}" data-current-value="${escAttr(value)}"${req}>${options}</select>`;
|
||||
}
|
||||
|
||||
function buildDropdownOptionsHTML(fieldId, selectedValue) {
|
||||
@@ -679,9 +762,7 @@
|
||||
div.style.position = "relative";
|
||||
|
||||
let html = "";
|
||||
if (meta.isAdmin) {
|
||||
html += `<button type="button" class="export-lims-btn action-btn" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" title="${isExported ? "Already exported" : "Export to LIMS"}" style="background:${isExported ? "#ccc" : "#eb0b0b"}; color:white; border:none; border-radius:5px; cursor:${isExported ? "not-allowed" : "pointer"}; ${isExported ? "opacity:0.5;" : ""}" ${isExported ? "disabled" : ""}><i class="fas fa-upload"></i></button>`;
|
||||
}
|
||||
html += `<button type="button" class="export-lims-btn action-btn" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" title="${isExported ? "Already exported" : "Export to LIMS"}" style="background:${isExported ? "#ccc" : "#eb0b0b"}; color:white; border:none; border-radius:5px; cursor:${isExported ? "not-allowed" : "pointer"}; ${isExported ? "opacity:0.5;" : ""}" ${isExported ? "disabled" : ""}><i class="fas fa-upload"></i></button>`;
|
||||
html += `<button type="button" class="save-btn action-btn" data-row="${rowIndex}" title="Save" style="background:#28a745; color:white; border:none; border-radius:5px; cursor:pointer;"><i class="fas fa-save"></i></button>`;
|
||||
html += `<button type="button" class="photos-btn action-btn" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" title="Photos" style="background:#007bff; color:white; border:none; border-radius:5px; cursor:pointer;"><i class="fas fa-camera"></i></button>`;
|
||||
html += `<button type="button" class="parts-btn action-btn" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" title="Parts" style="background:#ffc107; color:white; border:none; border-radius:5px; cursor:pointer;"><i class="fas fa-puzzle-piece"></i></button>`;
|
||||
@@ -821,6 +902,76 @@
|
||||
updateDirtyIndicator();
|
||||
}
|
||||
|
||||
// Aggiorna in-place SOLO le celle di una colonna in tutte le righe visibili,
|
||||
// senza distruggere/ricostruire l'intera griglia (evita il freeze in propagazione).
|
||||
function updateColumnCells(colKey, colType) {
|
||||
if (!rowContainer) return;
|
||||
const cells = rowContainer.querySelectorAll(
|
||||
`.grid-cell[data-col="${CSS.escape(String(colKey))}"][data-col-type="${colType}"]`,
|
||||
);
|
||||
cells.forEach((cell) => {
|
||||
const rowIndex = parseInt(cell.dataset.row, 10);
|
||||
if (Number.isNaN(rowIndex) || !data[rowIndex]) return;
|
||||
|
||||
// Ricava il valore corrente dal modello dati
|
||||
let value = "";
|
||||
if (colType === "detail" || colType === "main_field") {
|
||||
value = data[rowIndex].details?.[String(colKey)] ?? "";
|
||||
} else if (colType === "fixed") {
|
||||
value = data[rowIndex].fixedFields?.[colKey] ?? "";
|
||||
} else if (colType === "cliente_fornitore_id") {
|
||||
value = data[rowIndex].cliente_fornitore_id ?? "";
|
||||
}
|
||||
|
||||
const input = cell.querySelector(".cell-input");
|
||||
if (!input) return;
|
||||
|
||||
if ($(input).hasClass("select2-hidden-accessible")) {
|
||||
// Assicura che l'opzione selezionata esista, poi imposta il valore
|
||||
if (
|
||||
value &&
|
||||
!input.querySelector(
|
||||
`option[value="${CSS.escape(String(value))}"]`,
|
||||
)
|
||||
) {
|
||||
const label =
|
||||
clientNameCache[value] ||
|
||||
(input.dataset?.fieldId
|
||||
? dropdownNameCache[
|
||||
input.dataset.fieldId + "_" + value
|
||||
]
|
||||
: null) ||
|
||||
value;
|
||||
input.add(new Option(label, value, true, true));
|
||||
}
|
||||
$(input)
|
||||
.val(value ? String(value) : "")
|
||||
.trigger("change.select2");
|
||||
} else if (input.tagName === "SELECT") {
|
||||
// Select nativo: aggiungi opzione se manca
|
||||
if (
|
||||
value &&
|
||||
!input.querySelector(
|
||||
`option[value="${CSS.escape(String(value))}"]`,
|
||||
)
|
||||
) {
|
||||
const label =
|
||||
(input.dataset?.fieldId
|
||||
? dropdownNameCache[
|
||||
input.dataset.fieldId + "_" + value
|
||||
]
|
||||
: null) || value;
|
||||
input.add(new Option(label, value, true, true));
|
||||
}
|
||||
input.value = value;
|
||||
} else {
|
||||
input.value = value;
|
||||
}
|
||||
|
||||
cell.classList.add("cell-changed");
|
||||
});
|
||||
}
|
||||
|
||||
function renderSingleRow(rowIndex) {
|
||||
const existing = rowContainer.querySelector(
|
||||
`.grid-row[data-id="${data[rowIndex].iddatadb}"]`,
|
||||
@@ -940,6 +1091,11 @@
|
||||
|
||||
if (!row) return;
|
||||
|
||||
// Salta le righe nascoste (filtrate / non nella selezione ristretta):
|
||||
// non devono essere sincronizzate né marcate dirty.
|
||||
const rowEl = cell.closest(".grid-row");
|
||||
if (rowEl && rowEl.style.display === "none") return;
|
||||
|
||||
const colType = cell.dataset.colType;
|
||||
const colKey = cell.dataset.col;
|
||||
const input = cell.querySelector(".cell-input");
|
||||
@@ -1072,7 +1228,7 @@
|
||||
`<button type="button" class="propagate-btn" data-column="idclient" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
|
||||
} else if (col.type === "cliente_fornitore_id") {
|
||||
cell.innerHTML =
|
||||
`<select class="custom-field dropdown-select client-select searchable-client" data-column="cliente_fornitore_id" id="clienteFornitoreSelect"><option value="">Select a supplier...</option></select>` +
|
||||
`<select class="custom-field dropdown-select client-select searchable-client" data-column="cliente_fornitore_id" data-client-tipo="attivo" id="clienteFornitoreSelect"><option value="">Select a supplier...</option></select>` +
|
||||
`<button type="button" class="propagate-btn" data-column="cliente_fornitore_id" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
|
||||
} else if (col.type === "fixed" && col.dataType === "DATE") {
|
||||
cell.innerHTML =
|
||||
@@ -1081,8 +1237,14 @@
|
||||
} else if (col.type === "fixed") {
|
||||
const isApiField = !!fixedFieldApiConfig[col.key];
|
||||
if (isApiField) {
|
||||
const tipo = clientFieldTipoMap[col.key] || "";
|
||||
const isClientSourced =
|
||||
fixedFieldApiConfig[col.key]?.source === "clients";
|
||||
const clientCls = isClientSourced
|
||||
? " client-select searchable-client"
|
||||
: "";
|
||||
cell.innerHTML =
|
||||
`<select class="custom-field dropdown-select api-fixed-select fixed-top" data-column="fixed_${col.key}" data-fixed-key="${col.key}"><option value="">Seleziona...</option></select>` +
|
||||
`<select class="custom-field dropdown-select api-fixed-select fixed-top${clientCls}" data-column="fixed_${col.key}" data-fixed-key="${col.key}" data-client-tipo="${tipo}"><option value="">Seleziona...</option></select>` +
|
||||
`<button type="button" class="propagate-btn" data-column="fixed_${col.key}" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
|
||||
} else {
|
||||
cell.innerHTML =
|
||||
@@ -1117,12 +1279,12 @@
|
||||
const clientSel = document.getElementById("clientSelect");
|
||||
if (clientSel) {
|
||||
clientSel.innerHTML = buildClientOptionsHTML(meta.defaultIdclient);
|
||||
$(clientSel).select2(clientSelect2Config);
|
||||
initClientSelect2(clientSel);
|
||||
}
|
||||
const fornitSel = document.getElementById("clienteFornitoreSelect");
|
||||
if (fornitSel) {
|
||||
fornitSel.innerHTML = buildClientOptionsHTML("");
|
||||
$(fornitSel).select2(clientSelect2Config);
|
||||
initClientSelect2(fornitSel);
|
||||
}
|
||||
|
||||
// Fixed field selects in top row
|
||||
@@ -1132,7 +1294,7 @@
|
||||
|
||||
// Client-sourced → init as AJAX Select2
|
||||
if (config && config.source === "clients") {
|
||||
$(sel).select2(clientSelect2Config);
|
||||
initClientSelect2(sel);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1165,6 +1327,12 @@
|
||||
items.forEach((item) =>
|
||||
sel.add(new Option(item.text, item.id)),
|
||||
);
|
||||
$(sel).select2({
|
||||
placeholder: "Seleziona...",
|
||||
allowClear: true,
|
||||
width: "100%",
|
||||
...rankedLocalSelect2(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1187,6 +1355,59 @@
|
||||
function attachEvents() {
|
||||
if (!rowContainer) return;
|
||||
|
||||
// ── Auto-copy: Sample Arrival date/time → Unlock date/time ──────────
|
||||
// Coppie basate sui fieldId dei campi (source → target)
|
||||
const AUTO_COPY_FIELD_PAIRS = [
|
||||
{ from: 259, to: 260 }, // Sample Arrival date → Unlock Date
|
||||
{ from: 348, to: 349 }, // Sample Arrival time → Unlock time
|
||||
];
|
||||
|
||||
// Risolve fieldId → col.key (mapping id) usando le definizioni colonne
|
||||
function colKeyByFieldId(fieldId) {
|
||||
const col = columns.find(
|
||||
(c) =>
|
||||
(c.type === "detail" || c.type === "main_field") &&
|
||||
String(c.fieldId) === String(fieldId),
|
||||
);
|
||||
return col ? String(col.key) : null;
|
||||
}
|
||||
|
||||
// Precalcola le coppie come col.key (sourceKey → targetKey)
|
||||
var AUTO_COPY_KEY_PAIRS = AUTO_COPY_FIELD_PAIRS.map((p) => ({
|
||||
fromKey: colKeyByFieldId(p.from),
|
||||
toKey: colKeyByFieldId(p.to),
|
||||
})).filter((p) => p.fromKey && p.toKey);
|
||||
|
||||
function applyAutoCopy(rowIndex, changedColKey, value) {
|
||||
const pair = AUTO_COPY_KEY_PAIRS.find(
|
||||
(p) => p.fromKey === String(changedColKey),
|
||||
);
|
||||
if (!pair) return;
|
||||
|
||||
const row = data[rowIndex];
|
||||
if (!row) return;
|
||||
if (!row.details) row.details = {};
|
||||
|
||||
row.details[pair.toKey] = value;
|
||||
row._dirty = true;
|
||||
|
||||
// Aggiorna la cella target visibile, se presente
|
||||
const targetCell = rowContainer.querySelector(
|
||||
`.grid-cell[data-row="${rowIndex}"][data-col="${pair.toKey}"]`,
|
||||
);
|
||||
if (targetCell) {
|
||||
const input = targetCell.querySelector(".cell-input");
|
||||
if (input) {
|
||||
if ($(input).hasClass("select2-hidden-accessible")) {
|
||||
$(input).val(value).trigger("change.select2");
|
||||
} else {
|
||||
input.value = value;
|
||||
}
|
||||
}
|
||||
targetCell.classList.add("cell-changed");
|
||||
}
|
||||
}
|
||||
|
||||
// Cell value changes → write to gridData
|
||||
rowContainer.addEventListener("change", function (e) {
|
||||
const cell = e.target.closest(".grid-cell");
|
||||
@@ -1201,6 +1422,7 @@
|
||||
data[rowIndex].mainFieldValue = value;
|
||||
}
|
||||
setDetailValue(rowIndex, colKey, value);
|
||||
applyAutoCopy(rowIndex, colKey, value);
|
||||
} else if (colType === "fixed") {
|
||||
setFixedValue(rowIndex, colKey, value);
|
||||
} else if (colType === "idclient") {
|
||||
@@ -1354,6 +1576,11 @@
|
||||
|
||||
if (column === "idclient") {
|
||||
data.forEach((row) => {
|
||||
if (
|
||||
window.isTargetGridId &&
|
||||
!window.isTargetGridId(row.iddatadb)
|
||||
)
|
||||
return;
|
||||
const oldClientId = row.idclient || "";
|
||||
|
||||
row.idclient = value;
|
||||
@@ -1387,11 +1614,19 @@
|
||||
|
||||
if (column === "cliente_fornitore_id") {
|
||||
data.forEach((row) => {
|
||||
if (
|
||||
window.isTargetGridId &&
|
||||
!window.isTargetGridId(row.iddatadb)
|
||||
)
|
||||
return;
|
||||
row.cliente_fornitore_id = value;
|
||||
row._dirty = true;
|
||||
});
|
||||
|
||||
renderVisibleRows();
|
||||
updateColumnCells(
|
||||
"cliente_fornitore_id",
|
||||
"cliente_fornitore_id",
|
||||
);
|
||||
updateDirtyIndicator();
|
||||
return;
|
||||
}
|
||||
@@ -1400,21 +1635,39 @@
|
||||
const fixedKey = column.replace("fixed_", "");
|
||||
|
||||
data.forEach((row) => {
|
||||
if (
|
||||
window.isTargetGridId &&
|
||||
!window.isTargetGridId(row.iddatadb)
|
||||
)
|
||||
return;
|
||||
if (!row.fixedFields) row.fixedFields = {};
|
||||
row.fixedFields[fixedKey] = value;
|
||||
row._dirty = true;
|
||||
});
|
||||
|
||||
renderVisibleRows();
|
||||
updateColumnCells(fixedKey, "fixed");
|
||||
updateDirtyIndicator();
|
||||
return;
|
||||
}
|
||||
|
||||
if (col && (col.type === "detail" || col.type === "main_field")) {
|
||||
const autoPair = AUTO_COPY_KEY_PAIRS.find(
|
||||
(p) => p.fromKey === String(col.key),
|
||||
);
|
||||
|
||||
data.forEach((row) => {
|
||||
if (
|
||||
window.isTargetGridId &&
|
||||
!window.isTargetGridId(row.iddatadb)
|
||||
)
|
||||
return;
|
||||
if (!row.details) row.details = {};
|
||||
row.details[col.key] = value;
|
||||
|
||||
if (autoPair) {
|
||||
row.details[autoPair.toKey] = value;
|
||||
}
|
||||
|
||||
if (col.type === "main_field") {
|
||||
row.mainFieldValue = value;
|
||||
}
|
||||
@@ -1422,7 +1675,15 @@
|
||||
row._dirty = true;
|
||||
});
|
||||
|
||||
renderVisibleRows();
|
||||
updateColumnCells(col.key, col.type);
|
||||
// Se esiste una colonna auto-copiata (es. Arrival→Unlock), aggiorna anche quella
|
||||
if (autoPair) {
|
||||
const pairCol = columns.find(
|
||||
(c) => String(c.key) === String(autoPair.toKey),
|
||||
);
|
||||
if (pairCol) updateColumnCells(pairCol.key, pairCol.type);
|
||||
}
|
||||
|
||||
updateDirtyIndicator();
|
||||
return;
|
||||
}
|
||||
@@ -1585,7 +1846,7 @@
|
||||
$(this)
|
||||
.find(".searchable-client:not(.select2-hidden-accessible)")
|
||||
.each(function () {
|
||||
$(this).select2(clientSelect2Config);
|
||||
initClientSelect2(this);
|
||||
});
|
||||
$(this)
|
||||
.find(".searchable-dropdown:not(.select2-hidden-accessible)")
|
||||
@@ -1593,9 +1854,18 @@
|
||||
const fieldId = this.dataset.fieldId;
|
||||
if (fieldId) $(this).select2(sceltaSelect2Config(fieldId));
|
||||
});
|
||||
$(this)
|
||||
.find(".searchable-fixed:not(.select2-hidden-accessible)")
|
||||
.each(function () {
|
||||
$(this).select2({
|
||||
placeholder: "Seleziona...",
|
||||
allowClear: true,
|
||||
width: "100%",
|
||||
...rankedLocalSelect2(),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Column resize ──────────────────────────────────────────────────────
|
||||
|
||||
function initColumnResizers() {
|
||||
@@ -1726,3 +1996,127 @@
|
||||
},
|
||||
};
|
||||
})();
|
||||
// ===================
|
||||
// NAVIGAZIONE GRIGLIA CON FRECCE TASTIERA
|
||||
// ===================
|
||||
(function () {
|
||||
// Naviga solo tra input di testo/numero/time e date-picker.
|
||||
// I Select2 (client, fornitore, SceltaMultipla) sono esclusi:
|
||||
// lì le frecce servono al dropdown.
|
||||
function isNavigable(el) {
|
||||
if (!el || !el.classList) return false;
|
||||
if (!el.classList.contains("cell-input")) return false;
|
||||
// Escludi i select (nativi o Select2)
|
||||
if (el.tagName === "SELECT") return false;
|
||||
if (el.classList.contains("select2-hidden-accessible")) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Tutte le celle navigabili della riga, ordinate per data-index
|
||||
function navCellsInRow($row) {
|
||||
return $row.find(".grid-cell[data-index]").filter(function () {
|
||||
const input = this.querySelector(".cell-input");
|
||||
return isNavigable(input);
|
||||
});
|
||||
}
|
||||
|
||||
function focusCellInput($cell) {
|
||||
if (!$cell || !$cell.length) return;
|
||||
const input = $cell.get(0).querySelector(".cell-input");
|
||||
if (!input) return;
|
||||
input.focus();
|
||||
if (input.type !== "date" && typeof input.select === "function") {
|
||||
try {
|
||||
input.select();
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
function atStart(el) {
|
||||
if (el.type === "date" || el.type === "number" || el.type === "time")
|
||||
return true;
|
||||
return el.selectionStart === 0 && el.selectionEnd === 0;
|
||||
}
|
||||
|
||||
function atEnd(el) {
|
||||
if (el.type === "date" || el.type === "number" || el.type === "time")
|
||||
return true;
|
||||
const len = (el.value || "").length;
|
||||
return el.selectionStart === len && el.selectionEnd === len;
|
||||
}
|
||||
|
||||
$(document).on("keydown", "#gridRowContainer .cell-input", function (e) {
|
||||
const el = this;
|
||||
if (!isNavigable(el)) return;
|
||||
|
||||
// Ignora se un dropdown Select2 è aperto
|
||||
if ($(".select2-container--open").length) return;
|
||||
|
||||
const key = e.key;
|
||||
if (
|
||||
key !== "ArrowUp" &&
|
||||
key !== "ArrowDown" &&
|
||||
key !== "ArrowLeft" &&
|
||||
key !== "ArrowRight"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $cell = $(el).closest(".grid-cell");
|
||||
const $row = $cell.closest(".grid-row");
|
||||
if (!$cell.length || !$row.length) return;
|
||||
|
||||
const colIndex = $cell.attr("data-index");
|
||||
|
||||
// --- SU / GIÙ: stessa colonna (data-index), riga adiacente ---
|
||||
if (key === "ArrowUp" || key === "ArrowDown") {
|
||||
const $targetRow =
|
||||
key === "ArrowUp"
|
||||
? $row.prev(".grid-row")
|
||||
: $row.next(".grid-row");
|
||||
if (!$targetRow.length) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// Cella stessa colonna nella riga di destinazione
|
||||
let $target = $targetRow.find(
|
||||
`.grid-cell[data-index="${colIndex}"]`,
|
||||
);
|
||||
|
||||
// Se quella colonna non è navigabile (es. è un select),
|
||||
// cerca la più vicina navigabile nella riga
|
||||
if (
|
||||
!$target.length ||
|
||||
!isNavigable($target.get(0).querySelector(".cell-input"))
|
||||
) {
|
||||
const $cells = navCellsInRow($targetRow);
|
||||
if (!$cells.length) return;
|
||||
$target = $cells.first();
|
||||
}
|
||||
|
||||
focusCellInput($target);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- SINISTRA / DESTRA: cella navigabile precedente/successiva nella riga ---
|
||||
const $cells = navCellsInRow($row);
|
||||
const curPos = $cells.index($cell);
|
||||
if (curPos === -1) return;
|
||||
|
||||
if (key === "ArrowLeft") {
|
||||
if (!atStart(el)) return;
|
||||
if (curPos <= 0) return;
|
||||
e.preventDefault();
|
||||
focusCellInput($cells.eq(curPos - 1));
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "ArrowRight") {
|
||||
if (!atEnd(el)) return;
|
||||
if (curPos >= $cells.length - 1) return;
|
||||
e.preventDefault();
|
||||
focusCellInput($cells.eq(curPos + 1));
|
||||
return;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -130,10 +130,60 @@
|
||||
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'); ?>
|
||||
@@ -231,10 +281,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
function createButton(template) {
|
||||
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}`;
|
||||
@@ -245,17 +305,62 @@
|
||||
btn.setAttribute("data-source-type", sourceType);
|
||||
|
||||
btn.innerHTML = `
|
||||
<i class="${iconClass} template-icon"></i>
|
||||
<span>${escapeHtml(template.button_label || 'Unnamed')}</span>
|
||||
`;
|
||||
<i class="${iconClass} template-icon"></i>
|
||||
<span>${escapeHtml(template.button_label || 'Unnamed')}</span>
|
||||
`;
|
||||
|
||||
return btn;
|
||||
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 escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
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() {
|
||||
@@ -264,6 +369,26 @@
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -295,9 +420,11 @@
|
||||
return;
|
||||
}
|
||||
|
||||
grouped[type].forEach(template => {
|
||||
container.appendChild(createButton(template));
|
||||
});
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ if (!file_exists(__DIR__ . '/import_debug.log')) {
|
||||
error_log("Inizio importazione alle " . date('Y-m-d H:i:s'));
|
||||
|
||||
include('include/headscript.php');
|
||||
require_once __DIR__ . '/class/binding-functions.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['template_id']) || !isset($_POST['selected_rows']) || !isset($_POST['filename'])) {
|
||||
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Richiesta non valida"));
|
||||
@@ -40,6 +41,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();
|
||||
|
||||
@@ -47,7 +71,7 @@ $pdo = $db->getConnection();
|
||||
$importReferenceCode = date('YmdHis') . '-' . uniqid();
|
||||
|
||||
// Recupera tutti i mapping dal template
|
||||
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field, auto_value FROM template_mapping WHERE template_id = ?");
|
||||
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, has_list, is_required, manual_default, is_manual, field_label, field_id, main_field, auto_value FROM template_mapping WHERE template_id = ?");
|
||||
$stmt->execute([$template_id]);
|
||||
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
@@ -65,6 +89,23 @@ foreach ($allMappings as $mapping) {
|
||||
}
|
||||
}
|
||||
|
||||
$fixedMappings = [];
|
||||
try {
|
||||
$fxStmt = $pdo->prepare(
|
||||
"SELECT id, fixed_field_key, data_type, default_value, default_source, excel_column
|
||||
FROM template_fixed_mapping
|
||||
WHERE template_id = ?"
|
||||
);
|
||||
$fxStmt->execute([$template_id]);
|
||||
$fixedMappings = $fxStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (Exception $e) {
|
||||
error_log("[FIXED] lettura template_fixed_mapping fallita: " . $e->getMessage());
|
||||
}
|
||||
|
||||
$savedBindings = []; // risolti con un binding gia' salvato
|
||||
$autoBindings = []; // collegati in automatico (corrispondenza esatta)
|
||||
$pendingBindings = []; // senza corrispondenza: da risolvere a mano
|
||||
|
||||
// Inserisci le righe selezionate in datadb
|
||||
$insertedIds = [];
|
||||
foreach ($selected_rows as $rowIndex) {
|
||||
@@ -105,7 +146,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));
|
||||
@@ -144,6 +190,24 @@ foreach ($selected_rows as $rowIndex) {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
binding_is_list_field($mapping)
|
||||
&& $fieldValue !== null
|
||||
&& trim((string) $fieldValue) !== ''
|
||||
) {
|
||||
$fieldValue = binding_resolve_for_import(
|
||||
$pdo,
|
||||
$mapping,
|
||||
(int) $template_id,
|
||||
(string) $fieldValue,
|
||||
(int) $iddatadb,
|
||||
(int) $user_id,
|
||||
$savedBindings,
|
||||
$autoBindings,
|
||||
$pendingBindings
|
||||
);
|
||||
}
|
||||
|
||||
if ($mapping['is_required'] && (is_null($fieldValue) || $fieldValue === '')) {
|
||||
error_log("Required field missing for mapping ID: " . $mapping['id'] . ", field: " . $mapping['field_label']);
|
||||
}
|
||||
@@ -152,10 +216,114 @@ foreach ($selected_rows as $rowIndex) {
|
||||
$stmt->execute([$iddatadb, $mapping['id'], $fieldValue]);
|
||||
error_log("Inserted into import_data_details for ID $iddatadb, Mapping ID: " . $mapping['id'] . ", Field Value: " . var_export($fieldValue, true));
|
||||
}
|
||||
$fixedValues = [];
|
||||
foreach ($fixedMappings as $fm) {
|
||||
$fixedKey = (string) $fm['fixed_field_key'];
|
||||
$value = null;
|
||||
|
||||
if (($fm['default_source'] ?? 'manual') === 'xls' && trim((string) ($fm['excel_column'] ?? '')) !== '') {
|
||||
$idx = binding_find_column_index((string) $fm['excel_column'], $columns);
|
||||
$raw = ($idx !== false && isset($row[$idx])) ? trim((string) $row[$idx]) : '';
|
||||
|
||||
if ($raw !== '') {
|
||||
$value = binding_resolve_fixed_for_import(
|
||||
$pdo,
|
||||
$fm,
|
||||
(int) $template_id,
|
||||
$raw,
|
||||
(int) $iddatadb,
|
||||
(int) $user_id,
|
||||
$savedBindings,
|
||||
$autoBindings,
|
||||
$pendingBindings
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($value === null || $value === '') {
|
||||
$default = binding_fixed_default($fm);
|
||||
$value = $default !== '' ? $default : null;
|
||||
}
|
||||
|
||||
if ($value !== null && $value !== '') {
|
||||
$fixedValues[$fixedKey] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($fixedValues)) {
|
||||
binding_write_fixed_columns($pdo, (int) $iddatadb, $fixedValues);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 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;
|
||||
|
||||
if (!empty($pendingBindings)) {
|
||||
$_SESSION['pending_bindings'] = [
|
||||
'template_id' => $template_id,
|
||||
'importref' => $importReferenceCode,
|
||||
'items' => array_values($pendingBindings),
|
||||
'auto' => array_values($autoBindings),
|
||||
'saved' => array_values($savedBindings),
|
||||
];
|
||||
|
||||
header("Location: resolve_bindings.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
unset($_SESSION['pending_bindings']);
|
||||
|
||||
header("Location: imported.php?id=" . urlencode($template_id) . "&importref=" . urlencode($importReferenceCode));
|
||||
exit;
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
ini_set('display_errors', 0); // in AJAX gli errori devono NON sporcare il JSON
|
||||
error_reporting(E_ALL);
|
||||
ini_set('log_errors', 1);
|
||||
ini_set('error_log', __DIR__ . '/import_debug.log');
|
||||
|
||||
include('include/headscript.php');
|
||||
require_once __DIR__ . '/class/binding-functions.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Legge il body JSON
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (
|
||||
!$input ||
|
||||
!isset($input['template_id']) ||
|
||||
!isset($input['importreferencecode']) ||
|
||||
!isset($input['filename']) ||
|
||||
!isset($input['columns']) ||
|
||||
!isset($input['batch_rows']) ||
|
||||
!isset($input['batch_excelrows'])
|
||||
) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Richiesta non valida']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$template_id = intval($input['template_id']);
|
||||
$importReferenceCode = (string)$input['importreferencecode'];
|
||||
$newFilename = (string)$input['filename'];
|
||||
$columns = $input['columns']; // array
|
||||
$batchRows = $input['batch_rows']; // array di righe (ognuna array di celle)
|
||||
$batchExcelrows = $input['batch_excelrows']; // array di excelrow paralleli a batchRows
|
||||
|
||||
$user_id = $iduserlogin ?? 1;
|
||||
|
||||
function normalizeColName($s): string
|
||||
{
|
||||
$s = (string)$s;
|
||||
$s = str_replace(
|
||||
["\xC2\xA0", "\xE2\x80\xAF", "\xE2\x80\x87", "\xE2\x80\x89", "\xE2\x80\x8A", "\xEF\xBB\xBF"],
|
||||
' ',
|
||||
$s
|
||||
);
|
||||
$s = preg_replace('/\s+/u', ' ', $s);
|
||||
return trim((string)$s);
|
||||
}
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
// Recupera i mapping
|
||||
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, has_list, is_required, manual_default, is_manual, field_label, field_id, main_field, auto_value FROM template_mapping WHERE template_id = ?");
|
||||
$stmt->execute([$template_id]);
|
||||
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($allMappings)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Nessun mapping trovato per il template']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// idclient di default (una volta sola)
|
||||
$template_stmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
|
||||
$template_stmt->execute([$template_id]);
|
||||
$tpl = $template_stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$default_idclient = $tpl['idclient'] ?? null;
|
||||
|
||||
// lims_user_id per il campo 244 (una volta sola)
|
||||
$stmtUser = $pdo->prepare("SELECT lims_user_id FROM auth_users WHERE id = ? LIMIT 1");
|
||||
$stmtUser->execute([(int)$user_id]);
|
||||
$limsUserId = $stmtUser->fetchColumn();
|
||||
$limsUserId = ($limsUserId !== false && $limsUserId !== null && $limsUserId !== '') ? (string)$limsUserId : '';
|
||||
|
||||
$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();
|
||||
|
||||
$insertedIds = [];
|
||||
|
||||
$fixedMappings = [];
|
||||
try {
|
||||
$fxStmt = $pdo->prepare(
|
||||
"SELECT id, fixed_field_key, data_type, default_value, default_source, excel_column
|
||||
FROM template_fixed_mapping
|
||||
WHERE template_id = ?"
|
||||
);
|
||||
$fxStmt->execute([$template_id]);
|
||||
$fixedMappings = $fxStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (Exception $e) {
|
||||
error_log("[FIXED] lettura template_fixed_mapping fallita: " . $e->getMessage());
|
||||
}
|
||||
|
||||
$savedBindings = [];
|
||||
$autoBindings = [];
|
||||
$pendingBindings = [];
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$insDatadb = $pdo->prepare("INSERT INTO datadb (templateid, importreferencecode, filename_import, status, user_id, limscode, importdate, excelrow, idclient) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$insDetail = $pdo->prepare("INSERT INTO import_data_details (id, mapping_id, field_value) VALUES (?, ?, ?)");
|
||||
|
||||
foreach ($batchRows as $i => $row) {
|
||||
$excelrow = $batchExcelrows[$i] ?? null;
|
||||
if ($row === null || $excelrow === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$insDatadb->execute([
|
||||
$template_id,
|
||||
$importReferenceCode,
|
||||
$newFilename,
|
||||
'i',
|
||||
$user_id,
|
||||
null,
|
||||
date('Y-m-d'),
|
||||
$excelrow,
|
||||
$default_idclient
|
||||
]);
|
||||
|
||||
$iddatadb = $pdo->lastInsertId();
|
||||
$insertedIds[] = $iddatadb;
|
||||
|
||||
foreach ($allMappings as $mapping) {
|
||||
$fieldValue = null;
|
||||
if (!$mapping['is_manual']) {
|
||||
$excelColumn = trim($mapping['excel_column']);
|
||||
$excelColumnIndex = array_search(
|
||||
normalizeColName($excelColumn),
|
||||
array_map('normalizeColName', $columns)
|
||||
);
|
||||
if ($excelColumnIndex !== false && isset($row[$excelColumnIndex]) && $row[$excelColumnIndex] !== '') {
|
||||
$fieldValue = $row[$excelColumnIndex];
|
||||
} else {
|
||||
$fieldValue = $mapping['manual_default'] ?? '';
|
||||
}
|
||||
switch ($mapping['data_type']) {
|
||||
case 'INT':
|
||||
$fieldValue = is_numeric($fieldValue) ? (int)$fieldValue : ($mapping['manual_default'] ?? 0);
|
||||
break;
|
||||
case 'DATE':
|
||||
$fieldValue = !empty($fieldValue) ? date('Y-m-d', strtotime($fieldValue)) : ($mapping['manual_default'] === 'today' ? date('Y-m-d') : ($mapping['manual_default'] ?? ''));
|
||||
break;
|
||||
case 'CHAR':
|
||||
$fieldValue = !empty($fieldValue) ? substr((string)$fieldValue, 0, 1) : ($mapping['manual_default'] ?? '');
|
||||
break;
|
||||
case 'Testo':
|
||||
case 'VARCHAR':
|
||||
default:
|
||||
$fieldValue = !empty($fieldValue) ? (string)$fieldValue : ($mapping['manual_default'] ?? '');
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$fieldValue = $mapping['manual_default'] ?? '';
|
||||
if ($mapping['data_type'] === 'DATE' && $mapping['manual_default'] === 'today') {
|
||||
$fieldValue = date('Y-m-d');
|
||||
}
|
||||
}
|
||||
if (($fieldValue === null || $fieldValue === '') && !empty($mapping['auto_value']) && $mapping['auto_value'] !== 'none') {
|
||||
if ($mapping['auto_value'] === 'import_date') {
|
||||
$fieldValue = date('Y-m-d');
|
||||
} elseif ($mapping['auto_value'] === 'import_time') {
|
||||
$fieldValue = date('H:i');
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
binding_is_list_field($mapping)
|
||||
&& $fieldValue !== null
|
||||
&& trim((string) $fieldValue) !== ''
|
||||
) {
|
||||
$fieldValue = binding_resolve_for_import(
|
||||
$pdo,
|
||||
$mapping,
|
||||
$template_id,
|
||||
(string) $fieldValue,
|
||||
(int) $iddatadb,
|
||||
(int) $user_id,
|
||||
$savedBindings,
|
||||
$autoBindings,
|
||||
$pendingBindings
|
||||
);
|
||||
}
|
||||
|
||||
$insDetail->execute([$iddatadb, $mapping['id'], $fieldValue]);
|
||||
}
|
||||
|
||||
$fixedValues = [];
|
||||
foreach ($fixedMappings as $fm) {
|
||||
$fixedKey = (string) $fm['fixed_field_key'];
|
||||
$value = null;
|
||||
|
||||
if (($fm['default_source'] ?? 'manual') === 'xls' && trim((string) ($fm['excel_column'] ?? '')) !== '') {
|
||||
$idx = binding_find_column_index((string) $fm['excel_column'], $columns);
|
||||
$raw = ($idx !== false && isset($row[$idx])) ? trim((string) $row[$idx]) : '';
|
||||
|
||||
if ($raw !== '') {
|
||||
$value = binding_resolve_fixed_for_import(
|
||||
$pdo,
|
||||
$fm,
|
||||
(int) $template_id,
|
||||
$raw,
|
||||
(int) $iddatadb,
|
||||
(int) $user_id,
|
||||
$savedBindings,
|
||||
$autoBindings,
|
||||
$pendingBindings
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($value === null || $value === '') {
|
||||
$default = binding_fixed_default($fm);
|
||||
$value = $default !== '' ? $default : null;
|
||||
}
|
||||
|
||||
if ($value !== null && $value !== '') {
|
||||
$fixedValues[$fixedKey] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($fixedValues)) {
|
||||
binding_write_fixed_columns($pdo, (int) $iddatadb, $fixedValues);
|
||||
}
|
||||
|
||||
// Campo 244 (Accettatore) con lims_user_id
|
||||
if ($limsUserId !== '' && $mappingId244 > 0) {
|
||||
$insDetail->execute([$iddatadb, $mappingId244, $limsUserId]);
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
$state = $_SESSION['pending_bindings'] ?? null;
|
||||
if (!is_array($state) || ($state['importref'] ?? null) !== $importReferenceCode) {
|
||||
$state = [
|
||||
'template_id' => $template_id,
|
||||
'importref' => $importReferenceCode,
|
||||
'items' => [],
|
||||
'auto' => [],
|
||||
'saved' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$state['items'] = binding_merge_buckets($state['items'], $pendingBindings);
|
||||
$state['auto'] = binding_merge_buckets($state['auto'], $autoBindings);
|
||||
$state['saved'] = binding_merge_buckets($state['saved'], $savedBindings);
|
||||
|
||||
$_SESSION['pending_bindings'] = $state;
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'inserted' => count($insertedIds),
|
||||
'pending_bindings' => count($state['items'])
|
||||
]);
|
||||
exit;
|
||||
} catch (Exception $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
error_log("[BATCH IMPORT] " . $e->getMessage());
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
+177
-20
@@ -50,6 +50,9 @@ error_log("Loaded template: " . print_r($template, true));
|
||||
border-radius: 0.25rem;
|
||||
background: #f8f9fa;
|
||||
display: none;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1020;
|
||||
}
|
||||
|
||||
.top-scrollbar-inner {
|
||||
@@ -161,6 +164,7 @@ error_log("Loaded template: " . print_r($template, true));
|
||||
<div class="mb-3 text">
|
||||
<a href="imported.php?id=<?= $id ?>" class="btn btn-warning me-2">Imported (i)</a>
|
||||
<a href="tolims.php?id=<?= $id ?>" class="btn btn-success">To LIMS (l)</a>
|
||||
<a href="errors.php?id=<?= $id ?>" class="btn btn-danger">Errors (x)</a>
|
||||
</div>
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
@@ -341,8 +345,9 @@ error_log("Loaded template: " . print_r($template, true));
|
||||
<input type="hidden" name="excelrows" id="selectedExcelRowsData" value="">
|
||||
<input type="hidden" name="filename" value="${data.filename}">
|
||||
|
||||
<!-- TOP BUTTON -->
|
||||
<div class="d-flex justify-content-end mb-3">
|
||||
<!-- TOP BUTTON -->
|
||||
<div class="d-flex justify-content-end align-items-center mb-3 gap-2">
|
||||
<span class="badge bg-secondary" id="selectedCountTop">0 selezionati</span>
|
||||
<button type="submit" class="btn btn-primary" id="proceedButtonTop" disabled>Prosegui</button>
|
||||
</div>
|
||||
|
||||
@@ -383,46 +388,191 @@ error_log("Loaded template: " . print_r($template, true));
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- BOTTOM BUTTON -->
|
||||
<button type="submit" class="btn btn-primary mt-3" id="proceedButtonBottom" disabled>Prosegui</button>
|
||||
<!-- BOTTOM BUTTON -->
|
||||
<div class="d-flex align-items-center mt-3 gap-2">
|
||||
<span class="badge bg-secondary" id="selectedCountBottom">0 selezionati</span>
|
||||
<button type="submit" class="btn btn-primary" id="proceedButtonBottom" disabled>Prosegui</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
tableContainer.innerHTML = html;
|
||||
|
||||
const selectRowsForm = document.getElementById('selectRowsForm');
|
||||
|
||||
selectRowsForm.addEventListener('submit', function(e) {
|
||||
// Clic sulla cella (oltre che sulla checkbox) per selezionare/deselezionare la riga
|
||||
document.querySelectorAll('#importPreviewTable tbody tr').forEach(tr => {
|
||||
tr.style.cursor = 'pointer';
|
||||
tr.addEventListener('click', function(e) {
|
||||
// Se ho cliccato proprio sulla checkbox, lascio fare a lei (evita doppio toggle)
|
||||
if (e.target.classList.contains('row-checkbox')) return;
|
||||
|
||||
const cb = this.querySelector('.row-checkbox');
|
||||
if (!cb) return;
|
||||
cb.checked = !cb.checked;
|
||||
cb.dispatchEvent(new Event('change', {
|
||||
bubbles: true
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
selectRowsForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault(); // gestiamo tutto via AJAX a batch
|
||||
|
||||
const checkedBoxes = Array.from(document.querySelectorAll('.row-checkbox:checked'));
|
||||
|
||||
if (checkedBoxes.length === 0) {
|
||||
e.preventDefault();
|
||||
alert('Seleziona almeno una riga.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Raccoglie le righe selezionate (riga mappata + excelrow parallelo)
|
||||
const selectedRows = [];
|
||||
const selectedExcelRows = [];
|
||||
|
||||
checkedBoxes.forEach((cb, newIndex) => {
|
||||
checkedBoxes.forEach(cb => {
|
||||
const originalIndex = parseInt(cb.value, 10);
|
||||
|
||||
if (data.rows && data.rows[originalIndex]) {
|
||||
selectedRows.push(data.rows[originalIndex]);
|
||||
selectedExcelRows.push(
|
||||
(data.excel_data && data.excel_data[originalIndex]) ?
|
||||
data.excel_data[originalIndex].excelrow :
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
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));
|
||||
const total = selectedRows.length;
|
||||
if (total === 0) {
|
||||
alert('Nessuna riga valida da importare.');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('selectedExcelRowsData').value =
|
||||
encodeURIComponent(JSON.stringify(selectedExcelRows));
|
||||
// importreferencecode generato UNA volta, condiviso da tutti i batch
|
||||
const importReferenceCode =
|
||||
new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14) +
|
||||
'-' + Math.random().toString(36).slice(2, 10);
|
||||
|
||||
const BATCH_SIZE = 20;
|
||||
|
||||
// Disabilita i pulsanti e mostra la barra di progresso
|
||||
const btnTop = document.getElementById('proceedButtonTop');
|
||||
const btnBottom = document.getElementById('proceedButtonBottom');
|
||||
if (btnTop) btnTop.disabled = true;
|
||||
if (btnBottom) btnBottom.disabled = true;
|
||||
|
||||
const progressHtml = `
|
||||
<div id="importProgressWrap" style="
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 20000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;">
|
||||
<div style="
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 25px 30px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.3);">
|
||||
<div class="mb-2"><strong>Importazione in corso...</strong>
|
||||
<span id="importProgressText" class="float-end">0 / ${total}</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 26px;">
|
||||
<div id="importProgressBar" class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar" style="width: 0%;">0%</div>
|
||||
</div>
|
||||
<div id="importProgressError" class="alert alert-danger mt-3 mb-0" style="display:none;"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.insertAdjacentHTML('beforeend', progressHtml);
|
||||
|
||||
const progressBar = document.getElementById('importProgressBar');
|
||||
const progressText = document.getElementById('importProgressText');
|
||||
const progressErr = document.getElementById('importProgressError');
|
||||
|
||||
let insertedTotal = 0;
|
||||
let pendingBindings = 0; // valori a lista senza corrispondenza LIMS
|
||||
|
||||
// Invia un batch e ritorna la risposta JSON
|
||||
async function sendBatch(startIndex) {
|
||||
const batchRows = selectedRows.slice(startIndex, startIndex + BATCH_SIZE);
|
||||
const batchExcelrows = selectedExcelRows.slice(startIndex, startIndex + BATCH_SIZE);
|
||||
|
||||
const resp = await fetch('import_insert_batch.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
template_id: data.template_id,
|
||||
importreferencecode: importReferenceCode,
|
||||
filename: data.filename,
|
||||
columns: data.columns,
|
||||
batch_rows: batchRows,
|
||||
batch_excelrows: batchExcelrows
|
||||
})
|
||||
});
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
// Cicla i batch in sequenza
|
||||
for (let start = 0; start < total; start += BATCH_SIZE) {
|
||||
let result;
|
||||
try {
|
||||
result = await sendBatch(start);
|
||||
} catch (err) {
|
||||
result = {
|
||||
ok: false,
|
||||
error: err.message
|
||||
};
|
||||
}
|
||||
|
||||
if (!result.ok) {
|
||||
// Batch fallito: chiediamo all'utente se continuare
|
||||
progressErr.textContent =
|
||||
'Errore su un blocco (righe ' + (start + 1) + '-' +
|
||||
Math.min(start + BATCH_SIZE, total) + '): ' + (result.error || 'errore sconosciuto');
|
||||
progressErr.style.display = 'block';
|
||||
|
||||
const continua = confirm(
|
||||
'Un blocco di righe non è stato importato.\n\n' +
|
||||
'Righe inserite finora: ' + insertedTotal + ' di ' + total + '.\n\n' +
|
||||
'Vuoi continuare con i blocchi successivi? (Annulla = interrompi)'
|
||||
);
|
||||
|
||||
if (!continua) {
|
||||
progressBar.classList.remove('progress-bar-animated');
|
||||
progressBar.classList.add('bg-warning');
|
||||
return;
|
||||
}
|
||||
// se continua, prosegue col prossimo blocco
|
||||
continue;
|
||||
}
|
||||
|
||||
insertedTotal += (result.inserted || 0);
|
||||
pendingBindings = result.pending_bindings || pendingBindings;
|
||||
const done = Math.min(start + BATCH_SIZE, total);
|
||||
const pct = Math.round((done / total) * 100);
|
||||
progressBar.style.width = pct + '%';
|
||||
progressBar.textContent = pct + '%';
|
||||
progressText.textContent = insertedTotal + ' / ' + total;
|
||||
}
|
||||
|
||||
// Fine: barra verde e redirect a imported.php
|
||||
progressBar.classList.remove('progress-bar-animated', 'progress-bar-striped');
|
||||
progressBar.classList.add('bg-success');
|
||||
progressText.textContent = insertedTotal + ' / ' + total + ' completate';
|
||||
|
||||
// PAUSA temporanea di 2 secondi per vedere la barra completa (da rimuovere dopo il test)
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
window.location.href = pendingBindings > 0
|
||||
? 'resolve_bindings.php'
|
||||
: 'imported.php?id=' +
|
||||
encodeURIComponent(data.template_id) +
|
||||
'&importref=' + encodeURIComponent(importReferenceCode);
|
||||
});
|
||||
|
||||
const topTableScrollbar = document.getElementById('topTableScrollbar');
|
||||
@@ -472,10 +622,17 @@ error_log("Loaded template: " . print_r($template, true));
|
||||
const checkboxes = document.querySelectorAll('.row-checkbox');
|
||||
|
||||
function updateProceedButton() {
|
||||
const enabled = Array.from(checkboxes).some(cb => cb.checked);
|
||||
const checkedCount = Array.from(checkboxes).filter(cb => cb.checked).length;
|
||||
const enabled = checkedCount > 0;
|
||||
|
||||
if (proceedButtonTop) proceedButtonTop.disabled = !enabled;
|
||||
if (proceedButtonBottom) proceedButtonBottom.disabled = !enabled;
|
||||
|
||||
const label = checkedCount + ' selezionati';
|
||||
const countTop = document.getElementById('selectedCountTop');
|
||||
const countBottom = document.getElementById('selectedCountBottom');
|
||||
if (countTop) countTop.textContent = label;
|
||||
if (countBottom) countBottom.textContent = label;
|
||||
}
|
||||
|
||||
selectAllCheckbox.addEventListener('change', function() {
|
||||
|
||||
+363
-28
@@ -99,7 +99,7 @@ $stmt = $pdo->prepare("
|
||||
FROM datadb d
|
||||
LEFT JOIN auth_users u ON d.user_id = u.id
|
||||
{$baseWhere}
|
||||
ORDER BY d.excelrow ASC, d.iddatadb ASC
|
||||
ORDER BY CAST(d.excelrow AS UNSIGNED) ASC, d.excelrow ASC, d.iddatadb ASC
|
||||
{$limitClause}
|
||||
");
|
||||
$stmt->execute($baseParams);
|
||||
@@ -147,6 +147,7 @@ $desiredOrder = [
|
||||
'ClienteResponsabile',
|
||||
'ClienteFornitore',
|
||||
'ClienteAnalisi',
|
||||
'ClienteFatturazione',
|
||||
'AnagraficaCertestObject',
|
||||
'AnagraficaCertestService',
|
||||
'MoltiplicatorePrezzo',
|
||||
@@ -181,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',
|
||||
@@ -285,6 +287,21 @@ $gridColumns[] = ['type' => 'idclient', 'key' => 'idclient', 'label' => 'Client'
|
||||
// 4. Cliente Fornitore
|
||||
$gridColumns[] = ['type' => 'cliente_fornitore_id', 'key' => 'cliente_fornitore_id', 'label' => $slugMapping['ClienteFornitore'] ?? 'ClienteFornitore', 'width' => 300];
|
||||
|
||||
// 4b. Fatturazione Avanzata — subito dopo Cliente Fornitore
|
||||
foreach ($fixedFields as $f) {
|
||||
if ($f['fixed_field_key'] === 'ClienteFatturazione') {
|
||||
$gridColumns[] = [
|
||||
'type' => 'fixed',
|
||||
'key' => 'ClienteFatturazione',
|
||||
'label' => 'Fatturazione avanzata',
|
||||
'dataType' => $f['data_type'],
|
||||
'isRequired' => (bool)$f['is_required'],
|
||||
'width' => 180,
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Other custom fields in schema order
|
||||
foreach ($allMappings as $mapping) {
|
||||
if (
|
||||
@@ -321,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,
|
||||
@@ -330,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];
|
||||
@@ -468,8 +497,16 @@ $gridMeta = [
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
background: #f8f9fa;
|
||||
position: sticky;
|
||||
top: 70px;
|
||||
/* regola sull'altezza reale della tua topbar fissa */
|
||||
position: sticky;
|
||||
top: 70px;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.top-scrollbar-inner {
|
||||
height: 1px;
|
||||
}
|
||||
@@ -750,6 +787,7 @@ $gridMeta = [
|
||||
padding: 10px 0;
|
||||
min-height: 0;
|
||||
flex-wrap: nowrap;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.grid-top .grid-cell {
|
||||
@@ -1291,6 +1329,195 @@ $gridMeta = [
|
||||
outline: 2px solid #dc3545 !important;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* ── Filtri / selezione righe ── */
|
||||
#filtersToggleBtn.active {
|
||||
background-color: #0dcaf0 !important;
|
||||
color: #fff !important;
|
||||
border-color: #0dcaf0 !important;
|
||||
}
|
||||
|
||||
/* checkbox dentro la cella Actions */
|
||||
.filter-cb-wrap {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.grid-container.filters-on .filter-cb-wrap {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.filter-cb-wrap input {
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.grid-container.filters-on .button-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.grid-row.row-selected {
|
||||
background-color: #cfe9ff !important;
|
||||
}
|
||||
|
||||
.grid-row.row-selected .button-cell,
|
||||
.grid-row.row-selected .grid-cell:nth-child(2),
|
||||
.grid-row.row-selected .grid-cell:nth-child(3) {
|
||||
background-color: #cfe9ff !important;
|
||||
}
|
||||
|
||||
.grid-filter-row {
|
||||
background: #eef6f9 !important;
|
||||
border-bottom: 2px solid #0dcaf0;
|
||||
}
|
||||
|
||||
.grid-filter-row .grid-cell {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.grid-filter-row .filter-actions-cell {
|
||||
background: #eef6f9 !important;
|
||||
}
|
||||
|
||||
.grid-container.filters-on .grid-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.grid-container.filters-on .grid-row input,
|
||||
.grid-container.filters-on .grid-row select,
|
||||
.grid-container.filters-on .grid-row textarea,
|
||||
.grid-container.filters-on .grid-row button,
|
||||
.grid-container.filters-on .grid-row a {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
/* Sticky su riga propagazione e filtri (fix scroll orizzontale) */
|
||||
.grid-top .grid-cell.save-all-cell {
|
||||
position: sticky !important;
|
||||
left: 0;
|
||||
z-index: 9;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.grid-top .grid-cell:nth-child(2) {
|
||||
position: sticky !important;
|
||||
left: 210px;
|
||||
z-index: 8;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* Card-header azioni sticky sotto la topbar */
|
||||
:root {
|
||||
--actions-top: 0px;
|
||||
/* posizione verticale barra azioni: abbassa/alza questo */
|
||||
--actions-height: 58px;
|
||||
/* altezza reale del card-header azioni */
|
||||
--scrollbar-height: 18px;
|
||||
/* altezza della .top-scrollbar */
|
||||
}
|
||||
|
||||
.card-header.sticky-actions {
|
||||
position: sticky;
|
||||
top: var(--actions-top);
|
||||
z-index: 40;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* La barra di scroll superiore segue sempre il card-header sticky */
|
||||
.top-scrollbar {
|
||||
top: calc(var(--actions-top) + var(--actions-height)) !important;
|
||||
}
|
||||
|
||||
/* Wrapper del grid-top: sticky verticale + scroll orizzontale proprio */
|
||||
.grid-top-scroller {
|
||||
position: sticky;
|
||||
top: calc(var(--actions-top) + var(--actions-height) + var(--scrollbar-height));
|
||||
z-index: 25;
|
||||
background: #fff;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
/* nasconde la barra di scroll di questo wrapper: usiamo la top-scrollbar e la griglia */
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.grid-top-scroller::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Tastino toggle sticky */
|
||||
.sticky-toggle-btn {
|
||||
border: 1px solid #ced4da;
|
||||
background: #fff;
|
||||
color: #adb5bd;
|
||||
transition: all .15s;
|
||||
}
|
||||
|
||||
.sticky-toggle-btn:hover {
|
||||
background: #f1f3f5;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.sticky-toggle-btn.active {
|
||||
background: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sticky-toggle-btn.active:hover {
|
||||
background: #0b5ed7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Icona ruotata quando sbloccato (puntina "storta") */
|
||||
.sticky-toggle-btn:not(.active) i {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
/* Quando sbloccato, card-header e grid-top non sono più sticky verticalmente */
|
||||
.card-header.actions-unpinned {
|
||||
position: static !important;
|
||||
top: auto !important;
|
||||
}
|
||||
|
||||
.grid-top-scroller.actions-unpinned {
|
||||
position: static !important;
|
||||
top: auto !important;
|
||||
}
|
||||
|
||||
<?php if (isset($mainFieldMappings) && count($mainFieldMappings) >= 2): ?>.grid-top .grid-cell:nth-child(3) {
|
||||
position: sticky !important;
|
||||
left: 360px;
|
||||
z-index: 7;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
<?php endif; ?>.grid-filter-row .filter-actions-cell {
|
||||
position: sticky !important;
|
||||
left: 0;
|
||||
z-index: 9;
|
||||
background: #eef6f9 !important;
|
||||
}
|
||||
|
||||
.grid-filter-row .grid-cell:nth-child(2) {
|
||||
position: sticky !important;
|
||||
left: 210px;
|
||||
z-index: 8;
|
||||
background: #eef6f9 !important;
|
||||
}
|
||||
|
||||
<?php if (isset($mainFieldMappings) && count($mainFieldMappings) >= 2): ?>.grid-filter-row .grid-cell:nth-child(3) {
|
||||
position: sticky !important;
|
||||
left: 360px;
|
||||
z-index: 7;
|
||||
background: #eef6f9 !important;
|
||||
}
|
||||
|
||||
<?php endif; ?>
|
||||
</style>
|
||||
<title>Edit Imported Data - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
</head>
|
||||
@@ -1303,7 +1530,8 @@ $gridMeta = [
|
||||
<div class="page-content">
|
||||
<div class="mb-3 text d-flex align-items-center gap-2">
|
||||
<a href="imported.php?id=<?= $template_id ?>" class="btn btn-warning me-2">Imported (i)</a>
|
||||
<a href="tolims.php?id=<?= $template_id ?>" class="btn btn-success">To LIMS (l)</a>
|
||||
<a href="tolims.php?id=<?= $template_id ?>" class="btn btn-success me-2">To LIMS (l)</a>
|
||||
<a href="errors.php?id=<?= $template_id ?>" class="btn btn-danger">Errors (x)</a>
|
||||
<?php if ($importref === ''): ?>
|
||||
<span class="ms-3">
|
||||
<label class="form-check-label" style="font-size: 13px; cursor: pointer;">
|
||||
@@ -1365,19 +1593,11 @@ $gridMeta = [
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<div class="card-header sticky-actions">
|
||||
<div class="d-flex align-items-center" style="min-height: 42px; gap: 12px;">
|
||||
<div class="dropdown actions-dropdown" style="flex-shrink: 0;">
|
||||
<button class="dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fas fa-cogs"></i> Actions
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
|
||||
<li><a class="dropdown-item export-all-lims-btn" href="#"><i class="fas fa-upload" style="color: #eb0b0b;"></i>Export All</a></li>
|
||||
|
||||
<li><a class="dropdown-item save-all-btn" href="#"><i class="fas fa-save" style="color: #28a745;"></i>Save All</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<button type="button" id="toggleStickyBtn" class="btn btn-sm sticky-toggle-btn active" style="flex-shrink:0;" title="Blocca/sblocca barra azioni"><i class="fas fa-thumbtack"></i></button>
|
||||
<a href="#" class="btn btn-outline-danger btn-sm export-all-lims-btn" style="flex-shrink:0;"><i class="fas fa-upload"></i> Export All</a>
|
||||
<a href="#" class="btn btn-outline-success btn-sm save-all-btn" style="flex-shrink:0;"><i class="fas fa-save"></i> Save All</a>
|
||||
<button type="button" class="btn btn-outline-success btn-sm" id="addRowBtn" style="flex-shrink:0;"><i class="fas fa-plus"></i> Add Row</button>
|
||||
<div id="unsavedChanges" style="display:none; color: red; font-weight: bold; font-size: 13px;">
|
||||
⚠️ Unsaved changes detected!
|
||||
@@ -1398,8 +1618,11 @@ $gridMeta = [
|
||||
<div class="top-scrollbar-inner" id="topScrollbarInner"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid-container" id="gridContainer">
|
||||
<div class="grid-top-scroller" id="gridTopScroller">
|
||||
<div class="grid-top" id="gridTopContainer"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid-container" id="gridContainer">
|
||||
<div class="grid-row" id="gridHeaderContainer" style="font-weight:600;"></div>
|
||||
<div id="gridRowContainer"></div>
|
||||
</div>
|
||||
@@ -1428,6 +1651,7 @@ $gridMeta = [
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<script src="tracking.js"></script>
|
||||
<script src="gridRenderer.js"></script>
|
||||
<script src="gridFilter.js"></script>
|
||||
<script src="saveAll.js"></script>
|
||||
<script src="exportLims_gridData.js"></script>
|
||||
<script src="modals_gridData.js"></script>
|
||||
@@ -1469,21 +1693,43 @@ $gridMeta = [
|
||||
topScrollbar.scrollLeft = gridContainer.scrollLeft;
|
||||
}
|
||||
|
||||
const gridTopScroller = document.getElementById('gridTopScroller');
|
||||
let syncing = false;
|
||||
|
||||
function syncScroll(source) {
|
||||
if (syncing) return;
|
||||
syncing = true;
|
||||
const x = source.scrollLeft;
|
||||
if (source !== topScrollbar) topScrollbar.scrollLeft = x;
|
||||
if (source !== gridContainer) gridContainer.scrollLeft = x;
|
||||
if (gridTopScroller && source !== gridTopScroller) gridTopScroller.scrollLeft = x;
|
||||
syncing = false;
|
||||
}
|
||||
|
||||
topScrollbar.addEventListener('scroll', function() {
|
||||
if (syncingFromGrid) return;
|
||||
syncingFromTop = true;
|
||||
gridContainer.scrollLeft = topScrollbar.scrollLeft;
|
||||
syncingFromTop = false;
|
||||
syncScroll(topScrollbar);
|
||||
});
|
||||
|
||||
gridContainer.addEventListener('scroll', function() {
|
||||
if (syncingFromTop) return;
|
||||
syncingFromGrid = true;
|
||||
topScrollbar.scrollLeft = gridContainer.scrollLeft;
|
||||
syncingFromGrid = false;
|
||||
syncScroll(gridContainer);
|
||||
});
|
||||
if (gridTopScroller) {
|
||||
gridTopScroller.addEventListener('scroll', function() {
|
||||
syncScroll(gridTopScroller);
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('resize', updateTopScrollbarWidth);
|
||||
let rafPending = false;
|
||||
|
||||
function scheduleUpdate() {
|
||||
if (rafPending) return;
|
||||
rafPending = true;
|
||||
requestAnimationFrame(function() {
|
||||
rafPending = false;
|
||||
updateTopScrollbarWidth();
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('resize', scheduleUpdate);
|
||||
|
||||
// Recalculate after JS grid rendering
|
||||
setTimeout(updateTopScrollbarWidth, 100);
|
||||
@@ -1492,13 +1738,13 @@ $gridMeta = [
|
||||
setTimeout(updateTopScrollbarWidth, 1500);
|
||||
|
||||
// Recalculate automatically when rows/header/top controls are rendered or changed
|
||||
const observer = new MutationObserver(updateTopScrollbarWidth);
|
||||
const observer = new MutationObserver(scheduleUpdate);
|
||||
|
||||
if (gridContainer) {
|
||||
observer.observe(gridContainer, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
attributes: false
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1663,7 +1909,96 @@ $gridMeta = [
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sessionModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:99999;align-items:center;justify-content:center">
|
||||
<div style="background:#fff;padding:28px 32px;border-radius:14px;max-width:420px;text-align:center;box-shadow:0 20px 60px rgba(0,0,0,.3)">
|
||||
<h5 style="margin:0 0 10px">La sessione sta per scadere</h5>
|
||||
<p style="color:#64748b;margin:0 0 6px">Per la tua sicurezza verrai disconnesso tra</p>
|
||||
<div id="sessCountdownBig" style="font:700 34px monospace;color:#dc2626;margin:8px 0 18px">00:30</div>
|
||||
<button id="sessStayBtn" class="btn btn-primary btn-lg px-4">Resta connesso</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
// ── TEST: tempi compressi. In produzione: SESSION_MINUTES=120, WARN_SECONDS=120
|
||||
const SESSION_MINUTES = 110; // durata simulata
|
||||
const WARN_SECONDS = 30; // preavviso prima della scadenza
|
||||
const LOGIN_URL = '../login'; // adatta al tuo path reale
|
||||
|
||||
const sessionMs = SESSION_MINUTES * 60 * 1000;
|
||||
let lastActivity = Date.now();
|
||||
let modalShown = false;
|
||||
|
||||
// ogni attività reale resetta il timer locale
|
||||
['mousemove', 'keydown', 'click', 'scroll', 'touchstart'].forEach(ev =>
|
||||
document.addEventListener(ev, () => {
|
||||
lastActivity = Date.now();
|
||||
if (!modalShown) return; // se il modal è aperto, l'attività NON conta: serve il click esplicito
|
||||
}, {
|
||||
passive: true
|
||||
})
|
||||
);
|
||||
|
||||
const modal = document.getElementById('sessionModal');
|
||||
const bigCd = document.getElementById('sessCountdownBig');
|
||||
|
||||
document.getElementById('sessStayBtn').addEventListener('click', async () => {
|
||||
try {
|
||||
const r = await fetch('session_keepalive.php', {
|
||||
cache: 'no-store'
|
||||
});
|
||||
// se la sessione era già morta, headscript reindirizza: la risposta non sarà il JSON atteso
|
||||
const ct = r.headers.get('content-type') || '';
|
||||
if (!ct.includes('application/json')) {
|
||||
window.location.href = LOGIN_URL;
|
||||
return;
|
||||
}
|
||||
await r.json();
|
||||
lastActivity = Date.now(); // timer riparte
|
||||
modalShown = false;
|
||||
modal.style.display = 'none';
|
||||
} catch (e) {
|
||||
window.location.href = LOGIN_URL;
|
||||
}
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
const elapsed = Date.now() - lastActivity;
|
||||
const remaining = Math.ceil((sessionMs - elapsed) / 1000);
|
||||
|
||||
if (remaining <= 0) {
|
||||
window.location.href = LOGIN_URL; // scaduta
|
||||
return;
|
||||
}
|
||||
if (remaining <= WARN_SECONDS && !modalShown) {
|
||||
modalShown = true;
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
if (modalShown) {
|
||||
const m = Math.floor(remaining / 60),
|
||||
s = remaining % 60;
|
||||
bigCd.textContent = String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
|
||||
}
|
||||
}, 1000);
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
(function() {
|
||||
const btn = document.getElementById('toggleStickyBtn');
|
||||
const cardHeader = document.querySelector('.card-header.sticky-actions');
|
||||
if (!btn || !cardHeader) return;
|
||||
|
||||
const gridTopScroller = document.getElementById('gridTopScroller');
|
||||
btn.addEventListener('click', function() {
|
||||
const pinned = btn.classList.toggle('active');
|
||||
cardHeader.classList.toggle('actions-unpinned', !pinned);
|
||||
if (gridTopScroller) gridTopScroller.classList.toggle('actions-unpinned', !pinned);
|
||||
btn.title = pinned ?
|
||||
'Sblocca barra azioni' :
|
||||
'Blocca barra azioni';
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
if (!function_exists('gdbFixedDefaultValue')) {
|
||||
/** Default di un fixed field (DATE 'today' → data odierna). */
|
||||
function gdbFixedDefaultValue(array $f): string
|
||||
{
|
||||
$v = $f['default_value'] ?? '';
|
||||
if (($f['data_type'] ?? '') === 'DATE' && $v === 'today') {
|
||||
return date('Y-m-d');
|
||||
}
|
||||
return (string)$v;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('buildGridConfig')) {
|
||||
/**
|
||||
* Config del template necessaria per costruire le righe: fixed fields (ordinati
|
||||
* come in imported.php), main field mappings, alias map, default idclient.
|
||||
*
|
||||
* @return array{fixedFields:array, fixedAliasMap:array, mainFieldMappings:array, default_idclient:mixed}
|
||||
*/
|
||||
function buildGridConfig(PDO $pdo, int $templateId): array
|
||||
{
|
||||
// Mappa logica fixed_field_key → colonna reale su datadb (come imported.php)
|
||||
$fixedAliasMap = [
|
||||
'ClienteResponsabile' => 'cliente_responsabile_id',
|
||||
'ClienteFornitore' => 'cliente_fornitore_id',
|
||||
'ClienteAnalisi' => 'clienteAnalisi',
|
||||
'ClienteFatturazione' => 'ClienteFatturazione',
|
||||
'MoltiplicatorePrezzo' => 'moltiplicatore_prezzo_id',
|
||||
'AnagraficaCertestObject' => 'anagrafica_certest_object_id',
|
||||
'AnagraficaCertestService' => 'anagrafica_certest_service_id',
|
||||
'ConsegnaRichiesta' => 'consegna_richiesta',
|
||||
];
|
||||
|
||||
// Fixed fields visibili, ordinati come imported.php
|
||||
$fixedStmt = $pdo->prepare("
|
||||
SELECT id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import
|
||||
FROM template_fixed_mapping
|
||||
WHERE template_id = ? AND is_visible_import = 1
|
||||
ORDER BY id
|
||||
");
|
||||
$fixedStmt->execute([$templateId]);
|
||||
$fixedFieldsRaw = $fixedStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$desiredOrder = [
|
||||
'ClienteResponsabile', 'ClienteFornitore', 'ClienteAnalisi', 'ClienteFatturazione',
|
||||
'AnagraficaCertestObject', 'AnagraficaCertestService', 'MoltiplicatorePrezzo', 'ConsegnaRichiesta',
|
||||
];
|
||||
$excludeFromFixed = ['ClienteFornitore']; // reso come colonna a sé
|
||||
|
||||
$fixedFields = [];
|
||||
$tempMap = [];
|
||||
foreach ($fixedFieldsRaw as $f) {
|
||||
if (in_array($f['fixed_field_key'], $excludeFromFixed, true)) continue;
|
||||
$tempMap[$f['fixed_field_key']] = $f;
|
||||
}
|
||||
foreach ($desiredOrder as $key) {
|
||||
if (isset($tempMap[$key])) {
|
||||
$fixedFields[] = $tempMap[$key];
|
||||
unset($tempMap[$key]);
|
||||
}
|
||||
}
|
||||
foreach ($tempMap as $f) {
|
||||
$fixedFields[] = $f;
|
||||
}
|
||||
|
||||
// Main field mappings (max 2), come imported.php
|
||||
$mapStmt = $pdo->prepare("
|
||||
SELECT id, field_label, data_type, is_required, field_id, field_order,
|
||||
main_field, is_visible_import, manual_default
|
||||
FROM template_mapping
|
||||
WHERE template_id = ?
|
||||
ORDER BY field_order ASC, id ASC
|
||||
");
|
||||
$mapStmt->execute([$templateId]);
|
||||
$allMappings = $mapStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$mainFieldMappings = [];
|
||||
foreach ($allMappings as $mapping) {
|
||||
if ((string)$mapping['main_field'] === '1' && (int)$mapping['is_visible_import'] === 1) {
|
||||
$mainFieldMappings[] = $mapping;
|
||||
}
|
||||
if (count($mainFieldMappings) >= 2) break;
|
||||
}
|
||||
|
||||
// Default idclient dal template
|
||||
$tplStmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
|
||||
$tplStmt->execute([$templateId]);
|
||||
$default_idclient = $tplStmt->fetchColumn();
|
||||
$default_idclient = $default_idclient !== false ? $default_idclient : null;
|
||||
|
||||
return [
|
||||
'fixedFields' => $fixedFields,
|
||||
'fixedAliasMap' => $fixedAliasMap,
|
||||
'mainFieldMappings' => $mainFieldMappings,
|
||||
'default_idclient' => $default_idclient,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('buildGridRows')) {
|
||||
/**
|
||||
* Costruisce le righe gridData per la lista di iddatadb data, nell'ordine passato.
|
||||
*
|
||||
* @param int[] $iddatadbList
|
||||
* @param array $config output di buildGridConfig()
|
||||
* @return array righe nella stessa forma di imported.php $gridDataArray
|
||||
*/
|
||||
function buildGridRows(PDO $pdo, array $iddatadbList, array $config): array
|
||||
{
|
||||
$iddatadbList = array_values(array_filter(array_map('intval', $iddatadbList), fn($v) => $v > 0));
|
||||
if (empty($iddatadbList)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$fixedFields = $config['fixedFields'];
|
||||
$fixedAliasMap = $config['fixedAliasMap'];
|
||||
$mainFieldMappings = $config['mainFieldMappings'];
|
||||
$default_idclient = $config['default_idclient'];
|
||||
|
||||
$ph = implode(',', array_fill(0, count($iddatadbList), '?'));
|
||||
|
||||
// Righe datadb + user_name
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT d.*, CONCAT(u.first_name, ' ', u.last_name) AS user_name
|
||||
FROM datadb d
|
||||
LEFT JOIN auth_users u ON d.user_id = u.id
|
||||
WHERE d.iddatadb IN ($ph)
|
||||
");
|
||||
$stmt->execute($iddatadbList);
|
||||
$byId = [];
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||
$byId[(int)$r['iddatadb']] = $r;
|
||||
}
|
||||
|
||||
// Dettagli (import_data_details)
|
||||
$detStmt = $pdo->prepare("
|
||||
SELECT d.id AS datadb_id, d.mapping_id, d.field_value,
|
||||
m.field_id, m.field_label, m.data_type, m.is_required, m.manual_default
|
||||
FROM import_data_details d
|
||||
JOIN template_mapping m ON d.mapping_id = m.id
|
||||
WHERE d.id IN ($ph)
|
||||
");
|
||||
$detStmt->execute($iddatadbList);
|
||||
$detailsByRow = [];
|
||||
foreach ($detStmt->fetchAll(PDO::FETCH_ASSOC) as $d) {
|
||||
$detailsByRow[(int)$d['datadb_id']][] = $d;
|
||||
}
|
||||
|
||||
// Costruzione righe nell'ordine della lista passata
|
||||
$rows = [];
|
||||
foreach ($iddatadbList as $id) {
|
||||
$row = $byId[$id] ?? null;
|
||||
if ($row === null) continue;
|
||||
|
||||
$rowObj = [
|
||||
'iddatadb' => (int)$row['iddatadb'],
|
||||
'status' => $row['status'] ?? 'i',
|
||||
'idclient' => $row['idclient'] ?? $default_idclient,
|
||||
'cliente_fornitore_id' => $row['cliente_fornitore_id'] ?? null,
|
||||
'tested_component' => $row['tested_component'] ?? '',
|
||||
'commessaweb' => $row['commessaweb'] ?? null,
|
||||
'user_name' => $row['user_name'] ?? '',
|
||||
'importreferencecode' => $row['importreferencecode'] ?? '',
|
||||
'filename_import' => $row['filename_import'] ?? '',
|
||||
'importdate' => $row['importdate'] ?? '',
|
||||
];
|
||||
|
||||
// Fixed fields
|
||||
$rowObj['fixedFields'] = [];
|
||||
foreach ($fixedFields as $f) {
|
||||
$key = $f['fixed_field_key'];
|
||||
$dbCol = $fixedAliasMap[$key] ?? $key;
|
||||
$val = $row[$dbCol] ?? '';
|
||||
if ($val === '' || $val === null) {
|
||||
$val = gdbFixedDefaultValue($f);
|
||||
}
|
||||
$rowObj['fixedFields'][$key] = (string)$val;
|
||||
}
|
||||
|
||||
// Details
|
||||
$rowObj['details'] = [];
|
||||
$rowDetails = $detailsByRow[$id] ?? [];
|
||||
foreach ($rowDetails as $d) {
|
||||
$rowObj['details'][(string)$d['mapping_id']] = $d['field_value'] ?? '';
|
||||
}
|
||||
|
||||
// Main field values
|
||||
foreach ($mainFieldMappings as $mainMapping) {
|
||||
$found = null;
|
||||
foreach ($rowDetails as $d) {
|
||||
if ($d['mapping_id'] == $mainMapping['id']) {
|
||||
$found = $d;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$rowObj['details'][(string)$mainMapping['id']] =
|
||||
($found['field_value'] ?? null) ?? ($mainMapping['manual_default'] ?? '');
|
||||
}
|
||||
|
||||
if (!empty($mainFieldMappings)) {
|
||||
$firstMain = $mainFieldMappings[0];
|
||||
$rowObj['mainFieldValue'] = $rowObj['details'][(string)$firstMain['id']] ?? '';
|
||||
}
|
||||
|
||||
$rowObj['_dirty'] = false;
|
||||
$rows[] = $rowObj;
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ $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';
|
||||
@@ -56,4 +56,12 @@ $photousername = basename($avatar);
|
||||
//include files
|
||||
require_once(__DIR__ . '/../../languages/en/general.php');
|
||||
|
||||
//include("generalsettings.php");
|
||||
//include("generalsettings.php");
|
||||
|
||||
// Timbra la sessione dell'utente a ogni pagina (rolling manuale)
|
||||
try {
|
||||
$pdoSess = DBHandlerSelect::getInstance()->getConnection();
|
||||
$pdoSess->prepare("UPDATE auth_sessions SET last_activity = ? WHERE user_id = ? ORDER BY last_activity DESC LIMIT 1")
|
||||
->execute([time(), $iduserlogin]);
|
||||
} catch (\Throwable $e) { /* non bloccare la pagina */
|
||||
}
|
||||
|
||||
@@ -6,102 +6,176 @@
|
||||
<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>
|
||||
<li>
|
||||
<a href="bindings_manage.php">
|
||||
<i class='bx bx-radio-circle'></i><?= htmlspecialchars($bindingsmanage ?? 'Value Bindings', ENT_QUOTES, 'UTF-8'); ?>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<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-bar-chart-alt-2"></i>
|
||||
</div>
|
||||
<div class="menu-title">Stats</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="routine_admin.php">
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-bar-chart-alt-2"></i>
|
||||
</div>
|
||||
<div class="menu-title">Routines</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="warm_cache_run.php">
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-bar-chart-alt-2"></i>
|
||||
</div>
|
||||
<div class="menu-title">Warm Cache</div>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<li class="menu-label">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>
|
||||
@@ -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; ?>
|
||||
@@ -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,16 @@
|
||||
<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><a class="dropdown-item d-flex align-items-center" href="notification_settings.php"><i class="bx bx-user fs-5"></i><span>Notifications</span></a>
|
||||
</li>
|
||||
<?php if ($user->hasRole('Admin') || $user->hasRole('SuperUser')): ?>
|
||||
<li>
|
||||
<a class="dropdown-item d-flex align-items-center" href="user-admin.php">
|
||||
<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>
|
||||
|
||||
@@ -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,11 +13,25 @@ try {
|
||||
throw new Exception('Database connection failed.');
|
||||
}
|
||||
|
||||
// Recupera solo i template attivi
|
||||
$stmt = $pdo->query("SELECT id, button_label, button_size, button_bg_color, button_text_color, source_type
|
||||
FROM excel_templates
|
||||
WHERE status = 'active'
|
||||
ORDER BY button_label ASC");
|
||||
$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;
|
||||
|
||||
@@ -77,7 +77,8 @@ $stmt = $pdo->prepare("
|
||||
field_label,
|
||||
main_field,
|
||||
is_visible_import,
|
||||
is_visible_parts
|
||||
is_visible_parts,
|
||||
is_addnote
|
||||
FROM template_mapping
|
||||
WHERE template_id = ?
|
||||
ORDER BY field_order ASC, id ASC
|
||||
@@ -87,7 +88,8 @@ $mappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Recupera i fixed fields dalla tabella template_fixed_mapping
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import
|
||||
SELECT id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import,
|
||||
default_source, excel_column
|
||||
FROM template_fixed_mapping
|
||||
WHERE template_id = ?
|
||||
ORDER BY id ASC
|
||||
@@ -183,7 +185,9 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
#schemaFieldsTable th:nth-child(2),
|
||||
#schemaFieldsTable td:nth-child(2),
|
||||
#schemaFieldsTable th:nth-child(3),
|
||||
#schemaFieldsTable td:nth-child(3) {
|
||||
#schemaFieldsTable td:nth-child(3),
|
||||
#schemaFieldsTable th:nth-child(4),
|
||||
#schemaFieldsTable td:nth-child(4) {
|
||||
width: 48px;
|
||||
/* 🔥 più stretto */
|
||||
text-align: center;
|
||||
@@ -195,7 +199,8 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
/* Make checkbox itself not oversized */
|
||||
#schemaFieldsTable td:nth-child(1) input[type="checkbox"],
|
||||
#schemaFieldsTable td:nth-child(2) input[type="checkbox"],
|
||||
#schemaFieldsTable td:nth-child(3) input[type="checkbox"] {
|
||||
#schemaFieldsTable td:nth-child(3) input[type="checkbox"],
|
||||
#schemaFieldsTable td:nth-child(4) input[type="checkbox"] {
|
||||
transform: scale(0.95);
|
||||
margin: 0;
|
||||
}
|
||||
@@ -398,6 +403,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
<th style="width:45px; text-align:center;">Main</th>
|
||||
<th style="width:45px; text-align:center;">Import</th>
|
||||
<th style="width:45px; text-align:center;">Parts</th>
|
||||
<th style="width:45px; text-align:center;">Note</th>
|
||||
<th style="width:320px;">Title</th>
|
||||
<th style="width:70px; text-align:center;">Order</th>
|
||||
<th style="width:120px;">Type</th>
|
||||
@@ -428,7 +434,12 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
data-mapping-id="<?php echo (int)$mapping['id']; ?>"
|
||||
<?php echo ((int)($mapping['is_visible_parts'] ?? 0) === 1) ? 'checked' : ''; ?>>
|
||||
</td>
|
||||
|
||||
<td class="text-center">
|
||||
<input type="checkbox"
|
||||
class="addnote-checkbox"
|
||||
data-mapping-id="<?php echo (int)$mapping['id']; ?>"
|
||||
<?php echo ((int)($mapping['is_addnote'] ?? 0) === 1) ? 'checked' : ''; ?>>
|
||||
</td>
|
||||
<td class="title-col">
|
||||
<?php echo htmlspecialchars($mapping['field_label'] ?? 'N/A'); ?>
|
||||
<?php if ((int)$mapping['is_required'] === 1): ?>
|
||||
@@ -442,8 +453,6 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
|
||||
<td><?php echo htmlspecialchars($mapping['data_type'] ?? 'N/A'); ?></td>
|
||||
|
||||
<td><?php echo htmlspecialchars($mapping['data_type'] ?? 'N/A'); ?></td>
|
||||
|
||||
<td>
|
||||
<?php
|
||||
$isSceltaMultipla = ($mapping['data_type'] === 'SceltaMultipla');
|
||||
@@ -451,9 +460,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
$autoValue = $mapping['auto_value'] ?? 'none';
|
||||
$hasAuto = ($autoValue && $autoValue !== 'none');
|
||||
|
||||
if ($isSceltaMultipla) {
|
||||
$mappingValue = 'manual';
|
||||
} elseif ($hasAuto) {
|
||||
if ($hasAuto && !$isSceltaMultipla) {
|
||||
$mappingValue = 'auto';
|
||||
} elseif ($sourceType === 'XLS' && !empty($mapping['excel_column'])) {
|
||||
$mappingValue = 'xls';
|
||||
@@ -462,24 +469,26 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
} elseif ((int)$mapping['is_manual'] === 1) {
|
||||
$mappingValue = 'manual';
|
||||
} else {
|
||||
$mappingValue = '';
|
||||
// SceltaMultipla senza colonna: valore di default dal dropdown
|
||||
$mappingValue = $isSceltaMultipla ? 'manual' : '';
|
||||
}
|
||||
?>
|
||||
|
||||
<select class="form-select mapping-select"
|
||||
data-id="<?php echo (int)$mapping['id']; ?>"
|
||||
data-field-id="<?php echo (int)$mapping['field_id']; ?>"
|
||||
<?php echo $isSceltaMultipla ? 'disabled' : ''; ?>>
|
||||
data-field-id="<?php echo (int)$mapping['field_id']; ?>">
|
||||
|
||||
<?php if (!$isSceltaMultipla): ?>
|
||||
<option value="">Select Option</option>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($sourceType === 'XLS'): ?>
|
||||
<option value="xls" <?php echo ($mappingValue === 'xls') ? 'selected' : ''; ?>>Map to XLS Column</option>
|
||||
<?php elseif ($sourceType === 'API'): ?>
|
||||
<option value="json" <?php echo ($mappingValue === 'json') ? 'selected' : ''; ?>>Map to JSON Node</option>
|
||||
<?php endif; ?>
|
||||
<?php if ($sourceType === 'XLS'): ?>
|
||||
<option value="xls" <?php echo ($mappingValue === 'xls') ? 'selected' : ''; ?>>Map to XLS Column</option>
|
||||
<?php elseif ($sourceType === 'API'): ?>
|
||||
<option value="json" <?php echo ($mappingValue === 'json') ? 'selected' : ''; ?>>Map to JSON Node</option>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$isSceltaMultipla): ?>
|
||||
<option value="auto" <?php echo ($mappingValue === 'auto') ? 'selected' : ''; ?>>Auto value</option>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -535,6 +544,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
|
||||
<td>
|
||||
<?php if ($mapping['data_type'] === 'SceltaMultipla'): ?>
|
||||
<div class="scelta-default-wrap" style="display:<?php echo ($mappingValue === 'xls' || $mappingValue === 'json') ? 'none' : 'block'; ?>;">
|
||||
<select class="form-select dropdown-select manual-default"
|
||||
data-id="<?php echo (int)$mapping['id']; ?>"
|
||||
data-field-id="<?php echo (int)$mapping['field_id']; ?>"
|
||||
@@ -542,6 +552,10 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
style="display:block;">
|
||||
<option value="">Seleziona...</option>
|
||||
</select>
|
||||
</div>
|
||||
<small class="text-muted scelta-binding-hint" style="display:<?php echo ($mappingValue === 'xls' || $mappingValue === 'json') ? 'block' : 'none'; ?>;">
|
||||
Values from the file are linked to the LIMS list via Value Bindings
|
||||
</small>
|
||||
<?php else: ?>
|
||||
<input type="text"
|
||||
class="form-control manual-default"
|
||||
@@ -586,6 +600,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
<th style="width:120px; text-align:center;">Required</th>
|
||||
<th>Field</th>
|
||||
<th style="width:140px;">Type</th>
|
||||
<th style="width:230px;">Source</th>
|
||||
<th>Default Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -609,11 +624,37 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<strong><?php echo htmlspecialchars($fm['fixed_field_key']); ?></strong>
|
||||
<strong><?php
|
||||
$fixedFieldLabels = ['ClienteAnalisi' => 'Buyer'];
|
||||
$fixedKey = $fm['fixed_field_key'];
|
||||
echo htmlspecialchars($fixedFieldLabels[$fixedKey] ?? $fixedKey);
|
||||
?></strong>
|
||||
</td>
|
||||
|
||||
<td><?php echo htmlspecialchars($fm['data_type']); ?></td>
|
||||
|
||||
<td>
|
||||
<?php $fixedSource = $fm['default_source'] ?? 'manual'; ?>
|
||||
<select class="form-select fixed-source-select"
|
||||
data-fixed-id="<?php echo (int)$fm['id']; ?>">
|
||||
<option value="manual" <?php echo $fixedSource === 'manual' ? 'selected' : ''; ?>>Default value</option>
|
||||
<?php if ($sourceType === 'XLS'): ?>
|
||||
<option value="xls" <?php echo $fixedSource === 'xls' ? 'selected' : ''; ?>>Map to XLS Column</option>
|
||||
<?php endif; ?>
|
||||
<?php if ($sourceType === 'API'): ?>
|
||||
<option value="json" <?php echo $fixedSource === 'json' ? 'selected' : ''; ?>>Map to JSON Node</option>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
|
||||
<?php if ($sourceType === 'XLS'): ?>
|
||||
<select class="form-select xls-columns fixed-xls-column mt-1"
|
||||
style="display:<?php echo $fixedSource === 'xls' ? 'block' : 'none'; ?>"
|
||||
data-fixed-id="<?php echo (int)$fm['id']; ?>"
|
||||
<?php echo !empty($fm['excel_column']) ? 'data-current-xls="' . htmlspecialchars($fm['excel_column'], ENT_QUOTES, 'UTF-8') . '"' : ''; ?>>
|
||||
</select>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
|
||||
<?php if ($fm['data_type'] === 'DATE'): ?>
|
||||
@@ -627,6 +668,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
'ClienteResponsabile',
|
||||
'ClienteFornitore',
|
||||
'ClienteAnalisi',
|
||||
'ClienteFatturazione',
|
||||
'MoltiplicatorePrezzo',
|
||||
'AnagraficaCertestObject',
|
||||
'AnagraficaCertestService'
|
||||
@@ -1266,8 +1308,6 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function saveJsonNodes(sampleJson, nodes) {
|
||||
return fetch('update_api_json_nodes.php', {
|
||||
method: 'POST',
|
||||
@@ -1350,6 +1390,41 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
});
|
||||
|
||||
// fallback (nel caso cambi senza select2)
|
||||
$(document).on('change', '.fixed-source-select', function() {
|
||||
const id = this.dataset.fixedId;
|
||||
const value = this.value;
|
||||
const $colSelect = $(this).closest('td').find('.fixed-xls-column');
|
||||
|
||||
$colSelect.toggle(value === 'xls');
|
||||
if (value !== 'xls') {
|
||||
$colSelect.val('');
|
||||
}
|
||||
|
||||
saveFixedField(id, 'default_source', value);
|
||||
});
|
||||
|
||||
$(document).on('change', '.fixed-xls-column', function() {
|
||||
saveFixedField(this.dataset.fixedId, 'excel_column', this.value || null);
|
||||
});
|
||||
|
||||
function saveFixedField(id, field, value) {
|
||||
const status = document.getElementById('fixedSaveStatus');
|
||||
if (status) status.textContent = 'Salvataggio...';
|
||||
|
||||
fetch('update_fixed_field.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: parseInt(id, 10), field: field, value: value })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (status) status.textContent = d.success ? 'Salvato' : ('Errore: ' + (d.message || ''));
|
||||
})
|
||||
.catch(() => {
|
||||
if (status) status.textContent = 'Errore di rete';
|
||||
});
|
||||
}
|
||||
|
||||
$(document).on('change', '.fixed-default-select', function() {
|
||||
saveFixedDefaultSelect(this);
|
||||
});
|
||||
@@ -1681,6 +1756,17 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
// SceltaMultipla: il default è un dropdown (Select2) nel wrapper; con colonna/nodo
|
||||
// mappato si nasconde (il valore arriva dal file e passa dal binding).
|
||||
const sceltaWrap = tr.querySelector('.scelta-default-wrap');
|
||||
const sceltaHint = tr.querySelector('.scelta-binding-hint');
|
||||
if (sceltaWrap) {
|
||||
const fromFile = mappingSelect.value === 'xls' || mappingSelect.value === 'json';
|
||||
sceltaWrap.style.display = fromFile ? 'none' : 'block';
|
||||
if (sceltaHint) sceltaHint.style.display = fromFile ? 'block' : 'none';
|
||||
}
|
||||
const isScelta = !!sceltaWrap;
|
||||
|
||||
if (mappingSelect.value === 'xls') {
|
||||
if (xlsSelect) xlsSelect.style.display = 'block';
|
||||
|
||||
@@ -1692,7 +1778,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
|
||||
if (autoSelect) autoSelect.style.display = 'none';
|
||||
|
||||
if (manualInput) {
|
||||
if (manualInput && !isScelta) {
|
||||
manualInput.style.display = 'none';
|
||||
manualInput.value = '';
|
||||
}
|
||||
@@ -1715,7 +1801,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
|
||||
if (autoSelect) autoSelect.style.display = 'none';
|
||||
|
||||
if (manualInput) {
|
||||
if (manualInput && !isScelta) {
|
||||
manualInput.style.display = 'none';
|
||||
manualInput.value = '';
|
||||
}
|
||||
@@ -1761,7 +1847,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
jsonSelect.value = '';
|
||||
}
|
||||
|
||||
if (manualInput) {
|
||||
if (manualInput && !isScelta) {
|
||||
manualInput.style.display = 'none';
|
||||
manualInput.value = '';
|
||||
}
|
||||
@@ -1785,7 +1871,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
|
||||
if (autoSelect) autoSelect.style.display = 'none';
|
||||
|
||||
if (manualInput) {
|
||||
if (manualInput && !isScelta) {
|
||||
manualInput.style.display = 'none';
|
||||
manualInput.value = '';
|
||||
}
|
||||
@@ -1869,12 +1955,78 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Save original Main checkbox state
|
||||
document.querySelectorAll('#schemaFieldsBody .main-field-checkbox').forEach(cb => {
|
||||
cb.dataset.originalChecked = cb.checked ? 'true' : 'false';
|
||||
});
|
||||
// =======================
|
||||
// SAVE: Main / Import / Parts checkboxes
|
||||
// =======================
|
||||
function saveMappingFlag(checkboxEl, field) {
|
||||
const mappingId = checkboxEl.getAttribute('data-mapping-id');
|
||||
const checked = checkboxEl.checked;
|
||||
|
||||
fetch('update_mapping_flag.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: mappingId,
|
||||
field: field,
|
||||
value: checked ? 1 : 0
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (!data.success) {
|
||||
console.error("❌ Error saving flag:", field, data.message);
|
||||
|
||||
if (data.limit_reached) {
|
||||
alert(data.message);
|
||||
}
|
||||
|
||||
// Revert: il server ha rifiutato, riporto la checkbox allo stato precedente
|
||||
checkboxEl.checked = !checked;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("✅ Flag saved:", field, "=", checked ? 1 : 0, "for id", mappingId);
|
||||
|
||||
// Se il server ha deselezionato un'altra riga (caso "parts"), rifletto anche in UI
|
||||
if (data.unchecked_others && field === 'is_visible_parts') {
|
||||
document.querySelectorAll('#schemaFieldsBody .visible-parts-checkbox').forEach(cb => {
|
||||
if (cb !== checkboxEl) {
|
||||
cb.checked = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("❌ Fetch error saving flag:", field, error);
|
||||
checkboxEl.checked = !checked;
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('schemaFieldsBody').addEventListener('change', function(event) {
|
||||
const el = event.target;
|
||||
|
||||
if (el.classList.contains('main-field-checkbox')) {
|
||||
saveMappingFlag(el, 'main_field');
|
||||
}
|
||||
|
||||
if (el.classList.contains('visible-import-checkbox')) {
|
||||
saveMappingFlag(el, 'is_visible_import');
|
||||
}
|
||||
|
||||
if (el.classList.contains('visible-parts-checkbox')) {
|
||||
saveMappingFlag(el, 'is_visible_parts');
|
||||
}
|
||||
|
||||
if (el.classList.contains('addnote-checkbox')) {
|
||||
saveMappingFlag(el, 'is_addnote');
|
||||
}
|
||||
});
|
||||
// AUTO VALUE select change -> save auto_value
|
||||
document.getElementById('schemaFieldsBody').addEventListener('change', function(event) {
|
||||
if (!event.target.classList.contains('auto-value-select')) return;
|
||||
@@ -1928,7 +2080,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
mappingSelect.value = '';
|
||||
if (mappedColumn) mappedColumn.style.display = 'none';
|
||||
e.target.style.display = 'none';
|
||||
saveMapping(mappingId, '', '', null, null, null);
|
||||
if (!resetSceltaToManual(tr, mappingId)) saveMapping(mappingId, '', '', null, null, null);
|
||||
updateXlsDropdowns();
|
||||
});
|
||||
}
|
||||
@@ -1951,7 +2103,6 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
document.getElementById('schemaFieldsBody').addEventListener('change', function(event) {
|
||||
if (event.target.classList.contains('manual-default') && event.target.tagName === 'SELECT') {
|
||||
let tr = event.target.closest('tr');
|
||||
@@ -2009,7 +2160,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
console.log("Removing XLS mapping:", {
|
||||
id: mappingId
|
||||
});
|
||||
saveMapping(mappingId, '', '', null, null, null);
|
||||
if (!resetSceltaToManual(tr, mappingId)) saveMapping(mappingId, '', '', null, null, null);
|
||||
updateXlsDropdowns();
|
||||
}
|
||||
});
|
||||
@@ -2039,11 +2190,26 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
|
||||
event.target.style.display = 'none';
|
||||
|
||||
saveMapping(mappingId, '', '', null, null, null);
|
||||
if (!resetSceltaToManual(tr, mappingId)) saveMapping(mappingId, '', '', null, null, null);
|
||||
updateJsonDropdowns();
|
||||
}
|
||||
});
|
||||
|
||||
// Rimozione colonna/nodo: un campo SceltaMultipla torna a "Manual Entry" col suo dropdown default
|
||||
// (non ha l'opzione vuota). Ritorna true se gestito.
|
||||
function resetSceltaToManual(tr, mappingId) {
|
||||
const wrap = tr.querySelector('.scelta-default-wrap');
|
||||
if (!wrap) return false;
|
||||
const sel = tr.querySelector('.mapping-select');
|
||||
if (sel) sel.value = 'manual';
|
||||
wrap.style.display = 'block';
|
||||
const hint = tr.querySelector('.scelta-binding-hint');
|
||||
if (hint) hint.style.display = 'none';
|
||||
const dd = tr.querySelector('select.manual-default.dropdown-select');
|
||||
saveMapping(mappingId, 'manual', dd ? dd.value : '', null, null, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
function saveMapping(mappingId, mappingType, defaultValue, excelColumn, autoValue, jsonNode) {
|
||||
console.log("Saving mapping:", {
|
||||
id: mappingId,
|
||||
@@ -2116,15 +2282,19 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
const tbody = document.querySelector('#fixedFieldsTable tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
const fixedFieldLabels = {
|
||||
'ClienteAnalisi': 'Buyer'
|
||||
};
|
||||
|
||||
const keysWithDropdown = [
|
||||
'ClienteResponsabile',
|
||||
'ClienteFornitore',
|
||||
'ClienteAnalisi',
|
||||
'ClienteFatturazione', // ← AGGIUNGI QUESTA RIGA
|
||||
'MoltiplicatorePrezzo',
|
||||
'AnagraficaCertestObject',
|
||||
'AnagraficaCertestService'
|
||||
];
|
||||
|
||||
tbody.innerHTML = rows.map(r => {
|
||||
const type = String(r.data_type || '').toUpperCase();
|
||||
const dv = r.default_value ?? '';
|
||||
@@ -2147,8 +2317,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
${parseInt(r.is_required) === 1 ? 'checked' : ''}>
|
||||
</td>
|
||||
|
||||
<td><strong>${escapeHtml(r.fixed_field_key)}</strong></td>
|
||||
|
||||
<td><strong>${escapeHtml(fixedFieldLabels[r.fixed_field_key] || r.fixed_field_key)}</strong></td>
|
||||
|
||||
<td>${escapeHtml(r.data_type || '')}</td>
|
||||
|
||||
@@ -2428,11 +2597,22 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
})).filter(x => x.id != null && x.label);
|
||||
}
|
||||
|
||||
if (key === 'ClienteFornitore' || key === 'ClienteAnalisi') {
|
||||
if (key === 'ClienteFornitore' || key === 'ClienteAnalisi' || key === 'ClienteFatturazione') {
|
||||
const j = await fetchJson('get_clienti.php');
|
||||
const list = normalizeList(j, key);
|
||||
|
||||
return list.map(x => ({
|
||||
const isInattivo = (x) => {
|
||||
const v = x.Inattivo;
|
||||
return v === true || v === 1 || v === '1' || v === 'true';
|
||||
};
|
||||
|
||||
// Fornitore e Fatturazione: solo clienti ATTIVI (Inattivo = false)
|
||||
// Analisi: solo clienti INATTIVI (Inattivo = true)
|
||||
const filtered = (key === 'ClienteAnalisi') ?
|
||||
list.filter(isInattivo) :
|
||||
list.filter(x => !isInattivo(x));
|
||||
|
||||
return filtered.map(x => ({
|
||||
id: x.IdCliente ?? x.Id ?? x.id,
|
||||
label: formatClientLabel(x)
|
||||
})).filter(x => x.id != null && x.label);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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()]);
|
||||
}
|
||||
@@ -136,14 +136,9 @@ $matrixGroups = array_values($matrixGroups);
|
||||
<button type="button"
|
||||
class="list-group-item list-group-item-action analysis-matrix-item <?= $index === 0 ? 'active' : '' ?>"
|
||||
data-matrix-id="<?= htmlspecialchars((string)($group['idmatrice'] ?? 'NO_MATRIX'), ENT_QUOTES, 'UTF-8') ?>">
|
||||
<div class="fw-semibold">
|
||||
<?= htmlspecialchars(
|
||||
!empty($group['NomeMatriceTraduzione'])
|
||||
? $group['NomeMatriceTraduzione']
|
||||
: (!empty($group['idmatrice']) ? ('Matrix without translation') : 'No Matrix'),
|
||||
ENT_QUOTES,
|
||||
'UTF-8'
|
||||
) ?>
|
||||
<div class="fw-semibold analysis-matrix-name"
|
||||
data-matrix-id="<?= htmlspecialchars((string)($group['idmatrice'] ?? 'NO_MATRIX'), ENT_QUOTES, 'UTF-8') ?>">
|
||||
<?= !empty($group['idmatrice']) ? ('#' . (int)$group['idmatrice']) : 'No Matrix' ?>
|
||||
</div>
|
||||
<div class="small text-muted">
|
||||
ID: <?= !empty($group['idmatrice']) ? (int)$group['idmatrice'] : '-' ?>
|
||||
@@ -193,14 +188,9 @@ $matrixGroups = array_values($matrixGroups);
|
||||
|
||||
<div class="small text-muted mt-1">
|
||||
Matrix:
|
||||
<strong>
|
||||
<?= htmlspecialchars(
|
||||
!empty($part['NomeMatriceTraduzione'])
|
||||
? $part['NomeMatriceTraduzione']
|
||||
: (!empty($part['idmatrice']) ? 'Matrix without translation' : 'No Matrix'),
|
||||
ENT_QUOTES,
|
||||
'UTF-8'
|
||||
) ?>
|
||||
<strong class="analysis-part-matrix-name"
|
||||
data-matrix-id="<?= htmlspecialchars($matrixId, ENT_QUOTES, 'UTF-8') ?>">
|
||||
<?= !empty($part['idmatrice']) ? ('#' . (int)$part['idmatrice']) : 'No Matrix' ?>
|
||||
</strong>
|
||||
<?php if (!empty($part['idmatrice'])): ?>
|
||||
<span class="small text-muted">(ID: <?= (int)$part['idmatrice'] ?>)</span>
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
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>
|
||||
@@ -280,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>
|
||||
@@ -29,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"
|
||||
@@ -45,6 +48,9 @@
|
||||
<button type="button" class="btn btn-primary btn-sm" id="clonePartsBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
|
||||
<i class="fas fa-clone"></i> Clona Parti
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning btn-sm d-none" id="quotationeBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
|
||||
<i class="fas fa-file-invoice-dollar"></i> Collega Quotazione
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm ms-2" id="toggleVoiceBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
|
||||
<i class="fas fa-microphone"></i> Voce
|
||||
</button>
|
||||
@@ -188,6 +194,43 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modale di conferma per la clonazione delle parti -->
|
||||
<div class="modal fade" id="cloneConfirmModal" tabindex="-1" aria-labelledby="cloneConfirmModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="cloneConfirmModalLabel">Conferma Clonazione</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="clone-confirm-message mb-3"></p>
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="cloneNotesCheckbox" checked>
|
||||
<label class="form-check-label" for="cloneNotesCheckbox">
|
||||
Duplica note
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="cloneAnalysesCheckbox">
|
||||
<label class="form-check-label" for="cloneAnalysesCheckbox">
|
||||
Duplica anche le analisi collegate
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mt-2">
|
||||
<input class="form-check-input" type="checkbox" id="cloneOverwriteCheckbox" checked>
|
||||
<label class="form-check-label" for="cloneOverwriteCheckbox">
|
||||
<strong>Sovrascrivi parti esistenti</strong> (elimina le parti già presenti nei record di destinazione prima di inserire)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Annulla</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" id="cloneConfirmBtn">Conferma</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Finestra modale "Aggiungi preventivo" -->
|
||||
<div class="modal fade" id="addQuotationModal" tabindex="-1" aria-labelledby="addQuotationModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-md">
|
||||
@@ -208,6 +251,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
|
||||
@@ -738,5 +798,13 @@
|
||||
padding: 0.1rem 0.25rem !important;
|
||||
}
|
||||
|
||||
#cloneConfirmModal {
|
||||
z-index: 1070 !important
|
||||
}
|
||||
|
||||
#cloneConfirmModal .modal-backdrop {
|
||||
z-index: 1065 !important
|
||||
}
|
||||
|
||||
/* rosso */
|
||||
</style>
|
||||
@@ -171,6 +171,11 @@
|
||||
);
|
||||
if (idx >= 0) window.gridData.splice(idx, 1);
|
||||
|
||||
// Aggiorna la lista dei record visibili (navigazione parti + clone)
|
||||
window.visibleIddatadbList = window.gridData
|
||||
.map((r) => parseInt(r.iddatadb, 10))
|
||||
.filter(Boolean);
|
||||
|
||||
// Re-render
|
||||
const gr = window.gridRenderer;
|
||||
if (gr) gr.renderVisibleRows();
|
||||
@@ -233,6 +238,11 @@
|
||||
// Add to beginning of gridData
|
||||
window.gridData.unshift(newRow);
|
||||
|
||||
// Aggiorna la lista dei record visibili (navigazione parti + clone)
|
||||
window.visibleIddatadbList = window.gridData
|
||||
.map((r) => parseInt(r.iddatadb, 10))
|
||||
.filter(Boolean);
|
||||
|
||||
// Re-render
|
||||
const gr = window.gridRenderer;
|
||||
if (gr) gr.renderVisibleRows();
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$userId = (int)($iduserlogin ?? 0);
|
||||
|
||||
// Carica le preferenze correnti dell'utente
|
||||
$prefs = ['notify_export_email' => 1];
|
||||
if ($userId > 0) {
|
||||
$s = $pdo->prepare("SELECT notify_export_email FROM auth_users WHERE id = ? LIMIT 1");
|
||||
$s->execute([$userId]);
|
||||
$row = $s->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row) {
|
||||
$prefs['notify_export_email'] = (int)($row['notify_export_email'] ?? 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Flash message post-save (via querystring)
|
||||
$saved = isset($_GET['saved']) && $_GET['saved'] === '1';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
<?php include('cssinclude.php'); ?>
|
||||
<title>SmartTRF - Notifiche</title>
|
||||
<style>
|
||||
.notif-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #eef0f2;
|
||||
}
|
||||
|
||||
.notif-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.notif-text {
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.notif-text h6 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.notif-text p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.form-switch .form-check-input {
|
||||
width: 2.6em;
|
||||
height: 1.4em;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h6 class="mb-0">Preferenze notifiche</h6>
|
||||
<small class="text-muted">Scegli quali email vuoi ricevere da SmartTRF.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<?php if ($saved): ?>
|
||||
<div class="alert alert-success py-2" role="alert">
|
||||
<i class="fas fa-check-circle me-1"></i> Preferenze salvate.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form action="update_notification_prefs.php" method="POST">
|
||||
<input type="hidden" name="iduserlogin" value="<?= htmlspecialchars((string)$userId, ENT_QUOTES, 'UTF-8') ?>">
|
||||
|
||||
<!-- Notifica: commesse esportate -->
|
||||
<div class="notif-row">
|
||||
<div class="notif-text">
|
||||
<h6>Ricezione email commesse esportate</h6>
|
||||
<p>Ricevi un riepilogo via email al termine di ogni esportazione verso il LIMS (singola o in blocco), con l'esito di ciascuna commessa.</p>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
id="notify_export_email" name="notify_export_email" value="1"
|
||||
<?= $prefs['notify_export_email'] === 1 ? 'checked' : '' ?>>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Qui in futuro altre notifiche, stessa struttura .notif-row -->
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-1"></i> Salva preferenze
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay toggle-icon"></div>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
<?php include('jsinclude.php'); ?>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+478
-521
File diff suppressed because it is too large
Load Diff
@@ -39,6 +39,7 @@ try {
|
||||
$button_bg_color = trim($_POST['button_bg_color'] ?? '#007bff');
|
||||
$button_text_color = trim($_POST['button_text_color'] ?? '#ffffff');
|
||||
$button_label = trim($_POST['button_label'] ?? 'Click Me');
|
||||
$template_note = trim($_POST['template_note'] ?? '');
|
||||
|
||||
// Allowed source types
|
||||
if (!in_array($source_type, ['XLS', 'API', 'JSON', 'PDF'], true)) {
|
||||
@@ -122,6 +123,7 @@ try {
|
||||
button_bg_color = ?,
|
||||
button_text_color = ?,
|
||||
button_label = ?,
|
||||
template_note = ?,
|
||||
updated_at = NOW()
|
||||
WHERE id = ?
|
||||
");
|
||||
@@ -144,6 +146,7 @@ try {
|
||||
$button_bg_color,
|
||||
$button_text_color,
|
||||
$button_label,
|
||||
$template_note,
|
||||
$id
|
||||
]);
|
||||
|
||||
|
||||
@@ -53,7 +53,143 @@ function normalizeColumnIndex($value): int
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim a cell value treating invisible/Unicode spaces as empty.
|
||||
* PHP's native trim() strips only ASCII whitespace (" \t\n\r\0\x0B"), so a cell
|
||||
* that contains only a non-breaking space (U+00A0), zero-width space (U+200B),
|
||||
* BOM (U+FEFF) or another Unicode space looks blank to a human but would still
|
||||
* count as "filled" — pulling a ghost column into the import or retaining a
|
||||
* visually-empty row. Normalize those to a real space before trimming.
|
||||
*/
|
||||
function cleanCellText($value): string
|
||||
{
|
||||
$raw = (string)$value;
|
||||
$cleaned = preg_replace(
|
||||
'/[\x{00A0}\x{200B}\x{FEFF}\x{2000}-\x{200A}\x{202F}\x{205F}\x{3000}]+/u',
|
||||
' ',
|
||||
$raw
|
||||
);
|
||||
|
||||
// preg_replace returns null on malformed UTF-8; fall back to the raw value.
|
||||
return trim($cleaned ?? $raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-clean an .xlsx by streaming out "ghost" cells: empty, self-closing
|
||||
* <c .../> (and <c ...></c>) elements that carry only leftover styling.
|
||||
*/
|
||||
function slimXlsxGhostCells(string $path): ?string
|
||||
{
|
||||
if (!class_exists('ZipArchive')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$slim = $path . '.slim.xlsx';
|
||||
|
||||
try {
|
||||
if (!copy($path, $slim)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Phase 1: stream-strip each worksheet to a temp file (low memory).
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($slim) !== true) {
|
||||
@unlink($slim);
|
||||
return null;
|
||||
}
|
||||
|
||||
$temps = [];
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$name = $zip->getNameIndex($i);
|
||||
if (!preg_match('#^xl/worksheets/sheet\d+\.xml$#', $name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$in = $zip->getStream($name);
|
||||
if (!$in) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tmp = tempnam(sys_get_temp_dir(), 'slim');
|
||||
$out = fopen($tmp, 'w');
|
||||
$carry = '';
|
||||
|
||||
while (!feof($in)) {
|
||||
$chunk = fread($in, 4194304);
|
||||
if ($chunk === '' || $chunk === false) {
|
||||
break;
|
||||
}
|
||||
// Only process up to the last complete '>' so a cell tag is
|
||||
// never split across a chunk boundary; carry the remainder.
|
||||
$buf = $carry . $chunk;
|
||||
$lastGt = strrpos($buf, '>');
|
||||
if ($lastGt === false) {
|
||||
$carry = $buf;
|
||||
continue;
|
||||
}
|
||||
$proc = substr($buf, 0, $lastGt + 1);
|
||||
$carry = substr($buf, $lastGt + 1);
|
||||
$proc = preg_replace(['#<c [^>]*/>#', '#<c [^>]*></c>#'], '', $proc);
|
||||
fwrite($out, $proc);
|
||||
}
|
||||
if ($carry !== '') {
|
||||
fwrite($out, $carry);
|
||||
}
|
||||
fclose($in);
|
||||
fclose($out);
|
||||
$temps[$name] = $tmp;
|
||||
}
|
||||
$zip->close();
|
||||
|
||||
if (!$temps) {
|
||||
@unlink($slim);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Phase 2: swap the stripped worksheets back into the archive.
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($slim) !== true) {
|
||||
foreach ($temps as $t) {
|
||||
@unlink($t);
|
||||
}
|
||||
@unlink($slim);
|
||||
return null;
|
||||
}
|
||||
foreach ($temps as $name => $tmp) {
|
||||
$zip->deleteName($name);
|
||||
$zip->addFile($tmp, $name);
|
||||
}
|
||||
$zip->close(); // addFile streams from disk here, so unlink only after.
|
||||
|
||||
foreach ($temps as $t) {
|
||||
@unlink($t);
|
||||
}
|
||||
|
||||
return $slim;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('slimXlsxGhostCells failed: ' . $e->getMessage());
|
||||
@unlink($slim);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Quando il body POST supera post_max_size, PHP scarta $_POST e $_FILES
|
||||
// (warning "Content-Length exceeds the limit ... in Unknown on line 0") e lo
|
||||
// script riceve una richiesta vuota. Lo intercettiamo per dare un messaggio
|
||||
// chiaro invece di "Richiesta non valida".
|
||||
if (
|
||||
$_SERVER['REQUEST_METHOD'] === 'POST'
|
||||
&& empty($_POST) && empty($_FILES)
|
||||
&& (int)($_SERVER['CONTENT_LENGTH'] ?? 0) > 0
|
||||
) {
|
||||
$postMax = ini_get('post_max_size');
|
||||
throw new Exception(
|
||||
"Il file caricato supera il limite di upload del server (post_max_size = {$postMax}). " .
|
||||
"Chiedi all'amministratore di aumentare post_max_size e upload_max_filesize."
|
||||
);
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['excel_file'])) {
|
||||
$template_id = isset($_POST['template_id']) ? intval($_POST['template_id']) : 0;
|
||||
|
||||
@@ -70,7 +206,7 @@ try {
|
||||
* Così non dipendiamo solo dal form e siamo sicuri di usare i dati salvati.
|
||||
*/
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
SELECT
|
||||
id,
|
||||
header_row,
|
||||
start_column,
|
||||
@@ -142,14 +278,14 @@ try {
|
||||
|
||||
// Recupera il mapping da template_mapping
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
field_id AS excel_column,
|
||||
field_id AS mysql_column,
|
||||
data_type,
|
||||
is_required,
|
||||
default_value,
|
||||
is_manual
|
||||
FROM template_mapping
|
||||
SELECT
|
||||
field_id AS excel_column,
|
||||
field_id AS mysql_column,
|
||||
data_type,
|
||||
is_required,
|
||||
default_value,
|
||||
is_manual
|
||||
FROM template_mapping
|
||||
WHERE template_id = ?
|
||||
");
|
||||
$stmt->execute([$template_id]);
|
||||
@@ -161,8 +297,28 @@ try {
|
||||
if (empty($mappings)) {
|
||||
$response['error'] = "Nessun mapping trovato per il template con ID $template_id";
|
||||
} else {
|
||||
// Carica il file rinominato con PHPSpreadsheet
|
||||
$spreadsheet = IOFactory::load($destination);
|
||||
// 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();
|
||||
@@ -189,8 +345,8 @@ try {
|
||||
|
||||
error_log("Selected XLS sheet - index: {$xlsSheetIndex}, name: {$selectedSheetName}");
|
||||
|
||||
$highestRow = $worksheet->getHighestRow();
|
||||
$highestColumn = $worksheet->getHighestColumn();
|
||||
$highestRow = $worksheet->getHighestDataRow();
|
||||
$highestColumn = $worksheet->getHighestDataColumn();
|
||||
$highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
|
||||
|
||||
$startRow = max(1, $header_row);
|
||||
@@ -199,7 +355,7 @@ try {
|
||||
// Advance startColumn to first non-empty cell in header row, matching JS behavior
|
||||
for ($sc = $startColumn; $sc <= $highestColumnIndex; $sc++) {
|
||||
$cl = Coordinate::stringFromColumnIndex($sc);
|
||||
$cv = trim((string)($worksheet->getCell($cl . $header_row)->getCalculatedValue() ?? ''));
|
||||
$cv = cleanCellText($worksheet->getCell($cl . $header_row)->getCalculatedValue() ?? '');
|
||||
|
||||
if ($cv !== '') {
|
||||
$startColumn = $sc;
|
||||
@@ -207,6 +363,19 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
$lastHeaderCol = $startColumn;
|
||||
for ($hc = $startColumn; $hc <= $highestColumnIndex; $hc++) {
|
||||
$hl = Coordinate::stringFromColumnIndex($hc);
|
||||
$hv = cleanCellText($worksheet->getCell($hl . $header_row)->getCalculatedValue() ?? '');
|
||||
|
||||
if ($hv !== '') {
|
||||
$lastHeaderCol = $hc;
|
||||
}
|
||||
}
|
||||
|
||||
$highestColumnIndex = $lastHeaderCol;
|
||||
$highestColumn = Coordinate::stringFromColumnIndex($highestColumnIndex);
|
||||
|
||||
// Debug dei parametri
|
||||
error_log(
|
||||
"Processing - template_id: $template_id, " .
|
||||
@@ -269,7 +438,7 @@ try {
|
||||
|
||||
$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
|
||||
@@ -306,7 +475,7 @@ try {
|
||||
$filledCount = 0;
|
||||
|
||||
foreach ($headerFilledIndices as $idx) {
|
||||
if (isset($rowData[$idx]) && trim((string)$rowData[$idx]) !== '') {
|
||||
if (isset($rowData[$idx]) && cleanCellText($rowData[$idx]) !== '') {
|
||||
$filledCount++;
|
||||
}
|
||||
}
|
||||
@@ -322,13 +491,13 @@ try {
|
||||
// Recupera routine dal template
|
||||
if ($template && !empty($template['idroutine'])) {
|
||||
$stmtRoutine = $pdo->prepare("
|
||||
SELECT
|
||||
idroutine,
|
||||
name,
|
||||
filename,
|
||||
headerrow,
|
||||
instruction
|
||||
FROM routine
|
||||
SELECT
|
||||
idroutine,
|
||||
name,
|
||||
filename,
|
||||
headerrow,
|
||||
instruction
|
||||
FROM routine
|
||||
WHERE idroutine = ?
|
||||
");
|
||||
$stmtRoutine->execute([$template['idroutine']]);
|
||||
@@ -337,6 +506,7 @@ try {
|
||||
if ($routineData) {
|
||||
$response['apply_routine'] = true;
|
||||
$response['routine_data'] = [
|
||||
'idroutine' => (int)$routineData['idroutine'],
|
||||
'name' => $routineData['name'] ?? 'Routine Sconosciuta',
|
||||
'instruction' => $routineData['instruction'] ?? 'Nessuna descrizione disponibile',
|
||||
'filename' => $routineData['filename'] ?? '',
|
||||
|
||||
@@ -0,0 +1,417 @@
|
||||
<?php
|
||||
|
||||
include('include/headscript.php');
|
||||
require_once __DIR__ . '/class/binding-functions.php';
|
||||
|
||||
$pending = $_SESSION['pending_bindings'] ?? null;
|
||||
|
||||
if (isset($_GET['done'])) {
|
||||
$tid = $pending['template_id'] ?? ($_SESSION['template_id'] ?? null);
|
||||
$ref = $pending['importref'] ?? '';
|
||||
unset($_SESSION['pending_bindings']);
|
||||
|
||||
header($tid
|
||||
? "Location: imported.php?id=" . urlencode($tid) . "&importref=" . urlencode($ref)
|
||||
: "Location: xlstemplates_grid.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (empty($pending) || empty($pending['items'])) {
|
||||
$tid = $pending['template_id'] ?? ($_SESSION['template_id'] ?? null);
|
||||
$ref = $pending['importref'] ?? '';
|
||||
unset($_SESSION['pending_bindings']);
|
||||
|
||||
if ($tid) {
|
||||
header("Location: imported.php?id=" . urlencode($tid) . "&importref=" . urlencode($ref));
|
||||
} else {
|
||||
header("Location: xlstemplates_grid.php");
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
$templateId = (int) $pending['template_id'];
|
||||
$importRef = (string) $pending['importref'];
|
||||
$items = array_values($pending['items'] ?? []);
|
||||
$autoItems = array_values($pending['auto'] ?? []);
|
||||
$savedItems = array_values($pending['saved'] ?? []);
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT name FROM excel_templates WHERE id = ?");
|
||||
$stmt->execute([$templateId]);
|
||||
$templateName = $stmt->fetchColumn() ?: ('Template ' . $templateId);
|
||||
|
||||
foreach ($items as $i => $item) {
|
||||
$items[$i]['suggestion'] = ($item['kind'] ?? 'custom') === 'fixed'
|
||||
? binding_suggest_fixed(
|
||||
$pdo,
|
||||
(string) ($item['fixed_field_key'] ?? ''),
|
||||
(string) $item['source_value'],
|
||||
(int) $item['mapping_id']
|
||||
)
|
||||
: binding_suggest(
|
||||
$pdo,
|
||||
(int) $item['field_id'],
|
||||
(string) $item['source_value'],
|
||||
(int) $item['mapping_id']
|
||||
);
|
||||
}
|
||||
|
||||
$resolvedItems = [];
|
||||
foreach ($autoItems as $a) {
|
||||
$a['badge'] = 'automatico';
|
||||
$a['badge_class'] = 'bg-success';
|
||||
$resolvedItems[] = $a;
|
||||
}
|
||||
foreach ($savedItems as $s) {
|
||||
$s['badge'] = 'binding salvato';
|
||||
$s['badge_class'] = 'bg-secondary';
|
||||
$resolvedItems[] = $s;
|
||||
}
|
||||
|
||||
$groups = [];
|
||||
$groupOrder = [];
|
||||
$pushToGroup = function (array $row, string $type, $idx = null) use (&$groups, &$groupOrder) {
|
||||
$kind = $row['kind'] ?? 'custom';
|
||||
$gkey = $kind . ':' . (int) ($row['mapping_id'] ?? 0);
|
||||
if (!isset($groups[$gkey])) {
|
||||
$groups[$gkey] = [
|
||||
'label' => $row['field_label'] ?? ('Mapping ' . $gkey),
|
||||
'kind' => $kind,
|
||||
'pending' => [],
|
||||
'resolved' => [],
|
||||
];
|
||||
$groupOrder[] = $gkey;
|
||||
}
|
||||
if ($type === 'pending') {
|
||||
$groups[$gkey]['pending'][] = ['idx' => $idx, 'data' => $row];
|
||||
} else {
|
||||
$groups[$gkey]['resolved'][] = ['data' => $row];
|
||||
}
|
||||
};
|
||||
foreach ($items as $idx => $item) {
|
||||
$pushToGroup($item, 'pending', $idx);
|
||||
}
|
||||
foreach ($resolvedItems as $res) {
|
||||
$pushToGroup($res, 'resolved');
|
||||
}
|
||||
|
||||
$pendingRecords = 0;
|
||||
foreach ($items as $item) {
|
||||
$pendingRecords += count($item['datadb_ids'] ?? []);
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
<?php include('cssinclude.php'); ?>
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.source-value {
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.binding-status {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
td .select2-container {
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
/* Allinea l'altezza di select2 a form-select/btn di Bootstrap */
|
||||
.select2-container--default .select2-selection--single {
|
||||
height: 38px;
|
||||
border-color: #ced4da;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||
height: 36px;
|
||||
}
|
||||
</style>
|
||||
<title>Binding valori → LIMS - <?= htmlspecialchars($titlewebsite ?? '', ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<div>
|
||||
<h6 class="mb-0">Binding valori importati → LIMS</h6>
|
||||
<small><?= htmlspecialchars($templateName) ?> · Template ID: <?= $templateId ?></small>
|
||||
</div>
|
||||
<a href="bindings_manage.php?template_id=<?= $templateId ?>" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-cog"></i> Gestione binding
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
Alcuni valori importati dal file non corrispondono a nessun valore delle liste LIMS.
|
||||
Finche' non vengono collegati, questi campi non possono essere esportati.
|
||||
Scegli il valore LIMS corretto per ciascuno: il binding viene salvato e
|
||||
riutilizzato nelle importazioni successive.
|
||||
<div class="mt-1">
|
||||
<strong><?= count($items) ?></strong> valore/i da risolvere su
|
||||
<strong><?= $pendingRecords ?></strong> record importati.
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!empty($autoItems)): ?>
|
||||
<div class="alert alert-success">
|
||||
<?= count($autoItems) ?> valore/i collegato/i automaticamente (corrispondenza esatta con il LIMS).
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div id="bindingError" class="alert alert-danger" style="display:none;"></div>
|
||||
|
||||
<form id="bindingForm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:32px;"></th>
|
||||
<th>Valore importato</th>
|
||||
<th style="width:90px;">Record</th>
|
||||
<th>Valore LIMS</th>
|
||||
<th style="width:210px;">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($groupOrder as $gkey): $g = $groups[$gkey];
|
||||
$pendCount = count($g['pending']);
|
||||
$totalCount = $pendCount + count($g['resolved']); ?>
|
||||
<tr class="binding-group-header table-light">
|
||||
<td colspan="5">
|
||||
<strong><?= htmlspecialchars($g['label']) ?></strong>
|
||||
<?php if (($g['kind'] ?? 'custom') === 'fixed'): ?><span class="badge bg-light text-dark border">campo fisso</span><?php endif; ?>
|
||||
<span class="text-muted">· <?= $totalCount ?> valore/i<?php if ($pendCount): ?>, <?= $pendCount ?> da risolvere<?php endif; ?></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php foreach ($g['pending'] as $p): $idx = $p['idx']; $item = $p['data'];
|
||||
$suggestion = $item['suggestion'] ?? null; ?>
|
||||
<tr class="binding-row"
|
||||
data-index="<?= $idx ?>"
|
||||
data-kind="<?= htmlspecialchars($item['kind'] ?? 'custom', ENT_QUOTES) ?>"
|
||||
data-fixed-key="<?= htmlspecialchars($item['fixed_field_key'] ?? '', ENT_QUOTES) ?>"
|
||||
data-mapping-id="<?= (int) $item['mapping_id'] ?>"
|
||||
data-field-id="<?= (int) $item['field_id'] ?>"
|
||||
data-source-value="<?= htmlspecialchars($item['source_value'], ENT_QUOTES) ?>"
|
||||
data-datadb-ids="<?= htmlspecialchars(json_encode($item['datadb_ids']), ENT_QUOTES) ?>">
|
||||
<td class="text-muted ps-4">›</td>
|
||||
<td class="source-value"><?= htmlspecialchars($item['source_value']) ?></td>
|
||||
<td><?= count($item['datadb_ids'] ?? []) ?></td>
|
||||
<td>
|
||||
<select class="form-select binding-select">
|
||||
<?php if ($suggestion): ?>
|
||||
<option value="<?= (int) $suggestion['lims_value_id'] ?>" selected><?= htmlspecialchars($suggestion['lims_value']) ?></option>
|
||||
<?php else: ?>
|
||||
<option value="">Seleziona valore LIMS...</option>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary skip-binding-btn">Nessuna corrispondenza</button>
|
||||
<div class="binding-status mt-1">
|
||||
<?php if ($suggestion): ?>
|
||||
<span class="badge bg-warning text-dark" title="Stesso valore gia' collegato in un altro template">
|
||||
suggerito · <?= htmlspecialchars($suggestion['template_name'] ?: ('template ' . $suggestion['template_id'])) ?>
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">In attesa</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php foreach ($g['resolved'] as $r): $res = $r['data']; ?>
|
||||
<tr class="binding-row"
|
||||
data-kind="<?= htmlspecialchars($res['kind'] ?? 'custom', ENT_QUOTES) ?>"
|
||||
data-fixed-key="<?= htmlspecialchars($res['fixed_field_key'] ?? '', ENT_QUOTES) ?>"
|
||||
data-mapping-id="<?= (int) $res['mapping_id'] ?>"
|
||||
data-field-id="<?= (int) $res['field_id'] ?>"
|
||||
data-source-value="<?= htmlspecialchars($res['source_value'], ENT_QUOTES) ?>"
|
||||
data-datadb-ids="<?= htmlspecialchars(json_encode($res['datadb_ids']), ENT_QUOTES) ?>">
|
||||
<td class="text-muted ps-4">›</td>
|
||||
<td class="source-value"><?= htmlspecialchars($res['source_value']) ?></td>
|
||||
<td><?= count($res['datadb_ids'] ?? []) ?></td>
|
||||
<td>
|
||||
<select class="form-select binding-select">
|
||||
<option value="<?= (int) $res['lims_value_id'] ?>" selected><?= htmlspecialchars($res['lims_value']) ?></option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary skip-binding-btn">Nessuna corrispondenza</button>
|
||||
<div class="binding-status mt-1"><span class="badge <?= $res['badge_class'] ?>"><?= $res['badge'] ?></span></div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end gap-2 mt-3">
|
||||
<a href="resolve_bindings.php?done=1"
|
||||
class="btn btn-outline-secondary">Salta per ora</a>
|
||||
<button type="submit" class="btn btn-primary" id="confirmBindingsBtn" disabled>
|
||||
Conferma e prosegui
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay toggle-icon"></div>
|
||||
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
const TEMPLATE_ID = <?= $templateId ?>;
|
||||
const IMPORT_REF = <?= json_encode($importRef) ?>;
|
||||
const CONTINUE_URL = 'resolve_bindings.php?done=1';
|
||||
|
||||
const $form = $('#bindingForm');
|
||||
const $btn = $('#confirmBindingsBtn');
|
||||
const $error = $('#bindingError');
|
||||
|
||||
$('.binding-select').each(function() {
|
||||
const $row = $(this).closest('.binding-row');
|
||||
const isFixed = $row.data('kind') === 'fixed';
|
||||
|
||||
$(this).select2({
|
||||
placeholder: 'Seleziona valore LIMS...',
|
||||
width: '100%',
|
||||
ajax: {
|
||||
url: isFixed ? 'search_fixed_field_values.php' : 'search_customfield_values.php',
|
||||
dataType: 'json',
|
||||
delay: 200,
|
||||
data: params => isFixed ? {
|
||||
field_key: $row.data('fixed-key'),
|
||||
template_id: TEMPLATE_ID,
|
||||
q: params.term || '',
|
||||
limit: 50
|
||||
} : {
|
||||
field_id: $row.data('field-id'),
|
||||
q: params.term || '',
|
||||
limit: 50
|
||||
},
|
||||
processResults: data => ({
|
||||
results: data.results || []
|
||||
})
|
||||
},
|
||||
minimumInputLength: 0
|
||||
});
|
||||
});
|
||||
|
||||
function refreshButton() {
|
||||
const allReady = $('.binding-row').toArray().every(row => {
|
||||
const $row = $(row);
|
||||
return $row.hasClass('is-skipped') || $row.find('.binding-select').val();
|
||||
});
|
||||
$btn.prop('disabled', !allReady);
|
||||
}
|
||||
|
||||
$('.binding-select').on('change', refreshButton);
|
||||
refreshButton();
|
||||
|
||||
$('.skip-binding-btn').on('click', function() {
|
||||
const $row = $(this).closest('.binding-row');
|
||||
const $select = $row.find('.binding-select');
|
||||
const $status = $row.find('.binding-status');
|
||||
const skipped = $row.toggleClass('is-skipped').hasClass('is-skipped');
|
||||
|
||||
$(this).toggleClass('btn-outline-secondary', !skipped).toggleClass('btn-secondary', skipped);
|
||||
$select.val(null).trigger('change').prop('disabled', skipped);
|
||||
$status.html(skipped ? 'Nessuna corrispondenza' : '<span class="text-muted">In attesa</span>');
|
||||
refreshButton();
|
||||
});
|
||||
|
||||
$form.on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
$error.hide();
|
||||
$btn.prop('disabled', true).text('Salvataggio...');
|
||||
|
||||
const tasks = $('.binding-row').toArray().map(row => {
|
||||
const $row = $(row);
|
||||
const $status = $row.find('.binding-status');
|
||||
const datadbIds = JSON.stringify($row.data('datadb-ids') || []);
|
||||
const sourceValue = String($row.data('source-value'));
|
||||
const mappingId = $row.data('mapping-id');
|
||||
|
||||
if ($row.hasClass('is-skipped')) {
|
||||
return $.post('skip_binding.php', {
|
||||
kind: $row.data('kind') || 'custom',
|
||||
mapping_id: mappingId,
|
||||
template_id: TEMPLATE_ID,
|
||||
source_value: sourceValue,
|
||||
datadb_ids: datadbIds
|
||||
}).then(resp => {
|
||||
if (resp && resp.success) {
|
||||
$status.html('<span class="text-success">Azzerato</span>');
|
||||
return true;
|
||||
}
|
||||
throw new Error((resp && resp.error) || 'Errore azzeramento valore');
|
||||
});
|
||||
}
|
||||
|
||||
const $select = $row.find('.binding-select');
|
||||
const selectedData = $select.select2('data')[0] || {};
|
||||
|
||||
return $.post('save_binding.php', {
|
||||
kind: $row.data('kind') || 'custom',
|
||||
mapping_id: mappingId,
|
||||
field_id: $row.data('field-id'),
|
||||
template_id: TEMPLATE_ID,
|
||||
source_value: sourceValue,
|
||||
lims_value_id: $select.val(),
|
||||
lims_value: selectedData.text || '',
|
||||
datadb_ids: datadbIds
|
||||
}).then(resp => {
|
||||
if (resp && resp.success) {
|
||||
$status.html('<span class="text-success">Salvato</span>');
|
||||
return true;
|
||||
}
|
||||
throw new Error((resp && resp.error) || 'Errore salvataggio binding');
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all(tasks)
|
||||
.then(() => {
|
||||
window.location.href = CONTINUE_URL;
|
||||
})
|
||||
.catch(err => {
|
||||
$error.text(err.message || 'Errore durante il salvataggio dei binding.').show();
|
||||
$btn.prop('disabled', false).text('Conferma e prosegui');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
// Evita che eventuale output di headscript rompa il JSON
|
||||
ob_start();
|
||||
include('include/headscript.php');
|
||||
ob_end_clean();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$iddatadb = (int)($_POST['iddatadb'] ?? 0);
|
||||
$newStatus = $_POST['status'] ?? 'i';
|
||||
|
||||
if ($iddatadb <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID record mancante']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Da questa azione consentiamo solo il ritorno a 'i'
|
||||
if ($newStatus !== 'i') {
|
||||
echo json_encode(['success' => false, 'message' => 'Stato non consentito']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
// Aggiorna solo se attualmente è in stato 'l' (To LIMS)
|
||||
$stmt = $pdo->prepare("UPDATE datadb SET status = ? WHERE iddatadb = ? AND status = 'l'");
|
||||
$stmt->execute([$newStatus, $iddatadb]);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => 'Record non trovato o già modificato']);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,549 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (!$user->hasRole('Admin') && !$user->hasRole('SuperUser')) {
|
||||
header('Location: import_dashboard.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
/*
|
||||
* Nome file del motore generico condiviso da tutte le routine engine.
|
||||
* Deve corrispondere al file presente in routines/.
|
||||
*/
|
||||
$engineFilename = '_engine.php';
|
||||
|
||||
/*
|
||||
* CSRF token.
|
||||
*/
|
||||
if (empty($_SESSION['routine_admin_csrf'])) {
|
||||
$_SESSION['routine_admin_csrf'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
$csrfToken = $_SESSION['routine_admin_csrf'];
|
||||
|
||||
function cleanInput($value)
|
||||
{
|
||||
return trim((string)($value ?? ''));
|
||||
}
|
||||
|
||||
function redirectWithMessage($type, $message)
|
||||
{
|
||||
$_SESSION['routine_admin_flash'] = [
|
||||
'type' => $type,
|
||||
'message' => $message
|
||||
];
|
||||
header('Location: ' . $_SERVER['PHP_SELF']);
|
||||
exit;
|
||||
}
|
||||
|
||||
/*
|
||||
* Gestione azioni POST.
|
||||
*/
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
try {
|
||||
$postedToken = $_POST['csrf_token'] ?? '';
|
||||
if (!hash_equals($csrfToken, $postedToken)) {
|
||||
throw new Exception('Token di sicurezza non valido.');
|
||||
}
|
||||
|
||||
$action = cleanInput($_POST['action'] ?? '');
|
||||
|
||||
if ($action === 'create' || $action === 'update') {
|
||||
$name = cleanInput($_POST['name'] ?? '');
|
||||
$description = cleanInput($_POST['description'] ?? '');
|
||||
$engine = (int)($_POST['engine'] ?? 0) === 1 ? 1 : 0;
|
||||
$headerrow = (int)($_POST['headerrow'] ?? 1);
|
||||
$filename = cleanInput($_POST['filename'] ?? '');
|
||||
|
||||
if ($name === '') {
|
||||
throw new Exception('Il nome della routine è obbligatorio.');
|
||||
}
|
||||
if ($headerrow < 1) {
|
||||
$headerrow = 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Se è una routine a motore, il filename è forzato al file del motore.
|
||||
* Se è legacy, serve un filename di file reale.
|
||||
*/
|
||||
if ($engine === 1) {
|
||||
$filename = $GLOBALS['engineFilename'];
|
||||
} else {
|
||||
if ($filename === '') {
|
||||
throw new Exception('Per una routine di tipo File indica il nome del file (es. fendi.php).');
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'create') {
|
||||
// instruction è NOT NULL nello schema: valorizziamo sempre.
|
||||
$instruction = $engine === 1
|
||||
? 'Routine gestita dal motore generico (routine_step).'
|
||||
: ('Routine su file: ' . $filename);
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO routine (name, description, instruction, filename, engine, headerrow)
|
||||
VALUES (:name, :descr, :instr, :fn, :engine, :hr)
|
||||
");
|
||||
$stmt->execute([
|
||||
':name' => $name,
|
||||
':descr' => ($description !== '' ? $description : null),
|
||||
':instr' => $instruction,
|
||||
':fn' => $filename,
|
||||
':engine' => $engine,
|
||||
':hr' => $headerrow,
|
||||
]);
|
||||
|
||||
redirectWithMessage('success', 'Routine creata correttamente.');
|
||||
} else {
|
||||
$idroutine = (int)($_POST['idroutine'] ?? 0);
|
||||
if ($idroutine <= 0) {
|
||||
throw new Exception('ID routine non valido.');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE routine
|
||||
SET name = :name,
|
||||
description = :descr,
|
||||
filename = :fn,
|
||||
engine = :engine,
|
||||
headerrow = :hr
|
||||
WHERE idroutine = :id
|
||||
");
|
||||
$stmt->execute([
|
||||
':name' => $name,
|
||||
':descr' => ($description !== '' ? $description : null),
|
||||
':fn' => $filename,
|
||||
':engine' => $engine,
|
||||
':hr' => $headerrow,
|
||||
':id' => $idroutine,
|
||||
]);
|
||||
|
||||
redirectWithMessage('success', 'Routine aggiornata correttamente.');
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'delete') {
|
||||
$idroutine = (int)($_POST['idroutine'] ?? 0);
|
||||
if ($idroutine <= 0) {
|
||||
throw new Exception('ID routine non valido.');
|
||||
}
|
||||
|
||||
// Rimuove prima gli eventuali step collegati, poi la routine.
|
||||
$stmt = $pdo->prepare("DELETE FROM routine_step WHERE idroutine = :id");
|
||||
$stmt->execute([':id' => $idroutine]);
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM routine WHERE idroutine = :id");
|
||||
$stmt->execute([':id' => $idroutine]);
|
||||
|
||||
redirectWithMessage('success', 'Routine eliminata correttamente.');
|
||||
}
|
||||
|
||||
throw new Exception('Azione non valida.');
|
||||
} catch (Exception $e) {
|
||||
redirectWithMessage('error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Caricamento routine + conteggio step.
|
||||
*/
|
||||
$stmt = $pdo->query("
|
||||
SELECT
|
||||
r.idroutine,
|
||||
r.name,
|
||||
r.description,
|
||||
r.filename,
|
||||
r.engine,
|
||||
r.headerrow,
|
||||
(SELECT COUNT(*) FROM routine_step s WHERE s.idroutine = r.idroutine) AS step_count
|
||||
FROM routine r
|
||||
ORDER BY r.name ASC
|
||||
");
|
||||
$routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$flash = $_SESSION['routine_admin_flash'] ?? null;
|
||||
unset($_SESSION['routine_admin_flash']);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
<?php include('cssinclude.php'); ?>
|
||||
|
||||
<title>Gestione Routine - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
|
||||
<style>
|
||||
.small-muted {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* La tabella non deve sforare la pagina */
|
||||
#routinesTable {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#routinesTable td,
|
||||
#routinesTable th {
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
white-space: normal;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Colonna descrizione: va a capo invece di allargare la tabella */
|
||||
#routinesTable td.desc-cell {
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
/* Le azioni restano su una riga */
|
||||
#routinesTable td.table-actions {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge-engine {
|
||||
background: #6f42c1;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.badge-file {
|
||||
background: #6c757d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filename-chip {
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
font-size: 12px;
|
||||
background: #f1f5f9;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 2px 8px;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.step-count-pill {
|
||||
display: inline-block;
|
||||
min-width: 26px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
border-radius: 999px;
|
||||
padding: 2px 10px;
|
||||
background: #eef2ff;
|
||||
color: #4338ca;
|
||||
border: 1px solid #e0e7ff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
<?php include('top_stat_widget.php'); ?>
|
||||
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h6 class="mb-0">Gestione Routine</h6>
|
||||
<small class="text-muted">Crea e configura le routine di importazione. Le routine di tipo Motore sono configurabili a step senza codice.</small>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" id="btnAddRoutine">
|
||||
<i class="bx bx-plus"></i> Nuova Routine
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table id="routinesTable" class="table table-striped table-bordered align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>Descrizione</th>
|
||||
<th style="width: 120px;">Tipo</th>
|
||||
<th style="width: 160px;">File</th>
|
||||
<th style="width: 90px;">Header row</th>
|
||||
<th style="width: 80px;">Step</th>
|
||||
<th style="width: 130px;">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($routines as $row): ?>
|
||||
<?php $isEngine = ((int)$row['engine'] === 1); ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($row['name'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
|
||||
<td class="small-muted desc-cell"><?= htmlspecialchars($row['description'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
|
||||
<td>
|
||||
<?php if ($isEngine): ?>
|
||||
<span class="badge badge-engine">Motore</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-file">File</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><span class="filename-chip"><?= htmlspecialchars($row['filename'] ?? '', ENT_QUOTES, 'UTF-8'); ?></span></td>
|
||||
<td class="text-center"><?= (int)$row['headerrow']; ?></td>
|
||||
<td class="text-center">
|
||||
<?php if ($isEngine): ?>
|
||||
<span class="step-count-pill"><?= (int)$row['step_count']; ?></span>
|
||||
<?php else: ?>
|
||||
<span class="small-muted">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="table-actions">
|
||||
<?php if ($isEngine): ?>
|
||||
<a class="btn btn-sm btn-outline-success"
|
||||
href="routine_steps.php?idroutine=<?= (int)$row['idroutine']; ?>"
|
||||
title="Gestisci step" data-bs-toggle="tooltip">
|
||||
<i class="bx bx-list-ul"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-primary btnEditRoutine"
|
||||
data-id="<?= (int)$row['idroutine']; ?>"
|
||||
data-name="<?= htmlspecialchars($row['name'] ?? '', ENT_QUOTES, 'UTF-8'); ?>"
|
||||
data-description="<?= htmlspecialchars($row['description'] ?? '', ENT_QUOTES, 'UTF-8'); ?>"
|
||||
data-engine="<?= (int)$row['engine']; ?>"
|
||||
data-filename="<?= htmlspecialchars($row['filename'] ?? '', ENT_QUOTES, 'UTF-8'); ?>"
|
||||
data-headerrow="<?= (int)$row['headerrow']; ?>"
|
||||
title="Modifica" data-bs-toggle="tooltip">
|
||||
<i class="bx bx-edit"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-danger btnDeleteRoutine"
|
||||
data-id="<?= (int)$row['idroutine']; ?>"
|
||||
data-name="<?= htmlspecialchars($row['name'] ?? '', ENT_QUOTES, 'UTF-8'); ?>"
|
||||
title="Elimina" data-bs-toggle="tooltip">
|
||||
<i class="bx bx-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overlay toggle-icon"></div>
|
||||
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<!-- Modal Routine -->
|
||||
<div class="modal fade" id="routineModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<form method="POST" class="modal-content" id="routineForm">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<input type="hidden" name="action" id="formAction" value="create">
|
||||
<input type="hidden" name="idroutine" id="idroutine" value="">
|
||||
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="routineModalTitle">Nuova Routine</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label for="name" class="form-label">Nome *</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<label for="description" class="form-label">Descrizione</label>
|
||||
<input type="text" class="form-control" id="description" name="description" maxlength="150">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="engine" class="form-label">Tipo routine *</label>
|
||||
<select class="form-select" id="engine" name="engine" required>
|
||||
<option value="1">Motore (step configurabili)</option>
|
||||
<option value="0">File (legacy)</option>
|
||||
</select>
|
||||
<div class="small-muted mt-1">
|
||||
Motore: configurabile a step da questa interfaccia. File: logica in un file PHP dedicato.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="headerrow" class="form-label">Riga intestazione (header row) *</label>
|
||||
<input type="number" class="form-control" id="headerrow" name="headerrow" min="1" value="1" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12" id="filenameWrapper">
|
||||
<label for="filename" class="form-label">Nome file</label>
|
||||
<input type="text" class="form-control" id="filename" name="filename" placeholder="es. fendi.php">
|
||||
<div class="small-muted mt-1" id="filenameHelp">
|
||||
Per le routine di tipo File indica il nome del file PHP presente nella cartella routines/.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-primary" id="saveRoutineButton">Salva Routine</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Form -->
|
||||
<form method="POST" id="deleteRoutineForm" style="display:none;">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="idroutine" id="deleteIdRoutine" value="">
|
||||
</form>
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const routineModal = new bootstrap.Modal(document.getElementById('routineModal'));
|
||||
|
||||
const formAction = document.getElementById('formAction');
|
||||
const idroutine = document.getElementById('idroutine');
|
||||
const routineModalTitle = document.getElementById('routineModalTitle');
|
||||
const saveRoutineButton = document.getElementById('saveRoutineButton');
|
||||
|
||||
const name = document.getElementById('name');
|
||||
const description = document.getElementById('description');
|
||||
const engine = document.getElementById('engine');
|
||||
const headerrow = document.getElementById('headerrow');
|
||||
const filename = document.getElementById('filename');
|
||||
const filenameWrapper = document.getElementById('filenameWrapper');
|
||||
const filenameHelp = document.getElementById('filenameHelp');
|
||||
|
||||
const engineFilename = "<?= htmlspecialchars($engineFilename, ENT_QUOTES, 'UTF-8'); ?>";
|
||||
|
||||
/*
|
||||
* Mostra/nasconde il campo filename in base al tipo di routine.
|
||||
* Motore -> filename forzato al file del motore e campo di sola lettura.
|
||||
*/
|
||||
function applyEngineUi() {
|
||||
if (engine.value === '1') {
|
||||
filename.value = engineFilename;
|
||||
filename.readOnly = true;
|
||||
filenameHelp.textContent = 'Le routine di tipo Motore usano automaticamente il file ' + engineFilename + '.';
|
||||
} else {
|
||||
if (filename.value === engineFilename) {
|
||||
filename.value = '';
|
||||
}
|
||||
filename.readOnly = false;
|
||||
filenameHelp.textContent = 'Per le routine di tipo File indica il nome del file PHP presente nella cartella routines/.';
|
||||
}
|
||||
}
|
||||
|
||||
engine.addEventListener('change', applyEngineUi);
|
||||
|
||||
function resetModalForCreate() {
|
||||
formAction.value = 'create';
|
||||
idroutine.value = '';
|
||||
routineModalTitle.textContent = 'Nuova Routine';
|
||||
saveRoutineButton.textContent = 'Crea Routine';
|
||||
|
||||
name.value = '';
|
||||
description.value = '';
|
||||
engine.value = '1';
|
||||
headerrow.value = '1';
|
||||
filename.value = '';
|
||||
applyEngineUi();
|
||||
}
|
||||
|
||||
function setModalForEdit(button) {
|
||||
formAction.value = 'update';
|
||||
idroutine.value = button.dataset.id || '';
|
||||
routineModalTitle.textContent = 'Modifica Routine';
|
||||
saveRoutineButton.textContent = 'Aggiorna Routine';
|
||||
|
||||
name.value = button.dataset.name || '';
|
||||
description.value = button.dataset.description || '';
|
||||
engine.value = button.dataset.engine === '1' ? '1' : '0';
|
||||
headerrow.value = button.dataset.headerrow || '1';
|
||||
filename.value = button.dataset.filename || '';
|
||||
applyEngineUi();
|
||||
}
|
||||
|
||||
document.getElementById('btnAddRoutine').addEventListener('click', function() {
|
||||
resetModalForCreate();
|
||||
routineModal.show();
|
||||
});
|
||||
|
||||
document.querySelectorAll('.btnEditRoutine').forEach(function(button) {
|
||||
button.addEventListener('click', function() {
|
||||
setModalForEdit(button);
|
||||
routineModal.show();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.btnDeleteRoutine').forEach(function(button) {
|
||||
button.addEventListener('click', function() {
|
||||
const id = button.dataset.id;
|
||||
const nm = button.dataset.name || 'questa routine';
|
||||
|
||||
Swal.fire({
|
||||
title: 'Eliminare la routine?',
|
||||
text: 'Vuoi eliminare "' + nm + '"? Verranno rimossi anche gli step collegati. Operazione irreversibile.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Sì, elimina',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonColor: '#d33'
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
document.getElementById('deleteIdRoutine').value = id;
|
||||
document.getElementById('deleteRoutineForm').submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (typeof $ !== 'undefined' && $.fn.DataTable) {
|
||||
$('#routinesTable').DataTable({
|
||||
pageLength: 25,
|
||||
order: [
|
||||
[0, 'asc']
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
<?php if ($flash): ?>
|
||||
Swal.fire({
|
||||
icon: '<?= htmlspecialchars($flash['type'], ENT_QUOTES, 'UTF-8'); ?>',
|
||||
title: '<?= $flash['type'] === 'success' ? 'Fatto' : ($flash['type'] === 'warning' ? 'Attenzione' : 'Errore'); ?>',
|
||||
text: '<?= htmlspecialchars($flash['message'], ENT_QUOTES, 'UTF-8'); ?>'
|
||||
});
|
||||
<?php endif; ?>
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* routine_preview.php
|
||||
* -------------------
|
||||
* Anteprima di una routine a motore su un XLS caricato temporaneamente.
|
||||
*
|
||||
* - Riceve via multipart/form-data: `file` (XLS/XLSX) e `idroutine`.
|
||||
* - NON salva nulla: il file viene letto dal tmp di PHP ed elaborato in RAM.
|
||||
* - Legge la PRIMA riga dati subito dopo l'header row (routine.headerrow).
|
||||
* - Impacchetta la riga nel formato excelData atteso dal motore:
|
||||
* [ ['excelrow' => N, 'data' => [0=>.., 1=>.., ...]] ]
|
||||
* - Include _engine.php e applica applyRoutine().
|
||||
* - Risponde JSON con la riga elaborata + intestazioni + lettere colonna.
|
||||
*/
|
||||
|
||||
ob_start();
|
||||
session_start();
|
||||
require_once '../../vendor/autoload.php';
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
|
||||
Dotenv\Dotenv::createImmutable(dirname(__DIR__, 2))->safeLoad();
|
||||
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$response = [
|
||||
'error' => '',
|
||||
'headers' => [], // intestazioni prese dalla header row
|
||||
'letters' => [], // lettere colonna (A, B, C, ...) allineate ai valori
|
||||
'row' => [], // valori della riga DOPO l'applicazione del motore
|
||||
'excelrow' => 0, // numero riga excel elaborata
|
||||
];
|
||||
|
||||
/**
|
||||
* Indice zero-based -> lettera colonna Excel (0 -> A, 25 -> Z, 26 -> AA...).
|
||||
*/
|
||||
function previewIndexToCol($index)
|
||||
{
|
||||
$index = (int)$index;
|
||||
if ($index < 0) {
|
||||
return '';
|
||||
}
|
||||
$letters = '';
|
||||
$index += 1;
|
||||
while ($index > 0) {
|
||||
$rem = ($index - 1) % 26;
|
||||
$letters = chr(ord('A') + $rem) . $letters;
|
||||
$index = intdiv($index - 1, 26);
|
||||
}
|
||||
return $letters;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
throw new Exception('Richiesta non valida.');
|
||||
}
|
||||
|
||||
$idroutine = (int)($_POST['idroutine'] ?? 0);
|
||||
if ($idroutine <= 0) {
|
||||
throw new Exception('ID routine mancante.');
|
||||
}
|
||||
|
||||
if (
|
||||
empty($_FILES['file']) ||
|
||||
!isset($_FILES['file']['tmp_name']) ||
|
||||
$_FILES['file']['error'] !== UPLOAD_ERR_OK
|
||||
) {
|
||||
throw new Exception('Nessun file caricato o upload non riuscito.');
|
||||
}
|
||||
|
||||
// --- Recupera la routine e verifica che sia a motore ---
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT idroutine, name, engine, headerrow FROM routine WHERE idroutine = :id LIMIT 1");
|
||||
$stmt->execute([':id' => $idroutine]);
|
||||
$routine = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$routine) {
|
||||
throw new Exception('Routine non trovata.');
|
||||
}
|
||||
if ((int)$routine['engine'] !== 1) {
|
||||
throw new Exception('Questa routine non è di tipo Motore: anteprima non disponibile.');
|
||||
}
|
||||
|
||||
$headerRow = max(1, (int)$routine['headerrow']); // riga 1-based dell'intestazione
|
||||
|
||||
// --- Legge l'XLS dal file temporaneo (nessun salvataggio) ---
|
||||
$tmpPath = $_FILES['file']['tmp_name'];
|
||||
|
||||
$reader = IOFactory::createReaderForFile($tmpPath);
|
||||
$reader->setReadDataOnly(true);
|
||||
$spreadsheet = $reader->load($tmpPath);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
$highestColIndex = Coordinate::columnIndexFromString($sheet->getHighestDataColumn()); // 1-based
|
||||
$highestRow = $sheet->getHighestDataRow();
|
||||
|
||||
// --- Intestazioni dalla header row ---
|
||||
$headers = [];
|
||||
$letters = [];
|
||||
for ($c = 1; $c <= $highestColIndex; $c++) {
|
||||
$val = $sheet->getCell([$c, $headerRow])->getValue();
|
||||
$headers[] = ($val === null ? '' : (string)$val);
|
||||
$letters[] = previewIndexToCol($c - 1); // zero-based -> lettera
|
||||
}
|
||||
|
||||
// --- Trova la prima riga dati valorizzata dopo l'header ---
|
||||
$dataRowNumber = null;
|
||||
$rowValues = null;
|
||||
|
||||
for ($r = $headerRow + 1; $r <= $highestRow; $r++) {
|
||||
$vals = [];
|
||||
$hasValue = false;
|
||||
for ($c = 1; $c <= $highestColIndex; $c++) {
|
||||
$v = $sheet->getCell([$c, $r])->getValue();
|
||||
$v = ($v === null ? '' : (string)$v);
|
||||
if ($v !== '') {
|
||||
$hasValue = true;
|
||||
}
|
||||
$vals[$c - 1] = $v; // indice zero-based, allineato alle colonne
|
||||
}
|
||||
if ($hasValue) {
|
||||
$dataRowNumber = $r;
|
||||
$rowValues = $vals;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($dataRowNumber === null) {
|
||||
throw new Exception('Nessuna riga dati valorizzata trovata dopo la riga di intestazione (' . $headerRow . ').');
|
||||
}
|
||||
|
||||
// --- Impacchetta nel formato excelData e applica il motore ---
|
||||
$excelData = [
|
||||
[
|
||||
'excelrow' => $dataRowNumber,
|
||||
'data' => $rowValues,
|
||||
],
|
||||
];
|
||||
|
||||
// Il motore legge gli step dal DB in base a idroutine.
|
||||
$routineData = [
|
||||
'idroutine' => $idroutine,
|
||||
'name' => $routine['name'],
|
||||
'headerrow' => $headerRow,
|
||||
'xls_headers' => $headers,
|
||||
];
|
||||
|
||||
require_once __DIR__ . '/routines/_engine.php';
|
||||
applyRoutine($excelData, $routineData); // modifica in place
|
||||
|
||||
$processed = $excelData[0]['data'] ?? [];
|
||||
|
||||
// Riallinea l'output all'ampiezza delle intestazioni (il merge può creare indici nuovi).
|
||||
$maxIndex = max(
|
||||
count($headers) - 1,
|
||||
empty($processed) ? 0 : max(array_keys($processed))
|
||||
);
|
||||
|
||||
$outRow = [];
|
||||
$outLetters = [];
|
||||
$outHeaders = [];
|
||||
for ($i = 0; $i <= $maxIndex; $i++) {
|
||||
$outRow[] = array_key_exists($i, $processed) ? (string)$processed[$i] : '';
|
||||
$outLetters[] = previewIndexToCol($i);
|
||||
$outHeaders[] = array_key_exists($i, $headers) ? $headers[$i] : '';
|
||||
}
|
||||
|
||||
$response['headers'] = $outHeaders;
|
||||
$response['letters'] = $outLetters;
|
||||
$response['row'] = $outRow;
|
||||
$response['excelrow'] = $dataRowNumber;
|
||||
} catch (Throwable $e) {
|
||||
$response['error'] = 'Errore anteprima: ' . $e->getMessage();
|
||||
error_log('routine_preview.php: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
ob_end_clean();
|
||||
echo json_encode($response, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
@@ -0,0 +1,976 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (!$user->hasRole('Admin') && !$user->hasRole('SuperUser')) {
|
||||
header('Location: import_dashboard.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
/*
|
||||
* CSRF token.
|
||||
*/
|
||||
if (empty($_SESSION['routine_steps_csrf'])) {
|
||||
$_SESSION['routine_steps_csrf'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
$csrfToken = $_SESSION['routine_steps_csrf'];
|
||||
|
||||
function cleanInput($value)
|
||||
{
|
||||
return trim((string)($value ?? ''));
|
||||
}
|
||||
|
||||
/*
|
||||
* Determina l'id routine dalla querystring (GET) o dal POST.
|
||||
*/
|
||||
$idroutine = 0;
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$idroutine = (int)($_POST['idroutine'] ?? 0);
|
||||
} else {
|
||||
$idroutine = (int)($_GET['idroutine'] ?? 0);
|
||||
}
|
||||
|
||||
function redirectToSteps($idroutine, $type, $message)
|
||||
{
|
||||
$_SESSION['routine_steps_flash'] = [
|
||||
'type' => $type,
|
||||
'message' => $message
|
||||
];
|
||||
header('Location: routine_steps.php?idroutine=' . (int)$idroutine);
|
||||
exit;
|
||||
}
|
||||
|
||||
/*
|
||||
* Converte lettera colonna Excel in indice zero-based. Accetta anche numeri.
|
||||
* A=0, B=1, ... Z=25, AA=26, ...
|
||||
*/
|
||||
function colToIndex($ref)
|
||||
{
|
||||
$ref = strtoupper(trim((string)$ref));
|
||||
if ($ref === '') {
|
||||
return null;
|
||||
}
|
||||
if (ctype_digit($ref)) {
|
||||
return (int)$ref;
|
||||
}
|
||||
$index = 0;
|
||||
$len = strlen($ref);
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$c = $ref[$i];
|
||||
if ($c < 'A' || $c > 'Z') {
|
||||
return null;
|
||||
}
|
||||
$index = $index * 26 + (ord($c) - ord('A') + 1);
|
||||
}
|
||||
return $index - 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Converte indice zero-based in lettera Excel (per mostrare gli step salvati).
|
||||
*/
|
||||
function indexToCol($index)
|
||||
{
|
||||
$index = (int)$index;
|
||||
if ($index < 0) {
|
||||
return '';
|
||||
}
|
||||
$letters = '';
|
||||
$index += 1;
|
||||
while ($index > 0) {
|
||||
$rem = ($index - 1) % 26;
|
||||
$letters = chr(ord('A') + $rem) . $letters;
|
||||
$index = intdiv($index - 1, 26);
|
||||
}
|
||||
return $letters;
|
||||
}
|
||||
|
||||
/*
|
||||
* Normalizza una stringa di colonne separate da virgola ("D, F, H") in
|
||||
* un array di indici zero-based, validando ogni riferimento.
|
||||
*/
|
||||
function parseColumnList($raw)
|
||||
{
|
||||
$raw = trim((string)$raw);
|
||||
if ($raw === '') {
|
||||
return [];
|
||||
}
|
||||
$parts = preg_split('/[\s,;]+/', $raw);
|
||||
$out = [];
|
||||
foreach ($parts as $p) {
|
||||
if ($p === '') {
|
||||
continue;
|
||||
}
|
||||
$idx = colToIndex($p);
|
||||
if ($idx === null) {
|
||||
throw new Exception('Riferimento colonna non valido: "' . $p . '". Usa lettere come D, F, H.');
|
||||
}
|
||||
$out[] = $idx;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/*
|
||||
* Verifica esistenza routine ed engine = 1.
|
||||
*/
|
||||
$routine = null;
|
||||
if ($idroutine > 0) {
|
||||
$stmt = $pdo->prepare("SELECT idroutine, name, description, engine, headerrow FROM routine WHERE idroutine = :id LIMIT 1");
|
||||
$stmt->execute([':id' => $idroutine]);
|
||||
$routine = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
if (!$routine) {
|
||||
$_SESSION['routine_admin_flash'] = ['type' => 'error', 'message' => 'Routine non trovata.'];
|
||||
header('Location: routine_admin.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ((int)$routine['engine'] !== 1) {
|
||||
$_SESSION['routine_admin_flash'] = ['type' => 'error', 'message' => 'Questa routine è di tipo File: gli step non sono configurabili da qui.'];
|
||||
header('Location: routine_admin.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
/*
|
||||
* Vocabolario operazioni disponibili (deve combaciare con gli handler di _engine.php).
|
||||
*/
|
||||
$operations = ['merge', 'copy', 'clear', 'trim', 'replace', 'uppercase', 'lowercase'];
|
||||
|
||||
/*
|
||||
* Gestione azioni POST.
|
||||
*/
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
try {
|
||||
$postedToken = $_POST['csrf_token'] ?? '';
|
||||
if (!hash_equals($csrfToken, $postedToken)) {
|
||||
throw new Exception('Token di sicurezza non valido.');
|
||||
}
|
||||
|
||||
$action = cleanInput($_POST['action'] ?? '');
|
||||
|
||||
if ($action === 'create' || $action === 'update') {
|
||||
$operation = strtolower(cleanInput($_POST['operation'] ?? ''));
|
||||
$active = (int)($_POST['active'] ?? 1) === 1 ? 1 : 0;
|
||||
|
||||
if (!in_array($operation, $operations, true)) {
|
||||
throw new Exception('Operazione non valida.');
|
||||
}
|
||||
|
||||
/*
|
||||
* Costruzione params in base all'operazione.
|
||||
* Le colonne arrivano come lettere e vengono convertite in indici.
|
||||
*/
|
||||
$params = [];
|
||||
|
||||
if ($operation === 'merge') {
|
||||
$sources = parseColumnList($_POST['source_columns'] ?? '');
|
||||
$target = colToIndex($_POST['target_column'] ?? '');
|
||||
if (empty($sources)) {
|
||||
throw new Exception('Indica almeno una colonna sorgente (es. D, F, H).');
|
||||
}
|
||||
if ($target === null) {
|
||||
throw new Exception('Indica la colonna di destinazione (es. J).');
|
||||
}
|
||||
$params = [
|
||||
'source_columns' => $sources,
|
||||
'target_column' => $target,
|
||||
'separator' => (string)($_POST['separator'] ?? ' '),
|
||||
'skip_empty' => isset($_POST['skip_empty']) ? true : false,
|
||||
];
|
||||
} elseif ($operation === 'copy') {
|
||||
$src = colToIndex($_POST['source_column'] ?? '');
|
||||
$dst = colToIndex($_POST['target_column'] ?? '');
|
||||
if ($src === null || $dst === null) {
|
||||
throw new Exception('Indica colonna sorgente e destinazione (es. D e J).');
|
||||
}
|
||||
$params = ['source_column' => $src, 'target_column' => $dst];
|
||||
} elseif ($operation === 'clear' || $operation === 'trim' || $operation === 'uppercase' || $operation === 'lowercase') {
|
||||
$cols = parseColumnList($_POST['columns'] ?? '');
|
||||
if (empty($cols)) {
|
||||
throw new Exception('Indica almeno una colonna (es. D, F).');
|
||||
}
|
||||
$params = ['columns' => $cols];
|
||||
if ($operation === 'trim') {
|
||||
$chars = (string)($_POST['chars'] ?? '');
|
||||
if ($chars !== '') {
|
||||
$params['chars'] = $chars;
|
||||
}
|
||||
}
|
||||
} elseif ($operation === 'replace') {
|
||||
$cols = parseColumnList($_POST['columns'] ?? '');
|
||||
if (empty($cols)) {
|
||||
throw new Exception('Indica almeno una colonna (es. D, F).');
|
||||
}
|
||||
$params = [
|
||||
'columns' => $cols,
|
||||
'search' => (string)($_POST['search'] ?? ''),
|
||||
'replace' => (string)($_POST['replace'] ?? ''),
|
||||
'regex' => isset($_POST['regex']) ? true : false,
|
||||
];
|
||||
}
|
||||
|
||||
$paramsJson = json_encode($params, JSON_UNESCAPED_UNICODE);
|
||||
|
||||
if ($action === 'create') {
|
||||
// step_order = max + 1
|
||||
$stmt = $pdo->prepare("SELECT COALESCE(MAX(step_order), 0) + 1 AS next_order FROM routine_step WHERE idroutine = :id");
|
||||
$stmt->execute([':id' => $idroutine]);
|
||||
$nextOrder = (int)$stmt->fetchColumn();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO routine_step (idroutine, step_order, operation, params, active)
|
||||
VALUES (:id, :ord, :op, :params, :active)
|
||||
");
|
||||
$stmt->execute([
|
||||
':id' => $idroutine,
|
||||
':ord' => $nextOrder,
|
||||
':op' => $operation,
|
||||
':params' => $paramsJson,
|
||||
':active' => $active,
|
||||
]);
|
||||
|
||||
redirectToSteps($idroutine, 'success', 'Step aggiunto correttamente.');
|
||||
} else {
|
||||
$idstep = (int)($_POST['idstep'] ?? 0);
|
||||
if ($idstep <= 0) {
|
||||
throw new Exception('ID step non valido.');
|
||||
}
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE routine_step
|
||||
SET operation = :op, params = :params, active = :active
|
||||
WHERE idstep = :idstep AND idroutine = :id
|
||||
");
|
||||
$stmt->execute([
|
||||
':op' => $operation,
|
||||
':params' => $paramsJson,
|
||||
':active' => $active,
|
||||
':idstep' => $idstep,
|
||||
':id' => $idroutine,
|
||||
]);
|
||||
|
||||
redirectToSteps($idroutine, 'success', 'Step aggiornato correttamente.');
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'delete') {
|
||||
$idstep = (int)($_POST['idstep'] ?? 0);
|
||||
if ($idstep <= 0) {
|
||||
throw new Exception('ID step non valido.');
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM routine_step WHERE idstep = :idstep AND idroutine = :id");
|
||||
$stmt->execute([':idstep' => $idstep, ':id' => $idroutine]);
|
||||
redirectToSteps($idroutine, 'success', 'Step eliminato.');
|
||||
}
|
||||
|
||||
if ($action === 'move') {
|
||||
$idstep = (int)($_POST['idstep'] ?? 0);
|
||||
$dir = cleanInput($_POST['direction'] ?? '');
|
||||
if ($idstep <= 0 || !in_array($dir, ['up', 'down'], true)) {
|
||||
throw new Exception('Parametri di spostamento non validi.');
|
||||
}
|
||||
|
||||
// Carica step correnti ordinati.
|
||||
$stmt = $pdo->prepare("SELECT idstep, step_order FROM routine_step WHERE idroutine = :id ORDER BY step_order ASC, idstep ASC");
|
||||
$stmt->execute([':id' => $idroutine]);
|
||||
$all = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Trova posizione corrente.
|
||||
$pos = null;
|
||||
foreach ($all as $i => $s) {
|
||||
if ((int)$s['idstep'] === $idstep) {
|
||||
$pos = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($pos !== null) {
|
||||
$swapWith = $dir === 'up' ? $pos - 1 : $pos + 1;
|
||||
if ($swapWith >= 0 && $swapWith < count($all)) {
|
||||
$a = $all[$pos];
|
||||
$b = $all[$swapWith];
|
||||
// Scambia gli step_order dei due record.
|
||||
$upd = $pdo->prepare("UPDATE routine_step SET step_order = :ord WHERE idstep = :idstep AND idroutine = :id");
|
||||
$upd->execute([':ord' => (int)$b['step_order'], ':idstep' => (int)$a['idstep'], ':id' => $idroutine]);
|
||||
$upd->execute([':ord' => (int)$a['step_order'], ':idstep' => (int)$b['idstep'], ':id' => $idroutine]);
|
||||
}
|
||||
}
|
||||
|
||||
redirectToSteps($idroutine, 'success', 'Ordine aggiornato.');
|
||||
}
|
||||
|
||||
throw new Exception('Azione non valida.');
|
||||
} catch (Exception $e) {
|
||||
redirectToSteps($idroutine, 'error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Caricamento step della routine.
|
||||
*/
|
||||
$stmt = $pdo->prepare("SELECT idstep, step_order, operation, params, active FROM routine_step WHERE idroutine = :id ORDER BY step_order ASC, idstep ASC");
|
||||
$stmt->execute([':id' => $idroutine]);
|
||||
$steps = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
/*
|
||||
* Costruisce una descrizione leggibile dei params per la tabella.
|
||||
*/
|
||||
function describeStep($operation, array $p)
|
||||
{
|
||||
$colsToLetters = function ($arr) {
|
||||
return implode(', ', array_map('indexToCol', $arr));
|
||||
};
|
||||
switch ($operation) {
|
||||
case 'merge':
|
||||
return 'Unisce ' . $colsToLetters($p['source_columns'] ?? []) .
|
||||
' → ' . indexToCol($p['target_column'] ?? 0) .
|
||||
' (sep: "' . ($p['separator'] ?? ' ') . '")';
|
||||
case 'copy':
|
||||
return 'Copia ' . indexToCol($p['source_column'] ?? 0) . ' → ' . indexToCol($p['target_column'] ?? 0);
|
||||
case 'clear':
|
||||
return 'Svuota ' . $colsToLetters($p['columns'] ?? []);
|
||||
case 'trim':
|
||||
return 'Trim ' . $colsToLetters($p['columns'] ?? []) . (isset($p['chars']) ? ' (chars: "' . $p['chars'] . '")' : '');
|
||||
case 'uppercase':
|
||||
return 'Maiuscolo ' . $colsToLetters($p['columns'] ?? []);
|
||||
case 'lowercase':
|
||||
return 'Minuscolo ' . $colsToLetters($p['columns'] ?? []);
|
||||
case 'replace':
|
||||
return 'Sostituisci in ' . $colsToLetters($p['columns'] ?? []) .
|
||||
': "' . ($p['search'] ?? '') . '" → "' . ($p['replace'] ?? '') . '"' .
|
||||
(!empty($p['regex']) ? ' (regex)' : '');
|
||||
default:
|
||||
return $operation;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Prepara i dati step per il JS (params già in lettere per il form di modifica).
|
||||
*/
|
||||
$stepsForJs = [];
|
||||
foreach ($steps as $s) {
|
||||
$p = json_decode($s['params'] ?? '[]', true);
|
||||
if (!is_array($p)) {
|
||||
$p = [];
|
||||
}
|
||||
// Converte indici -> lettere per il form.
|
||||
$letters = function ($arr) {
|
||||
return implode(', ', array_map('indexToCol', $arr));
|
||||
};
|
||||
$formData = ['operation' => $s['operation'], 'active' => (int)$s['active']];
|
||||
if ($s['operation'] === 'merge') {
|
||||
$formData['source_columns'] = $letters($p['source_columns'] ?? []);
|
||||
$formData['target_column'] = indexToCol($p['target_column'] ?? 0);
|
||||
$formData['separator'] = $p['separator'] ?? ' ';
|
||||
$formData['skip_empty'] = !empty($p['skip_empty']);
|
||||
} elseif ($s['operation'] === 'copy') {
|
||||
$formData['source_column'] = indexToCol($p['source_column'] ?? 0);
|
||||
$formData['target_column'] = indexToCol($p['target_column'] ?? 0);
|
||||
} elseif (in_array($s['operation'], ['clear', 'trim', 'uppercase', 'lowercase'], true)) {
|
||||
$formData['columns'] = $letters($p['columns'] ?? []);
|
||||
if ($s['operation'] === 'trim') {
|
||||
$formData['chars'] = $p['chars'] ?? '';
|
||||
}
|
||||
} elseif ($s['operation'] === 'replace') {
|
||||
$formData['columns'] = $letters($p['columns'] ?? []);
|
||||
$formData['search'] = $p['search'] ?? '';
|
||||
$formData['replace'] = $p['replace'] ?? '';
|
||||
$formData['regex'] = !empty($p['regex']);
|
||||
}
|
||||
$stepsForJs[(int)$s['idstep']] = $formData;
|
||||
}
|
||||
|
||||
$flash = $_SESSION['routine_steps_flash'] ?? null;
|
||||
unset($_SESSION['routine_steps_flash']);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
<?php include('cssinclude.php'); ?>
|
||||
|
||||
<title>Step Routine - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
|
||||
<style>
|
||||
.small-muted {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.op-badge {
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
font-size: 12px;
|
||||
background: #eef2ff;
|
||||
color: #4338ca;
|
||||
border: 1px solid #e0e7ff;
|
||||
border-radius: 6px;
|
||||
padding: 2px 8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.step-inactive {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.col-hint {
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
color: #166534;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.order-btns .btn {
|
||||
padding: 2px 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
<?php include('top_stat_widget.php'); ?>
|
||||
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h6 class="mb-0">Step routine: <?= htmlspecialchars($routine['name'], ENT_QUOTES, 'UTF-8'); ?></h6>
|
||||
<small class="text-muted">
|
||||
Configura le operazioni applicate all'XLS. Vengono eseguite dall'alto verso il basso.
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<a href="routine_admin.php" class="btn btn-light">
|
||||
<i class="bx bx-arrow-back"></i> Torna alle routine
|
||||
</a>
|
||||
<button type="button" class="btn btn-primary" id="btnAddStep">
|
||||
<i class="bx bx-plus"></i> Aggiungi step
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="col-hint mb-3">
|
||||
Le colonne si indicano con le <strong>lettere dell'Excel</strong> (A, B, C, …). Per più colonne separale con la virgola: <strong>D, F, H</strong>.
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 70px;">Ordine</th>
|
||||
<th style="width: 120px;">Operazione</th>
|
||||
<th>Dettaglio</th>
|
||||
<th style="width: 90px;">Stato</th>
|
||||
<th style="width: 240px;">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($steps)): ?>
|
||||
<tr>
|
||||
<td colspan="5" class="text-center small-muted py-4">
|
||||
Nessuno step configurato. Usa "Aggiungi step" per iniziare.
|
||||
</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($steps as $i => $s): ?>
|
||||
<?php
|
||||
$p = json_decode($s['params'] ?? '[]', true);
|
||||
if (!is_array($p)) {
|
||||
$p = [];
|
||||
}
|
||||
$isFirst = ($i === 0);
|
||||
$isLast = ($i === count($steps) - 1);
|
||||
$rowClass = ((int)$s['active'] === 1) ? '' : 'step-inactive';
|
||||
?>
|
||||
<tr class="<?= $rowClass; ?>">
|
||||
<td class="text-center">
|
||||
<div class="order-btns d-inline-flex flex-column gap-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary btnMove"
|
||||
data-idstep="<?= (int)$s['idstep']; ?>" data-direction="up"
|
||||
<?= $isFirst ? 'disabled' : ''; ?>>
|
||||
<i class="bx bx-chevron-up"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary btnMove"
|
||||
data-idstep="<?= (int)$s['idstep']; ?>" data-direction="down"
|
||||
<?= $isLast ? 'disabled' : ''; ?>>
|
||||
<i class="bx bx-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="op-badge"><?= htmlspecialchars($s['operation'], ENT_QUOTES, 'UTF-8'); ?></span></td>
|
||||
<td><?= htmlspecialchars(describeStep($s['operation'], $p), ENT_QUOTES, 'UTF-8'); ?></td>
|
||||
<td>
|
||||
<?php if ((int)$s['active'] === 1): ?>
|
||||
<span class="badge bg-success">Attivo</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-secondary">Disattivo</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="table-actions">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary btnEditStep"
|
||||
data-idstep="<?= (int)$s['idstep']; ?>">
|
||||
Modifica
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger btnDeleteStep"
|
||||
data-idstep="<?= (int)$s['idstep']; ?>">
|
||||
Elimina
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<div>
|
||||
<h6 class="mb-0">Prova la routine</h6>
|
||||
<small class="text-muted">Carica un XLS di esempio: viene mostrata la prima riga dati dopo l'intestazione, elaborata con gli step qui sopra. Il file non viene salvato.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-8">
|
||||
<label for="previewFile" class="form-label">File XLS / XLSX di prova</label>
|
||||
<input type="file" class="form-control" id="previewFile" accept=".xls,.xlsx">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button type="button" class="btn btn-outline-primary w-100" id="btnRunPreview">
|
||||
<i class="bx bx-play-circle"></i> Esegui anteprima
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="previewStatus" class="small-muted mt-2"></div>
|
||||
|
||||
<div class="table-responsive mt-3" id="previewResultWrapper" style="display:none;">
|
||||
<table class="table table-bordered table-sm align-middle" id="previewTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 90px;">Colonna</th>
|
||||
<th style="width: 40%;">Intestazione</th>
|
||||
<th>Valore dopo la routine</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overlay toggle-icon"></div>
|
||||
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<!-- Modal Step -->
|
||||
<div class="modal fade" id="stepModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<form method="POST" class="modal-content" id="stepForm">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<input type="hidden" name="idroutine" value="<?= (int)$idroutine; ?>">
|
||||
<input type="hidden" name="action" id="stepFormAction" value="create">
|
||||
<input type="hidden" name="idstep" id="idstep" value="">
|
||||
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="stepModalTitle">Aggiungi step</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8">
|
||||
<label for="operation" class="form-label">Operazione *</label>
|
||||
<select class="form-select" id="operation" name="operation" required>
|
||||
<option value="merge">merge — unisci più colonne in una</option>
|
||||
<option value="copy">copy — copia una colonna in un'altra</option>
|
||||
<option value="clear">clear — svuota colonne</option>
|
||||
<option value="trim">trim — rimuovi spazi</option>
|
||||
<option value="replace">replace — sostituisci testo</option>
|
||||
<option value="uppercase">uppercase — MAIUSCOLO</option>
|
||||
<option value="lowercase">lowercase — minuscolo</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label for="active" class="form-label">Stato *</label>
|
||||
<select class="form-select" id="active" name="active" required>
|
||||
<option value="1">Attivo</option>
|
||||
<option value="0">Disattivo</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Campi MERGE -->
|
||||
<div class="col-md-8 op-field op-merge">
|
||||
<label class="form-label">Colonne sorgente *</label>
|
||||
<input type="text" class="form-control" name="source_columns" id="merge_source" placeholder="es. D, F, H">
|
||||
<div class="small-muted mt-1">Lettere separate da virgola.</div>
|
||||
</div>
|
||||
<div class="col-md-4 op-field op-merge">
|
||||
<label class="form-label">Colonna destinazione *</label>
|
||||
<input type="text" class="form-control" name="target_column" id="merge_target" placeholder="es. J">
|
||||
</div>
|
||||
<div class="col-md-8 op-field op-merge">
|
||||
<label class="form-label">Separatore</label>
|
||||
<input type="text" class="form-control" name="separator" id="merge_sep" value=" " placeholder="spazio">
|
||||
<div class="small-muted mt-1">Carattere inserito tra i valori uniti. Default: spazio.</div>
|
||||
</div>
|
||||
<div class="col-md-4 op-field op-merge d-flex align-items-end">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="skip_empty" id="merge_skip" checked>
|
||||
<label class="form-check-label" for="merge_skip">Ignora valori vuoti</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Campi COPY -->
|
||||
<div class="col-md-6 op-field op-copy">
|
||||
<label class="form-label">Colonna sorgente *</label>
|
||||
<input type="text" class="form-control" name="source_column" id="copy_source" placeholder="es. D">
|
||||
</div>
|
||||
<div class="col-md-6 op-field op-copy">
|
||||
<label class="form-label">Colonna destinazione *</label>
|
||||
<input type="text" class="form-control" name="target_column_copy" id="copy_target" placeholder="es. J">
|
||||
</div>
|
||||
|
||||
<!-- Campi COLONNE (clear/trim/uppercase/lowercase/replace) -->
|
||||
<div class="col-md-12 op-field op-columns">
|
||||
<label class="form-label">Colonne *</label>
|
||||
<input type="text" class="form-control" name="columns" id="columns_input" placeholder="es. D, F">
|
||||
<div class="small-muted mt-1">Una o più lettere separate da virgola.</div>
|
||||
</div>
|
||||
|
||||
<!-- Campo TRIM chars -->
|
||||
<div class="col-md-12 op-field op-trim">
|
||||
<label class="form-label">Caratteri da rimuovere (opzionale)</label>
|
||||
<input type="text" class="form-control" name="chars" id="trim_chars" placeholder="lascia vuoto per spazi">
|
||||
</div>
|
||||
|
||||
<!-- Campi REPLACE -->
|
||||
<div class="col-md-6 op-field op-replace">
|
||||
<label class="form-label">Testo da cercare</label>
|
||||
<input type="text" class="form-control" name="search" id="replace_search">
|
||||
</div>
|
||||
<div class="col-md-6 op-field op-replace">
|
||||
<label class="form-label">Testo sostitutivo</label>
|
||||
<input type="text" class="form-control" name="replace" id="replace_replace">
|
||||
</div>
|
||||
<div class="col-md-12 op-field op-replace">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="regex" id="replace_regex">
|
||||
<label class="form-check-label" for="replace_regex">
|
||||
Usa espressione regolare (regex) — solo se sai cosa stai facendo
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-primary" id="saveStepButton">Salva step</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form nascosti -->
|
||||
<form method="POST" id="deleteStepForm" style="display:none;">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<input type="hidden" name="idroutine" value="<?= (int)$idroutine; ?>">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="idstep" id="deleteIdStep" value="">
|
||||
</form>
|
||||
|
||||
<form method="POST" id="moveStepForm" style="display:none;">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<input type="hidden" name="idroutine" value="<?= (int)$idroutine; ?>">
|
||||
<input type="hidden" name="action" value="move">
|
||||
<input type="hidden" name="idstep" id="moveIdStep" value="">
|
||||
<input type="hidden" name="direction" id="moveDirection" value="">
|
||||
</form>
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
|
||||
<script>
|
||||
const stepsData = <?= json_encode($stepsForJs, JSON_UNESCAPED_UNICODE); ?>;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const stepModal = new bootstrap.Modal(document.getElementById('stepModal'));
|
||||
|
||||
const stepFormAction = document.getElementById('stepFormAction');
|
||||
const idstep = document.getElementById('idstep');
|
||||
const stepModalTitle = document.getElementById('stepModalTitle');
|
||||
const saveStepButton = document.getElementById('saveStepButton');
|
||||
const operation = document.getElementById('operation');
|
||||
const active = document.getElementById('active');
|
||||
|
||||
// Mappa quali gruppi di campi mostrare per ogni operazione.
|
||||
const fieldGroups = {
|
||||
merge: ['op-merge'],
|
||||
copy: ['op-copy'],
|
||||
clear: ['op-columns'],
|
||||
trim: ['op-columns', 'op-trim'],
|
||||
replace: ['op-columns', 'op-replace'],
|
||||
uppercase: ['op-columns'],
|
||||
lowercase: ['op-columns']
|
||||
};
|
||||
|
||||
function showFieldsForOperation(op) {
|
||||
// Nasconde tutti i gruppi.
|
||||
document.querySelectorAll('.op-field').forEach(function(el) {
|
||||
el.style.display = 'none';
|
||||
});
|
||||
// Mostra i gruppi dell'operazione scelta.
|
||||
(fieldGroups[op] || []).forEach(function(cls) {
|
||||
document.querySelectorAll('.' + cls).forEach(function(el) {
|
||||
el.style.display = '';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
operation.addEventListener('change', function() {
|
||||
showFieldsForOperation(operation.value);
|
||||
});
|
||||
|
||||
function resetStepForm() {
|
||||
document.getElementById('stepForm').reset();
|
||||
// reset non ripristina i checkbox "checked" di default in modo affidabile: li forziamo
|
||||
document.getElementById('merge_skip').checked = true;
|
||||
document.getElementById('replace_regex').checked = false;
|
||||
document.getElementById('merge_sep').value = ' ';
|
||||
}
|
||||
|
||||
function openForCreate() {
|
||||
stepFormAction.value = 'create';
|
||||
idstep.value = '';
|
||||
stepModalTitle.textContent = 'Aggiungi step';
|
||||
saveStepButton.textContent = 'Aggiungi step';
|
||||
|
||||
resetStepForm();
|
||||
operation.value = 'merge';
|
||||
active.value = '1';
|
||||
showFieldsForOperation('merge');
|
||||
stepModal.show();
|
||||
}
|
||||
|
||||
function openForEdit(id) {
|
||||
const data = stepsData[id];
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
stepFormAction.value = 'update';
|
||||
idstep.value = id;
|
||||
stepModalTitle.textContent = 'Modifica step';
|
||||
saveStepButton.textContent = 'Aggiorna step';
|
||||
|
||||
resetStepForm();
|
||||
operation.value = data.operation;
|
||||
active.value = String(data.active);
|
||||
|
||||
// Popola i campi in base all'operazione.
|
||||
if (data.operation === 'merge') {
|
||||
document.getElementById('merge_source').value = data.source_columns || '';
|
||||
document.getElementById('merge_target').value = data.target_column || '';
|
||||
document.getElementById('merge_sep').value = (data.separator !== undefined ? data.separator : ' ');
|
||||
document.getElementById('merge_skip').checked = !!data.skip_empty;
|
||||
} else if (data.operation === 'copy') {
|
||||
document.getElementById('copy_source').value = data.source_column || '';
|
||||
document.getElementById('copy_target').value = data.target_column || '';
|
||||
} else if (['clear', 'trim', 'uppercase', 'lowercase'].includes(data.operation)) {
|
||||
document.getElementById('columns_input').value = data.columns || '';
|
||||
if (data.operation === 'trim') {
|
||||
document.getElementById('trim_chars').value = data.chars || '';
|
||||
}
|
||||
} else if (data.operation === 'replace') {
|
||||
document.getElementById('columns_input').value = data.columns || '';
|
||||
document.getElementById('replace_search').value = data.search || '';
|
||||
document.getElementById('replace_replace').value = data.replace || '';
|
||||
document.getElementById('replace_regex').checked = !!data.regex;
|
||||
}
|
||||
|
||||
showFieldsForOperation(data.operation);
|
||||
stepModal.show();
|
||||
}
|
||||
|
||||
/*
|
||||
* NOTA sul campo destinazione di copy:
|
||||
* l'input HTML si chiama target_column_copy per non collidere con merge,
|
||||
* ma il backend si aspetta target_column. Rinominiamo al submit.
|
||||
*/
|
||||
document.getElementById('stepForm').addEventListener('submit', function(e) {
|
||||
if (operation.value === 'copy') {
|
||||
const copyTarget = document.getElementById('copy_target');
|
||||
// Crea/aggiorna un campo hidden target_column col valore di copy.
|
||||
let hidden = document.getElementById('copy_target_hidden');
|
||||
if (!hidden) {
|
||||
hidden = document.createElement('input');
|
||||
hidden.type = 'hidden';
|
||||
hidden.name = 'target_column';
|
||||
hidden.id = 'copy_target_hidden';
|
||||
this.appendChild(hidden);
|
||||
}
|
||||
hidden.value = copyTarget.value;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('btnAddStep').addEventListener('click', openForCreate);
|
||||
|
||||
document.querySelectorAll('.btnEditStep').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
openForEdit(btn.dataset.idstep);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.btnDeleteStep').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
Swal.fire({
|
||||
title: 'Eliminare lo step?',
|
||||
text: 'Operazione irreversibile.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Sì, elimina',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonColor: '#d33'
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
document.getElementById('deleteIdStep').value = btn.dataset.idstep;
|
||||
document.getElementById('deleteStepForm').submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.btnMove').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
document.getElementById('moveIdStep').value = btn.dataset.idstep;
|
||||
document.getElementById('moveDirection').value = btn.dataset.direction;
|
||||
document.getElementById('moveStepForm').submit();
|
||||
});
|
||||
});
|
||||
|
||||
// --- Anteprima routine su XLS caricato (nessun salvataggio) ---
|
||||
const previewFile = document.getElementById('previewFile');
|
||||
const btnRunPreview = document.getElementById('btnRunPreview');
|
||||
const previewStatus = document.getElementById('previewStatus');
|
||||
const previewResultWrapper = document.getElementById('previewResultWrapper');
|
||||
const previewTableBody = document.querySelector('#previewTable tbody');
|
||||
const currentIdRoutine = "<?= (int)$idroutine; ?>";
|
||||
|
||||
btnRunPreview.addEventListener('click', function() {
|
||||
if (!previewFile.files || previewFile.files.length === 0) {
|
||||
Swal.fire({
|
||||
icon: 'info',
|
||||
title: 'Nessun file',
|
||||
text: 'Seleziona un file XLS di prova.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('idroutine', currentIdRoutine);
|
||||
formData.append('file', previewFile.files[0]);
|
||||
|
||||
previewStatus.textContent = 'Elaborazione in corso...';
|
||||
previewResultWrapper.style.display = 'none';
|
||||
previewTableBody.innerHTML = '';
|
||||
btnRunPreview.disabled = true;
|
||||
|
||||
fetch('routine_preview.php', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
cache: 'no-store'
|
||||
})
|
||||
.then(function(res) {
|
||||
return res.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
btnRunPreview.disabled = false;
|
||||
|
||||
if (data.error) {
|
||||
previewStatus.textContent = '';
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'Anteprima non riuscita',
|
||||
text: data.error
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
previewStatus.textContent = 'Riga elaborata: riga Excel ' + data.excelrow + '.';
|
||||
|
||||
const letters = data.letters || [];
|
||||
const headers = data.headers || [];
|
||||
const row = data.row || [];
|
||||
|
||||
for (let i = 0; i < row.length; i++) {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
const tdCol = document.createElement('td');
|
||||
tdCol.innerHTML = '<strong>' + (letters[i] || '') + '</strong>';
|
||||
|
||||
const tdHead = document.createElement('td');
|
||||
tdHead.className = 'small-muted';
|
||||
tdHead.textContent = headers[i] || '';
|
||||
|
||||
const tdVal = document.createElement('td');
|
||||
tdVal.textContent = row[i] || '';
|
||||
|
||||
tr.appendChild(tdCol);
|
||||
tr.appendChild(tdHead);
|
||||
tr.appendChild(tdVal);
|
||||
previewTableBody.appendChild(tr);
|
||||
}
|
||||
|
||||
previewResultWrapper.style.display = '';
|
||||
})
|
||||
.catch(function(err) {
|
||||
btnRunPreview.disabled = false;
|
||||
previewStatus.textContent = '';
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Errore',
|
||||
text: 'Impossibile eseguire l\'anteprima.'
|
||||
});
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
<?php if ($flash): ?>
|
||||
Swal.fire({
|
||||
icon: '<?= htmlspecialchars($flash['type'], ENT_QUOTES, 'UTF-8'); ?>',
|
||||
title: '<?= $flash['type'] === 'success' ? 'Fatto' : ($flash['type'] === 'warning' ? 'Attenzione' : 'Errore'); ?>',
|
||||
text: '<?= htmlspecialchars($flash['message'], ENT_QUOTES, 'UTF-8'); ?>'
|
||||
});
|
||||
<?php endif; ?>
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,296 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Routine motore generico (_engine.php)
|
||||
* -------------------------------------
|
||||
* File UNICO condiviso da tutte le routine con engine = 1.
|
||||
*
|
||||
* Espone la stessa firma delle routine legacy:
|
||||
*
|
||||
* applyRoutine(&$excelData, $routineData)
|
||||
*
|
||||
* così apply_routine.php lo include allo stesso modo. La differenza è che qui
|
||||
* NON c'è logica hardcoded: gli step vengono letti dalla tabella routine_step
|
||||
* (in base a routineData['idroutine']) ed eseguiti in ordine.
|
||||
*
|
||||
* Ogni step ha una `operation` (verbo del vocabolario) e `params` JSON.
|
||||
* Le colonne nei params possono essere indici zero-based (3) o lettere Excel ("D").
|
||||
*/
|
||||
|
||||
if (!function_exists('applyRoutine')) {
|
||||
|
||||
/**
|
||||
* Converte un riferimento colonna in indice zero-based.
|
||||
* Accetta int/indici numerici (3) o lettere Excel ("D", "AA", ...).
|
||||
*/
|
||||
function routineEngineColToIndex($ref): int
|
||||
{
|
||||
if (is_int($ref)) {
|
||||
return $ref;
|
||||
}
|
||||
$ref = trim((string)$ref);
|
||||
if ($ref === '') {
|
||||
return 0;
|
||||
}
|
||||
// Numerico puro -> indice diretto
|
||||
if (ctype_digit($ref)) {
|
||||
return (int)$ref;
|
||||
}
|
||||
// Lettere Excel -> indice zero-based (A=0, B=1, ..., Z=25, AA=26, ...)
|
||||
$ref = strtoupper($ref);
|
||||
$index = 0;
|
||||
$len = strlen($ref);
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$c = $ref[$i];
|
||||
if ($c < 'A' || $c > 'Z') {
|
||||
continue;
|
||||
}
|
||||
$index = $index * 26 + (ord($c) - ord('A') + 1);
|
||||
}
|
||||
return $index - 1;
|
||||
}
|
||||
|
||||
/** Normalizza una lista di riferimenti colonna in indici zero-based. */
|
||||
function routineEngineColsToIndexes(array $refs): array
|
||||
{
|
||||
return array_map('routineEngineColToIndex', $refs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Carica gli step attivi di una routine dal DB, ordinati per step_order.
|
||||
* Ritorna array di ['operation' => string, 'params' => array].
|
||||
*/
|
||||
function routineEngineLoadSteps(int $idroutine): array
|
||||
{
|
||||
// Riusa la connessione PDO del progetto.
|
||||
// db-functions.php sta in .../class/ rispetto alla cartella routines/.
|
||||
// Aggiusta il require se nel tuo albero il path differisce.
|
||||
$dbFile = dirname(__DIR__) . '/class/db-functions.php';
|
||||
if (!class_exists('DBHandlerSelect') && file_exists($dbFile)) {
|
||||
require_once $dbFile;
|
||||
}
|
||||
|
||||
if (!class_exists('DBHandlerSelect')) {
|
||||
throw new Exception("Motore routine: classe DBHandlerSelect non trovata (path db-functions.php da verificare).");
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT operation, params
|
||||
FROM routine_step
|
||||
WHERE idroutine = :id AND active = 1
|
||||
ORDER BY step_order ASC, idstep ASC"
|
||||
);
|
||||
$stmt->execute([':id' => $idroutine]);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
$steps = [];
|
||||
foreach ($rows as $r) {
|
||||
$params = [];
|
||||
if (!empty($r['params'])) {
|
||||
$decoded = json_decode($r['params'], true);
|
||||
if (is_array($decoded)) {
|
||||
$params = $decoded;
|
||||
} else {
|
||||
error_log("Motore routine: params JSON non valido per operazione {$r['operation']} (routine {$idroutine}).");
|
||||
}
|
||||
}
|
||||
$steps[] = ['operation' => strtolower(trim($r['operation'])), 'params' => $params];
|
||||
}
|
||||
return $steps;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* HANDLERS – ogni operazione modifica $excelData in place.
|
||||
* Aggiungere una capacità nuova = aggiungere un handler qui, una volta.
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* merge: unisce N colonne sorgente in una destinazione.
|
||||
* params: source_columns [], target_column, separator=" ", skip_empty=true
|
||||
*/
|
||||
function routineOpMerge(array &$excelData, array $params): void
|
||||
{
|
||||
$sources = routineEngineColsToIndexes($params['source_columns'] ?? []);
|
||||
$target = routineEngineColToIndex($params['target_column'] ?? 0);
|
||||
$sep = array_key_exists('separator', $params) ? (string)$params['separator'] : ' ';
|
||||
$skip = $params['skip_empty'] ?? true;
|
||||
|
||||
foreach ($excelData as $rowIndex => &$row) {
|
||||
if (!isset($row['data']) || !is_array($row['data'])) {
|
||||
error_log("merge: struttura riga non valida all'indice {$rowIndex}.");
|
||||
continue;
|
||||
}
|
||||
$parts = [];
|
||||
foreach ($sources as $col) {
|
||||
$val = trim((string)($row['data'][$col] ?? ''));
|
||||
if ($skip && $val === '') {
|
||||
continue;
|
||||
}
|
||||
$parts[] = $val;
|
||||
}
|
||||
$row['data'][$target] = implode($sep, $parts);
|
||||
}
|
||||
unset($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* copy: copia una colonna sorgente in una destinazione.
|
||||
* params: source_column, target_column
|
||||
*/
|
||||
function routineOpCopy(array &$excelData, array $params): void
|
||||
{
|
||||
$src = routineEngineColToIndex($params['source_column'] ?? 0);
|
||||
$dst = routineEngineColToIndex($params['target_column'] ?? 0);
|
||||
foreach ($excelData as &$row) {
|
||||
if (!isset($row['data']) || !is_array($row['data'])) {
|
||||
continue;
|
||||
}
|
||||
$row['data'][$dst] = $row['data'][$src] ?? '';
|
||||
}
|
||||
unset($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* clear: svuota una o più colonne.
|
||||
* params: columns [] (oppure column singola)
|
||||
*/
|
||||
function routineOpClear(array &$excelData, array $params): void
|
||||
{
|
||||
$cols = $params['columns'] ?? (isset($params['column']) ? [$params['column']] : []);
|
||||
$cols = routineEngineColsToIndexes($cols);
|
||||
foreach ($excelData as &$row) {
|
||||
if (!isset($row['data']) || !is_array($row['data'])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($cols as $c) {
|
||||
$row['data'][$c] = '';
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* trim: rimuove spazi (o caratteri indicati) da una o più colonne.
|
||||
* params: columns [] (o column), chars (opzionale)
|
||||
*/
|
||||
function routineOpTrim(array &$excelData, array $params): void
|
||||
{
|
||||
$cols = $params['columns'] ?? (isset($params['column']) ? [$params['column']] : []);
|
||||
$cols = routineEngineColsToIndexes($cols);
|
||||
$chars = $params['chars'] ?? null;
|
||||
foreach ($excelData as &$row) {
|
||||
if (!isset($row['data']) || !is_array($row['data'])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($cols as $c) {
|
||||
$v = (string)($row['data'][$c] ?? '');
|
||||
$row['data'][$c] = $chars === null ? trim($v) : trim($v, $chars);
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* replace: sostituzione testo in una o più colonne.
|
||||
* params: columns [] (o column), search, replace="", regex=false
|
||||
*/
|
||||
function routineOpReplace(array &$excelData, array $params): void
|
||||
{
|
||||
$cols = $params['columns'] ?? (isset($params['column']) ? [$params['column']] : []);
|
||||
$cols = routineEngineColsToIndexes($cols);
|
||||
$search = $params['search'] ?? '';
|
||||
$replace = $params['replace'] ?? '';
|
||||
$regex = $params['regex'] ?? false;
|
||||
|
||||
foreach ($excelData as &$row) {
|
||||
if (!isset($row['data']) || !is_array($row['data'])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($cols as $c) {
|
||||
$v = (string)($row['data'][$c] ?? '');
|
||||
if ($regex) {
|
||||
$out = @preg_replace($search, $replace, $v);
|
||||
$row['data'][$c] = $out === null ? $v : $out;
|
||||
} else {
|
||||
$row['data'][$c] = str_replace($search, $replace, $v);
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
}
|
||||
|
||||
/** uppercase: maiuscolo su una o più colonne. params: columns [] (o column) */
|
||||
function routineOpUppercase(array &$excelData, array $params): void
|
||||
{
|
||||
$cols = $params['columns'] ?? (isset($params['column']) ? [$params['column']] : []);
|
||||
$cols = routineEngineColsToIndexes($cols);
|
||||
foreach ($excelData as &$row) {
|
||||
if (!isset($row['data']) || !is_array($row['data'])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($cols as $c) {
|
||||
$row['data'][$c] = mb_strtoupper((string)($row['data'][$c] ?? ''), 'UTF-8');
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
}
|
||||
|
||||
/** lowercase: minuscolo su una o più colonne. params: columns [] (o column) */
|
||||
function routineOpLowercase(array &$excelData, array $params): void
|
||||
{
|
||||
$cols = $params['columns'] ?? (isset($params['column']) ? [$params['column']] : []);
|
||||
$cols = routineEngineColsToIndexes($cols);
|
||||
foreach ($excelData as &$row) {
|
||||
if (!isset($row['data']) || !is_array($row['data'])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($cols as $c) {
|
||||
$row['data'][$c] = mb_strtolower((string)($row['data'][$c] ?? ''), 'UTF-8');
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point richiamato da apply_routine.php.
|
||||
* Stessa firma delle routine legacy.
|
||||
*/
|
||||
function applyRoutine(&$excelData, $routineData = [])
|
||||
{
|
||||
$idroutine = isset($routineData['idroutine']) ? (int)$routineData['idroutine'] : 0;
|
||||
if ($idroutine <= 0) {
|
||||
throw new Exception("Motore routine: idroutine mancante in routineData.");
|
||||
}
|
||||
|
||||
$steps = routineEngineLoadSteps($idroutine);
|
||||
if (empty($steps)) {
|
||||
error_log("Motore routine: nessuno step attivo per routine {$idroutine}.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Dispatch table verbo -> handler
|
||||
$handlers = [
|
||||
'merge' => 'routineOpMerge',
|
||||
'copy' => 'routineOpCopy',
|
||||
'clear' => 'routineOpClear',
|
||||
'trim' => 'routineOpTrim',
|
||||
'replace' => 'routineOpReplace',
|
||||
'uppercase' => 'routineOpUppercase',
|
||||
'lowercase' => 'routineOpLowercase',
|
||||
];
|
||||
|
||||
foreach ($steps as $i => $step) {
|
||||
$op = $step['operation'];
|
||||
if (!isset($handlers[$op])) {
|
||||
error_log("Motore routine: operazione sconosciuta '{$op}' (routine {$idroutine}, step " . ($i + 1) . ") — saltata.");
|
||||
continue;
|
||||
}
|
||||
$handlers[$op]($excelData, $step['params']);
|
||||
error_log("Motore routine: applicata operazione '{$op}' (routine {$idroutine}, step " . ($i + 1) . ").");
|
||||
}
|
||||
|
||||
error_log("Motore routine {$idroutine} completato (" . count($steps) . " step).");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+77
-41
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* saveAll.js — Save All functionality using gridData
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
let saveAllRunning = false;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
}
|
||||
|
||||
// ── Save single row ──────────────────────────────────────────────────
|
||||
$(document).on('click', '.save-btn', async function() {
|
||||
$(document).on("click", ".save-btn", async function () {
|
||||
const btn = this;
|
||||
const rowIndex = parseInt(btn.dataset.row);
|
||||
const row = window.gridData?.[rowIndex];
|
||||
@@ -23,29 +23,44 @@
|
||||
|
||||
try {
|
||||
const formData = window.buildSavePayload(rowIndex);
|
||||
const resp = await fetch('save_edited_row.php', { method: 'POST', body: formData });
|
||||
const resp = await fetch("save_edited_row.php", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
const result = await resp.json();
|
||||
|
||||
if (result.success) {
|
||||
row._dirty = false;
|
||||
if (window.gridRenderer?.clearDirty) window.gridRenderer.clearDirty(rowIndex);
|
||||
if (window.gridRenderer?.clearDirty)
|
||||
window.gridRenderer.clearDirty(rowIndex);
|
||||
// Flash success on row without re-rendering (preserves Select2 state)
|
||||
const gridRow = document.querySelector(`.grid-row[data-id="${row.iddatadb}"]`);
|
||||
const gridRow = document.querySelector(
|
||||
`.grid-row[data-id="${row.iddatadb}"]`,
|
||||
);
|
||||
if (gridRow) {
|
||||
gridRow.classList.remove('row-dirty');
|
||||
gridRow.querySelectorAll('.grid-cell').forEach(cell => {
|
||||
cell.classList.remove('cell-changed');
|
||||
cell.classList.add('flash-success');
|
||||
gridRow.classList.remove("row-dirty");
|
||||
gridRow.querySelectorAll(".grid-cell").forEach((cell) => {
|
||||
cell.classList.remove("cell-changed");
|
||||
cell.classList.add("flash-success");
|
||||
});
|
||||
setTimeout(() => gridRow.querySelectorAll('.flash-success').forEach(c => c.classList.remove('flash-success')), 500);
|
||||
setTimeout(
|
||||
() =>
|
||||
gridRow
|
||||
.querySelectorAll(".flash-success")
|
||||
.forEach((c) =>
|
||||
c.classList.remove("flash-success"),
|
||||
),
|
||||
500,
|
||||
);
|
||||
}
|
||||
const toastEl = document.getElementById('saveSuccessToast');
|
||||
if (toastEl) bootstrap.Toast.getOrCreateInstance(toastEl).show();
|
||||
const toastEl = document.getElementById("saveSuccessToast");
|
||||
if (toastEl)
|
||||
bootstrap.Toast.getOrCreateInstance(toastEl).show();
|
||||
} else {
|
||||
alert('Errore: ' + result.message);
|
||||
alert("Errore: " + result.message);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Errore: ' + e.message);
|
||||
alert("Errore: " + e.message);
|
||||
} finally {
|
||||
btn.innerHTML = origHtml;
|
||||
btn.disabled = false;
|
||||
@@ -53,49 +68,68 @@
|
||||
});
|
||||
|
||||
// ── Save All ─────────────────────────────────────────────────────────
|
||||
$(document).on('click', '.save-all-btn', function(e) {
|
||||
$(document).on("click", ".save-all-btn", function (e) {
|
||||
e.preventDefault();
|
||||
if (isBusy()) return;
|
||||
const modalEl = document.getElementById('saveAllConfirmModal');
|
||||
const modalEl = document.getElementById("saveAllConfirmModal");
|
||||
if (!modalEl) return;
|
||||
new bootstrap.Modal(modalEl, { keyboard: false }).show();
|
||||
});
|
||||
|
||||
$(document).on('click', '#saveAllConfirmBtn', async function() {
|
||||
const confirmModal = bootstrap.Modal.getInstance(document.getElementById('saveAllConfirmModal'));
|
||||
$(document).on("click", "#saveAllConfirmBtn", async function () {
|
||||
const confirmModal = bootstrap.Modal.getInstance(
|
||||
document.getElementById("saveAllConfirmModal"),
|
||||
);
|
||||
if (confirmModal) confirmModal.hide();
|
||||
saveAllRunning = true;
|
||||
|
||||
const bar = document.getElementById('batchExportBar');
|
||||
const statusEl = document.getElementById('batchExportStatus');
|
||||
const cancelBtn = document.getElementById('exportBatchCancelBtn');
|
||||
if (bar) bar.style.display = '';
|
||||
if (cancelBtn) cancelBtn.style.display = 'none';
|
||||
if (statusEl) statusEl.textContent = 'Saving...';
|
||||
const bar = document.getElementById("batchExportBar");
|
||||
const statusEl = document.getElementById("batchExportStatus");
|
||||
const cancelBtn = document.getElementById("exportBatchCancelBtn");
|
||||
if (bar) bar.style.display = "";
|
||||
if (cancelBtn) cancelBtn.style.display = "none";
|
||||
if (statusEl) statusEl.textContent = "Saving...";
|
||||
|
||||
const data = window.gridData || [];
|
||||
const dirtyRows = data.map((r, i) => r._dirty ? i : -1).filter(i => i >= 0);
|
||||
const target =
|
||||
typeof window.getTargetGridIds === "function"
|
||||
? window.getTargetGridIds()
|
||||
: null;
|
||||
const dirtyRows = data
|
||||
.map((r, i) => (r._dirty ? i : -1))
|
||||
.filter((i) => i >= 0)
|
||||
.filter(
|
||||
(i) => !target || target.has(parseInt(data[i].iddatadb, 10)),
|
||||
);
|
||||
|
||||
if (dirtyRows.length === 0) {
|
||||
saveAllRunning = false;
|
||||
if (bar) bar.style.display = 'none';
|
||||
const msgEl = document.getElementById('saveAllResultMessage');
|
||||
if (msgEl) msgEl.textContent = 'No changes to save.';
|
||||
new bootstrap.Modal(document.getElementById('saveAllResultModal')).show();
|
||||
if (bar) bar.style.display = "none";
|
||||
const msgEl = document.getElementById("saveAllResultMessage");
|
||||
if (msgEl) msgEl.textContent = "No changes to save.";
|
||||
new bootstrap.Modal(
|
||||
document.getElementById("saveAllResultModal"),
|
||||
).show();
|
||||
return;
|
||||
}
|
||||
|
||||
let success = 0, fail = 0;
|
||||
let success = 0,
|
||||
fail = 0;
|
||||
|
||||
for (const idx of dirtyRows) {
|
||||
if (statusEl) statusEl.textContent = `Saving ${success + fail + 1} / ${dirtyRows.length}...`;
|
||||
if (statusEl)
|
||||
statusEl.textContent = `Saving ${success + fail + 1} / ${dirtyRows.length}...`;
|
||||
try {
|
||||
const formData = window.buildSavePayload(idx);
|
||||
const resp = await fetch('save_edited_row.php', { method: 'POST', body: formData });
|
||||
const resp = await fetch("save_edited_row.php", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (result.success) {
|
||||
data[idx]._dirty = false;
|
||||
if (window.gridRenderer?.clearDirty) window.gridRenderer.clearDirty(idx);
|
||||
if (window.gridRenderer?.clearDirty)
|
||||
window.gridRenderer.clearDirty(idx);
|
||||
success++;
|
||||
} else {
|
||||
fail++;
|
||||
@@ -106,22 +140,24 @@
|
||||
}
|
||||
|
||||
saveAllRunning = false;
|
||||
if (bar) bar.style.display = 'none';
|
||||
if (bar) bar.style.display = "none";
|
||||
|
||||
const gr = window.gridRenderer;
|
||||
if (gr) gr.renderVisibleRows();
|
||||
|
||||
const msg = `Saved: ${success}` + (fail > 0 ? `, Errors: ${fail}` : '');
|
||||
const msgEl = document.getElementById('saveAllResultMessage');
|
||||
const msg = `Saved: ${success}` + (fail > 0 ? `, Errors: ${fail}` : "");
|
||||
const msgEl = document.getElementById("saveAllResultMessage");
|
||||
if (msgEl) msgEl.textContent = msg;
|
||||
new bootstrap.Modal(document.getElementById('saveAllResultModal')).show();
|
||||
new bootstrap.Modal(
|
||||
document.getElementById("saveAllResultModal"),
|
||||
).show();
|
||||
});
|
||||
|
||||
// ── beforeunload ─────────────────────────────────────────────────────
|
||||
window.addEventListener('beforeunload', function(e) {
|
||||
if (window.gridData && window.gridData.some(r => r._dirty)) {
|
||||
window.addEventListener("beforeunload", function (e) {
|
||||
if (window.gridData && window.gridData.some((r) => r._dirty)) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
e.returnValue = "";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
require_once __DIR__ . '/class/binding-functions.php';
|
||||
include dirname(__DIR__) . '/../extra/auth.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
if (!Auth::check()) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$kind = ($_POST['kind'] ?? 'custom') === 'fixed' ? 'fixed' : 'custom';
|
||||
$templateId = intval($_POST['template_id'] ?? 0);
|
||||
$mappingId = intval($_POST['mapping_id'] ?? 0);
|
||||
$fieldId = intval($_POST['field_id'] ?? 0);
|
||||
$sourceValue = (string) ($_POST['source_value'] ?? '');
|
||||
$limsValueId = intval($_POST['lims_value_id'] ?? 0);
|
||||
$limsValue = (string) ($_POST['lims_value'] ?? '');
|
||||
|
||||
$datadbIds = [];
|
||||
if (isset($_POST['datadb_ids'])) {
|
||||
$decoded = json_decode($_POST['datadb_ids'], true);
|
||||
if (is_array($decoded)) {
|
||||
$datadbIds = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
if ($templateId <= 0 || $mappingId <= 0 || trim($sourceValue) === '' || $limsValueId <= 0) {
|
||||
http_response_code(422);
|
||||
echo json_encode(['success' => false, 'error' => 'Missing required parameters']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
$createdBy = Auth::user() ? (int) Auth::user()->present()->id : null;
|
||||
|
||||
if ($kind === 'fixed') {
|
||||
$stmt = $pdo->prepare("SELECT fixed_field_key FROM template_fixed_mapping WHERE id = ?");
|
||||
$stmt->execute([$mappingId]);
|
||||
$fixedKey = (string) ($stmt->fetchColumn() ?: '');
|
||||
|
||||
if ($fixedKey === '' || !binding_fixed_is_list($fixedKey)) {
|
||||
http_response_code(422);
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid fixed field']);
|
||||
exit;
|
||||
}
|
||||
|
||||
binding_upsert($pdo, $templateId, $mappingId, 0, $sourceValue, $limsValueId, $limsValue, $createdBy, 'fixed');
|
||||
|
||||
$applied = !empty($datadbIds)
|
||||
? binding_apply_to_datadb($pdo, $fixedKey, (string) $limsValueId, $datadbIds)
|
||||
: 0;
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'applied_rows' => $applied,
|
||||
'kind' => 'fixed',
|
||||
'source_value' => $sourceValue,
|
||||
'lims_value' => $limsValue,
|
||||
'lims_value_id' => $limsValueId,
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($fieldId <= 0) {
|
||||
$stmt = $pdo->prepare("SELECT field_id FROM template_mapping WHERE id = ?");
|
||||
$stmt->execute([$mappingId]);
|
||||
$fieldId = (int) ($stmt->fetchColumn() ?: 0);
|
||||
}
|
||||
|
||||
binding_upsert($pdo, $templateId, $mappingId, $fieldId, $sourceValue, $limsValueId, $limsValue, $createdBy);
|
||||
|
||||
$applied = !empty($datadbIds)
|
||||
? binding_apply_to_details($pdo, $mappingId, (string) $limsValueId, $datadbIds)
|
||||
: 0;
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'applied_rows' => $applied,
|
||||
'kind' => 'custom',
|
||||
'source_value' => $sourceValue,
|
||||
'lims_value' => $limsValue,
|
||||
'lims_value_id' => $limsValueId,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -22,6 +22,8 @@ try {
|
||||
$analysisLevel = isset($_POST['analysis_level']) && $_POST['analysis_level'] !== '' ? (int)$_POST['analysis_level'] : null;
|
||||
$isWebSelectable = isset($_POST['is_web_selectable']) ? (int)$_POST['is_web_selectable'] : 0;
|
||||
$isAccredited = isset($_POST['is_accredited']) ? (int)$_POST['is_accredited'] : 0;
|
||||
$analysisPriceRaw = str_replace(',', '.', trim((string)($_POST['analysis_price'] ?? '')));
|
||||
$analysisPrice = is_numeric($analysisPriceRaw) ? round((float)$analysisPriceRaw, 2) : null;
|
||||
|
||||
if ($partId <= 0 || $analysisRecordkey === '') {
|
||||
http_response_code(400);
|
||||
@@ -42,7 +44,8 @@ try {
|
||||
analysis_method,
|
||||
analysis_level,
|
||||
is_web_selectable,
|
||||
is_accredited
|
||||
is_accredited,
|
||||
analysis_price
|
||||
) VALUES (
|
||||
:part_id,
|
||||
:iddatadb,
|
||||
@@ -52,17 +55,9 @@ try {
|
||||
:analysis_method,
|
||||
:analysis_level,
|
||||
:is_web_selectable,
|
||||
:is_accredited
|
||||
:is_accredited,
|
||||
:analysis_price
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
analysis_name = VALUES(analysis_name),
|
||||
analysis_method = VALUES(analysis_method),
|
||||
analysis_level = VALUES(analysis_level),
|
||||
is_web_selectable = VALUES(is_web_selectable),
|
||||
is_accredited = VALUES(is_accredited),
|
||||
iddatadb = VALUES(iddatadb),
|
||||
idmatrice = VALUES(idmatrice),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
@@ -75,11 +70,13 @@ try {
|
||||
':analysis_level' => $analysisLevel,
|
||||
':is_web_selectable' => $isWebSelectable,
|
||||
':is_accredited' => $isAccredited,
|
||||
':analysis_price' => $analysisPrice,
|
||||
]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Association saved'
|
||||
'message' => 'Association saved',
|
||||
'id' => (int)$pdo->lastInsertId()
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
|
||||
@@ -4,6 +4,7 @@ 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);
|
||||
|
||||
@@ -15,46 +16,73 @@ if (!$idquotations || empty($parts)) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$part = $parts[0];
|
||||
$partId = $part['id'] ?? null;
|
||||
$partNumber = $part['part_number'] ?? null;
|
||||
$partDescription = $part['part_description'] ?? '';
|
||||
$mix = $part['mix'] ?? 'N';
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
$results = [];
|
||||
|
||||
foreach ($parts as $part) {
|
||||
$partId = $part['id'] ?? null;
|
||||
$partNumber = $part['part_number'] ?? null;
|
||||
$partDescription = $part['part_description'] ?? '';
|
||||
$mix = $part['mix'] ?? 'N';
|
||||
$idmatrice = $part['idmatrice'] ?? null;
|
||||
$note = $part['note'] ?? null;
|
||||
$dateexpiry = $part['dateexpiry'] ?? null;
|
||||
|
||||
if ($partDescription) {
|
||||
try {
|
||||
if ($partId) {
|
||||
// UPDATE se esiste già la parte
|
||||
// UPDATE se la parte esiste
|
||||
$stmt = $pdo->prepare("UPDATE identification_parts
|
||||
SET part_number = :part_number,
|
||||
part_description = :part_description,
|
||||
mix = :mix,
|
||||
idmatrice = :idmatrice,
|
||||
note = :note,
|
||||
dateexpiry = :dateexpiry,
|
||||
updated_at = NOW()
|
||||
WHERE id = :id");
|
||||
WHERE id = :id AND idquotations = :idquotations");
|
||||
$stmt->execute([
|
||||
':id' => $partId,
|
||||
':part_number' => $partNumber,
|
||||
':part_description' => $partDescription,
|
||||
':mix' => $mix
|
||||
':mix' => $mix,
|
||||
':idmatrice' => $idmatrice,
|
||||
':note' => $note,
|
||||
':dateexpiry' => $dateexpiry,
|
||||
':idquotations' => $idquotations,
|
||||
]);
|
||||
echo json_encode(['success' => true, 'part_id' => $partId, 'part_number' => $partNumber, 'message' => 'Parte aggiornata con successo']);
|
||||
} else {
|
||||
// INSERT se è nuova
|
||||
|
||||
$results[] = [
|
||||
'part_id' => $partId,
|
||||
'part_number' => $partNumber,
|
||||
'message' => 'Parte aggiornata con successo'
|
||||
];
|
||||
} else if ($partDescription || $note || $dateexpiry) {
|
||||
// INSERT per nuova parte (solo se ha contenuto)
|
||||
$stmt = $pdo->prepare("INSERT INTO identification_parts
|
||||
(idquotations, part_number, part_description, mix, created_at, updated_at)
|
||||
VALUES (:idquotations, :part_number, :part_description, :mix, NOW(), NOW())");
|
||||
(idquotations, part_number, part_description, mix, idmatrice, note, dateexpiry, created_at, updated_at)
|
||||
VALUES (:idquotations, :part_number, :part_description, :mix, :idmatrice, :note, :dateexpiry, NOW(), NOW())");
|
||||
$stmt->execute([
|
||||
':idquotations' => $idquotations,
|
||||
':part_number' => $partNumber,
|
||||
':part_description' => $partDescription,
|
||||
':mix' => $mix
|
||||
':mix' => $mix,
|
||||
':idmatrice' => $idmatrice,
|
||||
':note' => $note,
|
||||
':dateexpiry' => $dateexpiry,
|
||||
]);
|
||||
$newId = $pdo->lastInsertId();
|
||||
echo json_encode(['success' => true, 'part_id' => $newId, 'part_number' => $partNumber, 'message' => 'Parte salvata con successo']);
|
||||
$newId = (int)$pdo->lastInsertId();
|
||||
|
||||
$results[] = [
|
||||
'part_id' => $newId,
|
||||
'part_number' => $partNumber,
|
||||
'message' => 'Parte salvata con successo'
|
||||
];
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore nel salvataggio: ' . $e->getMessage()]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => 'Descrizione mancante']);
|
||||
|
||||
$pdo->commit();
|
||||
echo json_encode(['success' => true, 'results' => $results]);
|
||||
} catch (PDOException $e) {
|
||||
$pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'message' => 'Errore nel salvataggio: ' . $e->getMessage()]);
|
||||
}
|
||||
|
||||
@@ -1,905 +0,0 @@
|
||||
{
|
||||
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#SchemaCustomField",
|
||||
"value": [
|
||||
{
|
||||
"IdSchemaCustomFields": 41,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Labostudio",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 42,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Schema 1",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 43,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Schema1",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 44,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Cuoio\/Pelle Standard \/ Leather",
|
||||
"Descrizione": "Schema per tutti i campioni in cuoio\/pelle per i quali non \u00e8 specificato un destinatario.\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 45,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Borse",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 46,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Borse Burberry",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 47,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "pelle calzatura",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 48,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Standard \/ Generico",
|
||||
"Descrizione": "\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 49,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Accessori Metallici \/ Metallic Accessories",
|
||||
"Descrizione": "Schema per tutti gli Accessori Metallici\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 50,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Tessile \/ Textiles",
|
||||
"Descrizione": "Schema per tutti i Tessuti\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 52,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Brasport",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 54,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Chanel Calzatura",
|
||||
"Descrizione": "\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 55,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Burberry",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 57,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Z_Ralph Lauren - Tessile",
|
||||
"Descrizione": "Schema per tutti i campioni in tessuto per Ralph Lauren.\r\n\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 58,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "LIU JO",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 59,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Migrazione tra Componenti",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 60,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Prova Outsourcing",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 61,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Marc Jacobs",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 62,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "A. TESTONI",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 63,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Chanel CHINA PROJECT",
|
||||
"Descrizione": "\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 66,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "GUESS",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 68,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Valentino",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 69,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "DESIGUAL",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 70,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "FENDI PELLETTERIA",
|
||||
"Descrizione": "Solo per FENDI Pelletteria_INGLESE\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 71,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Z_Cuoio\/Pelle Standard ACCREDIA",
|
||||
"Descrizione": "Schema per tutti i campioni in cuoio\/pelle per i quali non \u00e8 specificato un destinatario.\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 72,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Z_BALLY",
|
||||
"Descrizione": "sia per calzatura che pelletteria. Aggiunto campo TEST DESCRIPTION, compreso nella fatturazione per le divisioni pelletteria.\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 73,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Z_Ralph Lauren - Leather",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 74,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Z_Ralph Lauren - Fabric",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 76,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "NORDSTROM",
|
||||
"Descrizione": "\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 77,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "ONWARD LUXURY GROUP",
|
||||
"Descrizione": "\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 78,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "P&G SRL_Progetto Durability",
|
||||
"Descrizione": "\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 79,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Bally qualsiasi divisione",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 80,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "FENDI CALZATURA",
|
||||
"Descrizione": "\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 81,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "LouBoutin",
|
||||
"Descrizione": "\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 82,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "MONCLER Brand",
|
||||
"Descrizione": "Da utilizzare solo per il Brand Diretto\r\nGR 19\/03\/2024"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 83,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "JIMMY CHOO",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 84,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Yves Saint Laurent",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 85,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "BENETTON",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 86,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "UPLOAD REPORT",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 87,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Hermes Calzatura",
|
||||
"Descrizione": "\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 88,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "C&A",
|
||||
"Descrizione": "Schema per fornitori e C&A diretto\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 89,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "AMERICAN EAGLE",
|
||||
"Descrizione": "Schema per American Eagle\/Todd snyder\r\n\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 91,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Ferragamo",
|
||||
"Descrizione": "\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 93,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Z_Alexander McQueen",
|
||||
"Descrizione": "\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 95,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Balenciaga Pelletteria",
|
||||
"Descrizione": "\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 96,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "DUNHILL",
|
||||
"Descrizione": "SCHEMA DUNHILL tutti i tipi di campioni\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 97,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "PRELIEVI ACQUE",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 98,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "LouBoutin_Tossicologico",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 99,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "ROBAN'S ",
|
||||
"Descrizione": "tutte le divisioni\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 100,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "PROGETTO LUGGAGE",
|
||||
"Descrizione": "Progetto Luggage Hermes\r\n\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 102,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Balmain",
|
||||
"Descrizione": "Schema per tutti i campioni Balmain"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 103,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "ZDHC",
|
||||
"Descrizione": "Schema di Riferimento ZDHC Wastewaters"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 104,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "GUCCI CDC 4172",
|
||||
"Descrizione": "\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 105,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Z_ASOS",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 106,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Z_BOUX AVENUE",
|
||||
"Descrizione": "28\/09 no indicazioni su esigenze di fatturazione (detto da Frosini)\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 107,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "BONPOINT",
|
||||
"Descrizione": "05\/10 creazione\r\n\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 108,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Canada Goose",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 109,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "DIESEL",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 110,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "AQC_Leather",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 111,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "BEAUMANOIR (CC-Mor-Bon-Breal)",
|
||||
"Descrizione": "04\/02\/21_ AGGIORNATO 05\/08\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 112,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "BooHoo",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 114,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Golden Goose",
|
||||
"Descrizione": "Golden goose\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 115,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Z_ZLABEL",
|
||||
"Descrizione": "Schema per ZLABEL"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 116,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Zadig & Voltaire",
|
||||
"Descrizione": "Schema per Zadig & Voltaire"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 117,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Gemo",
|
||||
"Descrizione": "Schema solo per Gemo\r\n\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 118,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "PROMOD",
|
||||
"Descrizione": "CHF 06\/08\r\n\r\n\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 119,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "LA HALLE (BEAUMANOIR)",
|
||||
"Descrizione": "13\/08\/2021 CHF\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 120,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "LA REDOUTE",
|
||||
"Descrizione": "14\/09\/2021 AD\r\n\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 121,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "TOD'S",
|
||||
"Descrizione": "Schema per materiale gb18401 e GB20400\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 122,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "ASOS rev 1",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 123,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Monoprix",
|
||||
"Descrizione": "Schema per Monoprix\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 124,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "DIOR Ita",
|
||||
"Descrizione": "CHF 21\/03\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 125,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "GEOX - Raw Material",
|
||||
"Descrizione": "SCHEMA DA USARE SOLO PER GEOX - SOLO PER LE SUOLE E MATERIALI DENOMINATI RPU \/ RAW MATERIAL"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 126,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "CAMAIEU",
|
||||
"Descrizione": "SCHEMA DA USARE SOLO PER CAMAIEU\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 127,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Z_Golden Goose FTW",
|
||||
"Descrizione": "Golden goose\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 128,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "STEVE MADDEN - FOOTWEAR",
|
||||
"Descrizione": "DA UTILIZZARE SOLO PER DESTINAZIONE D'USO CALZATURA\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 129,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Z_Carhartt",
|
||||
"Descrizione": "SCHEMA DA UTILIZZARE SOLO PER RICHIESTE UFFICIALI CARHARTT\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 131,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "DAMART",
|
||||
"Descrizione": "\r\n\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 132,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Gruppo OTB",
|
||||
"Descrizione": "Schema per Staff International, Diesel e Brave kid\r\n\r\n\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 133,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "FERRARI",
|
||||
"Descrizione": "Schema da usare per Ferrari e fornitori\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 134,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "GAP - ABBIGLIAMENTO",
|
||||
"Descrizione": "Schema GAP da utilizzare solo per destinazione abbigliamento\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 135,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "GAP - CALZATURA",
|
||||
"Descrizione": "Schema GAP da utilizzare solo per destinazione calzatura\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 136,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "SALLING GROUP",
|
||||
"Descrizione": "Schema per Salling Group\r\n\r\n\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 137,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "SOFT SURROUNDINGS",
|
||||
"Descrizione": "Schema da utilizzare per Soft Surroundings\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 138,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "E-LIMS - Cuoio\/Pelle Standard \/ Leather",
|
||||
"Descrizione": "Schema da utilizzare solo per accettazioni online\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 139,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Z_ZALANDO",
|
||||
"Descrizione": "Schema per ZALANDO ------ (NON per ZLABEL che ha il suo)\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 140,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "LVMH",
|
||||
"Descrizione": "gennaio 2023\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 141,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Valentino - eLims",
|
||||
"Descrizione": "SCHEMA DA UTILIZZARE SOLO PER L'ACCETTAZIONE ONLINE DI VALENTINO"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 142,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "FENDI PELLETTERIA COMPLIANCE",
|
||||
"Descrizione": "Solo per FENDI Pelletteria_INGLESE 04\/05\/2023\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 143,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "TAPESTRY",
|
||||
"Descrizione": "04\/05\/2023, agg secondo procedura 08\/11\/23\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 144,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "ALEXANDER MC QUEEN",
|
||||
"Descrizione": "ALEXANDER MC QUEEN 15\/05\/2023\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 145,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Hermes_Chimici_Parigi",
|
||||
"Descrizione": "\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 146,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "CELINE ABBIGLIAMENTO_Progetto Speciale",
|
||||
"Descrizione": "IO 202 CELINE ABBIGLIAMENTO_06\/06\/203_CHF\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 147,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "GEOX - Prodotto Finito",
|
||||
"Descrizione": "SCHEMA DA USARE SOLO PER GEOX - SOLO PER PRODOTTO FINITO"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 149,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Hermes\/IDO",
|
||||
"Descrizione": "creato il 05\/07\/2023 CHF\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 150,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Tessile \/ Textiles PER CLIENTI CON OBLO",
|
||||
"Descrizione": "Schema per tutti i Tessuti QUANDO IL CLIENTE NECESSITA DI NR OBLO\r\n\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 151,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "J JIll",
|
||||
"Descrizione": "Schema per cliente J JIll, necessario riportare la recommended fiber content in schema verde\r\n\r\n\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 152,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "AUCHAN - TAPE A L'OEIL",
|
||||
"Descrizione": "Schema AUCHAN da utilizzare solo per la divisione TAPE A L'OEIL SA, successivamente se uguale da usare anche per le altre divisioni ed uniformarlo come generico"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 153,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "ALL SAINTS - BUSCEMI",
|
||||
"Descrizione": "SCHE DA USARE SOLO PER ALL SAINTS E BUSCEMI\r\n\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 154,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "CELINE - LVMH",
|
||||
"Descrizione": "Schema con aggiunta campi LVMH per estrazioni\r\nAD-29\/03\/2024"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 155,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "SeyMeChamLou",
|
||||
"Descrizione": "DA USARE SOLO PER SEYMECHAMLOU (LOUBOUTIN)\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 156,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "PRADA",
|
||||
"Descrizione": "PER ADESSO DA USARE PER ABBIGLIAMENTO E CALZATURA\r\n\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 157,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Hermes_HCP",
|
||||
"Descrizione": "Schema per unicamente HCP"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 158,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "STONE ISLAND",
|
||||
"Descrizione": "Schema per tutti i campioni in cuoio\/pelle per i quali non \u00e8 specificato un destinatario.\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 159,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Chanel_Pierre Damien",
|
||||
"Descrizione": "Schema da usare per Campioni ricevuti da Pierre-Damien Verine e suo staff CHANEL.\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 160,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Z_ACNE",
|
||||
"Descrizione": "\r\n.\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 161,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "OBERALP GROUP",
|
||||
"Descrizione": "Schema da usare solo per Oberalp Group\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 162,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Club Monaco",
|
||||
"Descrizione": "da usare solo per Club Monaco GR 20\/02\/2024"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 163,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Devred",
|
||||
"Descrizione": "Schema creato per cliente DEVRED\r\nGR 18\/03\/2024 aggiornato il 23\/04\/2026\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 164,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "MONCLER Supplier",
|
||||
"Descrizione": "Da utilizzare per fornitori Moncler \r\nGR 19\/03\/2024"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 165,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Kering Sunglasses",
|
||||
"Descrizione": "Schema per tutti i campioni di qualsiasi matrice escluso cuoio\/pelle\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 166,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Giorgio Armani",
|
||||
"Descrizione": "\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 167,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Chanel USA",
|
||||
"Descrizione": "\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 168,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Staff Calzatura",
|
||||
"Descrizione": "Schema per tutti i campioni di qualsiasi matrice escluso cuoio\/pelle\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 169,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "P&G SRL",
|
||||
"Descrizione": "\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 170,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "INCOM",
|
||||
"Descrizione": "Schema da utilizzare per INCOM"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 171,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "RIVER ISLAND",
|
||||
"Descrizione": "SCHEDA DA USARE SOLO PER RIVER ISLAND - NO CALZATURA\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 172,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Longchamp",
|
||||
"Descrizione": "Schema per tutti i campioni con RIchiesta Longchamp ufficiale\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 173,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Bottega Veneta",
|
||||
"Descrizione": "Schema per tutti i campioni di Bottega Veneta di Stella"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 174,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Mint Velvet",
|
||||
"Descrizione": "Schema per tutti i campioni di Mint Velvel con richiesta ufficiale.\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 176,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "RIVER ISLAND FOOTWEAR",
|
||||
"Descrizione": "SCHEDA DA USARE SOLO PER RIVER ISLAND - USARE SOLO PER\r\n CALZATURA\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 177,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Phoebe philo ACC",
|
||||
"Descrizione": "(scarpe, borse, cinture, occhiali, gioielleria)\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 178,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "New Guards Group",
|
||||
"Descrizione": "\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 179,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Giorgio Armani Operation",
|
||||
"Descrizione": "\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 180,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Chloe'",
|
||||
"Descrizione": "\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 181,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "LVMH non diretti",
|
||||
"Descrizione": "utilizzato ogni qualvolta siano caricati pacchetti \/ prove singole che non rientrano nel programma LVMH \r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 182,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Ralph Lauren - All testing V.10",
|
||||
"Descrizione": "AGGIORNAMENTO AL 16\/03\/2026 per estrazione fatturazione"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 183,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "TWINSET",
|
||||
"Descrizione": "Schema da usare per Twinset - come da mail di EP del 20\/05\/2025\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 184,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "ACNE STUDIOS rev 01",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 185,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Brunello cucinelli",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 186,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "TWINSET",
|
||||
"Descrizione": "Schema per tutti i campioni di qualsiasi matrice escluso cuoio\/pelle\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 187,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "LORO PIANA",
|
||||
"Descrizione": "Schema per tutti i campioni Loro Piana\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 188,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "LEMAIRE",
|
||||
"Descrizione": "Schema per tutti i campioni in cuoio\/pelle per i quali non \u00e8 specificato un destinatario.\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 189,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "TEST_IT_SCHEMA",
|
||||
"Descrizione": "Test per IT"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 190,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "LBS Interscambio",
|
||||
"Descrizione": "Schema per tutti i campioni interscambio LBS\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 191,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "KIABI",
|
||||
"Descrizione": "Schema per il cliente Kiabi\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 192,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "INDITEX",
|
||||
"Descrizione": "Schema da usare solo per richieste INDITEX"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 193,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "AQC_Other materials",
|
||||
"Descrizione": null
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 194,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Hermes ADM & Silk",
|
||||
"Descrizione": "SCHEMA DA USARE SOLO PER LA DIVISIONE ADM & SILK\r\n\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 195,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Chloe'_APPAREL",
|
||||
"Descrizione": "Schema aggiornato il 18\/12\/25 secondo modulo excel x codifica Elims\r\nGR"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 196,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "DUNHILL_DRAFT NON USARE",
|
||||
"Descrizione": "SCHEMA DUNHILL NON USARE STUDIO NUOVA PROCEDURA NOVEMBRE 2025\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 197,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "BERLUTI - LVMH",
|
||||
"Descrizione": "Creato per accettazione Elims\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 198,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "PIAGET (gruppo Cartier)",
|
||||
"Descrizione": "Schema per tutti i campioni PIAGET (gruppo Cartier)\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 199,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "HERMES PAP FEMME",
|
||||
"Descrizione": "Schema per tutti i campioni di HERMES PAP FEMME \r\n15\/01\/2025\r\n\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 200,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "CARHARTT ( provvisiorio in revisione)",
|
||||
"Descrizione": "Schema per tutti i campioni CARHARTT provvissorio\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 201,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "ROSSIMODA",
|
||||
"Descrizione": "Per tutti i campioni di ROSSIMODA\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 202,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "LIMS-CIM - MAX MARA",
|
||||
"Descrizione": "Schema per MAX MARA scambio dati Database"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 203,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Vince",
|
||||
"Descrizione": "Schema per tutti i campioni di VINCE\r\n\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 204,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Max Mara",
|
||||
"Descrizione": "Schema da usare per Max Mara\r\n"
|
||||
},
|
||||
{
|
||||
"IdSchemaCustomFields": 205,
|
||||
"ConteggioClienti": 0,
|
||||
"Nome": "Chanel Flammability",
|
||||
"Descrizione": "Schema per Chanel Flammability\r\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -19,6 +19,14 @@ $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'] ?? '');
|
||||
@@ -36,20 +44,20 @@ function formatClientLabel(array $client): string
|
||||
$suffix = '--';
|
||||
}
|
||||
|
||||
return $name . ' - ' . $suffix . ' (ID: ' . $id . ')';
|
||||
return $name . ' - ' . $suffix . ' - ' . $id;
|
||||
}
|
||||
|
||||
try {
|
||||
// Load from cache or API
|
||||
$cacheFile = __DIR__ . '/cache/clienti.json';
|
||||
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 7200)) {
|
||||
$data = json_decode(file_get_contents($cacheFile), true);
|
||||
} else {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
$params = [
|
||||
'$select' => 'IdCliente,Nominativo,CodiceCliente',
|
||||
'$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo',
|
||||
'$orderby' => 'Nominativo asc'
|
||||
];
|
||||
|
||||
@@ -64,6 +72,14 @@ try {
|
||||
|
||||
$clients = $data['value'] ?? [];
|
||||
|
||||
// Filtro tipo, applicato SOLO alla ricerca, non al lookup per id
|
||||
$searchClients = $clients;
|
||||
if ($tipo === 'attivo') {
|
||||
$searchClients = array_values(array_filter($clients, fn($c) => !isInattivo($c)));
|
||||
} elseif ($tipo === 'inattivo') {
|
||||
$searchClients = array_values(array_filter($clients, fn($c) => isInattivo($c)));
|
||||
}
|
||||
|
||||
// If requesting by specific ID, used for loading selected value
|
||||
if ($id !== null) {
|
||||
foreach ($clients as $c) {
|
||||
@@ -88,18 +104,20 @@ try {
|
||||
// Search by query
|
||||
$results = [];
|
||||
|
||||
foreach ($clients as $c) {
|
||||
foreach ($searchClients as $c) {
|
||||
$name = trim($c['Nominativo'] ?? '');
|
||||
$code = trim($c['CodiceCliente'] ?? '');
|
||||
|
||||
// Cerca solo su ciò che viene visualizzato: Nome - Lettera - ID
|
||||
$label = formatClientLabel($c);
|
||||
|
||||
if (
|
||||
$q === '' ||
|
||||
mb_strpos(mb_strtolower($name), $q) !== false ||
|
||||
mb_strpos(mb_strtolower($code), $q) !== false
|
||||
mb_strpos(mb_strtolower($label), $q) !== false
|
||||
) {
|
||||
$results[] = [
|
||||
'id' => $c['IdCliente'],
|
||||
'text' => formatClientLabel($c),
|
||||
'text' => $label,
|
||||
'IdCliente' => $c['IdCliente'],
|
||||
'Nominativo' => $name,
|
||||
'CodiceCliente' => $code
|
||||
|
||||
@@ -29,7 +29,7 @@ try {
|
||||
$cacheDir = __DIR__ . '/cache';
|
||||
$cacheFile = $cacheDir . '/customfield_' . $fieldId . '.json';
|
||||
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 7200)) {
|
||||
$values = json_decode(file_get_contents($cacheFile), true);
|
||||
} else {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
@@ -51,18 +51,74 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Search by query
|
||||
$results = [];
|
||||
foreach ($values as $v) {
|
||||
$text = $v['Valore'] ?? '';
|
||||
if ($q === '' || mb_strpos(mb_strtolower($text), $q) !== false) {
|
||||
$results[] = ['id' => $v['IdCustomFieldsValue'], 'text' => $text];
|
||||
if ($limit > 0 && count($results) >= $limit) break;
|
||||
// Search — multi-term AND, position-independent, ranked
|
||||
$terms = array_values(array_filter(
|
||||
preg_split('/\s+/u', $q, -1, PREG_SPLIT_NO_EMPTY)
|
||||
));
|
||||
|
||||
// Nessun termine digitato: alfabetico, taglio al limite
|
||||
if (empty($terms)) {
|
||||
$all = [];
|
||||
foreach ($values as $v) {
|
||||
$all[] = ['id' => $v['IdCustomFieldsValue'], 'text' => $v['Valore'] ?? ''];
|
||||
}
|
||||
usort($all, fn($a, $b) => strcasecmp($a['text'], $b['text']));
|
||||
if ($limit > 0) $all = array_slice($all, 0, $limit);
|
||||
echo json_encode(['results' => $all]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Sort alphabetically
|
||||
usort($results, fn($a, $b) => strcasecmp($a['text'], $b['text']));
|
||||
$firstTerm = $terms[0];
|
||||
$scored = [];
|
||||
|
||||
foreach ($values as $v) {
|
||||
$text = $v['Valore'] ?? '';
|
||||
$textLower = mb_strtolower($text);
|
||||
|
||||
// AND: tutti i termini devono essere presenti, in qualsiasi posizione
|
||||
$matchAll = true;
|
||||
foreach ($terms as $t) {
|
||||
if (mb_strpos($textLower, $t) === false) {
|
||||
$matchAll = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$matchAll) continue;
|
||||
|
||||
// Punteggio di pertinenza (più basso = più rilevante)
|
||||
if ($textLower === $q) {
|
||||
$score = 0; // match esatto
|
||||
} elseif (mb_strpos($textLower, $firstTerm) === 0) {
|
||||
$score = 1; // inizia col primo termine
|
||||
} elseif (preg_match('/(^|\s)' . preg_quote($firstTerm, '/') . '/u', $textLower)) {
|
||||
$score = 2; // primo termine a inizio di una parola
|
||||
} else {
|
||||
$score = 3; // presente ma non a inizio parola
|
||||
}
|
||||
|
||||
$scored[] = [
|
||||
'id' => $v['IdCustomFieldsValue'],
|
||||
'text' => $text,
|
||||
'_s' => $score,
|
||||
'_n' => $textLower
|
||||
];
|
||||
}
|
||||
|
||||
// Ordina per punteggio, poi alfabetico
|
||||
usort($scored, function ($a, $b) {
|
||||
if ($a['_s'] !== $b['_s']) return $a['_s'] - $b['_s'];
|
||||
return strcmp($a['_n'], $b['_n']);
|
||||
});
|
||||
|
||||
// Taglia DOPO l'ordinamento e rimuovi i campi interni
|
||||
if ($limit > 0) {
|
||||
$scored = array_slice($scored, 0, $limit);
|
||||
}
|
||||
|
||||
$results = array_map(
|
||||
fn($r) => ['id' => $r['id'], 'text' => $r['text']],
|
||||
$scored
|
||||
);
|
||||
|
||||
echo json_encode(['results' => $results]);
|
||||
} catch (Exception $e) {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
require_once __DIR__ . '/class/binding-functions.php';
|
||||
include dirname(__DIR__) . '/../extra/auth.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
if (!Auth::check()) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$fieldKey = trim($_GET['field_key'] ?? '');
|
||||
$templateId = (int) ($_GET['template_id'] ?? 0);
|
||||
$q = binding_normalize_value($_GET['q'] ?? '');
|
||||
$rawLimit = (int) ($_GET['limit'] ?? 50);
|
||||
$limit = $rawLimit <= 0 ? 0 : max(1, min(500, $rawLimit));
|
||||
|
||||
if ($fieldKey === '' || !binding_fixed_is_list($fieldKey)) {
|
||||
echo json_encode(['results' => []]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
$values = binding_get_fixed_values($pdo, $fieldKey, $templateId);
|
||||
|
||||
if ($q !== '') {
|
||||
$terms = array_values(array_filter(preg_split('/\s+/u', $q, -1, PREG_SPLIT_NO_EMPTY)));
|
||||
$values = array_values(array_filter($values, function ($v) use ($terms) {
|
||||
$hay = binding_normalize_value($v['text'] ?? '');
|
||||
foreach ($terms as $t) {
|
||||
if (mb_strpos($hay, $t) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
if ($limit > 0) {
|
||||
$values = array_slice($values, 0, $limit);
|
||||
}
|
||||
|
||||
echo json_encode(['results' => $values]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage(), 'results' => []]);
|
||||
}
|
||||
@@ -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');
|
||||
@@ -58,16 +62,71 @@ if (isset($_GET['macro_list'])) {
|
||||
echo json_encode(['success' => true, 'value' => $macros]);
|
||||
exit;
|
||||
}
|
||||
// Return full matrix list (same shape as get_matrici_db.php)
|
||||
if (isset($_GET['full_list'])) {
|
||||
echo json_encode(['success' => true, 'value' => $matrici]);
|
||||
exit;
|
||||
}
|
||||
// Search (with optional MacroMatrice filter) - multi-term AND, position-independent, ranked
|
||||
$terms = array_values(array_filter(
|
||||
preg_split('/\s+/u', $q, -1, PREG_SPLIT_NO_EMPTY)
|
||||
));
|
||||
|
||||
// Nessun termine digitato: comportamento invariato (alfabetico, taglio al limite)
|
||||
if (empty($terms)) {
|
||||
$results = [];
|
||||
foreach ($matrici as $m) {
|
||||
if ($macro !== '' && ($m['MacroMatrice'] ?? '') !== $macro) continue;
|
||||
$results[] = ['id' => $m['IdMatrice'], 'text' => $m['NomeMatrice'] ?? ''];
|
||||
if (count($results) >= $limit) break;
|
||||
}
|
||||
echo json_encode(['results' => $results]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$firstTerm = $terms[0];
|
||||
$scored = [];
|
||||
|
||||
// Search (with optional MacroMatrice filter)
|
||||
$results = [];
|
||||
foreach ($matrici as $m) {
|
||||
$nome = $m['NomeMatrice'] ?? '';
|
||||
if ($macro !== '' && ($m['MacroMatrice'] ?? '') !== $macro) continue;
|
||||
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]);
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
require_once __DIR__ . '/class/Mailer.class.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
/**
|
||||
* Risolve [IdCliente => Nominativo] leggendo la stessa cache di search_clienti.php.
|
||||
* Non chiama l'API: usa cache/clienti.json (già mantenuto aggiornato dalla ricerca clienti).
|
||||
*/
|
||||
function resolveClientNames(array $ids): array
|
||||
{
|
||||
$ids = array_values(array_filter(array_map('intval', $ids), fn($v) => $v > 0));
|
||||
if (empty($ids)) return [];
|
||||
|
||||
$cacheFile = __DIR__ . '/cache/clienti.json';
|
||||
if (!file_exists($cacheFile)) return [];
|
||||
|
||||
$data = json_decode((string)@file_get_contents($cacheFile), true);
|
||||
$clients = $data['value'] ?? [];
|
||||
if (empty($clients)) return [];
|
||||
|
||||
// indicizza per IdCliente
|
||||
$byId = [];
|
||||
foreach ($clients as $c) {
|
||||
$cid = (int)($c['IdCliente'] ?? 0);
|
||||
if ($cid > 0) {
|
||||
$byId[$cid] = trim((string)($c['Nominativo'] ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
$out = [];
|
||||
foreach ($ids as $id) {
|
||||
if (isset($byId[$id]) && $byId[$id] !== '') {
|
||||
$out[$id] = $byId[$id];
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
// input: solo POST (endpoint che invia email — niente attivazione via URL)
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Metodo non consentito']);
|
||||
exit;
|
||||
}
|
||||
$ids = $_POST['ids'] ?? [];
|
||||
if (!is_array($ids)) $ids = explode(',', (string)$ids);
|
||||
$ids = array_values(array_filter(array_map('intval', $ids), fn($v) => $v > 0));
|
||||
$templateId = (int)($_POST['template_id'] ?? 0);
|
||||
|
||||
if (empty($ids)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Nessun id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Fallback: se il template_id non arriva, ricavalo dalla prima commessa
|
||||
if ($templateId <= 0) {
|
||||
$tq = $pdo->prepare("SELECT templateid FROM datadb WHERE iddatadb = ? LIMIT 1");
|
||||
$tq->execute([$ids[0]]);
|
||||
$templateId = (int)$tq->fetchColumn();
|
||||
}
|
||||
|
||||
// tutti i main field del template (possono essere fino a 2), in ordine
|
||||
$mainMappingIds = [];
|
||||
$templateName = '';
|
||||
if ($templateId > 0) {
|
||||
$s = $pdo->prepare("
|
||||
SELECT id FROM template_mapping
|
||||
WHERE template_id = ? AND main_field = '1' AND is_visible_import = 1
|
||||
ORDER BY field_order ASC, id ASC
|
||||
");
|
||||
$s->execute([$templateId]);
|
||||
$mainMappingIds = array_map('intval', $s->fetchAll(PDO::FETCH_COLUMN));
|
||||
|
||||
// nome del template per l'intestazione mail
|
||||
$t = $pdo->prepare("SELECT name FROM excel_templates WHERE id = ? LIMIT 1");
|
||||
$t->execute([$templateId]);
|
||||
$templateName = (string)$t->fetchColumn();
|
||||
}
|
||||
|
||||
// esiti delle commesse
|
||||
$ph = implode(',', array_fill(0, count($ids), '?'));
|
||||
$sql = "
|
||||
SELECT d.iddatadb, d.commessaweb, d.status, d.user_id,
|
||||
d.idclient, d.ClienteFatturazione,
|
||||
CONCAT(u.first_name,' ',u.last_name) AS user_name,
|
||||
u.email AS user_email, u.notify_export_email
|
||||
FROM datadb d
|
||||
LEFT JOIN auth_users u ON d.user_id = u.id
|
||||
WHERE d.iddatadb IN ($ph)
|
||||
";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($ids);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if (empty($rows)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Righe non trovate']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- Risoluzione nomi cliente (idclient + ClienteFatturazione) ---
|
||||
$clientIdsToResolve = [];
|
||||
foreach ($rows as $r) {
|
||||
if (!empty($r['idclient'])) $clientIdsToResolve[(int)$r['idclient']] = true;
|
||||
if (!empty($r['ClienteFatturazione'])) $clientIdsToResolve[(int)$r['ClienteFatturazione']] = true;
|
||||
}
|
||||
$clientNames = resolveClientNames(array_keys($clientIdsToResolve)); // [id => nome]
|
||||
|
||||
// helper: id cliente → nome, con fallback all'id se non risolto
|
||||
$clientLabel = function ($id) use ($clientNames) {
|
||||
$id = (int)$id;
|
||||
if ($id <= 0) return '—';
|
||||
return htmlspecialchars($clientNames[$id] ?? (string)$id);
|
||||
};
|
||||
|
||||
// valori dei main field per ogni iddatadb — concatena i main valorizzati
|
||||
$mainValues = [];
|
||||
if (!empty($mainMappingIds)) {
|
||||
$mph = implode(',', array_fill(0, count($mainMappingIds), '?'));
|
||||
$s = $pdo->prepare("
|
||||
SELECT id AS iddatadb, mapping_id, field_value
|
||||
FROM import_data_details
|
||||
WHERE id IN ($ph) AND mapping_id IN ($mph)
|
||||
");
|
||||
$s->execute(array_merge($ids, $mainMappingIds));
|
||||
|
||||
// raggruppa per iddatadb, mantenendo l'ordine dei main
|
||||
$byRow = []; // iddatadb => [mapping_id => value]
|
||||
foreach ($s->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||
$byRow[(int)$r['iddatadb']][(int)$r['mapping_id']] = $r['field_value'];
|
||||
}
|
||||
foreach ($byRow as $idd => $vals) {
|
||||
$parts = [];
|
||||
foreach ($mainMappingIds as $mid) { // ordine dei main
|
||||
$v = trim((string)($vals[$mid] ?? ''));
|
||||
if ($v !== '') $parts[] = $v;
|
||||
}
|
||||
$mainValues[$idd] = implode(' — ', $parts); // es. "descrizione — style code"
|
||||
}
|
||||
}
|
||||
|
||||
// analisi senza prezzo (NULL o 0) per ogni iddatadb
|
||||
$noPriceByRow = []; // iddatadb => [ [part_number, part_description, analysis_name], ... ]
|
||||
$s = $pdo->prepare("
|
||||
SELECT a.iddatadb, MAX(a.analysis_name) AS analysis_name
|
||||
FROM identification_parts_analyses a
|
||||
WHERE a.iddatadb IN ($ph)
|
||||
AND (a.analysis_price IS NULL OR a.analysis_price <= 0)
|
||||
GROUP BY a.iddatadb, a.analysis_recordkey
|
||||
ORDER BY a.iddatadb, analysis_name
|
||||
");
|
||||
$s->execute($ids);
|
||||
foreach ($s->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||
$noPriceByRow[(int)$r['iddatadb']][] = $r;
|
||||
}
|
||||
$noPriceTotal = array_sum(array_map('count', $noPriceByRow));
|
||||
|
||||
// destinatari: utente/i dell'export che hanno la notifica attiva + admin fisso
|
||||
$recipients = [];
|
||||
foreach ($rows as $r) {
|
||||
if (!empty($r['user_email']) && (int)($r['notify_export_email'] ?? 1) === 1) {
|
||||
$recipients[$r['user_email']] = true;
|
||||
}
|
||||
}
|
||||
$adminEmail = env('EXPORT_SUMMARY_ADMIN', 'admin@cesoft.io'); // sempre, indipendente dalla preferenza
|
||||
if ($adminEmail) $recipients[$adminEmail] = true;
|
||||
$recipients = array_keys($recipients);
|
||||
|
||||
// Se dopo il filtro non resta nessuno, non inviare
|
||||
if (empty($recipients)) {
|
||||
echo json_encode(['success' => true, 'message' => 'Nessun destinatario (notifiche disattivate)', 'recipients' => []]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// conteggi
|
||||
$okCount = 0;
|
||||
$errCount = 0;
|
||||
foreach ($rows as $r) {
|
||||
if ($r['status'] === 'l') $okCount++;
|
||||
elseif ($r['status'] === 'x') $errCount++;
|
||||
}
|
||||
$hasError = $errCount > 0;
|
||||
|
||||
// --- tabella righe ---
|
||||
$rowsHtml = '';
|
||||
foreach ($rows as $r) {
|
||||
$st = $r['status'];
|
||||
if ($st === 'l') {
|
||||
$label = 'Inviata';
|
||||
$bg = '#e6f4ea';
|
||||
$col = '#1e7e34';
|
||||
} elseif ($st === 'x') {
|
||||
$label = 'ERRORE';
|
||||
$bg = '#fdecea';
|
||||
$col = '#c0392b';
|
||||
} else {
|
||||
$label = htmlspecialchars((string)$st);
|
||||
$bg = '#fff8e1';
|
||||
$col = '#8a6d3b';
|
||||
}
|
||||
|
||||
$commessa = htmlspecialchars($r['commessaweb'] ?: '—');
|
||||
$main = htmlspecialchars($mainValues[(int)$r['iddatadb']] ?? '—');
|
||||
|
||||
$committente = $clientLabel($r['idclient'] ?? 0);
|
||||
$fatturazione = $clientLabel($r['ClienteFatturazione'] ?? 0);
|
||||
|
||||
$rowsHtml .= "<tr>
|
||||
<td style='padding:10px 10px;border-bottom:1px solid #eee;font-family:monospace;white-space:nowrap;vertical-align:top;'>{$commessa}</td>
|
||||
<td style='padding:10px 10px;border-bottom:1px solid #eee;word-break:break-word;vertical-align:top;'>{$committente}</td>
|
||||
<td style='padding:10px 10px;border-bottom:1px solid #eee;word-break:break-word;vertical-align:top;'>{$fatturazione}</td>
|
||||
<td style='padding:10px 10px;border-bottom:1px solid #eee;word-break:break-word;vertical-align:top;'>{$main}</td>
|
||||
<td style='padding:10px 10px;border-bottom:1px solid #eee;text-align:center;white-space:nowrap;vertical-align:top;'>
|
||||
<span style='display:inline-block;padding:3px 10px;border-radius:12px;background:{$bg};color:{$col};font-weight:600;font-size:13px;'>{$label}</span>
|
||||
</td>
|
||||
</tr>";
|
||||
|
||||
// sotto-riga: solo le analisi No Price di questa commessa
|
||||
$np = $noPriceByRow[(int)$r['iddatadb']] ?? [];
|
||||
if (!empty($np)) {
|
||||
$items = '';
|
||||
foreach ($np as $n) {
|
||||
$analysisName = htmlspecialchars((string)($n['analysis_name'] ?: 'Unnamed analysis'));
|
||||
$items .= "<li style='margin:2px 0;'>{$analysisName}</li>";
|
||||
}
|
||||
$npCount = count($np);
|
||||
|
||||
$rowsHtml .= "<tr>
|
||||
<td colspan='5' style='padding:8px 12px 12px 24px;border-bottom:1px solid #eee;background:#fff5f5;'>
|
||||
<div style='margin-bottom:4px;font-size:12px;color:#c0392b;font-weight:600;'>
|
||||
<span style='display:inline-block;padding:2px 8px;border-radius:10px;background:#c0392b;color:#fff;font-size:11px;'>No price</span>
|
||||
Analisi senza prezzo ({$npCount})
|
||||
</div>
|
||||
<ul style='margin:0;padding-left:18px;font-size:13px;color:#555;'>{$items}</ul>
|
||||
</td>
|
||||
</tr>";
|
||||
}
|
||||
}
|
||||
|
||||
$headerColor = $hasError ? '#c0392b' : '#1e7e34';
|
||||
$headerText = $hasError ? 'Export completato con errori' : 'Export completato con successo';
|
||||
$total = count($rows);
|
||||
|
||||
$html = "
|
||||
<div style='font-family:Arial,Helvetica,sans-serif;max-width:820px;margin:0 auto;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden;'>
|
||||
<div style='background:{$headerColor};color:#fff;padding:18px 24px;'>
|
||||
<h2 style='margin:0;font-size:18px;'>{$headerText}</h2>
|
||||
<p style='margin:6px 0 0;font-size:13px;opacity:.9;'>Riepilogo esportazione verso LIMS" .
|
||||
($templateName !== '' ? " · Template: <strong>" . htmlspecialchars($templateName) . "</strong>" : "") . "</p>
|
||||
<p style='margin:6px 0 0;font-size:13px;opacity:.95;'>
|
||||
Committente: <strong>" . $clientLabel($rows[0]['idclient'] ?? 0) . "</strong>
|
||||
·
|
||||
FatturazioneAvanzata: <strong>" . $clientLabel($rows[0]['ClienteFatturazione'] ?? 0) . "</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div style='padding:20px 24px;'>
|
||||
<p style='margin:0 0 14px;font-size:14px;color:#333;'>
|
||||
Totale commesse: <strong>{$total}</strong> •
|
||||
<span style='color:#1e7e34;'>Inviate: {$okCount}</span> •
|
||||
<span style='color:#c0392b;'>Errori: {$errCount}</span>" .
|
||||
($noPriceTotal > 0 ? " • <span style='color:#c0392b;'>Analisi No price: {$noPriceTotal}</span>" : "") . "
|
||||
</p>
|
||||
<table style='width:100%;border-collapse:collapse;font-size:14px;table-layout:fixed;'>
|
||||
<thead>
|
||||
<tr style='background:#f5f5f5;'>
|
||||
<th style='padding:10px 10px;text-align:left;border-bottom:2px solid #ddd;width:14%;'>Commessa</th>
|
||||
<th style='padding:10px 10px;text-align:left;border-bottom:2px solid #ddd;width:24%;'>Committente</th>
|
||||
<th style='padding:10px 10px;text-align:left;border-bottom:2px solid #ddd;width:24%;'>Fatturazione</th>
|
||||
<th style='padding:10px 10px;text-align:left;border-bottom:2px solid #ddd;width:24%;'>Riferimento</th>
|
||||
<th style='padding:10px 10px;text-align:center;border-bottom:2px solid #ddd;width:14%;'>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{$rowsHtml}</tbody>
|
||||
</table>
|
||||
" . ($hasError ? "<p style='margin:18px 0 0;padding:12px;background:#fdecea;border-left:4px solid #c0392b;color:#c0392b;font-size:13px;border-radius:4px;'>⚠️ Alcune commesse non sono state inviate al LIMS. Controlla la sezione <strong>Errors</strong> nell'applicazione per i dettagli e per rilanciarle.</p>" : "") . "
|
||||
</div>
|
||||
<div style='background:#fafafa;padding:14px 24px;font-size:12px;color:#999;border-top:1px solid #eee;'>
|
||||
Messaggio automatico da SmartTRF — non rispondere a questa email.
|
||||
</div>
|
||||
</div>";
|
||||
|
||||
$subjectPrefix = $hasError ? '⚠️ Errori Export LIMS' : '✅ Export LIMS OK';
|
||||
$subject = "{$subjectPrefix} — {$total} commesse ({$okCount} ok, {$errCount} err)";
|
||||
|
||||
$res = Mailer::send($recipients, $subject, $html);
|
||||
|
||||
echo json_encode([
|
||||
'success' => $res['ok'],
|
||||
'message' => $res['ok'] ? 'Mail inviata' : ('Mail non inviata: ' . $res['error']),
|
||||
'recipients' => $recipients,
|
||||
'counts' => ['ok' => $okCount, 'err' => $errCount, 'total' => $total],
|
||||
]);
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/include/headscript.php'; // autentica e dà $iduserlogin
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE auth_sessions SET last_activity = ? WHERE user_id = ? ORDER BY last_activity DESC LIMIT 1"
|
||||
);
|
||||
$stmt->execute([time(), $iduserlogin]);
|
||||
echo json_encode(['ok' => true, 'rows' => $stmt->rowCount(), 'ts' => time()]);
|
||||
} catch (\Throwable $e) {
|
||||
echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
require_once __DIR__ . '/class/binding-functions.php';
|
||||
include dirname(__DIR__) . '/../extra/auth.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
if (!Auth::check()) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$kind = ($_POST['kind'] ?? 'custom') === 'fixed' ? 'fixed' : 'custom';
|
||||
$mappingId = intval($_POST['mapping_id'] ?? 0);
|
||||
$sourceValue = (string) ($_POST['source_value'] ?? '');
|
||||
|
||||
$datadbIds = [];
|
||||
if (isset($_POST['datadb_ids'])) {
|
||||
$decoded = json_decode($_POST['datadb_ids'], true);
|
||||
if (is_array($decoded)) {
|
||||
$datadbIds = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
if ($mappingId <= 0 || trim($sourceValue) === '') {
|
||||
http_response_code(422);
|
||||
echo json_encode(['success' => false, 'error' => 'Missing required parameters']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
if ($kind === 'fixed') {
|
||||
$stmt = $pdo->prepare("SELECT fixed_field_key FROM template_fixed_mapping WHERE id = ?");
|
||||
$stmt->execute([$mappingId]);
|
||||
$fixedKey = (string) ($stmt->fetchColumn() ?: '');
|
||||
|
||||
if ($fixedKey === '') {
|
||||
http_response_code(422);
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid fixed field']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$cleared = !empty($datadbIds)
|
||||
? binding_apply_to_datadb($pdo, $fixedKey, null, $datadbIds)
|
||||
: 0;
|
||||
} else {
|
||||
$cleared = !empty($datadbIds)
|
||||
? binding_apply_to_details($pdo, $mappingId, '', $datadbIds)
|
||||
: 0;
|
||||
}
|
||||
|
||||
binding_delete($pdo, $mappingId, $sourceValue, $kind);
|
||||
|
||||
echo json_encode(['success' => true, 'cleared_rows' => $cleared]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
-- json_node e default_source possono gia' esistere: in quel caso saltare le ADD COLUMN.
|
||||
|
||||
ALTER TABLE `template_fixed_mapping`
|
||||
ADD COLUMN `json_node` varchar(500) DEFAULT NULL,
|
||||
ADD COLUMN `default_source` enum('manual','xls','json') NOT NULL DEFAULT 'manual';
|
||||
|
||||
ALTER TABLE `template_fixed_mapping`
|
||||
ADD COLUMN `excel_column` varchar(255) DEFAULT NULL AFTER `json_node`;
|
||||
|
||||
ALTER TABLE `template_fixed_mapping`
|
||||
MODIFY COLUMN `default_source` enum('manual','xls','json') NOT NULL DEFAULT 'manual';
|
||||
|
||||
ALTER TABLE `lims_value_binding`
|
||||
ADD COLUMN `binding_kind` enum('custom','fixed') NOT NULL DEFAULT 'custom' AFTER `template_id`,
|
||||
MODIFY COLUMN `field_id` int(11) NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE `lims_value_binding` DROP INDEX `uq_mapping_source`;
|
||||
ALTER TABLE `lims_value_binding`
|
||||
ADD UNIQUE KEY `uq_kind_mapping_source` (`binding_kind`, `mapping_id`, `source_key`);
|
||||
@@ -0,0 +1,17 @@
|
||||
CREATE TABLE IF NOT EXISTS `lims_value_binding` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`template_id` int(11) NOT NULL,
|
||||
`mapping_id` int(11) NOT NULL,
|
||||
`field_id` int(11) NOT NULL,
|
||||
`source_value` varchar(500) NOT NULL,
|
||||
`source_key` varchar(500) NOT NULL,
|
||||
`lims_value_id` int(11) NOT NULL,
|
||||
`lims_value` varchar(500) DEFAULT NULL,
|
||||
`created_by` int(11) DEFAULT NULL,
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uq_mapping_source` (`mapping_id`, `source_key`),
|
||||
KEY `idx_template` (`template_id`),
|
||||
KEY `idx_field_source` (`field_id`, `source_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
@@ -0,0 +1,546 @@
|
||||
<?php include('include/headscript.php'); ?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
<?php include('cssinclude.php'); ?>
|
||||
<title>TRF-Project - Statistiche Export LIMS</title>
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||
<style>
|
||||
.stat-card .card-body {
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .03em;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.chart-wrap {
|
||||
position: relative;
|
||||
height: 420px;
|
||||
}
|
||||
|
||||
#tblClienti,
|
||||
#tblUtenti {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#tblClienti th,
|
||||
#tblClienti td,
|
||||
#tblUtenti th,
|
||||
#tblUtenti td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
gap: .75rem;
|
||||
}
|
||||
|
||||
.stat-card-extra {
|
||||
background: linear-gradient(135deg, #021bff 0%, #010983 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.stat-card-extra .stat-label {
|
||||
color: #d7d2f5;
|
||||
}
|
||||
|
||||
.stat-card-extra .stat-value {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.stat-card-extra small.text-muted {
|
||||
color: #c9c2ec !important;
|
||||
}
|
||||
|
||||
.kpi-row {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<!-- ===== FILTRO GIORNATA ===== -->
|
||||
<div class="card radius-10">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center flex-wrap filter-bar">
|
||||
<h6 class="mb-0 me-3"><i class="bx bx-bar-chart-alt-2"></i> Statistiche Export LIMS</h6>
|
||||
<div class="d-flex align-items-center">
|
||||
<label for="filterDate" class="me-2 mb-0 fw-semibold">Giornata:</label>
|
||||
<input type="date" id="filterDate" class="form-control form-control-sm" style="width:auto;">
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm ms-2" role="group">
|
||||
<button class="btn btn-outline-secondary" id="btnToday">Oggi</button>
|
||||
<button class="btn btn-outline-secondary" id="btnYesterday">Ieri</button>
|
||||
</div>
|
||||
<a href="stats_export_lims_monthly.php" class="btn btn-outline-primary btn-sm ms-auto">
|
||||
<i class="bx bx-calendar-alt"></i> Vista mensile
|
||||
</a>
|
||||
<button class="btn btn-primary btn-sm ms-2" id="btnRefresh">
|
||||
<i class="bx bx-refresh"></i> Aggiorna
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== KPI ===== -->
|
||||
<div class="row row-cols-2 row-cols-md-3 row-cols-lg-5 g-3 kpi-row">
|
||||
<div class="col">
|
||||
<div class="card radius-10 stat-card stat-card-extra h-100">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">Campioni mese <span id="kpiMonthLabel"></span></div>
|
||||
<div class="stat-value" id="kpiPartsMonth">0</div>
|
||||
<small class="text-muted">Commesse: <span id="kpiExportMonth">0</span> · dal 16/07/2026</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card radius-10 stat-card stat-card-extra h-100">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">Campioni anno <span id="kpiYearLabel"></span></div>
|
||||
<div class="stat-value" id="kpiPartsYear">0</div>
|
||||
<small class="text-muted">Commesse: <span id="kpiExportYear">0</span> · dal 16/07/2026</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card radius-10 stat-card h-100">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">Campioni esportati (giorno)</div>
|
||||
<div class="stat-value text-primary" id="kpiParts">0</div>
|
||||
<small class="text-muted">Commesse: <span id="kpiExport">0</span></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card radius-10 stat-card h-100">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">Campioni inseriti LIMS</div>
|
||||
<div class="stat-value text-info" id="kpiLimsSamples">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card radius-10 stat-card h-100">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">% Esportati / inseriti</div>
|
||||
<div class="stat-value text-success" id="kpiPerc">—</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== DATO GIORNALIERO CAMPIONI LIMS ===== -->
|
||||
<div class="card radius-10">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center flex-wrap" style="gap:.75rem;">
|
||||
<span class="fw-semibold">Campioni totali inseriti nel LIMS per questa giornata:</span>
|
||||
<input type="number" min="0" id="limsSamplesInput" class="form-control form-control-sm" style="width:140px;" placeholder="0">
|
||||
<button class="btn btn-success btn-sm" id="btnSaveLimsSamples">
|
||||
<i class="bx bx-save"></i> Salva dato
|
||||
</button>
|
||||
<small class="text-muted">Un valore al giorno. Usato per calcolare la % di export.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== GRAFICO TORTA ESPORTATI vs TOTALE LIMS ===== -->
|
||||
<div class="row" id="pieRow" style="display:none;">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Esportati vs totale inserito nel LIMS</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart-wrap" style="height:340px;"><canvas id="chartPie"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== GRAFICI ===== -->
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Export per cliente (idclient) <small class="text-muted">— top 15</small></h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart-wrap"><canvas id="chartClienti"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Export per utente (user_id) <small class="text-muted">— top 15</small></h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart-wrap"><canvas id="chartUtenti"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== LISTE ===== -->
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Dettaglio per cliente</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table id="tblClienti" class="table table-striped table-bordered table-sm w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID Cliente</th>
|
||||
<th>Cliente</th>
|
||||
<th>Campioni</th>
|
||||
<th>Commesse</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Dettaglio per utente</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table id="tblUtenti" class="table table-striped table-bordered table-sm w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID Utente</th>
|
||||
<th>Utente</th>
|
||||
<th>Campioni</th>
|
||||
<th>Commesse</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overlay toggle-icon"></div>
|
||||
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
|
||||
<script>
|
||||
let chartClienti = null;
|
||||
let chartUtenti = null;
|
||||
let chartPie = null;
|
||||
let dtClienti = null;
|
||||
let dtUtenti = null;
|
||||
|
||||
function todayStr() {
|
||||
const d = new Date();
|
||||
const off = d.getTimezoneOffset();
|
||||
return new Date(d.getTime() - off * 60000).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function initTables() {
|
||||
dtClienti = $('#tblClienti').DataTable({
|
||||
paging: false,
|
||||
searching: false,
|
||||
info: false,
|
||||
order: [
|
||||
[2, 'desc']
|
||||
],
|
||||
columns: [{
|
||||
data: 'idclient'
|
||||
},
|
||||
{
|
||||
data: 'clientname',
|
||||
defaultContent: '—'
|
||||
},
|
||||
{
|
||||
data: 'parts',
|
||||
className: 'text-end'
|
||||
},
|
||||
{
|
||||
data: 'tot',
|
||||
className: 'text-end'
|
||||
}
|
||||
],
|
||||
language: {
|
||||
emptyTable: 'Nessun export in questa giornata'
|
||||
}
|
||||
});
|
||||
dtUtenti = $('#tblUtenti').DataTable({
|
||||
paging: false,
|
||||
searching: false,
|
||||
info: false,
|
||||
order: [
|
||||
[2, 'desc']
|
||||
],
|
||||
columns: [{
|
||||
data: 'user_id'
|
||||
},
|
||||
{
|
||||
data: 'username',
|
||||
defaultContent: '—'
|
||||
},
|
||||
{
|
||||
data: 'parts',
|
||||
className: 'text-end'
|
||||
},
|
||||
{
|
||||
data: 'tot',
|
||||
className: 'text-end'
|
||||
}
|
||||
],
|
||||
language: {
|
||||
emptyTable: 'Nessun export in questa giornata'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderBarChart(existing, canvasId, labels, values, label) {
|
||||
if (existing) existing.destroy();
|
||||
const ctx = document.getElementById(canvasId).getContext('2d');
|
||||
return new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: label,
|
||||
data: values,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
precision: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Torta: esportati vs non esportati (totale LIMS - esportati)
|
||||
function renderPie(exported, limsTotal) {
|
||||
const pieRow = document.getElementById('pieRow');
|
||||
// Mostra solo se il totale è inserito e valido
|
||||
if (!limsTotal || limsTotal <= 0) {
|
||||
pieRow.style.display = 'none';
|
||||
if (chartPie) {
|
||||
chartPie.destroy();
|
||||
chartPie = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
pieRow.style.display = '';
|
||||
|
||||
const notExported = Math.max(0, limsTotal - exported);
|
||||
if (chartPie) chartPie.destroy();
|
||||
const ctx = document.getElementById('chartPie').getContext('2d');
|
||||
chartPie = new Chart(ctx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: ['Esportati', 'Totali LIMS'],
|
||||
datasets: [{
|
||||
data: [exported, notExported],
|
||||
backgroundColor: ['#198754', '#dee2e6'],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(c) {
|
||||
const perc = limsTotal > 0 ? ((c.parsed / limsTotal) * 100).toFixed(1) : 0;
|
||||
return c.label + ': ' + c.parsed + ' (' + perc + '%)';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Prende i primi N record (già ordinati desc dal backend) per il grafico
|
||||
const CHART_TOP_N = 15;
|
||||
|
||||
function topN(arr) {
|
||||
return arr.slice(0, CHART_TOP_N);
|
||||
}
|
||||
|
||||
function loadStats() {
|
||||
const date = $('#filterDate').val() || todayStr();
|
||||
|
||||
$.getJSON('stats_export_lims_data.php', {
|
||||
date: date
|
||||
}, function(res) {
|
||||
if (!res || !res.success) {
|
||||
Swal.fire('Errore', (res && res.message) || 'Errore nel caricamento', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// KPI — dato principale = parti/campioni, sotto le commesse
|
||||
$('#kpiParts').text(res.totals.parts ?? 0);
|
||||
$('#kpiExport').text(res.totals.export ?? 0);
|
||||
$('#kpiPartsMonth').text(res.totalsMonth ? (res.totalsMonth.parts ?? 0) : 0);
|
||||
$('#kpiExportMonth').text(res.totalsMonth ? (res.totalsMonth.export ?? 0) : 0);
|
||||
$('#kpiPartsYear').text(res.totalsYear ? (res.totalsYear.parts ?? 0) : 0);
|
||||
$('#kpiExportYear').text(res.totalsYear ? (res.totalsYear.export ?? 0) : 0);
|
||||
$('#kpiLimsSamples').text(res.limsSamples ?? 0);
|
||||
$('#limsSamplesInput').val(res.limsSamples ?? '');
|
||||
|
||||
// Etichette mese/anno dei box totali
|
||||
if (res.month) {
|
||||
const MESI_IT = ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno',
|
||||
'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'
|
||||
];
|
||||
const mParts = res.month.split('-');
|
||||
const mIdx = parseInt(mParts[1], 10) - 1;
|
||||
if (mIdx >= 0 && mIdx < 12) {
|
||||
$('#kpiMonthLabel').text('(' + MESI_IT[mIdx] + ' ' + mParts[0] + ')');
|
||||
}
|
||||
}
|
||||
if (res.year) {
|
||||
$('#kpiYearLabel').text('(' + res.year + ')');
|
||||
}
|
||||
|
||||
const limsTot = parseInt(res.limsSamples ?? 0, 10);
|
||||
const partsTot = parseInt(res.totals.parts ?? 0, 10);
|
||||
if (limsTot > 0) {
|
||||
const perc = (partsTot / limsTot) * 100;
|
||||
$('#kpiPerc').text(perc.toFixed(1) + '%');
|
||||
} else {
|
||||
$('#kpiPerc').text('—');
|
||||
}
|
||||
|
||||
// Torta parti esportate vs totale inserito nel LIMS
|
||||
renderPie(partsTot, limsTot);
|
||||
|
||||
// Grafici — valori = campioni/parti
|
||||
const clientTop = topN(res.byClient);
|
||||
const userTop = topN(res.byUser);
|
||||
chartClienti = renderBarChart(chartClienti, 'chartClienti',
|
||||
clientTop.map(r => r.clientname ? (r.clientname + ' (' + r.idclient + ')') : ('ID ' + r.idclient)),
|
||||
clientTop.map(r => r.parts), 'Campioni');
|
||||
chartUtenti = renderBarChart(chartUtenti, 'chartUtenti',
|
||||
userTop.map(r => r.username ? (r.username + ' (' + r.user_id + ')') : ('ID ' + r.user_id)),
|
||||
userTop.map(r => r.parts), 'Campioni');
|
||||
|
||||
// Tabelle
|
||||
dtClienti.clear().rows.add(res.byClient).draw();
|
||||
dtUtenti.clear().rows.add(res.byUser).draw();
|
||||
}).fail(function(xhr) {
|
||||
Swal.fire('Errore', 'Richiesta fallita: ' + xhr.status, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#filterDate').val(todayStr());
|
||||
initTables();
|
||||
loadStats();
|
||||
|
||||
$('#btnRefresh').on('click', loadStats);
|
||||
$('#filterDate').on('change', loadStats);
|
||||
|
||||
$('#btnToday').on('click', function() {
|
||||
$('#filterDate').val(todayStr());
|
||||
loadStats();
|
||||
});
|
||||
$('#btnYesterday').on('click', function() {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - 1);
|
||||
const off = d.getTimezoneOffset();
|
||||
$('#filterDate').val(new Date(d.getTime() - off * 60000).toISOString().slice(0, 10));
|
||||
loadStats();
|
||||
});
|
||||
|
||||
$('#btnSaveLimsSamples').on('click', function() {
|
||||
const date = $('#filterDate').val() || todayStr();
|
||||
const total = parseInt($('#limsSamplesInput').val(), 10);
|
||||
if (isNaN(total) || total < 0) {
|
||||
Swal.fire('Attenzione', 'Inserisci un numero valido', 'warning');
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
url: 'stats_save_lims_samples.php',
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
date: date,
|
||||
total_samples: total
|
||||
},
|
||||
success: function(res) {
|
||||
if (res && res.success) {
|
||||
Swal.fire({
|
||||
title: 'Salvato',
|
||||
icon: 'success',
|
||||
timer: 1200,
|
||||
showConfirmButton: false
|
||||
});
|
||||
loadStats();
|
||||
} else {
|
||||
Swal.fire('Errore', (res && res.message) || 'Salvataggio fallito', 'error');
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
Swal.fire('Errore', 'Richiesta fallita: ' + xhr.status, 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$dbHandler = DBHandlerSelect::getInstance();
|
||||
$pdo = $dbHandler->getConnection();
|
||||
|
||||
try {
|
||||
// Data richiesta (default oggi). Formato atteso: Y-m-d
|
||||
$date = $_GET['date'] ?? date('Y-m-d');
|
||||
$d = DateTime::createFromFormat('Y-m-d', $date);
|
||||
if (!$d || $d->format('Y-m-d') !== $date) {
|
||||
throw new Exception('Data non valida');
|
||||
}
|
||||
|
||||
// --- Export per cliente (status = 'l', giornata su export_date) ---
|
||||
// tot = numero commesse, parts = numero parti/campioni
|
||||
$stmtC = $pdo->prepare("
|
||||
SELECT d.idclient AS idclient,
|
||||
COUNT(DISTINCT d.iddatadb) AS tot,
|
||||
COUNT(ip.id) AS parts
|
||||
FROM datadb d
|
||||
LEFT JOIN identification_parts ip ON ip.iddatadb = d.iddatadb
|
||||
WHERE d.status = 'l'
|
||||
AND DATE(d.export_date) = :d
|
||||
GROUP BY d.idclient
|
||||
ORDER BY parts DESC
|
||||
");
|
||||
$stmtC->execute(['d' => $date]);
|
||||
$byClient = $stmtC->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// --- Risolvi Nominativo cliente dalla cache locale (search_clienti.php) ---
|
||||
$clientNames = [];
|
||||
$cacheFile = __DIR__ . '/cache/clienti.json';
|
||||
if (is_file($cacheFile)) {
|
||||
$cache = json_decode(file_get_contents($cacheFile), true);
|
||||
foreach (($cache['value'] ?? []) as $c) {
|
||||
$clientNames[(int)($c['IdCliente'] ?? 0)] = trim($c['Nominativo'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Export per utente (status = 'l', giornata su export_date) ---
|
||||
// tot = numero commesse, parts = numero parti/campioni
|
||||
$stmtU = $pdo->prepare("
|
||||
SELECT d.user_id AS user_id,
|
||||
TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS username,
|
||||
COUNT(DISTINCT d.iddatadb) AS tot,
|
||||
COUNT(ip.id) AS parts
|
||||
FROM datadb d
|
||||
LEFT JOIN auth_users u ON u.id = d.user_id
|
||||
LEFT JOIN identification_parts ip ON ip.iddatadb = d.iddatadb
|
||||
WHERE d.status = 'l'
|
||||
AND DATE(d.export_date) = :d
|
||||
GROUP BY d.user_id, username
|
||||
ORDER BY parts DESC
|
||||
");
|
||||
$stmtU->execute(['d' => $date]);
|
||||
$byUser = $stmtU->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Normalizza tipi (COUNT torna stringa in PDO)
|
||||
foreach ($byClient as &$r) {
|
||||
$r['idclient'] = $r['idclient'] !== null ? (int)$r['idclient'] : null;
|
||||
$r['tot'] = (int)$r['tot'];
|
||||
$r['parts'] = (int)$r['parts'];
|
||||
$nm = $clientNames[$r['idclient']] ?? '';
|
||||
$r['clientname'] = $nm !== '' ? $nm : null;
|
||||
}
|
||||
unset($r);
|
||||
foreach ($byUser as &$r) {
|
||||
$r['user_id'] = $r['user_id'] !== null ? (int)$r['user_id'] : null;
|
||||
$r['tot'] = (int)$r['tot'];
|
||||
$r['parts'] = (int)$r['parts'];
|
||||
if (isset($r['username'])) $r['username'] = $r['username'] !== null && $r['username'] !== '' ? $r['username'] : null;
|
||||
}
|
||||
unset($r);
|
||||
|
||||
// --- Totale export della giornata ---
|
||||
$totExport = array_sum(array_column($byClient, 'tot'));
|
||||
|
||||
// --- Totale PARTI/CAMPIONI della giornata (righe identification_parts legate
|
||||
// ai datadb esportati status='l' in questa giornata) ---
|
||||
$stmtP = $pdo->prepare("
|
||||
SELECT COUNT(*)
|
||||
FROM identification_parts ip
|
||||
INNER JOIN datadb d ON d.iddatadb = ip.iddatadb
|
||||
WHERE d.status = 'l'
|
||||
AND DATE(d.export_date) = :d
|
||||
");
|
||||
$stmtP->execute(['d' => $date]);
|
||||
$totParts = (int)$stmtP->fetchColumn();
|
||||
|
||||
// --- Totale COMMESSE del MESE (status = 'l') ---
|
||||
$stmtM = $pdo->prepare("
|
||||
SELECT COUNT(*)
|
||||
FROM datadb d
|
||||
WHERE d.status = 'l'
|
||||
AND DATE_FORMAT(d.export_date, '%Y-%m') = :m
|
||||
");
|
||||
$stmtM->execute(['m' => $d->format('Y-m')]);
|
||||
$totExportMonth = (int)$stmtM->fetchColumn();
|
||||
|
||||
// --- Totale PARTI del MESE (status = 'l') ---
|
||||
$stmtMP = $pdo->prepare("
|
||||
SELECT COUNT(*)
|
||||
FROM identification_parts ip
|
||||
INNER JOIN datadb d ON d.iddatadb = ip.iddatadb
|
||||
WHERE d.status = 'l'
|
||||
AND DATE_FORMAT(d.export_date, '%Y-%m') = :m
|
||||
");
|
||||
$stmtMP->execute(['m' => $d->format('Y-m')]);
|
||||
$totPartsMonth = (int)$stmtMP->fetchColumn();
|
||||
|
||||
// --- Totale COMMESSE dell'ANNO (status = 'l') ---
|
||||
$stmtY = $pdo->prepare("
|
||||
SELECT COUNT(*)
|
||||
FROM datadb d
|
||||
WHERE d.status = 'l'
|
||||
AND YEAR(d.export_date) = :y
|
||||
");
|
||||
$stmtY->execute(['y' => $d->format('Y')]);
|
||||
$totExportYear = (int)$stmtY->fetchColumn();
|
||||
|
||||
// --- Totale PARTI dell'ANNO (status = 'l') ---
|
||||
$stmtYP = $pdo->prepare("
|
||||
SELECT COUNT(*)
|
||||
FROM identification_parts ip
|
||||
INNER JOIN datadb d ON d.iddatadb = ip.iddatadb
|
||||
WHERE d.status = 'l'
|
||||
AND YEAR(d.export_date) = :y
|
||||
");
|
||||
$stmtYP->execute(['y' => $d->format('Y')]);
|
||||
$totPartsYear = (int)$stmtYP->fetchColumn();
|
||||
|
||||
// --- Campioni totali inseriti nel LIMS per la giornata (tabella lims_daily_samples) ---
|
||||
$stmtL = $pdo->prepare("SELECT total_samples FROM lims_daily_samples WHERE sample_date = :d LIMIT 1");
|
||||
$stmtL->execute(['d' => $date]);
|
||||
$limsSamples = $stmtL->fetchColumn();
|
||||
$limsSamples = ($limsSamples !== false) ? (int)$limsSamples : null;
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'date' => $date,
|
||||
'month' => $d->format('Y-m'),
|
||||
'year' => (int)$d->format('Y'),
|
||||
'totals' => ['export' => (int)$totExport, 'parts' => $totParts],
|
||||
'totalsMonth' => ['export' => $totExportMonth, 'parts' => $totPartsMonth],
|
||||
'totalsYear' => ['export' => $totExportYear, 'parts' => $totPartsYear],
|
||||
'byClient' => $byClient,
|
||||
'byUser' => $byUser,
|
||||
'limsSamples' => $limsSamples,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
error_log('Stats export LIMS error: ' . $e->getMessage());
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
<?php include('include/headscript.php'); ?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
<?php include('cssinclude.php'); ?>
|
||||
<title>TRF-Project - Statistiche Export LIMS Mensili</title>
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||
<style>
|
||||
.stat-card .card-body {
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .03em;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.chart-wrap {
|
||||
position: relative;
|
||||
height: 420px;
|
||||
}
|
||||
|
||||
#tblClienti,
|
||||
#tblUtenti {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#tblClienti th,
|
||||
#tblClienti td,
|
||||
#tblUtenti th,
|
||||
#tblUtenti td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
gap: .75rem;
|
||||
}
|
||||
|
||||
.month-label {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stat-card-extra {
|
||||
background: linear-gradient(135deg, #021bff 0%, #010983 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.stat-card-extra .stat-label {
|
||||
color: #c9cfff;
|
||||
}
|
||||
|
||||
.stat-card-extra .stat-value {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.stat-card-extra small.text-muted {
|
||||
color: #b3bbff !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<!-- ===== FILTRO MESE ===== -->
|
||||
<div class="card radius-10">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center flex-wrap filter-bar">
|
||||
<h6 class="mb-0 me-3"><i class="bx bx-bar-chart-alt-2"></i> Statistiche Export LIMS — Mensili</h6>
|
||||
<div class="d-flex align-items-center">
|
||||
<label for="filterMonth" class="me-2 mb-0 fw-semibold">Mese:</label>
|
||||
<input type="month" id="filterMonth" class="form-control form-control-sm" style="width:auto;">
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm ms-2" role="group">
|
||||
<button class="btn btn-outline-secondary" id="btnCurrentMonth">Mese corrente</button>
|
||||
<button class="btn btn-outline-secondary" id="btnPrevMonth">Mese precedente</button>
|
||||
</div>
|
||||
<span class="month-label ms-2 text-primary" id="monthLabel">—</span>
|
||||
<a href="stats_export_lims.php" class="btn btn-outline-primary btn-sm ms-auto">
|
||||
<i class="bx bx-calendar"></i> Vista giornaliera
|
||||
</a>
|
||||
<button class="btn btn-primary btn-sm ms-2" id="btnRefresh">
|
||||
<i class="bx bx-refresh"></i> Aggiorna
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== KPI (una sola riga) ===== -->
|
||||
<div class="row row-cols-2 row-cols-md-3 row-cols-lg-5 g-3">
|
||||
<div class="col">
|
||||
<div class="card radius-10 stat-card h-100">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">Campioni mese</div>
|
||||
<div class="stat-value text-primary" id="kpiParts">0</div>
|
||||
<small class="text-muted">Commesse: <span id="kpiExport">0</span></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card radius-10 stat-card stat-card-extra h-100">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">Campioni anno <span id="yearLabel">—</span></div>
|
||||
<div class="stat-value" id="kpiPartsYear">0</div>
|
||||
<small class="text-muted">Commesse: <span id="kpiExportYear">0</span> · dal 16/07/2026</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card radius-10 stat-card h-100">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">Campioni inseriti LIMS (mese)</div>
|
||||
<div class="stat-value text-info" id="kpiLimsSamples">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card radius-10 stat-card h-100">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">% Esportati / inseriti</div>
|
||||
<div class="stat-value text-success" id="kpiPerc">—</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card radius-10 stat-card h-100">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">Clienti distinti (mese)</div>
|
||||
<div class="stat-value text-warning" id="kpiClienti">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== GRAFICI ===== -->
|
||||
<div class="row mt-1">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Campioni per cliente (idclient) <small class="text-muted">— top 15</small></h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart-wrap"><canvas id="chartClienti"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Campioni per utente (user_id) <small class="text-muted">— top 15</small></h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart-wrap"><canvas id="chartUtenti"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== LISTE ===== -->
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Dettaglio per cliente</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table id="tblClienti" class="table table-striped table-bordered table-sm w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID Cliente</th>
|
||||
<th>Cliente</th>
|
||||
<th>Campioni</th>
|
||||
<th>Commesse</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Dettaglio per utente</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table id="tblUtenti" class="table table-striped table-bordered table-sm w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID Utente</th>
|
||||
<th>Utente</th>
|
||||
<th>Campioni</th>
|
||||
<th>Commesse</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overlay toggle-icon"></div>
|
||||
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
|
||||
<script>
|
||||
let chartClienti = null;
|
||||
let chartUtenti = null;
|
||||
let dtClienti = null;
|
||||
let dtUtenti = null;
|
||||
|
||||
const MESI_IT = ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno',
|
||||
'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'
|
||||
];
|
||||
|
||||
function currentMonthStr() {
|
||||
const d = new Date();
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
return `${y}-${m}`;
|
||||
}
|
||||
|
||||
function monthLabelFromStr(monthStr) {
|
||||
const parts = (monthStr || '').split('-');
|
||||
if (parts.length !== 2) return '—';
|
||||
const y = parseInt(parts[0], 10);
|
||||
const m = parseInt(parts[1], 10);
|
||||
if (isNaN(y) || isNaN(m) || m < 1 || m > 12) return '—';
|
||||
return `${MESI_IT[m - 1]} ${y}`;
|
||||
}
|
||||
|
||||
function initTables() {
|
||||
dtClienti = $('#tblClienti').DataTable({
|
||||
paging: false,
|
||||
searching: false,
|
||||
info: false,
|
||||
order: [
|
||||
[2, 'desc']
|
||||
],
|
||||
columns: [{
|
||||
data: 'idclient'
|
||||
},
|
||||
{
|
||||
data: 'clientname',
|
||||
defaultContent: '—'
|
||||
},
|
||||
{
|
||||
data: 'parts',
|
||||
className: 'text-end'
|
||||
},
|
||||
{
|
||||
data: 'tot',
|
||||
className: 'text-end'
|
||||
}
|
||||
],
|
||||
language: {
|
||||
emptyTable: 'Nessun export in questo mese'
|
||||
}
|
||||
});
|
||||
dtUtenti = $('#tblUtenti').DataTable({
|
||||
paging: false,
|
||||
searching: false,
|
||||
info: false,
|
||||
order: [
|
||||
[2, 'desc']
|
||||
],
|
||||
columns: [{
|
||||
data: 'user_id'
|
||||
},
|
||||
{
|
||||
data: 'username',
|
||||
defaultContent: '—'
|
||||
},
|
||||
{
|
||||
data: 'parts',
|
||||
className: 'text-end'
|
||||
},
|
||||
{
|
||||
data: 'tot',
|
||||
className: 'text-end'
|
||||
}
|
||||
],
|
||||
language: {
|
||||
emptyTable: 'Nessun export in questo mese'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderBarChart(existing, canvasId, labels, values, label) {
|
||||
if (existing) existing.destroy();
|
||||
const ctx = document.getElementById(canvasId).getContext('2d');
|
||||
return new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: label,
|
||||
data: values,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
precision: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const CHART_TOP_N = 15;
|
||||
|
||||
function topN(arr) {
|
||||
return arr.slice(0, CHART_TOP_N);
|
||||
}
|
||||
|
||||
function loadStats() {
|
||||
const month = $('#filterMonth').val() || currentMonthStr();
|
||||
$('#monthLabel').text(monthLabelFromStr(month));
|
||||
|
||||
$.getJSON('stats_export_lims_monthly_data.php', {
|
||||
month: month
|
||||
}, function(res) {
|
||||
if (!res || !res.success) {
|
||||
Swal.fire('Errore', (res && res.message) || 'Errore nel caricamento', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// KPI mese — dato principale = campioni/parti, sotto le commesse
|
||||
$('#kpiParts').text(res.totals.parts ?? 0);
|
||||
$('#kpiExport').text(res.totals.export ?? 0);
|
||||
$('#kpiClienti').text(res.byClient.length);
|
||||
$('#kpiLimsSamples').text(res.limsSamples ?? 0);
|
||||
|
||||
const limsTot = parseInt(res.limsSamples ?? 0, 10);
|
||||
const partsTot = parseInt(res.totals.parts ?? 0, 10);
|
||||
if (limsTot > 0) {
|
||||
const perc = (partsTot / limsTot) * 100;
|
||||
$('#kpiPerc').text(perc.toFixed(1) + '%');
|
||||
} else {
|
||||
$('#kpiPerc').text('—');
|
||||
}
|
||||
|
||||
// Box annuale — campioni + commesse
|
||||
$('#kpiPartsYear').text(res.totalsYear ? (res.totalsYear.parts ?? 0) : 0);
|
||||
$('#kpiExportYear').text(res.totalsYear ? (res.totalsYear.export ?? 0) : 0);
|
||||
$('#yearLabel').text(res.year);
|
||||
|
||||
// Grafici — valori = campioni/parti
|
||||
const clientTop = topN(res.byClient);
|
||||
const userTop = topN(res.byUser);
|
||||
chartClienti = renderBarChart(chartClienti, 'chartClienti',
|
||||
clientTop.map(r => r.clientname ? (r.clientname + ' (' + r.idclient + ')') : ('ID ' + r.idclient)),
|
||||
clientTop.map(r => r.parts), 'Campioni');
|
||||
chartUtenti = renderBarChart(chartUtenti, 'chartUtenti',
|
||||
userTop.map(r => r.username ? (r.username + ' (' + r.user_id + ')') : ('ID ' + r.user_id)),
|
||||
userTop.map(r => r.parts), 'Campioni');
|
||||
|
||||
// Tabelle
|
||||
dtClienti.clear().rows.add(res.byClient).draw();
|
||||
dtUtenti.clear().rows.add(res.byUser).draw();
|
||||
}).fail(function(xhr) {
|
||||
Swal.fire('Errore', 'Richiesta fallita: ' + xhr.status, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#filterMonth').val(currentMonthStr());
|
||||
initTables();
|
||||
loadStats();
|
||||
|
||||
$('#btnRefresh').on('click', loadStats);
|
||||
$('#filterMonth').on('change', loadStats);
|
||||
|
||||
$('#btnCurrentMonth').on('click', function() {
|
||||
$('#filterMonth').val(currentMonthStr());
|
||||
loadStats();
|
||||
});
|
||||
$('#btnPrevMonth').on('click', function() {
|
||||
const cur = $('#filterMonth').val() || currentMonthStr();
|
||||
const parts = cur.split('-');
|
||||
let y = parseInt(parts[0], 10);
|
||||
let m = parseInt(parts[1], 10);
|
||||
m -= 1;
|
||||
if (m < 1) {
|
||||
m = 12;
|
||||
y -= 1;
|
||||
}
|
||||
$('#filterMonth').val(`${y}-${String(m).padStart(2, '0')}`);
|
||||
loadStats();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$dbHandler = DBHandlerSelect::getInstance();
|
||||
$pdo = $dbHandler->getConnection();
|
||||
|
||||
try {
|
||||
// Mese richiesto (default mese corrente). Formato atteso: Y-m
|
||||
$month = $_GET['month'] ?? date('Y-m');
|
||||
$dm = DateTime::createFromFormat('Y-m', $month);
|
||||
if (!$dm || $dm->format('Y-m') !== $month) {
|
||||
throw new Exception('Mese non valido');
|
||||
}
|
||||
|
||||
$year = $dm->format('Y');
|
||||
|
||||
// --- Export per cliente (status = 'l', filtro mese) ---
|
||||
// tot = numero commesse, parts = numero parti/campioni
|
||||
$stmtC = $pdo->prepare("
|
||||
SELECT d.idclient AS idclient,
|
||||
COUNT(DISTINCT d.iddatadb) AS tot,
|
||||
COUNT(ip.id) AS parts
|
||||
FROM datadb d
|
||||
LEFT JOIN identification_parts ip ON ip.iddatadb = d.iddatadb
|
||||
WHERE d.status = 'l'
|
||||
AND DATE_FORMAT(d.export_date, '%Y-%m') = :m
|
||||
GROUP BY d.idclient
|
||||
ORDER BY parts DESC
|
||||
");
|
||||
$stmtC->execute(['m' => $month]);
|
||||
$byClient = $stmtC->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// --- Risolvi Nominativo cliente dalla cache locale ---
|
||||
$clientNames = [];
|
||||
$cacheFile = __DIR__ . '/cache/clienti.json';
|
||||
if (is_file($cacheFile)) {
|
||||
$cache = json_decode(file_get_contents($cacheFile), true);
|
||||
foreach (($cache['value'] ?? []) as $c) {
|
||||
$clientNames[(int)($c['IdCliente'] ?? 0)] = trim($c['Nominativo'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Export per utente (status = 'l', filtro mese) ---
|
||||
// tot = numero commesse, parts = numero parti/campioni
|
||||
$stmtU = $pdo->prepare("
|
||||
SELECT d.user_id AS user_id,
|
||||
TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS username,
|
||||
COUNT(DISTINCT d.iddatadb) AS tot,
|
||||
COUNT(ip.id) AS parts
|
||||
FROM datadb d
|
||||
LEFT JOIN auth_users u ON u.id = d.user_id
|
||||
LEFT JOIN identification_parts ip ON ip.iddatadb = d.iddatadb
|
||||
WHERE d.status = 'l'
|
||||
AND DATE_FORMAT(d.export_date, '%Y-%m') = :m
|
||||
GROUP BY d.user_id, username
|
||||
ORDER BY parts DESC
|
||||
");
|
||||
$stmtU->execute(['m' => $month]);
|
||||
$byUser = $stmtU->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Normalizza tipi
|
||||
foreach ($byClient as &$r) {
|
||||
$r['idclient'] = $r['idclient'] !== null ? (int)$r['idclient'] : null;
|
||||
$r['tot'] = (int)$r['tot'];
|
||||
$r['parts'] = (int)$r['parts'];
|
||||
$nm = $clientNames[$r['idclient']] ?? '';
|
||||
$r['clientname'] = $nm !== '' ? $nm : null;
|
||||
}
|
||||
unset($r);
|
||||
foreach ($byUser as &$r) {
|
||||
$r['user_id'] = $r['user_id'] !== null ? (int)$r['user_id'] : null;
|
||||
$r['tot'] = (int)$r['tot'];
|
||||
$r['parts'] = (int)$r['parts'];
|
||||
if (isset($r['username'])) $r['username'] = $r['username'] !== null && $r['username'] !== '' ? $r['username'] : null;
|
||||
}
|
||||
unset($r);
|
||||
|
||||
// --- Totale COMMESSE del mese ---
|
||||
$totExport = array_sum(array_column($byClient, 'tot'));
|
||||
|
||||
// --- Totale PARTI/CAMPIONI del mese (status = 'l') ---
|
||||
$stmtP = $pdo->prepare("
|
||||
SELECT COUNT(*)
|
||||
FROM identification_parts ip
|
||||
INNER JOIN datadb d ON d.iddatadb = ip.iddatadb
|
||||
WHERE d.status = 'l'
|
||||
AND DATE_FORMAT(d.export_date, '%Y-%m') = :m
|
||||
");
|
||||
$stmtP->execute(['m' => $month]);
|
||||
$totParts = (int)$stmtP->fetchColumn();
|
||||
|
||||
// --- Totale COMMESSE dell'ANNO del mese selezionato ---
|
||||
$stmtY = $pdo->prepare("
|
||||
SELECT COUNT(*)
|
||||
FROM datadb d
|
||||
WHERE d.status = 'l'
|
||||
AND YEAR(d.export_date) = :y
|
||||
");
|
||||
$stmtY->execute(['y' => $year]);
|
||||
$totExportYear = (int)$stmtY->fetchColumn();
|
||||
|
||||
// --- Totale PARTI dell'ANNO del mese selezionato ---
|
||||
$stmtYP = $pdo->prepare("
|
||||
SELECT COUNT(*)
|
||||
FROM identification_parts ip
|
||||
INNER JOIN datadb d ON d.iddatadb = ip.iddatadb
|
||||
WHERE d.status = 'l'
|
||||
AND YEAR(d.export_date) = :y
|
||||
");
|
||||
$stmtYP->execute(['y' => $year]);
|
||||
$totPartsYear = (int)$stmtYP->fetchColumn();
|
||||
|
||||
// --- Campioni LIMS: somma dei total_samples di tutti i giorni del mese ---
|
||||
$stmtL = $pdo->prepare("
|
||||
SELECT COALESCE(SUM(total_samples), 0)
|
||||
FROM lims_daily_samples
|
||||
WHERE DATE_FORMAT(sample_date, '%Y-%m') = :m
|
||||
");
|
||||
$stmtL->execute(['m' => $month]);
|
||||
$limsSamples = (int)$stmtL->fetchColumn();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'month' => $month,
|
||||
'year' => (int)$year,
|
||||
'totals' => ['export' => (int)$totExport, 'parts' => $totParts],
|
||||
'totalsYear' => ['export' => $totExportYear, 'parts' => $totPartsYear],
|
||||
'byClient' => $byClient,
|
||||
'byUser' => $byUser,
|
||||
'limsSamples' => $limsSamples,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
error_log('Stats export LIMS monthly error: ' . $e->getMessage());
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -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()]);
|
||||
}
|
||||
@@ -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);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user