From 92417f7567eaf57254db6b0888a37682ca0d33da Mon Sep 17 00:00:00 2001 From: Arsen Mirzaev Tatyano-Muradovich Date: Thu, 21 Dec 2023 00:06:16 +0700 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=9E=D0=A5=D0=A3=D0=AF=20=D0=94=D0=9E?= =?UTF-8?q?=D0=91=D0=90=D0=92=D0=98=D0=9B=20=D0=9E=D0=A7=D0=95=D0=9D=D0=AC?= =?UTF-8?q?=20=D0=9C=D0=9E=D0=A9=D0=9D=D0=9E=20=D0=95=D0=91=D0=90=D0=9D?= =?UTF-8?q?=D0=A3=D0=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mirzaev/ebala/system/controllers/account.php | 267 + .../system/controllers/administrator.php | 193 +- mirzaev/ebala/system/controllers/core.php | 22 +- mirzaev/ebala/system/controllers/index.php | 18 +- mirzaev/ebala/system/controllers/market.php | 383 +- mirzaev/ebala/system/controllers/operator.php | 184 +- mirzaev/ebala/system/controllers/session.php | 36 +- mirzaev/ebala/system/controllers/task.php | 2730 +++++++-- mirzaev/ebala/system/controllers/worker.php | 441 +- mirzaev/ebala/system/models/account.php | 166 +- mirzaev/ebala/system/models/market.php | 33 +- mirzaev/ebala/system/models/registry.php | 84 +- mirzaev/ebala/system/models/session.php | 51 +- mirzaev/ebala/system/models/task.php | 26 +- mirzaev/ebala/system/models/traits/status.php | 8 +- mirzaev/ebala/system/models/worker.php | 35 +- mirzaev/ebala/system/public/css/account.css | 13 +- .../ebala/system/public/css/animations.css | 17 +- .../ebala/system/public/css/icons/enter.css | 28 + .../ebala/system/public/css/icons/nametag.css | 16 + .../system/public/css/icons/shopping_cart.css | 30 + .../ebala/system/public/css/icons/user.css | 9 + mirzaev/ebala/system/public/css/list.css | 277 +- mirzaev/ebala/system/public/css/main.css | 516 +- .../public/css/pages/administrators.css | 49 +- .../ebala/system/public/css/pages/markets.css | 61 +- .../system/public/css/pages/operators.css | 41 +- .../ebala/system/public/css/pages/tasks.css | 44 +- .../ebala/system/public/css/pages/workers.css | 78 +- mirzaev/ebala/system/public/css/popup.css | 245 +- .../public/css/themes/harmony/earth.css | 178 + mirzaev/ebala/system/public/index.php | 17 + .../ebala/system/public/js/administrators.js | 2214 +++++-- mirzaev/ebala/system/public/js/chat.js | 971 ++++ mirzaev/ebala/system/public/js/damper.js | 17 +- mirzaev/ebala/system/public/js/markets.js | 3462 +++++++++-- mirzaev/ebala/system/public/js/operators.js | 2198 +++++-- mirzaev/ebala/system/public/js/tasks.js | 5071 ++++++++++++----- mirzaev/ebala/system/public/js/workers.js | 4212 ++++++++++++-- .../system/public/sounds/notification.mp3 | Bin 0 -> 5256 bytes .../system/views/elements/administrators.html | 28 +- mirzaev/ebala/system/views/elements/chat.html | 8 + .../system/views/elements/lists/market.html | 13 +- .../lists/{task.html => task/actual.html} | 3 +- .../views/elements/lists/task/passed.html | 13 + .../system/views/elements/lists/worker.html | 23 +- .../ebala/system/views/elements/markets.html | 23 +- .../system/views/elements/operators.html | 24 +- .../ebala/system/views/elements/tasks.html | 55 +- .../ebala/system/views/elements/workers.html | 55 +- mirzaev/ebala/system/views/head.html | 1 + mirzaev/ebala/system/views/index.html | 1 + mirzaev/ebala/system/views/lists/markets.html | 4 +- mirzaev/ebala/system/views/lists/workers.html | 5 +- .../system/views/pages/administrators.html | 16 +- .../views/pages/entry/administrator.html | 22 +- .../system/views/pages/entry/market.html | 26 +- .../system/views/pages/entry/operator.html | 16 +- .../system/views/pages/entry/worker.html | 16 +- mirzaev/ebala/system/views/pages/info.txt | 1 + mirzaev/ebala/system/views/pages/markets.html | 21 +- .../ebala/system/views/pages/operators.html | 16 +- mirzaev/ebala/system/views/pages/tasks.html | 55 +- .../ebala/system/views/pages/tasks.html.old | 140 - mirzaev/ebala/system/views/pages/workers.html | 32 +- mirzaev/ebala/system/views/templater.php | 53 +- 66 files changed, 19619 insertions(+), 5492 deletions(-) create mode 100755 mirzaev/ebala/system/controllers/account.php create mode 100755 mirzaev/ebala/system/public/css/icons/enter.css create mode 100644 mirzaev/ebala/system/public/css/themes/harmony/earth.css create mode 100644 mirzaev/ebala/system/public/js/chat.js create mode 100644 mirzaev/ebala/system/public/sounds/notification.mp3 create mode 100644 mirzaev/ebala/system/views/elements/chat.html rename mirzaev/ebala/system/views/elements/lists/{task.html => task/actual.html} (91%) create mode 100644 mirzaev/ebala/system/views/elements/lists/task/passed.html delete mode 100644 mirzaev/ebala/system/views/pages/tasks.html.old diff --git a/mirzaev/ebala/system/controllers/account.php b/mirzaev/ebala/system/controllers/account.php new file mode 100755 index 0000000..aa1419c --- /dev/null +++ b/mirzaev/ebala/system/controllers/account.php @@ -0,0 +1,267 @@ + + */ +final class account extends core +{ + use errors; + + /** + * Прочитать данные + * + * @param array $parameters Параметры запроса + */ + public function fields(array $parameters = []): ?string + { + if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) { + // Авторизован аккаунт администратора или оператора + + // Инициализация данных аккаунта + $account = model::read('d._key == "' . $parameters['id'] . '"', return: '{ name: d.name, number: d.number, mail: d.mail, commentary: d.commentary }')->getAll(); + + if (!empty($account)) { + // Найдены данные аккаунта + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode($account); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } + } + + // Возврат (провал) + return null; + } + + /** + * Обновить данные + * + * @param array $parameters Параметры запроса + */ + public function update(array $parameters = []): ?string + { + if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) { + // Авторизован аккаунт администратора или оператора + + // Инициализация данных аккаунта + $account = model::read('d._key == "' . $parameters['id'] . '"'); + + if (!empty($account)) { + // Найден аккаунт + + // Инициализация буфера изменённости пароля + $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 (_core::update($account)) { + // Записаны данные аккаунта + + if ($account->type === 'worker') { + // Сотрудник + + // Инициализация строки в глобальную переменную шаблонизатора + $this->view->rows = registry::workers( + before: sprintf( + "FILTER a._id == '%s' && a.deleted != true", + $account->getId() + ), + after: <<type === 'market') { + // Магазин + + // Инициализация строки в глобальную переменную шаблонизатора + $this->view->rows = registry::markets( + before: sprintf( + "FILTER a._id == '%s' && a.deleted != true", + $account->getId() + ), + after: <<view->rows = [['account' => $account->getAll()]]; + } + + // Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение) + $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 . $account->type . 's.html'), + 'errors' => self::parse_only_text($this->errors) + ]; + + if ($password) $return['clipboard'] = <<getKey()} + Пароль: {$parameters['password']} + TEXT; + + // Генерация ответа + 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 delete(array $parameters = []): ?string + { + 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->deleted = true; + + if ($account->type === 'worker') { + // Сотрудник + + // Инициализация сотрудника + if (empty($worker = model::worker($account->getId()))) throw new exception('Не удалось инициализировать сотрудника'); + + // Удаление + $worker->active = false; + $worker->deleted = true; + + // Запись в ArangoDB + if (!_core::update($worker)) throw throw new exception('Не удалось записать изменения в базу данных'); + } else if ($account->type === 'market') { + // Магазин + + // Инициализация магазина + if (empty($market = model::market($account->getId()))) throw new exception('Не удалось инициализировать магазин'); + + // Удаление + $market->active = false; + $market->deleted = true; + + // Запись в ArangoDB + if (!_core::update($market)) throw throw new exception('Не удалось записать изменения в базу данных'); + } + + if (_core::update($account)) { + // Записаны данные аккаунта + + // Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение) + $this->view->page = null; + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + 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('Не удалось найти аккаунт'); + } + + // Возврат (провал) + return null; + } +} diff --git a/mirzaev/ebala/system/controllers/administrator.php b/mirzaev/ebala/system/controllers/administrator.php index 4ea2ecb..b528ba1 100755 --- a/mirzaev/ebala/system/controllers/administrator.php +++ b/mirzaev/ebala/system/controllers/administrator.php @@ -7,10 +7,11 @@ namespace mirzaev\ebala\controllers; // Файлы проекта use mirzaev\ebala\controllers\core, mirzaev\ebala\controllers\traits\errors, + mirzaev\ebala\models\account, mirzaev\ebala\models\registry; -// Библиотека для ArangoDB -use ArangoDBClient\Document as _document; +// Встроенные библиотеки +use exception; /** * Контроллер администратора @@ -30,10 +31,10 @@ final class administrator extends core public function index(array $parameters = []): ?string { // Авторизация - if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'administrator')) { - // Авторизован аккаунт оператора или администратора + if ($this->account->status() && $this->account->type === 'administrator') { + // Авторизован аккаунт администратора - foreach (['confirmed', 'waiting', 'published', 'unpublished', 'problematic', 'hided', 'completed'] as $name) { + foreach (['active', 'inactive'] as $name) { // Перебор фильтров статусов // Инициализация значения (приоритет у cookie) @@ -70,62 +71,89 @@ final class administrator extends core */ public function read(array $parameters = []): ?string { - if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'administrator')) { - // Авторизован аккаунт оператора или администратора + if ($this->account->status() && $this->account->type === 'administrator') { + // Авторизован аккаунт администратора // Реинициализация актуальной страницы if (isset($parameters['page'])) $this->session->write(['administrators' => ['page' => $parameters['page']]]); else if (empty($this->session->buffer[$_SERVER['INTERFACE']]['administrators']['page'])) $this->session->write(['administrators' => ['page' => 1]]); - // Инициализация буфера AQL-выражения для инъекции фильтра по интервалу - $polysemantic = ''; + // Инициализация буферов AQL-выражений для инъекции фильтра по статусам + $filters_statuses_and = ''; + $filters_statuses_or = ''; - // Инициализация допустимых статусов - $statuses = ['active', 'inactive', 'fined', 'decent', 'hided', 'fired']; - - // Инициализация буфера AQL-выражения для инъекции фильтра по статусам (И) - $statuses_and = ''; - - foreach ($statuses as $name) { + foreach (['active', 'inactive'] as $name) { // Перебор фильтров статусов (И) // Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue) if (empty($value = $_COOKIE["administrators_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['administrators']['filters'][$name] ?? 0)) continue; + // Конвертация ярлыков + $converted = match ($name) { + 'inactive' => 'active', + default => $name + }; + + // Генерация выражения + $expression = "account.$converted == " . ($name === $converted ? 'true' : 'false'); + // Генерация AQL-выражения для инъекции в строку запроса - if ($value === '1') $statuses_and .= " && administrator.$name == true"; + if ($value === '1') $filters_statuses_and .= " && " . $expression; + else if ($value === '2') $filters_statuses_or .= " || " . $expression; } // Очистка от бинарных операторов сравнения с только одним операндом (крайние) - $statuses_and = trim(trim(trim($statuses_and), '&&')); - - // Инициализация буфера AQL-выражения для инъекции фильтра по статусам (ИЛИ) - $statuses_or = ''; - - foreach ($statuses as $name) { - // Перебор фильтров статусов (ИЛИ) - - // Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue) - if (empty($value = $_COOKIE["administrators_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['administrators']['filters'][$name] ?? 0)) continue; - - // Генерация AQL-выражения для инъекции в строку запроса - if ($value === '2') $statuses_or .= " || administrator.$name == true"; - } - - // Очистка от бинарных операторов сравнения с только одним операндом (крайние) - $statuses_or = trim(trim(trim($statuses_or), '||')); + $filters_statuses_and = trim(trim(trim($filters_statuses_and), '&&')); + $filters_statuses_or = trim(trim(trim($filters_statuses_or), '||')); // Инициализация буфера с объёдинёнными буферами c AQL-выражениям "И" и "ИЛИ" - $statuses_merged = (empty($statuses_and) ? '' : "($statuses_and)") . (empty($statuses_or) ? '' : (empty($statuses_and) ? '' : ' || ') . "($statuses_or)"); + $filters_statuses_merged = (empty($filters_statuses_and) ? '' : "($filters_statuses_and)") . (empty($filters_statuses_or) ? '' : (empty($filters_statuses_and) ? '' : ' || ') . "($filters_statuses_or)"); // Инициализация общего буфера с AQL-выражениями $filters = ''; - // Объединение фильров в единую строку с AQL-выражениями для инъекции - if (!empty($statuses_merged)) $filters .= empty($filters) ? $statuses_merged : " && ($statuses_merged)"; + // Объединение фильтров в единую строку с AQL-выражениями для инъекции + 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'] ?? ''; + if (mb_strlen($search) < 3) $search = null; + $search_query = empty($search) + ? null + : << 6 && LEVENSHTEIN_MATCH(account._key, TOKENS(@search, 'text_en')[0], 2, true)) + || (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(account.name.first, TOKENS(@search, 'text_ru')[0], 2, true)) + || (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(account.name.second, TOKENS(@search, 'text_ru')[0], 2, true)) + || (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(account.name.last, TOKENS(@search, 'text_ru')[0], 2, true)) + || (LENGTH(@search) > 6 && LEVENSHTEIN_MATCH(account.number, TOKENS(@search, 'text_en')[0], 2, true)) + || (LENGTH(@search) > 6 && LEVENSHTEIN_MATCH(account.mail, TOKENS(@search, 'text_en')[0], 2, true)) + AQL; // Инициализация данных для генерации HTML-документа с таблицей - $this->view->rows = registry::administrators(before: empty($filters) ? null : "FILTER ($filters)", page: (int) $this->session->buffer[$_SERVER['INTERFACE']]['administrators']['page']); + $this->view->rows = registry::administrators( + before: sprintf( + <<session->buffer[$_SERVER['INTERFACE']]['administrators']['page'], + target: empty($search) ? account::COLLECTION : 'registry_accounts', + binds: empty($search) ? [] : [ + 'search' => $search + ] + ); + // Запись в cookie (только таким методом можно записать "hostonly: true") setcookie( @@ -150,4 +178,93 @@ final class administrator extends core // Возврат (провал) return null; } + + /** + * Создать + * + * @param array $parameters Параметры запроса + * + * @return void В буфер вывода JSON-документ с запрашиваемыми параметрами + */ + public function create(array $parameters = []): void + { + if ($this->account->status() && $this->account->type === 'administrator') { + // Авторизован аккаунт администратора + + // Инициализация буфера ошибок + $this->errors['account'] ??= []; + + try { + // Проверка наличия переданного пароля + if (!isset($parameters['password'])) throw new exception('Не получен пароль'); + else if (strlen($parameters['password']) < 6) throw new exception('Пароль должен быть не менее 6 символов'); + else if (!empty($parameters['number']) && strlen($parameters['number']) < 11) throw new exception('Несоответствие формату SIM-номера'); + else if (!empty($parameters['mail']) && preg_match('/^.+@.+\.\w+$/', $parameters['mail']) === 0) throw new exception('Несоответствие формату почты'); + + // Универсализация + /* $parameters['number'] = (int) $parameters['number']; */ + + // Создание аккаунта + $account = account::create( + data: [ + 'type' => 'administrator', + 'name' => [ + 'first' => $parameters['name_first'], + 'second' => $parameters['name_second'], + 'last' => $parameters['name_last'] + ], + 'number' => $parameters['number'] === 0 ? '' : $parameters['number'], + 'mail' => $parameters['mail'], + 'password' => sodium_crypto_pwhash_str( + $parameters['password'], + SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE + ), + 'commentary' => $parameters['commentary'] + ], + errors: $this->errors['account'] + ); + + // Проверка существования созданного аккаунта + if (empty($account)) 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() + ]; + } + + // Инициализация идентификатора аккаунта (ключ документа инстанции аккаунта в базе данных) + $_key = preg_replace('/.+\//', '', $account ?? ''); + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'clipboard' => empty($this->errors['account']) ? << self::parse_only_text($this->errors['account']) + ] + ); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } + } } diff --git a/mirzaev/ebala/system/controllers/core.php b/mirzaev/ebala/system/controllers/core.php index a8f4695..ec40780 100755 --- a/mirzaev/ebala/system/controllers/core.php +++ b/mirzaev/ebala/system/controllers/core.php @@ -37,11 +37,6 @@ class core extends controller */ protected readonly ?account $account; - /** - * Инстанция магазина - */ - protected readonly ?market $market; - /** * Реестр ошибок */ @@ -75,9 +70,9 @@ class core extends controller $_COOKIE["session"] ??= null; // Инициализация сессии - $this->session = new session($_COOKIE["session"], $expires); - - if ($_COOKIE["session"] !== ($this->session->hash)) { + $this->session = new session($_COOKIE["session"], $expires, $this->errors['session']); + if (!empty($this->errors['session'])) die; + else if ($_COOKIE["session"] !== $this->session->hash) { // Изменился хеш сессии (подразумевается, что сессия устарела) // Запись хеша новой сессии @@ -100,9 +95,6 @@ class core extends controller if ($this->account->status()) { // Инициализирован аккаунт - // Инициализация магазина - if ($this->account->type === 'market') $this->market = new market(account::market($this->account->getId())); - if ($this->account->type !== $_SERVER['INTERFACE']) { // Не соответствие типа аккаунта к запрошенному интерфейсу (например, если оператор зашел на интерфейс магазина) @@ -141,7 +133,7 @@ class core extends controller // Оператор // Инициализация данных аккаунтов для генерации представления - $this->view->accounts = account::read('d.type == "operator" && d.status == "active"', 'd.name.first DESC', 100, 1, errors: $this->errors['account']); + $this->view->accounts = account::read('d.type == "operator" && d.active == true', 'd.name.first DESC, d._key DESC', 100, 1, errors: $this->errors['account']); // Преобразование в массив, если вернуло инстанцию одного документа, вместо массива инстанций документов if (!is_array($this->view->accounts)) $this->view->accounts = [$this->view->accounts]; @@ -149,7 +141,7 @@ class core extends controller // Магазин // Инициализация данных аккаунтов для генерации представления - $this->view->accounts = account::read('d.type == "market" && d.status == "active"', 'd.name.first DESC', 100, 1, $this->errors['account']); + $this->view->accounts = account::read('d.type == "market" && d.active == true', 'd.name.first DESC, d._key DESC', 100, 1, errors: $this->errors['account']); // Преобразование в массив, если вернуло инстанцию одного документа, вместо массива инстанций документов if (!is_array($this->view->accounts)) $this->view->accounts = [$this->view->accounts]; @@ -158,7 +150,7 @@ class core extends controller $buffer = []; // Инициализация данных магазина для аккаунта для генерации представления - foreach ($this->view->accounts as $vendor) $buffer[] = ['vendor' => $vendor, 'market' => account::market($vendor->getId(), $this->errors['account'])]; + foreach ($this->view->accounts as $vendor) $buffer[] = ['account' => $vendor, 'market' => account::market($vendor->getId(), errors: $this->errors['account'])]; // Запись в глобальную переменную из буфера $this->view->accounts = $buffer; @@ -166,7 +158,7 @@ class core extends controller // Администратор // Инициализация данных аккаунтов для генерации представления - $this->view->accounts = account::read('d.type == "administrator" && d.status == "active"', 'd.name.first DESC', 100, 1, errors: $this->errors['account']); + $this->view->accounts = account::read('d.type == "administrator" && d.active == true', 'd.name.first DESC, d._key DESC', 100, 1, errors: $this->errors['account']); // Преобразование в массив, если вернуло инстанцию одного документа, вместо массива инстанций документов if (!is_array($this->view->accounts)) $this->view->accounts = [$this->view->accounts]; diff --git a/mirzaev/ebala/system/controllers/index.php b/mirzaev/ebala/system/controllers/index.php index 28e7154..f408266 100755 --- a/mirzaev/ebala/system/controllers/index.php +++ b/mirzaev/ebala/system/controllers/index.php @@ -66,6 +66,19 @@ final class index extends core return null; } + /** + * Заглушка для отправки формы при аутентификации + * + * Используется для того, чтобы отработали события браузера связанные с отправкой формы + * + * @param array $parameters Параметры запроса + */ + public function entry(array $parameters = []): ?string + { + // Возврат (успех) + return 'хули смотришь сюда выродок'; + } + /** * Main menu * @@ -76,8 +89,11 @@ final class index extends core if ($this->account->status()) { // Авторизован аккаунт - // Генерация представления + // Генерация и возврат (успех) return $this->view->render(DIRECTORY_SEPARATOR . 'menu.html'); } + + // Возврат (провал) + return null; } } diff --git a/mirzaev/ebala/system/controllers/market.php b/mirzaev/ebala/system/controllers/market.php index 27af9f7..de8895f 100755 --- a/mirzaev/ebala/system/controllers/market.php +++ b/mirzaev/ebala/system/controllers/market.php @@ -8,11 +8,17 @@ namespace mirzaev\ebala\controllers; use mirzaev\ebala\controllers\core, mirzaev\ebala\controllers\traits\errors, mirzaev\ebala\models\registry, - mirzaev\ebala\models\market as model; + mirzaev\ebala\models\account, + mirzaev\ebala\models\market as model, + mirzaev\ebala\models\core as _core; // Библиотека для ArangoDB use ArangoDBClient\Document as _document; +// Встроенные библиотеки +use exception, + datetime; + /** * Контроллер магазина * @@ -34,7 +40,7 @@ final class market extends core if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) { // Авторизован аккаунт оператора или администратора - foreach (['confirmed', 'waiting', 'published', 'unpublished', 'problematic', 'hided', 'completed'] as $name) { + foreach (['active', 'inactive'] as $name) { // Перебор фильтров статусов // Инициализация значения (приоритет у cookie) @@ -52,7 +58,7 @@ final class market extends core }; } - // Генерация представлениямя + // Генерация представления $main = $this->view->render(DIRECTORY_SEPARATOR . 'pages' . DIRECTORY_SEPARATOR . 'markets.html'); } else $main = $this->authorization(); @@ -78,55 +84,110 @@ final class market extends core if (isset($parameters['page'])) $this->session->write(['markets' => ['page' => $parameters['page']]]); else if (empty($this->session->buffer[$_SERVER['INTERFACE']]['markets']['page'])) $this->session->write(['markets' => ['page' => 1]]); - // Инициализация буфера AQL-выражения для инъекции фильтра по интервалу - $polysemantic = ''; + // Инициализация буферов AQL-выражений для инъекции фильтра по статусам + $filters_statuses_before_and = ''; + $filters_statuses_before_or = ''; + $filters_statuses_after_and = ''; + $filters_statuses_after_or = ''; - // Инициализация допустимых статусов - $statuses = ['active', 'inactive', 'fined', 'decent', 'hided', 'fired']; - - // Инициализация буфера AQL-выражения для инъекции фильтра по статусам (И) - $statuses_and = ''; - - foreach ($statuses as $name) { - // Перебор фильтров статусов (И) + foreach (['active', 'inactive'] as $name) { + // Перебор фильтров статусов // Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue) if (empty($value = $_COOKIE["markets_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['markets']['filters'][$name] ?? 0)) continue; + // Конвертация ярлыков + $converted = match ($name) { + 'inactive' => 'active', + default => $name + }; + + // Принадлежность фильтра + $target = match ($converted) { + 'active' => 'account', + default => 'market' + }; + + // Генерация выражения + $expression = "$target.$converted == " . ($name === $converted ? 'true' : 'false'); + // Генерация AQL-выражения для инъекции в строку запроса - if ($value === '1') $statuses_and .= " && market.$name == true"; + if ($value === '1') ${'filters_statuses_' . ($target === 'account' ? 'before' : 'after') . '_and'} .= " && " . $expression; + else if ($value === '2') ${'filters_statuses_' . ($target === 'account' ? 'before' : 'after') . '_or'} .= " || " . $expression; } // Очистка от бинарных операторов сравнения с только одним операндом (крайние) - $statuses_and = trim(trim(trim($statuses_and), '&&')); - - // Инициализация буфера AQL-выражения для инъекции фильтра по статусам (ИЛИ) - $statuses_or = ''; - - foreach ($statuses as $name) { - // Перебор фильтров статусов (ИЛИ) - - // Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue) - if (empty($value = $_COOKIE["markets_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['markets']['filters'][$name] ?? 0)) continue; - - // Генерация AQL-выражения для инъекции в строку запроса - if ($value === '2') $statuses_or .= " || market.$name == true"; - } - - // Очистка от бинарных операторов сравнения с только одним операндом (крайние) - $statuses_or = trim(trim(trim($statuses_or), '||')); + $filters_statuses_before_and = trim(trim(trim($filters_statuses_before_and), '&&')); + $filters_statuses_before_or = trim(trim(trim($filters_statuses_before_or), '||')); + $filters_statuses_after_and = trim(trim(trim($filters_statuses_after_and), '&&')); + $filters_statuses_after_or = trim(trim(trim($filters_statuses_after_or), '||')); // Инициализация буфера с объёдинёнными буферами c AQL-выражениям "И" и "ИЛИ" - $statuses_merged = (empty($statuses_and) ? '' : "($statuses_and)") . (empty($statuses_or) ? '' : (empty($statuses_and) ? '' : ' || ') . "($statuses_or)"); + $filters_statuses_before_merged = (empty($filters_statuses_before_and) ? '' : "($filters_statuses_before_and)") . (empty($filters_statuses_before_or) ? '' : (empty($filters_statuses_before_and) ? '' : ' || ') . "($filters_statuses_before_or)"); + $filters_statuses_after_merged = (empty($filters_statuses_after_and) ? '' : "($filters_statuses_after_and)") . (empty($filters_statuses_after_or) ? '' : (empty($filters_statuses_after_and) ? '' : ' || ') . "($filters_statuses_after_or)"); // Инициализация общего буфера с AQL-выражениями - $filters = ''; + $filters_before = ''; + $filters_after = ''; - // Объединение фильров в единую строку с AQL-выражениями для инъекции - if (!empty($statuses_merged)) $filters .= empty($filters) ? $statuses_merged : " && ($statuses_merged)"; + // Объединение фильтров в единую строку с AQL-выражениями для инъекции + if (!empty($filters_statuses_before_merged)) $filters_before .= empty($filters_before) ? $filters_statuses_before_merged : " && ($filters_statuses_before_merged)"; + 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'] ?? ''; + if (mb_strlen($search) < 3) $search = null; + $search_query = empty($search) + ? null + : << 5 && LEVENSHTEIN_MATCH(a._key, TOKENS(@search, 'text_en')[0], 2, 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)) + || (LENGTH(@search) > 7 && LEVENSHTEIN_MATCH(a.address, TOKENS(@search, 'text_ru')[0], 2, true)) + || (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(a.city, TOKENS(@search, 'text_ru')[0], 1, true)) + || (LENGTH(@search) > 5 && LEVENSHTEIN_MATCH(a.number, TOKENS(@search, 'text_en')[0], 2, true)) + || (LENGTH(@search) > 5 && LEVENSHTEIN_MATCH(a.mail, TOKENS(@search, 'text_en')[0], 2, true)) + OPTIONS { collections: ["account", "market"] } + AQL; // Инициализация данных для генерации HTML-документа с таблицей - $this->view->rows = registry::markets(before: empty($filters) ? null : "FILTER ($filters)", page: (int) $this->session->buffer[$_SERVER['INTERFACE']]['markets']['page']); + $this->view->rows = registry::markets( + before: sprintf( + <<session->buffer[$_SERVER['INTERFACE']]['markets']['page'], + target: empty($search) ? account::COLLECTION : 'registry_accounts', + binds: empty($search) ? [] : [ + 'search' => $search + ] + ); // Запись в cookie (только таким методом можно записать "hostonly: true") setcookie( @@ -152,6 +213,254 @@ final class market extends core return null; } + /** + * Создать + * + * @param array $parameters Параметры запроса + * + * @return void В буфер вывода JSON-документ с запрашиваемыми параметрами + */ + public function create(array $parameters = []): void + { + if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) { + // Авторизован аккаунт администратора или оператора + + // Инициализация буфера ошибок + $this->errors['account'] ??= []; + + try { + // Проверка наличия переданного пароля + if (!isset($parameters['account_password'])) throw new exception('Не получен пароль'); + else if (strlen($parameters['account_password']) < 6) throw new exception('Пароль должен быть не менее 6 символов'); + else if (!empty($parameters['market_number']) && strlen($parameters['market_number']) < 11) throw new exception('Несоответствие формату SIM-номера представителя'); + 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('Несоответствие формату почты аккаунта представителя'); + + // Универсализация + /* $parameters['market_number'] = (int) $parameters['market_number']; */ + /* $parameters['account_number'] = (int) $parameters['account_number']; */ + + // Создание аккаунта + $account = account::create( + data: [ + 'type' => 'market', + 'name' => [ + 'first' => $parameters['account_name_first'], + 'second' => $parameters['account_name_second'], + 'last' => $parameters['account_name_last'] + ], + 'number' => $parameters['account_number'] === 0 ? '' : $parameters['account_number'], + 'mail' => $parameters['account_mail'], + 'password' => sodium_crypto_pwhash_str( + $parameters['account_password'], + SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE + ), + 'commentary' => $parameters['account_commentary'] + ], + errors: $this->errors['account'] + ); + + // Проверка существования созданного аккаунта + if (empty($account)) 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() + ]; + } + + // Инициализация идентификатора аккаунта (ключ документа инстанции аккаунта в базе данных) + $_key = preg_replace('/.+\//', '', $account ?? ''); + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'clipboard' => empty($this->errors['account']) ? << self::parse_only_text($this->errors['account']) + ] + ); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + + try { + // Создание магазина + $market = model::create( + data: [ + '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'], + ], + errors: $this->errors['account'] + ); + + // Проверка существования созданного магазина + if (empty($market)) throw new exception('Не удалось создать магазин'); + + // Создание ребра: account -> market + account::connect($account, $market, 'market', $this->errors['account']); + } catch (exception $e) { + // Write to the errors registry + $this->errors['account'][] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + } + } + } + + /** + * Прочитать данные + * + * @param array $parameters Параметры запроса + */ + public function fields(array $parameters = []): ?string + { + if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) { + // Авторизован аккаунт администратора или оператора + + // Инициализация данных магазина + $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(); + + if (!empty($market)) { + // Найдены данные магазина + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode($market); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } + } + + // Возврат (провал) + return null; + } + + /** + * Обновить данные + * + * @param array $parameters Параметры запроса + */ + public function update(array $parameters = []): ?string + { + if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) { + // Авторизован аккаунт администратора или оператора + + // Инициализация данных магазина + $market = model::read('d._key == "' . $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 (_core::update($market)) { + // Записаны данные магазина + + // Инициализация строки в глобальную переменную шаблонизатора + $this->view->rows = registry::markets( + before: sprintf( + "FILTER a._id == '%s' && a.deleted != true", + $market->getId() + ), + after: <<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('Не удалось найти аккаунт'); + } + + // Возврат (провал) + return null; + } + /** * Прочитать данные магазинов для * @@ -163,7 +472,7 @@ final class market extends core // Авторизован аккаунт оператора или магазина // Инициализация данных магазинов - $this->view->markets = model::read(filter: 'd.status == "active"', amount: 10000, return: '{ id: d.id, director: d.director }'); + $this->view->markets = model::read(filter: 'd.active == true', amount: 10000, return: '{ _key: d._key, name: d.name }'); // Универсализация if ($this->view->markets instanceof _document) $this->view->markets = [$this->view->markets]; diff --git a/mirzaev/ebala/system/controllers/operator.php b/mirzaev/ebala/system/controllers/operator.php index 8636f4a..cd15289 100755 --- a/mirzaev/ebala/system/controllers/operator.php +++ b/mirzaev/ebala/system/controllers/operator.php @@ -7,10 +7,11 @@ namespace mirzaev\ebala\controllers; // Файлы проекта use mirzaev\ebala\controllers\core, mirzaev\ebala\controllers\traits\errors, + mirzaev\ebala\models\account, mirzaev\ebala\models\registry; -// Библиотека для ArangoDB -use ArangoDBClient\Document as _document; +// Встроенные библиотеки +use exception; /** * Контроллер оператора @@ -33,7 +34,7 @@ final class operator extends core if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) { // Авторизован аккаунт оператора или администратора - foreach (['confirmed', 'waiting', 'published', 'unpublished', 'problematic', 'hided', 'completed'] as $name) { + foreach (['confirmed', 'waiting'] as $name) { // Перебор фильтров статусов // Инициализация значения (приоритет у cookie) @@ -77,55 +78,81 @@ final class operator extends core if (isset($parameters['page'])) $this->session->write(['operators' => ['page' => $parameters['page']]]); else if (empty($this->session->buffer[$_SERVER['INTERFACE']]['operators']['page'])) $this->session->write(['operators' => ['page' => 1]]); - // Инициализация буфера AQL-выражения для инъекции фильтра по интервалу - $polysemantic = ''; + // Инициализация буферов AQL-выражений для инъекции фильтра по статусам + $filters_statuses_and = ''; + $filters_statuses_or = ''; - // Инициализация допустимых статусов - $statuses = ['active', 'inactive', 'fined', 'decent', 'hided', 'fired']; - - // Инициализация буфера AQL-выражения для инъекции фильтра по статусам (И) - $statuses_and = ''; - - foreach ($statuses as $name) { + foreach (['active', 'inactive'] as $name) { // Перебор фильтров статусов (И) // Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue) if (empty($value = $_COOKIE["operators_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['operators']['filters'][$name] ?? 0)) continue; + // Конвертация ярлыков + $converted = match ($name) { + 'inactive' => 'active', + default => $name + }; + + // Генерация выражения + $expression = "account.$converted == " . ($name === $converted ? 'true' : 'false'); + // Генерация AQL-выражения для инъекции в строку запроса - if ($value === '1') $statuses_and .= " && operator.$name == true"; + if ($value === '1') $filters_statuses_and .= " && " . $expression; + else if ($value === '2') $filters_statuses_or .= " || " . $expression; } // Очистка от бинарных операторов сравнения с только одним операндом (крайние) - $statuses_and = trim(trim(trim($statuses_and), '&&')); - - // Инициализация буфера AQL-выражения для инъекции фильтра по статусам (ИЛИ) - $statuses_or = ''; - - foreach ($statuses as $name) { - // Перебор фильтров статусов (ИЛИ) - - // Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue) - if (empty($value = $_COOKIE["operators_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['operators']['filters'][$name] ?? 0)) continue; - - // Генерация AQL-выражения для инъекции в строку запроса - if ($value === '2') $statuses_or .= " || operator.$name == true"; - } - - // Очистка от бинарных операторов сравнения с только одним операндом (крайние) - $statuses_or = trim(trim(trim($statuses_or), '||')); + $filters_statuses_and = trim(trim(trim($filters_statuses_and), '&&')); + $filters_statuses_or = trim(trim(trim($filters_statuses_or), '||')); // Инициализация буфера с объёдинёнными буферами c AQL-выражениям "И" и "ИЛИ" - $statuses_merged = (empty($statuses_and) ? '' : "($statuses_and)") . (empty($statuses_or) ? '' : (empty($statuses_and) ? '' : ' || ') . "($statuses_or)"); + $filters_statuses_merged = (empty($filters_statuses_and) ? '' : "($filters_statuses_and)") . (empty($filters_statuses_or) ? '' : (empty($filters_statuses_and) ? '' : ' || ') . "($filters_statuses_or)"); // Инициализация общего буфера с AQL-выражениями $filters = ''; - // Объединение фильров в единую строку с AQL-выражениями для инъекции - if (!empty($statuses_merged)) $filters .= empty($filters) ? $statuses_merged : " && ($statuses_merged)"; + // Объединение фильтров в единую строку с AQL-выражениями для инъекции + 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'] ?? ''; + if (mb_strlen($search) < 3) $search = null; + $search_query = empty($search) + ? null + : << 6 && LEVENSHTEIN_MATCH(account._key, TOKENS(@search, 'text_en')[0], 2, true)) + || (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(account.name.first, TOKENS(@search, 'text_ru')[0], 2, true)) + || (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(account.name.second, TOKENS(@search, 'text_ru')[0], 2, true)) + || (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(account.name.last, TOKENS(@search, 'text_ru')[0], 2, true)) + || (LENGTH(@search) > 6 && LEVENSHTEIN_MATCH(account.number, TOKENS(@search, 'text_en')[0], 2, true)) + || (LENGTH(@search) > 6 && LEVENSHTEIN_MATCH(account.mail, TOKENS(@search, 'text_en')[0], 2, true)) + AQL; // Инициализация данных для генерации HTML-документа с таблицей - $this->view->rows = registry::operators(before: empty($filters) ? null : "FILTER ($filters)", page: (int) $this->session->buffer[$_SERVER['INTERFACE']]['operators']['page']); + $this->view->rows = registry::operators( + before: sprintf( + <<session->buffer[$_SERVER['INTERFACE']]['operators']['page'], + target: empty($search) ? account::COLLECTION : 'registry_accounts', + binds: empty($search) ? [] : [ + 'search' => $search + ] + ); // Запись в cookie (только таким методом можно записать "hostonly: true") setcookie( @@ -150,4 +177,93 @@ final class operator extends core // Возврат (провал) return null; } + + /** + * Создать + * + * @param array $parameters Параметры запроса + * + * @return void В буфер вывода JSON-документ с запрашиваемыми параметрами + */ + public function create(array $parameters = []): void + { + if ($this->account->status() && $this->account->type === 'administrator') { + // Авторизован аккаунт администратора + + // Инициализация буфера ошибок + $this->errors['account'] ??= []; + + try { + // Проверка наличия переданного пароля + if (!isset($parameters['password'])) throw new exception('Не получен пароль'); + else if (strlen($parameters['password']) < 6) throw new exception('Пароль должен быть не менее 6 символов'); + else if (!empty($parameters['number']) && strlen($parameters['number']) < 11) throw new exception('Несоответствие формату SIM-номера'); + else if (!empty($parameters['mail']) && preg_match('/^.+@.+\.\w+$/', $parameters['mail']) === 0) throw new exception('Несоответствие формату почты'); + + // Универсализация + /* $parameters['number'] = (int) $parameters['number']; */ + + // Создание аккаунта + $account = account::create( + data: [ + 'type' => 'operator', + 'name' => [ + 'first' => $parameters['name_first'], + 'second' => $parameters['name_second'], + 'last' => $parameters['name_last'] + ], + 'number' => $parameters['number'] === 0 ? '' : $parameters['number'], + 'mail' => $parameters['mail'], + 'password' => sodium_crypto_pwhash_str( + $parameters['password'], + SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE + ), + 'commentary' => $parameters['commentary'] + ], + errors: $this->errors['account'] + ); + + // Проверка существования созданного аккаунта + if (empty($account)) 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() + ]; + } + + // Инициализация идентификатора аккаунта (ключ документа инстанции аккаунта в базе данных) + $_key = preg_replace('/.+\//', '', $account ?? ''); + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'clipboard' => empty($this->errors['account']) ? << self::parse_only_text($this->errors['account']) + ] + ); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } + } } diff --git a/mirzaev/ebala/system/controllers/session.php b/mirzaev/ebala/system/controllers/session.php index 2805bf4..29ab9ea 100755 --- a/mirzaev/ebala/system/controllers/session.php +++ b/mirzaev/ebala/system/controllers/session.php @@ -55,11 +55,11 @@ final class session extends core $parameters['worker'] = isset($matches[1]) ? 7 . $matches[1] : $parameters['worker']; // Вычисление длины - $length = strlen($parameters['worker']); + $length = mb_strlen($parameters['worker']); // Проверка параметров на соответствование требованиям if ($length === 0) throw new exception('Номер не может быть пустым'); - if ($length != 11) throw new exception('Номер должен иметь 11 цифр'); + if ($length !== 11) throw new exception('Номер должен иметь 11 цифр'); if (preg_match_all('/[^\d\(\)\-\s\r\n\t\0]+/u', $parameters['worker'], $matches) > 0) throw new exception('Нельзя использовать символы: ' . implode(', ', ...$matches)); if ($remember = isset($parameters['remember']) && $parameters['remember'] === '1') { @@ -157,11 +157,11 @@ final class session extends core $parameters['administrator'] = $matches[0]; // Вычисление длины - $length = strlen($parameters['administrator']); + $length = mb_strlen($parameters['administrator']); // Проверка параметров на соответствование требованиям - if ($length === 0) throw new exception('Идентификатор не может быть пустым'); - if ($length > 12) throw new exception('Идентификатор должен иметь не более 12 цифр'); + if ($length === 0) throw new exception('Идентификатор аккаунта не может быть пустым'); + if ($length > 12) throw new exception('Идентификатор аккаунта должен иметь не более 12 цифр'); if (preg_match_all('/[^\d\(\)\-\s\r\n\t\0]+/u', $parameters['administrator'], $matches) > 0) throw new exception('Нельзя использовать символы: ' . implode(', ', ...$matches)); if ($remember = isset($parameters['remember']) && $parameters['remember'] === '1') { @@ -263,11 +263,11 @@ final class session extends core $parameters['operator'] = $matches[0]; // Вычисление длины - $length = strlen($parameters['operator']); + $length = mb_strlen($parameters['operator']); // Проверка параметров на соответствование требованиям - if ($length === 0) throw new exception('Идентификатор не может быть пустым'); - if ($length > 12) throw new exception('Идентификатор должен иметь не более 12 цифр'); + if ($length === 0) throw new exception('Идентификатор аккаунта не может быть пустым'); + if ($length > 12) throw new exception('Идентификатор аккаунта должен иметь не более 12 цифр'); if (preg_match_all('/[^\d\(\)\-\s\r\n\t\0]+/u', $parameters['operator'], $matches) > 0) throw new exception('Нельзя использовать символы: ' . implode(', ', ...$matches)); if ($remember = isset($parameters['remember']) && $parameters['remember'] === '1') { @@ -359,19 +359,18 @@ final class session extends core if (empty($parameters['market'])) throw new exception('Необходимо передать идентификатор'); // Вычисление длины - $length = strlen($parameters['market']); + $length = mb_strlen($parameters['market']); // Проверка параметров на соответствование требованиям - if ($parameters['market'][0] !== 'K') throw new exception('Идентификатор должен начинаться с английской буквы "K"'); - if ($length <= 1) throw new exception('Идентификатор не может быть пустым'); - if ($length > 40) throw new exception('Идентификатор должен иметь не более 40 символов'); - if (preg_match_all('/[^\dK]+/u', $parameters['market'], $matches) > 0) throw new exception('Нельзя использовать символы: ' . implode(', ', ...$matches)); + 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 ($remember = isset($parameters['remember']) && $parameters['remember'] === '1') { // Запрошено запоминание // Запись в cookie - setcookie('entry_id', $parameters['market'], [ + setcookie('entry__key', $parameters['market'], [ 'expires' => strtotime('+1 day'), 'path' => '/', 'secure' => true, @@ -380,18 +379,15 @@ final class session extends core ]); } - // Поиск магазина - $market = market::read('d.id == "' . $parameters['market'] . '"', amount: 1); - // Поиск аккаунта - $account = market::account($market->getId()); + $account = account::read('d._key == "' . $parameters['market'] . '"', amount: 1); // Генерация ответа по запрашиваемым параметрам foreach ($return as $parameter) match ($parameter) { 'exist' => $buffer['exist'] = isset($account), 'account' => (function () use ($parameters, $remember, &$buffer) { // Запись в буфер сессии - if ($remember) $this->session->write(['entry' => ['id' => $parameters['market']]], $this->errors); + if ($remember) $this->session->write(['entry' => ['_key' => $parameters['market']]], $this->errors); // Поиск аккаунта и запись в буфер вывода $buffer['account'] = (new account($this->session, 'market', $this->errors))?->instance() instanceof _document; @@ -433,7 +429,7 @@ final class session extends core // Запись в буфер сессии if (!in_array('account', $return, true) && ($remember ?? false)) - $this->session->write(['entry' => ['id' => $parameters['market']]]); + $this->session->write(['entry' => ['_key' => $parameters['market']]]); } /** diff --git a/mirzaev/ebala/system/controllers/task.php b/mirzaev/ebala/system/controllers/task.php index bb422d4..0a54940 100755 --- a/mirzaev/ebala/system/controllers/task.php +++ b/mirzaev/ebala/system/controllers/task.php @@ -18,6 +18,7 @@ use ArangoDBClient\Document as _document; // System libraries use datetime, + datetimezone, exception; /** @@ -39,17 +40,49 @@ final class task extends core */ public function create(array $parameters = []): void { - if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'market')) { - // Авторизован аккаунт администратора, оператора или магазина + try { + if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'market')) { + // Авторизован аккаунт администратора, оператора или магазина - // Инициализация буфера ошибок - $this->errors['tasks'] ??= []; + // Инициализация буфера ошибок + $this->errors['tasks'] ??= []; - // Создание строк - for ($i = 0, $parameters['cashiers'] = (int) $parameters['cashiers']; $i < $parameters['cashiers']; ++$i) model::create(work: 'Кассир', market: $this->account->type === 'market' ? $this->account->id : null, start: $parameters['start'], end: $parameters['end'], date: $parameters['date'], errors: $this->errors['tasks']); - for ($i = 0, $parameters['displayers'] = (int) $parameters['displayers']; $i < $parameters['displayers']; ++$i) model::create(work: 'Выкладчик', market: $this->account->type === 'market' ? $this->account->id : null, start: $parameters['start'], end: $parameters['end'], date: $parameters['date'], errors: $this->errors['tasks']); - for ($i = 0, $parameters['loaders'] = (int) $parameters['loaders']; $i < $parameters['loaders']; ++$i) model::create(work: 'Грузчик', market: $this->account->type === 'market' ? $this->account->id : null, start: $parameters['start'], end: $parameters['end'], date: $parameters['date'], errors: $this->errors['tasks']); - for ($i = 0, $parameters['gastronomes'] = (int) $parameters['gastronomes']; $i < $parameters['gastronomes']; ++$i) model::create(work: 'Гастроном', market: $this->account->type === 'market' ? $this->account->id : null, start: $parameters['start'], end: $parameters['end'], date: $parameters['date'], errors: $this->errors['tasks']); + // Создание строк + for ($i = 0, $parameters['cashiers'] = (int) $parameters['cashiers']; $i < $parameters['cashiers']; ++$i) model::create(work: 'Кассир', market: $this->account->type === 'market' ? account::market($this->account->getId())?->getKey() : null, start: $parameters['start'], end: $parameters['end'], date: $parameters['date'], errors: $this->errors['tasks']); + for ($i = 0, $parameters['displayers'] = (int) $parameters['displayers']; $i < $parameters['displayers']; ++$i) model::create(work: 'Выкладчик', market: $this->account->type === 'market' ? account::market($this->account->getId())?->getKey() : null, start: $parameters['start'], end: $parameters['end'], date: $parameters['date'], errors: $this->errors['tasks']); + for ($i = 0, $parameters['loaders'] = (int) $parameters['loaders']; $i < $parameters['loaders']; ++$i) model::create(work: 'Грузчик', market: $this->account->type === 'market' ? account::market($this->account->getId())?->getKey() : null, start: $parameters['start'], end: $parameters['end'], date: $parameters['date'], errors: $this->errors['tasks']); + for ($i = 0, $parameters['gastronomes'] = (int) $parameters['gastronomes']; $i < $parameters['gastronomes']; ++$i) model::create(work: 'Гастроном', market: $this->account->type === 'market' ? account::market($this->account->getId())?->getKey() : null, start: $parameters['start'], end: $parameters['end'], date: $parameters['date'], errors: $this->errors['tasks']); + + // Запись заголовков ответа + 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(); + } else throw new exception('Вы не авторизованы'); + } catch (exception $e) { + // Запись в реестр ошибок + $this->errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; // Запись заголовков ответа header('Content-Type: application/json'); @@ -79,162 +112,278 @@ final class task extends core * Прочитать * * @param array $parameters Параметры запроса + * + * @return void В буфер вывода JSON-документ с запрашиваемыми параметрами */ - public function read(array $parameters = []): ?string + public function read(array $parameters = []): void { - if ($this->account->status()) { - // Авторизован аккаунт + try { + if ($this->account->status()) { + // Авторизован аккаунт - // Инициализация буфера AQL-выражения для инъекции фильтра по интервалу - $interval = ''; + // Инициализация буфера AQL-выражения для инъекции фильтра по интервалу + $interval = ''; - foreach (['from', 'to'] as $name) { - // Перебор фильтров временного промежутка (И) + foreach (['from', 'to'] as $name) { + // Перебор фильтров временного промежутка (И) - // Инициализация значения (приоритет у cookie) - if (empty($value = (int) ($_COOKIE["tasks_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? (($name === 'from') ? time() : strtotime('+1 month'))))) continue; + // Инициализация значения (приоритет у cookie) + if (empty($value = (int) ($_COOKIE["tasks_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? (($name === 'from') ? time() : strtotime('+1 month'))))) continue; - // Генерация AQL-выражения для инъекции в строку запроса - if ($name === 'from') $interval .= " && task.date >= $value"; - else if ($name === 'to') $interval .= " && task.date <= $value"; - } + // Генерация AQL-выражения для инъекции в строку запроса + if ($name === 'from') $interval .= " && task.date >= $value"; + else if ($name === 'to') $interval .= " && task.date <= $value"; + } - // Очистка от бинарных операторов сравнения с только одним операндом (крайние) - $interval = trim(trim(trim($interval), '&&')); + // Очистка от бинарных операторов сравнения с только одним операндом (крайние) + $interval = trim(trim(trim($interval), '&&')); - // Инициализация буфера AQL-выражения для инъекции фильтра по интервалу - $polysemantic = ''; + // Инициализация буфера AQL-выражения для инъекции фильтра по интервалу + $polysemantic = ''; - foreach (['rating'] as $name) { - // Перебор фильтров с произвольными значениями (И) + foreach (['rating'] as $name) { + // Перебор фильтров с произвольными значениями (И) - // Инициализация значения (приоритет у cookie) - $value = $_COOKIE["tasks_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? null; + // Инициализация значения (приоритет у cookie) + $value = $_COOKIE["tasks_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? null; - // Найдено значение? - if ($value === null) continue; + // Найдено значение? + if ($value === null) continue; - // Генерация AQL-выражения для инъекции в строку запроса - if ($name === 'rating' && $value > 0) $polysemantic .= " && task.rating >= $value"; - } + // Генерация AQL-выражения для инъекции в строку запроса + if ($name === 'rating' && $value > 0) $polysemantic .= " && task.rating >= $value"; + } - // Очистка от бинарных операторов сравнения с только одним операндом (крайние) - $polysemantic = trim(trim(trim($polysemantic), '&&')); + // Очистка от бинарных операторов сравнения с только одним операндом (крайние) + $polysemantic = trim(trim(trim($polysemantic), '&&')); + // Инициализация буферов AQL-выражений для инъекции фильтра по статусам + $statuses_and = ''; + $statuses_or = ''; - // Инициализация допустимых статусов - $statuses = ['confirmed', 'waiting', 'published', 'unpublished', 'problematic', 'hided', 'completed']; + foreach (['confirmed', 'waiting', 'published', 'unpublished', 'problematic', 'hided', 'completed'] as $name) { + // Перебор фильтров по статусам - // Инициализация буферов AQL-выражений для инъекции фильтра по статусам - $statuses_and = ''; - $statuses_or = ''; + // Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue) + if (empty($value = $_COOKIE["tasks_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? 0)) continue; - foreach ($statuses as $name) { - // Перебор фильтров статусов (И) + // Конвертация ярлыков + $converted = match ($name) { + 'waiting' => 'confirmed', + 'unpublished' => 'published', + default => $name + }; - // Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue) - if (empty($value = $_COOKIE["tasks_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? 0)) continue; + // Генерация выражения + $expression = "task.$converted == " . ($name === 'unpublished' || $name === 'waiting' ? 'false' : 'true'); - // Конвертация ярлыков - $converted = match ($name) { - 'waiting' => 'confirmed', - 'unpublished' => 'published', - default => $name + // Генерация AQL-выражения для инъекции в строку запроса + if ($value === '1') $statuses_and .= " && " . $expression; + else if ($value === '2') $statuses_or .= " || " . $expression; + } + + // Очистка от бинарных операторов сравнения с только одним операндом (крайние) + $statuses_and = trim(trim(trim($statuses_and), '&&')); + $statuses_or = trim(trim(trim($statuses_or), '||')); + + // Инициализация буфера с объёдинёнными буферами c AQL-выражениям "И" и "ИЛИ" + $statuses_merged = (empty($statuses_and) ? '' : "($statuses_and)") . (empty($statuses_or) ? '' : (empty($statuses_and) ? '' : ' || ') . "($statuses_or)"); + + // Инициализация общего буфера с AQL-выражениями + $filters = ''; + + // Объединение фильров в единую строку с AQL-выражениями для инъекции + if (!empty($interval)) $filters .= $interval; + if (!empty($statuses_merged)) $filters .= empty($filters) ? $statuses_merged : " && ($statuses_merged)"; + if (!empty($polysemantic)) $filters .= empty($filters) ? $polysemantic : " && $polysemantic"; + + if (isset($parameters['row'])) { + // Запрошена строка + + // Добавление идентификатора строки в фильтр + $filters .= ' && task._key == "' . $parameters['row'] . '"'; + + // Инициализация данных для генерации HTML-документа с таблицей + if ($_SERVER['INTERFACE'] === 'worker') + $this->view->rows = model::list(before: 'FILTER task.worker == "' . account::worker($this->account->getId())?->getKey() . '"' . " && ($filters)"); + else if ($_SERVER['INTERFACE'] === 'operator') + $this->view->rows = model::list(before: "FILTER ($filters)"); + else if ($_SERVER['INTERFACE'] === 'market') + $this->view->rows = model::list(before: 'FILTER task.market == "' . account::market($this->account->getId())?->getKey() . '"' . " && ($filters)"); + else if ($_SERVER['INTERFACE'] === 'administrator') + $this->view->rows = model::list(before: "FILTER ($filters)"); + else $this->view->rows = []; + } else { + // Запрошена страница (множество строк) + + // Реинициализация номера актуальной страницы + if (isset($parameters['page'])) $this->session->write(['tasks' => ['page' => $parameters['page']]]); + else if (empty($this->session->buffer[$_SERVER['INTERFACE']]['tasks']['page'])) $this->session->write(['tasks' => ['page' => 1]]); + + // Инициализация строки поиска + $search = $_COOKIE["tasks_filter_search"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters']['search'] ?? ''; + if (mb_strlen($search) < 3) $search = null; + $search_query = empty($search) + ? null + : << 6 && LEVENSHTEIN_MATCH(task._key, TOKENS(@search, 'text_en')[0], 2, true)) + || (LENGTH(@search) > 6 && LEVENSHTEIN_MATCH(task.worker, TOKENS(@search, 'text_en')[0], 2, true)) + || (LENGTH(@search) > 6 && LEVENSHTEIN_MATCH(task.market, TOKENS(@search, 'text_en')[0], 2, true)) +AQL; + + // Инициализация данных для генерации HTML-документа с таблицей + if ($_SERVER['INTERFACE'] === 'worker') + $this->view->rows = model::list( + before: sprintf( + <<session->buffer['worker']['tasks']['page'], + target: empty($search) ? model::COLLECTION : 'registry_tasks', + binds: ['worker' => account::worker($this->account->getId())?->getKey()] + (empty($search) ? [] : ['search' => $search]) + ); + else if ($_SERVER['INTERFACE'] === 'market') + $this->view->rows = model::list( + before: sprintf( + <<session->buffer['market']['tasks']['page'], + target: empty($search) ? model::COLLECTION : 'registry_tasks', + binds: ['market' => account::market($this->account->getId())?->getKey()] + (empty($search) ? [] : ['search' => $search]) + ); + else if ($_SERVER['INTERFACE'] === 'operator') + $this->view->rows = model::list( + before: sprintf( + <<session->buffer['operator']['tasks']['page'], + target: empty($search) ? model::COLLECTION : 'registry_tasks', + binds: empty($search) ? [] : [ + 'search' => $search + ] + ); + else if ($_SERVER['INTERFACE'] === 'administrator') + $this->view->rows = model::list( + before: sprintf( + <<session->buffer['administrator']['tasks']['page'], + target: empty($search) ? model::COLLECTION : 'registry_tasks', + binds: empty($search) ? [] : [ + 'search' => $search + ] + ); + else $this->view->rows = []; + + // Запись в cookie (только таким методом можно записать "hostonly: true") + setcookie( + 'tasks_page', + (string) $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['page'], + [ + 'expires' => strtotime('+1 hour'), + 'path' => '/', + 'secure' => true, + 'httponly' => false, + 'samesite' => 'strict' + ] + ); + + // Запись в глобальную переменную шаблонизатора обрабатываемой страницы + $this->view->page = $parameters['page']; }; - // Генерация выражения - $expression = "task.$converted == " . ($name === 'unpublished' || $name === 'waiting' ? 'false' : 'true'); + // Предобработка строк перед генерацией документа + $this->view->rows = static::preprocessing($this->account, $this->view->rows); - // Генерация AQL-выражения для инъекции в строку запроса - if ($value === '1') $statuses_and .= " && " . $expression; - else if ($value === '2') $statuses_or .= " || " . $expression; - } + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); - // Очистка от бинарных операторов сравнения с только одним операндом (крайние) - $statuses_and = trim(trim(trim($statuses_and), '&&')); - $statuses_or = trim(trim(trim($statuses_or), '||')); + // Инициализация буфера вывода + ob_start(); - // Инициализация буфера с объёдинёнными буферами c AQL-выражениям "И" и "ИЛИ" - $statuses_merged = (empty($statuses_and) ? '' : "($statuses_and)") . (empty($statuses_or) ? '' : (empty($statuses_and) ? '' : ' || ') . "($statuses_or)"); - - // Инициализация общего буфера с AQL-выражениями - $filters = ''; - - // Объединение фильров в единую строку с AQL-выражениями для инъекции - if (!empty($interval)) $filters .= $interval; - if (!empty($statuses_merged)) $filters .= empty($filters) ? $statuses_merged : " && ($statuses_merged)"; - if (!empty($polysemantic)) $filters .= empty($filters) ? $polysemantic : " && $polysemantic"; - - if (isset($parameters['row'])) { - // Запрошена строка - - // Добавление идентификатора строки в фильтр - $filters .= ' && task._key == "' . $parameters['row'] . '"'; - - // Инициализация данных для генерации HTML-документа с таблицей - if ($_SERVER['INTERFACE'] === 'worker') - $this->view->rows = model::list(before: 'FILTER task.worker == "' . account::worker($this->account->number)?->id . '"' . " && ($filters)"); - else if ($_SERVER['INTERFACE'] === 'operator') - $this->view->rows = model::list(before: "FILTER ($filters)"); - else if ($_SERVER['INTERFACE'] === 'market') - $this->view->rows = model::list(before: 'FILTER task.market == "' . $this->market->id . '"' . " && ($filters)"); - else if ($_SERVER['INTERFACE'] === 'administrator') - $this->view->rows = model::list(before: "FILTER ($filters)"); - else $this->view->rows = []; - } else { - // Запрошена страница (множество строк) - - // Реинициализация актуальной страницы - if (isset($parameters['page'])) $this->session->write(['tasks' => ['page' => $parameters['page']]]); - else if (empty($this->session->buffer[$_SERVER['INTERFACE']]['tasks']['page'])) $this->session->write(['tasks' => ['page' => 1]]); - - // Инициализация данных для генерации HTML-документа с таблицей - if ($_SERVER['INTERFACE'] === 'worker') - $this->view->rows = model::list(before: 'FILTER task.worker == "' . account::worker($this->account->number)?->id . '"' . empty($filters) ? null : " && ($filters)", page: (int) $this->session->buffer['worker']['tasks']['page']); - else if ($_SERVER['INTERFACE'] === 'operator') - $this->view->rows = model::list(before: empty($filters) ? null : "FILTER ($filters)", page: (int) $this->session->buffer['operator']['tasks']['page']); - else if ($_SERVER['INTERFACE'] === 'market') - $this->view->rows = model::list(before: 'FILTER task.market == "' . $this->market->id . '"' . empty($filters) ? null : " && ($filters)", page: (int) $this->session->buffer['market']['tasks']['page']); - else if ($_SERVER['INTERFACE'] === 'administrator') - $this->view->rows = model::list(before: empty($filters) ? null : "FILTER ($filters)", page: (int) $this->session->buffer['administrator']['tasks']['page']); - else $this->view->rows = []; - - // Запись в cookie (только таким методом можно записать "hostonly: true") - setcookie( - 'tasks_page', - (string) $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['page'], + // Генерация ответа + echo json_encode( [ - 'expires' => strtotime('+1 hour'), - 'path' => '/', - 'secure' => true, - 'httponly' => false, - 'samesite' => 'strict' + 'rows' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'), + 'errors' => self::parse_only_text($this->errors) ] ); - // Запись в глобальную переменную шаблонизатора обрабатываемой страницы - $this->view->page = $parameters['page']; - }; + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); - // Предобработка строк перед генерацией документа - $this->view->rows = static::preprocessing($this->view->rows); + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } else throw new exception('Вы не авторизованы'); + } catch (exception $e) { + // Запись в реестр ошибок + $this->errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; - // Инициализация блока - return $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'); + // Запись заголовков ответа + 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(); } - - // Возврат (провал) - return null; } /** * Предобработка строк перед генерацией документа * + * @param account $account Аккаунт * @param array $rows Строки * * @return array Обработанные строки */ - public static function preprocessing(array $rows): array + protected static function preprocessing(account $account, array $rows): array { // Инициализация буфера прочитанных из базы данных строк $buffer = $rows; @@ -245,9 +394,38 @@ final class task extends core // Инициализация ярлыка $link = &$buffer[$number]; - // Конвертация времён - if (!empty($row->task['start'])) $link->task = ['start' => datetime::createFromFormat('H:i', (string) $row->task['start'])] + $link->task; - if (!empty($row->task['end'])) $link->task = ['end' => datetime::createFromFormat('H:i', (string) $row->task['end'])] + $link->task; + if (!empty($row->task['start'])) { + // Найдено начало + + // Инициализация даты + $date = (new DateTime('@' . $row->task['date']))->setTimezone(new DateTimeZone('Asia/Krasnoyarsk')); + + // Инициализация времени + $start = datetime::createFromFormat('H:i', (string) $row->task['start']); + + // Перенос времени в дату + $start = $date->setTime((int) $start->format('H'), (int) $start->format('i')); + + + // Запись в буфер + $link->task = ['start' => $start] + $link->task; + } + + if (!empty($row->task['end'])) { + // Найден конец + + // Инициализация даты + $date = (new DateTime('@' . $row->task['date']))->setTimezone(new DateTimeZone('Asia/Krasnoyarsk')); + + // Инициализация времени + $end = datetime::createFromFormat('H:i', (string) $row->task['end']); + + // Перенос времени в дату + $end = $date->setTime((int) $end->format('H'), (int) $end->format('i')); + + // Запись в буфер + $link->task = ['end' => $end] + $link->task; + } // Инициализация буфера сгенерированных данных работы для шаблонизатора $generated = []; @@ -265,6 +443,41 @@ final class task extends core if ($link->task['start'] instanceof datetime) $generated['start'] = $link->task['start']->format('H:i'); if ($link->task['end'] instanceof datetime) $generated['end'] = $link->task['end']->format('H:i'); + // Инициализация счётчика непрочитанных сообщений + $generated['chat'] = [ + 'unreaded' => 0 + ]; + + if ($account->type === 'worker') { + // Оператор или администратор + + foreach ($link->task['chats']['worker'] ?? [] as $message) { + // Перебор сообщений из чата: СОТРУДНИК <-> ОПЕРАТОР + + // Подсчёт непрочитанных сообщений + if (!array_key_exists((string) $account->getKey(), $message['readed'] ?? [])) ++$generated['chat']['unreaded']; + } + } else if ($account->type === 'market') { + // Оператор или администратор + + foreach ($link->task['chats']['market'] ?? [] as $message) { + // Перебор сообщений из чата: МАГАЗИН <-> ОПЕРАТОР + + // Подсчёт непрочитанных сообщений + if (!array_key_exists((string) $account->getKey(), $message['readed'] ?? [])) ++$generated['chat']['unreaded']; + } + } else if ($account->type === 'operator' || $account->type === 'administrator') { + // Оператор или администратор + + foreach ($link->task['chats'] ?? [] as $chat) + foreach ($chat as $message) { + // Перебор сообщений из всех чатов + + // Подсчёт непрочитанных сообщений + if (!array_key_exists((string) $account->getKey(), $message['readed'] ?? [])) ++$generated['chat']['unreaded']; + } + } + // Запись из буфера сгенерированных данных работы для шаблонизатора в буфер переменных окружения шаблонизатора $link->task = ['generated' => $generated] + $link->task; } @@ -276,6 +489,8 @@ final class task extends core /** * Заменить сотрудника или магазин * + * @param array $parameters Параметры запроса + * * @return void В буфер вывода JSON-документ с запрашиваемыми параметрами */ public function update(array $parameters = []): void @@ -285,8 +500,21 @@ final class task extends core // Авторизован аккаунт администратора, оператора или магазина if (($task = model::read('d._key == "' . $parameters['task'] . '"', amount: 1)) instanceof _document) { + // Найдена заявка + + // Заявка не принадлежит запросившему магазину? + if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->getKey()) + throw new exception('Вы не авторизованы для редактирования этой заявки'); + + // Заявка подтверждена? + if ($task->confirmed) throw new exception('Запрещено редактировать подтверждённую заявку'); + + // Заявка завершена? + if ($this->account->type === 'market' && $task->completed) throw new exception('Запрещено редактировать завершённую заявку'); + if (!empty($parameters['worker'])) { // Передан сотрудник + if ($parameters['worker'] === 'delete') { // Удалить сотрудника @@ -297,7 +525,7 @@ final class task extends core // Записано изменение в базу данных // Инициализация строки в глобальную переменную шаблонизатора - $this->view->rows = static::preprocessing(model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); + $this->view->rows = static::preprocessing($this->account, model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); // Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение) $this->view->page = null; @@ -329,20 +557,23 @@ final class task extends core } else { // Записать нового сотрудника - if (($worker = worker::read('d.id == "' . $parameters['worker'] . '" && d.status == "active"', amount: 1)) instanceof _document) { + if (($worker = worker::read('d._key == "' . $parameters['worker'] . '" && d.active == true', amount: 1)) instanceof _document) { // Найден сотрудник (запрашиваемый для записи сотрудник существует в базе данных) if ($task->worker !== $parameters['worker']) { // Идентификатор запрашиваемого сотрудника не равен актуальному // Запись сотрудника - $task->worker = $worker->id; + $task->worker = $worker->getKey(); + + // Снятие с публикации + $task->published = false; if (_core::update($task)) { // Записано изменение в базу данных // Инициализация строки в глобальную переменную шаблонизатора - $this->view->rows = static::preprocessing(model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); + $this->view->rows = static::preprocessing($this->account, model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); // Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение) $this->view->page = null; @@ -377,6 +608,9 @@ final class task extends core } else if (!empty($parameters['market'])) { // Передан магазин + // Магазин пытается перезаписать магазин? + if ($this->account->type === 'market') throw new exception('Вы не авторизованы переназначать магазин заявки'); + if ($parameters['market'] === 'delete') { // Удалить магазин @@ -387,7 +621,7 @@ final class task extends core // Записано изменение в базу данных // Инициализация строки в глобальную переменную шаблонизатора - $this->view->rows = static::preprocessing(model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); + $this->view->rows = static::preprocessing($this->account, model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); // Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение) $this->view->page = null; @@ -419,20 +653,20 @@ final class task extends core } else { // Записать новый магазин - if (($market = market::read('d.id == "' . $parameters['market'] . '" && d.status == "active"', amount: 1)) instanceof _document) { + if (($market = market::read('d._key == "' . $parameters['market'] . '" && d.active == true', amount: 1)) instanceof _document) { // Найден магазин (запрашиваемый для записи магазин существует в базе данных) if ($task->market !== $parameters['market']) { // Идентификатор запрашиваемого сотрудника не равен актуальному // Запись магазина - $task->market = $market->id; + $task->market = $market->getKey(); if (_core::update($task)) { // Записано изменение в базу данных // Инициализация строки в глобальную переменную шаблонизатора - $this->view->rows = static::preprocessing(model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); + $this->view->rows = static::preprocessing($this->account, model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); // Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение) $this->view->page = null; @@ -465,7 +699,7 @@ final class task extends core } else throw new exception('Не найден магазин'); } } else throw new exception('Не получены данные для записи'); - } else throw new exception('Не найдено задание'); + } else throw new exception('Не найдена заявка'); } else throw new exception('Вы не авторизованы'); } catch (exception $e) { // Запись в реестр ошибок @@ -501,52 +735,145 @@ final class task extends core } } - /** * Прочитать данные задачи * * @param array $parameters Параметры запроса + * + * @return void В буфер вывода JSON-документ с запрашиваемыми параметрами */ - public function task(array $parameters = []): ?string + public function task(array $parameters = []): void { - 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' || $this->account->type === 'market')) { + // Авторизован аккаунт администратора, оператора или магазина - // Инициализация данных сотрудника - $this->view->task = model::read('d._key == "' . $parameters['task'] . '"', return: '{_key: d._key, created: d.created, updated: d.updated, confirmed: d.confirmed, hided: d.hided }')->getAll(); + // Инициализация данных сотрудника + $this->view->task = model::read( + 'd._key == "' . $parameters['task'] . '"', + return: $this->account->type === 'market' + ? '{_key: d._key, created: d.created, updated: d.updated, date: d.date, start: d.start, end: d.end, market: d.market, confirmed: d.confirmed, completed: d.completed }' + : '{_key: d._key, created: d.created, updated: d.updated, date: d.date, start: d.start, end: d.end, market: d.market, confirmed: d.confirmed, completed: d.completed, hided: d.hided }' + )->getAll(); - if (!empty($this->view->task)) { - // Найдены данные сотрудника + // Заявка не принадлежит запросившему магазину? + if ($this->account->type === 'market' and $this->view->task['market'] !== account::market($this->account->getId())?->getKey()) + throw new exception('Вы не авторизованы для чтения этой заявки'); - // Инициализация буфера данных сотрудника - $buffer = []; + // Удаление данных из выдачи + $this->view->task = ['market' => null] + $this->view->task; - // Перевод ключей на русский язык - foreach ($this->view->task as $key => $value) - if (match ($key) { - 'created', 'updated', 'confirmed', 'hided', '_key' => true, - default => false - }) $buffer[$key] = [ - 'label' => match ($key) { - 'created' => 'Создано', - 'updated' => 'Обновлено', - 'confirmed' => 'Подтверждено', - 'hided' => 'Скрыто', - default => $key - }, - 'value' => $value - ]; + if (!empty($this->view->task)) { + // Найдены данные задачи - // Запись из буфера данных сотрудника - $this->view->task = $buffer; + // Инициализация даты + $date = (new DateTime('@' . $this->view->task['date']))->setTimezone(new DateTimeZone('Asia/Krasnoyarsk')); - // Возврат (успех) - return $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'lists' . DIRECTORY_SEPARATOR . 'task.html'); - } + // Инициализация времени + $start = datetime::createFromFormat('H:i', (string) $this->view->task['start']); + $end = datetime::createFromFormat('H:i', (string) $this->view->task['end']); + + // Перенос времени в дату + $start = $date->setTime((int) $start->format('H'), (int) $start->format('i'))->format('U'); + $end = $date->setTime((int) $end->format('H'), (int) $end->format('i'))->format('U'); + + // Деинициализация неактуальных параметров + $this->view->task = ['date' => null, 'start' => null, 'end' => null] + $this->view->task; + + // Заявка уже началась? (в том числе может и завершена) + $passed = (new DateTime())->setTimezone(new DateTimeZone('Asia/Krasnoyarsk'))->format('U') - $start >= 0; + + // Инициализация буфера данных задачи + $buffer = []; + + // Перевод ключей на русский язык + foreach ($this->view->task as $key => $value) + if (match ($key) { + 'created', 'updated', 'confirmed', 'hided', 'completed', '_key' => true, + 'start', 'end' => $passed, // Только для завершённой заявки + default => false + }) $buffer[$key] = [ + 'label' => match ($key) { + 'created' => 'Создано', + 'updated' => 'Обновлено', + 'confirmed' => 'Подтверждено', + 'hided' => 'Скрыто', + 'completed' => 'Завершено', + 'start' => 'Начало', + 'end' => 'Конец', + default => $key + }, + 'value' => $value + ]; + + if ($passed) { + // Начатая заявка (в том числе завершённая) + + // Конвертация времени в дату + $buffer['start']['value'] = $start; + $buffer['end']['value'] = $end; + } + + // Запись из буфера данных задачи + $this->view->task = $buffer; + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode([ + 'start' => $start, + 'end' => $end, + 'task' => $this->view->render( + DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'lists' . DIRECTORY_SEPARATOR . 'task' . DIRECTORY_SEPARATOR . ($passed ? 'passed.html' : 'actual.html') + ), + 'errors' => self::parse_only_text($this->errors) + ]); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } else throw new exception('Не найдена заявка'); + } else throw new exception('Вы не авторизованы'); + } catch (exception $e) { + // Запись в реестр ошибок + $this->errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'errors' => self::parse_only_text($this->errors) + ] + ); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); } - - // Возврат (провал) - return null; } @@ -554,134 +881,170 @@ final class task extends core * Прочитать данные сотрудника * * @param array $parameters Параметры запроса + * + * @return void В буфер вывода JSON-документ с запрашиваемыми параметрами */ - public function worker(array $parameters = []): ?string + public function worker(array $parameters = []): void { - 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' || $this->account->type === 'market')) { + // Авторизован аккаунт администратора или оператора - // Инициализация данных сотрудника - $this->view->worker = worker::read('d.id == "' . $parameters['worker'] . '"')->getAll(); + // Инициализация данных сотрудника + $this->view->worker = worker::read( + 'd._key == "' . $parameters['worker'] . '"', + return: $this->account->type === 'market' + ? '{_key: d._key, created: d.created, updated: d.updated, name: d.name, number: d.number, mail: d.mail, birth: d.birth, rating: d.rating}' + : 'd' + )->getAll(); - if (!empty($this->view->worker)) { - // Найдены данные сотрудника + if (!empty($this->view->worker)) { + // Найдены данные сотрудника - // Инициализация буфера данных сотрудника - $buffer = []; + // Инициализация буфера данных сотрудника + $buffer = []; - // Перевод ключей на русский язык - foreach ($this->view->worker as $key => $value) $buffer[$key] = [ - 'label' => match ($key) { - 'created' => 'Создано', - 'updated' => 'Обновлено', - 'id' => 'Идентификатор', - 'phone', 'number' => 'Номер', - 'birth' => 'Дата рождения', - 'address' => 'Адрес', - 'activity' => 'Деятельность', - 'passport' => 'Пасорт', - 'issued' => 'Место выдачи', - 'hiring' => 'Дата найма', - 'district' => 'Адрес регистрации', - 'department' => 'Отделение', - 'requisites' => 'Реквизиты', - 'fired' => 'Уволен', - 'tax' => 'ИНН', - 'city' => 'Город', - 'commentary' => 'Комментарий', - 'payment' => 'Получатель', - default => $key - }, - 'value' => $value - ]; + // Перевод ключей на русский язык + foreach ($this->view->worker as $key => $value) $buffer[$key] = [ + 'label' => match ($key) { + 'created' => 'Создано', + 'updated' => 'Обновлено', + 'name' => 'ФИО', + 'number' => 'Номер', + 'mail' => 'Почта', + 'birth' => 'Дата рождения', + 'address' => 'Адрес', + 'rating' => 'Рейтинг', + 'passport' => 'Паспорт', + 'issued' => 'Место выдачи', + 'hiring' => 'Дата найма', + 'district' => 'Адрес регистрации', + 'department' => 'Отделение', + 'requisites' => 'Реквизиты', + 'fired' => 'Уволен', + 'tax' => 'ИНН', + 'city' => 'Город', + 'payment' => 'Форма оплаты', + default => $key + }, + 'value' => $value + ]; - // Запись из буфера данных - $this->view->worker = $buffer; + // Удаление ненужных параметров из выдачи + foreach ($buffer as $key => $value) if (match ($key) { + 'type', 'active' => true, + default => false + }) unset($buffer[$key]); - // Возврат (успех) - return $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'lists' . DIRECTORY_SEPARATOR . 'worker.html'); - } + // Запись из буфера данных + $this->view->worker = $buffer; + + // Возврат (успех) + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'worker' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'lists' . DIRECTORY_SEPARATOR . 'worker.html'), + 'errors' => self::parse_only_text($this->errors) + ] + ); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } else throw new exception('Не найдена заявка'); + } else throw new exception('Вы не авторизованы'); + } catch (exception $e) { + // Запись в реестр ошибок + $this->errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'errors' => self::parse_only_text($this->errors) + ] + ); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); } - - // Возврат (провал) - return null; } /** * Прочитать данные магазина * * @param array $parameters Параметры запроса - */ - public function market(array $parameters = []): ?string - { - if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) { - // Авторизован аккаунт оператора - - // Инициализация данных - $this->view->market = market::read('CONCAT("K", SUBSTRING(d.id, 1)) == "' . $parameters['market'] . '"')?->getAll(); - - if (!empty($this->view->market)) { - // Найдены данные магазина - - // Инициализация буфера данных - $buffer = []; - - // Перевод ключей на русский язык - foreach ($this->view->market as $key => $value) $buffer[$key] = [ - 'label' => match ($key) { - 'created' => 'Создано', - 'updated' => 'Обновлено', - 'id' => 'Идентификатор', - 'type' => 'Тип', - 'director' => 'Директор', - 'address' => 'Адрес', - 'city' => 'Город', - default => $key - }, - 'value' => $value - ]; - - // Запись из буфера данных - $this->view->market = $buffer; - - // Возврат (успех) - return $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'lists' . DIRECTORY_SEPARATOR . 'market.html'); - } - } - - // Возврат (провал) - return null; - } - - /** - * Подтвердить (отклонить) * - * @param array $parameters Параметры запроса - * * @return void В буфер вывода JSON-документ с запрашиваемыми параметрами */ - public function confirm(array $parameters = []): void + public function market(array $parameters = []): void { - 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')) { + // Авторизован аккаунт администратора или оператора - // Инициализация данных - $task = model::read('d._key == "' . $parameters['task'] . '"'); + // Инициализация данных + $this->view->market = market::read('d._key == "' . $parameters['market'] . '"')?->getAll(); - if ($task instanceof _document) { - // Найдена заявка + if (!empty($this->view->market)) { + // Найдены данные магазина - // Изменение статуса подтверждения - $task->confirmed = !$task->confirmed; + // Инициализация буфера данных + $buffer = []; - if (_core::update($task)) { - // Записано изменение в базу данных + // Перевод ключей на русский язык + foreach ($this->view->market as $key => $value) $buffer[$key] = [ + 'label' => match ($key) { + 'created' => 'Создано', + 'updated' => 'Обновлено', + 'name' => 'ФИО', + 'type' => 'Тип', + 'director' => 'Директор', + 'address' => 'Адрес', + 'city' => 'Город', + 'number' => 'Номер', + 'mail' => 'Почта', + 'commentary' => 'Комментарий', + default => $key + }, + 'value' => $value + ]; - // Инициализация строки в глобальную переменную шаблонизатора - $this->view->rows = static::preprocessing(model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); + // Удаление ненужных параметров из выдачи + foreach ($buffer as $key => $value) if (match ($key) { + 'active' => true, + default => false + }) unset($buffer[$key]); - // Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение) - $this->view->page = null; + // Запись из буфера данных + $this->view->market = $buffer; // Запись заголовков ответа header('Content-Type: application/json'); @@ -694,8 +1057,7 @@ final class task extends core // Генерация ответа echo json_encode( [ - 'confirmed' => $this->view->rows[0]->task['confirmed'], - 'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'), + 'market' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'lists' . DIRECTORY_SEPARATOR . 'market.html'), 'errors' => self::parse_only_text($this->errors) ] ); @@ -706,8 +1068,380 @@ final class task extends core // Отправка и деинициализация буфера вывода ob_end_flush(); flush(); - } - } + } 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() + ]; + + // Запись заголовков ответа + 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 В буфер вывода JSON-документ с запрашиваемыми параметрами + */ + public function confirm(array $parameters = []): void + { + try { + if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) { + // Авторизован аккаунт администратора или оператора + + // Инициализация данных + $task = model::read('d._key == "' . $parameters['task'] . '"'); + + if ($task instanceof _document) { + // Найдена заявка + + // Изменение статуса подтверждения + $task->confirmed = !$task->confirmed; + + if (_core::update($task)) { + // Записано изменение в базу данных + + // Инициализация строки в глобальную переменную шаблонизатора + $this->view->rows = static::preprocessing($this->account, model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); + + // Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение) + $this->view->page = null; + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'confirmed' => $this->view->rows[0]->task['confirmed'], + 'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'), + 'errors' => self::parse_only_text($this->errors) + ] + ); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } else throw new exception('Не удалось обновить заявку'); + } 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() + ]; + + // Запись заголовков ответа + 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 В буфер вывода JSON-документ с запрашиваемыми параметрами + */ + public function problem(array $parameters = []): void + { + try { + if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'market')) { + // Авторизован аккаунт администратора, оператора и магазина + + // Инициализация данных + $task = model::read('d._key == "' . $parameters['task'] . '"'); + + // Заявка не принадлежит запросившему магазину? + if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->getKey()) + throw new exception('Вы не авторизованы для заявления о проблеме с этой заявкой'); + + // Инициализация даты + $date = (new DateTime('@' . $task->date))->setTimezone(new DateTimeZone('Asia/Krasnoyarsk')); + + // Инициализация времени + $start = datetime::createFromFormat('H:i', (string) $task->start); + $end = datetime::createFromFormat('H:i', (string) $task->end); + + // Перенос времени в дату + $start = $date->setTime((int) $start->format('H'), (int) $start->format('i'))->format('U'); + $end = $date->setTime((int) $end->format('H'), (int) $end->format('i'))->format('U'); + + if ($task instanceof _document) { + // Найдена заявка + + if ($task->problematic) { + // Проблемная заявка (запрошена отмена статуса проблемной заявки) + + // Изменение статуса наличия проблемы + $task->problematic = false; + } else { + // Не проблемная заявка (запрошена установка статуса проблемной заявки) + + if (empty($parameters['text'])) throw new exception('Необходимо передать текст с объяснением проблемы'); + else { + // Получен текст с описанием проблемы + + // Изменение статуса наличия проблемы + $task->problematic = true; + } + } + + if (_core::update($task)) { + // Записано изменение в базу данных + + // Инициализация строки в глобальную переменную шаблонизатора + $this->view->rows = static::preprocessing($this->account, model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); + + // Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение) + $this->view->page = null; + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'problematic' => $this->view->rows[0]->task['problematic'], + 'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'), + 'errors' => self::parse_only_text($this->errors) + ] + ); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } else throw new exception('Не удалось обновить заявку'); + } 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() + ]; + + // Запись заголовков ответа + 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 В буфер вывода JSON-документ с запрашиваемыми параметрами + */ + public function complete(array $parameters = []): void + { + try { + if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'market')) { + // Авторизован аккаунт администратора, оператора и магазина + + // Инициализация данных + $task = model::read('d._key == "' . $parameters['task'] . '"'); + + // Заявка не принадлежит запросившему магазину? + if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->getKey()) + throw new exception('Вы не авторизованы для завершения этой заявки'); + + // Заявка завершена? + if ($task->completed) throw new exception('Заявка уже завершена'); + + // Инициализация даты + $date = (new DateTime('@' . $task->date))->setTimezone(new DateTimeZone('Asia/Krasnoyarsk')); + + // Инициализация времени + $start = datetime::createFromFormat('H:i', (string) $task->start); + $end = datetime::createFromFormat('H:i', (string) $task->end); + + // Перенос времени в дату + $start = $date->setTime((int) $start->format('H'), (int) $start->format('i'))->format('U'); + $end = $date->setTime((int) $end->format('H'), (int) $end->format('i'))->format('U'); + + // Заявка ещё не начата + if ($this->account->type === 'market' and time() - $start < 0) + throw new exception('Запрещено завершать не начатую заявку'); + + if ($task instanceof _document) { + // Найдена заявка + + if (empty($parameters['rating'])) throw new exception('Необходимо передать оценку'); + else if (($parameters['rating'] = (int) $parameters['rating']) < 1) throw new exception('Оценка должна быть не менее 1'); + else if ($parameters['rating'] > 5) throw new exception('Оценка должна быть не более 5'); + else { + // Получена оценка + + // Запись оценики + $task->rating = $parameters['rating']; + + if (!empty($parameters['review'])) { + // Получен отзыв + + // Запись отзыва + $task->review = $parameters['review']; + } + + // Запись статуса завершения + $task->completed = true; + + // Снятие с публикации + $task->published = false; + } + + if (_core::update($task)) { + // Записано изменение в базу данных + + // Инициализация строки в глобальную переменную шаблонизатора + $this->view->rows = static::preprocessing($this->account, model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); + + // Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение) + $this->view->page = null; + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'completed' => $this->view->rows[0]->task['completed'], + 'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'), + 'errors' => self::parse_only_text($this->errors) + ] + ); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } else throw new exception('Не удалось обновить заявку'); + } 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() + ]; + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'completed' => $task->completed ?? false, + 'errors' => self::parse_only_text($this->errors) + ] + ); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); } } @@ -720,52 +1454,84 @@ final class task extends core */ public function hide(array $parameters = []): void { - 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')) { + // Авторизован аккаунт администратора или оператора - // Инициализация данных - $task = model::read('d._key == "' . $parameters['task'] . '"'); + // Инициализация данных + $task = model::read('d._key == "' . $parameters['task'] . '"'); - if ($task instanceof _document) { - // Найдена заявка + if ($task instanceof _document) { + // Найдена заявка - // Изменение статуса скрытия - $task->hided = !$task->hided; + // Изменение статуса скрытия + $task->hided = !$task->hided; - if (_core::update($task)) { - // Записано изменение в базу данных + if (_core::update($task)) { + // Записано изменение в базу данных - // Инициализация строки в глобальную переменную шаблонизатора - $this->view->rows = static::preprocessing(model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); + // Инициализация строки в глобальную переменную шаблонизатора + $this->view->rows = static::preprocessing($this->account, model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); - // Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение) - $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(); - // Генерация ответа - echo json_encode( - [ - 'hided' => $this->view->rows[0]->task['hided'], - 'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'), - 'errors' => self::parse_only_text($this->errors) - ] - ); + // Генерация ответа + echo json_encode( + [ + 'hided' => $this->view->rows[0]->task['hided'], + 'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'), + 'errors' => self::parse_only_text($this->errors) + ] + ); - // Запись заголовков ответа - header('Content-Length: ' . ob_get_length()); + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); - // Отправка и деинициализация буфера вывода - ob_end_flush(); - flush(); - } - } + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } 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() + ]; + + // Запись заголовков ответа + 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(); } } @@ -778,20 +1544,132 @@ final class task extends core */ public function remove(array $parameters = []): void { - 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' || $this->account->type === 'market')) { + // Авторизован аккаунт администратора, оператора и магазина - // Инициализация данных - $task = model::read('d._key == "' . $parameters['task'] . '"'); + // Инициализация данных + $task = model::read('d._key == "' . $parameters['task'] . '"'); - if ($task instanceof _document) { - // Найдена заявка + // Заявка не принадлежит запросившему магазину? + if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->getKey()) + throw new exception('Вы не авторизованы для редактирования типа работы этой заявки'); - // Изменение статуса - $task->status = 'deleted'; + // Заявка подтверждена? + if ($task->confirmed) throw new exception('Запрещено удалять подтверждённую заявку'); - if (_core::update($task)) { - // Помечено как удалённое + // Инициализация даты + $date = (new DateTime('@' . $task->date))->setTimezone(new DateTimeZone('Asia/Krasnoyarsk')); + + // Инициализация времени + $start = datetime::createFromFormat('H:i', (string) $task->start); + $end = datetime::createFromFormat('H:i', (string) $task->end); + + // Перенос времени в дату + $start = $date->setTime((int) $start->format('H'), (int) $start->format('i'))->format('U'); + $end = $date->setTime((int) $end->format('H'), (int) $end->format('i'))->format('U'); + + // Заявка уже начата + if ($this->account->type === 'market' and time() - $start > 0) + throw new exception('Запрещено удалять начатую заявку'); + + // Заявка уже завершена + if ($this->account->type === 'market' and $task->completed === true || time() - $end > 0) + throw new exception('Запрещено удалять завершённую заявку'); + + // Прошло более 30 минут после создания заявки? (1800 секунд = 30 минут) + if ($this->account->type === 'market' and time() - $task->created > 1800) + throw new exception('Запрещено удалять заявку спустя 30 минут после создания'); + + if ($task instanceof _document) { + // Найдена заявка + + // Изменение статуса + $task->status = 'deleted'; + + if (_core::update($task)) { + // Помечено как удалённое + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + 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('Не найдена заявка'); + } else throw new exception('Вы не авторизованы'); + } catch (exception $e) { + // Запись в реестр ошибок + $this->errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'errors' => self::parse_only_text($this->errors) + ] + ); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } + } + + /** + * Прочитать значение по идентификатору + * + * @param array $parameters Параметры запроса + * + * @return void В буфер вывода JSON-документ с запрашиваемыми параметрами + */ + public function value(array $parameters = []): void + { + try { + if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'market')) { + // Авторизован аккаунт администратора, оператора или магазина + + // Инициализация данных + $task = model::read('d._key == "' . $parameters['task'] . '"'); + + // Заявка не принадлежит запросившему магазину? + if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->getKey()) + throw new exception('Вы не авторизованы для чтения параметров этой заявки'); + + if ($task instanceof _document) { + // Найдена заявка // Запись заголовков ответа header('Content-Type: application/json'); @@ -804,7 +1682,7 @@ final class task extends core // Генерация ответа echo json_encode( [ - 'deleted' => true, + 'value' => $task->{$parameters['name']} ?? null, 'errors' => self::parse_only_text($this->errors) ] ); @@ -815,68 +1693,122 @@ final class task extends core // Отправка и деинициализация буфера вывода ob_end_flush(); flush(); - } - } + } 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() + ]; + + // Запись заголовков ответа + 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 mixed Значение из инстанции задачи в базе данных по имени - */ - public function value(array $parameters = []): mixed - { - if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'market')) { - // Авторизован аккаунт оператора или магазина - - // Инициализация данных - $task = model::read('d._key == "' . $parameters['task'] . '"'); - - if ($task instanceof _document) { - // Найдена заявка - - // Возврат (успех) - return $task->{$parameters['name']} ?? null; - } - } - - // Возврат (провал) - return null; - } - /** * Прочитать данные работ для * * @param array $parameters Параметры запроса + * + * @return void В буфер вывода JSON-документ с запрашиваемыми параметрами */ - public function works(array $parameters = []): ?string + public function works(array $parameters = []): void { - if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'market')) { - // Авторизован аккаунт оператора или магазина + try { + if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'market')) { + // Авторизован аккаунт администратора, оператора или магазина - // Инициализация данных - $this->view->task = model::read('d._key == "' . $parameters['task'] . '"'); + // Инициализация данных + $this->view->task = model::read('d._key == "' . $parameters['task'] . '"'); - if ($this->view->task instanceof _document) { - // Найдена заявка + if ($this->view->task instanceof _document) { + // Найдена заявка - // Инициализация списка работ - $this->view->works = ['Кассир', 'Выкладчик', 'Грузчик', 'Гастроном']; + // Инициализация списка работ + $this->view->works = ['Кассир', 'Выкладчик', 'Грузчик', 'Гастроном']; - // Проверка на существование записанной в задаче работы в списке существующих работ и запись об этом в глобальную переменную шаблонизатора - foreach ($this->view->works as $work) if ($this->view->task->work === $work) $this->view->exist = true; + // Проверка на существование записанной в задаче работы в списке существующих работ и запись об этом в глобальную переменную шаблонизатора + foreach ($this->view->works as $work) if ($this->view->task->work === $work) $this->view->exist = true; - // Возврат (успех) - return $this->view->render(DIRECTORY_SEPARATOR . 'lists' . DIRECTORY_SEPARATOR . 'works.html'); - } + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'works' => $this->view->render(DIRECTORY_SEPARATOR . 'lists' . DIRECTORY_SEPARATOR . 'works.html'), + 'errors' => self::parse_only_text($this->errors) + ] + ); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } else throw new exception('Не найдена заявка'); + } else throw new exception('Вы не авторизованы'); + } catch (exception $e) { + // Запись в реестр ошибок + $this->errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'errors' => self::parse_only_text($this->errors) + ] + ); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); } - - // Возврат (провал) - return null; } /** @@ -888,55 +1820,113 @@ final class task extends core */ public function work(array $parameters = []): void { - if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'market')) { - // Авторизован аккаунт оператора + try { + if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'market')) { + // Авторизован аккаунт администратора, оператора или магазина - // Инициализация данных - $task = model::read('d._key == "' . $parameters['task'] . '"'); + // Инициализация данных + $task = model::read('d._key == "' . $parameters['task'] . '"'); - if ($task instanceof _document) { - // Найдена заявка + // Заявка не принадлежит запросившему магазину? + if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->getKey()) + throw new exception('Вы не авторизованы для редактирования типа работы этой заявки'); - // Изменение статуса - $task->work = match ($parameters['work']) { - 'Кассир', 'Выкладчик', 'Грузчик', 'Гастроном' => $parameters['work'], - default => 'Кассир' - }; + // Заявка подтверждена? + if ($task->confirmed) throw new exception('Запрещено редактировать тип работы у подтверждённой заявки'); - if (_core::update($task)) { - // Записано в ArangoDB + // Инициализация даты + $date = (new DateTime('@' . $task->date))->setTimezone(new DateTimeZone('Asia/Krasnoyarsk')); - // Инициализация строки в глобальную переменную шаблонизатора - $this->view->rows = static::preprocessing(model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); + // Инициализация времени + $start = datetime::createFromFormat('H:i', (string) $task->start); + $end = datetime::createFromFormat('H:i', (string) $task->end); - // Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение) - $this->view->page = null; + // Перенос времени в дату + $start = $date->setTime((int) $start->format('H'), (int) $start->format('i'))->format('U'); + $end = $date->setTime((int) $end->format('H'), (int) $end->format('i'))->format('U'); - // Запись заголовков ответа - header('Content-Type: application/json'); - header('Content-Encoding: none'); - header('X-Accel-Buffering: no'); + // Заявка уже начата + if ($this->account->type === 'market' and time() - $start > 0) + throw new exception('Запрещено редактировать тип работы начатой заявки'); - // Инициализация буфера вывода - ob_start(); + // Заявка уже завершена + if ($this->account->type === 'market' and $task->completed === true || time() - $end > 0) + throw new exception('Запрещено редактировать тип работы завершённой заявки'); - // Генерация ответа - echo json_encode( - [ - 'writed' => true, - 'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'), - 'errors' => self::parse_only_text($this->errors) - ] - ); + if ($task instanceof _document) { + // Найдена заявка - // Запись заголовков ответа - header('Content-Length: ' . ob_get_length()); + // Изменение статуса + $task->work = match ($parameters['work']) { + 'Кассир', 'Выкладчик', 'Грузчик', 'Гастроном' => $parameters['work'], + default => 'Кассир' + }; - // Отправка и деинициализация буфера вывода - ob_end_flush(); - flush(); - } - } + if (_core::update($task)) { + // Записано в ArangoDB + + // Инициализация строки в глобальную переменную шаблонизатора + $this->view->rows = static::preprocessing($this->account, model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); + + // Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение) + $this->view->page = null; + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'writed' => true, + 'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'), + 'errors' => self::parse_only_text($this->errors) + ] + ); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } else throw new exception('Не удалось обновить заявку'); + } 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() + ]; + + // Запись заголовков ответа + 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(); } } @@ -949,45 +1939,103 @@ final class task extends core */ public function description(array $parameters = []): void { - if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'market')) { - // Авторизован аккаунт оператора + try { + if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'market')) { + // Авторизован аккаунт администратора, оператора или магазина - // Инициализация данных - $task = model::read('d._key == "' . $parameters['task'] . '"'); + // Инициализация данных + $task = model::read('d._key == "' . $parameters['task'] . '"'); - if ($task instanceof _document) { - // Найдена заявка + // Заявка не принадлежит запросившему магазину? + if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->getKey()) + throw new exception('Вы не авторизованы для редактирования описания этой заявки'); - // Изменение статуса - $task->description = $parameters['description']; + // Заявка подтверждена? + if ($task->confirmed) throw new exception('Запрещено редактировать описание у подтверждённой заявки'); - if (_core::update($task)) { - // Записано в ArangoDB + // Инициализация даты + $date = (new DateTime('@' . $task->date))->setTimezone(new DateTimeZone('Asia/Krasnoyarsk')); - // Запись заголовков ответа - header('Content-Type: application/json'); - header('Content-Encoding: none'); - header('X-Accel-Buffering: no'); + // Инициализация времени + $start = datetime::createFromFormat('H:i', (string) $task->start); + $end = datetime::createFromFormat('H:i', (string) $task->end); - // Инициализация буфера вывода - ob_start(); + // Перенос времени в дату + $start = $date->setTime((int) $start->format('H'), (int) $start->format('i'))->format('U'); + $end = $date->setTime((int) $end->format('H'), (int) $end->format('i'))->format('U'); - // Генерация ответа - echo json_encode( - [ - 'writed' => true, - 'errors' => self::parse_only_text($this->errors) - ] - ); + // Заявка уже начата + if ($this->account->type === 'market' and time() - $start > 0) + throw new exception('Запрещено редактировать описание начатой заявки'); - // Запись заголовков ответа - header('Content-Length: ' . ob_get_length()); + // Заявка уже завершена + if ($this->account->type === 'market' and $task->completed === true || time() - $end > 0) + throw new exception('Запрещено редактировать описание завершённой заявки'); - // Отправка и деинициализация буфера вывода - ob_end_flush(); - flush(); - } - } + if ($task instanceof _document) { + // Найдена заявка + + // Изменение статуса + $task->description = $parameters['description']; + + if (_core::update($task)) { + // Записано в ArangoDB + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'writed' => 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('Не найдена заявка'); + } else throw new exception('Вы не авторизованы'); + } catch (exception $e) { + // Запись в реестр ошибок + $this->errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'errors' => self::parse_only_text($this->errors) + ] + ); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); } } @@ -1000,51 +2048,109 @@ final class task extends core */ public function date(array $parameters = []): void { - if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'market')) { - // Авторизован аккаунт оператора + try { + if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'market')) { + // Авторизован аккаунт администратора, оператора или магазина - // Инициализация данных - $task = model::read('d._key == "' . $parameters['task'] . '"'); + // Инициализация данных + $task = model::read('d._key == "' . $parameters['task'] . '"'); - if ($task instanceof _document) { - // Найдена заявка + // Заявка не принадлежит запросившему магазину? + if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->getKey()) + throw new exception('Вы не авторизованы для редактирования даты и времени этой заявки'); - // Запись даты и времени - if (!empty($parameters['date'])) $task->date = +$parameters['date']; - if (!empty($parameters['start'])) $task->start = $parameters['start']; - if (!empty($parameters['end'])) $task->end = $parameters['end']; + // Заявка подтверждена? + if ($task->confirmed) throw new exception('Запрещено редактировать дату и время у подтверждённой заявки'); - if (_core::update($task)) { - // Записано в ArangoDB + // Инициализация даты + $date = (new DateTime('@' . $task->date))->setTimezone(new DateTimeZone('Asia/Krasnoyarsk')); - // Инициализация строки в глобальную переменную шаблонизатора - $this->view->rows = static::preprocessing(model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); + // Инициализация времени + $start = datetime::createFromFormat('H:i', (string) $task->start); + $end = datetime::createFromFormat('H:i', (string) $task->end); - // Запись заголовков ответа - header('Content-Type: application/json'); - header('Content-Encoding: none'); - header('X-Accel-Buffering: no'); + // Перенос времени в дату + $start = $date->setTime((int) $start->format('H'), (int) $start->format('i'))->format('U'); + $end = $date->setTime((int) $end->format('H'), (int) $end->format('i'))->format('U'); - // Инициализация буфера вывода - ob_start(); + // Заявка уже начата + if ($this->account->type === 'market' and time() - $start > 0) + throw new exception('Запрещено редактировать дату и время начатой заявки'); - // Генерация ответа - echo json_encode( - [ - 'writed' => true, - 'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'), - 'errors' => self::parse_only_text($this->errors) - ] - ); + // Заявка уже завершена + if ($this->account->type === 'market' and $task->completed === true || time() - $end > 0) + throw new exception('Запрещено редактировать дату и время завершённой заявки'); - // Запись заголовков ответа - header('Content-Length: ' . ob_get_length()); + if ($task instanceof _document) { + // Найдена заявка - // Отправка и деинициализация буфера вывода - ob_end_flush(); - flush(); - } - } + // Запись даты и времени + if (!empty($parameters['date'])) $task->date = +$parameters['date']; + if (!empty($parameters['start'])) $task->start = $parameters['start']; + if (!empty($parameters['end'])) $task->end = $parameters['end']; + + if (_core::update($task)) { + // Записано в ArangoDB + + // Инициализация строки в глобальную переменную шаблонизатора + $this->view->rows = static::preprocessing($this->account, model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'writed' => true, + 'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'), + 'errors' => self::parse_only_text($this->errors) + ] + ); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } else throw new exception('Не удалось обновить заявку'); + } 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() + ]; + + // Запись заголовков ответа + 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(); } } @@ -1057,49 +2163,81 @@ final class task extends core */ public function commentary(array $parameters = []): void { - if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'market')) { - // Авторизован аккаунт оператора, администратора или магазина + try { + if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) { + // Авторизован аккаунт администратора или оператора - // Инициализация данных - $task = model::read('d._key == "' . $parameters['task'] . '"'); + // Инициализация данных + $task = model::read('d._key == "' . $parameters['task'] . '"'); - if ($task instanceof _document) { - // Найдена заявка + if ($task instanceof _document) { + // Найдена заявка - // Запись комментария - $task->commentary = $parameters['commentary']; + // Запись комментария + $task->commentary = $parameters['commentary']; - if (_core::update($task)) { - // Записано в ArangoDB + if (_core::update($task)) { + // Записано в ArangoDB - // Инициализация строки в глобальную переменную шаблонизатора - $this->view->rows = static::preprocessing(model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); + // Инициализация строки в глобальную переменную шаблонизатора + $this->view->rows = static::preprocessing($this->account, model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); - // Запись заголовков ответа - 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(); - // Генерация ответа - echo json_encode( - [ - 'writed' => true, - 'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'), - 'errors' => self::parse_only_text($this->errors) - ] - ); + // Генерация ответа + echo json_encode( + [ + 'writed' => true, + 'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'), + 'errors' => self::parse_only_text($this->errors) + ] + ); - // Запись заголовков ответа - header('Content-Length: ' . ob_get_length()); + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); - // Отправка и деинициализация буфера вывода - ob_end_flush(); - flush(); - } - } + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } 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() + ]; + + // Запись заголовков ответа + 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(); } } @@ -1112,49 +2250,81 @@ final class task extends core */ public function publish(array $parameters = []): void { - 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')) { + // Авторизован аккаунт администратора или оператора - // Инициализация данных - $task = model::read('d._key == "' . $parameters['task'] . '"'); + // Инициализация данных + $task = model::read('d._key == "' . $parameters['task'] . '"'); - if ($task instanceof _document) { - // Найдена заявка + if ($task instanceof _document) { + // Найдена заявка - // Запись статуса о публикации - $task->published = true; + // Запись статуса о публикации + $task->published = true; - if (_core::update($task)) { - // Записано в ArangoDB + if (_core::update($task)) { + // Записано в ArangoDB - // Инициализация строки в глобальную переменную шаблонизатора - $this->view->rows = static::preprocessing(model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); + // Инициализация строки в глобальную переменную шаблонизатора + $this->view->rows = static::preprocessing($this->account, model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); - // Запись заголовков ответа - 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(); - // Генерация ответа - echo json_encode( - [ - 'published' => true, - 'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'), - 'errors' => self::parse_only_text($this->errors) - ] - ); + // Генерация ответа + echo json_encode( + [ + 'published' => true, + 'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'), + 'errors' => self::parse_only_text($this->errors) + ] + ); - // Запись заголовков ответа - header('Content-Length: ' . ob_get_length()); + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); - // Отправка и деинициализация буфера вывода - ob_end_flush(); - flush(); - } - } + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } 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() + ]; + + // Запись заголовков ответа + 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(); } } @@ -1167,23 +2337,246 @@ final class task extends core */ public function unpublish(array $parameters = []): void { - 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')) { + // Авторизован аккаунт администратора или оператора - // Инициализация данных - $task = model::read('d._key == "' . $parameters['task'] . '"'); + // Инициализация данных + $task = model::read('d._key == "' . $parameters['task'] . '"'); - if ($task instanceof _document) { - // Найдена заявка + if ($task instanceof _document) { + // Найдена заявка - // Запись статуса о публикации - $task->published = false; + // Запись статуса о публикации + $task->published = false; - if (_core::update($task)) { - // Записано в ArangoDB + if (_core::update($task)) { + // Записано в ArangoDB + + // Инициализация строки в глобальную переменную шаблонизатора + $this->view->rows = static::preprocessing($this->account, model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'unpublished' => true, + 'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'), + 'errors' => self::parse_only_text($this->errors) + ] + ); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } else throw new exception('Не удалось обновить заявку'); + } 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() + ]; + + // Запись заголовков ответа + 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 В буфер вывода JSON-документ с запрашиваемыми параметрами + */ + public function chat(array $parameters = []): void + { + try { + if ($this->account->status()) { + // Авторизован аккаунт (любой) + + // Инициализация данных + $task = model::read('d._key == "' . $parameters['task'] . '"'); + + if ($task instanceof _document) { + // Найдена заявка + + // Инициализация функции сортировки сообщений по дате + function sort(array $a, array $b) + { + if ($a['date'] === $b['date']) return 0; + return $a['date'] > $b['date'] ? 1 : -1; + } + + if ($parameters['chat'] === 'worker') { + // Сотрудник + + if ( + $this->account->type === 'operator' + || $this->account->type === 'administrator' + || ($this->account->type === 'worker' && $task->worker === account::worker($this->account->getId())?->getKey()) + ) { + // Авторизован аккаунт (если сотрудник, то назначен на эту заявку) + + // Инициализация буфера чата + $chat = $task->chats; + + if ($this->account->type === 'worker') { + // Сотрудник + + foreach ($task->chats['worker'] ?? [] as $key => $message) { + // Перебор сообщений + + // Запись статуса о прочтении сообщения + $chat = [ + 'worker' => [ + $key => [ + 'readed' => [ + $this->account->getKey() => true + ] + ($chat['worker'][$key]['readed'] ?? []) + ] + $chat['worker'][$key] + ] + $chat['worker'] + ] + $chat; + } + } else if ($this->account->type === 'operator' || $this->account->type === 'administrator') { + // Оператор или администратор + + // Из-за того, что отправляется сразу 2 запроса, то не успевает записаться как прочитанный чат либо сотрудника, либо магазина + foreach ($task->chats ?? [] as $name => $messages) + foreach ($messages as $key => $message) { + // Перебор сообщений + + // Запись статуса о прочтении сообщения + $chat = [ + $name => [ + $key => [ + 'readed' => [ + $this->account->getKey() => true + ] + ($chat[$name][$key]['readed'] ?? []) + ] + $chat[$name][$key] + ] + $chat[$name] + ] + $chat; + } + } + + // Запись чата из буфера + $task->chats = $chat; + + // Запись данных чтения сообщений в ArangoDB + if (!_core::update($task)) throw new exception('Не удалось прочитать сообщения'); + + // Запись сообщений в буфер сортировки + $buffer = $task->chats['worker'] ?? []; + + // Сортировка + if (count($buffer) > 1) uasort($buffer, __NAMESPACE__ . '\sort'); + + // Запись из буфера сортировки в буфер шаблонизатора + $this->view->messages = $buffer; + } else throw new exception('Вы не авторизованы для чтения этого чата'); + } else if ($parameters['chat'] === 'market') { + // Магазин + + if ( + $this->account->type === 'operator' + || $this->account->type === 'administrator' + || ($this->account->type === 'market' && $task->market === account::market($this->account->getId())?->getKey()) + ) { + // Авторизован аккаунт (если магазин, то назначен на эту заявку) + + // Инициализация буфера чата + $chat = $task->chats; + + if ($this->account->type === 'market') { + // Магазин + + foreach ($task->chats['market'] ?? [] as $key => $message) { + // Перебор сообщений + + // Запись статуса о прочтении сообщения + $chat = [ + 'market' => [ + $key => [ + 'readed' => [ + $this->account->getKey() => true + ] + $chat['market'][$key]['readed'] ?? [] + ] + $chat['market'][$key] + ] + $chat['market'] + ] + $chat; + } + } else if ($this->account->type === 'operator' || $this->account->type === 'administrator') { + // Оператор или администратор + + // Из-за того, что отправляется сразу 2 запроса, то не успевает записаться как прочитанный чат либо сотрудника, либо магазина + foreach ($task->chats ?? [] as $name => $messages) + foreach ($messages as $key => $message) { + // Перебор сообщений + + // Запись статуса о прочтении сообщения + $chat = [ + $name => [ + $key => [ + 'readed' => [ + $this->account->getKey() => true + ] + ($chat[$name][$key]['readed'] ?? []) + ] + $chat[$name][$key] + ] + $chat[$name] + ] + $chat; + } + } + + // Запись чата из буфера + $task->chats = $chat; + + // Запись данных чтения сообщений в ArangoDB + if (!_core::update($task)) throw new exception('Не удалось прочитать сообщения'); + + // Запись сообщений в буфер сортировки + $buffer = $task->chats['market'] ?? []; + + // Сортировка + if (count($buffer) > 1) uasort($buffer, __NAMESPACE__ . '\sort'); + + // Запись из буфера сортировки в буфер шаблонизатора + $this->view->messages = $buffer; + } else throw new exception('Вы не авторизованы для чтения этого чата'); + } // Инициализация строки в глобальную переменную шаблонизатора - $this->view->rows = static::preprocessing(model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); + $this->view->rows = static::preprocessing($this->account, model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); // Запись заголовков ответа header('Content-Type: application/json'); @@ -1196,7 +2589,7 @@ final class task extends core // Генерация ответа echo json_encode( [ - 'unpublished' => true, + 'chat' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'chat.html'), 'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'), 'errors' => self::parse_only_text($this->errors) ] @@ -1208,9 +2601,242 @@ final class task extends core // Отправка и деинициализация буфера вывода ob_end_flush(); flush(); - } - } + } 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() + ]; + + // Запись заголовков ответа + 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 В буфер вывода JSON-документ с запрашиваемыми параметрами + */ + public function message(array $parameters = []): void + { + try { + if ($this->account->status()) { + // Авторизован аккаунт (любой) + + // Инициализация данных + $task = model::read('d._key == "' . $parameters['task'] . '"'); + + if ($task instanceof _document) { + // Найдена заявка + + if (mb_strlen($parameters['text']) > 0) { + // Получено сообщение + + if ($parameters['chat'] === 'worker') { + // Сотрудник + + if ( + $this->account->type === 'operator' + || $this->account->type === 'administrator' + || ($this->account->type === 'worker' && $task->worker === account::worker($this->account->getId())?->getKey()) + ) { + // Авторизован аккаунт (если сотрудник, то назначен на эту заявку) + + // Инициализация значения по умолчанию + if (!isset($task->chats['worker'])) $task->chats = ['worker' => []] + ($task->chats ?? []); + + // Запись сообщения + $task->chats = [ + 'worker' => [ + count($task->chats['worker']) => [ + 'from' => [ + '_key' => $this->account->getKey(), + 'type' => $this->account->type + ], + 'type' => $parameters['type'] ?? 'message', + 'text' => $parameters['text'], + 'readed' => [$this->account->getKey() => 0], + 'date' => time() + ] + ] + $task->chats['worker'] + ] + $task->chats; + + if ($parameters['type'] === 'problem') { + // Сообщение о проблеме + + // Изменение статуса наличия проблемы + $task->problematic = true; + } else if ($parameters['type'] === 'solution') { + // Сообщение о решении проблемы + + // Изменение статуса наличия проблемы + $task->problematic = false; + } + + if (_core::update($task)) { + // Записано в ArangoDB + + // Инициализация строки в глобальную переменную шаблонизатора + $this->view->rows = static::preprocessing($this->account, model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'sended' => true, + 'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'), + 'errors' => self::parse_only_text($this->errors) + ] + ); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } else throw new exception('Не удалось отправить сообщение'); + } else throw new exception('Вы не авторизованы для чтения этого чата'); + } else if ($parameters['chat'] === 'market') { + // Магазин + + if ( + $this->account->type === 'operator' + || $this->account->type === 'administrator' + || ($this->account->type === 'market' && $task->market === account::market($this->account->getId())?->getKey()) + ) { + // Авторизован аккаунт (если магазин, то назначен на эту заявку) + + // Инициализация значения по умолчанию + if (!isset($task->chats['market'])) $task->chats = ['market' => []] + ($task->chats ?? []); + + // Запись сообщения + $task->chats = [ + 'market' => [ + count($task->chats['market']) => [ + 'from' => [ + '_key' => $this->account->getKey(), + 'type' => $this->account->type + ], + 'type' => $parameters['type'] ?? 'message', + 'text' => $parameters['text'], + 'readed' => [$this->account->getKey() => 0], + 'date' => time() + ] + ] + $task->chats['market'] + ] + $task->chats; + + if ($parameters['type'] === 'problem') { + // Сообщение о проблеме + + // Изменение статуса наличия проблемы + $task->problematic = true; + } else if ($parameters['type'] === 'solution') { + // Сообщение о решении проблемы + + // Изменение статуса наличия проблемы + $task->problematic = false; + } + + if (_core::update($task)) { + // Записано в ArangoDB + + // Инициализация строки в глобальную переменную шаблонизатора + $this->view->rows = static::preprocessing($this->account, model::list(before: 'FILTER task._key == "' . $parameters['task'] . '"', amount: 1)); + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'sended' => true, + 'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'tasks.html'), + 'errors' => self::parse_only_text($this->errors) + ] + ); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } else throw new exception('Не удалось отправить сообщение'); + } else throw new exception('Вы не авторизованы для чтения этого чата'); + } + } 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() + ]; + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'sended' => false, + 'errors' => self::parse_only_text($this->errors) + ] + ); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } + } } diff --git a/mirzaev/ebala/system/controllers/worker.php b/mirzaev/ebala/system/controllers/worker.php index 8f2c6b6..5575e2e 100755 --- a/mirzaev/ebala/system/controllers/worker.php +++ b/mirzaev/ebala/system/controllers/worker.php @@ -7,12 +7,18 @@ namespace mirzaev\ebala\controllers; // Файлы проекта use mirzaev\ebala\controllers\core, mirzaev\ebala\controllers\traits\errors, + mirzaev\ebala\models\account, mirzaev\ebala\models\registry, - mirzaev\ebala\models\worker as model; + mirzaev\ebala\models\worker as model, + mirzaev\ebala\models\core as _core; // Библиотека для ArangoDB use ArangoDBClient\Document as _document; +// Встроенные библиотеки +use exception, + datetime; + /** * Контроллер сотрудника * @@ -33,7 +39,7 @@ 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) { // Перебор фильтров статусов @@ -78,72 +84,125 @@ final class worker extends core if (isset($parameters['page'])) $this->session->write(['workers' => ['page' => $parameters['page']]]); else if (empty($this->session->buffer[$_SERVER['INTERFACE']]['workers']['page'])) $this->session->write(['workers' => ['page' => 1]]); - // Инициализация буфера AQL-выражения для инъекции фильтра по интервалу - $polysemantic = ''; + // Инициализация буферов AQL-выражений для инъекции фильтра по статусам + $filters_statuses_before_and = ''; + $filters_statuses_before_or = ''; + $filters_statuses_after_and = ''; + $filters_statuses_after_or = ''; - foreach (['rating'] as $name) { - // Перебор фильтров с произвольными значениями (И) - - // Инициализация значения (приоритет у cookie) - $value = $_COOKIE["workers_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['workers']['filters'][$name] ?? null; - - // Найдено значение? - if ($value === null) continue; - - // Генерация AQL-выражения для инъекции в строку запроса - if ($name === 'rating' && $value > 0) $polysemantic .= " && worker.rating >= $value"; - } - - // Очистка от бинарных операторов сравнения с только одним операндом (крайние) - $polysemantic = trim(trim(trim($polysemantic), '&&')); - - // Инициализация допустимых статусов - $statuses = ['active', 'inactive', 'fined', 'decent', 'hided', 'fired']; - - // Инициализация буфера AQL-выражения для инъекции фильтра по статусам (И) - $statuses_and = ''; - - foreach ($statuses as $name) { - // Перебор фильтров статусов (И) + foreach (['active', 'inactive', 'fined', 'decent', 'hided', 'fired'] as $name) { + // Перебор фильтров статусов // Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue) if (empty($value = $_COOKIE["workers_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['workers']['filters'][$name] ?? 0)) continue; + // Конвертация ярлыков + $converted = match ($name) { + 'inactive' => 'active', + 'decent' => 'fined', + default => $name + }; + + $target = match ($converted) { + 'active' => 'account', + default => 'worker' + }; + + // Генерация выражения + $expression = "$target.$converted == " . ($name === $converted ? 'true' : 'false'); + // Генерация AQL-выражения для инъекции в строку запроса - if ($value === '1') $statuses_and .= " && worker.$name == true"; + if ($value === '1') ${'filters_statuses_' . ($target === 'account' ? 'before' : 'after') . '_and'} .= " && " . $expression; + else if ($value === '2') ${'filters_statuses_' . ($target === 'account' ? 'before' : 'after') . '_or'} .= " || " . $expression; } // Очистка от бинарных операторов сравнения с только одним операндом (крайние) - $statuses_and = trim(trim(trim($statuses_and), '&&')); - - // Инициализация буфера AQL-выражения для инъекции фильтра по статусам (ИЛИ) - $statuses_or = ''; - - foreach ($statuses as $name) { - // Перебор фильтров статусов (ИЛИ) - - // Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue) - if (empty($value = $_COOKIE["workers_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['workers']['filters'][$name] ?? 0)) continue; - - // Генерация AQL-выражения для инъекции в строку запроса - if ($value === '2') $statuses_or .= " || worker.$name == true"; - } - - // Очистка от бинарных операторов сравнения с только одним операндом (крайние) - $statuses_or = trim(trim(trim($statuses_or), '||')); + $filters_statuses_before_and = trim(trim(trim($filters_statuses_before_and), '&&')); + $filters_statuses_before_or = trim(trim(trim($filters_statuses_before_or), '||')); + $filters_statuses_after_and = trim(trim(trim($filters_statuses_after_and), '&&')); + $filters_statuses_after_or = trim(trim(trim($filters_statuses_after_or), '||')); // Инициализация буфера с объёдинёнными буферами c AQL-выражениям "И" и "ИЛИ" - $statuses_merged = (empty($statuses_and) ? '' : "($statuses_and)") . (empty($statuses_or) ? '' : (empty($statuses_and) ? '' : ' || ') . "($statuses_or)"); + $filters_statuses_before_merged = (empty($filters_statuses_before_and) ? '' : "($filters_statuses_before_and)") . (empty($filters_statuses_before_or) ? '' : (empty($filters_statuses_before_and) ? '' : ' || ') . "($filters_statuses_before_or)"); + $filters_statuses_after_merged = (empty($filters_statuses_after_and) ? '' : "($filters_statuses_after_and)") . (empty($filters_statuses_after_or) ? '' : (empty($filters_statuses_after_and) ? '' : ' || ') . "($filters_statuses_after_or)"); // Инициализация общего буфера с AQL-выражениями - $filters = ''; + $filters_before = ''; + $filters_after = ''; - // Объединение фильров в единую строку с AQL-выражениями для инъекции - if (!empty($statuses_merged)) $filters .= empty($filters) ? $statuses_merged : " && ($statuses_merged)"; - if (!empty($polysemantic)) $filters .= empty($filters) ? $polysemantic : " && $polysemantic"; + // Объединение фильтров в единую строку с AQL-выражениями для инъекции + if (!empty($filters_statuses_before_merged)) $filters_before .= empty($filters_before) ? $filters_statuses_before_merged : " && ($filters_statuses_before_merged)"; + 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'] ?? ''; + if (mb_strlen($search) < 3) $search = null; + $search_query = empty($search) + ? null + : << 5 && LEVENSHTEIN_MATCH(a._key, TOKENS(@search, 'text_en')[0], 2, 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)) + || (LENGTH(@search) > 7 && LEVENSHTEIN_MATCH(a.address, TOKENS(@search, 'text_ru')[0], 2, true)) + || (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(a.city, TOKENS(@search, 'text_ru')[0], 1, true)) + || (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(a.district, TOKENS(@search, 'text_ru')[0], 1, true)) + || (LENGTH(@search) > 5 && LEVENSHTEIN_MATCH(a.number, TOKENS(@search, 'text_en')[0], 2, true)) + || (LENGTH(@search) > 5 && LEVENSHTEIN_MATCH(a.mail, TOKENS(@search, 'text_en')[0], 2, true)) + || (LENGTH(@search) > 5 && LEVENSHTEIN_MATCH(a.passport, TOKENS(@search, 'text_ru')[0], 1, true)) + || (LENGTH(@search) > 6 && LEVENSHTEIN_MATCH(a.department.number, TOKENS(@search, 'text_en')[0], 1, true)) + || (LENGTH(@search) > 5 && LEVENSHTEIN_MATCH(a.department.address, TOKENS(@search, 'text_ru')[0], 1, true)) + || (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; // Инициализация данных для генерации HTML-документа с таблицей - $this->view->rows = registry::workers(before: empty($filters) ? null : "FILTER ($filters)", page: (int) $this->session->buffer[$_SERVER['INTERFACE']]['workers']['page']); + $this->view->rows = registry::workers( + before: sprintf( + <<session->buffer[$_SERVER['INTERFACE']]['workers']['page'], + target: empty($search) ? account::COLLECTION : 'registry_accounts', + binds: empty($search) ? [] : [ + 'search' => $search + ] + ); // Запись в cookie (только таким методом можно записать "hostonly: true") setcookie( @@ -169,6 +228,282 @@ final class worker extends core return null; } + /** + * Создать + * + * @param array $parameters Параметры запроса + * + * @return void В буфер вывода JSON-документ с запрашиваемыми параметрами + */ + public function create(array $parameters = []): void + { + if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) { + // Авторизован аккаунт администратора или оператора + + // Инициализация буфера ошибок + $this->errors['account'] ??= []; + + try { + // Проверка наличия переданного пароля + if (!isset($parameters['account_password'])) throw new exception('Не получен пароль'); + else if (strlen($parameters['account_password']) < 6) throw new exception('Пароль должен быть не менее 6 символов'); + else if (!empty($parameters['worker_number']) && strlen($parameters['worker_number']) < 11) throw new exception('Несоответствие формату SIM-номера сотрудника'); + 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('Несоответствие формату почты аккаунта сотрудника'); + + // Универсализация + /* $parameters['worker_number'] = (int) $parameters['worker_number']; */ + /* $parameters['account_number'] = (int) $parameters['account_number']; */ + 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['worker_hiring'])) $parameters['worker_hiring'] = DateTime::createFromFormat('Y-m-d', $parameters['worker_hiring'])->getTimestamp(); + + // Создание аккаунта + $account = account::create( + data: [ + 'type' => 'worker', + 'name' => [ + 'first' => $parameters['account_name_first'], + 'second' => $parameters['account_name_second'], + 'last' => $parameters['account_name_last'] + ], + 'number' => $parameters['account_number'] === 0 ? '' : $parameters['account_number'], + 'mail' => $parameters['account_mail'], + 'password' => sodium_crypto_pwhash_str( + $parameters['account_password'], + SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE + ), + 'commentary' => $parameters['account_commentary'] + ], + errors: $this->errors['account'] + ); + + // Проверка существования созданного аккаунта + if (empty($account)) 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() + ]; + } + + // Инициализация идентификатора аккаунта (ключ документа инстанции аккаунта в базе данных) + $_key = preg_replace('/.+\//', '', $account ?? ''); + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode( + [ + 'clipboard' => empty($this->errors['account']) ? << self::parse_only_text($this->errors['account']) + ] + ); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + 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 (empty($worker)) throw new exception('Не удалось создать сотрудника'); + + // Создание ребра: account -> worker + account::connect($account, $worker, 'worker', $this->errors['account']); + } catch (exception $e) { + // Write to the errors registry + $this->errors['account'][] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + } + } + } + + /** + * Прочитать данные + * + * @param array $parameters Параметры запроса + */ + public function fields(array $parameters = []): ?string + { + if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) { + // Авторизован аккаунт администратора или оператора + + // Инициализация данных сотрудника + $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(); + + if (!empty($worker)) { + // Найдены данные сотрудника + + // Запись заголовков ответа + header('Content-Type: application/json'); + header('Content-Encoding: none'); + header('X-Accel-Buffering: no'); + + // Инициализация буфера вывода + ob_start(); + + // Генерация ответа + echo json_encode($worker); + + // Запись заголовков ответа + header('Content-Length: ' . ob_get_length()); + + // Отправка и деинициализация буфера вывода + ob_end_flush(); + flush(); + } + } + + // Возврат (провал) + return null; + } + + /** + * Обновить данные + * + * @param array $parameters Параметры запроса + */ + public function update(array $parameters = []): ?string + { + if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) { + // Авторизован аккаунт администратора или оператора + + // Инициализация данных сотрудника + $worker = model::read('d._key == "' . $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 ($parameters['name_first'] !== $worker->name['first']) $worker->name = ['first' => $parameters['name_first']] + $worker->name; + if ($parameters['name_second'] !== $worker->name['second']) $worker->name = ['second' => $parameters['name_second']] + $worker->name; + if ($parameters['name_last'] !== $worker->name['last']) $worker->name = ['last' => $parameters['name_last']] + $worker->name; + if ($parameters['number'] !== $worker->number) + if (mb_strlen($parameters['number']) === 11) $worker->number = $parameters['number']; + else throw new exception('Номер должен состоять из 11 символов'); + if ($parameters['mail'] !== $worker->mail) $worker->mail = $parameters['mail']; + if ($parameters['birth'] !== $worker->birth) $worker->birth = $parameters['birth']; + if ($parameters['passport'] !== $worker->passport) $worker->passport = $parameters['passport']; + if ($parameters['issued'] !== $worker->issued) $worker->issued = $parameters['issued']; + if ($parameters['department_number'] !== $worker->department['number']) $worker->department = ['number' => $parameters['department_number']] + $worker->department; + if ($parameters['department_address'] !== $worker->department['address']) $worker->department = ['address' => $parameters['department_address']] + $worker->department; + if ($parameters['requisites'] !== $worker->requisites) $worker->requisites = $parameters['requisites']; + if ($parameters['payment'] !== $worker->payment) $worker->payment = $parameters['payment']; + if ($parameters['tax'] !== $worker->tax) $worker->tax = $parameters['tax']; + 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['hiring'] !== $worker->hiring) $worker->hiring = $parameters['hiring']; + + if (_core::update($worker)) { + // Записаны данные сотрудника + + // Инициализация строки в глобальную переменную шаблонизатора + $this->view->rows = registry::workers( + before: sprintf( + "FILTER a._id == '%s' && a.deleted != true", + $worker->getId() + ), + after: <<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 . 'workers.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('Не удалось найти аккаунт'); + } + + // Возврат (провал) + return null; + } + /** * Прочитать данные сотрудников для * @@ -180,7 +515,7 @@ final class worker extends core // Авторизован аккаунт оператора или магазина // Инициализация данных сотрудников - $this->view->workers = model::read(filter: 'd.status == "active"', amount: 10000, return: '{ id: d.id, name: d.name }'); + $this->view->workers = model::read(filter: 'd.active == true', amount: 10000, return: '{ _key: d._key, name: d.name }'); // Универсализация if ($this->view->workers instanceof _document) $this->view->workers = [$this->view->workers]; diff --git a/mirzaev/ebala/system/models/account.php b/mirzaev/ebala/system/models/account.php index 7c53d17..1a5e147 100755 --- a/mirzaev/ebala/system/models/account.php +++ b/mirzaev/ebala/system/models/account.php @@ -8,13 +8,13 @@ namespace mirzaev\ebala\models; use mirzaev\ebala\models\traits\instance, mirzaev\ebala\models\traits\status; +// Библиотека для ArangoDB +use ArangoDBClient\Document as _document; + // Фреймворк ArangoDB use mirzaev\arangodb\collection, mirzaev\arangodb\document; -// Библиотека для ArangoDB -use ArangoDBClient\Document as _document; - // Встроенные библиотеки use exception; @@ -60,7 +60,7 @@ final class account extends core $this->document = $account->document; // Связь сессии с аккаунтом - $session->connect($this, $errors); + session::connect($session->getId(), $this->document->getId(), $errors); return $this; } else { @@ -79,7 +79,7 @@ final class account extends core if (!empty($session->buffer['worker']['entry']['password'])) { // Найден пароль в буфере сессии - if (($account = self::read('d.number == ' . $session->buffer['worker']['entry']['number'], amount: 1, errors: $errors)) instanceof _document) { + if (($account = self::read('d.number == "' . $session->buffer['worker']['entry']['number'] . '" && d.type == "worker"', amount: 1, errors: $errors)) instanceof _document) { // Найден аккаунт сотрудника (игнорируются ошибки) if (sodium_crypto_pwhash_str_verify($account->password, $session->buffer['worker']['entry']['password'])) { @@ -89,7 +89,7 @@ final class account extends core $this->document = $account; // Связь сессии с аккаунтом - $session->connect($this, $errors); + session::connect($session->getId(), $this->document->getId(), $errors); // Удаление использованных данных из буфера сессии $session->write(['entry' => ['number' => null, 'password' => null]]); @@ -99,40 +99,6 @@ final class account extends core } else throw new exception('Неправильный пароль'); throw new exception('Неизвестная ошибка на этапе проверки пароля'); - } else if ($worker = static::worker($session->buffer['worker']['entry']['number'])) { - // Найден сотрудник - - if (self::create([ - 'number' => $session->buffer['worker']['entry']['number'], - 'password' => sodium_crypto_pwhash_str( - $session->buffer['worker']['entry']['password'], - SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE, - SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE - ), - 'status' => 'active', - 'type' => 'worker' - ], $errors)) { - // Зарегистрирован аккаунт - - if (($account = self::read('d.number == "' . $session->buffer['worker']['entry']['number'] . '"', amount: 1, errors: $errors)) instanceof _document) { - // Найден аккаунт - - // Инициализация инстанции документа аккаунта в базе данных - $this->document = $account; - - // Связь сессии с аккаунтом - $session->connect($this, $errors); - - // Связь аккаунта с сотрудником - $this->connect($worker, $errors); - - // Удаление использованных данных из буфера сессии - $session->write(['entry' => ['number' => null, 'password' => null]]); - - // Выход (успех) - return $this; - } else throw new exception('Не удалось аутентифицировать аккаунт после его регистрации'); - } else throw new exception('Не удалось зарегистрировать аккаунт'); } else throw new exception('Не найден аккаунт'); } else throw new exception('Не найден пароль в буфере сессии'); } else if (!empty($session->buffer['operator']['entry']['_key'])) { @@ -141,7 +107,7 @@ final class account extends core if (!empty($session->buffer['operator']['entry']['password'])) { // Найден пароль в буфере сессии - if (($account = self::read('d._key == "' . $session->buffer['operator']['entry']['_key'] . '"', amount: 1)) instanceof _document) { + if (($account = self::read('d._key == "' . $session->buffer['operator']['entry']['_key'] . '" && d.type == "operator"', amount: 1)) instanceof _document) { // Найден аккаунт оператора (игнорируются ошибки) if (sodium_crypto_pwhash_str_verify($account->password, $session->buffer['operator']['entry']['password'])) { @@ -151,7 +117,7 @@ final class account extends core $this->document = $account; // Связь сессии с аккаунтом - $session->connect($this, $errors); + session::connect($session->getId(), $this->document->getId(), $errors); // Удаление использованных данных из буфера сессии $session->write(['entry' => ['_key' => null, 'password' => null]]); @@ -163,36 +129,32 @@ final class account extends core throw new exception('Неизвестная ошибка на этапе проверки пароля'); } } else throw new exception('Не найден пароль в буфере сессии'); - } else if (!empty($session->buffer['market']['entry'])) { + } else if (!empty($session->buffer['market']['entry']['_key'])) { // Найден идентификатор магазина в буфере сессии if (!empty($session->buffer['market']['entry']['password'])) { // Найден пароль в буфере сессии - if (($market = market::read('d.id == "' . $session->buffer['market']['entry']['id'] . '"', amount: 1)) instanceof _document) { - // Найден магазин (игнорируются ошибки) + if (($account = self::read('d._key == "' . $session->buffer['market']['entry']['_key'] . '" && d.type == "market"', amount: 1)) instanceof _document) { + // Найден аккаунт (игнорируются ошибки) - if (($account = market::account($market->getId())) instanceof _document) { - // Найден аккаунт (игнорируются ошибки) + if (sodium_crypto_pwhash_str_verify($account->password, $session->buffer['market']['entry']['password'])) { + // Аутентифицирован аккаунт (прошёл проверку пароль) - if (sodium_crypto_pwhash_str_verify($account->password, $session->buffer['market']['entry']['password'])) { - // Аутентифицирован аккаунт (прошёл проверку пароль) + // Инициализация инстанции документа аккаунта в базе данных + $this->document = $account; - // Инициализация инстанции документа аккаунта в базе данных - $this->document = $account; + // Связь сессии с аккаунтом + session::connect($session->getId(), $this->document->getId(), $errors); - // Связь сессии с аккаунтом - $session->connect($this, $errors); + // Удаление использованных данных из буфера сессии + $session->write(['entry' => ['_key' => null, 'password' => null]]); - // Удаление использованных данных из буфера сессии - $session->write(['entry' => ['id' => null, 'password' => null]]); + // Выход (успех) + return $this; + } else throw new exception('Неправильный пароль'); - // Выход (успех) - return $this; - } else throw new exception('Неправильный пароль'); - - throw new exception('Неизвестная ошибка на этапе проверки пароля'); - } + throw new exception('Неизвестная ошибка на этапе проверки пароля'); } } else throw new exception('Не найден пароль в буфере сессии'); } else if (!empty($session->buffer['administrator']['entry'])) { @@ -201,7 +163,7 @@ final class account extends core if (!empty($session->buffer['administrator']['entry']['password'])) { // Найден пароль в буфере сессии - if (($account = self::read('d._key == "' . $session->buffer['administrator']['entry']['_key'] . '"', amount: 1)) instanceof _document) { + if (($account = self::read('d._key == "' . $session->buffer['administrator']['entry']['_key'] . '" && d.type == "administrator"', amount: 1)) instanceof _document) { // Найден аккаунт администратора (игнорируются ошибки) if (sodium_crypto_pwhash_str_verify($account->password, $session->buffer['administrator']['entry']['password'])) { @@ -211,7 +173,7 @@ final class account extends core $this->document = $account; // Связь сессии с аккаунтом - $session->connect($this, $errors); + session::connect($session->getId(), $this->document->getId(), $errors); // Удаление использованных данных из буфера сессии $session->write(['entry' => ['_key' => null, 'password' => null]]); @@ -239,30 +201,40 @@ final class account extends core } /** - * Найти сотрудника + * Найти связанного с аккаунтом сотрудника * - * @param int|string $number Номер + * @param string $_id Идентификатор аккаунта связанного с сотрудником * @param array &$errors Реестр ошибок * * @return ?_document Инстанция документа сотрудника в базе данных, если найдена */ - public static function worker(int|string $number, array &$errors = []): ?_document + public static function worker(string $_id, array &$errors = []): ?_document { try { if (collection::init(static::$arangodb->session, worker::COLLECTION)) { // Инициализирована коллекция + // Поиск сотрудника $worker = collection::search( static::$arangodb->session, sprintf( <<<'AQL' FOR d IN %s - FILTER d.phone == '%s' - SORT d.created DESC + LET e = ( + FOR e IN %s + FILTER e._from == '%s' + SORT e.created DESC, e._key DESC + LIMIT 1 + RETURN e + ) + FILTER d._id == e[0]._to && d.active == true + SORT d.created DESC, d._key DESC + LIMIT 1 RETURN d AQL, worker::COLLECTION, - $number + static::COLLECTION . '_edge_worker', + $_id ) ); @@ -283,12 +255,12 @@ final class account extends core } /** - * Найти связанный магазин + * Найти связанный с аккаунтом магазин * * @param string $_id Идентификатор аккаунта связанного с магазином * @param array &$errors Реестр ошибок * - * @return _document|null Инстанция документа магазина в базе данных, если найдена + * @return ?_document Инстанция документа магазина в базе данных, если найдена */ public static function market(string $_id, array &$errors = []): ?_document { @@ -300,7 +272,8 @@ final class account extends core ) { // Инициализированы коллекции - return collection::search( + // Поиск магазина + $market = collection::search( static::$arangodb->session, sprintf( <<<'AQL' @@ -308,12 +281,12 @@ final class account extends core LET e = ( FOR e IN %s FILTER e._from == '%s' - SORT e._key DESC + SORT e.created DESC, e._key DESC LIMIT 1 RETURN e ) - FILTER d._id == e[0]._to && d.status == 'active' - SORT d.created DESC + FILTER d._id == e[0]._to && d.active == true + SORT d.created DESC, d._key DESC LIMIT 1 RETURN d AQL, @@ -322,6 +295,8 @@ final class account extends core $_id ) ); + + return $market instanceof _document ? $market : throw new exception('Не удалось найти инстанцию магазина в базе данных'); } else throw new exception('Не удалось инициализировать коллекции'); } catch (exception $e) { // Запись в реестр ошибок @@ -341,43 +316,45 @@ final class account extends core * * Ищет связь аккаунта с сотрудником, если не находит, то создаёт её * - * @param _document $worker Инстанция документа в базе данных сотрудника + * @param string $worker Идентификатор инстанции документа аккаунта в базе данных + * @param string $target Идентификатор инстанции документа цели в базе данны (подразумевается сотрудник или магазин) + * @param string $type Тип подключения (worker|market) * @param array &$errors Реестр ошибок * * @return bool Связан аккаунт с сотрудником? */ - public function connect(_document $worker, array &$errors = []): bool + public static function connect(string $account, string $target, string $type = 'worker', array &$errors = []): bool { try { if ( - collection::init($this::$arangodb->session, 'worker') - && collection::init($this::$arangodb->session, self::COLLECTION) - && collection::init($this::$arangodb->session, self::COLLECTION . '_edge_worker', true) + collection::init(static::$arangodb->session, $type) + && collection::init(static::$arangodb->session, self::COLLECTION) + && collection::init(static::$arangodb->session, self::COLLECTION . "_edge_$type", true) ) { // Инициализированы коллекции if ( - collection::search($this::$arangodb->session, sprintf( + collection::search(static::$arangodb->session, sprintf( <<document->getId(), - $worker->getId() + self::COLLECTION . '_edge_' . worker::COLLECTION, + $account, + $target )) instanceof _document - || document::write($this::$arangodb->session, self::COLLECTION . '_edge_worker', [ - '_from' => $this->document->getId(), - '_to' => $worker->getId() + || $id = document::write(static::$arangodb->session, self::COLLECTION . "_edge_$type", [ + '_from' => $account, + '_to' => $target ]) ) { - // Найдено, либо создано ребро: account -> worker + // Найдено, либо создано ребро: account -> $type return true; - } else throw new exception('Не удалось создать ребро: account -> worker'); + } else throw new exception("Не удалось создать ребро: account -> $type"); } else throw new exception('Не удалось инициализировать коллекцию'); } catch (exception $e) { // Запись в реестр ошибок @@ -391,19 +368,20 @@ final class account extends core return false; } + /** * Создать * * @param array $data Данные аккаунта * @param array &$errors Реестр ошибок * - * @return bool Создан аккаунт? + * @return string|null Идентификатор документа, если он был создан */ - public static function create(array $data = [], array &$errors = []): bool + public static function create(array $data = [], array &$errors = []): ?string { try { if (collection::init(static::$arangodb->session, self::COLLECTION)) - if (document::write(static::$arangodb->session, self::COLLECTION, $data)) return true; + if ($id = document::write(static::$arangodb->session, self::COLLECTION, $data + ['active' => true])) return $id; else throw new exception('Не удалось создать аккаунт'); else throw new exception('Не удалось инициализировать коллекцию'); } catch (exception $e) { @@ -416,7 +394,7 @@ final class account extends core ]; } - return false; + return null; } /** diff --git a/mirzaev/ebala/system/models/market.php b/mirzaev/ebala/system/models/market.php index dbdbdd6..318d8f8 100755 --- a/mirzaev/ebala/system/models/market.php +++ b/mirzaev/ebala/system/models/market.php @@ -9,7 +9,8 @@ use mirzaev\ebala\models\traits\instance, mirzaev\ebala\models\traits\status; // Фреймворк ArangoDB -use mirzaev\arangodb\collection; +use mirzaev\arangodb\collection, + mirzaev\arangodb\document; // Библиотека для ArangoDB use ArangoDBClient\Document as _document; @@ -63,6 +64,34 @@ final class market extends core } } + /** + * Создать + * + * @param array $data Данные + * @param array &$errors Реестр ошибок + * + * @return string|null Идентификатор документа, если он был создан + */ + public static function create(array $data = [], array &$errors = []): ?string + { + try { + if (collection::init(static::$arangodb->session, self::COLLECTION)) + if ($id = document::write(static::$arangodb->session, self::COLLECTION, $data + ['active' => true])) return $id; + else throw new exception('Не удалось создать магазин'); + else throw new exception('Не удалось инициализировать коллекцию'); + } catch (exception $e) { + // Запись в реестр ошибок + $errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + } + + return null; + } + /** * Найти связанный аккаунт * @@ -93,7 +122,7 @@ final class market extends core LIMIT 1 RETURN e ) - FILTER d._id == e[0]._from && d.status == "active" + FILTER d._id == e[0]._from && d.active == true SORT d.created DESC LIMIT 1 RETURN d diff --git a/mirzaev/ebala/system/models/registry.php b/mirzaev/ebala/system/models/registry.php index eaf0011..042f68e 100755 --- a/mirzaev/ebala/system/models/registry.php +++ b/mirzaev/ebala/system/models/registry.php @@ -14,9 +14,6 @@ use mirzaev\ebala\models\traits\instance, // Фреймворк ArangoDB use mirzaev\arangodb\collection; -// Библиотека для ArangoDB -use ArangoDBClient\Document as _document; - // Встроенные библиотеки use exception; @@ -35,18 +32,24 @@ final class registry extends core /** * Generate workers list * - * @param ?string $before Injection of AQL-code before search + * @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 workers * @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 workers( ?string $before = '', + ?string $after = '', int $amount = 100, int $page = 1, - string $sort = 'worker.created DESC, worker._key DESC', + string $sort = 'account.created DESC, account._key DESC', + string $target = account::COLLECTION, + array $binds = [], array &$errors = [] ): array { try { @@ -56,21 +59,25 @@ final class registry extends core // Search the session data in ArangoDB $workers = collection::search(static::$arangodb->session, sprintf( <<session, account::COLLECTION) && collection::init(static::$arangodb->session, market::COLLECTION)) { // Инициализированы коллекции - + // Search the session data in ArangoDB $markets = collection::search(static::$arangodb->session, sprintf( <<session, sprintf( <<session, sprintf( << $e->getLine(), 'stack' => $e->getTrace() ]; +var_dump($errors); } // Exit (fail) diff --git a/mirzaev/ebala/system/models/session.php b/mirzaev/ebala/system/models/session.php index 0089bb8..0e7ac22 100755 --- a/mirzaev/ebala/system/models/session.php +++ b/mirzaev/ebala/system/models/session.php @@ -55,35 +55,12 @@ final class session extends core if (collection::init(static::$arangodb->session, self::COLLECTION)) { // Инициализирована коллекция - // Инициализация идентификации по IP (через прокси или напрямую) - /* $ip = isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? 'd[\'x-forwarded-for\'] == \'' . $_SERVER['HTTP_X_FORWARDED_FOR'] . '\'' : 'd.ip == \'' . $_SERVER['REMOTE_ADDR'] . '\''; - if ( isset($hash) && $session = collection::search($this::$arangodb->session, sprintf( << %d && d.status == 'active' - RETURN d - AQL, - self::COLLECTION, - $ip, - $hash, - time() - )) - ) { - // Received session hash and session found - - // Запись в свойство - $this->document = $session; - } */ - - if ( - isset($hash) - && $session = collection::search($this::$arangodb->session, sprintf( - << %d && d.status == 'active' + FILTER d.hash == '%s' && d.expires > %d && d.active == true RETURN d AQL, self::COLLECTION, @@ -100,7 +77,7 @@ final class session extends core // Запись сессии в базу данных $_id = document::write($this::$arangodb->session, self::COLLECTION, [ - 'status' => 'active', + 'active' => true, 'expires' => $expires ?? time() + 604800, 'ip' => $_SERVER['REMOTE_ADDR'], 'x-forwarded-for' => $_SERVER['HTTP_X_FORWARDED_FOR'] ?? null, @@ -111,7 +88,7 @@ final class session extends core if ($session = collection::search($this::$arangodb->session, sprintf( << %d && d.status == 'active' + FILTER d._id == '$_id' && d.expires > %d && d.active == true RETURN d AQL, self::COLLECTION, @@ -151,7 +128,7 @@ final class session extends core $_document = collection::search(static::$arangodb->session, sprintf( << %d && d.status == 'active' + FILTER d.hash == '$hash' && d.expires > %d && d.active == true RETURN d AQL, self::COLLECTION, @@ -197,18 +174,18 @@ final class session extends core * * @return bool Связана сессия с аккаунтом? */ - public function connect(account $account, array &$errors = []): bool + public static function connect(string $session, string $account, array &$errors = []): bool { try { if ( - collection::init($this::$arangodb->session, self::COLLECTION) - && collection::init($this::$arangodb->session, account::COLLECTION) - && collection::init($this::$arangodb->session, self::COLLECTION . '_edge_' . account::COLLECTION, true) + 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($this::$arangodb->session, sprintf( + collection::search(static::$arangodb->session, sprintf( <<document->getId(), - $account->getId() + $session, + $account )) instanceof _document - || document::write($this::$arangodb->session, self::COLLECTION . '_edge_' . account::COLLECTION, [ - '_from' => $this->document->getId(), - '_to' => $account->getId() + || document::write(static::$arangodb->session, self::COLLECTION . '_edge_' . account::COLLECTION, [ + '_from' => $session, + '_to' => $account ]) ) { // Найдено, либо создано ребро: session -> account diff --git a/mirzaev/ebala/system/models/task.php b/mirzaev/ebala/system/models/task.php index 1bef9a9..ff5db99 100755 --- a/mirzaev/ebala/system/models/task.php +++ b/mirzaev/ebala/system/models/task.php @@ -113,16 +113,23 @@ final class task extends core * @param ?string $after Injection of AQL-code after search of worker and market * @param int $amount Amount of tasks * @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 + * + * @TODO + * 1. Remake task.worker with task.market to separate documents (edges) (not important) */ public static function list( ?string $before = '', ?string $after = '', int $amount = 100, int $page = 1, - string $sort = 'task.created DESC, task._key DESC', + string $sort = 'task.date DESC, task.created DESC, task._key DESC', + string $target = self::COLLECTION, + array $binds = [], array &$errors = [] ): array { try { @@ -133,28 +140,27 @@ final class task extends core ) { // Инициализированы коллекции - // Search the session data in ArangoDB (LET можно заменить на поиск по рёбрам но тут и так сойдёт) + // Search the session data in ArangoDB with VIEW (LET можно заменить на поиск по рёбрам но тут и так сойдёт) $tasks = collection::search(static::$arangodb->session, sprintf( << $e->getLine(), 'stack' => $e->getTrace() ]; + + var_dump($errors); } // Exit (fail) diff --git a/mirzaev/ebala/system/models/traits/status.php b/mirzaev/ebala/system/models/traits/status.php index 89a3349..46e179f 100755 --- a/mirzaev/ebala/system/models/traits/status.php +++ b/mirzaev/ebala/system/models/traits/status.php @@ -24,11 +24,8 @@ trait status public function status(array &$errors = []): ?bool { try { - return match ($this->document->status ?? null) { - 'active' => true, - 'inactive' => false, - default => null - }; + // Чтение и возврат (успех) + return $this->document->active ?? false; } catch (exception $e) { // Запись в реестр ошибок $errors[] = [ @@ -39,6 +36,7 @@ trait status ]; } + // Возврат (провал) return null; } } diff --git a/mirzaev/ebala/system/models/worker.php b/mirzaev/ebala/system/models/worker.php index 82ab627..9e976d7 100755 --- a/mirzaev/ebala/system/models/worker.php +++ b/mirzaev/ebala/system/models/worker.php @@ -8,12 +8,13 @@ namespace mirzaev\ebala\models; use mirzaev\ebala\models\traits\instance, mirzaev\ebala\models\traits\status; -// Фреймворк ArangoDB -use mirzaev\arangodb\collection; - // Библиотека для ArangoDB use ArangoDBClient\Document as _document; +// Фреймворк ArangoDB +use mirzaev\arangodb\collection, + mirzaev\arangodb\document; + // Встроенные библиотеки use exception; @@ -61,6 +62,34 @@ final class worker extends core } } + /** + * Создать + * + * @param array $data Данные + * @param array &$errors Реестр ошибок + * + * @return string|null Идентификатор документа, если он был создан + */ + public static function create(array $data = [], array &$errors = []): ?string + { + try { + if (collection::init(static::$arangodb->session, self::COLLECTION)) + if ($id = document::write(static::$arangodb->session, self::COLLECTION, $data + ['active' => true])) return $id; + else throw new exception('Не удалось создать сотрудника'); + else throw new exception('Не удалось инициализировать коллекцию'); + } catch (exception $e) { + // Запись в реестр ошибок + $errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + } + + return null; + } + /** * Записать * diff --git a/mirzaev/ebala/system/public/css/account.css b/mirzaev/ebala/system/public/css/account.css index e344f4d..cfce2f0 100755 --- a/mirzaev/ebala/system/public/css/account.css +++ b/mirzaev/ebala/system/public/css/account.css @@ -1,8 +1,11 @@ +@charset "UTF-8"; + main { z-index: 1000; - margin: 20vh auto; + margin: 30vh auto; position: relative; height: unset; + min-height: 60vh; display: flex; flex-direction: column; justify-content: unset; @@ -11,12 +14,12 @@ main { } section.panel { - --display: flex; z-index: 1000; width: 400px; position: relative; display: flex; flex-direction: column; + border-radius: 3px; } div.column > section.panel { @@ -92,7 +95,6 @@ section.panel > section.header { align-items: end; animation-duration: 120s; border-radius: 3px 3px 0 0; - background-color: var(--background-above); } section.panel > section.header > :is(h1, h2, h3) { @@ -105,13 +107,16 @@ section.panel > section.body { display: flex; flex-direction: column; border-radius: 0 0 3px 3px; - background-color: var(--background-above); } section.panel > section.postscript { padding: 10px 12px; } +section.panel > section.body *:is(input, textarea) { + --background: var(--snow-deep); +} + section#entry.panel > section.body > form > label > button { height: 100%; padding: 0 10px; diff --git a/mirzaev/ebala/system/public/css/animations.css b/mirzaev/ebala/system/public/css/animations.css index 5f8dd5f..f501469 100755 --- a/mirzaev/ebala/system/public/css/animations.css +++ b/mirzaev/ebala/system/public/css/animations.css @@ -1,38 +1,33 @@ +@charset "UTF-8"; + @keyframes input-error { 0%, 20% { - background-color: var(--input-error); + background-color: var(--clay-above); } 50%, 100% { - background-color: var(--background-above-1); + background-color: var(--background); } } @keyframes icon-error { 0%, - 50% { + 70% { color: var(--icon-error); } - - 80%, - 100% { - color: var(--text-inverse-below-1); - } } @keyframes row-reinitialized { 0%, 20% { - filter: contrast(0.4); - background-color: var(--earth-background); + filter: contrast(0.2); } 50%, 100% { filter: unset; - background-color: var(--background, --earth-background-above); } } diff --git a/mirzaev/ebala/system/public/css/icons/enter.css b/mirzaev/ebala/system/public/css/icons/enter.css new file mode 100755 index 0000000..5a8b2c8 --- /dev/null +++ b/mirzaev/ebala/system/public/css/icons/enter.css @@ -0,0 +1,28 @@ +i.icon.enter, +i.icon.enter::before { + height: var(--height, 6px); + display: block; + box-sizing: border-box; + border-bottom: 2px solid; +} + +i.icon.enter { + --width: 14px; + --height: 6px; + position: relative; + margin-top: -3px; + width: var(--width, 14px); + border-right: 2px solid; + border-bottom-right-radius: 4px; +} + +i.icon.enter::before { + content: ""; + position: absolute; + left: -1px; + bottom: -4px; + width: 6px; + border-left: 2px solid; + transform: rotate(45deg); +} + diff --git a/mirzaev/ebala/system/public/css/icons/nametag.css b/mirzaev/ebala/system/public/css/icons/nametag.css index 8366a37..19a94b5 100755 --- a/mirzaev/ebala/system/public/css/icons/nametag.css +++ b/mirzaev/ebala/system/public/css/icons/nametag.css @@ -9,6 +9,14 @@ i.icon.nametag { border: 2px solid; } +i.icon.nametag.bold { + --width: 8px; + --height: 8px; + width: 8px; + height: 8px; + border: 3px solid; +} + i.icon.nametag::before { left: -5px; top: -5px; @@ -21,6 +29,14 @@ i.icon.nametag::before { box-shadow: -5px -5px 0 -3px, 5px 5px 0 -3px, 5px -5px 0 -3px, -5px 5px 0 -3px; } +i.icon.nametag.bold::before { + left: -6px; + top: -6px; + width: 14px; + height: 14px; + box-shadow: -6px -6px 0 -3px, 6px 6px 0 -3px, 6px -6px 0 -3px, -6px 6px 0 -3px; +} + label > i.icon.nametag:first-of-type { left: 13px; } diff --git a/mirzaev/ebala/system/public/css/icons/shopping_cart.css b/mirzaev/ebala/system/public/css/icons/shopping_cart.css index 33029bf..eb4fadb 100644 --- a/mirzaev/ebala/system/public/css/icons/shopping_cart.css +++ b/mirzaev/ebala/system/public/css/icons/shopping_cart.css @@ -17,6 +17,20 @@ i.icon.shopping.cart { radial-gradient(circle, currentColor 60%, transparent 40%) no-repeat 6px 17px/4px 4px; } +i.icon.shopping.cart.bold { + --height: 22px; + height: 22px; + background: linear-gradient(to left, currentColor 13px, transparent 0) + no-repeat 0px 6px/19px 3px, + linear-gradient(to left, currentColor 14px, transparent 0) no-repeat 4px + 14px/16px 3px, + linear-gradient(to left, currentColor 14px, transparent 0) no-repeat 0 2px/5px + 3px, + radial-gradient(circle, currentColor 60%, transparent 40%) no-repeat 14px + 18px/4px 4px, + radial-gradient(circle, currentColor 60%, transparent 40%) no-repeat 5px + 18px/4px 4px; +} i.icon.shopping.cart::after, i.icon.shopping.cart::before { left: 4px; @@ -30,9 +44,25 @@ i.icon.shopping.cart::before { transform: skew(12deg); background: currentColor; } +i.icon.shopping.cart.bold::after, +i.icon.shopping.cart.bold::before { + width: 3px; +} i.icon.shopping.cart::after { left: 16px; top: 6px; height: 10px; transform: skew(-12deg); } +i.icon.shopping.cart.bold::after { + left: 18px; +} + +label > i.icon.shopping.cart:first-of-type { + left: 10px; + top: 6px; +} + +i.icon.shopping.cart + input { + padding-left: 37px; +} diff --git a/mirzaev/ebala/system/public/css/icons/user.css b/mirzaev/ebala/system/public/css/icons/user.css index 2dc2f43..ec69db7 100644 --- a/mirzaev/ebala/system/public/css/icons/user.css +++ b/mirzaev/ebala/system/public/css/icons/user.css @@ -53,3 +53,12 @@ i.icon.user.bold::after { border-top-left-radius: 4px; border-top-right-radius: 4px; } + +label > i.icon.user:first-of-type { + left: 12px; + top: 7px; +} + +i.icon.user + input { + padding-left: 33px; +} diff --git a/mirzaev/ebala/system/public/css/list.css b/mirzaev/ebala/system/public/css/list.css index 64381c1..b9af32e 100755 --- a/mirzaev/ebala/system/public/css/list.css +++ b/mirzaev/ebala/system/public/css/list.css @@ -17,63 +17,94 @@ section.panel.list.medium { width: 80%; } -section.panel.list > form.row.menu { - margin-bottom: 10px; +section.panel.list > :is(form, search).row.menu { + margin-bottom: 10px;%s" transition: 0s; } -section.panel.list > form.row.menu > label { +section.panel.list > :is(form, search).row.menu > label { height: max-content; min-height: 30px; display: flex; } -section.panel.list > form.row.menu > label:not(.solid) { +section.panel.list > :is(form, search).row.menu > label:not(.solid) { gap: 15px; } -section.panel.list > form.row.menu.wide > label { +section.panel.list > :is(form, search).row.menu.wide > label { height: 36px; } -section.panel.list > form.row.menu:is(.separated, :last-of-type) { +section.panel.list > :is(form, search).row.menu.separated { margin-bottom: 20px; } -section.panel.list > form.row.menu > label > button { - height: 30px; - align-self: center; +div#popup > section.list > div.row.endless { + height: auto; } -section.panel.list > form.row.menu.stretched > label > button, -section.panel.list > form.row.menu.stretched > label > input[type="search"] { +section.panel.list > :is(form, search).row.menu > label > button { + position: relative; + display: flex; + justify-content: center; + align-items: center; + height: 30px; +} + +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 { + content: ""; + left: -12px; + position: absolute; + margin: auto 0px; + height: 80%; + 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"] { flex-grow: 1; } -section.panel.list > form.row.menu.stretched > label > button { +section.panel.list > :is(form, search).row.menu.stretched > label > button { max-width: 250px; } -section.panel.list > form.row.menu > label > input { +section.panel.list > :is(form, search).row.menu > label > input { padding: 0 10px; } -section.panel.list > form.row.menu > label > input:not(.merged) { +section.panel.list > :is(form, search).row.menu > label > input:not(.merged) { border-radius: 3px; } -section.panel.list > form.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 > form.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 { + margin-top: 20px; height: 50px; background-color: var(--background-below-6); } @@ -85,21 +116,37 @@ section.panel.list > div#title > span { } section.panel.list > div.row { - --background: var(--background-above-1); + --gap: 12px; + --background: var(--cloud); position: relative; left: 0px; width: calc(100% - 24px); height: 35px; display: flex; - gap: 12px; - padding: 0 12px; + gap: var(--gap, 12px); + padding: 0 var(--gap, 12px); border-radius: 0px; +} + +section.panel.list > div.row:not(:nth-of-type(1)) { background-color: var(--background); } -section.panel.list > div.row:not(:nth-of-type(1)):hover { +section.panel.list > div.row:not(:nth-of-type(1)) > span { + height: 100%; + line-height: 2.2; + padding: 0; + background-color: var(--background); + box-shadow: var(--box-shadow); + -webkit-box-shadow: var(--box-shadow); + -moz-box-shadow: var(--box-shadow); +} + +section.panel.list > div.row:not(:nth-of-type(1)):is(:hover, :focus) { + --padding-left: 24px; + --padding-right: 24px; left: -12px; - padding: 0 24px; + padding: 0 var(--padding-left, 24px) 0 var(--padding-right, 24px); border-radius: 3px; transition: 0s; } @@ -112,15 +159,15 @@ section.panel.list > div.row:last-of-type { border-radius: 0 0 3px 3px; } -section.panel.list > div.row:hover * { +section.panel.list > div.row:is(:hover, :focus) * { transition: unset; } -section.panel.list > div.row:nth-of-type(2n + 1) { - --background: var(--background-above-2); +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; @@ -139,7 +186,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; @@ -159,50 +206,70 @@ section.panel.list > div.row[data-selected="true"]::after { color: var(--interface-brown); } -section.panel.list > div.row.confirmed { - --background: var(--grass-background-above-2); +section.panel.list > div.row:not(:nth-of-type(1)).confirmed { + --background: var(--grass); } -section.panel.list > div.row.confirmed:nth-of-type(2n + 1) { - --background: var(--grass-background-above-1); +section.panel.list + > div.row:not(:nth-of-type(1)):nth-child(2n + 1).confirmed { + --background: var(--grass-above); } -section.panel.list > div.row.published { - --background: var(--river-background); +section.panel.list > div.row:not(:nth-of-type(1)).published { + --background: var(--river); } -section.panel.list > div.row.published:nth-of-type(2n + 1) { - --background: var(--river-background-above); +section.panel.list + > div.row:not(:nth-of-type(1)):nth-child(2n + 1).published { + --background: var(--river-above); } -section.panel.list > div.row.confirmed.published { - --background: var(--sea-background); +section.panel.list > div.row:not(:nth-of-type(1)).confirmed.published:not(.problematic) { + --background: var(--sea); } -section.panel.list > div.row.confirmed.published:nth-of-type(2n + 1) { - --background: var(--sea-background-above); +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.hided { - /* cursor: not-allowed !important; */ - box-shadow: 0px 0px 0px 1000px rgba(0, 0, 0, 0.2) inset; - -webkit-box-shadow: 0px 0px 0px 1000px rgba(0, 0, 0, 0.2) inset; - -moz-box-shadow: 0px 0px 0px 1000px rgba(0, 0, 0, 0.2) inset; +section.panel.list > div.row:not(:nth-of-type(1)).problematic { + --background: var(--clay); } -/* -section.panel.list > div.row.hided:hover { - box-shadow: unset; - -webkit-box-shadow: unset; - -moz-box-shadow: unset; -} */ +section.panel.list + > div.row:not(:nth-of-type(1)):nth-child(2n + 1).problematic { + --background: var(--clay-above); +} -section.panel.list > div.row.hided * { +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.hided:hover * { +section.panel.list > div.row:not(:nth-of-type(1)).hided:is(:hover, :focus) * { filter: unset; opacity: unset; } @@ -214,8 +281,11 @@ section.panel.list > div.row.reinitialized { } section.panel.list - > div.row:not(:nth-of-type(1), [data-selected="true"]).reinitializable:before { - content: var(--counter, "0"); + > div.row:not( + :nth-of-type(1), + [data-selected="true"] + ).reinitializable:before { + content: attr(data-counter); position: absolute; left: -95px; align-self: center; @@ -223,7 +293,17 @@ section.panel.list text-align: right; font-size: small; font-weight: bold; - color: var(--earth-text-above); + pointer-events: none; + color: var(--earth-text); +} + +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 { @@ -231,6 +311,35 @@ section.panel.list > div.row > span { margin: auto 0; padding: 8px 0; text-align: left; + transition: 0s; +} + +section.panel.list > div.row:is(:hover, :focus) > span { + transition: 0s; +} + +section.panel.list > div.row > span:not(:first-child) { + --padding-left: calc(var(--gap) / 2); +} + +section.panel.list > div.row > span:not(:last-child) { + --padding-right: calc(var(--gap) / 2); +} + +section.panel.list > div.row > span:first-child { + border-radius: 3px 0 0 3px; +} + +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 { + --padding-left: var(--gap, 12px); +} + +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 { @@ -249,3 +358,63 @@ section.panel.list > div.row > span[onclick] { 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)) { + --margin: calc(var(--gap) / 2); + --border-left: calc(var(--padding-left, var(--margin, 0px)) * -1); + --border-right: var(--padding-right, var(--margin, 0px)); + --background: var(--cloud-rainy); + --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)) { + --background: var(--cloud-rainy-above); +} + +section.panel.list > div.row: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)) { + --background: var(--river-deep-above); +} + +section.panel.list > div.row: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)) { + --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)) { + --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)) { + --background: var(--sea-deep-above); +} + +section.panel.list > div.row: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)) { + --background: var(--clay-important-above); +} + +section.panel.list > div.row: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)) { + --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)) { + --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)) { + --background: var(--sand-important-above); +} diff --git a/mirzaev/ebala/system/public/css/main.css b/mirzaev/ebala/system/public/css/main.css index 497197d..30f7f81 100755 --- a/mirzaev/ebala/system/public/css/main.css +++ b/mirzaev/ebala/system/public/css/main.css @@ -1,112 +1,17 @@ -/*@media (prefers-color-scheme: above) {*/ -:root { - --background-above-5: #a69c9c; - --background-above-4: #b9b8b8; - --background-above-3: #ecdfdf; - --background-above-2: #f0eded; - --background-above-1: #fffbfb; - --background-above: #f4eaea; - --background: #e8d9d9; - --background-below: #d7c5c5; - --background-below-6: #8e8282; - --background-inverse: #221e1e; - --background-inverse-below: #120f0f; - --node-background-important: #c3eac3; - --node-background-completed: #b0c0b0; - --node-background: #bdb; - --connection: #b2b7b2; - --connection-completed: #d1d1d1; - --text: #151313; - --text-hover: #463e3e; - --text-active: #0e0e0e; - --text-inverse-above: #fff; - --text-inverse: #efefef; - --text-inverse-below: #d0d0d0; - --text-inverse-below-1: #bbadad; +@charset "UTF-8"; - /* --beige-text-above: #defaff; - --beige-text: #806d64; - --beige-text-below: #72c2d0; - --beige-background-above: #ffffda; - --beige-background: #f4f4e1; - --beige-background-below: #f4f4e1; */ - - --clay-text-above: #ffe9be; - --clay-text: #f7d1a1; - --clay-text-below: #c8af7d; - --clay-background-above-1: #dc4343; - --clay-background-above: #bf3737; - --clay-background: #a43333; - --clay-background-below: #8d2a2a; - --clay-background-below-1: #792727; - - --grass-text-above: #fcffae; - --grass-text: #fbff80; - --grass-text-below: #e3e85e; - --grass-background-above-2: #79b757; - --grass-background-above-1: #89c866; - --grass-background-above: #42ce1c; - --grass-background: #3dbb1a; - --grass-background-below: #38a818; - - --earth-text-above: #975151; - --earth-text: #794040; - --earth-text-below: #511f1f; - --earth-background-above: #d9c5b3; - --earth-background: #b0a296; - --earth-background-below: #918377; - - --sand-text-above: #c8b25f; - --sand-text: #b0844a; - --sand-text-below: #976b31; - --sand-text-below-1: #6a4a1f; - --sand-background-above: #fbf4d9; - --sand-background: #fff1bb; - --sand-background-below: #dfc79a; - - --river-text-above: #2b91c4; - --river-text: #335cbb; - --river-text-below: #2b4480; - --river-background-above: #bbc9ff; - --river-background: #9dadec; - --river-background-below: #7a8cd7; - - --sea-text-above: #ccf8ff; - --sea-text: #b7f1fb; - --sea-text-below: #99d5df; - --sea-background-above: #6a68dd; - --sea-background: #5d5bc1; - --sea-background-below: #504f9f; - - --sky-text-above: #43b0c1; - --sky-text: #1e4c53; - --sky-text-below: ; - --sky-background-above: #dfffff; - --sky-background: #e6f9ff; - --sky-background-below: ; - - --input-error: #d25050; - --icon-error: #792323; - --interface-brown: #9f704e; - --menu-background: #775653; - --menu-text-above: #ffd88e; - /* --menu-text: #cab59b; */ - --menu-text: #d0b88a; - --menu-text-below: #a48f68; - --input-beige: #fffce8; - --input-clay: #fff8f8; +* { + text-decoration: none; + outline: none; + border: none; + color: var(--coal-text); + font-family: Fira, sans-serif; + transition: 0.1s ease-out; } -/*}*/ ::selection { color: var(--clay-text-above); - background-color: var(--clay-background-above); -} - -:root { - --link: #3c76ff; - --link-hover: #6594ff; - --link-active: #3064dd; + background-color: var(--clay-above); } .unselectable { @@ -119,16 +24,26 @@ } .hidden:not(.animation) { + position: absolute; display: none !important; } -* { - text-decoration: none; - outline: none; - border: none; - color: var(--text); - font-family: Fira, sans-serif; - transition: 0.1s ease-out; +*[data-interactive="true"] { + outline: 2px solid #00000000; + cursor: pointer; +} + +*[data-interactive="true"] :not(input, textarea) { + cursor: pointer; +} + +*[data-interactive="true"]:is(:hover, :focus) { + outline: 2px solid var(--earth-above); + background-color: var(--earth-above); +} + +*[data-interactive="true"]:is(:hover, :focus)>label>input { + outline: 2px solid var(--earth-above); } pre, @@ -136,12 +51,20 @@ code { font-family: Hack, monospace; } +input[type="range"] { + --background: rgba(0, 0, 0, 0); +} + button, input[type="submit"], input[type="range"] { cursor: pointer; } +input { + outline: var(--outline-size) var(--outline-type) var(--outline-color); +} + input[type="range"] { margin: unset; background: transparent; @@ -155,8 +78,14 @@ input[type="range"]::-webkit-slider-runnable-track { input[type="range"].sand::-moz-range-track, input[type="range"].sand::-webkit-slider-runnable-track { - border: 1px solid var(--sand-background-below); - background-color: var(--sand-background); + border: 1px solid var(--sand-below); + background-color: var(--sand); +} + +input[type="range"].cloud::-moz-range-track, +input[type="range"].cloud::-webkit-slider-runnable-track { + border: 1px solid var(--cloud-below); + background-color: var(--cloud); } input[type="range"]::-moz-range-progress { @@ -167,6 +96,10 @@ input[type="range"].sand::-moz-range-progress { background-color: var(--sand-text); } +input[type="range"].cloud::-moz-range-progress { + background-color: var(--cloud-text); +} + input[type="range"]::-moz-range-thumb, input[type="range"]::-webkit-slider-thumb { width: 20px; @@ -181,15 +114,34 @@ input[type="range"].sand::-webkit-slider-thumb { background-color: var(--sand-text-above); } +input[type="range"].cloud::-moz-range-thumb, +input[type="range"].cloud::-webkit-slider-thumb { + border: 2px solid var(--cloud-text-below); + background-color: var(--cloud-text-above); +} + input[type="range"]:active::-moz-range-thumb, input[type="range"]:active::-webkit-slider-thumb { cursor: grabbing; } -input[type="range"] + datalist > option { +input[type="range"]+datalist>option { cursor: help; } +:is(input:is([type="text"], [type="password"]), textarea)[required="true"] { + --outline-size: 2px; + --outline-type: solid; + --outline-color: var(--earth-above); +} + +:is(input:is([type="text"], [type="password"]), textarea):not([disabled], [readonly], .borderless):focus { + --outline-size: 2px; + --outline-type: solid; + --outline-color: var(--earth); + transition: 0s; +} + div.range { position: relative; display: flex; @@ -197,15 +149,15 @@ div.range { } div.range.small { - width: 140px; - flex-shrink: 0; + width: 100px; + flex-grow: 1; } -div.range.small > input[type="range"] { +div.range.small>input[type="range"] { margin: auto 0; } -div.range.small > input[type="range"] + i.value { +div.range.small>input[type="range"]+i.value { position: absolute; left: var(--left, 0px); margin-left: -10.5px; @@ -220,20 +172,16 @@ div.range.small > input[type="range"] + i.value { transition: 0s; } -div.range.small > input[type="range"].sand + i.value { +div.range.small>input[type="range"].sand+i.value { color: var(--sand-text-below-1); } -div.range > datalist { +div.range>datalist { display: flex; justify-content: space-between; padding: 0 3px; } -input.beige { - background-color: var(--input-beige); -} - input.clay { background-color: var(--input-clay); } @@ -248,7 +196,7 @@ input.clay { border-radius: 0 3px 3px 0; } -button:is(.transparent, .transparent:hover, .transparent:active) { +button:is(.transparent, .transparent:is(:hover, :focus), .transparent:active) { background: unset; } @@ -256,7 +204,7 @@ a { color: var(--link); } -a:hover { +a:is(:hover, :focus) { color: var(--link-hover); } @@ -266,19 +214,18 @@ a:active { } label { + --row-height: 26px; position: relative; - height: 26px; + height: var(--row-height); display: flex; overflow: hidden; border-radius: 2px; } -label > i:first-of-type { - left: 8px; - top: calc((26px - var(--height)) / 2); +label>i:first-of-type { position: absolute !important; margin: auto; - color: var(--text-inverse-below-1); + color: var(--text-inverse-below-1) !important; } label * { @@ -292,63 +239,95 @@ label * { filter: contrast(0.6) grayscale(0.7); } -select { - padding: 3px 13px; - cursor: pointer; +.rounded { border-radius: 3px; - background-color: var(--background-below); } textarea { - width: 396px; - min-width: calc(100% - 28px); + width: 100%; + min-width: calc(100% - 24px); min-height: 120px; - max-width: calc(100% - 28px); + max-width: calc(100% - 24px); max-height: 300px; - padding: 8px 14px; + padding: 8px 12px; font-size: smaller; overflow: hidden; border-radius: 3px; transition: 0s; } +textarea:not(.horizontal) { + resize: vertical; +} + +select { + --padding-left: 13px; + --padding-right: 13px; + padding: 3px var(--padding-right, 13px) 3px var(--padding-left, 13px); + cursor: pointer; + border-radius: 3px; + background-color: var(--background-below); +} + +select, +small { + width: 60px; +} + +select.medium { + width: 150px; +} + +select.large { + width: 200px; +} + input { border-radius: 3px; } +label>input { + --padding-left: 8px; + --padding-right: 8px; + height: initial; + padding: 0 var(--padding-right, 8px) 0 var(--padding-left, 8px); + cursor: text; +} + input.small { - width: 50px; + width: calc(60px - var(--padding-left, 0px) - var(--padding-right, 0px)); } input.medium { - width: 140px; + width: calc(150px - var(--padding-left, 0px) - var(--padding-right, 0px)); } -input.center { +input.large { + width: calc(200px - var(--padding-left, 0px) - var(--padding-right, 0px)); +} + +:is(input, select).center { text-align: center; } -label > input { - height: initial; - padding: 0 8px; - cursor: text; - background-color: var(--background-above-1); +label>*:first-child.separated.right { + margin: auto auto auto 0; } -label > *:first-child.separated.right { - margin: auto auto auto 0; +label>i.icon { + top: calc((var(--row-height, 26px) - var(--height)) / 2) } i.icon { z-index: 10; } -i.icon + input { +i.icon+input { padding-left: 30px; } i.icon.error { - animation-duration: 3s; + animation-duration: 2s; animation-name: icon-error; animation-fill-mode: forwards; animation-timing-function: ease-out; @@ -361,25 +340,26 @@ i.icon.error { animation-timing-function: ease-in; } -section.header > h1 { +section.header>h1 { font-size: 1.3rem; line-height: 1.3rem; } -section.header > :is(h2, h3) { +section.header> :is(h2, h3) { font-size: 1.1rem; line-height: 1.1rem; } body { margin: 0; + min-height: 100vh; padding: 0; display: flex; flex-direction: column; background-color: var(--background); } -body > div.background { +body>div.background { z-index: -50000; left: -350%; top: -350%; @@ -392,11 +372,9 @@ body > div.background { animation-iteration-count: infinite; background-repeat: no-repeat; animation-timing-function: linear; - background-image: radial-gradient( - circle, - var(--background-above) 0%, - rgba(0, 0, 0, 0) 100% - ); + background-image: radial-gradient(circle, + var(--cloud-below) 0%, + rgba(0, 0, 0, 0) 100%); } aside { @@ -407,11 +385,14 @@ aside { } header { + --box-shadow: 2px 0 5px rgba(0, 0, 0, 0.3); z-index: 5000; position: absolute; display: flex; flex-direction: column; - box-shadow: 2px 0 5px rgba(0, 0, 0, 0.3); + box-shadow: var(--box-shadow); + -webkit-box-shadow: var(--box-shadow); + -moz-box-shadow: var(--box-shadow); } main { @@ -430,8 +411,8 @@ footer { } .stretched { - width: 100%; - flex-grow: 10; + width: 100% !important; + flex-grow: 10 !important; } button { @@ -442,26 +423,27 @@ button { } button.grass { + --background: var(--grass); color: var(--grass-text); - background-color: var(--grass-background); + background-color: var(--background); } button.grass * { color: var(--grass-text); } -button.grass:hover { +button.grass:is(:hover, :focus) { color: var(--grass-text-above); - background-color: var(--grass-background-above); + background-color: var(--grass-above); } -button.grass:hover * { +button.grass:is(:hover, :focus) * { color: var(--grass-text-above); } button.grass:active { color: var(--grass-text-below); - background-color: var(--grass-background-below); + background-color: var(--grass-below); transition: unset; } @@ -470,85 +452,88 @@ button.grass:active * { transition: unset; } -button.blue { - color: var(--blue-text); - background-color: var(--blue-background); +button.grass.dense { + --background: var(--grass-dense); + color: var(--grass-dense-text); + background-color: var(--background); } -button.blue * { - color: var(--blue-text); +button.grass.dense * { + color: var(--grass-dense-text); } -button.blue:hover { - color: var(--blue-text-above); - background-color: var(--blue-background-above); +button.grass.dense:is(:hover, :focus) { + color: var(--grass-dense-text-above); + background-color: var(--grass-dense-above); } -button.blue:hover * { - color: var(--blue-text-above); +button.grass.dense:is(:hover, :focus) * { + color: var(--grass-dense-text-above); } -button.blue:active { - color: var(--blue-text-below); - background-color: var(--blue-background-below); +button.grass.dense:active { + color: var(--grass-dense-text-below); + background-color: var(--grass-dense-below); transition: unset; } -button.blue:active * { - color: var(--blue-text-below); +button.grass.dense:active * { + color: var(--grass-dense-text-below); transition: unset; } .clay { + --background: var(--clay); color: var(--clay-text); - background-color: var(--clay-background); + background-color: var(--background); } .clay * { color: var(--clay-text); } -button.clay:hover { +.clay:is(:hover, :focus) { color: var(--clay-text-above); - background-color: var(--clay-background-above); + background-color: var(--clay-above); } -button.clay:hover * { +.clay:is(:hover, :focus) * { color: var(--clay-text-above); } -button.clay:active { +.clay:active { color: var(--clay-text-below); - background-color: var(--clay-background-below); + background-color: var(--clay-below); transition: unset; } -button.clay:active * { +.clay:active * { color: var(--clay-text-below); transition: unset; } .earth { + --background: var(--earth); color: var(--earth-text); - background-color: var(--earth-background); + background-color: var(--background); } .earth * { color: var(--earth-text); } -button.earth:hover { +button.earth:is(:hover, :focus) { color: var(--earth-text-above); - background-color: var(--earth-background-above); + background-color: var(--earth-above); } -button.earth:hover * { +button.earth:is(:hover, :focus) * { color: var(--earth-text-above); } button.earth:active { color: var(--earth-text-below); - background-color: var(--earth-background-below); + background-color: var(--earth-below); transition: unset; } @@ -558,26 +543,27 @@ button.earth:active * { } .sand { + --background: var(--sand); color: var(--sand-text); - background-color: var(--sand-background); + background-color: var(--background); } .sand * { color: var(--sand-text); } -button.sand:hover { +button.sand:is(:hover, :focus) { color: var(--sand-text-above); - background-color: var(--sand-background-above); + background-color: var(--sand-above); } -button.sand:hover * { +button.sand:is(:hover, :focus) * { color: var(--sand-text-above); } button.sand:active { color: var(--sand-text-below); - background-color: var(--sand-background-below); + background-color: var(--sand-below); transition: unset; } @@ -587,26 +573,27 @@ button.sand:active * { } .river { + --background: var(--river); color: var(--river-text); - background-color: var(--river-background); + background-color: var(--background); } .river * { color: var(--river-text); } -button.river:hover { +button.river:is(:hover, :focus) { color: var(--river-text-above); - background-color: var(--river-background-above); + background-color: var(--river-above); } -button.river:hover * { +button.river:is(:hover, :focus) * { color: var(--river-text-above); } button.river:active { color: var(--river-text-below); - background-color: var(--river-background-below); + background-color: var(--river-below); transition: unset; } @@ -615,56 +602,88 @@ button.river:active * { transition: unset; } -.sky { - color: var(--sky-text); - background-color: var(--sky-background); +.cloud { + --background: var(--cloud); + color: var(--cloud-text); + background-color: var(--background); } -.sky * { - color: var(--sky-text); +.cloud * { + color: var(--cloud-text); } -button.sky:hover { - color: var(--sky-text-above); - background-color: var(--sky-background-above); +button.cloud:is(:hover, :focus) { + color: var(--cloud-text-above); + background-color: var(--cloud-above); } -button.sky:hover * { - color: var(--sky-text-above); +button.cloud:is(:hover, :focus) * { + color: var(--cloud-text-above); } -button.sky:active { - color: var(--sky-text-below); - background-color: var(--sky-background-below); +button.cloud:active { + color: var(--cloud-text-below); + background-color: var(--cloud-below); transition: unset; } -button.sky:active * { - color: var(--sky-text-below); +button.cloud:active * { + color: var(--cloud-text-below); + transition: unset; +} + +.snow { + --background: var(--snow); + color: var(--snow-text); + background-color: var(--background); +} + +.snow *:not(.earth, .clay, .sand, .ice, .cloud, .river, .sea, .coal) { + color: var(--snow-text); +} + +button.snow:is(:hover, :focus) { + color: var(--snow-text-above); + background-color: var(--snow-above); +} + +button.snow:is(:hover, :focus) *:not(.earth, .clay, .sand, .ice, .cloud, .river, .sea, .coal) { + color: var(--snow-text-above); +} + +button.snow:active { + color: var(--snow-text-below); + background-color: var(--snow-deep); + transition: unset; +} + +button.snow:active *:not(.earth, .clay, .sand, .ice, .cloud, .river, .sea, .coal) { + color: var(--snow-text-below); transition: unset; } .sea { + --background: var(--sea); color: var(--sea-text); - background-color: var(--sea-background); + background-color: var(--background); } .sea * { color: var(--sea-text); } -button.sea:hover { +button.sea:is(:hover, :focus) { color: var(--sea-text-above); - background-color: var(--sea-background-above); + background-color: var(--sea-above); } -button.sea:hover * { +button.sea:is(:hover, :focus) * { color: var(--sea-text-above); } button.sea:active { color: var(--sea-text-below); - background-color: var(--sea-background-below); + background-color: var(--sea-below); transition: unset; } @@ -673,6 +692,37 @@ button.sea:active * { transition: unset; } +.coal { + --background: var(--coal); + color: var(--coal-text); + background-color: var(--background); +} + +.coal * { + color: var(--coal-text); +} + +button.coal:is(:hover, :focus) { + color: var(--coal-text-above); + background-color: var(--coal-above); +} + +button.coal:is(:hover, :focus) * { + color: var(--coal-text-above); +} + +button.coal:active { + color: var(--coal-text-below); + background-color: var(--coal-below); + transition: unset; +} + +button.coal:active * { + color: var(--coal-text-below); + transition: unset; +} + + button.wide { flex-grow: 1; } @@ -683,7 +733,7 @@ section#menu { height: 120px; } -section#menu > nav { +section#menu>nav { top: 0; position: sticky; display: flex; @@ -691,7 +741,7 @@ section#menu > nav { background: var(--menu-background); } -section#menu > nav > ul { +section#menu>nav>ul { margin: 0; min-width: 800px; width: 80%; @@ -702,38 +752,38 @@ section#menu > nav > ul { list-style: none; } -section#menu > nav > ul > li { +section#menu>nav>ul>li { height: 100%; display: flex; align-items: center; } -section#menu > nav > ul > li.divided { +section#menu>nav>ul>li.divided { margin-left: 9px; } -section#menu > nav > ul > li.divided:before { +section#menu>nav>ul>li.divided:before { content: ""; margin-right: 9px; height: 60%; border-left: 1px solid var(--menu-text-below); } -section#menu > nav > ul > li#account:last-child { +section#menu>nav>ul>li#account:last-child { margin-left: auto; } -section#menu > nav > ul > li > button { +section#menu>nav>ul>li>button { height: 100%; padding: 0 30px; color: var(--menu-text); } -section#menu > nav > ul > li > button:hover { +section#menu>nav>ul>li>button:is(:hover, :focus) { color: var(--menu-text-above); } -section#menu > nav > ul > li > button:active { +section#menu>nav>ul>li>button:active { transition: 0s; color: var(--menu-text-below); } diff --git a/mirzaev/ebala/system/public/css/pages/administrators.css b/mirzaev/ebala/system/public/css/pages/administrators.css index 51af5c3..b23be77 100755 --- a/mirzaev/ebala/system/public/css/pages/administrators.css +++ b/mirzaev/ebala/system/public/css/pages/administrators.css @@ -1,3 +1,5 @@ +@charset "UTF-8"; + section#administrators.panel.list > div.row:nth-of-type(1) > span[data-column="end"] @@ -9,13 +11,11 @@ section#administrators.panel.list section#administrators.panel.list > div.row > span:is( - [data-column="id"], + [data-column="account"], [data-column="name"], [data-column="number"], [data-column="mail"], - [data-column="commentary"], - [data-column="status"], - + [data-column="commentary"] ) { min-width: 220px; width: 220px; @@ -24,40 +24,47 @@ section#administrators.panel.list overflow: hidden; } -section#administrators.panel.list > div.row > span[data-column="id"] { +section#administrators.panel.list > div.row > span[data-column="account"] { min-width: 102px; width: 102px; font-weight: bold; text-align: center; } -section#administrators.panel.list > div.row:nth-of-type(1) > span[data-column="id"] { - margin-top: 8px; +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: 180px; - width: 180px; -} - -section#administrators.panel.list > div.row > span[data-column="number"] { min-width: 130px; width: 130px; } +section#administrators.panel.list > div.row > span[data-column="number"] { + min-width: 160px; + width: 160px; +} + +section#administrators.panel.list + > div.row:not(:nth-of-type(1)) + > span[data-column="number"] { + text-align: center; +} + section#administrators.panel.list > div.row > span[data-column="mail"] { - min-width: 300px; - width: 300px; + min-width: 140px; + width: 140px; +} + +section#administrators.panel.list + > div.row:not(:nth-of-type(1)) + > span[data-column="mail"] { + text-align: center; } section#administrators.panel.list > div.row > span[data-column="commentary"] { min-width: unset; width: 100%; } - -section#administrators.panel.list > div.row > span[data-column="status"] { - min-width: 100px; - width: 100px; - font-size: small; - text-align: center; -} diff --git a/mirzaev/ebala/system/public/css/pages/markets.css b/mirzaev/ebala/system/public/css/pages/markets.css index e059616..00b6241 100755 --- a/mirzaev/ebala/system/public/css/pages/markets.css +++ b/mirzaev/ebala/system/public/css/pages/markets.css @@ -1,3 +1,5 @@ +@charset "UTF-8"; + section#markets.panel.list > div.row:nth-of-type(1) > span[data-column="end"] @@ -9,8 +11,11 @@ section#markets.panel.list section#markets.panel.list > div.row > span:is( - [data-column="id"], - [data-column="director"], + [data-column="account"], + [data-column="market"], + [data-column="name"], + [data-column="number"], + [data-column="mail"], [data-column="address"], [data-column="type"], [data-column="commentary"], @@ -23,24 +28,47 @@ section#markets.panel.list overflow: hidden; } -section#markets.panel.list > div.row > span[data-column="id"] { - min-width: 67px; - width: 67px; +section#markets.panel.list > div.row > span:is([data-column="account"], [data-column="market"]) { + min-width: 102px; + width: 102px; font-weight: bold; + text-align: center; } -section#markets.panel.list > div.row:nth-of-type(1) > span[data-column="id"] { - margin-top: 8px; +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="director"] { - min-width: 180px; - width: 180px; +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="name"] { + min-width: 130px; + width: 130px; +} + +section#markets.panel.list > div.row > span[data-column="number"] { + min-width: 160px; + width: 160px; +} + +section#markets.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="number"] { + text-align: center; +} + +section#markets.panel.list > div.row > span[data-column="mail"] { + min-width: 140px; + width: 140px; +} + +section#markets.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="mail"] { + text-align: center; } section#markets.panel.list > div.row > span[data-column="address"] { - min-width: 450px; - width: 450px; + min-width: 180px; + width: 180px; } section#markets.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="type"] { @@ -51,6 +79,7 @@ section#markets.panel.list > div.row > span[data-column="type"] { min-width: 80px; width: 80px; font-size: small; + line-height: 250%; } section#markets.panel.list > div.row > span[data-column="commentary"] { @@ -59,8 +88,12 @@ section#markets.panel.list > div.row > span[data-column="commentary"] { } section#markets.panel.list > div.row > span[data-column="status"] { - min-width: 100px; - width: 100px; + min-width: 80px; + width: 80px; font-size: small; + line-height: 250%; +} + +section#markets.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="status"] { text-align: center; } diff --git a/mirzaev/ebala/system/public/css/pages/operators.css b/mirzaev/ebala/system/public/css/pages/operators.css index 2dbfd4a..bc8583a 100755 --- a/mirzaev/ebala/system/public/css/pages/operators.css +++ b/mirzaev/ebala/system/public/css/pages/operators.css @@ -1,3 +1,5 @@ +@charset "UTF-8"; + section#operators.panel.list > div.row:nth-of-type(1) > span[data-column="end"] @@ -9,13 +11,11 @@ section#operators.panel.list section#operators.panel.list > div.row > span:is( - [data-column="id"], + [data-column="account"], [data-column="name"], [data-column="number"], [data-column="mail"], [data-column="commentary"], - [data-column="status"], - ) { min-width: 220px; width: 220px; @@ -24,40 +24,41 @@ section#operators.panel.list overflow: hidden; } -section#operators.panel.list > div.row > span[data-column="id"] { +section#operators.panel.list > div.row > span[data-column="account"] { min-width: 102px; width: 102px; font-weight: bold; text-align: center; } -section#operators.panel.list > div.row:nth-of-type(1) > span[data-column="id"] { - margin-top: 8px; +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: 180px; - width: 180px; -} - -section#operators.panel.list > div.row > span[data-column="number"] { min-width: 130px; width: 130px; } +section#operators.panel.list > div.row > span[data-column="number"] { + min-width: 160px; + width: 160px; +} + +section#operators.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="number"] { + text-align: center; +} + section#operators.panel.list > div.row > span[data-column="mail"] { - min-width: 300px; - width: 300px; + min-width: 140px; + width: 140px; +} + +section#operators.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="mail"] { + text-align: center; } section#operators.panel.list > div.row > span[data-column="commentary"] { min-width: unset; width: 100%; } - -section#operators.panel.list > div.row > span[data-column="status"] { - min-width: 100px; - width: 100px; - font-size: small; - text-align: center; -} diff --git a/mirzaev/ebala/system/public/css/pages/tasks.css b/mirzaev/ebala/system/public/css/pages/tasks.css index 6a9c364..087722f 100755 --- a/mirzaev/ebala/system/public/css/pages/tasks.css +++ b/mirzaev/ebala/system/public/css/pages/tasks.css @@ -1,4 +1,9 @@ -section#tasks.panel.list > div.row:nth-of-type(1) > span[data-column="end"] > i.home { +@charset "UTF-8"; + +section#tasks.panel.list + > div.row:nth-of-type(1) + > span[data-column="end"] + > i.home { margin-top: 5px; height: 12px; } @@ -28,9 +33,9 @@ section#tasks.panel.list > div.row > span[data-column="date"] { font-weight: bold; } -section#tasks.panel.list > div.row > span[data-column="worker"] { - min-width: 67px; - width: 67px; +section#tasks.panel.list > div.row > span:is([data-column="worker"], [data-column="market"]) { + min-width: 102px; + width: 102px; font-weight: bold; } @@ -39,8 +44,8 @@ section#tasks.panel.list > div.row:nth-of-type(1) > span[data-column="worker"] { } section#tasks.panel.list > div.row > span[data-column="name"] { - min-width: 180px; - width: 180px; + min-width: 130px; + width: 130px; } section#tasks.panel.list > div.row > span[data-column="task"] { @@ -48,7 +53,9 @@ section#tasks.panel.list > div.row > span[data-column="task"] { width: 100px; } -section#tasks.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="task"] { +section#tasks.panel.list + > div.row:not(:nth-of-type(1)) + > span[data-column="task"] { text-align: right; } @@ -60,12 +67,14 @@ section#tasks.panel.list > div.row > span[data-column="start"] { min-width: 37px; width: 37px; font-size: small; + line-height: 2.8; } section#tasks.panel.list > div.row > span[data-column="end"] { min-width: 37px; width: 37px; font-size: small; + line-height: 2.8; } section#tasks.panel.list > div.row:nth-of-type(1) > span[data-column="hours"] { @@ -76,9 +85,12 @@ section#tasks.panel.list > div.row > span[data-column="hours"] { min-width: 27px; width: 27px; font-size: small; + line-height: 2.8; } -section#tasks.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="hours"] { +section#tasks.panel.list + > div.row:not(:nth-of-type(1)) + > span[data-column="hours"] { text-align: center; } @@ -86,12 +98,6 @@ section#tasks.panel.list > div.row:nth-of-type(1) > span[data-column="market"] { margin-bottom: 8px; } -section#tasks.panel.list > div.row > span[data-column="market"] { - min-width: 60px; - width: 60px; - font-weight: bold; -} - section#tasks.panel.list > div.row > span[data-column="address"] { min-width: 180px; width: 180px; @@ -101,9 +107,12 @@ section#tasks.panel.list > div.row > span[data-column="type"] { min-width: 80px; width: 80px; font-size: small; + line-height: 2.8; } -section#tasks.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="type"] { +section#tasks.panel.list + > div.row:not(:nth-of-type(1)) + > span[data-column="type"] { text-align: center; } @@ -111,9 +120,12 @@ section#tasks.panel.list > div.row > span[data-column="tax"] { min-width: 100px; width: 100px; font-size: small; + line-height: 2.8; } -section#tasks.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="tax"] { +section#tasks.panel.list + > div.row:not(:nth-of-type(1)) + > span[data-column="tax"] { text-align: center; } diff --git a/mirzaev/ebala/system/public/css/pages/workers.css b/mirzaev/ebala/system/public/css/pages/workers.css index 15b7b80..a50e7ee 100755 --- a/mirzaev/ebala/system/public/css/pages/workers.css +++ b/mirzaev/ebala/system/public/css/pages/workers.css @@ -1,3 +1,5 @@ +@charset "UTF-8"; + section#workers.panel.list > div.row:nth-of-type(1) > span[data-column="end"] @@ -9,18 +11,17 @@ section#workers.panel.list section#workers.panel.list > div.row > span:is( - [data-column="id"], + [data-column="account"], + [data-column="worker"], [data-column="name"], - [data-column="birth"], [data-column="number"], + [data-column="mail"], [data-column="passport"], - [data-column="department"], - [data-column="city"], [data-column="address"], - [data-column="requisites"], [data-column="tax"], + [data-column="requisites"], [data-column="commentary"], - [data-column="status"], + ) { min-width: 220px; width: 220px; @@ -29,30 +30,46 @@ section#workers.panel.list overflow: hidden; } -section#workers.panel.list > div.row > span[data-column="id"] { - min-width: 67px; - width: 67px; +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="id"] { +section#workers.panel.list > div.row:nth-of-type(1) > span[data-column="account"] { + margin-top: 6px; +} + +section#workers.panel.list > div.row:nth-of-type(1) > span[data-column="worker"] { margin-top: 8px; } section#workers.panel.list > div.row > span[data-column="name"] { - min-width: 180px; - width: 180px; -} - -section#workers.panel.list > div.row > span[data-column="birth"] { - min-width: 80px; - width: 80px; - font-size: small; + min-width: 130px; + width: 130px; } section#workers.panel.list > div.row > span[data-column="number"] { - min-width: 120px; - width: 120px; + min-width: 160px; + width: 160px; +} + +section#workers.panel.list + > div.row:not(:nth-of-type(1)) + > span[data-column="number"] { + text-align: center; +} + +section#workers.panel.list > div.row > span[data-column="mail"] { + min-width: 140px; + width: 140px; +} + +section#workers.panel.list + > div.row:not(:nth-of-type(1)) + > span[data-column="mail"] { + text-align: center; } section#workers.panel.list > div.row > span[data-column="passport"] { @@ -76,22 +93,19 @@ section#workers.panel.list > div.row > span[data-column="requisites"] { } section#workers.panel.list > div.row > span[data-column="tax"] { - min-width: 55px; - width: 55px; + min-width: 100px; + width: 100px; font-size: small; + line-height: 2.8; +} + +section#workers.panel.list + > div.row:not(:nth-of-type(1)) + > span[data-column="tax"] { + text-align: center; } section#workers.panel.list > div.row > span[data-column="commentary"] { min-width: unset; width: 100%; } - -section#workers.panel.list > div.row > span[data-column="status"] { - min-width: 100px; - width: 100px; - font-size: small; -} - -section#workers.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="status"] { - text-align: center; -} diff --git a/mirzaev/ebala/system/public/css/popup.css b/mirzaev/ebala/system/public/css/popup.css index f335a8d..f11fad7 100644 --- a/mirzaev/ebala/system/public/css/popup.css +++ b/mirzaev/ebala/system/public/css/popup.css @@ -1,3 +1,5 @@ +@charset "UTF-8"; + div#popup { z-index: 999999999999; left: 0; @@ -13,82 +15,87 @@ div#popup { background-color: rgba(18, 3, 3, 0.4); } -div#popup > section { - background-color: var(--background-above-2); +div#popup>section { + background-color: var(--cloud-above); } -div#popup > section.errors { +div#popup>section.errors { + --padding-horizontal: 30px; + --padding-vertical: 8px; top: var(--top, 80%); position: absolute; height: var(--height); - padding: 8px 30px !important; + padding: var(--padding-vertical, 0px) var(--padding-horizontal, 0px) !important; } -div#popup > section.small { +div#popup>section.small { width: 420px; } -div#popup > section.list { +div#popup>section.stretched { + width: min-content; + max-width: 80wv; + flex-grow: unset; +} + + +div#popup>section.calculated { + width: calc(var(--calculated-width) - var(--padding-horizontal, 0px) * 2); +} + +div#popup>section.list { display: flex; flex-direction: column; - gap: 8px; padding: 30px; border-radius: 3px; } -div#popup > section.list > h3 { +div#popup>section.list>h3 { margin-top: 4px; - margin-bottom: 18px; + margin-bottom: 22px; width: 100%; text-align: center; } -div#popup > section.list > span { +div#popup>section.list h4 { + margin-top: unset; + margin-bottom: 10px; + width: 100%; + text-align: center; +} + +div#popup>section.list>section.main { + display: flex; + gap: 15px; +} + +div#popup>section.list>section.main>div.column { + flex-grow: 1; + display: flex; + flex-direction: column; + gap: 8px; +} + +div#popup>section.list>section.main>div.column:only-child { + width: 100%; +} + +div#popup>section.list>section.main>div.column>span { width: 100%; display: flex; text-align: right; } -div#popup > section.list > span > b { +div#popup>section.list>section.main>div.column>span>b { margin-right: auto; text-align: left; } -div#popup > section.list > :is(div, select).row.buttons { - height: 33px; +div#popup>section.list>section.main>div.column> :is(div, select).row { + border-radius: 3px; } -div#popup > section.list > :is(div, select).row:not(.buttons, .stretchable), -div#popup > section.list > :is(div, select).row:not(.buttons, .stretchable) > button { - height: 29px; -} - -div#popup > section.list > div.row:not(.merged) + div.row.merged { - margin-top: 8px; -} - -div#popup > section.list > div.row:not(.merged) { - margin-top: 8px; -} - -div#popup > section.list > div.row:not(.merged):last-of-type { - margin-top: 8px; -} - -div#popup > section.list > div.row.divided { - margin-top: 8px; -} - -div#popup > section.list > :not(h3):first-child:not(.divided), -div#popup > section.list > h3 + div.row:not(.divided) { - margin-top: unset !important; -} - -div#popup > section.list > :only-child { - margin: unset !important; -} - -div#popup > section.list > div.row { +div#popup>section.list>section.main>div.column>:is(div, section).row { height: fit-content; position: relative; display: flex; @@ -96,42 +103,162 @@ div#popup > section.list > div.row { transition: 0s; } -div#popup > section.list > div.row:not(.monolithic) { +div#popup>section.list>section.main>div.column>:is(div, section).row:not(.monolithic) { gap: 10px; } -div#popup > section.list > div.row > label { +div#popup>section.list>section.main>div.column>:is(div, section).row>label { display: contents; } -div#popup > section.list > div.row > label > i { - left: 13px; - top: 7px; - position: absolute; -} - -div#popup > section.list > div.row > label > input[type="date"]:not(.small, .medium) { +div#popup>section.list>section.main>div.column>:is(div, section).row>label>input[type="date"]:not(.small, .medium, .large) { flex-grow: 1; text-align: center; } -div#popup > section.list > div.row > label > input[type="time"]:not(.small, .medium) { +div#popup>section.list>section.main>div.column>:is(div, section).row>label>input[type="time"]:not(.small, .medium, .large) { width: 55px; text-align: center; } -div#popup > section.list > div.row > label > :is(input, button):only-child { +div#popup>section.list>section.main>div.column>:is(div, section).row>label> :is(input, button):only-child { width: 100%; } -div#popup > section.list > div.row > label > :is(input, select):only-of-type:not(.small, .medium) { +div#popup>section.list>section.main>div.column>:is(div, section).row>label> :is(input, select):only-of-type:not(.small, .medium, .large) { width: 100%; } -/* div#popup > section.list > div.row > label > input:only-of-type:not(.center) { - padding-left: 37px; +div#popup>section.list>section.main>div.column> :is(div, select).row[data-interactive="true"]:is(:hover, :focus, :active) { + padding-left: 12px; + transition: unset; +} + +div#popup>section.list>section.main>div.column> :is(div, select).row.buttons { + height: 33px; +} + +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; +} + +div#popup>section.list>section.main>div.column>:is(div, section).row:not(.merged)+:is(div, section).row.merged { + margin-top: 8px; +} + +div#popup>section.list>section.main>div.column>:is(div, section).row:not(.merged, :first-of-type) { + margin-top: 8px; +} + +div#popup>section.list>section.main>div.column>:is(div, section).row:not(.merged):last-of-type:not(:only-child) { + margin-top: 8px; +} + +div#popup>section.list>section.main>div.column>:is(div, section).row.divided { + margin-top: 8px; +} + +/* div#popup > section.list > :only-child { + margin: unset !important; } */ -div#popup > section.list.errors > section.body > dl > dd { +div#popup>section.list>section.main>div.column>section.row.chat { + width: 460px; + height: 60vh; + display: flex; + flex-direction: column; + justify-content: start; + gap: unset; + overflow-x: hidden; + overflow-y: scroll; +} + +div#popup>section.list>section.main>div.column>section.row.chat>div.message { + padding: 14px 16px; +} + +div#popup>section.list>section.main>div.column>section.row.chat>div.message:nth-child(even) { + backdrop-filter: brightness(0.8); +} + +div#popup>section.list>section.main>div.column>section.row.chat>div.message:hover { + backdrop-filter: brightness(0.7); +} + +div#popup>section.list>section.main>div.column>section.row.chat>div.message.problem { + background-color: var(--clay-above); +} + +div#popup>section.list>section.main>div.column>section.row.chat>div.message.problem:nth-child(even) { + background-color: var(--clay); +} + +div#popup>section.list>section.main>div.column>section.row.chat>div.message.problem:hover { + background-color: var(--clay-important-above); +} + +div#popup>section.list>section.main>div.column>section.row.chat>div.message.problem:nth-child(even):hover { + background-color: var(--clay-important); +} + +div#popup>section.list>section.main>div.column>section.row.chat>div.message.solution { + background-color: var(--grass); +} + +div#popup>section.list>section.main>div.column>section.row.chat>div.message.solution { + background-color: var(--grass-above); +} + +div#popup>section.list>section.main>div.column>section.row.chat>div.message.solution:nth-child(even) { + background-color: var(--grass); +} + +div#popup>section.list>section.main>div.column>section.row.chat>div.message.solution:hover { + background-color: var(--grass-dense-above); +} + +div#popup>section.list>section.main>div.column>section.row.chat>div.message.solution:nth-child(even):hover { + background-color: var(--grass-dense); +} + +div#popup>section.list>section.main>div.column>section.row.chat>div.message:is(.solution, .problem) p { + color: var(--coal-text); +} + +div#popup>section.list>section.main>div.column>section.row.chat>div.message>h3 { + width: 100%; + margin: unset; + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 0.9rem; + font-weight: normal; +} + +div#popup>section.list>section.main>div.column>section.row.chat>div.message>h3>span.date { + margin-left: auto; +} + +div#popup>section.list>section.main>div.column>section.row.chat>div.message>p { + margin: 5px 0 0 8px; + font-size: 0.9rem; + word-wrap: break-word; +} + +div#popup>section.list>section.main>div.column>section.row.message>textarea { + min-height: 29px; + min-width: unset; + height: 29px; + resize: none; + overflow: scroll; +} + +div#popup>section.list>section.main>div.column>section.row.message>textarea+button { + height: 100%; + padding: 0 25px; +} + +div#popup>section.list.errors>section.body>dl>dd { margin-left: 20px; } diff --git a/mirzaev/ebala/system/public/css/themes/harmony/earth.css b/mirzaev/ebala/system/public/css/themes/harmony/earth.css new file mode 100644 index 0000000..5f45585 --- /dev/null +++ b/mirzaev/ebala/system/public/css/themes/harmony/earth.css @@ -0,0 +1,178 @@ +@charset "UTF-8"; + +:root { + --background-above-5: #a69c9c; + /* --background-above-4: #b9b8b8; */ + --background-above-4: #d5cdcd; + --background-above-3: #ecdfdf; + + --snow-above: #f9fbff; + --snow: #f4f7fd; + --snow-deep: #dadfe6; + + --ice-above: #d0d3f7; + --ice: #bfc1ea; + --ice-below: red; + + /* персиковый #dddd5c */ + /* персиковый тёмный #bfbf6e */ + + /* @TODO + * 1. Change the amount of sun and moon rays depending on the time on the device + */ + --sun-intensity: 0.3; + --sun-above: ; + --sun: rgb(219, 219, 126, var(--sun-intensity)); + --sun-below: ; + + --moon-above: ; + --moon: ; + --moon-below: ; + + --background: #e8d9d9; + --background-below: #d7c5c5; + --background-below-6: #8e8282; + --background-inverse: #221e1e; + --background-inverse-below: #120f0f; + --node-background-important: #c3eac3; + --node-background-completed: #b0c0b0; + --node-background: #bdb; + --connection: #b2b7b2; + --connection-completed: #d1d1d1; + --text: #151313; + --text-hover: #463e3e; + --text-active: #0e0e0e; + --text-inverse-above: #fff; + --text-inverse: #efefef; + --text-inverse-below: #d0d0d0; + --text-inverse-below-1: #bbadad; + + --earth-text-above: #975151; + --earth-text: #794040; + --earth-text-below: #511f1f; + --earth-text-important-above: #a44242; + --earth-text-important: #371a1a; + --earth-text-important-below: #602525; + --earth-above: #d9c5b3; + --earth: #b0a296; + --earth-below: #918377; + --earth-important-above: #b0a296; + --earth-important: #6c5f53; + --earth-important-below: #b0a296; + + --clay-text-above: #ffe9be; + --clay-text: #f7d1a1; + --clay-text-below: #c8af7d; + --clay-above-1: #dc4343; + --clay-above: #bf3737; + --clay: #a43333; + --clay-below: #8d2a2a; + --clay-below-1: #792727; + --clay-important-above: #9f0c0c; + --clay-important: #910b0b; + --clay-important-below: #8d2a2a; + + --sand-text-above: #c8b25f; + --sand-text: #b0844a; + --sand-text-below: #976b31; + --sand-text-below-1: #6a4a1f; + --sand-above: #fff5ce; + --sand: #f0e2ab; + --sand-below: #dfc79a; + --sand-important-above: #e6d595; + --sand-important: #d7c06c; + --sand-important-below: #dfc79a; + + --magma-text-above: ; + --magma-text: ; + --magma-text-below: ; + --magma-text-below-1: ; + --magma-above: #ffd325; + --magma: #e6bf26; + --magma-below: ; + --magma-important-above: #e8a21c; + --magma-important: #c6850c; + --magma-important-below: ; + + --grass-above: #89c866; + --grass: #79b757; + --grass-below: #38a818; + --grass-text-above: #fcffae; + --grass-text: #fbff80; + --grass-text-below: #e3e85e; + --grass-dense-above: #1abb4a; + --grass-dense: #12ac4e; + --grass-dense-below: #109745; + --grass-dense-text-above: var(--grass-text-above); + --grass-dense-text: var(--grass-text); + --grass-dense-text-below: var(--grass-text-below); + + --river-text-above: #2b91c4; + --river-text: #335cbb; + --river-text-below: #2b4480; + --river-text-important-above: #2b91c4; + --river-text-important: #335cbb; + --river-text-important-below: #2b4480; + --river-above: #b8c0ea; + --river: #93a2df; + --river-below: #7a8cd7; + --river-deep-above: #8397e3; + --river-deep: #677ed7; + --river-deep-below: #687dd5; + + --sea-text-above: #ccf8ff; + --sea-text: #b7f1fb; + --sea-text-below: #99d5df; + --sea-text-important-above: #ccf8ff; + --sea-text-important: #b7f1fb; + --sea-text-important-below: #99d5df; + --sea-above: #6a68dd; + --sea: #5d5bc1; + --sea-below: #504f9f; + --sea-deep-above: #5148c4; + --sea-deep: #312ea8; + --sea-deep-below: #4443aa; + + --cloud-text-above: #43b0c1; + --cloud-text: #1e4c53; + --cloud-text-below: ; + --cloud-text-important-above: #43b0c1; + --cloud-text-important: #1e4c53; + --cloud-text-important-below: ; + --cloud-above: #e6eaf0; + --cloud: #cfd3d9; + --cloud-below: #b7babf; + + --cloud-rainy-above: #b2bbce; + --cloud-rainy: #a5a4b9; + --cloud-rainy-below: ; + + --coal-text-above: ; + --coal-text: #392020; + --coal-text-below: ; + --coal-above: ; + --coal: ; + --coal-below: ; + + --input-error: #d25050; + --icon-error: #792323; + --interface-brown: #9f704e; + --menu-background: #775653; + --menu-text-above: #ffd88e; + /* --menu-text: #cab59b; */ + --menu-text: #d0b88a; + --menu-text-below: #a48f68; + --input-beige: #fffce8; + --input-clay: #fff8f8; + + --link: #3c76ff; + --link-hover: #6594ff; + --link-active: #3064dd; +} + +body { + --body-rays: inset 0px 45px 100vw 3vw var(--sun); + box-shadow: var(--body-rays); + -webkit-box-shadow: var(--body-rays); + -moz-box-shadow: var(--body-rays); +} diff --git a/mirzaev/ebala/system/public/index.php b/mirzaev/ebala/system/public/index.php index 03b65c9..75cea38 100755 --- a/mirzaev/ebala/system/public/index.php +++ b/mirzaev/ebala/system/public/index.php @@ -35,26 +35,39 @@ $router = new router; // Запись маршрутов $router->write('/', 'index', 'index', 'GET'); $router->write('/', 'index', 'index', 'POST'); +$router->write('/entry', 'index', 'entry', 'POST'); $router->write('/worker/$worker/read', 'task', 'worker', 'POST'); +$router->write('/worker/$id/fields', 'worker', 'fields', 'POST'); +$router->write('/worker/$id/update', 'worker', 'update', 'POST'); +$router->write('/worker/$id/fire', 'worker', 'fire', 'POST'); $router->write('/workers', 'worker', 'index', 'GET'); $router->write('/workers', 'worker', 'index', 'POST'); $router->write('/workers/read', 'worker', 'read', 'POST'); $router->write('/workers/list', 'worker', 'datalist', 'POST'); +$router->write('/workers/create', 'worker', 'create', 'POST'); $router->write('/market/$market/read', 'task', 'market', 'POST'); +$router->write('/market/$id/fields', 'market', 'fields', 'POST'); +$router->write('/market/$id/update', 'market', 'update', 'POST'); $router->write('/markets', 'market', 'index', 'GET'); $router->write('/markets', 'market', 'index', 'POST'); $router->write('/markets/read', 'market', 'read', 'POST'); $router->write('/markets/list', 'market', 'datalist', 'POST'); +$router->write('/markets/create', 'market', 'create', 'POST'); $router->write('/operators', 'operator', 'index', 'GET'); $router->write('/operators', 'operator', 'index', 'POST'); $router->write('/operators/read', 'operator', 'read', 'POST'); +$router->write('/operators/create', 'operator', 'create', 'POST'); $router->write('/administrators', 'administrator', 'index', 'GET'); $router->write('/administrators', 'administrator', 'index', 'POST'); $router->write('/administrators/read', 'administrator', 'read', 'POST'); +$router->write('/administrators/create', 'administrator', 'create', 'POST'); $router->write('/settings', 'settings', 'index', 'GET'); $router->write('/settings', 'settings', 'index', 'POST'); $router->write('/$id', 'account', 'index', 'GET'); $router->write('/$id', 'account', 'index', 'POST'); +$router->write('/$id/fields', 'account', 'fields', 'POST'); +$router->write('/$id/update', 'account', 'update', 'POST'); +$router->write('/$id/delete', 'account', 'delete', 'POST'); $router->write('/session/worker', 'session', 'worker', 'POST'); $router->write('/session/write', 'session', 'write', 'POST'); $router->write('/session/read', 'session', 'read', 'POST'); @@ -69,6 +82,8 @@ $router->write('/works/list', 'work', 'datalist', 'POST'); $router->write('/task/$task/read', 'task', 'task', 'POST'); $router->write('/task/$task/value', 'task', 'value', 'POST'); $router->write('/task/$task/confirm', 'task', 'confirm', 'POST'); +$router->write('/task/$task/problem', 'task', 'problem', 'POST'); +$router->write('/task/$task/complete', 'task', 'complete', 'POST'); $router->write('/task/$task/hide', 'task', 'hide', 'POST'); $router->write('/task/$task/remove', 'task', 'remove', 'POST'); $router->write('/task/$task/work', 'task', 'work', 'POST'); @@ -80,6 +95,8 @@ $router->write('/task/$task/worker/update', 'task', 'update', 'POST'); $router->write('/task/$task/market/update', 'task', 'update', 'POST'); $router->write('/task/$task/publish', 'task', 'publish', 'POST'); $router->write('/task/$task/unpublish', 'task', 'unpublish', 'POST'); +$router->write('/task/$task/chat', 'task', 'chat', 'POST'); +$router->write('/task/$task/chat/send', 'task', 'message', 'POST'); $router->write('/elements/menu', 'index', 'menu', 'POST'); // Инициализация ядра diff --git a/mirzaev/ebala/system/public/js/administrators.js b/mirzaev/ebala/system/public/js/administrators.js index 1d206ee..676fe98 100644 --- a/mirzaev/ebala/system/public/js/administrators.js +++ b/mirzaev/ebala/system/public/js/administrators.js @@ -20,6 +20,14 @@ if (typeof window.administrators !== "function") { */ static initialized = false; + /** + * Управление кнопками (escape, enter...) + * + * Содержит функцию инициализирующую обработчики событий keydown для document + * функция деинициализируется с document при закрытии окна + */ + static buttons; + /** * Ожидание зависимости: ядро * @@ -103,94 +111,102 @@ if (typeof window.administrators !== "function") { } /** - * Создать заявку + * Обновить аккаунт (вызов демпфера) * - * @param {HTMLElement} cashiers Количество кассиров - * @param {HTMLElement} displayers Количество выкладчиков - * @param {HTMLElement} loaders Количество грузчиков - * @param {HTMLElement} gastronomes Количество гастрономов - * @param {HTMLElement} start Начало работы (00:00) - * @param {HTMLElement} end Конец работы (00:00) - * @param {HTMLElement} date Дата работы (d.m.Y) - * @param {HTMLElement} button Кнопка создания заявки