Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ec5419a86 | |||
| c05091e020 | |||
| 0b470f290e | |||
| e74870c8d3 | |||
| 9001eff317 | |||
| 650676037a | |||
| 2fc34c3cf4 | |||
| 955a7ed9e9 | |||
| e6a805f1f7 | |||
| fe84d446e7 | |||
| 2ddf575191 | |||
| d73a8bb8d3 | |||
| fa2f293835 | |||
| fc35adc7f9 | |||
| ac942dcdc8 | |||
| 5728afa788 | |||
| 1946648b1b | |||
| a1bcab3188 | |||
| 2bbeb11726 | |||
| dd5edab2f3 | |||
| 1fadc22178 | |||
| d3ee9a3790 | |||
| c387b71cae | |||
| b2cfec77df | |||
| 73b9a3d890 | |||
| 0550ffe923 | |||
| d2e5cc8b2b | |||
| d7b6a58407 |
+4
-2
@@ -31,6 +31,8 @@ MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
|
||||
MANAGER_USER_ID=
|
||||
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
@@ -55,5 +57,5 @@ AZURE_REDIRECT_URI=https://your-app.com/auth/azure/callback
|
||||
AZURE_TENANT_ID=
|
||||
|
||||
MICROSOFT_CLIENT_ID=your_client_id_here
|
||||
MICROSOFT_CLIENT_SECRET=your_client_secret_here
|
||||
MICROSOFT_REDIRECT_URI="${APP_URL}/auth/microsoft/callback"
|
||||
MICROSOFT_CLIENT_SECRET=your_client_secret_here
|
||||
MICROSOFT_REDIRECT_URI="${APP_URL}/auth/microsoft/callback"
|
||||
|
||||
@@ -66,3 +66,6 @@ public/userarea/logsapi/commessaweb_customfields_763.json
|
||||
public/userarea/logsapi/commessaweb_invia_762.json
|
||||
public/userarea/logsapi/commessaweb_invia_763.json
|
||||
public/userarea/logsapi/last_auth_url.txt
|
||||
|
||||
# User uploaded files
|
||||
/public/userarea/files/
|
||||
@@ -111,6 +111,14 @@ class LoginController extends Controller
|
||||
return redirect()->to('userarea/production_dashboard.php');
|
||||
} elseif ($user->hasRole('User')) {
|
||||
return redirect()->to('userarea/production_dashboard.php');
|
||||
} elseif ($user->hasRole('HR')) {
|
||||
return redirect()->to('userarea/production_dashboard.php');
|
||||
} elseif ($user->hasRole('SuperUser')) {
|
||||
return redirect()->to('userarea/production_dashboard.php');
|
||||
} elseif ($user->hasRole('Management')) {
|
||||
return redirect()->to('userarea/production_dashboard.php');
|
||||
} elseif ($user->hasRole('Quality')) {
|
||||
return redirect()->to('userarea/production_dashboard.php');
|
||||
}
|
||||
|
||||
// Se il ruolo non è specificato, reindirizza alla home predefinita
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"phpmailer/phpmailer": "^6.9",
|
||||
"phpoffice/phpspreadsheet": "^4.1",
|
||||
"proengsoft/laravel-jsvalidation": "^4.0.0",
|
||||
"robmorgan/phinx": "^0.16.11",
|
||||
"socialiteproviders/microsoft": "^4.7",
|
||||
"spatie/laravel-query-builder": "^5.0",
|
||||
"vanguardapp/activity-log": "^6.0",
|
||||
|
||||
Generated
+646
-2
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "9c4f1e3bc3ee2180211c055e70635aef",
|
||||
"content-hash": "076e7721d08cfea8b06ce75dd8c6c576",
|
||||
"packages": [
|
||||
{
|
||||
"name": "akaunting/laravel-setting",
|
||||
@@ -251,6 +251,330 @@
|
||||
],
|
||||
"time": "2023-11-29T23:19:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "cakephp/chronos",
|
||||
"version": "3.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cakephp/chronos.git",
|
||||
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cakephp/chronos/zipball/e6e777b534244911566face8a5dbdbd7f7bda5a6",
|
||||
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"psr/clock": "^1.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/clock-implementation": "1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"cakephp/cakephp-codesniffer": "^5.0",
|
||||
"phpunit/phpunit": "^10.5.58 || ^11.5.3 || ^12.1.3"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Cake\\Chronos\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Brian Nesbitt",
|
||||
"email": "brian@nesbot.com",
|
||||
"homepage": "http://nesbot.com"
|
||||
},
|
||||
{
|
||||
"name": "The CakePHP Team",
|
||||
"homepage": "https://cakephp.org"
|
||||
}
|
||||
],
|
||||
"description": "A simple API extension for DateTime.",
|
||||
"homepage": "https://cakephp.org",
|
||||
"keywords": [
|
||||
"date",
|
||||
"datetime",
|
||||
"time"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/cakephp/chronos/issues",
|
||||
"source": "https://github.com/cakephp/chronos"
|
||||
},
|
||||
"time": "2026-04-10T02:50:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "cakephp/core",
|
||||
"version": "5.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cakephp/core.git",
|
||||
"reference": "eb012517900ed288f580aa3487e9a09f28ea85f9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cakephp/core/zipball/eb012517900ed288f580aa3487e9a09f28ea85f9",
|
||||
"reference": "eb012517900ed288f580aa3487e9a09f28ea85f9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"cakephp/utility": "^5.3.0",
|
||||
"league/container": "^5.1",
|
||||
"php": ">=8.2",
|
||||
"psr/container": "^1.1 || ^2.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/container-implementation": "^2.0"
|
||||
},
|
||||
"suggest": {
|
||||
"cakephp/cache": "To use Configure::store() and restore().",
|
||||
"cakephp/event": "To use PluginApplicationInterface or plugin applications.",
|
||||
"league/container": "To use Container and ServiceProvider classes"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-5.next": "5.4.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Cake\\Core\\": "."
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "CakePHP Community",
|
||||
"homepage": "https://github.com/cakephp/core/graphs/contributors"
|
||||
}
|
||||
],
|
||||
"description": "CakePHP Framework Core classes",
|
||||
"homepage": "https://cakephp.org",
|
||||
"keywords": [
|
||||
"cakephp",
|
||||
"core",
|
||||
"framework"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://stackoverflow.com/tags/cakephp",
|
||||
"irc": "irc://irc.freenode.org/cakephp",
|
||||
"issues": "https://github.com/cakephp/cakephp/issues",
|
||||
"source": "https://github.com/cakephp/core"
|
||||
},
|
||||
"time": "2026-03-31T06:25:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "cakephp/database",
|
||||
"version": "5.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cakephp/database.git",
|
||||
"reference": "cf94dcb57c54a1a308fd866b038cd6995910e36e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cakephp/database/zipball/cf94dcb57c54a1a308fd866b038cd6995910e36e",
|
||||
"reference": "cf94dcb57c54a1a308fd866b038cd6995910e36e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"cakephp/chronos": "^3.3",
|
||||
"cakephp/core": "^5.3.0",
|
||||
"cakephp/datasource": "^5.3.0",
|
||||
"php": ">=8.2",
|
||||
"psr/log": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"cakephp/i18n": "^5.3.0",
|
||||
"cakephp/log": "^5.3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"cakephp/i18n": "If you are using locale-aware datetime formats.",
|
||||
"cakephp/log": "If you want to use query logging without providing a logger yourself."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-5.next": "5.4.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Cake\\Database\\": "."
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "CakePHP Community",
|
||||
"homepage": "https://github.com/cakephp/database/graphs/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Flexible and powerful Database abstraction library with a familiar PDO-like API",
|
||||
"homepage": "https://cakephp.org",
|
||||
"keywords": [
|
||||
"abstraction",
|
||||
"cakephp",
|
||||
"database",
|
||||
"database abstraction",
|
||||
"pdo"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://stackoverflow.com/tags/cakephp",
|
||||
"irc": "irc://irc.freenode.org/cakephp",
|
||||
"issues": "https://github.com/cakephp/cakephp/issues",
|
||||
"source": "https://github.com/cakephp/database"
|
||||
},
|
||||
"time": "2026-03-31T06:25:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "cakephp/datasource",
|
||||
"version": "5.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cakephp/datasource.git",
|
||||
"reference": "512464eb27b19316b515ec338089b83822c9ab5a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cakephp/datasource/zipball/512464eb27b19316b515ec338089b83822c9ab5a",
|
||||
"reference": "512464eb27b19316b515ec338089b83822c9ab5a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"cakephp/core": "^5.3.0",
|
||||
"php": ">=8.2",
|
||||
"psr/simple-cache": "^2.0 || ^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"cakephp/cache": "^5.3.0",
|
||||
"cakephp/collection": "^5.3.0",
|
||||
"cakephp/utility": "^5.3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"cakephp/cache": "If you decide to use Query caching.",
|
||||
"cakephp/collection": "If you decide to use ResultSetInterface.",
|
||||
"cakephp/utility": "If you decide to use EntityTrait."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-5.next": "5.4.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Cake\\Datasource\\": "."
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "CakePHP Community",
|
||||
"homepage": "https://github.com/cakephp/datasource/graphs/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides connection managing and traits for Entities and Queries that can be reused for different datastores",
|
||||
"homepage": "https://cakephp.org",
|
||||
"keywords": [
|
||||
"cakephp",
|
||||
"connection management",
|
||||
"datasource",
|
||||
"entity",
|
||||
"query"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://stackoverflow.com/tags/cakephp",
|
||||
"irc": "irc://irc.freenode.org/cakephp",
|
||||
"issues": "https://github.com/cakephp/cakephp/issues",
|
||||
"source": "https://github.com/cakephp/datasource"
|
||||
},
|
||||
"time": "2026-04-04T08:08:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "cakephp/utility",
|
||||
"version": "5.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cakephp/utility.git",
|
||||
"reference": "4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cakephp/utility/zipball/4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99",
|
||||
"reference": "4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"cakephp/core": "^5.3.0",
|
||||
"php": ">=8.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "To use Text::transliterate() or Text::slug()",
|
||||
"lib-ICU": "To use Text::transliterate() or Text::slug()"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-5.next": "5.4.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Cake\\Utility\\": "."
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "CakePHP Community",
|
||||
"homepage": "https://github.com/cakephp/utility/graphs/contributors"
|
||||
}
|
||||
],
|
||||
"description": "CakePHP Utility classes such as Inflector, String, Hash, and Security",
|
||||
"homepage": "https://cakephp.org",
|
||||
"keywords": [
|
||||
"cakephp",
|
||||
"hash",
|
||||
"inflector",
|
||||
"security",
|
||||
"string",
|
||||
"utility"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://stackoverflow.com/tags/cakephp",
|
||||
"irc": "irc://irc.freenode.org/cakephp",
|
||||
"issues": "https://github.com/cakephp/cakephp/issues",
|
||||
"source": "https://github.com/cakephp/utility"
|
||||
},
|
||||
"time": "2026-03-09T09:38:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "carbonphp/carbon-doctrine-types",
|
||||
"version": "3.2.0",
|
||||
@@ -2627,6 +2951,90 @@
|
||||
],
|
||||
"time": "2022-12-11T20:36:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/container",
|
||||
"version": "5.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/container.git",
|
||||
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/container/zipball/58accbc032f0090a9bd08326f93062c5a658b2c5",
|
||||
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"psr/container": "^2.0.2",
|
||||
"psr/event-dispatcher": "^1.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/container-implementation": "^1.0"
|
||||
},
|
||||
"replace": {
|
||||
"orno/di": "~2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"nette/php-generator": "^4.1",
|
||||
"nikic/php-parser": "^5.0",
|
||||
"phpstan/phpstan": "^2.1.11",
|
||||
"phpunit/phpunit": "^10.5.45|^11.5.15|^12.0",
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"scrutinizer/ocular": "^1.9",
|
||||
"squizlabs/php_codesniffer": "^3.9"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-1.x": "1.x-dev",
|
||||
"dev-2.x": "2.x-dev",
|
||||
"dev-3.x": "3.x-dev",
|
||||
"dev-4.x": "4.x-dev",
|
||||
"dev-5.x": "5.x-dev",
|
||||
"dev-master": "5.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\Container\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Phil Bennett",
|
||||
"email": "mail@philbennett.co.uk",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A fast and intuitive dependency injection container.",
|
||||
"homepage": "https://github.com/thephpleague/container",
|
||||
"keywords": [
|
||||
"container",
|
||||
"dependency",
|
||||
"di",
|
||||
"injection",
|
||||
"league",
|
||||
"provider",
|
||||
"service"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/thephpleague/container/issues",
|
||||
"source": "https://github.com/thephpleague/container/tree/5.2.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/philipobenito",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-19T18:52:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem",
|
||||
"version": "3.28.0",
|
||||
@@ -4980,6 +5388,93 @@
|
||||
],
|
||||
"time": "2024-04-27T21:32:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "robmorgan/phinx",
|
||||
"version": "0.16.11",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cakephp/phinx.git",
|
||||
"reference": "a03014fea316ba021fc0776982e5bed2d10228d4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cakephp/phinx/zipball/a03014fea316ba021fc0776982e5bed2d10228d4",
|
||||
"reference": "a03014fea316ba021fc0776982e5bed2d10228d4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"cakephp/database": "^5.0.2",
|
||||
"composer-runtime-api": "^2.0",
|
||||
"php-64bit": ">=8.1",
|
||||
"psr/container": "^1.1|^2.0",
|
||||
"symfony/config": "^4.0|^5.0|^6.0|^7.0|^8.0",
|
||||
"symfony/console": "^6.0|^7.0|^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"cakephp/cakephp-codesniffer": "^5.0",
|
||||
"cakephp/i18n": "^5.0",
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"phpunit/phpunit": "^10.5",
|
||||
"symfony/yaml": "^4.0|^5.0|^6.0|^7.0|^8.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-json": "Install if using JSON configuration format",
|
||||
"ext-pdo": "PDO extension is needed",
|
||||
"symfony/yaml": "Install if using YAML configuration format"
|
||||
},
|
||||
"bin": [
|
||||
"bin/phinx"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Phinx\\": "src/Phinx/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Rob Morgan",
|
||||
"email": "robbym@gmail.com",
|
||||
"homepage": "https://robmorgan.id.au",
|
||||
"role": "Lead Developer"
|
||||
},
|
||||
{
|
||||
"name": "Woody Gilk",
|
||||
"email": "woody.gilk@gmail.com",
|
||||
"homepage": "https://shadowhand.me",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Richard Quadling",
|
||||
"email": "rquadling@gmail.com",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "CakePHP Community",
|
||||
"homepage": "https://github.com/cakephp/phinx/graphs/contributors",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Phinx makes it ridiculously easy to manage the database migrations for your PHP app.",
|
||||
"homepage": "https://phinx.org",
|
||||
"keywords": [
|
||||
"database",
|
||||
"database migrations",
|
||||
"db",
|
||||
"migrations",
|
||||
"phinx"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/cakephp/phinx/issues",
|
||||
"source": "https://github.com/cakephp/phinx/tree/0.16.11"
|
||||
},
|
||||
"time": "2026-03-15T00:04:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "socialiteproviders/manager",
|
||||
"version": "v4.8.1",
|
||||
@@ -5312,6 +5807,85 @@
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/config",
|
||||
"version": "v7.4.10",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/config.git",
|
||||
"reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/config/zipball/d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57",
|
||||
"reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/filesystem": "^7.1|^8.0",
|
||||
"symfony/polyfill-ctype": "~1.8"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/finder": "<6.4",
|
||||
"symfony/service-contracts": "<2.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
|
||||
"symfony/finder": "^6.4|^7.0|^8.0",
|
||||
"symfony/messenger": "^6.4|^7.0|^8.0",
|
||||
"symfony/service-contracts": "^2.5|^3",
|
||||
"symfony/yaml": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Config\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/config/tree/v7.4.10"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-03T14:20:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v7.1.3",
|
||||
@@ -5768,6 +6342,76 @@
|
||||
],
|
||||
"time": "2024-04-18T09:32:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "v7.4.11",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
|
||||
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/polyfill-ctype": "~1.8",
|
||||
"symfony/polyfill-mbstring": "~1.8"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/process": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Filesystem\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides basic utilities for the filesystem",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/filesystem/tree/v7.4.11"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-11T16:38:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v7.1.3",
|
||||
@@ -11355,6 +11999,6 @@
|
||||
"php": "^8.2.0",
|
||||
"ext-json": "*"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class BaselineExistingDatabase extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Change Method.
|
||||
*
|
||||
* Write your reversible migrations using this method.
|
||||
*
|
||||
* More information on writing migrations is available here:
|
||||
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
|
||||
*
|
||||
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||
* with the Table class.
|
||||
*/
|
||||
public function change(): void
|
||||
{
|
||||
// Baseline migration.
|
||||
// Existing database structure starts being tracked from this point.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreatePhinxTestTable extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Change Method.
|
||||
*
|
||||
* Write your reversible migrations using this method.
|
||||
*
|
||||
* More information on writing migrations is available here:
|
||||
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
|
||||
*
|
||||
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||
* with the Table class.
|
||||
*/
|
||||
public function change(): void
|
||||
{
|
||||
$table = $this->table('phinx_test_table');
|
||||
|
||||
$table
|
||||
->addColumn('name', 'string', [
|
||||
'limit' => 100,
|
||||
'null' => false,
|
||||
])
|
||||
->addColumn('created_at', 'timestamp', [
|
||||
'default' => 'CURRENT_TIMESTAMP',
|
||||
'null' => false,
|
||||
])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddFunctionsToScadDeadlines extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('scad_functions', [
|
||||
'id' => false,
|
||||
'primary_key' => ['id'],
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'encoding' => 'utf8mb4',
|
||||
])
|
||||
->addColumn('id', 'integer', [
|
||||
'identity' => true,
|
||||
'signed' => false,
|
||||
])
|
||||
->addColumn('name', 'string', [
|
||||
'limit' => 255,
|
||||
'null' => false,
|
||||
])
|
||||
->addColumn('description', 'text', [
|
||||
'null' => true,
|
||||
])
|
||||
->addColumn('status', 'string', [
|
||||
'limit' => 20,
|
||||
'null' => false,
|
||||
'default' => 'active',
|
||||
])
|
||||
->addColumn('created_at', 'timestamp', [
|
||||
'null' => false,
|
||||
'default' => 'CURRENT_TIMESTAMP',
|
||||
])
|
||||
->addColumn('updated_at', 'timestamp', [
|
||||
'null' => false,
|
||||
'default' => 'CURRENT_TIMESTAMP',
|
||||
'update' => 'CURRENT_TIMESTAMP',
|
||||
])
|
||||
->addIndex(['name'], [
|
||||
'unique' => true,
|
||||
'name' => 'uniq_scad_functions_name',
|
||||
])
|
||||
->create();
|
||||
|
||||
$this->table('scad_deadlines')
|
||||
->addColumn('function_id', 'integer', [
|
||||
'signed' => false,
|
||||
'null' => true,
|
||||
'after' => 'subject_id',
|
||||
])
|
||||
->addIndex(['function_id'], [
|
||||
'name' => 'idx_scad_deadlines_function_id',
|
||||
])
|
||||
->addForeignKey('function_id', 'scad_functions', 'id', [
|
||||
'delete' => 'SET_NULL',
|
||||
'update' => 'CASCADE',
|
||||
'constraint' => 'fk_scad_deadlines_function',
|
||||
])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
if (file_exists(__DIR__ . '/.env')) {
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
|
||||
$dotenv->safeLoad();
|
||||
}
|
||||
|
||||
return [
|
||||
'paths' => [
|
||||
'migrations' => __DIR__ . '/db/migrations',
|
||||
'seeds' => __DIR__ . '/db/seeds',
|
||||
],
|
||||
|
||||
'environments' => [
|
||||
'default_migration_table' => 'phinxlog',
|
||||
'default_environment' => 'development',
|
||||
|
||||
'development' => [
|
||||
'adapter' => $_ENV['DB_CONNECTION'] ?? 'mysql',
|
||||
'host' => $_ENV['DB_HOST'] ?? 'localhost',
|
||||
'name' => $_ENV['DB_DATABASE'] ?? '',
|
||||
'user' => $_ENV['DB_USERNAME'] ?? '',
|
||||
'pass' => $_ENV['DB_PASSWORD'] ?? '',
|
||||
'port' => $_ENV['DB_PORT'] ?? 3306,
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
],
|
||||
],
|
||||
|
||||
'version_order' => 'creation',
|
||||
];
|
||||
@@ -0,0 +1,799 @@
|
||||
<?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/bootstrap@5.3.3/dist/js/bootstrap.bundle.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 toggled">
|
||||
<?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>
|
||||
+184
-21
@@ -21,11 +21,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
|
||||
$employee_code = trim($_POST['employee_code'] ?? '');
|
||||
$first_name = trim($_POST['first_name'] ?? '');
|
||||
$last_name = trim($_POST['last_name'] ?? '');
|
||||
$department = trim($_POST['department'] ?? '');
|
||||
$department_id = $_POST['department_id'] !== '' ? (int)$_POST['department_id'] : null;
|
||||
$position = trim($_POST['position'] ?? '');
|
||||
$hire_date = trim($_POST['hire_date'] ?? '');
|
||||
$status = trim($_POST['status'] ?? 'active');
|
||||
$auth_user_id = $_POST['auth_user_id'] !== '' ? (int)$_POST['auth_user_id'] : null;
|
||||
$role_id = $_POST['role_id'] !== '' ? (int)$_POST['role_id'] : null;
|
||||
|
||||
if ($first_name === '' || $last_name === '') {
|
||||
echo json_encode([
|
||||
@@ -40,21 +41,34 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO employees
|
||||
(auth_user_id, employee_code, first_name, last_name, department, position, hire_date, status, created_at, updated_at)
|
||||
VALUES
|
||||
(:auth_user_id, :employee_code, :first_name, :last_name, :department, :position, :hire_date, :status, NOW(), NOW())";
|
||||
(auth_user_id, employee_code, first_name, last_name, department_id, position, hire_date, status, created_at, updated_at)
|
||||
VALUES
|
||||
(:auth_user_id, :employee_code, :first_name, :last_name, :department_id, :position, :hire_date, :status, NOW(), NOW())";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([
|
||||
'auth_user_id' => $auth_user_id,
|
||||
'employee_code' => $employee_code !== '' ? $employee_code : null,
|
||||
'first_name' => $first_name,
|
||||
'last_name' => $last_name,
|
||||
'department' => $department !== '' ? $department : null,
|
||||
'department_id' => $department_id,
|
||||
'position' => $position !== '' ? $position : null,
|
||||
'hire_date' => $hire_date !== '' ? $hire_date : null,
|
||||
'status' => $status
|
||||
]);
|
||||
|
||||
if ($auth_user_id !== null && $role_id !== null) {
|
||||
$checkRole = $pdo->prepare("SELECT COUNT(*) FROM auth_roles WHERE id = ?");
|
||||
$checkRole->execute([$role_id]);
|
||||
|
||||
if ((int)$checkRole->fetchColumn() > 0) {
|
||||
$stmtRole = $pdo->prepare("UPDATE auth_users SET role_id = :role_id, updated_at = NOW() WHERE id = :auth_user_id");
|
||||
$stmtRole->execute([
|
||||
'role_id' => $role_id,
|
||||
'auth_user_id' => $auth_user_id
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
@@ -65,11 +79,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
|
||||
$employee_code = trim($_POST['employee_code'] ?? '');
|
||||
$first_name = trim($_POST['first_name'] ?? '');
|
||||
$last_name = trim($_POST['last_name'] ?? '');
|
||||
$department = trim($_POST['department'] ?? '');
|
||||
$department_id = $_POST['department_id'] !== '' ? (int)$_POST['department_id'] : null;
|
||||
$position = trim($_POST['position'] ?? '');
|
||||
$hire_date = trim($_POST['hire_date'] ?? '');
|
||||
$status = trim($_POST['status'] ?? 'active');
|
||||
$auth_user_id = $_POST['auth_user_id'] !== '' ? (int)$_POST['auth_user_id'] : null;
|
||||
$role_id = $_POST['role_id'] !== '' ? (int)$_POST['role_id'] : null;
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid employee ID.']);
|
||||
@@ -93,7 +108,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
|
||||
employee_code = :employee_code,
|
||||
first_name = :first_name,
|
||||
last_name = :last_name,
|
||||
department = :department,
|
||||
department_id = :department_id,
|
||||
position = :position,
|
||||
hire_date = :hire_date,
|
||||
status = :status,
|
||||
@@ -105,13 +120,26 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
|
||||
'employee_code' => $employee_code !== '' ? $employee_code : null,
|
||||
'first_name' => $first_name,
|
||||
'last_name' => $last_name,
|
||||
'department' => $department !== '' ? $department : null,
|
||||
'department_id' => $department_id,
|
||||
'position' => $position !== '' ? $position : null,
|
||||
'hire_date' => $hire_date !== '' ? $hire_date : null,
|
||||
'status' => $status,
|
||||
'id' => $id
|
||||
]);
|
||||
|
||||
if ($auth_user_id !== null && $role_id !== null) {
|
||||
$checkRole = $pdo->prepare("SELECT COUNT(*) FROM auth_roles WHERE id = ?");
|
||||
$checkRole->execute([$role_id]);
|
||||
|
||||
if ((int)$checkRole->fetchColumn() > 0) {
|
||||
$stmtRole = $pdo->prepare("UPDATE auth_users SET role_id = :role_id, updated_at = NOW() WHERE id = :auth_user_id");
|
||||
$stmtRole->execute([
|
||||
'role_id' => $role_id,
|
||||
'auth_user_id' => $auth_user_id
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
@@ -193,10 +221,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
|
||||
// Employees list
|
||||
$sql = "
|
||||
SELECT e.*,
|
||||
d.name AS department_name,
|
||||
d.color AS department_color,
|
||||
au.email AS user_email,
|
||||
au.role_id AS user_role_id,
|
||||
ar.display_name AS role_display_name,
|
||||
ar.name AS role_name,
|
||||
CONCAT(COALESCE(au.first_name, ''), ' ', COALESCE(au.last_name, '')) AS user_fullname
|
||||
FROM employees e
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
LEFT JOIN auth_users au ON e.auth_user_id = au.id
|
||||
LEFT JOIN auth_roles ar ON ar.id = au.role_id
|
||||
ORDER BY e.id DESC
|
||||
";
|
||||
$stmtEmployees = $pdo->query($sql);
|
||||
@@ -205,6 +240,7 @@ $employees = $stmtEmployees->fetchAll(PDO::FETCH_ASSOC);
|
||||
// Users list for select
|
||||
$sqlUsers = "
|
||||
SELECT id,
|
||||
role_id,
|
||||
CONCAT(
|
||||
COALESCE(first_name, ''),
|
||||
' ',
|
||||
@@ -219,6 +255,25 @@ $sqlUsers = "
|
||||
$stmtUsers = $pdo->query($sqlUsers);
|
||||
$users = $stmtUsers->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Roles list for select
|
||||
$sqlRoles = "
|
||||
SELECT id, name, display_name
|
||||
FROM auth_roles
|
||||
ORDER BY display_name, name
|
||||
";
|
||||
$stmtRoles = $pdo->query($sqlRoles);
|
||||
$roles = $stmtRoles->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Departments list for select
|
||||
$sqlDepartments = "
|
||||
SELECT id, name, code, color
|
||||
FROM departments
|
||||
WHERE is_active = 1
|
||||
ORDER BY sort_order ASC, name ASC
|
||||
";
|
||||
$stmtDepartments = $pdo->query($sqlDepartments);
|
||||
$departments = $stmtDepartments->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Skills list for JS
|
||||
$sqlSkills = "
|
||||
SELECT s.id, s.name, pl.name as line_name, pl.line_number
|
||||
@@ -347,6 +402,15 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
background-color: #157347;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.department-badge {
|
||||
display: inline-block;
|
||||
color: #fff;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -421,7 +485,15 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
<td><?= (int)$row['id'] ?></td>
|
||||
<td><?= htmlspecialchars($row['employee_code'] ?? '') ?></td>
|
||||
<td><?= htmlspecialchars($fullName) ?></td>
|
||||
<td><?= htmlspecialchars($row['department'] ?? '') ?></td>
|
||||
<td>
|
||||
<?php if (!empty($row['department_name'])): ?>
|
||||
<span class="department-badge" style="background-color: <?= htmlspecialchars($row['department_color'] ?? '#6c757d', ENT_QUOTES) ?>;">
|
||||
<?= htmlspecialchars($row['department_name']) ?>
|
||||
</span>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($row['position'] ?? '') ?></td>
|
||||
<td><?= $hireDate ?></td>
|
||||
<td>
|
||||
@@ -437,11 +509,12 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
data-code="<?= htmlspecialchars($row['employee_code'] ?? '', ENT_QUOTES) ?>"
|
||||
data-first_name="<?= htmlspecialchars($row['first_name'] ?? '', ENT_QUOTES) ?>"
|
||||
data-last_name="<?= htmlspecialchars($row['last_name'] ?? '', ENT_QUOTES) ?>"
|
||||
data-department="<?= htmlspecialchars($row['department'] ?? '', ENT_QUOTES) ?>"
|
||||
data-department_id="<?= $row['department_id'] !== null ? (int)$row['department_id'] : '' ?>"
|
||||
data-position="<?= htmlspecialchars($row['position'] ?? '', ENT_QUOTES) ?>"
|
||||
data-hire_date="<?= htmlspecialchars($row['hire_date'] ?? '', ENT_QUOTES) ?>"
|
||||
data-status="<?= htmlspecialchars($status, ENT_QUOTES) ?>"
|
||||
data-auth_user_id="<?= $row['auth_user_id'] !== null ? (int)$row['auth_user_id'] : '' ?>">
|
||||
data-auth_user_id="<?= $row['auth_user_id'] !== null ? (int)$row['auth_user_id'] : '' ?>"
|
||||
data-role_id="<?= $row['user_role_id'] !== null ? (int)$row['user_role_id'] : '' ?>">
|
||||
✏️ Modifica
|
||||
</button>
|
||||
|
||||
@@ -477,7 +550,7 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
<!-- MODALE AGGIUNTA DIPENDENTE -->
|
||||
<div class="modal fade" id="addEmployeeModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header" style="background-color:#cfe3ff;">
|
||||
<h5 class="modal-title">Aggiungi Nuovo Dipendente</h5>
|
||||
@@ -505,7 +578,15 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Department</label>
|
||||
<input type="text" class="form-control" id="addDepartment" name="department" placeholder="e.g. Production">
|
||||
<select class="form-select" id="addDepartmentId" name="department_id" style="width:100%;">
|
||||
<option value="">-- Select Department --</option>
|
||||
<?php foreach ($departments as $d): ?>
|
||||
<option value="<?= (int)$d['id'] ?>">
|
||||
<?= htmlspecialchars($d['name']) ?>
|
||||
<?= !empty($d['code']) ? ' (' . htmlspecialchars($d['code']) . ')' : '' ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Position</label>
|
||||
@@ -533,13 +614,26 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
<select class="form-select" id="addAuthUserId" name="auth_user_id" style="width:100%;">
|
||||
<option value="">-- None --</option>
|
||||
<?php foreach ($users as $u): ?>
|
||||
<option value="<?= (int)$u['id'] ?>">
|
||||
<option value="<?= (int)$u['id'] ?>" data-role_id="<?= (int)$u['role_id'] ?>">
|
||||
<?= htmlspecialchars($u['label']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 d-none" id="addRoleWrapper">
|
||||
<label class="form-label fw-semibold">User Role</label>
|
||||
<select class="form-select" id="addRoleId" name="role_id" style="width:100%;">
|
||||
<option value="">-- Select Role --</option>
|
||||
<?php foreach ($roles as $r): ?>
|
||||
<option value="<?= (int)$r['id'] ?>">
|
||||
<?= htmlspecialchars($r['display_name'] ?: $r['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<small class="text-muted">Visible only when an auth user is linked.</small>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<button type="submit" class="btn btn-add">💾 Save</button>
|
||||
</div>
|
||||
@@ -552,7 +646,7 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
<!-- MODALE EDIT DIPENDENTE -->
|
||||
<div class="modal fade" id="editEmployeeModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header" style="background-color:#cfe3ff;">
|
||||
<h5 class="modal-title">Modifica Dipendente</h5>
|
||||
@@ -582,7 +676,15 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Department</label>
|
||||
<input type="text" class="form-control" id="editDepartment" name="department">
|
||||
<select class="form-select" id="editDepartmentId" name="department_id" style="width:100%;">
|
||||
<option value="">-- Select Department --</option>
|
||||
<?php foreach ($departments as $d): ?>
|
||||
<option value="<?= (int)$d['id'] ?>">
|
||||
<?= htmlspecialchars($d['name']) ?>
|
||||
<?= !empty($d['code']) ? ' (' . htmlspecialchars($d['code']) . ')' : '' ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Position</label>
|
||||
@@ -610,13 +712,26 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
<select class="form-select" id="editAuthUserId" name="auth_user_id" style="width:100%;">
|
||||
<option value="">-- None --</option>
|
||||
<?php foreach ($users as $u): ?>
|
||||
<option value="<?= (int)$u['id'] ?>">
|
||||
<option value="<?= (int)$u['id'] ?>" data-role_id="<?= (int)$u['role_id'] ?>">
|
||||
<?= htmlspecialchars($u['label']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 d-none" id="editRoleWrapper">
|
||||
<label class="form-label fw-semibold">User Role</label>
|
||||
<select class="form-select" id="editRoleId" name="role_id" style="width:100%;">
|
||||
<option value="">-- Select Role --</option>
|
||||
<?php foreach ($roles as $r): ?>
|
||||
<option value="<?= (int)$r['id'] ?>">
|
||||
<?= htmlspecialchars($r['display_name'] ?: $r['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<small class="text-muted">Visible only when an auth user is linked.</small>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<button type="submit" class="btn btn-add">💾 Save Changes</button>
|
||||
</div>
|
||||
@@ -669,11 +784,46 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
|
||||
// Select2 on user selects
|
||||
$('#addAuthUserId, #editAuthUserId').select2({
|
||||
$('#addAuthUserId, #editAuthUserId, #addDepartmentId, #editDepartmentId, #addRoleId, #editRoleId').select2({
|
||||
theme: 'bootstrap-5',
|
||||
width: '100%'
|
||||
});
|
||||
|
||||
function syncAddRoleVisibility() {
|
||||
const authUserId = $('#addAuthUserId').val();
|
||||
|
||||
if (authUserId) {
|
||||
$('#addRoleWrapper').removeClass('d-none');
|
||||
|
||||
const selectedRoleId = $('#addAuthUserId option:selected').data('role_id');
|
||||
if (selectedRoleId) {
|
||||
$('#addRoleId').val(String(selectedRoleId)).trigger('change');
|
||||
}
|
||||
} else {
|
||||
$('#addRoleWrapper').addClass('d-none');
|
||||
$('#addRoleId').val('').trigger('change');
|
||||
}
|
||||
}
|
||||
|
||||
function syncEditRoleVisibility() {
|
||||
const authUserId = $('#editAuthUserId').val();
|
||||
|
||||
if (authUserId) {
|
||||
$('#editRoleWrapper').removeClass('d-none');
|
||||
|
||||
const selectedRoleId = $('#editAuthUserId option:selected').data('role_id');
|
||||
if (selectedRoleId) {
|
||||
$('#editRoleId').val(String(selectedRoleId)).trigger('change');
|
||||
}
|
||||
} else {
|
||||
$('#editRoleWrapper').addClass('d-none');
|
||||
$('#editRoleId').val('').trigger('change');
|
||||
}
|
||||
}
|
||||
|
||||
$('#addAuthUserId').on('change', syncAddRoleVisibility);
|
||||
$('#editAuthUserId').on('change', syncEditRoleVisibility);
|
||||
|
||||
/* -------- ADD EMPLOYEE -------- */
|
||||
$("#addEmployeeForm").on("submit", function(e) {
|
||||
e.preventDefault();
|
||||
@@ -684,11 +834,12 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
payload.append('employee_code', $("#addEmployeeCode").val().trim());
|
||||
payload.append('first_name', $("#addFirstName").val().trim());
|
||||
payload.append('last_name', $("#addLastName").val().trim());
|
||||
payload.append('department', $("#addDepartment").val().trim());
|
||||
payload.append('department_id', $("#addDepartmentId").val() || '');
|
||||
payload.append('position', $("#addPosition").val().trim());
|
||||
payload.append('hire_date', $("#addHireDate").val());
|
||||
payload.append('status', $("#addStatus").val());
|
||||
payload.append('auth_user_id', $("#addAuthUserId").val() || '');
|
||||
payload.append('role_id', $("#addAuthUserId").val() ? ($("#addRoleId").val() || '') : '');
|
||||
|
||||
fetch("", {
|
||||
method: "POST",
|
||||
@@ -732,7 +883,7 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
$("#editEmployeeCode").val(btn.data("code"));
|
||||
$("#editFirstName").val(btn.data("first_name"));
|
||||
$("#editLastName").val(btn.data("last_name"));
|
||||
$("#editDepartment").val(btn.data("department"));
|
||||
$("#editDepartmentId").val(btn.data("department_id") ? String(btn.data("department_id")) : '').trigger('change');
|
||||
$("#editPosition").val(btn.data("position"));
|
||||
$("#editHireDate").val(btn.data("hire_date"));
|
||||
$("#editStatus").val(btn.data("status"));
|
||||
@@ -740,6 +891,17 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
const authUserId = btn.data("auth_user_id");
|
||||
$("#editAuthUserId").val(authUserId ? String(authUserId) : '').trigger('change');
|
||||
|
||||
const roleId = btn.data("role_id");
|
||||
if (authUserId && roleId) {
|
||||
$("#editRoleWrapper").removeClass('d-none');
|
||||
$("#editRoleId").val(String(roleId)).trigger('change');
|
||||
} else {
|
||||
$("#editRoleWrapper").addClass('d-none');
|
||||
$("#editRoleId").val('').trigger('change');
|
||||
}
|
||||
|
||||
$("#editEmployeeModal").modal("show");
|
||||
|
||||
$("#editEmployeeModal").modal("show");
|
||||
});
|
||||
|
||||
@@ -754,11 +916,12 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
payload.append('employee_code', $("#editEmployeeCode").val().trim());
|
||||
payload.append('first_name', $("#editFirstName").val().trim());
|
||||
payload.append('last_name', $("#editLastName").val().trim());
|
||||
payload.append('department', $("#editDepartment").val().trim());
|
||||
payload.append('department_id', $("#editDepartmentId").val() || '');
|
||||
payload.append('position', $("#editPosition").val().trim());
|
||||
payload.append('hire_date', $("#editHireDate").val());
|
||||
payload.append('status', $("#editStatus").val());
|
||||
payload.append('auth_user_id', $("#editAuthUserId").val() || '');
|
||||
payload.append('role_id', $("#editAuthUserId").val() ? ($("#editRoleId").val() || '') : '');
|
||||
|
||||
fetch("", {
|
||||
method: "POST",
|
||||
|
||||
@@ -7,16 +7,21 @@ ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL | E_STRICT);
|
||||
// This should be equal to: PATH_TO_VANGUARD_FOLDER/extra/auth.php
|
||||
include('../../extra/auth.php');
|
||||
include(__DIR__ . '/../../../extra/auth.php');
|
||||
//require_once __DIR__ . '/extra/auth.php';
|
||||
|
||||
// Here we just check if user is not
|
||||
// Here we just check if user is not
|
||||
// logged in, and in that case we redirect
|
||||
// the user to vanguard login page.
|
||||
|
||||
if (! Auth::check()) {
|
||||
|
||||
redirectTo('../../public/login');
|
||||
// Cut everything at /userarea/ and append /login.
|
||||
$scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
|
||||
$basePath = substr($scriptName, 0, strpos($scriptName, '/userarea/'));
|
||||
if ($basePath === false || $basePath === '') {
|
||||
$basePath = '';
|
||||
}
|
||||
redirectTo($basePath . '/login');
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
@@ -57,5 +62,5 @@ $photousername = basename($avatar);
|
||||
require_once(__DIR__ . '/../../languages/en/general.php');
|
||||
|
||||
//include("generalsettings.php");
|
||||
|
||||
?>
|
||||
require_once __DIR__ . '/permissions_helper.php';
|
||||
?>
|
||||
|
||||
@@ -6,101 +6,386 @@
|
||||
<div>
|
||||
<h4 class="logo-text"><?= htmlspecialchars('ZIBOGOMMA', ENT_QUOTES, 'UTF-8'); ?></h4>
|
||||
</div>
|
||||
<div class="toggle-icon ms-auto"><i class='bx bx-arrow-back'></i>
|
||||
<div class="toggle-icon ms-auto">
|
||||
<i class='bx bx-arrow-back'></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--navigation-->
|
||||
<ul class="metismenu" id="menu">
|
||||
<!-- user, admin, superuser menù -->
|
||||
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('User')) || (Auth::user()->hasRole('Superuser'))) : ?>
|
||||
|
||||
<?php if (userCan('production.dashboard.view')) : ?>
|
||||
<li>
|
||||
<a href="production_dashboard.php">
|
||||
<div class="parent-icon"><i class="bx bx-home-alt"></i>
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-home-alt"></i>
|
||||
</div>
|
||||
<div class="menu-title">Dashboard</div>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php
|
||||
$canSeeProgramming =
|
||||
userCan('production.programming.view')
|
||||
|| userCan('templates.dashboard.view')
|
||||
|| userCan('templates.create.view');
|
||||
?>
|
||||
|
||||
<?php if ($canSeeProgramming) : ?>
|
||||
<li>
|
||||
<a href="javascript:;" class="has-arrow">
|
||||
<div class="parent-icon"><i class="bx bx-category"></i>
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-category"></i>
|
||||
</div>
|
||||
<div class="menu-title">Programmazione</div>
|
||||
</a>
|
||||
<ul>
|
||||
<li> <a href="templates_dashboard.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?></a>
|
||||
</li>
|
||||
<li> <a href="insert_template_xls.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?></a>
|
||||
</li>
|
||||
|
||||
<ul>
|
||||
<?php if (userCan('templates.dashboard.view')) : ?>
|
||||
<li>
|
||||
<a href="templates_dashboard.php">
|
||||
<i class='bx bx-radio-circle'></i>
|
||||
<?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('templates.create.view')) : ?>
|
||||
<li>
|
||||
<a href="insert_template_xls.php">
|
||||
<i class='bx bx-radio-circle'></i>
|
||||
<?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('production.programming.view')) : ?>
|
||||
<li>
|
||||
<a href="produzione_programmazione_drag.php">
|
||||
<i class='bx bx-radio-circle'></i>
|
||||
Programmazione Produzione
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php
|
||||
$canSeeFunctions =
|
||||
userCan('masterdata.mescole.view')
|
||||
|| userCan('masterdata.matrici.view')
|
||||
|| userCan('masterdata.linee.view')
|
||||
|| userCan('masterdata.packaging.view')
|
||||
|| userCan('masterdata.suppliers.view')
|
||||
|| userCan('masterdata.lookup.view')
|
||||
|| userCan('masterdata.worksheets.view');
|
||||
?>
|
||||
|
||||
<?php if ($canSeeFunctions) : ?>
|
||||
<li>
|
||||
<a href="javascript:;" class="has-arrow">
|
||||
<div class="parent-icon"><i class="bx bx-category"></i>
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-category"></i>
|
||||
</div>
|
||||
<div class="menu-title">Funzioni</div>
|
||||
</a>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="mescole.php"><i class='bx bx-radio-circle'></i>Mescole</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="matrici.php"><i class='bx bx-radio-circle'></i>Matrici</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="linee.php"><i class='bx bx-radio-circle'></i>Linee di produzione</a>
|
||||
</li>
|
||||
<?php if (userCan('masterdata.mescole.view')) : ?>
|
||||
<li>
|
||||
<a href="mescole.php">
|
||||
<i class='bx bx-radio-circle'></i>Mescole
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('masterdata.matrici.view')) : ?>
|
||||
<li>
|
||||
<a href="matrici.php">
|
||||
<i class='bx bx-radio-circle'></i>Matrici
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('masterdata.linee.view')) : ?>
|
||||
<li>
|
||||
<a href="linee.php">
|
||||
<i class='bx bx-radio-circle'></i>Linee di produzione
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('masterdata.packaging.view')) : ?>
|
||||
<li>
|
||||
<a href="packaging_items.php">
|
||||
<i class='bx bx-radio-circle'></i>Imballaggi
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('masterdata.suppliers.view')) : ?>
|
||||
<li>
|
||||
<a href="suppliers.php">
|
||||
<i class='bx bx-radio-circle'></i>Suppliers
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('masterdata.lookup.view')) : ?>
|
||||
<li>
|
||||
<a href="lookup_values.php">
|
||||
<i class='bx bx-radio-circle'></i>Setup
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('masterdata.worksheets.view')) : ?>
|
||||
<li>
|
||||
<a href="worksheets.php">
|
||||
<i class='bx bx-radio-circle'></i>Fogli di lavoro
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php
|
||||
$canSeeProduction =
|
||||
userCan('production.line_view.view')
|
||||
|| userCan('production.stats.view')
|
||||
|| userCan('production.manager.view')
|
||||
|| userCan('production.manager_stats.view')
|
||||
|| userCan('warehouse.dashboard.view');
|
||||
?>
|
||||
|
||||
|
||||
|
||||
<li class="menu-label">Others</li>
|
||||
|
||||
|
||||
<?php if ($canSeeProduction) : ?>
|
||||
<li>
|
||||
<a href="https://helpdesk.cesoft.io" target="_blank">
|
||||
<div class="parent-icon"><i class="bx bx-support"></i>
|
||||
<a href="javascript:;" class="has-arrow">
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-line-chart"></i>
|
||||
</div>
|
||||
<div class="menu-title">Support</div>
|
||||
<div class="menu-title">Produzione</div>
|
||||
</a>
|
||||
|
||||
<ul>
|
||||
<?php if (userCan('production.line_view.view')) : ?>
|
||||
<li>
|
||||
<a href="production_line_view2.php">
|
||||
<i class='bx bx-radio-circle'></i>Line View
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('production.stats.view')) : ?>
|
||||
<li>
|
||||
<a href="production_stats.php">
|
||||
<i class='bx bx-radio-circle'></i>Statistiche
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('production.manager.view')) : ?>
|
||||
<li>
|
||||
<a href="manager_produzione.php">
|
||||
<i class='bx bx-radio-circle'></i>Manager
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('production.manager_stats.view')) : ?>
|
||||
<li>
|
||||
<a href="manager_stats.php">
|
||||
<i class='bx bx-radio-circle'></i>Manager Stats
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('warehouse.dashboard.view')) : ?>
|
||||
<li>
|
||||
<a href="warehouse_dashboard.php">
|
||||
<i class='bx bx-radio-circle'></i>Magazzino
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php
|
||||
endif; ?>
|
||||
<!-- admin, superuser menù -->
|
||||
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('Superuser'))) : ?>
|
||||
$canSeeServices =
|
||||
userCan('services.status.view')
|
||||
|| userCan('services.pause_reasons.view')
|
||||
|| userCan('services.tools.view');
|
||||
?>
|
||||
|
||||
<?php if ($canSeeServices) : ?>
|
||||
<li>
|
||||
<a href="javascript:;" class="has-arrow">
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-wrench"></i>
|
||||
</div>
|
||||
<div class="menu-title">Servizi</div>
|
||||
</a>
|
||||
|
||||
<ul>
|
||||
<?php if (userCan('services.status.view')) : ?>
|
||||
<li>
|
||||
<a href="production_status.php">
|
||||
<i class='bx bx-radio-circle'></i>Status
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('services.pause_reasons.view')) : ?>
|
||||
<li>
|
||||
<a href="production_pause_reasons.php">
|
||||
<i class='bx bx-radio-circle'></i>Cause di Pausa
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('services.tools.view')) : ?>
|
||||
<li>
|
||||
<a href="production_tools.php">
|
||||
<i class='bx bx-radio-circle'></i>Attrezzature
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php
|
||||
endif; ?>
|
||||
<!-- admin menù -->
|
||||
<?php if (Auth::user()->hasRole('Admin')) : ?>
|
||||
$canSeeHr =
|
||||
userCan('hr.employees.view')
|
||||
|| userCan('hr.departments.view')
|
||||
|| userCan('hr.job_roles.view')
|
||||
|| userCan('hr.training_topics.view')
|
||||
|| userCan('hr.trainings.view')
|
||||
|| userCan('hr.skills.view');
|
||||
?>
|
||||
|
||||
<?php if ($canSeeHr) : ?>
|
||||
<li>
|
||||
<a href="javascript:;" class="has-arrow">
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-group"></i>
|
||||
</div>
|
||||
<div class="menu-title">Personale</div>
|
||||
</a>
|
||||
|
||||
<ul>
|
||||
<?php if (userCan('hr.employees.view')) : ?>
|
||||
<li>
|
||||
<a href="employees.php">
|
||||
<i class='bx bx-radio-circle'></i>Dipendenti
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('hr.departments.view')) : ?>
|
||||
<li>
|
||||
<a href="departments.php">
|
||||
<i class='bx bx-radio-circle'></i>Reparti
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('hr.job_roles.view')) : ?>
|
||||
<li>
|
||||
<a href="job_roles.php">
|
||||
<i class='bx bx-radio-circle'></i>Mansioni
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('hr.training_topics.view')) : ?>
|
||||
<li>
|
||||
<a href="training_topics.php">
|
||||
<i class='bx bx-radio-circle'></i>Corsi di Formazione
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('hr.trainings.view')) : ?>
|
||||
<li>
|
||||
<a href="trainings.php">
|
||||
<i class='bx bx-radio-circle'></i>Storico Formazione
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('hr.skills.view')) : ?>
|
||||
<li>
|
||||
<a href="skills.php">
|
||||
<i class='bx bx-radio-circle'></i>Skills
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php if (userCan('deadlines.view')) : ?>
|
||||
<li>
|
||||
<a href="javascript:;" class="has-arrow">
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-calendar-check"></i>
|
||||
</div>
|
||||
<div class="menu-title">Scadenzario</div>
|
||||
</a>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="scadenzario/index.php">
|
||||
<i class='bx bx-radio-circle'></i>Lista Scadenze
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="scadenzario/calendar.php">
|
||||
<i class='bx bx-radio-circle'></i>Calendario
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<li class="menu-label">Others</li>
|
||||
|
||||
<li>
|
||||
<a href="https://helpdesk.cesoft.io" target="_blank">
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-support"></i>
|
||||
</div>
|
||||
<div class="menu-title">Support</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
<?php if (userCan('users.manage')) : ?>
|
||||
<li class="menu-label">Admin Menù</li>
|
||||
|
||||
<li>
|
||||
<a href="../" target="_blank">
|
||||
<div class="parent-icon"><i class="bx bx-support"></i>
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-user-circle"></i>
|
||||
</div>
|
||||
<div class="menu-title">User Management</div>
|
||||
</a>
|
||||
</li>
|
||||
<!-- <li>
|
||||
<a href="template/index.html" target="_blank">
|
||||
<div class="parent-icon"><i class="bx bx-support"></i>
|
||||
</div>
|
||||
<div class="menu-title">Template</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://codervent.com/rocker/documentation/index.html" target="_blank">
|
||||
<div class="parent-icon"><i class="bx bx-folder"></i>
|
||||
</div>
|
||||
<div class="menu-title">Documentation</div>
|
||||
</a>
|
||||
</li> -->
|
||||
<?php
|
||||
endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
</ul>
|
||||
<!--end navigation-->
|
||||
</div>
|
||||
@@ -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']);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@
|
||||
</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="user_settings.php">
|
||||
<i class="bx bx-user fs-5"></i><span>Utente</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -1,4 +1,184 @@
|
||||
<?php include('include/headscript.php'); ?>
|
||||
<?php
|
||||
$dashboardSections = [
|
||||
[
|
||||
'id' => 'secOperativo',
|
||||
'title' => 'Operativo',
|
||||
'subtitle' => 'Azioni principali di produzione e attività in scadenza',
|
||||
'icon' => '🚀',
|
||||
'open' => true,
|
||||
'buttons' => [
|
||||
[
|
||||
'label' => 'Programmazione',
|
||||
'icon' => '🗓️',
|
||||
'class' => 'btn-programmazione',
|
||||
'url' => 'produzione_programmazione_drag.php',
|
||||
'permission' => 'production.programming.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Line View',
|
||||
'icon' => '⚙️',
|
||||
'class' => 'btn-status',
|
||||
'url' => 'production_line_view2.php',
|
||||
'permission' => 'production.line_view.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Statistiche',
|
||||
'icon' => '📈',
|
||||
'class' => 'btn-statistiche',
|
||||
'url' => 'production_stats.php',
|
||||
'permission' => 'production.stats.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Manager',
|
||||
'icon' => '👔',
|
||||
'class' => 'btn-manager',
|
||||
'url' => 'manager_produzione.php',
|
||||
'permission' => 'production.manager.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Manager Stats',
|
||||
'icon' => '📊',
|
||||
'class' => 'btn-manager-stats',
|
||||
'url' => 'manager_stats.php',
|
||||
'permission' => 'production.manager_stats.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Magazzino',
|
||||
'icon' => '📦',
|
||||
'class' => 'btn-magazzino',
|
||||
'url' => 'warehouse_dashboard.php',
|
||||
'permission' => 'warehouse.dashboard.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Smart-Alert',
|
||||
'icon' => '⏰',
|
||||
'class' => 'btn-scadenziario',
|
||||
'url' => 'scadenzario/index.php',
|
||||
'permission' => 'deadlines.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => 'secAnagrafiche',
|
||||
'title' => 'Anagrafiche',
|
||||
'subtitle' => 'Dati di base e setup di produzione',
|
||||
'icon' => '🗂️',
|
||||
'open' => false,
|
||||
'buttons' => [
|
||||
[
|
||||
'label' => 'Mescole',
|
||||
'icon' => '⚗️',
|
||||
'class' => 'btn-mescole',
|
||||
'url' => 'mescole.php',
|
||||
'permission' => 'masterdata.mescole.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Elenco Profili',
|
||||
'icon' => '🧩',
|
||||
'class' => 'btn-matrici',
|
||||
'url' => 'matrici.php',
|
||||
'permission' => 'masterdata.matrici.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Linee Produzione',
|
||||
'icon' => '🏭',
|
||||
'class' => 'btn-linee',
|
||||
'url' => 'linee.php',
|
||||
'permission' => 'masterdata.linee.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Imballaggi',
|
||||
'icon' => '📦',
|
||||
'class' => 'btn-setup',
|
||||
'url' => 'packaging_items.php',
|
||||
'permission' => 'masterdata.packaging.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Suppliers',
|
||||
'icon' => '🏷️',
|
||||
'class' => 'btn-setup',
|
||||
'url' => 'suppliers.php',
|
||||
'permission' => 'masterdata.suppliers.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Setup',
|
||||
'icon' => '⚙️',
|
||||
'class' => 'btn-setup',
|
||||
'url' => 'lookup_values.php',
|
||||
'permission' => 'masterdata.lookup.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Fogli di lavoro',
|
||||
'icon' => '🗒️',
|
||||
'class' => 'btn-setup',
|
||||
'url' => 'worksheets.php',
|
||||
'permission' => 'masterdata.worksheets.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => 'secServizi',
|
||||
'title' => 'Servizi',
|
||||
'subtitle' => 'Status, cause pausa, attrezzature',
|
||||
'icon' => '🧰',
|
||||
'open' => false,
|
||||
'buttons' => [
|
||||
[
|
||||
'label' => 'Status',
|
||||
'icon' => '📋',
|
||||
'class' => 'btn-setup',
|
||||
'url' => 'production_status.php',
|
||||
'permission' => 'services.status.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Cause di Pausa',
|
||||
'icon' => '🛑',
|
||||
'class' => 'btn-problem',
|
||||
'url' => 'production_pause_reasons.php',
|
||||
'permission' => 'services.pause_reasons.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Attrezzature',
|
||||
'icon' => '🛠️',
|
||||
'class' => 'btn-tools',
|
||||
'url' => 'production_tools.php',
|
||||
'permission' => 'services.tools.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => 'secPersonale',
|
||||
'title' => 'Personale',
|
||||
'subtitle' => 'Dipendenti, skill',
|
||||
'icon' => '👥',
|
||||
'open' => false,
|
||||
'buttons' => [
|
||||
[
|
||||
'label' => 'Employees',
|
||||
'icon' => '👥',
|
||||
'class' => 'btn-employees',
|
||||
'url' => 'employees.php',
|
||||
'permission' => 'hr.employees.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Departments',
|
||||
'icon' => '🏢',
|
||||
'class' => 'btn-departments',
|
||||
'url' => 'departments.php',
|
||||
'permission' => 'hr.departments.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Skills',
|
||||
'icon' => '🧠',
|
||||
'class' => 'btn-setup',
|
||||
'url' => 'skills.php',
|
||||
'permission' => 'hr.skills.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
@@ -7,7 +187,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
<?php include('cssinclude.php'); ?>
|
||||
<title>Dashboard Produzione - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
<title>Dashboard <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
|
||||
<!-- Bootstrap + jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
@@ -255,6 +435,15 @@
|
||||
background: linear-gradient(135deg, #a5b4fc, #c7d2fe);
|
||||
}
|
||||
|
||||
.btn-departments {
|
||||
background: linear-gradient(135deg, #bfdbfe, #dbeafe);
|
||||
color: #1f2d3d;
|
||||
}
|
||||
|
||||
.btn-departments:hover {
|
||||
background: linear-gradient(135deg, #93c5fd, #bfdbfe);
|
||||
}
|
||||
|
||||
.btn-setup {
|
||||
background: linear-gradient(135deg, #e5e7eb, #f3f4f6);
|
||||
}
|
||||
@@ -296,7 +485,12 @@
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<h3 class="dashboard-title">Dashboard Produzione</h3>
|
||||
<?php
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
include(__DIR__ . '/scadenzario/include/my_deadlines_widget.php');
|
||||
?>
|
||||
|
||||
<h3 class="dashboard-title">Dashboard</h3>
|
||||
|
||||
<!-- ===== STATISTICHE PRINCIPALI ===== -->
|
||||
<div class="stats-row">
|
||||
@@ -333,183 +527,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>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Installation — Scadenzario
|
||||
|
||||
## 1. Database
|
||||
|
||||
Run the schema script:
|
||||
|
||||
```bash
|
||||
mysql -u <user> -p <database> < public/userarea/scadenzario/sql/1_create_tables.sql
|
||||
```
|
||||
|
||||
This creates 5 tables:
|
||||
|
||||
| Table | Purpose |
|
||||
|---|---|
|
||||
| `scad_deadlines` | Main deadline records |
|
||||
| `scad_deadline_employee` | M2M assignment of individual employees |
|
||||
| `scad_deadline_attachments` | File attachments |
|
||||
| `scad_deadline_histories` | Audit log (created/updated/completed/...) |
|
||||
| `scad_deadline_notifications` | Sent-notification log (deduplication) |
|
||||
|
||||
Departments are stored as a comma-separated string in `scad_deadlines.departments` (matching `employees.department` values). No separate `departments` table.
|
||||
|
||||
## 2. Filesystem permissions
|
||||
|
||||
The `attachments/` folder must be writable by the web server:
|
||||
|
||||
```bash
|
||||
chmod 755 public/userarea/scadenzario/attachments
|
||||
chown www-data:www-data public/userarea/scadenzario/attachments
|
||||
```
|
||||
|
||||
The included `.htaccess` denies direct web access. Files are served only through the auth-protected `ajax/download_attachment.php` endpoint.
|
||||
|
||||
## 3. SMTP configuration
|
||||
|
||||
Email notifications use the project-wide SMTP settings in `.env`:
|
||||
|
||||
```env
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=smtp.example.com
|
||||
MAIL_PORT=465
|
||||
MAIL_USERNAME=your_user
|
||||
MAIL_PASSWORD=your_password
|
||||
MAIL_ENCRYPTION=ssl
|
||||
MAIL_FROM_ADDRESS=scadenzario@your-domain.com
|
||||
MAIL_FROM_NAME="Scadenzario"
|
||||
APP_URL=https://your-domain.com
|
||||
```
|
||||
|
||||
## 4. Cron schedule
|
||||
|
||||
Add to the system crontab (run as the web user):
|
||||
|
||||
```cron
|
||||
0 7 * * * php /var/www/html/public/userarea/scadenzario/cron/send_notifications.php >> /var/log/scadenzario.log 2>&1
|
||||
```
|
||||
|
||||
This sends notifications daily at 07:00 for:
|
||||
|
||||
- **Approaching** — `due_date <= today + notification_days` (per-deadline lead time)
|
||||
- **Overdue** — `due_date < today`
|
||||
|
||||
Completed deadlines are skipped. Recipients without an `auth_user_id` are silently skipped.
|
||||
|
||||
## 5. Linking employees to auth users
|
||||
|
||||
For an employee to receive email notifications:
|
||||
|
||||
1. The corresponding `auth_users` row must exist with a valid `email`.
|
||||
2. Set `employees.auth_user_id` to that user ID:
|
||||
```sql
|
||||
UPDATE employees SET auth_user_id = <user_id> WHERE id = <employee_id>;
|
||||
```
|
||||
|
||||
Employees without `auth_user_id` are silently skipped by the cron.
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* Auth check for AJAX endpoints.
|
||||
* Include this at the top of every ajax handler.
|
||||
* Sets $currentUserId from session or returns 401 JSON.
|
||||
*/
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (empty($_SESSION['iduserlogin'])) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'message' => 'Non autorizzato. Effettua il login.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$currentUserId = (int)$_SESSION['iduserlogin'];
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
$rawId = $_POST['id'] ?? $_GET['id'] ?? null;
|
||||
if ($rawId === null || !is_numeric($rawId)) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int)$rawId;
|
||||
|
||||
// Whether to create the next (recurring) deadline. Absent or '1' => create; '0' => complete only.
|
||||
$createNext = ($_POST['create_next'] ?? '1') !== '0';
|
||||
|
||||
// Whether to carry the attachment links over to the new deadline. Default ON ("default all activate").
|
||||
$copyAttachments = ($_POST['copy_attachments'] ?? '1') !== '0';
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM scad_deadlines WHERE id = ? AND status = 'active'");
|
||||
$stmt->execute([$id]);
|
||||
$deadline = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$deadline) {
|
||||
echo json_encode(['success' => false, 'message' => 'Scadenza non trovata o già completata.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Mark as completed
|
||||
$pdo->prepare("UPDATE scad_deadlines SET status = 'completed', completed_at = NOW(), completed_by = ? WHERE id = ?")
|
||||
->execute([$currentUserId, $id]);
|
||||
|
||||
// History
|
||||
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action) VALUES (?, ?, 'completed')")
|
||||
->execute([$id, $currentUserId]);
|
||||
|
||||
$newId = null;
|
||||
$newDueDate = null;
|
||||
|
||||
// If recurring AND the user asked for it, create the next deadline
|
||||
if ($deadline['recurrence_type'] !== 'once' && $createNext) {
|
||||
$dueDate = new DateTime($deadline['due_date']);
|
||||
$checkDate = $deadline['check_date'] ? new DateTime($deadline['check_date']) : null;
|
||||
$documentDate = $deadline['document_date'] ? new DateTime($deadline['document_date']) : null;
|
||||
|
||||
switch ($deadline['recurrence_type']) {
|
||||
case 'monthly': $interval = new DateInterval('P1M'); break;
|
||||
case 'quarterly': $interval = new DateInterval('P3M'); break;
|
||||
case 'semiannual': $interval = new DateInterval('P6M'); break;
|
||||
case 'annual': $interval = new DateInterval('P1Y'); break;
|
||||
case 'biennial': $interval = new DateInterval('P2Y'); break;
|
||||
case 'triennial': $interval = new DateInterval('P3Y'); break;
|
||||
case 'quadriennial': $interval = new DateInterval('P4Y'); break;
|
||||
case 'quinquennial': $interval = new DateInterval('P5Y'); break;
|
||||
case 'decennial': $interval = new DateInterval('P10Y'); break;
|
||||
case 'quindecennial': $interval = new DateInterval('P15Y'); break;
|
||||
default: $interval = null;
|
||||
}
|
||||
|
||||
if ($interval) {
|
||||
$dueDate->add($interval);
|
||||
if ($checkDate) $checkDate->add($interval);
|
||||
if ($documentDate) $documentDate->add($interval);
|
||||
|
||||
$ins = $pdo->prepare("
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, function_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$ins->execute([
|
||||
$deadline['subject_id'], $deadline['function_id'], $deadline['topic'], $deadline['law_regulation'],
|
||||
$deadline['recurrence_type'], $dueDate->format('Y-m-d'),
|
||||
$checkDate ? $checkDate->format('Y-m-d') : null,
|
||||
$documentDate ? $documentDate->format('Y-m-d') : null,
|
||||
$deadline['notification_days'], $deadline['storage_location'],
|
||||
$deadline['notes'], $deadline['created_by'], $deadline['departments']
|
||||
]);
|
||||
|
||||
$newId = $pdo->lastInsertId();
|
||||
$newDueDate = $dueDate;
|
||||
|
||||
// Copy employee assignments
|
||||
$empStmt = $pdo->prepare("SELECT employee_id FROM scad_deadline_employee WHERE deadline_id = ?");
|
||||
$empStmt->execute([$id]);
|
||||
$empIds = $empStmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if (!empty($empIds)) {
|
||||
$insertEmp = $pdo->prepare("INSERT INTO scad_deadline_employee (deadline_id, employee_id) VALUES (?, ?)");
|
||||
foreach ($empIds as $empId) {
|
||||
$insertEmp->execute([$newId, $empId]);
|
||||
}
|
||||
}
|
||||
|
||||
// Carry forward ALL attachment links from the source deadline (shared physical file, same stored_name).
|
||||
// Individual links can later be removed on the new deadline without deleting the file.
|
||||
if ($copyAttachments) {
|
||||
$attSel = $pdo->prepare("
|
||||
SELECT original_name, stored_name, mime_type, size
|
||||
FROM scad_deadline_attachments
|
||||
WHERE deadline_id = ?
|
||||
");
|
||||
$attSel->execute([$id]);
|
||||
$attRows = $attSel->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($attRows) {
|
||||
$attIns = $pdo->prepare("
|
||||
INSERT INTO scad_deadline_attachments
|
||||
(deadline_id, original_name, stored_name, mime_type, size, uploaded_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$attHist = $pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'attachment_linked', ?)");
|
||||
foreach ($attRows as $a) {
|
||||
$attIns->execute([$newId, $a['original_name'], $a['stored_name'], $a['mime_type'], $a['size'], $currentUserId]);
|
||||
$attHist->execute([$newId, $currentUserId, $a['original_name']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// History for new
|
||||
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'created', ?)")
|
||||
->execute([$newId, $currentUserId, 'Creata automaticamente dalla scadenza #' . $id]);
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
$msg = 'Scadenza completata con successo.';
|
||||
if ($newId) {
|
||||
$msg .= ' Nuova scadenza creata con data ' . $newDueDate->format('d/m/Y') . '.';
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'message' => $msg, 'new_id' => $newId]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
if (isset($pdo) && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int)$_GET['id'];
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM scad_deadline_attachments WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$att = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$att) {
|
||||
echo json_encode(['success' => false, 'message' => 'Allegato non trovato.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Remove this link (DB record) first
|
||||
$pdo->prepare("DELETE FROM scad_deadline_attachments WHERE id = ?")->execute([$id]);
|
||||
|
||||
// The same physical file may be shared with other deadlines (carried forward on completion).
|
||||
// Only unlink it when no other link references the same stored file.
|
||||
$refStmt = $pdo->prepare("SELECT COUNT(*) FROM scad_deadline_attachments WHERE stored_name = ?");
|
||||
$refStmt->execute([$att['stored_name']]);
|
||||
$stillReferenced = (int)$refStmt->fetchColumn() > 0;
|
||||
|
||||
if ($stillReferenced) {
|
||||
$action = 'attachment_unlinked';
|
||||
$message = 'Collegamento rimosso. Il file è conservato (usato da un\'altra scadenza).';
|
||||
} else {
|
||||
$filePath = __DIR__ . '/../attachments/' . $att['stored_name'];
|
||||
if (file_exists($filePath)) {
|
||||
unlink($filePath);
|
||||
}
|
||||
$action = 'attachment_removed';
|
||||
$message = 'Allegato eliminato.';
|
||||
}
|
||||
|
||||
// History
|
||||
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, ?, ?)")
|
||||
->execute([$att['deadline_id'], $currentUserId, $action, $att['original_name']]);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => $message]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int)$_GET['id'];
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
// Collect the physical files referenced by this deadline before the FK cascade removes its links
|
||||
$attStmt = $pdo->prepare("SELECT DISTINCT stored_name FROM scad_deadline_attachments WHERE deadline_id = ?");
|
||||
$attStmt->execute([$id]);
|
||||
$storedNames = $attStmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
// Deleting the deadline cascades to its attachment/employee/history rows (FK ON DELETE CASCADE)
|
||||
$stmt = $pdo->prepare("DELETE FROM scad_deadlines WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
// Unlink physical files no longer referenced by any other deadline (shared-file safe)
|
||||
if (!empty($storedNames)) {
|
||||
$refStmt = $pdo->prepare("SELECT COUNT(*) FROM scad_deadline_attachments WHERE stored_name = ?");
|
||||
foreach ($storedNames as $storedName) {
|
||||
$refStmt->execute([$storedName]);
|
||||
if ((int)$refStmt->fetchColumn() === 0) {
|
||||
$filePath = __DIR__ . '/../attachments/' . $storedName;
|
||||
if (file_exists($filePath)) {
|
||||
unlink($filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
echo json_encode(['success' => true, 'message' => 'Scadenza eliminata con successo.']);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => 'Scadenza non trovata.']);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
http_response_code(400);
|
||||
echo 'ID non valido.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int)$_GET['id'];
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM scad_deadline_attachments WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$att = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$att) {
|
||||
http_response_code(404);
|
||||
echo 'Allegato non trovato.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$filePath = __DIR__ . '/../attachments/' . $att['stored_name'];
|
||||
if (!file_exists($filePath)) {
|
||||
http_response_code(404);
|
||||
echo 'File non trovato sul server.';
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Type: ' . ($att['mime_type'] ?: 'application/octet-stream'));
|
||||
header('Content-Disposition: attachment; filename="' . addslashes($att['original_name']) . '"');
|
||||
header('Content-Length: ' . filesize($filePath));
|
||||
readfile($filePath);
|
||||
exit;
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$start = $_GET['start'] ?? null;
|
||||
$end = $_GET['end'] ?? null;
|
||||
$filterStatus = $_GET['status'] ?? '';
|
||||
$filterDept = $_GET['department'] ?? '';
|
||||
$filterEmployee = $_GET['employee'] ?? '';
|
||||
|
||||
$sql = "SELECT DISTINCT d.id, d.topic, d.due_date, d.status, d.notification_days, d.departments,
|
||||
s.name AS subject_name, s.color AS subject_color
|
||||
FROM scad_deadlines d
|
||||
LEFT JOIN scad_subjects s ON s.id = d.subject_id
|
||||
LEFT JOIN scad_deadline_employee de ON de.deadline_id = d.id
|
||||
LEFT JOIN employees e ON e.id = de.employee_id";
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($start && $end) {
|
||||
$where[] = "d.due_date >= ? AND d.due_date <= ?";
|
||||
$params[] = $start;
|
||||
$params[] = $end;
|
||||
}
|
||||
|
||||
if ($filterStatus === 'non-completata') {
|
||||
$where[] = "d.status != 'completed'";
|
||||
} elseif ($filterStatus === 'completata') {
|
||||
$where[] = "d.status = 'completed'";
|
||||
} elseif ($filterStatus === 'scaduta') {
|
||||
$where[] = "d.status = 'active' AND d.due_date < CURDATE()";
|
||||
} elseif ($filterStatus === 'in-scadenza') {
|
||||
$where[] = "d.status = 'active' AND d.due_date >= CURDATE() AND d.due_date <= DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)";
|
||||
} elseif ($filterStatus === 'attiva') {
|
||||
$where[] = "d.status = 'active' AND d.due_date > DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)";
|
||||
}
|
||||
|
||||
if ($filterDept) {
|
||||
$where[] = "(e.department = ? OR FIND_IN_SET(?, d.departments))";
|
||||
$params[] = $filterDept;
|
||||
$params[] = $filterDept;
|
||||
}
|
||||
|
||||
if ($filterEmployee) {
|
||||
$where[] = "CONCAT(e.first_name, ' ', e.last_name) = ?";
|
||||
$params[] = $filterEmployee;
|
||||
}
|
||||
|
||||
if (!empty($where)) {
|
||||
$sql .= " WHERE " . implode(' AND ', $where);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$events = [];
|
||||
|
||||
foreach ($deadlines as $d) {
|
||||
$isCompleted = $d['status'] === 'completed';
|
||||
$isOverdue = !$isCompleted && $d['due_date'] < $today;
|
||||
$approachDate = date('Y-m-d', strtotime($today . ' + ' . (int)$d['notification_days'] . ' days'));
|
||||
$isApproaching = !$isCompleted && !$isOverdue && $d['due_date'] <= $approachDate;
|
||||
|
||||
if ($isCompleted) { $color = '#198754'; }
|
||||
elseif ($isOverdue) { $color = '#dc3545'; }
|
||||
elseif ($isApproaching) { $color = '#e8930c'; }
|
||||
else { $color = '#5a8fd8'; }
|
||||
|
||||
$title = $d['topic'];
|
||||
if (!empty($d['subject_name'])) $title = $d['subject_name'] . ': ' . $title;
|
||||
|
||||
$events[] = [
|
||||
'id' => $d['id'],
|
||||
'title' => $title,
|
||||
'start' => $d['due_date'],
|
||||
'backgroundColor' => $color,
|
||||
'borderColor' => !empty($d['subject_color']) ? $d['subject_color'] : $color,
|
||||
'url' => 'scadenzario/detail.php?id=' . $d['id'],
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode($events);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode([]);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int)$_GET['id'];
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM scad_deadlines WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$deadline = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$deadline) {
|
||||
echo json_encode(['success' => false, 'message' => 'Scadenza non trovata.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get assigned employee IDs
|
||||
$empStmt = $pdo->prepare("SELECT employee_id FROM scad_deadline_employee WHERE deadline_id = ?");
|
||||
$empStmt->execute([$id]);
|
||||
$deadline['employee_ids'] = $empStmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
// Parse departments into array
|
||||
$deadline['department_names'] = [];
|
||||
if (!empty($deadline['departments'])) {
|
||||
$deadline['department_names'] = array_map('trim', explode(',', $deadline['departments']));
|
||||
}
|
||||
|
||||
// Get attachments
|
||||
$attStmt = $pdo->prepare("SELECT id, original_name, mime_type, size, created_at FROM scad_deadline_attachments WHERE deadline_id = ? ORDER BY created_at DESC");
|
||||
$attStmt->execute([$id]);
|
||||
$deadline['attachments'] = $attStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode(['success' => true, 'data' => $deadline]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int)$_GET['id'];
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT h.*,
|
||||
au.first_name as user_first_name,
|
||||
au.last_name as user_last_name
|
||||
FROM scad_deadline_histories h
|
||||
LEFT JOIN auth_users au ON au.id = h.user_id
|
||||
WHERE h.deadline_id = ?
|
||||
ORDER BY h.created_at DESC
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$history = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Format for display
|
||||
$actionLabels = [
|
||||
'created' => 'Creata',
|
||||
'updated' => 'Modificata',
|
||||
'completed' => 'Completata',
|
||||
'attachment_added' => 'Allegato aggiunto',
|
||||
'attachment_removed' => 'Allegato rimosso',
|
||||
'notification_sent' => 'Notifica inviata'
|
||||
];
|
||||
|
||||
foreach ($history as &$h) {
|
||||
$h['action_label'] = $actionLabels[$h['action']] ?? $h['action'];
|
||||
$h['user_name'] = trim(($h['user_first_name'] ?? '') . ' ' . ($h['user_last_name'] ?? '')) ?: 'Sistema';
|
||||
$h['changes'] = $h['changes'] ? json_decode($h['changes'], true) : null;
|
||||
unset($h['user_first_name'], $h['user_last_name']);
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'data' => $history]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : null;
|
||||
$subject_id = isset($_POST['subject_id']) && is_numeric($_POST['subject_id']) && (int)$_POST['subject_id'] > 0 ? (int)$_POST['subject_id'] : null;
|
||||
$function_id = isset($_POST['function_id']) && is_numeric($_POST['function_id']) && (int)$_POST['function_id'] > 0 ? (int)$_POST['function_id'] : null;
|
||||
$topic = trim($_POST['topic'] ?? '');
|
||||
$law_regulation = trim($_POST['law_regulation'] ?? '') ?: null;
|
||||
$recurrence_type = $_POST['recurrence_type'] ?? 'once';
|
||||
$due_date = $_POST['due_date'] ?? '';
|
||||
$check_date = trim($_POST['check_date'] ?? '') ?: null;
|
||||
$document_date = trim($_POST['document_date'] ?? '') ?: null;
|
||||
$notification_days = isset($_POST['notification_days']) && is_numeric($_POST['notification_days']) ? (int)$_POST['notification_days'] : 7;
|
||||
$storage_location = trim($_POST['storage_location'] ?? '') ?: null;
|
||||
$notes = trim($_POST['notes'] ?? '') ?: null;
|
||||
$employee_ids = $_POST['employee_ids'] ?? [];
|
||||
$department_names = $_POST['department_names'] ?? [];
|
||||
|
||||
// Validation
|
||||
if ($topic === '') {
|
||||
echo json_encode(['success' => false, 'message' => 'Il campo Tema è obbligatorio.']);
|
||||
exit;
|
||||
}
|
||||
if ($due_date === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $due_date)) {
|
||||
echo json_encode(['success' => false, 'message' => 'La data di scadenza è obbligatoria.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$validRecurrences = ['once', 'monthly', 'quarterly', 'semiannual', 'annual', 'biennial', 'triennial', 'quadriennial', 'quinquennial', 'decennial', 'quindecennial'];
|
||||
if (!in_array($recurrence_type, $validRecurrences)) {
|
||||
$recurrence_type = 'once';
|
||||
}
|
||||
|
||||
if (!is_array($employee_ids)) {
|
||||
$employee_ids = [];
|
||||
}
|
||||
$employee_ids = array_filter(array_map('intval', $employee_ids));
|
||||
|
||||
if (!is_array($department_names)) {
|
||||
$department_names = [];
|
||||
}
|
||||
$department_names = array_filter(array_map('trim', $department_names));
|
||||
$departmentsStr = !empty($department_names) ? implode(', ', $department_names) : null;
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
if ($id) {
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE scad_deadlines SET
|
||||
subject_id = ?, function_id = ?, topic = ?, law_regulation = ?, recurrence_type = ?,
|
||||
due_date = ?, check_date = ?, document_date = ?, notification_days = ?,
|
||||
storage_location = ?, notes = ?, departments = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([
|
||||
$subject_id,
|
||||
$function_id,
|
||||
$topic,
|
||||
$law_regulation,
|
||||
$recurrence_type,
|
||||
$due_date,
|
||||
$check_date,
|
||||
$document_date,
|
||||
$notification_days,
|
||||
$storage_location,
|
||||
$notes,
|
||||
$departmentsStr,
|
||||
$id
|
||||
]);
|
||||
|
||||
// Re-link employees
|
||||
$pdo->prepare("DELETE FROM scad_deadline_employee WHERE deadline_id = ?")->execute([$id]);
|
||||
|
||||
// History
|
||||
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action) VALUES (?, ?, 'updated')")
|
||||
->execute([$id, $currentUserId ?: null]);
|
||||
|
||||
$deadlineId = $id;
|
||||
} else {
|
||||
// INSERT
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, function_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([
|
||||
$subject_id,
|
||||
$function_id,
|
||||
$topic,
|
||||
$law_regulation,
|
||||
$recurrence_type,
|
||||
$due_date,
|
||||
$check_date,
|
||||
$document_date,
|
||||
$notification_days,
|
||||
$storage_location,
|
||||
$notes,
|
||||
$currentUserId,
|
||||
$departmentsStr
|
||||
]);
|
||||
|
||||
$deadlineId = $pdo->lastInsertId();
|
||||
|
||||
// History
|
||||
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action) VALUES (?, ?, 'created')")
|
||||
->execute([$deadlineId, $currentUserId ?: null]);
|
||||
}
|
||||
|
||||
// Link employees
|
||||
if (!empty($employee_ids)) {
|
||||
$insertEmployee = $pdo->prepare("INSERT INTO scad_deadline_employee (deadline_id, employee_id) VALUES (?, ?)");
|
||||
foreach ($employee_ids as $empId) {
|
||||
$insertEmployee->execute([$deadlineId, $empId]);
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => $id ? 'Scadenza aggiornata con successo.' : 'Scadenza creata con successo.',
|
||||
'id' => $deadlineId
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
if (isset($pdo) && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
if (!isset($_POST['deadline_id']) || !is_numeric($_POST['deadline_id'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID scadenza non valido.']);
|
||||
exit;
|
||||
}
|
||||
if (empty($_FILES['files']['name'][0])) {
|
||||
echo json_encode(['success' => false, 'message' => 'Nessun file selezionato.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$deadlineId = (int)$_POST['deadline_id'];
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
// Verify deadline exists
|
||||
$check = $pdo->prepare("SELECT id FROM scad_deadlines WHERE id = ?");
|
||||
$check->execute([$deadlineId]);
|
||||
if (!$check->fetch()) {
|
||||
echo json_encode(['success' => false, 'message' => 'Scadenza non trovata.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$uploadDir = __DIR__ . '/../attachments/';
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
|
||||
$inserted = [];
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO scad_deadline_attachments (deadline_id, original_name, stored_name, mime_type, size, uploaded_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$histStmt = $pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'attachment_added', ?)");
|
||||
|
||||
$fileCount = count($_FILES['files']['name']);
|
||||
for ($i = 0; $i < $fileCount; $i++) {
|
||||
if ($_FILES['files']['error'][$i] !== UPLOAD_ERR_OK) continue;
|
||||
|
||||
$originalName = $_FILES['files']['name'][$i];
|
||||
$mimeType = $_FILES['files']['type'][$i];
|
||||
$size = $_FILES['files']['size'][$i];
|
||||
$storedName = uniqid('att_') . '_' . preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName);
|
||||
|
||||
if (!move_uploaded_file($_FILES['files']['tmp_name'][$i], $uploadDir . $storedName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stmt->execute([$deadlineId, $originalName, $storedName, $mimeType, $size, $currentUserId]);
|
||||
$histStmt->execute([$deadlineId, $currentUserId, $originalName]);
|
||||
$inserted[] = ['id' => $pdo->lastInsertId(), 'original_name' => $originalName, 'stored_name' => $storedName];
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => count($inserted) . ' file caricato/i con successo.',
|
||||
'files' => $inserted
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
if (isset($pdo) && $pdo->inTransaction()) $pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
*
|
||||
!.gitignore
|
||||
!.htaccess
|
||||
@@ -0,0 +1 @@
|
||||
Deny from all
|
||||
@@ -0,0 +1,275 @@
|
||||
<?php include('../include/headscript.php'); ?>
|
||||
<?php
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
$employees = $pdo->query("SELECT id, first_name, last_name, department FROM employees WHERE status = 'active' ORDER BY first_name")->fetchAll(PDO::FETCH_ASSOC);
|
||||
$departments = $pdo->query("SELECT DISTINCT department FROM employees WHERE department IS NOT NULL AND department != '' ORDER BY department")->fetchAll(PDO::FETCH_COLUMN);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<?php
|
||||
$scriptDir = dirname($_SERVER['SCRIPT_NAME']);
|
||||
$baseHref = dirname($scriptDir) . '/';
|
||||
?>
|
||||
<base href="<?= $baseHref ?>">
|
||||
<?php include('../cssinclude.php'); ?>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.9/index.global.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.9/locales/it.global.min.js"></script>
|
||||
<title>Calendario - Scadenzario</title>
|
||||
<script>if(window.innerWidth>1024)document.addEventListener('DOMContentLoaded',function(){document.getElementById('appWrapper').classList.add('toggled')})</script>
|
||||
<style>
|
||||
:root {
|
||||
--scad-primary: #5a8fd8;
|
||||
--scad-primary-hover: #4578c0;
|
||||
--scad-heading: #2c3e6b;
|
||||
--scad-card-bg: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%);
|
||||
--scad-card-border: #dde4f0;
|
||||
}
|
||||
.scad-card {
|
||||
border: none; border-radius: 0.75rem;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden;
|
||||
}
|
||||
.scad-card .card-header {
|
||||
background: var(--scad-card-bg);
|
||||
border-bottom: 1px solid var(--scad-card-border);
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
.scad-card .card-header h5 {
|
||||
font-weight: 700; color: var(--scad-heading);
|
||||
margin: 0; font-size: 1.05rem;
|
||||
}
|
||||
.scad-card .card-body { padding: 1.25rem; }
|
||||
.btn-scad-outline {
|
||||
background: transparent; border: 1.5px solid var(--scad-primary); color: var(--scad-primary);
|
||||
font-weight: 600; font-size: 0.85rem; padding: 0.45rem 1rem; border-radius: 0.5rem; transition: all 0.2s;
|
||||
}
|
||||
.btn-scad-outline:hover { background: var(--scad-primary); color: #fff; }
|
||||
.scad-breadcrumb { background: transparent; padding: 0; margin-bottom: 1rem; }
|
||||
.scad-breadcrumb .breadcrumb-item a { color: var(--scad-primary); text-decoration: none; font-weight: 500; }
|
||||
.scad-breadcrumb .breadcrumb-item a:hover { color: var(--scad-primary-hover); }
|
||||
.scad-breadcrumb .breadcrumb-item.active { color: #6c757d; font-weight: 600; }
|
||||
|
||||
/* FullCalendar overrides */
|
||||
.fc { font-size: 0.9rem; }
|
||||
.fc .fc-toolbar-title { font-size: 1.15rem; font-weight: 700; color: var(--scad-heading); }
|
||||
.fc .fc-button-primary {
|
||||
background: var(--scad-primary); border-color: var(--scad-primary);
|
||||
font-weight: 600; font-size: 0.82rem; border-radius: 0.4rem;
|
||||
}
|
||||
.fc .fc-button-primary:hover { background: var(--scad-primary-hover); border-color: var(--scad-primary-hover); }
|
||||
.fc .fc-button-primary:disabled { background: #9bbce6; border-color: #9bbce6; }
|
||||
.fc .fc-button-primary:not(:disabled).fc-button-active { background: var(--scad-heading); border-color: var(--scad-heading); }
|
||||
.fc .fc-daygrid-day-number { color: var(--scad-heading); font-weight: 500; }
|
||||
.fc .fc-daygrid-day.fc-day-today { background: #f0f4ff; }
|
||||
.fc .fc-event { border-radius: 0.3rem; padding: 2px 4px; font-weight: 600; cursor: pointer; }
|
||||
.fc .fc-event:hover { filter: brightness(0.9); }
|
||||
.fc .fc-list-event:hover td { background: #f0f4ff; }
|
||||
|
||||
/* Legend */
|
||||
.legend { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 1rem; }
|
||||
.legend-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.82rem; color: #6c757d; }
|
||||
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.fc .fc-toolbar { flex-direction: column; gap: 0.5rem; }
|
||||
.fc .fc-toolbar-title { font-size: 1rem; }
|
||||
/* Stack list events vertically on mobile */
|
||||
.fc .fc-list-table { display: block; }
|
||||
.fc .fc-list-table tbody { display: block; }
|
||||
.fc .fc-list-day { display: block; }
|
||||
.fc .fc-list-day th { display: block; padding: 8px 12px; }
|
||||
.fc .fc-list-event { display: flex; flex-direction: column; padding: 8px 12px; border-bottom: 1px solid #e8eeff; }
|
||||
.fc .fc-list-event td { display: block; border: none; padding: 0; }
|
||||
.fc .fc-list-event-time { font-size: 0.75rem; color: #8e99b0; order: 2; }
|
||||
.fc .fc-list-event-graphic { display: none; }
|
||||
.fc .fc-list-event-title { font-size: 0.9rem; word-break: break-word; white-space: normal; order: 1; margin-bottom: 2px; }
|
||||
.fc .fc-list-event-dot { display: inline-block; margin-right: 6px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('../include/navbar.php'); ?>
|
||||
<?php include('../include/topbar.php'); ?>
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<?php include(__DIR__ . '/include/my_deadlines_widget.php'); ?>
|
||||
|
||||
<div class="d-flex gap-2 mb-3 flex-wrap align-items-center">
|
||||
<button type="button" class="btn btn-scad-outline d-inline-flex align-items-center gap-2" data-bs-toggle="modal" data-bs-target="#filtersModal">
|
||||
<i class="fa-solid fa-filter"></i>
|
||||
<span>Filtri</span>
|
||||
<span id="filterCountBadge" class="badge bg-primary rounded-pill d-none" style="font-size:0.7rem">0</span>
|
||||
</button>
|
||||
<button id="btnResetFilters" type="button" class="btn btn-light border d-inline-flex align-items-center justify-content-center gap-1" title="Reset filtri" style="min-width:38px;height:38px">
|
||||
<i class="fa-solid fa-rotate-left"></i>
|
||||
<span class="d-none d-sm-inline">Reset</span>
|
||||
</button>
|
||||
<span id="activeFiltersSummary" class="text-muted small text-truncate d-none d-md-inline"></span>
|
||||
</div>
|
||||
<div id="activeFiltersSummaryMobile" class="text-muted small d-md-none mb-2" style="padding-left:0.25rem"></div>
|
||||
|
||||
<!-- Filters Modal -->
|
||||
<div class="modal fade" id="filtersModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fa-solid fa-filter me-2"></i>Filtri calendario</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Stato</label>
|
||||
<select id="filterStatus" class="form-select">
|
||||
<option value="non-completata" selected>Non completate</option>
|
||||
<option value="">Tutti</option>
|
||||
<option value="attiva">Attive</option>
|
||||
<option value="in-scadenza">In scadenza</option>
|
||||
<option value="scaduta">Scadute</option>
|
||||
<option value="completata">Completate</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Reparto</label>
|
||||
<select id="filterDepartment" class="form-select">
|
||||
<option value="">Tutti</option>
|
||||
<?php foreach ($departments as $dept): ?>
|
||||
<option value="<?= htmlspecialchars($dept, ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($dept, ENT_QUOTES, 'UTF-8') ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label class="form-label fw-semibold">Responsabile</label>
|
||||
<select id="filterEmployee" class="form-select">
|
||||
<option value="">Tutti</option>
|
||||
<?php foreach ($employees as $emp): ?>
|
||||
<option value="<?= htmlspecialchars(trim($emp['first_name'] . ' ' . $emp['last_name']), ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars(trim($emp['first_name'] . ' ' . $emp['last_name']), ENT_QUOTES, 'UTF-8') ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light border" id="btnResetFiltersModal">
|
||||
<i class="fa-solid fa-rotate-left me-1"></i> Reset
|
||||
</button>
|
||||
<button type="button" class="btn btn-scad-primary" data-bs-dismiss="modal">
|
||||
<i class="fa-solid fa-check me-1"></i> Applica
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card scad-card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<h5 class="d-none d-md-flex align-items-center mb-0"><i class="fa-solid fa-calendar-days me-2"></i>Calendario Scadenze</h5>
|
||||
<div class="header-actions d-flex gap-2 flex-wrap ms-auto">
|
||||
<a href="scadenzario/index.php" class="btn btn-scad-outline d-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-list"></i><span>Lista Scadenze</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="legend">
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#5a8fd8"></span> Attiva</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#e8930c"></span> In scadenza</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#dc3545"></span> Scaduta</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#198754"></span> Completata</div>
|
||||
</div>
|
||||
<div id="calendar"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php include('../include/footer.php'); ?>
|
||||
</div>
|
||||
<?php include('../jsinclude.php'); ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var isMobile = window.innerWidth < 768;
|
||||
var calendarEl = document.getElementById('calendar');
|
||||
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
locale: 'it',
|
||||
initialView: isMobile ? 'listWeek' : 'dayGridMonth',
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: isMobile ? 'listWeek,dayGridMonth' : 'dayGridMonth,listWeek'
|
||||
},
|
||||
height: 'auto',
|
||||
navLinks: true,
|
||||
eventSources: [{
|
||||
url: 'scadenzario/ajax/get_calendar_events.php',
|
||||
extraParams: function() {
|
||||
return {
|
||||
status: document.getElementById('filterStatus').value,
|
||||
department: document.getElementById('filterDepartment').value,
|
||||
employee: document.getElementById('filterEmployee').value
|
||||
};
|
||||
},
|
||||
failure: function() {
|
||||
Swal.fire('Errore', 'Impossibile caricare gli eventi.', 'error');
|
||||
}
|
||||
}],
|
||||
eventClick: function(info) {
|
||||
info.jsEvent.preventDefault();
|
||||
if (info.event.url) {
|
||||
window.location.href = info.event.url;
|
||||
}
|
||||
},
|
||||
windowResize: function(view) {
|
||||
if (window.innerWidth < 768) {
|
||||
calendar.changeView('listWeek');
|
||||
} else {
|
||||
calendar.changeView('dayGridMonth');
|
||||
}
|
||||
}
|
||||
});
|
||||
calendar.render();
|
||||
|
||||
// Filters
|
||||
function updateFilterBadge() {
|
||||
var active = 0;
|
||||
var summary = [];
|
||||
var st = document.getElementById('filterStatus');
|
||||
var stv = st.value;
|
||||
if (stv && stv !== 'non-completata') { active++; summary.push(st.options[st.selectedIndex].text); }
|
||||
var dept = document.getElementById('filterDepartment').value;
|
||||
if (dept) { active++; summary.push('Reparto: ' + dept); }
|
||||
var emp = document.getElementById('filterEmployee').value;
|
||||
if (emp) { active++; summary.push('Responsabile: ' + emp); }
|
||||
|
||||
var badge = document.getElementById('filterCountBadge');
|
||||
if (active > 0) { badge.textContent = active; badge.classList.remove('d-none'); }
|
||||
else { badge.classList.add('d-none'); }
|
||||
|
||||
var summaryText = summary.length ? summary.slice(0, 2).join(' • ') + (summary.length > 2 ? ' +' + (summary.length - 2) : '') : '';
|
||||
document.getElementById('activeFiltersSummary').textContent = summaryText;
|
||||
document.getElementById('activeFiltersSummaryMobile').textContent = summaryText;
|
||||
}
|
||||
|
||||
document.querySelectorAll('#filterStatus, #filterDepartment, #filterEmployee').forEach(function(el) {
|
||||
el.addEventListener('change', function() { calendar.refetchEvents(); updateFilterBadge(); });
|
||||
});
|
||||
|
||||
function resetFilters() {
|
||||
document.getElementById('filterStatus').value = 'non-completata';
|
||||
document.getElementById('filterDepartment').value = '';
|
||||
document.getElementById('filterEmployee').value = '';
|
||||
calendar.refetchEvents();
|
||||
updateFilterBadge();
|
||||
}
|
||||
document.getElementById('btnResetFilters').addEventListener('click', resetFilters);
|
||||
document.getElementById('btnResetFiltersModal').addEventListener('click', resetFilters);
|
||||
|
||||
updateFilterBadge();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
/**
|
||||
* Scadenzario — Email notification cron script
|
||||
* Run daily: 0 7 * * * php /var/www/html/public/userarea/scadenzario/cron/send_notifications.php
|
||||
*
|
||||
* Sends "approaching" emails N days before due_date (per-deadline notification_days).
|
||||
* Sends "overdue" emails when due_date has passed.
|
||||
* Skips completed deadlines and already-sent notifications (same deadline+employee+type+date).
|
||||
* Email is taken from employees.auth_user_id → auth_users.email.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../class/db-functions.php';
|
||||
require_once __DIR__ . '/../../../../vendor/autoload.php';
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
$dotenv = Dotenv::createImmutable(__DIR__ . '/../../../../');
|
||||
$dotenv->load();
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$appUrl = rtrim($_ENV['APP_URL'] ?? 'http://localhost:8001', '/');
|
||||
|
||||
// Manager email for Cc — taken from MANAGER_USER_ID → auth_users.email
|
||||
$managerCcEmail = null;
|
||||
if (!empty($_ENV['MANAGER_USER_ID']) && is_numeric($_ENV['MANAGER_USER_ID'])) {
|
||||
$mgrStmt = $pdo->prepare("SELECT email FROM auth_users WHERE id = ?");
|
||||
$mgrStmt->execute([(int)$_ENV['MANAGER_USER_ID']]);
|
||||
$mgrEmail = $mgrStmt->fetchColumn();
|
||||
if (!empty($mgrEmail)) {
|
||||
$managerCcEmail = $mgrEmail;
|
||||
}
|
||||
}
|
||||
|
||||
$sent = 0;
|
||||
$skipped = 0;
|
||||
$errors = 0;
|
||||
|
||||
// Get active deadlines that are approaching or overdue
|
||||
$stmt = $pdo->query("
|
||||
SELECT d.id, d.topic, s.name AS subject_name, d.due_date, d.notification_days
|
||||
FROM scad_deadlines d
|
||||
LEFT JOIN scad_subjects s ON s.id = d.subject_id
|
||||
WHERE d.status = 'active'
|
||||
AND d.due_date <= DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)
|
||||
");
|
||||
$deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($deadlines)) {
|
||||
echo date('Y-m-d H:i:s') . " — Nessuna scadenza da notificare.\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Prepare statements
|
||||
$getRecipients = $pdo->prepare("
|
||||
SELECT DISTINCT e.id as employee_id, au.email, e.first_name, e.last_name
|
||||
FROM scad_deadline_employee de
|
||||
JOIN employees e ON e.id = de.employee_id
|
||||
JOIN auth_users au ON au.id = e.auth_user_id
|
||||
WHERE de.deadline_id = ?
|
||||
AND e.auth_user_id IS NOT NULL
|
||||
AND au.email IS NOT NULL
|
||||
AND au.email != ''
|
||||
");
|
||||
|
||||
// Also get employees from assigned departments
|
||||
$getDeptRecipients = $pdo->prepare("
|
||||
SELECT DISTINCT e.id as employee_id, au.email, e.first_name, e.last_name
|
||||
FROM employees e
|
||||
JOIN auth_users au ON au.id = e.auth_user_id
|
||||
WHERE e.department IN (SELECT TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(d.departments, ',', n.n), ',', -1))
|
||||
FROM scad_deadlines d
|
||||
CROSS JOIN (SELECT 1 n UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) n
|
||||
WHERE d.id = ?
|
||||
AND d.departments IS NOT NULL
|
||||
AND n.n <= 1 + LENGTH(d.departments) - LENGTH(REPLACE(d.departments, ',', '')))
|
||||
AND e.auth_user_id IS NOT NULL
|
||||
AND au.email IS NOT NULL
|
||||
AND au.email != ''
|
||||
");
|
||||
|
||||
$checkSent = $pdo->prepare("
|
||||
SELECT COUNT(*) FROM scad_deadline_notifications
|
||||
WHERE deadline_id = ? AND employee_id = ? AND type = ? AND DATE(sent_at) = CURDATE()
|
||||
");
|
||||
|
||||
$insertNotif = $pdo->prepare("
|
||||
INSERT INTO scad_deadline_notifications (deadline_id, employee_id, type) VALUES (?, ?, ?)
|
||||
");
|
||||
|
||||
$insertHistory = $pdo->prepare("
|
||||
INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, NULL, 'notification_sent', ?)
|
||||
");
|
||||
|
||||
foreach ($deadlines as $dl) {
|
||||
$isOverdue = $dl['due_date'] < $today;
|
||||
$type = $isOverdue ? 'overdue' : 'approaching';
|
||||
$daysLeft = (int)((strtotime($dl['due_date']) - strtotime($today)) / 86400);
|
||||
|
||||
// Collect all recipients (direct + department)
|
||||
$recipients = [];
|
||||
|
||||
$getRecipients->execute([$dl['id']]);
|
||||
foreach ($getRecipients->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||
$recipients[$r['employee_id']] = $r;
|
||||
}
|
||||
|
||||
$getDeptRecipients->execute([$dl['id']]);
|
||||
foreach ($getDeptRecipients->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||
$recipients[$r['employee_id']] = $r;
|
||||
}
|
||||
|
||||
if (empty($recipients)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($recipients as $emp) {
|
||||
// Check if already sent today
|
||||
$checkSent->execute([$dl['id'], $emp['employee_id'], $type]);
|
||||
if ($checkSent->fetchColumn() > 0) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Send email
|
||||
try {
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
// SMTP config from .env
|
||||
$mailer = $_ENV['MAIL_MAILER'] ?? 'mail';
|
||||
if ($mailer === 'smtp') {
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $_ENV['MAIL_HOST'] ?? 'localhost';
|
||||
$mail->Port = (int)($_ENV['MAIL_PORT'] ?? 587);
|
||||
if (!empty($_ENV['MAIL_USERNAME']) && $_ENV['MAIL_USERNAME'] !== 'null') {
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $_ENV['MAIL_USERNAME'];
|
||||
$mail->Password = $_ENV['MAIL_PASSWORD'] ?? '';
|
||||
}
|
||||
$enc = $_ENV['MAIL_ENCRYPTION'] ?? '';
|
||||
if ($enc && $enc !== 'null') {
|
||||
$mail->SMTPSecure = $enc;
|
||||
}
|
||||
}
|
||||
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->setFrom(
|
||||
$_ENV['MAIL_FROM_ADDRESS'] ?? 'noreply@zibogomma.it',
|
||||
$_ENV['MAIL_FROM_NAME'] ?? 'Scadenzario ZIBOGOMMA'
|
||||
);
|
||||
$mail->addAddress($emp['email'], trim($emp['first_name'] . ' ' . $emp['last_name']));
|
||||
|
||||
// Cc the manager (unless they are the direct recipient)
|
||||
if ($managerCcEmail && strcasecmp($managerCcEmail, $emp['email']) !== 0) {
|
||||
$mail->addCC($managerCcEmail);
|
||||
}
|
||||
|
||||
$detailUrl = $appUrl . '/userarea/scadenzario/detail.php?id=' . $dl['id'];
|
||||
$topicText = (!empty($dl['subject_name']) ? $dl['subject_name'] . ' — ' : '') . $dl['topic'];
|
||||
|
||||
if ($isOverdue) {
|
||||
$mail->Subject = '⚠️ Scadenza superata: ' . $dl['topic'];
|
||||
$mail->Body = buildHtml(
|
||||
'Scadenza superata',
|
||||
$topicText,
|
||||
'La scadenza era prevista per il <strong>' . date('d/m/Y', strtotime($dl['due_date'])) . '</strong> ed è stata superata da <strong>' . abs($daysLeft) . ' giorni</strong>.',
|
||||
'#dc3545',
|
||||
$detailUrl
|
||||
);
|
||||
} else {
|
||||
$mail->Subject = '📅 Scadenza in arrivo: ' . $dl['topic'];
|
||||
$daysText = $daysLeft === 0 ? 'oggi' : 'tra <strong>' . $daysLeft . ' giorni</strong>';
|
||||
$mail->Body = buildHtml(
|
||||
'Scadenza in arrivo',
|
||||
$topicText,
|
||||
'La scadenza è prevista per il <strong>' . date('d/m/Y', strtotime($dl['due_date'])) . '</strong> (' . $daysText . ').',
|
||||
'#e8930c',
|
||||
$detailUrl
|
||||
);
|
||||
}
|
||||
|
||||
$mail->isHTML(true);
|
||||
$mail->AltBody = strip_tags(str_replace('<br>', "\n", $mail->Body));
|
||||
|
||||
$mail->send();
|
||||
|
||||
// Record notification
|
||||
$insertNotif->execute([$dl['id'], $emp['employee_id'], $type]);
|
||||
$sent++;
|
||||
|
||||
echo date('H:i:s') . " ✓ {$type} → {$emp['email']} — {$dl['topic']}\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
$errors++;
|
||||
echo date('H:i:s') . " ✗ Errore {$emp['email']}: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
|
||||
// History (one per deadline, not per recipient)
|
||||
$recipientNames = implode(', ', array_map(fn($r) => trim($r['first_name'] . ' ' . $r['last_name']), $recipients));
|
||||
$insertHistory->execute([$dl['id'], "Notifica {$type} inviata a: {$recipientNames}"]);
|
||||
}
|
||||
|
||||
echo "\n" . date('Y-m-d H:i:s') . " — Completato. Inviate: {$sent}, Saltate: {$skipped}, Errori: {$errors}\n";
|
||||
|
||||
// --- HTML email template ---
|
||||
function buildHtml(string $title, string $topic, string $message, string $accentColor, string $url): string
|
||||
{
|
||||
return '
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="UTF-8"></head>
|
||||
<body style="margin:0;padding:0;background:#f4f6f9;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="padding:30px 0">
|
||||
<tr><td align="center">
|
||||
<table width="560" cellpadding="0" cellspacing="0" style="background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.06)">
|
||||
<tr><td style="background:' . $accentColor . ';padding:20px 30px">
|
||||
<h1 style="margin:0;color:#fff;font-size:18px">' . htmlspecialchars($title) . '</h1>
|
||||
</td></tr>
|
||||
<tr><td style="padding:30px">
|
||||
<h2 style="margin:0 0 15px;color:#2c3e6b;font-size:16px">' . htmlspecialchars($topic) . '</h2>
|
||||
<p style="margin:0 0 20px;color:#444;font-size:14px;line-height:1.6">' . $message . '</p>
|
||||
<a href="' . htmlspecialchars($url) . '" style="display:inline-block;background:#5a8fd8;color:#fff;padding:10px 24px;border-radius:6px;text-decoration:none;font-weight:600;font-size:14px">Vai alla scadenza</a>
|
||||
</td></tr>
|
||||
<tr><td style="padding:15px 30px;background:#f8f9fb;border-top:1px solid #eee">
|
||||
<p style="margin:0;color:#999;font-size:11px">ZIBOGOMMA — Scadenzario</p>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>';
|
||||
}
|
||||
@@ -0,0 +1,877 @@
|
||||
<?php include('../include/headscript.php'); ?>
|
||||
<?php
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
$error = null;
|
||||
$deadline = null;
|
||||
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
$error = 'ID non valido.';
|
||||
} else {
|
||||
$id = (int)$_GET['id'];
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT d.*, s.name AS subject_name, s.color AS subject_color
|
||||
FROM scad_deadlines d
|
||||
LEFT JOIN scad_subjects s ON s.id = d.subject_id
|
||||
WHERE d.id = ?
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$deadline = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$deadline) {
|
||||
$error = 'Scadenza non trovata.';
|
||||
} else {
|
||||
$empStmt = $pdo->prepare("
|
||||
SELECT e.first_name, e.last_name, e.department
|
||||
FROM scad_deadline_employee de
|
||||
JOIN employees e ON e.id = de.employee_id
|
||||
WHERE de.deadline_id = ?
|
||||
ORDER BY e.first_name
|
||||
");
|
||||
$empStmt->execute([$id]);
|
||||
$employees = $empStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$attStmt = $pdo->prepare("SELECT * FROM scad_deadline_attachments WHERE deadline_id = ? ORDER BY created_at DESC");
|
||||
$attStmt->execute([$id]);
|
||||
$attachments = $attStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$histStmt = $pdo->prepare("
|
||||
SELECT h.*, au.first_name as user_fname, au.last_name as user_lname
|
||||
FROM scad_deadline_histories h
|
||||
LEFT JOIN auth_users au ON au.id = h.user_id
|
||||
WHERE h.deadline_id = ?
|
||||
ORDER BY h.created_at DESC
|
||||
");
|
||||
$histStmt->execute([$id]);
|
||||
$history = $histStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$isCompleted = $deadline['status'] === 'completed';
|
||||
$isOverdue = !$isCompleted && $deadline['due_date'] < $today;
|
||||
$approachDate = date('Y-m-d', strtotime($today . ' + ' . (int)$deadline['notification_days'] . ' days'));
|
||||
$isApproaching = !$isCompleted && !$isOverdue && $deadline['due_date'] <= $approachDate;
|
||||
|
||||
if ($isCompleted) {
|
||||
$statusLabel = 'Completata';
|
||||
$statusClass = 'badge-completata';
|
||||
} elseif ($isOverdue) {
|
||||
$statusLabel = 'Scaduta';
|
||||
$statusClass = 'badge-scaduta';
|
||||
} elseif ($isApproaching) {
|
||||
$statusLabel = 'In scadenza';
|
||||
$statusClass = 'badge-in-scadenza';
|
||||
} else {
|
||||
$statusLabel = 'Attiva';
|
||||
$statusClass = 'badge-attiva';
|
||||
}
|
||||
|
||||
$recurrenceLabels = ['once' => 'Una tantum', 'monthly' => 'Mensile', 'quarterly' => 'Trimestrale', 'semiannual' => 'Semestrale', 'annual' => 'Annuale', 'biennial' => 'Biennale', 'triennial' => 'Triennale', 'quadriennial' => 'Quadriennale', 'quinquennial' => 'Quinquennale', 'decennial' => 'Decennale', 'quindecennial' => 'Quindicennale'];
|
||||
$actionLabels = ['created' => 'Creata', 'updated' => 'Modificata', 'completed' => 'Completata', 'attachment_added' => 'Allegato aggiunto', 'attachment_removed' => 'Allegato rimosso', 'attachment_linked' => 'Allegato collegato', 'attachment_unlinked' => 'Collegamento rimosso', 'notification_sent' => 'Notifica inviata'];
|
||||
$actionColors = ['created' => '#198754', 'updated' => '#5a8fd8', 'completed' => '#6f42c1', 'attachment_added' => '#e8930c', 'attachment_removed' => '#e8930c', 'attachment_linked' => '#0dcaf0', 'attachment_unlinked' => '#adb5bd', 'notification_sent' => '#adb5bd'];
|
||||
$actionIcons = ['created' => 'fa-plus', 'updated' => 'fa-pen', 'completed' => 'fa-check', 'attachment_added' => 'fa-paperclip', 'attachment_removed' => 'fa-trash', 'attachment_linked' => 'fa-link', 'attachment_unlinked' => 'fa-link-slash', 'notification_sent' => 'fa-bell'];
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<?php
|
||||
$scriptDir = dirname($_SERVER['SCRIPT_NAME']);
|
||||
$baseHref = dirname($scriptDir) . '/';
|
||||
?>
|
||||
<base href="<?= $baseHref ?>">
|
||||
<?php include('../cssinclude.php'); ?>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/i18n/it.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/it.js"></script>
|
||||
<?php include __DIR__ . '/include/deadline_modal_css.php'; ?>
|
||||
<title><?= $deadline ? htmlspecialchars($deadline['topic'], ENT_QUOTES, 'UTF-8') . ' — ' : '' ?>Scadenzario</title>
|
||||
<script>
|
||||
if (window.innerWidth > 1024) document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('appWrapper').classList.add('toggled')
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
:root {
|
||||
--scad-primary: #5a8fd8;
|
||||
--scad-primary-hover: #4578c0;
|
||||
--scad-heading: #2c3e6b;
|
||||
--scad-card-bg: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%);
|
||||
--scad-card-border: #dde4f0;
|
||||
--scad-red: #dc3545;
|
||||
--scad-orange: #e8930c;
|
||||
--scad-green: #198754;
|
||||
}
|
||||
|
||||
.scad-card {
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scad-card .card-header {
|
||||
background: var(--scad-card-bg);
|
||||
border-bottom: 1px solid var(--scad-card-border);
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.scad-card .card-header h5 {
|
||||
font-weight: 700;
|
||||
color: var(--scad-heading);
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.scad-card .card-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.btn-scad-primary {
|
||||
background: var(--scad-primary);
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-scad-primary:hover {
|
||||
background: var(--scad-primary-hover);
|
||||
color: #fff;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(90, 143, 216, 0.35);
|
||||
}
|
||||
|
||||
.btn-scad-outline {
|
||||
background: transparent;
|
||||
border: 1.5px solid var(--scad-primary);
|
||||
color: var(--scad-primary);
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.45rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-scad-outline:hover {
|
||||
background: var(--scad-primary);
|
||||
color: #fff;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-scad-green {
|
||||
background: var(--scad-green);
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-scad-green:hover {
|
||||
background: #157347;
|
||||
color: #fff;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(25, 135, 84, 0.35);
|
||||
}
|
||||
|
||||
.badge-status {
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
padding: 0.4em 0.75em;
|
||||
border-radius: 2rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.badge-attiva {
|
||||
background: #e8eeff;
|
||||
color: #3a6bb5;
|
||||
}
|
||||
|
||||
.badge-scaduta {
|
||||
background: #fde8e8;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.badge-in-scadenza {
|
||||
background: #fef3cd;
|
||||
color: #92600a;
|
||||
}
|
||||
|
||||
.badge-completata {
|
||||
background: #d1f2e0;
|
||||
color: #0f5132;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: #8e99b0;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 0.95rem;
|
||||
color: var(--scad-heading);
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.detail-value.text-danger-date {
|
||||
color: var(--scad-red);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-value.text-warning-date {
|
||||
color: var(--scad-orange);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.person-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
background: #f0f4ff;
|
||||
border: 1px solid #dde4f0;
|
||||
border-radius: 2rem;
|
||||
padding: 0.3rem 0.75rem 0.3rem 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
margin: 0.2rem 0.15rem;
|
||||
color: var(--scad-heading);
|
||||
}
|
||||
|
||||
.person-chip i {
|
||||
color: var(--scad-primary);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.person-chip .chip-dept {
|
||||
color: #8e99b0;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.dept-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
background: #eef6ee;
|
||||
border: 1px solid #c8e6c9;
|
||||
border-radius: 2rem;
|
||||
padding: 0.3rem 0.75rem 0.3rem 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
margin: 0.2rem 0.15rem;
|
||||
color: #2e5e2e;
|
||||
}
|
||||
|
||||
.dept-chip i {
|
||||
color: #4caf50;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Attachments */
|
||||
.att-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.65rem 0;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
}
|
||||
|
||||
.att-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.att-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 0.4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.att-icon-pdf {
|
||||
background: #fde8e8;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.att-icon-img {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.att-icon-file {
|
||||
background: #e8eeff;
|
||||
color: #3a6bb5;
|
||||
}
|
||||
|
||||
.att-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.att-name {
|
||||
font-weight: 600;
|
||||
color: var(--scad-heading);
|
||||
font-size: 0.9rem;
|
||||
text-decoration: none;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.att-name:hover {
|
||||
color: var(--scad-primary);
|
||||
}
|
||||
|
||||
.att-meta {
|
||||
font-size: 0.78rem;
|
||||
color: #8e99b0;
|
||||
}
|
||||
|
||||
/* Timeline */
|
||||
.timeline {
|
||||
position: relative;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.timeline::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0.55rem;
|
||||
top: 0.5rem;
|
||||
bottom: 0.5rem;
|
||||
width: 2px;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
padding-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.timeline-item:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
position: absolute;
|
||||
left: -1.7rem;
|
||||
top: 0.15rem;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.6rem;
|
||||
color: #fff;
|
||||
z-index: 1;
|
||||
box-shadow: 0 0 0 3px #fff;
|
||||
}
|
||||
|
||||
.timeline-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
|
||||
.timeline-action {
|
||||
font-weight: 700;
|
||||
font-size: 0.88rem;
|
||||
color: var(--scad-heading);
|
||||
}
|
||||
|
||||
.timeline-user {
|
||||
font-size: 0.82rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.timeline-date {
|
||||
font-size: 0.78rem;
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.timeline-notes {
|
||||
font-size: 0.83rem;
|
||||
color: #6c757d;
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
.timeline-changes {
|
||||
font-size: 0.82rem;
|
||||
margin-top: 0.3rem;
|
||||
background: #f8f9fb;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.timeline-changes .change-field {
|
||||
font-weight: 600;
|
||||
color: var(--scad-heading);
|
||||
}
|
||||
|
||||
.timeline-changes .change-old {
|
||||
color: var(--scad-red);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.timeline-changes .change-new {
|
||||
color: var(--scad-green);
|
||||
}
|
||||
|
||||
.scad-breadcrumb {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.scad-breadcrumb .breadcrumb-item a {
|
||||
color: var(--scad-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.scad-breadcrumb .breadcrumb-item a:hover {
|
||||
color: var(--scad-primary-hover);
|
||||
}
|
||||
|
||||
.scad-breadcrumb .breadcrumb-item.active {
|
||||
color: #6c757d;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.action-bar {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-bar .btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
|
||||
.sidebar-wrapper,
|
||||
.topbar,
|
||||
.page-footer,
|
||||
.action-bar,
|
||||
.scad-breadcrumb {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.scad-card {
|
||||
box-shadow: none;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
@page {
|
||||
size: portrait;
|
||||
margin: 1cm;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-law-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
margin-left: 0.65rem;
|
||||
padding: 0.42rem 0.85rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(90, 143, 216, 0.45);
|
||||
background: linear-gradient(135deg, #5a8fd8 0%, #6f42c1 100%);
|
||||
color: #ffffff !important;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
box-shadow: 0 4px 14px rgba(90, 143, 216, 0.35);
|
||||
letter-spacing: 0.02em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ai-law-btn i {
|
||||
font-size: 0.82rem;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.ai-law-btn:hover {
|
||||
color: #ffffff !important;
|
||||
background: linear-gradient(135deg, #4578c0 0%, #5b35a5 100%);
|
||||
box-shadow: 0 6px 18px rgba(90, 143, 216, 0.45);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('../include/navbar.php'); ?>
|
||||
<?php include('../include/topbar.php'); ?>
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger">
|
||||
<i class="fa-solid fa-triangle-exclamation me-2"></i><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?>
|
||||
<a href="scadenzario/index.php" class="alert-link ms-2">Torna alla lista</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="scad-breadcrumb" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><a href="scadenzario/index.php">Scadenzario</a></li>
|
||||
<li class="breadcrumb-item"><a href="scadenzario/index.php">Lista Scadenze</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page"><?= htmlspecialchars($deadline['topic'], ENT_QUOTES, 'UTF-8') ?></li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Action Bar -->
|
||||
<div class="action-bar d-flex gap-2 mb-3 flex-wrap">
|
||||
<a href="scadenzario/index.php" class="btn btn-scad-outline d-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-arrow-left"></i><span>Torna alla lista</span>
|
||||
</a>
|
||||
<?php if (!$isCompleted): ?>
|
||||
<button class="btn btn-scad-primary d-inline-flex align-items-center gap-2" id="btnModifica">
|
||||
<i class="fa-solid fa-pen"></i><span>Modifica</span>
|
||||
</button>
|
||||
<button class="btn btn-scad-green d-inline-flex align-items-center gap-2" id="btnCompleta">
|
||||
<i class="fa-solid fa-check"></i><span>Completa</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<button class="btn btn-scad-outline d-inline-flex align-items-center gap-2" onclick="window.print()">
|
||||
<i class="fa-solid fa-print"></i><span>Stampa</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Main Detail Card -->
|
||||
<div class="card scad-card mb-3">
|
||||
<div class="card-header">
|
||||
<h5><i class="fa-solid fa-file-lines me-2"></i>Dettagli Scadenza</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<!-- Left Column -->
|
||||
<div class="col-12 col-md-6">
|
||||
<?php if (!empty($deadline['subject_name'])): ?>
|
||||
<div class="detail-label">Argomento</div>
|
||||
<div class="detail-value">
|
||||
<span style="display:inline-block;padding:0.25rem 0.7rem;border-radius:1rem;color:#fff;font-weight:600;font-size:0.85rem;background: <?= htmlspecialchars($deadline['subject_color'] ?: '#6c757d', ENT_QUOTES, 'UTF-8') ?>">
|
||||
<?= htmlspecialchars($deadline['subject_name'], ENT_QUOTES, 'UTF-8') ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="detail-label">Dettaglio</div>
|
||||
<div class="detail-value" style="font-size:1.15rem; font-weight:700;">
|
||||
<?= htmlspecialchars($deadline['topic'], ENT_QUOTES, 'UTF-8') ?>
|
||||
</div>
|
||||
|
||||
<?php if ($deadline['law_regulation']): ?>
|
||||
<div class="detail-label">Legge / Articolo</div>
|
||||
<div class="detail-value">
|
||||
<?= htmlspecialchars($deadline['law_regulation'], ENT_QUOTES, 'UTF-8') ?>
|
||||
|
||||
<span class="ai-law-btn" title="Funzione AI disponibile prossimamente">
|
||||
<i class="fa-solid fa-wand-magic-sparkles"></i>
|
||||
AI
|
||||
</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="detail-label">Periodicità</div>
|
||||
<div class="detail-value"><?= htmlspecialchars($recurrenceLabels[$deadline['recurrence_type']] ?? $deadline['recurrence_type'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="detail-label">Stato</div>
|
||||
<div class="detail-value">
|
||||
<span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span>
|
||||
<?php if ($isCompleted && $deadline['completed_at']): ?>
|
||||
<span class="text-muted ms-2" style="font-size:0.82rem"><?= date('d/m/Y H:i', strtotime($deadline['completed_at'])) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="detail-label">Data scadenza</div>
|
||||
<div class="detail-value <?= $isOverdue ? 'text-danger-date' : ($isApproaching ? 'text-warning-date' : '') ?>">
|
||||
<i class="fa-regular fa-calendar me-1"></i><?= date('d/m/Y', strtotime($deadline['due_date'])) ?>
|
||||
<?php if ($isOverdue): ?><span class="ms-1" style="font-size:0.8rem">(scaduta)</span><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($deadline['document_date']): ?>
|
||||
<div class="detail-label">Data documento</div>
|
||||
<div class="detail-value"><?= date('d/m/Y', strtotime($deadline['document_date'])) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="detail-label">Data ultimo controllo</div>
|
||||
<div class="detail-value"><?= $deadline['check_date'] ? date('d/m/Y', strtotime($deadline['check_date'])) : '—' ?></div>
|
||||
|
||||
<div class="detail-label">Giorni preavviso notifica</div>
|
||||
<div class="detail-value"><?= (int)$deadline['notification_days'] ?> giorni</div>
|
||||
|
||||
<?php if ($deadline['storage_location']): ?>
|
||||
<div class="detail-label">Luogo archiviazione</div>
|
||||
<div class="detail-value"><i class="fa-regular fa-folder-open me-1"></i><?= htmlspecialchars($deadline['storage_location'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($deadline['notes']): ?>
|
||||
<div class="detail-label">Note</div>
|
||||
<div class="detail-value"><?= nl2br(htmlspecialchars($deadline['notes'], ENT_QUOTES, 'UTF-8')) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Responsabili -->
|
||||
<?php if ($deadline['departments'] || !empty($employees)): ?>
|
||||
<hr class="my-3" style="border-color:#e8eeff">
|
||||
<?php if ($deadline['departments']): ?>
|
||||
<div class="detail-label">Reparti responsabili</div>
|
||||
<div class="detail-value">
|
||||
<?php foreach (array_map('trim', explode(',', $deadline['departments'])) as $dept): ?>
|
||||
<span class="dept-chip"><i class="fa-solid fa-building"></i><?= htmlspecialchars($dept, ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($employees)): ?>
|
||||
<div class="detail-label">Singoli responsabili</div>
|
||||
<div class="detail-value">
|
||||
<?php foreach ($employees as $emp): ?>
|
||||
<span class="person-chip">
|
||||
<i class="fa-solid fa-user"></i>
|
||||
<?= htmlspecialchars(trim($emp['first_name'] . ' ' . $emp['last_name']), ENT_QUOTES, 'UTF-8') ?>
|
||||
<?php if ($emp['department']): ?>
|
||||
<span class="chip-dept">(<?= htmlspecialchars($emp['department'], ENT_QUOTES, 'UTF-8') ?>)</span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Attachments Card -->
|
||||
<?php if (!empty($attachments)): ?>
|
||||
<div class="card scad-card mb-3">
|
||||
<div class="card-header">
|
||||
<h5><i class="fa-solid fa-paperclip me-2"></i>Allegati (<?= count($attachments) ?>)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php foreach ($attachments as $att):
|
||||
$mime = $att['mime_type'] ?? '';
|
||||
if (strpos($mime, 'pdf') !== false) {
|
||||
$iconClass = 'att-icon-pdf';
|
||||
$icon = 'fa-file-pdf';
|
||||
} elseif (strpos($mime, 'image') !== false) {
|
||||
$iconClass = 'att-icon-img';
|
||||
$icon = 'fa-file-image';
|
||||
} else {
|
||||
$iconClass = 'att-icon-file';
|
||||
$icon = 'fa-file';
|
||||
}
|
||||
$sizeKB = round(($att['size'] ?? 0) / 1024, 1);
|
||||
$sizeStr = $sizeKB >= 1024 ? round($sizeKB / 1024, 1) . ' MB' : $sizeKB . ' KB';
|
||||
?>
|
||||
<div class="att-row">
|
||||
<div class="att-icon <?= $iconClass ?>"><i class="fa-solid <?= $icon ?>"></i></div>
|
||||
<div class="att-info">
|
||||
<a href="scadenzario/ajax/download_attachment.php?id=<?= (int)$att['id'] ?>" class="att-name"><?= htmlspecialchars($att['original_name'], ENT_QUOTES, 'UTF-8') ?></a>
|
||||
<div class="att-meta"><?= $sizeStr ?> · <?= date('d/m/Y', strtotime($att['created_at'])) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- History Card -->
|
||||
<?php if (!empty($history)): ?>
|
||||
<div class="card scad-card mb-3">
|
||||
<div class="card-header">
|
||||
<h5><i class="fa-solid fa-clock-rotate-left me-2"></i>Cronologia</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="timeline">
|
||||
<?php foreach ($history as $h):
|
||||
$color = $actionColors[$h['action']] ?? '#adb5bd';
|
||||
$iconCls = $actionIcons[$h['action']] ?? 'fa-circle';
|
||||
$label = $actionLabels[$h['action']] ?? $h['action'];
|
||||
$userName = trim(($h['user_fname'] ?? '') . ' ' . ($h['user_lname'] ?? '')) ?: 'Sistema';
|
||||
$changes = $h['changes'] ? json_decode($h['changes'], true) : null;
|
||||
?>
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-dot" style="background:<?= $color ?>"><i class="fa-solid <?= $iconCls ?>"></i></div>
|
||||
<div class="timeline-header">
|
||||
<span class="timeline-action"><?= htmlspecialchars($label, ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<span class="timeline-user">da <?= htmlspecialchars($userName, ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<span class="timeline-date"><?= date('d/m/Y H:i', strtotime($h['created_at'])) ?></span>
|
||||
</div>
|
||||
<?php if ($h['notes']): ?>
|
||||
<div class="timeline-notes"><i class="fa-regular fa-comment me-1"></i><?= htmlspecialchars($h['notes'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($changes && is_array($changes)): ?>
|
||||
<div class="timeline-changes">
|
||||
<?php foreach ($changes as $field => $vals): ?>
|
||||
<div>
|
||||
<span class="change-field"><?= htmlspecialchars($field, ENT_QUOTES, 'UTF-8') ?>:</span>
|
||||
<span class="change-old"><?= htmlspecialchars($vals['old'] ?? '—', ENT_QUOTES, 'UTF-8') ?></span>
|
||||
→
|
||||
<span class="change-new"><?= htmlspecialchars($vals['new'] ?? '—', ENT_QUOTES, 'UTF-8') ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php include('../include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<?php if ($deadline && !$isCompleted): ?>
|
||||
<?php require __DIR__ . '/include/deadline_form_data.php'; ?>
|
||||
<?php include __DIR__ . '/include/deadline_modal.php'; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php include('../jsinclude.php'); ?>
|
||||
<?php if ($deadline && !$isCompleted): ?>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Used by the shared modal JS to auto-open edit on "#edit"
|
||||
window.SCAD_DETAIL_ID = <?= (int)$deadline['id'] ?>;
|
||||
|
||||
$('#btnModifica').on('click', function() {
|
||||
window.openDeadlineEdit(<?= (int)$deadline['id'] ?>);
|
||||
});
|
||||
|
||||
function detailSubmitComplete(createNext, copyAttachments) {
|
||||
var fd = new FormData();
|
||||
fd.append('id', '<?= (int)$deadline['id'] ?>');
|
||||
fd.append('create_next', createNext ? '1' : '0');
|
||||
fd.append('copy_attachments', copyAttachments ? '1' : '0');
|
||||
|
||||
fetch('scadenzario/ajax/complete_deadline.php', {
|
||||
method: 'POST',
|
||||
body: fd
|
||||
})
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Completata',
|
||||
text: data.message,
|
||||
timer: 1800,
|
||||
showConfirmButton: false
|
||||
})
|
||||
.then(function() {
|
||||
if (data.new_id) {
|
||||
window.location.href = 'scadenzario/detail.php?id=' + data.new_id + '#edit';
|
||||
} else {
|
||||
window.location.href = 'scadenzario/index.php';
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Errore', data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
Swal.fire('Errore', 'Errore di connessione.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
$('#btnCompleta').on('click', function() {
|
||||
var recurrence = <?= json_encode($deadline['recurrence_type'] ?? 'once') ?>;
|
||||
var attCount = <?= count($attachments) ?>;
|
||||
|
||||
if (recurrence === 'once') {
|
||||
Swal.fire({
|
||||
title: 'Completare la scadenza?',
|
||||
text: 'La scadenza verrà contrassegnata come completata.',
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#198754',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonText: 'Completa',
|
||||
reverseButtons: true
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
detailSubmitComplete(false, false);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var attCheckbox = attCount > 0 ?
|
||||
'<div class="form-check d-flex align-items-center justify-content-center gap-2 mt-3">' +
|
||||
'<input class="form-check-input" type="checkbox" id="swCopyAtt" checked>' +
|
||||
'<label class="form-check-label" for="swCopyAtt">Copia gli allegati (' + attCount + ') sulla nuova scadenza</label>' +
|
||||
'</div>' :
|
||||
'';
|
||||
|
||||
Swal.fire({
|
||||
title: 'Completare la scadenza?',
|
||||
html: 'Vuoi creare automaticamente la prossima scadenza ricorrente?' + attCheckbox,
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
showDenyButton: true,
|
||||
confirmButtonColor: '#198754',
|
||||
denyButtonColor: '#6c757d',
|
||||
confirmButtonText: 'Completa e crea la prossima',
|
||||
denyButtonText: 'Completa senza nuova',
|
||||
cancelButtonText: 'Annulla',
|
||||
reverseButtons: true
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
var copy = attCount > 0 ? document.getElementById('swCopyAtt').checked : false;
|
||||
detailSubmitComplete(true, copy);
|
||||
} else if (result.isDenied) {
|
||||
detailSubmitComplete(false, false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php include __DIR__ . '/include/deadline_modal_js.php'; ?>
|
||||
<?php endif; ?>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../../ajax/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : 0;
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM scad_deadlines WHERE function_id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$inUse = (int)$stmt->fetchColumn();
|
||||
|
||||
if ($inUse > 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => "Impossibile eliminare: la funzione è utilizzata in $inUse scadenz" . ($inUse === 1 ? 'a' : 'e') . '.',
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo->prepare("DELETE FROM scad_functions WHERE id = ?")->execute([$id]);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => 'Funzione eliminata.']);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../../ajax/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : null;
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '') ?: null;
|
||||
|
||||
if ($name === '') {
|
||||
echo json_encode(['success' => false, 'message' => 'Il nome è obbligatorio.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (mb_strlen($name) > 255) {
|
||||
echo json_encode(['success' => false, 'message' => 'Il nome supera 255 caratteri.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($id) {
|
||||
$stmt = $pdo->prepare("SELECT id FROM scad_functions WHERE name = ? AND id <> ?");
|
||||
$stmt->execute([$name, $id]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("SELECT id FROM scad_functions WHERE name = ?");
|
||||
$stmt->execute([$name]);
|
||||
}
|
||||
|
||||
if ($stmt->fetch()) {
|
||||
echo json_encode(['success' => false, 'message' => 'Esiste già una funzione con questo nome.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($id) {
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE scad_functions
|
||||
SET name = ?, description = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$name, $description, $id]);
|
||||
$savedId = $id;
|
||||
} else {
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO scad_functions (name, description, status)
|
||||
VALUES (?, ?, 'active')
|
||||
");
|
||||
$stmt->execute([$name, $description]);
|
||||
$savedId = (int)$pdo->lastInsertId();
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => $id ? 'Funzione aggiornata.' : 'Funzione creata.',
|
||||
'id' => $savedId,
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
<?php include('../../include/headscript.php'); ?>
|
||||
<?php
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$functions = $pdo->query("
|
||||
SELECT f.*,
|
||||
(SELECT COUNT(*) FROM scad_deadlines d WHERE d.function_id = f.id) AS deadline_count,
|
||||
(SELECT COUNT(*) FROM scad_deadlines d WHERE d.function_id = f.id AND d.status <> 'completed') AS open_count
|
||||
FROM scad_functions f
|
||||
ORDER BY f.name ASC
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<?php
|
||||
$scriptDir = dirname($_SERVER['SCRIPT_NAME']);
|
||||
$baseHref = dirname(dirname($scriptDir)) . '/';
|
||||
?>
|
||||
<base href="<?= $baseHref ?>">
|
||||
<?php include('../../cssinclude.php'); ?>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<title>Scadenzario - Funzioni</title>
|
||||
<script>
|
||||
if (window.innerWidth > 1024) {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('appWrapper').classList.add('toggled');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
:root {
|
||||
--scad-primary: #5a8fd8;
|
||||
--scad-primary-hover: #4578c0;
|
||||
--scad-heading: #2c3e6b;
|
||||
--scad-card-bg: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%);
|
||||
--scad-card-border: #dde4f0;
|
||||
}
|
||||
|
||||
.scad-card {
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scad-card .card-header {
|
||||
background: var(--scad-card-bg);
|
||||
border-bottom: 1px solid var(--scad-card-border);
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.scad-card .card-header h5 {
|
||||
font-weight: 700;
|
||||
color: var(--scad-heading);
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.scad-card .card-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.btn-scad-primary {
|
||||
background: var(--scad-primary);
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-scad-primary:hover {
|
||||
background: var(--scad-primary-hover);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-scad-outline {
|
||||
background: transparent;
|
||||
border: 1.5px solid var(--scad-primary);
|
||||
color: var(--scad-primary);
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.45rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-scad-outline:hover {
|
||||
background: var(--scad-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 0.4rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.btn-action-edit {
|
||||
background: rgba(90, 143, 216, 0.12);
|
||||
color: var(--scad-primary);
|
||||
}
|
||||
|
||||
.btn-action-edit:hover {
|
||||
background: var(--scad-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-action-delete {
|
||||
background: rgba(220, 53, 69, 0.12);
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.btn-action-delete:hover {
|
||||
background: #dc3545;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.function-card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--scad-card-border);
|
||||
border-radius: 0.6rem;
|
||||
padding: 0.85rem 0.95rem;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.function-card .fc-name {
|
||||
font-weight: 700;
|
||||
color: var(--scad-heading);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.function-card .fc-stats {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.function-card .fc-stats strong {
|
||||
color: var(--scad-heading);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 3rem;
|
||||
opacity: 0.3;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.scad-card .card-header {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-actions .btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('../../include/navbar.php'); ?>
|
||||
<?php include('../../include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb" style="background:transparent;padding:0;margin:0;font-size:0.85rem">
|
||||
<li class="breadcrumb-item"><a href="scadenzario/index.php">Scadenzario</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Funzioni</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="card scad-card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<h5><i class="fa-solid fa-briefcase me-2"></i>Funzioni</h5>
|
||||
|
||||
<div class="header-actions d-flex gap-2 flex-wrap">
|
||||
<a href="scadenzario/index.php" class="btn btn-scad-outline d-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-arrow-left"></i><span>Scadenzario</span>
|
||||
</a>
|
||||
|
||||
<button class="btn btn-scad-primary d-inline-flex align-items-center gap-2" id="btnAddFunction">
|
||||
<i class="fa-solid fa-plus"></i><span>Nuova Funzione</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<?php if (count($functions) === 0): ?>
|
||||
<div class="empty-state">
|
||||
<i class="fa-solid fa-briefcase"></i>
|
||||
<p>Nessuna funzione definita.<br>Clicca <strong>"Nuova Funzione"</strong> per aggiungere la prima.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
|
||||
<div id="functionsList">
|
||||
|
||||
<div class="d-md-none">
|
||||
<?php foreach ($functions as $f): ?>
|
||||
<div class="function-card"
|
||||
data-id="<?= (int)$f['id'] ?>"
|
||||
data-name="<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-description="<?= htmlspecialchars($f['description'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-status="<?= htmlspecialchars($f['status'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-in-use="<?= (int)$f['deadline_count'] ?>">
|
||||
|
||||
<div class="fc-name"><?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
|
||||
<?php if (!empty($f['description'])): ?>
|
||||
<div class="text-muted small mt-1"><?= htmlspecialchars($f['description'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="fc-stats">
|
||||
<span>Scadenze: <strong><?= (int)$f['deadline_count'] ?></strong></span>
|
||||
<span>Aperte: <strong><?= (int)$f['open_count'] ?></strong></span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-1 justify-content-end">
|
||||
<button class="btn-action btn-action-edit btn-edit" title="Modifica">
|
||||
<i class="fa-solid fa-pen"></i>
|
||||
</button>
|
||||
<button class="btn-action btn-action-delete btn-delete" title="Elimina">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="d-none d-md-block">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>Descrizione</th>
|
||||
<th class="text-center" style="width:120px">Scadenze</th>
|
||||
<th class="text-center" style="width:120px">Aperte</th>
|
||||
<th class="text-center" style="width:120px">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($functions as $f): ?>
|
||||
<tr data-id="<?= (int)$f['id'] ?>"
|
||||
data-name="<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-description="<?= htmlspecialchars($f['description'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-status="<?= htmlspecialchars($f['status'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-in-use="<?= (int)$f['deadline_count'] ?>">
|
||||
|
||||
<td class="fw-semibold" style="color:var(--scad-heading)">
|
||||
<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>
|
||||
</td>
|
||||
<td class="text-muted">
|
||||
<?= htmlspecialchars($f['description'] ?? '—', ENT_QUOTES, 'UTF-8') ?>
|
||||
</td>
|
||||
<td class="text-center"><?= (int)$f['deadline_count'] ?></td>
|
||||
<td class="text-center"><?= (int)$f['open_count'] ?></td>
|
||||
<td class="text-center">
|
||||
<div class="d-inline-flex gap-1">
|
||||
<button class="btn-action btn-action-edit btn-edit" title="Modifica">
|
||||
<i class="fa-solid fa-pen"></i>
|
||||
</button>
|
||||
<button class="btn-action btn-action-delete btn-delete" title="Elimina">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include('../../include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="functionModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="functionModalTitle">Nuova Funzione</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
|
||||
</div>
|
||||
|
||||
<form id="functionForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="functionId" name="id" value="">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="functionName" class="form-label fw-semibold">Nome <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="functionName" name="name" maxlength="255" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="functionDescription" class="form-label fw-semibold">Descrizione</label>
|
||||
<textarea class="form-control" id="functionDescription" name="description" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-scad-primary">Salva</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include('../../jsinclude.php'); ?>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
function openModal(data) {
|
||||
const isEdit = !!data;
|
||||
|
||||
$('#functionModalTitle').text(isEdit ? 'Modifica Funzione' : 'Nuova Funzione');
|
||||
$('#functionId').val(isEdit ? data.id : '');
|
||||
$('#functionName').val(isEdit ? data.name : '');
|
||||
$('#functionDescription').val(isEdit ? data.description : '');
|
||||
|
||||
new bootstrap.Modal('#functionModal').show();
|
||||
}
|
||||
|
||||
$('#btnAddFunction').on('click', function() {
|
||||
openModal(null);
|
||||
});
|
||||
|
||||
$('#functionsList').on('click', '.btn-edit', function() {
|
||||
const $row = $(this).closest('[data-id]');
|
||||
|
||||
openModal({
|
||||
id: $row.data('id'),
|
||||
name: $row.data('name'),
|
||||
description: $row.data('description')
|
||||
});
|
||||
});
|
||||
|
||||
$('#functionsList').on('click', '.btn-delete', function() {
|
||||
const $row = $(this).closest('[data-id]');
|
||||
const inUse = parseInt($row.data('in-use') || 0, 10);
|
||||
const name = $row.data('name');
|
||||
|
||||
if (inUse > 0) {
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'Impossibile eliminare',
|
||||
text: `La funzione "${name}" è utilizzata in ${inUse} scadenz${inUse === 1 ? 'a' : 'e'}.`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: `Eliminare "${name}"?`,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Elimina',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonColor: '#dc3545'
|
||||
}).then(function(result) {
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
$.post('scadenzario/functions/ajax/delete_function.php', {
|
||||
id: $row.data('id')
|
||||
})
|
||||
.done(function(res) {
|
||||
if (res.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Errore',
|
||||
text: res.message
|
||||
});
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Errore di rete'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$('#functionForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const payload = {
|
||||
id: $('#functionId').val(),
|
||||
name: $('#functionName').val().trim(),
|
||||
description: $('#functionDescription').val().trim()
|
||||
};
|
||||
|
||||
if (!payload.name) {
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'Nome obbligatorio'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
$.post('scadenzario/functions/ajax/save_function.php', payload)
|
||||
.done(function(res) {
|
||||
if (res.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Errore',
|
||||
text: res.message
|
||||
});
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Errore di rete'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Shared data for the deadline modal form (used by index.php and detail.php).
|
||||
* Populates $employees, $departments, $subjects. Safe to include more than once.
|
||||
*/
|
||||
if (!isset($pdo) || !$pdo) {
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
}
|
||||
|
||||
if (!isset($employees)) {
|
||||
$employees = $pdo->query("
|
||||
SELECT e.id, e.first_name, e.last_name, e.department_id, dep.name AS department_name
|
||||
FROM employees e
|
||||
LEFT JOIN departments dep ON dep.id = e.department_id
|
||||
WHERE e.status = 'active'
|
||||
ORDER BY e.first_name, e.last_name
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
if (!isset($departments)) {
|
||||
$departments = $pdo->query("
|
||||
SELECT id, name, code, color
|
||||
FROM departments
|
||||
WHERE is_active = 1
|
||||
ORDER BY sort_order ASC, name ASC
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
if (!isset($subjects)) {
|
||||
$subjects = $pdo->query("SELECT id, name, color FROM scad_subjects ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
if (!isset($functions)) {
|
||||
$functions = $pdo->query("
|
||||
SELECT id, name
|
||||
FROM scad_functions
|
||||
WHERE status = 'active'
|
||||
ORDER BY name ASC
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Shared "Nuova/Modifica Scadenza" modal markup (used by index.php and detail.php).
|
||||
* Requires $subjects, $departments, $employees in scope (see deadline_form_data.php).
|
||||
* The accompanying JS lives in deadline_modal_js.php.
|
||||
*/
|
||||
?>
|
||||
<!-- Deadline Modal -->
|
||||
<div class="modal fade" id="deadlineModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-fullscreen-sm-down">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modalTitle">Nuova Scadenza</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
|
||||
</div>
|
||||
<form id="deadlineForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="dlId" name="id" value="">
|
||||
|
||||
<!-- Group 1: Informazioni principali -->
|
||||
<div class="form-section-title">Informazioni principali</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="dlSubject" class="form-label fw-semibold">Argomento</label>
|
||||
<div class="d-flex gap-2">
|
||||
<select class="form-select" id="dlSubject" name="subject_id" style="flex:1">
|
||||
<option value="">— Nessuno —</option>
|
||||
<?php foreach ($subjects as $s): ?>
|
||||
<option value="<?= (int)$s['id'] ?>" data-color="<?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<a href="scadenzario/subjects/index.php" target="_blank" class="btn btn-scad-outline" title="Gestisci argomenti">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="dlFunction" class="form-label fw-semibold">Funzione</label>
|
||||
<div class="d-flex gap-2">
|
||||
<select class="form-select" id="dlFunction" name="function_id" style="flex:1">
|
||||
<option value="">— Nessuna —</option>
|
||||
<?php foreach ($functions as $fn): ?>
|
||||
<option value="<?= (int)$fn['id'] ?>">
|
||||
<?= htmlspecialchars($fn['name'], ENT_QUOTES, 'UTF-8') ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<a href="scadenzario/functions/index.php" target="_blank" class="btn btn-scad-outline" title="Gestisci funzioni">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="dlLaw" class="form-label fw-semibold">Legge / Articolo</label>
|
||||
<input type="text" class="form-control" id="dlLaw" name="law_regulation" maxlength="500" placeholder="es. D.Lgs. 81/2008, D.M. 10.03.1998...">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="dlTopic" class="form-label fw-semibold">Dettaglio <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="dlTopic" name="topic" required maxlength="500" rows="2" placeholder="es. Verifica estintori, Autorizzazione trasporto rifiuti..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 2: Date e frequenza -->
|
||||
<div class="form-section-title">Date e frequenza</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="dlRecurrence" class="form-label fw-semibold">Periodicità</label>
|
||||
<select class="form-select" id="dlRecurrence" name="recurrence_type">
|
||||
<option value="once">Una tantum</option>
|
||||
<option value="monthly">Mensile</option>
|
||||
<option value="quarterly">Trimestrale</option>
|
||||
<option value="semiannual">Semestrale</option>
|
||||
<option value="annual">Annuale</option>
|
||||
<option value="biennial">Biennale</option>
|
||||
<option value="triennial">Triennale</option>
|
||||
<option value="quadriennial">Quadriennale</option>
|
||||
<option value="quinquennial">Quinquennale</option>
|
||||
<option value="decennial">Decennale</option>
|
||||
<option value="quindecennial">Quindicennale</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="dlDocDate" class="form-label fw-semibold">Data documento</label>
|
||||
<input type="text" class="form-control js-date-it" id="dlDocDate" name="document_date" placeholder="gg/mm/aaaa" autocomplete="off">
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="dlDueDate" class="form-label fw-semibold">Data scadenza <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control js-date-it" id="dlDueDate" name="due_date" placeholder="gg/mm/aaaa" autocomplete="off" required>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="dlCheckDate" class="form-label fw-semibold">Data ultimo controllo</label>
|
||||
<input type="text" class="form-control js-date-it" id="dlCheckDate" name="check_date" placeholder="gg/mm/aaaa" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 3: Responsabili -->
|
||||
<div class="form-section-title">Responsabili</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12">
|
||||
<label for="dlDepartments" class="form-label fw-semibold">Reparti</label>
|
||||
<select class="form-select" id="dlDepartments" name="department_names[]" multiple>
|
||||
<?php foreach ($departments as $dept): ?>
|
||||
<option value="<?= htmlspecialchars($dept['name'], ENT_QUOTES, 'UTF-8') ?>">
|
||||
<?= htmlspecialchars($dept['name'], ENT_QUOTES, 'UTF-8') ?>
|
||||
<?= !empty($dept['code']) ? ' (' . htmlspecialchars($dept['code'], ENT_QUOTES, 'UTF-8') . ')' : '' ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-text">Tutto il reparto sarà responsabile</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="dlEmployees" class="form-label fw-semibold">Singoli responsabili</label>
|
||||
<select class="form-select" id="dlEmployees" name="employee_ids[]" multiple>
|
||||
<?php foreach ($employees as $emp): ?>
|
||||
<option value="<?= (int)$emp['id'] ?>">
|
||||
<?= htmlspecialchars($emp['first_name'] . ' ' . $emp['last_name'], ENT_QUOTES, 'UTF-8') ?><?php if (!empty($emp['department_name'])): ?> (<?= htmlspecialchars($emp['department_name'], ENT_QUOTES, 'UTF-8') ?>)<?php endif; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 4: Dettagli aggiuntivi -->
|
||||
<div class="form-section-title">Dettagli aggiuntivi</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="dlNotifDays" class="form-label fw-semibold">Giorni preavviso</label>
|
||||
<input type="number" class="form-control" id="dlNotifDays" name="notification_days" value="7" min="1" max="365">
|
||||
</div>
|
||||
<div class="col-12 col-md-8">
|
||||
<label for="dlStorage" class="form-label fw-semibold">Luogo archiviazione</label>
|
||||
<input type="text" class="form-control" id="dlStorage" name="storage_location" maxlength="500" placeholder="es. Armadio A3, Server/Documenti/Sicurezza...">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="dlNotes" class="form-label fw-semibold">Note</label>
|
||||
<textarea class="form-control" id="dlNotes" name="notes" rows="3" placeholder="es. Scadenza 09/06/2026, Attività in appalto a Ditta specializzata..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 5: Allegati -->
|
||||
<div class="form-section-title mt-4">Allegati</div>
|
||||
<div id="attachmentsList" class="mb-3"></div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label for="dlFiles" class="form-label fw-semibold">Carica file</label>
|
||||
<input type="file" class="form-control" id="dlFiles" multiple>
|
||||
<div class="form-text">Puoi selezionare più file contemporaneamente</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-scad-primary">
|
||||
<i class="fa-solid fa-check me-1"></i> Salva
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Shared styles for the deadline modal (deadline_modal.php).
|
||||
* Relies on the --scad-* CSS variables defined on each page's :root.
|
||||
*/
|
||||
?>
|
||||
<style>
|
||||
.form-section-title {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: var(--scad-heading);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.4rem;
|
||||
border-bottom: 2px solid #e8eeff;
|
||||
}
|
||||
|
||||
#deadlineModal.modal {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
#deadlineModal .modal-content,
|
||||
#deadlineModal .modal-body,
|
||||
#deadlineModal .modal-footer {
|
||||
background: #fff !important;
|
||||
}
|
||||
|
||||
#deadlineModal .modal-header {
|
||||
background: var(--scad-card-bg);
|
||||
border-bottom: 1px solid var(--scad-card-border);
|
||||
}
|
||||
|
||||
#deadlineModal .modal-title {
|
||||
font-weight: 700;
|
||||
color: var(--scad-heading);
|
||||
}
|
||||
|
||||
/* Attachment list in modal */
|
||||
.att-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 0.4rem;
|
||||
margin-bottom: 0.4rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.att-item .att-name {
|
||||
color: var(--scad-heading);
|
||||
font-weight: 500;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.att-item .att-actions {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
flex-shrink: 0;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.att-item .att-actions a,
|
||||
.att-item .att-actions button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.3rem;
|
||||
border: none;
|
||||
font-size: 0.75rem;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.att-item .att-download {
|
||||
background: #eef3ff;
|
||||
color: var(--scad-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.att-item .att-download:hover {
|
||||
background: var(--scad-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.att-item .att-remove {
|
||||
background: #fff0f0;
|
||||
color: var(--scad-red, #dc3545);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.att-item .att-remove:hover {
|
||||
background: var(--scad-red, #dc3545);
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Self-contained JS for the deadline modal (deadline_modal.php).
|
||||
* Requires jQuery, Bootstrap, flatpickr, select2 and SweetAlert2 to be loaded first.
|
||||
*
|
||||
* Exposes:
|
||||
* window.openDeadlineCreate() — open the modal empty (new deadline)
|
||||
* window.openDeadlineEdit(id) — fetch a deadline and open the modal in edit mode
|
||||
*
|
||||
* Auto-open on load:
|
||||
* #edit=<id> → opens edit for that id
|
||||
* #edit → opens edit for window.SCAD_DETAIL_ID (used by detail.php)
|
||||
*/
|
||||
?>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
|
||||
// --- Flatpickr date fields (visible dd/mm/yyyy, submitted yyyy-mm-dd) ---
|
||||
var fpOptsDate = {
|
||||
dateFormat: 'Y-m-d',
|
||||
altInput: true,
|
||||
altFormat: 'd/m/Y',
|
||||
locale: 'it',
|
||||
allowInput: true
|
||||
};
|
||||
var fpDocDate = flatpickr('#dlDocDate', fpOptsDate);
|
||||
var fpDueDate = flatpickr('#dlDueDate', fpOptsDate);
|
||||
var fpCheckDate = flatpickr('#dlCheckDate', fpOptsDate);
|
||||
|
||||
// --- Select2 ---
|
||||
var s2Opts = {
|
||||
theme: 'bootstrap-5',
|
||||
allowClear: true,
|
||||
dropdownParent: $('#deadlineModal .modal-body'),
|
||||
language: 'it',
|
||||
width: '100%'
|
||||
};
|
||||
$('#dlSubject').select2($.extend({}, s2Opts, { placeholder: 'Seleziona argomento...' }));
|
||||
$('#dlDepartments').select2($.extend({}, s2Opts, { placeholder: 'Seleziona reparti...' }));
|
||||
$('#dlEmployees').select2($.extend({}, s2Opts, { placeholder: 'Seleziona persone...' }));
|
||||
$('#dlFunction').select2($.extend({}, s2Opts, { placeholder: 'Seleziona funzione...' }));
|
||||
|
||||
// --- Auto-calc due_date from document_date + recurrence ---
|
||||
var RECURRENCE_OFFSETS = {
|
||||
monthly: { months: 1 },
|
||||
quarterly: { months: 3 },
|
||||
semiannual: { months: 6 },
|
||||
annual: { years: 1 },
|
||||
biennial: { years: 2 },
|
||||
triennial: { years: 3 },
|
||||
quadriennial: { years: 4 },
|
||||
quinquennial: { years: 5 },
|
||||
decennial: { years: 10 },
|
||||
quindecennial: { years: 15 }
|
||||
};
|
||||
|
||||
function computeDueDate() {
|
||||
var docVal = document.getElementById('dlDocDate').value;
|
||||
var recurrence = document.getElementById('dlRecurrence').value;
|
||||
var offset = RECURRENCE_OFFSETS[recurrence];
|
||||
if (!docVal || !offset) return;
|
||||
var d = new Date(docVal + 'T00:00:00');
|
||||
if (isNaN(d.getTime())) return;
|
||||
if (offset.months) d.setMonth(d.getMonth() + offset.months);
|
||||
if (offset.years) d.setFullYear(d.getFullYear() + offset.years);
|
||||
var iso = d.getFullYear() + '-' +
|
||||
String(d.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(d.getDate()).padStart(2, '0');
|
||||
fpDueDate.setDate(iso, true, 'Y-m-d');
|
||||
}
|
||||
$('#dlDocDate, #dlRecurrence').on('change', computeDueDate);
|
||||
|
||||
// --- Modal instance ---
|
||||
var modal = new bootstrap.Modal(document.getElementById('deadlineModal'));
|
||||
var form = document.getElementById('deadlineForm');
|
||||
|
||||
// --- Render attachments list ---
|
||||
function renderAttachments(attachments) {
|
||||
var container = document.getElementById('attachmentsList');
|
||||
if (!attachments || attachments.length === 0) {
|
||||
container.innerHTML = '<div class="text-muted" style="font-size:0.85rem">Nessun allegato</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = attachments.map(function(a) {
|
||||
return '<div class="att-item" data-att-id="' + a.id + '">' +
|
||||
'<span class="att-name"><i class="fa-solid fa-paperclip me-1"></i>' + $('<span>').text(a.original_name).html() + '</span>' +
|
||||
'<span class="att-actions">' +
|
||||
'<a href="scadenzario/ajax/download_attachment.php?id=' + a.id + '" class="att-download" title="Scarica"><i class="fa-solid fa-download"></i></a>' +
|
||||
'<button type="button" class="att-remove" title="Elimina" data-att-id="' + a.id + '"><i class="fa-solid fa-xmark"></i></button>' +
|
||||
'</span></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// --- Open modal (create) ---
|
||||
window.openDeadlineCreate = function() {
|
||||
form.reset();
|
||||
document.getElementById('dlId').value = '';
|
||||
document.getElementById('dlNotifDays').value = '7';
|
||||
document.getElementById('modalTitle').textContent = 'Nuova Scadenza';
|
||||
document.getElementById('dlFiles').value = '';
|
||||
fpDocDate.clear();
|
||||
fpDueDate.clear();
|
||||
fpCheckDate.clear();
|
||||
$('#dlSubject').val('').trigger('change');
|
||||
$('#dlDepartments').val(null).trigger('change');
|
||||
$('#dlEmployees').val(null).trigger('change');
|
||||
$('#dlFunction').val('').trigger('change');
|
||||
renderAttachments([]);
|
||||
modal.show();
|
||||
};
|
||||
|
||||
// --- Open modal (edit) ---
|
||||
window.openDeadlineEdit = function(id) {
|
||||
fetch('scadenzario/ajax/get_deadline.php?id=' + id)
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (!data.success) {
|
||||
Swal.fire('Errore', data.message, 'error');
|
||||
return;
|
||||
}
|
||||
var d = data.data;
|
||||
document.getElementById('dlId').value = d.id;
|
||||
$('#dlSubject').val(d.subject_id || '').trigger('change');
|
||||
document.getElementById('dlTopic').value = d.topic || '';
|
||||
$('#dlFunction').val(d.function_id || '').trigger('change');
|
||||
document.getElementById('dlLaw').value = d.law_regulation || '';
|
||||
document.getElementById('dlRecurrence').value = d.recurrence_type || 'once';
|
||||
fpDocDate.setDate(d.document_date || null, false, 'Y-m-d');
|
||||
fpDueDate.setDate(d.due_date || null, false, 'Y-m-d');
|
||||
fpCheckDate.setDate(d.check_date || null, false, 'Y-m-d');
|
||||
document.getElementById('dlNotifDays').value = d.notification_days || 7;
|
||||
document.getElementById('dlStorage').value = d.storage_location || '';
|
||||
document.getElementById('dlNotes').value = d.notes || '';
|
||||
document.getElementById('dlFiles').value = '';
|
||||
document.getElementById('modalTitle').textContent = 'Modifica Scadenza';
|
||||
$('#dlDepartments').val(d.department_names || []).trigger('change');
|
||||
if (Array.isArray(d.employee_ids)) {
|
||||
$('#dlEmployees').val(d.employee_ids.map(String)).trigger('change');
|
||||
} else {
|
||||
$('#dlEmployees').val(null).trigger('change');
|
||||
}
|
||||
renderAttachments(d.attachments || []);
|
||||
modal.show();
|
||||
})
|
||||
.catch(function() {
|
||||
Swal.fire('Errore', 'Errore di connessione.', 'error');
|
||||
});
|
||||
};
|
||||
|
||||
// --- Save ---
|
||||
var isSaving = false;
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
if (isSaving) return;
|
||||
isSaving = true;
|
||||
var saveBtn = form.querySelector('[type="submit"]');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin me-1"></i> Salvataggio...';
|
||||
var formData = new FormData(form);
|
||||
|
||||
fetch('scadenzario/ajax/save_deadline.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
var deadlineId = data.id;
|
||||
var fileInput = document.getElementById('dlFiles');
|
||||
if (fileInput.files.length > 0) {
|
||||
var fileData = new FormData();
|
||||
fileData.append('deadline_id', deadlineId);
|
||||
for (var i = 0; i < fileInput.files.length; i++) {
|
||||
fileData.append('files[]', fileInput.files[i]);
|
||||
}
|
||||
return fetch('scadenzario/ajax/upload_attachment.php', {
|
||||
method: 'POST',
|
||||
body: fileData
|
||||
})
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(upData) {
|
||||
modal.hide();
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Salvato',
|
||||
text: data.message + ' ' + upData.message,
|
||||
timer: 2000,
|
||||
showConfirmButton: false
|
||||
})
|
||||
.then(function() {
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
modal.hide();
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Salvato',
|
||||
text: data.message,
|
||||
timer: 1500,
|
||||
showConfirmButton: false
|
||||
})
|
||||
.then(function() {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Swal.fire('Errore', data.message, 'error');
|
||||
isSaving = false;
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = '<i class="fa-solid fa-check me-1"></i> Salva';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
Swal.fire('Errore', 'Errore di connessione.', 'error');
|
||||
isSaving = false;
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = '<i class="fa-solid fa-check me-1"></i> Salva';
|
||||
});
|
||||
});
|
||||
|
||||
// --- Delete attachment ---
|
||||
$(document).on('click', '.att-remove', function(e) {
|
||||
e.preventDefault();
|
||||
var btn = $(this);
|
||||
var attId = btn.data('att-id');
|
||||
Swal.fire({
|
||||
title: 'Rimuovere l\'allegato?',
|
||||
text: 'Il collegamento verrà rimosso da questa scadenza. Il file resta disponibile se è usato da altre scadenze.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#dc3545',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonText: 'Rimuovi',
|
||||
reverseButtons: true
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
fetch('scadenzario/ajax/delete_attachment.php?id=' + attId)
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
btn.closest('.att-item').remove();
|
||||
if ($('#attachmentsList .att-item').length === 0) {
|
||||
renderAttachments([]);
|
||||
}
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Fatto',
|
||||
text: data.message,
|
||||
timer: 1800,
|
||||
showConfirmButton: false
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Errore', data.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// --- Auto-open from hash (#edit=ID or #edit for the current detail page) ---
|
||||
var hash = window.location.hash;
|
||||
var hashMatch = hash.match(/^#edit=(\d+)$/);
|
||||
var autoEditId = hashMatch ? hashMatch[1] :
|
||||
(hash === '#edit' && window.SCAD_DETAIL_ID ? window.SCAD_DETAIL_ID : null);
|
||||
if (autoEditId) {
|
||||
history.replaceState(null, '', window.location.pathname + window.location.search);
|
||||
window.openDeadlineEdit(autoEditId);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Renders two status banners for the current user:
|
||||
* - red -> overdue deadlines (scaduta)
|
||||
* - orange -> approaching deadlines (in scadenza)
|
||||
* Scope: deadlines assigned directly to the user OR to their department.
|
||||
*/
|
||||
|
||||
$_empStmt = $pdo->prepare("SELECT id, department FROM employees WHERE auth_user_id = ? LIMIT 1");
|
||||
$_empStmt->execute([(int)$iduserlogin]);
|
||||
$_emp = $_empStmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||
|
||||
$_overdue = 0;
|
||||
$_approaching = 0;
|
||||
|
||||
if ($_emp) {
|
||||
$_empId = (int)$_emp['id'];
|
||||
$_deptRaw = (string)($_emp['department'] ?? '');
|
||||
$_dept = trim($_deptRaw);
|
||||
|
||||
$_sql = "
|
||||
SELECT
|
||||
SUM(CASE WHEN d.due_date < CURDATE() THEN 1 ELSE 0 END) AS overdue_cnt,
|
||||
SUM(CASE WHEN d.due_date >= CURDATE()
|
||||
AND d.due_date <= DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)
|
||||
THEN 1 ELSE 0 END) AS approaching_cnt
|
||||
FROM scad_deadlines d
|
||||
WHERE d.status <> 'completed'
|
||||
AND (
|
||||
d.id IN (SELECT deadline_id FROM scad_deadline_employee WHERE employee_id = ?)
|
||||
OR (? <> '' AND FIND_IN_SET(?, REPLACE(d.departments, ', ', ',')) > 0)
|
||||
)
|
||||
";
|
||||
$_st = $pdo->prepare($_sql);
|
||||
$_st->execute([$_empId, $_dept, $_dept]);
|
||||
$_row = $_st->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
$_overdue = (int)($_row['overdue_cnt'] ?? 0);
|
||||
$_approaching = (int)($_row['approaching_cnt'] ?? 0);
|
||||
}
|
||||
|
||||
if (!$_emp || ($_overdue === 0 && $_approaching === 0)) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<style>
|
||||
.my-deadlines-widgets {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.my-deadlines-widgets .mdw {
|
||||
flex: 1 1 260px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.9rem;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 0.6rem;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||
transition: transform 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.my-deadlines-widgets .mdw:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.my-deadlines-widgets .mdw-red {
|
||||
background: linear-gradient(135deg, #dc3545 0%, #b02a37 100%);
|
||||
}
|
||||
|
||||
.my-deadlines-widgets .mdw-orange {
|
||||
background: linear-gradient(135deg, #e8930c 0%, #c77a00 100%);
|
||||
}
|
||||
|
||||
.my-deadlines-widgets .mdw-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
font-size: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.my-deadlines-widgets .mdw-body {
|
||||
flex: 1;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.my-deadlines-widgets .mdw-count {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.my-deadlines-widgets .mdw-label {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.my-deadlines-widgets .mdw-arrow {
|
||||
opacity: 0.7;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
<div class="my-deadlines-widgets">
|
||||
<?php if ($_overdue > 0): ?>
|
||||
<a class="mdw mdw-red" href="scadenzario/index.php?filter_my=1&filter_status=scaduta">
|
||||
<span class="mdw-icon"><i class="fa-solid fa-triangle-exclamation"></i></span>
|
||||
<span class="mdw-body">
|
||||
<span class="mdw-count"><?= $_overdue ?></span>
|
||||
<span class="mdw-label d-block">Task<?= $_overdue === 1 ? '' : 's' ?> scadut<?= $_overdue === 1 ? 'o' : 'i' ?> — <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span>
|
||||
</span>
|
||||
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($_approaching > 0): ?>
|
||||
<a class="mdw mdw-orange" href="scadenzario/index.php?filter_my=1&filter_status=in-scadenza">
|
||||
<span class="mdw-icon"><i class="fa-solid fa-clock"></i></span>
|
||||
<span class="mdw-body">
|
||||
<span class="mdw-count"><?= $_approaching ?></span>
|
||||
<span class="mdw-label d-block">In scadenza a breve — <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span>
|
||||
</span>
|
||||
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,202 @@
|
||||
<?php include('../include/headscript.php'); ?>
|
||||
<?php
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$sql = "
|
||||
SELECT d.*,
|
||||
s.name AS subject_name,
|
||||
s.color AS subject_color,
|
||||
GROUP_CONCAT(DISTINCT CONCAT(e.first_name, ' ', e.last_name) ORDER BY e.first_name SEPARATOR ', ') as responsabili,
|
||||
GROUP_CONCAT(DISTINCT e.department ORDER BY e.department SEPARATOR ', ') as reparti_persone,
|
||||
d.departments as reparti_assegnati
|
||||
FROM scad_deadlines d
|
||||
LEFT JOIN scad_subjects s ON s.id = d.subject_id
|
||||
LEFT JOIN scad_deadline_employee de ON de.deadline_id = d.id
|
||||
LEFT JOIN employees e ON e.id = de.employee_id
|
||||
";
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
$filterStatus = $_GET['status'] ?? '';
|
||||
$filterDept = $_GET['department'] ?? '';
|
||||
$filterEmployee = $_GET['employee'] ?? '';
|
||||
|
||||
if ($filterStatus === 'non-completata') {
|
||||
$where[] = "d.status != 'completed'";
|
||||
} elseif ($filterStatus === 'completata') {
|
||||
$where[] = "d.status = 'completed'";
|
||||
} elseif ($filterStatus === 'scaduta') {
|
||||
$where[] = "d.status = 'active' AND d.due_date < CURDATE()";
|
||||
} elseif ($filterStatus === 'in-scadenza') {
|
||||
$where[] = "d.status = 'active' AND d.due_date >= CURDATE() AND d.due_date <= DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)";
|
||||
} elseif ($filterStatus === 'attiva') {
|
||||
$where[] = "d.status = 'active' AND d.due_date > DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)";
|
||||
}
|
||||
|
||||
if ($filterEmployee) {
|
||||
$where[] = "EXISTS (SELECT 1 FROM scad_deadline_employee de2 JOIN employees e2 ON e2.id = de2.employee_id WHERE de2.deadline_id = d.id AND CONCAT(e2.first_name, ' ', e2.last_name) = ?)";
|
||||
$params[] = $filterEmployee;
|
||||
}
|
||||
|
||||
$dueFrom = $_GET['due_from'] ?? '';
|
||||
$dueTo = $_GET['due_to'] ?? '';
|
||||
$checkFrom = $_GET['check_from'] ?? '';
|
||||
$checkTo = $_GET['check_to'] ?? '';
|
||||
|
||||
if ($dueFrom) { $where[] = "d.due_date >= ?"; $params[] = $dueFrom; }
|
||||
if ($dueTo) { $where[] = "d.due_date <= ?"; $params[] = $dueTo; }
|
||||
if ($checkFrom) { $where[] = "d.check_date >= ?"; $params[] = $checkFrom; }
|
||||
if ($checkTo) { $where[] = "d.check_date <= ?"; $params[] = $checkTo; }
|
||||
|
||||
if (!empty($where)) {
|
||||
$sql .= " WHERE " . implode(' AND ', $where);
|
||||
}
|
||||
|
||||
$sql .= " GROUP BY d.id ORDER BY (d.status = 'completed') ASC, d.due_date ASC";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$recurrenceLabels = ['once'=>'Una tantum','monthly'=>'Mensile','quarterly'=>'Trimestrale','semiannual'=>'Semestrale','annual'=>'Annuale','biennial'=>'Biennale','triennial'=>'Triennale','quadriennial'=>'Quadriennale','quinquennial'=>'Quinquennale','decennial'=>'Decennale','quindecennial'=>'Quindicennale'];
|
||||
|
||||
$filterLabel = '';
|
||||
if ($filterStatus) {
|
||||
$statusLabels = ['non-completata'=>'Non completate','attiva'=>'Attive','in-scadenza'=>'In scadenza','scaduta'=>'Scadute','completata'=>'Completate'];
|
||||
$filterLabel = $statusLabels[$filterStatus] ?? '';
|
||||
}
|
||||
if ($filterDept) {
|
||||
$filterLabel .= ($filterLabel ? ' — ' : '') . 'Reparto: ' . $filterDept;
|
||||
}
|
||||
if ($filterEmployee) {
|
||||
$filterLabel .= ($filterLabel ? ' — ' : '') . 'Responsabile: ' . $filterEmployee;
|
||||
}
|
||||
if ($dueFrom || $dueTo) {
|
||||
$filterLabel .= ($filterLabel ? ' — ' : '') . 'Scadenza: ' . ($dueFrom ? date('d/m/Y', strtotime($dueFrom)) : '...') . ' → ' . ($dueTo ? date('d/m/Y', strtotime($dueTo)) : '...');
|
||||
}
|
||||
if ($checkFrom || $checkTo) {
|
||||
$filterLabel .= ($filterLabel ? ' — ' : '') . 'Controllo: ' . ($checkFrom ? date('d/m/Y', strtotime($checkFrom)) : '...') . ' → ' . ($checkTo ? date('d/m/Y', strtotime($checkTo)) : '...');
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Stampa Scadenzario</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 11px; color: #222; background: #fff; }
|
||||
.print-header { padding: 20px 20px 10px; border-bottom: 2px solid #2c3e6b; margin-bottom: 10px; }
|
||||
.print-header h1 { font-size: 18px; color: #2c3e6b; margin: 0; }
|
||||
.print-header .print-meta { font-size: 10px; color: #666; margin-top: 4px; }
|
||||
.print-actions { padding: 10px 20px; display: flex; gap: 10px; }
|
||||
.print-actions button { padding: 8px 20px; font-size: 13px; font-weight: 600; border-radius: 6px; cursor: pointer; border: none; }
|
||||
.btn-print { background: #5a8fd8; color: #fff; }
|
||||
.btn-back { background: #f0f0f0; color: #333; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 0 20px; }
|
||||
table { width: calc(100% - 40px); }
|
||||
th { background: #2c3e6b; color: #fff; font-weight: 600; font-size: 10px; text-transform: uppercase; letter-spacing: 0.03em; padding: 6px 8px; text-align: left; white-space: nowrap; }
|
||||
td { padding: 5px 8px; border-bottom: 1px solid #e0e0e0; vertical-align: top; font-size: 10.5px; line-height: 1.4; }
|
||||
tr:nth-child(even) { background: #f9fafb; }
|
||||
tr.row-overdue { background: #fff5f5; }
|
||||
tr.row-overdue td { color: #991b1b; }
|
||||
tr.row-approaching { background: #fffbeb; }
|
||||
tr.row-completed { opacity: 0.5; }
|
||||
.status-badge { font-size: 9px; font-weight: 700; padding: 2px 6px; border-radius: 10px; white-space: nowrap; }
|
||||
.st-attiva { background: #e8eeff; color: #3a6bb5; }
|
||||
.st-scaduta { background: #fde8e8; color: #b91c1c; }
|
||||
.st-in-scadenza { background: #fef3cd; color: #92600a; }
|
||||
.st-completata { background: #d1f2e0; color: #0f5132; }
|
||||
.print-footer { padding: 10px 20px; font-size: 9px; color: #999; border-top: 1px solid #e0e0e0; margin-top: 10px; text-align: right; }
|
||||
@media print {
|
||||
.print-actions { display: none; }
|
||||
body { font-size: 9px; }
|
||||
th { font-size: 8.5px; padding: 4px 6px; }
|
||||
td { font-size: 9px; padding: 3px 6px; }
|
||||
@page { size: landscape; margin: 0.8cm; }
|
||||
}
|
||||
@media screen {
|
||||
table { margin: 0 auto; width: calc(100% - 40px); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="print-header">
|
||||
<h1>ELENCO PRESCRIZIONI LEGALI ED ALTRE PRESCRIZIONI APPLICABILI<br>SICUREZZA ED AMBIENTE CON VERIFICA DEL RISPETTO DELLE STESSE</h1>
|
||||
<div class="print-meta">
|
||||
Stampato il <?= date('d/m/Y H:i') ?>
|
||||
<?php if ($filterLabel): ?> — Filtro: <?= htmlspecialchars($filterLabel, ENT_QUOTES, 'UTF-8') ?><?php endif; ?>
|
||||
— Totale: <?= count($deadlines) ?> scadenze
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="print-actions">
|
||||
<button class="btn-print" onclick="window.print()"><i class="fa-solid fa-print"></i> Stampa</button>
|
||||
<button class="btn-back" onclick="window.close()">Chiudi</button>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Argomento</th>
|
||||
<th>Legge / Art.</th>
|
||||
<th>Dettaglio</th>
|
||||
<th>Periodicità Scadenza</th>
|
||||
<th>Data Documento</th>
|
||||
<th>Data Scadenza</th>
|
||||
<th>Data Ultimo Controllo</th>
|
||||
<th>Responsabilità</th>
|
||||
<th>Luogo di Archiviazione</th>
|
||||
<th>Note</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($deadlines as $row):
|
||||
$dueDate = $row['due_date'];
|
||||
$nDays = (int)$row['notification_days'];
|
||||
$approachDate = date('Y-m-d', strtotime($today . ' + ' . $nDays . ' days'));
|
||||
$isCompleted = $row['status'] === 'completed';
|
||||
$isOverdue = !$isCompleted && $dueDate < $today;
|
||||
$isApproaching = !$isCompleted && !$isOverdue && $dueDate <= $approachDate;
|
||||
|
||||
if ($isCompleted) { $statusLabel = 'Completata'; $stClass = 'st-completata'; $rowClass = 'row-completed'; }
|
||||
elseif ($isOverdue) { $statusLabel = 'Scaduta'; $stClass = 'st-scaduta'; $rowClass = 'row-overdue'; }
|
||||
elseif ($isApproaching) { $statusLabel = 'In scadenza'; $stClass = 'st-in-scadenza'; $rowClass = 'row-approaching'; }
|
||||
else { $statusLabel = 'Attiva'; $stClass = 'st-attiva'; $rowClass = ''; }
|
||||
|
||||
// Merge departments
|
||||
$allDepts = [];
|
||||
if (!empty($row['reparti_assegnati'])) $allDepts = array_map('trim', explode(',', $row['reparti_assegnati']));
|
||||
if (!empty($row['reparti_persone'])) $allDepts = array_merge($allDepts, array_map('trim', explode(',', $row['reparti_persone'])));
|
||||
$reparti = implode(', ', array_unique(array_filter($allDepts)));
|
||||
?>
|
||||
<tr class="<?= $rowClass ?>"<?= !empty($row['subject_color']) ? ' style="border-left: 4px solid ' . htmlspecialchars($row['subject_color'], ENT_QUOTES, 'UTF-8') . '"' : '' ?>>
|
||||
<td><strong><?= htmlspecialchars($row['subject_name'] ?? '', ENT_QUOTES, 'UTF-8') ?></strong></td>
|
||||
<td><?= htmlspecialchars($row['law_regulation'] ?? '', ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td><?= htmlspecialchars($row['topic'], ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td><?= htmlspecialchars($recurrenceLabels[$row['recurrence_type']] ?? $row['recurrence_type'], ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td style="white-space:nowrap"><?= $row['document_date'] ? date('d/m/Y', strtotime($row['document_date'])) : '' ?></td>
|
||||
<td style="white-space:nowrap"><?= date('d/m/Y', strtotime($dueDate)) ?></td>
|
||||
<td style="white-space:nowrap"><?= $row['check_date'] ? date('d/m/Y', strtotime($row['check_date'])) : '' ?></td>
|
||||
<td><?php
|
||||
$resp = [];
|
||||
if ($reparti) $resp[] = $reparti;
|
||||
if ($row['responsabili']) $resp[] = $row['responsabili'];
|
||||
echo htmlspecialchars(implode(', ', $resp), ENT_QUOTES, 'UTF-8');
|
||||
?></td>
|
||||
<td><?= htmlspecialchars($row['storage_location'] ?? '', ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td><?= htmlspecialchars($row['notes'] ?? '', ENT_QUOTES, 'UTF-8') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="print-footer">
|
||||
ZIBOGOMMA — Scadenzario — <?= date('d/m/Y') ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,70 @@
|
||||
-- Scadenzario tables
|
||||
-- Responsible persons = employees (existing table)
|
||||
-- Departments = employees.department (varchar field)
|
||||
-- Notification email = employees.auth_user_id -> auth_users.email
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scad_deadlines (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
category VARCHAR(100) NULL,
|
||||
topic VARCHAR(500) NOT NULL,
|
||||
law_regulation VARCHAR(500) NULL,
|
||||
details TEXT NULL,
|
||||
recurrence_type VARCHAR(20) NOT NULL DEFAULT 'once',
|
||||
due_date DATE NOT NULL,
|
||||
check_date DATE NULL,
|
||||
document_date DATE NULL,
|
||||
notification_days SMALLINT UNSIGNED NOT NULL DEFAULT 7,
|
||||
storage_location VARCHAR(500) NULL,
|
||||
notes TEXT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
completed_at TIMESTAMP NULL,
|
||||
completed_by INT UNSIGNED NULL,
|
||||
created_by INT UNSIGNED NOT NULL,
|
||||
departments VARCHAR(500) NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_due_date (due_date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scad_deadline_employee (
|
||||
deadline_id INT UNSIGNED NOT NULL,
|
||||
employee_id INT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (deadline_id, employee_id),
|
||||
CONSTRAINT fk_de_deadline FOREIGN KEY (deadline_id) REFERENCES scad_deadlines(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_de_employee FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scad_deadline_attachments (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
deadline_id INT UNSIGNED NOT NULL,
|
||||
original_name VARCHAR(500) NOT NULL,
|
||||
stored_name VARCHAR(500) NOT NULL,
|
||||
mime_type VARCHAR(100) NULL,
|
||||
size INT UNSIGNED NULL,
|
||||
uploaded_by INT UNSIGNED NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_att_deadline FOREIGN KEY (deadline_id) REFERENCES scad_deadlines(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scad_deadline_histories (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
deadline_id INT UNSIGNED NOT NULL,
|
||||
user_id INT UNSIGNED NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
changes JSON NULL,
|
||||
notes TEXT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_hist_deadline (deadline_id),
|
||||
CONSTRAINT fk_hist_deadline FOREIGN KEY (deadline_id) REFERENCES scad_deadlines(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scad_deadline_notifications (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
deadline_id INT UNSIGNED NOT NULL,
|
||||
employee_id INT UNSIGNED NOT NULL,
|
||||
sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
type VARCHAR(30) NOT NULL,
|
||||
INDEX idx_notif_deadline (deadline_id),
|
||||
CONSTRAINT fk_notif_deadline FOREIGN KEY (deadline_id) REFERENCES scad_deadlines(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,25 @@
|
||||
-- One-time migration: move scad_deadlines.category into dedicated scad_subjects table
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scad_subjects (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
color VARCHAR(7) NOT NULL DEFAULT '#6c757d',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uniq_name (name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT IGNORE INTO scad_subjects (name)
|
||||
SELECT DISTINCT TRIM(category) FROM scad_deadlines
|
||||
WHERE category IS NOT NULL AND TRIM(category) <> '';
|
||||
|
||||
ALTER TABLE scad_deadlines
|
||||
ADD COLUMN subject_id INT UNSIGNED NULL AFTER id,
|
||||
ADD INDEX idx_subject (subject_id),
|
||||
ADD CONSTRAINT fk_deadlines_subject FOREIGN KEY (subject_id) REFERENCES scad_subjects(id) ON DELETE SET NULL;
|
||||
|
||||
UPDATE scad_deadlines d
|
||||
JOIN scad_subjects s ON s.name = TRIM(d.category)
|
||||
SET d.subject_id = s.id;
|
||||
|
||||
ALTER TABLE scad_deadlines DROP COLUMN category;
|
||||
@@ -0,0 +1,531 @@
|
||||
START TRANSACTION;
|
||||
|
||||
DELETE FROM scad_deadline_attachments;
|
||||
DELETE FROM scad_deadlines;
|
||||
DELETE FROM scad_subjects;
|
||||
|
||||
ALTER TABLE scad_deadlines AUTO_INCREMENT = 1;
|
||||
ALTER TABLE scad_subjects AUTO_INCREMENT = 1;
|
||||
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Ambiente', '#198754') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Manutenzioni', '#5a8fd8') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Sicurezza', '#e8930c') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Ambienti - Scarichi idrici', '#0dcaf0') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Prescrizioni generali', '#6f42c1') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Antincendio', '#dc3545') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Ambiente - Manutenzione', '#20c997') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Ambiente - Rifiuti', '#795548') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Medicina del Lavoro', '#d63384') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Prescrizioni generali Medicina del Lavoro', '#343a40') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Adempimenti', '#b88a44') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Attività in appalto', '#8b4513') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Formazione', '#17a2b8') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Appaltatori', '#212529') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Ambienti - Emissioni in atmosfera', '#0d6efd') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Ambiente - Emissioni in atmosfera', '#28a745') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Prevenzione - Sicurezza', '#ffc107') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
INSERT INTO scad_subjects (name, color) VALUES ('Pronto Soccorso', '#e83e8c') ON DUPLICATE KEY UPDATE color = VALUES(color);
|
||||
|
||||
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES (NULL, 'Autorizzazione raccolta e trasporto rifiuti speciali non pericolosi TRAMONTO ANTONIO', 'D.M. 10.03.1998, UNI 9994', 'quinquennial', '2025-12-22', '2021-01-02', '2020-12-22', 7, NULL, NULL, 1, 'Amministrazione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente'), 'Autorizzazione raccolta e trasporto rifiuti speciali pericolosi TRAMONTO ANTONIO', 'D.Lgs 81/08 art. 174, 176', 'quinquennial', '2026-01-09', '2021-01-19', '2021-01-09', 7, NULL, NULL, 1, 'Amministrazione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Manutenzioni'), 'Manutenzione Transpalett manuali', 'D.Lgs 81/08 all.V', 'quarterly', '2026-03-31', '2026-02-24', '2025-12-23', 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Sicurezza'), 'Manutenzione Carrelli Elevatori', 'D.Lgs 81/08 all.V', 'quarterly', '2026-03-31', '2026-02-24', '2025-12-23', 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambienti - Scarichi idrici'), 'Sostituzione valvola Recipienti Pressione', NULL, 'triennial', '2026-04-03', '2024-07-05', '2023-07-04', 7, 'Locale Archivio', 'Autoclave l 1.000 pozzi', 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Rinnovo CPI', 'D.M. 10.03.1998, D.M. 16.06.1982, DPR 151 del 2011', 'quinquennial', '2026-04-30', '2021-10-21', '2021-10-21', 7, NULL, 'Scadenza 09/06/2026', 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Verifica funzionamento impianto schiuma', 'D.Lgs. 81/2008 (art. 18, c 1, u) D.Lgs. 81/2008 (art. 26)', 'semiannual', '2026-04-30', '2026-02-24', '2026-02-19', 7, 'Locale Archivio', 'Attività in appalto a Ditta specializzata', 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Verifica funzionamento stazione di pompaggio antincendio', 'D.Lgs. 81/2008 (art. 18, c 1, u) D.Lgs. 81/2008 (art. 26)', 'quarterly', '2026-04-30', '2026-02-24', '2026-02-19', 7, 'Locale Archivio', 'Attività in appalto a Ditta specializzata', 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Verifica funzionamento valvola a diluvio impianto schiuma', 'D.Lgs. 81/2008 (art. 18, c 1, u) D.Lgs. 81/2008 (art. 26)', 'quarterly', '2026-04-30', '2026-02-24', '2026-02-19', 7, 'Locale Archivio', 'Attività in appalto a Ditta specializzata', 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Manutenzioni'), 'Verifica impianto rivelazione incendi', 'D.Lgs. 81/2008 (art. 18, c 1, u) D.Lgs. 81/2008 (art. 26)', 'semiannual', '2026-05-12', '2026-03-21', '2025-11-13', 7, 'Locale Archivio', NULL, 1, 'Delegato di funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambienti - Scarichi idrici'), 'Sanificazione filtri rubinetti e pigne docce con candeggina', 'D.Lgs 81/08 art. 209', 'quarterly', '2026-05-31', '2026-02-24', '2026-02-24', 7, NULL, NULL, 1, 'Ditta Incaricata');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES (NULL, 'Manutenzione e controllo fughe pompa di calore ELCO', 'D.M. 17/03/03', 'annual', '2026-05-31', '2026-02-24', NULL, 7, NULL, 'Presentazione a cura delle ditte incaricate', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente'), 'Gestione Rischi Impianto Trattamento Aria ((Unità trattamento aria - Filtri Aria - Umidificazione a vapore - Batterie di scambio termico - Ventilazione UTA - Raffreddatori di liquido - Apparecchi terminali) e registrazione su quaderno e libretti', 'D.Lgs 81/08', 'once', '2026-05-31', '2026-03-21', '2025-10-13', 7, 'Locale Archivio', NULL, 1, 'Delegato di funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Manutenzione'), 'Dichiarazione MUD', NULL, 'annual', '2026-06-30', '2026-02-24', '2025-06-19', 7, NULL, NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Comunicazione BANCA DATI GAS FLUORURATI refrigeratori produzione riguardante le emissioni in atmosfera di gas fluorurati', 'Accordo Stato regioni 07.02.2013', 'annual', '2026-06-30', '2026-02-24', NULL, 7, NULL, 'Presentazione a cura delle ditte incaricate', 1, 'DdL-RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Comunicazione BANCA DATI GAS FLUORURATI pompa di calore riguardante le emissioni in atmosfera di gas fluorurati', 'Accordo Stato regioni 07.02.2013', 'annual', '2026-06-30', '2026-02-24', NULL, 7, NULL, 'Presentazione a cura delle ditte incaricate', 1, 'DdL-RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Manutenzione e controllo fughe refrigeratori produzione', 'D.M. 17/03/03', 'annual', '2026-06-30', '2026-02-24', NULL, 7, NULL, NULL, 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Formazione RLS - 4 ore anno', 'D.Lgs 81/08 art. 18, 35', 'annual', '2026-06-30', '2026-02-24', '2025-06-12', 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Medicina del Lavoro'), 'Derattizzazione', NULL, 'semiannual', '2026-06-30', '2026-02-24', NULL, 7, 'Locale Matrici', NULL, 1, 'Incaricato');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Manutenzioni'), 'Pulizia piazzali', NULL, 'semiannual', '2026-06-30', '2026-02-24', NULL, 7, NULL, 'Audit Interno', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Autorizzazione raccolta e trasporto rifiuti speciali non pericolosi FEROLMET', 'D.Lgs 81/08 art. 19, Accordo Stato Regioni 2011', 'once', '2026-07-30', '2023-02-07', NULL, 7, NULL, NULL, 1, 'Amministrazione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente'), 'Autorizzazione raccolta e trasporto rifiuti speciali pericolosi FEROLMET', 'D.Lgs 81/08 art. 202', 'once', '2026-07-30', '2023-02-07', NULL, 7, NULL, NULL, 1, 'Amministrazione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Autocontrolli periodici emissioni in atmosfera', 'D.Lgs 81/08 art. 223', 'annual', '2026-07-31', '2026-02-24', '2025-06-18', 7, NULL, NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES (NULL, 'Comunicazione BANCA DATI GAS FLUORURATI Pompa di Calore Evaporatore riguardante le emissioni in atmosfera di gas fluorurati', 'D.Lgs 81/08 art. 53', 'annual', '2026-07-31', '2026-03-21', NULL, 7, 'Locale Archivio', 'Presentazione a cura della ditta incaricata EcoTechno', 1, 'Delegato di funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Manutenzioni'), 'Controllo documentazione Sicurezza', 'D.Lgs 152/2006 Parte III (art. 124)', 'semiannual', '2026-08-31', '2026-02-24', '2026-02-23', 7, 'Locale Archivio', 'Audit Interno', 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES (NULL, 'Comunicazione BANCA DATI GAS FLUORURATI refrigeratore raffrescamento inizio linee riguardante le emissioni in atmosfera di gas fluorurati', 'Accordo Stato regioni 07.02.2013', 'annual', '2026-08-31', '2026-02-24', NULL, 7, NULL, 'Presentazione a cura delle ditte incaricate', 1, 'DdL-RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Manutenzioni'), 'Verifica dispositivi su uscite di emergenza', 'D.Lgs 81/08 art. 26', 'semiannual', '2026-08-31', '2026-02-24', '2026-02-26', 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Verifica integrità ed efficienza porte tagliafuoco', 'D.Lgs. 81/2008 (art. 18, c 1, u) D.Lgs. 81/2008 (art. 26)', 'semiannual', '2026-08-31', '2026-02-24', '2026-02-26', 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali Medicina del Lavoro'), 'Controllo dispositivi lotta antincendio (estintori, idranti, lance, manichette)', 'D.Lgs 81/08 art. 37, 73, Accordo Stato Regioni 2011', 'semiannual', '2026-08-31', '2026-02-24', '2026-02-26', 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Adempimenti'), 'Formazione del Preposto', NULL, 'quinquennial', '2026-09-20', '2023-02-07', '2021-09-21', 7, NULL, 'Accordo Stato Regioni 21.12.2011', 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Indagine Ambientale', 'D.Lgs 81/08 - Salute e Sicurezza nei luoghi di lavoro', 'annual', '2026-09-30', '2026-02-24', '2025-09-10', 7, 'Locale Archivio', NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente'), 'Controllo fumi e funzionamento caldaia', 'D.Lgs 81/08 art. 190', 'annual', '2026-10-31', '2026-03-21', '2025-05-15', 7, 'Locale Archivio', NULL, 1, 'Delegato di funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Attività in appalto'), 'Riunione periodica art. 35', 'D.Lgs 152/2006 Parte III (art. 124)', 'annual', '2026-11-30', '2025-11-20', '2025-11-20', 7, 'Locale Archivio', NULL, 1, 'Delegato di Funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Valutazione Rischio Vibrazioni', 'D.M. 10.03.1998', 'quadriennial', '2026-12-01', '2023-02-07', '2022-12-02', 7, 'Locale Archivio', 'Accordo Stato Regioni 21.12.2011', 1, 'DdL - RSPP Medico - RLS');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Medicina del Lavoro'), 'Valutazione stress lavoro-correllato', 'D. 20 del 24/01/2011', 'biennial', '2026-12-19', '2024-12-20', '2024-12-20', 7, 'Locale Archivio', NULL, 1, 'DdL - RSPP Medico - RLS');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'Verifica scadenza sostanze assorbenti e neutralizzanti al fine di prevenire l''inquinamento del suolo da parte dell''elettrolita delle batterie', 'D.Lgs 152/2006 Parte V', 'annual', '2026-12-31', '2025-12-22', '2025-12-22', 7, NULL, 'Audit Interno', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambienti - Scarichi idrici'), 'Emergenze (alluvione e sversamento oli) Verifica stato tombini e armadio materiale emergenza alluvione e presenza materiale per pulizia olii', 'D.Lgs 81/08 art. 216', 'annual', '2026-12-31', '2025-12-22', '2025-12-22', 7, NULL, 'Verifica stato tombini eseguita da Roberto+Daniele', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'Simulazione periodica di evacuazione antincendio', 'D.Lgs 81/08 art. 25', 'annual', '2026-12-31', '2025-12-22', '2025-12-22', 7, 'Locale Archivio', NULL, 1, 'Delegato di Funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Contributo CONAI', 'D.Lgs 152/2006 Parte V', 'once', '2027-01-20', '2026-01-20', '2026-01-20', 7, NULL, NULL, 1, '--');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Sicurezza'), 'Nomina 3° resp. per caldaie >35kw', 'D.Lgs 81/08 art. 71', 'once', '2027-01-31', '2026-01-19', '2026-01-19', 7, NULL, NULL, 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente'), 'Analisi schiuma delll''impianto', 'D.Lgs. 81/2008 (art. 18, c 1, u) D.Lgs. 81/2008 (art. 26)', 'annual', '2027-01-31', '2026-03-21', '2026-02-04', 7, 'Locale Archivio', 'Attività in appalto a Ditta specializzata', 1, 'Delegato di funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Verifica impianto di Messa a Terra', 'DPR 462/01', 'biennial', '2027-02-12', '2025-02-19', '2025-02-12', 7, 'Locale Archivio', NULL, 1, 'Delegato di funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Test carrellisti uso sostanze dopanti', 'D.Lgs 81/08', 'annual', '2027-02-28', '2025-02-10', '2026-02-09', 7, NULL, NULL, 1, 'Medico competente');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Visite Mediche ai dipendenti', 'D.Lgs 81/08 art. 18, 38, 41', 'annual', '2027-02-28', '2025-02-19', '2026-02-09', 7, 'Locale Archivio', NULL, 1, 'Medico Competente');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambienti - Scarichi idrici'), 'Controllo Legionella Acqua', 'D.Lgs 81/08 art. 32, Accordo Stato Regioni 2006', 'annual', '2027-02-28', '2026-02-24', '2026-02-19', 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Appaltatori'), 'Verbale di Sopralluogo dei Locali del Medico Competente', 'D.Lgs 81/08', 'annual', '2027-02-28', '2026-02-24', '2026-02-09', 7, 'Locale Archivio', NULL, 1, 'Medico Competente');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Manutenzioni'), 'Dichiarazione acque derivate da pozzi', 'ART. 22 DEL D.LGS. 11 MAGGIO 1999, N. 152, COSÌ COME MODIFICATO DALL’ART. 6, COMMA 1, LETT. A) DEL D.LGS. 18 AGOSTO 2000, N. 258', 'annual', '2027-03-31', '2026-03-21', '2026-03-20', 7, 'Locale Archivio', NULL, 1, 'Delegato di funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'Verifica Periodica Recipienti Pressione', NULL, 'triennial', '2027-04-04', '2024-07-05', '2024-07-05', 7, 'Locale Archivio', 'Autoclave l 1.000 pozzi', 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Revisione scala a gabbia Stp sul tetto del fabbricato tra porzione uffici (più alta) e porzione reparto', 'D.Lgs. n. 81/2008', 'quinquennial', '2027-10-24', '2023-01-31', '2022-10-25', 7, 'Locale Archivio', NULL, 1, 'DdL-RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Visita medica per Addetti VDT', 'D.Lgs 152/2006 Parte III (art. 124)', 'biennial', '2028-02-28', '2026-02-09', '2026-02-09', 7, 'Locale Archivio', NULL, 1, 'Medico Competente');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'Valutazione Rischio da ROA', 'D.P.R. 147 del 15/02/2006', 'quadriennial', '2028-07-14', '2024-11-23', '2024-07-15', 7, 'Locale Archivio', 'Accordo Stato Regioni 21.12.2011', 1, 'DdL - RSPP Medico - RLS');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Valutazione Rischio da CEM', 'LEGGE N. 70/94 D.M. 17/12/2009', 'quadriennial', '2028-07-14', '2024-11-23', '2024-07-15', 7, 'Locale Archivio', 'Accordo Stato Regioni 21.12.2011', 1, 'DdL - RSPP Medico - RLS');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Sicurezza'), 'Valutazione Rischio Rumore', 'D.M. 16/03/1998 L. n° 447/95', 'quadriennial', '2028-09-30', '2024-11-23', '2024-10-02', 7, 'Locale Archivio', NULL, 1, 'DdL - RSPP Medico - RLS');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Protezione contro i fulmini - Valutazione del rischio e scelta delle misure di protezione', 'D.Lgs. 81/2008 / CEI EN 62305-1-2-3-4 / CEI 81-29 / CEI EN IEC 62858', 'quinquennial', '2028-12-31', '2023-09-04', '2023-07-31', 7, 'Locale Archivio', 'Rivalutazione del valore NG', 1, 'DdL-RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambienti - Scarichi idrici'), 'Autorizzazione allo scarico rete fognaria di acque reflue domestiche', 'D.Lgs 152/2006 Parte III (art. 124)', 'quadriennial', '2028-05-21', '2026-03-21', '2025-05-22', 7, NULL, 'Validità 4 anni sino al 21/05/2029. Domanda di rinnovo almeno 1 anno prima della scadenza', 1, 'Condominio Fornace Venere in persona dell''Amministratore');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambienti - Emissioni in atmosfera'), 'Bombola acetilene l 5 omologata 2020/01', 'D.Lgs 03.04.2006 n.152 Legge R. 21.06.1999 n.18', 'decennial', '2030-01-31', '2021-02-23', '2021-02-23', 7, 'Locale Archivio', NULL, 1, 'DdL');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'Bombola ossigeno l 5 omologata 2020/11', 'D.Lgs 81/08 art. 36, 73,', 'decennial', '2030-11-30', '2021-02-23', '2021-02-23', 7, 'Locale Archivio', NULL, 1, 'DdL');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Sicurezza'), 'Revisione parapetti sul tetto del fabbricato (n. 3 sui lati corti)', 'EN14122 - NTC2018', 'decennial', '2032-10-24', '2023-01-31', '2022-10-25', 7, 'Locale Archivio', NULL, 1, 'DdL-RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Sicurezza'), 'Revisione parapetti soletta locali tecnici (refrigeratori)', 'EN ISO 14122-3:2001+A1:2010', 'decennial', '2032-12-18', '2023-01-31', '2022-12-19', 7, 'Locale Archivio', NULL, 1, 'DdL-RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Emissioni in atmosfera'), 'Rinnovo Autorizzazione alle emissioni in atmosfera', 'D.Lgs 152/2006 Parte III (art. 124)', 'quindecennial', '2037-08-23', '2023-02-24', '2023-02-24', 7, 'Locale Archivio', NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Medicina del Lavoro'), 'Informazione ai lavoratori specifica', 'D.Lgs 81/08 art. 18', 'once', '2027-04-18', NULL, NULL, 7, NULL, 'Accordo Stato Regioni 21.12.2011', 1, 'Delegato di funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Formazione ai lavoratori specifica', 'D.Lgs 81/08 art. 17,28,29', 'once', '2027-04-18', NULL, NULL, 7, NULL, 'Accordo Stato Regioni 21.12.2011', 1, 'Delegato di funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Documento Valutazione dei Rischi', 'D.Lgs 81/08 art. 18 D.M. 10.03.1998', 'once', '2026-10-13', '2025-10-13', '2025-10-13', 7, 'Locale Archivio', NULL, 1, 'DdL');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Piano di Emergenza', 'D.Lgs 81/08 art. 47', 'once', '2026-01-15', '2025-01-15', '2025-01-15', 7, 'Locale Archivio', NULL, 1, 'Delegato di Funzioni');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Nomina Medico Competente', 'D.Lgs 81/08 art. 47', 'once', '2026-11-20', '2025-11-20', '2025-11-20', 7, 'Locale Archivio', NULL, 1, 'DdL');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Segnalazione all''INAIL nominativo RLS', 'D.Lgs 81/08 art. 31', 'once', '2027-04-18', '2023-02-07', NULL, 7, 'Locale Archivio', NULL, 1, 'DdL');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prevenzione - Sicurezza'), 'Nomina RSPP', 'D.P.R. 43/12 attuazione del Regolamento CE 842/2006', 'once', '2023-02-01', '2023-02-07', '2022-02-01', 7, NULL, NULL, 1, 'DdL');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES (NULL, 'Elezione RLS', NULL, 'once', '2020-02-18', '2023-02-07', '2019-02-18', 7, 'Locale Archivio', NULL, 1, 'Direzione-Lavoratori');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'Valutazione impatto acustico', 'D.Lgs 81/08 art. 19, Accordo Stato Regioni 2011', 'once', '2018-11-07', '2023-02-07', '2017-11-07', 7, NULL, NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Adempimenti'), 'Nomina Addetti Prev. Incendi', 'Lege n° 457/78', 'once', '2027-04-18', '2023-02-07', NULL, 7, 'Locale Archivio', 'Vedi Organigramma', 1, 'DdL');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Adempimenti'), 'Comunicazione dei requisiti come Medico Competente', 'Regolamento Comunale 56', 'once', '2027-04-18', '2023-02-07', NULL, 7, 'Locale Archivio', NULL, 1, 'Medico Competente');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Adempimenti'), 'Protocollo Sanitario', 'D.M. 37/08', 'annual', '2022-12-02', '2023-02-07', '2021-12-02', 7, 'Locale Archivio', 'Rivalidato tramite Riunione Periodica', 1, 'Medico Competente');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Adempimenti'), 'Nomina Dirigenti e Preposti', 'D.M. 37/08', 'once', '2027-04-18', '2023-02-07', NULL, 7, 'Locale Archivio', 'v. incarichi', 1, 'DdL');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Adempimenti'), 'Cassetta di Pronto Soccorso, controllo presenza e scadenza dispositivi e medicinali', 'D.M. 37/08', 'monthly', '2026-05-18', '2023-02-07', NULL, 7, 'Cassette P.S. Reparto e magazzino', NULL, 1, 'Incaricata');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Adempimenti'), 'Nomina Addetti Primo Soccorso', 'D.Lgs 334/99', 'once', '2027-04-18', '2023-02-07', NULL, 7, 'Locale Archivio', 'Vedi Organigramma', 1, 'DdL');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente'), 'Formazione RSPP', 'SIS - Corporate', 'quinquennial', '2031-04-18', '2023-02-07', NULL, 7, NULL, 'Non applicabile. Nomina R.S.P.P. esterno', 1, 'DdL');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente'), 'Decontaminazione/Sanificazione ambienti uffici-spogliatoi-servizi igienici-parti comuni con perossido di idrogeno', NULL, 'once', '2022-12-18', '2023-02-07', '2021-12-18', 7, NULL, NULL, 1, 'Ditta Incaricata');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Controllo scaffalature', 'D.Lgs 81/08 art. 71', 'annual', '2023-01-17', '2023-02-07', '2022-01-17', 7, NULL, 'Audit Interno', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Valutazione Rischio Esplosione', 'D.M. 10.03.1998, UNI 9994', 'once', '2018-03-24', '2023-02-07', '2017-03-24', 7, 'Locale Archivio', NULL, 1, 'DdL - RSPP Medico - RLS');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Valutazione Rischio Chimico', 'D.Lgs 152/2006 Parte IV', 'once', '2027-03-05', '2026-03-21', '2026-03-05', 7, 'Locale Archivio', 'Accordo Stato Regioni 21.12.2011', 1, 'DdL - RSPP Medico - RLS');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Agibilità locali', 'D.Lgs 152/2006 Parte IV', 'once', '2027-04-18', NULL, NULL, 7, 'Locale Archivio', NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Autorizzazione Sanitaria', 'D.M. 17/12/2009', 'once', '2027-04-18', NULL, NULL, 7, 'Locale Archivio', NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Conformità Impianto Elettrico', 'D.M. 17/12/2009', 'once', '2027-04-18', NULL, NULL, 7, 'Locale Archivio', NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Conformità Impianto Gas', 'D.Lgs 152/2006 Parte IV', 'once', '2027-04-18', NULL, NULL, 7, 'Locale Archivio', NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Conformità Impianto Idrico', 'D.Lgs 152/2006 Parte IV', 'once', '2027-04-18', NULL, NULL, 7, 'Locale Archivio', NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Notifica per aziende a rischio rilevante d''incendio', NULL, 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, '--');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambienti - Scarichi idrici'), 'Rendicontazione indicatori ambientali', 'D.Lgs 152/2006 Parte III (art. 124)', 'annual', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Etichettatura contenitori rifiuti pericolosi', 'D.M. 10.03.1998, UNI 9994', 'once', '2027-04-18', NULL, NULL, 7, NULL, 'Audit Interno', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Formulario d’identificazione-compilazione 1° copia e rientro 4° entro 3 mesi', 'D.M. 10.03.1998, UNI 9994', 'once', '2027-04-18', NULL, NULL, 7, NULL, 'Audit Interno', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Tenuta dei registri di carico e scarico rifiuti', 'DPR 151/2011 (art. 6) D.M. 10/03/1998', 'once', '2027-04-18', NULL, NULL, 7, NULL, 'Audit Interno', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente - Rifiuti'), 'Verifica autorizzazione trasportatori-smaltitori', 'D.Lgs 81/08 art. 290', 'annual', '2027-04-18', NULL, NULL, 7, NULL, 'Audit Interno', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente'), 'Monitoraggio consumi: acqua, energia elettrica, metano. materia prima trasformate e rottamata', 'D.M. 10.03.1998', 'monthly', '2026-05-18', NULL, NULL, 7, NULL, 'Audit Interno', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente'), 'Aggiornamento quaderno conduzione impianti. Verifica ed eventuale pulizia/sostituzione filtri cappe emissioni', 'D.LGS. 06.11.2011', 'once', '2027-04-18', NULL, NULL, 7, NULL, 'vedi registro', 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Ambiente'), 'Invio analisi dati analitici acque scaricate', 'D.M. 10.03.1998, UNI 9994', 'once', '2027-04-18', NULL, NULL, 7, NULL, 'Presenza di scarichi assimilabili ai domestici', 1, '--');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Controllo segnaletica / cartellonistica antincendio', 'D.M. 10.03.1998, UNI 9994', 'semiannual', '2026-10-18', NULL, NULL, 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Tenuta di REGISTO ANTINCENDIO per attività soggette al DPR 151/2011', 'D.M. 10.03.1998, UNI 9994', 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, '--');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Valutazione Rischio Incendio', 'D.M. 10.03.1998, UNI 9994', 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Antincendio'), 'Verifica Maniglioni antipanico non certificati CE', 'D.M. 10.03.1998, UNI 9994', 'semiannual', '2026-10-18', NULL, NULL, 7, NULL, 'Maniglioni certificati CE', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'Verifica periodica funzionamento impianto luci di emergenza', 'Norma CEI 11-27:2014 " lavoratori su quadri elettrici"', 'semiannual', '2026-10-18', NULL, NULL, 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'Qualifica Appaltatori', 'D.M. 10.03.1998', 'once', '2027-04-18', NULL, NULL, 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'DURC aggiornato', 'D.M. 388/2003', 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'Presenza DUVRI per attività in appalto (aggiornamento o prima emissione)', 'Accordo Stato Regioni 22.02.2012', 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Formazione'), 'Presenza verbale di sopralluogo per coordinamento', 'D.Lgs 81/08 art. 19, Accordo Stato Regioni 2011', 'once', '2027-04-18', NULL, NULL, 7, NULL, 'All''avvio dei lavori, se necessario', 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Manutenzioni'), 'Richiesta all''appaltatore / prestatore d''Opera dell''idoneità tecnico professionale: iscrizione Camera di Commercio, autocertificazione i.d.p.', 'D.M. 329/04', 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Manutenzioni'), 'Verificare che i lavoratori in regime di appalto mostrino apposita tessera di riconoscimento (fotografia, generalità del lavoratore e indicazione del datore di lavoro)', 'D.M. 17/03/03', 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Medicina del Lavoro'), 'Formazione addetti che effettuano lavoro sotto tensione', 'D.Lgs 81/08 art. 25, 40', 'quinquennial', '2031-04-18', NULL, NULL, 7, NULL, 'vedi Elenco formazione', 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Medicina del Lavoro'), 'Formazione Addetti Prev. Incendi', 'D.Lgs 81/08 art. 25', 'triennial', '2029-04-18', NULL, NULL, 7, 'Locale Archivio', 'vedi Elenco formazione', 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Formazione Addetti Primo Soccorso', 'D.Lgs 81/08', 'triennial', '2029-04-18', NULL, NULL, 7, 'Locale Archivio', 'vedi Elenco formazione', 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Formazione Carrellisti', 'D.Lgs 81/08 art. 18', 'quinquennial', '2031-04-18', NULL, NULL, 7, 'Locale Archivio', 'vedi Elenco formazione', 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Formazione del Dirigente', 'D.Lgs 81/08 art. 63, allegato IV', 'quinquennial', '2031-04-18', NULL, NULL, 7, NULL, 'Accordo Stato Regioni 21.12.2011', 1, '--');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Comunicazione Messa in Servizio rec. Pressione', 'D.Lgs 81/08 art. 18,74, 75, 76, 77, 78, 79', 'once', '2019-09-07', NULL, '2018-09-07', 7, NULL, NULL, 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Controllo dispositivi lotta antincendio', 'D.Lgs 81/08 art. 71', 'monthly', '2026-05-18', NULL, NULL, 7, 'Locale Archivio', NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Comunicazione all''INAIL degli infortuni', 'D.Lgs 81/08 art. 71', 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Conformità Ambienti di Lavoro', 'D.Lgs 81/08 art. 19, 37', 'annual', '2027-04-18', NULL, NULL, 7, NULL, 'Audit Interno', 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Consegna e Gestione DPI', 'D.Lgs 81/08 art. 161', 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Controllo scale', 'D.Lgs 81/08 art. 167', 'once', '2027-04-18', NULL, NULL, 7, NULL, 'Audit Interno', 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Controllo imbragature, golfari', 'D.Lgs 81/08 art. 172', 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, '--');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Controllo macchinari', 'D.Lgs 81/08', 'once', '2027-04-18', NULL, NULL, 7, NULL, 'Audit Interno', 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Presenza segnaletica', 'D.Lgs 81/08 art. 271', 'once', '2027-04-18', NULL, NULL, 7, 'Locale Archivio', 'Audit Interno', 1, 'RSPP');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Valutazione del Rischio da MMC', 'D.Lgs 81/08 art. 249', 'once', '2027-04-18', NULL, NULL, 7, 'Locale Archivio', 'in caso di variazioni', 1, 'DdL - RSPP Medico - RLS');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Medicina del Lavoro'), 'Valutazione del Rischio da VDT', 'D.Lgs 81/08 art. 236', 'once', '2027-04-18', NULL, NULL, 7, 'Locale Archivio', 'in caso di variazioni', 1, 'DdL - RSPP Medico - RLS');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Pronto Soccorso'), 'Valutazione Protezione Fulmini', 'D.M. 388/2003', 'once', '2027-04-18', NULL, NULL, 7, 'Locale Archivio', NULL, 1, 'Direzione');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Pronto Soccorso'), 'Valutazione Rischio da Agenti Biologici', 'D.Lgs 81/08 art. 18, 43, 45', 'annual', '2018-05-22', NULL, '2017-05-22', 7, 'Locale Archivio', 'Riunione periodica', 1, 'DdL - RSPP Medico - RLS');
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Valutazione Rischio da Amianto', NULL, 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, NULL);
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES ((SELECT id FROM scad_subjects WHERE name = 'Prescrizioni generali'), 'Valutazione Rischio da Cancerogeni', NULL, 'once', '2027-04-18', NULL, NULL, 7, NULL, NULL, 1, NULL);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../../ajax/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : 0;
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM scad_deadlines WHERE subject_id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$inUse = (int)$stmt->fetchColumn();
|
||||
|
||||
if ($inUse > 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => "Impossibile eliminare: l'argomento è utilizzato in $inUse scadenz" . ($inUse === 1 ? 'a' : 'e') . '.',
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo->prepare("DELETE FROM scad_subjects WHERE id = ?")->execute([$id]);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => 'Argomento eliminato.']);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../../ajax/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : null;
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$color = trim($_POST['color'] ?? '');
|
||||
|
||||
if ($name === '') {
|
||||
echo json_encode(['success' => false, 'message' => 'Il nome è obbligatorio.']);
|
||||
exit;
|
||||
}
|
||||
if (mb_strlen($name) > 100) {
|
||||
echo json_encode(['success' => false, 'message' => 'Il nome supera 100 caratteri.']);
|
||||
exit;
|
||||
}
|
||||
if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $color)) {
|
||||
$color = '#6c757d';
|
||||
}
|
||||
|
||||
// Uniqueness check
|
||||
if ($id) {
|
||||
$stmt = $pdo->prepare("SELECT id FROM scad_subjects WHERE name = ? AND id <> ?");
|
||||
$stmt->execute([$name, $id]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("SELECT id FROM scad_subjects WHERE name = ?");
|
||||
$stmt->execute([$name]);
|
||||
}
|
||||
if ($stmt->fetch()) {
|
||||
echo json_encode(['success' => false, 'message' => 'Esiste già un argomento con questo nome.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($id) {
|
||||
$stmt = $pdo->prepare("UPDATE scad_subjects SET name = ?, color = ? WHERE id = ?");
|
||||
$stmt->execute([$name, $color, $id]);
|
||||
$savedId = $id;
|
||||
} else {
|
||||
$stmt = $pdo->prepare("INSERT INTO scad_subjects (name, color) VALUES (?, ?)");
|
||||
$stmt->execute([$name, $color]);
|
||||
$savedId = (int)$pdo->lastInsertId();
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => $id ? 'Argomento aggiornato.' : 'Argomento creato.',
|
||||
'id' => $savedId,
|
||||
'name' => $name,
|
||||
'color' => $color,
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
<?php include('../../include/headscript.php'); ?>
|
||||
<?php
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$subjects = $pdo->query("
|
||||
SELECT s.*,
|
||||
(SELECT COUNT(*) FROM scad_deadlines d WHERE d.subject_id = s.id) AS deadline_count,
|
||||
(SELECT COUNT(*) FROM scad_deadlines d WHERE d.subject_id = s.id AND d.status <> 'completed') AS open_count
|
||||
FROM scad_subjects s
|
||||
ORDER BY s.name
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<?php
|
||||
$scriptDir = dirname($_SERVER['SCRIPT_NAME']);
|
||||
// subjects/index.php -> scadenzario -> userarea
|
||||
$baseHref = dirname(dirname($scriptDir)) . '/';
|
||||
?>
|
||||
<base href="<?= $baseHref ?>">
|
||||
<?php include('../../cssinclude.php'); ?>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<title>Scadenzario - Argomenti</title>
|
||||
<script>if(window.innerWidth>1024)document.addEventListener('DOMContentLoaded',function(){document.getElementById('appWrapper').classList.add('toggled')})</script>
|
||||
<style>
|
||||
:root {
|
||||
--scad-primary: #5a8fd8;
|
||||
--scad-primary-hover: #4578c0;
|
||||
--scad-heading: #2c3e6b;
|
||||
--scad-card-bg: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%);
|
||||
--scad-card-border: #dde4f0;
|
||||
}
|
||||
.scad-card { border: none; border-radius: 0.75rem; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden; }
|
||||
.scad-card .card-header { background: var(--scad-card-bg); border-bottom: 1px solid var(--scad-card-border); padding: 1rem 1.25rem; }
|
||||
.scad-card .card-header h5 { font-weight: 700; color: var(--scad-heading); margin: 0; font-size: 1.1rem; letter-spacing: -0.01em; }
|
||||
.scad-card .card-body { padding: 1.25rem; }
|
||||
|
||||
.btn-scad-primary { background: var(--scad-primary); border: none; color: #fff; font-weight: 600; font-size: 0.85rem; padding: 0.5rem 1rem; border-radius: 0.5rem; transition: all 0.2s; }
|
||||
.btn-scad-primary:hover { background: var(--scad-primary-hover); color: #fff; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(90,143,216,0.35); }
|
||||
.btn-scad-outline { background: transparent; border: 1.5px solid var(--scad-primary); color: var(--scad-primary); font-weight: 600; font-size: 0.85rem; padding: 0.45rem 1rem; border-radius: 0.5rem; transition: all 0.2s; }
|
||||
.btn-scad-outline:hover { background: var(--scad-primary); color: #fff; transform: translateY(-1px); }
|
||||
|
||||
.btn-action { width: 32px; height: 32px; padding: 0; display: inline-flex; align-items: center; justify-content: center; border: none; border-radius: 0.4rem; font-size: 0.85rem; transition: all 0.15s; }
|
||||
.btn-action-edit { background: rgba(90,143,216,0.12); color: var(--scad-primary); }
|
||||
.btn-action-edit:hover { background: var(--scad-primary); color: #fff; }
|
||||
.btn-action-delete { background: rgba(220,53,69,0.12); color: #dc3545; }
|
||||
.btn-action-delete:hover { background: #dc3545; color: #fff; }
|
||||
.btn-action-history { background: rgba(108,117,125,0.12); color: #495057; }
|
||||
.btn-action-history:hover { background: #495057; color: #fff; }
|
||||
|
||||
.color-swatch { width: 28px; height: 28px; border-radius: 6px; display: inline-block; border: 1px solid rgba(0,0,0,0.08); vertical-align: middle; }
|
||||
.subject-row { border-left: 4px solid var(--row-color, #e9ecef); }
|
||||
|
||||
/* Mobile cards */
|
||||
.subject-card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--scad-card-border);
|
||||
border-left: 5px solid var(--row-color, #e9ecef);
|
||||
border-radius: 0.6rem;
|
||||
padding: 0.85rem 0.95rem;
|
||||
margin-bottom: 0.6rem;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
||||
}
|
||||
.subject-card .sc-header {
|
||||
display: flex; align-items: center; gap: 0.6rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.subject-card .sc-swatch {
|
||||
width: 22px; height: 22px; border-radius: 5px;
|
||||
border: 1px solid rgba(0,0,0,0.08); flex-shrink: 0;
|
||||
}
|
||||
.subject-card .sc-name {
|
||||
font-weight: 700; color: var(--scad-heading);
|
||||
font-size: 0.95rem; flex: 1; word-break: break-word;
|
||||
}
|
||||
.subject-card .sc-stats {
|
||||
display: flex; gap: 0.75rem; font-size: 0.8rem; color: #6c757d;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.subject-card .sc-stats strong { color: var(--scad-heading); }
|
||||
.subject-card .sc-actions {
|
||||
display: flex; gap: 0.4rem; justify-content: flex-end;
|
||||
}
|
||||
|
||||
.empty-state { text-align: center; padding: 3rem 1rem; color: #6c757d; }
|
||||
.empty-state i { font-size: 3rem; opacity: 0.3; margin-bottom: 1rem; }
|
||||
|
||||
/* Color picker swatches */
|
||||
.color-picker-grid { display: grid; grid-template-columns: repeat(10, 1fr); gap: 0.4rem; margin-bottom: 0.75rem; }
|
||||
.color-picker-swatch { width: 100%; aspect-ratio: 1; border-radius: 6px; cursor: pointer; border: 2px solid transparent; transition: all 0.15s; }
|
||||
.color-picker-swatch:hover { transform: scale(1.1); }
|
||||
.color-picker-swatch.selected { border-color: #2c3e6b; transform: scale(1.1); box-shadow: 0 2px 8px rgba(44,62,107,0.3); }
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.scad-card .card-header { flex-direction: column; gap: 0.75rem; align-items: flex-start !important; }
|
||||
.header-actions { width: 100%; }
|
||||
.header-actions .btn { width: 100%; justify-content: center; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('../../include/navbar.php'); ?>
|
||||
<?php include('../../include/topbar.php'); ?>
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb" style="background:transparent;padding:0;margin:0;font-size:0.85rem">
|
||||
<li class="breadcrumb-item"><a href="scadenzario/index.php">Scadenzario</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Argomenti</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="card scad-card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<h5><i class="fa-solid fa-tags me-2"></i>Argomenti</h5>
|
||||
<div class="header-actions d-flex gap-2 flex-wrap">
|
||||
<a href="scadenzario/index.php" class="btn btn-scad-outline d-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-arrow-left"></i><span>Scadenzario</span>
|
||||
</a>
|
||||
<button class="btn btn-scad-primary d-inline-flex align-items-center gap-2" id="btnAddSubject">
|
||||
<i class="fa-solid fa-plus"></i><span>Nuovo Argomento</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (count($subjects) === 0): ?>
|
||||
<div class="empty-state">
|
||||
<i class="fa-solid fa-tags"></i>
|
||||
<p>Nessun argomento definito.<br>Clicca <strong>"Nuovo Argomento"</strong> per aggiungere il primo.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div id="subjectsList">
|
||||
<!-- MOBILE: Cards (< md) -->
|
||||
<div class="d-md-none">
|
||||
<?php foreach ($subjects as $s): ?>
|
||||
<div class="subject-card"
|
||||
style="--row-color: <?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-id="<?= (int)$s['id'] ?>"
|
||||
data-name="<?= htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-color="<?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-in-use="<?= (int)$s['deadline_count'] ?>">
|
||||
<div class="sc-header">
|
||||
<span class="sc-swatch" style="background: <?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"></span>
|
||||
<span class="sc-name"><?= htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') ?></span>
|
||||
</div>
|
||||
<div class="sc-stats">
|
||||
<span>Scadenze: <strong><?= (int)$s['deadline_count'] ?></strong></span>
|
||||
<span>Aperte: <strong><?= (int)$s['open_count'] ?></strong></span>
|
||||
</div>
|
||||
<div class="sc-actions">
|
||||
<a href="scadenzario/index.php?subject_id=<?= (int)$s['id'] ?>" class="btn-action btn-action-history" title="Storico scadenze">
|
||||
<i class="fa-solid fa-clock-rotate-left"></i>
|
||||
</a>
|
||||
<button class="btn-action btn-action-edit btn-edit" title="Modifica"><i class="fa-solid fa-pen"></i></button>
|
||||
<button class="btn-action btn-action-delete btn-delete" title="Elimina"><i class="fa-solid fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- DESKTOP: Table (>= md) -->
|
||||
<div class="d-none d-md-block">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:60px">Colore</th>
|
||||
<th>Nome</th>
|
||||
<th class="text-center" style="width:120px">Scadenze</th>
|
||||
<th class="text-center" style="width:120px">Aperte</th>
|
||||
<th class="text-center" style="width:180px">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($subjects as $s): ?>
|
||||
<tr class="subject-row"
|
||||
style="--row-color: <?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-id="<?= (int)$s['id'] ?>"
|
||||
data-name="<?= htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-color="<?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-in-use="<?= (int)$s['deadline_count'] ?>">
|
||||
<td><span class="color-swatch" style="background: <?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"></span></td>
|
||||
<td class="fw-semibold" style="color:var(--scad-heading)"><?= htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td class="text-center"><?= (int)$s['deadline_count'] ?></td>
|
||||
<td class="text-center"><?= (int)$s['open_count'] ?></td>
|
||||
<td class="text-center">
|
||||
<div class="d-inline-flex gap-1">
|
||||
<a href="scadenzario/index.php?subject_id=<?= (int)$s['id'] ?>" class="btn-action btn-action-history" title="Storico scadenze">
|
||||
<i class="fa-solid fa-clock-rotate-left"></i>
|
||||
</a>
|
||||
<button class="btn-action btn-action-edit btn-edit" title="Modifica"><i class="fa-solid fa-pen"></i></button>
|
||||
<button class="btn-action btn-action-delete btn-delete" title="Elimina"><i class="fa-solid fa-trash"></i></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php include('../../include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<!-- Subject Modal -->
|
||||
<div class="modal fade" id="subjectModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="subjectModalTitle">Nuovo Argomento</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
|
||||
</div>
|
||||
<form id="subjectForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="subjId" name="id" value="">
|
||||
<div class="mb-3">
|
||||
<label for="subjName" class="form-label fw-semibold">Nome <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="subjName" name="name" maxlength="100" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Colore</label>
|
||||
<div class="color-picker-grid" id="colorPickerGrid"></div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<input type="color" class="form-control form-control-color" id="subjColor" name="color" value="#6c757d" style="width:56px;height:38px;padding:2px">
|
||||
<input type="text" class="form-control" id="subjColorText" maxlength="7" placeholder="#RRGGBB" style="max-width:130px;font-family:monospace">
|
||||
<span class="text-muted small">Personalizzato</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-scad-primary">Salva</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include('../../jsinclude.php'); ?>
|
||||
<script>
|
||||
$(function () {
|
||||
const PRESET_COLORS = [
|
||||
'#dc3545','#e8930c','#ffc107','#198754','#20c997',
|
||||
'#0dcaf0','#0d6efd','#5a8fd8','#6f42c1','#d63384',
|
||||
'#6c757d','#495057','#212529','#8b4513','#795548',
|
||||
'#b88a44','#e83e8c','#17a2b8','#28a745','#343a40'
|
||||
];
|
||||
|
||||
function buildPicker(selected) {
|
||||
const $grid = $('#colorPickerGrid').empty();
|
||||
PRESET_COLORS.forEach(c => {
|
||||
const $sw = $('<div class="color-picker-swatch"></div>')
|
||||
.css('background', c)
|
||||
.attr('data-color', c);
|
||||
if (c.toLowerCase() === (selected || '').toLowerCase()) $sw.addClass('selected');
|
||||
$sw.on('click', function () {
|
||||
$('#colorPickerGrid .color-picker-swatch').removeClass('selected');
|
||||
$(this).addClass('selected');
|
||||
$('#subjColor').val(c);
|
||||
$('#subjColorText').val(c);
|
||||
});
|
||||
$grid.append($sw);
|
||||
});
|
||||
}
|
||||
|
||||
function openModal(data) {
|
||||
const isEdit = !!data;
|
||||
$('#subjectModalTitle').text(isEdit ? 'Modifica Argomento' : 'Nuovo Argomento');
|
||||
$('#subjId').val(isEdit ? data.id : '');
|
||||
$('#subjName').val(isEdit ? data.name : '');
|
||||
const color = isEdit ? data.color : '#6c757d';
|
||||
$('#subjColor').val(color);
|
||||
$('#subjColorText').val(color);
|
||||
buildPicker(color);
|
||||
new bootstrap.Modal('#subjectModal').show();
|
||||
}
|
||||
|
||||
$('#btnAddSubject').on('click', () => openModal(null));
|
||||
|
||||
$('#subjectsList').on('click', '.btn-edit', function () {
|
||||
const $tr = $(this).closest('[data-id]');
|
||||
openModal({ id: $tr.data('id'), name: $tr.data('name'), color: $tr.data('color') });
|
||||
});
|
||||
|
||||
$('#subjectsList').on('click', '.btn-delete', function () {
|
||||
const $tr = $(this).closest('[data-id]');
|
||||
const inUse = parseInt($tr.data('in-use') || 0, 10);
|
||||
const name = $tr.data('name');
|
||||
if (inUse > 0) {
|
||||
Swal.fire({ icon: 'warning', title: 'Impossibile eliminare',
|
||||
text: `L'argomento "${name}" è utilizzato in ${inUse} scadenz${inUse === 1 ? 'a' : 'e'}.` });
|
||||
return;
|
||||
}
|
||||
Swal.fire({
|
||||
title: `Eliminare "${name}"?`,
|
||||
icon: 'warning', showCancelButton: true,
|
||||
confirmButtonText: 'Elimina', cancelButtonText: 'Annulla',
|
||||
confirmButtonColor: '#dc3545'
|
||||
}).then(r => {
|
||||
if (!r.isConfirmed) return;
|
||||
$.post('scadenzario/subjects/ajax/delete_subject.php', { id: $tr.data('id') })
|
||||
.done(res => {
|
||||
if (res.success) { location.reload(); }
|
||||
else { Swal.fire({ icon: 'error', title: 'Errore', text: res.message }); }
|
||||
})
|
||||
.fail(() => Swal.fire({ icon: 'error', title: 'Errore di rete' }));
|
||||
});
|
||||
});
|
||||
|
||||
$('#subjColor').on('input', function () { $('#subjColorText').val($(this).val()); });
|
||||
$('#subjColorText').on('input', function () {
|
||||
const v = $(this).val();
|
||||
if (/^#[0-9A-Fa-f]{6}$/.test(v)) $('#subjColor').val(v);
|
||||
});
|
||||
|
||||
$('#subjectForm').on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const payload = {
|
||||
id: $('#subjId').val(),
|
||||
name: $('#subjName').val().trim(),
|
||||
color: $('#subjColor').val()
|
||||
};
|
||||
if (!payload.name) { Swal.fire({ icon: 'warning', title: 'Nome obbligatorio' }); return; }
|
||||
$.post('scadenzario/subjects/ajax/save_subject.php', payload)
|
||||
.done(res => {
|
||||
if (res.success) { location.reload(); }
|
||||
else { Swal.fire({ icon: 'error', title: 'Errore', text: res.message }); }
|
||||
})
|
||||
.fail(() => Swal.fire({ icon: 'error', title: 'Errore di rete' }));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,868 @@
|
||||
<?php include('include/headscript.php'); ?>
|
||||
<?php
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$userId = (int)($iduserlogin ?? 0);
|
||||
|
||||
if ($userId <= 0) {
|
||||
die('Utente non valido.');
|
||||
}
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (empty($_SESSION['user_settings_csrf'])) {
|
||||
$_SESSION['user_settings_csrf'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
$csrfToken = $_SESSION['user_settings_csrf'];
|
||||
|
||||
$successMessage = '';
|
||||
$errorMessage = '';
|
||||
|
||||
// Load countries.
|
||||
$countries = [];
|
||||
try {
|
||||
$stmtCountries = $pdo->query("
|
||||
SELECT id, name, iso_3166_2
|
||||
FROM auth_countries
|
||||
ORDER BY name ASC
|
||||
");
|
||||
$countries = $stmtCountries->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (Exception $e) {
|
||||
$countries = [];
|
||||
}
|
||||
|
||||
// Load current user.
|
||||
$stmtProfileUser = $pdo->prepare("
|
||||
SELECT
|
||||
id,
|
||||
email,
|
||||
password,
|
||||
first_name,
|
||||
last_name,
|
||||
phone,
|
||||
avatar,
|
||||
address,
|
||||
country_id,
|
||||
birthday,
|
||||
role_id,
|
||||
status,
|
||||
last_login
|
||||
FROM auth_users
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmtProfileUser->execute([$userId]);
|
||||
$profileUser = $stmtProfileUser->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$profileUser) {
|
||||
die('Utente non trovato.');
|
||||
}
|
||||
|
||||
function e($value)
|
||||
{
|
||||
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function normalizeAvatarPath($avatar)
|
||||
{
|
||||
$avatar = trim((string)$avatar);
|
||||
|
||||
if ($avatar === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// If the database already contains a complete relative path, use it as it is.
|
||||
if (
|
||||
str_starts_with($avatar, '../') ||
|
||||
str_starts_with($avatar, './') ||
|
||||
str_starts_with($avatar, '/') ||
|
||||
str_starts_with($avatar, 'http://') ||
|
||||
str_starts_with($avatar, 'https://')
|
||||
) {
|
||||
return $avatar;
|
||||
}
|
||||
|
||||
// If the database contains only the filename, build the expected user upload path.
|
||||
return '../upload/users/' . $avatar;
|
||||
}
|
||||
|
||||
function getAvatarInitials($profileUser)
|
||||
{
|
||||
$first = trim((string)($profileUser['first_name'] ?? ''));
|
||||
$last = trim((string)($profileUser['last_name'] ?? ''));
|
||||
$email = trim((string)($profileUser['email'] ?? ''));
|
||||
|
||||
$initials = '';
|
||||
|
||||
if ($first !== '') {
|
||||
$initials .= mb_substr($first, 0, 1);
|
||||
}
|
||||
|
||||
if ($last !== '') {
|
||||
$initials .= mb_substr($last, 0, 1);
|
||||
}
|
||||
|
||||
if ($initials === '' && $email !== '') {
|
||||
$initials = mb_substr($email, 0, 1);
|
||||
}
|
||||
|
||||
return strtoupper($initials ?: 'U');
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$postedToken = $_POST['csrf_token'] ?? '';
|
||||
|
||||
if (!hash_equals($csrfToken, $postedToken)) {
|
||||
$errorMessage = 'Sessione non valida. Ricarica la pagina e riprova.';
|
||||
} else {
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$firstName = trim($_POST['first_name'] ?? '');
|
||||
$lastName = trim($_POST['last_name'] ?? '');
|
||||
$phone = trim($_POST['phone'] ?? '');
|
||||
$address = trim($_POST['address'] ?? '');
|
||||
$countryId = $_POST['country_id'] !== '' ? (int)$_POST['country_id'] : null;
|
||||
$birthday = trim($_POST['birthday'] ?? '');
|
||||
|
||||
$currentPassword = $_POST['current_password'] ?? '';
|
||||
$newPassword = $_POST['new_password'] ?? '';
|
||||
$confirmPassword = $_POST['confirm_password'] ?? '';
|
||||
|
||||
$birthdayValue = null;
|
||||
$avatarToSave = $profileUser['avatar'];
|
||||
|
||||
if ($birthday !== '') {
|
||||
$dateObj = DateTime::createFromFormat('Y-m-d', $birthday);
|
||||
|
||||
if (!$dateObj || $dateObj->format('Y-m-d') !== $birthday) {
|
||||
$errorMessage = 'La data di nascita non è valida.';
|
||||
} else {
|
||||
$birthdayValue = $birthday;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$errorMessage && $email === '') {
|
||||
$errorMessage = 'L’email è obbligatoria.';
|
||||
}
|
||||
|
||||
if (!$errorMessage && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$errorMessage = 'L’email inserita non è valida.';
|
||||
}
|
||||
|
||||
// Check unique email.
|
||||
if (!$errorMessage) {
|
||||
$stmtCheckEmail = $pdo->prepare("
|
||||
SELECT id
|
||||
FROM auth_users
|
||||
WHERE email = ? AND id <> ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmtCheckEmail->execute([$email, $userId]);
|
||||
|
||||
if ($stmtCheckEmail->fetchColumn()) {
|
||||
$errorMessage = 'Questa email è già utilizzata da un altro utente.';
|
||||
}
|
||||
}
|
||||
|
||||
// Avatar upload.
|
||||
if (!$errorMessage && isset($_FILES['avatar']) && $_FILES['avatar']['error'] !== UPLOAD_ERR_NO_FILE) {
|
||||
if ($_FILES['avatar']['error'] !== UPLOAD_ERR_OK) {
|
||||
$errorMessage = 'Errore durante il caricamento dell’avatar.';
|
||||
} else {
|
||||
$maxFileSize = 2 * 1024 * 1024; // 2 MB
|
||||
|
||||
if ($_FILES['avatar']['size'] > $maxFileSize) {
|
||||
$errorMessage = 'L’avatar non può superare 2 MB.';
|
||||
} else {
|
||||
$tmpFile = $_FILES['avatar']['tmp_name'];
|
||||
$originalName = $_FILES['avatar']['name'];
|
||||
|
||||
$allowedMimeTypes = [
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/png' => 'png',
|
||||
'image/webp' => 'webp',
|
||||
'image/gif' => 'gif',
|
||||
];
|
||||
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$mimeType = $finfo->file($tmpFile);
|
||||
|
||||
if (!array_key_exists($mimeType, $allowedMimeTypes)) {
|
||||
$errorMessage = 'Formato avatar non valido. Sono consentiti JPG, PNG, WEBP o GIF.';
|
||||
} else {
|
||||
$uploadDir = __DIR__ . '/../upload/users/';
|
||||
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
|
||||
$extension = $allowedMimeTypes[$mimeType];
|
||||
$safeOriginalName = preg_replace('/[^A-Za-z0-9_\-\.]/', '_', pathinfo($originalName, PATHINFO_FILENAME));
|
||||
$fileName = time() . '_' . $userId . '_' . $safeOriginalName . '.' . $extension;
|
||||
|
||||
$destination = $uploadDir . $fileName;
|
||||
|
||||
if (!move_uploaded_file($tmpFile, $destination)) {
|
||||
$errorMessage = 'Impossibile salvare il file avatar.';
|
||||
} else {
|
||||
// Path used by pages inside userarea, for example:
|
||||
// <img src="../upload/users/file.jpg">
|
||||
$avatarToSave = $fileName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$passwordToSave = null;
|
||||
$wantsPasswordChange = ($currentPassword !== '' || $newPassword !== '' || $confirmPassword !== '');
|
||||
|
||||
if (!$errorMessage && $wantsPasswordChange) {
|
||||
if ($currentPassword === '') {
|
||||
$errorMessage = 'Inserisci la password attuale.';
|
||||
} elseif ($newPassword === '') {
|
||||
$errorMessage = 'Inserisci la nuova password.';
|
||||
} elseif (strlen($newPassword) < 8) {
|
||||
$errorMessage = 'La nuova password deve contenere almeno 8 caratteri.';
|
||||
} elseif ($newPassword !== $confirmPassword) {
|
||||
$errorMessage = 'La conferma password non corrisponde.';
|
||||
} elseif (!password_verify($currentPassword, $profileUser['password'])) {
|
||||
$errorMessage = 'La password attuale non è corretta.';
|
||||
} else {
|
||||
// Password is encrypted before saving.
|
||||
$passwordToSave = password_hash($newPassword, PASSWORD_DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$errorMessage) {
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$stmtUpdate = $pdo->prepare("
|
||||
UPDATE auth_users
|
||||
SET
|
||||
email = :email,
|
||||
first_name = :first_name,
|
||||
last_name = :last_name,
|
||||
phone = :phone,
|
||||
avatar = :avatar,
|
||||
address = :address,
|
||||
country_id = :country_id,
|
||||
birthday = :birthday,
|
||||
updated_at = NOW()
|
||||
WHERE id = :id
|
||||
LIMIT 1
|
||||
");
|
||||
|
||||
$stmtUpdate->execute([
|
||||
':email' => $email,
|
||||
':first_name' => $firstName !== '' ? $firstName : null,
|
||||
':last_name' => $lastName !== '' ? $lastName : null,
|
||||
':phone' => $phone !== '' ? $phone : null,
|
||||
':avatar' => $avatarToSave !== '' ? $avatarToSave : null,
|
||||
':address' => $address !== '' ? $address : null,
|
||||
':country_id' => $countryId,
|
||||
':birthday' => $birthdayValue,
|
||||
':id' => $userId,
|
||||
]);
|
||||
|
||||
if ($passwordToSave !== null) {
|
||||
$stmtPassword = $pdo->prepare("
|
||||
UPDATE auth_users
|
||||
SET password = ?, updated_at = NOW()
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmtPassword->execute([$passwordToSave, $userId]);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
$successMessage = $passwordToSave !== null
|
||||
? 'Profilo, avatar e password aggiornati correttamente.'
|
||||
: 'Profilo aggiornato correttamente.';
|
||||
|
||||
// Reload updated user.
|
||||
$stmtProfileUser->execute([$userId]);
|
||||
$profileUser = $stmtProfileUser->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$_SESSION['user_settings_csrf'] = bin2hex(random_bytes(32));
|
||||
$csrfToken = $_SESSION['user_settings_csrf'];
|
||||
} catch (Exception $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
$errorMessage = 'Errore durante il salvataggio delle impostazioni.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$avatarPath = normalizeAvatarPath($profileUser['avatar'] ?? '');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
<?php include('cssinclude.php'); ?>
|
||||
<title>Impostazioni Utente <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #f3f6f8, #e8eef3);
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
color: #2b3e50;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: 1.4rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.settings-wrap {
|
||||
width: 100%;
|
||||
max-width: 1280px;
|
||||
}
|
||||
|
||||
h3.settings-title {
|
||||
text-align: center;
|
||||
font-weight: 800;
|
||||
color: #2b3e50;
|
||||
margin-bottom: 1.2rem;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
padding: 16px 18px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.settings-icon {
|
||||
font-size: 1.8rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.settings-heading {
|
||||
margin: 0;
|
||||
font-weight: 800;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.settings-subtitle {
|
||||
margin: 0;
|
||||
color: #6b7a89;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.settings-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.profile-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
gap: 24px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.avatar-panel {
|
||||
background: linear-gradient(135deg, #f7fbff, #edf5ff);
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 18px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.avatar-box {
|
||||
width: 132px;
|
||||
height: 132px;
|
||||
border-radius: 34px;
|
||||
background: linear-gradient(135deg, #cde5ff, #dff0ff);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3rem;
|
||||
font-weight: 800;
|
||||
color: #2b3e50;
|
||||
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
margin: 0 auto 14px auto;
|
||||
}
|
||||
|
||||
.avatar-box img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-name {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 800;
|
||||
margin: 0;
|
||||
color: #2b3e50;
|
||||
}
|
||||
|
||||
.avatar-email {
|
||||
color: #6b7a89;
|
||||
margin: 4px 0 14px 0;
|
||||
font-size: 0.92rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.avatar-upload-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
border-radius: 14px;
|
||||
padding: 11px 14px;
|
||||
cursor: pointer;
|
||||
font-weight: 800;
|
||||
color: #2b3e50;
|
||||
background: linear-gradient(135deg, #e5e7eb, #f3f4f6);
|
||||
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.2s ease;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.avatar-upload-label:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.13);
|
||||
}
|
||||
|
||||
.avatar-upload-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.avatar-help {
|
||||
font-size: 0.82rem;
|
||||
color: #6b7a89;
|
||||
margin-top: 10px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.profile-meta {
|
||||
color: #6b7a89;
|
||||
font-size: 0.88rem;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 700;
|
||||
color: #2b3e50;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select {
|
||||
border-radius: 12px;
|
||||
border: 1px solid #d8e0e7;
|
||||
padding: 10px 12px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: #8bbcf7;
|
||||
box-shadow: 0 0 0 0.18rem rgba(139, 188, 247, 0.25);
|
||||
}
|
||||
|
||||
.help-text {
|
||||
font-size: 0.86rem;
|
||||
color: #6b7a89;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.password-box {
|
||||
background: linear-gradient(135deg, #fff7e6, #fffaf0);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
border: 1px solid rgba(255, 184, 107, 0.45);
|
||||
}
|
||||
|
||||
.btn-save-settings {
|
||||
border: 0;
|
||||
border-radius: 16px;
|
||||
padding: 13px 24px;
|
||||
font-size: 1rem;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #61ce5dff, #61ce5dff);
|
||||
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-save-settings:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.13);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
border: 0;
|
||||
border-radius: 16px;
|
||||
padding: 13px 20px;
|
||||
font-size: 1rem;
|
||||
font-weight: 800;
|
||||
color: #2b3e50;
|
||||
background: linear-gradient(135deg, #e5e7eb, #f3f4f6);
|
||||
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-back:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.13);
|
||||
color: #2b3e50;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 16px;
|
||||
border: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.readonly-note {
|
||||
background: linear-gradient(135deg, #cde5ff, #dff0ff);
|
||||
border-radius: 16px;
|
||||
padding: 14px 16px;
|
||||
color: #2b3e50;
|
||||
font-weight: 600;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.actions-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.selected-file-name {
|
||||
font-size: 0.84rem;
|
||||
color: #2b3e50;
|
||||
margin-top: 8px;
|
||||
font-weight: 600;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.profile-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.avatar-panel {
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.settings-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.actions-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-save-settings,
|
||||
.btn-back {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<div class="settings-wrap">
|
||||
<h3 class="settings-title">Impostazioni Utente</h3>
|
||||
|
||||
<?php if ($successMessage): ?>
|
||||
<div class="alert alert-success">
|
||||
<?= e($successMessage); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($errorMessage): ?>
|
||||
<div class="alert alert-danger">
|
||||
<?= e($errorMessage); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= e($csrfToken); ?>">
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="settings-header">
|
||||
<div class="settings-icon">👤</div>
|
||||
<div>
|
||||
<p class="settings-heading">Profilo personale</p>
|
||||
<p class="settings-subtitle">Dati anagrafici, contatti e avatar utente</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-body">
|
||||
|
||||
<div class="readonly-note">
|
||||
Ruolo, stato account e impostazioni di sicurezza avanzate non sono modificabili da questa pagina.
|
||||
</div>
|
||||
|
||||
<div class="profile-layout">
|
||||
<div class="avatar-panel">
|
||||
<div class="avatar-box" id="avatarPreviewBox">
|
||||
<?php if (!empty($avatarPath)): ?>
|
||||
<img src="<?= e($avatarPath); ?>" class="user-img" alt="user avatar" id="avatarPreviewImage">
|
||||
<?php else: ?>
|
||||
<span id="avatarInitials"><?= e(getAvatarInitials($profileUser)); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<p class="avatar-name">
|
||||
<?= e(trim(($profileUser['first_name'] ?? '') . ' ' . ($profileUser['last_name'] ?? '')) ?: 'Utente'); ?>
|
||||
</p>
|
||||
|
||||
<p class="avatar-email">
|
||||
<?= e($profileUser['email']); ?>
|
||||
</p>
|
||||
|
||||
<label for="avatar" class="avatar-upload-label">
|
||||
Carica avatar
|
||||
</label>
|
||||
|
||||
<input type="file"
|
||||
class="avatar-upload-input"
|
||||
id="avatar"
|
||||
name="avatar"
|
||||
accept="image/jpeg,image/png,image/webp,image/gif">
|
||||
|
||||
<div id="selectedFileName" class="selected-file-name"></div>
|
||||
|
||||
<div class="avatar-help">
|
||||
Formati consentiti: JPG, PNG, WEBP, GIF.<br>
|
||||
Dimensione massima: 2 MB.
|
||||
</div>
|
||||
|
||||
<div class="profile-meta">
|
||||
Stato account: <?= e($profileUser['status']); ?>
|
||||
<?php if (!empty($profileUser['last_login'])): ?>
|
||||
<br>Ultimo accesso: <?= e(date('d/m/Y H:i', strtotime($profileUser['last_login']))); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="first_name">Nome</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="first_name"
|
||||
name="first_name"
|
||||
value="<?= e($profileUser['first_name']); ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="last_name">Cognome</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="last_name"
|
||||
name="last_name"
|
||||
value="<?= e($profileUser['last_name']); ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="email">Email</label>
|
||||
<input type="email"
|
||||
class="form-control"
|
||||
id="email"
|
||||
name="email"
|
||||
value="<?= e($profileUser['email']); ?>"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="phone">Telefono</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="phone"
|
||||
name="phone"
|
||||
value="<?= e($profileUser['phone']); ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="birthday">Data di nascita</label>
|
||||
<input type="date"
|
||||
class="form-control"
|
||||
id="birthday"
|
||||
name="birthday"
|
||||
value="<?= e($profileUser['birthday']); ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="country_id">Paese</label>
|
||||
<select class="form-select" id="country_id" name="country_id">
|
||||
<option value="">Seleziona...</option>
|
||||
<?php foreach ($countries as $country): ?>
|
||||
<option value="<?= (int)$country['id']; ?>"
|
||||
<?= ((int)$profileUser['country_id'] === (int)$country['id']) ? 'selected' : ''; ?>>
|
||||
<?= e($country['name'] . (!empty($country['iso_3166_2']) ? ' (' . $country['iso_3166_2'] . ')' : '')); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<label class="form-label" for="address">Indirizzo</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="address"
|
||||
name="address"
|
||||
value="<?= e($profileUser['address']); ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="settings-header">
|
||||
<div class="settings-icon">🔐</div>
|
||||
<div>
|
||||
<p class="settings-heading">Cambio password</p>
|
||||
<p class="settings-subtitle">Compila questa sezione solo se vuoi modificare la password</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-body">
|
||||
<div class="password-box">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="current_password">Password attuale</label>
|
||||
<input type="password"
|
||||
class="form-control"
|
||||
id="current_password"
|
||||
name="current_password"
|
||||
autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="new_password">Nuova password</label>
|
||||
<input type="password"
|
||||
class="form-control"
|
||||
id="new_password"
|
||||
name="new_password"
|
||||
autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="confirm_password">Conferma nuova password</label>
|
||||
<input type="password"
|
||||
class="form-control"
|
||||
id="confirm_password"
|
||||
name="confirm_password"
|
||||
autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-text">
|
||||
Se lasci questi campi vuoti, la password attuale rimane invariata.
|
||||
La nuova password deve avere almeno 8 caratteri.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions-row">
|
||||
<a href="production_dashboard.php" class="btn-back">← Torna alla dashboard</a>
|
||||
<button type="submit" class="btn-save-settings">Salva impostazioni</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const avatarInput = document.getElementById('avatar');
|
||||
const previewBox = document.getElementById('avatarPreviewBox');
|
||||
const selectedFileName = document.getElementById('selectedFileName');
|
||||
|
||||
if (!avatarInput || !previewBox) {
|
||||
return;
|
||||
}
|
||||
|
||||
avatarInput.addEventListener('change', function() {
|
||||
const file = this.files && this.files[0] ? this.files[0] : null;
|
||||
|
||||
if (!file) {
|
||||
selectedFileName.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
selectedFileName.textContent = file.name;
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(event) {
|
||||
previewBox.innerHTML = '';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = event.target.result;
|
||||
img.className = 'user-img';
|
||||
img.alt = 'user avatar';
|
||||
|
||||
previewBox.appendChild(img);
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
; This file is for unifying the coding style for different editors and IDEs.
|
||||
; More information at https://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.yml]
|
||||
indent_size = 2
|
||||
@@ -1,25 +0,0 @@
|
||||
# Set the default behavior, in case people don't have core.autocrlf set.
|
||||
* text eol=lf
|
||||
|
||||
# Explicitly declare text files you want to always be normalized and converted
|
||||
# to native line endings on checkout.
|
||||
*.c text
|
||||
*.h text
|
||||
|
||||
# Declare files that will always have CRLF line endings on checkout.
|
||||
*.sln text eol=crlf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.otf binary
|
||||
*.eot binary
|
||||
*.svg binary
|
||||
*.ttf binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
|
||||
*.css linguist-vendored
|
||||
*.scss linguist-vendored
|
||||
*.js linguist-vendored
|
||||
CHANGELOG.md export-ignore
|
||||
@@ -1,37 +0,0 @@
|
||||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
|
||||
name: PHP ${{ matrix.php }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['7.3', '7.4', '8.0', '8.1']
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Cache composer
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.composer/cache/files
|
||||
key: php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extension-csv: bcmath, ctype, dom, fileinfo, intl, gd, json, mbstring, pdo, pdo_sqlite, openssl, sqlite, xml, zip
|
||||
coverage: none
|
||||
|
||||
- name: Install composer
|
||||
run: composer install --no-interaction --no-scripts --no-suggest --prefer-source
|
||||
|
||||
- name: Execute tests
|
||||
run: vendor/bin/phpunit
|
||||
@@ -1,9 +0,0 @@
|
||||
/.idea
|
||||
/.history
|
||||
/.vscode
|
||||
/tests/databases
|
||||
/vendor
|
||||
.DS_Store
|
||||
.phpunit.result.cache
|
||||
composer.phar
|
||||
composer.lock
|
||||
@@ -1,4 +0,0 @@
|
||||
preset: psr2
|
||||
|
||||
enabled:
|
||||
- concat_with_spaces
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Andreas Lutro
|
||||
|
||||
Copyright (c) 2017 Akaunting
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
-186
@@ -1,186 +0,0 @@
|
||||
# Persistent settings package for Laravel
|
||||
|
||||
[](https://github.com/akaunting/laravel-setting)
|
||||
[](https://styleci.io/repos/101231817)
|
||||
[](LICENSE.md)
|
||||
|
||||
This package allows you to save settings in a more persistent way. You can use the database and/or json file to save your settings. You can also override the Laravel config.
|
||||
|
||||
* Driver support
|
||||
* Helper function
|
||||
* Blade directive
|
||||
* Override config values
|
||||
* Encryption
|
||||
* Custom file, table and columns
|
||||
* Auto save
|
||||
* Extra columns
|
||||
* Cache support
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 1. Install
|
||||
|
||||
Run the following command:
|
||||
|
||||
```bash
|
||||
composer require akaunting/laravel-setting
|
||||
```
|
||||
|
||||
### 2. Register (for Laravel < 5.5)
|
||||
|
||||
Register the service provider in `config/app.php`
|
||||
|
||||
```php
|
||||
Akaunting\Setting\Provider::class,
|
||||
```
|
||||
|
||||
Add alias if you want to use the facade.
|
||||
|
||||
```php
|
||||
'Setting' => Akaunting\Setting\Facade::class,
|
||||
```
|
||||
|
||||
### 3. Publish
|
||||
|
||||
Publish config file.
|
||||
|
||||
```bash
|
||||
php artisan vendor:publish --tag=setting
|
||||
```
|
||||
|
||||
### 4. Database
|
||||
|
||||
Create table for database driver
|
||||
|
||||
```bash
|
||||
php artisan migrate
|
||||
```
|
||||
|
||||
### 5. Configure
|
||||
|
||||
You can change the options of your app from `config/setting.php` file
|
||||
|
||||
## Usage
|
||||
|
||||
You can either use the helper method like `setting('foo')` or the facade `Setting::get('foo')`
|
||||
|
||||
### Facade
|
||||
|
||||
```php
|
||||
Setting::get('foo', 'default');
|
||||
Setting::get('nested.element');
|
||||
Setting::set('foo', 'bar');
|
||||
Setting::forget('foo');
|
||||
$settings = Setting::all();
|
||||
```
|
||||
|
||||
### Helper
|
||||
|
||||
```php
|
||||
setting('foo', 'default');
|
||||
setting('nested.element');
|
||||
setting(['foo' => 'bar']);
|
||||
setting()->forget('foo');
|
||||
$settings = setting()->all();
|
||||
```
|
||||
|
||||
You can call the `save()` method to save the changes.
|
||||
|
||||
### Auto Save
|
||||
|
||||
If you enable the `auto_save` option in the config file, settings will be saved automatically every time the application shuts down if anything has been changed.
|
||||
|
||||
### Blade Directive
|
||||
|
||||
You can get the settings directly in your blade templates using the helper method or the blade directive like `@setting('foo')`
|
||||
|
||||
### Override Config Values
|
||||
|
||||
You can easily override default config values by adding them to the `override` option in `config/setting.php`, thereby eliminating the need to modify the default config files and also allowing you to change said values during production. Ex:
|
||||
|
||||
```php
|
||||
'override' => [
|
||||
"app.name" => "app_name",
|
||||
"app.env" => "app_env",
|
||||
"mail.driver" => "app_mail_driver",
|
||||
"mail.host" => "app_mail_host",
|
||||
],
|
||||
```
|
||||
|
||||
The values on the left corresponds to the respective config value (Ex: config('app.name')) and the value on the right is the name of the `key` in your settings table/json file.
|
||||
|
||||
### Encryption
|
||||
|
||||
If you like to encrypt the values for a given key, you can pass the key to the `encrypted_keys` option in `config/setting.php` and the rest is automatically handled by using Laravel's built-in encryption facilities. Ex:
|
||||
|
||||
```php
|
||||
'encrypted_keys' => [
|
||||
"payment.key",
|
||||
],
|
||||
```
|
||||
|
||||
### JSON Storage
|
||||
|
||||
You can modify the path used on run-time using `setting()->setPath($path)`.
|
||||
|
||||
### Database Storage
|
||||
|
||||
If you want to use the database as settings storage then you should run the `php artisan migrate`. You can modify the table fields from the `create_settings_table` file in the migrations directory.
|
||||
|
||||
#### Extra Columns
|
||||
|
||||
If you want to store settings for multiple users/clients in the same database you can do so by specifying extra columns:
|
||||
|
||||
```php
|
||||
setting()->setExtraColumns(['user_id' => Auth::user()->id]);
|
||||
```
|
||||
|
||||
where `user_id = x` will now be added to the database query when settings are retrieved, and when new settings are saved, the `user_id` will be populated.
|
||||
|
||||
If you need more fine-tuned control over which data gets queried, you can use the `setConstraint` method which takes a closure with two arguments:
|
||||
|
||||
- `$query` is the query builder instance
|
||||
- `$insert` is a boolean telling you whether the query is an insert or not. If it is an insert, you usually don't need to do anything to `$query`.
|
||||
|
||||
```php
|
||||
setting()->setConstraint(function($query, $insert) {
|
||||
if ($insert) return;
|
||||
$query->where(/* ... */);
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Drivers
|
||||
|
||||
This package uses the Laravel `Manager` class under the hood, so it's easy to add your own storage driver. All you need to do is extend the abstract `Driver` class, implement the abstract methods and call `setting()->extend`.
|
||||
|
||||
```php
|
||||
class MyDriver extends Akaunting\Setting\Contracts\Driver
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
app('setting.manager')->extend('mydriver', function($app) {
|
||||
return $app->make('MyDriver');
|
||||
});
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
Please see [Releases](../../releases) for more information what has changed recently.
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests are more than welcome. You must follow the PSR coding standards.
|
||||
|
||||
## Security
|
||||
|
||||
If you discover any security related issues, please email security@akaunting.com instead of using the issue tracker.
|
||||
|
||||
## Credits
|
||||
|
||||
- [Denis Duliçi](https://github.com/denisdulici)
|
||||
- [All Contributors](../../contributors)
|
||||
|
||||
## License
|
||||
|
||||
The MIT License (MIT). Please see [LICENSE](LICENSE.md) for more information.
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"name": "akaunting/laravel-setting",
|
||||
"description": "Persistent settings package for Laravel",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"persistent",
|
||||
"settings",
|
||||
"config"
|
||||
],
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Denis Duliçi",
|
||||
"email": "info@akaunting.com",
|
||||
"homepage": "https://akaunting.com",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.5.9",
|
||||
"laravel/framework": ">=5.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": ">=4.8",
|
||||
"mockery/mockery": "0.9.*",
|
||||
"laravel/framework": ">=5.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Akaunting\\Setting\\": "./src"
|
||||
},
|
||||
"files": [
|
||||
"src/helpers.php"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Akaunting\\Setting\\Provider"
|
||||
],
|
||||
"aliases": {
|
||||
"Setting": "Akaunting\\Setting\\Facade"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit backupGlobals="false"
|
||||
backupStaticAttributes="false"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false"
|
||||
syntaxCheck="false"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Package Test Suite">
|
||||
<directory suffix=".php">./tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
@@ -1,132 +0,0 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Enable / Disable auto save
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Auto-save every time the application shuts down
|
||||
|
|
||||
*/
|
||||
'auto_save' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Options for caching. Set whether to enable cache, its key, time to live
|
||||
| in seconds and whether to auto clear after save.
|
||||
|
|
||||
*/
|
||||
'cache' => [
|
||||
'enabled' => false,
|
||||
'key' => 'setting',
|
||||
'ttl' => 3600,
|
||||
'auto_clear' => true,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Setting driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Select where to store the settings.
|
||||
|
|
||||
| Supported: "database", "json", "memory"
|
||||
|
|
||||
*/
|
||||
'driver' => 'database',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Options for database driver. Enter which connection to use, null means
|
||||
| the default connection. Set the table and column names.
|
||||
|
|
||||
*/
|
||||
'database' => [
|
||||
'connection' => null,
|
||||
'table' => 'settings',
|
||||
'key' => 'key',
|
||||
'value' => 'value',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| JSON driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Options for json driver. Enter the full path to the .json file.
|
||||
|
|
||||
*/
|
||||
'json' => [
|
||||
'path' => storage_path() . '/settings.json',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Override application config values
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If defined, settings package will override these config values.
|
||||
|
|
||||
| Sample:
|
||||
| "app.locale" => "settings.locale",
|
||||
|
|
||||
*/
|
||||
'override' => [
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Fallback
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Define fallback settings to be used in case the default is null
|
||||
|
|
||||
| Sample:
|
||||
| "currency" => "USD",
|
||||
|
|
||||
*/
|
||||
'fallback' => [
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Required Extra Columns
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The list of columns required to be set up
|
||||
|
|
||||
| Sample:
|
||||
| "user_id",
|
||||
| "tenant_id",
|
||||
|
|
||||
*/
|
||||
'required_extra_columns' => [
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Define the keys which should be crypt automatically.
|
||||
|
|
||||
| Sample:
|
||||
| "payment.key"
|
||||
|
|
||||
*/
|
||||
'encrypted_keys' => [
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,321 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Akaunting\Setting\Contracts;
|
||||
|
||||
use Akaunting\Setting\Support\Arr;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
abstract class Driver
|
||||
{
|
||||
/**
|
||||
* The settings data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* Whether the store has changed since it was last loaded.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $unsaved = false;
|
||||
|
||||
/**
|
||||
* Whether the settings data are loaded.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $loaded = false;
|
||||
|
||||
/**
|
||||
* Include and merge with fallbacks
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $with_fallback = true;
|
||||
|
||||
/**
|
||||
* Excludes fallback data
|
||||
*/
|
||||
public function withoutFallback()
|
||||
{
|
||||
$this->with_fallback = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific key from the settings data.
|
||||
*
|
||||
* @param string|array $key
|
||||
* @param mixed $default Optional default value.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get($key, $default = null)
|
||||
{
|
||||
if (!$this->checkExtraColumns()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->load();
|
||||
|
||||
return Arr::get($this->data, $key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fallback value if default is null.
|
||||
*
|
||||
* @param string|array $key
|
||||
* @param mixed $default
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFallback($key, $default = null)
|
||||
{
|
||||
if (($default !== null) || is_array($key)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return Arr::get((array) config('setting.fallback'), $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given value is same as fallback.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEqualToFallback($key, $value)
|
||||
{
|
||||
return (string) $this->getFallback($key) == (string) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a key exists in the settings data.
|
||||
*
|
||||
* @param string $key
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has($key)
|
||||
{
|
||||
if (!$this->checkExtraColumns()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->load();
|
||||
|
||||
return Arr::has($this->data, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a specific key to a value in the settings data.
|
||||
*
|
||||
* @param string|array $key Key string or associative array of key => value
|
||||
* @param mixed $value Optional only if the first argument is an array
|
||||
*/
|
||||
public function set($key, $value = null)
|
||||
{
|
||||
if (!$this->checkExtraColumns()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->load();
|
||||
$this->unsaved = true;
|
||||
|
||||
if (is_array($key)) {
|
||||
foreach ($key as $k => $v) {
|
||||
Arr::set($this->data, $k, $v);
|
||||
}
|
||||
} else {
|
||||
Arr::set($this->data, $key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset a key in the settings data.
|
||||
*
|
||||
* @param string $key
|
||||
*/
|
||||
public function forget($key)
|
||||
{
|
||||
if (!$this->checkExtraColumns()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->unsaved = true;
|
||||
|
||||
if ($this->has($key)) {
|
||||
Arr::forget($this->data, $key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset all keys in the settings data.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function forgetAll()
|
||||
{
|
||||
if (!$this->checkExtraColumns()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (config('setting.cache.enabled')) {
|
||||
Cache::forget($this->getCacheKey());
|
||||
}
|
||||
|
||||
$this->unsaved = true;
|
||||
$this->data = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all settings data.
|
||||
*
|
||||
* @return array|bool
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
if (!$this->checkExtraColumns()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$this->load();
|
||||
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save any changes done to the settings data.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
if (!$this->checkExtraColumns()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->unsaved) {
|
||||
// either nothing has been changed, or data has not been loaded, so
|
||||
// do nothing by returning early
|
||||
return;
|
||||
}
|
||||
|
||||
if (config('setting.cache.enabled') && config('setting.cache.auto_clear')) {
|
||||
Cache::forget($this->getCacheKey());
|
||||
}
|
||||
|
||||
$this->write($this->data);
|
||||
$this->unsaved = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure data is loaded.
|
||||
*
|
||||
* @param $force Force a reload of data. Default false.
|
||||
*/
|
||||
public function load($force = false)
|
||||
{
|
||||
if (!$this->checkExtraColumns()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->loaded && !$force) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fallback_data = $this->with_fallback ? config('setting.fallback') : [];
|
||||
$driver_data = $this->readData();
|
||||
|
||||
$this->data = Arr::merge((array) $fallback_data, (array) $driver_data);
|
||||
$this->loaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read data from driver or cache
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function readData()
|
||||
{
|
||||
if (config('setting.cache.enabled')) {
|
||||
return $this->readDataFromCache();
|
||||
}
|
||||
|
||||
return $this->read();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read data from cache
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function readDataFromCache()
|
||||
{
|
||||
return Cache::remember($this->getCacheKey(), config('setting.cache.ttl'), function () {
|
||||
return $this->read();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if extra columns are set up.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function checkExtraColumns()
|
||||
{
|
||||
if (!$required_extra_columns = config('setting.required_extra_columns')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (array_keys_exists($required_extra_columns, $this->getExtraColumns())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key based on extra columns.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCacheKey()
|
||||
{
|
||||
$key = config('setting.cache.key');
|
||||
|
||||
foreach ($this->getExtraColumns() as $name => $value) {
|
||||
$key .= '_' . $name . '_' . $value;
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extra columns added to the rows.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function getExtraColumns();
|
||||
|
||||
/**
|
||||
* Read data from driver.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function read();
|
||||
|
||||
/**
|
||||
* Write data to driver.
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function write(array $data);
|
||||
}
|
||||
@@ -1,372 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Akaunting\Setting\Drivers;
|
||||
|
||||
use Akaunting\Setting\Contracts\Driver;
|
||||
use Akaunting\Setting\Support\Arr;
|
||||
use Closure;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Support\Arr as LaravelArr;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class Database extends Driver
|
||||
{
|
||||
/**
|
||||
* The database connection instance.
|
||||
*
|
||||
* @var \Illuminate\Database\Connection
|
||||
*/
|
||||
protected $connection;
|
||||
|
||||
/**
|
||||
* The table to query from.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table;
|
||||
|
||||
/**
|
||||
* The key column name to query from.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $key;
|
||||
|
||||
/**
|
||||
* The value column name to query from.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $value;
|
||||
|
||||
/**
|
||||
* Keys which should be encrypt automatically.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $encrypted_keys;
|
||||
|
||||
/**
|
||||
* Any query constraints that should be applied.
|
||||
*
|
||||
* @var Closure|null
|
||||
*/
|
||||
protected $query_constraint;
|
||||
|
||||
/**
|
||||
* Any extra columns that should be added to the rows.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $extra_columns = [];
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Database\Connection $connection
|
||||
* @param string $table
|
||||
*/
|
||||
public function __construct(Connection $connection, $table = null, $key = null, $value = null, array $encrypted_keys = [])
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->table = $table ?: 'settings';
|
||||
$this->key = $key ?: 'key';
|
||||
$this->value = $value ?: 'value';
|
||||
$this->encrypted_keys = $encrypted_keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the table to query from.
|
||||
*
|
||||
* @param string $table
|
||||
*/
|
||||
public function setTable($table)
|
||||
{
|
||||
$this->table = $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the key column name to query from.
|
||||
*
|
||||
* @param string $key
|
||||
*/
|
||||
public function setKey($key)
|
||||
{
|
||||
$this->key = $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value column name to query from.
|
||||
*
|
||||
* @param string $value
|
||||
*/
|
||||
public function setValue($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the query constraint.
|
||||
*
|
||||
* @param Closure $callback
|
||||
*/
|
||||
public function setConstraint(Closure $callback)
|
||||
{
|
||||
$this->data = [];
|
||||
$this->loaded = false;
|
||||
$this->query_constraint = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set extra columns to be added to the rows.
|
||||
*
|
||||
* @param array $columns
|
||||
*/
|
||||
public function setExtraColumns(array $columns)
|
||||
{
|
||||
$this->extra_columns = $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extra columns added to the rows.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getExtraColumns()
|
||||
{
|
||||
return $this->extra_columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function forget($key)
|
||||
{
|
||||
parent::forget($key);
|
||||
|
||||
// because the database driver cannot store empty arrays, remove empty
|
||||
// arrays to keep data consistent before and after saving
|
||||
$segments = explode('.', $key);
|
||||
array_pop($segments);
|
||||
|
||||
while ($segments) {
|
||||
$segment = implode('.', $segments);
|
||||
|
||||
// non-empty array - exit out of the loop
|
||||
if ($this->get($segment)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// remove the empty array and move on to the next segment
|
||||
$this->forget($segment);
|
||||
array_pop($segments);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function write(array $data)
|
||||
{
|
||||
// Get current data
|
||||
$db_data = $this->newQuery()->get([$this->key, $this->value])->toArray();
|
||||
|
||||
$insert_data = LaravelArr::dot($data);
|
||||
$update_data = [];
|
||||
$delete_keys = [];
|
||||
|
||||
foreach ($db_data as $db_row) {
|
||||
$key = $db_row->{$this->key};
|
||||
$value = $db_row->{$this->value};
|
||||
|
||||
$is_in_insert = $is_different_in_db = $is_same_as_fallback = false;
|
||||
|
||||
if (isset($insert_data[$key])) {
|
||||
$is_in_insert = true;
|
||||
$is_different_in_db = (string) $insert_data[$key] != (string) $value;
|
||||
$is_same_as_fallback = $this->isEqualToFallback($key, $insert_data[$key]);
|
||||
}
|
||||
|
||||
if ($is_in_insert) {
|
||||
if ($is_same_as_fallback) {
|
||||
// Delete if new data is same as fallback
|
||||
$delete_keys[] = $key;
|
||||
} elseif ($is_different_in_db) {
|
||||
// Update if new data is different from db
|
||||
$update_data[$key] = $insert_data[$key];
|
||||
}
|
||||
} else {
|
||||
// Delete if current db not available in new data
|
||||
$delete_keys[] = $key;
|
||||
}
|
||||
|
||||
unset($insert_data[$key]);
|
||||
}
|
||||
|
||||
foreach ($update_data as $key => $value) {
|
||||
$value = $this->prepareValue($key, $value);
|
||||
|
||||
$this->newQuery()
|
||||
->where($this->key, '=', $key)
|
||||
->update([$this->value => $value]);
|
||||
}
|
||||
|
||||
if ($insert_data) {
|
||||
$this->newQuery(true)
|
||||
->insert($this->prepareInsertData($insert_data));
|
||||
}
|
||||
|
||||
if ($delete_keys) {
|
||||
$this->newQuery()
|
||||
->whereIn($this->key, $delete_keys)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms settings data into an array ready to be insterted into the
|
||||
* database. Call array_dot on a multidimensional array before passing it
|
||||
* into this method!
|
||||
*
|
||||
* @param array $data Call array_dot on a multidimensional array before passing it into this method!
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function prepareInsertData(array $data)
|
||||
{
|
||||
$db_data = [];
|
||||
|
||||
if ($this->getExtraColumns()) {
|
||||
foreach ($data as $key => $value) {
|
||||
$value = $this->prepareValue($key, $value);
|
||||
|
||||
// Don't insert if same as fallback
|
||||
if ($this->isEqualToFallback($key, $value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$db_data[] = array_merge(
|
||||
$this->getExtraColumns(),
|
||||
[$this->key => $key, $this->value => $value]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
foreach ($data as $key => $value) {
|
||||
$value = $this->prepareValue($key, $value);
|
||||
|
||||
// Don't insert if same as fallback
|
||||
if ($this->isEqualToFallback($key, $value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$db_data[] = [$this->key => $key, $this->value => $value];
|
||||
}
|
||||
}
|
||||
|
||||
return $db_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided key should be encrypted or not.
|
||||
* Also type casts the given value to a string so errors with booleans or integers are handeled.
|
||||
* Otherwise it returns the original value.
|
||||
*
|
||||
* @param string $key Key to check if it's inside the encryptedValues variable.
|
||||
* @param mixed $value Info: Encryption only supports strings.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function prepareValue(string $key, $value)
|
||||
{
|
||||
// Check if key should be encrypted
|
||||
if (in_array($key, $this->encrypted_keys)) {
|
||||
// Cast to string to avoid error when a user passes a boolean value
|
||||
return Crypt::encryptString((string) $value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided key should be decrypted or not.
|
||||
* Otherwise it returns the original value.
|
||||
*
|
||||
* @param string $key Key to check if it's inside the encryptedValues variable.
|
||||
* @param mixed $value Info: Encryption only supports strings.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function unpackValue(string $key, $value)
|
||||
{
|
||||
// Check if key should be encrypted
|
||||
if (in_array($key, $this->encrypted_keys)) {
|
||||
// Cast to string to avoid error when a user passes a boolean value
|
||||
return Crypt::decryptString((string) $value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function read()
|
||||
{
|
||||
return $this->parseReadData($this->newQuery()->get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse data coming from the database.
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function parseReadData($data)
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($data as $row) {
|
||||
if (is_array($row)) {
|
||||
$key = $row[$this->key];
|
||||
$value = $row[$this->value];
|
||||
} elseif (is_object($row)) {
|
||||
$key = $row->{$this->key};
|
||||
$value = $row->{$this->value};
|
||||
} else {
|
||||
$msg = 'Expected array or object, got ' . gettype($row);
|
||||
throw new \UnexpectedValueException($msg);
|
||||
}
|
||||
|
||||
// Encryption
|
||||
$value = $this->unpackValue($key, $value);
|
||||
|
||||
Arr::set($results, $key, $value);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new query builder instance.
|
||||
*
|
||||
* @param bool $insert
|
||||
*
|
||||
* @return \Illuminate\Database\Query\Builder
|
||||
*/
|
||||
protected function newQuery($insert = false)
|
||||
{
|
||||
$query = $this->connection->table($this->table);
|
||||
|
||||
if (!$insert) {
|
||||
foreach ($this->getExtraColumns() as $key => $value) {
|
||||
$query->where($key, '=', $value);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->query_constraint !== null) {
|
||||
$callback = $this->query_constraint;
|
||||
$callback($query, $insert);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Akaunting\Setting\Drivers;
|
||||
|
||||
use Akaunting\Setting\Contracts\Driver;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
|
||||
class Json extends Driver
|
||||
{
|
||||
/**
|
||||
* @param \Illuminate\Filesystem\Filesystem $files
|
||||
* @param string $path
|
||||
*/
|
||||
public function __construct(Filesystem $files, $path = null)
|
||||
{
|
||||
$this->files = $files;
|
||||
|
||||
$this->setPath($path ?: storage_path() . '/settings.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the path for the JSON file.
|
||||
*
|
||||
* @param string $path
|
||||
*/
|
||||
public function setPath($path)
|
||||
{
|
||||
// If the file does not already exist, we will attempt to create it.
|
||||
if (!$this->files->exists($path)) {
|
||||
$result = $this->files->put($path, '{}');
|
||||
if ($result === false) {
|
||||
throw new \InvalidArgumentException("Could not write to $path.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->files->isWritable($path)) {
|
||||
throw new \InvalidArgumentException("$path is not writable.");
|
||||
}
|
||||
|
||||
$this->path = $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExtraColumns()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function read()
|
||||
{
|
||||
$contents = $this->files->get($this->path);
|
||||
|
||||
$data = json_decode($contents, true);
|
||||
|
||||
if ($data === null) {
|
||||
throw new \RuntimeException("Invalid JSON in {$this->path}");
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function write(array $data)
|
||||
{
|
||||
if ($data) {
|
||||
$contents = json_encode($data);
|
||||
} else {
|
||||
$contents = '{}';
|
||||
}
|
||||
|
||||
$this->files->put($this->path, $contents);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Akaunting\Setting\Drivers;
|
||||
|
||||
use Akaunting\Setting\Contracts\Driver;
|
||||
|
||||
class Memory extends Driver
|
||||
{
|
||||
/**
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct(array $data = null)
|
||||
{
|
||||
if ($data) {
|
||||
$this->data = $data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExtraColumns()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function read()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function write(array $data)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Akaunting\Setting;
|
||||
|
||||
use Illuminate\Support\Facades\Facade as BaseFacade;
|
||||
|
||||
class Facade extends BaseFacade
|
||||
{
|
||||
/**
|
||||
* Get the registered name of the component.
|
||||
*/
|
||||
public static function getFacadeAccessor()
|
||||
{
|
||||
return 'setting';
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Akaunting\Setting;
|
||||
|
||||
use Akaunting\Setting\Drivers\Database;
|
||||
use Akaunting\Setting\Drivers\Json;
|
||||
use Akaunting\Setting\Drivers\Memory;
|
||||
use Illuminate\Support\Manager as BaseManager;
|
||||
|
||||
class Manager extends BaseManager
|
||||
{
|
||||
/**
|
||||
* The container instance.
|
||||
*
|
||||
* @var \Illuminate\Contracts\Container\Container
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* The application instance.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Foundation\Application $app
|
||||
*/
|
||||
public function __construct($app = null)
|
||||
{
|
||||
$this->container = $app ?? app();
|
||||
|
||||
parent::__construct($this->container);
|
||||
}
|
||||
|
||||
public function getDefaultDriver()
|
||||
{
|
||||
return config('setting.driver');
|
||||
}
|
||||
|
||||
public function createJsonDriver()
|
||||
{
|
||||
$path = config('setting.json.path');
|
||||
|
||||
return new Json($this->container['files'], $path);
|
||||
}
|
||||
|
||||
public function createDatabaseDriver()
|
||||
{
|
||||
$connection = $this->container['db']->connection(config('setting.database.connection'));
|
||||
$table = config('setting.database.table');
|
||||
$key = config('setting.database.key');
|
||||
$value = config('setting.database.value');
|
||||
$encryptedKeys = config('setting.encrypted_keys');
|
||||
|
||||
return new Database($connection, $table, $key, $value, $encryptedKeys);
|
||||
}
|
||||
|
||||
public function createMemoryDriver()
|
||||
{
|
||||
return new Memory();
|
||||
}
|
||||
|
||||
public function createArrayDriver()
|
||||
{
|
||||
return $this->createMemoryDriver();
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Akaunting\Setting\Middleware;
|
||||
|
||||
use Closure;
|
||||
|
||||
class AutoSaveSetting
|
||||
{
|
||||
/**
|
||||
* Create a new save settings middleware.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->setting = app('setting');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
$this->setting->save();
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
Vendored
-42
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateSettingsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Set up the options.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->table = config('setting.database.table');
|
||||
$this->key = config('setting.database.key');
|
||||
$this->value = config('setting.database.value');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create($this->table, function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string($this->key)->index();
|
||||
$table->text($this->value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::drop($this->table);
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Akaunting\Setting;
|
||||
|
||||
use Akaunting\Setting\Middleware\AutoSaveSetting;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\View\Compilers\BladeCompiler;
|
||||
|
||||
class Provider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->publishes([
|
||||
__DIR__ . '/Config/setting.php' => config_path('setting.php'),
|
||||
__DIR__ . '/Migrations/2017_08_24_000000_create_settings_table.php' => database_path('migrations/2017_08_24_000000_create_settings_table.php'),
|
||||
], 'setting');
|
||||
|
||||
// Auto save setting
|
||||
if (config('setting.auto_save')) {
|
||||
$kernel = $this->app['Illuminate\Contracts\Http\Kernel'];
|
||||
$kernel->pushMiddleware(AutoSaveSetting::class);
|
||||
}
|
||||
|
||||
$this->override();
|
||||
|
||||
// Register blade directive
|
||||
$this->callAfterResolving('blade.compiler', function (BladeCompiler $compiler) {
|
||||
$compiler->directive('setting', function ($expression) {
|
||||
return "<?php echo setting($expression); ?>";
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->singleton('setting.manager', function ($app) {
|
||||
return new Manager($app);
|
||||
});
|
||||
|
||||
$this->app->singleton('setting', function ($app) {
|
||||
return $app['setting.manager']->driver();
|
||||
});
|
||||
|
||||
$this->mergeConfigFrom(__DIR__ . '/Config/setting.php', 'setting');
|
||||
}
|
||||
|
||||
private function override()
|
||||
{
|
||||
$override = config('setting.override', []);
|
||||
|
||||
foreach (Arr::dot($override) as $config_key => $setting_key) {
|
||||
$config_key = is_string($config_key) ? $config_key : $setting_key;
|
||||
|
||||
try {
|
||||
if (! is_null($value = setting($setting_key))) {
|
||||
config([$config_key => $value]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Akaunting\Setting\Support;
|
||||
|
||||
class Arr
|
||||
{
|
||||
/**
|
||||
* This class is a static class and should not be instantiated.
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an element from an array.
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $key Specify a nested element by separating keys with full stops.
|
||||
* @param mixed $default If the element is not found, return this.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get(array $data, $key, $default = null)
|
||||
{
|
||||
if ($key === null) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
if (is_array($key)) {
|
||||
return static::getArray($data, $key, $default);
|
||||
}
|
||||
|
||||
foreach (explode('.', $key) as $segment) {
|
||||
if (!is_array($data)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
if (!array_key_exists($segment, $data)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$data = $data[$segment];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected static function getArray(array $input, $keys, $default = null)
|
||||
{
|
||||
$output = [];
|
||||
|
||||
foreach ($keys as $key) {
|
||||
static::set($output, $key, static::get($input, $key, $default));
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an array has a given key.
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $key
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function has(array $data, $key)
|
||||
{
|
||||
foreach (explode('.', $key) as $segment) {
|
||||
if (!is_array($data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!array_key_exists($segment, $data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = $data[$segment];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an element of an array.
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $key Specify a nested element by separating keys with full stops.
|
||||
* @param mixed $value
|
||||
*/
|
||||
public static function set(array &$data, $key, $value)
|
||||
{
|
||||
$segments = explode('.', $key);
|
||||
|
||||
$key = array_pop($segments);
|
||||
|
||||
// iterate through all of $segments except the last one
|
||||
foreach ($segments as $segment) {
|
||||
if (!array_key_exists($segment, $data)) {
|
||||
$data[$segment] = array();
|
||||
} elseif (!is_array($data[$segment])) {
|
||||
throw new \UnexpectedValueException('Non-array segment encountered');
|
||||
}
|
||||
|
||||
$data = &$data[$segment];
|
||||
}
|
||||
|
||||
$data[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset an element from an array.
|
||||
*
|
||||
* @param array &$data
|
||||
* @param string $key Specify a nested element by separating keys with full stops.
|
||||
*/
|
||||
public static function forget(array &$data, $key)
|
||||
{
|
||||
$segments = explode('.', $key);
|
||||
|
||||
$key = array_pop($segments);
|
||||
|
||||
// iterate through all of $segments except the last one
|
||||
foreach ($segments as $segment) {
|
||||
if (!array_key_exists($segment, $data)) {
|
||||
return;
|
||||
} elseif (!is_array($data[$segment])) {
|
||||
throw new \UnexpectedValueException('Non-array segment encountered');
|
||||
}
|
||||
|
||||
$data = &$data[$segment];
|
||||
}
|
||||
|
||||
unset($data[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two multidimensional arrays recursive
|
||||
*
|
||||
* @param array $array_1
|
||||
* @param array $array_2
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function merge(array $array_1, array $array_2)
|
||||
{
|
||||
$merged = $array_1;
|
||||
|
||||
foreach ($array_2 as $key => $value) {
|
||||
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
|
||||
$merged[$key] = static::merge($merged[$key], $value);
|
||||
} elseif (is_numeric($key)) {
|
||||
if (!in_array($value, $merged)) {
|
||||
$merged[] = $value;
|
||||
}
|
||||
} else {
|
||||
$merged[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $merged;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
if (!function_exists('array_keys_exists')) {
|
||||
/**
|
||||
* Easily check if multiple array keys exist.
|
||||
*
|
||||
* @param array $keys
|
||||
* @param array $arr
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
function array_keys_exists(array $keys, array $arr)
|
||||
{
|
||||
return !array_diff_key(array_flip($keys), $arr);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('setting')) {
|
||||
/**
|
||||
* Get / set the specified setting value.
|
||||
*
|
||||
* If an array is passed as the key, we will assume you want to set an array of values.
|
||||
*
|
||||
* @param array|string $key
|
||||
* @param mixed $default
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
function setting($key = null, $default = null)
|
||||
{
|
||||
$setting = app('setting');
|
||||
|
||||
if (is_null($key)) {
|
||||
return $setting;
|
||||
}
|
||||
|
||||
if (is_array($key)) {
|
||||
$setting->set($key);
|
||||
|
||||
return $setting;
|
||||
}
|
||||
|
||||
return $setting->get($key, $default);
|
||||
}
|
||||
}
|
||||
-118
@@ -1,118 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Akaunting\Setting\Drivers\Database;
|
||||
|
||||
abstract class AbstractFunctionalTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
abstract protected function createStore(array $data = []);
|
||||
|
||||
protected function assertStoreEquals($store, $expected, $message = null)
|
||||
{
|
||||
$this->assertEquals($expected, $store->all(), $message);
|
||||
$store->save();
|
||||
$store = $this->createStore();
|
||||
$this->assertEquals($expected, $store->all(), $message);
|
||||
}
|
||||
|
||||
protected function assertStoreKeyEquals($store, $key, $expected, $message = null)
|
||||
{
|
||||
$this->assertEquals($expected, $store->get($key), $message);
|
||||
$store->save();
|
||||
$store = $this->createStore();
|
||||
$this->assertEquals($expected, $store->get($key), $message);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function store_is_initially_empty()
|
||||
{
|
||||
$store = $this->createStore();
|
||||
$this->assertEquals([], $store->all());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function written_changes_are_saved()
|
||||
{
|
||||
$store = $this->createStore();
|
||||
$store->set('foo', 'bar');
|
||||
$this->assertStoreKeyEquals($store, 'foo', 'bar');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function nested_keys_are_nested()
|
||||
{
|
||||
$store = $this->createStore();
|
||||
$store->set('foo.bar', 'baz');
|
||||
$this->assertStoreEquals($store, ['foo' => ['bar' => 'baz']]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function cannot_set_nested_key_on_non_array_member()
|
||||
{
|
||||
$store = $this->createStore();
|
||||
$store->set('foo', 'bar');
|
||||
$this->setExpectedException('UnexpectedValueException', 'Non-array segment encountered');
|
||||
$store->set('foo.bar', 'baz');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function can_forget_key()
|
||||
{
|
||||
$store = $this->createStore();
|
||||
$store->set('foo', 'bar');
|
||||
$store->set('bar', 'baz');
|
||||
$this->assertStoreEquals($store, ['foo' => 'bar', 'bar' => 'baz']);
|
||||
|
||||
$store->forget('foo');
|
||||
$this->assertStoreEquals($store, ['bar' => 'baz']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function can_forget_nested_key()
|
||||
{
|
||||
$store = $this->createStore();
|
||||
$store->set('foo.bar', 'baz');
|
||||
$store->set('foo.baz', 'bar');
|
||||
$store->set('bar.foo', 'baz');
|
||||
$this->assertStoreEquals($store, [
|
||||
'foo' => [
|
||||
'bar' => 'baz',
|
||||
'baz' => 'bar',
|
||||
],
|
||||
'bar' => [
|
||||
'foo' => 'baz',
|
||||
],
|
||||
]);
|
||||
|
||||
$store->forget('foo.bar');
|
||||
$this->assertStoreEquals($store, [
|
||||
'foo' => [
|
||||
'baz' => 'bar',
|
||||
],
|
||||
'bar' => [
|
||||
'foo' => 'baz',
|
||||
],
|
||||
]);
|
||||
|
||||
$store->forget('bar.foo');
|
||||
$expected = [
|
||||
'foo' => [
|
||||
'baz' => 'bar',
|
||||
],
|
||||
'bar' => [
|
||||
],
|
||||
];
|
||||
if ($store instanceof Database) {
|
||||
unset($expected['bar']);
|
||||
}
|
||||
$this->assertStoreEquals($store, $expected);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function can_forget_all()
|
||||
{
|
||||
$store = $this->createStore(['foo' => 'bar']);
|
||||
$this->assertStoreEquals($store, ['foo' => 'bar']);
|
||||
$store->forgetAll();
|
||||
$this->assertStoreEquals($store, []);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
class DatabaseTest extends AbstractFunctionalTest
|
||||
{
|
||||
public function setUp()
|
||||
{
|
||||
$this->container = new \Illuminate\Container\Container();
|
||||
$this->capsule = new \Illuminate\Database\Capsule\Manager($this->container);
|
||||
$this->capsule->setAsGlobal();
|
||||
$this->container['db'] = $this->capsule;
|
||||
$this->capsule->addConnection([
|
||||
'driver' => 'sqlite',
|
||||
'database' => ':memory:',
|
||||
'prefix' => '',
|
||||
]);
|
||||
|
||||
$this->capsule->schema()->create('settings', function ($t) {
|
||||
$t->string('key', 64)->unique();
|
||||
$t->string('value', 4096);
|
||||
});
|
||||
}
|
||||
|
||||
public function tearDown()
|
||||
{
|
||||
$this->capsule->schema()->drop('settings');
|
||||
unset($this->capsule);
|
||||
unset($this->container);
|
||||
}
|
||||
|
||||
protected function createStore(array $data = [])
|
||||
{
|
||||
if ($data) {
|
||||
$store = $this->createStore();
|
||||
$store->set($data);
|
||||
$store->save();
|
||||
unset($store);
|
||||
}
|
||||
|
||||
return new \Akaunting\Setting\Drivers\Database(
|
||||
$this->capsule->getConnection()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
class JsonTest extends AbstractFunctionalTest
|
||||
{
|
||||
protected function createStore(array $data = null)
|
||||
{
|
||||
$path = dirname(__DIR__) . '/tmp/store.json';
|
||||
|
||||
if ($data !== null) {
|
||||
if ($data) {
|
||||
$json = json_encode($data);
|
||||
} else {
|
||||
$json = '{}';
|
||||
}
|
||||
|
||||
file_put_contents($path, $json);
|
||||
}
|
||||
|
||||
return new \Akaunting\Setting\Drivers\Json(
|
||||
new \Illuminate\Filesystem\Filesystem(),
|
||||
$path
|
||||
);
|
||||
}
|
||||
|
||||
public function tearDown()
|
||||
{
|
||||
$path = dirname(__DIR__) . '/tmp/store.json';
|
||||
unlink($path);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
class MemoryTest extends AbstractFunctionalTest
|
||||
{
|
||||
protected function assertStoreEquals($store, $expected, $message = null)
|
||||
{
|
||||
$this->assertEquals($expected, $store->all(), $message);
|
||||
// removed persistance test assertions
|
||||
}
|
||||
|
||||
protected function assertStoreKeyEquals($store, $key, $expected, $message = null)
|
||||
{
|
||||
$this->assertEquals($expected, $store->get($key), $message);
|
||||
// removed persistance test assertions
|
||||
}
|
||||
|
||||
protected function createStore(array $data = null)
|
||||
{
|
||||
return new \Akaunting\Setting\Drivers\Memory($data);
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Akaunting\Setting\Support\Arr;
|
||||
|
||||
class ArrayUtilityTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider getGetData
|
||||
*/
|
||||
public function getReturnsCorrectValue(array $data, $key, $expected)
|
||||
{
|
||||
$this->assertEquals($expected, Arr::get($data, $key));
|
||||
}
|
||||
|
||||
public function getGetData()
|
||||
{
|
||||
return [
|
||||
[[], 'foo', null],
|
||||
[['foo' => 'bar'], 'foo', 'bar'],
|
||||
[['foo' => 'bar'], 'bar', null],
|
||||
[['foo' => 'bar'], 'foo.bar', null],
|
||||
[['foo' => ['bar' => 'baz']], 'foo.bar', 'baz'],
|
||||
[['foo' => ['bar' => 'baz']], 'foo.baz', null],
|
||||
[['foo' => ['bar' => 'baz']], 'foo', ['bar' => 'baz']],
|
||||
[
|
||||
['foo' => 'bar', 'bar' => 'baz'],
|
||||
['foo', 'bar'],
|
||||
['foo' => 'bar', 'bar' => 'baz'],
|
||||
],
|
||||
[
|
||||
['foo' => ['bar' => 'baz'], 'bar' => 'baz'],
|
||||
['foo.bar', 'bar'],
|
||||
['foo' => ['bar' => 'baz'], 'bar' => 'baz'],
|
||||
],
|
||||
[
|
||||
['foo' => ['bar' => 'baz'], 'bar' => 'baz'],
|
||||
['foo.bar'],
|
||||
['foo' => ['bar' => 'baz']],
|
||||
],
|
||||
[
|
||||
['foo' => ['bar' => 'baz'], 'bar' => 'baz'],
|
||||
['foo.bar', 'baz'],
|
||||
['foo' => ['bar' => 'baz'], 'baz' => null],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider getSetData
|
||||
*/
|
||||
public function setSetsCorrectKeyToValue(array $input, $key, $value, array $expected)
|
||||
{
|
||||
Arr::set($input, $key, $value);
|
||||
$this->assertEquals($expected, $input);
|
||||
}
|
||||
|
||||
public function getSetData()
|
||||
{
|
||||
return [
|
||||
[
|
||||
['foo' => 'bar'],
|
||||
'foo',
|
||||
'baz',
|
||||
['foo' => 'baz'],
|
||||
],
|
||||
[
|
||||
[],
|
||||
'foo',
|
||||
'bar',
|
||||
['foo' => 'bar'],
|
||||
],
|
||||
[
|
||||
[],
|
||||
'foo.bar',
|
||||
'baz',
|
||||
['foo' => ['bar' => 'baz']],
|
||||
],
|
||||
[
|
||||
['foo' => ['bar' => 'baz']],
|
||||
'foo.baz',
|
||||
'foo',
|
||||
['foo' => ['bar' => 'baz', 'baz' => 'foo']],
|
||||
],
|
||||
[
|
||||
['foo' => ['bar' => 'baz']],
|
||||
'foo.baz.bar',
|
||||
'baz',
|
||||
['foo' => ['bar' => 'baz', 'baz' => ['bar' => 'baz']]],
|
||||
],
|
||||
[
|
||||
[],
|
||||
'foo.bar.baz',
|
||||
'foo',
|
||||
['foo' => ['bar' => ['baz' => 'foo']]],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function setThrowsExceptionOnNonArraySegment()
|
||||
{
|
||||
$data = ['foo' => 'bar'];
|
||||
$this->setExpectedException('UnexpectedValueException', 'Non-array segment encountered');
|
||||
Arr::set($data, 'foo.bar', 'baz');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider getHasData
|
||||
*/
|
||||
public function hasReturnsCorrectly(array $input, $key, $expected)
|
||||
{
|
||||
$this->assertEquals($expected, Arr::has($input, $key));
|
||||
}
|
||||
|
||||
public function getHasData()
|
||||
{
|
||||
return [
|
||||
[[], 'foo', false],
|
||||
[['foo' => 'bar'], 'foo', true],
|
||||
[['foo' => 'bar'], 'bar', false],
|
||||
[['foo' => 'bar'], 'foo.bar', false],
|
||||
[['foo' => ['bar' => 'baz']], 'foo.bar', true],
|
||||
[['foo' => ['bar' => 'baz']], 'foo.baz', false],
|
||||
[['foo' => ['bar' => 'baz']], 'foo', true],
|
||||
[['foo' => null], 'foo', true],
|
||||
[['foo' => ['bar' => null]], 'foo.bar', true],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Mockery as m;
|
||||
|
||||
class DatabaseDriverTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function tearDown()
|
||||
{
|
||||
m::close();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function correct_data_is_inserted_and_updated()
|
||||
{
|
||||
$connection = $this->mockConnection();
|
||||
$query = $this->mockQuery($connection);
|
||||
|
||||
$query->shouldReceive('get')->once()->andReturn([
|
||||
['key' => 'nest.one', 'value' => 'old'],
|
||||
]);
|
||||
$query->shouldReceive('lists')->atMost(1)->andReturn(['nest.one']);
|
||||
$query->shouldReceive('pluck')->atMost(1)->andReturn(['nest.one']);
|
||||
$dbData = $this->getDbData();
|
||||
unset($dbData[1]); // remove the nest.one array member
|
||||
$query->shouldReceive('where')->with('key', '=', 'nest.one')->andReturn(m::self())->getMock()
|
||||
->shouldReceive('update')->with(['value' => 'nestone']);
|
||||
$self = $this; // 5.3 compatibility
|
||||
$query->shouldReceive('insert')->once()->andReturnUsing(function ($arg) use ($dbData, $self) {
|
||||
$self->assertEquals(count($dbData), count($arg));
|
||||
foreach ($dbData as $key => $value) {
|
||||
$self->assertContains($value, $arg);
|
||||
}
|
||||
});
|
||||
|
||||
$store = $this->makeStore($connection);
|
||||
$store->set('foo', 'bar');
|
||||
$store->set('nest.one', 'nestone');
|
||||
$store->set('nest.two', 'nesttwo');
|
||||
$store->set('array', ['one', 'two']);
|
||||
$store->save();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function extra_columns_are_queried()
|
||||
{
|
||||
$connection = $this->mockConnection();
|
||||
$query = $this->mockQuery($connection);
|
||||
$query->shouldReceive('where')->once()->with('foo', '=', 'bar')
|
||||
->andReturn(m::self())->getMock()
|
||||
->shouldReceive('get')->once()->andReturn([
|
||||
['key' => 'foo', 'value' => 'bar'],
|
||||
]);
|
||||
|
||||
$store = $this->makeStore($connection);
|
||||
$store->setExtraColumns(['foo' => 'bar']);
|
||||
$this->assertEquals('bar', $store->get('foo'));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function extra_columns_are_inserted()
|
||||
{
|
||||
$connection = $this->mockConnection();
|
||||
$query = $this->mockQuery($connection);
|
||||
$query->shouldReceive('where')->times(2)->with('extracol', '=', 'extradata')
|
||||
->andReturn(m::self());
|
||||
$query->shouldReceive('get')->once()->andReturn([]);
|
||||
$query->shouldReceive('lists')->atMost(1)->andReturn([]);
|
||||
$query->shouldReceive('pluck')->atMost(1)->andReturn([]);
|
||||
$query->shouldReceive('insert')->once()->with([
|
||||
['key' => 'foo', 'value' => 'bar', 'extracol' => 'extradata'],
|
||||
]);
|
||||
|
||||
$store = $this->makeStore($connection);
|
||||
$store->setExtraColumns(['extracol' => 'extradata']);
|
||||
$store->set('foo', 'bar');
|
||||
$store->save();
|
||||
}
|
||||
|
||||
protected function getDbData()
|
||||
{
|
||||
return [
|
||||
['key' => 'foo', 'value' => 'bar'],
|
||||
['key' => 'nest.one', 'value' => 'nestone'],
|
||||
['key' => 'nest.two', 'value' => 'nesttwo'],
|
||||
['key' => 'array.0', 'value' => 'one'],
|
||||
['key' => 'array.1', 'value' => 'two'],
|
||||
];
|
||||
}
|
||||
|
||||
protected function mockConnection()
|
||||
{
|
||||
return m::mock('Illuminate\Database\Connection');
|
||||
}
|
||||
|
||||
protected function mockQuery($connection)
|
||||
{
|
||||
$query = m::mock('Illuminate\Database\Query\Builder');
|
||||
$connection->shouldReceive('table')->andReturn($query);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function makeStore($connection)
|
||||
{
|
||||
return new Akaunting\Setting\Drivers\Database($connection);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
use Mockery as m;
|
||||
|
||||
class HelperTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
public static $functions;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
self::$functions = m::mock();
|
||||
|
||||
Container::setInstance(new Container());
|
||||
|
||||
$store = m::mock('Akaunting\Setting\Contracts\Driver');
|
||||
|
||||
app()->bind('setting', function () use ($store) {
|
||||
return $store;
|
||||
});
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function helper_without_parameters_returns_store()
|
||||
{
|
||||
$this->assertInstanceOf('Akaunting\Setting\Contracts\Driver', setting());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function single_parameter_get_a_key_from_store()
|
||||
{
|
||||
app('setting')->shouldReceive('get')->with('foo', null)->once();
|
||||
|
||||
setting('foo');
|
||||
}
|
||||
|
||||
public function two_parameters_return_a_default_value()
|
||||
{
|
||||
app('setting')->shouldReceive('get')->with('foo', 'bar')->once();
|
||||
|
||||
setting('foo', 'bar');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function array_parameter_call_set_method_into_store()
|
||||
{
|
||||
app('setting')->shouldReceive('set')->with(['foo', 'bar'])->once();
|
||||
|
||||
setting(['foo', 'bar']);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Mockery as m;
|
||||
|
||||
class JsonDriverTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function tearDown()
|
||||
{
|
||||
m::close();
|
||||
}
|
||||
|
||||
protected function mockFilesystem()
|
||||
{
|
||||
return m::mock('Illuminate\Filesystem\Filesystem');
|
||||
}
|
||||
|
||||
protected function makeStore($files, $path = 'fakepath')
|
||||
{
|
||||
return new Akaunting\Setting\Drivers\Json($files, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function throws_exception_when_file_not_writeable()
|
||||
{
|
||||
$files = $this->mockFilesystem();
|
||||
$files->shouldReceive('exists')->once()->with('fakepath')->andReturn(true);
|
||||
$files->shouldReceive('isWritable')->once()->with('fakepath')->andReturn(false);
|
||||
$store = $this->makeStore($files);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function throws_exception_when_files_put_fails()
|
||||
{
|
||||
$files = $this->mockFilesystem();
|
||||
$files->shouldReceive('exists')->once()->with('fakepath')->andReturn(false);
|
||||
$files->shouldReceive('put')->once()->with('fakepath', '{}')->andReturn(false);
|
||||
$store = $this->makeStore($files);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @expectedException RuntimeException
|
||||
*/
|
||||
public function throws_exception_when_file_contains_invalid_json()
|
||||
{
|
||||
$files = $this->mockFilesystem();
|
||||
$files->shouldReceive('exists')->once()->with('fakepath')->andReturn(true);
|
||||
$files->shouldReceive('isWritable')->once()->with('fakepath')->andReturn(true);
|
||||
$files->shouldReceive('get')->once()->with('fakepath')->andReturn('[[!1!11]');
|
||||
|
||||
$store = $this->makeStore($files);
|
||||
$store->get('foo');
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
/vendor
|
||||
composer.phar
|
||||
composer.lock
|
||||
.DS_Store
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
language: php
|
||||
|
||||
dist: trusty
|
||||
|
||||
php:
|
||||
- 5.5
|
||||
- 5.6
|
||||
- 7.0
|
||||
- 7.1
|
||||
- 7.2
|
||||
|
||||
before_script:
|
||||
- travis_retry composer self-update
|
||||
- travis_retry composer install --prefer-source --no-interaction --dev
|
||||
|
||||
script:
|
||||
- composer install
|
||||
- vendor/bin/phpunit
|
||||
Vendored
-22
@@ -1,22 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Nguyễn Văn Ánh
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
-197
@@ -1,197 +0,0 @@
|
||||
No CAPTCHA reCAPTCHA
|
||||
==========
|
||||
|
||||
[](https://travis-ci.org/anhskohbo/no-captcha)
|
||||
[](https://packagist.org/packages/anhskohbo/no-captcha)
|
||||
[](https://packagist.org/packages/anhskohbo/no-captcha)
|
||||
[](https://packagist.org/packages/anhskohbo/no-captcha)
|
||||
[](https://packagist.org/packages/anhskohbo/no-captcha)
|
||||
|
||||

|
||||
|
||||
> For Laravel 4 use [v1](https://github.com/anhskohbo/no-captcha/tree/v1) branch.
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
composer require anhskohbo/no-captcha
|
||||
```
|
||||
|
||||
## Laravel 5 and above
|
||||
|
||||
### Setup
|
||||
|
||||
**_NOTE_** This package supports the auto-discovery feature of Laravel 5.5 and above, So skip these `Setup` instructions if you're using Laravel 5.5 and above.
|
||||
|
||||
In `app/config/app.php` add the following :
|
||||
|
||||
1- The ServiceProvider to the providers array :
|
||||
|
||||
```php
|
||||
Anhskohbo\NoCaptcha\NoCaptchaServiceProvider::class,
|
||||
```
|
||||
|
||||
2- The class alias to the aliases array :
|
||||
|
||||
```php
|
||||
'NoCaptcha' => Anhskohbo\NoCaptcha\Facades\NoCaptcha::class,
|
||||
```
|
||||
|
||||
3- Publish the config file
|
||||
|
||||
```ssh
|
||||
php artisan vendor:publish --provider="Anhskohbo\NoCaptcha\NoCaptchaServiceProvider"
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Add `NOCAPTCHA_SECRET` and `NOCAPTCHA_SITEKEY` in **.env** file :
|
||||
|
||||
```
|
||||
NOCAPTCHA_SECRET=secret-key
|
||||
NOCAPTCHA_SITEKEY=site-key
|
||||
```
|
||||
|
||||
(You can obtain them from [here](https://www.google.com/recaptcha/admin))
|
||||
|
||||
### Usage
|
||||
|
||||
#### Init js source
|
||||
|
||||
With default options :
|
||||
|
||||
```php
|
||||
{!! NoCaptcha::renderJs() !!}
|
||||
```
|
||||
|
||||
With [language support](https://developers.google.com/recaptcha/docs/language) or [onloadCallback](https://developers.google.com/recaptcha/docs/display#explicit_render) option :
|
||||
|
||||
```php
|
||||
{!! NoCaptcha::renderJs('fr', true, 'recaptchaCallback') !!}
|
||||
```
|
||||
|
||||
#### Display reCAPTCHA
|
||||
|
||||
Default widget :
|
||||
|
||||
```php
|
||||
{!! NoCaptcha::display() !!}
|
||||
```
|
||||
|
||||
With [custom attributes](https://developers.google.com/recaptcha/docs/display#render_param) (theme, size, callback ...) :
|
||||
|
||||
```php
|
||||
{!! NoCaptcha::display(['data-theme' => 'dark']) !!}
|
||||
```
|
||||
|
||||
Invisible reCAPTCHA using a [submit button](https://developers.google.com/recaptcha/docs/invisible):
|
||||
|
||||
```php
|
||||
{!! NoCaptcha::displaySubmit('my-form-id', 'submit now!', ['data-theme' => 'dark']) !!}
|
||||
```
|
||||
Notice that the id of the form is required in this method to let the autogenerated
|
||||
callback submit the form on a successful captcha verification.
|
||||
|
||||
#### Validation
|
||||
|
||||
Add `'g-recaptcha-response' => 'required|captcha'` to rules array :
|
||||
|
||||
```php
|
||||
$validate = Validator::make(Input::all(), [
|
||||
'g-recaptcha-response' => 'required|captcha'
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
##### Custom Validation Message
|
||||
|
||||
Add the following values to the `custom` array in the `validation` language file :
|
||||
|
||||
```php
|
||||
'custom' => [
|
||||
'g-recaptcha-response' => [
|
||||
'required' => 'Please verify that you are not a robot.',
|
||||
'captcha' => 'Captcha error! try again later or contact site admin.',
|
||||
],
|
||||
],
|
||||
```
|
||||
|
||||
Then check for captcha errors in the `Form` :
|
||||
|
||||
```php
|
||||
@if ($errors->has('g-recaptcha-response'))
|
||||
<span class="help-block">
|
||||
<strong>{{ $errors->first('g-recaptcha-response') }}</strong>
|
||||
</span>
|
||||
@endif
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
When using the [Laravel Testing functionality](http://laravel.com/docs/5.5/testing), you will need to mock out the response for the captcha form element.
|
||||
|
||||
So for any form tests involving the captcha, you can do this by mocking the facade behavior:
|
||||
|
||||
```php
|
||||
// prevent validation error on captcha
|
||||
NoCaptcha::shouldReceive('verifyResponse')
|
||||
->once()
|
||||
->andReturn(true);
|
||||
|
||||
// provide hidden input for your 'required' validation
|
||||
NoCaptcha::shouldReceive('display')
|
||||
->zeroOrMoreTimes()
|
||||
->andReturn('<input type="hidden" name="g-recaptcha-response" value="1" />');
|
||||
```
|
||||
|
||||
You can then test the remainder of your form as normal.
|
||||
|
||||
When using HTTP tests you can add the `g-recaptcha-response` to the request body for the 'required' validation:
|
||||
|
||||
```php
|
||||
// prevent validation error on captcha
|
||||
NoCaptcha::shouldReceive('verifyResponse')
|
||||
->once()
|
||||
->andReturn(true);
|
||||
|
||||
// POST request, with request body including g-recaptcha-response
|
||||
$response = $this->json('POST', '/register', [
|
||||
'g-recaptcha-response' => '1',
|
||||
'name' => 'John',
|
||||
'email' => 'john@example.com',
|
||||
'password' => '123456',
|
||||
'password_confirmation' => '123456',
|
||||
]);
|
||||
```
|
||||
|
||||
## Without Laravel
|
||||
|
||||
Checkout example below:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
require_once "vendor/autoload.php";
|
||||
|
||||
$secret = 'CAPTCHA-SECRET';
|
||||
$sitekey = 'CAPTCHA-SITEKEY';
|
||||
$captcha = new \Anhskohbo\NoCaptcha\NoCaptcha($secret, $sitekey);
|
||||
|
||||
if (! empty($_POST)) {
|
||||
var_dump($captcha->verifyResponse($_POST['g-recaptcha-response']));
|
||||
exit();
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<form action="?" method="POST">
|
||||
<?php echo $captcha->display(); ?>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
<?php echo $captcha->renderJs(); ?>
|
||||
```
|
||||
|
||||
## Contribute
|
||||
|
||||
https://github.com/anhskohbo/no-captcha/pulls
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"name": "anhskohbo/no-captcha",
|
||||
"description": "No CAPTCHA reCAPTCHA For Laravel.",
|
||||
"keywords": [
|
||||
"recaptcha",
|
||||
"no-captcha",
|
||||
"captcha",
|
||||
"laravel",
|
||||
"laravel4",
|
||||
"laravel5",
|
||||
"laravel6"
|
||||
],
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "anhskohbo",
|
||||
"email": "anhskohbo@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.5.5",
|
||||
"illuminate/support": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
|
||||
"guzzlehttp/guzzle": "^6.2|^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.8|^9.5.10|^10.5"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Anhskohbo\\NoCaptcha\\": "src/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Anhskohbo\\NoCaptcha\\NoCaptchaServiceProvider"
|
||||
],
|
||||
"aliases": {
|
||||
"NoCaptcha": "Anhskohbo\\NoCaptcha\\Facades\\NoCaptcha"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit backupGlobals="false"
|
||||
backupStaticAttributes="false"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false"
|
||||
syntaxCheck="false"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Package Test Suite">
|
||||
<directory suffix=".php">./tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Anhskohbo\NoCaptcha\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class NoCaptcha extends Facade
|
||||
{
|
||||
/**
|
||||
* Get the registered name of the component.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'captcha';
|
||||
}
|
||||
}
|
||||
-246
@@ -1,246 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Anhskohbo\NoCaptcha;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use GuzzleHttp\Client;
|
||||
|
||||
class NoCaptcha
|
||||
{
|
||||
const CLIENT_API = 'https://www.google.com/recaptcha/api.js';
|
||||
const VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify';
|
||||
|
||||
/**
|
||||
* The recaptcha secret key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $secret;
|
||||
|
||||
/**
|
||||
* The recaptcha sitekey key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $sitekey;
|
||||
|
||||
/**
|
||||
* @var \GuzzleHttp\Client
|
||||
*/
|
||||
protected $http;
|
||||
|
||||
/**
|
||||
* The cached verified responses.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $verifiedResponses = [];
|
||||
|
||||
/**
|
||||
* NoCaptcha.
|
||||
*
|
||||
* @param string $secret
|
||||
* @param string $sitekey
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct($secret, $sitekey, $options = [])
|
||||
{
|
||||
$this->secret = $secret;
|
||||
$this->sitekey = $sitekey;
|
||||
$this->http = new Client($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render HTML captcha.
|
||||
*
|
||||
* @param array $attributes
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function display($attributes = [])
|
||||
{
|
||||
$attributes = $this->prepareAttributes($attributes);
|
||||
return '<div' . $this->buildAttributes($attributes) . '></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* @see display()
|
||||
*/
|
||||
public function displayWidget($attributes = [])
|
||||
{
|
||||
return $this->display($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a Invisible reCAPTCHA by embedding a callback into a form submit button.
|
||||
*
|
||||
* @param string $formIdentifier the html ID of the form that should be submitted.
|
||||
* @param string $text the text inside the form button
|
||||
* @param array $attributes array of additional html elements
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function displaySubmit($formIdentifier, $text = 'submit', $attributes = [])
|
||||
{
|
||||
$javascript = '';
|
||||
if (!isset($attributes['data-callback'])) {
|
||||
$functionName = 'onSubmit' . str_replace(['-', '=', '\'', '"', '<', '>', '`'], '', $formIdentifier);
|
||||
$attributes['data-callback'] = $functionName;
|
||||
$javascript = sprintf(
|
||||
'<script>function %s(){document.getElementById("%s").submit();}</script>',
|
||||
$functionName,
|
||||
$formIdentifier
|
||||
);
|
||||
}
|
||||
|
||||
$attributes = $this->prepareAttributes($attributes);
|
||||
|
||||
$button = sprintf('<button%s><span>%s</span></button>', $this->buildAttributes($attributes), $text);
|
||||
|
||||
return $button . $javascript;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render js source
|
||||
*
|
||||
* @param null $lang
|
||||
* @param bool $callback
|
||||
* @param string $onLoadClass
|
||||
* @return string
|
||||
*/
|
||||
public function renderJs($lang = null, $callback = false, $onLoadClass = 'onloadCallBack')
|
||||
{
|
||||
return '<script src="'.$this->getJsLink($lang, $callback, $onLoadClass).'" async defer></script>'."\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify no-captcha response.
|
||||
*
|
||||
* @param string $response
|
||||
* @param string $clientIp
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function verifyResponse($response, $clientIp = null)
|
||||
{
|
||||
if (empty($response)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Return true if response already verfied before.
|
||||
if (in_array($response, $this->verifiedResponses)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$verifyResponse = $this->sendRequestVerify([
|
||||
'secret' => $this->secret,
|
||||
'response' => $response,
|
||||
'remoteip' => $clientIp,
|
||||
]);
|
||||
|
||||
if (isset($verifyResponse['success']) && $verifyResponse['success'] === true) {
|
||||
// A response can only be verified once from google, so we need to
|
||||
// cache it to make it work in case we want to verify it multiple times.
|
||||
$this->verifiedResponses[] = $response;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify no-captcha response by Symfony Request.
|
||||
*
|
||||
* @param Request $request
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function verifyRequest(Request $request)
|
||||
{
|
||||
return $this->verifyResponse(
|
||||
$request->get('g-recaptcha-response'),
|
||||
$request->getClientIp()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recaptcha js link.
|
||||
*
|
||||
* @param string $lang
|
||||
* @param boolean $callback
|
||||
* @param string $onLoadClass
|
||||
* @return string
|
||||
*/
|
||||
public function getJsLink($lang = null, $callback = false, $onLoadClass = 'onloadCallBack')
|
||||
{
|
||||
$client_api = static::CLIENT_API;
|
||||
$params = [];
|
||||
|
||||
$callback ? $this->setCallBackParams($params, $onLoadClass) : false;
|
||||
$lang ? $params['hl'] = $lang : null;
|
||||
|
||||
return $client_api . '?'. http_build_query($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $params
|
||||
* @param $onLoadClass
|
||||
*/
|
||||
protected function setCallBackParams(&$params, $onLoadClass)
|
||||
{
|
||||
$params['render'] = 'explicit';
|
||||
$params['onload'] = $onLoadClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send verify request.
|
||||
*
|
||||
* @param array $query
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function sendRequestVerify(array $query = [])
|
||||
{
|
||||
$response = $this->http->request('POST', static::VERIFY_URL, [
|
||||
'form_params' => $query,
|
||||
]);
|
||||
|
||||
return json_decode($response->getBody(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare HTML attributes and assure that the correct classes and attributes for captcha are inserted.
|
||||
*
|
||||
* @param array $attributes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function prepareAttributes(array $attributes)
|
||||
{
|
||||
$attributes['data-sitekey'] = $this->sitekey;
|
||||
if (!isset($attributes['class'])) {
|
||||
$attributes['class'] = '';
|
||||
}
|
||||
$attributes['class'] = trim('g-recaptcha ' . $attributes['class']);
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build HTML attributes.
|
||||
*
|
||||
* @param array $attributes
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function buildAttributes(array $attributes)
|
||||
{
|
||||
$html = [];
|
||||
|
||||
foreach ($attributes as $key => $value) {
|
||||
$html[] = $key.'="'.$value.'"';
|
||||
}
|
||||
|
||||
return count($html) ? ' '.implode(' ', $html) : '';
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Anhskohbo\NoCaptcha;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class NoCaptchaServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Indicates if loading of the provider is deferred.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $defer = false;
|
||||
|
||||
/**
|
||||
* Bootstrap the application events.
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$app = $this->app;
|
||||
|
||||
$this->bootConfig();
|
||||
|
||||
$app['validator']->extend('captcha', function ($attribute, $value) use ($app) {
|
||||
return $app['captcha']->verifyResponse($value, $app['request']->getClientIp());
|
||||
});
|
||||
|
||||
if ($app->bound('form')) {
|
||||
$app['form']->macro('captcha', function ($attributes = []) use ($app) {
|
||||
return $app['captcha']->display($attributes, $app->getLocale());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Booting configure.
|
||||
*/
|
||||
protected function bootConfig()
|
||||
{
|
||||
$path = __DIR__.'/config/captcha.php';
|
||||
|
||||
$this->mergeConfigFrom($path, 'captcha');
|
||||
|
||||
if (function_exists('config_path')) {
|
||||
$this->publishes([$path => config_path('captcha.php')]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the service provider.
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->singleton('captcha', function ($app) {
|
||||
return new NoCaptcha(
|
||||
$app['config']['captcha.secret'],
|
||||
$app['config']['captcha.sitekey'],
|
||||
$app['config']['captcha.options']
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function provides()
|
||||
{
|
||||
return ['captcha'];
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'secret' => env('NOCAPTCHA_SECRET'),
|
||||
'sitekey' => env('NOCAPTCHA_SITEKEY'),
|
||||
'options' => [
|
||||
'timeout' => 30,
|
||||
],
|
||||
];
|
||||
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Anhskohbo\NoCaptcha\NoCaptcha;
|
||||
|
||||
class NoCaptchaTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* @var NoCaptcha
|
||||
*/
|
||||
private $captcha;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
$this->captcha = new NoCaptcha('{secret-key}', '{site-key}');
|
||||
}
|
||||
|
||||
public function testRequestShouldWorks()
|
||||
{
|
||||
$response = $this->captcha->verifyResponse('should_false');
|
||||
}
|
||||
|
||||
public function testJsLink()
|
||||
{
|
||||
$this->assertTrue($this->captcha instanceof NoCaptcha);
|
||||
|
||||
$simple = '<script src="https://www.google.com/recaptcha/api.js?" async defer></script>'."\n";
|
||||
$withLang = '<script src="https://www.google.com/recaptcha/api.js?hl=vi" async defer></script>'."\n";
|
||||
$withCallback = '<script src="https://www.google.com/recaptcha/api.js?render=explicit&onload=myOnloadCallback" async defer></script>'."\n";
|
||||
|
||||
$this->assertEquals($simple, $this->captcha->renderJs());
|
||||
$this->assertEquals($withLang, $this->captcha->renderJs('vi'));
|
||||
$this->assertEquals($withCallback, $this->captcha->renderJs(null, true, 'myOnloadCallback'));
|
||||
}
|
||||
|
||||
public function testDisplay()
|
||||
{
|
||||
$this->assertTrue($this->captcha instanceof NoCaptcha);
|
||||
|
||||
$simple = '<div data-sitekey="{site-key}" class="g-recaptcha"></div>';
|
||||
$withAttrs = '<div data-theme="light" data-sitekey="{site-key}" class="g-recaptcha"></div>';
|
||||
|
||||
$this->assertEquals($simple, $this->captcha->display());
|
||||
$this->assertEquals($withAttrs, $this->captcha->display(['data-theme' => 'light']));
|
||||
}
|
||||
|
||||
public function testdisplaySubmit()
|
||||
{
|
||||
$this->assertTrue($this->captcha instanceof NoCaptcha);
|
||||
|
||||
$javascript = '<script>function onSubmittest(){document.getElementById("test").submit();}</script>';
|
||||
$simple = '<button data-callback="onSubmittest" data-sitekey="{site-key}" class="g-recaptcha"><span>submit</span></button>';
|
||||
$withAttrs = '<button data-theme="light" class="g-recaptcha 123" data-callback="onSubmittest" data-sitekey="{site-key}"><span>submit123</span></button>';
|
||||
|
||||
$this->assertEquals($simple . $javascript, $this->captcha->displaySubmit('test'));
|
||||
$withAttrsResult = $this->captcha->displaySubmit('test','submit123',['data-theme' => 'light', 'class' => '123']);
|
||||
$this->assertEquals($withAttrs . $javascript, $withAttrsResult);
|
||||
}
|
||||
|
||||
public function testdisplaySubmitWithCustomCallback()
|
||||
{
|
||||
$this->assertTrue($this->captcha instanceof NoCaptcha);
|
||||
|
||||
$withAttrs = '<button data-theme="light" class="g-recaptcha 123" data-callback="onSubmitCustomCallback" data-sitekey="{site-key}"><span>submit123</span></button>';
|
||||
|
||||
$withAttrsResult = $this->captcha->displaySubmit('test-custom','submit123',['data-theme' => 'light', 'class' => '123', 'data-callback' => 'onSubmitCustomCallback']);
|
||||
$this->assertEquals($withAttrs, $withAttrsResult);
|
||||
}
|
||||
}
|
||||
Vendored
-25
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
// autoload.php @generated by Composer
|
||||
|
||||
if (PHP_VERSION_ID < 50600) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, $err);
|
||||
} elseif (!headers_sent()) {
|
||||
echo $err;
|
||||
}
|
||||
}
|
||||
trigger_error(
|
||||
$err,
|
||||
E_USER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/composer/autoload_real.php';
|
||||
|
||||
return ComposerAutoloaderInitc91cd9c5b1e6a9e8573a14b799ea9342::getLoader();
|
||||
Vendored
-22
@@ -1,22 +0,0 @@
|
||||
Copyright (c) 2017, Ben Scholzen 'DASPRiD'
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
Vendored
-57
@@ -1,57 +0,0 @@
|
||||
# QR Code generator
|
||||
|
||||
[](https://github.com/Bacon/BaconQrCode/actions/workflows/ci.yml)
|
||||
[](https://codecov.io/gh/Bacon/BaconQrCode)
|
||||
[](https://packagist.org/packages/bacon/bacon-qr-code)
|
||||
[](https://packagist.org/packages/bacon/bacon-qr-code)
|
||||
[](https://packagist.org/packages/bacon/bacon-qr-code)
|
||||
|
||||
|
||||
## Introduction
|
||||
BaconQrCode is a port of QR code portion of the ZXing library. It currently
|
||||
only features the encoder part, but could later receive the decoder part as
|
||||
well.
|
||||
|
||||
As the Reed Solomon codec implementation of the ZXing library performs quite
|
||||
slow in PHP, it was exchanged with the implementation by Phil Karn.
|
||||
|
||||
|
||||
## Example usage
|
||||
```php
|
||||
use BaconQrCode\Renderer\ImageRenderer;
|
||||
use BaconQrCode\Renderer\Image\ImagickImageBackEnd;
|
||||
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
|
||||
use BaconQrCode\Writer;
|
||||
|
||||
$renderer = new ImageRenderer(
|
||||
new RendererStyle(400),
|
||||
new ImagickImageBackEnd()
|
||||
);
|
||||
$writer = new Writer($renderer);
|
||||
$writer->writeFile('Hello World!', 'qrcode.png');
|
||||
```
|
||||
|
||||
## Available image renderer back ends
|
||||
BaconQrCode comes with multiple back ends for rendering images. Currently included are the following:
|
||||
|
||||
- `ImagickImageBackEnd`: renders raster images using the Imagick library
|
||||
- `SvgImageBackEnd`: renders SVG files using XMLWriter
|
||||
- `EpsImageBackEnd`: renders EPS files
|
||||
|
||||
### GDLib Renderer
|
||||
GD library has so many limitations, that GD support is not added as backend, but as separated renderer.
|
||||
Use `GDLibRenderer` instead of `ImageRenderer`. These are the limitations:
|
||||
|
||||
- Does not support gradient.
|
||||
- Does not support any curves, so you QR code is always squared.
|
||||
|
||||
Example usage:
|
||||
|
||||
```php
|
||||
use BaconQrCode\Renderer\GDLibRenderer;
|
||||
use BaconQrCode\Writer;
|
||||
|
||||
$renderer = new GDLibRenderer(400);
|
||||
$writer = new Writer($renderer);
|
||||
$writer->writeFile('Hello World!', 'qrcode.png');
|
||||
```
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
"description": "BaconQrCode is a QR code generator for PHP.",
|
||||
"license": "BSD-2-Clause",
|
||||
"homepage": "https://github.com/Bacon/BaconQrCode",
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"ext-iconv": "*",
|
||||
"dasprid/enum": "^1.0.3"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-imagick": "to generate QR code images"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ben Scholzen 'DASPRiD'",
|
||||
"email": "mail@dasprids.de",
|
||||
"homepage": "https://dasprids.de/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"BaconQrCode\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"BaconQrCodeTest\\": "test/"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.5.11 || 11.0.4",
|
||||
"spatie/phpunit-snapshot-assertions": "^5.1.5",
|
||||
"squizlabs/php_codesniffer": "^3.9",
|
||||
"phly/keep-a-changelog": "^2.12"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"ocramius/package-versions": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"archive": {
|
||||
"exclude": [
|
||||
"/test",
|
||||
"/phpunit.xml.dist"
|
||||
]
|
||||
}
|
||||
}
|
||||
-364
@@ -1,364 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
use BaconQrCode\Exception\InvalidArgumentException;
|
||||
use SplFixedArray;
|
||||
|
||||
/**
|
||||
* A simple, fast array of bits.
|
||||
*/
|
||||
final class BitArray
|
||||
{
|
||||
/**
|
||||
* Bits represented as an array of integers.
|
||||
*
|
||||
* @var SplFixedArray<int>
|
||||
*/
|
||||
private SplFixedArray $bits;
|
||||
|
||||
/**
|
||||
* Creates a new bit array with a given size.
|
||||
*/
|
||||
public function __construct(private int $size = 0)
|
||||
{
|
||||
$this->bits = SplFixedArray::fromArray(array_fill(0, ($this->size + 31) >> 3, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the size in bits.
|
||||
*/
|
||||
public function getSize() : int
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the size in bytes.
|
||||
*/
|
||||
public function getSizeInBytes() : int
|
||||
{
|
||||
return ($this->size + 7) >> 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the array has a minimum capacity.
|
||||
*/
|
||||
public function ensureCapacity(int $size) : void
|
||||
{
|
||||
if ($size > count($this->bits) << 5) {
|
||||
$this->bits->setSize(($size + 31) >> 5);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific bit.
|
||||
*/
|
||||
public function get(int $i) : bool
|
||||
{
|
||||
return 0 !== ($this->bits[$i >> 5] & (1 << ($i & 0x1f)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a specific bit.
|
||||
*/
|
||||
public function set(int $i) : void
|
||||
{
|
||||
$this->bits[$i >> 5] = $this->bits[$i >> 5] | 1 << ($i & 0x1f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flips a specific bit.
|
||||
*/
|
||||
public function flip(int $i) : void
|
||||
{
|
||||
$this->bits[$i >> 5] ^= 1 << ($i & 0x1f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next set bit position from a given position.
|
||||
*/
|
||||
public function getNextSet(int $from) : int
|
||||
{
|
||||
if ($from >= $this->size) {
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
$bitsOffset = $from >> 5;
|
||||
$currentBits = $this->bits[$bitsOffset];
|
||||
$bitsLength = count($this->bits);
|
||||
$currentBits &= ~((1 << ($from & 0x1f)) - 1);
|
||||
|
||||
while (0 === $currentBits) {
|
||||
if (++$bitsOffset === $bitsLength) {
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
$currentBits = $this->bits[$bitsOffset];
|
||||
}
|
||||
|
||||
$result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
|
||||
return min($result, $this->size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next unset bit position from a given position.
|
||||
*/
|
||||
public function getNextUnset(int $from) : int
|
||||
{
|
||||
if ($from >= $this->size) {
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
$bitsOffset = $from >> 5;
|
||||
$currentBits = ~$this->bits[$bitsOffset];
|
||||
$bitsLength = count($this->bits);
|
||||
$currentBits &= ~((1 << ($from & 0x1f)) - 1);
|
||||
|
||||
while (0 === $currentBits) {
|
||||
if (++$bitsOffset === $bitsLength) {
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
$currentBits = ~$this->bits[$bitsOffset];
|
||||
}
|
||||
|
||||
$result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
|
||||
return min($result, $this->size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a bulk of bits.
|
||||
*/
|
||||
public function setBulk(int $i, int $newBits) : void
|
||||
{
|
||||
$this->bits[$i >> 5] = $newBits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a range of bits.
|
||||
*
|
||||
* @throws InvalidArgumentException if end is smaller than start
|
||||
*/
|
||||
public function setRange(int $start, int $end) : void
|
||||
{
|
||||
if ($end < $start) {
|
||||
throw new InvalidArgumentException('End must be greater or equal to start');
|
||||
}
|
||||
|
||||
if ($end === $start) {
|
||||
return;
|
||||
}
|
||||
|
||||
--$end;
|
||||
|
||||
$firstInt = $start >> 5;
|
||||
$lastInt = $end >> 5;
|
||||
|
||||
for ($i = $firstInt; $i <= $lastInt; ++$i) {
|
||||
$firstBit = $i > $firstInt ? 0 : $start & 0x1f;
|
||||
$lastBit = $i < $lastInt ? 31 : $end & 0x1f;
|
||||
|
||||
if (0 === $firstBit && 31 === $lastBit) {
|
||||
$mask = 0x7fffffff;
|
||||
} else {
|
||||
$mask = 0;
|
||||
|
||||
for ($j = $firstBit; $j < $lastBit; ++$j) {
|
||||
$mask |= 1 << $j;
|
||||
}
|
||||
}
|
||||
|
||||
$this->bits[$i] = $this->bits[$i] | $mask;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the bit array, unsetting every bit.
|
||||
*/
|
||||
public function clear() : void
|
||||
{
|
||||
$bitsLength = count($this->bits);
|
||||
|
||||
for ($i = 0; $i < $bitsLength; ++$i) {
|
||||
$this->bits[$i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a range of bits is set or not set.
|
||||
|
||||
* @throws InvalidArgumentException if end is smaller than start
|
||||
*/
|
||||
public function isRange(int $start, int $end, bool $value) : bool
|
||||
{
|
||||
if ($end < $start) {
|
||||
throw new InvalidArgumentException('End must be greater or equal to start');
|
||||
}
|
||||
|
||||
if ($end === $start) {
|
||||
return true;
|
||||
}
|
||||
|
||||
--$end;
|
||||
|
||||
$firstInt = $start >> 5;
|
||||
$lastInt = $end >> 5;
|
||||
|
||||
for ($i = $firstInt; $i <= $lastInt; ++$i) {
|
||||
$firstBit = $i > $firstInt ? 0 : $start & 0x1f;
|
||||
$lastBit = $i < $lastInt ? 31 : $end & 0x1f;
|
||||
|
||||
if (0 === $firstBit && 31 === $lastBit) {
|
||||
$mask = 0x7fffffff;
|
||||
} else {
|
||||
$mask = 0;
|
||||
|
||||
for ($j = $firstBit; $j <= $lastBit; ++$j) {
|
||||
$mask |= 1 << $j;
|
||||
}
|
||||
}
|
||||
|
||||
if (($this->bits[$i] & $mask) !== ($value ? $mask : 0)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a bit to the array.
|
||||
*/
|
||||
public function appendBit(bool $bit) : void
|
||||
{
|
||||
$this->ensureCapacity($this->size + 1);
|
||||
|
||||
if ($bit) {
|
||||
$this->bits[$this->size >> 5] = $this->bits[$this->size >> 5] | (1 << ($this->size & 0x1f));
|
||||
}
|
||||
|
||||
++$this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a number of bits (up to 32) to the array.
|
||||
|
||||
* @throws InvalidArgumentException if num bits is not between 0 and 32
|
||||
*/
|
||||
public function appendBits(int $value, int $numBits) : void
|
||||
{
|
||||
if ($numBits < 0 || $numBits > 32) {
|
||||
throw new InvalidArgumentException('Num bits must be between 0 and 32');
|
||||
}
|
||||
|
||||
$this->ensureCapacity($this->size + $numBits);
|
||||
|
||||
for ($numBitsLeft = $numBits; $numBitsLeft > 0; $numBitsLeft--) {
|
||||
$this->appendBit((($value >> ($numBitsLeft - 1)) & 0x01) === 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends another bit array to this array.
|
||||
*/
|
||||
public function appendBitArray(self $other) : void
|
||||
{
|
||||
$otherSize = $other->getSize();
|
||||
$this->ensureCapacity($this->size + $other->getSize());
|
||||
|
||||
for ($i = 0; $i < $otherSize; ++$i) {
|
||||
$this->appendBit($other->get($i));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes an exclusive-or comparision on the current bit array.
|
||||
*
|
||||
* @throws InvalidArgumentException if sizes don't match
|
||||
*/
|
||||
public function xorBits(self $other) : void
|
||||
{
|
||||
$bitsLength = count($this->bits);
|
||||
$otherBits = $other->getBitArray();
|
||||
|
||||
if ($bitsLength !== count($otherBits)) {
|
||||
throw new InvalidArgumentException('Sizes don\'t match');
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $bitsLength; ++$i) {
|
||||
$this->bits[$i] = $this->bits[$i] ^ $otherBits[$i];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the bit array to a byte array.
|
||||
*
|
||||
* @return SplFixedArray<int>
|
||||
*/
|
||||
public function toBytes(int $bitOffset, int $numBytes) : SplFixedArray
|
||||
{
|
||||
$bytes = new SplFixedArray($numBytes);
|
||||
|
||||
for ($i = 0; $i < $numBytes; ++$i) {
|
||||
$byte = 0;
|
||||
|
||||
for ($j = 0; $j < 8; ++$j) {
|
||||
if ($this->get($bitOffset)) {
|
||||
$byte |= 1 << (7 - $j);
|
||||
}
|
||||
|
||||
++$bitOffset;
|
||||
}
|
||||
|
||||
$bytes[$i] = $byte;
|
||||
}
|
||||
|
||||
return $bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the internal bit array.
|
||||
*
|
||||
* @return SplFixedArray<int>
|
||||
*/
|
||||
public function getBitArray() : SplFixedArray
|
||||
{
|
||||
return $this->bits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses the array.
|
||||
*/
|
||||
public function reverse() : void
|
||||
{
|
||||
$newBits = new SplFixedArray(count($this->bits));
|
||||
|
||||
for ($i = 0; $i < $this->size; ++$i) {
|
||||
if ($this->get($this->size - $i - 1)) {
|
||||
$newBits[$i >> 5] = $newBits[$i >> 5] | (1 << ($i & 0x1f));
|
||||
}
|
||||
}
|
||||
|
||||
$this->bits = $newBits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the bit array.
|
||||
*/
|
||||
public function __toString() : string
|
||||
{
|
||||
$result = '';
|
||||
|
||||
for ($i = 0; $i < $this->size; ++$i) {
|
||||
if (0 === ($i & 0x07)) {
|
||||
$result .= ' ';
|
||||
}
|
||||
|
||||
$result .= $this->get($i) ? 'X' : '.';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
-307
@@ -1,307 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
use BaconQrCode\Exception\InvalidArgumentException;
|
||||
use SplFixedArray;
|
||||
|
||||
/**
|
||||
* Bit matrix.
|
||||
*
|
||||
* Represents a 2D matrix of bits. In function arguments below, and throughout
|
||||
* the common module, x is the column position, and y is the row position. The
|
||||
* ordering is always x, y. The origin is at the top-left.
|
||||
*/
|
||||
class BitMatrix
|
||||
{
|
||||
/**
|
||||
* Width of the bit matrix.
|
||||
*/
|
||||
private int $width;
|
||||
|
||||
/**
|
||||
* Height of the bit matrix.
|
||||
*/
|
||||
private ?int $height;
|
||||
|
||||
/**
|
||||
* Size in bits of each individual row.
|
||||
*/
|
||||
private int $rowSize;
|
||||
|
||||
/**
|
||||
* Bits representation.
|
||||
*
|
||||
* @var SplFixedArray<int>
|
||||
*/
|
||||
private SplFixedArray $bits;
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if a dimension is smaller than zero
|
||||
*/
|
||||
public function __construct(int $width, ?int $height = null)
|
||||
{
|
||||
if (null === $height) {
|
||||
$height = $width;
|
||||
}
|
||||
|
||||
if ($width < 1 || $height < 1) {
|
||||
throw new InvalidArgumentException('Both dimensions must be greater than zero');
|
||||
}
|
||||
|
||||
$this->width = $width;
|
||||
$this->height = $height;
|
||||
$this->rowSize = ($width + 31) >> 5;
|
||||
$this->bits = SplFixedArray::fromArray(array_fill(0, $this->rowSize * $height, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the requested bit, where true means black.
|
||||
*/
|
||||
public function get(int $x, int $y) : bool
|
||||
{
|
||||
$offset = $y * $this->rowSize + ($x >> 5);
|
||||
return 0 !== (BitUtils::unsignedRightShift($this->bits[$offset], ($x & 0x1f)) & 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the given bit to true.
|
||||
*/
|
||||
public function set(int $x, int $y) : void
|
||||
{
|
||||
$offset = $y * $this->rowSize + ($x >> 5);
|
||||
$this->bits[$offset] = $this->bits[$offset] | (1 << ($x & 0x1f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Flips the given bit.
|
||||
*/
|
||||
public function flip(int $x, int $y) : void
|
||||
{
|
||||
$offset = $y * $this->rowSize + ($x >> 5);
|
||||
$this->bits[$offset] = $this->bits[$offset] ^ (1 << ($x & 0x1f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all bits (set to false).
|
||||
*/
|
||||
public function clear() : void
|
||||
{
|
||||
$max = count($this->bits);
|
||||
|
||||
for ($i = 0; $i < $max; ++$i) {
|
||||
$this->bits[$i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a square region of the bit matrix to true.
|
||||
*
|
||||
* @throws InvalidArgumentException if left or top are negative
|
||||
* @throws InvalidArgumentException if width or height are smaller than 1
|
||||
* @throws InvalidArgumentException if region does not fit into the matix
|
||||
*/
|
||||
public function setRegion(int $left, int $top, int $width, int $height) : void
|
||||
{
|
||||
if ($top < 0 || $left < 0) {
|
||||
throw new InvalidArgumentException('Left and top must be non-negative');
|
||||
}
|
||||
|
||||
if ($height < 1 || $width < 1) {
|
||||
throw new InvalidArgumentException('Width and height must be at least 1');
|
||||
}
|
||||
|
||||
$right = $left + $width;
|
||||
$bottom = $top + $height;
|
||||
|
||||
if ($bottom > $this->height || $right > $this->width) {
|
||||
throw new InvalidArgumentException('The region must fit inside the matrix');
|
||||
}
|
||||
|
||||
for ($y = $top; $y < $bottom; ++$y) {
|
||||
$offset = $y * $this->rowSize;
|
||||
|
||||
for ($x = $left; $x < $right; ++$x) {
|
||||
$index = $offset + ($x >> 5);
|
||||
$this->bits[$index] = $this->bits[$index] | (1 << ($x & 0x1f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A fast method to retrieve one row of data from the matrix as a BitArray.
|
||||
*/
|
||||
public function getRow(int $y, ?BitArray $row = null) : BitArray
|
||||
{
|
||||
if (null === $row || $row->getSize() < $this->width) {
|
||||
$row = new BitArray($this->width);
|
||||
}
|
||||
|
||||
$offset = $y * $this->rowSize;
|
||||
|
||||
for ($x = 0; $x < $this->rowSize; ++$x) {
|
||||
$row->setBulk($x << 5, $this->bits[$offset + $x]);
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a row of data from a BitArray.
|
||||
*/
|
||||
public function setRow(int $y, BitArray $row) : void
|
||||
{
|
||||
$bits = $row->getBitArray();
|
||||
|
||||
for ($i = 0; $i < $this->rowSize; ++$i) {
|
||||
$this->bits[$y * $this->rowSize + $i] = $bits[$i];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is useful in detecting the enclosing rectangle of a 'pure' barcode.
|
||||
*
|
||||
* @return int[]|null
|
||||
*/
|
||||
public function getEnclosingRectangle() : ?array
|
||||
{
|
||||
$left = $this->width;
|
||||
$top = $this->height;
|
||||
$right = -1;
|
||||
$bottom = -1;
|
||||
|
||||
for ($y = 0; $y < $this->height; ++$y) {
|
||||
for ($x32 = 0; $x32 < $this->rowSize; ++$x32) {
|
||||
$bits = $this->bits[$y * $this->rowSize + $x32];
|
||||
|
||||
if (0 !== $bits) {
|
||||
if ($y < $top) {
|
||||
$top = $y;
|
||||
}
|
||||
|
||||
if ($y > $bottom) {
|
||||
$bottom = $y;
|
||||
}
|
||||
|
||||
if ($x32 * 32 < $left) {
|
||||
$bit = 0;
|
||||
|
||||
while (($bits << (31 - $bit)) === 0) {
|
||||
$bit++;
|
||||
}
|
||||
|
||||
if (($x32 * 32 + $bit) < $left) {
|
||||
$left = $x32 * 32 + $bit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($x32 * 32 + 31 > $right) {
|
||||
$bit = 31;
|
||||
|
||||
while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
|
||||
--$bit;
|
||||
}
|
||||
|
||||
if (($x32 * 32 + $bit) > $right) {
|
||||
$right = $x32 * 32 + $bit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$width = $right - $left;
|
||||
$height = $bottom - $top;
|
||||
|
||||
if ($width < 0 || $height < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [$left, $top, $width, $height];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the most top left set bit.
|
||||
*
|
||||
* This is useful in detecting a corner of a 'pure' barcode.
|
||||
*
|
||||
* @return int[]|null
|
||||
*/
|
||||
public function getTopLeftOnBit() : ?array
|
||||
{
|
||||
$bitsOffset = 0;
|
||||
|
||||
while ($bitsOffset < count($this->bits) && 0 === $this->bits[$bitsOffset]) {
|
||||
++$bitsOffset;
|
||||
}
|
||||
|
||||
if (count($this->bits) === $bitsOffset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$x = intdiv($bitsOffset, $this->rowSize);
|
||||
$y = ($bitsOffset % $this->rowSize) << 5;
|
||||
|
||||
$bits = $this->bits[$bitsOffset];
|
||||
$bit = 0;
|
||||
|
||||
while (0 === ($bits << (31 - $bit))) {
|
||||
++$bit;
|
||||
}
|
||||
|
||||
$x += $bit;
|
||||
|
||||
return [$x, $y];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the most bottom right set bit.
|
||||
*
|
||||
* This is useful in detecting a corner of a 'pure' barcode.
|
||||
*
|
||||
* @return int[]|null
|
||||
*/
|
||||
public function getBottomRightOnBit() : ?array
|
||||
{
|
||||
$bitsOffset = count($this->bits) - 1;
|
||||
|
||||
while ($bitsOffset >= 0 && 0 === $this->bits[$bitsOffset]) {
|
||||
--$bitsOffset;
|
||||
}
|
||||
|
||||
if ($bitsOffset < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$x = intdiv($bitsOffset, $this->rowSize);
|
||||
$y = ($bitsOffset % $this->rowSize) << 5;
|
||||
|
||||
$bits = $this->bits[$bitsOffset];
|
||||
$bit = 0;
|
||||
|
||||
while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
|
||||
--$bit;
|
||||
}
|
||||
|
||||
$x += $bit;
|
||||
|
||||
return [$x, $y];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the width of the matrix,
|
||||
*/
|
||||
public function getWidth() : int
|
||||
{
|
||||
return $this->width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the height of the matrix.
|
||||
*/
|
||||
public function getHeight() : int
|
||||
{
|
||||
return $this->height;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
/**
|
||||
* General bit utilities.
|
||||
*
|
||||
* All utility methods are based on 32-bit integers and also work on 64-bit
|
||||
* systems.
|
||||
*/
|
||||
final class BitUtils
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an unsigned right shift.
|
||||
*
|
||||
* This is the same as the unsigned right shift operator ">>>" in other
|
||||
* languages.
|
||||
*/
|
||||
public static function unsignedRightShift(int $a, int $b) : int
|
||||
{
|
||||
return (
|
||||
$a >= 0
|
||||
? $a >> $b
|
||||
: (($a & 0x7fffffff) >> $b) | (0x40000000 >> ($b - 1))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of trailing zeros.
|
||||
*/
|
||||
public static function numberOfTrailingZeros(int $i) : int
|
||||
{
|
||||
$lastPos = strrpos(str_pad(decbin($i), 32, '0', STR_PAD_LEFT), '1');
|
||||
return $lastPos === false ? 32 : 31 - $lastPos;
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
use BaconQrCode\Exception\InvalidArgumentException;
|
||||
use DASPRiD\Enum\AbstractEnum;
|
||||
|
||||
/**
|
||||
* Encapsulates a Character Set ECI, according to "Extended Channel Interpretations" 5.3.1.1 of ISO 18004.
|
||||
*
|
||||
* @method static self CP437()
|
||||
* @method static self ISO8859_1()
|
||||
* @method static self ISO8859_2()
|
||||
* @method static self ISO8859_3()
|
||||
* @method static self ISO8859_4()
|
||||
* @method static self ISO8859_5()
|
||||
* @method static self ISO8859_6()
|
||||
* @method static self ISO8859_7()
|
||||
* @method static self ISO8859_8()
|
||||
* @method static self ISO8859_9()
|
||||
* @method static self ISO8859_10()
|
||||
* @method static self ISO8859_11()
|
||||
* @method static self ISO8859_12()
|
||||
* @method static self ISO8859_13()
|
||||
* @method static self ISO8859_14()
|
||||
* @method static self ISO8859_15()
|
||||
* @method static self ISO8859_16()
|
||||
* @method static self SJIS()
|
||||
* @method static self CP1250()
|
||||
* @method static self CP1251()
|
||||
* @method static self CP1252()
|
||||
* @method static self CP1256()
|
||||
* @method static self UNICODE_BIG_UNMARKED()
|
||||
* @method static self UTF8()
|
||||
* @method static self ASCII()
|
||||
* @method static self BIG5()
|
||||
* @method static self GB18030()
|
||||
* @method static self EUC_KR()
|
||||
*/
|
||||
final class CharacterSetEci extends AbstractEnum
|
||||
{
|
||||
protected const CP437 = [[0, 2]];
|
||||
protected const ISO8859_1 = [[1, 3], 'ISO-8859-1'];
|
||||
protected const ISO8859_2 = [[4], 'ISO-8859-2'];
|
||||
protected const ISO8859_3 = [[5], 'ISO-8859-3'];
|
||||
protected const ISO8859_4 = [[6], 'ISO-8859-4'];
|
||||
protected const ISO8859_5 = [[7], 'ISO-8859-5'];
|
||||
protected const ISO8859_6 = [[8], 'ISO-8859-6'];
|
||||
protected const ISO8859_7 = [[9], 'ISO-8859-7'];
|
||||
protected const ISO8859_8 = [[10], 'ISO-8859-8'];
|
||||
protected const ISO8859_9 = [[11], 'ISO-8859-9'];
|
||||
protected const ISO8859_10 = [[12], 'ISO-8859-10'];
|
||||
protected const ISO8859_11 = [[13], 'ISO-8859-11'];
|
||||
protected const ISO8859_12 = [[14], 'ISO-8859-12'];
|
||||
protected const ISO8859_13 = [[15], 'ISO-8859-13'];
|
||||
protected const ISO8859_14 = [[16], 'ISO-8859-14'];
|
||||
protected const ISO8859_15 = [[17], 'ISO-8859-15'];
|
||||
protected const ISO8859_16 = [[18], 'ISO-8859-16'];
|
||||
protected const SJIS = [[20], 'Shift_JIS'];
|
||||
protected const CP1250 = [[21], 'windows-1250'];
|
||||
protected const CP1251 = [[22], 'windows-1251'];
|
||||
protected const CP1252 = [[23], 'windows-1252'];
|
||||
protected const CP1256 = [[24], 'windows-1256'];
|
||||
protected const UNICODE_BIG_UNMARKED = [[25], 'UTF-16BE', 'UnicodeBig'];
|
||||
protected const UTF8 = [[26], 'UTF-8'];
|
||||
protected const ASCII = [[27, 170], 'US-ASCII'];
|
||||
protected const BIG5 = [[28]];
|
||||
protected const GB18030 = [[29], 'GB2312', 'EUC_CN', 'GBK'];
|
||||
protected const EUC_KR = [[30], 'EUC-KR'];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private array $otherEncodingNames;
|
||||
|
||||
/**
|
||||
* @var array<int, self>|null
|
||||
*/
|
||||
private static ?array $valueToEci;
|
||||
|
||||
/**
|
||||
* @var array<string, self>|null
|
||||
*/
|
||||
private static ?array $nameToEci = null;
|
||||
|
||||
/**
|
||||
* @param int[] $values
|
||||
*/
|
||||
public function __construct(private readonly array $values, string ...$otherEncodingNames)
|
||||
{
|
||||
$this->otherEncodingNames = $otherEncodingNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the primary value.
|
||||
*/
|
||||
public function getValue() : int
|
||||
{
|
||||
return $this->values[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets character set ECI by value.
|
||||
*
|
||||
* Returns the representing ECI of a given value, or null if it is legal but unsupported.
|
||||
*
|
||||
* @throws InvalidArgumentException if value is not between 0 and 900
|
||||
*/
|
||||
public static function getCharacterSetEciByValue(int $value) : ?self
|
||||
{
|
||||
if ($value < 0 || $value >= 900) {
|
||||
throw new InvalidArgumentException('Value must be between 0 and 900');
|
||||
}
|
||||
|
||||
$valueToEci = self::valueToEci();
|
||||
|
||||
if (! array_key_exists($value, $valueToEci)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $valueToEci[$value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns character set ECI by name.
|
||||
*
|
||||
* Returns the representing ECI of a given name, or null if it is legal but unsupported
|
||||
*/
|
||||
public static function getCharacterSetEciByName(string $name) : ?self
|
||||
{
|
||||
$nameToEci = self::nameToEci();
|
||||
$name = strtolower($name);
|
||||
|
||||
if (! array_key_exists($name, $nameToEci)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $nameToEci[$name];
|
||||
}
|
||||
|
||||
private static function valueToEci() : array
|
||||
{
|
||||
if (null !== self::$valueToEci) {
|
||||
return self::$valueToEci;
|
||||
}
|
||||
|
||||
self::$valueToEci = [];
|
||||
|
||||
foreach (self::values() as $eci) {
|
||||
foreach ($eci->values as $value) {
|
||||
self::$valueToEci[$value] = $eci;
|
||||
}
|
||||
}
|
||||
|
||||
return self::$valueToEci;
|
||||
}
|
||||
|
||||
private static function nameToEci() : array
|
||||
{
|
||||
if (null !== self::$nameToEci) {
|
||||
return self::$nameToEci;
|
||||
}
|
||||
|
||||
self::$nameToEci = [];
|
||||
|
||||
foreach (self::values() as $eci) {
|
||||
self::$nameToEci[strtolower($eci->name())] = $eci;
|
||||
|
||||
foreach ($eci->otherEncodingNames as $name) {
|
||||
self::$nameToEci[strtolower($name)] = $eci;
|
||||
}
|
||||
}
|
||||
|
||||
return self::$nameToEci;
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
/**
|
||||
* Encapsulates the parameters for one error-correction block in one symbol version.
|
||||
*
|
||||
* This includes the number of data codewords, and the number of times a block with these parameters is used
|
||||
* consecutively in the QR code version's format.
|
||||
*/
|
||||
final class EcBlock
|
||||
{
|
||||
public function __construct(private readonly int $count, private readonly int $dataCodewords)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns how many times the block is used.
|
||||
*/
|
||||
public function getCount() : int
|
||||
{
|
||||
return $this->count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of data codewords.
|
||||
*/
|
||||
public function getDataCodewords() : int
|
||||
{
|
||||
return $this->dataCodewords;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user