21 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
11058 changed files with 10924 additions and 1351411 deletions
+2
View File
@@ -31,6 +31,8 @@ MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_ENCRYPTION=null MAIL_ENCRYPTION=null
MANAGER_USER_ID=
PUSHER_APP_ID= PUSHER_APP_ID=
PUSHER_APP_KEY= PUSHER_APP_KEY=
PUSHER_APP_SECRET= PUSHER_APP_SECRET=
+3
View File
@@ -66,3 +66,6 @@ public/userarea/logsapi/commessaweb_customfields_763.json
public/userarea/logsapi/commessaweb_invia_762.json public/userarea/logsapi/commessaweb_invia_762.json
public/userarea/logsapi/commessaweb_invia_763.json public/userarea/logsapi/commessaweb_invia_763.json
public/userarea/logsapi/last_auth_url.txt public/userarea/logsapi/last_auth_url.txt
# User uploaded files
/public/userarea/files/
@@ -111,6 +111,14 @@ class LoginController extends Controller
return redirect()->to('userarea/production_dashboard.php'); return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('User')) { } elseif ($user->hasRole('User')) {
return redirect()->to('userarea/production_dashboard.php'); return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('HR')) {
return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('SuperUser')) {
return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('Management')) {
return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('Quality')) {
return redirect()->to('userarea/production_dashboard.php');
} }
// Se il ruolo non è specificato, reindirizza alla home predefinita // Se il ruolo non è specificato, reindirizza alla home predefinita
+1
View File
@@ -44,6 +44,7 @@
"phpmailer/phpmailer": "^6.9", "phpmailer/phpmailer": "^6.9",
"phpoffice/phpspreadsheet": "^4.1", "phpoffice/phpspreadsheet": "^4.1",
"proengsoft/laravel-jsvalidation": "^4.0.0", "proengsoft/laravel-jsvalidation": "^4.0.0",
"robmorgan/phinx": "^0.16.11",
"socialiteproviders/microsoft": "^4.7", "socialiteproviders/microsoft": "^4.7",
"spatie/laravel-query-builder": "^5.0", "spatie/laravel-query-builder": "^5.0",
"vanguardapp/activity-log": "^6.0", "vanguardapp/activity-log": "^6.0",
Generated
+646 -2
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "9c4f1e3bc3ee2180211c055e70635aef", "content-hash": "076e7721d08cfea8b06ce75dd8c6c576",
"packages": [ "packages": [
{ {
"name": "akaunting/laravel-setting", "name": "akaunting/laravel-setting",
@@ -251,6 +251,330 @@
], ],
"time": "2023-11-29T23:19:16+00:00" "time": "2023-11-29T23:19:16+00:00"
}, },
{
"name": "cakephp/chronos",
"version": "3.5.0",
"source": {
"type": "git",
"url": "https://github.com/cakephp/chronos.git",
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/chronos/zipball/e6e777b534244911566face8a5dbdbd7f7bda5a6",
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/clock": "^1.0"
},
"provide": {
"psr/clock-implementation": "1.0"
},
"require-dev": {
"cakephp/cakephp-codesniffer": "^5.0",
"phpunit/phpunit": "^10.5.58 || ^11.5.3 || ^12.1.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Cake\\Chronos\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brian Nesbitt",
"email": "brian@nesbot.com",
"homepage": "http://nesbot.com"
},
{
"name": "The CakePHP Team",
"homepage": "https://cakephp.org"
}
],
"description": "A simple API extension for DateTime.",
"homepage": "https://cakephp.org",
"keywords": [
"date",
"datetime",
"time"
],
"support": {
"issues": "https://github.com/cakephp/chronos/issues",
"source": "https://github.com/cakephp/chronos"
},
"time": "2026-04-10T02:50:39+00:00"
},
{
"name": "cakephp/core",
"version": "5.3.5",
"source": {
"type": "git",
"url": "https://github.com/cakephp/core.git",
"reference": "eb012517900ed288f580aa3487e9a09f28ea85f9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/core/zipball/eb012517900ed288f580aa3487e9a09f28ea85f9",
"reference": "eb012517900ed288f580aa3487e9a09f28ea85f9",
"shasum": ""
},
"require": {
"cakephp/utility": "^5.3.0",
"league/container": "^5.1",
"php": ">=8.2",
"psr/container": "^1.1 || ^2.0"
},
"provide": {
"psr/container-implementation": "^2.0"
},
"suggest": {
"cakephp/cache": "To use Configure::store() and restore().",
"cakephp/event": "To use PluginApplicationInterface or plugin applications.",
"league/container": "To use Container and ServiceProvider classes"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"files": [
"functions.php"
],
"psr-4": {
"Cake\\Core\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/core/graphs/contributors"
}
],
"description": "CakePHP Framework Core classes",
"homepage": "https://cakephp.org",
"keywords": [
"cakephp",
"core",
"framework"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/core"
},
"time": "2026-03-31T06:25:23+00:00"
},
{
"name": "cakephp/database",
"version": "5.3.5",
"source": {
"type": "git",
"url": "https://github.com/cakephp/database.git",
"reference": "cf94dcb57c54a1a308fd866b038cd6995910e36e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/database/zipball/cf94dcb57c54a1a308fd866b038cd6995910e36e",
"reference": "cf94dcb57c54a1a308fd866b038cd6995910e36e",
"shasum": ""
},
"require": {
"cakephp/chronos": "^3.3",
"cakephp/core": "^5.3.0",
"cakephp/datasource": "^5.3.0",
"php": ">=8.2",
"psr/log": "^3.0"
},
"require-dev": {
"cakephp/i18n": "^5.3.0",
"cakephp/log": "^5.3.0"
},
"suggest": {
"cakephp/i18n": "If you are using locale-aware datetime formats.",
"cakephp/log": "If you want to use query logging without providing a logger yourself."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"psr-4": {
"Cake\\Database\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/database/graphs/contributors"
}
],
"description": "Flexible and powerful Database abstraction library with a familiar PDO-like API",
"homepage": "https://cakephp.org",
"keywords": [
"abstraction",
"cakephp",
"database",
"database abstraction",
"pdo"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/database"
},
"time": "2026-03-31T06:25:23+00:00"
},
{
"name": "cakephp/datasource",
"version": "5.3.5",
"source": {
"type": "git",
"url": "https://github.com/cakephp/datasource.git",
"reference": "512464eb27b19316b515ec338089b83822c9ab5a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/datasource/zipball/512464eb27b19316b515ec338089b83822c9ab5a",
"reference": "512464eb27b19316b515ec338089b83822c9ab5a",
"shasum": ""
},
"require": {
"cakephp/core": "^5.3.0",
"php": ">=8.2",
"psr/simple-cache": "^2.0 || ^3.0"
},
"require-dev": {
"cakephp/cache": "^5.3.0",
"cakephp/collection": "^5.3.0",
"cakephp/utility": "^5.3.0"
},
"suggest": {
"cakephp/cache": "If you decide to use Query caching.",
"cakephp/collection": "If you decide to use ResultSetInterface.",
"cakephp/utility": "If you decide to use EntityTrait."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"psr-4": {
"Cake\\Datasource\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/datasource/graphs/contributors"
}
],
"description": "Provides connection managing and traits for Entities and Queries that can be reused for different datastores",
"homepage": "https://cakephp.org",
"keywords": [
"cakephp",
"connection management",
"datasource",
"entity",
"query"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/datasource"
},
"time": "2026-04-04T08:08:42+00:00"
},
{
"name": "cakephp/utility",
"version": "5.3.5",
"source": {
"type": "git",
"url": "https://github.com/cakephp/utility.git",
"reference": "4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/utility/zipball/4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99",
"reference": "4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99",
"shasum": ""
},
"require": {
"cakephp/core": "^5.3.0",
"php": ">=8.2"
},
"suggest": {
"ext-intl": "To use Text::transliterate() or Text::slug()",
"lib-ICU": "To use Text::transliterate() or Text::slug()"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Cake\\Utility\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/utility/graphs/contributors"
}
],
"description": "CakePHP Utility classes such as Inflector, String, Hash, and Security",
"homepage": "https://cakephp.org",
"keywords": [
"cakephp",
"hash",
"inflector",
"security",
"string",
"utility"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/utility"
},
"time": "2026-03-09T09:38:36+00:00"
},
{ {
"name": "carbonphp/carbon-doctrine-types", "name": "carbonphp/carbon-doctrine-types",
"version": "3.2.0", "version": "3.2.0",
@@ -2627,6 +2951,90 @@
], ],
"time": "2022-12-11T20:36:23+00:00" "time": "2022-12-11T20:36:23+00:00"
}, },
{
"name": "league/container",
"version": "5.2.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/container.git",
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/container/zipball/58accbc032f0090a9bd08326f93062c5a658b2c5",
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5",
"shasum": ""
},
"require": {
"php": "^8.1",
"psr/container": "^2.0.2",
"psr/event-dispatcher": "^1.0"
},
"provide": {
"psr/container-implementation": "^1.0"
},
"replace": {
"orno/di": "~2.0"
},
"require-dev": {
"nette/php-generator": "^4.1",
"nikic/php-parser": "^5.0",
"phpstan/phpstan": "^2.1.11",
"phpunit/phpunit": "^10.5.45|^11.5.15|^12.0",
"roave/security-advisories": "dev-latest",
"scrutinizer/ocular": "^1.9",
"squizlabs/php_codesniffer": "^3.9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-1.x": "1.x-dev",
"dev-2.x": "2.x-dev",
"dev-3.x": "3.x-dev",
"dev-4.x": "4.x-dev",
"dev-5.x": "5.x-dev",
"dev-master": "5.x-dev"
}
},
"autoload": {
"psr-4": {
"League\\Container\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Phil Bennett",
"email": "mail@philbennett.co.uk",
"role": "Developer"
}
],
"description": "A fast and intuitive dependency injection container.",
"homepage": "https://github.com/thephpleague/container",
"keywords": [
"container",
"dependency",
"di",
"injection",
"league",
"provider",
"service"
],
"support": {
"issues": "https://github.com/thephpleague/container/issues",
"source": "https://github.com/thephpleague/container/tree/5.2.0"
},
"funding": [
{
"url": "https://github.com/philipobenito",
"type": "github"
}
],
"time": "2026-03-19T18:52:39+00:00"
},
{ {
"name": "league/flysystem", "name": "league/flysystem",
"version": "3.28.0", "version": "3.28.0",
@@ -4980,6 +5388,93 @@
], ],
"time": "2024-04-27T21:32:50+00:00" "time": "2024-04-27T21:32:50+00:00"
}, },
{
"name": "robmorgan/phinx",
"version": "0.16.11",
"source": {
"type": "git",
"url": "https://github.com/cakephp/phinx.git",
"reference": "a03014fea316ba021fc0776982e5bed2d10228d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/phinx/zipball/a03014fea316ba021fc0776982e5bed2d10228d4",
"reference": "a03014fea316ba021fc0776982e5bed2d10228d4",
"shasum": ""
},
"require": {
"cakephp/database": "^5.0.2",
"composer-runtime-api": "^2.0",
"php-64bit": ">=8.1",
"psr/container": "^1.1|^2.0",
"symfony/config": "^4.0|^5.0|^6.0|^7.0|^8.0",
"symfony/console": "^6.0|^7.0|^8.0"
},
"require-dev": {
"cakephp/cakephp-codesniffer": "^5.0",
"cakephp/i18n": "^5.0",
"ext-json": "*",
"ext-pdo": "*",
"phpunit/phpunit": "^10.5",
"symfony/yaml": "^4.0|^5.0|^6.0|^7.0|^8.0"
},
"suggest": {
"ext-json": "Install if using JSON configuration format",
"ext-pdo": "PDO extension is needed",
"symfony/yaml": "Install if using YAML configuration format"
},
"bin": [
"bin/phinx"
],
"type": "library",
"autoload": {
"psr-4": {
"Phinx\\": "src/Phinx/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Rob Morgan",
"email": "robbym@gmail.com",
"homepage": "https://robmorgan.id.au",
"role": "Lead Developer"
},
{
"name": "Woody Gilk",
"email": "woody.gilk@gmail.com",
"homepage": "https://shadowhand.me",
"role": "Developer"
},
{
"name": "Richard Quadling",
"email": "rquadling@gmail.com",
"role": "Developer"
},
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/phinx/graphs/contributors",
"role": "Developer"
}
],
"description": "Phinx makes it ridiculously easy to manage the database migrations for your PHP app.",
"homepage": "https://phinx.org",
"keywords": [
"database",
"database migrations",
"db",
"migrations",
"phinx"
],
"support": {
"issues": "https://github.com/cakephp/phinx/issues",
"source": "https://github.com/cakephp/phinx/tree/0.16.11"
},
"time": "2026-03-15T00:04:32+00:00"
},
{ {
"name": "socialiteproviders/manager", "name": "socialiteproviders/manager",
"version": "v4.8.1", "version": "v4.8.1",
@@ -5312,6 +5807,85 @@
], ],
"time": "2024-05-31T14:57:53+00:00" "time": "2024-05-31T14:57:53+00:00"
}, },
{
"name": "symfony/config",
"version": "v7.4.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
"reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57",
"reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/filesystem": "^7.1|^8.0",
"symfony/polyfill-ctype": "~1.8"
},
"conflict": {
"symfony/finder": "<6.4",
"symfony/service-contracts": "<2.5"
},
"require-dev": {
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
"symfony/finder": "^6.4|^7.0|^8.0",
"symfony/messenger": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/yaml": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Config\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/config/tree/v7.4.10"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-03T14:20:49+00:00"
},
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v7.1.3", "version": "v7.1.3",
@@ -5768,6 +6342,76 @@
], ],
"time": "2024-04-18T09:32:20+00:00" "time": "2024-04-18T09:32:20+00:00"
}, },
{
"name": "symfony/filesystem",
"version": "v7.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.8"
},
"require-dev": {
"symfony/process": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Filesystem\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/filesystem/tree/v7.4.11"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-11T16:38:44+00:00"
},
{ {
"name": "symfony/finder", "name": "symfony/finder",
"version": "v7.1.3", "version": "v7.1.3",
@@ -11355,6 +11999,6 @@
"php": "^8.2.0", "php": "^8.2.0",
"ext-json": "*" "ext-json": "*"
}, },
"platform-dev": [], "platform-dev": {},
"plugin-api-version": "2.6.0" "plugin-api-version": "2.6.0"
} }
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class BaselineExistingDatabase extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
// Baseline migration.
// Existing database structure starts being tracked from this point.
}
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreatePhinxTestTable extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$table = $this->table('phinx_test_table');
$table
->addColumn('name', 'string', [
'limit' => 100,
'null' => false,
])
->addColumn('created_at', 'timestamp', [
'default' => 'CURRENT_TIMESTAMP',
'null' => false,
])
->create();
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddFunctionsToScadDeadlines extends AbstractMigration
{
public function change(): void
{
$this->table('scad_functions', [
'id' => false,
'primary_key' => ['id'],
'collation' => 'utf8mb4_unicode_ci',
'encoding' => 'utf8mb4',
])
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('name', 'string', [
'limit' => 255,
'null' => false,
])
->addColumn('description', 'text', [
'null' => true,
])
->addColumn('status', 'string', [
'limit' => 20,
'null' => false,
'default' => 'active',
])
->addColumn('created_at', 'timestamp', [
'null' => false,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => false,
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['name'], [
'unique' => true,
'name' => 'uniq_scad_functions_name',
])
->create();
$this->table('scad_deadlines')
->addColumn('function_id', 'integer', [
'signed' => false,
'null' => true,
'after' => 'subject_id',
])
->addIndex(['function_id'], [
'name' => 'idx_scad_deadlines_function_id',
])
->addForeignKey('function_id', 'scad_functions', 'id', [
'delete' => 'SET_NULL',
'update' => 'CASCADE',
'constraint' => 'fk_scad_deadlines_function',
])
->update();
}
}
+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,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>';
}
+1 -2
View File
@@ -256,7 +256,6 @@ $departments = $stmtDepartments->fetchAll(PDO::FETCH_ASSOC);
<!-- jQuery and Bootstrap --> <!-- jQuery and Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables --> <!-- DataTables -->
@@ -367,7 +366,7 @@ $departments = $stmtDepartments->fetchAll(PDO::FETCH_ASSOC);
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
File diff suppressed because it is too large Load Diff
+163 -64
View File
@@ -17,12 +17,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
try { try {
if ($action === 'add') { if ($action === 'add') {
// Codice originale per add
$employee_code = trim($_POST['employee_code'] ?? ''); $employee_code = trim($_POST['employee_code'] ?? '');
$first_name = trim($_POST['first_name'] ?? ''); $first_name = trim($_POST['first_name'] ?? '');
$last_name = trim($_POST['last_name'] ?? ''); $last_name = trim($_POST['last_name'] ?? '');
$address = trim($_POST['address'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$email = trim($_POST['email'] ?? '');
$department_id = $_POST['department_id'] !== '' ? (int)$_POST['department_id'] : null; $department_id = $_POST['department_id'] !== '' ? (int)$_POST['department_id'] : null;
$position = trim($_POST['position'] ?? ''); $job_role_id = ($_POST['job_role_id'] ?? '') !== '' ? (int)$_POST['job_role_id'] : null;
$hire_date = trim($_POST['hire_date'] ?? ''); $hire_date = trim($_POST['hire_date'] ?? '');
$status = trim($_POST['status'] ?? 'active'); $status = trim($_POST['status'] ?? 'active');
$auth_user_id = $_POST['auth_user_id'] !== '' ? (int)$_POST['auth_user_id'] : null; $auth_user_id = $_POST['auth_user_id'] !== '' ? (int)$_POST['auth_user_id'] : null;
@@ -35,23 +37,31 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
]); ]);
exit; exit;
} }
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo json_encode(['success' => false, 'message' => 'Email non valida.']);
exit;
}
if (!in_array($status, ['active', 'inactive', 'suspended'], true)) { if (!in_array($status, ['active', 'inactive', 'suspended'], true)) {
$status = 'active'; $status = 'active';
} }
$sql = "INSERT INTO employees $sql = "INSERT INTO employees
(auth_user_id, employee_code, first_name, last_name, department_id, position, hire_date, status, created_at, updated_at) (auth_user_id, employee_code, first_name, last_name, address, phone, email,
department_id, job_role_id, hire_date, status, created_at, updated_at)
VALUES VALUES
(:auth_user_id, :employee_code, :first_name, :last_name, :department_id, :position, :hire_date, :status, NOW(), NOW())"; (:auth_user_id, :employee_code, :first_name, :last_name, :address, :phone, :email,
:department_id, :job_role_id, :hire_date, :status, NOW(), NOW())";
$stmt = $pdo->prepare($sql); $stmt = $pdo->prepare($sql);
$stmt->execute([ $stmt->execute([
'auth_user_id' => $auth_user_id, 'auth_user_id' => $auth_user_id,
'employee_code' => $employee_code !== '' ? $employee_code : null, 'employee_code' => $employee_code !== '' ? $employee_code : null,
'first_name' => $first_name, 'first_name' => $first_name,
'last_name' => $last_name, 'last_name' => $last_name,
'address' => $address !== '' ? $address : null,
'phone' => $phone !== '' ? $phone : null,
'email' => $email !== '' ? $email : null,
'department_id' => $department_id, 'department_id' => $department_id,
'position' => $position !== '' ? $position : null, 'job_role_id' => $job_role_id,
'hire_date' => $hire_date !== '' ? $hire_date : null, 'hire_date' => $hire_date !== '' ? $hire_date : null,
'status' => $status 'status' => $status
]); ]);
@@ -74,13 +84,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
} }
if ($action === 'edit') { if ($action === 'edit') {
// Codice originale per edit
$id = (int)($_POST['id'] ?? 0); $id = (int)($_POST['id'] ?? 0);
$employee_code = trim($_POST['employee_code'] ?? ''); $employee_code = trim($_POST['employee_code'] ?? '');
$first_name = trim($_POST['first_name'] ?? ''); $first_name = trim($_POST['first_name'] ?? '');
$last_name = trim($_POST['last_name'] ?? ''); $last_name = trim($_POST['last_name'] ?? '');
$address = trim($_POST['address'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$email = trim($_POST['email'] ?? '');
$department_id = $_POST['department_id'] !== '' ? (int)$_POST['department_id'] : null; $department_id = $_POST['department_id'] !== '' ? (int)$_POST['department_id'] : null;
$position = trim($_POST['position'] ?? ''); $job_role_id = ($_POST['job_role_id'] ?? '') !== '' ? (int)$_POST['job_role_id'] : null;
$hire_date = trim($_POST['hire_date'] ?? ''); $hire_date = trim($_POST['hire_date'] ?? '');
$status = trim($_POST['status'] ?? 'active'); $status = trim($_POST['status'] ?? 'active');
$auth_user_id = $_POST['auth_user_id'] !== '' ? (int)$_POST['auth_user_id'] : null; $auth_user_id = $_POST['auth_user_id'] !== '' ? (int)$_POST['auth_user_id'] : null;
@@ -98,7 +110,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
]); ]);
exit; exit;
} }
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo json_encode(['success' => false, 'message' => 'Email non valida.']);
exit;
}
if (!in_array($status, ['active', 'inactive', 'suspended'], true)) { if (!in_array($status, ['active', 'inactive', 'suspended'], true)) {
$status = 'active'; $status = 'active';
} }
@@ -108,8 +123,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
employee_code = :employee_code, employee_code = :employee_code,
first_name = :first_name, first_name = :first_name,
last_name = :last_name, last_name = :last_name,
address = :address,
phone = :phone,
email = :email,
department_id = :department_id, department_id = :department_id,
position = :position, job_role_id = :job_role_id,
hire_date = :hire_date, hire_date = :hire_date,
status = :status, status = :status,
updated_at = NOW() updated_at = NOW()
@@ -120,8 +138,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
'employee_code' => $employee_code !== '' ? $employee_code : null, 'employee_code' => $employee_code !== '' ? $employee_code : null,
'first_name' => $first_name, 'first_name' => $first_name,
'last_name' => $last_name, 'last_name' => $last_name,
'address' => $address !== '' ? $address : null,
'phone' => $phone !== '' ? $phone : null,
'email' => $email !== '' ? $email : null,
'department_id' => $department_id, 'department_id' => $department_id,
'position' => $position !== '' ? $position : null, 'job_role_id' => $job_role_id,
'hire_date' => $hire_date !== '' ? $hire_date : null, 'hire_date' => $hire_date !== '' ? $hire_date : null,
'status' => $status, 'status' => $status,
'id' => $id 'id' => $id
@@ -223,6 +244,7 @@ $sql = "
SELECT e.*, SELECT e.*,
d.name AS department_name, d.name AS department_name,
d.color AS department_color, d.color AS department_color,
jr.name AS job_role_name,
au.email AS user_email, au.email AS user_email,
au.role_id AS user_role_id, au.role_id AS user_role_id,
ar.display_name AS role_display_name, ar.display_name AS role_display_name,
@@ -230,6 +252,7 @@ $sql = "
CONCAT(COALESCE(au.first_name, ''), ' ', COALESCE(au.last_name, '')) AS user_fullname CONCAT(COALESCE(au.first_name, ''), ' ', COALESCE(au.last_name, '')) AS user_fullname
FROM employees e FROM employees e
LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN job_roles jr ON jr.id = e.job_role_id
LEFT JOIN auth_users au ON e.auth_user_id = au.id LEFT JOIN auth_users au ON e.auth_user_id = au.id
LEFT JOIN auth_roles ar ON ar.id = au.role_id LEFT JOIN auth_roles ar ON ar.id = au.role_id
ORDER BY e.id DESC ORDER BY e.id DESC
@@ -237,6 +260,11 @@ $sql = "
$stmtEmployees = $pdo->query($sql); $stmtEmployees = $pdo->query($sql);
$employees = $stmtEmployees->fetchAll(PDO::FETCH_ASSOC); $employees = $stmtEmployees->fetchAll(PDO::FETCH_ASSOC);
// Job roles for the dropdown
$jobRoles = $pdo->query("
SELECT id, name FROM job_roles WHERE is_active = 1 ORDER BY sort_order, name
")->fetchAll(PDO::FETCH_ASSOC);
// Users list for select // Users list for select
$sqlUsers = " $sqlUsers = "
SELECT id, SELECT id,
@@ -297,7 +325,6 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
<!-- jQuery e Bootstrap --> <!-- jQuery e Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables --> <!-- DataTables -->
@@ -415,7 +442,7 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
@@ -448,14 +475,15 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>ID</th>
<th>Code</th> <th>Codice</th>
<th>Name</th> <th>Nome</th>
<th>Department</th> <th>Contatti</th>
<th>Position</th> <th>Reparto</th>
<th>Hire Date</th> <th>Mansione</th>
<th>Status</th> <th>Data Assunzione</th>
<th>Linked User</th> <th>Stato</th>
<th>Actions</th> <th>Utente collegato</th>
<th>Azioni</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -484,7 +512,24 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
<tr> <tr>
<td><?= (int)$row['id'] ?></td> <td><?= (int)$row['id'] ?></td>
<td><?= htmlspecialchars($row['employee_code'] ?? '') ?></td> <td><?= htmlspecialchars($row['employee_code'] ?? '') ?></td>
<td><?= htmlspecialchars($fullName) ?></td> <td>
<a href="employee-profile.php?id=<?= (int)$row['id'] ?>" class="fw-semibold text-decoration-none">
<?= htmlspecialchars($fullName) ?>
</a>
</td>
<td class="text-start">
<?php if (!empty($row['email'])): ?>
<a href="mailto:<?= htmlspecialchars($row['email'], ENT_QUOTES) ?>" class="text-decoration-none small">
✉️ <?= htmlspecialchars($row['email']) ?>
</a><br>
<?php endif; ?>
<?php if (!empty($row['phone'])): ?>
<a href="tel:<?= htmlspecialchars($row['phone'], ENT_QUOTES) ?>" class="text-decoration-none small">
📞 <?= htmlspecialchars($row['phone']) ?>
</a>
<?php endif; ?>
<?php if (empty($row['email']) && empty($row['phone'])): ?>-<?php endif; ?>
</td>
<td> <td>
<?php if (!empty($row['department_name'])): ?> <?php if (!empty($row['department_name'])): ?>
<span class="department-badge" style="background-color: <?= htmlspecialchars($row['department_color'] ?? '#6c757d', ENT_QUOTES) ?>;"> <span class="department-badge" style="background-color: <?= htmlspecialchars($row['department_color'] ?? '#6c757d', ENT_QUOTES) ?>;">
@@ -494,7 +539,7 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
- -
<?php endif; ?> <?php endif; ?>
</td> </td>
<td><?= htmlspecialchars($row['position'] ?? '') ?></td> <td><?= !empty($row['job_role_name']) ? htmlspecialchars($row['job_role_name']) : '-' ?></td>
<td><?= $hireDate ?></td> <td><?= $hireDate ?></td>
<td> <td>
<span class="badge-status <?= $statusClass ?>"> <span class="badge-status <?= $statusClass ?>">
@@ -510,7 +555,10 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
data-first_name="<?= htmlspecialchars($row['first_name'] ?? '', ENT_QUOTES) ?>" data-first_name="<?= htmlspecialchars($row['first_name'] ?? '', ENT_QUOTES) ?>"
data-last_name="<?= htmlspecialchars($row['last_name'] ?? '', ENT_QUOTES) ?>" data-last_name="<?= htmlspecialchars($row['last_name'] ?? '', ENT_QUOTES) ?>"
data-department_id="<?= $row['department_id'] !== null ? (int)$row['department_id'] : '' ?>" data-department_id="<?= $row['department_id'] !== null ? (int)$row['department_id'] : '' ?>"
data-position="<?= htmlspecialchars($row['position'] ?? '', ENT_QUOTES) ?>" data-job_role_id="<?= $row['job_role_id'] !== null ? (int)$row['job_role_id'] : '' ?>"
data-address="<?= htmlspecialchars($row['address'] ?? '', ENT_QUOTES) ?>"
data-phone="<?= htmlspecialchars($row['phone'] ?? '', ENT_QUOTES) ?>"
data-email="<?= htmlspecialchars($row['email'] ?? '', ENT_QUOTES) ?>"
data-hire_date="<?= htmlspecialchars($row['hire_date'] ?? '', ENT_QUOTES) ?>" data-hire_date="<?= htmlspecialchars($row['hire_date'] ?? '', ENT_QUOTES) ?>"
data-status="<?= htmlspecialchars($status, ENT_QUOTES) ?>" data-status="<?= htmlspecialchars($status, ENT_QUOTES) ?>"
data-auth_user_id="<?= $row['auth_user_id'] !== null ? (int)$row['auth_user_id'] : '' ?>" data-auth_user_id="<?= $row['auth_user_id'] !== null ? (int)$row['auth_user_id'] : '' ?>"
@@ -560,26 +608,42 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
<div class="modal-body"> <div class="modal-body">
<form id="addEmployeeForm"> <form id="addEmployeeForm">
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-semibold">Employee Code</label> <label class="form-label fw-semibold">Codice Dipendente</label>
<input type="text" class="form-control" id="addEmployeeCode" name="employee_code" placeholder="Optional"> <input type="text" class="form-control" id="addEmployeeCode" name="employee_code" placeholder="Opzionale">
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">First Name</label> <label class="form-label fw-semibold">Nome</label>
<input type="text" class="form-control" id="addFirstName" name="first_name" required> <input type="text" class="form-control" id="addFirstName" name="first_name" required>
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Last Name</label> <label class="form-label fw-semibold">Cognome</label>
<input type="text" class="form-control" id="addLastName" name="last_name" required> <input type="text" class="form-control" id="addLastName" name="last_name" required>
</div> </div>
</div> </div>
<div class="mb-3">
<label class="form-label fw-semibold">Indirizzo</label>
<input type="text" class="form-control" id="addAddress" name="address" placeholder="Via, città, CAP">
</div>
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Department</label> <label class="form-label fw-semibold">Telefono</label>
<input type="tel" class="form-control" id="addPhone" name="phone">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Email</label>
<input type="email" class="form-control" id="addEmail" name="email">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Reparto</label>
<select class="form-select" id="addDepartmentId" name="department_id" style="width:100%;"> <select class="form-select" id="addDepartmentId" name="department_id" style="width:100%;">
<option value="">-- Select Department --</option> <option value="">-- Nessuno --</option>
<?php foreach ($departments as $d): ?> <?php foreach ($departments as $d): ?>
<option value="<?= (int)$d['id'] ?>"> <option value="<?= (int)$d['id'] ?>">
<?= htmlspecialchars($d['name']) ?> <?= htmlspecialchars($d['name']) ?>
@@ -589,30 +653,35 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
</select> </select>
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Position</label> <label class="form-label fw-semibold">Mansione</label>
<input type="text" class="form-control" id="addPosition" name="position" placeholder="e.g. Line Operator"> <select class="form-select" id="addJobRoleId" name="job_role_id" style="width:100%;">
<option value="">-- Nessuna --</option>
<?php foreach ($jobRoles as $jr): ?>
<option value="<?= (int)$jr['id'] ?>"><?= htmlspecialchars($jr['name']) ?></option>
<?php endforeach; ?>
</select>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Hire Date</label> <label class="form-label fw-semibold">Data Assunzione</label>
<input type="date" class="form-control" id="addHireDate" name="hire_date"> <input type="date" class="form-control" id="addHireDate" name="hire_date">
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Status</label> <label class="form-label fw-semibold">Stato</label>
<select class="form-select" id="addStatus" name="status"> <select class="form-select" id="addStatus" name="status">
<option value="active" selected>Active</option> <option value="active" selected>Attivo</option>
<option value="inactive">Inactive</option> <option value="inactive">Cessato</option>
<option value="suspended">Suspended</option> <option value="suspended">Sospeso</option>
</select> </select>
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-semibold">Linked User (auth_users)</label> <label class="form-label fw-semibold">Utente collegato (account login)</label>
<select class="form-select" id="addAuthUserId" name="auth_user_id" style="width:100%;"> <select class="form-select" id="addAuthUserId" name="auth_user_id" style="width:100%;">
<option value="">-- None --</option> <option value="">-- Nessuno --</option>
<?php foreach ($users as $u): ?> <?php foreach ($users as $u): ?>
<option value="<?= (int)$u['id'] ?>" data-role_id="<?= (int)$u['role_id'] ?>"> <option value="<?= (int)$u['id'] ?>" data-role_id="<?= (int)$u['role_id'] ?>">
<?= htmlspecialchars($u['label']) ?> <?= htmlspecialchars($u['label']) ?>
@@ -622,16 +691,16 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
</div> </div>
<div class="mb-3 d-none" id="addRoleWrapper"> <div class="mb-3 d-none" id="addRoleWrapper">
<label class="form-label fw-semibold">User Role</label> <label class="form-label fw-semibold">Ruolo di accesso</label>
<select class="form-select" id="addRoleId" name="role_id" style="width:100%;"> <select class="form-select" id="addRoleId" name="role_id" style="width:100%;">
<option value="">-- Select Role --</option> <option value="">-- Seleziona ruolo --</option>
<?php foreach ($roles as $r): ?> <?php foreach ($roles as $r): ?>
<option value="<?= (int)$r['id'] ?>"> <option value="<?= (int)$r['id'] ?>">
<?= htmlspecialchars($r['display_name'] ?: $r['name']) ?> <?= htmlspecialchars($r['display_name'] ?: $r['name']) ?>
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<small class="text-muted">Visible only when an auth user is linked.</small> <small class="text-muted">Visibile solo quando è collegato un utente di sistema.</small>
</div> </div>
<div class="text-center"> <div class="text-center">
@@ -658,26 +727,42 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
<input type="hidden" id="editEmployeeId"> <input type="hidden" id="editEmployeeId">
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-semibold">Employee Code</label> <label class="form-label fw-semibold">Codice Dipendente</label>
<input type="text" class="form-control" id="editEmployeeCode" name="employee_code" placeholder="Optional"> <input type="text" class="form-control" id="editEmployeeCode" name="employee_code" placeholder="Opzionale">
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">First Name</label> <label class="form-label fw-semibold">Nome</label>
<input type="text" class="form-control" id="editFirstName" name="first_name" required> <input type="text" class="form-control" id="editFirstName" name="first_name" required>
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Last Name</label> <label class="form-label fw-semibold">Cognome</label>
<input type="text" class="form-control" id="editLastName" name="last_name" required> <input type="text" class="form-control" id="editLastName" name="last_name" required>
</div> </div>
</div> </div>
<div class="mb-3">
<label class="form-label fw-semibold">Indirizzo</label>
<input type="text" class="form-control" id="editAddress" name="address">
</div>
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Department</label> <label class="form-label fw-semibold">Telefono</label>
<input type="tel" class="form-control" id="editPhone" name="phone">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Email</label>
<input type="email" class="form-control" id="editEmail" name="email">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Reparto</label>
<select class="form-select" id="editDepartmentId" name="department_id" style="width:100%;"> <select class="form-select" id="editDepartmentId" name="department_id" style="width:100%;">
<option value="">-- Select Department --</option> <option value="">-- Nessuno --</option>
<?php foreach ($departments as $d): ?> <?php foreach ($departments as $d): ?>
<option value="<?= (int)$d['id'] ?>"> <option value="<?= (int)$d['id'] ?>">
<?= htmlspecialchars($d['name']) ?> <?= htmlspecialchars($d['name']) ?>
@@ -687,30 +772,35 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
</select> </select>
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Position</label> <label class="form-label fw-semibold">Mansione</label>
<input type="text" class="form-control" id="editPosition" name="position"> <select class="form-select" id="editJobRoleId" name="job_role_id" style="width:100%;">
<option value="">-- Nessuna --</option>
<?php foreach ($jobRoles as $jr): ?>
<option value="<?= (int)$jr['id'] ?>"><?= htmlspecialchars($jr['name']) ?></option>
<?php endforeach; ?>
</select>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Hire Date</label> <label class="form-label fw-semibold">Data Assunzione</label>
<input type="date" class="form-control" id="editHireDate" name="hire_date"> <input type="date" class="form-control" id="editHireDate" name="hire_date">
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Status</label> <label class="form-label fw-semibold">Stato</label>
<select class="form-select" id="editStatus" name="status"> <select class="form-select" id="editStatus" name="status">
<option value="active">Active</option> <option value="active">Attivo</option>
<option value="inactive">Inactive</option> <option value="inactive">Cessato</option>
<option value="suspended">Suspended</option> <option value="suspended">Sospeso</option>
</select> </select>
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-semibold">Linked User (auth_users)</label> <label class="form-label fw-semibold">Utente collegato (account login)</label>
<select class="form-select" id="editAuthUserId" name="auth_user_id" style="width:100%;"> <select class="form-select" id="editAuthUserId" name="auth_user_id" style="width:100%;">
<option value="">-- None --</option> <option value="">-- Nessuno --</option>
<?php foreach ($users as $u): ?> <?php foreach ($users as $u): ?>
<option value="<?= (int)$u['id'] ?>" data-role_id="<?= (int)$u['role_id'] ?>"> <option value="<?= (int)$u['id'] ?>" data-role_id="<?= (int)$u['role_id'] ?>">
<?= htmlspecialchars($u['label']) ?> <?= htmlspecialchars($u['label']) ?>
@@ -720,16 +810,16 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
</div> </div>
<div class="mb-3 d-none" id="editRoleWrapper"> <div class="mb-3 d-none" id="editRoleWrapper">
<label class="form-label fw-semibold">User Role</label> <label class="form-label fw-semibold">Ruolo di accesso</label>
<select class="form-select" id="editRoleId" name="role_id" style="width:100%;"> <select class="form-select" id="editRoleId" name="role_id" style="width:100%;">
<option value="">-- Select Role --</option> <option value="">-- Seleziona ruolo --</option>
<?php foreach ($roles as $r): ?> <?php foreach ($roles as $r): ?>
<option value="<?= (int)$r['id'] ?>"> <option value="<?= (int)$r['id'] ?>">
<?= htmlspecialchars($r['display_name'] ?: $r['name']) ?> <?= htmlspecialchars($r['display_name'] ?: $r['name']) ?>
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<small class="text-muted">Visible only when an auth user is linked.</small> <small class="text-muted">Visibile solo quando è collegato un utente di sistema.</small>
</div> </div>
<div class="text-center"> <div class="text-center">
@@ -784,7 +874,7 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
// Select2 on user selects // Select2 on user selects
$('#addAuthUserId, #editAuthUserId, #addDepartmentId, #editDepartmentId, #addRoleId, #editRoleId').select2({ $('#addAuthUserId, #editAuthUserId, #addDepartmentId, #editDepartmentId, #addRoleId, #editRoleId, #addJobRoleId, #editJobRoleId').select2({
theme: 'bootstrap-5', theme: 'bootstrap-5',
width: '100%' width: '100%'
}); });
@@ -834,8 +924,11 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
payload.append('employee_code', $("#addEmployeeCode").val().trim()); payload.append('employee_code', $("#addEmployeeCode").val().trim());
payload.append('first_name', $("#addFirstName").val().trim()); payload.append('first_name', $("#addFirstName").val().trim());
payload.append('last_name', $("#addLastName").val().trim()); payload.append('last_name', $("#addLastName").val().trim());
payload.append('address', $("#addAddress").val().trim());
payload.append('phone', $("#addPhone").val().trim());
payload.append('email', $("#addEmail").val().trim());
payload.append('department_id', $("#addDepartmentId").val() || ''); payload.append('department_id', $("#addDepartmentId").val() || '');
payload.append('position', $("#addPosition").val().trim()); payload.append('job_role_id', $("#addJobRoleId").val() || '');
payload.append('hire_date', $("#addHireDate").val()); payload.append('hire_date', $("#addHireDate").val());
payload.append('status', $("#addStatus").val()); payload.append('status', $("#addStatus").val());
payload.append('auth_user_id', $("#addAuthUserId").val() || ''); payload.append('auth_user_id', $("#addAuthUserId").val() || '');
@@ -884,7 +977,10 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
$("#editFirstName").val(btn.data("first_name")); $("#editFirstName").val(btn.data("first_name"));
$("#editLastName").val(btn.data("last_name")); $("#editLastName").val(btn.data("last_name"));
$("#editDepartmentId").val(btn.data("department_id") ? String(btn.data("department_id")) : '').trigger('change'); $("#editDepartmentId").val(btn.data("department_id") ? String(btn.data("department_id")) : '').trigger('change');
$("#editPosition").val(btn.data("position")); $("#editJobRoleId").val(btn.data("job_role_id") ? String(btn.data("job_role_id")) : '').trigger('change');
$("#editAddress").val(btn.data("address"));
$("#editPhone").val(btn.data("phone"));
$("#editEmail").val(btn.data("email"));
$("#editHireDate").val(btn.data("hire_date")); $("#editHireDate").val(btn.data("hire_date"));
$("#editStatus").val(btn.data("status")); $("#editStatus").val(btn.data("status"));
@@ -916,8 +1012,11 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
payload.append('employee_code', $("#editEmployeeCode").val().trim()); payload.append('employee_code', $("#editEmployeeCode").val().trim());
payload.append('first_name', $("#editFirstName").val().trim()); payload.append('first_name', $("#editFirstName").val().trim());
payload.append('last_name', $("#editLastName").val().trim()); payload.append('last_name', $("#editLastName").val().trim());
payload.append('address', $("#editAddress").val().trim());
payload.append('phone', $("#editPhone").val().trim());
payload.append('email', $("#editEmail").val().trim());
payload.append('department_id', $("#editDepartmentId").val() || ''); payload.append('department_id', $("#editDepartmentId").val() || '');
payload.append('position', $("#editPosition").val().trim()); payload.append('job_role_id', $("#editJobRoleId").val() || '');
payload.append('hire_date', $("#editHireDate").val()); payload.append('hire_date', $("#editHireDate").val());
payload.append('status', $("#editStatus").val()); payload.append('status', $("#editStatus").val());
payload.append('auth_user_id', $("#editAuthUserId").val() || ''); payload.append('auth_user_id', $("#editAuthUserId").val() || '');
+3 -6
View File
@@ -40,8 +40,7 @@ $kindofrole = $user->present()->role_id;
//$iduserlogin="1"; //$iduserlogin="1";
//$nameuser="Claudio"; //$nameuser="Claudio";
//$emailuser="info@claudiosironi.com"; //$emailuser="info@claudiosironi.com";
?>
<?php
if (session_status() == PHP_SESSION_NONE) { if (session_status() == PHP_SESSION_NONE) {
session_start(); session_start();
} }
@@ -54,13 +53,11 @@ $_SESSION["emailuser"] = $emailuser;
$_SESSION["photouser"] = $avatar; $_SESSION["photouser"] = $avatar;
$photouser = $_SESSION["photouser"]; $photouser = $_SESSION["photouser"];
$photousername = basename($avatar); $photousername = basename($avatar);
?>
//include files
<?php //include files
require_once(__DIR__ . '/../../languages/en/general.php'); require_once(__DIR__ . '/../../languages/en/general.php');
//include("generalsettings.php"); //include("generalsettings.php");
require_once __DIR__ . '/permissions_helper.php';
?> ?>
+324 -50
View File
@@ -6,117 +6,391 @@
<div> <div>
<h4 class="logo-text"><?= htmlspecialchars('ZIBOGOMMA', ENT_QUOTES, 'UTF-8'); ?></h4> <h4 class="logo-text"><?= htmlspecialchars('ZIBOGOMMA', ENT_QUOTES, 'UTF-8'); ?></h4>
</div> </div>
<div class="toggle-icon ms-auto"><i class='bx bx-arrow-back'></i> <div class="toggle-icon ms-auto">
<i class='bx bx-arrow-back'></i>
</div> </div>
</div> </div>
<!--navigation--> <!--navigation-->
<ul class="metismenu" id="menu"> <ul class="metismenu" id="menu">
<!-- user, admin, superuser menù -->
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('User')) || (Auth::user()->hasRole('Superuser'))) : ?> <?php if (userCan('production.dashboard.view')) : ?>
<li> <li>
<a href="production_dashboard.php"> <a href="production_dashboard.php">
<div class="parent-icon"><i class="bx bx-home-alt"></i> <div class="parent-icon">
<i class="bx bx-home-alt"></i>
</div> </div>
<div class="menu-title">Dashboard</div> <div class="menu-title">Dashboard</div>
</a> </a>
</li> </li>
<?php endif; ?>
<?php
$canSeeProgramming =
userCan('production.programming.view')
|| userCan('templates.dashboard.view')
|| userCan('templates.create.view');
?>
<?php if ($canSeeProgramming) : ?>
<li> <li>
<a href="javascript:;" class="has-arrow"> <a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class="bx bx-category"></i> <div class="parent-icon">
<i class="bx bx-category"></i>
</div> </div>
<div class="menu-title">Programmazione</div> <div class="menu-title">Programmazione</div>
</a> </a>
<ul>
<li> <a href="templates_dashboard.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?></a>
</li>
<li> <a href="insert_template_xls.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?></a>
</li>
<ul>
<?php if (userCan('templates.dashboard.view')) : ?>
<li>
<a href="templates_dashboard.php">
<i class='bx bx-radio-circle'></i>
<?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?>
</a>
</li>
<?php endif; ?>
<?php if (userCan('templates.create.view')) : ?>
<li>
<a href="insert_template_xls.php">
<i class='bx bx-radio-circle'></i>
<?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?>
</a>
</li>
<?php endif; ?>
<?php if (userCan('production.programming.view')) : ?>
<li>
<a href="produzione_programmazione_drag.php">
<i class='bx bx-radio-circle'></i>
Programmazione Produzione
</a>
</li>
<?php endif; ?>
</ul> </ul>
</li> </li>
<?php endif; ?>
<?php
$canSeeFunctions =
userCan('masterdata.mescole.view')
|| userCan('masterdata.matrici.view')
|| userCan('masterdata.linee.view')
|| userCan('masterdata.packaging.view')
|| userCan('masterdata.suppliers.view')
|| userCan('masterdata.lookup.view')
|| userCan('masterdata.worksheets.view');
?>
<?php if ($canSeeFunctions) : ?>
<li> <li>
<a href="javascript:;" class="has-arrow"> <a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class="bx bx-category"></i> <div class="parent-icon">
<i class="bx bx-category"></i>
</div> </div>
<div class="menu-title">Funzioni</div> <div class="menu-title">Funzioni</div>
</a> </a>
<ul> <ul>
<?php if (userCan('masterdata.mescole.view')) : ?>
<li> <li>
<a href="mescole.php"><i class='bx bx-radio-circle'></i>Mescole</a> <a href="mescole.php">
</li> <i class='bx bx-radio-circle'></i>Mescole
<li> </a>
<a href="matrici.php"><i class='bx bx-radio-circle'></i>Matrici</a>
</li>
<li>
<a href="linee.php"><i class='bx bx-radio-circle'></i>Linee di produzione</a>
</li> </li>
<?php endif; ?>
<?php if (userCan('masterdata.matrici.view')) : ?>
<li>
<a href="matrici.php">
<i class='bx bx-radio-circle'></i>Matrici
</a>
</li>
<?php endif; ?>
<?php if (userCan('masterdata.linee.view')) : ?>
<li>
<a href="linee.php">
<i class='bx bx-radio-circle'></i>Linee di produzione
</a>
</li>
<?php endif; ?>
<?php if (userCan('masterdata.packaging.view')) : ?>
<li>
<a href="packaging_items.php">
<i class='bx bx-radio-circle'></i>Imballaggi
</a>
</li>
<?php endif; ?>
<?php if (userCan('masterdata.suppliers.view')) : ?>
<li>
<a href="suppliers.php">
<i class='bx bx-radio-circle'></i>Suppliers
</a>
</li>
<?php endif; ?>
<?php if (userCan('masterdata.lookup.view')) : ?>
<li>
<a href="lookup_values.php">
<i class='bx bx-radio-circle'></i>Setup
</a>
</li>
<?php endif; ?>
<?php if (userCan('masterdata.worksheets.view')) : ?>
<li>
<a href="worksheets.php">
<i class='bx bx-radio-circle'></i>Fogli di lavoro
</a>
</li>
<?php endif; ?>
</ul> </ul>
</li> </li>
<?php endif; ?>
<?php
$canSeeProduction =
userCan('production.line_view.view')
|| userCan('production.stats.view')
|| userCan('production.manager.view')
|| userCan('production.manager_stats.view')
|| userCan('warehouse.dashboard.view');
?>
<?php if ($canSeeProduction) : ?>
<li> <li>
<a href="javascript:;" class="has-arrow"> <a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class="bx bx-calendar-check"></i> <div class="parent-icon">
<i class="bx bx-line-chart"></i>
</div>
<div class="menu-title">Produzione</div>
</a>
<ul>
<?php if (userCan('production.line_view.view')) : ?>
<li>
<a href="production_line_view2.php">
<i class='bx bx-radio-circle'></i>Line View
</a>
</li>
<?php endif; ?>
<?php if (userCan('production.stats.view')) : ?>
<li>
<a href="production_stats.php">
<i class='bx bx-radio-circle'></i>Statistiche
</a>
</li>
<?php endif; ?>
<?php if (userCan('production.manager.view')) : ?>
<li>
<a href="manager_produzione.php">
<i class='bx bx-radio-circle'></i>Manager
</a>
</li>
<?php endif; ?>
<?php if (userCan('production.manager_stats.view')) : ?>
<li>
<a href="manager_stats.php">
<i class='bx bx-radio-circle'></i>Manager Stats
</a>
</li>
<?php endif; ?>
<?php if (userCan('warehouse.dashboard.view')) : ?>
<li>
<a href="warehouse_dashboard.php">
<i class='bx bx-radio-circle'></i>Magazzino
</a>
</li>
<?php endif; ?>
</ul>
</li>
<?php endif; ?>
<?php
$canSeeServices =
userCan('services.status.view')
|| userCan('services.pause_reasons.view')
|| userCan('services.tools.view');
?>
<?php if ($canSeeServices) : ?>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon">
<i class="bx bx-wrench"></i>
</div>
<div class="menu-title">Servizi</div>
</a>
<ul>
<?php if (userCan('services.status.view')) : ?>
<li>
<a href="production_status.php">
<i class='bx bx-radio-circle'></i>Status
</a>
</li>
<?php endif; ?>
<?php if (userCan('services.pause_reasons.view')) : ?>
<li>
<a href="production_pause_reasons.php">
<i class='bx bx-radio-circle'></i>Cause di Pausa
</a>
</li>
<?php endif; ?>
<?php if (userCan('services.tools.view')) : ?>
<li>
<a href="production_tools.php">
<i class='bx bx-radio-circle'></i>Attrezzature
</a>
</li>
<?php endif; ?>
</ul>
</li>
<?php endif; ?>
<?php
$canSeeHr =
userCan('hr.employees.view')
|| userCan('hr.departments.view')
|| userCan('hr.job_roles.view')
|| userCan('hr.training_topics.view')
|| userCan('hr.trainings.view')
|| userCan('hr.skills.view');
?>
<?php if ($canSeeHr) : ?>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon">
<i class="bx bx-group"></i>
</div>
<div class="menu-title">Personale</div>
</a>
<ul>
<?php if (userCan('hr.employees.view')) : ?>
<li>
<a href="employees.php">
<i class='bx bx-radio-circle'></i>Dipendenti
</a>
</li>
<?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>
<div class="menu-title">Scadenzario</div> <div class="menu-title">Scadenzario</div>
</a> </a>
<ul> <ul>
<li> <li>
<a href="scadenzario/index.php"><i class='bx bx-radio-circle'></i>Lista Scadenze</a> <a href="scadenzario/index.php">
<i class='bx bx-radio-circle'></i>Lista Scadenze
</a>
</li> </li>
<li> <li>
<a href="scadenzario/calendar.php"><i class='bx bx-radio-circle'></i>Calendario</a> <a href="scadenzario/calendar.php">
<i class='bx bx-radio-circle'></i>Calendario
</a>
</li> </li>
</ul> </ul>
</li> </li>
<?php endif; ?>
<li class="menu-label">Others</li> <li class="menu-label">Others</li>
<li> <li>
<a href="https://helpdesk.cesoft.io" target="_blank"> <a href="https://helpdesk.cesoft.io" target="_blank">
<div class="parent-icon"><i class="bx bx-support"></i> <div class="parent-icon">
<i class="bx bx-support"></i>
</div> </div>
<div class="menu-title">Support</div> <div class="menu-title">Support</div>
</a> </a>
</li> </li>
<?php
endif; ?>
<!-- admin, superuser menù --> <?php if (userCan('users.manage')) : ?>
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('Superuser'))) : ?>
<?php
endif; ?>
<!-- admin menù -->
<?php if (Auth::user()->hasRole('Admin')) : ?>
<li class="menu-label">Admin Menù</li> <li class="menu-label">Admin Menù</li>
<li> <li>
<a href="../" target="_blank"> <a href="../" target="_blank">
<div class="parent-icon"><i class="bx bx-support"></i> <div class="parent-icon">
<i class="bx bx-user-circle"></i>
</div> </div>
<div class="menu-title">User Management</div> <div class="menu-title">User Management</div>
</a> </a>
</li> </li>
<!-- <li> <?php endif; ?>
<a href="template/index.html" target="_blank">
<div class="parent-icon"><i class="bx bx-support"></i>
</div>
<div class="menu-title">Template</div>
</a>
</li>
<li>
<a href="https://codervent.com/rocker/documentation/index.html" target="_blank">
<div class="parent-icon"><i class="bx bx-folder"></i>
</div>
<div class="menu-title">Documentation</div>
</a>
</li> -->
<?php
endif; ?>
</ul> </ul>
<!--end navigation--> <!--end navigation-->
</div> </div>
@@ -0,0 +1,62 @@
<?php
if (!function_exists('userCan')) {
/**
* Check if current user has a Vanguard permission.
* Uses Vanguard native method if available, otherwise falls back to DB check.
*/
function userCan($permissionName)
{
global $kindofrole;
$user = Auth::user();
if (!$user) {
return false;
}
// Vanguard / Laravel-style methods, depending on installed version/customization.
if (method_exists($user, 'hasPermission')) {
return $user->hasPermission($permissionName);
}
if (method_exists($user, 'hasPermissionTo')) {
return $user->hasPermissionTo($permissionName);
}
if (method_exists($user, 'can')) {
return $user->can($permissionName);
}
// Fallback: direct DB check using existing Vanguard tables.
static $permissions = null;
if ($permissions === null) {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->prepare("
SELECT p.name
FROM auth_permissions p
INNER JOIN auth_permission_role pr ON pr.permission_id = p.id
WHERE pr.role_id = ?
");
$stmt->execute([(int)$kindofrole]);
$permissions = $stmt->fetchAll(PDO::FETCH_COLUMN);
}
return in_array($permissionName, $permissions, true);
}
}
if (!function_exists('visibleButtons')) {
/**
* Filter visible buttons.
*/
function visibleButtons(array $buttons)
{
return array_values(array_filter($buttons, function ($button) {
return empty($button['permission']) || userCan($button['permission']);
}));
}
}
+15 -1
View File
@@ -1,3 +1,11 @@
<?php
// Build an absolute URL to employee-profile.php so it works from any depth
// (e.g. /userarea/index.php, /userarea/scadenzario/index.php).
$__scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
$__pos = strpos($__scriptName, '/userarea/');
$__base = $__pos !== false ? substr($__scriptName, 0, $__pos) : '';
$__myProfileHref = $__base . '/userarea/employee-profile.php';
?>
<header> <header>
<div class="topbar d-flex align-items-center"> <div class="topbar d-flex align-items-center">
<nav class="navbar navbar-expand gap-3"> <nav class="navbar navbar-expand gap-3">
@@ -86,7 +94,13 @@
</a> </a>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li> <li>
<a class="dropdown-item d-flex align-items-center" href="../users"> <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="user_settings.php">
<i class="bx bx-user fs-5"></i><span>Utente</span> <i class="bx bx-user fs-5"></i><span>Utente</span>
</a> </a>
</li> </li>
+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>
+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>
-1
View File
@@ -42,7 +42,6 @@ $params = $stmtParams->fetchAll(PDO::FETCH_ASSOC);
<!-- jQuery / Bootstrap / SweetAlert --> <!-- jQuery / Bootstrap / SweetAlert -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables --> <!-- DataTables -->
+1 -2
View File
@@ -11,7 +11,6 @@
<!-- jQuery e Bootstrap --> <!-- jQuery e Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables --> <!-- DataTables -->
@@ -118,7 +117,7 @@
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -1
View File
@@ -261,7 +261,7 @@ function h($v)
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -1
View File
@@ -874,7 +874,7 @@ $isEdit = ($worksheet_id > 0);
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -1
View File
@@ -168,7 +168,7 @@ $rows_special = array_filter($rows, function ($r) {
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -1
View File
@@ -551,7 +551,7 @@ function revisionLabel($rev)
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -2
View File
@@ -11,7 +11,6 @@
<!-- jQuery e Bootstrap --> <!-- jQuery e Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables --> <!-- DataTables -->
@@ -138,7 +137,7 @@
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -1
View File
@@ -231,7 +231,7 @@
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -2
View File
@@ -11,7 +11,6 @@
<!-- jQuery + Bootstrap --> <!-- jQuery + Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables --> <!-- DataTables -->
@@ -133,7 +132,7 @@
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
-1
View File
@@ -13,7 +13,6 @@
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Bootstrap (se già incluso puoi rimuoverlo) --> <!-- Bootstrap (se già incluso puoi rimuoverlo) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- SweetAlert2 --> <!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
-1
View File
@@ -13,7 +13,6 @@
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Bootstrap --> <!-- Bootstrap -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- DataTables --> <!-- DataTables -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css"> <link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
+279 -167
View File
@@ -1,4 +1,184 @@
<?php include('include/headscript.php'); ?> <?php include('include/headscript.php'); ?>
<?php
$dashboardSections = [
[
'id' => 'secOperativo',
'title' => 'Operativo',
'subtitle' => 'Azioni principali di produzione e attività in scadenza',
'icon' => '🚀',
'open' => true,
'buttons' => [
[
'label' => 'Programmazione',
'icon' => '🗓️',
'class' => 'btn-programmazione',
'url' => 'produzione_programmazione_drag.php',
'permission' => 'production.programming.view',
],
[
'label' => 'Line View',
'icon' => '⚙️',
'class' => 'btn-status',
'url' => 'production_line_view2.php',
'permission' => 'production.line_view.view',
],
[
'label' => 'Statistiche',
'icon' => '📈',
'class' => 'btn-statistiche',
'url' => 'production_stats.php',
'permission' => 'production.stats.view',
],
[
'label' => 'Manager',
'icon' => '👔',
'class' => 'btn-manager',
'url' => 'manager_produzione.php',
'permission' => 'production.manager.view',
],
[
'label' => 'Manager Stats',
'icon' => '📊',
'class' => 'btn-manager-stats',
'url' => 'manager_stats.php',
'permission' => 'production.manager_stats.view',
],
[
'label' => 'Magazzino',
'icon' => '📦',
'class' => 'btn-magazzino',
'url' => 'warehouse_dashboard.php',
'permission' => 'warehouse.dashboard.view',
],
[
'label' => 'Smart-Alert',
'icon' => '⏰',
'class' => 'btn-scadenziario',
'url' => 'scadenzario/index.php',
'permission' => 'deadlines.view',
],
],
],
[
'id' => 'secAnagrafiche',
'title' => 'Anagrafiche',
'subtitle' => 'Dati di base e setup di produzione',
'icon' => '🗂️',
'open' => false,
'buttons' => [
[
'label' => 'Mescole',
'icon' => '⚗️',
'class' => 'btn-mescole',
'url' => 'mescole.php',
'permission' => 'masterdata.mescole.view',
],
[
'label' => 'Elenco Profili',
'icon' => '🧩',
'class' => 'btn-matrici',
'url' => 'matrici.php',
'permission' => 'masterdata.matrici.view',
],
[
'label' => 'Linee Produzione',
'icon' => '🏭',
'class' => 'btn-linee',
'url' => 'linee.php',
'permission' => 'masterdata.linee.view',
],
[
'label' => 'Imballaggi',
'icon' => '📦',
'class' => 'btn-setup',
'url' => 'packaging_items.php',
'permission' => 'masterdata.packaging.view',
],
[
'label' => 'Suppliers',
'icon' => '🏷️',
'class' => 'btn-setup',
'url' => 'suppliers.php',
'permission' => 'masterdata.suppliers.view',
],
[
'label' => 'Setup',
'icon' => '⚙️',
'class' => 'btn-setup',
'url' => 'lookup_values.php',
'permission' => 'masterdata.lookup.view',
],
[
'label' => 'Fogli di lavoro',
'icon' => '🗒️',
'class' => 'btn-setup',
'url' => 'worksheets.php',
'permission' => 'masterdata.worksheets.view',
],
],
],
[
'id' => 'secServizi',
'title' => 'Servizi',
'subtitle' => 'Status, cause pausa, attrezzature',
'icon' => '🧰',
'open' => false,
'buttons' => [
[
'label' => 'Status',
'icon' => '📋',
'class' => 'btn-setup',
'url' => 'production_status.php',
'permission' => 'services.status.view',
],
[
'label' => 'Cause di Pausa',
'icon' => '🛑',
'class' => 'btn-problem',
'url' => 'production_pause_reasons.php',
'permission' => 'services.pause_reasons.view',
],
[
'label' => 'Attrezzature',
'icon' => '🛠️',
'class' => 'btn-tools',
'url' => 'production_tools.php',
'permission' => 'services.tools.view',
],
],
],
[
'id' => 'secPersonale',
'title' => 'Personale',
'subtitle' => 'Dipendenti, skill',
'icon' => '👥',
'open' => false,
'buttons' => [
[
'label' => 'Employees',
'icon' => '👥',
'class' => 'btn-employees',
'url' => 'employees.php',
'permission' => 'hr.employees.view',
],
[
'label' => 'Departments',
'icon' => '🏢',
'class' => 'btn-departments',
'url' => 'departments.php',
'permission' => 'hr.departments.view',
],
[
'label' => 'Skills',
'icon' => '🧠',
'class' => 'btn-setup',
'url' => 'skills.php',
'permission' => 'hr.skills.view',
],
],
],
];
?>
<!doctype html> <!doctype html>
<html lang="it"> <html lang="it">
@@ -7,7 +187,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" /> <link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?> <?php include('cssinclude.php'); ?>
<title>Dashboard Produzione - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title> <title>Dashboard <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<!-- Bootstrap + jQuery --> <!-- Bootstrap + jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
@@ -298,19 +478,68 @@
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
<div class="page-wrapper"> <div class="page-wrapper">
<div class="page-content"> <div class="page-content">
<?php <?php $pdo = DBHandlerSelect::getInstance()->getConnection(); ?>
$pdo = DBHandlerSelect::getInstance()->getConnection(); <style>
include(__DIR__ . '/scadenzario/include/my_deadlines_widget.php'); .my-deadlines-widgets {
?> display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-bottom: 1rem;
width: 100%;
}
.my-deadlines-widgets:empty { display: none; }
/* Each widget wraps itself in .my-deadlines-widgets; collapse the nested
wrapper so all cards flow into the outer flex (single row). */
.my-deadlines-widgets .my-deadlines-widgets {
display: contents;
}
.my-deadlines-widgets .mdw {
flex: 1 1 0;
min-width: 0;
display: flex; align-items: center; gap: 0.75rem;
padding: 0.8rem 0.9rem;
border-radius: 0.6rem;
text-decoration: none;
color: #fff;
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
transition: transform 0.15s, box-shadow 0.15s;
}
@media (max-width: 991.98px) {
.my-deadlines-widgets .mdw { flex: 1 1 calc(50% - 0.375rem); }
}
@media (max-width: 575.98px) {
.my-deadlines-widgets .mdw { flex: 1 1 100%; }
}
.my-deadlines-widgets .mdw:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); color: #fff; }
.my-deadlines-widgets .mdw-red { background: linear-gradient(135deg, #dc3545 0%, #b02a37 100%); }
.my-deadlines-widgets .mdw-orange { background: linear-gradient(135deg, #e8930c 0%, #c77a00 100%); }
.my-deadlines-widgets .mdw-gray { background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%); }
.my-deadlines-widgets .mdw-icon {
width: 38px; height: 38px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
background: rgba(255,255,255,0.22); font-size: 1.05rem; flex-shrink: 0;
}
.my-deadlines-widgets .mdw-body { flex: 1; line-height: 1.2; min-width: 0; }
.my-deadlines-widgets .mdw-count { font-size: 1.5rem; font-weight: 700; }
.my-deadlines-widgets .mdw-label {
font-size: 0.78rem; opacity: 0.95;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.my-deadlines-widgets .mdw-arrow { opacity: 0.7; font-size: 0.85rem; flex-shrink: 0; }
</style>
<div class="my-deadlines-widgets">
<?php include(__DIR__ . '/scadenzario/include/my_deadlines_widget.php'); ?>
<?php include(__DIR__ . '/include/training_widget.php'); ?>
</div>
<h3 class="dashboard-title">Dashboard Produzione</h3> <h3 class="dashboard-title">Dashboard</h3>
<!-- ===== STATISTICHE PRINCIPALI ===== --> <!-- ===== STATISTICHE PRINCIPALI ===== -->
<div class="stats-row"> <div class="stats-row">
@@ -347,188 +576,71 @@
<!-- ===== SEZIONI COLLASSABILI ===== --> <!-- ===== SEZIONI COLLASSABILI ===== -->
<div class="sections-wrap" id="prodAccordion"> <div class="sections-wrap" id="prodAccordion">
<!-- OPERATIVO --> <?php
$hasVisibleSections = false;
foreach ($dashboardSections as $section):
$buttons = visibleButtons($section['buttons']);
// If no visible buttons are available, do not show the section.
if (empty($buttons)) {
continue;
}
$hasVisibleSections = true;
$sectionId = htmlspecialchars($section['id'], ENT_QUOTES, 'UTF-8');
$isOpen = !empty($section['open']);
?>
<div class="section-card"> <div class="section-card">
<button type="button" class="section-header" data-bs-toggle="collapse" data-bs-target="#secOperativo" aria-expanded="true" aria-controls="secOperativo"> <button type="button"
class="section-header"
data-bs-toggle="collapse"
data-bs-target="#<?= $sectionId ?>"
aria-expanded="<?= $isOpen ? 'true' : 'false' ?>"
aria-controls="<?= $sectionId ?>">
<div class="section-left"> <div class="section-left">
<div class="section-icon">🚀</div> <div class="section-icon"><?= htmlspecialchars($section['icon'], ENT_QUOTES, 'UTF-8') ?></div>
<div style="min-width:0;"> <div style="min-width:0;">
<p class="section-title">Operativo</p> <p class="section-title"><?= htmlspecialchars($section['title'], ENT_QUOTES, 'UTF-8') ?></p>
<p class="section-subtitle">Azioni principali di produzione e attività in scadenza</p> <p class="section-subtitle"><?= htmlspecialchars($section['subtitle'], ENT_QUOTES, 'UTF-8') ?></p>
</div> </div>
</div> </div>
<div class="chev"></div> <div class="chev"></div>
</button> </button>
<div id="secOperativo" class="collapse show" data-bs-parent="#prodAccordion"> <div id="<?= $sectionId ?>"
class="collapse <?= $isOpen ? 'show' : '' ?>"
data-bs-parent="#prodAccordion">
<div class="section-body"> <div class="section-body">
<div class="dashboard-grid"> <div class="dashboard-grid">
<button class="dash-btn btn-programmazione" onclick="location.href='produzione_programmazione_drag.php'">
<div class="dash-icon">🗓️</div> <?php foreach ($buttons as $button): ?>
<div>Programmazione</div> <button class="dash-btn <?= htmlspecialchars($button['class'], ENT_QUOTES, 'UTF-8') ?>"
onclick="location.href='<?= htmlspecialchars($button['url'], ENT_QUOTES, 'UTF-8') ?>'">
<div class="dash-icon"><?= htmlspecialchars($button['icon'], ENT_QUOTES, 'UTF-8') ?></div>
<div><?= htmlspecialchars($button['label'], ENT_QUOTES, 'UTF-8') ?></div>
</button> </button>
<?php endforeach; ?>
<button class="dash-btn btn-status" onclick="location.href='production_line_view2.php'">
<div class="dash-icon">⚙️</div>
<div>Line View</div>
</button>
<button class="dash-btn btn-statistiche" onclick="location.href='production_stats.php'">
<div class="dash-icon">📈</div>
<div>Statistiche</div>
</button>
<button class="dash-btn btn-manager" onclick="location.href='manager_produzione.php'">
<div class="dash-icon">👔</div>
<div>Manager</div>
</button>
<button class="dash-btn btn-manager-stats" onclick="location.href='manager_stats.php'">
<div class="dash-icon">📊</div>
<div>Manager Stats</div>
</button>
<button class="dash-btn btn-magazzino" onclick="location.href='warehouse_dashboard.php'">
<div class="dash-icon">📦</div>
<div>Magazzino</div>
</button>
<button class="dash-btn btn-scadenziario" onclick="location.href='scadenzario/index.php'">
<div class="dash-icon"></div>
<div>Scadenziario</div>
</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- ANAGRAFICHE --> <?php endforeach; ?>
<?php if (!$hasVisibleSections): ?>
<div class="section-card"> <div class="section-card">
<button type="button" class="section-header" data-bs-toggle="collapse" data-bs-target="#secAnagrafiche" aria-expanded="false" aria-controls="secAnagrafiche"> <div class="section-body text-center">
<div class="section-left"> Nessuna sezione disponibile per il tuo profilo.
<div class="section-icon">🗂️</div>
<div style="min-width:0;">
<p class="section-title">Anagrafiche</p>
<p class="section-subtitle">Dati di base e setup di produzione</p>
</div> </div>
</div> </div>
<div class="chev"></div> <?php endif; ?>
</button>
<div id="secAnagrafiche" class="collapse" data-bs-parent="#prodAccordion">
<div class="section-body">
<div class="dashboard-grid">
<button class="dash-btn btn-mescole" onclick="location.href='mescole.php'">
<div class="dash-icon">⚗️</div>
<div>Mescole</div>
</button>
<button class="dash-btn btn-matrici" onclick="location.href='matrici.php'">
<div class="dash-icon">🧩</div>
<div>Elenco Profili</div>
</button>
<button class="dash-btn btn-linee" onclick="location.href='linee.php'">
<div class="dash-icon">🏭</div>
<div>Linee Produzione</div>
</button>
<button class="dash-btn btn-setup" onclick="location.href='packaging_items.php'">
<div class="dash-icon">📦</div>
<div>Imballaggi</div>
</button>
<button class="dash-btn btn-setup" onclick="location.href='suppliers.php'">
<div class="dash-icon">🏷️</div>
<div>Suppliers</div>
</button>
<button class="dash-btn btn-setup" onclick="location.href='lookup_values.php'">
<div class="dash-icon">⚙️</div>
<div>Setup</div>
</button>
<button class="dash-btn btn-setup" onclick="location.href='worksheets.php'">
<div class="dash-icon">🗒️</div>
<div>Fogli di lavoro</div>
</button>
</div> </div>
</div> <!-- /sections-wrap -->
</div>
</div>
<!-- QUALITÀ / SERVIZI -->
<div class="section-card">
<button type="button" class="section-header" data-bs-toggle="collapse" data-bs-target="#secServizi" aria-expanded="false" aria-controls="secServizi">
<div class="section-left">
<div class="section-icon">🧰</div>
<div style="min-width:0;">
<p class="section-title">Servizi</p>
<p class="section-subtitle">Status, cause pausa, attrezzature</p>
</div>
</div>
<div class="chev"></div>
</button>
<div id="secServizi" class="collapse" data-bs-parent="#prodAccordion">
<div class="section-body">
<div class="dashboard-grid">
<button class="dash-btn btn-setup" onclick="location.href='production_status.php'">
<div class="dash-icon">📋</div>
<div>Status</div>
</button>
<button class="dash-btn btn-problem" onclick="location.href='production_pause_reasons.php'">
<div class="dash-icon">🛑</div>
<div>Cause di Pausa</div>
</button>
<button class="dash-btn btn-tools" onclick="location.href='production_tools.php'">
<div class="dash-icon">🛠️</div>
<div>Attrezzature</div>
</button>
</div>
</div>
</div>
</div>
<!-- PERSONALE -->
<div class="section-card">
<button type="button" class="section-header" data-bs-toggle="collapse" data-bs-target="#secPersonale" aria-expanded="false" aria-controls="secPersonale">
<div class="section-left">
<div class="section-icon">👥</div>
<div style="min-width:0;">
<p class="section-title">Personale</p>
<p class="section-subtitle">Dipendenti, skill</p>
</div>
</div>
<div class="chev"></div>
</button>
<div id="secPersonale" class="collapse" data-bs-parent="#prodAccordion">
<div class="section-body">
<div class="dashboard-grid">
<button class="dash-btn btn-employees" onclick="location.href='employees.php'">
<div class="dash-icon">👥</div>
<div>Employees</div>
</button>
<button class="dash-btn btn-departments" onclick="location.href='departments.php'">
<div class="dash-icon">🏢</div>
<div>Departments</div>
</button>
<button class="dash-btn btn-setup" onclick="location.href='skills.php'">
<div class="dash-icon">🧠</div>
<div>Skills</div>
</button>
</div>
</div>
</div>
</div>
</div> <!-- /sections-wrap -->
</div> </div>
</div> </div>
+1 -1
View File
@@ -1114,7 +1114,7 @@ if (!empty($_GET['ajax'])) {
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -1
View File
@@ -80,7 +80,7 @@
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
@@ -363,7 +363,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['action'])) {
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); <?php include('include/navbar.php');
include('include/topbar.php'); ?> include('include/topbar.php'); ?>
<div class="page-wrapper"> <div class="page-wrapper">
@@ -4,12 +4,19 @@ header('Content-Type: application/json');
require_once(__DIR__ . '/../../class/db-functions.php'); require_once(__DIR__ . '/../../class/db-functions.php');
try { try {
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) { $rawId = $_POST['id'] ?? $_GET['id'] ?? null;
if ($rawId === null || !is_numeric($rawId)) {
echo json_encode(['success' => false, 'message' => 'ID non valido.']); echo json_encode(['success' => false, 'message' => 'ID non valido.']);
exit; exit;
} }
$id = (int)$_GET['id']; $id = (int)$rawId;
// Whether to create the next (recurring) deadline. Absent or '1' => create; '0' => complete only.
$createNext = ($_POST['create_next'] ?? '1') !== '0';
// Whether to carry the attachment links over to the new deadline. Default ON ("default all activate").
$copyAttachments = ($_POST['copy_attachments'] ?? '1') !== '0';
$db = DBHandlerSelect::getInstance(); $db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection(); $pdo = $db->getConnection();
@@ -34,11 +41,13 @@ try {
->execute([$id, $currentUserId]); ->execute([$id, $currentUserId]);
$newId = null; $newId = null;
$newDueDate = null;
// If recurring, create next deadline // If recurring AND the user asked for it, create the next deadline
if ($deadline['recurrence_type'] !== 'once') { if ($deadline['recurrence_type'] !== 'once' && $createNext) {
$dueDate = new DateTime($deadline['due_date']); $dueDate = new DateTime($deadline['due_date']);
$checkDate = $deadline['check_date'] ? new DateTime($deadline['check_date']) : null; $checkDate = $deadline['check_date'] ? new DateTime($deadline['check_date']) : null;
$documentDate = $deadline['document_date'] ? new DateTime($deadline['document_date']) : null;
switch ($deadline['recurrence_type']) { switch ($deadline['recurrence_type']) {
case 'monthly': $interval = new DateInterval('P1M'); break; case 'monthly': $interval = new DateInterval('P1M'); break;
@@ -57,23 +66,25 @@ try {
if ($interval) { if ($interval) {
$dueDate->add($interval); $dueDate->add($interval);
if ($checkDate) $checkDate->add($interval); if ($checkDate) $checkDate->add($interval);
if ($documentDate) $documentDate->add($interval);
$ins = $pdo->prepare(" $ins = $pdo->prepare("
INSERT INTO scad_deadlines INSERT INTO scad_deadlines
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date, (subject_id, function_id, topic, law_regulation, recurrence_type, due_date, check_date,
document_date, notification_days, storage_location, notes, created_by, departments) document_date, notification_days, storage_location, notes, created_by, departments)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"); ");
$ins->execute([ $ins->execute([
$deadline['subject_id'], $deadline['topic'], $deadline['law_regulation'], $deadline['subject_id'], $deadline['function_id'], $deadline['topic'], $deadline['law_regulation'],
$deadline['recurrence_type'], $dueDate->format('Y-m-d'), $deadline['recurrence_type'], $dueDate->format('Y-m-d'),
$checkDate ? $checkDate->format('Y-m-d') : null, $checkDate ? $checkDate->format('Y-m-d') : null,
$deadline['document_date'], $documentDate ? $documentDate->format('Y-m-d') : null,
$deadline['notification_days'], $deadline['storage_location'], $deadline['notification_days'], $deadline['storage_location'],
$deadline['notes'], $deadline['created_by'], $deadline['departments'] $deadline['notes'], $deadline['created_by'], $deadline['departments']
]); ]);
$newId = $pdo->lastInsertId(); $newId = $pdo->lastInsertId();
$newDueDate = $dueDate;
// Copy employee assignments // Copy employee assignments
$empStmt = $pdo->prepare("SELECT employee_id FROM scad_deadline_employee WHERE deadline_id = ?"); $empStmt = $pdo->prepare("SELECT employee_id FROM scad_deadline_employee WHERE deadline_id = ?");
@@ -87,6 +98,31 @@ try {
} }
} }
// Carry forward ALL attachment links from the source deadline (shared physical file, same stored_name).
// Individual links can later be removed on the new deadline without deleting the file.
if ($copyAttachments) {
$attSel = $pdo->prepare("
SELECT original_name, stored_name, mime_type, size
FROM scad_deadline_attachments
WHERE deadline_id = ?
");
$attSel->execute([$id]);
$attRows = $attSel->fetchAll(PDO::FETCH_ASSOC);
if ($attRows) {
$attIns = $pdo->prepare("
INSERT INTO scad_deadline_attachments
(deadline_id, original_name, stored_name, mime_type, size, uploaded_by)
VALUES (?, ?, ?, ?, ?, ?)
");
$attHist = $pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'attachment_linked', ?)");
foreach ($attRows as $a) {
$attIns->execute([$newId, $a['original_name'], $a['stored_name'], $a['mime_type'], $a['size'], $currentUserId]);
$attHist->execute([$newId, $currentUserId, $a['original_name']]);
}
}
}
// History for new // History for new
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'created', ?)") $pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'created', ?)")
->execute([$newId, $currentUserId, 'Creata automaticamente dalla scadenza #' . $id]); ->execute([$newId, $currentUserId, 'Creata automaticamente dalla scadenza #' . $id]);
@@ -97,7 +133,7 @@ try {
$msg = 'Scadenza completata con successo.'; $msg = 'Scadenza completata con successo.';
if ($newId) { if ($newId) {
$msg .= ' Nuova scadenza creata con data ' . $dueDate->format('d/m/Y') . '.'; $msg .= ' Nuova scadenza creata con data ' . $newDueDate->format('d/m/Y') . '.';
} }
echo json_encode(['success' => true, 'message' => $msg, 'new_id' => $newId]); echo json_encode(['success' => true, 'message' => $msg, 'new_id' => $newId]);
@@ -23,20 +23,32 @@ try {
exit; exit;
} }
// Delete file // Remove this link (DB record) first
$pdo->prepare("DELETE FROM scad_deadline_attachments WHERE id = ?")->execute([$id]);
// The same physical file may be shared with other deadlines (carried forward on completion).
// Only unlink it when no other link references the same stored file.
$refStmt = $pdo->prepare("SELECT COUNT(*) FROM scad_deadline_attachments WHERE stored_name = ?");
$refStmt->execute([$att['stored_name']]);
$stillReferenced = (int)$refStmt->fetchColumn() > 0;
if ($stillReferenced) {
$action = 'attachment_unlinked';
$message = 'Collegamento rimosso. Il file è conservato (usato da un\'altra scadenza).';
} else {
$filePath = __DIR__ . '/../attachments/' . $att['stored_name']; $filePath = __DIR__ . '/../attachments/' . $att['stored_name'];
if (file_exists($filePath)) { if (file_exists($filePath)) {
unlink($filePath); unlink($filePath);
} }
$action = 'attachment_removed';
// Delete DB record $message = 'Allegato eliminato.';
$pdo->prepare("DELETE FROM scad_deadline_attachments WHERE id = ?")->execute([$id]); }
// History // History
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'attachment_removed', ?)") $pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, ?, ?)")
->execute([$att['deadline_id'], $currentUserId, $att['original_name']]); ->execute([$att['deadline_id'], $currentUserId, $action, $att['original_name']]);
echo json_encode(['success' => true, 'message' => 'Allegato eliminato.']); echo json_encode(['success' => true, 'message' => $message]);
} catch (Exception $e) { } catch (Exception $e) {
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]); echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
@@ -13,10 +13,29 @@ try {
$db = DBHandlerSelect::getInstance(); $db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection(); $pdo = $db->getConnection();
// Collect the physical files referenced by this deadline before the FK cascade removes its links
$attStmt = $pdo->prepare("SELECT DISTINCT stored_name FROM scad_deadline_attachments WHERE deadline_id = ?");
$attStmt->execute([$id]);
$storedNames = $attStmt->fetchAll(PDO::FETCH_COLUMN);
// Deleting the deadline cascades to its attachment/employee/history rows (FK ON DELETE CASCADE)
$stmt = $pdo->prepare("DELETE FROM scad_deadlines WHERE id = ?"); $stmt = $pdo->prepare("DELETE FROM scad_deadlines WHERE id = ?");
$stmt->execute([$id]); $stmt->execute([$id]);
if ($stmt->rowCount() > 0) { if ($stmt->rowCount() > 0) {
// Unlink physical files no longer referenced by any other deadline (shared-file safe)
if (!empty($storedNames)) {
$refStmt = $pdo->prepare("SELECT COUNT(*) FROM scad_deadline_attachments WHERE stored_name = ?");
foreach ($storedNames as $storedName) {
$refStmt->execute([$storedName]);
if ((int)$refStmt->fetchColumn() === 0) {
$filePath = __DIR__ . '/../attachments/' . $storedName;
if (file_exists($filePath)) {
unlink($filePath);
}
}
}
}
echo json_encode(['success' => true, 'message' => 'Scadenza eliminata con successo.']); echo json_encode(['success' => true, 'message' => 'Scadenza eliminata con successo.']);
} else { } else {
echo json_encode(['success' => false, 'message' => 'Scadenza non trovata.']); echo json_encode(['success' => false, 'message' => 'Scadenza non trovata.']);
@@ -9,6 +9,7 @@ try {
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : null; $id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : null;
$subject_id = isset($_POST['subject_id']) && is_numeric($_POST['subject_id']) && (int)$_POST['subject_id'] > 0 ? (int)$_POST['subject_id'] : null; $subject_id = isset($_POST['subject_id']) && is_numeric($_POST['subject_id']) && (int)$_POST['subject_id'] > 0 ? (int)$_POST['subject_id'] : null;
$function_id = isset($_POST['function_id']) && is_numeric($_POST['function_id']) && (int)$_POST['function_id'] > 0 ? (int)$_POST['function_id'] : null;
$topic = trim($_POST['topic'] ?? ''); $topic = trim($_POST['topic'] ?? '');
$law_regulation = trim($_POST['law_regulation'] ?? '') ?: null; $law_regulation = trim($_POST['law_regulation'] ?? '') ?: null;
$recurrence_type = $_POST['recurrence_type'] ?? 'once'; $recurrence_type = $_POST['recurrence_type'] ?? 'once';
@@ -52,15 +53,25 @@ try {
if ($id) { if ($id) {
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
UPDATE scad_deadlines SET UPDATE scad_deadlines SET
subject_id = ?, topic = ?, law_regulation = ?, recurrence_type = ?, subject_id = ?, function_id = ?, topic = ?, law_regulation = ?, recurrence_type = ?,
due_date = ?, check_date = ?, document_date = ?, notification_days = ?, due_date = ?, check_date = ?, document_date = ?, notification_days = ?,
storage_location = ?, notes = ?, departments = ? storage_location = ?, notes = ?, departments = ?
WHERE id = ? WHERE id = ?
"); ");
$stmt->execute([ $stmt->execute([
$subject_id, $topic, $law_regulation, $recurrence_type, $subject_id,
$due_date, $check_date, $document_date, $notification_days, $function_id,
$storage_location, $notes, $departmentsStr, $id $topic,
$law_regulation,
$recurrence_type,
$due_date,
$check_date,
$document_date,
$notification_days,
$storage_location,
$notes,
$departmentsStr,
$id
]); ]);
// Re-link employees // Re-link employees
@@ -75,14 +86,24 @@ try {
// INSERT // INSERT
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO scad_deadlines INSERT INTO scad_deadlines
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date, (subject_id, function_id, topic, law_regulation, recurrence_type, due_date, check_date,
document_date, notification_days, storage_location, notes, created_by, departments) document_date, notification_days, storage_location, notes, created_by, departments)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"); ");
$stmt->execute([ $stmt->execute([
$subject_id, $topic, $law_regulation, $recurrence_type, $subject_id,
$due_date, $check_date, $document_date, $notification_days, $function_id,
$storage_location, $notes, $currentUserId, $departmentsStr $topic,
$law_regulation,
$recurrence_type,
$due_date,
$check_date,
$document_date,
$notification_days,
$storage_location,
$notes,
$currentUserId,
$departmentsStr
]); ]);
$deadlineId = $pdo->lastInsertId(); $deadlineId = $pdo->lastInsertId();
@@ -107,7 +128,6 @@ try {
'message' => $id ? 'Scadenza aggiornata con successo.' : 'Scadenza creata con successo.', 'message' => $id ? 'Scadenza aggiornata con successo.' : 'Scadenza creata con successo.',
'id' => $deadlineId 'id' => $deadlineId
]); ]);
} catch (Exception $e) { } catch (Exception $e) {
if (isset($pdo) && $pdo->inTransaction()) { if (isset($pdo) && $pdo->inTransaction()) {
$pdo->rollBack(); $pdo->rollBack();
@@ -25,6 +25,17 @@ $pdo = $db->getConnection();
$today = date('Y-m-d'); $today = date('Y-m-d');
$appUrl = rtrim($_ENV['APP_URL'] ?? 'http://localhost:8001', '/'); $appUrl = rtrim($_ENV['APP_URL'] ?? 'http://localhost:8001', '/');
// Manager email for Cc — taken from MANAGER_USER_ID → auth_users.email
$managerCcEmail = null;
if (!empty($_ENV['MANAGER_USER_ID']) && is_numeric($_ENV['MANAGER_USER_ID'])) {
$mgrStmt = $pdo->prepare("SELECT email FROM auth_users WHERE id = ?");
$mgrStmt->execute([(int)$_ENV['MANAGER_USER_ID']]);
$mgrEmail = $mgrStmt->fetchColumn();
if (!empty($mgrEmail)) {
$managerCcEmail = $mgrEmail;
}
}
$sent = 0; $sent = 0;
$skipped = 0; $skipped = 0;
$errors = 0; $errors = 0;
@@ -143,6 +154,11 @@ foreach ($deadlines as $dl) {
); );
$mail->addAddress($emp['email'], trim($emp['first_name'] . ' ' . $emp['last_name'])); $mail->addAddress($emp['email'], trim($emp['first_name'] . ' ' . $emp['last_name']));
// Cc the manager (unless they are the direct recipient)
if ($managerCcEmail && strcasecmp($managerCcEmail, $emp['email']) !== 0) {
$mail->addCC($managerCcEmail);
}
$detailUrl = $appUrl . '/userarea/scadenzario/detail.php?id=' . $dl['id']; $detailUrl = $appUrl . '/userarea/scadenzario/detail.php?id=' . $dl['id'];
$topicText = (!empty($dl['subject_name']) ? $dl['subject_name'] . ' — ' : '') . $dl['topic']; $topicText = (!empty($dl['subject_name']) ? $dl['subject_name'] . ' — ' : '') . $dl['topic'];
+86 -16
View File
@@ -66,9 +66,9 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
} }
$recurrenceLabels = ['once' => 'Una tantum', 'monthly' => 'Mensile', 'quarterly' => 'Trimestrale', 'semiannual' => 'Semestrale', 'annual' => 'Annuale', 'biennial' => 'Biennale', 'triennial' => 'Triennale', 'quadriennial' => 'Quadriennale', 'quinquennial' => 'Quinquennale', 'decennial' => 'Decennale', 'quindecennial' => 'Quindicennale']; $recurrenceLabels = ['once' => 'Una tantum', 'monthly' => 'Mensile', 'quarterly' => 'Trimestrale', 'semiannual' => 'Semestrale', 'annual' => 'Annuale', 'biennial' => 'Biennale', 'triennial' => 'Triennale', 'quadriennial' => 'Quadriennale', 'quinquennial' => 'Quinquennale', 'decennial' => 'Decennale', 'quindecennial' => 'Quindicennale'];
$actionLabels = ['created' => 'Creata', 'updated' => 'Modificata', 'completed' => 'Completata', 'attachment_added' => 'Allegato aggiunto', 'attachment_removed' => 'Allegato rimosso', 'notification_sent' => 'Notifica inviata']; $actionLabels = ['created' => 'Creata', 'updated' => 'Modificata', 'completed' => 'Completata', 'attachment_added' => 'Allegato aggiunto', 'attachment_removed' => 'Allegato rimosso', 'attachment_linked' => 'Allegato collegato', 'attachment_unlinked' => 'Collegamento rimosso', 'notification_sent' => 'Notifica inviata'];
$actionColors = ['created' => '#198754', 'updated' => '#5a8fd8', 'completed' => '#6f42c1', 'attachment_added' => '#e8930c', 'attachment_removed' => '#e8930c', 'notification_sent' => '#adb5bd']; $actionColors = ['created' => '#198754', 'updated' => '#5a8fd8', 'completed' => '#6f42c1', 'attachment_added' => '#e8930c', 'attachment_removed' => '#e8930c', 'attachment_linked' => '#0dcaf0', 'attachment_unlinked' => '#adb5bd', 'notification_sent' => '#adb5bd'];
$actionIcons = ['created' => 'fa-plus', 'updated' => 'fa-pen', 'completed' => 'fa-check', 'attachment_added' => 'fa-paperclip', 'attachment_removed' => 'fa-trash', 'notification_sent' => 'fa-bell']; $actionIcons = ['created' => 'fa-plus', 'updated' => 'fa-pen', 'completed' => 'fa-check', 'attachment_added' => 'fa-paperclip', 'attachment_removed' => 'fa-trash', 'attachment_linked' => 'fa-link', 'attachment_unlinked' => 'fa-link-slash', 'notification_sent' => 'fa-bell'];
} }
} }
?> ?>
@@ -85,6 +85,14 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
<base href="<?= $baseHref ?>"> <base href="<?= $baseHref ?>">
<?php include('../cssinclude.php'); ?> <?php include('../cssinclude.php'); ?>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<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">
<link href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/i18n/it.js"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/it.js"></script>
<?php include __DIR__ . '/include/deadline_modal_css.php'; ?>
<title><?= $deadline ? htmlspecialchars($deadline['topic'], ENT_QUOTES, 'UTF-8') . ' — ' : '' ?>Scadenzario</title> <title><?= $deadline ? htmlspecialchars($deadline['topic'], ENT_QUOTES, 'UTF-8') . ' — ' : '' ?>Scadenzario</title>
<script> <script>
if (window.innerWidth > 1024) document.addEventListener('DOMContentLoaded', function() { if (window.innerWidth > 1024) document.addEventListener('DOMContentLoaded', function() {
@@ -755,25 +763,33 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
</div> </div>
<?php include('../include/footer.php'); ?> <?php include('../include/footer.php'); ?>
</div> </div>
<?php if ($deadline && !$isCompleted): ?>
<?php require __DIR__ . '/include/deadline_form_data.php'; ?>
<?php include __DIR__ . '/include/deadline_modal.php'; ?>
<?php endif; ?>
<?php include('../jsinclude.php'); ?> <?php include('../jsinclude.php'); ?>
<?php if ($deadline && !$isCompleted): ?> <?php if ($deadline && !$isCompleted): ?>
<script> <script>
$(document).ready(function() { $(document).ready(function() {
// Used by the shared modal JS to auto-open edit on "#edit"
window.SCAD_DETAIL_ID = <?= (int)$deadline['id'] ?>;
$('#btnModifica').on('click', function() { $('#btnModifica').on('click', function() {
window.location.href = 'scadenzario/index.php?edit=<?= (int)$deadline['id'] ?>'; window.openDeadlineEdit(<?= (int)$deadline['id'] ?>);
}); });
$('#btnCompleta').on('click', function() { function detailSubmitComplete(createNext, copyAttachments) {
Swal.fire({ var fd = new FormData();
title: 'Completare la scadenza?', fd.append('id', '<?= (int)$deadline['id'] ?>');
icon: 'question', fd.append('create_next', createNext ? '1' : '0');
showCancelButton: true, fd.append('copy_attachments', copyAttachments ? '1' : '0');
confirmButtonColor: '#198754',
cancelButtonText: 'Annulla', fetch('scadenzario/ajax/complete_deadline.php', {
confirmButtonText: 'Completa' method: 'POST',
}).then(function(result) { body: fd
if (result.isConfirmed) { })
fetch('scadenzario/ajax/complete_deadline.php?id=<?= (int)$deadline['id'] ?>')
.then(function(r) { .then(function(r) {
return r.json(); return r.json();
}) })
@@ -783,11 +799,15 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
icon: 'success', icon: 'success',
title: 'Completata', title: 'Completata',
text: data.message, text: data.message,
timer: 2500, timer: 1800,
showConfirmButton: false showConfirmButton: false
}) })
.then(function() { .then(function() {
if (data.new_id) {
window.location.href = 'scadenzario/detail.php?id=' + data.new_id + '#edit';
} else {
window.location.href = 'scadenzario/index.php'; window.location.href = 'scadenzario/index.php';
}
}); });
} else { } else {
Swal.fire('Errore', data.message, 'error'); Swal.fire('Errore', data.message, 'error');
@@ -797,10 +817,60 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
Swal.fire('Errore', 'Errore di connessione.', 'error'); Swal.fire('Errore', 'Errore di connessione.', 'error');
}); });
} }
$('#btnCompleta').on('click', function() {
var recurrence = <?= json_encode($deadline['recurrence_type'] ?? 'once') ?>;
var attCount = <?= count($attachments) ?>;
if (recurrence === 'once') {
Swal.fire({
title: 'Completare la scadenza?',
text: 'La scadenza verrà contrassegnata come completata.',
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#198754',
cancelButtonText: 'Annulla',
confirmButtonText: 'Completa',
reverseButtons: true
}).then(function(result) {
if (result.isConfirmed) {
detailSubmitComplete(false, false);
}
});
return;
}
var attCheckbox = attCount > 0 ?
'<div class="form-check d-flex align-items-center justify-content-center gap-2 mt-3">' +
'<input class="form-check-input" type="checkbox" id="swCopyAtt" checked>' +
'<label class="form-check-label" for="swCopyAtt">Copia gli allegati (' + attCount + ') sulla nuova scadenza</label>' +
'</div>' :
'';
Swal.fire({
title: 'Completare la scadenza?',
html: 'Vuoi creare automaticamente la prossima scadenza ricorrente?' + attCheckbox,
icon: 'question',
showCancelButton: true,
showDenyButton: true,
confirmButtonColor: '#198754',
denyButtonColor: '#6c757d',
confirmButtonText: 'Completa e crea la prossima',
denyButtonText: 'Completa senza nuova',
cancelButtonText: 'Annulla',
reverseButtons: true
}).then(function(result) {
if (result.isConfirmed) {
var copy = attCount > 0 ? document.getElementById('swCopyAtt').checked : false;
detailSubmitComplete(true, copy);
} else if (result.isDenied) {
detailSubmitComplete(false, false);
}
}); });
}); });
}); });
</script> </script>
<?php include __DIR__ . '/include/deadline_modal_js.php'; ?>
<?php endif; ?> <?php endif; ?>
</body> </body>
@@ -0,0 +1,34 @@
<?php
require_once(__DIR__ . '/../../ajax/auth_check.php');
header('Content-Type: application/json');
require_once(__DIR__ . '/../../../class/db-functions.php');
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : 0;
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
exit;
}
$stmt = $pdo->prepare("SELECT COUNT(*) FROM scad_deadlines WHERE function_id = ?");
$stmt->execute([$id]);
$inUse = (int)$stmt->fetchColumn();
if ($inUse > 0) {
echo json_encode([
'success' => false,
'message' => "Impossibile eliminare: la funzione è utilizzata in $inUse scadenz" . ($inUse === 1 ? 'a' : 'e') . '.',
]);
exit;
}
$pdo->prepare("DELETE FROM scad_functions WHERE id = ?")->execute([$id]);
echo json_encode(['success' => true, 'message' => 'Funzione eliminata.']);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
}
@@ -0,0 +1,63 @@
<?php
require_once(__DIR__ . '/../../ajax/auth_check.php');
header('Content-Type: application/json');
require_once(__DIR__ . '/../../../class/db-functions.php');
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : null;
$name = trim($_POST['name'] ?? '');
$description = trim($_POST['description'] ?? '') ?: null;
if ($name === '') {
echo json_encode(['success' => false, 'message' => 'Il nome è obbligatorio.']);
exit;
}
if (mb_strlen($name) > 255) {
echo json_encode(['success' => false, 'message' => 'Il nome supera 255 caratteri.']);
exit;
}
if ($id) {
$stmt = $pdo->prepare("SELECT id FROM scad_functions WHERE name = ? AND id <> ?");
$stmt->execute([$name, $id]);
} else {
$stmt = $pdo->prepare("SELECT id FROM scad_functions WHERE name = ?");
$stmt->execute([$name]);
}
if ($stmt->fetch()) {
echo json_encode(['success' => false, 'message' => 'Esiste già una funzione con questo nome.']);
exit;
}
if ($id) {
$stmt = $pdo->prepare("
UPDATE scad_functions
SET name = ?, description = ?
WHERE id = ?
");
$stmt->execute([$name, $description, $id]);
$savedId = $id;
} else {
$stmt = $pdo->prepare("
INSERT INTO scad_functions (name, description, status)
VALUES (?, ?, 'active')
");
$stmt->execute([$name, $description]);
$savedId = (int)$pdo->lastInsertId();
}
echo json_encode([
'success' => true,
'message' => $id ? 'Funzione aggiornata.' : 'Funzione creata.',
'id' => $savedId,
'name' => $name,
'description' => $description,
]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
}
@@ -0,0 +1,464 @@
<?php include('../../include/headscript.php'); ?>
<?php
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$functions = $pdo->query("
SELECT f.*,
(SELECT COUNT(*) FROM scad_deadlines d WHERE d.function_id = f.id) AS deadline_count,
(SELECT COUNT(*) FROM scad_deadlines d WHERE d.function_id = f.id AND d.status <> 'completed') AS open_count
FROM scad_functions f
ORDER BY f.name ASC
")->fetchAll(PDO::FETCH_ASSOC);
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<?php
$scriptDir = dirname($_SERVER['SCRIPT_NAME']);
$baseHref = dirname(dirname($scriptDir)) . '/';
?>
<base href="<?= $baseHref ?>">
<?php include('../../cssinclude.php'); ?>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<title>Scadenzario - Funzioni</title>
<script>
if (window.innerWidth > 1024) {
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('appWrapper').classList.add('toggled');
});
}
</script>
<style>
:root {
--scad-primary: #5a8fd8;
--scad-primary-hover: #4578c0;
--scad-heading: #2c3e6b;
--scad-card-bg: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%);
--scad-card-border: #dde4f0;
}
.scad-card {
border: none;
border-radius: 0.75rem;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
overflow: hidden;
}
.scad-card .card-header {
background: var(--scad-card-bg);
border-bottom: 1px solid var(--scad-card-border);
padding: 1rem 1.25rem;
}
.scad-card .card-header h5 {
font-weight: 700;
color: var(--scad-heading);
margin: 0;
font-size: 1.1rem;
}
.scad-card .card-body {
padding: 1.25rem;
}
.btn-scad-primary {
background: var(--scad-primary);
border: none;
color: #fff;
font-weight: 600;
font-size: 0.85rem;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
}
.btn-scad-primary:hover {
background: var(--scad-primary-hover);
color: #fff;
}
.btn-scad-outline {
background: transparent;
border: 1.5px solid var(--scad-primary);
color: var(--scad-primary);
font-weight: 600;
font-size: 0.85rem;
padding: 0.45rem 1rem;
border-radius: 0.5rem;
}
.btn-scad-outline:hover {
background: var(--scad-primary);
color: #fff;
}
.btn-action {
width: 32px;
height: 32px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 0.4rem;
font-size: 0.85rem;
}
.btn-action-edit {
background: rgba(90, 143, 216, 0.12);
color: var(--scad-primary);
}
.btn-action-edit:hover {
background: var(--scad-primary);
color: #fff;
}
.btn-action-delete {
background: rgba(220, 53, 69, 0.12);
color: #dc3545;
}
.btn-action-delete:hover {
background: #dc3545;
color: #fff;
}
.function-card {
background: #fff;
border: 1px solid var(--scad-card-border);
border-radius: 0.6rem;
padding: 0.85rem 0.95rem;
margin-bottom: 0.6rem;
}
.function-card .fc-name {
font-weight: 700;
color: var(--scad-heading);
font-size: 0.95rem;
}
.function-card .fc-stats {
display: flex;
gap: 0.75rem;
font-size: 0.8rem;
color: #6c757d;
margin: 0.5rem 0;
}
.function-card .fc-stats strong {
color: var(--scad-heading);
}
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: #6c757d;
}
.empty-state i {
font-size: 3rem;
opacity: 0.3;
margin-bottom: 1rem;
}
@media (max-width: 575.98px) {
.scad-card .card-header {
flex-direction: column;
gap: 0.75rem;
align-items: flex-start !important;
}
.header-actions {
width: 100%;
}
.header-actions .btn {
width: 100%;
justify-content: center;
}
}
</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">
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb" style="background:transparent;padding:0;margin:0;font-size:0.85rem">
<li class="breadcrumb-item"><a href="scadenzario/index.php">Scadenzario</a></li>
<li class="breadcrumb-item active" aria-current="page">Funzioni</li>
</ol>
</nav>
<div class="card scad-card">
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
<h5><i class="fa-solid fa-briefcase me-2"></i>Funzioni</h5>
<div class="header-actions d-flex gap-2 flex-wrap">
<a href="scadenzario/index.php" class="btn btn-scad-outline d-inline-flex align-items-center gap-2">
<i class="fa-solid fa-arrow-left"></i><span>Scadenzario</span>
</a>
<button class="btn btn-scad-primary d-inline-flex align-items-center gap-2" id="btnAddFunction">
<i class="fa-solid fa-plus"></i><span>Nuova Funzione</span>
</button>
</div>
</div>
<div class="card-body">
<?php if (count($functions) === 0): ?>
<div class="empty-state">
<i class="fa-solid fa-briefcase"></i>
<p>Nessuna funzione definita.<br>Clicca <strong>"Nuova Funzione"</strong> per aggiungere la prima.</p>
</div>
<?php else: ?>
<div id="functionsList">
<div class="d-md-none">
<?php foreach ($functions as $f): ?>
<div class="function-card"
data-id="<?= (int)$f['id'] ?>"
data-name="<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>"
data-description="<?= htmlspecialchars($f['description'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-status="<?= htmlspecialchars($f['status'], ENT_QUOTES, 'UTF-8') ?>"
data-in-use="<?= (int)$f['deadline_count'] ?>">
<div class="fc-name"><?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?></div>
<?php if (!empty($f['description'])): ?>
<div class="text-muted small mt-1"><?= htmlspecialchars($f['description'], ENT_QUOTES, 'UTF-8') ?></div>
<?php endif; ?>
<div class="fc-stats">
<span>Scadenze: <strong><?= (int)$f['deadline_count'] ?></strong></span>
<span>Aperte: <strong><?= (int)$f['open_count'] ?></strong></span>
</div>
<div class="d-flex gap-1 justify-content-end">
<button class="btn-action btn-action-edit btn-edit" title="Modifica">
<i class="fa-solid fa-pen"></i>
</button>
<button class="btn-action btn-action-delete btn-delete" title="Elimina">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="d-none d-md-block">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th>Nome</th>
<th>Descrizione</th>
<th class="text-center" style="width:120px">Scadenze</th>
<th class="text-center" style="width:120px">Aperte</th>
<th class="text-center" style="width:120px">Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($functions as $f): ?>
<tr data-id="<?= (int)$f['id'] ?>"
data-name="<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>"
data-description="<?= htmlspecialchars($f['description'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-status="<?= htmlspecialchars($f['status'], ENT_QUOTES, 'UTF-8') ?>"
data-in-use="<?= (int)$f['deadline_count'] ?>">
<td class="fw-semibold" style="color:var(--scad-heading)">
<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>
</td>
<td class="text-muted">
<?= htmlspecialchars($f['description'] ?? '—', ENT_QUOTES, 'UTF-8') ?>
</td>
<td class="text-center"><?= (int)$f['deadline_count'] ?></td>
<td class="text-center"><?= (int)$f['open_count'] ?></td>
<td class="text-center">
<div class="d-inline-flex gap-1">
<button class="btn-action btn-action-edit btn-edit" title="Modifica">
<i class="fa-solid fa-pen"></i>
</button>
<button class="btn-action btn-action-delete btn-delete" title="Elimina">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php include('../../include/footer.php'); ?>
</div>
<div class="modal fade" id="functionModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="functionModalTitle">Nuova Funzione</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<form id="functionForm">
<div class="modal-body">
<input type="hidden" id="functionId" name="id" value="">
<div class="mb-3">
<label for="functionName" class="form-label fw-semibold">Nome <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="functionName" name="name" maxlength="255" required>
</div>
<div class="mb-3">
<label for="functionDescription" class="form-label fw-semibold">Descrizione</label>
<textarea class="form-control" id="functionDescription" name="description" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-scad-primary">Salva</button>
</div>
</form>
</div>
</div>
</div>
<?php include('../../jsinclude.php'); ?>
<script>
$(function() {
function openModal(data) {
const isEdit = !!data;
$('#functionModalTitle').text(isEdit ? 'Modifica Funzione' : 'Nuova Funzione');
$('#functionId').val(isEdit ? data.id : '');
$('#functionName').val(isEdit ? data.name : '');
$('#functionDescription').val(isEdit ? data.description : '');
new bootstrap.Modal('#functionModal').show();
}
$('#btnAddFunction').on('click', function() {
openModal(null);
});
$('#functionsList').on('click', '.btn-edit', function() {
const $row = $(this).closest('[data-id]');
openModal({
id: $row.data('id'),
name: $row.data('name'),
description: $row.data('description')
});
});
$('#functionsList').on('click', '.btn-delete', function() {
const $row = $(this).closest('[data-id]');
const inUse = parseInt($row.data('in-use') || 0, 10);
const name = $row.data('name');
if (inUse > 0) {
Swal.fire({
icon: 'warning',
title: 'Impossibile eliminare',
text: `La funzione "${name}" è utilizzata in ${inUse} scadenz${inUse === 1 ? 'a' : 'e'}.`
});
return;
}
Swal.fire({
title: `Eliminare "${name}"?`,
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Elimina',
cancelButtonText: 'Annulla',
confirmButtonColor: '#dc3545'
}).then(function(result) {
if (!result.isConfirmed) return;
$.post('scadenzario/functions/ajax/delete_function.php', {
id: $row.data('id')
})
.done(function(res) {
if (res.success) {
location.reload();
} else {
Swal.fire({
icon: 'error',
title: 'Errore',
text: res.message
});
}
})
.fail(function() {
Swal.fire({
icon: 'error',
title: 'Errore di rete'
});
});
});
});
$('#functionForm').on('submit', function(e) {
e.preventDefault();
const payload = {
id: $('#functionId').val(),
name: $('#functionName').val().trim(),
description: $('#functionDescription').val().trim()
};
if (!payload.name) {
Swal.fire({
icon: 'warning',
title: 'Nome obbligatorio'
});
return;
}
$.post('scadenzario/functions/ajax/save_function.php', payload)
.done(function(res) {
if (res.success) {
location.reload();
} else {
Swal.fire({
icon: 'error',
title: 'Errore',
text: res.message
});
}
})
.fail(function() {
Swal.fire({
icon: 'error',
title: 'Errore di rete'
});
});
});
});
</script>
</body>
</html>
@@ -0,0 +1,41 @@
<?php
/**
* Shared data for the deadline modal form (used by index.php and detail.php).
* Populates $employees, $departments, $subjects. Safe to include more than once.
*/
if (!isset($pdo) || !$pdo) {
$pdo = DBHandlerSelect::getInstance()->getConnection();
}
if (!isset($employees)) {
$employees = $pdo->query("
SELECT e.id, e.first_name, e.last_name, e.department_id, dep.name AS department_name
FROM employees e
LEFT JOIN departments dep ON dep.id = e.department_id
WHERE e.status = 'active'
ORDER BY e.first_name, e.last_name
")->fetchAll(PDO::FETCH_ASSOC);
}
if (!isset($departments)) {
$departments = $pdo->query("
SELECT id, name, code, color
FROM departments
WHERE is_active = 1
ORDER BY sort_order ASC, name ASC
")->fetchAll(PDO::FETCH_ASSOC);
}
if (!isset($subjects)) {
$subjects = $pdo->query("SELECT id, name, color FROM scad_subjects ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
}
if (!isset($functions)) {
$functions = $pdo->query("
SELECT id, name
FROM scad_functions
WHERE status = 'active'
ORDER BY name ASC
")->fetchAll(PDO::FETCH_ASSOC);
}
@@ -0,0 +1,162 @@
<?php
/**
* Shared "Nuova/Modifica Scadenza" modal markup (used by index.php and detail.php).
* Requires $subjects, $departments, $employees in scope (see deadline_form_data.php).
* The accompanying JS lives in deadline_modal_js.php.
*/
?>
<!-- Deadline Modal -->
<div class="modal fade" id="deadlineModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl modal-fullscreen-sm-down">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalTitle">Nuova Scadenza</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<form id="deadlineForm">
<div class="modal-body">
<input type="hidden" id="dlId" name="id" value="">
<!-- Group 1: Informazioni principali -->
<div class="form-section-title">Informazioni principali</div>
<div class="row g-3 mb-4">
<div class="col-12 col-md-6">
<label for="dlSubject" class="form-label fw-semibold">Argomento</label>
<div class="d-flex gap-2">
<select class="form-select" id="dlSubject" name="subject_id" style="flex:1">
<option value=""> Nessuno </option>
<?php foreach ($subjects as $s): ?>
<option value="<?= (int)$s['id'] ?>" data-color="<?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
<a href="scadenzario/subjects/index.php" target="_blank" class="btn btn-scad-outline" title="Gestisci argomenti">
<i class="fa-solid fa-gear"></i>
</a>
</div>
</div>
<div class="col-12 col-md-6">
<label for="dlFunction" class="form-label fw-semibold">Funzione</label>
<div class="d-flex gap-2">
<select class="form-select" id="dlFunction" name="function_id" style="flex:1">
<option value=""> Nessuna </option>
<?php foreach ($functions as $fn): ?>
<option value="<?= (int)$fn['id'] ?>">
<?= htmlspecialchars($fn['name'], ENT_QUOTES, 'UTF-8') ?>
</option>
<?php endforeach; ?>
</select>
<a href="scadenzario/functions/index.php" target="_blank" class="btn btn-scad-outline" title="Gestisci funzioni">
<i class="fa-solid fa-gear"></i>
</a>
</div>
</div>
<div class="col-12 col-md-6">
<label for="dlLaw" class="form-label fw-semibold">Legge / Articolo</label>
<input type="text" class="form-control" id="dlLaw" name="law_regulation" maxlength="500" placeholder="es. D.Lgs. 81/2008, D.M. 10.03.1998...">
</div>
<div class="col-12">
<label for="dlTopic" class="form-label fw-semibold">Dettaglio <span class="text-danger">*</span></label>
<textarea class="form-control" id="dlTopic" name="topic" required maxlength="500" rows="2" placeholder="es. Verifica estintori, Autorizzazione trasporto rifiuti..."></textarea>
</div>
</div>
<!-- Group 2: Date e frequenza -->
<div class="form-section-title">Date e frequenza</div>
<div class="row g-3 mb-4">
<div class="col-12 col-md-4">
<label for="dlRecurrence" class="form-label fw-semibold">Periodicità</label>
<select class="form-select" id="dlRecurrence" name="recurrence_type">
<option value="once">Una tantum</option>
<option value="monthly">Mensile</option>
<option value="quarterly">Trimestrale</option>
<option value="semiannual">Semestrale</option>
<option value="annual">Annuale</option>
<option value="biennial">Biennale</option>
<option value="triennial">Triennale</option>
<option value="quadriennial">Quadriennale</option>
<option value="quinquennial">Quinquennale</option>
<option value="decennial">Decennale</option>
<option value="quindecennial">Quindicennale</option>
</select>
</div>
<div class="col-12 col-md-4">
<label for="dlDocDate" class="form-label fw-semibold">Data documento</label>
<input type="text" class="form-control js-date-it" id="dlDocDate" name="document_date" placeholder="gg/mm/aaaa" autocomplete="off">
</div>
<div class="col-12 col-md-4">
<label for="dlDueDate" class="form-label fw-semibold">Data scadenza <span class="text-danger">*</span></label>
<input type="text" class="form-control js-date-it" id="dlDueDate" name="due_date" placeholder="gg/mm/aaaa" autocomplete="off" required>
</div>
<div class="col-12 col-md-4">
<label for="dlCheckDate" class="form-label fw-semibold">Data ultimo controllo</label>
<input type="text" class="form-control js-date-it" id="dlCheckDate" name="check_date" placeholder="gg/mm/aaaa" autocomplete="off">
</div>
</div>
<!-- Group 3: Responsabili -->
<div class="form-section-title">Responsabili</div>
<div class="row g-3 mb-4">
<div class="col-12">
<label for="dlDepartments" class="form-label fw-semibold">Reparti</label>
<select class="form-select" id="dlDepartments" name="department_names[]" multiple>
<?php foreach ($departments as $dept): ?>
<option value="<?= htmlspecialchars($dept['name'], ENT_QUOTES, 'UTF-8') ?>">
<?= htmlspecialchars($dept['name'], ENT_QUOTES, 'UTF-8') ?>
<?= !empty($dept['code']) ? ' (' . htmlspecialchars($dept['code'], ENT_QUOTES, 'UTF-8') . ')' : '' ?>
</option>
<?php endforeach; ?>
</select>
<div class="form-text">Tutto il reparto sarà responsabile</div>
</div>
<div class="col-12">
<label for="dlEmployees" class="form-label fw-semibold">Singoli responsabili</label>
<select class="form-select" id="dlEmployees" name="employee_ids[]" multiple>
<?php foreach ($employees as $emp): ?>
<option value="<?= (int)$emp['id'] ?>">
<?= htmlspecialchars($emp['first_name'] . ' ' . $emp['last_name'], ENT_QUOTES, 'UTF-8') ?><?php if (!empty($emp['department_name'])): ?> (<?= htmlspecialchars($emp['department_name'], ENT_QUOTES, 'UTF-8') ?>)<?php endif; ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<!-- Group 4: Dettagli aggiuntivi -->
<div class="form-section-title">Dettagli aggiuntivi</div>
<div class="row g-3">
<div class="col-12 col-md-4">
<label for="dlNotifDays" class="form-label fw-semibold">Giorni preavviso</label>
<input type="number" class="form-control" id="dlNotifDays" name="notification_days" value="7" min="1" max="365">
</div>
<div class="col-12 col-md-8">
<label for="dlStorage" class="form-label fw-semibold">Luogo archiviazione</label>
<input type="text" class="form-control" id="dlStorage" name="storage_location" maxlength="500" placeholder="es. Armadio A3, Server/Documenti/Sicurezza...">
</div>
<div class="col-12">
<label for="dlNotes" class="form-label fw-semibold">Note</label>
<textarea class="form-control" id="dlNotes" name="notes" rows="3" placeholder="es. Scadenza 09/06/2026, Attività in appalto a Ditta specializzata..."></textarea>
</div>
</div>
<!-- Group 5: Allegati -->
<div class="form-section-title mt-4">Allegati</div>
<div id="attachmentsList" class="mb-3"></div>
<div class="row g-3">
<div class="col-12">
<label for="dlFiles" class="form-label fw-semibold">Carica file</label>
<input type="file" class="form-control" id="dlFiles" multiple>
<div class="form-text">Puoi selezionare più file contemporaneamente</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-scad-primary">
<i class="fa-solid fa-check me-1"></i> Salva
</button>
</div>
</form>
</div>
</div>
</div>
@@ -0,0 +1,99 @@
<?php
/**
* Shared styles for the deadline modal (deadline_modal.php).
* Relies on the --scad-* CSS variables defined on each page's :root.
*/
?>
<style>
.form-section-title {
font-size: 0.8rem;
font-weight: 700;
color: var(--scad-heading);
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 0.75rem;
padding-bottom: 0.4rem;
border-bottom: 2px solid #e8eeff;
}
#deadlineModal.modal {
position: fixed;
}
#deadlineModal .modal-content,
#deadlineModal .modal-body,
#deadlineModal .modal-footer {
background: #fff !important;
}
#deadlineModal .modal-header {
background: var(--scad-card-bg);
border-bottom: 1px solid var(--scad-card-border);
}
#deadlineModal .modal-title {
font-weight: 700;
color: var(--scad-heading);
}
/* Attachment list in modal */
.att-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 0.75rem;
background: #f8f9fa;
border-radius: 0.4rem;
margin-bottom: 0.4rem;
font-size: 0.85rem;
}
.att-item .att-name {
color: var(--scad-heading);
font-weight: 500;
word-break: break-all;
}
.att-item .att-actions {
display: flex;
gap: 0.4rem;
flex-shrink: 0;
margin-left: 0.5rem;
}
.att-item .att-actions a,
.att-item .att-actions button {
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 0.3rem;
border: none;
font-size: 0.75rem;
transition: all 0.15s;
}
.att-item .att-download {
background: #eef3ff;
color: var(--scad-primary);
text-decoration: none;
}
.att-item .att-download:hover {
background: var(--scad-primary);
color: #fff;
}
.att-item .att-remove {
background: #fff0f0;
color: var(--scad-red, #dc3545);
cursor: pointer;
}
.att-item .att-remove:hover {
background: var(--scad-red, #dc3545);
color: #fff;
}
</style>
@@ -0,0 +1,280 @@
<?php
/**
* Self-contained JS for the deadline modal (deadline_modal.php).
* Requires jQuery, Bootstrap, flatpickr, select2 and SweetAlert2 to be loaded first.
*
* Exposes:
* window.openDeadlineCreate() open the modal empty (new deadline)
* window.openDeadlineEdit(id) fetch a deadline and open the modal in edit mode
*
* Auto-open on load:
* #edit=<id> → opens edit for that id
* #edit → opens edit for window.SCAD_DETAIL_ID (used by detail.php)
*/
?>
<script>
$(document).ready(function() {
// --- Flatpickr date fields (visible dd/mm/yyyy, submitted yyyy-mm-dd) ---
var fpOptsDate = {
dateFormat: 'Y-m-d',
altInput: true,
altFormat: 'd/m/Y',
locale: 'it',
allowInput: true
};
var fpDocDate = flatpickr('#dlDocDate', fpOptsDate);
var fpDueDate = flatpickr('#dlDueDate', fpOptsDate);
var fpCheckDate = flatpickr('#dlCheckDate', fpOptsDate);
// --- Select2 ---
var s2Opts = {
theme: 'bootstrap-5',
allowClear: true,
dropdownParent: $('#deadlineModal .modal-body'),
language: 'it',
width: '100%'
};
$('#dlSubject').select2($.extend({}, s2Opts, { placeholder: 'Seleziona argomento...' }));
$('#dlDepartments').select2($.extend({}, s2Opts, { placeholder: 'Seleziona reparti...' }));
$('#dlEmployees').select2($.extend({}, s2Opts, { placeholder: 'Seleziona persone...' }));
$('#dlFunction').select2($.extend({}, s2Opts, { placeholder: 'Seleziona funzione...' }));
// --- Auto-calc due_date from document_date + recurrence ---
var RECURRENCE_OFFSETS = {
monthly: { months: 1 },
quarterly: { months: 3 },
semiannual: { months: 6 },
annual: { years: 1 },
biennial: { years: 2 },
triennial: { years: 3 },
quadriennial: { years: 4 },
quinquennial: { years: 5 },
decennial: { years: 10 },
quindecennial: { years: 15 }
};
function computeDueDate() {
var docVal = document.getElementById('dlDocDate').value;
var recurrence = document.getElementById('dlRecurrence').value;
var offset = RECURRENCE_OFFSETS[recurrence];
if (!docVal || !offset) return;
var d = new Date(docVal + 'T00:00:00');
if (isNaN(d.getTime())) return;
if (offset.months) d.setMonth(d.getMonth() + offset.months);
if (offset.years) d.setFullYear(d.getFullYear() + offset.years);
var iso = d.getFullYear() + '-' +
String(d.getMonth() + 1).padStart(2, '0') + '-' +
String(d.getDate()).padStart(2, '0');
fpDueDate.setDate(iso, true, 'Y-m-d');
}
$('#dlDocDate, #dlRecurrence').on('change', computeDueDate);
// --- Modal instance ---
var modal = new bootstrap.Modal(document.getElementById('deadlineModal'));
var form = document.getElementById('deadlineForm');
// --- Render attachments list ---
function renderAttachments(attachments) {
var container = document.getElementById('attachmentsList');
if (!attachments || attachments.length === 0) {
container.innerHTML = '<div class="text-muted" style="font-size:0.85rem">Nessun allegato</div>';
return;
}
container.innerHTML = attachments.map(function(a) {
return '<div class="att-item" data-att-id="' + a.id + '">' +
'<span class="att-name"><i class="fa-solid fa-paperclip me-1"></i>' + $('<span>').text(a.original_name).html() + '</span>' +
'<span class="att-actions">' +
'<a href="scadenzario/ajax/download_attachment.php?id=' + a.id + '" class="att-download" title="Scarica"><i class="fa-solid fa-download"></i></a>' +
'<button type="button" class="att-remove" title="Elimina" data-att-id="' + a.id + '"><i class="fa-solid fa-xmark"></i></button>' +
'</span></div>';
}).join('');
}
// --- Open modal (create) ---
window.openDeadlineCreate = function() {
form.reset();
document.getElementById('dlId').value = '';
document.getElementById('dlNotifDays').value = '7';
document.getElementById('modalTitle').textContent = 'Nuova Scadenza';
document.getElementById('dlFiles').value = '';
fpDocDate.clear();
fpDueDate.clear();
fpCheckDate.clear();
$('#dlSubject').val('').trigger('change');
$('#dlDepartments').val(null).trigger('change');
$('#dlEmployees').val(null).trigger('change');
$('#dlFunction').val('').trigger('change');
renderAttachments([]);
modal.show();
};
// --- Open modal (edit) ---
window.openDeadlineEdit = function(id) {
fetch('scadenzario/ajax/get_deadline.php?id=' + id)
.then(function(r) {
return r.json();
})
.then(function(data) {
if (!data.success) {
Swal.fire('Errore', data.message, 'error');
return;
}
var d = data.data;
document.getElementById('dlId').value = d.id;
$('#dlSubject').val(d.subject_id || '').trigger('change');
document.getElementById('dlTopic').value = d.topic || '';
$('#dlFunction').val(d.function_id || '').trigger('change');
document.getElementById('dlLaw').value = d.law_regulation || '';
document.getElementById('dlRecurrence').value = d.recurrence_type || 'once';
fpDocDate.setDate(d.document_date || null, false, 'Y-m-d');
fpDueDate.setDate(d.due_date || null, false, 'Y-m-d');
fpCheckDate.setDate(d.check_date || null, false, 'Y-m-d');
document.getElementById('dlNotifDays').value = d.notification_days || 7;
document.getElementById('dlStorage').value = d.storage_location || '';
document.getElementById('dlNotes').value = d.notes || '';
document.getElementById('dlFiles').value = '';
document.getElementById('modalTitle').textContent = 'Modifica Scadenza';
$('#dlDepartments').val(d.department_names || []).trigger('change');
if (Array.isArray(d.employee_ids)) {
$('#dlEmployees').val(d.employee_ids.map(String)).trigger('change');
} else {
$('#dlEmployees').val(null).trigger('change');
}
renderAttachments(d.attachments || []);
modal.show();
})
.catch(function() {
Swal.fire('Errore', 'Errore di connessione.', 'error');
});
};
// --- Save ---
var isSaving = false;
form.addEventListener('submit', function(e) {
e.preventDefault();
if (isSaving) return;
isSaving = true;
var saveBtn = form.querySelector('[type="submit"]');
saveBtn.disabled = true;
saveBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin me-1"></i> Salvataggio...';
var formData = new FormData(form);
fetch('scadenzario/ajax/save_deadline.php', {
method: 'POST',
body: formData
})
.then(function(r) {
return r.json();
})
.then(function(data) {
if (data.success) {
var deadlineId = data.id;
var fileInput = document.getElementById('dlFiles');
if (fileInput.files.length > 0) {
var fileData = new FormData();
fileData.append('deadline_id', deadlineId);
for (var i = 0; i < fileInput.files.length; i++) {
fileData.append('files[]', fileInput.files[i]);
}
return fetch('scadenzario/ajax/upload_attachment.php', {
method: 'POST',
body: fileData
})
.then(function(r) {
return r.json();
})
.then(function(upData) {
modal.hide();
Swal.fire({
icon: 'success',
title: 'Salvato',
text: data.message + ' ' + upData.message,
timer: 2000,
showConfirmButton: false
})
.then(function() {
location.reload();
});
});
} else {
modal.hide();
Swal.fire({
icon: 'success',
title: 'Salvato',
text: data.message,
timer: 1500,
showConfirmButton: false
})
.then(function() {
location.reload();
});
}
} else {
Swal.fire('Errore', data.message, 'error');
isSaving = false;
saveBtn.disabled = false;
saveBtn.innerHTML = '<i class="fa-solid fa-check me-1"></i> Salva';
}
})
.catch(function() {
Swal.fire('Errore', 'Errore di connessione.', 'error');
isSaving = false;
saveBtn.disabled = false;
saveBtn.innerHTML = '<i class="fa-solid fa-check me-1"></i> Salva';
});
});
// --- Delete attachment ---
$(document).on('click', '.att-remove', function(e) {
e.preventDefault();
var btn = $(this);
var attId = btn.data('att-id');
Swal.fire({
title: 'Rimuovere l\'allegato?',
text: 'Il collegamento verrà rimosso da questa scadenza. Il file resta disponibile se è usato da altre scadenze.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#dc3545',
cancelButtonText: 'Annulla',
confirmButtonText: 'Rimuovi',
reverseButtons: true
}).then(function(result) {
if (result.isConfirmed) {
fetch('scadenzario/ajax/delete_attachment.php?id=' + attId)
.then(function(r) {
return r.json();
})
.then(function(data) {
if (data.success) {
btn.closest('.att-item').remove();
if ($('#attachmentsList .att-item').length === 0) {
renderAttachments([]);
}
Swal.fire({
icon: 'success',
title: 'Fatto',
text: data.message,
timer: 1800,
showConfirmButton: false
});
} else {
Swal.fire('Errore', data.message, 'error');
}
});
}
});
});
// --- Auto-open from hash (#edit=ID or #edit for the current detail page) ---
var hash = window.location.hash;
var hashMatch = hash.match(/^#edit=(\d+)$/);
var autoEditId = hashMatch ? hashMatch[1] :
(hash === '#edit' && window.SCAD_DETAIL_ID ? window.SCAD_DETAIL_ID : null);
if (autoEditId) {
history.replaceState(null, '', window.location.pathname + window.location.search);
window.openDeadlineEdit(autoEditId);
}
});
</script>
@@ -1,4 +1,5 @@
<?php <?php
/** /**
* Renders two status banners for the current user: * Renders two status banners for the current user:
* - red -> overdue deadlines (scaduta) * - red -> overdue deadlines (scaduta)
@@ -43,10 +44,18 @@ if (!$_emp || ($_overdue === 0 && $_approaching === 0)) {
} }
?> ?>
<style> <style>
.my-deadlines-widgets { display: flex; gap: 0.75rem; margin-bottom: 1rem; flex-wrap: wrap; } .my-deadlines-widgets {
display: flex;
gap: 0.75rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.my-deadlines-widgets .mdw { .my-deadlines-widgets .mdw {
flex: 1 1 260px; flex: 1 1 260px;
display: flex; align-items: center; gap: 0.9rem; display: flex;
align-items: center;
gap: 0.9rem;
padding: 0.85rem 1rem; padding: 0.85rem 1rem;
border-radius: 0.6rem; border-radius: 0.6rem;
text-decoration: none; text-decoration: none;
@@ -54,18 +63,52 @@ if (!$_emp || ($_overdue === 0 && $_approaching === 0)) {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
transition: transform 0.15s, box-shadow 0.15s; transition: transform 0.15s, box-shadow 0.15s;
} }
.my-deadlines-widgets .mdw:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); color: #fff; }
.my-deadlines-widgets .mdw-red { background: linear-gradient(135deg, #dc3545 0%, #b02a37 100%); } .my-deadlines-widgets .mdw:hover {
.my-deadlines-widgets .mdw-orange { background: linear-gradient(135deg, #e8930c 0%, #c77a00 100%); } transform: translateY(-1px);
.my-deadlines-widgets .mdw-icon { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
width: 42px; height: 42px; border-radius: 50%; color: #fff;
display: flex; align-items: center; justify-content: center; }
background: rgba(255,255,255,0.22); font-size: 1.2rem; flex-shrink: 0;
.my-deadlines-widgets .mdw-red {
background: linear-gradient(135deg, #dc3545 0%, #b02a37 100%);
}
.my-deadlines-widgets .mdw-orange {
background: linear-gradient(135deg, #e8930c 0%, #c77a00 100%);
}
.my-deadlines-widgets .mdw-icon {
width: 42px;
height: 42px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.22);
font-size: 1.2rem;
flex-shrink: 0;
}
.my-deadlines-widgets .mdw-body {
flex: 1;
line-height: 1.2;
}
.my-deadlines-widgets .mdw-count {
font-size: 1.6rem;
font-weight: 700;
}
.my-deadlines-widgets .mdw-label {
font-size: 0.8rem;
opacity: 0.95;
}
.my-deadlines-widgets .mdw-arrow {
opacity: 0.7;
font-size: 0.9rem;
} }
.my-deadlines-widgets .mdw-body { flex: 1; line-height: 1.2; }
.my-deadlines-widgets .mdw-count { font-size: 1.6rem; font-weight: 700; }
.my-deadlines-widgets .mdw-label { font-size: 0.8rem; opacity: 0.95; }
.my-deadlines-widgets .mdw-arrow { opacity: 0.7; font-size: 0.9rem; }
</style> </style>
<div class="my-deadlines-widgets"> <div class="my-deadlines-widgets">
<?php if ($_overdue > 0): ?> <?php if ($_overdue > 0): ?>
@@ -73,7 +116,7 @@ if (!$_emp || ($_overdue === 0 && $_approaching === 0)) {
<span class="mdw-icon"><i class="fa-solid fa-triangle-exclamation"></i></span> <span class="mdw-icon"><i class="fa-solid fa-triangle-exclamation"></i></span>
<span class="mdw-body"> <span class="mdw-body">
<span class="mdw-count"><?= $_overdue ?></span> <span class="mdw-count"><?= $_overdue ?></span>
<span class="mdw-label d-block">Scadenz<?= $_overdue === 1 ? 'a' : 'e' ?> scadut<?= $_overdue === 1 ? 'a' : 'e' ?> — <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span> <span class="mdw-label d-block">Task<?= $_overdue === 1 ? '' : 's' ?> scadut<?= $_overdue === 1 ? 'o' : 'i' ?> — <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span>
</span> </span>
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span> <span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
</a> </a>
+103 -472
View File
@@ -37,12 +37,14 @@ $sql = "
SELECT d.*, SELECT d.*,
s.name AS subject_name, s.name AS subject_name,
s.color AS subject_color, s.color AS subject_color,
f.name AS function_name,
GROUP_CONCAT(DISTINCT CONCAT(e.first_name, ' ', e.last_name) ORDER BY e.first_name SEPARATOR ', ') as responsabili, GROUP_CONCAT(DISTINCT CONCAT(e.first_name, ' ', e.last_name) ORDER BY e.first_name SEPARATOR ', ') as responsabili,
GROUP_CONCAT(DISTINCT dep.name ORDER BY dep.name SEPARATOR ', ') as reparti_persone, GROUP_CONCAT(DISTINCT dep.name ORDER BY dep.name SEPARATOR ', ') as reparti_persone,
d.departments as reparti_assegnati, d.departments as reparti_assegnati,
(SELECT COUNT(*) FROM scad_deadline_attachments att WHERE att.deadline_id = d.id) as attachment_count (SELECT COUNT(*) FROM scad_deadline_attachments att WHERE att.deadline_id = d.id) as attachment_count
FROM scad_deadlines d FROM scad_deadlines d
LEFT JOIN scad_subjects s ON s.id = d.subject_id LEFT JOIN scad_subjects s ON s.id = d.subject_id
LEFT JOIN scad_functions f ON f.id = d.function_id
LEFT JOIN scad_deadline_employee de ON de.deadline_id = d.id LEFT JOIN scad_deadline_employee de ON de.deadline_id = d.id
LEFT JOIN employees e ON e.id = de.employee_id LEFT JOIN employees e ON e.id = de.employee_id
LEFT JOIN departments dep ON dep.id = e.department_id LEFT JOIN departments dep ON dep.id = e.department_id
@@ -69,27 +71,7 @@ $stmt = $pdo->prepare($sql);
$stmt->execute($params); $stmt->execute($params);
$deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC); $deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC);
$employees = $pdo->query(" require __DIR__ . '/include/deadline_form_data.php';
SELECT
e.id,
e.first_name,
e.last_name,
e.department_id,
dep.name AS department_name
FROM employees e
LEFT JOIN departments dep ON dep.id = e.department_id
WHERE e.status = 'active'
ORDER BY e.first_name, e.last_name
")->fetchAll(PDO::FETCH_ASSOC);
$departments = $pdo->query("
SELECT id, name, code, color
FROM departments
WHERE is_active = 1
ORDER BY sort_order ASC, name ASC
")->fetchAll(PDO::FETCH_ASSOC);
$subjects = $pdo->query("SELECT id, name, color FROM scad_subjects ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
$today = date('Y-m-d'); $today = date('Y-m-d');
@@ -494,7 +476,8 @@ function getContrastTextColor($hexColor)
} }
#deadlinesTable td:first-child { #deadlinesTable td:first-child {
max-width: 150px; max-width: 110px;
width: 110px;
} }
/* Attachment list in modal */ /* Attachment list in modal */
@@ -824,6 +807,9 @@ function getContrastTextColor($hexColor)
<a href="scadenzario/subjects/index.php" class="btn btn-scad-outline d-none d-md-inline-flex align-items-center gap-2"> <a href="scadenzario/subjects/index.php" class="btn btn-scad-outline d-none d-md-inline-flex align-items-center gap-2">
<i class="fa-solid fa-tags"></i><span>Argomenti</span> <i class="fa-solid fa-tags"></i><span>Argomenti</span>
</a> </a>
<a href="scadenzario/functions/index.php" class="btn btn-scad-outline d-none d-md-inline-flex align-items-center gap-2">
<i class="fa-solid fa-briefcase"></i><span>Funzioni</span>
</a>
<a href="scadenzario/calendar.php" class="btn btn-scad-outline d-none d-md-inline-flex align-items-center gap-2"> <a href="scadenzario/calendar.php" class="btn btn-scad-outline d-none d-md-inline-flex align-items-center gap-2">
<i class="fa-solid fa-calendar-days"></i><span>Calendario</span> <i class="fa-solid fa-calendar-days"></i><span>Calendario</span>
</a> </a>
@@ -842,6 +828,7 @@ function getContrastTextColor($hexColor)
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item d-flex align-items-center gap-2" href="scadenzario/subjects/index.php"><i class="fa-solid fa-tags"></i> Argomenti</a></li> <li><a class="dropdown-item d-flex align-items-center gap-2" href="scadenzario/subjects/index.php"><i class="fa-solid fa-tags"></i> Argomenti</a></li>
<li><a class="dropdown-item d-flex align-items-center gap-2" href="scadenzario/functions/index.php"><i class="fa-solid fa-briefcase"></i> Funzioni</a></li>
<li><a class="dropdown-item d-flex align-items-center gap-2" href="scadenzario/calendar.php"><i class="fa-solid fa-calendar-days"></i> Calendario</a></li> <li><a class="dropdown-item d-flex align-items-center gap-2" href="scadenzario/calendar.php"><i class="fa-solid fa-calendar-days"></i> Calendario</a></li>
<li><button type="button" class="dropdown-item d-flex align-items-center gap-2" id="btnStampaMobile"><i class="fa-solid fa-print"></i> Stampa</button></li> <li><button type="button" class="dropdown-item d-flex align-items-center gap-2" id="btnStampaMobile"><i class="fa-solid fa-print"></i> Stampa</button></li>
</ul> </ul>
@@ -923,7 +910,9 @@ function getContrastTextColor($hexColor)
data-department="<?= htmlspecialchars($row['reparti'] ?? '', ENT_QUOTES, 'UTF-8') ?>" data-department="<?= htmlspecialchars($row['reparti'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-employees="<?= htmlspecialchars($row['responsabili'] ?? '', ENT_QUOTES, 'UTF-8') ?>" data-employees="<?= htmlspecialchars($row['responsabili'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-due-date="<?= htmlspecialchars($row['due_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>" data-due-date="<?= htmlspecialchars($row['due_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-check-date="<?= htmlspecialchars($row['check_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"> data-check-date="<?= htmlspecialchars($row['check_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-recurrence="<?= htmlspecialchars($row['recurrence_type'] ?? 'once', ENT_QUOTES, 'UTF-8') ?>"
data-att-count="<?= (int)$row['attachment_count'] ?>">
<?php if (!empty($row['subject_name'])): ?> <?php if (!empty($row['subject_name'])): ?>
<div class="mb-1"><?php <div class="mb-1"><?php
$subjectBadgeBg = $row['subject_color'] ?: '#6c757d'; $subjectBadgeBg = $row['subject_color'] ?: '#6c757d';
@@ -972,11 +961,12 @@ function getContrastTextColor($hexColor)
<table id="deadlinesTable" class="table table-hover align-middle mb-0" style="width:100%"> <table id="deadlinesTable" class="table table-hover align-middle mb-0" style="width:100%">
<thead> <thead>
<tr> <tr>
<th>Argomento</th> <th style="width:110px">Argomento</th>
<th>Dettaglio</th> <th>Dettaglio</th>
<th class="d-none d-lg-table-cell">Legge/Art.</th> <th class="d-none d-lg-table-cell">Legge/Art.</th>
<th>Scadenza</th> <th>Scadenza</th>
<th class="d-none d-lg-table-cell">Verifica</th> <th class="d-none d-lg-table-cell">Verifica</th>
<th>Funzione</th>
<th>Responsabili</th> <th>Responsabili</th>
<th>Stato</th> <th>Stato</th>
<th class="text-center" style="width:120px">Azioni</th> <th class="text-center" style="width:120px">Azioni</th>
@@ -1000,7 +990,9 @@ function getContrastTextColor($hexColor)
data-department="<?= htmlspecialchars($row['reparti'] ?? '', ENT_QUOTES, 'UTF-8') ?>" data-department="<?= htmlspecialchars($row['reparti'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-employees="<?= htmlspecialchars($row['responsabili'] ?? '', ENT_QUOTES, 'UTF-8') ?>" data-employees="<?= htmlspecialchars($row['responsabili'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-due-date="<?= htmlspecialchars($row['due_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>" data-due-date="<?= htmlspecialchars($row['due_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-check-date="<?= htmlspecialchars($row['check_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"> data-check-date="<?= htmlspecialchars($row['check_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-recurrence="<?= htmlspecialchars($row['recurrence_type'] ?? 'once', ENT_QUOTES, 'UTF-8') ?>"
data-att-count="<?= (int)$row['attachment_count'] ?>">
<td> <td>
<?php if (!empty($row['subject_name'])): ?> <?php if (!empty($row['subject_name'])): ?>
<?php <?php
@@ -1014,6 +1006,7 @@ function getContrastTextColor($hexColor)
<span class="text-muted"></span> <span class="text-muted"></span>
<?php endif; ?> <?php endif; ?>
</td> </td>
<td> <td>
<a href="scadenzario/detail.php?id=<?= (int)$row['id'] ?>" class="fw-semibold text-decoration-none" style="color:var(--scad-heading)"><?= htmlspecialchars($row['topic'], ENT_QUOTES, 'UTF-8') ?></a> <a href="scadenzario/detail.php?id=<?= (int)$row['id'] ?>" class="fw-semibold text-decoration-none" style="color:var(--scad-heading)"><?= htmlspecialchars($row['topic'], ENT_QUOTES, 'UTF-8') ?></a>
<?php if ((int)$row['attachment_count'] > 0): ?> <?php if ((int)$row['attachment_count'] > 0): ?>
@@ -1023,6 +1016,17 @@ function getContrastTextColor($hexColor)
<td class="d-none d-lg-table-cell text-muted"><?= htmlspecialchars($row['law_regulation'] ?? '—', ENT_QUOTES, 'UTF-8') ?></td> <td class="d-none d-lg-table-cell text-muted"><?= htmlspecialchars($row['law_regulation'] ?? '—', ENT_QUOTES, 'UTF-8') ?></td>
<td><span class="text-nowrap"><?= $row['_dueFmt'] ?></span></td> <td><span class="text-nowrap"><?= $row['_dueFmt'] ?></span></td>
<td class="d-none d-lg-table-cell text-muted"><?= $row['_checkFmt'] ?></td> <td class="d-none d-lg-table-cell text-muted"><?= $row['_checkFmt'] ?></td>
<td>
<?php if (!empty($row['function_name'])): ?>
<span class="text-muted">
<i class="fa-solid fa-briefcase me-1"></i>
<?= htmlspecialchars($row['function_name'], ENT_QUOTES, 'UTF-8') ?>
</span>
<?php else: ?>
<span class="text-muted"></span>
<?php endif; ?>
</td>
<td> <td>
<?php if ($row['reparti']): ?><span class="text-muted"><i class="fa-regular fa-building me-1"></i><?= htmlspecialchars($row['reparti'], ENT_QUOTES, 'UTF-8') ?></span><?php endif; ?> <?php if ($row['reparti']): ?><span class="text-muted"><i class="fa-regular fa-building me-1"></i><?= htmlspecialchars($row['reparti'], ENT_QUOTES, 'UTF-8') ?></span><?php endif; ?>
<?php if ($row['reparti'] && $row['responsabili']): ?><br><?php endif; ?> <?php if ($row['reparti'] && $row['responsabili']): ?><br><?php endif; ?>
@@ -1055,143 +1059,7 @@ function getContrastTextColor($hexColor)
<?php include('../include/footer.php'); ?> <?php include('../include/footer.php'); ?>
</div> </div>
<!-- Deadline Modal --> <?php include __DIR__ . '/include/deadline_modal.php'; ?>
<div class="modal fade" id="deadlineModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl modal-fullscreen-sm-down">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalTitle">Nuova Scadenza</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<form id="deadlineForm">
<div class="modal-body">
<input type="hidden" id="dlId" name="id" value="">
<!-- Group 1: Informazioni principali -->
<div class="form-section-title">Informazioni principali</div>
<div class="row g-3 mb-4">
<div class="col-12 col-md-6">
<label for="dlSubject" class="form-label fw-semibold">Argomento</label>
<div class="d-flex gap-2">
<select class="form-select" id="dlSubject" name="subject_id" style="flex:1">
<option value=""> Nessuno </option>
<?php foreach ($subjects as $s): ?>
<option value="<?= (int)$s['id'] ?>" data-color="<?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
<a href="scadenzario/subjects/index.php" target="_blank" class="btn btn-scad-outline" title="Gestisci argomenti">
<i class="fa-solid fa-gear"></i>
</a>
</div>
</div>
<div class="col-12 col-md-6">
<label for="dlLaw" class="form-label fw-semibold">Legge / Articolo</label>
<input type="text" class="form-control" id="dlLaw" name="law_regulation" maxlength="500" placeholder="es. D.Lgs. 81/2008, D.M. 10.03.1998...">
</div>
<div class="col-12">
<label for="dlTopic" class="form-label fw-semibold">Dettaglio <span class="text-danger">*</span></label>
<textarea class="form-control" id="dlTopic" name="topic" required maxlength="500" rows="2" placeholder="es. Verifica estintori, Autorizzazione trasporto rifiuti..."></textarea>
</div>
</div>
<!-- Group 2: Date e frequenza -->
<div class="form-section-title">Date e frequenza</div>
<div class="row g-3 mb-4">
<div class="col-12 col-md-4">
<label for="dlRecurrence" class="form-label fw-semibold">Periodicità</label>
<select class="form-select" id="dlRecurrence" name="recurrence_type">
<option value="once">Una tantum</option>
<option value="monthly">Mensile</option>
<option value="quarterly">Trimestrale</option>
<option value="semiannual">Semestrale</option>
<option value="annual">Annuale</option>
<option value="biennial">Biennale</option>
<option value="triennial">Triennale</option>
<option value="quadriennial">Quadriennale</option>
<option value="quinquennial">Quinquennale</option>
<option value="decennial">Decennale</option>
<option value="quindecennial">Quindicennale</option>
</select>
</div>
<div class="col-12 col-md-4">
<label for="dlDocDate" class="form-label fw-semibold">Data documento</label>
<input type="date" class="form-control" id="dlDocDate" name="document_date">
</div>
<div class="col-12 col-md-4">
<label for="dlDueDate" class="form-label fw-semibold">Data scadenza <span class="text-danger">*</span></label>
<input type="date" class="form-control" id="dlDueDate" name="due_date" required>
</div>
<div class="col-12 col-md-4">
<label for="dlCheckDate" class="form-label fw-semibold">Data ultimo controllo</label>
<input type="date" class="form-control" id="dlCheckDate" name="check_date">
</div>
</div>
<!-- Group 3: Responsabili -->
<div class="form-section-title">Responsabili</div>
<div class="row g-3 mb-4">
<div class="col-12">
<label for="dlDepartments" class="form-label fw-semibold">Reparti</label>
<select class="form-select" id="dlDepartments" name="department_names[]" multiple>
<?php foreach ($departments as $dept): ?>
<option value="<?= htmlspecialchars($dept['name'], ENT_QUOTES, 'UTF-8') ?>">
<?= htmlspecialchars($dept['name'], ENT_QUOTES, 'UTF-8') ?>
<?= !empty($dept['code']) ? ' (' . htmlspecialchars($dept['code'], ENT_QUOTES, 'UTF-8') . ')' : '' ?>
</option>
<?php endforeach; ?>
</select>
<div class="form-text">Tutto il reparto sarà responsabile</div>
</div>
<div class="col-12">
<label for="dlEmployees" class="form-label fw-semibold">Singoli responsabili</label>
<select class="form-select" id="dlEmployees" name="employee_ids[]" multiple>
<?php foreach ($employees as $emp): ?>
<option value="<?= (int)$emp['id'] ?>">
<?= htmlspecialchars($emp['first_name'] . ' ' . $emp['last_name'], ENT_QUOTES, 'UTF-8') ?><?php if (!empty($emp['department_name'])): ?> (<?= htmlspecialchars($emp['department_name'], ENT_QUOTES, 'UTF-8') ?>)<?php endif; ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<!-- Group 4: Dettagli aggiuntivi -->
<div class="form-section-title">Dettagli aggiuntivi</div>
<div class="row g-3">
<div class="col-12 col-md-4">
<label for="dlNotifDays" class="form-label fw-semibold">Giorni preavviso</label>
<input type="number" class="form-control" id="dlNotifDays" name="notification_days" value="7" min="1" max="365">
</div>
<div class="col-12 col-md-8">
<label for="dlStorage" class="form-label fw-semibold">Luogo archiviazione</label>
<input type="text" class="form-control" id="dlStorage" name="storage_location" maxlength="500" placeholder="es. Armadio A3, Server/Documenti/Sicurezza...">
</div>
<div class="col-12">
<label for="dlNotes" class="form-label fw-semibold">Note</label>
<textarea class="form-control" id="dlNotes" name="notes" rows="3" placeholder="es. Scadenza 09/06/2026, Attività in appalto a Ditta specializzata..."></textarea>
</div>
</div>
<!-- Group 5: Allegati -->
<div class="form-section-title mt-4">Allegati</div>
<div id="attachmentsList" class="mb-3"></div>
<div class="row g-3">
<div class="col-12">
<label for="dlFiles" class="form-label fw-semibold">Carica file</label>
<input type="file" class="form-control" id="dlFiles" multiple>
<div class="form-text">Puoi selezionare più file contemporaneamente</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-scad-primary">
<i class="fa-solid fa-check me-1"></i> Salva
</button>
</div>
</form>
</div>
</div>
</div>
<?php include('../jsinclude.php'); ?> <?php include('../jsinclude.php'); ?>
<script src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js"></script> <script src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js"></script>
@@ -1221,84 +1089,6 @@ function getContrastTextColor($hexColor)
var fpDue = flatpickr('#filterDueRange', fpOpts); var fpDue = flatpickr('#filterDueRange', fpOpts);
var fpCheck = flatpickr('#filterCheckRange', fpOpts); var fpCheck = flatpickr('#filterCheckRange', fpOpts);
// --- Select2 ---
$('#dlSubject').select2({
theme: 'bootstrap-5',
placeholder: 'Seleziona argomento...',
allowClear: true,
dropdownParent: $('#deadlineModal .modal-body'),
language: 'it',
width: '100%'
});
$('#dlDepartments').select2({
theme: 'bootstrap-5',
placeholder: 'Seleziona reparti...',
allowClear: true,
dropdownParent: $('#deadlineModal .modal-body'),
language: 'it',
width: '100%'
});
$('#dlEmployees').select2({
theme: 'bootstrap-5',
placeholder: 'Seleziona persone...',
allowClear: true,
dropdownParent: $('#deadlineModal .modal-body'),
language: 'it',
width: '100%'
});
// --- Auto-calc due_date from document_date + recurrence ---
var RECURRENCE_OFFSETS = {
monthly: {
months: 1
},
quarterly: {
months: 3
},
semiannual: {
months: 6
},
annual: {
years: 1
},
biennial: {
years: 2
},
triennial: {
years: 3
},
quadriennial: {
years: 4
},
quinquennial: {
years: 5
},
decennial: {
years: 10
},
quindecennial: {
years: 15
}
};
function computeDueDate() {
var docVal = document.getElementById('dlDocDate').value;
var recurrence = document.getElementById('dlRecurrence').value;
var offset = RECURRENCE_OFFSETS[recurrence];
if (!docVal || !offset) return;
var d = new Date(docVal + 'T00:00:00');
if (isNaN(d.getTime())) return;
if (offset.months) d.setMonth(d.getMonth() + offset.months);
if (offset.years) d.setFullYear(d.getFullYear() + offset.years);
var iso = d.getFullYear() + '-' +
String(d.getMonth() + 1).padStart(2, '0') + '-' +
String(d.getDate()).padStart(2, '0');
document.getElementById('dlDueDate').value = iso;
}
$('#dlDocDate, #dlRecurrence').on('change', computeDueDate);
// --- DataTables custom filters --- // --- DataTables custom filters ---
$.fn.dataTable.ext.search.push(function(settings, data, dataIndex) { $.fn.dataTable.ext.search.push(function(settings, data, dataIndex) {
if (settings.nTable.id !== 'deadlinesTable') return true; if (settings.nTable.id !== 'deadlinesTable') return true;
@@ -1460,148 +1250,8 @@ function getContrastTextColor($hexColor)
// Apply default filter on load // Apply default filter on load
applyFiltersRefresh(); applyFiltersRefresh();
// --- Modal ---
var modal = new bootstrap.Modal(document.getElementById('deadlineModal'));
var form = document.getElementById('deadlineForm');
// Add
document.getElementById('btnAddDeadline').addEventListener('click', function() { document.getElementById('btnAddDeadline').addEventListener('click', function() {
form.reset(); if (window.openDeadlineCreate) window.openDeadlineCreate();
document.getElementById('dlId').value = '';
document.getElementById('dlNotifDays').value = '7';
document.getElementById('modalTitle').textContent = 'Nuova Scadenza';
document.getElementById('dlFiles').value = '';
$('#dlSubject').val('').trigger('change');
$('#dlDepartments').val(null).trigger('change');
$('#dlEmployees').val(null).trigger('change');
renderAttachments([]);
modal.show();
});
// Save
var isSaving = false;
form.addEventListener('submit', function(e) {
e.preventDefault();
if (isSaving) return;
isSaving = true;
var saveBtn = form.querySelector('[type="submit"]');
saveBtn.disabled = true;
saveBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin me-1"></i> Salvataggio...';
var formData = new FormData(form);
fetch('scadenzario/ajax/save_deadline.php', {
method: 'POST',
body: formData
})
.then(function(r) {
return r.json();
})
.then(function(data) {
if (data.success) {
var deadlineId = data.id;
var fileInput = document.getElementById('dlFiles');
if (fileInput.files.length > 0) {
// Upload files
var fileData = new FormData();
fileData.append('deadline_id', deadlineId);
for (var i = 0; i < fileInput.files.length; i++) {
fileData.append('files[]', fileInput.files[i]);
}
return fetch('scadenzario/ajax/upload_attachment.php', {
method: 'POST',
body: fileData
})
.then(function(r) {
return r.json();
})
.then(function(upData) {
modal.hide();
Swal.fire({
icon: 'success',
title: 'Salvato',
text: data.message + ' ' + upData.message,
timer: 2000,
showConfirmButton: false
})
.then(function() {
location.reload();
});
});
} else {
modal.hide();
Swal.fire({
icon: 'success',
title: 'Salvato',
text: data.message,
timer: 1500,
showConfirmButton: false
})
.then(function() {
location.reload();
});
}
} else {
Swal.fire('Errore', data.message, 'error');
isSaving = false;
saveBtn.disabled = false;
saveBtn.innerHTML = '<i class="fa-solid fa-check me-1"></i> Salva';
}
})
.catch(function() {
Swal.fire('Errore', 'Errore di connessione.', 'error');
isSaving = false;
saveBtn.disabled = false;
saveBtn.innerHTML = '<i class="fa-solid fa-check me-1"></i> Salva';
});
});
// Render attachments list
function renderAttachments(attachments) {
var container = document.getElementById('attachmentsList');
if (!attachments || attachments.length === 0) {
container.innerHTML = '<div class="text-muted" style="font-size:0.85rem">Nessun allegato</div>';
return;
}
container.innerHTML = attachments.map(function(a) {
return '<div class="att-item" data-att-id="' + a.id + '">' +
'<span class="att-name"><i class="fa-solid fa-paperclip me-1"></i>' + $('<span>').text(a.original_name).html() + '</span>' +
'<span class="att-actions">' +
'<a href="scadenzario/ajax/download_attachment.php?id=' + a.id + '" class="att-download" title="Scarica"><i class="fa-solid fa-download"></i></a>' +
'<button type="button" class="att-remove" title="Elimina" data-att-id="' + a.id + '"><i class="fa-solid fa-xmark"></i></button>' +
'</span></div>';
}).join('');
}
// Delete attachment
$(document).on('click', '.att-remove', function(e) {
e.preventDefault();
var btn = $(this);
var attId = btn.data('att-id');
Swal.fire({
title: 'Eliminare allegato?',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#dc3545',
cancelButtonText: 'Annulla',
confirmButtonText: 'Elimina'
}).then(function(result) {
if (result.isConfirmed) {
fetch('scadenzario/ajax/delete_attachment.php?id=' + attId)
.then(function(r) {
return r.json();
})
.then(function(data) {
if (data.success) {
btn.closest('.att-item').remove();
if ($('#attachmentsList .att-item').length === 0) {
renderAttachments([]);
}
} else {
Swal.fire('Errore', data.message, 'error');
}
});
}
});
}); });
// Edit with confirmation // Edit with confirmation
@@ -1618,69 +1268,23 @@ function getContrastTextColor($hexColor)
confirmButtonText: 'Sì, modifica', confirmButtonText: 'Sì, modifica',
reverseButtons: true reverseButtons: true
}).then(function(result) { }).then(function(result) {
if (!result.isConfirmed) { if (result.isConfirmed && window.openDeadlineEdit) {
return; window.openDeadlineEdit(id);
} }
fetch('scadenzario/ajax/get_deadline.php?id=' + id)
.then(function(r) {
return r.json();
})
.then(function(data) {
if (!data.success) {
Swal.fire('Errore', data.message, 'error');
return;
}
var d = data.data;
document.getElementById('dlId').value = d.id;
$('#dlSubject').val(d.subject_id || '').trigger('change');
document.getElementById('dlTopic').value = d.topic || '';
document.getElementById('dlLaw').value = d.law_regulation || '';
document.getElementById('dlRecurrence').value = d.recurrence_type || 'once';
document.getElementById('dlDocDate').value = d.document_date || '';
document.getElementById('dlDueDate').value = d.due_date || '';
document.getElementById('dlCheckDate').value = d.check_date || '';
document.getElementById('dlNotifDays').value = d.notification_days || 7;
document.getElementById('dlStorage').value = d.storage_location || '';
document.getElementById('dlNotes').value = d.notes || '';
document.getElementById('dlFiles').value = '';
document.getElementById('modalTitle').textContent = 'Modifica Scadenza';
$('#dlDepartments').val(d.department_names || []).trigger('change');
if (Array.isArray(d.employee_ids)) {
$('#dlEmployees').val(d.employee_ids.map(String)).trigger('change');
} else {
$('#dlEmployees').val(null).trigger('change');
}
renderAttachments(d.attachments || []);
modal.show();
})
.catch(function() {
Swal.fire('Errore', 'Errore di connessione.', 'error');
});
}); });
}); });
// Complete // Complete
$(document).on('click', '.btn-complete', function() { function submitComplete(id, createNext, copyAttachments) {
var el = $(this).closest('[data-id]'); var fd = new FormData();
var id = el.data('id'); fd.append('id', id);
Swal.fire({ fd.append('create_next', createNext ? '1' : '0');
title: 'Completare la scadenza?', fd.append('copy_attachments', copyAttachments ? '1' : '0');
icon: 'question',
showCancelButton: true, fetch('scadenzario/ajax/complete_deadline.php', {
confirmButtonColor: '#198754', method: 'POST',
cancelButtonText: 'Annulla', body: fd
confirmButtonText: 'Completa' })
}).then(function(result) {
if (result.isConfirmed) {
fetch('scadenzario/ajax/complete_deadline.php?id=' + id)
.then(function(r) { .then(function(r) {
return r.json(); return r.json();
}) })
@@ -1690,11 +1294,16 @@ function getContrastTextColor($hexColor)
icon: 'success', icon: 'success',
title: 'Completata', title: 'Completata',
text: data.message, text: data.message,
timer: 2500, timer: 1800,
showConfirmButton: false showConfirmButton: false
}) })
.then(function() { .then(function() {
// Open the new deadline's detail page with the edit modal auto-opening
if (data.new_id) {
window.location = 'scadenzario/detail.php?id=' + data.new_id + '#edit';
} else {
location.reload(); location.reload();
}
}); });
} else { } else {
Swal.fire('Errore', data.message, 'error'); Swal.fire('Errore', data.message, 'error');
@@ -1704,6 +1313,59 @@ function getContrastTextColor($hexColor)
Swal.fire('Errore', 'Errore di connessione.', 'error'); Swal.fire('Errore', 'Errore di connessione.', 'error');
}); });
} }
$(document).on('click', '.btn-complete', function() {
var el = $(this).closest('[data-id]');
var id = el.data('id');
var recurrence = el.data('recurrence') || 'once';
var attCount = parseInt(el.data('att-count'), 10) || 0;
// Non-recurring: simple confirm, no new deadline is created
if (recurrence === 'once') {
Swal.fire({
title: 'Completare la scadenza?',
text: 'La scadenza verrà contrassegnata come completata.',
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#198754',
cancelButtonText: 'Annulla',
confirmButtonText: 'Completa',
reverseButtons: true
}).then(function(result) {
if (result.isConfirmed) {
submitComplete(id, false, false);
}
});
return;
}
// Recurring: ask whether to create the next deadline; optionally carry attachments over
var attCheckbox = attCount > 0 ?
'<div class="form-check d-flex align-items-center justify-content-center gap-2 mt-3">' +
'<input class="form-check-input" type="checkbox" id="swCopyAtt" checked>' +
'<label class="form-check-label" for="swCopyAtt">Copia gli allegati (' + attCount + ') sulla nuova scadenza</label>' +
'</div>' :
'';
Swal.fire({
title: 'Completare la scadenza?',
html: 'Vuoi creare automaticamente la prossima scadenza ricorrente?' + attCheckbox,
icon: 'question',
showCancelButton: true,
showDenyButton: true,
confirmButtonColor: '#198754',
denyButtonColor: '#6c757d',
confirmButtonText: 'Completa e crea la prossima',
denyButtonText: 'Completa senza nuova',
cancelButtonText: 'Annulla',
reverseButtons: true
}).then(function(result) {
if (result.isConfirmed) {
var copy = attCount > 0 ? document.getElementById('swCopyAtt').checked : false;
submitComplete(id, true, copy);
} else if (result.isDenied) {
submitComplete(id, false, false);
}
}); });
}); });
@@ -1748,38 +1410,6 @@ function getContrastTextColor($hexColor)
}); });
}); });
// Auto-open edit modal from ?edit=ID
var urlParams = new URLSearchParams(window.location.search);
var editId = urlParams.get('edit');
if (editId) {
history.replaceState(null, '', 'scadenzario/index.php');
fetch('scadenzario/ajax/get_deadline.php?id=' + editId)
.then(function(r) {
return r.json();
})
.then(function(data) {
if (!data.success) return;
var d = data.data;
document.getElementById('dlId').value = d.id;
$('#dlSubject').val(d.subject_id || '').trigger('change');
document.getElementById('dlTopic').value = d.topic || '';
document.getElementById('dlLaw').value = d.law_regulation || '';
document.getElementById('dlRecurrence').value = d.recurrence_type || 'once';
document.getElementById('dlDocDate').value = d.document_date || '';
document.getElementById('dlDueDate').value = d.due_date || '';
document.getElementById('dlCheckDate').value = d.check_date || '';
document.getElementById('dlNotifDays').value = d.notification_days || 7;
document.getElementById('dlStorage').value = d.storage_location || '';
document.getElementById('dlNotes').value = d.notes || '';
document.getElementById('dlFiles').value = '';
document.getElementById('modalTitle').textContent = 'Modifica Scadenza';
$('#dlDepartments').val(d.department_names || []).trigger('change');
$('#dlEmployees').val(d.employee_ids.map(String)).trigger('change');
renderAttachments(d.attachments || []);
modal.show();
});
}
// Stampa // Stampa
function doStampa() { function doStampa() {
var params = []; var params = [];
@@ -1802,6 +1432,7 @@ function getContrastTextColor($hexColor)
if (btnStampaMobile) btnStampaMobile.addEventListener('click', doStampa); if (btnStampaMobile) btnStampaMobile.addEventListener('click', doStampa);
}); });
</script> </script>
<?php include __DIR__ . '/include/deadline_modal_js.php'; ?>
</body> </body>
</html> </html>
+1 -1
View File
@@ -208,7 +208,7 @@ while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) {
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -2
View File
@@ -130,7 +130,6 @@ $tools = $pdo->query("
<title>Gestione Skills</title> <title>Gestione Skills</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css"> <link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
@@ -177,7 +176,7 @@ $tools = $pdo->query("
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -2
View File
@@ -11,7 +11,6 @@
<!-- jQuery e Bootstrap --> <!-- jQuery e Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables --> <!-- DataTables -->
@@ -119,7 +118,7 @@
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
-1
View File
@@ -13,7 +13,6 @@
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Bootstrap (se già incluso puoi rimuoverlo) --> <!-- Bootstrap (se già incluso puoi rimuoverlo) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- SweetAlert2 --> <!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
+217
View File
@@ -0,0 +1,217 @@
<?php
include('include/headscript.php');
$pdo = DBHandlerSelect::getInstance()->getConnection();
/* ==========================================
PERMISSIONS (mirror trainings.php)
========================================== */
$isHrManager = Auth::user()->hasRole('Admin')
|| Auth::user()->hasRole('Superuser')
|| Auth::user()->hasRole('employee-hr')
|| Auth::user()->hasRole('manager');
if (!$isHrManager) {
header('Location: employee-profile.php');
exit;
}
/* Dropdown data */
$employees = $pdo->query("
SELECT id, first_name, last_name, employee_code
FROM employees
ORDER BY last_name, first_name
")->fetchAll(PDO::FETCH_ASSOC);
$topics = $pdo->query("
SELECT id, name FROM training_topics WHERE is_active = 1 ORDER BY sort_order, name
")->fetchAll(PDO::FETCH_ASSOC);
$departments = $pdo->query("
SELECT id, name FROM departments WHERE is_active = 1 ORDER BY sort_order, name
")->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'); ?>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.9/index.global.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.9/locales/it.global.min.js"></script>
<title>Calendario Formazione - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<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;
}
.legend { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 1rem; }
.legend-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; color: #64748b; }
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
/* FullCalendar overrides */
.fc { font-size: 0.95rem; }
.fc .fc-toolbar-title { font-size: 1.15rem; font-weight: 700; color: #2c3e6b; }
.fc .fc-button-primary {
background: #5a8fd8; border-color: #5a8fd8;
font-weight: 600; font-size: 0.82rem; border-radius: 0.4rem;
}
.fc .fc-button-primary:hover { background: #4578c0; border-color: #4578c0; }
.fc .fc-button-primary:disabled { background: #9bbce6; border-color: #9bbce6; }
.fc .fc-button-primary:not(:disabled).fc-button-active { background: #2c3e6b; border-color: #2c3e6b; }
.fc .fc-daygrid-day-number { color: #2c3e6b; font-weight: 500; }
.fc .fc-daygrid-day.fc-day-today { background: #f0f4ff; }
.fc .fc-event { border-radius: 0.3rem; padding: 2px 4px; font-weight: 600; cursor: pointer; }
.fc .fc-event:hover { filter: brightness(0.92); }
.fc .fc-list-event:hover td { background: #f0f4ff; }
@media (max-width: 767.98px) {
.card-header { flex-direction: column; align-items: flex-start !important; gap: .5rem; }
.fc .fc-toolbar { flex-direction: column; gap: 0.5rem; }
.fc .fc-toolbar-title { font-size: 1rem; }
}
</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">📅 Calendario Formazione</h5>
<div class="d-flex gap-2 flex-wrap">
<a href="trainings.php" class="btn btn-light border d-inline-flex align-items-center gap-2">
📚 <span>Storico Formazione</span>
</a>
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
↩️ Torna alla Dashboard
</button>
</div>
</div>
<div class="card-body">
<!-- FILTERS -->
<div class="row g-2 mb-3">
<div class="col-6 col-md-3">
<label class="form-label small text-muted mb-1">Stato</label>
<select id="filterStatus" class="form-select">
<option value="">Tutti</option>
<option value="expired">Scaduti</option>
<option value="due_soon">Da aggiornare</option>
<option value="compliant">Conformi</option>
</select>
</div>
<div class="col-6 col-md-3">
<label class="form-label small text-muted mb-1">Corso</label>
<select id="filterTopic" class="form-select">
<option value="">Tutti</option>
<?php foreach ($topics as $t): ?>
<option value="<?= (int)$t['id'] ?>"><?= htmlspecialchars($t['name'], ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-6 col-md-3">
<label class="form-label small text-muted mb-1">Reparto</label>
<select id="filterDepartment" class="form-select">
<option value="">Tutti</option>
<?php foreach ($departments as $d): ?>
<option value="<?= (int)$d['id'] ?>"><?= htmlspecialchars($d['name'], ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-6 col-md-3">
<label class="form-label small text-muted mb-1">Dipendente</label>
<div class="d-flex gap-2">
<select id="filterEmployee" class="form-select">
<option value="">Tutti</option>
<?php foreach ($employees as $e): ?>
<option value="<?= (int)$e['id'] ?>"><?= htmlspecialchars(trim($e['last_name'] . ' ' . $e['first_name']), ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
<button id="btnResetFilters" type="button" class="btn btn-light border flex-shrink-0" title="Reset filtri">
<i class="fa-solid fa-rotate-left"></i>
</button>
</div>
</div>
</div>
<!-- LEGEND -->
<div class="legend">
<div class="legend-item"><span class="legend-dot" style="background:#dc3545"></span> Scaduto</div>
<div class="legend-item"><span class="legend-dot" style="background:#e8930c"></span> Da aggiornare</div>
<div class="legend-item"><span class="legend-dot" style="background:#198754"></span> Conforme</div>
</div>
<div id="calendar"></div>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
var isMobile = window.innerWidth < 768;
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
locale: 'it',
initialView: isMobile ? 'listMonth' : 'dayGridMonth',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: isMobile ? 'listMonth,dayGridMonth' : 'dayGridMonth,listMonth'
},
height: 'auto',
navLinks: true,
eventSources: [{
url: 'ajax/trainings/calendar_events.php',
extraParams: function() {
return {
status: document.getElementById('filterStatus').value,
topic_id: document.getElementById('filterTopic').value,
department_id: document.getElementById('filterDepartment').value,
employee_id: document.getElementById('filterEmployee').value
};
},
failure: function() {
if (window.Swal) Swal.fire('Errore', 'Impossibile caricare gli eventi.', 'error');
}
}],
eventClick: function(info) {
info.jsEvent.preventDefault();
if (info.event.url) window.location.href = info.event.url;
},
windowResize: function() {
calendar.changeView(window.innerWidth < 768 ? 'listMonth' : 'dayGridMonth');
}
});
calendar.render();
document.querySelectorAll('#filterStatus, #filterTopic, #filterDepartment, #filterEmployee').forEach(function(el) {
el.addEventListener('change', function() { calendar.refetchEvents(); });
});
document.getElementById('btnResetFilters').addEventListener('click', function() {
['filterStatus', 'filterTopic', 'filterDepartment', 'filterEmployee'].forEach(function(id) {
document.getElementById(id).value = '';
});
calendar.refetchEvents();
});
});
</script>
</body>
</html>
+530
View File
@@ -0,0 +1,530 @@
<?php
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
/* ==========================================
PAGE DATA
========================================== */
$sql = "
SELECT tt.*,
(SELECT COUNT(*) FROM employee_trainings et WHERE et.training_topic_id = tt.id) AS trainings_count
FROM training_topics tt
ORDER BY tt.sort_order ASC, tt.name ASC
";
$topics = $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 Corsi di Formazione - <?= 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; }
#tabellaTopics 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: 280px; white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; text-align: left;
}
.num-pill {
display: inline-block; padding: 2px 10px; border-radius: 999px;
background: #eef2ff; color: #3730a3; font-weight: 600; font-size: 0.85rem;
}
@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%; }
}
.tt-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);
}
.tt-card-title {
font-size: 1.1rem;
font-weight: 600;
color: #1f2937;
margin: 0 0 4px 0;
word-break: break-word;
}
.tt-card-desc {
color: #475569;
font-size: 0.95rem;
margin: 0 0 10px 0;
word-break: break-word;
}
.tt-card-meta {
display: flex;
flex-wrap: wrap;
gap: 8px 14px;
font-size: 0.85rem;
color: #64748b;
margin-bottom: 12px;
}
.tt-card-meta b { color: #1f2937; font-weight: 600; }
.tt-card-actions {
display: flex;
gap: 8px;
}
.tt-card-actions .btn { flex: 1; }
.tt-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 Corsi di Formazione</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 Corsi / Training Topics</h6>
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addTopicModal">
Aggiungi Corso
</button>
</div>
<!-- DESKTOP / TABLET ≥768px: TABLE -->
<div class="table-responsive d-none d-md-block">
<table id="tabellaTopics" class="table table-striped align-middle text-center" style="width:100%;">
<thead>
<tr>
<th>ID</th>
<th>Nome</th>
<th>Descrizione</th>
<th>Frequenza<br>(mesi)</th>
<th>Promemoria<br>(giorni)</th>
<th>Ordine</th>
<th>Stato</th>
<th>Formazioni</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($topics as $row): ?>
<?php
$id = (int)$row['id'];
$name = $row['name'] ?? '';
$description = $row['description'] ?? '';
$freq = $row['default_frequency_months'];
$rem = (int)($row['default_reminder_days'] ?? 30);
$sortOrder = (int)($row['sort_order'] ?? 999);
$isActive = (int)($row['is_active'] ?? 1);
$isMandatory = (int)($row['is_mandatory'] ?? 0);
$cnt = (int)($row['trainings_count'] ?? 0);
$statusClass = $isActive === 1 ? 'active' : 'inactive';
$statusLabel = $isActive === 1 ? 'Attivo' : 'Inattivo';
?>
<tr>
<td><?= $id ?></td>
<td class="fw-semibold text-start">
<?= htmlspecialchars($name) ?>
<?php if ($isMandatory === 1): ?>
<span class="badge bg-warning text-dark ms-1" title="Obbligatorio per tutti"> Obbl.</span>
<?php endif; ?>
</td>
<td class="description-cell" title="<?= htmlspecialchars($description, ENT_QUOTES) ?>">
<?= $description !== '' ? htmlspecialchars($description) : '-' ?>
</td>
<td>
<?php if ($freq === null || $freq === ''): ?>
<span class="text-muted">una tantum</span>
<?php else: ?>
<span class="num-pill"><?= (int)$freq ?></span>
<?php endif; ?>
</td>
<td><span class="num-pill"><?= $rem ?></span></td>
<td><?= $sortOrder ?></td>
<td><span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span></td>
<td><?= $cnt ?></td>
<td>
<button class="btn btn-sm btn-outline-secondary edit-topic"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
data-freq="<?= $freq === null ? '' : (int)$freq ?>"
data-rem="<?= $rem ?>"
data-sort_order="<?= $sortOrder ?>"
data-is_active="<?= $isActive ?>"
data-is_mandatory="<?= $isMandatory ?>">
✏️ Modifica
</button>
<button class="btn btn-sm btn-outline-danger delete-topic"
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($topics)): ?>
<div class="tt-empty">Nessun corso presente</div>
<?php endif; ?>
<?php foreach ($topics as $row): ?>
<?php
$id = (int)$row['id'];
$name = $row['name'] ?? '';
$description = $row['description'] ?? '';
$freq = $row['default_frequency_months'];
$rem = (int)($row['default_reminder_days'] ?? 30);
$sortOrder = (int)($row['sort_order'] ?? 999);
$isActive = (int)($row['is_active'] ?? 1);
$isMandatory = (int)($row['is_mandatory'] ?? 0);
$cnt = (int)($row['trainings_count'] ?? 0);
$statusClass = $isActive === 1 ? 'active' : 'inactive';
$statusLabel = $isActive === 1 ? 'Attivo' : 'Inattivo';
$freqLabel = ($freq === null || $freq === '') ? 'una tantum' : ((int)$freq . ' mesi');
?>
<div class="tt-card">
<h6 class="tt-card-title">
<?= htmlspecialchars($name) ?>
<?php if ($isMandatory === 1): ?>
<span class="badge bg-warning text-dark ms-1" title="Obbligatorio per tutti"> Obbl.</span>
<?php endif; ?>
</h6>
<?php if ($description !== ''): ?>
<p class="tt-card-desc"><?= htmlspecialchars($description) ?></p>
<?php endif; ?>
<div class="tt-card-meta">
<span><span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span></span>
<span><b>Frequenza:</b> <?= htmlspecialchars($freqLabel) ?></span>
<span><b>Promemoria:</b> <?= $rem ?> gg</span>
<span><b>Formazioni:</b> <?= $cnt ?></span>
<span><b>Ordine:</b> <?= $sortOrder ?></span>
</div>
<div class="tt-card-actions">
<button class="btn btn-sm btn-outline-secondary edit-topic"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
data-freq="<?= $freq === null ? '' : (int)$freq ?>"
data-rem="<?= $rem ?>"
data-sort_order="<?= $sortOrder ?>"
data-is_active="<?= $isActive ?>"
data-is_mandatory="<?= $isMandatory ?>">
✏️ Modifica
</button>
<button class="btn btn-sm btn-outline-danger delete-topic"
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 -->
<div class="modal fade" id="addTopicModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-lg modal-fullscreen-sm-down">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">Aggiungi Corso</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addTopicForm">
<div class="mb-3">
<label class="form-label fw-semibold">Nome</label>
<input type="text" class="form-control" id="addName" name="name" placeholder="es. Sicurezza antincendio" 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">Frequenza aggiornamento</label>
<select class="form-select" id="addFreq" name="default_frequency_months">
<option value="" selected>Una tantum (nessun aggiornamento)</option>
<option value="3">3 mesi</option>
<option value="6">6 mesi</option>
<option value="12">12 mesi (1 anno)</option>
<option value="18">18 mesi</option>
<option value="24">24 mesi (2 anni)</option>
<option value="36">36 mesi (3 anni)</option>
<option value="48">48 mesi (4 anni)</option>
<option value="60">60 mesi (5 anni)</option>
</select>
</div>
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Promemoria (giorni prima della scadenza)</label>
<input type="number" class="form-control" id="addRem" name="default_reminder_days" value="30" min="0">
</div>
</div>
<div class="row">
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Ordine</label>
<input type="number" class="form-control" id="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="form-check mb-3">
<input class="form-check-input" type="checkbox" id="addIsMandatory" value="1">
<label class="form-check-label fw-semibold" for="addIsMandatory">
Obbligatorio per tutti i dipendenti
</label>
<div class="small text-muted">
Se attivo, i dipendenti senza registrazione di questo corso compaiono come "Non presente" nello storico.
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add">💾 Salva</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- EDIT -->
<div class="modal fade" id="editTopicModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-lg modal-fullscreen-sm-down">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">Modifica Corso</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editTopicForm">
<input type="hidden" id="editTopicId">
<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">Frequenza aggiornamento</label>
<select class="form-select" id="editFreq" name="default_frequency_months">
<option value="">Una tantum (nessun aggiornamento)</option>
<option value="3">3 mesi</option>
<option value="6">6 mesi</option>
<option value="12">12 mesi (1 anno)</option>
<option value="18">18 mesi</option>
<option value="24">24 mesi (2 anni)</option>
<option value="36">36 mesi (3 anni)</option>
<option value="48">48 mesi (4 anni)</option>
<option value="60">60 mesi (5 anni)</option>
</select>
</div>
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Promemoria (giorni prima della scadenza)</label>
<input type="number" class="form-control" id="editRem" name="default_reminder_days" min="0">
</div>
</div>
<div class="row">
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Ordine</label>
<input type="number" class="form-control" id="editSortOrder" name="sort_order" 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="form-check mb-3">
<input class="form-check-input" type="checkbox" id="editIsMandatory" value="1">
<label class="form-check-label fw-semibold" for="editIsMandatory">
Obbligatorio per tutti i dipendenti
</label>
<div class="small text-muted">
Se attivo, i dipendenti senza registrazione di questo corso compaiono come "Non presente" nello storico.
</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() {
$('#tabellaTopics').DataTable({
order: [[5, 'asc'], [1, 'asc']],
pageLength: 25,
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json',
emptyTable: 'Nessun corso 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);
});
}
$("#addTopicForm").on("submit", function(e) {
e.preventDefault();
const p = new URLSearchParams();
p.append('name', $("#addName").val().trim());
p.append('description', $("#addDescription").val().trim());
p.append('default_frequency_months', $("#addFreq").val());
p.append('default_reminder_days', $("#addRem").val());
p.append('sort_order', $("#addSortOrder").val());
p.append('is_active', $("#addIsActive").val());
p.append('is_mandatory', $("#addIsMandatory").is(':checked') ? '1' : '0');
ajaxPost("ajax/training_topics/save.php", p, "Salvato!", "Impossibile salvare il corso.");
});
$(document).on("click", ".edit-topic", function() {
const b = $(this);
const rawFreq = b.data("freq");
const freqStr = (rawFreq === '' || rawFreq === null || rawFreq === undefined) ? '' : String(rawFreq);
if (freqStr !== '' && $("#editFreq option[value='" + freqStr + "']").length === 0) {
$("#editFreq").append('<option value="' + freqStr + '">' + freqStr + ' mesi</option>');
}
$("#editTopicId").val(b.data("id"));
$("#editName").val(b.data("name"));
$("#editDescription").val(b.data("description"));
$("#editFreq").val(freqStr);
$("#editRem").val(b.data("rem"));
$("#editSortOrder").val(b.data("sort_order"));
$("#editIsActive").val(String(b.data("is_active")));
$("#editIsMandatory").prop('checked', String(b.data("is_mandatory")) === '1');
$("#editTopicModal").modal("show");
});
$("#editTopicForm").on("submit", function(e) {
e.preventDefault();
const p = new URLSearchParams();
p.append('id', $("#editTopicId").val());
p.append('name', $("#editName").val().trim());
p.append('description', $("#editDescription").val().trim());
p.append('default_frequency_months', $("#editFreq").val());
p.append('default_reminder_days', $("#editRem").val());
p.append('sort_order', $("#editSortOrder").val());
p.append('is_active', $("#editIsActive").val());
p.append('is_mandatory', $("#editIsMandatory").is(':checked') ? '1' : '0');
ajaxPost("ajax/training_topics/save.php", p, "Aggiornato!", "Impossibile aggiornare il corso.");
});
$(document).on("click", ".delete-topic", 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: "Il corso \"" + name + "\" ha " + cnt + " registrazione/i di formazione. Cancella prima le registrazioni."
});
return;
}
Swal.fire({
title: "Confermi la cancellazione?",
text: name ? ("Corso: " + name) : "Il corso verrà cancellato.",
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/training_topics/delete.php", p, "Cancellato!", "Impossibile cancellare il corso.");
});
});
});
</script>
</body>
</html>
+753
View File
@@ -0,0 +1,753 @@
<?php
include('include/headscript.php');
$pdo = DBHandlerSelect::getInstance()->getConnection();
/* ==========================================
PERMISSIONS
========================================== */
$isHrManager = Auth::user()->hasRole('Admin')
|| Auth::user()->hasRole('Superuser')
|| Auth::user()->hasRole('employee-hr')
|| Auth::user()->hasRole('manager');
if (!$isHrManager) {
header('Location: employee-profile.php');
exit;
}
/* ==========================================
FILTERS (from GET)
========================================== */
$fEmployeeId = isset($_GET['employee_id']) && $_GET['employee_id'] !== '' ? (int)$_GET['employee_id'] : 0;
$fTopicId = isset($_GET['topic_id']) && $_GET['topic_id'] !== '' ? (int)$_GET['topic_id'] : 0;
$fStatus = isset($_GET['status']) ? trim($_GET['status']) : '';
$fType = isset($_GET['type']) ? trim($_GET['type']) : '';
$fDepartmentId = isset($_GET['department_id'])&& $_GET['department_id']!== '' ? (int)$_GET['department_id']: 0;
/* ==========================================
LOAD DATA
========================================== */
$where = [];
$params = [];
// Only the most recent record per (employee, topic) — older initial/refresher
// rows stay as history on the employee profile, not in this overview.
$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 ($fEmployeeId > 0) { $where[] = 'et.employee_id = :eid'; $params['eid'] = $fEmployeeId; }
if ($fTopicId > 0) { $where[] = 'et.training_topic_id = :tid'; $params['tid'] = $fTopicId; }
if ($fType !== '' && in_array($fType, ['initial', 'refresher'], true)) {
$where[] = 'et.training_type = :ty';
$params['ty'] = $fType;
}
if ($fDepartmentId > 0) { $where[] = 'e.department_id = :did'; $params['did'] = $fDepartmentId; }
$whereSql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
$stmt = $pdo->prepare("
SELECT et.*,
tt.name AS topic_name,
tt.default_reminder_days AS topic_default_rem,
e.first_name, e.last_name, e.employee_code,
d.name AS department_name, d.color AS department_color,
(SELECT COUNT(*) FROM employee_training_attachments a WHERE a.training_id = et.id) AS attachments_count
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 departments d ON d.id = e.department_id
$whereSql
ORDER BY et.next_due_date IS NULL, et.next_due_date ASC, e.last_name, e.first_name
");
$stmt->execute($params);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
/* Filter by computed status */
function trainingStatus(?string $nextDue, ?int $reminderDays, ?int $topicDefaultRem): array {
if (!$nextDue) {
return ['code' => 'compliant', 'label' => 'Conforme', 'class' => 'success'];
}
$rem = $reminderDays !== null ? $reminderDays : ($topicDefaultRem !== null ? $topicDefaultRem : 30);
$today = new DateTime('today');
$due = DateTime::createFromFormat('Y-m-d', $nextDue);
if (!$due) return ['code' => 'compliant', 'label' => 'Conforme', 'class' => 'success'];
$daysLeft = (int)$today->diff($due)->format('%r%a');
if ($daysLeft < 0) return ['code' => 'expired', 'label' => 'Scaduto', 'class' => 'danger', 'days' => $daysLeft];
if ($daysLeft <= $rem) return ['code' => 'due_soon', 'label' => 'Da aggiornare', 'class' => 'warning', 'days' => $daysLeft];
return ['code' => 'compliant', 'label' => 'Conforme', 'class' => 'success', 'days' => $daysLeft];
}
$filtered = [];
$counters = ['compliant' => 0, 'due_soon' => 0, 'expired' => 0, 'not_present' => 0, 'all' => 0];
foreach ($rows as $r) {
$s = trainingStatus($r['next_due_date'] ?: null,
$r['reminder_days'] !== null ? (int)$r['reminder_days'] : null,
$r['topic_default_rem'] !== null ? (int)$r['topic_default_rem'] : null);
$r['_status'] = $s;
$counters['all']++;
$counters[$s['code']] = ($counters[$s['code']] ?? 0) + 1;
if ($fStatus !== '' && $fStatus !== $s['code']) continue;
$filtered[] = $r;
}
/* ==========================================
"NOT PRESENT" mandatory topics without any record for an employee.
Apply the same filters (employee_id / topic_id / department_id / type=initial).
========================================== */
if ($fType === '' || $fType === 'initial') {
$missingWhere = [];
$missingParams = [];
if ($fEmployeeId > 0) { $missingWhere[] = 'e.id = :eid'; $missingParams['eid'] = $fEmployeeId; }
if ($fTopicId > 0) { $missingWhere[] = 'tt.id = :tid'; $missingParams['tid'] = $fTopicId; }
if ($fDepartmentId > 0) { $missingWhere[] = 'e.department_id = :did'; $missingParams['did'] = $fDepartmentId; }
$missingWhereSql = $missingWhere ? ('AND ' . implode(' AND ', $missingWhere)) : '';
$missingStmt = $pdo->prepare("
SELECT e.id AS employee_id, e.first_name, e.last_name, e.employee_code,
d.name AS department_name, d.color AS department_color,
tt.id AS topic_id, tt.name AS topic_name
FROM employees e
CROSS JOIN training_topics tt
LEFT JOIN departments d ON d.id = e.department_id
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
)
$missingWhereSql
ORDER BY e.last_name, e.first_name, tt.name
");
$missingStmt->execute($missingParams);
$missingRows = $missingStmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($missingRows as $m) {
$counters['all']++;
$counters['not_present']++;
if ($fStatus !== '' && $fStatus !== 'not_present') continue;
$filtered[] = [
'id' => null,
'_virtual' => true,
'employee_id' => $m['employee_id'],
'first_name' => $m['first_name'],
'last_name' => $m['last_name'],
'employee_code' => $m['employee_code'],
'department_name' => $m['department_name'],
'department_color' => $m['department_color'],
'training_topic_id' => $m['topic_id'],
'topic_name' => $m['topic_name'],
'training_type' => null,
'completed_date' => null,
'next_due_date' => null,
'attachments_count' => 0,
'_status' => ['code' => 'not_present', 'label' => 'Non presente', 'class' => 'secondary', 'days' => null],
];
}
}
/* Dropdown data */
$employees = $pdo->query("
SELECT id, first_name, last_name, employee_code, department_id
FROM employees
ORDER BY last_name, first_name
")->fetchAll(PDO::FETCH_ASSOC);
$topics = $pdo->query("
SELECT id, name, default_frequency_months, default_reminder_days
FROM training_topics WHERE is_active = 1 ORDER BY sort_order, name
")->fetchAll(PDO::FETCH_ASSOC);
$departments = $pdo->query("
SELECT id, name, color FROM departments WHERE is_active = 1 ORDER BY sort_order, name
")->fetchAll(PDO::FETCH_ASSOC);
function fmtDate(?string $d): string {
if (!$d || $d === '0000-00-00') return '—';
$ts = strtotime($d);
return $ts ? date('d/m/Y', $ts) : '—';
}
?>
<!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>Storico Formazione - <?= 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 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.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;
}
.stat-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; margin-bottom: 20px; }
@media (max-width: 991.98px) { .stat-row { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 575.98px) { .stat-row { grid-template-columns: repeat(2, 1fr); } }
.stat-card {
border-radius: 14px; padding: 14px 16px; text-align: center;
background: #fff; box-shadow: 0 2px 6px rgba(0,0,0,.05);
cursor: pointer; transition: transform .15s;
}
.stat-card:hover { transform: translateY(-2px); }
.stat-card.active { outline: 3px solid #0d6efd; }
.stat-card .stat-num { font-size: 1.8rem; font-weight: 700; line-height: 1; }
.stat-card .stat-label { font-size: 0.85rem; color: #64748b; margin-top: 4px; }
.stat-card.all .stat-num { color: #1f2937; }
.stat-card.compliant .stat-num { color: #16a34a; }
.stat-card.due_soon .stat-num { color: #d97706; }
.stat-card.expired .stat-num { color: #dc2626; }
.stat-card.not_present .stat-num { color: #6b7280; }
.pill { display: inline-block; padding: 3px 10px; border-radius: 999px; font-size: 0.85rem; font-weight: 600; }
.pill-success { background: #d1fae5; color: #065f46; }
.pill-warning { background: #fef3c7; color: #92400e; }
.pill-danger { background: #fee2e2; color: #991b1b; }
.pill-secondary { background: #e5e7eb; color: #374151; }
.pill-role { background: #fff; color: #334155; border: 1px solid #cbd5e1; }
.pill-dept-inline { padding: 2px 8px; }
.tr-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);
}
.tr-card .name a { color: #1f2937; font-weight: 600; text-decoration: none; }
.tr-card .topic { color: #475569; }
.tr-card .meta { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 0.85rem; color: #64748b; margin-top: 8px; }
.tr-card .meta b { color: #1f2937; font-weight: 600; }
@media (max-width: 767.98px) {
.card-header { flex-direction: column; align-items: flex-start !important; gap: .5rem; }
.back-dashboard { width: 100%; }
}
</style>
</head>
<body>
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card p-3">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h5 class="mb-0">📚 Storico Formazione</h5>
<div class="d-flex gap-2 flex-wrap">
<button type="button" class="btn btn-primary" id="btnBulkTraining">
Aggiungi sessione
</button>
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
↩️ Torna alla Dashboard
</button>
</div>
</div>
<div class="card-body">
<!-- COUNTERS -->
<div class="stat-row">
<a class="stat-card all <?= $fStatus === '' ? 'active' : '' ?>" href="?<?= http_build_query(array_filter(['employee_id' => $fEmployeeId, 'topic_id' => $fTopicId, 'type' => $fType, 'department_id' => $fDepartmentId])) ?>">
<div class="stat-num"><?= (int)$counters['all'] ?></div>
<div class="stat-label">Tutte</div>
</a>
<a class="stat-card compliant <?= $fStatus === 'compliant' ? 'active' : '' ?>" href="?<?= http_build_query(array_filter(['status' => 'compliant', 'employee_id' => $fEmployeeId, 'topic_id' => $fTopicId, 'type' => $fType, 'department_id' => $fDepartmentId])) ?>">
<div class="stat-num"><?= (int)($counters['compliant'] ?? 0) ?></div>
<div class="stat-label">Conformi</div>
</a>
<a class="stat-card due_soon <?= $fStatus === 'due_soon' ? 'active' : '' ?>" href="?<?= http_build_query(array_filter(['status' => 'due_soon', 'employee_id' => $fEmployeeId, 'topic_id' => $fTopicId, 'type' => $fType, 'department_id' => $fDepartmentId])) ?>">
<div class="stat-num"><?= (int)($counters['due_soon'] ?? 0) ?></div>
<div class="stat-label">Da aggiornare</div>
</a>
<a class="stat-card expired <?= $fStatus === 'expired' ? 'active' : '' ?>" href="?<?= http_build_query(array_filter(['status' => 'expired', 'employee_id' => $fEmployeeId, 'topic_id' => $fTopicId, 'type' => $fType, 'department_id' => $fDepartmentId])) ?>">
<div class="stat-num"><?= (int)($counters['expired'] ?? 0) ?></div>
<div class="stat-label">Scaduti</div>
</a>
<a class="stat-card not_present <?= $fStatus === 'not_present' ? 'active' : '' ?>" href="?<?= http_build_query(array_filter(['status' => 'not_present', 'employee_id' => $fEmployeeId, 'topic_id' => $fTopicId, 'department_id' => $fDepartmentId])) ?>">
<div class="stat-num"><?= (int)($counters['not_present'] ?? 0) ?></div>
<div class="stat-label">Non presenti</div>
</a>
</div>
<!-- FILTERS -->
<form method="get" class="row g-2 mb-3" id="filtersForm">
<input type="hidden" name="status" value="<?= htmlspecialchars($fStatus, ENT_QUOTES) ?>">
<div class="col-12 col-md-6 col-lg-3">
<label class="form-label small fw-semibold">Dipendente</label>
<select name="employee_id" class="form-select form-select-sm" onchange="this.form.submit()">
<option value=""> Tutti </option>
<?php foreach ($employees as $e): ?>
<option value="<?= (int)$e['id'] ?>" <?= $fEmployeeId === (int)$e['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars(trim($e['first_name'] . ' ' . $e['last_name'])) ?>
<?php if (!empty($e['employee_code'])): ?>(<?= htmlspecialchars($e['employee_code']) ?>)<?php endif; ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-md-6 col-lg-3">
<label class="form-label small fw-semibold">Corso</label>
<select name="topic_id" class="form-select form-select-sm" onchange="this.form.submit()">
<option value=""> Tutti </option>
<?php foreach ($topics as $t): ?>
<option value="<?= (int)$t['id'] ?>" <?= $fTopicId === (int)$t['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($t['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-md-6 col-lg-3">
<label class="form-label small fw-semibold">Reparto</label>
<select name="department_id" class="form-select form-select-sm" onchange="this.form.submit()">
<option value=""> Tutti </option>
<?php foreach ($departments as $d): ?>
<option value="<?= (int)$d['id'] ?>" <?= $fDepartmentId === (int)$d['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($d['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-md-6 col-lg-3">
<label class="form-label small fw-semibold">Tipo</label>
<select name="type" class="form-select form-select-sm" onchange="this.form.submit()">
<option value=""> Tutti </option>
<option value="initial" <?= $fType === 'initial' ? 'selected' : '' ?>>Iniziale</option>
<option value="refresher" <?= $fType === 'refresher' ? 'selected' : '' ?>>Aggiornamento</option>
</select>
</div>
<?php if ($fEmployeeId || $fTopicId || $fDepartmentId || $fType || $fStatus): ?>
<div class="col-12">
<a href="trainings.php" class="btn btn-sm btn-outline-secondary">✖️ Pulisci filtri</a>
</div>
<?php endif; ?>
</form>
<div id="bulkBar" class="d-none align-items-center flex-wrap gap-2 mb-3 p-2" style="background:#fff6e5;border:1px solid #ffe0a6;border-radius:10px;">
<span class="fw-semibold"><span id="bulkSelCount">0</span> selezionati</span>
<button type="button" class="btn btn-sm btn-warning" id="btnBulkRenew">🔄 Aggiorna scadenza</button>
<button type="button" class="btn btn-sm btn-link text-decoration-none" id="btnBulkDeselect">Deseleziona</button>
</div>
<?php if (empty($filtered)): ?>
<div class="text-center text-muted py-4">
Nessuna formazione corrispondente ai filtri.
</div>
<?php else: ?>
<!-- DESKTOP TABLE -->
<div class="table-responsive d-none d-md-block">
<table class="table table-striped align-middle">
<thead style="background-color:#cfe3ff;">
<tr>
<th style="width:36px"><input type="checkbox" class="form-check-input" id="checkAll" title="Seleziona tutti"></th>
<th>Dipendente</th>
<th>Reparto</th>
<th>Corso</th>
<th>Tipo</th>
<th>Completato</th>
<th>Prossimo agg.</th>
<th>Stato</th>
<th>Giorni</th>
</tr>
</thead>
<tbody>
<?php foreach ($filtered as $r): ?>
<?php
$fullName = trim($r['first_name'] . ' ' . $r['last_name']);
$typeLbl = $r['training_type'] === 'refresher' ? 'Aggiornamento' : ($r['training_type'] === 'initial' ? 'Iniziale' : '—');
$days = $r['_status']['days'] ?? null;
?>
<tr>
<td>
<?php if (!empty($r['id'])): ?>
<input type="checkbox" class="form-check-input row-check" value="<?= (int)$r['id'] ?>">
<?php endif; ?>
</td>
<td>
<a href="employee-profile.php?id=<?= (int)$r['employee_id'] ?>#tab-training" class="fw-semibold text-decoration-none">
<?= htmlspecialchars($fullName) ?>
</a>
<?php if (!empty($r['employee_code'])): ?>
<div class="small text-muted"><?= htmlspecialchars($r['employee_code']) ?></div>
<?php endif; ?>
</td>
<td>
<?php if (!empty($r['department_name'])): ?>
<span class="pill pill-dept-inline" style="background:<?= htmlspecialchars($r['department_color'] ?? '#e5e7eb', ENT_QUOTES) ?>20; color:<?= htmlspecialchars($r['department_color'] ?? '#374151', ENT_QUOTES) ?>;">
<?= htmlspecialchars($r['department_name']) ?>
</span>
<?php else: ?>—<?php endif; ?>
</td>
<td><?= htmlspecialchars($r['topic_name']) ?></td>
<td><span class="pill pill-role"><?= $typeLbl ?></span></td>
<td><?= fmtDate($r['completed_date']) ?></td>
<td><?= fmtDate($r['next_due_date']) ?></td>
<td><span class="pill pill-<?= $r['_status']['class'] ?>"><?= $r['_status']['label'] ?></span></td>
<td>
<?php if ($days === null): ?>
<?php elseif ($days < 0): ?>
<span class="text-danger fw-semibold"><?= $days ?></span>
<?php else: ?>
+<?= $days ?>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- MOBILE CARDS -->
<div class="d-block d-md-none">
<?php foreach ($filtered as $r): ?>
<?php
$fullName = trim($r['first_name'] . ' ' . $r['last_name']);
$typeLbl = $r['training_type'] === 'refresher' ? 'Aggiornamento' : ($r['training_type'] === 'initial' ? 'Iniziale' : '—');
$days = $r['_status']['days'] ?? null;
?>
<div class="tr-card">
<div class="d-flex justify-content-between align-items-start gap-2 mb-1">
<div class="name d-flex align-items-start gap-2">
<?php if (!empty($r['id'])): ?>
<input type="checkbox" class="form-check-input row-check mt-1" value="<?= (int)$r['id'] ?>">
<?php endif; ?>
<a href="employee-profile.php?id=<?= (int)$r['employee_id'] ?>#tab-training">
<?= htmlspecialchars($fullName) ?>
</a>
</div>
<span class="pill pill-<?= $r['_status']['class'] ?>"><?= $r['_status']['label'] ?></span>
</div>
<div class="topic">📖 <?= htmlspecialchars($r['topic_name']) ?></div>
<div class="meta">
<span><b>Tipo:</b> <?= $typeLbl ?></span>
<span><b>Completato:</b> <?= fmtDate($r['completed_date']) ?></span>
<?php if ($r['next_due_date']): ?>
<span><b>Prossimo:</b> <?= fmtDate($r['next_due_date']) ?>
<?php if ($days !== null && $days < 0): ?>
<span class="text-danger fw-semibold">(<?= $days ?>g)</span>
<?php elseif ($days !== null): ?>
(+<?= $days ?>g)
<?php endif; ?>
</span>
<?php endif; ?>
<?php if (!empty($r['department_name'])): ?>
<span><b>Reparto:</b> <?= htmlspecialchars($r['department_name']) ?></span>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<!-- BULK TRAINING SESSION MODAL -->
<div class="modal fade" id="bulkTrainingModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"> Nuova sessione formativa</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<form id="bulkTrainingForm">
<div class="modal-body" style="max-height:65vh; overflow-y:auto;">
<p class="text-muted small">Registra lo stesso corso, con gli stessi parametri, per più dipendenti contemporaneamente.</p>
<div class="row g-3">
<div class="col-12 col-md-6">
<label class="form-label fw-semibold">Corso <span class="text-danger">*</span></label>
<select id="bulkTopic" class="form-select" required>
<option value=""> Seleziona </option>
<?php foreach ($topics as $t): ?>
<option value="<?= (int)$t['id'] ?>"
data-freq="<?= $t['default_frequency_months'] !== null ? (int)$t['default_frequency_months'] : '' ?>"
data-rem="<?= $t['default_reminder_days'] !== null ? (int)$t['default_reminder_days'] : '' ?>">
<?= htmlspecialchars($t['name'], ENT_QUOTES, 'UTF-8') ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-6 col-md-3">
<label class="form-label fw-semibold">Data completamento <span class="text-danger">*</span></label>
<input type="date" id="bulkCompletedDate" class="form-control" value="<?= date('Y-m-d') ?>" required>
</div>
<div class="col-6 col-md-3">
<label class="form-label fw-semibold">Tipo</label>
<select id="bulkType" class="form-select">
<option value="initial">Iniziale</option>
<option value="refresher">Aggiornamento</option>
</select>
</div>
<div class="col-6 col-md-3">
<label class="form-label fw-semibold">Frequenza (mesi)</label>
<input type="number" id="bulkFreq" class="form-control" min="0" max="600" placeholder="default corso">
<div class="form-text">Vuoto = una tantum</div>
</div>
<div class="col-6 col-md-3">
<label class="form-label fw-semibold">Promemoria (giorni)</label>
<input type="number" id="bulkRem" class="form-control" min="0" max="365" placeholder="default corso">
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-semibold">Erogato da</label>
<input type="text" id="bulkDeliveredBy" class="form-control" maxlength="255" placeholder="es. Ente formatore, docente interno...">
</div>
<div class="col-12">
<label class="form-label fw-semibold">Descrizione / note</label>
<textarea id="bulkDescription" class="form-control" rows="2"></textarea>
</div>
<div class="col-12"><hr class="my-1"></div>
<div class="col-12">
<label class="form-label fw-semibold">Dipendenti <span class="text-danger">*</span></label>
<div class="d-flex flex-wrap gap-2 mb-2 align-items-end">
<div>
<select id="bulkDept" class="form-select form-select-sm" style="min-width:180px">
<option value=""> Reparto </option>
<?php foreach ($departments as $d): ?>
<option value="<?= (int)$d['id'] ?>"><?= htmlspecialchars($d['name'], ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="button" class="btn btn-sm btn-outline-primary" id="bulkAddDept">+ Aggiungi reparto</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="bulkSelectAll">Tutti</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="bulkClear">Pulisci</button>
</div>
<select id="bulkEmployees" class="form-select" multiple required>
<?php foreach ($employees as $e): ?>
<option value="<?= (int)$e['id'] ?>" data-dept="<?= (int)($e['department_id'] ?? 0) ?>">
<?= htmlspecialchars(trim($e['last_name'] . ' ' . $e['first_name']), ENT_QUOTES, 'UTF-8') ?><?php if (!empty($e['employee_code'])): ?> (<?= htmlspecialchars($e['employee_code'], ENT_QUOTES, 'UTF-8') ?>)<?php endif; ?>
</option>
<?php endforeach; ?>
</select>
<div class="form-text"><span id="bulkCount">0</span> dipendenti selezionati</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light border" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-primary" id="bulkSaveBtn">Registra formazione</button>
</div>
</form>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
var bulkModal = new bootstrap.Modal(document.getElementById('bulkTrainingModal'));
var $emp = $('#bulkEmployees');
$emp.select2({
theme: 'bootstrap-5',
placeholder: 'Seleziona dipendenti...',
dropdownParent: $('#bulkTrainingModal'),
closeOnSelect: false,
width: '100%'
});
function updateCount() {
document.getElementById('bulkCount').textContent = ($emp.val() || []).length;
}
$emp.on('change', updateCount);
document.getElementById('btnBulkTraining').addEventListener('click', function() {
document.getElementById('bulkTrainingForm').reset();
$emp.val(null).trigger('change');
document.getElementById('bulkTopic').value = '';
document.getElementById('bulkType').value = 'initial';
updateCount();
bulkModal.show();
});
// Prefill frequency/reminder from the selected course
document.getElementById('bulkTopic').addEventListener('change', function() {
var opt = this.options[this.selectedIndex];
document.getElementById('bulkFreq').value = opt ? (opt.getAttribute('data-freq') || '') : '';
document.getElementById('bulkRem').value = opt ? (opt.getAttribute('data-rem') || '') : '';
});
// Add all employees of the chosen department to the selection
document.getElementById('bulkAddDept').addEventListener('click', function() {
var dept = document.getElementById('bulkDept').value;
if (!dept) return;
var current = ($emp.val() || []).map(String);
$emp.find('option').each(function() {
if (this.getAttribute('data-dept') === String(dept) && current.indexOf(this.value) === -1) {
current.push(this.value);
}
});
$emp.val(current).trigger('change');
});
document.getElementById('bulkSelectAll').addEventListener('click', function() {
var all = $emp.find('option').map(function() { return this.value; }).get();
$emp.val(all).trigger('change');
});
document.getElementById('bulkClear').addEventListener('click', function() {
$emp.val(null).trigger('change');
});
document.getElementById('bulkTrainingForm').addEventListener('submit', function(e) {
e.preventDefault();
var topicId = document.getElementById('bulkTopic').value;
var completed = document.getElementById('bulkCompletedDate').value;
var emps = $emp.val() || [];
if (!topicId) { Swal.fire('Attenzione', 'Selezionare un corso.', 'warning'); return; }
if (!completed) { Swal.fire('Attenzione', 'Indicare la data di completamento.', 'warning'); return; }
if (emps.length === 0) { Swal.fire('Attenzione', 'Selezionare almeno un dipendente.', 'warning'); return; }
var btn = document.getElementById('bulkSaveBtn');
btn.disabled = true;
var orig = btn.innerHTML;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Salvataggio...';
var fd = new FormData();
fd.append('training_topic_id', topicId);
fd.append('completed_date', completed);
fd.append('training_type', document.getElementById('bulkType').value);
fd.append('delivered_by', document.getElementById('bulkDeliveredBy').value);
fd.append('description', document.getElementById('bulkDescription').value);
fd.append('update_frequency_months', document.getElementById('bulkFreq').value);
fd.append('reminder_days', document.getElementById('bulkRem').value);
emps.forEach(function(id) { fd.append('employee_ids[]', id); });
fetch('ajax/trainings/save_bulk_training.php', { method: 'POST', body: fd })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
bulkModal.hide();
Swal.fire({ icon: 'success', title: 'Fatto', text: data.message, timer: 1800, showConfirmButton: false })
.then(function() { location.reload(); });
} else {
btn.disabled = false; btn.innerHTML = orig;
Swal.fire('Errore', data.message || 'Errore.', 'error');
}
})
.catch(function() {
btn.disabled = false; btn.innerHTML = orig;
Swal.fire('Errore', 'Errore di connessione.', 'error');
});
});
});
</script>
<!-- BULK RENEW DEADLINE MODAL -->
<div class="modal fade" id="bulkRenewModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form id="bulkRenewForm">
<div class="modal-header">
<h5 class="modal-title">🔄 Aggiorna scadenza</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<div class="modal-body">
<p class="text-muted small">Imposta la data di completamento per <b id="renewCount">0</b> record selezionati. Le prossime scadenze verranno ricalcolate in base alla frequenza di ciascun corso.</p>
<label class="form-label fw-semibold">Nuova data di completamento</label>
<input type="date" id="renewDate" class="form-control" value="<?= date('Y-m-d') ?>" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light border" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-warning" id="renewSaveBtn">Aggiorna</button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var renewModal = new bootstrap.Modal(document.getElementById('bulkRenewModal'));
var checkAll = document.getElementById('checkAll');
function checkedIds() {
return Array.prototype.slice.call(document.querySelectorAll('.row-check:checked'))
.map(function(c) { return c.value; });
}
function refreshBulkBar() {
var ids = checkedIds();
var bar = document.getElementById('bulkBar');
document.getElementById('bulkSelCount').textContent = ids.length;
if (ids.length > 0) { bar.classList.remove('d-none'); bar.classList.add('d-flex'); }
else { bar.classList.add('d-none'); bar.classList.remove('d-flex'); }
var all = document.querySelectorAll('.row-check');
if (checkAll) checkAll.checked = (all.length > 0 && ids.length === all.length);
}
document.addEventListener('change', function(e) {
if (e.target && e.target.classList && e.target.classList.contains('row-check')) refreshBulkBar();
});
if (checkAll) checkAll.addEventListener('change', function() {
document.querySelectorAll('.row-check').forEach(function(c) { c.checked = checkAll.checked; });
refreshBulkBar();
});
document.getElementById('btnBulkDeselect').addEventListener('click', function() {
document.querySelectorAll('.row-check').forEach(function(c) { c.checked = false; });
if (checkAll) checkAll.checked = false;
refreshBulkBar();
});
document.getElementById('btnBulkRenew').addEventListener('click', function() {
var ids = checkedIds();
if (ids.length === 0) return;
document.getElementById('renewCount').textContent = ids.length;
renewModal.show();
});
document.getElementById('bulkRenewForm').addEventListener('submit', function(e) {
e.preventDefault();
var ids = checkedIds();
var date = document.getElementById('renewDate').value;
if (ids.length === 0) { renewModal.hide(); return; }
if (!date) { Swal.fire('Attenzione', 'Indicare la data.', 'warning'); return; }
var btn = document.getElementById('renewSaveBtn');
btn.disabled = true;
var orig = btn.innerHTML;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Salvataggio...';
var fd = new FormData();
fd.append('completed_date', date);
ids.forEach(function(id) { fd.append('training_ids[]', id); });
fetch('ajax/trainings/bulk_update_deadline.php', { method: 'POST', body: fd })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
renewModal.hide();
Swal.fire({ icon: 'success', title: 'Fatto', text: data.message, timer: 1800, showConfirmButton: false })
.then(function() { location.reload(); });
} else {
btn.disabled = false; btn.innerHTML = orig;
Swal.fire('Errore', data.message || 'Errore.', 'error');
}
})
.catch(function() {
btn.disabled = false; btn.innerHTML = orig;
Swal.fire('Errore', 'Errore di connessione.', 'error');
});
});
});
</script>
</body>
</html>
+868
View File
@@ -0,0 +1,868 @@
<?php include('include/headscript.php'); ?>
<?php
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$userId = (int)($iduserlogin ?? 0);
if ($userId <= 0) {
die('Utente non valido.');
}
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (empty($_SESSION['user_settings_csrf'])) {
$_SESSION['user_settings_csrf'] = bin2hex(random_bytes(32));
}
$csrfToken = $_SESSION['user_settings_csrf'];
$successMessage = '';
$errorMessage = '';
// Load countries.
$countries = [];
try {
$stmtCountries = $pdo->query("
SELECT id, name, iso_3166_2
FROM auth_countries
ORDER BY name ASC
");
$countries = $stmtCountries->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
$countries = [];
}
// Load current user.
$stmtProfileUser = $pdo->prepare("
SELECT
id,
email,
password,
first_name,
last_name,
phone,
avatar,
address,
country_id,
birthday,
role_id,
status,
last_login
FROM auth_users
WHERE id = ?
LIMIT 1
");
$stmtProfileUser->execute([$userId]);
$profileUser = $stmtProfileUser->fetch(PDO::FETCH_ASSOC);
if (!$profileUser) {
die('Utente non trovato.');
}
function e($value)
{
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
}
function normalizeAvatarPath($avatar)
{
$avatar = trim((string)$avatar);
if ($avatar === '') {
return '';
}
// If the database already contains a complete relative path, use it as it is.
if (
str_starts_with($avatar, '../') ||
str_starts_with($avatar, './') ||
str_starts_with($avatar, '/') ||
str_starts_with($avatar, 'http://') ||
str_starts_with($avatar, 'https://')
) {
return $avatar;
}
// If the database contains only the filename, build the expected user upload path.
return '../upload/users/' . $avatar;
}
function getAvatarInitials($profileUser)
{
$first = trim((string)($profileUser['first_name'] ?? ''));
$last = trim((string)($profileUser['last_name'] ?? ''));
$email = trim((string)($profileUser['email'] ?? ''));
$initials = '';
if ($first !== '') {
$initials .= mb_substr($first, 0, 1);
}
if ($last !== '') {
$initials .= mb_substr($last, 0, 1);
}
if ($initials === '' && $email !== '') {
$initials = mb_substr($email, 0, 1);
}
return strtoupper($initials ?: 'U');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$postedToken = $_POST['csrf_token'] ?? '';
if (!hash_equals($csrfToken, $postedToken)) {
$errorMessage = 'Sessione non valida. Ricarica la pagina e riprova.';
} else {
$email = trim($_POST['email'] ?? '');
$firstName = trim($_POST['first_name'] ?? '');
$lastName = trim($_POST['last_name'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$address = trim($_POST['address'] ?? '');
$countryId = $_POST['country_id'] !== '' ? (int)$_POST['country_id'] : null;
$birthday = trim($_POST['birthday'] ?? '');
$currentPassword = $_POST['current_password'] ?? '';
$newPassword = $_POST['new_password'] ?? '';
$confirmPassword = $_POST['confirm_password'] ?? '';
$birthdayValue = null;
$avatarToSave = $profileUser['avatar'];
if ($birthday !== '') {
$dateObj = DateTime::createFromFormat('Y-m-d', $birthday);
if (!$dateObj || $dateObj->format('Y-m-d') !== $birthday) {
$errorMessage = 'La data di nascita non è valida.';
} else {
$birthdayValue = $birthday;
}
}
if (!$errorMessage && $email === '') {
$errorMessage = 'Lemail è obbligatoria.';
}
if (!$errorMessage && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errorMessage = 'Lemail inserita non è valida.';
}
// Check unique email.
if (!$errorMessage) {
$stmtCheckEmail = $pdo->prepare("
SELECT id
FROM auth_users
WHERE email = ? AND id <> ?
LIMIT 1
");
$stmtCheckEmail->execute([$email, $userId]);
if ($stmtCheckEmail->fetchColumn()) {
$errorMessage = 'Questa email è già utilizzata da un altro utente.';
}
}
// Avatar upload.
if (!$errorMessage && isset($_FILES['avatar']) && $_FILES['avatar']['error'] !== UPLOAD_ERR_NO_FILE) {
if ($_FILES['avatar']['error'] !== UPLOAD_ERR_OK) {
$errorMessage = 'Errore durante il caricamento dellavatar.';
} else {
$maxFileSize = 2 * 1024 * 1024; // 2 MB
if ($_FILES['avatar']['size'] > $maxFileSize) {
$errorMessage = 'Lavatar non può superare 2 MB.';
} else {
$tmpFile = $_FILES['avatar']['tmp_name'];
$originalName = $_FILES['avatar']['name'];
$allowedMimeTypes = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
'image/gif' => 'gif',
];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($tmpFile);
if (!array_key_exists($mimeType, $allowedMimeTypes)) {
$errorMessage = 'Formato avatar non valido. Sono consentiti JPG, PNG, WEBP o GIF.';
} else {
$uploadDir = __DIR__ . '/../upload/users/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$extension = $allowedMimeTypes[$mimeType];
$safeOriginalName = preg_replace('/[^A-Za-z0-9_\-\.]/', '_', pathinfo($originalName, PATHINFO_FILENAME));
$fileName = time() . '_' . $userId . '_' . $safeOriginalName . '.' . $extension;
$destination = $uploadDir . $fileName;
if (!move_uploaded_file($tmpFile, $destination)) {
$errorMessage = 'Impossibile salvare il file avatar.';
} else {
// Path used by pages inside userarea, for example:
// <img src="../upload/users/file.jpg">
$avatarToSave = $fileName;
}
}
}
}
}
$passwordToSave = null;
$wantsPasswordChange = ($currentPassword !== '' || $newPassword !== '' || $confirmPassword !== '');
if (!$errorMessage && $wantsPasswordChange) {
if ($currentPassword === '') {
$errorMessage = 'Inserisci la password attuale.';
} elseif ($newPassword === '') {
$errorMessage = 'Inserisci la nuova password.';
} elseif (strlen($newPassword) < 8) {
$errorMessage = 'La nuova password deve contenere almeno 8 caratteri.';
} elseif ($newPassword !== $confirmPassword) {
$errorMessage = 'La conferma password non corrisponde.';
} elseif (!password_verify($currentPassword, $profileUser['password'])) {
$errorMessage = 'La password attuale non è corretta.';
} else {
// Password is encrypted before saving.
$passwordToSave = password_hash($newPassword, PASSWORD_DEFAULT);
}
}
if (!$errorMessage) {
try {
$pdo->beginTransaction();
$stmtUpdate = $pdo->prepare("
UPDATE auth_users
SET
email = :email,
first_name = :first_name,
last_name = :last_name,
phone = :phone,
avatar = :avatar,
address = :address,
country_id = :country_id,
birthday = :birthday,
updated_at = NOW()
WHERE id = :id
LIMIT 1
");
$stmtUpdate->execute([
':email' => $email,
':first_name' => $firstName !== '' ? $firstName : null,
':last_name' => $lastName !== '' ? $lastName : null,
':phone' => $phone !== '' ? $phone : null,
':avatar' => $avatarToSave !== '' ? $avatarToSave : null,
':address' => $address !== '' ? $address : null,
':country_id' => $countryId,
':birthday' => $birthdayValue,
':id' => $userId,
]);
if ($passwordToSave !== null) {
$stmtPassword = $pdo->prepare("
UPDATE auth_users
SET password = ?, updated_at = NOW()
WHERE id = ?
LIMIT 1
");
$stmtPassword->execute([$passwordToSave, $userId]);
}
$pdo->commit();
$successMessage = $passwordToSave !== null
? 'Profilo, avatar e password aggiornati correttamente.'
: 'Profilo aggiornato correttamente.';
// Reload updated user.
$stmtProfileUser->execute([$userId]);
$profileUser = $stmtProfileUser->fetch(PDO::FETCH_ASSOC);
$_SESSION['user_settings_csrf'] = bin2hex(random_bytes(32));
$csrfToken = $_SESSION['user_settings_csrf'];
} catch (Exception $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
$errorMessage = 'Errore durante il salvataggio delle impostazioni.';
}
}
}
}
$avatarPath = normalizeAvatarPath($profileUser['avatar'] ?? '');
?>
<!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>Impostazioni Utente <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<style>
body {
background: linear-gradient(135deg, #f3f6f8, #e8eef3);
font-family: 'Segoe UI', sans-serif;
color: #2b3e50;
}
.page-content {
padding: 1.4rem 1rem;
display: flex;
flex-direction: column;
align-items: center;
}
.settings-wrap {
width: 100%;
max-width: 1280px;
}
h3.settings-title {
text-align: center;
font-weight: 800;
color: #2b3e50;
margin-bottom: 1.2rem;
letter-spacing: 0.3px;
}
.settings-card {
background: #fff;
border-radius: 16px;
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
margin-bottom: 16px;
overflow: hidden;
}
.settings-header {
padding: 16px 18px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
display: flex;
align-items: center;
gap: 12px;
}
.settings-icon {
font-size: 1.8rem;
line-height: 1;
}
.settings-heading {
margin: 0;
font-weight: 800;
font-size: 1.1rem;
}
.settings-subtitle {
margin: 0;
color: #6b7a89;
font-size: 0.92rem;
}
.settings-body {
padding: 20px;
}
.profile-layout {
display: grid;
grid-template-columns: 280px 1fr;
gap: 24px;
align-items: flex-start;
}
.avatar-panel {
background: linear-gradient(135deg, #f7fbff, #edf5ff);
border: 1px solid #dbeafe;
border-radius: 18px;
padding: 20px;
text-align: center;
}
.avatar-box {
width: 132px;
height: 132px;
border-radius: 34px;
background: linear-gradient(135deg, #cde5ff, #dff0ff);
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
font-weight: 800;
color: #2b3e50;
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
overflow: hidden;
margin: 0 auto 14px auto;
}
.avatar-box img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-name {
font-size: 1.1rem;
font-weight: 800;
margin: 0;
color: #2b3e50;
}
.avatar-email {
color: #6b7a89;
margin: 4px 0 14px 0;
font-size: 0.92rem;
word-break: break-word;
}
.avatar-upload-label {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
border-radius: 14px;
padding: 11px 14px;
cursor: pointer;
font-weight: 800;
color: #2b3e50;
background: linear-gradient(135deg, #e5e7eb, #f3f4f6);
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
transition: all 0.2s ease;
margin-top: 8px;
}
.avatar-upload-label:hover {
transform: translateY(-2px);
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.13);
}
.avatar-upload-input {
display: none;
}
.avatar-help {
font-size: 0.82rem;
color: #6b7a89;
margin-top: 10px;
line-height: 1.35;
}
.profile-meta {
color: #6b7a89;
font-size: 0.88rem;
margin-top: 14px;
}
.form-label {
font-weight: 700;
color: #2b3e50;
margin-bottom: 6px;
}
.form-control,
.form-select {
border-radius: 12px;
border: 1px solid #d8e0e7;
padding: 10px 12px;
font-size: 0.95rem;
}
.form-control:focus,
.form-select:focus {
border-color: #8bbcf7;
box-shadow: 0 0 0 0.18rem rgba(139, 188, 247, 0.25);
}
.help-text {
font-size: 0.86rem;
color: #6b7a89;
margin-top: 6px;
}
.password-box {
background: linear-gradient(135deg, #fff7e6, #fffaf0);
border-radius: 16px;
padding: 16px;
border: 1px solid rgba(255, 184, 107, 0.45);
}
.btn-save-settings {
border: 0;
border-radius: 16px;
padding: 13px 24px;
font-size: 1rem;
font-weight: 800;
color: #fff;
background: linear-gradient(135deg, #61ce5dff, #61ce5dff);
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
transition: all 0.2s ease;
}
.btn-save-settings:hover {
transform: translateY(-2px);
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.13);
color: #fff;
}
.btn-back {
border: 0;
border-radius: 16px;
padding: 13px 20px;
font-size: 1rem;
font-weight: 800;
color: #2b3e50;
background: linear-gradient(135deg, #e5e7eb, #f3f4f6);
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
transition: all 0.2s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
}
.btn-back:hover {
transform: translateY(-2px);
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.13);
color: #2b3e50;
}
.alert {
border-radius: 16px;
border: 0;
font-weight: 600;
}
.readonly-note {
background: linear-gradient(135deg, #cde5ff, #dff0ff);
border-radius: 16px;
padding: 14px 16px;
color: #2b3e50;
font-weight: 600;
margin-bottom: 18px;
}
.actions-row {
display: flex;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-top: 18px;
}
.selected-file-name {
font-size: 0.84rem;
color: #2b3e50;
margin-top: 8px;
font-weight: 600;
word-break: break-word;
}
@media (max-width: 992px) {
.profile-layout {
grid-template-columns: 1fr;
}
.avatar-panel {
max-width: 420px;
margin: 0 auto;
width: 100%;
}
}
@media (max-width: 768px) {
.settings-body {
padding: 16px;
}
.actions-row {
flex-direction: column;
}
.btn-save-settings,
.btn-back {
width: 100%;
justify-content: center;
}
}
</style>
</head>
<body>
<div class="wrapper toggled">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="settings-wrap">
<h3 class="settings-title">Impostazioni Utente</h3>
<?php if ($successMessage): ?>
<div class="alert alert-success">
<?= e($successMessage); ?>
</div>
<?php endif; ?>
<?php if ($errorMessage): ?>
<div class="alert alert-danger">
<?= e($errorMessage); ?>
</div>
<?php endif; ?>
<form method="post" enctype="multipart/form-data" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= e($csrfToken); ?>">
<div class="settings-card">
<div class="settings-header">
<div class="settings-icon">👤</div>
<div>
<p class="settings-heading">Profilo personale</p>
<p class="settings-subtitle">Dati anagrafici, contatti e avatar utente</p>
</div>
</div>
<div class="settings-body">
<div class="readonly-note">
Ruolo, stato account e impostazioni di sicurezza avanzate non sono modificabili da questa pagina.
</div>
<div class="profile-layout">
<div class="avatar-panel">
<div class="avatar-box" id="avatarPreviewBox">
<?php if (!empty($avatarPath)): ?>
<img src="<?= e($avatarPath); ?>" class="user-img" alt="user avatar" id="avatarPreviewImage">
<?php else: ?>
<span id="avatarInitials"><?= e(getAvatarInitials($profileUser)); ?></span>
<?php endif; ?>
</div>
<p class="avatar-name">
<?= e(trim(($profileUser['first_name'] ?? '') . ' ' . ($profileUser['last_name'] ?? '')) ?: 'Utente'); ?>
</p>
<p class="avatar-email">
<?= e($profileUser['email']); ?>
</p>
<label for="avatar" class="avatar-upload-label">
Carica avatar
</label>
<input type="file"
class="avatar-upload-input"
id="avatar"
name="avatar"
accept="image/jpeg,image/png,image/webp,image/gif">
<div id="selectedFileName" class="selected-file-name"></div>
<div class="avatar-help">
Formati consentiti: JPG, PNG, WEBP, GIF.<br>
Dimensione massima: 2 MB.
</div>
<div class="profile-meta">
Stato account: <?= e($profileUser['status']); ?>
<?php if (!empty($profileUser['last_login'])): ?>
<br>Ultimo accesso: <?= e(date('d/m/Y H:i', strtotime($profileUser['last_login']))); ?>
<?php endif; ?>
</div>
</div>
<div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label" for="first_name">Nome</label>
<input type="text"
class="form-control"
id="first_name"
name="first_name"
value="<?= e($profileUser['first_name']); ?>">
</div>
<div class="col-md-6">
<label class="form-label" for="last_name">Cognome</label>
<input type="text"
class="form-control"
id="last_name"
name="last_name"
value="<?= e($profileUser['last_name']); ?>">
</div>
<div class="col-md-6">
<label class="form-label" for="email">Email</label>
<input type="email"
class="form-control"
id="email"
name="email"
value="<?= e($profileUser['email']); ?>"
required>
</div>
<div class="col-md-6">
<label class="form-label" for="phone">Telefono</label>
<input type="text"
class="form-control"
id="phone"
name="phone"
value="<?= e($profileUser['phone']); ?>">
</div>
<div class="col-md-6">
<label class="form-label" for="birthday">Data di nascita</label>
<input type="date"
class="form-control"
id="birthday"
name="birthday"
value="<?= e($profileUser['birthday']); ?>">
</div>
<div class="col-md-6">
<label class="form-label" for="country_id">Paese</label>
<select class="form-select" id="country_id" name="country_id">
<option value="">Seleziona...</option>
<?php foreach ($countries as $country): ?>
<option value="<?= (int)$country['id']; ?>"
<?= ((int)$profileUser['country_id'] === (int)$country['id']) ? 'selected' : ''; ?>>
<?= e($country['name'] . (!empty($country['iso_3166_2']) ? ' (' . $country['iso_3166_2'] . ')' : '')); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-12">
<label class="form-label" for="address">Indirizzo</label>
<input type="text"
class="form-control"
id="address"
name="address"
value="<?= e($profileUser['address']); ?>">
</div>
</div>
</div>
</div>
</div>
</div>
<div class="settings-card">
<div class="settings-header">
<div class="settings-icon">🔐</div>
<div>
<p class="settings-heading">Cambio password</p>
<p class="settings-subtitle">Compila questa sezione solo se vuoi modificare la password</p>
</div>
</div>
<div class="settings-body">
<div class="password-box">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label" for="current_password">Password attuale</label>
<input type="password"
class="form-control"
id="current_password"
name="current_password"
autocomplete="current-password">
</div>
<div class="col-md-4">
<label class="form-label" for="new_password">Nuova password</label>
<input type="password"
class="form-control"
id="new_password"
name="new_password"
autocomplete="new-password">
</div>
<div class="col-md-4">
<label class="form-label" for="confirm_password">Conferma nuova password</label>
<input type="password"
class="form-control"
id="confirm_password"
name="confirm_password"
autocomplete="new-password">
</div>
</div>
<div class="help-text">
Se lasci questi campi vuoti, la password attuale rimane invariata.
La nuova password deve avere almeno 8 caratteri.
</div>
</div>
<div class="actions-row">
<a href="production_dashboard.php" class="btn-back"> Torna alla dashboard</a>
<button type="submit" class="btn-save-settings">Salva impostazioni</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<?php include('include/footer.php'); ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const avatarInput = document.getElementById('avatar');
const previewBox = document.getElementById('avatarPreviewBox');
const selectedFileName = document.getElementById('selectedFileName');
if (!avatarInput || !previewBox) {
return;
}
avatarInput.addEventListener('change', function() {
const file = this.files && this.files[0] ? this.files[0] : null;
if (!file) {
selectedFileName.textContent = '';
return;
}
selectedFileName.textContent = file.name;
if (!file.type.startsWith('image/')) {
return;
}
const reader = new FileReader();
reader.onload = function(event) {
previewBox.innerHTML = '';
const img = document.createElement('img');
img.src = event.target.result;
img.className = 'user-img';
img.alt = 'user avatar';
previewBox.appendChild(img);
};
reader.readAsDataURL(file);
});
});
</script>
</body>
</html>
+1 -1
View File
@@ -139,7 +139,7 @@
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -2
View File
@@ -11,7 +11,6 @@
<!-- jQuery e Bootstrap --> <!-- jQuery e Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables --> <!-- DataTables -->
@@ -117,7 +116,7 @@
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -2
View File
@@ -11,7 +11,6 @@
<!-- jQuery e Bootstrap --> <!-- jQuery e Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables --> <!-- DataTables -->
@@ -111,7 +110,7 @@
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -2
View File
@@ -308,7 +308,6 @@ $worksheets = $pdo->query("
<title>Fogli di Lavoro</title> <title>Fogli di Lavoro</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css"> <link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
@@ -454,7 +453,7 @@ $worksheets = $pdo->query("
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
-18
View File
@@ -1,18 +0,0 @@
; This file is for unifying the coding style for different editors and IDEs.
; More information at https://editorconfig.org
root = true
[*]
charset = utf-8
indent_size = 4
indent_style = space
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.yml]
indent_size = 2
@@ -1,25 +0,0 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text eol=lf
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.c text
*.h text
# Declare files that will always have CRLF line endings on checkout.
*.sln text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary
*.otf binary
*.eot binary
*.svg binary
*.ttf binary
*.woff binary
*.woff2 binary
*.css linguist-vendored
*.scss linguist-vendored
*.js linguist-vendored
CHANGELOG.md export-ignore
@@ -1,37 +0,0 @@
name: Tests
on: [push, pull_request]
jobs:
tests:
name: PHP ${{ matrix.php }}
runs-on: ubuntu-latest
strategy:
matrix:
php: ['7.3', '7.4', '8.0', '8.1']
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Cache composer
uses: actions/cache@v1
with:
path: ~/.composer/cache/files
key: php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extension-csv: bcmath, ctype, dom, fileinfo, intl, gd, json, mbstring, pdo, pdo_sqlite, openssl, sqlite, xml, zip
coverage: none
- name: Install composer
run: composer install --no-interaction --no-scripts --no-suggest --prefer-source
- name: Execute tests
run: vendor/bin/phpunit
@@ -1,9 +0,0 @@
/.idea
/.history
/.vscode
/tests/databases
/vendor
.DS_Store
.phpunit.result.cache
composer.phar
composer.lock
-4
View File
@@ -1,4 +0,0 @@
preset: psr2
enabled:
- concat_with_spaces
-23
View File
@@ -1,23 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Andreas Lutro
Copyright (c) 2017 Akaunting
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-186
View File
@@ -1,186 +0,0 @@
# Persistent settings package for Laravel
[![Downloads](https://poser.pugx.org/akaunting/laravel-setting/d/total.svg)](https://github.com/akaunting/laravel-setting)
[![StyleCI](https://styleci.io/repos/101231817/shield?style=flat&branch=master)](https://styleci.io/repos/101231817)
[![License](https://poser.pugx.org/akaunting/laravel-setting/license.svg)](LICENSE.md)
This package allows you to save settings in a more persistent way. You can use the database and/or json file to save your settings. You can also override the Laravel config.
* Driver support
* Helper function
* Blade directive
* Override config values
* Encryption
* Custom file, table and columns
* Auto save
* Extra columns
* Cache support
## Getting Started
### 1. Install
Run the following command:
```bash
composer require akaunting/laravel-setting
```
### 2. Register (for Laravel < 5.5)
Register the service provider in `config/app.php`
```php
Akaunting\Setting\Provider::class,
```
Add alias if you want to use the facade.
```php
'Setting' => Akaunting\Setting\Facade::class,
```
### 3. Publish
Publish config file.
```bash
php artisan vendor:publish --tag=setting
```
### 4. Database
Create table for database driver
```bash
php artisan migrate
```
### 5. Configure
You can change the options of your app from `config/setting.php` file
## Usage
You can either use the helper method like `setting('foo')` or the facade `Setting::get('foo')`
### Facade
```php
Setting::get('foo', 'default');
Setting::get('nested.element');
Setting::set('foo', 'bar');
Setting::forget('foo');
$settings = Setting::all();
```
### Helper
```php
setting('foo', 'default');
setting('nested.element');
setting(['foo' => 'bar']);
setting()->forget('foo');
$settings = setting()->all();
```
You can call the `save()` method to save the changes.
### Auto Save
If you enable the `auto_save` option in the config file, settings will be saved automatically every time the application shuts down if anything has been changed.
### Blade Directive
You can get the settings directly in your blade templates using the helper method or the blade directive like `@setting('foo')`
### Override Config Values
You can easily override default config values by adding them to the `override` option in `config/setting.php`, thereby eliminating the need to modify the default config files and also allowing you to change said values during production. Ex:
```php
'override' => [
"app.name" => "app_name",
"app.env" => "app_env",
"mail.driver" => "app_mail_driver",
"mail.host" => "app_mail_host",
],
```
The values on the left corresponds to the respective config value (Ex: config('app.name')) and the value on the right is the name of the `key` in your settings table/json file.
### Encryption
If you like to encrypt the values for a given key, you can pass the key to the `encrypted_keys` option in `config/setting.php` and the rest is automatically handled by using Laravel's built-in encryption facilities. Ex:
```php
'encrypted_keys' => [
"payment.key",
],
```
### JSON Storage
You can modify the path used on run-time using `setting()->setPath($path)`.
### Database Storage
If you want to use the database as settings storage then you should run the `php artisan migrate`. You can modify the table fields from the `create_settings_table` file in the migrations directory.
#### Extra Columns
If you want to store settings for multiple users/clients in the same database you can do so by specifying extra columns:
```php
setting()->setExtraColumns(['user_id' => Auth::user()->id]);
```
where `user_id = x` will now be added to the database query when settings are retrieved, and when new settings are saved, the `user_id` will be populated.
If you need more fine-tuned control over which data gets queried, you can use the `setConstraint` method which takes a closure with two arguments:
- `$query` is the query builder instance
- `$insert` is a boolean telling you whether the query is an insert or not. If it is an insert, you usually don't need to do anything to `$query`.
```php
setting()->setConstraint(function($query, $insert) {
if ($insert) return;
$query->where(/* ... */);
});
```
### Custom Drivers
This package uses the Laravel `Manager` class under the hood, so it's easy to add your own storage driver. All you need to do is extend the abstract `Driver` class, implement the abstract methods and call `setting()->extend`.
```php
class MyDriver extends Akaunting\Setting\Contracts\Driver
{
// ...
}
app('setting.manager')->extend('mydriver', function($app) {
return $app->make('MyDriver');
});
```
## Changelog
Please see [Releases](../../releases) for more information what has changed recently.
## Contributing
Pull requests are more than welcome. You must follow the PSR coding standards.
## Security
If you discover any security related issues, please email security@akaunting.com instead of using the issue tracker.
## Credits
- [Denis Duliçi](https://github.com/denisdulici)
- [All Contributors](../../contributors)
## License
The MIT License (MIT). Please see [LICENSE](LICENSE.md) for more information.
-48
View File
@@ -1,48 +0,0 @@
{
"name": "akaunting/laravel-setting",
"description": "Persistent settings package for Laravel",
"keywords": [
"laravel",
"persistent",
"settings",
"config"
],
"license": "MIT",
"authors": [
{
"name": "Denis Duliçi",
"email": "info@akaunting.com",
"homepage": "https://akaunting.com",
"role": "Developer"
}
],
"require": {
"php": ">=5.5.9",
"laravel/framework": ">=5.3"
},
"require-dev": {
"phpunit/phpunit": ">=4.8",
"mockery/mockery": "0.9.*",
"laravel/framework": ">=5.3"
},
"autoload": {
"psr-4": {
"Akaunting\\Setting\\": "./src"
},
"files": [
"src/helpers.php"
]
},
"extra": {
"laravel": {
"providers": [
"Akaunting\\Setting\\Provider"
],
"aliases": {
"Setting": "Akaunting\\Setting\\Facade"
}
}
},
"minimum-stability": "dev",
"prefer-stable": true
}
-18
View File
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
>
<testsuites>
<testsuite name="Package Test Suite">
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
</phpunit>
-132
View File
@@ -1,132 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Enable / Disable auto save
|--------------------------------------------------------------------------
|
| Auto-save every time the application shuts down
|
*/
'auto_save' => false,
/*
|--------------------------------------------------------------------------
| Cache
|--------------------------------------------------------------------------
|
| Options for caching. Set whether to enable cache, its key, time to live
| in seconds and whether to auto clear after save.
|
*/
'cache' => [
'enabled' => false,
'key' => 'setting',
'ttl' => 3600,
'auto_clear' => true,
],
/*
|--------------------------------------------------------------------------
| Setting driver
|--------------------------------------------------------------------------
|
| Select where to store the settings.
|
| Supported: "database", "json", "memory"
|
*/
'driver' => 'database',
/*
|--------------------------------------------------------------------------
| Database driver
|--------------------------------------------------------------------------
|
| Options for database driver. Enter which connection to use, null means
| the default connection. Set the table and column names.
|
*/
'database' => [
'connection' => null,
'table' => 'settings',
'key' => 'key',
'value' => 'value',
],
/*
|--------------------------------------------------------------------------
| JSON driver
|--------------------------------------------------------------------------
|
| Options for json driver. Enter the full path to the .json file.
|
*/
'json' => [
'path' => storage_path() . '/settings.json',
],
/*
|--------------------------------------------------------------------------
| Override application config values
|--------------------------------------------------------------------------
|
| If defined, settings package will override these config values.
|
| Sample:
| "app.locale" => "settings.locale",
|
*/
'override' => [
],
/*
|--------------------------------------------------------------------------
| Fallback
|--------------------------------------------------------------------------
|
| Define fallback settings to be used in case the default is null
|
| Sample:
| "currency" => "USD",
|
*/
'fallback' => [
],
/*
|--------------------------------------------------------------------------
| Required Extra Columns
|--------------------------------------------------------------------------
|
| The list of columns required to be set up
|
| Sample:
| "user_id",
| "tenant_id",
|
*/
'required_extra_columns' => [
],
/*
|--------------------------------------------------------------------------
| Encryption
|--------------------------------------------------------------------------
|
| Define the keys which should be crypt automatically.
|
| Sample:
| "payment.key"
|
*/
'encrypted_keys' => [
],
];
@@ -1,321 +0,0 @@
<?php
namespace Akaunting\Setting\Contracts;
use Akaunting\Setting\Support\Arr;
use Illuminate\Support\Facades\Cache;
abstract class Driver
{
/**
* The settings data.
*
* @var array
*/
protected $data = [];
/**
* Whether the store has changed since it was last loaded.
*
* @var bool
*/
protected $unsaved = false;
/**
* Whether the settings data are loaded.
*
* @var bool
*/
protected $loaded = false;
/**
* Include and merge with fallbacks
*
* @var bool
*/
protected $with_fallback = true;
/**
* Excludes fallback data
*/
public function withoutFallback()
{
$this->with_fallback = false;
return $this;
}
/**
* Get a specific key from the settings data.
*
* @param string|array $key
* @param mixed $default Optional default value.
*
* @return mixed
*/
public function get($key, $default = null)
{
if (!$this->checkExtraColumns()) {
return false;
}
$this->load();
return Arr::get($this->data, $key, $default);
}
/**
* Get the fallback value if default is null.
*
* @param string|array $key
* @param mixed $default
*
* @return mixed
*/
public function getFallback($key, $default = null)
{
if (($default !== null) || is_array($key)) {
return $default;
}
return Arr::get((array) config('setting.fallback'), $key);
}
/**
* Check if the given value is same as fallback.
*
* @param string $key
* @param string $value
*
* @return bool
*/
public function isEqualToFallback($key, $value)
{
return (string) $this->getFallback($key) == (string) $value;
}
/**
* Determine if a key exists in the settings data.
*
* @param string $key
*
* @return bool
*/
public function has($key)
{
if (!$this->checkExtraColumns()) {
return false;
}
$this->load();
return Arr::has($this->data, $key);
}
/**
* Set a specific key to a value in the settings data.
*
* @param string|array $key Key string or associative array of key => value
* @param mixed $value Optional only if the first argument is an array
*/
public function set($key, $value = null)
{
if (!$this->checkExtraColumns()) {
return;
}
$this->load();
$this->unsaved = true;
if (is_array($key)) {
foreach ($key as $k => $v) {
Arr::set($this->data, $k, $v);
}
} else {
Arr::set($this->data, $key, $value);
}
}
/**
* Unset a key in the settings data.
*
* @param string $key
*/
public function forget($key)
{
if (!$this->checkExtraColumns()) {
return;
}
$this->unsaved = true;
if ($this->has($key)) {
Arr::forget($this->data, $key);
}
}
/**
* Unset all keys in the settings data.
*
* @return void
*/
public function forgetAll()
{
if (!$this->checkExtraColumns()) {
return;
}
if (config('setting.cache.enabled')) {
Cache::forget($this->getCacheKey());
}
$this->unsaved = true;
$this->data = [];
}
/**
* Get all settings data.
*
* @return array|bool
*/
public function all()
{
if (!$this->checkExtraColumns()) {
return [];
}
$this->load();
return $this->data;
}
/**
* Save any changes done to the settings data.
*
* @return void
*/
public function save()
{
if (!$this->checkExtraColumns()) {
return;
}
if (!$this->unsaved) {
// either nothing has been changed, or data has not been loaded, so
// do nothing by returning early
return;
}
if (config('setting.cache.enabled') && config('setting.cache.auto_clear')) {
Cache::forget($this->getCacheKey());
}
$this->write($this->data);
$this->unsaved = false;
}
/**
* Make sure data is loaded.
*
* @param $force Force a reload of data. Default false.
*/
public function load($force = false)
{
if (!$this->checkExtraColumns()) {
return;
}
if ($this->loaded && !$force) {
return;
}
$fallback_data = $this->with_fallback ? config('setting.fallback') : [];
$driver_data = $this->readData();
$this->data = Arr::merge((array) $fallback_data, (array) $driver_data);
$this->loaded = true;
}
/**
* Read data from driver or cache
*
* @return array
*/
public function readData()
{
if (config('setting.cache.enabled')) {
return $this->readDataFromCache();
}
return $this->read();
}
/**
* Read data from cache
*
* @return array
*/
public function readDataFromCache()
{
return Cache::remember($this->getCacheKey(), config('setting.cache.ttl'), function () {
return $this->read();
});
}
/**
* Check if extra columns are set up.
*
* @return boolean
*/
public function checkExtraColumns()
{
if (!$required_extra_columns = config('setting.required_extra_columns')) {
return true;
}
if (array_keys_exists($required_extra_columns, $this->getExtraColumns())) {
return true;
}
return false;
}
/**
* Get cache key based on extra columns.
*
* @return string
*/
public function getCacheKey()
{
$key = config('setting.cache.key');
foreach ($this->getExtraColumns() as $name => $value) {
$key .= '_' . $name . '_' . $value;
}
return $key;
}
/**
* Get extra columns added to the rows.
*
* @return array
*/
abstract protected function getExtraColumns();
/**
* Read data from driver.
*
* @return array
*/
abstract protected function read();
/**
* Write data to driver.
*
* @param array $data
*
* @return void
*/
abstract protected function write(array $data);
}
@@ -1,372 +0,0 @@
<?php
namespace Akaunting\Setting\Drivers;
use Akaunting\Setting\Contracts\Driver;
use Akaunting\Setting\Support\Arr;
use Closure;
use Illuminate\Database\Connection;
use Illuminate\Support\Arr as LaravelArr;
use Illuminate\Support\Facades\Crypt;
class Database extends Driver
{
/**
* The database connection instance.
*
* @var \Illuminate\Database\Connection
*/
protected $connection;
/**
* The table to query from.
*
* @var string
*/
protected $table;
/**
* The key column name to query from.
*
* @var string
*/
protected $key;
/**
* The value column name to query from.
*
* @var string
*/
protected $value;
/**
* Keys which should be encrypt automatically.
*
* @var array
*/
protected $encrypted_keys;
/**
* Any query constraints that should be applied.
*
* @var Closure|null
*/
protected $query_constraint;
/**
* Any extra columns that should be added to the rows.
*
* @var array
*/
protected $extra_columns = [];
/**
* @param \Illuminate\Database\Connection $connection
* @param string $table
*/
public function __construct(Connection $connection, $table = null, $key = null, $value = null, array $encrypted_keys = [])
{
$this->connection = $connection;
$this->table = $table ?: 'settings';
$this->key = $key ?: 'key';
$this->value = $value ?: 'value';
$this->encrypted_keys = $encrypted_keys;
}
/**
* Set the table to query from.
*
* @param string $table
*/
public function setTable($table)
{
$this->table = $table;
}
/**
* Set the key column name to query from.
*
* @param string $key
*/
public function setKey($key)
{
$this->key = $key;
}
/**
* Set the value column name to query from.
*
* @param string $value
*/
public function setValue($value)
{
$this->value = $value;
}
/**
* Set the query constraint.
*
* @param Closure $callback
*/
public function setConstraint(Closure $callback)
{
$this->data = [];
$this->loaded = false;
$this->query_constraint = $callback;
}
/**
* Set extra columns to be added to the rows.
*
* @param array $columns
*/
public function setExtraColumns(array $columns)
{
$this->extra_columns = $columns;
}
/**
* Get extra columns added to the rows.
*
* @return array
*/
public function getExtraColumns()
{
return $this->extra_columns;
}
/**
* {@inheritdoc}
*/
public function forget($key)
{
parent::forget($key);
// because the database driver cannot store empty arrays, remove empty
// arrays to keep data consistent before and after saving
$segments = explode('.', $key);
array_pop($segments);
while ($segments) {
$segment = implode('.', $segments);
// non-empty array - exit out of the loop
if ($this->get($segment)) {
break;
}
// remove the empty array and move on to the next segment
$this->forget($segment);
array_pop($segments);
}
}
/**
* {@inheritdoc}
*/
protected function write(array $data)
{
// Get current data
$db_data = $this->newQuery()->get([$this->key, $this->value])->toArray();
$insert_data = LaravelArr::dot($data);
$update_data = [];
$delete_keys = [];
foreach ($db_data as $db_row) {
$key = $db_row->{$this->key};
$value = $db_row->{$this->value};
$is_in_insert = $is_different_in_db = $is_same_as_fallback = false;
if (isset($insert_data[$key])) {
$is_in_insert = true;
$is_different_in_db = (string) $insert_data[$key] != (string) $value;
$is_same_as_fallback = $this->isEqualToFallback($key, $insert_data[$key]);
}
if ($is_in_insert) {
if ($is_same_as_fallback) {
// Delete if new data is same as fallback
$delete_keys[] = $key;
} elseif ($is_different_in_db) {
// Update if new data is different from db
$update_data[$key] = $insert_data[$key];
}
} else {
// Delete if current db not available in new data
$delete_keys[] = $key;
}
unset($insert_data[$key]);
}
foreach ($update_data as $key => $value) {
$value = $this->prepareValue($key, $value);
$this->newQuery()
->where($this->key, '=', $key)
->update([$this->value => $value]);
}
if ($insert_data) {
$this->newQuery(true)
->insert($this->prepareInsertData($insert_data));
}
if ($delete_keys) {
$this->newQuery()
->whereIn($this->key, $delete_keys)
->delete();
}
}
/**
* Transforms settings data into an array ready to be insterted into the
* database. Call array_dot on a multidimensional array before passing it
* into this method!
*
* @param array $data Call array_dot on a multidimensional array before passing it into this method!
*
* @return array
*/
protected function prepareInsertData(array $data)
{
$db_data = [];
if ($this->getExtraColumns()) {
foreach ($data as $key => $value) {
$value = $this->prepareValue($key, $value);
// Don't insert if same as fallback
if ($this->isEqualToFallback($key, $value)) {
continue;
}
$db_data[] = array_merge(
$this->getExtraColumns(),
[$this->key => $key, $this->value => $value]
);
}
} else {
foreach ($data as $key => $value) {
$value = $this->prepareValue($key, $value);
// Don't insert if same as fallback
if ($this->isEqualToFallback($key, $value)) {
continue;
}
$db_data[] = [$this->key => $key, $this->value => $value];
}
}
return $db_data;
}
/**
* Checks if the provided key should be encrypted or not.
* Also type casts the given value to a string so errors with booleans or integers are handeled.
* Otherwise it returns the original value.
*
* @param string $key Key to check if it's inside the encryptedValues variable.
* @param mixed $value Info: Encryption only supports strings.
*
* @return string
*/
protected function prepareValue(string $key, $value)
{
// Check if key should be encrypted
if (in_array($key, $this->encrypted_keys)) {
// Cast to string to avoid error when a user passes a boolean value
return Crypt::encryptString((string) $value);
}
return $value;
}
/**
* Checks if the provided key should be decrypted or not.
* Otherwise it returns the original value.
*
* @param string $key Key to check if it's inside the encryptedValues variable.
* @param mixed $value Info: Encryption only supports strings.
*
* @return string
*/
protected function unpackValue(string $key, $value)
{
// Check if key should be encrypted
if (in_array($key, $this->encrypted_keys)) {
// Cast to string to avoid error when a user passes a boolean value
return Crypt::decryptString((string) $value);
}
return $value;
}
/**
* {@inheritdoc}
*/
protected function read()
{
return $this->parseReadData($this->newQuery()->get());
}
/**
* Parse data coming from the database.
*
* @param array $data
*
* @return array
*/
public function parseReadData($data)
{
$results = [];
foreach ($data as $row) {
if (is_array($row)) {
$key = $row[$this->key];
$value = $row[$this->value];
} elseif (is_object($row)) {
$key = $row->{$this->key};
$value = $row->{$this->value};
} else {
$msg = 'Expected array or object, got ' . gettype($row);
throw new \UnexpectedValueException($msg);
}
// Encryption
$value = $this->unpackValue($key, $value);
Arr::set($results, $key, $value);
}
return $results;
}
/**
* Create a new query builder instance.
*
* @param bool $insert
*
* @return \Illuminate\Database\Query\Builder
*/
protected function newQuery($insert = false)
{
$query = $this->connection->table($this->table);
if (!$insert) {
foreach ($this->getExtraColumns() as $key => $value) {
$query->where($key, '=', $value);
}
}
if ($this->query_constraint !== null) {
$callback = $this->query_constraint;
$callback($query, $insert);
}
return $query;
}
}
-80
View File
@@ -1,80 +0,0 @@
<?php
namespace Akaunting\Setting\Drivers;
use Akaunting\Setting\Contracts\Driver;
use Illuminate\Filesystem\Filesystem;
class Json extends Driver
{
/**
* @param \Illuminate\Filesystem\Filesystem $files
* @param string $path
*/
public function __construct(Filesystem $files, $path = null)
{
$this->files = $files;
$this->setPath($path ?: storage_path() . '/settings.json');
}
/**
* Set the path for the JSON file.
*
* @param string $path
*/
public function setPath($path)
{
// If the file does not already exist, we will attempt to create it.
if (!$this->files->exists($path)) {
$result = $this->files->put($path, '{}');
if ($result === false) {
throw new \InvalidArgumentException("Could not write to $path.");
}
}
if (!$this->files->isWritable($path)) {
throw new \InvalidArgumentException("$path is not writable.");
}
$this->path = $path;
}
/**
* {@inheritdoc}
*/
protected function getExtraColumns()
{
return [];
}
/**
* {@inheritdoc}
*/
protected function read()
{
$contents = $this->files->get($this->path);
$data = json_decode($contents, true);
if ($data === null) {
throw new \RuntimeException("Invalid JSON in {$this->path}");
}
return $data;
}
/**
* {@inheritdoc}
*/
protected function write(array $data)
{
if ($data) {
$contents = json_encode($data);
} else {
$contents = '{}';
}
$this->files->put($this->path, $contents);
}
}
-42
View File
@@ -1,42 +0,0 @@
<?php
namespace Akaunting\Setting\Drivers;
use Akaunting\Setting\Contracts\Driver;
class Memory extends Driver
{
/**
* @param array $data
*/
public function __construct(array $data = null)
{
if ($data) {
$this->data = $data;
}
}
/**
* {@inheritdoc}
*/
protected function getExtraColumns()
{
return [];
}
/**
* {@inheritdoc}
*/
protected function read()
{
return $this->data;
}
/**
* {@inheritdoc}
*/
protected function write(array $data)
{
// do nothing
}
}
-16
View File
@@ -1,16 +0,0 @@
<?php
namespace Akaunting\Setting;
use Illuminate\Support\Facades\Facade as BaseFacade;
class Facade extends BaseFacade
{
/**
* Get the registered name of the component.
*/
public static function getFacadeAccessor()
{
return 'setting';
}
}
-63
View File
@@ -1,63 +0,0 @@
<?php
namespace Akaunting\Setting;
use Akaunting\Setting\Drivers\Database;
use Akaunting\Setting\Drivers\Json;
use Akaunting\Setting\Drivers\Memory;
use Illuminate\Support\Manager as BaseManager;
class Manager extends BaseManager
{
/**
* The container instance.
*
* @var \Illuminate\Contracts\Container\Container
*/
protected $container;
/**
* The application instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
*/
public function __construct($app = null)
{
$this->container = $app ?? app();
parent::__construct($this->container);
}
public function getDefaultDriver()
{
return config('setting.driver');
}
public function createJsonDriver()
{
$path = config('setting.json.path');
return new Json($this->container['files'], $path);
}
public function createDatabaseDriver()
{
$connection = $this->container['db']->connection(config('setting.database.connection'));
$table = config('setting.database.table');
$key = config('setting.database.key');
$value = config('setting.database.value');
$encryptedKeys = config('setting.encrypted_keys');
return new Database($connection, $table, $key, $value, $encryptedKeys);
}
public function createMemoryDriver()
{
return new Memory();
}
public function createArrayDriver()
{
return $this->createMemoryDriver();
}
}

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