Compare commits

...

18 Commits

Author SHA1 Message Date
Arsen Mirzaev Tatyano-Muradovich a42c7bc39f Обновить README.md 2024-10-16 13:46:59 +07:00
Arsen Mirzaev Tatyano-Muradovich 19a5dbd9b7 resolved #129, resolved #124, resolved #123, resolved #119, resolved #118, resolved #114, resolved #111 2024-05-31 10:50:05 +07:00
Arsen Mirzaev Tatyano-Muradovich a5b771a99a resolved #125, resolved #122, resolved #121, resolved #120, resolved #110, resolved #109, resolved #108, resolved #90, resolved #68 2024-05-17 18:49:48 +07:00
Arsen Mirzaev Tatyano-Muradovich 5b10141217 resolved #106, resolved #102, resolved #97, resolved #83 2024-04-08 10:05:58 +07:00
Arsen Mirzaev Tatyano-Muradovich 8c1736f4fd resolved #100, resolved #101, resolved #103, resolved #104 2024-04-08 04:49:17 +07:00
Arsen Mirzaev Tatyano-Muradovich 46ff0a1d9b resolved #98 2024-04-08 02:37:20 +07:00
Arsen Mirzaev Tatyano-Muradovich cb5abd9358 resolved #84 2024-04-07 05:28:15 +07:00
Arsen Mirzaev Tatyano-Muradovich efb85a2609 resolved #96, resolved #79, resolved #65 2024-04-07 04:22:40 +07:00
Arsen Mirzaev Tatyano-Muradovich 03391a1269 resolved #92, resolved #94, resolved #78, resolved #69 2024-04-01 04:27:51 +07:00
Arsen Mirzaev Tatyano-Muradovich 26eced8fed fix #64, fix #87, fix #72, fix #86, fix #77, fix #71, fix #91, fix #88 2024-03-19 00:45:47 +07:00
Arsen Mirzaev Tatyano-Muradovich 4c6e5cdd1d resolved #30, resolved #63 2024-02-29 02:39:27 +07:00
Arsen Mirzaev Tatyano-Muradovich c5e940fc25 resolved #56 2024-02-23 04:48:42 +07:00
Arsen Mirzaev Tatyano-Muradovich 8df0254271 resolved #57, resolved #58 2024-02-22 22:58:45 +07:00
Arsen Mirzaev Tatyano-Muradovich 6c4c0b1ada resolved #61, resolved #59 2024-02-22 20:17:00 +07:00
Arsen Mirzaev Tatyano-Muradovich 5641a686a2 fix #55 2024-01-22 19:34:42 +07:00
Arsen Mirzaev Tatyano-Muradovich da90b0c533 fix #33 fix #52 fix #53 2024-01-22 19:25:55 +07:00
Arsen Mirzaev Tatyano-Muradovich db9c99390e fix #50 fix #51 2024-01-22 11:27:21 +07:00
Arsen Mirzaev Tatyano-Muradovich f6d090f250 fix #31 fix #35 fix #39 fix #40 fix #41 fix #42 fix #43 fix #47 fix #49 2024-01-18 01:54:00 +07:00
198 changed files with 18539 additions and 10537 deletions

View File

@ -1,10 +1,6 @@
# Ebala
### Site for providing outsourced employees to retail stores
Site-registry of tasks for outsourced employees
My customer uses this development to process thousands of applications throughout Krasnoyarsk and neighboring cities.<br/>
**9 000 000** Russian rubles pass through the site every **week** 🤟.
For this project I received **600 000** Russian rubles. At the current exchange rate this is **$6000**.<br/>
If I had not been lazy and submitted the order a few months ago, it would have been **$8000** 😥 (super sad).
I did not sign any documents prohibiting the dissemination of information. You can use the code under the **WTFPL** license 🤝 (read ./LICENSE)
From this project i earned >**1 000 000** Russian rubles</br>
**DEVELOPMENT COMPLETED. PROJECT CLOSED.**</br>
</br>

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,113 +106,158 @@ final class account extends core
*/
public function update(array $parameters = []): ?string
{
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
try {
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
// Инициализация данных аккаунта
$account = model::read('d._key == "' . $parameters['id'] . '"');
// Инициализация данных аккаунта
$account = model::read('d._key == "' . $parameters['id'] . '"');
if (!empty($account)) {
// Найден аккаунт
if (!empty($account)) {
// Найден аккаунт
// Инициализация буфера изменённости пароля
$password = false;
// Инициализация буфера изменённости пароля
$password = false;
// Инициализация параметров (перезапись переданными значениями)
if ($parameters['name_first'] !== $account->name['first']) $account->name = ['first' => $parameters['name_first']] + $account->name;
if ($parameters['name_second'] !== $account->name['second']) $account->name = ['second' => $parameters['name_second']] + $account->name;
if ($parameters['name_last'] !== $account->name['last']) $account->name = ['last' => $parameters['name_last']] + $account->name;
if ($parameters['number'] !== $account->number)
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 (mb_strlen($parameters['password']) > 6) $account->password = sodium_crypto_pwhash_str(
$parameters['password'],
SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE,
SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE
);
else throw new exception('Пароль должен быть длиннее 6 символов');
if ($parameters['commentary'] !== $account->commentary) $account->commentary = $parameters['commentary'];
// Инициализация параметров (перезапись переданными значениями)
if ($parameters['name_first'] !== $account->name['first']) $account->name = ['first' => $parameters['name_first']] + $account->name;
if ($parameters['name_second'] !== $account->name['second']) $account->name = ['second' => $parameters['name_second']] + $account->name;
if ($parameters['name_last'] !== $account->name['last']) $account->name = ['last' => $parameters['name_last']] + $account->name;
if ($parameters['number'] !== $account->number)
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 (mb_strlen($parameters['password']) > 6) $account->password = sodium_crypto_pwhash_str(
$parameters['password'],
SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE,
SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE
);
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)) {
// Записаны данные аккаунта
if (_core::update($account)) {
// Записаны данные аккаунта
if ($account->type === 'worker') {
// Сотрудник
if ($account->type === 'worker') {
// Сотрудник
// Инициализация строки в глобальную переменную шаблонизатора
$this->view->rows = registry::workers(
before: sprintf(
"FILTER a._id == '%s' && a.deleted != true",
$account->getId()
),
after: <<<AQL
// Инициализация строки в глобальную переменную шаблонизатора
$this->view->rows = registry::workers(
before: sprintf(
"FILTER a._id == '%s' && a.deleted != true",
$account->getId()
),
after: <<<AQL
let account = (IS_SAME_COLLECTION('account', a) ? a : b)
let worker = (IS_SAME_COLLECTION('worker', a) ? a : b)
FILTER account.type == 'worker' && b.deleted != true
AQL,
amount: 1
);
} else if ($account->type === 'market') {
// Магазин
amount: 1
);
} else if ($account->type === 'market') {
// Магазин
// Инициализация строки в глобальную переменную шаблонизатора
$this->view->rows = registry::markets(
before: sprintf(
"FILTER a._id == '%s' && a.deleted != true",
$account->getId()
),
after: <<<AQL
// Инициализация строки в глобальную переменную шаблонизатора
$this->view->rows = registry::markets(
before: sprintf(
"FILTER a._id == '%s' && a.deleted != true",
$account->getId()
),
after: <<<AQL
let account = (IS_SAME_COLLECTION('account', a) ? a : b)
let market = (IS_SAME_COLLECTION('market', a) ? a : b)
FILTER account.type == 'market' && b.deleted != true
AQL,
amount: 1
);
} else {
// Администратор или оператор (подразумевается)
amount: 1
);
} else {
// Администратор или оператор (подразумевается)
// Инициализация строки в глобальную переменную шаблонизатора
$this->view->rows = [['account' => $account->getAll()]];
}
// Инициализация строки в глобальную переменную шаблонизатора
$this->view->rows = [['account' => $account->getAll()]];
}
// Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение)
$this->view->page = null;
// Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение)
$this->view->page = null;
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'updated' => true,
'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . $account->type . 's.html'),
'errors' => self::parse_only_text($this->errors)
];
// Инициализация буфера ответа
$return = [
'updated' => true,
'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . $account->type . 's.html'),
'errors' => self::parse_only_text($this->errors)
];
if ($password) $return['clipboard'] = <<<TEXT
Идентификатор: {$account->getKey()}
Пароль: {$parameters['password']}
TEXT;
if ($password) $return['clipboard'] = match ($account->type) {
'worker' => 'Номер: ' . model::worker($account->getId())?->number,
'market' => 'Идентификатор: ' . model::market($account->getId())?->id,
'operator' => "Идентификатор: {$account->getKey()}",
'administrator' => "Идентификатор: {$account->getKey()}",
default => "Идентификатор: {$account->getKey()}"
}
. "\nПароль: {$parameters['password']}";
// Генерация ответа
echo json_encode($return);
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не удалось записать изменения в базу данных');
} else throw new exception('Не удалось найти аккаунт');
// Отправка и деинициализация буфера вывода
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 = [
'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;
}
@ -192,75 +269,282 @@ final class account extends core
*/
public function delete(array $parameters = []): ?string
{
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
try {
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
// Инициализация данных аккаунта
$account = model::read('d._key == "' . $parameters['id'] . '"');
// Инициализация данных аккаунта
$account = model::read('d._key == "' . $parameters['id'] . '"');
if (!empty($account)) {
// Найден аккаунт
// Удаление
$account->active = false;
$account->deleted = true;
if ($account->type === 'worker') {
// Сотрудник
// Инициализация сотрудника
if (empty($worker = model::worker($account->getId()))) throw new exception('Не удалось инициализировать сотрудника');
if (!empty($account)) {
// Найден аккаунт
// Удаление
$worker->active = false;
$worker->deleted = true;
$account->active = false;
$account->deleted = true;
// Запись в ArangoDB
if (!_core::update($worker)) throw throw new exception('Не удалось записать изменения в базу данных');
} else if ($account->type === 'market') {
// Магазин
if ($account->type === 'worker') {
// Сотрудник
// Инициализация магазина
if (empty($market = model::market($account->getId()))) throw new exception('Не удалось инициализировать магазин');
// Инициализация сотрудника
if (empty($worker = model::worker($account->getId()))) throw new exception('Не удалось инициализировать сотрудника');
// Удаление
$market->active = false;
$market->deleted = true;
// Удаление
$worker->active = false;
$worker->deleted = true;
// Запись в ArangoDB
if (!_core::update($market)) throw throw new exception('Не удалось записать изменения в базу данных');
}
// Запись в ArangoDB
if (!_core::update($worker)) throw throw new exception('Не удалось записать изменения в базу данных');
} else if ($account->type === 'market') {
// Магазин
if (_core::update($account)) {
// Записаны данные аккаунта
// Инициализация магазина
if (empty($market = model::market($account->getId()))) throw new exception('Не удалось инициализировать магазин');
// Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение)
$this->view->page = null;
// Удаление
$market->active = false;
$market->deleted = true;
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Запись в ArangoDB
if (!_core::update($market)) throw throw new exception('Не удалось записать изменения в базу данных');
}
// Инициализация буфера вывода
ob_start();
if (_core::update($account)) {
// Записаны данные аккаунта
// Генерация ответа
echo json_encode([
'deleted' => true,
'errors' => self::parse_only_text($this->errors)
]);
// Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение)
$this->view->page = null;
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не удалось записать изменения в базу данных');
} else throw new exception('Не удалось найти аккаунт');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode([
'deleted' => 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 = [
'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);
@ -150,7 +153,7 @@ class core extends controller
$buffer = [];
// Инициализация данных магазина для аккаунта для генерации представления
foreach ($this->view->accounts as $vendor) $buffer[] = ['account' => $vendor, 'market' => account::market($vendor->getId(), errors: $this->errors['account'])];
foreach ($this->view->accounts as $account) $buffer[] = ['account' => $account, 'market' => account::market($account->getId(), errors: $this->errors['account'])];
// Запись в глобальную переменную из буфера
$this->view->accounts = $buffer;

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
@ -144,6 +144,7 @@ final class market extends core
a.commentary IN TOKENS(@search, 'text_ru')
|| a.address IN TOKENS(@search, 'text_ru')
|| STARTS_WITH(a._key, @search)
|| STARTS_WITH(a.id, @search)
|| STARTS_WITH(a.name.first, @search)
|| STARTS_WITH(a.name.second, @search)
|| STARTS_WITH(a.name.last, @search)
@ -152,6 +153,7 @@ final class market extends core
|| STARTS_WITH(a.number, @search)
|| STARTS_WITH(a.mail, @search)
|| (LENGTH(@search) > 5 && LEVENSHTEIN_MATCH(a._key, TOKENS(@search, 'text_en')[0], 2, true))
|| (LENGTH(@search) > 2 && LEVENSHTEIN_MATCH(a.id, TOKENS(@search, 'text_en')[0], 1, true))
|| (LENGTH(@search) > 3 && LEVENSHTEIN_MATCH(a.name.first, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 3 && LEVENSHTEIN_MATCH(a.name.second, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 3 && LEVENSHTEIN_MATCH(a.name.last, TOKENS(@search, 'text_ru')[0], 2, true))
@ -178,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
]
@ -236,6 +241,7 @@ final class market extends core
else if (!empty($parameters['account_number']) && strlen($parameters['account_number']) < 11) throw new exception('Несоответствие формату SIM-номера аккаунта представителя');
else if (!empty($parameters['market_mail']) && preg_match('/^.+@.+\.\w+$/', $parameters['market_mail']) === 0) throw new exception('Несоответствие формату почты представителя');
else if (!empty($parameters['account_mail']) && preg_match('/^.+@.+\.\w+$/', $parameters['account_mail']) === 0) throw new exception('Несоответствие формату почты аккаунта представителя');
else if (!empty($parameters['market_id']) && model::read('d.id == "' . $parameters['market_id'] . '"', errors: $this->errors['account']) instanceof _document) throw new exception('Уже существует магазин с данным идентификатором');
// Универсализация
/* $parameters['market_number'] = (int) $parameters['market_number']; */
@ -274,8 +280,8 @@ final class market extends core
];
}
// Инициализация идентификатора аккаунта (ключ документа инстанции аккаунта в базе данных)
$_key = preg_replace('/.+\//', '', $account ?? '');
// Инициализация идентификатора магазина
$id = empty($parameters['market_id']) ? model::id() : $parameters['market_id'];
// Запись заголовков ответа
header('Content-Type: application/json');
@ -289,7 +295,7 @@ final class market extends core
echo json_encode(
[
'clipboard' => empty($this->errors['account']) ? <<<TEXT
Идентификатор: $_key
Идентификатор: $id
Пароль: {$parameters['account_password']}
TEXT : '',
'errors' => self::parse_only_text($this->errors['account'])
@ -304,32 +310,38 @@ final class market extends core
flush();
try {
// Создание магазина
$market = model::create(
data: [
'name' => [
'first' => $parameters['market_name_first'],
'second' => $parameters['market_name_second'],
'last' => $parameters['market_name_last']
if (isset($account)) {
// Инициализирован аккаунт
// Создание магазина
$market = model::create(
data: [
'id' => (string) $id,
'name' => [
'first' => $parameters['market_name_first'],
'second' => $parameters['market_name_second'],
'last' => $parameters['market_name_last']
],
'number' => $parameters['market_number'] === 0 ? '' : $parameters['market_number'],
'mail' => $parameters['market_mail'],
'type' => $parameters['market_type'],
'city' => $parameters['market_city'],
'district' => $parameters['market_district'],
'address' => $parameters['market_address'],
],
'number' => $parameters['market_number'] === 0 ? '' : $parameters['market_number'],
'mail' => $parameters['market_mail'],
'type' => $parameters['market_type'],
'city' => $parameters['market_city'],
'district' => $parameters['market_district'],
'address' => $parameters['market_address'],
],
errors: $this->errors['account']
);
errors: $this->errors['account']
);
// Проверка существования созданного магазина
if (empty($market)) throw new exception('Не удалось создать магазин');
// Проверка существования созданного магазина
if (empty($market)) throw new exception('Не удалось создать магазин');
// Создание ребра: account -> market
account::connect($account, $market, 'market', $this->errors['account']);
// Создание ребра: account -> market
account::connect($account, $market, 'market', $this->errors['account']);
}
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(),
@ -350,7 +362,7 @@ final class market extends core
// Авторизован аккаунт администратора или оператора
// Инициализация данных магазина
$market = model::read('d._key == "' . $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)) {
// Найдены данные магазина
@ -380,51 +392,201 @@ final class market extends core
}
/**
* Обновить данные
* Заблокировать сотрудника в магазине
*
* @param array $parameters Параметры запроса
*/
public function update(array $parameters = []): ?string
public function ban(array $parameters = []): ?string
{
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
try {
if ($this->account->status() && $this->account->type === 'market') {
// Авторизован аккаунт магазина
// Инициализация данных магазина
$market = model::read('d._key == "' . $parameters['id'] . '"');
// Инициализация данных магазина
$market = account::market($this->account->getId());
if (!empty($market)) {
// Найден магазин
// Инициализация параметров (перезапись переданными значениями)
if ($parameters['name_first'] !== $market->name['first']) $market->name = ['first' => $parameters['name_first']] + $market->name;
if ($parameters['name_second'] !== $market->name['second']) $market->name = ['second' => $parameters['name_second']] + $market->name;
if ($parameters['name_last'] !== $market->name['last']) $market->name = ['last' => $parameters['name_last']] + $market->name;
if ($parameters['number'] !== $market->number)
if (mb_strlen($parameters['number']) === 11) $market->number = $parameters['number'];
else throw new exception('Номер должен состоять из 11 символов');
if ($parameters['mail'] !== $market->mail) $market->mail = $parameters['mail'];
if ($parameters['type'] !== $market->type) $market->type = $parameters['type'];
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 ($market instanceof _document) {
// Найден магазин
if (_core::update($market)) {
// Записаны данные магазина
// Блокировка сотрудника
if (!in_array($parameters['worker'], $market->bans ??= [], true)) $market->bans = $market->bans + [$parameters['worker']];
// Инициализация строки в глобальную переменную шаблонизатора
$this->view->rows = registry::markets(
before: sprintf(
"FILTER a._id == '%s' && a.deleted != true",
$market->getId()
),
after: <<<AQL
let account = (IS_SAME_COLLECTION('account', a) ? a : b)
let market = (IS_SAME_COLLECTION('market', a) ? a : b)
FILTER account.type == 'market' && b.deleted != true
AQL,
amount: 1,
target: model::COLLECTION
);
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;
@ -439,8 +601,7 @@ final class market extends core
// Инициализация буфера ответа
$return = [
'updated' => true,
'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'markets.html'),
'banned' => in_array($parameters['worker'], $market->bans ?? [], true),
'errors' => self::parse_only_text($this->errors)
];
@ -453,8 +614,155 @@ final class market extends core
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не удалось записать изменения в базу данных');
} 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 = [
'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 update(array $parameters = []): ?string
{
try {
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
// Инициализация данных магазина
$market = model::read('d.id == "' . urldecode($parameters['id']) . '"');
if (!empty($market)) {
// Найден магазин
// Инициализация параметров (перезапись переданными значениями)
if ($parameters['name_first'] !== $market->name['first']) $market->name = ['first' => $parameters['name_first']] + $market->name;
if ($parameters['name_second'] !== $market->name['second']) $market->name = ['second' => $parameters['name_second']] + $market->name;
if ($parameters['name_last'] !== $market->name['last']) $market->name = ['last' => $parameters['name_last']] + $market->name;
if ($parameters['number'] !== $market->number)
if (mb_strlen($parameters['number']) === 11) $market->number = $parameters['number'];
else throw new exception('Номер должен состоять из 11 символов');
if ($parameters['mail'] !== $market->mail) $market->mail = $parameters['mail'];
if ($parameters['type'] !== $market->type) $market->type = $parameters['type'];
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)) {
// Записаны данные магазина
// Инициализация строки в глобальную переменную шаблонизатора
$this->view->rows = registry::markets(
before: sprintf(
"FILTER a._id == '%s' && a.deleted != true",
$market->getId()
),
after: <<<AQL
let account = (IS_SAME_COLLECTION('account', a) ? a : b)
let market = (IS_SAME_COLLECTION('market', a) ? a : b)
FILTER account.type == 'market' && b.deleted != true
AQL,
amount: 1,
target: model::COLLECTION
);
// Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение)
$this->view->page = null;
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'updated' => true,
'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'markets.html'),
'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 = [
'updated' => false,
'errors' => self::parse_only_text($this->errors)
];
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
// Возврат (провал)
@ -472,7 +780,7 @@ final class market extends core
// Авторизован аккаунт оператора или магазина
// Инициализация данных магазинов
$this->view->markets = model::read(filter: 'd.active == true', amount: 10000, return: '{ _key: d._key, name: d.name }');
$this->view->markets = model::read(filter: 'd.active == true', amount: 10000, return: '{ id: d.id, name: d.name }');
// Универсализация
if ($this->view->markets instanceof _document) $this->view->markets = [$this->view->markets];

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

@ -8,6 +8,7 @@ namespace mirzaev\ebala\controllers;
use mirzaev\ebala\controllers\core,
mirzaev\ebala\controllers\traits\errors,
mirzaev\ebala\models\account,
mirzaev\ebala\models\worker,
mirzaev\ebala\models\market;
// Библиотека для ArangoDB
@ -76,11 +77,11 @@ final class session extends core
}
// Поиск аккаунта
$account = account::read('d.number == "' . $parameters['worker'] . '"', amount: 1);
$worker = worker::read('d.number == "' . $parameters['worker'] . '"', amount: 1);
// Генерация ответа по запрашиваемым параметрам
foreach ($return as $parameter) match ($parameter) {
'exist' => $buffer['exist'] = isset($account),
'exist' => $buffer['exist'] = isset($worker),
'account' => (function () use ($parameters, $remember, &$buffer) {
// Запись в буфер сессии
if ($remember) $this->session->write(['entry' => ['number' => $parameters['worker']]], $this->errors);
@ -363,14 +364,14 @@ 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') {
// Запрошено запоминание
// Запись в cookie
setcookie('entry__key', $parameters['market'], [
setcookie('entry_id', $parameters['market'], [
'expires' => strtotime('+1 day'),
'path' => '/',
'secure' => true,
@ -379,15 +380,15 @@ final class session extends core
]);
}
// Поиск аккаунта
$account = account::read('d._key == "' . $parameters['market'] . '"', amount: 1);
// Поиск магазина
$market = market::read('d.id == "' . $parameters['market'] . '"', amount: 1);
// Генерация ответа по запрашиваемым параметрам
foreach ($return as $parameter) match ($parameter) {
'exist' => $buffer['exist'] = isset($account),
'exist' => $buffer['exist'] = isset($market),
'account' => (function () use ($parameters, $remember, &$buffer) {
// Запись в буфер сессии
if ($remember) $this->session->write(['entry' => ['_key' => $parameters['market']]], $this->errors);
if ($remember) $this->session->write(['entry' => ['id' => $parameters['market']]], $this->errors);
// Поиск аккаунта и запись в буфер вывода
$buffer['account'] = (new account($this->session, 'market', $this->errors))?->instance() instanceof _document;
@ -429,7 +430,7 @@ final class session extends core
// Запись в буфер сессии
if (!in_array('account', $return, true) && ($remember ?? false))
$this->session->write(['entry' => ['_key' => $parameters['market']]]);
$this->session->write(['entry' => ['id' => $parameters['market']]]);
}
/**

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;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -29,6 +29,11 @@ final class worker extends core
{
use errors;
/**
* Типы работ
*/
final public const WORKS = ['Кассир', 'Выкладчик', 'Гастроном', 'Бригадир', 'Грузчик', 'Мобильный грузчик', 'Мобильный универсал'];
/**
* Главная страница
*
@ -39,12 +44,12 @@ final class worker extends core
// Авторизация
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт оператора или администратора
foreach (['active', 'inactive', 'fined', 'decent', 'hided', 'fired'] as $name) {
// Перебор фильтров статусов
// Инициализация значения (приоритет у 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
@ -147,6 +152,7 @@ final class worker extends core
|| a.department.address IN TOKENS(@search, 'text_ru')
|| a.requisites IN TOKENS(@search, 'text_ru')
|| STARTS_WITH(a._key, @search)
|| STARTS_WITH(a.id, @search)
|| STARTS_WITH(a.name.first, @search)
|| STARTS_WITH(a.name.second, @search)
|| STARTS_WITH(a.name.last, @search)
@ -161,6 +167,7 @@ final class worker extends core
|| STARTS_WITH(a.requisites, @search)
|| STARTS_WITH(a.tax, @search)
|| (LENGTH(@search) > 5 && LEVENSHTEIN_MATCH(a._key, TOKENS(@search, 'text_en')[0], 2, true))
|| (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(a.id, TOKENS(@search, 'text_en')[0], 1, true))
|| (LENGTH(@search) > 3 && LEVENSHTEIN_MATCH(a.name.first, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 3 && LEVENSHTEIN_MATCH(a.name.second, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 3 && LEVENSHTEIN_MATCH(a.name.last, TOKENS(@search, 'text_ru')[0], 2, true))
@ -175,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(
@ -193,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
]
@ -251,6 +260,7 @@ final class worker extends core
else if (!empty($parameters['account_number']) && strlen($parameters['account_number']) < 11) throw new exception('Несоответствие формату SIM-номера аккаунта сотрудника');
else if (!empty($parameters['worker_mail']) && preg_match('/^.+@.+\.\w+$/', $parameters['worker_mail']) === 0) throw new exception('Несоответствие формату почты сотрудника');
else if (!empty($parameters['account_mail']) && preg_match('/^.+@.+\.\w+$/', $parameters['account_mail']) === 0) throw new exception('Несоответствие формату почты аккаунта сотрудника');
else if (!empty($parameters['worker_id']) && model::read('d.id == "' . $parameters['worker_id'] . '"', errors: $this->errors['account']) instanceof _document) throw new exception('Уже существует сотрудник с данным идентификатором');
// Универсализация
/* $parameters['worker_number'] = (int) $parameters['worker_number']; */
@ -258,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();
// Создание аккаунта
@ -269,7 +280,7 @@ final class worker extends core
'second' => $parameters['account_name_second'],
'last' => $parameters['account_name_last']
],
'number' => $parameters['account_number'] === 0 ? '' : $parameters['account_number'],
'number' => $parameters['account_number'],
'mail' => $parameters['account_mail'],
'password' => sodium_crypto_pwhash_str(
$parameters['account_password'],
@ -293,8 +304,8 @@ final class worker extends core
];
}
// Инициализация идентификатора аккаунта (ключ документа инстанции аккаунта в базе данных)
$_key = preg_replace('/.+\//', '', $account ?? '');
// Инициализация идентификатора сотрудника
$id = empty($parameters['worker_id']) ? model::id() : $parameters['worker_id'];
// Запись заголовков ответа
header('Content-Type: application/json');
@ -308,7 +319,7 @@ final class worker extends core
echo json_encode(
[
'clipboard' => empty($this->errors['account']) ? <<<TEXT
Идентификатор: $_key
Номер: {$parameters['account_number']}
Пароль: {$parameters['account_password']}
TEXT : '',
'errors' => self::parse_only_text($this->errors['account'])
@ -323,40 +334,47 @@ final class worker extends core
flush();
try {
// Создание сотрудника
$worker = model::create(
data: [
'name' => [
'first' => $parameters['worker_name_first'],
'second' => $parameters['worker_name_second'],
'last' => $parameters['worker_name_last']
],
'number' => $parameters['worker_number'] === 0 ? '' : $parameters['worker_number'],
'mail' => $parameters['worker_mail'],
'birth' => $parameters['worker_birth'],
'passport' => $parameters['worker_passport'],
'issued' => $parameters['worker_issued'],
'department' => [
'number' => $parameters['worker_department_number'],
'address' => $parameters['worker_department_address']
],
'requisites' => $parameters['worker_requisites'],
'payment' => $parameters['worker_payment'],
'tax' => $parameters['worker_tax'],
'city' => $parameters['worker_city'],
'district' => $parameters['worker_district'],
'address' => $parameters['worker_address'],
'hiring' => $parameters['worker_hiring'],
'rating' => 3
],
errors: $this->errors['account']
);
if (isset($account)) {
// Инициализирован аккаунт
// Проверка существования созданного сотрудника
if (empty($worker)) throw new exception('Не удалось создать сотрудника');
// Создание сотрудника
$worker = model::create(
data: [
'id' => (string) $id,
'name' => [
'first' => $parameters['worker_name_first'],
'second' => $parameters['worker_name_second'],
'last' => $parameters['worker_name_last']
],
'number' => $parameters['worker_number'],
'mail' => $parameters['worker_mail'],
'birth' => $parameters['worker_birth'],
'passport' => $parameters['worker_passport'],
'issued' => $parameters['worker_issued'],
'department' => [
'number' => $parameters['worker_department_number'],
'address' => $parameters['worker_department_address']
],
'requisites' => $parameters['worker_requisites'],
'payment' => $parameters['worker_payment'],
'tax' => $parameters['worker_tax'],
'city' => $parameters['worker_city'],
'district' => $parameters['worker_district'],
'address' => $parameters['worker_address'],
'work' => $parameters['worker_work'],
'hiring' => $parameters['worker_hiring'],
'rating' => 3
],
errors: $this->errors['account']
);
// Создание ребра: account -> worker
account::connect($account, $worker, 'worker', $this->errors['account']);
// Проверка существования созданного сотрудника
if (empty($worker)) throw new exception('Не удалось создать сотрудника');
// Создание ребра: account -> worker
account::connect($account, $worker, 'worker', $this->errors['account']);
}
throw new exception('Не инициализирован аккаунт');
} catch (exception $e) {
// Write to the errors registry
$this->errors['account'][] = [
@ -380,7 +398,7 @@ final class worker extends core
// Авторизован аккаунт администратора или оператора
// Инициализация данных сотрудника
$worker = model::read('d._key == "' . $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)) {
// Найдены данные сотрудника
@ -420,15 +438,20 @@ final class worker extends core
// Авторизован аккаунт администратора или оператора
// Инициализация данных сотрудника
$worker = model::read('d._key == "' . $parameters['id'] . '"');
$worker = model::read('d.id == "' . urldecode($parameters['id']) . '"');
if (!empty($worker)) {
// Найден сотрудник
// Универсализация
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;
@ -449,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)) {
// Записаны данные сотрудника
@ -504,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>
*
@ -515,7 +648,7 @@ final class worker extends core
// Авторизован аккаунт оператора или магазина
// Инициализация данных сотрудников
$this->view->workers = model::read(filter: 'd.active == true', amount: 10000, return: '{ _key: d._key, name: d.name }');
$this->view->workers = model::read(filter: 'd.active == true', amount: 10000, return: '{ id: d.id, name: d.name }');
// Универсализация
if ($this->view->workers instanceof _document) $this->view->workers = [$this->view->workers];

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,17 +137,15 @@ 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']['_key'])) {
} else if (!empty($session->buffer['market']['entry']['id'])) {
// Найден идентификатор магазина в буфере сессии
if (!empty($session->buffer['market']['entry']['password'])) {
// Найден пароль в буфере сессии
if (($account = self::read('d._key == "' . $session->buffer['market']['entry']['_key'] . '" && d.type == "market"', amount: 1)) instanceof _document) {
if (($account = market::account(market::read('d.id == "' . $session->buffer['market']['entry']['id'] . '"', amount: 1)?->getId()) ?? null) instanceof _document) {
// Найден аккаунт (игнорируются ошибки)
if (sodium_crypto_pwhash_str_verify($account->password, $session->buffer['market']['entry']['password'])) {
@ -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,8 +291,8 @@ final class account extends core
LIMIT 1
RETURN e
)
FILTER d._id == e[0]._to && d.active == true
SORT d.created DESC, d._key DESC
FILTER d._id == e[0]._to
SORT d.created DESC, d.id DESC
LIMIT 1
RETURN d
AQL,
@ -312,16 +318,19 @@ final class account extends core
}
/**
* Инициализировать связь аккаунта с сотрудником
* Подключить к сотруднику
*
* Ищет связь аккаунта с сотрудником, если не находит, то создаёт её
*
* @param string $worker Идентификатор инстанции документа аккаунта в базе данных
* @param string $account Идентификатор инстанции документа аккаунта в базе данных
* @param string $target Идентификатор инстанции документа цели в базе данны (подразумевается сотрудник или магазин)
* @param string $type Тип подключения (worker|market)
* @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
{
@ -342,11 +351,11 @@ final class account extends core
LIMIT 1
RETURN d
AQL,
self::COLLECTION . '_edge_' . worker::COLLECTION,
self::COLLECTION . "_edge_$type",
$account,
$target
)) instanceof _document
|| $id = document::write(static::$arangodb->session, self::COLLECTION . "_edge_$type", [
|| document::write(static::$arangodb->session, self::COLLECTION . "_edge_$type", [
'_from' => $account,
'_to' => $target
])
@ -381,7 +390,7 @@ final class account extends core
{
try {
if (collection::init(static::$arangodb->session, self::COLLECTION))
if ($id = document::write(static::$arangodb->session, self::COLLECTION, $data + ['active' => true])) return $id;
if ($id = (string) document::write(static::$arangodb->session, self::COLLECTION, $data + ['active' => true])) return $id;
else throw new exception('Не удалось создать аккаунт');
else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
@ -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,146 @@ 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
*
* @param ?string $collection Коллекция для подсчёта
* @param array &$errors Реестр ошибок
*
* @return int|null Количество документов в базе данных, если найдены
*/
public static function count(?string $collection = null, array &$errors = []): int|null
{
try {
if (collection::init(static::$arangodb->session, static::COLLECTION)) {
// Инициализирована коллекция
// Exit (success)
return collection::search(
static::$arangodb->session,
sprintf(
<<<'AQL'
RETURN LENGTH(%s)
AQL,
$collection ?? static::COLLECTION
)
);
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в реестр ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Generate identifier
*
* @param array &$errors Реестр ошибок
*
* @return int Идентиикатор (свободный)
*/
public static function id(array &$errors = []): int
{
try {
if (collection::init(static::$arangodb->session, static::COLLECTION)) {
// Инициализирована коллекция
// Exit (success)
return collection::search(
static::$arangodb->session,
sprintf(
<<<'AQL'
RETURN MAX((FOR d in %s RETURN +d.id))
AQL,
$collection ?? static::COLLECTION
)
) + 1;
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в реестр ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return 0;
}
/**
* Delete from ArangoDB
*

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,13 +121,14 @@ 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 {
try {
if (collection::init(static::$arangodb->session, account::COLLECTION) && collection::init(static::$arangodb->session, market::COLLECTION)) {
// Инициализированы коллекции
// Search the session data in ArangoDB
$markets = collection::search(static::$arangodb->session, sprintf(
<<<AQL
@ -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
{
@ -272,7 +217,8 @@ final class session extends core
'stack' => $e->getTrace()
];
}
// Возврат (провал)
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;
/**
* Модель заданий
@ -40,7 +41,7 @@ final class task extends core
/**
* Create task in ArangoDB
*
* @param ?string $date
* @param string|int|null $date
* @param ?string $worker
* @param ?string $work
* @param ?string $start
@ -51,22 +52,24 @@ final class task extends core
* @param bool $hided
* @param bool $problematic
* @param bool $completed
* @param ?string $commentary
* @param array $errors
*
* @return ?string Identificator of instance of ArangoDB
*/
public static function create(
?string $date = null,
string|int|null $date = null,
?string $worker = null,
?string $work = null,
?string $start = null,
?string $end = null,
?string $market = null,
bool $confirmed = false,
bool $published = true,
bool $published = false,
bool $hided = false,
bool $problematic = false,
bool $completed = false,
?string $commentary = null,
array &$errors = []
): ?string {
try {
@ -90,6 +93,7 @@ final class task extends core
'hided' => $hided,
'problematic' => $problematic,
'completed' => $completed,
'commentary' => $commentary,
]);
} else throw new exception('Не удалось инициализировать коллекции');
} catch (exception $e) {
@ -114,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
*
@ -129,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 {
@ -145,12 +151,12 @@ final class task extends core
<<<AQL
FOR task IN %s
%s
LET worker = (FOR worker in %s FILTER worker._key LIKE task.worker SORT worker.created DESC, worker._key DESC LIMIT 1 RETURN worker)[0]
LET market = (FOR market in %s FILTER market._key LIKE task.market SORT market.created DESC, market._key DESC LIMIT 1 RETURN market)[0]
LET worker = (FOR worker in %s FILTER worker.id != null && worker.id LIKE task.worker SORT worker.created DESC, worker.id DESC LIMIT 1 RETURN worker)[0]
LET market = (FOR market in %s FILTER market.id != null && market.id LIKE task.market SORT market.created DESC, market.id DESC LIMIT 1 RETURN market)[0]
%s
SORT %s
LIMIT %d, %d
RETURN {task, worker, market}
RETURN %s
AQL,
$target,
$before,
@ -159,7 +165,8 @@ final class task extends core
$after,
$sort,
--$page <= 0 ? 0 : $amount * $page,
$amount
$amount,
$return
), $binds);
// Exit (success)
@ -173,11 +180,214 @@ 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
*
* @param string $work Type of work
*
* @return string
*/
public static function label(string $work): string
{
return match (mb_strtolower($work)) {
'cashiers', 'cashier', 'кассиры', 'кассир' => 'Кассир',
'displayers', 'displayer', 'выкладчики', 'выкладчик' => 'Выкладчик',
'gastronomes', 'gastronome', 'гастрономы', 'гастроном' => 'Гастроном',
'brigadiers', 'brigadier', 'бригадиры', 'бригадир' => 'Бригадир',
'loaders', 'loader', 'грузчики', 'грузчик' => 'Грузчик',
'loaders_mobile', 'loader_mobile', 'мобильные грузчики', 'мобильный грузчик' => 'Мобильный грузчик',
'universals_mobile', 'universal_mobile', 'мобильные универсалы', 'мобильный универсал' => 'Мобильный универсал',
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;
}
@ -302,12 +304,14 @@ label * {
}
textarea {
--padding-x: 12px;
--padding-y: 8px;
width: 100%;
min-width: calc(100% - 24px);
min-height: 120px;
max-width: calc(100% - 24px);
max-height: 300px;
padding: 8px 12px;
padding: var(--padding-y, 8px) var(--padding-x, 12px);
font-size: smaller;
overflow: hidden;
border-radius: 3px;

View File

@ -32,12 +32,6 @@ section#administrators.panel.list > div.row > span[data-column="account"] {
text-align: center;
}
section#administrators.panel.list
> div.row:nth-of-type(1)
> span[data-column="account"] {
margin-top: 6px;
}
section#administrators.panel.list > div.row > span[data-column="name"] {
min-width: 130px;
width: 130px;

View File

@ -30,18 +30,22 @@ section#markets.panel.list
}
section#markets.panel.list > div.row > span:is([data-column="account"], [data-column="market"]) {
min-width: 102px;
width: 102px;
font-weight: bold;
}
section#markets.panel.list > div.row:nth-of-type(1) > span[data-column="account"] {
text-align: center;
}
section#workers.panel.list > div.row:nth-of-type(1) > span[data-column="account"] {
margin-top: 6px;
section#markets.panel.list > div.row > span[data-column="account"] {
min-width: 102px;
width: 102px;
text-align: center;
}
section#workers.panel.list > div.row:nth-of-type(1) > span[data-column="market"] {
margin-top: 0px;
section#markets.panel.list > div.row > span[data-column="market"] {
min-width: 67px;
width: 67px;
}
section#markets.panel.list > div.row > span[data-column="name"] {

View File

@ -32,10 +32,6 @@ section#operators.panel.list > div.row > span[data-column="account"] {
text-align: center;
}
section#operators.panel.list > div.row:nth-of-type(1) > span[data-column="account"] {
margin-top: 6px;
}
section#operators.panel.list > div.row > span[data-column="name"] {
min-width: 130px;
width: 130px;
@ -63,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"],
@ -34,14 +34,26 @@ section#tasks.panel.list > div.row > span[data-column="date"] {
font-weight: bold;
}
section#tasks.panel.list > div.row > span:is([data-column="worker"], [data-column="market"]) {
min-width: 102px;
width: 102px;
section#tasks.panel.list > div.row > span:is([data-column="market"], [data-column="worker"]) {
font-weight: bold;
}
section#tasks.panel.list > div.row:nth-of-type(1) > span[data-column="market"] {
text-align: center;
}
section#tasks.panel.list > div.row:nth-of-type(1) > span[data-column="worker"] {
margin-top: 8px;
margin-top: 8px;
}
section#tasks.panel.list > div.row > span[data-column="market"] {
min-width: 67px;
width: 67px;
text-align: right;
}
section#tasks.panel.list > div.row > span[data-column="worker"] {
min-width: 67px;
width: 67px;
}
section#tasks.panel.list > div.row > span[data-column="name"] {
@ -49,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,18 +32,26 @@ section#workers.panel.list
}
section#workers.panel.list > div.row > span:is([data-column="account"], [data-column="worker"]) {
min-width: 102px;
width: 102px;
font-weight: bold;
text-align: center;
}
section#workers.panel.list > div.row:nth-of-type(1) > span[data-column="account"] {
margin-top: 6px;
text-align: center;
}
section#workers.panel.list > div.row:nth-of-type(1) > span[data-column="worker"] {
margin-top: 8px;
margin-top: 8px;
}
section#workers.panel.list > div.row > span[data-column="account"] {
min-width: 102px;
width: 102px;
text-align: center;
}
section#workers.panel.list > div.row > span[data-column="worker"] {
min-width: 67px;
width: 67px;
}
section#workers.panel.list > div.row > span[data-column="name"] {

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

@ -38,18 +38,25 @@ div#popup>section.stretched {
flex-grow: unset;
}
div#popup>section.calculated {
width: calc(var(--calculated-width) - var(--padding-horizontal, 0px) * 2);
}
div#popup>section.list {
max-width: max(70vw, 1300px);
max-height: max(62vh, 600px);
display: flex;
flex-direction: column;
padding: 30px;
overflow-y: scroll;
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;
@ -65,17 +72,30 @@ div#popup>section.list h4 {
}
div#popup>section.list>section.main {
--gap: 15px;
display: flex;
gap: 15px;
flex-flow: row wrap;
justify-content: space-between;
gap: var(--gap, 15px);
}
div#popup>section.list>section.main>div.column {
flex-grow: 1;
display: flex;
flex-grow: 1;
flex-direction: column;
gap: 8px;
}
div#popup>section.list>section.main.flow>div.column:not(:only-child) {
width: 300px;
}
div#popup>section.list>section.main>div.column:not(:only-child)[data-column="buttons"]:last-of-type {
margin-left: auto;
justify-content: end;
}
div#popup>section.list>section.main>div.column:only-child {
width: 100%;
}
@ -121,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%;
}
@ -140,7 +179,21 @@ div#popup>section.list>section.main>div.column> :is(div, select).row.buttons {
div#popup>section.list>section.main>div.column> :is(div, select).row:not(.buttons, .stretchable, .endless),
div#popup>section.list>section.main>div.column> :is(div, select).row:not(.buttons, .stretchable, .endless)>button {
height: 29px;
--height: 29px;
height: var(--height, 29px);
}
div#popup>section.list>section.main>div.column> :is(div, select).row:not(.buttons .endless).stretchable,
div#popup>section.list>section.main>div.column> :is(div, select).row:not(.buttons, .endless).stretchable>button {
--height: 29px;
height: max(var(--height, 29px), fit-content);
}
div#popup>section.list>section.main>div.column> :is(div, select).row:not(.buttons .endless).stretchable>textarea {
/* min-height: calc(var(--height, 29px) - var(--padding-y, 8ox) * 2); */
min-height: 1rem;
max-height: 3rem;
height: 1rem;
}
div#popup>section.list>section.main>div.column>:is(div, section).row:not(.merged)+:is(div, section).row.merged {
@ -262,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

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