Compare commits
249
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 | ||
|
|
6ec0c2062e | ||
|
|
03771e3ca8 |
+6
-17
@@ -7,9 +7,9 @@ LOG_CHANNEL=stack
|
|||||||
|
|
||||||
DB_CONNECTION=mysql
|
DB_CONNECTION=mysql
|
||||||
DB_HOST="localhost"
|
DB_HOST="localhost"
|
||||||
DB_DATABASE="trfcertest"
|
DB_DATABASE="xxxx"
|
||||||
DB_USERNAME="solocla"
|
DB_USERNAME="xxxx"
|
||||||
DB_PASSWORD="xxxxxxx"
|
DB_PASSWORD="xxxxx"
|
||||||
DB_PREFIX="auth_"
|
DB_PREFIX="auth_"
|
||||||
|
|
||||||
BROADCAST_DRIVER=log
|
BROADCAST_DRIVER=log
|
||||||
@@ -41,19 +41,8 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
|||||||
|
|
||||||
# Credenziali API VisualLims
|
# Credenziali API VisualLims
|
||||||
SIMULATE_EXPORT_LIMS=true
|
SIMULATE_EXPORT_LIMS=true
|
||||||
API_BASE_URL=https://93.43.5.102/limsapi
|
API_BASE_URL=https://bvcpsitaly-elims.com/limsapi
|
||||||
API_USERNAME=xxxx
|
API_USERNAME=WebApiUser
|
||||||
API_PASSWORD=XXXX
|
API_PASSWORD=webapiuser01
|
||||||
|
|
||||||
BASE_URL=http://localhost:8000/userarea/
|
BASE_URL=http://localhost:8000/userarea/
|
||||||
|
|
||||||
# Credenziali ENTRAID
|
|
||||||
|
|
||||||
AZURE_CLIENT_ID=your-client-id
|
|
||||||
AZURE_CLIENT_SECRET=your-client-secret
|
|
||||||
AZURE_REDIRECT_URI=https://your-app.com/auth/azure/callback
|
|
||||||
AZURE_TENANT_ID=
|
|
||||||
|
|
||||||
MICROSOFT_CLIENT_ID=your_client_id_here
|
|
||||||
MICROSOFT_CLIENT_SECRET=your_client_secret_here
|
|
||||||
MICROSOFT_REDIRECT_URI="${APP_URL}/auth/microsoft/callback"
|
|
||||||
+48
-40
@@ -1,66 +1,74 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
/node_modules
|
/node_modules
|
||||||
/public/hot
|
|
||||||
/public/storage
|
|
||||||
/storage/*.key
|
|
||||||
/vendor
|
/vendor
|
||||||
|
|
||||||
/.idea
|
/.idea
|
||||||
/.fleet
|
/.fleet
|
||||||
/.vscode
|
/.vscode
|
||||||
/.vagrant
|
/.vagrant
|
||||||
Homestead.json
|
|
||||||
Homestead.yaml
|
/public/hot
|
||||||
npm-debug.log
|
/public/storage
|
||||||
yarn-error.log
|
|
||||||
.env
|
|
||||||
.phpunit.result.cache
|
|
||||||
.php_cs.cache
|
|
||||||
/documentation
|
|
||||||
/.phpunit.cache
|
|
||||||
/public/build
|
/public/build
|
||||||
|
|
||||||
|
/storage/*.key
|
||||||
|
|
||||||
|
.env
|
||||||
.env.backup
|
.env.backup
|
||||||
.env.production
|
.env.production
|
||||||
auth.json
|
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/*.json
|
||||||
/public/userarea/*.log
|
/public/userarea/*.log
|
||||||
/public/userarea/*.txt
|
/public/userarea/*.txt
|
||||||
|
/public/userarea/*_response.json
|
||||||
# File di log nella sottocartella class
|
/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/*.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
|
# File XLSX temporanei importati
|
||||||
/public/userarea/imported_trf/*.xlsx
|
/public/userarea/imported_trf/*.xlsx
|
||||||
/public/userarea/xlstemplates/*.xlsx
|
/public/userarea/xlstemplates/*.xlsx
|
||||||
|
|
||||||
# Ignora cartelle di foto generate
|
# Cartelle foto generate
|
||||||
/public/photostrf/
|
/public/photostrf/
|
||||||
/public/photostrf/qrcodes/
|
/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
|
*.log
|
||||||
|
|
||||||
|
public/userarea/cache/
|
||||||
|
public/userarea/error_log.txt
|
||||||
|
|
||||||
# Ignora cartella photostrf in public/userarea
|
# PDF/QR generati dai template (userarea)
|
||||||
/public/userarea/photostrf/
|
/public/userarea/pdftemplates/
|
||||||
public/userarea/customfield_values_response.json
|
# Runtime / rigenerati - non tracciare
|
||||||
/public/userarea/logaspi/
|
public/userarea/error_log.txt
|
||||||
|
public/userarea/schemi_base_response.json
|
||||||
public/userarea/logsapi/campione_762_1.json
|
public/userarea/logs/
|
||||||
public/userarea/logsapi/campione_763_1.json
|
public/userarea/imported_trf/
|
||||||
public/userarea/logsapi/campione_762_2.json
|
public/userarea/xlstemplates/
|
||||||
public/userarea/logsapi/campione_763_2.json
|
.well-known/
|
||||||
public/userarea/logsapi/commessaweb_create_762.json
|
|
||||||
public/userarea/logsapi/commessaweb_create_763.json
|
|
||||||
public/userarea/logsapi/commessaweb_customfields_762.json
|
|
||||||
public/userarea/logsapi/commessaweb_customfields_763.json
|
|
||||||
public/userarea/logsapi/commessaweb_invia_762.json
|
|
||||||
public/userarea/logsapi/commessaweb_invia_763.json
|
|
||||||
public/userarea/logsapi/last_auth_url.txt
|
|
||||||
|
|||||||
@@ -109,6 +109,8 @@ class LoginController extends Controller
|
|||||||
// Reindirizza in base al ruolo
|
// Reindirizza in base al ruolo
|
||||||
if ($user->hasRole('Admin')) {
|
if ($user->hasRole('Admin')) {
|
||||||
return redirect()->to('userarea/import_dashboard.php');
|
return redirect()->to('userarea/import_dashboard.php');
|
||||||
|
} elseif ($user->hasRole('SuperUser')) {
|
||||||
|
return redirect()->to('userarea/import_dashboard.php');
|
||||||
} elseif ($user->hasRole('User')) {
|
} elseif ($user->hasRole('User')) {
|
||||||
return redirect()->to('userarea/import_dashboard.php');
|
return redirect()->to('userarea/import_dashboard.php');
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -41,9 +41,10 @@
|
|||||||
"laravel/socialite": "^5.16",
|
"laravel/socialite": "^5.16",
|
||||||
"laravel/tinker": "^2.7",
|
"laravel/tinker": "^2.7",
|
||||||
"laravel/ui": "^4.0",
|
"laravel/ui": "^4.0",
|
||||||
"phpmailer/phpmailer": "^6.9",
|
"phpmailer/phpmailer": "^7.1",
|
||||||
"phpoffice/phpspreadsheet": "^4.1",
|
"phpoffice/phpspreadsheet": "^4.1",
|
||||||
"proengsoft/laravel-jsvalidation": "^4.0.0",
|
"proengsoft/laravel-jsvalidation": "^4.0.0",
|
||||||
|
"robmorgan/phinx": "^0.16.12",
|
||||||
"socialiteproviders/microsoft": "^4.7",
|
"socialiteproviders/microsoft": "^4.7",
|
||||||
"spatie/laravel-query-builder": "^5.0",
|
"spatie/laravel-query-builder": "^5.0",
|
||||||
"vanguardapp/activity-log": "^6.0",
|
"vanguardapp/activity-log": "^6.0",
|
||||||
|
|||||||
Generated
+656
-11
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "9c4f1e3bc3ee2180211c055e70635aef",
|
"content-hash": "e54cf3e0bbc04b94b563b01620ac3416",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "akaunting/laravel-setting",
|
"name": "akaunting/laravel-setting",
|
||||||
@@ -251,6 +251,330 @@
|
|||||||
],
|
],
|
||||||
"time": "2023-11-29T23:19:16+00:00"
|
"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",
|
"name": "carbonphp/carbon-doctrine-types",
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
@@ -2627,6 +2951,90 @@
|
|||||||
],
|
],
|
||||||
"time": "2022-12-11T20:36:23+00:00"
|
"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",
|
"name": "league/flysystem",
|
||||||
"version": "3.28.0",
|
"version": "3.28.0",
|
||||||
@@ -3758,16 +4166,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpmailer/phpmailer",
|
"name": "phpmailer/phpmailer",
|
||||||
"version": "v6.9.3",
|
"version": "v7.1.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
||||||
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e"
|
"reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/2f5c94fe7493efc213f643c23b1b1c249d40f47e",
|
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
|
||||||
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e",
|
"reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -3781,13 +4189,14 @@
|
|||||||
"doctrine/annotations": "^1.2.6 || ^1.13.3",
|
"doctrine/annotations": "^1.2.6 || ^1.13.3",
|
||||||
"php-parallel-lint/php-console-highlighter": "^1.0.0",
|
"php-parallel-lint/php-console-highlighter": "^1.0.0",
|
||||||
"php-parallel-lint/php-parallel-lint": "^1.3.2",
|
"php-parallel-lint/php-parallel-lint": "^1.3.2",
|
||||||
"phpcompatibility/php-compatibility": "^9.3.5",
|
"phpcompatibility/php-compatibility": "^10.0.0@dev",
|
||||||
"roave/security-advisories": "dev-latest",
|
"squizlabs/php_codesniffer": "^3.13.5",
|
||||||
"squizlabs/php_codesniffer": "^3.7.2",
|
|
||||||
"yoast/phpunit-polyfills": "^1.0.4"
|
"yoast/phpunit-polyfills": "^1.0.4"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
|
"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-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
|
||||||
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
||||||
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
|
"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",
|
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
|
"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": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3835,7 +4244,7 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-11-24T18:04:13+00:00"
|
"time": "2026-05-18T08:06:14+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpoffice/phpspreadsheet",
|
"name": "phpoffice/phpspreadsheet",
|
||||||
@@ -4980,6 +5389,93 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-04-27T21:32:50+00:00"
|
"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",
|
"name": "socialiteproviders/manager",
|
||||||
"version": "v4.8.1",
|
"version": "v4.8.1",
|
||||||
@@ -5312,6 +5808,85 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-05-31T14:57:53+00:00"
|
"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",
|
"name": "symfony/console",
|
||||||
"version": "v7.1.3",
|
"version": "v7.1.3",
|
||||||
@@ -5768,6 +6343,76 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-04-18T09:32:20+00:00"
|
"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",
|
"name": "symfony/finder",
|
||||||
"version": "v7.1.3",
|
"version": "v7.1.3",
|
||||||
@@ -11355,6 +12000,6 @@
|
|||||||
"php": "^8.2.0",
|
"php": "^8.2.0",
|
||||||
"ext-json": "*"
|
"ext-json": "*"
|
||||||
},
|
},
|
||||||
"platform-dev": [],
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.6.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class CreatePhinxTestTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('phinx_test');
|
||||||
|
$table
|
||||||
|
->addColumn('name', 'string', ['limit' => 100, 'null' => false])
|
||||||
|
->addColumn('created_at', 'datetime', ['default' => 'CURRENT_TIMESTAMP'])
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddAddnoteToTemplateMapping extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('template_mapping');
|
||||||
|
$table->addColumn('is_addnote', 'boolean', [
|
||||||
|
'default' => 0,
|
||||||
|
'null' => false,
|
||||||
|
'after' => 'is_visible_parts',
|
||||||
|
'comment' => 'Campo utilizzabile come addnote (multi-selezione)',
|
||||||
|
])
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class CreateOptionsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('options');
|
||||||
|
$table
|
||||||
|
->addColumn('opt_key', 'string', ['limit' => 100])
|
||||||
|
->addColumn('opt_value', 'text', ['null' => true])
|
||||||
|
->addColumn('created_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP'])
|
||||||
|
->addColumn('updated_at', 'timestamp', [
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
'update' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addIndex(['opt_key'], ['unique' => true])
|
||||||
|
->create();
|
||||||
|
|
||||||
|
// Righe iniziali del popup
|
||||||
|
$this->table('options')->insert([
|
||||||
|
[
|
||||||
|
'opt_key' => 'popup_enabled',
|
||||||
|
'opt_value' => '1',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'opt_key' => 'popup_version',
|
||||||
|
'opt_value' => '2026-07-migrazione',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'opt_key' => 'popup',
|
||||||
|
'opt_value' =>
|
||||||
|
'<h2 style="margin-top:0;">Nuovo portale disponibile</h2>'
|
||||||
|
. '<p>Ci siamo spostati. Da ora usa il nuovo indirizzo:</p>'
|
||||||
|
. '<p style="text-align:center;">'
|
||||||
|
. '<a href="https://trfsmart-bv.cesoft.io/" '
|
||||||
|
. 'style="display:inline-block;padding:12px 24px;background:#2563eb;'
|
||||||
|
. 'color:#fff;text-decoration:none;border-radius:6px;">Vai al nuovo portale</a>'
|
||||||
|
. '</p>'
|
||||||
|
. '<p style="font-size:13px;color:#666;">Salva il nuovo indirizzo nei preferiti. '
|
||||||
|
. 'Se riscontri problemi puoi continuare a usare questo portale per ora.</p>',
|
||||||
|
],
|
||||||
|
])->saveData();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddExportDateToDatadb extends AbstractMigration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Aggiunge export_date su datadb: data/ora di esportazione verso il LIMS,
|
||||||
|
* valorizzata insieme al numero commessa quando status passa a 'l'.
|
||||||
|
*/
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$this->table('datadb')
|
||||||
|
->addColumn('export_date', 'datetime', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'after' => 'commessaweb',
|
||||||
|
'comment' => 'Data/ora esportazione verso LIMS (valorizzata quando status = l)',
|
||||||
|
])
|
||||||
|
->addIndex(['export_date'], ['name' => 'idx_datadb_export_date'])
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class CreateLimsDailySamplesTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Tabella con un dato al giorno: numero totale di campioni inseriti
|
||||||
|
* nel LIMS (loro software) in quella giornata. Serve per calcolare
|
||||||
|
* la % di campioni esportati sul totale inserito nel LIMS.
|
||||||
|
*/
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('lims_daily_samples', [
|
||||||
|
'id' => false,
|
||||||
|
'primary_key' => ['id'],
|
||||||
|
'comment' => 'Totale campioni inseriti nel LIMS per giornata',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('id', 'integer', [
|
||||||
|
'identity' => true,
|
||||||
|
'signed' => false,
|
||||||
|
])
|
||||||
|
->addColumn('sample_date', 'date', [
|
||||||
|
'null' => false,
|
||||||
|
'comment' => 'Giornata di riferimento',
|
||||||
|
])
|
||||||
|
->addColumn('total_samples', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
'default' => 0,
|
||||||
|
'comment' => 'Totale campioni inseriti nel LIMS in quella giornata',
|
||||||
|
])
|
||||||
|
->addColumn('note', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
])
|
||||||
|
->addColumn('created_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addColumn('updated_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'update' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addIndex(['sample_date'], [
|
||||||
|
'unique' => true,
|
||||||
|
'name' => 'uq_lims_daily_samples_date',
|
||||||
|
])
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddTemplateNoteToExcelTemplates extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$this->table('excel_templates')
|
||||||
|
->addColumn('template_note', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'after' => 'idroutine',
|
||||||
|
'comment' => 'Nota opzionale del template, aggiunta in fondo alle note in esportazione',
|
||||||
|
])
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class DropUniqPartAnalysisIndex extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('identification_parts_analyses');
|
||||||
|
|
||||||
|
if ($table->hasIndexByName('uniq_part_analysis')) {
|
||||||
|
$table->removeIndexByName('uniq_part_analysis')->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('identification_parts_analyses');
|
||||||
|
|
||||||
|
if (!$table->hasIndexByName('uniq_part_analysis')) {
|
||||||
|
$table->addIndex(['part_id', 'analysis_recordkey'], [
|
||||||
|
'unique' => true,
|
||||||
|
'name' => 'uniq_part_analysis',
|
||||||
|
])->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class ChangeExcelrowToVarchar extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$this->table('datadb')
|
||||||
|
->changeColumn('excelrow', 'string', [
|
||||||
|
'limit' => 100,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$this->table('datadb')
|
||||||
|
->changeColumn('excelrow', 'integer', [
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggiunge il flag `engine` alla tabella `routine` esistente (0 = legacy su file,
|
||||||
|
* 1 = motore generico basato su routine_step) e crea la tabella `routine_step`.
|
||||||
|
*
|
||||||
|
* Migrazione puramente additiva: le routine legacy esistenti (id 1..4) restano
|
||||||
|
* intatte e con engine = 0 continuano a funzionare come prima.
|
||||||
|
*/
|
||||||
|
final class AddEngineAndRoutineStep extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
// 1) Flag engine sulla tabella esistente `routine`
|
||||||
|
$routine = $this->table('routine');
|
||||||
|
if (!$routine->hasColumn('engine')) {
|
||||||
|
$routine
|
||||||
|
->addColumn('engine', 'boolean', [
|
||||||
|
'default' => 0,
|
||||||
|
'null' => false,
|
||||||
|
'comment' => '0 = routine legacy su file, 1 = motore generico (routine_step)',
|
||||||
|
'after' => 'filename',
|
||||||
|
])
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Nuova tabella routine_step
|
||||||
|
if (!$this->hasTable('routine_step')) {
|
||||||
|
$table = $this->table('routine_step', [
|
||||||
|
'id' => 'idstep',
|
||||||
|
'signed' => false,
|
||||||
|
'engine' => 'InnoDB',
|
||||||
|
'collation' => 'utf8mb4_general_ci',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('idroutine', 'integer', [
|
||||||
|
'null' => false,
|
||||||
|
'comment' => 'FK verso routine.idroutine',
|
||||||
|
])
|
||||||
|
->addColumn('step_order', 'integer', [
|
||||||
|
'default' => 1,
|
||||||
|
'null' => false,
|
||||||
|
'comment' => 'Ordine di esecuzione dello step nella routine',
|
||||||
|
])
|
||||||
|
->addColumn('operation', 'string', [
|
||||||
|
'limit' => 50,
|
||||||
|
'null' => false,
|
||||||
|
'comment' => 'Verbo operazione: merge, replace, trim, copy, clear, uppercase, lowercase',
|
||||||
|
])
|
||||||
|
->addColumn('params', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
'comment' => 'Parametri operazione in formato JSON',
|
||||||
|
])
|
||||||
|
->addColumn('active', 'boolean', [
|
||||||
|
'default' => 1,
|
||||||
|
'null' => false,
|
||||||
|
'comment' => 'Se 0 lo step viene saltato',
|
||||||
|
])
|
||||||
|
->addIndex(['idroutine', 'step_order'], ['name' => 'idx_routine_order'])
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddLogFileToDatadb extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('datadb');
|
||||||
|
if (!$table->hasColumn('log_file')) {
|
||||||
|
$table->addColumn('log_file', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'after' => 'commessaweb', // aggiusta se la colonna non esiste
|
||||||
|
])->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddErrorStatusToDatadb extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Aggiunge 'x' (errore export) all'enum status, preservando i valori esistenti
|
||||||
|
$this->execute("ALTER TABLE `datadb`
|
||||||
|
MODIFY `status` ENUM('i','P','l','x') NULL DEFAULT NULL");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// Rollback: riporta le eventuali 'x' a NULL prima di togliere il valore
|
||||||
|
$this->execute("UPDATE `datadb` SET `status` = NULL WHERE `status` = 'x'");
|
||||||
|
$this->execute("ALTER TABLE `datadb`
|
||||||
|
MODIFY `status` ENUM('i','P','l') NULL DEFAULT NULL");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddNotifyExportEmailToAuthUsers extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('auth_users');
|
||||||
|
if (!$table->hasColumn('notify_export_email')) {
|
||||||
|
$table->addColumn('notify_export_email', 'boolean', [
|
||||||
|
'default' => true, // di default ricevono la mail
|
||||||
|
'null' => false,
|
||||||
|
'after' => 'lims_global_user_id',
|
||||||
|
])->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddAnalysisPriceToPartAnalyses extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$this->table('identification_parts_analyses')
|
||||||
|
->addColumn('analysis_price', 'decimal', [
|
||||||
|
'precision' => 10,
|
||||||
|
'scale' => 2,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'after' => 'analysis_level',
|
||||||
|
])
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ex public/userarea/sql/lims_value_binding.sql (feature/xls-mapping)
|
||||||
|
*/
|
||||||
|
final class CreateLimsValueBinding extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if ($this->hasTable('lims_value_binding')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->execute("
|
||||||
|
CREATE TABLE `lims_value_binding` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`template_id` int(11) NOT NULL,
|
||||||
|
`mapping_id` int(11) NOT NULL,
|
||||||
|
`field_id` int(11) NOT NULL,
|
||||||
|
`source_value` varchar(500) NOT NULL,
|
||||||
|
`source_key` varchar(500) NOT NULL,
|
||||||
|
`lims_value_id` int(11) NOT NULL,
|
||||||
|
`lims_value` varchar(500) DEFAULT NULL,
|
||||||
|
`created_by` int(11) DEFAULT NULL,
|
||||||
|
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uq_mapping_source` (`mapping_id`, `source_key`),
|
||||||
|
KEY `idx_template` (`template_id`),
|
||||||
|
KEY `idx_field_source` (`field_id`, `source_key`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$this->execute("DROP TABLE IF EXISTS `lims_value_binding`");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ex public/userarea/sql/fixed_field_binding.sql (feature/xls-mapping)
|
||||||
|
* Idempotente: salta colonne/indici gia' presenti.
|
||||||
|
*/
|
||||||
|
final class XlsFixedFieldBinding extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// --- template_fixed_mapping ---
|
||||||
|
$tfm = $this->table('template_fixed_mapping');
|
||||||
|
|
||||||
|
if (!$tfm->hasColumn('json_node')) {
|
||||||
|
$this->execute("ALTER TABLE `template_fixed_mapping`
|
||||||
|
ADD COLUMN `json_node` varchar(500) DEFAULT NULL");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$tfm->hasColumn('default_source')) {
|
||||||
|
$this->execute("ALTER TABLE `template_fixed_mapping`
|
||||||
|
ADD COLUMN `default_source` enum('manual','xls','json') NOT NULL DEFAULT 'manual'");
|
||||||
|
} else {
|
||||||
|
$this->execute("ALTER TABLE `template_fixed_mapping`
|
||||||
|
MODIFY COLUMN `default_source` enum('manual','xls','json') NOT NULL DEFAULT 'manual'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$tfm->hasColumn('excel_column')) {
|
||||||
|
$this->execute("ALTER TABLE `template_fixed_mapping`
|
||||||
|
ADD COLUMN `excel_column` varchar(255) DEFAULT NULL AFTER `json_node`");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- lims_value_binding ---
|
||||||
|
$lvb = $this->table('lims_value_binding');
|
||||||
|
|
||||||
|
if (!$lvb->hasColumn('binding_kind')) {
|
||||||
|
$this->execute("ALTER TABLE `lims_value_binding`
|
||||||
|
ADD COLUMN `binding_kind` enum('custom','fixed') NOT NULL DEFAULT 'custom' AFTER `template_id`");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->execute("ALTER TABLE `lims_value_binding`
|
||||||
|
MODIFY COLUMN `field_id` int(11) NOT NULL DEFAULT 0");
|
||||||
|
|
||||||
|
if ($lvb->hasIndexByName('uq_mapping_source')) {
|
||||||
|
$this->execute("ALTER TABLE `lims_value_binding` DROP INDEX `uq_mapping_source`");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$lvb->hasIndexByName('uq_kind_mapping_source')) {
|
||||||
|
$this->execute("ALTER TABLE `lims_value_binding`
|
||||||
|
ADD UNIQUE KEY `uq_kind_mapping_source` (`binding_kind`, `mapping_id`, `source_key`)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$lvb = $this->table('lims_value_binding');
|
||||||
|
|
||||||
|
if ($lvb->hasIndexByName('uq_kind_mapping_source')) {
|
||||||
|
$this->execute("ALTER TABLE `lims_value_binding` DROP INDEX `uq_kind_mapping_source`");
|
||||||
|
}
|
||||||
|
if (!$lvb->hasIndexByName('uq_mapping_source')) {
|
||||||
|
$this->execute("ALTER TABLE `lims_value_binding`
|
||||||
|
ADD UNIQUE KEY `uq_mapping_source` (`mapping_id`, `source_key`)");
|
||||||
|
}
|
||||||
|
if ($lvb->hasColumn('binding_kind')) {
|
||||||
|
$this->execute("ALTER TABLE `lims_value_binding` DROP COLUMN `binding_kind`");
|
||||||
|
}
|
||||||
|
$this->execute("ALTER TABLE `lims_value_binding`
|
||||||
|
MODIFY COLUMN `field_id` int(11) NOT NULL");
|
||||||
|
|
||||||
|
$tfm = $this->table('template_fixed_mapping');
|
||||||
|
foreach (['excel_column', 'default_source', 'json_node'] as $col) {
|
||||||
|
if ($tfm->hasColumn($col)) {
|
||||||
|
$this->execute("ALTER TABLE `template_fixed_mapping` DROP COLUMN `$col`");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Seed\AbstractSeed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed dimostrativo del motore generico.
|
||||||
|
*
|
||||||
|
* - Assicura che le routine legacy esistenti abbiano engine = 0 (nessun impatto).
|
||||||
|
* - Crea una routine di esempio "merge_D_F_H_into_J" con engine = 1
|
||||||
|
* e il relativo step: unisce le colonne D, F, H nella colonna J.
|
||||||
|
*
|
||||||
|
* Nota: filename per le routine engine punta al file motore condiviso _engine.php,
|
||||||
|
* così apply_routine.php lo risolve correttamente anche se il flusso invia il filename.
|
||||||
|
*/
|
||||||
|
final class RoutineEngineSeed extends AbstractSeed
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$pdo = $this->getAdapter()->getConnection();
|
||||||
|
|
||||||
|
// 1) Le legacy restano engine = 0 (idempotente).
|
||||||
|
$pdo->exec("UPDATE routine SET engine = 0 WHERE engine IS NULL");
|
||||||
|
|
||||||
|
// 2) Crea/aggiorna la routine engine di esempio.
|
||||||
|
$name = 'merge_D_F_H_into_J';
|
||||||
|
$description = 'Unisce le colonne D, F, H nella colonna J durante l\'importazione';
|
||||||
|
$filename = '_engine.php';
|
||||||
|
|
||||||
|
$check = $pdo->prepare("SELECT idroutine FROM routine WHERE name = :name LIMIT 1");
|
||||||
|
$check->execute([':name' => $name]);
|
||||||
|
$existing = $check->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$idroutine = (int)$existing['idroutine'];
|
||||||
|
$upd = $pdo->prepare(
|
||||||
|
"UPDATE routine
|
||||||
|
SET description = :descr, filename = :fn, engine = 1, instruction = :instr
|
||||||
|
WHERE idroutine = :id"
|
||||||
|
);
|
||||||
|
$upd->execute([
|
||||||
|
':descr' => $description,
|
||||||
|
':fn' => $filename,
|
||||||
|
':instr' => 'Routine gestita dal motore generico (routine_step).',
|
||||||
|
':id' => $idroutine,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$ins = $pdo->prepare(
|
||||||
|
"INSERT INTO routine (name, description, instruction, filename, engine, headerrow)
|
||||||
|
VALUES (:name, :descr, :instr, :fn, 1, 1)"
|
||||||
|
);
|
||||||
|
$ins->execute([
|
||||||
|
':name' => $name,
|
||||||
|
':descr' => $description,
|
||||||
|
':instr' => 'Routine gestita dal motore generico (routine_step).',
|
||||||
|
':fn' => $filename,
|
||||||
|
]);
|
||||||
|
$idroutine = (int)$pdo->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Ripulisci eventuali step precedenti di questa routine e reinserisci.
|
||||||
|
$del = $pdo->prepare("DELETE FROM routine_step WHERE idroutine = :id");
|
||||||
|
$del->execute([':id' => $idroutine]);
|
||||||
|
|
||||||
|
// Step unico: merge D,F,H -> J. Colonne espresse in lettere per leggibilità.
|
||||||
|
$params = json_encode([
|
||||||
|
'source_columns' => ['D', 'F', 'H'],
|
||||||
|
'target_column' => 'J',
|
||||||
|
'separator' => ' ',
|
||||||
|
'skip_empty' => true,
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
$insStep = $pdo->prepare(
|
||||||
|
"INSERT INTO routine_step (idroutine, step_order, operation, params, active)
|
||||||
|
VALUES (:id, 1, 'merge', :params, 1)"
|
||||||
|
);
|
||||||
|
$insStep->execute([':id' => $idroutine, ':params' => $params]);
|
||||||
|
}
|
||||||
|
}
|
||||||
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";
|
$newtemplate = "New Template";
|
||||||
$dashtemplate = "Dashboard Templates";
|
$dashtemplate = "Dashboard Templates";
|
||||||
$insertnewtemplatexls = "Insert New Template XLS";
|
$insertnewtemplatexls = "Insert New Template XLS";
|
||||||
|
$bindingsmanage = "Value Bindings";
|
||||||
// Datatables
|
// Datatables
|
||||||
$langdatatables = [
|
$langdatatables = [
|
||||||
"paginate_first" => "First",
|
"paginate_first" => "First",
|
||||||
|
|||||||
@@ -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", "");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -19,9 +19,14 @@ $(document).ready(function () {
|
|||||||
let descriptionTextbox = null;
|
let descriptionTextbox = null;
|
||||||
let markerObjects = {};
|
let markerObjects = {};
|
||||||
let nextMarkerId = 1;
|
let nextMarkerId = 1;
|
||||||
|
let freeTextMode = false;
|
||||||
|
let freeTextObjects = {};
|
||||||
|
let nextFreeTextId = 1;
|
||||||
let partsListData = [];
|
let partsListData = [];
|
||||||
// DIMENSIONE GLOBALE MARKER
|
// DIMENSIONE GLOBALE MARKER
|
||||||
let globalMarkerSize = 16;
|
let globalMarkerSize = 16;
|
||||||
|
// COLORE TESTO LISTA DESCRIZIONI
|
||||||
|
let globalDescriptionColor = "#000000";
|
||||||
|
|
||||||
// ===================
|
// ===================
|
||||||
// MODAL INITIALIZATION
|
// MODAL INITIALIZATION
|
||||||
@@ -53,7 +58,7 @@ $(document).ready(function () {
|
|||||||
.data("iddatadb", iddatadb || null)
|
.data("iddatadb", iddatadb || null)
|
||||||
.data("idquotations", idquotations || null);
|
.data("idquotations", idquotations || null);
|
||||||
|
|
||||||
loadPhoto(iddatadb, idquotations);
|
// Parti (non legate al canvas) possono partire subito
|
||||||
loadExistingParts(iddatadb, idquotations);
|
loadExistingParts(iddatadb, idquotations);
|
||||||
|
|
||||||
const modalElement = document.getElementById("annotationsModal");
|
const modalElement = document.getElementById("annotationsModal");
|
||||||
@@ -72,6 +77,18 @@ $(document).ready(function () {
|
|||||||
focus: true,
|
focus: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IMPORTANTE: carica la foto SOLO dopo che il modale è completamente
|
||||||
|
// visibile (animazione fade terminata). Se l'immagine è già in cache
|
||||||
|
// del browser, l'evento "load" può scattare quasi subito, PRIMA che
|
||||||
|
// il contenitore del canvas abbia dimensioni reali (parent ancora
|
||||||
|
// display:none durante il fade) — questo rompeva il canvas Fabric.js
|
||||||
|
// per la prima foto mostrata (bug riprodotto solo con foto già in cache,
|
||||||
|
// tipico del server di test dove le foto erano già state visualizzate).
|
||||||
|
$(modalElement).one("shown.bs.modal", function () {
|
||||||
|
loadPhoto(iddatadb, idquotations);
|
||||||
|
});
|
||||||
|
|
||||||
modal.show();
|
modal.show();
|
||||||
// Inizializza slider dimensione marker
|
// Inizializza slider dimensione marker
|
||||||
$("#markerSizeSlider").val(globalMarkerSize);
|
$("#markerSizeSlider").val(globalMarkerSize);
|
||||||
@@ -130,6 +147,9 @@ $(document).ready(function () {
|
|||||||
selectedPartNumber = null;
|
selectedPartNumber = null;
|
||||||
unsavedChanges = false;
|
unsavedChanges = false;
|
||||||
partsListData = [];
|
partsListData = [];
|
||||||
|
freeTextMode = false; // ← aggiungi
|
||||||
|
freeTextObjects = {}; // ← aggiungi
|
||||||
|
$("#addFreeTextBtnAnnotations").removeClass("active"); // ← aggiungi
|
||||||
if (fabricCanvas) {
|
if (fabricCanvas) {
|
||||||
fabricCanvas.off();
|
fabricCanvas.off();
|
||||||
fabricCanvas.dispose();
|
fabricCanvas.dispose();
|
||||||
@@ -138,6 +158,7 @@ $(document).ready(function () {
|
|||||||
descriptionTextbox = null;
|
descriptionTextbox = null;
|
||||||
markerObjects = {};
|
markerObjects = {};
|
||||||
globalMarkerSize = 16;
|
globalMarkerSize = 16;
|
||||||
|
globalDescriptionColor = "#000000";
|
||||||
$("#photoSelectorContainerAnnotations").empty().hide();
|
$("#photoSelectorContainerAnnotations").empty().hide();
|
||||||
$("#samplePhotoAnnotations").attr("src", "");
|
$("#samplePhotoAnnotations").attr("src", "");
|
||||||
$("#partsListAnnotations").empty();
|
$("#partsListAnnotations").empty();
|
||||||
@@ -179,6 +200,60 @@ $(document).ready(function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ===================
|
// ===================
|
||||||
|
// TESTO LIBERO — TOGGLE MODALITÀ
|
||||||
|
// ===================
|
||||||
|
$(document)
|
||||||
|
.off("click.freeTextToggle", "#addFreeTextBtnAnnotations")
|
||||||
|
.on("click.freeTextToggle", "#addFreeTextBtnAnnotations", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
freeTextMode = !freeTextMode;
|
||||||
|
$(this).toggleClass("active", freeTextMode);
|
||||||
|
|
||||||
|
// Disattiva la selezione parte/marker mentre si aggiunge testo libero
|
||||||
|
if (freeTextMode) {
|
||||||
|
selectedPartNumber = null;
|
||||||
|
$("#partsListAnnotations .list-group-item").removeClass(
|
||||||
|
"active",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Free text mode:", freeTextMode);
|
||||||
|
});
|
||||||
|
// ===================
|
||||||
|
// COLORE LISTA DESCRIZIONI
|
||||||
|
// ===================
|
||||||
|
$(document)
|
||||||
|
.off("input.descriptionColor", "#descriptionColorPickerAnnotations")
|
||||||
|
.on(
|
||||||
|
"input.descriptionColor",
|
||||||
|
"#descriptionColorPickerAnnotations",
|
||||||
|
function () {
|
||||||
|
globalDescriptionColor = $(this).val();
|
||||||
|
|
||||||
|
if (descriptionTextbox && fabricCanvas) {
|
||||||
|
descriptionTextbox.set("fill", globalDescriptionColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggiorna anche i testi liberi già piazzati sulla foto corrente
|
||||||
|
const currentPhoto = $("#samplePhotoAnnotations").attr("src");
|
||||||
|
const annotations = photoAnnotations[currentPhoto];
|
||||||
|
if (annotations && annotations.freeTexts) {
|
||||||
|
annotations.freeTexts.forEach((entry) => {
|
||||||
|
entry.color = globalDescriptionColor;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Object.values(freeTextObjects).forEach((obj) => {
|
||||||
|
obj.set("fill", globalDescriptionColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fabricCanvas) fabricCanvas.renderAll();
|
||||||
|
|
||||||
|
markUnsaved();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// ===================
|
||||||
// PHOTO LOADERS
|
// PHOTO LOADERS
|
||||||
// ===================
|
// ===================
|
||||||
function loadPhoto(iddatadb, idquotations) {
|
function loadPhoto(iddatadb, idquotations) {
|
||||||
@@ -349,7 +424,29 @@ $(document).ready(function () {
|
|||||||
console.log(
|
console.log(
|
||||||
"Evento mouse:down su canvas, selectedPartNumber:",
|
"Evento mouse:down su canvas, selectedPartNumber:",
|
||||||
selectedPartNumber,
|
selectedPartNumber,
|
||||||
|
"freeTextMode:",
|
||||||
|
freeTextMode,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Modalità testo libero: ha priorità, un solo click e si disattiva da sola
|
||||||
|
if (freeTextMode) {
|
||||||
|
if (options.target) {
|
||||||
|
console.log(
|
||||||
|
"Click su un oggetto esistente, ignoro.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pointer = fabricCanvas.getPointer(options.e);
|
||||||
|
const x = pointer.x / photoData.scale;
|
||||||
|
const y = pointer.y / photoData.scale;
|
||||||
|
|
||||||
|
addFreeText(x, y);
|
||||||
|
|
||||||
|
freeTextMode = false;
|
||||||
|
$("#addFreeTextBtnAnnotations").removeClass("active");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedPartNumber === null) {
|
if (selectedPartNumber === null) {
|
||||||
console.log(
|
console.log(
|
||||||
"Nessuna parte selezionata, ignoro il click.",
|
"Nessuna parte selezionata, ignoro il click.",
|
||||||
@@ -404,6 +501,7 @@ $(document).ready(function () {
|
|||||||
|
|
||||||
updateMarkers();
|
updateMarkers();
|
||||||
updateDescriptions();
|
updateDescriptions();
|
||||||
|
updateFreeTexts();
|
||||||
}, 10);
|
}, 10);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -614,17 +712,11 @@ $(document).ready(function () {
|
|||||||
partsListData.forEach((part) => {
|
partsListData.forEach((part) => {
|
||||||
const partNumber = part.part_number;
|
const partNumber = part.part_number;
|
||||||
const partDescription = part.part_description;
|
const partDescription = part.part_description;
|
||||||
|
const isMixPart = String(part.mix || "N").toUpperCase() === "Y";
|
||||||
|
|
||||||
const partColor =
|
const partColor =
|
||||||
partColors[partNumber] ||
|
partColors[partNumber] || (isMixPart ? "#0000ff" : "#ff0000");
|
||||||
(partDescription.toLowerCase().startsWith("mix")
|
if (partNumber && partDescription && (showMixParts || !isMixPart)) {
|
||||||
? "#0000ff"
|
|
||||||
: "#ff0000");
|
|
||||||
if (
|
|
||||||
partNumber &&
|
|
||||||
partDescription &&
|
|
||||||
(showMixParts ||
|
|
||||||
!partDescription.toLowerCase().startsWith("mix"))
|
|
||||||
) {
|
|
||||||
const colorOptions = predefinedColors
|
const colorOptions = predefinedColors
|
||||||
.map(
|
.map(
|
||||||
(color) =>
|
(color) =>
|
||||||
@@ -798,6 +890,48 @@ $(document).ready(function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enableDragDropPartsList() {
|
||||||
|
const list = $("#partsListAnnotations");
|
||||||
|
if (!list.length || typeof $.ui === "undefined" || !$.ui.sortable) {
|
||||||
|
console.warn("jQuery UI o .sortable non disponibile. Ritento...");
|
||||||
|
setTimeout(enableDragDropPartsList, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list.hasClass("ui-sortable")) {
|
||||||
|
list.sortable("destroy"); // evita duplicati
|
||||||
|
}
|
||||||
|
|
||||||
|
list.sortable({
|
||||||
|
items: "li.list-group-item",
|
||||||
|
placeholder: "list-group-item placeholder",
|
||||||
|
axis: "y",
|
||||||
|
containment: "parent",
|
||||||
|
tolerance: "pointer",
|
||||||
|
start: function (e, ui) {
|
||||||
|
ui.item.addClass("dragging");
|
||||||
|
},
|
||||||
|
stop: function (e, ui) {
|
||||||
|
ui.item.removeClass("dragging");
|
||||||
|
const newOrder = [];
|
||||||
|
list.find("li").each(function () {
|
||||||
|
const partNumber = $(this).data("part-number");
|
||||||
|
const part = partsListData.find(
|
||||||
|
(p) => p.part_number == partNumber,
|
||||||
|
);
|
||||||
|
if (part) newOrder.push(part);
|
||||||
|
});
|
||||||
|
partsListData = newOrder;
|
||||||
|
console.log(
|
||||||
|
"Ordine parti aggiornato:",
|
||||||
|
partsListData.map((p) => p.part_number),
|
||||||
|
);
|
||||||
|
markUnsaved();
|
||||||
|
updateMarkers();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Delegazione evento per il checkbox
|
// Delegazione evento per il checkbox
|
||||||
$(document)
|
$(document)
|
||||||
.off("change.showMix", "#showMixPartsAnnotations")
|
.off("change.showMix", "#showMixPartsAnnotations")
|
||||||
@@ -830,6 +964,7 @@ $(document).ready(function () {
|
|||||||
}
|
}
|
||||||
updatePartsList();
|
updatePartsList();
|
||||||
updateMarkers();
|
updateMarkers();
|
||||||
|
setTimeout(enableDragDropPartsList, 50);
|
||||||
if (
|
if (
|
||||||
photoAnnotations[$("#samplePhotoAnnotations").attr("src")]
|
photoAnnotations[$("#samplePhotoAnnotations").attr("src")]
|
||||||
?.hasDescriptions
|
?.hasDescriptions
|
||||||
@@ -879,11 +1014,10 @@ $(document).ready(function () {
|
|||||||
) {
|
) {
|
||||||
partsListData = response.parts;
|
partsListData = response.parts;
|
||||||
response.parts.forEach((part) => {
|
response.parts.forEach((part) => {
|
||||||
const defaultColor = part.part_description
|
const isMixPart =
|
||||||
.toLowerCase()
|
String(part.mix || "N").toUpperCase() === "Y";
|
||||||
.startsWith("mix")
|
const defaultColor = isMixPart ? "#0000ff" : "#ff0000";
|
||||||
? "#0000ff"
|
|
||||||
: "#ff0000";
|
|
||||||
partColors[part.part_number] = defaultColor;
|
partColors[part.part_number] = defaultColor;
|
||||||
});
|
});
|
||||||
updatePartsList();
|
updatePartsList();
|
||||||
@@ -928,7 +1062,128 @@ $(document).ready(function () {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// ===================
|
||||||
|
// TESTO LIBERO — CREAZIONE E RENDER
|
||||||
|
// ===================
|
||||||
|
function addFreeText(x, y) {
|
||||||
|
const currentPhoto = $("#samplePhotoAnnotations").attr("src");
|
||||||
|
|
||||||
|
if (!photoAnnotations[currentPhoto]) {
|
||||||
|
photoAnnotations[currentPhoto] = {
|
||||||
|
markers: [],
|
||||||
|
freeTexts: [],
|
||||||
|
hasDescriptions: false,
|
||||||
|
descriptionPosition: { x: 10, y: 10 },
|
||||||
|
descriptionSize: {
|
||||||
|
width: photoData.displayWidth * 0.3,
|
||||||
|
height: photoData.displayHeight * 0.3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!photoAnnotations[currentPhoto].freeTexts) {
|
||||||
|
photoAnnotations[currentPhoto].freeTexts = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = {
|
||||||
|
id: nextFreeTextId++,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
text: "Testo",
|
||||||
|
color: globalDescriptionColor,
|
||||||
|
fontSize: Math.max(16, Math.round(globalMarkerSize * 0.8)),
|
||||||
|
};
|
||||||
|
|
||||||
|
photoAnnotations[currentPhoto].freeTexts.push(entry);
|
||||||
|
renderFreeText(entry, true);
|
||||||
|
markUnsaved();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFreeText(entry, startEditing = false) {
|
||||||
|
const itext = new fabric.IText(entry.text, {
|
||||||
|
left: entry.x * photoData.scale,
|
||||||
|
top: entry.y * photoData.scale,
|
||||||
|
fontFamily: "Arial",
|
||||||
|
fontSize: entry.fontSize,
|
||||||
|
fill: entry.color,
|
||||||
|
selectable: true,
|
||||||
|
hasControls: true,
|
||||||
|
editable: true,
|
||||||
|
borderColor: "#333",
|
||||||
|
cornerColor: "#000",
|
||||||
|
cornerSize: 12,
|
||||||
|
transparentCorners: false,
|
||||||
|
cornerStyle: "rect",
|
||||||
|
borderScaleFactor: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
itext.freeTextId = entry.id;
|
||||||
|
// FIX: Fabric.js appende la <textarea> nascosta (usata per catturare la
|
||||||
|
// tastiera durante l'editing) a document.body, FUORI dal modale.
|
||||||
|
// Il focus trap di Bootstrap la considera "esterna" e ruba il focus
|
||||||
|
// ogni volta che Fabric lo assegna a quella textarea — così i tasti
|
||||||
|
// digitati non arrivano mai al testo. Spostiamo la textarea dentro
|
||||||
|
// il modale non appena l'editing viene avviato.
|
||||||
|
itext.on("editing:entered", function () {
|
||||||
|
const modalEl = document.getElementById("annotationsModal");
|
||||||
|
if (
|
||||||
|
this.hiddenTextarea &&
|
||||||
|
modalEl &&
|
||||||
|
this.hiddenTextarea.parentNode !== modalEl
|
||||||
|
) {
|
||||||
|
modalEl.appendChild(this.hiddenTextarea);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
itext.on("moving", function () {
|
||||||
|
entry.x = this.left / photoData.scale;
|
||||||
|
entry.y = this.top / photoData.scale;
|
||||||
|
markUnsaved();
|
||||||
|
});
|
||||||
|
|
||||||
|
itext.on("editing:exited", function () {
|
||||||
|
entry.text = this.text;
|
||||||
|
markUnsaved();
|
||||||
|
});
|
||||||
|
|
||||||
|
itext.on("scaling", function () {
|
||||||
|
entry.fontSize = Math.round(this.fontSize * this.scaleY);
|
||||||
|
this.set({ fontSize: entry.fontSize, scaleX: 1, scaleY: 1 });
|
||||||
|
markUnsaved();
|
||||||
|
});
|
||||||
|
|
||||||
|
fabricCanvas.add(itext);
|
||||||
|
freeTextObjects[entry.id] = itext;
|
||||||
|
|
||||||
|
// IMPORTANTE: non attivare l'editing nello stesso evento mouse:down che
|
||||||
|
// ha creato l'oggetto. Fabric processa quel click come "click su canvas
|
||||||
|
// vuoto" (il target non esisteva ancora al momento del mousedown) e al
|
||||||
|
// successivo mouseup deseleziona tutto, interrompendo l'editing appena
|
||||||
|
// avviato — l'utente non riesce a scrivere né a spostare il testo.
|
||||||
|
// Rimandiamo l'attivazione al prossimo giro di event loop, così avviene
|
||||||
|
// DOPO che Fabric ha concluso la gestione nativa di quel click.
|
||||||
|
if (startEditing) {
|
||||||
|
setTimeout(() => {
|
||||||
|
fabricCanvas.setActiveObject(itext);
|
||||||
|
itext.enterEditing();
|
||||||
|
itext.selectAll();
|
||||||
|
fabricCanvas.renderAll();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fabricCanvas.renderAll();
|
||||||
|
}
|
||||||
|
function updateFreeTexts() {
|
||||||
|
for (let id in freeTextObjects) {
|
||||||
|
fabricCanvas.remove(freeTextObjects[id]);
|
||||||
|
delete freeTextObjects[id];
|
||||||
|
}
|
||||||
|
freeTextObjects = {};
|
||||||
|
|
||||||
|
const currentPhoto = $("#samplePhotoAnnotations").attr("src");
|
||||||
|
const annotations = photoAnnotations[currentPhoto];
|
||||||
|
if (!annotations || !annotations.freeTexts) return;
|
||||||
|
|
||||||
|
annotations.freeTexts.forEach((entry) => renderFreeText(entry, false));
|
||||||
|
}
|
||||||
// ===================
|
// ===================
|
||||||
// MARKERS & DESCRIPTIONS
|
// MARKERS & DESCRIPTIONS
|
||||||
// ===================
|
// ===================
|
||||||
@@ -960,11 +1215,10 @@ $(document).ready(function () {
|
|||||||
(p) => p.part_number == marker.partNumber,
|
(p) => p.part_number == marker.partNumber,
|
||||||
);
|
);
|
||||||
const partDescription = part ? part.part_description : "";
|
const partDescription = part ? part.part_description : "";
|
||||||
if (
|
const isMixPart =
|
||||||
!showMixParts &&
|
part && String(part.mix || "N").toUpperCase() === "Y";
|
||||||
partDescription &&
|
|
||||||
partDescription.toLowerCase().startsWith("mix")
|
if (!showMixParts && isMixPart) {
|
||||||
) {
|
|
||||||
console.log("Ignoro marker per parte Mix:", marker.partNumber);
|
console.log("Ignoro marker per parte Mix:", marker.partNumber);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1072,11 +1326,10 @@ $(document).ready(function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const partsList = partsListData
|
const partsList = partsListData
|
||||||
.filter(
|
.filter((part) => {
|
||||||
(part) =>
|
const isMixPart = String(part.mix || "N").toUpperCase() === "Y";
|
||||||
showMixParts ||
|
return showMixParts || !isMixPart;
|
||||||
!part.part_description.toLowerCase().startsWith("mix"),
|
})
|
||||||
)
|
|
||||||
.map((part) => `${part.part_number} ${part.part_description}`);
|
.map((part) => `${part.part_number} ${part.part_description}`);
|
||||||
|
|
||||||
const text = partsList.join("\n");
|
const text = partsList.join("\n");
|
||||||
@@ -1096,7 +1349,7 @@ $(document).ready(function () {
|
|||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
fontFamily: "Arial",
|
fontFamily: "Arial",
|
||||||
fontSize: Math.max(16, Math.round(globalMarkerSize * 0.8)),
|
fontSize: Math.max(16, Math.round(globalMarkerSize * 0.8)),
|
||||||
fill: "#000000",
|
fill: globalDescriptionColor,
|
||||||
padding: 10,
|
padding: 10,
|
||||||
editable: false,
|
editable: false,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
@@ -1236,6 +1489,40 @@ $(document).ready(function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================
|
||||||
|
// TESTO LIBERO — RIMOZIONE CON CANC/DELETE
|
||||||
|
// ===================
|
||||||
|
$(document).on("keydown.freeTextDelete", function (e) {
|
||||||
|
// Solo se il modale annotazioni è aperto
|
||||||
|
if (!$("#annotationsModal").hasClass("show")) return;
|
||||||
|
if (!fabricCanvas) return;
|
||||||
|
|
||||||
|
// Ignora se si sta scrivendo dentro un campo di editing testo
|
||||||
|
// (es. l'utente sta modificando il contenuto dell'IText stesso)
|
||||||
|
const active = fabricCanvas.getActiveObject();
|
||||||
|
if (active && active.isEditing) return;
|
||||||
|
|
||||||
|
if (e.key !== "Delete" && e.key !== "Backspace") return;
|
||||||
|
if (!active || active.freeTextId === undefined) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const currentPhoto = $("#samplePhotoAnnotations").attr("src");
|
||||||
|
const annotations = photoAnnotations[currentPhoto];
|
||||||
|
|
||||||
|
if (annotations && annotations.freeTexts) {
|
||||||
|
annotations.freeTexts = annotations.freeTexts.filter(
|
||||||
|
(entry) => entry.id !== active.freeTextId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fabricCanvas.remove(active);
|
||||||
|
delete freeTextObjects[active.freeTextId];
|
||||||
|
fabricCanvas.renderAll();
|
||||||
|
|
||||||
|
markUnsaved();
|
||||||
|
});
|
||||||
|
|
||||||
// Delegazione evento per il pulsante "Descrizioni"
|
// Delegazione evento per il pulsante "Descrizioni"
|
||||||
$(document)
|
$(document)
|
||||||
.off("click.addDescriptions", "#addDescriptionsBtnAnnotations")
|
.off("click.addDescriptions", "#addDescriptionsBtnAnnotations")
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ ob_start();
|
|||||||
session_start();
|
session_start();
|
||||||
require_once '../../vendor/autoload.php';
|
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' => []];
|
$response = ['error' => '', 'rows' => [], 'columns' => [], 'template_id' => 0, 'filename' => '', 'excel_data' => []];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -18,12 +21,50 @@ try {
|
|||||||
throw new Exception("Dati della routine mancanti.");
|
throw new Exception("Dati della routine mancanti.");
|
||||||
}
|
}
|
||||||
|
|
||||||
$routineFile = __DIR__ . '/routines/' . $filename;
|
// --- NUOVO: risolvi la routine dal DB per decidere legacy vs motore ---
|
||||||
|
// Riusa la connessione PDO del progetto.
|
||||||
|
require_once __DIR__ . '/class/db-functions.php';
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
// Identifica la routine: preferisci idroutine se passato, altrimenti risali dal filename.
|
||||||
|
$idroutine = isset($input['routine_data']['idroutine'])
|
||||||
|
? (int)$input['routine_data']['idroutine']
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if ($idroutine > 0) {
|
||||||
|
$stmt = $pdo->prepare("SELECT idroutine, name, filename, engine FROM routine WHERE idroutine = :id LIMIT 1");
|
||||||
|
$stmt->execute([':id' => $idroutine]);
|
||||||
|
} else {
|
||||||
|
// Il matching per filename è ambiguo per le routine a motore: tutte
|
||||||
|
// condividono _engine.php. Senza idroutine non possiamo sapere quale
|
||||||
|
// routine engine applicare -> meglio fermarsi che eseguire quella sbagliata.
|
||||||
|
if ($filename === '_engine.php') {
|
||||||
|
throw new Exception("Routine a motore senza idroutine: impossibile determinare quale routine applicare. Verifica che il template inoltri routine_data.idroutine.");
|
||||||
|
}
|
||||||
|
$stmt = $pdo->prepare("SELECT idroutine, name, filename, engine FROM routine WHERE filename = :fn LIMIT 1");
|
||||||
|
$stmt->execute([':fn' => $filename]);
|
||||||
|
}
|
||||||
|
$routineRow = $stmt->fetch();
|
||||||
|
|
||||||
|
// engine = 1 -> motore generico (_engine.php); 0 o record assente -> legacy come oggi.
|
||||||
|
$useEngine = $routineRow && (int)$routineRow['engine'] === 1;
|
||||||
|
|
||||||
|
if ($useEngine) {
|
||||||
|
// Il motore ha bisogno di sapere quale routine caricare dagli step.
|
||||||
|
$routineData['idroutine'] = (int)$routineRow['idroutine'];
|
||||||
|
$routineData['name'] = $routineData['name'] ?? ($routineRow['name'] ?? '');
|
||||||
|
$routineFile = __DIR__ . '/routines/_engine.php';
|
||||||
|
} else {
|
||||||
|
// Percorso legacy: il vecchio file della routine, identico a prima.
|
||||||
|
$legacyFilename = $routineRow['filename'] ?? $filename;
|
||||||
|
$routineFile = __DIR__ . '/routines/' . $legacyFilename;
|
||||||
|
}
|
||||||
|
|
||||||
if (file_exists($routineFile)) {
|
if (file_exists($routineFile)) {
|
||||||
include_once $routineFile;
|
include_once $routineFile;
|
||||||
$routineData['xls_headers'] = $_SESSION['headers'] ?? [];
|
$routineData['xls_headers'] = $_SESSION['headers'] ?? [];
|
||||||
applyRoutine($excelData, $routineData); // Modifica $excelData in place
|
applyRoutine($excelData, $routineData); // Modifica $excelData in place
|
||||||
error_log("Routine {$routineData['name']} applicata (file: {$filename}) per template {$template_id}, header row: {$headerrow}");
|
error_log("Routine " . ($routineData['name'] ?? '') . " applicata (" . ($useEngine ? "motore" : "file: {$filename}") . ") per template {$template_id}, header row: {$headerrow}");
|
||||||
} else {
|
} else {
|
||||||
throw new Exception("File della routine non trovato: $routineFile");
|
throw new Exception("File della routine non trovato: $routineFile");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,299 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
include('include/headscript.php');
|
||||||
|
require_once __DIR__ . '/class/binding-functions.php';
|
||||||
|
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
$templateFilter = isset($_GET['template_id']) ? (int) $_GET['template_id'] : 0;
|
||||||
|
$search = trim($_GET['q'] ?? '');
|
||||||
|
|
||||||
|
$templates = $pdo->query("SELECT id, name FROM excel_templates ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$conds = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($templateFilter > 0) {
|
||||||
|
$conds[] = 'b.template_id = ?';
|
||||||
|
$params[] = $templateFilter;
|
||||||
|
}
|
||||||
|
if ($search !== '') {
|
||||||
|
$conds[] = '(b.source_value LIKE ? OR b.lims_value LIKE ? OR m.field_label LIKE ? OR f.fixed_field_key LIKE ?)';
|
||||||
|
$like = '%' . $search . '%';
|
||||||
|
array_push($params, $like, $like, $like, $like);
|
||||||
|
}
|
||||||
|
$where = $conds ? ('WHERE ' . implode(' AND ', $conds)) : '';
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT b.id, b.template_id, b.binding_kind, b.mapping_id, b.field_id, b.source_value,
|
||||||
|
b.lims_value_id, b.lims_value, b.updated_at,
|
||||||
|
t.name AS template_name,
|
||||||
|
COALESCE(m.field_label, f.fixed_field_key) AS field_label,
|
||||||
|
COALESCE(m.excel_column, f.excel_column) AS excel_column,
|
||||||
|
f.fixed_field_key
|
||||||
|
FROM lims_value_binding b
|
||||||
|
LEFT JOIN excel_templates t ON t.id = b.template_id
|
||||||
|
LEFT JOIN template_mapping m ON m.id = b.mapping_id AND b.binding_kind = 'custom'
|
||||||
|
LEFT JOIN template_fixed_mapping f ON f.id = b.mapping_id AND b.binding_kind = 'fixed'
|
||||||
|
$where
|
||||||
|
ORDER BY t.name ASC, field_label ASC, b.source_value ASC"
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$groups = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$kind = $r['binding_kind'] ?? 'custom';
|
||||||
|
$gkey = $kind . ':' . (int) $r['mapping_id'];
|
||||||
|
if (!isset($groups[$gkey])) {
|
||||||
|
$groups[$gkey] = [
|
||||||
|
'kind' => $kind,
|
||||||
|
'label' => $kind === 'fixed'
|
||||||
|
? binding_fixed_label((string) $r['fixed_field_key'])
|
||||||
|
: ($r['field_label'] ?: ('Mapping ' . $gkey)),
|
||||||
|
'excel_column' => $r['excel_column'] ?: '',
|
||||||
|
'template_name' => $r['template_name'] ?: ('Template ' . $r['template_id']),
|
||||||
|
'template_id' => (int) $r['template_id'],
|
||||||
|
'rows' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$groups[$gkey]['rows'][] = $r;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||||
|
<?php include('cssinclude.php'); ?>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
.source-value {
|
||||||
|
font-family: Consolas, Monaco, monospace;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
td .select2-container {
|
||||||
|
min-width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--default .select2-selection--single {
|
||||||
|
height: 38px;
|
||||||
|
border-color: #ced4da;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||||
|
line-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<title>Gestione binding - <?= htmlspecialchars($titlewebsite ?? '', ENT_QUOTES, 'UTF-8'); ?></title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="wrapper">
|
||||||
|
<?php include('include/navbar.php'); ?>
|
||||||
|
<?php include('include/topbar.php'); ?>
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<div class="page-content">
|
||||||
|
|
||||||
|
<div class="card radius-10">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">Binding valori importati → LIMS</h6>
|
||||||
|
<small class="text-muted">
|
||||||
|
Le modifiche valgono dalla prossima importazione: i record gia' importati non vengono aggiornati.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="row g-2 align-items-end mb-3" method="get">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label mb-1">Template</label>
|
||||||
|
<select class="form-select" name="template_id" onchange="this.form.submit()">
|
||||||
|
<option value="">Tutti</option>
|
||||||
|
<?php foreach ($templates as $t): ?>
|
||||||
|
<option value="<?= (int) $t['id'] ?>" <?= $templateFilter === (int) $t['id'] ? 'selected' : '' ?>>
|
||||||
|
<?= htmlspecialchars($t['name']) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label mb-1">Cerca</label>
|
||||||
|
<input type="text" class="form-control" name="q" value="<?= htmlspecialchars($search) ?>"
|
||||||
|
placeholder="valore importato, valore LIMS, campo...">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<button type="submit" class="btn btn-primary">Filtra</button>
|
||||||
|
<a href="bindings_manage.php" class="btn btn-outline-secondary">Azzera</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="bindingError" class="alert alert-danger" style="display:none;"></div>
|
||||||
|
|
||||||
|
<?php if (empty($rows)): ?>
|
||||||
|
<div class="alert alert-info mb-0">Nessun binding salvato per i filtri selezionati.</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:32px;"></th>
|
||||||
|
<th>Valore importato</th>
|
||||||
|
<th>Valore LIMS</th>
|
||||||
|
<th style="width:150px;">Aggiornato</th>
|
||||||
|
<th style="width:110px;">Azioni</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($groups as $gkey => $g): ?>
|
||||||
|
<tr class="table-light">
|
||||||
|
<td colspan="5">
|
||||||
|
<strong><?= htmlspecialchars($g['label']) ?></strong>
|
||||||
|
<?php if (($g['kind'] ?? 'custom') === 'fixed'): ?><span class="badge bg-light text-dark border">campo fisso</span><?php endif; ?>
|
||||||
|
<span class="text-muted">
|
||||||
|
· <?= htmlspecialchars($g['template_name']) ?>
|
||||||
|
<?php if ($g['excel_column'] !== ''): ?>
|
||||||
|
· colonna «<?= htmlspecialchars($g['excel_column']) ?>»
|
||||||
|
<?php endif; ?>
|
||||||
|
· <?= count($g['rows']) ?> valore/i
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php foreach ($g['rows'] as $r): ?>
|
||||||
|
<tr class="binding-row"
|
||||||
|
data-binding-id="<?= (int) $r['id'] ?>"
|
||||||
|
data-kind="<?= htmlspecialchars($r['binding_kind'] ?? 'custom', ENT_QUOTES) ?>"
|
||||||
|
data-fixed-key="<?= htmlspecialchars($r['fixed_field_key'] ?? '', ENT_QUOTES) ?>"
|
||||||
|
data-template-id="<?= (int) $r['template_id'] ?>"
|
||||||
|
data-mapping-id="<?= (int) $r['mapping_id'] ?>"
|
||||||
|
data-field-id="<?= (int) $r['field_id'] ?>"
|
||||||
|
data-source-value="<?= htmlspecialchars($r['source_value'], ENT_QUOTES) ?>">
|
||||||
|
<td class="text-muted ps-4">›</td>
|
||||||
|
<td class="source-value"><?= htmlspecialchars($r['source_value']) ?></td>
|
||||||
|
<td>
|
||||||
|
<select class="form-select binding-select">
|
||||||
|
<option value="<?= (int) $r['lims_value_id'] ?>" selected>
|
||||||
|
<?= htmlspecialchars($r['lims_value'] ?: ('#' . $r['lims_value_id'])) ?>
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<div class="binding-status small mt-1"></div>
|
||||||
|
</td>
|
||||||
|
<td class="small text-muted"><?= htmlspecialchars((string) $r['updated_at']) ?></td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger delete-binding-btn">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overlay toggle-icon"></div>
|
||||||
|
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
||||||
|
<?php include('include/footer.php'); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('jsinclude.php'); ?>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(function() {
|
||||||
|
const $error = $('#bindingError');
|
||||||
|
|
||||||
|
$('.binding-select').each(function() {
|
||||||
|
const $row = $(this).closest('.binding-row');
|
||||||
|
const isFixed = $row.data('kind') === 'fixed';
|
||||||
|
|
||||||
|
$(this).select2({
|
||||||
|
placeholder: 'Seleziona valore LIMS...',
|
||||||
|
width: '100%',
|
||||||
|
ajax: {
|
||||||
|
url: isFixed ? 'search_fixed_field_values.php' : 'search_customfield_values.php',
|
||||||
|
dataType: 'json',
|
||||||
|
delay: 200,
|
||||||
|
data: params => isFixed ? {
|
||||||
|
field_key: $row.data('fixed-key'),
|
||||||
|
template_id: $row.data('template-id'),
|
||||||
|
q: params.term || '',
|
||||||
|
limit: 50
|
||||||
|
} : {
|
||||||
|
field_id: $row.data('field-id'),
|
||||||
|
q: params.term || '',
|
||||||
|
limit: 50
|
||||||
|
},
|
||||||
|
processResults: data => ({
|
||||||
|
results: data.results || []
|
||||||
|
})
|
||||||
|
},
|
||||||
|
minimumInputLength: 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.binding-select').on('change', function() {
|
||||||
|
const $select = $(this);
|
||||||
|
const $row = $select.closest('.binding-row');
|
||||||
|
const $status = $row.find('.binding-status');
|
||||||
|
const selected = $select.select2('data')[0] || {};
|
||||||
|
|
||||||
|
if (!$select.val()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$status.removeClass('text-success text-danger').addClass('text-muted').text('Salvataggio...');
|
||||||
|
|
||||||
|
$.post('save_binding.php', {
|
||||||
|
kind: $row.data('kind') || 'custom',
|
||||||
|
template_id: $row.data('template-id'),
|
||||||
|
mapping_id: $row.data('mapping-id'),
|
||||||
|
field_id: $row.data('field-id'),
|
||||||
|
source_value: String($row.data('source-value')),
|
||||||
|
lims_value_id: $select.val(),
|
||||||
|
lims_value: selected.text || ''
|
||||||
|
}).then(resp => {
|
||||||
|
if (resp && resp.success) {
|
||||||
|
$status.removeClass('text-muted').addClass('text-success').text('Salvato');
|
||||||
|
} else {
|
||||||
|
$status.removeClass('text-muted').addClass('text-danger').text((resp && resp.error) || 'Errore');
|
||||||
|
}
|
||||||
|
}).fail(() => {
|
||||||
|
$status.removeClass('text-muted').addClass('text-danger').text('Errore di rete');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.delete-binding-btn').on('click', function() {
|
||||||
|
const $row = $(this).closest('.binding-row');
|
||||||
|
|
||||||
|
$error.hide();
|
||||||
|
|
||||||
|
$.post('delete_binding.php', {
|
||||||
|
binding_id: $row.data('binding-id')
|
||||||
|
}).then(resp => {
|
||||||
|
if (resp && resp.success) {
|
||||||
|
$row.remove();
|
||||||
|
} else {
|
||||||
|
$error.text((resp && resp.error) || 'Errore durante l\'eliminazione.').show();
|
||||||
|
}
|
||||||
|
}).fail(() => {
|
||||||
|
$error.text('Errore di rete durante l\'eliminazione.').show();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use PHPMailer\PHPMailer\PHPMailer;
|
||||||
|
use PHPMailer\PHPMailer\Exception as PHPMailerException;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../../vendor/autoload.php'; // aggiusta il path se serve
|
||||||
|
|
||||||
|
class Mailer
|
||||||
|
{
|
||||||
|
public static function send(array $to, string $subject, string $htmlBody, ?string $altBody = null): array
|
||||||
|
{
|
||||||
|
$mail = new PHPMailer(true);
|
||||||
|
try {
|
||||||
|
$mail->isSMTP();
|
||||||
|
$mail->Host = env('MAIL_HOST');
|
||||||
|
$mail->SMTPAuth = true;
|
||||||
|
$mail->Username = env('MAIL_USERNAME');
|
||||||
|
$mail->Password = env('MAIL_PASSWORD');
|
||||||
|
$mail->Port = (int) env('MAIL_PORT', 465);
|
||||||
|
$enc = env('MAIL_ENCRYPTION', 'ssl');
|
||||||
|
$mail->SMTPSecure = ($enc === 'tls')
|
||||||
|
? PHPMailer::ENCRYPTION_STARTTLS
|
||||||
|
: PHPMailer::ENCRYPTION_SMTPS;
|
||||||
|
$mail->CharSet = 'UTF-8';
|
||||||
|
|
||||||
|
$mail->setFrom(env('MAIL_FROM_ADDRESS'), env('MAIL_FROM_NAME', 'SmartTRF'));
|
||||||
|
|
||||||
|
foreach ($to as $addr) {
|
||||||
|
$addr = trim((string)$addr);
|
||||||
|
if ($addr !== '' && filter_var($addr, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$mail->addAddress($addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$mail->getToAddresses()) {
|
||||||
|
return ['ok' => false, 'error' => 'Nessun destinatario valido'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$mail->isHTML(true);
|
||||||
|
$mail->Subject = $subject;
|
||||||
|
$mail->Body = $htmlBody;
|
||||||
|
$mail->AltBody = $altBody ?: strip_tags($htmlBody);
|
||||||
|
|
||||||
|
$mail->send();
|
||||||
|
return ['ok' => true, 'error' => null];
|
||||||
|
} catch (PHPMailerException $e) {
|
||||||
|
error_log('Mailer error: ' . $mail->ErrorInfo);
|
||||||
|
return ['ok' => false, 'error' => $mail->ErrorInfo];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,17 @@ class VisualLimsApiClient
|
|||||||
public static function getInstance()
|
public static function getInstance()
|
||||||
{
|
{
|
||||||
if (self::$instance === null) {
|
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;
|
return self::$instance;
|
||||||
}
|
}
|
||||||
@@ -193,8 +203,91 @@ class VisualLimsApiClient
|
|||||||
return json_decode($response, true);
|
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()
|
public function getBaseUrl()
|
||||||
{
|
{
|
||||||
return $this->baseUrl;
|
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);
|
||||||
|
}
|
||||||
@@ -3,6 +3,9 @@ require_once dirname(__DIR__, 3) . '/vendor/autoload.php';
|
|||||||
|
|
||||||
use Dotenv\Dotenv;
|
use Dotenv\Dotenv;
|
||||||
|
|
||||||
|
Dotenv::createImmutable(dirname(__DIR__, 3))->safeLoad();
|
||||||
|
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
|
||||||
|
|
||||||
class DBHandlerSelect
|
class DBHandlerSelect
|
||||||
{
|
{
|
||||||
private static $instance = null;
|
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";
|
||||||
@@ -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
@@ -17,6 +17,8 @@ try {
|
|||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
|
||||||
|
|
||||||
// Recupera le variabili d'ambiente
|
// Recupera le variabili d'ambiente
|
||||||
$dbHost = $_ENV['DB_HOST'];
|
$dbHost = $_ENV['DB_HOST'];
|
||||||
$dbName = $_ENV['DB_DATABASE'];
|
$dbName = $_ENV['DB_DATABASE'];
|
||||||
|
|||||||
@@ -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,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);
|
||||||
|
}
|
||||||
@@ -1,29 +1,75 @@
|
|||||||
<?php include('include/headscript.php');
|
<?php include('include/headscript.php');
|
||||||
|
|
||||||
// Controlla se è stato passato un ID valido
|
// Check if a valid ID was provided
|
||||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||||
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Invalid ID"));
|
header("Location: templates_dashboard.php?status=error&message=" . urlencode("Invalid ID"));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$id = intval($_GET['id']); // Sanifica l'ID
|
$id = intval($_GET['id']);
|
||||||
|
|
||||||
// Recupera il template dal database
|
// Retrieve template from database
|
||||||
$db = DBHandlerSelect::getInstance();
|
$db = DBHandlerSelect::getInstance();
|
||||||
$pdo = $db->getConnection();
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT * FROM excel_templates WHERE id = ?");
|
$stmt = $pdo->prepare("SELECT * FROM excel_templates WHERE id = ?");
|
||||||
$stmt->execute([$id]);
|
$stmt->execute([$id]);
|
||||||
$template = $stmt->fetch(PDO::FETCH_ASSOC);
|
$template = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if (!$template) {
|
if (!$template) {
|
||||||
header("Location: template_dashboard.php?status=error&message=" . urlencode("Template not found"));
|
header("Location: templates_dashboard.php?status=error&message=" . urlencode("Template not found"));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recupera tutte le routine dal database
|
// Retrieve all routines
|
||||||
$stmt = $pdo->prepare("SELECT * FROM routine");
|
$stmt = $pdo->prepare("SELECT * FROM routine");
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
$routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Retrieve active API/JSON configurations
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT id, name, provider_code, api_type, php_class_name
|
||||||
|
FROM api_configurations
|
||||||
|
WHERE is_active = 1
|
||||||
|
ORDER BY name ASC
|
||||||
|
");
|
||||||
|
$stmt->execute();
|
||||||
|
$apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$buttonBgPalette = [
|
||||||
|
'#0d6efd' => 'Blue',
|
||||||
|
'#6610f2' => 'Indigo',
|
||||||
|
'#6f42c1' => 'Purple',
|
||||||
|
'#d63384' => 'Pink',
|
||||||
|
'#dc3545' => 'Red',
|
||||||
|
'#fd7e14' => 'Orange',
|
||||||
|
'#ffc107' => 'Yellow',
|
||||||
|
'#198754' => 'Green',
|
||||||
|
'#20c997' => 'Teal',
|
||||||
|
'#0dcaf0' => 'Cyan',
|
||||||
|
'#212529' => 'Dark',
|
||||||
|
'#6c757d' => 'Gray',
|
||||||
|
'#495057' => 'Slate',
|
||||||
|
'#795548' => 'Brown',
|
||||||
|
'#2f5d50' => 'Sage Green',
|
||||||
|
];
|
||||||
|
|
||||||
|
$buttonTextPalette = [
|
||||||
|
'#ffffff' => 'White',
|
||||||
|
'#6c757d' => 'Gray',
|
||||||
|
'#000000' => 'Black',
|
||||||
|
];
|
||||||
|
|
||||||
|
$currentButtonBgColor = strtolower($template['button_bg_color'] ?? '#007bff');
|
||||||
|
$currentButtonTextColor = strtolower($template['button_text_color'] ?? '#ffffff');
|
||||||
|
|
||||||
|
if (!array_key_exists($currentButtonBgColor, array_change_key_case($buttonBgPalette, CASE_LOWER))) {
|
||||||
|
$currentButtonBgColor = '#0d6efd';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonTextPalette, CASE_LOWER))) {
|
||||||
|
$currentButtonTextColor = '#ffffff';
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@@ -34,7 +80,64 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||||
<?php include('cssinclude.php'); ?>
|
<?php include('cssinclude.php'); ?>
|
||||||
<!-- Include jQuery prima di Select2 -->
|
<style>
|
||||||
|
.color-palette {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-option {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-option input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-swatch {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 2px solid #d6dbe0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
|
||||||
|
transition: all 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-option input:checked+.color-swatch {
|
||||||
|
border: 3px solid #111827;
|
||||||
|
transform: scale(1.08);
|
||||||
|
box-shadow: 0 0 0 4px rgba(13, 110, 253, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-swatch .check-mark {
|
||||||
|
display: none;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-option input:checked+.color-swatch .check-mark {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #555;
|
||||||
|
max-width: 55px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||||
<title>Edit Template <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
<title>Edit Template <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||||
@@ -44,19 +147,22 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
|
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="mb-0">Update XLS Template</h5>
|
<h5 class="mb-0">Update Template</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="mb-2">Edit the following form in order to update the selected import XLS template</p>
|
<p class="mb-2">Edit the following form in order to update the selected import template</p>
|
||||||
<p class="mb-2">Mandatory Fields</p>
|
<p class="mb-2">Mandatory Fields</p>
|
||||||
<ul class="mb-0">
|
<ul class="mb-0">
|
||||||
<li>Template Name</li>
|
<li>Template Name</li>
|
||||||
<li>Row Header and Column Header: where the title of the excel starts</li>
|
<li>Source Type</li>
|
||||||
<li>Schema</li>
|
<li>Schema and Client</li>
|
||||||
|
<li>Row Header and Column Header only for XLS templates</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,34 +175,104 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<form id="editTemplateForm" method="POST">
|
<form id="editTemplateForm" method="POST">
|
||||||
<input type="hidden" name="id" value="<?php echo $template['id']; ?>">
|
<input type="hidden" name="id" value="<?php echo (int)$template['id']; ?>">
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label"><?= htmlspecialchars($templatename, ENT_QUOTES, 'UTF-8'); ?> *</label>
|
<label class="form-label"><?= htmlspecialchars($templatename, ENT_QUOTES, 'UTF-8'); ?> *</label>
|
||||||
<input type="text" name="name" class="form-control" value="<?php echo htmlspecialchars($template['name']); ?>" required>
|
<input type="text" name="name" class="form-control" value="<?php echo htmlspecialchars($template['name'] ?? ''); ?>" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Source Type *</label>
|
||||||
|
<select name="source_type" id="sourceType" class="form-control" required>
|
||||||
|
<option value="XLS" <?php echo (($template['source_type'] ?? 'XLS') === 'XLS') ? 'selected' : ''; ?>>XLS</option>
|
||||||
|
<option value="API" <?php echo (($template['source_type'] ?? 'XLS') === 'API') ? 'selected' : ''; ?>>API</option>
|
||||||
|
<option value="JSON" <?php echo (($template['source_type'] ?? 'XLS') === 'JSON') ? 'selected' : ''; ?>>JSON</option>
|
||||||
|
<option value="PDF" <?php echo (($template['source_type'] ?? 'XLS') === 'PDF') ? 'selected' : ''; ?>>PDF</option>
|
||||||
|
</select>
|
||||||
|
<small class="text-muted">Choose the source used by this template</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3" id="headerRowWrapper">
|
||||||
<label class="form-label"><?= htmlspecialchars($rowheader, ENT_QUOTES, 'UTF-8'); ?> *</label>
|
<label class="form-label"><?= htmlspecialchars($rowheader, ENT_QUOTES, 'UTF-8'); ?> *</label>
|
||||||
<input type="number" name="header_row" class="form-control" value="<?php echo $template['header_row']; ?>" required>
|
<input type="number" name="header_row" id="headerRow" class="form-control" value="<?php echo htmlspecialchars($template['header_row'] ?? ''); ?>">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3" id="startColumnWrapper">
|
||||||
<label class="form-label"><?= htmlspecialchars($columnheader, ENT_QUOTES, 'UTF-8'); ?>*</label>
|
<label class="form-label"><?= htmlspecialchars($columnheader, ENT_QUOTES, 'UTF-8'); ?> *</label>
|
||||||
<input type="text" name="start_column" class="form-control" value="<?php echo htmlspecialchars($template['start_column']); ?>" required>
|
<input type="text" name="start_column" id="startColumn" class="form-control" value="<?php echo htmlspecialchars($template['start_column'] ?? ''); ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3" id="xlsSheetNumberWrapper">
|
||||||
|
<label class="form-label">XLS Sheet Number</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="xls_sheet_index"
|
||||||
|
id="xlsSheetIndex"
|
||||||
|
class="form-control"
|
||||||
|
min="0"
|
||||||
|
value="<?php echo htmlspecialchars($template['xls_sheet_index'] ?? 0); ?>">
|
||||||
|
<small class="text-muted">
|
||||||
|
Use 0 for the first sheet, 1 for the second sheet, 2 for the third sheet, and so on.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3" id="apiConfigWrapper" style="display: none;">
|
||||||
|
<label class="form-label">API / JSON Configuration *</label>
|
||||||
|
<select name="api_config_id" id="apiConfigSelect" class="form-control">
|
||||||
|
<option value="">Select an API configuration...</option>
|
||||||
|
|
||||||
|
<?php foreach ($apiConfigurations as $apiConfig): ?>
|
||||||
|
<?php
|
||||||
|
$apiLabelParts = [];
|
||||||
|
|
||||||
|
if (!empty($apiConfig['name'])) {
|
||||||
|
$apiLabelParts[] = $apiConfig['name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($apiConfig['provider_code'])) {
|
||||||
|
$apiLabelParts[] = '[' . $apiConfig['provider_code'] . ']';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($apiConfig['api_type'])) {
|
||||||
|
$apiLabelParts[] = '(' . $apiConfig['api_type'] . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($apiConfig['php_class_name'])) {
|
||||||
|
$apiLabelParts[] = '- ' . $apiConfig['php_class_name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiLabel = implode(' ', $apiLabelParts);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<option
|
||||||
|
value="<?php echo (int)$apiConfig['id']; ?>"
|
||||||
|
<?php echo ((int)($template['api_config_id'] ?? 0) === (int)$apiConfig['id']) ? 'selected' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars($apiLabel, ENT_QUOTES, 'UTF-8'); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<small class="text-muted">
|
||||||
|
Select the API/JSON configuration linked to this template.
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label"><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></label>
|
<label class="form-label"><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></label>
|
||||||
<textarea name="description" class="form-control"><?php echo htmlspecialchars($template['description']); ?></textarea>
|
<textarea name="description" class="form-control"><?php echo htmlspecialchars($template['description'] ?? ''); ?></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label"><?= htmlspecialchars($desttable, ENT_QUOTES, 'UTF-8'); ?>*</label>
|
<label class="form-label">Template Note</label>
|
||||||
<input type="text" name="target_table" class="form-control" value="<?php echo htmlspecialchars($template['target_table']); ?>" readonly required>
|
<textarea name="template_note" class="form-control" rows="3"><?php echo htmlspecialchars($template['template_note'] ?? ''); ?></textarea>
|
||||||
|
<small class="text-muted">If set, this note will be appended at the end of the notes during export.</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"><?= htmlspecialchars($desttable, ENT_QUOTES, 'UTF-8'); ?> *</label>
|
||||||
|
<input type="text" name="target_table" class="form-control" value="<?php echo htmlspecialchars($template['target_table'] ?? 'datadb'); ?>" readonly required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -110,12 +286,46 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Button Background Color</label>
|
<label class="form-label">Button Background Color</label>
|
||||||
<input type="color" name="button_bg_color" class="form-control" value="<?php echo htmlspecialchars($template['button_bg_color'] ?? '#007bff'); ?>">
|
|
||||||
|
<div class="color-palette">
|
||||||
|
<?php foreach ($buttonBgPalette as $colorValue => $colorLabel): ?>
|
||||||
|
<?php $isChecked = strtolower($colorValue) === $currentButtonBgColor; ?>
|
||||||
|
|
||||||
|
<label class="color-option" title="<?= htmlspecialchars($colorLabel, ENT_QUOTES, 'UTF-8'); ?>">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="button_bg_color"
|
||||||
|
value="<?= htmlspecialchars($colorValue, ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
|
<?= $isChecked ? 'checked' : ''; ?>>
|
||||||
|
<span class="color-swatch" style="background-color: <?= htmlspecialchars($colorValue, ENT_QUOTES, 'UTF-8'); ?>;">
|
||||||
|
<span class="check-mark">✓</span>
|
||||||
|
</span>
|
||||||
|
<span class="color-name"><?= htmlspecialchars($colorLabel, ENT_QUOTES, 'UTF-8'); ?></span>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Button Text Color</label>
|
<label class="form-label">Button Text Color</label>
|
||||||
<input type="color" name="button_text_color" class="form-control" value="<?php echo htmlspecialchars($template['button_text_color'] ?? '#ffffff'); ?>">
|
|
||||||
|
<div class="color-palette">
|
||||||
|
<?php foreach ($buttonTextPalette as $colorValue => $colorLabel): ?>
|
||||||
|
<?php $isChecked = strtolower($colorValue) === $currentButtonTextColor; ?>
|
||||||
|
|
||||||
|
<label class="color-option" title="<?= htmlspecialchars($colorLabel, ENT_QUOTES, 'UTF-8'); ?>">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="button_text_color"
|
||||||
|
value="<?= htmlspecialchars($colorValue, ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
|
<?= $isChecked ? 'checked' : ''; ?>>
|
||||||
|
<span class="color-swatch" style="background-color: <?= htmlspecialchars($colorValue, ENT_QUOTES, 'UTF-8'); ?>;">
|
||||||
|
<span class="check-mark">✓</span>
|
||||||
|
</span>
|
||||||
|
<span class="color-name"><?= htmlspecialchars($colorLabel, ENT_QUOTES, 'UTF-8'); ?></span>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -128,7 +338,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
<select name="client_id" id="clientSelect" class="form-control" required>
|
<select name="client_id" id="clientSelect" class="form-control" required>
|
||||||
<option value="">Select a client...</option>
|
<option value="">Select a client...</option>
|
||||||
</select>
|
</select>
|
||||||
<span id="clientLoadingStatus" class="text-muted" style="margin-left: 10px; display: none;">Recupero clienti in corso...</span>
|
<span id="clientLoadingStatus" class="text-muted" style="margin-left: 10px; display: none;">Loading clients...</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -136,7 +346,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
<select name="schema_id" id="schemaSelect" class="form-control" required>
|
<select name="schema_id" id="schemaSelect" class="form-control" required>
|
||||||
<option value="">Select a schema...</option>
|
<option value="">Select a schema...</option>
|
||||||
</select>
|
</select>
|
||||||
<span id="schemaLoadingStatus" class="text-muted" style="margin-left: 10px; display: none;">Caricamento schemi in corso...</span>
|
<span id="schemaLoadingStatus" class="text-muted" style="margin-left: 10px; display: none;">Loading schemas...</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -144,11 +354,12 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
<select name="idroutine" id="routineSelect" class="form-control">
|
<select name="idroutine" id="routineSelect" class="form-control">
|
||||||
<option value="">Select a routine...</option>
|
<option value="">Select a routine...</option>
|
||||||
<?php foreach ($routines as $routine): ?>
|
<?php foreach ($routines as $routine): ?>
|
||||||
<option value="<?php echo $routine['idroutine']; ?>" <?php echo ($template['idroutine'] ?? '') == $routine['idroutine'] ? 'selected' : ''; ?>>
|
<option value="<?php echo $routine['idroutine']; ?>" <?php echo (($template['idroutine'] ?? '') == $routine['idroutine']) ? 'selected' : ''; ?>>
|
||||||
<?php echo htmlspecialchars($routine['name']); ?>
|
<?php echo htmlspecialchars($routine['name']); ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div id="routineDetails" class="mt-2" style="display: none;">
|
<div id="routineDetails" class="mt-2" style="display: none;">
|
||||||
<h6>Routine Details</h6>
|
<h6>Routine Details</h6>
|
||||||
<p><strong>Name:</strong> <span id="routineName"></span></p>
|
<p><strong>Name:</strong> <span id="routineName"></span></p>
|
||||||
@@ -166,8 +377,10 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overlay toggle-icon"></div>
|
<div class="overlay toggle-icon"></div>
|
||||||
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
||||||
<?php include('include/footer.php'); ?>
|
<?php include('include/footer.php'); ?>
|
||||||
@@ -175,9 +388,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
// Verifica che jQuery sia caricato
|
|
||||||
if (typeof jQuery === 'undefined') {
|
if (typeof jQuery === 'undefined') {
|
||||||
alert("Errore: jQuery non è caricato. Contatta l'amministratore.");
|
alert("Error: jQuery is not loaded.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,12 +404,21 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
const routineAction2 = document.getElementById("routineAction2");
|
const routineAction2 = document.getElementById("routineAction2");
|
||||||
const routineAction3 = document.getElementById("routineAction3");
|
const routineAction3 = document.getElementById("routineAction3");
|
||||||
|
|
||||||
if (!form || !clientLoadingStatus || !schemaLoadingStatus || !routineSelect || !routineDetails) {
|
const sourceType = document.getElementById("sourceType");
|
||||||
alert("Errore: Uno o più elementi della pagina non sono stati trovati. Contatta l'amministratore.");
|
|
||||||
return;
|
const headerRowWrapper = document.getElementById("headerRowWrapper");
|
||||||
}
|
const startColumnWrapper = document.getElementById("startColumnWrapper");
|
||||||
|
const xlsSheetNumberWrapper = document.getElementById("xlsSheetNumberWrapper");
|
||||||
|
const apiConfigWrapper = document.getElementById("apiConfigWrapper");
|
||||||
|
|
||||||
|
const headerRow = document.getElementById("headerRow");
|
||||||
|
const startColumn = document.getElementById("startColumn");
|
||||||
|
const xlsSheetIndex = document.getElementById("xlsSheetIndex");
|
||||||
|
const apiConfigSelect = document.getElementById("apiConfigSelect");
|
||||||
|
|
||||||
|
const selectedClientId = <?php echo json_encode((int)($template['idclient'] ?? 0)); ?>;
|
||||||
|
const selectedSchemaId = <?php echo json_encode((int)($template['idschema'] ?? 0)); ?>;
|
||||||
|
|
||||||
// Inizializza Select2
|
|
||||||
$('#clientSelect').select2({
|
$('#clientSelect').select2({
|
||||||
placeholder: "Search for a client...",
|
placeholder: "Search for a client...",
|
||||||
allowClear: true
|
allowClear: true
|
||||||
@@ -213,108 +434,201 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
allowClear: true
|
allowClear: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Carica i clienti
|
$('#apiConfigSelect').select2({
|
||||||
|
placeholder: "Select an API configuration...",
|
||||||
|
allowClear: true
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateSourceFields() {
|
||||||
|
const selectedSource = sourceType.value;
|
||||||
|
|
||||||
|
const isXls = selectedSource === 'XLS';
|
||||||
|
const isApiOrJson = selectedSource === 'API' || selectedSource === 'JSON';
|
||||||
|
|
||||||
|
if (isXls) {
|
||||||
|
headerRowWrapper.style.display = 'block';
|
||||||
|
startColumnWrapper.style.display = 'block';
|
||||||
|
xlsSheetNumberWrapper.style.display = 'block';
|
||||||
|
|
||||||
|
headerRow.required = true;
|
||||||
|
startColumn.required = true;
|
||||||
|
|
||||||
|
headerRow.disabled = false;
|
||||||
|
startColumn.disabled = false;
|
||||||
|
xlsSheetIndex.disabled = false;
|
||||||
|
|
||||||
|
apiConfigWrapper.style.display = 'none';
|
||||||
|
apiConfigSelect.required = false;
|
||||||
|
apiConfigSelect.disabled = true;
|
||||||
|
$('#apiConfigSelect').val(null).trigger('change');
|
||||||
|
} else {
|
||||||
|
headerRowWrapper.style.display = 'none';
|
||||||
|
startColumnWrapper.style.display = 'none';
|
||||||
|
xlsSheetNumberWrapper.style.display = 'none';
|
||||||
|
|
||||||
|
headerRow.required = false;
|
||||||
|
startColumn.required = false;
|
||||||
|
|
||||||
|
headerRow.disabled = true;
|
||||||
|
startColumn.disabled = true;
|
||||||
|
xlsSheetIndex.disabled = true;
|
||||||
|
|
||||||
|
if (isApiOrJson) {
|
||||||
|
apiConfigWrapper.style.display = 'block';
|
||||||
|
apiConfigSelect.required = true;
|
||||||
|
apiConfigSelect.disabled = false;
|
||||||
|
} else {
|
||||||
|
apiConfigWrapper.style.display = 'none';
|
||||||
|
apiConfigSelect.required = false;
|
||||||
|
apiConfigSelect.disabled = true;
|
||||||
|
$('#apiConfigSelect').val(null).trigger('change');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceType.addEventListener('change', updateSourceFields);
|
||||||
|
updateSourceFields();
|
||||||
|
|
||||||
|
function formatClientLabel(client) {
|
||||||
|
const nome = client.Nominativo || "Name not available";
|
||||||
|
const id = client.IdCliente || "";
|
||||||
|
const codiceCliente = (client.CodiceCliente ?? client.codiceCliente ?? "").toString().trim();
|
||||||
|
const suffix = (codiceCliente.split("_")[1] || "").trim();
|
||||||
|
const shortCode = suffix || (codiceCliente ? codiceCliente.charAt(0) : "--");
|
||||||
|
|
||||||
|
return `${nome.trim()} - ${shortCode} (ID: ${id})`;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadClients() {
|
async function loadClients() {
|
||||||
try {
|
try {
|
||||||
clientLoadingStatus.style.display = 'inline';
|
clientLoadingStatus.style.display = 'inline';
|
||||||
clientLoadingStatus.textContent = 'Recupero clienti in corso...';
|
clientLoadingStatus.textContent = 'Loading clients...';
|
||||||
|
|
||||||
const response = await fetch("get_clienti.php", {
|
const response = await fetch("get_clienti.php", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (!response.ok) throw new Error(data.error || `Errore HTTP: ${response.status}`);
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || `HTTP error: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
const select = document.getElementById("clientSelect");
|
const select = document.getElementById("clientSelect");
|
||||||
select.innerHTML = '<option value="">Select a client...</option>';
|
select.innerHTML = '<option value="">Select a client...</option>';
|
||||||
|
|
||||||
data.value.forEach(client => {
|
data.value.forEach(client => {
|
||||||
const nome = client.Nominativo || "Nome non disponibile";
|
const id = client.IdCliente || "";
|
||||||
const id = client.IdCliente || "ID non disponibile";
|
const option = new Option(formatClientLabel(client), id);
|
||||||
const option = new Option(`${nome.trim()} (ID: ${id})`, id);
|
|
||||||
if (parseInt(id) === parseInt(<?php echo json_encode($template['idclient'] ?? 0); ?>)) {
|
if (parseInt(id) === parseInt(selectedClientId)) {
|
||||||
option.selected = true;
|
option.selected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
select.add(option);
|
select.add(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
$(select).trigger('change');
|
$(select).trigger('change');
|
||||||
clientLoadingStatus.textContent = "Clienti caricati.";
|
clientLoadingStatus.textContent = "Clients loaded.";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
clientLoadingStatus.textContent = "Errore nel caricamento.";
|
clientLoadingStatus.textContent = "Loading error.";
|
||||||
|
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: "Errore!",
|
title: "Error!",
|
||||||
text: "Impossibile caricare i clienti: " + error.message,
|
text: "Unable to load clients: " + error.message,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
confirmButtonText: "OK"
|
confirmButtonText: "OK"
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setTimeout(() => clientLoadingStatus.style.display = 'none', 2000);
|
setTimeout(() => clientLoadingStatus.style.display = 'none', 1500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Carica gli schemi
|
|
||||||
async function loadSchemas() {
|
async function loadSchemas() {
|
||||||
try {
|
try {
|
||||||
schemaLoadingStatus.style.display = 'inline';
|
schemaLoadingStatus.style.display = 'inline';
|
||||||
schemaLoadingStatus.textContent = 'Caricamento schemi in corso...';
|
schemaLoadingStatus.textContent = 'Loading schemas...';
|
||||||
|
|
||||||
const response = await fetch("get_schemi.php", {
|
const response = await fetch("get_schemi.php", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (!response.ok) throw new Error(data.error || `Errore HTTP: ${response.status}`);
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || `HTTP error: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
const select = document.getElementById("schemaSelect");
|
const select = document.getElementById("schemaSelect");
|
||||||
select.innerHTML = '<option value="">Select a schema...</option>';
|
select.innerHTML = '<option value="">Select a schema...</option>';
|
||||||
data.value.forEach(schema => {
|
|
||||||
const nome = schema.Nome || "Nome non disponibile";
|
const sortedSchemas = [...data.value].sort((a, b) => {
|
||||||
const id = schema.IdSchemaCustomFields || "ID non disponibile";
|
const nomeA = (a.Nome || "").trim().toLowerCase();
|
||||||
|
const nomeB = (b.Nome || "").trim().toLowerCase();
|
||||||
|
return nomeA.localeCompare(nomeB, 'it', {
|
||||||
|
sensitivity: 'base'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
sortedSchemas.forEach(schema => {
|
||||||
|
const nome = schema.Nome || "Name not available";
|
||||||
|
const id = schema.IdSchemaCustomFields || "";
|
||||||
const option = new Option(`${nome.trim()} (ID: ${id})`, id);
|
const option = new Option(`${nome.trim()} (ID: ${id})`, id);
|
||||||
if (parseInt(id) === parseInt(<?php echo json_encode($template['idschema'] ?? 0); ?>)) {
|
|
||||||
|
if (parseInt(id) === parseInt(selectedSchemaId)) {
|
||||||
option.selected = true;
|
option.selected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
select.add(option);
|
select.add(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
$(select).trigger('change');
|
$(select).trigger('change');
|
||||||
schemaLoadingStatus.textContent = "Schemi caricati.";
|
schemaLoadingStatus.textContent = "Schemas loaded.";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
schemaLoadingStatus.textContent = "Errore nel caricamento.";
|
schemaLoadingStatus.textContent = "Loading error.";
|
||||||
|
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: "Errore!",
|
title: "Error!",
|
||||||
text: "Impossibile caricare gli schemi: " + error.message,
|
text: "Unable to load schemas: " + error.message,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
confirmButtonText: "OK"
|
confirmButtonText: "OK"
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setTimeout(() => schemaLoadingStatus.style.display = 'none', 2000);
|
setTimeout(() => schemaLoadingStatus.style.display = 'none', 1500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Carica i dati
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
try {
|
try {
|
||||||
await loadClients();
|
await loadClients();
|
||||||
await loadSchemas();
|
await loadSchemas();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: "Errore!",
|
title: "Error!",
|
||||||
text: "Errore nel caricamento dei dati: " + error.message,
|
text: "Error while loading data: " + error.message,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
confirmButtonText: "OK"
|
confirmButtonText: "OK"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadData();
|
loadData();
|
||||||
|
|
||||||
// Routine dettagli
|
|
||||||
const routines = <?php echo json_encode($routines); ?>;
|
const routines = <?php echo json_encode($routines); ?>;
|
||||||
|
|
||||||
function updateRoutineDetails() {
|
function updateRoutineDetails() {
|
||||||
const selectedId = routineSelect.value;
|
const selectedId = routineSelect.value;
|
||||||
routineDetails.style.display = selectedId ? 'block' : 'none';
|
routineDetails.style.display = selectedId ? 'block' : 'none';
|
||||||
|
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
const routine = routines.find(r => r.idroutine == selectedId);
|
const routine = routines.find(r => r.idroutine == selectedId);
|
||||||
|
|
||||||
if (routine) {
|
if (routine) {
|
||||||
routineName.textContent = routine.name || 'N/A';
|
routineName.textContent = routine.name || 'N/A';
|
||||||
routineDescription.textContent = routine.description || 'N/A';
|
routineDescription.textContent = routine.description || 'N/A';
|
||||||
@@ -336,10 +650,10 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
routineAction3.textContent = '';
|
routineAction3.textContent = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
routineSelect.addEventListener('change', updateRoutineDetails);
|
|
||||||
updateRoutineDetails(); // Inizializza dettagli se una routine è preselezionata
|
|
||||||
|
|
||||||
// Submit del form
|
routineSelect.addEventListener('change', updateRoutineDetails);
|
||||||
|
updateRoutineDetails();
|
||||||
|
|
||||||
form.addEventListener("submit", function(e) {
|
form.addEventListener("submit", function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -351,8 +665,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
|
|
||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: "Errore!",
|
title: "Error!",
|
||||||
text: "Per favore seleziona un cliente.",
|
text: "Please select a client.",
|
||||||
icon: "error",
|
icon: "error",
|
||||||
confirmButtonText: "OK"
|
confirmButtonText: "OK"
|
||||||
});
|
});
|
||||||
@@ -373,8 +687,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
|
|
||||||
if (!schemaId) {
|
if (!schemaId) {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: "Errore!",
|
title: "Error!",
|
||||||
text: "Per favore seleziona uno schema.",
|
text: "Please select a schema.",
|
||||||
icon: "error",
|
icon: "error",
|
||||||
confirmButtonText: "OK"
|
confirmButtonText: "OK"
|
||||||
});
|
});
|
||||||
@@ -387,13 +701,35 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
const nameMatch = optionText.match(/^(.+?)(?:\s*\(ID:\s*\d+\))?$/);
|
const nameMatch = optionText.match(/^(.+?)(?:\s*\(ID:\s*\d+\))?$/);
|
||||||
schemaName = nameMatch ? nameMatch[1].trim() : optionText;
|
schemaName = nameMatch ? nameMatch[1].trim() : optionText;
|
||||||
}
|
}
|
||||||
|
|
||||||
formData.append("idschema", schemaId);
|
formData.append("idschema", schemaId);
|
||||||
formData.append("schemaname", schemaName);
|
formData.append("schemaname", schemaName);
|
||||||
|
|
||||||
// Aggiungi idroutine
|
|
||||||
const routineId = routineSelect.value;
|
const routineId = routineSelect.value;
|
||||||
formData.append("idroutine", routineId);
|
formData.append("idroutine", routineId);
|
||||||
|
|
||||||
|
const selectedSource = sourceType.value;
|
||||||
|
|
||||||
|
if ((selectedSource === 'API' || selectedSource === 'JSON') && !apiConfigSelect.value) {
|
||||||
|
Swal.fire({
|
||||||
|
title: "Error!",
|
||||||
|
text: "Please select an API/JSON configuration.",
|
||||||
|
icon: "error",
|
||||||
|
confirmButtonText: "OK"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedSource === 'XLS' && xlsSheetIndex.value === '') {
|
||||||
|
Swal.fire({
|
||||||
|
title: "Error!",
|
||||||
|
text: "Please enter the XLS sheet number.",
|
||||||
|
icon: "error",
|
||||||
|
confirmButtonText: "OK"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
fetch("process_edit_template_xls.php", {
|
fetch("process_edit_template_xls.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData
|
body: formData
|
||||||
@@ -402,8 +738,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: "Successo!",
|
title: "Success!",
|
||||||
text: "Template aggiornato con successo!",
|
text: "Template updated successfully!",
|
||||||
icon: "success",
|
icon: "success",
|
||||||
confirmButtonText: "OK"
|
confirmButtonText: "OK"
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
@@ -411,17 +747,17 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: "Errore!",
|
title: "Error!",
|
||||||
text: data.message,
|
text: data.message,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
confirmButtonText: "OK"
|
confirmButtonText: "OK"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(() => {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: "Errore!",
|
title: "Error!",
|
||||||
text: "Si è verificato un errore imprevisto.",
|
text: "An unexpected error occurred.",
|
||||||
icon: "error",
|
icon: "error",
|
||||||
confirmButtonText: "OK"
|
confirmButtonText: "OK"
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
2025-06-10 12:21:44 - Errore nel recupero dati: HTTP 404, Risposta:
|
|
||||||
2025-06-16 11:15:30 - Errore nel recupero dati: HTTP 404, Risposta: {"title":"Not Found","status":404,"detail":"Not Found","instance":"GET /api/odata/Rapporto(2523026)","errorCode":"d25cbd678"}
|
|
||||||
2025-06-16 11:34:59 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to 93.43.5.102 port 443 after 21033 ms: Couldn't connect to server, Risposta:
|
|
||||||
2025-06-16 11:42:03 - Errore nella richiesta: Recv failure: Connection was reset
|
|
||||||
2025-07-04 10:42:49 - Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
|
|
||||||
2025-07-04 10:44:13 - Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
|
|
||||||
2025-07-04 10:48:07 - Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
|
|
||||||
2025-08-19 16:29:25 - Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
|
|
||||||
2025-08-26 16:47:19 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
|
|
||||||
2025-08-26 16:48:15 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
|
|
||||||
2025-08-26 16:48:44 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
|
|
||||||
2025-08-26 16:49:24 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
|
|
||||||
2025-08-26 16:50:23 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
|
|
||||||
2025-09-08 08:39:17 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
|
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
})();
|
||||||
+774
-213
File diff suppressed because it is too large
Load Diff
@@ -13,9 +13,72 @@ if (!is_dir($logDir)) {
|
|||||||
mkdir($logDir, 0755, true);
|
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
|
// 🔹 Base URL API
|
||||||
$apiBaseUrl = 'https://93.43.5.102/limsapi/api/odata/';
|
$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
|
// 🔹 Funzione per validare e convertire date
|
||||||
function validateDate($value)
|
function validateDate($value)
|
||||||
{
|
{
|
||||||
@@ -27,15 +90,83 @@ function validateDate($value)
|
|||||||
return null; // Imposta null se non è una data valida
|
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 {
|
try {
|
||||||
$iddatadb = $_POST['iddatadb'] ?? null;
|
$iddatadb = $_POST['iddatadb'] ?? null;
|
||||||
if (!$iddatadb) {
|
if (!$iddatadb) {
|
||||||
throw new Exception("Missing 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
|
// 🔹 STEP 1+2: Fetch Cliente ID from datadb and Schema ID from excel_templates
|
||||||
|
// Also fetch fixed fields stored in datadb
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
SELECT d.idclient AS clienteId, et.idschema AS schemaId
|
SELECT d.idclient AS clienteId, et.idschema AS schemaId, et.id AS templateId,
|
||||||
|
et.template_note,
|
||||||
|
d.cliente_responsabile_id,
|
||||||
|
d.moltiplicatore_prezzo_id,
|
||||||
|
d.anagrafica_certest_object_id,
|
||||||
|
d.anagrafica_certest_service_id,
|
||||||
|
d.cliente_fornitore_id,
|
||||||
|
d.clienteAnalisi,
|
||||||
|
d.ClienteFatturazione,
|
||||||
|
d.consegna_richiesta
|
||||||
FROM datadb as d
|
FROM datadb as d
|
||||||
INNER JOIN excel_templates as et ON d.templateid = et.id
|
INNER JOIN excel_templates as et ON d.templateid = et.id
|
||||||
WHERE d.iddatadb = :iddatadb
|
WHERE d.iddatadb = :iddatadb
|
||||||
@@ -50,23 +181,73 @@ try {
|
|||||||
|
|
||||||
$clienteId = (int) $result['clienteId'];
|
$clienteId = (int) $result['clienteId'];
|
||||||
$schemaId = (int) $result['schemaId'];
|
$schemaId = (int) $result['schemaId'];
|
||||||
|
$templateId = (int) $result['templateId'];
|
||||||
|
|
||||||
// 🔹 STEP 3: Fetch Parts (including idmatrice)
|
// 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("
|
$stmt = $pdo->prepare("
|
||||||
SELECT part_number, part_description, material, color, mix, idmatrice
|
SELECT id AS part_id, part_number, part_description, material, color, mix, idmatrice, dateexpiry, note
|
||||||
FROM identification_parts
|
FROM identification_parts
|
||||||
WHERE iddatadb = :iddatadb
|
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]);
|
$stmt->execute(['iddatadb' => $iddatadb]);
|
||||||
$parts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$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
|
// 🔹 STEP 4: Fetch Field Values with Labels
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
SELECT
|
SELECT
|
||||||
idd.field_value,
|
idd.field_value,
|
||||||
m.field_label,
|
m.field_label,
|
||||||
m.schema_id,
|
m.schema_id,
|
||||||
m.field_id
|
m.field_id,
|
||||||
|
m.field_order,
|
||||||
|
m.is_addnote
|
||||||
FROM
|
FROM
|
||||||
import_data_details as idd
|
import_data_details as idd
|
||||||
JOIN template_mapping m ON idd.mapping_id = m.id
|
JOIN template_mapping m ON idd.mapping_id = m.id
|
||||||
@@ -86,6 +267,44 @@ try {
|
|||||||
$valueMap[(int) $row['field_id']] = $row['field_value'];
|
$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
|
// Logga i fieldValues in error_log
|
||||||
$logFieldValues = "FieldValues dal DB (iddatadb={$iddatadb}):\n" . json_encode($fieldValues, JSON_PRETTY_PRINT);
|
$logFieldValues = "FieldValues dal DB (iddatadb={$iddatadb}):\n" . json_encode($fieldValues, JSON_PRETTY_PRINT);
|
||||||
error_log($logFieldValues);
|
error_log($logFieldValues);
|
||||||
@@ -93,12 +312,22 @@ try {
|
|||||||
// 🔹 Initialize API client
|
// 🔹 Initialize API client
|
||||||
$api = VisualLimsApiClient::getInstance();
|
$api = VisualLimsApiClient::getInstance();
|
||||||
|
|
||||||
// 🔹 STEP 5: Create CommessaWeb (NOT WebOrder)
|
// 🔹 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 = [
|
$commessaWebPayload = [
|
||||||
"Cliente" => $clienteId,
|
"Cliente" => $clienteId,
|
||||||
"SchemaCustomField" => $schemaId,
|
"SchemaCustomField" => $schemaId,
|
||||||
"Richiedente" => "Test Web Import",
|
"Richiedente" => "From TRFSmart Application", // TODO: replace with real value
|
||||||
"Descrizione" => "TEST CommessaWeb",
|
"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
|
// Costruisci log curl-like per STEP 5
|
||||||
@@ -114,7 +343,7 @@ try {
|
|||||||
|
|
||||||
// Salva log
|
// Salva log
|
||||||
$logFileStep5 = $logDir . "commessa_create_step5_" . $iddatadb . "_" . time() . ".txt";
|
$logFileStep5 = $logDir . "commessa_create_step5_" . $iddatadb . "_" . time() . ".txt";
|
||||||
file_put_contents($logFileStep5, $logContentStep5);
|
$writeLog($logFileStep5, $logContentStep5, "STEP 5 - Create CommessaWeb (iddatadb={$iddatadb})");
|
||||||
|
|
||||||
$commessaId = $commessaWeb["IdCommessa"];
|
$commessaId = $commessaWeb["IdCommessa"];
|
||||||
$commessaWebCode = substr($commessaWeb["CodiceCommessa"] ?? "TEST CommessaWeb", 0, 30); // Limite a 30 caratteri
|
$commessaWebCode = substr($commessaWeb["CodiceCommessa"] ?? "TEST CommessaWeb", 0, 30); // Limite a 30 caratteri
|
||||||
@@ -130,12 +359,34 @@ try {
|
|||||||
throw new Exception("Invalid or missing idmatrice for part: " . ($part["part_number"] ?? "Unknown"));
|
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 = [
|
$campionePayload = [
|
||||||
"Commessa" => $commessaId,
|
"Commessa" => $commessaId,
|
||||||
"Matrice" => $matriceId,
|
"Matrice" => $matriceId,
|
||||||
"SottoMatrice" => null,
|
"SottoMatrice" => null,
|
||||||
"SchemaCustomField" => $schemaId,
|
"SchemaCustomField" => $schemaId,
|
||||||
"NoteWeb" => $part["part_description"] ?? ""
|
// "Riferimento" => $part["part_description"] ?? "",
|
||||||
|
"NoteWeb" => $noteWebValue,
|
||||||
|
"ConsegnaRichiesta" => !empty($part["dateexpiry"]) ? $part["dateexpiry"] : $consegnaRichiesta,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Costruisci curl-like per questo campione
|
// Costruisci curl-like per questo campione
|
||||||
@@ -160,20 +411,266 @@ try {
|
|||||||
|
|
||||||
// Salva log per STEP 6
|
// Salva log per STEP 6
|
||||||
$logFileStep6 = $logDir . "commessa_{$commessaId}_campioni_step6_" . time() . ".txt";
|
$logFileStep6 = $logDir . "commessa_{$commessaId}_campioni_step6_" . time() . ".txt";
|
||||||
file_put_contents($logFileStep6, $logContentStep6);
|
$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
|
// 🔹 STEP 7: Update Custom Fields for CommessaWeb
|
||||||
if (!empty($fieldValues)) {
|
if (!empty($fieldValues)) {
|
||||||
// GET con espansione per CustomField
|
// GET con espansione per CustomField
|
||||||
$expand = "CommesseCustomFields(\$expand=CustomField)";
|
$expand = "CommesseCustomFields(\$expand=CustomField)";
|
||||||
$commessaWithFields = $api->get("CommessaWeb(" . $commessaId . ")?\$expand=" . $expand);
|
$commessaWithFields = $api->get("CommessaWeb({$commessaId})?\$expand={$expand}");
|
||||||
|
|
||||||
// Logga il GET
|
// Logga il GET
|
||||||
$logContentGet = "curl --location --request GET '{$apiBaseUrl}CommessaWeb({$commessaId})?\$expand=CommesseCustomFields(\$expand=CustomField)' \\\n" .
|
$logContentGet = "curl --location --request GET '{$apiBaseUrl}CommessaWeb({$commessaId})?\$expand=CommesseCustomFields(\$expand=CustomField)' \\\n" .
|
||||||
"--header 'Authorization: Bearer ••••••'\n\n" .
|
"--header 'Authorization: Bearer ••••••'\n\n" .
|
||||||
"RESPONSE:\n" . json_encode($commessaWithFields, JSON_PRETTY_PRINT);
|
"RESPONSE:\n" . json_encode($commessaWithFields, JSON_PRETTY_PRINT);
|
||||||
$logFileGet = $logDir . "commessa_{$commessaId}_get_step7_" . time() . ".txt";
|
$logFileGet = $logDir . "commessa_{$commessaId}_get_step7_" . time() . ".txt";
|
||||||
file_put_contents($logFileGet, $logContentGet);
|
$writeLog($logFileGet, $logContentGet, "STEP 7 - GET CustomFields (commessa={$commessaId})");
|
||||||
|
|
||||||
// Prepara payload PATCH
|
// Prepara payload PATCH
|
||||||
$commessaCustomFields = [];
|
$commessaCustomFields = [];
|
||||||
@@ -186,7 +683,7 @@ try {
|
|||||||
|
|
||||||
// Valida se il campo è di tipo Data
|
// Valida se il campo è di tipo Data
|
||||||
if ($fieldType === 'Data' && $newValue !== $currentValue) {
|
if ($fieldType === 'Data' && $newValue !== $currentValue) {
|
||||||
$newValue = validateDate($newValue);
|
$newValue = formatDateToExport($newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
$commessaCustomFields[] = [
|
$commessaCustomFields[] = [
|
||||||
@@ -195,6 +692,7 @@ try {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$logFileStep7 = null;
|
||||||
if (!empty($commessaCustomFields)) {
|
if (!empty($commessaCustomFields)) {
|
||||||
$updatePayload = ["CommesseCustomFields" => $commessaCustomFields];
|
$updatePayload = ["CommesseCustomFields" => $commessaCustomFields];
|
||||||
|
|
||||||
@@ -209,35 +707,66 @@ try {
|
|||||||
|
|
||||||
$logContentStep7 .= "\n\nRESPONSE:\n" . json_encode($patchResponse, JSON_PRETTY_PRINT);
|
$logContentStep7 .= "\n\nRESPONSE:\n" . json_encode($patchResponse, JSON_PRETTY_PRINT);
|
||||||
$logFileStep7 = $logDir . "commessa_{$commessaId}_update_step7_" . time() . ".txt";
|
$logFileStep7 = $logDir . "commessa_{$commessaId}_update_step7_" . time() . ".txt";
|
||||||
file_put_contents($logFileStep7, $logContentStep7);
|
$writeLog($logFileStep7, $logContentStep7, "STEP 7 - PATCH CustomFields (commessa={$commessaId})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔹 STEP 8: Update datadb with idcommessaweb, commessaweb, and status
|
// 🔹 STEP 8: Update datadb with idcommessaweb, commessaweb, log_file and export date
|
||||||
|
// NB: lo status 'l' viene impostato SOLO dopo invio+import ok (vedi più sotto),
|
||||||
|
// così una riga rimasta indietro non risulta "To LIMS" per errore.
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
UPDATE datadb
|
UPDATE datadb
|
||||||
SET idcommessaweb = :idcommessaweb, commessaweb = :commessaweb, status = 'l'
|
SET idcommessaweb = :idcommessaweb, commessaweb = :commessaweb,
|
||||||
|
log_file = :log_file, export_date = NOW()
|
||||||
WHERE iddatadb = :iddatadb
|
WHERE iddatadb = :iddatadb
|
||||||
");
|
");
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
'idcommessaweb' => $commessaId,
|
'idcommessaweb' => $commessaId,
|
||||||
'commessaweb' => $commessaWebCode,
|
'commessaweb' => $commessaWebCode,
|
||||||
'iddatadb' => $iddatadb
|
'log_file' => basename($commessaLogFile),
|
||||||
|
'iddatadb' => $iddatadb
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 🔹 STEP 9: Send CommessaWeb to laboratory (commentato come richiesto)
|
// 🔹 STEP 9: Send CommessaWeb to laboratory (commentato come richiesto)
|
||||||
/*
|
|
||||||
$sendResult = $api->post("CommessaWeb({$commessaId})/InviaCommessa", []);
|
$sendResult = $api->post("CommessaWeb({$commessaId})/InviaCommessa", []);
|
||||||
|
|
||||||
// Logga il POST
|
// Logga il POST
|
||||||
$logContentStep9 = "curl --location --request POST '{$apiBaseUrl}CommessaWeb({$commessaId})/InviaCommessa' \\\n" .
|
$logContentStep9 = "curl --location --request POST '{$apiBaseUrl}CommessaWeb({$commessaId})/InviaCommessa' \\\n" .
|
||||||
"--header 'Content-Type: application/json' \\\n" .
|
"--header 'Content-Type: application/json' \\\n" .
|
||||||
"--header 'Authorization: Bearer ••••••' \\\n" .
|
"--header 'Authorization: Bearer ••••••' \\\n" .
|
||||||
"--data '{}'\n\n" .
|
"--data '{}'\n\n" .
|
||||||
"RESPONSE:\n" . json_encode($sendResult, JSON_PRETTY_PRINT);
|
"RESPONSE:\n" . json_encode($sendResult, JSON_PRETTY_PRINT);
|
||||||
$logFileStep9 = $logDir . "commessa_{$commessaId}_send_step9_" . time() . ".txt";
|
$logFileStep9 = $logDir . "commessa_{$commessaId}_send_step9_" . time() . ".txt";
|
||||||
file_put_contents($logFileStep9, $logContentStep9);
|
$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
|
// 🔹 STEP 10: GET di controllo post-PATCH
|
||||||
$expand = "CommesseCustomFields(\$expand=CustomField)";
|
$expand = "CommesseCustomFields(\$expand=CustomField)";
|
||||||
@@ -248,8 +777,27 @@ try {
|
|||||||
"--header 'Authorization: Bearer ••••••'\n\n" .
|
"--header 'Authorization: Bearer ••••••'\n\n" .
|
||||||
"RESPONSE:\n" . json_encode($commessaAfterPatch, JSON_PRETTY_PRINT);
|
"RESPONSE:\n" . json_encode($commessaAfterPatch, JSON_PRETTY_PRINT);
|
||||||
$logFileStep10 = $logDir . "commessa_{$commessaId}_get_step10_" . time() . ".txt";
|
$logFileStep10 = $logDir . "commessa_{$commessaId}_get_step10_" . time() . ".txt";
|
||||||
file_put_contents($logFileStep10, $logContentStep10);
|
$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
|
// 🔹 STEP 11: Prepare final response
|
||||||
$finalCommessa = [
|
$finalCommessa = [
|
||||||
"Cliente" => $clienteId,
|
"Cliente" => $clienteId,
|
||||||
@@ -262,30 +810,82 @@ try {
|
|||||||
];
|
];
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
"success" => true,
|
"success" => true,
|
||||||
"commessaWeb" => $finalCommessa,
|
"idcommessaweb" => $commessaId,
|
||||||
|
"commessaweb" => $finalCodiceCommessa ?: $commessaWebCode,
|
||||||
|
"commessaWeb" => $finalCommessa,
|
||||||
"commessaWebApiResponse" => $commessaWeb, // Incluso per debug
|
"commessaWebApiResponse" => $commessaWeb, // Incluso per debug
|
||||||
"totalCampioni" => count($campioni),
|
"totalCampioni" => count($campioni),
|
||||||
"totalCustomFields" => count($commessaAfterPatch["CommesseCustomFields"] ?? []),
|
"totalCustomFields" => count($commessaAfterPatch["CommesseCustomFields"] ?? []),
|
||||||
"message" => "Export successful",
|
"totalPhotos" => count($photos),
|
||||||
"logFiles" => [
|
"totalAnalyses" => $totalAnalyses,
|
||||||
"step5_create" => $logFileStep5,
|
"addedAnalyses" => $addedAnalyses,
|
||||||
"step6_campioni" => $logFileStep6,
|
"failedAnalyses" => $failedAnalyses,
|
||||||
"step7_patch" => $logFileStep7,
|
"noPriceCount" => count($noPriceAnalyses),
|
||||||
"step10_get" => $logFileStep10
|
"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) {
|
} 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());
|
error_log("LIMS Export Error: " . $e->getMessage() . "\nTrace: " . $e->getTraceAsString());
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
"success" => false,
|
"success" => false,
|
||||||
"message" => "Export failed: " . $e->getMessage(),
|
"message" => "Export failed: " . $e->getMessage(),
|
||||||
|
"errorLog" => $logDir . $errFileToSave,
|
||||||
"logFiles" => [
|
"logFiles" => [
|
||||||
"step5_create" => $logFileStep5 ?? null,
|
"step5_create" => $logFileStep5 ?? null,
|
||||||
"step6_campioni" => $logFileStep6 ?? null,
|
"step5_2_photos" => $logFilePhotos ?? null,
|
||||||
"step7_patch" => $logFileStep7 ?? null,
|
"step6_campioni" => $logFileStep6 ?? null,
|
||||||
"step10_get" => $logFileStep10 ?? 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()]);
|
||||||
|
}
|
||||||
@@ -9,11 +9,28 @@ ini_set('display_errors', '0');
|
|||||||
error_reporting(E_ALL);
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$api = VisualLimsApiClient::getInstance();
|
$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
|
// Parametri OData
|
||||||
$params = [
|
$params = [
|
||||||
'$select' => 'IdCliente,Nominativo,CodiceNazioneFatturazione',
|
'$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo',
|
||||||
'$orderby' => 'Nominativo asc'
|
'$orderby' => 'Nominativo asc'
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -57,10 +74,21 @@ try {
|
|||||||
throw new Exception("Massimo numero di tentativi raggiunto per $endpoint");
|
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
|
// Esegui la chiamata con retry
|
||||||
$data = makeApiRequest($api, $endpoint);
|
$data = makeApiRequest($api, $endpoint);
|
||||||
|
|
||||||
echo json_encode($data);
|
$json = json_encode($data);
|
||||||
|
if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true);
|
||||||
|
file_put_contents($cacheFile, $json);
|
||||||
|
|
||||||
|
echo $json;
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
$errorResponse = [
|
$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);
|
||||||
|
}
|
||||||
@@ -21,17 +21,26 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$results = [];
|
$results = [];
|
||||||
|
$cacheDir = __DIR__ . '/cache';
|
||||||
|
if (!is_dir($cacheDir)) mkdir($cacheDir, 0777, true);
|
||||||
|
|
||||||
foreach ($fieldIds as $customFieldId) {
|
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";
|
$endpoint = "CustomField($customFieldId)?\$expand=CustomFieldsValues";
|
||||||
$data = $api->get($endpoint);
|
$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);
|
echo json_encode($results);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
http_response_code(500);
|
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()]);
|
||||||
|
}
|
||||||
@@ -1,59 +1,46 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php'; // Risale a root/vendor/
|
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||||
use Dotenv\Dotenv;
|
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; }
|
||||||
|
|
||||||
// Set JSON header
|
|
||||||
header('Content-Type: application/json');
|
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 {
|
try {
|
||||||
$dotenv = Dotenv::createImmutable($envPath);
|
// Read from matrici cache (populated by get_matrici.php / warm_cache.php)
|
||||||
$dotenv->load();
|
$cacheFile = __DIR__ . '/cache/matrici.json';
|
||||||
} catch (Exception $e) {
|
if (file_exists($cacheFile)) {
|
||||||
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - Errore caricamento .env: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
|
$data = json_decode(file_get_contents($cacheFile), true);
|
||||||
echo json_encode(['success' => false, 'message' => 'Errore caricamento configurazione: ' . $e->getMessage()]);
|
$matrici = $data['value'] ?? [];
|
||||||
exit(1);
|
} 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]));
|
||||||
|
}
|
||||||
|
|
||||||
// Recupera le variabili d'ambiente
|
// Extract unique MacroMatrice values
|
||||||
$dbHost = $_ENV['DB_HOST'];
|
$macroValues = [];
|
||||||
$dbName = $_ENV['DB_DATABASE'];
|
foreach ($matrici as $m) {
|
||||||
$dbUser = $_ENV['DB_USERNAME'];
|
$macro = $m['MacroMatrice'] ?? null;
|
||||||
$dbPass = $_ENV['DB_PASSWORD'];
|
if ($macro !== null && $macro !== '' && substr($macro, 0, 1) !== '*') {
|
||||||
$dbPrefix = $_ENV['DB_PREFIX'];
|
$macroValues[$macro] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$macroMatrici = array_keys($macroValues);
|
||||||
|
sort($macroMatrici);
|
||||||
|
|
||||||
// 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 i valori distinti di MacroMatrice, escludendo quelli che iniziano con '*' e ordinandoli
|
|
||||||
$query = "SELECT DISTINCT MacroMatrice FROM {$dbPrefix}matrici WHERE MacroMatrice IS NOT NULL AND MacroMatrice NOT LIKE '*%' ORDER BY MacroMatrice ASC";
|
|
||||||
$stmt = $pdo->prepare($query);
|
|
||||||
$stmt->execute();
|
|
||||||
$macroMatrici = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
|
||||||
|
|
||||||
// Debug: Log del numero di MacroMatrice recuperate
|
|
||||||
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . ' - Retrieved ' . count($macroMatrici) . ' MacroMatrice from database' . PHP_EOL, FILE_APPEND);
|
|
||||||
|
|
||||||
// Restituisci risposta JSON
|
|
||||||
echo json_encode(['success' => true, 'value' => $macroMatrici]);
|
echo json_encode(['success' => true, 'value' => $macroMatrici]);
|
||||||
} catch (PDOException $e) {
|
} catch (Exception $e) {
|
||||||
// Log errore e restituisci risposta di errore
|
http_response_code(500);
|
||||||
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - Errore nel recupero delle MacroMatrice: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
echo json_encode(['success' => false, 'message' => 'Errore nel recupero delle MacroMatrice: ' . $e->getMessage()]);
|
|
||||||
exit(1);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,55 @@
|
|||||||
|
<?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 i Moltiplicatori Prezzo
|
||||||
|
// (dal documento: GET api/odata/MoltiplicatorePrezzi)
|
||||||
|
$endpoint = 'MoltiplicatorePrezzi';
|
||||||
|
|
||||||
|
// Opzionale: parametri OData ($top, $filter, $orderby, ecc.)
|
||||||
|
$options = [
|
||||||
|
'$orderby' => 'Descrizione asc'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Debug: salva URL usata
|
||||||
|
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
|
||||||
|
$queryParts = [];
|
||||||
|
foreach ($options as $k => $v) {
|
||||||
|
// mantieni il $ nella chiave, encoda solo il valore
|
||||||
|
$queryParts[] = $k . '=' . rawurlencode($v);
|
||||||
|
}
|
||||||
|
$query = implode('&', $queryParts);
|
||||||
|
$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);
|
||||||
|
// ✅ Force sort locally by "Descrizione" (A→Z)
|
||||||
|
if (isset($data['value']) && is_array($data['value'])) {
|
||||||
|
usort($data['value'], function ($a, $b) {
|
||||||
|
$da = isset($a['Descrizione']) ? trim((string)$a['Descrizione']) : '';
|
||||||
|
$db = isset($b['Descrizione']) ? trim((string)$b['Descrizione']) : '';
|
||||||
|
return strcasecmp($da, $db); // case-insensitive alphabetical
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Salva il JSON in locale
|
||||||
|
file_put_contents(__DIR__ . '/moltiplicatori_prezzo_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,38 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/include/headscript.php'; // o il tuo bootstrap standard
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!isset($_GET['iddatadb']) || !is_numeric($_GET['iddatadb'])) {
|
||||||
|
echo json_encode(['success' => true, 'field' => null]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$iddatadb = (int)$_GET['iddatadb'];
|
||||||
|
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
// 1) prendo templateid da datadb
|
||||||
|
// 2) cerco max 1 record in template_mapping con is_visible_parts=1
|
||||||
|
$sql = "
|
||||||
|
SELECT tm.field_id, tm.field_label, tm.data_type, tm.has_list
|
||||||
|
FROM datadb d
|
||||||
|
JOIN template_mapping tm ON tm.template_id = d.templateid
|
||||||
|
WHERE d.iddatadb = ?
|
||||||
|
AND tm.is_visible_parts = 1
|
||||||
|
ORDER BY tm.id ASC
|
||||||
|
LIMIT 1
|
||||||
|
";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([$iddatadb]);
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'field' => $row ? $row : null
|
||||||
|
]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
<?php
|
||||||
|
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||||
|
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
ini_set('display_errors', '0');
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uso:
|
||||||
|
* rapporto_full_by_codice.php?codice=2621521
|
||||||
|
* rapporto_full_by_codice.php?codice=2621521&download=1
|
||||||
|
*/
|
||||||
|
|
||||||
|
try {
|
||||||
|
$api = VisualLimsApiClient::getInstance();
|
||||||
|
|
||||||
|
$codiceRapporto = trim($_GET['codice'] ?? '');
|
||||||
|
$downloadPdf = isset($_GET['download']) && $_GET['download'] == '1';
|
||||||
|
|
||||||
|
if ($codiceRapporto === '') {
|
||||||
|
throw new Exception("Parametro codice mancante. Usa ?codice=2621521");
|
||||||
|
}
|
||||||
|
|
||||||
|
$debugDir = __DIR__ . '/logs/lims_rapporti/';
|
||||||
|
$pdfDir = __DIR__ . '/pdf/rapporti/';
|
||||||
|
|
||||||
|
if (!is_dir($debugDir)) mkdir($debugDir, 0755, true);
|
||||||
|
if (!is_dir($pdfDir)) mkdir($pdfDir, 0755, true);
|
||||||
|
|
||||||
|
$codiceRapportoSafe = str_replace("'", "''", $codiceRapporto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* STEP 1 - Search IdRapporto
|
||||||
|
*/
|
||||||
|
$searchParams = [
|
||||||
|
'$filter' => "CodiceRapporto eq '{$codiceRapportoSafe}'",
|
||||||
|
'$select' => 'IdRapporto,CodiceRapporto,Data,Versione,Firmato,DataStampa',
|
||||||
|
'$top' => 1
|
||||||
|
];
|
||||||
|
|
||||||
|
$searchEndpoint = "Rapporto?" . http_build_query($searchParams);
|
||||||
|
$searchData = $api->get($searchEndpoint);
|
||||||
|
|
||||||
|
$rapporti = $searchData['value'] ?? [];
|
||||||
|
|
||||||
|
if (empty($rapporti)) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Nessun rapporto trovato per questo CodiceRapporto.',
|
||||||
|
'codice_rapporto' => $codiceRapporto
|
||||||
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rapportoBase = $rapporti[0];
|
||||||
|
$idRapporto = intval($rapportoBase['IdRapporto'] ?? 0);
|
||||||
|
|
||||||
|
if (!$idRapporto) {
|
||||||
|
throw new Exception("IdRapporto non trovato nella risposta.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* STEP 2 - Dettaglio Rapporto
|
||||||
|
*/
|
||||||
|
$clienteExpandError = null;
|
||||||
|
$detailEndpoint = "Rapporto({$idRapporto})?\$expand=Cliente,RapportiFiles,CampioniDatiRapporto";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$detailData = $api->get($detailEndpoint);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$clienteExpandError = $e->getMessage();
|
||||||
|
$detailEndpoint = "Rapporto({$idRapporto})?\$expand=RapportiFiles,CampioniDatiRapporto";
|
||||||
|
$detailData = $api->get($detailEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents(
|
||||||
|
$debugDir . "rapporto_{$codiceRapportoSafe}_light.json",
|
||||||
|
json_encode($detailData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
|
||||||
|
);
|
||||||
|
|
||||||
|
$cliente = $detailData['Cliente'] ?? null;
|
||||||
|
$rapportiFiles = $detailData['RapportiFiles'] ?? [];
|
||||||
|
$campioniDatiRapporto = $detailData['CampioniDatiRapporto'] ?? [];
|
||||||
|
|
||||||
|
// ====================== CLIENTE ======================
|
||||||
|
$clienteInfo = [
|
||||||
|
'found' => is_array($cliente),
|
||||||
|
'id_cliente' => $cliente['IdCliente'] ?? $cliente['Id'] ?? null,
|
||||||
|
'codice_cliente' => $cliente['CodiceCliente'] ?? $cliente['Codice'] ?? null,
|
||||||
|
'nome_cliente' => $cliente['Nominativo'] ?? $cliente['RagioneSociale'] ?? $cliente['Nome'] ?? $cliente['Descrizione'] ?? null,
|
||||||
|
'partita_iva' => $cliente['PartitaIva'] ?? null,
|
||||||
|
'codice_fiscale' => $cliente['CodiceFiscale'] ?? null
|
||||||
|
];
|
||||||
|
|
||||||
|
// ====================== FILE PDF ======================
|
||||||
|
$selectedRapportoFile = null;
|
||||||
|
$idRapportoFile = null;
|
||||||
|
$pdfFileName = $codiceRapporto . '.pdf';
|
||||||
|
$pdfFullPath = $pdfDir . $pdfFileName;
|
||||||
|
|
||||||
|
if (is_array($rapportiFiles) && count($rapportiFiles) > 0) {
|
||||||
|
foreach ($rapportiFiles as $file) {
|
||||||
|
$fileName = $file['FileName'] ?? '';
|
||||||
|
$tipoRapporto = $file['TipoRapporto'] ?? '';
|
||||||
|
$categoria = $file['Categoria'] ?? '';
|
||||||
|
|
||||||
|
if (
|
||||||
|
stripos($fileName, '.pdf') !== false ||
|
||||||
|
stripos($tipoRapporto, 'Rapporto') !== false ||
|
||||||
|
stripos($categoria, 'Rapporti') !== false
|
||||||
|
) {
|
||||||
|
$selectedRapportoFile = $file;
|
||||||
|
$idRapportoFile = $file['IdRapportoFile'] ?? null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$selectedRapportoFile && count($rapportiFiles) > 0) {
|
||||||
|
$selectedRapportoFile = $rapportiFiles[0];
|
||||||
|
$idRapportoFile = $rapportiFiles[0]['IdRapportoFile'] ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== DOWNLOAD PDF - DEBUG AVANZATO ======================
|
||||||
|
$pdfDownloaded = false;
|
||||||
|
$pdfError = null;
|
||||||
|
$endpointUsato = null;
|
||||||
|
|
||||||
|
if ($downloadPdf && $idRapportoFile) {
|
||||||
|
|
||||||
|
$tentativi = [
|
||||||
|
"RapportoFile({$idRapportoFile})/\$value",
|
||||||
|
"RapportoFile({$idRapportoFile})/Contenuto/\$value",
|
||||||
|
"RapportoFile({$idRapportoFile})/File/\$value",
|
||||||
|
"Rapporto({$idRapporto})/RapportiFiles({$idRapportoFile})/\$value",
|
||||||
|
"Rapporto({$idRapporto})/RapportoFile/\$value",
|
||||||
|
"GetRapportoFile?IdRapportoFile={$idRapportoFile}",
|
||||||
|
"RapportoFile/Download?Id={$idRapportoFile}",
|
||||||
|
"Rapporto/DownloadFile?IdRapporto={$idRapporto}"
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($tentativi as $ep) {
|
||||||
|
try {
|
||||||
|
$pdfContent = $api->getRaw($ep);
|
||||||
|
|
||||||
|
if (strlen($pdfContent) > 2000) { // PDF decente deve essere più grande
|
||||||
|
file_put_contents($pdfFullPath, $pdfContent);
|
||||||
|
$pdfDownloaded = true;
|
||||||
|
$endpointUsato = $ep;
|
||||||
|
$pdfError = "SUCCESSO con: " . $ep;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
$pdfError = "Contenuto troppo piccolo con: " . $ep;
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$pdfError = "Fallito con " . $ep . " → " . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== CAMPIONI + RATING (ripristinato dal tuo originale) ======================
|
||||||
|
$campioniSintesi = [];
|
||||||
|
$ratingFinale = null;
|
||||||
|
$ratingSource = null;
|
||||||
|
$hasIrregolare = false;
|
||||||
|
$hasPositiveResults = false;
|
||||||
|
|
||||||
|
if (is_array($campioniDatiRapporto)) {
|
||||||
|
foreach ($campioniDatiRapporto as $campione) {
|
||||||
|
$giudizioRapporto = $campione['GiudizioRapporto'] ?? null;
|
||||||
|
$giudizioCertificato = $campione['GiudizioCertificato'] ?? null;
|
||||||
|
$esitoGiudizioLMR = $campione['EsitoGiudizioLMR'] ?? null;
|
||||||
|
$esitoGiudizioImpiego = $campione['EsitoGiudizioImpiego'] ?? null;
|
||||||
|
$risultatiIrregolari = ($campione['RisultatiIrregolari'] ?? false) === true;
|
||||||
|
$risultatiPositivi = ($campione['RisultatiPositivi'] ?? false) === true;
|
||||||
|
|
||||||
|
$campioniSintesi[] = [
|
||||||
|
'id_campione_dati_rapporto' => $campione['IdCampioneDatiRapporto'] ?? null,
|
||||||
|
'codice_campione' => $campione['CodiceCampione'] ?? null,
|
||||||
|
'stato_campione' => $campione['StatoCampione'] ?? null,
|
||||||
|
'stato_campione_web' => $campione['StatoCampioneWeb'] ?? null,
|
||||||
|
'matrice' => $campione['Matrice'] ?? null,
|
||||||
|
'macro_matrice' => $campione['MacroMatrice'] ?? null,
|
||||||
|
'giudizio_rapporto' => $giudizioRapporto,
|
||||||
|
'giudizio_certificato' => $giudizioCertificato,
|
||||||
|
'esito_giudizio_lmr' => $esitoGiudizioLMR,
|
||||||
|
'esito_giudizio_impiego' => $esitoGiudizioImpiego,
|
||||||
|
'risultati_positivi' => $risultatiPositivi,
|
||||||
|
'risultati_irregolari' => $risultatiIrregolari
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!$ratingFinale && !empty($giudizioRapporto)) {
|
||||||
|
$ratingFinale = $giudizioRapporto;
|
||||||
|
$ratingSource = 'CampioniDatiRapporto.GiudizioRapporto';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$ratingFinale && !empty($giudizioCertificato)) {
|
||||||
|
$ratingFinale = $giudizioCertificato;
|
||||||
|
$ratingSource = 'CampioniDatiRapporto.GiudizioCertificato';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($risultatiIrregolari) $hasIrregolare = true;
|
||||||
|
if ($risultatiPositivi) $hasPositiveResults = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback rating
|
||||||
|
if (!$ratingFinale) {
|
||||||
|
if ($hasIrregolare) {
|
||||||
|
$ratingFinale = 'Irregolare';
|
||||||
|
$ratingSource = 'RisultatiIrregolari';
|
||||||
|
} elseif ($hasPositiveResults || count($campioniSintesi) > 0) {
|
||||||
|
$ratingFinale = 'Regolare';
|
||||||
|
$ratingSource = 'fallback';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== RISPOSTA FINALE ======================
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
|
||||||
|
'codice_rapporto' => $codiceRapporto,
|
||||||
|
'id_rapporto' => $idRapporto,
|
||||||
|
|
||||||
|
'search_endpoint' => $searchEndpoint,
|
||||||
|
'detail_endpoint' => $detailEndpoint,
|
||||||
|
'cliente_expand_error' => $clienteExpandError,
|
||||||
|
|
||||||
|
'rapporto_base' => $rapportoBase,
|
||||||
|
'cliente' => $clienteInfo,
|
||||||
|
|
||||||
|
'rating_finale' => $ratingFinale,
|
||||||
|
'rating_source' => $ratingSource,
|
||||||
|
|
||||||
|
'rapporti_files_count' => count($rapportiFiles),
|
||||||
|
'selected_rapporto_file' => $selectedRapportoFile,
|
||||||
|
|
||||||
|
'pdf_file_name' => $pdfFileName,
|
||||||
|
'pdf_path' => $pdfDownloaded ? $pdfFullPath : null,
|
||||||
|
'pdf_downloaded' => $pdfDownloaded,
|
||||||
|
'pdf_error' => $pdfError,
|
||||||
|
|
||||||
|
'campioni_count' => count($campioniSintesi),
|
||||||
|
'campioni_sintesi' => $campioniSintesi,
|
||||||
|
|
||||||
|
'download_requested' => $downloadPdf
|
||||||
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||||
|
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
ini_set('display_errors', '0');
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$api = VisualLimsApiClient::getInstance();
|
||||||
|
|
||||||
|
$commessa = trim($_GET['commessa'] ?? '');
|
||||||
|
|
||||||
|
if ($commessa === '') {
|
||||||
|
throw new Exception("Parametro commessa mancante");
|
||||||
|
}
|
||||||
|
|
||||||
|
$debugDir = __DIR__ . '/logs/lims_rapporto/';
|
||||||
|
if (!is_dir($debugDir)) {
|
||||||
|
mkdir($debugDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* STEP 1
|
||||||
|
* Provo a cercare il rapporto con filtri leggeri.
|
||||||
|
* NIENTE expand pesanti.
|
||||||
|
*/
|
||||||
|
$attempts = [];
|
||||||
|
|
||||||
|
$filters = [
|
||||||
|
'CodiceCommessa' => "CodiceCommessa eq '{$commessa}'",
|
||||||
|
'Commessa_CodiceCommessa' => "Commessa/CodiceCommessa eq '{$commessa}'",
|
||||||
|
'Commessa_IdCommessa' => is_numeric($commessa) ? "Commessa/IdCommessa eq {$commessa}" : null,
|
||||||
|
'Codice' => "Codice eq '{$commessa}'"
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($filters as $label => $filter) {
|
||||||
|
if (!$filter) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$options = [
|
||||||
|
'$filter' => $filter,
|
||||||
|
'$top' => 10
|
||||||
|
];
|
||||||
|
|
||||||
|
$data = $api->get('Rapporto', $options);
|
||||||
|
|
||||||
|
$attempts[$label] = [
|
||||||
|
'success' => true,
|
||||||
|
'filter' => $filter,
|
||||||
|
'records' => isset($data['value']) && is_array($data['value']) ? count($data['value']) : null,
|
||||||
|
'data' => $data
|
||||||
|
];
|
||||||
|
|
||||||
|
file_put_contents(
|
||||||
|
$debugDir . "rapporto_{$commessa}_{$label}.json",
|
||||||
|
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
|
||||||
|
);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$attempts[$label] = [
|
||||||
|
'success' => false,
|
||||||
|
'filter' => $filter,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* STEP 2
|
||||||
|
* Prendo solo eventuali rapporti trovati.
|
||||||
|
*/
|
||||||
|
$rapportiFound = [];
|
||||||
|
|
||||||
|
foreach ($attempts as $label => $attempt) {
|
||||||
|
if (!$attempt['success']) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = $attempt['data']['value'] ?? [];
|
||||||
|
|
||||||
|
if (!is_array($items)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$rapportiFound[] = [
|
||||||
|
'matched_by' => $label,
|
||||||
|
'rapporto' => $item
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'input_commessa' => $commessa,
|
||||||
|
'message' => 'Ricerca leggera su Rapporto completata. Se trovi un rapporto, poi recuperiamo RapportiFiles solo per quello.',
|
||||||
|
'rapporti_found_count' => count($rapportiFound),
|
||||||
|
'rapporti_found' => $rapportiFound,
|
||||||
|
'attempts_summary' => array_map(function ($a) {
|
||||||
|
return [
|
||||||
|
'success' => $a['success'],
|
||||||
|
'filter' => $a['filter'],
|
||||||
|
'records' => $a['records'] ?? null,
|
||||||
|
'error' => $a['error'] ?? null
|
||||||
|
];
|
||||||
|
}, $attempts)
|
||||||
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
@@ -2,25 +2,140 @@
|
|||||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||||
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
ini_set('display_errors', '0');
|
ini_set('display_errors', '0');
|
||||||
error_reporting(E_ALL);
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$api = VisualLimsApiClient::getInstance();
|
$api = VisualLimsApiClient::getInstance();
|
||||||
$rapporto_id = 533329;
|
|
||||||
|
|
||||||
// Costruzione manuale dell'endpoint con espansione annidata
|
// Esempi:
|
||||||
$endpoint = "Rapporto($rapporto_id)?\$expand=CampioniDatiRapporto(\$expand=AnalisiDatiRapporto,CustomFieldsDatiRapporto)";
|
// rapporto_by_codice_expand_step.php?codice=2541111&step=base
|
||||||
|
// rapporto_by_codice_expand_step.php?codice=2541111&step=files
|
||||||
|
// rapporto_by_codice_expand_step.php?codice=2541111&step=campioni
|
||||||
|
// rapporto_by_codice_expand_step.php?codice=2541111&step=files_campioni
|
||||||
|
|
||||||
// Non passiamo options, già incluso nell'endpoint
|
$codiceRapporto = trim($_GET['codice'] ?? '');
|
||||||
$data = $api->get($endpoint);
|
$step = trim($_GET['step'] ?? 'base');
|
||||||
|
|
||||||
file_put_contents(__DIR__ . '/rapporto_expanded.json', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
if ($codiceRapporto === '') {
|
||||||
echo json_encode($data);
|
throw new Exception("Parametro codice mancante. Usa ?codice=2541111");
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedSteps = [
|
||||||
|
'base' => '',
|
||||||
|
'files' => 'RapportiFiles',
|
||||||
|
'allegati' => 'RapportiAllegati',
|
||||||
|
'campioni' => 'CampioniDatiRapporto',
|
||||||
|
'files_campioni' => 'RapportiFiles,CampioniDatiRapporto'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!array_key_exists($step, $allowedSteps)) {
|
||||||
|
throw new Exception("Step non valido. Usa: " . implode(', ', array_keys($allowedSteps)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape OData per eventuali apostrofi
|
||||||
|
$codiceRapportoSafe = str_replace("'", "''", $codiceRapporto);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* STEP 1 - Trova IdRapporto partendo da CodiceRapporto.
|
||||||
|
* Query leggera, con $select e $top=1.
|
||||||
|
*/
|
||||||
|
$searchParams = [
|
||||||
|
'$filter' => "CodiceRapporto eq '{$codiceRapportoSafe}'",
|
||||||
|
'$select' => 'IdRapporto,CodiceRapporto,Data,Versione,Firmato,DataStampa',
|
||||||
|
'$top' => 1
|
||||||
|
];
|
||||||
|
|
||||||
|
$searchEndpoint = "Rapporto?" . http_build_query($searchParams);
|
||||||
|
|
||||||
|
$searchData = $api->get($searchEndpoint);
|
||||||
|
|
||||||
|
$items = $searchData['value'] ?? [];
|
||||||
|
|
||||||
|
if (!is_array($items) || count($items) === 0) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Nessun rapporto trovato per questo CodiceRapporto.',
|
||||||
|
'codice_rapporto' => $codiceRapporto,
|
||||||
|
'search_endpoint' => $searchEndpoint,
|
||||||
|
'search_data' => $searchData
|
||||||
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rapportoBase = $items[0];
|
||||||
|
$rapportoId = intval($rapportoBase['IdRapporto'] ?? 0);
|
||||||
|
|
||||||
|
if (!$rapportoId) {
|
||||||
|
throw new Exception("IdRapporto non trovato nella risposta.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* STEP 2 - Se step=base, restituisco solo la ricerca base.
|
||||||
|
*/
|
||||||
|
if ($step === 'base') {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'codice_rapporto' => $codiceRapporto,
|
||||||
|
'id_rapporto' => $rapportoId,
|
||||||
|
'step' => $step,
|
||||||
|
'search_endpoint' => $searchEndpoint,
|
||||||
|
'rapporto_base' => $rapportoBase
|
||||||
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* STEP 3 - Espande SOLO il rapporto trovato.
|
||||||
|
*/
|
||||||
|
$expandValue = $allowedSteps[$step];
|
||||||
|
|
||||||
|
$detailParams = [
|
||||||
|
'$expand' => $expandValue
|
||||||
|
];
|
||||||
|
|
||||||
|
$detailEndpoint = "Rapporto({$rapportoId})?" . http_build_query($detailParams);
|
||||||
|
|
||||||
|
file_put_contents(
|
||||||
|
__DIR__ . '/last_rapporto_by_codice_expand_endpoint.txt',
|
||||||
|
'[' . date('Y-m-d H:i:s') . '] SEARCH: ' . $searchEndpoint . PHP_EOL .
|
||||||
|
'[' . date('Y-m-d H:i:s') . '] DETAIL: ' . $detailEndpoint . PHP_EOL,
|
||||||
|
FILE_APPEND
|
||||||
|
);
|
||||||
|
|
||||||
|
$detailData = $api->get($detailEndpoint);
|
||||||
|
|
||||||
|
file_put_contents(
|
||||||
|
__DIR__ . "/rapporto_codice_{$codiceRapportoSafe}_{$step}.json",
|
||||||
|
json_encode([
|
||||||
|
'search' => $searchData,
|
||||||
|
'detail' => $detailData
|
||||||
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
|
||||||
|
);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'codice_rapporto' => $codiceRapporto,
|
||||||
|
'id_rapporto' => $rapportoId,
|
||||||
|
'step' => $step,
|
||||||
|
'search_endpoint' => $searchEndpoint,
|
||||||
|
'detail_endpoint' => $detailEndpoint,
|
||||||
|
'rapporto_base' => $rapportoBase,
|
||||||
|
'data' => $detailData
|
||||||
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
|
file_put_contents(
|
||||||
|
__DIR__ . '/error_log.txt',
|
||||||
|
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
|
||||||
|
FILE_APPEND
|
||||||
|
);
|
||||||
|
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode(['error' => $e->getMessage()]);
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<?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 users
|
||||||
|
if (($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true') {
|
||||||
|
$fakeUsers = [
|
||||||
|
[
|
||||||
|
'IdUtente' => 1001,
|
||||||
|
'Nominativo' => 'Utente Simulato 1001'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'IdUtente' => 1002,
|
||||||
|
'Nominativo' => 'Utente Simulato 1002'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
echo json_encode(['value' => $fakeUsers]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OData parameters
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
// Build query string
|
||||||
|
$queryString = http_build_query($params);
|
||||||
|
|
||||||
|
// Final endpoint
|
||||||
|
$endpoint = "Utente?$queryString";
|
||||||
|
|
||||||
|
// Function to execute request with 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__ . '/utenti_response.json', json_encode($data, JSON_PRETTY_PRINT));
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$errorMessage = $e->getMessage();
|
||||||
|
|
||||||
|
// Retry only on token/auth-related issue
|
||||||
|
if (
|
||||||
|
strpos($errorMessage, 'HTTP 400') !== false &&
|
||||||
|
strpos($errorMessage, 'Cannot persist the object') !== false
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
$api->refreshToken(); // must exist in VisualLimsApiClient
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
usleep(500000); // 500 ms
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception("Massimo numero di tentativi raggiunto per $endpoint");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute request
|
||||||
|
$data = makeApiRequest($api, $endpoint);
|
||||||
|
|
||||||
|
echo json_encode($data);
|
||||||
|
} 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_utenti.php: " . json_encode($errorResponse));
|
||||||
|
echo json_encode($errorResponse);
|
||||||
|
}
|
||||||
@@ -0,0 +1,844 @@
|
|||||||
|
/**
|
||||||
|
* gridFilter.js — Selezione righe + filtri colonna a scomparsa per imported.php
|
||||||
|
* Dipende da window.gridRenderer (gridRenderer.js). Includere DOPO gridRenderer.js.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
let filtersActive = false;
|
||||||
|
let restricted = false;
|
||||||
|
const selected = new Set(); // iddatadb selezionati
|
||||||
|
const colFilters = {}; // colKey -> testo filtro
|
||||||
|
let restrictedIds = null; // Set di iddatadb visibili quando ristretto
|
||||||
|
|
||||||
|
function R() {
|
||||||
|
return window.gridRenderer;
|
||||||
|
}
|
||||||
|
function data() {
|
||||||
|
return R().getData();
|
||||||
|
}
|
||||||
|
function meta() {
|
||||||
|
return R().getMeta();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Filtro SERVER-SIDE (paginato, come la lista normale ma filtrata) ──────
|
||||||
|
// Il filtro gira su TUTTO il template; il server restituisce UNA PAGINA di righe
|
||||||
|
// filtrate + il totale, e navighiamo le pagine del set filtrato via AJAX.
|
||||||
|
const TEMPLATE_ID =
|
||||||
|
new URLSearchParams(location.search).get("id") || (meta().templateId ?? "");
|
||||||
|
const SHOW_ALL =
|
||||||
|
new URLSearchParams(location.search).get("all_users") === "1";
|
||||||
|
const IMPORTREF =
|
||||||
|
new URLSearchParams(location.search).get("importref") || "";
|
||||||
|
const FILTER_PER_PAGE =
|
||||||
|
parseInt(new URLSearchParams(location.search).get("limit"), 10) || 20;
|
||||||
|
|
||||||
|
let serverFiltered = false;
|
||||||
|
let filterPage = 1;
|
||||||
|
let filterTotalPages = 1;
|
||||||
|
let filterTotal = 0;
|
||||||
|
let originalData = null; // snapshot della pagina originale (per ripristino)
|
||||||
|
let filterReqSeq = 0;
|
||||||
|
let showingSelection = false; // true quando mostriamo l'UNIONE dei selezionati
|
||||||
|
let pagerFn = null; // paginazione corrente (applyServerFilter | showSelectedRecords)
|
||||||
|
|
||||||
|
function collectActiveFilters() {
|
||||||
|
const cols = meta().columns || [];
|
||||||
|
const out = [];
|
||||||
|
for (const [key, term] of Object.entries(colFilters)) {
|
||||||
|
if (!term || !String(term).trim()) continue;
|
||||||
|
const col = cols.find((c) => String(c.key) === String(key));
|
||||||
|
if (!col) continue;
|
||||||
|
out.push({ key: String(key), type: col.type, term: String(term).trim() });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sostituisce il dataset del renderer in-place e ridisegna.
|
||||||
|
function swapData(rows) {
|
||||||
|
const arr = data();
|
||||||
|
arr.length = 0;
|
||||||
|
(rows || []).forEach((r) => arr.push(r));
|
||||||
|
R().renderVisibleRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hidePagination(hide) {
|
||||||
|
// Durante il filtro la paginazione server (per pagina, via URL) è fuorviante.
|
||||||
|
document.querySelectorAll(".pager-bar").forEach((el) => {
|
||||||
|
el.style.display = hide ? "none" : "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pager del set filtrato (Prec / pag X di Y / Succ) nella toolbar.
|
||||||
|
function updateFilterPager() {
|
||||||
|
const el = document.getElementById("filterPager");
|
||||||
|
if (!el) return;
|
||||||
|
if (!serverFiltered) {
|
||||||
|
el.style.display = "none";
|
||||||
|
el.innerHTML = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.style.display = "inline-flex";
|
||||||
|
const label = showingSelection ? "selezionati" : "trovati";
|
||||||
|
el.innerHTML =
|
||||||
|
`<button type="button" class="btn btn-outline-secondary btn-sm" id="filterPrevBtn" ${filterPage <= 1 ? "disabled" : ""}>‹</button>` +
|
||||||
|
`<span style="font-size:12px;color:#333;">${filterTotal} ${label} · pag ${filterPage}/${filterTotalPages}</span>` +
|
||||||
|
`<button type="button" class="btn btn-outline-secondary btn-sm" id="filterNextBtn" ${filterPage >= filterTotalPages ? "disabled" : ""}>›</button>`;
|
||||||
|
const go = pagerFn || applyServerFilter;
|
||||||
|
const prev = document.getElementById("filterPrevBtn");
|
||||||
|
const next = document.getElementById("filterNextBtn");
|
||||||
|
if (prev) prev.addEventListener("click", () => go(filterPage - 1));
|
||||||
|
if (next) next.addEventListener("click", () => go(filterPage + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreOriginal() {
|
||||||
|
serverFiltered = false;
|
||||||
|
filterPage = 1;
|
||||||
|
filterTotalPages = 1;
|
||||||
|
filterTotal = 0;
|
||||||
|
if (originalData) swapData(originalData);
|
||||||
|
updateFilterPager();
|
||||||
|
hidePagination(false);
|
||||||
|
updateToolbar();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyServerFilter(page = 1) {
|
||||||
|
const active = collectActiveFilters();
|
||||||
|
if (active.length === 0) {
|
||||||
|
if (serverFiltered) restoreOriginal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!originalData) originalData = [...data()]; // snapshot pagina 1 originale
|
||||||
|
|
||||||
|
const seq = ++filterReqSeq;
|
||||||
|
try {
|
||||||
|
const resp = await fetch("filter_records.php", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
template_id: parseInt(TEMPLATE_ID, 10),
|
||||||
|
all_users: SHOW_ALL,
|
||||||
|
importref: IMPORTREF,
|
||||||
|
status: "i",
|
||||||
|
filters: active,
|
||||||
|
page: Math.max(1, page),
|
||||||
|
per_page: FILTER_PER_PAGE,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const json = await resp.json();
|
||||||
|
if (seq !== filterReqSeq) return; // risposta obsoleta: ignora
|
||||||
|
if (!json.success) {
|
||||||
|
console.error("[gridFilter] filtro:", json.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
serverFiltered = true;
|
||||||
|
showingSelection = false;
|
||||||
|
pagerFn = applyServerFilter;
|
||||||
|
filterPage = json.page;
|
||||||
|
filterTotalPages = json.total_pages;
|
||||||
|
filterTotal = json.total;
|
||||||
|
swapData(json.rows || []);
|
||||||
|
updateFilterPager();
|
||||||
|
hidePagination(true);
|
||||||
|
updateToolbar();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[gridFilter] fetch filtro fallita", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mostra i SELEZIONATI (unione tra filtri diversi) ──────────────────
|
||||||
|
// La selezione si accumula tra filtri; questo carica dal server TUTTI i record
|
||||||
|
// selezionati (per id) e li mostra, paginati, a prescindere dai filtri correnti.
|
||||||
|
async function showSelectedRecords(page = 1) {
|
||||||
|
const ids = [...selected];
|
||||||
|
if (!ids.length) {
|
||||||
|
showingSelection = false;
|
||||||
|
updateToolbar();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pagerFn = showSelectedRecords;
|
||||||
|
try {
|
||||||
|
const resp = await fetch("filter_records.php", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
template_id: parseInt(TEMPLATE_ID, 10),
|
||||||
|
all_users: SHOW_ALL,
|
||||||
|
importref: IMPORTREF,
|
||||||
|
status: "i",
|
||||||
|
ids: ids,
|
||||||
|
page: Math.max(1, page),
|
||||||
|
per_page: FILTER_PER_PAGE,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const json = await resp.json();
|
||||||
|
if (!json.success) {
|
||||||
|
console.error("[gridFilter] mostra selezionati:", json.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!originalData) originalData = [...data()];
|
||||||
|
showingSelection = true;
|
||||||
|
serverFiltered = true;
|
||||||
|
filterPage = json.page;
|
||||||
|
filterTotalPages = json.total_pages;
|
||||||
|
filterTotal = json.total;
|
||||||
|
swapData(json.rows || []);
|
||||||
|
updateFilterPager();
|
||||||
|
hidePagination(true);
|
||||||
|
updateToolbar();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[gridFilter] mostra selezionati fallita", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tumbler "Mostra selezionati" / "Torna al filtro".
|
||||||
|
function toggleShowSelected() {
|
||||||
|
if (!showingSelection) {
|
||||||
|
if (!selected.size) return;
|
||||||
|
showSelectedRecords(1);
|
||||||
|
} else {
|
||||||
|
exitSelectionView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Torna dal "mostra selezionati" al filtro corrente (o alla pagina originale).
|
||||||
|
function exitSelectionView() {
|
||||||
|
showingSelection = false;
|
||||||
|
pagerFn = applyServerFilter;
|
||||||
|
if (collectActiveFilters().length) {
|
||||||
|
applyServerFilter(1);
|
||||||
|
} else {
|
||||||
|
restoreOriginal();
|
||||||
|
}
|
||||||
|
updateToolbar();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deseleziona tutto.
|
||||||
|
function clearSelection() {
|
||||||
|
selected.clear();
|
||||||
|
document
|
||||||
|
.querySelectorAll("#gridRowContainer .grid-row.row-selected")
|
||||||
|
.forEach((el) => el.classList.remove("row-selected"));
|
||||||
|
document
|
||||||
|
.querySelectorAll(".filter-row-checkbox")
|
||||||
|
.forEach((cb) => (cb.checked = false));
|
||||||
|
const sa = document.getElementById("filterSelectAll");
|
||||||
|
if (sa) sa.checked = false;
|
||||||
|
if (showingSelection) {
|
||||||
|
exitSelectionView();
|
||||||
|
} else {
|
||||||
|
updateToolbar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Seleziona tutti i filtrati": chiede al server TUTTI gli id del match e li seleziona.
|
||||||
|
async function selectAllMatching(on) {
|
||||||
|
if (!serverFiltered) return;
|
||||||
|
const active = collectActiveFilters();
|
||||||
|
try {
|
||||||
|
const resp = await fetch("filter_records.php", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
template_id: parseInt(TEMPLATE_ID, 10),
|
||||||
|
all_users: SHOW_ALL,
|
||||||
|
importref: IMPORTREF,
|
||||||
|
status: "i",
|
||||||
|
filters: active,
|
||||||
|
page: filterPage,
|
||||||
|
per_page: FILTER_PER_PAGE,
|
||||||
|
want_all_ids: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const json = await resp.json();
|
||||||
|
if (!json.success) return;
|
||||||
|
(json.all_ids || []).forEach((id) => (on ? selected.add(id) : selected.delete(id)));
|
||||||
|
document
|
||||||
|
.querySelectorAll("#gridRowContainer .grid-row")
|
||||||
|
.forEach((rowEl) => {
|
||||||
|
rowEl.classList.toggle("row-selected", on);
|
||||||
|
const cb = rowEl.querySelector(".filter-row-checkbox");
|
||||||
|
if (cb) cb.checked = on;
|
||||||
|
});
|
||||||
|
updateToolbar();
|
||||||
|
if (json.all_ids_capped) {
|
||||||
|
alert("Selezione limitata a " + (json.all_ids || []).length + " record.");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[gridFilter] select-all fallita", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncFilterRow() {
|
||||||
|
const fr = document.getElementById("gridFilterRow");
|
||||||
|
const header = document.getElementById("gridHeaderContainer");
|
||||||
|
if (!fr || !header) return;
|
||||||
|
|
||||||
|
const actHeader = header.querySelector(".button-header");
|
||||||
|
const actCell = fr.querySelector(".filter-actions-cell");
|
||||||
|
if (actHeader && actCell) {
|
||||||
|
const aw = actHeader.offsetWidth;
|
||||||
|
actCell.style.flex = `0 0 ${aw}px`;
|
||||||
|
actCell.style.minWidth = `${aw}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerCells = header.querySelectorAll(
|
||||||
|
".grid-header:not(.button-header)",
|
||||||
|
);
|
||||||
|
const filterCells = fr.querySelectorAll(
|
||||||
|
".grid-cell:not(.filter-actions-cell)",
|
||||||
|
);
|
||||||
|
headerCells.forEach((hc, i) => {
|
||||||
|
const fc = filterCells[i];
|
||||||
|
if (fc) fc.style.flex = `0 0 ${hc.offsetWidth}px`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncStickyLeft() {
|
||||||
|
const btnHeader = document.querySelector(
|
||||||
|
"#gridHeaderContainer .button-header",
|
||||||
|
);
|
||||||
|
if (!btnHeader) return;
|
||||||
|
const w1 = btnHeader.offsetWidth; // larghezza reale colonna Actions
|
||||||
|
|
||||||
|
const fixRow = (rowEl) => {
|
||||||
|
if (!rowEl) return;
|
||||||
|
const cells = rowEl.querySelectorAll(
|
||||||
|
":scope > .grid-cell, :scope > .grid-header",
|
||||||
|
);
|
||||||
|
const c2 = cells[1];
|
||||||
|
const c3 = cells[2];
|
||||||
|
if (c2 && getComputedStyle(c2).position === "sticky") {
|
||||||
|
c2.style.left = `${w1}px`;
|
||||||
|
}
|
||||||
|
if (c3 && getComputedStyle(c3).position === "sticky") {
|
||||||
|
c3.style.left = `${w1 + (c2 ? c2.offsetWidth : 0)}px`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fixRow(document.getElementById("gridHeaderContainer"));
|
||||||
|
document.querySelectorAll(".grid-top").forEach(fixRow);
|
||||||
|
document
|
||||||
|
.querySelectorAll("#gridRowContainer .grid-row")
|
||||||
|
.forEach(fixRow);
|
||||||
|
fixRow(document.getElementById("gridFilterRow"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let _headerObs = null;
|
||||||
|
let _rowObs = null;
|
||||||
|
let _syncRaf = null;
|
||||||
|
|
||||||
|
function reconnectObservers() {
|
||||||
|
const header = document.getElementById("gridHeaderContainer");
|
||||||
|
const rows = document.getElementById("gridRowContainer");
|
||||||
|
if (_headerObs && header)
|
||||||
|
_headerObs.observe(header, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["style"],
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
if (_rowObs && rows) _rowObs.observe(rows, { childList: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleSync() {
|
||||||
|
if (_syncRaf) return;
|
||||||
|
_syncRaf = requestAnimationFrame(() => {
|
||||||
|
_syncRaf = null;
|
||||||
|
if (_headerObs) _headerObs.disconnect();
|
||||||
|
if (_rowObs) _rowObs.disconnect();
|
||||||
|
syncStickyLeft();
|
||||||
|
syncFilterRow();
|
||||||
|
reconnectObservers();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hookGridSync() {
|
||||||
|
_headerObs = new MutationObserver(scheduleSync);
|
||||||
|
_rowObs = new MutationObserver(scheduleSync);
|
||||||
|
reconnectObservers();
|
||||||
|
scheduleSync(); // primo allineamento
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowMatchesFilters(row) {
|
||||||
|
if (serverFiltered) return true;
|
||||||
|
for (const [key, term] of Object.entries(colFilters)) {
|
||||||
|
if (!term) continue;
|
||||||
|
const t = term.toLowerCase();
|
||||||
|
const val = getRowColValue(row, key);
|
||||||
|
if (!String(val).toLowerCase().includes(t)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRowColValue(row, key) {
|
||||||
|
const cols = meta().columns || [];
|
||||||
|
const col = cols.find((c) => String(c.key) === String(key));
|
||||||
|
if (!col) return "";
|
||||||
|
switch (col.type) {
|
||||||
|
case "detail":
|
||||||
|
case "main_field":
|
||||||
|
return row.details?.[String(key)] ?? "";
|
||||||
|
case "fixed":
|
||||||
|
return row.fixedFields?.[key] ?? "";
|
||||||
|
case "idclient":
|
||||||
|
return row.idclient ?? "";
|
||||||
|
case "cliente_fornitore_id":
|
||||||
|
return row.cliente_fornitore_id ?? "";
|
||||||
|
case "tested_component":
|
||||||
|
return row.tested_component ?? "";
|
||||||
|
case "static":
|
||||||
|
return row[key] ?? "";
|
||||||
|
case "status":
|
||||||
|
return row.status ?? "";
|
||||||
|
default:
|
||||||
|
return row[key] ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Applica visibilità righe (filtri + restrizione) ──
|
||||||
|
function applyVisibility() {
|
||||||
|
const rows = document.querySelectorAll("#gridRowContainer .grid-row");
|
||||||
|
rows.forEach((rowEl) => {
|
||||||
|
const id = parseInt(rowEl.dataset.id, 10);
|
||||||
|
const row = data().find((r) => String(r.iddatadb) === String(id));
|
||||||
|
let show = true;
|
||||||
|
if (row && !rowMatchesFilters(row)) show = false;
|
||||||
|
if (restricted && restrictedIds && !restrictedIds.has(id))
|
||||||
|
show = false;
|
||||||
|
rowEl.style.display = show ? "" : "none";
|
||||||
|
});
|
||||||
|
window.dispatchEvent(new Event("resize")); // ricalcola scrollbar top
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lista iddatadb attualmente visibili (per propagazione/export ecc.) ──
|
||||||
|
window.getVisibleGridIds = function () {
|
||||||
|
if (serverFiltered) {
|
||||||
|
return data()
|
||||||
|
.map((r) => parseInt(r.iddatadb, 10))
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
const ids = [];
|
||||||
|
document
|
||||||
|
.querySelectorAll("#gridRowContainer .grid-row")
|
||||||
|
.forEach((el) => {
|
||||||
|
if (el.style.display !== "none")
|
||||||
|
ids.push(parseInt(el.dataset.id, 10));
|
||||||
|
});
|
||||||
|
return ids.filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Righe target per operazioni bulk (propaga/export/clona/save) ──
|
||||||
|
// Regola: filtri attivi + selezione → solo selezionate;
|
||||||
|
// filtri attivi senza selezione → solo visibili;
|
||||||
|
// filtri non attivi → null (= tutte, comportamento originale).
|
||||||
|
window.getTargetGridIds = function () {
|
||||||
|
if (!filtersActive) return null; // null = nessun vincolo
|
||||||
|
if (selected.size > 0) return new Set([...selected]);
|
||||||
|
return new Set(window.getVisibleGridIds());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper: true se l'id è tra i target (o se non c'è vincolo)
|
||||||
|
window.isTargetGridId = function (id) {
|
||||||
|
const t = window.getTargetGridIds();
|
||||||
|
if (t === null) return true;
|
||||||
|
return t.has(parseInt(id, 10));
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Toggle selezione riga ──
|
||||||
|
function toggleRow(id, on) {
|
||||||
|
id = parseInt(id, 10);
|
||||||
|
if (on) selected.add(id);
|
||||||
|
else selected.delete(id);
|
||||||
|
const rowEl = document.querySelector(
|
||||||
|
`#gridRowContainer .grid-row[data-id="${id}"]`,
|
||||||
|
);
|
||||||
|
if (rowEl) {
|
||||||
|
rowEl.classList.toggle("row-selected", selected.has(id));
|
||||||
|
const cb = rowEl.querySelector(".filter-row-checkbox");
|
||||||
|
if (cb) cb.checked = selected.has(id);
|
||||||
|
}
|
||||||
|
updateToolbar();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Inietta checkbox nelle righe visibili ──
|
||||||
|
// ── Inietta checkbox dentro la cella Actions (già sticky) ──
|
||||||
|
function injectCheckboxes() {
|
||||||
|
document
|
||||||
|
.querySelectorAll("#gridRowContainer .grid-row")
|
||||||
|
.forEach((rowEl) => {
|
||||||
|
const btnCell = rowEl.querySelector(".button-cell");
|
||||||
|
if (!btnCell) return;
|
||||||
|
if (btnCell.querySelector(".filter-row-checkbox")) return;
|
||||||
|
const id = parseInt(rowEl.dataset.id, 10);
|
||||||
|
const wrap = document.createElement("label");
|
||||||
|
wrap.className = "filter-cb-wrap";
|
||||||
|
wrap.innerHTML = `<input type="checkbox" class="filter-row-checkbox" ${selected.has(id) ? "checked" : ""}>`;
|
||||||
|
btnCell.insertBefore(wrap, btnCell.firstChild);
|
||||||
|
if (selected.has(id)) rowEl.classList.add("row-selected");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCheckboxes() {
|
||||||
|
document
|
||||||
|
.querySelectorAll(".filter-cb-wrap")
|
||||||
|
.forEach((el) => el.remove());
|
||||||
|
document
|
||||||
|
.querySelectorAll(".grid-row.row-selected")
|
||||||
|
.forEach((el) => el.classList.remove("row-selected"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Riga input filtri colonna (allineata alle larghezze reali) ──
|
||||||
|
// ── Riga input filtri colonna (allineata alle larghezze reali) ──
|
||||||
|
function injectFilterRow() {
|
||||||
|
if (document.getElementById("gridFilterRow")) return;
|
||||||
|
const top = document.getElementById("gridTopContainer");
|
||||||
|
const header = document.getElementById("gridHeaderContainer");
|
||||||
|
if (!top || !header) return;
|
||||||
|
|
||||||
|
const fr = document.createElement("div");
|
||||||
|
fr.className = "grid-row grid-filter-row";
|
||||||
|
fr.id = "gridFilterRow";
|
||||||
|
|
||||||
|
// cella allineata alla colonna Actions (sticky, legge larghezza reale)
|
||||||
|
const actHeader = header.querySelector(".button-header");
|
||||||
|
const actCell = document.createElement("div");
|
||||||
|
actCell.className = "grid-cell button-cell filter-actions-cell";
|
||||||
|
const aw = actHeader ? actHeader.offsetWidth : 220;
|
||||||
|
actCell.style.flex = `0 0 ${aw}px`;
|
||||||
|
actCell.style.minWidth = `${aw}px`;
|
||||||
|
actCell.innerHTML = `<label class="filter-cb-wrap" style="display:inline-flex;" title="Seleziona tutti i filtrati"><input type="checkbox" id="filterSelectAll"></label>`;
|
||||||
|
fr.appendChild(actCell);
|
||||||
|
|
||||||
|
// una cella per colonna, larghezza letta dall'header reale
|
||||||
|
const headerCells = header.querySelectorAll(
|
||||||
|
".grid-header:not(.button-header)",
|
||||||
|
);
|
||||||
|
(meta().columns || []).forEach((col, i) => {
|
||||||
|
const hc = headerCells[i];
|
||||||
|
const w = hc ? hc.offsetWidth : col.width || 150;
|
||||||
|
const cell = document.createElement("div");
|
||||||
|
cell.className = "grid-cell";
|
||||||
|
cell.style.flex = `0 0 ${w}px`;
|
||||||
|
if (col.type === "tracking" || col.type === "awb") {
|
||||||
|
// niente filtro
|
||||||
|
} else if (col.type === "status") {
|
||||||
|
// Status: dropdown (3 valori) invece del testo libero.
|
||||||
|
const cur = colFilters[col.key] || "";
|
||||||
|
const opt = (v, lbl) =>
|
||||||
|
`<option value="${v}" ${cur === v ? "selected" : ""}>${lbl}</option>`;
|
||||||
|
cell.innerHTML =
|
||||||
|
`<select class="filter-col-input" data-col-key="${col.key}" style="width:100%;padding:3px 6px;font-size:12px;border:1px solid #ced4da;border-radius:4px;">` +
|
||||||
|
opt("", "— tutti —") +
|
||||||
|
opt("i", "Imported") +
|
||||||
|
opt("P", "In Progress") +
|
||||||
|
opt("l", "To LIMS") +
|
||||||
|
`</select>`;
|
||||||
|
} else {
|
||||||
|
cell.innerHTML = `<input type="text" class="filter-col-input" data-col-key="${col.key}" placeholder="🔍 ${col.label || ""}" value="${colFilters[col.key] || ""}" style="width:100%;padding:3px 6px;font-size:12px;border:1px solid #ced4da;border-radius:4px;">`;
|
||||||
|
}
|
||||||
|
fr.appendChild(cell);
|
||||||
|
});
|
||||||
|
|
||||||
|
top.parentNode.insertBefore(fr, top);
|
||||||
|
scheduleSync(); // allinea larghezze filtri + offset sticky
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFilterRow() {
|
||||||
|
const fr = document.getElementById("gridFilterRow");
|
||||||
|
if (fr) fr.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Toolbar (bottoni contestuali) ──
|
||||||
|
function updateToolbar() {
|
||||||
|
const bar = document.getElementById("filterActionBar");
|
||||||
|
if (!bar) return;
|
||||||
|
const count = selected.size;
|
||||||
|
bar.style.display = filtersActive ? "inline-flex" : "none";
|
||||||
|
const cntEl = document.getElementById("filterSelCount");
|
||||||
|
if (cntEl) cntEl.textContent = count;
|
||||||
|
document.getElementById("filterDeleteBtn").disabled = count === 0;
|
||||||
|
|
||||||
|
const showBtn = document.getElementById("filterRestrictBtn");
|
||||||
|
showBtn.disabled = count === 0 && !showingSelection;
|
||||||
|
showBtn.innerHTML = showingSelection
|
||||||
|
? '<i class="fas fa-eye"></i> Torna al filtro'
|
||||||
|
: '<i class="fas fa-list-check"></i> Mostra selezionati';
|
||||||
|
|
||||||
|
const clearSelBtn = document.getElementById("filterClearSelBtn");
|
||||||
|
if (clearSelBtn) clearSelBtn.disabled = count === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Main toggle ──
|
||||||
|
function toggleFilters() {
|
||||||
|
filtersActive = !filtersActive;
|
||||||
|
const grid = document.getElementById("gridContainer");
|
||||||
|
const btn = document.getElementById("filtersToggleBtn");
|
||||||
|
if (filtersActive) {
|
||||||
|
grid.classList.add("filters-on");
|
||||||
|
btn.classList.add("active");
|
||||||
|
injectCheckboxes();
|
||||||
|
injectFilterRow();
|
||||||
|
} else {
|
||||||
|
grid.classList.remove("filters-on");
|
||||||
|
btn.classList.remove("active");
|
||||||
|
removeCheckboxes();
|
||||||
|
removeFilterRow();
|
||||||
|
// reset filtri colonna + ripristina la pagina originale
|
||||||
|
Object.keys(colFilters).forEach((k) => delete colFilters[k]);
|
||||||
|
if (serverFiltered) restoreOriginal();
|
||||||
|
}
|
||||||
|
updateToolbar();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Batch delete ──
|
||||||
|
async function batchDelete() {
|
||||||
|
const ids = [...selected];
|
||||||
|
if (!ids.length) return;
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
`Eliminare ${ids.length} righe dal database? Operazione irreversibile.`,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const btn = document.getElementById("filterDeleteBtn");
|
||||||
|
btn.disabled = true;
|
||||||
|
const original = btn.innerHTML;
|
||||||
|
|
||||||
|
let ok = 0,
|
||||||
|
fail = 0;
|
||||||
|
for (const id of ids) {
|
||||||
|
try {
|
||||||
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${ok + fail + 1}/${ids.length}`;
|
||||||
|
const resp = await fetch("delete_record.php", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ id: id }),
|
||||||
|
});
|
||||||
|
const json = await resp.json();
|
||||||
|
if (json.success) {
|
||||||
|
ok++;
|
||||||
|
// rimuovi da gridData
|
||||||
|
const arr = data();
|
||||||
|
const idx = arr.findIndex(
|
||||||
|
(r) => String(r.iddatadb) === String(id),
|
||||||
|
);
|
||||||
|
if (idx !== -1) arr.splice(idx, 1);
|
||||||
|
selected.delete(id);
|
||||||
|
if (restrictedIds) restrictedIds.delete(id);
|
||||||
|
} else fail++;
|
||||||
|
} catch (e) {
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.innerHTML = original;
|
||||||
|
R().renderVisibleRows();
|
||||||
|
if (filtersActive) {
|
||||||
|
injectCheckboxes();
|
||||||
|
}
|
||||||
|
applyVisibility();
|
||||||
|
updateToolbar();
|
||||||
|
alert(`Eliminate: ${ok}${fail ? " — Falliti: " + fail : ""}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Restringi / mostra tutti ──
|
||||||
|
function toggleRestrict() {
|
||||||
|
if (!restricted) {
|
||||||
|
if (!selected.size) return;
|
||||||
|
restrictedIds = new Set([...selected]);
|
||||||
|
restricted = true;
|
||||||
|
} else {
|
||||||
|
restricted = false;
|
||||||
|
restrictedIds = null;
|
||||||
|
}
|
||||||
|
applyVisibility();
|
||||||
|
updateToolbar();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Costruzione UI toolbar (bottoni in cima) ──
|
||||||
|
function buildUI() {
|
||||||
|
const addBtn = document.getElementById("addRowBtn");
|
||||||
|
if (!addBtn) return;
|
||||||
|
|
||||||
|
// bottone Filters
|
||||||
|
const fBtn = document.createElement("button");
|
||||||
|
fBtn.type = "button";
|
||||||
|
fBtn.id = "filtersToggleBtn";
|
||||||
|
fBtn.className = "btn btn-outline-info btn-sm";
|
||||||
|
fBtn.style.flexShrink = "0";
|
||||||
|
fBtn.innerHTML = '<i class="fas fa-filter"></i> Filters';
|
||||||
|
addBtn.parentNode.insertBefore(fBtn, addBtn.nextSibling);
|
||||||
|
|
||||||
|
// barra azioni contestuali
|
||||||
|
const bar = document.createElement("span");
|
||||||
|
bar.id = "filterActionBar";
|
||||||
|
bar.style.cssText =
|
||||||
|
"display:none;align-items:center;gap:8px;flex-shrink:0;";
|
||||||
|
bar.innerHTML = `
|
||||||
|
<span id="filterPager" style="display:none;align-items:center;gap:6px;"></span>
|
||||||
|
<button type="button" id="filterClearBtn" class="btn btn-outline-secondary btn-sm"><i class="fas fa-times"></i> Pulisci filtri</button>
|
||||||
|
<span style="font-size:12px;color:#555;">Selezionate: <strong id="filterSelCount">0</strong></span>
|
||||||
|
<button type="button" id="filterRestrictBtn" class="btn btn-outline-primary btn-sm"><i class="fas fa-list-check"></i> Mostra selezionati</button>
|
||||||
|
<button type="button" id="filterClearSelBtn" class="btn btn-outline-secondary btn-sm"><i class="fas fa-square"></i> Deseleziona</button>
|
||||||
|
<button type="button" id="filterDeleteBtn" class="btn btn-outline-danger btn-sm"><i class="fas fa-trash"></i> Elimina selezionati</button>
|
||||||
|
`;
|
||||||
|
fBtn.parentNode.insertBefore(bar, fBtn.nextSibling);
|
||||||
|
|
||||||
|
fBtn.addEventListener("click", toggleFilters);
|
||||||
|
document
|
||||||
|
.getElementById("filterClearBtn")
|
||||||
|
.addEventListener("click", clearFilters);
|
||||||
|
document
|
||||||
|
.getElementById("filterDeleteBtn")
|
||||||
|
.addEventListener("click", batchDelete);
|
||||||
|
document
|
||||||
|
.getElementById("filterRestrictBtn")
|
||||||
|
.addEventListener("click", toggleShowSelected);
|
||||||
|
document
|
||||||
|
.getElementById("filterClearSelBtn")
|
||||||
|
.addEventListener("click", clearSelection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Svuota tutti i filtri di colonna (input + select) e torna alla pagina originale.
|
||||||
|
function clearFilters() {
|
||||||
|
Object.keys(colFilters).forEach((k) => delete colFilters[k]);
|
||||||
|
document
|
||||||
|
.querySelectorAll("#gridFilterRow .filter-col-input")
|
||||||
|
.forEach((el) => {
|
||||||
|
el.value = "";
|
||||||
|
});
|
||||||
|
if (serverFiltered) restoreOriginal();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Event delegation ──
|
||||||
|
function attachEvents() {
|
||||||
|
// checkbox click
|
||||||
|
document.addEventListener("change", function (e) {
|
||||||
|
if (e.target.classList.contains("filter-row-checkbox")) {
|
||||||
|
const rowEl = e.target.closest(".grid-row");
|
||||||
|
toggleRow(rowEl.dataset.id, e.target.checked);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Seleziona / deseleziona tutti i FILTRATI
|
||||||
|
document.addEventListener("change", function (e) {
|
||||||
|
if (e.target.id !== "filterSelectAll") return;
|
||||||
|
const on = e.target.checked;
|
||||||
|
|
||||||
|
// Con filtro server-side: seleziona TUTTI i match (anche altre pagine).
|
||||||
|
if (serverFiltered) {
|
||||||
|
selectAllMatching(on);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelectorAll("#gridRowContainer .grid-row")
|
||||||
|
.forEach((rowEl) => {
|
||||||
|
if (rowEl.style.display === "none") return;
|
||||||
|
const id = parseInt(rowEl.dataset.id, 10);
|
||||||
|
if (!id) return;
|
||||||
|
if (on) selected.add(id);
|
||||||
|
else selected.delete(id);
|
||||||
|
rowEl.classList.toggle("row-selected", on);
|
||||||
|
const cb = rowEl.querySelector(".filter-row-checkbox");
|
||||||
|
if (cb) cb.checked = on;
|
||||||
|
});
|
||||||
|
updateToolbar();
|
||||||
|
});
|
||||||
|
|
||||||
|
// click su QUALSIASI punto della riga (tranne campi interattivi) = seleziona
|
||||||
|
document.addEventListener("click", function (e) {
|
||||||
|
if (!filtersActive) return;
|
||||||
|
const rowEl = e.target.closest("#gridRowContainer .grid-row");
|
||||||
|
if (!rowEl) return;
|
||||||
|
|
||||||
|
// ignora click su elementi interattivi
|
||||||
|
if (
|
||||||
|
e.target.closest(
|
||||||
|
"input, select, textarea, button, a, .select2-container, .action-btn, .propagate-btn, .add-part-btn",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// ma se ho cliccato proprio la checkbox, lascia fare al change
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cb = rowEl.querySelector(".filter-row-checkbox");
|
||||||
|
if (!cb) return;
|
||||||
|
cb.checked = !cb.checked;
|
||||||
|
toggleRow(rowEl.dataset.id, cb.checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
// filtri colonna testo (debounce) → filtro server-side su tutto il template
|
||||||
|
let t = null;
|
||||||
|
document.addEventListener("input", function (e) {
|
||||||
|
if (!e.target.classList.contains("filter-col-input")) return;
|
||||||
|
if (e.target.tagName === "SELECT") return; // i select li gestisce 'change'
|
||||||
|
const key = e.target.dataset.colKey;
|
||||||
|
colFilters[key] = e.target.value;
|
||||||
|
clearTimeout(t);
|
||||||
|
t = setTimeout(applyServerFilter, 250);
|
||||||
|
});
|
||||||
|
|
||||||
|
// dropdown (es. Status): applica subito, senza debounce
|
||||||
|
document.addEventListener("change", function (e) {
|
||||||
|
if (!e.target.classList.contains("filter-col-input")) return;
|
||||||
|
if (e.target.tagName !== "SELECT") return;
|
||||||
|
colFilters[e.target.dataset.colKey] = e.target.value;
|
||||||
|
applyServerFilter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ripristina stato filtri dopo un re-render (checkbox + restrizione) ──
|
||||||
|
let _restoring = false;
|
||||||
|
function restoreFilterState() {
|
||||||
|
if (!filtersActive || _restoring) return;
|
||||||
|
_restoring = true; // evita loop con l'observer mentre iniettiamo/nascondiamo
|
||||||
|
try {
|
||||||
|
injectCheckboxes();
|
||||||
|
applyVisibility(); // ri-applica filtri colonna E restringi selezione
|
||||||
|
} finally {
|
||||||
|
// rilascia dopo il tick così le nostre mutazioni non ci ri-triggerano subito
|
||||||
|
setTimeout(() => {
|
||||||
|
_restoring = false;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Observer: qualunque cosa ricrei le righe (propagazione, save, ecc.)
|
||||||
|
// ripristina automaticamente checkbox + restrizione. Robusto perché
|
||||||
|
// non dipende da chi chiama renderVisibleRows. ──
|
||||||
|
function hookRerender() {
|
||||||
|
const rowC = document.getElementById("gridRowContainer");
|
||||||
|
if (!rowC) return;
|
||||||
|
|
||||||
|
const obs = new MutationObserver(() => {
|
||||||
|
if (!filtersActive || _restoring) return;
|
||||||
|
// debounce leggero: aspetta che il render finisca
|
||||||
|
clearTimeout(hookRerender._t);
|
||||||
|
hookRerender._t = setTimeout(restoreFilterState, 30);
|
||||||
|
});
|
||||||
|
|
||||||
|
obs.observe(rowC, { childList: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (!window.gridRenderer) {
|
||||||
|
console.error("[gridFilter] gridRenderer non trovato");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
buildUI();
|
||||||
|
attachEvents();
|
||||||
|
hookRerender();
|
||||||
|
hookGridSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// parte dopo che gridRenderer ha finito init
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", () =>
|
||||||
|
setTimeout(init, 300),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setTimeout(init, 300);
|
||||||
|
}
|
||||||
|
})();
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -59,7 +59,8 @@ foreach ($allMappings as $mapping) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!$mainFieldMapping) {
|
if (!$mainFieldMapping) {
|
||||||
$mainFieldMapping = reset(array_filter($allMappings, fn($m) => !$m['is_manual']));
|
$filtered = array_filter($allMappings, fn($m) => !$m['is_manual']);
|
||||||
|
$mainFieldMapping = reset($filtered);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve data from datadb
|
// Retrieve data from datadb
|
||||||
|
|||||||
@@ -10,70 +10,180 @@
|
|||||||
<title>Template Buttons - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
<title>Template Buttons - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Layout flessibile per gestire dimensioni diverse */
|
/* Main buttons container */
|
||||||
#templateButtons {
|
.template-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
/* Allinea a sinistra */
|
padding: 10px 0;
|
||||||
padding: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Definizione delle dimensioni */
|
/* Button sizes */
|
||||||
/* Definizione delle dimensioni */
|
|
||||||
.btn-small {
|
.btn-small {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
min-height: 30px;
|
min-height: 36px;
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
/* Aggiunto */
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
/* Aggiunto */
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
/* Aggiunto */
|
gap: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-medium {
|
.btn-medium {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
min-width: 130px;
|
min-width: 140px;
|
||||||
min-height: 45px;
|
min-height: 48px;
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
/* Aggiunto */
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
/* Aggiunto */
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
/* Aggiunto */
|
gap: 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-large {
|
.btn-large {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
padding: 14px 28px;
|
padding: 14px 28px;
|
||||||
min-width: 180px;
|
min-width: 190px;
|
||||||
min-height: 60px;
|
min-height: 64px;
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
/* Aggiunto */
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
/* Aggiunto */
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
/* Aggiunto */
|
gap: 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Stile della barra di ricerca */
|
.template-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-large .template-icon {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-small .template-icon {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search box */
|
||||||
#searchInput {
|
#searchInput {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 10px 14px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 18px;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #d9d9d9;
|
||||||
border-radius: 5px;
|
border-radius: 8px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchInput:focus {
|
||||||
|
border-color: #0d6efd;
|
||||||
|
box-shadow: 0 0 0 0.15rem rgba(13, 110, 253, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
.custom-tabs {
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tabs .nav-link {
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px 10px 0 0;
|
||||||
|
color: #555;
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tabs .nav-link:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tabs .nav-link.active {
|
||||||
|
background: #0d6efd;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-pane {
|
||||||
|
min-height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-message {
|
||||||
|
color: #6c757d;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-message {
|
||||||
|
color: #6c757d;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6c757d;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 6px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-btn-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-star {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
right: -6px;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
|
||||||
|
font-size: 13px;
|
||||||
|
color: #ccc;
|
||||||
|
z-index: 2;
|
||||||
|
transition: color 0.15s, transform 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-star:hover {
|
||||||
|
transform: scale(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-star.active {
|
||||||
|
color: #ffc107;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<?php include('include/popup.php'); ?>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
@@ -87,14 +197,54 @@
|
|||||||
<h6 class="mb-0">Active Templates</h6>
|
<h6 class="mb-0">Active Templates</h6>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<!-- Barra di ricerca -->
|
|
||||||
<input type="text" id="searchInput" placeholder="Search template...">
|
<input type="text" id="searchInput" placeholder="Search template...">
|
||||||
<div id="templateButtons"></div>
|
|
||||||
|
<ul class="nav nav-tabs custom-tabs" id="templateTabs" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" id="xls-tab" data-bs-toggle="tab" data-bs-target="#xls-pane" type="button" role="tab" aria-controls="xls-pane" aria-selected="true">
|
||||||
|
<i class="bx bx-spreadsheet template-icon"></i>
|
||||||
|
XLS
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="api-tab" data-bs-toggle="tab" data-bs-target="#api-pane" type="button" role="tab" aria-controls="api-pane" aria-selected="false">
|
||||||
|
<i class="bx bx-transfer-alt template-icon"></i>
|
||||||
|
JSON/API
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="pdf-tab" data-bs-toggle="tab" data-bs-target="#pdf-pane" type="button" role="tab" aria-controls="pdf-pane" aria-selected="false">
|
||||||
|
<i class="bx bx-file-pdf template-icon"></i>
|
||||||
|
PDF
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content" id="templateTabsContent">
|
||||||
|
<div class="tab-pane fade show active" id="xls-pane" role="tabpanel" aria-labelledby="xls-tab">
|
||||||
|
<div id="templateButtonsXLS" class="template-buttons">
|
||||||
|
<div class="loading-message">Loading XLS templates...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="api-pane" role="tabpanel" aria-labelledby="api-tab">
|
||||||
|
<div id="templateButtonsAPI" class="template-buttons">
|
||||||
|
<div class="loading-message">Loading API templates...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="pdf-pane" role="tabpanel" aria-labelledby="pdf-tab">
|
||||||
|
<div id="templateButtonsPDF" class="template-buttons">
|
||||||
|
<div class="loading-message">Loading PDF templates...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overlay toggle-icon"></div>
|
<div class="overlay toggle-icon"></div>
|
||||||
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
||||||
<?php include('include/footer.php'); ?>
|
<?php include('include/footer.php'); ?>
|
||||||
@@ -104,46 +254,214 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
const allTemplates = [];
|
||||||
|
|
||||||
|
const containers = {
|
||||||
|
XLS: document.getElementById("templateButtonsXLS"),
|
||||||
|
API: document.getElementById("templateButtonsAPI"),
|
||||||
|
PDF: document.getElementById("templateButtonsPDF")
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSizeClass(buttonSize) {
|
||||||
|
if (buttonSize === "small") return "btn-small";
|
||||||
|
if (buttonSize === "large") return "btn-large";
|
||||||
|
return "btn-medium";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTemplateIcon(sourceType) {
|
||||||
|
switch ((sourceType || '').toUpperCase()) {
|
||||||
|
case 'XLS':
|
||||||
|
return 'bx bx-spreadsheet';
|
||||||
|
case 'API':
|
||||||
|
return 'bx bx-transfer-alt';
|
||||||
|
case 'PDF':
|
||||||
|
return 'bx bx-file-pdf';
|
||||||
|
default:
|
||||||
|
return 'bx bx-file';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createButtonWrapper(template) {
|
||||||
|
const sizeClass = getSizeClass(template.button_size);
|
||||||
|
const sourceType = (template.source_type || '').toUpperCase();
|
||||||
|
const iconClass = getTemplateIcon(sourceType);
|
||||||
|
const isFavorite = !!parseInt(template.is_favorite);
|
||||||
|
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "template-btn-wrapper";
|
||||||
|
|
||||||
|
const btn = document.createElement("a");
|
||||||
|
btn.href = `import_xls2.php?id=${template.id}`;
|
||||||
|
btn.className = `btn ${sizeClass}`;
|
||||||
|
btn.style.backgroundColor = template.button_bg_color || '#0d6efd';
|
||||||
|
btn.style.color = template.button_text_color || '#ffffff';
|
||||||
|
btn.setAttribute("data-label", (template.button_label || '').toLowerCase());
|
||||||
|
btn.setAttribute("data-source-type", sourceType);
|
||||||
|
|
||||||
|
btn.innerHTML = `
|
||||||
|
<i class="${iconClass} template-icon"></i>
|
||||||
|
<span>${escapeHtml(template.button_label || 'Unnamed')}</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const star = document.createElement("span");
|
||||||
|
star.className = "favorite-star" + (isFavorite ? " active" : "");
|
||||||
|
star.innerHTML = `<i class="bx ${isFavorite ? 'bxs-star' : 'bx-star'}"></i>`;
|
||||||
|
star.setAttribute("data-template-id", template.id);
|
||||||
|
star.title = isFavorite ? "Rimuovi dai preferiti" : "Aggiungi ai preferiti";
|
||||||
|
|
||||||
|
star.addEventListener("click", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleFavorite(template.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.appendChild(btn);
|
||||||
|
wrapper.appendChild(star);
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFavorite(templateId) {
|
||||||
|
const template = allTemplates.find(t => String(t.id) === String(templateId));
|
||||||
|
if (!template) return;
|
||||||
|
|
||||||
|
const newState = !parseInt(template.is_favorite);
|
||||||
|
|
||||||
|
// Optimistic update
|
||||||
|
template.is_favorite = newState ? 1 : 0;
|
||||||
|
renderTemplates(document.getElementById("searchInput").value.toLowerCase().trim());
|
||||||
|
|
||||||
|
fetch("toggle_template_favorite.php", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
template_id: templateId,
|
||||||
|
is_favorite: newState
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (!data.success) {
|
||||||
|
console.error("Failed to toggle favorite:", data.message);
|
||||||
|
// Rollback in caso di errore
|
||||||
|
template.is_favorite = newState ? 0 : 1;
|
||||||
|
renderTemplates(document.getElementById("searchInput").value.toLowerCase().trim());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Fetch error toggling favorite:", error);
|
||||||
|
template.is_favorite = newState ? 0 : 1;
|
||||||
|
renderTemplates(document.getElementById("searchInput").value.toLowerCase().trim());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearContainers() {
|
||||||
|
Object.values(containers).forEach(container => {
|
||||||
|
container.innerHTML = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSection(container, title, templates) {
|
||||||
|
if (templates.length === 0) return;
|
||||||
|
|
||||||
|
const heading = document.createElement("div");
|
||||||
|
heading.className = "section-title";
|
||||||
|
heading.textContent = title;
|
||||||
|
container.appendChild(heading);
|
||||||
|
|
||||||
|
const group = document.createElement("div");
|
||||||
|
group.className = "template-buttons";
|
||||||
|
group.style.marginTop = "0";
|
||||||
|
group.style.paddingTop = "0";
|
||||||
|
|
||||||
|
templates.forEach(template => {
|
||||||
|
group.appendChild(createButtonWrapper(template));
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTemplates(searchValue = '') {
|
||||||
|
clearContainers();
|
||||||
|
|
||||||
|
const grouped = {
|
||||||
|
XLS: [],
|
||||||
|
API: [],
|
||||||
|
PDF: []
|
||||||
|
};
|
||||||
|
|
||||||
|
allTemplates.forEach(template => {
|
||||||
|
const sourceType = (template.source_type || '').toUpperCase();
|
||||||
|
const label = (template.button_label || '').toLowerCase();
|
||||||
|
|
||||||
|
if (searchValue && !label.includes(searchValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grouped[sourceType]) {
|
||||||
|
grouped[sourceType].push(template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(grouped).forEach(type => {
|
||||||
|
const container = containers[type];
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
if (grouped[type].length === 0) {
|
||||||
|
container.innerHTML = `<div class="empty-message">No templates found in this section.</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const favorites = grouped[type].filter(t => parseInt(t.is_favorite) === 1);
|
||||||
|
const others = grouped[type].filter(t => parseInt(t.is_favorite) !== 1);
|
||||||
|
|
||||||
|
renderSection(container, "★ Preferiti", favorites);
|
||||||
|
renderSection(container, "Altri", others);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fetch("load_active_templates.php")
|
fetch("load_active_templates.php")
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
console.error("Error loading templates:", data.message);
|
console.error("Error loading templates:", data.message);
|
||||||
|
clearContainers();
|
||||||
|
Object.values(containers).forEach(container => {
|
||||||
|
container.innerHTML = `<div class="empty-message">Error loading templates.</div>`;
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let templateButtons = document.getElementById("templateButtons");
|
if (!Array.isArray(data.data)) {
|
||||||
templateButtons.innerHTML = '';
|
clearContainers();
|
||||||
|
Object.values(containers).forEach(container => {
|
||||||
data.data.forEach(template => {
|
container.innerHTML = `<div class="empty-message">Invalid response format.</div>`;
|
||||||
let sizeClass = "btn-medium"; // Default
|
});
|
||||||
if (template.button_size === "small") sizeClass = "btn-small";
|
return;
|
||||||
if (template.button_size === "large") sizeClass = "btn-large";
|
|
||||||
|
|
||||||
let btn = document.createElement("a");
|
|
||||||
btn.href = `import_xls2.php?id=${template.id}`;
|
|
||||||
btn.className = `btn ${sizeClass}`;
|
|
||||||
btn.style.backgroundColor = template.button_bg_color;
|
|
||||||
btn.style.color = template.button_text_color;
|
|
||||||
btn.textContent = template.button_label;
|
|
||||||
btn.setAttribute("data-label", template.button_label.toLowerCase()); // Attributo per ricerca
|
|
||||||
|
|
||||||
templateButtons.appendChild(btn);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(error => console.error("Fetch error:", error));
|
|
||||||
|
|
||||||
// Funzione per la ricerca live
|
|
||||||
document.getElementById("searchInput").addEventListener("input", function() {
|
|
||||||
let searchValue = this.value.toLowerCase();
|
|
||||||
document.querySelectorAll("#templateButtons a").forEach(btn => {
|
|
||||||
let label = btn.getAttribute("data-label");
|
|
||||||
if (label.includes(searchValue)) {
|
|
||||||
btn.style.display = "inline-block";
|
|
||||||
} else {
|
|
||||||
btn.style.display = "none";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allTemplates.push(...data.data);
|
||||||
|
renderTemplates();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Fetch error:", error);
|
||||||
|
clearContainers();
|
||||||
|
Object.values(containers).forEach(container => {
|
||||||
|
container.innerHTML = `<div class="empty-message">Fetch error while loading templates.</div>`;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById("searchInput").addEventListener("input", function() {
|
||||||
|
const searchValue = this.value.toLowerCase().trim();
|
||||||
|
renderTemplates(searchValue);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+1804
-323
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ if (!file_exists(__DIR__ . '/import_debug.log')) {
|
|||||||
error_log("Inizio importazione alle " . date('Y-m-d H:i:s'));
|
error_log("Inizio importazione alle " . date('Y-m-d H:i:s'));
|
||||||
|
|
||||||
include('include/headscript.php');
|
include('include/headscript.php');
|
||||||
|
require_once __DIR__ . '/class/binding-functions.php';
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['template_id']) || !isset($_POST['selected_rows']) || !isset($_POST['filename'])) {
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['template_id']) || !isset($_POST['selected_rows']) || !isset($_POST['filename'])) {
|
||||||
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Richiesta non valida"));
|
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Richiesta non valida"));
|
||||||
@@ -20,10 +21,11 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['template_id']) || !i
|
|||||||
|
|
||||||
$template_id = intval($_POST['template_id']);
|
$template_id = intval($_POST['template_id']);
|
||||||
$selected_rows = array_map('intval', $_POST['selected_rows']);
|
$selected_rows = array_map('intval', $_POST['selected_rows']);
|
||||||
$columns = json_decode($_POST['columns'], true);
|
$columns = json_decode(urldecode($_POST['columns'] ?? '[]'), true);
|
||||||
$rows = json_decode($_POST['rows'], true);
|
$rows = json_decode(urldecode($_POST['rows'] ?? '[]'), true);
|
||||||
$excelrows = json_decode($_POST['excelrows'], true);
|
$excelrows = json_decode(urldecode($_POST['excelrows'] ?? '[]'), true);
|
||||||
$newFilename = htmlspecialchars($_POST['filename']);
|
|
||||||
|
$newFilename = $_POST['filename'];
|
||||||
|
|
||||||
$_SESSION['template_id'] = $template_id;
|
$_SESSION['template_id'] = $template_id;
|
||||||
$_SESSION['selected_rows'] = $selected_rows;
|
$_SESSION['selected_rows'] = $selected_rows;
|
||||||
@@ -39,6 +41,29 @@ error_log("Excelrows: " . json_encode($excelrows));
|
|||||||
|
|
||||||
$user_id = $iduserlogin ?? 1;
|
$user_id = $iduserlogin ?? 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizza un nome colonna per il confronto header Excel <-> mapping.
|
||||||
|
* Risolve alla radice i mismatch causati da caratteri invisibili:
|
||||||
|
* - non-breaking space (U+00A0), narrow NBSP (U+202F), figure space (U+2007), ecc. -> spazio normale
|
||||||
|
* - tab, newline e altri whitespace -> spazio normale
|
||||||
|
* - spazi multipli consecutivi -> un solo spazio
|
||||||
|
* - trim ai bordi
|
||||||
|
* NB: usata SOLO per il matching, non altera i valori importati.
|
||||||
|
*/
|
||||||
|
function normalizeColName($s): string
|
||||||
|
{
|
||||||
|
$s = (string)$s;
|
||||||
|
// Converte i principali spazi Unicode "insidiosi" in spazio normale
|
||||||
|
$s = str_replace(
|
||||||
|
["\xC2\xA0", "\xE2\x80\xAF", "\xE2\x80\x87", "\xE2\x80\x89", "\xE2\x80\x8A", "\xEF\xBB\xBF"],
|
||||||
|
' ',
|
||||||
|
$s
|
||||||
|
);
|
||||||
|
// Qualsiasi sequenza di whitespace (spazi, tab, newline) -> singolo spazio
|
||||||
|
$s = preg_replace('/\s+/u', ' ', $s);
|
||||||
|
return trim((string)$s);
|
||||||
|
}
|
||||||
|
|
||||||
$db = DBHandlerSelect::getInstance();
|
$db = DBHandlerSelect::getInstance();
|
||||||
$pdo = $db->getConnection();
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
@@ -46,7 +71,7 @@ $pdo = $db->getConnection();
|
|||||||
$importReferenceCode = date('YmdHis') . '-' . uniqid();
|
$importReferenceCode = date('YmdHis') . '-' . uniqid();
|
||||||
|
|
||||||
// Recupera tutti i mapping dal template
|
// Recupera tutti i mapping dal template
|
||||||
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field FROM template_mapping WHERE template_id = ?");
|
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, has_list, is_required, manual_default, is_manual, field_label, field_id, main_field, auto_value FROM template_mapping WHERE template_id = ?");
|
||||||
$stmt->execute([$template_id]);
|
$stmt->execute([$template_id]);
|
||||||
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
@@ -64,6 +89,23 @@ foreach ($allMappings as $mapping) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$fixedMappings = [];
|
||||||
|
try {
|
||||||
|
$fxStmt = $pdo->prepare(
|
||||||
|
"SELECT id, fixed_field_key, data_type, default_value, default_source, excel_column
|
||||||
|
FROM template_fixed_mapping
|
||||||
|
WHERE template_id = ?"
|
||||||
|
);
|
||||||
|
$fxStmt->execute([$template_id]);
|
||||||
|
$fixedMappings = $fxStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("[FIXED] lettura template_fixed_mapping fallita: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$savedBindings = []; // risolti con un binding gia' salvato
|
||||||
|
$autoBindings = []; // collegati in automatico (corrispondenza esatta)
|
||||||
|
$pendingBindings = []; // senza corrispondenza: da risolvere a mano
|
||||||
|
|
||||||
// Inserisci le righe selezionate in datadb
|
// Inserisci le righe selezionate in datadb
|
||||||
$insertedIds = [];
|
$insertedIds = [];
|
||||||
foreach ($selected_rows as $rowIndex) {
|
foreach ($selected_rows as $rowIndex) {
|
||||||
@@ -104,7 +146,12 @@ foreach ($selected_rows as $rowIndex) {
|
|||||||
$fieldValue = null;
|
$fieldValue = null;
|
||||||
if (!$mapping['is_manual']) {
|
if (!$mapping['is_manual']) {
|
||||||
$excelColumn = trim($mapping['excel_column']);
|
$excelColumn = trim($mapping['excel_column']);
|
||||||
$excelColumnIndex = array_search($excelColumn, array_map('trim', $columns));
|
// Match robusto: normalizza header e mapping (NBSP, spazi doppi, tab...)
|
||||||
|
// così un solo/doppio spazio o un non-breaking space non rompono il confronto.
|
||||||
|
$excelColumnIndex = array_search(
|
||||||
|
normalizeColName($excelColumn),
|
||||||
|
array_map('normalizeColName', $columns)
|
||||||
|
);
|
||||||
if ($excelColumnIndex !== false && isset($row[$excelColumnIndex]) && $row[$excelColumnIndex] !== '') {
|
if ($excelColumnIndex !== false && isset($row[$excelColumnIndex]) && $row[$excelColumnIndex] !== '') {
|
||||||
$fieldValue = $row[$excelColumnIndex];
|
$fieldValue = $row[$excelColumnIndex];
|
||||||
error_log("Found Excel column '$excelColumn' at index $excelColumnIndex, value: " . var_export($fieldValue, true));
|
error_log("Found Excel column '$excelColumn' at index $excelColumnIndex, value: " . var_export($fieldValue, true));
|
||||||
@@ -125,7 +172,7 @@ foreach ($selected_rows as $rowIndex) {
|
|||||||
case 'Testo':
|
case 'Testo':
|
||||||
case 'VARCHAR':
|
case 'VARCHAR':
|
||||||
default:
|
default:
|
||||||
$fieldValue = !empty($fieldValue) ? htmlspecialchars((string)$fieldValue) : ($mapping['manual_default'] ?? '');
|
$fieldValue = !empty($fieldValue) ? (string)$fieldValue : ($mapping['manual_default'] ?? '');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -134,6 +181,33 @@ foreach ($selected_rows as $rowIndex) {
|
|||||||
$fieldValue = date('Y-m-d');
|
$fieldValue = date('Y-m-d');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Apply auto_value if field is still empty
|
||||||
|
if (($fieldValue === null || $fieldValue === '') && !empty($mapping['auto_value']) && $mapping['auto_value'] !== 'none') {
|
||||||
|
if ($mapping['auto_value'] === 'import_date') {
|
||||||
|
$fieldValue = date('Y-m-d');
|
||||||
|
} elseif ($mapping['auto_value'] === 'import_time') {
|
||||||
|
$fieldValue = date('H:i');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
binding_is_list_field($mapping)
|
||||||
|
&& $fieldValue !== null
|
||||||
|
&& trim((string) $fieldValue) !== ''
|
||||||
|
) {
|
||||||
|
$fieldValue = binding_resolve_for_import(
|
||||||
|
$pdo,
|
||||||
|
$mapping,
|
||||||
|
(int) $template_id,
|
||||||
|
(string) $fieldValue,
|
||||||
|
(int) $iddatadb,
|
||||||
|
(int) $user_id,
|
||||||
|
$savedBindings,
|
||||||
|
$autoBindings,
|
||||||
|
$pendingBindings
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ($mapping['is_required'] && (is_null($fieldValue) || $fieldValue === '')) {
|
if ($mapping['is_required'] && (is_null($fieldValue) || $fieldValue === '')) {
|
||||||
error_log("Required field missing for mapping ID: " . $mapping['id'] . ", field: " . $mapping['field_label']);
|
error_log("Required field missing for mapping ID: " . $mapping['id'] . ", field: " . $mapping['field_label']);
|
||||||
}
|
}
|
||||||
@@ -142,32 +216,114 @@ foreach ($selected_rows as $rowIndex) {
|
|||||||
$stmt->execute([$iddatadb, $mapping['id'], $fieldValue]);
|
$stmt->execute([$iddatadb, $mapping['id'], $fieldValue]);
|
||||||
error_log("Inserted into import_data_details for ID $iddatadb, Mapping ID: " . $mapping['id'] . ", Field Value: " . var_export($fieldValue, true));
|
error_log("Inserted into import_data_details for ID $iddatadb, Mapping ID: " . $mapping['id'] . ", Field Value: " . var_export($fieldValue, true));
|
||||||
}
|
}
|
||||||
|
$fixedValues = [];
|
||||||
|
foreach ($fixedMappings as $fm) {
|
||||||
|
$fixedKey = (string) $fm['fixed_field_key'];
|
||||||
|
$value = null;
|
||||||
|
|
||||||
|
if (($fm['default_source'] ?? 'manual') === 'xls' && trim((string) ($fm['excel_column'] ?? '')) !== '') {
|
||||||
|
$idx = binding_find_column_index((string) $fm['excel_column'], $columns);
|
||||||
|
$raw = ($idx !== false && isset($row[$idx])) ? trim((string) $row[$idx]) : '';
|
||||||
|
|
||||||
|
if ($raw !== '') {
|
||||||
|
$value = binding_resolve_fixed_for_import(
|
||||||
|
$pdo,
|
||||||
|
$fm,
|
||||||
|
(int) $template_id,
|
||||||
|
$raw,
|
||||||
|
(int) $iddatadb,
|
||||||
|
(int) $user_id,
|
||||||
|
$savedBindings,
|
||||||
|
$autoBindings,
|
||||||
|
$pendingBindings
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value === null || $value === '') {
|
||||||
|
$default = binding_fixed_default($fm);
|
||||||
|
$value = $default !== '' ? $default : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value !== null && $value !== '') {
|
||||||
|
$fixedValues[$fixedKey] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($fixedValues)) {
|
||||||
|
binding_write_fixed_columns($pdo, (int) $iddatadb, $fixedValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// AUTO SET: schema field_id = 244 (Accettatore) con auth_users.lims_user_id
|
||||||
|
// Spostato dal vecchio flusso (import_edit.php) al flusso nuovo.
|
||||||
|
// Autocompila il custom field 244 con il lims_user_id dell'utente loggato.
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
try {
|
||||||
|
// 1) Leggi il lims_user_id dell'utente loggato
|
||||||
|
$stmtUser = $pdo->prepare("SELECT lims_user_id FROM auth_users WHERE id = ? LIMIT 1");
|
||||||
|
$stmtUser->execute([(int)$user_id]);
|
||||||
|
$limsUserId = $stmtUser->fetchColumn();
|
||||||
|
|
||||||
|
// normalize
|
||||||
|
$limsUserId = ($limsUserId !== false && $limsUserId !== null && $limsUserId !== '') ? (string)$limsUserId : '';
|
||||||
|
|
||||||
|
if ($limsUserId !== '' && !empty($insertedIds)) {
|
||||||
|
|
||||||
|
// 2) Trova il mapping_id per field_id = 244 in questo template
|
||||||
|
$stmtMap = $pdo->prepare("SELECT id FROM template_mapping WHERE template_id = ? AND field_id = 244 LIMIT 1");
|
||||||
|
$stmtMap->execute([(int)$template_id]);
|
||||||
|
$mappingId244 = (int)$stmtMap->fetchColumn();
|
||||||
|
|
||||||
|
if ($mappingId244 > 0) {
|
||||||
|
|
||||||
|
// 3) Upsert del valore in import_data_details per ogni riga inserita
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
$updStmt = $pdo->prepare("UPDATE import_data_details SET field_value = ? WHERE id = ? AND mapping_id = ?");
|
||||||
|
$insStmt = $pdo->prepare("INSERT INTO import_data_details (id, mapping_id, field_value) VALUES (?, ?, ?)");
|
||||||
|
|
||||||
|
foreach ($insertedIds as $iddatadb) {
|
||||||
|
$iddatadb = (int)$iddatadb;
|
||||||
|
if ($iddatadb <= 0) continue;
|
||||||
|
|
||||||
|
$updStmt->execute([$limsUserId, $iddatadb, $mappingId244]);
|
||||||
|
|
||||||
|
if ($updStmt->rowCount() === 0) {
|
||||||
|
$insStmt->execute([$iddatadb, $mappingId244, $limsUserId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
} else {
|
||||||
|
error_log("[AUTO FIELD 244] Nessun mapping field_id=244 per template $template_id");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error_log("[AUTO FIELD 244] limsUserId vuoto per user_id=$user_id o nessuna riga inserita");
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Non bloccare l'import se questo fallisce; logga soltanto
|
||||||
|
error_log("[AUTO FIELD 244] " . $e->getMessage());
|
||||||
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
}
|
}
|
||||||
|
|
||||||
$_SESSION['inserted_ids'] = $insertedIds;
|
$_SESSION['inserted_ids'] = $insertedIds;
|
||||||
|
|
||||||
$params = [
|
if (!empty($pendingBindings)) {
|
||||||
'template_id' => $template_id,
|
$_SESSION['pending_bindings'] = [
|
||||||
'filename' => $newFilename,
|
'template_id' => $template_id,
|
||||||
];
|
'importref' => $importReferenceCode,
|
||||||
|
'items' => array_values($pendingBindings),
|
||||||
|
'auto' => array_values($autoBindings),
|
||||||
|
'saved' => array_values($savedBindings),
|
||||||
|
];
|
||||||
|
|
||||||
?>
|
header("Location: resolve_bindings.php");
|
||||||
<form id="redirectForm" action="import_edit2.php" method="post">
|
exit;
|
||||||
<input type="hidden" name="template_id" value="<?= htmlspecialchars($template_id) ?>">
|
}
|
||||||
<input type="hidden" name="filename" value="<?= htmlspecialchars($newFilename) ?>">
|
|
||||||
<?php foreach ($selected_rows as $row): ?>
|
unset($_SESSION['pending_bindings']);
|
||||||
<input type="hidden" name="selected_rows[]" value="<?= htmlspecialchars($row) ?>">
|
|
||||||
<?php endforeach; ?>
|
header("Location: imported.php?id=" . urlencode($template_id) . "&importref=" . urlencode($importReferenceCode));
|
||||||
<?php foreach ($insertedIds as $id): ?>
|
|
||||||
<input type="hidden" name="inserted_ids[]" value="<?= htmlspecialchars($id) ?>">
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<input type="hidden" name="columns" value='<?= json_encode($columns) ?>'>
|
|
||||||
<input type="hidden" name="rows" value='<?= json_encode($rows) ?>'>
|
|
||||||
<input type="hidden" name="excelrows" value='<?= json_encode($excelrows) ?>'>
|
|
||||||
</form>
|
|
||||||
<script>
|
|
||||||
document.getElementById('redirectForm').submit();
|
|
||||||
</script>
|
|
||||||
<?php
|
|
||||||
exit;
|
exit;
|
||||||
?>
|
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
<?php
|
||||||
|
ini_set('display_errors', 0); // in AJAX gli errori devono NON sporcare il JSON
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
ini_set('log_errors', 1);
|
||||||
|
ini_set('error_log', __DIR__ . '/import_debug.log');
|
||||||
|
|
||||||
|
include('include/headscript.php');
|
||||||
|
require_once __DIR__ . '/class/binding-functions.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
// Legge il body JSON
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!$input ||
|
||||||
|
!isset($input['template_id']) ||
|
||||||
|
!isset($input['importreferencecode']) ||
|
||||||
|
!isset($input['filename']) ||
|
||||||
|
!isset($input['columns']) ||
|
||||||
|
!isset($input['batch_rows']) ||
|
||||||
|
!isset($input['batch_excelrows'])
|
||||||
|
) {
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Richiesta non valida']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$template_id = intval($input['template_id']);
|
||||||
|
$importReferenceCode = (string)$input['importreferencecode'];
|
||||||
|
$newFilename = (string)$input['filename'];
|
||||||
|
$columns = $input['columns']; // array
|
||||||
|
$batchRows = $input['batch_rows']; // array di righe (ognuna array di celle)
|
||||||
|
$batchExcelrows = $input['batch_excelrows']; // array di excelrow paralleli a batchRows
|
||||||
|
|
||||||
|
$user_id = $iduserlogin ?? 1;
|
||||||
|
|
||||||
|
function normalizeColName($s): string
|
||||||
|
{
|
||||||
|
$s = (string)$s;
|
||||||
|
$s = str_replace(
|
||||||
|
["\xC2\xA0", "\xE2\x80\xAF", "\xE2\x80\x87", "\xE2\x80\x89", "\xE2\x80\x8A", "\xEF\xBB\xBF"],
|
||||||
|
' ',
|
||||||
|
$s
|
||||||
|
);
|
||||||
|
$s = preg_replace('/\s+/u', ' ', $s);
|
||||||
|
return trim((string)$s);
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
// Recupera i mapping
|
||||||
|
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, has_list, is_required, manual_default, is_manual, field_label, field_id, main_field, auto_value FROM template_mapping WHERE template_id = ?");
|
||||||
|
$stmt->execute([$template_id]);
|
||||||
|
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (empty($allMappings)) {
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Nessun mapping trovato per il template']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// idclient di default (una volta sola)
|
||||||
|
$template_stmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
|
||||||
|
$template_stmt->execute([$template_id]);
|
||||||
|
$tpl = $template_stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$default_idclient = $tpl['idclient'] ?? null;
|
||||||
|
|
||||||
|
// lims_user_id per il campo 244 (una volta sola)
|
||||||
|
$stmtUser = $pdo->prepare("SELECT lims_user_id FROM auth_users WHERE id = ? LIMIT 1");
|
||||||
|
$stmtUser->execute([(int)$user_id]);
|
||||||
|
$limsUserId = $stmtUser->fetchColumn();
|
||||||
|
$limsUserId = ($limsUserId !== false && $limsUserId !== null && $limsUserId !== '') ? (string)$limsUserId : '';
|
||||||
|
|
||||||
|
$stmtMap = $pdo->prepare("SELECT id FROM template_mapping WHERE template_id = ? AND field_id = 244 LIMIT 1");
|
||||||
|
$stmtMap->execute([(int)$template_id]);
|
||||||
|
$mappingId244 = (int)$stmtMap->fetchColumn();
|
||||||
|
|
||||||
|
$insertedIds = [];
|
||||||
|
|
||||||
|
$fixedMappings = [];
|
||||||
|
try {
|
||||||
|
$fxStmt = $pdo->prepare(
|
||||||
|
"SELECT id, fixed_field_key, data_type, default_value, default_source, excel_column
|
||||||
|
FROM template_fixed_mapping
|
||||||
|
WHERE template_id = ?"
|
||||||
|
);
|
||||||
|
$fxStmt->execute([$template_id]);
|
||||||
|
$fixedMappings = $fxStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("[FIXED] lettura template_fixed_mapping fallita: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$savedBindings = [];
|
||||||
|
$autoBindings = [];
|
||||||
|
$pendingBindings = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
$insDatadb = $pdo->prepare("INSERT INTO datadb (templateid, importreferencecode, filename_import, status, user_id, limscode, importdate, excelrow, idclient) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
|
$insDetail = $pdo->prepare("INSERT INTO import_data_details (id, mapping_id, field_value) VALUES (?, ?, ?)");
|
||||||
|
|
||||||
|
foreach ($batchRows as $i => $row) {
|
||||||
|
$excelrow = $batchExcelrows[$i] ?? null;
|
||||||
|
if ($row === null || $excelrow === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$insDatadb->execute([
|
||||||
|
$template_id,
|
||||||
|
$importReferenceCode,
|
||||||
|
$newFilename,
|
||||||
|
'i',
|
||||||
|
$user_id,
|
||||||
|
null,
|
||||||
|
date('Y-m-d'),
|
||||||
|
$excelrow,
|
||||||
|
$default_idclient
|
||||||
|
]);
|
||||||
|
|
||||||
|
$iddatadb = $pdo->lastInsertId();
|
||||||
|
$insertedIds[] = $iddatadb;
|
||||||
|
|
||||||
|
foreach ($allMappings as $mapping) {
|
||||||
|
$fieldValue = null;
|
||||||
|
if (!$mapping['is_manual']) {
|
||||||
|
$excelColumn = trim($mapping['excel_column']);
|
||||||
|
$excelColumnIndex = array_search(
|
||||||
|
normalizeColName($excelColumn),
|
||||||
|
array_map('normalizeColName', $columns)
|
||||||
|
);
|
||||||
|
if ($excelColumnIndex !== false && isset($row[$excelColumnIndex]) && $row[$excelColumnIndex] !== '') {
|
||||||
|
$fieldValue = $row[$excelColumnIndex];
|
||||||
|
} else {
|
||||||
|
$fieldValue = $mapping['manual_default'] ?? '';
|
||||||
|
}
|
||||||
|
switch ($mapping['data_type']) {
|
||||||
|
case 'INT':
|
||||||
|
$fieldValue = is_numeric($fieldValue) ? (int)$fieldValue : ($mapping['manual_default'] ?? 0);
|
||||||
|
break;
|
||||||
|
case 'DATE':
|
||||||
|
$fieldValue = !empty($fieldValue) ? date('Y-m-d', strtotime($fieldValue)) : ($mapping['manual_default'] === 'today' ? date('Y-m-d') : ($mapping['manual_default'] ?? ''));
|
||||||
|
break;
|
||||||
|
case 'CHAR':
|
||||||
|
$fieldValue = !empty($fieldValue) ? substr((string)$fieldValue, 0, 1) : ($mapping['manual_default'] ?? '');
|
||||||
|
break;
|
||||||
|
case 'Testo':
|
||||||
|
case 'VARCHAR':
|
||||||
|
default:
|
||||||
|
$fieldValue = !empty($fieldValue) ? (string)$fieldValue : ($mapping['manual_default'] ?? '');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$fieldValue = $mapping['manual_default'] ?? '';
|
||||||
|
if ($mapping['data_type'] === 'DATE' && $mapping['manual_default'] === 'today') {
|
||||||
|
$fieldValue = date('Y-m-d');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (($fieldValue === null || $fieldValue === '') && !empty($mapping['auto_value']) && $mapping['auto_value'] !== 'none') {
|
||||||
|
if ($mapping['auto_value'] === 'import_date') {
|
||||||
|
$fieldValue = date('Y-m-d');
|
||||||
|
} elseif ($mapping['auto_value'] === 'import_time') {
|
||||||
|
$fieldValue = date('H:i');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
binding_is_list_field($mapping)
|
||||||
|
&& $fieldValue !== null
|
||||||
|
&& trim((string) $fieldValue) !== ''
|
||||||
|
) {
|
||||||
|
$fieldValue = binding_resolve_for_import(
|
||||||
|
$pdo,
|
||||||
|
$mapping,
|
||||||
|
$template_id,
|
||||||
|
(string) $fieldValue,
|
||||||
|
(int) $iddatadb,
|
||||||
|
(int) $user_id,
|
||||||
|
$savedBindings,
|
||||||
|
$autoBindings,
|
||||||
|
$pendingBindings
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$insDetail->execute([$iddatadb, $mapping['id'], $fieldValue]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$fixedValues = [];
|
||||||
|
foreach ($fixedMappings as $fm) {
|
||||||
|
$fixedKey = (string) $fm['fixed_field_key'];
|
||||||
|
$value = null;
|
||||||
|
|
||||||
|
if (($fm['default_source'] ?? 'manual') === 'xls' && trim((string) ($fm['excel_column'] ?? '')) !== '') {
|
||||||
|
$idx = binding_find_column_index((string) $fm['excel_column'], $columns);
|
||||||
|
$raw = ($idx !== false && isset($row[$idx])) ? trim((string) $row[$idx]) : '';
|
||||||
|
|
||||||
|
if ($raw !== '') {
|
||||||
|
$value = binding_resolve_fixed_for_import(
|
||||||
|
$pdo,
|
||||||
|
$fm,
|
||||||
|
(int) $template_id,
|
||||||
|
$raw,
|
||||||
|
(int) $iddatadb,
|
||||||
|
(int) $user_id,
|
||||||
|
$savedBindings,
|
||||||
|
$autoBindings,
|
||||||
|
$pendingBindings
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value === null || $value === '') {
|
||||||
|
$default = binding_fixed_default($fm);
|
||||||
|
$value = $default !== '' ? $default : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value !== null && $value !== '') {
|
||||||
|
$fixedValues[$fixedKey] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($fixedValues)) {
|
||||||
|
binding_write_fixed_columns($pdo, (int) $iddatadb, $fixedValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Campo 244 (Accettatore) con lims_user_id
|
||||||
|
if ($limsUserId !== '' && $mappingId244 > 0) {
|
||||||
|
$insDetail->execute([$iddatadb, $mappingId244, $limsUserId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
$state = $_SESSION['pending_bindings'] ?? null;
|
||||||
|
if (!is_array($state) || ($state['importref'] ?? null) !== $importReferenceCode) {
|
||||||
|
$state = [
|
||||||
|
'template_id' => $template_id,
|
||||||
|
'importref' => $importReferenceCode,
|
||||||
|
'items' => [],
|
||||||
|
'auto' => [],
|
||||||
|
'saved' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$state['items'] = binding_merge_buckets($state['items'], $pendingBindings);
|
||||||
|
$state['auto'] = binding_merge_buckets($state['auto'], $autoBindings);
|
||||||
|
$state['saved'] = binding_merge_buckets($state['saved'], $savedBindings);
|
||||||
|
|
||||||
|
$_SESSION['pending_bindings'] = $state;
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => true,
|
||||||
|
'inserted' => count($insertedIds),
|
||||||
|
'pending_bindings' => count($state['items'])
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
|
error_log("[BATCH IMPORT] " . $e->getMessage());
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
+389
-46
@@ -40,6 +40,25 @@ error_log("Loaded template: " . print_r($template, true));
|
|||||||
<?php include('cssinclude.php'); ?>
|
<?php include('cssinclude.php'); ?>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
|
.top-scrollbar {
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
width: 100%;
|
||||||
|
height: 18px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
display: none;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1020;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-scrollbar-inner {
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-container {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -121,6 +140,16 @@ error_log("Loaded template: " . print_r($template, true));
|
|||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.column-filters th {
|
||||||
|
background: #ffffff;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-filters input {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<title><?= htmlspecialchars($template['name']) ?> - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
<title><?= htmlspecialchars($template['name']) ?> - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||||
</head>
|
</head>
|
||||||
@@ -133,16 +162,21 @@ error_log("Loaded template: " . print_r($template, true));
|
|||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<?php include('top_stat_widget.php'); ?>
|
<?php include('top_stat_widget.php'); ?>
|
||||||
<div class="mb-3 text">
|
<div class="mb-3 text">
|
||||||
<a href="historical_trf.php?id=<?= $id ?>&status=i" class="btn btn-warning me-2">Imported (i)</a>
|
<a href="imported.php?id=<?= $id ?>" class="btn btn-warning me-2">Imported (i)</a>
|
||||||
<a href="historical_trf.php?id=<?= $id ?>&status=P" class="btn btn-primary me-2">In Progress (P)</a>
|
<a href="tolims.php?id=<?= $id ?>" class="btn btn-success">To LIMS (l)</a>
|
||||||
<a href="historical_trf.php?id=<?= $id ?>&status=l" class="btn btn-success">To LIMS (l)</a>
|
<a href="errors.php?id=<?= $id ?>" class="btn btn-danger">Errors (x)</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card radius-10">
|
<div class="card radius-10">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div>
|
<div>
|
||||||
<h6 class="mb-0"><?= htmlspecialchars($template['name']) ?></h6>
|
<h6 class="mb-0"><?= htmlspecialchars($template['name']) ?></h6>
|
||||||
<small>Template ID: <?= $id ?>, Start Row: <?= $template['header_row'] ?>, Start Column: <?= $template['start_column'] ?></small>
|
<small>
|
||||||
|
Template ID: <?= $id ?>,
|
||||||
|
Sheet Number: <?= (int)($template['xls_sheet_index'] ?? 0) ?>,
|
||||||
|
Start Row: <?= $template['header_row'] ?>,
|
||||||
|
Start Column: <?= $template['start_column'] ?>
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -219,8 +253,9 @@ error_log("Loaded template: " . print_r($template, true));
|
|||||||
const templateId = <?= $id ?>;
|
const templateId = <?= $id ?>;
|
||||||
console.log('Template ID passed to formData:', templateId);
|
console.log('Template ID passed to formData:', templateId);
|
||||||
formData.append('template_id', templateId);
|
formData.append('template_id', templateId);
|
||||||
formData.append('header_row', <?= $template['header_row'] ?>);
|
formData.append('header_row', <?= (int)$template['header_row'] ?>);
|
||||||
formData.append('start_column', <?= $template['start_column'] ?>);
|
formData.append('start_column', <?= json_encode($template['start_column']) ?>);
|
||||||
|
formData.append('xls_sheet_index', <?= (int)($template['xls_sheet_index'] ?? 0) ?>);
|
||||||
|
|
||||||
fetch('process_import_xls2.php', {
|
fetch('process_import_xls2.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -303,57 +338,330 @@ error_log("Loaded template: " . print_r($template, true));
|
|||||||
function showTable(data) {
|
function showTable(data) {
|
||||||
console.log('Mostro tabella con dati:', data);
|
console.log('Mostro tabella con dati:', data);
|
||||||
let html = `
|
let html = `
|
||||||
<form id="selectRowsForm" action="import_insert.php" method="POST">
|
<form id="selectRowsForm" action="import_insert.php" method="POST">
|
||||||
<input type="hidden" name="template_id" value="${data.template_id}">
|
<input type="hidden" name="template_id" value="${data.template_id}">
|
||||||
<input type="hidden" name="columns" value='${JSON.stringify(data.columns)}'>
|
<input type="hidden" name="columns" value="${encodeURIComponent(JSON.stringify(data.columns))}">
|
||||||
<input type="hidden" name="rows" value='${JSON.stringify(data.rows)}'>
|
<input type="hidden" name="rows" id="selectedRowsData" value="">
|
||||||
<input type="hidden" name="excelrows" value='${JSON.stringify(data.excel_data.map(row => row.excelrow))}'>
|
<input type="hidden" name="excelrows" id="selectedExcelRowsData" value="">
|
||||||
<input type="hidden" name="filename" value="${data.filename}">
|
<input type="hidden" name="filename" value="${data.filename}">
|
||||||
<div class="search-container">
|
|
||||||
<input type="text" id="searchInput" class="form-control" placeholder="Cerca nelle righe...">
|
<!-- TOP BUTTON -->
|
||||||
</div>
|
<div class="d-flex justify-content-end align-items-center mb-3 gap-2">
|
||||||
<div class="table-container">
|
<span class="badge bg-secondary" id="selectedCountTop">0 selezionati</span>
|
||||||
<table class="table table-striped table-bordered">
|
<button type="submit" class="btn btn-primary" id="proceedButtonTop" disabled>Prosegui</button>
|
||||||
<thead>
|
</div>
|
||||||
<tr>
|
|
||||||
<th><input type="checkbox" id="selectAll"> Seleziona</th>
|
<div class="top-scrollbar" id="topTableScrollbar">
|
||||||
${data.columns.map(col => `<th>${col || 'Colonna senza nome'}<div class="resize-handle"></div></th>`).join('')}
|
<div class="top-scrollbar-inner" id="topTableScrollbarInner"></div>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
|
||||||
<tbody>
|
<div class="table-container" id="mainTableContainer">
|
||||||
${data.excel_data.map((row, index) => `
|
<table class="table table-striped table-bordered" id="importPreviewTable">
|
||||||
<tr>
|
<thead>
|
||||||
<td><input type="checkbox" class="row-checkbox" name="selected_rows[]" value="${index}" data-excelrow="${row.excelrow}"></td>
|
<tr>
|
||||||
${row.data.map(cell => `<td>${cell}</td>`).join('')}
|
<th><input type="checkbox" id="selectAll"> Seleziona</th>
|
||||||
</tr>
|
${data.columns.map(col => {
|
||||||
`).join('')}
|
const label = !col ? 'Colonna senza nome' : (col.match(/^__empty_\d+__$/) ? 'Colonna senza nome' : col);
|
||||||
</tbody>
|
return `<th>${label}<div class="resize-handle"></div></th>`;
|
||||||
</table>
|
}).join('')}
|
||||||
</div>
|
</tr>
|
||||||
<button type="submit" class="btn btn-primary mt-3" id="proceedButton" disabled>Prosegui</button>
|
<tr class="column-filters">
|
||||||
</form>
|
<th></th>
|
||||||
`;
|
${data.columns.map((col, i) => `
|
||||||
|
<th>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control form-control-sm column-filter"
|
||||||
|
data-col-index="${i}"
|
||||||
|
placeholder="Filter...">
|
||||||
|
</th>
|
||||||
|
`).join('')}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${data.excel_data.map((row, index) => `
|
||||||
|
<tr>
|
||||||
|
<td><input type="checkbox" class="row-checkbox" name="selected_rows[]" value="${index}" data-excelrow="${row.excelrow}"></td>
|
||||||
|
${row.data.map(cell => `<td>${cell}</td>`).join('')}
|
||||||
|
</tr>
|
||||||
|
`).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BOTTOM BUTTON -->
|
||||||
|
<div class="d-flex align-items-center mt-3 gap-2">
|
||||||
|
<span class="badge bg-secondary" id="selectedCountBottom">0 selezionati</span>
|
||||||
|
<button type="submit" class="btn btn-primary" id="proceedButtonBottom" disabled>Prosegui</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
tableContainer.innerHTML = html;
|
tableContainer.innerHTML = html;
|
||||||
|
|
||||||
const proceedButton = document.getElementById('proceedButton');
|
const selectRowsForm = document.getElementById('selectRowsForm');
|
||||||
|
|
||||||
|
// Clic sulla cella (oltre che sulla checkbox) per selezionare/deselezionare la riga
|
||||||
|
document.querySelectorAll('#importPreviewTable tbody tr').forEach(tr => {
|
||||||
|
tr.style.cursor = 'pointer';
|
||||||
|
tr.addEventListener('click', function(e) {
|
||||||
|
// Se ho cliccato proprio sulla checkbox, lascio fare a lei (evita doppio toggle)
|
||||||
|
if (e.target.classList.contains('row-checkbox')) return;
|
||||||
|
|
||||||
|
const cb = this.querySelector('.row-checkbox');
|
||||||
|
if (!cb) return;
|
||||||
|
cb.checked = !cb.checked;
|
||||||
|
cb.dispatchEvent(new Event('change', {
|
||||||
|
bubbles: true
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
selectRowsForm.addEventListener('submit', async function(e) {
|
||||||
|
e.preventDefault(); // gestiamo tutto via AJAX a batch
|
||||||
|
|
||||||
|
const checkedBoxes = Array.from(document.querySelectorAll('.row-checkbox:checked'));
|
||||||
|
|
||||||
|
if (checkedBoxes.length === 0) {
|
||||||
|
alert('Seleziona almeno una riga.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raccoglie le righe selezionate (riga mappata + excelrow parallelo)
|
||||||
|
const selectedRows = [];
|
||||||
|
const selectedExcelRows = [];
|
||||||
|
|
||||||
|
checkedBoxes.forEach(cb => {
|
||||||
|
const originalIndex = parseInt(cb.value, 10);
|
||||||
|
if (data.rows && data.rows[originalIndex]) {
|
||||||
|
selectedRows.push(data.rows[originalIndex]);
|
||||||
|
selectedExcelRows.push(
|
||||||
|
(data.excel_data && data.excel_data[originalIndex]) ?
|
||||||
|
data.excel_data[originalIndex].excelrow :
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const total = selectedRows.length;
|
||||||
|
if (total === 0) {
|
||||||
|
alert('Nessuna riga valida da importare.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// importreferencecode generato UNA volta, condiviso da tutti i batch
|
||||||
|
const importReferenceCode =
|
||||||
|
new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14) +
|
||||||
|
'-' + Math.random().toString(36).slice(2, 10);
|
||||||
|
|
||||||
|
const BATCH_SIZE = 20;
|
||||||
|
|
||||||
|
// Disabilita i pulsanti e mostra la barra di progresso
|
||||||
|
const btnTop = document.getElementById('proceedButtonTop');
|
||||||
|
const btnBottom = document.getElementById('proceedButtonBottom');
|
||||||
|
if (btnTop) btnTop.disabled = true;
|
||||||
|
if (btnBottom) btnBottom.disabled = true;
|
||||||
|
|
||||||
|
const progressHtml = `
|
||||||
|
<div id="importProgressWrap" style="
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
z-index: 20000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;">
|
||||||
|
<div style="
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 25px 30px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0,0,0,0.3);">
|
||||||
|
<div class="mb-2"><strong>Importazione in corso...</strong>
|
||||||
|
<span id="importProgressText" class="float-end">0 / ${total}</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress" style="height: 26px;">
|
||||||
|
<div id="importProgressBar" class="progress-bar progress-bar-striped progress-bar-animated"
|
||||||
|
role="progressbar" style="width: 0%;">0%</div>
|
||||||
|
</div>
|
||||||
|
<div id="importProgressError" class="alert alert-danger mt-3 mb-0" style="display:none;"></div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
document.body.insertAdjacentHTML('beforeend', progressHtml);
|
||||||
|
|
||||||
|
const progressBar = document.getElementById('importProgressBar');
|
||||||
|
const progressText = document.getElementById('importProgressText');
|
||||||
|
const progressErr = document.getElementById('importProgressError');
|
||||||
|
|
||||||
|
let insertedTotal = 0;
|
||||||
|
let pendingBindings = 0; // valori a lista senza corrispondenza LIMS
|
||||||
|
|
||||||
|
// Invia un batch e ritorna la risposta JSON
|
||||||
|
async function sendBatch(startIndex) {
|
||||||
|
const batchRows = selectedRows.slice(startIndex, startIndex + BATCH_SIZE);
|
||||||
|
const batchExcelrows = selectedExcelRows.slice(startIndex, startIndex + BATCH_SIZE);
|
||||||
|
|
||||||
|
const resp = await fetch('import_insert_batch.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
template_id: data.template_id,
|
||||||
|
importreferencecode: importReferenceCode,
|
||||||
|
filename: data.filename,
|
||||||
|
columns: data.columns,
|
||||||
|
batch_rows: batchRows,
|
||||||
|
batch_excelrows: batchExcelrows
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cicla i batch in sequenza
|
||||||
|
for (let start = 0; start < total; start += BATCH_SIZE) {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = await sendBatch(start);
|
||||||
|
} catch (err) {
|
||||||
|
result = {
|
||||||
|
ok: false,
|
||||||
|
error: err.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
// Batch fallito: chiediamo all'utente se continuare
|
||||||
|
progressErr.textContent =
|
||||||
|
'Errore su un blocco (righe ' + (start + 1) + '-' +
|
||||||
|
Math.min(start + BATCH_SIZE, total) + '): ' + (result.error || 'errore sconosciuto');
|
||||||
|
progressErr.style.display = 'block';
|
||||||
|
|
||||||
|
const continua = confirm(
|
||||||
|
'Un blocco di righe non è stato importato.\n\n' +
|
||||||
|
'Righe inserite finora: ' + insertedTotal + ' di ' + total + '.\n\n' +
|
||||||
|
'Vuoi continuare con i blocchi successivi? (Annulla = interrompi)'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!continua) {
|
||||||
|
progressBar.classList.remove('progress-bar-animated');
|
||||||
|
progressBar.classList.add('bg-warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// se continua, prosegue col prossimo blocco
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
insertedTotal += (result.inserted || 0);
|
||||||
|
pendingBindings = result.pending_bindings || pendingBindings;
|
||||||
|
const done = Math.min(start + BATCH_SIZE, total);
|
||||||
|
const pct = Math.round((done / total) * 100);
|
||||||
|
progressBar.style.width = pct + '%';
|
||||||
|
progressBar.textContent = pct + '%';
|
||||||
|
progressText.textContent = insertedTotal + ' / ' + total;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fine: barra verde e redirect a imported.php
|
||||||
|
progressBar.classList.remove('progress-bar-animated', 'progress-bar-striped');
|
||||||
|
progressBar.classList.add('bg-success');
|
||||||
|
progressText.textContent = insertedTotal + ' / ' + total + ' completate';
|
||||||
|
|
||||||
|
// PAUSA temporanea di 2 secondi per vedere la barra completa (da rimuovere dopo il test)
|
||||||
|
await new Promise(r => setTimeout(r, 2000));
|
||||||
|
|
||||||
|
window.location.href = pendingBindings > 0
|
||||||
|
? 'resolve_bindings.php'
|
||||||
|
: 'imported.php?id=' +
|
||||||
|
encodeURIComponent(data.template_id) +
|
||||||
|
'&importref=' + encodeURIComponent(importReferenceCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
const topTableScrollbar = document.getElementById('topTableScrollbar');
|
||||||
|
const topTableScrollbarInner = document.getElementById('topTableScrollbarInner');
|
||||||
|
const mainTableContainer = document.getElementById('mainTableContainer');
|
||||||
|
const importPreviewTable = document.getElementById('importPreviewTable');
|
||||||
|
|
||||||
|
function updateTopTableScrollbar() {
|
||||||
|
if (!topTableScrollbar || !topTableScrollbarInner || !mainTableContainer || !importPreviewTable) return;
|
||||||
|
|
||||||
|
topTableScrollbarInner.style.width = importPreviewTable.scrollWidth + 'px';
|
||||||
|
|
||||||
|
if (mainTableContainer.scrollWidth > mainTableContainer.clientWidth) {
|
||||||
|
topTableScrollbar.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
topTableScrollbar.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let syncingTop = false;
|
||||||
|
let syncingBottom = false;
|
||||||
|
|
||||||
|
if (topTableScrollbar && mainTableContainer) {
|
||||||
|
topTableScrollbar.addEventListener('scroll', function() {
|
||||||
|
if (syncingBottom) return;
|
||||||
|
syncingTop = true;
|
||||||
|
mainTableContainer.scrollLeft = topTableScrollbar.scrollLeft;
|
||||||
|
syncingTop = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
mainTableContainer.addEventListener('scroll', function() {
|
||||||
|
if (syncingTop) return;
|
||||||
|
syncingBottom = true;
|
||||||
|
topTableScrollbar.scrollLeft = mainTableContainer.scrollLeft;
|
||||||
|
syncingBottom = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTopTableScrollbar();
|
||||||
|
setTimeout(updateTopTableScrollbar, 100);
|
||||||
|
setTimeout(updateTopTableScrollbar, 300);
|
||||||
|
window.addEventListener('resize', updateTopTableScrollbar);
|
||||||
|
|
||||||
|
const proceedButtonTop = document.getElementById('proceedButtonTop');
|
||||||
|
const proceedButtonBottom = document.getElementById('proceedButtonBottom');
|
||||||
const selectAllCheckbox = document.getElementById('selectAll');
|
const selectAllCheckbox = document.getElementById('selectAll');
|
||||||
const checkboxes = document.querySelectorAll('.row-checkbox');
|
const checkboxes = document.querySelectorAll('.row-checkbox');
|
||||||
|
|
||||||
function updateProceedButton() {
|
function updateProceedButton() {
|
||||||
proceedButton.disabled = !Array.from(checkboxes).some(cb => cb.checked);
|
const checkedCount = Array.from(checkboxes).filter(cb => cb.checked).length;
|
||||||
|
const enabled = checkedCount > 0;
|
||||||
|
|
||||||
|
if (proceedButtonTop) proceedButtonTop.disabled = !enabled;
|
||||||
|
if (proceedButtonBottom) proceedButtonBottom.disabled = !enabled;
|
||||||
|
|
||||||
|
const label = checkedCount + ' selezionati';
|
||||||
|
const countTop = document.getElementById('selectedCountTop');
|
||||||
|
const countBottom = document.getElementById('selectedCountBottom');
|
||||||
|
if (countTop) countTop.textContent = label;
|
||||||
|
if (countBottom) countBottom.textContent = label;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectAllCheckbox.addEventListener('change', function() {
|
selectAllCheckbox.addEventListener('change', function() {
|
||||||
checkboxes.forEach(checkbox => {
|
const visibleRows = Array.from(document.querySelectorAll('.table tbody tr'))
|
||||||
checkbox.checked = this.checked;
|
.filter(row => row.style.display !== 'none');
|
||||||
|
|
||||||
|
visibleRows.forEach(row => {
|
||||||
|
const checkbox = row.querySelector('.row-checkbox');
|
||||||
|
if (checkbox) {
|
||||||
|
checkbox.checked = this.checked;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
updateProceedButton();
|
updateProceedButton();
|
||||||
});
|
});
|
||||||
|
|
||||||
checkboxes.forEach(checkbox => {
|
checkboxes.forEach(checkbox => {
|
||||||
checkbox.addEventListener('change', function() {
|
checkbox.addEventListener('change', function() {
|
||||||
console.log('Checkbox changed, checked:', this.checked, 'excelrow:', this.dataset.excelrow);
|
console.log('Checkbox changed, checked:', this.checked, 'excelrow:', this.dataset.excelrow);
|
||||||
selectAllCheckbox.checked = Array.from(checkboxes).every(cb => cb.checked);
|
|
||||||
|
const visibleCheckboxes = Array.from(document.querySelectorAll('.table tbody tr'))
|
||||||
|
.filter(row => row.style.display !== 'none')
|
||||||
|
.map(row => row.querySelector('.row-checkbox'))
|
||||||
|
.filter(cb => cb !== null);
|
||||||
|
|
||||||
|
selectAllCheckbox.checked =
|
||||||
|
visibleCheckboxes.length > 0 &&
|
||||||
|
visibleCheckboxes.every(cb => cb.checked);
|
||||||
|
|
||||||
updateProceedButton();
|
updateProceedButton();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -393,14 +701,49 @@ error_log("Loaded template: " . print_r($template, true));
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchInput = document.getElementById('searchInput');
|
|
||||||
const rows = document.querySelectorAll('.table tbody tr');
|
const rows = document.querySelectorAll('.table tbody tr');
|
||||||
|
const filterInputs = document.querySelectorAll('.column-filter');
|
||||||
|
|
||||||
searchInput.addEventListener('input', function() {
|
// Stato filtri: key = colIndex, value = testo
|
||||||
const searchTerm = this.value.toLowerCase();
|
const activeFilters = {};
|
||||||
|
|
||||||
|
function applyColumnFilters() {
|
||||||
rows.forEach(row => {
|
rows.forEach(row => {
|
||||||
const text = Array.from(row.cells).slice(1).map(cell => cell.textContent.toLowerCase()).join(' ');
|
let visible = true;
|
||||||
row.style.display = text.includes(searchTerm) ? '' : 'none';
|
|
||||||
|
for (const [colIndexStr, filterValue] of Object.entries(activeFilters)) {
|
||||||
|
const colIndex = parseInt(colIndexStr, 10);
|
||||||
|
const cell = row.cells[colIndex + 1];
|
||||||
|
|
||||||
|
const cellText = (cell?.textContent || '').toLowerCase();
|
||||||
|
const searchText = (filterValue || '').toLowerCase().trim();
|
||||||
|
|
||||||
|
if (searchText && !cellText.includes(searchText)) {
|
||||||
|
visible = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
row.style.display = visible ? '' : 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
const visibleCheckboxes = Array.from(document.querySelectorAll('.table tbody tr'))
|
||||||
|
.filter(row => row.style.display !== 'none')
|
||||||
|
.map(row => row.querySelector('.row-checkbox'))
|
||||||
|
.filter(cb => cb !== null);
|
||||||
|
|
||||||
|
selectAllCheckbox.checked =
|
||||||
|
visibleCheckboxes.length > 0 &&
|
||||||
|
visibleCheckboxes.every(cb => cb.checked);
|
||||||
|
|
||||||
|
updateProceedButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
filterInputs.forEach(input => {
|
||||||
|
input.addEventListener('input', function() {
|
||||||
|
const colIndex = this.dataset.colIndex; // string
|
||||||
|
activeFilters[colIndex] = this.value;
|
||||||
|
applyColumnFilters();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,213 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
if (!function_exists('gdbFixedDefaultValue')) {
|
||||||
|
/** Default di un fixed field (DATE 'today' → data odierna). */
|
||||||
|
function gdbFixedDefaultValue(array $f): string
|
||||||
|
{
|
||||||
|
$v = $f['default_value'] ?? '';
|
||||||
|
if (($f['data_type'] ?? '') === 'DATE' && $v === 'today') {
|
||||||
|
return date('Y-m-d');
|
||||||
|
}
|
||||||
|
return (string)$v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('buildGridConfig')) {
|
||||||
|
/**
|
||||||
|
* Config del template necessaria per costruire le righe: fixed fields (ordinati
|
||||||
|
* come in imported.php), main field mappings, alias map, default idclient.
|
||||||
|
*
|
||||||
|
* @return array{fixedFields:array, fixedAliasMap:array, mainFieldMappings:array, default_idclient:mixed}
|
||||||
|
*/
|
||||||
|
function buildGridConfig(PDO $pdo, int $templateId): array
|
||||||
|
{
|
||||||
|
// Mappa logica fixed_field_key → colonna reale su datadb (come imported.php)
|
||||||
|
$fixedAliasMap = [
|
||||||
|
'ClienteResponsabile' => 'cliente_responsabile_id',
|
||||||
|
'ClienteFornitore' => 'cliente_fornitore_id',
|
||||||
|
'ClienteAnalisi' => 'clienteAnalisi',
|
||||||
|
'ClienteFatturazione' => 'ClienteFatturazione',
|
||||||
|
'MoltiplicatorePrezzo' => 'moltiplicatore_prezzo_id',
|
||||||
|
'AnagraficaCertestObject' => 'anagrafica_certest_object_id',
|
||||||
|
'AnagraficaCertestService' => 'anagrafica_certest_service_id',
|
||||||
|
'ConsegnaRichiesta' => 'consegna_richiesta',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Fixed fields visibili, ordinati come imported.php
|
||||||
|
$fixedStmt = $pdo->prepare("
|
||||||
|
SELECT id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import
|
||||||
|
FROM template_fixed_mapping
|
||||||
|
WHERE template_id = ? AND is_visible_import = 1
|
||||||
|
ORDER BY id
|
||||||
|
");
|
||||||
|
$fixedStmt->execute([$templateId]);
|
||||||
|
$fixedFieldsRaw = $fixedStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$desiredOrder = [
|
||||||
|
'ClienteResponsabile', 'ClienteFornitore', 'ClienteAnalisi', 'ClienteFatturazione',
|
||||||
|
'AnagraficaCertestObject', 'AnagraficaCertestService', 'MoltiplicatorePrezzo', 'ConsegnaRichiesta',
|
||||||
|
];
|
||||||
|
$excludeFromFixed = ['ClienteFornitore']; // reso come colonna a sé
|
||||||
|
|
||||||
|
$fixedFields = [];
|
||||||
|
$tempMap = [];
|
||||||
|
foreach ($fixedFieldsRaw as $f) {
|
||||||
|
if (in_array($f['fixed_field_key'], $excludeFromFixed, true)) continue;
|
||||||
|
$tempMap[$f['fixed_field_key']] = $f;
|
||||||
|
}
|
||||||
|
foreach ($desiredOrder as $key) {
|
||||||
|
if (isset($tempMap[$key])) {
|
||||||
|
$fixedFields[] = $tempMap[$key];
|
||||||
|
unset($tempMap[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($tempMap as $f) {
|
||||||
|
$fixedFields[] = $f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main field mappings (max 2), come imported.php
|
||||||
|
$mapStmt = $pdo->prepare("
|
||||||
|
SELECT id, field_label, data_type, is_required, field_id, field_order,
|
||||||
|
main_field, is_visible_import, manual_default
|
||||||
|
FROM template_mapping
|
||||||
|
WHERE template_id = ?
|
||||||
|
ORDER BY field_order ASC, id ASC
|
||||||
|
");
|
||||||
|
$mapStmt->execute([$templateId]);
|
||||||
|
$allMappings = $mapStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$mainFieldMappings = [];
|
||||||
|
foreach ($allMappings as $mapping) {
|
||||||
|
if ((string)$mapping['main_field'] === '1' && (int)$mapping['is_visible_import'] === 1) {
|
||||||
|
$mainFieldMappings[] = $mapping;
|
||||||
|
}
|
||||||
|
if (count($mainFieldMappings) >= 2) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default idclient dal template
|
||||||
|
$tplStmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
|
||||||
|
$tplStmt->execute([$templateId]);
|
||||||
|
$default_idclient = $tplStmt->fetchColumn();
|
||||||
|
$default_idclient = $default_idclient !== false ? $default_idclient : null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'fixedFields' => $fixedFields,
|
||||||
|
'fixedAliasMap' => $fixedAliasMap,
|
||||||
|
'mainFieldMappings' => $mainFieldMappings,
|
||||||
|
'default_idclient' => $default_idclient,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('buildGridRows')) {
|
||||||
|
/**
|
||||||
|
* Costruisce le righe gridData per la lista di iddatadb data, nell'ordine passato.
|
||||||
|
*
|
||||||
|
* @param int[] $iddatadbList
|
||||||
|
* @param array $config output di buildGridConfig()
|
||||||
|
* @return array righe nella stessa forma di imported.php $gridDataArray
|
||||||
|
*/
|
||||||
|
function buildGridRows(PDO $pdo, array $iddatadbList, array $config): array
|
||||||
|
{
|
||||||
|
$iddatadbList = array_values(array_filter(array_map('intval', $iddatadbList), fn($v) => $v > 0));
|
||||||
|
if (empty($iddatadbList)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$fixedFields = $config['fixedFields'];
|
||||||
|
$fixedAliasMap = $config['fixedAliasMap'];
|
||||||
|
$mainFieldMappings = $config['mainFieldMappings'];
|
||||||
|
$default_idclient = $config['default_idclient'];
|
||||||
|
|
||||||
|
$ph = implode(',', array_fill(0, count($iddatadbList), '?'));
|
||||||
|
|
||||||
|
// Righe datadb + user_name
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT d.*, CONCAT(u.first_name, ' ', u.last_name) AS user_name
|
||||||
|
FROM datadb d
|
||||||
|
LEFT JOIN auth_users u ON d.user_id = u.id
|
||||||
|
WHERE d.iddatadb IN ($ph)
|
||||||
|
");
|
||||||
|
$stmt->execute($iddatadbList);
|
||||||
|
$byId = [];
|
||||||
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||||
|
$byId[(int)$r['iddatadb']] = $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dettagli (import_data_details)
|
||||||
|
$detStmt = $pdo->prepare("
|
||||||
|
SELECT d.id AS datadb_id, d.mapping_id, d.field_value,
|
||||||
|
m.field_id, m.field_label, m.data_type, m.is_required, m.manual_default
|
||||||
|
FROM import_data_details d
|
||||||
|
JOIN template_mapping m ON d.mapping_id = m.id
|
||||||
|
WHERE d.id IN ($ph)
|
||||||
|
");
|
||||||
|
$detStmt->execute($iddatadbList);
|
||||||
|
$detailsByRow = [];
|
||||||
|
foreach ($detStmt->fetchAll(PDO::FETCH_ASSOC) as $d) {
|
||||||
|
$detailsByRow[(int)$d['datadb_id']][] = $d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Costruzione righe nell'ordine della lista passata
|
||||||
|
$rows = [];
|
||||||
|
foreach ($iddatadbList as $id) {
|
||||||
|
$row = $byId[$id] ?? null;
|
||||||
|
if ($row === null) continue;
|
||||||
|
|
||||||
|
$rowObj = [
|
||||||
|
'iddatadb' => (int)$row['iddatadb'],
|
||||||
|
'status' => $row['status'] ?? 'i',
|
||||||
|
'idclient' => $row['idclient'] ?? $default_idclient,
|
||||||
|
'cliente_fornitore_id' => $row['cliente_fornitore_id'] ?? null,
|
||||||
|
'tested_component' => $row['tested_component'] ?? '',
|
||||||
|
'commessaweb' => $row['commessaweb'] ?? null,
|
||||||
|
'user_name' => $row['user_name'] ?? '',
|
||||||
|
'importreferencecode' => $row['importreferencecode'] ?? '',
|
||||||
|
'filename_import' => $row['filename_import'] ?? '',
|
||||||
|
'importdate' => $row['importdate'] ?? '',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Fixed fields
|
||||||
|
$rowObj['fixedFields'] = [];
|
||||||
|
foreach ($fixedFields as $f) {
|
||||||
|
$key = $f['fixed_field_key'];
|
||||||
|
$dbCol = $fixedAliasMap[$key] ?? $key;
|
||||||
|
$val = $row[$dbCol] ?? '';
|
||||||
|
if ($val === '' || $val === null) {
|
||||||
|
$val = gdbFixedDefaultValue($f);
|
||||||
|
}
|
||||||
|
$rowObj['fixedFields'][$key] = (string)$val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Details
|
||||||
|
$rowObj['details'] = [];
|
||||||
|
$rowDetails = $detailsByRow[$id] ?? [];
|
||||||
|
foreach ($rowDetails as $d) {
|
||||||
|
$rowObj['details'][(string)$d['mapping_id']] = $d['field_value'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main field values
|
||||||
|
foreach ($mainFieldMappings as $mainMapping) {
|
||||||
|
$found = null;
|
||||||
|
foreach ($rowDetails as $d) {
|
||||||
|
if ($d['mapping_id'] == $mainMapping['id']) {
|
||||||
|
$found = $d;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$rowObj['details'][(string)$mainMapping['id']] =
|
||||||
|
($found['field_value'] ?? null) ?? ($mainMapping['manual_default'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($mainFieldMappings)) {
|
||||||
|
$firstMain = $mainFieldMappings[0];
|
||||||
|
$rowObj['mainFieldValue'] = $rowObj['details'][(string)$firstMain['id']] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$rowObj['_dirty'] = false;
|
||||||
|
$rows[] = $rowObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,11 +5,14 @@ $db = DBHandlerSelect::getInstance()->getConnection();
|
|||||||
|
|
||||||
ini_set('display_errors', 1);
|
ini_set('display_errors', 1);
|
||||||
ini_set('display_startup_errors', 1);
|
ini_set('display_startup_errors', 1);
|
||||||
error_reporting(E_ALL | E_STRICT);
|
error_reporting(E_ALL);
|
||||||
// This should be equal to: PATH_TO_VANGUARD_FOLDER/extra/auth.php
|
// This should be equal to: PATH_TO_VANGUARD_FOLDER/extra/auth.php
|
||||||
include('../../extra/auth.php');
|
include('../../extra/auth.php');
|
||||||
//require_once __DIR__ . '/extra/auth.php';
|
//require_once __DIR__ . '/extra/auth.php';
|
||||||
|
|
||||||
|
// Laravel bootstrap (loaded by auth.php) forces UTC via config/app.php — re-apply our TZ
|
||||||
|
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
|
||||||
|
|
||||||
// Here we just check if user is not
|
// Here we just check if user is not
|
||||||
// logged in, and in that case we redirect
|
// logged in, and in that case we redirect
|
||||||
// the user to vanguard login page.
|
// the user to vanguard login page.
|
||||||
@@ -26,6 +29,8 @@ $nameuser = $user->present()->first_name;
|
|||||||
$surnameuser = $user->present()->last_name;
|
$surnameuser = $user->present()->last_name;
|
||||||
$emailuser = $user->present()->email;
|
$emailuser = $user->present()->email;
|
||||||
$avatar = $user->present()->avatar;
|
$avatar = $user->present()->avatar;
|
||||||
|
$lims_user_id = $user->lims_user_id ?? '';
|
||||||
|
$lims_global_user_id = $user->lims_global_user_id ?? '';
|
||||||
|
|
||||||
$kindofrole = $user->present()->role_id;
|
$kindofrole = $user->present()->role_id;
|
||||||
|
|
||||||
@@ -35,8 +40,6 @@ $kindofrole = $user->present()->role_id;
|
|||||||
//$iduserlogin="1";
|
//$iduserlogin="1";
|
||||||
//$nameuser="Claudio";
|
//$nameuser="Claudio";
|
||||||
//$emailuser="info@claudiosironi.com";
|
//$emailuser="info@claudiosironi.com";
|
||||||
?>
|
|
||||||
<?php
|
|
||||||
if (session_status() == PHP_SESSION_NONE) {
|
if (session_status() == PHP_SESSION_NONE) {
|
||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
@@ -49,13 +52,16 @@ $_SESSION["emailuser"] = $emailuser;
|
|||||||
$_SESSION["photouser"] = $avatar;
|
$_SESSION["photouser"] = $avatar;
|
||||||
$photouser = $_SESSION["photouser"];
|
$photouser = $_SESSION["photouser"];
|
||||||
$photousername = basename($avatar);
|
$photousername = basename($avatar);
|
||||||
?>
|
|
||||||
|
|
||||||
|
|
||||||
<?php //include files
|
|
||||||
|
|
||||||
|
//include files
|
||||||
require_once(__DIR__ . '/../../languages/en/general.php');
|
require_once(__DIR__ . '/../../languages/en/general.php');
|
||||||
|
|
||||||
//include("generalsettings.php");
|
//include("generalsettings.php");
|
||||||
|
|
||||||
?>
|
// Timbra la sessione dell'utente a ogni pagina (rolling manuale)
|
||||||
|
try {
|
||||||
|
$pdoSess = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
$pdoSess->prepare("UPDATE auth_sessions SET last_activity = ? WHERE user_id = ? ORDER BY last_activity DESC LIMIT 1")
|
||||||
|
->execute([time(), $iduserlogin]);
|
||||||
|
} catch (\Throwable $e) { /* non bloccare la pagina */
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,102 +6,176 @@
|
|||||||
<div>
|
<div>
|
||||||
<h4 class="logo-text"><?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></h4>
|
<h4 class="logo-text"><?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="toggle-icon ms-auto"><i class='bx bx-arrow-back'></i>
|
<div class="toggle-icon ms-auto">
|
||||||
|
<i class='bx bx-arrow-back'></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--navigation-->
|
<!--navigation-->
|
||||||
<ul class="metismenu" id="menu">
|
<ul class="metismenu" id="menu">
|
||||||
<!-- user, admin, superuser menù -->
|
|
||||||
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('User')) || (Auth::user()->hasRole('Superuser'))) : ?>
|
<?php
|
||||||
|
$currentUser = Auth::user();
|
||||||
|
|
||||||
|
$isAdmin = $currentUser->hasRole('Admin');
|
||||||
|
$isSuperUser = $currentUser->hasRole('SuperUser');
|
||||||
|
$isUser = $currentUser->hasRole('User');
|
||||||
|
|
||||||
|
$canAccessMainMenu = $isAdmin || $isSuperUser || $isUser;
|
||||||
|
$canAccessUserAdmin = $isAdmin || $isSuperUser;
|
||||||
|
$canAccessTemplates = $isAdmin || $isSuperUser;
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- user, admin, superuser menu -->
|
||||||
|
<?php if ($canAccessMainMenu) : ?>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="javascript:;" class="has-arrow">
|
<a href="javascript:;" class="has-arrow">
|
||||||
<div class="parent-icon"><i class='bx bx-home-alt'></i>
|
<div class="parent-icon">
|
||||||
|
<i class='bx bx-home-alt'></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-title">Dashboard</div>
|
<div class="menu-title">Dashboard</div>
|
||||||
</a>
|
</a>
|
||||||
<ul>
|
<ul>
|
||||||
<!-- <li> <a href="index.php"><i class='bx bx-radio-circle'></i>Default</a>
|
<li>
|
||||||
</li> -->
|
<a href="import_dashboard.php">
|
||||||
<li> <a href="import_dashboard.php"><i class='bx bx-radio-circle'></i>XLS Import</a>
|
<i class='bx bx-radio-circle'></i>XLS Import
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<a href="javascript:;" class="has-arrow">
|
|
||||||
<div class="parent-icon"><i class="bx bx-category"></i>
|
|
||||||
</div>
|
|
||||||
<div class="menu-title">Templates</div>
|
|
||||||
</a>
|
|
||||||
<ul>
|
|
||||||
<li> <a href="templates_dashboard.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?></a>
|
|
||||||
</li>
|
|
||||||
<li> <a href="insert_template_xls.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?></a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
</ul>
|
<?php if ($canAccessTemplates) : ?>
|
||||||
</li>
|
<li>
|
||||||
|
<a href="javascript:;" class="has-arrow">
|
||||||
|
<div class="parent-icon">
|
||||||
|
<i class="bx bx-category"></i>
|
||||||
|
</div>
|
||||||
|
<div class="menu-title">Templates</div>
|
||||||
|
</a>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="templates_dashboard.php">
|
||||||
|
<i class='bx bx-radio-circle'></i><?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="insert_template_xls.php">
|
||||||
|
<i class='bx bx-radio-circle'></i><?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="bindings_manage.php">
|
||||||
|
<i class='bx bx-radio-circle'></i><?= htmlspecialchars($bindingsmanage ?? 'Value Bindings', ENT_QUOTES, 'UTF-8'); ?>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="javascript:;" class="has-arrow">
|
<a href="javascript:;" class="has-arrow">
|
||||||
<div class="parent-icon"><i class="bx bx-category"></i>
|
<div class="parent-icon">
|
||||||
|
<i class="bx bx-category"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-title">Other Functions</div>
|
<div class="menu-title">Other Functions</div>
|
||||||
</a>
|
</a>
|
||||||
<ul>
|
<ul>
|
||||||
<li> <a href="quotations.php"><i class='bx bx-radio-circle'></i><?php echo $quotationstitle; ?></a>
|
<li>
|
||||||
|
<a href="quotations.php">
|
||||||
|
<i class='bx bx-radio-circle'></i><?= htmlspecialchars($quotationstitle, ENT_QUOTES, 'UTF-8'); ?>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<?php if ($canAccessUserAdmin) : ?>
|
||||||
|
<li class="menu-label">Administration</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a href="user-admin.php">
|
||||||
|
<div class="parent-icon">
|
||||||
|
<i class="bx bx-user-plus"></i>
|
||||||
|
</div>
|
||||||
|
<div class="menu-title">User Admin</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="stats_export_lims.php">
|
||||||
|
<div class="parent-icon">
|
||||||
|
<i class="bx bx-bar-chart-alt-2"></i>
|
||||||
|
</div>
|
||||||
|
<div class="menu-title">Stats</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="routine_admin.php">
|
||||||
|
<div class="parent-icon">
|
||||||
|
<i class="bx bx-bar-chart-alt-2"></i>
|
||||||
|
</div>
|
||||||
|
<div class="menu-title">Routines</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="warm_cache_run.php">
|
||||||
|
<div class="parent-icon">
|
||||||
|
<i class="bx bx-bar-chart-alt-2"></i>
|
||||||
|
</div>
|
||||||
|
<div class="menu-title">Warm Cache</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<li class="menu-label">Others</li>
|
<li class="menu-label">Others</li>
|
||||||
|
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="https://helpdesk.cesoft.io" target="_blank">
|
<a href="https://helpdesk.cesoft.io" target="_blank">
|
||||||
<div class="parent-icon"><i class="bx bx-support"></i>
|
<div class="parent-icon">
|
||||||
|
<i class="bx bx-support"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-title">Support</div>
|
<div class="menu-title">Support</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<?php
|
|
||||||
endif; ?>
|
<?php endif; ?>
|
||||||
<!-- admin, superuser menù -->
|
|
||||||
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('Superuser'))) : ?>
|
|
||||||
<?php
|
<!-- admin only menu -->
|
||||||
endif; ?>
|
<?php if ($isAdmin) : ?>
|
||||||
<!-- admin menù -->
|
|
||||||
<?php if (Auth::user()->hasRole('Admin')) : ?>
|
|
||||||
<li class="menu-label">Admin Menù</li>
|
<li class="menu-label">Admin Menù</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="../" target="_blank">
|
<a href="../" target="_blank">
|
||||||
<div class="parent-icon"><i class="bx bx-support"></i>
|
<div class="parent-icon">
|
||||||
|
<i class="bx bx-support"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-title">User Management</div>
|
<div class="menu-title">User Management</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<!-- <li>
|
|
||||||
|
<!--
|
||||||
|
<li>
|
||||||
<a href="template/index.html" target="_blank">
|
<a href="template/index.html" target="_blank">
|
||||||
<div class="parent-icon"><i class="bx bx-support"></i>
|
<div class="parent-icon">
|
||||||
|
<i class="bx bx-support"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-title">Template</div>
|
<div class="menu-title">Template</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="https://codervent.com/rocker/documentation/index.html" target="_blank">
|
<a href="https://codervent.com/rocker/documentation/index.html" target="_blank">
|
||||||
<div class="parent-icon"><i class="bx bx-folder"></i>
|
<div class="parent-icon">
|
||||||
|
<i class="bx bx-folder"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-title">Documentation</div>
|
<div class="menu-title">Documentation</div>
|
||||||
</a>
|
</a>
|
||||||
</li> -->
|
</li>
|
||||||
<?php
|
-->
|
||||||
endif; ?>
|
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
<!--end navigation-->
|
<!--end navigation-->
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
$popupEnabled = (($_ENV['POPUP_ENABLED'] ?? getenv('POPUP_ENABLED')) === 'true');
|
||||||
|
|
||||||
|
if ($popupEnabled):
|
||||||
|
?>
|
||||||
|
<div id="app-popup-overlay" style="position:fixed;inset:0;background:rgba(0,0,0,.65);display:flex;align-items:center;justify-content:center;z-index:99999;padding:20px;">
|
||||||
|
<div style="background:#fff;max-width:640px;width:100%;max-height:90vh;overflow-y:auto;padding:40px;border-radius:14px;position:relative;box-shadow:0 20px 60px rgba(0,0,0,.4);text-align:center;">
|
||||||
|
|
||||||
|
<div style="font-size:46px;line-height:1;margin-bottom:10px;">🚀</div>
|
||||||
|
|
||||||
|
<h2 style="margin:0 0 8px;font-size:26px;color:#111;">Nuovo sito in produzione</h2>
|
||||||
|
<p style="font-size:16px;color:#444;margin:0 0 20px;">Il portale si è spostato su un nuovo indirizzo.</p>
|
||||||
|
|
||||||
|
<div style="background:#f1f5ff;border:1px solid #c9d8ff;border-radius:10px;padding:16px;margin-bottom:24px;">
|
||||||
|
<p style="margin:0 0 4px;font-size:13px;color:#667;text-transform:uppercase;letter-spacing:.5px;">Nuovo indirizzo</p>
|
||||||
|
<p style="margin:0;font-size:19px;font-weight:700;color:#1d4ed8;word-break:break-all;">https://trfsmart-bv.cesoft.io/</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="https://trfsmart-bv.cesoft.io/"
|
||||||
|
style="display:inline-block;padding:16px 36px;background:#2563eb;color:#fff;text-decoration:none;border-radius:8px;font-size:18px;font-weight:600;box-shadow:0 4px 14px rgba(37,99,235,.35);">
|
||||||
|
Clicca qui per accedere al nuovo sito
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<p style="font-size:13px;color:#888;margin:16px 0 0;">Ti consigliamo di salvare il nuovo indirizzo nei preferiti.</p>
|
||||||
|
|
||||||
|
<hr style="border:none;border-top:1px solid #eee;margin:28px 0 20px;">
|
||||||
|
|
||||||
|
<div style="background:#fff8e6;border:1px solid #ffe08a;border-radius:10px;padding:16px 18px;text-align:left;margin-bottom:20px;">
|
||||||
|
<p style="margin:0;font-size:14px;color:#8a6d00;">
|
||||||
|
<strong>⚠️ Attenzione:</strong> chiudi questo messaggio <u>solo</u> se riscontri problemi
|
||||||
|
con il nuovo sito. Continuando su questo portale userai la versione precedente.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onclick="chiudiPopupMigrazione()"
|
||||||
|
style="padding:11px 22px;background:#f0f0f0;color:#555;border:1px solid #ddd;border-radius:8px;cursor:pointer;font-size:14px;">
|
||||||
|
Ho problemi col nuovo sito, resto su questo portale
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function chiudiPopupMigrazione() {
|
||||||
|
var conferma = confirm(
|
||||||
|
"Sei sicuro di voler restare sul portale precedente?\n\n" +
|
||||||
|
"Il nuovo sito è quello ufficiale in produzione. " +
|
||||||
|
"Chiudi questo avviso solo se hai riscontrato un problema reale sul nuovo sito."
|
||||||
|
);
|
||||||
|
if (conferma) {
|
||||||
|
document.getElementById('app-popup-overlay').remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<?php endif; ?>
|
||||||
@@ -1,8 +1,22 @@
|
|||||||
<header>
|
<header>
|
||||||
<div class="topbar d-flex align-items-center">
|
<div class="topbar d-flex align-items-center">
|
||||||
<nav class="navbar navbar-expand gap-3">
|
|
||||||
<div class="mobile-toggle-menu"><i class='bx bx-menu'></i>
|
<?php
|
||||||
|
// Controllo ambiente TEST
|
||||||
|
$testEnv = strtolower($_ENV['TEST_ENVIRONMENT'] ?? getenv('TEST_ENVIRONMENT') ?? 'true');
|
||||||
|
|
||||||
|
if ($testEnv === 'true'):
|
||||||
|
?>
|
||||||
|
<div class="alert alert-danger border-0 rounded-0 mb-0 py-2 text-center fw-bold fs-5 w-100 shadow-sm"
|
||||||
|
style="margin: 0 -15px 0 -15px;">
|
||||||
|
<i class='bx bx-test-tube me-2'></i>
|
||||||
|
AMBIENTE DI TEST — NON È PRODUZIONE
|
||||||
</div>
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<nav class="navbar navbar-expand gap-3 w-100">
|
||||||
|
<!-- Tutto il resto del tuo codice della topbar rimane identico -->
|
||||||
|
<div class="mobile-toggle-menu"><i class='bx bx-menu'></i></div>
|
||||||
|
|
||||||
<div class="search-bar d-lg-block d-none" data-bs-toggle="modal" data-bs-target="#SearchModal">
|
<div class="search-bar d-lg-block d-none" data-bs-toggle="modal" data-bs-target="#SearchModal">
|
||||||
<!-- <a href="avascript:;" class="btn d-flex align-items-center"><i class='bx bx-search'></i>Search</a> -->
|
<!-- <a href="avascript:;" class="btn d-flex align-items-center"><i class='bx bx-search'></i>Search</a> -->
|
||||||
@@ -94,8 +108,16 @@
|
|||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li><a class="dropdown-item d-flex align-items-center" href="user-profile.php"><i class="bx bx-user fs-5"></i><span>Profile</span></a>
|
<li><a class="dropdown-item d-flex align-items-center" href="user-profile.php"><i class="bx bx-user fs-5"></i><span>Profile</span></a>
|
||||||
</li>
|
</li>
|
||||||
<li><a class="dropdown-item d-flex align-items-center" href="settings.php"><i class="bx bx-cog fs-5"></i><span>Settings</span></a>
|
<li><a class="dropdown-item d-flex align-items-center" href="notification_settings.php"><i class="bx bx-user fs-5"></i><span>Notifications</span></a>
|
||||||
</li>
|
</li>
|
||||||
|
<?php if ($user->hasRole('Admin') || $user->hasRole('SuperUser')): ?>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item d-flex align-items-center" href="user-admin.php">
|
||||||
|
<i class="bx bx-user-plus fs-5"></i>
|
||||||
|
<span>User Admin</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endif; ?>
|
||||||
<li>
|
<li>
|
||||||
<div class="dropdown-divider mb-0"></div>
|
<div class="dropdown-divider mb-0"></div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
<?php include('include/headscript.php');
|
<?php include('include/headscript.php');
|
||||||
|
|
||||||
// Recupera tutte le routine dal database
|
// Retrieve all routines from database
|
||||||
$db = DBHandlerSelect::getInstance();
|
$db = DBHandlerSelect::getInstance();
|
||||||
$pdo = $db->getConnection();
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT * FROM routine");
|
$stmt = $pdo->prepare("SELECT * FROM routine");
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
$routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Retrieve active API/JSON configurations
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT id, name, provider_code, api_type, php_class_name
|
||||||
|
FROM api_configurations
|
||||||
|
WHERE is_active = 1
|
||||||
|
ORDER BY name ASC
|
||||||
|
");
|
||||||
|
$stmt->execute();
|
||||||
|
$apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@@ -18,26 +29,30 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
<?php include('cssinclude.php'); ?>
|
<?php include('cssinclude.php'); ?>
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||||
<title>Insert XLS Template <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
<title>Insert Template <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
|
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="mb-0">Insert new XLS Template</h5>
|
<h5 class="mb-0">Insert New Template</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="mb-2">Fill the following form in order to create a new import XLS template</p>
|
<p class="mb-2">Fill the following form in order to create a new import template</p>
|
||||||
<p class="mb-2">Mandatory Fields</p>
|
<p class="mb-2">Mandatory Fields</p>
|
||||||
<ul class="mb-0">
|
<ul class="mb-0">
|
||||||
<li>Template Name</li>
|
<li>Template Name</li>
|
||||||
<li>Row Header and Column Header: where the title of the excel starts</li>
|
<li>Source Type</li>
|
||||||
<li>Schema and client</li>
|
<li>Schema and Client</li>
|
||||||
|
<li>Row Header, Column Header and Sheet Number only for XLS templates</li>
|
||||||
|
<li>API / JSON Configuration only for API / JSON templates</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,22 +65,86 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<form id="insertTemplateForm" method="POST">
|
<form id="insertTemplateForm" method="POST">
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label"><?= htmlspecialchars($templatename, ENT_QUOTES, 'UTF-8'); ?> *</label>
|
<label class="form-label"><?= htmlspecialchars($templatename, ENT_QUOTES, 'UTF-8'); ?> *</label>
|
||||||
<input type="text" name="name" class="form-control" required>
|
<input type="text" name="name" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label"><?= htmlspecialchars($rowheader, ENT_QUOTES, 'UTF-8'); ?> *</label>
|
<label class="form-label">Source Type *</label>
|
||||||
<input type="number" name="header_row" class="form-control" required>
|
<select name="source_type" id="sourceType" class="form-control" required>
|
||||||
|
<option value="XLS" selected>XLS</option>
|
||||||
|
<option value="API">API / JSON</option>
|
||||||
|
<option value="PDF">PDF</option>
|
||||||
|
</select>
|
||||||
|
<small class="text-muted">Choose the source used by this template</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3" id="headerRowWrapper">
|
||||||
|
<label class="form-label"><?= htmlspecialchars($rowheader, ENT_QUOTES, 'UTF-8'); ?> *</label>
|
||||||
|
<input type="number" name="header_row" id="headerRow" class="form-control" value="1" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3" id="startColumnWrapper">
|
||||||
<label class="form-label"><?= htmlspecialchars($columnheader, ENT_QUOTES, 'UTF-8'); ?> *</label>
|
<label class="form-label"><?= htmlspecialchars($columnheader, ENT_QUOTES, 'UTF-8'); ?> *</label>
|
||||||
<input type="text" name="start_column" class="form-control" required>
|
<input type="text" name="start_column" id="startColumn" class="form-control" value="A" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3" id="xlsSheetNumberWrapper">
|
||||||
|
<label class="form-label">XLS Sheet Number</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="xls_sheet_index"
|
||||||
|
id="xlsSheetIndex"
|
||||||
|
class="form-control"
|
||||||
|
min="0"
|
||||||
|
value="0">
|
||||||
|
<small class="text-muted">
|
||||||
|
Use 0 for the first sheet, 1 for the second sheet, 2 for the third sheet, and so on.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3" id="apiConfigWrapper" style="display: none;">
|
||||||
|
<label class="form-label">API / JSON Configuration *</label>
|
||||||
|
<select name="api_config_id" id="apiConfigSelect" class="form-control">
|
||||||
|
<option value="">Select an API configuration...</option>
|
||||||
|
|
||||||
|
<?php foreach ($apiConfigurations as $apiConfig): ?>
|
||||||
|
<?php
|
||||||
|
$apiLabelParts = [];
|
||||||
|
|
||||||
|
if (!empty($apiConfig['name'])) {
|
||||||
|
$apiLabelParts[] = $apiConfig['name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($apiConfig['provider_code'])) {
|
||||||
|
$apiLabelParts[] = '[' . $apiConfig['provider_code'] . ']';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($apiConfig['api_type'])) {
|
||||||
|
$apiLabelParts[] = '(' . $apiConfig['api_type'] . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($apiConfig['php_class_name'])) {
|
||||||
|
$apiLabelParts[] = '- ' . $apiConfig['php_class_name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiLabel = implode(' ', $apiLabelParts);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<option value="<?php echo (int)$apiConfig['id']; ?>">
|
||||||
|
<?php echo htmlspecialchars($apiLabel, ENT_QUOTES, 'UTF-8'); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<small class="text-muted">
|
||||||
|
Select the API/JSON configuration linked to this template.
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -86,12 +165,12 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Button Background Color</label>
|
<label class="form-label">Button Background Color</label>
|
||||||
<input type="color" name="button_bg_color" class="form-control" value="#007bff">
|
<input type="color" name="button_bg_color" class="form-control form-control-color" value="#007bff">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Button Text Color</label>
|
<label class="form-label">Button Text Color</label>
|
||||||
<input type="color" name="button_text_color" class="form-control" value="#ffffff">
|
<input type="color" name="button_text_color" class="form-control form-control-color" value="#ffffff">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -125,6 +204,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div id="routineDetails" class="mt-2" style="display: none;">
|
<div id="routineDetails" class="mt-2" style="display: none;">
|
||||||
<h6>Routine Details</h6>
|
<h6>Routine Details</h6>
|
||||||
<p><strong>Name:</strong> <span id="routineName"></span></p>
|
<p><strong>Name:</strong> <span id="routineName"></span></p>
|
||||||
@@ -142,8 +222,10 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overlay toggle-icon"></div>
|
<div class="overlay toggle-icon"></div>
|
||||||
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
||||||
<?php include('include/footer.php'); ?>
|
<?php include('include/footer.php'); ?>
|
||||||
@@ -167,6 +249,18 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
const routineAction2 = document.getElementById("routineAction2");
|
const routineAction2 = document.getElementById("routineAction2");
|
||||||
const routineAction3 = document.getElementById("routineAction3");
|
const routineAction3 = document.getElementById("routineAction3");
|
||||||
|
|
||||||
|
const sourceType = document.getElementById("sourceType");
|
||||||
|
|
||||||
|
const headerRowWrapper = document.getElementById("headerRowWrapper");
|
||||||
|
const startColumnWrapper = document.getElementById("startColumnWrapper");
|
||||||
|
const xlsSheetNumberWrapper = document.getElementById("xlsSheetNumberWrapper");
|
||||||
|
const apiConfigWrapper = document.getElementById("apiConfigWrapper");
|
||||||
|
|
||||||
|
const headerRow = document.getElementById("headerRow");
|
||||||
|
const startColumn = document.getElementById("startColumn");
|
||||||
|
const xlsSheetIndex = document.getElementById("xlsSheetIndex");
|
||||||
|
const apiConfigSelect = document.getElementById("apiConfigSelect");
|
||||||
|
|
||||||
if (!form || !clientLoadingStatus || !schemaLoadingStatus || !routineSelect || !routineDetails) {
|
if (!form || !clientLoadingStatus || !schemaLoadingStatus || !routineSelect || !routineDetails) {
|
||||||
alert("Errore: Uno o più elementi della pagina non sono stati trovati. Contatta l'amministratore.");
|
alert("Errore: Uno o più elementi della pagina non sono stati trovati. Contatta l'amministratore.");
|
||||||
return;
|
return;
|
||||||
@@ -187,30 +281,101 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
allowClear: true
|
allowClear: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#apiConfigSelect').select2({
|
||||||
|
placeholder: "Select an API configuration...",
|
||||||
|
allowClear: true
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateSourceFields() {
|
||||||
|
const selectedSource = sourceType.value;
|
||||||
|
|
||||||
|
const isXls = selectedSource === 'XLS';
|
||||||
|
const isApiJson = selectedSource === 'API';
|
||||||
|
|
||||||
|
if (isXls) {
|
||||||
|
headerRowWrapper.style.display = 'block';
|
||||||
|
startColumnWrapper.style.display = 'block';
|
||||||
|
xlsSheetNumberWrapper.style.display = 'block';
|
||||||
|
|
||||||
|
headerRow.required = true;
|
||||||
|
startColumn.required = true;
|
||||||
|
xlsSheetIndex.required = true;
|
||||||
|
|
||||||
|
headerRow.disabled = false;
|
||||||
|
startColumn.disabled = false;
|
||||||
|
xlsSheetIndex.disabled = false;
|
||||||
|
|
||||||
|
apiConfigWrapper.style.display = 'none';
|
||||||
|
apiConfigSelect.required = false;
|
||||||
|
apiConfigSelect.disabled = true;
|
||||||
|
$('#apiConfigSelect').val(null).trigger('change');
|
||||||
|
} else {
|
||||||
|
headerRowWrapper.style.display = 'none';
|
||||||
|
startColumnWrapper.style.display = 'none';
|
||||||
|
xlsSheetNumberWrapper.style.display = 'none';
|
||||||
|
|
||||||
|
headerRow.required = false;
|
||||||
|
startColumn.required = false;
|
||||||
|
xlsSheetIndex.required = false;
|
||||||
|
|
||||||
|
headerRow.disabled = true;
|
||||||
|
startColumn.disabled = true;
|
||||||
|
xlsSheetIndex.disabled = true;
|
||||||
|
|
||||||
|
if (isApiJson) {
|
||||||
|
apiConfigWrapper.style.display = 'block';
|
||||||
|
apiConfigSelect.required = true;
|
||||||
|
apiConfigSelect.disabled = false;
|
||||||
|
} else {
|
||||||
|
apiConfigWrapper.style.display = 'none';
|
||||||
|
apiConfigSelect.required = false;
|
||||||
|
apiConfigSelect.disabled = true;
|
||||||
|
$('#apiConfigSelect').val(null).trigger('change');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceType.addEventListener('change', updateSourceFields);
|
||||||
|
updateSourceFields();
|
||||||
|
|
||||||
async function loadClients() {
|
async function loadClients() {
|
||||||
try {
|
try {
|
||||||
clientLoadingStatus.style.display = 'inline';
|
clientLoadingStatus.style.display = 'inline';
|
||||||
clientLoadingStatus.textContent = 'Recupero clienti in corso...';
|
clientLoadingStatus.textContent = 'Recupero clienti in corso...';
|
||||||
|
|
||||||
const response = await fetch("get_clienti.php", {
|
const response = await fetch("get_clienti.php", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (!response.ok) throw new Error(data.error || `Errore HTTP: ${response.status}`);
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || `Errore HTTP: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
const select = document.getElementById("clientSelect");
|
const select = document.getElementById("clientSelect");
|
||||||
select.innerHTML = '<option value="">Select a client...</option>';
|
select.innerHTML = '<option value="">Select a client...</option>';
|
||||||
|
|
||||||
data.value.forEach(client => {
|
data.value.forEach(client => {
|
||||||
const nome = client.Nominativo || "Nome non disponibile";
|
const nome = client.Nominativo || "Nome non disponibile";
|
||||||
const id = client.IdCliente || "ID non disponibile";
|
const id = client.IdCliente || "ID non disponibile";
|
||||||
const option = new Option(`${nome.trim()} (ID: ${id})`, id);
|
|
||||||
|
const codiceCliente = (client.CodiceCliente ?? client.codiceCliente ?? "").toString().trim();
|
||||||
|
const suffix = (codiceCliente.split("_")[1] || "").trim();
|
||||||
|
const shortCode = suffix || (codiceCliente ? codiceCliente.charAt(0) : "--");
|
||||||
|
|
||||||
|
const option = new Option(`${nome.trim()} - ${shortCode} (ID: ${id})`, id);
|
||||||
select.add(option);
|
select.add(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
$(select).trigger('change');
|
$(select).trigger('change');
|
||||||
clientLoadingStatus.textContent = "Clienti caricati.";
|
clientLoadingStatus.textContent = "Clienti caricati.";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
clientLoadingStatus.textContent = "Errore nel caricamento.";
|
clientLoadingStatus.textContent = "Errore nel caricamento.";
|
||||||
|
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: "Errore!",
|
title: "Errore!",
|
||||||
text: "Impossibile caricare i clienti: " + error.message,
|
text: "Impossibile caricare i clienti: " + error.message,
|
||||||
@@ -226,26 +391,43 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
try {
|
try {
|
||||||
schemaLoadingStatus.style.display = 'inline';
|
schemaLoadingStatus.style.display = 'inline';
|
||||||
schemaLoadingStatus.textContent = 'Caricamento schemi in corso...';
|
schemaLoadingStatus.textContent = 'Caricamento schemi in corso...';
|
||||||
|
|
||||||
const response = await fetch("get_schemi.php", {
|
const response = await fetch("get_schemi.php", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (!response.ok) throw new Error(data.error || `Errore HTTP: ${response.status}`);
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || `Errore HTTP: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
const select = document.getElementById("schemaSelect");
|
const select = document.getElementById("schemaSelect");
|
||||||
select.innerHTML = '<option value="">Select a schema...</option>';
|
select.innerHTML = '<option value="">Select a schema...</option>';
|
||||||
data.value.forEach(schema => {
|
|
||||||
|
const sortedSchemas = [...data.value].sort((a, b) => {
|
||||||
|
const nomeA = (a.Nome || "").trim().toLowerCase();
|
||||||
|
const nomeB = (b.Nome || "").trim().toLowerCase();
|
||||||
|
return nomeA.localeCompare(nomeB, 'it', {
|
||||||
|
sensitivity: 'base'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
sortedSchemas.forEach(schema => {
|
||||||
const nome = schema.Nome || "Nome non disponibile";
|
const nome = schema.Nome || "Nome non disponibile";
|
||||||
const id = schema.IdSchemaCustomFields || "ID non disponibile";
|
const id = schema.IdSchemaCustomFields || "ID non disponibile";
|
||||||
const option = new Option(`${nome.trim()} (ID: ${id})`, id);
|
const option = new Option(`${nome.trim()} (ID: ${id})`, id);
|
||||||
select.add(option);
|
select.add(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
$(select).trigger('change');
|
$(select).trigger('change');
|
||||||
schemaLoadingStatus.textContent = "Schemi caricati.";
|
schemaLoadingStatus.textContent = "Schemi caricati.";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
schemaLoadingStatus.textContent = "Errore nel caricamento.";
|
schemaLoadingStatus.textContent = "Errore nel caricamento.";
|
||||||
|
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: "Errore!",
|
title: "Errore!",
|
||||||
text: "Impossibile caricare gli schemi: " + error.message,
|
text: "Impossibile caricare gli schemi: " + error.message,
|
||||||
@@ -270,6 +452,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadData();
|
loadData();
|
||||||
|
|
||||||
const routines = <?php echo json_encode($routines); ?>;
|
const routines = <?php echo json_encode($routines); ?>;
|
||||||
@@ -277,8 +460,10 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
function updateRoutineDetails() {
|
function updateRoutineDetails() {
|
||||||
const selectedId = routineSelect.value;
|
const selectedId = routineSelect.value;
|
||||||
routineDetails.style.display = selectedId ? 'block' : 'none';
|
routineDetails.style.display = selectedId ? 'block' : 'none';
|
||||||
|
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
const routine = routines.find(r => r.idroutine == selectedId);
|
const routine = routines.find(r => r.idroutine == selectedId);
|
||||||
|
|
||||||
if (routine) {
|
if (routine) {
|
||||||
routineName.textContent = routine.name || 'N/A';
|
routineName.textContent = routine.name || 'N/A';
|
||||||
routineDescription.textContent = routine.description || 'N/A';
|
routineDescription.textContent = routine.description || 'N/A';
|
||||||
@@ -300,6 +485,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
routineAction3.textContent = '';
|
routineAction3.textContent = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
routineSelect.addEventListener('change', updateRoutineDetails);
|
routineSelect.addEventListener('change', updateRoutineDetails);
|
||||||
updateRoutineDetails();
|
updateRoutineDetails();
|
||||||
|
|
||||||
@@ -308,6 +494,28 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
|
|
||||||
let formData = new FormData(this);
|
let formData = new FormData(this);
|
||||||
|
|
||||||
|
const selectedSource = sourceType.value;
|
||||||
|
|
||||||
|
if (selectedSource === 'XLS' && xlsSheetIndex.value === '') {
|
||||||
|
Swal.fire({
|
||||||
|
title: "Errore!",
|
||||||
|
text: "Inserisci il numero del foglio XLS.",
|
||||||
|
icon: "error",
|
||||||
|
confirmButtonText: "OK"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedSource === 'API' && !apiConfigSelect.value) {
|
||||||
|
Swal.fire({
|
||||||
|
title: "Errore!",
|
||||||
|
text: "Seleziona una configurazione API / JSON.",
|
||||||
|
icon: "error",
|
||||||
|
confirmButtonText: "OK"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const clientSelect = document.getElementById("clientSelect");
|
const clientSelect = document.getElementById("clientSelect");
|
||||||
const clientId = clientSelect.value;
|
const clientId = clientSelect.value;
|
||||||
const selectedClientOption = clientSelect.options[clientSelect.selectedIndex];
|
const selectedClientOption = clientSelect.options[clientSelect.selectedIndex];
|
||||||
@@ -350,6 +558,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
const nameMatch = optionText.match(/^(.+?)(?:\s*\(ID:\s*\d+\))?$/);
|
const nameMatch = optionText.match(/^(.+?)(?:\s*\(ID:\s*\d+\))?$/);
|
||||||
schemaName = nameMatch ? nameMatch[1].trim() : optionText;
|
schemaName = nameMatch ? nameMatch[1].trim() : optionText;
|
||||||
}
|
}
|
||||||
|
|
||||||
formData.append("idschema", schemaId);
|
formData.append("idschema", schemaId);
|
||||||
formData.append("schemaname", schemaName);
|
formData.append("schemaname", schemaName);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
include('include/headscript.php');
|
||||||
require_once 'class/db-functions.php';
|
require_once 'class/db-functions.php';
|
||||||
|
|
||||||
$response = ["success" => false, "data" => [], "message" => ""];
|
$response = ["success" => false, "data" => [], "message" => ""];
|
||||||
@@ -12,8 +13,25 @@ try {
|
|||||||
throw new Exception('Database connection failed.');
|
throw new Exception('Database connection failed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recupera solo i template attivi
|
$userId = $iduserlogin ?? 0;
|
||||||
$stmt = $pdo->query("SELECT id, button_label, button_bg_color, button_text_color, button_size FROM excel_templates WHERE status = 'active'");
|
|
||||||
|
// Recupera i template attivi, con flag is_favorite per l'utente corrente
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT
|
||||||
|
et.id,
|
||||||
|
et.button_label,
|
||||||
|
et.button_size,
|
||||||
|
et.button_bg_color,
|
||||||
|
et.button_text_color,
|
||||||
|
et.source_type,
|
||||||
|
CASE WHEN uf.id IS NOT NULL THEN 1 ELSE 0 END AS is_favorite
|
||||||
|
FROM excel_templates et
|
||||||
|
LEFT JOIN user_template_favorites uf
|
||||||
|
ON uf.template_id = et.id AND uf.user_id = :user_id
|
||||||
|
WHERE et.status = 'active'
|
||||||
|
ORDER BY et.button_label ASC
|
||||||
|
");
|
||||||
|
$stmt->execute(['user_id' => $userId]);
|
||||||
$templates = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$templates = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$response["success"] = true;
|
$response["success"] = true;
|
||||||
|
|||||||
@@ -14,11 +14,57 @@ if (!$iddatadb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$stmt = $pdo->prepare("SELECT id, iddatadb, part_number, part_description, idmatrice, note, dateexpiry FROM identification_parts WHERE iddatadb = :iddatadb ORDER BY part_number ASC");
|
// 1) prendo templateid da datadb
|
||||||
$stmt->execute([':iddatadb' => $iddatadb]);
|
$stmtTpl = $pdo->prepare("SELECT templateid FROM datadb WHERE iddatadb = :iddatadb LIMIT 1");
|
||||||
|
$stmtTpl->execute([':iddatadb' => $iddatadb]);
|
||||||
|
$templateid = $stmtTpl->fetchColumn();
|
||||||
|
|
||||||
|
// 2) prendo (max 1) field_id visibile in parts
|
||||||
|
$extraFieldId = null;
|
||||||
|
if ($templateid) {
|
||||||
|
$stmtEF = $pdo->prepare("SELECT field_id FROM template_mapping WHERE template_id = :templateid AND is_visible_parts = 1 ORDER BY id ASC LIMIT 1");
|
||||||
|
$stmtEF->execute([':templateid' => $templateid]);
|
||||||
|
$extraFieldId = $stmtEF->fetchColumn();
|
||||||
|
if ($extraFieldId !== false) $extraFieldId = (int)$extraFieldId;
|
||||||
|
else $extraFieldId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) carico parts + join su tabella figlia
|
||||||
|
if ($extraFieldId) {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT
|
||||||
|
p.id, p.iddatadb, p.part_number, p.part_description, p.mix, p.idmatrice, p.note, p.dateexpiry,
|
||||||
|
cf.value_id AS extra_value_id,
|
||||||
|
cf.value_text AS extra_value_text
|
||||||
|
FROM identification_parts p
|
||||||
|
LEFT JOIN identification_parts_customfields cf
|
||||||
|
ON cf.part_id = p.id AND cf.field_id = :extraFieldId
|
||||||
|
WHERE p.iddatadb = :iddatadb
|
||||||
|
ORDER BY p.part_number ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
':iddatadb' => $iddatadb,
|
||||||
|
':extraFieldId' => $extraFieldId
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT id, iddatadb, part_number, part_description, mix, idmatrice, note, dateexpiry,
|
||||||
|
NULL AS extra_value_id, NULL AS extra_value_text
|
||||||
|
FROM identification_parts
|
||||||
|
WHERE iddatadb = :iddatadb
|
||||||
|
ORDER BY part_number ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([':iddatadb' => $iddatadb]);
|
||||||
|
}
|
||||||
|
|
||||||
$parts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$parts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
echo json_encode(['success' => true, 'parts' => $parts]);
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'parts' => $parts,
|
||||||
|
'extra_field_id' => $extraFieldId,
|
||||||
|
'debug_sql' => ($extraFieldId ? 'WITH_CF_JOIN' : 'NO_CF')
|
||||||
|
]);
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
echo json_encode(['success' => false, 'message' => 'Errore nel caricamento: ' . $e->getMessage()]);
|
echo json_encode(['success' => false, 'message' => 'Errore nel caricamento: ' . $e->getMessage()]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
include('include/headscript.php');
|
||||||
|
|
||||||
|
$dbHandler = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $dbHandler->getConnection();
|
||||||
|
|
||||||
|
// Recupera l'ID dell'utente loggato
|
||||||
|
$user_id = $iduserlogin ?? 1;
|
||||||
|
|
||||||
|
if (!$user_id) {
|
||||||
|
echo json_encode(['success' => false, 'message' => "ID dell'utente autenticato mancante"]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT DISTINCT q.*
|
||||||
|
FROM quotations q
|
||||||
|
INNER JOIN identification_parts ip
|
||||||
|
ON ip.idquotations = q.id
|
||||||
|
AND ip.iddatadb IS NULL
|
||||||
|
WHERE q.iduser = :iduser"
|
||||||
|
);
|
||||||
|
$stmt->execute([':iduser' => $user_id]);
|
||||||
|
$quotations = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'quotations' => $quotations]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Errore nel caricamento delle quotations: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user