Compare commits
325
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 | ||
|
|
dab8d9aebf | ||
|
|
375a10a678 | ||
|
|
15990be884 | ||
|
|
c3a6dd73b6 | ||
|
|
44ed1186e0 | ||
|
|
9050cb1006 | ||
|
|
e6820fdb62 | ||
|
|
5da37a7836 | ||
|
|
c5f27cb69a | ||
|
|
1d81d6c996 | ||
|
|
0c72dbf5ae | ||
|
|
8455be04e1 | ||
|
|
e42d1b9c51 | ||
|
|
3e69e3c322 | ||
|
|
0eb4f7a2ad | ||
|
|
4f2cfc1930 | ||
|
|
df075dd76a | ||
|
|
6460454201 | ||
|
|
574ddbbd32 | ||
|
|
41f414db5c | ||
|
|
4a863e8c16 | ||
|
|
b431f1d4e9 | ||
|
|
f97b52f158 | ||
|
|
836fc055ec | ||
|
|
e8dd585df4 | ||
|
|
198b8c08ad | ||
|
|
28c467d55e | ||
|
|
56eee99a67 | ||
|
|
f514b3d2c7 | ||
|
|
a3eb0f0a57 | ||
|
|
aa355905d7 | ||
|
|
b38f3e1240 | ||
|
|
cbd0c5b68a | ||
|
|
f3e5cb4ffd | ||
|
|
0f0c6a04b7 | ||
|
|
813bd66f96 | ||
|
|
ce00247d1c | ||
|
|
3a7dd266c8 | ||
|
|
ba8dc4c721 | ||
|
|
cfbbc36116 | ||
|
|
50d578eea1 | ||
|
|
6b0d2aa9b9 | ||
|
|
67bbd9bbbb | ||
|
|
fa7997c980 | ||
|
|
66be442eb6 | ||
|
|
19a2d6e3f7 | ||
|
|
a15ab08576 | ||
|
|
f71e8a56b5 | ||
|
|
cb38bfb75a | ||
|
|
28a708dad3 | ||
|
|
6b9cf20ab9 | ||
|
|
d40fc7d177 | ||
|
|
b812563023 | ||
|
|
39a821357e | ||
|
|
53c223ea5f | ||
|
|
9775a12d4a | ||
|
|
7dfb935e33 | ||
|
|
abb4200215 | ||
|
|
0be7109df4 | ||
|
|
578671e013 | ||
|
|
d24836e2b1 | ||
|
|
d983659000 | ||
|
|
7c5aa7734f | ||
|
|
4f0dbc7e91 | ||
|
|
fb09f033ae | ||
|
|
8bae2d7008 | ||
|
|
7463fc6726 | ||
|
|
0bc2ff7e9d | ||
|
|
aa1c32b7ed | ||
|
|
c573a46318 | ||
|
|
bf18a904bd | ||
|
|
b3ce489348 | ||
|
|
08b89e01cc | ||
|
|
3e66d67dc5 | ||
|
|
490786731a | ||
|
|
e96f538a47 | ||
|
|
cc96ecb67f | ||
|
|
4cf03ae742 | ||
|
|
7a486e9dcf | ||
|
|
2a7b1fae17 | ||
|
|
a05d9fed2b | ||
|
|
65170a0a7c | ||
|
|
223688c372 | ||
|
|
b562eb4033 | ||
|
|
0645a0c675 | ||
|
|
a02a6b2c4c | ||
|
|
45dd8d6907 | ||
|
|
755f6812d4 | ||
|
|
5e59ae2162 | ||
|
|
71a19144c8 | ||
|
|
381a05341b | ||
|
|
5a58decd40 | ||
|
|
eb21910ef3 | ||
|
|
5dedc779df | ||
|
|
f4e0074a73 | ||
|
|
9c850e4ea6 | ||
|
|
f300811341 | ||
|
|
48387a9945 | ||
|
|
0e90db8219 | ||
|
|
eaf70d5a46 | ||
|
|
cdd6551e9c | ||
|
|
1b97bf4362 | ||
|
|
c516589483 | ||
|
|
817bbadf22 | ||
|
|
1f27bc48d4 | ||
|
|
c9fba48d88 | ||
|
|
70d4c0759e | ||
|
|
5d7880160a | ||
|
|
bbe74d1529 | ||
|
|
93930227a2 | ||
|
|
2598a4c91b | ||
|
|
5e677a8b9c | ||
|
|
540c44d89a | ||
|
|
2ee9f2ecb1 | ||
|
|
c9122774b1 | ||
|
|
1fed113c5c | ||
|
|
e3994d6f9f | ||
|
|
9af0df3cca | ||
|
|
48d2a3ff42 | ||
|
|
cf44e67922 | ||
|
|
35021e9d9b | ||
|
|
497ebda65a | ||
|
|
b0024edb70 | ||
|
|
b9852ba226 | ||
|
|
407d6884a1 | ||
|
|
1a4beadbb2 | ||
|
|
4bb0445cff | ||
|
|
73dd8f4ce8 | ||
|
|
bdc4e0e60c | ||
|
|
ef8f2d8000 | ||
|
|
74c9a1d02a | ||
|
|
cd3bccd183 | ||
|
|
78154e43a9 | ||
|
|
9fe9243e60 | ||
|
|
a8330d4aba | ||
|
|
82d6a2ee18 | ||
|
|
d8eddb3aa5 | ||
|
|
f60dc64b2d | ||
|
|
4e4cae1df8 | ||
|
|
8838edf3a1 | ||
|
|
e75be99e43 | ||
|
|
a482d975da | ||
|
|
598a2cc84c | ||
|
|
6ec0c2062e | ||
|
|
5eb5bd1613 | ||
|
|
03771e3ca8 | ||
|
|
03642fdfab | ||
|
|
f6ea17388c | ||
|
|
1c2b4ab7a6 | ||
|
|
31cb23b00e | ||
|
|
d29563d20d | ||
|
|
82af925ac1 | ||
|
|
5d8360dd87 | ||
|
|
683073c244 | ||
|
|
8d6fe92481 | ||
|
|
dbc66723a6 | ||
|
|
218fc14462 | ||
|
|
29e4b41874 | ||
|
|
eef9ae8d36 | ||
|
|
68c867a3f4 | ||
|
|
a9827e4e81 | ||
|
|
b51936f784 | ||
|
|
15b6f38e8b | ||
|
|
12c6cc5f95 | ||
|
|
a0b12463c0 | ||
|
|
07ddcafd3f | ||
|
|
7843d4b1fc | ||
|
|
4eae855e23 | ||
|
|
c709f64a17 | ||
|
|
d5f0690f59 | ||
|
|
6bbd3fcae9 | ||
|
|
9e19e9e1d4 | ||
|
|
7caee9c994 | ||
|
|
f8320315f7 | ||
|
|
7397d86bc2 | ||
|
|
2deb1f101a | ||
|
|
ed4467337f | ||
|
|
864714d198 | ||
|
|
33aacfb469 | ||
|
|
e0e262fd32 | ||
|
|
5d6302fa9c | ||
|
|
3da8ff81c9 | ||
|
|
a36dd02771 | ||
|
|
0a6fb98476 | ||
|
|
57ddd4bb5a | ||
|
|
df5e6d5656 | ||
|
|
78495880ca | ||
|
|
960832efb1 | ||
|
|
447a0d1dea | ||
|
|
5b47416841 | ||
|
|
3e4a627ca7 | ||
|
|
420b0a0405 | ||
|
|
b39d601ec9 | ||
|
|
89d13699b4 | ||
|
|
9826331545 | ||
|
|
9d8718d110 | ||
|
|
16e00f8573 | ||
|
|
baf3f6da32 | ||
|
|
62bf4ebd92 | ||
|
|
6e465e3010 | ||
|
|
8b08969c69 | ||
|
|
25d4519684 | ||
|
|
34d4dc8660 | ||
|
|
1510ef03f1 | ||
|
|
ce8c95921f | ||
|
|
095a6ae879 | ||
|
|
296143016a | ||
|
|
412dce8941 | ||
|
|
586226ceaf | ||
|
|
ac09d8d0eb | ||
|
|
33e3ae059d | ||
|
|
3aa2504f3c | ||
|
|
c1a396f246 | ||
|
|
a45ba1c8b3 | ||
|
|
7a944a73f7 | ||
|
|
71595cc8de | ||
|
|
f89dbd0c23 | ||
|
|
9ba859e15b | ||
|
|
672e448e9a | ||
|
|
0749032fbc |
@@ -1,47 +0,0 @@
|
||||
APP_ENV=production
|
||||
APP_DEBUG=true
|
||||
APP_KEY=base64:C+sutHm6xP5sE4QXhoZFhYjArlVN11s2mDU1F8beUkM=
|
||||
APP_URL=http://vanguard.test
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST="localhost"
|
||||
DB_DATABASE="trfcertest"
|
||||
DB_USERNAME="solocla"
|
||||
DB_PASSWORD="!Massarosa2"
|
||||
DB_PREFIX="auth_"
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=file
|
||||
QUEUE_DRIVER=sync
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=mail
|
||||
MAIL_FROM_NAME=Vanguard
|
||||
MAIL_FROM_ADDRESS=vanguard@test.dev
|
||||
MAIL_HOST=smtp.mailtrap.io
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
PUSHER_APP_CLUSTER=mt1
|
||||
|
||||
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
||||
# Credenziali API VisualLims
|
||||
API_BASE_URL=https://93.43.5.102/limsapi
|
||||
API_USERNAME=WebApiUser
|
||||
API_PASSWORD=webapiuser01
|
||||
|
||||
BASE_URL=http://localhost:8000/userarea/
|
||||
+14
-7
@@ -1,16 +1,16 @@
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_KEY=base64:C+sutHm6xP5sE4QXhoZFhYjArlVN11s2mDU1F8beUkM=
|
||||
APP_URL=http://vanguard.test
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=localhost
|
||||
DB_DATABASE=vanguard
|
||||
DB_USERNAME=homestead
|
||||
DB_PASSWORD=secret
|
||||
DB_PREFIX=vg_
|
||||
DB_HOST="localhost"
|
||||
DB_DATABASE="xxxx"
|
||||
DB_USERNAME="xxxx"
|
||||
DB_PASSWORD="xxxxx"
|
||||
DB_PREFIX="auth_"
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=file
|
||||
@@ -39,3 +39,10 @@ PUSHER_APP_CLUSTER=mt1
|
||||
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
||||
# Credenziali API VisualLims
|
||||
SIMULATE_EXPORT_LIMS=true
|
||||
API_BASE_URL=https://bvcpsitaly-elims.com/limsapi
|
||||
API_USERNAME=WebApiUser
|
||||
API_PASSWORD=webapiuser01
|
||||
|
||||
BASE_URL=http://localhost:8000/userarea/
|
||||
+48
-28
@@ -1,54 +1,74 @@
|
||||
.DS_Store
|
||||
|
||||
/node_modules
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/vendor
|
||||
|
||||
/.idea
|
||||
/.fleet
|
||||
/.vscode
|
||||
/.vagrant
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
.env
|
||||
.phpunit.result.cache
|
||||
.php_cs.cache
|
||||
/documentation
|
||||
/.phpunit.cache
|
||||
|
||||
/public/hot
|
||||
/public/storage
|
||||
/public/build
|
||||
|
||||
/storage/*.key
|
||||
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
auth.json
|
||||
# File di debug e temporanei JSON e log
|
||||
|
||||
.phpunit.result.cache
|
||||
.php_cs.cache
|
||||
/.phpunit.cache
|
||||
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
/documentation
|
||||
|
||||
# --- Runtime / Debug (userarea) ---
|
||||
/public/userarea/*.json
|
||||
/public/userarea/*.log
|
||||
/public/userarea/*.txt
|
||||
|
||||
# File di log nella sottocartella class
|
||||
/public/userarea/*_response.json
|
||||
/public/userarea/customfield_values_response.json
|
||||
/public/userarea/error_log.txt
|
||||
/public/userarea/import_debug.log
|
||||
/public/userarea/last_url.txt
|
||||
/public/userarea/logaspi/
|
||||
/public/userarea/logs/api/
|
||||
/public/userarea/logs/api/**
|
||||
/public/userarea/logs/
|
||||
/public/userarea/logs/**
|
||||
/public/userarea/photostrf/
|
||||
/public/userarea/class/*.log
|
||||
/public/userarea/class/curl_auth_debug.log
|
||||
/public/userarea/class/curl_request_debug.log
|
||||
/public/userarea/schema_dettagli_response.json
|
||||
public/userarea/schemi_base_response.json
|
||||
|
||||
# File XLSX temporanei importati
|
||||
/public/userarea/imported_trf/*.xlsx
|
||||
/public/userarea/xlstemplates/*.xlsx
|
||||
|
||||
# Ignora cartelle di foto generate
|
||||
# Cartelle foto generate
|
||||
/public/photostrf/
|
||||
/public/photostrf/qrcodes/
|
||||
public/userarea/import_debug.log
|
||||
public/userarea/last_url.txt
|
||||
public/userarea/class/curl_auth_debug.log
|
||||
public/userarea/class/curl_request_debug.log
|
||||
public/userarea/last_url.txt
|
||||
public/userarea/class/curl_auth_debug.log
|
||||
public/userarea/class/curl_request_debug.log
|
||||
|
||||
# Ignora tutti i log
|
||||
# Ignora tutti i log ovunque
|
||||
*.log
|
||||
|
||||
public/userarea/cache/
|
||||
public/userarea/error_log.txt
|
||||
|
||||
# Ignora cartella photostrf in public/userarea
|
||||
/public/userarea/photostrf/
|
||||
public/userarea/customfield_values_response.json
|
||||
|
||||
# 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');
|
||||
}
|
||||
|
||||
@@ -28,10 +28,21 @@ class AppServiceProvider extends ServiceProvider
|
||||
\Illuminate\Database\Schema\Builder::defaultStringLength(191);
|
||||
|
||||
Factory::guessFactoryNamesUsing(function (string $modelName) {
|
||||
return 'Database\Factories\\'.class_basename($modelName).'Factory';
|
||||
return 'Database\Factories\\' . class_basename($modelName) . 'Factory';
|
||||
});
|
||||
|
||||
\Illuminate\Pagination\Paginator::useBootstrap();
|
||||
|
||||
// Register Microsoft Socialite driver
|
||||
$this->app->make('Laravel\Socialite\Contracts\Factory')->extend('microsoft', function ($app) {
|
||||
$config = $app['config']['services.microsoft'];
|
||||
return new \SocialiteProviders\Microsoft\Provider(
|
||||
$app['request'],
|
||||
$config['client_id'],
|
||||
$config['client_secret'],
|
||||
$config['redirect']
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,6 +34,9 @@ class EventServiceProvider extends ServiceProvider
|
||||
Verified::class => [
|
||||
ActivateUser::class,
|
||||
],
|
||||
\SocialiteProviders\Manager\SocialiteWasMapped::class => [
|
||||
\SocialiteProviders\Microsoft\MicrosoftExtendSocialite::class,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
*/
|
||||
|
||||
$app = new Illuminate\Foundation\Application(
|
||||
realpath(__DIR__.'/../')
|
||||
realpath(__DIR__ . '/../')
|
||||
);
|
||||
|
||||
/*
|
||||
|
||||
+4
-2
@@ -38,12 +38,14 @@
|
||||
"laravel/fortify": "^1.21",
|
||||
"laravel/framework": "^11.0",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/socialite": "^5.0",
|
||||
"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",
|
||||
"vanguardapp/announcements": "^6.0",
|
||||
|
||||
Generated
+793
-23
@@ -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": "ef3e05e7260284f5b7c7b4b6f93b252b",
|
||||
"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",
|
||||
@@ -2240,16 +2564,16 @@
|
||||
},
|
||||
{
|
||||
"name": "laravel/socialite",
|
||||
"version": "v5.15.1",
|
||||
"version": "v5.16.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/socialite.git",
|
||||
"reference": "cc02625f0bd1f95dc3688eb041cce0f1e709d029"
|
||||
"reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/socialite/zipball/cc02625f0bd1f95dc3688eb041cce0f1e709d029",
|
||||
"reference": "cc02625f0bd1f95dc3688eb041cce0f1e709d029",
|
||||
"url": "https://api.github.com/repos/laravel/socialite/zipball/40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf",
|
||||
"reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2271,16 +2595,16 @@
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "5.x-dev"
|
||||
},
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Laravel\\Socialite\\SocialiteServiceProvider"
|
||||
],
|
||||
"aliases": {
|
||||
"Socialite": "Laravel\\Socialite\\Facades\\Socialite"
|
||||
}
|
||||
},
|
||||
"providers": [
|
||||
"Laravel\\Socialite\\SocialiteServiceProvider"
|
||||
]
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-master": "5.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@@ -2308,7 +2632,7 @@
|
||||
"issues": "https://github.com/laravel/socialite/issues",
|
||||
"source": "https://github.com/laravel/socialite"
|
||||
},
|
||||
"time": "2024-06-28T20:09:34+00:00"
|
||||
"time": "2024-09-03T09:46:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/tinker",
|
||||
@@ -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,218 @@
|
||||
],
|
||||
"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",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/SocialiteProviders/Manager.git",
|
||||
"reference": "8180ec14bef230ec2351cff993d5d2d7ca470ef4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/8180ec14bef230ec2351cff993d5d2d7ca470ef4",
|
||||
"reference": "8180ec14bef230ec2351cff993d5d2d7ca470ef4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0",
|
||||
"laravel/socialite": "^5.5",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.2",
|
||||
"phpunit/phpunit": "^9.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"SocialiteProviders\\Manager\\ServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"SocialiteProviders\\Manager\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Andy Wendt",
|
||||
"email": "andy@awendt.com"
|
||||
},
|
||||
{
|
||||
"name": "Anton Komarev",
|
||||
"email": "a.komarev@cybercog.su"
|
||||
},
|
||||
{
|
||||
"name": "Miguel Piedrafita",
|
||||
"email": "soy@miguelpiedrafita.com"
|
||||
},
|
||||
{
|
||||
"name": "atymic",
|
||||
"email": "atymicq@gmail.com",
|
||||
"homepage": "https://atymic.dev"
|
||||
}
|
||||
],
|
||||
"description": "Easily add new or override built-in providers in Laravel Socialite.",
|
||||
"homepage": "https://socialiteproviders.com",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"manager",
|
||||
"oauth",
|
||||
"providers",
|
||||
"socialite"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/socialiteproviders/manager/issues",
|
||||
"source": "https://github.com/socialiteproviders/manager"
|
||||
},
|
||||
"time": "2025-02-24T19:33:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "socialiteproviders/microsoft",
|
||||
"version": "4.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/SocialiteProviders/Microsoft.git",
|
||||
"reference": "824ef97a4f6e3f363c21702b76676d54e8265573"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/SocialiteProviders/Microsoft/zipball/824ef97a4f6e3f363c21702b76676d54e8265573",
|
||||
"reference": "824ef97a4f6e3f363c21702b76676d54e8265573",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"firebase/php-jwt": "^6.8",
|
||||
"php": "^8.0",
|
||||
"socialiteproviders/manager": "^4.4"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"SocialiteProviders\\Microsoft\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Brian Faust",
|
||||
"email": "hello@brianfaust.de"
|
||||
}
|
||||
],
|
||||
"description": "Microsoft OAuth2 Provider for Laravel Socialite",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"microsoft",
|
||||
"oauth",
|
||||
"provider",
|
||||
"socialite"
|
||||
],
|
||||
"support": {
|
||||
"docs": "https://socialiteproviders.com/microsoft",
|
||||
"issues": "https://github.com/socialiteproviders/providers/issues",
|
||||
"source": "https://github.com/socialiteproviders/providers"
|
||||
},
|
||||
"time": "2025-07-06T00:25:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-package-tools",
|
||||
"version": "1.16.4",
|
||||
@@ -5187,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",
|
||||
@@ -5643,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",
|
||||
@@ -11230,6 +12000,6 @@
|
||||
"php": "^8.2.0",
|
||||
"ext-json": "*"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -228,6 +228,7 @@ return [
|
||||
Proengsoft\JsValidation\JsValidationServiceProvider::class,
|
||||
Anhskohbo\NoCaptcha\NoCaptchaServiceProvider::class,
|
||||
Laravel\Socialite\SocialiteServiceProvider::class,
|
||||
\SocialiteProviders\Manager\ServiceProvider::class,
|
||||
Webpatser\Countries\CountriesServiceProvider::class,
|
||||
Intervention\Image\ImageServiceProvider::class,
|
||||
Jenssegers\Agent\AgentServiceProvider::class,
|
||||
|
||||
+3
-3
@@ -11,9 +11,9 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
// 'social' => [
|
||||
// 'providers' => ['facebook', 'twitter', 'google'],
|
||||
// ],
|
||||
'social' => [
|
||||
'providers' => ['microsoft'],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
+9
-3
@@ -64,9 +64,15 @@ return [
|
||||
'redirect' => env('GOOGLE_CALLBACK_URI'),
|
||||
],
|
||||
|
||||
// 'authy' => [
|
||||
// 'key' => env('AUTHY_KEY'),
|
||||
// ],
|
||||
'microsoft' => [
|
||||
'client_id' => env('MICROSOFT_CLIENT_ID'),
|
||||
'client_secret' => env('MICROSOFT_CLIENT_SECRET'),
|
||||
'redirect' => env('MICROSOFT_REDIRECT_URI'),
|
||||
],
|
||||
|
||||
// 'authy' => [
|
||||
// 'key' => env('AUTHY_KEY'),
|
||||
// ],
|
||||
|
||||
'resend' => [
|
||||
'key' => env('RESEND_KEY'),
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
# DB LOCALE (Windows 11)
|
||||
url=jdbc:mysql://localhost:3306/trfcertest
|
||||
username=solocla
|
||||
password=!Massarosa2
|
||||
driver=com.mysql.cj.jdbc.Driver
|
||||
changeLogFile=liquibase/changelog/db.changelog-master.yaml
|
||||
@@ -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",
|
||||
@@ -32,3 +33,4 @@ $langdatatables = [
|
||||
"paginate_next" => "Next",
|
||||
"paginate_previous" => "Previous"
|
||||
];
|
||||
$quotationstitle = "Quotations";
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
|
||||
ini_set('display_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once(__DIR__ . '/include/headscript.php'); // Auth + $iduserlogin + DBHandlerSelect
|
||||
|
||||
try {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
throw new Exception('Invalid request method');
|
||||
}
|
||||
|
||||
$iddatadb = isset($_POST['iddatadb']) ? (int)$_POST['iddatadb'] : 0;
|
||||
|
||||
if ($iddatadb <= 0) {
|
||||
throw new Exception('Missing iddatadb');
|
||||
}
|
||||
|
||||
// ✅ Supporta sia singola stringa (con |) che array part_descriptions[]
|
||||
$parts = [];
|
||||
|
||||
if (isset($_POST['part_descriptions']) && is_array($_POST['part_descriptions'])) {
|
||||
$parts = $_POST['part_descriptions'];
|
||||
} else {
|
||||
$raw = isset($_POST['part_description']) ? (string)$_POST['part_description'] : '';
|
||||
// split su "|" per multi-part
|
||||
$parts = preg_split('/\s*\|\s*/', $raw);
|
||||
}
|
||||
|
||||
// normalizza: trim + rimuovi vuoti + dedup
|
||||
$cleanParts = [];
|
||||
foreach ($parts as $p) {
|
||||
$p = trim((string)$p);
|
||||
if ($p === '') continue;
|
||||
|
||||
// limita lunghezza come da DB varchar(255)
|
||||
if (mb_strlen($p) > 255) {
|
||||
$p = mb_substr($p, 0, 255);
|
||||
}
|
||||
$cleanParts[] = $p;
|
||||
}
|
||||
$cleanParts = array_values(array_unique($cleanParts));
|
||||
|
||||
if (count($cleanParts) === 0) {
|
||||
throw new Exception('Part description is empty');
|
||||
}
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
// prende il prossimo part_number per iddatadb
|
||||
$stmtMax = $pdo->prepare("SELECT COALESCE(MAX(part_number), 0) AS maxnum FROM identification_parts WHERE iddatadb = ?");
|
||||
$stmtMax->execute([$iddatadb]);
|
||||
$nextNumber = (int)$stmtMax->fetchColumn() + 1;
|
||||
|
||||
$stmtIns = $pdo->prepare("
|
||||
INSERT INTO identification_parts (iddatadb, part_number, part_description)
|
||||
VALUES (?, ?, ?)
|
||||
");
|
||||
|
||||
$insertedIds = [];
|
||||
|
||||
foreach ($cleanParts as $p) {
|
||||
$ok = $stmtIns->execute([$iddatadb, $nextNumber, $p]);
|
||||
if (!$ok) {
|
||||
throw new Exception('Insert failed');
|
||||
}
|
||||
$insertedIds[] = $pdo->lastInsertId();
|
||||
$nextNumber++;
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Parts added',
|
||||
'count' => count($insertedIds),
|
||||
'ids' => $insertedIds
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
exit;
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$templateId = intval($input['template_id'] ?? 0);
|
||||
$importRefFromClient = trim($input['importreferencecode'] ?? '');
|
||||
|
||||
if (!$templateId) {
|
||||
throw new Exception('Template ID missing');
|
||||
}
|
||||
|
||||
$userId = $iduserlogin ?? 1;
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
// Get default idclient from template
|
||||
$stmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
|
||||
$stmt->execute([$templateId]);
|
||||
$template = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$idclient = $template['idclient'] ?? null;
|
||||
|
||||
// Use provided importreferencecode (from filtered page) or generate new
|
||||
$importReferenceCode = $importRefFromClient !== '' ? $importRefFromClient : date('YmdHis') . '-' . uniqid();
|
||||
|
||||
// Insert empty record
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO datadb (templateid, user_id, status, idclient, importreferencecode, importdate)
|
||||
VALUES (?, ?, 'i', ?, ?, NOW())
|
||||
");
|
||||
$stmt->execute([$templateId, $userId, $idclient, $importReferenceCode]);
|
||||
$iddatadb = (int)$pdo->lastInsertId();
|
||||
|
||||
// Create import_data_details for all mappings (with auto_value support)
|
||||
$mappingStmt = $pdo->prepare("SELECT id, auto_value, manual_default, data_type FROM template_mapping WHERE template_id = ?");
|
||||
$mappingStmt->execute([$templateId]);
|
||||
$mappings = $mappingStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($mappings)) {
|
||||
$insertStmt = $pdo->prepare("INSERT INTO import_data_details (id, mapping_id, field_value) VALUES (?, ?, ?)");
|
||||
foreach ($mappings as $m) {
|
||||
$val = '';
|
||||
$auto = $m['auto_value'] ?? 'none';
|
||||
if ($auto === 'import_date') {
|
||||
$val = date('Y-m-d');
|
||||
} elseif ($auto === 'import_time') {
|
||||
$val = date('H:i');
|
||||
} elseif ($m['data_type'] === 'DATE' && ($m['manual_default'] ?? '') === 'today') {
|
||||
$val = date('Y-m-d');
|
||||
} elseif (!empty($m['manual_default'])) {
|
||||
$val = $m['manual_default'];
|
||||
}
|
||||
$insertStmt->execute([$iddatadb, $m['id'], $val]);
|
||||
}
|
||||
}
|
||||
|
||||
// Get user name
|
||||
$userStmt = $pdo->prepare("SELECT CONCAT(first_name, ' ', last_name) AS user_name FROM auth_users WHERE id = ?");
|
||||
$userStmt->execute([$userId]);
|
||||
$userName = $userStmt->fetchColumn() ?: '';
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'iddatadb' => $iddatadb,
|
||||
'importreferencecode' => $importReferenceCode,
|
||||
'user_name' => $userName,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,913 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
let analysisMatriciMap = {};
|
||||
let analysisLoadedCache = {};
|
||||
let analysisAssignedState = {};
|
||||
let analysisSelectedState = {};
|
||||
|
||||
function loadAnalysisMatrixNames() {
|
||||
return $.ajax({
|
||||
url: "search_matrici.php?full_list=1",
|
||||
method: "GET",
|
||||
dataType: "json",
|
||||
})
|
||||
.done(function (data) {
|
||||
analysisMatriciMap = {};
|
||||
|
||||
(data.value || []).forEach(function (matrice) {
|
||||
analysisMatriciMap[String(matrice.IdMatrice)] =
|
||||
matrice.NomeMatrice || "#" + matrice.IdMatrice;
|
||||
});
|
||||
|
||||
applyAnalysisMatrixNames();
|
||||
})
|
||||
.fail(function () {
|
||||
analysisMatriciMap = {};
|
||||
applyAnalysisMatrixNames();
|
||||
});
|
||||
}
|
||||
|
||||
function applyAnalysisMatrixNames() {
|
||||
const modal = document.getElementById("analysisModal");
|
||||
if (!modal) return;
|
||||
|
||||
modal.querySelectorAll(".analysis-matrix-item").forEach((item) => {
|
||||
const matrixId = item.getAttribute("data-matrix-id");
|
||||
const nameEl = item.querySelector(".analysis-matrix-name");
|
||||
|
||||
let matrixName = "No Matrix";
|
||||
if (matrixId && matrixId !== "NO_MATRIX") {
|
||||
matrixName =
|
||||
analysisMatriciMap[String(matrixId)] || "#" + matrixId;
|
||||
}
|
||||
|
||||
item.setAttribute("data-matrix-name", matrixName);
|
||||
|
||||
if (nameEl) {
|
||||
nameEl.textContent = matrixName;
|
||||
}
|
||||
});
|
||||
|
||||
modal.querySelectorAll(".analysis-part-matrix-name").forEach((el) => {
|
||||
const matrixId = el.getAttribute("data-matrix-id");
|
||||
|
||||
if (!matrixId || matrixId === "NO_MATRIX") {
|
||||
el.textContent = "No Matrix";
|
||||
return;
|
||||
}
|
||||
|
||||
el.textContent =
|
||||
analysisMatriciMap[String(matrixId)] || "#" + matrixId;
|
||||
});
|
||||
}
|
||||
function readInitialAssignedAnalyses() {
|
||||
const jsonEl = document.getElementById("analysisAssignedInitialData");
|
||||
if (!jsonEl) {
|
||||
analysisAssignedState = {};
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
analysisAssignedState =
|
||||
JSON.parse(jsonEl.textContent || "{}") || {};
|
||||
} catch (e) {
|
||||
analysisAssignedState = {};
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedPartIds() {
|
||||
const modal = document.getElementById("analysisModal");
|
||||
if (!modal) return [];
|
||||
|
||||
return Array.from(
|
||||
modal.querySelectorAll(".analysis-part-checkbox:checked"),
|
||||
).map((el) => String(el.value));
|
||||
}
|
||||
|
||||
function getCurrentSelectedAnalysisRecordKeys() {
|
||||
return Object.keys(analysisSelectedState).filter(function (key) {
|
||||
return analysisSelectedState[key] === true;
|
||||
});
|
||||
}
|
||||
|
||||
function renderAssignedAnalysesForPart(partId) {
|
||||
const container = document.getElementById(
|
||||
"analysisAssignedListPart" + partId,
|
||||
);
|
||||
if (!container) return;
|
||||
|
||||
const items = Array.isArray(analysisAssignedState[String(partId)])
|
||||
? analysisAssignedState[String(partId)]
|
||||
: [];
|
||||
|
||||
if (!items.length) {
|
||||
container.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = items
|
||||
.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 || "";
|
||||
|
||||
return `
|
||||
<div class="analysis-assigned-chip" data-recordkey="${escapeHtml(recordKey)}">
|
||||
<div class="analysis-assigned-chip-text">
|
||||
<div class="analysis-assigned-chip-title">${escapeHtml(title)}</div>
|
||||
${method ? `<div class="analysis-assigned-chip-method">${escapeHtml(method)}</div>` : ""}
|
||||
</div>
|
||||
<button type="button"
|
||||
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>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderAssignedAnalysesForSelectedParts() {
|
||||
const modal = document.getElementById("analysisModal");
|
||||
if (!modal) return;
|
||||
|
||||
modal.querySelectorAll(".analysis-part-item").forEach(function (item) {
|
||||
const partId = item.getAttribute("data-part-id");
|
||||
renderAssignedAnalysesForPart(partId);
|
||||
});
|
||||
}
|
||||
|
||||
function syncSelectedAnalysisRows() {
|
||||
const modal = document.getElementById("analysisModal");
|
||||
if (!modal) return;
|
||||
|
||||
modal
|
||||
.querySelectorAll(".analysis-analysis-item")
|
||||
.forEach(function (item) {
|
||||
const recordKey = item.getAttribute("data-recordkey") || "";
|
||||
if (recordKey && analysisSelectedState[recordKey]) {
|
||||
item.classList.add("analysis-selected");
|
||||
} else {
|
||||
item.classList.remove("analysis-selected");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildAnalysisPayloadFromRow(rowEl) {
|
||||
return {
|
||||
analysis_recordkey: rowEl.getAttribute("data-recordkey") || "",
|
||||
analysis_name: rowEl.getAttribute("data-analysis-name") || "",
|
||||
analysis_method: rowEl.getAttribute("data-analysis-method") || "",
|
||||
analysis_level: rowEl.getAttribute("data-analysis-level") || "",
|
||||
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,
|
||||
newId,
|
||||
) {
|
||||
const key = String(partId);
|
||||
if (!Array.isArray(analysisAssignedState[key])) {
|
||||
analysisAssignedState[key] = [];
|
||||
}
|
||||
|
||||
{
|
||||
analysisAssignedState[key].push({
|
||||
id: newId || null,
|
||||
part_id: parseInt(partId, 10),
|
||||
iddatadb: iddatadb || null,
|
||||
idmatrice: idmatrice || null,
|
||||
analysis_recordkey: payload.analysis_recordkey,
|
||||
analysis_name: payload.analysis_name,
|
||||
analysis_method: payload.analysis_method,
|
||||
analysis_level:
|
||||
payload.analysis_level !== ""
|
||||
? parseInt(payload.analysis_level, 10)
|
||||
: 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, index) {
|
||||
const key = String(partId);
|
||||
if (!Array.isArray(analysisAssignedState[key])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const i = parseInt(index, 10);
|
||||
if (!isNaN(i) && i >= 0 && i < analysisAssignedState[key].length) {
|
||||
analysisAssignedState[key].splice(i, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
const found = analysisAssignedState[key].findIndex(function (item) {
|
||||
return item.analysis_recordkey === recordKey;
|
||||
});
|
||||
if (found !== -1) {
|
||||
analysisAssignedState[key].splice(found, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function saveAnalysisAssociation(partId, payload, callback) {
|
||||
const modal = document.getElementById("analysisModal");
|
||||
if (!modal) return;
|
||||
|
||||
const iddatadb =
|
||||
modal.querySelector("#analysisModalIddatadb")?.value || "";
|
||||
const partItem = modal.querySelector(
|
||||
'.analysis-part-item[data-part-id="' + partId + '"]',
|
||||
);
|
||||
const idmatrice = partItem
|
||||
? partItem.getAttribute("data-matrix-id") || ""
|
||||
: "";
|
||||
|
||||
$.ajax({
|
||||
url: "save_part_analysis.php",
|
||||
method: "POST",
|
||||
dataType: "json",
|
||||
data: {
|
||||
part_id: partId,
|
||||
iddatadb: iddatadb,
|
||||
idmatrice: idmatrice !== "NO_MATRIX" ? idmatrice : "",
|
||||
analysis_recordkey: payload.analysis_recordkey,
|
||||
analysis_name: payload.analysis_name,
|
||||
analysis_method: payload.analysis_method,
|
||||
analysis_level: payload.analysis_level,
|
||||
is_web_selectable: payload.is_web_selectable,
|
||||
is_accredited: payload.is_accredited,
|
||||
analysis_price: payload.analysis_price,
|
||||
},
|
||||
})
|
||||
.done(function (resp) {
|
||||
addAnalysisToLocalState(
|
||||
partId,
|
||||
payload,
|
||||
iddatadb,
|
||||
idmatrice !== "NO_MATRIX" ? idmatrice : null,
|
||||
resp && resp.id ? resp.id : null,
|
||||
);
|
||||
renderAssignedAnalysesForPart(partId);
|
||||
if (typeof callback === "function") callback(true);
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
let message = "Error saving analysis association";
|
||||
if (xhr && xhr.responseJSON && xhr.responseJSON.message) {
|
||||
message = xhr.responseJSON.message;
|
||||
}
|
||||
alert(message);
|
||||
if (typeof callback === "function") callback(false);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteAnalysisAssociation(
|
||||
partId,
|
||||
recordKey,
|
||||
rowId,
|
||||
index,
|
||||
callback,
|
||||
) {
|
||||
$.ajax({
|
||||
url: "delete_part_analysis.php",
|
||||
method: "POST",
|
||||
dataType: "json",
|
||||
data: {
|
||||
part_id: partId,
|
||||
analysis_recordkey: recordKey,
|
||||
row_id: rowId || "",
|
||||
},
|
||||
})
|
||||
.done(function () {
|
||||
removeAnalysisFromLocalState(partId, recordKey, index);
|
||||
renderAssignedAnalysesForPart(partId);
|
||||
if (typeof callback === "function") callback(true);
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
let message = "Error deleting analysis association";
|
||||
if (xhr && xhr.responseJSON && xhr.responseJSON.message) {
|
||||
message = xhr.responseJSON.message;
|
||||
}
|
||||
alert(message);
|
||||
if (typeof callback === "function") callback(false);
|
||||
});
|
||||
}
|
||||
function setAnalysisLoadingState(isLoading) {
|
||||
const modal = document.getElementById("analysisModal");
|
||||
if (!modal) return;
|
||||
|
||||
const loadingEl = modal.querySelector("#analysisLoadingBox");
|
||||
if (!loadingEl) return;
|
||||
|
||||
if (isLoading) {
|
||||
loadingEl.classList.remove("d-none");
|
||||
} else {
|
||||
loadingEl.classList.add("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
function showAnalysisError(message) {
|
||||
const modal = document.getElementById("analysisModal");
|
||||
if (!modal) return;
|
||||
|
||||
const errorEl = modal.querySelector("#analysisErrorBox");
|
||||
const emptyEl = modal.querySelector("#analysisEmptyBox");
|
||||
const listEl = modal.querySelector("#analysisList");
|
||||
|
||||
if (listEl) listEl.innerHTML = "";
|
||||
if (emptyEl) emptyEl.classList.add("d-none");
|
||||
|
||||
if (errorEl) {
|
||||
errorEl.textContent = message || "Error loading analyses";
|
||||
errorEl.classList.remove("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
function showAnalysisEmpty(message) {
|
||||
const modal = document.getElementById("analysisModal");
|
||||
if (!modal) return;
|
||||
|
||||
const errorEl = modal.querySelector("#analysisErrorBox");
|
||||
const emptyEl = modal.querySelector("#analysisEmptyBox");
|
||||
const listEl = modal.querySelector("#analysisList");
|
||||
|
||||
if (listEl) listEl.innerHTML = "";
|
||||
if (errorEl) errorEl.classList.add("d-none");
|
||||
|
||||
if (emptyEl) {
|
||||
emptyEl.textContent =
|
||||
message || "No analyses found for this matrix";
|
||||
emptyEl.classList.remove("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function renderAnalysesList(analyses) {
|
||||
const modal = document.getElementById("analysisModal");
|
||||
if (!modal) return;
|
||||
|
||||
const errorEl = modal.querySelector("#analysisErrorBox");
|
||||
const emptyEl = modal.querySelector("#analysisEmptyBox");
|
||||
const listEl = modal.querySelector("#analysisList");
|
||||
|
||||
if (!listEl) return;
|
||||
|
||||
if (errorEl) errorEl.classList.add("d-none");
|
||||
|
||||
if (!Array.isArray(analyses) || analyses.length === 0) {
|
||||
showAnalysisEmpty("No analyses found for this matrix");
|
||||
return;
|
||||
}
|
||||
|
||||
if (emptyEl) emptyEl.classList.add("d-none");
|
||||
|
||||
listEl.innerHTML = analyses
|
||||
.map(function (item) {
|
||||
const recordKey = item.RecordKey || "";
|
||||
const analysisName =
|
||||
item.NomeAnalisiTraduzione ||
|
||||
item.NomeAnalisi ||
|
||||
"Unnamed analysis";
|
||||
const methodName = item.MetodoNome || "";
|
||||
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 +
|
||||
" " +
|
||||
methodName
|
||||
).toLowerCase();
|
||||
|
||||
let badges = "";
|
||||
if (selectable) {
|
||||
badges += '<span class="badge bg-success">Web</span>';
|
||||
} else {
|
||||
badges += '<span class="badge bg-secondary">Not web</span>';
|
||||
}
|
||||
|
||||
if (accredited) {
|
||||
badges +=
|
||||
'<span class="badge bg-info text-dark">Accredited</span>';
|
||||
}
|
||||
|
||||
if (level !== "") {
|
||||
badges +=
|
||||
'<span class="badge bg-light text-dark border">Level ' +
|
||||
escapeHtml(level) +
|
||||
"</span>";
|
||||
}
|
||||
|
||||
if (!hasPrice) {
|
||||
badges += '<span class="badge bg-danger">No price</span>';
|
||||
}
|
||||
|
||||
return `
|
||||
<div
|
||||
class="list-group-item analysis-analysis-item"
|
||||
data-recordkey="${escapeHtml(recordKey)}"
|
||||
data-analysis-name="${escapeHtml(analysisName)}"
|
||||
data-analysis-method="${escapeHtml(methodName)}"
|
||||
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>
|
||||
|
||||
${methodName ? `<div class="analysis-analysis-meta mt-1">${escapeHtml(methodName)}</div>` : ""}
|
||||
|
||||
<div class="analysis-analysis-badges mt-2">
|
||||
${badges}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
filterAnalysisList();
|
||||
syncSelectedAnalysisRows();
|
||||
}
|
||||
function filterAnalysisList() {
|
||||
const modal = document.getElementById("analysisModal");
|
||||
if (!modal) return;
|
||||
|
||||
const webOnlyEl = modal.querySelector("#analysisWebOnly");
|
||||
const searchEl = modal.querySelector("#analysisSearchInput");
|
||||
const items = modal.querySelectorAll(".analysis-analysis-item");
|
||||
const emptyEl = modal.querySelector("#analysisEmptyBox");
|
||||
const errorEl = modal.querySelector("#analysisErrorBox");
|
||||
|
||||
const webOnly = true;
|
||||
const searchValue = searchEl ? searchEl.value.trim().toLowerCase() : "";
|
||||
|
||||
let visibleCount = 0;
|
||||
|
||||
items.forEach((item) => {
|
||||
const isWeb = item.getAttribute("data-web") === "1";
|
||||
const searchText = (
|
||||
item.getAttribute("data-search") || ""
|
||||
).toLowerCase();
|
||||
|
||||
let visible = true;
|
||||
|
||||
if (webOnly && !isWeb) {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
if (visible && searchValue && !searchText.includes(searchValue)) {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
item.style.display = visible ? "" : "none";
|
||||
|
||||
if (visible) {
|
||||
visibleCount++;
|
||||
}
|
||||
});
|
||||
|
||||
if (errorEl) {
|
||||
errorEl.classList.add("d-none");
|
||||
}
|
||||
|
||||
if (emptyEl) {
|
||||
if (items.length === 0) {
|
||||
emptyEl.textContent = "No analyses found for this matrix";
|
||||
emptyEl.classList.remove("d-none");
|
||||
} else if (visibleCount === 0) {
|
||||
emptyEl.textContent = "No analyses match the current filters";
|
||||
emptyEl.classList.remove("d-none");
|
||||
} else {
|
||||
emptyEl.classList.add("d-none");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadAnalysesByMatrix(matrixId) {
|
||||
const modal = document.getElementById("analysisModal");
|
||||
if (!modal) return;
|
||||
|
||||
if (!matrixId || matrixId === "NO_MATRIX") {
|
||||
showAnalysisEmpty("No matrix selected");
|
||||
return;
|
||||
}
|
||||
|
||||
const listEl = modal.querySelector("#analysisList");
|
||||
const errorEl = modal.querySelector("#analysisErrorBox");
|
||||
const emptyEl = modal.querySelector("#analysisEmptyBox");
|
||||
|
||||
if (listEl) listEl.innerHTML = "";
|
||||
if (errorEl) errorEl.classList.add("d-none");
|
||||
if (emptyEl) {
|
||||
emptyEl.textContent = "";
|
||||
emptyEl.classList.add("d-none");
|
||||
}
|
||||
|
||||
const cacheKey = String(matrixId) + "_WEB_ONLY";
|
||||
|
||||
if (analysisLoadedCache[cacheKey]) {
|
||||
renderAnalysesList(analysisLoadedCache[cacheKey]);
|
||||
return;
|
||||
}
|
||||
|
||||
setAnalysisLoadingState(true);
|
||||
|
||||
$.ajax({
|
||||
url: "get_analisi_matrice_filter.php",
|
||||
method: "GET",
|
||||
dataType: "json",
|
||||
data: {
|
||||
id_matrice: matrixId,
|
||||
web_only: 1,
|
||||
},
|
||||
})
|
||||
.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 ||
|
||||
item.SelezionabileSuWeb === "1"
|
||||
);
|
||||
})
|
||||
: [];
|
||||
|
||||
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);
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
let message = "Error loading analyses";
|
||||
if (xhr && xhr.responseJSON && xhr.responseJSON.error) {
|
||||
message = xhr.responseJSON.error;
|
||||
}
|
||||
showAnalysisError(message);
|
||||
})
|
||||
.always(function () {
|
||||
setAnalysisLoadingState(false);
|
||||
});
|
||||
}
|
||||
|
||||
function updateSelectedPartsInfo() {
|
||||
const modal = document.getElementById("analysisModal");
|
||||
if (!modal) return;
|
||||
|
||||
const checked = modal.querySelectorAll(
|
||||
".analysis-part-checkbox:checked",
|
||||
);
|
||||
const ids = Array.from(checked).map((el) => el.value);
|
||||
|
||||
const countEl = modal.querySelector("#analysisSelectedPartsCount");
|
||||
const idsEl = modal.querySelector("#analysisSelectedPartsIds");
|
||||
|
||||
if (countEl) {
|
||||
countEl.textContent = `${ids.length} selected`;
|
||||
}
|
||||
|
||||
if (idsEl) {
|
||||
idsEl.textContent = ids.length ? ids.join(", ") : "-";
|
||||
}
|
||||
|
||||
modal.querySelectorAll(".analysis-part-item").forEach((item) => {
|
||||
const checkbox = item.querySelector(".analysis-part-checkbox");
|
||||
if (checkbox && checkbox.checked) {
|
||||
item.classList.add("part-checked");
|
||||
} else {
|
||||
item.classList.remove("part-checked");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function selectPartsByMatrix(matrixId, matrixLabel) {
|
||||
const modal = document.getElementById("analysisModal");
|
||||
if (!modal) return;
|
||||
|
||||
const partItems = modal.querySelectorAll(".analysis-part-item");
|
||||
const matrixLabelEl = modal.querySelector("#analysisCurrentMatrix");
|
||||
|
||||
partItems.forEach((item) => {
|
||||
const itemMatrixId = item.getAttribute("data-matrix-id");
|
||||
const checkbox = item.querySelector(".analysis-part-checkbox");
|
||||
|
||||
item.classList.remove("matrix-active");
|
||||
|
||||
if (itemMatrixId === String(matrixId)) {
|
||||
item.classList.add("matrix-active");
|
||||
if (checkbox) checkbox.checked = true;
|
||||
} else {
|
||||
if (checkbox) checkbox.checked = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (matrixLabelEl) {
|
||||
matrixLabelEl.textContent = matrixLabel || "-";
|
||||
}
|
||||
analysisSelectedState = {};
|
||||
|
||||
const selectedPartIds = getSelectedPartIds();
|
||||
selectedPartIds.forEach(function (partId) {
|
||||
const assigned = Array.isArray(
|
||||
analysisAssignedState[String(partId)],
|
||||
)
|
||||
? analysisAssignedState[String(partId)]
|
||||
: [];
|
||||
|
||||
assigned.forEach(function (item) {
|
||||
if (item.analysis_recordkey) {
|
||||
analysisSelectedState[item.analysis_recordkey] = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
updateSelectedPartsInfo();
|
||||
loadAnalysesByMatrix(matrixId);
|
||||
}
|
||||
|
||||
function clearAnalysisSelection() {
|
||||
const modal = document.getElementById("analysisModal");
|
||||
if (!modal) return;
|
||||
|
||||
modal.querySelectorAll(".analysis-matrix-item").forEach((item) => {
|
||||
item.classList.remove("active");
|
||||
});
|
||||
|
||||
modal.querySelectorAll(".analysis-part-item").forEach((item) => {
|
||||
item.classList.remove("matrix-active", "part-checked");
|
||||
});
|
||||
|
||||
modal.querySelectorAll(".analysis-part-checkbox").forEach((cb) => {
|
||||
cb.checked = false;
|
||||
});
|
||||
|
||||
const matrixLabelEl = modal.querySelector("#analysisCurrentMatrix");
|
||||
if (matrixLabelEl) matrixLabelEl.textContent = "-";
|
||||
|
||||
const webOnlyEl = modal.querySelector("#analysisWebOnly");
|
||||
if (webOnlyEl) webOnlyEl.checked = false;
|
||||
|
||||
const searchEl = modal.querySelector("#analysisSearchInput");
|
||||
if (searchEl) searchEl.value = "";
|
||||
|
||||
const listEl = modal.querySelector("#analysisList");
|
||||
if (listEl) listEl.innerHTML = "";
|
||||
|
||||
showAnalysisEmpty("Select a matrix to load analyses");
|
||||
updateSelectedPartsInfo();
|
||||
}
|
||||
|
||||
function initAnalysisModal() {
|
||||
const modal = document.getElementById("analysisModal");
|
||||
if (!modal) return;
|
||||
|
||||
analysisLoadedCache = {};
|
||||
analysisSelectedState = {};
|
||||
readInitialAssignedAnalyses();
|
||||
renderAssignedAnalysesForSelectedParts();
|
||||
|
||||
modal.querySelectorAll(".analysis-matrix-item").forEach((btn) => {
|
||||
btn.addEventListener("click", function () {
|
||||
modal
|
||||
.querySelectorAll(".analysis-matrix-item")
|
||||
.forEach((x) => x.classList.remove("active"));
|
||||
this.classList.add("active");
|
||||
|
||||
const matrixId = this.getAttribute("data-matrix-id");
|
||||
const matrixLabel =
|
||||
this.getAttribute("data-matrix-name") ||
|
||||
this.textContent.trim();
|
||||
|
||||
selectPartsByMatrix(matrixId, matrixLabel);
|
||||
});
|
||||
});
|
||||
|
||||
modal.querySelectorAll(".analysis-part-checkbox").forEach((cb) => {
|
||||
cb.addEventListener("change", function () {
|
||||
updateSelectedPartsInfo();
|
||||
});
|
||||
});
|
||||
|
||||
const clearBtn = modal.querySelector("#analysisClearSelectionBtn");
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener("click", function () {
|
||||
clearAnalysisSelection();
|
||||
});
|
||||
}
|
||||
|
||||
// WEB only is now fixed by default
|
||||
|
||||
const searchEl = modal.querySelector("#analysisSearchInput");
|
||||
if (searchEl) {
|
||||
searchEl.addEventListener("input", function () {
|
||||
filterAnalysisList();
|
||||
});
|
||||
}
|
||||
modal.addEventListener("click", function (e) {
|
||||
const removeBtn = e.target.closest(".analysis-remove-btn");
|
||||
if (removeBtn) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const partId = removeBtn.getAttribute("data-part-id");
|
||||
const recordKey = removeBtn.getAttribute("data-recordkey");
|
||||
const rowId = removeBtn.getAttribute("data-row-id");
|
||||
const index = removeBtn.getAttribute("data-index");
|
||||
|
||||
deleteAnalysisAssociation(
|
||||
partId,
|
||||
recordKey,
|
||||
rowId,
|
||||
index,
|
||||
function () {
|
||||
const stillUsed = Object.keys(
|
||||
analysisAssignedState,
|
||||
).some(function (pid) {
|
||||
return (
|
||||
Array.isArray(analysisAssignedState[pid]) &&
|
||||
analysisAssignedState[pid].some(
|
||||
function (item) {
|
||||
return (
|
||||
item.analysis_recordkey ===
|
||||
recordKey
|
||||
);
|
||||
},
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
if (!stillUsed) {
|
||||
delete analysisSelectedState[recordKey];
|
||||
}
|
||||
|
||||
syncSelectedAnalysisRows();
|
||||
},
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const row = e.target.closest(".analysis-analysis-item");
|
||||
if (!row) return;
|
||||
|
||||
const selectedPartIds = getSelectedPartIds();
|
||||
if (!selectedPartIds.length) {
|
||||
alert("Select at least one part first");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = buildAnalysisPayloadFromRow(row);
|
||||
if (!payload.analysis_recordkey) {
|
||||
alert("Invalid analysis record key");
|
||||
return;
|
||||
}
|
||||
|
||||
const recordKey = payload.analysis_recordkey;
|
||||
|
||||
let pending = selectedPartIds.length;
|
||||
let atLeastOneSaved = false;
|
||||
|
||||
selectedPartIds.forEach(function (partId) {
|
||||
saveAnalysisAssociation(partId, payload, function (ok) {
|
||||
if (ok) atLeastOneSaved = true;
|
||||
|
||||
pending--;
|
||||
if (pending <= 0 && atLeastOneSaved) {
|
||||
analysisSelectedState[recordKey] = true;
|
||||
syncSelectedAnalysisRows();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const firstActiveMatrix = modal.querySelector(
|
||||
".analysis-matrix-item.active",
|
||||
);
|
||||
if (firstActiveMatrix) {
|
||||
const matrixId = firstActiveMatrix.getAttribute("data-matrix-id");
|
||||
const matrixLabel =
|
||||
firstActiveMatrix.getAttribute("data-matrix-name") ||
|
||||
firstActiveMatrix.textContent.trim();
|
||||
|
||||
selectPartsByMatrix(matrixId, matrixLabel);
|
||||
} else {
|
||||
updateSelectedPartsInfo();
|
||||
}
|
||||
}
|
||||
|
||||
// OPEN ANALYSIS MODAL FROM PARTS MODAL BUTTON
|
||||
$(document).on("click", ".open-analysis-modal-btn", function () {
|
||||
const partsModal = document.getElementById("partsModal");
|
||||
const iddatadb = $("#partsModal").data("iddatadb");
|
||||
|
||||
if (!iddatadb) {
|
||||
console.error("iddatadb not found on #partsModal");
|
||||
alert("iddatadb not found");
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: "modal_analysis.php",
|
||||
method: "GET",
|
||||
data: { iddatadb: iddatadb },
|
||||
success: function (response) {
|
||||
$("#analysisModalContainer").html(response);
|
||||
|
||||
const modalElement = document.getElementById("analysisModal");
|
||||
if (!modalElement) {
|
||||
console.error("Analysis modal not found: #analysisModal");
|
||||
return;
|
||||
}
|
||||
|
||||
let modal = bootstrap.Modal.getInstance(modalElement);
|
||||
if (!modal) {
|
||||
modal = new bootstrap.Modal(modalElement, {
|
||||
backdrop: true,
|
||||
keyboard: true,
|
||||
focus: true,
|
||||
});
|
||||
}
|
||||
|
||||
loadAnalysisMatrixNames().always(function () {
|
||||
initAnalysisModal();
|
||||
modal.show();
|
||||
});
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
console.error("Error loading analysis modal:", error);
|
||||
alert("Error loading analysis modal: " + error);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// CLEANUP ON CLOSE
|
||||
$(document).on("hidden.bs.modal", "#analysisModal", function () {
|
||||
const modalElement = document.getElementById("analysisModal");
|
||||
if (modalElement) {
|
||||
const modal = bootstrap.Modal.getInstance(modalElement);
|
||||
if (modal) modal.dispose();
|
||||
}
|
||||
|
||||
$("#analysisModalContainer").empty();
|
||||
|
||||
// keep parts modal alive, but remove extra backdrop leftovers
|
||||
$(".modal-backdrop").last().remove();
|
||||
|
||||
if ($("#partsModal").hasClass("show")) {
|
||||
$("body").addClass("modal-open");
|
||||
} else {
|
||||
$("body").removeClass("modal-open").css("padding-right", "");
|
||||
}
|
||||
});
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
ob_start();
|
||||
session_start();
|
||||
require_once '../../vendor/autoload.php';
|
||||
|
||||
Dotenv\Dotenv::createImmutable(dirname(__DIR__, 2))->safeLoad();
|
||||
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
|
||||
|
||||
$response = ['error' => '', 'rows' => [], 'columns' => [], 'template_id' => 0, 'filename' => '', 'excel_data' => []];
|
||||
|
||||
try {
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$template_id = isset($input['template_id']) ? intval($input['template_id']) : 0;
|
||||
$filename = $input['routine_data']['filename'] ?? '';
|
||||
$headerrow = $input['routine_data']['headerrow'] ?? 1;
|
||||
$excelData = $input['excel_data'] ?? [];
|
||||
$routineData = $input['routine_data'] ?? [];
|
||||
|
||||
if (!$filename || empty($excelData)) {
|
||||
throw new Exception("Dati della routine mancanti.");
|
||||
}
|
||||
|
||||
// --- 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 (" . ($useEngine ? "motore" : "file: {$filename}") . ") per template {$template_id}, header row: {$headerrow}");
|
||||
} else {
|
||||
throw new Exception("File della routine non trovato: $routineFile");
|
||||
}
|
||||
|
||||
// Aggiorna la sessione con i dati modificati
|
||||
$_SESSION['excel_data'] = $excelData;
|
||||
|
||||
$response['excel_data'] = $excelData;
|
||||
$response['rows'] = array_column($excelData, 'data');
|
||||
$response['columns'] = $_SESSION['headers'];
|
||||
$response['template_id'] = $template_id;
|
||||
$response['filename'] = $input['filename'] ?? '';
|
||||
} else {
|
||||
$response['error'] = "Richiesta non valida.";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$response['error'] = "Errore durante l'applicazione della routine: " . $e->getMessage();
|
||||
error_log("Exception in apply_routine.php: " . $e->getMessage());
|
||||
}
|
||||
|
||||
ob_end_clean();
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
@@ -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,131 @@
|
||||
<?php
|
||||
require_once "class/VisualLimsApiClient.class.php";
|
||||
include('include/headscript.php');
|
||||
|
||||
$dbHandler = DBHandlerSelect::getInstance();
|
||||
$pdo = $dbHandler->getConnection();
|
||||
|
||||
header("Content-Type: application/json");
|
||||
|
||||
// 🔹 Configura directory log (creala se non esiste)
|
||||
$logDir = __DIR__ . '/logs/api/';
|
||||
if (!is_dir($logDir)) {
|
||||
mkdir($logDir, 0755, true);
|
||||
}
|
||||
|
||||
// 🔹 Base URL API
|
||||
$apiBaseUrl = 'https://93.43.5.102/limsapi/api/odata/';
|
||||
|
||||
// 🔹 Hardcoded values
|
||||
$iddatadb = 846;
|
||||
$commessaId = 533357;
|
||||
|
||||
try {
|
||||
// 🔹 STEP 4: Fetch Field Values with Labels (usa dati reali per iddatadb=845)
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
idd.field_value,
|
||||
m.field_label,
|
||||
m.schema_id,
|
||||
m.field_id
|
||||
FROM
|
||||
import_data_details as idd
|
||||
JOIN template_mapping m ON idd.mapping_id = m.id
|
||||
WHERE idd.id = :iddatadb
|
||||
");
|
||||
$stmt->execute(['iddatadb' => $iddatadb]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$fieldValues = [];
|
||||
$valueMap = []; // Mappa per field_id -> valore
|
||||
foreach ($rows as $row) {
|
||||
$fieldValues[] = [
|
||||
"IdCommesseCustomFields" => (int) $row['field_id'],
|
||||
"Valore" => $row['field_value'],
|
||||
"FieldLabel" => $row['field_label']
|
||||
];
|
||||
$valueMap[(int) $row['field_id']] = $row['field_value']; // Mappa per ID definizione
|
||||
}
|
||||
|
||||
// Logga i fieldValues in error_log
|
||||
$logFieldValues = "FieldValues dal DB (iddatadb={$iddatadb}):\n" . json_encode($fieldValues, JSON_PRETTY_PRINT);
|
||||
error_log($logFieldValues);
|
||||
|
||||
// 🔹 Initialize API client
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
// 🔹 STEP A: GET iniziale per CommesseCustomFields con espansione CustomField
|
||||
$expand = "CommesseCustomFields(\$expand=CustomField)"; // Espansione come da istruzioni fornitore
|
||||
$commessaWithFields = $api->get("CommessaWeb(" . $commessaId . ")?\$expand=" . $expand);
|
||||
|
||||
// 🔹 STEP B: Prepara payload PATCH (tutti i campi, sovrascrivi se match su CustomField.IdCustomField == field_id)
|
||||
$commessaCustomFields = [];
|
||||
foreach ($commessaWithFields["CommesseCustomFields"] as $customField) {
|
||||
$definitionId = (int) ($customField["CustomField"]["IdCustomField"] ?? 0); // Usa IdCustomField dal CustomField
|
||||
$currentValue = $customField["Valore"] ?? '';
|
||||
$newValue = isset($valueMap[$definitionId]) ? $valueMap[$definitionId] : $currentValue;
|
||||
|
||||
$commessaCustomFields[] = [
|
||||
"IdCommesseCustomFields" => (int) $customField["IdCommesseCustomFields"],
|
||||
"Valore" => $newValue
|
||||
];
|
||||
}
|
||||
|
||||
// 🔹 Unico file di log per tutto
|
||||
$logFile = $logDir . "commessa_{$commessaId}_patch_and_get_" . time() . ".txt";
|
||||
$logContent = "FieldValues dal DB (iddatadb={$iddatadb}):\n" . json_encode($fieldValues, JSON_PRETTY_PRINT) . "\n\n";
|
||||
|
||||
// Log curl-like per GET iniziale
|
||||
$logContent .= "GET iniziale:\n" .
|
||||
"curl --location --request GET '{$apiBaseUrl}CommessaWeb({$commessaId})?\$expand=CommesseCustomFields(\$expand=CustomField)' \\\n" .
|
||||
"--header 'Authorization: Bearer ••••••'\n\n" .
|
||||
"RESPONSE:\n" . json_encode($commessaWithFields, JSON_PRETTY_PRINT) . "\n\n---\n";
|
||||
|
||||
if (!empty($commessaCustomFields)) {
|
||||
$updatePayload = ["CommesseCustomFields" => $commessaCustomFields];
|
||||
|
||||
// Log curl-like per PATCH
|
||||
$jsonPayload = json_encode($updatePayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
$logContent .= "PATCH:\n" .
|
||||
"curl --location --request PATCH '{$apiBaseUrl}CommessaWeb({$commessaId})' \\\n" .
|
||||
"--header 'Content-Type: application/json' \\\n" .
|
||||
"--header 'Authorization: Bearer ••••••' \\\n" .
|
||||
"--data '{$jsonPayload}'\n\n";
|
||||
|
||||
$patchResponse = $api->patch("CommessaWeb({$commessaId})", $updatePayload);
|
||||
|
||||
$logContent .= "PATCH RESPONSE:\n" . json_encode($patchResponse, JSON_PRETTY_PRINT) . "\n\n---\n";
|
||||
} else {
|
||||
$logContent .= "PATCH: Nessun campo custom da aggiornare\n\n---\n";
|
||||
}
|
||||
|
||||
// 🔹 STEP C: GET di controllo post-PATCH
|
||||
$commessaAfterPatch = $api->get("CommessaWeb(" . $commessaId . ")?\$expand=" . $expand);
|
||||
|
||||
// Log curl-like per GET di controllo
|
||||
$logContent .= "GET di controllo:\n" .
|
||||
"curl --location --request GET '{$apiBaseUrl}CommessaWeb({$commessaId})?\$expand=CommesseCustomFields(\$expand=CustomField)' \\\n" .
|
||||
"--header 'Authorization: Bearer ••••••'\n\n" .
|
||||
"RESPONSE:\n" . json_encode($commessaAfterPatch, JSON_PRETTY_PRINT);
|
||||
|
||||
// Salva log unico
|
||||
file_put_contents($logFile, $logContent);
|
||||
|
||||
// 🔹 Output a schermo
|
||||
echo json_encode([
|
||||
"success" => true,
|
||||
"message" => "PATCH eseguito su commessa {$commessaId} con dati da iddatadb {$iddatadb}",
|
||||
"commessaAfterPatch" => $commessaAfterPatch,
|
||||
"totalCustomFieldsUpdated" => count($commessaCustomFields),
|
||||
"fieldValues" => $fieldValues,
|
||||
"logFile" => $logFile
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
error_log("Patch Error: " . $e->getMessage() . "\nTrace: " . $e->getTraceAsString());
|
||||
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"message" => "Patch failed: " . $e->getMessage(),
|
||||
"logFile" => $logFile ?? 'Nessun log generato'
|
||||
]);
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,12 +24,22 @@ class VisualLimsApiClient
|
||||
public static function getInstance()
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new VisualLimsApiClient();
|
||||
$dotenv = Dotenv::createImmutable(dirname(__DIR__, 3));
|
||||
$dotenv->load();
|
||||
|
||||
$simulate = ($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true';
|
||||
|
||||
if ($simulate) {
|
||||
require_once __DIR__ . '/VisualLimsApiClientMock.class.php';
|
||||
self::$instance = new VisualLimsApiClientMock();
|
||||
} else {
|
||||
self::$instance = new VisualLimsApiClient();
|
||||
}
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function authenticate()
|
||||
private function authenticate($retryCount = 0, $maxRetries = 3)
|
||||
{
|
||||
$ch = curl_init("{$this->baseUrl}/api/authentication/authenticate");
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
@@ -45,16 +55,22 @@ class VisualLimsApiClient
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_VERBOSE, true);
|
||||
$log = fopen(__DIR__ . '/curl_auth_debug.log', 'w') ?: fopen('php://stderr', 'w');
|
||||
$log = fopen(__DIR__ . '/curl_auth_debug.log', 'a') ?: fopen('php://stderr', 'w');
|
||||
curl_setopt($ch, CURLOPT_STDERR, $log);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curl_error = curl_error($ch);
|
||||
$log_message = date('Y-m-d H:i:s') . " - Auth attempt {$retryCount}: HTTP {$http_code}, Error: {$curl_error}, Response: " . substr($response, 0, 1000) . "\n";
|
||||
fwrite($log, $log_message);
|
||||
fclose($log);
|
||||
curl_close($ch);
|
||||
|
||||
if ($response === false || $http_code != 200) {
|
||||
if ($http_code === 400 && strpos($response, 'Cannot persist the object') !== false && $retryCount < $maxRetries) {
|
||||
usleep(500000); // Ritardo di 500ms
|
||||
return $this->authenticate($retryCount + 1, $maxRetries); // Riprova
|
||||
}
|
||||
throw new Exception("Autenticazione fallita: HTTP {$http_code}, Errore cURL: {$curl_error}, Risposta: " . substr($response, 0, 1000));
|
||||
}
|
||||
|
||||
@@ -120,4 +136,158 @@ class VisualLimsApiClient
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function post($endpoint, $payload)
|
||||
{
|
||||
$token = $this->getToken();
|
||||
$url = "{$this->baseUrl}/api/odata/{$endpoint}";
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
"Authorization: Bearer {$token}",
|
||||
"Content-Type: application/json",
|
||||
"Accept: application/json"
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curl_error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($response === false) {
|
||||
throw new Exception("Errore nella richiesta POST: {$curl_error}");
|
||||
}
|
||||
|
||||
if ($http_code < 200 || $http_code >= 300) {
|
||||
throw new Exception("POST fallito: HTTP {$http_code}, Risposta: " . substr($response, 0, 1000));
|
||||
}
|
||||
|
||||
return json_decode($response, true);
|
||||
}
|
||||
|
||||
public function patch($endpoint, $payload)
|
||||
{
|
||||
$token = $this->getToken();
|
||||
$url = "{$this->baseUrl}/api/odata/{$endpoint}";
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PATCH");
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
"Authorization: Bearer {$token}",
|
||||
"Content-Type: application/json",
|
||||
"Accept: application/json"
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curl_error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($response === false) {
|
||||
throw new Exception("Errore nella richiesta PATCH: {$curl_error}");
|
||||
}
|
||||
|
||||
if ($http_code < 200 || $http_code >= 300) {
|
||||
throw new Exception("PATCH fallito: HTTP {$http_code}, Risposta: " . substr($response, 0, 1000));
|
||||
}
|
||||
|
||||
return json_decode($response, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST a file as multipart/form-data (used for photo/attachment uploads).
|
||||
*
|
||||
* @param string $endpoint OData endpoint, e.g. "Campione(613388)/UploadCampioneFile"
|
||||
* @param string $filePath Absolute path to the file on disk
|
||||
* @param string $fileName Original file name to send
|
||||
* @param array $extraFields Additional form fields to include
|
||||
* @return array|null Decoded JSON response
|
||||
*/
|
||||
public function postMultipart($endpoint, $filePath, $fileName, array $extraFields = [])
|
||||
{
|
||||
$token = $this->getToken();
|
||||
$url = "{$this->baseUrl}/api/odata/{$endpoint}";
|
||||
|
||||
$cfile = new CURLFile($filePath, mime_content_type($filePath) ?: 'application/octet-stream', $fileName);
|
||||
|
||||
$payload = array_merge($extraFields, [
|
||||
'file' => $cfile,
|
||||
]);
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
"Authorization: Bearer {$token}",
|
||||
"Accept: application/json",
|
||||
// Content-Type is set automatically to multipart/form-data by cURL
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curl_error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($response === false) {
|
||||
throw new Exception("Errore nella richiesta POST multipart: {$curl_error}");
|
||||
}
|
||||
|
||||
if ($http_code < 200 || $http_code >= 300) {
|
||||
throw new Exception("POST multipart fallito: HTTP {$http_code}, Risposta: " . substr($response, 0, 1000));
|
||||
}
|
||||
|
||||
return json_decode($response, true);
|
||||
}
|
||||
|
||||
public function getBaseUrl()
|
||||
{
|
||||
return $this->baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recupera contenuto binario - Adattato per https://bvcpsitaly-elims.com/limsapi
|
||||
*/
|
||||
public function getRaw($endpoint)
|
||||
{
|
||||
$token = $this->getToken();
|
||||
|
||||
// IMPORTANTE: usa /odata/ e NON /api/odata/
|
||||
$url = "{$this->baseUrl}/odata/{$endpoint}";
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
"Authorization: Bearer {$token}",
|
||||
"Accept: */*"
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curl_error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($response === false) {
|
||||
throw new Exception("Errore cURL: " . $curl_error);
|
||||
}
|
||||
|
||||
if ($http_code !== 200) {
|
||||
throw new Exception("HTTP {$http_code} su endpoint: " . $url);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Mock implementation of VisualLimsApiClient.
|
||||
* Activated when SIMULATE_EXPORT_LIMS=true in .env.
|
||||
* All HTTP calls are skipped; fake but structurally valid data is returned.
|
||||
* Every simulated call is logged via error_log() with a [SIMULATE] prefix.
|
||||
*/
|
||||
class VisualLimsApiClientMock
|
||||
{
|
||||
private int $fakeCommessaId;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Stable fake ID for the lifetime of a single request
|
||||
$this->fakeCommessaId = mt_rand(90001, 99999);
|
||||
error_log("[SIMULATE] VisualLimsApiClientMock initialised (fakeCommessaId={$this->fakeCommessaId})");
|
||||
}
|
||||
|
||||
public function get(string $endpoint): array
|
||||
{
|
||||
error_log("[SIMULATE] GET {$endpoint}");
|
||||
|
||||
// --- Fixed-field dropdown lists ---
|
||||
|
||||
if (str_starts_with($endpoint, 'MoltiplicatorePrezzi')) {
|
||||
return ['value' => [
|
||||
['IdMoltiplicatorePrezzo' => 1, 'Codice' => 'MP-01', 'Descrizione' => 'Standard (1x)'],
|
||||
['IdMoltiplicatorePrezzo' => 2, 'Codice' => 'MP-02', 'Descrizione' => 'Urgente (1.5x)'],
|
||||
['IdMoltiplicatorePrezzo' => 3, 'Codice' => 'MP-03', 'Descrizione' => 'Extra Urgente (2x)'],
|
||||
]];
|
||||
}
|
||||
|
||||
if (str_starts_with($endpoint, 'AnagraficaCertestObject')) {
|
||||
return ['value' => [
|
||||
['IdAnagrafica' => 1, 'Codice' => 'OBJ-01', 'NomeAnagrafica' => 'Articolo Tessile'],
|
||||
['IdAnagrafica' => 2, 'Codice' => 'OBJ-02', 'NomeAnagrafica' => 'Componente Meccanico'],
|
||||
['IdAnagrafica' => 3, 'Codice' => 'OBJ-03', 'NomeAnagrafica' => 'Materiale Plastico'],
|
||||
]];
|
||||
}
|
||||
|
||||
if (str_starts_with($endpoint, 'AnagraficaCertestService')) {
|
||||
return ['value' => [
|
||||
['IdAnagrafica' => 1, 'Codice' => 'SRV-01', 'NomeAnagrafica' => 'Analisi Chimica'],
|
||||
['IdAnagrafica' => 2, 'Codice' => 'SRV-02', 'NomeAnagrafica' => 'Test Meccanico'],
|
||||
['IdAnagrafica' => 3, 'Codice' => 'SRV-03', 'NomeAnagrafica' => 'Prova Ambientale'],
|
||||
]];
|
||||
}
|
||||
|
||||
// Cliente? list — get_clienti.php exits early in simulate mode, but guard here too
|
||||
if (str_starts_with($endpoint, 'Cliente?')) {
|
||||
return ['value' => []];
|
||||
}
|
||||
|
||||
// Cliente(N)?$expand=Responsabili
|
||||
if (str_starts_with($endpoint, 'Cliente(')) {
|
||||
preg_match('/Cliente\((\d+)\)/', $endpoint, $m);
|
||||
$clienteId = isset($m[1]) ? (int) $m[1] : 0;
|
||||
return [
|
||||
'IdCliente' => $clienteId,
|
||||
'Responsabili' => [
|
||||
['IdClienteResponsabile' => 1, 'Nominativo' => 'Marco Bianchi'],
|
||||
['IdClienteResponsabile' => 2, 'Nominativo' => 'Giulia Ferrari'],
|
||||
['IdClienteResponsabile' => 3, 'Nominativo' => 'Andrea Russo'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// --- CustomField dropdown values (get_customfield_values.php) ---
|
||||
|
||||
if (str_starts_with($endpoint, 'CustomField(')) {
|
||||
preg_match('/CustomField\((\d+)\)/', $endpoint, $m);
|
||||
$fieldId = isset($m[1]) ? (int) $m[1] : 0;
|
||||
return [
|
||||
'CustomFieldsValues' => [
|
||||
['IdCustomFieldsValue' => $fieldId * 10 + 1, 'Valore' => 'Opzione A'],
|
||||
['IdCustomFieldsValue' => $fieldId * 10 + 2, 'Valore' => 'Opzione B'],
|
||||
['IdCustomFieldsValue' => $fieldId * 10 + 3, 'Valore' => 'Opzione C'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// --- CommessaWeb OData calls (STEP 7 GET + STEP 10 verification) ---
|
||||
|
||||
preg_match('/\((\d+)\)/', $endpoint, $m);
|
||||
$id = isset($m[1]) ? (int) $m[1] : $this->fakeCommessaId;
|
||||
|
||||
return [
|
||||
'IdCommessa' => $id,
|
||||
'CodiceCommessa' => "SIM-{$id}",
|
||||
'CommesseCustomFields' => [], // Empty → PATCH step is skipped correctly
|
||||
];
|
||||
}
|
||||
|
||||
public function post(string $endpoint, array $payload): array
|
||||
{
|
||||
error_log("[SIMULATE] POST {$endpoint} payload=" . json_encode($payload));
|
||||
|
||||
// CommessaWeb creation
|
||||
if ($endpoint === 'CommessaWeb') {
|
||||
return [
|
||||
'IdCommessa' => $this->fakeCommessaId,
|
||||
'CodiceCommessa' => "SIM-{$this->fakeCommessaId}",
|
||||
'Richiedente' => $payload['Richiedente'] ?? '',
|
||||
'Descrizione' => $payload['Descrizione'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
// Campione creation
|
||||
if ($endpoint === 'Campione') {
|
||||
return [
|
||||
'IdCampione' => mt_rand(10001, 19999),
|
||||
'Commessa' => $payload['Commessa'] ?? null,
|
||||
'Matrice' => $payload['Matrice'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
// InviaCommessa / ImportaCommessa (currently commented out upstream)
|
||||
return ['simulated' => true, 'endpoint' => $endpoint];
|
||||
}
|
||||
|
||||
public function patch(string $endpoint, array $payload): array
|
||||
{
|
||||
error_log("[SIMULATE] PATCH {$endpoint} payload=" . json_encode($payload));
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public function postMultipart(string $endpoint, string $filePath, string $fileName, array $extraFields = []): array
|
||||
{
|
||||
error_log("[SIMULATE] POST multipart {$endpoint} file={$fileName}");
|
||||
|
||||
return ['simulated' => true, 'file' => $fileName];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
* Trying 93.43.5.102:443...
|
||||
* Connected to 93.43.5.102 (93.43.5.102) port 443
|
||||
* ALPN: curl offers h2,http/1.1
|
||||
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
|
||||
* ALPN: server accepted h2
|
||||
* Server certificate:
|
||||
* subject: C=FR; ST=Île-de-France; O=Bureau Veritas; CN=bvcpsitaly-elims.it
|
||||
* start date: Feb 17 00:00:00 2025 GMT
|
||||
* expire date: Feb 17 23:59:59 2026 GMT
|
||||
* issuer: C=US; O=Corporation Service Company; CN=Corporation Service Company RSA OV SSL CA
|
||||
* SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
|
||||
* using HTTP/2
|
||||
* [HTTP/2] [1] OPENED stream for https://93.43.5.102/limsapi/api/authentication/authenticate
|
||||
* [HTTP/2] [1] [:method: POST]
|
||||
* [HTTP/2] [1] [:scheme: https]
|
||||
* [HTTP/2] [1] [:authority: 93.43.5.102]
|
||||
* [HTTP/2] [1] [:path: /limsapi/api/authentication/authenticate]
|
||||
* [HTTP/2] [1] [content-type: application/json]
|
||||
* [HTTP/2] [1] [accept: application/json]
|
||||
* [HTTP/2] [1] [content-length: 51]
|
||||
> POST /limsapi/api/authentication/authenticate HTTP/2
|
||||
Host: 93.43.5.102
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
Content-Length: 51
|
||||
|
||||
< HTTP/2 200
|
||||
< cache-control: max-age=0
|
||||
< content-type: application/json; charset=utf-8
|
||||
< server: Microsoft-IIS/10.0
|
||||
< strict-transport-security: max-age=2592000
|
||||
< x-powered-by: ASP.NET
|
||||
< x-content-type-options: nosniff
|
||||
< date: Fri, 29 Aug 2025 13:36:31 GMT
|
||||
<
|
||||
* Connection #0 to host 93.43.5.102 left intact
|
||||
@@ -1,35 +0,0 @@
|
||||
* Trying 93.43.5.102:443...
|
||||
* Connected to 93.43.5.102 (93.43.5.102) port 443
|
||||
* ALPN: curl offers h2,http/1.1
|
||||
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
|
||||
* ALPN: server accepted h2
|
||||
* Server certificate:
|
||||
* subject: C=FR; ST=Île-de-France; O=Bureau Veritas; CN=bvcpsitaly-elims.it
|
||||
* start date: Feb 17 00:00:00 2025 GMT
|
||||
* expire date: Feb 17 23:59:59 2026 GMT
|
||||
* issuer: C=US; O=Corporation Service Company; CN=Corporation Service Company RSA OV SSL CA
|
||||
* SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
|
||||
* using HTTP/2
|
||||
* [HTTP/2] [1] OPENED stream for https://93.43.5.102/limsapi/api/odata/CustomField(1083)?$expand=CustomFieldsValues
|
||||
* [HTTP/2] [1] [:method: GET]
|
||||
* [HTTP/2] [1] [:scheme: https]
|
||||
* [HTTP/2] [1] [:authority: 93.43.5.102]
|
||||
* [HTTP/2] [1] [:path: /limsapi/api/odata/CustomField(1083)?$expand=CustomFieldsValues]
|
||||
* [HTTP/2] [1] [authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjQ5MiIsIlhhZlNlY3VyaXR5QXV0aFBhc3NlZCI6IlhhZlNlY3VyaXR5QXV0aFBhc3NlZCIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiJXZWJBcGlVc2VyIiwiWGFmU2VjdXJpdHkiOiJYYWZTZWN1cml0eSIsIlhhZkxvZ29uUGFyYW1zIjoicTFZS0xVNHQ4a3ZNVFZXeVVncFBUWElzeUFRSktPa29CU1FXRjVmbkY2VUF4Y3RUa3hJTE1rdUI0Z2FHU3JVQSIsImV4cCI6MTc1NjQ4MTc5MSwiaXNzIjoiTXkiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjQyMDAifQ.066odH6XJ_XK1D_w6xYBCXncA6Hx1AgFqsanfvKULyk]
|
||||
* [HTTP/2] [1] [accept: application/json]
|
||||
> GET /limsapi/api/odata/CustomField(1083)?$expand=CustomFieldsValues HTTP/2
|
||||
Host: 93.43.5.102
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjQ5MiIsIlhhZlNlY3VyaXR5QXV0aFBhc3NlZCI6IlhhZlNlY3VyaXR5QXV0aFBhc3NlZCIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiJXZWJBcGlVc2VyIiwiWGFmU2VjdXJpdHkiOiJYYWZTZWN1cml0eSIsIlhhZkxvZ29uUGFyYW1zIjoicTFZS0xVNHQ4a3ZNVFZXeVVncFBUWElzeUFRSktPa29CU1FXRjVmbkY2VUF4Y3RUa3hJTE1rdUI0Z2FHU3JVQSIsImV4cCI6MTc1NjQ4MTc5MSwiaXNzIjoiTXkiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjQyMDAifQ.066odH6XJ_XK1D_w6xYBCXncA6Hx1AgFqsanfvKULyk
|
||||
Accept: application/json
|
||||
|
||||
< HTTP/2 200
|
||||
< cache-control: max-age=0
|
||||
< content-type: application/json; odata.metadata=minimal; odata.streaming=true; charset=utf-8
|
||||
< server: Microsoft-IIS/10.0
|
||||
< strict-transport-security: max-age=2592000
|
||||
< odata-version: 4.0
|
||||
< x-powered-by: ASP.NET
|
||||
< x-content-type-options: nosniff
|
||||
< date: Fri, 29 Aug 2025 13:36:33 GMT
|
||||
<
|
||||
* Connection #0 to host 93.43.5.102 left intact
|
||||
@@ -3,6 +3,9 @@ require_once dirname(__DIR__, 3) . '/vendor/autoload.php';
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
Dotenv::createImmutable(dirname(__DIR__, 3))->safeLoad();
|
||||
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
|
||||
|
||||
class DBHandlerSelect
|
||||
{
|
||||
private static $instance = null;
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* debug_odata_metadata.php — TEMPORANEO, da rimuovere dopo il test.
|
||||
* Scopo: leggere $metadata (XML standard OData) per vedere:
|
||||
* - tutti gli EntitySet disponibili (non solo "Cliente")
|
||||
* - tutti i campi (Property) definiti per l'EntityType Cliente
|
||||
* Questo bypassa qualunque vista/proiezione ristretta lato server
|
||||
* applicata sull'endpoint "Cliente" normale.
|
||||
*/
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
include dirname(__DIR__) . '/../extra/auth.php';
|
||||
|
||||
if (!Auth::check()) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
ini_set('display_errors', '1');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
$dotenv = Dotenv::createImmutable(dirname(__DIR__, 2));
|
||||
$dotenv->load();
|
||||
|
||||
$baseUrl = $_ENV['API_BASE_URL'];
|
||||
$username = $_ENV['API_USERNAME'];
|
||||
$password = $_ENV['API_PASSWORD'];
|
||||
|
||||
$scriptStart = microtime(true);
|
||||
|
||||
// ── 1. Autenticazione (stessa logica della classe originale) ──────────────
|
||||
$ch = curl_init("{$baseUrl}/api/authentication/authenticate");
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
|
||||
'Username' => $username,
|
||||
'Password' => $password
|
||||
]));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'Accept: application/json'
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
|
||||
$authResponse = curl_exec($ch);
|
||||
$authHttpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($authHttpCode !== 200) {
|
||||
echo "AUTH FAILED: HTTP {$authHttpCode}\n{$authResponse}\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
$tokenData = json_decode($authResponse, true);
|
||||
$token = null;
|
||||
if (is_array($tokenData) && isset($tokenData['token'])) {
|
||||
$token = $tokenData['token'];
|
||||
} elseif (is_string($tokenData)) {
|
||||
$token = trim($tokenData, '"');
|
||||
} else {
|
||||
$token = trim($authResponse, '"');
|
||||
}
|
||||
|
||||
if (empty($token)) {
|
||||
echo "TOKEN NOT RECEIVED\n{$authResponse}\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
$authElapsed = microtime(true) - $scriptStart;
|
||||
|
||||
// ── 2. Richiesta $metadata (XML, non JSON) ─────────────────────────────────
|
||||
// Cache-buster nell'URL: alcuni server/reverse-proxy potrebbero cachare per URL esatto.
|
||||
$cacheBuster = uniqid('cb_', true);
|
||||
$metadataUrl = "{$baseUrl}/api/odata/\$metadata?_cb={$cacheBuster}";
|
||||
|
||||
$metadataStart = microtime(true);
|
||||
|
||||
$ch = curl_init($metadataUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
"Authorization: Bearer {$token}",
|
||||
"Accept: application/xml"
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
|
||||
curl_setopt($ch, CURLOPT_HEADER, true); // includi header risposta per ispezione
|
||||
|
||||
$rawResponse = curl_exec($ch);
|
||||
$curlInfo = curl_getinfo($ch);
|
||||
$httpCode = $curlInfo['http_code'];
|
||||
$curlTotalTime = $curlInfo['total_time'];
|
||||
$curlError = curl_error($ch);
|
||||
$headerSize = $curlInfo['header_size'];
|
||||
curl_close($ch);
|
||||
|
||||
$metadataElapsed = microtime(true) - $metadataStart;
|
||||
|
||||
if ($rawResponse === false || $httpCode !== 200) {
|
||||
echo "METADATA REQUEST FAILED: HTTP {$httpCode}\nError: {$curlError}\n{$rawResponse}\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
$responseHeaders = substr($rawResponse, 0, $headerSize);
|
||||
$xml = substr($rawResponse, $headerSize);
|
||||
|
||||
echo "==================== TIMING (prova che NON è cache) ====================\n";
|
||||
echo "Tempo autenticazione: " . round($authElapsed * 1000, 1) . " ms\n";
|
||||
echo "Tempo chiamata \$metadata (curl total_time): " . round($curlTotalTime * 1000, 1) . " ms\n";
|
||||
echo "Tempo chiamata \$metadata (wall clock PHP): " . round($metadataElapsed * 1000, 1) . " ms\n";
|
||||
echo "Cache-buster usato: {$cacheBuster}\n";
|
||||
echo "Dimensione XML ricevuto: " . strlen($xml) . " bytes\n\n";
|
||||
|
||||
echo "==================== HEADER RISPOSTA (controlla se c'è Cache-Control/Age/X-Cache) ====================\n";
|
||||
echo $responseHeaders . "\n";
|
||||
|
||||
// ── 3. Estrai tutti gli EntitySet disponibili ──────────────────────────────
|
||||
echo "==================== ENTITY SETS DISPONIBILI ====================\n";
|
||||
if (preg_match_all('/<EntitySet Name="([^"]+)"/', $xml, $matches)) {
|
||||
foreach ($matches[1] as $name) {
|
||||
echo "- {$name}\n";
|
||||
}
|
||||
} else {
|
||||
echo "(nessun EntitySet trovato — controlla il formato XML grezzo sotto)\n";
|
||||
}
|
||||
|
||||
// ── 4. Estrai tutte le Property dell'EntityType "Cliente" (e simili) ──────
|
||||
echo "\n==================== CAMPI PER ENTITYTYPE CHE CONTENGONO 'Client' ====================\n";
|
||||
if (preg_match_all('/<EntityType Name="([^"]*[Cc]lient[^"]*)"[^>]*>(.*?)<\/EntityType>/s', $xml, $entityMatches, PREG_SET_ORDER)) {
|
||||
foreach ($entityMatches as $entity) {
|
||||
$entityName = $entity[1];
|
||||
$entityBody = $entity[2];
|
||||
echo "\n--- EntityType: {$entityName} ---\n";
|
||||
if (preg_match_all('/<Property Name="([^"]+)" Type="([^"]+)"/', $entityBody, $propMatches, PREG_SET_ORDER)) {
|
||||
foreach ($propMatches as $prop) {
|
||||
echo " {$prop[1]} ({$prop[2]})\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo "(nessun EntityType con 'Client' nel nome trovato)\n";
|
||||
}
|
||||
|
||||
// ── 5. Salva l'XML grezzo completo per ispezione manuale, se serve ────────
|
||||
file_put_contents(__DIR__ . '/metadata_debug.xml', $xml);
|
||||
echo "\n\nXML completo salvato in: metadata_debug.xml (rimuovilo dopo il test)\n";
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include('include/headscript.php');
|
||||
|
||||
$dbHandler = DBHandlerSelect::getInstance();
|
||||
$pdo = $dbHandler->getConnection();
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$sourceIddatadb = isset($data['source_iddatadb']) ? (int)$data['source_iddatadb'] : 0;
|
||||
$targetList = $data['target_iddatadb_list'] ?? [];
|
||||
// Default true: se il client non manda il parametro, cloniamo comunque le note
|
||||
// (comportamento di default richiesto, coerente con la checkbox pre-spuntata)
|
||||
$cloneNotes = array_key_exists('clone_notes', $data) ? !empty($data['clone_notes']) : true;
|
||||
$cloneAnalyses = !empty($data['clone_analyses']);
|
||||
$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;
|
||||
})));
|
||||
|
||||
if ($sourceIddatadb <= 0 || empty($targetIds)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Missing source or target records'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// 1. Load source parts
|
||||
$stmtParts = $pdo->prepare("
|
||||
SELECT id, part_number, part_description, mix, idmatrice, note, dateexpiry
|
||||
FROM identification_parts
|
||||
WHERE iddatadb = ?
|
||||
AND part_description IS NOT NULL
|
||||
AND TRIM(part_description) <> ''
|
||||
ORDER BY part_number ASC, id ASC
|
||||
");
|
||||
$stmtParts->execute([$sourceIddatadb]);
|
||||
$sourceParts = $stmtParts->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($sourceParts)) {
|
||||
$pdo->rollBack();
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'No parts found for source record'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. Prepare statements
|
||||
$stmtInsertPart = $pdo->prepare("
|
||||
INSERT INTO identification_parts
|
||||
(iddatadb, part_number, part_description, mix, idmatrice, note, dateexpiry, created_at, updated_at)
|
||||
VALUES
|
||||
(:iddatadb, :part_number, :part_description, :mix, :idmatrice, :note, :dateexpiry, NOW(), NOW())
|
||||
");
|
||||
|
||||
$stmtLoadCF = $pdo->prepare("
|
||||
SELECT field_id, value_id, value_text
|
||||
FROM identification_parts_customfields
|
||||
WHERE part_id = ?
|
||||
ORDER BY id ASC
|
||||
");
|
||||
|
||||
$stmtLoadAnalyses = $pdo->prepare("
|
||||
SELECT idmatrice, analysis_recordkey, analysis_name, analysis_method,
|
||||
analysis_level, is_web_selectable, is_accredited
|
||||
FROM identification_parts_analyses
|
||||
WHERE part_id = ?
|
||||
ORDER BY id ASC
|
||||
");
|
||||
|
||||
$stmtInsertAnalysis = $pdo->prepare("
|
||||
INSERT INTO identification_parts_analyses
|
||||
(part_id, iddatadb, idmatrice, analysis_recordkey, analysis_name, analysis_method,
|
||||
analysis_level, is_web_selectable, is_accredited, created_at, updated_at)
|
||||
VALUES
|
||||
(:part_id, :iddatadb, :idmatrice, :analysis_recordkey, :analysis_name, :analysis_method,
|
||||
:analysis_level, :is_web_selectable, :is_accredited, NOW(), NOW())
|
||||
");
|
||||
|
||||
$stmtInsertCF = $pdo->prepare("
|
||||
INSERT INTO identification_parts_customfields
|
||||
(part_id, field_id, value_id, value_text, created_at, updated_at)
|
||||
VALUES
|
||||
(:part_id, :field_id, :value_id, :value_text, NOW(), NOW())
|
||||
");
|
||||
|
||||
$details = [];
|
||||
$totalClonedParts = 0;
|
||||
$totalClonedAnalyses = 0;
|
||||
$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) {
|
||||
$clonedCountForTarget = 0;
|
||||
|
||||
foreach ($sourceParts as $part) {
|
||||
$stmtInsertPart->execute([
|
||||
':iddatadb' => $targetIddatadb,
|
||||
':part_number' => $part['part_number'],
|
||||
':part_description' => $part['part_description'],
|
||||
':mix' => $part['mix'] ?? 'N',
|
||||
':idmatrice' => $part['idmatrice'] !== '' ? $part['idmatrice'] : null,
|
||||
':note' => $cloneNotes ? ($part['note'] ?? null) : null,
|
||||
':dateexpiry' => $part['dateexpiry'] ?? null,
|
||||
]);
|
||||
|
||||
$newPartId = (int)$pdo->lastInsertId();
|
||||
|
||||
// Load source custom fields for this part
|
||||
$stmtLoadCF->execute([(int)$part['id']]);
|
||||
$customFields = $stmtLoadCF->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($customFields as $cf) {
|
||||
$stmtInsertCF->execute([
|
||||
':part_id' => $newPartId,
|
||||
':field_id' => (int)$cf['field_id'],
|
||||
':value_id' => ($cf['value_id'] !== null && $cf['value_id'] !== '') ? (int)$cf['value_id'] : null,
|
||||
':value_text' => $cf['value_text'] !== '' ? $cf['value_text'] : null,
|
||||
]);
|
||||
}
|
||||
|
||||
// Clone linked analyses, if requested
|
||||
if ($cloneAnalyses) {
|
||||
$stmtLoadAnalyses->execute([(int)$part['id']]);
|
||||
$analyses = $stmtLoadAnalyses->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($analyses as $an) {
|
||||
$stmtInsertAnalysis->execute([
|
||||
':part_id' => $newPartId,
|
||||
':iddatadb' => $targetIddatadb,
|
||||
':idmatrice' => ($an['idmatrice'] !== null && $an['idmatrice'] !== '') ? (int)$an['idmatrice'] : null,
|
||||
':analysis_recordkey' => $an['analysis_recordkey'],
|
||||
':analysis_name' => $an['analysis_name'] ?? null,
|
||||
':analysis_method' => $an['analysis_method'] ?? null,
|
||||
':analysis_level' => ($an['analysis_level'] !== null && $an['analysis_level'] !== '') ? (int)$an['analysis_level'] : null,
|
||||
':is_web_selectable' => (int)($an['is_web_selectable'] ?? 0),
|
||||
':is_accredited' => (int)($an['is_accredited'] ?? 0),
|
||||
]);
|
||||
$totalClonedAnalyses++;
|
||||
}
|
||||
}
|
||||
|
||||
$clonedCountForTarget++;
|
||||
$totalClonedParts++;
|
||||
}
|
||||
|
||||
$details[] = [
|
||||
'target_iddatadb' => $targetIddatadb,
|
||||
'cloned_parts' => $clonedCountForTarget
|
||||
];
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'source_iddatadb' => $sourceIddatadb,
|
||||
'cloned_targets' => count($targetIds),
|
||||
'total_cloned_parts' => $totalClonedParts,
|
||||
'total_cloned_analyses' => $totalClonedAnalyses,
|
||||
'details' => $details
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Clone failed: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
require_once 'class/db-functions.php';
|
||||
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
die("Invalid template ID");
|
||||
}
|
||||
|
||||
$sourceTemplateId = (int)$_GET['id'];
|
||||
|
||||
try {
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// 1. Get source template
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
name,
|
||||
source_type,
|
||||
header_row,
|
||||
start_column,
|
||||
description,
|
||||
target_table,
|
||||
sample_xlsx,
|
||||
button_size,
|
||||
button_bg_color,
|
||||
button_text_color,
|
||||
button_label,
|
||||
status,
|
||||
client_specific_fields,
|
||||
idclient,
|
||||
clientname,
|
||||
schemaname,
|
||||
idschema,
|
||||
schemajson,
|
||||
xls_headers,
|
||||
api_sample_json,
|
||||
json_nodes,
|
||||
idroutine
|
||||
FROM excel_templates
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$sourceTemplateId]);
|
||||
$template = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$template) {
|
||||
throw new Exception("Template not found.");
|
||||
}
|
||||
|
||||
// 2. Generate unique clone name
|
||||
$baseName = 'Copia di ' . $template['name'];
|
||||
$newName = $baseName;
|
||||
|
||||
$checkStmt = $pdo->prepare("SELECT COUNT(*) FROM excel_templates WHERE name = ?");
|
||||
$checkStmt->execute([$newName]);
|
||||
|
||||
if ((int)$checkStmt->fetchColumn() > 0) {
|
||||
$counter = 2;
|
||||
|
||||
do {
|
||||
$newName = $baseName . ' (' . $counter . ')';
|
||||
$checkStmt->execute([$newName]);
|
||||
$exists = (int)$checkStmt->fetchColumn() > 0;
|
||||
$counter++;
|
||||
} while ($exists);
|
||||
}
|
||||
|
||||
// 3. Insert cloned template
|
||||
$insertTemplate = $pdo->prepare("
|
||||
INSERT INTO excel_templates (
|
||||
name,
|
||||
source_type,
|
||||
created_at,
|
||||
updated_at,
|
||||
header_row,
|
||||
start_column,
|
||||
description,
|
||||
target_table,
|
||||
sample_xlsx,
|
||||
button_size,
|
||||
button_bg_color,
|
||||
button_text_color,
|
||||
button_label,
|
||||
status,
|
||||
client_specific_fields,
|
||||
idclient,
|
||||
clientname,
|
||||
schemaname,
|
||||
idschema,
|
||||
schemajson,
|
||||
xls_headers,
|
||||
api_sample_json,
|
||||
json_nodes,
|
||||
idroutine
|
||||
) VALUES (
|
||||
:name,
|
||||
:source_type,
|
||||
NOW(),
|
||||
NOW(),
|
||||
:header_row,
|
||||
:start_column,
|
||||
:description,
|
||||
:target_table,
|
||||
:sample_xlsx,
|
||||
:button_size,
|
||||
:button_bg_color,
|
||||
:button_text_color,
|
||||
:button_label,
|
||||
:status,
|
||||
:client_specific_fields,
|
||||
:idclient,
|
||||
:clientname,
|
||||
:schemaname,
|
||||
:idschema,
|
||||
:schemajson,
|
||||
:xls_headers,
|
||||
:api_sample_json,
|
||||
:json_nodes,
|
||||
:idroutine
|
||||
)
|
||||
");
|
||||
|
||||
$insertTemplate->execute([
|
||||
':name' => $newName,
|
||||
':source_type' => $template['source_type'],
|
||||
':header_row' => $template['header_row'],
|
||||
':start_column' => $template['start_column'],
|
||||
':description' => $template['description'],
|
||||
':target_table' => $template['target_table'],
|
||||
':sample_xlsx' => $template['sample_xlsx'],
|
||||
':button_size' => $template['button_size'],
|
||||
':button_bg_color' => $template['button_bg_color'],
|
||||
':button_text_color' => $template['button_text_color'],
|
||||
':button_label' => $template['button_label'],
|
||||
':status' => $template['status'],
|
||||
':client_specific_fields' => $template['client_specific_fields'],
|
||||
':idclient' => $template['idclient'],
|
||||
':clientname' => $template['clientname'],
|
||||
':schemaname' => $template['schemaname'],
|
||||
':idschema' => $template['idschema'],
|
||||
':schemajson' => $template['schemajson'],
|
||||
':xls_headers' => $template['xls_headers'],
|
||||
':api_sample_json' => $template['api_sample_json'],
|
||||
':json_nodes' => $template['json_nodes'],
|
||||
':idroutine' => $template['idroutine']
|
||||
]);
|
||||
|
||||
$newTemplateId = (int)$pdo->lastInsertId();
|
||||
|
||||
if ($newTemplateId <= 0) {
|
||||
throw new Exception("Unable to create cloned template.");
|
||||
}
|
||||
|
||||
// 4. Clone template_mapping rows
|
||||
$cloneMappings = $pdo->prepare("
|
||||
INSERT INTO template_mapping (
|
||||
template_id,
|
||||
schema_id,
|
||||
field_id,
|
||||
excel_column,
|
||||
json_node,
|
||||
is_manual,
|
||||
manual_default,
|
||||
auto_value,
|
||||
data_type,
|
||||
is_required,
|
||||
default_value,
|
||||
has_list,
|
||||
length,
|
||||
decimals,
|
||||
min_value,
|
||||
max_value,
|
||||
default_curr_date,
|
||||
tablename,
|
||||
field_label,
|
||||
main_field,
|
||||
is_visible_import,
|
||||
is_visible_parts
|
||||
)
|
||||
SELECT
|
||||
:new_template_id,
|
||||
schema_id,
|
||||
field_id,
|
||||
excel_column,
|
||||
json_node,
|
||||
is_manual,
|
||||
manual_default,
|
||||
auto_value,
|
||||
data_type,
|
||||
is_required,
|
||||
default_value,
|
||||
has_list,
|
||||
length,
|
||||
decimals,
|
||||
min_value,
|
||||
max_value,
|
||||
default_curr_date,
|
||||
tablename,
|
||||
field_label,
|
||||
main_field,
|
||||
is_visible_import,
|
||||
is_visible_parts
|
||||
FROM template_mapping
|
||||
WHERE template_id = :source_template_id
|
||||
");
|
||||
|
||||
$cloneMappings->execute([
|
||||
':new_template_id' => $newTemplateId,
|
||||
':source_template_id' => $sourceTemplateId
|
||||
]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
header("Location: templates_dashboard.php?cloned=1&new_id=" . $newTemplateId);
|
||||
exit;
|
||||
} catch (Exception $e) {
|
||||
if (isset($pdo) && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
die("Clone error: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'));
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
|
||||
require_once(__DIR__ . '/include/headscript.php');
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$templateId = (int)($input['template_id'] ?? 0);
|
||||
|
||||
if ($templateId <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid template_id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// If already exists, do nothing
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM template_fixed_mapping WHERE template_id = ?");
|
||||
$stmt->execute([$templateId]);
|
||||
if ((int)$stmt->fetchColumn() > 0) {
|
||||
echo json_encode(['success' => true, 'created' => 0, 'message' => 'Fixed fields already exist']);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXED FIELDS STANDARD
|
||||
*/
|
||||
$fixedFields = [
|
||||
['ClienteResponsabile', 'INT'],
|
||||
['ClienteFornitore', 'INT'],
|
||||
['ClienteAnalisi', 'INT'],
|
||||
['ClienteFatturazione', 'INT'], // ← NUOVO
|
||||
['MoltiplicatorePrezzo', 'INT'],
|
||||
['AnagraficaCertestObject', 'INT'],
|
||||
['AnagraficaCertestService', 'INT'],
|
||||
['ConsegnaRichiesta', 'DATE'],
|
||||
];
|
||||
|
||||
|
||||
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)
|
||||
");
|
||||
|
||||
foreach ($fixedFields as $f) {
|
||||
$ins->execute([
|
||||
':template_id' => $templateId,
|
||||
':fixed_field_key' => $f[0],
|
||||
':data_type' => $f[1],
|
||||
]);
|
||||
}
|
||||
|
||||
$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
|
||||
");
|
||||
|
||||
$stmt->execute([$templateId]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode(['success' => true, 'created' => count($rows), 'rows' => $rows]);
|
||||
} catch (Exception $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 3) . '/vendor/autoload.php'; // Risale a root/vendor/
|
||||
require_once dirname(__FILE__) . '/../class/VisualLimsApiClient.class.php'; // In root/public/userarea/class/
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
// Debug: Log the path where we expect the .env file
|
||||
$envPath = dirname(__DIR__, 3);
|
||||
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . ' - Expected .env path: ' . $envPath . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Carica il file .env dalla root del progetto
|
||||
try {
|
||||
$dotenv = Dotenv::createImmutable($envPath);
|
||||
$dotenv->load();
|
||||
} catch (Exception $e) {
|
||||
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - Errore caricamento .env: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
|
||||
|
||||
// Recupera le variabili d'ambiente
|
||||
$dbHost = $_ENV['DB_HOST'];
|
||||
$dbName = $_ENV['DB_DATABASE'];
|
||||
$dbUser = $_ENV['DB_USERNAME'];
|
||||
$dbPass = $_ENV['DB_PASSWORD'];
|
||||
$dbPrefix = $_ENV['DB_PREFIX'];
|
||||
|
||||
// Debug: Log database connection details (excluding password)
|
||||
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . " - DB Connection: host=$dbHost, dbname=$dbName, user=$dbUser, prefix=$dbPrefix" . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Connessione al database MySQL
|
||||
try {
|
||||
$pdo = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8mb4", $dbUser, $dbPass);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
} catch (PDOException $e) {
|
||||
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - Errore connessione DB: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
// Endpoint per recuperare le Matrici
|
||||
$endpoint = 'Matrice';
|
||||
|
||||
// (Opzionale) aggiungi parametri
|
||||
$options = []; // es. ['$top' => 100]
|
||||
|
||||
// Debug: salva URL usato
|
||||
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
|
||||
$query = http_build_query($options);
|
||||
$full_url = $base_url . $endpoint . ($query ? '?' . $query : '');
|
||||
file_put_contents(__DIR__ . '/last_url.txt', $full_url . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Chiamata API
|
||||
$data = $api->get($endpoint, $options);
|
||||
|
||||
// Debug: Log the API response size
|
||||
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . ' - API response received, value count: ' . (isset($data['value']) ? count($data['value']) : 0) . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Salva il JSON in locale (opzionale, per debug)
|
||||
file_put_contents(__DIR__ . '/matrici_response.json', json_encode($data, JSON_PRETTY_PRINT));
|
||||
|
||||
// Svuota la tabella (dump rapido)
|
||||
$pdo->exec("TRUNCATE TABLE {$dbPrefix}matrici");
|
||||
|
||||
// Debug: Log after truncate
|
||||
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . ' - Table truncated: ' . $dbPrefix . 'matrici' . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Prepara l'insert
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO {$dbPrefix}matrici (
|
||||
IdMatrice, NomeMatriceTraduzione, DescrizioneTraduzione, MacroMatrice, NomeMatrice, Descrizione
|
||||
) VALUES (
|
||||
:IdMatrice, :NomeMatriceTraduzione, :DescrizioneTraduzione, :MacroMatrice, :NomeMatrice, :Descrizione
|
||||
)
|
||||
");
|
||||
|
||||
// Inserisci i dati
|
||||
$insertedRows = 0;
|
||||
if (isset($data['value']) && is_array($data['value'])) {
|
||||
foreach ($data['value'] as $item) {
|
||||
$stmt->execute([
|
||||
':IdMatrice' => $item['IdMatrice'],
|
||||
':NomeMatriceTraduzione' => $item['NomeMatriceTraduzione'],
|
||||
':DescrizioneTraduzione' => $item['DescrizioneTraduzione'] ?? null,
|
||||
':MacroMatrice' => $item['MacroMatrice'] ?? null,
|
||||
':NomeMatrice' => $item['NomeMatrice'],
|
||||
':Descrizione' => $item['Descrizione'] ?? null,
|
||||
]);
|
||||
$insertedRows++;
|
||||
// Debug: Log each inserted row
|
||||
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . ' - Inserted row with IdMatrice: ' . $item['IdMatrice'] . PHP_EOL, FILE_APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
// Log successo
|
||||
file_put_contents(__DIR__ . '/success_log.txt', date('Y-m-d H:i:s') . ' - Aggiornamento completato: ' . $insertedRows . ' record inseriti.' . PHP_EOL, FILE_APPEND);
|
||||
} catch (Exception $e) {
|
||||
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
exit(0); // Esci con successo per cron
|
||||
@@ -0,0 +1 @@
|
||||
https://93.43.5.102/limsapi/api/odata/Matrice
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
2026-04-03 10:07:01 - Aggiornamento completato: 3198 record inseriti.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,290 @@
|
||||
<?php
|
||||
require_once "class/VisualLimsApiClient.class.php";
|
||||
include('include/headscript.php');
|
||||
|
||||
// Force HTML response
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
|
||||
// Initialize variables
|
||||
$error = null;
|
||||
$result = null;
|
||||
$entityType = $_GET['entity_type'] ?? 'CommessaWeb';
|
||||
$entityId = isset($_GET['entity_id']) ? (int)$_GET['entity_id'] : 0;
|
||||
$customExpand = trim($_GET['expand'] ?? '');
|
||||
$rawEndpoint = null;
|
||||
|
||||
function h($value)
|
||||
{
|
||||
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function renderArrayAsTable(array $items)
|
||||
{
|
||||
if (empty($items)) {
|
||||
echo '<div class="alert alert-secondary">No records found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
$firstRow = reset($items);
|
||||
if (!is_array($firstRow)) {
|
||||
echo '<pre class="bg-light p-3 border rounded">' . h(json_encode($items, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) . '</pre>';
|
||||
return;
|
||||
}
|
||||
|
||||
$headers = [];
|
||||
foreach ($items as $row) {
|
||||
if (is_array($row)) {
|
||||
$headers = array_unique(array_merge($headers, array_keys($row)));
|
||||
}
|
||||
}
|
||||
|
||||
echo '<div class="table-responsive">';
|
||||
echo '<table class="table table-striped table-bordered table-sm align-middle">';
|
||||
echo '<thead class="table-dark"><tr>';
|
||||
foreach ($headers as $header) {
|
||||
echo '<th>' . h($header) . '</th>';
|
||||
}
|
||||
echo '</tr></thead><tbody>';
|
||||
|
||||
foreach ($items as $row) {
|
||||
echo '<tr>';
|
||||
foreach ($headers as $header) {
|
||||
$value = $row[$header] ?? '';
|
||||
if (is_array($value) || is_object($value)) {
|
||||
$value = json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
echo '<td><pre style="white-space:pre-wrap; margin:0;">' . h($value) . '</pre></td>';
|
||||
} else {
|
||||
echo '<td>' . h($value) . '</td>';
|
||||
}
|
||||
}
|
||||
echo '</tr>';
|
||||
}
|
||||
|
||||
echo '</tbody></table>';
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
try {
|
||||
if ($entityId > 0) {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
// Default expands for better debugging
|
||||
if ($customExpand !== '') {
|
||||
$expand = $customExpand;
|
||||
} else {
|
||||
if ($entityType === 'CommessaWeb') {
|
||||
$expand = implode(',', [
|
||||
'CommesseCustomFields($expand=CustomField)',
|
||||
'Campioni'
|
||||
]);
|
||||
} else {
|
||||
$expand = implode(',', [
|
||||
'CommesseCustomFields($expand=CustomField)',
|
||||
'Campioni'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$rawEndpoint = $entityType . '(' . $entityId . ')?$expand=' . $expand;
|
||||
$result = $api->get($rawEndpoint);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Debug Commessa API</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {
|
||||
background: #f5f7fb;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
.debug-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.08);
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.label-box {
|
||||
display: inline-block;
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
background: #eef3ff;
|
||||
color: #2446a8;
|
||||
font-weight: 600;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mini-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mini-box {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e4e7eb;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.mini-box strong {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: #1f2937;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container py-4">
|
||||
<div class="debug-card">
|
||||
<h2 class="mb-3">Commessa / CommessaWeb API Inspector</h2>
|
||||
|
||||
<form method="GET" class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Entity Type</label>
|
||||
<select name="entity_type" class="form-select">
|
||||
<option value="CommessaWeb" <?= $entityType === 'CommessaWeb' ? 'selected' : '' ?>>CommessaWeb</option>
|
||||
<option value="Commessa" <?= $entityType === 'Commessa' ? 'selected' : '' ?>>Commessa</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Entity ID</label>
|
||||
<input type="number" name="entity_id" class="form-control" value="<?= h($entityId) ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5">
|
||||
<label class="form-label">Expand</label>
|
||||
<input type="text" name="expand" class="form-control" value="<?= h($customExpand) ?>" placeholder="CommesseCustomFields($expand=CustomField),Campioni">
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100">Load Data</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php if ($rawEndpoint): ?>
|
||||
<div class="debug-card">
|
||||
<div class="section-title">Requested Endpoint</div>
|
||||
<pre><?= h($rawEndpoint) ?></pre>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="debug-card">
|
||||
<div class="alert alert-danger mb-0">
|
||||
<strong>API Error:</strong><br>
|
||||
<?= h($error) ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($result && is_array($result)): ?>
|
||||
<div class="debug-card">
|
||||
<div class="section-title">Main Information</div>
|
||||
<div class="mini-grid">
|
||||
<div class="mini-box">
|
||||
<strong>ID</strong>
|
||||
<?= h($result['IdCommessa'] ?? $result['IdCommessaWeb'] ?? '') ?>
|
||||
</div>
|
||||
<div class="mini-box">
|
||||
<strong>Code</strong>
|
||||
<?= h($result['CodiceCommessa'] ?? '') ?>
|
||||
</div>
|
||||
<div class="mini-box">
|
||||
<strong>Cliente</strong>
|
||||
<?= h($result['Cliente'] ?? '') ?>
|
||||
</div>
|
||||
<div class="mini-box">
|
||||
<strong>SchemaCustomField</strong>
|
||||
<?= h($result['SchemaCustomField'] ?? '') ?>
|
||||
</div>
|
||||
<div class="mini-box">
|
||||
<strong>Richiedente</strong>
|
||||
<?= h($result['Richiedente'] ?? '') ?>
|
||||
</div>
|
||||
<div class="mini-box">
|
||||
<strong>Descrizione</strong>
|
||||
<?= h($result['Descrizione'] ?? '') ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="debug-card">
|
||||
<div class="section-title">Direct Properties</div>
|
||||
<pre><?= h(json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) ?></pre>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($result['CommesseCustomFields']) && is_array($result['CommesseCustomFields'])): ?>
|
||||
<div class="debug-card">
|
||||
<div class="section-title">CommesseCustomFields</div>
|
||||
<?php
|
||||
$fieldsRows = [];
|
||||
foreach ($result['CommesseCustomFields'] as $field) {
|
||||
$fieldsRows[] = [
|
||||
'IdCommesseCustomFields' => $field['IdCommesseCustomFields'] ?? '',
|
||||
'Valore' => $field['Valore'] ?? '',
|
||||
'CustomFieldId' => $field['CustomField']['IdCustomField'] ?? '',
|
||||
'Label' => $field['CustomField']['Descrizione'] ?? ($field['CustomField']['Name'] ?? ''),
|
||||
'Tipo' => $field['CustomField']['Tipo'] ?? '',
|
||||
];
|
||||
}
|
||||
renderArrayAsTable($fieldsRows);
|
||||
?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($result['Campioni']) && is_array($result['Campioni'])): ?>
|
||||
<div class="debug-card">
|
||||
<div class="section-title">Campioni</div>
|
||||
<?php renderArrayAsTable($result['Campioni']); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="debug-card">
|
||||
<div class="section-title">Raw JSON</div>
|
||||
<pre><?= h(json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) ?></pre>
|
||||
</div>
|
||||
<?php elseif ($entityId > 0 && !$error): ?>
|
||||
<div class="debug-card">
|
||||
<div class="alert alert-warning mb-0">No data returned from API</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
<!-- Example of use
|
||||
debug_commessa_api.php?entity_type=CommessaWeb&entity_id=564779
|
||||
debug_commessa_api.php?entity_type=Commessa&entity_id=12345
|
||||
debug_commessa_api.php?entity_type=CommessaWeb&entity_id=564779&expand=CommesseCustomFields($expand=CustomField),Campioni
|
||||
-->
|
||||
@@ -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()]);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$partId = isset($_POST['part_id']) ? (int)$_POST['part_id'] : 0;
|
||||
$analysisRecordkey = trim($_POST['analysis_recordkey'] ?? '');
|
||||
$rowId = isset($_POST['row_id']) && $_POST['row_id'] !== '' ? (int)$_POST['row_id'] : 0;
|
||||
|
||||
if ($partId <= 0 || ($analysisRecordkey === '' && $rowId <= 0)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Missing required data']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
if ($rowId > 0) {
|
||||
$stmt = $pdo->prepare("
|
||||
DELETE FROM identification_parts_analyses
|
||||
WHERE id = :id
|
||||
AND part_id = :part_id
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([
|
||||
':id' => $rowId,
|
||||
':part_id' => $partId,
|
||||
]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("
|
||||
DELETE FROM identification_parts_analyses
|
||||
WHERE part_id = :part_id
|
||||
AND analysis_recordkey = :analysis_recordkey
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([
|
||||
':part_id' => $partId,
|
||||
':analysis_recordkey' => $analysisRecordkey,
|
||||
]);
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Association deleted'
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include('include/headscript.php');
|
||||
|
||||
$dbHandler = DBHandlerSelect::getInstance();
|
||||
$pdo = $dbHandler->getConnection();
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$partId = $data['part_id'] ?? null;
|
||||
|
||||
if (!$partId) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID parte mancante']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("DELETE FROM identification_parts WHERE id = :part_id");
|
||||
$stmt->execute([':part_id' => $partId]);
|
||||
$rowCount = $stmt->rowCount();
|
||||
if ($rowCount > 0) {
|
||||
echo json_encode(['success' => true, 'message' => 'Parte eliminata con successo']);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => 'Nessuna parte trovata con ID ' . $partId]);
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore nell\'eliminazione: ' . $e->getMessage()]);
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,23 @@
|
||||
# Swagger Codegen Ignore
|
||||
# Generated by swagger-codegen https://github.com/swagger-api/swagger-codegen
|
||||
|
||||
# Use this file to prevent files from being overwritten by the generator.
|
||||
# The patterns follow closely to .gitignore or .dockerignore.
|
||||
|
||||
# As an example, the C# client generator defines ApiClient.cs.
|
||||
# You can make changes and tell Swagger Codgen to ignore just this file by uncommenting the following line:
|
||||
#ApiClient.cs
|
||||
|
||||
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
|
||||
#foo/*/qux
|
||||
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
|
||||
|
||||
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
|
||||
#foo/**/qux
|
||||
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
|
||||
|
||||
# You can also negate patterns with an exclamation (!).
|
||||
# For example, you can ignore all files in a docs folder with the file extension .md:
|
||||
#docs/*.md
|
||||
# Then explicitly reverse the ignore rule for a single file:
|
||||
#!docs/README.md
|
||||
@@ -0,0 +1 @@
|
||||
3.0.34
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
/**
|
||||
* Uso:
|
||||
* download_rapporto_file.php?id=123
|
||||
*/
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
$id = isset($_GET['id']) ? intval($_GET['id']) : 0;
|
||||
|
||||
if (!$id) {
|
||||
throw new Exception("ID RapportoFile mancante");
|
||||
}
|
||||
|
||||
$debugDir = __DIR__ . '/logs/lims_get_commessa/';
|
||||
|
||||
if (!is_dir($debugDir)) {
|
||||
mkdir($debugDir, 0755, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recupero il record RapportoFile.
|
||||
*/
|
||||
$data = $api->get("RapportoFile({$id})", []);
|
||||
|
||||
file_put_contents(
|
||||
$debugDir . "rapporto_file_{$id}.json",
|
||||
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
|
||||
/**
|
||||
* Provo vari nomi campo possibili per il contenuto PDF.
|
||||
* Dopo il primo test potremo adattarlo al nome esatto.
|
||||
*/
|
||||
$possibleContentFields = [
|
||||
'File',
|
||||
'file',
|
||||
'Content',
|
||||
'content',
|
||||
'FileContent',
|
||||
'fileContent',
|
||||
'Contenuto',
|
||||
'contenuto',
|
||||
'Bytes',
|
||||
'bytes'
|
||||
];
|
||||
|
||||
$base64 = null;
|
||||
|
||||
foreach ($possibleContentFields as $field) {
|
||||
if (!empty($data[$field])) {
|
||||
$base64 = $data[$field];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$base64) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Record RapportoFile recuperato, ma non ho trovato un campo base64 del PDF.',
|
||||
'hint' => 'Apri logs/lims_get_commessa/rapporto_file_' . $id . '.json e controlla il nome reale del campo file.',
|
||||
'data' => $data
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Se il contenuto ha prefisso data URI, lo pulisco.
|
||||
*/
|
||||
if (strpos($base64, 'base64,') !== false) {
|
||||
$parts = explode('base64,', $base64);
|
||||
$base64 = end($parts);
|
||||
}
|
||||
|
||||
$pdfBinary = base64_decode($base64, true);
|
||||
|
||||
if ($pdfBinary === false) {
|
||||
throw new Exception("Il contenuto trovato non è un base64 valido");
|
||||
}
|
||||
|
||||
$filename = $data['NomeFile']
|
||||
?? $data['nomeFile']
|
||||
?? $data['FileName']
|
||||
?? $data['fileName']
|
||||
?? "rapporto_{$id}.pdf";
|
||||
|
||||
if (!str_ends_with(strtolower($filename), '.pdf')) {
|
||||
$filename .= '.pdf';
|
||||
}
|
||||
|
||||
header('Content-Type: application/pdf');
|
||||
header('Content-Disposition: inline; filename="' . basename($filename) . '"');
|
||||
header('Content-Length: ' . strlen($pdfBinary));
|
||||
|
||||
echo $pdfBinary;
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +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
|
||||
@@ -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>
|
||||
@@ -0,0 +1,836 @@
|
||||
/**
|
||||
* exportLims_gridData.js — Export to LIMS using gridData (for imported.php)
|
||||
*
|
||||
* Replaces export_to_lims.js for pages that use gridData instead of DOM-rendered rows.
|
||||
* Single export + batch export (Export All) with validation.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
let pendingConfirmHandler = null;
|
||||
let batchRunning = false;
|
||||
Object.defineProperty(window, "batchRunning", { get: () => batchRunning });
|
||||
|
||||
let batchCancelled = false;
|
||||
let pendingBatchConfirmHandler = null;
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
function cleanupBackdrop() {
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
// Invia la mail di riepilogo per gli iddatadb processati (fire-and-forget).
|
||||
// Una sola chiamata a fine export (singolo o batch).
|
||||
function sendExportSummaryMail(iddatadbList) {
|
||||
const ids = (iddatadbList || [])
|
||||
.map((v) => parseInt(v, 10))
|
||||
.filter(Boolean);
|
||||
if (!ids.length) return;
|
||||
|
||||
const body = new URLSearchParams();
|
||||
ids.forEach((id) => body.append("ids[]", id));
|
||||
const tid = window.gridMeta && window.gridMeta.templateId;
|
||||
if (tid) body.append("template_id", tid);
|
||||
|
||||
fetch("send_export_summary.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: body.toString(),
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((res) => console.log("Summary mail:", res))
|
||||
.catch((err) => console.warn("Summary mail failed:", err));
|
||||
}
|
||||
|
||||
// ── Validation ──────────────────────────────────────────────────────
|
||||
|
||||
async function validateRows(rowsToValidate) {
|
||||
const response = await fetch("validate_export.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rows: rowsToValidate }),
|
||||
});
|
||||
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();
|
||||
});
|
||||
document
|
||||
.querySelectorAll(".grid-row.validation-row-error")
|
||||
.forEach((row) => row.classList.remove("validation-row-error"));
|
||||
clearAllRowErrors();
|
||||
}
|
||||
|
||||
function showValidationErrors(gridRow, iddatadb, errors) {
|
||||
// Store in gridData for re-render persistence
|
||||
const idx = getRowIndexByIddatadb(iddatadb);
|
||||
if (idx >= 0) window.gridData[idx]._validationErrors = errors;
|
||||
|
||||
if (!gridRow) return;
|
||||
gridRow.classList.add("validation-row-error");
|
||||
const messages = [];
|
||||
|
||||
errors.forEach((err) => {
|
||||
messages.push(err.message);
|
||||
if (!err.field) return;
|
||||
|
||||
let cell = null;
|
||||
if (err.field.startsWith("field_label:")) {
|
||||
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");
|
||||
});
|
||||
if (targetIndex) {
|
||||
cell = gridRow.querySelector(
|
||||
`.grid-cell[data-index="${targetIndex}"]`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
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"),
|
||||
);
|
||||
let tooltip = cell.querySelector(".validation-tooltip");
|
||||
if (!tooltip) {
|
||||
tooltip = document.createElement("div");
|
||||
tooltip.className = "validation-tooltip";
|
||||
cell.appendChild(tooltip);
|
||||
}
|
||||
tooltip.textContent = err.message;
|
||||
}
|
||||
});
|
||||
|
||||
showRowError(gridRow, iddatadb, messages.join("\n"));
|
||||
}
|
||||
|
||||
// ── Send export ─────────────────────────────────────────────────────
|
||||
|
||||
async function sendExport(iddatadb, batchUuid) {
|
||||
const formData = new FormData();
|
||||
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 data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Update gridData
|
||||
const idx = getRowIndexByIddatadb(iddatadb);
|
||||
if (idx >= 0) {
|
||||
window.gridData[idx].status = "l";
|
||||
window.gridData[idx].commessaweb = data.commessaweb;
|
||||
window.gridData[idx].noPriceCount = data.noPriceCount || 0;
|
||||
window.gridData[idx].noPriceAnalyses =
|
||||
data.noPriceAnalyses || [];
|
||||
}
|
||||
|
||||
// Update visible DOM row
|
||||
const gridRow = getGridRow(iddatadb);
|
||||
if (gridRow) {
|
||||
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"]',
|
||||
);
|
||||
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;";
|
||||
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) {
|
||||
exportBtn.disabled = true;
|
||||
exportBtn.style.background = "#ccc";
|
||||
exportBtn.style.cursor = "not-allowed";
|
||||
exportBtn.style.opacity = "0.5";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// ── Show result modal ───────────────────────────────────────────────
|
||||
|
||||
function showExportResult(data) {
|
||||
const el = document.getElementById("exportResponseModal");
|
||||
if (!el) return;
|
||||
const modal = new bootstrap.Modal(el, { keyboard: false });
|
||||
const msg = document.getElementById("exportResponseMessage");
|
||||
const label = document.getElementById("exportResponseModalLabel");
|
||||
|
||||
if (data.success) {
|
||||
msg.innerHTML =
|
||||
`${data.message.replace(/\n/g, "<br>")}` +
|
||||
`<br>ID CommessaWeb: ${data.idcommessaweb}` +
|
||||
`<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}`;
|
||||
label.textContent = "Errore Esportazione";
|
||||
}
|
||||
modal.show();
|
||||
el.addEventListener("hidden.bs.modal", cleanupBackdrop, { once: true });
|
||||
}
|
||||
|
||||
// ── Row UI helpers ──────────────────────────────────────────────────
|
||||
|
||||
function setRowExporting(row, active) {
|
||||
if (!row) return;
|
||||
const btnCell = row.querySelector(".button-cell");
|
||||
if (active) {
|
||||
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";
|
||||
});
|
||||
const spinner = document.createElement("span");
|
||||
spinner.className = "batch-row-spinner";
|
||||
spinner.innerHTML =
|
||||
'<i class="fas fa-spinner fa-spin"></i> Exporting...';
|
||||
btnCell.appendChild(spinner);
|
||||
}
|
||||
} else {
|
||||
row.classList.remove("batch-exporting");
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showRowError(row, iddatadb, message) {
|
||||
if (!row) return;
|
||||
row.classList.add("batch-row-error");
|
||||
const btnCell = row.querySelector(".button-cell");
|
||||
if (btnCell) {
|
||||
const old = btnCell.querySelector(".batch-error-msg");
|
||||
if (old) old.remove();
|
||||
const errorEl = document.createElement("div");
|
||||
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();
|
||||
});
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
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 = "";
|
||||
}
|
||||
}
|
||||
|
||||
// ── Single row export: validate → confirm → send ────────────────────
|
||||
|
||||
function startExportConfirmFlow(iddatadb, rowIndex) {
|
||||
const gridRow = getGridRow(iddatadb);
|
||||
clearValidationErrors();
|
||||
|
||||
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...';
|
||||
}
|
||||
|
||||
validateRows([{ iddatadb: parseInt(iddatadb), index: rowIndex }])
|
||||
.then((validationData) => {
|
||||
if (gridRow) {
|
||||
setRowExporting(gridRow, false);
|
||||
gridRow.classList.remove("batch-disabled");
|
||||
}
|
||||
|
||||
if (!validationData.success) {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
showConfirmAndExport(iddatadb, rowIndex);
|
||||
})
|
||||
.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");
|
||||
if (!confirmModalElement) return;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const confirmHandler = async () => {
|
||||
pendingConfirmHandler = null;
|
||||
confirmModal.hide();
|
||||
|
||||
const gridRow = getGridRow(iddatadb);
|
||||
if (gridRow) setRowExporting(gridRow, true);
|
||||
|
||||
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",
|
||||
);
|
||||
showExportResult(data);
|
||||
// Mail di riepilogo per la singola commessa
|
||||
sendExportSummaryMail([iddatadb]);
|
||||
} catch (error) {
|
||||
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);
|
||||
pendingConfirmHandler = confirmHandler;
|
||||
confirmBtn.addEventListener("click", confirmHandler, { once: true });
|
||||
}
|
||||
|
||||
// ── Single row click (event delegation) ─────────────────────────────
|
||||
|
||||
$(document).on("click", ".export-lims-btn", function (e) {
|
||||
e.preventDefault();
|
||||
if (batchRunning) return;
|
||||
|
||||
const iddatadb = this.dataset.iddatadb;
|
||||
const rowIndex = parseInt(this.dataset.row);
|
||||
const gridRow = getGridRow(iddatadb);
|
||||
|
||||
// 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 },
|
||||
);
|
||||
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 },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
startExportConfirmFlow(iddatadb, rowIndex);
|
||||
});
|
||||
|
||||
// ── Batch export (Export All) ───────────────────────────────────────
|
||||
|
||||
function collectEligibleRows() {
|
||||
// Read from gridData, not DOM. Se i filtri sono attivi, limita al target set.
|
||||
const target =
|
||||
typeof window.getTargetGridIds === "function"
|
||||
? window.getTargetGridIds()
|
||||
: null;
|
||||
const eligible = [];
|
||||
(window.gridData || []).forEach((row, index) => {
|
||||
if (row.status === "l") return;
|
||||
if (target && !target.has(parseInt(row.iddatadb, 10))) return;
|
||||
eligible.push({
|
||||
iddatadb: row.iddatadb,
|
||||
index,
|
||||
row: getGridRow(row.iddatadb),
|
||||
});
|
||||
});
|
||||
return eligible;
|
||||
}
|
||||
|
||||
function hasUnsavedChanges() {
|
||||
const target =
|
||||
typeof window.getTargetGridIds === "function"
|
||||
? window.getTargetGridIds()
|
||||
: null;
|
||||
return (window.gridData || []).some(
|
||||
(r) =>
|
||||
r._dirty && (!target || target.has(parseInt(r.iddatadb, 10))),
|
||||
);
|
||||
}
|
||||
|
||||
async function validateAndFilter(eligibleRows) {
|
||||
const rowsToValidate = eligibleRows.map(({ iddatadb, index }) => ({
|
||||
iddatadb: parseInt(iddatadb),
|
||||
index,
|
||||
}));
|
||||
const validationData = await validateRows(rowsToValidate);
|
||||
if (!validationData.success)
|
||||
throw new Error(validationData.message || "Validation error");
|
||||
|
||||
const validRows = [];
|
||||
let invalidCount = 0;
|
||||
|
||||
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,
|
||||
);
|
||||
invalidCount++;
|
||||
} else {
|
||||
validRows.push(item);
|
||||
}
|
||||
}
|
||||
return { validRows, invalidCount };
|
||||
}
|
||||
|
||||
function showValidationSpinner(show) {
|
||||
const bar = document.getElementById("batchExportBar");
|
||||
const statusEl = document.getElementById("batchExportStatus");
|
||||
const cancelBtn = document.getElementById("exportBatchCancelBtn");
|
||||
if (show) {
|
||||
if (bar) bar.style.display = "";
|
||||
if (statusEl) statusEl.textContent = "Validating...";
|
||||
if (cancelBtn) cancelBtn.style.display = "none";
|
||||
} else {
|
||||
if (bar) bar.style.display = "none";
|
||||
if (cancelBtn) cancelBtn.style.display = "";
|
||||
}
|
||||
}
|
||||
|
||||
function showBatchConfirm(eligibleRows) {
|
||||
clearValidationErrors();
|
||||
showValidationSpinner(true);
|
||||
|
||||
validateAndFilter(eligibleRows)
|
||||
.then(({ validRows, invalidCount }) => {
|
||||
showValidationSpinner(false);
|
||||
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();
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
confirmModal.show();
|
||||
|
||||
const confirmBtn = document.getElementById(
|
||||
"exportBatchConfirmBtn",
|
||||
);
|
||||
if (pendingBatchConfirmHandler)
|
||||
confirmBtn.removeEventListener(
|
||||
"click",
|
||||
pendingBatchConfirmHandler,
|
||||
);
|
||||
pendingBatchConfirmHandler = () => {
|
||||
pendingBatchConfirmHandler = null;
|
||||
confirmModal.hide();
|
||||
startBatchExport(validRows);
|
||||
};
|
||||
confirmBtn.addEventListener(
|
||||
"click",
|
||||
pendingBatchConfirmHandler,
|
||||
{ once: true },
|
||||
);
|
||||
})
|
||||
.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).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 },
|
||||
);
|
||||
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 },
|
||||
);
|
||||
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();
|
||||
return;
|
||||
}
|
||||
showBatchConfirm(eligibleRows);
|
||||
});
|
||||
|
||||
function startBatchExport(eligibleRows) {
|
||||
batchCancelled = false;
|
||||
batchRunning = true;
|
||||
const batchUuid = crypto.randomUUID();
|
||||
const total = eligibleRows.length;
|
||||
let processed = 0,
|
||||
succeeded = 0,
|
||||
failed = 0;
|
||||
const exportedList = []; // { commessaweb, noPriceCount, noPriceAnalyses }
|
||||
const processedIds = []; // iddatadb processati (ok o errore) per la mail
|
||||
|
||||
disableAllRowButtons();
|
||||
|
||||
const bar = document.getElementById("batchExportBar");
|
||||
const statusEl = document.getElementById("batchExportStatus");
|
||||
const cancelBtn = document.getElementById("exportBatchCancelBtn");
|
||||
if (bar) bar.style.display = "";
|
||||
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 },
|
||||
);
|
||||
|
||||
(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})...`;
|
||||
|
||||
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",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
processed++;
|
||||
failed++;
|
||||
const errIdx = getRowIndexByIddatadb(iddatadb);
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// Restore error state from gridData._exportError
|
||||
(window.gridData || []).forEach((row, idx) => {
|
||||
if (row._exportError) {
|
||||
const gridRow = getGridRow(row.iddatadb);
|
||||
if (gridRow)
|
||||
showRowError(gridRow, row.iddatadb, row._exportError);
|
||||
}
|
||||
});
|
||||
|
||||
const msgEl = document.getElementById("exportResponseMessage");
|
||||
const labelEl = document.getElementById("exportResponseModalLabel");
|
||||
|
||||
if (batchCancelled) {
|
||||
labelEl.textContent = "Export All — Cancelled";
|
||||
msgEl.innerHTML = `Exported: <strong>${succeeded}</strong><br>Errors: <strong>${failed}</strong><br>Not processed: <strong>${total - processed}</strong>`;
|
||||
} else if (failed === 0) {
|
||||
labelEl.textContent = "Export All — Complete";
|
||||
msgEl.innerHTML = `All <strong>${succeeded}</strong> rows exported successfully.`;
|
||||
} else {
|
||||
labelEl.textContent = "Export All — Completed with errors";
|
||||
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,
|
||||
});
|
||||
})();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,800 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
console.log("export_to_lims.js loaded");
|
||||
|
||||
const exportButtons = document.querySelectorAll(".export-lims-btn");
|
||||
console.log(`Found ${exportButtons.length} export-lims-btn buttons`);
|
||||
|
||||
// Tracks the active confirm handler so it can be replaced on re-open
|
||||
let pendingConfirmHandler = null;
|
||||
let batchRunning = false;
|
||||
// Expose for Save All to check
|
||||
Object.defineProperty(window, "batchRunning", {
|
||||
get: () => batchRunning,
|
||||
});
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
function cleanupBackdrop() {
|
||||
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";
|
||||
}
|
||||
|
||||
// ── Validation ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Call the validation endpoint for an array of { iddatadb, index } objects.
|
||||
* Returns the parsed JSON response.
|
||||
*/
|
||||
async function validateRows(rowsToValidate) {
|
||||
const response = await fetch("validate_export.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rows: rowsToValidate }),
|
||||
});
|
||||
if (!response.ok)
|
||||
throw new Error(`Validation HTTP error: ${response.status}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all validation-error highlights from the grid.
|
||||
*/
|
||||
function clearValidationErrors() {
|
||||
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");
|
||||
});
|
||||
// Also clear batch-row-error that came from validation
|
||||
clearAllRowErrors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight specific cells on a row based on validation errors.
|
||||
* Each error has { field, message }.
|
||||
* field can be: a data-col value, "parts", "field_label:SomeLabel", or null (row-level).
|
||||
*/
|
||||
function showValidationErrors(row, iddatadb, errors) {
|
||||
row.classList.add("validation-row-error");
|
||||
|
||||
const messages = [];
|
||||
|
||||
errors.forEach((err) => {
|
||||
messages.push(err.message);
|
||||
|
||||
if (!err.field) return; // row-level error, no specific cell
|
||||
|
||||
let cell = null;
|
||||
|
||||
if (err.field === "parts") {
|
||||
// No specific cell to highlight, but we highlight the parts button area
|
||||
// just add to messages
|
||||
return;
|
||||
} else if (err.field.startsWith("field_label:")) {
|
||||
// Match by field_label text — find the header with this label, get its index
|
||||
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");
|
||||
}
|
||||
});
|
||||
if (targetIndex) {
|
||||
const rowIndex = row.querySelector(".grid-cell[data-row]")?.getAttribute("data-row");
|
||||
if (rowIndex !== null) {
|
||||
cell = row.querySelector(`.grid-cell[data-row="${rowIndex}"][data-index="${targetIndex}"]`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Direct data-col match
|
||||
const rowIndex = row.querySelector(".grid-cell[data-row]")?.getAttribute("data-row");
|
||||
if (rowIndex !== null) {
|
||||
cell = row.querySelector(`.grid-cell[data-col="${err.field}"][data-row="${rowIndex}"]`);
|
||||
}
|
||||
}
|
||||
|
||||
if (cell) {
|
||||
cell.classList.add("validation-error");
|
||||
// Mark the input/select inside the cell
|
||||
cell.querySelectorAll("input, select").forEach((el) => {
|
||||
el.classList.add("input-validation-error");
|
||||
});
|
||||
// Add tooltip with error message
|
||||
let tooltip = cell.querySelector(".validation-tooltip");
|
||||
if (!tooltip) {
|
||||
tooltip = document.createElement("div");
|
||||
tooltip.className = "validation-tooltip";
|
||||
cell.appendChild(tooltip);
|
||||
}
|
||||
tooltip.textContent = err.message;
|
||||
}
|
||||
});
|
||||
|
||||
// Show aggregated error on the row using existing mechanism
|
||||
showRowError(row, iddatadb, messages.join("\n"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a single export request and update the row UI on success.
|
||||
* Returns the parsed JSON response.
|
||||
*/
|
||||
async function sendExport(iddatadb, gridRow, batchUuid = null) {
|
||||
const formData = new FormData();
|
||||
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 data = await response.json();
|
||||
|
||||
if (data.success && gridRow) {
|
||||
// Update 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";
|
||||
}
|
||||
|
||||
// Insert/update CommessaWeb code span
|
||||
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.title = "CommessaWeb";
|
||||
const hiddenInput = statusCell.querySelector(
|
||||
'input[type="hidden"]',
|
||||
);
|
||||
hiddenInput
|
||||
? statusCell.insertBefore(cwSpan, hiddenInput)
|
||||
: statusCell.appendChild(cwSpan);
|
||||
}
|
||||
cwSpan.textContent = data.commessaweb;
|
||||
}
|
||||
|
||||
// Disable export button for this row
|
||||
const exportBtn = gridRow.querySelector(".export-lims-btn");
|
||||
if (exportBtn) {
|
||||
exportBtn.disabled = true;
|
||||
exportBtn.style.background = "#ccc";
|
||||
exportBtn.style.cursor = "not-allowed";
|
||||
exportBtn.style.opacity = "0.5";
|
||||
exportBtn.title = "Già esportato";
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the result of a single export in the response modal.
|
||||
*/
|
||||
function showExportResult(data) {
|
||||
const responseModalElement =
|
||||
document.getElementById("exportResponseModal");
|
||||
if (!responseModalElement) {
|
||||
alert("Errore: Modale di risposta non trovato");
|
||||
return;
|
||||
}
|
||||
|
||||
const responseModal = new bootstrap.Modal(responseModalElement, {
|
||||
keyboard: false,
|
||||
});
|
||||
const responseMessage = document.getElementById(
|
||||
"exportResponseMessage",
|
||||
);
|
||||
|
||||
if (data.success) {
|
||||
responseMessage.innerHTML =
|
||||
`${data.message.replace(/\n/g, "<br>")}` +
|
||||
`<br>ID CommessaWeb: ${data.idcommessaweb}` +
|
||||
`<br>Codice CommessaWeb: ${data.commessaweb}` +
|
||||
(data.totalPhotos > 0
|
||||
? `<br>Foto trovate: ${data.totalPhotos}`
|
||||
: "");
|
||||
document.getElementById("exportResponseModalLabel").textContent =
|
||||
"Esportazione Completata";
|
||||
} else {
|
||||
responseMessage.textContent = `Errore durante la generazione dei payload: ${data.message}`;
|
||||
document.getElementById("exportResponseModalLabel").textContent =
|
||||
"Errore Esportazione";
|
||||
}
|
||||
|
||||
responseModal.show();
|
||||
responseModalElement.addEventListener(
|
||||
"hidden.bs.modal",
|
||||
cleanupBackdrop,
|
||||
{ once: true },
|
||||
);
|
||||
}
|
||||
|
||||
// ── Row button helpers (disable/enable during batch) ────────────────────
|
||||
|
||||
function setRowExporting(row, active) {
|
||||
const btnCell = row.querySelector(".button-cell");
|
||||
if (active) {
|
||||
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";
|
||||
});
|
||||
const spinner = document.createElement("span");
|
||||
spinner.className = "batch-row-spinner";
|
||||
spinner.innerHTML =
|
||||
'<i class="fas fa-spinner fa-spin"></i> Exporting...';
|
||||
btnCell.appendChild(spinner);
|
||||
}
|
||||
} else {
|
||||
row.classList.remove("batch-exporting");
|
||||
row.classList.add("batch-disabled");
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showRowError(row, iddatadb, message) {
|
||||
row.classList.add("batch-row-error");
|
||||
const btnCell = row.querySelector(".button-cell");
|
||||
if (btnCell) {
|
||||
// Remove existing error msg
|
||||
const old = btnCell.querySelector(".batch-error-msg");
|
||||
if (old) old.remove();
|
||||
|
||||
const errorEl = document.createElement("div");
|
||||
errorEl.className = "batch-error-msg";
|
||||
errorEl.textContent = "⚠ Errore — clicca per dettagli";
|
||||
errorEl.addEventListener("click", () => {
|
||||
document.getElementById("exportResponseMessage").innerHTML = message.replace(/\n/g, "<br>");
|
||||
document.getElementById("exportResponseModalLabel").textContent = "Errore Validazione (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();
|
||||
});
|
||||
}
|
||||
|
||||
function disableAllRowButtons() {
|
||||
document.querySelectorAll(".grid-row[data-id]").forEach((row) => {
|
||||
row.classList.add("batch-disabled");
|
||||
});
|
||||
// Disable Actions dropdown
|
||||
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 = "";
|
||||
}
|
||||
}
|
||||
|
||||
// ── Single row export: validate, confirm modal, then send ───────────────
|
||||
|
||||
function startExportConfirmFlow(iddatadb, btn) {
|
||||
const gridRow = btn.closest(".grid-row");
|
||||
const rowIndex = btn.dataset.row;
|
||||
|
||||
// Validate first
|
||||
clearValidationErrors();
|
||||
|
||||
// Show validating state on the row
|
||||
setRowExporting(gridRow, true);
|
||||
const spinner = gridRow.querySelector(".batch-row-spinner");
|
||||
if (spinner) spinner.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Validating...';
|
||||
|
||||
validateRows([{ iddatadb: parseInt(iddatadb), index: parseInt(rowIndex) }])
|
||||
.then((validationData) => {
|
||||
setRowExporting(gridRow, false);
|
||||
gridRow.classList.remove("batch-disabled");
|
||||
|
||||
if (!validationData.success) {
|
||||
showExportResult({ success: false, message: validationData.message || "Errore di validazione" });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = validationData.results[rowIndex];
|
||||
if (result && !result.valid) {
|
||||
// Show validation errors on the row
|
||||
showValidationErrors(gridRow, iddatadb, result.errors);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validation passed — show confirm modal
|
||||
showConfirmAndExport(iddatadb, btn);
|
||||
})
|
||||
.catch((error) => {
|
||||
setRowExporting(gridRow, false);
|
||||
gridRow.classList.remove("batch-disabled");
|
||||
|
||||
console.error("Validation error:", error);
|
||||
showExportResult({ success: false, message: "Errore di validazione: " + error.message });
|
||||
});
|
||||
}
|
||||
|
||||
function showConfirmAndExport(iddatadb, btn) {
|
||||
const confirmModalElement =
|
||||
document.getElementById("exportConfirmModal");
|
||||
if (!confirmModalElement) {
|
||||
alert("Errore: Modale di conferma non trovato");
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmModal = new bootstrap.Modal(confirmModalElement, {
|
||||
keyboard: false,
|
||||
});
|
||||
document.getElementById("exportIddatadb").textContent = iddatadb;
|
||||
confirmModal.show();
|
||||
|
||||
const confirmBtn = document.getElementById("exportConfirmBtn");
|
||||
if (!confirmBtn) {
|
||||
confirmModal.hide();
|
||||
alert("Errore: Pulsante di conferma non trovato");
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmHandler = async () => {
|
||||
pendingConfirmHandler = null;
|
||||
console.log(`Confirmed export for iddatadb: ${iddatadb}`);
|
||||
confirmModal.hide();
|
||||
|
||||
const gridRow = btn.closest(".grid-row");
|
||||
setRowExporting(gridRow, true);
|
||||
|
||||
try {
|
||||
const data = await sendExport(iddatadb, gridRow);
|
||||
console.log("Export response:", data);
|
||||
|
||||
// Stop spinner, fully restore row
|
||||
setRowExporting(gridRow, false);
|
||||
gridRow.classList.remove("batch-disabled");
|
||||
|
||||
if (!data.success) {
|
||||
showRowError(gridRow, iddatadb, data.message || "Errore sconosciuto");
|
||||
}
|
||||
showExportResult(data);
|
||||
} catch (error) {
|
||||
console.error("Export error:", error);
|
||||
|
||||
// Stop spinner, fully restore row so user can retry
|
||||
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);
|
||||
}
|
||||
pendingConfirmHandler = confirmHandler;
|
||||
confirmBtn.addEventListener("click", confirmHandler, { once: true });
|
||||
}
|
||||
|
||||
// ── Single row click handler ────────────────────────────────────────────
|
||||
|
||||
exportButtons.forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
if (batchRunning) return;
|
||||
|
||||
const rowIndex = btn.dataset.row;
|
||||
const iddatadb = btn.dataset.iddatadb;
|
||||
console.log(
|
||||
`Export to LIMS clicked for row ${rowIndex}, iddatadb: ${iddatadb}`,
|
||||
);
|
||||
|
||||
const gridRow = btn.closest(".grid-row");
|
||||
|
||||
if (gridRow && gridRow.querySelector(".cell-changed")) {
|
||||
const unsavedModal = new bootstrap.Modal(
|
||||
document.getElementById("exportUnsavedModal"),
|
||||
{ keyboard: false },
|
||||
);
|
||||
unsavedModal.show();
|
||||
|
||||
document.getElementById("saveAndExportBtn").addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
unsavedModal.hide();
|
||||
const saveBtn = gridRow.querySelector(".save-btn");
|
||||
if (!saveBtn) return;
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
if (!gridRow.querySelector(".cell-changed")) {
|
||||
observer.disconnect();
|
||||
startExportConfirmFlow(iddatadb, btn);
|
||||
}
|
||||
});
|
||||
observer.observe(gridRow, {
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
|
||||
saveBtn.click();
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// No unsaved changes — go straight to validate + export confirm
|
||||
startExportConfirmFlow(iddatadb, btn);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Batch export (Export All) ───────────────────────────────────────────
|
||||
|
||||
const exportAllBtn = document.querySelector(".export-all-lims-btn");
|
||||
if (!exportAllBtn) return;
|
||||
|
||||
let batchCancelled = false;
|
||||
let pendingBatchConfirmHandler = null;
|
||||
|
||||
function collectEligibleRows() {
|
||||
const allRows = document.querySelectorAll(".grid-row[data-id]");
|
||||
const eligible = [];
|
||||
allRows.forEach((row) => {
|
||||
const statusBadge = row.querySelector(
|
||||
'.grid-cell[data-col="status"] .status-badge',
|
||||
);
|
||||
if (statusBadge && !statusBadge.classList.contains("status-l")) {
|
||||
const iddatadb = row.dataset.id;
|
||||
if (iddatadb) {
|
||||
eligible.push({ iddatadb, row });
|
||||
}
|
||||
}
|
||||
});
|
||||
return eligible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data-row index for a grid row element.
|
||||
*/
|
||||
function getRowIndex(row) {
|
||||
const cell = row.querySelector(".grid-cell[data-row]");
|
||||
return cell ? parseInt(cell.getAttribute("data-row")) : null;
|
||||
}
|
||||
|
||||
function hasUnsavedChanges() {
|
||||
return !!document.querySelector(".grid-row[data-id] .cell-changed");
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate all eligible rows, show errors, and return only the valid ones.
|
||||
*/
|
||||
async function validateAndFilter(eligibleRows) {
|
||||
const rowsToValidate = eligibleRows.map(({ iddatadb, row }) => ({
|
||||
iddatadb: parseInt(iddatadb),
|
||||
index: getRowIndex(row),
|
||||
}));
|
||||
|
||||
const validationData = await validateRows(rowsToValidate);
|
||||
|
||||
if (!validationData.success) {
|
||||
throw new Error(validationData.message || "Errore di validazione");
|
||||
}
|
||||
|
||||
const validRows = [];
|
||||
let invalidCount = 0;
|
||||
|
||||
for (const { iddatadb, row } of eligibleRows) {
|
||||
const rowIdx = getRowIndex(row);
|
||||
const result = validationData.results[rowIdx];
|
||||
|
||||
if (result && !result.valid) {
|
||||
showValidationErrors(row, iddatadb, result.errors);
|
||||
invalidCount++;
|
||||
} else {
|
||||
validRows.push({ iddatadb, row });
|
||||
}
|
||||
}
|
||||
|
||||
return { validRows, invalidCount };
|
||||
}
|
||||
|
||||
function showValidationSpinner(show) {
|
||||
const bar = document.getElementById("batchExportBar");
|
||||
const statusEl = document.getElementById("batchExportStatus");
|
||||
const cancelBtn = document.getElementById("exportBatchCancelBtn");
|
||||
if (show) {
|
||||
bar.style.display = "";
|
||||
statusEl.textContent = "Validazione in corso...";
|
||||
cancelBtn.style.display = "none";
|
||||
} else {
|
||||
bar.style.display = "none";
|
||||
cancelBtn.style.display = "";
|
||||
}
|
||||
}
|
||||
|
||||
function showBatchConfirm(eligibleRows) {
|
||||
clearValidationErrors();
|
||||
showValidationSpinner(true);
|
||||
|
||||
// Validate before showing confirm
|
||||
validateAndFilter(eligibleRows)
|
||||
.then(({ validRows, invalidCount }) => {
|
||||
showValidationSpinner(false);
|
||||
if (validRows.length === 0) {
|
||||
document.getElementById("exportResponseMessage").innerHTML =
|
||||
`Nessuna riga valida per l'esportazione.<br>` +
|
||||
`<strong>${invalidCount}</strong> righe con errori di validazione.`;
|
||||
document.getElementById("exportResponseModalLabel").textContent =
|
||||
"Validazione Fallita";
|
||||
new bootstrap.Modal(
|
||||
document.getElementById("exportResponseModal"),
|
||||
{ keyboard: false },
|
||||
).show();
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmModal = new bootstrap.Modal(
|
||||
document.getElementById("exportBatchConfirmModal"),
|
||||
{ keyboard: false },
|
||||
);
|
||||
|
||||
let countText = String(validRows.length);
|
||||
if (invalidCount > 0) {
|
||||
countText += ` (${invalidCount} escluse per errori di validazione)`;
|
||||
}
|
||||
document.getElementById("exportBatchCount").textContent = countText;
|
||||
confirmModal.show();
|
||||
|
||||
const confirmBtn = document.getElementById("exportBatchConfirmBtn");
|
||||
if (pendingBatchConfirmHandler) {
|
||||
confirmBtn.removeEventListener("click", pendingBatchConfirmHandler);
|
||||
}
|
||||
pendingBatchConfirmHandler = () => {
|
||||
pendingBatchConfirmHandler = null;
|
||||
confirmModal.hide();
|
||||
startBatchExport(validRows);
|
||||
};
|
||||
confirmBtn.addEventListener("click", pendingBatchConfirmHandler, { once: true });
|
||||
})
|
||||
.catch((error) => {
|
||||
showValidationSpinner(false);
|
||||
console.error("Batch validation error:", error);
|
||||
document.getElementById("exportResponseMessage").textContent =
|
||||
"Errore di validazione: " + error.message;
|
||||
document.getElementById("exportResponseModalLabel").textContent =
|
||||
"Errore Validazione";
|
||||
new bootstrap.Modal(
|
||||
document.getElementById("exportResponseModal"),
|
||||
{ keyboard: false },
|
||||
).show();
|
||||
});
|
||||
}
|
||||
|
||||
exportAllBtn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
if (batchRunning) return;
|
||||
|
||||
// Check unsaved changes first
|
||||
if (hasUnsavedChanges()) {
|
||||
const unsavedModal = new bootstrap.Modal(
|
||||
document.getElementById("exportBatchUnsavedModal"),
|
||||
{ keyboard: false },
|
||||
);
|
||||
unsavedModal.show();
|
||||
|
||||
document
|
||||
.getElementById("batchSaveAndExportBtn")
|
||||
.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
unsavedModal.hide();
|
||||
// Trigger Save All, then proceed
|
||||
const saveAllEl =
|
||||
document.querySelector(".save-all-btn");
|
||||
if (!saveAllEl) return;
|
||||
|
||||
// Watch for all .cell-changed to disappear
|
||||
const observer = new MutationObserver(() => {
|
||||
if (!hasUnsavedChanges()) {
|
||||
observer.disconnect();
|
||||
const eligibleRows = collectEligibleRows();
|
||||
if (eligibleRows.length === 0) {
|
||||
document.getElementById(
|
||||
"exportResponseMessage",
|
||||
).textContent =
|
||||
"Tutte le righe sono già state esportate al LIMS.";
|
||||
document.getElementById(
|
||||
"exportResponseModalLabel",
|
||||
).textContent = "Export All";
|
||||
new bootstrap.Modal(
|
||||
document.getElementById(
|
||||
"exportResponseModal",
|
||||
),
|
||||
{ keyboard: false },
|
||||
).show();
|
||||
return;
|
||||
}
|
||||
showBatchConfirm(eligibleRows);
|
||||
}
|
||||
});
|
||||
observer.observe(
|
||||
document.querySelector(".grid-container"),
|
||||
{
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
},
|
||||
);
|
||||
|
||||
saveAllEl.click();
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const eligibleRows = collectEligibleRows();
|
||||
|
||||
if (eligibleRows.length === 0) {
|
||||
document.getElementById("exportResponseMessage").textContent =
|
||||
"Tutte le righe sono già state esportate al LIMS.";
|
||||
document.getElementById("exportResponseModalLabel").textContent =
|
||||
"Export All";
|
||||
const modal = new bootstrap.Modal(
|
||||
document.getElementById("exportResponseModal"),
|
||||
{ keyboard: false },
|
||||
);
|
||||
modal.show();
|
||||
return;
|
||||
}
|
||||
|
||||
showBatchConfirm(eligibleRows);
|
||||
});
|
||||
|
||||
function startBatchExport(eligibleRows) {
|
||||
batchCancelled = false;
|
||||
batchRunning = true;
|
||||
// Don't clear validation errors — they should stay visible for invalid rows
|
||||
const batchUuid = crypto.randomUUID();
|
||||
const total = eligibleRows.length;
|
||||
let processed = 0;
|
||||
let succeeded = 0;
|
||||
let failed = 0;
|
||||
|
||||
// Disable all row buttons
|
||||
disableAllRowButtons();
|
||||
|
||||
// Show inline status bar
|
||||
const bar = document.getElementById("batchExportBar");
|
||||
const statusEl = document.getElementById("batchExportStatus");
|
||||
const cancelBtn = document.getElementById("exportBatchCancelBtn");
|
||||
bar.style.display = "";
|
||||
cancelBtn.disabled = false;
|
||||
statusEl.textContent = `Esportazione 0 / ${total}...`;
|
||||
|
||||
// Cancel handler
|
||||
cancelBtn.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
batchCancelled = true;
|
||||
statusEl.textContent =
|
||||
"Annullamento... (attendi riga corrente)";
|
||||
cancelBtn.disabled = true;
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
|
||||
(async () => {
|
||||
for (let i = 0; i < eligibleRows.length; i++) {
|
||||
if (batchCancelled) break;
|
||||
|
||||
const { iddatadb, row } = eligibleRows[i];
|
||||
statusEl.textContent = `Esportazione ${processed + 1} / ${total} (id: ${iddatadb})...`;
|
||||
|
||||
// Highlight current row
|
||||
setRowExporting(row, true);
|
||||
|
||||
try {
|
||||
const data = await sendExport(iddatadb, row, batchUuid);
|
||||
processed++;
|
||||
if (data.success) {
|
||||
succeeded++;
|
||||
} else {
|
||||
failed++;
|
||||
showRowError(row, iddatadb, data.message || "Errore sconosciuto");
|
||||
}
|
||||
} catch (error) {
|
||||
processed++;
|
||||
failed++;
|
||||
showRowError(row, iddatadb, error.message);
|
||||
}
|
||||
|
||||
// Remove highlight from current row
|
||||
setRowExporting(row, false);
|
||||
}
|
||||
|
||||
// Finished
|
||||
batchRunning = false;
|
||||
enableAllRowButtons();
|
||||
bar.style.display = "none";
|
||||
|
||||
// Show result modal
|
||||
const msgEl = document.getElementById("exportResponseMessage");
|
||||
const labelEl = document.getElementById("exportResponseModalLabel");
|
||||
|
||||
if (batchCancelled) {
|
||||
labelEl.textContent = "Export All — Annullato";
|
||||
msgEl.innerHTML =
|
||||
`Esportate: <strong>${succeeded}</strong><br>` +
|
||||
`Errori: <strong>${failed}</strong><br>` +
|
||||
`Non processate: <strong>${total - processed}</strong>`;
|
||||
} else if (failed === 0) {
|
||||
labelEl.textContent = "Export All — Completato";
|
||||
msgEl.innerHTML = `Tutte le <strong>${succeeded}</strong> righe esportate con successo.`;
|
||||
} else {
|
||||
labelEl.textContent = "Export All — Completato con errori";
|
||||
msgEl.innerHTML =
|
||||
`Esportate: <strong>${succeeded}</strong><br>` +
|
||||
`Errori: <strong>${failed}</strong>`;
|
||||
}
|
||||
|
||||
const modalEl = document.getElementById("exportResponseModal");
|
||||
const modal = new bootstrap.Modal(modalEl, { keyboard: false });
|
||||
modal.show();
|
||||
modalEl.addEventListener("hidden.bs.modal", cleanupBackdrop, { once: true });
|
||||
})();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,891 @@
|
||||
<?php
|
||||
require_once "class/VisualLimsApiClient.class.php";
|
||||
include('include/headscript.php');
|
||||
|
||||
$dbHandler = DBHandlerSelect::getInstance();
|
||||
$pdo = $dbHandler->getConnection();
|
||||
|
||||
header("Content-Type: application/json");
|
||||
|
||||
// 🔹 Configura directory log (creala se non esiste)
|
||||
$logDir = __DIR__ . '/logs/api/';
|
||||
if (!is_dir($logDir)) {
|
||||
mkdir($logDir, 0755, true);
|
||||
}
|
||||
|
||||
$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;
|
||||
|
||||
// File unico per questa commessa (nominato per iddatadb, che conosciamo sempre)
|
||||
$iddatadbForLog = $_POST['iddatadb'] ?? 'unknown';
|
||||
$commessaLogFile = $logDir . "commessa_{$iddatadbForLog}_" . time() . ".log";
|
||||
|
||||
// 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)
|
||||
{
|
||||
// Prova a validare come data (accetta formati comuni)
|
||||
$date = DateTime::createFromFormat('Y-m-d', $value) ?: DateTime::createFromFormat('Y-m-d H:i:s', $value);
|
||||
if ($date) {
|
||||
return $date->format('Y-m-d\TH:i:sP'); // Formato ISO 8601
|
||||
}
|
||||
return null; // Imposta null se non è una data valida
|
||||
}
|
||||
|
||||
// 🔹 Funzione per validare e convertire date
|
||||
function formatDateToExport($value)
|
||||
{
|
||||
$date = DateTime::createFromFormat('Y-m-d', $value) ?: DateTime::createFromFormat('Y-m-d H:i:s', $value);
|
||||
if ($date) {
|
||||
return $date->format('d/m/Y');
|
||||
}
|
||||
return null; // Imposta null se non è una data valida
|
||||
}
|
||||
|
||||
// ImportaCommessa con retry: la chiamata è asincrona lato LIMS e a volte
|
||||
// risponde 200 senza importare (StatoCommessaWeb resta "Inviata"/"Nuova").
|
||||
// Riprova con backoff esponenziale finché non passa a "Elaborata".
|
||||
function importaCommessaWithRetry($api, $commessaId, array $payload, $maxRetries = 3, $initialBackoff = 1)
|
||||
{
|
||||
$result = null;
|
||||
$stato = null;
|
||||
$succeeded = false;
|
||||
$log = "";
|
||||
$backoff = $initialBackoff;
|
||||
|
||||
set_time_limit(120); // i backoff non devono far scadere il timeout della richiesta
|
||||
|
||||
for ($attempt = 1; $attempt <= $maxRetries + 1; $attempt++) {
|
||||
try {
|
||||
$result = $api->post("CommessaWeb({$commessaId})/ImportaCommessa", $payload);
|
||||
$stato = $result['StatoCommessaWeb'] ?? null;
|
||||
$log .= "Attempt {$attempt}: HTTP 200, StatoCommessaWeb=" . ($stato ?? 'null') . "\n";
|
||||
} catch (Exception $e) {
|
||||
$stato = null;
|
||||
$log .= "Attempt {$attempt}: EXCEPTION " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
if ($stato === 'Elaborata') {
|
||||
$succeeded = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($attempt <= $maxRetries) {
|
||||
$log .= " -> not Elaborata, waiting {$backoff}s before retry\n";
|
||||
sleep($backoff);
|
||||
$backoff *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'succeeded' => $succeeded,
|
||||
'stato' => $stato,
|
||||
'result' => $result,
|
||||
'log' => $log,
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$iddatadb = $_POST['iddatadb'] ?? null;
|
||||
if (!$iddatadb) {
|
||||
throw new Exception("Missing iddatadb");
|
||||
}
|
||||
|
||||
// TEMP: simulate error on every other row for testing
|
||||
if (env('SIMULATE_EXPORT_LIMS') && $iddatadb % 2 === 0) {
|
||||
throw new Exception("Simulated error for iddatadb $iddatadb");
|
||||
}
|
||||
|
||||
// 🔹 STEP 1+2: Fetch Cliente ID from datadb and Schema ID from excel_templates
|
||||
// Also fetch fixed fields stored in datadb
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT d.idclient AS clienteId, et.idschema AS schemaId, et.id AS templateId,
|
||||
et.template_note,
|
||||
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
|
||||
WHERE d.iddatadb = :iddatadb
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute(['iddatadb' => $iddatadb]);
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$result) {
|
||||
throw new Exception("No Cliente/Schema found for iddatadb {$iddatadb}");
|
||||
}
|
||||
|
||||
$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;
|
||||
$moltiplicatorePrezzo = !empty($result['moltiplicatore_prezzo_id']) ? (int) $result['moltiplicatore_prezzo_id'] : null;
|
||||
$anagraficaObject = !empty($result['anagrafica_certest_object_id']) ? (int) $result['anagrafica_certest_object_id'] : null;
|
||||
$anagraficaService = !empty($result['anagrafica_certest_service_id']) ? (int) $result['anagrafica_certest_service_id'] : null;
|
||||
$clienteFornitore = !empty($result['cliente_fornitore_id']) ? (int) $result['cliente_fornitore_id'] : null;
|
||||
$clienteAnalisi = !empty($result['clienteAnalisi']) ? (int) $result['clienteAnalisi'] : null;
|
||||
$clienteFatturazione = !empty($result['ClienteFatturazione']) ? (int) $result['ClienteFatturazione'] : null;
|
||||
$consegnaRichiesta = !empty($result['consegna_richiesta']) ? $result['consegna_richiesta'] : null;
|
||||
|
||||
// 🔹 STEP 3: Fetch Parts (including idmatrice and part id for custom fields)
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id AS part_id, part_number, part_description, material, color, mix, idmatrice, dateexpiry, note
|
||||
FROM identification_parts
|
||||
WHERE iddatadb = :iddatadb
|
||||
AND part_description IS NOT NULL
|
||||
AND TRIM(part_description) <> ''
|
||||
ORDER BY CAST(part_number AS UNSIGNED) ASC, part_number ASC
|
||||
");
|
||||
$stmt->execute(['iddatadb' => $iddatadb]);
|
||||
$parts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 🔹 STEP 3.1: Fetch custom field values per part from identification_parts_customfields
|
||||
$partIds = array_column($parts, 'part_id');
|
||||
$partsCustomFields = []; // part_id => [ { field_id, value_id, value_text }, ... ]
|
||||
if (!empty($partIds)) {
|
||||
$placeholders = implode(',', array_fill(0, count($partIds), '?'));
|
||||
$cfStmt = $pdo->prepare("
|
||||
SELECT part_id, field_id, value_id, value_text
|
||||
FROM identification_parts_customfields
|
||||
WHERE part_id IN ({$placeholders})
|
||||
");
|
||||
$cfStmt->execute($partIds);
|
||||
foreach ($cfStmt->fetchAll(PDO::FETCH_ASSOC) as $cfRow) {
|
||||
$partsCustomFields[(int)$cfRow['part_id']][] = $cfRow;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 🔹 STEP 4a: Auto-populate export_date / export_time fields
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE import_data_details idd
|
||||
JOIN template_mapping m ON idd.mapping_id = m.id
|
||||
SET idd.field_value = CASE m.auto_value
|
||||
WHEN 'export_date' THEN :export_date
|
||||
WHEN 'export_time' THEN :export_time
|
||||
END
|
||||
WHERE idd.id = :iddatadb
|
||||
AND m.auto_value IN ('export_date', 'export_time')
|
||||
");
|
||||
$stmt->execute([
|
||||
'iddatadb' => $iddatadb,
|
||||
'export_date' => date('Y-m-d'),
|
||||
'export_time' => date('H:i'),
|
||||
]);
|
||||
|
||||
// 🔹 STEP 4: Fetch Field Values with Labels
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
idd.field_value,
|
||||
m.field_label,
|
||||
m.schema_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
|
||||
WHERE idd.id = :iddatadb
|
||||
");
|
||||
$stmt->execute(['iddatadb' => $iddatadb]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$fieldValues = [];
|
||||
$valueMap = [];
|
||||
foreach ($rows as $row) {
|
||||
$fieldValues[] = [
|
||||
"IdCommesseCustomFields" => (int) $row['field_id'],
|
||||
"Valore" => $row['field_value'],
|
||||
"FieldLabel" => $row['field_label']
|
||||
];
|
||||
$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);
|
||||
|
||||
// 🔹 Initialize API client
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
// 🔹 STEP 5: Create CommessaWeb
|
||||
// Fixed fields are sent as direct properties — they are navigation properties on Commessa
|
||||
// accepted by the XAF API even though not explicitly listed in OData $metadata
|
||||
$commessaWebPayload = [
|
||||
"Cliente" => $clienteId,
|
||||
"SchemaCustomField" => $schemaId,
|
||||
"Richiedente" => "From TRFSmart Application", // TODO: replace with real value
|
||||
"Descrizione" => "From TRFSmart Application", // TODO: replace with real value
|
||||
"ClienteResponsabile" => $clienteResponsabile,
|
||||
"MoltiplicatorePrezzo" => $moltiplicatorePrezzo,
|
||||
"AnagraficaCertestObject" => $anagraficaObject,
|
||||
"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
|
||||
];
|
||||
|
||||
// Costruisci log curl-like per STEP 5
|
||||
$jsonPayload = json_encode($commessaWebPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
$logContentStep5 = "curl --location --request POST '{$apiBaseUrl}CommessaWeb' \\\n" .
|
||||
"--header 'Content-Type: application/json' \\\n" .
|
||||
"--header 'Authorization: Bearer ••••••' \\\n" .
|
||||
"--data '{$jsonPayload}'";
|
||||
|
||||
$commessaWeb = $api->post("CommessaWeb", $commessaWebPayload);
|
||||
|
||||
$logContentStep5 .= "\n\nRESPONSE:\n" . json_encode($commessaWeb, JSON_PRETTY_PRINT);
|
||||
|
||||
// Salva log
|
||||
$logFileStep5 = $logDir . "commessa_create_step5_" . $iddatadb . "_" . time() . ".txt";
|
||||
$writeLog($logFileStep5, $logContentStep5, "STEP 5 - Create CommessaWeb (iddatadb={$iddatadb})");
|
||||
|
||||
$commessaId = $commessaWeb["IdCommessa"];
|
||||
$commessaWebCode = substr($commessaWeb["CodiceCommessa"] ?? "TEST CommessaWeb", 0, 30); // Limite a 30 caratteri
|
||||
|
||||
// 🔹 STEP 6: Create Campioni (Samples) for each part
|
||||
$campioni = [];
|
||||
$logContentStep6 = "";
|
||||
|
||||
foreach ($parts as $index => $part) {
|
||||
$matriceId = (int) ($part["idmatrice"] ?? 0);
|
||||
|
||||
if ($matriceId <= 0) {
|
||||
throw new Exception("Invalid or missing idmatrice for part: " . ($part["part_number"] ?? "Unknown"));
|
||||
}
|
||||
|
||||
$partDescription = $part["part_description"] ?? "";
|
||||
$partNote = trim((string)($part["note"] ?? ""));
|
||||
|
||||
// Note aggiuntive dai custom field flaggati is_addnote
|
||||
$partDescription = $part["part_description"] ?? "";
|
||||
$partNote = trim((string)($part["note"] ?? ""));
|
||||
|
||||
// Componi NoteWeb: descrizione [- note] [- addnote di commessa]
|
||||
$noteParts = [];
|
||||
if ($partDescription !== "") {
|
||||
$noteParts[] = $partDescription;
|
||||
}
|
||||
if ($partNote !== "") {
|
||||
$noteParts[] = $partNote;
|
||||
}
|
||||
if ($addnoteText !== "") {
|
||||
$noteParts[] = $addnoteText;
|
||||
}
|
||||
$noteWebValue = implode(" - ", $noteParts);
|
||||
|
||||
$campionePayload = [
|
||||
"Commessa" => $commessaId,
|
||||
"Matrice" => $matriceId,
|
||||
"SottoMatrice" => null,
|
||||
"SchemaCustomField" => $schemaId,
|
||||
// "Riferimento" => $part["part_description"] ?? "",
|
||||
"NoteWeb" => $noteWebValue,
|
||||
"ConsegnaRichiesta" => !empty($part["dateexpiry"]) ? $part["dateexpiry"] : $consegnaRichiesta,
|
||||
];
|
||||
|
||||
// Costruisci curl-like per questo campione
|
||||
$jsonPayload = json_encode($campionePayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
$logContentStep6 .= "CAMPIONE #{$index}\n" .
|
||||
"curl --location --request POST '{$apiBaseUrl}Campione' \\\n" .
|
||||
"--header 'Content-Type: application/json' \\\n" .
|
||||
"--header 'Authorization: Bearer ••••••' \\\n" .
|
||||
"--data '{$jsonPayload}'\n\n";
|
||||
|
||||
$campione = $api->post("Campione", $campionePayload);
|
||||
|
||||
$logContentStep6 .= "RESPONSE:\n" . json_encode($campione, JSON_PRETTY_PRINT) . "\n\n---\n";
|
||||
|
||||
$campione["PartNumber"] = $part["part_number"] ?? "";
|
||||
$campione["Material"] = $part["material"] ?? "";
|
||||
$campione["Color"] = $part["color"] ?? "";
|
||||
$campione["Mix"] = $part["mix"] ?? "";
|
||||
|
||||
$campioni[] = $campione;
|
||||
}
|
||||
|
||||
// Salva log per STEP 6
|
||||
$logFileStep6 = $logDir . "commessa_{$commessaId}_campioni_step6_" . time() . ".txt";
|
||||
$writeLog($logFileStep6, $logContentStep6, "STEP 6 - Campioni (commessa={$commessaId})");
|
||||
|
||||
// 🔹 STEP 6.0: PATCH each Campione custom fields:
|
||||
// - field 189 (Tested Component) = part_description
|
||||
// - additional fields from identification_parts_customfields (field_id → value_id/value_text)
|
||||
$logContentStep63 = "";
|
||||
foreach ($campioni as $index => $campione) {
|
||||
$campioneId = (int)($campione['IdCampione'] ?? 0);
|
||||
$partDescription = $parts[$index]['part_description'] ?? '';
|
||||
$partId = (int)($parts[$index]['part_id'] ?? 0);
|
||||
|
||||
if ($campioneId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build a map of overrides: IdCustomField => value
|
||||
$overrides = [];
|
||||
|
||||
// Override 1: Tested Component (field 189) = part_description
|
||||
if ($partDescription !== '') {
|
||||
$overrides[189] = $partDescription;
|
||||
}
|
||||
|
||||
// Override 2: values from identification_parts_customfields
|
||||
$partCFs = $partsCustomFields[$partId] ?? [];
|
||||
foreach ($partCFs as $pcf) {
|
||||
$cfFieldId = (int)$pcf['field_id'];
|
||||
$cfValue = $pcf['value_text'] ?? $pcf['value_id'] ?? null;
|
||||
if ($cfFieldId > 0 && $cfValue !== null && $cfValue !== '') {
|
||||
$overrides[$cfFieldId] = (string)$cfValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip if nothing to override
|
||||
if (empty($overrides)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// GET campione custom fields
|
||||
$campioneWithFields = $api->get("Campione({$campioneId})?\$expand=CampioniCustomFields(\$expand=CustomField)");
|
||||
|
||||
$logContentStep63 .= "GET Campione({$campioneId}) CustomFields:\n" .
|
||||
json_encode($campioneWithFields['CampioniCustomFields'] ?? [], JSON_PRETTY_PRINT) . "\n\n";
|
||||
$logContentStep63 .= "Overrides for part {$partId}: " . json_encode($overrides) . "\n\n";
|
||||
|
||||
// Build PATCH payload — apply overrides where IdCustomField matches
|
||||
$campioniCustomFields = [];
|
||||
foreach ($campioneWithFields["CampioniCustomFields"] ?? [] as $cf) {
|
||||
$definitionId = (int)($cf["CustomField"]["IdCustomField"] ?? 0);
|
||||
$fieldInstanceId = (int)$cf["IdCampioniCustomFields"];
|
||||
$currentValue = $cf["Valore"] ?? '';
|
||||
|
||||
$newValue = $overrides[$definitionId] ?? $currentValue;
|
||||
|
||||
$campioniCustomFields[] = [
|
||||
"IdCampioniCustomFields" => $fieldInstanceId,
|
||||
"Valore" => $newValue
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($campioniCustomFields)) {
|
||||
$patchPayload = ["CampioniCustomFields" => $campioniCustomFields];
|
||||
$patchJson = json_encode($patchPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$logContentStep63 .= "PATCH Campione({$campioneId}):\n" .
|
||||
"curl --location --request PATCH '{$apiBaseUrl}Campione({$campioneId})' \\\n" .
|
||||
"--header 'Content-Type: application/json' \\\n" .
|
||||
"--header 'Authorization: Bearer ••••••' \\\n" .
|
||||
"--data '{$patchJson}'\n\n";
|
||||
|
||||
$patchResult = $api->patch("Campione({$campioneId})", $patchPayload);
|
||||
|
||||
$logContentStep63 .= "RESPONSE:\n" . json_encode($patchResult, JSON_PRETTY_PRINT) . "\n\n---\n";
|
||||
}
|
||||
}
|
||||
|
||||
$logFileStep63 = $logDir . "commessa_{$commessaId}_campioni_customfields_step60_" . time() . ".txt";
|
||||
$writeLog($logFileStep63, $logContentStep63, "STEP 6.0 - Campioni CustomFields (commessa={$commessaId})");
|
||||
|
||||
// 🔹 STEP 6.1: Fetch photos linked to this iddatadb
|
||||
$stmtPhotos = $pdo->prepare("
|
||||
SELECT id, file_path, file_name, StampaNelRapporto, PrimaPagina
|
||||
FROM datadb_photos
|
||||
WHERE iddatadb = :iddatadb
|
||||
ORDER BY id ASC
|
||||
");
|
||||
$stmtPhotos->execute(['iddatadb' => $iddatadb]);
|
||||
$photos = $stmtPhotos->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
|
||||
// 🔹 STEP 6.2: Upload photos to Campione .01 (fetched from API)
|
||||
$photosUploaded = 0;
|
||||
$logContentPhotos = "Photos for CommessaWeb {$commessaId} (iddatadb={$iddatadb}):\n";
|
||||
$logContentPhotos .= "Total photos found: " . count($photos) . ", campioni: " . count($campioni) . "\n\n";
|
||||
|
||||
if (!empty($campioni) && !empty($photos)) {
|
||||
// Fetch campioni list from API to find the .01 campione
|
||||
$commessaCampioni = $api->get("CommessaWeb({$commessaId})?\$expand=Campioni");
|
||||
$apiCampioni = $commessaCampioni['Campioni'] ?? [];
|
||||
|
||||
// Sort by CodiceCampione to find .01
|
||||
usort($apiCampioni, fn($a, $b) => strcmp($a['CodiceCampione'] ?? '', $b['CodiceCampione'] ?? ''));
|
||||
|
||||
$mainCampione = $apiCampioni[0] ?? null;
|
||||
$campioneId = (int)($mainCampione['IdCampione'] ?? 0);
|
||||
|
||||
$logContentPhotos .= "API Campioni order:\n";
|
||||
foreach ($apiCampioni as $ac) {
|
||||
$logContentPhotos .= " - {$ac['CodiceCampione']} (IdCampione: {$ac['IdCampione']})\n";
|
||||
}
|
||||
$logContentPhotos .= "Selected .01 campione: IdCampione={$campioneId}\n\n";
|
||||
|
||||
if ($campioneId > 0) {
|
||||
$logContentPhotos .= "=== Campione {$campioneId} (main) ===\n";
|
||||
|
||||
foreach ($photos as $photo) {
|
||||
$fullPath = resolvePhotoFullPath($photo['file_path'], $uploadDir, __DIR__);
|
||||
|
||||
if ($fullPath === null) {
|
||||
$logContentPhotos .= "SKIP (file not found): file_path='{$photo['file_path']}'\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
$photoEndpoint = "Campione({$campioneId})/UploadCampioneFile";
|
||||
$stampaNelRapporto = !empty($photo['StampaNelRapporto']);
|
||||
$primaPagina = !empty($photo['PrimaPagina']);
|
||||
|
||||
$logContentPhotos .= "curl --location --request POST '{$apiBaseUrl}{$photoEndpoint}' \\\n" .
|
||||
"--header 'Authorization: Bearer ••••••' \\\n" .
|
||||
"--form 'file=@{$fullPath}'\n\n";
|
||||
|
||||
// Step 1: Upload file (flags are ignored by API during upload)
|
||||
$photoResult = $api->postMultipart($photoEndpoint, $fullPath, $photo['file_name']);
|
||||
$logContentPhotos .= "UPLOAD RESPONSE:\n" . json_encode($photoResult, JSON_PRETTY_PRINT) . "\n\n";
|
||||
|
||||
// Step 2: PATCH CampioneFile to set flags (StampaNelRapporto, PrimaPagina)
|
||||
$campioneFileId = (int)($photoResult['IdCampioneFile'] ?? 0);
|
||||
if ($campioneFileId > 0 && ($stampaNelRapporto || $primaPagina)) {
|
||||
$patchPayload = [];
|
||||
|
||||
if ($stampaNelRapporto) {
|
||||
$patchPayload['StampaNelRapporto'] = true;
|
||||
}
|
||||
|
||||
if ($primaPagina) {
|
||||
$patchPayload['PrimaPagina'] = true;
|
||||
}
|
||||
|
||||
$patchEndpoint = "CampioneFile({$campioneFileId})";
|
||||
$patchJsonLog = json_encode($patchPayload, JSON_PRETTY_PRINT);
|
||||
$logContentPhotos .= "curl --location --request PATCH '{$apiBaseUrl}{$patchEndpoint}' \\\n" .
|
||||
"--header 'Content-Type: application/json' \\\n" .
|
||||
"--header 'Authorization: Bearer ••••••' \\\n" .
|
||||
"--data '{$patchJsonLog}'\n\n";
|
||||
|
||||
$patchResult = $api->patch($patchEndpoint, $patchPayload);
|
||||
$logContentPhotos .= "PATCH RESPONSE:\n" . json_encode($patchResult, JSON_PRETTY_PRINT) . "\n\n";
|
||||
}
|
||||
|
||||
$logContentPhotos .= "---\n";
|
||||
$photosUploaded++;
|
||||
}
|
||||
} else {
|
||||
$logContentPhotos .= "SKIP: main campione has invalid IdCampione\n";
|
||||
}
|
||||
} elseif (empty($campioni)) {
|
||||
$logContentPhotos .= "SKIP: no campioni created, cannot upload photos\n";
|
||||
}
|
||||
|
||||
$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
|
||||
$expand = "CommesseCustomFields(\$expand=CustomField)";
|
||||
$commessaWithFields = $api->get("CommessaWeb({$commessaId})?\$expand={$expand}");
|
||||
|
||||
// Logga il GET
|
||||
$logContentGet = "curl --location --request GET '{$apiBaseUrl}CommessaWeb({$commessaId})?\$expand=CommesseCustomFields(\$expand=CustomField)' \\\n" .
|
||||
"--header 'Authorization: Bearer ••••••'\n\n" .
|
||||
"RESPONSE:\n" . json_encode($commessaWithFields, JSON_PRETTY_PRINT);
|
||||
$logFileGet = $logDir . "commessa_{$commessaId}_get_step7_" . time() . ".txt";
|
||||
$writeLog($logFileGet, $logContentGet, "STEP 7 - GET CustomFields (commessa={$commessaId})");
|
||||
|
||||
// Prepara payload PATCH
|
||||
$commessaCustomFields = [];
|
||||
foreach ($commessaWithFields["CommesseCustomFields"] as $customField) {
|
||||
$definitionId = (int) ($customField["CustomField"]["IdCustomField"] ?? 0);
|
||||
$fieldId = (int) $customField["IdCommesseCustomFields"];
|
||||
$currentValue = $customField["Valore"] ?? '';
|
||||
$fieldType = $customField["CustomField"]["Tipo"] ?? '';
|
||||
$newValue = isset($valueMap[$definitionId]) ? $valueMap[$definitionId] : $currentValue;
|
||||
|
||||
// Valida se il campo è di tipo Data
|
||||
if ($fieldType === 'Data' && $newValue !== $currentValue) {
|
||||
$newValue = formatDateToExport($newValue);
|
||||
}
|
||||
|
||||
$commessaCustomFields[] = [
|
||||
"IdCommesseCustomFields" => $fieldId,
|
||||
"Valore" => $newValue
|
||||
];
|
||||
}
|
||||
|
||||
$logFileStep7 = null;
|
||||
if (!empty($commessaCustomFields)) {
|
||||
$updatePayload = ["CommesseCustomFields" => $commessaCustomFields];
|
||||
|
||||
// Logga payload e response
|
||||
$jsonPayload = json_encode($updatePayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
$logContentStep7 = "curl --location --request PATCH '{$apiBaseUrl}CommessaWeb({$commessaId})' \\\n" .
|
||||
"--header 'Content-Type: application/json' \\\n" .
|
||||
"--header 'Authorization: Bearer ••••••' \\\n" .
|
||||
"--data '{$jsonPayload}'";
|
||||
|
||||
$patchResponse = $api->patch("CommessaWeb({$commessaId})", $updatePayload);
|
||||
|
||||
$logContentStep7 .= "\n\nRESPONSE:\n" . json_encode($patchResponse, JSON_PRETTY_PRINT);
|
||||
$logFileStep7 = $logDir . "commessa_{$commessaId}_update_step7_" . time() . ".txt";
|
||||
$writeLog($logFileStep7, $logContentStep7, "STEP 7 - PATCH CustomFields (commessa={$commessaId})");
|
||||
}
|
||||
}
|
||||
|
||||
// 🔹 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,
|
||||
log_file = :log_file, export_date = NOW()
|
||||
WHERE iddatadb = :iddatadb
|
||||
");
|
||||
$stmt->execute([
|
||||
'idcommessaweb' => $commessaId,
|
||||
'commessaweb' => $commessaWebCode,
|
||||
'log_file' => basename($commessaLogFile),
|
||||
'iddatadb' => $iddatadb
|
||||
]);
|
||||
|
||||
// 🔹 STEP 9: Send CommessaWeb to laboratory (commentato come richiesto)
|
||||
|
||||
$sendResult = $api->post("CommessaWeb({$commessaId})/InviaCommessa", []);
|
||||
|
||||
// Logga il POST
|
||||
$logContentStep9 = "curl --location --request POST '{$apiBaseUrl}CommessaWeb({$commessaId})/InviaCommessa' \\\n" .
|
||||
"--header 'Content-Type: application/json' \\\n" .
|
||||
"--header 'Authorization: Bearer ••••••' \\\n" .
|
||||
"--data '{}'\n\n" .
|
||||
"RESPONSE:\n" . json_encode($sendResult, JSON_PRETTY_PRINT);
|
||||
$logFileStep9 = $logDir . "commessa_{$commessaId}_send_step9_" . time() . ".txt";
|
||||
$writeLog($logFileStep9, $logContentStep9, "STEP 9 - InviaCommessa (commessa={$commessaId})");
|
||||
|
||||
|
||||
// 🔹 STEP 9.5: Importazione da CommessaWeb a Commessa (con retry)
|
||||
// Supplier call: POST api/odata/CommessaWeb(XXX)/ImportaCommessa
|
||||
$importUserId = (!empty($lims_global_user_id) && is_numeric($lims_global_user_id))
|
||||
? (int) $lims_global_user_id
|
||||
: 285;
|
||||
|
||||
$importPayload = [
|
||||
"IdUtente" => $importUserId
|
||||
];
|
||||
$importPayloadLog = json_encode($importPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$importOutcome = importaCommessaWithRetry($api, $commessaId, $importPayload);
|
||||
$importResult = $importOutcome['result'];
|
||||
$importStato = $importOutcome['stato'];
|
||||
$importSucceeded = $importOutcome['succeeded'];
|
||||
|
||||
// Logga il POST (tutti i tentativi)
|
||||
$logContentStep91 = "curl --location --request POST '{$apiBaseUrl}CommessaWeb({$commessaId})/ImportaCommessa' \\\n" .
|
||||
"--header 'Content-Type: application/json' \\\n" .
|
||||
"--header 'Authorization: Bearer ••••••' \\\n" .
|
||||
"--data '{$importPayloadLog}'\n\n" .
|
||||
"ATTEMPTS:\n" . $importOutcome['log'] . "\n" .
|
||||
"SUCCEEDED: " . ($importSucceeded ? 'yes' : 'NO') . "\n\n" .
|
||||
"LAST RESPONSE:\n" . json_encode($importResult, JSON_PRETTY_PRINT);
|
||||
$logFileStep91 = $logDir . "commessa_{$commessaId}_importa_step91_" . time() . ".txt";
|
||||
$writeLog($logFileStep91, $logContentStep91, "STEP 9.5 - ImportaCommessa (commessa={$commessaId}, succeeded=" . ($importSucceeded ? '1' : '0') . ")");
|
||||
|
||||
// 🔹 STEP 10: GET di controllo post-PATCH
|
||||
$expand = "CommesseCustomFields(\$expand=CustomField)";
|
||||
$commessaAfterPatch = $api->get("CommessaWeb(" . $commessaId . ")?\$expand=" . $expand);
|
||||
|
||||
// Logga il GET di controllo
|
||||
$logContentStep10 = "curl --location --request GET '{$apiBaseUrl}CommessaWeb({$commessaId})?\$expand=CommesseCustomFields(\$expand=CustomField)' \\\n" .
|
||||
"--header 'Authorization: Bearer ••••••'\n\n" .
|
||||
"RESPONSE:\n" . json_encode($commessaAfterPatch, JSON_PRETTY_PRINT);
|
||||
$logFileStep10 = $logDir . "commessa_{$commessaId}_get_step10_" . time() . ".txt";
|
||||
$writeLog($logFileStep10, $logContentStep10, "STEP 10 - GET verify (commessa={$commessaId})");
|
||||
// 🔹 STEP 10.1: Save final CodiceCommessa into datadb.commessaweb
|
||||
// After ImportaCommessa, the API returns the final LIMS job code in CodiceCommessa.
|
||||
// Example: CodiceCommessa = 2614795, CodiceCommessaWeb = 26C0103.
|
||||
$finalCodiceCommessa = trim((string)($commessaAfterPatch['CodiceCommessa'] ?? ''));
|
||||
|
||||
if ($finalCodiceCommessa !== '') {
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE datadb
|
||||
SET commessaweb = :commessaweb,
|
||||
status = 'l'
|
||||
WHERE iddatadb = :iddatadb
|
||||
");
|
||||
$stmt->execute([
|
||||
'commessaweb' => substr($finalCodiceCommessa, 0, 30),
|
||||
'iddatadb' => $iddatadb
|
||||
]);
|
||||
}
|
||||
|
||||
// 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,
|
||||
"SchemaCustomField" => $schemaId,
|
||||
"Richiedente" => $commessaWeb["Richiedente"] ?? "Web Import",
|
||||
"Descrizione" => $commessaWeb["Descrizione"] ?? "",
|
||||
"CommesseCustomFields" => $commessaAfterPatch["CommesseCustomFields"] ?? [],
|
||||
"Campioni" => $campioni,
|
||||
"Inviata" => 0 // Non inviato, come richiesto
|
||||
];
|
||||
|
||||
echo json_encode([
|
||||
"success" => true,
|
||||
"idcommessaweb" => $commessaId,
|
||||
"commessaweb" => $finalCodiceCommessa ?: $commessaWebCode,
|
||||
"commessaWeb" => $finalCommessa,
|
||||
"commessaWebApiResponse" => $commessaWeb, // Incluso per debug
|
||||
"totalCampioni" => count($campioni),
|
||||
"totalCustomFields" => count($commessaAfterPatch["CommesseCustomFields"] ?? []),
|
||||
"totalPhotos" => count($photos),
|
||||
"totalAnalyses" => $totalAnalyses,
|
||||
"addedAnalyses" => $addedAnalyses,
|
||||
"failedAnalyses" => $failedAnalyses,
|
||||
"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(),
|
||||
"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()]);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
// Endpoint: GET api/odata/AnagraficaCertestObject
|
||||
$endpoint = 'AnagraficaCertestObject';
|
||||
|
||||
// Opzionale: parametri OData ($top, $filter, $orderby, ecc.)
|
||||
$options = []; // es: ['$top' => 100]
|
||||
|
||||
// Debug: salva URL usata
|
||||
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
|
||||
$query = http_build_query($options);
|
||||
$full_url = $base_url . $endpoint . ($query ? '?' . $query : '');
|
||||
file_put_contents(__DIR__ . '/last_url.txt', $full_url . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Chiamata API
|
||||
$data = $api->get($endpoint, $options);
|
||||
|
||||
// Salva il JSON in locale
|
||||
file_put_contents(__DIR__ . '/anagrafica_certest_object_response.json', json_encode($data, JSON_PRETTY_PRINT));
|
||||
|
||||
echo json_encode($data);
|
||||
} catch (Exception $e) {
|
||||
file_put_contents(
|
||||
__DIR__ . '/error_log.txt',
|
||||
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
|
||||
FILE_APPEND
|
||||
);
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
// Endpoint: GET api/odata/AnagraficaCertestService
|
||||
$endpoint = 'AnagraficaCertestService';
|
||||
|
||||
// Opzionale: parametri OData ($top, $filter, $orderby, ecc.)
|
||||
$options = []; // es: ['$top' => 100]
|
||||
|
||||
// Debug: salva URL usata
|
||||
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
|
||||
$query = http_build_query($options);
|
||||
$full_url = $base_url . $endpoint . ($query ? '?' . $query : '');
|
||||
file_put_contents(__DIR__ . '/last_url.txt', $full_url . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Chiamata API
|
||||
$data = $api->get($endpoint, $options);
|
||||
|
||||
// Salva il JSON in locale
|
||||
file_put_contents(__DIR__ . '/anagrafica_certest_service_response.json', json_encode($data, JSON_PRETTY_PRINT));
|
||||
|
||||
echo json_encode($data);
|
||||
} catch (Exception $e) {
|
||||
file_put_contents(
|
||||
__DIR__ . '/error_log.txt',
|
||||
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
|
||||
FILE_APPEND
|
||||
);
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
// Endpoint per recuperare le Analisi
|
||||
$endpoint = 'Analisi';
|
||||
|
||||
// Opzioni OData
|
||||
$options = [
|
||||
// Restituisce solo i campi principali utili
|
||||
'$select' => 'IdAnalisi,Codice,NomeAnalisi,ClientiAbilitati,MatriciAbilitate,IsGenerico,Tipo,ParentKey,SelezionabileSuWeb',
|
||||
|
||||
// Solo analisi effettivamente selezionabili sul web
|
||||
'$filter' => 'SelezionabileSuWeb eq true',
|
||||
|
||||
// Ordinamento alfabetico per nome analisi
|
||||
'$orderby' => 'NomeAnalisi asc'
|
||||
];
|
||||
|
||||
// Debug: salva URL usato
|
||||
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
|
||||
$query = http_build_query($options);
|
||||
$full_url = $base_url . $endpoint . ($query ? '?' . $query : '');
|
||||
file_put_contents(__DIR__ . '/last_analisi_url.txt', $full_url . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Chiamata API
|
||||
$data = $api->get($endpoint, $options);
|
||||
|
||||
// Salva il JSON in locale
|
||||
file_put_contents(__DIR__ . '/analisi_response.json', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
} catch (Exception $e) {
|
||||
file_put_contents(
|
||||
__DIR__ . '/analisi_error_log.txt',
|
||||
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
|
||||
FILE_APPEND
|
||||
);
|
||||
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$idCliente = isset($_GET['id_cliente']) ? (int)$_GET['id_cliente'] : 0;
|
||||
$idMatrice = isset($_GET['id_matrice']) ? (int)$_GET['id_matrice'] : 0;
|
||||
$debug = isset($_GET['debug']) ? (int)$_GET['debug'] : 0;
|
||||
|
||||
if ($idCliente <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Missing or invalid id_cliente']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
if ($idMatrice > 0) {
|
||||
$expandExpr = "AnalisiAbilitate(\$filter=Matrice/IdMatrice eq $idMatrice)";
|
||||
} else {
|
||||
$expandExpr = "AnalisiAbilitate";
|
||||
}
|
||||
|
||||
// Encode only the $expand expression because it contains spaces, slash and parentheses
|
||||
$expandEncoded = rawurlencode($expandExpr);
|
||||
$endpoint = "Cliente($idCliente)?\$expand={$expandEncoded}";
|
||||
|
||||
$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);
|
||||
|
||||
$data = $api->get($endpoint, []);
|
||||
|
||||
file_put_contents(
|
||||
__DIR__ . '/analisi_by_cliente_matrice_response.json',
|
||||
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
|
||||
if ($debug === 1) {
|
||||
echo json_encode([
|
||||
'endpoint' => $endpoint,
|
||||
'full_url' => $full_url,
|
||||
'raw_response' => $data
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$analisi = $data['AnalisiAbilitate'] ?? [];
|
||||
|
||||
echo json_encode([
|
||||
'value' => $analisi,
|
||||
'count' => is_array($analisi) ? count($analisi) : 0
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
} catch (Exception $e) {
|
||||
file_put_contents(
|
||||
__DIR__ . '/error_log_analisi.txt',
|
||||
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
|
||||
FILE_APPEND
|
||||
);
|
||||
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$idMatrice = isset($_GET['id_matrice']) ? (int)$_GET['id_matrice'] : 0;
|
||||
|
||||
if ($idMatrice <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Missing or invalid id_matrice']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
/**
|
||||
* OData hypothesis:
|
||||
* - Expand enabled matrices
|
||||
* - Return only selectable analyses
|
||||
* - Include generic analyses OR analyses enabled for the given matrix
|
||||
*
|
||||
* This endpoint must be verified against the real VisualLims API metadata.
|
||||
*/
|
||||
$filter = rawurlencode("SelezionabileSuWeb eq true and (IsGenerico eq true or MatriciAbilitate/any(m:m/IdMatrice eq $idMatrice))");
|
||||
$expand = rawurlencode("MatriciAbilitate");
|
||||
$endpoint = "Analisi?\$top=10";
|
||||
|
||||
// Debug URL
|
||||
$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);
|
||||
|
||||
$data = $api->get($endpoint, []);
|
||||
|
||||
file_put_contents(
|
||||
__DIR__ . '/analisi_by_matrice_response.json',
|
||||
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
|
||||
$analisi = $data['value'] ?? [];
|
||||
|
||||
echo json_encode(['value' => $analisi], JSON_UNESCAPED_UNICODE);
|
||||
} catch (Exception $e) {
|
||||
file_put_contents(
|
||||
__DIR__ . '/error_log_analisi.txt',
|
||||
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
|
||||
FILE_APPEND
|
||||
);
|
||||
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$idMatrice = isset($_GET['id_matrice']) ? (int)$_GET['id_matrice'] : 0;
|
||||
$debug = isset($_GET['debug']) ? (int)$_GET['debug'] : 0;
|
||||
|
||||
if ($idMatrice <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Missing or invalid id_matrice']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$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";
|
||||
|
||||
if ($webOnly === 1) {
|
||||
$filterString .= " and SelezionabileSuWeb eq true";
|
||||
}
|
||||
|
||||
$filter = rawurlencode($filterString);
|
||||
$endpoint = "Analisi?\$filter={$filter}";
|
||||
|
||||
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
|
||||
$full_url = $base_url . $endpoint;
|
||||
|
||||
// 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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
$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
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
} catch (Exception $e) {
|
||||
file_put_contents(
|
||||
__DIR__ . '/error_log_analisi.txt',
|
||||
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
|
||||
FILE_APPEND
|
||||
);
|
||||
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
// Campione hardcoded del tuo esempio
|
||||
$idCampione = 749027;
|
||||
|
||||
// Analisi che stai provando ad assegnare
|
||||
$targetRecordKey = '11;1218320';
|
||||
$targetIdAnalisi = '1218320';
|
||||
|
||||
// Endpoint: recupera il campione espandendo le analisi abilitate
|
||||
$endpoint = "Campione($idCampione)";
|
||||
|
||||
// ATTENZIONE:
|
||||
// nella tua classe normalmente le opzioni vengono trasformate in query string.
|
||||
// Qui serve: ?$expand=AnalisiAbilitate
|
||||
$options = [
|
||||
'$expand' => 'AnalisiAbilitate'
|
||||
];
|
||||
|
||||
// Debug: salva URL usato
|
||||
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
|
||||
$query = http_build_query($options);
|
||||
$query = urldecode($query); // rende leggibile $expand invece di %24expand
|
||||
|
||||
$full_url = $base_url . $endpoint . ($query ? '?' . $query : '');
|
||||
file_put_contents(__DIR__ . '/last_url_analisi_abilitate.txt', $full_url . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Chiamata API
|
||||
$data = $api->get($endpoint, $options);
|
||||
|
||||
// Recupera AnalisiAbilitate dalla risposta
|
||||
$analisiAbilitate = $data['AnalisiAbilitate'] ?? [];
|
||||
|
||||
// Alcune API OData possono restituire collection dentro "value"
|
||||
if (isset($analisiAbilitate['value']) && is_array($analisiAbilitate['value'])) {
|
||||
$analisiAbilitate = $analisiAbilitate['value'];
|
||||
}
|
||||
|
||||
// Cerca se il RecordKey / IdAnalisi che stai usando è effettivamente assegnabile
|
||||
$matches = [];
|
||||
|
||||
foreach ($analisiAbilitate as $analisi) {
|
||||
$recordKey = isset($analisi['RecordKey']) ? (string)$analisi['RecordKey'] : '';
|
||||
$idAnalisi = isset($analisi['IdAnalisi']) ? (string)$analisi['IdAnalisi'] : '';
|
||||
|
||||
if ($recordKey === $targetRecordKey || $idAnalisi === $targetIdAnalisi) {
|
||||
$matches[] = $analisi;
|
||||
}
|
||||
}
|
||||
|
||||
// Output diagnostico
|
||||
$output = [
|
||||
'success' => true,
|
||||
'idCampione' => $idCampione,
|
||||
'request_url' => $full_url,
|
||||
'targetRecordKey' => $targetRecordKey,
|
||||
'targetIdAnalisi' => $targetIdAnalisi,
|
||||
'enabled_analyses_count' => is_array($analisiAbilitate) ? count($analisiAbilitate) : 0,
|
||||
'target_found' => count($matches) > 0,
|
||||
'target_matches' => $matches,
|
||||
'analisi_abilitate' => $analisiAbilitate,
|
||||
'raw_response' => $data
|
||||
];
|
||||
|
||||
// Salva il JSON in locale
|
||||
file_put_contents(
|
||||
__DIR__ . '/analisi_abilitate_campione_749027_response.json',
|
||||
json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
|
||||
echo json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
} catch (Exception $e) {
|
||||
file_put_contents(
|
||||
__DIR__ . '/error_log_analisi_abilitate.txt',
|
||||
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
|
||||
FILE_APPEND
|
||||
);
|
||||
|
||||
http_response_code(500);
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
// Campione hardcoded del tuo esempio
|
||||
$idCampione = 749027;
|
||||
|
||||
// Matrice attesa dal log STEP 6
|
||||
$expectedMatriceId = 6430;
|
||||
|
||||
// Endpoint con expand della Matrice
|
||||
$endpoint = "Campione($idCampione)";
|
||||
$options = [
|
||||
'$expand' => 'Matrice'
|
||||
];
|
||||
|
||||
// Debug URL
|
||||
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
|
||||
$query = http_build_query($options);
|
||||
$queryReadable = urldecode($query);
|
||||
|
||||
$full_url = $base_url . $endpoint . ($queryReadable ? '?' . $queryReadable : '');
|
||||
|
||||
file_put_contents(
|
||||
__DIR__ . '/last_url_check_matrice.txt',
|
||||
$full_url . PHP_EOL,
|
||||
FILE_APPEND
|
||||
);
|
||||
|
||||
// Chiamata API
|
||||
$data = $api->get($endpoint, $options);
|
||||
|
||||
// Recupero Matrice dalla response
|
||||
$matrice = $data['Matrice'] ?? null;
|
||||
|
||||
$actualMatriceId = null;
|
||||
|
||||
if (is_array($matrice)) {
|
||||
// Provo i nomi più probabili
|
||||
$actualMatriceId = $matrice['IdMatrice']
|
||||
?? $matrice['idMatrice']
|
||||
?? $matrice['Id']
|
||||
?? $matrice['ID']
|
||||
?? null;
|
||||
}
|
||||
|
||||
$matrice_ok = ((int)$actualMatriceId === (int)$expectedMatriceId);
|
||||
|
||||
$output = [
|
||||
'success' => true,
|
||||
'idCampione' => $idCampione,
|
||||
'expectedMatriceId' => $expectedMatriceId,
|
||||
'actualMatriceId' => $actualMatriceId,
|
||||
'matrice_ok' => $matrice_ok,
|
||||
'request_url' => $full_url,
|
||||
'matrice' => $matrice,
|
||||
'raw_response' => $data
|
||||
];
|
||||
|
||||
// Salva JSON completo
|
||||
file_put_contents(
|
||||
__DIR__ . '/check_matrice_campione_749027_response.json',
|
||||
json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
|
||||
echo json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
} catch (Exception $e) {
|
||||
file_put_contents(
|
||||
__DIR__ . '/error_log_check_matrice.txt',
|
||||
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
|
||||
FILE_APPEND
|
||||
);
|
||||
|
||||
http_response_code(500);
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$idCliente = isset($_GET['id_cliente']) ? (int)$_GET['id_cliente'] : 0;
|
||||
if ($idCliente <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Missing or invalid id_cliente']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
// Build endpoint with OData $expand WITHOUT encoding '$' as %24
|
||||
$endpoint = "Cliente($idCliente)?\$expand=Responsabili";
|
||||
|
||||
// Debug URL (real final URL)
|
||||
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
|
||||
$full_url = $base_url . $endpoint;
|
||||
file_put_contents(__DIR__ . '/last_url.txt', $full_url . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Call API: options must be empty because expand is already in endpoint
|
||||
$data = $api->get($endpoint, []);
|
||||
|
||||
|
||||
file_put_contents(__DIR__ . '/cliente_responsabili_response.json', json_encode($data, JSON_PRETTY_PRINT));
|
||||
|
||||
// Return only the list (standard shape used by the frontend)
|
||||
$responsabili = $data['Responsabili'] ?? [];
|
||||
echo json_encode(['value' => $responsabili]);
|
||||
} catch (Exception $e) {
|
||||
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -1,24 +1,102 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php'; // Torna al livello di public per trovare vendor/
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Disabilita la visualizzazione degli errori PHP per evitare output HTML
|
||||
// Disable PHP error display
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
$data = $api->get("Cliente"); // Recupera i clienti
|
||||
$api = VisualLimsApiClient::getInstance(); // also loads dotenv
|
||||
|
||||
// Salva la risposta in un file per debug
|
||||
file_put_contents(__DIR__ . '/clienti_response.json', json_encode($data));
|
||||
// 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;
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
// 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);
|
||||
echo json_encode([
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
$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,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,103 @@
|
||||
<?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.
|
||||
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], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Endpoint senza filtri: recupera tutto
|
||||
$endpoint = 'Cliente?$top=50';
|
||||
|
||||
// Funzione per eseguire la chiamata con retry
|
||||
function makeApiRequest($api, $endpoint, $maxRetries = 3)
|
||||
{
|
||||
for ($retry = 0; $retry < $maxRetries; $retry++) {
|
||||
try {
|
||||
$data = $api->get($endpoint);
|
||||
|
||||
// Save response for debug
|
||||
file_put_contents(
|
||||
__DIR__ . '/clienti_response.json',
|
||||
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
|
||||
return $data;
|
||||
} catch (Exception $e) {
|
||||
$errorMessage = $e->getMessage();
|
||||
|
||||
// Retry only for specific auth/token related issue
|
||||
if (
|
||||
strpos($errorMessage, 'HTTP 400') !== false &&
|
||||
strpos($errorMessage, 'Cannot persist the object') !== false
|
||||
) {
|
||||
try {
|
||||
if (method_exists($api, 'refreshToken')) {
|
||||
$api->refreshToken();
|
||||
error_log("Tentativo {$retry}: refresh token eseguito per endpoint {$endpoint}");
|
||||
} else {
|
||||
throw new Exception('Il metodo refreshToken() non esiste in VisualLimsApiClient');
|
||||
}
|
||||
} catch (Exception $refreshEx) {
|
||||
error_log("Errore durante il refresh del token: " . $refreshEx->getMessage());
|
||||
throw new Exception("Impossibile eseguire il refresh del token: " . $refreshEx->getMessage());
|
||||
}
|
||||
|
||||
usleep(500000); // 500ms
|
||||
continue;
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception("Massimo numero di tentativi raggiunto per {$endpoint}");
|
||||
}
|
||||
|
||||
// Esegui la chiamata
|
||||
$data = makeApiRequest($api, $endpoint);
|
||||
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
} 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, JSON_UNESCAPED_UNICODE));
|
||||
echo json_encode($errorResponse, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
// ID della CommessaWeb specifica (cambialo di volta in volta)
|
||||
$id = 529435; // TODO: Cambia questo valore con l'ID desiderato
|
||||
|
||||
// Endpoint per recuperare la CommessaWeb specifica con espansione dello schema custom
|
||||
$endpoint = "CommessaWeb({$id})";
|
||||
|
||||
// Opzioni per l'espansione: includi OrderCustomFields per ottenere i campi custom dello schema assegnato all'ordine
|
||||
$options = ['$expand' => 'OrderCustomFields'];
|
||||
|
||||
// Debug: salva URL usato
|
||||
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
|
||||
$query = http_build_query($options);
|
||||
$full_url = $base_url . $endpoint . ($query ? '?' . $query : '');
|
||||
file_put_contents(__DIR__ . '/last_url.txt', $full_url . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Chiamata API
|
||||
$data = $api->get($endpoint, $options);
|
||||
|
||||
// Salva il JSON in locale
|
||||
file_put_contents(__DIR__ . '/commessaweb_schema_response.json', json_encode($data, JSON_PRETTY_PRINT));
|
||||
|
||||
echo json_encode($data);
|
||||
} catch (Exception $e) {
|
||||
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -21,17 +21,26 @@ try {
|
||||
}
|
||||
|
||||
$results = [];
|
||||
$cacheDir = __DIR__ . '/cache';
|
||||
if (!is_dir($cacheDir)) mkdir($cacheDir, 0777, true);
|
||||
|
||||
foreach ($fieldIds as $customFieldId) {
|
||||
$cacheFile = $cacheDir . '/customfield_' . $customFieldId . '.json';
|
||||
|
||||
// Use cache if fresh (1 hour)
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 7200)) {
|
||||
$results[$customFieldId] = json_decode(file_get_contents($cacheFile), true);
|
||||
continue;
|
||||
}
|
||||
|
||||
$endpoint = "CustomField($customFieldId)?\$expand=CustomFieldsValues";
|
||||
$data = $api->get($endpoint);
|
||||
$values = $data['CustomFieldsValues'] ?? [];
|
||||
$results[$customFieldId] = $values;
|
||||
|
||||
$results[$customFieldId] = $data['CustomFieldsValues'] ?? [];
|
||||
file_put_contents($cacheFile, json_encode($values));
|
||||
}
|
||||
|
||||
// Debug ფაილი
|
||||
file_put_contents(__DIR__ . '/customfield_values_response.json', json_encode($results));
|
||||
|
||||
echo json_encode($results);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
// get_fixed_field_data.php
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
$field = $_GET['field'] ?? ''; // es: MoltiplicatorePrezzo, AnagraficaCertestObject, ...
|
||||
|
||||
if (!$field) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Parametro "field" mancante']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$api = VisualLimsApiClient::getInstance(); // also loads dotenv as a side-effect
|
||||
$simulate = ($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true';
|
||||
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
|
||||
|
||||
$data = null;
|
||||
$endpoint = null;
|
||||
$cache_file = null;
|
||||
$options = []; // qui puoi aggiungere filtri/ordering per campo se serve
|
||||
|
||||
try {
|
||||
switch ($field) {
|
||||
case 'MoltiplicatorePrezzo':
|
||||
$endpoint = 'MoltiplicatorePrezzi';
|
||||
$cache_file = __DIR__ . '/cache/moltiplicatori_prezzo.json';
|
||||
break;
|
||||
|
||||
case 'AnagraficaCertestObject':
|
||||
$endpoint = 'AnagraficaCertestObject';
|
||||
$cache_file = __DIR__ . '/cache/anagrafica_certest_object.json';
|
||||
break;
|
||||
|
||||
case 'AnagraficaCertestService':
|
||||
$endpoint = 'AnagraficaCertestService';
|
||||
$cache_file = __DIR__ . '/cache/anagrafica_certest_service.json';
|
||||
break;
|
||||
|
||||
case 'ClienteResponsabile':
|
||||
$id_cliente = (int)($_GET['id_cliente'] ?? 0);
|
||||
if ($id_cliente <= 0) {
|
||||
if (!$simulate) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Manca o invalido id_cliente']);
|
||||
exit;
|
||||
}
|
||||
$id_cliente = 1; // dummy — mock ignores the actual ID
|
||||
}
|
||||
$endpoint = "Cliente($id_cliente)?\$expand=Responsabili";
|
||||
$cache_file = __DIR__ . '/cache/cliente_responsabili_' . $id_cliente . '.json';
|
||||
break;
|
||||
|
||||
// case 'FuturoCampo5':
|
||||
// $endpoint = 'QualcosaElse';
|
||||
// break;
|
||||
|
||||
default:
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => "Campo non supportato: $field"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Caching skipped in simulate mode to avoid polluting real-data cache files
|
||||
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);
|
||||
|
||||
if (!$simulate && $cache_file) {
|
||||
file_put_contents($cache_file, json_encode($data, JSON_PRETTY_PRINT));
|
||||
}
|
||||
}
|
||||
|
||||
// Log ultimo URL (opzionale, per debug)
|
||||
$full_url = $base_url . $endpoint . ($options ? '?' . http_build_query($options) : '');
|
||||
file_put_contents(__DIR__ . '/last_urls.log', date('c') . " - $field - $full_url\n", FILE_APPEND);
|
||||
|
||||
echo json_encode($data);
|
||||
} catch (Exception $e) {
|
||||
file_put_contents(
|
||||
__DIR__ . '/error_log.txt',
|
||||
date('Y-m-d H:i:s') . " [$field] " . $e->getMessage() . PHP_EOL,
|
||||
FILE_APPEND
|
||||
);
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
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; }
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
// Read from matrici cache (populated by get_matrici.php / warm_cache.php)
|
||||
$cacheFile = __DIR__ . '/cache/matrici.json';
|
||||
if (file_exists($cacheFile)) {
|
||||
$data = json_decode(file_get_contents($cacheFile), true);
|
||||
$matrici = $data['value'] ?? [];
|
||||
} else {
|
||||
// Fallback: trigger cache creation
|
||||
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
$apiData = $api->get('Matrice');
|
||||
$matrici = [];
|
||||
foreach (($apiData['value'] ?? []) as $item) {
|
||||
$nome = $item['NomeMatrice'] ?? '';
|
||||
if ($nome !== '' && substr($nome, 0, 1) !== '*') {
|
||||
$matrici[] = ['IdMatrice' => $item['IdMatrice'], 'NomeMatrice' => $nome, 'MacroMatrice' => $item['MacroMatrice'] ?? null];
|
||||
}
|
||||
}
|
||||
if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true);
|
||||
file_put_contents($cacheFile, json_encode(['success' => true, 'value' => $matrici]));
|
||||
}
|
||||
|
||||
// Extract unique MacroMatrice values
|
||||
$macroValues = [];
|
||||
foreach ($matrici as $m) {
|
||||
$macro = $m['MacroMatrice'] ?? null;
|
||||
if ($macro !== null && $macro !== '' && substr($macro, 0, 1) !== '*') {
|
||||
$macroValues[$macro] = true;
|
||||
}
|
||||
}
|
||||
$macroMatrici = array_keys($macroValues);
|
||||
sort($macroMatrici);
|
||||
|
||||
echo json_encode(['success' => true, 'value' => $macroMatrici]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
// Endpoint per recuperare le Matrici
|
||||
$endpoint = 'Matrice';
|
||||
|
||||
// (Opzionale) aggiungi parametri, ad esempio $top per limitare i risultati
|
||||
$options = []; // oppure ad esempio: ['$top' => 100]
|
||||
|
||||
// Debug: salva URL usato
|
||||
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
|
||||
$query = http_build_query($options);
|
||||
$full_url = $base_url . $endpoint . ($query ? '?' . $query : '');
|
||||
file_put_contents(__DIR__ . '/last_url.txt', $full_url . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Chiamata API
|
||||
$data = $api->get($endpoint, $options);
|
||||
|
||||
// Salva il JSON in locale
|
||||
file_put_contents(__DIR__ . '/matrici_response.json', json_encode($data, JSON_PRETTY_PRINT));
|
||||
|
||||
echo json_encode($data);
|
||||
} catch (Exception $e) {
|
||||
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
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;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
// Cache file (1 hour TTL)
|
||||
$cacheFile = __DIR__ . '/cache/matrici.json';
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 7200)) {
|
||||
readfile($cacheFile);
|
||||
exit;
|
||||
}
|
||||
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
$data = $api->get('Matrice');
|
||||
|
||||
$matrici = [];
|
||||
if (isset($data['value']) && is_array($data['value'])) {
|
||||
foreach ($data['value'] as $item) {
|
||||
$nome = $item['NomeMatrice'] ?? '';
|
||||
if ($nome !== '' && substr($nome, 0, 1) !== '*') {
|
||||
$matrici[] = [
|
||||
'IdMatrice' => $item['IdMatrice'],
|
||||
'NomeMatrice' => $nome,
|
||||
'MacroMatrice' => $item['MacroMatrice'] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
usort($matrici, fn($a, $b) => strcasecmp($a['NomeMatrice'], $b['NomeMatrice']));
|
||||
}
|
||||
|
||||
$json = json_encode(['success' => true, 'value' => $matrici]);
|
||||
if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true);
|
||||
file_put_contents($cacheFile, $json);
|
||||
|
||||
echo $json;
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php'; // Risale a root/vendor/
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
// Set JSON header
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Debug: Log the path where we expect the .env file
|
||||
$envPath = dirname(__DIR__, 2);
|
||||
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . ' - Expected .env path: ' . $envPath . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Carica il file .env dalla root del progetto
|
||||
try {
|
||||
$dotenv = Dotenv::createImmutable($envPath);
|
||||
$dotenv->load();
|
||||
} catch (Exception $e) {
|
||||
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - Errore caricamento .env: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
|
||||
echo json_encode(['success' => false, 'message' => 'Errore caricamento configurazione: ' . $e->getMessage()]);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Recupera le variabili d'ambiente
|
||||
$dbHost = $_ENV['DB_HOST'];
|
||||
$dbName = $_ENV['DB_DATABASE'];
|
||||
$dbUser = $_ENV['DB_USERNAME'];
|
||||
$dbPass = $_ENV['DB_PASSWORD'];
|
||||
$dbPrefix = $_ENV['DB_PREFIX'];
|
||||
|
||||
// Debug: Log database connection details (excluding password)
|
||||
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . " - DB Connection: host=$dbHost, dbname=$dbName, user=$dbUser, prefix=$dbPrefix" . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Connessione al database MySQL
|
||||
try {
|
||||
$pdo = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8mb4", $dbUser, $dbPass);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
} catch (PDOException $e) {
|
||||
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - Errore connessione DB: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
|
||||
echo json_encode(['success' => false, 'message' => 'Errore connessione al database: ' . $e->getMessage()]);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
// Query per recuperare le matrici, includendo MacroMatrice, escludendo quelle che iniziano con '*' e ordinandole
|
||||
$query = "SELECT IdMatrice, NomeMatrice, MacroMatrice FROM {$dbPrefix}matrici WHERE NomeMatrice NOT LIKE '*%' ORDER BY NomeMatrice ASC";
|
||||
$stmt = $pdo->prepare($query);
|
||||
$stmt->execute();
|
||||
$matrici = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Debug: Log del numero di matrici recuperate
|
||||
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . ' - Retrieved ' . count($matrici) . ' matrices from database' . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Restituisci risposta JSON
|
||||
echo json_encode(['success' => true, 'value' => $matrici]);
|
||||
} catch (PDOException $e) {
|
||||
// Log errore e restituisci risposta di errore
|
||||
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - Errore nel recupero delle matrici: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
|
||||
echo json_encode(['success' => false, 'message' => 'Errore nel recupero delle matrici: ' . $e->getMessage()]);
|
||||
exit(1);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user