20 Commits

Author SHA1 Message Date
solocla fdc3af01f3 Merge branch 'feature/user_profile' of https://gitea.solocla.synology.me/solocla/zibo-dashboard into feature/user_profile 2026-05-26 16:05:26 +02:00
solocla 3d54140280 fixed employee profile 2026-05-26 16:05:24 +02:00
RMubarakzyanov bfdbbbfc8f Merge branch 'main' into feature/user_profile 2026-05-26 16:52:41 +03:00
RMubarakzyanov 40a5771a4b bulk operations 2026-05-24 01:04:41 +03:00
RMubarakzyanov 9f5a585717 Merge branch 'main' into feature/user_profile 2026-05-24 00:16:28 +03:00
RMubarakzyanov 9ec5419a86 dlFunction fix 2026-05-24 00:15:09 +03:00
RMubarakzyanov c05091e020 Merge branch 'main' into feature/20260520_scadenziario
# Conflicts:
#	public/userarea/scadenzario/index.php
2026-05-24 00:02:20 +03:00
RMubarakzyanov 0b470f290e fix auto-open 2026-05-23 23:56:43 +03:00
solocla e74870c8d3 added functions 2026-05-22 09:16:46 +02:00
RMubarakzyanov 9001eff317 file repo, cc, auto-open 2026-05-21 23:31:36 +03:00
RMubarakzyanov 7cbd74111d Merge branch 'main' into feature/user_profile
# Conflicts:
#	public/userarea/include/topbar.php
#	public/userarea/scadenzario/include/my_deadlines_widget.php
2026-05-21 22:44:42 +03:00
solocla 650676037a fixed date format 2026-05-20 14:48:45 +02:00
solocla 2fc34c3cf4 fixed redirection 2026-05-18 13:49:22 +02:00
solocla 955a7ed9e9 fixed user setting 2026-05-18 13:31:34 +02:00
RMubarakzyanov cb221a8039 fix initial+refresher 2026-05-17 21:13:57 +03:00
RMubarakzyanov ece1beb87f Merge branch 'main' into feature/user_profile
# Conflicts:
#	public/userarea/include/navbar.php
2026-05-17 20:06:15 +03:00
solocla e6a805f1f7 fixed permission 2026-05-15 21:10:28 +02:00
solocla fe84d446e7 stop tracking vendor 2026-05-15 20:54:16 +02:00
solocla 2ddf575191 phinx 2026-05-15 20:50:06 +02:00
solocla d73a8bb8d3 add permission to dashboard and navbar 2026-05-15 17:13:29 +02:00
11012 changed files with 6057 additions and 1352091 deletions
+4 -2
View File
@@ -31,6 +31,8 @@ MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_ENCRYPTION=null MAIL_ENCRYPTION=null
MANAGER_USER_ID=
PUSHER_APP_ID= PUSHER_APP_ID=
PUSHER_APP_KEY= PUSHER_APP_KEY=
PUSHER_APP_SECRET= PUSHER_APP_SECRET=
@@ -55,5 +57,5 @@ AZURE_REDIRECT_URI=https://your-app.com/auth/azure/callback
AZURE_TENANT_ID= AZURE_TENANT_ID=
MICROSOFT_CLIENT_ID=your_client_id_here MICROSOFT_CLIENT_ID=your_client_id_here
MICROSOFT_CLIENT_SECRET=your_client_secret_here MICROSOFT_CLIENT_SECRET=your_client_secret_here
MICROSOFT_REDIRECT_URI="${APP_URL}/auth/microsoft/callback" MICROSOFT_REDIRECT_URI="${APP_URL}/auth/microsoft/callback"
+3
View File
@@ -66,3 +66,6 @@ public/userarea/logsapi/commessaweb_customfields_763.json
public/userarea/logsapi/commessaweb_invia_762.json public/userarea/logsapi/commessaweb_invia_762.json
public/userarea/logsapi/commessaweb_invia_763.json public/userarea/logsapi/commessaweb_invia_763.json
public/userarea/logsapi/last_auth_url.txt public/userarea/logsapi/last_auth_url.txt
# User uploaded files
/public/userarea/files/
@@ -111,6 +111,14 @@ class LoginController extends Controller
return redirect()->to('userarea/production_dashboard.php'); return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('User')) { } elseif ($user->hasRole('User')) {
return redirect()->to('userarea/production_dashboard.php'); return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('HR')) {
return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('SuperUser')) {
return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('Management')) {
return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('Quality')) {
return redirect()->to('userarea/production_dashboard.php');
} }
// Se il ruolo non è specificato, reindirizza alla home predefinita // Se il ruolo non è specificato, reindirizza alla home predefinita
+1
View File
@@ -44,6 +44,7 @@
"phpmailer/phpmailer": "^6.9", "phpmailer/phpmailer": "^6.9",
"phpoffice/phpspreadsheet": "^4.1", "phpoffice/phpspreadsheet": "^4.1",
"proengsoft/laravel-jsvalidation": "^4.0.0", "proengsoft/laravel-jsvalidation": "^4.0.0",
"robmorgan/phinx": "^0.16.11",
"socialiteproviders/microsoft": "^4.7", "socialiteproviders/microsoft": "^4.7",
"spatie/laravel-query-builder": "^5.0", "spatie/laravel-query-builder": "^5.0",
"vanguardapp/activity-log": "^6.0", "vanguardapp/activity-log": "^6.0",
Generated
+646 -2
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "9c4f1e3bc3ee2180211c055e70635aef", "content-hash": "076e7721d08cfea8b06ce75dd8c6c576",
"packages": [ "packages": [
{ {
"name": "akaunting/laravel-setting", "name": "akaunting/laravel-setting",
@@ -251,6 +251,330 @@
], ],
"time": "2023-11-29T23:19:16+00:00" "time": "2023-11-29T23:19:16+00:00"
}, },
{
"name": "cakephp/chronos",
"version": "3.5.0",
"source": {
"type": "git",
"url": "https://github.com/cakephp/chronos.git",
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/chronos/zipball/e6e777b534244911566face8a5dbdbd7f7bda5a6",
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/clock": "^1.0"
},
"provide": {
"psr/clock-implementation": "1.0"
},
"require-dev": {
"cakephp/cakephp-codesniffer": "^5.0",
"phpunit/phpunit": "^10.5.58 || ^11.5.3 || ^12.1.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Cake\\Chronos\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brian Nesbitt",
"email": "brian@nesbot.com",
"homepage": "http://nesbot.com"
},
{
"name": "The CakePHP Team",
"homepage": "https://cakephp.org"
}
],
"description": "A simple API extension for DateTime.",
"homepage": "https://cakephp.org",
"keywords": [
"date",
"datetime",
"time"
],
"support": {
"issues": "https://github.com/cakephp/chronos/issues",
"source": "https://github.com/cakephp/chronos"
},
"time": "2026-04-10T02:50:39+00:00"
},
{
"name": "cakephp/core",
"version": "5.3.5",
"source": {
"type": "git",
"url": "https://github.com/cakephp/core.git",
"reference": "eb012517900ed288f580aa3487e9a09f28ea85f9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/core/zipball/eb012517900ed288f580aa3487e9a09f28ea85f9",
"reference": "eb012517900ed288f580aa3487e9a09f28ea85f9",
"shasum": ""
},
"require": {
"cakephp/utility": "^5.3.0",
"league/container": "^5.1",
"php": ">=8.2",
"psr/container": "^1.1 || ^2.0"
},
"provide": {
"psr/container-implementation": "^2.0"
},
"suggest": {
"cakephp/cache": "To use Configure::store() and restore().",
"cakephp/event": "To use PluginApplicationInterface or plugin applications.",
"league/container": "To use Container and ServiceProvider classes"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"files": [
"functions.php"
],
"psr-4": {
"Cake\\Core\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/core/graphs/contributors"
}
],
"description": "CakePHP Framework Core classes",
"homepage": "https://cakephp.org",
"keywords": [
"cakephp",
"core",
"framework"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/core"
},
"time": "2026-03-31T06:25:23+00:00"
},
{
"name": "cakephp/database",
"version": "5.3.5",
"source": {
"type": "git",
"url": "https://github.com/cakephp/database.git",
"reference": "cf94dcb57c54a1a308fd866b038cd6995910e36e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/database/zipball/cf94dcb57c54a1a308fd866b038cd6995910e36e",
"reference": "cf94dcb57c54a1a308fd866b038cd6995910e36e",
"shasum": ""
},
"require": {
"cakephp/chronos": "^3.3",
"cakephp/core": "^5.3.0",
"cakephp/datasource": "^5.3.0",
"php": ">=8.2",
"psr/log": "^3.0"
},
"require-dev": {
"cakephp/i18n": "^5.3.0",
"cakephp/log": "^5.3.0"
},
"suggest": {
"cakephp/i18n": "If you are using locale-aware datetime formats.",
"cakephp/log": "If you want to use query logging without providing a logger yourself."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"psr-4": {
"Cake\\Database\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/database/graphs/contributors"
}
],
"description": "Flexible and powerful Database abstraction library with a familiar PDO-like API",
"homepage": "https://cakephp.org",
"keywords": [
"abstraction",
"cakephp",
"database",
"database abstraction",
"pdo"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/database"
},
"time": "2026-03-31T06:25:23+00:00"
},
{
"name": "cakephp/datasource",
"version": "5.3.5",
"source": {
"type": "git",
"url": "https://github.com/cakephp/datasource.git",
"reference": "512464eb27b19316b515ec338089b83822c9ab5a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/datasource/zipball/512464eb27b19316b515ec338089b83822c9ab5a",
"reference": "512464eb27b19316b515ec338089b83822c9ab5a",
"shasum": ""
},
"require": {
"cakephp/core": "^5.3.0",
"php": ">=8.2",
"psr/simple-cache": "^2.0 || ^3.0"
},
"require-dev": {
"cakephp/cache": "^5.3.0",
"cakephp/collection": "^5.3.0",
"cakephp/utility": "^5.3.0"
},
"suggest": {
"cakephp/cache": "If you decide to use Query caching.",
"cakephp/collection": "If you decide to use ResultSetInterface.",
"cakephp/utility": "If you decide to use EntityTrait."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"psr-4": {
"Cake\\Datasource\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/datasource/graphs/contributors"
}
],
"description": "Provides connection managing and traits for Entities and Queries that can be reused for different datastores",
"homepage": "https://cakephp.org",
"keywords": [
"cakephp",
"connection management",
"datasource",
"entity",
"query"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/datasource"
},
"time": "2026-04-04T08:08:42+00:00"
},
{
"name": "cakephp/utility",
"version": "5.3.5",
"source": {
"type": "git",
"url": "https://github.com/cakephp/utility.git",
"reference": "4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/utility/zipball/4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99",
"reference": "4ac9826fe5faa1505ec5aa3c171d6b58b6ab4e99",
"shasum": ""
},
"require": {
"cakephp/core": "^5.3.0",
"php": ">=8.2"
},
"suggest": {
"ext-intl": "To use Text::transliterate() or Text::slug()",
"lib-ICU": "To use Text::transliterate() or Text::slug()"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Cake\\Utility\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/utility/graphs/contributors"
}
],
"description": "CakePHP Utility classes such as Inflector, String, Hash, and Security",
"homepage": "https://cakephp.org",
"keywords": [
"cakephp",
"hash",
"inflector",
"security",
"string",
"utility"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/utility"
},
"time": "2026-03-09T09:38:36+00:00"
},
{ {
"name": "carbonphp/carbon-doctrine-types", "name": "carbonphp/carbon-doctrine-types",
"version": "3.2.0", "version": "3.2.0",
@@ -2627,6 +2951,90 @@
], ],
"time": "2022-12-11T20:36:23+00:00" "time": "2022-12-11T20:36:23+00:00"
}, },
{
"name": "league/container",
"version": "5.2.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/container.git",
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/container/zipball/58accbc032f0090a9bd08326f93062c5a658b2c5",
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5",
"shasum": ""
},
"require": {
"php": "^8.1",
"psr/container": "^2.0.2",
"psr/event-dispatcher": "^1.0"
},
"provide": {
"psr/container-implementation": "^1.0"
},
"replace": {
"orno/di": "~2.0"
},
"require-dev": {
"nette/php-generator": "^4.1",
"nikic/php-parser": "^5.0",
"phpstan/phpstan": "^2.1.11",
"phpunit/phpunit": "^10.5.45|^11.5.15|^12.0",
"roave/security-advisories": "dev-latest",
"scrutinizer/ocular": "^1.9",
"squizlabs/php_codesniffer": "^3.9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-1.x": "1.x-dev",
"dev-2.x": "2.x-dev",
"dev-3.x": "3.x-dev",
"dev-4.x": "4.x-dev",
"dev-5.x": "5.x-dev",
"dev-master": "5.x-dev"
}
},
"autoload": {
"psr-4": {
"League\\Container\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Phil Bennett",
"email": "mail@philbennett.co.uk",
"role": "Developer"
}
],
"description": "A fast and intuitive dependency injection container.",
"homepage": "https://github.com/thephpleague/container",
"keywords": [
"container",
"dependency",
"di",
"injection",
"league",
"provider",
"service"
],
"support": {
"issues": "https://github.com/thephpleague/container/issues",
"source": "https://github.com/thephpleague/container/tree/5.2.0"
},
"funding": [
{
"url": "https://github.com/philipobenito",
"type": "github"
}
],
"time": "2026-03-19T18:52:39+00:00"
},
{ {
"name": "league/flysystem", "name": "league/flysystem",
"version": "3.28.0", "version": "3.28.0",
@@ -4980,6 +5388,93 @@
], ],
"time": "2024-04-27T21:32:50+00:00" "time": "2024-04-27T21:32:50+00:00"
}, },
{
"name": "robmorgan/phinx",
"version": "0.16.11",
"source": {
"type": "git",
"url": "https://github.com/cakephp/phinx.git",
"reference": "a03014fea316ba021fc0776982e5bed2d10228d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/phinx/zipball/a03014fea316ba021fc0776982e5bed2d10228d4",
"reference": "a03014fea316ba021fc0776982e5bed2d10228d4",
"shasum": ""
},
"require": {
"cakephp/database": "^5.0.2",
"composer-runtime-api": "^2.0",
"php-64bit": ">=8.1",
"psr/container": "^1.1|^2.0",
"symfony/config": "^4.0|^5.0|^6.0|^7.0|^8.0",
"symfony/console": "^6.0|^7.0|^8.0"
},
"require-dev": {
"cakephp/cakephp-codesniffer": "^5.0",
"cakephp/i18n": "^5.0",
"ext-json": "*",
"ext-pdo": "*",
"phpunit/phpunit": "^10.5",
"symfony/yaml": "^4.0|^5.0|^6.0|^7.0|^8.0"
},
"suggest": {
"ext-json": "Install if using JSON configuration format",
"ext-pdo": "PDO extension is needed",
"symfony/yaml": "Install if using YAML configuration format"
},
"bin": [
"bin/phinx"
],
"type": "library",
"autoload": {
"psr-4": {
"Phinx\\": "src/Phinx/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Rob Morgan",
"email": "robbym@gmail.com",
"homepage": "https://robmorgan.id.au",
"role": "Lead Developer"
},
{
"name": "Woody Gilk",
"email": "woody.gilk@gmail.com",
"homepage": "https://shadowhand.me",
"role": "Developer"
},
{
"name": "Richard Quadling",
"email": "rquadling@gmail.com",
"role": "Developer"
},
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/phinx/graphs/contributors",
"role": "Developer"
}
],
"description": "Phinx makes it ridiculously easy to manage the database migrations for your PHP app.",
"homepage": "https://phinx.org",
"keywords": [
"database",
"database migrations",
"db",
"migrations",
"phinx"
],
"support": {
"issues": "https://github.com/cakephp/phinx/issues",
"source": "https://github.com/cakephp/phinx/tree/0.16.11"
},
"time": "2026-03-15T00:04:32+00:00"
},
{ {
"name": "socialiteproviders/manager", "name": "socialiteproviders/manager",
"version": "v4.8.1", "version": "v4.8.1",
@@ -5312,6 +5807,85 @@
], ],
"time": "2024-05-31T14:57:53+00:00" "time": "2024-05-31T14:57:53+00:00"
}, },
{
"name": "symfony/config",
"version": "v7.4.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
"reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57",
"reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/filesystem": "^7.1|^8.0",
"symfony/polyfill-ctype": "~1.8"
},
"conflict": {
"symfony/finder": "<6.4",
"symfony/service-contracts": "<2.5"
},
"require-dev": {
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
"symfony/finder": "^6.4|^7.0|^8.0",
"symfony/messenger": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/yaml": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Config\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/config/tree/v7.4.10"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-03T14:20:49+00:00"
},
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v7.1.3", "version": "v7.1.3",
@@ -5768,6 +6342,76 @@
], ],
"time": "2024-04-18T09:32:20+00:00" "time": "2024-04-18T09:32:20+00:00"
}, },
{
"name": "symfony/filesystem",
"version": "v7.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.8"
},
"require-dev": {
"symfony/process": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Filesystem\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/filesystem/tree/v7.4.11"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-11T16:38:44+00:00"
},
{ {
"name": "symfony/finder", "name": "symfony/finder",
"version": "v7.1.3", "version": "v7.1.3",
@@ -11355,6 +11999,6 @@
"php": "^8.2.0", "php": "^8.2.0",
"ext-json": "*" "ext-json": "*"
}, },
"platform-dev": [], "platform-dev": {},
"plugin-api-version": "2.6.0" "plugin-api-version": "2.6.0"
} }
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class BaselineExistingDatabase extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
// Baseline migration.
// Existing database structure starts being tracked from this point.
}
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreatePhinxTestTable extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$table = $this->table('phinx_test_table');
$table
->addColumn('name', 'string', [
'limit' => 100,
'null' => false,
])
->addColumn('created_at', 'timestamp', [
'default' => 'CURRENT_TIMESTAMP',
'null' => false,
])
->create();
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddFunctionsToScadDeadlines extends AbstractMigration
{
public function change(): void
{
$this->table('scad_functions', [
'id' => false,
'primary_key' => ['id'],
'collation' => 'utf8mb4_unicode_ci',
'encoding' => 'utf8mb4',
])
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('name', 'string', [
'limit' => 255,
'null' => false,
])
->addColumn('description', 'text', [
'null' => true,
])
->addColumn('status', 'string', [
'limit' => 20,
'null' => false,
'default' => 'active',
])
->addColumn('created_at', 'timestamp', [
'null' => false,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => false,
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['name'], [
'unique' => true,
'name' => 'uniq_scad_functions_name',
])
->create();
$this->table('scad_deadlines')
->addColumn('function_id', 'integer', [
'signed' => false,
'null' => true,
'after' => 'subject_id',
])
->addIndex(['function_id'], [
'name' => 'idx_scad_deadlines_function_id',
])
->addForeignKey('function_id', 'scad_functions', 'id', [
'delete' => 'SET_NULL',
'update' => 'CASCADE',
'constraint' => 'fk_scad_deadlines_function',
])
->update();
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
if (file_exists(__DIR__ . '/.env')) {
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->safeLoad();
}
return [
'paths' => [
'migrations' => __DIR__ . '/db/migrations',
'seeds' => __DIR__ . '/db/seeds',
],
'environments' => [
'default_migration_table' => 'phinxlog',
'default_environment' => 'development',
'development' => [
'adapter' => $_ENV['DB_CONNECTION'] ?? 'mysql',
'host' => $_ENV['DB_HOST'] ?? 'localhost',
'name' => $_ENV['DB_DATABASE'] ?? '',
'user' => $_ENV['DB_USERNAME'] ?? '',
'pass' => $_ENV['DB_PASSWORD'] ?? '',
'port' => $_ENV['DB_PORT'] ?? 3306,
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
],
],
'version_order' => 'creation',
];
@@ -0,0 +1,104 @@
<?php
/**
* Bulk "renew": set a common completed_date on the selected training records
*/
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
// $pdo and $currentUserId from hr_auth_check.php
$completedDate = trim($_POST['completed_date'] ?? '');
$ids = $_POST['training_ids'] ?? [];
if (!is_array($ids)) {
$ids = [];
}
$ids = array_values(array_unique(array_filter(array_map('intval', $ids), fn($v) => $v > 0)));
if ($completedDate === '' || !DateTime::createFromFormat('Y-m-d', $completedDate)) {
echo json_encode(['success' => false, 'message' => 'Indicare una data valida.']);
exit;
}
if (empty($ids)) {
echo json_encode(['success' => false, 'message' => 'Selezionare almeno un record.']);
exit;
}
try {
$pdo->beginTransaction();
// Load each record with its topic default frequency
$rowStmt = $pdo->prepare("
SELECT et.id, et.employee_id, et.completed_date, et.next_due_date,
et.update_frequency_months, tt.default_frequency_months
FROM employee_trainings et
JOIN training_topics tt ON tt.id = et.training_topic_id
WHERE et.id = :id
");
$upd = $pdo->prepare("
UPDATE employee_trainings
SET completed_date = :cd, next_due_date = :nd, updated_at = NOW()
WHERE id = :id
");
$logStmt = $pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, :tid, 'updated', :field, :old_v, :new_v, :cb, NOW())
");
$updated = 0;
foreach ($ids as $id) {
$rowStmt->execute(['id' => $id]);
$row = $rowStmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
continue;
}
// Effective frequency: per-record override, else topic default
$effFreq = $row['update_frequency_months'] !== null
? (int)$row['update_frequency_months']
: ($row['default_frequency_months'] !== null ? (int)$row['default_frequency_months'] : null);
$nextDue = null;
if ($effFreq !== null && $effFreq > 0) {
$d = DateTime::createFromFormat('Y-m-d', $completedDate);
if ($d) {
$d->modify('+' . $effFreq . ' months');
$nextDue = $d->format('Y-m-d');
}
}
$upd->execute(['cd' => $completedDate, 'nd' => $nextDue, 'id' => $id]);
if ((string)$row['completed_date'] !== (string)$completedDate) {
$logStmt->execute([
'eid' => $row['employee_id'], 'tid' => $id, 'field' => 'completed_date',
'old_v' => $row['completed_date'], 'new_v' => $completedDate, 'cb' => $currentUserId,
]);
}
if ((string)$row['next_due_date'] !== (string)$nextDue) {
$logStmt->execute([
'eid' => $row['employee_id'], 'tid' => $id, 'field' => 'next_due_date',
'old_v' => $row['next_due_date'], 'new_v' => $nextDue, 'cb' => $currentUserId,
]);
}
$updated++;
}
$pdo->commit();
echo json_encode([
'success' => true,
'updated' => $updated,
'message' => $updated . ' record aggiornat' . ($updated === 1 ? 'o' : 'i') . '.',
]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,92 @@
<?php
/**
* Calendar events for the training calendar (training_calendar.php).
* Returns FullCalendar event objects for the *current* training record per
* (employee, topic) that has a next_due_date, colored by computed status.
* HR-only.
*/
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
try {
// $pdo and $currentUserId provided by hr_auth_check.php
$start = $_GET['start'] ?? null;
$end = $_GET['end'] ?? null;
$fStatus = isset($_GET['status']) ? trim($_GET['status']) : '';
$fDept = isset($_GET['department_id']) && $_GET['department_id'] !== '' ? (int)$_GET['department_id'] : 0;
$fTopic = isset($_GET['topic_id']) && $_GET['topic_id'] !== '' ? (int)$_GET['topic_id'] : 0;
$fEmp = isset($_GET['employee_id']) && $_GET['employee_id'] !== '' ? (int)$_GET['employee_id'] : 0;
$where = [];
$params = [];
// Deadlines only (one-time trainings have no next_due_date)
$where[] = "et.next_due_date IS NOT NULL";
// Only the most recent record per (employee, topic)
$where[] = "NOT EXISTS (
SELECT 1 FROM employee_trainings et2
WHERE et2.employee_id = et.employee_id
AND et2.training_topic_id = et.training_topic_id
AND (et2.completed_date > et.completed_date
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
)";
if ($start && $end) {
$where[] = "et.next_due_date >= :start AND et.next_due_date <= :end";
$params['start'] = $start;
$params['end'] = $end;
}
if ($fDept > 0) { $where[] = "e.department_id = :did"; $params['did'] = $fDept; }
if ($fTopic > 0) { $where[] = "et.training_topic_id = :tid"; $params['tid'] = $fTopic; }
if ($fEmp > 0) { $where[] = "et.employee_id = :eid"; $params['eid'] = $fEmp; }
$whereSql = 'WHERE ' . implode(' AND ', $where);
$stmt = $pdo->prepare("
SELECT et.id, et.employee_id, et.next_due_date, et.reminder_days,
tt.name AS topic_name, tt.default_reminder_days AS topic_default_rem,
e.first_name, e.last_name
FROM employee_trainings et
JOIN training_topics tt ON tt.id = et.training_topic_id
JOIN employees e ON e.id = et.employee_id
$whereSql
");
$stmt->execute($params);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$today = new DateTime('today');
$events = [];
foreach ($rows as $r) {
$rem = $r['reminder_days'] !== null
? (int)$r['reminder_days']
: ($r['topic_default_rem'] !== null ? (int)$r['topic_default_rem'] : 30);
$due = DateTime::createFromFormat('Y-m-d', $r['next_due_date']);
if (!$due) continue;
$daysLeft = (int)$today->diff($due)->format('%r%a');
if ($daysLeft < 0) { $code = 'expired'; $color = '#dc3545'; }
elseif ($daysLeft <= $rem){ $code = 'due_soon'; $color = '#e8930c'; }
else { $code = 'compliant'; $color = '#198754'; }
if ($fStatus !== '' && $fStatus !== $code) continue;
$name = trim($r['first_name'] . ' ' . $r['last_name']);
$events[] = [
'id' => (int)$r['id'],
'title' => $name . ' — ' . $r['topic_name'],
'start' => $r['next_due_date'],
'allDay' => true,
'backgroundColor' => $color,
'borderColor' => $color,
'url' => 'employee-profile.php?id=' . (int)$r['employee_id'] . '#tab-training',
];
}
echo json_encode($events);
} catch (Exception $e) {
echo json_encode([]);
}
@@ -0,0 +1,131 @@
<?php
/**
* Bulk-create training records: one employee_trainings row per selected employee,
* all sharing the same course + parameters (a single training "session").
* Mirrors the next_due_date logic of ajax/employee_profile/save_training.php.
* HR-only.
*/
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
// $pdo and $currentUserId from hr_auth_check.php
$topicId = (int)($_POST['training_topic_id'] ?? 0);
$completedDate = trim($_POST['completed_date'] ?? '');
$deliveredBy = trim($_POST['delivered_by'] ?? '');
$description = trim($_POST['description'] ?? '');
$trainingType = trim($_POST['training_type'] ?? 'initial');
$freqRaw = $_POST['update_frequency_months'] ?? '';
$remRaw = $_POST['reminder_days'] ?? '';
$employeeIds = $_POST['employee_ids'] ?? [];
if (!is_array($employeeIds)) {
$employeeIds = [];
}
$employeeIds = array_values(array_unique(array_filter(array_map('intval', $employeeIds), fn($v) => $v > 0)));
if ($topicId <= 0) {
echo json_encode(['success' => false, 'message' => 'Selezionare un corso.']);
exit;
}
if ($completedDate === '' || !DateTime::createFromFormat('Y-m-d', $completedDate)) {
echo json_encode(['success' => false, 'message' => 'La data di completamento è obbligatoria.']);
exit;
}
if (empty($employeeIds)) {
echo json_encode(['success' => false, 'message' => 'Selezionare almeno un dipendente.']);
exit;
}
if (!in_array($trainingType, ['initial', 'refresher'], true)) {
$trainingType = 'initial';
}
$topicStmt = $pdo->prepare("SELECT default_frequency_months, default_reminder_days FROM training_topics WHERE id = :id");
$topicStmt->execute(['id' => $topicId]);
$topic = $topicStmt->fetch(PDO::FETCH_ASSOC);
if (!$topic) {
echo json_encode(['success' => false, 'message' => 'Corso non trovato.']);
exit;
}
$freq = ($freqRaw === '' || $freqRaw === null) ? null : max(0, (int)$freqRaw);
$rem = ($remRaw === '' || $remRaw === null) ? null : max(0, (int)$remRaw);
/* Effective frequency → next_due_date (same for every employee: same date + same frequency) */
$effFreq = $freq !== null ? $freq : ($topic['default_frequency_months'] !== null ? (int)$topic['default_frequency_months'] : null);
$nextDue = null;
if ($effFreq !== null && $effFreq > 0) {
$d = DateTime::createFromFormat('Y-m-d', $completedDate);
if ($d) {
$d->modify('+' . (int)$effFreq . ' months');
$nextDue = $d->format('Y-m-d');
}
}
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
$description = $description !== '' ? $description : null;
try {
$pdo->beginTransaction();
// Only insert for employees that actually exist
$checkEmp = $pdo->prepare("SELECT id FROM employees WHERE id = :id");
$ins = $pdo->prepare("
INSERT INTO employee_trainings
(employee_id, training_topic_id, completed_date,
delivered_by, description,
training_type, update_frequency_months, reminder_days, next_due_date,
created_by, created_at, updated_at)
VALUES
(:eid, :tid, :completed_date,
:delivered_by, :description,
:training_type, :freq, :rem, :next_due,
:cb, NOW(), NOW())
");
$logStmt = $pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, :tid, 'created', NULL, NULL, NULL, :cb, NOW())
");
$created = 0;
foreach ($employeeIds as $eid) {
$checkEmp->execute(['id' => $eid]);
if (!$checkEmp->fetchColumn()) {
continue;
}
$ins->execute([
'eid' => $eid,
'tid' => $topicId,
'completed_date' => $completedDate,
'delivered_by' => $deliveredBy,
'description' => $description,
'training_type' => $trainingType,
'freq' => $freq,
'rem' => $rem,
'next_due' => $nextDue,
'cb' => $currentUserId,
]);
$newId = (int)$pdo->lastInsertId();
$logStmt->execute(['eid' => $eid, 'tid' => $newId, 'cb' => $currentUserId]);
$created++;
}
$pdo->commit();
echo json_encode([
'success' => true,
'created' => $created,
'message' => $created . ' formazion' . ($created === 1 ? 'e registrata' : 'i registrate') . '.',
]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -47,7 +47,8 @@ $sent = 0;
$skipped = 0; $skipped = 0;
$errors = 0; $errors = 0;
/* Candidate trainings (with optional override reminder + topic default) */ /* Candidate trainings (with optional override reminder + topic default).
Only the most recent record per (employee, topic) older history rows skipped. */
$stmt = $pdo->query(" $stmt = $pdo->query("
SELECT et.id, et.employee_id, et.completed_date, et.next_due_date, SELECT et.id, et.employee_id, et.completed_date, et.next_due_date,
et.reminder_days, et.delivered_by, et.reminder_days, et.delivered_by,
@@ -60,6 +61,13 @@ $stmt = $pdo->query("
JOIN employees e ON e.id = et.employee_id JOIN employees e ON e.id = et.employee_id
LEFT JOIN auth_users au ON au.id = e.auth_user_id LEFT JOIN auth_users au ON au.id = e.auth_user_id
WHERE et.next_due_date IS NOT NULL WHERE et.next_due_date IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM employee_trainings et2
WHERE et2.employee_id = et.employee_id
AND et2.training_topic_id = et.training_topic_id
AND (et2.completed_date > et.completed_date
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
)
"); ");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -59,5 +59,5 @@ $photousername = basename($avatar);
require_once(__DIR__ . '/../../languages/en/general.php'); require_once(__DIR__ . '/../../languages/en/general.php');
//include("generalsettings.php"); //include("generalsettings.php");
require_once __DIR__ . '/permissions_helper.php';
?> ?>
+332 -107
View File
@@ -6,166 +6,391 @@
<div> <div>
<h4 class="logo-text"><?= htmlspecialchars('ZIBOGOMMA', ENT_QUOTES, 'UTF-8'); ?></h4> <h4 class="logo-text"><?= htmlspecialchars('ZIBOGOMMA', ENT_QUOTES, 'UTF-8'); ?></h4>
</div> </div>
<div class="toggle-icon ms-auto"><i class='bx bx-arrow-back'></i> <div class="toggle-icon ms-auto">
<i class='bx bx-arrow-back'></i>
</div> </div>
</div> </div>
<!--navigation--> <!--navigation-->
<ul class="metismenu" id="menu"> <ul class="metismenu" id="menu">
<!-- Production: Admin / User / Superuser / employee-hr / manager -->
<?php if ( <?php if (userCan('production.dashboard.view')) : ?>
Auth::user()->hasRole('Admin')
|| Auth::user()->hasRole('User')
|| Auth::user()->hasRole('Superuser')
|| Auth::user()->hasRole('employee-hr')
|| Auth::user()->hasRole('manager')
) : ?>
<li> <li>
<a href="production_dashboard.php"> <a href="production_dashboard.php">
<div class="parent-icon"><i class="bx bx-home-alt"></i> <div class="parent-icon">
<i class="bx bx-home-alt"></i>
</div> </div>
<div class="menu-title">Dashboard</div> <div class="menu-title">Dashboard</div>
</a> </a>
</li> </li>
<?php endif; ?>
<?php
$canSeeProgramming =
userCan('production.programming.view')
|| userCan('templates.dashboard.view')
|| userCan('templates.create.view');
?>
<?php if ($canSeeProgramming) : ?>
<li> <li>
<a href="javascript:;" class="has-arrow"> <a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class="bx bx-category"></i> <div class="parent-icon">
<i class="bx bx-category"></i>
</div> </div>
<div class="menu-title">Programmazione</div> <div class="menu-title">Programmazione</div>
</a> </a>
<ul>
<li> <a href="templates_dashboard.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?></a>
</li>
<li> <a href="insert_template_xls.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?></a>
</li>
<ul>
<?php if (userCan('templates.dashboard.view')) : ?>
<li>
<a href="templates_dashboard.php">
<i class='bx bx-radio-circle'></i>
<?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?>
</a>
</li>
<?php endif; ?>
<?php if (userCan('templates.create.view')) : ?>
<li>
<a href="insert_template_xls.php">
<i class='bx bx-radio-circle'></i>
<?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?>
</a>
</li>
<?php endif; ?>
<?php if (userCan('production.programming.view')) : ?>
<li>
<a href="produzione_programmazione_drag.php">
<i class='bx bx-radio-circle'></i>
Programmazione Produzione
</a>
</li>
<?php endif; ?>
</ul> </ul>
</li> </li>
<?php endif; ?>
<?php
$canSeeFunctions =
userCan('masterdata.mescole.view')
|| userCan('masterdata.matrici.view')
|| userCan('masterdata.linee.view')
|| userCan('masterdata.packaging.view')
|| userCan('masterdata.suppliers.view')
|| userCan('masterdata.lookup.view')
|| userCan('masterdata.worksheets.view');
?>
<?php if ($canSeeFunctions) : ?>
<li> <li>
<a href="javascript:;" class="has-arrow"> <a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class="bx bx-category"></i> <div class="parent-icon">
<i class="bx bx-category"></i>
</div> </div>
<div class="menu-title">Funzioni</div> <div class="menu-title">Funzioni</div>
</a> </a>
<ul> <ul>
<li> <?php if (userCan('masterdata.mescole.view')) : ?>
<a href="mescole.php"><i class='bx bx-radio-circle'></i>Mescole</a> <li>
</li> <a href="mescole.php">
<li> <i class='bx bx-radio-circle'></i>Mescole
<a href="matrici.php"><i class='bx bx-radio-circle'></i>Matrici</a> </a>
</li> </li>
<li> <?php endif; ?>
<a href="linee.php"><i class='bx bx-radio-circle'></i>Linee di produzione</a>
</li>
<?php if (userCan('masterdata.matrici.view')) : ?>
<li>
<a href="matrici.php">
<i class='bx bx-radio-circle'></i>Matrici
</a>
</li>
<?php endif; ?>
<?php if (userCan('masterdata.linee.view')) : ?>
<li>
<a href="linee.php">
<i class='bx bx-radio-circle'></i>Linee di produzione
</a>
</li>
<?php endif; ?>
<?php if (userCan('masterdata.packaging.view')) : ?>
<li>
<a href="packaging_items.php">
<i class='bx bx-radio-circle'></i>Imballaggi
</a>
</li>
<?php endif; ?>
<?php if (userCan('masterdata.suppliers.view')) : ?>
<li>
<a href="suppliers.php">
<i class='bx bx-radio-circle'></i>Suppliers
</a>
</li>
<?php endif; ?>
<?php if (userCan('masterdata.lookup.view')) : ?>
<li>
<a href="lookup_values.php">
<i class='bx bx-radio-circle'></i>Setup
</a>
</li>
<?php endif; ?>
<?php if (userCan('masterdata.worksheets.view')) : ?>
<li>
<a href="worksheets.php">
<i class='bx bx-radio-circle'></i>Fogli di lavoro
</a>
</li>
<?php endif; ?>
</ul> </ul>
</li> </li>
<?php endif; ?> <?php endif; ?>
<!-- Personale: only Admin / Superuser / employee-hr / manager (not regular User) -->
<?php if ( <?php
Auth::user()->hasRole('Admin') $canSeeProduction =
|| Auth::user()->hasRole('Superuser') userCan('production.line_view.view')
|| Auth::user()->hasRole('employee-hr') || userCan('production.stats.view')
|| Auth::user()->hasRole('manager') || userCan('production.manager.view')
) : ?> || userCan('production.manager_stats.view')
|| userCan('warehouse.dashboard.view');
?>
<?php if ($canSeeProduction) : ?>
<li> <li>
<a href="javascript:;" class="has-arrow"> <a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class="bx bx-group"></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>
<div class="menu-title">Personale</div> <div class="menu-title">Personale</div>
</a> </a>
<ul>
<?php if (userCan('hr.employees.view')) : ?>
<li>
<a href="employees.php">
<i class='bx bx-radio-circle'></i>Dipendenti
</a>
</li>
<?php endif; ?>
<?php if (userCan('hr.departments.view')) : ?>
<li>
<a href="departments.php">
<i class='bx bx-radio-circle'></i>Reparti
</a>
</li>
<?php endif; ?>
<?php if (userCan('hr.job_roles.view')) : ?>
<li>
<a href="job_roles.php">
<i class='bx bx-radio-circle'></i>Mansioni
</a>
</li>
<?php endif; ?>
<?php if (userCan('hr.training_topics.view')) : ?>
<li>
<a href="training_topics.php">
<i class='bx bx-radio-circle'></i>Corsi di Formazione
</a>
</li>
<?php endif; ?>
<?php if (userCan('hr.trainings.view')) : ?>
<li>
<a href="trainings.php">
<i class='bx bx-radio-circle'></i>Storico Formazione
</a>
</li>
<li>
<a href="training_calendar.php">
<i class='bx bx-radio-circle'></i>Calendario Formazione
</a>
</li>
<?php endif; ?>
<?php if (userCan('hr.skills.view')) : ?>
<li>
<a href="skills.php">
<i class='bx bx-radio-circle'></i>Skills
</a>
</li>
<?php endif; ?>
</ul>
</li>
<?php endif; ?>
<?php if (userCan('deadlines.view')) : ?>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon">
<i class="bx bx-calendar-check"></i>
</div>
<div class="menu-title">Scadenzario</div>
</a>
<ul> <ul>
<li> <li>
<a href="employees.php"><i class='bx bx-radio-circle'></i>Dipendenti</a> <a href="scadenzario/index.php">
<i class='bx bx-radio-circle'></i>Lista Scadenze
</a>
</li> </li>
<li> <li>
<a href="departments.php"><i class='bx bx-radio-circle'></i>Reparti</a> <a href="scadenzario/calendar.php">
</li> <i class='bx bx-radio-circle'></i>Calendario
<li> </a>
<a href="job_roles.php"><i class='bx bx-radio-circle'></i>Mansioni</a>
</li>
<li>
<a href="training_topics.php"><i class='bx bx-radio-circle'></i>Corsi di Formazione</a>
</li>
<li>
<a href="trainings.php"><i class='bx bx-radio-circle'></i>Storico Formazione</a>
</li> </li>
</ul> </ul>
</li> </li>
<?php endif; ?> <?php endif; ?>
<!-- Scadenzario: Admin / User / Superuser / employee-hr / manager -->
<?php if (
Auth::user()->hasRole('Admin')
|| Auth::user()->hasRole('User')
|| Auth::user()->hasRole('Superuser')
|| Auth::user()->hasRole('employee-hr')
|| Auth::user()->hasRole('manager')
) : ?>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class="bx bx-calendar-check"></i>
</div>
<div class="menu-title">Scadenzario</div>
</a>
<ul>
<li>
<a href="scadenzario/index.php"><i class='bx bx-radio-circle'></i>Lista Scadenze</a>
</li>
<li>
<a href="scadenzario/calendar.php"><i class='bx bx-radio-circle'></i>Calendario</a>
</li>
</ul>
</li>
<li class="menu-label">Others</li> <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>
<li> <?php if (userCan('users.manage')) : ?>
<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">Admin Menù</li> <li class="menu-label">Admin Menù</li>
<li> <li>
<a href="../" target="_blank"> <a href="../" target="_blank">
<div class="parent-icon"><i class="bx bx-support"></i> <div class="parent-icon">
<i class="bx bx-user-circle"></i>
</div> </div>
<div class="menu-title">User Management</div> <div class="menu-title">User Management</div>
</a> </a>
</li> </li>
<!-- <li> <?php endif; ?>
<a href="template/index.html" target="_blank">
<div class="parent-icon"><i class="bx bx-support"></i>
</div>
<div class="menu-title">Template</div>
</a>
</li>
<li>
<a href="https://codervent.com/rocker/documentation/index.html" target="_blank">
<div class="parent-icon"><i class="bx bx-folder"></i>
</div>
<div class="menu-title">Documentation</div>
</a>
</li> -->
<?php
endif; ?>
</ul> </ul>
<!--end navigation--> <!--end navigation-->
</div> </div>
@@ -0,0 +1,62 @@
<?php
if (!function_exists('userCan')) {
/**
* Check if current user has a Vanguard permission.
* Uses Vanguard native method if available, otherwise falls back to DB check.
*/
function userCan($permissionName)
{
global $kindofrole;
$user = Auth::user();
if (!$user) {
return false;
}
// Vanguard / Laravel-style methods, depending on installed version/customization.
if (method_exists($user, 'hasPermission')) {
return $user->hasPermission($permissionName);
}
if (method_exists($user, 'hasPermissionTo')) {
return $user->hasPermissionTo($permissionName);
}
if (method_exists($user, 'can')) {
return $user->can($permissionName);
}
// Fallback: direct DB check using existing Vanguard tables.
static $permissions = null;
if ($permissions === null) {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->prepare("
SELECT p.name
FROM auth_permissions p
INNER JOIN auth_permission_role pr ON pr.permission_id = p.id
WHERE pr.role_id = ?
");
$stmt->execute([(int)$kindofrole]);
$permissions = $stmt->fetchAll(PDO::FETCH_COLUMN);
}
return in_array($permissionName, $permissions, true);
}
}
if (!function_exists('visibleButtons')) {
/**
* Filter visible buttons.
*/
function visibleButtons(array $buttons)
{
return array_values(array_filter($buttons, function ($button) {
return empty($button['permission']) || userCan($button['permission']);
}));
}
}
+2 -2
View File
@@ -100,7 +100,7 @@
</a> </a>
</li> </li>
<li> <li>
<a class="dropdown-item d-flex align-items-center" href="../users"> <a class="dropdown-item d-flex align-items-center" href="user_settings.php">
<i class="bx bx-user fs-5"></i><span>Utente</span> <i class="bx bx-user fs-5"></i><span>Utente</span>
</a> </a>
</li> </li>
@@ -117,4 +117,4 @@
</div> </div>
</nav> </nav>
</div> </div>
</header> </header>
@@ -19,6 +19,7 @@ if (!$__trWidgetHr) {
return; return;
} }
/* Only the most recent record per (employee, topic) — older history rows ignored. */
$__trRows = $pdo->query(" $__trRows = $pdo->query("
SELECT et.id, SELECT et.id,
et.next_due_date, et.next_due_date,
@@ -27,6 +28,13 @@ $__trRows = $pdo->query("
FROM employee_trainings et FROM employee_trainings et
JOIN training_topics tt ON tt.id = et.training_topic_id JOIN training_topics tt ON tt.id = et.training_topic_id
WHERE et.next_due_date IS NOT NULL WHERE et.next_due_date IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM employee_trainings et2
WHERE et2.employee_id = et.employee_id
AND et2.training_topic_id = et.training_topic_id
AND (et2.completed_date > et.completed_date
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
)
")->fetchAll(PDO::FETCH_ASSOC); ")->fetchAll(PDO::FETCH_ASSOC);
$__expiredCount = 0; $__expiredCount = 0;
+235 -172
View File
@@ -1,4 +1,184 @@
<?php include('include/headscript.php'); ?> <?php include('include/headscript.php'); ?>
<?php
$dashboardSections = [
[
'id' => 'secOperativo',
'title' => 'Operativo',
'subtitle' => 'Azioni principali di produzione e attività in scadenza',
'icon' => '🚀',
'open' => true,
'buttons' => [
[
'label' => 'Programmazione',
'icon' => '🗓️',
'class' => 'btn-programmazione',
'url' => 'produzione_programmazione_drag.php',
'permission' => 'production.programming.view',
],
[
'label' => 'Line View',
'icon' => '⚙️',
'class' => 'btn-status',
'url' => 'production_line_view2.php',
'permission' => 'production.line_view.view',
],
[
'label' => 'Statistiche',
'icon' => '📈',
'class' => 'btn-statistiche',
'url' => 'production_stats.php',
'permission' => 'production.stats.view',
],
[
'label' => 'Manager',
'icon' => '👔',
'class' => 'btn-manager',
'url' => 'manager_produzione.php',
'permission' => 'production.manager.view',
],
[
'label' => 'Manager Stats',
'icon' => '📊',
'class' => 'btn-manager-stats',
'url' => 'manager_stats.php',
'permission' => 'production.manager_stats.view',
],
[
'label' => 'Magazzino',
'icon' => '📦',
'class' => 'btn-magazzino',
'url' => 'warehouse_dashboard.php',
'permission' => 'warehouse.dashboard.view',
],
[
'label' => 'Smart-Alert',
'icon' => '⏰',
'class' => 'btn-scadenziario',
'url' => 'scadenzario/index.php',
'permission' => 'deadlines.view',
],
],
],
[
'id' => 'secAnagrafiche',
'title' => 'Anagrafiche',
'subtitle' => 'Dati di base e setup di produzione',
'icon' => '🗂️',
'open' => false,
'buttons' => [
[
'label' => 'Mescole',
'icon' => '⚗️',
'class' => 'btn-mescole',
'url' => 'mescole.php',
'permission' => 'masterdata.mescole.view',
],
[
'label' => 'Elenco Profili',
'icon' => '🧩',
'class' => 'btn-matrici',
'url' => 'matrici.php',
'permission' => 'masterdata.matrici.view',
],
[
'label' => 'Linee Produzione',
'icon' => '🏭',
'class' => 'btn-linee',
'url' => 'linee.php',
'permission' => 'masterdata.linee.view',
],
[
'label' => 'Imballaggi',
'icon' => '📦',
'class' => 'btn-setup',
'url' => 'packaging_items.php',
'permission' => 'masterdata.packaging.view',
],
[
'label' => 'Suppliers',
'icon' => '🏷️',
'class' => 'btn-setup',
'url' => 'suppliers.php',
'permission' => 'masterdata.suppliers.view',
],
[
'label' => 'Setup',
'icon' => '⚙️',
'class' => 'btn-setup',
'url' => 'lookup_values.php',
'permission' => 'masterdata.lookup.view',
],
[
'label' => 'Fogli di lavoro',
'icon' => '🗒️',
'class' => 'btn-setup',
'url' => 'worksheets.php',
'permission' => 'masterdata.worksheets.view',
],
],
],
[
'id' => 'secServizi',
'title' => 'Servizi',
'subtitle' => 'Status, cause pausa, attrezzature',
'icon' => '🧰',
'open' => false,
'buttons' => [
[
'label' => 'Status',
'icon' => '📋',
'class' => 'btn-setup',
'url' => 'production_status.php',
'permission' => 'services.status.view',
],
[
'label' => 'Cause di Pausa',
'icon' => '🛑',
'class' => 'btn-problem',
'url' => 'production_pause_reasons.php',
'permission' => 'services.pause_reasons.view',
],
[
'label' => 'Attrezzature',
'icon' => '🛠️',
'class' => 'btn-tools',
'url' => 'production_tools.php',
'permission' => 'services.tools.view',
],
],
],
[
'id' => 'secPersonale',
'title' => 'Personale',
'subtitle' => 'Dipendenti, skill',
'icon' => '👥',
'open' => false,
'buttons' => [
[
'label' => 'Employees',
'icon' => '👥',
'class' => 'btn-employees',
'url' => 'employees.php',
'permission' => 'hr.employees.view',
],
[
'label' => 'Departments',
'icon' => '🏢',
'class' => 'btn-departments',
'url' => 'departments.php',
'permission' => 'hr.departments.view',
],
[
'label' => 'Skills',
'icon' => '🧠',
'class' => 'btn-setup',
'url' => 'skills.php',
'permission' => 'hr.skills.view',
],
],
],
];
?>
<!doctype html> <!doctype html>
<html lang="it"> <html lang="it">
@@ -7,7 +187,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" /> <link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?> <?php include('cssinclude.php'); ?>
<title>Dashboard Produzione - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title> <title>Dashboard <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<!-- Bootstrap + jQuery --> <!-- Bootstrap + jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
@@ -359,7 +539,7 @@
<?php include(__DIR__ . '/include/training_widget.php'); ?> <?php include(__DIR__ . '/include/training_widget.php'); ?>
</div> </div>
<h3 class="dashboard-title">Dashboard Produzione</h3> <h3 class="dashboard-title">Dashboard</h3>
<!-- ===== STATISTICHE PRINCIPALI ===== --> <!-- ===== STATISTICHE PRINCIPALI ===== -->
<div class="stats-row"> <div class="stats-row">
@@ -396,188 +576,71 @@
<!-- ===== SEZIONI COLLASSABILI ===== --> <!-- ===== SEZIONI COLLASSABILI ===== -->
<div class="sections-wrap" id="prodAccordion"> <div class="sections-wrap" id="prodAccordion">
<!-- OPERATIVO --> <?php
<div class="section-card"> $hasVisibleSections = false;
<button type="button" class="section-header" data-bs-toggle="collapse" data-bs-target="#secOperativo" aria-expanded="true" aria-controls="secOperativo">
<div class="section-left"> foreach ($dashboardSections as $section):
<div class="section-icon">🚀</div> $buttons = visibleButtons($section['buttons']);
<div style="min-width:0;">
<p class="section-title">Operativo</p> // If no visible buttons are available, do not show the section.
<p class="section-subtitle">Azioni principali di produzione e attività in scadenza</p> 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> <div class="chev"></div>
<div class="chev"></div> </button>
</button>
<div id="secOperativo" class="collapse show" data-bs-parent="#prodAccordion"> <div id="<?= $sectionId ?>"
<div class="section-body"> class="collapse <?= $isOpen ? 'show' : '' ?>"
<div class="dashboard-grid"> data-bs-parent="#prodAccordion">
<button class="dash-btn btn-programmazione" onclick="location.href='produzione_programmazione_drag.php'"> <div class="section-body">
<div class="dash-icon">🗓️</div> <div class="dashboard-grid">
<div>Programmazione</div>
</button>
<?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; ?>
</div>
<button class="dash-btn btn-status" onclick="location.href='production_line_view2.php'">
<div class="dash-icon">⚙️</div>
<div>Line View</div>
</button>
<button class="dash-btn btn-statistiche" onclick="location.href='production_stats.php'">
<div class="dash-icon">📈</div>
<div>Statistiche</div>
</button>
<button class="dash-btn btn-manager" onclick="location.href='manager_produzione.php'">
<div class="dash-icon">👔</div>
<div>Manager</div>
</button>
<button class="dash-btn btn-manager-stats" onclick="location.href='manager_stats.php'">
<div class="dash-icon">📊</div>
<div>Manager Stats</div>
</button>
<button class="dash-btn btn-magazzino" onclick="location.href='warehouse_dashboard.php'">
<div class="dash-icon">📦</div>
<div>Magazzino</div>
</button>
<button class="dash-btn btn-scadenziario" onclick="location.href='scadenzario/index.php'">
<div class="dash-icon"></div>
<div>Scadenziario</div>
</button>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- ANAGRAFICHE --> <?php endforeach; ?>
<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>
<button class="dash-btn btn-matrici" onclick="location.href='matrici.php'"> <?php if (!$hasVisibleSections): ?>
<div class="dash-icon">🧩</div> <div class="section-card">
<div>Elenco Profili</div> <div class="section-body text-center">
</button> Nessuna sezione disponibile per il tuo profilo.
<button class="dash-btn btn-linee" onclick="location.href='linee.php'">
<div class="dash-icon">🏭</div>
<div>Linee Produzione</div>
</button>
<button class="dash-btn btn-setup" onclick="location.href='packaging_items.php'">
<div class="dash-icon">📦</div>
<div>Imballaggi</div>
</button>
<button class="dash-btn btn-setup" onclick="location.href='suppliers.php'">
<div class="dash-icon">🏷️</div>
<div>Suppliers</div>
</button>
<button class="dash-btn btn-setup" onclick="location.href='lookup_values.php'">
<div class="dash-icon">⚙️</div>
<div>Setup</div>
</button>
<button class="dash-btn btn-setup" onclick="location.href='worksheets.php'">
<div class="dash-icon">🗒️</div>
<div>Fogli di lavoro</div>
</button>
</div>
</div> </div>
</div> </div>
</div> <?php endif; ?>
<!-- QUALITÀ / SERVIZI --> </div>
<div class="section-card"> <!-- /sections-wrap -->
<button type="button" class="section-header" data-bs-toggle="collapse" data-bs-target="#secServizi" aria-expanded="false" aria-controls="secServizi">
<div class="section-left">
<div class="section-icon">🧰</div>
<div style="min-width:0;">
<p class="section-title">Servizi</p>
<p class="section-subtitle">Status, cause pausa, attrezzature</p>
</div>
</div>
<div class="chev"></div>
</button>
<div id="secServizi" class="collapse" data-bs-parent="#prodAccordion">
<div class="section-body">
<div class="dashboard-grid">
<button class="dash-btn btn-setup" onclick="location.href='production_status.php'">
<div class="dash-icon">📋</div>
<div>Status</div>
</button>
<button class="dash-btn btn-problem" onclick="location.href='production_pause_reasons.php'">
<div class="dash-icon">🛑</div>
<div>Cause di Pausa</div>
</button>
<button class="dash-btn btn-tools" onclick="location.href='production_tools.php'">
<div class="dash-icon">🛠️</div>
<div>Attrezzature</div>
</button>
</div>
</div>
</div>
</div>
<!-- PERSONALE -->
<div class="section-card">
<button type="button" class="section-header" data-bs-toggle="collapse" data-bs-target="#secPersonale" aria-expanded="false" aria-controls="secPersonale">
<div class="section-left">
<div class="section-icon">👥</div>
<div style="min-width:0;">
<p class="section-title">Personale</p>
<p class="section-subtitle">Dipendenti, skill</p>
</div>
</div>
<div class="chev"></div>
</button>
<div id="secPersonale" class="collapse" data-bs-parent="#prodAccordion">
<div class="section-body">
<div class="dashboard-grid">
<button class="dash-btn btn-employees" onclick="location.href='employees.php'">
<div class="dash-icon">👥</div>
<div>Employees</div>
</button>
<button class="dash-btn btn-departments" onclick="location.href='departments.php'">
<div class="dash-icon">🏢</div>
<div>Departments</div>
</button>
<button class="dash-btn btn-setup" onclick="location.href='skills.php'">
<div class="dash-icon">🧠</div>
<div>Skills</div>
</button>
</div>
</div>
</div>
</div>
</div> <!-- /sections-wrap -->
</div> </div>
</div> </div>
@@ -4,12 +4,19 @@ header('Content-Type: application/json');
require_once(__DIR__ . '/../../class/db-functions.php'); require_once(__DIR__ . '/../../class/db-functions.php');
try { try {
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) { $rawId = $_POST['id'] ?? $_GET['id'] ?? null;
if ($rawId === null || !is_numeric($rawId)) {
echo json_encode(['success' => false, 'message' => 'ID non valido.']); echo json_encode(['success' => false, 'message' => 'ID non valido.']);
exit; exit;
} }
$id = (int)$_GET['id']; $id = (int)$rawId;
// Whether to create the next (recurring) deadline. Absent or '1' => create; '0' => complete only.
$createNext = ($_POST['create_next'] ?? '1') !== '0';
// Whether to carry the attachment links over to the new deadline. Default ON ("default all activate").
$copyAttachments = ($_POST['copy_attachments'] ?? '1') !== '0';
$db = DBHandlerSelect::getInstance(); $db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection(); $pdo = $db->getConnection();
@@ -34,11 +41,13 @@ try {
->execute([$id, $currentUserId]); ->execute([$id, $currentUserId]);
$newId = null; $newId = null;
$newDueDate = null;
// If recurring, create next deadline // If recurring AND the user asked for it, create the next deadline
if ($deadline['recurrence_type'] !== 'once') { if ($deadline['recurrence_type'] !== 'once' && $createNext) {
$dueDate = new DateTime($deadline['due_date']); $dueDate = new DateTime($deadline['due_date']);
$checkDate = $deadline['check_date'] ? new DateTime($deadline['check_date']) : null; $checkDate = $deadline['check_date'] ? new DateTime($deadline['check_date']) : null;
$documentDate = $deadline['document_date'] ? new DateTime($deadline['document_date']) : null;
switch ($deadline['recurrence_type']) { switch ($deadline['recurrence_type']) {
case 'monthly': $interval = new DateInterval('P1M'); break; case 'monthly': $interval = new DateInterval('P1M'); break;
@@ -57,23 +66,25 @@ try {
if ($interval) { if ($interval) {
$dueDate->add($interval); $dueDate->add($interval);
if ($checkDate) $checkDate->add($interval); if ($checkDate) $checkDate->add($interval);
if ($documentDate) $documentDate->add($interval);
$ins = $pdo->prepare(" $ins = $pdo->prepare("
INSERT INTO scad_deadlines INSERT INTO scad_deadlines
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date, (subject_id, function_id, topic, law_regulation, recurrence_type, due_date, check_date,
document_date, notification_days, storage_location, notes, created_by, departments) document_date, notification_days, storage_location, notes, created_by, departments)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"); ");
$ins->execute([ $ins->execute([
$deadline['subject_id'], $deadline['topic'], $deadline['law_regulation'], $deadline['subject_id'], $deadline['function_id'], $deadline['topic'], $deadline['law_regulation'],
$deadline['recurrence_type'], $dueDate->format('Y-m-d'), $deadline['recurrence_type'], $dueDate->format('Y-m-d'),
$checkDate ? $checkDate->format('Y-m-d') : null, $checkDate ? $checkDate->format('Y-m-d') : null,
$deadline['document_date'], $documentDate ? $documentDate->format('Y-m-d') : null,
$deadline['notification_days'], $deadline['storage_location'], $deadline['notification_days'], $deadline['storage_location'],
$deadline['notes'], $deadline['created_by'], $deadline['departments'] $deadline['notes'], $deadline['created_by'], $deadline['departments']
]); ]);
$newId = $pdo->lastInsertId(); $newId = $pdo->lastInsertId();
$newDueDate = $dueDate;
// Copy employee assignments // Copy employee assignments
$empStmt = $pdo->prepare("SELECT employee_id FROM scad_deadline_employee WHERE deadline_id = ?"); $empStmt = $pdo->prepare("SELECT employee_id FROM scad_deadline_employee WHERE deadline_id = ?");
@@ -87,6 +98,31 @@ try {
} }
} }
// Carry forward ALL attachment links from the source deadline (shared physical file, same stored_name).
// Individual links can later be removed on the new deadline without deleting the file.
if ($copyAttachments) {
$attSel = $pdo->prepare("
SELECT original_name, stored_name, mime_type, size
FROM scad_deadline_attachments
WHERE deadline_id = ?
");
$attSel->execute([$id]);
$attRows = $attSel->fetchAll(PDO::FETCH_ASSOC);
if ($attRows) {
$attIns = $pdo->prepare("
INSERT INTO scad_deadline_attachments
(deadline_id, original_name, stored_name, mime_type, size, uploaded_by)
VALUES (?, ?, ?, ?, ?, ?)
");
$attHist = $pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'attachment_linked', ?)");
foreach ($attRows as $a) {
$attIns->execute([$newId, $a['original_name'], $a['stored_name'], $a['mime_type'], $a['size'], $currentUserId]);
$attHist->execute([$newId, $currentUserId, $a['original_name']]);
}
}
}
// History for new // History for new
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'created', ?)") $pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'created', ?)")
->execute([$newId, $currentUserId, 'Creata automaticamente dalla scadenza #' . $id]); ->execute([$newId, $currentUserId, 'Creata automaticamente dalla scadenza #' . $id]);
@@ -97,7 +133,7 @@ try {
$msg = 'Scadenza completata con successo.'; $msg = 'Scadenza completata con successo.';
if ($newId) { if ($newId) {
$msg .= ' Nuova scadenza creata con data ' . $dueDate->format('d/m/Y') . '.'; $msg .= ' Nuova scadenza creata con data ' . $newDueDate->format('d/m/Y') . '.';
} }
echo json_encode(['success' => true, 'message' => $msg, 'new_id' => $newId]); echo json_encode(['success' => true, 'message' => $msg, 'new_id' => $newId]);
@@ -23,20 +23,32 @@ try {
exit; exit;
} }
// Delete file // Remove this link (DB record) first
$filePath = __DIR__ . '/../attachments/' . $att['stored_name'];
if (file_exists($filePath)) {
unlink($filePath);
}
// Delete DB record
$pdo->prepare("DELETE FROM scad_deadline_attachments WHERE id = ?")->execute([$id]); $pdo->prepare("DELETE FROM scad_deadline_attachments WHERE id = ?")->execute([$id]);
// History // The same physical file may be shared with other deadlines (carried forward on completion).
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'attachment_removed', ?)") // Only unlink it when no other link references the same stored file.
->execute([$att['deadline_id'], $currentUserId, $att['original_name']]); $refStmt = $pdo->prepare("SELECT COUNT(*) FROM scad_deadline_attachments WHERE stored_name = ?");
$refStmt->execute([$att['stored_name']]);
$stillReferenced = (int)$refStmt->fetchColumn() > 0;
echo json_encode(['success' => true, 'message' => 'Allegato eliminato.']); if ($stillReferenced) {
$action = 'attachment_unlinked';
$message = 'Collegamento rimosso. Il file è conservato (usato da un\'altra scadenza).';
} else {
$filePath = __DIR__ . '/../attachments/' . $att['stored_name'];
if (file_exists($filePath)) {
unlink($filePath);
}
$action = 'attachment_removed';
$message = 'Allegato eliminato.';
}
// History
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, ?, ?)")
->execute([$att['deadline_id'], $currentUserId, $action, $att['original_name']]);
echo json_encode(['success' => true, 'message' => $message]);
} catch (Exception $e) { } catch (Exception $e) {
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]); echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
@@ -13,10 +13,29 @@ try {
$db = DBHandlerSelect::getInstance(); $db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection(); $pdo = $db->getConnection();
// Collect the physical files referenced by this deadline before the FK cascade removes its links
$attStmt = $pdo->prepare("SELECT DISTINCT stored_name FROM scad_deadline_attachments WHERE deadline_id = ?");
$attStmt->execute([$id]);
$storedNames = $attStmt->fetchAll(PDO::FETCH_COLUMN);
// Deleting the deadline cascades to its attachment/employee/history rows (FK ON DELETE CASCADE)
$stmt = $pdo->prepare("DELETE FROM scad_deadlines WHERE id = ?"); $stmt = $pdo->prepare("DELETE FROM scad_deadlines WHERE id = ?");
$stmt->execute([$id]); $stmt->execute([$id]);
if ($stmt->rowCount() > 0) { if ($stmt->rowCount() > 0) {
// Unlink physical files no longer referenced by any other deadline (shared-file safe)
if (!empty($storedNames)) {
$refStmt = $pdo->prepare("SELECT COUNT(*) FROM scad_deadline_attachments WHERE stored_name = ?");
foreach ($storedNames as $storedName) {
$refStmt->execute([$storedName]);
if ((int)$refStmt->fetchColumn() === 0) {
$filePath = __DIR__ . '/../attachments/' . $storedName;
if (file_exists($filePath)) {
unlink($filePath);
}
}
}
}
echo json_encode(['success' => true, 'message' => 'Scadenza eliminata con successo.']); echo json_encode(['success' => true, 'message' => 'Scadenza eliminata con successo.']);
} else { } else {
echo json_encode(['success' => false, 'message' => 'Scadenza non trovata.']); echo json_encode(['success' => false, 'message' => 'Scadenza non trovata.']);
@@ -9,6 +9,7 @@ try {
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : null; $id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : null;
$subject_id = isset($_POST['subject_id']) && is_numeric($_POST['subject_id']) && (int)$_POST['subject_id'] > 0 ? (int)$_POST['subject_id'] : null; $subject_id = isset($_POST['subject_id']) && is_numeric($_POST['subject_id']) && (int)$_POST['subject_id'] > 0 ? (int)$_POST['subject_id'] : null;
$function_id = isset($_POST['function_id']) && is_numeric($_POST['function_id']) && (int)$_POST['function_id'] > 0 ? (int)$_POST['function_id'] : null;
$topic = trim($_POST['topic'] ?? ''); $topic = trim($_POST['topic'] ?? '');
$law_regulation = trim($_POST['law_regulation'] ?? '') ?: null; $law_regulation = trim($_POST['law_regulation'] ?? '') ?: null;
$recurrence_type = $_POST['recurrence_type'] ?? 'once'; $recurrence_type = $_POST['recurrence_type'] ?? 'once';
@@ -51,16 +52,26 @@ try {
if ($id) { if ($id) {
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
UPDATE scad_deadlines SET UPDATE scad_deadlines SET
subject_id = ?, topic = ?, law_regulation = ?, recurrence_type = ?, subject_id = ?, function_id = ?, topic = ?, law_regulation = ?, recurrence_type = ?,
due_date = ?, check_date = ?, document_date = ?, notification_days = ?, due_date = ?, check_date = ?, document_date = ?, notification_days = ?,
storage_location = ?, notes = ?, departments = ? storage_location = ?, notes = ?, departments = ?
WHERE id = ? WHERE id = ?
"); ");
$stmt->execute([ $stmt->execute([
$subject_id, $topic, $law_regulation, $recurrence_type, $subject_id,
$due_date, $check_date, $document_date, $notification_days, $function_id,
$storage_location, $notes, $departmentsStr, $id $topic,
$law_regulation,
$recurrence_type,
$due_date,
$check_date,
$document_date,
$notification_days,
$storage_location,
$notes,
$departmentsStr,
$id
]); ]);
// Re-link employees // Re-link employees
@@ -75,14 +86,24 @@ try {
// INSERT // INSERT
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO scad_deadlines INSERT INTO scad_deadlines
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date, (subject_id, function_id, topic, law_regulation, recurrence_type, due_date, check_date,
document_date, notification_days, storage_location, notes, created_by, departments) document_date, notification_days, storage_location, notes, created_by, departments)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"); ");
$stmt->execute([ $stmt->execute([
$subject_id, $topic, $law_regulation, $recurrence_type, $subject_id,
$due_date, $check_date, $document_date, $notification_days, $function_id,
$storage_location, $notes, $currentUserId, $departmentsStr $topic,
$law_regulation,
$recurrence_type,
$due_date,
$check_date,
$document_date,
$notification_days,
$storage_location,
$notes,
$currentUserId,
$departmentsStr
]); ]);
$deadlineId = $pdo->lastInsertId(); $deadlineId = $pdo->lastInsertId();
@@ -107,7 +128,6 @@ try {
'message' => $id ? 'Scadenza aggiornata con successo.' : 'Scadenza creata con successo.', 'message' => $id ? 'Scadenza aggiornata con successo.' : 'Scadenza creata con successo.',
'id' => $deadlineId 'id' => $deadlineId
]); ]);
} catch (Exception $e) { } catch (Exception $e) {
if (isset($pdo) && $pdo->inTransaction()) { if (isset($pdo) && $pdo->inTransaction()) {
$pdo->rollBack(); $pdo->rollBack();
@@ -25,6 +25,17 @@ $pdo = $db->getConnection();
$today = date('Y-m-d'); $today = date('Y-m-d');
$appUrl = rtrim($_ENV['APP_URL'] ?? 'http://localhost:8001', '/'); $appUrl = rtrim($_ENV['APP_URL'] ?? 'http://localhost:8001', '/');
// Manager email for Cc — taken from MANAGER_USER_ID → auth_users.email
$managerCcEmail = null;
if (!empty($_ENV['MANAGER_USER_ID']) && is_numeric($_ENV['MANAGER_USER_ID'])) {
$mgrStmt = $pdo->prepare("SELECT email FROM auth_users WHERE id = ?");
$mgrStmt->execute([(int)$_ENV['MANAGER_USER_ID']]);
$mgrEmail = $mgrStmt->fetchColumn();
if (!empty($mgrEmail)) {
$managerCcEmail = $mgrEmail;
}
}
$sent = 0; $sent = 0;
$skipped = 0; $skipped = 0;
$errors = 0; $errors = 0;
@@ -143,6 +154,11 @@ foreach ($deadlines as $dl) {
); );
$mail->addAddress($emp['email'], trim($emp['first_name'] . ' ' . $emp['last_name'])); $mail->addAddress($emp['email'], trim($emp['first_name'] . ' ' . $emp['last_name']));
// Cc the manager (unless they are the direct recipient)
if ($managerCcEmail && strcasecmp($managerCcEmail, $emp['email']) !== 0) {
$mail->addCC($managerCcEmail);
}
$detailUrl = $appUrl . '/userarea/scadenzario/detail.php?id=' . $dl['id']; $detailUrl = $appUrl . '/userarea/scadenzario/detail.php?id=' . $dl['id'];
$topicText = (!empty($dl['subject_name']) ? $dl['subject_name'] . ' — ' : '') . $dl['topic']; $topicText = (!empty($dl['subject_name']) ? $dl['subject_name'] . ' — ' : '') . $dl['topic'];
+98 -28
View File
@@ -66,9 +66,9 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
} }
$recurrenceLabels = ['once' => 'Una tantum', 'monthly' => 'Mensile', 'quarterly' => 'Trimestrale', 'semiannual' => 'Semestrale', 'annual' => 'Annuale', 'biennial' => 'Biennale', 'triennial' => 'Triennale', 'quadriennial' => 'Quadriennale', 'quinquennial' => 'Quinquennale', 'decennial' => 'Decennale', 'quindecennial' => 'Quindicennale']; $recurrenceLabels = ['once' => 'Una tantum', 'monthly' => 'Mensile', 'quarterly' => 'Trimestrale', 'semiannual' => 'Semestrale', 'annual' => 'Annuale', 'biennial' => 'Biennale', 'triennial' => 'Triennale', 'quadriennial' => 'Quadriennale', 'quinquennial' => 'Quinquennale', 'decennial' => 'Decennale', 'quindecennial' => 'Quindicennale'];
$actionLabels = ['created' => 'Creata', 'updated' => 'Modificata', 'completed' => 'Completata', 'attachment_added' => 'Allegato aggiunto', 'attachment_removed' => 'Allegato rimosso', 'notification_sent' => 'Notifica inviata']; $actionLabels = ['created' => 'Creata', 'updated' => 'Modificata', 'completed' => 'Completata', 'attachment_added' => 'Allegato aggiunto', 'attachment_removed' => 'Allegato rimosso', 'attachment_linked' => 'Allegato collegato', 'attachment_unlinked' => 'Collegamento rimosso', 'notification_sent' => 'Notifica inviata'];
$actionColors = ['created' => '#198754', 'updated' => '#5a8fd8', 'completed' => '#6f42c1', 'attachment_added' => '#e8930c', 'attachment_removed' => '#e8930c', 'notification_sent' => '#adb5bd']; $actionColors = ['created' => '#198754', 'updated' => '#5a8fd8', 'completed' => '#6f42c1', 'attachment_added' => '#e8930c', 'attachment_removed' => '#e8930c', 'attachment_linked' => '#0dcaf0', 'attachment_unlinked' => '#adb5bd', 'notification_sent' => '#adb5bd'];
$actionIcons = ['created' => 'fa-plus', 'updated' => 'fa-pen', 'completed' => 'fa-check', 'attachment_added' => 'fa-paperclip', 'attachment_removed' => 'fa-trash', 'notification_sent' => 'fa-bell']; $actionIcons = ['created' => 'fa-plus', 'updated' => 'fa-pen', 'completed' => 'fa-check', 'attachment_added' => 'fa-paperclip', 'attachment_removed' => 'fa-trash', 'attachment_linked' => 'fa-link', 'attachment_unlinked' => 'fa-link-slash', 'notification_sent' => 'fa-bell'];
} }
} }
?> ?>
@@ -85,6 +85,14 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
<base href="<?= $baseHref ?>"> <base href="<?= $baseHref ?>">
<?php include('../cssinclude.php'); ?> <?php include('../cssinclude.php'); ?>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/i18n/it.js"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/it.js"></script>
<?php include __DIR__ . '/include/deadline_modal_css.php'; ?>
<title><?= $deadline ? htmlspecialchars($deadline['topic'], ENT_QUOTES, 'UTF-8') . ' — ' : '' ?>Scadenzario</title> <title><?= $deadline ? htmlspecialchars($deadline['topic'], ENT_QUOTES, 'UTF-8') . ' — ' : '' ?>Scadenzario</title>
<script> <script>
if (window.innerWidth > 1024) document.addEventListener('DOMContentLoaded', function() { if (window.innerWidth > 1024) document.addEventListener('DOMContentLoaded', function() {
@@ -755,52 +763,114 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
</div> </div>
<?php include('../include/footer.php'); ?> <?php include('../include/footer.php'); ?>
</div> </div>
<?php if ($deadline && !$isCompleted): ?>
<?php require __DIR__ . '/include/deadline_form_data.php'; ?>
<?php include __DIR__ . '/include/deadline_modal.php'; ?>
<?php endif; ?>
<?php include('../jsinclude.php'); ?> <?php include('../jsinclude.php'); ?>
<?php if ($deadline && !$isCompleted): ?> <?php if ($deadline && !$isCompleted): ?>
<script> <script>
$(document).ready(function() { $(document).ready(function() {
// Used by the shared modal JS to auto-open edit on "#edit"
window.SCAD_DETAIL_ID = <?= (int)$deadline['id'] ?>;
$('#btnModifica').on('click', function() { $('#btnModifica').on('click', function() {
window.location.href = 'scadenzario/index.php?edit=<?= (int)$deadline['id'] ?>'; window.openDeadlineEdit(<?= (int)$deadline['id'] ?>);
}); });
function detailSubmitComplete(createNext, copyAttachments) {
var fd = new FormData();
fd.append('id', '<?= (int)$deadline['id'] ?>');
fd.append('create_next', createNext ? '1' : '0');
fd.append('copy_attachments', copyAttachments ? '1' : '0');
fetch('scadenzario/ajax/complete_deadline.php', {
method: 'POST',
body: fd
})
.then(function(r) {
return r.json();
})
.then(function(data) {
if (data.success) {
Swal.fire({
icon: 'success',
title: 'Completata',
text: data.message,
timer: 1800,
showConfirmButton: false
})
.then(function() {
if (data.new_id) {
window.location.href = 'scadenzario/detail.php?id=' + data.new_id + '#edit';
} else {
window.location.href = 'scadenzario/index.php';
}
});
} else {
Swal.fire('Errore', data.message, 'error');
}
})
.catch(function() {
Swal.fire('Errore', 'Errore di connessione.', 'error');
});
}
$('#btnCompleta').on('click', function() { $('#btnCompleta').on('click', function() {
var recurrence = <?= json_encode($deadline['recurrence_type'] ?? 'once') ?>;
var attCount = <?= count($attachments) ?>;
if (recurrence === 'once') {
Swal.fire({
title: 'Completare la scadenza?',
text: 'La scadenza verrà contrassegnata come completata.',
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#198754',
cancelButtonText: 'Annulla',
confirmButtonText: 'Completa',
reverseButtons: true
}).then(function(result) {
if (result.isConfirmed) {
detailSubmitComplete(false, false);
}
});
return;
}
var attCheckbox = attCount > 0 ?
'<div class="form-check d-flex align-items-center justify-content-center gap-2 mt-3">' +
'<input class="form-check-input" type="checkbox" id="swCopyAtt" checked>' +
'<label class="form-check-label" for="swCopyAtt">Copia gli allegati (' + attCount + ') sulla nuova scadenza</label>' +
'</div>' :
'';
Swal.fire({ Swal.fire({
title: 'Completare la scadenza?', title: 'Completare la scadenza?',
html: 'Vuoi creare automaticamente la prossima scadenza ricorrente?' + attCheckbox,
icon: 'question', icon: 'question',
showCancelButton: true, showCancelButton: true,
showDenyButton: true,
confirmButtonColor: '#198754', confirmButtonColor: '#198754',
denyButtonColor: '#6c757d',
confirmButtonText: 'Completa e crea la prossima',
denyButtonText: 'Completa senza nuova',
cancelButtonText: 'Annulla', cancelButtonText: 'Annulla',
confirmButtonText: 'Completa' reverseButtons: true
}).then(function(result) { }).then(function(result) {
if (result.isConfirmed) { if (result.isConfirmed) {
fetch('scadenzario/ajax/complete_deadline.php?id=<?= (int)$deadline['id'] ?>') var copy = attCount > 0 ? document.getElementById('swCopyAtt').checked : false;
.then(function(r) { detailSubmitComplete(true, copy);
return r.json(); } else if (result.isDenied) {
}) detailSubmitComplete(false, false);
.then(function(data) {
if (data.success) {
Swal.fire({
icon: 'success',
title: 'Completata',
text: data.message,
timer: 2500,
showConfirmButton: false
})
.then(function() {
window.location.href = 'scadenzario/index.php';
});
} else {
Swal.fire('Errore', data.message, 'error');
}
})
.catch(function() {
Swal.fire('Errore', 'Errore di connessione.', 'error');
});
} }
}); });
}); });
}); });
</script> </script>
<?php include __DIR__ . '/include/deadline_modal_js.php'; ?>
<?php endif; ?> <?php endif; ?>
</body> </body>
@@ -0,0 +1,34 @@
<?php
require_once(__DIR__ . '/../../ajax/auth_check.php');
header('Content-Type: application/json');
require_once(__DIR__ . '/../../../class/db-functions.php');
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : 0;
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
exit;
}
$stmt = $pdo->prepare("SELECT COUNT(*) FROM scad_deadlines WHERE function_id = ?");
$stmt->execute([$id]);
$inUse = (int)$stmt->fetchColumn();
if ($inUse > 0) {
echo json_encode([
'success' => false,
'message' => "Impossibile eliminare: la funzione è utilizzata in $inUse scadenz" . ($inUse === 1 ? 'a' : 'e') . '.',
]);
exit;
}
$pdo->prepare("DELETE FROM scad_functions WHERE id = ?")->execute([$id]);
echo json_encode(['success' => true, 'message' => 'Funzione eliminata.']);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
}
@@ -0,0 +1,63 @@
<?php
require_once(__DIR__ . '/../../ajax/auth_check.php');
header('Content-Type: application/json');
require_once(__DIR__ . '/../../../class/db-functions.php');
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : null;
$name = trim($_POST['name'] ?? '');
$description = trim($_POST['description'] ?? '') ?: null;
if ($name === '') {
echo json_encode(['success' => false, 'message' => 'Il nome è obbligatorio.']);
exit;
}
if (mb_strlen($name) > 255) {
echo json_encode(['success' => false, 'message' => 'Il nome supera 255 caratteri.']);
exit;
}
if ($id) {
$stmt = $pdo->prepare("SELECT id FROM scad_functions WHERE name = ? AND id <> ?");
$stmt->execute([$name, $id]);
} else {
$stmt = $pdo->prepare("SELECT id FROM scad_functions WHERE name = ?");
$stmt->execute([$name]);
}
if ($stmt->fetch()) {
echo json_encode(['success' => false, 'message' => 'Esiste già una funzione con questo nome.']);
exit;
}
if ($id) {
$stmt = $pdo->prepare("
UPDATE scad_functions
SET name = ?, description = ?
WHERE id = ?
");
$stmt->execute([$name, $description, $id]);
$savedId = $id;
} else {
$stmt = $pdo->prepare("
INSERT INTO scad_functions (name, description, status)
VALUES (?, ?, 'active')
");
$stmt->execute([$name, $description]);
$savedId = (int)$pdo->lastInsertId();
}
echo json_encode([
'success' => true,
'message' => $id ? 'Funzione aggiornata.' : 'Funzione creata.',
'id' => $savedId,
'name' => $name,
'description' => $description,
]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
}
@@ -0,0 +1,464 @@
<?php include('../../include/headscript.php'); ?>
<?php
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$functions = $pdo->query("
SELECT f.*,
(SELECT COUNT(*) FROM scad_deadlines d WHERE d.function_id = f.id) AS deadline_count,
(SELECT COUNT(*) FROM scad_deadlines d WHERE d.function_id = f.id AND d.status <> 'completed') AS open_count
FROM scad_functions f
ORDER BY f.name ASC
")->fetchAll(PDO::FETCH_ASSOC);
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<?php
$scriptDir = dirname($_SERVER['SCRIPT_NAME']);
$baseHref = dirname(dirname($scriptDir)) . '/';
?>
<base href="<?= $baseHref ?>">
<?php include('../../cssinclude.php'); ?>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<title>Scadenzario - Funzioni</title>
<script>
if (window.innerWidth > 1024) {
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('appWrapper').classList.add('toggled');
});
}
</script>
<style>
:root {
--scad-primary: #5a8fd8;
--scad-primary-hover: #4578c0;
--scad-heading: #2c3e6b;
--scad-card-bg: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%);
--scad-card-border: #dde4f0;
}
.scad-card {
border: none;
border-radius: 0.75rem;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
overflow: hidden;
}
.scad-card .card-header {
background: var(--scad-card-bg);
border-bottom: 1px solid var(--scad-card-border);
padding: 1rem 1.25rem;
}
.scad-card .card-header h5 {
font-weight: 700;
color: var(--scad-heading);
margin: 0;
font-size: 1.1rem;
}
.scad-card .card-body {
padding: 1.25rem;
}
.btn-scad-primary {
background: var(--scad-primary);
border: none;
color: #fff;
font-weight: 600;
font-size: 0.85rem;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
}
.btn-scad-primary:hover {
background: var(--scad-primary-hover);
color: #fff;
}
.btn-scad-outline {
background: transparent;
border: 1.5px solid var(--scad-primary);
color: var(--scad-primary);
font-weight: 600;
font-size: 0.85rem;
padding: 0.45rem 1rem;
border-radius: 0.5rem;
}
.btn-scad-outline:hover {
background: var(--scad-primary);
color: #fff;
}
.btn-action {
width: 32px;
height: 32px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 0.4rem;
font-size: 0.85rem;
}
.btn-action-edit {
background: rgba(90, 143, 216, 0.12);
color: var(--scad-primary);
}
.btn-action-edit:hover {
background: var(--scad-primary);
color: #fff;
}
.btn-action-delete {
background: rgba(220, 53, 69, 0.12);
color: #dc3545;
}
.btn-action-delete:hover {
background: #dc3545;
color: #fff;
}
.function-card {
background: #fff;
border: 1px solid var(--scad-card-border);
border-radius: 0.6rem;
padding: 0.85rem 0.95rem;
margin-bottom: 0.6rem;
}
.function-card .fc-name {
font-weight: 700;
color: var(--scad-heading);
font-size: 0.95rem;
}
.function-card .fc-stats {
display: flex;
gap: 0.75rem;
font-size: 0.8rem;
color: #6c757d;
margin: 0.5rem 0;
}
.function-card .fc-stats strong {
color: var(--scad-heading);
}
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: #6c757d;
}
.empty-state i {
font-size: 3rem;
opacity: 0.3;
margin-bottom: 1rem;
}
@media (max-width: 575.98px) {
.scad-card .card-header {
flex-direction: column;
gap: 0.75rem;
align-items: flex-start !important;
}
.header-actions {
width: 100%;
}
.header-actions .btn {
width: 100%;
justify-content: center;
}
}
</style>
</head>
<body>
<div class="wrapper" id="appWrapper">
<?php include('../../include/navbar.php'); ?>
<?php include('../../include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb" style="background:transparent;padding:0;margin:0;font-size:0.85rem">
<li class="breadcrumb-item"><a href="scadenzario/index.php">Scadenzario</a></li>
<li class="breadcrumb-item active" aria-current="page">Funzioni</li>
</ol>
</nav>
<div class="card scad-card">
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
<h5><i class="fa-solid fa-briefcase me-2"></i>Funzioni</h5>
<div class="header-actions d-flex gap-2 flex-wrap">
<a href="scadenzario/index.php" class="btn btn-scad-outline d-inline-flex align-items-center gap-2">
<i class="fa-solid fa-arrow-left"></i><span>Scadenzario</span>
</a>
<button class="btn btn-scad-primary d-inline-flex align-items-center gap-2" id="btnAddFunction">
<i class="fa-solid fa-plus"></i><span>Nuova Funzione</span>
</button>
</div>
</div>
<div class="card-body">
<?php if (count($functions) === 0): ?>
<div class="empty-state">
<i class="fa-solid fa-briefcase"></i>
<p>Nessuna funzione definita.<br>Clicca <strong>"Nuova Funzione"</strong> per aggiungere la prima.</p>
</div>
<?php else: ?>
<div id="functionsList">
<div class="d-md-none">
<?php foreach ($functions as $f): ?>
<div class="function-card"
data-id="<?= (int)$f['id'] ?>"
data-name="<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>"
data-description="<?= htmlspecialchars($f['description'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-status="<?= htmlspecialchars($f['status'], ENT_QUOTES, 'UTF-8') ?>"
data-in-use="<?= (int)$f['deadline_count'] ?>">
<div class="fc-name"><?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?></div>
<?php if (!empty($f['description'])): ?>
<div class="text-muted small mt-1"><?= htmlspecialchars($f['description'], ENT_QUOTES, 'UTF-8') ?></div>
<?php endif; ?>
<div class="fc-stats">
<span>Scadenze: <strong><?= (int)$f['deadline_count'] ?></strong></span>
<span>Aperte: <strong><?= (int)$f['open_count'] ?></strong></span>
</div>
<div class="d-flex gap-1 justify-content-end">
<button class="btn-action btn-action-edit btn-edit" title="Modifica">
<i class="fa-solid fa-pen"></i>
</button>
<button class="btn-action btn-action-delete btn-delete" title="Elimina">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="d-none d-md-block">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th>Nome</th>
<th>Descrizione</th>
<th class="text-center" style="width:120px">Scadenze</th>
<th class="text-center" style="width:120px">Aperte</th>
<th class="text-center" style="width:120px">Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($functions as $f): ?>
<tr data-id="<?= (int)$f['id'] ?>"
data-name="<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>"
data-description="<?= htmlspecialchars($f['description'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-status="<?= htmlspecialchars($f['status'], ENT_QUOTES, 'UTF-8') ?>"
data-in-use="<?= (int)$f['deadline_count'] ?>">
<td class="fw-semibold" style="color:var(--scad-heading)">
<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>
</td>
<td class="text-muted">
<?= htmlspecialchars($f['description'] ?? '—', ENT_QUOTES, 'UTF-8') ?>
</td>
<td class="text-center"><?= (int)$f['deadline_count'] ?></td>
<td class="text-center"><?= (int)$f['open_count'] ?></td>
<td class="text-center">
<div class="d-inline-flex gap-1">
<button class="btn-action btn-action-edit btn-edit" title="Modifica">
<i class="fa-solid fa-pen"></i>
</button>
<button class="btn-action btn-action-delete btn-delete" title="Elimina">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php include('../../include/footer.php'); ?>
</div>
<div class="modal fade" id="functionModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="functionModalTitle">Nuova Funzione</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<form id="functionForm">
<div class="modal-body">
<input type="hidden" id="functionId" name="id" value="">
<div class="mb-3">
<label for="functionName" class="form-label fw-semibold">Nome <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="functionName" name="name" maxlength="255" required>
</div>
<div class="mb-3">
<label for="functionDescription" class="form-label fw-semibold">Descrizione</label>
<textarea class="form-control" id="functionDescription" name="description" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-scad-primary">Salva</button>
</div>
</form>
</div>
</div>
</div>
<?php include('../../jsinclude.php'); ?>
<script>
$(function() {
function openModal(data) {
const isEdit = !!data;
$('#functionModalTitle').text(isEdit ? 'Modifica Funzione' : 'Nuova Funzione');
$('#functionId').val(isEdit ? data.id : '');
$('#functionName').val(isEdit ? data.name : '');
$('#functionDescription').val(isEdit ? data.description : '');
new bootstrap.Modal('#functionModal').show();
}
$('#btnAddFunction').on('click', function() {
openModal(null);
});
$('#functionsList').on('click', '.btn-edit', function() {
const $row = $(this).closest('[data-id]');
openModal({
id: $row.data('id'),
name: $row.data('name'),
description: $row.data('description')
});
});
$('#functionsList').on('click', '.btn-delete', function() {
const $row = $(this).closest('[data-id]');
const inUse = parseInt($row.data('in-use') || 0, 10);
const name = $row.data('name');
if (inUse > 0) {
Swal.fire({
icon: 'warning',
title: 'Impossibile eliminare',
text: `La funzione "${name}" è utilizzata in ${inUse} scadenz${inUse === 1 ? 'a' : 'e'}.`
});
return;
}
Swal.fire({
title: `Eliminare "${name}"?`,
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Elimina',
cancelButtonText: 'Annulla',
confirmButtonColor: '#dc3545'
}).then(function(result) {
if (!result.isConfirmed) return;
$.post('scadenzario/functions/ajax/delete_function.php', {
id: $row.data('id')
})
.done(function(res) {
if (res.success) {
location.reload();
} else {
Swal.fire({
icon: 'error',
title: 'Errore',
text: res.message
});
}
})
.fail(function() {
Swal.fire({
icon: 'error',
title: 'Errore di rete'
});
});
});
});
$('#functionForm').on('submit', function(e) {
e.preventDefault();
const payload = {
id: $('#functionId').val(),
name: $('#functionName').val().trim(),
description: $('#functionDescription').val().trim()
};
if (!payload.name) {
Swal.fire({
icon: 'warning',
title: 'Nome obbligatorio'
});
return;
}
$.post('scadenzario/functions/ajax/save_function.php', payload)
.done(function(res) {
if (res.success) {
location.reload();
} else {
Swal.fire({
icon: 'error',
title: 'Errore',
text: res.message
});
}
})
.fail(function() {
Swal.fire({
icon: 'error',
title: 'Errore di rete'
});
});
});
});
</script>
</body>
</html>
@@ -0,0 +1,41 @@
<?php
/**
* Shared data for the deadline modal form (used by index.php and detail.php).
* Populates $employees, $departments, $subjects. Safe to include more than once.
*/
if (!isset($pdo) || !$pdo) {
$pdo = DBHandlerSelect::getInstance()->getConnection();
}
if (!isset($employees)) {
$employees = $pdo->query("
SELECT e.id, e.first_name, e.last_name, e.department_id, dep.name AS department_name
FROM employees e
LEFT JOIN departments dep ON dep.id = e.department_id
WHERE e.status = 'active'
ORDER BY e.first_name, e.last_name
")->fetchAll(PDO::FETCH_ASSOC);
}
if (!isset($departments)) {
$departments = $pdo->query("
SELECT id, name, code, color
FROM departments
WHERE is_active = 1
ORDER BY sort_order ASC, name ASC
")->fetchAll(PDO::FETCH_ASSOC);
}
if (!isset($subjects)) {
$subjects = $pdo->query("SELECT id, name, color FROM scad_subjects ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
}
if (!isset($functions)) {
$functions = $pdo->query("
SELECT id, name
FROM scad_functions
WHERE status = 'active'
ORDER BY name ASC
")->fetchAll(PDO::FETCH_ASSOC);
}
@@ -0,0 +1,162 @@
<?php
/**
* Shared "Nuova/Modifica Scadenza" modal markup (used by index.php and detail.php).
* Requires $subjects, $departments, $employees in scope (see deadline_form_data.php).
* The accompanying JS lives in deadline_modal_js.php.
*/
?>
<!-- Deadline Modal -->
<div class="modal fade" id="deadlineModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl modal-fullscreen-sm-down">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalTitle">Nuova Scadenza</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<form id="deadlineForm">
<div class="modal-body">
<input type="hidden" id="dlId" name="id" value="">
<!-- Group 1: Informazioni principali -->
<div class="form-section-title">Informazioni principali</div>
<div class="row g-3 mb-4">
<div class="col-12 col-md-6">
<label for="dlSubject" class="form-label fw-semibold">Argomento</label>
<div class="d-flex gap-2">
<select class="form-select" id="dlSubject" name="subject_id" style="flex:1">
<option value=""> Nessuno </option>
<?php foreach ($subjects as $s): ?>
<option value="<?= (int)$s['id'] ?>" data-color="<?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
<a href="scadenzario/subjects/index.php" target="_blank" class="btn btn-scad-outline" title="Gestisci argomenti">
<i class="fa-solid fa-gear"></i>
</a>
</div>
</div>
<div class="col-12 col-md-6">
<label for="dlFunction" class="form-label fw-semibold">Funzione</label>
<div class="d-flex gap-2">
<select class="form-select" id="dlFunction" name="function_id" style="flex:1">
<option value=""> Nessuna </option>
<?php foreach ($functions as $fn): ?>
<option value="<?= (int)$fn['id'] ?>">
<?= htmlspecialchars($fn['name'], ENT_QUOTES, 'UTF-8') ?>
</option>
<?php endforeach; ?>
</select>
<a href="scadenzario/functions/index.php" target="_blank" class="btn btn-scad-outline" title="Gestisci funzioni">
<i class="fa-solid fa-gear"></i>
</a>
</div>
</div>
<div class="col-12 col-md-6">
<label for="dlLaw" class="form-label fw-semibold">Legge / Articolo</label>
<input type="text" class="form-control" id="dlLaw" name="law_regulation" maxlength="500" placeholder="es. D.Lgs. 81/2008, D.M. 10.03.1998...">
</div>
<div class="col-12">
<label for="dlTopic" class="form-label fw-semibold">Dettaglio <span class="text-danger">*</span></label>
<textarea class="form-control" id="dlTopic" name="topic" required maxlength="500" rows="2" placeholder="es. Verifica estintori, Autorizzazione trasporto rifiuti..."></textarea>
</div>
</div>
<!-- Group 2: Date e frequenza -->
<div class="form-section-title">Date e frequenza</div>
<div class="row g-3 mb-4">
<div class="col-12 col-md-4">
<label for="dlRecurrence" class="form-label fw-semibold">Periodicità</label>
<select class="form-select" id="dlRecurrence" name="recurrence_type">
<option value="once">Una tantum</option>
<option value="monthly">Mensile</option>
<option value="quarterly">Trimestrale</option>
<option value="semiannual">Semestrale</option>
<option value="annual">Annuale</option>
<option value="biennial">Biennale</option>
<option value="triennial">Triennale</option>
<option value="quadriennial">Quadriennale</option>
<option value="quinquennial">Quinquennale</option>
<option value="decennial">Decennale</option>
<option value="quindecennial">Quindicennale</option>
</select>
</div>
<div class="col-12 col-md-4">
<label for="dlDocDate" class="form-label fw-semibold">Data documento</label>
<input type="text" class="form-control js-date-it" id="dlDocDate" name="document_date" placeholder="gg/mm/aaaa" autocomplete="off">
</div>
<div class="col-12 col-md-4">
<label for="dlDueDate" class="form-label fw-semibold">Data scadenza <span class="text-danger">*</span></label>
<input type="text" class="form-control js-date-it" id="dlDueDate" name="due_date" placeholder="gg/mm/aaaa" autocomplete="off" required>
</div>
<div class="col-12 col-md-4">
<label for="dlCheckDate" class="form-label fw-semibold">Data ultimo controllo</label>
<input type="text" class="form-control js-date-it" id="dlCheckDate" name="check_date" placeholder="gg/mm/aaaa" autocomplete="off">
</div>
</div>
<!-- Group 3: Responsabili -->
<div class="form-section-title">Responsabili</div>
<div class="row g-3 mb-4">
<div class="col-12">
<label for="dlDepartments" class="form-label fw-semibold">Reparti</label>
<select class="form-select" id="dlDepartments" name="department_names[]" multiple>
<?php foreach ($departments as $dept): ?>
<option value="<?= htmlspecialchars($dept['name'], ENT_QUOTES, 'UTF-8') ?>">
<?= htmlspecialchars($dept['name'], ENT_QUOTES, 'UTF-8') ?>
<?= !empty($dept['code']) ? ' (' . htmlspecialchars($dept['code'], ENT_QUOTES, 'UTF-8') . ')' : '' ?>
</option>
<?php endforeach; ?>
</select>
<div class="form-text">Tutto il reparto sarà responsabile</div>
</div>
<div class="col-12">
<label for="dlEmployees" class="form-label fw-semibold">Singoli responsabili</label>
<select class="form-select" id="dlEmployees" name="employee_ids[]" multiple>
<?php foreach ($employees as $emp): ?>
<option value="<?= (int)$emp['id'] ?>">
<?= htmlspecialchars($emp['first_name'] . ' ' . $emp['last_name'], ENT_QUOTES, 'UTF-8') ?><?php if (!empty($emp['department_name'])): ?> (<?= htmlspecialchars($emp['department_name'], ENT_QUOTES, 'UTF-8') ?>)<?php endif; ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<!-- Group 4: Dettagli aggiuntivi -->
<div class="form-section-title">Dettagli aggiuntivi</div>
<div class="row g-3">
<div class="col-12 col-md-4">
<label for="dlNotifDays" class="form-label fw-semibold">Giorni preavviso</label>
<input type="number" class="form-control" id="dlNotifDays" name="notification_days" value="7" min="1" max="365">
</div>
<div class="col-12 col-md-8">
<label for="dlStorage" class="form-label fw-semibold">Luogo archiviazione</label>
<input type="text" class="form-control" id="dlStorage" name="storage_location" maxlength="500" placeholder="es. Armadio A3, Server/Documenti/Sicurezza...">
</div>
<div class="col-12">
<label for="dlNotes" class="form-label fw-semibold">Note</label>
<textarea class="form-control" id="dlNotes" name="notes" rows="3" placeholder="es. Scadenza 09/06/2026, Attività in appalto a Ditta specializzata..."></textarea>
</div>
</div>
<!-- Group 5: Allegati -->
<div class="form-section-title mt-4">Allegati</div>
<div id="attachmentsList" class="mb-3"></div>
<div class="row g-3">
<div class="col-12">
<label for="dlFiles" class="form-label fw-semibold">Carica file</label>
<input type="file" class="form-control" id="dlFiles" multiple>
<div class="form-text">Puoi selezionare più file contemporaneamente</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-scad-primary">
<i class="fa-solid fa-check me-1"></i> Salva
</button>
</div>
</form>
</div>
</div>
</div>
@@ -0,0 +1,99 @@
<?php
/**
* Shared styles for the deadline modal (deadline_modal.php).
* Relies on the --scad-* CSS variables defined on each page's :root.
*/
?>
<style>
.form-section-title {
font-size: 0.8rem;
font-weight: 700;
color: var(--scad-heading);
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 0.75rem;
padding-bottom: 0.4rem;
border-bottom: 2px solid #e8eeff;
}
#deadlineModal.modal {
position: fixed;
}
#deadlineModal .modal-content,
#deadlineModal .modal-body,
#deadlineModal .modal-footer {
background: #fff !important;
}
#deadlineModal .modal-header {
background: var(--scad-card-bg);
border-bottom: 1px solid var(--scad-card-border);
}
#deadlineModal .modal-title {
font-weight: 700;
color: var(--scad-heading);
}
/* Attachment list in modal */
.att-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 0.75rem;
background: #f8f9fa;
border-radius: 0.4rem;
margin-bottom: 0.4rem;
font-size: 0.85rem;
}
.att-item .att-name {
color: var(--scad-heading);
font-weight: 500;
word-break: break-all;
}
.att-item .att-actions {
display: flex;
gap: 0.4rem;
flex-shrink: 0;
margin-left: 0.5rem;
}
.att-item .att-actions a,
.att-item .att-actions button {
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 0.3rem;
border: none;
font-size: 0.75rem;
transition: all 0.15s;
}
.att-item .att-download {
background: #eef3ff;
color: var(--scad-primary);
text-decoration: none;
}
.att-item .att-download:hover {
background: var(--scad-primary);
color: #fff;
}
.att-item .att-remove {
background: #fff0f0;
color: var(--scad-red, #dc3545);
cursor: pointer;
}
.att-item .att-remove:hover {
background: var(--scad-red, #dc3545);
color: #fff;
}
</style>
@@ -0,0 +1,280 @@
<?php
/**
* Self-contained JS for the deadline modal (deadline_modal.php).
* Requires jQuery, Bootstrap, flatpickr, select2 and SweetAlert2 to be loaded first.
*
* Exposes:
* window.openDeadlineCreate() open the modal empty (new deadline)
* window.openDeadlineEdit(id) fetch a deadline and open the modal in edit mode
*
* Auto-open on load:
* #edit=<id> → opens edit for that id
* #edit → opens edit for window.SCAD_DETAIL_ID (used by detail.php)
*/
?>
<script>
$(document).ready(function() {
// --- Flatpickr date fields (visible dd/mm/yyyy, submitted yyyy-mm-dd) ---
var fpOptsDate = {
dateFormat: 'Y-m-d',
altInput: true,
altFormat: 'd/m/Y',
locale: 'it',
allowInput: true
};
var fpDocDate = flatpickr('#dlDocDate', fpOptsDate);
var fpDueDate = flatpickr('#dlDueDate', fpOptsDate);
var fpCheckDate = flatpickr('#dlCheckDate', fpOptsDate);
// --- Select2 ---
var s2Opts = {
theme: 'bootstrap-5',
allowClear: true,
dropdownParent: $('#deadlineModal .modal-body'),
language: 'it',
width: '100%'
};
$('#dlSubject').select2($.extend({}, s2Opts, { placeholder: 'Seleziona argomento...' }));
$('#dlDepartments').select2($.extend({}, s2Opts, { placeholder: 'Seleziona reparti...' }));
$('#dlEmployees').select2($.extend({}, s2Opts, { placeholder: 'Seleziona persone...' }));
$('#dlFunction').select2($.extend({}, s2Opts, { placeholder: 'Seleziona funzione...' }));
// --- Auto-calc due_date from document_date + recurrence ---
var RECURRENCE_OFFSETS = {
monthly: { months: 1 },
quarterly: { months: 3 },
semiannual: { months: 6 },
annual: { years: 1 },
biennial: { years: 2 },
triennial: { years: 3 },
quadriennial: { years: 4 },
quinquennial: { years: 5 },
decennial: { years: 10 },
quindecennial: { years: 15 }
};
function computeDueDate() {
var docVal = document.getElementById('dlDocDate').value;
var recurrence = document.getElementById('dlRecurrence').value;
var offset = RECURRENCE_OFFSETS[recurrence];
if (!docVal || !offset) return;
var d = new Date(docVal + 'T00:00:00');
if (isNaN(d.getTime())) return;
if (offset.months) d.setMonth(d.getMonth() + offset.months);
if (offset.years) d.setFullYear(d.getFullYear() + offset.years);
var iso = d.getFullYear() + '-' +
String(d.getMonth() + 1).padStart(2, '0') + '-' +
String(d.getDate()).padStart(2, '0');
fpDueDate.setDate(iso, true, 'Y-m-d');
}
$('#dlDocDate, #dlRecurrence').on('change', computeDueDate);
// --- Modal instance ---
var modal = new bootstrap.Modal(document.getElementById('deadlineModal'));
var form = document.getElementById('deadlineForm');
// --- Render attachments list ---
function renderAttachments(attachments) {
var container = document.getElementById('attachmentsList');
if (!attachments || attachments.length === 0) {
container.innerHTML = '<div class="text-muted" style="font-size:0.85rem">Nessun allegato</div>';
return;
}
container.innerHTML = attachments.map(function(a) {
return '<div class="att-item" data-att-id="' + a.id + '">' +
'<span class="att-name"><i class="fa-solid fa-paperclip me-1"></i>' + $('<span>').text(a.original_name).html() + '</span>' +
'<span class="att-actions">' +
'<a href="scadenzario/ajax/download_attachment.php?id=' + a.id + '" class="att-download" title="Scarica"><i class="fa-solid fa-download"></i></a>' +
'<button type="button" class="att-remove" title="Elimina" data-att-id="' + a.id + '"><i class="fa-solid fa-xmark"></i></button>' +
'</span></div>';
}).join('');
}
// --- Open modal (create) ---
window.openDeadlineCreate = function() {
form.reset();
document.getElementById('dlId').value = '';
document.getElementById('dlNotifDays').value = '7';
document.getElementById('modalTitle').textContent = 'Nuova Scadenza';
document.getElementById('dlFiles').value = '';
fpDocDate.clear();
fpDueDate.clear();
fpCheckDate.clear();
$('#dlSubject').val('').trigger('change');
$('#dlDepartments').val(null).trigger('change');
$('#dlEmployees').val(null).trigger('change');
$('#dlFunction').val('').trigger('change');
renderAttachments([]);
modal.show();
};
// --- Open modal (edit) ---
window.openDeadlineEdit = function(id) {
fetch('scadenzario/ajax/get_deadline.php?id=' + id)
.then(function(r) {
return r.json();
})
.then(function(data) {
if (!data.success) {
Swal.fire('Errore', data.message, 'error');
return;
}
var d = data.data;
document.getElementById('dlId').value = d.id;
$('#dlSubject').val(d.subject_id || '').trigger('change');
document.getElementById('dlTopic').value = d.topic || '';
$('#dlFunction').val(d.function_id || '').trigger('change');
document.getElementById('dlLaw').value = d.law_regulation || '';
document.getElementById('dlRecurrence').value = d.recurrence_type || 'once';
fpDocDate.setDate(d.document_date || null, false, 'Y-m-d');
fpDueDate.setDate(d.due_date || null, false, 'Y-m-d');
fpCheckDate.setDate(d.check_date || null, false, 'Y-m-d');
document.getElementById('dlNotifDays').value = d.notification_days || 7;
document.getElementById('dlStorage').value = d.storage_location || '';
document.getElementById('dlNotes').value = d.notes || '';
document.getElementById('dlFiles').value = '';
document.getElementById('modalTitle').textContent = 'Modifica Scadenza';
$('#dlDepartments').val(d.department_names || []).trigger('change');
if (Array.isArray(d.employee_ids)) {
$('#dlEmployees').val(d.employee_ids.map(String)).trigger('change');
} else {
$('#dlEmployees').val(null).trigger('change');
}
renderAttachments(d.attachments || []);
modal.show();
})
.catch(function() {
Swal.fire('Errore', 'Errore di connessione.', 'error');
});
};
// --- Save ---
var isSaving = false;
form.addEventListener('submit', function(e) {
e.preventDefault();
if (isSaving) return;
isSaving = true;
var saveBtn = form.querySelector('[type="submit"]');
saveBtn.disabled = true;
saveBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin me-1"></i> Salvataggio...';
var formData = new FormData(form);
fetch('scadenzario/ajax/save_deadline.php', {
method: 'POST',
body: formData
})
.then(function(r) {
return r.json();
})
.then(function(data) {
if (data.success) {
var deadlineId = data.id;
var fileInput = document.getElementById('dlFiles');
if (fileInput.files.length > 0) {
var fileData = new FormData();
fileData.append('deadline_id', deadlineId);
for (var i = 0; i < fileInput.files.length; i++) {
fileData.append('files[]', fileInput.files[i]);
}
return fetch('scadenzario/ajax/upload_attachment.php', {
method: 'POST',
body: fileData
})
.then(function(r) {
return r.json();
})
.then(function(upData) {
modal.hide();
Swal.fire({
icon: 'success',
title: 'Salvato',
text: data.message + ' ' + upData.message,
timer: 2000,
showConfirmButton: false
})
.then(function() {
location.reload();
});
});
} else {
modal.hide();
Swal.fire({
icon: 'success',
title: 'Salvato',
text: data.message,
timer: 1500,
showConfirmButton: false
})
.then(function() {
location.reload();
});
}
} else {
Swal.fire('Errore', data.message, 'error');
isSaving = false;
saveBtn.disabled = false;
saveBtn.innerHTML = '<i class="fa-solid fa-check me-1"></i> Salva';
}
})
.catch(function() {
Swal.fire('Errore', 'Errore di connessione.', 'error');
isSaving = false;
saveBtn.disabled = false;
saveBtn.innerHTML = '<i class="fa-solid fa-check me-1"></i> Salva';
});
});
// --- Delete attachment ---
$(document).on('click', '.att-remove', function(e) {
e.preventDefault();
var btn = $(this);
var attId = btn.data('att-id');
Swal.fire({
title: 'Rimuovere l\'allegato?',
text: 'Il collegamento verrà rimosso da questa scadenza. Il file resta disponibile se è usato da altre scadenze.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#dc3545',
cancelButtonText: 'Annulla',
confirmButtonText: 'Rimuovi',
reverseButtons: true
}).then(function(result) {
if (result.isConfirmed) {
fetch('scadenzario/ajax/delete_attachment.php?id=' + attId)
.then(function(r) {
return r.json();
})
.then(function(data) {
if (data.success) {
btn.closest('.att-item').remove();
if ($('#attachmentsList .att-item').length === 0) {
renderAttachments([]);
}
Swal.fire({
icon: 'success',
title: 'Fatto',
text: data.message,
timer: 1800,
showConfirmButton: false
});
} else {
Swal.fire('Errore', data.message, 'error');
}
});
}
});
});
// --- Auto-open from hash (#edit=ID or #edit for the current detail page) ---
var hash = window.location.hash;
var hashMatch = hash.match(/^#edit=(\d+)$/);
var autoEditId = hashMatch ? hashMatch[1] :
(hash === '#edit' && window.SCAD_DETAIL_ID ? window.SCAD_DETAIL_ID : null);
if (autoEditId) {
history.replaceState(null, '', window.location.pathname + window.location.search);
window.openDeadlineEdit(autoEditId);
}
});
</script>
@@ -1,4 +1,5 @@
<?php <?php
/** /**
* Renders two status banners for the current user: * Renders two status banners for the current user:
* - red -> overdue deadlines (scaduta) * - red -> overdue deadlines (scaduta)
@@ -44,63 +45,90 @@ if (!$_emp || ($_overdue === 0 && $_approaching === 0)) {
?> ?>
<style> <style>
.my-deadlines-widgets { .my-deadlines-widgets {
display: flex; flex-wrap: wrap; gap: 0.75rem; display: flex;
margin-bottom: 1rem; width: 100%; gap: 0.75rem;
} margin-bottom: 1rem;
.my-deadlines-widgets:empty { display: none; } flex-wrap: wrap;
/* When two widget containers are nested inside an outer .my-deadlines-widgets
(e.g. on the production dashboard), let their children flow into the outer flex. */
.my-deadlines-widgets .my-deadlines-widgets {
display: contents;
} }
.my-deadlines-widgets .mdw { .my-deadlines-widgets .mdw {
flex: 1 1 0; min-width: 0; flex: 1 1 260px;
display: flex; align-items: center; gap: 0.75rem; display: flex;
padding: 0.8rem 0.9rem; border-radius: 0.6rem; align-items: center;
text-decoration: none; color: #fff; gap: 0.9rem;
box-shadow: 0 2px 6px rgba(0,0,0,0.08); padding: 0.85rem 1rem;
border-radius: 0.6rem;
text-decoration: none;
color: #fff;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
transition: transform 0.15s, box-shadow 0.15s; 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:hover {
.my-deadlines-widgets .mdw-orange { background: linear-gradient(135deg, #e8930c 0%, #c77a00 100%); } transform: translateY(-1px);
.my-deadlines-widgets .mdw-gray { background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%); } box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
color: #fff;
}
.my-deadlines-widgets .mdw-red {
background: linear-gradient(135deg, #dc3545 0%, #b02a37 100%);
}
.my-deadlines-widgets .mdw-orange {
background: linear-gradient(135deg, #e8930c 0%, #c77a00 100%);
}
.my-deadlines-widgets .mdw-icon { .my-deadlines-widgets .mdw-icon {
width: 38px; height: 38px; border-radius: 50%; width: 42px;
display: flex; align-items: center; justify-content: center; height: 42px;
background: rgba(255,255,255,0.22); font-size: 1.05rem; flex-shrink: 0; border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.22);
font-size: 1.2rem;
flex-shrink: 0;
} }
.my-deadlines-widgets .mdw-body { flex: 1; line-height: 1.2; min-width: 0; }
.my-deadlines-widgets .mdw-count { font-size: 1.5rem; font-weight: 700; } .my-deadlines-widgets .mdw-body {
.my-deadlines-widgets .mdw-label { font-size: 0.78rem; opacity: 0.95; flex: 1;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } line-height: 1.2;
.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-count {
font-size: 1.6rem;
font-weight: 700;
}
.my-deadlines-widgets .mdw-label {
font-size: 0.8rem;
opacity: 0.95;
}
.my-deadlines-widgets .mdw-arrow {
opacity: 0.7;
font-size: 0.9rem;
} }
</style> </style>
<div class="my-deadlines-widgets"> <div class="my-deadlines-widgets">
<?php if ($_overdue > 0): ?> <?php if ($_overdue > 0): ?>
<a class="mdw mdw-red" href="scadenzario/index.php?filter_my=1&filter_status=scaduta"> <a class="mdw mdw-red" href="scadenzario/index.php?filter_my=1&filter_status=scaduta">
<span class="mdw-icon"><i class="fa-solid fa-triangle-exclamation"></i></span> <span class="mdw-icon"><i class="fa-solid fa-triangle-exclamation"></i></span>
<span class="mdw-body"> <span class="mdw-body">
<span class="mdw-count"><?= $_overdue ?></span> <span class="mdw-count"><?= $_overdue ?></span>
<span class="mdw-label d-block">Scadenz<?= $_overdue === 1 ? 'a' : 'e' ?> scadut<?= $_overdue === 1 ? 'a' : 'e' ?> — <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span> <span class="mdw-label d-block">Task<?= $_overdue === 1 ? '' : 's' ?> scadut<?= $_overdue === 1 ? 'o' : 'i' ?> — <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span>
</span> </span>
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span> <span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
</a> </a>
<?php endif; ?> <?php endif; ?>
<?php if ($_approaching > 0): ?> <?php if ($_approaching > 0): ?>
<a class="mdw mdw-orange" href="scadenzario/index.php?filter_my=1&filter_status=in-scadenza"> <a class="mdw mdw-orange" href="scadenzario/index.php?filter_my=1&filter_status=in-scadenza">
<span class="mdw-icon"><i class="fa-solid fa-clock"></i></span> <span class="mdw-icon"><i class="fa-solid fa-clock"></i></span>
<span class="mdw-body"> <span class="mdw-body">
<span class="mdw-count"><?= $_approaching ?></span> <span class="mdw-count"><?= $_approaching ?></span>
<span class="mdw-label d-block">In scadenza a breve <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span> <span class="mdw-label d-block">In scadenza a breve <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span>
</span> </span>
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span> <span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
</a> </a>
<?php endif; ?> <?php endif; ?>
</div> </div>
+114 -483
View File
@@ -37,12 +37,14 @@ $sql = "
SELECT d.*, SELECT d.*,
s.name AS subject_name, s.name AS subject_name,
s.color AS subject_color, s.color AS subject_color,
f.name AS function_name,
GROUP_CONCAT(DISTINCT CONCAT(e.first_name, ' ', e.last_name) ORDER BY e.first_name SEPARATOR ', ') as responsabili, GROUP_CONCAT(DISTINCT CONCAT(e.first_name, ' ', e.last_name) ORDER BY e.first_name SEPARATOR ', ') as responsabili,
GROUP_CONCAT(DISTINCT dep.name ORDER BY dep.name SEPARATOR ', ') as reparti_persone, GROUP_CONCAT(DISTINCT dep.name ORDER BY dep.name SEPARATOR ', ') as reparti_persone,
d.departments as reparti_assegnati, d.departments as reparti_assegnati,
(SELECT COUNT(*) FROM scad_deadline_attachments att WHERE att.deadline_id = d.id) as attachment_count (SELECT COUNT(*) FROM scad_deadline_attachments att WHERE att.deadline_id = d.id) as attachment_count
FROM scad_deadlines d FROM scad_deadlines d
LEFT JOIN scad_subjects s ON s.id = d.subject_id LEFT JOIN scad_subjects s ON s.id = d.subject_id
LEFT JOIN scad_functions f ON f.id = d.function_id
LEFT JOIN scad_deadline_employee de ON de.deadline_id = d.id LEFT JOIN scad_deadline_employee de ON de.deadline_id = d.id
LEFT JOIN employees e ON e.id = de.employee_id LEFT JOIN employees e ON e.id = de.employee_id
LEFT JOIN departments dep ON dep.id = e.department_id LEFT JOIN departments dep ON dep.id = e.department_id
@@ -69,27 +71,7 @@ $stmt = $pdo->prepare($sql);
$stmt->execute($params); $stmt->execute($params);
$deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC); $deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC);
$employees = $pdo->query(" require __DIR__ . '/include/deadline_form_data.php';
SELECT
e.id,
e.first_name,
e.last_name,
e.department_id,
dep.name AS department_name
FROM employees e
LEFT JOIN departments dep ON dep.id = e.department_id
WHERE e.status = 'active'
ORDER BY e.first_name, e.last_name
")->fetchAll(PDO::FETCH_ASSOC);
$departments = $pdo->query("
SELECT id, name, code, color
FROM departments
WHERE is_active = 1
ORDER BY sort_order ASC, name ASC
")->fetchAll(PDO::FETCH_ASSOC);
$subjects = $pdo->query("SELECT id, name, color FROM scad_subjects ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
$today = date('Y-m-d'); $today = date('Y-m-d');
@@ -494,7 +476,8 @@ function getContrastTextColor($hexColor)
} }
#deadlinesTable td:first-child { #deadlinesTable td:first-child {
max-width: 150px; max-width: 110px;
width: 110px;
} }
/* Attachment list in modal */ /* Attachment list in modal */
@@ -824,6 +807,9 @@ function getContrastTextColor($hexColor)
<a href="scadenzario/subjects/index.php" class="btn btn-scad-outline d-none d-md-inline-flex align-items-center gap-2"> <a href="scadenzario/subjects/index.php" class="btn btn-scad-outline d-none d-md-inline-flex align-items-center gap-2">
<i class="fa-solid fa-tags"></i><span>Argomenti</span> <i class="fa-solid fa-tags"></i><span>Argomenti</span>
</a> </a>
<a href="scadenzario/functions/index.php" class="btn btn-scad-outline d-none d-md-inline-flex align-items-center gap-2">
<i class="fa-solid fa-briefcase"></i><span>Funzioni</span>
</a>
<a href="scadenzario/calendar.php" class="btn btn-scad-outline d-none d-md-inline-flex align-items-center gap-2"> <a href="scadenzario/calendar.php" class="btn btn-scad-outline d-none d-md-inline-flex align-items-center gap-2">
<i class="fa-solid fa-calendar-days"></i><span>Calendario</span> <i class="fa-solid fa-calendar-days"></i><span>Calendario</span>
</a> </a>
@@ -842,6 +828,7 @@ function getContrastTextColor($hexColor)
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item d-flex align-items-center gap-2" href="scadenzario/subjects/index.php"><i class="fa-solid fa-tags"></i> Argomenti</a></li> <li><a class="dropdown-item d-flex align-items-center gap-2" href="scadenzario/subjects/index.php"><i class="fa-solid fa-tags"></i> Argomenti</a></li>
<li><a class="dropdown-item d-flex align-items-center gap-2" href="scadenzario/functions/index.php"><i class="fa-solid fa-briefcase"></i> Funzioni</a></li>
<li><a class="dropdown-item d-flex align-items-center gap-2" href="scadenzario/calendar.php"><i class="fa-solid fa-calendar-days"></i> Calendario</a></li> <li><a class="dropdown-item d-flex align-items-center gap-2" href="scadenzario/calendar.php"><i class="fa-solid fa-calendar-days"></i> Calendario</a></li>
<li><button type="button" class="dropdown-item d-flex align-items-center gap-2" id="btnStampaMobile"><i class="fa-solid fa-print"></i> Stampa</button></li> <li><button type="button" class="dropdown-item d-flex align-items-center gap-2" id="btnStampaMobile"><i class="fa-solid fa-print"></i> Stampa</button></li>
</ul> </ul>
@@ -923,7 +910,9 @@ function getContrastTextColor($hexColor)
data-department="<?= htmlspecialchars($row['reparti'] ?? '', ENT_QUOTES, 'UTF-8') ?>" data-department="<?= htmlspecialchars($row['reparti'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-employees="<?= htmlspecialchars($row['responsabili'] ?? '', ENT_QUOTES, 'UTF-8') ?>" data-employees="<?= htmlspecialchars($row['responsabili'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-due-date="<?= htmlspecialchars($row['due_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>" data-due-date="<?= htmlspecialchars($row['due_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-check-date="<?= htmlspecialchars($row['check_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"> data-check-date="<?= htmlspecialchars($row['check_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-recurrence="<?= htmlspecialchars($row['recurrence_type'] ?? 'once', ENT_QUOTES, 'UTF-8') ?>"
data-att-count="<?= (int)$row['attachment_count'] ?>">
<?php if (!empty($row['subject_name'])): ?> <?php if (!empty($row['subject_name'])): ?>
<div class="mb-1"><?php <div class="mb-1"><?php
$subjectBadgeBg = $row['subject_color'] ?: '#6c757d'; $subjectBadgeBg = $row['subject_color'] ?: '#6c757d';
@@ -972,11 +961,12 @@ function getContrastTextColor($hexColor)
<table id="deadlinesTable" class="table table-hover align-middle mb-0" style="width:100%"> <table id="deadlinesTable" class="table table-hover align-middle mb-0" style="width:100%">
<thead> <thead>
<tr> <tr>
<th>Argomento</th> <th style="width:110px">Argomento</th>
<th>Dettaglio</th> <th>Dettaglio</th>
<th class="d-none d-lg-table-cell">Legge/Art.</th> <th class="d-none d-lg-table-cell">Legge/Art.</th>
<th>Scadenza</th> <th>Scadenza</th>
<th class="d-none d-lg-table-cell">Verifica</th> <th class="d-none d-lg-table-cell">Verifica</th>
<th>Funzione</th>
<th>Responsabili</th> <th>Responsabili</th>
<th>Stato</th> <th>Stato</th>
<th class="text-center" style="width:120px">Azioni</th> <th class="text-center" style="width:120px">Azioni</th>
@@ -1000,7 +990,9 @@ function getContrastTextColor($hexColor)
data-department="<?= htmlspecialchars($row['reparti'] ?? '', ENT_QUOTES, 'UTF-8') ?>" data-department="<?= htmlspecialchars($row['reparti'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-employees="<?= htmlspecialchars($row['responsabili'] ?? '', ENT_QUOTES, 'UTF-8') ?>" data-employees="<?= htmlspecialchars($row['responsabili'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-due-date="<?= htmlspecialchars($row['due_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>" data-due-date="<?= htmlspecialchars($row['due_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-check-date="<?= htmlspecialchars($row['check_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"> data-check-date="<?= htmlspecialchars($row['check_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-recurrence="<?= htmlspecialchars($row['recurrence_type'] ?? 'once', ENT_QUOTES, 'UTF-8') ?>"
data-att-count="<?= (int)$row['attachment_count'] ?>">
<td> <td>
<?php if (!empty($row['subject_name'])): ?> <?php if (!empty($row['subject_name'])): ?>
<?php <?php
@@ -1014,6 +1006,7 @@ function getContrastTextColor($hexColor)
<span class="text-muted"></span> <span class="text-muted"></span>
<?php endif; ?> <?php endif; ?>
</td> </td>
<td> <td>
<a href="scadenzario/detail.php?id=<?= (int)$row['id'] ?>" class="fw-semibold text-decoration-none" style="color:var(--scad-heading)"><?= htmlspecialchars($row['topic'], ENT_QUOTES, 'UTF-8') ?></a> <a href="scadenzario/detail.php?id=<?= (int)$row['id'] ?>" class="fw-semibold text-decoration-none" style="color:var(--scad-heading)"><?= htmlspecialchars($row['topic'], ENT_QUOTES, 'UTF-8') ?></a>
<?php if ((int)$row['attachment_count'] > 0): ?> <?php if ((int)$row['attachment_count'] > 0): ?>
@@ -1023,6 +1016,17 @@ function getContrastTextColor($hexColor)
<td class="d-none d-lg-table-cell text-muted"><?= htmlspecialchars($row['law_regulation'] ?? '—', ENT_QUOTES, 'UTF-8') ?></td> <td class="d-none d-lg-table-cell text-muted"><?= htmlspecialchars($row['law_regulation'] ?? '—', ENT_QUOTES, 'UTF-8') ?></td>
<td><span class="text-nowrap"><?= $row['_dueFmt'] ?></span></td> <td><span class="text-nowrap"><?= $row['_dueFmt'] ?></span></td>
<td class="d-none d-lg-table-cell text-muted"><?= $row['_checkFmt'] ?></td> <td class="d-none d-lg-table-cell text-muted"><?= $row['_checkFmt'] ?></td>
<td>
<?php if (!empty($row['function_name'])): ?>
<span class="text-muted">
<i class="fa-solid fa-briefcase me-1"></i>
<?= htmlspecialchars($row['function_name'], ENT_QUOTES, 'UTF-8') ?>
</span>
<?php else: ?>
<span class="text-muted"></span>
<?php endif; ?>
</td>
<td> <td>
<?php if ($row['reparti']): ?><span class="text-muted"><i class="fa-regular fa-building me-1"></i><?= htmlspecialchars($row['reparti'], ENT_QUOTES, 'UTF-8') ?></span><?php endif; ?> <?php if ($row['reparti']): ?><span class="text-muted"><i class="fa-regular fa-building me-1"></i><?= htmlspecialchars($row['reparti'], ENT_QUOTES, 'UTF-8') ?></span><?php endif; ?>
<?php if ($row['reparti'] && $row['responsabili']): ?><br><?php endif; ?> <?php if ($row['reparti'] && $row['responsabili']): ?><br><?php endif; ?>
@@ -1055,143 +1059,7 @@ function getContrastTextColor($hexColor)
<?php include('../include/footer.php'); ?> <?php include('../include/footer.php'); ?>
</div> </div>
<!-- Deadline Modal --> <?php include __DIR__ . '/include/deadline_modal.php'; ?>
<div class="modal fade" id="deadlineModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl modal-fullscreen-sm-down">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalTitle">Nuova Scadenza</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<form id="deadlineForm">
<div class="modal-body">
<input type="hidden" id="dlId" name="id" value="">
<!-- Group 1: Informazioni principali -->
<div class="form-section-title">Informazioni principali</div>
<div class="row g-3 mb-4">
<div class="col-12 col-md-6">
<label for="dlSubject" class="form-label fw-semibold">Argomento</label>
<div class="d-flex gap-2">
<select class="form-select" id="dlSubject" name="subject_id" style="flex:1">
<option value=""> Nessuno </option>
<?php foreach ($subjects as $s): ?>
<option value="<?= (int)$s['id'] ?>" data-color="<?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
<a href="scadenzario/subjects/index.php" target="_blank" class="btn btn-scad-outline" title="Gestisci argomenti">
<i class="fa-solid fa-gear"></i>
</a>
</div>
</div>
<div class="col-12 col-md-6">
<label for="dlLaw" class="form-label fw-semibold">Legge / Articolo</label>
<input type="text" class="form-control" id="dlLaw" name="law_regulation" maxlength="500" placeholder="es. D.Lgs. 81/2008, D.M. 10.03.1998...">
</div>
<div class="col-12">
<label for="dlTopic" class="form-label fw-semibold">Dettaglio <span class="text-danger">*</span></label>
<textarea class="form-control" id="dlTopic" name="topic" required maxlength="500" rows="2" placeholder="es. Verifica estintori, Autorizzazione trasporto rifiuti..."></textarea>
</div>
</div>
<!-- Group 2: Date e frequenza -->
<div class="form-section-title">Date e frequenza</div>
<div class="row g-3 mb-4">
<div class="col-12 col-md-4">
<label for="dlRecurrence" class="form-label fw-semibold">Periodicità</label>
<select class="form-select" id="dlRecurrence" name="recurrence_type">
<option value="once">Una tantum</option>
<option value="monthly">Mensile</option>
<option value="quarterly">Trimestrale</option>
<option value="semiannual">Semestrale</option>
<option value="annual">Annuale</option>
<option value="biennial">Biennale</option>
<option value="triennial">Triennale</option>
<option value="quadriennial">Quadriennale</option>
<option value="quinquennial">Quinquennale</option>
<option value="decennial">Decennale</option>
<option value="quindecennial">Quindicennale</option>
</select>
</div>
<div class="col-12 col-md-4">
<label for="dlDocDate" class="form-label fw-semibold">Data documento</label>
<input type="date" class="form-control" id="dlDocDate" name="document_date">
</div>
<div class="col-12 col-md-4">
<label for="dlDueDate" class="form-label fw-semibold">Data scadenza <span class="text-danger">*</span></label>
<input type="date" class="form-control" id="dlDueDate" name="due_date" required>
</div>
<div class="col-12 col-md-4">
<label for="dlCheckDate" class="form-label fw-semibold">Data ultimo controllo</label>
<input type="date" class="form-control" id="dlCheckDate" name="check_date">
</div>
</div>
<!-- Group 3: Responsabili -->
<div class="form-section-title">Responsabili</div>
<div class="row g-3 mb-4">
<div class="col-12">
<label for="dlDepartments" class="form-label fw-semibold">Reparti</label>
<select class="form-select" id="dlDepartments" name="department_names[]" multiple>
<?php foreach ($departments as $dept): ?>
<option value="<?= htmlspecialchars($dept['name'], ENT_QUOTES, 'UTF-8') ?>">
<?= htmlspecialchars($dept['name'], ENT_QUOTES, 'UTF-8') ?>
<?= !empty($dept['code']) ? ' (' . htmlspecialchars($dept['code'], ENT_QUOTES, 'UTF-8') . ')' : '' ?>
</option>
<?php endforeach; ?>
</select>
<div class="form-text">Tutto il reparto sarà responsabile</div>
</div>
<div class="col-12">
<label for="dlEmployees" class="form-label fw-semibold">Singoli responsabili</label>
<select class="form-select" id="dlEmployees" name="employee_ids[]" multiple>
<?php foreach ($employees as $emp): ?>
<option value="<?= (int)$emp['id'] ?>">
<?= htmlspecialchars($emp['first_name'] . ' ' . $emp['last_name'], ENT_QUOTES, 'UTF-8') ?><?php if (!empty($emp['department_name'])): ?> (<?= htmlspecialchars($emp['department_name'], ENT_QUOTES, 'UTF-8') ?>)<?php endif; ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<!-- Group 4: Dettagli aggiuntivi -->
<div class="form-section-title">Dettagli aggiuntivi</div>
<div class="row g-3">
<div class="col-12 col-md-4">
<label for="dlNotifDays" class="form-label fw-semibold">Giorni preavviso</label>
<input type="number" class="form-control" id="dlNotifDays" name="notification_days" value="7" min="1" max="365">
</div>
<div class="col-12 col-md-8">
<label for="dlStorage" class="form-label fw-semibold">Luogo archiviazione</label>
<input type="text" class="form-control" id="dlStorage" name="storage_location" maxlength="500" placeholder="es. Armadio A3, Server/Documenti/Sicurezza...">
</div>
<div class="col-12">
<label for="dlNotes" class="form-label fw-semibold">Note</label>
<textarea class="form-control" id="dlNotes" name="notes" rows="3" placeholder="es. Scadenza 09/06/2026, Attività in appalto a Ditta specializzata..."></textarea>
</div>
</div>
<!-- Group 5: Allegati -->
<div class="form-section-title mt-4">Allegati</div>
<div id="attachmentsList" class="mb-3"></div>
<div class="row g-3">
<div class="col-12">
<label for="dlFiles" class="form-label fw-semibold">Carica file</label>
<input type="file" class="form-control" id="dlFiles" multiple>
<div class="form-text">Puoi selezionare più file contemporaneamente</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-scad-primary">
<i class="fa-solid fa-check me-1"></i> Salva
</button>
</div>
</form>
</div>
</div>
</div>
<?php include('../jsinclude.php'); ?> <?php include('../jsinclude.php'); ?>
<script src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js"></script> <script src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js"></script>
@@ -1221,84 +1089,6 @@ function getContrastTextColor($hexColor)
var fpDue = flatpickr('#filterDueRange', fpOpts); var fpDue = flatpickr('#filterDueRange', fpOpts);
var fpCheck = flatpickr('#filterCheckRange', fpOpts); var fpCheck = flatpickr('#filterCheckRange', fpOpts);
// --- Select2 ---
$('#dlSubject').select2({
theme: 'bootstrap-5',
placeholder: 'Seleziona argomento...',
allowClear: true,
dropdownParent: $('#deadlineModal .modal-body'),
language: 'it',
width: '100%'
});
$('#dlDepartments').select2({
theme: 'bootstrap-5',
placeholder: 'Seleziona reparti...',
allowClear: true,
dropdownParent: $('#deadlineModal .modal-body'),
language: 'it',
width: '100%'
});
$('#dlEmployees').select2({
theme: 'bootstrap-5',
placeholder: 'Seleziona persone...',
allowClear: true,
dropdownParent: $('#deadlineModal .modal-body'),
language: 'it',
width: '100%'
});
// --- Auto-calc due_date from document_date + recurrence ---
var RECURRENCE_OFFSETS = {
monthly: {
months: 1
},
quarterly: {
months: 3
},
semiannual: {
months: 6
},
annual: {
years: 1
},
biennial: {
years: 2
},
triennial: {
years: 3
},
quadriennial: {
years: 4
},
quinquennial: {
years: 5
},
decennial: {
years: 10
},
quindecennial: {
years: 15
}
};
function computeDueDate() {
var docVal = document.getElementById('dlDocDate').value;
var recurrence = document.getElementById('dlRecurrence').value;
var offset = RECURRENCE_OFFSETS[recurrence];
if (!docVal || !offset) return;
var d = new Date(docVal + 'T00:00:00');
if (isNaN(d.getTime())) return;
if (offset.months) d.setMonth(d.getMonth() + offset.months);
if (offset.years) d.setFullYear(d.getFullYear() + offset.years);
var iso = d.getFullYear() + '-' +
String(d.getMonth() + 1).padStart(2, '0') + '-' +
String(d.getDate()).padStart(2, '0');
document.getElementById('dlDueDate').value = iso;
}
$('#dlDocDate, #dlRecurrence').on('change', computeDueDate);
// --- DataTables custom filters --- // --- DataTables custom filters ---
$.fn.dataTable.ext.search.push(function(settings, data, dataIndex) { $.fn.dataTable.ext.search.push(function(settings, data, dataIndex) {
if (settings.nTable.id !== 'deadlinesTable') return true; if (settings.nTable.id !== 'deadlinesTable') return true;
@@ -1460,148 +1250,8 @@ function getContrastTextColor($hexColor)
// Apply default filter on load // Apply default filter on load
applyFiltersRefresh(); applyFiltersRefresh();
// --- Modal ---
var modal = new bootstrap.Modal(document.getElementById('deadlineModal'));
var form = document.getElementById('deadlineForm');
// Add
document.getElementById('btnAddDeadline').addEventListener('click', function() { document.getElementById('btnAddDeadline').addEventListener('click', function() {
form.reset(); if (window.openDeadlineCreate) window.openDeadlineCreate();
document.getElementById('dlId').value = '';
document.getElementById('dlNotifDays').value = '7';
document.getElementById('modalTitle').textContent = 'Nuova Scadenza';
document.getElementById('dlFiles').value = '';
$('#dlSubject').val('').trigger('change');
$('#dlDepartments').val(null).trigger('change');
$('#dlEmployees').val(null).trigger('change');
renderAttachments([]);
modal.show();
});
// Save
var isSaving = false;
form.addEventListener('submit', function(e) {
e.preventDefault();
if (isSaving) return;
isSaving = true;
var saveBtn = form.querySelector('[type="submit"]');
saveBtn.disabled = true;
saveBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin me-1"></i> Salvataggio...';
var formData = new FormData(form);
fetch('scadenzario/ajax/save_deadline.php', {
method: 'POST',
body: formData
})
.then(function(r) {
return r.json();
})
.then(function(data) {
if (data.success) {
var deadlineId = data.id;
var fileInput = document.getElementById('dlFiles');
if (fileInput.files.length > 0) {
// Upload files
var fileData = new FormData();
fileData.append('deadline_id', deadlineId);
for (var i = 0; i < fileInput.files.length; i++) {
fileData.append('files[]', fileInput.files[i]);
}
return fetch('scadenzario/ajax/upload_attachment.php', {
method: 'POST',
body: fileData
})
.then(function(r) {
return r.json();
})
.then(function(upData) {
modal.hide();
Swal.fire({
icon: 'success',
title: 'Salvato',
text: data.message + ' ' + upData.message,
timer: 2000,
showConfirmButton: false
})
.then(function() {
location.reload();
});
});
} else {
modal.hide();
Swal.fire({
icon: 'success',
title: 'Salvato',
text: data.message,
timer: 1500,
showConfirmButton: false
})
.then(function() {
location.reload();
});
}
} else {
Swal.fire('Errore', data.message, 'error');
isSaving = false;
saveBtn.disabled = false;
saveBtn.innerHTML = '<i class="fa-solid fa-check me-1"></i> Salva';
}
})
.catch(function() {
Swal.fire('Errore', 'Errore di connessione.', 'error');
isSaving = false;
saveBtn.disabled = false;
saveBtn.innerHTML = '<i class="fa-solid fa-check me-1"></i> Salva';
});
});
// Render attachments list
function renderAttachments(attachments) {
var container = document.getElementById('attachmentsList');
if (!attachments || attachments.length === 0) {
container.innerHTML = '<div class="text-muted" style="font-size:0.85rem">Nessun allegato</div>';
return;
}
container.innerHTML = attachments.map(function(a) {
return '<div class="att-item" data-att-id="' + a.id + '">' +
'<span class="att-name"><i class="fa-solid fa-paperclip me-1"></i>' + $('<span>').text(a.original_name).html() + '</span>' +
'<span class="att-actions">' +
'<a href="scadenzario/ajax/download_attachment.php?id=' + a.id + '" class="att-download" title="Scarica"><i class="fa-solid fa-download"></i></a>' +
'<button type="button" class="att-remove" title="Elimina" data-att-id="' + a.id + '"><i class="fa-solid fa-xmark"></i></button>' +
'</span></div>';
}).join('');
}
// Delete attachment
$(document).on('click', '.att-remove', function(e) {
e.preventDefault();
var btn = $(this);
var attId = btn.data('att-id');
Swal.fire({
title: 'Eliminare allegato?',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#dc3545',
cancelButtonText: 'Annulla',
confirmButtonText: 'Elimina'
}).then(function(result) {
if (result.isConfirmed) {
fetch('scadenzario/ajax/delete_attachment.php?id=' + attId)
.then(function(r) {
return r.json();
})
.then(function(data) {
if (data.success) {
btn.closest('.att-item').remove();
if ($('#attachmentsList .att-item').length === 0) {
renderAttachments([]);
}
} else {
Swal.fire('Errore', data.message, 'error');
}
});
}
});
}); });
// Edit with confirmation // Edit with confirmation
@@ -1618,91 +1268,103 @@ function getContrastTextColor($hexColor)
confirmButtonText: 'Sì, modifica', confirmButtonText: 'Sì, modifica',
reverseButtons: true reverseButtons: true
}).then(function(result) { }).then(function(result) {
if (!result.isConfirmed) { if (result.isConfirmed && window.openDeadlineEdit) {
return; window.openDeadlineEdit(id);
} }
fetch('scadenzario/ajax/get_deadline.php?id=' + id)
.then(function(r) {
return r.json();
})
.then(function(data) {
if (!data.success) {
Swal.fire('Errore', data.message, 'error');
return;
}
var d = data.data;
document.getElementById('dlId').value = d.id;
$('#dlSubject').val(d.subject_id || '').trigger('change');
document.getElementById('dlTopic').value = d.topic || '';
document.getElementById('dlLaw').value = d.law_regulation || '';
document.getElementById('dlRecurrence').value = d.recurrence_type || 'once';
document.getElementById('dlDocDate').value = d.document_date || '';
document.getElementById('dlDueDate').value = d.due_date || '';
document.getElementById('dlCheckDate').value = d.check_date || '';
document.getElementById('dlNotifDays').value = d.notification_days || 7;
document.getElementById('dlStorage').value = d.storage_location || '';
document.getElementById('dlNotes').value = d.notes || '';
document.getElementById('dlFiles').value = '';
document.getElementById('modalTitle').textContent = 'Modifica Scadenza';
$('#dlDepartments').val(d.department_names || []).trigger('change');
if (Array.isArray(d.employee_ids)) {
$('#dlEmployees').val(d.employee_ids.map(String)).trigger('change');
} else {
$('#dlEmployees').val(null).trigger('change');
}
renderAttachments(d.attachments || []);
modal.show();
})
.catch(function() {
Swal.fire('Errore', 'Errore di connessione.', 'error');
});
}); });
}); });
// Complete // Complete
function submitComplete(id, createNext, copyAttachments) {
var fd = new FormData();
fd.append('id', id);
fd.append('create_next', createNext ? '1' : '0');
fd.append('copy_attachments', copyAttachments ? '1' : '0');
fetch('scadenzario/ajax/complete_deadline.php', {
method: 'POST',
body: fd
})
.then(function(r) {
return r.json();
})
.then(function(data) {
if (data.success) {
Swal.fire({
icon: 'success',
title: 'Completata',
text: data.message,
timer: 1800,
showConfirmButton: false
})
.then(function() {
// Open the new deadline's detail page with the edit modal auto-opening
if (data.new_id) {
window.location = 'scadenzario/detail.php?id=' + data.new_id + '#edit';
} else {
location.reload();
}
});
} else {
Swal.fire('Errore', data.message, 'error');
}
})
.catch(function() {
Swal.fire('Errore', 'Errore di connessione.', 'error');
});
}
$(document).on('click', '.btn-complete', function() { $(document).on('click', '.btn-complete', function() {
var el = $(this).closest('[data-id]'); var el = $(this).closest('[data-id]');
var id = el.data('id'); var id = el.data('id');
var recurrence = el.data('recurrence') || 'once';
var attCount = parseInt(el.data('att-count'), 10) || 0;
// Non-recurring: simple confirm, no new deadline is created
if (recurrence === 'once') {
Swal.fire({
title: 'Completare la scadenza?',
text: 'La scadenza verrà contrassegnata come completata.',
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#198754',
cancelButtonText: 'Annulla',
confirmButtonText: 'Completa',
reverseButtons: true
}).then(function(result) {
if (result.isConfirmed) {
submitComplete(id, false, false);
}
});
return;
}
// Recurring: ask whether to create the next deadline; optionally carry attachments over
var attCheckbox = attCount > 0 ?
'<div class="form-check d-flex align-items-center justify-content-center gap-2 mt-3">' +
'<input class="form-check-input" type="checkbox" id="swCopyAtt" checked>' +
'<label class="form-check-label" for="swCopyAtt">Copia gli allegati (' + attCount + ') sulla nuova scadenza</label>' +
'</div>' :
'';
Swal.fire({ Swal.fire({
title: 'Completare la scadenza?', title: 'Completare la scadenza?',
html: 'Vuoi creare automaticamente la prossima scadenza ricorrente?' + attCheckbox,
icon: 'question', icon: 'question',
showCancelButton: true, showCancelButton: true,
showDenyButton: true,
confirmButtonColor: '#198754', confirmButtonColor: '#198754',
denyButtonColor: '#6c757d',
confirmButtonText: 'Completa e crea la prossima',
denyButtonText: 'Completa senza nuova',
cancelButtonText: 'Annulla', cancelButtonText: 'Annulla',
confirmButtonText: 'Completa' reverseButtons: true
}).then(function(result) { }).then(function(result) {
if (result.isConfirmed) { if (result.isConfirmed) {
fetch('scadenzario/ajax/complete_deadline.php?id=' + id) var copy = attCount > 0 ? document.getElementById('swCopyAtt').checked : false;
.then(function(r) { submitComplete(id, true, copy);
return r.json(); } else if (result.isDenied) {
}) submitComplete(id, false, false);
.then(function(data) {
if (data.success) {
Swal.fire({
icon: 'success',
title: 'Completata',
text: data.message,
timer: 2500,
showConfirmButton: false
})
.then(function() {
location.reload();
});
} else {
Swal.fire('Errore', data.message, 'error');
}
})
.catch(function() {
Swal.fire('Errore', 'Errore di connessione.', 'error');
});
} }
}); });
}); });
@@ -1748,38 +1410,6 @@ function getContrastTextColor($hexColor)
}); });
}); });
// Auto-open edit modal from ?edit=ID
var urlParams = new URLSearchParams(window.location.search);
var editId = urlParams.get('edit');
if (editId) {
history.replaceState(null, '', 'scadenzario/index.php');
fetch('scadenzario/ajax/get_deadline.php?id=' + editId)
.then(function(r) {
return r.json();
})
.then(function(data) {
if (!data.success) return;
var d = data.data;
document.getElementById('dlId').value = d.id;
$('#dlSubject').val(d.subject_id || '').trigger('change');
document.getElementById('dlTopic').value = d.topic || '';
document.getElementById('dlLaw').value = d.law_regulation || '';
document.getElementById('dlRecurrence').value = d.recurrence_type || 'once';
document.getElementById('dlDocDate').value = d.document_date || '';
document.getElementById('dlDueDate').value = d.due_date || '';
document.getElementById('dlCheckDate').value = d.check_date || '';
document.getElementById('dlNotifDays').value = d.notification_days || 7;
document.getElementById('dlStorage').value = d.storage_location || '';
document.getElementById('dlNotes').value = d.notes || '';
document.getElementById('dlFiles').value = '';
document.getElementById('modalTitle').textContent = 'Modifica Scadenza';
$('#dlDepartments').val(d.department_names || []).trigger('change');
$('#dlEmployees').val(d.employee_ids.map(String)).trigger('change');
renderAttachments(d.attachments || []);
modal.show();
});
}
// Stampa // Stampa
function doStampa() { function doStampa() {
var params = []; var params = [];
@@ -1802,6 +1432,7 @@ function getContrastTextColor($hexColor)
if (btnStampaMobile) btnStampaMobile.addEventListener('click', doStampa); if (btnStampaMobile) btnStampaMobile.addEventListener('click', doStampa);
}); });
</script> </script>
<?php include __DIR__ . '/include/deadline_modal_js.php'; ?>
</body> </body>
</html> </html>
+217
View File
@@ -0,0 +1,217 @@
<?php
include('include/headscript.php');
$pdo = DBHandlerSelect::getInstance()->getConnection();
/* ==========================================
PERMISSIONS (mirror trainings.php)
========================================== */
$isHrManager = Auth::user()->hasRole('Admin')
|| Auth::user()->hasRole('Superuser')
|| Auth::user()->hasRole('employee-hr')
|| Auth::user()->hasRole('manager');
if (!$isHrManager) {
header('Location: employee-profile.php');
exit;
}
/* Dropdown data */
$employees = $pdo->query("
SELECT id, first_name, last_name, employee_code
FROM employees
ORDER BY last_name, first_name
")->fetchAll(PDO::FETCH_ASSOC);
$topics = $pdo->query("
SELECT id, name FROM training_topics WHERE is_active = 1 ORDER BY sort_order, name
")->fetchAll(PDO::FETCH_ASSOC);
$departments = $pdo->query("
SELECT id, name FROM departments WHERE is_active = 1 ORDER BY sort_order, name
")->fetchAll(PDO::FETCH_ASSOC);
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.9/index.global.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.9/locales/it.global.min.js"></script>
<title>Calendario Formazione - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<style>
body { font-size: 1.05rem; background: #f8fafc; }
.card { border-radius: 16px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); }
.back-dashboard {
background-color: #cfe3ff !important; color: #1f2d3d !important;
border: 1px solid #bcd4f4 !important; border-radius: 10px;
font-weight: 600; padding: 10px 18px;
}
.legend { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 1rem; }
.legend-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; color: #64748b; }
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
/* FullCalendar overrides */
.fc { font-size: 0.95rem; }
.fc .fc-toolbar-title { font-size: 1.15rem; font-weight: 700; color: #2c3e6b; }
.fc .fc-button-primary {
background: #5a8fd8; border-color: #5a8fd8;
font-weight: 600; font-size: 0.82rem; border-radius: 0.4rem;
}
.fc .fc-button-primary:hover { background: #4578c0; border-color: #4578c0; }
.fc .fc-button-primary:disabled { background: #9bbce6; border-color: #9bbce6; }
.fc .fc-button-primary:not(:disabled).fc-button-active { background: #2c3e6b; border-color: #2c3e6b; }
.fc .fc-daygrid-day-number { color: #2c3e6b; font-weight: 500; }
.fc .fc-daygrid-day.fc-day-today { background: #f0f4ff; }
.fc .fc-event { border-radius: 0.3rem; padding: 2px 4px; font-weight: 600; cursor: pointer; }
.fc .fc-event:hover { filter: brightness(0.92); }
.fc .fc-list-event:hover td { background: #f0f4ff; }
@media (max-width: 767.98px) {
.card-header { flex-direction: column; align-items: flex-start !important; gap: .5rem; }
.fc .fc-toolbar { flex-direction: column; gap: 0.5rem; }
.fc .fc-toolbar-title { font-size: 1rem; }
}
</style>
</head>
<body>
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card p-3">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h5 class="mb-0">📅 Calendario Formazione</h5>
<div class="d-flex gap-2 flex-wrap">
<a href="trainings.php" class="btn btn-light border d-inline-flex align-items-center gap-2">
📚 <span>Storico Formazione</span>
</a>
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
↩️ Torna alla Dashboard
</button>
</div>
</div>
<div class="card-body">
<!-- FILTERS -->
<div class="row g-2 mb-3">
<div class="col-6 col-md-3">
<label class="form-label small text-muted mb-1">Stato</label>
<select id="filterStatus" class="form-select">
<option value="">Tutti</option>
<option value="expired">Scaduti</option>
<option value="due_soon">Da aggiornare</option>
<option value="compliant">Conformi</option>
</select>
</div>
<div class="col-6 col-md-3">
<label class="form-label small text-muted mb-1">Corso</label>
<select id="filterTopic" class="form-select">
<option value="">Tutti</option>
<?php foreach ($topics as $t): ?>
<option value="<?= (int)$t['id'] ?>"><?= htmlspecialchars($t['name'], ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-6 col-md-3">
<label class="form-label small text-muted mb-1">Reparto</label>
<select id="filterDepartment" class="form-select">
<option value="">Tutti</option>
<?php foreach ($departments as $d): ?>
<option value="<?= (int)$d['id'] ?>"><?= htmlspecialchars($d['name'], ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-6 col-md-3">
<label class="form-label small text-muted mb-1">Dipendente</label>
<div class="d-flex gap-2">
<select id="filterEmployee" class="form-select">
<option value="">Tutti</option>
<?php foreach ($employees as $e): ?>
<option value="<?= (int)$e['id'] ?>"><?= htmlspecialchars(trim($e['last_name'] . ' ' . $e['first_name']), ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
<button id="btnResetFilters" type="button" class="btn btn-light border flex-shrink-0" title="Reset filtri">
<i class="fa-solid fa-rotate-left"></i>
</button>
</div>
</div>
</div>
<!-- LEGEND -->
<div class="legend">
<div class="legend-item"><span class="legend-dot" style="background:#dc3545"></span> Scaduto</div>
<div class="legend-item"><span class="legend-dot" style="background:#e8930c"></span> Da aggiornare</div>
<div class="legend-item"><span class="legend-dot" style="background:#198754"></span> Conforme</div>
</div>
<div id="calendar"></div>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
var isMobile = window.innerWidth < 768;
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
locale: 'it',
initialView: isMobile ? 'listMonth' : 'dayGridMonth',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: isMobile ? 'listMonth,dayGridMonth' : 'dayGridMonth,listMonth'
},
height: 'auto',
navLinks: true,
eventSources: [{
url: 'ajax/trainings/calendar_events.php',
extraParams: function() {
return {
status: document.getElementById('filterStatus').value,
topic_id: document.getElementById('filterTopic').value,
department_id: document.getElementById('filterDepartment').value,
employee_id: document.getElementById('filterEmployee').value
};
},
failure: function() {
if (window.Swal) Swal.fire('Errore', 'Impossibile caricare gli eventi.', 'error');
}
}],
eventClick: function(info) {
info.jsEvent.preventDefault();
if (info.event.url) window.location.href = info.event.url;
},
windowResize: function() {
calendar.changeView(window.innerWidth < 768 ? 'listMonth' : 'dayGridMonth');
}
});
calendar.render();
document.querySelectorAll('#filterStatus, #filterTopic, #filterDepartment, #filterEmployee').forEach(function(el) {
el.addEventListener('change', function() { calendar.refetchEvents(); });
});
document.getElementById('btnResetFilters').addEventListener('click', function() {
['filterStatus', 'filterTopic', 'filterDepartment', 'filterEmployee'].forEach(function(id) {
document.getElementById(id).value = '';
});
calendar.refetchEvents();
});
});
</script>
</body>
</html>
+329 -6
View File
@@ -30,6 +30,15 @@ $fDepartmentId = isset($_GET['department_id'])&& $_GET['department_id']!== '' ?
========================================== */ ========================================== */
$where = []; $where = [];
$params = []; $params = [];
// Only the most recent record per (employee, topic) — older initial/refresher
// rows stay as history on the employee profile, not in this overview.
$where[] = "NOT EXISTS (
SELECT 1 FROM employee_trainings et2
WHERE et2.employee_id = et.employee_id
AND et2.training_topic_id = et.training_topic_id
AND (et2.completed_date > et.completed_date
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
)";
if ($fEmployeeId > 0) { $where[] = 'et.employee_id = :eid'; $params['eid'] = $fEmployeeId; } if ($fEmployeeId > 0) { $where[] = 'et.employee_id = :eid'; $params['eid'] = $fEmployeeId; }
if ($fTopicId > 0) { $where[] = 'et.training_topic_id = :tid'; $params['tid'] = $fTopicId; } if ($fTopicId > 0) { $where[] = 'et.training_topic_id = :tid'; $params['tid'] = $fTopicId; }
if ($fType !== '' && in_array($fType, ['initial', 'refresher'], true)) { if ($fType !== '' && in_array($fType, ['initial', 'refresher'], true)) {
@@ -142,12 +151,13 @@ if ($fType === '' || $fType === 'initial') {
/* Dropdown data */ /* Dropdown data */
$employees = $pdo->query(" $employees = $pdo->query("
SELECT id, first_name, last_name, employee_code SELECT id, first_name, last_name, employee_code, department_id
FROM employees FROM employees
ORDER BY last_name, first_name ORDER BY last_name, first_name
")->fetchAll(PDO::FETCH_ASSOC); ")->fetchAll(PDO::FETCH_ASSOC);
$topics = $pdo->query(" $topics = $pdo->query("
SELECT id, name FROM training_topics WHERE is_active = 1 ORDER BY sort_order, name SELECT id, name, default_frequency_months, default_reminder_days
FROM training_topics WHERE is_active = 1 ORDER BY sort_order, name
")->fetchAll(PDO::FETCH_ASSOC); ")->fetchAll(PDO::FETCH_ASSOC);
$departments = $pdo->query(" $departments = $pdo->query("
SELECT id, name, color FROM departments WHERE is_active = 1 ORDER BY sort_order, name SELECT id, name, color FROM departments WHERE is_active = 1 ORDER BY sort_order, name
@@ -171,6 +181,9 @@ function fmtDate(?string $d): string {
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<style> <style>
body { font-size: 1.05rem; background: #f8fafc; } body { font-size: 1.05rem; background: #f8fafc; }
@@ -234,9 +247,14 @@ function fmtDate(?string $d): string {
<div class="card p-3"> <div class="card p-3">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2"> <div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h5 class="mb-0">📚 Storico Formazione</h5> <h5 class="mb-0">📚 Storico Formazione</h5>
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'"> <div class="d-flex gap-2 flex-wrap">
↩️ Torna alla Dashboard <button type="button" class="btn btn-primary" id="btnBulkTraining">
</button> Aggiungi sessione
</button>
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
↩️ Torna alla Dashboard
</button>
</div>
</div> </div>
<div class="card-body"> <div class="card-body">
@@ -316,6 +334,12 @@ function fmtDate(?string $d): string {
<?php endif; ?> <?php endif; ?>
</form> </form>
<div id="bulkBar" class="d-none align-items-center flex-wrap gap-2 mb-3 p-2" style="background:#fff6e5;border:1px solid #ffe0a6;border-radius:10px;">
<span class="fw-semibold"><span id="bulkSelCount">0</span> selezionati</span>
<button type="button" class="btn btn-sm btn-warning" id="btnBulkRenew">🔄 Aggiorna scadenza</button>
<button type="button" class="btn btn-sm btn-link text-decoration-none" id="btnBulkDeselect">Deseleziona</button>
</div>
<?php if (empty($filtered)): ?> <?php if (empty($filtered)): ?>
<div class="text-center text-muted py-4"> <div class="text-center text-muted py-4">
Nessuna formazione corrispondente ai filtri. Nessuna formazione corrispondente ai filtri.
@@ -326,6 +350,7 @@ function fmtDate(?string $d): string {
<table class="table table-striped align-middle"> <table class="table table-striped align-middle">
<thead style="background-color:#cfe3ff;"> <thead style="background-color:#cfe3ff;">
<tr> <tr>
<th style="width:36px"><input type="checkbox" class="form-check-input" id="checkAll" title="Seleziona tutti"></th>
<th>Dipendente</th> <th>Dipendente</th>
<th>Reparto</th> <th>Reparto</th>
<th>Corso</th> <th>Corso</th>
@@ -344,6 +369,11 @@ function fmtDate(?string $d): string {
$days = $r['_status']['days'] ?? null; $days = $r['_status']['days'] ?? null;
?> ?>
<tr> <tr>
<td>
<?php if (!empty($r['id'])): ?>
<input type="checkbox" class="form-check-input row-check" value="<?= (int)$r['id'] ?>">
<?php endif; ?>
</td>
<td> <td>
<a href="employee-profile.php?id=<?= (int)$r['employee_id'] ?>#tab-training" class="fw-semibold text-decoration-none"> <a href="employee-profile.php?id=<?= (int)$r['employee_id'] ?>#tab-training" class="fw-semibold text-decoration-none">
<?= htmlspecialchars($fullName) ?> <?= htmlspecialchars($fullName) ?>
@@ -388,7 +418,10 @@ function fmtDate(?string $d): string {
?> ?>
<div class="tr-card"> <div class="tr-card">
<div class="d-flex justify-content-between align-items-start gap-2 mb-1"> <div class="d-flex justify-content-between align-items-start gap-2 mb-1">
<div class="name"> <div class="name d-flex align-items-start gap-2">
<?php if (!empty($r['id'])): ?>
<input type="checkbox" class="form-check-input row-check mt-1" value="<?= (int)$r['id'] ?>">
<?php endif; ?>
<a href="employee-profile.php?id=<?= (int)$r['employee_id'] ?>#tab-training"> <a href="employee-profile.php?id=<?= (int)$r['employee_id'] ?>#tab-training">
<?= htmlspecialchars($fullName) ?> <?= htmlspecialchars($fullName) ?>
</a> </a>
@@ -425,6 +458,296 @@ function fmtDate(?string $d): string {
<?php include('include/footer.php'); ?> <?php include('include/footer.php'); ?>
</div> </div>
<!-- BULK TRAINING SESSION MODAL -->
<div class="modal fade" id="bulkTrainingModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"> Nuova sessione formativa</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<form id="bulkTrainingForm">
<div class="modal-body" style="max-height:65vh; overflow-y:auto;">
<p class="text-muted small">Registra lo stesso corso, con gli stessi parametri, per più dipendenti contemporaneamente.</p>
<div class="row g-3">
<div class="col-12 col-md-6">
<label class="form-label fw-semibold">Corso <span class="text-danger">*</span></label>
<select id="bulkTopic" class="form-select" required>
<option value=""> Seleziona </option>
<?php foreach ($topics as $t): ?>
<option value="<?= (int)$t['id'] ?>"
data-freq="<?= $t['default_frequency_months'] !== null ? (int)$t['default_frequency_months'] : '' ?>"
data-rem="<?= $t['default_reminder_days'] !== null ? (int)$t['default_reminder_days'] : '' ?>">
<?= htmlspecialchars($t['name'], ENT_QUOTES, 'UTF-8') ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-6 col-md-3">
<label class="form-label fw-semibold">Data completamento <span class="text-danger">*</span></label>
<input type="date" id="bulkCompletedDate" class="form-control" value="<?= date('Y-m-d') ?>" required>
</div>
<div class="col-6 col-md-3">
<label class="form-label fw-semibold">Tipo</label>
<select id="bulkType" class="form-select">
<option value="initial">Iniziale</option>
<option value="refresher">Aggiornamento</option>
</select>
</div>
<div class="col-6 col-md-3">
<label class="form-label fw-semibold">Frequenza (mesi)</label>
<input type="number" id="bulkFreq" class="form-control" min="0" max="600" placeholder="default corso">
<div class="form-text">Vuoto = una tantum</div>
</div>
<div class="col-6 col-md-3">
<label class="form-label fw-semibold">Promemoria (giorni)</label>
<input type="number" id="bulkRem" class="form-control" min="0" max="365" placeholder="default corso">
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-semibold">Erogato da</label>
<input type="text" id="bulkDeliveredBy" class="form-control" maxlength="255" placeholder="es. Ente formatore, docente interno...">
</div>
<div class="col-12">
<label class="form-label fw-semibold">Descrizione / note</label>
<textarea id="bulkDescription" class="form-control" rows="2"></textarea>
</div>
<div class="col-12"><hr class="my-1"></div>
<div class="col-12">
<label class="form-label fw-semibold">Dipendenti <span class="text-danger">*</span></label>
<div class="d-flex flex-wrap gap-2 mb-2 align-items-end">
<div>
<select id="bulkDept" class="form-select form-select-sm" style="min-width:180px">
<option value=""> Reparto </option>
<?php foreach ($departments as $d): ?>
<option value="<?= (int)$d['id'] ?>"><?= htmlspecialchars($d['name'], ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="button" class="btn btn-sm btn-outline-primary" id="bulkAddDept">+ Aggiungi reparto</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="bulkSelectAll">Tutti</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="bulkClear">Pulisci</button>
</div>
<select id="bulkEmployees" class="form-select" multiple required>
<?php foreach ($employees as $e): ?>
<option value="<?= (int)$e['id'] ?>" data-dept="<?= (int)($e['department_id'] ?? 0) ?>">
<?= htmlspecialchars(trim($e['last_name'] . ' ' . $e['first_name']), ENT_QUOTES, 'UTF-8') ?><?php if (!empty($e['employee_code'])): ?> (<?= htmlspecialchars($e['employee_code'], ENT_QUOTES, 'UTF-8') ?>)<?php endif; ?>
</option>
<?php endforeach; ?>
</select>
<div class="form-text"><span id="bulkCount">0</span> dipendenti selezionati</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light border" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-primary" id="bulkSaveBtn">Registra formazione</button>
</div>
</form>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?> <?php include('jsinclude.php'); ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
var bulkModal = new bootstrap.Modal(document.getElementById('bulkTrainingModal'));
var $emp = $('#bulkEmployees');
$emp.select2({
theme: 'bootstrap-5',
placeholder: 'Seleziona dipendenti...',
dropdownParent: $('#bulkTrainingModal'),
closeOnSelect: false,
width: '100%'
});
function updateCount() {
document.getElementById('bulkCount').textContent = ($emp.val() || []).length;
}
$emp.on('change', updateCount);
document.getElementById('btnBulkTraining').addEventListener('click', function() {
document.getElementById('bulkTrainingForm').reset();
$emp.val(null).trigger('change');
document.getElementById('bulkTopic').value = '';
document.getElementById('bulkType').value = 'initial';
updateCount();
bulkModal.show();
});
// Prefill frequency/reminder from the selected course
document.getElementById('bulkTopic').addEventListener('change', function() {
var opt = this.options[this.selectedIndex];
document.getElementById('bulkFreq').value = opt ? (opt.getAttribute('data-freq') || '') : '';
document.getElementById('bulkRem').value = opt ? (opt.getAttribute('data-rem') || '') : '';
});
// Add all employees of the chosen department to the selection
document.getElementById('bulkAddDept').addEventListener('click', function() {
var dept = document.getElementById('bulkDept').value;
if (!dept) return;
var current = ($emp.val() || []).map(String);
$emp.find('option').each(function() {
if (this.getAttribute('data-dept') === String(dept) && current.indexOf(this.value) === -1) {
current.push(this.value);
}
});
$emp.val(current).trigger('change');
});
document.getElementById('bulkSelectAll').addEventListener('click', function() {
var all = $emp.find('option').map(function() { return this.value; }).get();
$emp.val(all).trigger('change');
});
document.getElementById('bulkClear').addEventListener('click', function() {
$emp.val(null).trigger('change');
});
document.getElementById('bulkTrainingForm').addEventListener('submit', function(e) {
e.preventDefault();
var topicId = document.getElementById('bulkTopic').value;
var completed = document.getElementById('bulkCompletedDate').value;
var emps = $emp.val() || [];
if (!topicId) { Swal.fire('Attenzione', 'Selezionare un corso.', 'warning'); return; }
if (!completed) { Swal.fire('Attenzione', 'Indicare la data di completamento.', 'warning'); return; }
if (emps.length === 0) { Swal.fire('Attenzione', 'Selezionare almeno un dipendente.', 'warning'); return; }
var btn = document.getElementById('bulkSaveBtn');
btn.disabled = true;
var orig = btn.innerHTML;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Salvataggio...';
var fd = new FormData();
fd.append('training_topic_id', topicId);
fd.append('completed_date', completed);
fd.append('training_type', document.getElementById('bulkType').value);
fd.append('delivered_by', document.getElementById('bulkDeliveredBy').value);
fd.append('description', document.getElementById('bulkDescription').value);
fd.append('update_frequency_months', document.getElementById('bulkFreq').value);
fd.append('reminder_days', document.getElementById('bulkRem').value);
emps.forEach(function(id) { fd.append('employee_ids[]', id); });
fetch('ajax/trainings/save_bulk_training.php', { method: 'POST', body: fd })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
bulkModal.hide();
Swal.fire({ icon: 'success', title: 'Fatto', text: data.message, timer: 1800, showConfirmButton: false })
.then(function() { location.reload(); });
} else {
btn.disabled = false; btn.innerHTML = orig;
Swal.fire('Errore', data.message || 'Errore.', 'error');
}
})
.catch(function() {
btn.disabled = false; btn.innerHTML = orig;
Swal.fire('Errore', 'Errore di connessione.', 'error');
});
});
});
</script>
<!-- BULK RENEW DEADLINE MODAL -->
<div class="modal fade" id="bulkRenewModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form id="bulkRenewForm">
<div class="modal-header">
<h5 class="modal-title">🔄 Aggiorna scadenza</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<div class="modal-body">
<p class="text-muted small">Imposta la data di completamento per <b id="renewCount">0</b> record selezionati. Le prossime scadenze verranno ricalcolate in base alla frequenza di ciascun corso.</p>
<label class="form-label fw-semibold">Nuova data di completamento</label>
<input type="date" id="renewDate" class="form-control" value="<?= date('Y-m-d') ?>" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light border" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-warning" id="renewSaveBtn">Aggiorna</button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var renewModal = new bootstrap.Modal(document.getElementById('bulkRenewModal'));
var checkAll = document.getElementById('checkAll');
function checkedIds() {
return Array.prototype.slice.call(document.querySelectorAll('.row-check:checked'))
.map(function(c) { return c.value; });
}
function refreshBulkBar() {
var ids = checkedIds();
var bar = document.getElementById('bulkBar');
document.getElementById('bulkSelCount').textContent = ids.length;
if (ids.length > 0) { bar.classList.remove('d-none'); bar.classList.add('d-flex'); }
else { bar.classList.add('d-none'); bar.classList.remove('d-flex'); }
var all = document.querySelectorAll('.row-check');
if (checkAll) checkAll.checked = (all.length > 0 && ids.length === all.length);
}
document.addEventListener('change', function(e) {
if (e.target && e.target.classList && e.target.classList.contains('row-check')) refreshBulkBar();
});
if (checkAll) checkAll.addEventListener('change', function() {
document.querySelectorAll('.row-check').forEach(function(c) { c.checked = checkAll.checked; });
refreshBulkBar();
});
document.getElementById('btnBulkDeselect').addEventListener('click', function() {
document.querySelectorAll('.row-check').forEach(function(c) { c.checked = false; });
if (checkAll) checkAll.checked = false;
refreshBulkBar();
});
document.getElementById('btnBulkRenew').addEventListener('click', function() {
var ids = checkedIds();
if (ids.length === 0) return;
document.getElementById('renewCount').textContent = ids.length;
renewModal.show();
});
document.getElementById('bulkRenewForm').addEventListener('submit', function(e) {
e.preventDefault();
var ids = checkedIds();
var date = document.getElementById('renewDate').value;
if (ids.length === 0) { renewModal.hide(); return; }
if (!date) { Swal.fire('Attenzione', 'Indicare la data.', 'warning'); return; }
var btn = document.getElementById('renewSaveBtn');
btn.disabled = true;
var orig = btn.innerHTML;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Salvataggio...';
var fd = new FormData();
fd.append('completed_date', date);
ids.forEach(function(id) { fd.append('training_ids[]', id); });
fetch('ajax/trainings/bulk_update_deadline.php', { method: 'POST', body: fd })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
renewModal.hide();
Swal.fire({ icon: 'success', title: 'Fatto', text: data.message, timer: 1800, showConfirmButton: false })
.then(function() { location.reload(); });
} else {
btn.disabled = false; btn.innerHTML = orig;
Swal.fire('Errore', data.message || 'Errore.', 'error');
}
})
.catch(function() {
btn.disabled = false; btn.innerHTML = orig;
Swal.fire('Errore', 'Errore di connessione.', 'error');
});
});
});
</script>
</body> </body>
</html> </html>
+868
View File
@@ -0,0 +1,868 @@
<?php include('include/headscript.php'); ?>
<?php
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$userId = (int)($iduserlogin ?? 0);
if ($userId <= 0) {
die('Utente non valido.');
}
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (empty($_SESSION['user_settings_csrf'])) {
$_SESSION['user_settings_csrf'] = bin2hex(random_bytes(32));
}
$csrfToken = $_SESSION['user_settings_csrf'];
$successMessage = '';
$errorMessage = '';
// Load countries.
$countries = [];
try {
$stmtCountries = $pdo->query("
SELECT id, name, iso_3166_2
FROM auth_countries
ORDER BY name ASC
");
$countries = $stmtCountries->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
$countries = [];
}
// Load current user.
$stmtProfileUser = $pdo->prepare("
SELECT
id,
email,
password,
first_name,
last_name,
phone,
avatar,
address,
country_id,
birthday,
role_id,
status,
last_login
FROM auth_users
WHERE id = ?
LIMIT 1
");
$stmtProfileUser->execute([$userId]);
$profileUser = $stmtProfileUser->fetch(PDO::FETCH_ASSOC);
if (!$profileUser) {
die('Utente non trovato.');
}
function e($value)
{
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
}
function normalizeAvatarPath($avatar)
{
$avatar = trim((string)$avatar);
if ($avatar === '') {
return '';
}
// If the database already contains a complete relative path, use it as it is.
if (
str_starts_with($avatar, '../') ||
str_starts_with($avatar, './') ||
str_starts_with($avatar, '/') ||
str_starts_with($avatar, 'http://') ||
str_starts_with($avatar, 'https://')
) {
return $avatar;
}
// If the database contains only the filename, build the expected user upload path.
return '../upload/users/' . $avatar;
}
function getAvatarInitials($profileUser)
{
$first = trim((string)($profileUser['first_name'] ?? ''));
$last = trim((string)($profileUser['last_name'] ?? ''));
$email = trim((string)($profileUser['email'] ?? ''));
$initials = '';
if ($first !== '') {
$initials .= mb_substr($first, 0, 1);
}
if ($last !== '') {
$initials .= mb_substr($last, 0, 1);
}
if ($initials === '' && $email !== '') {
$initials = mb_substr($email, 0, 1);
}
return strtoupper($initials ?: 'U');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$postedToken = $_POST['csrf_token'] ?? '';
if (!hash_equals($csrfToken, $postedToken)) {
$errorMessage = 'Sessione non valida. Ricarica la pagina e riprova.';
} else {
$email = trim($_POST['email'] ?? '');
$firstName = trim($_POST['first_name'] ?? '');
$lastName = trim($_POST['last_name'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$address = trim($_POST['address'] ?? '');
$countryId = $_POST['country_id'] !== '' ? (int)$_POST['country_id'] : null;
$birthday = trim($_POST['birthday'] ?? '');
$currentPassword = $_POST['current_password'] ?? '';
$newPassword = $_POST['new_password'] ?? '';
$confirmPassword = $_POST['confirm_password'] ?? '';
$birthdayValue = null;
$avatarToSave = $profileUser['avatar'];
if ($birthday !== '') {
$dateObj = DateTime::createFromFormat('Y-m-d', $birthday);
if (!$dateObj || $dateObj->format('Y-m-d') !== $birthday) {
$errorMessage = 'La data di nascita non è valida.';
} else {
$birthdayValue = $birthday;
}
}
if (!$errorMessage && $email === '') {
$errorMessage = 'Lemail è obbligatoria.';
}
if (!$errorMessage && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errorMessage = 'Lemail inserita non è valida.';
}
// Check unique email.
if (!$errorMessage) {
$stmtCheckEmail = $pdo->prepare("
SELECT id
FROM auth_users
WHERE email = ? AND id <> ?
LIMIT 1
");
$stmtCheckEmail->execute([$email, $userId]);
if ($stmtCheckEmail->fetchColumn()) {
$errorMessage = 'Questa email è già utilizzata da un altro utente.';
}
}
// Avatar upload.
if (!$errorMessage && isset($_FILES['avatar']) && $_FILES['avatar']['error'] !== UPLOAD_ERR_NO_FILE) {
if ($_FILES['avatar']['error'] !== UPLOAD_ERR_OK) {
$errorMessage = 'Errore durante il caricamento dellavatar.';
} else {
$maxFileSize = 2 * 1024 * 1024; // 2 MB
if ($_FILES['avatar']['size'] > $maxFileSize) {
$errorMessage = 'Lavatar non può superare 2 MB.';
} else {
$tmpFile = $_FILES['avatar']['tmp_name'];
$originalName = $_FILES['avatar']['name'];
$allowedMimeTypes = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
'image/gif' => 'gif',
];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($tmpFile);
if (!array_key_exists($mimeType, $allowedMimeTypes)) {
$errorMessage = 'Formato avatar non valido. Sono consentiti JPG, PNG, WEBP o GIF.';
} else {
$uploadDir = __DIR__ . '/../upload/users/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$extension = $allowedMimeTypes[$mimeType];
$safeOriginalName = preg_replace('/[^A-Za-z0-9_\-\.]/', '_', pathinfo($originalName, PATHINFO_FILENAME));
$fileName = time() . '_' . $userId . '_' . $safeOriginalName . '.' . $extension;
$destination = $uploadDir . $fileName;
if (!move_uploaded_file($tmpFile, $destination)) {
$errorMessage = 'Impossibile salvare il file avatar.';
} else {
// Path used by pages inside userarea, for example:
// <img src="../upload/users/file.jpg">
$avatarToSave = $fileName;
}
}
}
}
}
$passwordToSave = null;
$wantsPasswordChange = ($currentPassword !== '' || $newPassword !== '' || $confirmPassword !== '');
if (!$errorMessage && $wantsPasswordChange) {
if ($currentPassword === '') {
$errorMessage = 'Inserisci la password attuale.';
} elseif ($newPassword === '') {
$errorMessage = 'Inserisci la nuova password.';
} elseif (strlen($newPassword) < 8) {
$errorMessage = 'La nuova password deve contenere almeno 8 caratteri.';
} elseif ($newPassword !== $confirmPassword) {
$errorMessage = 'La conferma password non corrisponde.';
} elseif (!password_verify($currentPassword, $profileUser['password'])) {
$errorMessage = 'La password attuale non è corretta.';
} else {
// Password is encrypted before saving.
$passwordToSave = password_hash($newPassword, PASSWORD_DEFAULT);
}
}
if (!$errorMessage) {
try {
$pdo->beginTransaction();
$stmtUpdate = $pdo->prepare("
UPDATE auth_users
SET
email = :email,
first_name = :first_name,
last_name = :last_name,
phone = :phone,
avatar = :avatar,
address = :address,
country_id = :country_id,
birthday = :birthday,
updated_at = NOW()
WHERE id = :id
LIMIT 1
");
$stmtUpdate->execute([
':email' => $email,
':first_name' => $firstName !== '' ? $firstName : null,
':last_name' => $lastName !== '' ? $lastName : null,
':phone' => $phone !== '' ? $phone : null,
':avatar' => $avatarToSave !== '' ? $avatarToSave : null,
':address' => $address !== '' ? $address : null,
':country_id' => $countryId,
':birthday' => $birthdayValue,
':id' => $userId,
]);
if ($passwordToSave !== null) {
$stmtPassword = $pdo->prepare("
UPDATE auth_users
SET password = ?, updated_at = NOW()
WHERE id = ?
LIMIT 1
");
$stmtPassword->execute([$passwordToSave, $userId]);
}
$pdo->commit();
$successMessage = $passwordToSave !== null
? 'Profilo, avatar e password aggiornati correttamente.'
: 'Profilo aggiornato correttamente.';
// Reload updated user.
$stmtProfileUser->execute([$userId]);
$profileUser = $stmtProfileUser->fetch(PDO::FETCH_ASSOC);
$_SESSION['user_settings_csrf'] = bin2hex(random_bytes(32));
$csrfToken = $_SESSION['user_settings_csrf'];
} catch (Exception $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
$errorMessage = 'Errore durante il salvataggio delle impostazioni.';
}
}
}
}
$avatarPath = normalizeAvatarPath($profileUser['avatar'] ?? '');
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>Impostazioni Utente <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<style>
body {
background: linear-gradient(135deg, #f3f6f8, #e8eef3);
font-family: 'Segoe UI', sans-serif;
color: #2b3e50;
}
.page-content {
padding: 1.4rem 1rem;
display: flex;
flex-direction: column;
align-items: center;
}
.settings-wrap {
width: 100%;
max-width: 1280px;
}
h3.settings-title {
text-align: center;
font-weight: 800;
color: #2b3e50;
margin-bottom: 1.2rem;
letter-spacing: 0.3px;
}
.settings-card {
background: #fff;
border-radius: 16px;
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
margin-bottom: 16px;
overflow: hidden;
}
.settings-header {
padding: 16px 18px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
display: flex;
align-items: center;
gap: 12px;
}
.settings-icon {
font-size: 1.8rem;
line-height: 1;
}
.settings-heading {
margin: 0;
font-weight: 800;
font-size: 1.1rem;
}
.settings-subtitle {
margin: 0;
color: #6b7a89;
font-size: 0.92rem;
}
.settings-body {
padding: 20px;
}
.profile-layout {
display: grid;
grid-template-columns: 280px 1fr;
gap: 24px;
align-items: flex-start;
}
.avatar-panel {
background: linear-gradient(135deg, #f7fbff, #edf5ff);
border: 1px solid #dbeafe;
border-radius: 18px;
padding: 20px;
text-align: center;
}
.avatar-box {
width: 132px;
height: 132px;
border-radius: 34px;
background: linear-gradient(135deg, #cde5ff, #dff0ff);
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
font-weight: 800;
color: #2b3e50;
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
overflow: hidden;
margin: 0 auto 14px auto;
}
.avatar-box img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-name {
font-size: 1.1rem;
font-weight: 800;
margin: 0;
color: #2b3e50;
}
.avatar-email {
color: #6b7a89;
margin: 4px 0 14px 0;
font-size: 0.92rem;
word-break: break-word;
}
.avatar-upload-label {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
border-radius: 14px;
padding: 11px 14px;
cursor: pointer;
font-weight: 800;
color: #2b3e50;
background: linear-gradient(135deg, #e5e7eb, #f3f4f6);
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
transition: all 0.2s ease;
margin-top: 8px;
}
.avatar-upload-label:hover {
transform: translateY(-2px);
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.13);
}
.avatar-upload-input {
display: none;
}
.avatar-help {
font-size: 0.82rem;
color: #6b7a89;
margin-top: 10px;
line-height: 1.35;
}
.profile-meta {
color: #6b7a89;
font-size: 0.88rem;
margin-top: 14px;
}
.form-label {
font-weight: 700;
color: #2b3e50;
margin-bottom: 6px;
}
.form-control,
.form-select {
border-radius: 12px;
border: 1px solid #d8e0e7;
padding: 10px 12px;
font-size: 0.95rem;
}
.form-control:focus,
.form-select:focus {
border-color: #8bbcf7;
box-shadow: 0 0 0 0.18rem rgba(139, 188, 247, 0.25);
}
.help-text {
font-size: 0.86rem;
color: #6b7a89;
margin-top: 6px;
}
.password-box {
background: linear-gradient(135deg, #fff7e6, #fffaf0);
border-radius: 16px;
padding: 16px;
border: 1px solid rgba(255, 184, 107, 0.45);
}
.btn-save-settings {
border: 0;
border-radius: 16px;
padding: 13px 24px;
font-size: 1rem;
font-weight: 800;
color: #fff;
background: linear-gradient(135deg, #61ce5dff, #61ce5dff);
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
transition: all 0.2s ease;
}
.btn-save-settings:hover {
transform: translateY(-2px);
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.13);
color: #fff;
}
.btn-back {
border: 0;
border-radius: 16px;
padding: 13px 20px;
font-size: 1rem;
font-weight: 800;
color: #2b3e50;
background: linear-gradient(135deg, #e5e7eb, #f3f4f6);
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
transition: all 0.2s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
}
.btn-back:hover {
transform: translateY(-2px);
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.13);
color: #2b3e50;
}
.alert {
border-radius: 16px;
border: 0;
font-weight: 600;
}
.readonly-note {
background: linear-gradient(135deg, #cde5ff, #dff0ff);
border-radius: 16px;
padding: 14px 16px;
color: #2b3e50;
font-weight: 600;
margin-bottom: 18px;
}
.actions-row {
display: flex;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-top: 18px;
}
.selected-file-name {
font-size: 0.84rem;
color: #2b3e50;
margin-top: 8px;
font-weight: 600;
word-break: break-word;
}
@media (max-width: 992px) {
.profile-layout {
grid-template-columns: 1fr;
}
.avatar-panel {
max-width: 420px;
margin: 0 auto;
width: 100%;
}
}
@media (max-width: 768px) {
.settings-body {
padding: 16px;
}
.actions-row {
flex-direction: column;
}
.btn-save-settings,
.btn-back {
width: 100%;
justify-content: center;
}
}
</style>
</head>
<body>
<div class="wrapper toggled">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="settings-wrap">
<h3 class="settings-title">Impostazioni Utente</h3>
<?php if ($successMessage): ?>
<div class="alert alert-success">
<?= e($successMessage); ?>
</div>
<?php endif; ?>
<?php if ($errorMessage): ?>
<div class="alert alert-danger">
<?= e($errorMessage); ?>
</div>
<?php endif; ?>
<form method="post" enctype="multipart/form-data" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= e($csrfToken); ?>">
<div class="settings-card">
<div class="settings-header">
<div class="settings-icon">👤</div>
<div>
<p class="settings-heading">Profilo personale</p>
<p class="settings-subtitle">Dati anagrafici, contatti e avatar utente</p>
</div>
</div>
<div class="settings-body">
<div class="readonly-note">
Ruolo, stato account e impostazioni di sicurezza avanzate non sono modificabili da questa pagina.
</div>
<div class="profile-layout">
<div class="avatar-panel">
<div class="avatar-box" id="avatarPreviewBox">
<?php if (!empty($avatarPath)): ?>
<img src="<?= e($avatarPath); ?>" class="user-img" alt="user avatar" id="avatarPreviewImage">
<?php else: ?>
<span id="avatarInitials"><?= e(getAvatarInitials($profileUser)); ?></span>
<?php endif; ?>
</div>
<p class="avatar-name">
<?= e(trim(($profileUser['first_name'] ?? '') . ' ' . ($profileUser['last_name'] ?? '')) ?: 'Utente'); ?>
</p>
<p class="avatar-email">
<?= e($profileUser['email']); ?>
</p>
<label for="avatar" class="avatar-upload-label">
Carica avatar
</label>
<input type="file"
class="avatar-upload-input"
id="avatar"
name="avatar"
accept="image/jpeg,image/png,image/webp,image/gif">
<div id="selectedFileName" class="selected-file-name"></div>
<div class="avatar-help">
Formati consentiti: JPG, PNG, WEBP, GIF.<br>
Dimensione massima: 2 MB.
</div>
<div class="profile-meta">
Stato account: <?= e($profileUser['status']); ?>
<?php if (!empty($profileUser['last_login'])): ?>
<br>Ultimo accesso: <?= e(date('d/m/Y H:i', strtotime($profileUser['last_login']))); ?>
<?php endif; ?>
</div>
</div>
<div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label" for="first_name">Nome</label>
<input type="text"
class="form-control"
id="first_name"
name="first_name"
value="<?= e($profileUser['first_name']); ?>">
</div>
<div class="col-md-6">
<label class="form-label" for="last_name">Cognome</label>
<input type="text"
class="form-control"
id="last_name"
name="last_name"
value="<?= e($profileUser['last_name']); ?>">
</div>
<div class="col-md-6">
<label class="form-label" for="email">Email</label>
<input type="email"
class="form-control"
id="email"
name="email"
value="<?= e($profileUser['email']); ?>"
required>
</div>
<div class="col-md-6">
<label class="form-label" for="phone">Telefono</label>
<input type="text"
class="form-control"
id="phone"
name="phone"
value="<?= e($profileUser['phone']); ?>">
</div>
<div class="col-md-6">
<label class="form-label" for="birthday">Data di nascita</label>
<input type="date"
class="form-control"
id="birthday"
name="birthday"
value="<?= e($profileUser['birthday']); ?>">
</div>
<div class="col-md-6">
<label class="form-label" for="country_id">Paese</label>
<select class="form-select" id="country_id" name="country_id">
<option value="">Seleziona...</option>
<?php foreach ($countries as $country): ?>
<option value="<?= (int)$country['id']; ?>"
<?= ((int)$profileUser['country_id'] === (int)$country['id']) ? 'selected' : ''; ?>>
<?= e($country['name'] . (!empty($country['iso_3166_2']) ? ' (' . $country['iso_3166_2'] . ')' : '')); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-12">
<label class="form-label" for="address">Indirizzo</label>
<input type="text"
class="form-control"
id="address"
name="address"
value="<?= e($profileUser['address']); ?>">
</div>
</div>
</div>
</div>
</div>
</div>
<div class="settings-card">
<div class="settings-header">
<div class="settings-icon">🔐</div>
<div>
<p class="settings-heading">Cambio password</p>
<p class="settings-subtitle">Compila questa sezione solo se vuoi modificare la password</p>
</div>
</div>
<div class="settings-body">
<div class="password-box">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label" for="current_password">Password attuale</label>
<input type="password"
class="form-control"
id="current_password"
name="current_password"
autocomplete="current-password">
</div>
<div class="col-md-4">
<label class="form-label" for="new_password">Nuova password</label>
<input type="password"
class="form-control"
id="new_password"
name="new_password"
autocomplete="new-password">
</div>
<div class="col-md-4">
<label class="form-label" for="confirm_password">Conferma nuova password</label>
<input type="password"
class="form-control"
id="confirm_password"
name="confirm_password"
autocomplete="new-password">
</div>
</div>
<div class="help-text">
Se lasci questi campi vuoti, la password attuale rimane invariata.
La nuova password deve avere almeno 8 caratteri.
</div>
</div>
<div class="actions-row">
<a href="production_dashboard.php" class="btn-back"> Torna alla dashboard</a>
<button type="submit" class="btn-save-settings">Salva impostazioni</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<?php include('include/footer.php'); ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const avatarInput = document.getElementById('avatar');
const previewBox = document.getElementById('avatarPreviewBox');
const selectedFileName = document.getElementById('selectedFileName');
if (!avatarInput || !previewBox) {
return;
}
avatarInput.addEventListener('change', function() {
const file = this.files && this.files[0] ? this.files[0] : null;
if (!file) {
selectedFileName.textContent = '';
return;
}
selectedFileName.textContent = file.name;
if (!file.type.startsWith('image/')) {
return;
}
const reader = new FileReader();
reader.onload = function(event) {
previewBox.innerHTML = '';
const img = document.createElement('img');
img.src = event.target.result;
img.className = 'user-img';
img.alt = 'user avatar';
previewBox.appendChild(img);
};
reader.readAsDataURL(file);
});
});
</script>
</body>
</html>
-18
View File
@@ -1,18 +0,0 @@
; This file is for unifying the coding style for different editors and IDEs.
; More information at https://editorconfig.org
root = true
[*]
charset = utf-8
indent_size = 4
indent_style = space
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.yml]
indent_size = 2
@@ -1,25 +0,0 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text eol=lf
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.c text
*.h text
# Declare files that will always have CRLF line endings on checkout.
*.sln text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary
*.otf binary
*.eot binary
*.svg binary
*.ttf binary
*.woff binary
*.woff2 binary
*.css linguist-vendored
*.scss linguist-vendored
*.js linguist-vendored
CHANGELOG.md export-ignore
@@ -1,37 +0,0 @@
name: Tests
on: [push, pull_request]
jobs:
tests:
name: PHP ${{ matrix.php }}
runs-on: ubuntu-latest
strategy:
matrix:
php: ['7.3', '7.4', '8.0', '8.1']
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Cache composer
uses: actions/cache@v1
with:
path: ~/.composer/cache/files
key: php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extension-csv: bcmath, ctype, dom, fileinfo, intl, gd, json, mbstring, pdo, pdo_sqlite, openssl, sqlite, xml, zip
coverage: none
- name: Install composer
run: composer install --no-interaction --no-scripts --no-suggest --prefer-source
- name: Execute tests
run: vendor/bin/phpunit
@@ -1,9 +0,0 @@
/.idea
/.history
/.vscode
/tests/databases
/vendor
.DS_Store
.phpunit.result.cache
composer.phar
composer.lock
-4
View File
@@ -1,4 +0,0 @@
preset: psr2
enabled:
- concat_with_spaces
-23
View File
@@ -1,23 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Andreas Lutro
Copyright (c) 2017 Akaunting
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-186
View File
@@ -1,186 +0,0 @@
# Persistent settings package for Laravel
[![Downloads](https://poser.pugx.org/akaunting/laravel-setting/d/total.svg)](https://github.com/akaunting/laravel-setting)
[![StyleCI](https://styleci.io/repos/101231817/shield?style=flat&branch=master)](https://styleci.io/repos/101231817)
[![License](https://poser.pugx.org/akaunting/laravel-setting/license.svg)](LICENSE.md)
This package allows you to save settings in a more persistent way. You can use the database and/or json file to save your settings. You can also override the Laravel config.
* Driver support
* Helper function
* Blade directive
* Override config values
* Encryption
* Custom file, table and columns
* Auto save
* Extra columns
* Cache support
## Getting Started
### 1. Install
Run the following command:
```bash
composer require akaunting/laravel-setting
```
### 2. Register (for Laravel < 5.5)
Register the service provider in `config/app.php`
```php
Akaunting\Setting\Provider::class,
```
Add alias if you want to use the facade.
```php
'Setting' => Akaunting\Setting\Facade::class,
```
### 3. Publish
Publish config file.
```bash
php artisan vendor:publish --tag=setting
```
### 4. Database
Create table for database driver
```bash
php artisan migrate
```
### 5. Configure
You can change the options of your app from `config/setting.php` file
## Usage
You can either use the helper method like `setting('foo')` or the facade `Setting::get('foo')`
### Facade
```php
Setting::get('foo', 'default');
Setting::get('nested.element');
Setting::set('foo', 'bar');
Setting::forget('foo');
$settings = Setting::all();
```
### Helper
```php
setting('foo', 'default');
setting('nested.element');
setting(['foo' => 'bar']);
setting()->forget('foo');
$settings = setting()->all();
```
You can call the `save()` method to save the changes.
### Auto Save
If you enable the `auto_save` option in the config file, settings will be saved automatically every time the application shuts down if anything has been changed.
### Blade Directive
You can get the settings directly in your blade templates using the helper method or the blade directive like `@setting('foo')`
### Override Config Values
You can easily override default config values by adding them to the `override` option in `config/setting.php`, thereby eliminating the need to modify the default config files and also allowing you to change said values during production. Ex:
```php
'override' => [
"app.name" => "app_name",
"app.env" => "app_env",
"mail.driver" => "app_mail_driver",
"mail.host" => "app_mail_host",
],
```
The values on the left corresponds to the respective config value (Ex: config('app.name')) and the value on the right is the name of the `key` in your settings table/json file.
### Encryption
If you like to encrypt the values for a given key, you can pass the key to the `encrypted_keys` option in `config/setting.php` and the rest is automatically handled by using Laravel's built-in encryption facilities. Ex:
```php
'encrypted_keys' => [
"payment.key",
],
```
### JSON Storage
You can modify the path used on run-time using `setting()->setPath($path)`.
### Database Storage
If you want to use the database as settings storage then you should run the `php artisan migrate`. You can modify the table fields from the `create_settings_table` file in the migrations directory.
#### Extra Columns
If you want to store settings for multiple users/clients in the same database you can do so by specifying extra columns:
```php
setting()->setExtraColumns(['user_id' => Auth::user()->id]);
```
where `user_id = x` will now be added to the database query when settings are retrieved, and when new settings are saved, the `user_id` will be populated.
If you need more fine-tuned control over which data gets queried, you can use the `setConstraint` method which takes a closure with two arguments:
- `$query` is the query builder instance
- `$insert` is a boolean telling you whether the query is an insert or not. If it is an insert, you usually don't need to do anything to `$query`.
```php
setting()->setConstraint(function($query, $insert) {
if ($insert) return;
$query->where(/* ... */);
});
```
### Custom Drivers
This package uses the Laravel `Manager` class under the hood, so it's easy to add your own storage driver. All you need to do is extend the abstract `Driver` class, implement the abstract methods and call `setting()->extend`.
```php
class MyDriver extends Akaunting\Setting\Contracts\Driver
{
// ...
}
app('setting.manager')->extend('mydriver', function($app) {
return $app->make('MyDriver');
});
```
## Changelog
Please see [Releases](../../releases) for more information what has changed recently.
## Contributing
Pull requests are more than welcome. You must follow the PSR coding standards.
## Security
If you discover any security related issues, please email security@akaunting.com instead of using the issue tracker.
## Credits
- [Denis Duliçi](https://github.com/denisdulici)
- [All Contributors](../../contributors)
## License
The MIT License (MIT). Please see [LICENSE](LICENSE.md) for more information.
-48
View File
@@ -1,48 +0,0 @@
{
"name": "akaunting/laravel-setting",
"description": "Persistent settings package for Laravel",
"keywords": [
"laravel",
"persistent",
"settings",
"config"
],
"license": "MIT",
"authors": [
{
"name": "Denis Duliçi",
"email": "info@akaunting.com",
"homepage": "https://akaunting.com",
"role": "Developer"
}
],
"require": {
"php": ">=5.5.9",
"laravel/framework": ">=5.3"
},
"require-dev": {
"phpunit/phpunit": ">=4.8",
"mockery/mockery": "0.9.*",
"laravel/framework": ">=5.3"
},
"autoload": {
"psr-4": {
"Akaunting\\Setting\\": "./src"
},
"files": [
"src/helpers.php"
]
},
"extra": {
"laravel": {
"providers": [
"Akaunting\\Setting\\Provider"
],
"aliases": {
"Setting": "Akaunting\\Setting\\Facade"
}
}
},
"minimum-stability": "dev",
"prefer-stable": true
}
-18
View File
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
>
<testsuites>
<testsuite name="Package Test Suite">
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
</phpunit>
-132
View File
@@ -1,132 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Enable / Disable auto save
|--------------------------------------------------------------------------
|
| Auto-save every time the application shuts down
|
*/
'auto_save' => false,
/*
|--------------------------------------------------------------------------
| Cache
|--------------------------------------------------------------------------
|
| Options for caching. Set whether to enable cache, its key, time to live
| in seconds and whether to auto clear after save.
|
*/
'cache' => [
'enabled' => false,
'key' => 'setting',
'ttl' => 3600,
'auto_clear' => true,
],
/*
|--------------------------------------------------------------------------
| Setting driver
|--------------------------------------------------------------------------
|
| Select where to store the settings.
|
| Supported: "database", "json", "memory"
|
*/
'driver' => 'database',
/*
|--------------------------------------------------------------------------
| Database driver
|--------------------------------------------------------------------------
|
| Options for database driver. Enter which connection to use, null means
| the default connection. Set the table and column names.
|
*/
'database' => [
'connection' => null,
'table' => 'settings',
'key' => 'key',
'value' => 'value',
],
/*
|--------------------------------------------------------------------------
| JSON driver
|--------------------------------------------------------------------------
|
| Options for json driver. Enter the full path to the .json file.
|
*/
'json' => [
'path' => storage_path() . '/settings.json',
],
/*
|--------------------------------------------------------------------------
| Override application config values
|--------------------------------------------------------------------------
|
| If defined, settings package will override these config values.
|
| Sample:
| "app.locale" => "settings.locale",
|
*/
'override' => [
],
/*
|--------------------------------------------------------------------------
| Fallback
|--------------------------------------------------------------------------
|
| Define fallback settings to be used in case the default is null
|
| Sample:
| "currency" => "USD",
|
*/
'fallback' => [
],
/*
|--------------------------------------------------------------------------
| Required Extra Columns
|--------------------------------------------------------------------------
|
| The list of columns required to be set up
|
| Sample:
| "user_id",
| "tenant_id",
|
*/
'required_extra_columns' => [
],
/*
|--------------------------------------------------------------------------
| Encryption
|--------------------------------------------------------------------------
|
| Define the keys which should be crypt automatically.
|
| Sample:
| "payment.key"
|
*/
'encrypted_keys' => [
],
];
@@ -1,321 +0,0 @@
<?php
namespace Akaunting\Setting\Contracts;
use Akaunting\Setting\Support\Arr;
use Illuminate\Support\Facades\Cache;
abstract class Driver
{
/**
* The settings data.
*
* @var array
*/
protected $data = [];
/**
* Whether the store has changed since it was last loaded.
*
* @var bool
*/
protected $unsaved = false;
/**
* Whether the settings data are loaded.
*
* @var bool
*/
protected $loaded = false;
/**
* Include and merge with fallbacks
*
* @var bool
*/
protected $with_fallback = true;
/**
* Excludes fallback data
*/
public function withoutFallback()
{
$this->with_fallback = false;
return $this;
}
/**
* Get a specific key from the settings data.
*
* @param string|array $key
* @param mixed $default Optional default value.
*
* @return mixed
*/
public function get($key, $default = null)
{
if (!$this->checkExtraColumns()) {
return false;
}
$this->load();
return Arr::get($this->data, $key, $default);
}
/**
* Get the fallback value if default is null.
*
* @param string|array $key
* @param mixed $default
*
* @return mixed
*/
public function getFallback($key, $default = null)
{
if (($default !== null) || is_array($key)) {
return $default;
}
return Arr::get((array) config('setting.fallback'), $key);
}
/**
* Check if the given value is same as fallback.
*
* @param string $key
* @param string $value
*
* @return bool
*/
public function isEqualToFallback($key, $value)
{
return (string) $this->getFallback($key) == (string) $value;
}
/**
* Determine if a key exists in the settings data.
*
* @param string $key
*
* @return bool
*/
public function has($key)
{
if (!$this->checkExtraColumns()) {
return false;
}
$this->load();
return Arr::has($this->data, $key);
}
/**
* Set a specific key to a value in the settings data.
*
* @param string|array $key Key string or associative array of key => value
* @param mixed $value Optional only if the first argument is an array
*/
public function set($key, $value = null)
{
if (!$this->checkExtraColumns()) {
return;
}
$this->load();
$this->unsaved = true;
if (is_array($key)) {
foreach ($key as $k => $v) {
Arr::set($this->data, $k, $v);
}
} else {
Arr::set($this->data, $key, $value);
}
}
/**
* Unset a key in the settings data.
*
* @param string $key
*/
public function forget($key)
{
if (!$this->checkExtraColumns()) {
return;
}
$this->unsaved = true;
if ($this->has($key)) {
Arr::forget($this->data, $key);
}
}
/**
* Unset all keys in the settings data.
*
* @return void
*/
public function forgetAll()
{
if (!$this->checkExtraColumns()) {
return;
}
if (config('setting.cache.enabled')) {
Cache::forget($this->getCacheKey());
}
$this->unsaved = true;
$this->data = [];
}
/**
* Get all settings data.
*
* @return array|bool
*/
public function all()
{
if (!$this->checkExtraColumns()) {
return [];
}
$this->load();
return $this->data;
}
/**
* Save any changes done to the settings data.
*
* @return void
*/
public function save()
{
if (!$this->checkExtraColumns()) {
return;
}
if (!$this->unsaved) {
// either nothing has been changed, or data has not been loaded, so
// do nothing by returning early
return;
}
if (config('setting.cache.enabled') && config('setting.cache.auto_clear')) {
Cache::forget($this->getCacheKey());
}
$this->write($this->data);
$this->unsaved = false;
}
/**
* Make sure data is loaded.
*
* @param $force Force a reload of data. Default false.
*/
public function load($force = false)
{
if (!$this->checkExtraColumns()) {
return;
}
if ($this->loaded && !$force) {
return;
}
$fallback_data = $this->with_fallback ? config('setting.fallback') : [];
$driver_data = $this->readData();
$this->data = Arr::merge((array) $fallback_data, (array) $driver_data);
$this->loaded = true;
}
/**
* Read data from driver or cache
*
* @return array
*/
public function readData()
{
if (config('setting.cache.enabled')) {
return $this->readDataFromCache();
}
return $this->read();
}
/**
* Read data from cache
*
* @return array
*/
public function readDataFromCache()
{
return Cache::remember($this->getCacheKey(), config('setting.cache.ttl'), function () {
return $this->read();
});
}
/**
* Check if extra columns are set up.
*
* @return boolean
*/
public function checkExtraColumns()
{
if (!$required_extra_columns = config('setting.required_extra_columns')) {
return true;
}
if (array_keys_exists($required_extra_columns, $this->getExtraColumns())) {
return true;
}
return false;
}
/**
* Get cache key based on extra columns.
*
* @return string
*/
public function getCacheKey()
{
$key = config('setting.cache.key');
foreach ($this->getExtraColumns() as $name => $value) {
$key .= '_' . $name . '_' . $value;
}
return $key;
}
/**
* Get extra columns added to the rows.
*
* @return array
*/
abstract protected function getExtraColumns();
/**
* Read data from driver.
*
* @return array
*/
abstract protected function read();
/**
* Write data to driver.
*
* @param array $data
*
* @return void
*/
abstract protected function write(array $data);
}
@@ -1,372 +0,0 @@
<?php
namespace Akaunting\Setting\Drivers;
use Akaunting\Setting\Contracts\Driver;
use Akaunting\Setting\Support\Arr;
use Closure;
use Illuminate\Database\Connection;
use Illuminate\Support\Arr as LaravelArr;
use Illuminate\Support\Facades\Crypt;
class Database extends Driver
{
/**
* The database connection instance.
*
* @var \Illuminate\Database\Connection
*/
protected $connection;
/**
* The table to query from.
*
* @var string
*/
protected $table;
/**
* The key column name to query from.
*
* @var string
*/
protected $key;
/**
* The value column name to query from.
*
* @var string
*/
protected $value;
/**
* Keys which should be encrypt automatically.
*
* @var array
*/
protected $encrypted_keys;
/**
* Any query constraints that should be applied.
*
* @var Closure|null
*/
protected $query_constraint;
/**
* Any extra columns that should be added to the rows.
*
* @var array
*/
protected $extra_columns = [];
/**
* @param \Illuminate\Database\Connection $connection
* @param string $table
*/
public function __construct(Connection $connection, $table = null, $key = null, $value = null, array $encrypted_keys = [])
{
$this->connection = $connection;
$this->table = $table ?: 'settings';
$this->key = $key ?: 'key';
$this->value = $value ?: 'value';
$this->encrypted_keys = $encrypted_keys;
}
/**
* Set the table to query from.
*
* @param string $table
*/
public function setTable($table)
{
$this->table = $table;
}
/**
* Set the key column name to query from.
*
* @param string $key
*/
public function setKey($key)
{
$this->key = $key;
}
/**
* Set the value column name to query from.
*
* @param string $value
*/
public function setValue($value)
{
$this->value = $value;
}
/**
* Set the query constraint.
*
* @param Closure $callback
*/
public function setConstraint(Closure $callback)
{
$this->data = [];
$this->loaded = false;
$this->query_constraint = $callback;
}
/**
* Set extra columns to be added to the rows.
*
* @param array $columns
*/
public function setExtraColumns(array $columns)
{
$this->extra_columns = $columns;
}
/**
* Get extra columns added to the rows.
*
* @return array
*/
public function getExtraColumns()
{
return $this->extra_columns;
}
/**
* {@inheritdoc}
*/
public function forget($key)
{
parent::forget($key);
// because the database driver cannot store empty arrays, remove empty
// arrays to keep data consistent before and after saving
$segments = explode('.', $key);
array_pop($segments);
while ($segments) {
$segment = implode('.', $segments);
// non-empty array - exit out of the loop
if ($this->get($segment)) {
break;
}
// remove the empty array and move on to the next segment
$this->forget($segment);
array_pop($segments);
}
}
/**
* {@inheritdoc}
*/
protected function write(array $data)
{
// Get current data
$db_data = $this->newQuery()->get([$this->key, $this->value])->toArray();
$insert_data = LaravelArr::dot($data);
$update_data = [];
$delete_keys = [];
foreach ($db_data as $db_row) {
$key = $db_row->{$this->key};
$value = $db_row->{$this->value};
$is_in_insert = $is_different_in_db = $is_same_as_fallback = false;
if (isset($insert_data[$key])) {
$is_in_insert = true;
$is_different_in_db = (string) $insert_data[$key] != (string) $value;
$is_same_as_fallback = $this->isEqualToFallback($key, $insert_data[$key]);
}
if ($is_in_insert) {
if ($is_same_as_fallback) {
// Delete if new data is same as fallback
$delete_keys[] = $key;
} elseif ($is_different_in_db) {
// Update if new data is different from db
$update_data[$key] = $insert_data[$key];
}
} else {
// Delete if current db not available in new data
$delete_keys[] = $key;
}
unset($insert_data[$key]);
}
foreach ($update_data as $key => $value) {
$value = $this->prepareValue($key, $value);
$this->newQuery()
->where($this->key, '=', $key)
->update([$this->value => $value]);
}
if ($insert_data) {
$this->newQuery(true)
->insert($this->prepareInsertData($insert_data));
}
if ($delete_keys) {
$this->newQuery()
->whereIn($this->key, $delete_keys)
->delete();
}
}
/**
* Transforms settings data into an array ready to be insterted into the
* database. Call array_dot on a multidimensional array before passing it
* into this method!
*
* @param array $data Call array_dot on a multidimensional array before passing it into this method!
*
* @return array
*/
protected function prepareInsertData(array $data)
{
$db_data = [];
if ($this->getExtraColumns()) {
foreach ($data as $key => $value) {
$value = $this->prepareValue($key, $value);
// Don't insert if same as fallback
if ($this->isEqualToFallback($key, $value)) {
continue;
}
$db_data[] = array_merge(
$this->getExtraColumns(),
[$this->key => $key, $this->value => $value]
);
}
} else {
foreach ($data as $key => $value) {
$value = $this->prepareValue($key, $value);
// Don't insert if same as fallback
if ($this->isEqualToFallback($key, $value)) {
continue;
}
$db_data[] = [$this->key => $key, $this->value => $value];
}
}
return $db_data;
}
/**
* Checks if the provided key should be encrypted or not.
* Also type casts the given value to a string so errors with booleans or integers are handeled.
* Otherwise it returns the original value.
*
* @param string $key Key to check if it's inside the encryptedValues variable.
* @param mixed $value Info: Encryption only supports strings.
*
* @return string
*/
protected function prepareValue(string $key, $value)
{
// Check if key should be encrypted
if (in_array($key, $this->encrypted_keys)) {
// Cast to string to avoid error when a user passes a boolean value
return Crypt::encryptString((string) $value);
}
return $value;
}
/**
* Checks if the provided key should be decrypted or not.
* Otherwise it returns the original value.
*
* @param string $key Key to check if it's inside the encryptedValues variable.
* @param mixed $value Info: Encryption only supports strings.
*
* @return string
*/
protected function unpackValue(string $key, $value)
{
// Check if key should be encrypted
if (in_array($key, $this->encrypted_keys)) {
// Cast to string to avoid error when a user passes a boolean value
return Crypt::decryptString((string) $value);
}
return $value;
}
/**
* {@inheritdoc}
*/
protected function read()
{
return $this->parseReadData($this->newQuery()->get());
}
/**
* Parse data coming from the database.
*
* @param array $data
*
* @return array
*/
public function parseReadData($data)
{
$results = [];
foreach ($data as $row) {
if (is_array($row)) {
$key = $row[$this->key];
$value = $row[$this->value];
} elseif (is_object($row)) {
$key = $row->{$this->key};
$value = $row->{$this->value};
} else {
$msg = 'Expected array or object, got ' . gettype($row);
throw new \UnexpectedValueException($msg);
}
// Encryption
$value = $this->unpackValue($key, $value);
Arr::set($results, $key, $value);
}
return $results;
}
/**
* Create a new query builder instance.
*
* @param bool $insert
*
* @return \Illuminate\Database\Query\Builder
*/
protected function newQuery($insert = false)
{
$query = $this->connection->table($this->table);
if (!$insert) {
foreach ($this->getExtraColumns() as $key => $value) {
$query->where($key, '=', $value);
}
}
if ($this->query_constraint !== null) {
$callback = $this->query_constraint;
$callback($query, $insert);
}
return $query;
}
}
-80
View File
@@ -1,80 +0,0 @@
<?php
namespace Akaunting\Setting\Drivers;
use Akaunting\Setting\Contracts\Driver;
use Illuminate\Filesystem\Filesystem;
class Json extends Driver
{
/**
* @param \Illuminate\Filesystem\Filesystem $files
* @param string $path
*/
public function __construct(Filesystem $files, $path = null)
{
$this->files = $files;
$this->setPath($path ?: storage_path() . '/settings.json');
}
/**
* Set the path for the JSON file.
*
* @param string $path
*/
public function setPath($path)
{
// If the file does not already exist, we will attempt to create it.
if (!$this->files->exists($path)) {
$result = $this->files->put($path, '{}');
if ($result === false) {
throw new \InvalidArgumentException("Could not write to $path.");
}
}
if (!$this->files->isWritable($path)) {
throw new \InvalidArgumentException("$path is not writable.");
}
$this->path = $path;
}
/**
* {@inheritdoc}
*/
protected function getExtraColumns()
{
return [];
}
/**
* {@inheritdoc}
*/
protected function read()
{
$contents = $this->files->get($this->path);
$data = json_decode($contents, true);
if ($data === null) {
throw new \RuntimeException("Invalid JSON in {$this->path}");
}
return $data;
}
/**
* {@inheritdoc}
*/
protected function write(array $data)
{
if ($data) {
$contents = json_encode($data);
} else {
$contents = '{}';
}
$this->files->put($this->path, $contents);
}
}
-42
View File
@@ -1,42 +0,0 @@
<?php
namespace Akaunting\Setting\Drivers;
use Akaunting\Setting\Contracts\Driver;
class Memory extends Driver
{
/**
* @param array $data
*/
public function __construct(array $data = null)
{
if ($data) {
$this->data = $data;
}
}
/**
* {@inheritdoc}
*/
protected function getExtraColumns()
{
return [];
}
/**
* {@inheritdoc}
*/
protected function read()
{
return $this->data;
}
/**
* {@inheritdoc}
*/
protected function write(array $data)
{
// do nothing
}
}
-16
View File
@@ -1,16 +0,0 @@
<?php
namespace Akaunting\Setting;
use Illuminate\Support\Facades\Facade as BaseFacade;
class Facade extends BaseFacade
{
/**
* Get the registered name of the component.
*/
public static function getFacadeAccessor()
{
return 'setting';
}
}
-63
View File
@@ -1,63 +0,0 @@
<?php
namespace Akaunting\Setting;
use Akaunting\Setting\Drivers\Database;
use Akaunting\Setting\Drivers\Json;
use Akaunting\Setting\Drivers\Memory;
use Illuminate\Support\Manager as BaseManager;
class Manager extends BaseManager
{
/**
* The container instance.
*
* @var \Illuminate\Contracts\Container\Container
*/
protected $container;
/**
* The application instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
*/
public function __construct($app = null)
{
$this->container = $app ?? app();
parent::__construct($this->container);
}
public function getDefaultDriver()
{
return config('setting.driver');
}
public function createJsonDriver()
{
$path = config('setting.json.path');
return new Json($this->container['files'], $path);
}
public function createDatabaseDriver()
{
$connection = $this->container['db']->connection(config('setting.database.connection'));
$table = config('setting.database.table');
$key = config('setting.database.key');
$value = config('setting.database.value');
$encryptedKeys = config('setting.encrypted_keys');
return new Database($connection, $table, $key, $value, $encryptedKeys);
}
public function createMemoryDriver()
{
return new Memory();
}
public function createArrayDriver()
{
return $this->createMemoryDriver();
}
}
@@ -1,33 +0,0 @@
<?php
namespace Akaunting\Setting\Middleware;
use Closure;
class AutoSaveSetting
{
/**
* Create a new save settings middleware.
*/
public function __construct()
{
$this->setting = app('setting');
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
$response = $next($request);
$this->setting->save();
return $response;
}
}
@@ -1,42 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateSettingsTable extends Migration
{
/**
* Set up the options.
*/
public function __construct()
{
$this->table = config('setting.database.table');
$this->key = config('setting.database.key');
$this->value = config('setting.database.value');
}
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create($this->table, function (Blueprint $table) {
$table->increments('id');
$table->string($this->key)->index();
$table->text($this->value);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop($this->table);
}
}
-74
View File
@@ -1,74 +0,0 @@
<?php
namespace Akaunting\Setting;
use Akaunting\Setting\Middleware\AutoSaveSetting;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Arr;
use Illuminate\View\Compilers\BladeCompiler;
class Provider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
$this->publishes([
__DIR__ . '/Config/setting.php' => config_path('setting.php'),
__DIR__ . '/Migrations/2017_08_24_000000_create_settings_table.php' => database_path('migrations/2017_08_24_000000_create_settings_table.php'),
], 'setting');
// Auto save setting
if (config('setting.auto_save')) {
$kernel = $this->app['Illuminate\Contracts\Http\Kernel'];
$kernel->pushMiddleware(AutoSaveSetting::class);
}
$this->override();
// Register blade directive
$this->callAfterResolving('blade.compiler', function (BladeCompiler $compiler) {
$compiler->directive('setting', function ($expression) {
return "<?php echo setting($expression); ?>";
});
});
}
/**
* Register the application services.
*
* @return void
*/
public function register()
{
$this->app->singleton('setting.manager', function ($app) {
return new Manager($app);
});
$this->app->singleton('setting', function ($app) {
return $app['setting.manager']->driver();
});
$this->mergeConfigFrom(__DIR__ . '/Config/setting.php', 'setting');
}
private function override()
{
$override = config('setting.override', []);
foreach (Arr::dot($override) as $config_key => $setting_key) {
$config_key = is_string($config_key) ? $config_key : $setting_key;
try {
if (! is_null($value = setting($setting_key))) {
config([$config_key => $value]);
}
} catch (\Exception $e) {
continue;
}
}
}
}
-164
View File
@@ -1,164 +0,0 @@
<?php
namespace Akaunting\Setting\Support;
class Arr
{
/**
* This class is a static class and should not be instantiated.
*/
private function __construct()
{
//
}
/**
* Get an element from an array.
*
* @param array $data
* @param string $key Specify a nested element by separating keys with full stops.
* @param mixed $default If the element is not found, return this.
*
* @return mixed
*/
public static function get(array $data, $key, $default = null)
{
if ($key === null) {
return $data;
}
if (is_array($key)) {
return static::getArray($data, $key, $default);
}
foreach (explode('.', $key) as $segment) {
if (!is_array($data)) {
return $default;
}
if (!array_key_exists($segment, $data)) {
return $default;
}
$data = $data[$segment];
}
return $data;
}
protected static function getArray(array $input, $keys, $default = null)
{
$output = [];
foreach ($keys as $key) {
static::set($output, $key, static::get($input, $key, $default));
}
return $output;
}
/**
* Determine if an array has a given key.
*
* @param array $data
* @param string $key
*
* @return bool
*/
public static function has(array $data, $key)
{
foreach (explode('.', $key) as $segment) {
if (!is_array($data)) {
return false;
}
if (!array_key_exists($segment, $data)) {
return false;
}
$data = $data[$segment];
}
return true;
}
/**
* Set an element of an array.
*
* @param array $data
* @param string $key Specify a nested element by separating keys with full stops.
* @param mixed $value
*/
public static function set(array &$data, $key, $value)
{
$segments = explode('.', $key);
$key = array_pop($segments);
// iterate through all of $segments except the last one
foreach ($segments as $segment) {
if (!array_key_exists($segment, $data)) {
$data[$segment] = array();
} elseif (!is_array($data[$segment])) {
throw new \UnexpectedValueException('Non-array segment encountered');
}
$data = &$data[$segment];
}
$data[$key] = $value;
}
/**
* Unset an element from an array.
*
* @param array &$data
* @param string $key Specify a nested element by separating keys with full stops.
*/
public static function forget(array &$data, $key)
{
$segments = explode('.', $key);
$key = array_pop($segments);
// iterate through all of $segments except the last one
foreach ($segments as $segment) {
if (!array_key_exists($segment, $data)) {
return;
} elseif (!is_array($data[$segment])) {
throw new \UnexpectedValueException('Non-array segment encountered');
}
$data = &$data[$segment];
}
unset($data[$key]);
}
/**
* Merge two multidimensional arrays recursive
*
* @param array $array_1
* @param array $array_2
*
* @return array
*/
public static function merge(array $array_1, array $array_2)
{
$merged = $array_1;
foreach ($array_2 as $key => $value) {
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
$merged[$key] = static::merge($merged[$key], $value);
} elseif (is_numeric($key)) {
if (!in_array($value, $merged)) {
$merged[] = $value;
}
} else {
$merged[$key] = $value;
}
}
return $merged;
}
}
-45
View File
@@ -1,45 +0,0 @@
<?php
if (!function_exists('array_keys_exists')) {
/**
* Easily check if multiple array keys exist.
*
* @param array $keys
* @param array $arr
*
* @return boolean
*/
function array_keys_exists(array $keys, array $arr)
{
return !array_diff_key(array_flip($keys), $arr);
}
}
if (!function_exists('setting')) {
/**
* Get / set the specified setting value.
*
* If an array is passed as the key, we will assume you want to set an array of values.
*
* @param array|string $key
* @param mixed $default
*
* @return mixed
*/
function setting($key = null, $default = null)
{
$setting = app('setting');
if (is_null($key)) {
return $setting;
}
if (is_array($key)) {
$setting->set($key);
return $setting;
}
return $setting->get($key, $default);
}
}
@@ -1,118 +0,0 @@
<?php
use Akaunting\Setting\Drivers\Database;
abstract class AbstractFunctionalTest extends PHPUnit_Framework_TestCase
{
abstract protected function createStore(array $data = []);
protected function assertStoreEquals($store, $expected, $message = null)
{
$this->assertEquals($expected, $store->all(), $message);
$store->save();
$store = $this->createStore();
$this->assertEquals($expected, $store->all(), $message);
}
protected function assertStoreKeyEquals($store, $key, $expected, $message = null)
{
$this->assertEquals($expected, $store->get($key), $message);
$store->save();
$store = $this->createStore();
$this->assertEquals($expected, $store->get($key), $message);
}
/** @test */
public function store_is_initially_empty()
{
$store = $this->createStore();
$this->assertEquals([], $store->all());
}
/** @test */
public function written_changes_are_saved()
{
$store = $this->createStore();
$store->set('foo', 'bar');
$this->assertStoreKeyEquals($store, 'foo', 'bar');
}
/** @test */
public function nested_keys_are_nested()
{
$store = $this->createStore();
$store->set('foo.bar', 'baz');
$this->assertStoreEquals($store, ['foo' => ['bar' => 'baz']]);
}
/** @test */
public function cannot_set_nested_key_on_non_array_member()
{
$store = $this->createStore();
$store->set('foo', 'bar');
$this->setExpectedException('UnexpectedValueException', 'Non-array segment encountered');
$store->set('foo.bar', 'baz');
}
/** @test */
public function can_forget_key()
{
$store = $this->createStore();
$store->set('foo', 'bar');
$store->set('bar', 'baz');
$this->assertStoreEquals($store, ['foo' => 'bar', 'bar' => 'baz']);
$store->forget('foo');
$this->assertStoreEquals($store, ['bar' => 'baz']);
}
/** @test */
public function can_forget_nested_key()
{
$store = $this->createStore();
$store->set('foo.bar', 'baz');
$store->set('foo.baz', 'bar');
$store->set('bar.foo', 'baz');
$this->assertStoreEquals($store, [
'foo' => [
'bar' => 'baz',
'baz' => 'bar',
],
'bar' => [
'foo' => 'baz',
],
]);
$store->forget('foo.bar');
$this->assertStoreEquals($store, [
'foo' => [
'baz' => 'bar',
],
'bar' => [
'foo' => 'baz',
],
]);
$store->forget('bar.foo');
$expected = [
'foo' => [
'baz' => 'bar',
],
'bar' => [
],
];
if ($store instanceof Database) {
unset($expected['bar']);
}
$this->assertStoreEquals($store, $expected);
}
/** @test */
public function can_forget_all()
{
$store = $this->createStore(['foo' => 'bar']);
$this->assertStoreEquals($store, ['foo' => 'bar']);
$store->forgetAll();
$this->assertStoreEquals($store, []);
}
}
@@ -1,43 +0,0 @@
<?php
class DatabaseTest extends AbstractFunctionalTest
{
public function setUp()
{
$this->container = new \Illuminate\Container\Container();
$this->capsule = new \Illuminate\Database\Capsule\Manager($this->container);
$this->capsule->setAsGlobal();
$this->container['db'] = $this->capsule;
$this->capsule->addConnection([
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]);
$this->capsule->schema()->create('settings', function ($t) {
$t->string('key', 64)->unique();
$t->string('value', 4096);
});
}
public function tearDown()
{
$this->capsule->schema()->drop('settings');
unset($this->capsule);
unset($this->container);
}
protected function createStore(array $data = [])
{
if ($data) {
$store = $this->createStore();
$store->set($data);
$store->save();
unset($store);
}
return new \Akaunting\Setting\Drivers\Database(
$this->capsule->getConnection()
);
}
}
@@ -1,30 +0,0 @@
<?php
class JsonTest extends AbstractFunctionalTest
{
protected function createStore(array $data = null)
{
$path = dirname(__DIR__) . '/tmp/store.json';
if ($data !== null) {
if ($data) {
$json = json_encode($data);
} else {
$json = '{}';
}
file_put_contents($path, $json);
}
return new \Akaunting\Setting\Drivers\Json(
new \Illuminate\Filesystem\Filesystem(),
$path
);
}
public function tearDown()
{
$path = dirname(__DIR__) . '/tmp/store.json';
unlink($path);
}
}
@@ -1,21 +0,0 @@
<?php
class MemoryTest extends AbstractFunctionalTest
{
protected function assertStoreEquals($store, $expected, $message = null)
{
$this->assertEquals($expected, $store->all(), $message);
// removed persistance test assertions
}
protected function assertStoreKeyEquals($store, $key, $expected, $message = null)
{
$this->assertEquals($expected, $store->get($key), $message);
// removed persistance test assertions
}
protected function createStore(array $data = null)
{
return new \Akaunting\Setting\Drivers\Memory($data);
}
}
@@ -1,132 +0,0 @@
<?php
use Akaunting\Setting\Support\Arr;
class ArrayUtilityTest extends PHPUnit_Framework_TestCase
{
/**
* @test
* @dataProvider getGetData
*/
public function getReturnsCorrectValue(array $data, $key, $expected)
{
$this->assertEquals($expected, Arr::get($data, $key));
}
public function getGetData()
{
return [
[[], 'foo', null],
[['foo' => 'bar'], 'foo', 'bar'],
[['foo' => 'bar'], 'bar', null],
[['foo' => 'bar'], 'foo.bar', null],
[['foo' => ['bar' => 'baz']], 'foo.bar', 'baz'],
[['foo' => ['bar' => 'baz']], 'foo.baz', null],
[['foo' => ['bar' => 'baz']], 'foo', ['bar' => 'baz']],
[
['foo' => 'bar', 'bar' => 'baz'],
['foo', 'bar'],
['foo' => 'bar', 'bar' => 'baz'],
],
[
['foo' => ['bar' => 'baz'], 'bar' => 'baz'],
['foo.bar', 'bar'],
['foo' => ['bar' => 'baz'], 'bar' => 'baz'],
],
[
['foo' => ['bar' => 'baz'], 'bar' => 'baz'],
['foo.bar'],
['foo' => ['bar' => 'baz']],
],
[
['foo' => ['bar' => 'baz'], 'bar' => 'baz'],
['foo.bar', 'baz'],
['foo' => ['bar' => 'baz'], 'baz' => null],
],
];
}
/**
* @test
* @dataProvider getSetData
*/
public function setSetsCorrectKeyToValue(array $input, $key, $value, array $expected)
{
Arr::set($input, $key, $value);
$this->assertEquals($expected, $input);
}
public function getSetData()
{
return [
[
['foo' => 'bar'],
'foo',
'baz',
['foo' => 'baz'],
],
[
[],
'foo',
'bar',
['foo' => 'bar'],
],
[
[],
'foo.bar',
'baz',
['foo' => ['bar' => 'baz']],
],
[
['foo' => ['bar' => 'baz']],
'foo.baz',
'foo',
['foo' => ['bar' => 'baz', 'baz' => 'foo']],
],
[
['foo' => ['bar' => 'baz']],
'foo.baz.bar',
'baz',
['foo' => ['bar' => 'baz', 'baz' => ['bar' => 'baz']]],
],
[
[],
'foo.bar.baz',
'foo',
['foo' => ['bar' => ['baz' => 'foo']]],
],
];
}
/** @test */
public function setThrowsExceptionOnNonArraySegment()
{
$data = ['foo' => 'bar'];
$this->setExpectedException('UnexpectedValueException', 'Non-array segment encountered');
Arr::set($data, 'foo.bar', 'baz');
}
/**
* @test
* @dataProvider getHasData
*/
public function hasReturnsCorrectly(array $input, $key, $expected)
{
$this->assertEquals($expected, Arr::has($input, $key));
}
public function getHasData()
{
return [
[[], 'foo', false],
[['foo' => 'bar'], 'foo', true],
[['foo' => 'bar'], 'bar', false],
[['foo' => 'bar'], 'foo.bar', false],
[['foo' => ['bar' => 'baz']], 'foo.bar', true],
[['foo' => ['bar' => 'baz']], 'foo.baz', false],
[['foo' => ['bar' => 'baz']], 'foo', true],
[['foo' => null], 'foo', true],
[['foo' => ['bar' => null]], 'foo.bar', true],
];
}
}
@@ -1,107 +0,0 @@
<?php
use Mockery as m;
class DatabaseDriverTest extends PHPUnit_Framework_TestCase
{
public function tearDown()
{
m::close();
}
/** @test */
public function correct_data_is_inserted_and_updated()
{
$connection = $this->mockConnection();
$query = $this->mockQuery($connection);
$query->shouldReceive('get')->once()->andReturn([
['key' => 'nest.one', 'value' => 'old'],
]);
$query->shouldReceive('lists')->atMost(1)->andReturn(['nest.one']);
$query->shouldReceive('pluck')->atMost(1)->andReturn(['nest.one']);
$dbData = $this->getDbData();
unset($dbData[1]); // remove the nest.one array member
$query->shouldReceive('where')->with('key', '=', 'nest.one')->andReturn(m::self())->getMock()
->shouldReceive('update')->with(['value' => 'nestone']);
$self = $this; // 5.3 compatibility
$query->shouldReceive('insert')->once()->andReturnUsing(function ($arg) use ($dbData, $self) {
$self->assertEquals(count($dbData), count($arg));
foreach ($dbData as $key => $value) {
$self->assertContains($value, $arg);
}
});
$store = $this->makeStore($connection);
$store->set('foo', 'bar');
$store->set('nest.one', 'nestone');
$store->set('nest.two', 'nesttwo');
$store->set('array', ['one', 'two']);
$store->save();
}
/** @test */
public function extra_columns_are_queried()
{
$connection = $this->mockConnection();
$query = $this->mockQuery($connection);
$query->shouldReceive('where')->once()->with('foo', '=', 'bar')
->andReturn(m::self())->getMock()
->shouldReceive('get')->once()->andReturn([
['key' => 'foo', 'value' => 'bar'],
]);
$store = $this->makeStore($connection);
$store->setExtraColumns(['foo' => 'bar']);
$this->assertEquals('bar', $store->get('foo'));
}
/** @test */
public function extra_columns_are_inserted()
{
$connection = $this->mockConnection();
$query = $this->mockQuery($connection);
$query->shouldReceive('where')->times(2)->with('extracol', '=', 'extradata')
->andReturn(m::self());
$query->shouldReceive('get')->once()->andReturn([]);
$query->shouldReceive('lists')->atMost(1)->andReturn([]);
$query->shouldReceive('pluck')->atMost(1)->andReturn([]);
$query->shouldReceive('insert')->once()->with([
['key' => 'foo', 'value' => 'bar', 'extracol' => 'extradata'],
]);
$store = $this->makeStore($connection);
$store->setExtraColumns(['extracol' => 'extradata']);
$store->set('foo', 'bar');
$store->save();
}
protected function getDbData()
{
return [
['key' => 'foo', 'value' => 'bar'],
['key' => 'nest.one', 'value' => 'nestone'],
['key' => 'nest.two', 'value' => 'nesttwo'],
['key' => 'array.0', 'value' => 'one'],
['key' => 'array.1', 'value' => 'two'],
];
}
protected function mockConnection()
{
return m::mock('Illuminate\Database\Connection');
}
protected function mockQuery($connection)
{
$query = m::mock('Illuminate\Database\Query\Builder');
$connection->shouldReceive('table')->andReturn($query);
return $query;
}
protected function makeStore($connection)
{
return new Akaunting\Setting\Drivers\Database($connection);
}
}
@@ -1,51 +0,0 @@
<?php
use Illuminate\Container\Container;
use Mockery as m;
class HelperTest extends PHPUnit_Framework_TestCase
{
public static $functions;
public function setUp()
{
self::$functions = m::mock();
Container::setInstance(new Container());
$store = m::mock('Akaunting\Setting\Contracts\Driver');
app()->bind('setting', function () use ($store) {
return $store;
});
}
/** @test */
public function helper_without_parameters_returns_store()
{
$this->assertInstanceOf('Akaunting\Setting\Contracts\Driver', setting());
}
/** @test */
public function single_parameter_get_a_key_from_store()
{
app('setting')->shouldReceive('get')->with('foo', null)->once();
setting('foo');
}
public function two_parameters_return_a_default_value()
{
app('setting')->shouldReceive('get')->with('foo', 'bar')->once();
setting('foo', 'bar');
}
/** @test */
public function array_parameter_call_set_method_into_store()
{
app('setting')->shouldReceive('set')->with(['foo', 'bar'])->once();
setting(['foo', 'bar']);
}
}
@@ -1,60 +0,0 @@
<?php
use Mockery as m;
class JsonDriverTest extends PHPUnit_Framework_TestCase
{
public function tearDown()
{
m::close();
}
protected function mockFilesystem()
{
return m::mock('Illuminate\Filesystem\Filesystem');
}
protected function makeStore($files, $path = 'fakepath')
{
return new Akaunting\Setting\Drivers\Json($files, $path);
}
/**
* @test
* @expectedException InvalidArgumentException
*/
public function throws_exception_when_file_not_writeable()
{
$files = $this->mockFilesystem();
$files->shouldReceive('exists')->once()->with('fakepath')->andReturn(true);
$files->shouldReceive('isWritable')->once()->with('fakepath')->andReturn(false);
$store = $this->makeStore($files);
}
/**
* @test
* @expectedException InvalidArgumentException
*/
public function throws_exception_when_files_put_fails()
{
$files = $this->mockFilesystem();
$files->shouldReceive('exists')->once()->with('fakepath')->andReturn(false);
$files->shouldReceive('put')->once()->with('fakepath', '{}')->andReturn(false);
$store = $this->makeStore($files);
}
/**
* @test
* @expectedException RuntimeException
*/
public function throws_exception_when_file_contains_invalid_json()
{
$files = $this->mockFilesystem();
$files->shouldReceive('exists')->once()->with('fakepath')->andReturn(true);
$files->shouldReceive('isWritable')->once()->with('fakepath')->andReturn(true);
$files->shouldReceive('get')->once()->with('fakepath')->andReturn('[[!1!11]');
$store = $this->makeStore($files);
$store->get('foo');
}
}
-4
View File
@@ -1,4 +0,0 @@
/vendor
composer.phar
composer.lock
.DS_Store
-18
View File
@@ -1,18 +0,0 @@
language: php
dist: trusty
php:
- 5.5
- 5.6
- 7.0
- 7.1
- 7.2
before_script:
- travis_retry composer self-update
- travis_retry composer install --prefer-source --no-interaction --dev
script:
- composer install
- vendor/bin/phpunit
-22
View File
@@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2014 Nguyễn Văn Ánh
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-197
View File
@@ -1,197 +0,0 @@
No CAPTCHA reCAPTCHA
==========
[![Build Status](https://travis-ci.org/anhskohbo/no-captcha.svg?branch=master&style=flat-square)](https://travis-ci.org/anhskohbo/no-captcha)
[![Latest Stable Version](https://poser.pugx.org/anhskohbo/no-captcha/v/stable)](https://packagist.org/packages/anhskohbo/no-captcha)
[![Total Downloads](https://poser.pugx.org/anhskohbo/no-captcha/downloads)](https://packagist.org/packages/anhskohbo/no-captcha)
[![Latest Unstable Version](https://poser.pugx.org/anhskohbo/no-captcha/v/unstable)](https://packagist.org/packages/anhskohbo/no-captcha)
[![License](https://poser.pugx.org/anhskohbo/no-captcha/license)](https://packagist.org/packages/anhskohbo/no-captcha)
![recaptcha_anchor 2x](https://cloud.githubusercontent.com/assets/1529454/5291635/1c426412-7b88-11e4-8d16-46161a081ece.gif)
> For Laravel 4 use [v1](https://github.com/anhskohbo/no-captcha/tree/v1) branch.
## Installation
```
composer require anhskohbo/no-captcha
```
## Laravel 5 and above
### Setup
**_NOTE_** This package supports the auto-discovery feature of Laravel 5.5 and above, So skip these `Setup` instructions if you're using Laravel 5.5 and above.
In `app/config/app.php` add the following :
1- The ServiceProvider to the providers array :
```php
Anhskohbo\NoCaptcha\NoCaptchaServiceProvider::class,
```
2- The class alias to the aliases array :
```php
'NoCaptcha' => Anhskohbo\NoCaptcha\Facades\NoCaptcha::class,
```
3- Publish the config file
```ssh
php artisan vendor:publish --provider="Anhskohbo\NoCaptcha\NoCaptchaServiceProvider"
```
### Configuration
Add `NOCAPTCHA_SECRET` and `NOCAPTCHA_SITEKEY` in **.env** file :
```
NOCAPTCHA_SECRET=secret-key
NOCAPTCHA_SITEKEY=site-key
```
(You can obtain them from [here](https://www.google.com/recaptcha/admin))
### Usage
#### Init js source
With default options :
```php
{!! NoCaptcha::renderJs() !!}
```
With [language support](https://developers.google.com/recaptcha/docs/language) or [onloadCallback](https://developers.google.com/recaptcha/docs/display#explicit_render) option :
```php
{!! NoCaptcha::renderJs('fr', true, 'recaptchaCallback') !!}
```
#### Display reCAPTCHA
Default widget :
```php
{!! NoCaptcha::display() !!}
```
With [custom attributes](https://developers.google.com/recaptcha/docs/display#render_param) (theme, size, callback ...) :
```php
{!! NoCaptcha::display(['data-theme' => 'dark']) !!}
```
Invisible reCAPTCHA using a [submit button](https://developers.google.com/recaptcha/docs/invisible):
```php
{!! NoCaptcha::displaySubmit('my-form-id', 'submit now!', ['data-theme' => 'dark']) !!}
```
Notice that the id of the form is required in this method to let the autogenerated
callback submit the form on a successful captcha verification.
#### Validation
Add `'g-recaptcha-response' => 'required|captcha'` to rules array :
```php
$validate = Validator::make(Input::all(), [
'g-recaptcha-response' => 'required|captcha'
]);
```
##### Custom Validation Message
Add the following values to the `custom` array in the `validation` language file :
```php
'custom' => [
'g-recaptcha-response' => [
'required' => 'Please verify that you are not a robot.',
'captcha' => 'Captcha error! try again later or contact site admin.',
],
],
```
Then check for captcha errors in the `Form` :
```php
@if ($errors->has('g-recaptcha-response'))
<span class="help-block">
<strong>{{ $errors->first('g-recaptcha-response') }}</strong>
</span>
@endif
```
### Testing
When using the [Laravel Testing functionality](http://laravel.com/docs/5.5/testing), you will need to mock out the response for the captcha form element.
So for any form tests involving the captcha, you can do this by mocking the facade behavior:
```php
// prevent validation error on captcha
NoCaptcha::shouldReceive('verifyResponse')
->once()
->andReturn(true);
// provide hidden input for your 'required' validation
NoCaptcha::shouldReceive('display')
->zeroOrMoreTimes()
->andReturn('<input type="hidden" name="g-recaptcha-response" value="1" />');
```
You can then test the remainder of your form as normal.
When using HTTP tests you can add the `g-recaptcha-response` to the request body for the 'required' validation:
```php
// prevent validation error on captcha
NoCaptcha::shouldReceive('verifyResponse')
->once()
->andReturn(true);
// POST request, with request body including g-recaptcha-response
$response = $this->json('POST', '/register', [
'g-recaptcha-response' => '1',
'name' => 'John',
'email' => 'john@example.com',
'password' => '123456',
'password_confirmation' => '123456',
]);
```
## Without Laravel
Checkout example below:
```php
<?php
require_once "vendor/autoload.php";
$secret = 'CAPTCHA-SECRET';
$sitekey = 'CAPTCHA-SITEKEY';
$captcha = new \Anhskohbo\NoCaptcha\NoCaptcha($secret, $sitekey);
if (! empty($_POST)) {
var_dump($captcha->verifyResponse($_POST['g-recaptcha-response']));
exit();
}
?>
<form action="?" method="POST">
<?php echo $captcha->display(); ?>
<button type="submit">Submit</button>
</form>
<?php echo $captcha->renderJs(); ?>
```
## Contribute
https://github.com/anhskohbo/no-captcha/pulls
-43
View File
@@ -1,43 +0,0 @@
{
"name": "anhskohbo/no-captcha",
"description": "No CAPTCHA reCAPTCHA For Laravel.",
"keywords": [
"recaptcha",
"no-captcha",
"captcha",
"laravel",
"laravel4",
"laravel5",
"laravel6"
],
"license": "MIT",
"authors": [
{
"name": "anhskohbo",
"email": "anhskohbo@gmail.com"
}
],
"require": {
"php": ">=5.5.5",
"illuminate/support": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
"guzzlehttp/guzzle": "^6.2|^7.0"
},
"require-dev": {
"phpunit/phpunit": "~4.8|^9.5.10|^10.5"
},
"autoload": {
"psr-4": {
"Anhskohbo\\NoCaptcha\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"Anhskohbo\\NoCaptcha\\NoCaptchaServiceProvider"
],
"aliases": {
"NoCaptcha": "Anhskohbo\\NoCaptcha\\Facades\\NoCaptcha"
}
}
}
}
-18
View File
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
>
<testsuites>
<testsuite name="Package Test Suite">
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
</phpunit>
-18
View File
@@ -1,18 +0,0 @@
<?php
namespace Anhskohbo\NoCaptcha\Facades;
use Illuminate\Support\Facades\Facade;
class NoCaptcha extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'captcha';
}
}
-246
View File
@@ -1,246 +0,0 @@
<?php
namespace Anhskohbo\NoCaptcha;
use Symfony\Component\HttpFoundation\Request;
use GuzzleHttp\Client;
class NoCaptcha
{
const CLIENT_API = 'https://www.google.com/recaptcha/api.js';
const VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify';
/**
* The recaptcha secret key.
*
* @var string
*/
protected $secret;
/**
* The recaptcha sitekey key.
*
* @var string
*/
protected $sitekey;
/**
* @var \GuzzleHttp\Client
*/
protected $http;
/**
* The cached verified responses.
*
* @var array
*/
protected $verifiedResponses = [];
/**
* NoCaptcha.
*
* @param string $secret
* @param string $sitekey
* @param array $options
*/
public function __construct($secret, $sitekey, $options = [])
{
$this->secret = $secret;
$this->sitekey = $sitekey;
$this->http = new Client($options);
}
/**
* Render HTML captcha.
*
* @param array $attributes
*
* @return string
*/
public function display($attributes = [])
{
$attributes = $this->prepareAttributes($attributes);
return '<div' . $this->buildAttributes($attributes) . '></div>';
}
/**
* @see display()
*/
public function displayWidget($attributes = [])
{
return $this->display($attributes);
}
/**
* Display a Invisible reCAPTCHA by embedding a callback into a form submit button.
*
* @param string $formIdentifier the html ID of the form that should be submitted.
* @param string $text the text inside the form button
* @param array $attributes array of additional html elements
*
* @return string
*/
public function displaySubmit($formIdentifier, $text = 'submit', $attributes = [])
{
$javascript = '';
if (!isset($attributes['data-callback'])) {
$functionName = 'onSubmit' . str_replace(['-', '=', '\'', '"', '<', '>', '`'], '', $formIdentifier);
$attributes['data-callback'] = $functionName;
$javascript = sprintf(
'<script>function %s(){document.getElementById("%s").submit();}</script>',
$functionName,
$formIdentifier
);
}
$attributes = $this->prepareAttributes($attributes);
$button = sprintf('<button%s><span>%s</span></button>', $this->buildAttributes($attributes), $text);
return $button . $javascript;
}
/**
* Render js source
*
* @param null $lang
* @param bool $callback
* @param string $onLoadClass
* @return string
*/
public function renderJs($lang = null, $callback = false, $onLoadClass = 'onloadCallBack')
{
return '<script src="'.$this->getJsLink($lang, $callback, $onLoadClass).'" async defer></script>'."\n";
}
/**
* Verify no-captcha response.
*
* @param string $response
* @param string $clientIp
*
* @return bool
*/
public function verifyResponse($response, $clientIp = null)
{
if (empty($response)) {
return false;
}
// Return true if response already verfied before.
if (in_array($response, $this->verifiedResponses)) {
return true;
}
$verifyResponse = $this->sendRequestVerify([
'secret' => $this->secret,
'response' => $response,
'remoteip' => $clientIp,
]);
if (isset($verifyResponse['success']) && $verifyResponse['success'] === true) {
// A response can only be verified once from google, so we need to
// cache it to make it work in case we want to verify it multiple times.
$this->verifiedResponses[] = $response;
return true;
} else {
return false;
}
}
/**
* Verify no-captcha response by Symfony Request.
*
* @param Request $request
*
* @return bool
*/
public function verifyRequest(Request $request)
{
return $this->verifyResponse(
$request->get('g-recaptcha-response'),
$request->getClientIp()
);
}
/**
* Get recaptcha js link.
*
* @param string $lang
* @param boolean $callback
* @param string $onLoadClass
* @return string
*/
public function getJsLink($lang = null, $callback = false, $onLoadClass = 'onloadCallBack')
{
$client_api = static::CLIENT_API;
$params = [];
$callback ? $this->setCallBackParams($params, $onLoadClass) : false;
$lang ? $params['hl'] = $lang : null;
return $client_api . '?'. http_build_query($params);
}
/**
* @param $params
* @param $onLoadClass
*/
protected function setCallBackParams(&$params, $onLoadClass)
{
$params['render'] = 'explicit';
$params['onload'] = $onLoadClass;
}
/**
* Send verify request.
*
* @param array $query
*
* @return array
*/
protected function sendRequestVerify(array $query = [])
{
$response = $this->http->request('POST', static::VERIFY_URL, [
'form_params' => $query,
]);
return json_decode($response->getBody(), true);
}
/**
* Prepare HTML attributes and assure that the correct classes and attributes for captcha are inserted.
*
* @param array $attributes
*
* @return array
*/
protected function prepareAttributes(array $attributes)
{
$attributes['data-sitekey'] = $this->sitekey;
if (!isset($attributes['class'])) {
$attributes['class'] = '';
}
$attributes['class'] = trim('g-recaptcha ' . $attributes['class']);
return $attributes;
}
/**
* Build HTML attributes.
*
* @param array $attributes
*
* @return string
*/
protected function buildAttributes(array $attributes)
{
$html = [];
foreach ($attributes as $key => $value) {
$html[] = $key.'="'.$value.'"';
}
return count($html) ? ' '.implode(' ', $html) : '';
}
}
@@ -1,73 +0,0 @@
<?php
namespace Anhskohbo\NoCaptcha;
use Illuminate\Support\ServiceProvider;
class NoCaptchaServiceProvider extends ServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = false;
/**
* Bootstrap the application events.
*/
public function boot()
{
$app = $this->app;
$this->bootConfig();
$app['validator']->extend('captcha', function ($attribute, $value) use ($app) {
return $app['captcha']->verifyResponse($value, $app['request']->getClientIp());
});
if ($app->bound('form')) {
$app['form']->macro('captcha', function ($attributes = []) use ($app) {
return $app['captcha']->display($attributes, $app->getLocale());
});
}
}
/**
* Booting configure.
*/
protected function bootConfig()
{
$path = __DIR__.'/config/captcha.php';
$this->mergeConfigFrom($path, 'captcha');
if (function_exists('config_path')) {
$this->publishes([$path => config_path('captcha.php')]);
}
}
/**
* Register the service provider.
*/
public function register()
{
$this->app->singleton('captcha', function ($app) {
return new NoCaptcha(
$app['config']['captcha.secret'],
$app['config']['captcha.sitekey'],
$app['config']['captcha.options']
);
});
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return ['captcha'];
}
}
View File
-9
View File
@@ -1,9 +0,0 @@
<?php
return [
'secret' => env('NOCAPTCHA_SECRET'),
'sitekey' => env('NOCAPTCHA_SITEKEY'),
'options' => [
'timeout' => 30,
],
];
View File
-69
View File
@@ -1,69 +0,0 @@
<?php
use Anhskohbo\NoCaptcha\NoCaptcha;
class NoCaptchaTest extends PHPUnit_Framework_TestCase
{
/**
* @var NoCaptcha
*/
private $captcha;
public function setUp()
{
parent::setUp();
$this->captcha = new NoCaptcha('{secret-key}', '{site-key}');
}
public function testRequestShouldWorks()
{
$response = $this->captcha->verifyResponse('should_false');
}
public function testJsLink()
{
$this->assertTrue($this->captcha instanceof NoCaptcha);
$simple = '<script src="https://www.google.com/recaptcha/api.js?" async defer></script>'."\n";
$withLang = '<script src="https://www.google.com/recaptcha/api.js?hl=vi" async defer></script>'."\n";
$withCallback = '<script src="https://www.google.com/recaptcha/api.js?render=explicit&onload=myOnloadCallback" async defer></script>'."\n";
$this->assertEquals($simple, $this->captcha->renderJs());
$this->assertEquals($withLang, $this->captcha->renderJs('vi'));
$this->assertEquals($withCallback, $this->captcha->renderJs(null, true, 'myOnloadCallback'));
}
public function testDisplay()
{
$this->assertTrue($this->captcha instanceof NoCaptcha);
$simple = '<div data-sitekey="{site-key}" class="g-recaptcha"></div>';
$withAttrs = '<div data-theme="light" data-sitekey="{site-key}" class="g-recaptcha"></div>';
$this->assertEquals($simple, $this->captcha->display());
$this->assertEquals($withAttrs, $this->captcha->display(['data-theme' => 'light']));
}
public function testdisplaySubmit()
{
$this->assertTrue($this->captcha instanceof NoCaptcha);
$javascript = '<script>function onSubmittest(){document.getElementById("test").submit();}</script>';
$simple = '<button data-callback="onSubmittest" data-sitekey="{site-key}" class="g-recaptcha"><span>submit</span></button>';
$withAttrs = '<button data-theme="light" class="g-recaptcha 123" data-callback="onSubmittest" data-sitekey="{site-key}"><span>submit123</span></button>';
$this->assertEquals($simple . $javascript, $this->captcha->displaySubmit('test'));
$withAttrsResult = $this->captcha->displaySubmit('test','submit123',['data-theme' => 'light', 'class' => '123']);
$this->assertEquals($withAttrs . $javascript, $withAttrsResult);
}
public function testdisplaySubmitWithCustomCallback()
{
$this->assertTrue($this->captcha instanceof NoCaptcha);
$withAttrs = '<button data-theme="light" class="g-recaptcha 123" data-callback="onSubmitCustomCallback" data-sitekey="{site-key}"><span>submit123</span></button>';
$withAttrsResult = $this->captcha->displaySubmit('test-custom','submit123',['data-theme' => 'light', 'class' => '123', 'data-callback' => 'onSubmitCustomCallback']);
$this->assertEquals($withAttrs, $withAttrsResult);
}
}
-25
View File
@@ -1,25 +0,0 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitc91cd9c5b1e6a9e8573a14b799ea9342::getLoader();
-22
View File
@@ -1,22 +0,0 @@
Copyright (c) 2017, Ben Scholzen 'DASPRiD'
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-57
View File
@@ -1,57 +0,0 @@
# QR Code generator
[![PHP CI](https://github.com/Bacon/BaconQrCode/actions/workflows/ci.yml/badge.svg)](https://github.com/Bacon/BaconQrCode/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/Bacon/BaconQrCode/branch/master/graph/badge.svg?token=rD0HcAiEEx)](https://codecov.io/gh/Bacon/BaconQrCode)
[![Latest Stable Version](https://poser.pugx.org/bacon/bacon-qr-code/v/stable)](https://packagist.org/packages/bacon/bacon-qr-code)
[![Total Downloads](https://poser.pugx.org/bacon/bacon-qr-code/downloads)](https://packagist.org/packages/bacon/bacon-qr-code)
[![License](https://poser.pugx.org/bacon/bacon-qr-code/license)](https://packagist.org/packages/bacon/bacon-qr-code)
## Introduction
BaconQrCode is a port of QR code portion of the ZXing library. It currently
only features the encoder part, but could later receive the decoder part as
well.
As the Reed Solomon codec implementation of the ZXing library performs quite
slow in PHP, it was exchanged with the implementation by Phil Karn.
## Example usage
```php
use BaconQrCode\Renderer\ImageRenderer;
use BaconQrCode\Renderer\Image\ImagickImageBackEnd;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
use BaconQrCode\Writer;
$renderer = new ImageRenderer(
new RendererStyle(400),
new ImagickImageBackEnd()
);
$writer = new Writer($renderer);
$writer->writeFile('Hello World!', 'qrcode.png');
```
## Available image renderer back ends
BaconQrCode comes with multiple back ends for rendering images. Currently included are the following:
- `ImagickImageBackEnd`: renders raster images using the Imagick library
- `SvgImageBackEnd`: renders SVG files using XMLWriter
- `EpsImageBackEnd`: renders EPS files
### GDLib Renderer
GD library has so many limitations, that GD support is not added as backend, but as separated renderer.
Use `GDLibRenderer` instead of `ImageRenderer`. These are the limitations:
- Does not support gradient.
- Does not support any curves, so you QR code is always squared.
Example usage:
```php
use BaconQrCode\Renderer\GDLibRenderer;
use BaconQrCode\Writer;
$renderer = new GDLibRenderer(400);
$writer = new Writer($renderer);
$writer->writeFile('Hello World!', 'qrcode.png');
```
-50
View File
@@ -1,50 +0,0 @@
{
"name": "bacon/bacon-qr-code",
"description": "BaconQrCode is a QR code generator for PHP.",
"license": "BSD-2-Clause",
"homepage": "https://github.com/Bacon/BaconQrCode",
"require": {
"php": "^8.1",
"ext-iconv": "*",
"dasprid/enum": "^1.0.3"
},
"suggest": {
"ext-imagick": "to generate QR code images"
},
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"autoload": {
"psr-4": {
"BaconQrCode\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"BaconQrCodeTest\\": "test/"
}
},
"require-dev": {
"phpunit/phpunit": "^10.5.11 || 11.0.4",
"spatie/phpunit-snapshot-assertions": "^5.1.5",
"squizlabs/php_codesniffer": "^3.9",
"phly/keep-a-changelog": "^2.12"
},
"config": {
"allow-plugins": {
"ocramius/package-versions": true,
"php-http/discovery": true
}
},
"archive": {
"exclude": [
"/test",
"/phpunit.xml.dist"
]
}
}
-364
View File
@@ -1,364 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use BaconQrCode\Exception\InvalidArgumentException;
use SplFixedArray;
/**
* A simple, fast array of bits.
*/
final class BitArray
{
/**
* Bits represented as an array of integers.
*
* @var SplFixedArray<int>
*/
private SplFixedArray $bits;
/**
* Creates a new bit array with a given size.
*/
public function __construct(private int $size = 0)
{
$this->bits = SplFixedArray::fromArray(array_fill(0, ($this->size + 31) >> 3, 0));
}
/**
* Gets the size in bits.
*/
public function getSize() : int
{
return $this->size;
}
/**
* Gets the size in bytes.
*/
public function getSizeInBytes() : int
{
return ($this->size + 7) >> 3;
}
/**
* Ensures that the array has a minimum capacity.
*/
public function ensureCapacity(int $size) : void
{
if ($size > count($this->bits) << 5) {
$this->bits->setSize(($size + 31) >> 5);
}
}
/**
* Gets a specific bit.
*/
public function get(int $i) : bool
{
return 0 !== ($this->bits[$i >> 5] & (1 << ($i & 0x1f)));
}
/**
* Sets a specific bit.
*/
public function set(int $i) : void
{
$this->bits[$i >> 5] = $this->bits[$i >> 5] | 1 << ($i & 0x1f);
}
/**
* Flips a specific bit.
*/
public function flip(int $i) : void
{
$this->bits[$i >> 5] ^= 1 << ($i & 0x1f);
}
/**
* Gets the next set bit position from a given position.
*/
public function getNextSet(int $from) : int
{
if ($from >= $this->size) {
return $this->size;
}
$bitsOffset = $from >> 5;
$currentBits = $this->bits[$bitsOffset];
$bitsLength = count($this->bits);
$currentBits &= ~((1 << ($from & 0x1f)) - 1);
while (0 === $currentBits) {
if (++$bitsOffset === $bitsLength) {
return $this->size;
}
$currentBits = $this->bits[$bitsOffset];
}
$result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
return min($result, $this->size);
}
/**
* Gets the next unset bit position from a given position.
*/
public function getNextUnset(int $from) : int
{
if ($from >= $this->size) {
return $this->size;
}
$bitsOffset = $from >> 5;
$currentBits = ~$this->bits[$bitsOffset];
$bitsLength = count($this->bits);
$currentBits &= ~((1 << ($from & 0x1f)) - 1);
while (0 === $currentBits) {
if (++$bitsOffset === $bitsLength) {
return $this->size;
}
$currentBits = ~$this->bits[$bitsOffset];
}
$result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
return min($result, $this->size);
}
/**
* Sets a bulk of bits.
*/
public function setBulk(int $i, int $newBits) : void
{
$this->bits[$i >> 5] = $newBits;
}
/**
* Sets a range of bits.
*
* @throws InvalidArgumentException if end is smaller than start
*/
public function setRange(int $start, int $end) : void
{
if ($end < $start) {
throw new InvalidArgumentException('End must be greater or equal to start');
}
if ($end === $start) {
return;
}
--$end;
$firstInt = $start >> 5;
$lastInt = $end >> 5;
for ($i = $firstInt; $i <= $lastInt; ++$i) {
$firstBit = $i > $firstInt ? 0 : $start & 0x1f;
$lastBit = $i < $lastInt ? 31 : $end & 0x1f;
if (0 === $firstBit && 31 === $lastBit) {
$mask = 0x7fffffff;
} else {
$mask = 0;
for ($j = $firstBit; $j < $lastBit; ++$j) {
$mask |= 1 << $j;
}
}
$this->bits[$i] = $this->bits[$i] | $mask;
}
}
/**
* Clears the bit array, unsetting every bit.
*/
public function clear() : void
{
$bitsLength = count($this->bits);
for ($i = 0; $i < $bitsLength; ++$i) {
$this->bits[$i] = 0;
}
}
/**
* Checks if a range of bits is set or not set.
* @throws InvalidArgumentException if end is smaller than start
*/
public function isRange(int $start, int $end, bool $value) : bool
{
if ($end < $start) {
throw new InvalidArgumentException('End must be greater or equal to start');
}
if ($end === $start) {
return true;
}
--$end;
$firstInt = $start >> 5;
$lastInt = $end >> 5;
for ($i = $firstInt; $i <= $lastInt; ++$i) {
$firstBit = $i > $firstInt ? 0 : $start & 0x1f;
$lastBit = $i < $lastInt ? 31 : $end & 0x1f;
if (0 === $firstBit && 31 === $lastBit) {
$mask = 0x7fffffff;
} else {
$mask = 0;
for ($j = $firstBit; $j <= $lastBit; ++$j) {
$mask |= 1 << $j;
}
}
if (($this->bits[$i] & $mask) !== ($value ? $mask : 0)) {
return false;
}
}
return true;
}
/**
* Appends a bit to the array.
*/
public function appendBit(bool $bit) : void
{
$this->ensureCapacity($this->size + 1);
if ($bit) {
$this->bits[$this->size >> 5] = $this->bits[$this->size >> 5] | (1 << ($this->size & 0x1f));
}
++$this->size;
}
/**
* Appends a number of bits (up to 32) to the array.
* @throws InvalidArgumentException if num bits is not between 0 and 32
*/
public function appendBits(int $value, int $numBits) : void
{
if ($numBits < 0 || $numBits > 32) {
throw new InvalidArgumentException('Num bits must be between 0 and 32');
}
$this->ensureCapacity($this->size + $numBits);
for ($numBitsLeft = $numBits; $numBitsLeft > 0; $numBitsLeft--) {
$this->appendBit((($value >> ($numBitsLeft - 1)) & 0x01) === 1);
}
}
/**
* Appends another bit array to this array.
*/
public function appendBitArray(self $other) : void
{
$otherSize = $other->getSize();
$this->ensureCapacity($this->size + $other->getSize());
for ($i = 0; $i < $otherSize; ++$i) {
$this->appendBit($other->get($i));
}
}
/**
* Makes an exclusive-or comparision on the current bit array.
*
* @throws InvalidArgumentException if sizes don't match
*/
public function xorBits(self $other) : void
{
$bitsLength = count($this->bits);
$otherBits = $other->getBitArray();
if ($bitsLength !== count($otherBits)) {
throw new InvalidArgumentException('Sizes don\'t match');
}
for ($i = 0; $i < $bitsLength; ++$i) {
$this->bits[$i] = $this->bits[$i] ^ $otherBits[$i];
}
}
/**
* Converts the bit array to a byte array.
*
* @return SplFixedArray<int>
*/
public function toBytes(int $bitOffset, int $numBytes) : SplFixedArray
{
$bytes = new SplFixedArray($numBytes);
for ($i = 0; $i < $numBytes; ++$i) {
$byte = 0;
for ($j = 0; $j < 8; ++$j) {
if ($this->get($bitOffset)) {
$byte |= 1 << (7 - $j);
}
++$bitOffset;
}
$bytes[$i] = $byte;
}
return $bytes;
}
/**
* Gets the internal bit array.
*
* @return SplFixedArray<int>
*/
public function getBitArray() : SplFixedArray
{
return $this->bits;
}
/**
* Reverses the array.
*/
public function reverse() : void
{
$newBits = new SplFixedArray(count($this->bits));
for ($i = 0; $i < $this->size; ++$i) {
if ($this->get($this->size - $i - 1)) {
$newBits[$i >> 5] = $newBits[$i >> 5] | (1 << ($i & 0x1f));
}
}
$this->bits = $newBits;
}
/**
* Returns a string representation of the bit array.
*/
public function __toString() : string
{
$result = '';
for ($i = 0; $i < $this->size; ++$i) {
if (0 === ($i & 0x07)) {
$result .= ' ';
}
$result .= $this->get($i) ? 'X' : '.';
}
return $result;
}
}
-307
View File
@@ -1,307 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use BaconQrCode\Exception\InvalidArgumentException;
use SplFixedArray;
/**
* Bit matrix.
*
* Represents a 2D matrix of bits. In function arguments below, and throughout
* the common module, x is the column position, and y is the row position. The
* ordering is always x, y. The origin is at the top-left.
*/
class BitMatrix
{
/**
* Width of the bit matrix.
*/
private int $width;
/**
* Height of the bit matrix.
*/
private ?int $height;
/**
* Size in bits of each individual row.
*/
private int $rowSize;
/**
* Bits representation.
*
* @var SplFixedArray<int>
*/
private SplFixedArray $bits;
/**
* @throws InvalidArgumentException if a dimension is smaller than zero
*/
public function __construct(int $width, ?int $height = null)
{
if (null === $height) {
$height = $width;
}
if ($width < 1 || $height < 1) {
throw new InvalidArgumentException('Both dimensions must be greater than zero');
}
$this->width = $width;
$this->height = $height;
$this->rowSize = ($width + 31) >> 5;
$this->bits = SplFixedArray::fromArray(array_fill(0, $this->rowSize * $height, 0));
}
/**
* Gets the requested bit, where true means black.
*/
public function get(int $x, int $y) : bool
{
$offset = $y * $this->rowSize + ($x >> 5);
return 0 !== (BitUtils::unsignedRightShift($this->bits[$offset], ($x & 0x1f)) & 1);
}
/**
* Sets the given bit to true.
*/
public function set(int $x, int $y) : void
{
$offset = $y * $this->rowSize + ($x >> 5);
$this->bits[$offset] = $this->bits[$offset] | (1 << ($x & 0x1f));
}
/**
* Flips the given bit.
*/
public function flip(int $x, int $y) : void
{
$offset = $y * $this->rowSize + ($x >> 5);
$this->bits[$offset] = $this->bits[$offset] ^ (1 << ($x & 0x1f));
}
/**
* Clears all bits (set to false).
*/
public function clear() : void
{
$max = count($this->bits);
for ($i = 0; $i < $max; ++$i) {
$this->bits[$i] = 0;
}
}
/**
* Sets a square region of the bit matrix to true.
*
* @throws InvalidArgumentException if left or top are negative
* @throws InvalidArgumentException if width or height are smaller than 1
* @throws InvalidArgumentException if region does not fit into the matix
*/
public function setRegion(int $left, int $top, int $width, int $height) : void
{
if ($top < 0 || $left < 0) {
throw new InvalidArgumentException('Left and top must be non-negative');
}
if ($height < 1 || $width < 1) {
throw new InvalidArgumentException('Width and height must be at least 1');
}
$right = $left + $width;
$bottom = $top + $height;
if ($bottom > $this->height || $right > $this->width) {
throw new InvalidArgumentException('The region must fit inside the matrix');
}
for ($y = $top; $y < $bottom; ++$y) {
$offset = $y * $this->rowSize;
for ($x = $left; $x < $right; ++$x) {
$index = $offset + ($x >> 5);
$this->bits[$index] = $this->bits[$index] | (1 << ($x & 0x1f));
}
}
}
/**
* A fast method to retrieve one row of data from the matrix as a BitArray.
*/
public function getRow(int $y, ?BitArray $row = null) : BitArray
{
if (null === $row || $row->getSize() < $this->width) {
$row = new BitArray($this->width);
}
$offset = $y * $this->rowSize;
for ($x = 0; $x < $this->rowSize; ++$x) {
$row->setBulk($x << 5, $this->bits[$offset + $x]);
}
return $row;
}
/**
* Sets a row of data from a BitArray.
*/
public function setRow(int $y, BitArray $row) : void
{
$bits = $row->getBitArray();
for ($i = 0; $i < $this->rowSize; ++$i) {
$this->bits[$y * $this->rowSize + $i] = $bits[$i];
}
}
/**
* This is useful in detecting the enclosing rectangle of a 'pure' barcode.
*
* @return int[]|null
*/
public function getEnclosingRectangle() : ?array
{
$left = $this->width;
$top = $this->height;
$right = -1;
$bottom = -1;
for ($y = 0; $y < $this->height; ++$y) {
for ($x32 = 0; $x32 < $this->rowSize; ++$x32) {
$bits = $this->bits[$y * $this->rowSize + $x32];
if (0 !== $bits) {
if ($y < $top) {
$top = $y;
}
if ($y > $bottom) {
$bottom = $y;
}
if ($x32 * 32 < $left) {
$bit = 0;
while (($bits << (31 - $bit)) === 0) {
$bit++;
}
if (($x32 * 32 + $bit) < $left) {
$left = $x32 * 32 + $bit;
}
}
}
if ($x32 * 32 + 31 > $right) {
$bit = 31;
while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
--$bit;
}
if (($x32 * 32 + $bit) > $right) {
$right = $x32 * 32 + $bit;
}
}
}
}
$width = $right - $left;
$height = $bottom - $top;
if ($width < 0 || $height < 0) {
return null;
}
return [$left, $top, $width, $height];
}
/**
* Gets the most top left set bit.
*
* This is useful in detecting a corner of a 'pure' barcode.
*
* @return int[]|null
*/
public function getTopLeftOnBit() : ?array
{
$bitsOffset = 0;
while ($bitsOffset < count($this->bits) && 0 === $this->bits[$bitsOffset]) {
++$bitsOffset;
}
if (count($this->bits) === $bitsOffset) {
return null;
}
$x = intdiv($bitsOffset, $this->rowSize);
$y = ($bitsOffset % $this->rowSize) << 5;
$bits = $this->bits[$bitsOffset];
$bit = 0;
while (0 === ($bits << (31 - $bit))) {
++$bit;
}
$x += $bit;
return [$x, $y];
}
/**
* Gets the most bottom right set bit.
*
* This is useful in detecting a corner of a 'pure' barcode.
*
* @return int[]|null
*/
public function getBottomRightOnBit() : ?array
{
$bitsOffset = count($this->bits) - 1;
while ($bitsOffset >= 0 && 0 === $this->bits[$bitsOffset]) {
--$bitsOffset;
}
if ($bitsOffset < 0) {
return null;
}
$x = intdiv($bitsOffset, $this->rowSize);
$y = ($bitsOffset % $this->rowSize) << 5;
$bits = $this->bits[$bitsOffset];
$bit = 0;
while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
--$bit;
}
$x += $bit;
return [$x, $y];
}
/**
* Gets the width of the matrix,
*/
public function getWidth() : int
{
return $this->width;
}
/**
* Gets the height of the matrix.
*/
public function getHeight() : int
{
return $this->height;
}
}
-41
View File
@@ -1,41 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
/**
* General bit utilities.
*
* All utility methods are based on 32-bit integers and also work on 64-bit
* systems.
*/
final class BitUtils
{
private function __construct()
{
}
/**
* Performs an unsigned right shift.
*
* This is the same as the unsigned right shift operator ">>>" in other
* languages.
*/
public static function unsignedRightShift(int $a, int $b) : int
{
return (
$a >= 0
? $a >> $b
: (($a & 0x7fffffff) >> $b) | (0x40000000 >> ($b - 1))
);
}
/**
* Gets the number of trailing zeros.
*/
public static function numberOfTrailingZeros(int $i) : int
{
$lastPos = strrpos(str_pad(decbin($i), 32, '0', STR_PAD_LEFT), '1');
return $lastPos === false ? 32 : 31 - $lastPos;
}
}
@@ -1,177 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use BaconQrCode\Exception\InvalidArgumentException;
use DASPRiD\Enum\AbstractEnum;
/**
* Encapsulates a Character Set ECI, according to "Extended Channel Interpretations" 5.3.1.1 of ISO 18004.
*
* @method static self CP437()
* @method static self ISO8859_1()
* @method static self ISO8859_2()
* @method static self ISO8859_3()
* @method static self ISO8859_4()
* @method static self ISO8859_5()
* @method static self ISO8859_6()
* @method static self ISO8859_7()
* @method static self ISO8859_8()
* @method static self ISO8859_9()
* @method static self ISO8859_10()
* @method static self ISO8859_11()
* @method static self ISO8859_12()
* @method static self ISO8859_13()
* @method static self ISO8859_14()
* @method static self ISO8859_15()
* @method static self ISO8859_16()
* @method static self SJIS()
* @method static self CP1250()
* @method static self CP1251()
* @method static self CP1252()
* @method static self CP1256()
* @method static self UNICODE_BIG_UNMARKED()
* @method static self UTF8()
* @method static self ASCII()
* @method static self BIG5()
* @method static self GB18030()
* @method static self EUC_KR()
*/
final class CharacterSetEci extends AbstractEnum
{
protected const CP437 = [[0, 2]];
protected const ISO8859_1 = [[1, 3], 'ISO-8859-1'];
protected const ISO8859_2 = [[4], 'ISO-8859-2'];
protected const ISO8859_3 = [[5], 'ISO-8859-3'];
protected const ISO8859_4 = [[6], 'ISO-8859-4'];
protected const ISO8859_5 = [[7], 'ISO-8859-5'];
protected const ISO8859_6 = [[8], 'ISO-8859-6'];
protected const ISO8859_7 = [[9], 'ISO-8859-7'];
protected const ISO8859_8 = [[10], 'ISO-8859-8'];
protected const ISO8859_9 = [[11], 'ISO-8859-9'];
protected const ISO8859_10 = [[12], 'ISO-8859-10'];
protected const ISO8859_11 = [[13], 'ISO-8859-11'];
protected const ISO8859_12 = [[14], 'ISO-8859-12'];
protected const ISO8859_13 = [[15], 'ISO-8859-13'];
protected const ISO8859_14 = [[16], 'ISO-8859-14'];
protected const ISO8859_15 = [[17], 'ISO-8859-15'];
protected const ISO8859_16 = [[18], 'ISO-8859-16'];
protected const SJIS = [[20], 'Shift_JIS'];
protected const CP1250 = [[21], 'windows-1250'];
protected const CP1251 = [[22], 'windows-1251'];
protected const CP1252 = [[23], 'windows-1252'];
protected const CP1256 = [[24], 'windows-1256'];
protected const UNICODE_BIG_UNMARKED = [[25], 'UTF-16BE', 'UnicodeBig'];
protected const UTF8 = [[26], 'UTF-8'];
protected const ASCII = [[27, 170], 'US-ASCII'];
protected const BIG5 = [[28]];
protected const GB18030 = [[29], 'GB2312', 'EUC_CN', 'GBK'];
protected const EUC_KR = [[30], 'EUC-KR'];
/**
* @var string[]
*/
private array $otherEncodingNames;
/**
* @var array<int, self>|null
*/
private static ?array $valueToEci;
/**
* @var array<string, self>|null
*/
private static ?array $nameToEci = null;
/**
* @param int[] $values
*/
public function __construct(private readonly array $values, string ...$otherEncodingNames)
{
$this->otherEncodingNames = $otherEncodingNames;
}
/**
* Returns the primary value.
*/
public function getValue() : int
{
return $this->values[0];
}
/**
* Gets character set ECI by value.
*
* Returns the representing ECI of a given value, or null if it is legal but unsupported.
*
* @throws InvalidArgumentException if value is not between 0 and 900
*/
public static function getCharacterSetEciByValue(int $value) : ?self
{
if ($value < 0 || $value >= 900) {
throw new InvalidArgumentException('Value must be between 0 and 900');
}
$valueToEci = self::valueToEci();
if (! array_key_exists($value, $valueToEci)) {
return null;
}
return $valueToEci[$value];
}
/**
* Returns character set ECI by name.
*
* Returns the representing ECI of a given name, or null if it is legal but unsupported
*/
public static function getCharacterSetEciByName(string $name) : ?self
{
$nameToEci = self::nameToEci();
$name = strtolower($name);
if (! array_key_exists($name, $nameToEci)) {
return null;
}
return $nameToEci[$name];
}
private static function valueToEci() : array
{
if (null !== self::$valueToEci) {
return self::$valueToEci;
}
self::$valueToEci = [];
foreach (self::values() as $eci) {
foreach ($eci->values as $value) {
self::$valueToEci[$value] = $eci;
}
}
return self::$valueToEci;
}
private static function nameToEci() : array
{
if (null !== self::$nameToEci) {
return self::$nameToEci;
}
self::$nameToEci = [];
foreach (self::values() as $eci) {
self::$nameToEci[strtolower($eci->name())] = $eci;
foreach ($eci->otherEncodingNames as $name) {
self::$nameToEci[strtolower($name)] = $eci;
}
}
return self::$nameToEci;
}
}
-33
View File
@@ -1,33 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
/**
* Encapsulates the parameters for one error-correction block in one symbol version.
*
* This includes the number of data codewords, and the number of times a block with these parameters is used
* consecutively in the QR code version's format.
*/
final class EcBlock
{
public function __construct(private readonly int $count, private readonly int $dataCodewords)
{
}
/**
* Returns how many times the block is used.
*/
public function getCount() : int
{
return $this->count;
}
/**
* Returns the number of data codewords.
*/
public function getDataCodewords() : int
{
return $this->dataCodewords;
}
}
-66
View File
@@ -1,66 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
/**
* Encapsulates a set of error-correction blocks in one symbol version.
*
* Most versions will use blocks of differing sizes within one version, so, this encapsulates the parameters for each
* set of blocks. It also holds the number of error-correction codewords per block since it will be the same across all
* blocks within one version.
*/
final class EcBlocks
{
/**
* List of EC blocks.
*
* @var EcBlock[]
*/
private array $ecBlocks;
public function __construct(private readonly int $ecCodewordsPerBlock, EcBlock ...$ecBlocks)
{
$this->ecBlocks = $ecBlocks;
}
/**
* Returns the number of EC codewords per block.
*/
public function getEcCodewordsPerBlock() : int
{
return $this->ecCodewordsPerBlock;
}
/**
* Returns the total number of EC block appearances.
*/
public function getNumBlocks() : int
{
$total = 0;
foreach ($this->ecBlocks as $ecBlock) {
$total += $ecBlock->getCount();
}
return $total;
}
/**
* Returns the total count of EC codewords.
*/
public function getTotalEcCodewords() : int
{
return $this->ecCodewordsPerBlock * $this->getNumBlocks();
}
/**
* Returns the EC blocks included in this collection.
*
* @return EcBlock[]
*/
public function getEcBlocks() : array
{
return $this->ecBlocks;
}
}
@@ -1,57 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use BaconQrCode\Exception\OutOfBoundsException;
use DASPRiD\Enum\AbstractEnum;
/**
* Enum representing the four error correction levels.
*
* @method static self L() ~7% correction
* @method static self M() ~15% correction
* @method static self Q() ~25% correction
* @method static self H() ~30% correction
*/
final class ErrorCorrectionLevel extends AbstractEnum
{
protected const L = [0x01];
protected const M = [0x00];
protected const Q = [0x03];
protected const H = [0x02];
protected function __construct(private readonly int $bits)
{
}
/**
* @throws OutOfBoundsException if number of bits is invalid
*/
public static function forBits(int $bits) : self
{
switch ($bits) {
case 0:
return self::M();
case 1:
return self::L();
case 2:
return self::H();
case 3:
return self::Q();
}
throw new OutOfBoundsException('Invalid number of bits');
}
/**
* Returns the two bits used to encode this error correction level.
*/
public function getBits() : int
{
return $this->bits;
}
}
@@ -1,196 +0,0 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Common;
/**
* Encapsulates a QR Code's format information, including the data mask used and error correction level.
*/
class FormatInformation
{
/**
* Mask for format information.
*/
private const FORMAT_INFO_MASK_QR = 0x5412;
/**
* Lookup table for decoding format information.
*
* See ISO 18004:2006, Annex C, Table C.1
*/
private const FORMAT_INFO_DECODE_LOOKUP = [
[0x5412, 0x00],
[0x5125, 0x01],
[0x5e7c, 0x02],
[0x5b4b, 0x03],
[0x45f9, 0x04],
[0x40ce, 0x05],
[0x4f97, 0x06],
[0x4aa0, 0x07],
[0x77c4, 0x08],
[0x72f3, 0x09],
[0x7daa, 0x0a],
[0x789d, 0x0b],
[0x662f, 0x0c],
[0x6318, 0x0d],
[0x6c41, 0x0e],
[0x6976, 0x0f],
[0x1689, 0x10],
[0x13be, 0x11],
[0x1ce7, 0x12],
[0x19d0, 0x13],
[0x0762, 0x14],
[0x0255, 0x15],
[0x0d0c, 0x16],
[0x083b, 0x17],
[0x355f, 0x18],
[0x3068, 0x19],
[0x3f31, 0x1a],
[0x3a06, 0x1b],
[0x24b4, 0x1c],
[0x2183, 0x1d],
[0x2eda, 0x1e],
[0x2bed, 0x1f],
];
/**
* Offset i holds the number of 1 bits in the binary representation of i.
*
* @var int[]
*/
private const BITS_SET_IN_HALF_BYTE = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4];
/**
* Error correction level.
*/
private ErrorCorrectionLevel $ecLevel;
private int $dataMask;
protected function __construct(int $formatInfo)
{
$this->ecLevel = ErrorCorrectionLevel::forBits(($formatInfo >> 3) & 0x3);
$this->dataMask = $formatInfo & 0x7;
}
/**
* Checks how many bits are different between two integers.
*/
public static function numBitsDiffering(int $a, int $b) : int
{
$a ^= $b;
return (
self::BITS_SET_IN_HALF_BYTE[$a & 0xf]
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 4) & 0xf)]
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 8) & 0xf)]
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 12) & 0xf)]
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 16) & 0xf)]
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 20) & 0xf)]
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 24) & 0xf)]
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 28) & 0xf)]
);
}
/**
* Decodes format information.
*/
public static function decodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2) : ?self
{
$formatInfo = self::doDecodeFormatInformation($maskedFormatInfo1, $maskedFormatInfo2);
if (null !== $formatInfo) {
return $formatInfo;
}
// Should return null, but, some QR codes apparently do not mask this info. Try again by actually masking the
// pattern first.
return self::doDecodeFormatInformation(
$maskedFormatInfo1 ^ self::FORMAT_INFO_MASK_QR,
$maskedFormatInfo2 ^ self::FORMAT_INFO_MASK_QR
);
}
/**
* Internal method for decoding format information.
*/
private static function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2) : ?self
{
$bestDifference = PHP_INT_MAX;
$bestFormatInfo = 0;
foreach (self::FORMAT_INFO_DECODE_LOOKUP as $decodeInfo) {
$targetInfo = $decodeInfo[0];
if ($targetInfo === $maskedFormatInfo1 || $targetInfo === $maskedFormatInfo2) {
// Found an exact match
return new self($decodeInfo[1]);
}
$bitsDifference = self::numBitsDiffering($maskedFormatInfo1, $targetInfo);
if ($bitsDifference < $bestDifference) {
$bestFormatInfo = $decodeInfo[1];
$bestDifference = $bitsDifference;
}
if ($maskedFormatInfo1 !== $maskedFormatInfo2) {
// Also try the other option
$bitsDifference = self::numBitsDiffering($maskedFormatInfo2, $targetInfo);
if ($bitsDifference < $bestDifference) {
$bestFormatInfo = $decodeInfo[1];
$bestDifference = $bitsDifference;
}
}
}
// Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match.
if ($bestDifference <= 3) {
return new self($bestFormatInfo);
}
return null;
}
/**
* Returns the error correction level.
*/
public function getErrorCorrectionLevel() : ErrorCorrectionLevel
{
return $this->ecLevel;
}
/**
* Returns the data mask.
*/
public function getDataMask() : int
{
return $this->dataMask;
}
/**
* Hashes the code of the EC level.
*/
public function hashCode() : int
{
return ($this->ecLevel->getBits() << 3) | $this->dataMask;
}
/**
* Verifies if this instance equals another one.
*/
public function equals(self $other) : bool
{
return (
$this->ecLevel === $other->ecLevel
&& $this->dataMask === $other->dataMask
);
}
}
-69
View File
@@ -1,69 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use DASPRiD\Enum\AbstractEnum;
/**
* Enum representing various modes in which data can be encoded to bits.
*
* @method static self TERMINATOR()
* @method static self NUMERIC()
* @method static self ALPHANUMERIC()
* @method static self STRUCTURED_APPEND()
* @method static self BYTE()
* @method static self ECI()
* @method static self KANJI()
* @method static self FNC1_FIRST_POSITION()
* @method static self FNC1_SECOND_POSITION()
* @method static self HANZI()
*/
final class Mode extends AbstractEnum
{
protected const TERMINATOR = [[0, 0, 0], 0x00];
protected const NUMERIC = [[10, 12, 14], 0x01];
protected const ALPHANUMERIC = [[9, 11, 13], 0x02];
protected const STRUCTURED_APPEND = [[0, 0, 0], 0x03];
protected const BYTE = [[8, 16, 16], 0x04];
protected const ECI = [[0, 0, 0], 0x07];
protected const KANJI = [[8, 10, 12], 0x08];
protected const FNC1_FIRST_POSITION = [[0, 0, 0], 0x05];
protected const FNC1_SECOND_POSITION = [[0, 0, 0], 0x09];
protected const HANZI = [[8, 10, 12], 0x0d];
/**
* @param int[] $characterCountBitsForVersions
*/
protected function __construct(
private readonly array $characterCountBitsForVersions,
private readonly int $bits
) {
}
/**
* Returns the number of bits used in a specific QR code version.
*/
public function getCharacterCountBits(Version $version) : int
{
$number = $version->getVersionNumber();
if ($number <= 9) {
$offset = 0;
} elseif ($number <= 26) {
$offset = 1;
} else {
$offset = 2;
}
return $this->characterCountBitsForVersions[$offset];
}
/**
* Returns the four bits used to encode this mode.
*/
public function getBits() : int
{
return $this->bits;
}
}
@@ -1,454 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use BaconQrCode\Exception\InvalidArgumentException;
use BaconQrCode\Exception\RuntimeException;
use SplFixedArray;
/**
* Reed-Solomon codec for 8-bit characters.
*
* Based on libfec by Phil Karn, KA9Q.
*/
final class ReedSolomonCodec
{
/**
* Symbol size in bits.
*/
private int $symbolSize;
/**
* Block size in symbols.
*/
private int $blockSize;
/**
* First root of RS code generator polynomial, index form.
*/
private int $firstRoot;
/**
* Primitive element to generate polynomial roots, index form.
*/
private int $primitive;
/**
* Prim-th root of 1, index form.
*/
private int $iPrimitive;
/**
* RS code generator polynomial degree (number of roots).
*/
private int $numRoots;
/**
* Padding bytes at front of shortened block.
*/
private int $padding;
/**
* Log lookup table.
*
* @var SplFixedArray
*/
private SplFixedArray $alphaTo;
/**
* Anti-Log lookup table.
*
* @var SplFixedArray
*/
private SplFixedArray $indexOf;
/**
* Generator polynomial.
*
* @var SplFixedArray
*/
private SplFixedArray $generatorPoly;
/**
* @throws InvalidArgumentException if symbol size ist not between 0 and 8
* @throws InvalidArgumentException if first root is invalid
* @throws InvalidArgumentException if num roots is invalid
* @throws InvalidArgumentException if padding is invalid
* @throws RuntimeException if field generator polynomial is not primitive
*/
public function __construct(
int $symbolSize,
int $gfPoly,
int $firstRoot,
int $primitive,
int $numRoots,
int $padding
) {
if ($symbolSize < 0 || $symbolSize > 8) {
throw new InvalidArgumentException('Symbol size must be between 0 and 8');
}
if ($firstRoot < 0 || $firstRoot >= (1 << $symbolSize)) {
throw new InvalidArgumentException('First root must be between 0 and ' . (1 << $symbolSize));
}
if ($numRoots < 0 || $numRoots >= (1 << $symbolSize)) {
throw new InvalidArgumentException('Num roots must be between 0 and ' . (1 << $symbolSize));
}
if ($padding < 0 || $padding >= ((1 << $symbolSize) - 1 - $numRoots)) {
throw new InvalidArgumentException(
'Padding must be between 0 and ' . ((1 << $symbolSize) - 1 - $numRoots)
);
}
$this->symbolSize = $symbolSize;
$this->blockSize = (1 << $symbolSize) - 1;
$this->padding = $padding;
$this->alphaTo = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false);
$this->indexOf = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false);
// Generate galous field lookup table
$this->indexOf[0] = $this->blockSize;
$this->alphaTo[$this->blockSize] = 0;
$sr = 1;
for ($i = 0; $i < $this->blockSize; ++$i) {
$this->indexOf[$sr] = $i;
$this->alphaTo[$i] = $sr;
$sr <<= 1;
if ($sr & (1 << $symbolSize)) {
$sr ^= $gfPoly;
}
$sr &= $this->blockSize;
}
if (1 !== $sr) {
throw new RuntimeException('Field generator polynomial is not primitive');
}
// Form RS code generator polynomial from its roots
$this->generatorPoly = SplFixedArray::fromArray(array_fill(0, $numRoots + 1, 0), false);
$this->firstRoot = $firstRoot;
$this->primitive = $primitive;
$this->numRoots = $numRoots;
// Find prim-th root of 1, used in decoding
for ($iPrimitive = 1; ($iPrimitive % $primitive) !== 0; $iPrimitive += $this->blockSize) {
}
$this->iPrimitive = intdiv($iPrimitive, $primitive);
$this->generatorPoly[0] = 1;
for ($i = 0, $root = $firstRoot * $primitive; $i < $numRoots; ++$i, $root += $primitive) {
$this->generatorPoly[$i + 1] = 1;
for ($j = $i; $j > 0; $j--) {
if ($this->generatorPoly[$j] !== 0) {
$this->generatorPoly[$j] = $this->generatorPoly[$j - 1] ^ $this->alphaTo[
$this->modNn($this->indexOf[$this->generatorPoly[$j]] + $root)
];
} else {
$this->generatorPoly[$j] = $this->generatorPoly[$j - 1];
}
}
$this->generatorPoly[$j] = $this->alphaTo[$this->modNn($this->indexOf[$this->generatorPoly[0]] + $root)];
}
// Convert generator poly to index form for quicker encoding
for ($i = 0; $i <= $numRoots; ++$i) {
$this->generatorPoly[$i] = $this->indexOf[$this->generatorPoly[$i]];
}
}
/**
* Encodes data and writes result back into parity array.
*/
public function encode(SplFixedArray $data, SplFixedArray $parity) : void
{
for ($i = 0; $i < $this->numRoots; ++$i) {
$parity[$i] = 0;
}
$iterations = $this->blockSize - $this->numRoots - $this->padding;
for ($i = 0; $i < $iterations; ++$i) {
$feedback = $this->indexOf[$data[$i] ^ $parity[0]];
if ($feedback !== $this->blockSize) {
// Feedback term is non-zero
$feedback = $this->modNn($this->blockSize - $this->generatorPoly[$this->numRoots] + $feedback);
for ($j = 1; $j < $this->numRoots; ++$j) {
$parity[$j] = $parity[$j] ^ $this->alphaTo[
$this->modNn($feedback + $this->generatorPoly[$this->numRoots - $j])
];
}
}
for ($j = 0; $j < $this->numRoots - 1; ++$j) {
$parity[$j] = $parity[$j + 1];
}
if ($feedback !== $this->blockSize) {
$parity[$this->numRoots - 1] = $this->alphaTo[$this->modNn($feedback + $this->generatorPoly[0])];
} else {
$parity[$this->numRoots - 1] = 0;
}
}
}
/**
* Decodes received data.
*/
public function decode(SplFixedArray $data, ?SplFixedArray $erasures = null) : ?int
{
// This speeds up the initialization a bit.
$numRootsPlusOne = SplFixedArray::fromArray(array_fill(0, $this->numRoots + 1, 0), false);
$numRoots = SplFixedArray::fromArray(array_fill(0, $this->numRoots, 0), false);
$lambda = clone $numRootsPlusOne;
$b = clone $numRootsPlusOne;
$t = clone $numRootsPlusOne;
$omega = clone $numRootsPlusOne;
$root = clone $numRoots;
$loc = clone $numRoots;
$numErasures = (null !== $erasures ? count($erasures) : 0);
// Form the Syndromes; i.e., evaluate data(x) at roots of g(x)
$syndromes = SplFixedArray::fromArray(array_fill(0, $this->numRoots, $data[0]), false);
for ($i = 1; $i < $this->blockSize - $this->padding; ++$i) {
for ($j = 0; $j < $this->numRoots; ++$j) {
if ($syndromes[$j] === 0) {
$syndromes[$j] = $data[$i];
} else {
$syndromes[$j] = $data[$i] ^ $this->alphaTo[
$this->modNn($this->indexOf[$syndromes[$j]] + ($this->firstRoot + $j) * $this->primitive)
];
}
}
}
// Convert syndromes to index form, checking for nonzero conditions
$syndromeError = 0;
for ($i = 0; $i < $this->numRoots; ++$i) {
$syndromeError |= $syndromes[$i];
$syndromes[$i] = $this->indexOf[$syndromes[$i]];
}
if (! $syndromeError) {
// If syndrome is zero, data[] is a codeword and there are no errors to correct, so return data[]
// unmodified.
return 0;
}
$lambda[0] = 1;
if ($numErasures > 0) {
// Init lambda to be the erasure locator polynomial
$lambda[1] = $this->alphaTo[$this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[0]))];
for ($i = 1; $i < $numErasures; ++$i) {
$u = $this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[$i]));
for ($j = $i + 1; $j > 0; --$j) {
$tmp = $this->indexOf[$lambda[$j - 1]];
if ($tmp !== $this->blockSize) {
$lambda[$j] = $lambda[$j] ^ $this->alphaTo[$this->modNn($u + $tmp)];
}
}
}
}
for ($i = 0; $i <= $this->numRoots; ++$i) {
$b[$i] = $this->indexOf[$lambda[$i]];
}
// Begin Berlekamp-Massey algorithm to determine error+erasure locator polynomial
$r = $numErasures;
$el = $numErasures;
while (++$r <= $this->numRoots) {
// Compute discrepancy at the r-th step in poly form
$discrepancyR = 0;
for ($i = 0; $i < $r; ++$i) {
if ($lambda[$i] !== 0 && $syndromes[$r - $i - 1] !== $this->blockSize) {
$discrepancyR ^= $this->alphaTo[
$this->modNn($this->indexOf[$lambda[$i]] + $syndromes[$r - $i - 1])
];
}
}
$discrepancyR = $this->indexOf[$discrepancyR];
if ($discrepancyR === $this->blockSize) {
$tmp = $b->toArray();
array_unshift($tmp, $this->blockSize);
array_pop($tmp);
$b = SplFixedArray::fromArray($tmp, false);
continue;
}
$t[0] = $lambda[0];
for ($i = 0; $i < $this->numRoots; ++$i) {
if ($b[$i] !== $this->blockSize) {
$t[$i + 1] = $lambda[$i + 1] ^ $this->alphaTo[$this->modNn($discrepancyR + $b[$i])];
} else {
$t[$i + 1] = $lambda[$i + 1];
}
}
if (2 * $el <= $r + $numErasures - 1) {
$el = $r + $numErasures - $el;
for ($i = 0; $i <= $this->numRoots; ++$i) {
$b[$i] = (
$lambda[$i] === 0
? $this->blockSize
: $this->modNn($this->indexOf[$lambda[$i]] - $discrepancyR + $this->blockSize)
);
}
} else {
$tmp = $b->toArray();
array_unshift($tmp, $this->blockSize);
array_pop($tmp);
$b = SplFixedArray::fromArray($tmp, false);
}
$lambda = clone $t;
}
// Convert lambda to index form and compute deg(lambda(x))
$degLambda = 0;
for ($i = 0; $i <= $this->numRoots; ++$i) {
$lambda[$i] = $this->indexOf[$lambda[$i]];
if ($lambda[$i] !== $this->blockSize) {
$degLambda = $i;
}
}
// Find roots of the error+erasure locator polynomial by Chien search.
$reg = clone $lambda;
$reg[0] = 0;
$count = 0;
$i = 1;
for ($k = $this->iPrimitive - 1; $i <= $this->blockSize; ++$i, $k = $this->modNn($k + $this->iPrimitive)) {
$q = 1;
for ($j = $degLambda; $j > 0; $j--) {
if ($reg[$j] !== $this->blockSize) {
$reg[$j] = $this->modNn($reg[$j] + $j);
$q ^= $this->alphaTo[$reg[$j]];
}
}
if ($q !== 0) {
// Not a root
continue;
}
// Store root (index-form) and error location number
$root[$count] = $i;
$loc[$count] = $k;
if (++$count === $degLambda) {
break;
}
}
if ($degLambda !== $count) {
// deg(lambda) unequal to number of roots: uncorrectable error detected
return null;
}
// Compute err+eras evaluate poly omega(x) = s(x)*lambda(x) (modulo x**numRoots). In index form. Also find
// deg(omega).
$degOmega = $degLambda - 1;
for ($i = 0; $i <= $degOmega; ++$i) {
$tmp = 0;
for ($j = $i; $j >= 0; --$j) {
if ($syndromes[$i - $j] !== $this->blockSize && $lambda[$j] !== $this->blockSize) {
$tmp ^= $this->alphaTo[$this->modNn($syndromes[$i - $j] + $lambda[$j])];
}
}
$omega[$i] = $this->indexOf[$tmp];
}
// Compute error values in poly-form. num1 = omega(inv(X(l))), num2 = inv(X(l))**(firstRoot-1) and
// den = lambda_pr(inv(X(l))) all in poly form.
for ($j = $count - 1; $j >= 0; --$j) {
$num1 = 0;
for ($i = $degOmega; $i >= 0; $i--) {
if ($omega[$i] !== $this->blockSize) {
$num1 ^= $this->alphaTo[$this->modNn($omega[$i] + $i * $root[$j])];
}
}
$num2 = $this->alphaTo[$this->modNn($root[$j] * ($this->firstRoot - 1) + $this->blockSize)];
$den = 0;
// lambda[i+1] for i even is the formal derivativelambda_pr of lambda[i]
for ($i = min($degLambda, $this->numRoots - 1) & ~1; $i >= 0; $i -= 2) {
if ($lambda[$i + 1] !== $this->blockSize) {
$den ^= $this->alphaTo[$this->modNn($lambda[$i + 1] + $i * $root[$j])];
}
}
// Apply error to data
if ($num1 !== 0 && $loc[$j] >= $this->padding) {
$data[$loc[$j] - $this->padding] = $data[$loc[$j] - $this->padding] ^ (
$this->alphaTo[
$this->modNn(
$this->indexOf[$num1] + $this->indexOf[$num2] + $this->blockSize - $this->indexOf[$den]
)
]
);
}
}
if (null !== $erasures) {
if (count($erasures) < $count) {
$erasures->setSize($count);
}
for ($i = 0; $i < $count; $i++) {
$erasures[$i] = $loc[$i];
}
}
return $count;
}
/**
* Computes $x % GF_SIZE, where GF_SIZE is 2**GF_BITS - 1, without a slow divide.
*/
private function modNn(int $x) : int
{
while ($x >= $this->blockSize) {
$x -= $this->blockSize;
$x = ($x >> $this->symbolSize) + ($x & $this->blockSize);
}
return $x;
}
}
-592
View File
@@ -1,592 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use BaconQrCode\Exception\InvalidArgumentException;
use SplFixedArray;
/**
* Version representation.
*/
final class Version
{
private const VERSION_DECODE_INFO = [
0x07c94,
0x085bc,
0x09a99,
0x0a4d3,
0x0bbf6,
0x0c762,
0x0d847,
0x0e60d,
0x0f928,
0x10b78,
0x1145d,
0x12a17,
0x13532,
0x149a6,
0x15683,
0x168c9,
0x177ec,
0x18ec4,
0x191e1,
0x1afab,
0x1b08e,
0x1cc1a,
0x1d33f,
0x1ed75,
0x1f250,
0x209d5,
0x216f0,
0x228ba,
0x2379f,
0x24b0b,
0x2542e,
0x26a64,
0x27541,
0x28c69,
];
/**
* Version number of this version.
*/
private int $versionNumber;
/**
* Alignment pattern centers.
*
* @var SplFixedArray|array
*/
private SplFixedArray|array $alignmentPatternCenters;
/**
* Error correction blocks.
*
* @var EcBlocks[]
*/
private array $ecBlocks;
/**
* Total number of codewords.
*/
private null|int|float $totalCodewords;
/**
* Cached version instances.
*
* @var array<int, self>|null
*/
private static ?array $versions = null;
/**
* @param int[] $alignmentPatternCenters
*/
private function __construct(
int $versionNumber,
array $alignmentPatternCenters,
EcBlocks ...$ecBlocks
) {
$this->versionNumber = $versionNumber;
$this->alignmentPatternCenters = $alignmentPatternCenters;
$this->ecBlocks = $ecBlocks;
$totalCodewords = 0;
$ecCodewords = $ecBlocks[0]->getEcCodewordsPerBlock();
foreach ($ecBlocks[0]->getEcBlocks() as $ecBlock) {
$totalCodewords += $ecBlock->getCount() * ($ecBlock->getDataCodewords() + $ecCodewords);
}
$this->totalCodewords = $totalCodewords;
}
/**
* Returns the version number.
*/
public function getVersionNumber() : int
{
return $this->versionNumber;
}
/**
* Returns the alignment pattern centers.
*
* @return int[]
*/
public function getAlignmentPatternCenters() : array
{
return $this->alignmentPatternCenters;
}
/**
* Returns the total number of codewords.
*/
public function getTotalCodewords() : int
{
return $this->totalCodewords;
}
/**
* Calculates the dimension for the current version.
*/
public function getDimensionForVersion() : int
{
return 17 + 4 * $this->versionNumber;
}
/**
* Returns the number of EC blocks for a specific EC level.
*/
public function getEcBlocksForLevel(ErrorCorrectionLevel $ecLevel) : EcBlocks
{
return $this->ecBlocks[$ecLevel->ordinal()];
}
/**
* Gets a provisional version number for a specific dimension.
*
* @throws InvalidArgumentException if dimension is not 1 mod 4
*/
public static function getProvisionalVersionForDimension(int $dimension) : self
{
if (1 !== $dimension % 4) {
throw new InvalidArgumentException('Dimension is not 1 mod 4');
}
return self::getVersionForNumber(intdiv($dimension - 17, 4));
}
/**
* Gets a version instance for a specific version number.
*
* @throws InvalidArgumentException if version number is out of range
*/
public static function getVersionForNumber(int $versionNumber) : self
{
if ($versionNumber < 1 || $versionNumber > 40) {
throw new InvalidArgumentException('Version number must be between 1 and 40');
}
return self::versions()[$versionNumber - 1];
}
/**
* Decodes version information from an integer and returns the version.
*/
public static function decodeVersionInformation(int $versionBits) : ?self
{
$bestDifference = PHP_INT_MAX;
$bestVersion = 0;
foreach (self::VERSION_DECODE_INFO as $i => $targetVersion) {
if ($targetVersion === $versionBits) {
return self::getVersionForNumber($i + 7);
}
$bitsDifference = FormatInformation::numBitsDiffering($versionBits, $targetVersion);
if ($bitsDifference < $bestDifference) {
$bestVersion = $i + 7;
$bestDifference = $bitsDifference;
}
}
if ($bestDifference <= 3) {
return self::getVersionForNumber($bestVersion);
}
return null;
}
/**
* Builds the function pattern for the current version.
*/
public function buildFunctionPattern() : BitMatrix
{
$dimension = $this->getDimensionForVersion();
$bitMatrix = new BitMatrix($dimension);
// Top left finder pattern + separator + format
$bitMatrix->setRegion(0, 0, 9, 9);
// Top right finder pattern + separator + format
$bitMatrix->setRegion($dimension - 8, 0, 8, 9);
// Bottom left finder pattern + separator + format
$bitMatrix->setRegion(0, $dimension - 8, 9, 8);
// Alignment patterns
$max = count($this->alignmentPatternCenters);
for ($x = 0; $x < $max; ++$x) {
$i = $this->alignmentPatternCenters[$x] - 2;
for ($y = 0; $y < $max; ++$y) {
if (($x === 0 && ($y === 0 || $y === $max - 1)) || ($x === $max - 1 && $y === 0)) {
// No alignment patterns near the three finder paterns
continue;
}
$bitMatrix->setRegion($this->alignmentPatternCenters[$y] - 2, $i, 5, 5);
}
}
// Vertical timing pattern
$bitMatrix->setRegion(6, 9, 1, $dimension - 17);
// Horizontal timing pattern
$bitMatrix->setRegion(9, 6, $dimension - 17, 1);
if ($this->versionNumber > 6) {
// Version info, top right
$bitMatrix->setRegion($dimension - 11, 0, 3, 6);
// Version info, bottom left
$bitMatrix->setRegion(0, $dimension - 11, 6, 3);
}
return $bitMatrix;
}
/**
* Returns a string representation for the version.
*/
public function __toString() : string
{
return (string) $this->versionNumber;
}
/**
* Build and cache a specific version.
*
* See ISO 18004:2006 6.5.1 Table 9.
*
* @return array<int, self>
*/
private static function versions() : array
{
if (null !== self::$versions) {
return self::$versions;
}
return self::$versions = [
new self(
1,
[],
new EcBlocks(7, new EcBlock(1, 19)),
new EcBlocks(10, new EcBlock(1, 16)),
new EcBlocks(13, new EcBlock(1, 13)),
new EcBlocks(17, new EcBlock(1, 9))
),
new self(
2,
[6, 18],
new EcBlocks(10, new EcBlock(1, 34)),
new EcBlocks(16, new EcBlock(1, 28)),
new EcBlocks(22, new EcBlock(1, 22)),
new EcBlocks(28, new EcBlock(1, 16))
),
new self(
3,
[6, 22],
new EcBlocks(15, new EcBlock(1, 55)),
new EcBlocks(26, new EcBlock(1, 44)),
new EcBlocks(18, new EcBlock(2, 17)),
new EcBlocks(22, new EcBlock(2, 13))
),
new self(
4,
[6, 26],
new EcBlocks(20, new EcBlock(1, 80)),
new EcBlocks(18, new EcBlock(2, 32)),
new EcBlocks(26, new EcBlock(2, 24)),
new EcBlocks(16, new EcBlock(4, 9))
),
new self(
5,
[6, 30],
new EcBlocks(26, new EcBlock(1, 108)),
new EcBlocks(24, new EcBlock(2, 43)),
new EcBlocks(18, new EcBlock(2, 15), new EcBlock(2, 16)),
new EcBlocks(22, new EcBlock(2, 11), new EcBlock(2, 12))
),
new self(
6,
[6, 34],
new EcBlocks(18, new EcBlock(2, 68)),
new EcBlocks(16, new EcBlock(4, 27)),
new EcBlocks(24, new EcBlock(4, 19)),
new EcBlocks(28, new EcBlock(4, 15))
),
new self(
7,
[6, 22, 38],
new EcBlocks(20, new EcBlock(2, 78)),
new EcBlocks(18, new EcBlock(4, 31)),
new EcBlocks(18, new EcBlock(2, 14), new EcBlock(4, 15)),
new EcBlocks(26, new EcBlock(4, 13), new EcBlock(1, 14))
),
new self(
8,
[6, 24, 42],
new EcBlocks(24, new EcBlock(2, 97)),
new EcBlocks(22, new EcBlock(2, 38), new EcBlock(2, 39)),
new EcBlocks(22, new EcBlock(4, 18), new EcBlock(2, 19)),
new EcBlocks(26, new EcBlock(4, 14), new EcBlock(2, 15))
),
new self(
9,
[6, 26, 46],
new EcBlocks(30, new EcBlock(2, 116)),
new EcBlocks(22, new EcBlock(3, 36), new EcBlock(2, 37)),
new EcBlocks(20, new EcBlock(4, 16), new EcBlock(4, 17)),
new EcBlocks(24, new EcBlock(4, 12), new EcBlock(4, 13))
),
new self(
10,
[6, 28, 50],
new EcBlocks(18, new EcBlock(2, 68), new EcBlock(2, 69)),
new EcBlocks(26, new EcBlock(4, 43), new EcBlock(1, 44)),
new EcBlocks(24, new EcBlock(6, 19), new EcBlock(2, 20)),
new EcBlocks(28, new EcBlock(6, 15), new EcBlock(2, 16))
),
new self(
11,
[6, 30, 54],
new EcBlocks(20, new EcBlock(4, 81)),
new EcBlocks(30, new EcBlock(1, 50), new EcBlock(4, 51)),
new EcBlocks(28, new EcBlock(4, 22), new EcBlock(4, 23)),
new EcBlocks(24, new EcBlock(3, 12), new EcBlock(8, 13))
),
new self(
12,
[6, 32, 58],
new EcBlocks(24, new EcBlock(2, 92), new EcBlock(2, 93)),
new EcBlocks(22, new EcBlock(6, 36), new EcBlock(2, 37)),
new EcBlocks(26, new EcBlock(4, 20), new EcBlock(6, 21)),
new EcBlocks(28, new EcBlock(7, 14), new EcBlock(4, 15))
),
new self(
13,
[6, 34, 62],
new EcBlocks(26, new EcBlock(4, 107)),
new EcBlocks(22, new EcBlock(8, 37), new EcBlock(1, 38)),
new EcBlocks(24, new EcBlock(8, 20), new EcBlock(4, 21)),
new EcBlocks(22, new EcBlock(12, 11), new EcBlock(4, 12))
),
new self(
14,
[6, 26, 46, 66],
new EcBlocks(30, new EcBlock(3, 115), new EcBlock(1, 116)),
new EcBlocks(24, new EcBlock(4, 40), new EcBlock(5, 41)),
new EcBlocks(20, new EcBlock(11, 16), new EcBlock(5, 17)),
new EcBlocks(24, new EcBlock(11, 12), new EcBlock(5, 13))
),
new self(
15,
[6, 26, 48, 70],
new EcBlocks(22, new EcBlock(5, 87), new EcBlock(1, 88)),
new EcBlocks(24, new EcBlock(5, 41), new EcBlock(5, 42)),
new EcBlocks(30, new EcBlock(5, 24), new EcBlock(7, 25)),
new EcBlocks(24, new EcBlock(11, 12), new EcBlock(7, 13))
),
new self(
16,
[6, 26, 50, 74],
new EcBlocks(24, new EcBlock(5, 98), new EcBlock(1, 99)),
new EcBlocks(28, new EcBlock(7, 45), new EcBlock(3, 46)),
new EcBlocks(24, new EcBlock(15, 19), new EcBlock(2, 20)),
new EcBlocks(30, new EcBlock(3, 15), new EcBlock(13, 16))
),
new self(
17,
[6, 30, 54, 78],
new EcBlocks(28, new EcBlock(1, 107), new EcBlock(5, 108)),
new EcBlocks(28, new EcBlock(10, 46), new EcBlock(1, 47)),
new EcBlocks(28, new EcBlock(1, 22), new EcBlock(15, 23)),
new EcBlocks(28, new EcBlock(2, 14), new EcBlock(17, 15))
),
new self(
18,
[6, 30, 56, 82],
new EcBlocks(30, new EcBlock(5, 120), new EcBlock(1, 121)),
new EcBlocks(26, new EcBlock(9, 43), new EcBlock(4, 44)),
new EcBlocks(28, new EcBlock(17, 22), new EcBlock(1, 23)),
new EcBlocks(28, new EcBlock(2, 14), new EcBlock(19, 15))
),
new self(
19,
[6, 30, 58, 86],
new EcBlocks(28, new EcBlock(3, 113), new EcBlock(4, 114)),
new EcBlocks(26, new EcBlock(3, 44), new EcBlock(11, 45)),
new EcBlocks(26, new EcBlock(17, 21), new EcBlock(4, 22)),
new EcBlocks(26, new EcBlock(9, 13), new EcBlock(16, 14))
),
new self(
20,
[6, 34, 62, 90],
new EcBlocks(28, new EcBlock(3, 107), new EcBlock(5, 108)),
new EcBlocks(26, new EcBlock(3, 41), new EcBlock(13, 42)),
new EcBlocks(30, new EcBlock(15, 24), new EcBlock(5, 25)),
new EcBlocks(28, new EcBlock(15, 15), new EcBlock(10, 16))
),
new self(
21,
[6, 28, 50, 72, 94],
new EcBlocks(28, new EcBlock(4, 116), new EcBlock(4, 117)),
new EcBlocks(26, new EcBlock(17, 42)),
new EcBlocks(28, new EcBlock(17, 22), new EcBlock(6, 23)),
new EcBlocks(30, new EcBlock(19, 16), new EcBlock(6, 17))
),
new self(
22,
[6, 26, 50, 74, 98],
new EcBlocks(28, new EcBlock(2, 111), new EcBlock(7, 112)),
new EcBlocks(28, new EcBlock(17, 46)),
new EcBlocks(30, new EcBlock(7, 24), new EcBlock(16, 25)),
new EcBlocks(24, new EcBlock(34, 13))
),
new self(
23,
[6, 30, 54, 78, 102],
new EcBlocks(30, new EcBlock(4, 121), new EcBlock(5, 122)),
new EcBlocks(28, new EcBlock(4, 47), new EcBlock(14, 48)),
new EcBlocks(30, new EcBlock(11, 24), new EcBlock(14, 25)),
new EcBlocks(30, new EcBlock(16, 15), new EcBlock(14, 16))
),
new self(
24,
[6, 28, 54, 80, 106],
new EcBlocks(30, new EcBlock(6, 117), new EcBlock(4, 118)),
new EcBlocks(28, new EcBlock(6, 45), new EcBlock(14, 46)),
new EcBlocks(30, new EcBlock(11, 24), new EcBlock(16, 25)),
new EcBlocks(30, new EcBlock(30, 16), new EcBlock(2, 17))
),
new self(
25,
[6, 32, 58, 84, 110],
new EcBlocks(26, new EcBlock(8, 106), new EcBlock(4, 107)),
new EcBlocks(28, new EcBlock(8, 47), new EcBlock(13, 48)),
new EcBlocks(30, new EcBlock(7, 24), new EcBlock(22, 25)),
new EcBlocks(30, new EcBlock(22, 15), new EcBlock(13, 16))
),
new self(
26,
[6, 30, 58, 86, 114],
new EcBlocks(28, new EcBlock(10, 114), new EcBlock(2, 115)),
new EcBlocks(28, new EcBlock(19, 46), new EcBlock(4, 47)),
new EcBlocks(28, new EcBlock(28, 22), new EcBlock(6, 23)),
new EcBlocks(30, new EcBlock(33, 16), new EcBlock(4, 17))
),
new self(
27,
[6, 34, 62, 90, 118],
new EcBlocks(30, new EcBlock(8, 122), new EcBlock(4, 123)),
new EcBlocks(28, new EcBlock(22, 45), new EcBlock(3, 46)),
new EcBlocks(30, new EcBlock(8, 23), new EcBlock(26, 24)),
new EcBlocks(30, new EcBlock(12, 15), new EcBlock(28, 16))
),
new self(
28,
[6, 26, 50, 74, 98, 122],
new EcBlocks(30, new EcBlock(3, 117), new EcBlock(10, 118)),
new EcBlocks(28, new EcBlock(3, 45), new EcBlock(23, 46)),
new EcBlocks(30, new EcBlock(4, 24), new EcBlock(31, 25)),
new EcBlocks(30, new EcBlock(11, 15), new EcBlock(31, 16))
),
new self(
29,
[6, 30, 54, 78, 102, 126],
new EcBlocks(30, new EcBlock(7, 116), new EcBlock(7, 117)),
new EcBlocks(28, new EcBlock(21, 45), new EcBlock(7, 46)),
new EcBlocks(30, new EcBlock(1, 23), new EcBlock(37, 24)),
new EcBlocks(30, new EcBlock(19, 15), new EcBlock(26, 16))
),
new self(
30,
[6, 26, 52, 78, 104, 130],
new EcBlocks(30, new EcBlock(5, 115), new EcBlock(10, 116)),
new EcBlocks(28, new EcBlock(19, 47), new EcBlock(10, 48)),
new EcBlocks(30, new EcBlock(15, 24), new EcBlock(25, 25)),
new EcBlocks(30, new EcBlock(23, 15), new EcBlock(25, 16))
),
new self(
31,
[6, 30, 56, 82, 108, 134],
new EcBlocks(30, new EcBlock(13, 115), new EcBlock(3, 116)),
new EcBlocks(28, new EcBlock(2, 46), new EcBlock(29, 47)),
new EcBlocks(30, new EcBlock(42, 24), new EcBlock(1, 25)),
new EcBlocks(30, new EcBlock(23, 15), new EcBlock(28, 16))
),
new self(
32,
[6, 34, 60, 86, 112, 138],
new EcBlocks(30, new EcBlock(17, 115)),
new EcBlocks(28, new EcBlock(10, 46), new EcBlock(23, 47)),
new EcBlocks(30, new EcBlock(10, 24), new EcBlock(35, 25)),
new EcBlocks(30, new EcBlock(19, 15), new EcBlock(35, 16))
),
new self(
33,
[6, 30, 58, 86, 114, 142],
new EcBlocks(30, new EcBlock(17, 115), new EcBlock(1, 116)),
new EcBlocks(28, new EcBlock(14, 46), new EcBlock(21, 47)),
new EcBlocks(30, new EcBlock(29, 24), new EcBlock(19, 25)),
new EcBlocks(30, new EcBlock(11, 15), new EcBlock(46, 16))
),
new self(
34,
[6, 34, 62, 90, 118, 146],
new EcBlocks(30, new EcBlock(13, 115), new EcBlock(6, 116)),
new EcBlocks(28, new EcBlock(14, 46), new EcBlock(23, 47)),
new EcBlocks(30, new EcBlock(44, 24), new EcBlock(7, 25)),
new EcBlocks(30, new EcBlock(59, 16), new EcBlock(1, 17))
),
new self(
35,
[6, 30, 54, 78, 102, 126, 150],
new EcBlocks(30, new EcBlock(12, 121), new EcBlock(7, 122)),
new EcBlocks(28, new EcBlock(12, 47), new EcBlock(26, 48)),
new EcBlocks(30, new EcBlock(39, 24), new EcBlock(14, 25)),
new EcBlocks(30, new EcBlock(22, 15), new EcBlock(41, 16))
),
new self(
36,
[6, 24, 50, 76, 102, 128, 154],
new EcBlocks(30, new EcBlock(6, 121), new EcBlock(14, 122)),
new EcBlocks(28, new EcBlock(6, 47), new EcBlock(34, 48)),
new EcBlocks(30, new EcBlock(46, 24), new EcBlock(10, 25)),
new EcBlocks(30, new EcBlock(2, 15), new EcBlock(64, 16))
),
new self(
37,
[6, 28, 54, 80, 106, 132, 158],
new EcBlocks(30, new EcBlock(17, 122), new EcBlock(4, 123)),
new EcBlocks(28, new EcBlock(29, 46), new EcBlock(14, 47)),
new EcBlocks(30, new EcBlock(49, 24), new EcBlock(10, 25)),
new EcBlocks(30, new EcBlock(24, 15), new EcBlock(46, 16))
),
new self(
38,
[6, 32, 58, 84, 110, 136, 162],
new EcBlocks(30, new EcBlock(4, 122), new EcBlock(18, 123)),
new EcBlocks(28, new EcBlock(13, 46), new EcBlock(32, 47)),
new EcBlocks(30, new EcBlock(48, 24), new EcBlock(14, 25)),
new EcBlocks(30, new EcBlock(42, 15), new EcBlock(32, 16))
),
new self(
39,
[6, 26, 54, 82, 110, 138, 166],
new EcBlocks(30, new EcBlock(20, 117), new EcBlock(4, 118)),
new EcBlocks(28, new EcBlock(40, 47), new EcBlock(7, 48)),
new EcBlocks(30, new EcBlock(43, 24), new EcBlock(22, 25)),
new EcBlocks(30, new EcBlock(10, 15), new EcBlock(67, 16))
),
new self(
40,
[6, 30, 58, 86, 114, 142, 170],
new EcBlocks(30, new EcBlock(19, 118), new EcBlock(6, 119)),
new EcBlocks(28, new EcBlock(18, 47), new EcBlock(31, 48)),
new EcBlocks(30, new EcBlock(34, 24), new EcBlock(34, 25)),
new EcBlocks(30, new EcBlock(20, 15), new EcBlock(61, 16))
),
];
}
}
-44
View File
@@ -1,44 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Encoder;
use SplFixedArray;
/**
* Block pair.
*/
final class BlockPair
{
/**
* Creates a new block pair.
*
* @param SplFixedArray<int> $dataBytes Data bytes in the block.
* @param SplFixedArray<int> $errorCorrectionBytes Error correction bytes in the block.
*/
public function __construct(
private readonly SplFixedArray $dataBytes,
private readonly SplFixedArray $errorCorrectionBytes
) {
}
/**
* Gets the data bytes.
*
* @return SplFixedArray<int>
*/
public function getDataBytes() : SplFixedArray
{
return $this->dataBytes;
}
/**
* Gets the error correction bytes.
*
* @return SplFixedArray<int>
*/
public function getErrorCorrectionBytes() : SplFixedArray
{
return $this->errorCorrectionBytes;
}
}
-134
View File
@@ -1,134 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Encoder;
use SplFixedArray;
use Traversable;
/**
* Byte matrix.
*/
final class ByteMatrix
{
/**
* Bytes in the matrix, represented as array.
*
* @var SplFixedArray<SplFixedArray<int>>
*/
private SplFixedArray $bytes;
public function __construct(private readonly int $width, private readonly int $height)
{
$this->bytes = new SplFixedArray($height);
for ($y = 0; $y < $height; ++$y) {
$this->bytes[$y] = SplFixedArray::fromArray(array_fill(0, $width, 0));
}
}
/**
* Gets the width of the matrix.
*/
public function getWidth() : int
{
return $this->width;
}
/**
* Gets the height of the matrix.
*/
public function getHeight() : int
{
return $this->height;
}
/**
* Gets the internal representation of the matrix.
*
* @return SplFixedArray<SplFixedArray<int>>
*/
public function getArray() : SplFixedArray
{
return $this->bytes;
}
/**
* @return Traversable<int>
*/
public function getBytes() : Traversable
{
foreach ($this->bytes as $row) {
foreach ($row as $byte) {
yield $byte;
}
}
}
/**
* Gets the byte for a specific position.
*/
public function get(int $x, int $y) : int
{
return $this->bytes[$y][$x];
}
/**
* Sets the byte for a specific position.
*/
public function set(int $x, int $y, int $value) : void
{
$this->bytes[$y][$x] = $value;
}
/**
* Clears the matrix with a specific value.
*/
public function clear(int $value) : void
{
for ($y = 0; $y < $this->height; ++$y) {
for ($x = 0; $x < $this->width; ++$x) {
$this->bytes[$y][$x] = $value;
}
}
}
public function __clone()
{
$this->bytes = clone $this->bytes;
foreach ($this->bytes as $index => $row) {
$this->bytes[$index] = clone $row;
}
}
/**
* Returns a string representation of the matrix.
*/
public function __toString() : string
{
$result = '';
for ($y = 0; $y < $this->height; $y++) {
for ($x = 0; $x < $this->width; $x++) {
switch ($this->bytes[$y][$x]) {
case 0:
$result .= ' 0';
break;
case 1:
$result .= ' 1';
break;
default:
$result .= ' ';
break;
}
}
$result .= "\n";
}
return $result;
}
}
-679
View File
@@ -1,679 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Encoder;
use BaconQrCode\Common\BitArray;
use BaconQrCode\Common\CharacterSetEci;
use BaconQrCode\Common\ErrorCorrectionLevel;
use BaconQrCode\Common\Mode;
use BaconQrCode\Common\ReedSolomonCodec;
use BaconQrCode\Common\Version;
use BaconQrCode\Exception\WriterException;
use SplFixedArray;
/**
* Encoder.
*/
final class Encoder
{
/**
* Default byte encoding.
*/
public const DEFAULT_BYTE_MODE_ENCODING = 'ISO-8859-1';
/** @deprecated use DEFAULT_BYTE_MODE_ENCODING */
public const DEFAULT_BYTE_MODE_ECODING = self::DEFAULT_BYTE_MODE_ENCODING;
/**
* The original table is defined in the table 5 of JISX0510:2004 (p.19).
*/
private const ALPHANUMERIC_TABLE = [
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x00-0x0f
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x10-0x1f
36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43, // 0x20-0x2f
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 44, -1, -1, -1, -1, -1, // 0x30-0x3f
-1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 0x40-0x4f
25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, // 0x50-0x5f
];
/**
* Codec cache.
*
* @var array<string,ReedSolomonCodec>
*/
private static array $codecs = [];
/**
* Encodes "content" with the error correction level "ecLevel".
*/
public static function encode(
string $content,
ErrorCorrectionLevel $ecLevel,
string $encoding = self::DEFAULT_BYTE_MODE_ENCODING,
?Version $forcedVersion = null,
// Barcode scanner might not be able to read the encoded message of the QR code with the prefix ECI of UTF-8
bool $prefixEci = true
) : QrCode {
// Pick an encoding mode appropriate for the content. Note that this
// will not attempt to use multiple modes / segments even if that were
// more efficient. Would be nice.
$mode = self::chooseMode($content, $encoding);
// This will store the header information, like mode and length, as well
// as "header" segments like an ECI segment.
$headerBits = new BitArray();
// Append ECI segment if applicable
if ($prefixEci && Mode::BYTE() === $mode && self::DEFAULT_BYTE_MODE_ENCODING !== $encoding) {
$eci = CharacterSetEci::getCharacterSetEciByName($encoding);
if (null !== $eci) {
self::appendEci($eci, $headerBits);
}
}
// (With ECI in place,) Write the mode marker
self::appendModeInfo($mode, $headerBits);
// Collect data within the main segment, separately, to count its size
// if needed. Don't add it to main payload yet.
$dataBits = new BitArray();
self::appendBytes($content, $mode, $dataBits, $encoding);
// Hard part: need to know version to know how many bits length takes.
// But need to know how many bits it takes to know version. First we
// take a guess at version by assuming version will be the minimum, 1:
$provisionalBitsNeeded = $headerBits->getSize()
+ $mode->getCharacterCountBits(Version::getVersionForNumber(1))
+ $dataBits->getSize();
$provisionalVersion = self::chooseVersion($provisionalBitsNeeded, $ecLevel);
// Use that guess to calculate the right version. I am still not sure
// this works in 100% of cases.
$bitsNeeded = $headerBits->getSize()
+ $mode->getCharacterCountBits($provisionalVersion)
+ $dataBits->getSize();
$version = self::chooseVersion($bitsNeeded, $ecLevel);
if (null !== $forcedVersion) {
// Forced version check
if ($version->getVersionNumber() <= $forcedVersion->getVersionNumber()) {
// Calculated minimum version is same or equal as forced version
$version = $forcedVersion;
} else {
throw new WriterException(
'Invalid version! Calculated version: '
. $version->getVersionNumber()
. ', requested version: '
. $forcedVersion->getVersionNumber()
);
}
}
$headerAndDataBits = new BitArray();
$headerAndDataBits->appendBitArray($headerBits);
// Find "length" of main segment and write it.
$numLetters = (Mode::BYTE() === $mode ? $dataBits->getSizeInBytes() : strlen($content));
self::appendLengthInfo($numLetters, $version, $mode, $headerAndDataBits);
// Put data together into the overall payload.
$headerAndDataBits->appendBitArray($dataBits);
$ecBlocks = $version->getEcBlocksForLevel($ecLevel);
$numDataBytes = $version->getTotalCodewords() - $ecBlocks->getTotalEcCodewords();
// Terminate the bits properly.
self::terminateBits($numDataBytes, $headerAndDataBits);
// Interleave data bits with error correction code.
$finalBits = self::interleaveWithEcBytes(
$headerAndDataBits,
$version->getTotalCodewords(),
$numDataBytes,
$ecBlocks->getNumBlocks()
);
// Choose the mask pattern.
$dimension = $version->getDimensionForVersion();
$matrix = new ByteMatrix($dimension, $dimension);
$maskPattern = self::chooseMaskPattern($finalBits, $ecLevel, $version, $matrix);
// Build the matrix.
MatrixUtil::buildMatrix($finalBits, $ecLevel, $version, $maskPattern, $matrix);
return new QrCode($mode, $ecLevel, $version, $maskPattern, $matrix);
}
/**
* Gets the alphanumeric code for a byte.
*/
private static function getAlphanumericCode(int $code) : int
{
if (isset(self::ALPHANUMERIC_TABLE[$code])) {
return self::ALPHANUMERIC_TABLE[$code];
}
return -1;
}
/**
* Chooses the best mode for a given content.
*/
private static function chooseMode(string $content, ?string $encoding = null) : Mode
{
if (null !== $encoding && 0 === strcasecmp($encoding, 'SHIFT-JIS')) {
return self::isOnlyDoubleByteKanji($content) ? Mode::KANJI() : Mode::BYTE();
}
$hasNumeric = false;
$hasAlphanumeric = false;
$contentLength = strlen($content);
for ($i = 0; $i < $contentLength; ++$i) {
$char = $content[$i];
if (ctype_digit($char)) {
$hasNumeric = true;
} elseif (-1 !== self::getAlphanumericCode(ord($char))) {
$hasAlphanumeric = true;
} else {
return Mode::BYTE();
}
}
if ($hasAlphanumeric) {
return Mode::ALPHANUMERIC();
} elseif ($hasNumeric) {
return Mode::NUMERIC();
}
return Mode::BYTE();
}
/**
* Calculates the mask penalty for a matrix.
*/
private static function calculateMaskPenalty(ByteMatrix $matrix) : int
{
return (
MaskUtil::applyMaskPenaltyRule1($matrix)
+ MaskUtil::applyMaskPenaltyRule2($matrix)
+ MaskUtil::applyMaskPenaltyRule3($matrix)
+ MaskUtil::applyMaskPenaltyRule4($matrix)
);
}
/**
* Checks if content only consists of double-byte kanji characters.
*/
private static function isOnlyDoubleByteKanji(string $content) : bool
{
$bytes = @iconv('utf-8', 'SHIFT-JIS', $content);
if (false === $bytes) {
return false;
}
$length = strlen($bytes);
if (0 !== $length % 2) {
return false;
}
for ($i = 0; $i < $length; $i += 2) {
$byte = ord($bytes[$i]) & 0xff;
if (($byte < 0x81 || $byte > 0x9f) && $byte < 0xe0 || $byte > 0xeb) {
return false;
}
}
return true;
}
/**
* Chooses the best mask pattern for a matrix.
*/
private static function chooseMaskPattern(
BitArray $bits,
ErrorCorrectionLevel $ecLevel,
Version $version,
ByteMatrix $matrix
) : int {
$minPenalty = PHP_INT_MAX;
$bestMaskPattern = -1;
for ($maskPattern = 0; $maskPattern < QrCode::NUM_MASK_PATTERNS; ++$maskPattern) {
MatrixUtil::buildMatrix($bits, $ecLevel, $version, $maskPattern, $matrix);
$penalty = self::calculateMaskPenalty($matrix);
if ($penalty < $minPenalty) {
$minPenalty = $penalty;
$bestMaskPattern = $maskPattern;
}
}
return $bestMaskPattern;
}
/**
* Chooses the best version for the input.
*
* @throws WriterException if data is too big
*/
private static function chooseVersion(int $numInputBits, ErrorCorrectionLevel $ecLevel) : Version
{
for ($versionNum = 1; $versionNum <= 40; ++$versionNum) {
$version = Version::getVersionForNumber($versionNum);
$numBytes = $version->getTotalCodewords();
$ecBlocks = $version->getEcBlocksForLevel($ecLevel);
$numEcBytes = $ecBlocks->getTotalEcCodewords();
$numDataBytes = $numBytes - $numEcBytes;
$totalInputBytes = intdiv($numInputBits + 8, 8);
if ($numDataBytes >= $totalInputBytes) {
return $version;
}
}
throw new WriterException('Data too big');
}
/**
* Terminates the bits in a bit array.
*
* @throws WriterException if data bits cannot fit in the QR code
* @throws WriterException if bits size does not equal the capacity
*/
private static function terminateBits(int $numDataBytes, BitArray $bits) : void
{
$capacity = $numDataBytes << 3;
if ($bits->getSize() > $capacity) {
throw new WriterException('Data bits cannot fit in the QR code');
}
for ($i = 0; $i < 4 && $bits->getSize() < $capacity; ++$i) {
$bits->appendBit(false);
}
$numBitsInLastByte = $bits->getSize() & 0x7;
if ($numBitsInLastByte > 0) {
for ($i = $numBitsInLastByte; $i < 8; ++$i) {
$bits->appendBit(false);
}
}
$numPaddingBytes = $numDataBytes - $bits->getSizeInBytes();
for ($i = 0; $i < $numPaddingBytes; ++$i) {
$bits->appendBits(0 === ($i & 0x1) ? 0xec : 0x11, 8);
}
if ($bits->getSize() !== $capacity) {
throw new WriterException('Bits size does not equal capacity');
}
}
/**
* Gets number of data- and EC bytes for a block ID.
*
* @return int[]
* @throws WriterException if block ID is too large
* @throws WriterException if EC bytes mismatch
* @throws WriterException if RS blocks mismatch
* @throws WriterException if total bytes mismatch
*/
private static function getNumDataBytesAndNumEcBytesForBlockId(
int $numTotalBytes,
int $numDataBytes,
int $numRsBlocks,
int $blockId
) : array {
if ($blockId >= $numRsBlocks) {
throw new WriterException('Block ID too large');
}
$numRsBlocksInGroup2 = $numTotalBytes % $numRsBlocks;
$numRsBlocksInGroup1 = $numRsBlocks - $numRsBlocksInGroup2;
$numTotalBytesInGroup1 = intdiv($numTotalBytes, $numRsBlocks);
$numTotalBytesInGroup2 = $numTotalBytesInGroup1 + 1;
$numDataBytesInGroup1 = intdiv($numDataBytes, $numRsBlocks);
$numDataBytesInGroup2 = $numDataBytesInGroup1 + 1;
$numEcBytesInGroup1 = $numTotalBytesInGroup1 - $numDataBytesInGroup1;
$numEcBytesInGroup2 = $numTotalBytesInGroup2 - $numDataBytesInGroup2;
if ($numEcBytesInGroup1 !== $numEcBytesInGroup2) {
throw new WriterException('EC bytes mismatch');
}
if ($numRsBlocks !== $numRsBlocksInGroup1 + $numRsBlocksInGroup2) {
throw new WriterException('RS blocks mismatch');
}
if ($numTotalBytes !==
(($numDataBytesInGroup1 + $numEcBytesInGroup1) * $numRsBlocksInGroup1)
+ (($numDataBytesInGroup2 + $numEcBytesInGroup2) * $numRsBlocksInGroup2)
) {
throw new WriterException('Total bytes mismatch');
}
if ($blockId < $numRsBlocksInGroup1) {
return [$numDataBytesInGroup1, $numEcBytesInGroup1];
} else {
return [$numDataBytesInGroup2, $numEcBytesInGroup2];
}
}
/**
* Interleaves data with EC bytes.
*
* @throws WriterException if number of bits and data bytes does not match
* @throws WriterException if data bytes does not match offset
* @throws WriterException if an interleaving error occurs
*/
private static function interleaveWithEcBytes(
BitArray $bits,
int $numTotalBytes,
int $numDataBytes,
int $numRsBlocks
) : BitArray {
if ($bits->getSizeInBytes() !== $numDataBytes) {
throw new WriterException('Number of bits and data bytes does not match');
}
$dataBytesOffset = 0;
$maxNumDataBytes = 0;
$maxNumEcBytes = 0;
$blocks = new SplFixedArray($numRsBlocks);
for ($i = 0; $i < $numRsBlocks; ++$i) {
list($numDataBytesInBlock, $numEcBytesInBlock) = self::getNumDataBytesAndNumEcBytesForBlockId(
$numTotalBytes,
$numDataBytes,
$numRsBlocks,
$i
);
$size = $numDataBytesInBlock;
$dataBytes = $bits->toBytes(8 * $dataBytesOffset, $size);
$ecBytes = self::generateEcBytes($dataBytes, $numEcBytesInBlock);
$blocks[$i] = new BlockPair($dataBytes, $ecBytes);
$maxNumDataBytes = max($maxNumDataBytes, $size);
$maxNumEcBytes = max($maxNumEcBytes, count($ecBytes));
$dataBytesOffset += $numDataBytesInBlock;
}
if ($numDataBytes !== $dataBytesOffset) {
throw new WriterException('Data bytes does not match offset');
}
$result = new BitArray();
for ($i = 0; $i < $maxNumDataBytes; ++$i) {
foreach ($blocks as $block) {
$dataBytes = $block->getDataBytes();
if ($i < count($dataBytes)) {
$result->appendBits($dataBytes[$i], 8);
}
}
}
for ($i = 0; $i < $maxNumEcBytes; ++$i) {
foreach ($blocks as $block) {
$ecBytes = $block->getErrorCorrectionBytes();
if ($i < count($ecBytes)) {
$result->appendBits($ecBytes[$i], 8);
}
}
}
if ($numTotalBytes !== $result->getSizeInBytes()) {
throw new WriterException(
'Interleaving error: ' . $numTotalBytes . ' and ' . $result->getSizeInBytes() . ' differ'
);
}
return $result;
}
/**
* Generates EC bytes for given data.
*
* @param SplFixedArray<int> $dataBytes
* @return SplFixedArray<int>
*/
private static function generateEcBytes(SplFixedArray $dataBytes, int $numEcBytesInBlock) : SplFixedArray
{
$numDataBytes = count($dataBytes);
$toEncode = new SplFixedArray($numDataBytes + $numEcBytesInBlock);
for ($i = 0; $i < $numDataBytes; $i++) {
$toEncode[$i] = $dataBytes[$i] & 0xff;
}
$ecBytes = new SplFixedArray($numEcBytesInBlock);
$codec = self::getCodec($numDataBytes, $numEcBytesInBlock);
$codec->encode($toEncode, $ecBytes);
return $ecBytes;
}
/**
* Gets an RS codec and caches it.
*/
private static function getCodec(int $numDataBytes, int $numEcBytesInBlock) : ReedSolomonCodec
{
$cacheId = $numDataBytes . '-' . $numEcBytesInBlock;
if (isset(self::$codecs[$cacheId])) {
return self::$codecs[$cacheId];
}
return self::$codecs[$cacheId] = new ReedSolomonCodec(
8,
0x11d,
0,
1,
$numEcBytesInBlock,
255 - $numDataBytes - $numEcBytesInBlock
);
}
/**
* Appends mode information to a bit array.
*/
private static function appendModeInfo(Mode $mode, BitArray $bits) : void
{
$bits->appendBits($mode->getBits(), 4);
}
/**
* Appends length information to a bit array.
*
* @throws WriterException if num letters is bigger than expected
*/
private static function appendLengthInfo(int $numLetters, Version $version, Mode $mode, BitArray $bits) : void
{
$numBits = $mode->getCharacterCountBits($version);
if ($numLetters >= (1 << $numBits)) {
throw new WriterException($numLetters . ' is bigger than ' . ((1 << $numBits) - 1));
}
$bits->appendBits($numLetters, $numBits);
}
/**
* Appends bytes to a bit array in a specific mode.
*
* @throws WriterException if an invalid mode was supplied
*/
private static function appendBytes(string $content, Mode $mode, BitArray $bits, string $encoding) : void
{
switch ($mode) {
case Mode::NUMERIC():
self::appendNumericBytes($content, $bits);
break;
case Mode::ALPHANUMERIC():
self::appendAlphanumericBytes($content, $bits);
break;
case Mode::BYTE():
self::append8BitBytes($content, $bits, $encoding);
break;
case Mode::KANJI():
self::appendKanjiBytes($content, $bits);
break;
default:
throw new WriterException('Invalid mode: ' . $mode);
}
}
/**
* Appends numeric bytes to a bit array.
*/
private static function appendNumericBytes(string $content, BitArray $bits) : void
{
$length = strlen($content);
$i = 0;
while ($i < $length) {
$num1 = (int) $content[$i];
if ($i + 2 < $length) {
// Encode three numeric letters in ten bits.
$num2 = (int) $content[$i + 1];
$num3 = (int) $content[$i + 2];
$bits->appendBits($num1 * 100 + $num2 * 10 + $num3, 10);
$i += 3;
} elseif ($i + 1 < $length) {
// Encode two numeric letters in seven bits.
$num2 = (int) $content[$i + 1];
$bits->appendBits($num1 * 10 + $num2, 7);
$i += 2;
} else {
// Encode one numeric letter in four bits.
$bits->appendBits($num1, 4);
++$i;
}
}
}
/**
* Appends alpha-numeric bytes to a bit array.
*
* @throws WriterException if an invalid alphanumeric code was found
*/
private static function appendAlphanumericBytes(string $content, BitArray $bits) : void
{
$length = strlen($content);
$i = 0;
while ($i < $length) {
$code1 = self::getAlphanumericCode(ord($content[$i]));
if (-1 === $code1) {
throw new WriterException('Invalid alphanumeric code');
}
if ($i + 1 < $length) {
$code2 = self::getAlphanumericCode(ord($content[$i + 1]));
if (-1 === $code2) {
throw new WriterException('Invalid alphanumeric code');
}
// Encode two alphanumeric letters in 11 bits.
$bits->appendBits($code1 * 45 + $code2, 11);
$i += 2;
} else {
// Encode one alphanumeric letter in six bits.
$bits->appendBits($code1, 6);
++$i;
}
}
}
/**
* Appends regular 8-bit bytes to a bit array.
*
* @throws WriterException if content cannot be encoded to target encoding
*/
private static function append8BitBytes(string $content, BitArray $bits, string $encoding) : void
{
$bytes = @iconv('utf-8', $encoding, $content);
if (false === $bytes) {
throw new WriterException('Could not encode content to ' . $encoding);
}
$length = strlen($bytes);
for ($i = 0; $i < $length; $i++) {
$bits->appendBits(ord($bytes[$i]), 8);
}
}
/**
* Appends KANJI bytes to a bit array.
*
* @throws WriterException if content does not seem to be encoded in SHIFT-JIS
* @throws WriterException if an invalid byte sequence occurs
*/
private static function appendKanjiBytes(string $content, BitArray $bits) : void
{
$bytes = @iconv('utf-8', 'SHIFT-JIS', $content);
if (false === $bytes) {
throw new WriterException('Content could not be converted to SHIFT-JIS');
}
if (strlen($bytes) % 2 > 0) {
// We just do a simple length check here. The for loop will check
// individual characters.
throw new WriterException('Content does not seem to be encoded in SHIFT-JIS');
}
$length = strlen($bytes);
for ($i = 0; $i < $length; $i += 2) {
$byte1 = ord($bytes[$i]) & 0xff;
$byte2 = ord($bytes[$i + 1]) & 0xff;
$code = ($byte1 << 8) | $byte2;
if ($code >= 0x8140 && $code <= 0x9ffc) {
$subtracted = $code - 0x8140;
} elseif ($code >= 0xe040 && $code <= 0xebbf) {
$subtracted = $code - 0xc140;
} else {
throw new WriterException('Invalid byte sequence');
}
$encoded = (($subtracted >> 8) * 0xc0) + ($subtracted & 0xff);
$bits->appendBits($encoded, 13);
}
}
/**
* Appends ECI information to a bit array.
*/
private static function appendEci(CharacterSetEci $eci, BitArray $bits) : void
{
$mode = Mode::ECI();
$bits->appendBits($mode->getBits(), 4);
$bits->appendBits($eci->getValue(), 8);
}
}
-271
View File
@@ -1,271 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Encoder;
use BaconQrCode\Common\BitUtils;
use BaconQrCode\Exception\InvalidArgumentException;
/**
* Mask utility.
*/
final class MaskUtil
{
/**#@+
* Penalty weights from section 6.8.2.1
*/
public const N1 = 3;
public const N2 = 3;
public const N3 = 40;
public const N4 = 10;
/**#@-*/
private function __construct()
{
}
/**
* Applies mask penalty rule 1 and returns the penalty.
*
* Finds repetitive cells with the same color and gives penalty to them.
* Example: 00000 or 11111.
*/
public static function applyMaskPenaltyRule1(ByteMatrix $matrix) : int
{
return (
self::applyMaskPenaltyRule1Internal($matrix, true)
+ self::applyMaskPenaltyRule1Internal($matrix, false)
);
}
/**
* Applies mask penalty rule 2 and returns the penalty.
*
* Finds 2x2 blocks with the same color and gives penalty to them. This is
* actually equivalent to the spec's rule, which is to find MxN blocks and
* give a penalty proportional to (M-1)x(N-1), because this is the number of
* 2x2 blocks inside such a block.
*/
public static function applyMaskPenaltyRule2(ByteMatrix $matrix) : int
{
$penalty = 0;
$array = $matrix->getArray();
$width = $matrix->getWidth();
$height = $matrix->getHeight();
for ($y = 0; $y < $height - 1; ++$y) {
for ($x = 0; $x < $width - 1; ++$x) {
$value = $array[$y][$x];
if ($value === $array[$y][$x + 1]
&& $value === $array[$y + 1][$x]
&& $value === $array[$y + 1][$x + 1]
) {
++$penalty;
}
}
}
return self::N2 * $penalty;
}
/**
* Applies mask penalty rule 3 and returns the penalty.
*
* Finds consecutive cells of 00001011101 or 10111010000, and gives penalty
* to them. If we find patterns like 000010111010000, we give penalties
* twice (i.e. 40 * 2).
*/
public static function applyMaskPenaltyRule3(ByteMatrix $matrix) : int
{
$penalty = 0;
$array = $matrix->getArray();
$width = $matrix->getWidth();
$height = $matrix->getHeight();
for ($y = 0; $y < $height; ++$y) {
for ($x = 0; $x < $width; ++$x) {
if ($x + 6 < $width
&& 1 === $array[$y][$x]
&& 0 === $array[$y][$x + 1]
&& 1 === $array[$y][$x + 2]
&& 1 === $array[$y][$x + 3]
&& 1 === $array[$y][$x + 4]
&& 0 === $array[$y][$x + 5]
&& 1 === $array[$y][$x + 6]
&& (
(
$x + 10 < $width
&& 0 === $array[$y][$x + 7]
&& 0 === $array[$y][$x + 8]
&& 0 === $array[$y][$x + 9]
&& 0 === $array[$y][$x + 10]
)
|| (
$x - 4 >= 0
&& 0 === $array[$y][$x - 1]
&& 0 === $array[$y][$x - 2]
&& 0 === $array[$y][$x - 3]
&& 0 === $array[$y][$x - 4]
)
)
) {
$penalty += self::N3;
}
if ($y + 6 < $height
&& 1 === $array[$y][$x]
&& 0 === $array[$y + 1][$x]
&& 1 === $array[$y + 2][$x]
&& 1 === $array[$y + 3][$x]
&& 1 === $array[$y + 4][$x]
&& 0 === $array[$y + 5][$x]
&& 1 === $array[$y + 6][$x]
&& (
(
$y + 10 < $height
&& 0 === $array[$y + 7][$x]
&& 0 === $array[$y + 8][$x]
&& 0 === $array[$y + 9][$x]
&& 0 === $array[$y + 10][$x]
)
|| (
$y - 4 >= 0
&& 0 === $array[$y - 1][$x]
&& 0 === $array[$y - 2][$x]
&& 0 === $array[$y - 3][$x]
&& 0 === $array[$y - 4][$x]
)
)
) {
$penalty += self::N3;
}
}
}
return $penalty;
}
/**
* Applies mask penalty rule 4 and returns the penalty.
*
* Calculates the ratio of dark cells and gives penalty if the ratio is far
* from 50%. It gives 10 penalty for 5% distance.
*/
public static function applyMaskPenaltyRule4(ByteMatrix $matrix) : int
{
$numDarkCells = 0;
$array = $matrix->getArray();
$width = $matrix->getWidth();
$height = $matrix->getHeight();
for ($y = 0; $y < $height; ++$y) {
$arrayY = $array[$y];
for ($x = 0; $x < $width; ++$x) {
if (1 === $arrayY[$x]) {
++$numDarkCells;
}
}
}
$numTotalCells = $height * $width;
$darkRatio = $numDarkCells / $numTotalCells;
$fixedPercentVariances = (int) (abs($darkRatio - 0.5) * 20);
return $fixedPercentVariances * self::N4;
}
/**
* Returns the mask bit for "getMaskPattern" at "x" and "y".
*
* See 8.8 of JISX0510:2004 for mask pattern conditions.
*
* @throws InvalidArgumentException if an invalid mask pattern was supplied
*/
public static function getDataMaskBit(int $maskPattern, int $x, int $y) : bool
{
switch ($maskPattern) {
case 0:
$intermediate = ($y + $x) & 0x1;
break;
case 1:
$intermediate = $y & 0x1;
break;
case 2:
$intermediate = $x % 3;
break;
case 3:
$intermediate = ($y + $x) % 3;
break;
case 4:
$intermediate = (BitUtils::unsignedRightShift($y, 1) + (int) ($x / 3)) & 0x1;
break;
case 5:
$temp = $y * $x;
$intermediate = ($temp & 0x1) + ($temp % 3);
break;
case 6:
$temp = $y * $x;
$intermediate = (($temp & 0x1) + ($temp % 3)) & 0x1;
break;
case 7:
$temp = $y * $x;
$intermediate = (($temp % 3) + (($y + $x) & 0x1)) & 0x1;
break;
default:
throw new InvalidArgumentException('Invalid mask pattern: ' . $maskPattern);
}
return 0 == $intermediate;
}
/**
* Helper function for applyMaskPenaltyRule1.
*
* We need this for doing this calculation in both vertical and horizontal
* orders respectively.
*/
private static function applyMaskPenaltyRule1Internal(ByteMatrix $matrix, bool $isHorizontal) : int
{
$penalty = 0;
$iLimit = $isHorizontal ? $matrix->getHeight() : $matrix->getWidth();
$jLimit = $isHorizontal ? $matrix->getWidth() : $matrix->getHeight();
$array = $matrix->getArray();
for ($i = 0; $i < $iLimit; ++$i) {
$numSameBitCells = 0;
$prevBit = -1;
for ($j = 0; $j < $jLimit; $j++) {
$bit = $isHorizontal ? $array[$i][$j] : $array[$j][$i];
if ($bit === $prevBit) {
++$numSameBitCells;
} else {
if ($numSameBitCells >= 5) {
$penalty += self::N1 + ($numSameBitCells - 5);
}
$numSameBitCells = 1;
$prevBit = $bit;
}
}
if ($numSameBitCells >= 5) {
$penalty += self::N1 + ($numSameBitCells - 5);
}
}
return $penalty;
}
}
-513
View File
@@ -1,513 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Encoder;
use BaconQrCode\Common\BitArray;
use BaconQrCode\Common\ErrorCorrectionLevel;
use BaconQrCode\Common\Version;
use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Exception\WriterException;
/**
* Matrix utility.
*/
final class MatrixUtil
{
/**
* Position detection pattern.
*/
private const POSITION_DETECTION_PATTERN = [
[1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 1],
[1, 0, 1, 1, 1, 0, 1],
[1, 0, 1, 1, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1],
];
/**
* Position adjustment pattern.
*/
private const POSITION_ADJUSTMENT_PATTERN = [
[1, 1, 1, 1, 1],
[1, 0, 0, 0, 1],
[1, 0, 1, 0, 1],
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 1],
];
/**
* Coordinates for position adjustment patterns for each version.
*/
private const POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE = [
[null, null, null, null, null, null, null], // Version 1
[ 6, 18, null, null, null, null, null], // Version 2
[ 6, 22, null, null, null, null, null], // Version 3
[ 6, 26, null, null, null, null, null], // Version 4
[ 6, 30, null, null, null, null, null], // Version 5
[ 6, 34, null, null, null, null, null], // Version 6
[ 6, 22, 38, null, null, null, null], // Version 7
[ 6, 24, 42, null, null, null, null], // Version 8
[ 6, 26, 46, null, null, null, null], // Version 9
[ 6, 28, 50, null, null, null, null], // Version 10
[ 6, 30, 54, null, null, null, null], // Version 11
[ 6, 32, 58, null, null, null, null], // Version 12
[ 6, 34, 62, null, null, null, null], // Version 13
[ 6, 26, 46, 66, null, null, null], // Version 14
[ 6, 26, 48, 70, null, null, null], // Version 15
[ 6, 26, 50, 74, null, null, null], // Version 16
[ 6, 30, 54, 78, null, null, null], // Version 17
[ 6, 30, 56, 82, null, null, null], // Version 18
[ 6, 30, 58, 86, null, null, null], // Version 19
[ 6, 34, 62, 90, null, null, null], // Version 20
[ 6, 28, 50, 72, 94, null, null], // Version 21
[ 6, 26, 50, 74, 98, null, null], // Version 22
[ 6, 30, 54, 78, 102, null, null], // Version 23
[ 6, 28, 54, 80, 106, null, null], // Version 24
[ 6, 32, 58, 84, 110, null, null], // Version 25
[ 6, 30, 58, 86, 114, null, null], // Version 26
[ 6, 34, 62, 90, 118, null, null], // Version 27
[ 6, 26, 50, 74, 98, 122, null], // Version 28
[ 6, 30, 54, 78, 102, 126, null], // Version 29
[ 6, 26, 52, 78, 104, 130, null], // Version 30
[ 6, 30, 56, 82, 108, 134, null], // Version 31
[ 6, 34, 60, 86, 112, 138, null], // Version 32
[ 6, 30, 58, 86, 114, 142, null], // Version 33
[ 6, 34, 62, 90, 118, 146, null], // Version 34
[ 6, 30, 54, 78, 102, 126, 150], // Version 35
[ 6, 24, 50, 76, 102, 128, 154], // Version 36
[ 6, 28, 54, 80, 106, 132, 158], // Version 37
[ 6, 32, 58, 84, 110, 136, 162], // Version 38
[ 6, 26, 54, 82, 110, 138, 166], // Version 39
[ 6, 30, 58, 86, 114, 142, 170], // Version 40
];
/**
* Type information coordinates.
*/
private const TYPE_INFO_COORDINATES = [
[8, 0],
[8, 1],
[8, 2],
[8, 3],
[8, 4],
[8, 5],
[8, 7],
[8, 8],
[7, 8],
[5, 8],
[4, 8],
[3, 8],
[2, 8],
[1, 8],
[0, 8],
];
/**
* Version information polynomial.
*/
private const VERSION_INFO_POLY = 0x1f25;
/**
* Type information polynomial.
*/
private const TYPE_INFO_POLY = 0x537;
/**
* Type information mask pattern.
*/
private const TYPE_INFO_MASK_PATTERN = 0x5412;
/**
* Clears a given matrix.
*/
public static function clearMatrix(ByteMatrix $matrix) : void
{
$matrix->clear(-1);
}
/**
* Builds a complete matrix.
*/
public static function buildMatrix(
BitArray $dataBits,
ErrorCorrectionLevel $level,
Version $version,
int $maskPattern,
ByteMatrix $matrix
) : void {
self::clearMatrix($matrix);
self::embedBasicPatterns($version, $matrix);
self::embedTypeInfo($level, $maskPattern, $matrix);
self::maybeEmbedVersionInfo($version, $matrix);
self::embedDataBits($dataBits, $maskPattern, $matrix);
}
/**
* Removes the position detection patterns from a matrix.
*
* This can be useful if you need to render those patterns separately.
*/
public static function removePositionDetectionPatterns(ByteMatrix $matrix) : void
{
$pdpWidth = count(self::POSITION_DETECTION_PATTERN[0]);
self::removePositionDetectionPattern(0, 0, $matrix);
self::removePositionDetectionPattern($matrix->getWidth() - $pdpWidth, 0, $matrix);
self::removePositionDetectionPattern(0, $matrix->getWidth() - $pdpWidth, $matrix);
}
/**
* Embeds type information into a matrix.
*/
private static function embedTypeInfo(ErrorCorrectionLevel $level, int $maskPattern, ByteMatrix $matrix) : void
{
$typeInfoBits = new BitArray();
self::makeTypeInfoBits($level, $maskPattern, $typeInfoBits);
$typeInfoBitsSize = $typeInfoBits->getSize();
for ($i = 0; $i < $typeInfoBitsSize; ++$i) {
$bit = $typeInfoBits->get($typeInfoBitsSize - 1 - $i);
$x1 = self::TYPE_INFO_COORDINATES[$i][0];
$y1 = self::TYPE_INFO_COORDINATES[$i][1];
$matrix->set($x1, $y1, (int) $bit);
if ($i < 8) {
$x2 = $matrix->getWidth() - $i - 1;
$y2 = 8;
} else {
$x2 = 8;
$y2 = $matrix->getHeight() - 7 + ($i - 8);
}
$matrix->set($x2, $y2, (int) $bit);
}
}
/**
* Generates type information bits and appends them to a bit array.
*
* @throws RuntimeException if bit array resulted in invalid size
*/
private static function makeTypeInfoBits(ErrorCorrectionLevel $level, int $maskPattern, BitArray $bits) : void
{
$typeInfo = ($level->getBits() << 3) | $maskPattern;
$bits->appendBits($typeInfo, 5);
$bchCode = self::calculateBchCode($typeInfo, self::TYPE_INFO_POLY);
$bits->appendBits($bchCode, 10);
$maskBits = new BitArray();
$maskBits->appendBits(self::TYPE_INFO_MASK_PATTERN, 15);
$bits->xorBits($maskBits);
if (15 !== $bits->getSize()) {
throw new RuntimeException('Bit array resulted in invalid size: ' . $bits->getSize());
}
}
/**
* Embeds version information if required.
*/
private static function maybeEmbedVersionInfo(Version $version, ByteMatrix $matrix) : void
{
if ($version->getVersionNumber() < 7) {
return;
}
$versionInfoBits = new BitArray();
self::makeVersionInfoBits($version, $versionInfoBits);
$bitIndex = 6 * 3 - 1;
for ($i = 0; $i < 6; ++$i) {
for ($j = 0; $j < 3; ++$j) {
$bit = $versionInfoBits->get($bitIndex);
--$bitIndex;
$matrix->set($i, $matrix->getHeight() - 11 + $j, (int) $bit);
$matrix->set($matrix->getHeight() - 11 + $j, $i, (int) $bit);
}
}
}
/**
* Generates version information bits and appends them to a bit array.
*
* @throws RuntimeException if bit array resulted in invalid size
*/
private static function makeVersionInfoBits(Version $version, BitArray $bits) : void
{
$bits->appendBits($version->getVersionNumber(), 6);
$bchCode = self::calculateBchCode($version->getVersionNumber(), self::VERSION_INFO_POLY);
$bits->appendBits($bchCode, 12);
if (18 !== $bits->getSize()) {
throw new RuntimeException('Bit array resulted in invalid size: ' . $bits->getSize());
}
}
/**
* Calculates the BCH code for a value and a polynomial.
*/
private static function calculateBchCode(int $value, int $poly) : int
{
$msbSetInPoly = self::findMsbSet($poly);
$value <<= $msbSetInPoly - 1;
while (self::findMsbSet($value) >= $msbSetInPoly) {
$value ^= $poly << (self::findMsbSet($value) - $msbSetInPoly);
}
return $value;
}
/**
* Finds and MSB set.
*/
private static function findMsbSet(int $value) : int
{
$numDigits = 0;
while (0 !== $value) {
$value >>= 1;
++$numDigits;
}
return $numDigits;
}
/**
* Embeds basic patterns into a matrix.
*/
private static function embedBasicPatterns(Version $version, ByteMatrix $matrix) : void
{
self::embedPositionDetectionPatternsAndSeparators($matrix);
self::embedDarkDotAtLeftBottomCorner($matrix);
self::maybeEmbedPositionAdjustmentPatterns($version, $matrix);
self::embedTimingPatterns($matrix);
}
/**
* Embeds position detection patterns and separators into a byte matrix.
*/
private static function embedPositionDetectionPatternsAndSeparators(ByteMatrix $matrix) : void
{
$pdpWidth = count(self::POSITION_DETECTION_PATTERN[0]);
self::embedPositionDetectionPattern(0, 0, $matrix);
self::embedPositionDetectionPattern($matrix->getWidth() - $pdpWidth, 0, $matrix);
self::embedPositionDetectionPattern(0, $matrix->getWidth() - $pdpWidth, $matrix);
$hspWidth = 8;
self::embedHorizontalSeparationPattern(0, $hspWidth - 1, $matrix);
self::embedHorizontalSeparationPattern($matrix->getWidth() - $hspWidth, $hspWidth - 1, $matrix);
self::embedHorizontalSeparationPattern(0, $matrix->getWidth() - $hspWidth, $matrix);
$vspSize = 7;
self::embedVerticalSeparationPattern($vspSize, 0, $matrix);
self::embedVerticalSeparationPattern($matrix->getHeight() - $vspSize - 1, 0, $matrix);
self::embedVerticalSeparationPattern($vspSize, $matrix->getHeight() - $vspSize, $matrix);
}
/**
* Embeds a single position detection pattern into a byte matrix.
*/
private static function embedPositionDetectionPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
{
for ($y = 0; $y < 7; ++$y) {
for ($x = 0; $x < 7; ++$x) {
$matrix->set($xStart + $x, $yStart + $y, self::POSITION_DETECTION_PATTERN[$y][$x]);
}
}
}
private static function removePositionDetectionPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
{
for ($y = 0; $y < 7; ++$y) {
for ($x = 0; $x < 7; ++$x) {
$matrix->set($xStart + $x, $yStart + $y, 0);
}
}
}
/**
* Embeds a single horizontal separation pattern.
*
* @throws RuntimeException if a byte was already set
*/
private static function embedHorizontalSeparationPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
{
for ($x = 0; $x < 8; $x++) {
if (-1 !== $matrix->get($xStart + $x, $yStart)) {
throw new RuntimeException('Byte already set');
}
$matrix->set($xStart + $x, $yStart, 0);
}
}
/**
* Embeds a single vertical separation pattern.
*
* @throws RuntimeException if a byte was already set
*/
private static function embedVerticalSeparationPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
{
for ($y = 0; $y < 7; $y++) {
if (-1 !== $matrix->get($xStart, $yStart + $y)) {
throw new RuntimeException('Byte already set');
}
$matrix->set($xStart, $yStart + $y, 0);
}
}
/**
* Embeds a dot at the left bottom corner.
*
* @throws RuntimeException if a byte was already set to 0
*/
private static function embedDarkDotAtLeftBottomCorner(ByteMatrix $matrix) : void
{
if (0 === $matrix->get(8, $matrix->getHeight() - 8)) {
throw new RuntimeException('Byte already set to 0');
}
$matrix->set(8, $matrix->getHeight() - 8, 1);
}
/**
* Embeds position adjustment patterns if required.
*/
private static function maybeEmbedPositionAdjustmentPatterns(Version $version, ByteMatrix $matrix) : void
{
if ($version->getVersionNumber() < 2) {
return;
}
$index = $version->getVersionNumber() - 1;
$coordinates = self::POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE[$index];
$numCoordinates = count($coordinates);
for ($i = 0; $i < $numCoordinates; ++$i) {
for ($j = 0; $j < $numCoordinates; ++$j) {
$y = $coordinates[$i];
$x = $coordinates[$j];
if (null === $x || null === $y) {
continue;
}
if (-1 === $matrix->get($x, $y)) {
self::embedPositionAdjustmentPattern($x - 2, $y - 2, $matrix);
}
}
}
}
/**
* Embeds a single position adjustment pattern.
*/
private static function embedPositionAdjustmentPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
{
for ($y = 0; $y < 5; $y++) {
for ($x = 0; $x < 5; $x++) {
$matrix->set($xStart + $x, $yStart + $y, self::POSITION_ADJUSTMENT_PATTERN[$y][$x]);
}
}
}
/**
* Embeds timing patterns into a matrix.
*/
private static function embedTimingPatterns(ByteMatrix $matrix) : void
{
$matrixWidth = $matrix->getWidth();
for ($i = 8; $i < $matrixWidth - 8; ++$i) {
$bit = ($i + 1) % 2;
if (-1 === $matrix->get($i, 6)) {
$matrix->set($i, 6, $bit);
}
if (-1 === $matrix->get(6, $i)) {
$matrix->set(6, $i, $bit);
}
}
}
/**
* Embeds "dataBits" using "getMaskPattern".
*
* For debugging purposes, it skips masking process if "getMaskPattern" is -1. See 8.7 of JISX0510:2004 (p.38) for
* how to embed data bits.
*
* @throws WriterException if not all bits could be consumed
*/
private static function embedDataBits(BitArray $dataBits, int $maskPattern, ByteMatrix $matrix) : void
{
$bitIndex = 0;
$direction = -1;
// Start from the right bottom cell.
$x = $matrix->getWidth() - 1;
$y = $matrix->getHeight() - 1;
while ($x > 0) {
// Skip vertical timing pattern.
if (6 === $x) {
--$x;
}
while ($y >= 0 && $y < $matrix->getHeight()) {
for ($i = 0; $i < 2; $i++) {
$xx = $x - $i;
// Skip the cell if it's not empty.
if (-1 !== $matrix->get($xx, $y)) {
continue;
}
if ($bitIndex < $dataBits->getSize()) {
$bit = $dataBits->get($bitIndex);
++$bitIndex;
} else {
// Padding bit. If there is no bit left, we'll fill the
// left cells with 0, as described in 8.4.9 of
// JISX0510:2004 (p. 24).
$bit = false;
}
// Skip masking if maskPattern is -1.
if (-1 !== $maskPattern && MaskUtil::getDataMaskBit($maskPattern, $xx, $y)) {
$bit = ! $bit;
}
$matrix->set($xx, $y, (int) $bit);
}
$y += $direction;
}
$direction = -$direction;
$y += $direction;
$x -= 2;
}
// All bits should be consumed
if ($dataBits->getSize() !== $bitIndex) {
throw new WriterException('Not all bits consumed (' . $bitIndex . ' out of ' . $dataBits->getSize() .')');
}
}
}

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