88 Commits

Author SHA1 Message Date
solocla 27cbc9f449 cad area update con autocontorno 2026-06-16 12:05:50 +02:00
solocla 4c09a0dcb4 cad area punti mpodifica manuale 2026-06-16 09:44:19 +02:00
solocla 8bb23ee563 cad area 2026-06-16 09:23:40 +02:00
solocla 20571c9e4b fixed navbar 2026-06-11 10:34:41 +02:00
solocla fdde16b113 added functions email and flag 2026-06-11 10:20:46 +02:00
solocla 33b627f328 image cad area size 2026-06-11 09:02:22 +02:00
solocla d96b4be9e0 fixed add sub roles 2026-06-05 14:53:34 +02:00
solocla 088e518db1 fixed migration 2026-06-05 14:50:51 +02:00
solocla 789c547bc7 Ensure job_roles table exists before job_sub_roles migration 2026-06-05 14:47:11 +02:00
solocla e5bf546ae7 fixed funzioni aziendali 2026-06-05 14:35:19 +02:00
solocla 6dd13e5d7d fixed employess job roles navbar 2026-06-05 10:45:34 +02:00
solocla b1f2bb60e3 added subroles and dpi association fixed all pages and migration 2026-06-04 12:17:17 +02:00
RMubarakzyanov f7e97f55e9 bulk operations for dpi 2026-05-26 20:11:55 +03:00
solocla 70b712ff3b trainings changed details 2026-05-26 16:55:04 +02:00
solocla fdc3af01f3 Merge branch 'feature/user_profile' of https://gitea.solocla.synology.me/solocla/zibo-dashboard into feature/user_profile 2026-05-26 16:05:26 +02:00
solocla 3d54140280 fixed employee profile 2026-05-26 16:05:24 +02:00
RMubarakzyanov bfdbbbfc8f Merge branch 'main' into feature/user_profile 2026-05-26 16:52:41 +03:00
RMubarakzyanov 40a5771a4b bulk operations 2026-05-24 01:04:41 +03:00
RMubarakzyanov 9f5a585717 Merge branch 'main' into feature/user_profile 2026-05-24 00:16:28 +03:00
RMubarakzyanov 9ec5419a86 dlFunction fix 2026-05-24 00:15:09 +03:00
RMubarakzyanov c05091e020 Merge branch 'main' into feature/20260520_scadenziario
# Conflicts:
#	public/userarea/scadenzario/index.php
2026-05-24 00:02:20 +03:00
RMubarakzyanov 0b470f290e fix auto-open 2026-05-23 23:56:43 +03:00
solocla e74870c8d3 added functions 2026-05-22 09:16:46 +02:00
RMubarakzyanov 9001eff317 file repo, cc, auto-open 2026-05-21 23:31:36 +03:00
RMubarakzyanov 7cbd74111d Merge branch 'main' into feature/user_profile
# Conflicts:
#	public/userarea/include/topbar.php
#	public/userarea/scadenzario/include/my_deadlines_widget.php
2026-05-21 22:44:42 +03:00
solocla 650676037a fixed date format 2026-05-20 14:48:45 +02:00
solocla 2fc34c3cf4 fixed redirection 2026-05-18 13:49:22 +02:00
solocla 955a7ed9e9 fixed user setting 2026-05-18 13:31:34 +02:00
RMubarakzyanov cb221a8039 fix initial+refresher 2026-05-17 21:13:57 +03:00
RMubarakzyanov ece1beb87f Merge branch 'main' into feature/user_profile
# Conflicts:
#	public/userarea/include/navbar.php
2026-05-17 20:06:15 +03:00
solocla e6a805f1f7 fixed permission 2026-05-15 21:10:28 +02:00
solocla fe84d446e7 stop tracking vendor 2026-05-15 20:54:16 +02:00
solocla 2ddf575191 phinx 2026-05-15 20:50:06 +02:00
solocla d73a8bb8d3 add permission to dashboard and navbar 2026-05-15 17:13:29 +02:00
RMubarakzyanov d155d1cbab user profile 2026-05-14 16:10:10 +03:00
solocla fa2f293835 added roles edit into employees 2026-05-07 14:39:50 +02:00
solocla fc35adc7f9 big modal deadlines 2026-05-07 09:46:25 +02:00
solocla ac942dcdc8 added depart,ments to employee and deadlines 2026-05-07 09:44:38 +02:00
solocla 5728afa788 added departments 2026-04-30 08:22:24 +02:00
solocla 1946648b1b add modifica record 2026-04-28 11:56:31 +02:00
solocla a1bcab3188 fixed color 2026-04-28 11:48:35 +02:00
solocla 2bbeb11726 fixed sql 2026-04-19 17:37:09 +02:00
RMubarakzyanov dd5edab2f3 deadline widget 2026-04-19 09:14:19 +03:00
RMubarakzyanov 1fadc22178 fix login redirect 2026-04-18 20:37:03 +03:00
RMubarakzyanov d3ee9a3790 mobile view 2026-04-18 17:28:49 +03:00
RMubarakzyanov c387b71cae xls converted to sql 2026-04-18 16:44:53 +03:00
RMubarakzyanov b2cfec77df xls converted to sql 2026-04-18 16:44:14 +03:00
RMubarakzyanov 73b9a3d890 dueDate autofill 2026-04-18 16:25:20 +03:00
RMubarakzyanov 0550ffe923 Subject CRUD 2026-04-18 15:26:04 +03:00
RMubarakzyanov d2e5cc8b2b dynamic basepath 2026-04-16 17:35:13 +03:00
RMubarakzyanov d7b6a58407 deadline feature 2026-04-10 15:51:30 +03:00
solocla 174fa73c2c added scadenziario button 2026-04-02 14:42:02 +02:00
solocla 0bd41b8eb0 Ignore matrici attachments 2026-04-02 14:41:52 +02:00
solocla 248ae63875 fixed supplier mescole 2026-04-01 15:03:39 +02:00
solocla d39e997beb fixed update fornitore 2026-04-01 14:57:20 +02:00
solocla 1b23885659 update dashboard 2026-04-01 14:48:03 +02:00
solocla ad16d84b2e added fogloiu di lavoro modals everywhere 2026-03-31 11:39:17 +02:00
solocla 8cf74608b8 move fogli di lavoro 2026-03-24 09:32:55 +01:00
solocla 2642906a9b fixed worksheet modal 2026-03-23 17:49:48 +01:00
solocla d2f2a9089e marici foglio lavoro 2026-03-20 22:09:39 +01:00
solocla 53b990ff40 fixed imballaggi worksheet 2026-03-20 12:24:22 +01:00
solocla f477f393ba prova push gitea gitlab 2026-03-20 08:59:19 +01:00
solocla bc806f37f4 fixed mescole matrici 2026-03-19 16:20:46 +01:00
solocla f043b43791 upgrade matrice with files 2026-03-19 16:16:41 +01:00
solocla 245750f057 mescole update 2026-03-06 08:42:18 +01:00
solocla 22e5b90fe4 loopkup, foglio di lavoro 2026-02-03 16:04:45 +01:00
solocla 31f22b4d92 matrix skills 2026-02-02 17:25:47 +01:00
solocla 340ebdcbce fixed refresh 2026-01-27 10:00:05 +01:00
solocla 1edf7b7239 fixed matrici 2026-01-27 09:57:47 +01:00
solocla 51cca3448a fixed dropdown user 2026-01-27 08:54:55 +01:00
solocla 8732f21af8 fix column matrici 2026-01-21 12:22:23 +01:00
solocla 3043522465 fixed titles 2026-01-21 12:13:05 +01:00
solocla 49435b8e44 fixed table and search 2026-01-21 12:10:05 +01:00
solocla e876cb9775 fix eomployees 2025-12-11 10:48:19 +01:00
solocla 7f78a61808 employees 2025-12-11 10:45:24 +01:00
solocla 50c808e605 update photo button 2025-12-11 09:09:30 +01:00
solocla 978b38c669 fixed modal 2025-12-10 15:07:38 +01:00
solocla 4683c4f40c fixed photo 2025-12-10 14:58:03 +01:00
solocla d9cbaf8df1 fixed line 2025-12-10 14:23:59 +01:00
solocla 9649751ad8 fixed drag lines 2025-12-10 14:14:43 +01:00
solocla 824ae278d1 fixed add instrument 2025-12-05 12:29:33 +01:00
solocla 37909e8175 fixed foto matrice 2025-12-03 14:29:35 +01:00
solocla 86782d26b2 fixed drag with arrows 2025-12-03 14:23:14 +01:00
solocla 329b3ffdeb added params crud lines 2025-12-03 14:07:11 +01:00
solocla 9447f3cf00 added foto matrice everywhere 2025-12-03 12:21:26 +01:00
solocla 5b7a8b57d5 added images 2025-12-03 11:47:00 +01:00
solocla 7a2cac27cd fixed images 2025-12-03 11:35:49 +01:00
solocla f9737fdf73 fixed photo upoload 2025-12-03 10:38:43 +01:00
12287 changed files with 43447 additions and 1351017 deletions
+2
View File
@@ -31,6 +31,8 @@ MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_ENCRYPTION=null MAIL_ENCRYPTION=null
MANAGER_USER_ID=
PUSHER_APP_ID= PUSHER_APP_ID=
PUSHER_APP_KEY= PUSHER_APP_KEY=
PUSHER_APP_SECRET= PUSHER_APP_SECRET=
+6
View File
@@ -32,6 +32,8 @@ auth.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
/public/userarea/photos/matrici/allegati/
/public/userarea/photos/matrici/allegati/*
# Ignora cartelle di foto generate # Ignora cartelle di foto generate
/public/photostrf/ /public/photostrf/
@@ -44,6 +46,7 @@ public/userarea/last_url.txt
public/userarea/class/curl_auth_debug.log public/userarea/class/curl_auth_debug.log
public/userarea/class/curl_request_debug.log public/userarea/class/curl_request_debug.log
public/userarea/uploads/cad_area/originals/*
# Ignora tutti i log # Ignora tutti i log
*.log *.log
@@ -64,3 +67,6 @@ public/userarea/logsapi/commessaweb_customfields_763.json
public/userarea/logsapi/commessaweb_invia_762.json public/userarea/logsapi/commessaweb_invia_762.json
public/userarea/logsapi/commessaweb_invia_763.json public/userarea/logsapi/commessaweb_invia_763.json
public/userarea/logsapi/last_auth_url.txt public/userarea/logsapi/last_auth_url.txt
# User uploaded files
/public/userarea/files/
@@ -111,6 +111,14 @@ class LoginController extends Controller
return redirect()->to('userarea/production_dashboard.php'); return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('User')) { } elseif ($user->hasRole('User')) {
return redirect()->to('userarea/production_dashboard.php'); return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('HR')) {
return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('SuperUser')) {
return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('Management')) {
return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('Quality')) {
return redirect()->to('userarea/production_dashboard.php');
} }
// Se il ruolo non è specificato, reindirizza alla home predefinita // Se il ruolo non è specificato, reindirizza alla home predefinita
+1
View File
@@ -44,6 +44,7 @@
"phpmailer/phpmailer": "^6.9", "phpmailer/phpmailer": "^6.9",
"phpoffice/phpspreadsheet": "^4.1", "phpoffice/phpspreadsheet": "^4.1",
"proengsoft/laravel-jsvalidation": "^4.0.0", "proengsoft/laravel-jsvalidation": "^4.0.0",
"robmorgan/phinx": "^0.16.11",
"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
+646 -2
View File
@@ -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": "076e7721d08cfea8b06ce75dd8c6c576",
"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.5",
"source": {
"type": "git",
"url": "https://github.com/cakephp/core.git",
"reference": "eb012517900ed288f580aa3487e9a09f28ea85f9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/core/zipball/eb012517900ed288f580aa3487e9a09f28ea85f9",
"reference": "eb012517900ed288f580aa3487e9a09f28ea85f9",
"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-03-31T06:25:23+00:00"
},
{
"name": "cakephp/database",
"version": "5.3.5",
"source": {
"type": "git",
"url": "https://github.com/cakephp/database.git",
"reference": "cf94dcb57c54a1a308fd866b038cd6995910e36e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/database/zipball/cf94dcb57c54a1a308fd866b038cd6995910e36e",
"reference": "cf94dcb57c54a1a308fd866b038cd6995910e36e",
"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-03-31T06:25:23+00:00"
},
{
"name": "cakephp/datasource",
"version": "5.3.5",
"source": {
"type": "git",
"url": "https://github.com/cakephp/datasource.git",
"reference": "512464eb27b19316b515ec338089b83822c9ab5a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/datasource/zipball/512464eb27b19316b515ec338089b83822c9ab5a",
"reference": "512464eb27b19316b515ec338089b83822c9ab5a",
"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-04-04T08:08:42+00:00"
},
{
"name": "cakephp/utility",
"version": "5.3.5",
"source": {
"type": "git",
"url": "https://github.com/cakephp/utility.git",
"reference": "4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/utility/zipball/4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99",
"reference": "4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99",
"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-03-09T09:38:36+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",
@@ -4980,6 +5388,93 @@
], ],
"time": "2024-04-27T21:32:50+00:00" "time": "2024-04-27T21:32:50+00:00"
}, },
{
"name": "robmorgan/phinx",
"version": "0.16.11",
"source": {
"type": "git",
"url": "https://github.com/cakephp/phinx.git",
"reference": "a03014fea316ba021fc0776982e5bed2d10228d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/phinx/zipball/a03014fea316ba021fc0776982e5bed2d10228d4",
"reference": "a03014fea316ba021fc0776982e5bed2d10228d4",
"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.11"
},
"time": "2026-03-15T00:04:32+00:00"
},
{ {
"name": "socialiteproviders/manager", "name": "socialiteproviders/manager",
"version": "v4.8.1", "version": "v4.8.1",
@@ -5312,6 +5807,85 @@
], ],
"time": "2024-05-31T14:57:53+00:00" "time": "2024-05-31T14:57:53+00:00"
}, },
{
"name": "symfony/config",
"version": "v7.4.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
"reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57",
"reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57",
"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.10"
},
"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-03T14:20:49+00:00"
},
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v7.1.3", "version": "v7.1.3",
@@ -5768,6 +6342,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 +11999,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,25 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class BaselineExistingDatabase extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
// Baseline migration.
// Existing database structure starts being tracked from this point.
}
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreatePhinxTestTable extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$table = $this->table('phinx_test_table');
$table
->addColumn('name', 'string', [
'limit' => 100,
'null' => false,
])
->addColumn('created_at', 'timestamp', [
'default' => 'CURRENT_TIMESTAMP',
'null' => false,
])
->create();
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddFunctionsToScadDeadlines extends AbstractMigration
{
public function change(): void
{
$this->table('scad_functions', [
'id' => false,
'primary_key' => ['id'],
'collation' => 'utf8mb4_unicode_ci',
'encoding' => 'utf8mb4',
])
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('name', 'string', [
'limit' => 255,
'null' => false,
])
->addColumn('description', 'text', [
'null' => true,
])
->addColumn('status', 'string', [
'limit' => 20,
'null' => false,
'default' => 'active',
])
->addColumn('created_at', 'timestamp', [
'null' => false,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => false,
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['name'], [
'unique' => true,
'name' => 'uniq_scad_functions_name',
])
->create();
$this->table('scad_deadlines')
->addColumn('function_id', 'integer', [
'signed' => false,
'null' => true,
'after' => 'subject_id',
])
->addIndex(['function_id'], [
'name' => 'idx_scad_deadlines_function_id',
])
->addForeignKey('function_id', 'scad_functions', 'id', [
'delete' => 'SET_NULL',
'update' => 'CASCADE',
'constraint' => 'fk_scad_deadlines_function',
])
->update();
}
}
@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateJobSubRolesTable extends AbstractMigration
{
public function change(): void
{
if (!$this->hasTable('job_roles')) {
$rolesTable = $this->table('job_roles', [
'id' => false,
'primary_key' => ['id'],
'collation' => 'utf8mb4_unicode_ci',
'encoding' => 'utf8mb4',
]);
$rolesTable
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('name', 'string', [
'limit' => 255,
'null' => false,
])
->addColumn('description', 'text', [
'null' => true,
'default' => null,
])
->addColumn('sort_order', 'integer', [
'signed' => false,
'null' => false,
'default' => 999,
])
->addColumn('is_active', 'boolean', [
'null' => false,
'default' => 1,
])
->addColumn('created_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['is_active'], [
'name' => 'idx_job_roles_is_active',
])
->addIndex(['sort_order'], [
'name' => 'idx_job_roles_sort_order',
])
->create();
}
if (!$this->hasTable('job_sub_roles')) {
$table = $this->table('job_sub_roles', [
'id' => false,
'primary_key' => ['id'],
'collation' => 'utf8mb4_unicode_ci',
'encoding' => 'utf8mb4',
]);
$table
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('job_role_id', 'integer', [
'signed' => false,
'null' => false,
])
->addColumn('name', 'string', [
'limit' => 255,
'null' => false,
])
->addColumn('description', 'text', [
'null' => true,
'default' => null,
])
->addColumn('sort_order', 'integer', [
'signed' => false,
'null' => false,
'default' => 999,
])
->addColumn('is_active', 'boolean', [
'null' => false,
'default' => 1,
])
->addColumn('created_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['job_role_id'], [
'name' => 'idx_job_sub_roles_job_role_id',
])
->addIndex(['is_active'], [
'name' => 'idx_job_sub_roles_is_active',
])
->addIndex(['sort_order'], [
'name' => 'idx_job_sub_roles_sort_order',
])
->addForeignKey(
'job_role_id',
'job_roles',
'id',
[
'delete' => 'CASCADE',
'update' => 'CASCADE',
'constraint' => 'fk_job_sub_roles_job_role',
]
)
->create();
}
}
}
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreatePpeItemsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('ppe_items', [
'id' => false,
'primary_key' => ['id'],
'collation' => 'utf8mb4_unicode_ci',
'encoding' => 'utf8mb4',
]);
$table
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('name', 'string', [
'limit' => 255,
'null' => false,
])
->addColumn('description', 'text', [
'null' => true,
'default' => null,
])
->addColumn('category', 'string', [
'limit' => 100,
'null' => true,
'default' => null,
'comment' => 'PPE category, for example Head, Hands, Eyes, Feet, Respiratory',
])
->addColumn('photo', 'string', [
'limit' => 255,
'null' => true,
'default' => null,
'comment' => 'PPE image path or filename',
])
->addColumn('standard_reference', 'string', [
'limit' => 255,
'null' => true,
'default' => null,
'comment' => 'Reference standard, for example EN ISO 20345',
])
->addColumn('validity_months', 'integer', [
'signed' => false,
'null' => true,
'default' => null,
'comment' => 'Default validity in months after assignment',
])
->addColumn('sort_order', 'integer', [
'signed' => false,
'null' => false,
'default' => 999,
])
->addColumn('is_active', 'boolean', [
'null' => false,
'default' => 1,
])
->addColumn('created_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['category'], [
'name' => 'idx_ppe_items_category',
])
->addIndex(['is_active'], [
'name' => 'idx_ppe_items_is_active',
])
->addIndex(['sort_order'], [
'name' => 'idx_ppe_items_sort_order',
])
->create();
}
}
@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateEmployeePpeItemsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('employee_ppe_items', [
'id' => false,
'primary_key' => ['id'],
'collation' => 'utf8mb4_unicode_ci',
'encoding' => 'utf8mb4',
]);
$table
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('employee_id', 'integer', [
'signed' => false,
'null' => false,
])
->addColumn('ppe_item_id', 'integer', [
'signed' => false,
'null' => false,
])
->addColumn('assigned_date', 'date', [
'null' => true,
'default' => null,
])
->addColumn('expiry_date', 'date', [
'null' => true,
'default' => null,
])
->addColumn('quantity', 'integer', [
'signed' => false,
'null' => false,
'default' => 1,
])
->addColumn('status', 'enum', [
'values' => [
'assigned',
'returned',
'expired',
'lost',
'damaged',
],
'null' => false,
'default' => 'assigned',
])
->addColumn('notes', 'text', [
'null' => true,
'default' => null,
])
->addColumn('created_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['employee_id'], [
'name' => 'idx_employee_ppe_items_employee_id',
])
->addIndex(['ppe_item_id'], [
'name' => 'idx_employee_ppe_items_ppe_item_id',
])
->addIndex(['status'], [
'name' => 'idx_employee_ppe_items_status',
])
->addIndex(['expiry_date'], [
'name' => 'idx_employee_ppe_items_expiry_date',
])
->addForeignKey(
'employee_id',
'employees',
'id',
[
'delete' => 'CASCADE',
'update' => 'CASCADE',
'constraint' => 'fk_employee_ppe_items_employee',
]
)
->addForeignKey(
'ppe_item_id',
'ppe_items',
'id',
[
'delete' => 'RESTRICT',
'update' => 'CASCADE',
'constraint' => 'fk_employee_ppe_items_ppe_item',
]
)
->create();
}
}
@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateJobSubRolePpeItemsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('job_sub_role_ppe_items', [
'id' => false,
'primary_key' => ['id'],
'collation' => 'utf8mb4_unicode_ci',
'encoding' => 'utf8mb4',
]);
$table
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('job_sub_role_id', 'integer', [
'signed' => false,
'null' => false,
])
->addColumn('ppe_item_id', 'integer', [
'signed' => false,
'null' => false,
])
->addColumn('requirement_type', 'enum', [
'values' => [
'mandatory',
'recommended',
'optional',
],
'null' => false,
'default' => 'mandatory',
'comment' => 'Defines if the PPE is mandatory, recommended or optional for the sub role',
])
->addColumn('notes', 'text', [
'null' => true,
'default' => null,
])
->addColumn('sort_order', 'integer', [
'signed' => false,
'null' => false,
'default' => 999,
])
->addColumn('is_active', 'boolean', [
'null' => false,
'default' => 1,
])
->addColumn('created_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['job_sub_role_id'], [
'name' => 'idx_job_sub_role_ppe_items_sub_role_id',
])
->addIndex(['ppe_item_id'], [
'name' => 'idx_job_sub_role_ppe_items_ppe_item_id',
])
->addIndex(['requirement_type'], [
'name' => 'idx_job_sub_role_ppe_items_requirement_type',
])
->addIndex(['is_active'], [
'name' => 'idx_job_sub_role_ppe_items_is_active',
])
->addIndex(['job_sub_role_id', 'ppe_item_id'], [
'unique' => true,
'name' => 'uq_job_sub_role_ppe_item',
])
->addForeignKey(
'job_sub_role_id',
'job_sub_roles',
'id',
[
'delete' => 'CASCADE',
'update' => 'CASCADE',
'constraint' => 'fk_job_sub_role_ppe_items_sub_role',
]
)
->addForeignKey(
'ppe_item_id',
'ppe_items',
'id',
[
'delete' => 'CASCADE',
'update' => 'CASCADE',
'constraint' => 'fk_job_sub_role_ppe_items_ppe_item',
]
)
->create();
}
}
@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddJobSubRoleIdToEmployeesTable extends AbstractMigration
{
public function up(): void
{
if (!$this->hasTable('employees')) {
throw new RuntimeException('Table employees does not exist.');
}
$table = $this->table('employees');
if (!$table->hasColumn('job_role_id')) {
$table
->addColumn('job_role_id', 'integer', [
'signed' => false,
'null' => true,
'after' => 'department_id',
])
->addIndex(['job_role_id'], [
'name' => 'idx_employees_job_role_id',
])
->addForeignKey(
'job_role_id',
'job_roles',
'id',
[
'delete' => 'SET_NULL',
'update' => 'CASCADE',
'constraint' => 'fk_employees_job_role',
]
)
->update();
}
$table = $this->table('employees');
if (!$table->hasColumn('job_sub_role_id')) {
$afterColumn = $table->hasColumn('job_role_id') ? 'job_role_id' : 'department_id';
$table
->addColumn('job_sub_role_id', 'integer', [
'signed' => false,
'null' => true,
'after' => $afterColumn,
])
->addIndex(['job_sub_role_id'], [
'name' => 'idx_employees_job_sub_role_id',
])
->addForeignKey(
'job_sub_role_id',
'job_sub_roles',
'id',
[
'delete' => 'SET_NULL',
'update' => 'CASCADE',
'constraint' => 'fk_employees_job_sub_role',
]
)
->update();
}
}
public function down(): void
{
if (!$this->hasTable('employees')) {
return;
}
$table = $this->table('employees');
if ($table->hasForeignKey('job_sub_role_id')) {
$table->dropForeignKey('job_sub_role_id')->update();
}
if ($table->hasForeignKey('job_role_id')) {
$table->dropForeignKey('job_role_id')->update();
}
$table = $this->table('employees');
if ($table->hasIndexByName('idx_employees_job_sub_role_id')) {
$table->removeIndexByName('idx_employees_job_sub_role_id')->update();
}
if ($table->hasIndexByName('idx_employees_job_role_id')) {
$table->removeIndexByName('idx_employees_job_role_id')->update();
}
$table = $this->table('employees');
if ($table->hasColumn('job_sub_role_id')) {
$table->removeColumn('job_sub_role_id')->update();
}
if ($table->hasColumn('job_role_id')) {
$table->removeColumn('job_role_id')->update();
}
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddDeliveryFieldsToEmployeePpeItemsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('employee_ppe_items');
$table
->addColumn('delivered_by', 'string', [
'limit' => 255,
'null' => true,
'default' => null,
'after' => 'expiry_date',
])
->addColumn('created_by', 'integer', [
'signed' => false,
'null' => true,
'default' => null,
'after' => 'notes',
])
->addIndex(['created_by'], [
'name' => 'idx_employee_ppe_items_created_by',
])
->addForeignKey(
'created_by',
'auth_users',
'id',
[
'delete' => 'SET_NULL',
'update' => 'CASCADE',
'constraint' => 'fk_employee_ppe_items_created_by',
]
)
->update();
}
}
@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateEmployeeJobSubRolesTable extends AbstractMigration
{
public function up(): void
{
if (!$this->hasTable('employee_job_sub_roles')) {
$table = $this->table('employee_job_sub_roles', [
'id' => false,
'primary_key' => ['id'],
'signed' => false,
'collation' => 'utf8mb4_general_ci',
'encoding' => 'utf8mb4',
]);
$table
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('employee_id', 'integer', [
'signed' => false,
'null' => false,
])
->addColumn('job_sub_role_id', 'integer', [
'signed' => false,
'null' => false,
])
->addColumn('is_primary', 'boolean', [
'null' => false,
'default' => false,
])
->addColumn('created_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
])
->addIndex(['employee_id', 'job_sub_role_id'], [
'unique' => true,
'name' => 'uq_employee_subrole',
])
->addIndex(['employee_id'], [
'name' => 'idx_employee_job_sub_roles_employee',
])
->addIndex(['job_sub_role_id'], [
'name' => 'idx_employee_job_sub_roles_subrole',
])
->addForeignKey(
'employee_id',
'employees',
'id',
[
'delete' => 'CASCADE',
'update' => 'CASCADE',
'constraint' => 'fk_employee_job_sub_roles_employee',
]
)
->addForeignKey(
'job_sub_role_id',
'job_sub_roles',
'id',
[
'delete' => 'CASCADE',
'update' => 'CASCADE',
'constraint' => 'fk_employee_job_sub_roles_subrole',
]
)
->create();
}
// Import existing single sub-role assignments from employees.job_sub_role_id
// into the new bridge table.
$this->execute("
INSERT IGNORE INTO employee_job_sub_roles
(employee_id, job_sub_role_id, is_primary, created_at)
SELECT
e.id,
e.job_sub_role_id,
1,
NOW()
FROM employees e
WHERE e.job_sub_role_id IS NOT NULL
AND e.job_sub_role_id > 0
");
}
public function down(): void
{
if ($this->hasTable('employee_job_sub_roles')) {
$this->table('employee_job_sub_roles')->drop()->save();
}
}
}
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateCompanyFunctionsTable extends AbstractMigration
{
public function up(): void
{
if (!$this->hasTable('company_functions')) {
$table = $this->table('company_functions', [
'id' => false,
'primary_key' => ['id'],
'signed' => false,
'collation' => 'utf8mb4_general_ci',
'encoding' => 'utf8mb4',
]);
$table
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('function_name', 'string', [
'limit' => 150,
'null' => false,
'comment' => 'Function name, for example RSPP, Medico del lavoro, RLS',
])
->addColumn('person_full_name', 'string', [
'limit' => 200,
'null' => false,
'comment' => 'Full name and surname of the person assigned to the function',
])
->addColumn('phone', 'string', [
'limit' => 80,
'null' => true,
])
->addColumn('email', 'string', [
'limit' => 190,
'null' => true,
])
->addColumn('notes', 'text', [
'null' => true,
])
->addColumn('sort_order', 'integer', [
'signed' => false,
'null' => false,
'default' => 0,
])
->addColumn('is_active', 'boolean', [
'null' => false,
'default' => true,
])
->addColumn('created_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => true,
'default' => null,
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['function_name'], [
'name' => 'idx_company_functions_function_name',
])
->addIndex(['person_full_name'], [
'name' => 'idx_company_functions_person_full_name',
])
->addIndex(['email'], [
'name' => 'idx_company_functions_email',
])
->addIndex(['is_active', 'sort_order'], [
'name' => 'idx_company_functions_active_sort',
])
->create();
}
$this->execute("
INSERT INTO company_functions
(function_name, person_full_name, phone, email, notes, sort_order, is_active, created_at, updated_at)
VALUES
('RSPP', '', NULL, NULL, NULL, 10, 1, NOW(), NOW()),
('Medico del lavoro', '', NULL, NULL, NULL, 20, 1, NOW(), NOW()),
('RLS', '', NULL, NULL, NULL, 30, 1, NOW(), NOW())
");
}
public function down(): void
{
if ($this->hasTable('company_functions')) {
$this->table('company_functions')->drop()->save();
}
}
}
@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AlterScadFunctionsAddContactFields extends AbstractMigration
{
public function up(): void
{
if (!$this->hasTable('scad_functions')) {
throw new RuntimeException('Table scad_functions does not exist.');
}
$table = $this->table('scad_functions');
if (!$table->hasColumn('person_full_name')) {
$table->addColumn('person_full_name', 'string', [
'limit' => 200,
'null' => true,
'after' => 'description',
'comment' => 'Full name and surname of the person assigned to the function',
]);
}
if (!$table->hasColumn('phone')) {
$table->addColumn('phone', 'string', [
'limit' => 80,
'null' => true,
'after' => 'person_full_name',
]);
}
if (!$table->hasColumn('email')) {
$table->addColumn('email', 'string', [
'limit' => 190,
'null' => true,
'after' => 'phone',
]);
}
if (!$table->hasColumn('notes')) {
$table->addColumn('notes', 'text', [
'null' => true,
'after' => 'email',
]);
}
if (!$table->hasColumn('sort_order')) {
$table->addColumn('sort_order', 'integer', [
'signed' => false,
'null' => false,
'default' => 0,
'after' => 'status',
]);
}
if (!$table->hasIndexByName('idx_scad_functions_name')) {
$table->addIndex(['name'], [
'name' => 'idx_scad_functions_name',
]);
}
if (!$table->hasIndexByName('idx_scad_functions_person_full_name')) {
$table->addIndex(['person_full_name'], [
'name' => 'idx_scad_functions_person_full_name',
]);
}
if (!$table->hasIndexByName('idx_scad_functions_email')) {
$table->addIndex(['email'], [
'name' => 'idx_scad_functions_email',
]);
}
if (!$table->hasIndexByName('idx_scad_functions_status_sort')) {
$table->addIndex(['status', 'sort_order'], [
'name' => 'idx_scad_functions_status_sort',
]);
}
$table->update();
// Set a default order for existing rows without changing their names.
$this->execute("
UPDATE scad_functions
SET sort_order = id * 10
WHERE sort_order = 0
");
}
public function down(): void
{
if (!$this->hasTable('scad_functions')) {
return;
}
$table = $this->table('scad_functions');
if ($table->hasIndexByName('idx_scad_functions_status_sort')) {
$table->removeIndexByName('idx_scad_functions_status_sort');
}
if ($table->hasIndexByName('idx_scad_functions_email')) {
$table->removeIndexByName('idx_scad_functions_email');
}
if ($table->hasIndexByName('idx_scad_functions_person_full_name')) {
$table->removeIndexByName('idx_scad_functions_person_full_name');
}
if ($table->hasIndexByName('idx_scad_functions_name')) {
$table->removeIndexByName('idx_scad_functions_name');
}
if ($table->hasColumn('sort_order')) {
$table->removeColumn('sort_order');
}
if ($table->hasColumn('notes')) {
$table->removeColumn('notes');
}
if ($table->hasColumn('email')) {
$table->removeColumn('email');
}
if ($table->hasColumn('phone')) {
$table->removeColumn('phone');
}
if ($table->hasColumn('person_full_name')) {
$table->removeColumn('person_full_name');
}
$table->update();
}
}
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateCadAreaJobsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('cad_area_jobs');
$table
->addColumn('iduser', 'integer', [
'null' => true,
'signed' => false,
'limit' => 10,
])
->addColumn('original_filename', 'string', [
'limit' => 255,
'null' => false,
])
->addColumn('stored_filename', 'string', [
'limit' => 255,
'null' => false,
])
->addColumn('file_path', 'string', [
'limit' => 500,
'null' => false,
])
->addColumn('file_url', 'string', [
'limit' => 500,
'null' => true,
])
->addColumn('file_size', 'integer', [
'null' => true,
'signed' => false,
])
->addColumn('status', 'enum', [
'values' => [
'uploaded',
'processing',
'completed',
'error',
],
'default' => 'uploaded',
'null' => false,
])
->addColumn('area_mm2', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
])
->addColumn('area_cm2', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
])
->addColumn('area_m2', 'decimal', [
'precision' => 18,
'scale' => 9,
'null' => true,
])
->addColumn('scale_detected', 'string', [
'limit' => 50,
'null' => true,
])
->addColumn('confidence', 'string', [
'limit' => 50,
'null' => true,
])
->addColumn('message', 'text', [
'null' => true,
])
->addColumn('python_response', 'text', [
'null' => true,
])
->addColumn('created_at', 'timestamp', [
'default' => 'CURRENT_TIMESTAMP',
'null' => true,
])
->addColumn('updated_at', 'timestamp', [
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
'null' => true,
])
->addIndex(['iduser'], [
'name' => 'idx_cad_area_jobs_iduser',
])
->addIndex(['status'], [
'name' => 'idx_cad_area_jobs_status',
])
->create();
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddNotifyFunctionToScadDeadlines extends AbstractMigration
{
public function up(): void
{
if (!$this->hasTable('scad_deadlines')) {
throw new RuntimeException('Table scad_deadlines does not exist.');
}
$table = $this->table('scad_deadlines');
if (!$table->hasColumn('notify_function')) {
$table
->addColumn('notify_function', 'boolean', [
'null' => false,
'default' => false,
'after' => 'function_id',
'comment' => 'Send deadline reminder also to the linked function email',
])
->update();
}
}
public function down(): void
{
if (!$this->hasTable('scad_deadlines')) {
return;
}
$table = $this->table('scad_deadlines');
if ($table->hasColumn('notify_function')) {
$table
->removeColumn('notify_function')
->update();
}
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddRoiFieldsToCadAreaJobsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('cad_area_jobs');
if (!$table->hasColumn('roi_x')) {
$table->addColumn('roi_x', 'decimal', [
'precision' => 12,
'scale' => 6,
'null' => true,
'after' => 'file_size',
]);
}
if (!$table->hasColumn('roi_y')) {
$table->addColumn('roi_y', 'decimal', [
'precision' => 12,
'scale' => 6,
'null' => true,
'after' => 'roi_x',
]);
}
if (!$table->hasColumn('roi_width')) {
$table->addColumn('roi_width', 'decimal', [
'precision' => 12,
'scale' => 6,
'null' => true,
'after' => 'roi_y',
]);
}
if (!$table->hasColumn('roi_height')) {
$table->addColumn('roi_height', 'decimal', [
'precision' => 12,
'scale' => 6,
'null' => true,
'after' => 'roi_width',
]);
}
if (!$table->hasColumn('roi_page')) {
$table->addColumn('roi_page', 'integer', [
'null' => true,
'default' => 1,
'after' => 'roi_height',
]);
}
if (!$table->hasColumn('calculation_mode')) {
$table->addColumn('calculation_mode', 'string', [
'limit' => 50,
'null' => true,
'default' => 'auto_roi',
'after' => 'roi_page',
]);
}
$table->update();
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddResultDetailFieldsToCadAreaJobsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('cad_area_jobs');
if (!$table->hasColumn('width_mm')) {
$table->addColumn('width_mm', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('height_mm')) {
$table->addColumn('height_mm', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('scale_used')) {
$table->addColumn('scale_used', 'decimal', [
'precision' => 12,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('strategy_used')) {
$table->addColumn('strategy_used', 'string', [
'limit' => 100,
'null' => true,
]);
}
$table->update();
}
}
@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddManualTracingFieldsToCadAreaJobsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('cad_area_jobs');
if (!$table->hasColumn('width_mm')) {
$table->addColumn('width_mm', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('height_mm')) {
$table->addColumn('height_mm', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('scale_used')) {
$table->addColumn('scale_used', 'decimal', [
'precision' => 12,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('strategy_used')) {
$table->addColumn('strategy_used', 'string', [
'limit' => 100,
'null' => true,
]);
}
if (!$table->hasColumn('manual_calibration_px')) {
$table->addColumn('manual_calibration_px', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('manual_calibration_mm')) {
$table->addColumn('manual_calibration_mm', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('manual_mm_per_px')) {
$table->addColumn('manual_mm_per_px', 'decimal', [
'precision' => 18,
'scale' => 10,
'null' => true,
]);
}
if (!$table->hasColumn('manual_polygon_json')) {
$table->addColumn('manual_polygon_json', 'text', [
'null' => true,
]);
}
if (!$table->hasColumn('manual_area_mm2')) {
$table->addColumn('manual_area_mm2', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('manual_area_cm2')) {
$table->addColumn('manual_area_cm2', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('manual_width_mm')) {
$table->addColumn('manual_width_mm', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('manual_height_mm')) {
$table->addColumn('manual_height_mm', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('manual_status')) {
$table->addColumn('manual_status', 'string', [
'limit' => 50,
'null' => true,
]);
}
$table->update();
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddManualHoleFieldsToCadAreaJobsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('cad_area_jobs');
if (!$table->hasColumn('manual_outer_area_mm2')) {
$table->addColumn('manual_outer_area_mm2', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('manual_holes_area_mm2')) {
$table->addColumn('manual_holes_area_mm2', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('manual_holes_json')) {
$table->addColumn('manual_holes_json', 'text', [
'null' => true,
]);
}
$table->update();
}
}
+246
View File
@@ -0,0 +1,246 @@
# 1. Database migration
```mysql
ALTER TABLE employees
ADD COLUMN address varchar(500) DEFAULT NULL AFTER last_name,
ADD COLUMN phone varchar(255) DEFAULT NULL AFTER address,
ADD COLUMN email varchar(255) DEFAULT NULL AFTER phone,
ADD COLUMN job_role_id int(10) UNSIGNED DEFAULT NULL AFTER department_id;
-- Replace ENUM status with plain VARCHAR for easier maintenance.
ALTER TABLE employees
MODIFY status varchar(255) NOT NULL DEFAULT 'active';
CREATE TABLE IF NOT EXISTS job_roles (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
description text DEFAULT NULL,
sort_order int(10) UNSIGNED NOT NULL DEFAULT 999,
is_active tinyint(1) NOT NULL DEFAULT 1,
created_at timestamp NULL DEFAULT current_timestamp(),
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (id),
UNIQUE KEY uniq_job_roles_name (name),
KEY idx_job_roles_active (is_active),
KEY idx_job_roles_sort_order (sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
ALTER TABLE employees
ADD KEY idx_employees_job_role_id (job_role_id);
ALTER TABLE employees
ADD CONSTRAINT fk_employees_job_role
FOREIGN KEY (job_role_id) REFERENCES job_roles (id)
ON DELETE SET NULL
ON UPDATE CASCADE;
-- 1) Seed job_roles with every distinct non-empty value of employees.position.
INSERT IGNORE INTO job_roles (name, is_active, sort_order, created_at, updated_at)
SELECT DISTINCT TRIM(position), 1, 999, NOW(), NOW()
FROM employees
WHERE position IS NOT NULL AND TRIM(position) <> '';
-- 2) Backfill employees.job_role_id by matching position text to job_roles.name.
UPDATE employees e
JOIN job_roles jr ON jr.name = TRIM(e.position)
SET e.job_role_id = jr.id
WHERE e.position IS NOT NULL AND TRIM(e.position) <> '';
-- 3) Drop the legacy column.
ALTER TABLE employees DROP COLUMN position;
CREATE TABLE IF NOT EXISTS training_topics (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
description text DEFAULT NULL,
default_frequency_months int(10) UNSIGNED DEFAULT NULL,
default_reminder_days int(10) UNSIGNED NOT NULL DEFAULT 30,
sort_order int(10) UNSIGNED NOT NULL DEFAULT 999,
is_active tinyint(1) NOT NULL DEFAULT 1,
is_mandatory tinyint(1) NOT NULL DEFAULT 0,
created_at timestamp NULL DEFAULT current_timestamp(),
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (id),
UNIQUE KEY uniq_training_topics_name (name),
KEY idx_training_topics_active (is_active),
KEY idx_training_topics_mandatory (is_mandatory),
KEY idx_training_topics_sort_order (sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS employee_documents (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
employee_id int(10) UNSIGNED NOT NULL,
category varchar(255) NOT NULL DEFAULT 'other',
original_name varchar(500) NOT NULL,
stored_name varchar(500) NOT NULL,
mime_type varchar(255) DEFAULT NULL,
size int(10) UNSIGNED DEFAULT NULL,
notes text DEFAULT NULL,
uploaded_by int(10) UNSIGNED DEFAULT NULL,
created_at timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (id),
KEY idx_employee_documents_employee (employee_id),
KEY idx_employee_documents_category (category),
KEY idx_employee_documents_uploaded_by (uploaded_by),
CONSTRAINT fk_employee_documents_employee
FOREIGN KEY (employee_id) REFERENCES employees (id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_employee_documents_uploaded_by
FOREIGN KEY (uploaded_by) REFERENCES auth_users (id)
ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS employee_ppe (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
employee_id int(10) UNSIGNED NOT NULL,
item_name varchar(255) NOT NULL,
delivery_date date DEFAULT NULL,
delivered_by varchar(255) DEFAULT NULL,
notes text DEFAULT NULL,
created_by int(10) UNSIGNED DEFAULT NULL,
created_at timestamp NULL DEFAULT current_timestamp(),
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (id),
KEY idx_employee_ppe_employee (employee_id),
KEY idx_employee_ppe_delivery_date (delivery_date),
CONSTRAINT fk_employee_ppe_employee
FOREIGN KEY (employee_id) REFERENCES employees (id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_employee_ppe_created_by
FOREIGN KEY (created_by) REFERENCES auth_users (id)
ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS employee_trainings (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
employee_id int(10) UNSIGNED NOT NULL,
training_topic_id int(10) UNSIGNED NOT NULL,
completed_date date NOT NULL,
delivered_by varchar(255) DEFAULT NULL,
description text DEFAULT NULL,
training_type varchar(255) NOT NULL DEFAULT 'initial',
update_frequency_months int(10) UNSIGNED DEFAULT NULL,
reminder_days int(10) UNSIGNED DEFAULT NULL,
next_due_date date DEFAULT NULL,
created_by int(10) UNSIGNED DEFAULT NULL,
created_at timestamp NULL DEFAULT current_timestamp(),
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (id),
KEY idx_employee_trainings_employee (employee_id),
KEY idx_employee_trainings_topic (training_topic_id),
KEY idx_employee_trainings_next_due (next_due_date),
KEY idx_employee_trainings_employee_topic (employee_id, training_topic_id),
KEY idx_employee_trainings_created_by (created_by),
CONSTRAINT fk_employee_trainings_employee
FOREIGN KEY (employee_id) REFERENCES employees (id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_employee_trainings_topic
FOREIGN KEY (training_topic_id) REFERENCES training_topics (id)
ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT fk_employee_trainings_created_by
FOREIGN KEY (created_by) REFERENCES auth_users (id)
ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS employee_training_attachments (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
training_id int(10) UNSIGNED NOT NULL,
original_name varchar(500) NOT NULL,
stored_name varchar(500) NOT NULL,
mime_type varchar(255) DEFAULT NULL,
size int(10) UNSIGNED DEFAULT NULL,
uploaded_by int(10) UNSIGNED DEFAULT NULL,
created_at timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (id),
KEY idx_employee_training_attachments_training (training_id),
KEY idx_employee_training_attachments_uploaded_by (uploaded_by),
CONSTRAINT fk_employee_training_attachments_training
FOREIGN KEY (training_id) REFERENCES employee_trainings (id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_employee_training_attachments_uploaded_by
FOREIGN KEY (uploaded_by) REFERENCES auth_users (id)
ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS employee_training_log (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
employee_id int(10) UNSIGNED DEFAULT NULL,
training_id int(10) UNSIGNED DEFAULT NULL,
action varchar(255) NOT NULL,
field varchar(255) DEFAULT NULL,
old_value text DEFAULT NULL,
new_value text DEFAULT NULL,
changed_by int(10) UNSIGNED DEFAULT NULL,
changed_at timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (id),
KEY idx_employee_training_log_employee (employee_id),
KEY idx_employee_training_log_training (training_id),
KEY idx_employee_training_log_changed_at (changed_at),
CONSTRAINT fk_employee_training_log_employee
FOREIGN KEY (employee_id) REFERENCES employees (id)
ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT fk_employee_training_log_training
FOREIGN KEY (training_id) REFERENCES employee_trainings (id)
ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT fk_employee_training_log_changed_by
FOREIGN KEY (changed_by) REFERENCES auth_users (id)
ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO auth_roles (name, display_name, description, removable, created_at, updated_at) VALUES
('employee', 'Employee', 'Read-only access to own employee profile.', 1, NOW(), NOW()),
('employee-hr', 'HR Manager', 'Can manage employee profiles, documents, PPE and training records.', 1, NOW(), NOW()),
('manager', 'Manager', 'Same permissions as HR Manager.', 1, NOW(), NOW())
ON DUPLICATE KEY UPDATE
display_name = VALUES(display_name),
description = VALUES(description),
updated_at = NOW();
CREATE TABLE IF NOT EXISTS training_reminder_log (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
training_id int(10) UNSIGNED DEFAULT NULL,
employee_id int(10) UNSIGNED DEFAULT NULL,
training_topic_id int(10) UNSIGNED DEFAULT NULL,
addressee_email varchar(255) NOT NULL,
next_due_date date DEFAULT NULL,
status_at_send varchar(255) NOT NULL,
sent_at timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (id),
KEY idx_training_reminder_log_dedup (training_id, addressee_email, next_due_date),
KEY idx_training_reminder_log_dedup_missing (employee_id, training_topic_id, addressee_email),
KEY idx_training_reminder_log_sent_at (sent_at),
CONSTRAINT fk_training_reminder_log_training
FOREIGN KEY (training_id) REFERENCES employee_trainings (id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_training_reminder_log_employee
FOREIGN KEY (employee_id) REFERENCES employees (id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_training_reminder_log_topic
FOREIGN KEY (training_topic_id) REFERENCES training_topics (id)
ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
# 2. Upload storage folder
Create the storage directory with the correct permissions for the web server:
```bash
mkdir -p /var/www/zibo-dashboard/public/userarea/files/employees
chown -R www-data:www-data /var/www/zibo-dashboard/public/userarea/files
chmod -R 775 /var/www/zibo-dashboard/public/userarea/files
```
Uploaded files will be organized as:
```
files/employees/{employee_id}/documents/ # File Repository (HR)
files/employees/{employee_id}/trainings/ # Training certificates
```
# 3. Cron for automated emails
```cron
0 7 * * * /usr/bin/php /var/www/zibo-dashboard/public/userarea/cron/send_training_reminders.php \
>> /var/www/zibo-dashboard/storage/logs/training_reminders.log 2>&1
```
+33
View File
@@ -0,0 +1,33 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
if (file_exists(__DIR__ . '/.env')) {
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->safeLoad();
}
return [
'paths' => [
'migrations' => __DIR__ . '/db/migrations',
'seeds' => __DIR__ . '/db/seeds',
],
'environments' => [
'default_migration_table' => 'phinxlog',
'default_environment' => 'development',
'development' => [
'adapter' => $_ENV['DB_CONNECTION'] ?? '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',
'collation' => 'utf8mb4_unicode_ci',
],
],
'version_order' => 'creation',
];
+18
View File
@@ -0,0 +1,18 @@
<?php
/**
* Auth check for AJAX endpoints under /userarea/ajax/.
* Include this at the top of every ajax handler.
* Sets $currentUserId from session or returns 401 JSON.
*/
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (empty($_SESSION['iduserlogin'])) {
header('Content-Type: application/json');
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'Non autorizzato. Effettua il login.']);
exit;
}
$currentUserId = (int)$_SESSION['iduserlogin'];
@@ -0,0 +1,40 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'ID documento non valido.']);
exit;
}
$stmt = $pdo->prepare("SELECT employee_id, stored_name FROM employee_documents WHERE id = :id LIMIT 1");
$stmt->execute(['id' => $id]);
$doc = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$doc) {
echo json_encode(['success' => false, 'message' => 'Documento non trovato.']);
exit;
}
try {
$del = $pdo->prepare("DELETE FROM employee_documents WHERE id = :id");
$del->execute(['id' => $id]);
$path = __DIR__ . '/../../files/employees/' . (int)$doc['employee_id'] . '/documents/' . $doc['stored_name'];
if (is_file($path)) {
@unlink($path);
}
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,38 @@
<?php
include('../../include/headscript.php');
header('Content-Type: application/json; charset=utf-8');
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode([
'success' => false,
'message' => 'ID DPI non valido.'
]);
exit;
}
$stmt = $pdo->prepare("
UPDATE employee_ppe_items
SET status = 'returned',
updated_at = NOW()
WHERE id = ?
");
$stmt->execute([$id]);
echo json_encode([
'success' => true,
'message' => 'DPI rimosso correttamente.'
]);
exit;
} catch (Throwable $e) {
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
exit;
}
@@ -0,0 +1,60 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
exit;
}
try {
$pdo->beginTransaction();
$row = $pdo->prepare("SELECT employee_id FROM employee_trainings WHERE id = :id");
$row->execute(['id' => $id]);
$tr = $row->fetch(PDO::FETCH_ASSOC);
if (!$tr) {
$pdo->rollBack();
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
exit;
}
// Collect attached files BEFORE deletion so we can unlink them after
$files = $pdo->prepare("SELECT stored_name FROM employee_training_attachments WHERE training_id = :id");
$files->execute(['id' => $id]);
$stored = $files->fetchAll(PDO::FETCH_COLUMN);
// Log BEFORE delete (FK on log allows SET NULL on training delete but we want a clean record)
$pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, NULL, 'deleted', NULL, NULL, NULL, :cb, NOW())
")->execute(['eid' => $tr['employee_id'], 'cb' => $currentUserId]);
$pdo->prepare("DELETE FROM employee_trainings WHERE id = :id")->execute(['id' => $id]);
$pdo->commit();
foreach ($stored as $name) {
$path = __DIR__ . '/../../files/employees/' . (int)$tr['employee_id'] . '/trainings/' . $name;
if (is_file($path)) {
@unlink($path);
}
}
echo json_encode(['success' => true]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,59 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'ID allegato non valido.']);
exit;
}
$row = $pdo->prepare("
SELECT a.stored_name, a.original_name, a.training_id, t.employee_id
FROM employee_training_attachments a
JOIN employee_trainings t ON t.id = a.training_id
WHERE a.id = :id
LIMIT 1
");
$row->execute(['id' => $id]);
$att = $row->fetch(PDO::FETCH_ASSOC);
if (!$att) {
echo json_encode(['success' => false, 'message' => 'Allegato non trovato.']);
exit;
}
try {
$pdo->beginTransaction();
$pdo->prepare("DELETE FROM employee_training_attachments WHERE id = :id")->execute(['id' => $id]);
$pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, :tid, 'attachment_deleted', 'attachment', :name, NULL, :cb, NOW())
")->execute([
'eid' => $att['employee_id'],
'tid' => $att['training_id'],
'name' => $att['original_name'],
'cb' => $currentUserId,
]);
$pdo->commit();
$path = __DIR__ . '/../../files/employees/' . (int)$att['employee_id'] . '/trainings/' . $att['stored_name'];
if (is_file($path)) {
@unlink($path);
}
echo json_encode(['success' => true]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,57 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) {
http_response_code(400);
exit('ID non valido.');
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->prepare("
SELECT d.*, e.auth_user_id
FROM employee_documents d
JOIN employees e ON e.id = d.employee_id
WHERE d.id = :id
LIMIT 1
");
$stmt->execute(['id' => $id]);
$doc = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$doc) {
http_response_code(404);
exit('Documento non trovato.');
}
/* Access check: HR roles can download any; otherwise only own employee */
$roleStmt = $pdo->prepare("
SELECT r.name
FROM auth_users u
LEFT JOIN auth_roles r ON r.id = u.role_id
WHERE u.id = :id LIMIT 1
");
$roleStmt->execute(['id' => $currentUserId]);
$role = (string)$roleStmt->fetchColumn();
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
$isHr = in_array($role, $hrRoles, true);
if (!$isHr && (int)$doc['auth_user_id'] !== $currentUserId) {
http_response_code(403);
exit('Accesso negato.');
}
$path = __DIR__ . '/../../files/employees/' . (int)$doc['employee_id'] . '/documents/' . $doc['stored_name'];
if (!is_file($path)) {
http_response_code(404);
exit('File non trovato sul server.');
}
while (ob_get_level() > 0) { ob_end_clean(); }
header('Content-Type: ' . (!empty($doc['mime_type']) ? $doc['mime_type'] : 'application/octet-stream'));
header('Content-Disposition: attachment; filename="' . rawurlencode($doc['original_name']) . '"');
header('Content-Length: ' . filesize($path));
header('Cache-Control: private, max-age=0, must-revalidate');
readfile($path);
exit;
@@ -0,0 +1,56 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) {
http_response_code(400);
exit('ID non valido.');
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->prepare("
SELECT a.*, t.employee_id, e.auth_user_id
FROM employee_training_attachments a
JOIN employee_trainings t ON t.id = a.training_id
JOIN employees e ON e.id = t.employee_id
WHERE a.id = :id
LIMIT 1
");
$stmt->execute(['id' => $id]);
$att = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$att) {
http_response_code(404);
exit('Allegato non trovato.');
}
/* Access: HR or owning employee */
$roleStmt = $pdo->prepare("
SELECT r.name FROM auth_users u
LEFT JOIN auth_roles r ON r.id = u.role_id
WHERE u.id = :id LIMIT 1
");
$roleStmt->execute(['id' => $currentUserId]);
$role = (string)$roleStmt->fetchColumn();
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
$isHr = in_array($role, $hrRoles, true);
if (!$isHr && (int)$att['auth_user_id'] !== $currentUserId) {
http_response_code(403);
exit('Accesso negato.');
}
$path = __DIR__ . '/../../files/employees/' . (int)$att['employee_id'] . '/trainings/' . $att['stored_name'];
if (!is_file($path)) {
http_response_code(404);
exit('File non trovato sul server.');
}
while (ob_get_level() > 0) { ob_end_clean(); }
header('Content-Type: ' . (!empty($att['mime_type']) ? $att['mime_type'] : 'application/octet-stream'));
header('Content-Disposition: attachment; filename="' . rawurlencode($att['original_name']) . '"');
header('Content-Length: ' . filesize($path));
header('Cache-Control: private, max-age=0, must-revalidate');
readfile($path);
exit;
@@ -0,0 +1,58 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
header('Content-Type: application/json');
$trainingId = (int)($_GET['training_id'] ?? 0);
if ($trainingId <= 0) {
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
/* Access: HR or owner */
$ownerStmt = $pdo->prepare("
SELECT e.auth_user_id
FROM employee_trainings t
JOIN employees e ON e.id = t.employee_id
WHERE t.id = :id LIMIT 1
");
$ownerStmt->execute(['id' => $trainingId]);
$ownerAuthUserId = $ownerStmt->fetchColumn();
if ($ownerAuthUserId === false) {
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
exit;
}
$roleStmt = $pdo->prepare("
SELECT r.name FROM auth_users u
LEFT JOIN auth_roles r ON r.id = u.role_id
WHERE u.id = :id LIMIT 1
");
$roleStmt->execute(['id' => $currentUserId]);
$role = (string)$roleStmt->fetchColumn();
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
$isHr = in_array($role, $hrRoles, true);
if (!$isHr && (int)$ownerAuthUserId !== $currentUserId) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'Accesso negato.']);
exit;
}
$stmt = $pdo->prepare("
SELECT id, original_name, mime_type, size, created_at
FROM employee_training_attachments
WHERE training_id = :tid
ORDER BY created_at DESC
");
$stmt->execute(['tid' => $trainingId]);
$attachments = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'attachments' => $attachments,
'can_edit' => $isHr,
]);
@@ -0,0 +1,57 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
header('Content-Type: application/json');
$trainingId = (int)($_GET['training_id'] ?? 0);
if ($trainingId <= 0) {
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
/* Access: HR or owner */
$ownerStmt = $pdo->prepare("
SELECT e.auth_user_id
FROM employee_trainings t
JOIN employees e ON e.id = t.employee_id
WHERE t.id = :id LIMIT 1
");
$ownerStmt->execute(['id' => $trainingId]);
$ownerAuthUserId = $ownerStmt->fetchColumn();
if ($ownerAuthUserId === false) {
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
exit;
}
$roleStmt = $pdo->prepare("
SELECT r.name FROM auth_users u
LEFT JOIN auth_roles r ON r.id = u.role_id
WHERE u.id = :id LIMIT 1
");
$roleStmt->execute(['id' => $currentUserId]);
$role = (string)$roleStmt->fetchColumn();
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
$isHr = in_array($role, $hrRoles, true);
if (!$isHr && (int)$ownerAuthUserId !== $currentUserId) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'Accesso negato.']);
exit;
}
$stmt = $pdo->prepare("
SELECT l.id, l.action, l.field, l.old_value, l.new_value, l.changed_at,
TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS changed_by_name,
u.email AS changed_by_email
FROM employee_training_log l
LEFT JOIN auth_users u ON u.id = l.changed_by
WHERE l.training_id = :tid
ORDER BY l.changed_at DESC, l.id DESC
");
$stmt->execute(['tid' => $trainingId]);
$entries = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'entries' => $entries]);
@@ -0,0 +1,86 @@
<?php
/**
* Bulk-assign a single DPI (PPE) item to several employees at once:
* one employee_ppe row per selected employee, all sharing the same
* item name / delivery date / delivered-by / notes.
* Mirrors ajax/trainings/save_bulk_training.php. HR-only.
*/
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
// $pdo and $currentUserId from hr_auth_check.php
$itemName = trim($_POST['item_name'] ?? '');
$deliveryDate = trim($_POST['delivery_date'] ?? '');
$deliveredBy = trim($_POST['delivered_by'] ?? '');
$notes = trim($_POST['notes'] ?? '');
$employeeIds = $_POST['employee_ids'] ?? [];
if (!is_array($employeeIds)) {
$employeeIds = [];
}
$employeeIds = array_values(array_unique(array_filter(array_map('intval', $employeeIds), fn($v) => $v > 0)));
if ($itemName === '') {
echo json_encode(['success' => false, 'message' => 'Il nome del DPI è obbligatorio.']);
exit;
}
if ($deliveryDate !== '' && !DateTime::createFromFormat('Y-m-d', $deliveryDate)) {
echo json_encode(['success' => false, 'message' => 'Data di consegna non valida.']);
exit;
}
if (empty($employeeIds)) {
echo json_encode(['success' => false, 'message' => 'Selezionare almeno un dipendente.']);
exit;
}
$deliveryDate = $deliveryDate === '' ? null : $deliveryDate;
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
$notes = $notes !== '' ? $notes : null;
try {
$pdo->beginTransaction();
// Only insert for employees that actually exist
$checkEmp = $pdo->prepare("SELECT id FROM employees WHERE id = :id");
$ins = $pdo->prepare("
INSERT INTO employee_ppe
(employee_id, item_name, delivery_date, delivered_by, notes, created_by, created_at, updated_at)
VALUES
(:employee_id, :item_name, :delivery_date, :delivered_by, :notes, :created_by, NOW(), NOW())
");
$created = 0;
foreach ($employeeIds as $eid) {
$checkEmp->execute(['id' => $eid]);
if (!$checkEmp->fetchColumn()) {
continue;
}
$ins->execute([
'employee_id' => $eid,
'item_name' => $itemName,
'delivery_date' => $deliveryDate,
'delivered_by' => $deliveredBy,
'notes' => $notes,
'created_by' => $currentUserId,
]);
$created++;
}
$pdo->commit();
echo json_encode([
'success' => true,
'created' => $created,
'message' => 'DPI assegnato a ' . $created . ' dipendent' . ($created === 1 ? 'e' : 'i') . '.',
]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,116 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$employeeId = (int)($_POST['employee_id'] ?? 0);
$firstName = trim($_POST['first_name'] ?? '');
$lastName = trim($_POST['last_name'] ?? '');
$employeeCode = trim($_POST['employee_code'] ?? '');
$address = trim($_POST['address'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$email = trim($_POST['email'] ?? '');
$hireDate = trim($_POST['hire_date'] ?? '');
$departmentId = $_POST['department_id'] ?? '';
$jobRoleId = $_POST['job_role_id'] ?? '';
$status = trim($_POST['status'] ?? '');
$authUserId = $_POST['auth_user_id'] ?? '';
$roleId = $_POST['role_id'] ?? '';
if ($employeeId <= 0) {
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
exit;
}
if ($firstName === '' || $lastName === '') {
echo json_encode(['success' => false, 'message' => 'Nome e cognome sono obbligatori.']);
exit;
}
$allowedStatus = ['active', 'inactive', 'suspended'];
if (!in_array($status, $allowedStatus, true)) {
$status = 'active';
}
$departmentId = ($departmentId === '' || $departmentId === null) ? null : (int)$departmentId;
$jobRoleId = ($jobRoleId === '' || $jobRoleId === null) ? null : (int)$jobRoleId;
$authUserId = ($authUserId === '' || $authUserId === null) ? null : (int)$authUserId;
$roleId = ($roleId === '' || $roleId === null) ? null : (int)$roleId;
$hireDate = $hireDate === '' ? null : $hireDate;
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo json_encode(['success' => false, 'message' => 'Email non valida.']);
exit;
}
if ($employeeCode !== '') {
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE employee_code = :code AND id <> :id");
$check->execute(['code' => $employeeCode, 'id' => $employeeId]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode(['success' => false, 'message' => 'Codice dipendente già in uso.']);
exit;
}
}
if ($authUserId !== null) {
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE auth_user_id = :uid AND id <> :id");
$check->execute(['uid' => $authUserId, 'id' => $employeeId]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode(['success' => false, 'message' => 'Questo utente è già associato ad un altro dipendente.']);
exit;
}
}
try {
$stmt = $pdo->prepare("
UPDATE employees
SET first_name = :first_name,
last_name = :last_name,
employee_code = :employee_code,
address = :address,
phone = :phone,
email = :email,
hire_date = :hire_date,
department_id = :department_id,
job_role_id = :job_role_id,
status = :status,
auth_user_id = :auth_user_id,
updated_at = NOW()
WHERE id = :id
");
$stmt->execute([
'first_name' => $firstName,
'last_name' => $lastName,
'employee_code' => $employeeCode !== '' ? $employeeCode : null,
'address' => $address !== '' ? $address : null,
'phone' => $phone !== '' ? $phone : null,
'email' => $email !== '' ? $email : null,
'hire_date' => $hireDate,
'department_id' => $departmentId,
'job_role_id' => $jobRoleId,
'status' => $status,
'auth_user_id' => $authUserId,
'id' => $employeeId,
]);
// Optionally update Vanguard role for the linked auth_user
if ($authUserId !== null && $roleId !== null) {
$check = $pdo->prepare("SELECT COUNT(*) FROM auth_roles WHERE id = ?");
$check->execute([$roleId]);
if ((int)$check->fetchColumn() > 0) {
$upd = $pdo->prepare("UPDATE auth_users SET role_id = :role_id, updated_at = NOW() WHERE id = :uid");
$upd->execute(['role_id' => $roleId, 'uid' => $authUserId]);
}
}
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,153 @@
<?php
include('../../include/headscript.php');
header('Content-Type: application/json; charset=utf-8');
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = isset($_POST['id']) && $_POST['id'] !== '' ? (int)$_POST['id'] : null;
$employeeId = (int)($_POST['employee_id'] ?? 0);
$ppeItemId = (int)($_POST['ppe_item_id'] ?? 0);
$assignedDate = trim($_POST['assigned_date'] ?? '');
$expiryDate = trim($_POST['expiry_date'] ?? '');
$deliveredBy = trim($_POST['delivered_by'] ?? '');
$status = trim($_POST['status'] ?? 'assigned');
$notes = trim($_POST['notes'] ?? '');
$allowedStatuses = [
'assigned',
'returned',
'expired',
'lost',
'damaged',
];
if ($employeeId <= 0) {
echo json_encode([
'success' => false,
'message' => 'Dipendente non valido.'
]);
exit;
}
if ($ppeItemId <= 0) {
echo json_encode([
'success' => false,
'message' => 'Selezionare un DPI.'
]);
exit;
}
if (!in_array($status, $allowedStatuses, true)) {
$status = 'assigned';
}
$checkEmployee = $pdo->prepare("SELECT id FROM employees WHERE id = ? LIMIT 1");
$checkEmployee->execute([$employeeId]);
if (!$checkEmployee->fetchColumn()) {
echo json_encode([
'success' => false,
'message' => 'Dipendente non trovato.'
]);
exit;
}
$checkPpe = $pdo->prepare("SELECT id FROM ppe_items WHERE id = ? LIMIT 1");
$checkPpe->execute([$ppeItemId]);
if (!$checkPpe->fetchColumn()) {
echo json_encode([
'success' => false,
'message' => 'DPI non trovato.'
]);
exit;
}
if ($id) {
$stmt = $pdo->prepare("
UPDATE employee_ppe_items
SET ppe_item_id = :ppe_item_id,
assigned_date = :assigned_date,
expiry_date = :expiry_date,
delivered_by = :delivered_by,
status = :status,
notes = :notes,
updated_at = NOW()
WHERE id = :id
AND employee_id = :employee_id
");
$stmt->execute([
'ppe_item_id' => $ppeItemId,
'assigned_date' => $assignedDate !== '' ? $assignedDate : null,
'expiry_date' => $expiryDate !== '' ? $expiryDate : null,
'delivered_by' => $deliveredBy !== '' ? $deliveredBy : null,
'status' => $status,
'notes' => $notes !== '' ? $notes : null,
'id' => $id,
'employee_id' => $employeeId,
]);
echo json_encode([
'success' => true,
'message' => 'DPI aggiornato.'
]);
exit;
}
$stmt = $pdo->prepare("
INSERT INTO employee_ppe_items
(
employee_id,
ppe_item_id,
assigned_date,
expiry_date,
delivered_by,
quantity,
status,
notes,
created_by,
created_at,
updated_at
)
VALUES
(
:employee_id,
:ppe_item_id,
:assigned_date,
:expiry_date,
:delivered_by,
1,
:status,
:notes,
:created_by,
NOW(),
NOW()
)
");
$stmt->execute([
'employee_id' => $employeeId,
'ppe_item_id' => $ppeItemId,
'assigned_date' => $assignedDate !== '' ? $assignedDate : null,
'expiry_date' => $expiryDate !== '' ? $expiryDate : null,
'delivered_by' => $deliveredBy !== '' ? $deliveredBy : null,
'status' => $status,
'notes' => $notes !== '' ? $notes : null,
'created_by' => isset($iduserlogin) ? (int)$iduserlogin : null,
]);
echo json_encode([
'success' => true,
'message' => 'DPI assegnato.'
]);
exit;
} catch (Throwable $e) {
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
exit;
}
@@ -0,0 +1,177 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
$employeeId = (int)($_POST['employee_id'] ?? 0);
$topicId = (int)($_POST['training_topic_id'] ?? 0);
$completedDate = trim($_POST['completed_date'] ?? '');
$deliveredBy = trim($_POST['delivered_by'] ?? '');
$description = trim($_POST['description'] ?? '');
$trainingType = trim($_POST['training_type'] ?? 'initial');
$freqRaw = $_POST['update_frequency_months'] ?? '';
$remRaw = $_POST['reminder_days'] ?? '';
if ($employeeId <= 0) {
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
exit;
}
if ($topicId <= 0) {
echo json_encode(['success' => false, 'message' => 'Selezionare un corso.']);
exit;
}
if ($completedDate === '') {
echo json_encode(['success' => false, 'message' => 'La data di completamento è obbligatoria.']);
exit;
}
if (!in_array($trainingType, ['initial', 'refresher'], true)) {
$trainingType = 'initial';
}
$topicStmt = $pdo->prepare("SELECT default_frequency_months, default_reminder_days FROM training_topics WHERE id = :id");
$topicStmt->execute(['id' => $topicId]);
$topic = $topicStmt->fetch(PDO::FETCH_ASSOC);
if (!$topic) {
echo json_encode(['success' => false, 'message' => 'Corso non trovato.']);
exit;
}
$freq = ($freqRaw === '' || $freqRaw === null) ? null : max(0, (int)$freqRaw);
$rem = ($remRaw === '' || $remRaw === null) ? null : max(0, (int)$remRaw);
/* Effective frequency for next_due_date: explicit override or topic default */
$effFreq = $freq !== null ? $freq : ($topic['default_frequency_months'] !== null ? (int)$topic['default_frequency_months'] : null);
$nextDue = null;
if ($effFreq !== null && $effFreq > 0) {
$d = DateTime::createFromFormat('Y-m-d', $completedDate);
if ($d) {
$d->modify('+' . (int)$effFreq . ' months');
$nextDue = $d->format('Y-m-d');
}
}
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
$description = $description !== '' ? $description : null;
try {
$pdo->beginTransaction();
if ($id > 0) {
$old = $pdo->prepare("SELECT * FROM employee_trainings WHERE id = :id");
$old->execute(['id' => $id]);
$oldRow = $old->fetch(PDO::FETCH_ASSOC);
if (!$oldRow) {
$pdo->rollBack();
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
exit;
}
$upd = $pdo->prepare("
UPDATE employee_trainings
SET training_topic_id = :topic_id,
completed_date = :completed_date,
delivered_by = :delivered_by,
description = :description,
training_type = :training_type,
update_frequency_months = :freq,
reminder_days = :rem,
next_due_date = :next_due,
updated_at = NOW()
WHERE id = :id
");
$upd->execute([
'topic_id' => $topicId,
'completed_date' => $completedDate,
'delivered_by' => $deliveredBy,
'description' => $description,
'training_type' => $trainingType,
'freq' => $freq,
'rem' => $rem,
'next_due' => $nextDue,
'id' => $id,
]);
$fields = [
'training_topic_id' => $topicId,
'completed_date' => $completedDate,
'delivered_by' => $deliveredBy,
'description' => $description,
'training_type' => $trainingType,
'update_frequency_months' => $freq,
'reminder_days' => $rem,
'next_due_date' => $nextDue,
];
$logStmt = $pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, :tid, 'updated', :field, :old_v, :new_v, :cb, NOW())
");
foreach ($fields as $f => $newV) {
$oldV = $oldRow[$f] ?? null;
if ((string)$oldV !== (string)$newV) {
$logStmt->execute([
'eid' => $employeeId,
'tid' => $id,
'field' => $f,
'old_v' => $oldV,
'new_v' => $newV,
'cb' => $currentUserId,
]);
}
}
$pdo->commit();
echo json_encode(['success' => true, 'id' => $id]);
exit;
}
$ins = $pdo->prepare("
INSERT INTO employee_trainings
(employee_id, training_topic_id, completed_date,
delivered_by, description,
training_type, update_frequency_months, reminder_days, next_due_date,
created_by, created_at, updated_at)
VALUES
(:eid, :tid, :completed_date,
:delivered_by, :description,
:training_type, :freq, :rem, :next_due,
:cb, NOW(), NOW())
");
$ins->execute([
'eid' => $employeeId,
'tid' => $topicId,
'completed_date' => $completedDate,
'delivered_by' => $deliveredBy,
'description' => $description,
'training_type' => $trainingType,
'freq' => $freq,
'rem' => $rem,
'next_due' => $nextDue,
'cb' => $currentUserId,
]);
$newId = (int)$pdo->lastInsertId();
$pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, :tid, 'created', NULL, NULL, NULL, :cb, NOW())
")->execute(['eid' => $employeeId, 'tid' => $newId, 'cb' => $currentUserId]);
$pdo->commit();
echo json_encode(['success' => true, 'id' => $newId]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,89 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$employeeId = (int)($_POST['employee_id'] ?? 0);
$category = trim($_POST['category'] ?? 'other');
$notes = trim($_POST['notes'] ?? '');
$allowedCategories = ['job_description', 'contract', 'rules', 'other'];
if (!in_array($category, $allowedCategories, true)) {
$category = 'other';
}
if ($employeeId <= 0) {
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
exit;
}
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE id = :id");
$check->execute(['id' => $employeeId]);
if ((int)$check->fetchColumn() === 0) {
echo json_encode(['success' => false, 'message' => 'Dipendente non trovato.']);
exit;
}
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
$errCode = $_FILES['file']['error'] ?? -1;
$msg = 'Errore nel caricamento del file.';
if ($errCode === UPLOAD_ERR_INI_SIZE || $errCode === UPLOAD_ERR_FORM_SIZE) {
$msg = 'Il file supera la dimensione massima consentita.';
}
echo json_encode(['success' => false, 'message' => $msg]);
exit;
}
$originalName = $_FILES['file']['name'];
$tmpPath = $_FILES['file']['tmp_name'];
$size = (int)$_FILES['file']['size'];
$mimeType = mime_content_type($tmpPath) ?: ($_FILES['file']['type'] ?? null);
$dir = __DIR__ . '/../../files/employees/' . $employeeId . '/documents';
if (!is_dir($dir)) {
if (!mkdir($dir, 0775, true) && !is_dir($dir)) {
echo json_encode(['success' => false, 'message' => 'Impossibile creare la cartella di destinazione.']);
exit;
}
}
$safeOriginal = preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName);
$storedName = uniqid('doc_') . '_' . $safeOriginal;
$destPath = $dir . '/' . $storedName;
if (!move_uploaded_file($tmpPath, $destPath)) {
echo json_encode(['success' => false, 'message' => 'Impossibile salvare il file su disco.']);
exit;
}
try {
$stmt = $pdo->prepare("
INSERT INTO employee_documents
(employee_id, category, original_name, stored_name, mime_type, size, notes, uploaded_by, created_at)
VALUES
(:employee_id, :category, :original_name, :stored_name, :mime_type, :size, :notes, :uploaded_by, NOW())
");
$stmt->execute([
'employee_id' => $employeeId,
'category' => $category,
'original_name' => $originalName,
'stored_name' => $storedName,
'mime_type' => $mimeType,
'size' => $size,
'notes' => $notes !== '' ? $notes : null,
'uploaded_by' => $currentUserId,
]);
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
} catch (Exception $e) {
@unlink($destPath);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,98 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$trainingId = (int)($_POST['training_id'] ?? 0);
if ($trainingId <= 0) {
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
exit;
}
$tr = $pdo->prepare("SELECT employee_id FROM employee_trainings WHERE id = :id");
$tr->execute(['id' => $trainingId]);
$trainingRow = $tr->fetch(PDO::FETCH_ASSOC);
if (!$trainingRow) {
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
exit;
}
$employeeId = (int)$trainingRow['employee_id'];
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
$errCode = $_FILES['file']['error'] ?? -1;
$msg = 'Errore nel caricamento del file.';
if ($errCode === UPLOAD_ERR_INI_SIZE || $errCode === UPLOAD_ERR_FORM_SIZE) {
$msg = 'Il file supera la dimensione massima consentita.';
}
echo json_encode(['success' => false, 'message' => $msg]);
exit;
}
$originalName = $_FILES['file']['name'];
$tmpPath = $_FILES['file']['tmp_name'];
$size = (int)$_FILES['file']['size'];
$mimeType = mime_content_type($tmpPath) ?: ($_FILES['file']['type'] ?? null);
$dir = __DIR__ . '/../../files/employees/' . $employeeId . '/trainings';
if (!is_dir($dir)) {
if (!mkdir($dir, 0775, true) && !is_dir($dir)) {
echo json_encode(['success' => false, 'message' => 'Impossibile creare la cartella di destinazione.']);
exit;
}
}
$safeOriginal = preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName);
$storedName = uniqid('tr_') . '_' . $safeOriginal;
$destPath = $dir . '/' . $storedName;
if (!move_uploaded_file($tmpPath, $destPath)) {
echo json_encode(['success' => false, 'message' => 'Impossibile salvare il file su disco.']);
exit;
}
try {
$pdo->beginTransaction();
$ins = $pdo->prepare("
INSERT INTO employee_training_attachments
(training_id, original_name, stored_name, mime_type, size, uploaded_by, created_at)
VALUES
(:tid, :original_name, :stored_name, :mime_type, :size, :uploaded_by, NOW())
");
$ins->execute([
'tid' => $trainingId,
'original_name' => $originalName,
'stored_name' => $storedName,
'mime_type' => $mimeType,
'size' => $size,
'uploaded_by' => $currentUserId,
]);
$attachmentId = (int)$pdo->lastInsertId();
$pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, :tid, 'attachment_added', 'attachment', NULL, :name, :cb, NOW())
")->execute([
'eid' => $employeeId,
'tid' => $trainingId,
'name' => $originalName,
'cb' => $currentUserId,
]);
$pdo->commit();
echo json_encode(['success' => true, 'id' => $attachmentId]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
@unlink($destPath);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+32
View File
@@ -0,0 +1,32 @@
<?php
/**
* HR auth check for AJAX endpoints that require HR-management permissions.
* Allowed roles: Admin, User, Superuser, employee-hr, manager.
* Sets $currentUserId and $currentUserRole, or returns 401/403 JSON.
*/
require_once(__DIR__ . '/auth_check.php');
require_once(__DIR__ . '/../class/db-functions.php');
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->prepare("
SELECT r.name AS role_name
FROM auth_users u
LEFT JOIN auth_roles r ON r.id = u.role_id
WHERE u.id = :id
LIMIT 1
");
$stmt->execute(['id' => $currentUserId]);
$currentUserRole = (string)$stmt->fetchColumn();
$allowedHrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
if (!in_array($currentUserRole, $allowedHrRoles, true)) {
header('Content-Type: application/json');
http_response_code(403);
echo json_encode([
'success' => false,
'message' => 'Permessi insufficienti per questa operazione.',
]);
exit;
}
+38
View File
@@ -0,0 +1,38 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'ID mansione non valido.']);
exit;
}
try {
$usage = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE job_role_id = :id");
$usage->execute(['id' => $id]);
if ((int)$usage->fetchColumn() > 0) {
echo json_encode([
'success' => false,
'message' => 'Questa mansione è associata a uno o più dipendenti e non può essere cancellata.',
]);
exit;
}
$stmt = $pdo->prepare("DELETE FROM job_roles WHERE id = :id");
$stmt->execute(['id' => $id]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+77
View File
@@ -0,0 +1,77 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$description = trim($_POST['description'] ?? '');
$sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 999;
$is_active = isset($_POST['is_active']) ? ((int)$_POST['is_active'] === 1 ? 1 : 0) : 1;
if ($name === '') {
echo json_encode(['success' => false, 'message' => 'Il nome della mansione è obbligatorio.']);
exit;
}
try {
if ($id > 0) {
$check = $pdo->prepare("SELECT COUNT(*) FROM job_roles WHERE name = :name AND id <> :id");
$check->execute(['name' => $name, 'id' => $id]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode(['success' => false, 'message' => 'Esiste già un\'altra mansione con questo nome.']);
exit;
}
$stmt = $pdo->prepare("
UPDATE job_roles
SET name = :name,
description = :description,
sort_order = :sort_order,
is_active = :is_active,
updated_at = NOW()
WHERE id = :id
");
$stmt->execute([
'name' => $name,
'description' => $description !== '' ? $description : null,
'sort_order' => $sort_order,
'is_active' => $is_active,
'id' => $id,
]);
echo json_encode(['success' => true, 'id' => $id]);
exit;
}
$check = $pdo->prepare("SELECT COUNT(*) FROM job_roles WHERE name = :name");
$check->execute(['name' => $name]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode(['success' => false, 'message' => 'Esiste già una mansione con questo nome.']);
exit;
}
$stmt = $pdo->prepare("
INSERT INTO job_roles (name, description, sort_order, is_active, created_at, updated_at)
VALUES (:name, :description, :sort_order, :is_active, NOW(), NOW())
");
$stmt->execute([
'name' => $name,
'description' => $description !== '' ? $description : null,
'sort_order' => $sort_order,
'is_active' => $is_active,
]);
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,38 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'ID corso non valido.']);
exit;
}
try {
$usage = $pdo->prepare("SELECT COUNT(*) FROM employee_trainings WHERE training_topic_id = :id");
$usage->execute(['id' => $id]);
if ((int)$usage->fetchColumn() > 0) {
echo json_encode([
'success' => false,
'message' => 'Questo corso ha già delle registrazioni di formazione e non può essere cancellato.',
]);
exit;
}
$stmt = $pdo->prepare("DELETE FROM training_topics WHERE id = :id");
$stmt->execute(['id' => $id]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,94 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$description = trim($_POST['description'] ?? '');
$freqRaw = $_POST['default_frequency_months'] ?? '';
$remRaw = $_POST['default_reminder_days'] ?? '';
$sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 999;
$is_active = isset($_POST['is_active']) ? ((int)$_POST['is_active'] === 1 ? 1 : 0) : 1;
$is_mandatory = isset($_POST['is_mandatory']) && (int)$_POST['is_mandatory'] === 1 ? 1 : 0;
$freq = ($freqRaw === '' || $freqRaw === null) ? null : max(0, (int)$freqRaw);
$rem = ($remRaw === '' || $remRaw === null) ? 30 : max(0, (int)$remRaw);
if ($name === '') {
echo json_encode(['success' => false, 'message' => 'Il nome del corso è obbligatorio.']);
exit;
}
try {
if ($id > 0) {
$check = $pdo->prepare("SELECT COUNT(*) FROM training_topics WHERE name = :name AND id <> :id");
$check->execute(['name' => $name, 'id' => $id]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode(['success' => false, 'message' => 'Esiste già un altro corso con questo nome.']);
exit;
}
$stmt = $pdo->prepare("
UPDATE training_topics
SET name = :name,
description = :description,
default_frequency_months = :freq,
default_reminder_days = :rem,
sort_order = :sort_order,
is_active = :is_active,
is_mandatory = :is_mandatory,
updated_at = NOW()
WHERE id = :id
");
$stmt->execute([
'name' => $name,
'description' => $description !== '' ? $description : null,
'freq' => $freq,
'rem' => $rem,
'sort_order' => $sort_order,
'is_active' => $is_active,
'is_mandatory' => $is_mandatory,
'id' => $id,
]);
echo json_encode(['success' => true, 'id' => $id]);
exit;
}
$check = $pdo->prepare("SELECT COUNT(*) FROM training_topics WHERE name = :name");
$check->execute(['name' => $name]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode(['success' => false, 'message' => 'Esiste già un corso con questo nome.']);
exit;
}
$stmt = $pdo->prepare("
INSERT INTO training_topics
(name, description, default_frequency_months, default_reminder_days, sort_order, is_active, is_mandatory, created_at, updated_at)
VALUES
(:name, :description, :freq, :rem, :sort_order, :is_active, :is_mandatory, NOW(), NOW())
");
$stmt->execute([
'name' => $name,
'description' => $description !== '' ? $description : null,
'freq' => $freq,
'rem' => $rem,
'sort_order' => $sort_order,
'is_active' => $is_active,
'is_mandatory' => $is_mandatory,
]);
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,104 @@
<?php
/**
* Bulk "renew": set a common completed_date on the selected training records
*/
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
// $pdo and $currentUserId from hr_auth_check.php
$completedDate = trim($_POST['completed_date'] ?? '');
$ids = $_POST['training_ids'] ?? [];
if (!is_array($ids)) {
$ids = [];
}
$ids = array_values(array_unique(array_filter(array_map('intval', $ids), fn($v) => $v > 0)));
if ($completedDate === '' || !DateTime::createFromFormat('Y-m-d', $completedDate)) {
echo json_encode(['success' => false, 'message' => 'Indicare una data valida.']);
exit;
}
if (empty($ids)) {
echo json_encode(['success' => false, 'message' => 'Selezionare almeno un record.']);
exit;
}
try {
$pdo->beginTransaction();
// Load each record with its topic default frequency
$rowStmt = $pdo->prepare("
SELECT et.id, et.employee_id, et.completed_date, et.next_due_date,
et.update_frequency_months, tt.default_frequency_months
FROM employee_trainings et
JOIN training_topics tt ON tt.id = et.training_topic_id
WHERE et.id = :id
");
$upd = $pdo->prepare("
UPDATE employee_trainings
SET completed_date = :cd, next_due_date = :nd, updated_at = NOW()
WHERE id = :id
");
$logStmt = $pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, :tid, 'updated', :field, :old_v, :new_v, :cb, NOW())
");
$updated = 0;
foreach ($ids as $id) {
$rowStmt->execute(['id' => $id]);
$row = $rowStmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
continue;
}
// Effective frequency: per-record override, else topic default
$effFreq = $row['update_frequency_months'] !== null
? (int)$row['update_frequency_months']
: ($row['default_frequency_months'] !== null ? (int)$row['default_frequency_months'] : null);
$nextDue = null;
if ($effFreq !== null && $effFreq > 0) {
$d = DateTime::createFromFormat('Y-m-d', $completedDate);
if ($d) {
$d->modify('+' . $effFreq . ' months');
$nextDue = $d->format('Y-m-d');
}
}
$upd->execute(['cd' => $completedDate, 'nd' => $nextDue, 'id' => $id]);
if ((string)$row['completed_date'] !== (string)$completedDate) {
$logStmt->execute([
'eid' => $row['employee_id'], 'tid' => $id, 'field' => 'completed_date',
'old_v' => $row['completed_date'], 'new_v' => $completedDate, 'cb' => $currentUserId,
]);
}
if ((string)$row['next_due_date'] !== (string)$nextDue) {
$logStmt->execute([
'eid' => $row['employee_id'], 'tid' => $id, 'field' => 'next_due_date',
'old_v' => $row['next_due_date'], 'new_v' => $nextDue, 'cb' => $currentUserId,
]);
}
$updated++;
}
$pdo->commit();
echo json_encode([
'success' => true,
'updated' => $updated,
'message' => $updated . ' record aggiornat' . ($updated === 1 ? 'o' : 'i') . '.',
]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,92 @@
<?php
/**
* Calendar events for the training calendar (training_calendar.php).
* Returns FullCalendar event objects for the *current* training record per
* (employee, topic) that has a next_due_date, colored by computed status.
* HR-only.
*/
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
try {
// $pdo and $currentUserId provided by hr_auth_check.php
$start = $_GET['start'] ?? null;
$end = $_GET['end'] ?? null;
$fStatus = isset($_GET['status']) ? trim($_GET['status']) : '';
$fDept = isset($_GET['department_id']) && $_GET['department_id'] !== '' ? (int)$_GET['department_id'] : 0;
$fTopic = isset($_GET['topic_id']) && $_GET['topic_id'] !== '' ? (int)$_GET['topic_id'] : 0;
$fEmp = isset($_GET['employee_id']) && $_GET['employee_id'] !== '' ? (int)$_GET['employee_id'] : 0;
$where = [];
$params = [];
// Deadlines only (one-time trainings have no next_due_date)
$where[] = "et.next_due_date IS NOT NULL";
// Only the most recent record per (employee, topic)
$where[] = "NOT EXISTS (
SELECT 1 FROM employee_trainings et2
WHERE et2.employee_id = et.employee_id
AND et2.training_topic_id = et.training_topic_id
AND (et2.completed_date > et.completed_date
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
)";
if ($start && $end) {
$where[] = "et.next_due_date >= :start AND et.next_due_date <= :end";
$params['start'] = $start;
$params['end'] = $end;
}
if ($fDept > 0) { $where[] = "e.department_id = :did"; $params['did'] = $fDept; }
if ($fTopic > 0) { $where[] = "et.training_topic_id = :tid"; $params['tid'] = $fTopic; }
if ($fEmp > 0) { $where[] = "et.employee_id = :eid"; $params['eid'] = $fEmp; }
$whereSql = 'WHERE ' . implode(' AND ', $where);
$stmt = $pdo->prepare("
SELECT et.id, et.employee_id, et.next_due_date, et.reminder_days,
tt.name AS topic_name, tt.default_reminder_days AS topic_default_rem,
e.first_name, e.last_name
FROM employee_trainings et
JOIN training_topics tt ON tt.id = et.training_topic_id
JOIN employees e ON e.id = et.employee_id
$whereSql
");
$stmt->execute($params);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$today = new DateTime('today');
$events = [];
foreach ($rows as $r) {
$rem = $r['reminder_days'] !== null
? (int)$r['reminder_days']
: ($r['topic_default_rem'] !== null ? (int)$r['topic_default_rem'] : 30);
$due = DateTime::createFromFormat('Y-m-d', $r['next_due_date']);
if (!$due) continue;
$daysLeft = (int)$today->diff($due)->format('%r%a');
if ($daysLeft < 0) { $code = 'expired'; $color = '#dc3545'; }
elseif ($daysLeft <= $rem){ $code = 'due_soon'; $color = '#e8930c'; }
else { $code = 'compliant'; $color = '#198754'; }
if ($fStatus !== '' && $fStatus !== $code) continue;
$name = trim($r['first_name'] . ' ' . $r['last_name']);
$events[] = [
'id' => (int)$r['id'],
'title' => $name . ' — ' . $r['topic_name'],
'start' => $r['next_due_date'],
'allDay' => true,
'backgroundColor' => $color,
'borderColor' => $color,
'url' => 'employee-profile.php?id=' . (int)$r['employee_id'] . '#tab-training',
];
}
echo json_encode($events);
} catch (Exception $e) {
echo json_encode([]);
}
@@ -0,0 +1,131 @@
<?php
/**
* Bulk-create training records: one employee_trainings row per selected employee,
* all sharing the same course + parameters (a single training "session").
* Mirrors the next_due_date logic of ajax/employee_profile/save_training.php.
* HR-only.
*/
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
// $pdo and $currentUserId from hr_auth_check.php
$topicId = (int)($_POST['training_topic_id'] ?? 0);
$completedDate = trim($_POST['completed_date'] ?? '');
$deliveredBy = trim($_POST['delivered_by'] ?? '');
$description = trim($_POST['description'] ?? '');
$trainingType = trim($_POST['training_type'] ?? 'initial');
$freqRaw = $_POST['update_frequency_months'] ?? '';
$remRaw = $_POST['reminder_days'] ?? '';
$employeeIds = $_POST['employee_ids'] ?? [];
if (!is_array($employeeIds)) {
$employeeIds = [];
}
$employeeIds = array_values(array_unique(array_filter(array_map('intval', $employeeIds), fn($v) => $v > 0)));
if ($topicId <= 0) {
echo json_encode(['success' => false, 'message' => 'Selezionare un corso.']);
exit;
}
if ($completedDate === '' || !DateTime::createFromFormat('Y-m-d', $completedDate)) {
echo json_encode(['success' => false, 'message' => 'La data di completamento è obbligatoria.']);
exit;
}
if (empty($employeeIds)) {
echo json_encode(['success' => false, 'message' => 'Selezionare almeno un dipendente.']);
exit;
}
if (!in_array($trainingType, ['initial', 'refresher'], true)) {
$trainingType = 'initial';
}
$topicStmt = $pdo->prepare("SELECT default_frequency_months, default_reminder_days FROM training_topics WHERE id = :id");
$topicStmt->execute(['id' => $topicId]);
$topic = $topicStmt->fetch(PDO::FETCH_ASSOC);
if (!$topic) {
echo json_encode(['success' => false, 'message' => 'Corso non trovato.']);
exit;
}
$freq = ($freqRaw === '' || $freqRaw === null) ? null : max(0, (int)$freqRaw);
$rem = ($remRaw === '' || $remRaw === null) ? null : max(0, (int)$remRaw);
/* Effective frequency → next_due_date (same for every employee: same date + same frequency) */
$effFreq = $freq !== null ? $freq : ($topic['default_frequency_months'] !== null ? (int)$topic['default_frequency_months'] : null);
$nextDue = null;
if ($effFreq !== null && $effFreq > 0) {
$d = DateTime::createFromFormat('Y-m-d', $completedDate);
if ($d) {
$d->modify('+' . (int)$effFreq . ' months');
$nextDue = $d->format('Y-m-d');
}
}
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
$description = $description !== '' ? $description : null;
try {
$pdo->beginTransaction();
// Only insert for employees that actually exist
$checkEmp = $pdo->prepare("SELECT id FROM employees WHERE id = :id");
$ins = $pdo->prepare("
INSERT INTO employee_trainings
(employee_id, training_topic_id, completed_date,
delivered_by, description,
training_type, update_frequency_months, reminder_days, next_due_date,
created_by, created_at, updated_at)
VALUES
(:eid, :tid, :completed_date,
:delivered_by, :description,
:training_type, :freq, :rem, :next_due,
:cb, NOW(), NOW())
");
$logStmt = $pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, :tid, 'created', NULL, NULL, NULL, :cb, NOW())
");
$created = 0;
foreach ($employeeIds as $eid) {
$checkEmp->execute(['id' => $eid]);
if (!$checkEmp->fetchColumn()) {
continue;
}
$ins->execute([
'eid' => $eid,
'tid' => $topicId,
'completed_date' => $completedDate,
'delivered_by' => $deliveredBy,
'description' => $description,
'training_type' => $trainingType,
'freq' => $freq,
'rem' => $rem,
'next_due' => $nextDue,
'cb' => $currentUserId,
]);
$newId = (int)$pdo->lastInsertId();
$logStmt->execute(['eid' => $eid, 'tid' => $newId, 'cb' => $currentUserId]);
$created++;
}
$pdo->commit();
echo json_encode([
'success' => true,
'created' => $created,
'message' => $created . ' formazion' . ($created === 1 ? 'e registrata' : 'i registrate') . '.',
]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,254 @@
<?php
ob_start();
ini_set('display_errors', 1);
error_reporting(E_ALL);
require_once(__DIR__ . '/../../../extra/auth.php');
require_once(__DIR__ . '/../class/db-functions.php');
while (ob_get_level()) {
ob_end_clean();
}
header('Content-Type: application/json; charset=utf-8');
if (!class_exists('Auth')) {
echo json_encode([
'success' => false,
'message' => 'Classe Auth non disponibile'
]);
exit;
}
if (!Auth::check()) {
echo json_encode([
'success' => false,
'message' => 'Sessione non valida o utente non autenticato'
]);
exit;
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
function formatDateIT($d)
{
if (!$d || $d === '0000-00-00') return '';
return date("d/m/Y", strtotime($d));
}
function formatDateTimeIT($d)
{
if (!$d || $d === '0000-00-00 00:00:00') return '';
return date("d/m/Y H:i", strtotime($d));
}
function worksheetNumberLabel($n)
{
$n = (int)$n;
return $n > 0 ? 'FL' . $n : '—';
}
function revisionLabel($rev)
{
$rev = trim((string)$rev);
return $rev !== '' ? $rev : '0';
}
$action = $_POST['action'] ?? '';
try {
if ($action === 'get_matrice_worksheets') {
$idmatrice = isset($_POST['idmatrice']) ? (int)$_POST['idmatrice'] : 0;
if ($idmatrice <= 0) {
echo json_encode([
'success' => false,
'message' => 'ID matrice non valido'
]);
exit;
}
$stmt = $pdo->prepare("
SELECT
ws.id,
ws.idmatrice,
ws.worksheet_number,
ws.revision_code,
ws.worksheet_status,
ws.worksheet_date,
ws.customer_name,
ws.profile_type_code,
ws.marking,
ws.approved_by,
ws.created_at,
ws.updated_at,
(
SELECT COUNT(*)
FROM work_sheet_mescole wsm
WHERE wsm.worksheet_id = ws.id
) AS mix_count
FROM work_sheets ws
WHERE ws.idmatrice = ?
ORDER BY
ws.worksheet_number DESC,
CASE
WHEN ws.revision_code IS NULL OR ws.revision_code = '' THEN 0
WHEN ws.revision_code REGEXP '^R[0-9]+$' THEN CAST(SUBSTRING(ws.revision_code, 2) AS UNSIGNED)
ELSE 0
END DESC,
ws.id DESC
");
$stmt->execute([$idmatrice]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$data = [];
foreach ($rows as $r) {
$data[] = [
'id' => (int)$r['id'],
'idmatrice' => (int)$r['idmatrice'],
'worksheet_number' => (int)($r['worksheet_number'] ?? 0),
'worksheet_number_label' => worksheetNumberLabel($r['worksheet_number'] ?? 0),
'revision_code' => $r['revision_code'] ?? '',
'revision_label' => revisionLabel($r['revision_code'] ?? ''),
'worksheet_status' => $r['worksheet_status'] ?? 'active',
'worksheet_status_label' => (($r['worksheet_status'] ?? 'active') === 'inactive') ? 'Inattivo' : 'Attivo',
'worksheet_date' => $r['worksheet_date'],
'worksheet_date_it' => formatDateIT($r['worksheet_date']),
'customer_name' => $r['customer_name'] ?? '',
'profile_type_code' => $r['profile_type_code'] ?? '',
'marking' => $r['marking'] ?? '',
'approved_by' => $r['approved_by'] ?? '',
'created_at' => $r['created_at'] ?? '',
'created_at_it' => formatDateTimeIT($r['created_at'] ?? ''),
'updated_at' => $r['updated_at'] ?? '',
'updated_at_it' => formatDateTimeIT($r['updated_at'] ?? ''),
'mix_count' => (int)($r['mix_count'] ?? 0)
];
}
echo json_encode([
'success' => true,
'worksheets' => $data
]);
exit;
}
if ($action === 'get_worksheet_detail') {
$worksheetId = isset($_POST['worksheet_id']) ? (int)$_POST['worksheet_id'] : 0;
if ($worksheetId <= 0) {
echo json_encode([
'success' => false,
'message' => 'ID foglio non valido'
]);
exit;
}
$stmt = $pdo->prepare("
SELECT
ws.*,
m.nome AS matrice_nome,
m.cliente AS matrice_cliente
FROM work_sheets ws
LEFT JOIN matrice m ON m.id = ws.idmatrice
WHERE ws.id = ?
LIMIT 1
");
$stmt->execute([$worksheetId]);
$ws = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$ws) {
echo json_encode([
'success' => false,
'message' => 'Foglio di lavoro non trovato'
]);
exit;
}
$stmtMix = $pdo->prepare("
SELECT
wsm.*,
me.nome AS mescola_nome,
me.nomeuscita AS mescola_uscita
FROM work_sheet_mescole wsm
LEFT JOIN mescole me ON me.id = wsm.idmescola
WHERE wsm.worksheet_id = ?
ORDER BY wsm.mix_position ASC, wsm.id ASC
");
$stmtMix->execute([$worksheetId]);
$mixRows = $stmtMix->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'worksheet' => [
'id' => (int)$ws['id'],
'worksheet_number' => (int)($ws['worksheet_number'] ?? 0),
'worksheet_number_label' => worksheetNumberLabel($ws['worksheet_number'] ?? 0),
'revision_code' => $ws['revision_code'] ?? '',
'revision_label' => revisionLabel($ws['revision_code'] ?? ''),
'worksheet_status' => $ws['worksheet_status'] ?? 'active',
'worksheet_status_label' => (($ws['worksheet_status'] ?? 'active') === 'inactive') ? 'Inattivo' : 'Attivo',
'idmatrice' => (int)$ws['idmatrice'],
'matrice_nome' => $ws['matrice_nome'] ?? '',
'matrice_cliente' => $ws['matrice_cliente'] ?? '',
'worksheet_date' => $ws['worksheet_date'] ?? '',
'worksheet_date_it' => formatDateIT($ws['worksheet_date'] ?? ''),
'customer_name' => $ws['customer_name'] ?? '',
'profile_type_code' => $ws['profile_type_code'] ?? '',
'marking' => $ws['marking'] ?? '',
'prod_control_measure_settings' => $ws['prod_control_measure_settings'] ?? '',
'control_frequency_cut' => $ws['control_frequency_cut'] ?? '',
'control_frequency_drawing' => $ws['control_frequency_drawing'] ?? '',
'control_frequency_jig' => $ws['control_frequency_jig'] ?? '',
'control_mode_jig' => $ws['control_mode_jig'] ?? '',
'requested_package_code' => $ws['requested_package_code'] ?? '',
'meters_per_package' => $ws['meters_per_package'] ?? '',
'meters_per_package_tolerance' => $ws['meters_per_package_tolerance'] ?? '',
'meters_per_package_notes' => $ws['meters_per_package_notes'] ?? '',
'box_type' => $ws['box_type'] ?? '',
'packages_or_pieces_per_box' => $ws['packages_or_pieces_per_box'] ?? '',
'meters_per_box' => $ws['meters_per_box'] ?? '',
'pallet_type' => $ws['pallet_type'] ?? '',
'boxes_or_packages_per_pallet' => $ws['boxes_or_packages_per_pallet'] ?? '',
'speed_expected_kg_h' => $ws['speed_expected_kg_h'] ?? '',
'speed_actual_kg_h' => $ws['speed_actual_kg_h'] ?? '',
'speed_expected_m_h' => $ws['speed_expected_m_h'] ?? '',
'speed_actual_m_h' => $ws['speed_actual_m_h'] ?? '',
'approved_by' => $ws['approved_by'] ?? '',
'notes' => $ws['notes'] ?? '',
'created_at' => $ws['created_at'] ?? '',
'created_at_it' => formatDateTimeIT($ws['created_at'] ?? ''),
'updated_at' => $ws['updated_at'] ?? '',
'updated_at_it' => formatDateTimeIT($ws['updated_at'] ?? '')
],
'mix_rows' => array_map(function ($r) {
return [
'id' => (int)$r['id'],
'mix_position' => (int)$r['mix_position'],
'mescola_nome' => $r['mescola_nome'] ?? '',
'mescola_uscita' => $r['mescola_uscita'] ?? '',
'mix_weight_g_m' => $r['mix_weight_g_m'] ?? '',
'required_density' => $r['required_density'] ?? '',
'required_hardness_shore_a' => $r['required_hardness_shore_a'] ?? '',
'lubrication_type' => $r['lubrication_type'] ?? '',
'lubrication_notes' => $r['lubrication_notes'] ?? ''
];
}, $mixRows)
]);
exit;
}
echo json_encode([
'success' => false,
'message' => 'Azione AJAX sconosciuta'
]);
exit;
} catch (Exception $e) {
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
exit;
}
@@ -0,0 +1,362 @@
(function () {
function escapeHtml(str) {
return String(str || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function valueOrDash(v) {
return v === null || v === undefined || String(v).trim() === ""
? "—"
: escapeHtml(v);
}
function renderReadonlyField(label, value) {
return `
<div class="readonly-label">${escapeHtml(label)}</div>
<div class="readonly-value">${valueOrDash(value)}</div>
`;
}
window.loadMatriceWorksheets = function (idmatrice, matriceNome, endpoint) {
endpoint = endpoint || "ajax/worksheet-linked-data.php";
$("#wl_idmatrice").val(idmatrice);
$("#wl_matrice_name").html(
`<span class="worksheet-chip"><i class="fa-solid fa-layer-group"></i>${escapeHtml(matriceNome || "")}</span>`,
);
$("#worksheetsListContainer").html(
'<div class="text-muted">Caricamento fogli di lavoro...</div>',
);
fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body:
"action=get_matrice_worksheets&idmatrice=" +
encodeURIComponent(idmatrice),
})
.then(async (r) => {
const text = await r.text();
try {
return JSON.parse(text);
} catch (e) {
console.error("Risposta non JSON worksheets:", text);
throw new Error("Risposta non JSON, vedi console");
}
})
.then((data) => {
if (!data.success) {
$("#worksheetsListContainer").html(
'<div class="text-danger">Errore nel caricamento dei fogli</div>',
);
return;
}
const rows = data.worksheets || [];
if (!rows.length) {
$("#worksheetsListContainer").html(
'<div class="text-muted">Nessun foglio di lavoro collegato a questo profilo</div>',
);
return;
}
let html = `
<div class="table-responsive">
<table class="table table-striped align-middle worksheet-list-table">
<thead>
<tr>
<th style="width:110px;">Foglio</th>
<th style="width:100px;">Rev.</th>
<th style="width:110px;">Stato</th>
<th style="width:140px;">Data foglio</th>
<th>Cliente</th>
<th>Codice profilo</th>
<th style="width:110px;">Mescole</th>
<th style="width:230px;">Azioni</th>
</tr>
</thead>
<tbody>
`;
rows.forEach((r) => {
const statusBadge =
r.worksheet_status === "inactive"
? `<span class="worksheet-badge-status-inactive">Inattivo</span>`
: `<span class="worksheet-badge-status-active">Attivo</span>`;
html += `
<tr>
<td><span class="worksheet-badge-fl">${escapeHtml(r.worksheet_number_label || "—")}</span></td>
<td><span class="worksheet-badge-rev">${escapeHtml(r.revision_label || "0")}</span></td>
<td>${statusBadge}</td>
<td>${escapeHtml(r.worksheet_date_it || "—")}</td>
<td title="${escapeHtml(r.customer_name || "")}">${escapeHtml(r.customer_name || "—")}</td>
<td>${escapeHtml(r.profile_type_code || "—")}</td>
<td>${escapeHtml(String(r.mix_count || 0))}</td>
<td class="text-nowrap">
<button type="button"
class="btn btn-view-worksheet open-worksheet-detail"
data-id="${r.id}"
data-endpoint="${escapeHtml(endpoint)}">
<i class="fa-solid fa-eye me-1"></i>Apri dettaglio
</button>
<a class="worksheet-open-link ms-1"
href="manage-worksheet.php?id=${r.id}"
target="_blank">
<i class="fa-solid fa-arrow-up-right-from-square"></i>Apri pagina
</a>
</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
`;
$("#worksheetsListContainer").html(html);
})
.catch(() => {
$("#worksheetsListContainer").html(
'<div class="text-danger">Errore nel caricamento dei fogli</div>',
);
});
};
window.loadWorksheetDetail = function (worksheetId, endpoint) {
endpoint = endpoint || "ajax/worksheet-linked-data.php";
$("#worksheetDetailContainer").html(
'<div class="text-muted">Caricamento dettaglio foglio...</div>',
);
fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body:
"action=get_worksheet_detail&worksheet_id=" +
encodeURIComponent(worksheetId),
})
.then(async (r) => {
const text = await r.text();
try {
return JSON.parse(text);
} catch (e) {
console.error("Risposta non JSON worksheet detail:", text);
throw new Error("Risposta non JSON, vedi console");
}
})
.then((data) => {
if (!data.success) {
console.error("Errore worksheets:", data);
$("#worksheetsListContainer").html(
'<div class="text-danger">Errore nel caricamento dei fogli: ' +
escapeHtml(
data.message || "nessun dettaglio restituito",
) +
"</div>",
);
return;
}
const ws = data.worksheet || {};
const mixRows = data.mix_rows || [];
let mixHtml = `<div class="text-muted">Nessuna mescola associata</div>`;
if (mixRows.length) {
mixHtml = `
<div class="table-responsive">
<table class="table table-striped align-middle mix-readonly-table">
<thead>
<tr>
<th style="width:80px;">Pos</th>
<th>Mescola</th>
<th style="width:120px;">Peso g/m</th>
<th style="width:140px;">Densità</th>
<th style="width:150px;">Durezza</th>
<th style="width:130px;">Lubr.</th>
<th>Note lubr.</th>
</tr>
</thead>
<tbody>
`;
mixRows.forEach((r) => {
const nomeMescola = r.mescola_uscita
? `${escapeHtml(r.mescola_nome || "—")} <div class="small text-muted">${escapeHtml(r.mescola_uscita)}</div>`
: escapeHtml(r.mescola_nome || "—");
mixHtml += `
<tr>
<td>${escapeHtml(String(r.mix_position || ""))}</td>
<td>${nomeMescola}</td>
<td>${valueOrDash(r.mix_weight_g_m)}</td>
<td>${valueOrDash(r.required_density)}</td>
<td>${valueOrDash(r.required_hardness_shore_a)}</td>
<td>${valueOrDash(r.lubrication_type)}</td>
<td>${valueOrDash(r.lubrication_notes)}</td>
</tr>
`;
});
mixHtml += `
</tbody>
</table>
</div>
`;
}
const html = `
<div class="worksheet-title-box mb-4">
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap">
<div>
<h4 class="mb-1">${escapeHtml(ws.worksheet_number_label || "—")} / Rev. ${escapeHtml(ws.revision_label || "0")}</h4>
<small>
Stato: ${escapeHtml(ws.worksheet_status_label || "Attivo")}
${ws.matrice_nome ? " • Profilo: " + escapeHtml(ws.matrice_nome) : ""}
${ws.matrice_cliente ? " • Cliente matrice: " + escapeHtml(ws.matrice_cliente) : ""}
</small>
</div>
<div>
<a href="manage-worksheet.php?id=${escapeHtml(String(ws.id || ""))}"
target="_blank"
class="worksheet-open-link">
<i class="fa-solid fa-arrow-up-right-from-square"></i>
Apri foglio completo
</a>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-lg-6">
<div class="readonly-card">
<div class="readonly-card-header">Dati principali</div>
<div class="readonly-card-body">
<div class="readonly-grid">
${renderReadonlyField("Foglio", ws.worksheet_number_label)}
${renderReadonlyField("Revisione", ws.revision_label)}
${renderReadonlyField("Stato", ws.worksheet_status_label)}
${renderReadonlyField("Data foglio", ws.worksheet_date_it)}
${renderReadonlyField("Cliente override", ws.customer_name)}
${renderReadonlyField("Codice profilo", ws.profile_type_code)}
${renderReadonlyField("Marchiatura", ws.marking)}
${renderReadonlyField("Approvato da", ws.approved_by)}
${renderReadonlyField("Creato il", ws.created_at_it)}
${renderReadonlyField("Ultimo aggiornamento", ws.updated_at_it)}
</div>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="readonly-card">
<div class="readonly-card-header">Controlli produzione</div>
<div class="readonly-card-body">
<div class="readonly-grid">
${renderReadonlyField("Taglio", ws.control_frequency_cut)}
${renderReadonlyField("Disegno", ws.control_frequency_drawing)}
${renderReadonlyField("Dima", ws.control_frequency_jig)}
${renderReadonlyField("Modalità dima", ws.control_mode_jig)}
</div>
<hr>
<div class="readonly-label mb-2">Impostazione misure controllo produzione</div>
<div class="readonly-value" style="white-space:pre-wrap;">${valueOrDash(ws.prod_control_measure_settings)}</div>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="readonly-card">
<div class="readonly-card-header">Packaging / Confezionamento</div>
<div class="readonly-card-body">
<div class="readonly-grid">
${renderReadonlyField("Codice confezione", ws.requested_package_code)}
${renderReadonlyField("Metri per confezione", ws.meters_per_package)}
${renderReadonlyField("Tolleranza metri/conf.", ws.meters_per_package_tolerance)}
${renderReadonlyField("Scatola tipo", ws.box_type)}
${renderReadonlyField("Conf./pezzi per scatola", ws.packages_or_pieces_per_box)}
${renderReadonlyField("Metri per scatola", ws.meters_per_box)}
${renderReadonlyField("Bancale tipo", ws.pallet_type)}
${renderReadonlyField("Scatole/conf. per bancale", ws.boxes_or_packages_per_pallet)}
</div>
<hr>
<div class="readonly-label mb-2">Note metri / confezione</div>
<div class="readonly-value" style="white-space:pre-wrap;">${valueOrDash(ws.meters_per_package_notes)}</div>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="readonly-card">
<div class="readonly-card-header">Velocità e note</div>
<div class="readonly-card-body">
<div class="readonly-grid">
${renderReadonlyField("Vel. prevista kg/h", ws.speed_expected_kg_h)}
${renderReadonlyField("Vel. effettiva kg/h", ws.speed_actual_kg_h)}
${renderReadonlyField("Vel. prevista m/h", ws.speed_expected_m_h)}
${renderReadonlyField("Vel. effettiva m/h", ws.speed_actual_m_h)}
</div>
<hr>
<div class="readonly-label mb-2">Note</div>
<div class="readonly-value" style="white-space:pre-wrap;">${valueOrDash(ws.notes)}</div>
</div>
</div>
</div>
<div class="col-12">
<div class="readonly-card">
<div class="readonly-card-header">Mescole associate al foglio</div>
<div class="readonly-card-body">
${mixHtml}
</div>
</div>
</div>
</div>
`;
$("#worksheetDetailContainer").html(html);
})
.catch((err) => {
console.error("Catch loadMatriceWorksheets:", err);
$("#worksheetsListContainer").html(
'<div class="text-danger">Errore nel caricamento dei fogli: ' +
escapeHtml(err.message || "errore JavaScript/fetch") +
"</div>",
);
});
};
$(document).on("click", ".worksheets, .show-worksheets", function () {
const idmatrice = $(this).data("id");
const nome = $(this).data("nome") || "";
const endpoint =
$(this).data("endpoint") || "ajax/worksheet-linked-data.php";
loadMatriceWorksheets(idmatrice, nome, endpoint);
new bootstrap.Modal(
document.getElementById("worksheetsListModal"),
).show();
});
$(document).on("click", ".open-worksheet-detail", function () {
const worksheetId = $(this).data("id");
const endpoint =
$(this).data("endpoint") || "ajax/worksheet-linked-data.php";
loadWorksheetDetail(worksheetId, endpoint);
new bootstrap.Modal(
document.getElementById("worksheetDetailModal"),
).show();
});
})();
File diff suppressed because it is too large Load Diff
+71
View File
@@ -0,0 +1,71 @@
<?php
header('Content-Type: application/json');
require_once(__DIR__ . '/include/headscript.php');
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$iduser = $iduserlogin ?? null;
$input = json_decode(file_get_contents('php://input'), true);
$id = (int)($input['id'] ?? 0);
if ($id <= 0) {
throw new Exception('ID non valido.');
}
if ($iduser === null) {
$stmt = $pdo->prepare("
SELECT *
FROM cad_area_jobs
WHERE id = :id
LIMIT 1
");
$stmt->execute([
':id' => $id
]);
} else {
$stmt = $pdo->prepare("
SELECT *
FROM cad_area_jobs
WHERE id = :id
AND iduser = :iduser
LIMIT 1
");
$stmt->execute([
':id' => $id,
':iduser' => $iduser
]);
}
$job = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$job) {
throw new Exception('Record non trovato.');
}
if (!empty($job['file_path']) && file_exists($job['file_path'])) {
unlink($job['file_path']);
}
$stmtDelete = $pdo->prepare("
DELETE FROM cad_area_jobs
WHERE id = :id
");
$stmtDelete->execute([':id' => $id]);
echo json_encode([
'success' => true
]);
} catch (Throwable $e) {
error_log('CAD area delete error: ' . $e->getMessage());
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
+338
View File
@@ -0,0 +1,338 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once(__DIR__ . '/include/headscript.php');
function jsonResponse(array $data): void
{
echo json_encode($data);
exit;
}
function updateJobProcessing(PDO $pdo, int $id): void
{
$stmt = $pdo->prepare("
UPDATE cad_area_jobs
SET
status = 'processing',
message = 'Elaborazione in corso...',
updated_at = NOW()
WHERE id = ?
");
$stmt->execute([$id]);
}
function updateJobError(PDO $pdo, int $id, string $message, ?array $pythonResponse = null): void
{
$stmt = $pdo->prepare("
UPDATE cad_area_jobs
SET
status = 'error',
message = ?,
python_response = ?,
updated_at = NOW()
WHERE id = ?
");
$stmt->execute([
$message,
$pythonResponse ? json_encode($pythonResponse) : null,
$id
]);
}
function updateJobCompleted(PDO $pdo, int $id, array $response): void
{
$stmt = $pdo->prepare("
UPDATE cad_area_jobs
SET
status = 'completed',
message = ?,
area_mm2 = ?,
area_cm2 = ?,
area_m2 = ?,
width_mm = ?,
height_mm = ?,
scale_detected = ?,
scale_used = ?,
confidence = ?,
python_response = ?,
updated_at = NOW()
WHERE id = ?
");
$stmt->execute([
$response['message'] ?? 'Area calcolata correttamente.',
$response['area_mm2'] ?? null,
$response['area_cm2'] ?? null,
$response['area_m2'] ?? null,
$response['width_mm'] ?? null,
$response['height_mm'] ?? null,
$response['scale_detected'] ?? null,
$response['scale_used'] ?? null,
$response['confidence'] ?? null,
json_encode($response),
$id
]);
}
function normalizeCalculationMode(?string $mode): string
{
$allowed = [
'auto_roi',
'stitch_contour',
'filled_union',
'closed_path'
];
if (!$mode || !in_array($mode, $allowed, true)) {
return 'auto_roi';
}
return $mode;
}
function hasValidRoi(array $job): bool
{
return (
array_key_exists('roi_x', $job) &&
array_key_exists('roi_y', $job) &&
array_key_exists('roi_width', $job) &&
array_key_exists('roi_height', $job) &&
$job['roi_x'] !== null &&
$job['roi_y'] !== null &&
$job['roi_width'] !== null &&
$job['roi_height'] !== null &&
(float)$job['roi_width'] > 0 &&
(float)$job['roi_height'] > 0
);
}
function callPythonAreaService(string $url, array $job): array
{
$filePath = $job['file_path'] ?? '';
$originalFilename = $job['original_filename'] ?? basename($filePath);
if (!$filePath || !file_exists($filePath)) {
return [
'success' => false,
'message' => 'File PDF non trovato sul server: ' . $filePath
];
}
$mode = normalizeCalculationMode($job['calculation_mode'] ?? 'auto_roi');
$scaleRatio = $job['scale_used'] ?? null;
if ($scaleRatio === null || $scaleRatio === '' || (float)$scaleRatio <= 0) {
$scaleRatio = '1';
}
$curlFile = new CURLFile(
$filePath,
'application/pdf',
$originalFilename
);
$postFields = [
'file' => $curlFile,
'mode' => $mode,
'scale_ratio' => (string)$scaleRatio,
'roi_x' => (string)$job['roi_x'],
'roi_y' => (string)$job['roi_y'],
'roi_width' => (string)$job['roi_width'],
'roi_height' => (string)$job['roi_height'],
'roi_page' => (string)($job['roi_page'] ?? 1)
];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postFields,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 180,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_HTTPHEADER => [
'Accept: application/json'
]
]);
$rawResponse = curl_exec($ch);
$curlError = curl_error($ch);
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($rawResponse === false) {
return [
'success' => false,
'message' => 'Errore cURL verso Python: ' . $curlError
];
}
$decoded = json_decode($rawResponse, true);
if (!is_array($decoded)) {
return [
'success' => false,
'message' => 'Risposta Python non JSON valida.',
'http_code' => $httpCode,
'raw_response' => $rawResponse
];
}
if ($httpCode < 200 || $httpCode >= 300) {
return [
'success' => false,
'message' => $decoded['message'] ?? ('Servizio Python HTTP ' . $httpCode),
'http_code' => $httpCode,
'python_response' => $decoded,
'raw_response' => $rawResponse
];
}
return $decoded;
}
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$iduser = $iduserlogin ?? null;
$input = json_decode(file_get_contents('php://input'), true);
if (!is_array($input)) {
jsonResponse([
'success' => false,
'message' => 'Payload JSON non valido.'
]);
}
$ids = $input['ids'] ?? [];
if (!is_array($ids) || count($ids) === 0) {
jsonResponse([
'success' => false,
'message' => 'Nessun ID ricevuto.'
]);
}
$ids = array_values(array_unique(array_map('intval', $ids)));
$ids = array_filter($ids, fn($id) => $id > 0);
if (count($ids) === 0) {
jsonResponse([
'success' => false,
'message' => 'Nessun ID valido ricevuto.'
]);
}
$pythonServiceUrl = 'http://127.0.0.1:5055/calculate';
$results = [];
foreach ($ids as $id) {
if ($iduser === null || $iduser === '') {
$stmt = $pdo->prepare("
SELECT *
FROM cad_area_jobs
WHERE id = ?
LIMIT 1
");
$stmt->execute([$id]);
} else {
$stmt = $pdo->prepare("
SELECT *
FROM cad_area_jobs
WHERE id = ?
AND iduser = ?
LIMIT 1
");
$stmt->execute([$id, $iduser]);
}
$job = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$job) {
$results[] = [
'id' => $id,
'success' => false,
'message' => 'Record non trovato.'
];
continue;
}
if (!hasValidRoi($job)) {
$message = 'Prima devi definire la sezione da misurare tramite il pulsante Sezione.';
updateJobError($pdo, $id, $message, [
'success' => false,
'message' => $message,
'job_roi_debug' => [
'roi_x' => $job['roi_x'] ?? null,
'roi_y' => $job['roi_y'] ?? null,
'roi_width' => $job['roi_width'] ?? null,
'roi_height' => $job['roi_height'] ?? null,
'roi_page' => $job['roi_page'] ?? null,
'calculation_mode' => $job['calculation_mode'] ?? null
]
]);
$results[] = [
'id' => $id,
'success' => false,
'message' => $message
];
continue;
}
updateJobProcessing($pdo, $id);
$pythonResponse = callPythonAreaService($pythonServiceUrl, $job);
if (!($pythonResponse['success'] ?? false)) {
$message = $pythonResponse['message'] ?? 'Errore durante il calcolo Python.';
updateJobError($pdo, $id, $message, $pythonResponse);
$results[] = [
'id' => $id,
'success' => false,
'message' => $message,
'python_response' => $pythonResponse
];
continue;
}
updateJobCompleted($pdo, $id, $pythonResponse);
$results[] = [
'id' => $id,
'success' => true,
'message' => $pythonResponse['message'] ?? 'Area calcolata.',
'area_mm2' => $pythonResponse['area_mm2'] ?? null,
'area_cm2' => $pythonResponse['area_cm2'] ?? null
];
}
jsonResponse([
'success' => true,
'results' => $results
]);
} catch (Throwable $e) {
error_log('CAD area process error: ' . $e->getMessage());
jsonResponse([
'success' => false,
'message' => $e->getMessage()
]);
}
@@ -0,0 +1,297 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once(__DIR__ . '/include/headscript.php');
function jsonResponse(array $data): void
{
echo json_encode($data);
exit;
}
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$iduser = $iduserlogin ?? null;
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);
if (!is_array($input)) {
jsonResponse([
'success' => false,
'message' => 'Payload JSON non valido.'
]);
}
$id = (int)($input['id'] ?? 0);
if ($id <= 0) {
jsonResponse([
'success' => false,
'message' => 'ID non valido.'
]);
}
$areaMm2 = isset($input['area_mm2']) ? (float)$input['area_mm2'] : 0;
$areaCm2 = isset($input['area_cm2']) ? (float)$input['area_cm2'] : 0;
$outerAreaMm2 = isset($input['manual_outer_area_mm2']) ? (float)$input['manual_outer_area_mm2'] : $areaMm2;
$holesAreaMm2 = isset($input['manual_holes_area_mm2']) ? (float)$input['manual_holes_area_mm2'] : 0;
$widthMm = isset($input['width_mm']) ? (float)$input['width_mm'] : null;
$heightMm = isset($input['height_mm']) ? (float)$input['height_mm'] : null;
$calibrationPx = isset($input['manual_calibration_px']) ? (float)$input['manual_calibration_px'] : 0;
$calibrationMm = isset($input['manual_calibration_mm']) ? (float)$input['manual_calibration_mm'] : 0;
$mmPerPx = isset($input['manual_mm_per_px']) ? (float)$input['manual_mm_per_px'] : 0;
$outerPolygon = $input['manual_polygon'] ?? null;
$holes = $input['manual_holes'] ?? [];
$roi = $input['roi'] ?? null;
if ($areaMm2 <= 0) {
jsonResponse([
'success' => false,
'message' => 'Area finale non valida.'
]);
}
if ($outerAreaMm2 <= 0) {
jsonResponse([
'success' => false,
'message' => 'Area esterna non valida.'
]);
}
if ($holesAreaMm2 < 0) {
jsonResponse([
'success' => false,
'message' => 'Area fori non valida.'
]);
}
if ($calibrationPx <= 0 || $calibrationMm <= 0 || $mmPerPx <= 0) {
jsonResponse([
'success' => false,
'message' => 'Calibrazione non valida.'
]);
}
if (!is_array($outerPolygon) || count($outerPolygon) < 3) {
jsonResponse([
'success' => false,
'message' => 'Poligono esterno non valido. Servono almeno 3 punti.'
]);
}
if (!is_array($holes)) {
$holes = [];
}
$manualPolygonJson = json_encode([
'outer_polygon' => $outerPolygon,
'holes' => $holes,
'roi' => $roi,
'calibration' => $input['calibration'] ?? null,
'canvas' => $input['canvas'] ?? null,
'areas' => [
'outer_area_mm2' => $outerAreaMm2,
'holes_area_mm2' => $holesAreaMm2,
'final_area_mm2' => $areaMm2,
'final_area_cm2' => $areaCm2
]
]);
$manualHolesJson = json_encode($holes);
$roiX = null;
$roiY = null;
$roiW = null;
$roiH = null;
if (is_array($roi)) {
$roiX = isset($roi['x']) ? (float)$roi['x'] : null;
$roiY = isset($roi['y']) ? (float)$roi['y'] : null;
$roiW = isset($roi['width']) ? (float)$roi['width'] : null;
$roiH = isset($roi['height']) ? (float)$roi['height'] : null;
}
if ($iduser === null || $iduser === '') {
$stmt = $pdo->prepare("
UPDATE cad_area_jobs
SET
roi_x = COALESCE(?, roi_x),
roi_y = COALESCE(?, roi_y),
roi_width = COALESCE(?, roi_width),
roi_height = COALESCE(?, roi_height),
roi_page = 1,
status = 'completed',
message = 'Area calcolata tramite tracciamento manuale calibrato.',
area_mm2 = ?,
area_cm2 = ?,
area_m2 = ?,
manual_area_mm2 = ?,
manual_area_cm2 = ?,
manual_outer_area_mm2 = ?,
manual_holes_area_mm2 = ?,
manual_width_mm = ?,
manual_height_mm = ?,
width_mm = ?,
height_mm = ?,
manual_calibration_px = ?,
manual_calibration_mm = ?,
manual_mm_per_px = ?,
manual_polygon_json = ?,
manual_holes_json = ?,
manual_status = 'completed',
scale_used = ?,
scale_detected = ?,
confidence = 'manual_validated',
strategy_used = 'manual_tracing_with_exclusions',
python_response = NULL,
updated_at = NOW()
WHERE id = ?
");
$stmt->execute([
$roiX,
$roiY,
$roiW,
$roiH,
$areaMm2,
$areaCm2,
$areaMm2 / 1000000,
$areaMm2,
$areaCm2,
$outerAreaMm2,
$holesAreaMm2,
$widthMm,
$heightMm,
$widthMm,
$heightMm,
$calibrationPx,
$calibrationMm,
$mmPerPx,
$manualPolygonJson,
$manualHolesJson,
$mmPerPx,
'manual',
$id
]);
} else {
$stmt = $pdo->prepare("
UPDATE cad_area_jobs
SET
roi_x = COALESCE(?, roi_x),
roi_y = COALESCE(?, roi_y),
roi_width = COALESCE(?, roi_width),
roi_height = COALESCE(?, roi_height),
roi_page = 1,
status = 'completed',
message = 'Area calcolata tramite tracciamento manuale calibrato.',
area_mm2 = ?,
area_cm2 = ?,
area_m2 = ?,
manual_area_mm2 = ?,
manual_area_cm2 = ?,
manual_outer_area_mm2 = ?,
manual_holes_area_mm2 = ?,
manual_width_mm = ?,
manual_height_mm = ?,
width_mm = ?,
height_mm = ?,
manual_calibration_px = ?,
manual_calibration_mm = ?,
manual_mm_per_px = ?,
manual_polygon_json = ?,
manual_holes_json = ?,
manual_status = 'completed',
scale_used = ?,
scale_detected = ?,
confidence = 'manual_validated',
strategy_used = 'manual_tracing_with_exclusions',
python_response = NULL,
updated_at = NOW()
WHERE id = ?
AND iduser = ?
");
$stmt->execute([
$roiX,
$roiY,
$roiW,
$roiH,
$areaMm2,
$areaCm2,
$areaMm2 / 1000000,
$areaMm2,
$areaCm2,
$outerAreaMm2,
$holesAreaMm2,
$widthMm,
$heightMm,
$widthMm,
$heightMm,
$calibrationPx,
$calibrationMm,
$mmPerPx,
$manualPolygonJson,
$manualHolesJson,
$mmPerPx,
'manual',
$id,
$iduser
]);
}
if ($stmt->rowCount() === 0) {
jsonResponse([
'success' => false,
'message' => 'Nessun record aggiornato. Controlla ID o utente.'
]);
}
jsonResponse([
'success' => true,
'message' => 'Area manuale salvata correttamente.',
'area_mm2' => $areaMm2,
'area_cm2' => $areaCm2,
'outer_area_mm2' => $outerAreaMm2,
'holes_area_mm2' => $holesAreaMm2
]);
} catch (Throwable $e) {
error_log('CAD manual area save error: ' . $e->getMessage());
jsonResponse([
'success' => false,
'message' => $e->getMessage()
]);
}
+124
View File
@@ -0,0 +1,124 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once(__DIR__ . '/include/headscript.php');
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$iduser = $iduserlogin ?? null;
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);
if (!is_array($input)) {
throw new Exception('Payload JSON non valido.');
}
$id = (int)($input['id'] ?? 0);
if ($id <= 0) {
throw new Exception('ID non valido.');
}
$roiX = isset($input['roi_x']) ? (float)$input['roi_x'] : null;
$roiY = isset($input['roi_y']) ? (float)$input['roi_y'] : null;
$roiW = isset($input['roi_width']) ? (float)$input['roi_width'] : null;
$roiH = isset($input['roi_height']) ? (float)$input['roi_height'] : null;
$roiPage = isset($input['roi_page']) ? (int)$input['roi_page'] : 1;
$mode = $input['calculation_mode'] ?? 'auto_roi';
if ($roiX === null || $roiY === null || $roiW === null || $roiH === null) {
throw new Exception('ROI non valida.');
}
if ($roiW <= 0 || $roiH <= 0) {
throw new Exception('Dimensioni ROI non valide.');
}
if ($roiX < 0 || $roiY < 0 || $roiX > 1 || $roiY > 1 || $roiW > 1 || $roiH > 1) {
throw new Exception('Coordinate ROI fuori scala.');
}
$allowedModes = [
'auto_roi',
'stitch_contour',
'filled_union',
'closed_path'
];
if (!in_array($mode, $allowedModes, true)) {
$mode = 'auto_roi';
}
if ($iduser === null || $iduser === '') {
$stmt = $pdo->prepare("
UPDATE cad_area_jobs
SET
roi_x = ?,
roi_y = ?,
roi_width = ?,
roi_height = ?,
roi_page = ?,
calculation_mode = ?,
status = 'uploaded',
message = NULL
WHERE id = ?
");
$stmt->execute([
$roiX,
$roiY,
$roiW,
$roiH,
$roiPage,
$mode,
$id
]);
} else {
$stmt = $pdo->prepare("
UPDATE cad_area_jobs
SET
roi_x = ?,
roi_y = ?,
roi_width = ?,
roi_height = ?,
roi_page = ?,
calculation_mode = ?,
status = 'uploaded',
message = NULL
WHERE id = ?
AND iduser = ?
");
$stmt->execute([
$roiX,
$roiY,
$roiW,
$roiH,
$roiPage,
$mode,
$id,
$iduser
]);
}
if ($stmt->rowCount() === 0) {
throw new Exception('Nessun record aggiornato. Controlla ID o utente.');
}
echo json_encode([
'success' => true,
'message' => 'ROI salvata correttamente.'
]);
exit;
} catch (Throwable $e) {
error_log('CAD area save ROI error: ' . $e->getMessage());
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
exit;
}
+106
View File
@@ -0,0 +1,106 @@
<?php
header('Content-Type: application/json');
require_once(__DIR__ . '/include/headscript.php');
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$iduser = $iduserlogin ?? null;
$uploadDir = __DIR__ . '/uploads/cad_area/originals/';
$publicBaseUrl = 'uploads/cad_area/originals/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
if (empty($_FILES['pdf_files'])) {
throw new Exception('Nessun file ricevuto.');
}
$files = $_FILES['pdf_files'];
$insertedIds = [];
for ($i = 0; $i < count($files['name']); $i++) {
if ($files['error'][$i] !== UPLOAD_ERR_OK) {
continue;
}
$originalName = $files['name'][$i];
$tmpName = $files['tmp_name'][$i];
$size = (int)$files['size'][$i];
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if ($extension !== 'pdf') {
continue;
}
if ($size > 25 * 1024 * 1024) {
continue;
}
$safeBaseName = preg_replace('/[^a-zA-Z0-9_\-]/', '_', pathinfo($originalName, PATHINFO_FILENAME));
$storedName = date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '_' . $safeBaseName . '.pdf';
$targetPath = $uploadDir . $storedName;
if (!move_uploaded_file($tmpName, $targetPath)) {
continue;
}
$relativeUrl = $publicBaseUrl . $storedName;
$stmt = $pdo->prepare("
INSERT INTO cad_area_jobs
(
iduser,
original_filename,
stored_filename,
file_path,
file_url,
file_size,
status
)
VALUES
(
:iduser,
:original_filename,
:stored_filename,
:file_path,
:file_url,
:file_size,
'uploaded'
)
");
$stmt->execute([
':iduser' => $iduser,
':original_filename' => $originalName,
':stored_filename' => $storedName,
':file_path' => $targetPath,
':file_url' => $relativeUrl,
':file_size' => $size
]);
$insertedIds[] = (int)$pdo->lastInsertId();
}
if (empty($insertedIds)) {
throw new Exception('Nessun PDF valido caricato.');
}
echo json_encode([
'success' => true,
'ids' => $insertedIds
]);
} catch (Throwable $e) {
error_log('CAD area upload error: ' . $e->getMessage());
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
+617
View File
@@ -0,0 +1,617 @@
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
function jsonResponse(array $data): void
{
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data);
exit;
}
function normalizeNullableInt($value): ?int
{
return (isset($value) && $value !== '') ? (int)$value : null;
}
function normalizeBoolValue($value): int
{
return ((string)$value === '0') ? 0 : 1;
}
function cleanString(?string $value): string
{
return trim((string)$value);
}
/* ==========================================
AJAX HANDLERS
========================================== */
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['ajax'] == '1') {
$action = $_POST['action'] ?? '';
try {
if ($action === 'add') {
$functionName = cleanString($_POST['function_name'] ?? '');
$personFullName = cleanString($_POST['person_full_name'] ?? '');
$phone = cleanString($_POST['phone'] ?? '');
$email = cleanString($_POST['email'] ?? '');
$notes = cleanString($_POST['notes'] ?? '');
$sortOrder = normalizeNullableInt($_POST['sort_order'] ?? '0') ?? 0;
$isActive = normalizeBoolValue($_POST['is_active'] ?? '1');
if ($functionName === '') {
jsonResponse(['success' => false, 'message' => 'Il nome funzione è obbligatorio.']);
}
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
jsonResponse(['success' => false, 'message' => 'Email non valida.']);
}
$stmt = $pdo->prepare("\n INSERT INTO company_functions\n (function_name, person_full_name, phone, email, notes, sort_order, is_active, created_at, updated_at)\n VALUES\n (:function_name, :person_full_name, :phone, :email, :notes, :sort_order, :is_active, NOW(), NOW())\n ");
$stmt->execute([
'function_name' => $functionName,
'person_full_name' => $personFullName !== '' ? $personFullName : '',
'phone' => $phone !== '' ? $phone : null,
'email' => $email !== '' ? $email : null,
'notes' => $notes !== '' ? $notes : null,
'sort_order' => $sortOrder,
'is_active' => $isActive,
]);
jsonResponse(['success' => true, 'message' => 'Funzione salvata correttamente.']);
}
if ($action === 'edit') {
$id = (int)($_POST['id'] ?? 0);
$functionName = cleanString($_POST['function_name'] ?? '');
$personFullName = cleanString($_POST['person_full_name'] ?? '');
$phone = cleanString($_POST['phone'] ?? '');
$email = cleanString($_POST['email'] ?? '');
$notes = cleanString($_POST['notes'] ?? '');
$sortOrder = normalizeNullableInt($_POST['sort_order'] ?? '0') ?? 0;
$isActive = normalizeBoolValue($_POST['is_active'] ?? '1');
if ($id <= 0) {
jsonResponse(['success' => false, 'message' => 'ID funzione non valido.']);
}
if ($functionName === '') {
jsonResponse(['success' => false, 'message' => 'Il nome funzione è obbligatorio.']);
}
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
jsonResponse(['success' => false, 'message' => 'Email non valida.']);
}
$stmt = $pdo->prepare("\n UPDATE company_functions\n SET function_name = :function_name,\n person_full_name = :person_full_name,\n phone = :phone,\n email = :email,\n notes = :notes,\n sort_order = :sort_order,\n is_active = :is_active,\n updated_at = NOW()\n WHERE id = :id\n ");
$stmt->execute([
'function_name' => $functionName,
'person_full_name' => $personFullName !== '' ? $personFullName : '',
'phone' => $phone !== '' ? $phone : null,
'email' => $email !== '' ? $email : null,
'notes' => $notes !== '' ? $notes : null,
'sort_order' => $sortOrder,
'is_active' => $isActive,
'id' => $id,
]);
jsonResponse(['success' => true, 'message' => 'Funzione aggiornata correttamente.']);
}
if ($action === 'delete') {
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
jsonResponse(['success' => false, 'message' => 'ID funzione non valido.']);
}
$stmt = $pdo->prepare("DELETE FROM company_functions WHERE id = :id");
$stmt->execute(['id' => $id]);
jsonResponse(['success' => true, 'message' => 'Funzione cancellata correttamente.']);
}
jsonResponse(['success' => false, 'message' => 'Azione non riconosciuta.']);
} catch (Exception $e) {
jsonResponse(['success' => false, 'message' => $e->getMessage()]);
}
}
/* ==========================================
PAGE DATA
========================================== */
$stmtFunctions = $pdo->query("\n SELECT id, function_name, person_full_name, phone, email, notes, sort_order, is_active, created_at, updated_at\n FROM company_functions\n ORDER BY is_active DESC, sort_order ASC, function_name ASC, person_full_name ASC\n");
$functions = $stmtFunctions->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'); ?>
<title>Funzioni Aziendali - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<style>
body {
font-size: 1.05rem;
background: #f8fafc;
}
.card {
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.back-dashboard {
background-color: #cfe3ff !important;
color: #1f2d3d !important;
border: 1px solid #bcd4f4 !important;
border-radius: 10px;
font-weight: 600;
font-size: 1rem;
padding: 10px 18px;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease-in-out;
}
.back-dashboard:hover {
background-color: #b9d3ff !important;
transform: translateY(-2px);
}
.btn-main-action,
.btn-add {
background-color: #0d6efd;
color: #fff;
border-radius: 8px;
padding: 10px 20px;
font-weight: 500;
transition: all 0.2s ease-in-out;
}
.btn-main-action:hover,
.btn-add:hover {
background-color: #0b5ed7;
color: #fff;
transform: scale(1.02);
}
.table thead {
background-color: #cfe3ff;
color: #1f2d3d;
}
#tabCompanyFunctions thead th {
text-align: center;
vertical-align: middle;
}
.modal-content {
border-radius: 16px;
}
.function-name {
font-weight: 700;
color: #1f2937;
}
.person-name {
font-weight: 600;
color: #334155;
}
.contact-line {
display: block;
font-size: 0.9rem;
text-decoration: none;
}
.notes-small {
color: #64748b;
font-size: 0.9rem;
max-width: 420px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.badge-status {
padding: 0.25rem 0.65rem;
border-radius: 999px;
font-size: 0.82rem;
font-weight: 700;
}
.badge-status.active {
background-color: #d1fae5;
color: #065f46;
}
.badge-status.inactive {
background-color: #e5e7eb;
color: #374151;
}
.empty-text {
color: #94a3b8;
font-style: italic;
}
@media (max-width: 767.98px) {
.card-header {
flex-direction: column;
align-items: flex-start !important;
gap: .5rem;
}
.back-dashboard,
.btn-main-action {
width: 100%;
}
}
</style>
</head>
<body>
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card p-3">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h5 class="mb-0">Funzioni Aziendali</h5>
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
↩️ Torna alla Dashboard
</button>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<div>
<h6 class="fw-semibold mb-1">Elenco Funzioni</h6>
<div class="text-muted small">Gestione di RSPP, medico del lavoro, RLS e altre funzioni aziendali.</div>
</div>
<button class="btn btn-main-action" data-bs-toggle="modal" data-bs-target="#companyFunctionModal" onclick="openCompanyFunctionModal()">
Aggiungi Funzione
</button>
</div>
<div class="table-responsive">
<table id="tabCompanyFunctions" class="table table-striped align-middle text-center" style="width:100%;">
<thead>
<tr>
<th>Funzione</th>
<th>Nominativo</th>
<th>Contatti</th>
<th>Note</th>
<th>Ordine</th>
<th>Stato</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($functions as $row): ?>
<?php
$id = (int)$row['id'];
$functionName = (string)($row['function_name'] ?? '');
$personFullName = (string)($row['person_full_name'] ?? '');
$phone = (string)($row['phone'] ?? '');
$email = (string)($row['email'] ?? '');
$notes = (string)($row['notes'] ?? '');
$sortOrder = (int)($row['sort_order'] ?? 0);
$isActive = (int)($row['is_active'] ?? 1);
?>
<tr>
<td class="text-start">
<div class="function-name"><?= htmlspecialchars($functionName, ENT_QUOTES, 'UTF-8') ?></div>
</td>
<td class="text-start">
<?php if ($personFullName !== ''): ?>
<div class="person-name"><?= htmlspecialchars($personFullName, ENT_QUOTES, 'UTF-8') ?></div>
<?php else: ?>
<span class="empty-text">Da definire</span>
<?php endif; ?>
</td>
<td class="text-start">
<?php if ($phone !== ''): ?>
<a class="contact-line" href="tel:<?= htmlspecialchars($phone, ENT_QUOTES, 'UTF-8') ?>">
📞 <?= htmlspecialchars($phone, ENT_QUOTES, 'UTF-8') ?>
</a>
<?php endif; ?>
<?php if ($email !== ''): ?>
<a class="contact-line" href="mailto:<?= htmlspecialchars($email, ENT_QUOTES, 'UTF-8') ?>">
✉️ <?= htmlspecialchars($email, ENT_QUOTES, 'UTF-8') ?>
</a>
<?php endif; ?>
<?php if ($phone === '' && $email === ''): ?>
<span class="empty-text">Nessun contatto</span>
<?php endif; ?>
</td>
<td class="text-start">
<?php if ($notes !== ''): ?>
<div class="notes-small" title="<?= htmlspecialchars($notes, ENT_QUOTES, 'UTF-8') ?>">
<?= htmlspecialchars($notes, ENT_QUOTES, 'UTF-8') ?>
</div>
<?php else: ?>
<span class="empty-text"></span>
<?php endif; ?>
</td>
<td><?= $sortOrder ?></td>
<td>
<?php if ($isActive === 1): ?>
<span class="badge-status active">Attiva</span>
<?php else: ?>
<span class="badge-status inactive">Non attiva</span>
<?php endif; ?>
</td>
<td>
<button
type="button"
class="btn btn-sm btn-outline-secondary edit-function mb-1"
data-id="<?= $id ?>"
data-function_name="<?= htmlspecialchars($functionName, ENT_QUOTES, 'UTF-8') ?>"
data-person_full_name="<?= htmlspecialchars($personFullName, ENT_QUOTES, 'UTF-8') ?>"
data-phone="<?= htmlspecialchars($phone, ENT_QUOTES, 'UTF-8') ?>"
data-email="<?= htmlspecialchars($email, ENT_QUOTES, 'UTF-8') ?>"
data-notes="<?= htmlspecialchars($notes, ENT_QUOTES, 'UTF-8') ?>"
data-sort_order="<?= $sortOrder ?>"
data-is_active="<?= $isActive ?>">
✏️ Modifica
</button>
<button
type="button"
class="btn btn-sm btn-outline-danger delete-function mb-1"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($functionName, ENT_QUOTES, 'UTF-8') ?>">
🗑️ Cancella
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<!-- MODALE ADD / EDIT FUNZIONE -->
<div class="modal fade" id="companyFunctionModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title" id="companyFunctionModalTitle">Aggiungi Funzione</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="companyFunctionForm">
<input type="hidden" id="functionId">
<div class="mb-3">
<label class="form-label fw-semibold">Nome funzione <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="functionName" placeholder="Es. RSPP, Medico del lavoro, RLS" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Nome e Cognome persona</label>
<input type="text" class="form-control" id="personFullName" placeholder="Es. Mario Rossi">
</div>
<div class="row">
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Telefono</label>
<input type="text" class="form-control" id="phone" placeholder="Es. +39 333 1234567">
</div>
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Email</label>
<input type="email" class="form-control" id="email" placeholder="nome@azienda.it">
</div>
</div>
<div class="row">
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Ordine</label>
<input type="number" class="form-control" id="sortOrder" value="0" min="0" step="1">
<small class="text-muted">Serve solo per ordinare la visualizzazione.</small>
</div>
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Stato</label>
<select class="form-select" id="isActive">
<option value="1">Attiva</option>
<option value="0">Non attiva</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Note</label>
<textarea class="form-control" id="notes" rows="3" placeholder="Note interne, riferimenti, disponibilità, ecc."></textarea>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add">💾 Salva</button>
</div>
</form>
</div>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
function escapeHtml(value) {
return $('<div>').text(value || '').html();
}
function openCompanyFunctionModal() {
$('#functionId').val('');
$('#functionName').val('');
$('#personFullName').val('');
$('#phone').val('');
$('#email').val('');
$('#notes').val('');
$('#sortOrder').val('0');
$('#isActive').val('1');
$('#companyFunctionModalTitle').text('Aggiungi Funzione');
}
$(document).ready(function() {
$('#tabCompanyFunctions').DataTable({
order: [
[5, 'asc'],
[4, 'asc'],
[0, 'asc']
],
pageLength: 25,
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json',
emptyTable: 'Nessuna funzione presente'
},
columnDefs: [{
targets: -1,
orderable: false,
searchable: false
}]
});
$(document).on('click', '.edit-function', function() {
const btn = $(this);
$('#functionId').val(btn.data('id'));
$('#functionName').val(btn.data('function_name'));
$('#personFullName').val(btn.data('person_full_name'));
$('#phone').val(btn.data('phone'));
$('#email').val(btn.data('email'));
$('#notes').val(btn.data('notes'));
$('#sortOrder').val(btn.data('sort_order'));
$('#isActive').val(String(btn.data('is_active')));
$('#companyFunctionModalTitle').text('Modifica Funzione');
$('#companyFunctionModal').modal('show');
});
$('#companyFunctionForm').on('submit', function(e) {
e.preventDefault();
const id = $('#functionId').val();
const payload = new URLSearchParams();
payload.append('ajax', '1');
payload.append('action', id ? 'edit' : 'add');
payload.append('id', id);
payload.append('function_name', $('#functionName').val().trim());
payload.append('person_full_name', $('#personFullName').val().trim());
payload.append('phone', $('#phone').val().trim());
payload.append('email', $('#email').val().trim());
payload.append('notes', $('#notes').val().trim());
payload.append('sort_order', $('#sortOrder').val() || '0');
payload.append('is_active', $('#isActive').val());
fetch('', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: payload.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: 'success',
title: 'Salvato!',
text: data.message || 'Operazione completata.',
confirmButtonColor: '#3085d6'
}).then(() => location.reload());
} else {
Swal.fire('Errore', data.message || 'Impossibile salvare.', 'error');
}
})
.catch(err => {
console.error(err);
Swal.fire('Errore', 'Errore di comunicazione.', 'error');
});
});
$(document).on('click', '.delete-function', function() {
const id = $(this).data('id');
const name = $(this).data('name');
Swal.fire({
title: 'Confermi la cancellazione?',
text: name ? ('Funzione: ' + name) : 'La funzione verrà cancellata.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#6c757d',
confirmButtonText: 'Sì, cancella',
cancelButtonText: 'Annulla'
}).then((result) => {
if (!result.isConfirmed) return;
const payload = new URLSearchParams();
payload.append('ajax', '1');
payload.append('action', 'delete');
payload.append('id', id);
fetch('', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: payload.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: 'success',
title: 'Cancellato!',
text: data.message || 'Funzione cancellata.',
confirmButtonColor: '#3085d6'
}).then(() => location.reload());
} else {
Swal.fire('Errore', data.message || 'Impossibile cancellare.', 'error');
}
})
.catch(err => {
console.error(err);
Swal.fire('Errore', 'Errore di comunicazione.', 'error');
});
});
});
});
</script>
</body>
</html>
@@ -8,14 +8,6 @@
<i class="bi bi-box-seam" style="font-size:1.8rem; color:#334155;"></i> <i class="bi bi-box-seam" style="font-size:1.8rem; color:#334155;"></i>
</button> </button>
<!-- Parametri macchina -->
<button class="photo-btn"
data-type="parametri_macchina"
data-production="<?= $r['id'] ?>"
title="Foto Parametri Macchina">
<i class="bi bi-speedometer" style="font-size:1.8rem; color:#334155;"></i>
</button>
<!-- Problemi --> <!-- Problemi -->
<button class="photo-btn" <button class="photo-btn"
data-type="problema" data-type="problema"
+1 -1
View File
@@ -1,4 +1,4 @@
<div id="photoModal" class="modal"> <div id="photoModal" class="custom-modal">
<div class="modal-content final-wide" style="max-width:800px; position:relative;"> <div class="modal-content final-wide" style="max-width:800px; position:relative;">
<!-- X per chiudere --> <!-- X per chiudere -->
@@ -0,0 +1,355 @@
<?php
/**
* Formazione Email reminder cron script
* Run daily: 0 7 * * * php /var/www/html/public/userarea/cron/send_training_reminders.php
*
* Sends "due_soon" emails when next_due_date is within the reminder window
* (override reminder_days > topic default > 30 days).
* Sends "expired" emails when next_due_date is in the past.
* Skips rows with next_due_date IS NULL (one-off trainings).
* Skips already-sent notifications (same training + addressee + next_due_date).
* Recipients: the employee (employees.email or auth_users.email) + every HR user
* with role Admin / Superuser / employee-hr / manager.
*
* Optional CLI flags:
* --dry-run log only, no SMTP, no DB write
* --only-email=foo@bar restrict to a single addressee (for testing)
*/
require_once __DIR__ . '/../class/db-functions.php';
require_once __DIR__ . '/../../../vendor/autoload.php';
use Dotenv\Dotenv;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
$dotenv = Dotenv::createImmutable(__DIR__ . '/../../../');
$dotenv->load();
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$today = date('Y-m-d');
$appUrl = rtrim($_ENV['APP_URL'] ?? 'http://localhost:8001', '/');
/* CLI flags */
$dryRun = false;
$onlyEmail = null;
foreach (array_slice($argv ?? [], 1) as $a) {
if ($a === '--dry-run' || $a === '-n') {
$dryRun = true;
} elseif (strpos($a, '--only-email=') === 0) {
$onlyEmail = substr($a, strlen('--only-email='));
}
}
$sent = 0;
$skipped = 0;
$errors = 0;
/* Candidate trainings (with optional override reminder + topic default).
Only the most recent record per (employee, topic) older history rows skipped. */
$stmt = $pdo->query("
SELECT et.id, et.employee_id, et.completed_date, et.next_due_date,
et.reminder_days, et.delivered_by,
tt.name AS topic_name, tt.default_reminder_days AS topic_default_rem,
e.first_name, e.last_name, e.employee_code,
e.email AS employee_email_direct,
au.email AS employee_email_auth
FROM employee_trainings et
JOIN training_topics tt ON tt.id = et.training_topic_id
JOIN employees e ON e.id = et.employee_id
LEFT JOIN auth_users au ON au.id = e.auth_user_id
WHERE et.next_due_date IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM employee_trainings et2
WHERE et2.employee_id = et.employee_id
AND et2.training_topic_id = et.training_topic_id
AND (et2.completed_date > et.completed_date
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
)
");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($rows)) {
echo date('Y-m-d H:i:s') . " — Nessuna formazione da notificare.\n";
exit(0);
}
/* HR addressees (one query, reused per training) */
$hrUsers = $pdo->query("
SELECT u.id, u.email, TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS name
FROM auth_users u
JOIN auth_roles r ON r.id = u.role_id
WHERE r.name IN ('Admin','Superuser','employee-hr','manager')
AND u.email IS NOT NULL AND u.email <> ''
")->fetchAll(PDO::FETCH_ASSOC);
$checkSent = $pdo->prepare("
SELECT COUNT(*) FROM training_reminder_log
WHERE training_id = ? AND addressee_email = ? AND next_due_date = ?
");
$insertLog = $pdo->prepare("
INSERT INTO training_reminder_log
(training_id, addressee_email, next_due_date, status_at_send, sent_at)
VALUES (?, ?, ?, ?, NOW())
");
foreach ($rows as $r) {
$rem = $r['reminder_days'] !== null
? (int)$r['reminder_days']
: ($r['topic_default_rem'] !== null ? (int)$r['topic_default_rem'] : 30);
$isOverdue = $r['next_due_date'] < $today;
$daysLeft = (int)((strtotime($r['next_due_date']) - strtotime($today)) / 86400);
if (!$isOverdue && $daysLeft > $rem) {
continue; // not yet in the reminder window
}
$type = $isOverdue ? 'expired' : 'update_to_be_scheduled';
$employeeFullName = trim($r['first_name'] . ' ' . $r['last_name']);
$employeeEmail = !empty($r['employee_email_direct'])
? $r['employee_email_direct']
: (!empty($r['employee_email_auth']) ? $r['employee_email_auth'] : null);
/* Collect addressees (employee + HR), deduplicated by lowercased email */
$recipients = [];
if ($employeeEmail) {
$key = strtolower(trim($employeeEmail));
$recipients[$key] = ['email' => $employeeEmail, 'name' => $employeeFullName, 'is_hr' => false];
}
foreach ($hrUsers as $hr) {
$key = strtolower(trim((string)$hr['email']));
if ($key === '' || isset($recipients[$key])) continue;
$recipients[$key] = ['email' => $hr['email'], 'name' => trim((string)$hr['name']), 'is_hr' => true];
}
if (empty($recipients)) {
continue;
}
foreach ($recipients as $email => $rec) {
if ($onlyEmail !== null && strcasecmp($rec['email'], $onlyEmail) !== 0) continue;
$checkSent->execute([$r['id'], $rec['email'], $r['next_due_date']]);
if ($checkSent->fetchColumn() > 0) {
$skipped++;
continue;
}
try {
$mail = new PHPMailer(true);
// SMTP config from .env
$mailer = $_ENV['MAIL_MAILER'] ?? 'mail';
if ($mailer === 'smtp') {
$mail->isSMTP();
$mail->Host = $_ENV['MAIL_HOST'] ?? 'localhost';
$mail->Port = (int)($_ENV['MAIL_PORT'] ?? 587);
if (!empty($_ENV['MAIL_USERNAME']) && $_ENV['MAIL_USERNAME'] !== 'null') {
$mail->SMTPAuth = true;
$mail->Username = $_ENV['MAIL_USERNAME'];
$mail->Password = $_ENV['MAIL_PASSWORD'] ?? '';
}
$enc = $_ENV['MAIL_ENCRYPTION'] ?? '';
if ($enc && $enc !== 'null') {
$mail->SMTPSecure = $enc;
}
}
$mail->CharSet = 'UTF-8';
$mail->setFrom(
$_ENV['MAIL_FROM_ADDRESS'] ?? 'noreply@zibogomma.it',
$_ENV['MAIL_FROM_NAME'] ?? 'Formazione ZIBOGOMMA'
);
$mail->addAddress($rec['email'], $rec['name'] ?: $rec['email']);
$profileUrl = $appUrl . '/userarea/employee-profile.php?id=' . (int)$r['employee_id'] . '#tab-training';
$topicText = $r['topic_name'] . ' — ' . $employeeFullName
. (!empty($r['employee_code']) ? ' (' . $r['employee_code'] . ')' : '');
if ($isOverdue) {
$mail->Subject = '⚠️ Formazione scaduta: ' . $r['topic_name'];
$mail->Body = buildHtml(
'Formazione scaduta',
$topicText,
'Completata il <strong>' . date('d/m/Y', strtotime($r['completed_date'])) . '</strong>. '
. 'Il prossimo aggiornamento era previsto per <strong>' . date('d/m/Y', strtotime($r['next_due_date'])) . '</strong>'
. ' (scaduta da <strong>' . abs($daysLeft) . ' giorni</strong>).',
'#dc3545',
$profileUrl,
$rec['is_hr']
);
} else {
$mail->Subject = '📚 Formazione in scadenza: ' . $r['topic_name'];
$daysText = $daysLeft === 0 ? 'oggi' : 'tra <strong>' . $daysLeft . ' giorni</strong>';
$mail->Body = buildHtml(
'Formazione in scadenza',
$topicText,
'Completata il <strong>' . date('d/m/Y', strtotime($r['completed_date'])) . '</strong>. '
. 'Prossimo aggiornamento previsto per <strong>' . date('d/m/Y', strtotime($r['next_due_date'])) . '</strong>'
. ' (' . $daysText . ').',
'#e8930c',
$profileUrl,
$rec['is_hr']
);
}
$mail->isHTML(true);
$mail->AltBody = strip_tags(str_replace('<br>', "\n", $mail->Body));
if ($dryRun) {
echo date('H:i:s') . " ◌ DRY {$type}{$rec['email']}{$r['topic_name']}\n";
$sent++;
continue;
}
$mail->send();
$insertLog->execute([$r['id'], $rec['email'], $r['next_due_date'], $type]);
$sent++;
echo date('H:i:s') . "{$type}{$rec['email']}{$r['topic_name']}\n";
} catch (Exception $e) {
$errors++;
echo date('H:i:s') . " ✗ Errore {$rec['email']}: {$e->getMessage()}\n";
}
}
}
/* ============================================================================
NOT-PRESENT reminders mandatory topics with no record for an employee.
Notify HR only.
De-dup by (employee_id, training_topic_id, addressee_email).
============================================================================ */
$missingStmt = $pdo->query("
SELECT e.id AS employee_id, e.first_name, e.last_name, e.employee_code,
tt.id AS topic_id, tt.name AS topic_name
FROM employees e
CROSS JOIN training_topics tt
WHERE tt.is_active = 1 AND tt.is_mandatory = 1
AND (e.status IS NULL OR e.status = 'active')
AND NOT EXISTS (
SELECT 1 FROM employee_trainings et
WHERE et.employee_id = e.id AND et.training_topic_id = tt.id
)
ORDER BY e.last_name, e.first_name, tt.name
");
$missingRows = $missingStmt->fetchAll(PDO::FETCH_ASSOC);
$checkMissingSent = $pdo->prepare("
SELECT COUNT(*) FROM training_reminder_log
WHERE employee_id = ? AND training_topic_id = ? AND addressee_email = ?
AND status_at_send = 'not_present'
");
$insertMissingLog = $pdo->prepare("
INSERT INTO training_reminder_log
(training_id, employee_id, training_topic_id, addressee_email, next_due_date, status_at_send, sent_at)
VALUES (NULL, ?, ?, ?, NULL, 'not_present', NOW())
");
foreach ($missingRows as $m) {
$employeeFullName = trim($m['first_name'] . ' ' . $m['last_name']);
foreach ($hrUsers as $hr) {
$email = trim((string)$hr['email']);
if ($email === '') continue;
if ($onlyEmail !== null && strcasecmp($email, $onlyEmail) !== 0) continue;
$checkMissingSent->execute([$m['employee_id'], $m['topic_id'], $email]);
if ($checkMissingSent->fetchColumn() > 0) {
$skipped++;
continue;
}
try {
$mail = new PHPMailer(true);
$mailer = $_ENV['MAIL_MAILER'] ?? 'mail';
if ($mailer === 'smtp') {
$mail->isSMTP();
$mail->Host = $_ENV['MAIL_HOST'] ?? 'localhost';
$mail->Port = (int)($_ENV['MAIL_PORT'] ?? 587);
if (!empty($_ENV['MAIL_USERNAME']) && $_ENV['MAIL_USERNAME'] !== 'null') {
$mail->SMTPAuth = true;
$mail->Username = $_ENV['MAIL_USERNAME'];
$mail->Password = $_ENV['MAIL_PASSWORD'] ?? '';
}
$enc = $_ENV['MAIL_ENCRYPTION'] ?? '';
if ($enc && $enc !== 'null') {
$mail->SMTPSecure = $enc;
}
}
$mail->CharSet = 'UTF-8';
$mail->setFrom(
$_ENV['MAIL_FROM_ADDRESS'] ?? 'noreply@zibogomma.it',
$_ENV['MAIL_FROM_NAME'] ?? 'Formazione ZIBOGOMMA'
);
$mail->addAddress($email, trim((string)$hr['name']) ?: $email);
$profileUrl = $appUrl . '/userarea/employee-profile.php?id=' . (int)$m['employee_id'] . '#tab-training';
$topicText = $m['topic_name'] . ' — ' . $employeeFullName
. (!empty($m['employee_code']) ? ' (' . $m['employee_code'] . ')' : '');
$mail->Subject = '🔔 Formazione obbligatoria non presente: ' . $m['topic_name'];
$mail->Body = buildHtml(
'Formazione obbligatoria non presente',
$topicText,
'Il dipendente <strong>' . htmlspecialchars($employeeFullName) . '</strong> non ha nessuna registrazione per il corso obbligatorio <strong>' . htmlspecialchars($m['topic_name']) . '</strong>. Programma la prima erogazione.',
'#6b7280',
$profileUrl,
true
);
$mail->isHTML(true);
$mail->AltBody = strip_tags(str_replace('<br>', "\n", $mail->Body));
if ($dryRun) {
echo date('H:i:s') . " ◌ DRY not_present → {$email}{$m['topic_name']} / {$employeeFullName}\n";
$sent++;
continue;
}
$mail->send();
$insertMissingLog->execute([$m['employee_id'], $m['topic_id'], $email]);
$sent++;
echo date('H:i:s') . " ✓ not_present → {$email}{$m['topic_name']} / {$employeeFullName}\n";
} catch (Exception $e) {
$errors++;
echo date('H:i:s') . " ✗ Errore {$email}: {$e->getMessage()}\n";
}
}
}
echo "\n" . date('Y-m-d H:i:s') . " — Completato. Inviate: {$sent}, Saltate: {$skipped}, Errori: {$errors}\n";
// --- HTML email template ---
function buildHtml(string $title, string $topic, string $message, string $accentColor, string $url, bool $isForHr): string
{
$greeting = $isForHr
? 'Una formazione richiede attenzione.'
: 'Una delle tue formazioni richiede attenzione.';
return '
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body style="margin:0;padding:0;background:#f4f6f9;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif">
<table width="100%" cellpadding="0" cellspacing="0" style="padding:30px 0">
<tr><td align="center">
<table width="560" cellpadding="0" cellspacing="0" style="background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.06)">
<tr><td style="background:' . $accentColor . ';padding:20px 30px">
<h1 style="margin:0;color:#fff;font-size:18px">' . htmlspecialchars($title) . '</h1>
</td></tr>
<tr><td style="padding:30px">
<p style="margin:0 0 12px;color:#444;font-size:14px">' . htmlspecialchars($greeting) . '</p>
<h2 style="margin:0 0 15px;color:#2c3e6b;font-size:16px">' . htmlspecialchars($topic) . '</h2>
<p style="margin:0 0 20px;color:#444;font-size:14px;line-height:1.6">' . $message . '</p>
<a href="' . htmlspecialchars($url) . '" style="display:inline-block;background:#5a8fd8;color:#fff;padding:10px 24px;border-radius:6px;text-decoration:none;font-weight:600;font-size:14px">Apri profilo</a>
</td></tr>
<tr><td style="padding:15px 30px;background:#f8f9fb;border-top:1px solid #eee">
<p style="margin:0;color:#999;font-size:11px">ZIBOGOMMA Formazione</p>
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>';
}
@@ -0,0 +1,61 @@
<?php
include('include/headscript.php');
header('Content-Type: application/json; charset=utf-8');
$response = [
'success' => false,
'message' => ''
];
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
throw new Exception('Metodo non consentito');
}
if (!isset($_POST['id']) || !is_numeric($_POST['id'])) {
throw new Exception('ID allegato non valido');
}
$id = (int)$_POST['id'];
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("SELECT id, file_path FROM matrice_attachments WHERE id = :id LIMIT 1");
$stmt->execute([':id' => $id]);
$attachment = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$attachment) {
throw new Exception('Allegato non trovato');
}
$filePathRelative = ltrim((string)$attachment['file_path'], '/\\');
$filePathAbsolute = __DIR__ . '/' . $filePathRelative;
$pdo->beginTransaction();
$deleteStmt = $pdo->prepare("DELETE FROM matrice_attachments WHERE id = :id");
$deleteStmt->execute([':id' => $id]);
if ($deleteStmt->rowCount() <= 0) {
throw new Exception('Impossibile eliminare il record allegato');
}
if (!empty($filePathRelative) && file_exists($filePathAbsolute) && is_file($filePathAbsolute)) {
@unlink($filePathAbsolute);
}
$pdo->commit();
$response['success'] = true;
$response['message'] = 'Allegato eliminato correttamente';
} catch (Throwable $e) {
if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) {
$pdo->rollBack();
}
$response['message'] = $e->getMessage();
}
echo json_encode($response, JSON_UNESCAPED_UNICODE);
exit;
@@ -0,0 +1,17 @@
<?php
include('include/headscript.php');
header('Content-Type: application/json');
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid id']);
exit;
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("DELETE FROM mescole_supplier_lots WHERE id = ?");
$ok = $stmt->execute([$id]);
echo json_encode(['success' => (bool)$ok]);
@@ -0,0 +1,20 @@
<?php
include('include/headscript.php');
header('Content-Type: application/json');
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid id']);
exit;
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
try {
$stmt = $pdo->prepare("DELETE FROM packaging_stock_lots WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(['success' => true]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+42
View File
@@ -0,0 +1,42 @@
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
header('Content-Type: application/json');
include('include/headscript.php');
try {
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid parameter id.']);
exit;
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Check existence
$stmt = $pdo->prepare("SELECT id FROM production_line_params WHERE id = :id");
$stmt->execute([':id' => $id]);
if (!$stmt->fetchColumn()) {
echo json_encode(['success' => false, 'message' => 'Parameter not found.']);
exit;
}
// Delete
$del = $pdo->prepare("DELETE FROM production_line_params WHERE id = :id");
$del->execute([':id' => $id]);
echo json_encode([
'success' => true,
'message' => 'Parameter deleted successfully.'
]);
} catch (Exception $e) {
echo json_encode([
'success' => false,
'message' => 'Server error: ' . $e->getMessage()
]);
}
+47
View File
@@ -0,0 +1,47 @@
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
header('Content-Type: application/json');
require_once 'include/headscript.php';
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$id = (int)($_POST['id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$tool_type = trim($_POST['tool_type'] ?? '');
$description = trim($_POST['description'] ?? '');
$is_active = isset($_POST['is_active']) ? (int)$_POST['is_active'] : 1;
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid ID.']);
exit;
}
if ($name === '') {
echo json_encode(['success' => false, 'message' => 'Name is required.']);
exit;
}
$sql = "UPDATE production_tools
SET name = :name,
tool_type = :tool_type,
description = :description,
is_active = :is_active
WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'name' => $name,
'tool_type' => $tool_type ?: null,
'description' => $description ?: null,
'is_active' => $is_active,
'id' => $id
]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+798
View File
@@ -0,0 +1,798 @@
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
/* ==========================================
AJAX HANDLERS
========================================== */
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['ajax'] == '1') {
header('Content-Type: application/json');
$action = $_POST['action'] ?? '';
try {
if ($action === 'add') {
$name = trim($_POST['name'] ?? '');
$code = trim($_POST['code'] ?? '');
$description = trim($_POST['description'] ?? '');
$color = trim($_POST['color'] ?? '#6c757d');
$sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 999;
$is_active = isset($_POST['is_active']) ? (int)$_POST['is_active'] : 1;
if ($name === '') {
echo json_encode([
'success' => false,
'message' => 'Department name is required.'
]);
exit;
}
if ($code === '') {
$code = strtoupper(str_replace(' ', '_', $name));
$code = preg_replace('/[^A-Z0-9_]/', '', $code);
} else {
$code = strtoupper($code);
$code = preg_replace('/[^A-Z0-9_]/', '', $code);
}
if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $color)) {
$color = '#6c757d';
}
$is_active = $is_active === 1 ? 1 : 0;
$check = $pdo->prepare("
SELECT COUNT(*)
FROM departments
WHERE name = :name OR code = :code
");
$check->execute([
'name' => $name,
'code' => $code
]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode([
'success' => false,
'message' => 'A department with the same name or code already exists.'
]);
exit;
}
$sql = "INSERT INTO departments
(name, code, description, color, sort_order, is_active, created_at, updated_at)
VALUES
(:name, :code, :description, :color, :sort_order, :is_active, NOW(), NOW())";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'name' => $name,
'code' => $code !== '' ? $code : null,
'description' => $description !== '' ? $description : null,
'color' => $color,
'sort_order' => $sort_order,
'is_active' => $is_active
]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'edit') {
$id = (int)($_POST['id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$code = trim($_POST['code'] ?? '');
$description = trim($_POST['description'] ?? '');
$color = trim($_POST['color'] ?? '#6c757d');
$sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 999;
$is_active = isset($_POST['is_active']) ? (int)$_POST['is_active'] : 1;
if ($id <= 0) {
echo json_encode([
'success' => false,
'message' => 'Invalid department ID.'
]);
exit;
}
if ($name === '') {
echo json_encode([
'success' => false,
'message' => 'Department name is required.'
]);
exit;
}
if ($code === '') {
$code = strtoupper(str_replace(' ', '_', $name));
$code = preg_replace('/[^A-Z0-9_]/', '', $code);
} else {
$code = strtoupper($code);
$code = preg_replace('/[^A-Z0-9_]/', '', $code);
}
if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $color)) {
$color = '#6c757d';
}
$is_active = $is_active === 1 ? 1 : 0;
$check = $pdo->prepare("
SELECT COUNT(*)
FROM departments
WHERE (name = :name OR code = :code)
AND id <> :id
");
$check->execute([
'name' => $name,
'code' => $code,
'id' => $id
]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode([
'success' => false,
'message' => 'Another department with the same name or code already exists.'
]);
exit;
}
$sql = "UPDATE departments
SET name = :name,
code = :code,
description = :description,
color = :color,
sort_order = :sort_order,
is_active = :is_active,
updated_at = NOW()
WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'name' => $name,
'code' => $code !== '' ? $code : null,
'description' => $description !== '' ? $description : null,
'color' => $color,
'sort_order' => $sort_order,
'is_active' => $is_active,
'id' => $id
]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'delete') {
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode([
'success' => false,
'message' => 'Invalid department ID.'
]);
exit;
}
/*
* Future-proof check:
* If later you add employees.department_id, this prevents deleting
* a department already used by employees.
*/
$columnCheck = $pdo->prepare("
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'employees'
AND COLUMN_NAME = 'department_id'
");
$columnCheck->execute();
$hasDepartmentId = (int)$columnCheck->fetchColumn() > 0;
if ($hasDepartmentId) {
$usageCheck = $pdo->prepare("
SELECT COUNT(*)
FROM employees
WHERE department_id = :id
");
$usageCheck->execute(['id' => $id]);
if ((int)$usageCheck->fetchColumn() > 0) {
echo json_encode([
'success' => false,
'message' => 'This department is linked to one or more employees and cannot be deleted.'
]);
exit;
}
}
$stmt = $pdo->prepare("DELETE FROM departments WHERE id = :id");
$stmt->execute(['id' => $id]);
echo json_encode(['success' => true]);
exit;
}
echo json_encode([
'success' => false,
'message' => 'Unknown action.'
]);
exit;
} catch (Exception $e) {
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
exit;
}
}
/* ==========================================
PAGE DATA
========================================== */
$sql = "
SELECT *
FROM departments
ORDER BY sort_order ASC, name ASC
";
$stmtDepartments = $pdo->query($sql);
$departments = $stmtDepartments->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'); ?>
<title>Gestione Departments - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<!-- jQuery and Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<style>
body {
font-size: 1.05rem;
background: #f8fafc;
}
.card {
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.back-dashboard {
background-color: #cfe3ff !important;
color: #1f2d3d !important;
border: 1px solid #bcd4f4 !important;
border-radius: 10px;
font-weight: 600;
font-size: 1rem;
padding: 10px 18px;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease-in-out;
}
.back-dashboard:hover {
background-color: #b9d3ff !important;
transform: translateY(-2px);
}
.btn-add {
background-color: #0d6efd;
color: #fff;
border-radius: 8px;
padding: 10px 20px;
font-weight: 500;
transition: all 0.2s ease-in-out;
}
.btn-add:hover {
background-color: #0b5ed7;
transform: scale(1.02);
}
.table thead {
background-color: #cfe3ff;
color: #1f2d3d;
}
.modal-content {
border-radius: 16px;
}
#tabellaDepartments thead th {
text-align: center;
vertical-align: middle;
}
.badge-status {
padding: 0.25rem 0.6rem;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 600;
}
.badge-status.active {
background-color: #d1fae5;
color: #065f46;
}
.badge-status.inactive {
background-color: #e5e7eb;
color: #374151;
}
.department-color-dot {
display: inline-block;
width: 18px;
height: 18px;
border-radius: 50%;
border: 1px solid rgba(0, 0, 0, 0.2);
vertical-align: middle;
margin-right: 6px;
}
.department-code {
font-family: Consolas, Monaco, monospace;
font-size: 0.9rem;
background: #f1f5f9;
padding: 4px 8px;
border-radius: 8px;
color: #334155;
}
.description-cell {
max-width: 320px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
}
</style>
</head>
<body>
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card p-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Gestione Departments</h5>
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
↩️ Torna alla Dashboard
</button>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-semibold mb-0">Elenco Reparti / Departments</h6>
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addDepartmentModal">
Aggiungi Department
</button>
</div>
<div class="table-responsive">
<table id="tabellaDepartments" class="table table-striped align-middle text-center" style="width:100%;">
<thead>
<tr>
<th>ID</th>
<th>Color</th>
<th>Name</th>
<th>Code</th>
<th>Description</th>
<th>Order</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php if (!empty($departments)): ?>
<?php foreach ($departments as $row): ?>
<?php
$id = (int)$row['id'];
$name = $row['name'] ?? '';
$code = $row['code'] ?? '';
$description = $row['description'] ?? '';
$color = $row['color'] ?? '#6c757d';
$sortOrder = (int)($row['sort_order'] ?? 999);
$isActive = (int)($row['is_active'] ?? 1);
$statusClass = $isActive === 1 ? 'active' : 'inactive';
$statusLabel = $isActive === 1 ? 'Active' : 'Inactive';
$createdAt = !empty($row['created_at'])
? date('d/m/Y H:i', strtotime($row['created_at']))
: '-';
?>
<tr>
<td><?= $id ?></td>
<td>
<span class="department-color-dot" style="background-color: <?= htmlspecialchars($color, ENT_QUOTES) ?>;"></span>
<?= htmlspecialchars($color) ?>
</td>
<td class="fw-semibold">
<?= htmlspecialchars($name) ?>
</td>
<td>
<?php if ($code !== ''): ?>
<span class="department-code"><?= htmlspecialchars($code) ?></span>
<?php else: ?>
-
<?php endif; ?>
</td>
<td class="description-cell" title="<?= htmlspecialchars($description, ENT_QUOTES) ?>">
<?= $description !== '' ? htmlspecialchars($description) : '-' ?>
</td>
<td><?= $sortOrder ?></td>
<td>
<span class="badge-status <?= $statusClass ?>">
<?= htmlspecialchars($statusLabel) ?>
</span>
</td>
<td><?= $createdAt ?></td>
<td>
<button
class="btn btn-sm btn-outline-secondary edit-department"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
data-code="<?= htmlspecialchars($code, ENT_QUOTES) ?>"
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
data-color="<?= htmlspecialchars($color, ENT_QUOTES) ?>"
data-sort_order="<?= $sortOrder ?>"
data-is_active="<?= $isActive ?>">
✏️ Modifica
</button>
<button
class="btn btn-sm btn-outline-danger delete-department"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>">
🗑️ Cancella
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<!-- ADD DEPARTMENT MODAL -->
<div class="modal fade" id="addDepartmentModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">Aggiungi Department</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addDepartmentForm">
<div class="mb-3">
<label class="form-label fw-semibold">Name</label>
<input type="text" class="form-control" id="addName" name="name" placeholder="e.g. Produzione" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Code</label>
<input type="text" class="form-control" id="addCode" name="code" placeholder="Optional, e.g. PRODUZIONE">
<small class="text-muted">If empty, it will be generated automatically from the name.</small>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Description</label>
<textarea class="form-control" id="addDescription" name="description" rows="3" placeholder="Optional notes"></textarea>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Color</label>
<input type="color" class="form-control form-control-color" id="addColor" name="color" value="#6c757d">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Sort Order</label>
<input type="number" class="form-control" id="addSortOrder" name="sort_order" value="999" min="0">
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Status</label>
<select class="form-select" id="addIsActive" name="is_active">
<option value="1" selected>Active</option>
<option value="0">Inactive</option>
</select>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add">💾 Save</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- EDIT DEPARTMENT MODAL -->
<div class="modal fade" id="editDepartmentModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">Modifica Department</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editDepartmentForm">
<input type="hidden" id="editDepartmentId">
<div class="mb-3">
<label class="form-label fw-semibold">Name</label>
<input type="text" class="form-control" id="editName" name="name" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Code</label>
<input type="text" class="form-control" id="editCode" name="code">
<small class="text-muted">If empty, it will be generated automatically from the name.</small>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Description</label>
<textarea class="form-control" id="editDescription" name="description" rows="3"></textarea>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Color</label>
<input type="color" class="form-control form-control-color" id="editColor" name="color" value="#6c757d">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Sort Order</label>
<input type="number" class="form-control" id="editSortOrder" name="sort_order" value="999" min="0">
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Status</label>
<select class="form-select" id="editIsActive" name="is_active">
<option value="1">Active</option>
<option value="0">Inactive</option>
</select>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add">💾 Save Changes</button>
</div>
</form>
</div>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
$(document).ready(function() {
$('#tabellaDepartments').DataTable({
order: [
[5, 'asc'],
[2, 'asc']
],
pageLength: 25,
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json',
emptyTable: 'Nessun department presente'
}
});
/* -------- ADD DEPARTMENT -------- */
$("#addDepartmentForm").on("submit", function(e) {
e.preventDefault();
const payload = new URLSearchParams();
payload.append('ajax', '1');
payload.append('action', 'add');
payload.append('name', $("#addName").val().trim());
payload.append('code', $("#addCode").val().trim());
payload.append('description', $("#addDescription").val().trim());
payload.append('color', $("#addColor").val());
payload.append('sort_order', $("#addSortOrder").val());
payload.append('is_active', $("#addIsActive").val());
fetch("", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: payload.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: "success",
title: "Saved!",
confirmButtonColor: "#3085d6"
}).then(() => location.reload());
} else {
Swal.fire({
icon: "error",
title: "Error",
text: data.message || "Unable to save department."
});
}
})
.catch(err => {
Swal.fire({
icon: "error",
title: "Error",
text: "Communication error."
});
console.error(err);
});
});
/* -------- OPEN EDIT MODAL -------- */
$(document).on("click", ".edit-department", function() {
const btn = $(this);
$("#editDepartmentId").val(btn.data("id"));
$("#editName").val(btn.data("name"));
$("#editCode").val(btn.data("code"));
$("#editDescription").val(btn.data("description"));
$("#editColor").val(btn.data("color") || '#6c757d');
$("#editSortOrder").val(btn.data("sort_order"));
$("#editIsActive").val(String(btn.data("is_active")));
$("#editDepartmentModal").modal("show");
});
/* -------- SAVE EDIT -------- */
$("#editDepartmentForm").on("submit", function(e) {
e.preventDefault();
const payload = new URLSearchParams();
payload.append('ajax', '1');
payload.append('action', 'edit');
payload.append('id', $("#editDepartmentId").val());
payload.append('name', $("#editName").val().trim());
payload.append('code', $("#editCode").val().trim());
payload.append('description', $("#editDescription").val().trim());
payload.append('color', $("#editColor").val());
payload.append('sort_order', $("#editSortOrder").val());
payload.append('is_active', $("#editIsActive").val());
fetch("", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: payload.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: "success",
title: "Updated!",
confirmButtonColor: "#3085d6"
}).then(() => location.reload());
} else {
Swal.fire({
icon: "error",
title: "Error",
text: data.message || "Unable to update department."
});
}
})
.catch(err => {
Swal.fire({
icon: "error",
title: "Error",
text: "Communication error."
});
console.error(err);
});
});
/* -------- DELETE DEPARTMENT -------- */
$(document).on("click", ".delete-department", function() {
const id = $(this).data("id");
const name = $(this).data("name");
Swal.fire({
title: "Confermi la cancellazione?",
text: name ? ("Department: " + name) : "This department will be deleted.",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#d33",
cancelButtonColor: "#6c757d",
confirmButtonText: "Sì, cancella",
cancelButtonText: "Annulla"
}).then((result) => {
if (!result.isConfirmed) return;
const payload = new URLSearchParams();
payload.append('ajax', '1');
payload.append('action', 'delete');
payload.append('id', id);
fetch("", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: payload.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: "success",
title: "Deleted!",
confirmButtonColor: "#3085d6"
}).then(() => location.reload());
} else {
Swal.fire({
icon: "error",
title: "Error",
text: data.message || "Unable to delete department."
});
}
})
.catch(err => {
Swal.fire({
icon: "error",
title: "Error",
text: "Communication error."
});
console.error(err);
});
});
});
});
</script>
</body>
</html>
+57
View File
@@ -0,0 +1,57 @@
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
header('Content-Type: application/json');
require_once 'include/headscript.php';
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$id = (int)($_POST['id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$registrationNumber = trim($_POST['registration_number'] ?? '');
$serialNumber = trim($_POST['serial_number'] ?? '');
$toolType = trim($_POST['tool_type'] ?? '');
$manufacturer = trim($_POST['manufacturer'] ?? '');
$description = trim($_POST['description'] ?? '');
$isActive = isset($_POST['is_active']) ? (int)$_POST['is_active'] : 1;
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid ID.']);
exit;
}
if ($name === '') {
echo json_encode(['success' => false, 'message' => 'Name is required.']);
exit;
}
$sql = "UPDATE production_tools
SET name = :name,
registration_number = :registration_number,
serial_number = :serial_number,
tool_type = :tool_type,
manufacturer = :manufacturer,
description = :description,
is_active = :is_active
WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'name' => $name,
'registration_number' => $registrationNumber ?: null,
'serial_number' => $serialNumber ?: null,
'tool_type' => $toolType ?: null,
'manufacturer' => $manufacturer ?: null,
'description' => $description ?: null,
'is_active' => $isActive,
'id' => $id
]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,65 @@
<?php
include('include/headscript.php');
header('Content-Type: application/json; charset=utf-8');
$response = [
'success' => false,
'attachments' => [],
'message' => ''
];
try {
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
throw new Exception('ID matrice non valido');
}
$idmatrice = (int)$_GET['id'];
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$sql = "SELECT
id,
matrice_id,
file_name,
file_path,
file_type,
description,
sort_order,
created_at,
updated_at
FROM matrice_attachments
WHERE matrice_id = :matrice_id
ORDER BY sort_order ASC, id DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute([':matrice_id' => $idmatrice]);
$attachments = [];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$relativePath = ltrim((string)$row['file_path'], '/\\');
$attachments[] = [
'id' => (int)$row['id'],
'matrice_id' => (int)$row['matrice_id'],
'file_name' => $row['file_name'],
'file_path' => $relativePath,
'file_url' => $relativePath,
'file_type' => $row['file_type'],
'description' => $row['description'] ?? '',
'sort_order' => (int)($row['sort_order'] ?? 0),
'created_at' => !empty($row['created_at']) ? date('d/m/Y H:i', strtotime($row['created_at'])) : '',
'updated_at' => !empty($row['updated_at']) ? date('d/m/Y H:i', strtotime($row['updated_at'])) : ''
];
}
$response['success'] = true;
$response['attachments'] = $attachments;
} catch (Throwable $e) {
$response['message'] = $e->getMessage();
}
echo json_encode($response, JSON_UNESCAPED_UNICODE);
exit;
+36
View File
@@ -0,0 +1,36 @@
<?php
include('include/headscript.php');
header('Content-Type: application/json; charset=utf-8');
try {
$idmatrice = (int)($_GET['id'] ?? 0);
if ($idmatrice <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid id']);
exit;
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// tutte le linee (attive + inattive se vuoi: qui prendo tutte)
$stmt = $pdo->query("
SELECT id, line_number, name
FROM production_lines
ORDER BY line_number ASC
");
$lines = $stmt->fetchAll(PDO::FETCH_ASSOC);
// linee già associate
$stmt = $pdo->prepare("SELECT idlinea FROM matrici_lines WHERE idmatrice = ?");
$stmt->execute([$idmatrice]);
$selected_ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
echo json_encode([
'success' => true,
'lines' => $lines,
'selected_ids' => $selected_ids
]);
} catch (Throwable $e) {
echo json_encode(['success' => false, 'message' => 'Server error']);
}
+35
View File
@@ -0,0 +1,35 @@
<?php
include('include/headscript.php');
header('Content-Type: application/json; charset=utf-8');
try {
$idmatrice = (int)($_GET['id'] ?? 0);
if ($idmatrice <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid id']);
exit;
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// tutte le mescole
$stmt = $pdo->query("
SELECT id, nome, nomeuscita
FROM mescole
ORDER BY nome ASC
");
$mescole = $stmt->fetchAll(PDO::FETCH_ASSOC);
// mescole già associate
$stmt = $pdo->prepare("SELECT idmescola FROM matrici_mescole WHERE idmatrice = ?");
$stmt->execute([$idmatrice]);
$selected_ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
echo json_encode([
'success' => true,
'mescole' => $mescole,
'selected_ids' => $selected_ids
]);
} catch (Throwable $e) {
echo json_encode(['success' => false, 'message' => 'Server error']);
}
@@ -0,0 +1,31 @@
<?php
include('include/headscript.php');
header('Content-Type: application/json');
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid id']);
exit;
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$sql = "SELECT
msl.id,
msl.idsupplier AS idsupplier,
s.supplier_name,
msl.supplier_mix_name,
msl.lot_code,
msl.expiry_date,
msl.qty
FROM mescole_supplier_lots msl
INNER JOIN suppliers s ON s.idsupplier = msl.idsupplier
WHERE msl.idmescola = ?
ORDER BY msl.id DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute([$id]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'rows' => $rows]);
@@ -0,0 +1,30 @@
<?php
include('include/headscript.php');
header('Content-Type: application/json');
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid id']);
exit;
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$sql = "SELECT
psl.id,
psl.idsupplier,
s.supplier_name,
psl.lot_code,
psl.expiry_date,
psl.qty
FROM packaging_stock_lots psl
INNER JOIN suppliers s ON s.idsupplier = psl.idsupplier
WHERE psl.idpackaging_item = ?
ORDER BY psl.id DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute([$id]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'rows' => $rows]);
+42
View File
@@ -0,0 +1,42 @@
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
header('Content-Type: application/json');
include('include/headscript.php');
try {
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid parameter id.']);
exit;
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("
SELECT id, line_id, position, short_label, label, icon
FROM production_line_params
WHERE id = :id
");
$stmt->execute([':id' => $id]);
$param = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$param) {
echo json_encode(['success' => false, 'message' => 'Parameter not found.']);
exit;
}
echo json_encode([
'success' => true,
'param' => $param
]);
} catch (Exception $e) {
echo json_encode([
'success' => false,
'message' => 'Server error: ' . $e->getMessage()
]);
}
+11
View File
@@ -0,0 +1,11 @@
<?php
include('include/headscript.php');
header('Content-Type: application/json');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->query("SELECT idsupplier, supplier_name FROM suppliers ORDER BY supplier_name ASC");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'rows' => $rows]);
+11 -9
View File
@@ -7,7 +7,7 @@ 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 | E_STRICT);
// 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(__DIR__ . '/../../../extra/auth.php');
//require_once __DIR__ . '/extra/auth.php'; //require_once __DIR__ . '/extra/auth.php';
// Here we just check if user is not // Here we just check if user is not
@@ -15,8 +15,13 @@ include('../../extra/auth.php');
// the user to vanguard login page. // the user to vanguard login page.
if (! Auth::check()) { if (! Auth::check()) {
// Cut everything at /userarea/ and append /login.
redirectTo('../../public/login'); $scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
$basePath = substr($scriptName, 0, strpos($scriptName, '/userarea/'));
if ($basePath === false || $basePath === '') {
$basePath = '';
}
redirectTo($basePath . '/login');
} }
$user = Auth::user(); $user = Auth::user();
@@ -35,8 +40,7 @@ $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 +53,11 @@ $_SESSION["emailuser"] = $emailuser;
$_SESSION["photouser"] = $avatar; $_SESSION["photouser"] = $avatar;
$photouser = $_SESSION["photouser"]; $photouser = $_SESSION["photouser"];
$photousername = basename($avatar); $photousername = basename($avatar);
?>
//include files
<?php //include files
require_once(__DIR__ . '/../../languages/en/general.php'); require_once(__DIR__ . '/../../languages/en/general.php');
//include("generalsettings.php"); //include("generalsettings.php");
require_once __DIR__ . '/permissions_helper.php';
?> ?>
+332 -45
View File
@@ -6,101 +6,388 @@
<div> <div>
<h4 class="logo-text"><?= htmlspecialchars('ZIBOGOMMA', ENT_QUOTES, 'UTF-8'); ?></h4> <h4 class="logo-text"><?= htmlspecialchars('ZIBOGOMMA', 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 if (userCan('production.dashboard.view')) : ?>
<li> <li>
<a href="production_dashboard.php"> <a href="production_dashboard.php">
<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>
</li> </li>
<?php endif; ?>
<?php
$canSeeProgramming =
userCan('production.programming.view')
|| userCan('templates.dashboard.view')
|| userCan('templates.create.view');
?>
<?php if ($canSeeProgramming) : ?>
<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">Programmazione</div> <div class="menu-title">Programmazione</div>
</a> </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 (userCan('templates.dashboard.view')) : ?>
<li>
<a href="templates_dashboard.php">
<i class='bx bx-radio-circle'></i>
<?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?>
</a>
</li>
<?php endif; ?>
<?php if (userCan('templates.create.view')) : ?>
<li>
<a href="insert_template_xls.php">
<i class='bx bx-radio-circle'></i>
<?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?>
</a>
</li>
<?php endif; ?>
<?php if (userCan('production.programming.view')) : ?>
<li>
<a href="produzione_programmazione_drag.php">
<i class='bx bx-radio-circle'></i>
Programmazione Produzione
</a>
</li>
<?php endif; ?>
</ul> </ul>
</li> </li>
<?php endif; ?>
<?php
$canSeeFunctions =
userCan('masterdata.mescole.view')
|| userCan('masterdata.matrici.view')
|| userCan('masterdata.linee.view')
|| userCan('masterdata.packaging.view')
|| userCan('masterdata.suppliers.view')
|| userCan('masterdata.lookup.view')
|| userCan('masterdata.worksheets.view');
?>
<?php if ($canSeeFunctions) : ?>
<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">Funzioni</div> <div class="menu-title">Funzioni</div>
</a> </a>
<ul> <ul>
<?php if (userCan('masterdata.mescole.view')) : ?>
<li> <li>
<a href="mescole.php"><i class='bx bx-radio-circle'></i>Mescole</a> <a href="mescole.php">
</li> <i class='bx bx-radio-circle'></i>Mescole
<li> </a>
<a href="matrici.php"><i class='bx bx-radio-circle'></i>Matrici</a>
</li>
<li>
<a href="linee.php"><i class='bx bx-radio-circle'></i>Linee di produzione</a>
</li> </li>
<?php endif; ?>
<?php if (userCan('masterdata.matrici.view')) : ?>
<li>
<a href="matrici.php">
<i class='bx bx-radio-circle'></i>Matrici
</a>
</li>
<?php endif; ?>
<?php if (userCan('masterdata.linee.view')) : ?>
<li>
<a href="linee.php">
<i class='bx bx-radio-circle'></i>Linee di produzione
</a>
</li>
<?php endif; ?>
<?php if (userCan('masterdata.packaging.view')) : ?>
<li>
<a href="packaging_items.php">
<i class='bx bx-radio-circle'></i>Imballaggi
</a>
</li>
<?php endif; ?>
<?php if (userCan('masterdata.suppliers.view')) : ?>
<li>
<a href="suppliers.php">
<i class='bx bx-radio-circle'></i>Suppliers
</a>
</li>
<?php endif; ?>
<?php if (userCan('masterdata.lookup.view')) : ?>
<li>
<a href="lookup_values.php">
<i class='bx bx-radio-circle'></i>Setup
</a>
</li>
<?php endif; ?>
<?php if (userCan('masterdata.worksheets.view')) : ?>
<li>
<a href="worksheets.php">
<i class='bx bx-radio-circle'></i>Fogli di lavoro
</a>
</li>
<?php endif; ?>
</ul> </ul>
</li> </li>
<?php endif; ?>
<?php
$canSeeProduction =
userCan('production.line_view.view')
|| userCan('production.stats.view')
|| userCan('production.manager.view')
|| userCan('production.manager_stats.view')
|| userCan('warehouse.dashboard.view');
?>
<?php if ($canSeeProduction) : ?>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon">
<i class="bx bx-line-chart"></i>
</div>
<div class="menu-title">Produzione</div>
</a>
<ul>
<?php if (userCan('production.line_view.view')) : ?>
<li>
<a href="production_line_view2.php">
<i class='bx bx-radio-circle'></i>Line View
</a>
</li>
<?php endif; ?>
<?php if (userCan('production.stats.view')) : ?>
<li>
<a href="production_stats.php">
<i class='bx bx-radio-circle'></i>Statistiche
</a>
</li>
<?php endif; ?>
<?php if (userCan('production.manager.view')) : ?>
<li>
<a href="manager_produzione.php">
<i class='bx bx-radio-circle'></i>Manager
</a>
</li>
<?php endif; ?>
<?php if (userCan('production.manager_stats.view')) : ?>
<li>
<a href="manager_stats.php">
<i class='bx bx-radio-circle'></i>Manager Stats
</a>
</li>
<?php endif; ?>
<?php if (userCan('warehouse.dashboard.view')) : ?>
<li>
<a href="warehouse_dashboard.php">
<i class='bx bx-radio-circle'></i>Magazzino
</a>
</li>
<?php endif; ?>
</ul>
</li>
<?php endif; ?>
<?php
$canSeeServices =
userCan('services.status.view')
|| userCan('services.pause_reasons.view')
|| userCan('services.tools.view');
?>
<?php if ($canSeeServices) : ?>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon">
<i class="bx bx-wrench"></i>
</div>
<div class="menu-title">Servizi</div>
</a>
<ul>
<?php if (userCan('services.status.view')) : ?>
<li>
<a href="production_status.php">
<i class='bx bx-radio-circle'></i>Status
</a>
</li>
<?php endif; ?>
<?php if (userCan('services.pause_reasons.view')) : ?>
<li>
<a href="production_pause_reasons.php">
<i class='bx bx-radio-circle'></i>Cause di Pausa
</a>
</li>
<?php endif; ?>
<?php if (userCan('services.tools.view')) : ?>
<li>
<a href="production_tools.php">
<i class='bx bx-radio-circle'></i>Attrezzature
</a>
</li>
<?php endif; ?>
</ul>
</li>
<?php endif; ?>
<?php
$canSeeHr =
userCan('hr.employees.view')
|| userCan('hr.departments.view')
|| userCan('hr.job_roles.view')
|| userCan('hr.training_topics.view')
|| userCan('hr.trainings.view')
|| userCan('hr.skills.view');
?>
<?php if ($canSeeHr) : ?>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon">
<i class="bx bx-group"></i>
</div>
<div class="menu-title">Personale</div>
</a>
<ul>
<?php if (userCan('hr.employees.view')) : ?>
<li>
<a href="employees.php">
<i class='bx bx-radio-circle'></i>Dipendenti
</a>
</li>
<li>
<a href="job-roles.php">
<i class='bx bx-radio-circle'></i>Mansioni
</a>
</li>
<li>
<a href="ppe-items.php">
<i class='bx bx-radio-circle'></i>DPI
</a>
</li>
<?php endif; ?>
<?php if (userCan('hr.departments.view')) : ?>
<li>
<a href="departments.php">
<i class='bx bx-radio-circle'></i>Reparti
</a>
</li>
<?php endif; ?>
<?php if (userCan('hr.trainings.view')) : ?>
<li>
<a href="trainings.php">
<i class='bx bx-radio-circle'></i>Gestione Formazione
</a>
</li>
<?php endif; ?>
<?php if (userCan('hr.skills.view')) : ?>
<li>
<a href="skills.php">
<i class='bx bx-radio-circle'></i>Skills
</a>
</li>
<?php endif; ?>
</ul>
</li>
<?php endif; ?>
<?php if (userCan('deadlines.view')) : ?>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon">
<i class="bx bx-calendar-check"></i>
</div>
<div class="menu-title">Scadenzario</div>
</a>
<ul>
<li>
<a href="scadenzario/index.php">
<i class='bx bx-radio-circle'></i>Lista Scadenze
</a>
</li>
<li>
<a href="scadenzario/calendar.php">
<i class='bx bx-radio-circle'></i>Calendario
</a>
</li>
<li>
<a href="scadenzario/functions/index.php">
<i class='bx bx-radio-circle'></i>Funzioni Aziendali
</a>
</li>
</ul>
</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; ?>
<!-- admin, superuser menù --> <?php if (userCan('users.manage')) : ?>
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('Superuser'))) : ?>
<?php
endif; ?>
<!-- admin menù -->
<?php if (Auth::user()->hasRole('Admin')) : ?>
<li class="menu-label">Admin Menù</li> <li 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-user-circle"></i>
</div> </div>
<div class="menu-title">User Management</div> <div class="menu-title">User Management</div>
</a> </a>
</li> </li>
<!-- <li> <?php endif; ?>
<a href="template/index.html" target="_blank">
<div class="parent-icon"><i class="bx bx-support"></i>
</div>
<div class="menu-title">Template</div>
</a>
</li>
<li>
<a href="https://codervent.com/rocker/documentation/index.html" target="_blank">
<div class="parent-icon"><i class="bx bx-folder"></i>
</div>
<div class="menu-title">Documentation</div>
</a>
</li> -->
<?php
endif; ?>
</ul> </ul>
<!--end navigation--> <!--end navigation-->
</div> </div>
@@ -0,0 +1,62 @@
<?php
if (!function_exists('userCan')) {
/**
* Check if current user has a Vanguard permission.
* Uses Vanguard native method if available, otherwise falls back to DB check.
*/
function userCan($permissionName)
{
global $kindofrole;
$user = Auth::user();
if (!$user) {
return false;
}
// Vanguard / Laravel-style methods, depending on installed version/customization.
if (method_exists($user, 'hasPermission')) {
return $user->hasPermission($permissionName);
}
if (method_exists($user, 'hasPermissionTo')) {
return $user->hasPermissionTo($permissionName);
}
if (method_exists($user, 'can')) {
return $user->can($permissionName);
}
// Fallback: direct DB check using existing Vanguard tables.
static $permissions = null;
if ($permissions === null) {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->prepare("
SELECT p.name
FROM auth_permissions p
INNER JOIN auth_permission_role pr ON pr.permission_id = p.id
WHERE pr.role_id = ?
");
$stmt->execute([(int)$kindofrole]);
$permissions = $stmt->fetchAll(PDO::FETCH_COLUMN);
}
return in_array($permissionName, $permissions, true);
}
}
if (!function_exists('visibleButtons')) {
/**
* Filter visible buttons.
*/
function visibleButtons(array $buttons)
{
return array_values(array_filter($buttons, function ($button) {
return empty($button['permission']) || userCan($button['permission']);
}));
}
}
+25 -13
View File
@@ -1,3 +1,11 @@
<?php
// Build an absolute URL to employee-profile.php so it works from any depth
// (e.g. /userarea/index.php, /userarea/scadenzario/index.php).
$__scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
$__pos = strpos($__scriptName, '/userarea/');
$__base = $__pos !== false ? substr($__scriptName, 0, $__pos) : '';
$__myProfileHref = $__base . '/userarea/employee-profile.php';
?>
<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"> <nav class="navbar navbar-expand gap-3">
@@ -5,17 +13,17 @@
</div> </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="javascript:;" class="btn d-flex align-items-center"><i class='bx bx-search'></i>Search</a> -->
</div> </div>
<div class="top-menu ms-auto"> <div class="top-menu ms-auto">
<ul class="navbar-nav align-items-center gap-1"> <ul class="navbar-nav align-items-center gap-1">
<li class="nav-item mobile-search-icon d-flex d-lg-none" data-bs-toggle="modal" data-bs-target="#SearchModal"> <li class="nav-item mobile-search-icon d-flex d-lg-none" data-bs-toggle="modal" data-bs-target="#SearchModal">
<a class="nav-link" href="avascript:;"><i class='bx bx-search'></i> <a class="nav-link" href="javascript:;"><i class='bx bx-search'></i>
</a> </a>
</li> </li>
<li class="nav-item dropdown dropdown-laungauge d-none d-sm-flex"> <li class="nav-item dropdown dropdown-laungauge d-none d-sm-flex">
<a class="nav-link dropdown-toggle dropdown-toggle-nocaret" href="avascript:;" data-bs-toggle="dropdown"><img src="assets/images/county/uk.svg" width="22" alt=""> <a class="nav-link dropdown-toggle dropdown-toggle-nocaret" href="javascript:;" data-bs-toggle="dropdown"><img src="assets/images/county/uk.svg" width="22" alt="">
</a> </a>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item d-flex align-items-center py-2" href="javascript:;"><img src="assets/images/county/uk.svg" width="20" alt=""><span class="ms-2">English</span></a> <li><a class="dropdown-item d-flex align-items-center py-2" href="javascript:;"><img src="assets/images/county/uk.svg" width="20" alt=""><span class="ms-2">English</span></a>
@@ -60,13 +68,6 @@
</div> </div>
</div> </div>
</a> </a>
</div> </div>
<a href="javascript:;"> <a href="javascript:;">
<div class="text-center msg-footer"> <div class="text-center msg-footer">
@@ -92,16 +93,27 @@
</div> </div>
</a> </a>
<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="<?= htmlspecialchars($__myProfileHref) ?>"
onclick="event.preventDefault(); window.location.assign(this.href);">
<i class="bx bx-id-card fs-5"></i><span>Il Mio Profilo</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="user_settings.php">
<i class="bx bx-user fs-5"></i><span>Utente</span>
</a>
</li> </li>
<li> <li>
<div class="dropdown-divider mb-0"></div> <div class="dropdown-divider mb-0"></div>
</li> </li>
<li><a class="dropdown-item d-flex align-items-center" href="../logout"><i class="bx bx-log-out-circle"></i><span>Logout</span></a> <li>
<a class="dropdown-item d-flex align-items-center" href="../logout">
<i class="bx bx-log-out-circle"></i><span>Logout</span>
</a>
</li> </li>
</ul> </ul>
</div> </div>
</nav> </nav>
</div> </div>
+101
View File
@@ -0,0 +1,101 @@
<?php
/**
* Training reminders widget for the production dashboard.
* Visible to HR / manager / Admin / User / Superuser.
*
* Expects $pdo to be set (DBHandlerSelect connection).
*/
if (!isset($pdo)) {
$pdo = DBHandlerSelect::getInstance()->getConnection();
}
$__trWidgetHr = isset($user)
&& ( $user->hasRole('Admin')
|| $user->hasRole('Superuser')
|| $user->hasRole('employee-hr')
|| $user->hasRole('manager'));
if (!$__trWidgetHr) {
return;
}
/* Only the most recent record per (employee, topic) — older history rows ignored. */
$__trRows = $pdo->query("
SELECT et.id,
et.next_due_date,
et.reminder_days,
tt.default_reminder_days
FROM employee_trainings et
JOIN training_topics tt ON tt.id = et.training_topic_id
WHERE et.next_due_date IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM employee_trainings et2
WHERE et2.employee_id = et.employee_id
AND et2.training_topic_id = et.training_topic_id
AND (et2.completed_date > et.completed_date
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
)
")->fetchAll(PDO::FETCH_ASSOC);
$__expiredCount = 0;
$__dueSoonCount = 0;
$__today = new DateTime('today');
foreach ($__trRows as $__r) {
$__rem = $__r['reminder_days'] !== null
? (int)$__r['reminder_days']
: ($__r['default_reminder_days'] !== null ? (int)$__r['default_reminder_days'] : 30);
$__due = DateTime::createFromFormat('Y-m-d', $__r['next_due_date']);
if (!$__due) continue;
$__days = (int)$__today->diff($__due)->format('%r%a');
if ($__days < 0) { $__expiredCount++; }
elseif ($__days <= $__rem) { $__dueSoonCount++; }
}
/* Missing mandatory trainings (status = not_present) */
$__notPresentCount = (int)$pdo->query("
SELECT COUNT(*)
FROM employees e
CROSS JOIN training_topics tt
WHERE tt.is_active = 1 AND tt.is_mandatory = 1
AND NOT EXISTS (
SELECT 1 FROM employee_trainings et
WHERE et.employee_id = e.id AND et.training_topic_id = tt.id
)
")->fetchColumn();
if ($__expiredCount === 0 && $__dueSoonCount === 0 && $__notPresentCount === 0) {
return;
}
?>
<div class="my-deadlines-widgets">
<?php if ($__expiredCount > 0): ?>
<a class="mdw mdw-red" href="trainings.php?status=expired">
<span class="mdw-icon"><i class="fa-solid fa-graduation-cap"></i></span>
<span class="mdw-body">
<span class="mdw-count"><?= (int)$__expiredCount ?></span>
<span class="mdw-label d-block">Formazion<?= $__expiredCount === 1 ? 'e scaduta' : 'i scadute' ?></span>
</span>
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
</a>
<?php endif; ?>
<?php if ($__dueSoonCount > 0): ?>
<a class="mdw mdw-orange" href="trainings.php?status=due_soon">
<span class="mdw-icon"><i class="fa-solid fa-hourglass-half"></i></span>
<span class="mdw-body">
<span class="mdw-count"><?= (int)$__dueSoonCount ?></span>
<span class="mdw-label d-block">Formazion<?= $__dueSoonCount === 1 ? 'e da aggiornare' : 'i da aggiornare' ?></span>
</span>
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
</a>
<?php endif; ?>
<?php if ($__notPresentCount > 0): ?>
<a class="mdw mdw-gray" href="trainings.php?status=not_present">
<span class="mdw-icon"><i class="fa-solid fa-circle-question"></i></span>
<span class="mdw-body">
<span class="mdw-count"><?= (int)$__notPresentCount ?></span>
<span class="mdw-label d-block">Obbligator<?= $__notPresentCount === 1 ? 'ia non presente' : 'ie non presenti' ?></span>
</span>
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
</a>
<?php endif; ?>
</div>
@@ -0,0 +1,46 @@
<!-- MODALE LISTA FOGLI DI LAVORO -->
<div class="modal fade" id="worksheetsListModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-worksheet-list">
<div class="modal-content">
<div class="modal-header" style="background:linear-gradient(135deg,#e7f1ff,#d6e9ff);">
<div>
<h5 class="modal-title mb-0">
<i class="fa-solid fa-clipboard-list me-2"></i>Fogli di lavoro collegati
</h5>
<small class="text-muted">Elenco fogli associati al profilo selezionato</small>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="wl_idmatrice">
<div class="fw-semibold mb-3" id="wl_matrice_name" style="color:#0b3d91;"></div>
<div id="worksheetsListContainer">
<div class="text-muted">Caricamento fogli di lavoro...</div>
</div>
</div>
</div>
</div>
</div>
<!-- MODALE DETTAGLIO FOGLIO DI LAVORO -->
<div class="modal fade" id="worksheetDetailModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-worksheet-view">
<div class="modal-content" style="min-height:92vh;">
<div class="modal-header" style="background:linear-gradient(135deg,#e7f1ff,#d6e9ff);">
<div>
<h5 class="modal-title mb-0">
<i class="fa-solid fa-file-lines me-2"></i>Dettaglio foglio di lavoro
</h5>
<small class="text-muted">Visualizzazione completa in sola lettura</small>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="worksheetDetailContainer">
<div class="text-muted">Caricamento dettaglio foglio...</div>
</div>
</div>
</div>
</div>
File diff suppressed because it is too large Load Diff
+428
View File
@@ -0,0 +1,428 @@
<?php
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
/* ==========================================
PAGE DATA
========================================== */
$sql = "
SELECT jr.*,
(SELECT COUNT(*) FROM employees e WHERE e.job_role_id = jr.id) AS employees_count
FROM job_roles jr
ORDER BY jr.sort_order ASC, jr.name ASC
";
$jobRoles = $pdo->query($sql)->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'); ?>
<title>Gestione Mansioni - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<style>
body { font-size: 1.05rem; background: #f8fafc; }
.card { border-radius: 16px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); }
.back-dashboard {
background-color: #cfe3ff !important; color: #1f2d3d !important;
border: 1px solid #bcd4f4 !important; border-radius: 10px;
font-weight: 600; padding: 10px 18px;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease-in-out;
}
.back-dashboard:hover { background-color: #b9d3ff !important; transform: translateY(-2px); }
.btn-add { background-color: #0d6efd; color: #fff; border-radius: 8px; padding: 10px 20px; font-weight: 500; }
.btn-add:hover { background-color: #0b5ed7; transform: scale(1.02); }
.table thead { background-color: #cfe3ff; color: #1f2d3d; }
.modal-content { border-radius: 16px; }
#tabellaJobRoles thead th { text-align: center; vertical-align: middle; }
.badge-status { padding: 0.25rem 0.6rem; border-radius: 999px; font-size: 0.8rem; font-weight: 600; }
.badge-status.active { background-color: #d1fae5; color: #065f46; }
.badge-status.inactive { background-color: #e5e7eb; color: #374151; }
.description-cell {
max-width: 320px; white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; text-align: left;
}
@media (max-width: 767.98px) {
.card-header { flex-direction: column; align-items: flex-start !important; gap: .5rem; }
.back-dashboard { width: 100%; }
.btn-add { width: 100%; }
}
.jr-card {
border: 1px solid #e2e8f0;
border-radius: 14px;
padding: 14px 16px;
margin-bottom: 12px;
background: #fff;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
}
.jr-card-title {
font-size: 1.1rem;
font-weight: 600;
color: #1f2937;
margin: 0 0 4px 0;
word-break: break-word;
}
.jr-card-desc {
color: #475569;
font-size: 0.95rem;
margin: 0 0 10px 0;
word-break: break-word;
}
.jr-card-meta {
display: flex;
flex-wrap: wrap;
gap: 8px 14px;
font-size: 0.85rem;
color: #64748b;
margin-bottom: 12px;
}
.jr-card-meta b { color: #1f2937; font-weight: 600; }
.jr-card-actions {
display: flex;
gap: 8px;
}
.jr-card-actions .btn {
flex: 1;
}
.jr-empty {
text-align: center;
color: #94a3b8;
padding: 24px 0;
}
</style>
</head>
<body>
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card p-3">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h5 class="mb-0">Gestione Mansioni</h5>
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
↩️ Torna alla Dashboard
</button>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<h6 class="fw-semibold mb-0">Elenco Mansioni / Job Roles</h6>
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addJobRoleModal">
Aggiungi Mansione
</button>
</div>
<!-- DESKTOP / TABLET ≥768px: TABLE -->
<div class="table-responsive d-none d-md-block"><!-- hide on <md -->
<table id="tabellaJobRoles" class="table table-striped align-middle text-center" style="width:100%;">
<thead>
<tr>
<th>ID</th>
<th>Nome</th>
<th>Descrizione</th>
<th>Ordine</th>
<th>Stato</th>
<th>Dipendenti</th>
<th>Creato</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($jobRoles as $row): ?>
<?php
$id = (int)$row['id'];
$name = $row['name'] ?? '';
$description = $row['description'] ?? '';
$sortOrder = (int)($row['sort_order'] ?? 999);
$isActive = (int)($row['is_active'] ?? 1);
$cnt = (int)($row['employees_count'] ?? 0);
$statusClass = $isActive === 1 ? 'active' : 'inactive';
$statusLabel = $isActive === 1 ? 'Attivo' : 'Inattivo';
$createdAt = !empty($row['created_at'])
? date('d/m/Y H:i', strtotime($row['created_at']))
: '-';
?>
<tr>
<td><?= $id ?></td>
<td class="fw-semibold text-start"><?= htmlspecialchars($name) ?></td>
<td class="description-cell" title="<?= htmlspecialchars($description, ENT_QUOTES) ?>">
<?= $description !== '' ? htmlspecialchars($description) : '-' ?>
</td>
<td><?= $sortOrder ?></td>
<td>
<span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span>
</td>
<td><?= $cnt ?></td>
<td><?= $createdAt ?></td>
<td>
<button class="btn btn-sm btn-outline-secondary edit-job-role"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
data-sort_order="<?= $sortOrder ?>"
data-is_active="<?= $isActive ?>">
✏️ Modifica
</button>
<button class="btn btn-sm btn-outline-danger delete-job-role"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
data-count="<?= $cnt ?>">
🗑️ Cancella
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- MOBILE <768px: CARDS -->
<div class="d-block d-md-none">
<?php if (empty($jobRoles)): ?>
<div class="jr-empty">Nessuna mansione presente</div>
<?php endif; ?>
<?php foreach ($jobRoles as $row): ?>
<?php
$id = (int)$row['id'];
$name = $row['name'] ?? '';
$description = $row['description'] ?? '';
$sortOrder = (int)($row['sort_order'] ?? 999);
$isActive = (int)($row['is_active'] ?? 1);
$cnt = (int)($row['employees_count'] ?? 0);
$statusClass = $isActive === 1 ? 'active' : 'inactive';
$statusLabel = $isActive === 1 ? 'Attivo' : 'Inattivo';
?>
<div class="jr-card">
<h6 class="jr-card-title"><?= htmlspecialchars($name) ?></h6>
<?php if ($description !== ''): ?>
<p class="jr-card-desc"><?= htmlspecialchars($description) ?></p>
<?php endif; ?>
<div class="jr-card-meta">
<span><span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span></span>
<span><b>Dipendenti:</b> <?= $cnt ?></span>
<span><b>Ordine:</b> <?= $sortOrder ?></span>
</div>
<div class="jr-card-actions">
<button class="btn btn-sm btn-outline-secondary edit-job-role"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
data-sort_order="<?= $sortOrder ?>"
data-is_active="<?= $isActive ?>">
✏️ Modifica
</button>
<button class="btn btn-sm btn-outline-danger delete-job-role"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
data-count="<?= $cnt ?>">
🗑️ Cancella
</button>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<!-- ADD MODAL -->
<div class="modal fade" id="addJobRoleModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-fullscreen-sm-down">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">Aggiungi Mansione</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addJobRoleForm">
<div class="mb-3">
<label class="form-label fw-semibold">Nome</label>
<input type="text" class="form-control" id="addName" name="name" placeholder="es. Saldatore" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Descrizione</label>
<textarea class="form-control" id="addDescription" name="description" rows="3" placeholder="Opzionale"></textarea>
</div>
<div class="row">
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Ordine</label>
<input type="number" class="form-control" id="addSortOrder" name="sort_order" value="999" min="0">
</div>
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Stato</label>
<select class="form-select" id="addIsActive" name="is_active">
<option value="1" selected>Attivo</option>
<option value="0">Inattivo</option>
</select>
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add">💾 Salva</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- EDIT MODAL -->
<div class="modal fade" id="editJobRoleModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-fullscreen-sm-down">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">Modifica Mansione</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editJobRoleForm">
<input type="hidden" id="editJobRoleId">
<div class="mb-3">
<label class="form-label fw-semibold">Nome</label>
<input type="text" class="form-control" id="editName" name="name" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Descrizione</label>
<textarea class="form-control" id="editDescription" name="description" rows="3"></textarea>
</div>
<div class="row">
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Ordine</label>
<input type="number" class="form-control" id="editSortOrder" name="sort_order" value="999" min="0">
</div>
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Stato</label>
<select class="form-select" id="editIsActive" name="is_active">
<option value="1">Attivo</option>
<option value="0">Inattivo</option>
</select>
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add">💾 Salva Modifiche</button>
</div>
</form>
</div>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
$(document).ready(function() {
$('#tabellaJobRoles').DataTable({
order: [[3, 'asc'], [1, 'asc']],
pageLength: 25,
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json',
emptyTable: 'Nessuna mansione presente'
}
});
function ajaxPost(url, payload, successTitle, errorFallback) {
return fetch(url, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: payload.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({ icon: "success", title: successTitle, confirmButtonColor: "#3085d6" })
.then(() => location.reload());
} else {
Swal.fire({ icon: "error", title: "Errore", text: data.message || errorFallback });
}
})
.catch(err => {
Swal.fire({ icon: "error", title: "Errore", text: "Errore di comunicazione." });
console.error(err);
});
}
$("#addJobRoleForm").on("submit", function(e) {
e.preventDefault();
const p = new URLSearchParams();
p.append('name', $("#addName").val().trim());
p.append('description', $("#addDescription").val().trim());
p.append('sort_order', $("#addSortOrder").val());
p.append('is_active', $("#addIsActive").val());
ajaxPost("ajax/job_roles/save.php", p, "Salvato!", "Impossibile salvare la mansione.");
});
$(document).on("click", ".edit-job-role", function() {
const b = $(this);
$("#editJobRoleId").val(b.data("id"));
$("#editName").val(b.data("name"));
$("#editDescription").val(b.data("description"));
$("#editSortOrder").val(b.data("sort_order"));
$("#editIsActive").val(String(b.data("is_active")));
$("#editJobRoleModal").modal("show");
});
$("#editJobRoleForm").on("submit", function(e) {
e.preventDefault();
const p = new URLSearchParams();
p.append('id', $("#editJobRoleId").val());
p.append('name', $("#editName").val().trim());
p.append('description', $("#editDescription").val().trim());
p.append('sort_order', $("#editSortOrder").val());
p.append('is_active', $("#editIsActive").val());
ajaxPost("ajax/job_roles/save.php", p, "Aggiornato!", "Impossibile aggiornare la mansione.");
});
$(document).on("click", ".delete-job-role", function() {
const id = $(this).data("id");
const name = $(this).data("name");
const cnt = parseInt($(this).data("count")) || 0;
if (cnt > 0) {
Swal.fire({
icon: "warning",
title: "Impossibile cancellare",
text: "La mansione \"" + name + "\" è assegnata a " + cnt + " dipendente/i. Rimuovi prima l'associazione."
});
return;
}
Swal.fire({
title: "Confermi la cancellazione?",
text: name ? ("Mansione: " + name) : "La mansione verrà cancellata.",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#d33",
cancelButtonColor: "#6c757d",
confirmButtonText: "Sì, cancella",
cancelButtonText: "Annulla"
}).then((result) => {
if (!result.isConfirmed) return;
const p = new URLSearchParams();
p.append('id', id);
ajaxPost("ajax/job_roles/delete.php", p, "Cancellato!", "Impossibile cancellare la mansione.");
});
});
});
</script>
</body>
</html>
+380
View File
@@ -0,0 +1,380 @@
<?php
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Read line id from GET
$lineId = isset($_GET['line_id']) ? (int)$_GET['line_id'] : 0;
if ($lineId <= 0) {
die("Invalid line id");
}
// Load line data
$stmtLine = $pdo->prepare("SELECT * FROM production_lines WHERE id = :id");
$stmtLine->execute([':id' => $lineId]);
$line = $stmtLine->fetch(PDO::FETCH_ASSOC);
if (!$line) {
die("Production line not found");
}
// Load parameters for this line
$stmtParams = $pdo->prepare("
SELECT *
FROM production_line_params
WHERE line_id = :line_id
ORDER BY position ASC, id ASC
");
$stmtParams->execute([':line_id' => $lineId]);
$params = $stmtParams->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'); ?>
<title>Parametri Linea <?= htmlspecialchars($line['name']) ?> - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<!-- jQuery / Bootstrap / SweetAlert -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<style>
body {
font-size: 1.05rem;
background: #f8fafc;
}
.card {
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
th {
text-align: center !important;
}
.back-lines {
background-color: #cfe3ff !important;
color: #1f2d3d !important;
border: 1px solid #bcd4f4 !important;
border-radius: 10px;
font-weight: 600;
font-size: 1rem;
padding: 10px 18px;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease-in-out;
}
.back-lines:hover {
background-color: #b9d3ff !important;
transform: translateY(-2px);
}
.btn-add {
background-color: #0d6efd;
color: #fff;
border-radius: 8px;
padding: 10px 20px;
font-weight: 500;
transition: all 0.2s ease-in-out;
}
.btn-add:hover {
background-color: #0b5ed7;
transform: scale(1.02);
}
.btn-action {
border: none;
background: transparent;
cursor: pointer;
font-size: 1.2rem;
}
.btn-action.edit {
color: #007bff;
}
.btn-action.delete {
color: #dc3545;
}
.btn-action:hover {
transform: scale(1.15);
}
.table thead {
background-color: #cfe3ff;
color: #1f2d3d;
}
.icon-preview i {
font-size: 1.4rem;
}
.modal-content {
border-radius: 16px;
}
</style>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card p-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0 w-100 text-center">
Parametri Linea: <?= htmlspecialchars($line['name']) ?> (Linea <?= (int)$line['line_number']; ?>)
</h5>
<button type="button" class="btn back-lines position-absolute end-0 me-3" onclick="location.href='linee.php'">
↩️ Torna alle Linee
</button>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h6 class="fw-semibold mb-0">Elenco Parametri</h6>
<small class="text-muted">
Linea ID: <?= (int)$line['id']; ?> - Colore:
<span style="display:inline-block;width:16px;height:16px;border-radius:4px;border:1px solid #999;background:<?= htmlspecialchars($line['color']); ?>;"></span>
</small>
</div>
<button class="btn btn-add" id="btnAddParam"> Aggiungi Parametro</button>
</div>
<div class="table-responsive">
<table id="tabellaParametri" class="table table-striped align-middle text-center" style="width:100%;">
<thead>
<tr>
<th>ID</th>
<th>Position</th>
<th>Short label</th>
<th>Label</th>
<th>Icon class</th>
<th>Preview</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($params)) : ?>
<tr>
<td colspan="7" class="text-muted">No parameters defined for this line.</td>
</tr>
<?php else : ?>
<?php foreach ($params as $p) : ?>
<tr>
<td><?= (int)$p['id']; ?></td>
<td><?= (int)$p['position']; ?></td>
<td><?= htmlspecialchars($p['short_label']); ?></td>
<td><?= htmlspecialchars($p['label']); ?></td>
<td><?= htmlspecialchars($p['icon']); ?></td>
<td class="icon-preview">
<?php if (!empty($p['icon'])) : ?>
<i class="bi <?= htmlspecialchars($p['icon']); ?>"></i>
<?php else : ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td>
<button class="btn-action edit"
data-id="<?= (int)$p['id']; ?>">
<i class="fas fa-edit"></i>
</button>
<button class="btn-action delete"
data-id="<?= (int)$p['id']; ?>">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<!-- ADD/EDIT PARAM MODAL -->
<div class="modal fade" id="paramModal" tabindex="-1" aria-labelledby="paramModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title" id="paramModalLabel">Add Parameter</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="paramForm">
<input type="hidden" name="param_id" id="param_id" value="">
<input type="hidden" name="line_id" id="line_id" value="<?= (int)$line['id']; ?>">
<div class="mb-3">
<label for="position" class="form-label fw-semibold">Position (slot)</label>
<input type="number" class="form-control" id="position" name="position" required min="1">
</div>
<div class="mb-3">
<label for="short_label" class="form-label fw-semibold">Short label</label>
<input type="text" class="form-control" id="short_label" name="short_label" maxlength="20" required>
</div>
<div class="mb-3">
<label for="label" class="form-label fw-semibold">Label</label>
<input type="text" class="form-control" id="label" name="label" maxlength="100" required>
</div>
<div class="mb-3">
<label for="icon" class="form-label fw-semibold">Bootstrap Icon class (optional)</label>
<input type="text" class="form-control" id="icon" name="icon" placeholder="es. bi-thermometer-half">
<small class="text-muted">Use a Bootstrap Icons class without the <code>bi</code> prefix (prefix is automatically added in preview).</small>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add" id="btnSaveParam">💾 Save</button>
</div>
</form>
</div>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
$(document).ready(function() {
const table = $('#tabellaParametri').DataTable({
order: [
[1, 'asc']
],
pageLength: 50,
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json'
}
});
const paramModal = new bootstrap.Modal(document.getElementById('paramModal'));
function resetForm() {
$('#param_id').val('');
$('#position').val('');
$('#short_label').val('');
$('#label').val('');
$('#icon').val('');
}
// Open modal for new parameter
$('#btnAddParam').on('click', function() {
resetForm();
$('#paramModalLabel').text('Add Parameter');
$('#btnSaveParam').text('💾 Save');
paramModal.show();
});
// Open modal for edit
$(document).on('click', '.edit', function() {
const id = $(this).data('id');
fetch('get_param.php?id=' + id)
.then(r => r.json())
.then(data => {
if (data.success) {
const p = data.param;
$('#param_id').val(p.id);
$('#line_id').val(p.line_id);
$('#position').val(p.position);
$('#short_label').val(p.short_label);
$('#label').val(p.label);
$('#icon').val(p.icon || '');
$('#paramModalLabel').text('Edit Parameter');
$('#btnSaveParam').text('💾 Update');
paramModal.show();
} else {
Swal.fire('Error', data.message || 'Unable to load parameter.', 'error');
}
})
.catch(() => {
Swal.fire('Error', 'Server communication error.', 'error');
});
});
// Save (add or update)
$('#paramForm').on('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch('save_param.php', {
method: 'POST',
body: formData
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire('Saved', data.message || 'Parameter saved correctly.', 'success')
.then(() => {
location.reload();
});
} else {
Swal.fire('Error', data.message || 'Error while saving parameter.', 'error');
}
})
.catch(() => {
Swal.fire('Error', 'Server communication error.', 'error');
});
});
// Delete parameter
$(document).on('click', '.delete', function() {
const id = $(this).data('id');
Swal.fire({
title: 'Are you sure?',
text: 'This action will permanently delete the parameter.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Yes, delete',
cancelButtonText: 'Cancel',
confirmButtonColor: '#d33'
}).then(result => {
if (result.isConfirmed) {
fetch('delete_param.php?id=' + id)
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire('Deleted', data.message || 'Parameter deleted.', 'success')
.then(() => location.reload());
} else {
Swal.fire('Error', data.message || 'Error while deleting parameter.', 'error');
}
})
.catch(() => {
Swal.fire('Error', 'Server communication error.', 'error');
});
}
});
});
});
</script>
</body>
</html>
+37 -16
View File
@@ -11,7 +11,6 @@
<!-- jQuery e Bootstrap --> <!-- jQuery e Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables --> <!-- DataTables -->
@@ -84,6 +83,10 @@
color: #128346; color: #128346;
} }
.btn-action.params {
color: #6f42c1;
}
.btn-action:hover { .btn-action:hover {
transform: scale(1.15); transform: scale(1.15);
} }
@@ -114,7 +117,7 @@
</head> </head>
<body> <body>
<div class="wrapper"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
@@ -134,7 +137,7 @@
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addLineaModal"> Aggiungi Linea</button> <button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addLineaModal"> Aggiungi Linea</button>
</div> </div>
<!-- TABELLA --> <!-- TABLE -->
<div class="table-responsive"> <div class="table-responsive">
<table id="tabellaLinee" class="table table-striped align-middle text-center" style="width:100%;"> <table id="tabellaLinee" class="table table-striped align-middle text-center" style="width:100%;">
<thead> <thead>
@@ -156,30 +159,42 @@
$stmt = $pdo->query("SELECT * FROM production_lines ORDER BY line_number ASC"); $stmt = $pdo->query("SELECT * FROM production_lines ORDER BY line_number ASC");
if ($stmt->rowCount() === 0) { if ($stmt->rowCount() === 0) {
echo "<tr><td colspan='7' class='text-muted'>Nessuna linea di produzione presente</td></tr>"; echo "<tr><td colspan='8' class='text-muted'>Nessuna linea di produzione presente</td></tr>";
} else { } else {
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$badge = $row['status'] === 'active' $badge = $row['status'] === 'active'
? "<span class='badge-active'>Attiva</span>" ? "<span class='badge-active'>Attiva</span>"
: "<span class='badge-inactive'>Inattiva</span>"; : "<span class='badge-inactive'>Inattiva</span>";
$lineNameEsc = htmlspecialchars($row['name'], ENT_QUOTES, 'UTF-8');
echo "<tr> echo "<tr>
<td>{$row['id']}</td> <td>{$row['id']}</td>
<td>{$row['line_number']}</td> <td>{$row['line_number']}</td>
<td>" . htmlspecialchars($row['name']) . "</td> <td>{$lineNameEsc}</td>
<td>" . htmlspecialchars($row['model']) . "</td> <td>" . htmlspecialchars($row['model']) . "</td>
<td>" . htmlspecialchars($row['brand']) . "</td> <td>" . htmlspecialchars($row['brand']) . "</td>
<td> <td>
<div style='width:28px; height:28px; border-radius:6px; border:1px solid #999; background: {$row['color']}; margin:auto;'></div> <div style='width:28px; height:28px; border-radius:6px; border:1px solid #999; background: {$row['color']}; margin:auto;'></div>
</td> </td>
<td>{$badge}</td> <td>{$badge}</td>
<td> <td>
<button class='btn-action params' title='Parametri linea'
<button class='btn-action edit' title='Modifica' data-id='{$row['id']}'><i class='fas fa-edit'></i></button> data-id='{$row['id']}'
<button class='btn-action delete' title='Elimina' data-id='{$row['id']}'><i class='fas fa-trash'></i></button> data-name='{$lineNameEsc}'>
<button class='btn-action toggle' title='Cambia stato' data-id='{$row['id']}' data-status='{$row['status']}'><i class='fas fa-power-off'></i></button> <i class='fas fa-sliders-h'></i>
</button>
<button class='btn-action edit' title='Modifica' data-id='{$row['id']}'>
<i class='fas fa-edit'></i>
</button>
<button class='btn-action delete' title='Elimina' data-id='{$row['id']}'>
<i class='fas fa-trash'></i>
</button>
<button class='btn-action toggle' title='Cambia stato'
data-id='{$row['id']}'
data-status='{$row['status']}'>
<i class='fas fa-power-off'></i>
</button>
</td> </td>
</tr>"; </tr>";
} }
@@ -196,7 +211,7 @@
<?php include('include/footer.php'); ?> <?php include('include/footer.php'); ?>
</div> </div>
<!-- MODALE AGGIUNTA LINEA --> <!-- ADD LINE MODAL -->
<div class="modal fade" id="addLineaModal" tabindex="-1" aria-labelledby="addLineaLabel" aria-hidden="true"> <div class="modal fade" id="addLineaModal" tabindex="-1" aria-labelledby="addLineaLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
@@ -250,7 +265,7 @@
} }
}); });
// Aggiungi linea // Add line
$("#addLineaForm").on("submit", function(e) { $("#addLineaForm").on("submit", function(e) {
e.preventDefault(); e.preventDefault();
const formData = new FormData(this); const formData = new FormData(this);
@@ -271,13 +286,19 @@
.catch(() => Swal.fire("Errore", "Impossibile contattare il server.", "error")); .catch(() => Swal.fire("Errore", "Impossibile contattare il server.", "error"));
}); });
// ✏️ Modifica // ✏️ Edit line
$(document).on("click", ".edit", function() { $(document).on("click", ".edit", function() {
const id = $(this).data("id"); const id = $(this).data("id");
location.href = "edit_linea.php?id=" + id; location.href = "edit_linea.php?id=" + id;
}); });
// 🗑️ Elimina // 🧩 Parameters management
$(document).on("click", ".params", function() {
const id = $(this).data("id");
location.href = "line_params.php?line_id=" + id;
});
// 🗑️ Delete line
$(document).on("click", ".delete", function() { $(document).on("click", ".delete", function() {
const id = $(this).data("id"); const id = $(this).data("id");
Swal.fire({ Swal.fire({
@@ -304,7 +325,7 @@
}); });
}); });
// 🔘 Toggle stato // 🔘 Toggle status
$(document).on("click", ".toggle", function() { $(document).on("click", ".toggle", function() {
const id = $(this).data("id"); const id = $(this).data("id");
const currentStatus = $(this).data("status"); const currentStatus = $(this).data("status");
+643
View File
@@ -0,0 +1,643 @@
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
/**
* ws_lookup_options manager page
* Table expected:
* - id (AI)
* - category (varchar)
* - value (varchar)
* - label (varchar)
* - sort_order (int)
* - is_default (tinyint 0/1)
* - is_active (tinyint 0/1)
*/
// AJAX HANDLERS
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['ajax'] == '1') {
header('Content-Type: application/json; charset=utf-8');
$action = $_POST['action'] ?? '';
try {
if ($action === 'add') {
$category = trim($_POST['category'] ?? '');
$value = trim($_POST['value'] ?? '');
$label = trim($_POST['label'] ?? '');
$sort_order = (int)($_POST['sort_order'] ?? 100);
$is_default = (int)($_POST['is_default'] ?? 0) ? 1 : 0;
$is_active = (int)($_POST['is_active'] ?? 1) ? 1 : 0;
if ($category === '' || $value === '' || $label === '') {
echo json_encode(['success' => false, 'message' => 'Compila category, value e label']);
exit;
}
$pdo->beginTransaction();
// If set as default, unset other defaults in same category (simple & reliable)
if ($is_default === 1) {
$stmt = $pdo->prepare("UPDATE ws_lookup_options SET is_default = 0 WHERE category = ?");
$stmt->execute([$category]);
}
$stmt = $pdo->prepare("
INSERT INTO ws_lookup_options
(category, value, label, sort_order, is_default, is_active)
VALUES (?, ?, ?, ?, ?, ?)
");
$stmt->execute([$category, $value, $label, $sort_order, $is_default, $is_active]);
$pdo->commit();
echo json_encode(['success' => true]);
exit;
}
if ($action === 'edit') {
$id = (int)($_POST['id'] ?? 0);
$category = trim($_POST['category'] ?? '');
$value = trim($_POST['value'] ?? '');
$label = trim($_POST['label'] ?? '');
$sort_order = (int)($_POST['sort_order'] ?? 100);
$is_default = (int)($_POST['is_default'] ?? 0) ? 1 : 0;
$is_active = (int)($_POST['is_active'] ?? 1) ? 1 : 0;
if ($id <= 0 || $category === '' || $value === '' || $label === '') {
echo json_encode(['success' => false, 'message' => 'Dati non validi']);
exit;
}
$pdo->beginTransaction();
// If set as default, unset other defaults in same category (excluding current)
if ($is_default === 1) {
$stmt = $pdo->prepare("UPDATE ws_lookup_options SET is_default = 0 WHERE category = ? AND id <> ?");
$stmt->execute([$category, $id]);
}
$stmt = $pdo->prepare("
UPDATE ws_lookup_options SET
category = ?,
value = ?,
label = ?,
sort_order = ?,
is_default = ?,
is_active = ?
WHERE id = ?
");
$stmt->execute([$category, $value, $label, $sort_order, $is_default, $is_active, $id]);
$pdo->commit();
echo json_encode(['success' => true]);
exit;
}
if ($action === 'delete') {
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'ID non valido']);
exit;
}
$stmt = $pdo->prepare("DELETE FROM ws_lookup_options WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(['success' => true]);
exit;
}
echo json_encode(['success' => false, 'message' => 'Azione sconosciuta']);
exit;
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
exit;
}
}
// Categories list (static for now, easy to extend)
$categories = [
'marking' => 'Marchiatura',
'lubrication_type' => 'Lubrificazione',
'control_frequency_cut' => 'Frequenza controllo (taglio)',
'control_frequency_drawing' => 'Frequenza controllo disegno',
'control_frequency_jig' => 'Frequenza controllo in dima',
'control_mode_jig' => 'Modalità controllo in dima',
'box_type' => 'Tipo scatola',
'pallet_type' => 'Tipo bancale',
'requested_package_code' => 'Confezione richiesta',
];
// Selected category filter
$categoryFilter = trim($_GET['cat'] ?? '');
if ($categoryFilter !== '' && !array_key_exists($categoryFilter, $categories)) {
$categoryFilter = '';
}
// Load options
$params = [];
$sql = "
SELECT id, category, value, label, sort_order, is_default, is_active, created_at, updated_at
FROM ws_lookup_options
";
if ($categoryFilter !== '') {
$sql .= " WHERE category = ? ";
$params[] = $categoryFilter;
}
$sql .= " ORDER BY category ASC, sort_order ASC, label ASC, id ASC ";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$options = $stmt->fetchAll(PDO::FETCH_ASSOC);
function h($v)
{
return htmlspecialchars((string)$v, ENT_QUOTES);
}
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<?php include('cssinclude.php'); ?>
<title>Lookup Worksheet</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<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/@ttskch/select2-bootstrap4-theme@1.5.2/dist/select2-bootstrap4.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<style>
body {
font-size: 0.95rem;
background: #f8fafc;
}
.card {
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.back-dashboard {
background: #cfe3ff !important;
color: #1f2d3d !important;
border: 1px solid #bcd4f4 !important;
border-radius: 10px;
font-weight: 600;
padding: 10px 18px;
}
.btn-add {
background: #0d6efd;
color: white;
border-radius: 8px;
padding: 10px 20px;
font-weight: 500;
}
.table thead {
background: #cfe3ff;
color: #1f2d3d;
}
.modal-content {
border-radius: 16px;
}
.small-hint {
color: #6b7280;
font-size: 0.85rem;
}
/* Select2 sizing alignment */
.select2-container .select2-selection--single {
height: calc(2.375rem + 2px);
padding: 0.375rem 0.75rem;
border: 1px solid #ced4da;
border-radius: 0.375rem;
display: flex;
align-items: center;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: 1.6;
padding-left: 0;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: calc(2.375rem + 2px);
}
.select2-container {
width: 100% !important;
}
.badge-soft {
background: #eef6ff;
color: #1f2d3d;
border: 1px solid #bcd4f4;
font-weight: 600;
}
.muted {
color: #6b7280;
}
</style>
</head>
<body>
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card p-3">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0">Lookup Worksheet</h5>
<div class="small-hint">Gestione valori predefiniti (tendine) per i campi dei fogli di lavoro</div>
</div>
<button class="btn back-dashboard" onclick="location.href='production_dashboard.php'">↩️ Dashboard</button>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center gap-2" style="min-width: 420px;">
<div class="fw-semibold">Categoria</div>
<select id="categoryFilter" class="form-select">
<option value="">-- Tutte --</option>
<?php foreach ($categories as $key => $label): ?>
<option value="<?= h($key) ?>" <?= $categoryFilter === $key ? 'selected' : '' ?>>
<?= h($label) ?> (<?= h($key) ?>)
</option>
<?php endforeach; ?>
</select>
</div>
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addModal">
Aggiungi valore
</button>
</div>
<div class="table-responsive">
<table id="tabellaLookup" class="table table-striped table-bordered">
<thead>
<tr>
<th>ID</th>
<th>Category</th>
<th>Value</th>
<th>Label</th>
<th>Ordine</th>
<th>Default</th>
<th>Attivo</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($options as $o): ?>
<tr>
<td><?= (int)$o['id'] ?></td>
<td>
<span class="badge badge-soft"><?= h($o['category']) ?></span>
<div class="small-hint">
<?= h($categories[$o['category']] ?? '-') ?>
</div>
</td>
<td class="fw-semibold"><?= h($o['value']) ?></td>
<td><?= h($o['label']) ?></td>
<td><?= (int)$o['sort_order'] ?></td>
<td><?= (int)$o['is_default'] === 1 ? '✅' : '-' ?></td>
<td><?= (int)$o['is_active'] === 1 ? '✅' : '⛔' ?></td>
<td class="text-nowrap">
<button class="btn btn-sm btn-outline-primary edit-row"
data-id="<?= (int)$o['id'] ?>"
data-category="<?= h($o['category']) ?>"
data-value="<?= h($o['value']) ?>"
data-label="<?= h($o['label']) ?>"
data-sort_order="<?= (int)$o['sort_order'] ?>"
data-is_default="<?= (int)$o['is_default'] ?>"
data-is_active="<?= (int)$o['is_active'] ?>">
✏️ Modifica
</button>
<button class="btn btn-sm btn-outline-danger delete-row"
data-id="<?= (int)$o['id'] ?>"
data-name="<?= h($o['category'] . ' / ' . $o['value']) ?>">
🗑️ Elimina
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="small-hint mt-2">
Suggerimento: imposta <b>Default</b> solo per 1 valore per categoria (questa pagina lo gestisce automaticamente).
</div>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<!-- MODAL ADD -->
<div class="modal fade" id="addModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header" style="background:#cfe3ff;">
<h5 class="modal-title">Aggiungi valore</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addForm">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold">Categoria *</label>
<select class="form-select" name="category" id="add_category" required>
<option value="">-- Seleziona --</option>
<?php foreach ($categories as $key => $label): ?>
<option value="<?= h($key) ?>"><?= h($label) ?> (<?= h($key) ?>)</option>
<?php endforeach; ?>
</select>
<div class="small-hint">Categoria = campo di destinazione nel worksheet</div>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Value *</label>
<input type="text" class="form-control" name="value" placeholder="es. S, N, EUR, EACH_COIL" required>
<div class="small-hint">Valore tecnico stabile (quello che salvi nel DB)</div>
</div>
<div class="col-md-8">
<label class="form-label fw-semibold">Label *</label>
<input type="text" class="form-control" name="label" placeholder="Testo visibile in tendina" required>
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Ordine</label>
<input type="number" class="form-control" name="sort_order" value="100" min="1">
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Default</label>
<select class="form-select" name="is_default">
<option value="0" selected>No</option>
<option value="1"></option>
</select>
<div class="small-hint">Se “Sì”, gli altri default della stessa categoria verranno disattivati</div>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Attivo</label>
<select class="form-select" name="is_active">
<option value="1" selected></option>
<option value="0">No</option>
</select>
</div>
<div class="col-12 text-center mt-2">
<button type="submit" class="btn btn-add">💾 Salva</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- MODAL EDIT -->
<div class="modal fade" id="editModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header" style="background:#cfe3ff;">
<h5 class="modal-title">Modifica valore</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editForm">
<input type="hidden" name="id" id="edit_id">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold">Categoria *</label>
<select class="form-select" name="category" id="edit_category" required>
<option value="">-- Seleziona --</option>
<?php foreach ($categories as $key => $label): ?>
<option value="<?= h($key) ?>"><?= h($label) ?> (<?= h($key) ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Value *</label>
<input type="text" class="form-control" name="value" id="edit_value" required>
</div>
<div class="col-md-8">
<label class="form-label fw-semibold">Label *</label>
<input type="text" class="form-control" name="label" id="edit_label" required>
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Ordine</label>
<input type="number" class="form-control" name="sort_order" id="edit_sort_order" min="1">
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Default</label>
<select class="form-select" name="is_default" id="edit_is_default">
<option value="0">No</option>
<option value="1"></option>
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Attivo</label>
<select class="form-select" name="is_active" id="edit_is_active">
<option value="1"></option>
<option value="0">No</option>
</select>
</div>
<div class="col-12 text-center mt-2">
<button type="submit" class="btn btn-add">💾 Salva modifiche</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
$(document).ready(function() {
// DataTable
$('#tabellaLookup').DataTable({
pageLength: 50,
lengthMenu: [10, 25, 50, 100],
order: [
[1, 'asc'],
[4, 'asc'],
[3, 'asc']
],
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json'
}
});
// Select2 for category filter + modal selects
$('#categoryFilter, #add_category, #edit_category').select2({
theme: 'bootstrap4',
width: '100%',
placeholder: '-- Seleziona --',
allowClear: true
});
// Filter redirect
$('#categoryFilter').on('change', function() {
const v = $(this).val() || '';
const base = window.location.pathname.split('/').pop();
if (v === '') {
window.location.href = base;
} else {
window.location.href = base + '?cat=' + encodeURIComponent(v);
}
});
// Add
$('#addForm').on('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
formData.append('ajax', '1');
formData.append('action', 'add');
fetch('', {
method: 'POST',
body: new URLSearchParams(formData)
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: 'success',
title: 'Salvato!',
timer: 900
})
.then(() => location.reload());
} else {
Swal.fire({
icon: 'error',
title: 'Errore',
text: data.message
});
}
});
});
// Open edit modal
$(document).on('click', '.edit-row', function() {
const btn = $(this);
$('#edit_id').val(btn.data('id'));
$('#edit_category').val(btn.data('category')).trigger('change');
$('#edit_value').val(btn.data('value'));
$('#edit_label').val(btn.data('label'));
$('#edit_sort_order').val(btn.data('sort_order'));
$('#edit_is_default').val(String(btn.data('is_default')));
$('#edit_is_active').val(String(btn.data('is_active')));
const modal = new bootstrap.Modal(document.getElementById('editModal'));
modal.show();
});
// Save edit
$('#editForm').on('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
formData.append('ajax', '1');
formData.append('action', 'edit');
fetch('', {
method: 'POST',
body: new URLSearchParams(formData)
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: 'success',
title: 'Aggiornato!',
timer: 900
})
.then(() => location.reload());
} else {
Swal.fire({
icon: 'error',
title: 'Errore',
text: data.message
});
}
});
});
// Delete
$(document).on('click', '.delete-row', function() {
const id = $(this).data('id');
const name = $(this).data('name');
Swal.fire({
title: 'Confermi eliminazione?',
text: name,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#6c757d',
confirmButtonText: 'Sì, elimina',
cancelButtonText: 'Annulla'
}).then(result => {
if (!result.isConfirmed) return;
fetch('', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `ajax=1&action=delete&id=${id}`
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: 'success',
title: 'Eliminato!',
timer: 900
})
.then(() => location.reload());
} else {
Swal.fire({
icon: 'error',
title: 'Errore',
text: data.message
});
}
});
});
});
});
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+48 -5
View File
@@ -12,6 +12,7 @@ $sql = "
SELECT SELECT
p.*, p.*,
m.nome AS matrice, m.nome AS matrice,
m.photo AS matrice_photo,
l.name AS linea, l.name AS linea,
c.nome AS cliente, c.nome AS cliente,
s.nome AS status_nome, s.nome AS status_nome,
@@ -167,7 +168,7 @@ $rows_special = array_filter($rows, function ($r) {
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
@@ -222,7 +223,20 @@ $rows_special = array_filter($rows, function ($r) {
data-seconds="<?= $sec ?>" data-seconds="<?= $sec ?>"
style="--rowcolor: <?= htmlspecialchars($r['line_color']) ?>;"> style="--rowcolor: <?= htmlspecialchars($r['line_color']) ?>;">
<td><?= htmlspecialchars($r['conferma_ordine'] ?? '') ?></td> <td><?= htmlspecialchars($r['conferma_ordine'] ?? '') ?></td>
<td><?= htmlspecialchars($r['matrice']) ?></td> <td>
<?= htmlspecialchars($r['matrice']) ?>
<?php if (!empty($r['matrice_photo'])): ?>
<br>
<img src="photos/matrici/<?= htmlspecialchars($r['matrice_photo']) ?>"
data-full="photos/matrici/<?= htmlspecialchars($r['matrice_photo']) ?>"
class="photo-thumb"
style="width:42px;height:42px;object-fit:cover;
border-radius:6px;border:1px solid #ced4da;
cursor:pointer;margin-top:3px;">
<?php endif; ?>
</td>
<?php <?php
$mescRaw = $r['mescole_list'] ?? ''; $mescRaw = $r['mescole_list'] ?? '';
$mescArray = $mescRaw !== '' ? explode(' | ', $mescRaw) : []; $mescArray = $mescRaw !== '' ? explode(' | ', $mescRaw) : [];
@@ -338,7 +352,20 @@ $rows_special = array_filter($rows, function ($r) {
data-status="<?= (int)$r['id_status'] ?>" data-status="<?= (int)$r['id_status'] ?>"
style="background-color: <?= htmlspecialchars($r['line_color']) ?>;"> style="background-color: <?= htmlspecialchars($r['line_color']) ?>;">
<td><?= htmlspecialchars($r['conferma_ordine'] ?? '') ?></td> <td><?= htmlspecialchars($r['conferma_ordine'] ?? '') ?></td>
<td><?= htmlspecialchars($r['matrice']) ?></td> <td>
<?= htmlspecialchars($r['matrice']) ?>
<?php if (!empty($r['matrice_photo'])): ?>
<br>
<img src="photos/matrici/<?= htmlspecialchars($r['matrice_photo']) ?>"
data-full="photos/matrici/<?= htmlspecialchars($r['matrice_photo']) ?>"
class="photo-thumb"
style="width:42px;height:42px;object-fit:cover;
border-radius:6px;border:1px solid #ced4da;
cursor:pointer;margin-top:3px;">
<?php endif; ?>
</td>
<?php <?php
$mescRaw = $r['mescole_list'] ?? ''; $mescRaw = $r['mescole_list'] ?? '';
$mescArray = $mescRaw !== '' ? explode(' | ', $mescRaw) : []; $mescArray = $mescRaw !== '' ? explode(' | ', $mescRaw) : [];
@@ -565,6 +592,19 @@ $rows_special = array_filter($rows, function ($r) {
$("#photoModal").css("display", "flex"); $("#photoModal").css("display", "flex");
}); });
// ❌ Chiudi modale foto (delegato)
$(document).on("click", "#photoModalCloseX, #photoCancel", function() {
$("#photoModal").hide();
});
// ❌ Chiudi cliccando sul backdrop (delegato)
$(document).on("click", "#photoModal", function(e) {
if (e.target.id === "photoModal") {
$("#photoModal").hide();
}
});
$("#photoModalCloseX, #photoCancel").on("click", function() { $("#photoModalCloseX, #photoCancel").on("click", function() {
$("#photoModal").hide(); $("#photoModal").hide();
}); });
@@ -610,16 +650,19 @@ $rows_special = array_filter($rows, function ($r) {
$("#imagePreviewModal").css("display", "flex"); $("#imagePreviewModal").css("display", "flex");
}); });
$("#previewCloseX").on("click", function() { // ❌ Chiudi preview con la X (delegato)
$(document).on("click", "#previewCloseX", function() {
$("#imagePreviewModal").hide(); $("#imagePreviewModal").hide();
}); });
$("#imagePreviewModal").on("click", function(e) { // ❌ Chiudi cliccando fuori (delegato)
$(document).on("click", "#imagePreviewModal", function(e) {
if (e.target.id === "imagePreviewModal") { if (e.target.id === "imagePreviewModal") {
$("#imagePreviewModal").hide(); $("#imagePreviewModal").hide();
} }
}); });
// ===== MODALE MESCOLE (Multi) ===== // ===== MODALE MESCOLE (Multi) =====
$(document).on("click", ".showMescole", function(e) { $(document).on("click", ".showMescole", function(e) {
e.preventDefault(); e.preventDefault();
File diff suppressed because it is too large Load Diff
+495 -25
View File
@@ -11,7 +11,6 @@
<!-- jQuery e Bootstrap --> <!-- jQuery e Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables --> <!-- DataTables -->
@@ -79,11 +78,66 @@
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
} }
/* --- FIX colonne lunghe: tronca con ellissi --- */
#tabellaMescole {
table-layout: fixed;
width: 100% !important;
}
#tabellaMescole th,
#tabellaMescole td {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ID */
#tabellaMescole th:nth-child(1),
#tabellaMescole td:nth-child(1) {
width: 70px;
max-width: 70px;
}
/* Nome Uscita */
#tabellaMescole th:nth-child(2),
#tabellaMescole td:nth-child(2) {
width: 360px;
max-width: 360px;
}
/* Q.tà totale */
#tabellaMescole th:nth-child(3),
#tabellaMescole td:nth-child(3) {
width: 140px;
max-width: 140px;
}
/* Linee */
#tabellaMescole th:nth-child(4),
#tabellaMescole td:nth-child(4) {
width: 260px;
max-width: 260px;
}
/* Stato */
#tabellaMescole th:nth-child(5),
#tabellaMescole td:nth-child(5) {
width: 140px;
max-width: 140px;
}
/* Azioni */
#tabellaMescole th:nth-child(6),
#tabellaMescole td:nth-child(6) {
width: 330px;
max-width: 330px;
}
</style> </style>
</head> </head>
<body> <body>
<div class="wrapper"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
@@ -99,7 +153,16 @@
<div class="card-body"> <div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center gap-3">
<h6 class="fw-semibold mb-0">Elenco Completo</h6> <h6 class="fw-semibold mb-0">Elenco Completo</h6>
<select id="filterActive" class="form-select form-select-sm" style="width:220px;">
<option value="all">Tutte</option>
<option value="1" selected>Solo Attive</option>
<option value="0">Solo Inattive</option>
</select>
</div>
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addMescolaModal"> Aggiungi Mescola</button> <button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addMescolaModal"> Aggiungi Mescola</button>
</div> </div>
@@ -109,47 +172,105 @@
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>ID</th>
<th>Nome Mescola</th>
<th>Nome Uscita</th> <th>Nome Uscita</th>
<th>Q. Totale</th>
<th>Linee Associate</th> <th>Linee Associate</th>
<th>Stato</th>
<th>Azioni</th> <th>Azioni</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php <?php
$db = DBHandlerSelect::getInstance(); $db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection(); $pdo = $db->getConnection();
// filtro: all / 1 / 0
$activeFilter = $_GET['active'] ?? '1';
if (!in_array($activeFilter, ['all', '0', '1'], true)) {
$activeFilter = '1';
}
$where = "";
$params = [];
if ($activeFilter !== 'all') {
$where = "WHERE m.is_active = ?";
$params[] = (int)$activeFilter;
}
// QTY IN SUBQUERY per evitare raddoppi dovuti alle linee
$sql = " $sql = "
SELECT m.id, m.nome, m.nomeuscita, SELECT
GROUP_CONCAT(pl.name SEPARATOR ', ') AS linee m.id,
m.nomeuscita,
m.is_active,
IFNULL(q.qty_totale, 0) AS qty_totale,
GROUP_CONCAT(DISTINCT pl.name SEPARATOR ', ') AS linee
FROM mescole m FROM mescole m
LEFT JOIN (
SELECT idmescola, SUM(qty) AS qty_totale
FROM mescole_supplier_lots
GROUP BY idmescola
) q ON q.idmescola = m.id
LEFT JOIN mescole_lines ml ON m.id = ml.idmescola LEFT JOIN mescole_lines ml ON m.id = ml.idmescola
LEFT JOIN production_lines pl ON ml.idlinea = pl.id LEFT JOIN production_lines pl ON ml.idlinea = pl.id
$where
GROUP BY m.id GROUP BY m.id
ORDER BY m.id DESC"; ORDER BY m.id DESC
";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$stmt = $pdo->query($sql);
if ($stmt->rowCount() === 0) { if ($stmt->rowCount() === 0) {
echo "<tr><td colspan='5' class='text-muted'>Nessuna mescola presente</td></tr>"; // DataTables-friendly: 6 TD reali
echo "<tr>
<td class='text-muted'>-</td>
<td class='text-muted'>Nessuna mescola presente</td>
<td class='text-muted'>-</td>
<td class='text-muted'>-</td>
<td class='text-muted'>-</td>
<td class='text-muted'>-</td>
</tr>";
} else { } else {
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$linee = $row['linee'] ? htmlspecialchars($row['linee']) : '<span class="text-muted">Nessuna</span>'; $linee = $row['linee'] ? htmlspecialchars($row['linee']) : '<span class="text-muted">Nessuna</span>';
$qtyTot = number_format((float)$row['qty_totale'], 3, ',', '.');
echo "<tr> $isActive = (int)$row['is_active'] === 1;
$badge = $isActive
? "<span class='badge bg-success'>Attiva</span>"
: "<span class='badge bg-secondary'>Inattiva</span>";
$toggleText = $isActive ? "Disattiva" : "Attiva";
$toggleClass = $isActive ? "btn-outline-warning" : "btn-outline-success";
echo "<tr data-mescola-id='{$row['id']}'>
<td>{$row['id']}</td> <td>{$row['id']}</td>
<td>" . htmlspecialchars($row['nome']) . "</td>
<td>" . htmlspecialchars($row['nomeuscita']) . "</td> <td>" . htmlspecialchars($row['nomeuscita']) . "</td>
<td><span class='fw-semibold'>{$qtyTot}</span></td>
<td>{$linee}</td> <td>{$linee}</td>
<td>{$badge}</td>
<td> <td>
<button class='btn btn-sm btn-outline-primary associa-linee' <button class='btn btn-sm btn-outline-dark associa-fornitori'
data-id='{$row['id']}' data-id='{$row['id']}'
data-nome='" . htmlspecialchars($row['nome'], ENT_QUOTES) . "'> data-nomeuscita='" . htmlspecialchars($row['nomeuscita'], ENT_QUOTES) . "'>
⚙️ Associa Linee 🧾 Fornitori
</button>
<button class='btn btn-sm btn-outline-primary associa-linee'
data-id='{$row['id']}'>
⚙️ Linee
</button>
<button class='btn btn-sm {$toggleClass} toggle-active'
data-id='{$row['id']}'>
🔁 {$toggleText}
</button> </button>
<button class='btn btn-sm btn-outline-secondary edit-mescola' <button class='btn btn-sm btn-outline-secondary edit-mescola'
data-id='{$row['id']}' data-id='{$row['id']}'
data-nome='" . htmlspecialchars($row['nome'], ENT_QUOTES) . "'
data-nomeuscita='" . htmlspecialchars($row['nomeuscita'], ENT_QUOTES) . "'> data-nomeuscita='" . htmlspecialchars($row['nomeuscita'], ENT_QUOTES) . "'>
✏️ Modifica ✏️ Modifica
</button> </button>
@@ -182,8 +303,8 @@
<div class="modal-body"> <div class="modal-body">
<form id="addMescolaForm"> <form id="addMescolaForm">
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-semibold">Nome Mescola</label> <label class="form-label fw-semibold">Nome Mescola (interno)</label>
<input type="text" class="form-control" id="nomeMescola" required> <input type="text" class="form-control" id="nomeMescola" placeholder="opzionale / interno">
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -215,8 +336,8 @@
<input type="hidden" id="editIdMescola"> <input type="hidden" id="editIdMescola">
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-semibold">Nome Mescola</label> <label class="form-label fw-semibold">Nome Mescola (interno)</label>
<input type="text" class="form-control" id="editNomeMescola" required> <input type="text" class="form-control" id="editNomeMescola" placeholder="opzionale / interno">
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -261,11 +382,79 @@
</div> </div>
</div> </div>
<!-- MODALE ASSOCIA FORNITORI / LOTTI -->
<div class="modal fade" id="associaFornitoriModal" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">Fornitori / Lotti - <span id="afNomeUscita"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="afIdMescola">
<input type="hidden" id="afEditId">
<div class="row g-2 align-items-end mb-3">
<div class="col-md-3">
<label class="form-label fw-semibold">Fornitore</label>
<select class="form-select" id="afIdSupplier" style="width:100%;"></select>
</div>
<div class="col-md-3">
<label class="form-label fw-semibold">Nome mescola fornitore</label>
<input type="text" class="form-control" id="afSupplierMixName" placeholder="Nome specifico fornitore">
</div>
<div class="col-md-2">
<label class="form-label fw-semibold">Lotto</label>
<input type="text" class="form-control" id="afLotCode" placeholder="LOT-...">
</div>
<div class="col-md-2">
<label class="form-label fw-semibold">Scadenza</label>
<input type="date" class="form-control" id="afExpiryDate">
</div>
<div class="col-md-2">
<label class="form-label fw-semibold">Q.</label>
<input type="number" step="0.001" class="form-control" id="afQty" value="0">
</div>
<div class="col-md-12 text-end">
<button class="btn btn-add" id="afSaveBtn"> Aggiungi Riga</button>
<button class="btn btn-outline-secondary ms-2" id="afCancelEdit" type="button" style="display:none;">Annulla modifica</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped align-middle text-center" id="afTable">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Fornitore</th>
<th>Nome mescola fornitore</th>
<th>Lotto</th>
<th>Scadenza</th>
<th>Q.</th>
<th>Azioni</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?> <?php include('jsinclude.php'); ?>
<script> <script>
/* ----------------- DATATABLE ----------------- */ /* ----------------- DATATABLE ----------------- */
$(document).ready(function() { $(document).ready(function() {
// init dropdown from URL
const urlParams = new URLSearchParams(window.location.search);
const activeParam = urlParams.get('active') || '1';
$('#filterActive').val(activeParam);
// datatable
$('#tabellaMescole').DataTable({ $('#tabellaMescole').DataTable({
order: [ order: [
[0, 'desc'] [0, 'desc']
@@ -275,6 +464,39 @@
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json' url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json'
} }
}); });
// filter reload
$('#filterActive').on('change', function() {
const v = $(this).val();
const url = new URL(window.location.href);
url.searchParams.set('active', v);
window.location.href = url.toString();
});
});
/* -------- TOGGLE ACTIVE -------- */
$(document).on('click', '.toggle-active', function() {
const id = $(this).data('id');
fetch('toggle_mescola_active.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `id=${encodeURIComponent(id)}`
})
.then(r => r.json())
.then(data => {
if (data.success) {
location.reload();
} else {
Swal.fire({
icon: "error",
title: "Errore",
text: data.message || "Operazione non riuscita"
});
}
});
}); });
/* -------- AGGIUNTA MESCOLA -------- */ /* -------- AGGIUNTA MESCOLA -------- */
@@ -304,7 +526,7 @@
Swal.fire({ Swal.fire({
icon: "error", icon: "error",
title: "Errore", title: "Errore",
text: data.message text: data.message || "Errore salvataggio"
}); });
} }
}); });
@@ -313,8 +535,8 @@
/* -------- APERTURA MODALE EDIT -------- */ /* -------- APERTURA MODALE EDIT -------- */
$(document).on("click", ".edit-mescola", function() { $(document).on("click", ".edit-mescola", function() {
$("#editIdMescola").val($(this).data("id")); $("#editIdMescola").val($(this).data("id"));
$("#editNomeMescola").val($(this).data("nome"));
$("#editNomeUscita").val($(this).data("nomeuscita")); $("#editNomeUscita").val($(this).data("nomeuscita"));
$("#editNomeMescola").val("");
$("#editMescolaModal").modal("show"); $("#editMescolaModal").modal("show");
}); });
@@ -331,7 +553,7 @@
headers: { headers: {
"Content-Type": "application/x-www-form-urlencoded" "Content-Type": "application/x-www-form-urlencoded"
}, },
body: `id=${id}&nome=${encodeURIComponent(nome)}&nomeuscita=${encodeURIComponent(nomeuscita)}` body: `id=${encodeURIComponent(id)}&nome=${encodeURIComponent(nome)}&nomeuscita=${encodeURIComponent(nomeuscita)}`
}) })
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => {
@@ -346,7 +568,7 @@
Swal.fire({ Swal.fire({
icon: "error", icon: "error",
title: "Errore", title: "Errore",
text: data.message text: data.message || "Errore aggiornamento"
}); });
} }
}); });
@@ -354,11 +576,10 @@
/* -------- MODALE ASSOCIA LINEE -------- */ /* -------- MODALE ASSOCIA LINEE -------- */
$(document).on("click", ".associa-linee", function() { $(document).on("click", ".associa-linee", function() {
const idMescola = $(this).data("id"); const idMescola = $(this).data("id");
$("#idMescolaLinee").val(idMescola); $("#idMescolaLinee").val(idMescola);
fetch("get_linee_mescola.php?id=" + idMescola) fetch("get_linee_mescola.php?id=" + encodeURIComponent(idMescola))
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => {
const select = $("#lineeSelect"); const select = $("#lineeSelect");
@@ -407,11 +628,260 @@
Swal.fire({ Swal.fire({
icon: "error", icon: "error",
title: "Errore", title: "Errore",
text: data.message text: data.message || "Errore salvataggio"
}); });
} }
}); });
}); });
// ============================
// FORNITORI / LOTTI
// ============================
function afResetForm() {
$("#afEditId").val("");
$("#afIdSupplier").val("").trigger("change");
$("#afSupplierMixName").val("");
$("#afLotCode").val("");
$("#afExpiryDate").val("");
$("#afQty").val("0");
$("#afSaveBtn").text(" Aggiungi Riga");
$("#afCancelEdit").hide();
}
function afSetSupplierValue(supplierId) {
const sel = $("#afIdSupplier");
const normalizedId = String(supplierId || "").trim();
if (normalizedId === "") {
sel.val("").trigger("change");
return;
}
if (sel.find('option[value="' + normalizedId + '"]').length > 0) {
sel.val(normalizedId).trigger("change");
} else {
sel.val("").trigger("change");
}
}
function afLoadSuppliers(selectedId = "") {
return fetch("get_suppliers.php")
.then(r => r.json())
.then(data => {
const sel = $("#afIdSupplier");
if (sel.hasClass("select2-hidden-accessible")) {
sel.select2("destroy");
}
sel.empty();
sel.append(`<option value="">Seleziona...</option>`);
if (data.success && Array.isArray(data.rows)) {
data.rows.forEach(s => {
const value = String(s.idsupplier);
sel.append(`<option value="${value}">${s.supplier_name}</option>`);
});
}
sel.select2({
theme: "bootstrap-5",
width: "100%",
dropdownParent: $("#associaFornitoriModal")
});
if (selectedId !== "") {
sel.val(String(selectedId)).trigger("change");
} else {
sel.val("").trigger("change");
}
});
}
function afLoadRows(idMescola) {
fetch("get_mescola_supplier_lots.php?id=" + encodeURIComponent(idMescola))
.then(r => r.json())
.then(data => {
console.log("ROWS LOTTI:", data);
const tbody = $("#afTable tbody");
tbody.empty();
if (!data.success || !Array.isArray(data.rows) || data.rows.length === 0) {
tbody.append(`<tr>
<td class="text-muted">-</td>
<td class="text-muted">Nessuna associazione presente</td>
<td class="text-muted">-</td>
<td class="text-muted">-</td>
<td class="text-muted">-</td>
<td class="text-muted">-</td>
<td class="text-muted">-</td>
</tr>`);
return;
}
data.rows.forEach(row => {
const exp = row.expiry_date ? row.expiry_date : "";
const supplierId = String(row.idsupplier ?? row.id_supplier ?? row.supplier_id ?? row.supplierId ?? "");
tbody.append(`
<tr>
<td>${row.id}</td>
<td>${row.supplier_name}</td>
<td>${row.supplier_mix_name}</td>
<td>${row.lot_code ?? ""}</td>
<td>${exp}</td>
<td>${row.qty}</td>
<td>
<button type="button" class="btn btn-sm btn-outline-secondary af-edit"
data-id="${row.id}"
data-idsupplier="${supplierId}"
data-mix="${String(row.supplier_mix_name).replace(/"/g, '&quot;')}"
data-lot="${String(row.lot_code ?? '').replace(/"/g, '&quot;')}"
data-exp="${exp}"
data-qty="${row.qty}">
✏️
</button>
<button type="button" class="btn btn-sm btn-outline-danger af-del" data-id="${row.id}">🗑️</button>
</td>
</tr>
`);
});
});
}
// Open modal
$(document).on("click", ".associa-fornitori", function() {
const idMescola = $(this).data("id");
const nomeUscita = $(this).data("nomeuscita");
$("#afIdMescola").val(idMescola);
$("#afNomeUscita").text(nomeUscita);
afResetForm();
afLoadSuppliers("")
.then(() => {
$("#associaFornitoriModal").modal("show");
afLoadRows(idMescola);
});
});
// Edit row in modal
$(document).on("click", ".af-edit", function() {
const editId = $(this).attr("data-id");
const supplierId = $(this).attr("data-idsupplier");
const mix = $(this).attr("data-mix");
const lot = $(this).attr("data-lot");
const exp = $(this).attr("data-exp");
const qty = $(this).attr("data-qty");
$("#afEditId").val(editId);
afSetSupplierValue(supplierId);
$("#afSupplierMixName").val(mix);
$("#afLotCode").val(lot);
$("#afExpiryDate").val(exp);
$("#afQty").val(qty);
$("#afSaveBtn").text("💾 Salva Modifica");
$("#afCancelEdit").show();
});
$("#afCancelEdit").on("click", function() {
afResetForm();
});
// Save (insert/update)
$("#afSaveBtn").on("click", function(e) {
e.preventDefault();
const idmescola = $("#afIdMescola").val();
const editId = $("#afEditId").val();
const idsupplier = String($("#afIdSupplier").val() || "").trim();
const supplier_mix_name = $("#afSupplierMixName").val().trim();
const lot_code = $("#afLotCode").val().trim();
const expiry_date = $("#afExpiryDate").val();
const qty = $("#afQty").val();
if (!idsupplier || !supplier_mix_name) {
Swal.fire({
icon: "warning",
title: "Attenzione",
text: "Fornitore e Nome mescola fornitore sono obbligatori"
});
return;
}
const actionUrl = editId ? "update_mescola_supplier_lot.php" : "save_mescola_supplier_lot.php";
const body =
(editId ? `id=${encodeURIComponent(editId)}&` : ``) +
`idmescola=${encodeURIComponent(idmescola)}` +
`&idsupplier=${encodeURIComponent(idsupplier)}` +
`&supplier_mix_name=${encodeURIComponent(supplier_mix_name)}` +
`&lot_code=${encodeURIComponent(lot_code)}` +
`&expiry_date=${encodeURIComponent(expiry_date)}` +
`&qty=${encodeURIComponent(qty)}`;
fetch(actionUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body
})
.then(r => r.json())
.then(data => {
if (data.success) {
afResetForm();
afLoadRows(idmescola);
} else {
Swal.fire({
icon: "error",
title: "Errore",
text: data.message || "Operazione non riuscita"
});
}
});
});
// Delete
$(document).on("click", ".af-del", function() {
const id = $(this).data("id");
const idmescola = $("#afIdMescola").val();
Swal.fire({
title: "Eliminare la riga?",
icon: "warning",
showCancelButton: true,
confirmButtonText: "Sì, elimina",
cancelButtonText: "Annulla",
confirmButtonColor: "#d33"
}).then((res) => {
if (!res.isConfirmed) return;
fetch("delete_mescola_supplier_lot.php", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: `id=${encodeURIComponent(id)}`
})
.then(r => r.json())
.then(data => {
if (data.success) {
afLoadRows(idmescola);
} else {
Swal.fire({
icon: "error",
title: "Errore",
text: data.message || "Cancellazione non riuscita"
});
}
});
});
});
</script> </script>
</body> </body>
+390
View File
@@ -0,0 +1,390 @@
<?php include('include/headscript.php'); ?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>Dashboard Produzione - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<!-- Bootstrap + jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
body {
background: linear-gradient(135deg, #f3f6f8, #e8eef3);
font-family: 'Segoe UI', sans-serif;
color: #2b3e50;
}
.page-content {
padding: 2rem 1rem;
display: flex;
flex-direction: column;
align-items: center;
}
h3.dashboard-title {
text-align: center;
font-weight: 700;
color: #2b3e50;
margin-bottom: 2rem;
letter-spacing: 0.3px;
}
/* ===== STATISTICHE ===== */
.stats-row {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 15px;
margin-bottom: 20px;
width: 100%;
max-width: 900px;
}
.stat-card {
flex: 1 1 250px;
background: #fff;
border-radius: 16px;
padding: 20px 25px;
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
text-align: center;
transition: all 0.2s ease;
}
.stat-card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.15);
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #2b3e50;
}
.stat-label {
font-size: 1rem;
font-weight: 500;
color: #6b7a89;
}
.stat-prod {
background: linear-gradient(135deg, #cde5ff, #dff0ff);
}
.stat-mese {
background: linear-gradient(135deg, #d2f7d9, #e7ffea);
}
.stat-scarti {
background: linear-gradient(135deg, #ffe7cc, #fff3df);
}
/* ===== BOTTONI ===== */
.dashboard-grid {
display: grid;
grid-template-columns: repeat(3, minmax(200px, 1fr));
gap: 25px 35px;
width: 100%;
max-width: 900px;
justify-items: center;
margin-bottom: 20px;
}
.dashboard-grid-bottom {
display: grid;
grid-template-columns: repeat(3, minmax(200px, 1fr));
gap: 15px 35px;
width: 100%;
max-width: 900px;
justify-items: center;
margin-top: 10px;
}
.btn-tools {
background: linear-gradient(135deg, #9f7aea, #b794f4);
}
.dash-btn {
width: 100%;
max-width: 280px;
border: none;
border-radius: 16px;
padding: 24px 10px;
color: #2b3e50;
font-size: 1.3rem;
font-weight: 600;
background: #fff;
transition: all 0.2s ease;
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.dash-btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.15);
cursor: pointer;
}
.dash-icon {
font-size: 3.2rem;
margin-bottom: 10px;
line-height: 1;
}
/* Colori pastello */
.btn-inserisci {
background: linear-gradient(135deg, #a4c2f3ff, #c1d8ffff);
}
.btn-visualizza {
background: linear-gradient(135deg, #82f09eff, #aaecaaff);
}
.btn-statistiche {
background: linear-gradient(135deg, #ffe0a3ff, #fff1c1ff);
}
.btn-mescole {
background: linear-gradient(135deg, #ffc853ff, #fdd98bff);
}
.btn-matrici {
background: linear-gradient(135deg, #ff8585ff, #ff9d9dff);
}
.btn-linee {
background: linear-gradient(135deg, #b9e3ffff, #d7f1ffff);
}
.btn-programmazione {
background: linear-gradient(135deg, #7c7afaff, #7c7afaff);
}
.btn-status {
background: linear-gradient(135deg, #61ce5dff, #61ce5dff);
}
.btn-problem {
background-color: #ef4444 !important;
color: #ffffff !important;
border-radius: 12px;
}
.btn-problem:hover {
background-color: #dc2626 !important;
}
.btn-tools {
background: linear-gradient(135deg, #9f7aea, #b794f4);
}
/* 🔹 Nuovo bottone Employees */
.btn-employees {
background: linear-gradient(135deg, #a5b4fc, #c7d2fe);
}
/* --- Pulsanti grandi (default) --- */
.dash-btn-large {
padding: 18px 10px;
font-size: 1.3rem;
}
/* --- Pulsanti di servizio: più bassi --- */
.dash-btn-small {
padding: 9px 10px !important;
font-size: 1.05rem !important;
min-height: 80px;
}
@media (max-width: 768px) {
.stats-row {
flex-direction: column;
align-items: center;
}
.dashboard-grid,
.dashboard-grid-bottom {
grid-template-columns: 1fr;
gap: 15px;
}
.dash-btn {
font-size: 1.2rem;
padding: 30px 8px;
}
.dash-icon {
font-size: 2.6rem;
}
}
</style>
</head>
<body>
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<h3 class="dashboard-title">Dashboard Produzione</h3>
<!-- ===== STATISTICHE PRINCIPALI ===== -->
<div class="stats-row">
<?php
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Totale odierno
$stmt = $pdo->query("SELECT SUM(kgprod) AS totale_oggi FROM productiondata WHERE DATE(Data) = CURDATE()");
$totOggi = number_format($stmt->fetchColumn() ?? 0, 2, ',', '.');
// Totale mese
$stmt = $pdo->query("SELECT SUM(kgprod) AS totale_mese FROM productiondata WHERE MONTH(Data) = MONTH(CURDATE()) AND YEAR(Data) = YEAR(CURDATE())");
$totMese = number_format($stmt->fetchColumn() ?? 0, 2, ',', '.');
// Scarti medi %
$stmt = $pdo->query("SELECT (SUM(scarto)/NULLIF(SUM(kgprod),0))*100 AS perc_scarto FROM productiondata WHERE MONTH(Data) = MONTH(CURDATE()) AND YEAR(Data) = YEAR(CURDATE())");
$percScarto = number_format($stmt->fetchColumn() ?? 0, 2, ',', '.');
?>
<div class="stat-card stat-prod">
<div class="stat-value"><?= $totOggi ?> kg</div>
<div class="stat-label">Produzione odierna</div>
</div>
<div class="stat-card stat-mese">
<div class="stat-value"><?= $totMese ?> kg</div>
<div class="stat-label">Produzione mese corrente</div>
</div>
<div class="stat-card stat-scarti">
<div class="stat-value"><?= $percScarto ?>%</div>
<div class="stat-label">Scarto medio mensile</div>
</div>
</div>
<!-- ===== PRIMA RIGA ===== -->
<div class="dashboard-grid">
<button class="dash-btn dash-btn-large btn-programmazione" onclick="location.href='produzione_programmazione_drag.php'">
<div class="dash-icon">🗓️</div>
<div>Programmazione</div>
</button>
<button class="dash-btn dash-btn-large btn-status" onclick="location.href='production_line_view2.php'">
<div class="dash-icon">⚙️</div>
<div>Line View</div>
</button>
<button class="dash-btn dash-btn-large btn-statistiche" onclick="location.href='production_stats.php'">
<div class="dash-icon">📈</div>
<div>Statistiche</div>
</button>
<button class="dash-btn dash-btn-large btn-manager" onclick="location.href='manager_produzione.php'">
<div class="dash-icon">👔</div>
<div>Manager</div>
</button>
<button class="dash-btn dash-btn-large btn-manager-stats" onclick="location.href='manager_stats.php'">
<div class="dash-icon">📊</div>
<div>Manager Stats</div>
</button>
<button class="dash-btn dash-btn-large btn-linee" onclick="location.href='warehouse_dashboard.php'">
<div class="dash-icon">📦</div>
<div>Magazzino</div>
</button>
</div>
<!-- ===== SECONDA RIGA ===== -->
<div class="dashboard-grid">
<button class="dash-btn dash-btn-small btn-mescole" onclick="location.href='mescole.php'">
<div class="dash-icon">⚗️</div>
<div>Mescole</div>
</button>
<button class="dash-btn dash-btn-small btn-matrici" onclick="location.href='matrici.php'">
<div class="dash-icon">🧩</div>
<div>Elenco Profili</div>
</button>
<button class="dash-btn dash-btn-small btn-linee" onclick="location.href='linee.php'">
<div class="dash-icon">🏭</div>
<div>Linee di Produzione</div>
</button>
</div>
<!-- ===== TERZA RIGA ===== -->
<div class="dashboard-grid-bottom">
<button class="dash-btn dash-btn-small btn-inserisci btn-inserisci-status" onclick="location.href='production_status.php'">
<div class="dash-icon">📋</div>
<div>Status</div>
</button>
<button class="dash-btn dash-btn-small btn-problem" onclick="location.href='production_pause_reasons.php'">
<div class="dash-icon">🛑</div>
<div>Cause di Pausa</div>
</button>
<button class="dash-btn dash-btn-small btn-tools" onclick="location.href='production_tools.php'">
<div class="dash-icon">🛠️</div>
<div>Attrezzature</div>
</button>
</div>
<!-- 🔹 QUARTA RIGA: EMPLOYEES -->
<div class="dashboard-grid-bottom" style="margin-top: 20px;">
<button class="dash-btn dash-btn-small btn-skills" onclick="location.href='worksheets.php'">
<div class="dash-icon">🛠️</div>
<div>Fogli di lavoro</div>
</button>
<button class="dash-btn dash-btn-small btn-employees" onclick="location.href='employees.php'">
<div class="dash-icon">👥</div>
<div>Employees</div>
</button>
<button class="dash-btn dash-btn-small btn-skills" onclick="location.href='skills.php'">
<div class="dash-icon">🛠️</div>
<div>Skills</div>
</button>
</div>
<!-- 🔹 Quinta RIGA: EMPLOYEES -->
<div class="dashboard-grid-bottom" style="margin-top: 20px;">
<button class="dash-btn dash-btn-small btn-skills" onclick="location.href='packaging_items.php'">
<div class="dash-icon">🛠️</div>
<div>Imballaggi</div>
</button>
<button class="dash-btn dash-btn-small btn-skills" onclick="location.href='suppliers.php'">
<div class="dash-icon">🛠️</div>
<div>Suppliers</div>
</button>
<button class="dash-btn dash-btn-small btn-skills" onclick="location.href='lookup_values.php'">
<div class="dash-icon">🛠️</div>
<div>Setup</div>
</button>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<?php include('include/footer.php'); ?>
</div>
</body>
</html>
+688
View File
@@ -0,0 +1,688 @@
<?php include('include/headscript.php'); ?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>Imballaggi - Anagrafica & Stock - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<!-- jQuery + Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<!-- Select2 -->
<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-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.full.min.js"></script>
<style>
body {
font-size: 1.05rem;
background: #f8fafc;
}
.card {
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, .08);
}
.table thead {
background: #cfe3ff;
color: #1f2d3d;
}
.modal-content {
border-radius: 16px;
}
.btn-add {
background-color: #0d6efd;
color: #fff;
border-radius: 8px;
padding: 10px 20px;
font-weight: 500;
}
.btn-add:hover {
background-color: #0b5ed7;
}
#tabPackaging {
table-layout: fixed;
width: 100% !important;
}
#tabPackaging th,
#tabPackaging td {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
/* Column widths */
#tabPackaging th:nth-child(1),
#tabPackaging td:nth-child(1) {
width: 60px;
max-width: 60px;
}
#tabPackaging th:nth-child(2),
#tabPackaging td:nth-child(2) {
width: 170px;
max-width: 170px;
}
#tabPackaging th:nth-child(4),
#tabPackaging td:nth-child(4) {
width: 140px;
max-width: 140px;
}
#tabPackaging th:nth-child(5),
#tabPackaging td:nth-child(5) {
width: 110px;
max-width: 110px;
}
#tabPackaging th:nth-child(6),
#tabPackaging td:nth-child(6) {
width: 120px;
max-width: 120px;
}
#tabPackaging th:nth-child(7),
#tabPackaging td:nth-child(7) {
width: 420px;
max-width: 420px;
}
.back-dashboard {
background-color: #cfe3ff !important;
color: #1f2d3d !important;
border: 1px solid #bcd4f4 !important;
border-radius: 10px;
font-weight: 600;
font-size: 1rem;
padding: 10px 18px;
box-shadow: 0 3px 8px rgba(0, 0, 0, .1);
}
.back-dashboard:hover {
background-color: #b9d3ff !important;
transform: translateY(-2px);
}
.qty-badge {
font-weight: 700;
}
tr.inactive-item {
opacity: 0.65;
}
</style>
</head>
<body>
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card p-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Imballaggi - Anagrafica & Stock</h5>
<div class="d-flex gap-2">
<button class="btn back-dashboard" onclick="location.href='warehouse_dashboard.php'">↩️ Torna a Magazzino</button>
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addItemModal"> Nuovo Imballo</button>
</div>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center gap-3">
<h6 class="fw-semibold mb-0">Elenco</h6>
<select id="filterActive" class="form-select form-select-sm" style="width:220px;">
<option value="all">Tutti</option>
<option value="1" selected>Solo Attivi</option>
<option value="0">Solo Inattivi</option>
</select>
<select id="filterCategory" class="form-select form-select-sm" style="width:240px;">
<option value="all">Tutte le categorie</option>
<option value="PACKAGING_TYPE">Tipo Confezione</option>
<option value="BOX">Scatole</option>
<option value="PALLET">Pallet</option>
<option value="OTHER">Altro</option>
</select>
</div>
</div>
<?php
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$active = $_GET['active'] ?? '1';
if (!in_array($active, ['all', '0', '1'], true)) $active = '1';
$cat = $_GET['cat'] ?? 'all';
$allowedCats = ['all', 'PACKAGING_TYPE', 'BOX', 'PALLET', 'OTHER'];
if (!in_array($cat, $allowedCats, true)) $cat = 'all';
$where = [];
$params = [];
if ($active !== 'all') {
$where[] = "pi.is_active = ?";
$params[] = (int)$active;
}
if ($cat !== 'all') {
$where[] = "pi.category = ?";
$params[] = $cat;
}
$whereSql = $where ? ("WHERE " . implode(" AND ", $where)) : "";
// Sum qty in subquery to avoid duplicates
$sql = "
SELECT
pi.id,
pi.category,
pi.item_name,
pi.item_code,
pi.is_active,
IFNULL(q.qty_totale, 0) AS qty_totale
FROM packaging_items pi
LEFT JOIN (
SELECT idpackaging_item, SUM(qty) AS qty_totale
FROM packaging_stock_lots
GROUP BY idpackaging_item
) q ON q.idpackaging_item = pi.id
$whereSql
ORDER BY pi.category ASC, pi.item_name ASC, pi.id DESC
";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
function catLabel($c)
{
return match ($c) {
'PACKAGING_TYPE' => 'Tipo Confezione',
'BOX' => 'Scatola',
'PALLET' => 'Pallet',
default => $c
};
}
?>
<div class="table-responsive">
<table id="tabPackaging" class="table table-striped align-middle text-center" style="width:100%;">
<thead>
<tr>
<th>ID</th>
<th>Categoria</th>
<th>Nome</th>
<th>Codice</th>
<th>Q. Tot</th>
<th>Stato</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php
if (!$rows) {
echo "<tr>
<td class='text-muted'>-</td>
<td class='text-muted'>Nessun elemento</td>
<td class='text-muted'>-</td>
<td class='text-muted'>-</td>
<td class='text-muted'>-</td>
<td class='text-muted'>-</td>
<td class='text-muted'>-</td>
</tr>";
} else {
foreach ($rows as $r) {
$isActive = ((int)$r['is_active'] === 1);
$badge = $isActive
? "<span class='badge bg-success'>Attivo</span>"
: "<span class='badge bg-secondary'>Inattivo</span>";
$toggleText = $isActive ? "Disattiva" : "Attiva";
$toggleClass = $isActive ? "btn-outline-warning" : "btn-outline-success";
$qtyTot = number_format((float)$r['qty_totale'], 3, ',', '.');
$trClass = $isActive ? "" : " class='inactive-item'";
echo "<tr{$trClass} data-item-id='{$r['id']}'>
<td>{$r['id']}</td>
<td>" . htmlspecialchars(catLabel($r['category'])) . "</td>
<td>" . htmlspecialchars($r['item_name']) . "</td>
<td><span class='fw-semibold'>" . htmlspecialchars($r['item_code']) . "</span></td>
<td><span class='qty-badge'>{$qtyTot}</span></td>
<td>{$badge}</td>
<td>
<button class='btn btn-sm btn-outline-dark manage-stock'
data-id='{$r['id']}'
data-name='" . htmlspecialchars($r['item_name'], ENT_QUOTES) . "'>
📦 Stock
</button>
<button class='btn btn-sm {$toggleClass} toggle-item'
data-id='{$r['id']}'>
🔁 {$toggleText}
</button>
</td>
</tr>";
}
}
?>
</tbody>
</table>
</div>
<div class="text-muted small mt-2">
Q. Tot = somma di tutti i lotti/fornitori collegati allimballo.
</div>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<!-- MODALE: NUOVO IMBALLO -->
<div class="modal fade" id="addItemModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">Nuovo Imballo</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addItemForm">
<div class="mb-3">
<label class="form-label fw-semibold">Categoria</label>
<select class="form-select" id="newCategory" required>
<option value="PACKAGING_TYPE">Tipo Confezione</option>
<option value="BOX">Scatola</option>
<option value="PALLET">Pallet</option>
<option value="OTHER">Altro</option>
</select>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Nome</label>
<input type="text" class="form-control" id="newItemName" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Codice</label>
<input type="text" class="form-control" id="newItemCode" required>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add">💾 Salva</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- MODALE: STOCK FORNITORI/LOTTI -->
<div class="modal fade" id="stockModal" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">Stock - <span id="stockItemName"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="stockItemId">
<input type="hidden" id="stockEditId">
<div class="row g-2 align-items-end mb-3">
<div class="col-md-3">
<label class="form-label fw-semibold">Fornitore</label>
<select class="form-select" id="stockSupplier" style="width:100%;"></select>
</div>
<div class="col-md-3">
<label class="form-label fw-semibold">Lotto</label>
<input type="text" class="form-control" id="stockLot" placeholder="LOT-...">
</div>
<div class="col-md-2">
<label class="form-label fw-semibold">Scadenza</label>
<input type="date" class="form-control" id="stockExpiry">
</div>
<div class="col-md-2">
<label class="form-label fw-semibold">Q.</label>
<input type="number" step="0.001" class="form-control" id="stockQty" value="0">
</div>
<div class="col-md-2 text-end">
<button class="btn btn-add w-100" id="stockSaveBtn"> Aggiungi</button>
<button class="btn btn-outline-secondary w-100 mt-2" id="stockCancelEdit" type="button" style="display:none;">Annulla</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped align-middle text-center" id="stockTable">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Fornitore</th>
<th>Lotto</th>
<th>Scadenza</th>
<th>Q.</th>
<th>Azioni</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="text-muted small">
Suggerimento: per cambiare quantità clicca ✏️, modifica Q. e salva.
</div>
</div>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
let dt;
function refreshUrlParams() {
const url = new URL(window.location.href);
url.searchParams.set('active', $('#filterActive').val());
url.searchParams.set('cat', $('#filterCategory').val());
window.location.href = url.toString();
}
function loadSuppliersSelect($select, dropdownParent) {
return fetch("get_suppliers.php")
.then(r => r.json())
.then(data => {
$select.empty();
$select.append(`<option value="">Seleziona...</option>`);
if (data.success && Array.isArray(data.rows)) {
data.rows.forEach(s => $select.append(`<option value="${s.idsupplier}">${s.supplier_name}</option>`));
}
$select.select2({
theme: "bootstrap-5",
width: "100%",
dropdownParent: dropdownParent
});
});
}
function stockResetForm() {
$("#stockEditId").val("");
$("#stockSupplier").val("").trigger("change");
$("#stockLot").val("");
$("#stockExpiry").val("");
$("#stockQty").val("0");
$("#stockSaveBtn").text(" Aggiungi");
$("#stockCancelEdit").hide();
}
function stockLoadRows(itemId) {
fetch("get_packaging_stock_lots.php?id=" + encodeURIComponent(itemId))
.then(r => r.json())
.then(data => {
const tbody = $("#stockTable tbody");
tbody.empty();
if (!data.success || !Array.isArray(data.rows) || data.rows.length === 0) {
tbody.append(`<tr>
<td class="text-muted">-</td>
<td class="text-muted">Nessuna riga</td>
<td class="text-muted">-</td>
<td class="text-muted">-</td>
<td class="text-muted">-</td>
<td class="text-muted">-</td>
</tr>`);
return;
}
data.rows.forEach(row => {
const exp = row.expiry_date ? row.expiry_date : "";
tbody.append(`
<tr data-row-id="${row.id}">
<td>${row.id}</td>
<td>${row.supplier_name}</td>
<td>${row.lot_code ?? ""}</td>
<td>${exp || "-"}</td>
<td>${row.qty}</td>
<td>
<button class="btn btn-sm btn-outline-secondary stock-edit"
data-id="${row.id}"
data-idsupplier="${row.idsupplier}"
data-lot="${(row.lot_code ?? "").replace(/"/g,'&quot;')}"
data-exp="${exp}"
data-qty="${row.qty}">
✏️
</button>
<button class="btn btn-sm btn-outline-danger stock-del" data-id="${row.id}">🗑️</button>
</td>
</tr>
`);
});
});
}
$(document).ready(function() {
// Init filters from URL
const urlParams = new URLSearchParams(window.location.search);
$('#filterActive').val(urlParams.get('active') || '1');
$('#filterCategory').val(urlParams.get('cat') || 'all');
dt = $('#tabPackaging').DataTable({
order: [
[2, 'asc']
],
pageLength: 50,
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json'
}
});
$('#filterActive, #filterCategory').on('change', refreshUrlParams);
});
// Create new item
$("#addItemForm").on("submit", function(e) {
e.preventDefault();
const category = $("#newCategory").val();
const item_name = $("#newItemName").val().trim();
const item_code = $("#newItemCode").val().trim();
fetch("save_packaging_item.php", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: `category=${encodeURIComponent(category)}&item_name=${encodeURIComponent(item_name)}&item_code=${encodeURIComponent(item_code)}`
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: "success",
title: "Creato!"
}).then(() => location.reload());
} else {
Swal.fire({
icon: "error",
title: "Errore",
text: data.message || "Non salvato"
});
}
});
});
// Toggle item active
$(document).on("click", ".toggle-item", function() {
const id = $(this).data("id");
fetch("toggle_packaging_item_active.php", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: `id=${encodeURIComponent(id)}`
})
.then(r => r.json())
.then(data => {
if (data.success) location.reload();
else Swal.fire({
icon: "error",
title: "Errore",
text: data.message || "Operazione non riuscita"
});
});
});
// Open stock modal
$(document).on("click", ".manage-stock", function() {
const id = $(this).data("id");
const name = $(this).data("name");
$("#stockItemId").val(id);
$("#stockItemName").text(name);
$("#stockModal").modal("show");
stockResetForm();
loadSuppliersSelect($("#stockSupplier"), $("#stockModal")).then(() => stockLoadRows(id));
});
// Click edit stock row
$(document).on("click", ".stock-edit", function() {
$("#stockEditId").val($(this).data("id"));
$("#stockSupplier").val(String($(this).data("idsupplier"))).trigger("change");
$("#stockLot").val($(this).data("lot"));
$("#stockExpiry").val($(this).data("exp"));
$("#stockQty").val($(this).data("qty"));
$("#stockSaveBtn").text("💾 Salva");
$("#stockCancelEdit").show();
});
// Cancel edit
$("#stockCancelEdit").on("click", function() {
stockResetForm();
});
// Save stock (insert/update)
$("#stockSaveBtn").on("click", function(e) {
e.preventDefault();
const itemId = $("#stockItemId").val();
const editId = $("#stockEditId").val();
const idsupplier = $("#stockSupplier").val();
const lot = $("#stockLot").val().trim();
const expiry = $("#stockExpiry").val();
const qty = $("#stockQty").val();
if (!idsupplier) {
Swal.fire({
icon: "warning",
title: "Attenzione",
text: "Seleziona un fornitore"
});
return;
}
const url = editId ? "update_packaging_stock_lot.php" : "save_packaging_stock_lot.php";
const body =
(editId ? `id=${encodeURIComponent(editId)}&` : ``) +
`idpackaging_item=${encodeURIComponent(itemId)}` +
`&idsupplier=${encodeURIComponent(idsupplier)}` +
`&lot_code=${encodeURIComponent(lot)}` +
`&expiry_date=${encodeURIComponent(expiry)}` +
`&qty=${encodeURIComponent(qty)}`;
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body
})
.then(r => r.json())
.then(data => {
if (data.success) {
stockResetForm();
stockLoadRows(itemId);
// Refresh totals by reloading page (simple and safe)
// If you want live update without reload, we can do it.
// location.reload();
} else {
Swal.fire({
icon: "error",
title: "Errore",
text: data.message || "Operazione non riuscita"
});
}
});
});
// Delete stock row
$(document).on("click", ".stock-del", function() {
const id = $(this).data("id");
const itemId = $("#stockItemId").val();
Swal.fire({
title: "Eliminare la riga?",
icon: "warning",
showCancelButton: true,
confirmButtonText: "Sì, elimina",
cancelButtonText: "Annulla",
confirmButtonColor: "#d33"
}).then((res) => {
if (!res.isConfirmed) return;
fetch("delete_packaging_stock_lot.php", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: `id=${encodeURIComponent(id)}`
})
.then(r => r.json())
.then(data => {
if (data.success) {
stockLoadRows(itemId);
// location.reload();
} else {
Swal.fire({
icon: "error",
title: "Errore",
text: data.message || "Cancellazione non riuscita"
});
}
});
});
});
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

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