Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fdc3af01f3 | |||
| 3d54140280 | |||
| bfdbbbfc8f | |||
| 40a5771a4b | |||
| 9f5a585717 | |||
| 9ec5419a86 | |||
| c05091e020 | |||
| 0b470f290e | |||
| e74870c8d3 | |||
| 9001eff317 | |||
| 7cbd74111d | |||
| 650676037a | |||
| 2fc34c3cf4 | |||
| 955a7ed9e9 | |||
| cb221a8039 | |||
| ece1beb87f | |||
| e6a805f1f7 | |||
| fe84d446e7 | |||
| 2ddf575191 | |||
| d73a8bb8d3 | |||
| d155d1cbab | |||
| fa2f293835 | |||
| fc35adc7f9 | |||
| ac942dcdc8 | |||
| 5728afa788 | |||
| 1946648b1b | |||
| a1bcab3188 | |||
| 2bbeb11726 | |||
| dd5edab2f3 | |||
| 1fadc22178 | |||
| d3ee9a3790 | |||
| c387b71cae | |||
| b2cfec77df | |||
| 73b9a3d890 | |||
| 0550ffe923 | |||
| d2e5cc8b2b | |||
| d7b6a58407 |
+4
-2
@@ -31,6 +31,8 @@ MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
|
||||
MANAGER_USER_ID=
|
||||
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
@@ -55,5 +57,5 @@ AZURE_REDIRECT_URI=https://your-app.com/auth/azure/callback
|
||||
AZURE_TENANT_ID=
|
||||
|
||||
MICROSOFT_CLIENT_ID=your_client_id_here
|
||||
MICROSOFT_CLIENT_SECRET=your_client_secret_here
|
||||
MICROSOFT_REDIRECT_URI="${APP_URL}/auth/microsoft/callback"
|
||||
MICROSOFT_CLIENT_SECRET=your_client_secret_here
|
||||
MICROSOFT_REDIRECT_URI="${APP_URL}/auth/microsoft/callback"
|
||||
|
||||
@@ -66,3 +66,6 @@ public/userarea/logsapi/commessaweb_customfields_763.json
|
||||
public/userarea/logsapi/commessaweb_invia_762.json
|
||||
public/userarea/logsapi/commessaweb_invia_763.json
|
||||
public/userarea/logsapi/last_auth_url.txt
|
||||
|
||||
# User uploaded files
|
||||
/public/userarea/files/
|
||||
@@ -111,6 +111,14 @@ class LoginController extends Controller
|
||||
return redirect()->to('userarea/production_dashboard.php');
|
||||
} elseif ($user->hasRole('User')) {
|
||||
return redirect()->to('userarea/production_dashboard.php');
|
||||
} elseif ($user->hasRole('HR')) {
|
||||
return redirect()->to('userarea/production_dashboard.php');
|
||||
} elseif ($user->hasRole('SuperUser')) {
|
||||
return redirect()->to('userarea/production_dashboard.php');
|
||||
} elseif ($user->hasRole('Management')) {
|
||||
return redirect()->to('userarea/production_dashboard.php');
|
||||
} elseif ($user->hasRole('Quality')) {
|
||||
return redirect()->to('userarea/production_dashboard.php');
|
||||
}
|
||||
|
||||
// Se il ruolo non è specificato, reindirizza alla home predefinita
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"phpmailer/phpmailer": "^6.9",
|
||||
"phpoffice/phpspreadsheet": "^4.1",
|
||||
"proengsoft/laravel-jsvalidation": "^4.0.0",
|
||||
"robmorgan/phinx": "^0.16.11",
|
||||
"socialiteproviders/microsoft": "^4.7",
|
||||
"spatie/laravel-query-builder": "^5.0",
|
||||
"vanguardapp/activity-log": "^6.0",
|
||||
|
||||
Generated
+646
-2
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "9c4f1e3bc3ee2180211c055e70635aef",
|
||||
"content-hash": "076e7721d08cfea8b06ce75dd8c6c576",
|
||||
"packages": [
|
||||
{
|
||||
"name": "akaunting/laravel-setting",
|
||||
@@ -251,6 +251,330 @@
|
||||
],
|
||||
"time": "2023-11-29T23:19:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "cakephp/chronos",
|
||||
"version": "3.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cakephp/chronos.git",
|
||||
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cakephp/chronos/zipball/e6e777b534244911566face8a5dbdbd7f7bda5a6",
|
||||
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"psr/clock": "^1.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/clock-implementation": "1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"cakephp/cakephp-codesniffer": "^5.0",
|
||||
"phpunit/phpunit": "^10.5.58 || ^11.5.3 || ^12.1.3"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Cake\\Chronos\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Brian Nesbitt",
|
||||
"email": "brian@nesbot.com",
|
||||
"homepage": "http://nesbot.com"
|
||||
},
|
||||
{
|
||||
"name": "The CakePHP Team",
|
||||
"homepage": "https://cakephp.org"
|
||||
}
|
||||
],
|
||||
"description": "A simple API extension for DateTime.",
|
||||
"homepage": "https://cakephp.org",
|
||||
"keywords": [
|
||||
"date",
|
||||
"datetime",
|
||||
"time"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/cakephp/chronos/issues",
|
||||
"source": "https://github.com/cakephp/chronos"
|
||||
},
|
||||
"time": "2026-04-10T02:50:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "cakephp/core",
|
||||
"version": "5.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cakephp/core.git",
|
||||
"reference": "eb012517900ed288f580aa3487e9a09f28ea85f9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cakephp/core/zipball/eb012517900ed288f580aa3487e9a09f28ea85f9",
|
||||
"reference": "eb012517900ed288f580aa3487e9a09f28ea85f9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"cakephp/utility": "^5.3.0",
|
||||
"league/container": "^5.1",
|
||||
"php": ">=8.2",
|
||||
"psr/container": "^1.1 || ^2.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/container-implementation": "^2.0"
|
||||
},
|
||||
"suggest": {
|
||||
"cakephp/cache": "To use Configure::store() and restore().",
|
||||
"cakephp/event": "To use PluginApplicationInterface or plugin applications.",
|
||||
"league/container": "To use Container and ServiceProvider classes"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-5.next": "5.4.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Cake\\Core\\": "."
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "CakePHP Community",
|
||||
"homepage": "https://github.com/cakephp/core/graphs/contributors"
|
||||
}
|
||||
],
|
||||
"description": "CakePHP Framework Core classes",
|
||||
"homepage": "https://cakephp.org",
|
||||
"keywords": [
|
||||
"cakephp",
|
||||
"core",
|
||||
"framework"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://stackoverflow.com/tags/cakephp",
|
||||
"irc": "irc://irc.freenode.org/cakephp",
|
||||
"issues": "https://github.com/cakephp/cakephp/issues",
|
||||
"source": "https://github.com/cakephp/core"
|
||||
},
|
||||
"time": "2026-03-31T06:25:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "cakephp/database",
|
||||
"version": "5.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cakephp/database.git",
|
||||
"reference": "cf94dcb57c54a1a308fd866b038cd6995910e36e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cakephp/database/zipball/cf94dcb57c54a1a308fd866b038cd6995910e36e",
|
||||
"reference": "cf94dcb57c54a1a308fd866b038cd6995910e36e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"cakephp/chronos": "^3.3",
|
||||
"cakephp/core": "^5.3.0",
|
||||
"cakephp/datasource": "^5.3.0",
|
||||
"php": ">=8.2",
|
||||
"psr/log": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"cakephp/i18n": "^5.3.0",
|
||||
"cakephp/log": "^5.3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"cakephp/i18n": "If you are using locale-aware datetime formats.",
|
||||
"cakephp/log": "If you want to use query logging without providing a logger yourself."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-5.next": "5.4.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Cake\\Database\\": "."
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "CakePHP Community",
|
||||
"homepage": "https://github.com/cakephp/database/graphs/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Flexible and powerful Database abstraction library with a familiar PDO-like API",
|
||||
"homepage": "https://cakephp.org",
|
||||
"keywords": [
|
||||
"abstraction",
|
||||
"cakephp",
|
||||
"database",
|
||||
"database abstraction",
|
||||
"pdo"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://stackoverflow.com/tags/cakephp",
|
||||
"irc": "irc://irc.freenode.org/cakephp",
|
||||
"issues": "https://github.com/cakephp/cakephp/issues",
|
||||
"source": "https://github.com/cakephp/database"
|
||||
},
|
||||
"time": "2026-03-31T06:25:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "cakephp/datasource",
|
||||
"version": "5.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cakephp/datasource.git",
|
||||
"reference": "512464eb27b19316b515ec338089b83822c9ab5a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cakephp/datasource/zipball/512464eb27b19316b515ec338089b83822c9ab5a",
|
||||
"reference": "512464eb27b19316b515ec338089b83822c9ab5a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"cakephp/core": "^5.3.0",
|
||||
"php": ">=8.2",
|
||||
"psr/simple-cache": "^2.0 || ^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"cakephp/cache": "^5.3.0",
|
||||
"cakephp/collection": "^5.3.0",
|
||||
"cakephp/utility": "^5.3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"cakephp/cache": "If you decide to use Query caching.",
|
||||
"cakephp/collection": "If you decide to use ResultSetInterface.",
|
||||
"cakephp/utility": "If you decide to use EntityTrait."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-5.next": "5.4.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Cake\\Datasource\\": "."
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "CakePHP Community",
|
||||
"homepage": "https://github.com/cakephp/datasource/graphs/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides connection managing and traits for Entities and Queries that can be reused for different datastores",
|
||||
"homepage": "https://cakephp.org",
|
||||
"keywords": [
|
||||
"cakephp",
|
||||
"connection management",
|
||||
"datasource",
|
||||
"entity",
|
||||
"query"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://stackoverflow.com/tags/cakephp",
|
||||
"irc": "irc://irc.freenode.org/cakephp",
|
||||
"issues": "https://github.com/cakephp/cakephp/issues",
|
||||
"source": "https://github.com/cakephp/datasource"
|
||||
},
|
||||
"time": "2026-04-04T08:08:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "cakephp/utility",
|
||||
"version": "5.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cakephp/utility.git",
|
||||
"reference": "4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cakephp/utility/zipball/4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99",
|
||||
"reference": "4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"cakephp/core": "^5.3.0",
|
||||
"php": ">=8.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "To use Text::transliterate() or Text::slug()",
|
||||
"lib-ICU": "To use Text::transliterate() or Text::slug()"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-5.next": "5.4.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Cake\\Utility\\": "."
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "CakePHP Community",
|
||||
"homepage": "https://github.com/cakephp/utility/graphs/contributors"
|
||||
}
|
||||
],
|
||||
"description": "CakePHP Utility classes such as Inflector, String, Hash, and Security",
|
||||
"homepage": "https://cakephp.org",
|
||||
"keywords": [
|
||||
"cakephp",
|
||||
"hash",
|
||||
"inflector",
|
||||
"security",
|
||||
"string",
|
||||
"utility"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://stackoverflow.com/tags/cakephp",
|
||||
"irc": "irc://irc.freenode.org/cakephp",
|
||||
"issues": "https://github.com/cakephp/cakephp/issues",
|
||||
"source": "https://github.com/cakephp/utility"
|
||||
},
|
||||
"time": "2026-03-09T09:38:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "carbonphp/carbon-doctrine-types",
|
||||
"version": "3.2.0",
|
||||
@@ -2627,6 +2951,90 @@
|
||||
],
|
||||
"time": "2022-12-11T20:36:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/container",
|
||||
"version": "5.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/container.git",
|
||||
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/container/zipball/58accbc032f0090a9bd08326f93062c5a658b2c5",
|
||||
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"psr/container": "^2.0.2",
|
||||
"psr/event-dispatcher": "^1.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/container-implementation": "^1.0"
|
||||
},
|
||||
"replace": {
|
||||
"orno/di": "~2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"nette/php-generator": "^4.1",
|
||||
"nikic/php-parser": "^5.0",
|
||||
"phpstan/phpstan": "^2.1.11",
|
||||
"phpunit/phpunit": "^10.5.45|^11.5.15|^12.0",
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"scrutinizer/ocular": "^1.9",
|
||||
"squizlabs/php_codesniffer": "^3.9"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-1.x": "1.x-dev",
|
||||
"dev-2.x": "2.x-dev",
|
||||
"dev-3.x": "3.x-dev",
|
||||
"dev-4.x": "4.x-dev",
|
||||
"dev-5.x": "5.x-dev",
|
||||
"dev-master": "5.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\Container\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Phil Bennett",
|
||||
"email": "mail@philbennett.co.uk",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A fast and intuitive dependency injection container.",
|
||||
"homepage": "https://github.com/thephpleague/container",
|
||||
"keywords": [
|
||||
"container",
|
||||
"dependency",
|
||||
"di",
|
||||
"injection",
|
||||
"league",
|
||||
"provider",
|
||||
"service"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/thephpleague/container/issues",
|
||||
"source": "https://github.com/thephpleague/container/tree/5.2.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/philipobenito",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-19T18:52:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem",
|
||||
"version": "3.28.0",
|
||||
@@ -4980,6 +5388,93 @@
|
||||
],
|
||||
"time": "2024-04-27T21:32:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "robmorgan/phinx",
|
||||
"version": "0.16.11",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cakephp/phinx.git",
|
||||
"reference": "a03014fea316ba021fc0776982e5bed2d10228d4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cakephp/phinx/zipball/a03014fea316ba021fc0776982e5bed2d10228d4",
|
||||
"reference": "a03014fea316ba021fc0776982e5bed2d10228d4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"cakephp/database": "^5.0.2",
|
||||
"composer-runtime-api": "^2.0",
|
||||
"php-64bit": ">=8.1",
|
||||
"psr/container": "^1.1|^2.0",
|
||||
"symfony/config": "^4.0|^5.0|^6.0|^7.0|^8.0",
|
||||
"symfony/console": "^6.0|^7.0|^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"cakephp/cakephp-codesniffer": "^5.0",
|
||||
"cakephp/i18n": "^5.0",
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"phpunit/phpunit": "^10.5",
|
||||
"symfony/yaml": "^4.0|^5.0|^6.0|^7.0|^8.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-json": "Install if using JSON configuration format",
|
||||
"ext-pdo": "PDO extension is needed",
|
||||
"symfony/yaml": "Install if using YAML configuration format"
|
||||
},
|
||||
"bin": [
|
||||
"bin/phinx"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Phinx\\": "src/Phinx/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Rob Morgan",
|
||||
"email": "robbym@gmail.com",
|
||||
"homepage": "https://robmorgan.id.au",
|
||||
"role": "Lead Developer"
|
||||
},
|
||||
{
|
||||
"name": "Woody Gilk",
|
||||
"email": "woody.gilk@gmail.com",
|
||||
"homepage": "https://shadowhand.me",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Richard Quadling",
|
||||
"email": "rquadling@gmail.com",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "CakePHP Community",
|
||||
"homepage": "https://github.com/cakephp/phinx/graphs/contributors",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Phinx makes it ridiculously easy to manage the database migrations for your PHP app.",
|
||||
"homepage": "https://phinx.org",
|
||||
"keywords": [
|
||||
"database",
|
||||
"database migrations",
|
||||
"db",
|
||||
"migrations",
|
||||
"phinx"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/cakephp/phinx/issues",
|
||||
"source": "https://github.com/cakephp/phinx/tree/0.16.11"
|
||||
},
|
||||
"time": "2026-03-15T00:04:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "socialiteproviders/manager",
|
||||
"version": "v4.8.1",
|
||||
@@ -5312,6 +5807,85 @@
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/config",
|
||||
"version": "v7.4.10",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/config.git",
|
||||
"reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/config/zipball/d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57",
|
||||
"reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/filesystem": "^7.1|^8.0",
|
||||
"symfony/polyfill-ctype": "~1.8"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/finder": "<6.4",
|
||||
"symfony/service-contracts": "<2.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
|
||||
"symfony/finder": "^6.4|^7.0|^8.0",
|
||||
"symfony/messenger": "^6.4|^7.0|^8.0",
|
||||
"symfony/service-contracts": "^2.5|^3",
|
||||
"symfony/yaml": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Config\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/config/tree/v7.4.10"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-03T14:20:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v7.1.3",
|
||||
@@ -5768,6 +6342,76 @@
|
||||
],
|
||||
"time": "2024-04-18T09:32:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "v7.4.11",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
|
||||
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/polyfill-ctype": "~1.8",
|
||||
"symfony/polyfill-mbstring": "~1.8"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/process": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Filesystem\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides basic utilities for the filesystem",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/filesystem/tree/v7.4.11"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-11T16:38:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v7.1.3",
|
||||
@@ -11355,6 +11999,6 @@
|
||||
"php": "^8.2.0",
|
||||
"ext-json": "*"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class BaselineExistingDatabase extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Change Method.
|
||||
*
|
||||
* Write your reversible migrations using this method.
|
||||
*
|
||||
* More information on writing migrations is available here:
|
||||
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
|
||||
*
|
||||
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||
* with the Table class.
|
||||
*/
|
||||
public function change(): void
|
||||
{
|
||||
// Baseline migration.
|
||||
// Existing database structure starts being tracked from this point.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreatePhinxTestTable extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Change Method.
|
||||
*
|
||||
* Write your reversible migrations using this method.
|
||||
*
|
||||
* More information on writing migrations is available here:
|
||||
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
|
||||
*
|
||||
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||
* with the Table class.
|
||||
*/
|
||||
public function change(): void
|
||||
{
|
||||
$table = $this->table('phinx_test_table');
|
||||
|
||||
$table
|
||||
->addColumn('name', 'string', [
|
||||
'limit' => 100,
|
||||
'null' => false,
|
||||
])
|
||||
->addColumn('created_at', 'timestamp', [
|
||||
'default' => 'CURRENT_TIMESTAMP',
|
||||
'null' => false,
|
||||
])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddFunctionsToScadDeadlines extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('scad_functions', [
|
||||
'id' => false,
|
||||
'primary_key' => ['id'],
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'encoding' => 'utf8mb4',
|
||||
])
|
||||
->addColumn('id', 'integer', [
|
||||
'identity' => true,
|
||||
'signed' => false,
|
||||
])
|
||||
->addColumn('name', 'string', [
|
||||
'limit' => 255,
|
||||
'null' => false,
|
||||
])
|
||||
->addColumn('description', 'text', [
|
||||
'null' => true,
|
||||
])
|
||||
->addColumn('status', 'string', [
|
||||
'limit' => 20,
|
||||
'null' => false,
|
||||
'default' => 'active',
|
||||
])
|
||||
->addColumn('created_at', 'timestamp', [
|
||||
'null' => false,
|
||||
'default' => 'CURRENT_TIMESTAMP',
|
||||
])
|
||||
->addColumn('updated_at', 'timestamp', [
|
||||
'null' => false,
|
||||
'default' => 'CURRENT_TIMESTAMP',
|
||||
'update' => 'CURRENT_TIMESTAMP',
|
||||
])
|
||||
->addIndex(['name'], [
|
||||
'unique' => true,
|
||||
'name' => 'uniq_scad_functions_name',
|
||||
])
|
||||
->create();
|
||||
|
||||
$this->table('scad_deadlines')
|
||||
->addColumn('function_id', 'integer', [
|
||||
'signed' => false,
|
||||
'null' => true,
|
||||
'after' => 'subject_id',
|
||||
])
|
||||
->addIndex(['function_id'], [
|
||||
'name' => 'idx_scad_deadlines_function_id',
|
||||
])
|
||||
->addForeignKey('function_id', 'scad_functions', 'id', [
|
||||
'delete' => 'SET_NULL',
|
||||
'update' => 'CASCADE',
|
||||
'constraint' => 'fk_scad_deadlines_function',
|
||||
])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
```
|
||||
@@ -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',
|
||||
];
|
||||
@@ -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()]);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()]);
|
||||
}
|
||||
@@ -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>';
|
||||
}
|
||||
@@ -0,0 +1,798 @@
|
||||
<?php
|
||||
ini_set('display_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
include('include/headscript.php');
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
/* ==========================================
|
||||
AJAX HANDLERS
|
||||
========================================== */
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['ajax'] == '1') {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
try {
|
||||
if ($action === 'add') {
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$code = trim($_POST['code'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$color = trim($_POST['color'] ?? '#6c757d');
|
||||
$sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 999;
|
||||
$is_active = isset($_POST['is_active']) ? (int)$_POST['is_active'] : 1;
|
||||
|
||||
if ($name === '') {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Department name is required.'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($code === '') {
|
||||
$code = strtoupper(str_replace(' ', '_', $name));
|
||||
$code = preg_replace('/[^A-Z0-9_]/', '', $code);
|
||||
} else {
|
||||
$code = strtoupper($code);
|
||||
$code = preg_replace('/[^A-Z0-9_]/', '', $code);
|
||||
}
|
||||
|
||||
if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $color)) {
|
||||
$color = '#6c757d';
|
||||
}
|
||||
|
||||
$is_active = $is_active === 1 ? 1 : 0;
|
||||
|
||||
$check = $pdo->prepare("
|
||||
SELECT COUNT(*)
|
||||
FROM departments
|
||||
WHERE name = :name OR code = :code
|
||||
");
|
||||
$check->execute([
|
||||
'name' => $name,
|
||||
'code' => $code
|
||||
]);
|
||||
|
||||
if ((int)$check->fetchColumn() > 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'A department with the same name or code already exists.'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO departments
|
||||
(name, code, description, color, sort_order, is_active, created_at, updated_at)
|
||||
VALUES
|
||||
(:name, :code, :description, :color, :sort_order, :is_active, NOW(), NOW())";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([
|
||||
'name' => $name,
|
||||
'code' => $code !== '' ? $code : null,
|
||||
'description' => $description !== '' ? $description : null,
|
||||
'color' => $color,
|
||||
'sort_order' => $sort_order,
|
||||
'is_active' => $is_active
|
||||
]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'edit') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$code = trim($_POST['code'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$color = trim($_POST['color'] ?? '#6c757d');
|
||||
$sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 999;
|
||||
$is_active = isset($_POST['is_active']) ? (int)$_POST['is_active'] : 1;
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Invalid department ID.'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($name === '') {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Department name is required.'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($code === '') {
|
||||
$code = strtoupper(str_replace(' ', '_', $name));
|
||||
$code = preg_replace('/[^A-Z0-9_]/', '', $code);
|
||||
} else {
|
||||
$code = strtoupper($code);
|
||||
$code = preg_replace('/[^A-Z0-9_]/', '', $code);
|
||||
}
|
||||
|
||||
if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $color)) {
|
||||
$color = '#6c757d';
|
||||
}
|
||||
|
||||
$is_active = $is_active === 1 ? 1 : 0;
|
||||
|
||||
$check = $pdo->prepare("
|
||||
SELECT COUNT(*)
|
||||
FROM departments
|
||||
WHERE (name = :name OR code = :code)
|
||||
AND id <> :id
|
||||
");
|
||||
$check->execute([
|
||||
'name' => $name,
|
||||
'code' => $code,
|
||||
'id' => $id
|
||||
]);
|
||||
|
||||
if ((int)$check->fetchColumn() > 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Another department with the same name or code already exists.'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sql = "UPDATE departments
|
||||
SET name = :name,
|
||||
code = :code,
|
||||
description = :description,
|
||||
color = :color,
|
||||
sort_order = :sort_order,
|
||||
is_active = :is_active,
|
||||
updated_at = NOW()
|
||||
WHERE id = :id";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([
|
||||
'name' => $name,
|
||||
'code' => $code !== '' ? $code : null,
|
||||
'description' => $description !== '' ? $description : null,
|
||||
'color' => $color,
|
||||
'sort_order' => $sort_order,
|
||||
'is_active' => $is_active,
|
||||
'id' => $id
|
||||
]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Invalid department ID.'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
/*
|
||||
* Future-proof check:
|
||||
* If later you add employees.department_id, this prevents deleting
|
||||
* a department already used by employees.
|
||||
*/
|
||||
$columnCheck = $pdo->prepare("
|
||||
SELECT COUNT(*)
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'employees'
|
||||
AND COLUMN_NAME = 'department_id'
|
||||
");
|
||||
$columnCheck->execute();
|
||||
$hasDepartmentId = (int)$columnCheck->fetchColumn() > 0;
|
||||
|
||||
if ($hasDepartmentId) {
|
||||
$usageCheck = $pdo->prepare("
|
||||
SELECT COUNT(*)
|
||||
FROM employees
|
||||
WHERE department_id = :id
|
||||
");
|
||||
$usageCheck->execute(['id' => $id]);
|
||||
|
||||
if ((int)$usageCheck->fetchColumn() > 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'This department is linked to one or more employees and cannot be deleted.'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM departments WHERE id = :id");
|
||||
$stmt->execute(['id' => $id]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Unknown action.'
|
||||
]);
|
||||
exit;
|
||||
} catch (Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage()
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
PAGE DATA
|
||||
========================================== */
|
||||
|
||||
$sql = "
|
||||
SELECT *
|
||||
FROM departments
|
||||
ORDER BY sort_order ASC, name ASC
|
||||
";
|
||||
$stmtDepartments = $pdo->query($sql);
|
||||
$departments = $stmtDepartments->fetchAll(PDO::FETCH_ASSOC);
|
||||
?>
|
||||
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
<?php include('cssinclude.php'); ?>
|
||||
<title>Gestione Departments - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
|
||||
<!-- jQuery and Bootstrap -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-size: 1.05rem;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.back-dashboard {
|
||||
background-color: #cfe3ff !important;
|
||||
color: #1f2d3d !important;
|
||||
border: 1px solid #bcd4f4 !important;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
padding: 10px 18px;
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.back-dashboard:hover {
|
||||
background-color: #b9d3ff !important;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
background-color: #0d6efd;
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 10px 20px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.btn-add:hover {
|
||||
background-color: #0b5ed7;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.table thead {
|
||||
background-color: #cfe3ff;
|
||||
color: #1f2d3d;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
#tabellaDepartments thead th {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.badge-status {
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge-status.active {
|
||||
background-color: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.badge-status.inactive {
|
||||
background-color: #e5e7eb;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.department-color-dot {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
vertical-align: middle;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.department-code {
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
font-size: 0.9rem;
|
||||
background: #f1f5f9;
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.description-cell {
|
||||
max-width: 320px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
<div class="card p-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Gestione Departments</h5>
|
||||
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
|
||||
↩️ Torna alla Dashboard
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-semibold mb-0">Elenco Reparti / Departments</h6>
|
||||
|
||||
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addDepartmentModal">
|
||||
➕ Aggiungi Department
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="tabellaDepartments" class="table table-striped align-middle text-center" style="width:100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Color</th>
|
||||
<th>Name</th>
|
||||
<th>Code</th>
|
||||
<th>Description</th>
|
||||
<th>Order</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<?php if (!empty($departments)): ?>
|
||||
<?php foreach ($departments as $row): ?>
|
||||
<?php
|
||||
$id = (int)$row['id'];
|
||||
$name = $row['name'] ?? '';
|
||||
$code = $row['code'] ?? '';
|
||||
$description = $row['description'] ?? '';
|
||||
$color = $row['color'] ?? '#6c757d';
|
||||
$sortOrder = (int)($row['sort_order'] ?? 999);
|
||||
$isActive = (int)($row['is_active'] ?? 1);
|
||||
|
||||
$statusClass = $isActive === 1 ? 'active' : 'inactive';
|
||||
$statusLabel = $isActive === 1 ? 'Active' : 'Inactive';
|
||||
|
||||
$createdAt = !empty($row['created_at'])
|
||||
? date('d/m/Y H:i', strtotime($row['created_at']))
|
||||
: '-';
|
||||
?>
|
||||
<tr>
|
||||
<td><?= $id ?></td>
|
||||
|
||||
<td>
|
||||
<span class="department-color-dot" style="background-color: <?= htmlspecialchars($color, ENT_QUOTES) ?>;"></span>
|
||||
<?= htmlspecialchars($color) ?>
|
||||
</td>
|
||||
|
||||
<td class="fw-semibold">
|
||||
<?= htmlspecialchars($name) ?>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<?php if ($code !== ''): ?>
|
||||
<span class="department-code"><?= htmlspecialchars($code) ?></span>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<td class="description-cell" title="<?= htmlspecialchars($description, ENT_QUOTES) ?>">
|
||||
<?= $description !== '' ? htmlspecialchars($description) : '-' ?>
|
||||
</td>
|
||||
|
||||
<td><?= $sortOrder ?></td>
|
||||
|
||||
<td>
|
||||
<span class="badge-status <?= $statusClass ?>">
|
||||
<?= htmlspecialchars($statusLabel) ?>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td><?= $createdAt ?></td>
|
||||
|
||||
<td>
|
||||
<button
|
||||
class="btn btn-sm btn-outline-secondary edit-department"
|
||||
data-id="<?= $id ?>"
|
||||
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
|
||||
data-code="<?= htmlspecialchars($code, ENT_QUOTES) ?>"
|
||||
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
|
||||
data-color="<?= htmlspecialchars($color, ENT_QUOTES) ?>"
|
||||
data-sort_order="<?= $sortOrder ?>"
|
||||
data-is_active="<?= $isActive ?>">
|
||||
✏️ Modifica
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-sm btn-outline-danger delete-department"
|
||||
data-id="<?= $id ?>"
|
||||
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>">
|
||||
🗑️ Cancella
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<!-- ADD DEPARTMENT MODAL -->
|
||||
<div class="modal fade" id="addDepartmentModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header" style="background-color:#cfe3ff;">
|
||||
<h5 class="modal-title">Aggiungi Department</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<form id="addDepartmentForm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Name</label>
|
||||
<input type="text" class="form-control" id="addName" name="name" placeholder="e.g. Produzione" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Code</label>
|
||||
<input type="text" class="form-control" id="addCode" name="code" placeholder="Optional, e.g. PRODUZIONE">
|
||||
<small class="text-muted">If empty, it will be generated automatically from the name.</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Description</label>
|
||||
<textarea class="form-control" id="addDescription" name="description" rows="3" placeholder="Optional notes"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Color</label>
|
||||
<input type="color" class="form-control form-control-color" id="addColor" name="color" value="#6c757d">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Sort Order</label>
|
||||
<input type="number" class="form-control" id="addSortOrder" name="sort_order" value="999" min="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Status</label>
|
||||
<select class="form-select" id="addIsActive" name="is_active">
|
||||
<option value="1" selected>Active</option>
|
||||
<option value="0">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<button type="submit" class="btn btn-add">💾 Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EDIT DEPARTMENT MODAL -->
|
||||
<div class="modal fade" id="editDepartmentModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header" style="background-color:#cfe3ff;">
|
||||
<h5 class="modal-title">Modifica Department</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<form id="editDepartmentForm">
|
||||
<input type="hidden" id="editDepartmentId">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Name</label>
|
||||
<input type="text" class="form-control" id="editName" name="name" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Code</label>
|
||||
<input type="text" class="form-control" id="editCode" name="code">
|
||||
<small class="text-muted">If empty, it will be generated automatically from the name.</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Description</label>
|
||||
<textarea class="form-control" id="editDescription" name="description" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Color</label>
|
||||
<input type="color" class="form-control form-control-color" id="editColor" name="color" value="#6c757d">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Sort Order</label>
|
||||
<input type="number" class="form-control" id="editSortOrder" name="sort_order" value="999" min="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Status</label>
|
||||
<select class="form-select" id="editIsActive" name="is_active">
|
||||
<option value="1">Active</option>
|
||||
<option value="0">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<button type="submit" class="btn btn-add">💾 Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#tabellaDepartments').DataTable({
|
||||
order: [
|
||||
[5, 'asc'],
|
||||
[2, 'asc']
|
||||
],
|
||||
pageLength: 25,
|
||||
language: {
|
||||
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json',
|
||||
emptyTable: 'Nessun department presente'
|
||||
}
|
||||
});
|
||||
|
||||
/* -------- ADD DEPARTMENT -------- */
|
||||
$("#addDepartmentForm").on("submit", function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const payload = new URLSearchParams();
|
||||
payload.append('ajax', '1');
|
||||
payload.append('action', 'add');
|
||||
payload.append('name', $("#addName").val().trim());
|
||||
payload.append('code', $("#addCode").val().trim());
|
||||
payload.append('description', $("#addDescription").val().trim());
|
||||
payload.append('color', $("#addColor").val());
|
||||
payload.append('sort_order', $("#addSortOrder").val());
|
||||
payload.append('is_active', $("#addIsActive").val());
|
||||
|
||||
fetch("", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
},
|
||||
body: payload.toString()
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
Swal.fire({
|
||||
icon: "success",
|
||||
title: "Saved!",
|
||||
confirmButtonColor: "#3085d6"
|
||||
}).then(() => location.reload());
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: "error",
|
||||
title: "Error",
|
||||
text: data.message || "Unable to save department."
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
Swal.fire({
|
||||
icon: "error",
|
||||
title: "Error",
|
||||
text: "Communication error."
|
||||
});
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
/* -------- OPEN EDIT MODAL -------- */
|
||||
$(document).on("click", ".edit-department", function() {
|
||||
const btn = $(this);
|
||||
|
||||
$("#editDepartmentId").val(btn.data("id"));
|
||||
$("#editName").val(btn.data("name"));
|
||||
$("#editCode").val(btn.data("code"));
|
||||
$("#editDescription").val(btn.data("description"));
|
||||
$("#editColor").val(btn.data("color") || '#6c757d');
|
||||
$("#editSortOrder").val(btn.data("sort_order"));
|
||||
$("#editIsActive").val(String(btn.data("is_active")));
|
||||
|
||||
$("#editDepartmentModal").modal("show");
|
||||
});
|
||||
|
||||
/* -------- SAVE EDIT -------- */
|
||||
$("#editDepartmentForm").on("submit", function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const payload = new URLSearchParams();
|
||||
payload.append('ajax', '1');
|
||||
payload.append('action', 'edit');
|
||||
payload.append('id', $("#editDepartmentId").val());
|
||||
payload.append('name', $("#editName").val().trim());
|
||||
payload.append('code', $("#editCode").val().trim());
|
||||
payload.append('description', $("#editDescription").val().trim());
|
||||
payload.append('color', $("#editColor").val());
|
||||
payload.append('sort_order', $("#editSortOrder").val());
|
||||
payload.append('is_active', $("#editIsActive").val());
|
||||
|
||||
fetch("", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
},
|
||||
body: payload.toString()
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
Swal.fire({
|
||||
icon: "success",
|
||||
title: "Updated!",
|
||||
confirmButtonColor: "#3085d6"
|
||||
}).then(() => location.reload());
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: "error",
|
||||
title: "Error",
|
||||
text: data.message || "Unable to update department."
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
Swal.fire({
|
||||
icon: "error",
|
||||
title: "Error",
|
||||
text: "Communication error."
|
||||
});
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
/* -------- DELETE DEPARTMENT -------- */
|
||||
$(document).on("click", ".delete-department", function() {
|
||||
const id = $(this).data("id");
|
||||
const name = $(this).data("name");
|
||||
|
||||
Swal.fire({
|
||||
title: "Confermi la cancellazione?",
|
||||
text: name ? ("Department: " + name) : "This department will be deleted.",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#d33",
|
||||
cancelButtonColor: "#6c757d",
|
||||
confirmButtonText: "Sì, cancella",
|
||||
cancelButtonText: "Annulla"
|
||||
}).then((result) => {
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
const payload = new URLSearchParams();
|
||||
payload.append('ajax', '1');
|
||||
payload.append('action', 'delete');
|
||||
payload.append('id', id);
|
||||
|
||||
fetch("", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
},
|
||||
body: payload.toString()
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
Swal.fire({
|
||||
icon: "success",
|
||||
title: "Deleted!",
|
||||
confirmButtonColor: "#3085d6"
|
||||
}).then(() => location.reload());
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: "error",
|
||||
title: "Error",
|
||||
text: data.message || "Unable to delete department."
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
Swal.fire({
|
||||
icon: "error",
|
||||
title: "Error",
|
||||
text: "Communication error."
|
||||
});
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
+337
-75
@@ -17,15 +17,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
|
||||
|
||||
try {
|
||||
if ($action === 'add') {
|
||||
// Codice originale per add
|
||||
$employee_code = trim($_POST['employee_code'] ?? '');
|
||||
$first_name = trim($_POST['first_name'] ?? '');
|
||||
$last_name = trim($_POST['last_name'] ?? '');
|
||||
$department = trim($_POST['department'] ?? '');
|
||||
$position = trim($_POST['position'] ?? '');
|
||||
$address = trim($_POST['address'] ?? '');
|
||||
$phone = trim($_POST['phone'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$department_id = $_POST['department_id'] !== '' ? (int)$_POST['department_id'] : null;
|
||||
$job_role_id = ($_POST['job_role_id'] ?? '') !== '' ? (int)$_POST['job_role_id'] : null;
|
||||
$hire_date = trim($_POST['hire_date'] ?? '');
|
||||
$status = trim($_POST['status'] ?? 'active');
|
||||
$auth_user_id = $_POST['auth_user_id'] !== '' ? (int)$_POST['auth_user_id'] : null;
|
||||
$role_id = $_POST['role_id'] !== '' ? (int)$_POST['role_id'] : null;
|
||||
|
||||
if ($first_name === '' || $last_name === '') {
|
||||
echo json_encode([
|
||||
@@ -34,42 +37,66 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
|
||||
]);
|
||||
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)) {
|
||||
$status = 'active';
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO employees
|
||||
(auth_user_id, employee_code, first_name, last_name, department, position, hire_date, status, created_at, updated_at)
|
||||
VALUES
|
||||
(:auth_user_id, :employee_code, :first_name, :last_name, :department, :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, created_at, updated_at)
|
||||
VALUES
|
||||
(: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->execute([
|
||||
'auth_user_id' => $auth_user_id,
|
||||
'employee_code' => $employee_code !== '' ? $employee_code : null,
|
||||
'first_name' => $first_name,
|
||||
'last_name' => $last_name,
|
||||
'department' => $department !== '' ? $department : null,
|
||||
'position' => $position !== '' ? $position : null,
|
||||
'address' => $address !== '' ? $address : null,
|
||||
'phone' => $phone !== '' ? $phone : null,
|
||||
'email' => $email !== '' ? $email : null,
|
||||
'department_id' => $department_id,
|
||||
'job_role_id' => $job_role_id,
|
||||
'hire_date' => $hire_date !== '' ? $hire_date : null,
|
||||
'status' => $status
|
||||
]);
|
||||
|
||||
if ($auth_user_id !== null && $role_id !== null) {
|
||||
$checkRole = $pdo->prepare("SELECT COUNT(*) FROM auth_roles WHERE id = ?");
|
||||
$checkRole->execute([$role_id]);
|
||||
|
||||
if ((int)$checkRole->fetchColumn() > 0) {
|
||||
$stmtRole = $pdo->prepare("UPDATE auth_users SET role_id = :role_id, updated_at = NOW() WHERE id = :auth_user_id");
|
||||
$stmtRole->execute([
|
||||
'role_id' => $role_id,
|
||||
'auth_user_id' => $auth_user_id
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'edit') {
|
||||
// Codice originale per edit
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$employee_code = trim($_POST['employee_code'] ?? '');
|
||||
$first_name = trim($_POST['first_name'] ?? '');
|
||||
$last_name = trim($_POST['last_name'] ?? '');
|
||||
$department = trim($_POST['department'] ?? '');
|
||||
$position = trim($_POST['position'] ?? '');
|
||||
$address = trim($_POST['address'] ?? '');
|
||||
$phone = trim($_POST['phone'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$department_id = $_POST['department_id'] !== '' ? (int)$_POST['department_id'] : null;
|
||||
$job_role_id = ($_POST['job_role_id'] ?? '') !== '' ? (int)$_POST['job_role_id'] : null;
|
||||
$hire_date = trim($_POST['hire_date'] ?? '');
|
||||
$status = trim($_POST['status'] ?? 'active');
|
||||
$auth_user_id = $_POST['auth_user_id'] !== '' ? (int)$_POST['auth_user_id'] : null;
|
||||
$role_id = $_POST['role_id'] !== '' ? (int)$_POST['role_id'] : null;
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid employee ID.']);
|
||||
@@ -83,7 +110,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
|
||||
]);
|
||||
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)) {
|
||||
$status = 'active';
|
||||
}
|
||||
@@ -93,8 +123,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
|
||||
employee_code = :employee_code,
|
||||
first_name = :first_name,
|
||||
last_name = :last_name,
|
||||
department = :department,
|
||||
position = :position,
|
||||
address = :address,
|
||||
phone = :phone,
|
||||
email = :email,
|
||||
department_id = :department_id,
|
||||
job_role_id = :job_role_id,
|
||||
hire_date = :hire_date,
|
||||
status = :status,
|
||||
updated_at = NOW()
|
||||
@@ -105,13 +138,29 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
|
||||
'employee_code' => $employee_code !== '' ? $employee_code : null,
|
||||
'first_name' => $first_name,
|
||||
'last_name' => $last_name,
|
||||
'department' => $department !== '' ? $department : null,
|
||||
'position' => $position !== '' ? $position : null,
|
||||
'address' => $address !== '' ? $address : null,
|
||||
'phone' => $phone !== '' ? $phone : null,
|
||||
'email' => $email !== '' ? $email : null,
|
||||
'department_id' => $department_id,
|
||||
'job_role_id' => $job_role_id,
|
||||
'hire_date' => $hire_date !== '' ? $hire_date : null,
|
||||
'status' => $status,
|
||||
'id' => $id
|
||||
]);
|
||||
|
||||
if ($auth_user_id !== null && $role_id !== null) {
|
||||
$checkRole = $pdo->prepare("SELECT COUNT(*) FROM auth_roles WHERE id = ?");
|
||||
$checkRole->execute([$role_id]);
|
||||
|
||||
if ((int)$checkRole->fetchColumn() > 0) {
|
||||
$stmtRole = $pdo->prepare("UPDATE auth_users SET role_id = :role_id, updated_at = NOW() WHERE id = :auth_user_id");
|
||||
$stmtRole->execute([
|
||||
'role_id' => $role_id,
|
||||
'auth_user_id' => $auth_user_id
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
@@ -193,18 +242,33 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
|
||||
// Employees list
|
||||
$sql = "
|
||||
SELECT e.*,
|
||||
d.name AS department_name,
|
||||
d.color AS department_color,
|
||||
jr.name AS job_role_name,
|
||||
au.email AS user_email,
|
||||
au.role_id AS user_role_id,
|
||||
ar.display_name AS role_display_name,
|
||||
ar.name AS role_name,
|
||||
CONCAT(COALESCE(au.first_name, ''), ' ', COALESCE(au.last_name, '')) AS user_fullname
|
||||
FROM employees e
|
||||
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_roles ar ON ar.id = au.role_id
|
||||
ORDER BY e.id DESC
|
||||
";
|
||||
$stmtEmployees = $pdo->query($sql);
|
||||
$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
|
||||
$sqlUsers = "
|
||||
SELECT id,
|
||||
role_id,
|
||||
CONCAT(
|
||||
COALESCE(first_name, ''),
|
||||
' ',
|
||||
@@ -219,6 +283,25 @@ $sqlUsers = "
|
||||
$stmtUsers = $pdo->query($sqlUsers);
|
||||
$users = $stmtUsers->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Roles list for select
|
||||
$sqlRoles = "
|
||||
SELECT id, name, display_name
|
||||
FROM auth_roles
|
||||
ORDER BY display_name, name
|
||||
";
|
||||
$stmtRoles = $pdo->query($sqlRoles);
|
||||
$roles = $stmtRoles->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Departments list for select
|
||||
$sqlDepartments = "
|
||||
SELECT id, name, code, color
|
||||
FROM departments
|
||||
WHERE is_active = 1
|
||||
ORDER BY sort_order ASC, name ASC
|
||||
";
|
||||
$stmtDepartments = $pdo->query($sqlDepartments);
|
||||
$departments = $stmtDepartments->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Skills list for JS
|
||||
$sqlSkills = "
|
||||
SELECT s.id, s.name, pl.name as line_name, pl.line_number
|
||||
@@ -242,7 +325,6 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
<!-- jQuery e Bootstrap -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
@@ -347,11 +429,20 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
background-color: #157347;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.department-badge {
|
||||
display: inline-block;
|
||||
color: #fff;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
@@ -384,14 +475,15 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Code</th>
|
||||
<th>Name</th>
|
||||
<th>Department</th>
|
||||
<th>Position</th>
|
||||
<th>Hire Date</th>
|
||||
<th>Status</th>
|
||||
<th>Linked User</th>
|
||||
<th>Actions</th>
|
||||
<th>Codice</th>
|
||||
<th>Nome</th>
|
||||
<th>Contatti</th>
|
||||
<th>Reparto</th>
|
||||
<th>Mansione</th>
|
||||
<th>Data Assunzione</th>
|
||||
<th>Stato</th>
|
||||
<th>Utente collegato</th>
|
||||
<th>Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -420,9 +512,34 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
<tr>
|
||||
<td><?= (int)$row['id'] ?></td>
|
||||
<td><?= htmlspecialchars($row['employee_code'] ?? '') ?></td>
|
||||
<td><?= htmlspecialchars($fullName) ?></td>
|
||||
<td><?= htmlspecialchars($row['department'] ?? '') ?></td>
|
||||
<td><?= htmlspecialchars($row['position'] ?? '') ?></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>
|
||||
<?php if (!empty($row['department_name'])): ?>
|
||||
<span class="department-badge" style="background-color: <?= htmlspecialchars($row['department_color'] ?? '#6c757d', ENT_QUOTES) ?>;">
|
||||
<?= htmlspecialchars($row['department_name']) ?>
|
||||
</span>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= !empty($row['job_role_name']) ? htmlspecialchars($row['job_role_name']) : '-' ?></td>
|
||||
<td><?= $hireDate ?></td>
|
||||
<td>
|
||||
<span class="badge-status <?= $statusClass ?>">
|
||||
@@ -437,11 +554,15 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
data-code="<?= htmlspecialchars($row['employee_code'] ?? '', ENT_QUOTES) ?>"
|
||||
data-first_name="<?= htmlspecialchars($row['first_name'] ?? '', ENT_QUOTES) ?>"
|
||||
data-last_name="<?= htmlspecialchars($row['last_name'] ?? '', ENT_QUOTES) ?>"
|
||||
data-department="<?= htmlspecialchars($row['department'] ?? '', ENT_QUOTES) ?>"
|
||||
data-position="<?= htmlspecialchars($row['position'] ?? '', ENT_QUOTES) ?>"
|
||||
data-department_id="<?= $row['department_id'] !== null ? (int)$row['department_id'] : '' ?>"
|
||||
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-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'] : '' ?>"
|
||||
data-role_id="<?= $row['user_role_id'] !== null ? (int)$row['user_role_id'] : '' ?>">
|
||||
✏️ Modifica
|
||||
</button>
|
||||
|
||||
@@ -477,7 +598,7 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
<!-- MODALE AGGIUNTA DIPENDENTE -->
|
||||
<div class="modal fade" id="addEmployeeModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header" style="background-color:#cfe3ff;">
|
||||
<h5 class="modal-title">Aggiungi Nuovo Dipendente</h5>
|
||||
@@ -487,59 +608,101 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
<div class="modal-body">
|
||||
<form id="addEmployeeForm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Employee Code</label>
|
||||
<input type="text" class="form-control" id="addEmployeeCode" name="employee_code" placeholder="Optional">
|
||||
<label class="form-label fw-semibold">Codice Dipendente</label>
|
||||
<input type="text" class="form-control" id="addEmployeeCode" name="employee_code" placeholder="Opzionale">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</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="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Department</label>
|
||||
<input type="text" class="form-control" id="addDepartment" name="department" placeholder="e.g. Production">
|
||||
<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">Position</label>
|
||||
<input type="text" class="form-control" id="addPosition" name="position" placeholder="e.g. Line Operator">
|
||||
<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">Hire Date</label>
|
||||
<label class="form-label fw-semibold">Reparto</label>
|
||||
<select class="form-select" id="addDepartmentId" name="department_id" style="width:100%;">
|
||||
<option value="">-- Nessuno --</option>
|
||||
<?php foreach ($departments as $d): ?>
|
||||
<option value="<?= (int)$d['id'] ?>">
|
||||
<?= htmlspecialchars($d['name']) ?>
|
||||
<?= !empty($d['code']) ? ' (' . htmlspecialchars($d['code']) . ')' : '' ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Mansione</label>
|
||||
<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 class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Data Assunzione</label>
|
||||
<input type="date" class="form-control" id="addHireDate" name="hire_date">
|
||||
</div>
|
||||
<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">
|
||||
<option value="active" selected>Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
<option value="suspended">Suspended</option>
|
||||
<option value="active" selected>Attivo</option>
|
||||
<option value="inactive">Cessato</option>
|
||||
<option value="suspended">Sospeso</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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%;">
|
||||
<option value="">-- None --</option>
|
||||
<option value="">-- Nessuno --</option>
|
||||
<?php foreach ($users as $u): ?>
|
||||
<option value="<?= (int)$u['id'] ?>">
|
||||
<option value="<?= (int)$u['id'] ?>" data-role_id="<?= (int)$u['role_id'] ?>">
|
||||
<?= htmlspecialchars($u['label']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 d-none" id="addRoleWrapper">
|
||||
<label class="form-label fw-semibold">Ruolo di accesso</label>
|
||||
<select class="form-select" id="addRoleId" name="role_id" style="width:100%;">
|
||||
<option value="">-- Seleziona ruolo --</option>
|
||||
<?php foreach ($roles as $r): ?>
|
||||
<option value="<?= (int)$r['id'] ?>">
|
||||
<?= htmlspecialchars($r['display_name'] ?: $r['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<small class="text-muted">Visibile solo quando è collegato un utente di sistema.</small>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<button type="submit" class="btn btn-add">💾 Save</button>
|
||||
</div>
|
||||
@@ -552,7 +715,7 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
<!-- MODALE EDIT DIPENDENTE -->
|
||||
<div class="modal fade" id="editEmployeeModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header" style="background-color:#cfe3ff;">
|
||||
<h5 class="modal-title">Modifica Dipendente</h5>
|
||||
@@ -564,59 +727,101 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
<input type="hidden" id="editEmployeeId">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Employee Code</label>
|
||||
<input type="text" class="form-control" id="editEmployeeCode" name="employee_code" placeholder="Optional">
|
||||
<label class="form-label fw-semibold">Codice Dipendente</label>
|
||||
<input type="text" class="form-control" id="editEmployeeCode" name="employee_code" placeholder="Opzionale">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</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="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Department</label>
|
||||
<input type="text" class="form-control" id="editDepartment" name="department">
|
||||
<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">Position</label>
|
||||
<input type="text" class="form-control" id="editPosition" name="position">
|
||||
<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">Hire Date</label>
|
||||
<label class="form-label fw-semibold">Reparto</label>
|
||||
<select class="form-select" id="editDepartmentId" name="department_id" style="width:100%;">
|
||||
<option value="">-- Nessuno --</option>
|
||||
<?php foreach ($departments as $d): ?>
|
||||
<option value="<?= (int)$d['id'] ?>">
|
||||
<?= htmlspecialchars($d['name']) ?>
|
||||
<?= !empty($d['code']) ? ' (' . htmlspecialchars($d['code']) . ')' : '' ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Mansione</label>
|
||||
<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 class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Data Assunzione</label>
|
||||
<input type="date" class="form-control" id="editHireDate" name="hire_date">
|
||||
</div>
|
||||
<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">
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
<option value="suspended">Suspended</option>
|
||||
<option value="active">Attivo</option>
|
||||
<option value="inactive">Cessato</option>
|
||||
<option value="suspended">Sospeso</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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%;">
|
||||
<option value="">-- None --</option>
|
||||
<option value="">-- Nessuno --</option>
|
||||
<?php foreach ($users as $u): ?>
|
||||
<option value="<?= (int)$u['id'] ?>">
|
||||
<option value="<?= (int)$u['id'] ?>" data-role_id="<?= (int)$u['role_id'] ?>">
|
||||
<?= htmlspecialchars($u['label']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 d-none" id="editRoleWrapper">
|
||||
<label class="form-label fw-semibold">Ruolo di accesso</label>
|
||||
<select class="form-select" id="editRoleId" name="role_id" style="width:100%;">
|
||||
<option value="">-- Seleziona ruolo --</option>
|
||||
<?php foreach ($roles as $r): ?>
|
||||
<option value="<?= (int)$r['id'] ?>">
|
||||
<?= htmlspecialchars($r['display_name'] ?: $r['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<small class="text-muted">Visibile solo quando è collegato un utente di sistema.</small>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<button type="submit" class="btn btn-add">💾 Save Changes</button>
|
||||
</div>
|
||||
@@ -669,11 +874,46 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
|
||||
// Select2 on user selects
|
||||
$('#addAuthUserId, #editAuthUserId').select2({
|
||||
$('#addAuthUserId, #editAuthUserId, #addDepartmentId, #editDepartmentId, #addRoleId, #editRoleId, #addJobRoleId, #editJobRoleId').select2({
|
||||
theme: 'bootstrap-5',
|
||||
width: '100%'
|
||||
});
|
||||
|
||||
function syncAddRoleVisibility() {
|
||||
const authUserId = $('#addAuthUserId').val();
|
||||
|
||||
if (authUserId) {
|
||||
$('#addRoleWrapper').removeClass('d-none');
|
||||
|
||||
const selectedRoleId = $('#addAuthUserId option:selected').data('role_id');
|
||||
if (selectedRoleId) {
|
||||
$('#addRoleId').val(String(selectedRoleId)).trigger('change');
|
||||
}
|
||||
} else {
|
||||
$('#addRoleWrapper').addClass('d-none');
|
||||
$('#addRoleId').val('').trigger('change');
|
||||
}
|
||||
}
|
||||
|
||||
function syncEditRoleVisibility() {
|
||||
const authUserId = $('#editAuthUserId').val();
|
||||
|
||||
if (authUserId) {
|
||||
$('#editRoleWrapper').removeClass('d-none');
|
||||
|
||||
const selectedRoleId = $('#editAuthUserId option:selected').data('role_id');
|
||||
if (selectedRoleId) {
|
||||
$('#editRoleId').val(String(selectedRoleId)).trigger('change');
|
||||
}
|
||||
} else {
|
||||
$('#editRoleWrapper').addClass('d-none');
|
||||
$('#editRoleId').val('').trigger('change');
|
||||
}
|
||||
}
|
||||
|
||||
$('#addAuthUserId').on('change', syncAddRoleVisibility);
|
||||
$('#editAuthUserId').on('change', syncEditRoleVisibility);
|
||||
|
||||
/* -------- ADD EMPLOYEE -------- */
|
||||
$("#addEmployeeForm").on("submit", function(e) {
|
||||
e.preventDefault();
|
||||
@@ -684,11 +924,15 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
payload.append('employee_code', $("#addEmployeeCode").val().trim());
|
||||
payload.append('first_name', $("#addFirstName").val().trim());
|
||||
payload.append('last_name', $("#addLastName").val().trim());
|
||||
payload.append('department', $("#addDepartment").val().trim());
|
||||
payload.append('position', $("#addPosition").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('job_role_id', $("#addJobRoleId").val() || '');
|
||||
payload.append('hire_date', $("#addHireDate").val());
|
||||
payload.append('status', $("#addStatus").val());
|
||||
payload.append('auth_user_id', $("#addAuthUserId").val() || '');
|
||||
payload.append('role_id', $("#addAuthUserId").val() ? ($("#addRoleId").val() || '') : '');
|
||||
|
||||
fetch("", {
|
||||
method: "POST",
|
||||
@@ -732,14 +976,28 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
$("#editEmployeeCode").val(btn.data("code"));
|
||||
$("#editFirstName").val(btn.data("first_name"));
|
||||
$("#editLastName").val(btn.data("last_name"));
|
||||
$("#editDepartment").val(btn.data("department"));
|
||||
$("#editPosition").val(btn.data("position"));
|
||||
$("#editDepartmentId").val(btn.data("department_id") ? String(btn.data("department_id")) : '').trigger('change');
|
||||
$("#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"));
|
||||
$("#editStatus").val(btn.data("status"));
|
||||
|
||||
const authUserId = btn.data("auth_user_id");
|
||||
$("#editAuthUserId").val(authUserId ? String(authUserId) : '').trigger('change');
|
||||
|
||||
const roleId = btn.data("role_id");
|
||||
if (authUserId && roleId) {
|
||||
$("#editRoleWrapper").removeClass('d-none');
|
||||
$("#editRoleId").val(String(roleId)).trigger('change');
|
||||
} else {
|
||||
$("#editRoleWrapper").addClass('d-none');
|
||||
$("#editRoleId").val('').trigger('change');
|
||||
}
|
||||
|
||||
$("#editEmployeeModal").modal("show");
|
||||
|
||||
$("#editEmployeeModal").modal("show");
|
||||
});
|
||||
|
||||
@@ -754,11 +1012,15 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
payload.append('employee_code', $("#editEmployeeCode").val().trim());
|
||||
payload.append('first_name', $("#editFirstName").val().trim());
|
||||
payload.append('last_name', $("#editLastName").val().trim());
|
||||
payload.append('department', $("#editDepartment").val().trim());
|
||||
payload.append('position', $("#editPosition").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('job_role_id', $("#editJobRoleId").val() || '');
|
||||
payload.append('hire_date', $("#editHireDate").val());
|
||||
payload.append('status', $("#editStatus").val());
|
||||
payload.append('auth_user_id', $("#editAuthUserId").val() || '');
|
||||
payload.append('role_id', $("#editAuthUserId").val() ? ($("#editRoleId").val() || '') : '');
|
||||
|
||||
fetch("", {
|
||||
method: "POST",
|
||||
|
||||
@@ -7,16 +7,21 @@ ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL | E_STRICT);
|
||||
// This should be equal to: PATH_TO_VANGUARD_FOLDER/extra/auth.php
|
||||
include('../../extra/auth.php');
|
||||
include(__DIR__ . '/../../../extra/auth.php');
|
||||
//require_once __DIR__ . '/extra/auth.php';
|
||||
|
||||
// Here we just check if user is not
|
||||
// Here we just check if user is not
|
||||
// logged in, and in that case we redirect
|
||||
// the user to vanguard login page.
|
||||
|
||||
if (! Auth::check()) {
|
||||
|
||||
redirectTo('../../public/login');
|
||||
// Cut everything at /userarea/ and append /login.
|
||||
$scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
|
||||
$basePath = substr($scriptName, 0, strpos($scriptName, '/userarea/'));
|
||||
if ($basePath === false || $basePath === '') {
|
||||
$basePath = '';
|
||||
}
|
||||
redirectTo($basePath . '/login');
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
@@ -35,8 +40,7 @@ $kindofrole = $user->present()->role_id;
|
||||
//$iduserlogin="1";
|
||||
//$nameuser="Claudio";
|
||||
//$emailuser="info@claudiosironi.com";
|
||||
?>
|
||||
<?php
|
||||
|
||||
if (session_status() == PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
@@ -49,13 +53,11 @@ $_SESSION["emailuser"] = $emailuser;
|
||||
$_SESSION["photouser"] = $avatar;
|
||||
$photouser = $_SESSION["photouser"];
|
||||
$photousername = basename($avatar);
|
||||
?>
|
||||
|
||||
|
||||
<?php //include files
|
||||
//include files
|
||||
|
||||
require_once(__DIR__ . '/../../languages/en/general.php');
|
||||
|
||||
//include("generalsettings.php");
|
||||
|
||||
?>
|
||||
require_once __DIR__ . '/permissions_helper.php';
|
||||
?>
|
||||
|
||||
@@ -6,101 +6,391 @@
|
||||
<div>
|
||||
<h4 class="logo-text"><?= htmlspecialchars('ZIBOGOMMA', ENT_QUOTES, 'UTF-8'); ?></h4>
|
||||
</div>
|
||||
<div class="toggle-icon ms-auto"><i class='bx bx-arrow-back'></i>
|
||||
<div class="toggle-icon ms-auto">
|
||||
<i class='bx bx-arrow-back'></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--navigation-->
|
||||
<ul class="metismenu" id="menu">
|
||||
<!-- user, admin, superuser menù -->
|
||||
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('User')) || (Auth::user()->hasRole('Superuser'))) : ?>
|
||||
|
||||
<?php if (userCan('production.dashboard.view')) : ?>
|
||||
<li>
|
||||
<a href="production_dashboard.php">
|
||||
<div class="parent-icon"><i class="bx bx-home-alt"></i>
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-home-alt"></i>
|
||||
</div>
|
||||
<div class="menu-title">Dashboard</div>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php
|
||||
$canSeeProgramming =
|
||||
userCan('production.programming.view')
|
||||
|| userCan('templates.dashboard.view')
|
||||
|| userCan('templates.create.view');
|
||||
?>
|
||||
|
||||
<?php if ($canSeeProgramming) : ?>
|
||||
<li>
|
||||
<a href="javascript:;" class="has-arrow">
|
||||
<div class="parent-icon"><i class="bx bx-category"></i>
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-category"></i>
|
||||
</div>
|
||||
<div class="menu-title">Programmazione</div>
|
||||
</a>
|
||||
<ul>
|
||||
<li> <a href="templates_dashboard.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?></a>
|
||||
</li>
|
||||
<li> <a href="insert_template_xls.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?></a>
|
||||
</li>
|
||||
|
||||
<ul>
|
||||
<?php if (userCan('templates.dashboard.view')) : ?>
|
||||
<li>
|
||||
<a href="templates_dashboard.php">
|
||||
<i class='bx bx-radio-circle'></i>
|
||||
<?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('templates.create.view')) : ?>
|
||||
<li>
|
||||
<a href="insert_template_xls.php">
|
||||
<i class='bx bx-radio-circle'></i>
|
||||
<?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('production.programming.view')) : ?>
|
||||
<li>
|
||||
<a href="produzione_programmazione_drag.php">
|
||||
<i class='bx bx-radio-circle'></i>
|
||||
Programmazione Produzione
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php
|
||||
$canSeeFunctions =
|
||||
userCan('masterdata.mescole.view')
|
||||
|| userCan('masterdata.matrici.view')
|
||||
|| userCan('masterdata.linee.view')
|
||||
|| userCan('masterdata.packaging.view')
|
||||
|| userCan('masterdata.suppliers.view')
|
||||
|| userCan('masterdata.lookup.view')
|
||||
|| userCan('masterdata.worksheets.view');
|
||||
?>
|
||||
|
||||
<?php if ($canSeeFunctions) : ?>
|
||||
<li>
|
||||
<a href="javascript:;" class="has-arrow">
|
||||
<div class="parent-icon"><i class="bx bx-category"></i>
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-category"></i>
|
||||
</div>
|
||||
<div class="menu-title">Funzioni</div>
|
||||
</a>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="mescole.php"><i class='bx bx-radio-circle'></i>Mescole</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="matrici.php"><i class='bx bx-radio-circle'></i>Matrici</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="linee.php"><i class='bx bx-radio-circle'></i>Linee di produzione</a>
|
||||
</li>
|
||||
<?php if (userCan('masterdata.mescole.view')) : ?>
|
||||
<li>
|
||||
<a href="mescole.php">
|
||||
<i class='bx bx-radio-circle'></i>Mescole
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('masterdata.matrici.view')) : ?>
|
||||
<li>
|
||||
<a href="matrici.php">
|
||||
<i class='bx bx-radio-circle'></i>Matrici
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('masterdata.linee.view')) : ?>
|
||||
<li>
|
||||
<a href="linee.php">
|
||||
<i class='bx bx-radio-circle'></i>Linee di produzione
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('masterdata.packaging.view')) : ?>
|
||||
<li>
|
||||
<a href="packaging_items.php">
|
||||
<i class='bx bx-radio-circle'></i>Imballaggi
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('masterdata.suppliers.view')) : ?>
|
||||
<li>
|
||||
<a href="suppliers.php">
|
||||
<i class='bx bx-radio-circle'></i>Suppliers
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('masterdata.lookup.view')) : ?>
|
||||
<li>
|
||||
<a href="lookup_values.php">
|
||||
<i class='bx bx-radio-circle'></i>Setup
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('masterdata.worksheets.view')) : ?>
|
||||
<li>
|
||||
<a href="worksheets.php">
|
||||
<i class='bx bx-radio-circle'></i>Fogli di lavoro
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php
|
||||
$canSeeProduction =
|
||||
userCan('production.line_view.view')
|
||||
|| userCan('production.stats.view')
|
||||
|| userCan('production.manager.view')
|
||||
|| userCan('production.manager_stats.view')
|
||||
|| userCan('warehouse.dashboard.view');
|
||||
?>
|
||||
|
||||
|
||||
|
||||
<li class="menu-label">Others</li>
|
||||
|
||||
|
||||
<?php if ($canSeeProduction) : ?>
|
||||
<li>
|
||||
<a href="https://helpdesk.cesoft.io" target="_blank">
|
||||
<div class="parent-icon"><i class="bx bx-support"></i>
|
||||
<a href="javascript:;" class="has-arrow">
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-line-chart"></i>
|
||||
</div>
|
||||
<div class="menu-title">Support</div>
|
||||
<div class="menu-title">Produzione</div>
|
||||
</a>
|
||||
|
||||
<ul>
|
||||
<?php if (userCan('production.line_view.view')) : ?>
|
||||
<li>
|
||||
<a href="production_line_view2.php">
|
||||
<i class='bx bx-radio-circle'></i>Line View
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('production.stats.view')) : ?>
|
||||
<li>
|
||||
<a href="production_stats.php">
|
||||
<i class='bx bx-radio-circle'></i>Statistiche
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('production.manager.view')) : ?>
|
||||
<li>
|
||||
<a href="manager_produzione.php">
|
||||
<i class='bx bx-radio-circle'></i>Manager
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('production.manager_stats.view')) : ?>
|
||||
<li>
|
||||
<a href="manager_stats.php">
|
||||
<i class='bx bx-radio-circle'></i>Manager Stats
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('warehouse.dashboard.view')) : ?>
|
||||
<li>
|
||||
<a href="warehouse_dashboard.php">
|
||||
<i class='bx bx-radio-circle'></i>Magazzino
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php
|
||||
endif; ?>
|
||||
<!-- admin, superuser menù -->
|
||||
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('Superuser'))) : ?>
|
||||
$canSeeServices =
|
||||
userCan('services.status.view')
|
||||
|| userCan('services.pause_reasons.view')
|
||||
|| userCan('services.tools.view');
|
||||
?>
|
||||
|
||||
<?php if ($canSeeServices) : ?>
|
||||
<li>
|
||||
<a href="javascript:;" class="has-arrow">
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-wrench"></i>
|
||||
</div>
|
||||
<div class="menu-title">Servizi</div>
|
||||
</a>
|
||||
|
||||
<ul>
|
||||
<?php if (userCan('services.status.view')) : ?>
|
||||
<li>
|
||||
<a href="production_status.php">
|
||||
<i class='bx bx-radio-circle'></i>Status
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('services.pause_reasons.view')) : ?>
|
||||
<li>
|
||||
<a href="production_pause_reasons.php">
|
||||
<i class='bx bx-radio-circle'></i>Cause di Pausa
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('services.tools.view')) : ?>
|
||||
<li>
|
||||
<a href="production_tools.php">
|
||||
<i class='bx bx-radio-circle'></i>Attrezzature
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php
|
||||
endif; ?>
|
||||
<!-- admin menù -->
|
||||
<?php if (Auth::user()->hasRole('Admin')) : ?>
|
||||
$canSeeHr =
|
||||
userCan('hr.employees.view')
|
||||
|| userCan('hr.departments.view')
|
||||
|| userCan('hr.job_roles.view')
|
||||
|| userCan('hr.training_topics.view')
|
||||
|| userCan('hr.trainings.view')
|
||||
|| userCan('hr.skills.view');
|
||||
?>
|
||||
|
||||
<?php if ($canSeeHr) : ?>
|
||||
<li>
|
||||
<a href="javascript:;" class="has-arrow">
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-group"></i>
|
||||
</div>
|
||||
<div class="menu-title">Personale</div>
|
||||
</a>
|
||||
|
||||
<ul>
|
||||
<?php if (userCan('hr.employees.view')) : ?>
|
||||
<li>
|
||||
<a href="employees.php">
|
||||
<i class='bx bx-radio-circle'></i>Dipendenti
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('hr.departments.view')) : ?>
|
||||
<li>
|
||||
<a href="departments.php">
|
||||
<i class='bx bx-radio-circle'></i>Reparti
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('hr.job_roles.view')) : ?>
|
||||
<li>
|
||||
<a href="job_roles.php">
|
||||
<i class='bx bx-radio-circle'></i>Mansioni
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('hr.training_topics.view')) : ?>
|
||||
<li>
|
||||
<a href="training_topics.php">
|
||||
<i class='bx bx-radio-circle'></i>Corsi di Formazione
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('hr.trainings.view')) : ?>
|
||||
<li>
|
||||
<a href="trainings.php">
|
||||
<i class='bx bx-radio-circle'></i>Storico Formazione
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="training_calendar.php">
|
||||
<i class='bx bx-radio-circle'></i>Calendario Formazione
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('hr.skills.view')) : ?>
|
||||
<li>
|
||||
<a href="skills.php">
|
||||
<i class='bx bx-radio-circle'></i>Skills
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php if (userCan('deadlines.view')) : ?>
|
||||
<li>
|
||||
<a href="javascript:;" class="has-arrow">
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-calendar-check"></i>
|
||||
</div>
|
||||
<div class="menu-title">Scadenzario</div>
|
||||
</a>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="scadenzario/index.php">
|
||||
<i class='bx bx-radio-circle'></i>Lista Scadenze
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="scadenzario/calendar.php">
|
||||
<i class='bx bx-radio-circle'></i>Calendario
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<li class="menu-label">Others</li>
|
||||
|
||||
<li>
|
||||
<a href="https://helpdesk.cesoft.io" target="_blank">
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-support"></i>
|
||||
</div>
|
||||
<div class="menu-title">Support</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
<?php if (userCan('users.manage')) : ?>
|
||||
<li class="menu-label">Admin Menù</li>
|
||||
|
||||
<li>
|
||||
<a href="../" target="_blank">
|
||||
<div class="parent-icon"><i class="bx bx-support"></i>
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-user-circle"></i>
|
||||
</div>
|
||||
<div class="menu-title">User Management</div>
|
||||
</a>
|
||||
</li>
|
||||
<!-- <li>
|
||||
<a href="template/index.html" target="_blank">
|
||||
<div class="parent-icon"><i class="bx bx-support"></i>
|
||||
</div>
|
||||
<div class="menu-title">Template</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://codervent.com/rocker/documentation/index.html" target="_blank">
|
||||
<div class="parent-icon"><i class="bx bx-folder"></i>
|
||||
</div>
|
||||
<div class="menu-title">Documentation</div>
|
||||
</a>
|
||||
</li> -->
|
||||
<?php
|
||||
endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
</ul>
|
||||
<!--end navigation-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
if (!function_exists('userCan')) {
|
||||
/**
|
||||
* Check if current user has a Vanguard permission.
|
||||
* Uses Vanguard native method if available, otherwise falls back to DB check.
|
||||
*/
|
||||
function userCan($permissionName)
|
||||
{
|
||||
global $kindofrole;
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vanguard / Laravel-style methods, depending on installed version/customization.
|
||||
if (method_exists($user, 'hasPermission')) {
|
||||
return $user->hasPermission($permissionName);
|
||||
}
|
||||
|
||||
if (method_exists($user, 'hasPermissionTo')) {
|
||||
return $user->hasPermissionTo($permissionName);
|
||||
}
|
||||
|
||||
if (method_exists($user, 'can')) {
|
||||
return $user->can($permissionName);
|
||||
}
|
||||
|
||||
// Fallback: direct DB check using existing Vanguard tables.
|
||||
static $permissions = null;
|
||||
|
||||
if ($permissions === null) {
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT p.name
|
||||
FROM auth_permissions p
|
||||
INNER JOIN auth_permission_role pr ON pr.permission_id = p.id
|
||||
WHERE pr.role_id = ?
|
||||
");
|
||||
$stmt->execute([(int)$kindofrole]);
|
||||
|
||||
$permissions = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
}
|
||||
|
||||
return in_array($permissionName, $permissions, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('visibleButtons')) {
|
||||
/**
|
||||
* Filter visible buttons.
|
||||
*/
|
||||
function visibleButtons(array $buttons)
|
||||
{
|
||||
return array_values(array_filter($buttons, function ($button) {
|
||||
return empty($button['permission']) || userCan($button['permission']);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,11 @@
|
||||
<?php
|
||||
// Build an absolute URL to employee-profile.php so it works from any depth
|
||||
// (e.g. /userarea/index.php, /userarea/scadenzario/index.php).
|
||||
$__scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
|
||||
$__pos = strpos($__scriptName, '/userarea/');
|
||||
$__base = $__pos !== false ? substr($__scriptName, 0, $__pos) : '';
|
||||
$__myProfileHref = $__base . '/userarea/employee-profile.php';
|
||||
?>
|
||||
<header>
|
||||
<div class="topbar d-flex align-items-center">
|
||||
<nav class="navbar navbar-expand gap-3">
|
||||
@@ -86,7 +94,13 @@
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<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>
|
||||
</a>
|
||||
</li>
|
||||
@@ -103,4 +117,4 @@
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
</header>
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
/**
|
||||
* Training reminders widget for the production dashboard.
|
||||
* Visible to HR / manager / Admin / User / Superuser.
|
||||
*
|
||||
* Expects $pdo to be set (DBHandlerSelect connection).
|
||||
*/
|
||||
if (!isset($pdo)) {
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
}
|
||||
|
||||
$__trWidgetHr = isset($user)
|
||||
&& ( $user->hasRole('Admin')
|
||||
|| $user->hasRole('Superuser')
|
||||
|| $user->hasRole('employee-hr')
|
||||
|| $user->hasRole('manager'));
|
||||
|
||||
if (!$__trWidgetHr) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Only the most recent record per (employee, topic) — older history rows ignored. */
|
||||
$__trRows = $pdo->query("
|
||||
SELECT et.id,
|
||||
et.next_due_date,
|
||||
et.reminder_days,
|
||||
tt.default_reminder_days
|
||||
FROM employee_trainings et
|
||||
JOIN training_topics tt ON tt.id = et.training_topic_id
|
||||
WHERE et.next_due_date IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM employee_trainings et2
|
||||
WHERE et2.employee_id = et.employee_id
|
||||
AND et2.training_topic_id = et.training_topic_id
|
||||
AND (et2.completed_date > et.completed_date
|
||||
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
|
||||
)
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$__expiredCount = 0;
|
||||
$__dueSoonCount = 0;
|
||||
$__today = new DateTime('today');
|
||||
foreach ($__trRows as $__r) {
|
||||
$__rem = $__r['reminder_days'] !== null
|
||||
? (int)$__r['reminder_days']
|
||||
: ($__r['default_reminder_days'] !== null ? (int)$__r['default_reminder_days'] : 30);
|
||||
$__due = DateTime::createFromFormat('Y-m-d', $__r['next_due_date']);
|
||||
if (!$__due) continue;
|
||||
$__days = (int)$__today->diff($__due)->format('%r%a');
|
||||
if ($__days < 0) { $__expiredCount++; }
|
||||
elseif ($__days <= $__rem) { $__dueSoonCount++; }
|
||||
}
|
||||
|
||||
/* Missing mandatory trainings (status = not_present) */
|
||||
$__notPresentCount = (int)$pdo->query("
|
||||
SELECT COUNT(*)
|
||||
FROM employees e
|
||||
CROSS JOIN training_topics tt
|
||||
WHERE tt.is_active = 1 AND tt.is_mandatory = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM employee_trainings et
|
||||
WHERE et.employee_id = e.id AND et.training_topic_id = tt.id
|
||||
)
|
||||
")->fetchColumn();
|
||||
|
||||
if ($__expiredCount === 0 && $__dueSoonCount === 0 && $__notPresentCount === 0) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<div class="my-deadlines-widgets">
|
||||
<?php if ($__expiredCount > 0): ?>
|
||||
<a class="mdw mdw-red" href="trainings.php?status=expired">
|
||||
<span class="mdw-icon"><i class="fa-solid fa-graduation-cap"></i></span>
|
||||
<span class="mdw-body">
|
||||
<span class="mdw-count"><?= (int)$__expiredCount ?></span>
|
||||
<span class="mdw-label d-block">Formazion<?= $__expiredCount === 1 ? 'e scaduta' : 'i scadute' ?></span>
|
||||
</span>
|
||||
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($__dueSoonCount > 0): ?>
|
||||
<a class="mdw mdw-orange" href="trainings.php?status=due_soon">
|
||||
<span class="mdw-icon"><i class="fa-solid fa-hourglass-half"></i></span>
|
||||
<span class="mdw-body">
|
||||
<span class="mdw-count"><?= (int)$__dueSoonCount ?></span>
|
||||
<span class="mdw-label d-block">Formazion<?= $__dueSoonCount === 1 ? 'e da aggiornare' : 'i da aggiornare' ?></span>
|
||||
</span>
|
||||
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($__notPresentCount > 0): ?>
|
||||
<a class="mdw mdw-gray" href="trainings.php?status=not_present">
|
||||
<span class="mdw-icon"><i class="fa-solid fa-circle-question"></i></span>
|
||||
<span class="mdw-body">
|
||||
<span class="mdw-count"><?= (int)$__notPresentCount ?></span>
|
||||
<span class="mdw-label d-block">Obbligator<?= $__notPresentCount === 1 ? 'ia non presente' : 'ie non presenti' ?></span>
|
||||
</span>
|
||||
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -0,0 +1,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>
|
||||
@@ -42,7 +42,6 @@ $params = $stmtParams->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
<!-- jQuery / Bootstrap / SweetAlert -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
<!-- jQuery e Bootstrap -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
@@ -118,7 +117,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -261,7 +261,7 @@ function h($v)
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -874,7 +874,7 @@ $isEdit = ($worksheet_id > 0);
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ $rows_special = array_filter($rows, function ($r) {
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -551,7 +551,7 @@ function revisionLabel($rev)
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
<!-- jQuery e Bootstrap -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
@@ -138,7 +137,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
<!-- jQuery + Bootstrap -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
@@ -133,7 +132,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
|
||||
<!-- 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 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||
|
||||
@@ -1,4 +1,184 @@
|
||||
<?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>
|
||||
<html lang="it">
|
||||
|
||||
@@ -7,7 +187,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
<?php include('cssinclude.php'); ?>
|
||||
<title>Dashboard Produzione - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
<title>Dashboard <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
|
||||
<!-- Bootstrap + jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
@@ -255,6 +435,15 @@
|
||||
background: linear-gradient(135deg, #a5b4fc, #c7d2fe);
|
||||
}
|
||||
|
||||
.btn-departments {
|
||||
background: linear-gradient(135deg, #bfdbfe, #dbeafe);
|
||||
color: #1f2d3d;
|
||||
}
|
||||
|
||||
.btn-departments:hover {
|
||||
background: linear-gradient(135deg, #93c5fd, #bfdbfe);
|
||||
}
|
||||
|
||||
.btn-setup {
|
||||
background: linear-gradient(135deg, #e5e7eb, #f3f4f6);
|
||||
}
|
||||
@@ -289,14 +478,68 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<h3 class="dashboard-title">Dashboard Produzione</h3>
|
||||
<?php $pdo = DBHandlerSelect::getInstance()->getConnection(); ?>
|
||||
<style>
|
||||
.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</h3>
|
||||
|
||||
<!-- ===== STATISTICHE PRINCIPALI ===== -->
|
||||
<div class="stats-row">
|
||||
@@ -333,183 +576,71 @@
|
||||
<!-- ===== SEZIONI COLLASSABILI ===== -->
|
||||
<div class="sections-wrap" id="prodAccordion">
|
||||
|
||||
<!-- OPERATIVO -->
|
||||
<div class="section-card">
|
||||
<button type="button" class="section-header" data-bs-toggle="collapse" data-bs-target="#secOperativo" aria-expanded="true" aria-controls="secOperativo">
|
||||
<div class="section-left">
|
||||
<div class="section-icon">🚀</div>
|
||||
<div style="min-width:0;">
|
||||
<p class="section-title">Operativo</p>
|
||||
<p class="section-subtitle">Azioni principali di produzione e attività in scadenza</p>
|
||||
<?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">
|
||||
<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-icon"><?= htmlspecialchars($section['icon'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<div style="min-width:0;">
|
||||
<p class="section-title"><?= htmlspecialchars($section['title'], ENT_QUOTES, 'UTF-8') ?></p>
|
||||
<p class="section-subtitle"><?= htmlspecialchars($section['subtitle'], ENT_QUOTES, 'UTF-8') ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chev">⌄</div>
|
||||
</button>
|
||||
<div class="chev">⌄</div>
|
||||
</button>
|
||||
|
||||
<div id="secOperativo" class="collapse show" data-bs-parent="#prodAccordion">
|
||||
<div class="section-body">
|
||||
<div class="dashboard-grid">
|
||||
<button class="dash-btn btn-programmazione" onclick="location.href='produzione_programmazione_drag.php'">
|
||||
<div class="dash-icon">🗓️</div>
|
||||
<div>Programmazione</div>
|
||||
</button>
|
||||
<div id="<?= $sectionId ?>"
|
||||
class="collapse <?= $isOpen ? 'show' : '' ?>"
|
||||
data-bs-parent="#prodAccordion">
|
||||
<div class="section-body">
|
||||
<div class="dashboard-grid">
|
||||
|
||||
<?php foreach ($buttons as $button): ?>
|
||||
<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>
|
||||
<?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='activities_deadlines.php'">
|
||||
<div class="dash-icon">⏰</div>
|
||||
<div>Scadenziario</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ANAGRAFICHE -->
|
||||
<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-left">
|
||||
<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 class="chev">⌄</div>
|
||||
</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>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<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>
|
||||
<?php if (!$hasVisibleSections): ?>
|
||||
<div class="section-card">
|
||||
<div class="section-body text-center">
|
||||
Nessuna sezione disponibile per il tuo profilo.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 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-setup" onclick="location.href='skills.php'">
|
||||
<div class="dash-icon">🧠</div>
|
||||
<div>Skills</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- /sections-wrap -->
|
||||
</div>
|
||||
<!-- /sections-wrap -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1114,7 +1114,7 @@ if (!empty($_GET['ajax'])) {
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -363,7 +363,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['action'])) {
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php');
|
||||
include('include/topbar.php'); ?>
|
||||
<div class="page-wrapper">
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Installation — Scadenzario
|
||||
|
||||
## 1. Database
|
||||
|
||||
Run the schema script:
|
||||
|
||||
```bash
|
||||
mysql -u <user> -p <database> < public/userarea/scadenzario/sql/1_create_tables.sql
|
||||
```
|
||||
|
||||
This creates 5 tables:
|
||||
|
||||
| Table | Purpose |
|
||||
|---|---|
|
||||
| `scad_deadlines` | Main deadline records |
|
||||
| `scad_deadline_employee` | M2M assignment of individual employees |
|
||||
| `scad_deadline_attachments` | File attachments |
|
||||
| `scad_deadline_histories` | Audit log (created/updated/completed/...) |
|
||||
| `scad_deadline_notifications` | Sent-notification log (deduplication) |
|
||||
|
||||
Departments are stored as a comma-separated string in `scad_deadlines.departments` (matching `employees.department` values). No separate `departments` table.
|
||||
|
||||
## 2. Filesystem permissions
|
||||
|
||||
The `attachments/` folder must be writable by the web server:
|
||||
|
||||
```bash
|
||||
chmod 755 public/userarea/scadenzario/attachments
|
||||
chown www-data:www-data public/userarea/scadenzario/attachments
|
||||
```
|
||||
|
||||
The included `.htaccess` denies direct web access. Files are served only through the auth-protected `ajax/download_attachment.php` endpoint.
|
||||
|
||||
## 3. SMTP configuration
|
||||
|
||||
Email notifications use the project-wide SMTP settings in `.env`:
|
||||
|
||||
```env
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=smtp.example.com
|
||||
MAIL_PORT=465
|
||||
MAIL_USERNAME=your_user
|
||||
MAIL_PASSWORD=your_password
|
||||
MAIL_ENCRYPTION=ssl
|
||||
MAIL_FROM_ADDRESS=scadenzario@your-domain.com
|
||||
MAIL_FROM_NAME="Scadenzario"
|
||||
APP_URL=https://your-domain.com
|
||||
```
|
||||
|
||||
## 4. Cron schedule
|
||||
|
||||
Add to the system crontab (run as the web user):
|
||||
|
||||
```cron
|
||||
0 7 * * * php /var/www/html/public/userarea/scadenzario/cron/send_notifications.php >> /var/log/scadenzario.log 2>&1
|
||||
```
|
||||
|
||||
This sends notifications daily at 07:00 for:
|
||||
|
||||
- **Approaching** — `due_date <= today + notification_days` (per-deadline lead time)
|
||||
- **Overdue** — `due_date < today`
|
||||
|
||||
Completed deadlines are skipped. Recipients without an `auth_user_id` are silently skipped.
|
||||
|
||||
## 5. Linking employees to auth users
|
||||
|
||||
For an employee to receive email notifications:
|
||||
|
||||
1. The corresponding `auth_users` row must exist with a valid `email`.
|
||||
2. Set `employees.auth_user_id` to that user ID:
|
||||
```sql
|
||||
UPDATE employees SET auth_user_id = <user_id> WHERE id = <employee_id>;
|
||||
```
|
||||
|
||||
Employees without `auth_user_id` are silently skipped by the cron.
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* Auth check for AJAX endpoints.
|
||||
* 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,146 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
$rawId = $_POST['id'] ?? $_GET['id'] ?? null;
|
||||
if ($rawId === null || !is_numeric($rawId)) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$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();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM scad_deadlines WHERE id = ? AND status = 'active'");
|
||||
$stmt->execute([$id]);
|
||||
$deadline = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$deadline) {
|
||||
echo json_encode(['success' => false, 'message' => 'Scadenza non trovata o già completata.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Mark as completed
|
||||
$pdo->prepare("UPDATE scad_deadlines SET status = 'completed', completed_at = NOW(), completed_by = ? WHERE id = ?")
|
||||
->execute([$currentUserId, $id]);
|
||||
|
||||
// History
|
||||
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action) VALUES (?, ?, 'completed')")
|
||||
->execute([$id, $currentUserId]);
|
||||
|
||||
$newId = null;
|
||||
$newDueDate = null;
|
||||
|
||||
// If recurring AND the user asked for it, create the next deadline
|
||||
if ($deadline['recurrence_type'] !== 'once' && $createNext) {
|
||||
$dueDate = new DateTime($deadline['due_date']);
|
||||
$checkDate = $deadline['check_date'] ? new DateTime($deadline['check_date']) : null;
|
||||
$documentDate = $deadline['document_date'] ? new DateTime($deadline['document_date']) : null;
|
||||
|
||||
switch ($deadline['recurrence_type']) {
|
||||
case 'monthly': $interval = new DateInterval('P1M'); break;
|
||||
case 'quarterly': $interval = new DateInterval('P3M'); break;
|
||||
case 'semiannual': $interval = new DateInterval('P6M'); break;
|
||||
case 'annual': $interval = new DateInterval('P1Y'); break;
|
||||
case 'biennial': $interval = new DateInterval('P2Y'); break;
|
||||
case 'triennial': $interval = new DateInterval('P3Y'); break;
|
||||
case 'quadriennial': $interval = new DateInterval('P4Y'); break;
|
||||
case 'quinquennial': $interval = new DateInterval('P5Y'); break;
|
||||
case 'decennial': $interval = new DateInterval('P10Y'); break;
|
||||
case 'quindecennial': $interval = new DateInterval('P15Y'); break;
|
||||
default: $interval = null;
|
||||
}
|
||||
|
||||
if ($interval) {
|
||||
$dueDate->add($interval);
|
||||
if ($checkDate) $checkDate->add($interval);
|
||||
if ($documentDate) $documentDate->add($interval);
|
||||
|
||||
$ins = $pdo->prepare("
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, function_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$ins->execute([
|
||||
$deadline['subject_id'], $deadline['function_id'], $deadline['topic'], $deadline['law_regulation'],
|
||||
$deadline['recurrence_type'], $dueDate->format('Y-m-d'),
|
||||
$checkDate ? $checkDate->format('Y-m-d') : null,
|
||||
$documentDate ? $documentDate->format('Y-m-d') : null,
|
||||
$deadline['notification_days'], $deadline['storage_location'],
|
||||
$deadline['notes'], $deadline['created_by'], $deadline['departments']
|
||||
]);
|
||||
|
||||
$newId = $pdo->lastInsertId();
|
||||
$newDueDate = $dueDate;
|
||||
|
||||
// Copy employee assignments
|
||||
$empStmt = $pdo->prepare("SELECT employee_id FROM scad_deadline_employee WHERE deadline_id = ?");
|
||||
$empStmt->execute([$id]);
|
||||
$empIds = $empStmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if (!empty($empIds)) {
|
||||
$insertEmp = $pdo->prepare("INSERT INTO scad_deadline_employee (deadline_id, employee_id) VALUES (?, ?)");
|
||||
foreach ($empIds as $empId) {
|
||||
$insertEmp->execute([$newId, $empId]);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'created', ?)")
|
||||
->execute([$newId, $currentUserId, 'Creata automaticamente dalla scadenza #' . $id]);
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
$msg = 'Scadenza completata con successo.';
|
||||
if ($newId) {
|
||||
$msg .= ' Nuova scadenza creata con data ' . $newDueDate->format('d/m/Y') . '.';
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'message' => $msg, 'new_id' => $newId]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
if (isset($pdo) && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int)$_GET['id'];
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM scad_deadline_attachments WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$att = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$att) {
|
||||
echo json_encode(['success' => false, 'message' => 'Allegato non trovato.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 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'];
|
||||
if (file_exists($filePath)) {
|
||||
unlink($filePath);
|
||||
}
|
||||
$action = 'attachment_removed';
|
||||
$message = 'Allegato eliminato.';
|
||||
}
|
||||
|
||||
// History
|
||||
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, ?, ?)")
|
||||
->execute([$att['deadline_id'], $currentUserId, $action, $att['original_name']]);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => $message]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int)$_GET['id'];
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$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->execute([$id]);
|
||||
|
||||
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.']);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => 'Scadenza non trovata.']);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
http_response_code(400);
|
||||
echo 'ID non valido.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int)$_GET['id'];
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM scad_deadline_attachments WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$att = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$att) {
|
||||
http_response_code(404);
|
||||
echo 'Allegato non trovato.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$filePath = __DIR__ . '/../attachments/' . $att['stored_name'];
|
||||
if (!file_exists($filePath)) {
|
||||
http_response_code(404);
|
||||
echo 'File non trovato sul server.';
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Type: ' . ($att['mime_type'] ?: 'application/octet-stream'));
|
||||
header('Content-Disposition: attachment; filename="' . addslashes($att['original_name']) . '"');
|
||||
header('Content-Length: ' . filesize($filePath));
|
||||
readfile($filePath);
|
||||
exit;
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$start = $_GET['start'] ?? null;
|
||||
$end = $_GET['end'] ?? null;
|
||||
$filterStatus = $_GET['status'] ?? '';
|
||||
$filterDept = $_GET['department'] ?? '';
|
||||
$filterEmployee = $_GET['employee'] ?? '';
|
||||
|
||||
$sql = "SELECT DISTINCT d.id, d.topic, d.due_date, d.status, d.notification_days, d.departments,
|
||||
s.name AS subject_name, s.color AS subject_color
|
||||
FROM scad_deadlines d
|
||||
LEFT JOIN scad_subjects s ON s.id = d.subject_id
|
||||
LEFT JOIN scad_deadline_employee de ON de.deadline_id = d.id
|
||||
LEFT JOIN employees e ON e.id = de.employee_id";
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($start && $end) {
|
||||
$where[] = "d.due_date >= ? AND d.due_date <= ?";
|
||||
$params[] = $start;
|
||||
$params[] = $end;
|
||||
}
|
||||
|
||||
if ($filterStatus === 'non-completata') {
|
||||
$where[] = "d.status != 'completed'";
|
||||
} elseif ($filterStatus === 'completata') {
|
||||
$where[] = "d.status = 'completed'";
|
||||
} elseif ($filterStatus === 'scaduta') {
|
||||
$where[] = "d.status = 'active' AND d.due_date < CURDATE()";
|
||||
} elseif ($filterStatus === 'in-scadenza') {
|
||||
$where[] = "d.status = 'active' AND d.due_date >= CURDATE() AND d.due_date <= DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)";
|
||||
} elseif ($filterStatus === 'attiva') {
|
||||
$where[] = "d.status = 'active' AND d.due_date > DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)";
|
||||
}
|
||||
|
||||
if ($filterDept) {
|
||||
$where[] = "(e.department = ? OR FIND_IN_SET(?, d.departments))";
|
||||
$params[] = $filterDept;
|
||||
$params[] = $filterDept;
|
||||
}
|
||||
|
||||
if ($filterEmployee) {
|
||||
$where[] = "CONCAT(e.first_name, ' ', e.last_name) = ?";
|
||||
$params[] = $filterEmployee;
|
||||
}
|
||||
|
||||
if (!empty($where)) {
|
||||
$sql .= " WHERE " . implode(' AND ', $where);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$events = [];
|
||||
|
||||
foreach ($deadlines as $d) {
|
||||
$isCompleted = $d['status'] === 'completed';
|
||||
$isOverdue = !$isCompleted && $d['due_date'] < $today;
|
||||
$approachDate = date('Y-m-d', strtotime($today . ' + ' . (int)$d['notification_days'] . ' days'));
|
||||
$isApproaching = !$isCompleted && !$isOverdue && $d['due_date'] <= $approachDate;
|
||||
|
||||
if ($isCompleted) { $color = '#198754'; }
|
||||
elseif ($isOverdue) { $color = '#dc3545'; }
|
||||
elseif ($isApproaching) { $color = '#e8930c'; }
|
||||
else { $color = '#5a8fd8'; }
|
||||
|
||||
$title = $d['topic'];
|
||||
if (!empty($d['subject_name'])) $title = $d['subject_name'] . ': ' . $title;
|
||||
|
||||
$events[] = [
|
||||
'id' => $d['id'],
|
||||
'title' => $title,
|
||||
'start' => $d['due_date'],
|
||||
'backgroundColor' => $color,
|
||||
'borderColor' => !empty($d['subject_color']) ? $d['subject_color'] : $color,
|
||||
'url' => 'scadenzario/detail.php?id=' . $d['id'],
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode($events);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode([]);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int)$_GET['id'];
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM scad_deadlines WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$deadline = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$deadline) {
|
||||
echo json_encode(['success' => false, 'message' => 'Scadenza non trovata.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get assigned employee IDs
|
||||
$empStmt = $pdo->prepare("SELECT employee_id FROM scad_deadline_employee WHERE deadline_id = ?");
|
||||
$empStmt->execute([$id]);
|
||||
$deadline['employee_ids'] = $empStmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
// Parse departments into array
|
||||
$deadline['department_names'] = [];
|
||||
if (!empty($deadline['departments'])) {
|
||||
$deadline['department_names'] = array_map('trim', explode(',', $deadline['departments']));
|
||||
}
|
||||
|
||||
// Get attachments
|
||||
$attStmt = $pdo->prepare("SELECT id, original_name, mime_type, size, created_at FROM scad_deadline_attachments WHERE deadline_id = ? ORDER BY created_at DESC");
|
||||
$attStmt->execute([$id]);
|
||||
$deadline['attachments'] = $attStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode(['success' => true, 'data' => $deadline]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int)$_GET['id'];
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT h.*,
|
||||
au.first_name as user_first_name,
|
||||
au.last_name as user_last_name
|
||||
FROM scad_deadline_histories h
|
||||
LEFT JOIN auth_users au ON au.id = h.user_id
|
||||
WHERE h.deadline_id = ?
|
||||
ORDER BY h.created_at DESC
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$history = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Format for display
|
||||
$actionLabels = [
|
||||
'created' => 'Creata',
|
||||
'updated' => 'Modificata',
|
||||
'completed' => 'Completata',
|
||||
'attachment_added' => 'Allegato aggiunto',
|
||||
'attachment_removed' => 'Allegato rimosso',
|
||||
'notification_sent' => 'Notifica inviata'
|
||||
];
|
||||
|
||||
foreach ($history as &$h) {
|
||||
$h['action_label'] = $actionLabels[$h['action']] ?? $h['action'];
|
||||
$h['user_name'] = trim(($h['user_first_name'] ?? '') . ' ' . ($h['user_last_name'] ?? '')) ?: 'Sistema';
|
||||
$h['changes'] = $h['changes'] ? json_decode($h['changes'], true) : null;
|
||||
unset($h['user_first_name'], $h['user_last_name']);
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'data' => $history]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/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;
|
||||
$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'] ?? '');
|
||||
$law_regulation = trim($_POST['law_regulation'] ?? '') ?: null;
|
||||
$recurrence_type = $_POST['recurrence_type'] ?? 'once';
|
||||
$due_date = $_POST['due_date'] ?? '';
|
||||
$check_date = trim($_POST['check_date'] ?? '') ?: null;
|
||||
$document_date = trim($_POST['document_date'] ?? '') ?: null;
|
||||
$notification_days = isset($_POST['notification_days']) && is_numeric($_POST['notification_days']) ? (int)$_POST['notification_days'] : 7;
|
||||
$storage_location = trim($_POST['storage_location'] ?? '') ?: null;
|
||||
$notes = trim($_POST['notes'] ?? '') ?: null;
|
||||
$employee_ids = $_POST['employee_ids'] ?? [];
|
||||
$department_names = $_POST['department_names'] ?? [];
|
||||
|
||||
// Validation
|
||||
if ($topic === '') {
|
||||
echo json_encode(['success' => false, 'message' => 'Il campo Tema è obbligatorio.']);
|
||||
exit;
|
||||
}
|
||||
if ($due_date === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $due_date)) {
|
||||
echo json_encode(['success' => false, 'message' => 'La data di scadenza è obbligatoria.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$validRecurrences = ['once', 'monthly', 'quarterly', 'semiannual', 'annual', 'biennial', 'triennial', 'quadriennial', 'quinquennial', 'decennial', 'quindecennial'];
|
||||
if (!in_array($recurrence_type, $validRecurrences)) {
|
||||
$recurrence_type = 'once';
|
||||
}
|
||||
|
||||
if (!is_array($employee_ids)) {
|
||||
$employee_ids = [];
|
||||
}
|
||||
$employee_ids = array_filter(array_map('intval', $employee_ids));
|
||||
|
||||
if (!is_array($department_names)) {
|
||||
$department_names = [];
|
||||
}
|
||||
$department_names = array_filter(array_map('trim', $department_names));
|
||||
$departmentsStr = !empty($department_names) ? implode(', ', $department_names) : null;
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
if ($id) {
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE scad_deadlines SET
|
||||
subject_id = ?, function_id = ?, topic = ?, law_regulation = ?, recurrence_type = ?,
|
||||
due_date = ?, check_date = ?, document_date = ?, notification_days = ?,
|
||||
storage_location = ?, notes = ?, departments = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([
|
||||
$subject_id,
|
||||
$function_id,
|
||||
$topic,
|
||||
$law_regulation,
|
||||
$recurrence_type,
|
||||
$due_date,
|
||||
$check_date,
|
||||
$document_date,
|
||||
$notification_days,
|
||||
$storage_location,
|
||||
$notes,
|
||||
$departmentsStr,
|
||||
$id
|
||||
]);
|
||||
|
||||
// Re-link employees
|
||||
$pdo->prepare("DELETE FROM scad_deadline_employee WHERE deadline_id = ?")->execute([$id]);
|
||||
|
||||
// History
|
||||
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action) VALUES (?, ?, 'updated')")
|
||||
->execute([$id, $currentUserId ?: null]);
|
||||
|
||||
$deadlineId = $id;
|
||||
} else {
|
||||
// INSERT
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, function_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([
|
||||
$subject_id,
|
||||
$function_id,
|
||||
$topic,
|
||||
$law_regulation,
|
||||
$recurrence_type,
|
||||
$due_date,
|
||||
$check_date,
|
||||
$document_date,
|
||||
$notification_days,
|
||||
$storage_location,
|
||||
$notes,
|
||||
$currentUserId,
|
||||
$departmentsStr
|
||||
]);
|
||||
|
||||
$deadlineId = $pdo->lastInsertId();
|
||||
|
||||
// History
|
||||
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action) VALUES (?, ?, 'created')")
|
||||
->execute([$deadlineId, $currentUserId ?: null]);
|
||||
}
|
||||
|
||||
// Link employees
|
||||
if (!empty($employee_ids)) {
|
||||
$insertEmployee = $pdo->prepare("INSERT INTO scad_deadline_employee (deadline_id, employee_id) VALUES (?, ?)");
|
||||
foreach ($employee_ids as $empId) {
|
||||
$insertEmployee->execute([$deadlineId, $empId]);
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => $id ? 'Scadenza aggiornata con successo.' : 'Scadenza creata con successo.',
|
||||
'id' => $deadlineId
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
if (isset($pdo) && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
if (!isset($_POST['deadline_id']) || !is_numeric($_POST['deadline_id'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID scadenza non valido.']);
|
||||
exit;
|
||||
}
|
||||
if (empty($_FILES['files']['name'][0])) {
|
||||
echo json_encode(['success' => false, 'message' => 'Nessun file selezionato.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$deadlineId = (int)$_POST['deadline_id'];
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
// Verify deadline exists
|
||||
$check = $pdo->prepare("SELECT id FROM scad_deadlines WHERE id = ?");
|
||||
$check->execute([$deadlineId]);
|
||||
if (!$check->fetch()) {
|
||||
echo json_encode(['success' => false, 'message' => 'Scadenza non trovata.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$uploadDir = __DIR__ . '/../attachments/';
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
|
||||
$inserted = [];
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO scad_deadline_attachments (deadline_id, original_name, stored_name, mime_type, size, uploaded_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$histStmt = $pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'attachment_added', ?)");
|
||||
|
||||
$fileCount = count($_FILES['files']['name']);
|
||||
for ($i = 0; $i < $fileCount; $i++) {
|
||||
if ($_FILES['files']['error'][$i] !== UPLOAD_ERR_OK) continue;
|
||||
|
||||
$originalName = $_FILES['files']['name'][$i];
|
||||
$mimeType = $_FILES['files']['type'][$i];
|
||||
$size = $_FILES['files']['size'][$i];
|
||||
$storedName = uniqid('att_') . '_' . preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName);
|
||||
|
||||
if (!move_uploaded_file($_FILES['files']['tmp_name'][$i], $uploadDir . $storedName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stmt->execute([$deadlineId, $originalName, $storedName, $mimeType, $size, $currentUserId]);
|
||||
$histStmt->execute([$deadlineId, $currentUserId, $originalName]);
|
||||
$inserted[] = ['id' => $pdo->lastInsertId(), 'original_name' => $originalName, 'stored_name' => $storedName];
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => count($inserted) . ' file caricato/i con successo.',
|
||||
'files' => $inserted
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
if (isset($pdo) && $pdo->inTransaction()) $pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
*
|
||||
!.gitignore
|
||||
!.htaccess
|
||||
@@ -0,0 +1 @@
|
||||
Deny from all
|
||||
@@ -0,0 +1,275 @@
|
||||
<?php include('../include/headscript.php'); ?>
|
||||
<?php
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
$employees = $pdo->query("SELECT id, first_name, last_name, department FROM employees WHERE status = 'active' ORDER BY first_name")->fetchAll(PDO::FETCH_ASSOC);
|
||||
$departments = $pdo->query("SELECT DISTINCT department FROM employees WHERE department IS NOT NULL AND department != '' ORDER BY department")->fetchAll(PDO::FETCH_COLUMN);
|
||||
?>
|
||||
<!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($scriptDir) . '/';
|
||||
?>
|
||||
<base href="<?= $baseHref ?>">
|
||||
<?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 - Scadenzario</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.05rem;
|
||||
}
|
||||
.scad-card .card-body { padding: 1.25rem; }
|
||||
.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; transition: all 0.2s;
|
||||
}
|
||||
.btn-scad-outline:hover { background: var(--scad-primary); color: #fff; }
|
||||
.scad-breadcrumb { background: transparent; padding: 0; margin-bottom: 1rem; }
|
||||
.scad-breadcrumb .breadcrumb-item a { color: var(--scad-primary); text-decoration: none; font-weight: 500; }
|
||||
.scad-breadcrumb .breadcrumb-item a:hover { color: var(--scad-primary-hover); }
|
||||
.scad-breadcrumb .breadcrumb-item.active { color: #6c757d; font-weight: 600; }
|
||||
|
||||
/* FullCalendar overrides */
|
||||
.fc { font-size: 0.9rem; }
|
||||
.fc .fc-toolbar-title { font-size: 1.15rem; font-weight: 700; color: var(--scad-heading); }
|
||||
.fc .fc-button-primary {
|
||||
background: var(--scad-primary); border-color: var(--scad-primary);
|
||||
font-weight: 600; font-size: 0.82rem; border-radius: 0.4rem;
|
||||
}
|
||||
.fc .fc-button-primary:hover { background: var(--scad-primary-hover); border-color: var(--scad-primary-hover); }
|
||||
.fc .fc-button-primary:disabled { background: #9bbce6; border-color: #9bbce6; }
|
||||
.fc .fc-button-primary:not(:disabled).fc-button-active { background: var(--scad-heading); border-color: var(--scad-heading); }
|
||||
.fc .fc-daygrid-day-number { color: var(--scad-heading); 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.9); }
|
||||
.fc .fc-list-event:hover td { background: #f0f4ff; }
|
||||
|
||||
/* Legend */
|
||||
.legend { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 1rem; }
|
||||
.legend-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.82rem; color: #6c757d; }
|
||||
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.fc .fc-toolbar { flex-direction: column; gap: 0.5rem; }
|
||||
.fc .fc-toolbar-title { font-size: 1rem; }
|
||||
/* Stack list events vertically on mobile */
|
||||
.fc .fc-list-table { display: block; }
|
||||
.fc .fc-list-table tbody { display: block; }
|
||||
.fc .fc-list-day { display: block; }
|
||||
.fc .fc-list-day th { display: block; padding: 8px 12px; }
|
||||
.fc .fc-list-event { display: flex; flex-direction: column; padding: 8px 12px; border-bottom: 1px solid #e8eeff; }
|
||||
.fc .fc-list-event td { display: block; border: none; padding: 0; }
|
||||
.fc .fc-list-event-time { font-size: 0.75rem; color: #8e99b0; order: 2; }
|
||||
.fc .fc-list-event-graphic { display: none; }
|
||||
.fc .fc-list-event-title { font-size: 0.9rem; word-break: break-word; white-space: normal; order: 1; margin-bottom: 2px; }
|
||||
.fc .fc-list-event-dot { display: inline-block; margin-right: 6px; }
|
||||
}
|
||||
</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">
|
||||
|
||||
<?php include(__DIR__ . '/include/my_deadlines_widget.php'); ?>
|
||||
|
||||
<div class="d-flex gap-2 mb-3 flex-wrap align-items-center">
|
||||
<button type="button" class="btn btn-scad-outline d-inline-flex align-items-center gap-2" data-bs-toggle="modal" data-bs-target="#filtersModal">
|
||||
<i class="fa-solid fa-filter"></i>
|
||||
<span>Filtri</span>
|
||||
<span id="filterCountBadge" class="badge bg-primary rounded-pill d-none" style="font-size:0.7rem">0</span>
|
||||
</button>
|
||||
<button id="btnResetFilters" type="button" class="btn btn-light border d-inline-flex align-items-center justify-content-center gap-1" title="Reset filtri" style="min-width:38px;height:38px">
|
||||
<i class="fa-solid fa-rotate-left"></i>
|
||||
<span class="d-none d-sm-inline">Reset</span>
|
||||
</button>
|
||||
<span id="activeFiltersSummary" class="text-muted small text-truncate d-none d-md-inline"></span>
|
||||
</div>
|
||||
<div id="activeFiltersSummaryMobile" class="text-muted small d-md-none mb-2" style="padding-left:0.25rem"></div>
|
||||
|
||||
<!-- Filters Modal -->
|
||||
<div class="modal fade" id="filtersModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fa-solid fa-filter me-2"></i>Filtri calendario</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Stato</label>
|
||||
<select id="filterStatus" class="form-select">
|
||||
<option value="non-completata" selected>Non completate</option>
|
||||
<option value="">Tutti</option>
|
||||
<option value="attiva">Attive</option>
|
||||
<option value="in-scadenza">In scadenza</option>
|
||||
<option value="scaduta">Scadute</option>
|
||||
<option value="completata">Completate</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Reparto</label>
|
||||
<select id="filterDepartment" class="form-select">
|
||||
<option value="">Tutti</option>
|
||||
<?php foreach ($departments as $dept): ?>
|
||||
<option value="<?= htmlspecialchars($dept, ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($dept, ENT_QUOTES, 'UTF-8') ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label class="form-label fw-semibold">Responsabile</label>
|
||||
<select id="filterEmployee" class="form-select">
|
||||
<option value="">Tutti</option>
|
||||
<?php foreach ($employees as $emp): ?>
|
||||
<option value="<?= htmlspecialchars(trim($emp['first_name'] . ' ' . $emp['last_name']), ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars(trim($emp['first_name'] . ' ' . $emp['last_name']), ENT_QUOTES, 'UTF-8') ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light border" id="btnResetFiltersModal">
|
||||
<i class="fa-solid fa-rotate-left me-1"></i> Reset
|
||||
</button>
|
||||
<button type="button" class="btn btn-scad-primary" data-bs-dismiss="modal">
|
||||
<i class="fa-solid fa-check me-1"></i> Applica
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card scad-card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<h5 class="d-none d-md-flex align-items-center mb-0"><i class="fa-solid fa-calendar-days me-2"></i>Calendario Scadenze</h5>
|
||||
<div class="header-actions d-flex gap-2 flex-wrap ms-auto">
|
||||
<a href="scadenzario/index.php" class="btn btn-scad-outline d-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-list"></i><span>Lista Scadenze</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="legend">
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#5a8fd8"></span> Attiva</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#e8930c"></span> In scadenza</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#dc3545"></span> Scaduta</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#198754"></span> Completata</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 ? 'listWeek' : 'dayGridMonth',
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: isMobile ? 'listWeek,dayGridMonth' : 'dayGridMonth,listWeek'
|
||||
},
|
||||
height: 'auto',
|
||||
navLinks: true,
|
||||
eventSources: [{
|
||||
url: 'scadenzario/ajax/get_calendar_events.php',
|
||||
extraParams: function() {
|
||||
return {
|
||||
status: document.getElementById('filterStatus').value,
|
||||
department: document.getElementById('filterDepartment').value,
|
||||
employee: document.getElementById('filterEmployee').value
|
||||
};
|
||||
},
|
||||
failure: function() {
|
||||
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(view) {
|
||||
if (window.innerWidth < 768) {
|
||||
calendar.changeView('listWeek');
|
||||
} else {
|
||||
calendar.changeView('dayGridMonth');
|
||||
}
|
||||
}
|
||||
});
|
||||
calendar.render();
|
||||
|
||||
// Filters
|
||||
function updateFilterBadge() {
|
||||
var active = 0;
|
||||
var summary = [];
|
||||
var st = document.getElementById('filterStatus');
|
||||
var stv = st.value;
|
||||
if (stv && stv !== 'non-completata') { active++; summary.push(st.options[st.selectedIndex].text); }
|
||||
var dept = document.getElementById('filterDepartment').value;
|
||||
if (dept) { active++; summary.push('Reparto: ' + dept); }
|
||||
var emp = document.getElementById('filterEmployee').value;
|
||||
if (emp) { active++; summary.push('Responsabile: ' + emp); }
|
||||
|
||||
var badge = document.getElementById('filterCountBadge');
|
||||
if (active > 0) { badge.textContent = active; badge.classList.remove('d-none'); }
|
||||
else { badge.classList.add('d-none'); }
|
||||
|
||||
var summaryText = summary.length ? summary.slice(0, 2).join(' • ') + (summary.length > 2 ? ' +' + (summary.length - 2) : '') : '';
|
||||
document.getElementById('activeFiltersSummary').textContent = summaryText;
|
||||
document.getElementById('activeFiltersSummaryMobile').textContent = summaryText;
|
||||
}
|
||||
|
||||
document.querySelectorAll('#filterStatus, #filterDepartment, #filterEmployee').forEach(function(el) {
|
||||
el.addEventListener('change', function() { calendar.refetchEvents(); updateFilterBadge(); });
|
||||
});
|
||||
|
||||
function resetFilters() {
|
||||
document.getElementById('filterStatus').value = 'non-completata';
|
||||
document.getElementById('filterDepartment').value = '';
|
||||
document.getElementById('filterEmployee').value = '';
|
||||
calendar.refetchEvents();
|
||||
updateFilterBadge();
|
||||
}
|
||||
document.getElementById('btnResetFilters').addEventListener('click', resetFilters);
|
||||
document.getElementById('btnResetFiltersModal').addEventListener('click', resetFilters);
|
||||
|
||||
updateFilterBadge();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
/**
|
||||
* Scadenzario — Email notification cron script
|
||||
* Run daily: 0 7 * * * php /var/www/html/public/userarea/scadenzario/cron/send_notifications.php
|
||||
*
|
||||
* Sends "approaching" emails N days before due_date (per-deadline notification_days).
|
||||
* Sends "overdue" emails when due_date has passed.
|
||||
* Skips completed deadlines and already-sent notifications (same deadline+employee+type+date).
|
||||
* Email is taken from employees.auth_user_id → auth_users.email.
|
||||
*/
|
||||
|
||||
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', '/');
|
||||
|
||||
// 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;
|
||||
$skipped = 0;
|
||||
$errors = 0;
|
||||
|
||||
// Get active deadlines that are approaching or overdue
|
||||
$stmt = $pdo->query("
|
||||
SELECT d.id, d.topic, s.name AS subject_name, d.due_date, d.notification_days
|
||||
FROM scad_deadlines d
|
||||
LEFT JOIN scad_subjects s ON s.id = d.subject_id
|
||||
WHERE d.status = 'active'
|
||||
AND d.due_date <= DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)
|
||||
");
|
||||
$deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($deadlines)) {
|
||||
echo date('Y-m-d H:i:s') . " — Nessuna scadenza da notificare.\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Prepare statements
|
||||
$getRecipients = $pdo->prepare("
|
||||
SELECT DISTINCT e.id as employee_id, au.email, e.first_name, e.last_name
|
||||
FROM scad_deadline_employee de
|
||||
JOIN employees e ON e.id = de.employee_id
|
||||
JOIN auth_users au ON au.id = e.auth_user_id
|
||||
WHERE de.deadline_id = ?
|
||||
AND e.auth_user_id IS NOT NULL
|
||||
AND au.email IS NOT NULL
|
||||
AND au.email != ''
|
||||
");
|
||||
|
||||
// Also get employees from assigned departments
|
||||
$getDeptRecipients = $pdo->prepare("
|
||||
SELECT DISTINCT e.id as employee_id, au.email, e.first_name, e.last_name
|
||||
FROM employees e
|
||||
JOIN auth_users au ON au.id = e.auth_user_id
|
||||
WHERE e.department IN (SELECT TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(d.departments, ',', n.n), ',', -1))
|
||||
FROM scad_deadlines d
|
||||
CROSS JOIN (SELECT 1 n UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) n
|
||||
WHERE d.id = ?
|
||||
AND d.departments IS NOT NULL
|
||||
AND n.n <= 1 + LENGTH(d.departments) - LENGTH(REPLACE(d.departments, ',', '')))
|
||||
AND e.auth_user_id IS NOT NULL
|
||||
AND au.email IS NOT NULL
|
||||
AND au.email != ''
|
||||
");
|
||||
|
||||
$checkSent = $pdo->prepare("
|
||||
SELECT COUNT(*) FROM scad_deadline_notifications
|
||||
WHERE deadline_id = ? AND employee_id = ? AND type = ? AND DATE(sent_at) = CURDATE()
|
||||
");
|
||||
|
||||
$insertNotif = $pdo->prepare("
|
||||
INSERT INTO scad_deadline_notifications (deadline_id, employee_id, type) VALUES (?, ?, ?)
|
||||
");
|
||||
|
||||
$insertHistory = $pdo->prepare("
|
||||
INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, NULL, 'notification_sent', ?)
|
||||
");
|
||||
|
||||
foreach ($deadlines as $dl) {
|
||||
$isOverdue = $dl['due_date'] < $today;
|
||||
$type = $isOverdue ? 'overdue' : 'approaching';
|
||||
$daysLeft = (int)((strtotime($dl['due_date']) - strtotime($today)) / 86400);
|
||||
|
||||
// Collect all recipients (direct + department)
|
||||
$recipients = [];
|
||||
|
||||
$getRecipients->execute([$dl['id']]);
|
||||
foreach ($getRecipients->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||
$recipients[$r['employee_id']] = $r;
|
||||
}
|
||||
|
||||
$getDeptRecipients->execute([$dl['id']]);
|
||||
foreach ($getDeptRecipients->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||
$recipients[$r['employee_id']] = $r;
|
||||
}
|
||||
|
||||
if (empty($recipients)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($recipients as $emp) {
|
||||
// Check if already sent today
|
||||
$checkSent->execute([$dl['id'], $emp['employee_id'], $type]);
|
||||
if ($checkSent->fetchColumn() > 0) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Send email
|
||||
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'] ?? 'Scadenzario ZIBOGOMMA'
|
||||
);
|
||||
$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'];
|
||||
$topicText = (!empty($dl['subject_name']) ? $dl['subject_name'] . ' — ' : '') . $dl['topic'];
|
||||
|
||||
if ($isOverdue) {
|
||||
$mail->Subject = '⚠️ Scadenza superata: ' . $dl['topic'];
|
||||
$mail->Body = buildHtml(
|
||||
'Scadenza superata',
|
||||
$topicText,
|
||||
'La scadenza era prevista per il <strong>' . date('d/m/Y', strtotime($dl['due_date'])) . '</strong> ed è stata superata da <strong>' . abs($daysLeft) . ' giorni</strong>.',
|
||||
'#dc3545',
|
||||
$detailUrl
|
||||
);
|
||||
} else {
|
||||
$mail->Subject = '📅 Scadenza in arrivo: ' . $dl['topic'];
|
||||
$daysText = $daysLeft === 0 ? 'oggi' : 'tra <strong>' . $daysLeft . ' giorni</strong>';
|
||||
$mail->Body = buildHtml(
|
||||
'Scadenza in arrivo',
|
||||
$topicText,
|
||||
'La scadenza è prevista per il <strong>' . date('d/m/Y', strtotime($dl['due_date'])) . '</strong> (' . $daysText . ').',
|
||||
'#e8930c',
|
||||
$detailUrl
|
||||
);
|
||||
}
|
||||
|
||||
$mail->isHTML(true);
|
||||
$mail->AltBody = strip_tags(str_replace('<br>', "\n", $mail->Body));
|
||||
|
||||
$mail->send();
|
||||
|
||||
// Record notification
|
||||
$insertNotif->execute([$dl['id'], $emp['employee_id'], $type]);
|
||||
$sent++;
|
||||
|
||||
echo date('H:i:s') . " ✓ {$type} → {$emp['email']} — {$dl['topic']}\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
$errors++;
|
||||
echo date('H:i:s') . " ✗ Errore {$emp['email']}: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
|
||||
// History (one per deadline, not per recipient)
|
||||
$recipientNames = implode(', ', array_map(fn($r) => trim($r['first_name'] . ' ' . $r['last_name']), $recipients));
|
||||
$insertHistory->execute([$dl['id'], "Notifica {$type} inviata a: {$recipientNames}"]);
|
||||
}
|
||||
|
||||
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): string
|
||||
{
|
||||
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">
|
||||
<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">Vai alla scadenza</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 — Scadenzario</p>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>';
|
||||
}
|
||||
@@ -0,0 +1,877 @@
|
||||
<?php include('../include/headscript.php'); ?>
|
||||
<?php
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
$error = null;
|
||||
$deadline = null;
|
||||
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
$error = 'ID non valido.';
|
||||
} else {
|
||||
$id = (int)$_GET['id'];
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT d.*, s.name AS subject_name, s.color AS subject_color
|
||||
FROM scad_deadlines d
|
||||
LEFT JOIN scad_subjects s ON s.id = d.subject_id
|
||||
WHERE d.id = ?
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$deadline = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$deadline) {
|
||||
$error = 'Scadenza non trovata.';
|
||||
} else {
|
||||
$empStmt = $pdo->prepare("
|
||||
SELECT e.first_name, e.last_name, e.department
|
||||
FROM scad_deadline_employee de
|
||||
JOIN employees e ON e.id = de.employee_id
|
||||
WHERE de.deadline_id = ?
|
||||
ORDER BY e.first_name
|
||||
");
|
||||
$empStmt->execute([$id]);
|
||||
$employees = $empStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$attStmt = $pdo->prepare("SELECT * FROM scad_deadline_attachments WHERE deadline_id = ? ORDER BY created_at DESC");
|
||||
$attStmt->execute([$id]);
|
||||
$attachments = $attStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$histStmt = $pdo->prepare("
|
||||
SELECT h.*, au.first_name as user_fname, au.last_name as user_lname
|
||||
FROM scad_deadline_histories h
|
||||
LEFT JOIN auth_users au ON au.id = h.user_id
|
||||
WHERE h.deadline_id = ?
|
||||
ORDER BY h.created_at DESC
|
||||
");
|
||||
$histStmt->execute([$id]);
|
||||
$history = $histStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$isCompleted = $deadline['status'] === 'completed';
|
||||
$isOverdue = !$isCompleted && $deadline['due_date'] < $today;
|
||||
$approachDate = date('Y-m-d', strtotime($today . ' + ' . (int)$deadline['notification_days'] . ' days'));
|
||||
$isApproaching = !$isCompleted && !$isOverdue && $deadline['due_date'] <= $approachDate;
|
||||
|
||||
if ($isCompleted) {
|
||||
$statusLabel = 'Completata';
|
||||
$statusClass = 'badge-completata';
|
||||
} elseif ($isOverdue) {
|
||||
$statusLabel = 'Scaduta';
|
||||
$statusClass = 'badge-scaduta';
|
||||
} elseif ($isApproaching) {
|
||||
$statusLabel = 'In scadenza';
|
||||
$statusClass = 'badge-in-scadenza';
|
||||
} else {
|
||||
$statusLabel = 'Attiva';
|
||||
$statusClass = 'badge-attiva';
|
||||
}
|
||||
|
||||
$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', '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', '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', 'attachment_linked' => 'fa-link', 'attachment_unlinked' => 'fa-link-slash', 'notification_sent' => 'fa-bell'];
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!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($scriptDir) . '/';
|
||||
?>
|
||||
<base href="<?= $baseHref ?>">
|
||||
<?php include('../cssinclude.php'); ?>
|
||||
<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>
|
||||
<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-red: #dc3545;
|
||||
--scad-orange: #e8930c;
|
||||
--scad-green: #198754;
|
||||
}
|
||||
|
||||
.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.05rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-scad-primary:hover {
|
||||
background: var(--scad-primary-hover);
|
||||
color: #fff;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(90, 143, 216, 0.35);
|
||||
}
|
||||
|
||||
.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;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-scad-outline:hover {
|
||||
background: var(--scad-primary);
|
||||
color: #fff;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-scad-green {
|
||||
background: var(--scad-green);
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-scad-green:hover {
|
||||
background: #157347;
|
||||
color: #fff;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(25, 135, 84, 0.35);
|
||||
}
|
||||
|
||||
.badge-status {
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
padding: 0.4em 0.75em;
|
||||
border-radius: 2rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.badge-attiva {
|
||||
background: #e8eeff;
|
||||
color: #3a6bb5;
|
||||
}
|
||||
|
||||
.badge-scaduta {
|
||||
background: #fde8e8;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.badge-in-scadenza {
|
||||
background: #fef3cd;
|
||||
color: #92600a;
|
||||
}
|
||||
|
||||
.badge-completata {
|
||||
background: #d1f2e0;
|
||||
color: #0f5132;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: #8e99b0;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 0.95rem;
|
||||
color: var(--scad-heading);
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.detail-value.text-danger-date {
|
||||
color: var(--scad-red);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-value.text-warning-date {
|
||||
color: var(--scad-orange);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.person-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
background: #f0f4ff;
|
||||
border: 1px solid #dde4f0;
|
||||
border-radius: 2rem;
|
||||
padding: 0.3rem 0.75rem 0.3rem 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
margin: 0.2rem 0.15rem;
|
||||
color: var(--scad-heading);
|
||||
}
|
||||
|
||||
.person-chip i {
|
||||
color: var(--scad-primary);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.person-chip .chip-dept {
|
||||
color: #8e99b0;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.dept-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
background: #eef6ee;
|
||||
border: 1px solid #c8e6c9;
|
||||
border-radius: 2rem;
|
||||
padding: 0.3rem 0.75rem 0.3rem 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
margin: 0.2rem 0.15rem;
|
||||
color: #2e5e2e;
|
||||
}
|
||||
|
||||
.dept-chip i {
|
||||
color: #4caf50;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Attachments */
|
||||
.att-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.65rem 0;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
}
|
||||
|
||||
.att-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.att-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 0.4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.att-icon-pdf {
|
||||
background: #fde8e8;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.att-icon-img {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.att-icon-file {
|
||||
background: #e8eeff;
|
||||
color: #3a6bb5;
|
||||
}
|
||||
|
||||
.att-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.att-name {
|
||||
font-weight: 600;
|
||||
color: var(--scad-heading);
|
||||
font-size: 0.9rem;
|
||||
text-decoration: none;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.att-name:hover {
|
||||
color: var(--scad-primary);
|
||||
}
|
||||
|
||||
.att-meta {
|
||||
font-size: 0.78rem;
|
||||
color: #8e99b0;
|
||||
}
|
||||
|
||||
/* Timeline */
|
||||
.timeline {
|
||||
position: relative;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.timeline::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0.55rem;
|
||||
top: 0.5rem;
|
||||
bottom: 0.5rem;
|
||||
width: 2px;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
padding-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.timeline-item:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
position: absolute;
|
||||
left: -1.7rem;
|
||||
top: 0.15rem;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.6rem;
|
||||
color: #fff;
|
||||
z-index: 1;
|
||||
box-shadow: 0 0 0 3px #fff;
|
||||
}
|
||||
|
||||
.timeline-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
|
||||
.timeline-action {
|
||||
font-weight: 700;
|
||||
font-size: 0.88rem;
|
||||
color: var(--scad-heading);
|
||||
}
|
||||
|
||||
.timeline-user {
|
||||
font-size: 0.82rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.timeline-date {
|
||||
font-size: 0.78rem;
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.timeline-notes {
|
||||
font-size: 0.83rem;
|
||||
color: #6c757d;
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
.timeline-changes {
|
||||
font-size: 0.82rem;
|
||||
margin-top: 0.3rem;
|
||||
background: #f8f9fb;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.timeline-changes .change-field {
|
||||
font-weight: 600;
|
||||
color: var(--scad-heading);
|
||||
}
|
||||
|
||||
.timeline-changes .change-old {
|
||||
color: var(--scad-red);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.timeline-changes .change-new {
|
||||
color: var(--scad-green);
|
||||
}
|
||||
|
||||
.scad-breadcrumb {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.scad-breadcrumb .breadcrumb-item a {
|
||||
color: var(--scad-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.scad-breadcrumb .breadcrumb-item a:hover {
|
||||
color: var(--scad-primary-hover);
|
||||
}
|
||||
|
||||
.scad-breadcrumb .breadcrumb-item.active {
|
||||
color: #6c757d;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.action-bar {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-bar .btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
|
||||
.sidebar-wrapper,
|
||||
.topbar,
|
||||
.page-footer,
|
||||
.action-bar,
|
||||
.scad-breadcrumb {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.scad-card {
|
||||
box-shadow: none;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
@page {
|
||||
size: portrait;
|
||||
margin: 1cm;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-law-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
margin-left: 0.65rem;
|
||||
padding: 0.42rem 0.85rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(90, 143, 216, 0.45);
|
||||
background: linear-gradient(135deg, #5a8fd8 0%, #6f42c1 100%);
|
||||
color: #ffffff !important;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
box-shadow: 0 4px 14px rgba(90, 143, 216, 0.35);
|
||||
letter-spacing: 0.02em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ai-law-btn i {
|
||||
font-size: 0.82rem;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.ai-law-btn:hover {
|
||||
color: #ffffff !important;
|
||||
background: linear-gradient(135deg, #4578c0 0%, #5b35a5 100%);
|
||||
box-shadow: 0 6px 18px rgba(90, 143, 216, 0.45);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
</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">
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger">
|
||||
<i class="fa-solid fa-triangle-exclamation me-2"></i><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?>
|
||||
<a href="scadenzario/index.php" class="alert-link ms-2">Torna alla lista</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="scad-breadcrumb" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><a href="scadenzario/index.php">Scadenzario</a></li>
|
||||
<li class="breadcrumb-item"><a href="scadenzario/index.php">Lista Scadenze</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page"><?= htmlspecialchars($deadline['topic'], ENT_QUOTES, 'UTF-8') ?></li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Action Bar -->
|
||||
<div class="action-bar d-flex gap-2 mb-3 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>Torna alla lista</span>
|
||||
</a>
|
||||
<?php if (!$isCompleted): ?>
|
||||
<button class="btn btn-scad-primary d-inline-flex align-items-center gap-2" id="btnModifica">
|
||||
<i class="fa-solid fa-pen"></i><span>Modifica</span>
|
||||
</button>
|
||||
<button class="btn btn-scad-green d-inline-flex align-items-center gap-2" id="btnCompleta">
|
||||
<i class="fa-solid fa-check"></i><span>Completa</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<button class="btn btn-scad-outline d-inline-flex align-items-center gap-2" onclick="window.print()">
|
||||
<i class="fa-solid fa-print"></i><span>Stampa</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Main Detail Card -->
|
||||
<div class="card scad-card mb-3">
|
||||
<div class="card-header">
|
||||
<h5><i class="fa-solid fa-file-lines me-2"></i>Dettagli Scadenza</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<!-- Left Column -->
|
||||
<div class="col-12 col-md-6">
|
||||
<?php if (!empty($deadline['subject_name'])): ?>
|
||||
<div class="detail-label">Argomento</div>
|
||||
<div class="detail-value">
|
||||
<span style="display:inline-block;padding:0.25rem 0.7rem;border-radius:1rem;color:#fff;font-weight:600;font-size:0.85rem;background: <?= htmlspecialchars($deadline['subject_color'] ?: '#6c757d', ENT_QUOTES, 'UTF-8') ?>">
|
||||
<?= htmlspecialchars($deadline['subject_name'], ENT_QUOTES, 'UTF-8') ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="detail-label">Dettaglio</div>
|
||||
<div class="detail-value" style="font-size:1.15rem; font-weight:700;">
|
||||
<?= htmlspecialchars($deadline['topic'], ENT_QUOTES, 'UTF-8') ?>
|
||||
</div>
|
||||
|
||||
<?php if ($deadline['law_regulation']): ?>
|
||||
<div class="detail-label">Legge / Articolo</div>
|
||||
<div class="detail-value">
|
||||
<?= htmlspecialchars($deadline['law_regulation'], ENT_QUOTES, 'UTF-8') ?>
|
||||
|
||||
<span class="ai-law-btn" title="Funzione AI disponibile prossimamente">
|
||||
<i class="fa-solid fa-wand-magic-sparkles"></i>
|
||||
AI
|
||||
</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="detail-label">Periodicità</div>
|
||||
<div class="detail-value"><?= htmlspecialchars($recurrenceLabels[$deadline['recurrence_type']] ?? $deadline['recurrence_type'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="detail-label">Stato</div>
|
||||
<div class="detail-value">
|
||||
<span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span>
|
||||
<?php if ($isCompleted && $deadline['completed_at']): ?>
|
||||
<span class="text-muted ms-2" style="font-size:0.82rem"><?= date('d/m/Y H:i', strtotime($deadline['completed_at'])) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="detail-label">Data scadenza</div>
|
||||
<div class="detail-value <?= $isOverdue ? 'text-danger-date' : ($isApproaching ? 'text-warning-date' : '') ?>">
|
||||
<i class="fa-regular fa-calendar me-1"></i><?= date('d/m/Y', strtotime($deadline['due_date'])) ?>
|
||||
<?php if ($isOverdue): ?><span class="ms-1" style="font-size:0.8rem">(scaduta)</span><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($deadline['document_date']): ?>
|
||||
<div class="detail-label">Data documento</div>
|
||||
<div class="detail-value"><?= date('d/m/Y', strtotime($deadline['document_date'])) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="detail-label">Data ultimo controllo</div>
|
||||
<div class="detail-value"><?= $deadline['check_date'] ? date('d/m/Y', strtotime($deadline['check_date'])) : '—' ?></div>
|
||||
|
||||
<div class="detail-label">Giorni preavviso notifica</div>
|
||||
<div class="detail-value"><?= (int)$deadline['notification_days'] ?> giorni</div>
|
||||
|
||||
<?php if ($deadline['storage_location']): ?>
|
||||
<div class="detail-label">Luogo archiviazione</div>
|
||||
<div class="detail-value"><i class="fa-regular fa-folder-open me-1"></i><?= htmlspecialchars($deadline['storage_location'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($deadline['notes']): ?>
|
||||
<div class="detail-label">Note</div>
|
||||
<div class="detail-value"><?= nl2br(htmlspecialchars($deadline['notes'], ENT_QUOTES, 'UTF-8')) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Responsabili -->
|
||||
<?php if ($deadline['departments'] || !empty($employees)): ?>
|
||||
<hr class="my-3" style="border-color:#e8eeff">
|
||||
<?php if ($deadline['departments']): ?>
|
||||
<div class="detail-label">Reparti responsabili</div>
|
||||
<div class="detail-value">
|
||||
<?php foreach (array_map('trim', explode(',', $deadline['departments'])) as $dept): ?>
|
||||
<span class="dept-chip"><i class="fa-solid fa-building"></i><?= htmlspecialchars($dept, ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($employees)): ?>
|
||||
<div class="detail-label">Singoli responsabili</div>
|
||||
<div class="detail-value">
|
||||
<?php foreach ($employees as $emp): ?>
|
||||
<span class="person-chip">
|
||||
<i class="fa-solid fa-user"></i>
|
||||
<?= htmlspecialchars(trim($emp['first_name'] . ' ' . $emp['last_name']), ENT_QUOTES, 'UTF-8') ?>
|
||||
<?php if ($emp['department']): ?>
|
||||
<span class="chip-dept">(<?= htmlspecialchars($emp['department'], ENT_QUOTES, 'UTF-8') ?>)</span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Attachments Card -->
|
||||
<?php if (!empty($attachments)): ?>
|
||||
<div class="card scad-card mb-3">
|
||||
<div class="card-header">
|
||||
<h5><i class="fa-solid fa-paperclip me-2"></i>Allegati (<?= count($attachments) ?>)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php foreach ($attachments as $att):
|
||||
$mime = $att['mime_type'] ?? '';
|
||||
if (strpos($mime, 'pdf') !== false) {
|
||||
$iconClass = 'att-icon-pdf';
|
||||
$icon = 'fa-file-pdf';
|
||||
} elseif (strpos($mime, 'image') !== false) {
|
||||
$iconClass = 'att-icon-img';
|
||||
$icon = 'fa-file-image';
|
||||
} else {
|
||||
$iconClass = 'att-icon-file';
|
||||
$icon = 'fa-file';
|
||||
}
|
||||
$sizeKB = round(($att['size'] ?? 0) / 1024, 1);
|
||||
$sizeStr = $sizeKB >= 1024 ? round($sizeKB / 1024, 1) . ' MB' : $sizeKB . ' KB';
|
||||
?>
|
||||
<div class="att-row">
|
||||
<div class="att-icon <?= $iconClass ?>"><i class="fa-solid <?= $icon ?>"></i></div>
|
||||
<div class="att-info">
|
||||
<a href="scadenzario/ajax/download_attachment.php?id=<?= (int)$att['id'] ?>" class="att-name"><?= htmlspecialchars($att['original_name'], ENT_QUOTES, 'UTF-8') ?></a>
|
||||
<div class="att-meta"><?= $sizeStr ?> · <?= date('d/m/Y', strtotime($att['created_at'])) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- History Card -->
|
||||
<?php if (!empty($history)): ?>
|
||||
<div class="card scad-card mb-3">
|
||||
<div class="card-header">
|
||||
<h5><i class="fa-solid fa-clock-rotate-left me-2"></i>Cronologia</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="timeline">
|
||||
<?php foreach ($history as $h):
|
||||
$color = $actionColors[$h['action']] ?? '#adb5bd';
|
||||
$iconCls = $actionIcons[$h['action']] ?? 'fa-circle';
|
||||
$label = $actionLabels[$h['action']] ?? $h['action'];
|
||||
$userName = trim(($h['user_fname'] ?? '') . ' ' . ($h['user_lname'] ?? '')) ?: 'Sistema';
|
||||
$changes = $h['changes'] ? json_decode($h['changes'], true) : null;
|
||||
?>
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-dot" style="background:<?= $color ?>"><i class="fa-solid <?= $iconCls ?>"></i></div>
|
||||
<div class="timeline-header">
|
||||
<span class="timeline-action"><?= htmlspecialchars($label, ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<span class="timeline-user">da <?= htmlspecialchars($userName, ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<span class="timeline-date"><?= date('d/m/Y H:i', strtotime($h['created_at'])) ?></span>
|
||||
</div>
|
||||
<?php if ($h['notes']): ?>
|
||||
<div class="timeline-notes"><i class="fa-regular fa-comment me-1"></i><?= htmlspecialchars($h['notes'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($changes && is_array($changes)): ?>
|
||||
<div class="timeline-changes">
|
||||
<?php foreach ($changes as $field => $vals): ?>
|
||||
<div>
|
||||
<span class="change-field"><?= htmlspecialchars($field, ENT_QUOTES, 'UTF-8') ?>:</span>
|
||||
<span class="change-old"><?= htmlspecialchars($vals['old'] ?? '—', ENT_QUOTES, 'UTF-8') ?></span>
|
||||
→
|
||||
<span class="change-new"><?= htmlspecialchars($vals['new'] ?? '—', ENT_QUOTES, 'UTF-8') ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php include('../include/footer.php'); ?>
|
||||
</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 if ($deadline && !$isCompleted): ?>
|
||||
<script>
|
||||
$(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() {
|
||||
window.openDeadlineEdit(<?= (int)$deadline['id'] ?>);
|
||||
});
|
||||
|
||||
function detailSubmitComplete(createNext, copyAttachments) {
|
||||
var fd = new FormData();
|
||||
fd.append('id', '<?= (int)$deadline['id'] ?>');
|
||||
fd.append('create_next', createNext ? '1' : '0');
|
||||
fd.append('copy_attachments', copyAttachments ? '1' : '0');
|
||||
|
||||
fetch('scadenzario/ajax/complete_deadline.php', {
|
||||
method: 'POST',
|
||||
body: fd
|
||||
})
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Completata',
|
||||
text: data.message,
|
||||
timer: 1800,
|
||||
showConfirmButton: false
|
||||
})
|
||||
.then(function() {
|
||||
if (data.new_id) {
|
||||
window.location.href = 'scadenzario/detail.php?id=' + data.new_id + '#edit';
|
||||
} else {
|
||||
window.location.href = 'scadenzario/index.php';
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Errore', data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
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>
|
||||
<?php include __DIR__ . '/include/deadline_modal_js.php'; ?>
|
||||
<?php endif; ?>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Renders two status banners for the current user:
|
||||
* - red -> overdue deadlines (scaduta)
|
||||
* - orange -> approaching deadlines (in scadenza)
|
||||
* Scope: deadlines assigned directly to the user OR to their department.
|
||||
*/
|
||||
|
||||
$_empStmt = $pdo->prepare("SELECT id, department FROM employees WHERE auth_user_id = ? LIMIT 1");
|
||||
$_empStmt->execute([(int)$iduserlogin]);
|
||||
$_emp = $_empStmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||
|
||||
$_overdue = 0;
|
||||
$_approaching = 0;
|
||||
|
||||
if ($_emp) {
|
||||
$_empId = (int)$_emp['id'];
|
||||
$_deptRaw = (string)($_emp['department'] ?? '');
|
||||
$_dept = trim($_deptRaw);
|
||||
|
||||
$_sql = "
|
||||
SELECT
|
||||
SUM(CASE WHEN d.due_date < CURDATE() THEN 1 ELSE 0 END) AS overdue_cnt,
|
||||
SUM(CASE WHEN d.due_date >= CURDATE()
|
||||
AND d.due_date <= DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)
|
||||
THEN 1 ELSE 0 END) AS approaching_cnt
|
||||
FROM scad_deadlines d
|
||||
WHERE d.status <> 'completed'
|
||||
AND (
|
||||
d.id IN (SELECT deadline_id FROM scad_deadline_employee WHERE employee_id = ?)
|
||||
OR (? <> '' AND FIND_IN_SET(?, REPLACE(d.departments, ', ', ',')) > 0)
|
||||
)
|
||||
";
|
||||
$_st = $pdo->prepare($_sql);
|
||||
$_st->execute([$_empId, $_dept, $_dept]);
|
||||
$_row = $_st->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
$_overdue = (int)($_row['overdue_cnt'] ?? 0);
|
||||
$_approaching = (int)($_row['approaching_cnt'] ?? 0);
|
||||
}
|
||||
|
||||
if (!$_emp || ($_overdue === 0 && $_approaching === 0)) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<style>
|
||||
.my-deadlines-widgets {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.my-deadlines-widgets .mdw {
|
||||
flex: 1 1 260px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.9rem;
|
||||
padding: 0.85rem 1rem;
|
||||
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;
|
||||
}
|
||||
|
||||
.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-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;
|
||||
}
|
||||
</style>
|
||||
<div class="my-deadlines-widgets">
|
||||
<?php if ($_overdue > 0): ?>
|
||||
<a class="mdw mdw-red" href="scadenzario/index.php?filter_my=1&filter_status=scaduta">
|
||||
<span class="mdw-icon"><i class="fa-solid fa-triangle-exclamation"></i></span>
|
||||
<span class="mdw-body">
|
||||
<span class="mdw-count"><?= $_overdue ?></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 class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($_approaching > 0): ?>
|
||||
<a class="mdw mdw-orange" href="scadenzario/index.php?filter_my=1&filter_status=in-scadenza">
|
||||
<span class="mdw-icon"><i class="fa-solid fa-clock"></i></span>
|
||||
<span class="mdw-body">
|
||||
<span class="mdw-count"><?= $_approaching ?></span>
|
||||
<span class="mdw-label d-block">In scadenza a breve — <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span>
|
||||
</span>
|
||||
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,202 @@
|
||||
<?php include('../include/headscript.php'); ?>
|
||||
<?php
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$sql = "
|
||||
SELECT d.*,
|
||||
s.name AS subject_name,
|
||||
s.color AS subject_color,
|
||||
GROUP_CONCAT(DISTINCT CONCAT(e.first_name, ' ', e.last_name) ORDER BY e.first_name SEPARATOR ', ') as responsabili,
|
||||
GROUP_CONCAT(DISTINCT e.department ORDER BY e.department SEPARATOR ', ') as reparti_persone,
|
||||
d.departments as reparti_assegnati
|
||||
FROM scad_deadlines d
|
||||
LEFT JOIN scad_subjects s ON s.id = d.subject_id
|
||||
LEFT JOIN scad_deadline_employee de ON de.deadline_id = d.id
|
||||
LEFT JOIN employees e ON e.id = de.employee_id
|
||||
";
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
$filterStatus = $_GET['status'] ?? '';
|
||||
$filterDept = $_GET['department'] ?? '';
|
||||
$filterEmployee = $_GET['employee'] ?? '';
|
||||
|
||||
if ($filterStatus === 'non-completata') {
|
||||
$where[] = "d.status != 'completed'";
|
||||
} elseif ($filterStatus === 'completata') {
|
||||
$where[] = "d.status = 'completed'";
|
||||
} elseif ($filterStatus === 'scaduta') {
|
||||
$where[] = "d.status = 'active' AND d.due_date < CURDATE()";
|
||||
} elseif ($filterStatus === 'in-scadenza') {
|
||||
$where[] = "d.status = 'active' AND d.due_date >= CURDATE() AND d.due_date <= DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)";
|
||||
} elseif ($filterStatus === 'attiva') {
|
||||
$where[] = "d.status = 'active' AND d.due_date > DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)";
|
||||
}
|
||||
|
||||
if ($filterEmployee) {
|
||||
$where[] = "EXISTS (SELECT 1 FROM scad_deadline_employee de2 JOIN employees e2 ON e2.id = de2.employee_id WHERE de2.deadline_id = d.id AND CONCAT(e2.first_name, ' ', e2.last_name) = ?)";
|
||||
$params[] = $filterEmployee;
|
||||
}
|
||||
|
||||
$dueFrom = $_GET['due_from'] ?? '';
|
||||
$dueTo = $_GET['due_to'] ?? '';
|
||||
$checkFrom = $_GET['check_from'] ?? '';
|
||||
$checkTo = $_GET['check_to'] ?? '';
|
||||
|
||||
if ($dueFrom) { $where[] = "d.due_date >= ?"; $params[] = $dueFrom; }
|
||||
if ($dueTo) { $where[] = "d.due_date <= ?"; $params[] = $dueTo; }
|
||||
if ($checkFrom) { $where[] = "d.check_date >= ?"; $params[] = $checkFrom; }
|
||||
if ($checkTo) { $where[] = "d.check_date <= ?"; $params[] = $checkTo; }
|
||||
|
||||
if (!empty($where)) {
|
||||
$sql .= " WHERE " . implode(' AND ', $where);
|
||||
}
|
||||
|
||||
$sql .= " GROUP BY d.id ORDER BY (d.status = 'completed') ASC, d.due_date ASC";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$recurrenceLabels = ['once'=>'Una tantum','monthly'=>'Mensile','quarterly'=>'Trimestrale','semiannual'=>'Semestrale','annual'=>'Annuale','biennial'=>'Biennale','triennial'=>'Triennale','quadriennial'=>'Quadriennale','quinquennial'=>'Quinquennale','decennial'=>'Decennale','quindecennial'=>'Quindicennale'];
|
||||
|
||||
$filterLabel = '';
|
||||
if ($filterStatus) {
|
||||
$statusLabels = ['non-completata'=>'Non completate','attiva'=>'Attive','in-scadenza'=>'In scadenza','scaduta'=>'Scadute','completata'=>'Completate'];
|
||||
$filterLabel = $statusLabels[$filterStatus] ?? '';
|
||||
}
|
||||
if ($filterDept) {
|
||||
$filterLabel .= ($filterLabel ? ' — ' : '') . 'Reparto: ' . $filterDept;
|
||||
}
|
||||
if ($filterEmployee) {
|
||||
$filterLabel .= ($filterLabel ? ' — ' : '') . 'Responsabile: ' . $filterEmployee;
|
||||
}
|
||||
if ($dueFrom || $dueTo) {
|
||||
$filterLabel .= ($filterLabel ? ' — ' : '') . 'Scadenza: ' . ($dueFrom ? date('d/m/Y', strtotime($dueFrom)) : '...') . ' → ' . ($dueTo ? date('d/m/Y', strtotime($dueTo)) : '...');
|
||||
}
|
||||
if ($checkFrom || $checkTo) {
|
||||
$filterLabel .= ($filterLabel ? ' — ' : '') . 'Controllo: ' . ($checkFrom ? date('d/m/Y', strtotime($checkFrom)) : '...') . ' → ' . ($checkTo ? date('d/m/Y', strtotime($checkTo)) : '...');
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Stampa Scadenzario</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 11px; color: #222; background: #fff; }
|
||||
.print-header { padding: 20px 20px 10px; border-bottom: 2px solid #2c3e6b; margin-bottom: 10px; }
|
||||
.print-header h1 { font-size: 18px; color: #2c3e6b; margin: 0; }
|
||||
.print-header .print-meta { font-size: 10px; color: #666; margin-top: 4px; }
|
||||
.print-actions { padding: 10px 20px; display: flex; gap: 10px; }
|
||||
.print-actions button { padding: 8px 20px; font-size: 13px; font-weight: 600; border-radius: 6px; cursor: pointer; border: none; }
|
||||
.btn-print { background: #5a8fd8; color: #fff; }
|
||||
.btn-back { background: #f0f0f0; color: #333; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 0 20px; }
|
||||
table { width: calc(100% - 40px); }
|
||||
th { background: #2c3e6b; color: #fff; font-weight: 600; font-size: 10px; text-transform: uppercase; letter-spacing: 0.03em; padding: 6px 8px; text-align: left; white-space: nowrap; }
|
||||
td { padding: 5px 8px; border-bottom: 1px solid #e0e0e0; vertical-align: top; font-size: 10.5px; line-height: 1.4; }
|
||||
tr:nth-child(even) { background: #f9fafb; }
|
||||
tr.row-overdue { background: #fff5f5; }
|
||||
tr.row-overdue td { color: #991b1b; }
|
||||
tr.row-approaching { background: #fffbeb; }
|
||||
tr.row-completed { opacity: 0.5; }
|
||||
.status-badge { font-size: 9px; font-weight: 700; padding: 2px 6px; border-radius: 10px; white-space: nowrap; }
|
||||
.st-attiva { background: #e8eeff; color: #3a6bb5; }
|
||||
.st-scaduta { background: #fde8e8; color: #b91c1c; }
|
||||
.st-in-scadenza { background: #fef3cd; color: #92600a; }
|
||||
.st-completata { background: #d1f2e0; color: #0f5132; }
|
||||
.print-footer { padding: 10px 20px; font-size: 9px; color: #999; border-top: 1px solid #e0e0e0; margin-top: 10px; text-align: right; }
|
||||
@media print {
|
||||
.print-actions { display: none; }
|
||||
body { font-size: 9px; }
|
||||
th { font-size: 8.5px; padding: 4px 6px; }
|
||||
td { font-size: 9px; padding: 3px 6px; }
|
||||
@page { size: landscape; margin: 0.8cm; }
|
||||
}
|
||||
@media screen {
|
||||
table { margin: 0 auto; width: calc(100% - 40px); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="print-header">
|
||||
<h1>ELENCO PRESCRIZIONI LEGALI ED ALTRE PRESCRIZIONI APPLICABILI<br>SICUREZZA ED AMBIENTE CON VERIFICA DEL RISPETTO DELLE STESSE</h1>
|
||||
<div class="print-meta">
|
||||
Stampato il <?= date('d/m/Y H:i') ?>
|
||||
<?php if ($filterLabel): ?> — Filtro: <?= htmlspecialchars($filterLabel, ENT_QUOTES, 'UTF-8') ?><?php endif; ?>
|
||||
— Totale: <?= count($deadlines) ?> scadenze
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="print-actions">
|
||||
<button class="btn-print" onclick="window.print()"><i class="fa-solid fa-print"></i> Stampa</button>
|
||||
<button class="btn-back" onclick="window.close()">Chiudi</button>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Argomento</th>
|
||||
<th>Legge / Art.</th>
|
||||
<th>Dettaglio</th>
|
||||
<th>Periodicità Scadenza</th>
|
||||
<th>Data Documento</th>
|
||||
<th>Data Scadenza</th>
|
||||
<th>Data Ultimo Controllo</th>
|
||||
<th>Responsabilità</th>
|
||||
<th>Luogo di Archiviazione</th>
|
||||
<th>Note</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($deadlines as $row):
|
||||
$dueDate = $row['due_date'];
|
||||
$nDays = (int)$row['notification_days'];
|
||||
$approachDate = date('Y-m-d', strtotime($today . ' + ' . $nDays . ' days'));
|
||||
$isCompleted = $row['status'] === 'completed';
|
||||
$isOverdue = !$isCompleted && $dueDate < $today;
|
||||
$isApproaching = !$isCompleted && !$isOverdue && $dueDate <= $approachDate;
|
||||
|
||||
if ($isCompleted) { $statusLabel = 'Completata'; $stClass = 'st-completata'; $rowClass = 'row-completed'; }
|
||||
elseif ($isOverdue) { $statusLabel = 'Scaduta'; $stClass = 'st-scaduta'; $rowClass = 'row-overdue'; }
|
||||
elseif ($isApproaching) { $statusLabel = 'In scadenza'; $stClass = 'st-in-scadenza'; $rowClass = 'row-approaching'; }
|
||||
else { $statusLabel = 'Attiva'; $stClass = 'st-attiva'; $rowClass = ''; }
|
||||
|
||||
// Merge departments
|
||||
$allDepts = [];
|
||||
if (!empty($row['reparti_assegnati'])) $allDepts = array_map('trim', explode(',', $row['reparti_assegnati']));
|
||||
if (!empty($row['reparti_persone'])) $allDepts = array_merge($allDepts, array_map('trim', explode(',', $row['reparti_persone'])));
|
||||
$reparti = implode(', ', array_unique(array_filter($allDepts)));
|
||||
?>
|
||||
<tr class="<?= $rowClass ?>"<?= !empty($row['subject_color']) ? ' style="border-left: 4px solid ' . htmlspecialchars($row['subject_color'], ENT_QUOTES, 'UTF-8') . '"' : '' ?>>
|
||||
<td><strong><?= htmlspecialchars($row['subject_name'] ?? '', ENT_QUOTES, 'UTF-8') ?></strong></td>
|
||||
<td><?= htmlspecialchars($row['law_regulation'] ?? '', ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td><?= htmlspecialchars($row['topic'], ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td><?= htmlspecialchars($recurrenceLabels[$row['recurrence_type']] ?? $row['recurrence_type'], ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td style="white-space:nowrap"><?= $row['document_date'] ? date('d/m/Y', strtotime($row['document_date'])) : '' ?></td>
|
||||
<td style="white-space:nowrap"><?= date('d/m/Y', strtotime($dueDate)) ?></td>
|
||||
<td style="white-space:nowrap"><?= $row['check_date'] ? date('d/m/Y', strtotime($row['check_date'])) : '' ?></td>
|
||||
<td><?php
|
||||
$resp = [];
|
||||
if ($reparti) $resp[] = $reparti;
|
||||
if ($row['responsabili']) $resp[] = $row['responsabili'];
|
||||
echo htmlspecialchars(implode(', ', $resp), ENT_QUOTES, 'UTF-8');
|
||||
?></td>
|
||||
<td><?= htmlspecialchars($row['storage_location'] ?? '', ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td><?= htmlspecialchars($row['notes'] ?? '', ENT_QUOTES, 'UTF-8') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="print-footer">
|
||||
ZIBOGOMMA — Scadenzario — <?= date('d/m/Y') ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,70 @@
|
||||
-- Scadenzario tables
|
||||
-- Responsible persons = employees (existing table)
|
||||
-- Departments = employees.department (varchar field)
|
||||
-- Notification email = employees.auth_user_id -> auth_users.email
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scad_deadlines (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
category VARCHAR(100) NULL,
|
||||
topic VARCHAR(500) NOT NULL,
|
||||
law_regulation VARCHAR(500) NULL,
|
||||
details TEXT NULL,
|
||||
recurrence_type VARCHAR(20) NOT NULL DEFAULT 'once',
|
||||
due_date DATE NOT NULL,
|
||||
check_date DATE NULL,
|
||||
document_date DATE NULL,
|
||||
notification_days SMALLINT UNSIGNED NOT NULL DEFAULT 7,
|
||||
storage_location VARCHAR(500) NULL,
|
||||
notes TEXT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
completed_at TIMESTAMP NULL,
|
||||
completed_by INT UNSIGNED NULL,
|
||||
created_by INT UNSIGNED NOT NULL,
|
||||
departments VARCHAR(500) NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_due_date (due_date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scad_deadline_employee (
|
||||
deadline_id INT UNSIGNED NOT NULL,
|
||||
employee_id INT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (deadline_id, employee_id),
|
||||
CONSTRAINT fk_de_deadline FOREIGN KEY (deadline_id) REFERENCES scad_deadlines(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_de_employee FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scad_deadline_attachments (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
deadline_id INT UNSIGNED NOT NULL,
|
||||
original_name VARCHAR(500) NOT NULL,
|
||||
stored_name VARCHAR(500) NOT NULL,
|
||||
mime_type VARCHAR(100) NULL,
|
||||
size INT UNSIGNED NULL,
|
||||
uploaded_by INT UNSIGNED NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_att_deadline FOREIGN KEY (deadline_id) REFERENCES scad_deadlines(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scad_deadline_histories (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
deadline_id INT UNSIGNED NOT NULL,
|
||||
user_id INT UNSIGNED NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
changes JSON NULL,
|
||||
notes TEXT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_hist_deadline (deadline_id),
|
||||
CONSTRAINT fk_hist_deadline FOREIGN KEY (deadline_id) REFERENCES scad_deadlines(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scad_deadline_notifications (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
deadline_id INT UNSIGNED NOT NULL,
|
||||
employee_id INT UNSIGNED NOT NULL,
|
||||
sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
type VARCHAR(30) NOT NULL,
|
||||
INDEX idx_notif_deadline (deadline_id),
|
||||
CONSTRAINT fk_notif_deadline FOREIGN KEY (deadline_id) REFERENCES scad_deadlines(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,25 @@
|
||||
-- One-time migration: move scad_deadlines.category into dedicated scad_subjects table
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scad_subjects (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
color VARCHAR(7) NOT NULL DEFAULT '#6c757d',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uniq_name (name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT IGNORE INTO scad_subjects (name)
|
||||
SELECT DISTINCT TRIM(category) FROM scad_deadlines
|
||||
WHERE category IS NOT NULL AND TRIM(category) <> '';
|
||||
|
||||
ALTER TABLE scad_deadlines
|
||||
ADD COLUMN subject_id INT UNSIGNED NULL AFTER id,
|
||||
ADD INDEX idx_subject (subject_id),
|
||||
ADD CONSTRAINT fk_deadlines_subject FOREIGN KEY (subject_id) REFERENCES scad_subjects(id) ON DELETE SET NULL;
|
||||
|
||||
UPDATE scad_deadlines d
|
||||
JOIN scad_subjects s ON s.name = TRIM(d.category)
|
||||
SET d.subject_id = s.id;
|
||||
|
||||
ALTER TABLE scad_deadlines DROP COLUMN category;
|
||||
@@ -0,0 +1,531 @@
|
||||
START TRANSACTION;
|
||||
|
||||
DELETE FROM scad_deadline_attachments;
|
||||
DELETE FROM scad_deadlines;
|
||||
DELETE FROM scad_subjects;
|
||||
|
||||
ALTER TABLE scad_deadlines AUTO_INCREMENT = 1;
|
||||
ALTER TABLE scad_subjects AUTO_INCREMENT = 1;
|
||||
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Ambiente', '#198754') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Manutenzioni', '#5a8fd8') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Sicurezza', '#e8930c') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Ambienti - Scarichi idrici', '#0dcaf0') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Prescrizioni generali', '#6f42c1') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Antincendio', '#dc3545') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Ambiente - Manutenzione', '#20c997') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Ambiente - Rifiuti', '#795548') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Medicina del Lavoro', '#d63384') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Prescrizioni generali Medicina del Lavoro', '#343a40') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Adempimenti', '#b88a44') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Attività in appalto', '#8b4513') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Formazione', '#17a2b8') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Appaltatori', '#212529') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Ambienti - Emissioni in atmosfera', '#0d6efd') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Ambiente - Emissioni in atmosfera', '#28a745') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Prevenzione - Sicurezza', '#ffc107') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Pronto Soccorso', '#e83e8c') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
|
||||
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES (NULL, 'Autorizzazione raccolta e trasporto rifiuti speciali non pericolosi TRAMONTO ANTONIO', 'D.M. 10.03.1998, UNI 9994', 'quinquennial', '2025-12-22', '2021-01-02', '2020-12-22', 7, NULL, NULL, 1, 'Amministrazione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente'), 'Autorizzazione raccolta e trasporto rifiuti speciali pericolosi TRAMONTO ANTONIO', 'D.Lgs 81/08 art. 174, 176', 'quinquennial', '2026-01-09', '2021-01-19', '2021-01-09', 7, NULL, NULL, 1, 'Amministrazione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Manutenzioni'), 'Manutenzione Transpalett manuali', 'D.Lgs 81/08 all.V', 'quarterly', '2026-03-31', '2026-02-24', '2025-12-23', 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Sicurezza'), 'Manutenzione Carrelli Elevatori', 'D.Lgs 81/08 all.V', 'quarterly', '2026-03-31', '2026-02-24', '2025-12-23', 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambienti - Scarichi idrici'), 'Sostituzione valvola Recipienti Pressione', NULL, 'triennial', '2026-04-03', '2024-07-05', '2023-07-04', 7, 'Locale Archivio', 'Autoclave l 1.000 pozzi', 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Rinnovo CPI', 'D.M. 10.03.1998, D.M. 16.06.1982, DPR 151 del 2011', 'quinquennial', '2026-04-30', '2021-10-21', '2021-10-21', 7, NULL, 'Scadenza 09/06/2026', 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Verifica funzionamento impianto schiuma', 'D.Lgs. 81/2008 (art. 18, c 1, u) D.Lgs. 81/2008 (art. 26)', 'semiannual', '2026-04-30', '2026-02-24', '2026-02-19', 7, 'Locale Archivio', 'Attività in appalto a Ditta specializzata', 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Verifica funzionamento stazione di pompaggio antincendio', 'D.Lgs. 81/2008 (art. 18, c 1, u) D.Lgs. 81/2008 (art. 26)', 'quarterly', '2026-04-30', '2026-02-24', '2026-02-19', 7, 'Locale Archivio', 'Attività in appalto a Ditta specializzata', 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Verifica funzionamento valvola a diluvio impianto schiuma', 'D.Lgs. 81/2008 (art. 18, c 1, u) D.Lgs. 81/2008 (art. 26)', 'quarterly', '2026-04-30', '2026-02-24', '2026-02-19', 7, 'Locale Archivio', 'Attività in appalto a Ditta specializzata', 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Manutenzioni'), 'Verifica impianto rivelazione incendi', 'D.Lgs. 81/2008 (art. 18, c 1, u) D.Lgs. 81/2008 (art. 26)', 'semiannual', '2026-05-12', '2026-03-21', '2025-11-13', 7, 'Locale Archivio', NULL, 1, 'Delegato di funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambienti - Scarichi idrici'), 'Sanificazione filtri rubinetti e pigne docce con candeggina', 'D.Lgs 81/08 art. 209', 'quarterly', '2026-05-31', '2026-02-24', '2026-02-24', 7, NULL, NULL, 1, 'Ditta Incaricata');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES (NULL, 'Manutenzione e controllo fughe pompa di calore ELCO', 'D.M. 17/03/03', 'annual', '2026-05-31', '2026-02-24', NULL, 7, NULL, 'Presentazione a cura delle ditte incaricate', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente'), 'Gestione Rischi Impianto Trattamento Aria ((Unità trattamento aria - Filtri Aria - Umidificazione a vapore - Batterie di scambio termico - Ventilazione UTA - Raffreddatori di liquido - Apparecchi terminali) e registrazione su quaderno e libretti', 'D.Lgs 81/08', 'once', '2026-05-31', '2026-03-21', '2025-10-13', 7, 'Locale Archivio', NULL, 1, 'Delegato di funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Manutenzione'), 'Dichiarazione MUD', NULL, 'annual', '2026-06-30', '2026-02-24', '2025-06-19', 7, NULL, NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Comunicazione BANCA DATI GAS FLUORURATI refrigeratori produzione riguardante le emissioni in atmosfera di gas fluorurati', 'Accordo Stato regioni 07.02.2013', 'annual', '2026-06-30', '2026-02-24', NULL, 7, NULL, 'Presentazione a cura delle ditte incaricate', 1, 'DdL-RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Comunicazione BANCA DATI GAS FLUORURATI pompa di calore riguardante le emissioni in atmosfera di gas fluorurati', 'Accordo Stato regioni 07.02.2013', 'annual', '2026-06-30', '2026-02-24', NULL, 7, NULL, 'Presentazione a cura delle ditte incaricate', 1, 'DdL-RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Manutenzione e controllo fughe refrigeratori produzione', 'D.M. 17/03/03', 'annual', '2026-06-30', '2026-02-24', NULL, 7, NULL, NULL, 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Formazione RLS - 4 ore anno', 'D.Lgs 81/08 art. 18, 35', 'annual', '2026-06-30', '2026-02-24', '2025-06-12', 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Medicina del Lavoro'), 'Derattizzazione', NULL, 'semiannual', '2026-06-30', '2026-02-24', NULL, 7, 'Locale Matrici', NULL, 1, 'Incaricato');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Manutenzioni'), 'Pulizia piazzali', NULL, 'semiannual', '2026-06-30', '2026-02-24', NULL, 7, NULL, 'Audit Interno', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Autorizzazione raccolta e trasporto rifiuti speciali non pericolosi FEROLMET', 'D.Lgs 81/08 art. 19, Accordo Stato Regioni 2011', 'once', '2026-07-30', '2023-02-07', NULL, 7, NULL, NULL, 1, 'Amministrazione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente'), 'Autorizzazione raccolta e trasporto rifiuti speciali pericolosi FEROLMET', 'D.Lgs 81/08 art. 202', 'once', '2026-07-30', '2023-02-07', NULL, 7, NULL, NULL, 1, 'Amministrazione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Autocontrolli periodici emissioni in atmosfera', 'D.Lgs 81/08 art. 223', 'annual', '2026-07-31', '2026-02-24', '2025-06-18', 7, NULL, NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES (NULL, 'Comunicazione BANCA DATI GAS FLUORURATI Pompa di Calore Evaporatore riguardante le emissioni in atmosfera di gas fluorurati', 'D.Lgs 81/08 art. 53', 'annual', '2026-07-31', '2026-03-21', NULL, 7, 'Locale Archivio', 'Presentazione a cura della ditta incaricata EcoTechno', 1, 'Delegato di funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Manutenzioni'), 'Controllo documentazione Sicurezza', 'D.Lgs 152/2006 Parte III (art. 124)', 'semiannual', '2026-08-31', '2026-02-24', '2026-02-23', 7, 'Locale Archivio', 'Audit Interno', 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES (NULL, 'Comunicazione BANCA DATI GAS FLUORURATI refrigeratore raffrescamento inizio linee riguardante le emissioni in atmosfera di gas fluorurati', 'Accordo Stato regioni 07.02.2013', 'annual', '2026-08-31', '2026-02-24', NULL, 7, NULL, 'Presentazione a cura delle ditte incaricate', 1, 'DdL-RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Manutenzioni'), 'Verifica dispositivi su uscite di emergenza', 'D.Lgs 81/08 art. 26', 'semiannual', '2026-08-31', '2026-02-24', '2026-02-26', 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Verifica integrità ed efficienza porte tagliafuoco', 'D.Lgs. 81/2008 (art. 18, c 1, u) D.Lgs. 81/2008 (art. 26)', 'semiannual', '2026-08-31', '2026-02-24', '2026-02-26', 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali Medicina del Lavoro'), 'Controllo dispositivi lotta antincendio (estintori, idranti, lance, manichette)', 'D.Lgs 81/08 art. 37, 73, Accordo Stato Regioni 2011', 'semiannual', '2026-08-31', '2026-02-24', '2026-02-26', 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Adempimenti'), 'Formazione del Preposto', NULL, 'quinquennial', '2026-09-20', '2023-02-07', '2021-09-21', 7, NULL, 'Accordo Stato Regioni 21.12.2011', 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Indagine Ambientale', 'D.Lgs 81/08 - Salute e Sicurezza nei luoghi di lavoro', 'annual', '2026-09-30', '2026-02-24', '2025-09-10', 7, 'Locale Archivio', NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente'), 'Controllo fumi e funzionamento caldaia', 'D.Lgs 81/08 art. 190', 'annual', '2026-10-31', '2026-03-21', '2025-05-15', 7, 'Locale Archivio', NULL, 1, 'Delegato di funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Attività in appalto'), 'Riunione periodica art. 35', 'D.Lgs 152/2006 Parte III (art. 124)', 'annual', '2026-11-30', '2025-11-20', '2025-11-20', 7, 'Locale Archivio', NULL, 1, 'Delegato di Funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Valutazione Rischio Vibrazioni', 'D.M. 10.03.1998', 'quadriennial', '2026-12-01', '2023-02-07', '2022-12-02', 7, 'Locale Archivio', 'Accordo Stato Regioni 21.12.2011', 1, 'DdL - RSPP Medico - RLS');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Medicina del Lavoro'), 'Valutazione stress lavoro-correllato', 'D. 20 del 24/01/2011', 'biennial', '2026-12-19', '2024-12-20', '2024-12-20', 7, 'Locale Archivio', NULL, 1, 'DdL - RSPP Medico - RLS');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'Verifica scadenza sostanze assorbenti e neutralizzanti al fine di prevenire l''inquinamento del suolo da parte dell''elettrolita delle batterie', 'D.Lgs 152/2006 Parte V', 'annual', '2026-12-31', '2025-12-22', '2025-12-22', 7, NULL, 'Audit Interno', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambienti - Scarichi idrici'), 'Emergenze (alluvione e sversamento oli) Verifica stato tombini e armadio materiale emergenza alluvione e presenza materiale per pulizia olii', 'D.Lgs 81/08 art. 216', 'annual', '2026-12-31', '2025-12-22', '2025-12-22', 7, NULL, 'Verifica stato tombini eseguita da Roberto+Daniele', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'Simulazione periodica di evacuazione antincendio', 'D.Lgs 81/08 art. 25', 'annual', '2026-12-31', '2025-12-22', '2025-12-22', 7, 'Locale Archivio', NULL, 1, 'Delegato di Funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Contributo CONAI', 'D.Lgs 152/2006 Parte V', 'once', '2027-01-20', '2026-01-20', '2026-01-20', 7, NULL, NULL, 1, '--');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Sicurezza'), 'Nomina 3° resp. per caldaie >35kw', 'D.Lgs 81/08 art. 71', 'once', '2027-01-31', '2026-01-19', '2026-01-19', 7, NULL, NULL, 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente'), 'Analisi schiuma delll''impianto', 'D.Lgs. 81/2008 (art. 18, c 1, u) D.Lgs. 81/2008 (art. 26)', 'annual', '2027-01-31', '2026-03-21', '2026-02-04', 7, 'Locale Archivio', 'Attività in appalto a Ditta specializzata', 1, 'Delegato di funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Verifica impianto di Messa a Terra', 'DPR 462/01', 'biennial', '2027-02-12', '2025-02-19', '2025-02-12', 7, 'Locale Archivio', NULL, 1, 'Delegato di funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Test carrellisti uso sostanze dopanti', 'D.Lgs 81/08', 'annual', '2027-02-28', '2025-02-10', '2026-02-09', 7, NULL, NULL, 1, 'Medico competente');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Visite Mediche ai dipendenti', 'D.Lgs 81/08 art. 18, 38, 41', 'annual', '2027-02-28', '2025-02-19', '2026-02-09', 7, 'Locale Archivio', NULL, 1, 'Medico Competente');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambienti - Scarichi idrici'), 'Controllo Legionella Acqua', 'D.Lgs 81/08 art. 32, Accordo Stato Regioni 2006', 'annual', '2027-02-28', '2026-02-24', '2026-02-19', 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Appaltatori'), 'Verbale di Sopralluogo dei Locali del Medico Competente', 'D.Lgs 81/08', 'annual', '2027-02-28', '2026-02-24', '2026-02-09', 7, 'Locale Archivio', NULL, 1, 'Medico Competente');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Manutenzioni'), 'Dichiarazione acque derivate da pozzi', 'ART. 22 DEL D.LGS. 11 MAGGIO 1999, N. 152, COSÌ COME MODIFICATO DALL’ART. 6, COMMA 1, LETT. A) DEL D.LGS. 18 AGOSTO 2000, N. 258', 'annual', '2027-03-31', '2026-03-21', '2026-03-20', 7, 'Locale Archivio', NULL, 1, 'Delegato di funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'Verifica Periodica Recipienti Pressione', NULL, 'triennial', '2027-04-04', '2024-07-05', '2024-07-05', 7, 'Locale Archivio', 'Autoclave l 1.000 pozzi', 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Revisione scala a gabbia Stp sul tetto del fabbricato tra porzione uffici (più alta) e porzione reparto', 'D.Lgs. n. 81/2008', 'quinquennial', '2027-10-24', '2023-01-31', '2022-10-25', 7, 'Locale Archivio', NULL, 1, 'DdL-RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Visita medica per Addetti VDT', 'D.Lgs 152/2006 Parte III (art. 124)', 'biennial', '2028-02-28', '2026-02-09', '2026-02-09', 7, 'Locale Archivio', NULL, 1, 'Medico Competente');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'Valutazione Rischio da ROA', 'D.P.R. 147 del 15/02/2006', 'quadriennial', '2028-07-14', '2024-11-23', '2024-07-15', 7, 'Locale Archivio', 'Accordo Stato Regioni 21.12.2011', 1, 'DdL - RSPP Medico - RLS');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Valutazione Rischio da CEM', 'LEGGE N. 70/94 D.M. 17/12/2009', 'quadriennial', '2028-07-14', '2024-11-23', '2024-07-15', 7, 'Locale Archivio', 'Accordo Stato Regioni 21.12.2011', 1, 'DdL - RSPP Medico - RLS');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Sicurezza'), 'Valutazione Rischio Rumore', 'D.M. 16/03/1998 L. n° 447/95', 'quadriennial', '2028-09-30', '2024-11-23', '2024-10-02', 7, 'Locale Archivio', NULL, 1, 'DdL - RSPP Medico - RLS');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Protezione contro i fulmini - Valutazione del rischio e scelta delle misure di protezione', 'D.Lgs. 81/2008 / CEI EN 62305-1-2-3-4 / CEI 81-29 / CEI EN IEC 62858', 'quinquennial', '2028-12-31', '2023-09-04', '2023-07-31', 7, 'Locale Archivio', 'Rivalutazione del valore NG', 1, 'DdL-RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambienti - Scarichi idrici'), 'Autorizzazione allo scarico rete fognaria di acque reflue domestiche', 'D.Lgs 152/2006 Parte III (art. 124)', 'quadriennial', '2028-05-21', '2026-03-21', '2025-05-22', 7, NULL, 'Validità 4 anni sino al 21/05/2029. Domanda di rinnovo almeno 1 anno prima della scadenza', 1, 'Condominio Fornace Venere in persona dell''Amministratore');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambienti - Emissioni in atmosfera'), 'Bombola acetilene l 5 omologata 2020/01', 'D.Lgs 03.04.2006 n.152 Legge R. 21.06.1999 n.18', 'decennial', '2030-01-31', '2021-02-23', '2021-02-23', 7, 'Locale Archivio', NULL, 1, 'DdL');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'Bombola ossigeno l 5 omologata 2020/11', 'D.Lgs 81/08 art. 36, 73,', 'decennial', '2030-11-30', '2021-02-23', '2021-02-23', 7, 'Locale Archivio', NULL, 1, 'DdL');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Sicurezza'), 'Revisione parapetti sul tetto del fabbricato (n. 3 sui lati corti)', 'EN14122 - NTC2018', 'decennial', '2032-10-24', '2023-01-31', '2022-10-25', 7, 'Locale Archivio', NULL, 1, 'DdL-RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Sicurezza'), 'Revisione parapetti soletta locali tecnici (refrigeratori)', 'EN ISO 14122-3:2001+A1:2010', 'decennial', '2032-12-18', '2023-01-31', '2022-12-19', 7, 'Locale Archivio', NULL, 1, 'DdL-RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Emissioni in atmosfera'), 'Rinnovo Autorizzazione alle emissioni in atmosfera', 'D.Lgs 152/2006 Parte III (art. 124)', 'quindecennial', '2037-08-23', '2023-02-24', '2023-02-24', 7, 'Locale Archivio', NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Medicina del Lavoro'), 'Informazione ai lavoratori specifica', 'D.Lgs 81/08 art. 18', 'once', '2027-04-18', NULL, NULL, 7, NULL, 'Accordo Stato Regioni 21.12.2011', 1, 'Delegato di funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Formazione ai lavoratori specifica', 'D.Lgs 81/08 art. 17,28,29', 'once', '2027-04-18', NULL, NULL, 7, NULL, 'Accordo Stato Regioni 21.12.2011', 1, 'Delegato di funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Documento Valutazione dei Rischi', 'D.Lgs 81/08 art. 18 D.M. 10.03.1998', 'once', '2026-10-13', '2025-10-13', '2025-10-13', 7, 'Locale Archivio', NULL, 1, 'DdL');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Piano di Emergenza', 'D.Lgs 81/08 art. 47', 'once', '2026-01-15', '2025-01-15', '2025-01-15', 7, 'Locale Archivio', NULL, 1, 'Delegato di Funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Nomina Medico Competente', 'D.Lgs 81/08 art. 47', 'once', '2026-11-20', '2025-11-20', '2025-11-20', 7, 'Locale Archivio', NULL, 1, 'DdL');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Segnalazione all''INAIL nominativo RLS', 'D.Lgs 81/08 art. 31', 'once', '2027-04-18', '2023-02-07', NULL, 7, 'Locale Archivio', NULL, 1, 'DdL');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prevenzione - Sicurezza'), 'Nomina RSPP', 'D.P.R. 43/12 attuazione del Regolamento CE 842/2006', 'once', '2023-02-01', '2023-02-07', '2022-02-01', 7, NULL, NULL, 1, 'DdL');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES (NULL, 'Elezione RLS', NULL, 'once', '2020-02-18', '2023-02-07', '2019-02-18', 7, 'Locale Archivio', NULL, 1, 'Direzione-Lavoratori');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'Valutazione impatto acustico', 'D.Lgs 81/08 art. 19, Accordo Stato Regioni 2011', 'once', '2018-11-07', '2023-02-07', '2017-11-07', 7, NULL, NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Adempimenti'), 'Nomina Addetti Prev. Incendi', 'Lege n° 457/78', 'once', '2027-04-18', '2023-02-07', NULL, 7, 'Locale Archivio', 'Vedi Organigramma', 1, 'DdL');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Adempimenti'), 'Comunicazione dei requisiti come Medico Competente', 'Regolamento Comunale 56', 'once', '2027-04-18', '2023-02-07', NULL, 7, 'Locale Archivio', NULL, 1, 'Medico Competente');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Adempimenti'), 'Protocollo Sanitario', 'D.M. 37/08', 'annual', '2022-12-02', '2023-02-07', '2021-12-02', 7, 'Locale Archivio', 'Rivalidato tramite Riunione Periodica', 1, 'Medico Competente');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Adempimenti'), 'Nomina Dirigenti e Preposti', 'D.M. 37/08', 'once', '2027-04-18', '2023-02-07', NULL, 7, 'Locale Archivio', 'v. incarichi', 1, 'DdL');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Adempimenti'), 'Cassetta di Pronto Soccorso, controllo presenza e scadenza dispositivi e medicinali', 'D.M. 37/08', 'monthly', '2026-05-18', '2023-02-07', NULL, 7, 'Cassette P.S. Reparto e magazzino', NULL, 1, 'Incaricata');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Adempimenti'), 'Nomina Addetti Primo Soccorso', 'D.Lgs 334/99', 'once', '2027-04-18', '2023-02-07', NULL, 7, 'Locale Archivio', 'Vedi Organigramma', 1, 'DdL');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente'), 'Formazione RSPP', 'SIS - Corporate', 'quinquennial', '2031-04-18', '2023-02-07', NULL, 7, NULL, 'Non applicabile. Nomina R.S.P.P. esterno', 1, 'DdL');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente'), 'Decontaminazione/Sanificazione ambienti uffici-spogliatoi-servizi igienici-parti comuni con perossido di idrogeno', NULL, 'once', '2022-12-18', '2023-02-07', '2021-12-18', 7, NULL, NULL, 1, 'Ditta Incaricata');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Controllo scaffalature', 'D.Lgs 81/08 art. 71', 'annual', '2023-01-17', '2023-02-07', '2022-01-17', 7, NULL, 'Audit Interno', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Valutazione Rischio Esplosione', 'D.M. 10.03.1998, UNI 9994', 'once', '2018-03-24', '2023-02-07', '2017-03-24', 7, 'Locale Archivio', NULL, 1, 'DdL - RSPP Medico - RLS');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Valutazione Rischio Chimico', 'D.Lgs 152/2006 Parte IV', 'once', '2027-03-05', '2026-03-21', '2026-03-05', 7, 'Locale Archivio', 'Accordo Stato Regioni 21.12.2011', 1, 'DdL - RSPP Medico - RLS');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Agibilità locali', 'D.Lgs 152/2006 Parte IV', 'once', '2027-04-18', NULL, NULL, 7, 'Locale Archivio', NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Autorizzazione Sanitaria', 'D.M. 17/12/2009', 'once', '2027-04-18', NULL, NULL, 7, 'Locale Archivio', NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Conformità Impianto Elettrico', 'D.M. 17/12/2009', 'once', '2027-04-18', NULL, NULL, 7, 'Locale Archivio', NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Conformità Impianto Gas', 'D.Lgs 152/2006 Parte IV', 'once', '2027-04-18', NULL, NULL, 7, 'Locale Archivio', NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Conformità Impianto Idrico', 'D.Lgs 152/2006 Parte IV', 'once', '2027-04-18', NULL, NULL, 7, 'Locale Archivio', NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Notifica per aziende a rischio rilevante d''incendio', NULL, 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, '--');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambienti - Scarichi idrici'), 'Rendicontazione indicatori ambientali', 'D.Lgs 152/2006 Parte III (art. 124)', 'annual', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Etichettatura contenitori rifiuti pericolosi', 'D.M. 10.03.1998, UNI 9994', 'once', '2027-04-18', NULL, NULL, 7, NULL, 'Audit Interno', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Formulario d’identificazione-compilazione 1° copia e rientro 4° entro 3 mesi', 'D.M. 10.03.1998, UNI 9994', 'once', '2027-04-18', NULL, NULL, 7, NULL, 'Audit Interno', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Tenuta dei registri di carico e scarico rifiuti', 'DPR 151/2011 (art. 6) D.M. 10/03/1998', 'once', '2027-04-18', NULL, NULL, 7, NULL, 'Audit Interno', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Verifica autorizzazione trasportatori-smaltitori', 'D.Lgs 81/08 art. 290', 'annual', '2027-04-18', NULL, NULL, 7, NULL, 'Audit Interno', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente'), 'Monitoraggio consumi: acqua, energia elettrica, metano. materia prima trasformate e rottamata', 'D.M. 10.03.1998', 'monthly', '2026-05-18', NULL, NULL, 7, NULL, 'Audit Interno', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente'), 'Aggiornamento quaderno conduzione impianti. Verifica ed eventuale pulizia/sostituzione filtri cappe emissioni', 'D.LGS. 06.11.2011', 'once', '2027-04-18', NULL, NULL, 7, NULL, 'vedi registro', 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente'), 'Invio analisi dati analitici acque scaricate', 'D.M. 10.03.1998, UNI 9994', 'once', '2027-04-18', NULL, NULL, 7, NULL, 'Presenza di scarichi assimilabili ai domestici', 1, '--');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Controllo segnaletica / cartellonistica antincendio', 'D.M. 10.03.1998, UNI 9994', 'semiannual', '2026-10-18', NULL, NULL, 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Tenuta di REGISTO ANTINCENDIO per attività soggette al DPR 151/2011', 'D.M. 10.03.1998, UNI 9994', 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, '--');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Valutazione Rischio Incendio', 'D.M. 10.03.1998, UNI 9994', 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Verifica Maniglioni antipanico non certificati CE', 'D.M. 10.03.1998, UNI 9994', 'semiannual', '2026-10-18', NULL, NULL, 7, NULL, 'Maniglioni certificati CE', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'Verifica periodica funzionamento impianto luci di emergenza', 'Norma CEI 11-27:2014 " lavoratori su quadri elettrici"', 'semiannual', '2026-10-18', NULL, NULL, 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'Qualifica Appaltatori', 'D.M. 10.03.1998', 'once', '2027-04-18', NULL, NULL, 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'DURC aggiornato', 'D.M. 388/2003', 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'Presenza DUVRI per attività in appalto (aggiornamento o prima emissione)', 'Accordo Stato Regioni 22.02.2012', 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'Presenza verbale di sopralluogo per coordinamento', 'D.Lgs 81/08 art. 19, Accordo Stato Regioni 2011', 'once', '2027-04-18', NULL, NULL, 7, NULL, 'All''avvio dei lavori, se necessario', 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Manutenzioni'), 'Richiesta all''appaltatore / prestatore d''Opera dell''idoneità tecnico professionale: iscrizione Camera di Commercio, autocertificazione i.d.p.', 'D.M. 329/04', 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Manutenzioni'), 'Verificare che i lavoratori in regime di appalto mostrino apposita tessera di riconoscimento (fotografia, generalità del lavoratore e indicazione del datore di lavoro)', 'D.M. 17/03/03', 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Medicina del Lavoro'), 'Formazione addetti che effettuano lavoro sotto tensione', 'D.Lgs 81/08 art. 25, 40', 'quinquennial', '2031-04-18', NULL, NULL, 7, NULL, 'vedi Elenco formazione', 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Medicina del Lavoro'), 'Formazione Addetti Prev. Incendi', 'D.Lgs 81/08 art. 25', 'triennial', '2029-04-18', NULL, NULL, 7, 'Locale Archivio', 'vedi Elenco formazione', 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Formazione Addetti Primo Soccorso', 'D.Lgs 81/08', 'triennial', '2029-04-18', NULL, NULL, 7, 'Locale Archivio', 'vedi Elenco formazione', 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Formazione Carrellisti', 'D.Lgs 81/08 art. 18', 'quinquennial', '2031-04-18', NULL, NULL, 7, 'Locale Archivio', 'vedi Elenco formazione', 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Formazione del Dirigente', 'D.Lgs 81/08 art. 63, allegato IV', 'quinquennial', '2031-04-18', NULL, NULL, 7, NULL, 'Accordo Stato Regioni 21.12.2011', 1, '--');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Comunicazione Messa in Servizio rec. Pressione', 'D.Lgs 81/08 art. 18,74, 75, 76, 77, 78, 79', 'once', '2019-09-07', NULL, '2018-09-07', 7, NULL, NULL, 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Controllo dispositivi lotta antincendio', 'D.Lgs 81/08 art. 71', 'monthly', '2026-05-18', NULL, NULL, 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Comunicazione all''INAIL degli infortuni', 'D.Lgs 81/08 art. 71', 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Conformità Ambienti di Lavoro', 'D.Lgs 81/08 art. 19, 37', 'annual', '2027-04-18', NULL, NULL, 7, NULL, 'Audit Interno', 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Consegna e Gestione DPI', 'D.Lgs 81/08 art. 161', 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Controllo scale', 'D.Lgs 81/08 art. 167', 'once', '2027-04-18', NULL, NULL, 7, NULL, 'Audit Interno', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Controllo imbragature, golfari', 'D.Lgs 81/08 art. 172', 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, '--');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Controllo macchinari', 'D.Lgs 81/08', 'once', '2027-04-18', NULL, NULL, 7, NULL, 'Audit Interno', 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Presenza segnaletica', 'D.Lgs 81/08 art. 271', 'once', '2027-04-18', NULL, NULL, 7, 'Locale Archivio', 'Audit Interno', 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Valutazione del Rischio da MMC', 'D.Lgs 81/08 art. 249', 'once', '2027-04-18', NULL, NULL, 7, 'Locale Archivio', 'in caso di variazioni', 1, 'DdL - RSPP Medico - RLS');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Medicina del Lavoro'), 'Valutazione del Rischio da VDT', 'D.Lgs 81/08 art. 236', 'once', '2027-04-18', NULL, NULL, 7, 'Locale Archivio', 'in caso di variazioni', 1, 'DdL - RSPP Medico - RLS');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Pronto Soccorso'), 'Valutazione Protezione Fulmini', 'D.M. 388/2003', 'once', '2027-04-18', NULL, NULL, 7, 'Locale Archivio', NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Pronto Soccorso'), 'Valutazione Rischio da Agenti Biologici', 'D.Lgs 81/08 art. 18, 43, 45', 'annual', '2018-05-22', NULL, '2017-05-22', 7, 'Locale Archivio', 'Riunione periodica', 1, 'DdL - RSPP Medico - RLS');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Valutazione Rischio da Amianto', NULL, 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Valutazione Rischio da Cancerogeni', NULL, 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, NULL);
|
||||
|
||||
COMMIT;
|
||||
@@ -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 subject_id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$inUse = (int)$stmt->fetchColumn();
|
||||
|
||||
if ($inUse > 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => "Impossibile eliminare: l'argomento è utilizzato in $inUse scadenz" . ($inUse === 1 ? 'a' : 'e') . '.',
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo->prepare("DELETE FROM scad_subjects WHERE id = ?")->execute([$id]);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => 'Argomento eliminato.']);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?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'] ?? '');
|
||||
$color = trim($_POST['color'] ?? '');
|
||||
|
||||
if ($name === '') {
|
||||
echo json_encode(['success' => false, 'message' => 'Il nome è obbligatorio.']);
|
||||
exit;
|
||||
}
|
||||
if (mb_strlen($name) > 100) {
|
||||
echo json_encode(['success' => false, 'message' => 'Il nome supera 100 caratteri.']);
|
||||
exit;
|
||||
}
|
||||
if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $color)) {
|
||||
$color = '#6c757d';
|
||||
}
|
||||
|
||||
// Uniqueness check
|
||||
if ($id) {
|
||||
$stmt = $pdo->prepare("SELECT id FROM scad_subjects WHERE name = ? AND id <> ?");
|
||||
$stmt->execute([$name, $id]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("SELECT id FROM scad_subjects WHERE name = ?");
|
||||
$stmt->execute([$name]);
|
||||
}
|
||||
if ($stmt->fetch()) {
|
||||
echo json_encode(['success' => false, 'message' => 'Esiste già un argomento con questo nome.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($id) {
|
||||
$stmt = $pdo->prepare("UPDATE scad_subjects SET name = ?, color = ? WHERE id = ?");
|
||||
$stmt->execute([$name, $color, $id]);
|
||||
$savedId = $id;
|
||||
} else {
|
||||
$stmt = $pdo->prepare("INSERT INTO scad_subjects (name, color) VALUES (?, ?)");
|
||||
$stmt->execute([$name, $color]);
|
||||
$savedId = (int)$pdo->lastInsertId();
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => $id ? 'Argomento aggiornato.' : 'Argomento creato.',
|
||||
'id' => $savedId,
|
||||
'name' => $name,
|
||||
'color' => $color,
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
<?php include('../../include/headscript.php'); ?>
|
||||
<?php
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$subjects = $pdo->query("
|
||||
SELECT s.*,
|
||||
(SELECT COUNT(*) FROM scad_deadlines d WHERE d.subject_id = s.id) AS deadline_count,
|
||||
(SELECT COUNT(*) FROM scad_deadlines d WHERE d.subject_id = s.id AND d.status <> 'completed') AS open_count
|
||||
FROM scad_subjects s
|
||||
ORDER BY s.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">
|
||||
<?php
|
||||
$scriptDir = dirname($_SERVER['SCRIPT_NAME']);
|
||||
// subjects/index.php -> scadenzario -> userarea
|
||||
$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 - Argomenti</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; letter-spacing: -0.01em; }
|
||||
.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; transition: all 0.2s; }
|
||||
.btn-scad-primary:hover { background: var(--scad-primary-hover); color: #fff; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(90,143,216,0.35); }
|
||||
.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; transition: all 0.2s; }
|
||||
.btn-scad-outline:hover { background: var(--scad-primary); color: #fff; transform: translateY(-1px); }
|
||||
|
||||
.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; transition: all 0.15s; }
|
||||
.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; }
|
||||
.btn-action-history { background: rgba(108,117,125,0.12); color: #495057; }
|
||||
.btn-action-history:hover { background: #495057; color: #fff; }
|
||||
|
||||
.color-swatch { width: 28px; height: 28px; border-radius: 6px; display: inline-block; border: 1px solid rgba(0,0,0,0.08); vertical-align: middle; }
|
||||
.subject-row { border-left: 4px solid var(--row-color, #e9ecef); }
|
||||
|
||||
/* Mobile cards */
|
||||
.subject-card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--scad-card-border);
|
||||
border-left: 5px solid var(--row-color, #e9ecef);
|
||||
border-radius: 0.6rem;
|
||||
padding: 0.85rem 0.95rem;
|
||||
margin-bottom: 0.6rem;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
||||
}
|
||||
.subject-card .sc-header {
|
||||
display: flex; align-items: center; gap: 0.6rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.subject-card .sc-swatch {
|
||||
width: 22px; height: 22px; border-radius: 5px;
|
||||
border: 1px solid rgba(0,0,0,0.08); flex-shrink: 0;
|
||||
}
|
||||
.subject-card .sc-name {
|
||||
font-weight: 700; color: var(--scad-heading);
|
||||
font-size: 0.95rem; flex: 1; word-break: break-word;
|
||||
}
|
||||
.subject-card .sc-stats {
|
||||
display: flex; gap: 0.75rem; font-size: 0.8rem; color: #6c757d;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.subject-card .sc-stats strong { color: var(--scad-heading); }
|
||||
.subject-card .sc-actions {
|
||||
display: flex; gap: 0.4rem; justify-content: flex-end;
|
||||
}
|
||||
|
||||
.empty-state { text-align: center; padding: 3rem 1rem; color: #6c757d; }
|
||||
.empty-state i { font-size: 3rem; opacity: 0.3; margin-bottom: 1rem; }
|
||||
|
||||
/* Color picker swatches */
|
||||
.color-picker-grid { display: grid; grid-template-columns: repeat(10, 1fr); gap: 0.4rem; margin-bottom: 0.75rem; }
|
||||
.color-picker-swatch { width: 100%; aspect-ratio: 1; border-radius: 6px; cursor: pointer; border: 2px solid transparent; transition: all 0.15s; }
|
||||
.color-picker-swatch:hover { transform: scale(1.1); }
|
||||
.color-picker-swatch.selected { border-color: #2c3e6b; transform: scale(1.1); box-shadow: 0 2px 8px rgba(44,62,107,0.3); }
|
||||
|
||||
@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">Argomenti</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-tags me-2"></i>Argomenti</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="btnAddSubject">
|
||||
<i class="fa-solid fa-plus"></i><span>Nuovo Argomento</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (count($subjects) === 0): ?>
|
||||
<div class="empty-state">
|
||||
<i class="fa-solid fa-tags"></i>
|
||||
<p>Nessun argomento definito.<br>Clicca <strong>"Nuovo Argomento"</strong> per aggiungere il primo.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div id="subjectsList">
|
||||
<!-- MOBILE: Cards (< md) -->
|
||||
<div class="d-md-none">
|
||||
<?php foreach ($subjects as $s): ?>
|
||||
<div class="subject-card"
|
||||
style="--row-color: <?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-id="<?= (int)$s['id'] ?>"
|
||||
data-name="<?= htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-color="<?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-in-use="<?= (int)$s['deadline_count'] ?>">
|
||||
<div class="sc-header">
|
||||
<span class="sc-swatch" style="background: <?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"></span>
|
||||
<span class="sc-name"><?= htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') ?></span>
|
||||
</div>
|
||||
<div class="sc-stats">
|
||||
<span>Scadenze: <strong><?= (int)$s['deadline_count'] ?></strong></span>
|
||||
<span>Aperte: <strong><?= (int)$s['open_count'] ?></strong></span>
|
||||
</div>
|
||||
<div class="sc-actions">
|
||||
<a href="scadenzario/index.php?subject_id=<?= (int)$s['id'] ?>" class="btn-action btn-action-history" title="Storico scadenze">
|
||||
<i class="fa-solid fa-clock-rotate-left"></i>
|
||||
</a>
|
||||
<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>
|
||||
|
||||
<!-- DESKTOP: Table (>= md) -->
|
||||
<div class="d-none d-md-block">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:60px">Colore</th>
|
||||
<th>Nome</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:180px">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($subjects as $s): ?>
|
||||
<tr class="subject-row"
|
||||
style="--row-color: <?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-id="<?= (int)$s['id'] ?>"
|
||||
data-name="<?= htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-color="<?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-in-use="<?= (int)$s['deadline_count'] ?>">
|
||||
<td><span class="color-swatch" style="background: <?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"></span></td>
|
||||
<td class="fw-semibold" style="color:var(--scad-heading)"><?= htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td class="text-center"><?= (int)$s['deadline_count'] ?></td>
|
||||
<td class="text-center"><?= (int)$s['open_count'] ?></td>
|
||||
<td class="text-center">
|
||||
<div class="d-inline-flex gap-1">
|
||||
<a href="scadenzario/index.php?subject_id=<?= (int)$s['id'] ?>" class="btn-action btn-action-history" title="Storico scadenze">
|
||||
<i class="fa-solid fa-clock-rotate-left"></i>
|
||||
</a>
|
||||
<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>
|
||||
|
||||
<!-- Subject Modal -->
|
||||
<div class="modal fade" id="subjectModal" 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="subjectModalTitle">Nuovo Argomento</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
|
||||
</div>
|
||||
<form id="subjectForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="subjId" name="id" value="">
|
||||
<div class="mb-3">
|
||||
<label for="subjName" class="form-label fw-semibold">Nome <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="subjName" name="name" maxlength="100" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Colore</label>
|
||||
<div class="color-picker-grid" id="colorPickerGrid"></div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<input type="color" class="form-control form-control-color" id="subjColor" name="color" value="#6c757d" style="width:56px;height:38px;padding:2px">
|
||||
<input type="text" class="form-control" id="subjColorText" maxlength="7" placeholder="#RRGGBB" style="max-width:130px;font-family:monospace">
|
||||
<span class="text-muted small">Personalizzato</span>
|
||||
</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">Salva</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include('../../jsinclude.php'); ?>
|
||||
<script>
|
||||
$(function () {
|
||||
const PRESET_COLORS = [
|
||||
'#dc3545','#e8930c','#ffc107','#198754','#20c997',
|
||||
'#0dcaf0','#0d6efd','#5a8fd8','#6f42c1','#d63384',
|
||||
'#6c757d','#495057','#212529','#8b4513','#795548',
|
||||
'#b88a44','#e83e8c','#17a2b8','#28a745','#343a40'
|
||||
];
|
||||
|
||||
function buildPicker(selected) {
|
||||
const $grid = $('#colorPickerGrid').empty();
|
||||
PRESET_COLORS.forEach(c => {
|
||||
const $sw = $('<div class="color-picker-swatch"></div>')
|
||||
.css('background', c)
|
||||
.attr('data-color', c);
|
||||
if (c.toLowerCase() === (selected || '').toLowerCase()) $sw.addClass('selected');
|
||||
$sw.on('click', function () {
|
||||
$('#colorPickerGrid .color-picker-swatch').removeClass('selected');
|
||||
$(this).addClass('selected');
|
||||
$('#subjColor').val(c);
|
||||
$('#subjColorText').val(c);
|
||||
});
|
||||
$grid.append($sw);
|
||||
});
|
||||
}
|
||||
|
||||
function openModal(data) {
|
||||
const isEdit = !!data;
|
||||
$('#subjectModalTitle').text(isEdit ? 'Modifica Argomento' : 'Nuovo Argomento');
|
||||
$('#subjId').val(isEdit ? data.id : '');
|
||||
$('#subjName').val(isEdit ? data.name : '');
|
||||
const color = isEdit ? data.color : '#6c757d';
|
||||
$('#subjColor').val(color);
|
||||
$('#subjColorText').val(color);
|
||||
buildPicker(color);
|
||||
new bootstrap.Modal('#subjectModal').show();
|
||||
}
|
||||
|
||||
$('#btnAddSubject').on('click', () => openModal(null));
|
||||
|
||||
$('#subjectsList').on('click', '.btn-edit', function () {
|
||||
const $tr = $(this).closest('[data-id]');
|
||||
openModal({ id: $tr.data('id'), name: $tr.data('name'), color: $tr.data('color') });
|
||||
});
|
||||
|
||||
$('#subjectsList').on('click', '.btn-delete', function () {
|
||||
const $tr = $(this).closest('[data-id]');
|
||||
const inUse = parseInt($tr.data('in-use') || 0, 10);
|
||||
const name = $tr.data('name');
|
||||
if (inUse > 0) {
|
||||
Swal.fire({ icon: 'warning', title: 'Impossibile eliminare',
|
||||
text: `L'argomento "${name}" è utilizzato in ${inUse} scadenz${inUse === 1 ? 'a' : 'e'}.` });
|
||||
return;
|
||||
}
|
||||
Swal.fire({
|
||||
title: `Eliminare "${name}"?`,
|
||||
icon: 'warning', showCancelButton: true,
|
||||
confirmButtonText: 'Elimina', cancelButtonText: 'Annulla',
|
||||
confirmButtonColor: '#dc3545'
|
||||
}).then(r => {
|
||||
if (!r.isConfirmed) return;
|
||||
$.post('scadenzario/subjects/ajax/delete_subject.php', { id: $tr.data('id') })
|
||||
.done(res => {
|
||||
if (res.success) { location.reload(); }
|
||||
else { Swal.fire({ icon: 'error', title: 'Errore', text: res.message }); }
|
||||
})
|
||||
.fail(() => Swal.fire({ icon: 'error', title: 'Errore di rete' }));
|
||||
});
|
||||
});
|
||||
|
||||
$('#subjColor').on('input', function () { $('#subjColorText').val($(this).val()); });
|
||||
$('#subjColorText').on('input', function () {
|
||||
const v = $(this).val();
|
||||
if (/^#[0-9A-Fa-f]{6}$/.test(v)) $('#subjColor').val(v);
|
||||
});
|
||||
|
||||
$('#subjectForm').on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const payload = {
|
||||
id: $('#subjId').val(),
|
||||
name: $('#subjName').val().trim(),
|
||||
color: $('#subjColor').val()
|
||||
};
|
||||
if (!payload.name) { Swal.fire({ icon: 'warning', title: 'Nome obbligatorio' }); return; }
|
||||
$.post('scadenzario/subjects/ajax/save_subject.php', payload)
|
||||
.done(res => {
|
||||
if (res.success) { location.reload(); }
|
||||
else { Swal.fire({ icon: 'error', title: 'Errore', text: res.message }); }
|
||||
})
|
||||
.fail(() => Swal.fire({ icon: 'error', title: 'Errore di rete' }));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -208,7 +208,7 @@ while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -130,7 +130,6 @@ $tools = $pdo->query("
|
||||
<title>Gestione Skills</title>
|
||||
|
||||
<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>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||
@@ -177,7 +176,7 @@ $tools = $pdo->query("
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
<!-- jQuery e Bootstrap -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
@@ -119,7 +118,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
|
||||
<!-- 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 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 = 'L’email è obbligatoria.';
|
||||
}
|
||||
|
||||
if (!$errorMessage && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$errorMessage = 'L’email 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 dell’avatar.';
|
||||
} else {
|
||||
$maxFileSize = 2 * 1024 * 1024; // 2 MB
|
||||
|
||||
if ($_FILES['avatar']['size'] > $maxFileSize) {
|
||||
$errorMessage = 'L’avatar 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>
|
||||
@@ -139,7 +139,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
<!-- jQuery e Bootstrap -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
@@ -117,7 +116,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
<!-- jQuery e Bootstrap -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
@@ -111,7 +110,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user