74 Commits

Author SHA1 Message Date
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
12244 changed files with 30040 additions and 1351018 deletions
+4 -2
View File
@@ -31,6 +31,8 @@ MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MANAGER_USER_ID=
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
@@ -55,5 +57,5 @@ AZURE_REDIRECT_URI=https://your-app.com/auth/azure/callback
AZURE_TENANT_ID=
MICROSOFT_CLIENT_ID=your_client_id_here
MICROSOFT_CLIENT_SECRET=your_client_secret_here
MICROSOFT_REDIRECT_URI="${APP_URL}/auth/microsoft/callback"
MICROSOFT_CLIENT_SECRET=your_client_secret_here
MICROSOFT_REDIRECT_URI="${APP_URL}/auth/microsoft/callback"
+5
View File
@@ -32,6 +32,8 @@ auth.json
# File XLSX temporanei importati
/public/userarea/imported_trf/*.xlsx
/public/userarea/xlstemplates/*.xlsx
/public/userarea/photos/matrici/allegati/
/public/userarea/photos/matrici/allegati/*
# Ignora cartelle di foto generate
/public/photostrf/
@@ -64,3 +66,6 @@ public/userarea/logsapi/commessaweb_customfields_763.json
public/userarea/logsapi/commessaweb_invia_762.json
public/userarea/logsapi/commessaweb_invia_763.json
public/userarea/logsapi/last_auth_url.txt
# User uploaded files
/public/userarea/files/
@@ -111,6 +111,14 @@ class LoginController extends Controller
return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('User')) {
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
+1
View File
@@ -44,6 +44,7 @@
"phpmailer/phpmailer": "^6.9",
"phpoffice/phpspreadsheet": "^4.1",
"proengsoft/laravel-jsvalidation": "^4.0.0",
"robmorgan/phinx": "^0.16.11",
"socialiteproviders/microsoft": "^4.7",
"spatie/laravel-query-builder": "^5.0",
"vanguardapp/activity-log": "^6.0",
Generated
+646 -2
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "9c4f1e3bc3ee2180211c055e70635aef",
"content-hash": "076e7721d08cfea8b06ce75dd8c6c576",
"packages": [
{
"name": "akaunting/laravel-setting",
@@ -251,6 +251,330 @@
],
"time": "2023-11-29T23:19:16+00:00"
},
{
"name": "cakephp/chronos",
"version": "3.5.0",
"source": {
"type": "git",
"url": "https://github.com/cakephp/chronos.git",
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/chronos/zipball/e6e777b534244911566face8a5dbdbd7f7bda5a6",
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/clock": "^1.0"
},
"provide": {
"psr/clock-implementation": "1.0"
},
"require-dev": {
"cakephp/cakephp-codesniffer": "^5.0",
"phpunit/phpunit": "^10.5.58 || ^11.5.3 || ^12.1.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Cake\\Chronos\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brian Nesbitt",
"email": "brian@nesbot.com",
"homepage": "http://nesbot.com"
},
{
"name": "The CakePHP Team",
"homepage": "https://cakephp.org"
}
],
"description": "A simple API extension for DateTime.",
"homepage": "https://cakephp.org",
"keywords": [
"date",
"datetime",
"time"
],
"support": {
"issues": "https://github.com/cakephp/chronos/issues",
"source": "https://github.com/cakephp/chronos"
},
"time": "2026-04-10T02:50:39+00:00"
},
{
"name": "cakephp/core",
"version": "5.3.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",
"version": "3.2.0",
@@ -2627,6 +2951,90 @@
],
"time": "2022-12-11T20:36:23+00:00"
},
{
"name": "league/container",
"version": "5.2.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/container.git",
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/container/zipball/58accbc032f0090a9bd08326f93062c5a658b2c5",
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5",
"shasum": ""
},
"require": {
"php": "^8.1",
"psr/container": "^2.0.2",
"psr/event-dispatcher": "^1.0"
},
"provide": {
"psr/container-implementation": "^1.0"
},
"replace": {
"orno/di": "~2.0"
},
"require-dev": {
"nette/php-generator": "^4.1",
"nikic/php-parser": "^5.0",
"phpstan/phpstan": "^2.1.11",
"phpunit/phpunit": "^10.5.45|^11.5.15|^12.0",
"roave/security-advisories": "dev-latest",
"scrutinizer/ocular": "^1.9",
"squizlabs/php_codesniffer": "^3.9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-1.x": "1.x-dev",
"dev-2.x": "2.x-dev",
"dev-3.x": "3.x-dev",
"dev-4.x": "4.x-dev",
"dev-5.x": "5.x-dev",
"dev-master": "5.x-dev"
}
},
"autoload": {
"psr-4": {
"League\\Container\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Phil Bennett",
"email": "mail@philbennett.co.uk",
"role": "Developer"
}
],
"description": "A fast and intuitive dependency injection container.",
"homepage": "https://github.com/thephpleague/container",
"keywords": [
"container",
"dependency",
"di",
"injection",
"league",
"provider",
"service"
],
"support": {
"issues": "https://github.com/thephpleague/container/issues",
"source": "https://github.com/thephpleague/container/tree/5.2.0"
},
"funding": [
{
"url": "https://github.com/philipobenito",
"type": "github"
}
],
"time": "2026-03-19T18:52:39+00:00"
},
{
"name": "league/flysystem",
"version": "3.28.0",
@@ -4980,6 +5388,93 @@
],
"time": "2024-04-27T21:32:50+00:00"
},
{
"name": "robmorgan/phinx",
"version": "0.16.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",
"version": "v4.8.1",
@@ -5312,6 +5807,85 @@
],
"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",
"version": "v7.1.3",
@@ -5768,6 +6342,76 @@
],
"time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/filesystem",
"version": "v7.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.8"
},
"require-dev": {
"symfony/process": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Filesystem\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/filesystem/tree/v7.4.11"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-11T16:38:44+00:00"
},
{
"name": "symfony/finder",
"version": "v7.1.3",
@@ -11355,6 +11999,6 @@
"php": "^8.2.0",
"ext-json": "*"
},
"platform-dev": [],
"platform-dev": {},
"plugin-api-version": "2.6.0"
}
@@ -0,0 +1,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();
}
}
+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,26 @@
<?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 DPI non valido.']);
exit;
}
try {
$stmt = $pdo->prepare("DELETE FROM employee_ppe 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,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,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,82 @@
<?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);
$itemName = trim($_POST['item_name'] ?? '');
$deliveryDate = trim($_POST['delivery_date'] ?? '');
$deliveredBy = trim($_POST['delivered_by'] ?? '');
$notes = trim($_POST['notes'] ?? '');
if ($employeeId <= 0) {
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
exit;
}
if ($itemName === '') {
echo json_encode(['success' => false, 'message' => 'Il nome del DPI è obbligatorio.']);
exit;
}
$deliveryDate = $deliveryDate === '' ? null : $deliveryDate;
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
$notes = $notes !== '' ? $notes : null;
try {
if ($id > 0) {
$stmt = $pdo->prepare("
UPDATE employee_ppe
SET item_name = :item_name,
delivery_date = :delivery_date,
delivered_by = :delivered_by,
notes = :notes,
updated_at = NOW()
WHERE id = :id AND employee_id = :eid
");
$stmt->execute([
'item_name' => $itemName,
'delivery_date' => $deliveryDate,
'delivered_by' => $deliveredBy,
'notes' => $notes,
'id' => $id,
'eid' => $employeeId,
]);
echo json_encode(['success' => true, 'id' => $id]);
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;
}
$stmt = $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())
");
$stmt->execute([
'employee_id' => $employeeId,
'item_name' => $itemName,
'delivery_date' => $deliveryDate,
'delivered_by' => $deliveredBy,
'notes' => $notes,
'created_by' => $currentUserId,
]);
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -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();
});
})();
@@ -8,14 +8,6 @@
<i class="bi bi-box-seam" style="font-size:1.8rem; color:#334155;"></i>
</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 -->
<button class="photo-btn"
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;">
<!-- 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]);
+13 -11
View File
@@ -7,16 +7,21 @@ ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL | E_STRICT);
// 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';
// Here we just check if user is not
// Here we just check if user is not
// logged in, and in that case we redirect
// the user to vanguard login page.
if (! Auth::check()) {
redirectTo('../../public/login');
// Cut everything at /userarea/ and append /login.
$scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
$basePath = substr($scriptName, 0, strpos($scriptName, '/userarea/'));
if ($basePath === false || $basePath === '') {
$basePath = '';
}
redirectTo($basePath . '/login');
}
$user = Auth::user();
@@ -35,8 +40,7 @@ $kindofrole = $user->present()->role_id;
//$iduserlogin="1";
//$nameuser="Claudio";
//$emailuser="info@claudiosironi.com";
?>
<?php
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
@@ -49,13 +53,11 @@ $_SESSION["emailuser"] = $emailuser;
$_SESSION["photouser"] = $avatar;
$photouser = $_SESSION["photouser"];
$photousername = basename($avatar);
?>
<?php //include files
//include files
require_once(__DIR__ . '/../../languages/en/general.php');
//include("generalsettings.php");
?>
require_once __DIR__ . '/permissions_helper.php';
?>
+342 -52
View File
@@ -6,101 +6,391 @@
<div>
<h4 class="logo-text"><?= htmlspecialchars('ZIBOGOMMA', ENT_QUOTES, 'UTF-8'); ?></h4>
</div>
<div class="toggle-icon ms-auto"><i class='bx bx-arrow-back'></i>
<div class="toggle-icon ms-auto">
<i class='bx bx-arrow-back'></i>
</div>
</div>
<!--navigation-->
<ul class="metismenu" id="menu">
<!-- user, admin, superuser menù -->
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('User')) || (Auth::user()->hasRole('Superuser'))) : ?>
<?php if (userCan('production.dashboard.view')) : ?>
<li>
<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 class="menu-title">Dashboard</div>
</a>
</li>
<?php endif; ?>
<?php
$canSeeProgramming =
userCan('production.programming.view')
|| userCan('templates.dashboard.view')
|| userCan('templates.create.view');
?>
<?php if ($canSeeProgramming) : ?>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class="bx bx-category"></i>
<div class="parent-icon">
<i class="bx bx-category"></i>
</div>
<div class="menu-title">Programmazione</div>
</a>
<ul>
<li> <a href="templates_dashboard.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?></a>
</li>
<li> <a href="insert_template_xls.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?></a>
</li>
<ul>
<?php if (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>
</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>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class="bx bx-category"></i>
<div class="parent-icon">
<i class="bx bx-category"></i>
</div>
<div class="menu-title">Funzioni</div>
</a>
<ul>
<li>
<a href="mescole.php"><i class='bx bx-radio-circle'></i>Mescole</a>
</li>
<li>
<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>
<?php if (userCan('masterdata.mescole.view')) : ?>
<li>
<a href="mescole.php">
<i class='bx bx-radio-circle'></i>Mescole
</a>
</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>
</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');
?>
<li class="menu-label">Others</li>
<?php if ($canSeeProduction) : ?>
<li>
<a href="https://helpdesk.cesoft.io" target="_blank">
<div class="parent-icon"><i class="bx bx-support"></i>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon">
<i class="bx bx-line-chart"></i>
</div>
<div class="menu-title">Support</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
endif; ?>
<!-- admin, superuser menù -->
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('Superuser'))) : ?>
$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
endif; ?>
<!-- admin menù -->
<?php if (Auth::user()->hasRole('Admin')) : ?>
$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>
<?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.job_roles.view')) : ?>
<li>
<a href="job_roles.php">
<i class='bx bx-radio-circle'></i>Mansioni
</a>
</li>
<?php endif; ?>
<?php if (userCan('hr.training_topics.view')) : ?>
<li>
<a href="training_topics.php">
<i class='bx bx-radio-circle'></i>Corsi di Formazione
</a>
</li>
<?php endif; ?>
<?php if (userCan('hr.trainings.view')) : ?>
<li>
<a href="trainings.php">
<i class='bx bx-radio-circle'></i>Storico Formazione
</a>
</li>
<li>
<a href="training_calendar.php">
<i class='bx bx-radio-circle'></i>Calendario 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>
</ul>
</li>
<?php endif; ?>
<li class="menu-label">Others</li>
<li>
<a href="https://helpdesk.cesoft.io" target="_blank">
<div class="parent-icon">
<i class="bx bx-support"></i>
</div>
<div class="menu-title">Support</div>
</a>
</li>
<?php if (userCan('users.manage')) : ?>
<li class="menu-label">Admin Menù</li>
<li>
<a href="../" target="_blank">
<div class="parent-icon"><i class="bx bx-support"></i>
<div class="parent-icon">
<i class="bx bx-user-circle"></i>
</div>
<div class="menu-title">User Management</div>
</a>
</li>
<!-- <li>
<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; ?>
<?php endif; ?>
</ul>
<!--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']);
}));
}
}
+26 -14
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>
<div class="topbar d-flex align-items-center">
<nav class="navbar navbar-expand gap-3">
@@ -5,17 +13,17 @@
</div>
<div class="search-bar d-lg-block d-none" data-bs-toggle="modal" data-bs-target="#SearchModal">
<!-- <a href="avascript:;" class="btn d-flex align-items-center"><i class='bx bx-search'></i>Search</a> -->
<!-- <a href="javascript:;" class="btn d-flex align-items-center"><i class='bx bx-search'></i>Search</a> -->
</div>
<div class="top-menu ms-auto">
<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">
<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>
</li>
<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>
<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>
@@ -60,13 +68,6 @@
</div>
</div>
</a>
</div>
<a href="javascript:;">
<div class="text-center msg-footer">
@@ -92,17 +93,28 @@
</div>
</a>
<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><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>
<div class="dropdown-divider mb-0"></div>
</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>
</ul>
</div>
</nav>
</div>
</header>
</header>
+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>
+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>
+39 -18
View File
@@ -11,7 +11,6 @@
<!-- jQuery e Bootstrap -->
<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>
<!-- DataTables -->
@@ -84,6 +83,10 @@
color: #128346;
}
.btn-action.params {
color: #6f42c1;
}
.btn-action:hover {
transform: scale(1.15);
}
@@ -114,7 +117,7 @@
</head>
<body>
<div class="wrapper">
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.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>
</div>
<!-- TABELLA -->
<!-- TABLE -->
<div class="table-responsive">
<table id="tabellaLinee" class="table table-striped align-middle text-center" style="width:100%;">
<thead>
@@ -156,32 +159,44 @@
$stmt = $pdo->query("SELECT * FROM production_lines ORDER BY line_number ASC");
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 {
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$badge = $row['status'] === 'active'
? "<span class='badge-active'>Attiva</span>"
: "<span class='badge-inactive'>Inattiva</span>";
$lineNameEsc = htmlspecialchars($row['name'], ENT_QUOTES, 'UTF-8');
echo "<tr>
<td>{$row['id']}</td>
<td>{$row['line_number']}</td>
<td>" . htmlspecialchars($row['name']) . "</td>
<td>{$lineNameEsc}</td>
<td>" . htmlspecialchars($row['model']) . "</td>
<td>" . htmlspecialchars($row['brand']) . "</td>
<td>
<div style='width:28px; height:28px; border-radius:6px; border:1px solid #999; background: {$row['color']}; margin:auto;'></div>
</td>
<td>{$badge}</td>
<td>
<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>
</tr>";
<button class='btn-action params' title='Parametri linea'
data-id='{$row['id']}'
data-name='{$lineNameEsc}'>
<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>
</tr>";
}
}
?>
@@ -196,7 +211,7 @@
<?php include('include/footer.php'); ?>
</div>
<!-- MODALE AGGIUNTA LINEA -->
<!-- ADD LINE MODAL -->
<div class="modal fade" id="addLineaModal" tabindex="-1" aria-labelledby="addLineaLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
@@ -250,7 +265,7 @@
}
});
// Aggiungi linea
// Add line
$("#addLineaForm").on("submit", function(e) {
e.preventDefault();
const formData = new FormData(this);
@@ -271,13 +286,19 @@
.catch(() => Swal.fire("Errore", "Impossibile contattare il server.", "error"));
});
// ✏️ Modifica
// ✏️ Edit line
$(document).on("click", ".edit", function() {
const id = $(this).data("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() {
const id = $(this).data("id");
Swal.fire({
@@ -304,7 +325,7 @@
});
});
// 🔘 Toggle stato
// 🔘 Toggle status
$(document).on("click", ".toggle", function() {
const id = $(this).data("id");
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
p.*,
m.nome AS matrice,
m.photo AS matrice_photo,
l.name AS linea,
c.nome AS cliente,
s.nome AS status_nome,
@@ -167,7 +168,7 @@ $rows_special = array_filter($rows, function ($r) {
</head>
<body>
<div class="wrapper toggled">
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
@@ -222,7 +223,20 @@ $rows_special = array_filter($rows, function ($r) {
data-seconds="<?= $sec ?>"
style="--rowcolor: <?= htmlspecialchars($r['line_color']) ?>;">
<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
$mescRaw = $r['mescole_list'] ?? '';
$mescArray = $mescRaw !== '' ? explode(' | ', $mescRaw) : [];
@@ -338,7 +352,20 @@ $rows_special = array_filter($rows, function ($r) {
data-status="<?= (int)$r['id_status'] ?>"
style="background-color: <?= htmlspecialchars($r['line_color']) ?>;">
<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
$mescRaw = $r['mescole_list'] ?? '';
$mescArray = $mescRaw !== '' ? explode(' | ', $mescRaw) : [];
@@ -565,6 +592,19 @@ $rows_special = array_filter($rows, function ($r) {
$("#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() {
$("#photoModal").hide();
});
@@ -610,16 +650,19 @@ $rows_special = array_filter($rows, function ($r) {
$("#imagePreviewModal").css("display", "flex");
});
$("#previewCloseX").on("click", function() {
// ❌ Chiudi preview con la X (delegato)
$(document).on("click", "#previewCloseX", function() {
$("#imagePreviewModal").hide();
});
$("#imagePreviewModal").on("click", function(e) {
// ❌ Chiudi cliccando fuori (delegato)
$(document).on("click", "#imagePreviewModal", function(e) {
if (e.target.id === "imagePreviewModal") {
$("#imagePreviewModal").hide();
}
});
// ===== MODALE MESCOLE (Multi) =====
$(document).on("click", ".showMescole", function(e) {
e.preventDefault();
File diff suppressed because it is too large Load Diff
+514 -44
View File
@@ -11,7 +11,6 @@
<!-- jQuery e Bootstrap -->
<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>
<!-- DataTables -->
@@ -79,11 +78,66 @@
text-align: center;
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>
</head>
<body>
<div class="wrapper">
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
@@ -99,7 +153,16 @@
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-semibold mb-0">Elenco Completo</h6>
<div class="d-flex align-items-center gap-3">
<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>
</div>
@@ -109,52 +172,110 @@
<thead>
<tr>
<th>ID</th>
<th>Nome Mescola</th>
<th>Nome Uscita</th>
<th>Q. Totale</th>
<th>Linee Associate</th>
<th>Stato</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$sql = "
SELECT m.id, m.nome, m.nomeuscita,
GROUP_CONCAT(pl.name SEPARATOR ', ') AS linee
FROM mescole m
LEFT JOIN mescole_lines ml ON m.id = ml.idmescola
LEFT JOIN production_lines pl ON ml.idlinea = pl.id
GROUP BY m.id
ORDER BY m.id DESC";
$stmt = $pdo->query($sql);
// 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 = "
SELECT
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
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 production_lines pl ON ml.idlinea = pl.id
$where
GROUP BY m.id
ORDER BY m.id DESC
";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
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 {
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$linee = $row['linee'] ? htmlspecialchars($row['linee']) : '<span class="text-muted">Nessuna</span>';
$qtyTot = number_format((float)$row['qty_totale'], 3, ',', '.');
echo "<tr>
<td>{$row['id']}</td>
<td>" . htmlspecialchars($row['nome']) . "</td>
<td>" . htmlspecialchars($row['nomeuscita']) . "</td>
<td>{$linee}</td>
<td>
<button class='btn btn-sm btn-outline-primary associa-linee'
data-id='{$row['id']}'
data-nome='" . htmlspecialchars($row['nome'], ENT_QUOTES) . "'>
⚙️ Associa Linee
</button>
$isActive = (int)$row['is_active'] === 1;
$badge = $isActive
? "<span class='badge bg-success'>Attiva</span>"
: "<span class='badge bg-secondary'>Inattiva</span>";
<button class='btn btn-sm btn-outline-secondary edit-mescola'
data-id='{$row['id']}'
data-nome='" . htmlspecialchars($row['nome'], ENT_QUOTES) . "'
data-nomeuscita='" . htmlspecialchars($row['nomeuscita'], ENT_QUOTES) . "'>
✏️ Modifica
</button>
</td>
</tr>";
$toggleText = $isActive ? "Disattiva" : "Attiva";
$toggleClass = $isActive ? "btn-outline-warning" : "btn-outline-success";
echo "<tr data-mescola-id='{$row['id']}'>
<td>{$row['id']}</td>
<td>" . htmlspecialchars($row['nomeuscita']) . "</td>
<td><span class='fw-semibold'>{$qtyTot}</span></td>
<td>{$linee}</td>
<td>{$badge}</td>
<td>
<button class='btn btn-sm btn-outline-dark associa-fornitori'
data-id='{$row['id']}'
data-nomeuscita='" . htmlspecialchars($row['nomeuscita'], ENT_QUOTES) . "'>
🧾 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 class='btn btn-sm btn-outline-secondary edit-mescola'
data-id='{$row['id']}'
data-nomeuscita='" . htmlspecialchars($row['nomeuscita'], ENT_QUOTES) . "'>
✏️ Modifica
</button>
</td>
</tr>";
}
}
?>
@@ -182,8 +303,8 @@
<div class="modal-body">
<form id="addMescolaForm">
<div class="mb-3">
<label class="form-label fw-semibold">Nome Mescola</label>
<input type="text" class="form-control" id="nomeMescola" required>
<label class="form-label fw-semibold">Nome Mescola (interno)</label>
<input type="text" class="form-control" id="nomeMescola" placeholder="opzionale / interno">
</div>
<div class="mb-3">
@@ -215,8 +336,8 @@
<input type="hidden" id="editIdMescola">
<div class="mb-3">
<label class="form-label fw-semibold">Nome Mescola</label>
<input type="text" class="form-control" id="editNomeMescola" required>
<label class="form-label fw-semibold">Nome Mescola (interno)</label>
<input type="text" class="form-control" id="editNomeMescola" placeholder="opzionale / interno">
</div>
<div class="mb-3">
@@ -261,11 +382,79 @@
</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'); ?>
<script>
/* ----------------- DATATABLE ----------------- */
$(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({
order: [
[0, 'desc']
@@ -275,6 +464,39 @@
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 -------- */
@@ -304,7 +526,7 @@
Swal.fire({
icon: "error",
title: "Errore",
text: data.message
text: data.message || "Errore salvataggio"
});
}
});
@@ -313,8 +535,8 @@
/* -------- APERTURA MODALE EDIT -------- */
$(document).on("click", ".edit-mescola", function() {
$("#editIdMescola").val($(this).data("id"));
$("#editNomeMescola").val($(this).data("nome"));
$("#editNomeUscita").val($(this).data("nomeuscita"));
$("#editNomeMescola").val("");
$("#editMescolaModal").modal("show");
});
@@ -331,7 +553,7 @@
headers: {
"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(data => {
@@ -346,7 +568,7 @@
Swal.fire({
icon: "error",
title: "Errore",
text: data.message
text: data.message || "Errore aggiornamento"
});
}
});
@@ -354,11 +576,10 @@
/* -------- MODALE ASSOCIA LINEE -------- */
$(document).on("click", ".associa-linee", function() {
const idMescola = $(this).data("id");
$("#idMescolaLinee").val(idMescola);
fetch("get_linee_mescola.php?id=" + idMescola)
fetch("get_linee_mescola.php?id=" + encodeURIComponent(idMescola))
.then(r => r.json())
.then(data => {
const select = $("#lineeSelect");
@@ -407,11 +628,260 @@
Swal.fire({
icon: "error",
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>
</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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 898 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 861 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

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