Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ece1beb87f | |||
| e6a805f1f7 | |||
| fe84d446e7 | |||
| 2ddf575191 | |||
| d73a8bb8d3 | |||
| d155d1cbab |
@@ -66,3 +66,6 @@ public/userarea/logsapi/commessaweb_customfields_763.json
|
||||
public/userarea/logsapi/commessaweb_invia_762.json
|
||||
public/userarea/logsapi/commessaweb_invia_763.json
|
||||
public/userarea/logsapi/last_auth_url.txt
|
||||
|
||||
# User uploaded files
|
||||
/public/userarea/files/
|
||||
@@ -44,6 +44,7 @@
|
||||
"phpmailer/phpmailer": "^6.9",
|
||||
"phpoffice/phpspreadsheet": "^4.1",
|
||||
"proengsoft/laravel-jsvalidation": "^4.0.0",
|
||||
"robmorgan/phinx": "^0.16.11",
|
||||
"socialiteproviders/microsoft": "^4.7",
|
||||
"spatie/laravel-query-builder": "^5.0",
|
||||
"vanguardapp/activity-log": "^6.0",
|
||||
|
||||
Generated
+646
-2
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "9c4f1e3bc3ee2180211c055e70635aef",
|
||||
"content-hash": "076e7721d08cfea8b06ce75dd8c6c576",
|
||||
"packages": [
|
||||
{
|
||||
"name": "akaunting/laravel-setting",
|
||||
@@ -251,6 +251,330 @@
|
||||
],
|
||||
"time": "2023-11-29T23:19:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "cakephp/chronos",
|
||||
"version": "3.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cakephp/chronos.git",
|
||||
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cakephp/chronos/zipball/e6e777b534244911566face8a5dbdbd7f7bda5a6",
|
||||
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"psr/clock": "^1.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/clock-implementation": "1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"cakephp/cakephp-codesniffer": "^5.0",
|
||||
"phpunit/phpunit": "^10.5.58 || ^11.5.3 || ^12.1.3"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Cake\\Chronos\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Brian Nesbitt",
|
||||
"email": "brian@nesbot.com",
|
||||
"homepage": "http://nesbot.com"
|
||||
},
|
||||
{
|
||||
"name": "The CakePHP Team",
|
||||
"homepage": "https://cakephp.org"
|
||||
}
|
||||
],
|
||||
"description": "A simple API extension for DateTime.",
|
||||
"homepage": "https://cakephp.org",
|
||||
"keywords": [
|
||||
"date",
|
||||
"datetime",
|
||||
"time"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/cakephp/chronos/issues",
|
||||
"source": "https://github.com/cakephp/chronos"
|
||||
},
|
||||
"time": "2026-04-10T02:50:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "cakephp/core",
|
||||
"version": "5.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cakephp/core.git",
|
||||
"reference": "eb012517900ed288f580aa3487e9a09f28ea85f9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cakephp/core/zipball/eb012517900ed288f580aa3487e9a09f28ea85f9",
|
||||
"reference": "eb012517900ed288f580aa3487e9a09f28ea85f9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"cakephp/utility": "^5.3.0",
|
||||
"league/container": "^5.1",
|
||||
"php": ">=8.2",
|
||||
"psr/container": "^1.1 || ^2.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/container-implementation": "^2.0"
|
||||
},
|
||||
"suggest": {
|
||||
"cakephp/cache": "To use Configure::store() and restore().",
|
||||
"cakephp/event": "To use PluginApplicationInterface or plugin applications.",
|
||||
"league/container": "To use Container and ServiceProvider classes"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-5.next": "5.4.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Cake\\Core\\": "."
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "CakePHP Community",
|
||||
"homepage": "https://github.com/cakephp/core/graphs/contributors"
|
||||
}
|
||||
],
|
||||
"description": "CakePHP Framework Core classes",
|
||||
"homepage": "https://cakephp.org",
|
||||
"keywords": [
|
||||
"cakephp",
|
||||
"core",
|
||||
"framework"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://stackoverflow.com/tags/cakephp",
|
||||
"irc": "irc://irc.freenode.org/cakephp",
|
||||
"issues": "https://github.com/cakephp/cakephp/issues",
|
||||
"source": "https://github.com/cakephp/core"
|
||||
},
|
||||
"time": "2026-03-31T06:25:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "cakephp/database",
|
||||
"version": "5.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cakephp/database.git",
|
||||
"reference": "cf94dcb57c54a1a308fd866b038cd6995910e36e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cakephp/database/zipball/cf94dcb57c54a1a308fd866b038cd6995910e36e",
|
||||
"reference": "cf94dcb57c54a1a308fd866b038cd6995910e36e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"cakephp/chronos": "^3.3",
|
||||
"cakephp/core": "^5.3.0",
|
||||
"cakephp/datasource": "^5.3.0",
|
||||
"php": ">=8.2",
|
||||
"psr/log": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"cakephp/i18n": "^5.3.0",
|
||||
"cakephp/log": "^5.3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"cakephp/i18n": "If you are using locale-aware datetime formats.",
|
||||
"cakephp/log": "If you want to use query logging without providing a logger yourself."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-5.next": "5.4.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Cake\\Database\\": "."
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "CakePHP Community",
|
||||
"homepage": "https://github.com/cakephp/database/graphs/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Flexible and powerful Database abstraction library with a familiar PDO-like API",
|
||||
"homepage": "https://cakephp.org",
|
||||
"keywords": [
|
||||
"abstraction",
|
||||
"cakephp",
|
||||
"database",
|
||||
"database abstraction",
|
||||
"pdo"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://stackoverflow.com/tags/cakephp",
|
||||
"irc": "irc://irc.freenode.org/cakephp",
|
||||
"issues": "https://github.com/cakephp/cakephp/issues",
|
||||
"source": "https://github.com/cakephp/database"
|
||||
},
|
||||
"time": "2026-03-31T06:25:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "cakephp/datasource",
|
||||
"version": "5.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cakephp/datasource.git",
|
||||
"reference": "512464eb27b19316b515ec338089b83822c9ab5a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cakephp/datasource/zipball/512464eb27b19316b515ec338089b83822c9ab5a",
|
||||
"reference": "512464eb27b19316b515ec338089b83822c9ab5a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"cakephp/core": "^5.3.0",
|
||||
"php": ">=8.2",
|
||||
"psr/simple-cache": "^2.0 || ^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"cakephp/cache": "^5.3.0",
|
||||
"cakephp/collection": "^5.3.0",
|
||||
"cakephp/utility": "^5.3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"cakephp/cache": "If you decide to use Query caching.",
|
||||
"cakephp/collection": "If you decide to use ResultSetInterface.",
|
||||
"cakephp/utility": "If you decide to use EntityTrait."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-5.next": "5.4.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Cake\\Datasource\\": "."
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "CakePHP Community",
|
||||
"homepage": "https://github.com/cakephp/datasource/graphs/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides connection managing and traits for Entities and Queries that can be reused for different datastores",
|
||||
"homepage": "https://cakephp.org",
|
||||
"keywords": [
|
||||
"cakephp",
|
||||
"connection management",
|
||||
"datasource",
|
||||
"entity",
|
||||
"query"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://stackoverflow.com/tags/cakephp",
|
||||
"irc": "irc://irc.freenode.org/cakephp",
|
||||
"issues": "https://github.com/cakephp/cakephp/issues",
|
||||
"source": "https://github.com/cakephp/datasource"
|
||||
},
|
||||
"time": "2026-04-04T08:08:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "cakephp/utility",
|
||||
"version": "5.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cakephp/utility.git",
|
||||
"reference": "4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cakephp/utility/zipball/4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99",
|
||||
"reference": "4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"cakephp/core": "^5.3.0",
|
||||
"php": ">=8.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "To use Text::transliterate() or Text::slug()",
|
||||
"lib-ICU": "To use Text::transliterate() or Text::slug()"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-5.next": "5.4.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Cake\\Utility\\": "."
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "CakePHP Community",
|
||||
"homepage": "https://github.com/cakephp/utility/graphs/contributors"
|
||||
}
|
||||
],
|
||||
"description": "CakePHP Utility classes such as Inflector, String, Hash, and Security",
|
||||
"homepage": "https://cakephp.org",
|
||||
"keywords": [
|
||||
"cakephp",
|
||||
"hash",
|
||||
"inflector",
|
||||
"security",
|
||||
"string",
|
||||
"utility"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://stackoverflow.com/tags/cakephp",
|
||||
"irc": "irc://irc.freenode.org/cakephp",
|
||||
"issues": "https://github.com/cakephp/cakephp/issues",
|
||||
"source": "https://github.com/cakephp/utility"
|
||||
},
|
||||
"time": "2026-03-09T09:38:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "carbonphp/carbon-doctrine-types",
|
||||
"version": "3.2.0",
|
||||
@@ -2627,6 +2951,90 @@
|
||||
],
|
||||
"time": "2022-12-11T20:36:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/container",
|
||||
"version": "5.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/container.git",
|
||||
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/container/zipball/58accbc032f0090a9bd08326f93062c5a658b2c5",
|
||||
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"psr/container": "^2.0.2",
|
||||
"psr/event-dispatcher": "^1.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/container-implementation": "^1.0"
|
||||
},
|
||||
"replace": {
|
||||
"orno/di": "~2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"nette/php-generator": "^4.1",
|
||||
"nikic/php-parser": "^5.0",
|
||||
"phpstan/phpstan": "^2.1.11",
|
||||
"phpunit/phpunit": "^10.5.45|^11.5.15|^12.0",
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"scrutinizer/ocular": "^1.9",
|
||||
"squizlabs/php_codesniffer": "^3.9"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-1.x": "1.x-dev",
|
||||
"dev-2.x": "2.x-dev",
|
||||
"dev-3.x": "3.x-dev",
|
||||
"dev-4.x": "4.x-dev",
|
||||
"dev-5.x": "5.x-dev",
|
||||
"dev-master": "5.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\Container\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Phil Bennett",
|
||||
"email": "mail@philbennett.co.uk",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A fast and intuitive dependency injection container.",
|
||||
"homepage": "https://github.com/thephpleague/container",
|
||||
"keywords": [
|
||||
"container",
|
||||
"dependency",
|
||||
"di",
|
||||
"injection",
|
||||
"league",
|
||||
"provider",
|
||||
"service"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/thephpleague/container/issues",
|
||||
"source": "https://github.com/thephpleague/container/tree/5.2.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/philipobenito",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-19T18:52:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem",
|
||||
"version": "3.28.0",
|
||||
@@ -4980,6 +5388,93 @@
|
||||
],
|
||||
"time": "2024-04-27T21:32:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "robmorgan/phinx",
|
||||
"version": "0.16.11",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cakephp/phinx.git",
|
||||
"reference": "a03014fea316ba021fc0776982e5bed2d10228d4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cakephp/phinx/zipball/a03014fea316ba021fc0776982e5bed2d10228d4",
|
||||
"reference": "a03014fea316ba021fc0776982e5bed2d10228d4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"cakephp/database": "^5.0.2",
|
||||
"composer-runtime-api": "^2.0",
|
||||
"php-64bit": ">=8.1",
|
||||
"psr/container": "^1.1|^2.0",
|
||||
"symfony/config": "^4.0|^5.0|^6.0|^7.0|^8.0",
|
||||
"symfony/console": "^6.0|^7.0|^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"cakephp/cakephp-codesniffer": "^5.0",
|
||||
"cakephp/i18n": "^5.0",
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"phpunit/phpunit": "^10.5",
|
||||
"symfony/yaml": "^4.0|^5.0|^6.0|^7.0|^8.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-json": "Install if using JSON configuration format",
|
||||
"ext-pdo": "PDO extension is needed",
|
||||
"symfony/yaml": "Install if using YAML configuration format"
|
||||
},
|
||||
"bin": [
|
||||
"bin/phinx"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Phinx\\": "src/Phinx/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Rob Morgan",
|
||||
"email": "robbym@gmail.com",
|
||||
"homepage": "https://robmorgan.id.au",
|
||||
"role": "Lead Developer"
|
||||
},
|
||||
{
|
||||
"name": "Woody Gilk",
|
||||
"email": "woody.gilk@gmail.com",
|
||||
"homepage": "https://shadowhand.me",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Richard Quadling",
|
||||
"email": "rquadling@gmail.com",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "CakePHP Community",
|
||||
"homepage": "https://github.com/cakephp/phinx/graphs/contributors",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Phinx makes it ridiculously easy to manage the database migrations for your PHP app.",
|
||||
"homepage": "https://phinx.org",
|
||||
"keywords": [
|
||||
"database",
|
||||
"database migrations",
|
||||
"db",
|
||||
"migrations",
|
||||
"phinx"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/cakephp/phinx/issues",
|
||||
"source": "https://github.com/cakephp/phinx/tree/0.16.11"
|
||||
},
|
||||
"time": "2026-03-15T00:04:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "socialiteproviders/manager",
|
||||
"version": "v4.8.1",
|
||||
@@ -5312,6 +5807,85 @@
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/config",
|
||||
"version": "v7.4.10",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/config.git",
|
||||
"reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/config/zipball/d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57",
|
||||
"reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/filesystem": "^7.1|^8.0",
|
||||
"symfony/polyfill-ctype": "~1.8"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/finder": "<6.4",
|
||||
"symfony/service-contracts": "<2.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
|
||||
"symfony/finder": "^6.4|^7.0|^8.0",
|
||||
"symfony/messenger": "^6.4|^7.0|^8.0",
|
||||
"symfony/service-contracts": "^2.5|^3",
|
||||
"symfony/yaml": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Config\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/config/tree/v7.4.10"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-03T14:20:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v7.1.3",
|
||||
@@ -5768,6 +6342,76 @@
|
||||
],
|
||||
"time": "2024-04-18T09:32:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "v7.4.11",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
|
||||
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/polyfill-ctype": "~1.8",
|
||||
"symfony/polyfill-mbstring": "~1.8"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/process": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Filesystem\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides basic utilities for the filesystem",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/filesystem/tree/v7.4.11"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-11T16:38:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v7.1.3",
|
||||
@@ -11355,6 +11999,6 @@
|
||||
"php": "^8.2.0",
|
||||
"ext-json": "*"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class BaselineExistingDatabase extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Change Method.
|
||||
*
|
||||
* Write your reversible migrations using this method.
|
||||
*
|
||||
* More information on writing migrations is available here:
|
||||
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
|
||||
*
|
||||
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||
* with the Table class.
|
||||
*/
|
||||
public function change(): void
|
||||
{
|
||||
// Baseline migration.
|
||||
// Existing database structure starts being tracked from this point.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreatePhinxTestTable extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Change Method.
|
||||
*
|
||||
* Write your reversible migrations using this method.
|
||||
*
|
||||
* More information on writing migrations is available here:
|
||||
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
|
||||
*
|
||||
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||
* with the Table class.
|
||||
*/
|
||||
public function change(): void
|
||||
{
|
||||
$table = $this->table('phinx_test_table');
|
||||
|
||||
$table
|
||||
->addColumn('name', 'string', [
|
||||
'limit' => 100,
|
||||
'null' => false,
|
||||
])
|
||||
->addColumn('created_at', 'timestamp', [
|
||||
'default' => 'CURRENT_TIMESTAMP',
|
||||
'null' => false,
|
||||
])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
# 1. Database migration
|
||||
|
||||
```mysql
|
||||
ALTER TABLE employees
|
||||
ADD COLUMN address varchar(500) DEFAULT NULL AFTER last_name,
|
||||
ADD COLUMN phone varchar(255) DEFAULT NULL AFTER address,
|
||||
ADD COLUMN email varchar(255) DEFAULT NULL AFTER phone,
|
||||
ADD COLUMN job_role_id int(10) UNSIGNED DEFAULT NULL AFTER department_id;
|
||||
|
||||
-- Replace ENUM status with plain VARCHAR for easier maintenance.
|
||||
ALTER TABLE employees
|
||||
MODIFY status varchar(255) NOT NULL DEFAULT 'active';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS job_roles (
|
||||
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
name varchar(255) NOT NULL,
|
||||
description text DEFAULT NULL,
|
||||
sort_order int(10) UNSIGNED NOT NULL DEFAULT 999,
|
||||
is_active tinyint(1) NOT NULL DEFAULT 1,
|
||||
created_at timestamp NULL DEFAULT current_timestamp(),
|
||||
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uniq_job_roles_name (name),
|
||||
KEY idx_job_roles_active (is_active),
|
||||
KEY idx_job_roles_sort_order (sort_order)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
ALTER TABLE employees
|
||||
ADD KEY idx_employees_job_role_id (job_role_id);
|
||||
|
||||
ALTER TABLE employees
|
||||
ADD CONSTRAINT fk_employees_job_role
|
||||
FOREIGN KEY (job_role_id) REFERENCES job_roles (id)
|
||||
ON DELETE SET NULL
|
||||
ON UPDATE CASCADE;
|
||||
|
||||
-- 1) Seed job_roles with every distinct non-empty value of employees.position.
|
||||
INSERT IGNORE INTO job_roles (name, is_active, sort_order, created_at, updated_at)
|
||||
SELECT DISTINCT TRIM(position), 1, 999, NOW(), NOW()
|
||||
FROM employees
|
||||
WHERE position IS NOT NULL AND TRIM(position) <> '';
|
||||
|
||||
-- 2) Backfill employees.job_role_id by matching position text to job_roles.name.
|
||||
UPDATE employees e
|
||||
JOIN job_roles jr ON jr.name = TRIM(e.position)
|
||||
SET e.job_role_id = jr.id
|
||||
WHERE e.position IS NOT NULL AND TRIM(e.position) <> '';
|
||||
|
||||
-- 3) Drop the legacy column.
|
||||
ALTER TABLE employees DROP COLUMN position;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS training_topics (
|
||||
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
name varchar(255) NOT NULL,
|
||||
description text DEFAULT NULL,
|
||||
default_frequency_months int(10) UNSIGNED DEFAULT NULL,
|
||||
default_reminder_days int(10) UNSIGNED NOT NULL DEFAULT 30,
|
||||
sort_order int(10) UNSIGNED NOT NULL DEFAULT 999,
|
||||
is_active tinyint(1) NOT NULL DEFAULT 1,
|
||||
is_mandatory tinyint(1) NOT NULL DEFAULT 0,
|
||||
created_at timestamp NULL DEFAULT current_timestamp(),
|
||||
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uniq_training_topics_name (name),
|
||||
KEY idx_training_topics_active (is_active),
|
||||
KEY idx_training_topics_mandatory (is_mandatory),
|
||||
KEY idx_training_topics_sort_order (sort_order)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS employee_documents (
|
||||
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
employee_id int(10) UNSIGNED NOT NULL,
|
||||
category varchar(255) NOT NULL DEFAULT 'other',
|
||||
original_name varchar(500) NOT NULL,
|
||||
stored_name varchar(500) NOT NULL,
|
||||
mime_type varchar(255) DEFAULT NULL,
|
||||
size int(10) UNSIGNED DEFAULT NULL,
|
||||
notes text DEFAULT NULL,
|
||||
uploaded_by int(10) UNSIGNED DEFAULT NULL,
|
||||
created_at timestamp NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_employee_documents_employee (employee_id),
|
||||
KEY idx_employee_documents_category (category),
|
||||
KEY idx_employee_documents_uploaded_by (uploaded_by),
|
||||
CONSTRAINT fk_employee_documents_employee
|
||||
FOREIGN KEY (employee_id) REFERENCES employees (id)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT fk_employee_documents_uploaded_by
|
||||
FOREIGN KEY (uploaded_by) REFERENCES auth_users (id)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS employee_ppe (
|
||||
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
employee_id int(10) UNSIGNED NOT NULL,
|
||||
item_name varchar(255) NOT NULL,
|
||||
delivery_date date DEFAULT NULL,
|
||||
delivered_by varchar(255) DEFAULT NULL,
|
||||
notes text DEFAULT NULL,
|
||||
created_by int(10) UNSIGNED DEFAULT NULL,
|
||||
created_at timestamp NULL DEFAULT current_timestamp(),
|
||||
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_employee_ppe_employee (employee_id),
|
||||
KEY idx_employee_ppe_delivery_date (delivery_date),
|
||||
CONSTRAINT fk_employee_ppe_employee
|
||||
FOREIGN KEY (employee_id) REFERENCES employees (id)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT fk_employee_ppe_created_by
|
||||
FOREIGN KEY (created_by) REFERENCES auth_users (id)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS employee_trainings (
|
||||
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
employee_id int(10) UNSIGNED NOT NULL,
|
||||
training_topic_id int(10) UNSIGNED NOT NULL,
|
||||
completed_date date NOT NULL,
|
||||
delivered_by varchar(255) DEFAULT NULL,
|
||||
description text DEFAULT NULL,
|
||||
training_type varchar(255) NOT NULL DEFAULT 'initial',
|
||||
update_frequency_months int(10) UNSIGNED DEFAULT NULL,
|
||||
reminder_days int(10) UNSIGNED DEFAULT NULL,
|
||||
next_due_date date DEFAULT NULL,
|
||||
created_by int(10) UNSIGNED DEFAULT NULL,
|
||||
created_at timestamp NULL DEFAULT current_timestamp(),
|
||||
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_employee_trainings_employee (employee_id),
|
||||
KEY idx_employee_trainings_topic (training_topic_id),
|
||||
KEY idx_employee_trainings_next_due (next_due_date),
|
||||
KEY idx_employee_trainings_employee_topic (employee_id, training_topic_id),
|
||||
KEY idx_employee_trainings_created_by (created_by),
|
||||
CONSTRAINT fk_employee_trainings_employee
|
||||
FOREIGN KEY (employee_id) REFERENCES employees (id)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT fk_employee_trainings_topic
|
||||
FOREIGN KEY (training_topic_id) REFERENCES training_topics (id)
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT fk_employee_trainings_created_by
|
||||
FOREIGN KEY (created_by) REFERENCES auth_users (id)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS employee_training_attachments (
|
||||
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
training_id int(10) UNSIGNED NOT NULL,
|
||||
original_name varchar(500) NOT NULL,
|
||||
stored_name varchar(500) NOT NULL,
|
||||
mime_type varchar(255) DEFAULT NULL,
|
||||
size int(10) UNSIGNED DEFAULT NULL,
|
||||
uploaded_by int(10) UNSIGNED DEFAULT NULL,
|
||||
created_at timestamp NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_employee_training_attachments_training (training_id),
|
||||
KEY idx_employee_training_attachments_uploaded_by (uploaded_by),
|
||||
CONSTRAINT fk_employee_training_attachments_training
|
||||
FOREIGN KEY (training_id) REFERENCES employee_trainings (id)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT fk_employee_training_attachments_uploaded_by
|
||||
FOREIGN KEY (uploaded_by) REFERENCES auth_users (id)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS employee_training_log (
|
||||
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
employee_id int(10) UNSIGNED DEFAULT NULL,
|
||||
training_id int(10) UNSIGNED DEFAULT NULL,
|
||||
action varchar(255) NOT NULL,
|
||||
field varchar(255) DEFAULT NULL,
|
||||
old_value text DEFAULT NULL,
|
||||
new_value text DEFAULT NULL,
|
||||
changed_by int(10) UNSIGNED DEFAULT NULL,
|
||||
changed_at timestamp NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_employee_training_log_employee (employee_id),
|
||||
KEY idx_employee_training_log_training (training_id),
|
||||
KEY idx_employee_training_log_changed_at (changed_at),
|
||||
CONSTRAINT fk_employee_training_log_employee
|
||||
FOREIGN KEY (employee_id) REFERENCES employees (id)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
CONSTRAINT fk_employee_training_log_training
|
||||
FOREIGN KEY (training_id) REFERENCES employee_trainings (id)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
CONSTRAINT fk_employee_training_log_changed_by
|
||||
FOREIGN KEY (changed_by) REFERENCES auth_users (id)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT INTO auth_roles (name, display_name, description, removable, created_at, updated_at) VALUES
|
||||
('employee', 'Employee', 'Read-only access to own employee profile.', 1, NOW(), NOW()),
|
||||
('employee-hr', 'HR Manager', 'Can manage employee profiles, documents, PPE and training records.', 1, NOW(), NOW()),
|
||||
('manager', 'Manager', 'Same permissions as HR Manager.', 1, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
display_name = VALUES(display_name),
|
||||
description = VALUES(description),
|
||||
updated_at = NOW();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS training_reminder_log (
|
||||
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
training_id int(10) UNSIGNED DEFAULT NULL,
|
||||
employee_id int(10) UNSIGNED DEFAULT NULL,
|
||||
training_topic_id int(10) UNSIGNED DEFAULT NULL,
|
||||
addressee_email varchar(255) NOT NULL,
|
||||
next_due_date date DEFAULT NULL,
|
||||
status_at_send varchar(255) NOT NULL,
|
||||
sent_at timestamp NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_training_reminder_log_dedup (training_id, addressee_email, next_due_date),
|
||||
KEY idx_training_reminder_log_dedup_missing (employee_id, training_topic_id, addressee_email),
|
||||
KEY idx_training_reminder_log_sent_at (sent_at),
|
||||
CONSTRAINT fk_training_reminder_log_training
|
||||
FOREIGN KEY (training_id) REFERENCES employee_trainings (id)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT fk_training_reminder_log_employee
|
||||
FOREIGN KEY (employee_id) REFERENCES employees (id)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT fk_training_reminder_log_topic
|
||||
FOREIGN KEY (training_topic_id) REFERENCES training_topics (id)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
# 2. Upload storage folder
|
||||
|
||||
Create the storage directory with the correct permissions for the web server:
|
||||
|
||||
```bash
|
||||
mkdir -p /var/www/zibo-dashboard/public/userarea/files/employees
|
||||
chown -R www-data:www-data /var/www/zibo-dashboard/public/userarea/files
|
||||
chmod -R 775 /var/www/zibo-dashboard/public/userarea/files
|
||||
```
|
||||
|
||||
Uploaded files will be organized as:
|
||||
|
||||
```
|
||||
files/employees/{employee_id}/documents/ # File Repository (HR)
|
||||
files/employees/{employee_id}/trainings/ # Training certificates
|
||||
```
|
||||
|
||||
# 3. Cron for automated emails
|
||||
|
||||
```cron
|
||||
0 7 * * * /usr/bin/php /var/www/zibo-dashboard/public/userarea/cron/send_training_reminders.php \
|
||||
>> /var/www/zibo-dashboard/storage/logs/training_reminders.log 2>&1
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
if (file_exists(__DIR__ . '/.env')) {
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
|
||||
$dotenv->safeLoad();
|
||||
}
|
||||
|
||||
return [
|
||||
'paths' => [
|
||||
'migrations' => __DIR__ . '/db/migrations',
|
||||
'seeds' => __DIR__ . '/db/seeds',
|
||||
],
|
||||
|
||||
'environments' => [
|
||||
'default_migration_table' => 'phinxlog',
|
||||
'default_environment' => 'development',
|
||||
|
||||
'development' => [
|
||||
'adapter' => $_ENV['DB_CONNECTION'] ?? 'mysql',
|
||||
'host' => $_ENV['DB_HOST'] ?? 'localhost',
|
||||
'name' => $_ENV['DB_DATABASE'] ?? '',
|
||||
'user' => $_ENV['DB_USERNAME'] ?? '',
|
||||
'pass' => $_ENV['DB_PASSWORD'] ?? '',
|
||||
'port' => $_ENV['DB_PORT'] ?? 3306,
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
],
|
||||
],
|
||||
|
||||
'version_order' => 'creation',
|
||||
];
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* Auth check for AJAX endpoints under /userarea/ajax/.
|
||||
* Include this at the top of every ajax handler.
|
||||
* Sets $currentUserId from session or returns 401 JSON.
|
||||
*/
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (empty($_SESSION['iduserlogin'])) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'message' => 'Non autorizzato. Effettua il login.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$currentUserId = (int)$_SESSION['iduserlogin'];
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID documento non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT employee_id, stored_name FROM employee_documents WHERE id = :id LIMIT 1");
|
||||
$stmt->execute(['id' => $id]);
|
||||
$doc = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$doc) {
|
||||
echo json_encode(['success' => false, 'message' => 'Documento non trovato.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$del = $pdo->prepare("DELETE FROM employee_documents WHERE id = :id");
|
||||
$del->execute(['id' => $id]);
|
||||
|
||||
$path = __DIR__ . '/../../files/employees/' . (int)$doc['employee_id'] . '/documents/' . $doc['stored_name'];
|
||||
if (is_file($path)) {
|
||||
@unlink($path);
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID DPI non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("DELETE FROM employee_ppe WHERE id = :id");
|
||||
$stmt->execute(['id' => $id]);
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$row = $pdo->prepare("SELECT employee_id FROM employee_trainings WHERE id = :id");
|
||||
$row->execute(['id' => $id]);
|
||||
$tr = $row->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$tr) {
|
||||
$pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Collect attached files BEFORE deletion so we can unlink them after
|
||||
$files = $pdo->prepare("SELECT stored_name FROM employee_training_attachments WHERE training_id = :id");
|
||||
$files->execute(['id' => $id]);
|
||||
$stored = $files->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
// Log BEFORE delete (FK on log allows SET NULL on training delete but we want a clean record)
|
||||
$pdo->prepare("
|
||||
INSERT INTO employee_training_log
|
||||
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
|
||||
VALUES
|
||||
(:eid, NULL, 'deleted', NULL, NULL, NULL, :cb, NOW())
|
||||
")->execute(['eid' => $tr['employee_id'], 'cb' => $currentUserId]);
|
||||
|
||||
$pdo->prepare("DELETE FROM employee_trainings WHERE id = :id")->execute(['id' => $id]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
foreach ($stored as $name) {
|
||||
$path = __DIR__ . '/../../files/employees/' . (int)$tr['employee_id'] . '/trainings/' . $name;
|
||||
if (is_file($path)) {
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (Exception $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID allegato non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$row = $pdo->prepare("
|
||||
SELECT a.stored_name, a.original_name, a.training_id, t.employee_id
|
||||
FROM employee_training_attachments a
|
||||
JOIN employee_trainings t ON t.id = a.training_id
|
||||
WHERE a.id = :id
|
||||
LIMIT 1
|
||||
");
|
||||
$row->execute(['id' => $id]);
|
||||
$att = $row->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$att) {
|
||||
echo json_encode(['success' => false, 'message' => 'Allegato non trovato.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
$pdo->prepare("DELETE FROM employee_training_attachments WHERE id = :id")->execute(['id' => $id]);
|
||||
$pdo->prepare("
|
||||
INSERT INTO employee_training_log
|
||||
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
|
||||
VALUES
|
||||
(:eid, :tid, 'attachment_deleted', 'attachment', :name, NULL, :cb, NOW())
|
||||
")->execute([
|
||||
'eid' => $att['employee_id'],
|
||||
'tid' => $att['training_id'],
|
||||
'name' => $att['original_name'],
|
||||
'cb' => $currentUserId,
|
||||
]);
|
||||
$pdo->commit();
|
||||
|
||||
$path = __DIR__ . '/../../files/employees/' . (int)$att['employee_id'] . '/trainings/' . $att['stored_name'];
|
||||
if (is_file($path)) {
|
||||
@unlink($path);
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (Exception $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../auth_check.php');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
http_response_code(400);
|
||||
exit('ID non valido.');
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT d.*, e.auth_user_id
|
||||
FROM employee_documents d
|
||||
JOIN employees e ON e.id = d.employee_id
|
||||
WHERE d.id = :id
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute(['id' => $id]);
|
||||
$doc = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$doc) {
|
||||
http_response_code(404);
|
||||
exit('Documento non trovato.');
|
||||
}
|
||||
|
||||
/* Access check: HR roles can download any; otherwise only own employee */
|
||||
$roleStmt = $pdo->prepare("
|
||||
SELECT r.name
|
||||
FROM auth_users u
|
||||
LEFT JOIN auth_roles r ON r.id = u.role_id
|
||||
WHERE u.id = :id LIMIT 1
|
||||
");
|
||||
$roleStmt->execute(['id' => $currentUserId]);
|
||||
$role = (string)$roleStmt->fetchColumn();
|
||||
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
|
||||
$isHr = in_array($role, $hrRoles, true);
|
||||
|
||||
if (!$isHr && (int)$doc['auth_user_id'] !== $currentUserId) {
|
||||
http_response_code(403);
|
||||
exit('Accesso negato.');
|
||||
}
|
||||
|
||||
$path = __DIR__ . '/../../files/employees/' . (int)$doc['employee_id'] . '/documents/' . $doc['stored_name'];
|
||||
if (!is_file($path)) {
|
||||
http_response_code(404);
|
||||
exit('File non trovato sul server.');
|
||||
}
|
||||
|
||||
while (ob_get_level() > 0) { ob_end_clean(); }
|
||||
header('Content-Type: ' . (!empty($doc['mime_type']) ? $doc['mime_type'] : 'application/octet-stream'));
|
||||
header('Content-Disposition: attachment; filename="' . rawurlencode($doc['original_name']) . '"');
|
||||
header('Content-Length: ' . filesize($path));
|
||||
header('Cache-Control: private, max-age=0, must-revalidate');
|
||||
readfile($path);
|
||||
exit;
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../auth_check.php');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
http_response_code(400);
|
||||
exit('ID non valido.');
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT a.*, t.employee_id, e.auth_user_id
|
||||
FROM employee_training_attachments a
|
||||
JOIN employee_trainings t ON t.id = a.training_id
|
||||
JOIN employees e ON e.id = t.employee_id
|
||||
WHERE a.id = :id
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute(['id' => $id]);
|
||||
$att = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$att) {
|
||||
http_response_code(404);
|
||||
exit('Allegato non trovato.');
|
||||
}
|
||||
|
||||
/* Access: HR or owning employee */
|
||||
$roleStmt = $pdo->prepare("
|
||||
SELECT r.name FROM auth_users u
|
||||
LEFT JOIN auth_roles r ON r.id = u.role_id
|
||||
WHERE u.id = :id LIMIT 1
|
||||
");
|
||||
$roleStmt->execute(['id' => $currentUserId]);
|
||||
$role = (string)$roleStmt->fetchColumn();
|
||||
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
|
||||
$isHr = in_array($role, $hrRoles, true);
|
||||
|
||||
if (!$isHr && (int)$att['auth_user_id'] !== $currentUserId) {
|
||||
http_response_code(403);
|
||||
exit('Accesso negato.');
|
||||
}
|
||||
|
||||
$path = __DIR__ . '/../../files/employees/' . (int)$att['employee_id'] . '/trainings/' . $att['stored_name'];
|
||||
if (!is_file($path)) {
|
||||
http_response_code(404);
|
||||
exit('File non trovato sul server.');
|
||||
}
|
||||
|
||||
while (ob_get_level() > 0) { ob_end_clean(); }
|
||||
header('Content-Type: ' . (!empty($att['mime_type']) ? $att['mime_type'] : 'application/octet-stream'));
|
||||
header('Content-Disposition: attachment; filename="' . rawurlencode($att['original_name']) . '"');
|
||||
header('Content-Length: ' . filesize($path));
|
||||
header('Cache-Control: private, max-age=0, must-revalidate');
|
||||
readfile($path);
|
||||
exit;
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../auth_check.php');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$trainingId = (int)($_GET['training_id'] ?? 0);
|
||||
if ($trainingId <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
/* Access: HR or owner */
|
||||
$ownerStmt = $pdo->prepare("
|
||||
SELECT e.auth_user_id
|
||||
FROM employee_trainings t
|
||||
JOIN employees e ON e.id = t.employee_id
|
||||
WHERE t.id = :id LIMIT 1
|
||||
");
|
||||
$ownerStmt->execute(['id' => $trainingId]);
|
||||
$ownerAuthUserId = $ownerStmt->fetchColumn();
|
||||
if ($ownerAuthUserId === false) {
|
||||
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$roleStmt = $pdo->prepare("
|
||||
SELECT r.name FROM auth_users u
|
||||
LEFT JOIN auth_roles r ON r.id = u.role_id
|
||||
WHERE u.id = :id LIMIT 1
|
||||
");
|
||||
$roleStmt->execute(['id' => $currentUserId]);
|
||||
$role = (string)$roleStmt->fetchColumn();
|
||||
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
|
||||
$isHr = in_array($role, $hrRoles, true);
|
||||
|
||||
if (!$isHr && (int)$ownerAuthUserId !== $currentUserId) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'message' => 'Accesso negato.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, original_name, mime_type, size, created_at
|
||||
FROM employee_training_attachments
|
||||
WHERE training_id = :tid
|
||||
ORDER BY created_at DESC
|
||||
");
|
||||
$stmt->execute(['tid' => $trainingId]);
|
||||
$attachments = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'attachments' => $attachments,
|
||||
'can_edit' => $isHr,
|
||||
]);
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../auth_check.php');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$trainingId = (int)($_GET['training_id'] ?? 0);
|
||||
if ($trainingId <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
/* Access: HR or owner */
|
||||
$ownerStmt = $pdo->prepare("
|
||||
SELECT e.auth_user_id
|
||||
FROM employee_trainings t
|
||||
JOIN employees e ON e.id = t.employee_id
|
||||
WHERE t.id = :id LIMIT 1
|
||||
");
|
||||
$ownerStmt->execute(['id' => $trainingId]);
|
||||
$ownerAuthUserId = $ownerStmt->fetchColumn();
|
||||
if ($ownerAuthUserId === false) {
|
||||
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$roleStmt = $pdo->prepare("
|
||||
SELECT r.name FROM auth_users u
|
||||
LEFT JOIN auth_roles r ON r.id = u.role_id
|
||||
WHERE u.id = :id LIMIT 1
|
||||
");
|
||||
$roleStmt->execute(['id' => $currentUserId]);
|
||||
$role = (string)$roleStmt->fetchColumn();
|
||||
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
|
||||
$isHr = in_array($role, $hrRoles, true);
|
||||
|
||||
if (!$isHr && (int)$ownerAuthUserId !== $currentUserId) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'message' => 'Accesso negato.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT l.id, l.action, l.field, l.old_value, l.new_value, l.changed_at,
|
||||
TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS changed_by_name,
|
||||
u.email AS changed_by_email
|
||||
FROM employee_training_log l
|
||||
LEFT JOIN auth_users u ON u.id = l.changed_by
|
||||
WHERE l.training_id = :tid
|
||||
ORDER BY l.changed_at DESC, l.id DESC
|
||||
");
|
||||
$stmt->execute(['tid' => $trainingId]);
|
||||
$entries = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode(['success' => true, 'entries' => $entries]);
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$employeeId = (int)($_POST['employee_id'] ?? 0);
|
||||
$firstName = trim($_POST['first_name'] ?? '');
|
||||
$lastName = trim($_POST['last_name'] ?? '');
|
||||
$employeeCode = trim($_POST['employee_code'] ?? '');
|
||||
$address = trim($_POST['address'] ?? '');
|
||||
$phone = trim($_POST['phone'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$hireDate = trim($_POST['hire_date'] ?? '');
|
||||
$departmentId = $_POST['department_id'] ?? '';
|
||||
$jobRoleId = $_POST['job_role_id'] ?? '';
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
$authUserId = $_POST['auth_user_id'] ?? '';
|
||||
$roleId = $_POST['role_id'] ?? '';
|
||||
|
||||
if ($employeeId <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
|
||||
exit;
|
||||
}
|
||||
if ($firstName === '' || $lastName === '') {
|
||||
echo json_encode(['success' => false, 'message' => 'Nome e cognome sono obbligatori.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$allowedStatus = ['active', 'inactive', 'suspended'];
|
||||
if (!in_array($status, $allowedStatus, true)) {
|
||||
$status = 'active';
|
||||
}
|
||||
|
||||
$departmentId = ($departmentId === '' || $departmentId === null) ? null : (int)$departmentId;
|
||||
$jobRoleId = ($jobRoleId === '' || $jobRoleId === null) ? null : (int)$jobRoleId;
|
||||
$authUserId = ($authUserId === '' || $authUserId === null) ? null : (int)$authUserId;
|
||||
$roleId = ($roleId === '' || $roleId === null) ? null : (int)$roleId;
|
||||
$hireDate = $hireDate === '' ? null : $hireDate;
|
||||
|
||||
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Email non valida.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($employeeCode !== '') {
|
||||
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE employee_code = :code AND id <> :id");
|
||||
$check->execute(['code' => $employeeCode, 'id' => $employeeId]);
|
||||
if ((int)$check->fetchColumn() > 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Codice dipendente già in uso.']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if ($authUserId !== null) {
|
||||
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE auth_user_id = :uid AND id <> :id");
|
||||
$check->execute(['uid' => $authUserId, 'id' => $employeeId]);
|
||||
if ((int)$check->fetchColumn() > 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Questo utente è già associato ad un altro dipendente.']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE employees
|
||||
SET first_name = :first_name,
|
||||
last_name = :last_name,
|
||||
employee_code = :employee_code,
|
||||
address = :address,
|
||||
phone = :phone,
|
||||
email = :email,
|
||||
hire_date = :hire_date,
|
||||
department_id = :department_id,
|
||||
job_role_id = :job_role_id,
|
||||
status = :status,
|
||||
auth_user_id = :auth_user_id,
|
||||
updated_at = NOW()
|
||||
WHERE id = :id
|
||||
");
|
||||
$stmt->execute([
|
||||
'first_name' => $firstName,
|
||||
'last_name' => $lastName,
|
||||
'employee_code' => $employeeCode !== '' ? $employeeCode : null,
|
||||
'address' => $address !== '' ? $address : null,
|
||||
'phone' => $phone !== '' ? $phone : null,
|
||||
'email' => $email !== '' ? $email : null,
|
||||
'hire_date' => $hireDate,
|
||||
'department_id' => $departmentId,
|
||||
'job_role_id' => $jobRoleId,
|
||||
'status' => $status,
|
||||
'auth_user_id' => $authUserId,
|
||||
'id' => $employeeId,
|
||||
]);
|
||||
|
||||
// Optionally update Vanguard role for the linked auth_user
|
||||
if ($authUserId !== null && $roleId !== null) {
|
||||
$check = $pdo->prepare("SELECT COUNT(*) FROM auth_roles WHERE id = ?");
|
||||
$check->execute([$roleId]);
|
||||
if ((int)$check->fetchColumn() > 0) {
|
||||
$upd = $pdo->prepare("UPDATE auth_users SET role_id = :role_id, updated_at = NOW() WHERE id = :uid");
|
||||
$upd->execute(['role_id' => $roleId, 'uid' => $authUserId]);
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$employeeId = (int)($_POST['employee_id'] ?? 0);
|
||||
$itemName = trim($_POST['item_name'] ?? '');
|
||||
$deliveryDate = trim($_POST['delivery_date'] ?? '');
|
||||
$deliveredBy = trim($_POST['delivered_by'] ?? '');
|
||||
$notes = trim($_POST['notes'] ?? '');
|
||||
|
||||
if ($employeeId <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
|
||||
exit;
|
||||
}
|
||||
if ($itemName === '') {
|
||||
echo json_encode(['success' => false, 'message' => 'Il nome del DPI è obbligatorio.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$deliveryDate = $deliveryDate === '' ? null : $deliveryDate;
|
||||
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
|
||||
$notes = $notes !== '' ? $notes : null;
|
||||
|
||||
try {
|
||||
if ($id > 0) {
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE employee_ppe
|
||||
SET item_name = :item_name,
|
||||
delivery_date = :delivery_date,
|
||||
delivered_by = :delivered_by,
|
||||
notes = :notes,
|
||||
updated_at = NOW()
|
||||
WHERE id = :id AND employee_id = :eid
|
||||
");
|
||||
$stmt->execute([
|
||||
'item_name' => $itemName,
|
||||
'delivery_date' => $deliveryDate,
|
||||
'delivered_by' => $deliveredBy,
|
||||
'notes' => $notes,
|
||||
'id' => $id,
|
||||
'eid' => $employeeId,
|
||||
]);
|
||||
echo json_encode(['success' => true, 'id' => $id]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE id = :id");
|
||||
$check->execute(['id' => $employeeId]);
|
||||
if ((int)$check->fetchColumn() === 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Dipendente non trovato.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO employee_ppe
|
||||
(employee_id, item_name, delivery_date, delivered_by, notes, created_by, created_at, updated_at)
|
||||
VALUES
|
||||
(:employee_id, :item_name, :delivery_date, :delivered_by, :notes, :created_by, NOW(), NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
'employee_id' => $employeeId,
|
||||
'item_name' => $itemName,
|
||||
'delivery_date' => $deliveryDate,
|
||||
'delivered_by' => $deliveredBy,
|
||||
'notes' => $notes,
|
||||
'created_by' => $currentUserId,
|
||||
]);
|
||||
|
||||
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$employeeId = (int)($_POST['employee_id'] ?? 0);
|
||||
$topicId = (int)($_POST['training_topic_id'] ?? 0);
|
||||
$completedDate = trim($_POST['completed_date'] ?? '');
|
||||
$deliveredBy = trim($_POST['delivered_by'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$trainingType = trim($_POST['training_type'] ?? 'initial');
|
||||
$freqRaw = $_POST['update_frequency_months'] ?? '';
|
||||
$remRaw = $_POST['reminder_days'] ?? '';
|
||||
|
||||
if ($employeeId <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
|
||||
exit;
|
||||
}
|
||||
if ($topicId <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Selezionare un corso.']);
|
||||
exit;
|
||||
}
|
||||
if ($completedDate === '') {
|
||||
echo json_encode(['success' => false, 'message' => 'La data di completamento è obbligatoria.']);
|
||||
exit;
|
||||
}
|
||||
if (!in_array($trainingType, ['initial', 'refresher'], true)) {
|
||||
$trainingType = 'initial';
|
||||
}
|
||||
|
||||
$topicStmt = $pdo->prepare("SELECT default_frequency_months, default_reminder_days FROM training_topics WHERE id = :id");
|
||||
$topicStmt->execute(['id' => $topicId]);
|
||||
$topic = $topicStmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$topic) {
|
||||
echo json_encode(['success' => false, 'message' => 'Corso non trovato.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$freq = ($freqRaw === '' || $freqRaw === null) ? null : max(0, (int)$freqRaw);
|
||||
$rem = ($remRaw === '' || $remRaw === null) ? null : max(0, (int)$remRaw);
|
||||
|
||||
/* Effective frequency for next_due_date: explicit override or topic default */
|
||||
$effFreq = $freq !== null ? $freq : ($topic['default_frequency_months'] !== null ? (int)$topic['default_frequency_months'] : null);
|
||||
|
||||
$nextDue = null;
|
||||
if ($effFreq !== null && $effFreq > 0) {
|
||||
$d = DateTime::createFromFormat('Y-m-d', $completedDate);
|
||||
if ($d) {
|
||||
$d->modify('+' . (int)$effFreq . ' months');
|
||||
$nextDue = $d->format('Y-m-d');
|
||||
}
|
||||
}
|
||||
|
||||
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
|
||||
$description = $description !== '' ? $description : null;
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
if ($id > 0) {
|
||||
$old = $pdo->prepare("SELECT * FROM employee_trainings WHERE id = :id");
|
||||
$old->execute(['id' => $id]);
|
||||
$oldRow = $old->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$oldRow) {
|
||||
$pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$upd = $pdo->prepare("
|
||||
UPDATE employee_trainings
|
||||
SET training_topic_id = :topic_id,
|
||||
completed_date = :completed_date,
|
||||
delivered_by = :delivered_by,
|
||||
description = :description,
|
||||
training_type = :training_type,
|
||||
update_frequency_months = :freq,
|
||||
reminder_days = :rem,
|
||||
next_due_date = :next_due,
|
||||
updated_at = NOW()
|
||||
WHERE id = :id
|
||||
");
|
||||
$upd->execute([
|
||||
'topic_id' => $topicId,
|
||||
'completed_date' => $completedDate,
|
||||
'delivered_by' => $deliveredBy,
|
||||
'description' => $description,
|
||||
'training_type' => $trainingType,
|
||||
'freq' => $freq,
|
||||
'rem' => $rem,
|
||||
'next_due' => $nextDue,
|
||||
'id' => $id,
|
||||
]);
|
||||
|
||||
$fields = [
|
||||
'training_topic_id' => $topicId,
|
||||
'completed_date' => $completedDate,
|
||||
'delivered_by' => $deliveredBy,
|
||||
'description' => $description,
|
||||
'training_type' => $trainingType,
|
||||
'update_frequency_months' => $freq,
|
||||
'reminder_days' => $rem,
|
||||
'next_due_date' => $nextDue,
|
||||
];
|
||||
$logStmt = $pdo->prepare("
|
||||
INSERT INTO employee_training_log
|
||||
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
|
||||
VALUES
|
||||
(:eid, :tid, 'updated', :field, :old_v, :new_v, :cb, NOW())
|
||||
");
|
||||
foreach ($fields as $f => $newV) {
|
||||
$oldV = $oldRow[$f] ?? null;
|
||||
if ((string)$oldV !== (string)$newV) {
|
||||
$logStmt->execute([
|
||||
'eid' => $employeeId,
|
||||
'tid' => $id,
|
||||
'field' => $f,
|
||||
'old_v' => $oldV,
|
||||
'new_v' => $newV,
|
||||
'cb' => $currentUserId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
echo json_encode(['success' => true, 'id' => $id]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$ins = $pdo->prepare("
|
||||
INSERT INTO employee_trainings
|
||||
(employee_id, training_topic_id, completed_date,
|
||||
delivered_by, description,
|
||||
training_type, update_frequency_months, reminder_days, next_due_date,
|
||||
created_by, created_at, updated_at)
|
||||
VALUES
|
||||
(:eid, :tid, :completed_date,
|
||||
:delivered_by, :description,
|
||||
:training_type, :freq, :rem, :next_due,
|
||||
:cb, NOW(), NOW())
|
||||
");
|
||||
$ins->execute([
|
||||
'eid' => $employeeId,
|
||||
'tid' => $topicId,
|
||||
'completed_date' => $completedDate,
|
||||
'delivered_by' => $deliveredBy,
|
||||
'description' => $description,
|
||||
'training_type' => $trainingType,
|
||||
'freq' => $freq,
|
||||
'rem' => $rem,
|
||||
'next_due' => $nextDue,
|
||||
'cb' => $currentUserId,
|
||||
]);
|
||||
$newId = (int)$pdo->lastInsertId();
|
||||
|
||||
$pdo->prepare("
|
||||
INSERT INTO employee_training_log
|
||||
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
|
||||
VALUES
|
||||
(:eid, :tid, 'created', NULL, NULL, NULL, :cb, NOW())
|
||||
")->execute(['eid' => $employeeId, 'tid' => $newId, 'cb' => $currentUserId]);
|
||||
|
||||
$pdo->commit();
|
||||
echo json_encode(['success' => true, 'id' => $newId]);
|
||||
} catch (Exception $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$employeeId = (int)($_POST['employee_id'] ?? 0);
|
||||
$category = trim($_POST['category'] ?? 'other');
|
||||
$notes = trim($_POST['notes'] ?? '');
|
||||
|
||||
$allowedCategories = ['job_description', 'contract', 'rules', 'other'];
|
||||
if (!in_array($category, $allowedCategories, true)) {
|
||||
$category = 'other';
|
||||
}
|
||||
|
||||
if ($employeeId <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE id = :id");
|
||||
$check->execute(['id' => $employeeId]);
|
||||
if ((int)$check->fetchColumn() === 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Dipendente non trovato.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
||||
$errCode = $_FILES['file']['error'] ?? -1;
|
||||
$msg = 'Errore nel caricamento del file.';
|
||||
if ($errCode === UPLOAD_ERR_INI_SIZE || $errCode === UPLOAD_ERR_FORM_SIZE) {
|
||||
$msg = 'Il file supera la dimensione massima consentita.';
|
||||
}
|
||||
echo json_encode(['success' => false, 'message' => $msg]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$originalName = $_FILES['file']['name'];
|
||||
$tmpPath = $_FILES['file']['tmp_name'];
|
||||
$size = (int)$_FILES['file']['size'];
|
||||
$mimeType = mime_content_type($tmpPath) ?: ($_FILES['file']['type'] ?? null);
|
||||
|
||||
$dir = __DIR__ . '/../../files/employees/' . $employeeId . '/documents';
|
||||
if (!is_dir($dir)) {
|
||||
if (!mkdir($dir, 0775, true) && !is_dir($dir)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Impossibile creare la cartella di destinazione.']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$safeOriginal = preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName);
|
||||
$storedName = uniqid('doc_') . '_' . $safeOriginal;
|
||||
$destPath = $dir . '/' . $storedName;
|
||||
|
||||
if (!move_uploaded_file($tmpPath, $destPath)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Impossibile salvare il file su disco.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO employee_documents
|
||||
(employee_id, category, original_name, stored_name, mime_type, size, notes, uploaded_by, created_at)
|
||||
VALUES
|
||||
(:employee_id, :category, :original_name, :stored_name, :mime_type, :size, :notes, :uploaded_by, NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
'employee_id' => $employeeId,
|
||||
'category' => $category,
|
||||
'original_name' => $originalName,
|
||||
'stored_name' => $storedName,
|
||||
'mime_type' => $mimeType,
|
||||
'size' => $size,
|
||||
'notes' => $notes !== '' ? $notes : null,
|
||||
'uploaded_by' => $currentUserId,
|
||||
]);
|
||||
|
||||
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
|
||||
} catch (Exception $e) {
|
||||
@unlink($destPath);
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$trainingId = (int)($_POST['training_id'] ?? 0);
|
||||
if ($trainingId <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$tr = $pdo->prepare("SELECT employee_id FROM employee_trainings WHERE id = :id");
|
||||
$tr->execute(['id' => $trainingId]);
|
||||
$trainingRow = $tr->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$trainingRow) {
|
||||
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
|
||||
exit;
|
||||
}
|
||||
$employeeId = (int)$trainingRow['employee_id'];
|
||||
|
||||
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
||||
$errCode = $_FILES['file']['error'] ?? -1;
|
||||
$msg = 'Errore nel caricamento del file.';
|
||||
if ($errCode === UPLOAD_ERR_INI_SIZE || $errCode === UPLOAD_ERR_FORM_SIZE) {
|
||||
$msg = 'Il file supera la dimensione massima consentita.';
|
||||
}
|
||||
echo json_encode(['success' => false, 'message' => $msg]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$originalName = $_FILES['file']['name'];
|
||||
$tmpPath = $_FILES['file']['tmp_name'];
|
||||
$size = (int)$_FILES['file']['size'];
|
||||
$mimeType = mime_content_type($tmpPath) ?: ($_FILES['file']['type'] ?? null);
|
||||
|
||||
$dir = __DIR__ . '/../../files/employees/' . $employeeId . '/trainings';
|
||||
if (!is_dir($dir)) {
|
||||
if (!mkdir($dir, 0775, true) && !is_dir($dir)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Impossibile creare la cartella di destinazione.']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$safeOriginal = preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName);
|
||||
$storedName = uniqid('tr_') . '_' . $safeOriginal;
|
||||
$destPath = $dir . '/' . $storedName;
|
||||
|
||||
if (!move_uploaded_file($tmpPath, $destPath)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Impossibile salvare il file su disco.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$ins = $pdo->prepare("
|
||||
INSERT INTO employee_training_attachments
|
||||
(training_id, original_name, stored_name, mime_type, size, uploaded_by, created_at)
|
||||
VALUES
|
||||
(:tid, :original_name, :stored_name, :mime_type, :size, :uploaded_by, NOW())
|
||||
");
|
||||
$ins->execute([
|
||||
'tid' => $trainingId,
|
||||
'original_name' => $originalName,
|
||||
'stored_name' => $storedName,
|
||||
'mime_type' => $mimeType,
|
||||
'size' => $size,
|
||||
'uploaded_by' => $currentUserId,
|
||||
]);
|
||||
$attachmentId = (int)$pdo->lastInsertId();
|
||||
|
||||
$pdo->prepare("
|
||||
INSERT INTO employee_training_log
|
||||
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
|
||||
VALUES
|
||||
(:eid, :tid, 'attachment_added', 'attachment', NULL, :name, :cb, NOW())
|
||||
")->execute([
|
||||
'eid' => $employeeId,
|
||||
'tid' => $trainingId,
|
||||
'name' => $originalName,
|
||||
'cb' => $currentUserId,
|
||||
]);
|
||||
|
||||
$pdo->commit();
|
||||
echo json_encode(['success' => true, 'id' => $attachmentId]);
|
||||
} catch (Exception $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
@unlink($destPath);
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* HR auth check for AJAX endpoints that require HR-management permissions.
|
||||
* Allowed roles: Admin, User, Superuser, employee-hr, manager.
|
||||
* Sets $currentUserId and $currentUserRole, or returns 401/403 JSON.
|
||||
*/
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
require_once(__DIR__ . '/../class/db-functions.php');
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT r.name AS role_name
|
||||
FROM auth_users u
|
||||
LEFT JOIN auth_roles r ON r.id = u.role_id
|
||||
WHERE u.id = :id
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute(['id' => $currentUserId]);
|
||||
$currentUserRole = (string)$stmt->fetchColumn();
|
||||
|
||||
$allowedHrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
|
||||
|
||||
if (!in_array($currentUserRole, $allowedHrRoles, true)) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Permessi insufficienti per questa operazione.',
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../auth_check.php');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID mansione non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$usage = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE job_role_id = :id");
|
||||
$usage->execute(['id' => $id]);
|
||||
if ((int)$usage->fetchColumn() > 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Questa mansione è associata a uno o più dipendenti e non può essere cancellata.',
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM job_roles WHERE id = :id");
|
||||
$stmt->execute(['id' => $id]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../auth_check.php');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 999;
|
||||
$is_active = isset($_POST['is_active']) ? ((int)$_POST['is_active'] === 1 ? 1 : 0) : 1;
|
||||
|
||||
if ($name === '') {
|
||||
echo json_encode(['success' => false, 'message' => 'Il nome della mansione è obbligatorio.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($id > 0) {
|
||||
$check = $pdo->prepare("SELECT COUNT(*) FROM job_roles WHERE name = :name AND id <> :id");
|
||||
$check->execute(['name' => $name, 'id' => $id]);
|
||||
if ((int)$check->fetchColumn() > 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Esiste già un\'altra mansione con questo nome.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE job_roles
|
||||
SET name = :name,
|
||||
description = :description,
|
||||
sort_order = :sort_order,
|
||||
is_active = :is_active,
|
||||
updated_at = NOW()
|
||||
WHERE id = :id
|
||||
");
|
||||
$stmt->execute([
|
||||
'name' => $name,
|
||||
'description' => $description !== '' ? $description : null,
|
||||
'sort_order' => $sort_order,
|
||||
'is_active' => $is_active,
|
||||
'id' => $id,
|
||||
]);
|
||||
|
||||
echo json_encode(['success' => true, 'id' => $id]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$check = $pdo->prepare("SELECT COUNT(*) FROM job_roles WHERE name = :name");
|
||||
$check->execute(['name' => $name]);
|
||||
if ((int)$check->fetchColumn() > 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Esiste già una mansione con questo nome.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO job_roles (name, description, sort_order, is_active, created_at, updated_at)
|
||||
VALUES (:name, :description, :sort_order, :is_active, NOW(), NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
'name' => $name,
|
||||
'description' => $description !== '' ? $description : null,
|
||||
'sort_order' => $sort_order,
|
||||
'is_active' => $is_active,
|
||||
]);
|
||||
|
||||
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../auth_check.php');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID corso non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$usage = $pdo->prepare("SELECT COUNT(*) FROM employee_trainings WHERE training_topic_id = :id");
|
||||
$usage->execute(['id' => $id]);
|
||||
if ((int)$usage->fetchColumn() > 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Questo corso ha già delle registrazioni di formazione e non può essere cancellato.',
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM training_topics WHERE id = :id");
|
||||
$stmt->execute(['id' => $id]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../auth_check.php');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$freqRaw = $_POST['default_frequency_months'] ?? '';
|
||||
$remRaw = $_POST['default_reminder_days'] ?? '';
|
||||
$sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 999;
|
||||
$is_active = isset($_POST['is_active']) ? ((int)$_POST['is_active'] === 1 ? 1 : 0) : 1;
|
||||
$is_mandatory = isset($_POST['is_mandatory']) && (int)$_POST['is_mandatory'] === 1 ? 1 : 0;
|
||||
|
||||
$freq = ($freqRaw === '' || $freqRaw === null) ? null : max(0, (int)$freqRaw);
|
||||
$rem = ($remRaw === '' || $remRaw === null) ? 30 : max(0, (int)$remRaw);
|
||||
|
||||
if ($name === '') {
|
||||
echo json_encode(['success' => false, 'message' => 'Il nome del corso è obbligatorio.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($id > 0) {
|
||||
$check = $pdo->prepare("SELECT COUNT(*) FROM training_topics WHERE name = :name AND id <> :id");
|
||||
$check->execute(['name' => $name, 'id' => $id]);
|
||||
if ((int)$check->fetchColumn() > 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Esiste già un altro corso con questo nome.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE training_topics
|
||||
SET name = :name,
|
||||
description = :description,
|
||||
default_frequency_months = :freq,
|
||||
default_reminder_days = :rem,
|
||||
sort_order = :sort_order,
|
||||
is_active = :is_active,
|
||||
is_mandatory = :is_mandatory,
|
||||
updated_at = NOW()
|
||||
WHERE id = :id
|
||||
");
|
||||
$stmt->execute([
|
||||
'name' => $name,
|
||||
'description' => $description !== '' ? $description : null,
|
||||
'freq' => $freq,
|
||||
'rem' => $rem,
|
||||
'sort_order' => $sort_order,
|
||||
'is_active' => $is_active,
|
||||
'is_mandatory' => $is_mandatory,
|
||||
'id' => $id,
|
||||
]);
|
||||
|
||||
echo json_encode(['success' => true, 'id' => $id]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$check = $pdo->prepare("SELECT COUNT(*) FROM training_topics WHERE name = :name");
|
||||
$check->execute(['name' => $name]);
|
||||
if ((int)$check->fetchColumn() > 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Esiste già un corso con questo nome.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO training_topics
|
||||
(name, description, default_frequency_months, default_reminder_days, sort_order, is_active, is_mandatory, created_at, updated_at)
|
||||
VALUES
|
||||
(:name, :description, :freq, :rem, :sort_order, :is_active, :is_mandatory, NOW(), NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
'name' => $name,
|
||||
'description' => $description !== '' ? $description : null,
|
||||
'freq' => $freq,
|
||||
'rem' => $rem,
|
||||
'sort_order' => $sort_order,
|
||||
'is_active' => $is_active,
|
||||
'is_mandatory' => $is_mandatory,
|
||||
]);
|
||||
|
||||
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,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>';
|
||||
}
|
||||
@@ -256,7 +256,6 @@ $departments = $stmtDepartments->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
<!-- jQuery and Bootstrap -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
@@ -367,7 +366,7 @@ $departments = $stmtDepartments->fetchAll(PDO::FETCH_ASSOC);
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+166
-67
@@ -17,16 +17,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
|
||||
|
||||
try {
|
||||
if ($action === 'add') {
|
||||
// Codice originale per add
|
||||
$employee_code = trim($_POST['employee_code'] ?? '');
|
||||
$first_name = trim($_POST['first_name'] ?? '');
|
||||
$last_name = trim($_POST['last_name'] ?? '');
|
||||
$address = trim($_POST['address'] ?? '');
|
||||
$phone = trim($_POST['phone'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$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'] ?? '');
|
||||
$status = trim($_POST['status'] ?? 'active');
|
||||
$auth_user_id = $_POST['auth_user_id'] !== '' ? (int)$_POST['auth_user_id'] : null;
|
||||
$role_id = $_POST['role_id'] !== '' ? (int)$_POST['role_id'] : null;
|
||||
$role_id = $_POST['role_id'] !== '' ? (int)$_POST['role_id'] : null;
|
||||
|
||||
if ($first_name === '' || $last_name === '') {
|
||||
echo json_encode([
|
||||
@@ -35,23 +37,31 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Email non valida.']);
|
||||
exit;
|
||||
}
|
||||
if (!in_array($status, ['active', 'inactive', 'suspended'], true)) {
|
||||
$status = 'active';
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO employees
|
||||
(auth_user_id, employee_code, first_name, last_name, department_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
|
||||
(: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->execute([
|
||||
'auth_user_id' => $auth_user_id,
|
||||
'employee_code' => $employee_code !== '' ? $employee_code : null,
|
||||
'first_name' => $first_name,
|
||||
'last_name' => $last_name,
|
||||
'address' => $address !== '' ? $address : null,
|
||||
'phone' => $phone !== '' ? $phone : null,
|
||||
'email' => $email !== '' ? $email : null,
|
||||
'department_id' => $department_id,
|
||||
'position' => $position !== '' ? $position : null,
|
||||
'job_role_id' => $job_role_id,
|
||||
'hire_date' => $hire_date !== '' ? $hire_date : null,
|
||||
'status' => $status
|
||||
]);
|
||||
@@ -74,17 +84,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
|
||||
}
|
||||
|
||||
if ($action === 'edit') {
|
||||
// Codice originale per edit
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$employee_code = trim($_POST['employee_code'] ?? '');
|
||||
$first_name = trim($_POST['first_name'] ?? '');
|
||||
$last_name = trim($_POST['last_name'] ?? '');
|
||||
$address = trim($_POST['address'] ?? '');
|
||||
$phone = trim($_POST['phone'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$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'] ?? '');
|
||||
$status = trim($_POST['status'] ?? 'active');
|
||||
$auth_user_id = $_POST['auth_user_id'] !== '' ? (int)$_POST['auth_user_id'] : null;
|
||||
$role_id = $_POST['role_id'] !== '' ? (int)$_POST['role_id'] : null;
|
||||
$role_id = $_POST['role_id'] !== '' ? (int)$_POST['role_id'] : null;
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid employee ID.']);
|
||||
@@ -98,7 +110,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Email non valida.']);
|
||||
exit;
|
||||
}
|
||||
if (!in_array($status, ['active', 'inactive', 'suspended'], true)) {
|
||||
$status = 'active';
|
||||
}
|
||||
@@ -108,8 +123,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
|
||||
employee_code = :employee_code,
|
||||
first_name = :first_name,
|
||||
last_name = :last_name,
|
||||
address = :address,
|
||||
phone = :phone,
|
||||
email = :email,
|
||||
department_id = :department_id,
|
||||
position = :position,
|
||||
job_role_id = :job_role_id,
|
||||
hire_date = :hire_date,
|
||||
status = :status,
|
||||
updated_at = NOW()
|
||||
@@ -120,8 +138,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj
|
||||
'employee_code' => $employee_code !== '' ? $employee_code : null,
|
||||
'first_name' => $first_name,
|
||||
'last_name' => $last_name,
|
||||
'address' => $address !== '' ? $address : null,
|
||||
'phone' => $phone !== '' ? $phone : null,
|
||||
'email' => $email !== '' ? $email : null,
|
||||
'department_id' => $department_id,
|
||||
'position' => $position !== '' ? $position : null,
|
||||
'job_role_id' => $job_role_id,
|
||||
'hire_date' => $hire_date !== '' ? $hire_date : null,
|
||||
'status' => $status,
|
||||
'id' => $id
|
||||
@@ -223,6 +244,7 @@ $sql = "
|
||||
SELECT e.*,
|
||||
d.name AS department_name,
|
||||
d.color AS department_color,
|
||||
jr.name AS job_role_name,
|
||||
au.email AS user_email,
|
||||
au.role_id AS user_role_id,
|
||||
ar.display_name AS role_display_name,
|
||||
@@ -230,6 +252,7 @@ $sql = "
|
||||
CONCAT(COALESCE(au.first_name, ''), ' ', COALESCE(au.last_name, '')) AS user_fullname
|
||||
FROM employees e
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
LEFT JOIN job_roles jr ON jr.id = e.job_role_id
|
||||
LEFT JOIN auth_users au ON e.auth_user_id = au.id
|
||||
LEFT JOIN auth_roles ar ON ar.id = au.role_id
|
||||
ORDER BY e.id DESC
|
||||
@@ -237,6 +260,11 @@ $sql = "
|
||||
$stmtEmployees = $pdo->query($sql);
|
||||
$employees = $stmtEmployees->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Job roles for the dropdown
|
||||
$jobRoles = $pdo->query("
|
||||
SELECT id, name FROM job_roles WHERE is_active = 1 ORDER BY sort_order, name
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Users list for select
|
||||
$sqlUsers = "
|
||||
SELECT id,
|
||||
@@ -297,7 +325,6 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
<!-- jQuery e Bootstrap -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
@@ -415,7 +442,7 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
@@ -448,14 +475,15 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Code</th>
|
||||
<th>Name</th>
|
||||
<th>Department</th>
|
||||
<th>Position</th>
|
||||
<th>Hire Date</th>
|
||||
<th>Status</th>
|
||||
<th>Linked User</th>
|
||||
<th>Actions</th>
|
||||
<th>Codice</th>
|
||||
<th>Nome</th>
|
||||
<th>Contatti</th>
|
||||
<th>Reparto</th>
|
||||
<th>Mansione</th>
|
||||
<th>Data Assunzione</th>
|
||||
<th>Stato</th>
|
||||
<th>Utente collegato</th>
|
||||
<th>Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -484,7 +512,24 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
<tr>
|
||||
<td><?= (int)$row['id'] ?></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>
|
||||
<?php if (!empty($row['department_name'])): ?>
|
||||
<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; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($row['position'] ?? '') ?></td>
|
||||
<td><?= !empty($row['job_role_name']) ? htmlspecialchars($row['job_role_name']) : '-' ?></td>
|
||||
<td><?= $hireDate ?></td>
|
||||
<td>
|
||||
<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-last_name="<?= htmlspecialchars($row['last_name'] ?? '', ENT_QUOTES) ?>"
|
||||
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-status="<?= htmlspecialchars($status, ENT_QUOTES) ?>"
|
||||
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">
|
||||
<form id="addEmployeeForm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Employee Code</label>
|
||||
<input type="text" class="form-control" id="addEmployeeCode" name="employee_code" placeholder="Optional">
|
||||
<label class="form-label fw-semibold">Codice Dipendente</label>
|
||||
<input type="text" class="form-control" id="addEmployeeCode" name="employee_code" placeholder="Opzionale">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">First Name</label>
|
||||
<label class="form-label fw-semibold">Nome</label>
|
||||
<input type="text" class="form-control" id="addFirstName" name="first_name" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Last Name</label>
|
||||
<label class="form-label fw-semibold">Cognome</label>
|
||||
<input type="text" class="form-control" id="addLastName" name="last_name" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Indirizzo</label>
|
||||
<input type="text" class="form-control" id="addAddress" name="address" placeholder="Via, città, CAP">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Department</label>
|
||||
<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%;">
|
||||
<option value="">-- Select Department --</option>
|
||||
<option value="">-- Nessuno --</option>
|
||||
<?php foreach ($departments as $d): ?>
|
||||
<option value="<?= (int)$d['id'] ?>">
|
||||
<?= htmlspecialchars($d['name']) ?>
|
||||
@@ -589,30 +653,35 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Position</label>
|
||||
<input type="text" class="form-control" id="addPosition" name="position" placeholder="e.g. Line Operator">
|
||||
<label class="form-label fw-semibold">Mansione</label>
|
||||
<select class="form-select" id="addJobRoleId" name="job_role_id" style="width:100%;">
|
||||
<option value="">-- Nessuna --</option>
|
||||
<?php foreach ($jobRoles as $jr): ?>
|
||||
<option value="<?= (int)$jr['id'] ?>"><?= htmlspecialchars($jr['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Hire Date</label>
|
||||
<label class="form-label fw-semibold">Data Assunzione</label>
|
||||
<input type="date" class="form-control" id="addHireDate" name="hire_date">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Status</label>
|
||||
<label class="form-label fw-semibold">Stato</label>
|
||||
<select class="form-select" id="addStatus" name="status">
|
||||
<option value="active" selected>Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
<option value="suspended">Suspended</option>
|
||||
<option value="active" selected>Attivo</option>
|
||||
<option value="inactive">Cessato</option>
|
||||
<option value="suspended">Sospeso</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Linked User (auth_users)</label>
|
||||
<label class="form-label fw-semibold">Utente collegato (account login)</label>
|
||||
<select class="form-select" id="addAuthUserId" name="auth_user_id" style="width:100%;">
|
||||
<option value="">-- None --</option>
|
||||
<option value="">-- Nessuno --</option>
|
||||
<?php foreach ($users as $u): ?>
|
||||
<option value="<?= (int)$u['id'] ?>" data-role_id="<?= (int)$u['role_id'] ?>">
|
||||
<?= htmlspecialchars($u['label']) ?>
|
||||
@@ -622,16 +691,16 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
</div>
|
||||
|
||||
<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%;">
|
||||
<option value="">-- Select Role --</option>
|
||||
<option value="">-- Seleziona ruolo --</option>
|
||||
<?php foreach ($roles as $r): ?>
|
||||
<option value="<?= (int)$r['id'] ?>">
|
||||
<?= htmlspecialchars($r['display_name'] ?: $r['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<small class="text-muted">Visible only when an auth user is linked.</small>
|
||||
<small class="text-muted">Visibile solo quando è collegato un utente di sistema.</small>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
@@ -658,26 +727,42 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
<input type="hidden" id="editEmployeeId">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Employee Code</label>
|
||||
<input type="text" class="form-control" id="editEmployeeCode" name="employee_code" placeholder="Optional">
|
||||
<label class="form-label fw-semibold">Codice Dipendente</label>
|
||||
<input type="text" class="form-control" id="editEmployeeCode" name="employee_code" placeholder="Opzionale">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">First Name</label>
|
||||
<label class="form-label fw-semibold">Nome</label>
|
||||
<input type="text" class="form-control" id="editFirstName" name="first_name" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Last Name</label>
|
||||
<label class="form-label fw-semibold">Cognome</label>
|
||||
<input type="text" class="form-control" id="editLastName" name="last_name" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Indirizzo</label>
|
||||
<input type="text" class="form-control" id="editAddress" name="address">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Department</label>
|
||||
<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%;">
|
||||
<option value="">-- Select Department --</option>
|
||||
<option value="">-- Nessuno --</option>
|
||||
<?php foreach ($departments as $d): ?>
|
||||
<option value="<?= (int)$d['id'] ?>">
|
||||
<?= htmlspecialchars($d['name']) ?>
|
||||
@@ -687,30 +772,35 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Position</label>
|
||||
<input type="text" class="form-control" id="editPosition" name="position">
|
||||
<label class="form-label fw-semibold">Mansione</label>
|
||||
<select class="form-select" id="editJobRoleId" name="job_role_id" style="width:100%;">
|
||||
<option value="">-- Nessuna --</option>
|
||||
<?php foreach ($jobRoles as $jr): ?>
|
||||
<option value="<?= (int)$jr['id'] ?>"><?= htmlspecialchars($jr['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Hire Date</label>
|
||||
<label class="form-label fw-semibold">Data Assunzione</label>
|
||||
<input type="date" class="form-control" id="editHireDate" name="hire_date">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Status</label>
|
||||
<label class="form-label fw-semibold">Stato</label>
|
||||
<select class="form-select" id="editStatus" name="status">
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
<option value="suspended">Suspended</option>
|
||||
<option value="active">Attivo</option>
|
||||
<option value="inactive">Cessato</option>
|
||||
<option value="suspended">Sospeso</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Linked User (auth_users)</label>
|
||||
<label class="form-label fw-semibold">Utente collegato (account login)</label>
|
||||
<select class="form-select" id="editAuthUserId" name="auth_user_id" style="width:100%;">
|
||||
<option value="">-- None --</option>
|
||||
<option value="">-- Nessuno --</option>
|
||||
<?php foreach ($users as $u): ?>
|
||||
<option value="<?= (int)$u['id'] ?>" data-role_id="<?= (int)$u['role_id'] ?>">
|
||||
<?= htmlspecialchars($u['label']) ?>
|
||||
@@ -720,16 +810,16 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
</div>
|
||||
|
||||
<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%;">
|
||||
<option value="">-- Select Role --</option>
|
||||
<option value="">-- Seleziona ruolo --</option>
|
||||
<?php foreach ($roles as $r): ?>
|
||||
<option value="<?= (int)$r['id'] ?>">
|
||||
<?= htmlspecialchars($r['display_name'] ?: $r['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<small class="text-muted">Visible only when an auth user is linked.</small>
|
||||
<small class="text-muted">Visibile solo quando è collegato un utente di sistema.</small>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
@@ -784,7 +874,7 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
|
||||
// Select2 on user selects
|
||||
$('#addAuthUserId, #editAuthUserId, #addDepartmentId, #editDepartmentId, #addRoleId, #editRoleId').select2({
|
||||
$('#addAuthUserId, #editAuthUserId, #addDepartmentId, #editDepartmentId, #addRoleId, #editRoleId, #addJobRoleId, #editJobRoleId').select2({
|
||||
theme: 'bootstrap-5',
|
||||
width: '100%'
|
||||
});
|
||||
@@ -834,8 +924,11 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
payload.append('employee_code', $("#addEmployeeCode").val().trim());
|
||||
payload.append('first_name', $("#addFirstName").val().trim());
|
||||
payload.append('last_name', $("#addLastName").val().trim());
|
||||
payload.append('address', $("#addAddress").val().trim());
|
||||
payload.append('phone', $("#addPhone").val().trim());
|
||||
payload.append('email', $("#addEmail").val().trim());
|
||||
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('status', $("#addStatus").val());
|
||||
payload.append('auth_user_id', $("#addAuthUserId").val() || '');
|
||||
@@ -884,7 +977,10 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
$("#editFirstName").val(btn.data("first_name"));
|
||||
$("#editLastName").val(btn.data("last_name"));
|
||||
$("#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"));
|
||||
$("#editStatus").val(btn.data("status"));
|
||||
|
||||
@@ -916,8 +1012,11 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|
||||
payload.append('employee_code', $("#editEmployeeCode").val().trim());
|
||||
payload.append('first_name', $("#editFirstName").val().trim());
|
||||
payload.append('last_name', $("#editLastName").val().trim());
|
||||
payload.append('address', $("#editAddress").val().trim());
|
||||
payload.append('phone', $("#editPhone").val().trim());
|
||||
payload.append('email', $("#editEmail").val().trim());
|
||||
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('status', $("#editStatus").val());
|
||||
payload.append('auth_user_id', $("#editAuthUserId").val() || '');
|
||||
|
||||
@@ -40,8 +40,7 @@ $kindofrole = $user->present()->role_id;
|
||||
//$iduserlogin="1";
|
||||
//$nameuser="Claudio";
|
||||
//$emailuser="info@claudiosironi.com";
|
||||
?>
|
||||
<?php
|
||||
|
||||
if (session_status() == PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
@@ -54,13 +53,11 @@ $_SESSION["emailuser"] = $emailuser;
|
||||
$_SESSION["photouser"] = $avatar;
|
||||
$photouser = $_SESSION["photouser"];
|
||||
$photousername = basename($avatar);
|
||||
?>
|
||||
|
||||
|
||||
<?php //include files
|
||||
//include files
|
||||
|
||||
require_once(__DIR__ . '/../../languages/en/general.php');
|
||||
|
||||
//include("generalsettings.php");
|
||||
|
||||
require_once __DIR__ . '/permissions_helper.php';
|
||||
?>
|
||||
|
||||
@@ -6,117 +6,386 @@
|
||||
<div>
|
||||
<h4 class="logo-text"><?= htmlspecialchars('ZIBOGOMMA', ENT_QUOTES, 'UTF-8'); ?></h4>
|
||||
</div>
|
||||
<div class="toggle-icon ms-auto"><i class='bx bx-arrow-back'></i>
|
||||
<div class="toggle-icon ms-auto">
|
||||
<i class='bx bx-arrow-back'></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--navigation-->
|
||||
<ul class="metismenu" id="menu">
|
||||
<!-- user, admin, superuser menù -->
|
||||
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('User')) || (Auth::user()->hasRole('Superuser'))) : ?>
|
||||
|
||||
<?php if (userCan('production.dashboard.view')) : ?>
|
||||
<li>
|
||||
<a href="production_dashboard.php">
|
||||
<div class="parent-icon"><i class="bx bx-home-alt"></i>
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-home-alt"></i>
|
||||
</div>
|
||||
<div class="menu-title">Dashboard</div>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php
|
||||
$canSeeProgramming =
|
||||
userCan('production.programming.view')
|
||||
|| userCan('templates.dashboard.view')
|
||||
|| userCan('templates.create.view');
|
||||
?>
|
||||
|
||||
<?php if ($canSeeProgramming) : ?>
|
||||
<li>
|
||||
<a href="javascript:;" class="has-arrow">
|
||||
<div class="parent-icon"><i class="bx bx-category"></i>
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-category"></i>
|
||||
</div>
|
||||
<div class="menu-title">Programmazione</div>
|
||||
</a>
|
||||
<ul>
|
||||
<li> <a href="templates_dashboard.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?></a>
|
||||
</li>
|
||||
<li> <a href="insert_template_xls.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?></a>
|
||||
</li>
|
||||
|
||||
<ul>
|
||||
<?php if (userCan('templates.dashboard.view')) : ?>
|
||||
<li>
|
||||
<a href="templates_dashboard.php">
|
||||
<i class='bx bx-radio-circle'></i>
|
||||
<?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('templates.create.view')) : ?>
|
||||
<li>
|
||||
<a href="insert_template_xls.php">
|
||||
<i class='bx bx-radio-circle'></i>
|
||||
<?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('production.programming.view')) : ?>
|
||||
<li>
|
||||
<a href="produzione_programmazione_drag.php">
|
||||
<i class='bx bx-radio-circle'></i>
|
||||
Programmazione Produzione
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php
|
||||
$canSeeFunctions =
|
||||
userCan('masterdata.mescole.view')
|
||||
|| userCan('masterdata.matrici.view')
|
||||
|| userCan('masterdata.linee.view')
|
||||
|| userCan('masterdata.packaging.view')
|
||||
|| userCan('masterdata.suppliers.view')
|
||||
|| userCan('masterdata.lookup.view')
|
||||
|| userCan('masterdata.worksheets.view');
|
||||
?>
|
||||
|
||||
<?php if ($canSeeFunctions) : ?>
|
||||
<li>
|
||||
<a href="javascript:;" class="has-arrow">
|
||||
<div class="parent-icon"><i class="bx bx-category"></i>
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-category"></i>
|
||||
</div>
|
||||
<div class="menu-title">Funzioni</div>
|
||||
</a>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="mescole.php"><i class='bx bx-radio-circle'></i>Mescole</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="matrici.php"><i class='bx bx-radio-circle'></i>Matrici</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="linee.php"><i class='bx bx-radio-circle'></i>Linee di produzione</a>
|
||||
</li>
|
||||
<?php if (userCan('masterdata.mescole.view')) : ?>
|
||||
<li>
|
||||
<a href="mescole.php">
|
||||
<i class='bx bx-radio-circle'></i>Mescole
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('masterdata.matrici.view')) : ?>
|
||||
<li>
|
||||
<a href="matrici.php">
|
||||
<i class='bx bx-radio-circle'></i>Matrici
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('masterdata.linee.view')) : ?>
|
||||
<li>
|
||||
<a href="linee.php">
|
||||
<i class='bx bx-radio-circle'></i>Linee di produzione
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('masterdata.packaging.view')) : ?>
|
||||
<li>
|
||||
<a href="packaging_items.php">
|
||||
<i class='bx bx-radio-circle'></i>Imballaggi
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('masterdata.suppliers.view')) : ?>
|
||||
<li>
|
||||
<a href="suppliers.php">
|
||||
<i class='bx bx-radio-circle'></i>Suppliers
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('masterdata.lookup.view')) : ?>
|
||||
<li>
|
||||
<a href="lookup_values.php">
|
||||
<i class='bx bx-radio-circle'></i>Setup
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('masterdata.worksheets.view')) : ?>
|
||||
<li>
|
||||
<a href="worksheets.php">
|
||||
<i class='bx bx-radio-circle'></i>Fogli di lavoro
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php
|
||||
$canSeeProduction =
|
||||
userCan('production.line_view.view')
|
||||
|| userCan('production.stats.view')
|
||||
|| userCan('production.manager.view')
|
||||
|| userCan('production.manager_stats.view')
|
||||
|| userCan('warehouse.dashboard.view');
|
||||
?>
|
||||
|
||||
|
||||
|
||||
<?php if ($canSeeProduction) : ?>
|
||||
<li>
|
||||
<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 class="menu-title">Scadenzario</div>
|
||||
</a>
|
||||
|
||||
<ul>
|
||||
<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>
|
||||
<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>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="menu-label">Others</li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<li>
|
||||
<a href="https://helpdesk.cesoft.io" target="_blank">
|
||||
<div class="parent-icon"><i class="bx bx-support"></i>
|
||||
</div>
|
||||
<div class="menu-title">Support</div>
|
||||
</a>
|
||||
</li>
|
||||
<?php
|
||||
endif; ?>
|
||||
<!-- admin, superuser menù -->
|
||||
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('Superuser'))) : ?>
|
||||
<?php
|
||||
endif; ?>
|
||||
<!-- admin menù -->
|
||||
<?php if (Auth::user()->hasRole('Admin')) : ?>
|
||||
<li class="menu-label">Others</li>
|
||||
|
||||
<li>
|
||||
<a href="https://helpdesk.cesoft.io" target="_blank">
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-support"></i>
|
||||
</div>
|
||||
<div class="menu-title">Support</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
<?php if (userCan('users.manage')) : ?>
|
||||
<li class="menu-label">Admin Menù</li>
|
||||
|
||||
<li>
|
||||
<a href="../" target="_blank">
|
||||
<div class="parent-icon"><i class="bx bx-support"></i>
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-user-circle"></i>
|
||||
</div>
|
||||
<div class="menu-title">User Management</div>
|
||||
</a>
|
||||
</li>
|
||||
<!-- <li>
|
||||
<a href="template/index.html" target="_blank">
|
||||
<div class="parent-icon"><i class="bx bx-support"></i>
|
||||
</div>
|
||||
<div class="menu-title">Template</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://codervent.com/rocker/documentation/index.html" target="_blank">
|
||||
<div class="parent-icon"><i class="bx bx-folder"></i>
|
||||
</div>
|
||||
<div class="menu-title">Documentation</div>
|
||||
</a>
|
||||
</li> -->
|
||||
<?php
|
||||
endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
</ul>
|
||||
<!--end navigation-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
if (!function_exists('userCan')) {
|
||||
/**
|
||||
* Check if current user has a Vanguard permission.
|
||||
* Uses Vanguard native method if available, otherwise falls back to DB check.
|
||||
*/
|
||||
function userCan($permissionName)
|
||||
{
|
||||
global $kindofrole;
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vanguard / Laravel-style methods, depending on installed version/customization.
|
||||
if (method_exists($user, 'hasPermission')) {
|
||||
return $user->hasPermission($permissionName);
|
||||
}
|
||||
|
||||
if (method_exists($user, 'hasPermissionTo')) {
|
||||
return $user->hasPermissionTo($permissionName);
|
||||
}
|
||||
|
||||
if (method_exists($user, 'can')) {
|
||||
return $user->can($permissionName);
|
||||
}
|
||||
|
||||
// Fallback: direct DB check using existing Vanguard tables.
|
||||
static $permissions = null;
|
||||
|
||||
if ($permissions === null) {
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT p.name
|
||||
FROM auth_permissions p
|
||||
INNER JOIN auth_permission_role pr ON pr.permission_id = p.id
|
||||
WHERE pr.role_id = ?
|
||||
");
|
||||
$stmt->execute([(int)$kindofrole]);
|
||||
|
||||
$permissions = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
}
|
||||
|
||||
return in_array($permissionName, $permissions, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('visibleButtons')) {
|
||||
/**
|
||||
* Filter visible buttons.
|
||||
*/
|
||||
function visibleButtons(array $buttons)
|
||||
{
|
||||
return array_values(array_filter($buttons, function ($button) {
|
||||
return empty($button['permission']) || userCan($button['permission']);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,11 @@
|
||||
<?php
|
||||
// Build an absolute URL to employee-profile.php so it works from any depth
|
||||
// (e.g. /userarea/index.php, /userarea/scadenzario/index.php).
|
||||
$__scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
|
||||
$__pos = strpos($__scriptName, '/userarea/');
|
||||
$__base = $__pos !== false ? substr($__scriptName, 0, $__pos) : '';
|
||||
$__myProfileHref = $__base . '/userarea/employee-profile.php';
|
||||
?>
|
||||
<header>
|
||||
<div class="topbar d-flex align-items-center">
|
||||
<nav class="navbar navbar-expand gap-3">
|
||||
@@ -85,6 +93,12 @@
|
||||
</div>
|
||||
</a>
|
||||
<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>
|
||||
<a class="dropdown-item d-flex align-items-center" href="../users">
|
||||
<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>
|
||||
@@ -0,0 +1,428 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
/* ==========================================
|
||||
PAGE DATA
|
||||
========================================== */
|
||||
$sql = "
|
||||
SELECT jr.*,
|
||||
(SELECT COUNT(*) FROM employees e WHERE e.job_role_id = jr.id) AS employees_count
|
||||
FROM job_roles jr
|
||||
ORDER BY jr.sort_order ASC, jr.name ASC
|
||||
";
|
||||
$jobRoles = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
<?php include('cssinclude.php'); ?>
|
||||
<title>Gestione Mansioni - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||
|
||||
<style>
|
||||
body { font-size: 1.05rem; background: #f8fafc; }
|
||||
.card { border-radius: 16px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); }
|
||||
.back-dashboard {
|
||||
background-color: #cfe3ff !important; color: #1f2d3d !important;
|
||||
border: 1px solid #bcd4f4 !important; border-radius: 10px;
|
||||
font-weight: 600; padding: 10px 18px;
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
.back-dashboard:hover { background-color: #b9d3ff !important; transform: translateY(-2px); }
|
||||
.btn-add { background-color: #0d6efd; color: #fff; border-radius: 8px; padding: 10px 20px; font-weight: 500; }
|
||||
.btn-add:hover { background-color: #0b5ed7; transform: scale(1.02); }
|
||||
.table thead { background-color: #cfe3ff; color: #1f2d3d; }
|
||||
.modal-content { border-radius: 16px; }
|
||||
#tabellaJobRoles thead th { text-align: center; vertical-align: middle; }
|
||||
.badge-status { padding: 0.25rem 0.6rem; border-radius: 999px; font-size: 0.8rem; font-weight: 600; }
|
||||
.badge-status.active { background-color: #d1fae5; color: #065f46; }
|
||||
.badge-status.inactive { background-color: #e5e7eb; color: #374151; }
|
||||
.description-cell {
|
||||
max-width: 320px; white-space: nowrap; overflow: hidden;
|
||||
text-overflow: ellipsis; text-align: left;
|
||||
}
|
||||
@media (max-width: 767.98px) {
|
||||
.card-header { flex-direction: column; align-items: flex-start !important; gap: .5rem; }
|
||||
.back-dashboard { width: 100%; }
|
||||
.btn-add { width: 100%; }
|
||||
}
|
||||
|
||||
.jr-card {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 12px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.jr-card-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0 0 4px 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
.jr-card-desc {
|
||||
color: #475569;
|
||||
font-size: 0.95rem;
|
||||
margin: 0 0 10px 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
.jr-card-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 14px;
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.jr-card-meta b { color: #1f2937; font-weight: 600; }
|
||||
.jr-card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.jr-card-actions .btn {
|
||||
flex: 1;
|
||||
}
|
||||
.jr-empty {
|
||||
text-align: center;
|
||||
color: #94a3b8;
|
||||
padding: 24px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
<div class="card p-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<h5 class="mb-0">Gestione Mansioni</h5>
|
||||
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
|
||||
↩️ Torna alla Dashboard
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
||||
<h6 class="fw-semibold mb-0">Elenco Mansioni / Job Roles</h6>
|
||||
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addJobRoleModal">
|
||||
➕ Aggiungi Mansione
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- DESKTOP / TABLET ≥768px: TABLE -->
|
||||
<div class="table-responsive d-none d-md-block"><!-- hide on <md -->
|
||||
<table id="tabellaJobRoles" class="table table-striped align-middle text-center" style="width:100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nome</th>
|
||||
<th>Descrizione</th>
|
||||
<th>Ordine</th>
|
||||
<th>Stato</th>
|
||||
<th>Dipendenti</th>
|
||||
<th>Creato</th>
|
||||
<th>Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($jobRoles as $row): ?>
|
||||
<?php
|
||||
$id = (int)$row['id'];
|
||||
$name = $row['name'] ?? '';
|
||||
$description = $row['description'] ?? '';
|
||||
$sortOrder = (int)($row['sort_order'] ?? 999);
|
||||
$isActive = (int)($row['is_active'] ?? 1);
|
||||
$cnt = (int)($row['employees_count'] ?? 0);
|
||||
$statusClass = $isActive === 1 ? 'active' : 'inactive';
|
||||
$statusLabel = $isActive === 1 ? 'Attivo' : 'Inattivo';
|
||||
$createdAt = !empty($row['created_at'])
|
||||
? date('d/m/Y H:i', strtotime($row['created_at']))
|
||||
: '-';
|
||||
?>
|
||||
<tr>
|
||||
<td><?= $id ?></td>
|
||||
<td class="fw-semibold text-start"><?= htmlspecialchars($name) ?></td>
|
||||
<td class="description-cell" title="<?= htmlspecialchars($description, ENT_QUOTES) ?>">
|
||||
<?= $description !== '' ? htmlspecialchars($description) : '-' ?>
|
||||
</td>
|
||||
<td><?= $sortOrder ?></td>
|
||||
<td>
|
||||
<span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span>
|
||||
</td>
|
||||
<td><?= $cnt ?></td>
|
||||
<td><?= $createdAt ?></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-secondary edit-job-role"
|
||||
data-id="<?= $id ?>"
|
||||
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
|
||||
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
|
||||
data-sort_order="<?= $sortOrder ?>"
|
||||
data-is_active="<?= $isActive ?>">
|
||||
✏️ Modifica
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger delete-job-role"
|
||||
data-id="<?= $id ?>"
|
||||
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
|
||||
data-count="<?= $cnt ?>">
|
||||
🗑️ Cancella
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- MOBILE <768px: CARDS -->
|
||||
<div class="d-block d-md-none">
|
||||
<?php if (empty($jobRoles)): ?>
|
||||
<div class="jr-empty">Nessuna mansione presente</div>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($jobRoles as $row): ?>
|
||||
<?php
|
||||
$id = (int)$row['id'];
|
||||
$name = $row['name'] ?? '';
|
||||
$description = $row['description'] ?? '';
|
||||
$sortOrder = (int)($row['sort_order'] ?? 999);
|
||||
$isActive = (int)($row['is_active'] ?? 1);
|
||||
$cnt = (int)($row['employees_count'] ?? 0);
|
||||
$statusClass = $isActive === 1 ? 'active' : 'inactive';
|
||||
$statusLabel = $isActive === 1 ? 'Attivo' : 'Inattivo';
|
||||
?>
|
||||
<div class="jr-card">
|
||||
<h6 class="jr-card-title"><?= htmlspecialchars($name) ?></h6>
|
||||
<?php if ($description !== ''): ?>
|
||||
<p class="jr-card-desc"><?= htmlspecialchars($description) ?></p>
|
||||
<?php endif; ?>
|
||||
<div class="jr-card-meta">
|
||||
<span><span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span></span>
|
||||
<span><b>Dipendenti:</b> <?= $cnt ?></span>
|
||||
<span><b>Ordine:</b> <?= $sortOrder ?></span>
|
||||
</div>
|
||||
<div class="jr-card-actions">
|
||||
<button class="btn btn-sm btn-outline-secondary edit-job-role"
|
||||
data-id="<?= $id ?>"
|
||||
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
|
||||
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
|
||||
data-sort_order="<?= $sortOrder ?>"
|
||||
data-is_active="<?= $isActive ?>">
|
||||
✏️ Modifica
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger delete-job-role"
|
||||
data-id="<?= $id ?>"
|
||||
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
|
||||
data-count="<?= $cnt ?>">
|
||||
🗑️ Cancella
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<!-- ADD MODAL -->
|
||||
<div class="modal fade" id="addJobRoleModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered modal-fullscreen-sm-down">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header" style="background-color:#cfe3ff;">
|
||||
<h5 class="modal-title">Aggiungi Mansione</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="addJobRoleForm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Nome</label>
|
||||
<input type="text" class="form-control" id="addName" name="name" placeholder="es. Saldatore" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Descrizione</label>
|
||||
<textarea class="form-control" id="addDescription" name="description" rows="3" placeholder="Opzionale"></textarea>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Ordine</label>
|
||||
<input type="number" class="form-control" id="addSortOrder" name="sort_order" value="999" min="0">
|
||||
</div>
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Stato</label>
|
||||
<select class="form-select" id="addIsActive" name="is_active">
|
||||
<option value="1" selected>Attivo</option>
|
||||
<option value="0">Inattivo</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<button type="submit" class="btn btn-add">💾 Salva</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EDIT MODAL -->
|
||||
<div class="modal fade" id="editJobRoleModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered modal-fullscreen-sm-down">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header" style="background-color:#cfe3ff;">
|
||||
<h5 class="modal-title">Modifica Mansione</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="editJobRoleForm">
|
||||
<input type="hidden" id="editJobRoleId">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Nome</label>
|
||||
<input type="text" class="form-control" id="editName" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Descrizione</label>
|
||||
<textarea class="form-control" id="editDescription" name="description" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Ordine</label>
|
||||
<input type="number" class="form-control" id="editSortOrder" name="sort_order" value="999" min="0">
|
||||
</div>
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Stato</label>
|
||||
<select class="form-select" id="editIsActive" name="is_active">
|
||||
<option value="1">Attivo</option>
|
||||
<option value="0">Inattivo</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<button type="submit" class="btn btn-add">💾 Salva Modifiche</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#tabellaJobRoles').DataTable({
|
||||
order: [[3, 'asc'], [1, 'asc']],
|
||||
pageLength: 25,
|
||||
language: {
|
||||
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json',
|
||||
emptyTable: 'Nessuna mansione presente'
|
||||
}
|
||||
});
|
||||
|
||||
function ajaxPost(url, payload, successTitle, errorFallback) {
|
||||
return fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: payload.toString()
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
Swal.fire({ icon: "success", title: successTitle, confirmButtonColor: "#3085d6" })
|
||||
.then(() => location.reload());
|
||||
} else {
|
||||
Swal.fire({ icon: "error", title: "Errore", text: data.message || errorFallback });
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
Swal.fire({ icon: "error", title: "Errore", text: "Errore di comunicazione." });
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
$("#addJobRoleForm").on("submit", function(e) {
|
||||
e.preventDefault();
|
||||
const p = new URLSearchParams();
|
||||
p.append('name', $("#addName").val().trim());
|
||||
p.append('description', $("#addDescription").val().trim());
|
||||
p.append('sort_order', $("#addSortOrder").val());
|
||||
p.append('is_active', $("#addIsActive").val());
|
||||
ajaxPost("ajax/job_roles/save.php", p, "Salvato!", "Impossibile salvare la mansione.");
|
||||
});
|
||||
|
||||
$(document).on("click", ".edit-job-role", function() {
|
||||
const b = $(this);
|
||||
$("#editJobRoleId").val(b.data("id"));
|
||||
$("#editName").val(b.data("name"));
|
||||
$("#editDescription").val(b.data("description"));
|
||||
$("#editSortOrder").val(b.data("sort_order"));
|
||||
$("#editIsActive").val(String(b.data("is_active")));
|
||||
$("#editJobRoleModal").modal("show");
|
||||
});
|
||||
|
||||
$("#editJobRoleForm").on("submit", function(e) {
|
||||
e.preventDefault();
|
||||
const p = new URLSearchParams();
|
||||
p.append('id', $("#editJobRoleId").val());
|
||||
p.append('name', $("#editName").val().trim());
|
||||
p.append('description', $("#editDescription").val().trim());
|
||||
p.append('sort_order', $("#editSortOrder").val());
|
||||
p.append('is_active', $("#editIsActive").val());
|
||||
ajaxPost("ajax/job_roles/save.php", p, "Aggiornato!", "Impossibile aggiornare la mansione.");
|
||||
});
|
||||
|
||||
$(document).on("click", ".delete-job-role", function() {
|
||||
const id = $(this).data("id");
|
||||
const name = $(this).data("name");
|
||||
const cnt = parseInt($(this).data("count")) || 0;
|
||||
|
||||
if (cnt > 0) {
|
||||
Swal.fire({
|
||||
icon: "warning",
|
||||
title: "Impossibile cancellare",
|
||||
text: "La mansione \"" + name + "\" è assegnata a " + cnt + " dipendente/i. Rimuovi prima l'associazione."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: "Confermi la cancellazione?",
|
||||
text: name ? ("Mansione: " + name) : "La mansione verrà cancellata.",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#d33",
|
||||
cancelButtonColor: "#6c757d",
|
||||
confirmButtonText: "Sì, cancella",
|
||||
cancelButtonText: "Annulla"
|
||||
}).then((result) => {
|
||||
if (!result.isConfirmed) return;
|
||||
const p = new URLSearchParams();
|
||||
p.append('id', id);
|
||||
ajaxPost("ajax/job_roles/delete.php", p, "Cancellato!", "Impossibile cancellare la mansione.");
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -42,7 +42,6 @@ $params = $stmtParams->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
<!-- jQuery / Bootstrap / SweetAlert -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
<!-- jQuery e Bootstrap -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
@@ -118,7 +117,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -261,7 +261,7 @@ function h($v)
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -874,7 +874,7 @@ $isEdit = ($worksheet_id > 0);
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ $rows_special = array_filter($rows, function ($r) {
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -551,7 +551,7 @@ function revisionLabel($rev)
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
<!-- jQuery e Bootstrap -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
@@ -138,7 +137,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
<!-- jQuery + Bootstrap -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
@@ -133,7 +132,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
|
||||
<!-- Bootstrap (se già incluso puoi rimuoverlo) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- SweetAlert2 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||
|
||||
@@ -1,4 +1,184 @@
|
||||
<?php include('include/headscript.php'); ?>
|
||||
<?php
|
||||
$dashboardSections = [
|
||||
[
|
||||
'id' => 'secOperativo',
|
||||
'title' => 'Operativo',
|
||||
'subtitle' => 'Azioni principali di produzione e attività in scadenza',
|
||||
'icon' => '🚀',
|
||||
'open' => true,
|
||||
'buttons' => [
|
||||
[
|
||||
'label' => 'Programmazione',
|
||||
'icon' => '🗓️',
|
||||
'class' => 'btn-programmazione',
|
||||
'url' => 'produzione_programmazione_drag.php',
|
||||
'permission' => 'production.programming.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Line View',
|
||||
'icon' => '⚙️',
|
||||
'class' => 'btn-status',
|
||||
'url' => 'production_line_view2.php',
|
||||
'permission' => 'production.line_view.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Statistiche',
|
||||
'icon' => '📈',
|
||||
'class' => 'btn-statistiche',
|
||||
'url' => 'production_stats.php',
|
||||
'permission' => 'production.stats.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Manager',
|
||||
'icon' => '👔',
|
||||
'class' => 'btn-manager',
|
||||
'url' => 'manager_produzione.php',
|
||||
'permission' => 'production.manager.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Manager Stats',
|
||||
'icon' => '📊',
|
||||
'class' => 'btn-manager-stats',
|
||||
'url' => 'manager_stats.php',
|
||||
'permission' => 'production.manager_stats.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Magazzino',
|
||||
'icon' => '📦',
|
||||
'class' => 'btn-magazzino',
|
||||
'url' => 'warehouse_dashboard.php',
|
||||
'permission' => 'warehouse.dashboard.view',
|
||||
],
|
||||
[
|
||||
'label' => '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>
|
||||
<html lang="it">
|
||||
|
||||
@@ -298,17 +478,66 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<?php
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
include(__DIR__ . '/scadenzario/include/my_deadlines_widget.php');
|
||||
?>
|
||||
<?php $pdo = DBHandlerSelect::getInstance()->getConnection(); ?>
|
||||
<style>
|
||||
.my-deadlines-widgets {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
.my-deadlines-widgets:empty { display: none; }
|
||||
/* Each widget wraps itself in .my-deadlines-widgets; collapse the nested
|
||||
wrapper so all cards flow into the outer flex (single row). */
|
||||
.my-deadlines-widgets .my-deadlines-widgets {
|
||||
display: contents;
|
||||
}
|
||||
.my-deadlines-widgets .mdw {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
padding: 0.8rem 0.9rem;
|
||||
border-radius: 0.6rem;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
|
||||
transition: transform 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
@media (max-width: 991.98px) {
|
||||
.my-deadlines-widgets .mdw { flex: 1 1 calc(50% - 0.375rem); }
|
||||
}
|
||||
@media (max-width: 575.98px) {
|
||||
.my-deadlines-widgets .mdw { flex: 1 1 100%; }
|
||||
}
|
||||
.my-deadlines-widgets .mdw:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); color: #fff; }
|
||||
.my-deadlines-widgets .mdw-red { background: linear-gradient(135deg, #dc3545 0%, #b02a37 100%); }
|
||||
.my-deadlines-widgets .mdw-orange { background: linear-gradient(135deg, #e8930c 0%, #c77a00 100%); }
|
||||
.my-deadlines-widgets .mdw-gray { background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%); }
|
||||
.my-deadlines-widgets .mdw-icon {
|
||||
width: 38px; height: 38px; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: rgba(255,255,255,0.22); font-size: 1.05rem; flex-shrink: 0;
|
||||
}
|
||||
.my-deadlines-widgets .mdw-body { flex: 1; line-height: 1.2; min-width: 0; }
|
||||
.my-deadlines-widgets .mdw-count { font-size: 1.5rem; font-weight: 700; }
|
||||
.my-deadlines-widgets .mdw-label {
|
||||
font-size: 0.78rem; opacity: 0.95;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.my-deadlines-widgets .mdw-arrow { opacity: 0.7; font-size: 0.85rem; flex-shrink: 0; }
|
||||
</style>
|
||||
<div class="my-deadlines-widgets">
|
||||
<?php include(__DIR__ . '/scadenzario/include/my_deadlines_widget.php'); ?>
|
||||
<?php include(__DIR__ . '/include/training_widget.php'); ?>
|
||||
</div>
|
||||
|
||||
<h3 class="dashboard-title">Dashboard Produzione</h3>
|
||||
|
||||
@@ -347,188 +576,71 @@
|
||||
<!-- ===== SEZIONI COLLASSABILI ===== -->
|
||||
<div class="sections-wrap" id="prodAccordion">
|
||||
|
||||
<!-- OPERATIVO -->
|
||||
<div class="section-card">
|
||||
<button type="button" class="section-header" data-bs-toggle="collapse" data-bs-target="#secOperativo" aria-expanded="true" aria-controls="secOperativo">
|
||||
<div class="section-left">
|
||||
<div class="section-icon">🚀</div>
|
||||
<div style="min-width:0;">
|
||||
<p class="section-title">Operativo</p>
|
||||
<p class="section-subtitle">Azioni principali di produzione e attività in scadenza</p>
|
||||
<?php
|
||||
$hasVisibleSections = false;
|
||||
|
||||
foreach ($dashboardSections as $section):
|
||||
$buttons = visibleButtons($section['buttons']);
|
||||
|
||||
// If no visible buttons are available, do not show the section.
|
||||
if (empty($buttons)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$hasVisibleSections = true;
|
||||
|
||||
$sectionId = htmlspecialchars($section['id'], ENT_QUOTES, 'UTF-8');
|
||||
$isOpen = !empty($section['open']);
|
||||
?>
|
||||
|
||||
<div class="section-card">
|
||||
<button type="button"
|
||||
class="section-header"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#<?= $sectionId ?>"
|
||||
aria-expanded="<?= $isOpen ? 'true' : 'false' ?>"
|
||||
aria-controls="<?= $sectionId ?>">
|
||||
<div class="section-left">
|
||||
<div class="section-icon"><?= htmlspecialchars($section['icon'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<div style="min-width:0;">
|
||||
<p class="section-title"><?= htmlspecialchars($section['title'], ENT_QUOTES, 'UTF-8') ?></p>
|
||||
<p class="section-subtitle"><?= htmlspecialchars($section['subtitle'], ENT_QUOTES, 'UTF-8') ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chev">⌄</div>
|
||||
</button>
|
||||
<div class="chev">⌄</div>
|
||||
</button>
|
||||
|
||||
<div id="secOperativo" class="collapse show" data-bs-parent="#prodAccordion">
|
||||
<div class="section-body">
|
||||
<div class="dashboard-grid">
|
||||
<button class="dash-btn btn-programmazione" onclick="location.href='produzione_programmazione_drag.php'">
|
||||
<div class="dash-icon">🗓️</div>
|
||||
<div>Programmazione</div>
|
||||
</button>
|
||||
<div id="<?= $sectionId ?>"
|
||||
class="collapse <?= $isOpen ? 'show' : '' ?>"
|
||||
data-bs-parent="#prodAccordion">
|
||||
<div class="section-body">
|
||||
<div class="dashboard-grid">
|
||||
|
||||
<?php foreach ($buttons as $button): ?>
|
||||
<button class="dash-btn <?= htmlspecialchars($button['class'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
onclick="location.href='<?= htmlspecialchars($button['url'], ENT_QUOTES, 'UTF-8') ?>'">
|
||||
<div class="dash-icon"><?= htmlspecialchars($button['icon'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<div><?= htmlspecialchars($button['label'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
|
||||
|
||||
<button class="dash-btn btn-status" onclick="location.href='production_line_view2.php'">
|
||||
<div class="dash-icon">⚙️</div>
|
||||
<div>Line View</div>
|
||||
</button>
|
||||
|
||||
<button class="dash-btn btn-statistiche" onclick="location.href='production_stats.php'">
|
||||
<div class="dash-icon">📈</div>
|
||||
<div>Statistiche</div>
|
||||
</button>
|
||||
|
||||
<button class="dash-btn btn-manager" onclick="location.href='manager_produzione.php'">
|
||||
<div class="dash-icon">👔</div>
|
||||
<div>Manager</div>
|
||||
</button>
|
||||
|
||||
<button class="dash-btn btn-manager-stats" onclick="location.href='manager_stats.php'">
|
||||
<div class="dash-icon">📊</div>
|
||||
<div>Manager Stats</div>
|
||||
</button>
|
||||
|
||||
<button class="dash-btn btn-magazzino" onclick="location.href='warehouse_dashboard.php'">
|
||||
<div class="dash-icon">📦</div>
|
||||
<div>Magazzino</div>
|
||||
|
||||
</button>
|
||||
<button class="dash-btn btn-scadenziario" onclick="location.href='scadenzario/index.php'">
|
||||
<div class="dash-icon">⏰</div>
|
||||
<div>Scadenziario</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ANAGRAFICHE -->
|
||||
<div class="section-card">
|
||||
<button type="button" class="section-header" data-bs-toggle="collapse" data-bs-target="#secAnagrafiche" aria-expanded="false" aria-controls="secAnagrafiche">
|
||||
<div class="section-left">
|
||||
<div class="section-icon">🗂️</div>
|
||||
<div style="min-width:0;">
|
||||
<p class="section-title">Anagrafiche</p>
|
||||
<p class="section-subtitle">Dati di base e setup di produzione</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chev">⌄</div>
|
||||
</button>
|
||||
<div id="secAnagrafiche" class="collapse" data-bs-parent="#prodAccordion">
|
||||
<div class="section-body">
|
||||
<div class="dashboard-grid">
|
||||
<button class="dash-btn btn-mescole" onclick="location.href='mescole.php'">
|
||||
<div class="dash-icon">⚗️</div>
|
||||
<div>Mescole</div>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<button class="dash-btn btn-matrici" onclick="location.href='matrici.php'">
|
||||
<div class="dash-icon">🧩</div>
|
||||
<div>Elenco Profili</div>
|
||||
</button>
|
||||
|
||||
<button class="dash-btn btn-linee" onclick="location.href='linee.php'">
|
||||
<div class="dash-icon">🏭</div>
|
||||
<div>Linee Produzione</div>
|
||||
</button>
|
||||
|
||||
<button class="dash-btn btn-setup" onclick="location.href='packaging_items.php'">
|
||||
<div class="dash-icon">📦</div>
|
||||
<div>Imballaggi</div>
|
||||
</button>
|
||||
|
||||
<button class="dash-btn btn-setup" onclick="location.href='suppliers.php'">
|
||||
<div class="dash-icon">🏷️</div>
|
||||
<div>Suppliers</div>
|
||||
</button>
|
||||
|
||||
<button class="dash-btn btn-setup" onclick="location.href='lookup_values.php'">
|
||||
<div class="dash-icon">⚙️</div>
|
||||
<div>Setup</div>
|
||||
</button>
|
||||
<button class="dash-btn btn-setup" onclick="location.href='worksheets.php'">
|
||||
<div class="dash-icon">🗒️</div>
|
||||
<div>Fogli di lavoro</div>
|
||||
</button>
|
||||
</div>
|
||||
<?php if (!$hasVisibleSections): ?>
|
||||
<div class="section-card">
|
||||
<div class="section-body text-center">
|
||||
Nessuna sezione disponibile per il tuo profilo.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- QUALITÀ / SERVIZI -->
|
||||
<div class="section-card">
|
||||
<button type="button" class="section-header" data-bs-toggle="collapse" data-bs-target="#secServizi" aria-expanded="false" aria-controls="secServizi">
|
||||
<div class="section-left">
|
||||
<div class="section-icon">🧰</div>
|
||||
<div style="min-width:0;">
|
||||
<p class="section-title">Servizi</p>
|
||||
<p class="section-subtitle">Status, cause pausa, attrezzature</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chev">⌄</div>
|
||||
</button>
|
||||
|
||||
<div id="secServizi" class="collapse" data-bs-parent="#prodAccordion">
|
||||
<div class="section-body">
|
||||
<div class="dashboard-grid">
|
||||
<button class="dash-btn btn-setup" onclick="location.href='production_status.php'">
|
||||
<div class="dash-icon">📋</div>
|
||||
<div>Status</div>
|
||||
</button>
|
||||
|
||||
<button class="dash-btn btn-problem" onclick="location.href='production_pause_reasons.php'">
|
||||
<div class="dash-icon">🛑</div>
|
||||
<div>Cause di Pausa</div>
|
||||
</button>
|
||||
|
||||
<button class="dash-btn btn-tools" onclick="location.href='production_tools.php'">
|
||||
<div class="dash-icon">🛠️</div>
|
||||
<div>Attrezzature</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PERSONALE -->
|
||||
<div class="section-card">
|
||||
<button type="button" class="section-header" data-bs-toggle="collapse" data-bs-target="#secPersonale" aria-expanded="false" aria-controls="secPersonale">
|
||||
<div class="section-left">
|
||||
<div class="section-icon">👥</div>
|
||||
<div style="min-width:0;">
|
||||
<p class="section-title">Personale</p>
|
||||
<p class="section-subtitle">Dipendenti, skill</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chev">⌄</div>
|
||||
</button>
|
||||
|
||||
<div id="secPersonale" class="collapse" data-bs-parent="#prodAccordion">
|
||||
<div class="section-body">
|
||||
<div class="dashboard-grid">
|
||||
|
||||
<button class="dash-btn btn-employees" onclick="location.href='employees.php'">
|
||||
<div class="dash-icon">👥</div>
|
||||
<div>Employees</div>
|
||||
</button>
|
||||
|
||||
<button class="dash-btn btn-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>
|
||||
<!-- /sections-wrap -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1114,7 +1114,7 @@ if (!empty($_GET['ajax'])) {
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -363,7 +363,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['action'])) {
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php');
|
||||
include('include/topbar.php'); ?>
|
||||
<div class="page-wrapper">
|
||||
|
||||
@@ -43,49 +43,64 @@ if (!$_emp || ($_overdue === 0 && $_approaching === 0)) {
|
||||
}
|
||||
?>
|
||||
<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 {
|
||||
flex: 1 1 260px;
|
||||
display: flex; align-items: center; gap: 0.9rem;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 0.6rem;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
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;
|
||||
}
|
||||
.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: 42px; height: 42px; border-radius: 50%;
|
||||
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.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>
|
||||
<div class="my-deadlines-widgets">
|
||||
<?php if ($_overdue > 0): ?>
|
||||
<a class="mdw mdw-red" href="scadenzario/index.php?filter_my=1&filter_status=scaduta">
|
||||
<span class="mdw-icon"><i class="fa-solid fa-triangle-exclamation"></i></span>
|
||||
<span class="mdw-body">
|
||||
<span class="mdw-count"><?= $_overdue ?></span>
|
||||
<span class="mdw-label d-block">Scadenz<?= $_overdue === 1 ? 'a' : 'e' ?> scadut<?= $_overdue === 1 ? 'a' : 'e' ?> — <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span>
|
||||
</span>
|
||||
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($_approaching > 0): ?>
|
||||
<a class="mdw mdw-orange" href="scadenzario/index.php?filter_my=1&filter_status=in-scadenza">
|
||||
<span class="mdw-icon"><i class="fa-solid fa-clock"></i></span>
|
||||
<span class="mdw-body">
|
||||
<span class="mdw-count"><?= $_approaching ?></span>
|
||||
<span class="mdw-label d-block">In scadenza a breve — <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span>
|
||||
</span>
|
||||
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($_overdue > 0): ?>
|
||||
<a class="mdw mdw-red" href="scadenzario/index.php?filter_my=1&filter_status=scaduta">
|
||||
<span class="mdw-icon"><i class="fa-solid fa-triangle-exclamation"></i></span>
|
||||
<span class="mdw-body">
|
||||
<span class="mdw-count"><?= $_overdue ?></span>
|
||||
<span class="mdw-label d-block">Scadenz<?= $_overdue === 1 ? 'a' : 'e' ?> scadut<?= $_overdue === 1 ? 'a' : 'e' ?> — <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span>
|
||||
</span>
|
||||
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($_approaching > 0): ?>
|
||||
<a class="mdw mdw-orange" href="scadenzario/index.php?filter_my=1&filter_status=in-scadenza">
|
||||
<span class="mdw-icon"><i class="fa-solid fa-clock"></i></span>
|
||||
<span class="mdw-body">
|
||||
<span class="mdw-count"><?= $_approaching ?></span>
|
||||
<span class="mdw-label d-block">In scadenza a breve — <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span>
|
||||
</span>
|
||||
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
@@ -208,7 +208,7 @@ while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -130,7 +130,6 @@ $tools = $pdo->query("
|
||||
<title>Gestione Skills</title>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||
@@ -177,7 +176,7 @@ $tools = $pdo->query("
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
<!-- jQuery e Bootstrap -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
@@ -119,7 +118,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
|
||||
<!-- Bootstrap (se già incluso puoi rimuoverlo) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- SweetAlert2 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
@@ -0,0 +1,530 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
/* ==========================================
|
||||
PAGE DATA
|
||||
========================================== */
|
||||
$sql = "
|
||||
SELECT tt.*,
|
||||
(SELECT COUNT(*) FROM employee_trainings et WHERE et.training_topic_id = tt.id) AS trainings_count
|
||||
FROM training_topics tt
|
||||
ORDER BY tt.sort_order ASC, tt.name ASC
|
||||
";
|
||||
$topics = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
<?php include('cssinclude.php'); ?>
|
||||
<title>Gestione Corsi di Formazione - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||
|
||||
<style>
|
||||
body { font-size: 1.05rem; background: #f8fafc; }
|
||||
.card { border-radius: 16px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); }
|
||||
.back-dashboard {
|
||||
background-color: #cfe3ff !important; color: #1f2d3d !important;
|
||||
border: 1px solid #bcd4f4 !important; border-radius: 10px;
|
||||
font-weight: 600; padding: 10px 18px;
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
.back-dashboard:hover { background-color: #b9d3ff !important; transform: translateY(-2px); }
|
||||
.btn-add { background-color: #0d6efd; color: #fff; border-radius: 8px; padding: 10px 20px; font-weight: 500; }
|
||||
.btn-add:hover { background-color: #0b5ed7; transform: scale(1.02); }
|
||||
.table thead { background-color: #cfe3ff; color: #1f2d3d; }
|
||||
.modal-content { border-radius: 16px; }
|
||||
#tabellaTopics thead th { text-align: center; vertical-align: middle; }
|
||||
.badge-status { padding: 0.25rem 0.6rem; border-radius: 999px; font-size: 0.8rem; font-weight: 600; }
|
||||
.badge-status.active { background-color: #d1fae5; color: #065f46; }
|
||||
.badge-status.inactive { background-color: #e5e7eb; color: #374151; }
|
||||
.description-cell {
|
||||
max-width: 280px; white-space: nowrap; overflow: hidden;
|
||||
text-overflow: ellipsis; text-align: left;
|
||||
}
|
||||
.num-pill {
|
||||
display: inline-block; padding: 2px 10px; border-radius: 999px;
|
||||
background: #eef2ff; color: #3730a3; font-weight: 600; font-size: 0.85rem;
|
||||
}
|
||||
@media (max-width: 767.98px) {
|
||||
.card-header { flex-direction: column; align-items: flex-start !important; gap: .5rem; }
|
||||
.back-dashboard { width: 100%; }
|
||||
.btn-add { width: 100%; }
|
||||
}
|
||||
|
||||
.tt-card {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 12px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.tt-card-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0 0 4px 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
.tt-card-desc {
|
||||
color: #475569;
|
||||
font-size: 0.95rem;
|
||||
margin: 0 0 10px 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
.tt-card-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 14px;
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.tt-card-meta b { color: #1f2937; font-weight: 600; }
|
||||
.tt-card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.tt-card-actions .btn { flex: 1; }
|
||||
.tt-empty {
|
||||
text-align: center;
|
||||
color: #94a3b8;
|
||||
padding: 24px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
<div class="card p-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<h5 class="mb-0">Gestione Corsi di Formazione</h5>
|
||||
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
|
||||
↩️ Torna alla Dashboard
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
||||
<h6 class="fw-semibold mb-0">Elenco Corsi / Training Topics</h6>
|
||||
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addTopicModal">
|
||||
➕ Aggiungi Corso
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- DESKTOP / TABLET ≥768px: TABLE -->
|
||||
<div class="table-responsive d-none d-md-block">
|
||||
<table id="tabellaTopics" class="table table-striped align-middle text-center" style="width:100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nome</th>
|
||||
<th>Descrizione</th>
|
||||
<th>Frequenza<br>(mesi)</th>
|
||||
<th>Promemoria<br>(giorni)</th>
|
||||
<th>Ordine</th>
|
||||
<th>Stato</th>
|
||||
<th>Formazioni</th>
|
||||
<th>Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($topics as $row): ?>
|
||||
<?php
|
||||
$id = (int)$row['id'];
|
||||
$name = $row['name'] ?? '';
|
||||
$description = $row['description'] ?? '';
|
||||
$freq = $row['default_frequency_months'];
|
||||
$rem = (int)($row['default_reminder_days'] ?? 30);
|
||||
$sortOrder = (int)($row['sort_order'] ?? 999);
|
||||
$isActive = (int)($row['is_active'] ?? 1);
|
||||
$isMandatory = (int)($row['is_mandatory'] ?? 0);
|
||||
$cnt = (int)($row['trainings_count'] ?? 0);
|
||||
$statusClass = $isActive === 1 ? 'active' : 'inactive';
|
||||
$statusLabel = $isActive === 1 ? 'Attivo' : 'Inattivo';
|
||||
?>
|
||||
<tr>
|
||||
<td><?= $id ?></td>
|
||||
<td class="fw-semibold text-start">
|
||||
<?= htmlspecialchars($name) ?>
|
||||
<?php if ($isMandatory === 1): ?>
|
||||
<span class="badge bg-warning text-dark ms-1" title="Obbligatorio per tutti">★ Obbl.</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="description-cell" title="<?= htmlspecialchars($description, ENT_QUOTES) ?>">
|
||||
<?= $description !== '' ? htmlspecialchars($description) : '-' ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($freq === null || $freq === ''): ?>
|
||||
<span class="text-muted">una tantum</span>
|
||||
<?php else: ?>
|
||||
<span class="num-pill"><?= (int)$freq ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><span class="num-pill"><?= $rem ?></span></td>
|
||||
<td><?= $sortOrder ?></td>
|
||||
<td><span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span></td>
|
||||
<td><?= $cnt ?></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-secondary edit-topic"
|
||||
data-id="<?= $id ?>"
|
||||
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
|
||||
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
|
||||
data-freq="<?= $freq === null ? '' : (int)$freq ?>"
|
||||
data-rem="<?= $rem ?>"
|
||||
data-sort_order="<?= $sortOrder ?>"
|
||||
data-is_active="<?= $isActive ?>"
|
||||
data-is_mandatory="<?= $isMandatory ?>">
|
||||
✏️ Modifica
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger delete-topic"
|
||||
data-id="<?= $id ?>"
|
||||
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
|
||||
data-count="<?= $cnt ?>">
|
||||
🗑️ Cancella
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- MOBILE <768px: CARDS -->
|
||||
<div class="d-block d-md-none">
|
||||
<?php if (empty($topics)): ?>
|
||||
<div class="tt-empty">Nessun corso presente</div>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($topics as $row): ?>
|
||||
<?php
|
||||
$id = (int)$row['id'];
|
||||
$name = $row['name'] ?? '';
|
||||
$description = $row['description'] ?? '';
|
||||
$freq = $row['default_frequency_months'];
|
||||
$rem = (int)($row['default_reminder_days'] ?? 30);
|
||||
$sortOrder = (int)($row['sort_order'] ?? 999);
|
||||
$isActive = (int)($row['is_active'] ?? 1);
|
||||
$isMandatory = (int)($row['is_mandatory'] ?? 0);
|
||||
$cnt = (int)($row['trainings_count'] ?? 0);
|
||||
$statusClass = $isActive === 1 ? 'active' : 'inactive';
|
||||
$statusLabel = $isActive === 1 ? 'Attivo' : 'Inattivo';
|
||||
$freqLabel = ($freq === null || $freq === '') ? 'una tantum' : ((int)$freq . ' mesi');
|
||||
?>
|
||||
<div class="tt-card">
|
||||
<h6 class="tt-card-title">
|
||||
<?= htmlspecialchars($name) ?>
|
||||
<?php if ($isMandatory === 1): ?>
|
||||
<span class="badge bg-warning text-dark ms-1" title="Obbligatorio per tutti">★ Obbl.</span>
|
||||
<?php endif; ?>
|
||||
</h6>
|
||||
<?php if ($description !== ''): ?>
|
||||
<p class="tt-card-desc"><?= htmlspecialchars($description) ?></p>
|
||||
<?php endif; ?>
|
||||
<div class="tt-card-meta">
|
||||
<span><span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span></span>
|
||||
<span><b>Frequenza:</b> <?= htmlspecialchars($freqLabel) ?></span>
|
||||
<span><b>Promemoria:</b> <?= $rem ?> gg</span>
|
||||
<span><b>Formazioni:</b> <?= $cnt ?></span>
|
||||
<span><b>Ordine:</b> <?= $sortOrder ?></span>
|
||||
</div>
|
||||
<div class="tt-card-actions">
|
||||
<button class="btn btn-sm btn-outline-secondary edit-topic"
|
||||
data-id="<?= $id ?>"
|
||||
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
|
||||
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
|
||||
data-freq="<?= $freq === null ? '' : (int)$freq ?>"
|
||||
data-rem="<?= $rem ?>"
|
||||
data-sort_order="<?= $sortOrder ?>"
|
||||
data-is_active="<?= $isActive ?>"
|
||||
data-is_mandatory="<?= $isMandatory ?>">
|
||||
✏️ Modifica
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger delete-topic"
|
||||
data-id="<?= $id ?>"
|
||||
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
|
||||
data-count="<?= $cnt ?>">
|
||||
🗑️ Cancella
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<!-- ADD -->
|
||||
<div class="modal fade" id="addTopicModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg modal-fullscreen-sm-down">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header" style="background-color:#cfe3ff;">
|
||||
<h5 class="modal-title">Aggiungi Corso</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="addTopicForm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Nome</label>
|
||||
<input type="text" class="form-control" id="addName" name="name" placeholder="es. Sicurezza antincendio" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Descrizione</label>
|
||||
<textarea class="form-control" id="addDescription" name="description" rows="3" placeholder="Opzionale"></textarea>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Frequenza aggiornamento</label>
|
||||
<select class="form-select" id="addFreq" name="default_frequency_months">
|
||||
<option value="" selected>Una tantum (nessun aggiornamento)</option>
|
||||
<option value="3">3 mesi</option>
|
||||
<option value="6">6 mesi</option>
|
||||
<option value="12">12 mesi (1 anno)</option>
|
||||
<option value="18">18 mesi</option>
|
||||
<option value="24">24 mesi (2 anni)</option>
|
||||
<option value="36">36 mesi (3 anni)</option>
|
||||
<option value="48">48 mesi (4 anni)</option>
|
||||
<option value="60">60 mesi (5 anni)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Promemoria (giorni prima della scadenza)</label>
|
||||
<input type="number" class="form-control" id="addRem" name="default_reminder_days" value="30" min="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Ordine</label>
|
||||
<input type="number" class="form-control" id="addSortOrder" name="sort_order" value="999" min="0">
|
||||
</div>
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Stato</label>
|
||||
<select class="form-select" id="addIsActive" name="is_active">
|
||||
<option value="1" selected>Attivo</option>
|
||||
<option value="0">Inattivo</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="addIsMandatory" value="1">
|
||||
<label class="form-check-label fw-semibold" for="addIsMandatory">
|
||||
Obbligatorio per tutti i dipendenti
|
||||
</label>
|
||||
<div class="small text-muted">
|
||||
Se attivo, i dipendenti senza registrazione di questo corso compaiono come "Non presente" nello storico.
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<button type="submit" class="btn btn-add">💾 Salva</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EDIT -->
|
||||
<div class="modal fade" id="editTopicModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg modal-fullscreen-sm-down">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header" style="background-color:#cfe3ff;">
|
||||
<h5 class="modal-title">Modifica Corso</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="editTopicForm">
|
||||
<input type="hidden" id="editTopicId">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Nome</label>
|
||||
<input type="text" class="form-control" id="editName" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Descrizione</label>
|
||||
<textarea class="form-control" id="editDescription" name="description" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Frequenza aggiornamento</label>
|
||||
<select class="form-select" id="editFreq" name="default_frequency_months">
|
||||
<option value="">Una tantum (nessun aggiornamento)</option>
|
||||
<option value="3">3 mesi</option>
|
||||
<option value="6">6 mesi</option>
|
||||
<option value="12">12 mesi (1 anno)</option>
|
||||
<option value="18">18 mesi</option>
|
||||
<option value="24">24 mesi (2 anni)</option>
|
||||
<option value="36">36 mesi (3 anni)</option>
|
||||
<option value="48">48 mesi (4 anni)</option>
|
||||
<option value="60">60 mesi (5 anni)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Promemoria (giorni prima della scadenza)</label>
|
||||
<input type="number" class="form-control" id="editRem" name="default_reminder_days" min="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Ordine</label>
|
||||
<input type="number" class="form-control" id="editSortOrder" name="sort_order" min="0">
|
||||
</div>
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Stato</label>
|
||||
<select class="form-select" id="editIsActive" name="is_active">
|
||||
<option value="1">Attivo</option>
|
||||
<option value="0">Inattivo</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="editIsMandatory" value="1">
|
||||
<label class="form-check-label fw-semibold" for="editIsMandatory">
|
||||
Obbligatorio per tutti i dipendenti
|
||||
</label>
|
||||
<div class="small text-muted">
|
||||
Se attivo, i dipendenti senza registrazione di questo corso compaiono come "Non presente" nello storico.
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<button type="submit" class="btn btn-add">💾 Salva Modifiche</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#tabellaTopics').DataTable({
|
||||
order: [[5, 'asc'], [1, 'asc']],
|
||||
pageLength: 25,
|
||||
language: {
|
||||
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json',
|
||||
emptyTable: 'Nessun corso presente'
|
||||
}
|
||||
});
|
||||
|
||||
function ajaxPost(url, payload, successTitle, errorFallback) {
|
||||
return fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: payload.toString()
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
Swal.fire({ icon: "success", title: successTitle, confirmButtonColor: "#3085d6" })
|
||||
.then(() => location.reload());
|
||||
} else {
|
||||
Swal.fire({ icon: "error", title: "Errore", text: data.message || errorFallback });
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
Swal.fire({ icon: "error", title: "Errore", text: "Errore di comunicazione." });
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
$("#addTopicForm").on("submit", function(e) {
|
||||
e.preventDefault();
|
||||
const p = new URLSearchParams();
|
||||
p.append('name', $("#addName").val().trim());
|
||||
p.append('description', $("#addDescription").val().trim());
|
||||
p.append('default_frequency_months', $("#addFreq").val());
|
||||
p.append('default_reminder_days', $("#addRem").val());
|
||||
p.append('sort_order', $("#addSortOrder").val());
|
||||
p.append('is_active', $("#addIsActive").val());
|
||||
p.append('is_mandatory', $("#addIsMandatory").is(':checked') ? '1' : '0');
|
||||
ajaxPost("ajax/training_topics/save.php", p, "Salvato!", "Impossibile salvare il corso.");
|
||||
});
|
||||
|
||||
$(document).on("click", ".edit-topic", function() {
|
||||
const b = $(this);
|
||||
const rawFreq = b.data("freq");
|
||||
const freqStr = (rawFreq === '' || rawFreq === null || rawFreq === undefined) ? '' : String(rawFreq);
|
||||
if (freqStr !== '' && $("#editFreq option[value='" + freqStr + "']").length === 0) {
|
||||
$("#editFreq").append('<option value="' + freqStr + '">' + freqStr + ' mesi</option>');
|
||||
}
|
||||
$("#editTopicId").val(b.data("id"));
|
||||
$("#editName").val(b.data("name"));
|
||||
$("#editDescription").val(b.data("description"));
|
||||
$("#editFreq").val(freqStr);
|
||||
$("#editRem").val(b.data("rem"));
|
||||
$("#editSortOrder").val(b.data("sort_order"));
|
||||
$("#editIsActive").val(String(b.data("is_active")));
|
||||
$("#editIsMandatory").prop('checked', String(b.data("is_mandatory")) === '1');
|
||||
$("#editTopicModal").modal("show");
|
||||
});
|
||||
|
||||
$("#editTopicForm").on("submit", function(e) {
|
||||
e.preventDefault();
|
||||
const p = new URLSearchParams();
|
||||
p.append('id', $("#editTopicId").val());
|
||||
p.append('name', $("#editName").val().trim());
|
||||
p.append('description', $("#editDescription").val().trim());
|
||||
p.append('default_frequency_months', $("#editFreq").val());
|
||||
p.append('default_reminder_days', $("#editRem").val());
|
||||
p.append('sort_order', $("#editSortOrder").val());
|
||||
p.append('is_active', $("#editIsActive").val());
|
||||
p.append('is_mandatory', $("#editIsMandatory").is(':checked') ? '1' : '0');
|
||||
ajaxPost("ajax/training_topics/save.php", p, "Aggiornato!", "Impossibile aggiornare il corso.");
|
||||
});
|
||||
|
||||
$(document).on("click", ".delete-topic", function() {
|
||||
const id = $(this).data("id");
|
||||
const name = $(this).data("name");
|
||||
const cnt = parseInt($(this).data("count")) || 0;
|
||||
|
||||
if (cnt > 0) {
|
||||
Swal.fire({
|
||||
icon: "warning",
|
||||
title: "Impossibile cancellare",
|
||||
text: "Il corso \"" + name + "\" ha " + cnt + " registrazione/i di formazione. Cancella prima le registrazioni."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: "Confermi la cancellazione?",
|
||||
text: name ? ("Corso: " + name) : "Il corso verrà cancellato.",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#d33",
|
||||
cancelButtonColor: "#6c757d",
|
||||
confirmButtonText: "Sì, cancella",
|
||||
cancelButtonText: "Annulla"
|
||||
}).then((result) => {
|
||||
if (!result.isConfirmed) return;
|
||||
const p = new URLSearchParams();
|
||||
p.append('id', id);
|
||||
ajaxPost("ajax/training_topics/delete.php", p, "Cancellato!", "Impossibile cancellare il corso.");
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,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>
|
||||
@@ -139,7 +139,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
<!-- jQuery e Bootstrap -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
@@ -117,7 +116,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
<!-- jQuery e Bootstrap -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
@@ -111,7 +110,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
@@ -308,7 +308,6 @@ $worksheets = $pdo->query("
|
||||
<title>Fogli di Lavoro</title>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||
@@ -454,7 +453,7 @@ $worksheets = $pdo->query("
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
; This file is for unifying the coding style for different editors and IDEs.
|
||||
; More information at https://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.yml]
|
||||
indent_size = 2
|
||||
@@ -1,25 +0,0 @@
|
||||
# Set the default behavior, in case people don't have core.autocrlf set.
|
||||
* text eol=lf
|
||||
|
||||
# Explicitly declare text files you want to always be normalized and converted
|
||||
# to native line endings on checkout.
|
||||
*.c text
|
||||
*.h text
|
||||
|
||||
# Declare files that will always have CRLF line endings on checkout.
|
||||
*.sln text eol=crlf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.otf binary
|
||||
*.eot binary
|
||||
*.svg binary
|
||||
*.ttf binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
|
||||
*.css linguist-vendored
|
||||
*.scss linguist-vendored
|
||||
*.js linguist-vendored
|
||||
CHANGELOG.md export-ignore
|
||||
@@ -1,37 +0,0 @@
|
||||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
|
||||
name: PHP ${{ matrix.php }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['7.3', '7.4', '8.0', '8.1']
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Cache composer
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.composer/cache/files
|
||||
key: php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extension-csv: bcmath, ctype, dom, fileinfo, intl, gd, json, mbstring, pdo, pdo_sqlite, openssl, sqlite, xml, zip
|
||||
coverage: none
|
||||
|
||||
- name: Install composer
|
||||
run: composer install --no-interaction --no-scripts --no-suggest --prefer-source
|
||||
|
||||
- name: Execute tests
|
||||
run: vendor/bin/phpunit
|
||||
@@ -1,9 +0,0 @@
|
||||
/.idea
|
||||
/.history
|
||||
/.vscode
|
||||
/tests/databases
|
||||
/vendor
|
||||
.DS_Store
|
||||
.phpunit.result.cache
|
||||
composer.phar
|
||||
composer.lock
|
||||
@@ -1,4 +0,0 @@
|
||||
preset: psr2
|
||||
|
||||
enabled:
|
||||
- concat_with_spaces
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Andreas Lutro
|
||||
|
||||
Copyright (c) 2017 Akaunting
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
-186
@@ -1,186 +0,0 @@
|
||||
# Persistent settings package for Laravel
|
||||
|
||||
[](https://github.com/akaunting/laravel-setting)
|
||||
[](https://styleci.io/repos/101231817)
|
||||
[](LICENSE.md)
|
||||
|
||||
This package allows you to save settings in a more persistent way. You can use the database and/or json file to save your settings. You can also override the Laravel config.
|
||||
|
||||
* Driver support
|
||||
* Helper function
|
||||
* Blade directive
|
||||
* Override config values
|
||||
* Encryption
|
||||
* Custom file, table and columns
|
||||
* Auto save
|
||||
* Extra columns
|
||||
* Cache support
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 1. Install
|
||||
|
||||
Run the following command:
|
||||
|
||||
```bash
|
||||
composer require akaunting/laravel-setting
|
||||
```
|
||||
|
||||
### 2. Register (for Laravel < 5.5)
|
||||
|
||||
Register the service provider in `config/app.php`
|
||||
|
||||
```php
|
||||
Akaunting\Setting\Provider::class,
|
||||
```
|
||||
|
||||
Add alias if you want to use the facade.
|
||||
|
||||
```php
|
||||
'Setting' => Akaunting\Setting\Facade::class,
|
||||
```
|
||||
|
||||
### 3. Publish
|
||||
|
||||
Publish config file.
|
||||
|
||||
```bash
|
||||
php artisan vendor:publish --tag=setting
|
||||
```
|
||||
|
||||
### 4. Database
|
||||
|
||||
Create table for database driver
|
||||
|
||||
```bash
|
||||
php artisan migrate
|
||||
```
|
||||
|
||||
### 5. Configure
|
||||
|
||||
You can change the options of your app from `config/setting.php` file
|
||||
|
||||
## Usage
|
||||
|
||||
You can either use the helper method like `setting('foo')` or the facade `Setting::get('foo')`
|
||||
|
||||
### Facade
|
||||
|
||||
```php
|
||||
Setting::get('foo', 'default');
|
||||
Setting::get('nested.element');
|
||||
Setting::set('foo', 'bar');
|
||||
Setting::forget('foo');
|
||||
$settings = Setting::all();
|
||||
```
|
||||
|
||||
### Helper
|
||||
|
||||
```php
|
||||
setting('foo', 'default');
|
||||
setting('nested.element');
|
||||
setting(['foo' => 'bar']);
|
||||
setting()->forget('foo');
|
||||
$settings = setting()->all();
|
||||
```
|
||||
|
||||
You can call the `save()` method to save the changes.
|
||||
|
||||
### Auto Save
|
||||
|
||||
If you enable the `auto_save` option in the config file, settings will be saved automatically every time the application shuts down if anything has been changed.
|
||||
|
||||
### Blade Directive
|
||||
|
||||
You can get the settings directly in your blade templates using the helper method or the blade directive like `@setting('foo')`
|
||||
|
||||
### Override Config Values
|
||||
|
||||
You can easily override default config values by adding them to the `override` option in `config/setting.php`, thereby eliminating the need to modify the default config files and also allowing you to change said values during production. Ex:
|
||||
|
||||
```php
|
||||
'override' => [
|
||||
"app.name" => "app_name",
|
||||
"app.env" => "app_env",
|
||||
"mail.driver" => "app_mail_driver",
|
||||
"mail.host" => "app_mail_host",
|
||||
],
|
||||
```
|
||||
|
||||
The values on the left corresponds to the respective config value (Ex: config('app.name')) and the value on the right is the name of the `key` in your settings table/json file.
|
||||
|
||||
### Encryption
|
||||
|
||||
If you like to encrypt the values for a given key, you can pass the key to the `encrypted_keys` option in `config/setting.php` and the rest is automatically handled by using Laravel's built-in encryption facilities. Ex:
|
||||
|
||||
```php
|
||||
'encrypted_keys' => [
|
||||
"payment.key",
|
||||
],
|
||||
```
|
||||
|
||||
### JSON Storage
|
||||
|
||||
You can modify the path used on run-time using `setting()->setPath($path)`.
|
||||
|
||||
### Database Storage
|
||||
|
||||
If you want to use the database as settings storage then you should run the `php artisan migrate`. You can modify the table fields from the `create_settings_table` file in the migrations directory.
|
||||
|
||||
#### Extra Columns
|
||||
|
||||
If you want to store settings for multiple users/clients in the same database you can do so by specifying extra columns:
|
||||
|
||||
```php
|
||||
setting()->setExtraColumns(['user_id' => Auth::user()->id]);
|
||||
```
|
||||
|
||||
where `user_id = x` will now be added to the database query when settings are retrieved, and when new settings are saved, the `user_id` will be populated.
|
||||
|
||||
If you need more fine-tuned control over which data gets queried, you can use the `setConstraint` method which takes a closure with two arguments:
|
||||
|
||||
- `$query` is the query builder instance
|
||||
- `$insert` is a boolean telling you whether the query is an insert or not. If it is an insert, you usually don't need to do anything to `$query`.
|
||||
|
||||
```php
|
||||
setting()->setConstraint(function($query, $insert) {
|
||||
if ($insert) return;
|
||||
$query->where(/* ... */);
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Drivers
|
||||
|
||||
This package uses the Laravel `Manager` class under the hood, so it's easy to add your own storage driver. All you need to do is extend the abstract `Driver` class, implement the abstract methods and call `setting()->extend`.
|
||||
|
||||
```php
|
||||
class MyDriver extends Akaunting\Setting\Contracts\Driver
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
app('setting.manager')->extend('mydriver', function($app) {
|
||||
return $app->make('MyDriver');
|
||||
});
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
Please see [Releases](../../releases) for more information what has changed recently.
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests are more than welcome. You must follow the PSR coding standards.
|
||||
|
||||
## Security
|
||||
|
||||
If you discover any security related issues, please email security@akaunting.com instead of using the issue tracker.
|
||||
|
||||
## Credits
|
||||
|
||||
- [Denis Duliçi](https://github.com/denisdulici)
|
||||
- [All Contributors](../../contributors)
|
||||
|
||||
## License
|
||||
|
||||
The MIT License (MIT). Please see [LICENSE](LICENSE.md) for more information.
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"name": "akaunting/laravel-setting",
|
||||
"description": "Persistent settings package for Laravel",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"persistent",
|
||||
"settings",
|
||||
"config"
|
||||
],
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Denis Duliçi",
|
||||
"email": "info@akaunting.com",
|
||||
"homepage": "https://akaunting.com",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.5.9",
|
||||
"laravel/framework": ">=5.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": ">=4.8",
|
||||
"mockery/mockery": "0.9.*",
|
||||
"laravel/framework": ">=5.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Akaunting\\Setting\\": "./src"
|
||||
},
|
||||
"files": [
|
||||
"src/helpers.php"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Akaunting\\Setting\\Provider"
|
||||
],
|
||||
"aliases": {
|
||||
"Setting": "Akaunting\\Setting\\Facade"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit backupGlobals="false"
|
||||
backupStaticAttributes="false"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false"
|
||||
syntaxCheck="false"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Package Test Suite">
|
||||
<directory suffix=".php">./tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
@@ -1,132 +0,0 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Enable / Disable auto save
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Auto-save every time the application shuts down
|
||||
|
|
||||
*/
|
||||
'auto_save' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Options for caching. Set whether to enable cache, its key, time to live
|
||||
| in seconds and whether to auto clear after save.
|
||||
|
|
||||
*/
|
||||
'cache' => [
|
||||
'enabled' => false,
|
||||
'key' => 'setting',
|
||||
'ttl' => 3600,
|
||||
'auto_clear' => true,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Setting driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Select where to store the settings.
|
||||
|
|
||||
| Supported: "database", "json", "memory"
|
||||
|
|
||||
*/
|
||||
'driver' => 'database',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Options for database driver. Enter which connection to use, null means
|
||||
| the default connection. Set the table and column names.
|
||||
|
|
||||
*/
|
||||
'database' => [
|
||||
'connection' => null,
|
||||
'table' => 'settings',
|
||||
'key' => 'key',
|
||||
'value' => 'value',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| JSON driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Options for json driver. Enter the full path to the .json file.
|
||||
|
|
||||
*/
|
||||
'json' => [
|
||||
'path' => storage_path() . '/settings.json',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Override application config values
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If defined, settings package will override these config values.
|
||||
|
|
||||
| Sample:
|
||||
| "app.locale" => "settings.locale",
|
||||
|
|
||||
*/
|
||||
'override' => [
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Fallback
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Define fallback settings to be used in case the default is null
|
||||
|
|
||||
| Sample:
|
||||
| "currency" => "USD",
|
||||
|
|
||||
*/
|
||||
'fallback' => [
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Required Extra Columns
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The list of columns required to be set up
|
||||
|
|
||||
| Sample:
|
||||
| "user_id",
|
||||
| "tenant_id",
|
||||
|
|
||||
*/
|
||||
'required_extra_columns' => [
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Define the keys which should be crypt automatically.
|
||||
|
|
||||
| Sample:
|
||||
| "payment.key"
|
||||
|
|
||||
*/
|
||||
'encrypted_keys' => [
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,321 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Akaunting\Setting\Contracts;
|
||||
|
||||
use Akaunting\Setting\Support\Arr;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
abstract class Driver
|
||||
{
|
||||
/**
|
||||
* The settings data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* Whether the store has changed since it was last loaded.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $unsaved = false;
|
||||
|
||||
/**
|
||||
* Whether the settings data are loaded.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $loaded = false;
|
||||
|
||||
/**
|
||||
* Include and merge with fallbacks
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $with_fallback = true;
|
||||
|
||||
/**
|
||||
* Excludes fallback data
|
||||
*/
|
||||
public function withoutFallback()
|
||||
{
|
||||
$this->with_fallback = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific key from the settings data.
|
||||
*
|
||||
* @param string|array $key
|
||||
* @param mixed $default Optional default value.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get($key, $default = null)
|
||||
{
|
||||
if (!$this->checkExtraColumns()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->load();
|
||||
|
||||
return Arr::get($this->data, $key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fallback value if default is null.
|
||||
*
|
||||
* @param string|array $key
|
||||
* @param mixed $default
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFallback($key, $default = null)
|
||||
{
|
||||
if (($default !== null) || is_array($key)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return Arr::get((array) config('setting.fallback'), $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given value is same as fallback.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEqualToFallback($key, $value)
|
||||
{
|
||||
return (string) $this->getFallback($key) == (string) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a key exists in the settings data.
|
||||
*
|
||||
* @param string $key
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has($key)
|
||||
{
|
||||
if (!$this->checkExtraColumns()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->load();
|
||||
|
||||
return Arr::has($this->data, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a specific key to a value in the settings data.
|
||||
*
|
||||
* @param string|array $key Key string or associative array of key => value
|
||||
* @param mixed $value Optional only if the first argument is an array
|
||||
*/
|
||||
public function set($key, $value = null)
|
||||
{
|
||||
if (!$this->checkExtraColumns()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->load();
|
||||
$this->unsaved = true;
|
||||
|
||||
if (is_array($key)) {
|
||||
foreach ($key as $k => $v) {
|
||||
Arr::set($this->data, $k, $v);
|
||||
}
|
||||
} else {
|
||||
Arr::set($this->data, $key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset a key in the settings data.
|
||||
*
|
||||
* @param string $key
|
||||
*/
|
||||
public function forget($key)
|
||||
{
|
||||
if (!$this->checkExtraColumns()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->unsaved = true;
|
||||
|
||||
if ($this->has($key)) {
|
||||
Arr::forget($this->data, $key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset all keys in the settings data.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function forgetAll()
|
||||
{
|
||||
if (!$this->checkExtraColumns()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (config('setting.cache.enabled')) {
|
||||
Cache::forget($this->getCacheKey());
|
||||
}
|
||||
|
||||
$this->unsaved = true;
|
||||
$this->data = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all settings data.
|
||||
*
|
||||
* @return array|bool
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
if (!$this->checkExtraColumns()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$this->load();
|
||||
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save any changes done to the settings data.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
if (!$this->checkExtraColumns()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->unsaved) {
|
||||
// either nothing has been changed, or data has not been loaded, so
|
||||
// do nothing by returning early
|
||||
return;
|
||||
}
|
||||
|
||||
if (config('setting.cache.enabled') && config('setting.cache.auto_clear')) {
|
||||
Cache::forget($this->getCacheKey());
|
||||
}
|
||||
|
||||
$this->write($this->data);
|
||||
$this->unsaved = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure data is loaded.
|
||||
*
|
||||
* @param $force Force a reload of data. Default false.
|
||||
*/
|
||||
public function load($force = false)
|
||||
{
|
||||
if (!$this->checkExtraColumns()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->loaded && !$force) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fallback_data = $this->with_fallback ? config('setting.fallback') : [];
|
||||
$driver_data = $this->readData();
|
||||
|
||||
$this->data = Arr::merge((array) $fallback_data, (array) $driver_data);
|
||||
$this->loaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read data from driver or cache
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function readData()
|
||||
{
|
||||
if (config('setting.cache.enabled')) {
|
||||
return $this->readDataFromCache();
|
||||
}
|
||||
|
||||
return $this->read();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read data from cache
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function readDataFromCache()
|
||||
{
|
||||
return Cache::remember($this->getCacheKey(), config('setting.cache.ttl'), function () {
|
||||
return $this->read();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if extra columns are set up.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function checkExtraColumns()
|
||||
{
|
||||
if (!$required_extra_columns = config('setting.required_extra_columns')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (array_keys_exists($required_extra_columns, $this->getExtraColumns())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key based on extra columns.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCacheKey()
|
||||
{
|
||||
$key = config('setting.cache.key');
|
||||
|
||||
foreach ($this->getExtraColumns() as $name => $value) {
|
||||
$key .= '_' . $name . '_' . $value;
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extra columns added to the rows.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function getExtraColumns();
|
||||
|
||||
/**
|
||||
* Read data from driver.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function read();
|
||||
|
||||
/**
|
||||
* Write data to driver.
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function write(array $data);
|
||||
}
|
||||
@@ -1,372 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Akaunting\Setting\Drivers;
|
||||
|
||||
use Akaunting\Setting\Contracts\Driver;
|
||||
use Akaunting\Setting\Support\Arr;
|
||||
use Closure;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Support\Arr as LaravelArr;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class Database extends Driver
|
||||
{
|
||||
/**
|
||||
* The database connection instance.
|
||||
*
|
||||
* @var \Illuminate\Database\Connection
|
||||
*/
|
||||
protected $connection;
|
||||
|
||||
/**
|
||||
* The table to query from.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table;
|
||||
|
||||
/**
|
||||
* The key column name to query from.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $key;
|
||||
|
||||
/**
|
||||
* The value column name to query from.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $value;
|
||||
|
||||
/**
|
||||
* Keys which should be encrypt automatically.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $encrypted_keys;
|
||||
|
||||
/**
|
||||
* Any query constraints that should be applied.
|
||||
*
|
||||
* @var Closure|null
|
||||
*/
|
||||
protected $query_constraint;
|
||||
|
||||
/**
|
||||
* Any extra columns that should be added to the rows.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $extra_columns = [];
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Database\Connection $connection
|
||||
* @param string $table
|
||||
*/
|
||||
public function __construct(Connection $connection, $table = null, $key = null, $value = null, array $encrypted_keys = [])
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->table = $table ?: 'settings';
|
||||
$this->key = $key ?: 'key';
|
||||
$this->value = $value ?: 'value';
|
||||
$this->encrypted_keys = $encrypted_keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the table to query from.
|
||||
*
|
||||
* @param string $table
|
||||
*/
|
||||
public function setTable($table)
|
||||
{
|
||||
$this->table = $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the key column name to query from.
|
||||
*
|
||||
* @param string $key
|
||||
*/
|
||||
public function setKey($key)
|
||||
{
|
||||
$this->key = $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value column name to query from.
|
||||
*
|
||||
* @param string $value
|
||||
*/
|
||||
public function setValue($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the query constraint.
|
||||
*
|
||||
* @param Closure $callback
|
||||
*/
|
||||
public function setConstraint(Closure $callback)
|
||||
{
|
||||
$this->data = [];
|
||||
$this->loaded = false;
|
||||
$this->query_constraint = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set extra columns to be added to the rows.
|
||||
*
|
||||
* @param array $columns
|
||||
*/
|
||||
public function setExtraColumns(array $columns)
|
||||
{
|
||||
$this->extra_columns = $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extra columns added to the rows.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getExtraColumns()
|
||||
{
|
||||
return $this->extra_columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function forget($key)
|
||||
{
|
||||
parent::forget($key);
|
||||
|
||||
// because the database driver cannot store empty arrays, remove empty
|
||||
// arrays to keep data consistent before and after saving
|
||||
$segments = explode('.', $key);
|
||||
array_pop($segments);
|
||||
|
||||
while ($segments) {
|
||||
$segment = implode('.', $segments);
|
||||
|
||||
// non-empty array - exit out of the loop
|
||||
if ($this->get($segment)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// remove the empty array and move on to the next segment
|
||||
$this->forget($segment);
|
||||
array_pop($segments);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function write(array $data)
|
||||
{
|
||||
// Get current data
|
||||
$db_data = $this->newQuery()->get([$this->key, $this->value])->toArray();
|
||||
|
||||
$insert_data = LaravelArr::dot($data);
|
||||
$update_data = [];
|
||||
$delete_keys = [];
|
||||
|
||||
foreach ($db_data as $db_row) {
|
||||
$key = $db_row->{$this->key};
|
||||
$value = $db_row->{$this->value};
|
||||
|
||||
$is_in_insert = $is_different_in_db = $is_same_as_fallback = false;
|
||||
|
||||
if (isset($insert_data[$key])) {
|
||||
$is_in_insert = true;
|
||||
$is_different_in_db = (string) $insert_data[$key] != (string) $value;
|
||||
$is_same_as_fallback = $this->isEqualToFallback($key, $insert_data[$key]);
|
||||
}
|
||||
|
||||
if ($is_in_insert) {
|
||||
if ($is_same_as_fallback) {
|
||||
// Delete if new data is same as fallback
|
||||
$delete_keys[] = $key;
|
||||
} elseif ($is_different_in_db) {
|
||||
// Update if new data is different from db
|
||||
$update_data[$key] = $insert_data[$key];
|
||||
}
|
||||
} else {
|
||||
// Delete if current db not available in new data
|
||||
$delete_keys[] = $key;
|
||||
}
|
||||
|
||||
unset($insert_data[$key]);
|
||||
}
|
||||
|
||||
foreach ($update_data as $key => $value) {
|
||||
$value = $this->prepareValue($key, $value);
|
||||
|
||||
$this->newQuery()
|
||||
->where($this->key, '=', $key)
|
||||
->update([$this->value => $value]);
|
||||
}
|
||||
|
||||
if ($insert_data) {
|
||||
$this->newQuery(true)
|
||||
->insert($this->prepareInsertData($insert_data));
|
||||
}
|
||||
|
||||
if ($delete_keys) {
|
||||
$this->newQuery()
|
||||
->whereIn($this->key, $delete_keys)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms settings data into an array ready to be insterted into the
|
||||
* database. Call array_dot on a multidimensional array before passing it
|
||||
* into this method!
|
||||
*
|
||||
* @param array $data Call array_dot on a multidimensional array before passing it into this method!
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function prepareInsertData(array $data)
|
||||
{
|
||||
$db_data = [];
|
||||
|
||||
if ($this->getExtraColumns()) {
|
||||
foreach ($data as $key => $value) {
|
||||
$value = $this->prepareValue($key, $value);
|
||||
|
||||
// Don't insert if same as fallback
|
||||
if ($this->isEqualToFallback($key, $value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$db_data[] = array_merge(
|
||||
$this->getExtraColumns(),
|
||||
[$this->key => $key, $this->value => $value]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
foreach ($data as $key => $value) {
|
||||
$value = $this->prepareValue($key, $value);
|
||||
|
||||
// Don't insert if same as fallback
|
||||
if ($this->isEqualToFallback($key, $value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$db_data[] = [$this->key => $key, $this->value => $value];
|
||||
}
|
||||
}
|
||||
|
||||
return $db_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided key should be encrypted or not.
|
||||
* Also type casts the given value to a string so errors with booleans or integers are handeled.
|
||||
* Otherwise it returns the original value.
|
||||
*
|
||||
* @param string $key Key to check if it's inside the encryptedValues variable.
|
||||
* @param mixed $value Info: Encryption only supports strings.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function prepareValue(string $key, $value)
|
||||
{
|
||||
// Check if key should be encrypted
|
||||
if (in_array($key, $this->encrypted_keys)) {
|
||||
// Cast to string to avoid error when a user passes a boolean value
|
||||
return Crypt::encryptString((string) $value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided key should be decrypted or not.
|
||||
* Otherwise it returns the original value.
|
||||
*
|
||||
* @param string $key Key to check if it's inside the encryptedValues variable.
|
||||
* @param mixed $value Info: Encryption only supports strings.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function unpackValue(string $key, $value)
|
||||
{
|
||||
// Check if key should be encrypted
|
||||
if (in_array($key, $this->encrypted_keys)) {
|
||||
// Cast to string to avoid error when a user passes a boolean value
|
||||
return Crypt::decryptString((string) $value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function read()
|
||||
{
|
||||
return $this->parseReadData($this->newQuery()->get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse data coming from the database.
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function parseReadData($data)
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($data as $row) {
|
||||
if (is_array($row)) {
|
||||
$key = $row[$this->key];
|
||||
$value = $row[$this->value];
|
||||
} elseif (is_object($row)) {
|
||||
$key = $row->{$this->key};
|
||||
$value = $row->{$this->value};
|
||||
} else {
|
||||
$msg = 'Expected array or object, got ' . gettype($row);
|
||||
throw new \UnexpectedValueException($msg);
|
||||
}
|
||||
|
||||
// Encryption
|
||||
$value = $this->unpackValue($key, $value);
|
||||
|
||||
Arr::set($results, $key, $value);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new query builder instance.
|
||||
*
|
||||
* @param bool $insert
|
||||
*
|
||||
* @return \Illuminate\Database\Query\Builder
|
||||
*/
|
||||
protected function newQuery($insert = false)
|
||||
{
|
||||
$query = $this->connection->table($this->table);
|
||||
|
||||
if (!$insert) {
|
||||
foreach ($this->getExtraColumns() as $key => $value) {
|
||||
$query->where($key, '=', $value);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->query_constraint !== null) {
|
||||
$callback = $this->query_constraint;
|
||||
$callback($query, $insert);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Akaunting\Setting\Drivers;
|
||||
|
||||
use Akaunting\Setting\Contracts\Driver;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
|
||||
class Json extends Driver
|
||||
{
|
||||
/**
|
||||
* @param \Illuminate\Filesystem\Filesystem $files
|
||||
* @param string $path
|
||||
*/
|
||||
public function __construct(Filesystem $files, $path = null)
|
||||
{
|
||||
$this->files = $files;
|
||||
|
||||
$this->setPath($path ?: storage_path() . '/settings.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the path for the JSON file.
|
||||
*
|
||||
* @param string $path
|
||||
*/
|
||||
public function setPath($path)
|
||||
{
|
||||
// If the file does not already exist, we will attempt to create it.
|
||||
if (!$this->files->exists($path)) {
|
||||
$result = $this->files->put($path, '{}');
|
||||
if ($result === false) {
|
||||
throw new \InvalidArgumentException("Could not write to $path.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->files->isWritable($path)) {
|
||||
throw new \InvalidArgumentException("$path is not writable.");
|
||||
}
|
||||
|
||||
$this->path = $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExtraColumns()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function read()
|
||||
{
|
||||
$contents = $this->files->get($this->path);
|
||||
|
||||
$data = json_decode($contents, true);
|
||||
|
||||
if ($data === null) {
|
||||
throw new \RuntimeException("Invalid JSON in {$this->path}");
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function write(array $data)
|
||||
{
|
||||
if ($data) {
|
||||
$contents = json_encode($data);
|
||||
} else {
|
||||
$contents = '{}';
|
||||
}
|
||||
|
||||
$this->files->put($this->path, $contents);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Akaunting\Setting\Drivers;
|
||||
|
||||
use Akaunting\Setting\Contracts\Driver;
|
||||
|
||||
class Memory extends Driver
|
||||
{
|
||||
/**
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct(array $data = null)
|
||||
{
|
||||
if ($data) {
|
||||
$this->data = $data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExtraColumns()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function read()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function write(array $data)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Akaunting\Setting;
|
||||
|
||||
use Illuminate\Support\Facades\Facade as BaseFacade;
|
||||
|
||||
class Facade extends BaseFacade
|
||||
{
|
||||
/**
|
||||
* Get the registered name of the component.
|
||||
*/
|
||||
public static function getFacadeAccessor()
|
||||
{
|
||||
return 'setting';
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Akaunting\Setting;
|
||||
|
||||
use Akaunting\Setting\Drivers\Database;
|
||||
use Akaunting\Setting\Drivers\Json;
|
||||
use Akaunting\Setting\Drivers\Memory;
|
||||
use Illuminate\Support\Manager as BaseManager;
|
||||
|
||||
class Manager extends BaseManager
|
||||
{
|
||||
/**
|
||||
* The container instance.
|
||||
*
|
||||
* @var \Illuminate\Contracts\Container\Container
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* The application instance.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Foundation\Application $app
|
||||
*/
|
||||
public function __construct($app = null)
|
||||
{
|
||||
$this->container = $app ?? app();
|
||||
|
||||
parent::__construct($this->container);
|
||||
}
|
||||
|
||||
public function getDefaultDriver()
|
||||
{
|
||||
return config('setting.driver');
|
||||
}
|
||||
|
||||
public function createJsonDriver()
|
||||
{
|
||||
$path = config('setting.json.path');
|
||||
|
||||
return new Json($this->container['files'], $path);
|
||||
}
|
||||
|
||||
public function createDatabaseDriver()
|
||||
{
|
||||
$connection = $this->container['db']->connection(config('setting.database.connection'));
|
||||
$table = config('setting.database.table');
|
||||
$key = config('setting.database.key');
|
||||
$value = config('setting.database.value');
|
||||
$encryptedKeys = config('setting.encrypted_keys');
|
||||
|
||||
return new Database($connection, $table, $key, $value, $encryptedKeys);
|
||||
}
|
||||
|
||||
public function createMemoryDriver()
|
||||
{
|
||||
return new Memory();
|
||||
}
|
||||
|
||||
public function createArrayDriver()
|
||||
{
|
||||
return $this->createMemoryDriver();
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Akaunting\Setting\Middleware;
|
||||
|
||||
use Closure;
|
||||
|
||||
class AutoSaveSetting
|
||||
{
|
||||
/**
|
||||
* Create a new save settings middleware.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->setting = app('setting');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
$this->setting->save();
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
Vendored
-42
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateSettingsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Set up the options.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->table = config('setting.database.table');
|
||||
$this->key = config('setting.database.key');
|
||||
$this->value = config('setting.database.value');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create($this->table, function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string($this->key)->index();
|
||||
$table->text($this->value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::drop($this->table);
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Akaunting\Setting;
|
||||
|
||||
use Akaunting\Setting\Middleware\AutoSaveSetting;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\View\Compilers\BladeCompiler;
|
||||
|
||||
class Provider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->publishes([
|
||||
__DIR__ . '/Config/setting.php' => config_path('setting.php'),
|
||||
__DIR__ . '/Migrations/2017_08_24_000000_create_settings_table.php' => database_path('migrations/2017_08_24_000000_create_settings_table.php'),
|
||||
], 'setting');
|
||||
|
||||
// Auto save setting
|
||||
if (config('setting.auto_save')) {
|
||||
$kernel = $this->app['Illuminate\Contracts\Http\Kernel'];
|
||||
$kernel->pushMiddleware(AutoSaveSetting::class);
|
||||
}
|
||||
|
||||
$this->override();
|
||||
|
||||
// Register blade directive
|
||||
$this->callAfterResolving('blade.compiler', function (BladeCompiler $compiler) {
|
||||
$compiler->directive('setting', function ($expression) {
|
||||
return "<?php echo setting($expression); ?>";
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->singleton('setting.manager', function ($app) {
|
||||
return new Manager($app);
|
||||
});
|
||||
|
||||
$this->app->singleton('setting', function ($app) {
|
||||
return $app['setting.manager']->driver();
|
||||
});
|
||||
|
||||
$this->mergeConfigFrom(__DIR__ . '/Config/setting.php', 'setting');
|
||||
}
|
||||
|
||||
private function override()
|
||||
{
|
||||
$override = config('setting.override', []);
|
||||
|
||||
foreach (Arr::dot($override) as $config_key => $setting_key) {
|
||||
$config_key = is_string($config_key) ? $config_key : $setting_key;
|
||||
|
||||
try {
|
||||
if (! is_null($value = setting($setting_key))) {
|
||||
config([$config_key => $value]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Akaunting\Setting\Support;
|
||||
|
||||
class Arr
|
||||
{
|
||||
/**
|
||||
* This class is a static class and should not be instantiated.
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an element from an array.
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $key Specify a nested element by separating keys with full stops.
|
||||
* @param mixed $default If the element is not found, return this.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get(array $data, $key, $default = null)
|
||||
{
|
||||
if ($key === null) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
if (is_array($key)) {
|
||||
return static::getArray($data, $key, $default);
|
||||
}
|
||||
|
||||
foreach (explode('.', $key) as $segment) {
|
||||
if (!is_array($data)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
if (!array_key_exists($segment, $data)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$data = $data[$segment];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected static function getArray(array $input, $keys, $default = null)
|
||||
{
|
||||
$output = [];
|
||||
|
||||
foreach ($keys as $key) {
|
||||
static::set($output, $key, static::get($input, $key, $default));
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an array has a given key.
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $key
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function has(array $data, $key)
|
||||
{
|
||||
foreach (explode('.', $key) as $segment) {
|
||||
if (!is_array($data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!array_key_exists($segment, $data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = $data[$segment];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an element of an array.
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $key Specify a nested element by separating keys with full stops.
|
||||
* @param mixed $value
|
||||
*/
|
||||
public static function set(array &$data, $key, $value)
|
||||
{
|
||||
$segments = explode('.', $key);
|
||||
|
||||
$key = array_pop($segments);
|
||||
|
||||
// iterate through all of $segments except the last one
|
||||
foreach ($segments as $segment) {
|
||||
if (!array_key_exists($segment, $data)) {
|
||||
$data[$segment] = array();
|
||||
} elseif (!is_array($data[$segment])) {
|
||||
throw new \UnexpectedValueException('Non-array segment encountered');
|
||||
}
|
||||
|
||||
$data = &$data[$segment];
|
||||
}
|
||||
|
||||
$data[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset an element from an array.
|
||||
*
|
||||
* @param array &$data
|
||||
* @param string $key Specify a nested element by separating keys with full stops.
|
||||
*/
|
||||
public static function forget(array &$data, $key)
|
||||
{
|
||||
$segments = explode('.', $key);
|
||||
|
||||
$key = array_pop($segments);
|
||||
|
||||
// iterate through all of $segments except the last one
|
||||
foreach ($segments as $segment) {
|
||||
if (!array_key_exists($segment, $data)) {
|
||||
return;
|
||||
} elseif (!is_array($data[$segment])) {
|
||||
throw new \UnexpectedValueException('Non-array segment encountered');
|
||||
}
|
||||
|
||||
$data = &$data[$segment];
|
||||
}
|
||||
|
||||
unset($data[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two multidimensional arrays recursive
|
||||
*
|
||||
* @param array $array_1
|
||||
* @param array $array_2
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function merge(array $array_1, array $array_2)
|
||||
{
|
||||
$merged = $array_1;
|
||||
|
||||
foreach ($array_2 as $key => $value) {
|
||||
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
|
||||
$merged[$key] = static::merge($merged[$key], $value);
|
||||
} elseif (is_numeric($key)) {
|
||||
if (!in_array($value, $merged)) {
|
||||
$merged[] = $value;
|
||||
}
|
||||
} else {
|
||||
$merged[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $merged;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
if (!function_exists('array_keys_exists')) {
|
||||
/**
|
||||
* Easily check if multiple array keys exist.
|
||||
*
|
||||
* @param array $keys
|
||||
* @param array $arr
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
function array_keys_exists(array $keys, array $arr)
|
||||
{
|
||||
return !array_diff_key(array_flip($keys), $arr);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('setting')) {
|
||||
/**
|
||||
* Get / set the specified setting value.
|
||||
*
|
||||
* If an array is passed as the key, we will assume you want to set an array of values.
|
||||
*
|
||||
* @param array|string $key
|
||||
* @param mixed $default
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
function setting($key = null, $default = null)
|
||||
{
|
||||
$setting = app('setting');
|
||||
|
||||
if (is_null($key)) {
|
||||
return $setting;
|
||||
}
|
||||
|
||||
if (is_array($key)) {
|
||||
$setting->set($key);
|
||||
|
||||
return $setting;
|
||||
}
|
||||
|
||||
return $setting->get($key, $default);
|
||||
}
|
||||
}
|
||||
-118
@@ -1,118 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Akaunting\Setting\Drivers\Database;
|
||||
|
||||
abstract class AbstractFunctionalTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
abstract protected function createStore(array $data = []);
|
||||
|
||||
protected function assertStoreEquals($store, $expected, $message = null)
|
||||
{
|
||||
$this->assertEquals($expected, $store->all(), $message);
|
||||
$store->save();
|
||||
$store = $this->createStore();
|
||||
$this->assertEquals($expected, $store->all(), $message);
|
||||
}
|
||||
|
||||
protected function assertStoreKeyEquals($store, $key, $expected, $message = null)
|
||||
{
|
||||
$this->assertEquals($expected, $store->get($key), $message);
|
||||
$store->save();
|
||||
$store = $this->createStore();
|
||||
$this->assertEquals($expected, $store->get($key), $message);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function store_is_initially_empty()
|
||||
{
|
||||
$store = $this->createStore();
|
||||
$this->assertEquals([], $store->all());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function written_changes_are_saved()
|
||||
{
|
||||
$store = $this->createStore();
|
||||
$store->set('foo', 'bar');
|
||||
$this->assertStoreKeyEquals($store, 'foo', 'bar');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function nested_keys_are_nested()
|
||||
{
|
||||
$store = $this->createStore();
|
||||
$store->set('foo.bar', 'baz');
|
||||
$this->assertStoreEquals($store, ['foo' => ['bar' => 'baz']]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function cannot_set_nested_key_on_non_array_member()
|
||||
{
|
||||
$store = $this->createStore();
|
||||
$store->set('foo', 'bar');
|
||||
$this->setExpectedException('UnexpectedValueException', 'Non-array segment encountered');
|
||||
$store->set('foo.bar', 'baz');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function can_forget_key()
|
||||
{
|
||||
$store = $this->createStore();
|
||||
$store->set('foo', 'bar');
|
||||
$store->set('bar', 'baz');
|
||||
$this->assertStoreEquals($store, ['foo' => 'bar', 'bar' => 'baz']);
|
||||
|
||||
$store->forget('foo');
|
||||
$this->assertStoreEquals($store, ['bar' => 'baz']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function can_forget_nested_key()
|
||||
{
|
||||
$store = $this->createStore();
|
||||
$store->set('foo.bar', 'baz');
|
||||
$store->set('foo.baz', 'bar');
|
||||
$store->set('bar.foo', 'baz');
|
||||
$this->assertStoreEquals($store, [
|
||||
'foo' => [
|
||||
'bar' => 'baz',
|
||||
'baz' => 'bar',
|
||||
],
|
||||
'bar' => [
|
||||
'foo' => 'baz',
|
||||
],
|
||||
]);
|
||||
|
||||
$store->forget('foo.bar');
|
||||
$this->assertStoreEquals($store, [
|
||||
'foo' => [
|
||||
'baz' => 'bar',
|
||||
],
|
||||
'bar' => [
|
||||
'foo' => 'baz',
|
||||
],
|
||||
]);
|
||||
|
||||
$store->forget('bar.foo');
|
||||
$expected = [
|
||||
'foo' => [
|
||||
'baz' => 'bar',
|
||||
],
|
||||
'bar' => [
|
||||
],
|
||||
];
|
||||
if ($store instanceof Database) {
|
||||
unset($expected['bar']);
|
||||
}
|
||||
$this->assertStoreEquals($store, $expected);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function can_forget_all()
|
||||
{
|
||||
$store = $this->createStore(['foo' => 'bar']);
|
||||
$this->assertStoreEquals($store, ['foo' => 'bar']);
|
||||
$store->forgetAll();
|
||||
$this->assertStoreEquals($store, []);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
class DatabaseTest extends AbstractFunctionalTest
|
||||
{
|
||||
public function setUp()
|
||||
{
|
||||
$this->container = new \Illuminate\Container\Container();
|
||||
$this->capsule = new \Illuminate\Database\Capsule\Manager($this->container);
|
||||
$this->capsule->setAsGlobal();
|
||||
$this->container['db'] = $this->capsule;
|
||||
$this->capsule->addConnection([
|
||||
'driver' => 'sqlite',
|
||||
'database' => ':memory:',
|
||||
'prefix' => '',
|
||||
]);
|
||||
|
||||
$this->capsule->schema()->create('settings', function ($t) {
|
||||
$t->string('key', 64)->unique();
|
||||
$t->string('value', 4096);
|
||||
});
|
||||
}
|
||||
|
||||
public function tearDown()
|
||||
{
|
||||
$this->capsule->schema()->drop('settings');
|
||||
unset($this->capsule);
|
||||
unset($this->container);
|
||||
}
|
||||
|
||||
protected function createStore(array $data = [])
|
||||
{
|
||||
if ($data) {
|
||||
$store = $this->createStore();
|
||||
$store->set($data);
|
||||
$store->save();
|
||||
unset($store);
|
||||
}
|
||||
|
||||
return new \Akaunting\Setting\Drivers\Database(
|
||||
$this->capsule->getConnection()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
class JsonTest extends AbstractFunctionalTest
|
||||
{
|
||||
protected function createStore(array $data = null)
|
||||
{
|
||||
$path = dirname(__DIR__) . '/tmp/store.json';
|
||||
|
||||
if ($data !== null) {
|
||||
if ($data) {
|
||||
$json = json_encode($data);
|
||||
} else {
|
||||
$json = '{}';
|
||||
}
|
||||
|
||||
file_put_contents($path, $json);
|
||||
}
|
||||
|
||||
return new \Akaunting\Setting\Drivers\Json(
|
||||
new \Illuminate\Filesystem\Filesystem(),
|
||||
$path
|
||||
);
|
||||
}
|
||||
|
||||
public function tearDown()
|
||||
{
|
||||
$path = dirname(__DIR__) . '/tmp/store.json';
|
||||
unlink($path);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
class MemoryTest extends AbstractFunctionalTest
|
||||
{
|
||||
protected function assertStoreEquals($store, $expected, $message = null)
|
||||
{
|
||||
$this->assertEquals($expected, $store->all(), $message);
|
||||
// removed persistance test assertions
|
||||
}
|
||||
|
||||
protected function assertStoreKeyEquals($store, $key, $expected, $message = null)
|
||||
{
|
||||
$this->assertEquals($expected, $store->get($key), $message);
|
||||
// removed persistance test assertions
|
||||
}
|
||||
|
||||
protected function createStore(array $data = null)
|
||||
{
|
||||
return new \Akaunting\Setting\Drivers\Memory($data);
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Akaunting\Setting\Support\Arr;
|
||||
|
||||
class ArrayUtilityTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider getGetData
|
||||
*/
|
||||
public function getReturnsCorrectValue(array $data, $key, $expected)
|
||||
{
|
||||
$this->assertEquals($expected, Arr::get($data, $key));
|
||||
}
|
||||
|
||||
public function getGetData()
|
||||
{
|
||||
return [
|
||||
[[], 'foo', null],
|
||||
[['foo' => 'bar'], 'foo', 'bar'],
|
||||
[['foo' => 'bar'], 'bar', null],
|
||||
[['foo' => 'bar'], 'foo.bar', null],
|
||||
[['foo' => ['bar' => 'baz']], 'foo.bar', 'baz'],
|
||||
[['foo' => ['bar' => 'baz']], 'foo.baz', null],
|
||||
[['foo' => ['bar' => 'baz']], 'foo', ['bar' => 'baz']],
|
||||
[
|
||||
['foo' => 'bar', 'bar' => 'baz'],
|
||||
['foo', 'bar'],
|
||||
['foo' => 'bar', 'bar' => 'baz'],
|
||||
],
|
||||
[
|
||||
['foo' => ['bar' => 'baz'], 'bar' => 'baz'],
|
||||
['foo.bar', 'bar'],
|
||||
['foo' => ['bar' => 'baz'], 'bar' => 'baz'],
|
||||
],
|
||||
[
|
||||
['foo' => ['bar' => 'baz'], 'bar' => 'baz'],
|
||||
['foo.bar'],
|
||||
['foo' => ['bar' => 'baz']],
|
||||
],
|
||||
[
|
||||
['foo' => ['bar' => 'baz'], 'bar' => 'baz'],
|
||||
['foo.bar', 'baz'],
|
||||
['foo' => ['bar' => 'baz'], 'baz' => null],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider getSetData
|
||||
*/
|
||||
public function setSetsCorrectKeyToValue(array $input, $key, $value, array $expected)
|
||||
{
|
||||
Arr::set($input, $key, $value);
|
||||
$this->assertEquals($expected, $input);
|
||||
}
|
||||
|
||||
public function getSetData()
|
||||
{
|
||||
return [
|
||||
[
|
||||
['foo' => 'bar'],
|
||||
'foo',
|
||||
'baz',
|
||||
['foo' => 'baz'],
|
||||
],
|
||||
[
|
||||
[],
|
||||
'foo',
|
||||
'bar',
|
||||
['foo' => 'bar'],
|
||||
],
|
||||
[
|
||||
[],
|
||||
'foo.bar',
|
||||
'baz',
|
||||
['foo' => ['bar' => 'baz']],
|
||||
],
|
||||
[
|
||||
['foo' => ['bar' => 'baz']],
|
||||
'foo.baz',
|
||||
'foo',
|
||||
['foo' => ['bar' => 'baz', 'baz' => 'foo']],
|
||||
],
|
||||
[
|
||||
['foo' => ['bar' => 'baz']],
|
||||
'foo.baz.bar',
|
||||
'baz',
|
||||
['foo' => ['bar' => 'baz', 'baz' => ['bar' => 'baz']]],
|
||||
],
|
||||
[
|
||||
[],
|
||||
'foo.bar.baz',
|
||||
'foo',
|
||||
['foo' => ['bar' => ['baz' => 'foo']]],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function setThrowsExceptionOnNonArraySegment()
|
||||
{
|
||||
$data = ['foo' => 'bar'];
|
||||
$this->setExpectedException('UnexpectedValueException', 'Non-array segment encountered');
|
||||
Arr::set($data, 'foo.bar', 'baz');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider getHasData
|
||||
*/
|
||||
public function hasReturnsCorrectly(array $input, $key, $expected)
|
||||
{
|
||||
$this->assertEquals($expected, Arr::has($input, $key));
|
||||
}
|
||||
|
||||
public function getHasData()
|
||||
{
|
||||
return [
|
||||
[[], 'foo', false],
|
||||
[['foo' => 'bar'], 'foo', true],
|
||||
[['foo' => 'bar'], 'bar', false],
|
||||
[['foo' => 'bar'], 'foo.bar', false],
|
||||
[['foo' => ['bar' => 'baz']], 'foo.bar', true],
|
||||
[['foo' => ['bar' => 'baz']], 'foo.baz', false],
|
||||
[['foo' => ['bar' => 'baz']], 'foo', true],
|
||||
[['foo' => null], 'foo', true],
|
||||
[['foo' => ['bar' => null]], 'foo.bar', true],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Mockery as m;
|
||||
|
||||
class DatabaseDriverTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function tearDown()
|
||||
{
|
||||
m::close();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function correct_data_is_inserted_and_updated()
|
||||
{
|
||||
$connection = $this->mockConnection();
|
||||
$query = $this->mockQuery($connection);
|
||||
|
||||
$query->shouldReceive('get')->once()->andReturn([
|
||||
['key' => 'nest.one', 'value' => 'old'],
|
||||
]);
|
||||
$query->shouldReceive('lists')->atMost(1)->andReturn(['nest.one']);
|
||||
$query->shouldReceive('pluck')->atMost(1)->andReturn(['nest.one']);
|
||||
$dbData = $this->getDbData();
|
||||
unset($dbData[1]); // remove the nest.one array member
|
||||
$query->shouldReceive('where')->with('key', '=', 'nest.one')->andReturn(m::self())->getMock()
|
||||
->shouldReceive('update')->with(['value' => 'nestone']);
|
||||
$self = $this; // 5.3 compatibility
|
||||
$query->shouldReceive('insert')->once()->andReturnUsing(function ($arg) use ($dbData, $self) {
|
||||
$self->assertEquals(count($dbData), count($arg));
|
||||
foreach ($dbData as $key => $value) {
|
||||
$self->assertContains($value, $arg);
|
||||
}
|
||||
});
|
||||
|
||||
$store = $this->makeStore($connection);
|
||||
$store->set('foo', 'bar');
|
||||
$store->set('nest.one', 'nestone');
|
||||
$store->set('nest.two', 'nesttwo');
|
||||
$store->set('array', ['one', 'two']);
|
||||
$store->save();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function extra_columns_are_queried()
|
||||
{
|
||||
$connection = $this->mockConnection();
|
||||
$query = $this->mockQuery($connection);
|
||||
$query->shouldReceive('where')->once()->with('foo', '=', 'bar')
|
||||
->andReturn(m::self())->getMock()
|
||||
->shouldReceive('get')->once()->andReturn([
|
||||
['key' => 'foo', 'value' => 'bar'],
|
||||
]);
|
||||
|
||||
$store = $this->makeStore($connection);
|
||||
$store->setExtraColumns(['foo' => 'bar']);
|
||||
$this->assertEquals('bar', $store->get('foo'));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function extra_columns_are_inserted()
|
||||
{
|
||||
$connection = $this->mockConnection();
|
||||
$query = $this->mockQuery($connection);
|
||||
$query->shouldReceive('where')->times(2)->with('extracol', '=', 'extradata')
|
||||
->andReturn(m::self());
|
||||
$query->shouldReceive('get')->once()->andReturn([]);
|
||||
$query->shouldReceive('lists')->atMost(1)->andReturn([]);
|
||||
$query->shouldReceive('pluck')->atMost(1)->andReturn([]);
|
||||
$query->shouldReceive('insert')->once()->with([
|
||||
['key' => 'foo', 'value' => 'bar', 'extracol' => 'extradata'],
|
||||
]);
|
||||
|
||||
$store = $this->makeStore($connection);
|
||||
$store->setExtraColumns(['extracol' => 'extradata']);
|
||||
$store->set('foo', 'bar');
|
||||
$store->save();
|
||||
}
|
||||
|
||||
protected function getDbData()
|
||||
{
|
||||
return [
|
||||
['key' => 'foo', 'value' => 'bar'],
|
||||
['key' => 'nest.one', 'value' => 'nestone'],
|
||||
['key' => 'nest.two', 'value' => 'nesttwo'],
|
||||
['key' => 'array.0', 'value' => 'one'],
|
||||
['key' => 'array.1', 'value' => 'two'],
|
||||
];
|
||||
}
|
||||
|
||||
protected function mockConnection()
|
||||
{
|
||||
return m::mock('Illuminate\Database\Connection');
|
||||
}
|
||||
|
||||
protected function mockQuery($connection)
|
||||
{
|
||||
$query = m::mock('Illuminate\Database\Query\Builder');
|
||||
$connection->shouldReceive('table')->andReturn($query);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function makeStore($connection)
|
||||
{
|
||||
return new Akaunting\Setting\Drivers\Database($connection);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
use Mockery as m;
|
||||
|
||||
class HelperTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
public static $functions;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
self::$functions = m::mock();
|
||||
|
||||
Container::setInstance(new Container());
|
||||
|
||||
$store = m::mock('Akaunting\Setting\Contracts\Driver');
|
||||
|
||||
app()->bind('setting', function () use ($store) {
|
||||
return $store;
|
||||
});
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function helper_without_parameters_returns_store()
|
||||
{
|
||||
$this->assertInstanceOf('Akaunting\Setting\Contracts\Driver', setting());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function single_parameter_get_a_key_from_store()
|
||||
{
|
||||
app('setting')->shouldReceive('get')->with('foo', null)->once();
|
||||
|
||||
setting('foo');
|
||||
}
|
||||
|
||||
public function two_parameters_return_a_default_value()
|
||||
{
|
||||
app('setting')->shouldReceive('get')->with('foo', 'bar')->once();
|
||||
|
||||
setting('foo', 'bar');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function array_parameter_call_set_method_into_store()
|
||||
{
|
||||
app('setting')->shouldReceive('set')->with(['foo', 'bar'])->once();
|
||||
|
||||
setting(['foo', 'bar']);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Mockery as m;
|
||||
|
||||
class JsonDriverTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function tearDown()
|
||||
{
|
||||
m::close();
|
||||
}
|
||||
|
||||
protected function mockFilesystem()
|
||||
{
|
||||
return m::mock('Illuminate\Filesystem\Filesystem');
|
||||
}
|
||||
|
||||
protected function makeStore($files, $path = 'fakepath')
|
||||
{
|
||||
return new Akaunting\Setting\Drivers\Json($files, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function throws_exception_when_file_not_writeable()
|
||||
{
|
||||
$files = $this->mockFilesystem();
|
||||
$files->shouldReceive('exists')->once()->with('fakepath')->andReturn(true);
|
||||
$files->shouldReceive('isWritable')->once()->with('fakepath')->andReturn(false);
|
||||
$store = $this->makeStore($files);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function throws_exception_when_files_put_fails()
|
||||
{
|
||||
$files = $this->mockFilesystem();
|
||||
$files->shouldReceive('exists')->once()->with('fakepath')->andReturn(false);
|
||||
$files->shouldReceive('put')->once()->with('fakepath', '{}')->andReturn(false);
|
||||
$store = $this->makeStore($files);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @expectedException RuntimeException
|
||||
*/
|
||||
public function throws_exception_when_file_contains_invalid_json()
|
||||
{
|
||||
$files = $this->mockFilesystem();
|
||||
$files->shouldReceive('exists')->once()->with('fakepath')->andReturn(true);
|
||||
$files->shouldReceive('isWritable')->once()->with('fakepath')->andReturn(true);
|
||||
$files->shouldReceive('get')->once()->with('fakepath')->andReturn('[[!1!11]');
|
||||
|
||||
$store = $this->makeStore($files);
|
||||
$store->get('foo');
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
/vendor
|
||||
composer.phar
|
||||
composer.lock
|
||||
.DS_Store
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
language: php
|
||||
|
||||
dist: trusty
|
||||
|
||||
php:
|
||||
- 5.5
|
||||
- 5.6
|
||||
- 7.0
|
||||
- 7.1
|
||||
- 7.2
|
||||
|
||||
before_script:
|
||||
- travis_retry composer self-update
|
||||
- travis_retry composer install --prefer-source --no-interaction --dev
|
||||
|
||||
script:
|
||||
- composer install
|
||||
- vendor/bin/phpunit
|
||||
Vendored
-22
@@ -1,22 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Nguyễn Văn Ánh
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
-197
@@ -1,197 +0,0 @@
|
||||
No CAPTCHA reCAPTCHA
|
||||
==========
|
||||
|
||||
[](https://travis-ci.org/anhskohbo/no-captcha)
|
||||
[](https://packagist.org/packages/anhskohbo/no-captcha)
|
||||
[](https://packagist.org/packages/anhskohbo/no-captcha)
|
||||
[](https://packagist.org/packages/anhskohbo/no-captcha)
|
||||
[](https://packagist.org/packages/anhskohbo/no-captcha)
|
||||
|
||||

|
||||
|
||||
> For Laravel 4 use [v1](https://github.com/anhskohbo/no-captcha/tree/v1) branch.
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
composer require anhskohbo/no-captcha
|
||||
```
|
||||
|
||||
## Laravel 5 and above
|
||||
|
||||
### Setup
|
||||
|
||||
**_NOTE_** This package supports the auto-discovery feature of Laravel 5.5 and above, So skip these `Setup` instructions if you're using Laravel 5.5 and above.
|
||||
|
||||
In `app/config/app.php` add the following :
|
||||
|
||||
1- The ServiceProvider to the providers array :
|
||||
|
||||
```php
|
||||
Anhskohbo\NoCaptcha\NoCaptchaServiceProvider::class,
|
||||
```
|
||||
|
||||
2- The class alias to the aliases array :
|
||||
|
||||
```php
|
||||
'NoCaptcha' => Anhskohbo\NoCaptcha\Facades\NoCaptcha::class,
|
||||
```
|
||||
|
||||
3- Publish the config file
|
||||
|
||||
```ssh
|
||||
php artisan vendor:publish --provider="Anhskohbo\NoCaptcha\NoCaptchaServiceProvider"
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Add `NOCAPTCHA_SECRET` and `NOCAPTCHA_SITEKEY` in **.env** file :
|
||||
|
||||
```
|
||||
NOCAPTCHA_SECRET=secret-key
|
||||
NOCAPTCHA_SITEKEY=site-key
|
||||
```
|
||||
|
||||
(You can obtain them from [here](https://www.google.com/recaptcha/admin))
|
||||
|
||||
### Usage
|
||||
|
||||
#### Init js source
|
||||
|
||||
With default options :
|
||||
|
||||
```php
|
||||
{!! NoCaptcha::renderJs() !!}
|
||||
```
|
||||
|
||||
With [language support](https://developers.google.com/recaptcha/docs/language) or [onloadCallback](https://developers.google.com/recaptcha/docs/display#explicit_render) option :
|
||||
|
||||
```php
|
||||
{!! NoCaptcha::renderJs('fr', true, 'recaptchaCallback') !!}
|
||||
```
|
||||
|
||||
#### Display reCAPTCHA
|
||||
|
||||
Default widget :
|
||||
|
||||
```php
|
||||
{!! NoCaptcha::display() !!}
|
||||
```
|
||||
|
||||
With [custom attributes](https://developers.google.com/recaptcha/docs/display#render_param) (theme, size, callback ...) :
|
||||
|
||||
```php
|
||||
{!! NoCaptcha::display(['data-theme' => 'dark']) !!}
|
||||
```
|
||||
|
||||
Invisible reCAPTCHA using a [submit button](https://developers.google.com/recaptcha/docs/invisible):
|
||||
|
||||
```php
|
||||
{!! NoCaptcha::displaySubmit('my-form-id', 'submit now!', ['data-theme' => 'dark']) !!}
|
||||
```
|
||||
Notice that the id of the form is required in this method to let the autogenerated
|
||||
callback submit the form on a successful captcha verification.
|
||||
|
||||
#### Validation
|
||||
|
||||
Add `'g-recaptcha-response' => 'required|captcha'` to rules array :
|
||||
|
||||
```php
|
||||
$validate = Validator::make(Input::all(), [
|
||||
'g-recaptcha-response' => 'required|captcha'
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
##### Custom Validation Message
|
||||
|
||||
Add the following values to the `custom` array in the `validation` language file :
|
||||
|
||||
```php
|
||||
'custom' => [
|
||||
'g-recaptcha-response' => [
|
||||
'required' => 'Please verify that you are not a robot.',
|
||||
'captcha' => 'Captcha error! try again later or contact site admin.',
|
||||
],
|
||||
],
|
||||
```
|
||||
|
||||
Then check for captcha errors in the `Form` :
|
||||
|
||||
```php
|
||||
@if ($errors->has('g-recaptcha-response'))
|
||||
<span class="help-block">
|
||||
<strong>{{ $errors->first('g-recaptcha-response') }}</strong>
|
||||
</span>
|
||||
@endif
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
When using the [Laravel Testing functionality](http://laravel.com/docs/5.5/testing), you will need to mock out the response for the captcha form element.
|
||||
|
||||
So for any form tests involving the captcha, you can do this by mocking the facade behavior:
|
||||
|
||||
```php
|
||||
// prevent validation error on captcha
|
||||
NoCaptcha::shouldReceive('verifyResponse')
|
||||
->once()
|
||||
->andReturn(true);
|
||||
|
||||
// provide hidden input for your 'required' validation
|
||||
NoCaptcha::shouldReceive('display')
|
||||
->zeroOrMoreTimes()
|
||||
->andReturn('<input type="hidden" name="g-recaptcha-response" value="1" />');
|
||||
```
|
||||
|
||||
You can then test the remainder of your form as normal.
|
||||
|
||||
When using HTTP tests you can add the `g-recaptcha-response` to the request body for the 'required' validation:
|
||||
|
||||
```php
|
||||
// prevent validation error on captcha
|
||||
NoCaptcha::shouldReceive('verifyResponse')
|
||||
->once()
|
||||
->andReturn(true);
|
||||
|
||||
// POST request, with request body including g-recaptcha-response
|
||||
$response = $this->json('POST', '/register', [
|
||||
'g-recaptcha-response' => '1',
|
||||
'name' => 'John',
|
||||
'email' => 'john@example.com',
|
||||
'password' => '123456',
|
||||
'password_confirmation' => '123456',
|
||||
]);
|
||||
```
|
||||
|
||||
## Without Laravel
|
||||
|
||||
Checkout example below:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
require_once "vendor/autoload.php";
|
||||
|
||||
$secret = 'CAPTCHA-SECRET';
|
||||
$sitekey = 'CAPTCHA-SITEKEY';
|
||||
$captcha = new \Anhskohbo\NoCaptcha\NoCaptcha($secret, $sitekey);
|
||||
|
||||
if (! empty($_POST)) {
|
||||
var_dump($captcha->verifyResponse($_POST['g-recaptcha-response']));
|
||||
exit();
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<form action="?" method="POST">
|
||||
<?php echo $captcha->display(); ?>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
<?php echo $captcha->renderJs(); ?>
|
||||
```
|
||||
|
||||
## Contribute
|
||||
|
||||
https://github.com/anhskohbo/no-captcha/pulls
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"name": "anhskohbo/no-captcha",
|
||||
"description": "No CAPTCHA reCAPTCHA For Laravel.",
|
||||
"keywords": [
|
||||
"recaptcha",
|
||||
"no-captcha",
|
||||
"captcha",
|
||||
"laravel",
|
||||
"laravel4",
|
||||
"laravel5",
|
||||
"laravel6"
|
||||
],
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "anhskohbo",
|
||||
"email": "anhskohbo@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.5.5",
|
||||
"illuminate/support": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
|
||||
"guzzlehttp/guzzle": "^6.2|^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.8|^9.5.10|^10.5"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Anhskohbo\\NoCaptcha\\": "src/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Anhskohbo\\NoCaptcha\\NoCaptchaServiceProvider"
|
||||
],
|
||||
"aliases": {
|
||||
"NoCaptcha": "Anhskohbo\\NoCaptcha\\Facades\\NoCaptcha"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit backupGlobals="false"
|
||||
backupStaticAttributes="false"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false"
|
||||
syntaxCheck="false"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Package Test Suite">
|
||||
<directory suffix=".php">./tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Anhskohbo\NoCaptcha\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class NoCaptcha extends Facade
|
||||
{
|
||||
/**
|
||||
* Get the registered name of the component.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'captcha';
|
||||
}
|
||||
}
|
||||
-246
@@ -1,246 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Anhskohbo\NoCaptcha;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use GuzzleHttp\Client;
|
||||
|
||||
class NoCaptcha
|
||||
{
|
||||
const CLIENT_API = 'https://www.google.com/recaptcha/api.js';
|
||||
const VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify';
|
||||
|
||||
/**
|
||||
* The recaptcha secret key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $secret;
|
||||
|
||||
/**
|
||||
* The recaptcha sitekey key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $sitekey;
|
||||
|
||||
/**
|
||||
* @var \GuzzleHttp\Client
|
||||
*/
|
||||
protected $http;
|
||||
|
||||
/**
|
||||
* The cached verified responses.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $verifiedResponses = [];
|
||||
|
||||
/**
|
||||
* NoCaptcha.
|
||||
*
|
||||
* @param string $secret
|
||||
* @param string $sitekey
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct($secret, $sitekey, $options = [])
|
||||
{
|
||||
$this->secret = $secret;
|
||||
$this->sitekey = $sitekey;
|
||||
$this->http = new Client($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render HTML captcha.
|
||||
*
|
||||
* @param array $attributes
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function display($attributes = [])
|
||||
{
|
||||
$attributes = $this->prepareAttributes($attributes);
|
||||
return '<div' . $this->buildAttributes($attributes) . '></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* @see display()
|
||||
*/
|
||||
public function displayWidget($attributes = [])
|
||||
{
|
||||
return $this->display($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a Invisible reCAPTCHA by embedding a callback into a form submit button.
|
||||
*
|
||||
* @param string $formIdentifier the html ID of the form that should be submitted.
|
||||
* @param string $text the text inside the form button
|
||||
* @param array $attributes array of additional html elements
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function displaySubmit($formIdentifier, $text = 'submit', $attributes = [])
|
||||
{
|
||||
$javascript = '';
|
||||
if (!isset($attributes['data-callback'])) {
|
||||
$functionName = 'onSubmit' . str_replace(['-', '=', '\'', '"', '<', '>', '`'], '', $formIdentifier);
|
||||
$attributes['data-callback'] = $functionName;
|
||||
$javascript = sprintf(
|
||||
'<script>function %s(){document.getElementById("%s").submit();}</script>',
|
||||
$functionName,
|
||||
$formIdentifier
|
||||
);
|
||||
}
|
||||
|
||||
$attributes = $this->prepareAttributes($attributes);
|
||||
|
||||
$button = sprintf('<button%s><span>%s</span></button>', $this->buildAttributes($attributes), $text);
|
||||
|
||||
return $button . $javascript;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render js source
|
||||
*
|
||||
* @param null $lang
|
||||
* @param bool $callback
|
||||
* @param string $onLoadClass
|
||||
* @return string
|
||||
*/
|
||||
public function renderJs($lang = null, $callback = false, $onLoadClass = 'onloadCallBack')
|
||||
{
|
||||
return '<script src="'.$this->getJsLink($lang, $callback, $onLoadClass).'" async defer></script>'."\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify no-captcha response.
|
||||
*
|
||||
* @param string $response
|
||||
* @param string $clientIp
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function verifyResponse($response, $clientIp = null)
|
||||
{
|
||||
if (empty($response)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Return true if response already verfied before.
|
||||
if (in_array($response, $this->verifiedResponses)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$verifyResponse = $this->sendRequestVerify([
|
||||
'secret' => $this->secret,
|
||||
'response' => $response,
|
||||
'remoteip' => $clientIp,
|
||||
]);
|
||||
|
||||
if (isset($verifyResponse['success']) && $verifyResponse['success'] === true) {
|
||||
// A response can only be verified once from google, so we need to
|
||||
// cache it to make it work in case we want to verify it multiple times.
|
||||
$this->verifiedResponses[] = $response;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify no-captcha response by Symfony Request.
|
||||
*
|
||||
* @param Request $request
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function verifyRequest(Request $request)
|
||||
{
|
||||
return $this->verifyResponse(
|
||||
$request->get('g-recaptcha-response'),
|
||||
$request->getClientIp()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recaptcha js link.
|
||||
*
|
||||
* @param string $lang
|
||||
* @param boolean $callback
|
||||
* @param string $onLoadClass
|
||||
* @return string
|
||||
*/
|
||||
public function getJsLink($lang = null, $callback = false, $onLoadClass = 'onloadCallBack')
|
||||
{
|
||||
$client_api = static::CLIENT_API;
|
||||
$params = [];
|
||||
|
||||
$callback ? $this->setCallBackParams($params, $onLoadClass) : false;
|
||||
$lang ? $params['hl'] = $lang : null;
|
||||
|
||||
return $client_api . '?'. http_build_query($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $params
|
||||
* @param $onLoadClass
|
||||
*/
|
||||
protected function setCallBackParams(&$params, $onLoadClass)
|
||||
{
|
||||
$params['render'] = 'explicit';
|
||||
$params['onload'] = $onLoadClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send verify request.
|
||||
*
|
||||
* @param array $query
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function sendRequestVerify(array $query = [])
|
||||
{
|
||||
$response = $this->http->request('POST', static::VERIFY_URL, [
|
||||
'form_params' => $query,
|
||||
]);
|
||||
|
||||
return json_decode($response->getBody(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare HTML attributes and assure that the correct classes and attributes for captcha are inserted.
|
||||
*
|
||||
* @param array $attributes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function prepareAttributes(array $attributes)
|
||||
{
|
||||
$attributes['data-sitekey'] = $this->sitekey;
|
||||
if (!isset($attributes['class'])) {
|
||||
$attributes['class'] = '';
|
||||
}
|
||||
$attributes['class'] = trim('g-recaptcha ' . $attributes['class']);
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build HTML attributes.
|
||||
*
|
||||
* @param array $attributes
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function buildAttributes(array $attributes)
|
||||
{
|
||||
$html = [];
|
||||
|
||||
foreach ($attributes as $key => $value) {
|
||||
$html[] = $key.'="'.$value.'"';
|
||||
}
|
||||
|
||||
return count($html) ? ' '.implode(' ', $html) : '';
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Anhskohbo\NoCaptcha;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class NoCaptchaServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Indicates if loading of the provider is deferred.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $defer = false;
|
||||
|
||||
/**
|
||||
* Bootstrap the application events.
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$app = $this->app;
|
||||
|
||||
$this->bootConfig();
|
||||
|
||||
$app['validator']->extend('captcha', function ($attribute, $value) use ($app) {
|
||||
return $app['captcha']->verifyResponse($value, $app['request']->getClientIp());
|
||||
});
|
||||
|
||||
if ($app->bound('form')) {
|
||||
$app['form']->macro('captcha', function ($attributes = []) use ($app) {
|
||||
return $app['captcha']->display($attributes, $app->getLocale());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Booting configure.
|
||||
*/
|
||||
protected function bootConfig()
|
||||
{
|
||||
$path = __DIR__.'/config/captcha.php';
|
||||
|
||||
$this->mergeConfigFrom($path, 'captcha');
|
||||
|
||||
if (function_exists('config_path')) {
|
||||
$this->publishes([$path => config_path('captcha.php')]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the service provider.
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->singleton('captcha', function ($app) {
|
||||
return new NoCaptcha(
|
||||
$app['config']['captcha.secret'],
|
||||
$app['config']['captcha.sitekey'],
|
||||
$app['config']['captcha.options']
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function provides()
|
||||
{
|
||||
return ['captcha'];
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user