Compare commits

..

11 Commits

196 changed files with 12039 additions and 5443 deletions

View File

@ -1,7 +1,6 @@
# Ebala
Site-registry of tasks for outsourced employees
From this project i earned **700 000** Russian rubles</br>
*As long as commits appear in the repository, this means that i continue paid development*</br>
From this project i earned >**1 000 000** Russian rubles</br>
**DEVELOPMENT COMPLETED. PROJECT CLOSED.**</br>
</br>
I am selling this site to **capitalist scum** for a lot of money, but you, friend, can use my code **for free** ✌️

View File

@ -30,16 +30,17 @@
}
],
"require": {
"php": "~8.2",
"ext-sodium": "~8.2.4",
"php": "~8.3",
"ext-sodium": "~8.3",
"mirzaev/minimal": "^2.0.x-dev",
"mirzaev/accounts": "~1.2.x-dev",
"mirzaev/arangodb": "^1.0.0",
"triagens/arangodb": "~3.9.x-dev",
"twig/twig": "^3.4",
"twig/twig": "^3.10",
"twig/extra-bundle": "^3.7",
"twig/intl-extra": "^3.7",
"phpoffice/phpspreadsheet": "^1.29"
"twig/intl-extra": "^3.10",
"phpoffice/phpspreadsheet": "^2.1",
"openswoole/core": "^22.1"
},
"require-dev": {
"phpunit/phpunit": "~9.5"

400
composer.lock generated
View File

@ -4,69 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "588f1020cbf90d4c8ed02b057592c14f",
"content-hash": "c0be2095032e176b9fb16a24e7a4d1a1",
"packages": [
{
"name": "ezyang/htmlpurifier",
"version": "v4.16.0",
"source": {
"type": "git",
"url": "https://github.com/ezyang/htmlpurifier.git",
"reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/523407fb06eb9e5f3d59889b3978d5bfe94299c8",
"reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8",
"shasum": ""
},
"require": {
"php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0"
},
"require-dev": {
"cerdic/css-tidy": "^1.7 || ^2.0",
"simpletest/simpletest": "dev-master"
},
"suggest": {
"cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
"ext-bcmath": "Used for unit conversion and imagecrash protection",
"ext-iconv": "Converts text to and from non-UTF-8 encodings",
"ext-tidy": "Used for pretty-printing HTML"
},
"type": "library",
"autoload": {
"files": [
"library/HTMLPurifier.composer.php"
],
"psr-0": {
"HTMLPurifier": "library/"
},
"exclude-from-classmap": [
"/library/HTMLPurifier/Language/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-or-later"
],
"authors": [
{
"name": "Edward Z. Yang",
"email": "admin@htmlpurifier.org",
"homepage": "http://ezyang.com"
}
],
"description": "Standards compliant HTML filter written in PHP",
"homepage": "http://htmlpurifier.org/",
"keywords": [
"html"
],
"support": {
"issues": "https://github.com/ezyang/htmlpurifier/issues",
"source": "https://github.com/ezyang/htmlpurifier/tree/v4.16.0"
},
"time": "2022-09-18T07:06:19+00:00"
},
{
"name": "guzzlehttp/guzzle",
"version": "7.7.0",
@ -728,17 +667,88 @@
"time": "2023-03-20T11:46:41+00:00"
},
{
"name": "phpoffice/phpspreadsheet",
"version": "1.29.0",
"name": "openswoole/core",
"version": "22.1.5",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "fde2ccf55eaef7e86021ff1acce26479160a0fa0"
"url": "https://github.com/openswoole/core.git",
"reference": "06dae68fdac73341ccf565ecef388434bd893141"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fde2ccf55eaef7e86021ff1acce26479160a0fa0",
"reference": "fde2ccf55eaef7e86021ff1acce26479160a0fa0",
"url": "https://api.github.com/repos/openswoole/core/zipball/06dae68fdac73341ccf565ecef388434bd893141",
"reference": "06dae68fdac73341ccf565ecef388434bd893141",
"shasum": ""
},
"require": {
"ext-openswoole": ">=22.0",
"php": ">=7.4",
"psr/http-message": "^1.0 || ^2.0",
"psr/http-server-middleware": "^1.0.0"
},
"require-dev": {
"ext-curl": "*",
"ext-sockets": "*",
"friendsofphp/php-cs-fixer": "^3.6",
"openswoole/ide-helper": "^22.0",
"php-http/psr7-integration-tests": "^1.1",
"phpunit/phpunit": "^9.5"
},
"suggest": {
"ext-mysqli": "*",
"ext-pdo": "*",
"ext-redis": "Required to use redis database, and the required version is greater than or equal to 3.1.3"
},
"type": "library",
"autoload": {
"files": [
"src/Coroutine/functions.php"
],
"psr-4": {
"OpenSwoole\\Core\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "OpenSwoole Group",
"email": "hello@openswoole.com"
}
],
"description": "Openswoole core library",
"homepage": "https://openswoole.com",
"keywords": [
"http",
"http2",
"mqtt",
"openswoole",
"php",
"tcp",
"websocket"
],
"support": {
"docs": "https://openswoole.com/docs",
"issues": "https://github.com/openswoole/openswoole/issues",
"pull-request": "https://github.com/openswoole/openswoole/pulls",
"source": "https://github.com/openswoole/openswoole"
},
"time": "2023-12-10T19:02:13+00:00"
},
{
"name": "phpoffice/phpspreadsheet",
"version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "dbed77bd3a0f68f96c0dd68ad4499d5674fecc3e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/dbed77bd3a0f68f96c0dd68ad4499d5674fecc3e",
"reference": "dbed77bd3a0f68f96c0dd68ad4499d5674fecc3e",
"shasum": ""
},
"require": {
@ -755,25 +765,24 @@
"ext-xmlwriter": "*",
"ext-zip": "*",
"ext-zlib": "*",
"ezyang/htmlpurifier": "^4.15",
"maennchen/zipstream-php": "^2.1 || ^3.0",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
"php": "^7.4 || ^8.0",
"php": "^8.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"dompdf/dompdf": "^1.0 || ^2.0",
"dompdf/dompdf": "^2.0",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.3",
"mpdf/mpdf": "^8.1.1",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.1",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^8.5 || ^9.0 || ^10.0",
"phpunit/phpunit": "^9.6",
"squizlabs/php_codesniffer": "^3.7",
"tecnickcom/tcpdf": "^6.5"
},
@ -828,9 +837,9 @@
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.0"
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/2.1.0"
},
"time": "2023-06-14T22:48:31+00:00"
"time": "2024-05-11T04:17:56+00:00"
},
{
"name": "psr/cache",
@ -1144,6 +1153,119 @@
},
"time": "2023-04-04T09:54:51+00:00"
},
{
"name": "psr/http-server-handler",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-server-handler.git",
"reference": "84c4fb66179be4caaf8e97bd239203245302e7d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4",
"reference": "84c4fb66179be4caaf8e97bd239203245302e7d4",
"shasum": ""
},
"require": {
"php": ">=7.0",
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Server\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP server-side request handler",
"keywords": [
"handler",
"http",
"http-interop",
"psr",
"psr-15",
"psr-7",
"request",
"response",
"server"
],
"support": {
"source": "https://github.com/php-fig/http-server-handler/tree/1.0.2"
},
"time": "2023-04-10T20:06:20+00:00"
},
{
"name": "psr/http-server-middleware",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-server-middleware.git",
"reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829",
"reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829",
"shasum": ""
},
"require": {
"php": ">=7.0",
"psr/http-message": "^1.0 || ^2.0",
"psr/http-server-handler": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Server\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP server-side middleware",
"keywords": [
"http",
"http-interop",
"middleware",
"psr",
"psr-15",
"psr-7",
"request",
"response"
],
"support": {
"issues": "https://github.com/php-fig/http-server-middleware/issues",
"source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2"
},
"time": "2023-04-11T06:14:47+00:00"
},
{
"name": "psr/log",
"version": "3.0.0",
@ -1619,16 +1741,16 @@
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.3.0",
"version": "v3.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "7c3aff79d10325257a001fcf92d991f24fc967cf"
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf",
"reference": "7c3aff79d10325257a001fcf92d991f24fc967cf",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"shasum": ""
},
"require": {
@ -1637,7 +1759,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.4-dev"
"dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
@ -1666,7 +1788,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0"
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0"
},
"funding": [
{
@ -1682,7 +1804,7 @@
"type": "tidelift"
}
],
"time": "2023-05-23T14:45:45+00:00"
"time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/error-handler",
@ -2377,25 +2499,25 @@
},
{
"name": "symfony/intl",
"version": "v6.3.2",
"version": "v7.0.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/intl.git",
"reference": "1f8cb145c869ed089a8531c51a6a4b31ed0b3c69"
"reference": "dd12042707110995e2e7d80103f8d9928bea8621"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/intl/zipball/1f8cb145c869ed089a8531c51a6a4b31ed0b3c69",
"reference": "1f8cb145c869ed089a8531c51a6a4b31ed0b3c69",
"url": "https://api.github.com/repos/symfony/intl/zipball/dd12042707110995e2e7d80103f8d9928bea8621",
"reference": "dd12042707110995e2e7d80103f8d9928bea8621",
"shasum": ""
},
"require": {
"php": ">=8.1"
"php": ">=8.2"
},
"require-dev": {
"symfony/filesystem": "^5.4|^6.0",
"symfony/finder": "^5.4|^6.0",
"symfony/var-exporter": "^5.4|^6.0"
"symfony/filesystem": "^6.4|^7.0",
"symfony/finder": "^6.4|^7.0",
"symfony/var-exporter": "^6.4|^7.0"
},
"type": "library",
"autoload": {
@ -2403,7 +2525,8 @@
"Symfony\\Component\\Intl\\": ""
},
"exclude-from-classmap": [
"/Tests/"
"/Tests/",
"/Resources/data/"
]
},
"notification-url": "https://packagist.org/downloads/",
@ -2439,7 +2562,7 @@
"localization"
],
"support": {
"source": "https://github.com/symfony/intl/tree/v6.3.2"
"source": "https://github.com/symfony/intl/tree/v7.0.7"
},
"funding": [
{
@ -2455,20 +2578,20 @@
"type": "tidelift"
}
],
"time": "2023-07-20T07:43:09+00:00"
"time": "2024-04-18T09:29:19+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.27.0",
"version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a"
"reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a",
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4",
"reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4",
"shasum": ""
},
"require": {
@ -2482,9 +2605,6 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.27-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@ -2521,7 +2641,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0"
},
"funding": [
{
@ -2537,20 +2657,20 @@
"type": "tidelift"
}
],
"time": "2022-11-03T14:55:06+00:00"
"time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.27.0",
"version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534"
"reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
"reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
"shasum": ""
},
"require": {
@ -2564,9 +2684,6 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.27-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@ -2604,7 +2721,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0"
},
"funding": [
{
@ -2620,20 +2737,20 @@
"type": "tidelift"
}
],
"time": "2022-11-03T14:55:06+00:00"
"time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.28.0",
"version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5"
"reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
"reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
"shasum": ""
},
"require": {
@ -2641,9 +2758,6 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@ -2687,7 +2801,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0"
},
"funding": [
{
@ -2703,7 +2817,7 @@
"type": "tidelift"
}
],
"time": "2023-01-26T09:26:14+00:00"
"time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/polyfill-php83",
@ -3516,25 +3630,25 @@
},
{
"name": "twig/intl-extra",
"version": "v3.7.1",
"version": "v3.10.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/intl-extra.git",
"reference": "4f4fe572f635534649cc069e1dafe4a8ad63774d"
"reference": "693f6beb8ca91fc6323e01b3addf983812f65c93"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/intl-extra/zipball/4f4fe572f635534649cc069e1dafe4a8ad63774d",
"reference": "4f4fe572f635534649cc069e1dafe4a8ad63774d",
"url": "https://api.github.com/repos/twigphp/intl-extra/zipball/693f6beb8ca91fc6323e01b3addf983812f65c93",
"reference": "693f6beb8ca91fc6323e01b3addf983812f65c93",
"shasum": ""
},
"require": {
"php": ">=7.1.3",
"symfony/intl": "^5.4|^6.0",
"twig/twig": "^2.7|^3.0"
"php": ">=7.2.5",
"symfony/intl": "^5.4|^6.4|^7.0",
"twig/twig": "^3.10"
},
"require-dev": {
"symfony/phpunit-bridge": "^5.4|^6.3"
"symfony/phpunit-bridge": "^6.4|^7.0"
},
"type": "library",
"autoload": {
@ -3564,7 +3678,7 @@
"twig"
],
"support": {
"source": "https://github.com/twigphp/intl-extra/tree/v3.7.1"
"source": "https://github.com/twigphp/intl-extra/tree/v3.10.0"
},
"funding": [
{
@ -3576,33 +3690,41 @@
"type": "tidelift"
}
],
"time": "2023-07-29T15:34:56+00:00"
"time": "2024-05-11T07:35:57+00:00"
},
{
"name": "twig/twig",
"version": "v3.6.1",
"version": "v3.10.3",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "7e7d5839d4bec168dfeef0ac66d5c5a2edbabffd"
"reference": "67f29781ffafa520b0bbfbd8384674b42db04572"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/7e7d5839d4bec168dfeef0ac66d5c5a2edbabffd",
"reference": "7e7d5839d4bec168dfeef0ac66d5c5a2edbabffd",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/67f29781ffafa520b0bbfbd8384674b42db04572",
"reference": "67f29781ffafa520b0bbfbd8384674b42db04572",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3"
"symfony/polyfill-mbstring": "^1.3",
"symfony/polyfill-php80": "^1.22"
},
"require-dev": {
"psr/container": "^1.0|^2.0",
"symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0"
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
},
"type": "library",
"autoload": {
"files": [
"src/Resources/core.php",
"src/Resources/debug.php",
"src/Resources/escaper.php",
"src/Resources/string_loader.php"
],
"psr-4": {
"Twig\\": "src/"
}
@ -3635,7 +3757,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.6.1"
"source": "https://github.com/twigphp/Twig/tree/v3.10.3"
},
"funding": [
{
@ -3647,7 +3769,7 @@
"type": "tidelift"
}
],
"time": "2023-06-08T12:52:13+00:00"
"time": "2024-05-16T10:04:27+00:00"
}
],
"packages-dev": [
@ -5393,8 +5515,8 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": "~8.2",
"ext-sodium": "~8.2.4"
"php": "~8.3",
"ext-sodium": "~8.3"
},
"platform-dev": [],
"plugin-api-version": "2.2.0"

17
ebala-socket.service Executable file
View File

@ -0,0 +1,17 @@
[Unit]
Description=Ebala-socket
Wants=network.target
After=syslog.target network-online.target
[Service]
ExecStart=sudo -u www-data /usr/bin/php /var/www/ebala/mirzaev/ebala/system/public/socket.php
PIDFile=/var/run/php/ebala-socket.pid
RemainAfterExit=no
RuntimeMaxSec=3600s
Restart=always
RestartSec=30s
[Install]
WantedBy=multi-user.target

View File

@ -8,6 +8,7 @@ namespace mirzaev\ebala\controllers;
use mirzaev\ebala\controllers\core,
mirzaev\ebala\controllers\traits\errors,
mirzaev\ebala\models\account as model,
mirzaev\ebala\models\task,
mirzaev\ebala\models\registry,
mirzaev\ebala\models\core as _core;
@ -27,6 +28,37 @@ final class account extends core
{
use errors;
/**
* Страница аккаунта
*
* @param array $parameters Параметры запроса
*/
public function index(array $parameters = []): ?string
{
if ($this->account->status()) {
// Авторизован аккаунт
// Инициализация истории заявок
$this->view->history = task::list(before: 'FILTER task.worker == "' . model::worker($this->account->getId())?->id . '"');
// Инициализация баланса счёта
// В будущем сделать перебор по всем связанным с аккаунтам сотрудникам и магазинам (сейчас только 1 сотрудник может быть)
$this->view->balance = 0;
foreach (task::list(before: 'FILTER task.worker == "' . model::worker($this->account->getId())?->id . '" && task.result.processed == false') as $task) $this->view->balance += $task->task['result']['hours'] * $task->task['result']['hour'] + $task->task['result']['penalty'] + $task->task['result']['bonus'];
// Генерация представления
$main = $this->view->render(DIRECTORY_SEPARATOR . 'pages' . DIRECTORY_SEPARATOR . 'account.html');
} else $main = $this->authorization();
// Возврат (успех)
if ($_SERVER['REQUEST_METHOD'] === 'GET') return $this->view->render(DIRECTORY_SEPARATOR . 'index.html', ['main' => $main]);
else if ($_SERVER['REQUEST_METHOD'] === 'POST') return $main;
// Возврат (провал)
return null;
}
/**
* Прочитать данные
*
@ -38,7 +70,7 @@ final class account extends core
// Авторизован аккаунт администратора или оператора
// Инициализация данных аккаунта
$account = model::read('d._key == "' . $parameters['id'] . '"', return: '{ name: d.name, number: d.number, mail: d.mail, commentary: d.commentary }')->getAll();
$account = model::read('d._key == "' . $parameters['id'] . '"', return: '{ name: d.name, number: d.number, mail: d.mail, commentary: d.commentary, transactions: d.transactions }')->getAll();
if (!empty($account)) {
// Найдены данные аккаунта
@ -74,6 +106,7 @@ final class account extends core
*/
public function update(array $parameters = []): ?string
{
try {
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
@ -94,7 +127,7 @@ final class account extends core
if (mb_strlen($parameters['number']) === 11) $account->number = $parameters['number'];
else throw new exception('Номер должен состоять из 11 символов');
if ($parameters['mail'] !== $account->mail) $account->mail = $parameters['mail'];
if (!empty($parameters['password']) && !sodium_crypto_pwhash_str_verify($parameters['password'], $account->password) && $password = true)
if (!empty($parameters['password']) && !@sodium_crypto_pwhash_str_verify($parameters['password'], $account->password ?? '') && $password = true)
if (mb_strlen($parameters['password']) > 6) $account->password = sodium_crypto_pwhash_str(
$parameters['password'],
SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE,
@ -102,6 +135,12 @@ final class account extends core
);
else throw new exception('Пароль должен быть длиннее 6 символов');
if ($parameters['commentary'] !== $account->commentary) $account->commentary = $parameters['commentary'];
if (!empty($parameters['transactions']) && $parameters['transactions'] !== $account->transactions)
$account->transactions = match ($parameters['transactions']) {
'true' => true,
'false' => false,
default => false
};
if (_core::update($account)) {
// Записаны данные аккаунта
@ -184,6 +223,40 @@ final class account extends core
} else throw new exception('Не удалось записать изменения в базу данных');
} else throw new exception('Не удалось найти аккаунт');
}
} catch (exception $e) {
// Write to the errors registry
$this->errors['account'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'updated' => false,
'errors' => self::parse_only_text($this->errors)
];
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
// Возврат (провал)
return null;
@ -196,6 +269,7 @@ final class account extends core
*/
public function delete(array $parameters = []): ?string
{
try {
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
@ -264,6 +338,212 @@ final class account extends core
} else throw new exception('Не удалось записать изменения в базу данных');
} else throw new exception('Не удалось найти аккаунт');
}
} catch (exception $e) {
// Write to the errors registry
$this->errors['account'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'deleted' => false,
'errors' => self::parse_only_text($this->errors)
];
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
// Возврат (провал)
return null;
}
/**
* Пометить заблокированным
*
* @param array $parameters Параметры запроса
*/
public function ban(array $parameters = []): ?string
{
try {
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
// Инициализация данных аккаунта
$account = model::read('d._key == "' . $parameters['id'] . '"');
if (!empty($account)) {
// Найден аккаунт
// Блокирование
$account->active = false;
$account->banned = true;
if (_core::update($account)) {
// Записаны данные аккаунта
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode([
'banned' => true,
'errors' => self::parse_only_text($this->errors)
]);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не удалось записать изменения в базу данных');
} else throw new exception('Не удалось найти аккаунт');
}
} catch (exception $e) {
// Write to the errors registry
$this->errors['account'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'banned' => false,
'errors' => self::parse_only_text($this->errors)
];
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
// Возврат (провал)
return null;
}
/**
* Снять пометку заблокированного (разблокировать)
*
* @param array $parameters Параметры запроса
*/
public function unban(array $parameters = []): ?string
{
try {
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
// Инициализация данных аккаунта
$account = model::read('d._key == "' . $parameters['id'] . '"');
if (!empty($account)) {
// Найден аккаунт
// Блокирование
$account->active = true;
$account->banned = false;
if (_core::update($account)) {
// Записаны данные аккаунта
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode([
'unbanned' => true,
'errors' => self::parse_only_text($this->errors)
]);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не удалось записать изменения в базу данных');
} else throw new exception('Не удалось найти аккаунт');
}
} catch (exception $e) {
// Write to the errors registry
$this->errors['account'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'unbanned' => false,
'errors' => self::parse_only_text($this->errors)
];
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
// Возврат (провал)
return null;

View File

@ -38,7 +38,7 @@ final class administrator extends core
// Перебор фильтров статусов
// Инициализация значения (приоритет у cookie)
$value = $_COOKIE["administrators_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['administrators']['filters'][$name] ?? 0;
$value = $_COOKIE["administrators_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['administrators']['filters'][$name] ?? 0;
// Инициализировано значение?
if ($value === null || $value === 0) continue;
@ -86,7 +86,7 @@ final class administrator extends core
// Перебор фильтров статусов (И)
// Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue)
if (empty($value = $_COOKIE["administrators_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['administrators']['filters'][$name] ?? 0)) continue;
if (empty($value = $_COOKIE["administrators_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['administrators']['filters'][$name] ?? 0)) continue;
// Конвертация ярлыков
$converted = match ($name) {
@ -116,7 +116,7 @@ final class administrator extends core
if (!empty($filters_statuses_merged)) $filters .= empty($filters) ? $filters_statuses_merged : " && ($filters_statuses_merged)";
// Инициализация строки поиска
$search = $_COOKIE["administrators_filter_search"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['administrators']['filters']['search'] ?? '';
$search = $_COOKIE["administrators_filter_search"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['administrators']['filters']['search'] ?? '';
if (mb_strlen($search) < 3) $search = null;
$search_query = empty($search)
? null
@ -147,8 +147,13 @@ final class administrator extends core
$search_query,
empty($filters) ? null : " && ($filters)"
),
after: <<<AQL
COLLECT x = account OPTIONS { method: "sorted" }
AQL,
page: (int) $this->session->buffer[$_SERVER['INTERFACE']]['administrators']['page'],
sort: 'x.created DESC, x._key DESC',
target: empty($search) ? account::COLLECTION : 'registry_accounts',
return: '{account: x}',
binds: empty($search) ? [] : [
'search' => $search
]
@ -229,12 +234,35 @@ final class administrator extends core
if (empty($account)) throw new exception('Не удалось создать аккаунт');
} catch (exception $e) {
// Write to the errors registry
$this->errors['account'][] = [
$this->errors['administrator'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'errors' => self::parse_only_text($this->errors)
];
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
// Инициализация идентификатора аккаунта (ключ документа инстанции аккаунта в базе данных)

View File

@ -14,6 +14,9 @@ use mirzaev\ebala\views\templater,
// Фреймворк PHP
use mirzaev\minimal\controller;
// Встроенные библиотеки
use exception;
/**
* Ядро контроллеров
*
@ -53,7 +56,7 @@ class core extends controller
public function __construct(bool $initialize = true)
{
// Блокировка запросов от CloudFlare
if ($_SERVER['HTTP_USER_AGENT'] === 'nginx-ssl early hints') return;
if (!empty($_SERVER['HTTP_USER_AGENT']) && $_SERVER['HTTP_USER_AGENT'] === 'nginx-ssl early hints') return;
parent::__construct($initialize);

View File

@ -30,7 +30,7 @@ final class index extends core
// Перебор фильтров временного промежутка
// Инициализация значения (приоритет у cookie)
if (empty($value = (int) ($_COOKIE["tasks_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? (($name === 'from') ? time() : strtotime('+1 month'))))) continue;
if (empty($value = (int) ($_COOKIE["tasks_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? (($name === 'from') ? time() : strtotime('+1 month'))))) continue;
// Генерация значения для аттрибута "value" для HTML-элемента <input>
$this->view->{$name} = (int) $value;
@ -40,7 +40,7 @@ final class index extends core
// Перебор фильтров статусов
// Инициализация значения (приоритет у cookie)
$value = $_COOKIE["tasks_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? null;
$value = $_COOKIE["tasks_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? null;
// Найдено значение?
if ($value === null) continue;

View File

@ -44,7 +44,7 @@ final class market extends core
// Перебор фильтров статусов
// Инициализация значения (приоритет у cookie)
$value = $_COOKIE["markets_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['markets']['filters'][$name] ?? 0;
$value = $_COOKIE["markets_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['markets']['filters'][$name] ?? 0;
// Инициализировано значение?
if ($value === null || $value === 0) continue;
@ -94,7 +94,7 @@ final class market extends core
// Перебор фильтров статусов
// Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue)
if (empty($value = $_COOKIE["markets_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['markets']['filters'][$name] ?? 0)) continue;
if (empty($value = $_COOKIE["markets_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['markets']['filters'][$name] ?? 0)) continue;
// Конвертация ярлыков
$converted = match ($name) {
@ -135,7 +135,7 @@ final class market extends core
if (!empty($filters_statuses_after_merged)) $filters_after .= empty($filters_after) ? $filters_statuses_after_merged : " && ($filters_statuses_after_merged)";
// Инициализация строки поиска
$search = $_COOKIE["markets_filter_search"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['markets']['filters']['search'] ?? '';
$search = $_COOKIE["markets_filter_search"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['markets']['filters']['search'] ?? '';
if (mb_strlen($search) < 3) $search = null;
$search_query = empty($search)
? null
@ -180,12 +180,15 @@ final class market extends core
FILTER account.type == 'market' && b.deleted != true
%s
%s
COLLECT x = account, y = market OPTIONS { method: "sorted" }
AQL,
empty($filters_before) ? null : "FILTER $filters_before",
empty($filters_after) ? null : "FILTER $filters_after"
),
page: (int) $this->session->buffer[$_SERVER['INTERFACE']]['markets']['page'],
sort: 'x.created DESC, y.created DESC, x._key DESC, y._key DESC',
target: empty($search) ? account::COLLECTION : 'registry_accounts',
return: '{account: x, market: y}',
binds: empty($search) ? [] : [
'search' => $search
]
@ -338,7 +341,7 @@ final class market extends core
throw new exception('Не инициализирован аккаунт');
} catch (exception $e) {
// Write to the errors registry
$this->errors['account'][] = [
$this->errors['market'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
@ -359,7 +362,7 @@ final class market extends core
// Авторизован аккаунт администратора или оператора
// Инициализация данных магазина
$market = model::read('d.id == "' . $parameters['id'] . '"', return: '{ name: d.name, number: d.number, mail: d.mail, type: d.type, city: d.city, district: d.district, address: d.address}')->getAll();
$market = model::read('d.id == "' . urldecode($parameters['id']) . '"', return: '{ name: d.name, number: d.number, mail: d.mail, type: d.type, city: d.city, district: d.district, address: d.address}')->getAll();
if (!empty($market)) {
// Найдены данные магазина
@ -388,6 +391,267 @@ final class market extends core
return null;
}
/**
* Заблокировать сотрудника в магазине
*
* @param array $parameters Параметры запроса
*/
public function ban(array $parameters = []): ?string
{
try {
if ($this->account->status() && $this->account->type === 'market') {
// Авторизован аккаунт магазина
// Инициализация данных магазина
$market = account::market($this->account->getId());
if ($market instanceof _document) {
// Найден магазин
// Блокировка сотрудника
if (!in_array($parameters['worker'], $market->bans ??= [], true)) $market->bans = $market->bans + [$parameters['worker']];
if (_core::update($market)) {
// Записаны данные магазина
// Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение)
$this->view->page = null;
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'banned' => true,
'errors' => self::parse_only_text($this->errors)
];
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не удалось записать изменения в базу данных');
} else throw new exception('Не удалось найти магазин');
}
} catch (exception $e) {
// Write to the errors registry
$this->errors['market'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'banned' => false,
'errors' => self::parse_only_text($this->errors)
];
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
// Возврат (провал)
return null;
}
/**
* Разблокировать сотрудника в магазине
*
* @param array $parameters Параметры запроса
*/
public function unban(array $parameters = []): ?string
{
try {
if ($this->account->status() && $this->account->type === 'market') {
// Авторизован аккаунт магазина
// Инициализация данных магазина
$market = account::market($this->account->getId());
if ($market instanceof _document) {
// Найден магазин
// Разблокировка сотрудника
if (in_array($parameters['worker'], $market->bans ??= [], true)) $market->bans = array_diff($market->bans ??= [], [$parameters['worker']]);
if (_core::update($market)) {
// Записаны данные магазина
// Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение)
$this->view->page = null;
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'unbanned' => true,
'errors' => self::parse_only_text($this->errors)
];
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не удалось записать изменения в базу данных');
} else throw new exception('Не удалось найти магазин');
}
} catch (exception $e) {
// Write to the errors registry
$this->errors['market'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'unbanned' => false,
'errors' => self::parse_only_text($this->errors)
];
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
// Возврат (провал)
return null;
}
/**
* Проверить наличие сотрудника в реестре заблокированных
*
* @param array $parameters Параметры запроса
*/
public function banned(array $parameters = []): ?string
{
try {
if ($this->account->status() && $this->account->type === 'market') {
// Авторизован аккаунт магазина
// Инициализация данных магазина
$market = account::market($this->account->getId());
if ($market instanceof _document) {
// Найден магазин
// Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение)
$this->view->page = null;
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'banned' => in_array($parameters['worker'], $market->bans ?? [], true),
'errors' => self::parse_only_text($this->errors)
];
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не удалось найти магазин');
}
} catch (exception $e) {
// Write to the errors registry
$this->errors['market'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'errors' => self::parse_only_text($this->errors)
];
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
return null;
}
/**
* Обновить данные
*
@ -395,11 +659,12 @@ final class market extends core
*/
public function update(array $parameters = []): ?string
{
try {
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
// Инициализация данных магазина
$market = model::read('d.id == "' . $parameters['id'] . '"');
$market = model::read('d.id == "' . urldecode($parameters['id']) . '"');
if (!empty($market)) {
// Найден магазин
@ -416,6 +681,7 @@ final class market extends core
if ($parameters['city'] !== $market->city) $market->city = $parameters['city'];
if ($parameters['district'] !== $market->district) $market->district = $parameters['district'];
if ($parameters['address'] !== $market->address) $market->address = $parameters['address'];
if (!in_array($parameters['ban'], $market->bans, true)) $market->bans[] = $parameters['ban'];
if (_core::update($market)) {
// Записаны данные магазина
@ -465,6 +731,39 @@ final class market extends core
} else throw new exception('Не удалось записать изменения в базу данных');
} else throw new exception('Не удалось найти аккаунт');
}
} catch (exception $e) {
// Write to the errors registry
$this->errors['market'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'updated' => false,
'errors' => self::parse_only_text($this->errors)
];
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
// Возврат (провал)
return null;

View File

@ -38,7 +38,7 @@ final class operator extends core
// Перебор фильтров статусов
// Инициализация значения (приоритет у cookie)
$value = $_COOKIE["operators_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['operators']['filters'][$name] ?? 0;
$value = $_COOKIE["operators_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['operators']['filters'][$name] ?? 0;
// Инициализировано значение?
if ($value === null || $value === 0) continue;
@ -86,7 +86,7 @@ final class operator extends core
// Перебор фильтров статусов (И)
// Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue)
if (empty($value = $_COOKIE["operators_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['operators']['filters'][$name] ?? 0)) continue;
if (empty($value = $_COOKIE["operators_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['operators']['filters'][$name] ?? 0)) continue;
// Конвертация ярлыков
$converted = match ($name) {
@ -116,7 +116,7 @@ final class operator extends core
if (!empty($filters_statuses_merged)) $filters .= empty($filters) ? $filters_statuses_merged : " && ($filters_statuses_merged)";
// Инициализация строки поиска
$search = $_COOKIE["operators_filter_search"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['operators']['filters']['search'] ?? '';
$search = $_COOKIE["operators_filter_search"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['operators']['filters']['search'] ?? '';
if (mb_strlen($search) < 3) $search = null;
$search_query = empty($search)
? null
@ -147,8 +147,13 @@ final class operator extends core
$search_query,
empty($filters) ? null : " && ($filters)"
),
after: <<<AQL
COLLECT x = account OPTIONS { method: "sorted" }
AQL,
page: (int) $this->session->buffer[$_SERVER['INTERFACE']]['operators']['page'],
sort: 'x.created DESC, x._key DESC',
target: empty($search) ? account::COLLECTION : 'registry_accounts',
return: '{account: x}',
binds: empty($search) ? [] : [
'search' => $search
]
@ -228,12 +233,35 @@ final class operator extends core
if (empty($account)) throw new exception('Не удалось создать аккаунт');
} catch (exception $e) {
// Write to the errors registry
$this->errors['account'][] = [
$this->errors['operator'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'errors' => self::parse_only_text($this->errors)
];
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
// Инициализация идентификатора аккаунта (ключ документа инстанции аккаунта в базе данных)

View File

@ -0,0 +1,265 @@
<?php
declare(strict_types=1);
namespace mirzaev\ebala\controllers;
// Файлы проекта
use mirzaev\ebala\controllers\core,
mirzaev\ebala\controllers\traits\errors,
mirzaev\ebala\models\payments as model;
// System libraries
use exception;
/**
* Контроллер выплат
*
* @package mirzaev\ebala\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class payments extends core
{
use errors;
/**
* Сотрудники
*
* Расчитать стоимость работы сотрудников за выбранный период и сгенерировать excel-документ
*
* @param array $parameters Параметры запроса
*
* @return void В буфер вывода excel-документ
*/
public function workers(array $parameters = []): void
{
try {
if ($this->account->status() && ($this->account->type === 'administrator' || ($this->account->type === 'operator' && $this->account->transactions))) {
// Авторизован аккаунт администратора или оператора (с доступом к транзакциям)
// Инициализация буфера ошибок
$this->errors['export'] ??= [];
if (!empty($from = (int) ($_COOKIE["tasks_filter_from"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters']['from']))) {
// Инициализирован параметр: from
if (!empty($to = (int) ($_COOKIE["tasks_filter_to"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters']['to']))) {
// Инициализирован параметр: to
// Сброс буфера вывода
if (ob_get_level()) {
ob_end_clean();
}
// Инициализация буфера вывода
ob_start();
if (model::workers($from, $to, $this->errors['export'])) {
// Сгенерирован excel-документ с выплатами (и отправлен в буфер вывода)
// Запись заголовков ответа
header('Content-Description: Spreadsheet transfer');
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment;filename=workers ' . gmdate("d.m.Y", $from) . ' - ' . gmdate("d.m.Y", $to) . '.xlsx');
header('Access-Control-Expose-Headers: Content-Disposition');
header('Cache-Control: max-age=0');
} else throw new exception('Не удалось сгенерировать excel-документ');
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не инициализирован параметр: to');
} else throw new exception('Не инициализирован параметр: from');
} else throw new exception('Вы не авторизованы');
} catch (exception $e) {
// Запись в реестр ошибок
$this->errors['export'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode(
[
'errors' => self::parse_only_text($this->errors)
]
);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
}
/**
* Магазины
*
* Расчитать прибыль с магазинов за выбранный период и сгенерировать excel-документ
*
* @param array $parameters Параметры запроса
*
* @return void В буфер вывода excel-документ
*/
public function markets(array $parameters = []): void
{
try {
if ($this->account->status() && ($this->account->type === 'administrator' || ($this->account->type === 'operator' && $this->account->transactions))) {
// Авторизован аккаунт администратора или оператора (с доступом к транзакциям)
// Инициализация буфера ошибок
$this->errors['export'] ??= [];
if (!empty($from = (int) ($_COOKIE["tasks_filter_from"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters']['from']))) {
// Инициализирован параметр: from
if (!empty($to = (int) ($_COOKIE["tasks_filter_to"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters']['to']))) {
// Инициализирован параметр: to
// Сброс буфера вывода
if (ob_get_level()) {
ob_end_clean();
}
// Инициализация буфера вывода
ob_start();
if (model::markets($from, $to, $this->errors['export'])) {
// Сгенерирован excel-документ с выплатами (и отправлен в буфер вывода)
// Запись заголовков ответа
header('Content-Description: Spreadsheet transfer');
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment;filename=markets ' . gmdate("d.m.Y", $from) . ' - ' . gmdate("d.m.Y", $to) . '.xlsx');
header('Access-Control-Expose-Headers: Content-Disposition');
header('Cache-Control: max-age=0');
} else throw new exception('Не удалось сгенерировать excel-документ');
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не инициализирован параметр: to');
} else throw new exception('Не инициализирован параметр: from');
} else throw new exception('Вы не авторизованы');
} catch (exception $e) {
// Запись в реестр ошибок
$this->errors['export'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode(
[
'errors' => self::parse_only_text($this->errors)
]
);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
}
/**
* Подтвердить
*
* Подтвердить выполнение операций с документом (магазины или сотрудники)
*
* @param array $parameters Параметры запроса
*
* @return void
*/
public function confirm(array $parameters = []): void
{
try {
if ($this->account->status() && ($this->account->type === 'administrator' || ($this->account->type === 'operator' && $this->account->transactions))) {
// Авторизован аккаунт администратора или оператора (с доступом к транзакциям)
// Инициализация буфера ошибок
$this->errors['confirm'] ??= [];
if (!empty($from = (int) ($_COOKIE["tasks_filter_from"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters']['from']))) {
// Инициализирован параметр: from
if (!empty($to = (int) ($_COOKIE["tasks_filter_to"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters']['to']))) {
// Инициализирован параметр: to
// Запуск процедуры подтверждения
model::confirm(
$from,
$to,
match ($parameters['type']) {
'workers' => 'workers',
'markets' => 'markets',
default => throw new exception('Для подтверждения обработки документа необходимо передать его тип: workers, markets')
},
$this->errors['confirm']
);
} else throw new exception('Не инициализирован параметр: to');
} else throw new exception('Не инициализирован параметр: from');
} else throw new exception('Вы не авторизованы');
} catch (exception $e) {
// Запись в реестр ошибок
$this->errors['confirm'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode(
[
'errors' => self::parse_only_text($this->errors)
]
);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
}
}

View File

@ -364,8 +364,8 @@ final class session extends core
// Проверка параметров на соответствование требованиям
if ($length === 0) throw new exception('Идентификатор аккаунта аккаунта не может быть пустым');
if ($length > 40) throw new exception('Идентификатор аккаунта аккаунта должен иметь не более 40 символов');
if (preg_match_all('/[^\d\(\)\-\s\r\n\t\0]+/u', $parameters['market'], $matches) > 0) throw new exception('Нельзя использовать символы: ' . implode(', ', ...$matches ?? []));
if ($length > 3) throw new exception('Идентификатор аккаунта аккаунта должен иметь не более 3 символов');
if (preg_match_all('/[^\d]+/u', $parameters['market'], $matches) > 0) throw new exception('Нельзя использовать символы: ' . implode(', ', ...$matches ?? []));
if ($remember = isset($parameters['remember']) && $parameters['remember'] === '1') {
// Запрошено запоминание

View File

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace mirzaev\ebala\controllers;
// Файлы проекта
use mirzaev\ebala\controllers\core,
mirzaev\ebala\models\settings as model,
mirzaev\ebala\models\core as _core;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// System libraries
use datetime,
datetimezone,
exception;
/**
* Контроллер настроек сайта
*
* @package mirzaev\ebala\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class settings extends core
{
/**
* Страница настроек
*
* @param array $parameters Параметры запроса
*/
public function index(array $parameters = []): ?string
{
if ($this->account->status() && $this->account->type === 'administrator') {
// Авторизован аккаунт (администратор)
// Чтение настроек
$this->view->settings = model::search(
before: 'FILTER setting.category != null'
);
// Генерация представления
$main = $this->view->render(DIRECTORY_SEPARATOR . 'pages' . DIRECTORY_SEPARATOR . 'settings.html');
// Возврат (успех)
if ($_SERVER['REQUEST_METHOD'] === 'GET') return $this->view->render(DIRECTORY_SEPARATOR . 'index.html', ['main' => $main]);
else if ($_SERVER['REQUEST_METHOD'] === 'POST') return $main;
}
// Возврат (провал)
return null;
}
/**
* Записать
*
* Записывает (обновляет) в ArangoDB
*
* @param array $parameters Параметры запроса
*
* @return void
*/
public function write(array $parameters = []): void
{
try {
if ($this->account->status() && $this->account->type === 'administrator') {
// Авторизован аккаунт администратора
// Инициализация инстанции настройки
$setting = model::read('d._key == "' . $parameters['id'] . '"');
if ($setting instanceof _document) {
// Найдена инстанция настройки
// Запись значения
$setting->value = $parameters['value'];
if (_core::update($setting)) {
// Записано в ArangoDB
} else throw new exception('Не удалось обновить заявку');
} else throw new exception('Не найдена заявка');
} else throw new exception('Вы не авторизованы');
} catch (exception $e) {
// Запись в реестр ошибок
$this->errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
}
}

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace mirzaev\ebala\controllers;
// Файлы проекта
use mirzaev\ebala\controllers\core,
mirzaev\ebala\controllers\traits\errors,
mirzaev\ebala\models\socket as model,
mirzaev\ebala\models\task,
mirzaev\ebala\models\session,
mirzaev\ebala\models\registry,
mirzaev\ebala\models\core as _core;
// Фреймворк ArangoDB
use mirzaev\arangodb\document;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Встроенные библиотеки
use exception;
/**
* Контроллер сокета
*
* @package mirzaev\ebala\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class socket extends core
{
use errors;
/**
* Регистрация
*
* @param array $parameters Параметры запроса
*/
public function registration(array $parameters = []): ?string
{
try {
if (model::session($this->session->getId(), $parameters['key'], $this->errors)) {
// Инициализировано соединение
// @todo сделать например возврат того что соединение удалось и на клиенте что-то оптимизировать (или не удалось чтобы повторил)
return null;
} else throw new exception('Не удалось зарегистрировать сокет');
} catch (exception $e) {
// Write to the errors registry
$this->errors['socket'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Возврат (провал)
return null;
}
}

View File

@ -11,6 +11,7 @@ use mirzaev\ebala\controllers\core,
mirzaev\ebala\models\account,
mirzaev\ebala\models\worker,
mirzaev\ebala\models\market,
mirzaev\ebala\models\payments,
mirzaev\ebala\models\core as _core;
// Библиотека для ArangoDB
@ -31,6 +32,11 @@ final class task extends core
{
use errors;
/**
* Типы работ
*/
final public const WORKS = ['Кассир', 'Выкладчик', 'Гастроном', 'Бригадир', 'Грузчик', 'Мобильный грузчик', 'Мобильный универсал'];
/**
* Создать
*
@ -48,7 +54,7 @@ final class task extends core
$this->errors['tasks'] ??= [];
if (!empty($json = json_decode(file_get_contents('php://input'), true, 4))) {
// Найден и декодирован json-документ с данными для создания заявок
foreach ($json as $work => $tasks) {
// Перебор категорий (колонок)
@ -145,7 +151,7 @@ final class task extends core
// Перебор фильтров временного промежутка (И)
// Инициализация значения (приоритет у cookie)
if (empty($value = (int) ($_COOKIE["tasks_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? (($name === 'from') ? time() : strtotime('+1 month'))))) continue;
if (empty($value = (int) ($_COOKIE["tasks_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? (($name === 'from') ? time() : strtotime('+1 month'))))) continue;
// Генерация AQL-выражения для инъекции в строку запроса
if ($name === 'from') $interval .= " && task.date >= $value";
@ -162,7 +168,7 @@ final class task extends core
// Перебор фильтров с произвольными значениями (И)
// Инициализация значения (приоритет у cookie)
$value = $_COOKIE["tasks_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? null;
$value = $_COOKIE["tasks_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? null;
// Найдено значение?
if ($value === null) continue;
@ -182,7 +188,7 @@ final class task extends core
// Перебор фильтров по статусам
// Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue)
if (empty($value = $_COOKIE["tasks_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? 0)) continue;
if (empty($value = $_COOKIE["tasks_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? 0)) continue;
// Конвертация ярлыков
$converted = match ($name) {
@ -238,7 +244,7 @@ final class task extends core
else if (empty($this->session->buffer[$_SERVER['INTERFACE']]['tasks']['page'])) $this->session->write(['tasks' => ['page' => 1]]);
// Инициализация строки поиска
$search = $_COOKIE["tasks_filter_search"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters']['search'] ?? '';
$search = $_COOKIE["tasks_filter_search"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters']['search'] ?? '';
if (mb_strlen($search) < 3) $search = null;
$search_query = empty($search)
? null
@ -266,8 +272,11 @@ final class task extends core
$search_query,
empty($filters) ? null : " && ($filters)"
),
after: 'COLLECT x = worker, y = market, z = task OPTIONS { method: "sorted" }',
page: (int) $this->session->buffer['worker']['tasks']['page'],
sort: 'z.date DESC, z.created DESC, z._key DESC, x.created DESC, y.created DESC, x._key DESC, y._key DESC',
target: empty($search) ? model::COLLECTION : 'registry_tasks',
return: '{task: z, worker: x, market: y}',
binds: ['worker' => account::worker($this->account->getId())?->id] + (empty($search) ? [] : ['search' => $search])
);
} else if ($_SERVER['INTERFACE'] === 'market') {
@ -282,8 +291,11 @@ final class task extends core
$search_query,
empty($filters) ? null : " && ($filters)"
),
after: 'COLLECT x = worker, y = market, z = task OPTIONS { method: "sorted" }',
page: (int) $this->session->buffer['market']['tasks']['page'],
sort: 'z.date DESC, z.created DESC, z._key DESC, x.created DESC, y.created DESC, x._key DESC, y._key DESC',
target: empty($search) ? model::COLLECTION : 'registry_tasks',
return: '{task: z, worker: x, market: y}',
binds: ['market' => account::market($this->account->getId())?->id] + (empty($search) ? [] : ['search' => $search])
);
} else if ($_SERVER['INTERFACE'] === 'operator') {
@ -298,8 +310,11 @@ final class task extends core
$search_query,
empty($filters) ? null : " && ($filters)"
),
after: 'COLLECT x = worker, y = market, z = task OPTIONS { method: "sorted" }',
page: (int) $this->session->buffer['operator']['tasks']['page'],
sort: 'z.date DESC, z.created DESC, z._key DESC, x.created DESC, y.created DESC, x._key DESC, y._key DESC',
target: empty($search) ? model::COLLECTION : 'registry_tasks',
return: '{task: z, worker: x, market: y}',
binds: empty($search) ? [] : [
'search' => $search
]
@ -316,8 +331,11 @@ final class task extends core
$search_query,
empty($filters) ? null : " && ($filters)"
),
after: 'COLLECT x = worker, y = market, z = task OPTIONS { method: "sorted" }',
page: (int) $this->session->buffer['administrator']['tasks']['page'],
sort: 'z.date DESC, z.created DESC, z._key DESC, x.created DESC, y.created DESC, x._key DESC, y._key DESC',
target: empty($search) ? model::COLLECTION : 'registry_tasks',
return: '{task: z, worker: x, market: y}',
binds: empty($search) ? [] : [
'search' => $search
]
@ -407,6 +425,8 @@ final class task extends core
* @param array $rows Строки
*
* @return array Обработанные строки
*
* @todo Переделать в model::hours($from, $to);
*/
protected static function preprocessing(account $account, array $rows): array
{
@ -431,7 +451,6 @@ final class task extends core
// Перенос времени в дату
$start = $date->setTime((int) $start->format('H'), (int) $start->format('i'));
// Запись в буфер
$link->task = ['start' => $start] + $link->task;
}
@ -452,6 +471,11 @@ final class task extends core
$link->task = ['end' => $end] + $link->task;
}
// Инициализация данных заблокировавшего аккаунта
if (isset($link->task['block']) && $link->task['block']['expires'] > time() && $link->task['block']['account'] !== (int) $account->getKey())
$link->task = ['block' => ['_account' => account::read('d._key == "' . $link->task['block']['account'] . '"')] + $link->task['block']] + $link->task;
else $link->task = ['block' => null] + $link->task;
// Инициализация буфера сгенерированных данных работы для шаблонизатора
$generated = [];
@ -475,7 +499,7 @@ final class task extends core
];
if ($account->type === 'worker') {
// Оператор или администратор
// Сотрудник
foreach ($link->task['chats']['worker'] ?? [] as $message) {
// Перебор сообщений из чата: СОТРУДНИК <-> ОПЕРАТОР
@ -484,7 +508,7 @@ final class task extends core
if (!array_key_exists((string) $account->getKey(), $message['readed'] ?? [])) ++$generated['chat']['unreaded'];
}
} else if ($account->type === 'market') {
// Оператор или администратор
// Магазин
foreach ($link->task['chats']['market'] ?? [] as $message) {
// Перебор сообщений из чата: МАГАЗИН <-> ОПЕРАТОР
@ -535,8 +559,46 @@ final class task extends core
// Заявка подтверждена?
if ($task->confirmed) throw new exception('Запрещено редактировать подтверждённую заявку');
// Заявка завершена?
if ($this->account->type === 'market' && $task->completed) throw new exception('Запрещено редактировать завершённую заявку');
if ($this->account->type === 'market') {
// Магазин
// Инициализация даты
$date = (new DateTime('@' . $task->date))->setTimezone(new DateTimeZone('Asia/Krasnoyarsk'));
// Инициализация времени
$start = datetime::createFromFormat('H:i', (string) $task->start);
$end = datetime::createFromFormat('H:i', (string) $task->end);
// Перенос времени в дату
$start = $date->setTime((int) $start->format('H'), (int) $start->format('i'))->format('U');
$end = $date->setTime((int) $end->format('H'), (int) $end->format('i'))->format('U');
// Заявка уже начата?
if (time() - $start > 0 && time() - $end < 0)
throw new exception('Запрещено редактировать начатую заявку');
// Заявка уже прошла?
else if (time() - $end > 0 && $task->completed !== true)
throw new exception('Запрещено редактировать прошедшую заявку');
// Заявка уже завершена?
else if ($task->completed === true)
throw new exception('Запрещено редактировать завершённую заявку');
// Прошло более 30 минут после создания заявки? (1800 секунд = 30 минут)
/* if (time() - $task->created > 1800)
throw new exception('Запрещено редактировать заявку спустя 30 минут после создания'); */
// До начала заявки осталось менее 16 часов? (57600 секунд = 16 часов)
if ($start - time() < 57600)
throw new exception('Запрещено редактировать заявку за менее 16 часов до её начала');
}
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (!empty($parameters['worker'])) {
// Передан сотрудник
@ -586,6 +648,15 @@ final class task extends core
if (($worker = worker::read('d.id == "' . $parameters['worker'] . '" && d.active == true', amount: 1)) instanceof _document) {
// Найден сотрудник (запрашиваемый для записи сотрудник существует в базе данных)
if (!$worker->fired) {
// Не уволен сотрудник
if (in_array($task->work, $worker->works, true)) {
// Работа соответствует подходящим сотруднику
if ((($age = (new DateTime)->diff(DateTime::createFromFormat('d.m.Y', $worker->birth))->y) > 15 && ($task->work === 'Выкладчик' || $task->work === 'Гастроном')) || $age > 17) {
// Подходит по возрасту сотрудник
if ($task->worker !== $parameters['worker']) {
// Идентификатор запрашиваемого сотрудника не равен актуальному
@ -629,6 +700,9 @@ final class task extends core
flush();
} else throw new exception('Не удалось записать изменения в базу данных');
} else throw new exception('Сотрудник уже назначен');
} else throw new exception('Сотрудник не подходит по возрасту');
} else throw new exception('Заявка не подходит по типу работы для сотрудника');
} else throw new exception('Нельзя назначить уволенного сотрудника');
} else throw new exception('Не найден сотрудник');
}
} else if (!empty($parameters['market'])) {
@ -777,9 +851,11 @@ final class task extends core
// Инициализация данных сотрудника
$this->view->task = model::read(
'd._key == "' . $parameters['task'] . '"',
return: $this->account->type === 'market'
? '{_key: d._key, created: d.created, updated: d.updated, date: d.date, start: d.start, end: d.end, market: d.market, confirmed: d.confirmed, completed: d.completed }'
: '{_key: d._key, created: d.created, updated: d.updated, date: d.date, start: d.start, end: d.end, market: d.market, confirmed: d.confirmed, completed: d.completed, hided: d.hided }'
return: match ($this->account->type) {
'market' => '{_key: d._key, created: d.created, updated: d.updated, date: d.date, start: d.start, end: d.end, market: d.market, confirmed: d.confirmed, completed: d.completed }',
'administrator', 'operator' => '{_key: d._key, created: d.created, updated: d.updated, date: d.date, start: d.start, end: d.end, market: d.market, confirmed: d.confirmed, completed: d.completed, hided: d.hided, updates: d.updates }',
default => 'd'
}
)->getAll();
// Заявка не принадлежит запросившему магазину?
@ -814,7 +890,20 @@ final class task extends core
// Перевод ключей на русский язык
foreach ($this->view->task as $key => $value)
if (match ($key) {
if ($key === 'updates')
foreach ($value ?? [] as $key => $value) $buffer['updates'][$key] = [
'label' => match ($key) {
'operator' => 'Оператор',
'market' => 'Магазин',
'administrator' => 'Администратор',
'worker' => 'Сотрудник',
default => $key
},
'value' => [
'id' => $value
] + account::read("d._key == \"$value\"", amount: 1)?->name ?? []
];
else if (match ($key) {
'created', 'updated', 'confirmed', 'hided', 'completed', '_key' => true,
'start', 'end' => $passed, // Только для завершённой заявки
default => false
@ -902,7 +991,6 @@ final class task extends core
}
}
/**
* Прочитать данные сотрудника
*
@ -951,6 +1039,7 @@ final class task extends core
'tax' => 'ИНН',
'city' => 'Город',
'payment' => 'Форма оплаты',
'works' => 'Типы работ',
default => $key
},
'value' => $value
@ -1058,6 +1147,7 @@ final class task extends core
'number' => 'Номер',
'mail' => 'Почта',
'commentary' => 'Комментарий',
'bans' => 'Заблокированные',
default => $key
},
'value' => $value
@ -1151,6 +1241,12 @@ final class task extends core
// Изменение статуса подтверждения
$task->confirmed = !$task->confirmed;
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано изменение в базу данных
@ -1171,7 +1267,7 @@ final class task extends core
// Генерация ответа
echo json_encode(
[
'confirmed' => $this->view->rows[0]->task['confirmed'],
'confirmed' => $this->view->rows[0]->task['confirmed'] ?? null,
'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'),
'errors' => self::parse_only_text($this->errors)
]
@ -1270,6 +1366,12 @@ final class task extends core
}
}
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано изменение в базу данных
@ -1385,7 +1487,7 @@ final class task extends core
else {
// Получена оценка
// Запись оценики
// Запись оценки
$task->rating = $parameters['rating'];
if (!empty($parameters['review'])) {
@ -1400,7 +1502,52 @@ final class task extends core
// Снятие с публикации
$task->published = false;
// Иниализация сотрудника
$worker = worker::read('d.id == "' . $task->worker . '"');
if ($worker instanceof _document) {
// Найден сотрудник
// Инициализация магазина
$market = market::read('d.id == "' . $task->market . '"');
if ($market instanceof _document) {
// Найден магазин
// Подсчёт часов работы
$hours = model::hours($task->start, $task->end, $this->errors);
// Инициализация цены работы сотрудника за 1 час
$hour = payments::hour('worker', $market->city, $task->work);
// Подсчёт оплаты за работу
$payment = $hour * $hours;
// Инициализация штрафа
$penalty = payments::penalty($task->rating ?? null);
if ($penalty === null) $penalty = -$payment;
// Инициализация премии
$bonus = payments::bonus($task->rating ?? null);
// Запись результатов
$task->result = [
'hours' => $hours,
'hour' => $hour,
'penalty' => $penalty,
'bonus' => $bonus,
'processed' => false
];
}
}
}
// Запись в реcстре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано изменение в базу данных
@ -1493,6 +1640,12 @@ final class task extends core
// Изменение статуса скрытия
$task->hided = !$task->hided;
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано изменение в базу данных
@ -1513,7 +1666,7 @@ final class task extends core
// Генерация ответа
echo json_encode(
[
'hided' => $this->view->rows[0]->task['hided'],
'hided' => $this->view->rows[0]->task['hided'] ?? null,
'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'),
'errors' => self::parse_only_text($this->errors)
]
@ -1561,6 +1714,94 @@ final class task extends core
}
}
/**
* Сгенерировать строку
*
* Используется тогда, когда нужно получить HTML-код строки заявки
*
* @param array $parameters Параметры запроса
*
* @return void В буфер вывода JSON-документ с запрашиваемыми параметрами
*/
public function row(array $parameters = []): void
{
try {
if ($this->account->status()) {
// Авторизован аккаунт
// Инициализация строки в глобальную переменную шаблонизатора
if ($this->account->type === 'worker')
$this->view->rows = model::list(before: 'FILTER task._key == "' . $parameters['task'] . '" && task.worker == "' . account::worker($this->account->getId())?->id . '"');
else if ($this->account->type === 'operator')
$this->view->rows = model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"');
else if ($this->account->type === 'market')
$this->view->rows = model::list(before: 'FILTER task._key == "' . $parameters['task'] . '" && task.market == "' . account::market($this->account->getId())?->id . '"');
else if ($this->account->type === 'administrator')
$this->view->rows = model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"');
else $this->view->rows = [];
// Генерация данных для генерации строки
$this->view->rows = static::preprocessing($this->account, $this->view->rows);
// Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение)
$this->view->page = null;
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode(
[
'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'),
'errors' => self::parse_only_text($this->errors)
]
);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Вы не авторизованы');
} catch (exception $e) {
// Запись в реестр ошибок
$this->errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode(
[
'errors' => self::parse_only_text($this->errors)
]
);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
}
/**
* Удалить
*
@ -1584,6 +1825,9 @@ final class task extends core
// Заявка подтверждена?
if ($task->confirmed) throw new exception('Запрещено удалять подтверждённую заявку');
if ($this->account->type === 'market') {
// Магазин
// Инициализация даты
$date = (new DateTime('@' . $task->date))->setTimezone(new DateTimeZone('Asia/Krasnoyarsk'));
@ -1595,17 +1839,26 @@ final class task extends core
$start = $date->setTime((int) $start->format('H'), (int) $start->format('i'))->format('U');
$end = $date->setTime((int) $end->format('H'), (int) $end->format('i'))->format('U');
// Заявка уже начата
if ($this->account->type === 'market' and time() - $start > 0)
// Заявка уже начата?
if (time() - $start > 0 && time() - $end < 0)
throw new exception('Запрещено удалять начатую заявку');
// Заявка уже завершена
if ($this->account->type === 'market' and $task->completed === true || time() - $end > 0)
// Заявка уже прошла?
else if (time() - $end > 0 && $task->completed !== true)
throw new exception('Запрещено удалять прошедшую заявку');
// Заявка уже завершена?
else if ($task->completed === true)
throw new exception('Запрещено удалять завершённую заявку');
// Прошло более 30 минут после создания заявки? (1800 секунд = 30 минут)
if ($this->account->type === 'market' and time() - $task->created > 1800)
throw new exception('Запрещено удалять заявку спустя 30 минут после создания');
/* if (time() - $task->created > 1800)
throw new exception('Запрещено удалять заявку спустя 30 минут после создания'); */
// До начала заявки осталось менее 16 часов? (57600 секунд = 16 часов)
if ($start - time() < 57600)
throw new exception('Запрещено удалять заявку за менее 16 часов до её начала');
}
if ($task instanceof _document) {
// Найдена заявка
@ -1613,6 +1866,12 @@ final class task extends core
// Изменение статуса
$task->status = 'deleted';
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Помечено как удалённое
@ -1754,7 +2013,6 @@ final class task extends core
}
}
/**
* Прочитать данные работ для <datalist>
*
@ -1765,7 +2023,10 @@ final class task extends core
public function works(array $parameters = []): void
{
try {
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'market')) {
if (!empty($parameters['task'])) {
// Запрошены данные работ по заявке
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'market' || $this->account->type === 'worker')) {
// Авторизован аккаунт администратора, оператора или магазина
// Инициализация данных
@ -1774,11 +2035,20 @@ final class task extends core
if ($this->view->task instanceof _document) {
// Найдена заявка
// Заявка не принадлежит запросившему магазину?
if ($this->account->type === 'market' and $this->view->task->market !== account::market($this->account->getId())?->id)
throw new exception('Вы не авторизованы для просмотра типа работы этой заявки');
// Заявка не принадлежит запросившему сотруднику?
if ($this->account->type === 'worker' and $this->view->task->worker !== account::worker($this->account->getId())?->id)
throw new exception('Вы не авторизованы для просмотра типа работы этой заявки');
// Инициализация списка работ
$this->view->works = ['Кассир', 'Выкладчик', 'Гастроном', 'Бригадир', 'Грузчик', 'Мобильный грузчик', 'Мобильный универсал'];
$this->view->works = static::WORKS;
// Проверка на существование записанной в задаче работы в списке существующих работ и запись об этом в глобальную переменную шаблонизатора
foreach ($this->view->works as $work) if ($this->view->task->work === $work) $this->view->exist = true;
foreach ($this->view->works as $work)
if (in_array($work, $this->view->worker->works)) $this->view->exist = true;
// Запись заголовков ответа
header('Content-Type: application/json');
@ -1804,6 +2074,86 @@ final class task extends core
flush();
} else throw new exception('Не найдена заявка');
} else throw new exception('Вы не авторизованы');
} else if (!empty($parameters['worker'])) {
// Запрошены данные работ по сотруднику
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'worker')) {
// Авторизован аккаунт администратора, оператора или магазина
// Инициализация данных
$this->view->worker = worker::read('d.id == "' . $parameters['worker'] . '"');
if ($this->view->worker instanceof _document) {
// Найден сотрудник
// Сотрудник не принадлежит запросившему аккаунту?
if ($this->account->type === 'worker' and $this->view->worker->id !== account::worker($this->account->getId())?->id)
throw new exception('Вы не авторизованы для просмотра типа работы этого сотрудника');
// Инициализация списка работ
$this->view->works = static::WORKS;
// Проверка на существование записанной в задаче работы в списке существующих работ и запись об этом в глобальную переменную шаблонизатора
foreach ($this->view->works as $work)
if (in_array($work, $this->view->worker->works)) $this->view->exist = true;
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode(
[
'works' => $this->view->render(DIRECTORY_SEPARATOR . 'lists' . DIRECTORY_SEPARATOR . 'works.html'),
'errors' => self::parse_only_text($this->errors)
]
);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не найден сотрудник');
} else throw new exception('Вы не авторизованы');
} else {
// Запрошен список работ
if ($this->account->status()) {
// Авторизован аккаунт
// Инициализация списка работ
$this->view->works = ['Кассир', 'Выкладчик', 'Гастроном', 'Бригадир', 'Грузчик', 'Мобильный грузчик', 'Мобильный универсал'];
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode(
[
'works' => $this->view->render(DIRECTORY_SEPARATOR . 'lists' . DIRECTORY_SEPARATOR . 'works.html'),
'errors' => self::parse_only_text($this->errors)
]
);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Вы не авторизованы');
}
} catch (exception $e) {
// Запись в реестр ошибок
$this->errors[] = [
@ -1860,6 +2210,9 @@ final class task extends core
// Заявка подтверждена?
if ($task->confirmed) throw new exception('Запрещено редактировать тип работы у подтверждённой заявки');
if ($this->account->type === 'market') {
// Магазин
// Инициализация даты
$date = (new DateTime('@' . $task->date))->setTimezone(new DateTimeZone('Asia/Krasnoyarsk'));
@ -1871,14 +2224,27 @@ final class task extends core
$start = $date->setTime((int) $start->format('H'), (int) $start->format('i'))->format('U');
$end = $date->setTime((int) $end->format('H'), (int) $end->format('i'))->format('U');
// Заявка уже начата
if ($this->account->type === 'market' and time() - $start > 0)
// Заявка уже начата?
if (time() - $start > 0 && time() - $end < 0)
throw new exception('Запрещено редактировать тип работы начатой заявки');
// Заявка уже завершена
if ($this->account->type === 'market' and $task->completed === true || time() - $end > 0)
// Заявка уже прошла?
else if (time() - $end > 0 && $task->completed !== true)
throw new exception('Запрещено редактировать тип работы прошедшей заявки');
// Заявка уже завершена?
else if ($task->completed === true)
throw new exception('Запрещено редактировать тип работы завершённой заявки');
// Прошло более 30 минут после создания заявки? (1800 секунд = 30 минут)
/* if (time() - $task->created > 1800)
throw new exception('Запрещено редактировать заявку спустя 30 минут после создания'); */
// До начала заявки осталось менее 16 часов? (57600 секунд = 16 часов)
if ($start - time() < 57600)
throw new exception('Запрещено редактировать тип работы заявки за менее 16 часов до её начала');
}
if ($task instanceof _document) {
// Найдена заявка
@ -1888,6 +2254,12 @@ final class task extends core
default => 'Кассир'
};
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано в ArangoDB
@ -2004,6 +2376,12 @@ final class task extends core
// Изменение статуса
$task->description = $parameters['description'];
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано в ArangoDB
@ -2088,6 +2466,9 @@ final class task extends core
// Заявка подтверждена?
if ($task->confirmed) throw new exception('Запрещено редактировать дату и время у подтверждённой заявки');
if ($this->account->type === 'market') {
// Магазин
// Инициализация даты
$date = (new DateTime('@' . $task->date))->setTimezone(new DateTimeZone('Asia/Krasnoyarsk'));
@ -2099,14 +2480,27 @@ final class task extends core
$start = $date->setTime((int) $start->format('H'), (int) $start->format('i'))->format('U');
$end = $date->setTime((int) $end->format('H'), (int) $end->format('i'))->format('U');
// Заявка уже начата
if ($this->account->type === 'market' and time() - $start > 0)
// Заявка уже начата?
if (time() - $start > 0 && time() - $end < 0)
throw new exception('Запрещено редактировать дату и время начатой заявки');
// Заявка уже завершена
if ($this->account->type === 'market' and $task->completed === true || time() - $end > 0)
// Заявка уже прошла?
else if (time() - $end > 0 && $task->completed !== true)
throw new exception('Запрещено редактировать дату и время прошедшей заявки');
// Заявка уже завершена?
else if ($task->completed === true)
throw new exception('Запрещено редактировать дату и время завершённой заявки');
// Прошло более 30 минут после создания заявки? (1800 секунд = 30 минут)
/* if (time() - $task->created > 1800)
throw new exception('Запрещено редактировать заявку спустя 30 минут после создания'); */
// До начала заявки осталось менее 16 часов? (57600 секунд = 16 часов)
if ($start - time() < 57600)
throw new exception('Запрещено редактировать дату и время заявки за менее 16 часов до её начала');
}
if ($task instanceof _document) {
// Найдена заявка
@ -2115,6 +2509,12 @@ final class task extends core
if (!empty($parameters['start'])) $task->start = $parameters['start'];
if (!empty($parameters['end'])) $task->end = $parameters['end'];
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано в ArangoDB
@ -2202,6 +2602,12 @@ final class task extends core
// Запись комментария
$task->commentary = $parameters['commentary'];
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано в ArangoDB
@ -2289,6 +2695,12 @@ final class task extends core
// Запись статуса о публикации
$task->published = true;
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано в ArangoDB
@ -2376,6 +2788,12 @@ final class task extends core
// Запись статуса о публикации
$task->published = false;
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано в ArangoDB
@ -2725,6 +3143,12 @@ final class task extends core
$task->problematic = false;
}
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано в ArangoDB
@ -2797,6 +3221,12 @@ final class task extends core
$task->problematic = false;
}
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано в ArangoDB
@ -2810,7 +3240,6 @@ final class task extends core
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode(
[

View File

@ -29,6 +29,11 @@ final class worker extends core
{
use errors;
/**
* Типы работ
*/
final public const WORKS = ['Кассир', 'Выкладчик', 'Гастроном', 'Бригадир', 'Грузчик', 'Мобильный грузчик', 'Мобильный универсал'];
/**
* Главная страница
*
@ -44,7 +49,7 @@ final class worker extends core
// Перебор фильтров статусов
// Инициализация значения (приоритет у cookie)
$value = $_COOKIE["workers_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['workers']['filters'][$name] ?? 0;
$value = $_COOKIE["workers_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['workers']['filters'][$name] ?? 0;
// Инициализировано значение?
if ($value === null || $value === 0) continue;
@ -94,7 +99,7 @@ final class worker extends core
// Перебор фильтров статусов
// Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue)
if (empty($value = $_COOKIE["workers_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['workers']['filters'][$name] ?? 0)) continue;
if (empty($value = $_COOKIE["workers_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['workers']['filters'][$name] ?? 0)) continue;
// Конвертация ярлыков
$converted = match ($name) {
@ -135,7 +140,7 @@ final class worker extends core
if (!empty($filters_statuses_after_merged)) $filters_after .= empty($filters_after) ? $filters_statuses_after_merged : " && ($filters_statuses_after_merged)";
// Инициализация строки поиска
$search = $_COOKIE["workers_filter_search"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['workers']['filters']['search'] ?? '';
$search = $_COOKIE["workers_filter_search"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['workers']['filters']['search'] ?? '';
if (mb_strlen($search) < 3) $search = null;
$search_query = empty($search)
? null
@ -177,8 +182,7 @@ final class worker extends core
|| (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(a.requisites, TOKENS(@search, 'text_ru')[0], 1, true))
|| (LENGTH(@search) > 7 && LEVENSHTEIN_MATCH(a.tax, TOKENS(@search, 'text_ru')[0], 1, true))
OPTIONS { collections: ["account", "worker"] }
AQL;
AQL;;
// Инициализация данных для генерации HTML-документа с таблицей
$this->view->rows = registry::workers(
before: sprintf(
@ -195,12 +199,15 @@ final class worker extends core
FILTER account.type == 'worker' && b.deleted != true
%s
%s
COLLECT x = account, y = worker OPTIONS { method: "sorted" }
AQL,
empty($filters_before) ? null : "FILTER $filters_before",
empty($filters_after) ? null : "FILTER $filters_after"
),
page: (int) $this->session->buffer[$_SERVER['INTERFACE']]['workers']['page'],
sort: 'x.created DESC, y.created DESC, x._key DESC, y._key DESC',
target: empty($search) ? account::COLLECTION : 'registry_accounts',
return: '{account: x, worker: y}',
binds: empty($search) ? [] : [
'search' => $search
]
@ -261,6 +268,7 @@ final class worker extends core
if (!empty($parameters['requisites']) && $parameters['worker_requisites'][-1] === '.') $parameters['worker_requisites'] .= '.';
if (!empty($parameters['worker_birth'])) $parameters['worker_birth'] = DateTime::createFromFormat('Y-m-d', $parameters['worker_birth'])->getTimestamp();
if (!empty($parameters['worker_issued'])) $parameters['worker_issued'] = DateTime::createFromFormat('Y-m-d', $parameters['worker_issued'])->getTimestamp();
if (!empty($parameters['works'])) $parameters['works'] = in_array($parameters['works'], static::WORKS) ? $parameters['works'] : static::WORKS[0];
if (!empty($parameters['worker_hiring'])) $parameters['worker_hiring'] = DateTime::createFromFormat('Y-m-d', $parameters['worker_hiring'])->getTimestamp();
// Создание аккаунта
@ -353,6 +361,7 @@ final class worker extends core
'city' => $parameters['worker_city'],
'district' => $parameters['worker_district'],
'address' => $parameters['worker_address'],
'work' => $parameters['worker_work'],
'hiring' => $parameters['worker_hiring'],
'rating' => 3
],
@ -389,7 +398,7 @@ final class worker extends core
// Авторизован аккаунт администратора или оператора
// Инициализация данных сотрудника
$worker = model::read('d.id == "' . $parameters['id'] . '"', return: '{ name: d.name, number: d.number, mail: d.mail, birth: d.birth, passport: d.passport, issued: d.issued, department: d.department, requisites: d.requisites, payment: d.payment, tax: d.tax, city: d.city, district: d.district, address: d.address, hiring: d.hiring}')->getAll();
$worker = model::read('d.id == "' . urldecode($parameters['id']) . '"', return: '{ name: d.name, number: d.number, mail: d.mail, birth: d.birth, passport: d.passport, issued: d.issued, department: d.department, requisites: d.requisites, payment: d.payment, tax: d.tax, city: d.city, district: d.district, address: d.address, worl: d.work, hiring: d.hiring, registration: d.registration}')->getAll();
if (!empty($worker)) {
// Найдены данные сотрудника
@ -429,7 +438,7 @@ final class worker extends core
// Авторизован аккаунт администратора или оператора
// Инициализация данных сотрудника
$worker = model::read('d.id == "' . $parameters['id'] . '"');
$worker = model::read('d.id == "' . urldecode($parameters['id']) . '"');
if (!empty($worker)) {
// Найден сотрудник
@ -438,6 +447,11 @@ final class worker extends core
if (!empty($parameters['birth'])) $parameters['birth'] = DateTime::createFromFormat('Y-m-d', $parameters['birth'])->getTimestamp();
if (!empty($parameters['issued'])) $parameters['issued'] = DateTime::createFromFormat('Y-m-d', $parameters['issued'])->getTimestamp();
if (!empty($parameters['hiring'])) $parameters['hiring'] = DateTime::createFromFormat('Y-m-d', $parameters['hiring'])->getTimestamp();
if (!empty($buffer = explode(':', $parameters['works']))) {
$parameters['works'] = [];
foreach ($buffer ?? [] as $work)
if (in_array($work, static::WORKS)) array_push($parameters['works'], $work);
}
// Инициализация параметров (перезапись переданными значениями)
if ($parameters['name_first'] !== $worker->name['first']) $worker->name = ['first' => $parameters['name_first']] + $worker->name;
@ -458,7 +472,9 @@ final class worker extends core
if ($parameters['city'] !== $worker->city) $worker->city = $parameters['city'];
if ($parameters['district'] !== $worker->district) $worker->district = $parameters['district'];
if ($parameters['address'] !== $worker->address) $worker->address = $parameters['address'];
if ($parameters['works'] !== $worker->works) $worker->works = $parameters['works'];
if ($parameters['hiring'] !== $worker->hiring) $worker->hiring = $parameters['hiring'];
if ($parameters['registration'] !== $worker->registration) $worker->registration = $parameters['registration'];
if (_core::update($worker)) {
// Записаны данные сотрудника
@ -513,6 +529,114 @@ final class worker extends core
return null;
}
/**
* Пометить уволенным
*
* @param array $parameters Параметры запроса
*/
public function fire(array $parameters = []): ?string
{
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
// Инициализация данных сотрудника
$worker = model::read('d.id == "' . urldecode($parameters['id']) . '"');
if (!empty($worker)) {
// Найден сотрудник
// Увольнение
$worker->active = false;
$worker->fired = true;
if (_core::update($worker)) {
// Записаны данные сотрудника
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'fired' => true,
'errors' => self::parse_only_text($this->errors)
];
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не удалось записать изменения в базу данных');
} else throw new exception('Не удалось найти аккаунт');
}
// Возврат (провал)
return null;
}
/**
* Снять пометку уволенного (нанять)
*
* @param array $parameters Параметры запроса
*/
public function hire(array $parameters = []): ?string
{
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
// Инициализация данных сотрудника
$worker = model::read('d.id == "' . urldecode($parameters['id']) . '"');
if (!empty($worker)) {
// Найден сотрудник
// Увольнение
$worker->active = true;
$worker->fired = false;
if (_core::update($worker)) {
// Записаны данные сотрудника
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'hired' => true,
'errors' => self::parse_only_text($this->errors)
];
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не удалось записать изменения в базу данных');
} else throw new exception('Не удалось найти аккаунт');
}
// Возврат (провал)
return null;
}
/**
* Прочитать данные сотрудников для <datalist>
*

View File

@ -42,7 +42,7 @@ final class account extends core
* Конструктор
*
* @param ?session $session Инстанция сессии
* @param ?string $authenticate Аутентифицировать аккаунт? Если да, то какой категории? ([worker|operator|market] из $_SERVER['INTERFACE'])
* @param ?string $authenticate Аутентифицировать аккаунт? Если да, то какой категории? ([worker|market|operator|administrator] из $_SERVER['INTERFACE'])
* @param array &$errors Реестр ошибок
*
* @return static Инстанция аккаунта
@ -60,14 +60,21 @@ final class account extends core
$this->document = $account->document;
// Связь сессии с аккаунтом
session::connect($session->getId(), $this->document->getId(), $errors);
self::session($session->getId(), $this->document->getId(), $errors);
// Блокировка доступа
if ($account?->active !== true) throw new exception('Свяжитесь с оператором');
else if ($account?->banned === true) throw new exception('Свяжитесь с оператором');
else if ($account->type === 'worker')
if (($worker = account::worker($account->getId()))?->active !== true) throw new exception('Свяжитесь с оператором');
else if ($worker?->fired === true) throw new exception('Свяжитесь с оператором');
return $this;
} else {
// Не найден связанный с сессией аккаунт
if (
match ($authenticate) {
'worker', 'operator', 'market', 'administrator' => true,
'worker', 'market', 'operator', 'administrator' => true,
default => false
}
) {
@ -89,16 +96,21 @@ final class account extends core
$this->document = $account;
// Связь сессии с аккаунтом
session::connect($session->getId(), $this->document->getId(), $errors);
self::session($session->getId(), $this->document->getId(), $errors);
// Удаление использованных данных из буфера сессии
$session->write(['entry' => ['number' => null, 'password' => null]]);
// Блокировка доступа
if ($account?->active !== true) throw new exception('Свяжитесь с оператором');
else if ($account?->banned === true) throw new exception('Свяжитесь с оператором');
else if ($account->type === 'worker')
if (($worker = account::worker($account->getId()))?->active !== true) throw new exception('Свяжитесь с оператором');
else if ($worker?->fired === true) throw new exception('Свяжитесь с оператором');
// Выход (успех)
return $this;
} else throw new exception('Неправильный пароль');
throw new exception('Неизвестная ошибка на этапе проверки пароля');
} else throw new exception('Не найден аккаунт');
} else throw new exception('Не найден пароль в буфере сессии');
} else if (!empty($session->buffer['operator']['entry']['_key'])) {
@ -117,7 +129,7 @@ final class account extends core
$this->document = $account;
// Связь сессии с аккаунтом
session::connect($session->getId(), $this->document->getId(), $errors);
self::session($session->getId(), $this->document->getId(), $errors);
// Удаление использованных данных из буфера сессии
$session->write(['entry' => ['_key' => null, 'password' => null]]);
@ -125,8 +137,6 @@ final class account extends core
// Выход (успех)
return $this;
} else throw new exception('Неправильный пароль');
throw new exception('Неизвестная ошибка на этапе проверки пароля');
}
} else throw new exception('Не найден пароль в буфере сессии');
} else if (!empty($session->buffer['market']['entry']['id'])) {
@ -145,7 +155,7 @@ final class account extends core
$this->document = $account;
// Связь сессии с аккаунтом
session::connect($session->getId(), $this->document->getId(), $errors);
self::session($session->getId(), $this->document->getId(), $errors);
// Удаление использованных данных из буфера сессии
$session->write(['entry' => ['_key' => null, 'password' => null]]);
@ -153,8 +163,6 @@ final class account extends core
// Выход (успех)
return $this;
} else throw new exception('Неправильный пароль');
throw new exception('Неизвестная ошибка на этапе проверки пароля');
}
} else throw new exception('Не найден пароль в буфере сессии');
} else if (!empty($session->buffer['administrator']['entry'])) {
@ -173,7 +181,7 @@ final class account extends core
$this->document = $account;
// Связь сессии с аккаунтом
session::connect($session->getId(), $this->document->getId(), $errors);
self::session($session->getId(), $this->document->getId(), $errors);
// Удаление использованных данных из буфера сессии
$session->write(['entry' => ['_key' => null, 'password' => null]]);
@ -181,8 +189,6 @@ final class account extends core
// Выход (успех)
return $this;
} else throw new exception('Неправильный пароль');
throw new exception('Неизвестная ошибка на этапе проверки пароля');
}
} else throw new exception('Не найден пароль в буфере сессии');
} else throw new exception('Не найдены данные первичной идентификации в буфере сессии');
@ -227,7 +233,7 @@ final class account extends core
LIMIT 1
RETURN e
)
FILTER d._id == e[0]._to && d.active == true
FILTER d._id == e[0]._to
SORT d.created DESC, d._key DESC
LIMIT 1
RETURN d
@ -285,7 +291,7 @@ final class account extends core
LIMIT 1
RETURN e
)
FILTER d._id == e[0]._to && d.active == true
FILTER d._id == e[0]._to
SORT d.created DESC, d.id DESC
LIMIT 1
RETURN d
@ -312,7 +318,7 @@ final class account extends core
}
/**
* Инициализировать связь аккаунта с сотрудником
* Подключить к сотруднику
*
* Ищет связь аккаунта с сотрудником, если не находит, то создаёт её
*
@ -322,6 +328,9 @@ final class account extends core
* @param array &$errors Реестр ошибок
*
* @return bool Связан аккаунт с сотрудником?
*
* @todo
* 1. Переделать на подобие account::session и перенести в mirzaev/ebala/models/worker
*/
public static function connect(string $account, string $target, string $type = 'worker', array &$errors = []): bool
{
@ -397,6 +406,65 @@ final class account extends core
return null;
}
/**
* Подключить к сессии
*
* Ищет связь сессии с аккаунтом, если не находит, то создаёт её
*
* @param string $session Идентификатор сессии
* @param string $account Идентификатор аккаунта
* @param array &$errors Реестр ошибок
*
* @return bool Аккаунт подключен к сессии?
*/
public static function session(string $session, string $account, array &$errors = []): bool
{
try {
if (
collection::init(static::$arangodb->session, self::COLLECTION)
&& collection::init(static::$arangodb->session, session::COLLECTION)
&& collection::init(static::$arangodb->session, session::COLLECTION . '_edge_' . self::COLLECTION, true)
) {
// Инициализированы коллекции
if (
collection::search(static::$arangodb->session, sprintf(
<<<AQL
FOR document IN %s
FILTER document._from == '%s' && document._to == '%s'
LIMIT 1
RETURN document
AQL,
session::COLLECTION . '_edge_' . self::COLLECTION,
$session,
$account
)) instanceof _document
|| document::write(static::$arangodb->session, session::COLLECTION . '_edge_' . self::COLLECTION, [
'_from' => $session,
'_to' => $account
])
) {
// Найдено, либо создано ребро: session -> account
// Возврат (успех)
return true;
} else throw new exception('Не удалось создать ребро: session -> account');
} else throw new exception('Не удалось инициализировать коллекции');
} catch (exception $e) {
// Запись в реестр ошибок
$errors['account'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Возврат (провал)
return false;
}
/**
* Записать
*

View File

@ -35,7 +35,7 @@ class core extends model
/**
* Путь до файла с настройками подключения к базе данных ArangoDB
*/
final public const ARANGODB = '../settings/arangodb.php';
final public const ARANGODB = __DIR__ . '/../settings/arangodb.php';
/**
* Соединение с базой данных ArangoDB
@ -132,6 +132,69 @@ class core extends model
return null;
}
/**
* Collect from ArangoDB
*
* @param string $filter Выражения для фильтрации на языке AQL
* @param string $sort Выражение для сортировки на языке AQL
* @param int $amount Количество документов для выборки
* @param int $page Страница
* @param string $index Параметр по которому будет производиться сборка
* @param string $return Выражение описываемое возвращаемые данные на языке AQL
* @param array &$errors Реестр ошибок
*
* @return _document|array|null Массив инстанций документов в базе данных, если найдены
*/
public static function collect(
string $filter = '',
string $sort = 'd.created DESC, d._key DESC',
int $amount = 1,
int $page = 1,
string $index = 'd.updated',
string $return = 'd',
array &$errors = []
): _document|array|null {
try {
if (collection::init(static::$arangodb->session, static::COLLECTION)) {
// Инициализирована коллекция
// Exit (success)
return collection::search(
static::$arangodb->session,
sprintf(
<<<'AQL'
FOR d IN %s
%s
%s
LIMIT %d, %d
COLLECT index = %s INTO group = %s
RETURN { [index]: group }
AQL,
static::COLLECTION,
empty($filter) ? '' : "FILTER $filter",
empty($sort) ? '' : "SORT $sort",
--$page <= 0 ? 0 : $amount * $page,
$amount,
$index,
$return
)
);
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в реестр ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Count documents in ArangoDB
*
@ -140,7 +203,8 @@ class core extends model
*
* @return int|null Количество документов в базе данных, если найдены
*/
public static function count(?string $collection = null, array &$errors = []): int|null {
public static function count(?string $collection = null, array &$errors = []): int|null
{
try {
if (collection::init(static::$arangodb->session, static::COLLECTION)) {
// Инициализирована коллекция

View File

@ -0,0 +1,685 @@
<?php
declare(strict_types=1);
namespace mirzaev\ebala\models;
// Файлы проекта
use mirzaev\ebala\models\traits\status;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Фреймворк для работы с таблицами
use PhpOffice\PhpSpreadsheet\IOFactory,
PhpOffice\PhpSpreadsheet\Style\Color,
PhpOffice\PhpSpreadsheet\Style\Fill,
PhpOffice\PhpSpreadsheet\Style\Conditional,
PhpOffice\PhpSpreadsheet\Style\Alignment,
PhpOffice\PhpSpreadsheet\Spreadsheet;
// System libraries
use exception;
/**
* Модель выплат
*
* @package mirzaev\ebala\models
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class payments extends core
{
use status;
/**
* Сотрудники
*
* Расчитать стоимость работы сотрудников за выбранный период и сгенерировать excel-документ
*
* @param int $from Начальная дата для выборки заявок (unixtime)
* @param int $to Конечная дата для выборки заявок (unixtime)
* @param array $errors Errors registry
*
* @return bool Записан буфер вывода сгенерированный excel-документ?
*/
public static function workers(int $from, int $to, array &$errors = []): bool
{
try {
// Чтение заявок
$tasks = @task::read("d.date >= $from && d.date <= $to && d.problematic == false && d.completed == true && d.result.processed != true", amount: 999999, return: '{worker: d.worker, market: d.market, date: d.date, work: d.work, start: d.start, end: d.end, commentary: d.commentary, rating: d.rating, review: d.review}', errors: $errors);
if (is_array($tasks) && count($tasks) > 0) {
// Найдены заявки
// Инициализация таблицы
$spreadsheet = new Spreadsheet();
// Конвертация unixtime в читаемую дату
$_from = gmdate("d.m.Y", $from);
$_to = gmdate("d.m.Y", $to);
// Запись настроек таблицы
$spreadsheet
->getProperties()
->setCreator('Спецресурс')
->setLastModifiedBy('Спецресурс')
->setTitle("$_from - $_to")
->setSubject("Зарплаты сотрудникам $_from - $_to")
->setDescription("Зарплаты сотрудникам за период с $_from по $_to")
->setKeywords('зарплата сотрудники');
// Открытие страницы
$spreadsheet->setActiveSheetIndex(0);
// Запись первой строки (названия колонок)
$spreadsheet
->getActiveSheet()
->setCellValue('A1', 'Адрес')
->setCellValue('B1', 'Дата выплаты')
->setCellValue('C1', 'Дата заявки')
->setCellValue('D1', 'Магазин')
->setCellValue('E1', 'Сотрудник')
->setCellValue('F1', 'Работа')
->setCellValue('G1', 'Начало')
->setCellValue('H1', 'Конец')
->setCellValue('I1', 'Часы')
->setCellValue('J1', 'Статус')
->setCellValue('K1', 'Рейтинг')
->setCellValue('L1', 'Отзыв')
->setCellValue('M1', 'ФИО')
->setCellValue('N1', 'Час')
->setCellValue('O1', 'Смена')
->setCellValue('P1', 'Штраф')
->setCellValue('Q1', 'Премия')
->setCellValue('R1', 'Полная оплата')
->setCellValue('S1', 'Наличными')
->setCellValue('T1', 'Наличные?')
->setCellValue('U1', 'Переводом')
->setCellValue('V1', 'Реквизиты')
->setCellValue('W1', 'Тариф')
->setCellValue('X1', 'Без НДС')
->setCellValue('Y1', 'Прибыль')
->setCellValue('Z1', 'Примечание')
->setCellValue('AA1', 'Долг сотрудника')
->setCellValue('AB1', 'Кто платит')
->setCellValue('AC1', 'Кто платит');
// Запись цвета верхнего колонтинула
$spreadsheet
->getActiveSheet()
->getStyle('A1:AC1')
->getFill()
->setFillType(Fill::FILL_SOLID)
->getStartColor()
->setARGB('ffffffb9');
// Запись толщины текста верхнего колонтинула
$spreadsheet
->getActiveSheet()
->getStyle('A1:AC1')
->getFont()
->setBold(true);
// Запись размера текста верхнего колонтинула
$spreadsheet
->getActiveSheet()
->getStyle('A1:AC1')
->getFont()
->setSize(13);
// Запись позиции текста верхнего колонтинула
$spreadsheet
->getActiveSheet()
->getStyle('A1:AC1')
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_CENTER)
->setVertical(Alignment::VERTICAL_CENTER);
// Запись ширины строки верхнего колонтинула
$spreadsheet
->getActiveSheet()
->getRowDimension(1)
->setRowHeight(24);
// Запись ширины колонок
$spreadsheet->getActiveSheet()->getColumnDimension('A')->setWidth(30);
$spreadsheet->getActiveSheet()->getColumnDimension('B')->setWidth(18);
$spreadsheet->getActiveSheet()->getColumnDimension('C')->setWidth(18);
$spreadsheet->getActiveSheet()->getColumnDimension('D')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('E')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('F')->setWidth(22);
$spreadsheet->getActiveSheet()->getColumnDimension('G')->setWidth(12);
$spreadsheet->getActiveSheet()->getColumnDimension('H')->setWidth(12);
$spreadsheet->getActiveSheet()->getColumnDimension('I')->setWidth(12);
$spreadsheet->getActiveSheet()->getColumnDimension('J')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('K')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('L')->setWidth(40);
$spreadsheet->getActiveSheet()->getColumnDimension('M')->setWidth(32);
$spreadsheet->getActiveSheet()->getColumnDimension('N')->setWidth(12);
$spreadsheet->getActiveSheet()->getColumnDimension('O')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('P')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('Q')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('R')->setWidth(16);
$spreadsheet->getActiveSheet()->getColumnDimension('S')->setWidth(16);
$spreadsheet->getActiveSheet()->getColumnDimension('T')->setWidth(22);
$spreadsheet->getActiveSheet()->getColumnDimension('U')->setWidth(22);
$spreadsheet->getActiveSheet()->getColumnDimension('V')->setWidth(80);
$spreadsheet->getActiveSheet()->getColumnDimension('W')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('X')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('Y')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('Z')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('AA')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('AB')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('AC')->setWidth(14);
// Инициализация счётчика строк
$row = 2;
foreach ($tasks as $task) {
// Перебор заявок
// Инициализация сотрудника
$worker = worker::read('d.id == "' . $task->worker . '"');
if ($worker instanceof _document) {
// Найден сотрудник
// Инициализация магазина
$market = market::read('d.id == "' . $task->market . '"');
if ($market instanceof _document) {
// Найден магазин
// Запись строки
$spreadsheet
->getActiveSheet()
->setCellValue("A$row", $market->city . ' ' . $market->address)
->setCellValue("B$row", '')
->setCellValue("C$row", gmdate("d.m.Y", $task->date))
->setCellValue("D$row", $market->id)
->setCellValue("E$row", $worker->id)
->setCellValue("F$row", $task->work)
->setCellValue("G$row", $task->start)
->setCellValue("H$row", $task->end)
->setCellValue("I$row", $hours = task::hours($task->start, $task->end, $errors))
->setCellValue("J$row", '')
->setCellValue("K$row", $task->rating ?? 'Отсутствует')
->setCellValue("L$row", $task->review ?? '')
->setCellValue("M$row", $worker->name['second'] . ' ' . $worker->name['first'] . ' ' . $worker->name['last'])
->setCellValue("N$row", $hour = static::hour('worker', $market->city, $task->work))
->setCellValue("O$row", $payment = $hour * $hours)
->setCellValue("P$row", ($penalty = static::penalty($task->rating ?? null)) === null ? -$payment : $penalty)
->setCellValue("Q$row", $bonus = static::bonus($task->rating ?? null))
->setCellValue("R$row", $payment + ($penalty === null ? -$payment : $penalty) + $bonus)
->setCellValue("S$row", '')
->setCellValue("T$row", $worker->payment) // Наличные?
->setCellValue("U$row", '')
->setCellValue("V$row", $worker->requisites)
->setCellValue("W$row", '')
->setCellValue("X$row", '')
->setCellValue("Y$row", '')
->setCellValue("Z$row", '')
->setCellValue("AA$row", '')
->setCellValue("AB$row", '')
->setCellValue("AC$row", '');
// Инкрементация счётчика для генерации следующей строки
++$row;
}
}
}
// Write to output buffer
IOFactory::createWriter($spreadsheet, 'Xlsx')->save('php://output');
// Exit (success)
return true;
}
throw new exception('Не найдены заявки');
} catch (exception $e) {
// Write to the errors registry
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return false;
}
/**
* Магазины
*
* Расчитать прибыль с магазинов и сгенерировать excel-документ
*
* @param int $from Начальная дата для выборки заявок (unixtime)
* @param int $to Конечная дата для выборки заявок (unixtime)
* @param array $errors Errors registry
*
* @return bool Записан буфер вывода сгенерированный excel-документ?
*/
public static function markets(int $from, int $to, array &$errors = []): bool
{
try {
// Чтение заявок
$tasks = @task::collect(
"d.date >= $from && d.date <= $to && d.problematic == false && d.completed == true",
sort: 'd.date DESC',
amount: 999999,
index: 'd.date',
return: '{worker: d.worker, market: d.market, date: d.date, work: d.work, start: d.start, end: d.end, commentary: d.commentary, rating: d.rating, review: d.review}',
errors: $errors
);
// Универсализация
if ($tasks instanceof _document) $tasks = [$tasks];
// Инициализация буфера объединённых заявок по дате (подразумеваются дни)
$merged = [];
foreach ($tasks as $groups) {
// Перебор групп заявок разделённых по датам
foreach ($groups->getAll() as $date => $_tasks) {
// Перебор дат (подразумевается только одна)
foreach ($_tasks as $task) {
// Перебор заявок
// Первичная инициализация данных в буфере объединённых заявок по дням
$merged[$task['market']] ??= [];
$merged[$task['market']][$date] ??= [];
$merged[$task['market']][$date][$task['work']] ??= ['workers' => 0, 'hours' => 0];
// Запись в буфер объединённых заявок по дням
$merged[$task['market']][$date][$task['work']]['workers']++;
$merged[$task['market']][$date][$task['work']]['hours'] += task::hours($task['start'], $task['end'], $errors);
}
}
}
if (count($merged) > 0) {
// Найдены сгенерированные данные
// Инициализация таблицы
$spreadsheet = new Spreadsheet();
// Конвертация unixtime в читаемую дату
$_from = gmdate("d.m.Y", $from);
$_to = gmdate("d.m.Y", $to);
// Запись настроек таблицы
$spreadsheet
->getProperties()
->setCreator('Спецресурс')
->setLastModifiedBy('Спецресурс')
->setTitle("$_from - $_to")
->setSubject(" $_from - $_to")
->setDescription(" за период с $_from по $_to")
->setKeywords('магазины');
// Открытие страницы
$spreadsheet->setActiveSheetIndex(0);
// Запись первых строк
$spreadsheet
->getActiveSheet()
->setCellValue('A1', 'К Договору от 0.0.20')
->setCellValue('A2', 'К Договору от 0.0.20')
->setCellValue('A4', 'Детализация выполненных заказов за период')
->setCellValue('A5', "Период: $_from - $_to")
->setCellValue('A6', "Заказчик: ")
->setCellValue('A8', "Магазин")
->setCellValue('B8', "Тип")
->setCellValue('C8', "Адрес")
->setCellValue('D8', "Дата")
->setCellValue('E8', "Работа")
->setCellValue('F8', "Сотрудники")
->setCellValue('G8', "Часы")
->setCellValue('H8', "Тариф")
->setCellValue('I8', "Без НДС")
->setCellValue('J8', "С НДС");
// Запись ширины колонок
$spreadsheet->getActiveSheet()->getColumnDimension('A')->setWidth(12);
$spreadsheet->getActiveSheet()->getColumnDimension('B')->setWidth(16);
$spreadsheet->getActiveSheet()->getColumnDimension('C')->setWidth(32);
$spreadsheet->getActiveSheet()->getColumnDimension('D')->setWidth(16);
$spreadsheet->getActiveSheet()->getColumnDimension('E')->setWidth(16);
$spreadsheet->getActiveSheet()->getColumnDimension('F')->setWidth(16);
$spreadsheet->getActiveSheet()->getColumnDimension('G')->setWidth(12);
$spreadsheet->getActiveSheet()->getColumnDimension('H')->setWidth(12);
$spreadsheet->getActiveSheet()->getColumnDimension('I')->setWidth(18);
$spreadsheet->getActiveSheet()->getColumnDimension('J')->setWidth(18);
// Фиксация верхнего колонтинула
$spreadsheet
->getActiveSheet()
->freezePane('K9');
// Объединение ячеек
$spreadsheet->getActiveSheet()->mergeCells('A1:J1');
$spreadsheet->getActiveSheet()->mergeCells('A2:J2');
$spreadsheet->getActiveSheet()->mergeCells('A4:J4');
$spreadsheet->getActiveSheet()->mergeCells('A5:J5');
$spreadsheet->getActiveSheet()->mergeCells('A6:J6');
// Запись позиций текстов "к договору"
$spreadsheet
->getActiveSheet()
->getStyle('A1:J2')
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_RIGHT);
// Запись позиций текста заголовка
$spreadsheet
->getActiveSheet()
->getStyle('A4:J4')
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_CENTER)
->setVertical(Alignment::VERTICAL_CENTER);
// Запись позиций текста верхнего колонтинула
$spreadsheet
->getActiveSheet()
->getStyle('A8:J8')
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_CENTER)
->setVertical(Alignment::VERTICAL_CENTER);
// Запись цвета верхнего колонтинула (левая половина)
$spreadsheet
->getActiveSheet()
->getStyle('A8:D8')
->getFill()
->setFillType(Fill::FILL_SOLID)
->getStartColor()
->setARGB('ffdfe4ec');
// Запись цвета верхнего колонтинула (правая половина)
$spreadsheet
->getActiveSheet()
->getStyle('E8:J8')
->getFill()
->setFillType(Fill::FILL_SOLID)
->getStartColor()
->setARGB('ff8093b3');
// Запись размера текста верхнего колонтинула
$spreadsheet
->getActiveSheet()
->getStyle('A8:J8')
->getFont()
->setSize(12);
// Запись толщины текста верхнего колонтинула
$spreadsheet
->getActiveSheet()
->getStyle('A8:J8')
->getFont()
->setBold(true);
// Запись ширины строки верхнего колонтинула
$spreadsheet
->getActiveSheet()
->getRowDimension(8)
->setRowHeight(32);
// Инициализация счётчика строк
$row = 9;
// Инициализация буфера объединённых данных всех магазинов
$total = [
'workers' => 0,
'hours' => 0,
'hour' => [],
'payment' => 0,
'vat' => 0
];
foreach ($merged as $id => $dates) {
// Перебор магазинов
// Инициализация магазина
$market = market::read('d.id == "' . $id . '"');
if ($market instanceof _document) {
// Найден магазин
// Инициализация буфера объединённых данных магазина
$result = [
'workers' => 0,
'hours' => 0,
'hour' => [],
'payment' => 0,
'vat' => 0
];
foreach ($dates as $date => $works) {
// Перебор дат заявок
foreach ($works as $work => $task) {
// Перебор заявок
// Запись строки с заявками по дате
$spreadsheet
->setActiveSheetIndex(0)
->setCellValue("A$row", $id)
->setCellValue("B$row", $market->type)
->setCellValue("C$row", $market->address)
->setCellValue("D$row", gmdate("d.m.Y", $date))
->setCellValue("E$row", $work)
->setCellValue("F$row", $task['workers'])
->setCellValue("G$row", $task['hours'])
->setCellValue("H$row", $hour = static::hour('market', $market->city, $work))
->setCellValue("I$row", $payment = $hour * $task['hours'])
->setCellValue("J$row", $payment);
// Запись в буфер объединённых данных магазина
$result['workers'] += $task['workers'];
$result['hours'] += $task['hours'];
$result['hour'][] = $hour;
$result['payment'] += $payment;
$result['vat'] += $payment;
// Инкрементация счётчика для генерации следующей строки
++$row;
}
} // Чтение заявок
$tasks = @task::collect(
"d.date >= $from && d.date <= $to && d.problematic == false && d.completed == true",
sort: 'd.date DESC',
amount: 999999,
index: 'd.date',
return: '{worker: d.worker, market: d.market, date: d.date, work: d.work, start: d.start, end: d.end, commentary: d.commentary, rating: d.rating, review: d.review}',
errors: $errors
);
// Запись строки с общими данными магазина
$spreadsheet
->setActiveSheetIndex(0)
->setCellValue("A$row", "Всего ($id)")
->setCellValue("B$row", '')
->setCellValue("C$row", '')
->setCellValue("D$row", '')
->setCellValue("E$row", '')
->setCellValue("F$row", $result['workers'])
->setCellValue("G$row", $result['hours'])
->setCellValue("H$row", $hour = array_sum($result['hour']) / count($result['hour']))
->setCellValue("I$row", $result['payment'])
->setCellValue("J$row", $result['vat']);
// Запись в буфер объединённых данных всех магазинов
$total['workers'] += $result['workers'];
$total['hours'] += $result['hours'];
$total['hour'][] = $hour;
$total['payment'] += $result['payment'];
$total['vat'] += $result['vat'];
// Запись цвета строки с общими данными магазина
$spreadsheet
->getActiveSheet()
->getStyle("A$row:J$row")
->getFill()
->setFillType(Fill::FILL_SOLID)
->getStartColor()
->setARGB('ffdfe4ec');
++$row;
}
}
// Запись строки с общими данными всех магазинов
$spreadsheet
->setActiveSheetIndex(0)
->setCellValue("A$row", "Итого")
->setCellValue("B$row", '')
->setCellValue("C$row", '')
->setCellValue("D$row", '')
->setCellValue("E$row", '')
->setCellValue("F$row", $total['workers'])
->setCellValue("G$row", $total['hours'])
->setCellValue("H$row", array_sum($total['hour']) / count($total['hour']))
->setCellValue("I$row", $total['payment'])
->setCellValue("J$row", $total['vat']);
// Запись цвета строки с общими данными всех магазинов
$spreadsheet
->getActiveSheet()
->getStyle("A$row:J$row")
->getFill()
->setFillType(Fill::FILL_SOLID)
->getStartColor()
->setARGB('ffdfe4ec');
// Запись жирного текста для строки с общими данными всех магазинов
$spreadsheet
->getActiveSheet()
->getStyle("A$row:J$row")
->getFont()
->setBold(true);
// Write to output buffer
IOFactory::createWriter($spreadsheet, 'Xlsx')->save('php://output');
// Exit (success)
return true;
}
throw new exception('Не найдены заявки');
} catch (exception $e) {
// Write to the errors registry
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return false;
}
/**
* Подтвердить обработку
*
* Отметить в базе данных то, что выбранные заявки были обработаны
*
* @param int $from Начальная дата для выборки заявок (unixtime)
* @param int $to Конечная дата для выборки заявок (unixtime)
* @param string $type Тип документа для подтверждения (workers, markets)
* @param array $errors Errors registry
*
* @return void
*/
public static function confirm(int $from, int $to, string $type, array &$errors = []): bool
{
try {
// Чтение заявок
$tasks = @task::read("d.date >= $from && d.date <= $to && d.problematic == false && d.completed == true && d.result.processed != true", amount: 999999, errors: $errors);
if (is_array($tasks) && count($tasks) > 0) {
// Найдены заявки
if ($type === 'workers') {
// Подтверждена обработка зарплат сотрудников за выбранный период
foreach ($tasks as $task) {
// Перебор заявок
// Подтверждение того, что заявка обработана (выплачены деньги сотруднику)
$task->result = ['processed' => true] + ($task->result ?? []);
// Запись обновления в базу данных
core::update($task);
}
}
// Exit (success)
return true;
}
throw new exception('Не найдены заявки');
} catch (exception $e) {
// Write to the errors registry
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return false;
}
/**
* Determine tariff
*
* @param string $type Type of tariffs (market, worker)
* @param string $city City in which the place of work is located
* @param string $work Type of work
*
* @return int|float Cost of work per hour (rubles)
*/
public static function hour(string $type, string $city, string $work): int|float
{
return
match (mb_strtolower($type)) {
'market', 'магазин' => settings::read("d.category == 'market_hour' && d.city == '$city' && d.work == '$work'")?->value ?? 0,
'worker', 'сотрудник' => settings::read("d.category == 'worker_hour' && d.city == '$city' && d.work == '$work'")?->value ?? 0,
default => 0
};
}
/**
* Bonus on task
*
* @param int $rating Rating of the task from the market
*
* @return int Bonus (rubles)
*/
public static function bonus(int $rating): int
{
return settings::read("d.category == 'worker_bonus' && d.rating == $rating")?->value ?? 0;
}
/**
* Penalty on task
*
* @param int $rating Rating of the task from the market
*
* @return int|null Penalty (rubles) (null - all payment)
*/
public static function penalty(int $rating): ?int
{
$penalty = settings::read("d.category == 'worker_penalty' && d.rating == $rating")?->value ?? 0;
return $penalty === 1 ? null : $penalty;
}
}

View File

@ -36,7 +36,9 @@ final class registry extends core
* @param ?string $after Injection of AQL-code after search of edges
* @param int $amount Amount of workers
* @param int $page Offset by amount
* @param string $sort Sort
* @param string $target Collection or view name
* @param string $return Data for return
* @param array $binds Binds for query
* @param array $errors Errors registry
*
@ -49,6 +51,7 @@ final class registry extends core
int $page = 1,
string $sort = 'account.created DESC, account._key DESC',
string $target = account::COLLECTION,
string $return = '{account, worker}',
array $binds = [],
array &$errors = []
): array {
@ -69,14 +72,15 @@ final class registry extends core
%s
SORT %s
LIMIT %d, %d
RETURN {account, worker}
RETURN %s
AQL,
$target,
$before,
$after,
$sort,
--$page <= 0 ? 0 : $amount * $page,
$amount
$amount,
$return
), $binds);
// Exit (success)
@ -104,6 +108,7 @@ final class registry extends core
* @param int $amount Amount of markets
* @param int $page Offset by amount
* @param string $target Collection or view name
* @param string $return Data for return
* @param array $binds Binds for query
* @param array $errors Errors registry
*
@ -116,6 +121,7 @@ final class registry extends core
int $page = 1,
string $sort = 'account.created DESC, account._key DESC',
string $target = account::COLLECTION,
string $return = '{account, market}',
array $binds = [],
array &$errors = []
): array {
@ -136,14 +142,15 @@ final class registry extends core
%s
SORT %s
LIMIT %d, %d
RETURN {account, market}
RETURN %s
AQL,
$target,
$before,
$after,
$sort,
--$page <= 0 ? 0 : $amount * $page,
$amount
$amount,
$return
), $binds);
// Exit (success)
@ -167,9 +174,11 @@ final class registry extends core
* Generate operators list
*
* @param ?string $before Injection of AQL-code before search of edges
* @param ?string $after Injection of AQL-code after search of edges
* @param int $amount Amount of operators
* @param int $page Offset by amount
* @param string $target Collection or view name
* @param string $return Data for return
* @param array $binds Binds for query
* @param array $errors Errors registry
*
@ -177,10 +186,12 @@ final class registry extends core
*/
public static function operators(
?string $before = '',
?string $after = '',
int $amount = 100,
int $page = 1,
string $sort = 'account.created DESC, account._key DESC',
string $target = account::COLLECTION,
string $return = '{account}',
array $binds = [],
array &$errors = []
): array {
@ -192,16 +203,19 @@ final class registry extends core
$operators = collection::search(static::$arangodb->session, sprintf(
<<<AQL
FOR account IN %s
%s
%s
SORT %s
LIMIT %d, %d
RETURN {account}
RETURN %s
AQL,
$target,
$before,
$after,
$sort,
--$page <= 0 ? 0 : $amount * $page,
$amount
$amount,
$return
), $binds);
// Exit (success)
@ -225,9 +239,11 @@ final class registry extends core
* Generate administrators list
*
* @param ?string $before Injection of AQL-code before search of edges
* @param ?string $after Injection of AQL-code after search of edges
* @param int $amount Amount of administrators
* @param int $page Offset by amount
* @param string $target Collection or view name
* @param string $return Data for return
* @param array $binds Binds for query
* @param array $errors Errors registry
*
@ -235,10 +251,12 @@ final class registry extends core
*/
public static function administrators(
?string $before = '',
?string $after = '',
int $amount = 100,
int $page = 1,
string $sort = 'account.created DESC, account._key DESC',
string $target = account::COLLECTION,
string $return = '{account}',
array $binds = [],
array &$errors = []
): array {
@ -250,16 +268,19 @@ final class registry extends core
$administrators = collection::search(static::$arangodb->session, sprintf(
<<<AQL
FOR account IN %s
%s
%s
SORT %s
LIMIT %d, %d
RETURN {account}
RETURN %s
AQL,
$target,
$before,
$after,
$sort,
--$page <= 0 ? 0 : $amount * $page,
$amount
$amount,
$return
), $binds);
// Exit (success)
@ -273,7 +294,6 @@ final class registry extends core
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
var_dump($errors);
}
// Exit (fail)

View File

@ -164,67 +164,12 @@ final class session extends core
// Закрыть сессию
}
/**
* Инициализировать связь сессии с аккаунтом
*
* Ищет связь сессии с аккаунтом, если не находит, то создаёт её
*
* @param account $account Инстанция аккаунта
* @param array &$errors Реестр ошибок
*
* @return bool Связана сессия с аккаунтом?
*/
public static function connect(string $session, string $account, array &$errors = []): bool
{
try {
if (
collection::init(static::$arangodb->session, self::COLLECTION)
&& collection::init(static::$arangodb->session, account::COLLECTION)
&& collection::init(static::$arangodb->session, self::COLLECTION . '_edge_' . account::COLLECTION, true)
) {
// Инициализированы коллекции
if (
collection::search(static::$arangodb->session, sprintf(
<<<AQL
FOR document IN %s
FILTER document._from == '%s' && document._to == '%s'
LIMIT 1
RETURN document
AQL,
self::COLLECTION . '_edge_' . account::COLLECTION,
$session,
$account
)) instanceof _document
|| document::write(static::$arangodb->session, self::COLLECTION . '_edge_' . account::COLLECTION, [
'_from' => $session,
'_to' => $account
])
) {
// Найдено, либо создано ребро: session -> account
return true;
} else throw new exception('Не удалось создать ребро: session -> account');
} else throw new exception('Не удалось инициализировать коллекции');
} catch (exception $e) {
// Запись в реестр ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return false;
}
/**
* Найти связанный аккаунт
*
* @param array &$errors Реестр ошибок
*
* @return ?account Инстанция аккаунта, если удалось найти
* @return account|null Инстанция аккаунта, если удалось найти
*/
public function account(array &$errors = []): ?account
{
@ -273,6 +218,7 @@ final class session extends core
];
}
// Возврат (провал)
return null;
}

View File

@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace mirzaev\ebala\models;
// Project files
use mirzaev\ebala\models\traits\instance,
mirzaev\ebala\models\traits\status,
mirzaev\ebala\models\account,
mirzaev\ebala\models\worker,
mirzaev\ebala\models\market;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Фреймворк ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Встроенные библиотеки
use exception;
/**
* Модель настроек
*
* Управляет записью и чтением настроек сайта из ArangoDB
*
* @package mirzaev\ebala\models
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class settings extends core
{
use instance, status;
/**
* Collection name in ArangoDB
*/
final public const COLLECTION = 'settings';
/**
* Инстанция документа в базе данных
*/
protected readonly _document $document;
/**
* Read (search)
*
* @param ?string $before Injection of AQL-code before search of edges
* @param int $amount Amount of administrators
* @param int $page Offset by amount
* @param string $target Collection or view name
* @param array $binds Binds for query
* @param array $errors Errors registry
*
* @return array Instances from ArangoDB
*/
public static function search(
?string $before = '',
int $amount = 1000,
int $page = 1,
string $sort = 'setting.category ASC, setting.city ASC, setting.work ASC, setting.rating DESC, setting.created DESC, setting._key DESC',
string $target = settings::COLLECTION,
array $binds = [],
array &$errors = []
): array {
try {
if (collection::init(static::$arangodb->session, settings::COLLECTION)) {
// Инициализирована коллекция
// Search the session data in ArangoDB
$settings = collection::search(static::$arangodb->session, sprintf(
<<<AQL
FOR setting IN %s
%s
SORT %s
LIMIT %d, %d
LET d = setting.category
COLLECT x = setting.category INTO groups
RETURN { [x]: groups[*]['setting'] }
AQL,
$target,
$before,
$sort,
--$page <= 0 ? 0 : $amount * $page,
$amount
), $binds);
// Универсализация значений
$buffer = [];
foreach (is_array($settings) ? $settings : [$settings] as $setting) foreach ($setting->getAll() ?? [] as $category => $data) $buffer[$category] = $data;
$settings = $buffer;
// Exit (success)
return $settings;
} else throw new exception('Не удалось инициализировать коллекции');
} catch (exception $e) {
// Write to the errors registry
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
var_dump($errors);
}
// Exit (fail)
return [];
}
}

View File

@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace mirzaev\ebala\models;
// Project files
use mirzaev\ebala\models\traits\instance,
mirzaev\ebala\models\traits\status,
mirzaev\ebala\models\account,
mirzaev\ebala\models\worker,
mirzaev\ebala\models\market;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Фреймворк ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Встроенные библиотеки
use exception;
/**
* Модель сокета
*
* Управляет записью и чтением настроек сайта из ArangoDB
*
* @package mirzaev\ebala\models
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class socket extends core
{
use instance, status;
/**
* Collection name in ArangoDB
*/
final public const COLLECTION = 'socket';
/**
* Инстанция документа в базе данных
*/
protected readonly _document $document;
/**
* Подключение к сессии
*
* @param string $session Идентификатор сессии
* @param string $key Ключ для регистрации (из документа в static::COLLECTION)
* @param array &$errors Реестр ошибок
*
* @return bool Сокет подключен к сессии?
*/
public static function session(string $session, string $key, array &$errors = []): bool
{
try {
if (
collection::init(static::$arangodb->session, self::COLLECTION)
&& collection::init(static::$arangodb->session, session::COLLECTION)
&& collection::init(static::$arangodb->session, $edge = session::COLLECTION . '_edge_' . self::COLLECTION, true)
) {
// Инициализирована коллекция
// Чтение сокета
$socket = self::read(
filter: "d.key == \"$key\" && d.expires > DATE_NOW() / 1000",
sort: 'd.created desc, d.expires desc',
amount: 1,
);
if ($socket instanceof _document) {
// Найден сокет
if (document::write(static::$arangodb->session, $edge, [
'_from' => $session,
'_to' => $socket->getId()
])) {
// Записано ребро: СЕССИЯ -> СОКЕТ
// Возврат (успех)
return true;
} else throw new exception('Не удалось записать изменения в базу данных');
} else throw new exception('Не удалось найти сокет');
} else throw new exception('Не удалось инициализировать коллекции');
} catch (exception $e) {
// Write to the errors registry
$errors['socket'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Возврат (провал)
return false;
}
/**
* Найти аккаунт
*
* Ищет аккаунт по идентификатору подключения к сокету
*
* @param int $socket Идентификатор подключения к сокету (во внутренней базе данных OpenSwoole)
* @param array &$errors Реестр ошибок
*
* @returnaccount|null Аккаунт, если найден
*/
public static function account(int $socket, array &$errors = []): ?account
{
try {
if (
collection::init(static::$arangodb->session, self::COLLECTION)
&& collection::init(static::$arangodb->session, session::COLLECTION)
&& collection::init(static::$arangodb->session, account::COLLECTION)
&& collection::init(static::$arangodb->session, session::COLLECTION . '_edge_' . self::COLLECTION, true)
&& collection::init(static::$arangodb->session, session::COLLECTION . '_edge_' . account::COLLECTION, true)
) {
// Инициализированы коллекции
// Инициализация инстанции аккаунта
$account = new account;
// Поиск инстанции аккаунта в базе данных
$instance = $account->instance(collection::search(static::$arangodb->session, sprintf(
<<<AQL
FOR a IN 1 OUTBOUND
(FOR s IN 1 INBOUND
(FOR s IN %s
FILTER s.id == %s && s.expires > DATE_NOW() / 1000
SORT s.created desc, s.expires desc
LIMIT 1
RETURN s
)[0]._id
%s
RETURN s
)[0]._id
%s
RETURN a
AQL,
socket::COLLECTION,
$socket,
session::COLLECTION . '_edge_' . socket::COLLECTION,
session::COLLECTION . '_edge_' . account::COLLECTION,
)));
// Возврат (успех)
return $instance instanceof _document ? $account : throw new exception('Не удалось найти инстанцию аккаунта в базе данных');
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в реестр ошибок
$errors['socket'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Возврат (провал)
return null;
}
}

View File

@ -14,8 +14,9 @@ use mirzaev\arangodb\collection,
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Встроенные библиотеки
use exception;
// System libraries
use datetime,
exception;
/**
* Модель заданий
@ -117,6 +118,7 @@ final class task extends core
* @param int $amount Amount of tasks
* @param int $page Offset by amount
* @param string $target Collection or view name
* @param string $return Data for return
* @param array $binds Binds for query
* @param array $errors Errors registry
*
@ -132,6 +134,7 @@ final class task extends core
int $page = 1,
string $sort = 'task.date DESC, task.created DESC, task._key DESC',
string $target = self::COLLECTION,
string $return = '{task, worker, market}',
array $binds = [],
array &$errors = []
): array {
@ -153,7 +156,7 @@ final class task extends core
%s
SORT %s
LIMIT %d, %d
RETURN {task, worker, market}
RETURN %s
AQL,
$target,
$before,
@ -162,7 +165,8 @@ final class task extends core
$after,
$sort,
--$page <= 0 ? 0 : $amount * $page,
$amount
$amount,
$return
), $binds);
// Exit (success)
@ -176,14 +180,54 @@ final class task extends core
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
var_dump($errors);
}
// Exit (fail)
return [];
}
/**
* Посчитать количество часов работы
*
* @param string $start Начало работы (H:i)
* @param string $end Конец работы (H:i)
* @param array $errors Errors registry
*
* @return ?float Количество часов, если удалось расчитать
*/
public static function hours(string $start, string $end, array &$errors = []): ?float
{
try {
if (
!empty($start = datetime::createFromFormat('H:i', (string) $start)) && $start instanceof datetime
&& !empty($end = datetime::createFromFormat('H:i', (string) $end)) && $end instanceof datetime
) {
// Инициализированы $start и $end
// Расчёт часов работы
$hours = (float) $start->diff($end)->format('%R%H.%i');
if ($hours < 0) $hours += 24;
if ($hours >= 6.5 && $hours < 9) $hours -= 0.5;
else if ($hours >= 9 && $hours < 12.5) $hours -= 1;
else if ($hours >= 12.5) $hours -= 1.5;
// Выход (успех)
return $hours;
}
} catch (exception $e) {
// Write to the errors registry
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Выход (провал)
return null;
}
/**
* Generate work type label in Russian
*
@ -204,4 +248,146 @@ final class task extends core
default => $work
};
}
/**
* Create a transaction for work on a task
*
* @param string $task
* @param string $worker
* @param int $amount
* @param array $errors
*
* @return ?string Identificator of instance of ArangoDB
*/
public static function transaction(
string $task,
string $worker,
int|float $amount = 0,
array &$errors = []
): ?string {
try {
if (
collection::init(static::$arangodb->session, self::COLLECTION)
&& collection::init(static::$arangodb->session, worker::COLLECTION)
&& collection::init(static::$arangodb->session, 'transaction', true)
) {
// Инициализированы коллекции
// Запись документа в базу данны и возврат (успех)
return document::write(static::$arangodb->session, 'transaction', [
'_from' => $task,
'_to' => $worker,
'amount' => $amount,
'processed' => 0,
]);
} else throw new exception('Не удалось инициализировать коллекции');
} catch (exception $e) {
// Write to the errors registry
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Block by account
*
* @param int $task (_key)
* @param int $account (_key)
* @param array $errors
*
* @return bool Task is blocked?
*/
public static function block(
int $task,
int $account,
array &$errors = []
): bool {
try {
if (collection::init(static::$arangodb->session, self::COLLECTION)) {
// Инициализирована коллекция
if (($task = static::read('d._key == "' . $task . '"')) instanceof _document) {
// Найдена заявка
if ($task->block === null || $task->block['expires'] < time()) {
// Не заблокирована заявка
// Блокировка
$task->block = ['account' => $account, 'expires' => (int) strtotime('+1 minute')];
// Запись обновления в базу данных и возврат (успех)
return core::update($task);
} else throw new exception('Заявка уже заблокирована: ' . $task->block['account']);
} else throw new exception('Не удалось инициализировать коллекции');
} else throw new exception('Не удалось инициализировать коллекции');
} catch (exception $e) {
// Write to the errors registry
$errors['task'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return false;
}
/**
* Unblock by account
*
* @param int $task (_key)
* @param int $account (_key)
* @param array $errors
*
* @return bool Task is blocked?
*/
public static function unblock(
int $task,
int $account,
array &$errors = []
): bool {
try {
if (collection::init(static::$arangodb->session, self::COLLECTION)) {
// Инициализирована коллекция
if (($task = static::read('d._key == "' . $task . '"')) instanceof _document) {
// Найдена заявка
if ($task->block !== null) {
// Заблокирована заявка
if ($task->block['account'] === $account || $task->block['expires'] > time()) {
// Аккаунт отменяет свою блокировку или истекло время блокировки
// Разблокировка
$task->block = null;
// Запись обновления в базу данных и возврат (успех)
return core::update($task);
}
} else throw new exception('Заявка не заблокирована');
} else throw new exception('Не удалось инициализировать коллекции');
} else throw new exception('Не удалось инициализировать коллекции');
} catch (exception $e) {
// Write to the errors registry
$errors['task'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return false;
}
}

View File

@ -0,0 +1,35 @@
@charset "UTF-8";
#connection {
z-index: 999999;
position: fixed;
bottom: 20px;
left: 20px;
height: 16px;
display: flex;
}
#connection > i#indicator {
width: 16px;
height: 16px;
display: block;
cursor: help;
border-radius: 100%;
}
#connection > small {
margin-left: 7px;
height: 16px;
display: flex;
align-items: center;
font-weight: bold;
color: var(--socket-text);
}
#connection > i#indicator.disconnected:not(.connected) {
background-color: var(--socket-disconnected);
}
#connection > i#indicator.connected:not(.disconnected) {
background-color: var(--socket-connected);
}

0
mirzaev/ebala/system/public/css/fonts/dejavu.css Normal file → Executable file
View File

0
mirzaev/ebala/system/public/css/fonts/fira.css Normal file → Executable file
View File

0
mirzaev/ebala/system/public/css/fonts/hack.css Normal file → Executable file
View File

0
mirzaev/ebala/system/public/css/icons/home.css Normal file → Executable file
View File

View File

0
mirzaev/ebala/system/public/css/icons/smartphone.css Normal file → Executable file
View File

0
mirzaev/ebala/system/public/css/icons/timer.css Normal file → Executable file
View File

0
mirzaev/ebala/system/public/css/icons/user.css Normal file → Executable file
View File

0
mirzaev/ebala/system/public/css/icons/work_alt.css Normal file → Executable file
View File

View File

@ -17,34 +17,40 @@ section.panel.list.medium {
width: 80%;
}
section.panel.list > :is(form, search).row.menu {
margin-bottom: 10px;%s"
section.panel.list> :is(form, search).row.menu {
margin-bottom: 10px;
transition: 0s;
}
section.panel.list > :is(form, search).row.menu > label {
section.panel.list> :is(form, search).row.menu>label {
height: max-content;
min-height: 30px;
display: flex;
}
section.panel.list > :is(form, search).row.menu > label:not(.solid) {
section.panel.list> :is(form, search).row.menu>label:not(.solid) {
gap: 15px;
}
section.panel.list > :is(form, search).row.menu.wide > label {
section.panel.list> :is(form, search).row.menu.wide>label {
height: 36px;
}
section.panel.list > :is(form, search).row.menu.separated {
section.panel.list> :is(form, search).row.menu.separated {
margin-bottom: 20px;
}
div#popup > section.list > div.row.endless {
div#popup>section.list>div.row.endless {
height: auto;
}
section.panel.list > :is(form, search).row.menu > label > button {
section.panel.list> :is(form, search).row.menu>label>div:has(>button) {
position: relative;
display: flex;
height: 30px;
}
section.panel.list> :is(form, search).row.menu>label>button {
position: relative;
display: flex;
justify-content: center;
@ -52,14 +58,11 @@ section.panel.list > :is(form, search).row.menu > label > button {
height: 30px;
}
section.panel.list > :is(form, search).row.menu > label > button.separated {
section.panel.list> :is(form, search).row.menu>label>button.separated {
margin-left: 7px;
}
section.panel.list
> :is(form, search).row.menu
> label
> button.separated:before {
section.panel.list> :is(form, search).row.menu>label>button.separated:before {
content: "";
left: -12px;
position: absolute;
@ -68,71 +71,65 @@ section.panel.list
border-left: 2px solid var(--earth-above);
}
section.panel.list > :is(form, search).row.menu.stretched > label > button,
section.panel.list
> :is(form, search).row.menu.stretched
> label
> input[type="search"] {
section.panel.list> :is(form, search).row.menu.stretched>label>button,
section.panel.list> :is(form, search).row.menu.stretched>label>input[type="search"] {
flex-grow: 1;
}
section.panel.list > :is(form, search).row.menu.stretched > label > button {
section.panel.list> :is(form, search).row.menu.stretched>label>button {
max-width: 250px;
}
section.panel.list > :is(form, search).row.menu > label > input {
section.panel.list> :is(form, search).row.menu>label>input {
padding: 0 10px;
}
section.panel.list > :is(form, search).row.menu > label > input:not(.merged) {
section.panel.list> :is(form, search).row.menu>label>input:not(.merged) {
border-radius: 3px;
}
section.panel.list > :is(form, search).row.menu > label > input[type="date"] {
section.panel.list> :is(form, search).row.menu>label>input[type="date"] {
width: 115px;
flex-shrink: 0;
}
section.panel.list
> :is(form, search).row.menu
> label
> input[type="search"]
+ button {
section.panel.list> :is(form, search).row.menu>label>input[type="search"]+button {
height: 100%;
padding: 0 30px;
flex-grow: 0;
}
section.panel.list > div#title {
section.panel.list>div#title {
margin-top: 20px;
height: 50px;
background-color: var(--background-below-6);
}
section.panel.list > div#title > span {
section.panel.list>div#title>span {
font-weight: unset;
font-size: unset;
color: unset;
}
section.panel.list > div.row {
section.panel.list>div.row {
--width: calc(100% - 24px);
--gap: 12px;
--background: var(--cloud);
position: relative;
left: 0px;
width: calc(100% - 24px);
height: 35px;
display: flex;
width: var(--width, calc(100% - 24px));
gap: var(--gap, 12px);
padding: 0 var(--gap, 12px);
border-radius: 0px;
}
section.panel.list > div.row:not(:nth-of-type(1)) {
section.panel.list>div.row:not(:nth-of-type(1)) {
background-color: var(--background);
}
section.panel.list > div.row:not(:nth-of-type(1)) > span {
section.panel.list>div.row:not(:nth-of-type(1))>span {
height: 100%;
line-height: 2.2;
padding: 0;
@ -142,7 +139,7 @@ section.panel.list > div.row:not(:nth-of-type(1)) > span {
-moz-box-shadow: var(--box-shadow);
}
section.panel.list > div.row:not(:nth-of-type(1)):is(:hover, :focus) {
section.panel.list>div.row:not(:nth-of-type(1)):not([data-blocked]):is(:hover, :focus) {
--padding-left: 24px;
--padding-right: 24px;
left: -12px;
@ -151,23 +148,23 @@ section.panel.list > div.row:not(:nth-of-type(1)):is(:hover, :focus) {
transition: 0s;
}
section.panel.list > div.row:first-of-type {
section.panel.list>div.row:first-of-type {
border-radius: 3px 3px 0 0;
}
section.panel.list > div.row:last-of-type {
section.panel.list>div.row:last-of-type {
border-radius: 0 0 3px 3px;
}
section.panel.list > div.row:is(:hover, :focus) * {
section.panel.list>div.row:is(:hover, :focus) * {
transition: unset;
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1) {
section.panel.list>div.row:not(:nth-of-type(1)):nth-child(2n + 1) {
--background: var(--cloud-above);
}
section.panel.list > div.row[data-selected="true"]:before {
section.panel.list>div.row[data-selected="true"]:before {
left: -25px;
top: 0.08rem;
position: absolute;
@ -186,7 +183,7 @@ section.panel.list > div.row[data-selected="true"]:before {
color: var(--interface-brown);
}
section.panel.list > div.row[data-selected="true"]:after {
section.panel.list>div.row[data-selected="true"]:after {
right: -25px;
bottom: 0.08rem;
rotate: 180deg;
@ -206,85 +203,14 @@ section.panel.list > div.row[data-selected="true"]:after {
color: var(--interface-brown);
}
section.panel.list > div.row:not(:nth-of-type(1)).confirmed {
--background: var(--grass);
}
section.panel.list
> div.row:not(:nth-of-type(1)):nth-child(2n + 1).confirmed {
--background: var(--grass-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).published {
--background: var(--river);
}
section.panel.list
> div.row:not(:nth-of-type(1)):nth-child(2n + 1).published {
--background: var(--river-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).confirmed.published:not(.problematic) {
--background: var(--sea);
}
section.panel.list
> div.row:not(:nth-of-type(1)).confirmed.published:not(.problematic):nth-child(2n + 1) {
--background: var(--sea-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).problematic {
--background: var(--clay);
}
section.panel.list
> div.row:not(:nth-of-type(1)):nth-child(2n + 1).problematic {
--background: var(--clay-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).coming {
--background: var(--magma);
}
section.panel.list
> div.row:not(:nth-of-type(1)):nth-child(2n + 1).coming {
--background: var(--magma-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).completed:not(.problematic) {
--background: var(--sand);
}
section.panel.list
> div.row:not(:nth-of-type(1)):nth-child(2n + 1).completed:not(.problematic) {
--background: var(--sand-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).passed {
filter: brightness(0.8);
}
section.panel.list > div.row:not(:nth-of-type(1)).hided * {
filter: blur(1px);
opacity: 0.3;
}
section.panel.list > div.row:not(:nth-of-type(1)).hided:is(:hover, :focus) * {
filter: unset;
opacity: unset;
}
section.panel.list > div.row.reinitialized {
section.panel.list>div.row.reinitialized {
animation-duration: 3s;
animation-name: row-reinitialized;
animation-timing-function: ease-in;
}
section.panel.list
> div.row:not(
:nth-of-type(1),
[data-selected="true"]
).reinitializable:before {
section.panel.list>div.row:not(:nth-of-type(1),
[data-selected="true"]).reinitializable:before {
content: attr(data-counter);
position: absolute;
left: -95px;
@ -297,16 +223,13 @@ section.panel.list
color: var(--earth-text);
}
section.panel.list
> div.row:not(:nth-of-type(1), [data-selected="true"]).reinitializable:is(
:hover,
:focus
):before {
section.panel.list>div.row:not(:nth-of-type(1), [data-selected="true"]).reinitializable:is(:hover,
:focus):before {
content: attr(id);
color: var(--earth-text-important-below);
}
section.panel.list > div.row > span {
section.panel.list>div.row>span {
position: relative;
margin: auto 0;
padding: 8px 0;
@ -314,52 +237,52 @@ section.panel.list > div.row > span {
transition: 0s;
}
section.panel.list > div.row:is(:hover, :focus) > span {
section.panel.list>div.row:is(:hover, :focus)>span {
transition: 0s;
}
section.panel.list > div.row > span:not(:first-child) {
section.panel.list>div.row>span:not(:first-child) {
--padding-left: calc(var(--gap) / 2);
}
section.panel.list > div.row > span:not(:last-child) {
section.panel.list>div.row>span:not(:last-child) {
--padding-right: calc(var(--gap) / 2);
}
section.panel.list > div.row > span:first-child {
section.panel.list>div.row>span:first-child {
border-radius: 3px 0 0 3px;
}
section.panel.list > div.row > span:last-child {
section.panel.list>div.row>span:last-child {
border-radius: 0 3px 3px 0;
}
section.panel.list > div.row:not(:hover, :focus) > span:first-child {
section.panel.list>div.row:not(:hover, :focus)>span:first-child {
--padding-left: var(--gap, 12px);
}
section.panel.list > div.row:not(:hover, :focus) > span:last-child {
section.panel.list>div.row:not(:hover, :focus)>span:last-child {
--padding-right: var(--gap, 12px);
}
section.panel.list > div.row:nth-of-type(1) > span {
section.panel.list>div.row:nth-of-type(1)>span {
text-align: center;
}
section.panel.list > div.row:nth-of-type(1) > span > i {
section.panel.list>div.row:nth-of-type(1)>span>i {
position: relative;
margin: auto;
}
section.panel.list > div.row > span[onclick] {
section.panel.list>div.row>span[onclick] {
cursor: pointer;
}
section.panel.list > div.row > span.field {
section.panel.list>div.row>span.field {
cursor: text;
}
section.panel.list > div.row:not(:nth-of-type(1)) > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row:not(:nth-of-type(1)):not([data-blocked])>span:is(.important, .interactive:is(:hover, :focus)) {
--margin: calc(var(--gap) / 2);
--border-left: calc(var(--padding-left, var(--margin, 0px)) * -1);
--border-right: var(--padding-right, var(--margin, 0px));
@ -367,54 +290,222 @@ section.panel.list > div.row:not(:nth-of-type(1)) > span:is(.important, .interac
--box-shadow: var(--border-left, 0) 0 0 0 var(--box-shadow-color, var(--background)), var(--border-right, 0) 0 0 0 var(--box-shadow-color, var(--background));
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1) > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).confirmed {
--background: var(--grass);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).confirmed {
--background: var(--grass-above);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).published {
--background: var(--river);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).published {
--background: var(--river-above);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).confirmed.published:not(.problematic) {
--background: var(--sea);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).confirmed.published:not(.problematic):nth-child(2n + 1) {
--background: var(--sea-above);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).problematic {
--background: var(--clay);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).problematic {
--background: var(--clay-above);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).coming {
--background: var(--magma);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).coming {
--background: var(--magma-above);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).completed:not(.problematic) {
--background: var(--sand);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).completed:not(.problematic) {
--background: var(--sand-above);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).passed {
filter: brightness(0.8);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)).banned {
--background: var(--clay);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)).banned a {
--color: var(--clay-text);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)).banned a:is(:hover, :focus) {
--color: var(--clay-text-above);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)).banned a:active {
--color: var(--clay-text-below);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)):nth-child(2n + 1).banned {
--background: var(--clay-above);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)):nth-child(2n + 1).banned a {
--color: var(--clay-text);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)):nth-child(2n + 1).banned a:is(:hover, :focus) {
--color: var(--clay-text-above);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)):nth-child(2n + 1).banned a:active {
--color: var(--clay-text-below);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)).fired {
--background: var(--magma);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)).fired a:is(:hover, :focus) {
--color: var(--magma-text-above);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)).fired a:active {
--color: var(--magma-text-below);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)).fired a {
--color: var(--magma-text);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)):nth-child(2n + 1).fired {
--background: var(--magma-above);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)):nth-child(2n + 1).fired a {
--color: var(--magma-text);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)):nth-child(2n + 1).fired a:is(:hover, :focus) {
--color: var(--magma-text-above);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)):nth-child(2n + 1).fired a:active {
--color: var(--magma-text-below);
}
section.panel.list>div.row:not(:nth-of-type(1)).hided * {
filter: blur(1px);
opacity: 0.3;
}
section.panel.list>div.row:not(:nth-of-type(1)).hided:is(:hover, :focus) * {
filter: unset;
opacity: unset;
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1)>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--cloud-rainy-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).published > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).published>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--river-deep);
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1).published > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).published>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--river-deep-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).confirmed > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).confirmed>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--grass-dense);
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1).confirmed > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).confirmed>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--grass-dense-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).confirmed.published:not(.problematic) > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).confirmed.published:not(.problematic)>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--sea-deep);
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1).confirmed.published:not(.problematic) > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).confirmed.published:not(.problematic)>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--sea-deep-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).problematic > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).problematic>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--clay-important);
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1).problematic > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).problematic>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--clay-important-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).coming > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).coming>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--magma-important);
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1).coming > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).coming>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--magma-important-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).completed:not(.problematic) > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).completed:not(.problematic)>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--sand-important);
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1).completed:not(.problematic) > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).completed:not(.problematic)>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--sand-important-above);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)).banned>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--clay-important);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)):nth-child(2n + 1).banned>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--clay-important-above);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)).fired>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--magma-important);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)):nth-child(2n + 1).fired>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--magma-important-above);
}
section.panel.list>div.row[data-blocked] {
margin-left: 8px;
width: calc(var(--width) - 16px);
cursor: progress;
opacity: 70% !important;
}
section.panel.list>div.row[data-blocked]:before {
content: attr(data-blocked) !important;
color: var(--earth-text-important-below);
}
section.panel.list>div.row[data-blocked] * {
pointer-events: none;
}
section.panel.list>div.row:not([data-blocked]):has(+ div.row[data-blocked]) {
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
}
section.panel.list>div.row[data-blocked]+div.row:not([data-blocked]) {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}

View File

@ -58,7 +58,8 @@ input[type="range"] {
button,
input[type="submit"],
input[type="range"] {
input[type="range"],
input[type="checkbox"] {
cursor: pointer;
}
@ -259,15 +260,16 @@ button:is(.transparent, .transparent:is(:hover, :focus), .transparent:active) {
}
a {
color: var(--link);
--color: var(--link);
color: var(--color);
}
a:is(:hover, :focus) {
color: var(--link-hover);
--color: var(--link-hover);
}
a:active {
color: var(--link-active);
--color: var(--link-active);
transition: unset;
}

View File

@ -59,3 +59,19 @@ section#operators.panel.list > div.row > span[data-column="commentary"] {
min-width: unset;
width: 100%;
}
section#operators.panel.list>div.row[data-row="operator"]:not(:nth-of-type(1)).transactions {
--background: var(--magma);
}
section#operators.panel.list>div.row[data-row="operator"]:not(:nth-of-type(1)):nth-child(2n + 1).transactions {
--background: var(--magma-above);
}
section#operators.panel.list>div.row[data-row="operator"]:not(:nth-of-type(1)).transactions>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--magma-important);
}
section#operators.panel.list>div.row[data-row="operator"]:not(:nth-of-type(1)):nth-child(2n + 1).transactions>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--magma-important-above);
}

View File

@ -0,0 +1,73 @@
@charset "UTF-8";
main>section#settings.panel {
z-index: 1000;
width: 800px;
position: relative;
margin-bottom: calc(15vh - 45px);
display: flex;
flex-direction: column;
gap: 60px;
border-radius: 3px;
}
main>section#settings.panel>section.category {
display: flex;
flex-direction: column;
gap: 20px;
padding: 30px 40px;
border-radius: 3px;
background-color: var(--snow);
}
main>section#settings.panel>section.category>div {
padding-bottom: 6px;
display: flex;
flex-direction: column;
}
main>section#settings.panel>section.category>div>div {
margin: 0;
margin-left: 30px;
margin-bottom: 6px;
display: inline-flex;
align-items: center;
}
main>section#settings.panel>section.category>div>div>h2 {
margin: 0;
}
main>section#settings.panel>section.category>div>div>i {
position: relative;
margin-left: auto;
margin-right: 20px;
}
main>section#settings.panel>section.category>div>small {
margin-left: 20px;
width: 100%;
}
main>section#settings.panel>section.category>section.subcategory {
padding: 20px 20px;
display: flex;
flex-direction: column;
gap: 8px;
border-radius: 3px;
background-color: var(--snow-deep);
}
main>section#settings.panel>section.category>section.subcategory>h3 {
margin: 6px 0px 3px 25px;
}
main>section#settings.panel>section.category>section.subcategory>label>input {
width: 90px;
}
main>section#settings.panel>section.category>section.subcategory>p.empty {
text-align: center;
padding: 0 20%;
font-weight: bold;
}

View File

@ -13,7 +13,7 @@ section#tasks.panel.list
> span:is(
[data-column="worker"],
[data-column="name"],
[data-column="task"],
[data-column="work"],
[data-column="address"],
[data-column="type"],
[data-column="tax"],
@ -61,14 +61,14 @@ section#tasks.panel.list > div.row > span[data-column="name"] {
width: 130px;
}
section#tasks.panel.list > div.row > span[data-column="task"] {
section#tasks.panel.list > div.row > span[data-column="work"] {
min-width: 100px;
width: 100px;
}
section#tasks.panel.list
> div.row:not(:nth-of-type(1))
> span[data-column="task"] {
> span[data-column="work"] {
text-align: right;
}

View File

@ -15,7 +15,7 @@ section#workers.panel.list
[data-column="worker"],
[data-column="name"],
[data-column="number"],
[data-column="mail"],
[data-column="work"],
[data-column="passport"],
[data-column="address"],
[data-column="tax"],

32
mirzaev/ebala/system/public/css/popup.css Normal file → Executable file
View File

@ -38,7 +38,6 @@ div#popup>section.stretched {
flex-grow: unset;
}
div#popup>section.calculated {
width: calc(var(--calculated-width) - var(--padding-horizontal, 0px) * 2);
}
@ -53,6 +52,11 @@ div#popup>section.list {
border-radius: 3px;
}
div#popup>section.list.extensive {
max-width: unset;
max-height: unset;
}
div#popup>section.list>h3 {
margin-top: 4px;
margin-bottom: 22px;
@ -137,6 +141,25 @@ div#popup>section.list>section.main>div.column>:is(div, section).row>label>input
text-align: center;
}
div#popup>section.list>section.main>div.column> :is(div, select).row:has(>label>select[multiple]) {
height: auto;
max-height: 60px;
}
div#popup>section.list>section.main>div.column> :is(div, select).row>label>select[multiple] {
height: auto;
padding: 0px;
border-radius: 3px;
}
div#popup>section.list>section.main>div.column> :is(div, select).row>label>select[multiple]>option {
padding: 5px 10px;
}
/* div#popup>section.list>section.main>div.column> :is(div, select).row>label>select[multiple]>option[selected] {
background-color: var(--background-above-5);
} */
div#popup>section.list>section.main>div.column>:is(div, section).row>label> :is(input, button):only-child {
width: 100%;
}
@ -292,3 +315,10 @@ div#popup>section.list>section.main>div.column>section.row.message>textarea+butt
div#popup>section.list.errors>section.body>dl>dd {
margin-left: 20px;
}
div#popup>section.list .separator {
border-top: 2px solid var(--separator, var(--cloud));
padding-top: 10px;
margin-top: 10px;
margin-bottom: 10px;
}

View File

@ -83,10 +83,9 @@
--sand-important: #d7c06c;
--sand-important-below: #dfc79a;
--magma-text-above: ;
--magma-text: ;
--magma-text-below: ;
--magma-text-below-1: ;
--magma-text-above: #111;
--magma-text: #5e1a1a;
--magma-text-below: #826d1c;
--magma-above: #ffd325;
--magma: #e6bf26;
--magma-below: ;
@ -168,6 +167,10 @@
--link: #3c76ff;
--link-hover: #6594ff;
--link-active: #3064dd;
--socket-connected: #2be851;
--socket-disconnected: #8e8181;
--socket-text: #b09999;
}
body {

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

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