51 Commits

Author SHA1 Message Date
solocla 27cbc9f449 cad area update con autocontorno 2026-06-16 12:05:50 +02:00
solocla 4c09a0dcb4 cad area punti mpodifica manuale 2026-06-16 09:44:19 +02:00
solocla 8bb23ee563 cad area 2026-06-16 09:23:40 +02:00
solocla 20571c9e4b fixed navbar 2026-06-11 10:34:41 +02:00
solocla fdde16b113 added functions email and flag 2026-06-11 10:20:46 +02:00
solocla 33b627f328 image cad area size 2026-06-11 09:02:22 +02:00
solocla d96b4be9e0 fixed add sub roles 2026-06-05 14:53:34 +02:00
solocla 088e518db1 fixed migration 2026-06-05 14:50:51 +02:00
solocla 789c547bc7 Ensure job_roles table exists before job_sub_roles migration 2026-06-05 14:47:11 +02:00
solocla e5bf546ae7 fixed funzioni aziendali 2026-06-05 14:35:19 +02:00
solocla 6dd13e5d7d fixed employess job roles navbar 2026-06-05 10:45:34 +02:00
solocla b1f2bb60e3 added subroles and dpi association fixed all pages and migration 2026-06-04 12:17:17 +02:00
RMubarakzyanov f7e97f55e9 bulk operations for dpi 2026-05-26 20:11:55 +03:00
solocla 70b712ff3b trainings changed details 2026-05-26 16:55:04 +02:00
solocla fdc3af01f3 Merge branch 'feature/user_profile' of https://gitea.solocla.synology.me/solocla/zibo-dashboard into feature/user_profile 2026-05-26 16:05:26 +02:00
solocla 3d54140280 fixed employee profile 2026-05-26 16:05:24 +02:00
RMubarakzyanov bfdbbbfc8f Merge branch 'main' into feature/user_profile 2026-05-26 16:52:41 +03:00
RMubarakzyanov 40a5771a4b bulk operations 2026-05-24 01:04:41 +03:00
RMubarakzyanov 9f5a585717 Merge branch 'main' into feature/user_profile 2026-05-24 00:16:28 +03:00
RMubarakzyanov 9ec5419a86 dlFunction fix 2026-05-24 00:15:09 +03:00
RMubarakzyanov c05091e020 Merge branch 'main' into feature/20260520_scadenziario
# Conflicts:
#	public/userarea/scadenzario/index.php
2026-05-24 00:02:20 +03:00
RMubarakzyanov 0b470f290e fix auto-open 2026-05-23 23:56:43 +03:00
solocla e74870c8d3 added functions 2026-05-22 09:16:46 +02:00
RMubarakzyanov 9001eff317 file repo, cc, auto-open 2026-05-21 23:31:36 +03:00
RMubarakzyanov 7cbd74111d Merge branch 'main' into feature/user_profile
# Conflicts:
#	public/userarea/include/topbar.php
#	public/userarea/scadenzario/include/my_deadlines_widget.php
2026-05-21 22:44:42 +03:00
solocla 650676037a fixed date format 2026-05-20 14:48:45 +02:00
solocla 2fc34c3cf4 fixed redirection 2026-05-18 13:49:22 +02:00
solocla 955a7ed9e9 fixed user setting 2026-05-18 13:31:34 +02:00
RMubarakzyanov cb221a8039 fix initial+refresher 2026-05-17 21:13:57 +03:00
RMubarakzyanov ece1beb87f Merge branch 'main' into feature/user_profile
# Conflicts:
#	public/userarea/include/navbar.php
2026-05-17 20:06:15 +03:00
solocla e6a805f1f7 fixed permission 2026-05-15 21:10:28 +02:00
solocla fe84d446e7 stop tracking vendor 2026-05-15 20:54:16 +02:00
solocla 2ddf575191 phinx 2026-05-15 20:50:06 +02:00
solocla d73a8bb8d3 add permission to dashboard and navbar 2026-05-15 17:13:29 +02:00
RMubarakzyanov d155d1cbab user profile 2026-05-14 16:10:10 +03:00
solocla fa2f293835 added roles edit into employees 2026-05-07 14:39:50 +02:00
solocla fc35adc7f9 big modal deadlines 2026-05-07 09:46:25 +02:00
solocla ac942dcdc8 added depart,ments to employee and deadlines 2026-05-07 09:44:38 +02:00
solocla 5728afa788 added departments 2026-04-30 08:22:24 +02:00
solocla 1946648b1b add modifica record 2026-04-28 11:56:31 +02:00
solocla a1bcab3188 fixed color 2026-04-28 11:48:35 +02:00
solocla 2bbeb11726 fixed sql 2026-04-19 17:37:09 +02:00
RMubarakzyanov dd5edab2f3 deadline widget 2026-04-19 09:14:19 +03:00
RMubarakzyanov 1fadc22178 fix login redirect 2026-04-18 20:37:03 +03:00
RMubarakzyanov d3ee9a3790 mobile view 2026-04-18 17:28:49 +03:00
RMubarakzyanov c387b71cae xls converted to sql 2026-04-18 16:44:53 +03:00
RMubarakzyanov b2cfec77df xls converted to sql 2026-04-18 16:44:14 +03:00
RMubarakzyanov 73b9a3d890 dueDate autofill 2026-04-18 16:25:20 +03:00
RMubarakzyanov 0550ffe923 Subject CRUD 2026-04-18 15:26:04 +03:00
RMubarakzyanov d2e5cc8b2b dynamic basepath 2026-04-16 17:35:13 +03:00
RMubarakzyanov d7b6a58407 deadline feature 2026-04-10 15:51:30 +03:00
11118 changed files with 30186 additions and 1351107 deletions
+4 -2
View File
@@ -31,6 +31,8 @@ MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MANAGER_USER_ID=
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
@@ -55,5 +57,5 @@ AZURE_REDIRECT_URI=https://your-app.com/auth/azure/callback
AZURE_TENANT_ID=
MICROSOFT_CLIENT_ID=your_client_id_here
MICROSOFT_CLIENT_SECRET=your_client_secret_here
MICROSOFT_REDIRECT_URI="${APP_URL}/auth/microsoft/callback"
MICROSOFT_CLIENT_SECRET=your_client_secret_here
MICROSOFT_REDIRECT_URI="${APP_URL}/auth/microsoft/callback"
+4
View File
@@ -46,6 +46,7 @@ public/userarea/last_url.txt
public/userarea/class/curl_auth_debug.log
public/userarea/class/curl_request_debug.log
public/userarea/uploads/cad_area/originals/*
# Ignora tutti i log
*.log
@@ -66,3 +67,6 @@ public/userarea/logsapi/commessaweb_customfields_763.json
public/userarea/logsapi/commessaweb_invia_762.json
public/userarea/logsapi/commessaweb_invia_763.json
public/userarea/logsapi/last_auth_url.txt
# User uploaded files
/public/userarea/files/
@@ -111,6 +111,14 @@ class LoginController extends Controller
return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('User')) {
return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('HR')) {
return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('SuperUser')) {
return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('Management')) {
return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('Quality')) {
return redirect()->to('userarea/production_dashboard.php');
}
// Se il ruolo non è specificato, reindirizza alla home predefinita
+1
View File
@@ -44,6 +44,7 @@
"phpmailer/phpmailer": "^6.9",
"phpoffice/phpspreadsheet": "^4.1",
"proengsoft/laravel-jsvalidation": "^4.0.0",
"robmorgan/phinx": "^0.16.11",
"socialiteproviders/microsoft": "^4.7",
"spatie/laravel-query-builder": "^5.0",
"vanguardapp/activity-log": "^6.0",
Generated
+646 -2
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "9c4f1e3bc3ee2180211c055e70635aef",
"content-hash": "076e7721d08cfea8b06ce75dd8c6c576",
"packages": [
{
"name": "akaunting/laravel-setting",
@@ -251,6 +251,330 @@
],
"time": "2023-11-29T23:19:16+00:00"
},
{
"name": "cakephp/chronos",
"version": "3.5.0",
"source": {
"type": "git",
"url": "https://github.com/cakephp/chronos.git",
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/chronos/zipball/e6e777b534244911566face8a5dbdbd7f7bda5a6",
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/clock": "^1.0"
},
"provide": {
"psr/clock-implementation": "1.0"
},
"require-dev": {
"cakephp/cakephp-codesniffer": "^5.0",
"phpunit/phpunit": "^10.5.58 || ^11.5.3 || ^12.1.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Cake\\Chronos\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brian Nesbitt",
"email": "brian@nesbot.com",
"homepage": "http://nesbot.com"
},
{
"name": "The CakePHP Team",
"homepage": "https://cakephp.org"
}
],
"description": "A simple API extension for DateTime.",
"homepage": "https://cakephp.org",
"keywords": [
"date",
"datetime",
"time"
],
"support": {
"issues": "https://github.com/cakephp/chronos/issues",
"source": "https://github.com/cakephp/chronos"
},
"time": "2026-04-10T02:50:39+00:00"
},
{
"name": "cakephp/core",
"version": "5.3.5",
"source": {
"type": "git",
"url": "https://github.com/cakephp/core.git",
"reference": "eb012517900ed288f580aa3487e9a09f28ea85f9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/core/zipball/eb012517900ed288f580aa3487e9a09f28ea85f9",
"reference": "eb012517900ed288f580aa3487e9a09f28ea85f9",
"shasum": ""
},
"require": {
"cakephp/utility": "^5.3.0",
"league/container": "^5.1",
"php": ">=8.2",
"psr/container": "^1.1 || ^2.0"
},
"provide": {
"psr/container-implementation": "^2.0"
},
"suggest": {
"cakephp/cache": "To use Configure::store() and restore().",
"cakephp/event": "To use PluginApplicationInterface or plugin applications.",
"league/container": "To use Container and ServiceProvider classes"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"files": [
"functions.php"
],
"psr-4": {
"Cake\\Core\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/core/graphs/contributors"
}
],
"description": "CakePHP Framework Core classes",
"homepage": "https://cakephp.org",
"keywords": [
"cakephp",
"core",
"framework"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/core"
},
"time": "2026-03-31T06:25:23+00:00"
},
{
"name": "cakephp/database",
"version": "5.3.5",
"source": {
"type": "git",
"url": "https://github.com/cakephp/database.git",
"reference": "cf94dcb57c54a1a308fd866b038cd6995910e36e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/database/zipball/cf94dcb57c54a1a308fd866b038cd6995910e36e",
"reference": "cf94dcb57c54a1a308fd866b038cd6995910e36e",
"shasum": ""
},
"require": {
"cakephp/chronos": "^3.3",
"cakephp/core": "^5.3.0",
"cakephp/datasource": "^5.3.0",
"php": ">=8.2",
"psr/log": "^3.0"
},
"require-dev": {
"cakephp/i18n": "^5.3.0",
"cakephp/log": "^5.3.0"
},
"suggest": {
"cakephp/i18n": "If you are using locale-aware datetime formats.",
"cakephp/log": "If you want to use query logging without providing a logger yourself."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"psr-4": {
"Cake\\Database\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/database/graphs/contributors"
}
],
"description": "Flexible and powerful Database abstraction library with a familiar PDO-like API",
"homepage": "https://cakephp.org",
"keywords": [
"abstraction",
"cakephp",
"database",
"database abstraction",
"pdo"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/database"
},
"time": "2026-03-31T06:25:23+00:00"
},
{
"name": "cakephp/datasource",
"version": "5.3.5",
"source": {
"type": "git",
"url": "https://github.com/cakephp/datasource.git",
"reference": "512464eb27b19316b515ec338089b83822c9ab5a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/datasource/zipball/512464eb27b19316b515ec338089b83822c9ab5a",
"reference": "512464eb27b19316b515ec338089b83822c9ab5a",
"shasum": ""
},
"require": {
"cakephp/core": "^5.3.0",
"php": ">=8.2",
"psr/simple-cache": "^2.0 || ^3.0"
},
"require-dev": {
"cakephp/cache": "^5.3.0",
"cakephp/collection": "^5.3.0",
"cakephp/utility": "^5.3.0"
},
"suggest": {
"cakephp/cache": "If you decide to use Query caching.",
"cakephp/collection": "If you decide to use ResultSetInterface.",
"cakephp/utility": "If you decide to use EntityTrait."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"psr-4": {
"Cake\\Datasource\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/datasource/graphs/contributors"
}
],
"description": "Provides connection managing and traits for Entities and Queries that can be reused for different datastores",
"homepage": "https://cakephp.org",
"keywords": [
"cakephp",
"connection management",
"datasource",
"entity",
"query"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/datasource"
},
"time": "2026-04-04T08:08:42+00:00"
},
{
"name": "cakephp/utility",
"version": "5.3.5",
"source": {
"type": "git",
"url": "https://github.com/cakephp/utility.git",
"reference": "4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/utility/zipball/4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99",
"reference": "4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99",
"shasum": ""
},
"require": {
"cakephp/core": "^5.3.0",
"php": ">=8.2"
},
"suggest": {
"ext-intl": "To use Text::transliterate() or Text::slug()",
"lib-ICU": "To use Text::transliterate() or Text::slug()"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Cake\\Utility\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/utility/graphs/contributors"
}
],
"description": "CakePHP Utility classes such as Inflector, String, Hash, and Security",
"homepage": "https://cakephp.org",
"keywords": [
"cakephp",
"hash",
"inflector",
"security",
"string",
"utility"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/utility"
},
"time": "2026-03-09T09:38:36+00:00"
},
{
"name": "carbonphp/carbon-doctrine-types",
"version": "3.2.0",
@@ -2627,6 +2951,90 @@
],
"time": "2022-12-11T20:36:23+00:00"
},
{
"name": "league/container",
"version": "5.2.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/container.git",
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/container/zipball/58accbc032f0090a9bd08326f93062c5a658b2c5",
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5",
"shasum": ""
},
"require": {
"php": "^8.1",
"psr/container": "^2.0.2",
"psr/event-dispatcher": "^1.0"
},
"provide": {
"psr/container-implementation": "^1.0"
},
"replace": {
"orno/di": "~2.0"
},
"require-dev": {
"nette/php-generator": "^4.1",
"nikic/php-parser": "^5.0",
"phpstan/phpstan": "^2.1.11",
"phpunit/phpunit": "^10.5.45|^11.5.15|^12.0",
"roave/security-advisories": "dev-latest",
"scrutinizer/ocular": "^1.9",
"squizlabs/php_codesniffer": "^3.9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-1.x": "1.x-dev",
"dev-2.x": "2.x-dev",
"dev-3.x": "3.x-dev",
"dev-4.x": "4.x-dev",
"dev-5.x": "5.x-dev",
"dev-master": "5.x-dev"
}
},
"autoload": {
"psr-4": {
"League\\Container\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Phil Bennett",
"email": "mail@philbennett.co.uk",
"role": "Developer"
}
],
"description": "A fast and intuitive dependency injection container.",
"homepage": "https://github.com/thephpleague/container",
"keywords": [
"container",
"dependency",
"di",
"injection",
"league",
"provider",
"service"
],
"support": {
"issues": "https://github.com/thephpleague/container/issues",
"source": "https://github.com/thephpleague/container/tree/5.2.0"
},
"funding": [
{
"url": "https://github.com/philipobenito",
"type": "github"
}
],
"time": "2026-03-19T18:52:39+00:00"
},
{
"name": "league/flysystem",
"version": "3.28.0",
@@ -4980,6 +5388,93 @@
],
"time": "2024-04-27T21:32:50+00:00"
},
{
"name": "robmorgan/phinx",
"version": "0.16.11",
"source": {
"type": "git",
"url": "https://github.com/cakephp/phinx.git",
"reference": "a03014fea316ba021fc0776982e5bed2d10228d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/phinx/zipball/a03014fea316ba021fc0776982e5bed2d10228d4",
"reference": "a03014fea316ba021fc0776982e5bed2d10228d4",
"shasum": ""
},
"require": {
"cakephp/database": "^5.0.2",
"composer-runtime-api": "^2.0",
"php-64bit": ">=8.1",
"psr/container": "^1.1|^2.0",
"symfony/config": "^4.0|^5.0|^6.0|^7.0|^8.0",
"symfony/console": "^6.0|^7.0|^8.0"
},
"require-dev": {
"cakephp/cakephp-codesniffer": "^5.0",
"cakephp/i18n": "^5.0",
"ext-json": "*",
"ext-pdo": "*",
"phpunit/phpunit": "^10.5",
"symfony/yaml": "^4.0|^5.0|^6.0|^7.0|^8.0"
},
"suggest": {
"ext-json": "Install if using JSON configuration format",
"ext-pdo": "PDO extension is needed",
"symfony/yaml": "Install if using YAML configuration format"
},
"bin": [
"bin/phinx"
],
"type": "library",
"autoload": {
"psr-4": {
"Phinx\\": "src/Phinx/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Rob Morgan",
"email": "robbym@gmail.com",
"homepage": "https://robmorgan.id.au",
"role": "Lead Developer"
},
{
"name": "Woody Gilk",
"email": "woody.gilk@gmail.com",
"homepage": "https://shadowhand.me",
"role": "Developer"
},
{
"name": "Richard Quadling",
"email": "rquadling@gmail.com",
"role": "Developer"
},
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/phinx/graphs/contributors",
"role": "Developer"
}
],
"description": "Phinx makes it ridiculously easy to manage the database migrations for your PHP app.",
"homepage": "https://phinx.org",
"keywords": [
"database",
"database migrations",
"db",
"migrations",
"phinx"
],
"support": {
"issues": "https://github.com/cakephp/phinx/issues",
"source": "https://github.com/cakephp/phinx/tree/0.16.11"
},
"time": "2026-03-15T00:04:32+00:00"
},
{
"name": "socialiteproviders/manager",
"version": "v4.8.1",
@@ -5312,6 +5807,85 @@
],
"time": "2024-05-31T14:57:53+00:00"
},
{
"name": "symfony/config",
"version": "v7.4.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
"reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57",
"reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/filesystem": "^7.1|^8.0",
"symfony/polyfill-ctype": "~1.8"
},
"conflict": {
"symfony/finder": "<6.4",
"symfony/service-contracts": "<2.5"
},
"require-dev": {
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
"symfony/finder": "^6.4|^7.0|^8.0",
"symfony/messenger": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/yaml": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Config\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/config/tree/v7.4.10"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-03T14:20:49+00:00"
},
{
"name": "symfony/console",
"version": "v7.1.3",
@@ -5768,6 +6342,76 @@
],
"time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/filesystem",
"version": "v7.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.8"
},
"require-dev": {
"symfony/process": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Filesystem\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/filesystem/tree/v7.4.11"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-11T16:38:44+00:00"
},
{
"name": "symfony/finder",
"version": "v7.1.3",
@@ -11355,6 +11999,6 @@
"php": "^8.2.0",
"ext-json": "*"
},
"platform-dev": [],
"platform-dev": {},
"plugin-api-version": "2.6.0"
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class BaselineExistingDatabase extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
// Baseline migration.
// Existing database structure starts being tracked from this point.
}
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreatePhinxTestTable extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$table = $this->table('phinx_test_table');
$table
->addColumn('name', 'string', [
'limit' => 100,
'null' => false,
])
->addColumn('created_at', 'timestamp', [
'default' => 'CURRENT_TIMESTAMP',
'null' => false,
])
->create();
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddFunctionsToScadDeadlines extends AbstractMigration
{
public function change(): void
{
$this->table('scad_functions', [
'id' => false,
'primary_key' => ['id'],
'collation' => 'utf8mb4_unicode_ci',
'encoding' => 'utf8mb4',
])
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('name', 'string', [
'limit' => 255,
'null' => false,
])
->addColumn('description', 'text', [
'null' => true,
])
->addColumn('status', 'string', [
'limit' => 20,
'null' => false,
'default' => 'active',
])
->addColumn('created_at', 'timestamp', [
'null' => false,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => false,
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['name'], [
'unique' => true,
'name' => 'uniq_scad_functions_name',
])
->create();
$this->table('scad_deadlines')
->addColumn('function_id', 'integer', [
'signed' => false,
'null' => true,
'after' => 'subject_id',
])
->addIndex(['function_id'], [
'name' => 'idx_scad_deadlines_function_id',
])
->addForeignKey('function_id', 'scad_functions', 'id', [
'delete' => 'SET_NULL',
'update' => 'CASCADE',
'constraint' => 'fk_scad_deadlines_function',
])
->update();
}
}
@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateJobSubRolesTable extends AbstractMigration
{
public function change(): void
{
if (!$this->hasTable('job_roles')) {
$rolesTable = $this->table('job_roles', [
'id' => false,
'primary_key' => ['id'],
'collation' => 'utf8mb4_unicode_ci',
'encoding' => 'utf8mb4',
]);
$rolesTable
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('name', 'string', [
'limit' => 255,
'null' => false,
])
->addColumn('description', 'text', [
'null' => true,
'default' => null,
])
->addColumn('sort_order', 'integer', [
'signed' => false,
'null' => false,
'default' => 999,
])
->addColumn('is_active', 'boolean', [
'null' => false,
'default' => 1,
])
->addColumn('created_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['is_active'], [
'name' => 'idx_job_roles_is_active',
])
->addIndex(['sort_order'], [
'name' => 'idx_job_roles_sort_order',
])
->create();
}
if (!$this->hasTable('job_sub_roles')) {
$table = $this->table('job_sub_roles', [
'id' => false,
'primary_key' => ['id'],
'collation' => 'utf8mb4_unicode_ci',
'encoding' => 'utf8mb4',
]);
$table
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('job_role_id', 'integer', [
'signed' => false,
'null' => false,
])
->addColumn('name', 'string', [
'limit' => 255,
'null' => false,
])
->addColumn('description', 'text', [
'null' => true,
'default' => null,
])
->addColumn('sort_order', 'integer', [
'signed' => false,
'null' => false,
'default' => 999,
])
->addColumn('is_active', 'boolean', [
'null' => false,
'default' => 1,
])
->addColumn('created_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['job_role_id'], [
'name' => 'idx_job_sub_roles_job_role_id',
])
->addIndex(['is_active'], [
'name' => 'idx_job_sub_roles_is_active',
])
->addIndex(['sort_order'], [
'name' => 'idx_job_sub_roles_sort_order',
])
->addForeignKey(
'job_role_id',
'job_roles',
'id',
[
'delete' => 'CASCADE',
'update' => 'CASCADE',
'constraint' => 'fk_job_sub_roles_job_role',
]
)
->create();
}
}
}
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreatePpeItemsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('ppe_items', [
'id' => false,
'primary_key' => ['id'],
'collation' => 'utf8mb4_unicode_ci',
'encoding' => 'utf8mb4',
]);
$table
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('name', 'string', [
'limit' => 255,
'null' => false,
])
->addColumn('description', 'text', [
'null' => true,
'default' => null,
])
->addColumn('category', 'string', [
'limit' => 100,
'null' => true,
'default' => null,
'comment' => 'PPE category, for example Head, Hands, Eyes, Feet, Respiratory',
])
->addColumn('photo', 'string', [
'limit' => 255,
'null' => true,
'default' => null,
'comment' => 'PPE image path or filename',
])
->addColumn('standard_reference', 'string', [
'limit' => 255,
'null' => true,
'default' => null,
'comment' => 'Reference standard, for example EN ISO 20345',
])
->addColumn('validity_months', 'integer', [
'signed' => false,
'null' => true,
'default' => null,
'comment' => 'Default validity in months after assignment',
])
->addColumn('sort_order', 'integer', [
'signed' => false,
'null' => false,
'default' => 999,
])
->addColumn('is_active', 'boolean', [
'null' => false,
'default' => 1,
])
->addColumn('created_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['category'], [
'name' => 'idx_ppe_items_category',
])
->addIndex(['is_active'], [
'name' => 'idx_ppe_items_is_active',
])
->addIndex(['sort_order'], [
'name' => 'idx_ppe_items_sort_order',
])
->create();
}
}
@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateEmployeePpeItemsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('employee_ppe_items', [
'id' => false,
'primary_key' => ['id'],
'collation' => 'utf8mb4_unicode_ci',
'encoding' => 'utf8mb4',
]);
$table
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('employee_id', 'integer', [
'signed' => false,
'null' => false,
])
->addColumn('ppe_item_id', 'integer', [
'signed' => false,
'null' => false,
])
->addColumn('assigned_date', 'date', [
'null' => true,
'default' => null,
])
->addColumn('expiry_date', 'date', [
'null' => true,
'default' => null,
])
->addColumn('quantity', 'integer', [
'signed' => false,
'null' => false,
'default' => 1,
])
->addColumn('status', 'enum', [
'values' => [
'assigned',
'returned',
'expired',
'lost',
'damaged',
],
'null' => false,
'default' => 'assigned',
])
->addColumn('notes', 'text', [
'null' => true,
'default' => null,
])
->addColumn('created_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['employee_id'], [
'name' => 'idx_employee_ppe_items_employee_id',
])
->addIndex(['ppe_item_id'], [
'name' => 'idx_employee_ppe_items_ppe_item_id',
])
->addIndex(['status'], [
'name' => 'idx_employee_ppe_items_status',
])
->addIndex(['expiry_date'], [
'name' => 'idx_employee_ppe_items_expiry_date',
])
->addForeignKey(
'employee_id',
'employees',
'id',
[
'delete' => 'CASCADE',
'update' => 'CASCADE',
'constraint' => 'fk_employee_ppe_items_employee',
]
)
->addForeignKey(
'ppe_item_id',
'ppe_items',
'id',
[
'delete' => 'RESTRICT',
'update' => 'CASCADE',
'constraint' => 'fk_employee_ppe_items_ppe_item',
]
)
->create();
}
}
@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateJobSubRolePpeItemsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('job_sub_role_ppe_items', [
'id' => false,
'primary_key' => ['id'],
'collation' => 'utf8mb4_unicode_ci',
'encoding' => 'utf8mb4',
]);
$table
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('job_sub_role_id', 'integer', [
'signed' => false,
'null' => false,
])
->addColumn('ppe_item_id', 'integer', [
'signed' => false,
'null' => false,
])
->addColumn('requirement_type', 'enum', [
'values' => [
'mandatory',
'recommended',
'optional',
],
'null' => false,
'default' => 'mandatory',
'comment' => 'Defines if the PPE is mandatory, recommended or optional for the sub role',
])
->addColumn('notes', 'text', [
'null' => true,
'default' => null,
])
->addColumn('sort_order', 'integer', [
'signed' => false,
'null' => false,
'default' => 999,
])
->addColumn('is_active', 'boolean', [
'null' => false,
'default' => 1,
])
->addColumn('created_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['job_sub_role_id'], [
'name' => 'idx_job_sub_role_ppe_items_sub_role_id',
])
->addIndex(['ppe_item_id'], [
'name' => 'idx_job_sub_role_ppe_items_ppe_item_id',
])
->addIndex(['requirement_type'], [
'name' => 'idx_job_sub_role_ppe_items_requirement_type',
])
->addIndex(['is_active'], [
'name' => 'idx_job_sub_role_ppe_items_is_active',
])
->addIndex(['job_sub_role_id', 'ppe_item_id'], [
'unique' => true,
'name' => 'uq_job_sub_role_ppe_item',
])
->addForeignKey(
'job_sub_role_id',
'job_sub_roles',
'id',
[
'delete' => 'CASCADE',
'update' => 'CASCADE',
'constraint' => 'fk_job_sub_role_ppe_items_sub_role',
]
)
->addForeignKey(
'ppe_item_id',
'ppe_items',
'id',
[
'delete' => 'CASCADE',
'update' => 'CASCADE',
'constraint' => 'fk_job_sub_role_ppe_items_ppe_item',
]
)
->create();
}
}
@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddJobSubRoleIdToEmployeesTable extends AbstractMigration
{
public function up(): void
{
if (!$this->hasTable('employees')) {
throw new RuntimeException('Table employees does not exist.');
}
$table = $this->table('employees');
if (!$table->hasColumn('job_role_id')) {
$table
->addColumn('job_role_id', 'integer', [
'signed' => false,
'null' => true,
'after' => 'department_id',
])
->addIndex(['job_role_id'], [
'name' => 'idx_employees_job_role_id',
])
->addForeignKey(
'job_role_id',
'job_roles',
'id',
[
'delete' => 'SET_NULL',
'update' => 'CASCADE',
'constraint' => 'fk_employees_job_role',
]
)
->update();
}
$table = $this->table('employees');
if (!$table->hasColumn('job_sub_role_id')) {
$afterColumn = $table->hasColumn('job_role_id') ? 'job_role_id' : 'department_id';
$table
->addColumn('job_sub_role_id', 'integer', [
'signed' => false,
'null' => true,
'after' => $afterColumn,
])
->addIndex(['job_sub_role_id'], [
'name' => 'idx_employees_job_sub_role_id',
])
->addForeignKey(
'job_sub_role_id',
'job_sub_roles',
'id',
[
'delete' => 'SET_NULL',
'update' => 'CASCADE',
'constraint' => 'fk_employees_job_sub_role',
]
)
->update();
}
}
public function down(): void
{
if (!$this->hasTable('employees')) {
return;
}
$table = $this->table('employees');
if ($table->hasForeignKey('job_sub_role_id')) {
$table->dropForeignKey('job_sub_role_id')->update();
}
if ($table->hasForeignKey('job_role_id')) {
$table->dropForeignKey('job_role_id')->update();
}
$table = $this->table('employees');
if ($table->hasIndexByName('idx_employees_job_sub_role_id')) {
$table->removeIndexByName('idx_employees_job_sub_role_id')->update();
}
if ($table->hasIndexByName('idx_employees_job_role_id')) {
$table->removeIndexByName('idx_employees_job_role_id')->update();
}
$table = $this->table('employees');
if ($table->hasColumn('job_sub_role_id')) {
$table->removeColumn('job_sub_role_id')->update();
}
if ($table->hasColumn('job_role_id')) {
$table->removeColumn('job_role_id')->update();
}
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddDeliveryFieldsToEmployeePpeItemsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('employee_ppe_items');
$table
->addColumn('delivered_by', 'string', [
'limit' => 255,
'null' => true,
'default' => null,
'after' => 'expiry_date',
])
->addColumn('created_by', 'integer', [
'signed' => false,
'null' => true,
'default' => null,
'after' => 'notes',
])
->addIndex(['created_by'], [
'name' => 'idx_employee_ppe_items_created_by',
])
->addForeignKey(
'created_by',
'auth_users',
'id',
[
'delete' => 'SET_NULL',
'update' => 'CASCADE',
'constraint' => 'fk_employee_ppe_items_created_by',
]
)
->update();
}
}
@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateEmployeeJobSubRolesTable extends AbstractMigration
{
public function up(): void
{
if (!$this->hasTable('employee_job_sub_roles')) {
$table = $this->table('employee_job_sub_roles', [
'id' => false,
'primary_key' => ['id'],
'signed' => false,
'collation' => 'utf8mb4_general_ci',
'encoding' => 'utf8mb4',
]);
$table
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('employee_id', 'integer', [
'signed' => false,
'null' => false,
])
->addColumn('job_sub_role_id', 'integer', [
'signed' => false,
'null' => false,
])
->addColumn('is_primary', 'boolean', [
'null' => false,
'default' => false,
])
->addColumn('created_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
])
->addIndex(['employee_id', 'job_sub_role_id'], [
'unique' => true,
'name' => 'uq_employee_subrole',
])
->addIndex(['employee_id'], [
'name' => 'idx_employee_job_sub_roles_employee',
])
->addIndex(['job_sub_role_id'], [
'name' => 'idx_employee_job_sub_roles_subrole',
])
->addForeignKey(
'employee_id',
'employees',
'id',
[
'delete' => 'CASCADE',
'update' => 'CASCADE',
'constraint' => 'fk_employee_job_sub_roles_employee',
]
)
->addForeignKey(
'job_sub_role_id',
'job_sub_roles',
'id',
[
'delete' => 'CASCADE',
'update' => 'CASCADE',
'constraint' => 'fk_employee_job_sub_roles_subrole',
]
)
->create();
}
// Import existing single sub-role assignments from employees.job_sub_role_id
// into the new bridge table.
$this->execute("
INSERT IGNORE INTO employee_job_sub_roles
(employee_id, job_sub_role_id, is_primary, created_at)
SELECT
e.id,
e.job_sub_role_id,
1,
NOW()
FROM employees e
WHERE e.job_sub_role_id IS NOT NULL
AND e.job_sub_role_id > 0
");
}
public function down(): void
{
if ($this->hasTable('employee_job_sub_roles')) {
$this->table('employee_job_sub_roles')->drop()->save();
}
}
}
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateCompanyFunctionsTable extends AbstractMigration
{
public function up(): void
{
if (!$this->hasTable('company_functions')) {
$table = $this->table('company_functions', [
'id' => false,
'primary_key' => ['id'],
'signed' => false,
'collation' => 'utf8mb4_general_ci',
'encoding' => 'utf8mb4',
]);
$table
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('function_name', 'string', [
'limit' => 150,
'null' => false,
'comment' => 'Function name, for example RSPP, Medico del lavoro, RLS',
])
->addColumn('person_full_name', 'string', [
'limit' => 200,
'null' => false,
'comment' => 'Full name and surname of the person assigned to the function',
])
->addColumn('phone', 'string', [
'limit' => 80,
'null' => true,
])
->addColumn('email', 'string', [
'limit' => 190,
'null' => true,
])
->addColumn('notes', 'text', [
'null' => true,
])
->addColumn('sort_order', 'integer', [
'signed' => false,
'null' => false,
'default' => 0,
])
->addColumn('is_active', 'boolean', [
'null' => false,
'default' => true,
])
->addColumn('created_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => true,
'default' => null,
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['function_name'], [
'name' => 'idx_company_functions_function_name',
])
->addIndex(['person_full_name'], [
'name' => 'idx_company_functions_person_full_name',
])
->addIndex(['email'], [
'name' => 'idx_company_functions_email',
])
->addIndex(['is_active', 'sort_order'], [
'name' => 'idx_company_functions_active_sort',
])
->create();
}
$this->execute("
INSERT INTO company_functions
(function_name, person_full_name, phone, email, notes, sort_order, is_active, created_at, updated_at)
VALUES
('RSPP', '', NULL, NULL, NULL, 10, 1, NOW(), NOW()),
('Medico del lavoro', '', NULL, NULL, NULL, 20, 1, NOW(), NOW()),
('RLS', '', NULL, NULL, NULL, 30, 1, NOW(), NOW())
");
}
public function down(): void
{
if ($this->hasTable('company_functions')) {
$this->table('company_functions')->drop()->save();
}
}
}
@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AlterScadFunctionsAddContactFields extends AbstractMigration
{
public function up(): void
{
if (!$this->hasTable('scad_functions')) {
throw new RuntimeException('Table scad_functions does not exist.');
}
$table = $this->table('scad_functions');
if (!$table->hasColumn('person_full_name')) {
$table->addColumn('person_full_name', 'string', [
'limit' => 200,
'null' => true,
'after' => 'description',
'comment' => 'Full name and surname of the person assigned to the function',
]);
}
if (!$table->hasColumn('phone')) {
$table->addColumn('phone', 'string', [
'limit' => 80,
'null' => true,
'after' => 'person_full_name',
]);
}
if (!$table->hasColumn('email')) {
$table->addColumn('email', 'string', [
'limit' => 190,
'null' => true,
'after' => 'phone',
]);
}
if (!$table->hasColumn('notes')) {
$table->addColumn('notes', 'text', [
'null' => true,
'after' => 'email',
]);
}
if (!$table->hasColumn('sort_order')) {
$table->addColumn('sort_order', 'integer', [
'signed' => false,
'null' => false,
'default' => 0,
'after' => 'status',
]);
}
if (!$table->hasIndexByName('idx_scad_functions_name')) {
$table->addIndex(['name'], [
'name' => 'idx_scad_functions_name',
]);
}
if (!$table->hasIndexByName('idx_scad_functions_person_full_name')) {
$table->addIndex(['person_full_name'], [
'name' => 'idx_scad_functions_person_full_name',
]);
}
if (!$table->hasIndexByName('idx_scad_functions_email')) {
$table->addIndex(['email'], [
'name' => 'idx_scad_functions_email',
]);
}
if (!$table->hasIndexByName('idx_scad_functions_status_sort')) {
$table->addIndex(['status', 'sort_order'], [
'name' => 'idx_scad_functions_status_sort',
]);
}
$table->update();
// Set a default order for existing rows without changing their names.
$this->execute("
UPDATE scad_functions
SET sort_order = id * 10
WHERE sort_order = 0
");
}
public function down(): void
{
if (!$this->hasTable('scad_functions')) {
return;
}
$table = $this->table('scad_functions');
if ($table->hasIndexByName('idx_scad_functions_status_sort')) {
$table->removeIndexByName('idx_scad_functions_status_sort');
}
if ($table->hasIndexByName('idx_scad_functions_email')) {
$table->removeIndexByName('idx_scad_functions_email');
}
if ($table->hasIndexByName('idx_scad_functions_person_full_name')) {
$table->removeIndexByName('idx_scad_functions_person_full_name');
}
if ($table->hasIndexByName('idx_scad_functions_name')) {
$table->removeIndexByName('idx_scad_functions_name');
}
if ($table->hasColumn('sort_order')) {
$table->removeColumn('sort_order');
}
if ($table->hasColumn('notes')) {
$table->removeColumn('notes');
}
if ($table->hasColumn('email')) {
$table->removeColumn('email');
}
if ($table->hasColumn('phone')) {
$table->removeColumn('phone');
}
if ($table->hasColumn('person_full_name')) {
$table->removeColumn('person_full_name');
}
$table->update();
}
}
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateCadAreaJobsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('cad_area_jobs');
$table
->addColumn('iduser', 'integer', [
'null' => true,
'signed' => false,
'limit' => 10,
])
->addColumn('original_filename', 'string', [
'limit' => 255,
'null' => false,
])
->addColumn('stored_filename', 'string', [
'limit' => 255,
'null' => false,
])
->addColumn('file_path', 'string', [
'limit' => 500,
'null' => false,
])
->addColumn('file_url', 'string', [
'limit' => 500,
'null' => true,
])
->addColumn('file_size', 'integer', [
'null' => true,
'signed' => false,
])
->addColumn('status', 'enum', [
'values' => [
'uploaded',
'processing',
'completed',
'error',
],
'default' => 'uploaded',
'null' => false,
])
->addColumn('area_mm2', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
])
->addColumn('area_cm2', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
])
->addColumn('area_m2', 'decimal', [
'precision' => 18,
'scale' => 9,
'null' => true,
])
->addColumn('scale_detected', 'string', [
'limit' => 50,
'null' => true,
])
->addColumn('confidence', 'string', [
'limit' => 50,
'null' => true,
])
->addColumn('message', 'text', [
'null' => true,
])
->addColumn('python_response', 'text', [
'null' => true,
])
->addColumn('created_at', 'timestamp', [
'default' => 'CURRENT_TIMESTAMP',
'null' => true,
])
->addColumn('updated_at', 'timestamp', [
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
'null' => true,
])
->addIndex(['iduser'], [
'name' => 'idx_cad_area_jobs_iduser',
])
->addIndex(['status'], [
'name' => 'idx_cad_area_jobs_status',
])
->create();
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddNotifyFunctionToScadDeadlines extends AbstractMigration
{
public function up(): void
{
if (!$this->hasTable('scad_deadlines')) {
throw new RuntimeException('Table scad_deadlines does not exist.');
}
$table = $this->table('scad_deadlines');
if (!$table->hasColumn('notify_function')) {
$table
->addColumn('notify_function', 'boolean', [
'null' => false,
'default' => false,
'after' => 'function_id',
'comment' => 'Send deadline reminder also to the linked function email',
])
->update();
}
}
public function down(): void
{
if (!$this->hasTable('scad_deadlines')) {
return;
}
$table = $this->table('scad_deadlines');
if ($table->hasColumn('notify_function')) {
$table
->removeColumn('notify_function')
->update();
}
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddRoiFieldsToCadAreaJobsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('cad_area_jobs');
if (!$table->hasColumn('roi_x')) {
$table->addColumn('roi_x', 'decimal', [
'precision' => 12,
'scale' => 6,
'null' => true,
'after' => 'file_size',
]);
}
if (!$table->hasColumn('roi_y')) {
$table->addColumn('roi_y', 'decimal', [
'precision' => 12,
'scale' => 6,
'null' => true,
'after' => 'roi_x',
]);
}
if (!$table->hasColumn('roi_width')) {
$table->addColumn('roi_width', 'decimal', [
'precision' => 12,
'scale' => 6,
'null' => true,
'after' => 'roi_y',
]);
}
if (!$table->hasColumn('roi_height')) {
$table->addColumn('roi_height', 'decimal', [
'precision' => 12,
'scale' => 6,
'null' => true,
'after' => 'roi_width',
]);
}
if (!$table->hasColumn('roi_page')) {
$table->addColumn('roi_page', 'integer', [
'null' => true,
'default' => 1,
'after' => 'roi_height',
]);
}
if (!$table->hasColumn('calculation_mode')) {
$table->addColumn('calculation_mode', 'string', [
'limit' => 50,
'null' => true,
'default' => 'auto_roi',
'after' => 'roi_page',
]);
}
$table->update();
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddResultDetailFieldsToCadAreaJobsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('cad_area_jobs');
if (!$table->hasColumn('width_mm')) {
$table->addColumn('width_mm', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('height_mm')) {
$table->addColumn('height_mm', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('scale_used')) {
$table->addColumn('scale_used', 'decimal', [
'precision' => 12,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('strategy_used')) {
$table->addColumn('strategy_used', 'string', [
'limit' => 100,
'null' => true,
]);
}
$table->update();
}
}
@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddManualTracingFieldsToCadAreaJobsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('cad_area_jobs');
if (!$table->hasColumn('width_mm')) {
$table->addColumn('width_mm', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('height_mm')) {
$table->addColumn('height_mm', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('scale_used')) {
$table->addColumn('scale_used', 'decimal', [
'precision' => 12,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('strategy_used')) {
$table->addColumn('strategy_used', 'string', [
'limit' => 100,
'null' => true,
]);
}
if (!$table->hasColumn('manual_calibration_px')) {
$table->addColumn('manual_calibration_px', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('manual_calibration_mm')) {
$table->addColumn('manual_calibration_mm', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('manual_mm_per_px')) {
$table->addColumn('manual_mm_per_px', 'decimal', [
'precision' => 18,
'scale' => 10,
'null' => true,
]);
}
if (!$table->hasColumn('manual_polygon_json')) {
$table->addColumn('manual_polygon_json', 'text', [
'null' => true,
]);
}
if (!$table->hasColumn('manual_area_mm2')) {
$table->addColumn('manual_area_mm2', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('manual_area_cm2')) {
$table->addColumn('manual_area_cm2', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('manual_width_mm')) {
$table->addColumn('manual_width_mm', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('manual_height_mm')) {
$table->addColumn('manual_height_mm', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('manual_status')) {
$table->addColumn('manual_status', 'string', [
'limit' => 50,
'null' => true,
]);
}
$table->update();
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddManualHoleFieldsToCadAreaJobsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('cad_area_jobs');
if (!$table->hasColumn('manual_outer_area_mm2')) {
$table->addColumn('manual_outer_area_mm2', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('manual_holes_area_mm2')) {
$table->addColumn('manual_holes_area_mm2', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
]);
}
if (!$table->hasColumn('manual_holes_json')) {
$table->addColumn('manual_holes_json', 'text', [
'null' => true,
]);
}
$table->update();
}
}
+246
View File
@@ -0,0 +1,246 @@
# 1. Database migration
```mysql
ALTER TABLE employees
ADD COLUMN address varchar(500) DEFAULT NULL AFTER last_name,
ADD COLUMN phone varchar(255) DEFAULT NULL AFTER address,
ADD COLUMN email varchar(255) DEFAULT NULL AFTER phone,
ADD COLUMN job_role_id int(10) UNSIGNED DEFAULT NULL AFTER department_id;
-- Replace ENUM status with plain VARCHAR for easier maintenance.
ALTER TABLE employees
MODIFY status varchar(255) NOT NULL DEFAULT 'active';
CREATE TABLE IF NOT EXISTS job_roles (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
description text DEFAULT NULL,
sort_order int(10) UNSIGNED NOT NULL DEFAULT 999,
is_active tinyint(1) NOT NULL DEFAULT 1,
created_at timestamp NULL DEFAULT current_timestamp(),
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (id),
UNIQUE KEY uniq_job_roles_name (name),
KEY idx_job_roles_active (is_active),
KEY idx_job_roles_sort_order (sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
ALTER TABLE employees
ADD KEY idx_employees_job_role_id (job_role_id);
ALTER TABLE employees
ADD CONSTRAINT fk_employees_job_role
FOREIGN KEY (job_role_id) REFERENCES job_roles (id)
ON DELETE SET NULL
ON UPDATE CASCADE;
-- 1) Seed job_roles with every distinct non-empty value of employees.position.
INSERT IGNORE INTO job_roles (name, is_active, sort_order, created_at, updated_at)
SELECT DISTINCT TRIM(position), 1, 999, NOW(), NOW()
FROM employees
WHERE position IS NOT NULL AND TRIM(position) <> '';
-- 2) Backfill employees.job_role_id by matching position text to job_roles.name.
UPDATE employees e
JOIN job_roles jr ON jr.name = TRIM(e.position)
SET e.job_role_id = jr.id
WHERE e.position IS NOT NULL AND TRIM(e.position) <> '';
-- 3) Drop the legacy column.
ALTER TABLE employees DROP COLUMN position;
CREATE TABLE IF NOT EXISTS training_topics (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
description text DEFAULT NULL,
default_frequency_months int(10) UNSIGNED DEFAULT NULL,
default_reminder_days int(10) UNSIGNED NOT NULL DEFAULT 30,
sort_order int(10) UNSIGNED NOT NULL DEFAULT 999,
is_active tinyint(1) NOT NULL DEFAULT 1,
is_mandatory tinyint(1) NOT NULL DEFAULT 0,
created_at timestamp NULL DEFAULT current_timestamp(),
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (id),
UNIQUE KEY uniq_training_topics_name (name),
KEY idx_training_topics_active (is_active),
KEY idx_training_topics_mandatory (is_mandatory),
KEY idx_training_topics_sort_order (sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS employee_documents (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
employee_id int(10) UNSIGNED NOT NULL,
category varchar(255) NOT NULL DEFAULT 'other',
original_name varchar(500) NOT NULL,
stored_name varchar(500) NOT NULL,
mime_type varchar(255) DEFAULT NULL,
size int(10) UNSIGNED DEFAULT NULL,
notes text DEFAULT NULL,
uploaded_by int(10) UNSIGNED DEFAULT NULL,
created_at timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (id),
KEY idx_employee_documents_employee (employee_id),
KEY idx_employee_documents_category (category),
KEY idx_employee_documents_uploaded_by (uploaded_by),
CONSTRAINT fk_employee_documents_employee
FOREIGN KEY (employee_id) REFERENCES employees (id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_employee_documents_uploaded_by
FOREIGN KEY (uploaded_by) REFERENCES auth_users (id)
ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS employee_ppe (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
employee_id int(10) UNSIGNED NOT NULL,
item_name varchar(255) NOT NULL,
delivery_date date DEFAULT NULL,
delivered_by varchar(255) DEFAULT NULL,
notes text DEFAULT NULL,
created_by int(10) UNSIGNED DEFAULT NULL,
created_at timestamp NULL DEFAULT current_timestamp(),
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (id),
KEY idx_employee_ppe_employee (employee_id),
KEY idx_employee_ppe_delivery_date (delivery_date),
CONSTRAINT fk_employee_ppe_employee
FOREIGN KEY (employee_id) REFERENCES employees (id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_employee_ppe_created_by
FOREIGN KEY (created_by) REFERENCES auth_users (id)
ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS employee_trainings (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
employee_id int(10) UNSIGNED NOT NULL,
training_topic_id int(10) UNSIGNED NOT NULL,
completed_date date NOT NULL,
delivered_by varchar(255) DEFAULT NULL,
description text DEFAULT NULL,
training_type varchar(255) NOT NULL DEFAULT 'initial',
update_frequency_months int(10) UNSIGNED DEFAULT NULL,
reminder_days int(10) UNSIGNED DEFAULT NULL,
next_due_date date DEFAULT NULL,
created_by int(10) UNSIGNED DEFAULT NULL,
created_at timestamp NULL DEFAULT current_timestamp(),
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (id),
KEY idx_employee_trainings_employee (employee_id),
KEY idx_employee_trainings_topic (training_topic_id),
KEY idx_employee_trainings_next_due (next_due_date),
KEY idx_employee_trainings_employee_topic (employee_id, training_topic_id),
KEY idx_employee_trainings_created_by (created_by),
CONSTRAINT fk_employee_trainings_employee
FOREIGN KEY (employee_id) REFERENCES employees (id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_employee_trainings_topic
FOREIGN KEY (training_topic_id) REFERENCES training_topics (id)
ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT fk_employee_trainings_created_by
FOREIGN KEY (created_by) REFERENCES auth_users (id)
ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS employee_training_attachments (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
training_id int(10) UNSIGNED NOT NULL,
original_name varchar(500) NOT NULL,
stored_name varchar(500) NOT NULL,
mime_type varchar(255) DEFAULT NULL,
size int(10) UNSIGNED DEFAULT NULL,
uploaded_by int(10) UNSIGNED DEFAULT NULL,
created_at timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (id),
KEY idx_employee_training_attachments_training (training_id),
KEY idx_employee_training_attachments_uploaded_by (uploaded_by),
CONSTRAINT fk_employee_training_attachments_training
FOREIGN KEY (training_id) REFERENCES employee_trainings (id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_employee_training_attachments_uploaded_by
FOREIGN KEY (uploaded_by) REFERENCES auth_users (id)
ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS employee_training_log (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
employee_id int(10) UNSIGNED DEFAULT NULL,
training_id int(10) UNSIGNED DEFAULT NULL,
action varchar(255) NOT NULL,
field varchar(255) DEFAULT NULL,
old_value text DEFAULT NULL,
new_value text DEFAULT NULL,
changed_by int(10) UNSIGNED DEFAULT NULL,
changed_at timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (id),
KEY idx_employee_training_log_employee (employee_id),
KEY idx_employee_training_log_training (training_id),
KEY idx_employee_training_log_changed_at (changed_at),
CONSTRAINT fk_employee_training_log_employee
FOREIGN KEY (employee_id) REFERENCES employees (id)
ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT fk_employee_training_log_training
FOREIGN KEY (training_id) REFERENCES employee_trainings (id)
ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT fk_employee_training_log_changed_by
FOREIGN KEY (changed_by) REFERENCES auth_users (id)
ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO auth_roles (name, display_name, description, removable, created_at, updated_at) VALUES
('employee', 'Employee', 'Read-only access to own employee profile.', 1, NOW(), NOW()),
('employee-hr', 'HR Manager', 'Can manage employee profiles, documents, PPE and training records.', 1, NOW(), NOW()),
('manager', 'Manager', 'Same permissions as HR Manager.', 1, NOW(), NOW())
ON DUPLICATE KEY UPDATE
display_name = VALUES(display_name),
description = VALUES(description),
updated_at = NOW();
CREATE TABLE IF NOT EXISTS training_reminder_log (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
training_id int(10) UNSIGNED DEFAULT NULL,
employee_id int(10) UNSIGNED DEFAULT NULL,
training_topic_id int(10) UNSIGNED DEFAULT NULL,
addressee_email varchar(255) NOT NULL,
next_due_date date DEFAULT NULL,
status_at_send varchar(255) NOT NULL,
sent_at timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (id),
KEY idx_training_reminder_log_dedup (training_id, addressee_email, next_due_date),
KEY idx_training_reminder_log_dedup_missing (employee_id, training_topic_id, addressee_email),
KEY idx_training_reminder_log_sent_at (sent_at),
CONSTRAINT fk_training_reminder_log_training
FOREIGN KEY (training_id) REFERENCES employee_trainings (id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_training_reminder_log_employee
FOREIGN KEY (employee_id) REFERENCES employees (id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_training_reminder_log_topic
FOREIGN KEY (training_topic_id) REFERENCES training_topics (id)
ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
# 2. Upload storage folder
Create the storage directory with the correct permissions for the web server:
```bash
mkdir -p /var/www/zibo-dashboard/public/userarea/files/employees
chown -R www-data:www-data /var/www/zibo-dashboard/public/userarea/files
chmod -R 775 /var/www/zibo-dashboard/public/userarea/files
```
Uploaded files will be organized as:
```
files/employees/{employee_id}/documents/ # File Repository (HR)
files/employees/{employee_id}/trainings/ # Training certificates
```
# 3. Cron for automated emails
```cron
0 7 * * * /usr/bin/php /var/www/zibo-dashboard/public/userarea/cron/send_training_reminders.php \
>> /var/www/zibo-dashboard/storage/logs/training_reminders.log 2>&1
```
+33
View File
@@ -0,0 +1,33 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
if (file_exists(__DIR__ . '/.env')) {
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->safeLoad();
}
return [
'paths' => [
'migrations' => __DIR__ . '/db/migrations',
'seeds' => __DIR__ . '/db/seeds',
],
'environments' => [
'default_migration_table' => 'phinxlog',
'default_environment' => 'development',
'development' => [
'adapter' => $_ENV['DB_CONNECTION'] ?? 'mysql',
'host' => $_ENV['DB_HOST'] ?? 'localhost',
'name' => $_ENV['DB_DATABASE'] ?? '',
'user' => $_ENV['DB_USERNAME'] ?? '',
'pass' => $_ENV['DB_PASSWORD'] ?? '',
'port' => $_ENV['DB_PORT'] ?? 3306,
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
],
],
'version_order' => 'creation',
];
+18
View File
@@ -0,0 +1,18 @@
<?php
/**
* Auth check for AJAX endpoints under /userarea/ajax/.
* Include this at the top of every ajax handler.
* Sets $currentUserId from session or returns 401 JSON.
*/
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (empty($_SESSION['iduserlogin'])) {
header('Content-Type: application/json');
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'Non autorizzato. Effettua il login.']);
exit;
}
$currentUserId = (int)$_SESSION['iduserlogin'];
@@ -0,0 +1,40 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'ID documento non valido.']);
exit;
}
$stmt = $pdo->prepare("SELECT employee_id, stored_name FROM employee_documents WHERE id = :id LIMIT 1");
$stmt->execute(['id' => $id]);
$doc = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$doc) {
echo json_encode(['success' => false, 'message' => 'Documento non trovato.']);
exit;
}
try {
$del = $pdo->prepare("DELETE FROM employee_documents WHERE id = :id");
$del->execute(['id' => $id]);
$path = __DIR__ . '/../../files/employees/' . (int)$doc['employee_id'] . '/documents/' . $doc['stored_name'];
if (is_file($path)) {
@unlink($path);
}
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,38 @@
<?php
include('../../include/headscript.php');
header('Content-Type: application/json; charset=utf-8');
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode([
'success' => false,
'message' => 'ID DPI non valido.'
]);
exit;
}
$stmt = $pdo->prepare("
UPDATE employee_ppe_items
SET status = 'returned',
updated_at = NOW()
WHERE id = ?
");
$stmt->execute([$id]);
echo json_encode([
'success' => true,
'message' => 'DPI rimosso correttamente.'
]);
exit;
} catch (Throwable $e) {
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
exit;
}
@@ -0,0 +1,60 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
exit;
}
try {
$pdo->beginTransaction();
$row = $pdo->prepare("SELECT employee_id FROM employee_trainings WHERE id = :id");
$row->execute(['id' => $id]);
$tr = $row->fetch(PDO::FETCH_ASSOC);
if (!$tr) {
$pdo->rollBack();
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
exit;
}
// Collect attached files BEFORE deletion so we can unlink them after
$files = $pdo->prepare("SELECT stored_name FROM employee_training_attachments WHERE training_id = :id");
$files->execute(['id' => $id]);
$stored = $files->fetchAll(PDO::FETCH_COLUMN);
// Log BEFORE delete (FK on log allows SET NULL on training delete but we want a clean record)
$pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, NULL, 'deleted', NULL, NULL, NULL, :cb, NOW())
")->execute(['eid' => $tr['employee_id'], 'cb' => $currentUserId]);
$pdo->prepare("DELETE FROM employee_trainings WHERE id = :id")->execute(['id' => $id]);
$pdo->commit();
foreach ($stored as $name) {
$path = __DIR__ . '/../../files/employees/' . (int)$tr['employee_id'] . '/trainings/' . $name;
if (is_file($path)) {
@unlink($path);
}
}
echo json_encode(['success' => true]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,59 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'ID allegato non valido.']);
exit;
}
$row = $pdo->prepare("
SELECT a.stored_name, a.original_name, a.training_id, t.employee_id
FROM employee_training_attachments a
JOIN employee_trainings t ON t.id = a.training_id
WHERE a.id = :id
LIMIT 1
");
$row->execute(['id' => $id]);
$att = $row->fetch(PDO::FETCH_ASSOC);
if (!$att) {
echo json_encode(['success' => false, 'message' => 'Allegato non trovato.']);
exit;
}
try {
$pdo->beginTransaction();
$pdo->prepare("DELETE FROM employee_training_attachments WHERE id = :id")->execute(['id' => $id]);
$pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, :tid, 'attachment_deleted', 'attachment', :name, NULL, :cb, NOW())
")->execute([
'eid' => $att['employee_id'],
'tid' => $att['training_id'],
'name' => $att['original_name'],
'cb' => $currentUserId,
]);
$pdo->commit();
$path = __DIR__ . '/../../files/employees/' . (int)$att['employee_id'] . '/trainings/' . $att['stored_name'];
if (is_file($path)) {
@unlink($path);
}
echo json_encode(['success' => true]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,57 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) {
http_response_code(400);
exit('ID non valido.');
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->prepare("
SELECT d.*, e.auth_user_id
FROM employee_documents d
JOIN employees e ON e.id = d.employee_id
WHERE d.id = :id
LIMIT 1
");
$stmt->execute(['id' => $id]);
$doc = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$doc) {
http_response_code(404);
exit('Documento non trovato.');
}
/* Access check: HR roles can download any; otherwise only own employee */
$roleStmt = $pdo->prepare("
SELECT r.name
FROM auth_users u
LEFT JOIN auth_roles r ON r.id = u.role_id
WHERE u.id = :id LIMIT 1
");
$roleStmt->execute(['id' => $currentUserId]);
$role = (string)$roleStmt->fetchColumn();
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
$isHr = in_array($role, $hrRoles, true);
if (!$isHr && (int)$doc['auth_user_id'] !== $currentUserId) {
http_response_code(403);
exit('Accesso negato.');
}
$path = __DIR__ . '/../../files/employees/' . (int)$doc['employee_id'] . '/documents/' . $doc['stored_name'];
if (!is_file($path)) {
http_response_code(404);
exit('File non trovato sul server.');
}
while (ob_get_level() > 0) { ob_end_clean(); }
header('Content-Type: ' . (!empty($doc['mime_type']) ? $doc['mime_type'] : 'application/octet-stream'));
header('Content-Disposition: attachment; filename="' . rawurlencode($doc['original_name']) . '"');
header('Content-Length: ' . filesize($path));
header('Cache-Control: private, max-age=0, must-revalidate');
readfile($path);
exit;
@@ -0,0 +1,56 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) {
http_response_code(400);
exit('ID non valido.');
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->prepare("
SELECT a.*, t.employee_id, e.auth_user_id
FROM employee_training_attachments a
JOIN employee_trainings t ON t.id = a.training_id
JOIN employees e ON e.id = t.employee_id
WHERE a.id = :id
LIMIT 1
");
$stmt->execute(['id' => $id]);
$att = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$att) {
http_response_code(404);
exit('Allegato non trovato.');
}
/* Access: HR or owning employee */
$roleStmt = $pdo->prepare("
SELECT r.name FROM auth_users u
LEFT JOIN auth_roles r ON r.id = u.role_id
WHERE u.id = :id LIMIT 1
");
$roleStmt->execute(['id' => $currentUserId]);
$role = (string)$roleStmt->fetchColumn();
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
$isHr = in_array($role, $hrRoles, true);
if (!$isHr && (int)$att['auth_user_id'] !== $currentUserId) {
http_response_code(403);
exit('Accesso negato.');
}
$path = __DIR__ . '/../../files/employees/' . (int)$att['employee_id'] . '/trainings/' . $att['stored_name'];
if (!is_file($path)) {
http_response_code(404);
exit('File non trovato sul server.');
}
while (ob_get_level() > 0) { ob_end_clean(); }
header('Content-Type: ' . (!empty($att['mime_type']) ? $att['mime_type'] : 'application/octet-stream'));
header('Content-Disposition: attachment; filename="' . rawurlencode($att['original_name']) . '"');
header('Content-Length: ' . filesize($path));
header('Cache-Control: private, max-age=0, must-revalidate');
readfile($path);
exit;
@@ -0,0 +1,58 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
header('Content-Type: application/json');
$trainingId = (int)($_GET['training_id'] ?? 0);
if ($trainingId <= 0) {
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
/* Access: HR or owner */
$ownerStmt = $pdo->prepare("
SELECT e.auth_user_id
FROM employee_trainings t
JOIN employees e ON e.id = t.employee_id
WHERE t.id = :id LIMIT 1
");
$ownerStmt->execute(['id' => $trainingId]);
$ownerAuthUserId = $ownerStmt->fetchColumn();
if ($ownerAuthUserId === false) {
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
exit;
}
$roleStmt = $pdo->prepare("
SELECT r.name FROM auth_users u
LEFT JOIN auth_roles r ON r.id = u.role_id
WHERE u.id = :id LIMIT 1
");
$roleStmt->execute(['id' => $currentUserId]);
$role = (string)$roleStmt->fetchColumn();
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
$isHr = in_array($role, $hrRoles, true);
if (!$isHr && (int)$ownerAuthUserId !== $currentUserId) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'Accesso negato.']);
exit;
}
$stmt = $pdo->prepare("
SELECT id, original_name, mime_type, size, created_at
FROM employee_training_attachments
WHERE training_id = :tid
ORDER BY created_at DESC
");
$stmt->execute(['tid' => $trainingId]);
$attachments = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'attachments' => $attachments,
'can_edit' => $isHr,
]);
@@ -0,0 +1,57 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
header('Content-Type: application/json');
$trainingId = (int)($_GET['training_id'] ?? 0);
if ($trainingId <= 0) {
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
/* Access: HR or owner */
$ownerStmt = $pdo->prepare("
SELECT e.auth_user_id
FROM employee_trainings t
JOIN employees e ON e.id = t.employee_id
WHERE t.id = :id LIMIT 1
");
$ownerStmt->execute(['id' => $trainingId]);
$ownerAuthUserId = $ownerStmt->fetchColumn();
if ($ownerAuthUserId === false) {
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
exit;
}
$roleStmt = $pdo->prepare("
SELECT r.name FROM auth_users u
LEFT JOIN auth_roles r ON r.id = u.role_id
WHERE u.id = :id LIMIT 1
");
$roleStmt->execute(['id' => $currentUserId]);
$role = (string)$roleStmt->fetchColumn();
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
$isHr = in_array($role, $hrRoles, true);
if (!$isHr && (int)$ownerAuthUserId !== $currentUserId) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'Accesso negato.']);
exit;
}
$stmt = $pdo->prepare("
SELECT l.id, l.action, l.field, l.old_value, l.new_value, l.changed_at,
TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS changed_by_name,
u.email AS changed_by_email
FROM employee_training_log l
LEFT JOIN auth_users u ON u.id = l.changed_by
WHERE l.training_id = :tid
ORDER BY l.changed_at DESC, l.id DESC
");
$stmt->execute(['tid' => $trainingId]);
$entries = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'entries' => $entries]);
@@ -0,0 +1,86 @@
<?php
/**
* Bulk-assign a single DPI (PPE) item to several employees at once:
* one employee_ppe row per selected employee, all sharing the same
* item name / delivery date / delivered-by / notes.
* Mirrors ajax/trainings/save_bulk_training.php. HR-only.
*/
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
// $pdo and $currentUserId from hr_auth_check.php
$itemName = trim($_POST['item_name'] ?? '');
$deliveryDate = trim($_POST['delivery_date'] ?? '');
$deliveredBy = trim($_POST['delivered_by'] ?? '');
$notes = trim($_POST['notes'] ?? '');
$employeeIds = $_POST['employee_ids'] ?? [];
if (!is_array($employeeIds)) {
$employeeIds = [];
}
$employeeIds = array_values(array_unique(array_filter(array_map('intval', $employeeIds), fn($v) => $v > 0)));
if ($itemName === '') {
echo json_encode(['success' => false, 'message' => 'Il nome del DPI è obbligatorio.']);
exit;
}
if ($deliveryDate !== '' && !DateTime::createFromFormat('Y-m-d', $deliveryDate)) {
echo json_encode(['success' => false, 'message' => 'Data di consegna non valida.']);
exit;
}
if (empty($employeeIds)) {
echo json_encode(['success' => false, 'message' => 'Selezionare almeno un dipendente.']);
exit;
}
$deliveryDate = $deliveryDate === '' ? null : $deliveryDate;
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
$notes = $notes !== '' ? $notes : null;
try {
$pdo->beginTransaction();
// Only insert for employees that actually exist
$checkEmp = $pdo->prepare("SELECT id FROM employees WHERE id = :id");
$ins = $pdo->prepare("
INSERT INTO employee_ppe
(employee_id, item_name, delivery_date, delivered_by, notes, created_by, created_at, updated_at)
VALUES
(:employee_id, :item_name, :delivery_date, :delivered_by, :notes, :created_by, NOW(), NOW())
");
$created = 0;
foreach ($employeeIds as $eid) {
$checkEmp->execute(['id' => $eid]);
if (!$checkEmp->fetchColumn()) {
continue;
}
$ins->execute([
'employee_id' => $eid,
'item_name' => $itemName,
'delivery_date' => $deliveryDate,
'delivered_by' => $deliveredBy,
'notes' => $notes,
'created_by' => $currentUserId,
]);
$created++;
}
$pdo->commit();
echo json_encode([
'success' => true,
'created' => $created,
'message' => 'DPI assegnato a ' . $created . ' dipendent' . ($created === 1 ? 'e' : 'i') . '.',
]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,116 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$employeeId = (int)($_POST['employee_id'] ?? 0);
$firstName = trim($_POST['first_name'] ?? '');
$lastName = trim($_POST['last_name'] ?? '');
$employeeCode = trim($_POST['employee_code'] ?? '');
$address = trim($_POST['address'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$email = trim($_POST['email'] ?? '');
$hireDate = trim($_POST['hire_date'] ?? '');
$departmentId = $_POST['department_id'] ?? '';
$jobRoleId = $_POST['job_role_id'] ?? '';
$status = trim($_POST['status'] ?? '');
$authUserId = $_POST['auth_user_id'] ?? '';
$roleId = $_POST['role_id'] ?? '';
if ($employeeId <= 0) {
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
exit;
}
if ($firstName === '' || $lastName === '') {
echo json_encode(['success' => false, 'message' => 'Nome e cognome sono obbligatori.']);
exit;
}
$allowedStatus = ['active', 'inactive', 'suspended'];
if (!in_array($status, $allowedStatus, true)) {
$status = 'active';
}
$departmentId = ($departmentId === '' || $departmentId === null) ? null : (int)$departmentId;
$jobRoleId = ($jobRoleId === '' || $jobRoleId === null) ? null : (int)$jobRoleId;
$authUserId = ($authUserId === '' || $authUserId === null) ? null : (int)$authUserId;
$roleId = ($roleId === '' || $roleId === null) ? null : (int)$roleId;
$hireDate = $hireDate === '' ? null : $hireDate;
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo json_encode(['success' => false, 'message' => 'Email non valida.']);
exit;
}
if ($employeeCode !== '') {
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE employee_code = :code AND id <> :id");
$check->execute(['code' => $employeeCode, 'id' => $employeeId]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode(['success' => false, 'message' => 'Codice dipendente già in uso.']);
exit;
}
}
if ($authUserId !== null) {
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE auth_user_id = :uid AND id <> :id");
$check->execute(['uid' => $authUserId, 'id' => $employeeId]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode(['success' => false, 'message' => 'Questo utente è già associato ad un altro dipendente.']);
exit;
}
}
try {
$stmt = $pdo->prepare("
UPDATE employees
SET first_name = :first_name,
last_name = :last_name,
employee_code = :employee_code,
address = :address,
phone = :phone,
email = :email,
hire_date = :hire_date,
department_id = :department_id,
job_role_id = :job_role_id,
status = :status,
auth_user_id = :auth_user_id,
updated_at = NOW()
WHERE id = :id
");
$stmt->execute([
'first_name' => $firstName,
'last_name' => $lastName,
'employee_code' => $employeeCode !== '' ? $employeeCode : null,
'address' => $address !== '' ? $address : null,
'phone' => $phone !== '' ? $phone : null,
'email' => $email !== '' ? $email : null,
'hire_date' => $hireDate,
'department_id' => $departmentId,
'job_role_id' => $jobRoleId,
'status' => $status,
'auth_user_id' => $authUserId,
'id' => $employeeId,
]);
// Optionally update Vanguard role for the linked auth_user
if ($authUserId !== null && $roleId !== null) {
$check = $pdo->prepare("SELECT COUNT(*) FROM auth_roles WHERE id = ?");
$check->execute([$roleId]);
if ((int)$check->fetchColumn() > 0) {
$upd = $pdo->prepare("UPDATE auth_users SET role_id = :role_id, updated_at = NOW() WHERE id = :uid");
$upd->execute(['role_id' => $roleId, 'uid' => $authUserId]);
}
}
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,153 @@
<?php
include('../../include/headscript.php');
header('Content-Type: application/json; charset=utf-8');
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = isset($_POST['id']) && $_POST['id'] !== '' ? (int)$_POST['id'] : null;
$employeeId = (int)($_POST['employee_id'] ?? 0);
$ppeItemId = (int)($_POST['ppe_item_id'] ?? 0);
$assignedDate = trim($_POST['assigned_date'] ?? '');
$expiryDate = trim($_POST['expiry_date'] ?? '');
$deliveredBy = trim($_POST['delivered_by'] ?? '');
$status = trim($_POST['status'] ?? 'assigned');
$notes = trim($_POST['notes'] ?? '');
$allowedStatuses = [
'assigned',
'returned',
'expired',
'lost',
'damaged',
];
if ($employeeId <= 0) {
echo json_encode([
'success' => false,
'message' => 'Dipendente non valido.'
]);
exit;
}
if ($ppeItemId <= 0) {
echo json_encode([
'success' => false,
'message' => 'Selezionare un DPI.'
]);
exit;
}
if (!in_array($status, $allowedStatuses, true)) {
$status = 'assigned';
}
$checkEmployee = $pdo->prepare("SELECT id FROM employees WHERE id = ? LIMIT 1");
$checkEmployee->execute([$employeeId]);
if (!$checkEmployee->fetchColumn()) {
echo json_encode([
'success' => false,
'message' => 'Dipendente non trovato.'
]);
exit;
}
$checkPpe = $pdo->prepare("SELECT id FROM ppe_items WHERE id = ? LIMIT 1");
$checkPpe->execute([$ppeItemId]);
if (!$checkPpe->fetchColumn()) {
echo json_encode([
'success' => false,
'message' => 'DPI non trovato.'
]);
exit;
}
if ($id) {
$stmt = $pdo->prepare("
UPDATE employee_ppe_items
SET ppe_item_id = :ppe_item_id,
assigned_date = :assigned_date,
expiry_date = :expiry_date,
delivered_by = :delivered_by,
status = :status,
notes = :notes,
updated_at = NOW()
WHERE id = :id
AND employee_id = :employee_id
");
$stmt->execute([
'ppe_item_id' => $ppeItemId,
'assigned_date' => $assignedDate !== '' ? $assignedDate : null,
'expiry_date' => $expiryDate !== '' ? $expiryDate : null,
'delivered_by' => $deliveredBy !== '' ? $deliveredBy : null,
'status' => $status,
'notes' => $notes !== '' ? $notes : null,
'id' => $id,
'employee_id' => $employeeId,
]);
echo json_encode([
'success' => true,
'message' => 'DPI aggiornato.'
]);
exit;
}
$stmt = $pdo->prepare("
INSERT INTO employee_ppe_items
(
employee_id,
ppe_item_id,
assigned_date,
expiry_date,
delivered_by,
quantity,
status,
notes,
created_by,
created_at,
updated_at
)
VALUES
(
:employee_id,
:ppe_item_id,
:assigned_date,
:expiry_date,
:delivered_by,
1,
:status,
:notes,
:created_by,
NOW(),
NOW()
)
");
$stmt->execute([
'employee_id' => $employeeId,
'ppe_item_id' => $ppeItemId,
'assigned_date' => $assignedDate !== '' ? $assignedDate : null,
'expiry_date' => $expiryDate !== '' ? $expiryDate : null,
'delivered_by' => $deliveredBy !== '' ? $deliveredBy : null,
'status' => $status,
'notes' => $notes !== '' ? $notes : null,
'created_by' => isset($iduserlogin) ? (int)$iduserlogin : null,
]);
echo json_encode([
'success' => true,
'message' => 'DPI assegnato.'
]);
exit;
} catch (Throwable $e) {
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
exit;
}
@@ -0,0 +1,177 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
$employeeId = (int)($_POST['employee_id'] ?? 0);
$topicId = (int)($_POST['training_topic_id'] ?? 0);
$completedDate = trim($_POST['completed_date'] ?? '');
$deliveredBy = trim($_POST['delivered_by'] ?? '');
$description = trim($_POST['description'] ?? '');
$trainingType = trim($_POST['training_type'] ?? 'initial');
$freqRaw = $_POST['update_frequency_months'] ?? '';
$remRaw = $_POST['reminder_days'] ?? '';
if ($employeeId <= 0) {
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
exit;
}
if ($topicId <= 0) {
echo json_encode(['success' => false, 'message' => 'Selezionare un corso.']);
exit;
}
if ($completedDate === '') {
echo json_encode(['success' => false, 'message' => 'La data di completamento è obbligatoria.']);
exit;
}
if (!in_array($trainingType, ['initial', 'refresher'], true)) {
$trainingType = 'initial';
}
$topicStmt = $pdo->prepare("SELECT default_frequency_months, default_reminder_days FROM training_topics WHERE id = :id");
$topicStmt->execute(['id' => $topicId]);
$topic = $topicStmt->fetch(PDO::FETCH_ASSOC);
if (!$topic) {
echo json_encode(['success' => false, 'message' => 'Corso non trovato.']);
exit;
}
$freq = ($freqRaw === '' || $freqRaw === null) ? null : max(0, (int)$freqRaw);
$rem = ($remRaw === '' || $remRaw === null) ? null : max(0, (int)$remRaw);
/* Effective frequency for next_due_date: explicit override or topic default */
$effFreq = $freq !== null ? $freq : ($topic['default_frequency_months'] !== null ? (int)$topic['default_frequency_months'] : null);
$nextDue = null;
if ($effFreq !== null && $effFreq > 0) {
$d = DateTime::createFromFormat('Y-m-d', $completedDate);
if ($d) {
$d->modify('+' . (int)$effFreq . ' months');
$nextDue = $d->format('Y-m-d');
}
}
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
$description = $description !== '' ? $description : null;
try {
$pdo->beginTransaction();
if ($id > 0) {
$old = $pdo->prepare("SELECT * FROM employee_trainings WHERE id = :id");
$old->execute(['id' => $id]);
$oldRow = $old->fetch(PDO::FETCH_ASSOC);
if (!$oldRow) {
$pdo->rollBack();
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
exit;
}
$upd = $pdo->prepare("
UPDATE employee_trainings
SET training_topic_id = :topic_id,
completed_date = :completed_date,
delivered_by = :delivered_by,
description = :description,
training_type = :training_type,
update_frequency_months = :freq,
reminder_days = :rem,
next_due_date = :next_due,
updated_at = NOW()
WHERE id = :id
");
$upd->execute([
'topic_id' => $topicId,
'completed_date' => $completedDate,
'delivered_by' => $deliveredBy,
'description' => $description,
'training_type' => $trainingType,
'freq' => $freq,
'rem' => $rem,
'next_due' => $nextDue,
'id' => $id,
]);
$fields = [
'training_topic_id' => $topicId,
'completed_date' => $completedDate,
'delivered_by' => $deliveredBy,
'description' => $description,
'training_type' => $trainingType,
'update_frequency_months' => $freq,
'reminder_days' => $rem,
'next_due_date' => $nextDue,
];
$logStmt = $pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, :tid, 'updated', :field, :old_v, :new_v, :cb, NOW())
");
foreach ($fields as $f => $newV) {
$oldV = $oldRow[$f] ?? null;
if ((string)$oldV !== (string)$newV) {
$logStmt->execute([
'eid' => $employeeId,
'tid' => $id,
'field' => $f,
'old_v' => $oldV,
'new_v' => $newV,
'cb' => $currentUserId,
]);
}
}
$pdo->commit();
echo json_encode(['success' => true, 'id' => $id]);
exit;
}
$ins = $pdo->prepare("
INSERT INTO employee_trainings
(employee_id, training_topic_id, completed_date,
delivered_by, description,
training_type, update_frequency_months, reminder_days, next_due_date,
created_by, created_at, updated_at)
VALUES
(:eid, :tid, :completed_date,
:delivered_by, :description,
:training_type, :freq, :rem, :next_due,
:cb, NOW(), NOW())
");
$ins->execute([
'eid' => $employeeId,
'tid' => $topicId,
'completed_date' => $completedDate,
'delivered_by' => $deliveredBy,
'description' => $description,
'training_type' => $trainingType,
'freq' => $freq,
'rem' => $rem,
'next_due' => $nextDue,
'cb' => $currentUserId,
]);
$newId = (int)$pdo->lastInsertId();
$pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, :tid, 'created', NULL, NULL, NULL, :cb, NOW())
")->execute(['eid' => $employeeId, 'tid' => $newId, 'cb' => $currentUserId]);
$pdo->commit();
echo json_encode(['success' => true, 'id' => $newId]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,89 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$employeeId = (int)($_POST['employee_id'] ?? 0);
$category = trim($_POST['category'] ?? 'other');
$notes = trim($_POST['notes'] ?? '');
$allowedCategories = ['job_description', 'contract', 'rules', 'other'];
if (!in_array($category, $allowedCategories, true)) {
$category = 'other';
}
if ($employeeId <= 0) {
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
exit;
}
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE id = :id");
$check->execute(['id' => $employeeId]);
if ((int)$check->fetchColumn() === 0) {
echo json_encode(['success' => false, 'message' => 'Dipendente non trovato.']);
exit;
}
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
$errCode = $_FILES['file']['error'] ?? -1;
$msg = 'Errore nel caricamento del file.';
if ($errCode === UPLOAD_ERR_INI_SIZE || $errCode === UPLOAD_ERR_FORM_SIZE) {
$msg = 'Il file supera la dimensione massima consentita.';
}
echo json_encode(['success' => false, 'message' => $msg]);
exit;
}
$originalName = $_FILES['file']['name'];
$tmpPath = $_FILES['file']['tmp_name'];
$size = (int)$_FILES['file']['size'];
$mimeType = mime_content_type($tmpPath) ?: ($_FILES['file']['type'] ?? null);
$dir = __DIR__ . '/../../files/employees/' . $employeeId . '/documents';
if (!is_dir($dir)) {
if (!mkdir($dir, 0775, true) && !is_dir($dir)) {
echo json_encode(['success' => false, 'message' => 'Impossibile creare la cartella di destinazione.']);
exit;
}
}
$safeOriginal = preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName);
$storedName = uniqid('doc_') . '_' . $safeOriginal;
$destPath = $dir . '/' . $storedName;
if (!move_uploaded_file($tmpPath, $destPath)) {
echo json_encode(['success' => false, 'message' => 'Impossibile salvare il file su disco.']);
exit;
}
try {
$stmt = $pdo->prepare("
INSERT INTO employee_documents
(employee_id, category, original_name, stored_name, mime_type, size, notes, uploaded_by, created_at)
VALUES
(:employee_id, :category, :original_name, :stored_name, :mime_type, :size, :notes, :uploaded_by, NOW())
");
$stmt->execute([
'employee_id' => $employeeId,
'category' => $category,
'original_name' => $originalName,
'stored_name' => $storedName,
'mime_type' => $mimeType,
'size' => $size,
'notes' => $notes !== '' ? $notes : null,
'uploaded_by' => $currentUserId,
]);
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
} catch (Exception $e) {
@unlink($destPath);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,98 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$trainingId = (int)($_POST['training_id'] ?? 0);
if ($trainingId <= 0) {
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
exit;
}
$tr = $pdo->prepare("SELECT employee_id FROM employee_trainings WHERE id = :id");
$tr->execute(['id' => $trainingId]);
$trainingRow = $tr->fetch(PDO::FETCH_ASSOC);
if (!$trainingRow) {
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
exit;
}
$employeeId = (int)$trainingRow['employee_id'];
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
$errCode = $_FILES['file']['error'] ?? -1;
$msg = 'Errore nel caricamento del file.';
if ($errCode === UPLOAD_ERR_INI_SIZE || $errCode === UPLOAD_ERR_FORM_SIZE) {
$msg = 'Il file supera la dimensione massima consentita.';
}
echo json_encode(['success' => false, 'message' => $msg]);
exit;
}
$originalName = $_FILES['file']['name'];
$tmpPath = $_FILES['file']['tmp_name'];
$size = (int)$_FILES['file']['size'];
$mimeType = mime_content_type($tmpPath) ?: ($_FILES['file']['type'] ?? null);
$dir = __DIR__ . '/../../files/employees/' . $employeeId . '/trainings';
if (!is_dir($dir)) {
if (!mkdir($dir, 0775, true) && !is_dir($dir)) {
echo json_encode(['success' => false, 'message' => 'Impossibile creare la cartella di destinazione.']);
exit;
}
}
$safeOriginal = preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName);
$storedName = uniqid('tr_') . '_' . $safeOriginal;
$destPath = $dir . '/' . $storedName;
if (!move_uploaded_file($tmpPath, $destPath)) {
echo json_encode(['success' => false, 'message' => 'Impossibile salvare il file su disco.']);
exit;
}
try {
$pdo->beginTransaction();
$ins = $pdo->prepare("
INSERT INTO employee_training_attachments
(training_id, original_name, stored_name, mime_type, size, uploaded_by, created_at)
VALUES
(:tid, :original_name, :stored_name, :mime_type, :size, :uploaded_by, NOW())
");
$ins->execute([
'tid' => $trainingId,
'original_name' => $originalName,
'stored_name' => $storedName,
'mime_type' => $mimeType,
'size' => $size,
'uploaded_by' => $currentUserId,
]);
$attachmentId = (int)$pdo->lastInsertId();
$pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, :tid, 'attachment_added', 'attachment', NULL, :name, :cb, NOW())
")->execute([
'eid' => $employeeId,
'tid' => $trainingId,
'name' => $originalName,
'cb' => $currentUserId,
]);
$pdo->commit();
echo json_encode(['success' => true, 'id' => $attachmentId]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
@unlink($destPath);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+32
View File
@@ -0,0 +1,32 @@
<?php
/**
* HR auth check for AJAX endpoints that require HR-management permissions.
* Allowed roles: Admin, User, Superuser, employee-hr, manager.
* Sets $currentUserId and $currentUserRole, or returns 401/403 JSON.
*/
require_once(__DIR__ . '/auth_check.php');
require_once(__DIR__ . '/../class/db-functions.php');
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->prepare("
SELECT r.name AS role_name
FROM auth_users u
LEFT JOIN auth_roles r ON r.id = u.role_id
WHERE u.id = :id
LIMIT 1
");
$stmt->execute(['id' => $currentUserId]);
$currentUserRole = (string)$stmt->fetchColumn();
$allowedHrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
if (!in_array($currentUserRole, $allowedHrRoles, true)) {
header('Content-Type: application/json');
http_response_code(403);
echo json_encode([
'success' => false,
'message' => 'Permessi insufficienti per questa operazione.',
]);
exit;
}
+38
View File
@@ -0,0 +1,38 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'ID mansione non valido.']);
exit;
}
try {
$usage = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE job_role_id = :id");
$usage->execute(['id' => $id]);
if ((int)$usage->fetchColumn() > 0) {
echo json_encode([
'success' => false,
'message' => 'Questa mansione è associata a uno o più dipendenti e non può essere cancellata.',
]);
exit;
}
$stmt = $pdo->prepare("DELETE FROM job_roles WHERE id = :id");
$stmt->execute(['id' => $id]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+77
View File
@@ -0,0 +1,77 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$description = trim($_POST['description'] ?? '');
$sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 999;
$is_active = isset($_POST['is_active']) ? ((int)$_POST['is_active'] === 1 ? 1 : 0) : 1;
if ($name === '') {
echo json_encode(['success' => false, 'message' => 'Il nome della mansione è obbligatorio.']);
exit;
}
try {
if ($id > 0) {
$check = $pdo->prepare("SELECT COUNT(*) FROM job_roles WHERE name = :name AND id <> :id");
$check->execute(['name' => $name, 'id' => $id]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode(['success' => false, 'message' => 'Esiste già un\'altra mansione con questo nome.']);
exit;
}
$stmt = $pdo->prepare("
UPDATE job_roles
SET name = :name,
description = :description,
sort_order = :sort_order,
is_active = :is_active,
updated_at = NOW()
WHERE id = :id
");
$stmt->execute([
'name' => $name,
'description' => $description !== '' ? $description : null,
'sort_order' => $sort_order,
'is_active' => $is_active,
'id' => $id,
]);
echo json_encode(['success' => true, 'id' => $id]);
exit;
}
$check = $pdo->prepare("SELECT COUNT(*) FROM job_roles WHERE name = :name");
$check->execute(['name' => $name]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode(['success' => false, 'message' => 'Esiste già una mansione con questo nome.']);
exit;
}
$stmt = $pdo->prepare("
INSERT INTO job_roles (name, description, sort_order, is_active, created_at, updated_at)
VALUES (:name, :description, :sort_order, :is_active, NOW(), NOW())
");
$stmt->execute([
'name' => $name,
'description' => $description !== '' ? $description : null,
'sort_order' => $sort_order,
'is_active' => $is_active,
]);
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,38 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'ID corso non valido.']);
exit;
}
try {
$usage = $pdo->prepare("SELECT COUNT(*) FROM employee_trainings WHERE training_topic_id = :id");
$usage->execute(['id' => $id]);
if ((int)$usage->fetchColumn() > 0) {
echo json_encode([
'success' => false,
'message' => 'Questo corso ha già delle registrazioni di formazione e non può essere cancellato.',
]);
exit;
}
$stmt = $pdo->prepare("DELETE FROM training_topics WHERE id = :id");
$stmt->execute(['id' => $id]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,94 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$description = trim($_POST['description'] ?? '');
$freqRaw = $_POST['default_frequency_months'] ?? '';
$remRaw = $_POST['default_reminder_days'] ?? '';
$sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 999;
$is_active = isset($_POST['is_active']) ? ((int)$_POST['is_active'] === 1 ? 1 : 0) : 1;
$is_mandatory = isset($_POST['is_mandatory']) && (int)$_POST['is_mandatory'] === 1 ? 1 : 0;
$freq = ($freqRaw === '' || $freqRaw === null) ? null : max(0, (int)$freqRaw);
$rem = ($remRaw === '' || $remRaw === null) ? 30 : max(0, (int)$remRaw);
if ($name === '') {
echo json_encode(['success' => false, 'message' => 'Il nome del corso è obbligatorio.']);
exit;
}
try {
if ($id > 0) {
$check = $pdo->prepare("SELECT COUNT(*) FROM training_topics WHERE name = :name AND id <> :id");
$check->execute(['name' => $name, 'id' => $id]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode(['success' => false, 'message' => 'Esiste già un altro corso con questo nome.']);
exit;
}
$stmt = $pdo->prepare("
UPDATE training_topics
SET name = :name,
description = :description,
default_frequency_months = :freq,
default_reminder_days = :rem,
sort_order = :sort_order,
is_active = :is_active,
is_mandatory = :is_mandatory,
updated_at = NOW()
WHERE id = :id
");
$stmt->execute([
'name' => $name,
'description' => $description !== '' ? $description : null,
'freq' => $freq,
'rem' => $rem,
'sort_order' => $sort_order,
'is_active' => $is_active,
'is_mandatory' => $is_mandatory,
'id' => $id,
]);
echo json_encode(['success' => true, 'id' => $id]);
exit;
}
$check = $pdo->prepare("SELECT COUNT(*) FROM training_topics WHERE name = :name");
$check->execute(['name' => $name]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode(['success' => false, 'message' => 'Esiste già un corso con questo nome.']);
exit;
}
$stmt = $pdo->prepare("
INSERT INTO training_topics
(name, description, default_frequency_months, default_reminder_days, sort_order, is_active, is_mandatory, created_at, updated_at)
VALUES
(:name, :description, :freq, :rem, :sort_order, :is_active, :is_mandatory, NOW(), NOW())
");
$stmt->execute([
'name' => $name,
'description' => $description !== '' ? $description : null,
'freq' => $freq,
'rem' => $rem,
'sort_order' => $sort_order,
'is_active' => $is_active,
'is_mandatory' => $is_mandatory,
]);
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,104 @@
<?php
/**
* Bulk "renew": set a common completed_date on the selected training records
*/
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
// $pdo and $currentUserId from hr_auth_check.php
$completedDate = trim($_POST['completed_date'] ?? '');
$ids = $_POST['training_ids'] ?? [];
if (!is_array($ids)) {
$ids = [];
}
$ids = array_values(array_unique(array_filter(array_map('intval', $ids), fn($v) => $v > 0)));
if ($completedDate === '' || !DateTime::createFromFormat('Y-m-d', $completedDate)) {
echo json_encode(['success' => false, 'message' => 'Indicare una data valida.']);
exit;
}
if (empty($ids)) {
echo json_encode(['success' => false, 'message' => 'Selezionare almeno un record.']);
exit;
}
try {
$pdo->beginTransaction();
// Load each record with its topic default frequency
$rowStmt = $pdo->prepare("
SELECT et.id, et.employee_id, et.completed_date, et.next_due_date,
et.update_frequency_months, tt.default_frequency_months
FROM employee_trainings et
JOIN training_topics tt ON tt.id = et.training_topic_id
WHERE et.id = :id
");
$upd = $pdo->prepare("
UPDATE employee_trainings
SET completed_date = :cd, next_due_date = :nd, updated_at = NOW()
WHERE id = :id
");
$logStmt = $pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, :tid, 'updated', :field, :old_v, :new_v, :cb, NOW())
");
$updated = 0;
foreach ($ids as $id) {
$rowStmt->execute(['id' => $id]);
$row = $rowStmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
continue;
}
// Effective frequency: per-record override, else topic default
$effFreq = $row['update_frequency_months'] !== null
? (int)$row['update_frequency_months']
: ($row['default_frequency_months'] !== null ? (int)$row['default_frequency_months'] : null);
$nextDue = null;
if ($effFreq !== null && $effFreq > 0) {
$d = DateTime::createFromFormat('Y-m-d', $completedDate);
if ($d) {
$d->modify('+' . $effFreq . ' months');
$nextDue = $d->format('Y-m-d');
}
}
$upd->execute(['cd' => $completedDate, 'nd' => $nextDue, 'id' => $id]);
if ((string)$row['completed_date'] !== (string)$completedDate) {
$logStmt->execute([
'eid' => $row['employee_id'], 'tid' => $id, 'field' => 'completed_date',
'old_v' => $row['completed_date'], 'new_v' => $completedDate, 'cb' => $currentUserId,
]);
}
if ((string)$row['next_due_date'] !== (string)$nextDue) {
$logStmt->execute([
'eid' => $row['employee_id'], 'tid' => $id, 'field' => 'next_due_date',
'old_v' => $row['next_due_date'], 'new_v' => $nextDue, 'cb' => $currentUserId,
]);
}
$updated++;
}
$pdo->commit();
echo json_encode([
'success' => true,
'updated' => $updated,
'message' => $updated . ' record aggiornat' . ($updated === 1 ? 'o' : 'i') . '.',
]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,92 @@
<?php
/**
* Calendar events for the training calendar (training_calendar.php).
* Returns FullCalendar event objects for the *current* training record per
* (employee, topic) that has a next_due_date, colored by computed status.
* HR-only.
*/
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
try {
// $pdo and $currentUserId provided by hr_auth_check.php
$start = $_GET['start'] ?? null;
$end = $_GET['end'] ?? null;
$fStatus = isset($_GET['status']) ? trim($_GET['status']) : '';
$fDept = isset($_GET['department_id']) && $_GET['department_id'] !== '' ? (int)$_GET['department_id'] : 0;
$fTopic = isset($_GET['topic_id']) && $_GET['topic_id'] !== '' ? (int)$_GET['topic_id'] : 0;
$fEmp = isset($_GET['employee_id']) && $_GET['employee_id'] !== '' ? (int)$_GET['employee_id'] : 0;
$where = [];
$params = [];
// Deadlines only (one-time trainings have no next_due_date)
$where[] = "et.next_due_date IS NOT NULL";
// Only the most recent record per (employee, topic)
$where[] = "NOT EXISTS (
SELECT 1 FROM employee_trainings et2
WHERE et2.employee_id = et.employee_id
AND et2.training_topic_id = et.training_topic_id
AND (et2.completed_date > et.completed_date
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
)";
if ($start && $end) {
$where[] = "et.next_due_date >= :start AND et.next_due_date <= :end";
$params['start'] = $start;
$params['end'] = $end;
}
if ($fDept > 0) { $where[] = "e.department_id = :did"; $params['did'] = $fDept; }
if ($fTopic > 0) { $where[] = "et.training_topic_id = :tid"; $params['tid'] = $fTopic; }
if ($fEmp > 0) { $where[] = "et.employee_id = :eid"; $params['eid'] = $fEmp; }
$whereSql = 'WHERE ' . implode(' AND ', $where);
$stmt = $pdo->prepare("
SELECT et.id, et.employee_id, et.next_due_date, et.reminder_days,
tt.name AS topic_name, tt.default_reminder_days AS topic_default_rem,
e.first_name, e.last_name
FROM employee_trainings et
JOIN training_topics tt ON tt.id = et.training_topic_id
JOIN employees e ON e.id = et.employee_id
$whereSql
");
$stmt->execute($params);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$today = new DateTime('today');
$events = [];
foreach ($rows as $r) {
$rem = $r['reminder_days'] !== null
? (int)$r['reminder_days']
: ($r['topic_default_rem'] !== null ? (int)$r['topic_default_rem'] : 30);
$due = DateTime::createFromFormat('Y-m-d', $r['next_due_date']);
if (!$due) continue;
$daysLeft = (int)$today->diff($due)->format('%r%a');
if ($daysLeft < 0) { $code = 'expired'; $color = '#dc3545'; }
elseif ($daysLeft <= $rem){ $code = 'due_soon'; $color = '#e8930c'; }
else { $code = 'compliant'; $color = '#198754'; }
if ($fStatus !== '' && $fStatus !== $code) continue;
$name = trim($r['first_name'] . ' ' . $r['last_name']);
$events[] = [
'id' => (int)$r['id'],
'title' => $name . ' — ' . $r['topic_name'],
'start' => $r['next_due_date'],
'allDay' => true,
'backgroundColor' => $color,
'borderColor' => $color,
'url' => 'employee-profile.php?id=' . (int)$r['employee_id'] . '#tab-training',
];
}
echo json_encode($events);
} catch (Exception $e) {
echo json_encode([]);
}
@@ -0,0 +1,131 @@
<?php
/**
* Bulk-create training records: one employee_trainings row per selected employee,
* all sharing the same course + parameters (a single training "session").
* Mirrors the next_due_date logic of ajax/employee_profile/save_training.php.
* HR-only.
*/
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
// $pdo and $currentUserId from hr_auth_check.php
$topicId = (int)($_POST['training_topic_id'] ?? 0);
$completedDate = trim($_POST['completed_date'] ?? '');
$deliveredBy = trim($_POST['delivered_by'] ?? '');
$description = trim($_POST['description'] ?? '');
$trainingType = trim($_POST['training_type'] ?? 'initial');
$freqRaw = $_POST['update_frequency_months'] ?? '';
$remRaw = $_POST['reminder_days'] ?? '';
$employeeIds = $_POST['employee_ids'] ?? [];
if (!is_array($employeeIds)) {
$employeeIds = [];
}
$employeeIds = array_values(array_unique(array_filter(array_map('intval', $employeeIds), fn($v) => $v > 0)));
if ($topicId <= 0) {
echo json_encode(['success' => false, 'message' => 'Selezionare un corso.']);
exit;
}
if ($completedDate === '' || !DateTime::createFromFormat('Y-m-d', $completedDate)) {
echo json_encode(['success' => false, 'message' => 'La data di completamento è obbligatoria.']);
exit;
}
if (empty($employeeIds)) {
echo json_encode(['success' => false, 'message' => 'Selezionare almeno un dipendente.']);
exit;
}
if (!in_array($trainingType, ['initial', 'refresher'], true)) {
$trainingType = 'initial';
}
$topicStmt = $pdo->prepare("SELECT default_frequency_months, default_reminder_days FROM training_topics WHERE id = :id");
$topicStmt->execute(['id' => $topicId]);
$topic = $topicStmt->fetch(PDO::FETCH_ASSOC);
if (!$topic) {
echo json_encode(['success' => false, 'message' => 'Corso non trovato.']);
exit;
}
$freq = ($freqRaw === '' || $freqRaw === null) ? null : max(0, (int)$freqRaw);
$rem = ($remRaw === '' || $remRaw === null) ? null : max(0, (int)$remRaw);
/* Effective frequency → next_due_date (same for every employee: same date + same frequency) */
$effFreq = $freq !== null ? $freq : ($topic['default_frequency_months'] !== null ? (int)$topic['default_frequency_months'] : null);
$nextDue = null;
if ($effFreq !== null && $effFreq > 0) {
$d = DateTime::createFromFormat('Y-m-d', $completedDate);
if ($d) {
$d->modify('+' . (int)$effFreq . ' months');
$nextDue = $d->format('Y-m-d');
}
}
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
$description = $description !== '' ? $description : null;
try {
$pdo->beginTransaction();
// Only insert for employees that actually exist
$checkEmp = $pdo->prepare("SELECT id FROM employees WHERE id = :id");
$ins = $pdo->prepare("
INSERT INTO employee_trainings
(employee_id, training_topic_id, completed_date,
delivered_by, description,
training_type, update_frequency_months, reminder_days, next_due_date,
created_by, created_at, updated_at)
VALUES
(:eid, :tid, :completed_date,
:delivered_by, :description,
:training_type, :freq, :rem, :next_due,
:cb, NOW(), NOW())
");
$logStmt = $pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, :tid, 'created', NULL, NULL, NULL, :cb, NOW())
");
$created = 0;
foreach ($employeeIds as $eid) {
$checkEmp->execute(['id' => $eid]);
if (!$checkEmp->fetchColumn()) {
continue;
}
$ins->execute([
'eid' => $eid,
'tid' => $topicId,
'completed_date' => $completedDate,
'delivered_by' => $deliveredBy,
'description' => $description,
'training_type' => $trainingType,
'freq' => $freq,
'rem' => $rem,
'next_due' => $nextDue,
'cb' => $currentUserId,
]);
$newId = (int)$pdo->lastInsertId();
$logStmt->execute(['eid' => $eid, 'tid' => $newId, 'cb' => $currentUserId]);
$created++;
}
$pdo->commit();
echo json_encode([
'success' => true,
'created' => $created,
'message' => $created . ' formazion' . ($created === 1 ? 'e registrata' : 'i registrate') . '.',
]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
File diff suppressed because it is too large Load Diff
+71
View File
@@ -0,0 +1,71 @@
<?php
header('Content-Type: application/json');
require_once(__DIR__ . '/include/headscript.php');
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$iduser = $iduserlogin ?? null;
$input = json_decode(file_get_contents('php://input'), true);
$id = (int)($input['id'] ?? 0);
if ($id <= 0) {
throw new Exception('ID non valido.');
}
if ($iduser === null) {
$stmt = $pdo->prepare("
SELECT *
FROM cad_area_jobs
WHERE id = :id
LIMIT 1
");
$stmt->execute([
':id' => $id
]);
} else {
$stmt = $pdo->prepare("
SELECT *
FROM cad_area_jobs
WHERE id = :id
AND iduser = :iduser
LIMIT 1
");
$stmt->execute([
':id' => $id,
':iduser' => $iduser
]);
}
$job = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$job) {
throw new Exception('Record non trovato.');
}
if (!empty($job['file_path']) && file_exists($job['file_path'])) {
unlink($job['file_path']);
}
$stmtDelete = $pdo->prepare("
DELETE FROM cad_area_jobs
WHERE id = :id
");
$stmtDelete->execute([':id' => $id]);
echo json_encode([
'success' => true
]);
} catch (Throwable $e) {
error_log('CAD area delete error: ' . $e->getMessage());
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
+338
View File
@@ -0,0 +1,338 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once(__DIR__ . '/include/headscript.php');
function jsonResponse(array $data): void
{
echo json_encode($data);
exit;
}
function updateJobProcessing(PDO $pdo, int $id): void
{
$stmt = $pdo->prepare("
UPDATE cad_area_jobs
SET
status = 'processing',
message = 'Elaborazione in corso...',
updated_at = NOW()
WHERE id = ?
");
$stmt->execute([$id]);
}
function updateJobError(PDO $pdo, int $id, string $message, ?array $pythonResponse = null): void
{
$stmt = $pdo->prepare("
UPDATE cad_area_jobs
SET
status = 'error',
message = ?,
python_response = ?,
updated_at = NOW()
WHERE id = ?
");
$stmt->execute([
$message,
$pythonResponse ? json_encode($pythonResponse) : null,
$id
]);
}
function updateJobCompleted(PDO $pdo, int $id, array $response): void
{
$stmt = $pdo->prepare("
UPDATE cad_area_jobs
SET
status = 'completed',
message = ?,
area_mm2 = ?,
area_cm2 = ?,
area_m2 = ?,
width_mm = ?,
height_mm = ?,
scale_detected = ?,
scale_used = ?,
confidence = ?,
python_response = ?,
updated_at = NOW()
WHERE id = ?
");
$stmt->execute([
$response['message'] ?? 'Area calcolata correttamente.',
$response['area_mm2'] ?? null,
$response['area_cm2'] ?? null,
$response['area_m2'] ?? null,
$response['width_mm'] ?? null,
$response['height_mm'] ?? null,
$response['scale_detected'] ?? null,
$response['scale_used'] ?? null,
$response['confidence'] ?? null,
json_encode($response),
$id
]);
}
function normalizeCalculationMode(?string $mode): string
{
$allowed = [
'auto_roi',
'stitch_contour',
'filled_union',
'closed_path'
];
if (!$mode || !in_array($mode, $allowed, true)) {
return 'auto_roi';
}
return $mode;
}
function hasValidRoi(array $job): bool
{
return (
array_key_exists('roi_x', $job) &&
array_key_exists('roi_y', $job) &&
array_key_exists('roi_width', $job) &&
array_key_exists('roi_height', $job) &&
$job['roi_x'] !== null &&
$job['roi_y'] !== null &&
$job['roi_width'] !== null &&
$job['roi_height'] !== null &&
(float)$job['roi_width'] > 0 &&
(float)$job['roi_height'] > 0
);
}
function callPythonAreaService(string $url, array $job): array
{
$filePath = $job['file_path'] ?? '';
$originalFilename = $job['original_filename'] ?? basename($filePath);
if (!$filePath || !file_exists($filePath)) {
return [
'success' => false,
'message' => 'File PDF non trovato sul server: ' . $filePath
];
}
$mode = normalizeCalculationMode($job['calculation_mode'] ?? 'auto_roi');
$scaleRatio = $job['scale_used'] ?? null;
if ($scaleRatio === null || $scaleRatio === '' || (float)$scaleRatio <= 0) {
$scaleRatio = '1';
}
$curlFile = new CURLFile(
$filePath,
'application/pdf',
$originalFilename
);
$postFields = [
'file' => $curlFile,
'mode' => $mode,
'scale_ratio' => (string)$scaleRatio,
'roi_x' => (string)$job['roi_x'],
'roi_y' => (string)$job['roi_y'],
'roi_width' => (string)$job['roi_width'],
'roi_height' => (string)$job['roi_height'],
'roi_page' => (string)($job['roi_page'] ?? 1)
];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postFields,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 180,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_HTTPHEADER => [
'Accept: application/json'
]
]);
$rawResponse = curl_exec($ch);
$curlError = curl_error($ch);
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($rawResponse === false) {
return [
'success' => false,
'message' => 'Errore cURL verso Python: ' . $curlError
];
}
$decoded = json_decode($rawResponse, true);
if (!is_array($decoded)) {
return [
'success' => false,
'message' => 'Risposta Python non JSON valida.',
'http_code' => $httpCode,
'raw_response' => $rawResponse
];
}
if ($httpCode < 200 || $httpCode >= 300) {
return [
'success' => false,
'message' => $decoded['message'] ?? ('Servizio Python HTTP ' . $httpCode),
'http_code' => $httpCode,
'python_response' => $decoded,
'raw_response' => $rawResponse
];
}
return $decoded;
}
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$iduser = $iduserlogin ?? null;
$input = json_decode(file_get_contents('php://input'), true);
if (!is_array($input)) {
jsonResponse([
'success' => false,
'message' => 'Payload JSON non valido.'
]);
}
$ids = $input['ids'] ?? [];
if (!is_array($ids) || count($ids) === 0) {
jsonResponse([
'success' => false,
'message' => 'Nessun ID ricevuto.'
]);
}
$ids = array_values(array_unique(array_map('intval', $ids)));
$ids = array_filter($ids, fn($id) => $id > 0);
if (count($ids) === 0) {
jsonResponse([
'success' => false,
'message' => 'Nessun ID valido ricevuto.'
]);
}
$pythonServiceUrl = 'http://127.0.0.1:5055/calculate';
$results = [];
foreach ($ids as $id) {
if ($iduser === null || $iduser === '') {
$stmt = $pdo->prepare("
SELECT *
FROM cad_area_jobs
WHERE id = ?
LIMIT 1
");
$stmt->execute([$id]);
} else {
$stmt = $pdo->prepare("
SELECT *
FROM cad_area_jobs
WHERE id = ?
AND iduser = ?
LIMIT 1
");
$stmt->execute([$id, $iduser]);
}
$job = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$job) {
$results[] = [
'id' => $id,
'success' => false,
'message' => 'Record non trovato.'
];
continue;
}
if (!hasValidRoi($job)) {
$message = 'Prima devi definire la sezione da misurare tramite il pulsante Sezione.';
updateJobError($pdo, $id, $message, [
'success' => false,
'message' => $message,
'job_roi_debug' => [
'roi_x' => $job['roi_x'] ?? null,
'roi_y' => $job['roi_y'] ?? null,
'roi_width' => $job['roi_width'] ?? null,
'roi_height' => $job['roi_height'] ?? null,
'roi_page' => $job['roi_page'] ?? null,
'calculation_mode' => $job['calculation_mode'] ?? null
]
]);
$results[] = [
'id' => $id,
'success' => false,
'message' => $message
];
continue;
}
updateJobProcessing($pdo, $id);
$pythonResponse = callPythonAreaService($pythonServiceUrl, $job);
if (!($pythonResponse['success'] ?? false)) {
$message = $pythonResponse['message'] ?? 'Errore durante il calcolo Python.';
updateJobError($pdo, $id, $message, $pythonResponse);
$results[] = [
'id' => $id,
'success' => false,
'message' => $message,
'python_response' => $pythonResponse
];
continue;
}
updateJobCompleted($pdo, $id, $pythonResponse);
$results[] = [
'id' => $id,
'success' => true,
'message' => $pythonResponse['message'] ?? 'Area calcolata.',
'area_mm2' => $pythonResponse['area_mm2'] ?? null,
'area_cm2' => $pythonResponse['area_cm2'] ?? null
];
}
jsonResponse([
'success' => true,
'results' => $results
]);
} catch (Throwable $e) {
error_log('CAD area process error: ' . $e->getMessage());
jsonResponse([
'success' => false,
'message' => $e->getMessage()
]);
}
@@ -0,0 +1,297 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once(__DIR__ . '/include/headscript.php');
function jsonResponse(array $data): void
{
echo json_encode($data);
exit;
}
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$iduser = $iduserlogin ?? null;
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);
if (!is_array($input)) {
jsonResponse([
'success' => false,
'message' => 'Payload JSON non valido.'
]);
}
$id = (int)($input['id'] ?? 0);
if ($id <= 0) {
jsonResponse([
'success' => false,
'message' => 'ID non valido.'
]);
}
$areaMm2 = isset($input['area_mm2']) ? (float)$input['area_mm2'] : 0;
$areaCm2 = isset($input['area_cm2']) ? (float)$input['area_cm2'] : 0;
$outerAreaMm2 = isset($input['manual_outer_area_mm2']) ? (float)$input['manual_outer_area_mm2'] : $areaMm2;
$holesAreaMm2 = isset($input['manual_holes_area_mm2']) ? (float)$input['manual_holes_area_mm2'] : 0;
$widthMm = isset($input['width_mm']) ? (float)$input['width_mm'] : null;
$heightMm = isset($input['height_mm']) ? (float)$input['height_mm'] : null;
$calibrationPx = isset($input['manual_calibration_px']) ? (float)$input['manual_calibration_px'] : 0;
$calibrationMm = isset($input['manual_calibration_mm']) ? (float)$input['manual_calibration_mm'] : 0;
$mmPerPx = isset($input['manual_mm_per_px']) ? (float)$input['manual_mm_per_px'] : 0;
$outerPolygon = $input['manual_polygon'] ?? null;
$holes = $input['manual_holes'] ?? [];
$roi = $input['roi'] ?? null;
if ($areaMm2 <= 0) {
jsonResponse([
'success' => false,
'message' => 'Area finale non valida.'
]);
}
if ($outerAreaMm2 <= 0) {
jsonResponse([
'success' => false,
'message' => 'Area esterna non valida.'
]);
}
if ($holesAreaMm2 < 0) {
jsonResponse([
'success' => false,
'message' => 'Area fori non valida.'
]);
}
if ($calibrationPx <= 0 || $calibrationMm <= 0 || $mmPerPx <= 0) {
jsonResponse([
'success' => false,
'message' => 'Calibrazione non valida.'
]);
}
if (!is_array($outerPolygon) || count($outerPolygon) < 3) {
jsonResponse([
'success' => false,
'message' => 'Poligono esterno non valido. Servono almeno 3 punti.'
]);
}
if (!is_array($holes)) {
$holes = [];
}
$manualPolygonJson = json_encode([
'outer_polygon' => $outerPolygon,
'holes' => $holes,
'roi' => $roi,
'calibration' => $input['calibration'] ?? null,
'canvas' => $input['canvas'] ?? null,
'areas' => [
'outer_area_mm2' => $outerAreaMm2,
'holes_area_mm2' => $holesAreaMm2,
'final_area_mm2' => $areaMm2,
'final_area_cm2' => $areaCm2
]
]);
$manualHolesJson = json_encode($holes);
$roiX = null;
$roiY = null;
$roiW = null;
$roiH = null;
if (is_array($roi)) {
$roiX = isset($roi['x']) ? (float)$roi['x'] : null;
$roiY = isset($roi['y']) ? (float)$roi['y'] : null;
$roiW = isset($roi['width']) ? (float)$roi['width'] : null;
$roiH = isset($roi['height']) ? (float)$roi['height'] : null;
}
if ($iduser === null || $iduser === '') {
$stmt = $pdo->prepare("
UPDATE cad_area_jobs
SET
roi_x = COALESCE(?, roi_x),
roi_y = COALESCE(?, roi_y),
roi_width = COALESCE(?, roi_width),
roi_height = COALESCE(?, roi_height),
roi_page = 1,
status = 'completed',
message = 'Area calcolata tramite tracciamento manuale calibrato.',
area_mm2 = ?,
area_cm2 = ?,
area_m2 = ?,
manual_area_mm2 = ?,
manual_area_cm2 = ?,
manual_outer_area_mm2 = ?,
manual_holes_area_mm2 = ?,
manual_width_mm = ?,
manual_height_mm = ?,
width_mm = ?,
height_mm = ?,
manual_calibration_px = ?,
manual_calibration_mm = ?,
manual_mm_per_px = ?,
manual_polygon_json = ?,
manual_holes_json = ?,
manual_status = 'completed',
scale_used = ?,
scale_detected = ?,
confidence = 'manual_validated',
strategy_used = 'manual_tracing_with_exclusions',
python_response = NULL,
updated_at = NOW()
WHERE id = ?
");
$stmt->execute([
$roiX,
$roiY,
$roiW,
$roiH,
$areaMm2,
$areaCm2,
$areaMm2 / 1000000,
$areaMm2,
$areaCm2,
$outerAreaMm2,
$holesAreaMm2,
$widthMm,
$heightMm,
$widthMm,
$heightMm,
$calibrationPx,
$calibrationMm,
$mmPerPx,
$manualPolygonJson,
$manualHolesJson,
$mmPerPx,
'manual',
$id
]);
} else {
$stmt = $pdo->prepare("
UPDATE cad_area_jobs
SET
roi_x = COALESCE(?, roi_x),
roi_y = COALESCE(?, roi_y),
roi_width = COALESCE(?, roi_width),
roi_height = COALESCE(?, roi_height),
roi_page = 1,
status = 'completed',
message = 'Area calcolata tramite tracciamento manuale calibrato.',
area_mm2 = ?,
area_cm2 = ?,
area_m2 = ?,
manual_area_mm2 = ?,
manual_area_cm2 = ?,
manual_outer_area_mm2 = ?,
manual_holes_area_mm2 = ?,
manual_width_mm = ?,
manual_height_mm = ?,
width_mm = ?,
height_mm = ?,
manual_calibration_px = ?,
manual_calibration_mm = ?,
manual_mm_per_px = ?,
manual_polygon_json = ?,
manual_holes_json = ?,
manual_status = 'completed',
scale_used = ?,
scale_detected = ?,
confidence = 'manual_validated',
strategy_used = 'manual_tracing_with_exclusions',
python_response = NULL,
updated_at = NOW()
WHERE id = ?
AND iduser = ?
");
$stmt->execute([
$roiX,
$roiY,
$roiW,
$roiH,
$areaMm2,
$areaCm2,
$areaMm2 / 1000000,
$areaMm2,
$areaCm2,
$outerAreaMm2,
$holesAreaMm2,
$widthMm,
$heightMm,
$widthMm,
$heightMm,
$calibrationPx,
$calibrationMm,
$mmPerPx,
$manualPolygonJson,
$manualHolesJson,
$mmPerPx,
'manual',
$id,
$iduser
]);
}
if ($stmt->rowCount() === 0) {
jsonResponse([
'success' => false,
'message' => 'Nessun record aggiornato. Controlla ID o utente.'
]);
}
jsonResponse([
'success' => true,
'message' => 'Area manuale salvata correttamente.',
'area_mm2' => $areaMm2,
'area_cm2' => $areaCm2,
'outer_area_mm2' => $outerAreaMm2,
'holes_area_mm2' => $holesAreaMm2
]);
} catch (Throwable $e) {
error_log('CAD manual area save error: ' . $e->getMessage());
jsonResponse([
'success' => false,
'message' => $e->getMessage()
]);
}
+124
View File
@@ -0,0 +1,124 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once(__DIR__ . '/include/headscript.php');
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$iduser = $iduserlogin ?? null;
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);
if (!is_array($input)) {
throw new Exception('Payload JSON non valido.');
}
$id = (int)($input['id'] ?? 0);
if ($id <= 0) {
throw new Exception('ID non valido.');
}
$roiX = isset($input['roi_x']) ? (float)$input['roi_x'] : null;
$roiY = isset($input['roi_y']) ? (float)$input['roi_y'] : null;
$roiW = isset($input['roi_width']) ? (float)$input['roi_width'] : null;
$roiH = isset($input['roi_height']) ? (float)$input['roi_height'] : null;
$roiPage = isset($input['roi_page']) ? (int)$input['roi_page'] : 1;
$mode = $input['calculation_mode'] ?? 'auto_roi';
if ($roiX === null || $roiY === null || $roiW === null || $roiH === null) {
throw new Exception('ROI non valida.');
}
if ($roiW <= 0 || $roiH <= 0) {
throw new Exception('Dimensioni ROI non valide.');
}
if ($roiX < 0 || $roiY < 0 || $roiX > 1 || $roiY > 1 || $roiW > 1 || $roiH > 1) {
throw new Exception('Coordinate ROI fuori scala.');
}
$allowedModes = [
'auto_roi',
'stitch_contour',
'filled_union',
'closed_path'
];
if (!in_array($mode, $allowedModes, true)) {
$mode = 'auto_roi';
}
if ($iduser === null || $iduser === '') {
$stmt = $pdo->prepare("
UPDATE cad_area_jobs
SET
roi_x = ?,
roi_y = ?,
roi_width = ?,
roi_height = ?,
roi_page = ?,
calculation_mode = ?,
status = 'uploaded',
message = NULL
WHERE id = ?
");
$stmt->execute([
$roiX,
$roiY,
$roiW,
$roiH,
$roiPage,
$mode,
$id
]);
} else {
$stmt = $pdo->prepare("
UPDATE cad_area_jobs
SET
roi_x = ?,
roi_y = ?,
roi_width = ?,
roi_height = ?,
roi_page = ?,
calculation_mode = ?,
status = 'uploaded',
message = NULL
WHERE id = ?
AND iduser = ?
");
$stmt->execute([
$roiX,
$roiY,
$roiW,
$roiH,
$roiPage,
$mode,
$id,
$iduser
]);
}
if ($stmt->rowCount() === 0) {
throw new Exception('Nessun record aggiornato. Controlla ID o utente.');
}
echo json_encode([
'success' => true,
'message' => 'ROI salvata correttamente.'
]);
exit;
} catch (Throwable $e) {
error_log('CAD area save ROI error: ' . $e->getMessage());
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
exit;
}
+106
View File
@@ -0,0 +1,106 @@
<?php
header('Content-Type: application/json');
require_once(__DIR__ . '/include/headscript.php');
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$iduser = $iduserlogin ?? null;
$uploadDir = __DIR__ . '/uploads/cad_area/originals/';
$publicBaseUrl = 'uploads/cad_area/originals/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
if (empty($_FILES['pdf_files'])) {
throw new Exception('Nessun file ricevuto.');
}
$files = $_FILES['pdf_files'];
$insertedIds = [];
for ($i = 0; $i < count($files['name']); $i++) {
if ($files['error'][$i] !== UPLOAD_ERR_OK) {
continue;
}
$originalName = $files['name'][$i];
$tmpName = $files['tmp_name'][$i];
$size = (int)$files['size'][$i];
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if ($extension !== 'pdf') {
continue;
}
if ($size > 25 * 1024 * 1024) {
continue;
}
$safeBaseName = preg_replace('/[^a-zA-Z0-9_\-]/', '_', pathinfo($originalName, PATHINFO_FILENAME));
$storedName = date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '_' . $safeBaseName . '.pdf';
$targetPath = $uploadDir . $storedName;
if (!move_uploaded_file($tmpName, $targetPath)) {
continue;
}
$relativeUrl = $publicBaseUrl . $storedName;
$stmt = $pdo->prepare("
INSERT INTO cad_area_jobs
(
iduser,
original_filename,
stored_filename,
file_path,
file_url,
file_size,
status
)
VALUES
(
:iduser,
:original_filename,
:stored_filename,
:file_path,
:file_url,
:file_size,
'uploaded'
)
");
$stmt->execute([
':iduser' => $iduser,
':original_filename' => $originalName,
':stored_filename' => $storedName,
':file_path' => $targetPath,
':file_url' => $relativeUrl,
':file_size' => $size
]);
$insertedIds[] = (int)$pdo->lastInsertId();
}
if (empty($insertedIds)) {
throw new Exception('Nessun PDF valido caricato.');
}
echo json_encode([
'success' => true,
'ids' => $insertedIds
]);
} catch (Throwable $e) {
error_log('CAD area upload error: ' . $e->getMessage());
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
+617
View File
@@ -0,0 +1,617 @@
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
function jsonResponse(array $data): void
{
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data);
exit;
}
function normalizeNullableInt($value): ?int
{
return (isset($value) && $value !== '') ? (int)$value : null;
}
function normalizeBoolValue($value): int
{
return ((string)$value === '0') ? 0 : 1;
}
function cleanString(?string $value): string
{
return trim((string)$value);
}
/* ==========================================
AJAX HANDLERS
========================================== */
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['ajax'] == '1') {
$action = $_POST['action'] ?? '';
try {
if ($action === 'add') {
$functionName = cleanString($_POST['function_name'] ?? '');
$personFullName = cleanString($_POST['person_full_name'] ?? '');
$phone = cleanString($_POST['phone'] ?? '');
$email = cleanString($_POST['email'] ?? '');
$notes = cleanString($_POST['notes'] ?? '');
$sortOrder = normalizeNullableInt($_POST['sort_order'] ?? '0') ?? 0;
$isActive = normalizeBoolValue($_POST['is_active'] ?? '1');
if ($functionName === '') {
jsonResponse(['success' => false, 'message' => 'Il nome funzione è obbligatorio.']);
}
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
jsonResponse(['success' => false, 'message' => 'Email non valida.']);
}
$stmt = $pdo->prepare("\n INSERT INTO company_functions\n (function_name, person_full_name, phone, email, notes, sort_order, is_active, created_at, updated_at)\n VALUES\n (:function_name, :person_full_name, :phone, :email, :notes, :sort_order, :is_active, NOW(), NOW())\n ");
$stmt->execute([
'function_name' => $functionName,
'person_full_name' => $personFullName !== '' ? $personFullName : '',
'phone' => $phone !== '' ? $phone : null,
'email' => $email !== '' ? $email : null,
'notes' => $notes !== '' ? $notes : null,
'sort_order' => $sortOrder,
'is_active' => $isActive,
]);
jsonResponse(['success' => true, 'message' => 'Funzione salvata correttamente.']);
}
if ($action === 'edit') {
$id = (int)($_POST['id'] ?? 0);
$functionName = cleanString($_POST['function_name'] ?? '');
$personFullName = cleanString($_POST['person_full_name'] ?? '');
$phone = cleanString($_POST['phone'] ?? '');
$email = cleanString($_POST['email'] ?? '');
$notes = cleanString($_POST['notes'] ?? '');
$sortOrder = normalizeNullableInt($_POST['sort_order'] ?? '0') ?? 0;
$isActive = normalizeBoolValue($_POST['is_active'] ?? '1');
if ($id <= 0) {
jsonResponse(['success' => false, 'message' => 'ID funzione non valido.']);
}
if ($functionName === '') {
jsonResponse(['success' => false, 'message' => 'Il nome funzione è obbligatorio.']);
}
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
jsonResponse(['success' => false, 'message' => 'Email non valida.']);
}
$stmt = $pdo->prepare("\n UPDATE company_functions\n SET function_name = :function_name,\n person_full_name = :person_full_name,\n phone = :phone,\n email = :email,\n notes = :notes,\n sort_order = :sort_order,\n is_active = :is_active,\n updated_at = NOW()\n WHERE id = :id\n ");
$stmt->execute([
'function_name' => $functionName,
'person_full_name' => $personFullName !== '' ? $personFullName : '',
'phone' => $phone !== '' ? $phone : null,
'email' => $email !== '' ? $email : null,
'notes' => $notes !== '' ? $notes : null,
'sort_order' => $sortOrder,
'is_active' => $isActive,
'id' => $id,
]);
jsonResponse(['success' => true, 'message' => 'Funzione aggiornata correttamente.']);
}
if ($action === 'delete') {
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
jsonResponse(['success' => false, 'message' => 'ID funzione non valido.']);
}
$stmt = $pdo->prepare("DELETE FROM company_functions WHERE id = :id");
$stmt->execute(['id' => $id]);
jsonResponse(['success' => true, 'message' => 'Funzione cancellata correttamente.']);
}
jsonResponse(['success' => false, 'message' => 'Azione non riconosciuta.']);
} catch (Exception $e) {
jsonResponse(['success' => false, 'message' => $e->getMessage()]);
}
}
/* ==========================================
PAGE DATA
========================================== */
$stmtFunctions = $pdo->query("\n SELECT id, function_name, person_full_name, phone, email, notes, sort_order, is_active, created_at, updated_at\n FROM company_functions\n ORDER BY is_active DESC, sort_order ASC, function_name ASC, person_full_name ASC\n");
$functions = $stmtFunctions->fetchAll(PDO::FETCH_ASSOC);
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>Funzioni Aziendali - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<style>
body {
font-size: 1.05rem;
background: #f8fafc;
}
.card {
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.back-dashboard {
background-color: #cfe3ff !important;
color: #1f2d3d !important;
border: 1px solid #bcd4f4 !important;
border-radius: 10px;
font-weight: 600;
font-size: 1rem;
padding: 10px 18px;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease-in-out;
}
.back-dashboard:hover {
background-color: #b9d3ff !important;
transform: translateY(-2px);
}
.btn-main-action,
.btn-add {
background-color: #0d6efd;
color: #fff;
border-radius: 8px;
padding: 10px 20px;
font-weight: 500;
transition: all 0.2s ease-in-out;
}
.btn-main-action:hover,
.btn-add:hover {
background-color: #0b5ed7;
color: #fff;
transform: scale(1.02);
}
.table thead {
background-color: #cfe3ff;
color: #1f2d3d;
}
#tabCompanyFunctions thead th {
text-align: center;
vertical-align: middle;
}
.modal-content {
border-radius: 16px;
}
.function-name {
font-weight: 700;
color: #1f2937;
}
.person-name {
font-weight: 600;
color: #334155;
}
.contact-line {
display: block;
font-size: 0.9rem;
text-decoration: none;
}
.notes-small {
color: #64748b;
font-size: 0.9rem;
max-width: 420px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.badge-status {
padding: 0.25rem 0.65rem;
border-radius: 999px;
font-size: 0.82rem;
font-weight: 700;
}
.badge-status.active {
background-color: #d1fae5;
color: #065f46;
}
.badge-status.inactive {
background-color: #e5e7eb;
color: #374151;
}
.empty-text {
color: #94a3b8;
font-style: italic;
}
@media (max-width: 767.98px) {
.card-header {
flex-direction: column;
align-items: flex-start !important;
gap: .5rem;
}
.back-dashboard,
.btn-main-action {
width: 100%;
}
}
</style>
</head>
<body>
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card p-3">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h5 class="mb-0">Funzioni Aziendali</h5>
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
↩️ Torna alla Dashboard
</button>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<div>
<h6 class="fw-semibold mb-1">Elenco Funzioni</h6>
<div class="text-muted small">Gestione di RSPP, medico del lavoro, RLS e altre funzioni aziendali.</div>
</div>
<button class="btn btn-main-action" data-bs-toggle="modal" data-bs-target="#companyFunctionModal" onclick="openCompanyFunctionModal()">
Aggiungi Funzione
</button>
</div>
<div class="table-responsive">
<table id="tabCompanyFunctions" class="table table-striped align-middle text-center" style="width:100%;">
<thead>
<tr>
<th>Funzione</th>
<th>Nominativo</th>
<th>Contatti</th>
<th>Note</th>
<th>Ordine</th>
<th>Stato</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($functions as $row): ?>
<?php
$id = (int)$row['id'];
$functionName = (string)($row['function_name'] ?? '');
$personFullName = (string)($row['person_full_name'] ?? '');
$phone = (string)($row['phone'] ?? '');
$email = (string)($row['email'] ?? '');
$notes = (string)($row['notes'] ?? '');
$sortOrder = (int)($row['sort_order'] ?? 0);
$isActive = (int)($row['is_active'] ?? 1);
?>
<tr>
<td class="text-start">
<div class="function-name"><?= htmlspecialchars($functionName, ENT_QUOTES, 'UTF-8') ?></div>
</td>
<td class="text-start">
<?php if ($personFullName !== ''): ?>
<div class="person-name"><?= htmlspecialchars($personFullName, ENT_QUOTES, 'UTF-8') ?></div>
<?php else: ?>
<span class="empty-text">Da definire</span>
<?php endif; ?>
</td>
<td class="text-start">
<?php if ($phone !== ''): ?>
<a class="contact-line" href="tel:<?= htmlspecialchars($phone, ENT_QUOTES, 'UTF-8') ?>">
📞 <?= htmlspecialchars($phone, ENT_QUOTES, 'UTF-8') ?>
</a>
<?php endif; ?>
<?php if ($email !== ''): ?>
<a class="contact-line" href="mailto:<?= htmlspecialchars($email, ENT_QUOTES, 'UTF-8') ?>">
✉️ <?= htmlspecialchars($email, ENT_QUOTES, 'UTF-8') ?>
</a>
<?php endif; ?>
<?php if ($phone === '' && $email === ''): ?>
<span class="empty-text">Nessun contatto</span>
<?php endif; ?>
</td>
<td class="text-start">
<?php if ($notes !== ''): ?>
<div class="notes-small" title="<?= htmlspecialchars($notes, ENT_QUOTES, 'UTF-8') ?>">
<?= htmlspecialchars($notes, ENT_QUOTES, 'UTF-8') ?>
</div>
<?php else: ?>
<span class="empty-text"></span>
<?php endif; ?>
</td>
<td><?= $sortOrder ?></td>
<td>
<?php if ($isActive === 1): ?>
<span class="badge-status active">Attiva</span>
<?php else: ?>
<span class="badge-status inactive">Non attiva</span>
<?php endif; ?>
</td>
<td>
<button
type="button"
class="btn btn-sm btn-outline-secondary edit-function mb-1"
data-id="<?= $id ?>"
data-function_name="<?= htmlspecialchars($functionName, ENT_QUOTES, 'UTF-8') ?>"
data-person_full_name="<?= htmlspecialchars($personFullName, ENT_QUOTES, 'UTF-8') ?>"
data-phone="<?= htmlspecialchars($phone, ENT_QUOTES, 'UTF-8') ?>"
data-email="<?= htmlspecialchars($email, ENT_QUOTES, 'UTF-8') ?>"
data-notes="<?= htmlspecialchars($notes, ENT_QUOTES, 'UTF-8') ?>"
data-sort_order="<?= $sortOrder ?>"
data-is_active="<?= $isActive ?>">
✏️ Modifica
</button>
<button
type="button"
class="btn btn-sm btn-outline-danger delete-function mb-1"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($functionName, ENT_QUOTES, 'UTF-8') ?>">
🗑️ Cancella
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<!-- MODALE ADD / EDIT FUNZIONE -->
<div class="modal fade" id="companyFunctionModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title" id="companyFunctionModalTitle">Aggiungi Funzione</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="companyFunctionForm">
<input type="hidden" id="functionId">
<div class="mb-3">
<label class="form-label fw-semibold">Nome funzione <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="functionName" placeholder="Es. RSPP, Medico del lavoro, RLS" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Nome e Cognome persona</label>
<input type="text" class="form-control" id="personFullName" placeholder="Es. Mario Rossi">
</div>
<div class="row">
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Telefono</label>
<input type="text" class="form-control" id="phone" placeholder="Es. +39 333 1234567">
</div>
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Email</label>
<input type="email" class="form-control" id="email" placeholder="nome@azienda.it">
</div>
</div>
<div class="row">
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Ordine</label>
<input type="number" class="form-control" id="sortOrder" value="0" min="0" step="1">
<small class="text-muted">Serve solo per ordinare la visualizzazione.</small>
</div>
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Stato</label>
<select class="form-select" id="isActive">
<option value="1">Attiva</option>
<option value="0">Non attiva</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Note</label>
<textarea class="form-control" id="notes" rows="3" placeholder="Note interne, riferimenti, disponibilità, ecc."></textarea>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add">💾 Salva</button>
</div>
</form>
</div>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
function escapeHtml(value) {
return $('<div>').text(value || '').html();
}
function openCompanyFunctionModal() {
$('#functionId').val('');
$('#functionName').val('');
$('#personFullName').val('');
$('#phone').val('');
$('#email').val('');
$('#notes').val('');
$('#sortOrder').val('0');
$('#isActive').val('1');
$('#companyFunctionModalTitle').text('Aggiungi Funzione');
}
$(document).ready(function() {
$('#tabCompanyFunctions').DataTable({
order: [
[5, 'asc'],
[4, 'asc'],
[0, 'asc']
],
pageLength: 25,
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json',
emptyTable: 'Nessuna funzione presente'
},
columnDefs: [{
targets: -1,
orderable: false,
searchable: false
}]
});
$(document).on('click', '.edit-function', function() {
const btn = $(this);
$('#functionId').val(btn.data('id'));
$('#functionName').val(btn.data('function_name'));
$('#personFullName').val(btn.data('person_full_name'));
$('#phone').val(btn.data('phone'));
$('#email').val(btn.data('email'));
$('#notes').val(btn.data('notes'));
$('#sortOrder').val(btn.data('sort_order'));
$('#isActive').val(String(btn.data('is_active')));
$('#companyFunctionModalTitle').text('Modifica Funzione');
$('#companyFunctionModal').modal('show');
});
$('#companyFunctionForm').on('submit', function(e) {
e.preventDefault();
const id = $('#functionId').val();
const payload = new URLSearchParams();
payload.append('ajax', '1');
payload.append('action', id ? 'edit' : 'add');
payload.append('id', id);
payload.append('function_name', $('#functionName').val().trim());
payload.append('person_full_name', $('#personFullName').val().trim());
payload.append('phone', $('#phone').val().trim());
payload.append('email', $('#email').val().trim());
payload.append('notes', $('#notes').val().trim());
payload.append('sort_order', $('#sortOrder').val() || '0');
payload.append('is_active', $('#isActive').val());
fetch('', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: payload.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: 'success',
title: 'Salvato!',
text: data.message || 'Operazione completata.',
confirmButtonColor: '#3085d6'
}).then(() => location.reload());
} else {
Swal.fire('Errore', data.message || 'Impossibile salvare.', 'error');
}
})
.catch(err => {
console.error(err);
Swal.fire('Errore', 'Errore di comunicazione.', 'error');
});
});
$(document).on('click', '.delete-function', function() {
const id = $(this).data('id');
const name = $(this).data('name');
Swal.fire({
title: 'Confermi la cancellazione?',
text: name ? ('Funzione: ' + name) : 'La funzione verrà cancellata.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#6c757d',
confirmButtonText: 'Sì, cancella',
cancelButtonText: 'Annulla'
}).then((result) => {
if (!result.isConfirmed) return;
const payload = new URLSearchParams();
payload.append('ajax', '1');
payload.append('action', 'delete');
payload.append('id', id);
fetch('', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: payload.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: 'success',
title: 'Cancellato!',
text: data.message || 'Funzione cancellata.',
confirmButtonColor: '#3085d6'
}).then(() => location.reload());
} else {
Swal.fire('Errore', data.message || 'Impossibile cancellare.', 'error');
}
})
.catch(err => {
console.error(err);
Swal.fire('Errore', 'Errore di comunicazione.', 'error');
});
});
});
});
</script>
</body>
</html>
@@ -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>';
}
+798
View File
@@ -0,0 +1,798 @@
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
/* ==========================================
AJAX HANDLERS
========================================== */
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['ajax'] == '1') {
header('Content-Type: application/json');
$action = $_POST['action'] ?? '';
try {
if ($action === 'add') {
$name = trim($_POST['name'] ?? '');
$code = trim($_POST['code'] ?? '');
$description = trim($_POST['description'] ?? '');
$color = trim($_POST['color'] ?? '#6c757d');
$sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 999;
$is_active = isset($_POST['is_active']) ? (int)$_POST['is_active'] : 1;
if ($name === '') {
echo json_encode([
'success' => false,
'message' => 'Department name is required.'
]);
exit;
}
if ($code === '') {
$code = strtoupper(str_replace(' ', '_', $name));
$code = preg_replace('/[^A-Z0-9_]/', '', $code);
} else {
$code = strtoupper($code);
$code = preg_replace('/[^A-Z0-9_]/', '', $code);
}
if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $color)) {
$color = '#6c757d';
}
$is_active = $is_active === 1 ? 1 : 0;
$check = $pdo->prepare("
SELECT COUNT(*)
FROM departments
WHERE name = :name OR code = :code
");
$check->execute([
'name' => $name,
'code' => $code
]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode([
'success' => false,
'message' => 'A department with the same name or code already exists.'
]);
exit;
}
$sql = "INSERT INTO departments
(name, code, description, color, sort_order, is_active, created_at, updated_at)
VALUES
(:name, :code, :description, :color, :sort_order, :is_active, NOW(), NOW())";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'name' => $name,
'code' => $code !== '' ? $code : null,
'description' => $description !== '' ? $description : null,
'color' => $color,
'sort_order' => $sort_order,
'is_active' => $is_active
]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'edit') {
$id = (int)($_POST['id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$code = trim($_POST['code'] ?? '');
$description = trim($_POST['description'] ?? '');
$color = trim($_POST['color'] ?? '#6c757d');
$sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 999;
$is_active = isset($_POST['is_active']) ? (int)$_POST['is_active'] : 1;
if ($id <= 0) {
echo json_encode([
'success' => false,
'message' => 'Invalid department ID.'
]);
exit;
}
if ($name === '') {
echo json_encode([
'success' => false,
'message' => 'Department name is required.'
]);
exit;
}
if ($code === '') {
$code = strtoupper(str_replace(' ', '_', $name));
$code = preg_replace('/[^A-Z0-9_]/', '', $code);
} else {
$code = strtoupper($code);
$code = preg_replace('/[^A-Z0-9_]/', '', $code);
}
if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $color)) {
$color = '#6c757d';
}
$is_active = $is_active === 1 ? 1 : 0;
$check = $pdo->prepare("
SELECT COUNT(*)
FROM departments
WHERE (name = :name OR code = :code)
AND id <> :id
");
$check->execute([
'name' => $name,
'code' => $code,
'id' => $id
]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode([
'success' => false,
'message' => 'Another department with the same name or code already exists.'
]);
exit;
}
$sql = "UPDATE departments
SET name = :name,
code = :code,
description = :description,
color = :color,
sort_order = :sort_order,
is_active = :is_active,
updated_at = NOW()
WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'name' => $name,
'code' => $code !== '' ? $code : null,
'description' => $description !== '' ? $description : null,
'color' => $color,
'sort_order' => $sort_order,
'is_active' => $is_active,
'id' => $id
]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'delete') {
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode([
'success' => false,
'message' => 'Invalid department ID.'
]);
exit;
}
/*
* Future-proof check:
* If later you add employees.department_id, this prevents deleting
* a department already used by employees.
*/
$columnCheck = $pdo->prepare("
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'employees'
AND COLUMN_NAME = 'department_id'
");
$columnCheck->execute();
$hasDepartmentId = (int)$columnCheck->fetchColumn() > 0;
if ($hasDepartmentId) {
$usageCheck = $pdo->prepare("
SELECT COUNT(*)
FROM employees
WHERE department_id = :id
");
$usageCheck->execute(['id' => $id]);
if ((int)$usageCheck->fetchColumn() > 0) {
echo json_encode([
'success' => false,
'message' => 'This department is linked to one or more employees and cannot be deleted.'
]);
exit;
}
}
$stmt = $pdo->prepare("DELETE FROM departments WHERE id = :id");
$stmt->execute(['id' => $id]);
echo json_encode(['success' => true]);
exit;
}
echo json_encode([
'success' => false,
'message' => 'Unknown action.'
]);
exit;
} catch (Exception $e) {
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
exit;
}
}
/* ==========================================
PAGE DATA
========================================== */
$sql = "
SELECT *
FROM departments
ORDER BY sort_order ASC, name ASC
";
$stmtDepartments = $pdo->query($sql);
$departments = $stmtDepartments->fetchAll(PDO::FETCH_ASSOC);
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>Gestione Departments - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<!-- jQuery and Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<style>
body {
font-size: 1.05rem;
background: #f8fafc;
}
.card {
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.back-dashboard {
background-color: #cfe3ff !important;
color: #1f2d3d !important;
border: 1px solid #bcd4f4 !important;
border-radius: 10px;
font-weight: 600;
font-size: 1rem;
padding: 10px 18px;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease-in-out;
}
.back-dashboard:hover {
background-color: #b9d3ff !important;
transform: translateY(-2px);
}
.btn-add {
background-color: #0d6efd;
color: #fff;
border-radius: 8px;
padding: 10px 20px;
font-weight: 500;
transition: all 0.2s ease-in-out;
}
.btn-add:hover {
background-color: #0b5ed7;
transform: scale(1.02);
}
.table thead {
background-color: #cfe3ff;
color: #1f2d3d;
}
.modal-content {
border-radius: 16px;
}
#tabellaDepartments thead th {
text-align: center;
vertical-align: middle;
}
.badge-status {
padding: 0.25rem 0.6rem;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 600;
}
.badge-status.active {
background-color: #d1fae5;
color: #065f46;
}
.badge-status.inactive {
background-color: #e5e7eb;
color: #374151;
}
.department-color-dot {
display: inline-block;
width: 18px;
height: 18px;
border-radius: 50%;
border: 1px solid rgba(0, 0, 0, 0.2);
vertical-align: middle;
margin-right: 6px;
}
.department-code {
font-family: Consolas, Monaco, monospace;
font-size: 0.9rem;
background: #f1f5f9;
padding: 4px 8px;
border-radius: 8px;
color: #334155;
}
.description-cell {
max-width: 320px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
}
</style>
</head>
<body>
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card p-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Gestione Departments</h5>
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
↩️ Torna alla Dashboard
</button>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-semibold mb-0">Elenco Reparti / Departments</h6>
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addDepartmentModal">
Aggiungi Department
</button>
</div>
<div class="table-responsive">
<table id="tabellaDepartments" class="table table-striped align-middle text-center" style="width:100%;">
<thead>
<tr>
<th>ID</th>
<th>Color</th>
<th>Name</th>
<th>Code</th>
<th>Description</th>
<th>Order</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php if (!empty($departments)): ?>
<?php foreach ($departments as $row): ?>
<?php
$id = (int)$row['id'];
$name = $row['name'] ?? '';
$code = $row['code'] ?? '';
$description = $row['description'] ?? '';
$color = $row['color'] ?? '#6c757d';
$sortOrder = (int)($row['sort_order'] ?? 999);
$isActive = (int)($row['is_active'] ?? 1);
$statusClass = $isActive === 1 ? 'active' : 'inactive';
$statusLabel = $isActive === 1 ? 'Active' : 'Inactive';
$createdAt = !empty($row['created_at'])
? date('d/m/Y H:i', strtotime($row['created_at']))
: '-';
?>
<tr>
<td><?= $id ?></td>
<td>
<span class="department-color-dot" style="background-color: <?= htmlspecialchars($color, ENT_QUOTES) ?>;"></span>
<?= htmlspecialchars($color) ?>
</td>
<td class="fw-semibold">
<?= htmlspecialchars($name) ?>
</td>
<td>
<?php if ($code !== ''): ?>
<span class="department-code"><?= htmlspecialchars($code) ?></span>
<?php else: ?>
-
<?php endif; ?>
</td>
<td class="description-cell" title="<?= htmlspecialchars($description, ENT_QUOTES) ?>">
<?= $description !== '' ? htmlspecialchars($description) : '-' ?>
</td>
<td><?= $sortOrder ?></td>
<td>
<span class="badge-status <?= $statusClass ?>">
<?= htmlspecialchars($statusLabel) ?>
</span>
</td>
<td><?= $createdAt ?></td>
<td>
<button
class="btn btn-sm btn-outline-secondary edit-department"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
data-code="<?= htmlspecialchars($code, ENT_QUOTES) ?>"
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
data-color="<?= htmlspecialchars($color, ENT_QUOTES) ?>"
data-sort_order="<?= $sortOrder ?>"
data-is_active="<?= $isActive ?>">
✏️ Modifica
</button>
<button
class="btn btn-sm btn-outline-danger delete-department"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>">
🗑️ Cancella
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<!-- ADD DEPARTMENT MODAL -->
<div class="modal fade" id="addDepartmentModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">Aggiungi Department</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addDepartmentForm">
<div class="mb-3">
<label class="form-label fw-semibold">Name</label>
<input type="text" class="form-control" id="addName" name="name" placeholder="e.g. Produzione" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Code</label>
<input type="text" class="form-control" id="addCode" name="code" placeholder="Optional, e.g. PRODUZIONE">
<small class="text-muted">If empty, it will be generated automatically from the name.</small>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Description</label>
<textarea class="form-control" id="addDescription" name="description" rows="3" placeholder="Optional notes"></textarea>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Color</label>
<input type="color" class="form-control form-control-color" id="addColor" name="color" value="#6c757d">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Sort Order</label>
<input type="number" class="form-control" id="addSortOrder" name="sort_order" value="999" min="0">
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Status</label>
<select class="form-select" id="addIsActive" name="is_active">
<option value="1" selected>Active</option>
<option value="0">Inactive</option>
</select>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add">💾 Save</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- EDIT DEPARTMENT MODAL -->
<div class="modal fade" id="editDepartmentModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">Modifica Department</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editDepartmentForm">
<input type="hidden" id="editDepartmentId">
<div class="mb-3">
<label class="form-label fw-semibold">Name</label>
<input type="text" class="form-control" id="editName" name="name" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Code</label>
<input type="text" class="form-control" id="editCode" name="code">
<small class="text-muted">If empty, it will be generated automatically from the name.</small>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Description</label>
<textarea class="form-control" id="editDescription" name="description" rows="3"></textarea>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Color</label>
<input type="color" class="form-control form-control-color" id="editColor" name="color" value="#6c757d">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Sort Order</label>
<input type="number" class="form-control" id="editSortOrder" name="sort_order" value="999" min="0">
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Status</label>
<select class="form-select" id="editIsActive" name="is_active">
<option value="1">Active</option>
<option value="0">Inactive</option>
</select>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add">💾 Save Changes</button>
</div>
</form>
</div>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
$(document).ready(function() {
$('#tabellaDepartments').DataTable({
order: [
[5, 'asc'],
[2, 'asc']
],
pageLength: 25,
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json',
emptyTable: 'Nessun department presente'
}
});
/* -------- ADD DEPARTMENT -------- */
$("#addDepartmentForm").on("submit", function(e) {
e.preventDefault();
const payload = new URLSearchParams();
payload.append('ajax', '1');
payload.append('action', 'add');
payload.append('name', $("#addName").val().trim());
payload.append('code', $("#addCode").val().trim());
payload.append('description', $("#addDescription").val().trim());
payload.append('color', $("#addColor").val());
payload.append('sort_order', $("#addSortOrder").val());
payload.append('is_active', $("#addIsActive").val());
fetch("", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: payload.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: "success",
title: "Saved!",
confirmButtonColor: "#3085d6"
}).then(() => location.reload());
} else {
Swal.fire({
icon: "error",
title: "Error",
text: data.message || "Unable to save department."
});
}
})
.catch(err => {
Swal.fire({
icon: "error",
title: "Error",
text: "Communication error."
});
console.error(err);
});
});
/* -------- OPEN EDIT MODAL -------- */
$(document).on("click", ".edit-department", function() {
const btn = $(this);
$("#editDepartmentId").val(btn.data("id"));
$("#editName").val(btn.data("name"));
$("#editCode").val(btn.data("code"));
$("#editDescription").val(btn.data("description"));
$("#editColor").val(btn.data("color") || '#6c757d');
$("#editSortOrder").val(btn.data("sort_order"));
$("#editIsActive").val(String(btn.data("is_active")));
$("#editDepartmentModal").modal("show");
});
/* -------- SAVE EDIT -------- */
$("#editDepartmentForm").on("submit", function(e) {
e.preventDefault();
const payload = new URLSearchParams();
payload.append('ajax', '1');
payload.append('action', 'edit');
payload.append('id', $("#editDepartmentId").val());
payload.append('name', $("#editName").val().trim());
payload.append('code', $("#editCode").val().trim());
payload.append('description', $("#editDescription").val().trim());
payload.append('color', $("#editColor").val());
payload.append('sort_order', $("#editSortOrder").val());
payload.append('is_active', $("#editIsActive").val());
fetch("", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: payload.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: "success",
title: "Updated!",
confirmButtonColor: "#3085d6"
}).then(() => location.reload());
} else {
Swal.fire({
icon: "error",
title: "Error",
text: data.message || "Unable to update department."
});
}
})
.catch(err => {
Swal.fire({
icon: "error",
title: "Error",
text: "Communication error."
});
console.error(err);
});
});
/* -------- DELETE DEPARTMENT -------- */
$(document).on("click", ".delete-department", function() {
const id = $(this).data("id");
const name = $(this).data("name");
Swal.fire({
title: "Confermi la cancellazione?",
text: name ? ("Department: " + name) : "This department will be deleted.",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#d33",
cancelButtonColor: "#6c757d",
confirmButtonText: "Sì, cancella",
cancelButtonText: "Annulla"
}).then((result) => {
if (!result.isConfirmed) return;
const payload = new URLSearchParams();
payload.append('ajax', '1');
payload.append('action', 'delete');
payload.append('id', id);
fetch("", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: payload.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: "success",
title: "Deleted!",
confirmButtonColor: "#3085d6"
}).then(() => location.reload());
} else {
Swal.fire({
icon: "error",
title: "Error",
text: data.message || "Unable to delete department."
});
}
})
.catch(err => {
Swal.fire({
icon: "error",
title: "Error",
text: "Communication error."
});
console.error(err);
});
});
});
});
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+13 -11
View File
@@ -7,16 +7,21 @@ ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL | E_STRICT);
// This should be equal to: PATH_TO_VANGUARD_FOLDER/extra/auth.php
include('../../extra/auth.php');
include(__DIR__ . '/../../../extra/auth.php');
//require_once __DIR__ . '/extra/auth.php';
// Here we just check if user is not
// Here we just check if user is not
// logged in, and in that case we redirect
// the user to vanguard login page.
if (! Auth::check()) {
redirectTo('../../public/login');
// Cut everything at /userarea/ and append /login.
$scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
$basePath = substr($scriptName, 0, strpos($scriptName, '/userarea/'));
if ($basePath === false || $basePath === '') {
$basePath = '';
}
redirectTo($basePath . '/login');
}
$user = Auth::user();
@@ -35,8 +40,7 @@ $kindofrole = $user->present()->role_id;
//$iduserlogin="1";
//$nameuser="Claudio";
//$emailuser="info@claudiosironi.com";
?>
<?php
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
@@ -49,13 +53,11 @@ $_SESSION["emailuser"] = $emailuser;
$_SESSION["photouser"] = $avatar;
$photouser = $_SESSION["photouser"];
$photousername = basename($avatar);
?>
<?php //include files
//include files
require_once(__DIR__ . '/../../languages/en/general.php');
//include("generalsettings.php");
?>
require_once __DIR__ . '/permissions_helper.php';
?>
+338 -51
View File
@@ -6,101 +6,388 @@
<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>
<li>
<a href="job-roles.php">
<i class='bx bx-radio-circle'></i>Mansioni
</a>
</li>
<li>
<a href="ppe-items.php">
<i class='bx bx-radio-circle'></i>DPI
</a>
</li>
<?php endif; ?>
<?php if (userCan('hr.departments.view')) : ?>
<li>
<a href="departments.php">
<i class='bx bx-radio-circle'></i>Reparti
</a>
</li>
<?php endif; ?>
<?php if (userCan('hr.trainings.view')) : ?>
<li>
<a href="trainings.php">
<i class='bx bx-radio-circle'></i>Gestione Formazione
</a>
</li>
<?php endif; ?>
<?php if (userCan('hr.skills.view')) : ?>
<li>
<a href="skills.php">
<i class='bx bx-radio-circle'></i>Skills
</a>
</li>
<?php endif; ?>
</ul>
</li>
<?php endif; ?>
<?php if (userCan('deadlines.view')) : ?>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon">
<i class="bx bx-calendar-check"></i>
</div>
<div class="menu-title">Scadenzario</div>
</a>
<ul>
<li>
<a href="scadenzario/index.php">
<i class='bx bx-radio-circle'></i>Lista Scadenze
</a>
</li>
<li>
<a href="scadenzario/calendar.php">
<i class='bx bx-radio-circle'></i>Calendario
</a>
</li>
<li>
<a href="scadenzario/functions/index.php">
<i class='bx bx-radio-circle'></i>Funzioni Aziendali
</a>
</li>
</ul>
</li>
<?php endif; ?>
<li class="menu-label">Others</li>
<li>
<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>
@@ -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']);
}));
}
}
+16 -2
View File
@@ -1,3 +1,11 @@
<?php
// Build an absolute URL to employee-profile.php so it works from any depth
// (e.g. /userarea/index.php, /userarea/scadenzario/index.php).
$__scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
$__pos = strpos($__scriptName, '/userarea/');
$__base = $__pos !== false ? substr($__scriptName, 0, $__pos) : '';
$__myProfileHref = $__base . '/userarea/employee-profile.php';
?>
<header>
<div class="topbar d-flex align-items-center">
<nav class="navbar navbar-expand gap-3">
@@ -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>
+101
View File
@@ -0,0 +1,101 @@
<?php
/**
* Training reminders widget for the production dashboard.
* Visible to HR / manager / Admin / User / Superuser.
*
* Expects $pdo to be set (DBHandlerSelect connection).
*/
if (!isset($pdo)) {
$pdo = DBHandlerSelect::getInstance()->getConnection();
}
$__trWidgetHr = isset($user)
&& ( $user->hasRole('Admin')
|| $user->hasRole('Superuser')
|| $user->hasRole('employee-hr')
|| $user->hasRole('manager'));
if (!$__trWidgetHr) {
return;
}
/* Only the most recent record per (employee, topic) — older history rows ignored. */
$__trRows = $pdo->query("
SELECT et.id,
et.next_due_date,
et.reminder_days,
tt.default_reminder_days
FROM employee_trainings et
JOIN training_topics tt ON tt.id = et.training_topic_id
WHERE et.next_due_date IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM employee_trainings et2
WHERE et2.employee_id = et.employee_id
AND et2.training_topic_id = et.training_topic_id
AND (et2.completed_date > et.completed_date
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
)
")->fetchAll(PDO::FETCH_ASSOC);
$__expiredCount = 0;
$__dueSoonCount = 0;
$__today = new DateTime('today');
foreach ($__trRows as $__r) {
$__rem = $__r['reminder_days'] !== null
? (int)$__r['reminder_days']
: ($__r['default_reminder_days'] !== null ? (int)$__r['default_reminder_days'] : 30);
$__due = DateTime::createFromFormat('Y-m-d', $__r['next_due_date']);
if (!$__due) continue;
$__days = (int)$__today->diff($__due)->format('%r%a');
if ($__days < 0) { $__expiredCount++; }
elseif ($__days <= $__rem) { $__dueSoonCount++; }
}
/* Missing mandatory trainings (status = not_present) */
$__notPresentCount = (int)$pdo->query("
SELECT COUNT(*)
FROM employees e
CROSS JOIN training_topics tt
WHERE tt.is_active = 1 AND tt.is_mandatory = 1
AND NOT EXISTS (
SELECT 1 FROM employee_trainings et
WHERE et.employee_id = e.id AND et.training_topic_id = tt.id
)
")->fetchColumn();
if ($__expiredCount === 0 && $__dueSoonCount === 0 && $__notPresentCount === 0) {
return;
}
?>
<div class="my-deadlines-widgets">
<?php if ($__expiredCount > 0): ?>
<a class="mdw mdw-red" href="trainings.php?status=expired">
<span class="mdw-icon"><i class="fa-solid fa-graduation-cap"></i></span>
<span class="mdw-body">
<span class="mdw-count"><?= (int)$__expiredCount ?></span>
<span class="mdw-label d-block">Formazion<?= $__expiredCount === 1 ? 'e scaduta' : 'i scadute' ?></span>
</span>
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
</a>
<?php endif; ?>
<?php if ($__dueSoonCount > 0): ?>
<a class="mdw mdw-orange" href="trainings.php?status=due_soon">
<span class="mdw-icon"><i class="fa-solid fa-hourglass-half"></i></span>
<span class="mdw-body">
<span class="mdw-count"><?= (int)$__dueSoonCount ?></span>
<span class="mdw-label d-block">Formazion<?= $__dueSoonCount === 1 ? 'e da aggiornare' : 'i da aggiornare' ?></span>
</span>
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
</a>
<?php endif; ?>
<?php if ($__notPresentCount > 0): ?>
<a class="mdw mdw-gray" href="trainings.php?status=not_present">
<span class="mdw-icon"><i class="fa-solid fa-circle-question"></i></span>
<span class="mdw-body">
<span class="mdw-count"><?= (int)$__notPresentCount ?></span>
<span class="mdw-label d-block">Obbligator<?= $__notPresentCount === 1 ? 'ia non presente' : 'ie non presenti' ?></span>
</span>
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
</a>
<?php endif; ?>
</div>
File diff suppressed because it is too large Load Diff
+428
View File
@@ -0,0 +1,428 @@
<?php
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
/* ==========================================
PAGE DATA
========================================== */
$sql = "
SELECT jr.*,
(SELECT COUNT(*) FROM employees e WHERE e.job_role_id = jr.id) AS employees_count
FROM job_roles jr
ORDER BY jr.sort_order ASC, jr.name ASC
";
$jobRoles = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>Gestione Mansioni - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<style>
body { font-size: 1.05rem; background: #f8fafc; }
.card { border-radius: 16px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); }
.back-dashboard {
background-color: #cfe3ff !important; color: #1f2d3d !important;
border: 1px solid #bcd4f4 !important; border-radius: 10px;
font-weight: 600; padding: 10px 18px;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease-in-out;
}
.back-dashboard:hover { background-color: #b9d3ff !important; transform: translateY(-2px); }
.btn-add { background-color: #0d6efd; color: #fff; border-radius: 8px; padding: 10px 20px; font-weight: 500; }
.btn-add:hover { background-color: #0b5ed7; transform: scale(1.02); }
.table thead { background-color: #cfe3ff; color: #1f2d3d; }
.modal-content { border-radius: 16px; }
#tabellaJobRoles thead th { text-align: center; vertical-align: middle; }
.badge-status { padding: 0.25rem 0.6rem; border-radius: 999px; font-size: 0.8rem; font-weight: 600; }
.badge-status.active { background-color: #d1fae5; color: #065f46; }
.badge-status.inactive { background-color: #e5e7eb; color: #374151; }
.description-cell {
max-width: 320px; white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; text-align: left;
}
@media (max-width: 767.98px) {
.card-header { flex-direction: column; align-items: flex-start !important; gap: .5rem; }
.back-dashboard { width: 100%; }
.btn-add { width: 100%; }
}
.jr-card {
border: 1px solid #e2e8f0;
border-radius: 14px;
padding: 14px 16px;
margin-bottom: 12px;
background: #fff;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
}
.jr-card-title {
font-size: 1.1rem;
font-weight: 600;
color: #1f2937;
margin: 0 0 4px 0;
word-break: break-word;
}
.jr-card-desc {
color: #475569;
font-size: 0.95rem;
margin: 0 0 10px 0;
word-break: break-word;
}
.jr-card-meta {
display: flex;
flex-wrap: wrap;
gap: 8px 14px;
font-size: 0.85rem;
color: #64748b;
margin-bottom: 12px;
}
.jr-card-meta b { color: #1f2937; font-weight: 600; }
.jr-card-actions {
display: flex;
gap: 8px;
}
.jr-card-actions .btn {
flex: 1;
}
.jr-empty {
text-align: center;
color: #94a3b8;
padding: 24px 0;
}
</style>
</head>
<body>
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card p-3">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h5 class="mb-0">Gestione Mansioni</h5>
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
↩️ Torna alla Dashboard
</button>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<h6 class="fw-semibold mb-0">Elenco Mansioni / Job Roles</h6>
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addJobRoleModal">
Aggiungi Mansione
</button>
</div>
<!-- DESKTOP / TABLET ≥768px: TABLE -->
<div class="table-responsive d-none d-md-block"><!-- hide on <md -->
<table id="tabellaJobRoles" class="table table-striped align-middle text-center" style="width:100%;">
<thead>
<tr>
<th>ID</th>
<th>Nome</th>
<th>Descrizione</th>
<th>Ordine</th>
<th>Stato</th>
<th>Dipendenti</th>
<th>Creato</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($jobRoles as $row): ?>
<?php
$id = (int)$row['id'];
$name = $row['name'] ?? '';
$description = $row['description'] ?? '';
$sortOrder = (int)($row['sort_order'] ?? 999);
$isActive = (int)($row['is_active'] ?? 1);
$cnt = (int)($row['employees_count'] ?? 0);
$statusClass = $isActive === 1 ? 'active' : 'inactive';
$statusLabel = $isActive === 1 ? 'Attivo' : 'Inattivo';
$createdAt = !empty($row['created_at'])
? date('d/m/Y H:i', strtotime($row['created_at']))
: '-';
?>
<tr>
<td><?= $id ?></td>
<td class="fw-semibold text-start"><?= htmlspecialchars($name) ?></td>
<td class="description-cell" title="<?= htmlspecialchars($description, ENT_QUOTES) ?>">
<?= $description !== '' ? htmlspecialchars($description) : '-' ?>
</td>
<td><?= $sortOrder ?></td>
<td>
<span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span>
</td>
<td><?= $cnt ?></td>
<td><?= $createdAt ?></td>
<td>
<button class="btn btn-sm btn-outline-secondary edit-job-role"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
data-sort_order="<?= $sortOrder ?>"
data-is_active="<?= $isActive ?>">
✏️ Modifica
</button>
<button class="btn btn-sm btn-outline-danger delete-job-role"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
data-count="<?= $cnt ?>">
🗑️ Cancella
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- MOBILE <768px: CARDS -->
<div class="d-block d-md-none">
<?php if (empty($jobRoles)): ?>
<div class="jr-empty">Nessuna mansione presente</div>
<?php endif; ?>
<?php foreach ($jobRoles as $row): ?>
<?php
$id = (int)$row['id'];
$name = $row['name'] ?? '';
$description = $row['description'] ?? '';
$sortOrder = (int)($row['sort_order'] ?? 999);
$isActive = (int)($row['is_active'] ?? 1);
$cnt = (int)($row['employees_count'] ?? 0);
$statusClass = $isActive === 1 ? 'active' : 'inactive';
$statusLabel = $isActive === 1 ? 'Attivo' : 'Inattivo';
?>
<div class="jr-card">
<h6 class="jr-card-title"><?= htmlspecialchars($name) ?></h6>
<?php if ($description !== ''): ?>
<p class="jr-card-desc"><?= htmlspecialchars($description) ?></p>
<?php endif; ?>
<div class="jr-card-meta">
<span><span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span></span>
<span><b>Dipendenti:</b> <?= $cnt ?></span>
<span><b>Ordine:</b> <?= $sortOrder ?></span>
</div>
<div class="jr-card-actions">
<button class="btn btn-sm btn-outline-secondary edit-job-role"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
data-sort_order="<?= $sortOrder ?>"
data-is_active="<?= $isActive ?>">
✏️ Modifica
</button>
<button class="btn btn-sm btn-outline-danger delete-job-role"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
data-count="<?= $cnt ?>">
🗑️ Cancella
</button>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<!-- ADD MODAL -->
<div class="modal fade" id="addJobRoleModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-fullscreen-sm-down">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">Aggiungi Mansione</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addJobRoleForm">
<div class="mb-3">
<label class="form-label fw-semibold">Nome</label>
<input type="text" class="form-control" id="addName" name="name" placeholder="es. Saldatore" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Descrizione</label>
<textarea class="form-control" id="addDescription" name="description" rows="3" placeholder="Opzionale"></textarea>
</div>
<div class="row">
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Ordine</label>
<input type="number" class="form-control" id="addSortOrder" name="sort_order" value="999" min="0">
</div>
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Stato</label>
<select class="form-select" id="addIsActive" name="is_active">
<option value="1" selected>Attivo</option>
<option value="0">Inattivo</option>
</select>
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add">💾 Salva</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- EDIT MODAL -->
<div class="modal fade" id="editJobRoleModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-fullscreen-sm-down">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">Modifica Mansione</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editJobRoleForm">
<input type="hidden" id="editJobRoleId">
<div class="mb-3">
<label class="form-label fw-semibold">Nome</label>
<input type="text" class="form-control" id="editName" name="name" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Descrizione</label>
<textarea class="form-control" id="editDescription" name="description" rows="3"></textarea>
</div>
<div class="row">
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Ordine</label>
<input type="number" class="form-control" id="editSortOrder" name="sort_order" value="999" min="0">
</div>
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Stato</label>
<select class="form-select" id="editIsActive" name="is_active">
<option value="1">Attivo</option>
<option value="0">Inattivo</option>
</select>
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add">💾 Salva Modifiche</button>
</div>
</form>
</div>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
$(document).ready(function() {
$('#tabellaJobRoles').DataTable({
order: [[3, 'asc'], [1, 'asc']],
pageLength: 25,
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json',
emptyTable: 'Nessuna mansione presente'
}
});
function ajaxPost(url, payload, successTitle, errorFallback) {
return fetch(url, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: payload.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({ icon: "success", title: successTitle, confirmButtonColor: "#3085d6" })
.then(() => location.reload());
} else {
Swal.fire({ icon: "error", title: "Errore", text: data.message || errorFallback });
}
})
.catch(err => {
Swal.fire({ icon: "error", title: "Errore", text: "Errore di comunicazione." });
console.error(err);
});
}
$("#addJobRoleForm").on("submit", function(e) {
e.preventDefault();
const p = new URLSearchParams();
p.append('name', $("#addName").val().trim());
p.append('description', $("#addDescription").val().trim());
p.append('sort_order', $("#addSortOrder").val());
p.append('is_active', $("#addIsActive").val());
ajaxPost("ajax/job_roles/save.php", p, "Salvato!", "Impossibile salvare la mansione.");
});
$(document).on("click", ".edit-job-role", function() {
const b = $(this);
$("#editJobRoleId").val(b.data("id"));
$("#editName").val(b.data("name"));
$("#editDescription").val(b.data("description"));
$("#editSortOrder").val(b.data("sort_order"));
$("#editIsActive").val(String(b.data("is_active")));
$("#editJobRoleModal").modal("show");
});
$("#editJobRoleForm").on("submit", function(e) {
e.preventDefault();
const p = new URLSearchParams();
p.append('id', $("#editJobRoleId").val());
p.append('name', $("#editName").val().trim());
p.append('description', $("#editDescription").val().trim());
p.append('sort_order', $("#editSortOrder").val());
p.append('is_active', $("#editIsActive").val());
ajaxPost("ajax/job_roles/save.php", p, "Aggiornato!", "Impossibile aggiornare la mansione.");
});
$(document).on("click", ".delete-job-role", function() {
const id = $(this).data("id");
const name = $(this).data("name");
const cnt = parseInt($(this).data("count")) || 0;
if (cnt > 0) {
Swal.fire({
icon: "warning",
title: "Impossibile cancellare",
text: "La mansione \"" + name + "\" è assegnata a " + cnt + " dipendente/i. Rimuovi prima l'associazione."
});
return;
}
Swal.fire({
title: "Confermi la cancellazione?",
text: name ? ("Mansione: " + name) : "La mansione verrà cancellata.",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#d33",
cancelButtonColor: "#6c757d",
confirmButtonText: "Sì, cancella",
cancelButtonText: "Annulla"
}).then((result) => {
if (!result.isConfirmed) return;
const p = new URLSearchParams();
p.append('id', id);
ajaxPost("ajax/job_roles/delete.php", p, "Cancellato!", "Impossibile cancellare la mansione.");
});
});
});
</script>
</body>
</html>
-1
View File
@@ -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 -->
+1 -2
View File
@@ -11,7 +11,6 @@
<!-- jQuery e Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables -->
@@ -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'); ?>
+1 -1
View File
@@ -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'); ?>
+1 -1
View File
@@ -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'); ?>
+1 -1
View File
@@ -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'); ?>
+1 -1
View File
@@ -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'); ?>
+1 -2
View File
@@ -11,7 +11,6 @@
<!-- jQuery e Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables -->
@@ -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'); ?>
+1 -1
View File
@@ -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'); ?>
+1 -2
View File
@@ -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'); ?>
File diff suppressed because it is too large Load Diff
-1
View File
@@ -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>
-1
View File
@@ -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">
+372 -168
View File
@@ -1,4 +1,205 @@
<?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, formazione, skill',
'icon' => '👥',
'open' => false,
'buttons' => [
[
'label' => 'Dipendenti',
'icon' => '👥',
'class' => 'btn-employees',
'url' => 'employees.php',
'permission' => 'hr.employees.view',
],
[
'label' => 'Mansioni',
'icon' => '🧩',
'class' => 'btn-setup',
'url' => 'job-roles.php',
'permission' => 'hr.employees.view',
],
[
'label' => 'Departments',
'icon' => '🏢',
'class' => 'btn-departments',
'url' => 'departments.php',
'permission' => 'hr.departments.view',
],
[
'label' => 'DPI',
'icon' => '🦺',
'class' => 'btn-setup',
'url' => 'ppe-items.php',
'permission' => 'hr.employees.view',
],
[
'label' => 'Gestione Formazione',
'icon' => '🎓',
'class' => 'btn-setup',
'url' => 'trainings.php',
'permission' => 'hr.trainings.view',
],
[
'label' => 'Skills',
'icon' => '🧠',
'class' => 'btn-setup',
'url' => 'skills.php',
'permission' => 'hr.skills.view',
],
],
],
];
?>
<!doctype html>
<html lang="it">
@@ -7,7 +208,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 +456,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 +499,120 @@
</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 +649,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>
+1 -1
View File
@@ -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'); ?>
+1 -1
View File
@@ -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,154 @@
<?php
include('../../include/headscript.php');
header('Content-Type: application/json; charset=utf-8');
$pdo = DBHandlerSelect::getInstance()->getConnection();
function jsonResponse(array $data): void
{
echo json_encode($data);
exit;
}
function normalizeNullableInt($value): ?int
{
return (isset($value) && $value !== '') ? (int)$value : null;
}
try {
$isHrManager = Auth::user()->hasRole('Admin')
|| Auth::user()->hasRole('Superuser')
|| Auth::user()->hasRole('employee-hr')
|| Auth::user()->hasRole('manager');
if (!$isHrManager) {
jsonResponse(['success' => false, 'message' => 'Non autorizzato.']);
}
$employeeId = (int)($_POST['employee_id'] ?? 0);
$firstName = trim($_POST['first_name'] ?? '');
$lastName = trim($_POST['last_name'] ?? '');
$employeeCode = trim($_POST['employee_code'] ?? '');
$hireDate = trim($_POST['hire_date'] ?? '');
$address = trim($_POST['address'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$email = trim($_POST['email'] ?? '');
$departmentId = normalizeNullableInt($_POST['department_id'] ?? '');
$status = trim($_POST['status'] ?? 'active');
$authUserId = normalizeNullableInt($_POST['auth_user_id'] ?? '');
$roleId = normalizeNullableInt($_POST['role_id'] ?? '');
$jobSubRoleIds = $_POST['job_sub_role_ids'] ?? [];
if (!is_array($jobSubRoleIds)) {
$jobSubRoleIds = [$jobSubRoleIds];
}
$jobSubRoleIds = array_values(array_unique(array_filter(array_map('intval', $jobSubRoleIds))));
if ($employeeId <= 0) {
jsonResponse(['success' => false, 'message' => 'ID dipendente non valido.']);
}
if ($firstName === '' || $lastName === '') {
jsonResponse(['success' => false, 'message' => 'Nome e cognome sono obbligatori.']);
}
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
jsonResponse(['success' => false, 'message' => 'Email non valida.']);
}
if (!in_array($status, ['active', 'inactive', 'suspended'], true)) {
$status = 'active';
}
$stmtEmployee = $pdo->prepare('SELECT id FROM employees WHERE id = ? LIMIT 1');
$stmtEmployee->execute([$employeeId]);
if (!$stmtEmployee->fetchColumn()) {
jsonResponse(['success' => false, 'message' => 'Dipendente non trovato.']);
}
$primaryJobRoleId = null;
$primaryJobSubRoleId = null;
if ($jobSubRoleIds) {
$placeholders = implode(',', array_fill(0, count($jobSubRoleIds), '?'));
$stmtSubRoles = $pdo->prepare("\n SELECT id, job_role_id\n FROM job_sub_roles\n WHERE id IN ($placeholders)\n AND is_active = 1\n ");
$stmtSubRoles->execute($jobSubRoleIds);
$validRows = $stmtSubRoles->fetchAll(PDO::FETCH_ASSOC);
$validMap = [];
foreach ($validRows as $row) {
$validMap[(int)$row['id']] = (int)$row['job_role_id'];
}
$jobSubRoleIds = array_values(array_filter($jobSubRoleIds, static function ($id) use ($validMap) {
return isset($validMap[(int)$id]);
}));
if ($jobSubRoleIds) {
$primaryJobSubRoleId = (int)$jobSubRoleIds[0];
$primaryJobRoleId = $validMap[$primaryJobSubRoleId] ?? null;
}
}
$pdo->beginTransaction();
$stmt = $pdo->prepare("\n UPDATE employees\n SET first_name = :first_name,\n last_name = :last_name,\n employee_code = :employee_code,\n hire_date = :hire_date,\n address = :address,\n phone = :phone,\n email = :email,\n department_id = :department_id,\n job_role_id = :job_role_id,\n job_sub_role_id = :job_sub_role_id,\n status = :status,\n auth_user_id = :auth_user_id,\n updated_at = NOW()\n WHERE id = :employee_id\n ");
$stmt->execute([
'first_name' => $firstName,
'last_name' => $lastName,
'employee_code' => $employeeCode !== '' ? $employeeCode : null,
'hire_date' => $hireDate !== '' ? $hireDate : null,
'address' => $address !== '' ? $address : null,
'phone' => $phone !== '' ? $phone : null,
'email' => $email !== '' ? $email : null,
'department_id' => $departmentId,
'job_role_id' => $primaryJobRoleId,
'job_sub_role_id' => $primaryJobSubRoleId,
'status' => $status,
'auth_user_id' => $authUserId,
'employee_id' => $employeeId,
]);
$stmtDelete = $pdo->prepare('DELETE FROM employee_job_sub_roles WHERE employee_id = ?');
$stmtDelete->execute([$employeeId]);
if ($jobSubRoleIds) {
$stmtInsert = $pdo->prepare("\n INSERT INTO employee_job_sub_roles\n (employee_id, job_sub_role_id, is_primary, created_at)\n VALUES\n (:employee_id, :job_sub_role_id, :is_primary, NOW())\n ");
foreach ($jobSubRoleIds as $index => $jobSubRoleId) {
$stmtInsert->execute([
'employee_id' => $employeeId,
'job_sub_role_id' => (int)$jobSubRoleId,
'is_primary' => $index === 0 ? 1 : 0,
]);
}
}
if ($authUserId !== null && $roleId !== null) {
$checkRole = $pdo->prepare('SELECT COUNT(*) FROM auth_roles WHERE id = ?');
$checkRole->execute([$roleId]);
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' => $roleId,
'auth_user_id' => $authUserId,
]);
}
}
$pdo->commit();
jsonResponse(['success' => true]);
} catch (Throwable $e) {
if (isset($pdo) && $pdo->inTransaction()) {
$pdo->rollBack();
}
jsonResponse([
'success' => false,
'message' => $e->getMessage(),
]);
}
@@ -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,139 @@
<?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;
$notify_function = isset($_POST['notify_function']) && (int)$_POST['notify_function'] === 1 ? 1 : 0;
$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 = ?, notify_function = ?, 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,
$notify_function,
$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, notify_function, 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,
$notify_function,
$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
+275
View File
@@ -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,340 @@
<?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,
d.notify_function,
f.email AS function_email,
f.person_full_name AS function_person,
f.name AS function_name
FROM scad_deadlines d
LEFT JOIN scad_subjects s ON s.id = d.subject_id
LEFT JOIN scad_functions f ON f.id = d.function_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 + optional function email)
$recipients = [];
$functionRecipient = null;
$getRecipients->execute([$dl['id']]);
foreach ($getRecipients->fetchAll(PDO::FETCH_ASSOC) as $r) {
$recipients[$r['employee_id']] = $r;
}
// Optional: also notify the linked function email if enabled on the deadline.
if (
!empty($dl['notify_function'])
&& !empty($dl['function_email'])
&& filter_var($dl['function_email'], FILTER_VALIDATE_EMAIL)
) {
$functionRecipient = [
'email' => $dl['function_email'],
'name' => trim(($dl['function_person'] ?? '') !== '' ? $dl['function_person'] : ($dl['function_name'] ?? 'Funzione')),
];
}
if (empty($recipients) && empty($functionRecipient)) {
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";
}
}
// Send notification to function email if enabled.
// It is tracked with employee_id = 0 to avoid duplicate daily sends.
if ($functionRecipient) {
$functionEmployeeId = 0;
$checkSent->execute([$dl['id'], $functionEmployeeId, $type]);
if ($checkSent->fetchColumn() > 0) {
$skipped++;
} else {
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'] ?? 'Scadenzario ZIBOGOMMA'
);
$mail->addAddress($functionRecipient['email'], $functionRecipient['name']);
if ($managerCcEmail && strcasecmp($managerCcEmail, $functionRecipient['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();
$insertNotif->execute([$dl['id'], $functionEmployeeId, $type]);
$sent++;
echo date('H:i:s') . "{$type} → funzione {$functionRecipient['email']}{$dl['topic']}\n";
} catch (Exception $e) {
$errors++;
echo date('H:i:s') . " ✗ Errore funzione {$functionRecipient['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));
if ($functionRecipient) {
$recipientNames .= ($recipientNames !== '' ? ', ' : '') . 'Funzione: ' . $functionRecipient['name'] . ' <' . $functionRecipient['email'] . '>';
}
$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>';
}
+877
View File
@@ -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()]);
}

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