6 Commits

Author SHA1 Message Date
RMubarakzyanov ece1beb87f Merge branch 'main' into feature/user_profile
# Conflicts:
#	public/userarea/include/navbar.php
2026-05-17 20:06:15 +03:00
solocla e6a805f1f7 fixed permission 2026-05-15 21:10:28 +02:00
solocla fe84d446e7 stop tracking vendor 2026-05-15 20:54:16 +02:00
solocla 2ddf575191 phinx 2026-05-15 20:50:06 +02:00
solocla d73a8bb8d3 add permission to dashboard and navbar 2026-05-15 17:13:29 +02:00
RMubarakzyanov d155d1cbab user profile 2026-05-14 16:10:10 +03:00
11036 changed files with 7008 additions and 1350865 deletions
+3
View File
@@ -66,3 +66,6 @@ public/userarea/logsapi/commessaweb_customfields_763.json
public/userarea/logsapi/commessaweb_invia_762.json public/userarea/logsapi/commessaweb_invia_762.json
public/userarea/logsapi/commessaweb_invia_763.json public/userarea/logsapi/commessaweb_invia_763.json
public/userarea/logsapi/last_auth_url.txt public/userarea/logsapi/last_auth_url.txt
# User uploaded files
/public/userarea/files/
+1
View File
@@ -44,6 +44,7 @@
"phpmailer/phpmailer": "^6.9", "phpmailer/phpmailer": "^6.9",
"phpoffice/phpspreadsheet": "^4.1", "phpoffice/phpspreadsheet": "^4.1",
"proengsoft/laravel-jsvalidation": "^4.0.0", "proengsoft/laravel-jsvalidation": "^4.0.0",
"robmorgan/phinx": "^0.16.11",
"socialiteproviders/microsoft": "^4.7", "socialiteproviders/microsoft": "^4.7",
"spatie/laravel-query-builder": "^5.0", "spatie/laravel-query-builder": "^5.0",
"vanguardapp/activity-log": "^6.0", "vanguardapp/activity-log": "^6.0",
Generated
+646 -2
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "9c4f1e3bc3ee2180211c055e70635aef", "content-hash": "076e7721d08cfea8b06ce75dd8c6c576",
"packages": [ "packages": [
{ {
"name": "akaunting/laravel-setting", "name": "akaunting/laravel-setting",
@@ -251,6 +251,330 @@
], ],
"time": "2023-11-29T23:19:16+00:00" "time": "2023-11-29T23:19:16+00:00"
}, },
{
"name": "cakephp/chronos",
"version": "3.5.0",
"source": {
"type": "git",
"url": "https://github.com/cakephp/chronos.git",
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/chronos/zipball/e6e777b534244911566face8a5dbdbd7f7bda5a6",
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/clock": "^1.0"
},
"provide": {
"psr/clock-implementation": "1.0"
},
"require-dev": {
"cakephp/cakephp-codesniffer": "^5.0",
"phpunit/phpunit": "^10.5.58 || ^11.5.3 || ^12.1.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Cake\\Chronos\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brian Nesbitt",
"email": "brian@nesbot.com",
"homepage": "http://nesbot.com"
},
{
"name": "The CakePHP Team",
"homepage": "https://cakephp.org"
}
],
"description": "A simple API extension for DateTime.",
"homepage": "https://cakephp.org",
"keywords": [
"date",
"datetime",
"time"
],
"support": {
"issues": "https://github.com/cakephp/chronos/issues",
"source": "https://github.com/cakephp/chronos"
},
"time": "2026-04-10T02:50:39+00:00"
},
{
"name": "cakephp/core",
"version": "5.3.5",
"source": {
"type": "git",
"url": "https://github.com/cakephp/core.git",
"reference": "eb012517900ed288f580aa3487e9a09f28ea85f9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/core/zipball/eb012517900ed288f580aa3487e9a09f28ea85f9",
"reference": "eb012517900ed288f580aa3487e9a09f28ea85f9",
"shasum": ""
},
"require": {
"cakephp/utility": "^5.3.0",
"league/container": "^5.1",
"php": ">=8.2",
"psr/container": "^1.1 || ^2.0"
},
"provide": {
"psr/container-implementation": "^2.0"
},
"suggest": {
"cakephp/cache": "To use Configure::store() and restore().",
"cakephp/event": "To use PluginApplicationInterface or plugin applications.",
"league/container": "To use Container and ServiceProvider classes"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"files": [
"functions.php"
],
"psr-4": {
"Cake\\Core\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/core/graphs/contributors"
}
],
"description": "CakePHP Framework Core classes",
"homepage": "https://cakephp.org",
"keywords": [
"cakephp",
"core",
"framework"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/core"
},
"time": "2026-03-31T06:25:23+00:00"
},
{
"name": "cakephp/database",
"version": "5.3.5",
"source": {
"type": "git",
"url": "https://github.com/cakephp/database.git",
"reference": "cf94dcb57c54a1a308fd866b038cd6995910e36e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/database/zipball/cf94dcb57c54a1a308fd866b038cd6995910e36e",
"reference": "cf94dcb57c54a1a308fd866b038cd6995910e36e",
"shasum": ""
},
"require": {
"cakephp/chronos": "^3.3",
"cakephp/core": "^5.3.0",
"cakephp/datasource": "^5.3.0",
"php": ">=8.2",
"psr/log": "^3.0"
},
"require-dev": {
"cakephp/i18n": "^5.3.0",
"cakephp/log": "^5.3.0"
},
"suggest": {
"cakephp/i18n": "If you are using locale-aware datetime formats.",
"cakephp/log": "If you want to use query logging without providing a logger yourself."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"psr-4": {
"Cake\\Database\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/database/graphs/contributors"
}
],
"description": "Flexible and powerful Database abstraction library with a familiar PDO-like API",
"homepage": "https://cakephp.org",
"keywords": [
"abstraction",
"cakephp",
"database",
"database abstraction",
"pdo"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/database"
},
"time": "2026-03-31T06:25:23+00:00"
},
{
"name": "cakephp/datasource",
"version": "5.3.5",
"source": {
"type": "git",
"url": "https://github.com/cakephp/datasource.git",
"reference": "512464eb27b19316b515ec338089b83822c9ab5a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/datasource/zipball/512464eb27b19316b515ec338089b83822c9ab5a",
"reference": "512464eb27b19316b515ec338089b83822c9ab5a",
"shasum": ""
},
"require": {
"cakephp/core": "^5.3.0",
"php": ">=8.2",
"psr/simple-cache": "^2.0 || ^3.0"
},
"require-dev": {
"cakephp/cache": "^5.3.0",
"cakephp/collection": "^5.3.0",
"cakephp/utility": "^5.3.0"
},
"suggest": {
"cakephp/cache": "If you decide to use Query caching.",
"cakephp/collection": "If you decide to use ResultSetInterface.",
"cakephp/utility": "If you decide to use EntityTrait."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"psr-4": {
"Cake\\Datasource\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/datasource/graphs/contributors"
}
],
"description": "Provides connection managing and traits for Entities and Queries that can be reused for different datastores",
"homepage": "https://cakephp.org",
"keywords": [
"cakephp",
"connection management",
"datasource",
"entity",
"query"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/datasource"
},
"time": "2026-04-04T08:08:42+00:00"
},
{
"name": "cakephp/utility",
"version": "5.3.5",
"source": {
"type": "git",
"url": "https://github.com/cakephp/utility.git",
"reference": "4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/utility/zipball/4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99",
"reference": "4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99",
"shasum": ""
},
"require": {
"cakephp/core": "^5.3.0",
"php": ">=8.2"
},
"suggest": {
"ext-intl": "To use Text::transliterate() or Text::slug()",
"lib-ICU": "To use Text::transliterate() or Text::slug()"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Cake\\Utility\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/utility/graphs/contributors"
}
],
"description": "CakePHP Utility classes such as Inflector, String, Hash, and Security",
"homepage": "https://cakephp.org",
"keywords": [
"cakephp",
"hash",
"inflector",
"security",
"string",
"utility"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/utility"
},
"time": "2026-03-09T09:38:36+00:00"
},
{ {
"name": "carbonphp/carbon-doctrine-types", "name": "carbonphp/carbon-doctrine-types",
"version": "3.2.0", "version": "3.2.0",
@@ -2627,6 +2951,90 @@
], ],
"time": "2022-12-11T20:36:23+00:00" "time": "2022-12-11T20:36:23+00:00"
}, },
{
"name": "league/container",
"version": "5.2.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/container.git",
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/container/zipball/58accbc032f0090a9bd08326f93062c5a658b2c5",
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5",
"shasum": ""
},
"require": {
"php": "^8.1",
"psr/container": "^2.0.2",
"psr/event-dispatcher": "^1.0"
},
"provide": {
"psr/container-implementation": "^1.0"
},
"replace": {
"orno/di": "~2.0"
},
"require-dev": {
"nette/php-generator": "^4.1",
"nikic/php-parser": "^5.0",
"phpstan/phpstan": "^2.1.11",
"phpunit/phpunit": "^10.5.45|^11.5.15|^12.0",
"roave/security-advisories": "dev-latest",
"scrutinizer/ocular": "^1.9",
"squizlabs/php_codesniffer": "^3.9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-1.x": "1.x-dev",
"dev-2.x": "2.x-dev",
"dev-3.x": "3.x-dev",
"dev-4.x": "4.x-dev",
"dev-5.x": "5.x-dev",
"dev-master": "5.x-dev"
}
},
"autoload": {
"psr-4": {
"League\\Container\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Phil Bennett",
"email": "mail@philbennett.co.uk",
"role": "Developer"
}
],
"description": "A fast and intuitive dependency injection container.",
"homepage": "https://github.com/thephpleague/container",
"keywords": [
"container",
"dependency",
"di",
"injection",
"league",
"provider",
"service"
],
"support": {
"issues": "https://github.com/thephpleague/container/issues",
"source": "https://github.com/thephpleague/container/tree/5.2.0"
},
"funding": [
{
"url": "https://github.com/philipobenito",
"type": "github"
}
],
"time": "2026-03-19T18:52:39+00:00"
},
{ {
"name": "league/flysystem", "name": "league/flysystem",
"version": "3.28.0", "version": "3.28.0",
@@ -4980,6 +5388,93 @@
], ],
"time": "2024-04-27T21:32:50+00:00" "time": "2024-04-27T21:32:50+00:00"
}, },
{
"name": "robmorgan/phinx",
"version": "0.16.11",
"source": {
"type": "git",
"url": "https://github.com/cakephp/phinx.git",
"reference": "a03014fea316ba021fc0776982e5bed2d10228d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/phinx/zipball/a03014fea316ba021fc0776982e5bed2d10228d4",
"reference": "a03014fea316ba021fc0776982e5bed2d10228d4",
"shasum": ""
},
"require": {
"cakephp/database": "^5.0.2",
"composer-runtime-api": "^2.0",
"php-64bit": ">=8.1",
"psr/container": "^1.1|^2.0",
"symfony/config": "^4.0|^5.0|^6.0|^7.0|^8.0",
"symfony/console": "^6.0|^7.0|^8.0"
},
"require-dev": {
"cakephp/cakephp-codesniffer": "^5.0",
"cakephp/i18n": "^5.0",
"ext-json": "*",
"ext-pdo": "*",
"phpunit/phpunit": "^10.5",
"symfony/yaml": "^4.0|^5.0|^6.0|^7.0|^8.0"
},
"suggest": {
"ext-json": "Install if using JSON configuration format",
"ext-pdo": "PDO extension is needed",
"symfony/yaml": "Install if using YAML configuration format"
},
"bin": [
"bin/phinx"
],
"type": "library",
"autoload": {
"psr-4": {
"Phinx\\": "src/Phinx/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Rob Morgan",
"email": "robbym@gmail.com",
"homepage": "https://robmorgan.id.au",
"role": "Lead Developer"
},
{
"name": "Woody Gilk",
"email": "woody.gilk@gmail.com",
"homepage": "https://shadowhand.me",
"role": "Developer"
},
{
"name": "Richard Quadling",
"email": "rquadling@gmail.com",
"role": "Developer"
},
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/phinx/graphs/contributors",
"role": "Developer"
}
],
"description": "Phinx makes it ridiculously easy to manage the database migrations for your PHP app.",
"homepage": "https://phinx.org",
"keywords": [
"database",
"database migrations",
"db",
"migrations",
"phinx"
],
"support": {
"issues": "https://github.com/cakephp/phinx/issues",
"source": "https://github.com/cakephp/phinx/tree/0.16.11"
},
"time": "2026-03-15T00:04:32+00:00"
},
{ {
"name": "socialiteproviders/manager", "name": "socialiteproviders/manager",
"version": "v4.8.1", "version": "v4.8.1",
@@ -5312,6 +5807,85 @@
], ],
"time": "2024-05-31T14:57:53+00:00" "time": "2024-05-31T14:57:53+00:00"
}, },
{
"name": "symfony/config",
"version": "v7.4.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
"reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57",
"reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/filesystem": "^7.1|^8.0",
"symfony/polyfill-ctype": "~1.8"
},
"conflict": {
"symfony/finder": "<6.4",
"symfony/service-contracts": "<2.5"
},
"require-dev": {
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
"symfony/finder": "^6.4|^7.0|^8.0",
"symfony/messenger": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/yaml": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Config\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/config/tree/v7.4.10"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-03T14:20:49+00:00"
},
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v7.1.3", "version": "v7.1.3",
@@ -5768,6 +6342,76 @@
], ],
"time": "2024-04-18T09:32:20+00:00" "time": "2024-04-18T09:32:20+00:00"
}, },
{
"name": "symfony/filesystem",
"version": "v7.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.8"
},
"require-dev": {
"symfony/process": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Filesystem\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/filesystem/tree/v7.4.11"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-11T16:38:44+00:00"
},
{ {
"name": "symfony/finder", "name": "symfony/finder",
"version": "v7.1.3", "version": "v7.1.3",
@@ -11355,6 +11999,6 @@
"php": "^8.2.0", "php": "^8.2.0",
"ext-json": "*" "ext-json": "*"
}, },
"platform-dev": [], "platform-dev": {},
"plugin-api-version": "2.6.0" "plugin-api-version": "2.6.0"
} }
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class BaselineExistingDatabase extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
// Baseline migration.
// Existing database structure starts being tracked from this point.
}
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreatePhinxTestTable extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$table = $this->table('phinx_test_table');
$table
->addColumn('name', 'string', [
'limit' => 100,
'null' => false,
])
->addColumn('created_at', 'timestamp', [
'default' => 'CURRENT_TIMESTAMP',
'null' => false,
])
->create();
}
}
+246
View File
@@ -0,0 +1,246 @@
# 1. Database migration
```mysql
ALTER TABLE employees
ADD COLUMN address varchar(500) DEFAULT NULL AFTER last_name,
ADD COLUMN phone varchar(255) DEFAULT NULL AFTER address,
ADD COLUMN email varchar(255) DEFAULT NULL AFTER phone,
ADD COLUMN job_role_id int(10) UNSIGNED DEFAULT NULL AFTER department_id;
-- Replace ENUM status with plain VARCHAR for easier maintenance.
ALTER TABLE employees
MODIFY status varchar(255) NOT NULL DEFAULT 'active';
CREATE TABLE IF NOT EXISTS job_roles (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
description text DEFAULT NULL,
sort_order int(10) UNSIGNED NOT NULL DEFAULT 999,
is_active tinyint(1) NOT NULL DEFAULT 1,
created_at timestamp NULL DEFAULT current_timestamp(),
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (id),
UNIQUE KEY uniq_job_roles_name (name),
KEY idx_job_roles_active (is_active),
KEY idx_job_roles_sort_order (sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
ALTER TABLE employees
ADD KEY idx_employees_job_role_id (job_role_id);
ALTER TABLE employees
ADD CONSTRAINT fk_employees_job_role
FOREIGN KEY (job_role_id) REFERENCES job_roles (id)
ON DELETE SET NULL
ON UPDATE CASCADE;
-- 1) Seed job_roles with every distinct non-empty value of employees.position.
INSERT IGNORE INTO job_roles (name, is_active, sort_order, created_at, updated_at)
SELECT DISTINCT TRIM(position), 1, 999, NOW(), NOW()
FROM employees
WHERE position IS NOT NULL AND TRIM(position) <> '';
-- 2) Backfill employees.job_role_id by matching position text to job_roles.name.
UPDATE employees e
JOIN job_roles jr ON jr.name = TRIM(e.position)
SET e.job_role_id = jr.id
WHERE e.position IS NOT NULL AND TRIM(e.position) <> '';
-- 3) Drop the legacy column.
ALTER TABLE employees DROP COLUMN position;
CREATE TABLE IF NOT EXISTS training_topics (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
description text DEFAULT NULL,
default_frequency_months int(10) UNSIGNED DEFAULT NULL,
default_reminder_days int(10) UNSIGNED NOT NULL DEFAULT 30,
sort_order int(10) UNSIGNED NOT NULL DEFAULT 999,
is_active tinyint(1) NOT NULL DEFAULT 1,
is_mandatory tinyint(1) NOT NULL DEFAULT 0,
created_at timestamp NULL DEFAULT current_timestamp(),
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (id),
UNIQUE KEY uniq_training_topics_name (name),
KEY idx_training_topics_active (is_active),
KEY idx_training_topics_mandatory (is_mandatory),
KEY idx_training_topics_sort_order (sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS employee_documents (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
employee_id int(10) UNSIGNED NOT NULL,
category varchar(255) NOT NULL DEFAULT 'other',
original_name varchar(500) NOT NULL,
stored_name varchar(500) NOT NULL,
mime_type varchar(255) DEFAULT NULL,
size int(10) UNSIGNED DEFAULT NULL,
notes text DEFAULT NULL,
uploaded_by int(10) UNSIGNED DEFAULT NULL,
created_at timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (id),
KEY idx_employee_documents_employee (employee_id),
KEY idx_employee_documents_category (category),
KEY idx_employee_documents_uploaded_by (uploaded_by),
CONSTRAINT fk_employee_documents_employee
FOREIGN KEY (employee_id) REFERENCES employees (id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_employee_documents_uploaded_by
FOREIGN KEY (uploaded_by) REFERENCES auth_users (id)
ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS employee_ppe (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
employee_id int(10) UNSIGNED NOT NULL,
item_name varchar(255) NOT NULL,
delivery_date date DEFAULT NULL,
delivered_by varchar(255) DEFAULT NULL,
notes text DEFAULT NULL,
created_by int(10) UNSIGNED DEFAULT NULL,
created_at timestamp NULL DEFAULT current_timestamp(),
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (id),
KEY idx_employee_ppe_employee (employee_id),
KEY idx_employee_ppe_delivery_date (delivery_date),
CONSTRAINT fk_employee_ppe_employee
FOREIGN KEY (employee_id) REFERENCES employees (id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_employee_ppe_created_by
FOREIGN KEY (created_by) REFERENCES auth_users (id)
ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS employee_trainings (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
employee_id int(10) UNSIGNED NOT NULL,
training_topic_id int(10) UNSIGNED NOT NULL,
completed_date date NOT NULL,
delivered_by varchar(255) DEFAULT NULL,
description text DEFAULT NULL,
training_type varchar(255) NOT NULL DEFAULT 'initial',
update_frequency_months int(10) UNSIGNED DEFAULT NULL,
reminder_days int(10) UNSIGNED DEFAULT NULL,
next_due_date date DEFAULT NULL,
created_by int(10) UNSIGNED DEFAULT NULL,
created_at timestamp NULL DEFAULT current_timestamp(),
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (id),
KEY idx_employee_trainings_employee (employee_id),
KEY idx_employee_trainings_topic (training_topic_id),
KEY idx_employee_trainings_next_due (next_due_date),
KEY idx_employee_trainings_employee_topic (employee_id, training_topic_id),
KEY idx_employee_trainings_created_by (created_by),
CONSTRAINT fk_employee_trainings_employee
FOREIGN KEY (employee_id) REFERENCES employees (id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_employee_trainings_topic
FOREIGN KEY (training_topic_id) REFERENCES training_topics (id)
ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT fk_employee_trainings_created_by
FOREIGN KEY (created_by) REFERENCES auth_users (id)
ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS employee_training_attachments (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
training_id int(10) UNSIGNED NOT NULL,
original_name varchar(500) NOT NULL,
stored_name varchar(500) NOT NULL,
mime_type varchar(255) DEFAULT NULL,
size int(10) UNSIGNED DEFAULT NULL,
uploaded_by int(10) UNSIGNED DEFAULT NULL,
created_at timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (id),
KEY idx_employee_training_attachments_training (training_id),
KEY idx_employee_training_attachments_uploaded_by (uploaded_by),
CONSTRAINT fk_employee_training_attachments_training
FOREIGN KEY (training_id) REFERENCES employee_trainings (id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_employee_training_attachments_uploaded_by
FOREIGN KEY (uploaded_by) REFERENCES auth_users (id)
ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS employee_training_log (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
employee_id int(10) UNSIGNED DEFAULT NULL,
training_id int(10) UNSIGNED DEFAULT NULL,
action varchar(255) NOT NULL,
field varchar(255) DEFAULT NULL,
old_value text DEFAULT NULL,
new_value text DEFAULT NULL,
changed_by int(10) UNSIGNED DEFAULT NULL,
changed_at timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (id),
KEY idx_employee_training_log_employee (employee_id),
KEY idx_employee_training_log_training (training_id),
KEY idx_employee_training_log_changed_at (changed_at),
CONSTRAINT fk_employee_training_log_employee
FOREIGN KEY (employee_id) REFERENCES employees (id)
ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT fk_employee_training_log_training
FOREIGN KEY (training_id) REFERENCES employee_trainings (id)
ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT fk_employee_training_log_changed_by
FOREIGN KEY (changed_by) REFERENCES auth_users (id)
ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO auth_roles (name, display_name, description, removable, created_at, updated_at) VALUES
('employee', 'Employee', 'Read-only access to own employee profile.', 1, NOW(), NOW()),
('employee-hr', 'HR Manager', 'Can manage employee profiles, documents, PPE and training records.', 1, NOW(), NOW()),
('manager', 'Manager', 'Same permissions as HR Manager.', 1, NOW(), NOW())
ON DUPLICATE KEY UPDATE
display_name = VALUES(display_name),
description = VALUES(description),
updated_at = NOW();
CREATE TABLE IF NOT EXISTS training_reminder_log (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
training_id int(10) UNSIGNED DEFAULT NULL,
employee_id int(10) UNSIGNED DEFAULT NULL,
training_topic_id int(10) UNSIGNED DEFAULT NULL,
addressee_email varchar(255) NOT NULL,
next_due_date date DEFAULT NULL,
status_at_send varchar(255) NOT NULL,
sent_at timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (id),
KEY idx_training_reminder_log_dedup (training_id, addressee_email, next_due_date),
KEY idx_training_reminder_log_dedup_missing (employee_id, training_topic_id, addressee_email),
KEY idx_training_reminder_log_sent_at (sent_at),
CONSTRAINT fk_training_reminder_log_training
FOREIGN KEY (training_id) REFERENCES employee_trainings (id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_training_reminder_log_employee
FOREIGN KEY (employee_id) REFERENCES employees (id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_training_reminder_log_topic
FOREIGN KEY (training_topic_id) REFERENCES training_topics (id)
ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
# 2. Upload storage folder
Create the storage directory with the correct permissions for the web server:
```bash
mkdir -p /var/www/zibo-dashboard/public/userarea/files/employees
chown -R www-data:www-data /var/www/zibo-dashboard/public/userarea/files
chmod -R 775 /var/www/zibo-dashboard/public/userarea/files
```
Uploaded files will be organized as:
```
files/employees/{employee_id}/documents/ # File Repository (HR)
files/employees/{employee_id}/trainings/ # Training certificates
```
# 3. Cron for automated emails
```cron
0 7 * * * /usr/bin/php /var/www/zibo-dashboard/public/userarea/cron/send_training_reminders.php \
>> /var/www/zibo-dashboard/storage/logs/training_reminders.log 2>&1
```
+33
View File
@@ -0,0 +1,33 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
if (file_exists(__DIR__ . '/.env')) {
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->safeLoad();
}
return [
'paths' => [
'migrations' => __DIR__ . '/db/migrations',
'seeds' => __DIR__ . '/db/seeds',
],
'environments' => [
'default_migration_table' => 'phinxlog',
'default_environment' => 'development',
'development' => [
'adapter' => $_ENV['DB_CONNECTION'] ?? 'mysql',
'host' => $_ENV['DB_HOST'] ?? 'localhost',
'name' => $_ENV['DB_DATABASE'] ?? '',
'user' => $_ENV['DB_USERNAME'] ?? '',
'pass' => $_ENV['DB_PASSWORD'] ?? '',
'port' => $_ENV['DB_PORT'] ?? 3306,
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
],
],
'version_order' => 'creation',
];
+18
View File
@@ -0,0 +1,18 @@
<?php
/**
* Auth check for AJAX endpoints under /userarea/ajax/.
* Include this at the top of every ajax handler.
* Sets $currentUserId from session or returns 401 JSON.
*/
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (empty($_SESSION['iduserlogin'])) {
header('Content-Type: application/json');
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'Non autorizzato. Effettua il login.']);
exit;
}
$currentUserId = (int)$_SESSION['iduserlogin'];
@@ -0,0 +1,40 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'ID documento non valido.']);
exit;
}
$stmt = $pdo->prepare("SELECT employee_id, stored_name FROM employee_documents WHERE id = :id LIMIT 1");
$stmt->execute(['id' => $id]);
$doc = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$doc) {
echo json_encode(['success' => false, 'message' => 'Documento non trovato.']);
exit;
}
try {
$del = $pdo->prepare("DELETE FROM employee_documents WHERE id = :id");
$del->execute(['id' => $id]);
$path = __DIR__ . '/../../files/employees/' . (int)$doc['employee_id'] . '/documents/' . $doc['stored_name'];
if (is_file($path)) {
@unlink($path);
}
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,26 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'ID DPI non valido.']);
exit;
}
try {
$stmt = $pdo->prepare("DELETE FROM employee_ppe WHERE id = :id");
$stmt->execute(['id' => $id]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,60 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
exit;
}
try {
$pdo->beginTransaction();
$row = $pdo->prepare("SELECT employee_id FROM employee_trainings WHERE id = :id");
$row->execute(['id' => $id]);
$tr = $row->fetch(PDO::FETCH_ASSOC);
if (!$tr) {
$pdo->rollBack();
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
exit;
}
// Collect attached files BEFORE deletion so we can unlink them after
$files = $pdo->prepare("SELECT stored_name FROM employee_training_attachments WHERE training_id = :id");
$files->execute(['id' => $id]);
$stored = $files->fetchAll(PDO::FETCH_COLUMN);
// Log BEFORE delete (FK on log allows SET NULL on training delete but we want a clean record)
$pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, NULL, 'deleted', NULL, NULL, NULL, :cb, NOW())
")->execute(['eid' => $tr['employee_id'], 'cb' => $currentUserId]);
$pdo->prepare("DELETE FROM employee_trainings WHERE id = :id")->execute(['id' => $id]);
$pdo->commit();
foreach ($stored as $name) {
$path = __DIR__ . '/../../files/employees/' . (int)$tr['employee_id'] . '/trainings/' . $name;
if (is_file($path)) {
@unlink($path);
}
}
echo json_encode(['success' => true]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,59 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'ID allegato non valido.']);
exit;
}
$row = $pdo->prepare("
SELECT a.stored_name, a.original_name, a.training_id, t.employee_id
FROM employee_training_attachments a
JOIN employee_trainings t ON t.id = a.training_id
WHERE a.id = :id
LIMIT 1
");
$row->execute(['id' => $id]);
$att = $row->fetch(PDO::FETCH_ASSOC);
if (!$att) {
echo json_encode(['success' => false, 'message' => 'Allegato non trovato.']);
exit;
}
try {
$pdo->beginTransaction();
$pdo->prepare("DELETE FROM employee_training_attachments WHERE id = :id")->execute(['id' => $id]);
$pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, :tid, 'attachment_deleted', 'attachment', :name, NULL, :cb, NOW())
")->execute([
'eid' => $att['employee_id'],
'tid' => $att['training_id'],
'name' => $att['original_name'],
'cb' => $currentUserId,
]);
$pdo->commit();
$path = __DIR__ . '/../../files/employees/' . (int)$att['employee_id'] . '/trainings/' . $att['stored_name'];
if (is_file($path)) {
@unlink($path);
}
echo json_encode(['success' => true]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,57 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) {
http_response_code(400);
exit('ID non valido.');
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->prepare("
SELECT d.*, e.auth_user_id
FROM employee_documents d
JOIN employees e ON e.id = d.employee_id
WHERE d.id = :id
LIMIT 1
");
$stmt->execute(['id' => $id]);
$doc = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$doc) {
http_response_code(404);
exit('Documento non trovato.');
}
/* Access check: HR roles can download any; otherwise only own employee */
$roleStmt = $pdo->prepare("
SELECT r.name
FROM auth_users u
LEFT JOIN auth_roles r ON r.id = u.role_id
WHERE u.id = :id LIMIT 1
");
$roleStmt->execute(['id' => $currentUserId]);
$role = (string)$roleStmt->fetchColumn();
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
$isHr = in_array($role, $hrRoles, true);
if (!$isHr && (int)$doc['auth_user_id'] !== $currentUserId) {
http_response_code(403);
exit('Accesso negato.');
}
$path = __DIR__ . '/../../files/employees/' . (int)$doc['employee_id'] . '/documents/' . $doc['stored_name'];
if (!is_file($path)) {
http_response_code(404);
exit('File non trovato sul server.');
}
while (ob_get_level() > 0) { ob_end_clean(); }
header('Content-Type: ' . (!empty($doc['mime_type']) ? $doc['mime_type'] : 'application/octet-stream'));
header('Content-Disposition: attachment; filename="' . rawurlencode($doc['original_name']) . '"');
header('Content-Length: ' . filesize($path));
header('Cache-Control: private, max-age=0, must-revalidate');
readfile($path);
exit;
@@ -0,0 +1,56 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) {
http_response_code(400);
exit('ID non valido.');
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->prepare("
SELECT a.*, t.employee_id, e.auth_user_id
FROM employee_training_attachments a
JOIN employee_trainings t ON t.id = a.training_id
JOIN employees e ON e.id = t.employee_id
WHERE a.id = :id
LIMIT 1
");
$stmt->execute(['id' => $id]);
$att = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$att) {
http_response_code(404);
exit('Allegato non trovato.');
}
/* Access: HR or owning employee */
$roleStmt = $pdo->prepare("
SELECT r.name FROM auth_users u
LEFT JOIN auth_roles r ON r.id = u.role_id
WHERE u.id = :id LIMIT 1
");
$roleStmt->execute(['id' => $currentUserId]);
$role = (string)$roleStmt->fetchColumn();
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
$isHr = in_array($role, $hrRoles, true);
if (!$isHr && (int)$att['auth_user_id'] !== $currentUserId) {
http_response_code(403);
exit('Accesso negato.');
}
$path = __DIR__ . '/../../files/employees/' . (int)$att['employee_id'] . '/trainings/' . $att['stored_name'];
if (!is_file($path)) {
http_response_code(404);
exit('File non trovato sul server.');
}
while (ob_get_level() > 0) { ob_end_clean(); }
header('Content-Type: ' . (!empty($att['mime_type']) ? $att['mime_type'] : 'application/octet-stream'));
header('Content-Disposition: attachment; filename="' . rawurlencode($att['original_name']) . '"');
header('Content-Length: ' . filesize($path));
header('Cache-Control: private, max-age=0, must-revalidate');
readfile($path);
exit;
@@ -0,0 +1,58 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
header('Content-Type: application/json');
$trainingId = (int)($_GET['training_id'] ?? 0);
if ($trainingId <= 0) {
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
/* Access: HR or owner */
$ownerStmt = $pdo->prepare("
SELECT e.auth_user_id
FROM employee_trainings t
JOIN employees e ON e.id = t.employee_id
WHERE t.id = :id LIMIT 1
");
$ownerStmt->execute(['id' => $trainingId]);
$ownerAuthUserId = $ownerStmt->fetchColumn();
if ($ownerAuthUserId === false) {
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
exit;
}
$roleStmt = $pdo->prepare("
SELECT r.name FROM auth_users u
LEFT JOIN auth_roles r ON r.id = u.role_id
WHERE u.id = :id LIMIT 1
");
$roleStmt->execute(['id' => $currentUserId]);
$role = (string)$roleStmt->fetchColumn();
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
$isHr = in_array($role, $hrRoles, true);
if (!$isHr && (int)$ownerAuthUserId !== $currentUserId) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'Accesso negato.']);
exit;
}
$stmt = $pdo->prepare("
SELECT id, original_name, mime_type, size, created_at
FROM employee_training_attachments
WHERE training_id = :tid
ORDER BY created_at DESC
");
$stmt->execute(['tid' => $trainingId]);
$attachments = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'attachments' => $attachments,
'can_edit' => $isHr,
]);
@@ -0,0 +1,57 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
header('Content-Type: application/json');
$trainingId = (int)($_GET['training_id'] ?? 0);
if ($trainingId <= 0) {
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
/* Access: HR or owner */
$ownerStmt = $pdo->prepare("
SELECT e.auth_user_id
FROM employee_trainings t
JOIN employees e ON e.id = t.employee_id
WHERE t.id = :id LIMIT 1
");
$ownerStmt->execute(['id' => $trainingId]);
$ownerAuthUserId = $ownerStmt->fetchColumn();
if ($ownerAuthUserId === false) {
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
exit;
}
$roleStmt = $pdo->prepare("
SELECT r.name FROM auth_users u
LEFT JOIN auth_roles r ON r.id = u.role_id
WHERE u.id = :id LIMIT 1
");
$roleStmt->execute(['id' => $currentUserId]);
$role = (string)$roleStmt->fetchColumn();
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
$isHr = in_array($role, $hrRoles, true);
if (!$isHr && (int)$ownerAuthUserId !== $currentUserId) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'Accesso negato.']);
exit;
}
$stmt = $pdo->prepare("
SELECT l.id, l.action, l.field, l.old_value, l.new_value, l.changed_at,
TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS changed_by_name,
u.email AS changed_by_email
FROM employee_training_log l
LEFT JOIN auth_users u ON u.id = l.changed_by
WHERE l.training_id = :tid
ORDER BY l.changed_at DESC, l.id DESC
");
$stmt->execute(['tid' => $trainingId]);
$entries = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'entries' => $entries]);
@@ -0,0 +1,116 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$employeeId = (int)($_POST['employee_id'] ?? 0);
$firstName = trim($_POST['first_name'] ?? '');
$lastName = trim($_POST['last_name'] ?? '');
$employeeCode = trim($_POST['employee_code'] ?? '');
$address = trim($_POST['address'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$email = trim($_POST['email'] ?? '');
$hireDate = trim($_POST['hire_date'] ?? '');
$departmentId = $_POST['department_id'] ?? '';
$jobRoleId = $_POST['job_role_id'] ?? '';
$status = trim($_POST['status'] ?? '');
$authUserId = $_POST['auth_user_id'] ?? '';
$roleId = $_POST['role_id'] ?? '';
if ($employeeId <= 0) {
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
exit;
}
if ($firstName === '' || $lastName === '') {
echo json_encode(['success' => false, 'message' => 'Nome e cognome sono obbligatori.']);
exit;
}
$allowedStatus = ['active', 'inactive', 'suspended'];
if (!in_array($status, $allowedStatus, true)) {
$status = 'active';
}
$departmentId = ($departmentId === '' || $departmentId === null) ? null : (int)$departmentId;
$jobRoleId = ($jobRoleId === '' || $jobRoleId === null) ? null : (int)$jobRoleId;
$authUserId = ($authUserId === '' || $authUserId === null) ? null : (int)$authUserId;
$roleId = ($roleId === '' || $roleId === null) ? null : (int)$roleId;
$hireDate = $hireDate === '' ? null : $hireDate;
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo json_encode(['success' => false, 'message' => 'Email non valida.']);
exit;
}
if ($employeeCode !== '') {
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE employee_code = :code AND id <> :id");
$check->execute(['code' => $employeeCode, 'id' => $employeeId]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode(['success' => false, 'message' => 'Codice dipendente già in uso.']);
exit;
}
}
if ($authUserId !== null) {
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE auth_user_id = :uid AND id <> :id");
$check->execute(['uid' => $authUserId, 'id' => $employeeId]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode(['success' => false, 'message' => 'Questo utente è già associato ad un altro dipendente.']);
exit;
}
}
try {
$stmt = $pdo->prepare("
UPDATE employees
SET first_name = :first_name,
last_name = :last_name,
employee_code = :employee_code,
address = :address,
phone = :phone,
email = :email,
hire_date = :hire_date,
department_id = :department_id,
job_role_id = :job_role_id,
status = :status,
auth_user_id = :auth_user_id,
updated_at = NOW()
WHERE id = :id
");
$stmt->execute([
'first_name' => $firstName,
'last_name' => $lastName,
'employee_code' => $employeeCode !== '' ? $employeeCode : null,
'address' => $address !== '' ? $address : null,
'phone' => $phone !== '' ? $phone : null,
'email' => $email !== '' ? $email : null,
'hire_date' => $hireDate,
'department_id' => $departmentId,
'job_role_id' => $jobRoleId,
'status' => $status,
'auth_user_id' => $authUserId,
'id' => $employeeId,
]);
// Optionally update Vanguard role for the linked auth_user
if ($authUserId !== null && $roleId !== null) {
$check = $pdo->prepare("SELECT COUNT(*) FROM auth_roles WHERE id = ?");
$check->execute([$roleId]);
if ((int)$check->fetchColumn() > 0) {
$upd = $pdo->prepare("UPDATE auth_users SET role_id = :role_id, updated_at = NOW() WHERE id = :uid");
$upd->execute(['role_id' => $roleId, 'uid' => $authUserId]);
}
}
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,82 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
$employeeId = (int)($_POST['employee_id'] ?? 0);
$itemName = trim($_POST['item_name'] ?? '');
$deliveryDate = trim($_POST['delivery_date'] ?? '');
$deliveredBy = trim($_POST['delivered_by'] ?? '');
$notes = trim($_POST['notes'] ?? '');
if ($employeeId <= 0) {
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
exit;
}
if ($itemName === '') {
echo json_encode(['success' => false, 'message' => 'Il nome del DPI è obbligatorio.']);
exit;
}
$deliveryDate = $deliveryDate === '' ? null : $deliveryDate;
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
$notes = $notes !== '' ? $notes : null;
try {
if ($id > 0) {
$stmt = $pdo->prepare("
UPDATE employee_ppe
SET item_name = :item_name,
delivery_date = :delivery_date,
delivered_by = :delivered_by,
notes = :notes,
updated_at = NOW()
WHERE id = :id AND employee_id = :eid
");
$stmt->execute([
'item_name' => $itemName,
'delivery_date' => $deliveryDate,
'delivered_by' => $deliveredBy,
'notes' => $notes,
'id' => $id,
'eid' => $employeeId,
]);
echo json_encode(['success' => true, 'id' => $id]);
exit;
}
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE id = :id");
$check->execute(['id' => $employeeId]);
if ((int)$check->fetchColumn() === 0) {
echo json_encode(['success' => false, 'message' => 'Dipendente non trovato.']);
exit;
}
$stmt = $pdo->prepare("
INSERT INTO employee_ppe
(employee_id, item_name, delivery_date, delivered_by, notes, created_by, created_at, updated_at)
VALUES
(:employee_id, :item_name, :delivery_date, :delivered_by, :notes, :created_by, NOW(), NOW())
");
$stmt->execute([
'employee_id' => $employeeId,
'item_name' => $itemName,
'delivery_date' => $deliveryDate,
'delivered_by' => $deliveredBy,
'notes' => $notes,
'created_by' => $currentUserId,
]);
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,177 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
$employeeId = (int)($_POST['employee_id'] ?? 0);
$topicId = (int)($_POST['training_topic_id'] ?? 0);
$completedDate = trim($_POST['completed_date'] ?? '');
$deliveredBy = trim($_POST['delivered_by'] ?? '');
$description = trim($_POST['description'] ?? '');
$trainingType = trim($_POST['training_type'] ?? 'initial');
$freqRaw = $_POST['update_frequency_months'] ?? '';
$remRaw = $_POST['reminder_days'] ?? '';
if ($employeeId <= 0) {
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
exit;
}
if ($topicId <= 0) {
echo json_encode(['success' => false, 'message' => 'Selezionare un corso.']);
exit;
}
if ($completedDate === '') {
echo json_encode(['success' => false, 'message' => 'La data di completamento è obbligatoria.']);
exit;
}
if (!in_array($trainingType, ['initial', 'refresher'], true)) {
$trainingType = 'initial';
}
$topicStmt = $pdo->prepare("SELECT default_frequency_months, default_reminder_days FROM training_topics WHERE id = :id");
$topicStmt->execute(['id' => $topicId]);
$topic = $topicStmt->fetch(PDO::FETCH_ASSOC);
if (!$topic) {
echo json_encode(['success' => false, 'message' => 'Corso non trovato.']);
exit;
}
$freq = ($freqRaw === '' || $freqRaw === null) ? null : max(0, (int)$freqRaw);
$rem = ($remRaw === '' || $remRaw === null) ? null : max(0, (int)$remRaw);
/* Effective frequency for next_due_date: explicit override or topic default */
$effFreq = $freq !== null ? $freq : ($topic['default_frequency_months'] !== null ? (int)$topic['default_frequency_months'] : null);
$nextDue = null;
if ($effFreq !== null && $effFreq > 0) {
$d = DateTime::createFromFormat('Y-m-d', $completedDate);
if ($d) {
$d->modify('+' . (int)$effFreq . ' months');
$nextDue = $d->format('Y-m-d');
}
}
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
$description = $description !== '' ? $description : null;
try {
$pdo->beginTransaction();
if ($id > 0) {
$old = $pdo->prepare("SELECT * FROM employee_trainings WHERE id = :id");
$old->execute(['id' => $id]);
$oldRow = $old->fetch(PDO::FETCH_ASSOC);
if (!$oldRow) {
$pdo->rollBack();
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
exit;
}
$upd = $pdo->prepare("
UPDATE employee_trainings
SET training_topic_id = :topic_id,
completed_date = :completed_date,
delivered_by = :delivered_by,
description = :description,
training_type = :training_type,
update_frequency_months = :freq,
reminder_days = :rem,
next_due_date = :next_due,
updated_at = NOW()
WHERE id = :id
");
$upd->execute([
'topic_id' => $topicId,
'completed_date' => $completedDate,
'delivered_by' => $deliveredBy,
'description' => $description,
'training_type' => $trainingType,
'freq' => $freq,
'rem' => $rem,
'next_due' => $nextDue,
'id' => $id,
]);
$fields = [
'training_topic_id' => $topicId,
'completed_date' => $completedDate,
'delivered_by' => $deliveredBy,
'description' => $description,
'training_type' => $trainingType,
'update_frequency_months' => $freq,
'reminder_days' => $rem,
'next_due_date' => $nextDue,
];
$logStmt = $pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, :tid, 'updated', :field, :old_v, :new_v, :cb, NOW())
");
foreach ($fields as $f => $newV) {
$oldV = $oldRow[$f] ?? null;
if ((string)$oldV !== (string)$newV) {
$logStmt->execute([
'eid' => $employeeId,
'tid' => $id,
'field' => $f,
'old_v' => $oldV,
'new_v' => $newV,
'cb' => $currentUserId,
]);
}
}
$pdo->commit();
echo json_encode(['success' => true, 'id' => $id]);
exit;
}
$ins = $pdo->prepare("
INSERT INTO employee_trainings
(employee_id, training_topic_id, completed_date,
delivered_by, description,
training_type, update_frequency_months, reminder_days, next_due_date,
created_by, created_at, updated_at)
VALUES
(:eid, :tid, :completed_date,
:delivered_by, :description,
:training_type, :freq, :rem, :next_due,
:cb, NOW(), NOW())
");
$ins->execute([
'eid' => $employeeId,
'tid' => $topicId,
'completed_date' => $completedDate,
'delivered_by' => $deliveredBy,
'description' => $description,
'training_type' => $trainingType,
'freq' => $freq,
'rem' => $rem,
'next_due' => $nextDue,
'cb' => $currentUserId,
]);
$newId = (int)$pdo->lastInsertId();
$pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, :tid, 'created', NULL, NULL, NULL, :cb, NOW())
")->execute(['eid' => $employeeId, 'tid' => $newId, 'cb' => $currentUserId]);
$pdo->commit();
echo json_encode(['success' => true, 'id' => $newId]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,89 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$employeeId = (int)($_POST['employee_id'] ?? 0);
$category = trim($_POST['category'] ?? 'other');
$notes = trim($_POST['notes'] ?? '');
$allowedCategories = ['job_description', 'contract', 'rules', 'other'];
if (!in_array($category, $allowedCategories, true)) {
$category = 'other';
}
if ($employeeId <= 0) {
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
exit;
}
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE id = :id");
$check->execute(['id' => $employeeId]);
if ((int)$check->fetchColumn() === 0) {
echo json_encode(['success' => false, 'message' => 'Dipendente non trovato.']);
exit;
}
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
$errCode = $_FILES['file']['error'] ?? -1;
$msg = 'Errore nel caricamento del file.';
if ($errCode === UPLOAD_ERR_INI_SIZE || $errCode === UPLOAD_ERR_FORM_SIZE) {
$msg = 'Il file supera la dimensione massima consentita.';
}
echo json_encode(['success' => false, 'message' => $msg]);
exit;
}
$originalName = $_FILES['file']['name'];
$tmpPath = $_FILES['file']['tmp_name'];
$size = (int)$_FILES['file']['size'];
$mimeType = mime_content_type($tmpPath) ?: ($_FILES['file']['type'] ?? null);
$dir = __DIR__ . '/../../files/employees/' . $employeeId . '/documents';
if (!is_dir($dir)) {
if (!mkdir($dir, 0775, true) && !is_dir($dir)) {
echo json_encode(['success' => false, 'message' => 'Impossibile creare la cartella di destinazione.']);
exit;
}
}
$safeOriginal = preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName);
$storedName = uniqid('doc_') . '_' . $safeOriginal;
$destPath = $dir . '/' . $storedName;
if (!move_uploaded_file($tmpPath, $destPath)) {
echo json_encode(['success' => false, 'message' => 'Impossibile salvare il file su disco.']);
exit;
}
try {
$stmt = $pdo->prepare("
INSERT INTO employee_documents
(employee_id, category, original_name, stored_name, mime_type, size, notes, uploaded_by, created_at)
VALUES
(:employee_id, :category, :original_name, :stored_name, :mime_type, :size, :notes, :uploaded_by, NOW())
");
$stmt->execute([
'employee_id' => $employeeId,
'category' => $category,
'original_name' => $originalName,
'stored_name' => $storedName,
'mime_type' => $mimeType,
'size' => $size,
'notes' => $notes !== '' ? $notes : null,
'uploaded_by' => $currentUserId,
]);
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
} catch (Exception $e) {
@unlink($destPath);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,98 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$trainingId = (int)($_POST['training_id'] ?? 0);
if ($trainingId <= 0) {
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
exit;
}
$tr = $pdo->prepare("SELECT employee_id FROM employee_trainings WHERE id = :id");
$tr->execute(['id' => $trainingId]);
$trainingRow = $tr->fetch(PDO::FETCH_ASSOC);
if (!$trainingRow) {
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
exit;
}
$employeeId = (int)$trainingRow['employee_id'];
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
$errCode = $_FILES['file']['error'] ?? -1;
$msg = 'Errore nel caricamento del file.';
if ($errCode === UPLOAD_ERR_INI_SIZE || $errCode === UPLOAD_ERR_FORM_SIZE) {
$msg = 'Il file supera la dimensione massima consentita.';
}
echo json_encode(['success' => false, 'message' => $msg]);
exit;
}
$originalName = $_FILES['file']['name'];
$tmpPath = $_FILES['file']['tmp_name'];
$size = (int)$_FILES['file']['size'];
$mimeType = mime_content_type($tmpPath) ?: ($_FILES['file']['type'] ?? null);
$dir = __DIR__ . '/../../files/employees/' . $employeeId . '/trainings';
if (!is_dir($dir)) {
if (!mkdir($dir, 0775, true) && !is_dir($dir)) {
echo json_encode(['success' => false, 'message' => 'Impossibile creare la cartella di destinazione.']);
exit;
}
}
$safeOriginal = preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName);
$storedName = uniqid('tr_') . '_' . $safeOriginal;
$destPath = $dir . '/' . $storedName;
if (!move_uploaded_file($tmpPath, $destPath)) {
echo json_encode(['success' => false, 'message' => 'Impossibile salvare il file su disco.']);
exit;
}
try {
$pdo->beginTransaction();
$ins = $pdo->prepare("
INSERT INTO employee_training_attachments
(training_id, original_name, stored_name, mime_type, size, uploaded_by, created_at)
VALUES
(:tid, :original_name, :stored_name, :mime_type, :size, :uploaded_by, NOW())
");
$ins->execute([
'tid' => $trainingId,
'original_name' => $originalName,
'stored_name' => $storedName,
'mime_type' => $mimeType,
'size' => $size,
'uploaded_by' => $currentUserId,
]);
$attachmentId = (int)$pdo->lastInsertId();
$pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, :tid, 'attachment_added', 'attachment', NULL, :name, :cb, NOW())
")->execute([
'eid' => $employeeId,
'tid' => $trainingId,
'name' => $originalName,
'cb' => $currentUserId,
]);
$pdo->commit();
echo json_encode(['success' => true, 'id' => $attachmentId]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
@unlink($destPath);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+32
View File
@@ -0,0 +1,32 @@
<?php
/**
* HR auth check for AJAX endpoints that require HR-management permissions.
* Allowed roles: Admin, User, Superuser, employee-hr, manager.
* Sets $currentUserId and $currentUserRole, or returns 401/403 JSON.
*/
require_once(__DIR__ . '/auth_check.php');
require_once(__DIR__ . '/../class/db-functions.php');
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->prepare("
SELECT r.name AS role_name
FROM auth_users u
LEFT JOIN auth_roles r ON r.id = u.role_id
WHERE u.id = :id
LIMIT 1
");
$stmt->execute(['id' => $currentUserId]);
$currentUserRole = (string)$stmt->fetchColumn();
$allowedHrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
if (!in_array($currentUserRole, $allowedHrRoles, true)) {
header('Content-Type: application/json');
http_response_code(403);
echo json_encode([
'success' => false,
'message' => 'Permessi insufficienti per questa operazione.',
]);
exit;
}
+38
View File
@@ -0,0 +1,38 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'ID mansione non valido.']);
exit;
}
try {
$usage = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE job_role_id = :id");
$usage->execute(['id' => $id]);
if ((int)$usage->fetchColumn() > 0) {
echo json_encode([
'success' => false,
'message' => 'Questa mansione è associata a uno o più dipendenti e non può essere cancellata.',
]);
exit;
}
$stmt = $pdo->prepare("DELETE FROM job_roles WHERE id = :id");
$stmt->execute(['id' => $id]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+77
View File
@@ -0,0 +1,77 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$description = trim($_POST['description'] ?? '');
$sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 999;
$is_active = isset($_POST['is_active']) ? ((int)$_POST['is_active'] === 1 ? 1 : 0) : 1;
if ($name === '') {
echo json_encode(['success' => false, 'message' => 'Il nome della mansione è obbligatorio.']);
exit;
}
try {
if ($id > 0) {
$check = $pdo->prepare("SELECT COUNT(*) FROM job_roles WHERE name = :name AND id <> :id");
$check->execute(['name' => $name, 'id' => $id]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode(['success' => false, 'message' => 'Esiste già un\'altra mansione con questo nome.']);
exit;
}
$stmt = $pdo->prepare("
UPDATE job_roles
SET name = :name,
description = :description,
sort_order = :sort_order,
is_active = :is_active,
updated_at = NOW()
WHERE id = :id
");
$stmt->execute([
'name' => $name,
'description' => $description !== '' ? $description : null,
'sort_order' => $sort_order,
'is_active' => $is_active,
'id' => $id,
]);
echo json_encode(['success' => true, 'id' => $id]);
exit;
}
$check = $pdo->prepare("SELECT COUNT(*) FROM job_roles WHERE name = :name");
$check->execute(['name' => $name]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode(['success' => false, 'message' => 'Esiste già una mansione con questo nome.']);
exit;
}
$stmt = $pdo->prepare("
INSERT INTO job_roles (name, description, sort_order, is_active, created_at, updated_at)
VALUES (:name, :description, :sort_order, :is_active, NOW(), NOW())
");
$stmt->execute([
'name' => $name,
'description' => $description !== '' ? $description : null,
'sort_order' => $sort_order,
'is_active' => $is_active,
]);
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,38 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'ID corso non valido.']);
exit;
}
try {
$usage = $pdo->prepare("SELECT COUNT(*) FROM employee_trainings WHERE training_topic_id = :id");
$usage->execute(['id' => $id]);
if ((int)$usage->fetchColumn() > 0) {
echo json_encode([
'success' => false,
'message' => 'Questo corso ha già delle registrazioni di formazione e non può essere cancellato.',
]);
exit;
}
$stmt = $pdo->prepare("DELETE FROM training_topics WHERE id = :id");
$stmt->execute(['id' => $id]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,94 @@
<?php
require_once(__DIR__ . '/../auth_check.php');
require_once(__DIR__ . '/../../class/db-functions.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$description = trim($_POST['description'] ?? '');
$freqRaw = $_POST['default_frequency_months'] ?? '';
$remRaw = $_POST['default_reminder_days'] ?? '';
$sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 999;
$is_active = isset($_POST['is_active']) ? ((int)$_POST['is_active'] === 1 ? 1 : 0) : 1;
$is_mandatory = isset($_POST['is_mandatory']) && (int)$_POST['is_mandatory'] === 1 ? 1 : 0;
$freq = ($freqRaw === '' || $freqRaw === null) ? null : max(0, (int)$freqRaw);
$rem = ($remRaw === '' || $remRaw === null) ? 30 : max(0, (int)$remRaw);
if ($name === '') {
echo json_encode(['success' => false, 'message' => 'Il nome del corso è obbligatorio.']);
exit;
}
try {
if ($id > 0) {
$check = $pdo->prepare("SELECT COUNT(*) FROM training_topics WHERE name = :name AND id <> :id");
$check->execute(['name' => $name, 'id' => $id]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode(['success' => false, 'message' => 'Esiste già un altro corso con questo nome.']);
exit;
}
$stmt = $pdo->prepare("
UPDATE training_topics
SET name = :name,
description = :description,
default_frequency_months = :freq,
default_reminder_days = :rem,
sort_order = :sort_order,
is_active = :is_active,
is_mandatory = :is_mandatory,
updated_at = NOW()
WHERE id = :id
");
$stmt->execute([
'name' => $name,
'description' => $description !== '' ? $description : null,
'freq' => $freq,
'rem' => $rem,
'sort_order' => $sort_order,
'is_active' => $is_active,
'is_mandatory' => $is_mandatory,
'id' => $id,
]);
echo json_encode(['success' => true, 'id' => $id]);
exit;
}
$check = $pdo->prepare("SELECT COUNT(*) FROM training_topics WHERE name = :name");
$check->execute(['name' => $name]);
if ((int)$check->fetchColumn() > 0) {
echo json_encode(['success' => false, 'message' => 'Esiste già un corso con questo nome.']);
exit;
}
$stmt = $pdo->prepare("
INSERT INTO training_topics
(name, description, default_frequency_months, default_reminder_days, sort_order, is_active, is_mandatory, created_at, updated_at)
VALUES
(:name, :description, :freq, :rem, :sort_order, :is_active, :is_mandatory, NOW(), NOW())
");
$stmt->execute([
'name' => $name,
'description' => $description !== '' ? $description : null,
'freq' => $freq,
'rem' => $rem,
'sort_order' => $sort_order,
'is_active' => $is_active,
'is_mandatory' => $is_mandatory,
]);
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,347 @@
<?php
/**
* Formazione Email reminder cron script
* Run daily: 0 7 * * * php /var/www/html/public/userarea/cron/send_training_reminders.php
*
* Sends "due_soon" emails when next_due_date is within the reminder window
* (override reminder_days > topic default > 30 days).
* Sends "expired" emails when next_due_date is in the past.
* Skips rows with next_due_date IS NULL (one-off trainings).
* Skips already-sent notifications (same training + addressee + next_due_date).
* Recipients: the employee (employees.email or auth_users.email) + every HR user
* with role Admin / Superuser / employee-hr / manager.
*
* Optional CLI flags:
* --dry-run log only, no SMTP, no DB write
* --only-email=foo@bar restrict to a single addressee (for testing)
*/
require_once __DIR__ . '/../class/db-functions.php';
require_once __DIR__ . '/../../../vendor/autoload.php';
use Dotenv\Dotenv;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
$dotenv = Dotenv::createImmutable(__DIR__ . '/../../../');
$dotenv->load();
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$today = date('Y-m-d');
$appUrl = rtrim($_ENV['APP_URL'] ?? 'http://localhost:8001', '/');
/* CLI flags */
$dryRun = false;
$onlyEmail = null;
foreach (array_slice($argv ?? [], 1) as $a) {
if ($a === '--dry-run' || $a === '-n') {
$dryRun = true;
} elseif (strpos($a, '--only-email=') === 0) {
$onlyEmail = substr($a, strlen('--only-email='));
}
}
$sent = 0;
$skipped = 0;
$errors = 0;
/* Candidate trainings (with optional override reminder + topic default) */
$stmt = $pdo->query("
SELECT et.id, et.employee_id, et.completed_date, et.next_due_date,
et.reminder_days, et.delivered_by,
tt.name AS topic_name, tt.default_reminder_days AS topic_default_rem,
e.first_name, e.last_name, e.employee_code,
e.email AS employee_email_direct,
au.email AS employee_email_auth
FROM employee_trainings et
JOIN training_topics tt ON tt.id = et.training_topic_id
JOIN employees e ON e.id = et.employee_id
LEFT JOIN auth_users au ON au.id = e.auth_user_id
WHERE et.next_due_date IS NOT NULL
");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($rows)) {
echo date('Y-m-d H:i:s') . " — Nessuna formazione da notificare.\n";
exit(0);
}
/* HR addressees (one query, reused per training) */
$hrUsers = $pdo->query("
SELECT u.id, u.email, TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS name
FROM auth_users u
JOIN auth_roles r ON r.id = u.role_id
WHERE r.name IN ('Admin','Superuser','employee-hr','manager')
AND u.email IS NOT NULL AND u.email <> ''
")->fetchAll(PDO::FETCH_ASSOC);
$checkSent = $pdo->prepare("
SELECT COUNT(*) FROM training_reminder_log
WHERE training_id = ? AND addressee_email = ? AND next_due_date = ?
");
$insertLog = $pdo->prepare("
INSERT INTO training_reminder_log
(training_id, addressee_email, next_due_date, status_at_send, sent_at)
VALUES (?, ?, ?, ?, NOW())
");
foreach ($rows as $r) {
$rem = $r['reminder_days'] !== null
? (int)$r['reminder_days']
: ($r['topic_default_rem'] !== null ? (int)$r['topic_default_rem'] : 30);
$isOverdue = $r['next_due_date'] < $today;
$daysLeft = (int)((strtotime($r['next_due_date']) - strtotime($today)) / 86400);
if (!$isOverdue && $daysLeft > $rem) {
continue; // not yet in the reminder window
}
$type = $isOverdue ? 'expired' : 'update_to_be_scheduled';
$employeeFullName = trim($r['first_name'] . ' ' . $r['last_name']);
$employeeEmail = !empty($r['employee_email_direct'])
? $r['employee_email_direct']
: (!empty($r['employee_email_auth']) ? $r['employee_email_auth'] : null);
/* Collect addressees (employee + HR), deduplicated by lowercased email */
$recipients = [];
if ($employeeEmail) {
$key = strtolower(trim($employeeEmail));
$recipients[$key] = ['email' => $employeeEmail, 'name' => $employeeFullName, 'is_hr' => false];
}
foreach ($hrUsers as $hr) {
$key = strtolower(trim((string)$hr['email']));
if ($key === '' || isset($recipients[$key])) continue;
$recipients[$key] = ['email' => $hr['email'], 'name' => trim((string)$hr['name']), 'is_hr' => true];
}
if (empty($recipients)) {
continue;
}
foreach ($recipients as $email => $rec) {
if ($onlyEmail !== null && strcasecmp($rec['email'], $onlyEmail) !== 0) continue;
$checkSent->execute([$r['id'], $rec['email'], $r['next_due_date']]);
if ($checkSent->fetchColumn() > 0) {
$skipped++;
continue;
}
try {
$mail = new PHPMailer(true);
// SMTP config from .env
$mailer = $_ENV['MAIL_MAILER'] ?? 'mail';
if ($mailer === 'smtp') {
$mail->isSMTP();
$mail->Host = $_ENV['MAIL_HOST'] ?? 'localhost';
$mail->Port = (int)($_ENV['MAIL_PORT'] ?? 587);
if (!empty($_ENV['MAIL_USERNAME']) && $_ENV['MAIL_USERNAME'] !== 'null') {
$mail->SMTPAuth = true;
$mail->Username = $_ENV['MAIL_USERNAME'];
$mail->Password = $_ENV['MAIL_PASSWORD'] ?? '';
}
$enc = $_ENV['MAIL_ENCRYPTION'] ?? '';
if ($enc && $enc !== 'null') {
$mail->SMTPSecure = $enc;
}
}
$mail->CharSet = 'UTF-8';
$mail->setFrom(
$_ENV['MAIL_FROM_ADDRESS'] ?? 'noreply@zibogomma.it',
$_ENV['MAIL_FROM_NAME'] ?? 'Formazione ZIBOGOMMA'
);
$mail->addAddress($rec['email'], $rec['name'] ?: $rec['email']);
$profileUrl = $appUrl . '/userarea/employee-profile.php?id=' . (int)$r['employee_id'] . '#tab-training';
$topicText = $r['topic_name'] . ' — ' . $employeeFullName
. (!empty($r['employee_code']) ? ' (' . $r['employee_code'] . ')' : '');
if ($isOverdue) {
$mail->Subject = '⚠️ Formazione scaduta: ' . $r['topic_name'];
$mail->Body = buildHtml(
'Formazione scaduta',
$topicText,
'Completata il <strong>' . date('d/m/Y', strtotime($r['completed_date'])) . '</strong>. '
. 'Il prossimo aggiornamento era previsto per <strong>' . date('d/m/Y', strtotime($r['next_due_date'])) . '</strong>'
. ' (scaduta da <strong>' . abs($daysLeft) . ' giorni</strong>).',
'#dc3545',
$profileUrl,
$rec['is_hr']
);
} else {
$mail->Subject = '📚 Formazione in scadenza: ' . $r['topic_name'];
$daysText = $daysLeft === 0 ? 'oggi' : 'tra <strong>' . $daysLeft . ' giorni</strong>';
$mail->Body = buildHtml(
'Formazione in scadenza',
$topicText,
'Completata il <strong>' . date('d/m/Y', strtotime($r['completed_date'])) . '</strong>. '
. 'Prossimo aggiornamento previsto per <strong>' . date('d/m/Y', strtotime($r['next_due_date'])) . '</strong>'
. ' (' . $daysText . ').',
'#e8930c',
$profileUrl,
$rec['is_hr']
);
}
$mail->isHTML(true);
$mail->AltBody = strip_tags(str_replace('<br>', "\n", $mail->Body));
if ($dryRun) {
echo date('H:i:s') . " ◌ DRY {$type}{$rec['email']}{$r['topic_name']}\n";
$sent++;
continue;
}
$mail->send();
$insertLog->execute([$r['id'], $rec['email'], $r['next_due_date'], $type]);
$sent++;
echo date('H:i:s') . "{$type}{$rec['email']}{$r['topic_name']}\n";
} catch (Exception $e) {
$errors++;
echo date('H:i:s') . " ✗ Errore {$rec['email']}: {$e->getMessage()}\n";
}
}
}
/* ============================================================================
NOT-PRESENT reminders mandatory topics with no record for an employee.
Notify HR only.
De-dup by (employee_id, training_topic_id, addressee_email).
============================================================================ */
$missingStmt = $pdo->query("
SELECT e.id AS employee_id, e.first_name, e.last_name, e.employee_code,
tt.id AS topic_id, tt.name AS topic_name
FROM employees e
CROSS JOIN training_topics tt
WHERE tt.is_active = 1 AND tt.is_mandatory = 1
AND (e.status IS NULL OR e.status = 'active')
AND NOT EXISTS (
SELECT 1 FROM employee_trainings et
WHERE et.employee_id = e.id AND et.training_topic_id = tt.id
)
ORDER BY e.last_name, e.first_name, tt.name
");
$missingRows = $missingStmt->fetchAll(PDO::FETCH_ASSOC);
$checkMissingSent = $pdo->prepare("
SELECT COUNT(*) FROM training_reminder_log
WHERE employee_id = ? AND training_topic_id = ? AND addressee_email = ?
AND status_at_send = 'not_present'
");
$insertMissingLog = $pdo->prepare("
INSERT INTO training_reminder_log
(training_id, employee_id, training_topic_id, addressee_email, next_due_date, status_at_send, sent_at)
VALUES (NULL, ?, ?, ?, NULL, 'not_present', NOW())
");
foreach ($missingRows as $m) {
$employeeFullName = trim($m['first_name'] . ' ' . $m['last_name']);
foreach ($hrUsers as $hr) {
$email = trim((string)$hr['email']);
if ($email === '') continue;
if ($onlyEmail !== null && strcasecmp($email, $onlyEmail) !== 0) continue;
$checkMissingSent->execute([$m['employee_id'], $m['topic_id'], $email]);
if ($checkMissingSent->fetchColumn() > 0) {
$skipped++;
continue;
}
try {
$mail = new PHPMailer(true);
$mailer = $_ENV['MAIL_MAILER'] ?? 'mail';
if ($mailer === 'smtp') {
$mail->isSMTP();
$mail->Host = $_ENV['MAIL_HOST'] ?? 'localhost';
$mail->Port = (int)($_ENV['MAIL_PORT'] ?? 587);
if (!empty($_ENV['MAIL_USERNAME']) && $_ENV['MAIL_USERNAME'] !== 'null') {
$mail->SMTPAuth = true;
$mail->Username = $_ENV['MAIL_USERNAME'];
$mail->Password = $_ENV['MAIL_PASSWORD'] ?? '';
}
$enc = $_ENV['MAIL_ENCRYPTION'] ?? '';
if ($enc && $enc !== 'null') {
$mail->SMTPSecure = $enc;
}
}
$mail->CharSet = 'UTF-8';
$mail->setFrom(
$_ENV['MAIL_FROM_ADDRESS'] ?? 'noreply@zibogomma.it',
$_ENV['MAIL_FROM_NAME'] ?? 'Formazione ZIBOGOMMA'
);
$mail->addAddress($email, trim((string)$hr['name']) ?: $email);
$profileUrl = $appUrl . '/userarea/employee-profile.php?id=' . (int)$m['employee_id'] . '#tab-training';
$topicText = $m['topic_name'] . ' — ' . $employeeFullName
. (!empty($m['employee_code']) ? ' (' . $m['employee_code'] . ')' : '');
$mail->Subject = '🔔 Formazione obbligatoria non presente: ' . $m['topic_name'];
$mail->Body = buildHtml(
'Formazione obbligatoria non presente',
$topicText,
'Il dipendente <strong>' . htmlspecialchars($employeeFullName) . '</strong> non ha nessuna registrazione per il corso obbligatorio <strong>' . htmlspecialchars($m['topic_name']) . '</strong>. Programma la prima erogazione.',
'#6b7280',
$profileUrl,
true
);
$mail->isHTML(true);
$mail->AltBody = strip_tags(str_replace('<br>', "\n", $mail->Body));
if ($dryRun) {
echo date('H:i:s') . " ◌ DRY not_present → {$email}{$m['topic_name']} / {$employeeFullName}\n";
$sent++;
continue;
}
$mail->send();
$insertMissingLog->execute([$m['employee_id'], $m['topic_id'], $email]);
$sent++;
echo date('H:i:s') . " ✓ not_present → {$email}{$m['topic_name']} / {$employeeFullName}\n";
} catch (Exception $e) {
$errors++;
echo date('H:i:s') . " ✗ Errore {$email}: {$e->getMessage()}\n";
}
}
}
echo "\n" . date('Y-m-d H:i:s') . " — Completato. Inviate: {$sent}, Saltate: {$skipped}, Errori: {$errors}\n";
// --- HTML email template ---
function buildHtml(string $title, string $topic, string $message, string $accentColor, string $url, bool $isForHr): string
{
$greeting = $isForHr
? 'Una formazione richiede attenzione.'
: 'Una delle tue formazioni richiede attenzione.';
return '
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body style="margin:0;padding:0;background:#f4f6f9;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif">
<table width="100%" cellpadding="0" cellspacing="0" style="padding:30px 0">
<tr><td align="center">
<table width="560" cellpadding="0" cellspacing="0" style="background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.06)">
<tr><td style="background:' . $accentColor . ';padding:20px 30px">
<h1 style="margin:0;color:#fff;font-size:18px">' . htmlspecialchars($title) . '</h1>
</td></tr>
<tr><td style="padding:30px">
<p style="margin:0 0 12px;color:#444;font-size:14px">' . htmlspecialchars($greeting) . '</p>
<h2 style="margin:0 0 15px;color:#2c3e6b;font-size:16px">' . htmlspecialchars($topic) . '</h2>
<p style="margin:0 0 20px;color:#444;font-size:14px;line-height:1.6">' . $message . '</p>
<a href="' . htmlspecialchars($url) . '" style="display:inline-block;background:#5a8fd8;color:#fff;padding:10px 24px;border-radius:6px;text-decoration:none;font-weight:600;font-size:14px">Apri profilo</a>
</td></tr>
<tr><td style="padding:15px 30px;background:#f8f9fb;border-top:1px solid #eee">
<p style="margin:0;color:#999;font-size:11px">ZIBOGOMMA Formazione</p>
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>';
}
+1 -2
View File
@@ -256,7 +256,6 @@ $departments = $stmtDepartments->fetchAll(PDO::FETCH_ASSOC);
<!-- jQuery and Bootstrap --> <!-- jQuery and Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables --> <!-- DataTables -->
@@ -367,7 +366,7 @@ $departments = $stmtDepartments->fetchAll(PDO::FETCH_ASSOC);
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
File diff suppressed because it is too large Load Diff
+163 -64
View File
@@ -17,12 +17,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
try { try {
if ($action === 'add') { if ($action === 'add') {
// Codice originale per add
$employee_code = trim($_POST['employee_code'] ?? ''); $employee_code = trim($_POST['employee_code'] ?? '');
$first_name = trim($_POST['first_name'] ?? ''); $first_name = trim($_POST['first_name'] ?? '');
$last_name = trim($_POST['last_name'] ?? ''); $last_name = trim($_POST['last_name'] ?? '');
$address = trim($_POST['address'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$email = trim($_POST['email'] ?? '');
$department_id = $_POST['department_id'] !== '' ? (int)$_POST['department_id'] : null; $department_id = $_POST['department_id'] !== '' ? (int)$_POST['department_id'] : null;
$position = trim($_POST['position'] ?? ''); $job_role_id = ($_POST['job_role_id'] ?? '') !== '' ? (int)$_POST['job_role_id'] : null;
$hire_date = trim($_POST['hire_date'] ?? ''); $hire_date = trim($_POST['hire_date'] ?? '');
$status = trim($_POST['status'] ?? 'active'); $status = trim($_POST['status'] ?? 'active');
$auth_user_id = $_POST['auth_user_id'] !== '' ? (int)$_POST['auth_user_id'] : null; $auth_user_id = $_POST['auth_user_id'] !== '' ? (int)$_POST['auth_user_id'] : null;
@@ -35,23 +37,31 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
]); ]);
exit; exit;
} }
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo json_encode(['success' => false, 'message' => 'Email non valida.']);
exit;
}
if (!in_array($status, ['active', 'inactive', 'suspended'], true)) { if (!in_array($status, ['active', 'inactive', 'suspended'], true)) {
$status = 'active'; $status = 'active';
} }
$sql = "INSERT INTO employees $sql = "INSERT INTO employees
(auth_user_id, employee_code, first_name, last_name, department_id, position, hire_date, status, created_at, updated_at) (auth_user_id, employee_code, first_name, last_name, address, phone, email,
department_id, job_role_id, hire_date, status, created_at, updated_at)
VALUES VALUES
(:auth_user_id, :employee_code, :first_name, :last_name, :department_id, :position, :hire_date, :status, NOW(), NOW())"; (:auth_user_id, :employee_code, :first_name, :last_name, :address, :phone, :email,
:department_id, :job_role_id, :hire_date, :status, NOW(), NOW())";
$stmt = $pdo->prepare($sql); $stmt = $pdo->prepare($sql);
$stmt->execute([ $stmt->execute([
'auth_user_id' => $auth_user_id, 'auth_user_id' => $auth_user_id,
'employee_code' => $employee_code !== '' ? $employee_code : null, 'employee_code' => $employee_code !== '' ? $employee_code : null,
'first_name' => $first_name, 'first_name' => $first_name,
'last_name' => $last_name, 'last_name' => $last_name,
'address' => $address !== '' ? $address : null,
'phone' => $phone !== '' ? $phone : null,
'email' => $email !== '' ? $email : null,
'department_id' => $department_id, 'department_id' => $department_id,
'position' => $position !== '' ? $position : null, 'job_role_id' => $job_role_id,
'hire_date' => $hire_date !== '' ? $hire_date : null, 'hire_date' => $hire_date !== '' ? $hire_date : null,
'status' => $status 'status' => $status
]); ]);
@@ -74,13 +84,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
} }
if ($action === 'edit') { if ($action === 'edit') {
// Codice originale per edit
$id = (int)($_POST['id'] ?? 0); $id = (int)($_POST['id'] ?? 0);
$employee_code = trim($_POST['employee_code'] ?? ''); $employee_code = trim($_POST['employee_code'] ?? '');
$first_name = trim($_POST['first_name'] ?? ''); $first_name = trim($_POST['first_name'] ?? '');
$last_name = trim($_POST['last_name'] ?? ''); $last_name = trim($_POST['last_name'] ?? '');
$address = trim($_POST['address'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$email = trim($_POST['email'] ?? '');
$department_id = $_POST['department_id'] !== '' ? (int)$_POST['department_id'] : null; $department_id = $_POST['department_id'] !== '' ? (int)$_POST['department_id'] : null;
$position = trim($_POST['position'] ?? ''); $job_role_id = ($_POST['job_role_id'] ?? '') !== '' ? (int)$_POST['job_role_id'] : null;
$hire_date = trim($_POST['hire_date'] ?? ''); $hire_date = trim($_POST['hire_date'] ?? '');
$status = trim($_POST['status'] ?? 'active'); $status = trim($_POST['status'] ?? 'active');
$auth_user_id = $_POST['auth_user_id'] !== '' ? (int)$_POST['auth_user_id'] : null; $auth_user_id = $_POST['auth_user_id'] !== '' ? (int)$_POST['auth_user_id'] : null;
@@ -98,7 +110,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
]); ]);
exit; exit;
} }
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo json_encode(['success' => false, 'message' => 'Email non valida.']);
exit;
}
if (!in_array($status, ['active', 'inactive', 'suspended'], true)) { if (!in_array($status, ['active', 'inactive', 'suspended'], true)) {
$status = 'active'; $status = 'active';
} }
@@ -108,8 +123,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
employee_code = :employee_code, employee_code = :employee_code,
first_name = :first_name, first_name = :first_name,
last_name = :last_name, last_name = :last_name,
address = :address,
phone = :phone,
email = :email,
department_id = :department_id, department_id = :department_id,
position = :position, job_role_id = :job_role_id,
hire_date = :hire_date, hire_date = :hire_date,
status = :status, status = :status,
updated_at = NOW() updated_at = NOW()
@@ -120,8 +138,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
'employee_code' => $employee_code !== '' ? $employee_code : null, 'employee_code' => $employee_code !== '' ? $employee_code : null,
'first_name' => $first_name, 'first_name' => $first_name,
'last_name' => $last_name, 'last_name' => $last_name,
'address' => $address !== '' ? $address : null,
'phone' => $phone !== '' ? $phone : null,
'email' => $email !== '' ? $email : null,
'department_id' => $department_id, 'department_id' => $department_id,
'position' => $position !== '' ? $position : null, 'job_role_id' => $job_role_id,
'hire_date' => $hire_date !== '' ? $hire_date : null, 'hire_date' => $hire_date !== '' ? $hire_date : null,
'status' => $status, 'status' => $status,
'id' => $id 'id' => $id
@@ -223,6 +244,7 @@ $sql = "
SELECT e.*, SELECT e.*,
d.name AS department_name, d.name AS department_name,
d.color AS department_color, d.color AS department_color,
jr.name AS job_role_name,
au.email AS user_email, au.email AS user_email,
au.role_id AS user_role_id, au.role_id AS user_role_id,
ar.display_name AS role_display_name, ar.display_name AS role_display_name,
@@ -230,6 +252,7 @@ $sql = "
CONCAT(COALESCE(au.first_name, ''), ' ', COALESCE(au.last_name, '')) AS user_fullname CONCAT(COALESCE(au.first_name, ''), ' ', COALESCE(au.last_name, '')) AS user_fullname
FROM employees e FROM employees e
LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN job_roles jr ON jr.id = e.job_role_id
LEFT JOIN auth_users au ON e.auth_user_id = au.id LEFT JOIN auth_users au ON e.auth_user_id = au.id
LEFT JOIN auth_roles ar ON ar.id = au.role_id LEFT JOIN auth_roles ar ON ar.id = au.role_id
ORDER BY e.id DESC ORDER BY e.id DESC
@@ -237,6 +260,11 @@ $sql = "
$stmtEmployees = $pdo->query($sql); $stmtEmployees = $pdo->query($sql);
$employees = $stmtEmployees->fetchAll(PDO::FETCH_ASSOC); $employees = $stmtEmployees->fetchAll(PDO::FETCH_ASSOC);
// Job roles for the dropdown
$jobRoles = $pdo->query("
SELECT id, name FROM job_roles WHERE is_active = 1 ORDER BY sort_order, name
")->fetchAll(PDO::FETCH_ASSOC);
// Users list for select // Users list for select
$sqlUsers = " $sqlUsers = "
SELECT id, SELECT id,
@@ -297,7 +325,6 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
<!-- jQuery e Bootstrap --> <!-- jQuery e Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables --> <!-- DataTables -->
@@ -415,7 +442,7 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
@@ -448,14 +475,15 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>ID</th>
<th>Code</th> <th>Codice</th>
<th>Name</th> <th>Nome</th>
<th>Department</th> <th>Contatti</th>
<th>Position</th> <th>Reparto</th>
<th>Hire Date</th> <th>Mansione</th>
<th>Status</th> <th>Data Assunzione</th>
<th>Linked User</th> <th>Stato</th>
<th>Actions</th> <th>Utente collegato</th>
<th>Azioni</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -484,7 +512,24 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
<tr> <tr>
<td><?= (int)$row['id'] ?></td> <td><?= (int)$row['id'] ?></td>
<td><?= htmlspecialchars($row['employee_code'] ?? '') ?></td> <td><?= htmlspecialchars($row['employee_code'] ?? '') ?></td>
<td><?= htmlspecialchars($fullName) ?></td> <td>
<a href="employee-profile.php?id=<?= (int)$row['id'] ?>" class="fw-semibold text-decoration-none">
<?= htmlspecialchars($fullName) ?>
</a>
</td>
<td class="text-start">
<?php if (!empty($row['email'])): ?>
<a href="mailto:<?= htmlspecialchars($row['email'], ENT_QUOTES) ?>" class="text-decoration-none small">
✉️ <?= htmlspecialchars($row['email']) ?>
</a><br>
<?php endif; ?>
<?php if (!empty($row['phone'])): ?>
<a href="tel:<?= htmlspecialchars($row['phone'], ENT_QUOTES) ?>" class="text-decoration-none small">
📞 <?= htmlspecialchars($row['phone']) ?>
</a>
<?php endif; ?>
<?php if (empty($row['email']) && empty($row['phone'])): ?>-<?php endif; ?>
</td>
<td> <td>
<?php if (!empty($row['department_name'])): ?> <?php if (!empty($row['department_name'])): ?>
<span class="department-badge" style="background-color: <?= htmlspecialchars($row['department_color'] ?? '#6c757d', ENT_QUOTES) ?>;"> <span class="department-badge" style="background-color: <?= htmlspecialchars($row['department_color'] ?? '#6c757d', ENT_QUOTES) ?>;">
@@ -494,7 +539,7 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
- -
<?php endif; ?> <?php endif; ?>
</td> </td>
<td><?= htmlspecialchars($row['position'] ?? '') ?></td> <td><?= !empty($row['job_role_name']) ? htmlspecialchars($row['job_role_name']) : '-' ?></td>
<td><?= $hireDate ?></td> <td><?= $hireDate ?></td>
<td> <td>
<span class="badge-status <?= $statusClass ?>"> <span class="badge-status <?= $statusClass ?>">
@@ -510,7 +555,10 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
data-first_name="<?= htmlspecialchars($row['first_name'] ?? '', ENT_QUOTES) ?>" data-first_name="<?= htmlspecialchars($row['first_name'] ?? '', ENT_QUOTES) ?>"
data-last_name="<?= htmlspecialchars($row['last_name'] ?? '', ENT_QUOTES) ?>" data-last_name="<?= htmlspecialchars($row['last_name'] ?? '', ENT_QUOTES) ?>"
data-department_id="<?= $row['department_id'] !== null ? (int)$row['department_id'] : '' ?>" data-department_id="<?= $row['department_id'] !== null ? (int)$row['department_id'] : '' ?>"
data-position="<?= htmlspecialchars($row['position'] ?? '', ENT_QUOTES) ?>" data-job_role_id="<?= $row['job_role_id'] !== null ? (int)$row['job_role_id'] : '' ?>"
data-address="<?= htmlspecialchars($row['address'] ?? '', ENT_QUOTES) ?>"
data-phone="<?= htmlspecialchars($row['phone'] ?? '', ENT_QUOTES) ?>"
data-email="<?= htmlspecialchars($row['email'] ?? '', ENT_QUOTES) ?>"
data-hire_date="<?= htmlspecialchars($row['hire_date'] ?? '', ENT_QUOTES) ?>" data-hire_date="<?= htmlspecialchars($row['hire_date'] ?? '', ENT_QUOTES) ?>"
data-status="<?= htmlspecialchars($status, ENT_QUOTES) ?>" data-status="<?= htmlspecialchars($status, ENT_QUOTES) ?>"
data-auth_user_id="<?= $row['auth_user_id'] !== null ? (int)$row['auth_user_id'] : '' ?>" data-auth_user_id="<?= $row['auth_user_id'] !== null ? (int)$row['auth_user_id'] : '' ?>"
@@ -560,26 +608,42 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
<div class="modal-body"> <div class="modal-body">
<form id="addEmployeeForm"> <form id="addEmployeeForm">
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-semibold">Employee Code</label> <label class="form-label fw-semibold">Codice Dipendente</label>
<input type="text" class="form-control" id="addEmployeeCode" name="employee_code" placeholder="Optional"> <input type="text" class="form-control" id="addEmployeeCode" name="employee_code" placeholder="Opzionale">
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">First Name</label> <label class="form-label fw-semibold">Nome</label>
<input type="text" class="form-control" id="addFirstName" name="first_name" required> <input type="text" class="form-control" id="addFirstName" name="first_name" required>
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Last Name</label> <label class="form-label fw-semibold">Cognome</label>
<input type="text" class="form-control" id="addLastName" name="last_name" required> <input type="text" class="form-control" id="addLastName" name="last_name" required>
</div> </div>
</div> </div>
<div class="mb-3">
<label class="form-label fw-semibold">Indirizzo</label>
<input type="text" class="form-control" id="addAddress" name="address" placeholder="Via, città, CAP">
</div>
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Department</label> <label class="form-label fw-semibold">Telefono</label>
<input type="tel" class="form-control" id="addPhone" name="phone">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Email</label>
<input type="email" class="form-control" id="addEmail" name="email">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Reparto</label>
<select class="form-select" id="addDepartmentId" name="department_id" style="width:100%;"> <select class="form-select" id="addDepartmentId" name="department_id" style="width:100%;">
<option value="">-- Select Department --</option> <option value="">-- Nessuno --</option>
<?php foreach ($departments as $d): ?> <?php foreach ($departments as $d): ?>
<option value="<?= (int)$d['id'] ?>"> <option value="<?= (int)$d['id'] ?>">
<?= htmlspecialchars($d['name']) ?> <?= htmlspecialchars($d['name']) ?>
@@ -589,30 +653,35 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
</select> </select>
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Position</label> <label class="form-label fw-semibold">Mansione</label>
<input type="text" class="form-control" id="addPosition" name="position" placeholder="e.g. Line Operator"> <select class="form-select" id="addJobRoleId" name="job_role_id" style="width:100%;">
<option value="">-- Nessuna --</option>
<?php foreach ($jobRoles as $jr): ?>
<option value="<?= (int)$jr['id'] ?>"><?= htmlspecialchars($jr['name']) ?></option>
<?php endforeach; ?>
</select>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Hire Date</label> <label class="form-label fw-semibold">Data Assunzione</label>
<input type="date" class="form-control" id="addHireDate" name="hire_date"> <input type="date" class="form-control" id="addHireDate" name="hire_date">
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Status</label> <label class="form-label fw-semibold">Stato</label>
<select class="form-select" id="addStatus" name="status"> <select class="form-select" id="addStatus" name="status">
<option value="active" selected>Active</option> <option value="active" selected>Attivo</option>
<option value="inactive">Inactive</option> <option value="inactive">Cessato</option>
<option value="suspended">Suspended</option> <option value="suspended">Sospeso</option>
</select> </select>
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-semibold">Linked User (auth_users)</label> <label class="form-label fw-semibold">Utente collegato (account login)</label>
<select class="form-select" id="addAuthUserId" name="auth_user_id" style="width:100%;"> <select class="form-select" id="addAuthUserId" name="auth_user_id" style="width:100%;">
<option value="">-- None --</option> <option value="">-- Nessuno --</option>
<?php foreach ($users as $u): ?> <?php foreach ($users as $u): ?>
<option value="<?= (int)$u['id'] ?>" data-role_id="<?= (int)$u['role_id'] ?>"> <option value="<?= (int)$u['id'] ?>" data-role_id="<?= (int)$u['role_id'] ?>">
<?= htmlspecialchars($u['label']) ?> <?= htmlspecialchars($u['label']) ?>
@@ -622,16 +691,16 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
</div> </div>
<div class="mb-3 d-none" id="addRoleWrapper"> <div class="mb-3 d-none" id="addRoleWrapper">
<label class="form-label fw-semibold">User Role</label> <label class="form-label fw-semibold">Ruolo di accesso</label>
<select class="form-select" id="addRoleId" name="role_id" style="width:100%;"> <select class="form-select" id="addRoleId" name="role_id" style="width:100%;">
<option value="">-- Select Role --</option> <option value="">-- Seleziona ruolo --</option>
<?php foreach ($roles as $r): ?> <?php foreach ($roles as $r): ?>
<option value="<?= (int)$r['id'] ?>"> <option value="<?= (int)$r['id'] ?>">
<?= htmlspecialchars($r['display_name'] ?: $r['name']) ?> <?= htmlspecialchars($r['display_name'] ?: $r['name']) ?>
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<small class="text-muted">Visible only when an auth user is linked.</small> <small class="text-muted">Visibile solo quando è collegato un utente di sistema.</small>
</div> </div>
<div class="text-center"> <div class="text-center">
@@ -658,26 +727,42 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
<input type="hidden" id="editEmployeeId"> <input type="hidden" id="editEmployeeId">
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-semibold">Employee Code</label> <label class="form-label fw-semibold">Codice Dipendente</label>
<input type="text" class="form-control" id="editEmployeeCode" name="employee_code" placeholder="Optional"> <input type="text" class="form-control" id="editEmployeeCode" name="employee_code" placeholder="Opzionale">
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">First Name</label> <label class="form-label fw-semibold">Nome</label>
<input type="text" class="form-control" id="editFirstName" name="first_name" required> <input type="text" class="form-control" id="editFirstName" name="first_name" required>
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Last Name</label> <label class="form-label fw-semibold">Cognome</label>
<input type="text" class="form-control" id="editLastName" name="last_name" required> <input type="text" class="form-control" id="editLastName" name="last_name" required>
</div> </div>
</div> </div>
<div class="mb-3">
<label class="form-label fw-semibold">Indirizzo</label>
<input type="text" class="form-control" id="editAddress" name="address">
</div>
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Department</label> <label class="form-label fw-semibold">Telefono</label>
<input type="tel" class="form-control" id="editPhone" name="phone">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Email</label>
<input type="email" class="form-control" id="editEmail" name="email">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Reparto</label>
<select class="form-select" id="editDepartmentId" name="department_id" style="width:100%;"> <select class="form-select" id="editDepartmentId" name="department_id" style="width:100%;">
<option value="">-- Select Department --</option> <option value="">-- Nessuno --</option>
<?php foreach ($departments as $d): ?> <?php foreach ($departments as $d): ?>
<option value="<?= (int)$d['id'] ?>"> <option value="<?= (int)$d['id'] ?>">
<?= htmlspecialchars($d['name']) ?> <?= htmlspecialchars($d['name']) ?>
@@ -687,30 +772,35 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
</select> </select>
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Position</label> <label class="form-label fw-semibold">Mansione</label>
<input type="text" class="form-control" id="editPosition" name="position"> <select class="form-select" id="editJobRoleId" name="job_role_id" style="width:100%;">
<option value="">-- Nessuna --</option>
<?php foreach ($jobRoles as $jr): ?>
<option value="<?= (int)$jr['id'] ?>"><?= htmlspecialchars($jr['name']) ?></option>
<?php endforeach; ?>
</select>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Hire Date</label> <label class="form-label fw-semibold">Data Assunzione</label>
<input type="date" class="form-control" id="editHireDate" name="hire_date"> <input type="date" class="form-control" id="editHireDate" name="hire_date">
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Status</label> <label class="form-label fw-semibold">Stato</label>
<select class="form-select" id="editStatus" name="status"> <select class="form-select" id="editStatus" name="status">
<option value="active">Active</option> <option value="active">Attivo</option>
<option value="inactive">Inactive</option> <option value="inactive">Cessato</option>
<option value="suspended">Suspended</option> <option value="suspended">Sospeso</option>
</select> </select>
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-semibold">Linked User (auth_users)</label> <label class="form-label fw-semibold">Utente collegato (account login)</label>
<select class="form-select" id="editAuthUserId" name="auth_user_id" style="width:100%;"> <select class="form-select" id="editAuthUserId" name="auth_user_id" style="width:100%;">
<option value="">-- None --</option> <option value="">-- Nessuno --</option>
<?php foreach ($users as $u): ?> <?php foreach ($users as $u): ?>
<option value="<?= (int)$u['id'] ?>" data-role_id="<?= (int)$u['role_id'] ?>"> <option value="<?= (int)$u['id'] ?>" data-role_id="<?= (int)$u['role_id'] ?>">
<?= htmlspecialchars($u['label']) ?> <?= htmlspecialchars($u['label']) ?>
@@ -720,16 +810,16 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
</div> </div>
<div class="mb-3 d-none" id="editRoleWrapper"> <div class="mb-3 d-none" id="editRoleWrapper">
<label class="form-label fw-semibold">User Role</label> <label class="form-label fw-semibold">Ruolo di accesso</label>
<select class="form-select" id="editRoleId" name="role_id" style="width:100%;"> <select class="form-select" id="editRoleId" name="role_id" style="width:100%;">
<option value="">-- Select Role --</option> <option value="">-- Seleziona ruolo --</option>
<?php foreach ($roles as $r): ?> <?php foreach ($roles as $r): ?>
<option value="<?= (int)$r['id'] ?>"> <option value="<?= (int)$r['id'] ?>">
<?= htmlspecialchars($r['display_name'] ?: $r['name']) ?> <?= htmlspecialchars($r['display_name'] ?: $r['name']) ?>
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<small class="text-muted">Visible only when an auth user is linked.</small> <small class="text-muted">Visibile solo quando è collegato un utente di sistema.</small>
</div> </div>
<div class="text-center"> <div class="text-center">
@@ -784,7 +874,7 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
// Select2 on user selects // Select2 on user selects
$('#addAuthUserId, #editAuthUserId, #addDepartmentId, #editDepartmentId, #addRoleId, #editRoleId').select2({ $('#addAuthUserId, #editAuthUserId, #addDepartmentId, #editDepartmentId, #addRoleId, #editRoleId, #addJobRoleId, #editJobRoleId').select2({
theme: 'bootstrap-5', theme: 'bootstrap-5',
width: '100%' width: '100%'
}); });
@@ -834,8 +924,11 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
payload.append('employee_code', $("#addEmployeeCode").val().trim()); payload.append('employee_code', $("#addEmployeeCode").val().trim());
payload.append('first_name', $("#addFirstName").val().trim()); payload.append('first_name', $("#addFirstName").val().trim());
payload.append('last_name', $("#addLastName").val().trim()); payload.append('last_name', $("#addLastName").val().trim());
payload.append('address', $("#addAddress").val().trim());
payload.append('phone', $("#addPhone").val().trim());
payload.append('email', $("#addEmail").val().trim());
payload.append('department_id', $("#addDepartmentId").val() || ''); payload.append('department_id', $("#addDepartmentId").val() || '');
payload.append('position', $("#addPosition").val().trim()); payload.append('job_role_id', $("#addJobRoleId").val() || '');
payload.append('hire_date', $("#addHireDate").val()); payload.append('hire_date', $("#addHireDate").val());
payload.append('status', $("#addStatus").val()); payload.append('status', $("#addStatus").val());
payload.append('auth_user_id', $("#addAuthUserId").val() || ''); payload.append('auth_user_id', $("#addAuthUserId").val() || '');
@@ -884,7 +977,10 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
$("#editFirstName").val(btn.data("first_name")); $("#editFirstName").val(btn.data("first_name"));
$("#editLastName").val(btn.data("last_name")); $("#editLastName").val(btn.data("last_name"));
$("#editDepartmentId").val(btn.data("department_id") ? String(btn.data("department_id")) : '').trigger('change'); $("#editDepartmentId").val(btn.data("department_id") ? String(btn.data("department_id")) : '').trigger('change');
$("#editPosition").val(btn.data("position")); $("#editJobRoleId").val(btn.data("job_role_id") ? String(btn.data("job_role_id")) : '').trigger('change');
$("#editAddress").val(btn.data("address"));
$("#editPhone").val(btn.data("phone"));
$("#editEmail").val(btn.data("email"));
$("#editHireDate").val(btn.data("hire_date")); $("#editHireDate").val(btn.data("hire_date"));
$("#editStatus").val(btn.data("status")); $("#editStatus").val(btn.data("status"));
@@ -916,8 +1012,11 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
payload.append('employee_code', $("#editEmployeeCode").val().trim()); payload.append('employee_code', $("#editEmployeeCode").val().trim());
payload.append('first_name', $("#editFirstName").val().trim()); payload.append('first_name', $("#editFirstName").val().trim());
payload.append('last_name', $("#editLastName").val().trim()); payload.append('last_name', $("#editLastName").val().trim());
payload.append('address', $("#editAddress").val().trim());
payload.append('phone', $("#editPhone").val().trim());
payload.append('email', $("#editEmail").val().trim());
payload.append('department_id', $("#editDepartmentId").val() || ''); payload.append('department_id', $("#editDepartmentId").val() || '');
payload.append('position', $("#editPosition").val().trim()); payload.append('job_role_id', $("#editJobRoleId").val() || '');
payload.append('hire_date', $("#editHireDate").val()); payload.append('hire_date', $("#editHireDate").val());
payload.append('status', $("#editStatus").val()); payload.append('status', $("#editStatus").val());
payload.append('auth_user_id', $("#editAuthUserId").val() || ''); payload.append('auth_user_id', $("#editAuthUserId").val() || '');
+3 -6
View File
@@ -40,8 +40,7 @@ $kindofrole = $user->present()->role_id;
//$iduserlogin="1"; //$iduserlogin="1";
//$nameuser="Claudio"; //$nameuser="Claudio";
//$emailuser="info@claudiosironi.com"; //$emailuser="info@claudiosironi.com";
?>
<?php
if (session_status() == PHP_SESSION_NONE) { if (session_status() == PHP_SESSION_NONE) {
session_start(); session_start();
} }
@@ -54,13 +53,11 @@ $_SESSION["emailuser"] = $emailuser;
$_SESSION["photouser"] = $avatar; $_SESSION["photouser"] = $avatar;
$photouser = $_SESSION["photouser"]; $photouser = $_SESSION["photouser"];
$photousername = basename($avatar); $photousername = basename($avatar);
?>
//include files
<?php //include files
require_once(__DIR__ . '/../../languages/en/general.php'); require_once(__DIR__ . '/../../languages/en/general.php');
//include("generalsettings.php"); //include("generalsettings.php");
require_once __DIR__ . '/permissions_helper.php';
?> ?>
+319 -50
View File
@@ -6,117 +6,386 @@
<div> <div>
<h4 class="logo-text"><?= htmlspecialchars('ZIBOGOMMA', ENT_QUOTES, 'UTF-8'); ?></h4> <h4 class="logo-text"><?= htmlspecialchars('ZIBOGOMMA', ENT_QUOTES, 'UTF-8'); ?></h4>
</div> </div>
<div class="toggle-icon ms-auto"><i class='bx bx-arrow-back'></i> <div class="toggle-icon ms-auto">
<i class='bx bx-arrow-back'></i>
</div> </div>
</div> </div>
<!--navigation--> <!--navigation-->
<ul class="metismenu" id="menu"> <ul class="metismenu" id="menu">
<!-- user, admin, superuser menù -->
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('User')) || (Auth::user()->hasRole('Superuser'))) : ?> <?php if (userCan('production.dashboard.view')) : ?>
<li> <li>
<a href="production_dashboard.php"> <a href="production_dashboard.php">
<div class="parent-icon"><i class="bx bx-home-alt"></i> <div class="parent-icon">
<i class="bx bx-home-alt"></i>
</div> </div>
<div class="menu-title">Dashboard</div> <div class="menu-title">Dashboard</div>
</a> </a>
</li> </li>
<?php endif; ?>
<?php
$canSeeProgramming =
userCan('production.programming.view')
|| userCan('templates.dashboard.view')
|| userCan('templates.create.view');
?>
<?php if ($canSeeProgramming) : ?>
<li> <li>
<a href="javascript:;" class="has-arrow"> <a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class="bx bx-category"></i> <div class="parent-icon">
<i class="bx bx-category"></i>
</div> </div>
<div class="menu-title">Programmazione</div> <div class="menu-title">Programmazione</div>
</a> </a>
<ul>
<li> <a href="templates_dashboard.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?></a>
</li>
<li> <a href="insert_template_xls.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?></a>
</li>
<ul>
<?php if (userCan('templates.dashboard.view')) : ?>
<li>
<a href="templates_dashboard.php">
<i class='bx bx-radio-circle'></i>
<?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?>
</a>
</li>
<?php endif; ?>
<?php if (userCan('templates.create.view')) : ?>
<li>
<a href="insert_template_xls.php">
<i class='bx bx-radio-circle'></i>
<?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?>
</a>
</li>
<?php endif; ?>
<?php if (userCan('production.programming.view')) : ?>
<li>
<a href="produzione_programmazione_drag.php">
<i class='bx bx-radio-circle'></i>
Programmazione Produzione
</a>
</li>
<?php endif; ?>
</ul> </ul>
</li> </li>
<?php endif; ?>
<?php
$canSeeFunctions =
userCan('masterdata.mescole.view')
|| userCan('masterdata.matrici.view')
|| userCan('masterdata.linee.view')
|| userCan('masterdata.packaging.view')
|| userCan('masterdata.suppliers.view')
|| userCan('masterdata.lookup.view')
|| userCan('masterdata.worksheets.view');
?>
<?php if ($canSeeFunctions) : ?>
<li> <li>
<a href="javascript:;" class="has-arrow"> <a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class="bx bx-category"></i> <div class="parent-icon">
<i class="bx bx-category"></i>
</div> </div>
<div class="menu-title">Funzioni</div> <div class="menu-title">Funzioni</div>
</a> </a>
<ul> <ul>
<?php if (userCan('masterdata.mescole.view')) : ?>
<li> <li>
<a href="mescole.php"><i class='bx bx-radio-circle'></i>Mescole</a> <a href="mescole.php">
</li> <i class='bx bx-radio-circle'></i>Mescole
<li> </a>
<a href="matrici.php"><i class='bx bx-radio-circle'></i>Matrici</a>
</li>
<li>
<a href="linee.php"><i class='bx bx-radio-circle'></i>Linee di produzione</a>
</li> </li>
<?php endif; ?>
<?php if (userCan('masterdata.matrici.view')) : ?>
<li>
<a href="matrici.php">
<i class='bx bx-radio-circle'></i>Matrici
</a>
</li>
<?php endif; ?>
<?php if (userCan('masterdata.linee.view')) : ?>
<li>
<a href="linee.php">
<i class='bx bx-radio-circle'></i>Linee di produzione
</a>
</li>
<?php endif; ?>
<?php if (userCan('masterdata.packaging.view')) : ?>
<li>
<a href="packaging_items.php">
<i class='bx bx-radio-circle'></i>Imballaggi
</a>
</li>
<?php endif; ?>
<?php if (userCan('masterdata.suppliers.view')) : ?>
<li>
<a href="suppliers.php">
<i class='bx bx-radio-circle'></i>Suppliers
</a>
</li>
<?php endif; ?>
<?php if (userCan('masterdata.lookup.view')) : ?>
<li>
<a href="lookup_values.php">
<i class='bx bx-radio-circle'></i>Setup
</a>
</li>
<?php endif; ?>
<?php if (userCan('masterdata.worksheets.view')) : ?>
<li>
<a href="worksheets.php">
<i class='bx bx-radio-circle'></i>Fogli di lavoro
</a>
</li>
<?php endif; ?>
</ul> </ul>
</li> </li>
<?php endif; ?>
<?php
$canSeeProduction =
userCan('production.line_view.view')
|| userCan('production.stats.view')
|| userCan('production.manager.view')
|| userCan('production.manager_stats.view')
|| userCan('warehouse.dashboard.view');
?>
<?php if ($canSeeProduction) : ?>
<li> <li>
<a href="javascript:;" class="has-arrow"> <a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class="bx bx-calendar-check"></i> <div class="parent-icon">
<i class="bx bx-line-chart"></i>
</div>
<div class="menu-title">Produzione</div>
</a>
<ul>
<?php if (userCan('production.line_view.view')) : ?>
<li>
<a href="production_line_view2.php">
<i class='bx bx-radio-circle'></i>Line View
</a>
</li>
<?php endif; ?>
<?php if (userCan('production.stats.view')) : ?>
<li>
<a href="production_stats.php">
<i class='bx bx-radio-circle'></i>Statistiche
</a>
</li>
<?php endif; ?>
<?php if (userCan('production.manager.view')) : ?>
<li>
<a href="manager_produzione.php">
<i class='bx bx-radio-circle'></i>Manager
</a>
</li>
<?php endif; ?>
<?php if (userCan('production.manager_stats.view')) : ?>
<li>
<a href="manager_stats.php">
<i class='bx bx-radio-circle'></i>Manager Stats
</a>
</li>
<?php endif; ?>
<?php if (userCan('warehouse.dashboard.view')) : ?>
<li>
<a href="warehouse_dashboard.php">
<i class='bx bx-radio-circle'></i>Magazzino
</a>
</li>
<?php endif; ?>
</ul>
</li>
<?php endif; ?>
<?php
$canSeeServices =
userCan('services.status.view')
|| userCan('services.pause_reasons.view')
|| userCan('services.tools.view');
?>
<?php if ($canSeeServices) : ?>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon">
<i class="bx bx-wrench"></i>
</div>
<div class="menu-title">Servizi</div>
</a>
<ul>
<?php if (userCan('services.status.view')) : ?>
<li>
<a href="production_status.php">
<i class='bx bx-radio-circle'></i>Status
</a>
</li>
<?php endif; ?>
<?php if (userCan('services.pause_reasons.view')) : ?>
<li>
<a href="production_pause_reasons.php">
<i class='bx bx-radio-circle'></i>Cause di Pausa
</a>
</li>
<?php endif; ?>
<?php if (userCan('services.tools.view')) : ?>
<li>
<a href="production_tools.php">
<i class='bx bx-radio-circle'></i>Attrezzature
</a>
</li>
<?php endif; ?>
</ul>
</li>
<?php endif; ?>
<?php
$canSeeHr =
userCan('hr.employees.view')
|| userCan('hr.departments.view')
|| userCan('hr.job_roles.view')
|| userCan('hr.training_topics.view')
|| userCan('hr.trainings.view')
|| userCan('hr.skills.view');
?>
<?php if ($canSeeHr) : ?>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon">
<i class="bx bx-group"></i>
</div>
<div class="menu-title">Personale</div>
</a>
<ul>
<?php if (userCan('hr.employees.view')) : ?>
<li>
<a href="employees.php">
<i class='bx bx-radio-circle'></i>Dipendenti
</a>
</li>
<?php endif; ?>
<?php if (userCan('hr.departments.view')) : ?>
<li>
<a href="departments.php">
<i class='bx bx-radio-circle'></i>Reparti
</a>
</li>
<?php endif; ?>
<?php if (userCan('hr.job_roles.view')) : ?>
<li>
<a href="job_roles.php">
<i class='bx bx-radio-circle'></i>Mansioni
</a>
</li>
<?php endif; ?>
<?php if (userCan('hr.training_topics.view')) : ?>
<li>
<a href="training_topics.php">
<i class='bx bx-radio-circle'></i>Corsi di Formazione
</a>
</li>
<?php endif; ?>
<?php if (userCan('hr.trainings.view')) : ?>
<li>
<a href="trainings.php">
<i class='bx bx-radio-circle'></i>Storico Formazione
</a>
</li>
<?php endif; ?>
<?php if (userCan('hr.skills.view')) : ?>
<li>
<a href="skills.php">
<i class='bx bx-radio-circle'></i>Skills
</a>
</li>
<?php endif; ?>
</ul>
</li>
<?php endif; ?>
<?php if (userCan('deadlines.view')) : ?>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon">
<i class="bx bx-calendar-check"></i>
</div> </div>
<div class="menu-title">Scadenzario</div> <div class="menu-title">Scadenzario</div>
</a> </a>
<ul> <ul>
<li> <li>
<a href="scadenzario/index.php"><i class='bx bx-radio-circle'></i>Lista Scadenze</a> <a href="scadenzario/index.php">
<i class='bx bx-radio-circle'></i>Lista Scadenze
</a>
</li> </li>
<li> <li>
<a href="scadenzario/calendar.php"><i class='bx bx-radio-circle'></i>Calendario</a> <a href="scadenzario/calendar.php">
<i class='bx bx-radio-circle'></i>Calendario
</a>
</li> </li>
</ul> </ul>
</li> </li>
<?php endif; ?>
<li class="menu-label">Others</li> <li class="menu-label">Others</li>
<li> <li>
<a href="https://helpdesk.cesoft.io" target="_blank"> <a href="https://helpdesk.cesoft.io" target="_blank">
<div class="parent-icon"><i class="bx bx-support"></i> <div class="parent-icon">
<i class="bx bx-support"></i>
</div> </div>
<div class="menu-title">Support</div> <div class="menu-title">Support</div>
</a> </a>
</li> </li>
<?php
endif; ?>
<!-- admin, superuser menù --> <?php if (userCan('users.manage')) : ?>
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('Superuser'))) : ?>
<?php
endif; ?>
<!-- admin menù -->
<?php if (Auth::user()->hasRole('Admin')) : ?>
<li class="menu-label">Admin Menù</li> <li class="menu-label">Admin Menù</li>
<li> <li>
<a href="../" target="_blank"> <a href="../" target="_blank">
<div class="parent-icon"><i class="bx bx-support"></i> <div class="parent-icon">
<i class="bx bx-user-circle"></i>
</div> </div>
<div class="menu-title">User Management</div> <div class="menu-title">User Management</div>
</a> </a>
</li> </li>
<!-- <li> <?php endif; ?>
<a href="template/index.html" target="_blank">
<div class="parent-icon"><i class="bx bx-support"></i>
</div>
<div class="menu-title">Template</div>
</a>
</li>
<li>
<a href="https://codervent.com/rocker/documentation/index.html" target="_blank">
<div class="parent-icon"><i class="bx bx-folder"></i>
</div>
<div class="menu-title">Documentation</div>
</a>
</li> -->
<?php
endif; ?>
</ul> </ul>
<!--end navigation--> <!--end navigation-->
</div> </div>
@@ -0,0 +1,62 @@
<?php
if (!function_exists('userCan')) {
/**
* Check if current user has a Vanguard permission.
* Uses Vanguard native method if available, otherwise falls back to DB check.
*/
function userCan($permissionName)
{
global $kindofrole;
$user = Auth::user();
if (!$user) {
return false;
}
// Vanguard / Laravel-style methods, depending on installed version/customization.
if (method_exists($user, 'hasPermission')) {
return $user->hasPermission($permissionName);
}
if (method_exists($user, 'hasPermissionTo')) {
return $user->hasPermissionTo($permissionName);
}
if (method_exists($user, 'can')) {
return $user->can($permissionName);
}
// Fallback: direct DB check using existing Vanguard tables.
static $permissions = null;
if ($permissions === null) {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->prepare("
SELECT p.name
FROM auth_permissions p
INNER JOIN auth_permission_role pr ON pr.permission_id = p.id
WHERE pr.role_id = ?
");
$stmt->execute([(int)$kindofrole]);
$permissions = $stmt->fetchAll(PDO::FETCH_COLUMN);
}
return in_array($permissionName, $permissions, true);
}
}
if (!function_exists('visibleButtons')) {
/**
* Filter visible buttons.
*/
function visibleButtons(array $buttons)
{
return array_values(array_filter($buttons, function ($button) {
return empty($button['permission']) || userCan($button['permission']);
}));
}
}
+14
View File
@@ -1,3 +1,11 @@
<?php
// Build an absolute URL to employee-profile.php so it works from any depth
// (e.g. /userarea/index.php, /userarea/scadenzario/index.php).
$__scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
$__pos = strpos($__scriptName, '/userarea/');
$__base = $__pos !== false ? substr($__scriptName, 0, $__pos) : '';
$__myProfileHref = $__base . '/userarea/employee-profile.php';
?>
<header> <header>
<div class="topbar d-flex align-items-center"> <div class="topbar d-flex align-items-center">
<nav class="navbar navbar-expand gap-3"> <nav class="navbar navbar-expand gap-3">
@@ -85,6 +93,12 @@
</div> </div>
</a> </a>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item d-flex align-items-center" href="<?= htmlspecialchars($__myProfileHref) ?>"
onclick="event.preventDefault(); window.location.assign(this.href);">
<i class="bx bx-id-card fs-5"></i><span>Il Mio Profilo</span>
</a>
</li>
<li> <li>
<a class="dropdown-item d-flex align-items-center" href="../users"> <a class="dropdown-item d-flex align-items-center" href="../users">
<i class="bx bx-user fs-5"></i><span>Utente</span> <i class="bx bx-user fs-5"></i><span>Utente</span>
@@ -0,0 +1,93 @@
<?php
/**
* Training reminders widget for the production dashboard.
* Visible to HR / manager / Admin / User / Superuser.
*
* Expects $pdo to be set (DBHandlerSelect connection).
*/
if (!isset($pdo)) {
$pdo = DBHandlerSelect::getInstance()->getConnection();
}
$__trWidgetHr = isset($user)
&& ( $user->hasRole('Admin')
|| $user->hasRole('Superuser')
|| $user->hasRole('employee-hr')
|| $user->hasRole('manager'));
if (!$__trWidgetHr) {
return;
}
$__trRows = $pdo->query("
SELECT et.id,
et.next_due_date,
et.reminder_days,
tt.default_reminder_days
FROM employee_trainings et
JOIN training_topics tt ON tt.id = et.training_topic_id
WHERE et.next_due_date IS NOT NULL
")->fetchAll(PDO::FETCH_ASSOC);
$__expiredCount = 0;
$__dueSoonCount = 0;
$__today = new DateTime('today');
foreach ($__trRows as $__r) {
$__rem = $__r['reminder_days'] !== null
? (int)$__r['reminder_days']
: ($__r['default_reminder_days'] !== null ? (int)$__r['default_reminder_days'] : 30);
$__due = DateTime::createFromFormat('Y-m-d', $__r['next_due_date']);
if (!$__due) continue;
$__days = (int)$__today->diff($__due)->format('%r%a');
if ($__days < 0) { $__expiredCount++; }
elseif ($__days <= $__rem) { $__dueSoonCount++; }
}
/* Missing mandatory trainings (status = not_present) */
$__notPresentCount = (int)$pdo->query("
SELECT COUNT(*)
FROM employees e
CROSS JOIN training_topics tt
WHERE tt.is_active = 1 AND tt.is_mandatory = 1
AND NOT EXISTS (
SELECT 1 FROM employee_trainings et
WHERE et.employee_id = e.id AND et.training_topic_id = tt.id
)
")->fetchColumn();
if ($__expiredCount === 0 && $__dueSoonCount === 0 && $__notPresentCount === 0) {
return;
}
?>
<div class="my-deadlines-widgets">
<?php if ($__expiredCount > 0): ?>
<a class="mdw mdw-red" href="trainings.php?status=expired">
<span class="mdw-icon"><i class="fa-solid fa-graduation-cap"></i></span>
<span class="mdw-body">
<span class="mdw-count"><?= (int)$__expiredCount ?></span>
<span class="mdw-label d-block">Formazion<?= $__expiredCount === 1 ? 'e scaduta' : 'i scadute' ?></span>
</span>
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
</a>
<?php endif; ?>
<?php if ($__dueSoonCount > 0): ?>
<a class="mdw mdw-orange" href="trainings.php?status=due_soon">
<span class="mdw-icon"><i class="fa-solid fa-hourglass-half"></i></span>
<span class="mdw-body">
<span class="mdw-count"><?= (int)$__dueSoonCount ?></span>
<span class="mdw-label d-block">Formazion<?= $__dueSoonCount === 1 ? 'e da aggiornare' : 'i da aggiornare' ?></span>
</span>
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
</a>
<?php endif; ?>
<?php if ($__notPresentCount > 0): ?>
<a class="mdw mdw-gray" href="trainings.php?status=not_present">
<span class="mdw-icon"><i class="fa-solid fa-circle-question"></i></span>
<span class="mdw-body">
<span class="mdw-count"><?= (int)$__notPresentCount ?></span>
<span class="mdw-label d-block">Obbligator<?= $__notPresentCount === 1 ? 'ia non presente' : 'ie non presenti' ?></span>
</span>
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
</a>
<?php endif; ?>
</div>
+428
View File
@@ -0,0 +1,428 @@
<?php
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
/* ==========================================
PAGE DATA
========================================== */
$sql = "
SELECT jr.*,
(SELECT COUNT(*) FROM employees e WHERE e.job_role_id = jr.id) AS employees_count
FROM job_roles jr
ORDER BY jr.sort_order ASC, jr.name ASC
";
$jobRoles = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>Gestione Mansioni - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<style>
body { font-size: 1.05rem; background: #f8fafc; }
.card { border-radius: 16px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); }
.back-dashboard {
background-color: #cfe3ff !important; color: #1f2d3d !important;
border: 1px solid #bcd4f4 !important; border-radius: 10px;
font-weight: 600; padding: 10px 18px;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease-in-out;
}
.back-dashboard:hover { background-color: #b9d3ff !important; transform: translateY(-2px); }
.btn-add { background-color: #0d6efd; color: #fff; border-radius: 8px; padding: 10px 20px; font-weight: 500; }
.btn-add:hover { background-color: #0b5ed7; transform: scale(1.02); }
.table thead { background-color: #cfe3ff; color: #1f2d3d; }
.modal-content { border-radius: 16px; }
#tabellaJobRoles thead th { text-align: center; vertical-align: middle; }
.badge-status { padding: 0.25rem 0.6rem; border-radius: 999px; font-size: 0.8rem; font-weight: 600; }
.badge-status.active { background-color: #d1fae5; color: #065f46; }
.badge-status.inactive { background-color: #e5e7eb; color: #374151; }
.description-cell {
max-width: 320px; white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; text-align: left;
}
@media (max-width: 767.98px) {
.card-header { flex-direction: column; align-items: flex-start !important; gap: .5rem; }
.back-dashboard { width: 100%; }
.btn-add { width: 100%; }
}
.jr-card {
border: 1px solid #e2e8f0;
border-radius: 14px;
padding: 14px 16px;
margin-bottom: 12px;
background: #fff;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
}
.jr-card-title {
font-size: 1.1rem;
font-weight: 600;
color: #1f2937;
margin: 0 0 4px 0;
word-break: break-word;
}
.jr-card-desc {
color: #475569;
font-size: 0.95rem;
margin: 0 0 10px 0;
word-break: break-word;
}
.jr-card-meta {
display: flex;
flex-wrap: wrap;
gap: 8px 14px;
font-size: 0.85rem;
color: #64748b;
margin-bottom: 12px;
}
.jr-card-meta b { color: #1f2937; font-weight: 600; }
.jr-card-actions {
display: flex;
gap: 8px;
}
.jr-card-actions .btn {
flex: 1;
}
.jr-empty {
text-align: center;
color: #94a3b8;
padding: 24px 0;
}
</style>
</head>
<body>
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card p-3">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h5 class="mb-0">Gestione Mansioni</h5>
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
↩️ Torna alla Dashboard
</button>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<h6 class="fw-semibold mb-0">Elenco Mansioni / Job Roles</h6>
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addJobRoleModal">
Aggiungi Mansione
</button>
</div>
<!-- DESKTOP / TABLET ≥768px: TABLE -->
<div class="table-responsive d-none d-md-block"><!-- hide on <md -->
<table id="tabellaJobRoles" class="table table-striped align-middle text-center" style="width:100%;">
<thead>
<tr>
<th>ID</th>
<th>Nome</th>
<th>Descrizione</th>
<th>Ordine</th>
<th>Stato</th>
<th>Dipendenti</th>
<th>Creato</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($jobRoles as $row): ?>
<?php
$id = (int)$row['id'];
$name = $row['name'] ?? '';
$description = $row['description'] ?? '';
$sortOrder = (int)($row['sort_order'] ?? 999);
$isActive = (int)($row['is_active'] ?? 1);
$cnt = (int)($row['employees_count'] ?? 0);
$statusClass = $isActive === 1 ? 'active' : 'inactive';
$statusLabel = $isActive === 1 ? 'Attivo' : 'Inattivo';
$createdAt = !empty($row['created_at'])
? date('d/m/Y H:i', strtotime($row['created_at']))
: '-';
?>
<tr>
<td><?= $id ?></td>
<td class="fw-semibold text-start"><?= htmlspecialchars($name) ?></td>
<td class="description-cell" title="<?= htmlspecialchars($description, ENT_QUOTES) ?>">
<?= $description !== '' ? htmlspecialchars($description) : '-' ?>
</td>
<td><?= $sortOrder ?></td>
<td>
<span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span>
</td>
<td><?= $cnt ?></td>
<td><?= $createdAt ?></td>
<td>
<button class="btn btn-sm btn-outline-secondary edit-job-role"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
data-sort_order="<?= $sortOrder ?>"
data-is_active="<?= $isActive ?>">
✏️ Modifica
</button>
<button class="btn btn-sm btn-outline-danger delete-job-role"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
data-count="<?= $cnt ?>">
🗑️ Cancella
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- MOBILE <768px: CARDS -->
<div class="d-block d-md-none">
<?php if (empty($jobRoles)): ?>
<div class="jr-empty">Nessuna mansione presente</div>
<?php endif; ?>
<?php foreach ($jobRoles as $row): ?>
<?php
$id = (int)$row['id'];
$name = $row['name'] ?? '';
$description = $row['description'] ?? '';
$sortOrder = (int)($row['sort_order'] ?? 999);
$isActive = (int)($row['is_active'] ?? 1);
$cnt = (int)($row['employees_count'] ?? 0);
$statusClass = $isActive === 1 ? 'active' : 'inactive';
$statusLabel = $isActive === 1 ? 'Attivo' : 'Inattivo';
?>
<div class="jr-card">
<h6 class="jr-card-title"><?= htmlspecialchars($name) ?></h6>
<?php if ($description !== ''): ?>
<p class="jr-card-desc"><?= htmlspecialchars($description) ?></p>
<?php endif; ?>
<div class="jr-card-meta">
<span><span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span></span>
<span><b>Dipendenti:</b> <?= $cnt ?></span>
<span><b>Ordine:</b> <?= $sortOrder ?></span>
</div>
<div class="jr-card-actions">
<button class="btn btn-sm btn-outline-secondary edit-job-role"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
data-sort_order="<?= $sortOrder ?>"
data-is_active="<?= $isActive ?>">
✏️ Modifica
</button>
<button class="btn btn-sm btn-outline-danger delete-job-role"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
data-count="<?= $cnt ?>">
🗑️ Cancella
</button>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<!-- ADD MODAL -->
<div class="modal fade" id="addJobRoleModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-fullscreen-sm-down">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">Aggiungi Mansione</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addJobRoleForm">
<div class="mb-3">
<label class="form-label fw-semibold">Nome</label>
<input type="text" class="form-control" id="addName" name="name" placeholder="es. Saldatore" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Descrizione</label>
<textarea class="form-control" id="addDescription" name="description" rows="3" placeholder="Opzionale"></textarea>
</div>
<div class="row">
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Ordine</label>
<input type="number" class="form-control" id="addSortOrder" name="sort_order" value="999" min="0">
</div>
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Stato</label>
<select class="form-select" id="addIsActive" name="is_active">
<option value="1" selected>Attivo</option>
<option value="0">Inattivo</option>
</select>
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add">💾 Salva</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- EDIT MODAL -->
<div class="modal fade" id="editJobRoleModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-fullscreen-sm-down">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">Modifica Mansione</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editJobRoleForm">
<input type="hidden" id="editJobRoleId">
<div class="mb-3">
<label class="form-label fw-semibold">Nome</label>
<input type="text" class="form-control" id="editName" name="name" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Descrizione</label>
<textarea class="form-control" id="editDescription" name="description" rows="3"></textarea>
</div>
<div class="row">
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Ordine</label>
<input type="number" class="form-control" id="editSortOrder" name="sort_order" value="999" min="0">
</div>
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Stato</label>
<select class="form-select" id="editIsActive" name="is_active">
<option value="1">Attivo</option>
<option value="0">Inattivo</option>
</select>
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add">💾 Salva Modifiche</button>
</div>
</form>
</div>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
$(document).ready(function() {
$('#tabellaJobRoles').DataTable({
order: [[3, 'asc'], [1, 'asc']],
pageLength: 25,
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json',
emptyTable: 'Nessuna mansione presente'
}
});
function ajaxPost(url, payload, successTitle, errorFallback) {
return fetch(url, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: payload.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({ icon: "success", title: successTitle, confirmButtonColor: "#3085d6" })
.then(() => location.reload());
} else {
Swal.fire({ icon: "error", title: "Errore", text: data.message || errorFallback });
}
})
.catch(err => {
Swal.fire({ icon: "error", title: "Errore", text: "Errore di comunicazione." });
console.error(err);
});
}
$("#addJobRoleForm").on("submit", function(e) {
e.preventDefault();
const p = new URLSearchParams();
p.append('name', $("#addName").val().trim());
p.append('description', $("#addDescription").val().trim());
p.append('sort_order', $("#addSortOrder").val());
p.append('is_active', $("#addIsActive").val());
ajaxPost("ajax/job_roles/save.php", p, "Salvato!", "Impossibile salvare la mansione.");
});
$(document).on("click", ".edit-job-role", function() {
const b = $(this);
$("#editJobRoleId").val(b.data("id"));
$("#editName").val(b.data("name"));
$("#editDescription").val(b.data("description"));
$("#editSortOrder").val(b.data("sort_order"));
$("#editIsActive").val(String(b.data("is_active")));
$("#editJobRoleModal").modal("show");
});
$("#editJobRoleForm").on("submit", function(e) {
e.preventDefault();
const p = new URLSearchParams();
p.append('id', $("#editJobRoleId").val());
p.append('name', $("#editName").val().trim());
p.append('description', $("#editDescription").val().trim());
p.append('sort_order', $("#editSortOrder").val());
p.append('is_active', $("#editIsActive").val());
ajaxPost("ajax/job_roles/save.php", p, "Aggiornato!", "Impossibile aggiornare la mansione.");
});
$(document).on("click", ".delete-job-role", function() {
const id = $(this).data("id");
const name = $(this).data("name");
const cnt = parseInt($(this).data("count")) || 0;
if (cnt > 0) {
Swal.fire({
icon: "warning",
title: "Impossibile cancellare",
text: "La mansione \"" + name + "\" è assegnata a " + cnt + " dipendente/i. Rimuovi prima l'associazione."
});
return;
}
Swal.fire({
title: "Confermi la cancellazione?",
text: name ? ("Mansione: " + name) : "La mansione verrà cancellata.",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#d33",
cancelButtonColor: "#6c757d",
confirmButtonText: "Sì, cancella",
cancelButtonText: "Annulla"
}).then((result) => {
if (!result.isConfirmed) return;
const p = new URLSearchParams();
p.append('id', id);
ajaxPost("ajax/job_roles/delete.php", p, "Cancellato!", "Impossibile cancellare la mansione.");
});
});
});
</script>
</body>
</html>
-1
View File
@@ -42,7 +42,6 @@ $params = $stmtParams->fetchAll(PDO::FETCH_ASSOC);
<!-- jQuery / Bootstrap / SweetAlert --> <!-- jQuery / Bootstrap / SweetAlert -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables --> <!-- DataTables -->
+1 -2
View File
@@ -11,7 +11,6 @@
<!-- jQuery e Bootstrap --> <!-- jQuery e Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables --> <!-- DataTables -->
@@ -118,7 +117,7 @@
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -1
View File
@@ -261,7 +261,7 @@ function h($v)
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -1
View File
@@ -874,7 +874,7 @@ $isEdit = ($worksheet_id > 0);
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -1
View File
@@ -168,7 +168,7 @@ $rows_special = array_filter($rows, function ($r) {
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -1
View File
@@ -551,7 +551,7 @@ function revisionLabel($rev)
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -2
View File
@@ -11,7 +11,6 @@
<!-- jQuery e Bootstrap --> <!-- jQuery e Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables --> <!-- DataTables -->
@@ -138,7 +137,7 @@
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -1
View File
@@ -231,7 +231,7 @@
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -2
View File
@@ -11,7 +11,6 @@
<!-- jQuery + Bootstrap --> <!-- jQuery + Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables --> <!-- DataTables -->
@@ -133,7 +132,7 @@
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
-1
View File
@@ -13,7 +13,6 @@
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Bootstrap (se già incluso puoi rimuoverlo) --> <!-- Bootstrap (se già incluso puoi rimuoverlo) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- SweetAlert2 --> <!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
-1
View File
@@ -13,7 +13,6 @@
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Bootstrap --> <!-- Bootstrap -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- DataTables --> <!-- DataTables -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css"> <link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
+277 -165
View File
@@ -1,4 +1,184 @@
<?php include('include/headscript.php'); ?> <?php include('include/headscript.php'); ?>
<?php
$dashboardSections = [
[
'id' => 'secOperativo',
'title' => 'Operativo',
'subtitle' => 'Azioni principali di produzione e attività in scadenza',
'icon' => '🚀',
'open' => true,
'buttons' => [
[
'label' => 'Programmazione',
'icon' => '🗓️',
'class' => 'btn-programmazione',
'url' => 'produzione_programmazione_drag.php',
'permission' => 'production.programming.view',
],
[
'label' => 'Line View',
'icon' => '⚙️',
'class' => 'btn-status',
'url' => 'production_line_view2.php',
'permission' => 'production.line_view.view',
],
[
'label' => 'Statistiche',
'icon' => '📈',
'class' => 'btn-statistiche',
'url' => 'production_stats.php',
'permission' => 'production.stats.view',
],
[
'label' => 'Manager',
'icon' => '👔',
'class' => 'btn-manager',
'url' => 'manager_produzione.php',
'permission' => 'production.manager.view',
],
[
'label' => 'Manager Stats',
'icon' => '📊',
'class' => 'btn-manager-stats',
'url' => 'manager_stats.php',
'permission' => 'production.manager_stats.view',
],
[
'label' => 'Magazzino',
'icon' => '📦',
'class' => 'btn-magazzino',
'url' => 'warehouse_dashboard.php',
'permission' => 'warehouse.dashboard.view',
],
[
'label' => 'Scadenziario',
'icon' => '⏰',
'class' => 'btn-scadenziario',
'url' => 'scadenzario/index.php',
'permission' => 'deadlines.view',
],
],
],
[
'id' => 'secAnagrafiche',
'title' => 'Anagrafiche',
'subtitle' => 'Dati di base e setup di produzione',
'icon' => '🗂️',
'open' => false,
'buttons' => [
[
'label' => 'Mescole',
'icon' => '⚗️',
'class' => 'btn-mescole',
'url' => 'mescole.php',
'permission' => 'masterdata.mescole.view',
],
[
'label' => 'Elenco Profili',
'icon' => '🧩',
'class' => 'btn-matrici',
'url' => 'matrici.php',
'permission' => 'masterdata.matrici.view',
],
[
'label' => 'Linee Produzione',
'icon' => '🏭',
'class' => 'btn-linee',
'url' => 'linee.php',
'permission' => 'masterdata.linee.view',
],
[
'label' => 'Imballaggi',
'icon' => '📦',
'class' => 'btn-setup',
'url' => 'packaging_items.php',
'permission' => 'masterdata.packaging.view',
],
[
'label' => 'Suppliers',
'icon' => '🏷️',
'class' => 'btn-setup',
'url' => 'suppliers.php',
'permission' => 'masterdata.suppliers.view',
],
[
'label' => 'Setup',
'icon' => '⚙️',
'class' => 'btn-setup',
'url' => 'lookup_values.php',
'permission' => 'masterdata.lookup.view',
],
[
'label' => 'Fogli di lavoro',
'icon' => '🗒️',
'class' => 'btn-setup',
'url' => 'worksheets.php',
'permission' => 'masterdata.worksheets.view',
],
],
],
[
'id' => 'secServizi',
'title' => 'Servizi',
'subtitle' => 'Status, cause pausa, attrezzature',
'icon' => '🧰',
'open' => false,
'buttons' => [
[
'label' => 'Status',
'icon' => '📋',
'class' => 'btn-setup',
'url' => 'production_status.php',
'permission' => 'services.status.view',
],
[
'label' => 'Cause di Pausa',
'icon' => '🛑',
'class' => 'btn-problem',
'url' => 'production_pause_reasons.php',
'permission' => 'services.pause_reasons.view',
],
[
'label' => 'Attrezzature',
'icon' => '🛠️',
'class' => 'btn-tools',
'url' => 'production_tools.php',
'permission' => 'services.tools.view',
],
],
],
[
'id' => 'secPersonale',
'title' => 'Personale',
'subtitle' => 'Dipendenti, skill',
'icon' => '👥',
'open' => false,
'buttons' => [
[
'label' => 'Employees',
'icon' => '👥',
'class' => 'btn-employees',
'url' => 'employees.php',
'permission' => 'hr.employees.view',
],
[
'label' => 'Departments',
'icon' => '🏢',
'class' => 'btn-departments',
'url' => 'departments.php',
'permission' => 'hr.departments.view',
],
[
'label' => 'Skills',
'icon' => '🧠',
'class' => 'btn-setup',
'url' => 'skills.php',
'permission' => 'hr.skills.view',
],
],
],
];
?>
<!doctype html> <!doctype html>
<html lang="it"> <html lang="it">
@@ -298,17 +478,66 @@
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
<div class="page-wrapper"> <div class="page-wrapper">
<div class="page-content"> <div class="page-content">
<?php <?php $pdo = DBHandlerSelect::getInstance()->getConnection(); ?>
$pdo = DBHandlerSelect::getInstance()->getConnection(); <style>
include(__DIR__ . '/scadenzario/include/my_deadlines_widget.php'); .my-deadlines-widgets {
?> display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-bottom: 1rem;
width: 100%;
}
.my-deadlines-widgets:empty { display: none; }
/* Each widget wraps itself in .my-deadlines-widgets; collapse the nested
wrapper so all cards flow into the outer flex (single row). */
.my-deadlines-widgets .my-deadlines-widgets {
display: contents;
}
.my-deadlines-widgets .mdw {
flex: 1 1 0;
min-width: 0;
display: flex; align-items: center; gap: 0.75rem;
padding: 0.8rem 0.9rem;
border-radius: 0.6rem;
text-decoration: none;
color: #fff;
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
transition: transform 0.15s, box-shadow 0.15s;
}
@media (max-width: 991.98px) {
.my-deadlines-widgets .mdw { flex: 1 1 calc(50% - 0.375rem); }
}
@media (max-width: 575.98px) {
.my-deadlines-widgets .mdw { flex: 1 1 100%; }
}
.my-deadlines-widgets .mdw:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); color: #fff; }
.my-deadlines-widgets .mdw-red { background: linear-gradient(135deg, #dc3545 0%, #b02a37 100%); }
.my-deadlines-widgets .mdw-orange { background: linear-gradient(135deg, #e8930c 0%, #c77a00 100%); }
.my-deadlines-widgets .mdw-gray { background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%); }
.my-deadlines-widgets .mdw-icon {
width: 38px; height: 38px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
background: rgba(255,255,255,0.22); font-size: 1.05rem; flex-shrink: 0;
}
.my-deadlines-widgets .mdw-body { flex: 1; line-height: 1.2; min-width: 0; }
.my-deadlines-widgets .mdw-count { font-size: 1.5rem; font-weight: 700; }
.my-deadlines-widgets .mdw-label {
font-size: 0.78rem; opacity: 0.95;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.my-deadlines-widgets .mdw-arrow { opacity: 0.7; font-size: 0.85rem; flex-shrink: 0; }
</style>
<div class="my-deadlines-widgets">
<?php include(__DIR__ . '/scadenzario/include/my_deadlines_widget.php'); ?>
<?php include(__DIR__ . '/include/training_widget.php'); ?>
</div>
<h3 class="dashboard-title">Dashboard Produzione</h3> <h3 class="dashboard-title">Dashboard Produzione</h3>
@@ -347,188 +576,71 @@
<!-- ===== SEZIONI COLLASSABILI ===== --> <!-- ===== SEZIONI COLLASSABILI ===== -->
<div class="sections-wrap" id="prodAccordion"> <div class="sections-wrap" id="prodAccordion">
<!-- OPERATIVO --> <?php
$hasVisibleSections = false;
foreach ($dashboardSections as $section):
$buttons = visibleButtons($section['buttons']);
// If no visible buttons are available, do not show the section.
if (empty($buttons)) {
continue;
}
$hasVisibleSections = true;
$sectionId = htmlspecialchars($section['id'], ENT_QUOTES, 'UTF-8');
$isOpen = !empty($section['open']);
?>
<div class="section-card"> <div class="section-card">
<button type="button" class="section-header" data-bs-toggle="collapse" data-bs-target="#secOperativo" aria-expanded="true" aria-controls="secOperativo"> <button type="button"
class="section-header"
data-bs-toggle="collapse"
data-bs-target="#<?= $sectionId ?>"
aria-expanded="<?= $isOpen ? 'true' : 'false' ?>"
aria-controls="<?= $sectionId ?>">
<div class="section-left"> <div class="section-left">
<div class="section-icon">🚀</div> <div class="section-icon"><?= htmlspecialchars($section['icon'], ENT_QUOTES, 'UTF-8') ?></div>
<div style="min-width:0;"> <div style="min-width:0;">
<p class="section-title">Operativo</p> <p class="section-title"><?= htmlspecialchars($section['title'], ENT_QUOTES, 'UTF-8') ?></p>
<p class="section-subtitle">Azioni principali di produzione e attività in scadenza</p> <p class="section-subtitle"><?= htmlspecialchars($section['subtitle'], ENT_QUOTES, 'UTF-8') ?></p>
</div> </div>
</div> </div>
<div class="chev"></div> <div class="chev"></div>
</button> </button>
<div id="secOperativo" class="collapse show" data-bs-parent="#prodAccordion"> <div id="<?= $sectionId ?>"
class="collapse <?= $isOpen ? 'show' : '' ?>"
data-bs-parent="#prodAccordion">
<div class="section-body"> <div class="section-body">
<div class="dashboard-grid"> <div class="dashboard-grid">
<button class="dash-btn btn-programmazione" onclick="location.href='produzione_programmazione_drag.php'">
<div class="dash-icon">🗓️</div> <?php foreach ($buttons as $button): ?>
<div>Programmazione</div> <button class="dash-btn <?= htmlspecialchars($button['class'], ENT_QUOTES, 'UTF-8') ?>"
onclick="location.href='<?= htmlspecialchars($button['url'], ENT_QUOTES, 'UTF-8') ?>'">
<div class="dash-icon"><?= htmlspecialchars($button['icon'], ENT_QUOTES, 'UTF-8') ?></div>
<div><?= htmlspecialchars($button['label'], ENT_QUOTES, 'UTF-8') ?></div>
</button> </button>
<?php endforeach; ?>
<button class="dash-btn btn-status" onclick="location.href='production_line_view2.php'">
<div class="dash-icon">⚙️</div>
<div>Line View</div>
</button>
<button class="dash-btn btn-statistiche" onclick="location.href='production_stats.php'">
<div class="dash-icon">📈</div>
<div>Statistiche</div>
</button>
<button class="dash-btn btn-manager" onclick="location.href='manager_produzione.php'">
<div class="dash-icon">👔</div>
<div>Manager</div>
</button>
<button class="dash-btn btn-manager-stats" onclick="location.href='manager_stats.php'">
<div class="dash-icon">📊</div>
<div>Manager Stats</div>
</button>
<button class="dash-btn btn-magazzino" onclick="location.href='warehouse_dashboard.php'">
<div class="dash-icon">📦</div>
<div>Magazzino</div>
</button>
<button class="dash-btn btn-scadenziario" onclick="location.href='scadenzario/index.php'">
<div class="dash-icon"></div>
<div>Scadenziario</div>
</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- ANAGRAFICHE --> <?php endforeach; ?>
<?php if (!$hasVisibleSections): ?>
<div class="section-card"> <div class="section-card">
<button type="button" class="section-header" data-bs-toggle="collapse" data-bs-target="#secAnagrafiche" aria-expanded="false" aria-controls="secAnagrafiche"> <div class="section-body text-center">
<div class="section-left"> Nessuna sezione disponibile per il tuo profilo.
<div class="section-icon">🗂️</div>
<div style="min-width:0;">
<p class="section-title">Anagrafiche</p>
<p class="section-subtitle">Dati di base e setup di produzione</p>
</div> </div>
</div> </div>
<div class="chev"></div> <?php endif; ?>
</button>
<div id="secAnagrafiche" class="collapse" data-bs-parent="#prodAccordion">
<div class="section-body">
<div class="dashboard-grid">
<button class="dash-btn btn-mescole" onclick="location.href='mescole.php'">
<div class="dash-icon">⚗️</div>
<div>Mescole</div>
</button>
<button class="dash-btn btn-matrici" onclick="location.href='matrici.php'">
<div class="dash-icon">🧩</div>
<div>Elenco Profili</div>
</button>
<button class="dash-btn btn-linee" onclick="location.href='linee.php'">
<div class="dash-icon">🏭</div>
<div>Linee Produzione</div>
</button>
<button class="dash-btn btn-setup" onclick="location.href='packaging_items.php'">
<div class="dash-icon">📦</div>
<div>Imballaggi</div>
</button>
<button class="dash-btn btn-setup" onclick="location.href='suppliers.php'">
<div class="dash-icon">🏷️</div>
<div>Suppliers</div>
</button>
<button class="dash-btn btn-setup" onclick="location.href='lookup_values.php'">
<div class="dash-icon">⚙️</div>
<div>Setup</div>
</button>
<button class="dash-btn btn-setup" onclick="location.href='worksheets.php'">
<div class="dash-icon">🗒️</div>
<div>Fogli di lavoro</div>
</button>
</div> </div>
</div> <!-- /sections-wrap -->
</div>
</div>
<!-- QUALITÀ / SERVIZI -->
<div class="section-card">
<button type="button" class="section-header" data-bs-toggle="collapse" data-bs-target="#secServizi" aria-expanded="false" aria-controls="secServizi">
<div class="section-left">
<div class="section-icon">🧰</div>
<div style="min-width:0;">
<p class="section-title">Servizi</p>
<p class="section-subtitle">Status, cause pausa, attrezzature</p>
</div>
</div>
<div class="chev"></div>
</button>
<div id="secServizi" class="collapse" data-bs-parent="#prodAccordion">
<div class="section-body">
<div class="dashboard-grid">
<button class="dash-btn btn-setup" onclick="location.href='production_status.php'">
<div class="dash-icon">📋</div>
<div>Status</div>
</button>
<button class="dash-btn btn-problem" onclick="location.href='production_pause_reasons.php'">
<div class="dash-icon">🛑</div>
<div>Cause di Pausa</div>
</button>
<button class="dash-btn btn-tools" onclick="location.href='production_tools.php'">
<div class="dash-icon">🛠️</div>
<div>Attrezzature</div>
</button>
</div>
</div>
</div>
</div>
<!-- PERSONALE -->
<div class="section-card">
<button type="button" class="section-header" data-bs-toggle="collapse" data-bs-target="#secPersonale" aria-expanded="false" aria-controls="secPersonale">
<div class="section-left">
<div class="section-icon">👥</div>
<div style="min-width:0;">
<p class="section-title">Personale</p>
<p class="section-subtitle">Dipendenti, skill</p>
</div>
</div>
<div class="chev"></div>
</button>
<div id="secPersonale" class="collapse" data-bs-parent="#prodAccordion">
<div class="section-body">
<div class="dashboard-grid">
<button class="dash-btn btn-employees" onclick="location.href='employees.php'">
<div class="dash-icon">👥</div>
<div>Employees</div>
</button>
<button class="dash-btn btn-departments" onclick="location.href='departments.php'">
<div class="dash-icon">🏢</div>
<div>Departments</div>
</button>
<button class="dash-btn btn-setup" onclick="location.href='skills.php'">
<div class="dash-icon">🧠</div>
<div>Skills</div>
</button>
</div>
</div>
</div>
</div>
</div> <!-- /sections-wrap -->
</div> </div>
</div> </div>
+1 -1
View File
@@ -1114,7 +1114,7 @@ if (!empty($_GET['ajax'])) {
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -1
View File
@@ -80,7 +80,7 @@
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
@@ -363,7 +363,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['action'])) {
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); <?php include('include/navbar.php');
include('include/topbar.php'); ?> include('include/topbar.php'); ?>
<div class="page-wrapper"> <div class="page-wrapper">
@@ -43,49 +43,64 @@ if (!$_emp || ($_overdue === 0 && $_approaching === 0)) {
} }
?> ?>
<style> <style>
.my-deadlines-widgets { display: flex; gap: 0.75rem; margin-bottom: 1rem; flex-wrap: wrap; } .my-deadlines-widgets {
display: flex; flex-wrap: wrap; gap: 0.75rem;
margin-bottom: 1rem; width: 100%;
}
.my-deadlines-widgets:empty { display: none; }
/* When two widget containers are nested inside an outer .my-deadlines-widgets
(e.g. on the production dashboard), let their children flow into the outer flex. */
.my-deadlines-widgets .my-deadlines-widgets {
display: contents;
}
.my-deadlines-widgets .mdw { .my-deadlines-widgets .mdw {
flex: 1 1 260px; flex: 1 1 0; min-width: 0;
display: flex; align-items: center; gap: 0.9rem; display: flex; align-items: center; gap: 0.75rem;
padding: 0.85rem 1rem; padding: 0.8rem 0.9rem; border-radius: 0.6rem;
border-radius: 0.6rem; text-decoration: none; color: #fff;
text-decoration: none;
color: #fff;
box-shadow: 0 2px 6px rgba(0,0,0,0.08); box-shadow: 0 2px 6px rgba(0,0,0,0.08);
transition: transform 0.15s, box-shadow 0.15s; transition: transform 0.15s, box-shadow 0.15s;
} }
.my-deadlines-widgets .mdw:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); color: #fff; } .my-deadlines-widgets .mdw: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-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-orange { background: linear-gradient(135deg, #e8930c 0%, #c77a00 100%); }
.my-deadlines-widgets .mdw-gray { background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%); }
.my-deadlines-widgets .mdw-icon { .my-deadlines-widgets .mdw-icon {
width: 42px; height: 42px; border-radius: 50%; width: 38px; height: 38px; border-radius: 50%;
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;
background: rgba(255,255,255,0.22); font-size: 1.2rem; flex-shrink: 0; background: rgba(255,255,255,0.22); font-size: 1.05rem; flex-shrink: 0;
}
.my-deadlines-widgets .mdw-body { flex: 1; line-height: 1.2; min-width: 0; }
.my-deadlines-widgets .mdw-count { font-size: 1.5rem; font-weight: 700; }
.my-deadlines-widgets .mdw-label { font-size: 0.78rem; opacity: 0.95;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.my-deadlines-widgets .mdw-arrow { opacity: 0.7; font-size: 0.85rem; flex-shrink: 0; }
@media (max-width: 991.98px) {
.my-deadlines-widgets .mdw { flex: 1 1 calc(50% - 0.375rem); }
}
@media (max-width: 575.98px) {
.my-deadlines-widgets .mdw { flex: 1 1 100%; }
} }
.my-deadlines-widgets .mdw-body { flex: 1; line-height: 1.2; }
.my-deadlines-widgets .mdw-count { font-size: 1.6rem; font-weight: 700; }
.my-deadlines-widgets .mdw-label { font-size: 0.8rem; opacity: 0.95; }
.my-deadlines-widgets .mdw-arrow { opacity: 0.7; font-size: 0.9rem; }
</style> </style>
<div class="my-deadlines-widgets"> <div class="my-deadlines-widgets">
<?php if ($_overdue > 0): ?> <?php if ($_overdue > 0): ?>
<a class="mdw mdw-red" href="scadenzario/index.php?filter_my=1&filter_status=scaduta"> <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-icon"><i class="fa-solid fa-triangle-exclamation"></i></span>
<span class="mdw-body"> <span class="mdw-body">
<span class="mdw-count"><?= $_overdue ?></span> <span class="mdw-count"><?= $_overdue ?></span>
<span class="mdw-label d-block">Scadenz<?= $_overdue === 1 ? 'a' : 'e' ?> scadut<?= $_overdue === 1 ? 'a' : 'e' ?> — <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span> <span class="mdw-label d-block">Scadenz<?= $_overdue === 1 ? 'a' : 'e' ?> scadut<?= $_overdue === 1 ? 'a' : 'e' ?> — <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span>
</span> </span>
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span> <span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
</a> </a>
<?php endif; ?> <?php endif; ?>
<?php if ($_approaching > 0): ?> <?php if ($_approaching > 0): ?>
<a class="mdw mdw-orange" href="scadenzario/index.php?filter_my=1&filter_status=in-scadenza"> <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-icon"><i class="fa-solid fa-clock"></i></span>
<span class="mdw-body"> <span class="mdw-body">
<span class="mdw-count"><?= $_approaching ?></span> <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 class="mdw-label d-block">In scadenza a breve <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span>
</span> </span>
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span> <span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
</a> </a>
<?php endif; ?> <?php endif; ?>
</div> </div>
+1 -1
View File
@@ -208,7 +208,7 @@ while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) {
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -2
View File
@@ -130,7 +130,6 @@ $tools = $pdo->query("
<title>Gestione Skills</title> <title>Gestione Skills</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css"> <link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
@@ -177,7 +176,7 @@ $tools = $pdo->query("
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -2
View File
@@ -11,7 +11,6 @@
<!-- jQuery e Bootstrap --> <!-- jQuery e Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables --> <!-- DataTables -->
@@ -119,7 +118,7 @@
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
-1
View File
@@ -13,7 +13,6 @@
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Bootstrap (se già incluso puoi rimuoverlo) --> <!-- Bootstrap (se già incluso puoi rimuoverlo) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- SweetAlert2 --> <!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
+530
View File
@@ -0,0 +1,530 @@
<?php
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
/* ==========================================
PAGE DATA
========================================== */
$sql = "
SELECT tt.*,
(SELECT COUNT(*) FROM employee_trainings et WHERE et.training_topic_id = tt.id) AS trainings_count
FROM training_topics tt
ORDER BY tt.sort_order ASC, tt.name ASC
";
$topics = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>Gestione Corsi di Formazione - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<style>
body { font-size: 1.05rem; background: #f8fafc; }
.card { border-radius: 16px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); }
.back-dashboard {
background-color: #cfe3ff !important; color: #1f2d3d !important;
border: 1px solid #bcd4f4 !important; border-radius: 10px;
font-weight: 600; padding: 10px 18px;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease-in-out;
}
.back-dashboard:hover { background-color: #b9d3ff !important; transform: translateY(-2px); }
.btn-add { background-color: #0d6efd; color: #fff; border-radius: 8px; padding: 10px 20px; font-weight: 500; }
.btn-add:hover { background-color: #0b5ed7; transform: scale(1.02); }
.table thead { background-color: #cfe3ff; color: #1f2d3d; }
.modal-content { border-radius: 16px; }
#tabellaTopics thead th { text-align: center; vertical-align: middle; }
.badge-status { padding: 0.25rem 0.6rem; border-radius: 999px; font-size: 0.8rem; font-weight: 600; }
.badge-status.active { background-color: #d1fae5; color: #065f46; }
.badge-status.inactive { background-color: #e5e7eb; color: #374151; }
.description-cell {
max-width: 280px; white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; text-align: left;
}
.num-pill {
display: inline-block; padding: 2px 10px; border-radius: 999px;
background: #eef2ff; color: #3730a3; font-weight: 600; font-size: 0.85rem;
}
@media (max-width: 767.98px) {
.card-header { flex-direction: column; align-items: flex-start !important; gap: .5rem; }
.back-dashboard { width: 100%; }
.btn-add { width: 100%; }
}
.tt-card {
border: 1px solid #e2e8f0;
border-radius: 14px;
padding: 14px 16px;
margin-bottom: 12px;
background: #fff;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
}
.tt-card-title {
font-size: 1.1rem;
font-weight: 600;
color: #1f2937;
margin: 0 0 4px 0;
word-break: break-word;
}
.tt-card-desc {
color: #475569;
font-size: 0.95rem;
margin: 0 0 10px 0;
word-break: break-word;
}
.tt-card-meta {
display: flex;
flex-wrap: wrap;
gap: 8px 14px;
font-size: 0.85rem;
color: #64748b;
margin-bottom: 12px;
}
.tt-card-meta b { color: #1f2937; font-weight: 600; }
.tt-card-actions {
display: flex;
gap: 8px;
}
.tt-card-actions .btn { flex: 1; }
.tt-empty {
text-align: center;
color: #94a3b8;
padding: 24px 0;
}
</style>
</head>
<body>
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card p-3">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h5 class="mb-0">Gestione Corsi di Formazione</h5>
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
↩️ Torna alla Dashboard
</button>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<h6 class="fw-semibold mb-0">Elenco Corsi / Training Topics</h6>
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addTopicModal">
Aggiungi Corso
</button>
</div>
<!-- DESKTOP / TABLET ≥768px: TABLE -->
<div class="table-responsive d-none d-md-block">
<table id="tabellaTopics" class="table table-striped align-middle text-center" style="width:100%;">
<thead>
<tr>
<th>ID</th>
<th>Nome</th>
<th>Descrizione</th>
<th>Frequenza<br>(mesi)</th>
<th>Promemoria<br>(giorni)</th>
<th>Ordine</th>
<th>Stato</th>
<th>Formazioni</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($topics as $row): ?>
<?php
$id = (int)$row['id'];
$name = $row['name'] ?? '';
$description = $row['description'] ?? '';
$freq = $row['default_frequency_months'];
$rem = (int)($row['default_reminder_days'] ?? 30);
$sortOrder = (int)($row['sort_order'] ?? 999);
$isActive = (int)($row['is_active'] ?? 1);
$isMandatory = (int)($row['is_mandatory'] ?? 0);
$cnt = (int)($row['trainings_count'] ?? 0);
$statusClass = $isActive === 1 ? 'active' : 'inactive';
$statusLabel = $isActive === 1 ? 'Attivo' : 'Inattivo';
?>
<tr>
<td><?= $id ?></td>
<td class="fw-semibold text-start">
<?= htmlspecialchars($name) ?>
<?php if ($isMandatory === 1): ?>
<span class="badge bg-warning text-dark ms-1" title="Obbligatorio per tutti"> Obbl.</span>
<?php endif; ?>
</td>
<td class="description-cell" title="<?= htmlspecialchars($description, ENT_QUOTES) ?>">
<?= $description !== '' ? htmlspecialchars($description) : '-' ?>
</td>
<td>
<?php if ($freq === null || $freq === ''): ?>
<span class="text-muted">una tantum</span>
<?php else: ?>
<span class="num-pill"><?= (int)$freq ?></span>
<?php endif; ?>
</td>
<td><span class="num-pill"><?= $rem ?></span></td>
<td><?= $sortOrder ?></td>
<td><span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span></td>
<td><?= $cnt ?></td>
<td>
<button class="btn btn-sm btn-outline-secondary edit-topic"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
data-freq="<?= $freq === null ? '' : (int)$freq ?>"
data-rem="<?= $rem ?>"
data-sort_order="<?= $sortOrder ?>"
data-is_active="<?= $isActive ?>"
data-is_mandatory="<?= $isMandatory ?>">
✏️ Modifica
</button>
<button class="btn btn-sm btn-outline-danger delete-topic"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
data-count="<?= $cnt ?>">
🗑️ Cancella
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- MOBILE <768px: CARDS -->
<div class="d-block d-md-none">
<?php if (empty($topics)): ?>
<div class="tt-empty">Nessun corso presente</div>
<?php endif; ?>
<?php foreach ($topics as $row): ?>
<?php
$id = (int)$row['id'];
$name = $row['name'] ?? '';
$description = $row['description'] ?? '';
$freq = $row['default_frequency_months'];
$rem = (int)($row['default_reminder_days'] ?? 30);
$sortOrder = (int)($row['sort_order'] ?? 999);
$isActive = (int)($row['is_active'] ?? 1);
$isMandatory = (int)($row['is_mandatory'] ?? 0);
$cnt = (int)($row['trainings_count'] ?? 0);
$statusClass = $isActive === 1 ? 'active' : 'inactive';
$statusLabel = $isActive === 1 ? 'Attivo' : 'Inattivo';
$freqLabel = ($freq === null || $freq === '') ? 'una tantum' : ((int)$freq . ' mesi');
?>
<div class="tt-card">
<h6 class="tt-card-title">
<?= htmlspecialchars($name) ?>
<?php if ($isMandatory === 1): ?>
<span class="badge bg-warning text-dark ms-1" title="Obbligatorio per tutti"> Obbl.</span>
<?php endif; ?>
</h6>
<?php if ($description !== ''): ?>
<p class="tt-card-desc"><?= htmlspecialchars($description) ?></p>
<?php endif; ?>
<div class="tt-card-meta">
<span><span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span></span>
<span><b>Frequenza:</b> <?= htmlspecialchars($freqLabel) ?></span>
<span><b>Promemoria:</b> <?= $rem ?> gg</span>
<span><b>Formazioni:</b> <?= $cnt ?></span>
<span><b>Ordine:</b> <?= $sortOrder ?></span>
</div>
<div class="tt-card-actions">
<button class="btn btn-sm btn-outline-secondary edit-topic"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
data-freq="<?= $freq === null ? '' : (int)$freq ?>"
data-rem="<?= $rem ?>"
data-sort_order="<?= $sortOrder ?>"
data-is_active="<?= $isActive ?>"
data-is_mandatory="<?= $isMandatory ?>">
✏️ Modifica
</button>
<button class="btn btn-sm btn-outline-danger delete-topic"
data-id="<?= $id ?>"
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
data-count="<?= $cnt ?>">
🗑️ Cancella
</button>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<!-- ADD -->
<div class="modal fade" id="addTopicModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-lg modal-fullscreen-sm-down">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">Aggiungi Corso</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addTopicForm">
<div class="mb-3">
<label class="form-label fw-semibold">Nome</label>
<input type="text" class="form-control" id="addName" name="name" placeholder="es. Sicurezza antincendio" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Descrizione</label>
<textarea class="form-control" id="addDescription" name="description" rows="3" placeholder="Opzionale"></textarea>
</div>
<div class="row">
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Frequenza aggiornamento</label>
<select class="form-select" id="addFreq" name="default_frequency_months">
<option value="" selected>Una tantum (nessun aggiornamento)</option>
<option value="3">3 mesi</option>
<option value="6">6 mesi</option>
<option value="12">12 mesi (1 anno)</option>
<option value="18">18 mesi</option>
<option value="24">24 mesi (2 anni)</option>
<option value="36">36 mesi (3 anni)</option>
<option value="48">48 mesi (4 anni)</option>
<option value="60">60 mesi (5 anni)</option>
</select>
</div>
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Promemoria (giorni prima della scadenza)</label>
<input type="number" class="form-control" id="addRem" name="default_reminder_days" value="30" min="0">
</div>
</div>
<div class="row">
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Ordine</label>
<input type="number" class="form-control" id="addSortOrder" name="sort_order" value="999" min="0">
</div>
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Stato</label>
<select class="form-select" id="addIsActive" name="is_active">
<option value="1" selected>Attivo</option>
<option value="0">Inattivo</option>
</select>
</div>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="addIsMandatory" value="1">
<label class="form-check-label fw-semibold" for="addIsMandatory">
Obbligatorio per tutti i dipendenti
</label>
<div class="small text-muted">
Se attivo, i dipendenti senza registrazione di questo corso compaiono come "Non presente" nello storico.
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add">💾 Salva</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- EDIT -->
<div class="modal fade" id="editTopicModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-lg modal-fullscreen-sm-down">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">Modifica Corso</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editTopicForm">
<input type="hidden" id="editTopicId">
<div class="mb-3">
<label class="form-label fw-semibold">Nome</label>
<input type="text" class="form-control" id="editName" name="name" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Descrizione</label>
<textarea class="form-control" id="editDescription" name="description" rows="3"></textarea>
</div>
<div class="row">
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Frequenza aggiornamento</label>
<select class="form-select" id="editFreq" name="default_frequency_months">
<option value="">Una tantum (nessun aggiornamento)</option>
<option value="3">3 mesi</option>
<option value="6">6 mesi</option>
<option value="12">12 mesi (1 anno)</option>
<option value="18">18 mesi</option>
<option value="24">24 mesi (2 anni)</option>
<option value="36">36 mesi (3 anni)</option>
<option value="48">48 mesi (4 anni)</option>
<option value="60">60 mesi (5 anni)</option>
</select>
</div>
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Promemoria (giorni prima della scadenza)</label>
<input type="number" class="form-control" id="editRem" name="default_reminder_days" min="0">
</div>
</div>
<div class="row">
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Ordine</label>
<input type="number" class="form-control" id="editSortOrder" name="sort_order" min="0">
</div>
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Stato</label>
<select class="form-select" id="editIsActive" name="is_active">
<option value="1">Attivo</option>
<option value="0">Inattivo</option>
</select>
</div>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="editIsMandatory" value="1">
<label class="form-check-label fw-semibold" for="editIsMandatory">
Obbligatorio per tutti i dipendenti
</label>
<div class="small text-muted">
Se attivo, i dipendenti senza registrazione di questo corso compaiono come "Non presente" nello storico.
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add">💾 Salva Modifiche</button>
</div>
</form>
</div>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
$(document).ready(function() {
$('#tabellaTopics').DataTable({
order: [[5, 'asc'], [1, 'asc']],
pageLength: 25,
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json',
emptyTable: 'Nessun corso presente'
}
});
function ajaxPost(url, payload, successTitle, errorFallback) {
return fetch(url, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: payload.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({ icon: "success", title: successTitle, confirmButtonColor: "#3085d6" })
.then(() => location.reload());
} else {
Swal.fire({ icon: "error", title: "Errore", text: data.message || errorFallback });
}
})
.catch(err => {
Swal.fire({ icon: "error", title: "Errore", text: "Errore di comunicazione." });
console.error(err);
});
}
$("#addTopicForm").on("submit", function(e) {
e.preventDefault();
const p = new URLSearchParams();
p.append('name', $("#addName").val().trim());
p.append('description', $("#addDescription").val().trim());
p.append('default_frequency_months', $("#addFreq").val());
p.append('default_reminder_days', $("#addRem").val());
p.append('sort_order', $("#addSortOrder").val());
p.append('is_active', $("#addIsActive").val());
p.append('is_mandatory', $("#addIsMandatory").is(':checked') ? '1' : '0');
ajaxPost("ajax/training_topics/save.php", p, "Salvato!", "Impossibile salvare il corso.");
});
$(document).on("click", ".edit-topic", function() {
const b = $(this);
const rawFreq = b.data("freq");
const freqStr = (rawFreq === '' || rawFreq === null || rawFreq === undefined) ? '' : String(rawFreq);
if (freqStr !== '' && $("#editFreq option[value='" + freqStr + "']").length === 0) {
$("#editFreq").append('<option value="' + freqStr + '">' + freqStr + ' mesi</option>');
}
$("#editTopicId").val(b.data("id"));
$("#editName").val(b.data("name"));
$("#editDescription").val(b.data("description"));
$("#editFreq").val(freqStr);
$("#editRem").val(b.data("rem"));
$("#editSortOrder").val(b.data("sort_order"));
$("#editIsActive").val(String(b.data("is_active")));
$("#editIsMandatory").prop('checked', String(b.data("is_mandatory")) === '1');
$("#editTopicModal").modal("show");
});
$("#editTopicForm").on("submit", function(e) {
e.preventDefault();
const p = new URLSearchParams();
p.append('id', $("#editTopicId").val());
p.append('name', $("#editName").val().trim());
p.append('description', $("#editDescription").val().trim());
p.append('default_frequency_months', $("#editFreq").val());
p.append('default_reminder_days', $("#editRem").val());
p.append('sort_order', $("#editSortOrder").val());
p.append('is_active', $("#editIsActive").val());
p.append('is_mandatory', $("#editIsMandatory").is(':checked') ? '1' : '0');
ajaxPost("ajax/training_topics/save.php", p, "Aggiornato!", "Impossibile aggiornare il corso.");
});
$(document).on("click", ".delete-topic", function() {
const id = $(this).data("id");
const name = $(this).data("name");
const cnt = parseInt($(this).data("count")) || 0;
if (cnt > 0) {
Swal.fire({
icon: "warning",
title: "Impossibile cancellare",
text: "Il corso \"" + name + "\" ha " + cnt + " registrazione/i di formazione. Cancella prima le registrazioni."
});
return;
}
Swal.fire({
title: "Confermi la cancellazione?",
text: name ? ("Corso: " + name) : "Il corso verrà cancellato.",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#d33",
cancelButtonColor: "#6c757d",
confirmButtonText: "Sì, cancella",
cancelButtonText: "Annulla"
}).then((result) => {
if (!result.isConfirmed) return;
const p = new URLSearchParams();
p.append('id', id);
ajaxPost("ajax/training_topics/delete.php", p, "Cancellato!", "Impossibile cancellare il corso.");
});
});
});
</script>
</body>
</html>
+430
View File
@@ -0,0 +1,430 @@
<?php
include('include/headscript.php');
$pdo = DBHandlerSelect::getInstance()->getConnection();
/* ==========================================
PERMISSIONS
========================================== */
$isHrManager = Auth::user()->hasRole('Admin')
|| Auth::user()->hasRole('Superuser')
|| Auth::user()->hasRole('employee-hr')
|| Auth::user()->hasRole('manager');
if (!$isHrManager) {
header('Location: employee-profile.php');
exit;
}
/* ==========================================
FILTERS (from GET)
========================================== */
$fEmployeeId = isset($_GET['employee_id']) && $_GET['employee_id'] !== '' ? (int)$_GET['employee_id'] : 0;
$fTopicId = isset($_GET['topic_id']) && $_GET['topic_id'] !== '' ? (int)$_GET['topic_id'] : 0;
$fStatus = isset($_GET['status']) ? trim($_GET['status']) : '';
$fType = isset($_GET['type']) ? trim($_GET['type']) : '';
$fDepartmentId = isset($_GET['department_id'])&& $_GET['department_id']!== '' ? (int)$_GET['department_id']: 0;
/* ==========================================
LOAD DATA
========================================== */
$where = [];
$params = [];
if ($fEmployeeId > 0) { $where[] = 'et.employee_id = :eid'; $params['eid'] = $fEmployeeId; }
if ($fTopicId > 0) { $where[] = 'et.training_topic_id = :tid'; $params['tid'] = $fTopicId; }
if ($fType !== '' && in_array($fType, ['initial', 'refresher'], true)) {
$where[] = 'et.training_type = :ty';
$params['ty'] = $fType;
}
if ($fDepartmentId > 0) { $where[] = 'e.department_id = :did'; $params['did'] = $fDepartmentId; }
$whereSql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
$stmt = $pdo->prepare("
SELECT et.*,
tt.name AS topic_name,
tt.default_reminder_days AS topic_default_rem,
e.first_name, e.last_name, e.employee_code,
d.name AS department_name, d.color AS department_color,
(SELECT COUNT(*) FROM employee_training_attachments a WHERE a.training_id = et.id) AS attachments_count
FROM employee_trainings et
JOIN training_topics tt ON tt.id = et.training_topic_id
JOIN employees e ON e.id = et.employee_id
LEFT JOIN departments d ON d.id = e.department_id
$whereSql
ORDER BY et.next_due_date IS NULL, et.next_due_date ASC, e.last_name, e.first_name
");
$stmt->execute($params);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
/* Filter by computed status */
function trainingStatus(?string $nextDue, ?int $reminderDays, ?int $topicDefaultRem): array {
if (!$nextDue) {
return ['code' => 'compliant', 'label' => 'Conforme', 'class' => 'success'];
}
$rem = $reminderDays !== null ? $reminderDays : ($topicDefaultRem !== null ? $topicDefaultRem : 30);
$today = new DateTime('today');
$due = DateTime::createFromFormat('Y-m-d', $nextDue);
if (!$due) return ['code' => 'compliant', 'label' => 'Conforme', 'class' => 'success'];
$daysLeft = (int)$today->diff($due)->format('%r%a');
if ($daysLeft < 0) return ['code' => 'expired', 'label' => 'Scaduto', 'class' => 'danger', 'days' => $daysLeft];
if ($daysLeft <= $rem) return ['code' => 'due_soon', 'label' => 'Da aggiornare', 'class' => 'warning', 'days' => $daysLeft];
return ['code' => 'compliant', 'label' => 'Conforme', 'class' => 'success', 'days' => $daysLeft];
}
$filtered = [];
$counters = ['compliant' => 0, 'due_soon' => 0, 'expired' => 0, 'not_present' => 0, 'all' => 0];
foreach ($rows as $r) {
$s = trainingStatus($r['next_due_date'] ?: null,
$r['reminder_days'] !== null ? (int)$r['reminder_days'] : null,
$r['topic_default_rem'] !== null ? (int)$r['topic_default_rem'] : null);
$r['_status'] = $s;
$counters['all']++;
$counters[$s['code']] = ($counters[$s['code']] ?? 0) + 1;
if ($fStatus !== '' && $fStatus !== $s['code']) continue;
$filtered[] = $r;
}
/* ==========================================
"NOT PRESENT" mandatory topics without any record for an employee.
Apply the same filters (employee_id / topic_id / department_id / type=initial).
========================================== */
if ($fType === '' || $fType === 'initial') {
$missingWhere = [];
$missingParams = [];
if ($fEmployeeId > 0) { $missingWhere[] = 'e.id = :eid'; $missingParams['eid'] = $fEmployeeId; }
if ($fTopicId > 0) { $missingWhere[] = 'tt.id = :tid'; $missingParams['tid'] = $fTopicId; }
if ($fDepartmentId > 0) { $missingWhere[] = 'e.department_id = :did'; $missingParams['did'] = $fDepartmentId; }
$missingWhereSql = $missingWhere ? ('AND ' . implode(' AND ', $missingWhere)) : '';
$missingStmt = $pdo->prepare("
SELECT e.id AS employee_id, e.first_name, e.last_name, e.employee_code,
d.name AS department_name, d.color AS department_color,
tt.id AS topic_id, tt.name AS topic_name
FROM employees e
CROSS JOIN training_topics tt
LEFT JOIN departments d ON d.id = e.department_id
WHERE tt.is_active = 1 AND tt.is_mandatory = 1
AND NOT EXISTS (
SELECT 1 FROM employee_trainings et
WHERE et.employee_id = e.id AND et.training_topic_id = tt.id
)
$missingWhereSql
ORDER BY e.last_name, e.first_name, tt.name
");
$missingStmt->execute($missingParams);
$missingRows = $missingStmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($missingRows as $m) {
$counters['all']++;
$counters['not_present']++;
if ($fStatus !== '' && $fStatus !== 'not_present') continue;
$filtered[] = [
'id' => null,
'_virtual' => true,
'employee_id' => $m['employee_id'],
'first_name' => $m['first_name'],
'last_name' => $m['last_name'],
'employee_code' => $m['employee_code'],
'department_name' => $m['department_name'],
'department_color' => $m['department_color'],
'training_topic_id' => $m['topic_id'],
'topic_name' => $m['topic_name'],
'training_type' => null,
'completed_date' => null,
'next_due_date' => null,
'attachments_count' => 0,
'_status' => ['code' => 'not_present', 'label' => 'Non presente', 'class' => 'secondary', 'days' => null],
];
}
}
/* Dropdown data */
$employees = $pdo->query("
SELECT id, first_name, last_name, employee_code
FROM employees
ORDER BY last_name, first_name
")->fetchAll(PDO::FETCH_ASSOC);
$topics = $pdo->query("
SELECT id, name FROM training_topics WHERE is_active = 1 ORDER BY sort_order, name
")->fetchAll(PDO::FETCH_ASSOC);
$departments = $pdo->query("
SELECT id, name, color FROM departments WHERE is_active = 1 ORDER BY sort_order, name
")->fetchAll(PDO::FETCH_ASSOC);
function fmtDate(?string $d): string {
if (!$d || $d === '0000-00-00') return '—';
$ts = strtotime($d);
return $ts ? date('d/m/Y', $ts) : '—';
}
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>Storico Formazione - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style>
body { font-size: 1.05rem; background: #f8fafc; }
.card { border-radius: 16px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); }
.back-dashboard {
background-color: #cfe3ff !important; color: #1f2d3d !important;
border: 1px solid #bcd4f4 !important; border-radius: 10px;
font-weight: 600; padding: 10px 18px;
}
.stat-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; margin-bottom: 20px; }
@media (max-width: 991.98px) { .stat-row { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 575.98px) { .stat-row { grid-template-columns: repeat(2, 1fr); } }
.stat-card {
border-radius: 14px; padding: 14px 16px; text-align: center;
background: #fff; box-shadow: 0 2px 6px rgba(0,0,0,.05);
cursor: pointer; transition: transform .15s;
}
.stat-card:hover { transform: translateY(-2px); }
.stat-card.active { outline: 3px solid #0d6efd; }
.stat-card .stat-num { font-size: 1.8rem; font-weight: 700; line-height: 1; }
.stat-card .stat-label { font-size: 0.85rem; color: #64748b; margin-top: 4px; }
.stat-card.all .stat-num { color: #1f2937; }
.stat-card.compliant .stat-num { color: #16a34a; }
.stat-card.due_soon .stat-num { color: #d97706; }
.stat-card.expired .stat-num { color: #dc2626; }
.stat-card.not_present .stat-num { color: #6b7280; }
.pill { display: inline-block; padding: 3px 10px; border-radius: 999px; font-size: 0.85rem; font-weight: 600; }
.pill-success { background: #d1fae5; color: #065f46; }
.pill-warning { background: #fef3c7; color: #92400e; }
.pill-danger { background: #fee2e2; color: #991b1b; }
.pill-secondary { background: #e5e7eb; color: #374151; }
.pill-role { background: #fff; color: #334155; border: 1px solid #cbd5e1; }
.pill-dept-inline { padding: 2px 8px; }
.tr-card {
border: 1px solid #e2e8f0; border-radius: 14px;
padding: 14px 16px; margin-bottom: 12px;
background: #fff;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
}
.tr-card .name a { color: #1f2937; font-weight: 600; text-decoration: none; }
.tr-card .topic { color: #475569; }
.tr-card .meta { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 0.85rem; color: #64748b; margin-top: 8px; }
.tr-card .meta b { color: #1f2937; font-weight: 600; }
@media (max-width: 767.98px) {
.card-header { flex-direction: column; align-items: flex-start !important; gap: .5rem; }
.back-dashboard { width: 100%; }
}
</style>
</head>
<body>
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card p-3">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h5 class="mb-0">📚 Storico Formazione</h5>
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
↩️ Torna alla Dashboard
</button>
</div>
<div class="card-body">
<!-- COUNTERS -->
<div class="stat-row">
<a class="stat-card all <?= $fStatus === '' ? 'active' : '' ?>" href="?<?= http_build_query(array_filter(['employee_id' => $fEmployeeId, 'topic_id' => $fTopicId, 'type' => $fType, 'department_id' => $fDepartmentId])) ?>">
<div class="stat-num"><?= (int)$counters['all'] ?></div>
<div class="stat-label">Tutte</div>
</a>
<a class="stat-card compliant <?= $fStatus === 'compliant' ? 'active' : '' ?>" href="?<?= http_build_query(array_filter(['status' => 'compliant', 'employee_id' => $fEmployeeId, 'topic_id' => $fTopicId, 'type' => $fType, 'department_id' => $fDepartmentId])) ?>">
<div class="stat-num"><?= (int)($counters['compliant'] ?? 0) ?></div>
<div class="stat-label">Conformi</div>
</a>
<a class="stat-card due_soon <?= $fStatus === 'due_soon' ? 'active' : '' ?>" href="?<?= http_build_query(array_filter(['status' => 'due_soon', 'employee_id' => $fEmployeeId, 'topic_id' => $fTopicId, 'type' => $fType, 'department_id' => $fDepartmentId])) ?>">
<div class="stat-num"><?= (int)($counters['due_soon'] ?? 0) ?></div>
<div class="stat-label">Da aggiornare</div>
</a>
<a class="stat-card expired <?= $fStatus === 'expired' ? 'active' : '' ?>" href="?<?= http_build_query(array_filter(['status' => 'expired', 'employee_id' => $fEmployeeId, 'topic_id' => $fTopicId, 'type' => $fType, 'department_id' => $fDepartmentId])) ?>">
<div class="stat-num"><?= (int)($counters['expired'] ?? 0) ?></div>
<div class="stat-label">Scaduti</div>
</a>
<a class="stat-card not_present <?= $fStatus === 'not_present' ? 'active' : '' ?>" href="?<?= http_build_query(array_filter(['status' => 'not_present', 'employee_id' => $fEmployeeId, 'topic_id' => $fTopicId, 'department_id' => $fDepartmentId])) ?>">
<div class="stat-num"><?= (int)($counters['not_present'] ?? 0) ?></div>
<div class="stat-label">Non presenti</div>
</a>
</div>
<!-- FILTERS -->
<form method="get" class="row g-2 mb-3" id="filtersForm">
<input type="hidden" name="status" value="<?= htmlspecialchars($fStatus, ENT_QUOTES) ?>">
<div class="col-12 col-md-6 col-lg-3">
<label class="form-label small fw-semibold">Dipendente</label>
<select name="employee_id" class="form-select form-select-sm" onchange="this.form.submit()">
<option value=""> Tutti </option>
<?php foreach ($employees as $e): ?>
<option value="<?= (int)$e['id'] ?>" <?= $fEmployeeId === (int)$e['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars(trim($e['first_name'] . ' ' . $e['last_name'])) ?>
<?php if (!empty($e['employee_code'])): ?>(<?= htmlspecialchars($e['employee_code']) ?>)<?php endif; ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-md-6 col-lg-3">
<label class="form-label small fw-semibold">Corso</label>
<select name="topic_id" class="form-select form-select-sm" onchange="this.form.submit()">
<option value=""> Tutti </option>
<?php foreach ($topics as $t): ?>
<option value="<?= (int)$t['id'] ?>" <?= $fTopicId === (int)$t['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($t['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-md-6 col-lg-3">
<label class="form-label small fw-semibold">Reparto</label>
<select name="department_id" class="form-select form-select-sm" onchange="this.form.submit()">
<option value=""> Tutti </option>
<?php foreach ($departments as $d): ?>
<option value="<?= (int)$d['id'] ?>" <?= $fDepartmentId === (int)$d['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($d['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-md-6 col-lg-3">
<label class="form-label small fw-semibold">Tipo</label>
<select name="type" class="form-select form-select-sm" onchange="this.form.submit()">
<option value=""> Tutti </option>
<option value="initial" <?= $fType === 'initial' ? 'selected' : '' ?>>Iniziale</option>
<option value="refresher" <?= $fType === 'refresher' ? 'selected' : '' ?>>Aggiornamento</option>
</select>
</div>
<?php if ($fEmployeeId || $fTopicId || $fDepartmentId || $fType || $fStatus): ?>
<div class="col-12">
<a href="trainings.php" class="btn btn-sm btn-outline-secondary">✖️ Pulisci filtri</a>
</div>
<?php endif; ?>
</form>
<?php if (empty($filtered)): ?>
<div class="text-center text-muted py-4">
Nessuna formazione corrispondente ai filtri.
</div>
<?php else: ?>
<!-- DESKTOP TABLE -->
<div class="table-responsive d-none d-md-block">
<table class="table table-striped align-middle">
<thead style="background-color:#cfe3ff;">
<tr>
<th>Dipendente</th>
<th>Reparto</th>
<th>Corso</th>
<th>Tipo</th>
<th>Completato</th>
<th>Prossimo agg.</th>
<th>Stato</th>
<th>Giorni</th>
</tr>
</thead>
<tbody>
<?php foreach ($filtered as $r): ?>
<?php
$fullName = trim($r['first_name'] . ' ' . $r['last_name']);
$typeLbl = $r['training_type'] === 'refresher' ? 'Aggiornamento' : ($r['training_type'] === 'initial' ? 'Iniziale' : '—');
$days = $r['_status']['days'] ?? null;
?>
<tr>
<td>
<a href="employee-profile.php?id=<?= (int)$r['employee_id'] ?>#tab-training" class="fw-semibold text-decoration-none">
<?= htmlspecialchars($fullName) ?>
</a>
<?php if (!empty($r['employee_code'])): ?>
<div class="small text-muted"><?= htmlspecialchars($r['employee_code']) ?></div>
<?php endif; ?>
</td>
<td>
<?php if (!empty($r['department_name'])): ?>
<span class="pill pill-dept-inline" style="background:<?= htmlspecialchars($r['department_color'] ?? '#e5e7eb', ENT_QUOTES) ?>20; color:<?= htmlspecialchars($r['department_color'] ?? '#374151', ENT_QUOTES) ?>;">
<?= htmlspecialchars($r['department_name']) ?>
</span>
<?php else: ?>—<?php endif; ?>
</td>
<td><?= htmlspecialchars($r['topic_name']) ?></td>
<td><span class="pill pill-role"><?= $typeLbl ?></span></td>
<td><?= fmtDate($r['completed_date']) ?></td>
<td><?= fmtDate($r['next_due_date']) ?></td>
<td><span class="pill pill-<?= $r['_status']['class'] ?>"><?= $r['_status']['label'] ?></span></td>
<td>
<?php if ($days === null): ?>
<?php elseif ($days < 0): ?>
<span class="text-danger fw-semibold"><?= $days ?></span>
<?php else: ?>
+<?= $days ?>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- MOBILE CARDS -->
<div class="d-block d-md-none">
<?php foreach ($filtered as $r): ?>
<?php
$fullName = trim($r['first_name'] . ' ' . $r['last_name']);
$typeLbl = $r['training_type'] === 'refresher' ? 'Aggiornamento' : ($r['training_type'] === 'initial' ? 'Iniziale' : '—');
$days = $r['_status']['days'] ?? null;
?>
<div class="tr-card">
<div class="d-flex justify-content-between align-items-start gap-2 mb-1">
<div class="name">
<a href="employee-profile.php?id=<?= (int)$r['employee_id'] ?>#tab-training">
<?= htmlspecialchars($fullName) ?>
</a>
</div>
<span class="pill pill-<?= $r['_status']['class'] ?>"><?= $r['_status']['label'] ?></span>
</div>
<div class="topic">📖 <?= htmlspecialchars($r['topic_name']) ?></div>
<div class="meta">
<span><b>Tipo:</b> <?= $typeLbl ?></span>
<span><b>Completato:</b> <?= fmtDate($r['completed_date']) ?></span>
<?php if ($r['next_due_date']): ?>
<span><b>Prossimo:</b> <?= fmtDate($r['next_due_date']) ?>
<?php if ($days !== null && $days < 0): ?>
<span class="text-danger fw-semibold">(<?= $days ?>g)</span>
<?php elseif ($days !== null): ?>
(+<?= $days ?>g)
<?php endif; ?>
</span>
<?php endif; ?>
<?php if (!empty($r['department_name'])): ?>
<span><b>Reparto:</b> <?= htmlspecialchars($r['department_name']) ?></span>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
</body>
</html>
+1 -1
View File
@@ -139,7 +139,7 @@
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -2
View File
@@ -11,7 +11,6 @@
<!-- jQuery e Bootstrap --> <!-- jQuery e Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables --> <!-- DataTables -->
@@ -117,7 +116,7 @@
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -2
View File
@@ -11,7 +11,6 @@
<!-- jQuery e Bootstrap --> <!-- jQuery e Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables --> <!-- DataTables -->
@@ -111,7 +110,7 @@
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
+1 -2
View File
@@ -308,7 +308,6 @@ $worksheets = $pdo->query("
<title>Fogli di Lavoro</title> <title>Fogli di Lavoro</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css"> <link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
@@ -454,7 +453,7 @@ $worksheets = $pdo->query("
</head> </head>
<body> <body>
<div class="wrapper toggled"> <div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
-18
View File
@@ -1,18 +0,0 @@
; This file is for unifying the coding style for different editors and IDEs.
; More information at https://editorconfig.org
root = true
[*]
charset = utf-8
indent_size = 4
indent_style = space
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.yml]
indent_size = 2
@@ -1,25 +0,0 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text eol=lf
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.c text
*.h text
# Declare files that will always have CRLF line endings on checkout.
*.sln text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary
*.otf binary
*.eot binary
*.svg binary
*.ttf binary
*.woff binary
*.woff2 binary
*.css linguist-vendored
*.scss linguist-vendored
*.js linguist-vendored
CHANGELOG.md export-ignore
@@ -1,37 +0,0 @@
name: Tests
on: [push, pull_request]
jobs:
tests:
name: PHP ${{ matrix.php }}
runs-on: ubuntu-latest
strategy:
matrix:
php: ['7.3', '7.4', '8.0', '8.1']
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Cache composer
uses: actions/cache@v1
with:
path: ~/.composer/cache/files
key: php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extension-csv: bcmath, ctype, dom, fileinfo, intl, gd, json, mbstring, pdo, pdo_sqlite, openssl, sqlite, xml, zip
coverage: none
- name: Install composer
run: composer install --no-interaction --no-scripts --no-suggest --prefer-source
- name: Execute tests
run: vendor/bin/phpunit
@@ -1,9 +0,0 @@
/.idea
/.history
/.vscode
/tests/databases
/vendor
.DS_Store
.phpunit.result.cache
composer.phar
composer.lock
-4
View File
@@ -1,4 +0,0 @@
preset: psr2
enabled:
- concat_with_spaces
-23
View File
@@ -1,23 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Andreas Lutro
Copyright (c) 2017 Akaunting
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-186
View File
@@ -1,186 +0,0 @@
# Persistent settings package for Laravel
[![Downloads](https://poser.pugx.org/akaunting/laravel-setting/d/total.svg)](https://github.com/akaunting/laravel-setting)
[![StyleCI](https://styleci.io/repos/101231817/shield?style=flat&branch=master)](https://styleci.io/repos/101231817)
[![License](https://poser.pugx.org/akaunting/laravel-setting/license.svg)](LICENSE.md)
This package allows you to save settings in a more persistent way. You can use the database and/or json file to save your settings. You can also override the Laravel config.
* Driver support
* Helper function
* Blade directive
* Override config values
* Encryption
* Custom file, table and columns
* Auto save
* Extra columns
* Cache support
## Getting Started
### 1. Install
Run the following command:
```bash
composer require akaunting/laravel-setting
```
### 2. Register (for Laravel < 5.5)
Register the service provider in `config/app.php`
```php
Akaunting\Setting\Provider::class,
```
Add alias if you want to use the facade.
```php
'Setting' => Akaunting\Setting\Facade::class,
```
### 3. Publish
Publish config file.
```bash
php artisan vendor:publish --tag=setting
```
### 4. Database
Create table for database driver
```bash
php artisan migrate
```
### 5. Configure
You can change the options of your app from `config/setting.php` file
## Usage
You can either use the helper method like `setting('foo')` or the facade `Setting::get('foo')`
### Facade
```php
Setting::get('foo', 'default');
Setting::get('nested.element');
Setting::set('foo', 'bar');
Setting::forget('foo');
$settings = Setting::all();
```
### Helper
```php
setting('foo', 'default');
setting('nested.element');
setting(['foo' => 'bar']);
setting()->forget('foo');
$settings = setting()->all();
```
You can call the `save()` method to save the changes.
### Auto Save
If you enable the `auto_save` option in the config file, settings will be saved automatically every time the application shuts down if anything has been changed.
### Blade Directive
You can get the settings directly in your blade templates using the helper method or the blade directive like `@setting('foo')`
### Override Config Values
You can easily override default config values by adding them to the `override` option in `config/setting.php`, thereby eliminating the need to modify the default config files and also allowing you to change said values during production. Ex:
```php
'override' => [
"app.name" => "app_name",
"app.env" => "app_env",
"mail.driver" => "app_mail_driver",
"mail.host" => "app_mail_host",
],
```
The values on the left corresponds to the respective config value (Ex: config('app.name')) and the value on the right is the name of the `key` in your settings table/json file.
### Encryption
If you like to encrypt the values for a given key, you can pass the key to the `encrypted_keys` option in `config/setting.php` and the rest is automatically handled by using Laravel's built-in encryption facilities. Ex:
```php
'encrypted_keys' => [
"payment.key",
],
```
### JSON Storage
You can modify the path used on run-time using `setting()->setPath($path)`.
### Database Storage
If you want to use the database as settings storage then you should run the `php artisan migrate`. You can modify the table fields from the `create_settings_table` file in the migrations directory.
#### Extra Columns
If you want to store settings for multiple users/clients in the same database you can do so by specifying extra columns:
```php
setting()->setExtraColumns(['user_id' => Auth::user()->id]);
```
where `user_id = x` will now be added to the database query when settings are retrieved, and when new settings are saved, the `user_id` will be populated.
If you need more fine-tuned control over which data gets queried, you can use the `setConstraint` method which takes a closure with two arguments:
- `$query` is the query builder instance
- `$insert` is a boolean telling you whether the query is an insert or not. If it is an insert, you usually don't need to do anything to `$query`.
```php
setting()->setConstraint(function($query, $insert) {
if ($insert) return;
$query->where(/* ... */);
});
```
### Custom Drivers
This package uses the Laravel `Manager` class under the hood, so it's easy to add your own storage driver. All you need to do is extend the abstract `Driver` class, implement the abstract methods and call `setting()->extend`.
```php
class MyDriver extends Akaunting\Setting\Contracts\Driver
{
// ...
}
app('setting.manager')->extend('mydriver', function($app) {
return $app->make('MyDriver');
});
```
## Changelog
Please see [Releases](../../releases) for more information what has changed recently.
## Contributing
Pull requests are more than welcome. You must follow the PSR coding standards.
## Security
If you discover any security related issues, please email security@akaunting.com instead of using the issue tracker.
## Credits
- [Denis Duliçi](https://github.com/denisdulici)
- [All Contributors](../../contributors)
## License
The MIT License (MIT). Please see [LICENSE](LICENSE.md) for more information.
-48
View File
@@ -1,48 +0,0 @@
{
"name": "akaunting/laravel-setting",
"description": "Persistent settings package for Laravel",
"keywords": [
"laravel",
"persistent",
"settings",
"config"
],
"license": "MIT",
"authors": [
{
"name": "Denis Duliçi",
"email": "info@akaunting.com",
"homepage": "https://akaunting.com",
"role": "Developer"
}
],
"require": {
"php": ">=5.5.9",
"laravel/framework": ">=5.3"
},
"require-dev": {
"phpunit/phpunit": ">=4.8",
"mockery/mockery": "0.9.*",
"laravel/framework": ">=5.3"
},
"autoload": {
"psr-4": {
"Akaunting\\Setting\\": "./src"
},
"files": [
"src/helpers.php"
]
},
"extra": {
"laravel": {
"providers": [
"Akaunting\\Setting\\Provider"
],
"aliases": {
"Setting": "Akaunting\\Setting\\Facade"
}
}
},
"minimum-stability": "dev",
"prefer-stable": true
}
-18
View File
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
>
<testsuites>
<testsuite name="Package Test Suite">
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
</phpunit>
-132
View File
@@ -1,132 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Enable / Disable auto save
|--------------------------------------------------------------------------
|
| Auto-save every time the application shuts down
|
*/
'auto_save' => false,
/*
|--------------------------------------------------------------------------
| Cache
|--------------------------------------------------------------------------
|
| Options for caching. Set whether to enable cache, its key, time to live
| in seconds and whether to auto clear after save.
|
*/
'cache' => [
'enabled' => false,
'key' => 'setting',
'ttl' => 3600,
'auto_clear' => true,
],
/*
|--------------------------------------------------------------------------
| Setting driver
|--------------------------------------------------------------------------
|
| Select where to store the settings.
|
| Supported: "database", "json", "memory"
|
*/
'driver' => 'database',
/*
|--------------------------------------------------------------------------
| Database driver
|--------------------------------------------------------------------------
|
| Options for database driver. Enter which connection to use, null means
| the default connection. Set the table and column names.
|
*/
'database' => [
'connection' => null,
'table' => 'settings',
'key' => 'key',
'value' => 'value',
],
/*
|--------------------------------------------------------------------------
| JSON driver
|--------------------------------------------------------------------------
|
| Options for json driver. Enter the full path to the .json file.
|
*/
'json' => [
'path' => storage_path() . '/settings.json',
],
/*
|--------------------------------------------------------------------------
| Override application config values
|--------------------------------------------------------------------------
|
| If defined, settings package will override these config values.
|
| Sample:
| "app.locale" => "settings.locale",
|
*/
'override' => [
],
/*
|--------------------------------------------------------------------------
| Fallback
|--------------------------------------------------------------------------
|
| Define fallback settings to be used in case the default is null
|
| Sample:
| "currency" => "USD",
|
*/
'fallback' => [
],
/*
|--------------------------------------------------------------------------
| Required Extra Columns
|--------------------------------------------------------------------------
|
| The list of columns required to be set up
|
| Sample:
| "user_id",
| "tenant_id",
|
*/
'required_extra_columns' => [
],
/*
|--------------------------------------------------------------------------
| Encryption
|--------------------------------------------------------------------------
|
| Define the keys which should be crypt automatically.
|
| Sample:
| "payment.key"
|
*/
'encrypted_keys' => [
],
];
@@ -1,321 +0,0 @@
<?php
namespace Akaunting\Setting\Contracts;
use Akaunting\Setting\Support\Arr;
use Illuminate\Support\Facades\Cache;
abstract class Driver
{
/**
* The settings data.
*
* @var array
*/
protected $data = [];
/**
* Whether the store has changed since it was last loaded.
*
* @var bool
*/
protected $unsaved = false;
/**
* Whether the settings data are loaded.
*
* @var bool
*/
protected $loaded = false;
/**
* Include and merge with fallbacks
*
* @var bool
*/
protected $with_fallback = true;
/**
* Excludes fallback data
*/
public function withoutFallback()
{
$this->with_fallback = false;
return $this;
}
/**
* Get a specific key from the settings data.
*
* @param string|array $key
* @param mixed $default Optional default value.
*
* @return mixed
*/
public function get($key, $default = null)
{
if (!$this->checkExtraColumns()) {
return false;
}
$this->load();
return Arr::get($this->data, $key, $default);
}
/**
* Get the fallback value if default is null.
*
* @param string|array $key
* @param mixed $default
*
* @return mixed
*/
public function getFallback($key, $default = null)
{
if (($default !== null) || is_array($key)) {
return $default;
}
return Arr::get((array) config('setting.fallback'), $key);
}
/**
* Check if the given value is same as fallback.
*
* @param string $key
* @param string $value
*
* @return bool
*/
public function isEqualToFallback($key, $value)
{
return (string) $this->getFallback($key) == (string) $value;
}
/**
* Determine if a key exists in the settings data.
*
* @param string $key
*
* @return bool
*/
public function has($key)
{
if (!$this->checkExtraColumns()) {
return false;
}
$this->load();
return Arr::has($this->data, $key);
}
/**
* Set a specific key to a value in the settings data.
*
* @param string|array $key Key string or associative array of key => value
* @param mixed $value Optional only if the first argument is an array
*/
public function set($key, $value = null)
{
if (!$this->checkExtraColumns()) {
return;
}
$this->load();
$this->unsaved = true;
if (is_array($key)) {
foreach ($key as $k => $v) {
Arr::set($this->data, $k, $v);
}
} else {
Arr::set($this->data, $key, $value);
}
}
/**
* Unset a key in the settings data.
*
* @param string $key
*/
public function forget($key)
{
if (!$this->checkExtraColumns()) {
return;
}
$this->unsaved = true;
if ($this->has($key)) {
Arr::forget($this->data, $key);
}
}
/**
* Unset all keys in the settings data.
*
* @return void
*/
public function forgetAll()
{
if (!$this->checkExtraColumns()) {
return;
}
if (config('setting.cache.enabled')) {
Cache::forget($this->getCacheKey());
}
$this->unsaved = true;
$this->data = [];
}
/**
* Get all settings data.
*
* @return array|bool
*/
public function all()
{
if (!$this->checkExtraColumns()) {
return [];
}
$this->load();
return $this->data;
}
/**
* Save any changes done to the settings data.
*
* @return void
*/
public function save()
{
if (!$this->checkExtraColumns()) {
return;
}
if (!$this->unsaved) {
// either nothing has been changed, or data has not been loaded, so
// do nothing by returning early
return;
}
if (config('setting.cache.enabled') && config('setting.cache.auto_clear')) {
Cache::forget($this->getCacheKey());
}
$this->write($this->data);
$this->unsaved = false;
}
/**
* Make sure data is loaded.
*
* @param $force Force a reload of data. Default false.
*/
public function load($force = false)
{
if (!$this->checkExtraColumns()) {
return;
}
if ($this->loaded && !$force) {
return;
}
$fallback_data = $this->with_fallback ? config('setting.fallback') : [];
$driver_data = $this->readData();
$this->data = Arr::merge((array) $fallback_data, (array) $driver_data);
$this->loaded = true;
}
/**
* Read data from driver or cache
*
* @return array
*/
public function readData()
{
if (config('setting.cache.enabled')) {
return $this->readDataFromCache();
}
return $this->read();
}
/**
* Read data from cache
*
* @return array
*/
public function readDataFromCache()
{
return Cache::remember($this->getCacheKey(), config('setting.cache.ttl'), function () {
return $this->read();
});
}
/**
* Check if extra columns are set up.
*
* @return boolean
*/
public function checkExtraColumns()
{
if (!$required_extra_columns = config('setting.required_extra_columns')) {
return true;
}
if (array_keys_exists($required_extra_columns, $this->getExtraColumns())) {
return true;
}
return false;
}
/**
* Get cache key based on extra columns.
*
* @return string
*/
public function getCacheKey()
{
$key = config('setting.cache.key');
foreach ($this->getExtraColumns() as $name => $value) {
$key .= '_' . $name . '_' . $value;
}
return $key;
}
/**
* Get extra columns added to the rows.
*
* @return array
*/
abstract protected function getExtraColumns();
/**
* Read data from driver.
*
* @return array
*/
abstract protected function read();
/**
* Write data to driver.
*
* @param array $data
*
* @return void
*/
abstract protected function write(array $data);
}
@@ -1,372 +0,0 @@
<?php
namespace Akaunting\Setting\Drivers;
use Akaunting\Setting\Contracts\Driver;
use Akaunting\Setting\Support\Arr;
use Closure;
use Illuminate\Database\Connection;
use Illuminate\Support\Arr as LaravelArr;
use Illuminate\Support\Facades\Crypt;
class Database extends Driver
{
/**
* The database connection instance.
*
* @var \Illuminate\Database\Connection
*/
protected $connection;
/**
* The table to query from.
*
* @var string
*/
protected $table;
/**
* The key column name to query from.
*
* @var string
*/
protected $key;
/**
* The value column name to query from.
*
* @var string
*/
protected $value;
/**
* Keys which should be encrypt automatically.
*
* @var array
*/
protected $encrypted_keys;
/**
* Any query constraints that should be applied.
*
* @var Closure|null
*/
protected $query_constraint;
/**
* Any extra columns that should be added to the rows.
*
* @var array
*/
protected $extra_columns = [];
/**
* @param \Illuminate\Database\Connection $connection
* @param string $table
*/
public function __construct(Connection $connection, $table = null, $key = null, $value = null, array $encrypted_keys = [])
{
$this->connection = $connection;
$this->table = $table ?: 'settings';
$this->key = $key ?: 'key';
$this->value = $value ?: 'value';
$this->encrypted_keys = $encrypted_keys;
}
/**
* Set the table to query from.
*
* @param string $table
*/
public function setTable($table)
{
$this->table = $table;
}
/**
* Set the key column name to query from.
*
* @param string $key
*/
public function setKey($key)
{
$this->key = $key;
}
/**
* Set the value column name to query from.
*
* @param string $value
*/
public function setValue($value)
{
$this->value = $value;
}
/**
* Set the query constraint.
*
* @param Closure $callback
*/
public function setConstraint(Closure $callback)
{
$this->data = [];
$this->loaded = false;
$this->query_constraint = $callback;
}
/**
* Set extra columns to be added to the rows.
*
* @param array $columns
*/
public function setExtraColumns(array $columns)
{
$this->extra_columns = $columns;
}
/**
* Get extra columns added to the rows.
*
* @return array
*/
public function getExtraColumns()
{
return $this->extra_columns;
}
/**
* {@inheritdoc}
*/
public function forget($key)
{
parent::forget($key);
// because the database driver cannot store empty arrays, remove empty
// arrays to keep data consistent before and after saving
$segments = explode('.', $key);
array_pop($segments);
while ($segments) {
$segment = implode('.', $segments);
// non-empty array - exit out of the loop
if ($this->get($segment)) {
break;
}
// remove the empty array and move on to the next segment
$this->forget($segment);
array_pop($segments);
}
}
/**
* {@inheritdoc}
*/
protected function write(array $data)
{
// Get current data
$db_data = $this->newQuery()->get([$this->key, $this->value])->toArray();
$insert_data = LaravelArr::dot($data);
$update_data = [];
$delete_keys = [];
foreach ($db_data as $db_row) {
$key = $db_row->{$this->key};
$value = $db_row->{$this->value};
$is_in_insert = $is_different_in_db = $is_same_as_fallback = false;
if (isset($insert_data[$key])) {
$is_in_insert = true;
$is_different_in_db = (string) $insert_data[$key] != (string) $value;
$is_same_as_fallback = $this->isEqualToFallback($key, $insert_data[$key]);
}
if ($is_in_insert) {
if ($is_same_as_fallback) {
// Delete if new data is same as fallback
$delete_keys[] = $key;
} elseif ($is_different_in_db) {
// Update if new data is different from db
$update_data[$key] = $insert_data[$key];
}
} else {
// Delete if current db not available in new data
$delete_keys[] = $key;
}
unset($insert_data[$key]);
}
foreach ($update_data as $key => $value) {
$value = $this->prepareValue($key, $value);
$this->newQuery()
->where($this->key, '=', $key)
->update([$this->value => $value]);
}
if ($insert_data) {
$this->newQuery(true)
->insert($this->prepareInsertData($insert_data));
}
if ($delete_keys) {
$this->newQuery()
->whereIn($this->key, $delete_keys)
->delete();
}
}
/**
* Transforms settings data into an array ready to be insterted into the
* database. Call array_dot on a multidimensional array before passing it
* into this method!
*
* @param array $data Call array_dot on a multidimensional array before passing it into this method!
*
* @return array
*/
protected function prepareInsertData(array $data)
{
$db_data = [];
if ($this->getExtraColumns()) {
foreach ($data as $key => $value) {
$value = $this->prepareValue($key, $value);
// Don't insert if same as fallback
if ($this->isEqualToFallback($key, $value)) {
continue;
}
$db_data[] = array_merge(
$this->getExtraColumns(),
[$this->key => $key, $this->value => $value]
);
}
} else {
foreach ($data as $key => $value) {
$value = $this->prepareValue($key, $value);
// Don't insert if same as fallback
if ($this->isEqualToFallback($key, $value)) {
continue;
}
$db_data[] = [$this->key => $key, $this->value => $value];
}
}
return $db_data;
}
/**
* Checks if the provided key should be encrypted or not.
* Also type casts the given value to a string so errors with booleans or integers are handeled.
* Otherwise it returns the original value.
*
* @param string $key Key to check if it's inside the encryptedValues variable.
* @param mixed $value Info: Encryption only supports strings.
*
* @return string
*/
protected function prepareValue(string $key, $value)
{
// Check if key should be encrypted
if (in_array($key, $this->encrypted_keys)) {
// Cast to string to avoid error when a user passes a boolean value
return Crypt::encryptString((string) $value);
}
return $value;
}
/**
* Checks if the provided key should be decrypted or not.
* Otherwise it returns the original value.
*
* @param string $key Key to check if it's inside the encryptedValues variable.
* @param mixed $value Info: Encryption only supports strings.
*
* @return string
*/
protected function unpackValue(string $key, $value)
{
// Check if key should be encrypted
if (in_array($key, $this->encrypted_keys)) {
// Cast to string to avoid error when a user passes a boolean value
return Crypt::decryptString((string) $value);
}
return $value;
}
/**
* {@inheritdoc}
*/
protected function read()
{
return $this->parseReadData($this->newQuery()->get());
}
/**
* Parse data coming from the database.
*
* @param array $data
*
* @return array
*/
public function parseReadData($data)
{
$results = [];
foreach ($data as $row) {
if (is_array($row)) {
$key = $row[$this->key];
$value = $row[$this->value];
} elseif (is_object($row)) {
$key = $row->{$this->key};
$value = $row->{$this->value};
} else {
$msg = 'Expected array or object, got ' . gettype($row);
throw new \UnexpectedValueException($msg);
}
// Encryption
$value = $this->unpackValue($key, $value);
Arr::set($results, $key, $value);
}
return $results;
}
/**
* Create a new query builder instance.
*
* @param bool $insert
*
* @return \Illuminate\Database\Query\Builder
*/
protected function newQuery($insert = false)
{
$query = $this->connection->table($this->table);
if (!$insert) {
foreach ($this->getExtraColumns() as $key => $value) {
$query->where($key, '=', $value);
}
}
if ($this->query_constraint !== null) {
$callback = $this->query_constraint;
$callback($query, $insert);
}
return $query;
}
}
-80
View File
@@ -1,80 +0,0 @@
<?php
namespace Akaunting\Setting\Drivers;
use Akaunting\Setting\Contracts\Driver;
use Illuminate\Filesystem\Filesystem;
class Json extends Driver
{
/**
* @param \Illuminate\Filesystem\Filesystem $files
* @param string $path
*/
public function __construct(Filesystem $files, $path = null)
{
$this->files = $files;
$this->setPath($path ?: storage_path() . '/settings.json');
}
/**
* Set the path for the JSON file.
*
* @param string $path
*/
public function setPath($path)
{
// If the file does not already exist, we will attempt to create it.
if (!$this->files->exists($path)) {
$result = $this->files->put($path, '{}');
if ($result === false) {
throw new \InvalidArgumentException("Could not write to $path.");
}
}
if (!$this->files->isWritable($path)) {
throw new \InvalidArgumentException("$path is not writable.");
}
$this->path = $path;
}
/**
* {@inheritdoc}
*/
protected function getExtraColumns()
{
return [];
}
/**
* {@inheritdoc}
*/
protected function read()
{
$contents = $this->files->get($this->path);
$data = json_decode($contents, true);
if ($data === null) {
throw new \RuntimeException("Invalid JSON in {$this->path}");
}
return $data;
}
/**
* {@inheritdoc}
*/
protected function write(array $data)
{
if ($data) {
$contents = json_encode($data);
} else {
$contents = '{}';
}
$this->files->put($this->path, $contents);
}
}
-42
View File
@@ -1,42 +0,0 @@
<?php
namespace Akaunting\Setting\Drivers;
use Akaunting\Setting\Contracts\Driver;
class Memory extends Driver
{
/**
* @param array $data
*/
public function __construct(array $data = null)
{
if ($data) {
$this->data = $data;
}
}
/**
* {@inheritdoc}
*/
protected function getExtraColumns()
{
return [];
}
/**
* {@inheritdoc}
*/
protected function read()
{
return $this->data;
}
/**
* {@inheritdoc}
*/
protected function write(array $data)
{
// do nothing
}
}
-16
View File
@@ -1,16 +0,0 @@
<?php
namespace Akaunting\Setting;
use Illuminate\Support\Facades\Facade as BaseFacade;
class Facade extends BaseFacade
{
/**
* Get the registered name of the component.
*/
public static function getFacadeAccessor()
{
return 'setting';
}
}
-63
View File
@@ -1,63 +0,0 @@
<?php
namespace Akaunting\Setting;
use Akaunting\Setting\Drivers\Database;
use Akaunting\Setting\Drivers\Json;
use Akaunting\Setting\Drivers\Memory;
use Illuminate\Support\Manager as BaseManager;
class Manager extends BaseManager
{
/**
* The container instance.
*
* @var \Illuminate\Contracts\Container\Container
*/
protected $container;
/**
* The application instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
*/
public function __construct($app = null)
{
$this->container = $app ?? app();
parent::__construct($this->container);
}
public function getDefaultDriver()
{
return config('setting.driver');
}
public function createJsonDriver()
{
$path = config('setting.json.path');
return new Json($this->container['files'], $path);
}
public function createDatabaseDriver()
{
$connection = $this->container['db']->connection(config('setting.database.connection'));
$table = config('setting.database.table');
$key = config('setting.database.key');
$value = config('setting.database.value');
$encryptedKeys = config('setting.encrypted_keys');
return new Database($connection, $table, $key, $value, $encryptedKeys);
}
public function createMemoryDriver()
{
return new Memory();
}
public function createArrayDriver()
{
return $this->createMemoryDriver();
}
}
@@ -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;
}
}
@@ -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);
}
}
-74
View File
@@ -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;
}
}
}
}
-164
View File
@@ -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;
}
}
-45
View File
@@ -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);
}
}
@@ -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');
}
}
-4
View File
@@ -1,4 +0,0 @@
/vendor
composer.phar
composer.lock
.DS_Store
-18
View File
@@ -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
-22
View File
@@ -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
View File
@@ -1,197 +0,0 @@
No CAPTCHA reCAPTCHA
==========
[![Build Status](https://travis-ci.org/anhskohbo/no-captcha.svg?branch=master&style=flat-square)](https://travis-ci.org/anhskohbo/no-captcha)
[![Latest Stable Version](https://poser.pugx.org/anhskohbo/no-captcha/v/stable)](https://packagist.org/packages/anhskohbo/no-captcha)
[![Total Downloads](https://poser.pugx.org/anhskohbo/no-captcha/downloads)](https://packagist.org/packages/anhskohbo/no-captcha)
[![Latest Unstable Version](https://poser.pugx.org/anhskohbo/no-captcha/v/unstable)](https://packagist.org/packages/anhskohbo/no-captcha)
[![License](https://poser.pugx.org/anhskohbo/no-captcha/license)](https://packagist.org/packages/anhskohbo/no-captcha)
![recaptcha_anchor 2x](https://cloud.githubusercontent.com/assets/1529454/5291635/1c426412-7b88-11e4-8d16-46161a081ece.gif)
> 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
View File
@@ -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
View File
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
>
<testsuites>
<testsuite name="Package Test Suite">
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
</phpunit>
-18
View File
@@ -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
View File
@@ -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'];
}
}

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