плотно навалил
This commit is contained in:
parent
c73ddc6ba8
commit
68aab813c4
|
@ -0,0 +1,3 @@
|
|||
!.gitignore
|
||||
composer.phar
|
||||
vendor
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"name": "mirzaev/ebala",
|
||||
"description": "PHP CRM based on ArangoDB",
|
||||
"readme": "README.md",
|
||||
"keywords": [
|
||||
"site",
|
||||
"crm",
|
||||
"arangodb"
|
||||
],
|
||||
"type": "project",
|
||||
"homepage": "https://git.mirzaev.sexy/mirzaev/ebala",
|
||||
"license": "WTFPL",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Arsen Mirzaev Tatyano-Muradovich",
|
||||
"email": "arsen@mirzaev.sexy",
|
||||
"homepage": "https://mirzaev.sexy",
|
||||
"role": "Programmer"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"email": "arsen@mirzaev.sexy",
|
||||
"wiki": "https://git.mirzaev.sexy/mirzaev/ebala/wiki",
|
||||
"issues": "https://git.mirzaev.sexy/mirzaev/ebala/issues"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"type": "funding",
|
||||
"url": "https://fund.mirzaev.sexy"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "~8.2",
|
||||
"ext-sodium": "~8.2.4",
|
||||
"mirzaev/minimal": "^2.0.x-dev",
|
||||
"mirzaev/accounts": "~1.2.x-dev",
|
||||
"mirzaev/arangodb": "^1.0.0",
|
||||
"triagens/arangodb": "~3.9.x-dev",
|
||||
"twig/twig": "^3.4",
|
||||
"twig/extra-bundle": "^3.7",
|
||||
"twig/intl-extra": "^3.7",
|
||||
"phpoffice/phpspreadsheet": "^1.29"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~9.5"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"mirzaev\\ebala\\": "mirzaev/ebala/system"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"mirzaev\\ebala\\tests\\": "mirzaev/ebala/tests"
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,194 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\ebala\controllers;
|
||||
|
||||
// Файлы проекта
|
||||
use mirzaev\ebala\views\templater,
|
||||
mirzaev\ebala\models\core as model,
|
||||
mirzaev\ebala\models\account,
|
||||
mirzaev\ebala\models\session,
|
||||
mirzaev\ebala\models\market;
|
||||
|
||||
// Фреймворк PHP
|
||||
use mirzaev\minimal\controller;
|
||||
|
||||
/**
|
||||
* Ядро контроллеров
|
||||
*
|
||||
* @package mirzaev\ebala\controllers
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
class core extends controller
|
||||
{
|
||||
/**
|
||||
* Постфикс
|
||||
*/
|
||||
final public const POSTFIX = '';
|
||||
|
||||
/**
|
||||
* Инстанция сессии
|
||||
*/
|
||||
protected readonly session $session;
|
||||
|
||||
/**
|
||||
* Инстанция аккаунта
|
||||
*/
|
||||
protected readonly ?account $account;
|
||||
|
||||
/**
|
||||
* Инстанция магазина
|
||||
*/
|
||||
protected readonly ?market $market;
|
||||
|
||||
/**
|
||||
* Реестр ошибок
|
||||
*/
|
||||
protected array $errors = [
|
||||
'session' => [],
|
||||
'account' => []
|
||||
];
|
||||
|
||||
/**
|
||||
* Конструктор
|
||||
*
|
||||
* @param bool $initialize Инициализировать контроллер?
|
||||
*/
|
||||
public function __construct(bool $initialize = true)
|
||||
{
|
||||
// Блокировка запросов от CloudFlare
|
||||
if ($_SERVER['HTTP_USER_AGENT'] === 'nginx-ssl early hints') return;
|
||||
|
||||
parent::__construct($initialize);
|
||||
|
||||
if ($initialize) {
|
||||
// Запрошена инициализация
|
||||
|
||||
// Инициализация ядра моделей (соединение с базой данных...)
|
||||
new model();
|
||||
|
||||
// Инициализация даты до которой будет активна сессия
|
||||
$expires = strtotime('+1 week');
|
||||
|
||||
// Инициализация значения по умолчанию
|
||||
$_COOKIE["session"] ??= null;
|
||||
|
||||
// Инициализация сессии
|
||||
$this->session = new session($_COOKIE["session"], $expires);
|
||||
|
||||
if ($_COOKIE["session"] !== ($this->session->hash)) {
|
||||
// Изменился хеш сессии (подразумевается, что сессия устарела)
|
||||
|
||||
// Запись хеша новой сессии
|
||||
setcookie(
|
||||
'session',
|
||||
$this->session->hash,
|
||||
[
|
||||
'expires' => $expires,
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'strict'
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Инициализация аккаунта
|
||||
$this->account = new account($this->session);
|
||||
|
||||
if ($this->account->status()) {
|
||||
// Инициализирован аккаунт
|
||||
|
||||
// Инициализация магазина
|
||||
if ($this->account->type === 'market') $this->market = new market(account::market($this->account->getId()));
|
||||
|
||||
if ($this->account->type !== $_SERVER['INTERFACE']) {
|
||||
// Не соответствие типа аккаунта к запрошенному интерфейсу (например, если оператор зашел на интерфейс магазина)
|
||||
|
||||
// Переадресация
|
||||
header(
|
||||
'Location: '
|
||||
. $_SERVER['SCHEME']
|
||||
. '://'
|
||||
. match ($this->account->type) {
|
||||
'worker' => 'панель',
|
||||
'operator' => 'оператор',
|
||||
'market' => 'магазин',
|
||||
'administrator' => 'администратор',
|
||||
default => 'панель'
|
||||
}
|
||||
. '.'
|
||||
. $_SERVER['DOMAIN']
|
||||
. $_SERVER['REQUEST_URI'],
|
||||
true,
|
||||
303
|
||||
);
|
||||
|
||||
// Выход (успех)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация шаблонизатора представлений
|
||||
$this->view = new templater($this->session, $this->account);
|
||||
}
|
||||
}
|
||||
|
||||
protected function authorization(): string
|
||||
{
|
||||
if ($_SERVER['INTERFACE'] === 'operator') {
|
||||
// Оператор
|
||||
|
||||
// Инициализация данных аккаунтов для генерации представления
|
||||
$this->view->accounts = account::read('d.type == "operator" && d.status == "active"', 'd.name.first DESC', 100, 1, errors: $this->errors['account']);
|
||||
|
||||
// Преобразование в массив, если вернуло инстанцию одного документа, вместо массива инстанций документов
|
||||
if (!is_array($this->view->accounts)) $this->view->accounts = [$this->view->accounts];
|
||||
} else if ($_SERVER['INTERFACE'] === 'market') {
|
||||
// Магазин
|
||||
|
||||
// Инициализация данных аккаунтов для генерации представления
|
||||
$this->view->accounts = account::read('d.type == "market" && d.status == "active"', 'd.name.first DESC', 100, 1, $this->errors['account']);
|
||||
|
||||
// Преобразование в массив, если вернуло инстанцию одного документа, вместо массива инстанций документов
|
||||
if (!is_array($this->view->accounts)) $this->view->accounts = [$this->view->accounts];
|
||||
|
||||
// Инициализация буфера оболочки аккаунта и магазина
|
||||
$buffer = [];
|
||||
|
||||
// Инициализация данных магазина для аккаунта для генерации представления
|
||||
foreach ($this->view->accounts as $vendor) $buffer[] = ['vendor' => $vendor, 'market' => account::market($vendor->getId(), $this->errors['account'])];
|
||||
|
||||
// Запись в глобальную переменную из буфера
|
||||
$this->view->accounts = $buffer;
|
||||
} else if ($_SERVER['INTERFACE'] === 'administrator') {
|
||||
// Администратор
|
||||
|
||||
// Инициализация данных аккаунтов для генерации представления
|
||||
$this->view->accounts = account::read('d.type == "administrator" && d.status == "active"', 'd.name.first DESC', 100, 1, errors: $this->errors['account']);
|
||||
|
||||
// Преобразование в массив, если вернуло инстанцию одного документа, вместо массива инстанций документов
|
||||
if (!is_array($this->view->accounts)) $this->view->accounts = [$this->view->accounts];
|
||||
}
|
||||
|
||||
return $this->view->render(DIRECTORY_SEPARATOR . 'pages' . DIRECTORY_SEPARATOR . 'entry' . DIRECTORY_SEPARATOR . $_SERVER['INTERFACE'] . '.html');
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить инициализированность
|
||||
*
|
||||
* Проверяет инициализированность свойства в инстанции документа аккаунта из базы данных
|
||||
*
|
||||
* @param string $name Название
|
||||
*
|
||||
* @return bool Свойство инициализировано?
|
||||
*/
|
||||
public function __isset(string $name): bool
|
||||
{
|
||||
return match ($name) {
|
||||
'account' => isset($this->account->document),
|
||||
default => isset($this->{$name})
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\ebala\controllers;
|
||||
|
||||
// Файлы проекта
|
||||
use mirzaev\ebala\controllers\core,
|
||||
mirzaev\ebala\models\account;
|
||||
|
||||
/**
|
||||
* Контроллер основной страницы
|
||||
*
|
||||
* @package mirzaev\ebala\controllers
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class index extends core
|
||||
{
|
||||
/**
|
||||
* Главная страница
|
||||
*
|
||||
* @param array $parameters Параметры запроса
|
||||
*/
|
||||
public function index(array $parameters = []): ?string
|
||||
{
|
||||
if ($this->account->status()) {
|
||||
// Авторизован аккаунт
|
||||
|
||||
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;
|
||||
|
||||
// Генерация значения для аттрибута "value" для HTML-элемента <input>
|
||||
$this->view->{$name} = (int) $value;
|
||||
}
|
||||
|
||||
foreach (['confirmed', 'waiting', 'published', 'unpublished', 'problematic', 'hided', 'completed'] as $name) {
|
||||
// Перебор фильтров статусов
|
||||
|
||||
// Инициализация значения (приоритет у cookie)
|
||||
$value = $_COOKIE["tasks_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? null;
|
||||
|
||||
// Найдено значение?
|
||||
if ($value === null) continue;
|
||||
|
||||
// Генерация класса для HTML-элемента по его состоянию (0 - ОТСУТСТВУЕТ, 1 - И, 2 - ИЛИ)
|
||||
$this->view->{$name} = match ($value) {
|
||||
'0', 0 => 'earth',
|
||||
'1', 1 => 'sand',
|
||||
'2', 2 => 'river',
|
||||
default => 'earth'
|
||||
};
|
||||
}
|
||||
|
||||
// Генерация представления
|
||||
$main = $this->view->render(DIRECTORY_SEPARATOR . 'pages' . DIRECTORY_SEPARATOR . 'tasks.html');
|
||||
} else $main = $this->authorization();
|
||||
|
||||
// Возврат (успех)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') return $this->view->render(DIRECTORY_SEPARATOR . 'index.html', ['main' => $main]);
|
||||
else if ($_SERVER['REQUEST_METHOD'] === 'POST') return $main;
|
||||
|
||||
// Возврат (провал)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main menu
|
||||
*
|
||||
* @param array $parameters Параметры запроса
|
||||
*/
|
||||
public function menu(array $parameters = []): ?string
|
||||
{
|
||||
if ($this->account->status()) {
|
||||
// Авторизован аккаунт
|
||||
|
||||
// Генерация представления
|
||||
return $this->view->render(DIRECTORY_SEPARATOR . 'menu.html');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\ebala\controllers;
|
||||
|
||||
// Файлы проекта
|
||||
use mirzaev\ebala\controllers\core,
|
||||
mirzaev\ebala\controllers\traits\errors,
|
||||
mirzaev\ebala\models\market as model,
|
||||
|
||||
// Библиотека для ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
// System libraries
|
||||
use datetime,
|
||||
exception;
|
||||
|
||||
/**
|
||||
* Контроллер магазина
|
||||
*
|
||||
* @package mirzaev\ebala\controllers
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class market extends core
|
||||
{
|
||||
use errors;
|
||||
|
||||
/**
|
||||
* Главная страница
|
||||
*
|
||||
* @param array $parameters Параметры запроса
|
||||
*/
|
||||
public function index(array $parameters = []): ?string
|
||||
{
|
||||
// Авторизация
|
||||
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
|
||||
// Авторизован аккаунт оператора или администратора
|
||||
|
||||
foreach (['confirmed', 'waiting', 'published', 'unpublished', 'problematic', 'hided', 'completed'] as $name) {
|
||||
// Перебор фильтров статусов
|
||||
|
||||
// Инициализация значения (приоритет у cookie)
|
||||
$value = $_COOKIE["markets_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['markets']['filters'][$name] ?? 0;
|
||||
|
||||
// Инициализировано значение?
|
||||
if ($value === null || $value === 0) continue;
|
||||
|
||||
// Генерация класса для HTML-элемента по его состоянию (0 - ОТСУТСТВУЕТ, 1 - И, 2 - ИЛИ)
|
||||
$this->view->{$name} = match ($value) {
|
||||
'0', 0 => 'earth',
|
||||
'1', 1 => 'sand',
|
||||
'2', 2 => 'river',
|
||||
default => 'earth'
|
||||
};
|
||||
}
|
||||
|
||||
// Генерация представлениямя
|
||||
$main = $this->view->render(DIRECTORY_SEPARATOR . 'pages' . DIRECTORY_SEPARATOR . 'markets.html');
|
||||
} else $main = $this->authorization();
|
||||
|
||||
// Возврат (успех)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') return $this->view->render(DIRECTORY_SEPARATOR . 'index.html', ['main' => $main]);
|
||||
else if ($_SERVER['REQUEST_METHOD'] === 'POST') return $main;
|
||||
|
||||
// Возврат (провал)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Прочитать
|
||||
*
|
||||
* @param array $parameters Параметры запроса
|
||||
*/
|
||||
public function read(array $parameters = []): ?string
|
||||
{
|
||||
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
|
||||
// Авторизован аккаунт оператора или администратора
|
||||
|
||||
// Реинициализация актуальной страницы
|
||||
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 = '';
|
||||
|
||||
// Инициализация допустимых статусов
|
||||
$statuses = ['active', 'inactive', 'fined', 'decent', 'hided', 'fired'];
|
||||
|
||||
// Инициализация буфера AQL-выражения для инъекции фильтра по статусам (И)
|
||||
$statuses_and = '';
|
||||
|
||||
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 === '1') $statuses_and .= " && market.$name == true";
|
||||
}
|
||||
|
||||
// Очистка от бинарных операторов сравнения с только одним операндом (крайние)
|
||||
$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), '||'));
|
||||
|
||||
// Инициализация буфера с объёдинёнными буферами c AQL-выражениям "И" и "ИЛИ"
|
||||
$statuses_merged = (empty($statuses_and) ? '' : "($statuses_and)") . (empty($statuses_or) ? '' : (empty($statuses_and) ? '' : ' || ') . "($statuses_or)");
|
||||
|
||||
// Инициализация общего буфера с AQL-выражениями
|
||||
$filters = '';
|
||||
|
||||
// Объединение фильров в единую строку с AQL-выражениями для инъекции
|
||||
if (!empty($statuses_merged)) $filters .= empty($filters) ? $statuses_merged : " && ($statuses_merged)";
|
||||
|
||||
// Инициализация данных для генерации HTML-документа с таблицей
|
||||
$this->view->rows = model::list(before: empty($filters) ? null : "FILTER ($filters)", page: (int) $this->session->buffer[$_SERVER['INTERFACE']]['markets']['page']);
|
||||
|
||||
// Запись в cookie (только таким методом можно записать "hostonly: true")
|
||||
setcookie(
|
||||
'markets_page',
|
||||
(string) $this->session->buffer[$_SERVER['INTERFACE']]['markets']['page'],
|
||||
[
|
||||
'expires' => strtotime('+1 hour'),
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => false,
|
||||
'samesite' => 'strict'
|
||||
]
|
||||
);
|
||||
|
||||
// Запись в глобальную переменную шаблонизатора обрабатываемой страницы
|
||||
$this->view->page = $parameters['page'];
|
||||
|
||||
// Инициализация блока
|
||||
return $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'markets.html');
|
||||
}
|
||||
|
||||
// Возврат (провал)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Прочитать данные магазинов для <datalist>
|
||||
*
|
||||
* @param array $parameters Параметры запроса
|
||||
*/
|
||||
public function datalist(array $parameters = []): ?string
|
||||
{
|
||||
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'market')) {
|
||||
// Авторизован аккаунт оператора или магазина
|
||||
|
||||
// Инициализация данных магазинов
|
||||
$this->view->markets = model::read(filter: 'd.status == "active"', amount: 10000, return: '{ id: d.id, director: d.director }');
|
||||
|
||||
// Универсализация
|
||||
if ($this->view->markets instanceof _document) $this->view->markets = [$this->view->markets];
|
||||
|
||||
// Возврат (успех)
|
||||
return $this->view->render(DIRECTORY_SEPARATOR . 'lists' . DIRECTORY_SEPARATOR . 'markets.html');
|
||||
}
|
||||
|
||||
// Возврат (провал)
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,629 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\ebala\controllers;
|
||||
|
||||
// Файлы проекта
|
||||
use mirzaev\ebala\controllers\core,
|
||||
mirzaev\ebala\controllers\traits\errors,
|
||||
mirzaev\ebala\models\account,
|
||||
mirzaev\ebala\models\market;
|
||||
|
||||
// Библиотека для ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
// Встроенные библиотеки
|
||||
use exception;
|
||||
|
||||
/**
|
||||
* Контроллер сессии
|
||||
*
|
||||
* @package mirzaev\ebala\controllers
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class session extends core
|
||||
{
|
||||
use errors;
|
||||
|
||||
/**
|
||||
* Записать номер сотрудника во все буферы сессии
|
||||
*
|
||||
* Проверяет существование аккаунта сотрудника с этим номером
|
||||
* и запоминает для использования в процессе аутентификации
|
||||
*
|
||||
* @param array $parameters Параметры запроса
|
||||
*
|
||||
* @return void В буфер вывода JSON-документ с запрашиваемыми параметрами
|
||||
*/
|
||||
public function worker(array $parameters = []): void
|
||||
{
|
||||
// Инициализация буфера ответа
|
||||
$buffer = [];
|
||||
|
||||
// Инициализация реестра возвращаемых параметров
|
||||
$return = explode(',', $parameters['return'], 50);
|
||||
|
||||
try {
|
||||
// Проверка наличия обязательных параметров
|
||||
if (empty($parameters['worker'])) throw new exception('Необходимо передать номер');
|
||||
|
||||
// Очистка всего кроме цифр, а потом поиск 10 первых чисел (без восьмёрки)
|
||||
preg_match('/^\d(\d{10})/', preg_replace("/[^\d]/", "", $parameters['worker']), $matches);
|
||||
|
||||
// Инициализация номера
|
||||
$parameters['worker'] = isset($matches[1]) ? 7 . $matches[1] : $parameters['worker'];
|
||||
|
||||
// Вычисление длины
|
||||
$length = strlen($parameters['worker']);
|
||||
|
||||
// Проверка параметров на соответствование требованиям
|
||||
if ($length === 0) throw new exception('Номер не может быть пустым');
|
||||
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') {
|
||||
// Запрошено запоминание
|
||||
|
||||
// Запись в cookie
|
||||
setcookie('entry_number', $parameters['worker'], [
|
||||
'expires' => strtotime('+1 day'),
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'strict'
|
||||
]);
|
||||
}
|
||||
|
||||
// Поиск аккаунта
|
||||
$account = account::read('d.number == "' . $parameters['worker'] . '"', 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' => ['number' => $parameters['worker']]], $this->errors);
|
||||
|
||||
// Поиск аккаунта и запись в буфер вывода
|
||||
$buffer['account'] = (new account($this->session, 'worker', $this->errors))?->instance() instanceof _document;
|
||||
})(),
|
||||
'verify' => $buffer['verify'] = true,
|
||||
'errors' => null,
|
||||
default => throw new exception("Параметр не найден: $parameter")
|
||||
};
|
||||
} catch (exception $e) {
|
||||
// Запись в реестр ошибок
|
||||
$this->errors['session'][] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Запись реестра ошибок в буфер ответа
|
||||
if (in_array('errors', $return, true)) $buffer['errors'] = self::parse_only_text($this->errors);
|
||||
|
||||
// Запись заголовков ответа
|
||||
header('Content-Type: application/json');
|
||||
header('Content-Encoding: none');
|
||||
header('X-Accel-Buffering: no');
|
||||
|
||||
// Инициализация буфера вывода
|
||||
ob_start();
|
||||
|
||||
// Генерация ответа
|
||||
echo json_encode($buffer);
|
||||
|
||||
// Запись заголовков ответа
|
||||
header('Content-Length: ' . ob_get_length());
|
||||
|
||||
// Отправка и деинициализация буфера вывода
|
||||
ob_end_flush();
|
||||
flush();
|
||||
|
||||
// Запись в буфер сессии
|
||||
if (!in_array('account', $return, true) && ($remember ?? false))
|
||||
$this->session->write(['entry' => ['number' => $parameters['worker']]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Записать идентификатор администратора во все буферы сессии
|
||||
*
|
||||
* Проверяет существование аккаунта администратора с этим идентификатором
|
||||
* и запоминает для использования в процессе аутентификации
|
||||
*
|
||||
* @param array $parameters Параметры запроса
|
||||
*
|
||||
* @return void В буфер вывода JSON-документ с запрашиваемыми параметрами
|
||||
*/
|
||||
public function administrator(array $parameters = []): void
|
||||
{
|
||||
// Инициализация буфера ответа
|
||||
$buffer = [];
|
||||
|
||||
// Инициализация реестра возвращаемых параметров
|
||||
$return = explode(',', $parameters['return'], 50);
|
||||
|
||||
try {
|
||||
// Проверка наличия обязательных параметров
|
||||
if (empty($parameters['administrator'])) throw new exception('Необходимо передать идентификатор');
|
||||
|
||||
// Очистка всего кроме цифр, а потом поиск 10 первых чисел (без восьмёрки)
|
||||
preg_match('/^\d{3,12}/', preg_replace("/[^\d]/", "", $parameters['administrator']), $matches);
|
||||
|
||||
// Инициализация номера
|
||||
$parameters['administrator'] = $matches[0];
|
||||
|
||||
// Вычисление длины
|
||||
$length = strlen($parameters['administrator']);
|
||||
|
||||
// Проверка параметров на соответствование требованиям
|
||||
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') {
|
||||
// Запрошено запоминание
|
||||
|
||||
// Запись в cookie
|
||||
setcookie(
|
||||
'entry__key',
|
||||
$parameters['administrator'],
|
||||
[
|
||||
'expires' => strtotime('+1 day'),
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'strict'
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Поиск аккаунта
|
||||
$account = account::read('d._key == "' . $parameters['administrator'] . '"', 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' => ['_key' => $parameters['administrator']]], $this->errors);
|
||||
|
||||
// Поиск аккаунта и запись в буфер вывода
|
||||
$buffer['account'] = (new account($this->session, 'administrator', $this->errors))?->instance() instanceof _document;
|
||||
})(),
|
||||
'verify' => $buffer['verify'] = true,
|
||||
'errors' => null,
|
||||
default => throw new exception("Параметр не найден: $parameter")
|
||||
};
|
||||
} catch (exception $e) {
|
||||
// Запись в реестр ошибок
|
||||
$this->errors['session'][] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Запись реестра ошибок в буфер ответа
|
||||
if (in_array('errors', $return, true)) $buffer['errors'] = self::parse_only_text($this->errors);
|
||||
|
||||
// Запись заголовков ответа
|
||||
header('Content-Type: application/json');
|
||||
header('Content-Encoding: none');
|
||||
header('X-Accel-Buffering: no');
|
||||
|
||||
// Инициализация буфера вывода
|
||||
ob_start();
|
||||
|
||||
// Генерация ответа
|
||||
echo json_encode($buffer);
|
||||
|
||||
// Запись заголовков ответа
|
||||
header('Content-Length: ' . ob_get_length());
|
||||
|
||||
// Отправка и деинициализация буфера вывода
|
||||
ob_end_flush();
|
||||
flush();
|
||||
|
||||
// Запись в буфер сессии
|
||||
if (!in_array('account', $return, true) && ($remember ?? false))
|
||||
$this->session->write(['entry' => ['_key' => $parameters['administrator']]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Записать идентификатор оператора во все буферы сессии
|
||||
*
|
||||
* Проверяет существование аккаунта оператора с этим идентификатором
|
||||
* и запоминает для использования в процессе аутентификации
|
||||
*
|
||||
* @param array $parameters Параметры запроса
|
||||
*
|
||||
* @return void В буфер вывода JSON-документ с запрашиваемыми параметрами
|
||||
*/
|
||||
public function operator(array $parameters = []): void
|
||||
{
|
||||
// Инициализация буфера ответа
|
||||
$buffer = [];
|
||||
|
||||
// Инициализация реестра возвращаемых параметров
|
||||
$return = explode(',', $parameters['return'], 50);
|
||||
|
||||
try {
|
||||
// Проверка наличия обязательных параметров
|
||||
if (empty($parameters['operator'])) throw new exception('Необходимо передать идентификатор');
|
||||
|
||||
// Очистка всего кроме цифр, а потом поиск 10 первых чисел (без восьмёрки)
|
||||
preg_match('/^\d{3,12}/', preg_replace("/[^\d]/", "", $parameters['operator']), $matches);
|
||||
|
||||
// Инициализация номера
|
||||
$parameters['operator'] = $matches[0];
|
||||
|
||||
// Вычисление длины
|
||||
$length = strlen($parameters['operator']);
|
||||
|
||||
// Проверка параметров на соответствование требованиям
|
||||
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') {
|
||||
// Запрошено запоминание
|
||||
|
||||
// Запись в cookie
|
||||
setcookie('entry__key', $parameters['operator'], [
|
||||
'expires' => strtotime('+1 day'),
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'strict'
|
||||
]);
|
||||
}
|
||||
|
||||
// Поиск аккаунта
|
||||
$account = account::read('d._key == "' . $parameters['operator'] . '"', 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' => ['_key' => $parameters['operator']]], $this->errors);
|
||||
|
||||
// Поиск аккаунта и запись в буфер вывода
|
||||
$buffer['account'] = (new account($this->session, 'operator', $this->errors))?->instance() instanceof _document;
|
||||
})(),
|
||||
'verify' => $buffer['verify'] = true,
|
||||
'errors' => null,
|
||||
default => throw new exception("Параметр не найден: $parameter")
|
||||
};
|
||||
} catch (exception $e) {
|
||||
// Запись в реестр ошибок
|
||||
$this->errors['session'][] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Запись реестра ошибок в буфер ответа
|
||||
if (in_array('errors', $return, true)) $buffer['errors'] = self::parse_only_text($this->errors);
|
||||
|
||||
// Запись заголовков ответа
|
||||
header('Content-Type: application/json');
|
||||
header('Content-Encoding: none');
|
||||
header('X-Accel-Buffering: no');
|
||||
|
||||
// Инициализация буфера вывода
|
||||
ob_start();
|
||||
|
||||
// Генерация ответа
|
||||
echo json_encode($buffer);
|
||||
|
||||
// Запись заголовков ответа
|
||||
header('Content-Length: ' . ob_get_length());
|
||||
|
||||
// Отправка и деинициализация буфера вывода
|
||||
ob_end_flush();
|
||||
flush();
|
||||
|
||||
// Запись в буфер сессии
|
||||
if (!in_array('account', $return, true) && ($remember ?? false))
|
||||
$this->session->write(['entry' => ['_key' => $parameters['operator']]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Записать идентификатор магазина во все буферы сессии
|
||||
*
|
||||
* Проверяет существование аккаунта магазина с этим идентификатором
|
||||
* и запоминает для использования в процессе аутентификации
|
||||
*
|
||||
* @param array $parameters Параметры запроса
|
||||
*
|
||||
* @return void В буфер вывода JSON-документ с запрашиваемыми параметрами
|
||||
*/
|
||||
public function market(array $parameters = []): void
|
||||
{
|
||||
// Инициализация буфера ответа
|
||||
$buffer = [];
|
||||
|
||||
// Инициализация реестра возвращаемых параметров
|
||||
$return = explode(',', $parameters['return'], 50);
|
||||
|
||||
try {
|
||||
// Проверка наличия обязательных параметров
|
||||
if (empty($parameters['market'])) throw new exception('Необходимо передать идентификатор');
|
||||
|
||||
// Вычисление длины
|
||||
$length = 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 ($remember = isset($parameters['remember']) && $parameters['remember'] === '1') {
|
||||
// Запрошено запоминание
|
||||
|
||||
// Запись в cookie
|
||||
setcookie('entry_id', $parameters['market'], [
|
||||
'expires' => strtotime('+1 day'),
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'strict'
|
||||
]);
|
||||
}
|
||||
|
||||
// Поиск магазина
|
||||
$market = market::read('d.id == "' . $parameters['market'] . '"', amount: 1);
|
||||
|
||||
// Поиск аккаунта
|
||||
$account = market::account($market->getId());
|
||||
|
||||
// Генерация ответа по запрашиваемым параметрам
|
||||
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);
|
||||
|
||||
// Поиск аккаунта и запись в буфер вывода
|
||||
$buffer['account'] = (new account($this->session, 'market', $this->errors))?->instance() instanceof _document;
|
||||
})(),
|
||||
'verify' => $buffer['verify'] = true,
|
||||
'errors' => null,
|
||||
default => throw new exception("Параметр не найден: $parameter")
|
||||
};
|
||||
} catch (exception $e) {
|
||||
// Запись в реестр ошибок
|
||||
$this->errors['session'][] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Запись реестра ошибок в буфер ответа
|
||||
if (in_array('errors', $return, true)) $buffer['errors'] = self::parse_only_text($this->errors);
|
||||
|
||||
// Запись заголовков ответа
|
||||
header('Content-Type: application/json');
|
||||
header('Content-Encoding: none');
|
||||
header('X-Accel-Buffering: no');
|
||||
|
||||
// Инициализация буфера вывода
|
||||
ob_start();
|
||||
|
||||
// Генерация ответа
|
||||
echo json_encode($buffer);
|
||||
|
||||
// Запись заголовков ответа
|
||||
header('Content-Length: ' . ob_get_length());
|
||||
|
||||
// Отправка и деинициализация буфера вывода
|
||||
ob_end_flush();
|
||||
flush();
|
||||
|
||||
// Запись в буфер сессии
|
||||
if (!in_array('account', $return, true) && ($remember ?? false))
|
||||
$this->session->write(['entry' => ['id' => $parameters['market']]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Записать пароль любого типа аккаунта во все буферы сессии
|
||||
*
|
||||
* Проверяет на соответствие требованиям
|
||||
* и запоминает для использования в процессе аутентификации
|
||||
*
|
||||
* @param array $parameters Параметры запроса
|
||||
*
|
||||
* @return void В буфер вывода JSON-документ с запрашиваемыми параметрами
|
||||
*/
|
||||
public function password(array $parameters = []): void
|
||||
{
|
||||
// Инициализация буфера ответа
|
||||
$buffer = [];
|
||||
|
||||
// Инициализация реестра возвращаемых параметров
|
||||
$return = explode(',', $parameters['return'], 50);
|
||||
|
||||
try {
|
||||
// Вычисление длины
|
||||
$length = strlen($parameters['password']);
|
||||
|
||||
// Проверка параметров на соответствование требованиям
|
||||
if ($length > 300) throw new exception('Пароль не может быть длиннее 300 символов');
|
||||
if (preg_match_all('/[^\w\s\r\n\t\0]+/u', $parameters['password'], $matches) > 0) throw new exception('Нельзя использовать символы: ' . implode(', ', ...$matches));
|
||||
|
||||
// Инициализация значения по умолчанию для типа аккаунта
|
||||
$parameters['type'] ??= 'worker';
|
||||
|
||||
// Генерация ответа по запрашиваемым параметрам
|
||||
foreach ($return as $parameter) match ($parameter) {
|
||||
'account' => (function () use ($parameters, &$buffer) {
|
||||
// Запись в буфер сессии
|
||||
if (isset($parameters['remember']) && $parameters['remember'] === '1')
|
||||
$this->session->write(['entry' => ['password' => $parameters['password']]], $this->errors);
|
||||
|
||||
// Поиск аккаунта и запись в буфер вывода
|
||||
$buffer['account'] = (new account($this->session, $parameters['type'], $this->errors))?->instance() instanceof _document;
|
||||
})(),
|
||||
'verify' => $buffer['verify'] = true,
|
||||
'errors' => null,
|
||||
default => throw new exception("Параметр не найден: $parameter")
|
||||
};
|
||||
} catch (exception $e) {
|
||||
// Запись в реестр ошибок
|
||||
$this->errors['session'][] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Запись реестра ошибок в буфер ответа
|
||||
if (in_array('errors', $return, true)) $buffer['errors'] = self::parse_only_text($this->errors);
|
||||
|
||||
// Запись заголовков ответа
|
||||
header('Content-Type: application/json');
|
||||
header('Content-Encoding: none');
|
||||
header('X-Accel-Buffering: no');
|
||||
|
||||
// Инициализация буфера вывода
|
||||
ob_start();
|
||||
|
||||
// Генерация ответа
|
||||
echo json_encode($buffer);
|
||||
|
||||
// Запись заголовков ответа
|
||||
header('Content-Length: ' . ob_get_length());
|
||||
|
||||
// Отправка и деинициализация буфера вывода
|
||||
ob_end_flush();
|
||||
flush();
|
||||
|
||||
// Запись в буфер сессии
|
||||
if (!in_array('account', $return, true) && isset($parameters['remember']) && $parameters['remember'] === '1')
|
||||
$this->session->write(['entry' => ['password' => $parameters['password']]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Записать в буфер сессии
|
||||
*
|
||||
* @param array $parameters Параметры запроса
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function write(array $parameters = []): void
|
||||
{
|
||||
try {
|
||||
if ($this->account->status()) {
|
||||
// Авторизован аккаунт
|
||||
|
||||
// Инициализация директорий для генерации
|
||||
$directories = explode('_', $parameters['name'], 100);
|
||||
|
||||
// Инициализированы директории?
|
||||
if (count($directories) === 0) return;
|
||||
|
||||
// Конвертация: filter -> filters
|
||||
if ($directories[1] === 'filter') $directories[1] .= 's';
|
||||
|
||||
// Инициализация буфера вывода
|
||||
$response = [];
|
||||
|
||||
// Инициализация буфера выполнения
|
||||
$buffer = &$response;
|
||||
|
||||
foreach ($directories as $directory) {
|
||||
// Перебор директорий
|
||||
|
||||
// Инициализация структуры
|
||||
$buffer[$directory] = ($next = next($directories) === false) ? $parameters['value'] : [];
|
||||
|
||||
// Реинициализация цели для инициализации структуры (углубление в массив)
|
||||
if ($next) unset($buffer);
|
||||
else $buffer = &$buffer[$directory];
|
||||
}
|
||||
|
||||
// Запись в буфер сессии
|
||||
$this->session->write($response);
|
||||
}
|
||||
} catch (exception $e) {
|
||||
// Запись в реестр ошибок
|
||||
$this->errors['session'][] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Прочитать из буфера сессии
|
||||
*
|
||||
* @param array $parameters Параметры запроса
|
||||
*
|
||||
* @return ?string Данные из буфера сессии, если найдены
|
||||
*/
|
||||
public function read(array $parameters = []): ?string
|
||||
{
|
||||
try {
|
||||
if ($this->account->status()) {
|
||||
// Авторизован аккаунт
|
||||
|
||||
// Инициализация директорий для генерации
|
||||
$directories = explode('_', $parameters['name'], 100);
|
||||
|
||||
// Инициализированы директории?
|
||||
if (count($directories) === 0) return null;
|
||||
|
||||
// Конвертация: filter -> filters
|
||||
if ($directories[1] === 'filter') $directories[1] .= 's';
|
||||
|
||||
// Инициализация буфера хранилища
|
||||
$storage = $this->session->buffer[$_SERVER['INTERFACE']];
|
||||
|
||||
// Инициализация буфера выполнения
|
||||
$buffer = &$storage[reset($directories)] ?? null;
|
||||
|
||||
// Найдена первая директория в базе данных?
|
||||
if (isset($buffer) === 0) return null;
|
||||
|
||||
foreach ($directories as &$directory) {
|
||||
// Перебор директорий
|
||||
|
||||
// Инициализация новой целевой директории
|
||||
if (isset($buffer[$directory])) $buffer = &$buffer[$directory];
|
||||
else continue;
|
||||
|
||||
// Реинициализация цели для инициализации структуры (углубление в массив)
|
||||
if (next($directories) === false) $buffer = $buffer[$directory];
|
||||
}
|
||||
|
||||
// Возврат (успех)
|
||||
return is_array($buffer) ? null : $buffer;
|
||||
}
|
||||
} catch (exception $e) {
|
||||
// Запись в реестр ошибок
|
||||
$this->errors['session'][] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Возврат (провал)
|
||||
return null;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\ebala\controllers\traits;
|
||||
|
||||
/**
|
||||
* Заготовка для обработки ошибок
|
||||
*
|
||||
* @package mirzaev\ebala\controllers\traits
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
trait errors
|
||||
{
|
||||
private static function parse_only_text(array $errors): array
|
||||
{
|
||||
// Инициализация буфера вывода
|
||||
$buffer = [];
|
||||
|
||||
foreach ($errors as $offset => $error) {
|
||||
// Перебор ошибок
|
||||
|
||||
// Проверка на вложенность и запись в буфер вывода (вход в рекурсию)
|
||||
if (isset($error['text'])) $buffer[] = $error['text'];
|
||||
else if (is_array($error) && count($error) > 0) $buffer[$offset] = static::parse_only_text($error);
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\ebala\controllers;
|
||||
|
||||
// Файлы проекта
|
||||
use mirzaev\ebala\controllers\core,
|
||||
mirzaev\ebala\controllers\traits\errors,
|
||||
mirzaev\ebala\models\worker as model;
|
||||
|
||||
// Библиотека для ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
/**
|
||||
* Контроллер сотрудника
|
||||
*
|
||||
* @package mirzaev\ebala\controllers
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class worker extends core
|
||||
{
|
||||
use errors;
|
||||
|
||||
/**
|
||||
* Главная страница
|
||||
*
|
||||
* @param array $parameters Параметры запроса
|
||||
*/
|
||||
public function index(array $parameters = []): ?string
|
||||
{
|
||||
// Авторизация
|
||||
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
|
||||
// Авторизован аккаунт оператора или администратора
|
||||
|
||||
foreach (['active', 'inactive', 'fined', 'decent', 'hided', 'fired'] as $name) {
|
||||
// Перебор фильтров статусов
|
||||
|
||||
// Инициализация значения (приоритет у cookie)
|
||||
$value = $_COOKIE["workers_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['workers']['filters'][$name] ?? 0;
|
||||
|
||||
// Инициализировано значение?
|
||||
if ($value === null || $value === 0) continue;
|
||||
|
||||
// Генерация класса для HTML-элемента по его состоянию (0 - ОТСУТСТВУЕТ, 1 - И, 2 - ИЛИ)
|
||||
$this->view->{$name} = match ($value) {
|
||||
'0', 0 => 'earth',
|
||||
'1', 1 => 'sand',
|
||||
'2', 2 => 'river',
|
||||
default => 'earth'
|
||||
};
|
||||
}
|
||||
|
||||
// Генерация представлениямя
|
||||
$main = $this->view->render(DIRECTORY_SEPARATOR . 'pages' . DIRECTORY_SEPARATOR . 'workers.html');
|
||||
} else $main = $this->authorization();
|
||||
|
||||
// Возврат (успех)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') return $this->view->render(DIRECTORY_SEPARATOR . 'index.html', ['main' => $main]);
|
||||
else if ($_SERVER['REQUEST_METHOD'] === 'POST') return $main;
|
||||
|
||||
// Возврат (провал)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Прочитать
|
||||
*
|
||||
* @param array $parameters Параметры запроса
|
||||
*/
|
||||
public function read(array $parameters = []): ?string
|
||||
{
|
||||
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
|
||||
// Авторизован аккаунт оператора или администратора
|
||||
|
||||
// Реинициализация актуальной страницы
|
||||
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 = '';
|
||||
|
||||
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) {
|
||||
// Перебор фильтров статусов (И)
|
||||
|
||||
// Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue)
|
||||
if (empty($value = $_COOKIE["workers_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['workers']['filters'][$name] ?? 0)) continue;
|
||||
|
||||
// Генерация AQL-выражения для инъекции в строку запроса
|
||||
if ($value === '1') $statuses_and .= " && worker.$name == true";
|
||||
}
|
||||
|
||||
// Очистка от бинарных операторов сравнения с только одним операндом (крайние)
|
||||
$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), '||'));
|
||||
|
||||
// Инициализация буфера с объёдинёнными буферами c AQL-выражениям "И" и "ИЛИ"
|
||||
$statuses_merged = (empty($statuses_and) ? '' : "($statuses_and)") . (empty($statuses_or) ? '' : (empty($statuses_and) ? '' : ' || ') . "($statuses_or)");
|
||||
|
||||
// Инициализация общего буфера с AQL-выражениями
|
||||
$filters = '';
|
||||
|
||||
// Объединение фильров в единую строку с AQL-выражениями для инъекции
|
||||
if (!empty($statuses_merged)) $filters .= empty($filters) ? $statuses_merged : " && ($statuses_merged)";
|
||||
if (!empty($polysemantic)) $filters .= empty($filters) ? $polysemantic : " && $polysemantic";
|
||||
|
||||
// Инициализация данных для генерации HTML-документа с таблицей
|
||||
$this->view->rows = model::list(before: empty($filters) ? null : "FILTER ($filters)", page: (int) $this->session->buffer[$_SERVER['INTERFACE']]['workers']['page']);
|
||||
|
||||
// Запись в cookie (только таким методом можно записать "hostonly: true")
|
||||
setcookie(
|
||||
'workers_page',
|
||||
(string) $this->session->buffer[$_SERVER['INTERFACE']]['workers']['page'],
|
||||
[
|
||||
'expires' => strtotime('+1 hour'),
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => false,
|
||||
'samesite' => 'strict'
|
||||
]
|
||||
);
|
||||
|
||||
// Запись в глобальную переменную шаблонизатора обрабатываемой страницы
|
||||
$this->view->page = $parameters['page'];
|
||||
|
||||
// Инициализация блока
|
||||
return $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'workers.html');
|
||||
}
|
||||
|
||||
// Возврат (провал)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Прочитать данные сотрудников для <datalist>
|
||||
*
|
||||
* @param array $parameters Параметры запроса
|
||||
*/
|
||||
public function datalist(array $parameters = []): ?string
|
||||
{
|
||||
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'market')) {
|
||||
// Авторизован аккаунт оператора или магазина
|
||||
|
||||
// Инициализация данных сотрудников
|
||||
$this->view->workers = model::read(filter: 'd.status == "active"', amount: 10000, return: '{ id: d.id, name: d.name }');
|
||||
|
||||
// Универсализация
|
||||
if ($this->view->workers instanceof _document) $this->view->workers = [$this->view->workers];
|
||||
|
||||
// Возврат (успех)
|
||||
return $this->view->render(DIRECTORY_SEPARATOR . 'lists' . DIRECTORY_SEPARATOR . 'workers.html');
|
||||
}
|
||||
|
||||
// Возврат (провал)
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,491 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\ebala\models;
|
||||
|
||||
// Project files
|
||||
use mirzaev\ebala\models\traits\instance,
|
||||
mirzaev\ebala\models\traits\status;
|
||||
|
||||
// Фреймворк ArangoDB
|
||||
use mirzaev\arangodb\collection,
|
||||
mirzaev\arangodb\document;
|
||||
|
||||
// Библиотека для ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
// Встроенные библиотеки
|
||||
use exception;
|
||||
|
||||
/**
|
||||
* Модель аккаунта
|
||||
*
|
||||
* @package mirzaev\ebala\models
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class account extends core
|
||||
{
|
||||
use instance, status;
|
||||
|
||||
/**
|
||||
* Коллекция
|
||||
*/
|
||||
final public const COLLECTION = 'account';
|
||||
|
||||
/**
|
||||
* Инстанция документа в базе данных
|
||||
*/
|
||||
protected readonly _document $document;
|
||||
|
||||
/**
|
||||
* Конструктор
|
||||
*
|
||||
* @param ?session $session Инстанция сессии
|
||||
* @param ?string $authenticate Аутентифицировать аккаунт? Если да, то какой категории? ([worker|operator|market] из $_SERVER['INTERFACE'])
|
||||
* @param array &$errors Реестр ошибок
|
||||
*
|
||||
* @return static Инстанция аккаунта
|
||||
*/
|
||||
public function __construct(?session $session = null, ?string $authenticate = null, array &$errors = [])
|
||||
{
|
||||
try {
|
||||
if (isset($session)) {
|
||||
// Получена инстанция сессии
|
||||
|
||||
if ($account = $session->account()) {
|
||||
// Найден связанный с сессией аккаунт
|
||||
|
||||
// Инициализация инстанции документа аккаунта в базе данных
|
||||
$this->document = $account->document;
|
||||
|
||||
// Связь сессии с аккаунтом
|
||||
$session->connect($this, $errors);
|
||||
|
||||
return $this;
|
||||
} else {
|
||||
// Не найден связанный с сессией аккаунт
|
||||
if (
|
||||
match ($authenticate) {
|
||||
'worker', 'operator', 'market', 'administrator' => true,
|
||||
default => false
|
||||
}
|
||||
) {
|
||||
// Запрошена аутентификация
|
||||
|
||||
if (!empty($session->buffer['worker']['entry']['number'])) {
|
||||
// Найден номер сотрудника в буфере сессии
|
||||
|
||||
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 (sodium_crypto_pwhash_str_verify($account->password, $session->buffer['worker']['entry']['password'])) {
|
||||
// Аутентифицирован аккаунт (прошёл проверку пароль)
|
||||
|
||||
// Инициализация инстанции документа аккаунта в базе данных
|
||||
$this->document = $account;
|
||||
|
||||
// Связь сессии с аккаунтом
|
||||
$session->connect($this, $errors);
|
||||
|
||||
// Удаление использованных данных из буфера сессии
|
||||
$session->write(['entry' => ['number' => null, 'password' => null]]);
|
||||
|
||||
// Выход (успех)
|
||||
return $this;
|
||||
} 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'])) {
|
||||
// Найден идентификатор оператора в буфере сессии
|
||||
|
||||
if (!empty($session->buffer['operator']['entry']['password'])) {
|
||||
// Найден пароль в буфере сессии
|
||||
|
||||
if (($account = self::read('d._key == "' . $session->buffer['operator']['entry']['_key'] . '"', amount: 1)) instanceof _document) {
|
||||
// Найден аккаунт оператора (игнорируются ошибки)
|
||||
|
||||
if (sodium_crypto_pwhash_str_verify($account->password, $session->buffer['operator']['entry']['password'])) {
|
||||
// Аутентифицирован аккаунт (прошёл проверку пароль)
|
||||
|
||||
// Инициализация инстанции документа аккаунта в базе данных
|
||||
$this->document = $account;
|
||||
|
||||
// Связь сессии с аккаунтом
|
||||
$session->connect($this, $errors);
|
||||
|
||||
// Удаление использованных данных из буфера сессии
|
||||
$session->write(['entry' => ['_key' => null, 'password' => null]]);
|
||||
|
||||
// Выход (успех)
|
||||
return $this;
|
||||
} else throw new exception('Неправильный пароль');
|
||||
|
||||
throw new exception('Неизвестная ошибка на этапе проверки пароля');
|
||||
}
|
||||
} else throw new exception('Не найден пароль в буфере сессии');
|
||||
} else if (!empty($session->buffer['market']['entry'])) {
|
||||
// Найден идентификатор магазина в буфере сессии
|
||||
|
||||
if (!empty($session->buffer['market']['entry']['password'])) {
|
||||
// Найден пароль в буфере сессии
|
||||
|
||||
if (($market = market::read('d.id == "' . $session->buffer['market']['entry']['id'] . '"', 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'])) {
|
||||
// Аутентифицирован аккаунт (прошёл проверку пароль)
|
||||
|
||||
// Инициализация инстанции документа аккаунта в базе данных
|
||||
$this->document = $account;
|
||||
|
||||
// Связь сессии с аккаунтом
|
||||
$session->connect($this, $errors);
|
||||
|
||||
// Удаление использованных данных из буфера сессии
|
||||
$session->write(['entry' => ['id' => null, 'password' => null]]);
|
||||
|
||||
// Выход (успех)
|
||||
return $this;
|
||||
} else throw new exception('Неправильный пароль');
|
||||
|
||||
throw new exception('Неизвестная ошибка на этапе проверки пароля');
|
||||
}
|
||||
}
|
||||
} else throw new exception('Не найден пароль в буфере сессии');
|
||||
} else if (!empty($session->buffer['administrator']['entry'])) {
|
||||
// Найден идентификатор администратора в буфере сессии
|
||||
|
||||
if (!empty($session->buffer['administrator']['entry']['password'])) {
|
||||
// Найден пароль в буфере сессии
|
||||
|
||||
if (($account = self::read('d._key == "' . $session->buffer['administrator']['entry']['_key'] . '"', amount: 1)) instanceof _document) {
|
||||
// Найден аккаунт администратора (игнорируются ошибки)
|
||||
|
||||
if (sodium_crypto_pwhash_str_verify($account->password, $session->buffer['administrator']['entry']['password'])) {
|
||||
// Аутентифицирован аккаунт (прошёл проверку пароль)
|
||||
|
||||
// Инициализация инстанции документа аккаунта в базе данных
|
||||
$this->document = $account;
|
||||
|
||||
// Связь сессии с аккаунтом
|
||||
$session->connect($this, $errors);
|
||||
|
||||
// Удаление использованных данных из буфера сессии
|
||||
$session->write(['entry' => ['_key' => null, 'password' => null]]);
|
||||
|
||||
// Выход (успех)
|
||||
return $this;
|
||||
} else throw new exception('Неправильный пароль');
|
||||
|
||||
throw new exception('Неизвестная ошибка на этапе проверки пароля');
|
||||
}
|
||||
} else throw new exception('Не найден пароль в буфере сессии');
|
||||
} else throw new exception('Не найдены данные первичной идентификации в буфере сессии');
|
||||
}
|
||||
}
|
||||
} else throw new exception('Не найдена сессия');
|
||||
} catch (exception $e) {
|
||||
// Запись в реестр ошибок
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Найти сотрудника
|
||||
*
|
||||
* @param int|string $number Номер
|
||||
* @param array &$errors Реестр ошибок
|
||||
*
|
||||
* @return ?_document Инстанция документа сотрудника в базе данных, если найдена
|
||||
*/
|
||||
public static function worker(int|string $number, 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
|
||||
RETURN d
|
||||
AQL,
|
||||
worker::COLLECTION,
|
||||
$number
|
||||
)
|
||||
);
|
||||
|
||||
// Возврат (успех)
|
||||
return $worker instanceof _document ? $worker : 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Найти связанный магазин
|
||||
*
|
||||
* @param string $_id Идентификатор аккаунта связанного с магазином
|
||||
* @param array &$errors Реестр ошибок
|
||||
*
|
||||
* @return _document|null Инстанция документа магазина в базе данных, если найдена
|
||||
*/
|
||||
public static function market(string $_id, array &$errors = []): ?_document
|
||||
{
|
||||
try {
|
||||
if (
|
||||
collection::init(static::$arangodb->session, static::COLLECTION)
|
||||
&& collection::init(static::$arangodb->session, market::COLLECTION)
|
||||
&& collection::init(static::$arangodb->session, static::COLLECTION . '_edge_market', true)
|
||||
) {
|
||||
// Инициализированы коллекции
|
||||
|
||||
return collection::search(
|
||||
static::$arangodb->session,
|
||||
sprintf(
|
||||
<<<'AQL'
|
||||
FOR d IN %s
|
||||
LET e = (
|
||||
FOR e IN %s
|
||||
FILTER e._from == '%s'
|
||||
SORT e._key DESC
|
||||
LIMIT 1
|
||||
RETURN e
|
||||
)
|
||||
FILTER d._id == e[0]._to && d.status == 'active'
|
||||
SORT d.created DESC
|
||||
LIMIT 1
|
||||
RETURN d
|
||||
AQL,
|
||||
market::COLLECTION,
|
||||
static::COLLECTION . '_edge_market',
|
||||
$_id
|
||||
)
|
||||
);
|
||||
} else throw new exception('Не удалось инициализировать коллекции');
|
||||
} catch (exception $e) {
|
||||
// Запись в реестр ошибок
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализировать связь аккаунта с сотрудником
|
||||
*
|
||||
* Ищет связь аккаунта с сотрудником, если не находит, то создаёт её
|
||||
*
|
||||
* @param _document $worker Инстанция документа в базе данных сотрудника
|
||||
* @param array &$errors Реестр ошибок
|
||||
*
|
||||
* @return bool Связан аккаунт с сотрудником?
|
||||
*/
|
||||
public function connect(_document $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)
|
||||
) {
|
||||
// Инициализированы коллекции
|
||||
|
||||
if (
|
||||
collection::search($this::$arangodb->session, sprintf(
|
||||
<<<AQL
|
||||
FOR d IN %s
|
||||
FILTER d._from == '%s' && d._to == '%s'
|
||||
SORT d.created DESC
|
||||
LIMIT 1
|
||||
RETURN d
|
||||
AQL,
|
||||
self::COLLECTION . '_edge_worker',
|
||||
$this->document->getId(),
|
||||
$worker->getId()
|
||||
)) instanceof _document
|
||||
|| document::write($this::$arangodb->session, self::COLLECTION . '_edge_worker', [
|
||||
'_from' => $this->document->getId(),
|
||||
'_to' => $worker->getId()
|
||||
])
|
||||
) {
|
||||
// Найдено, либо создано ребро: account -> worker
|
||||
|
||||
return true;
|
||||
} else throw new exception('Не удалось создать ребро: account -> worker');
|
||||
} else throw new exception('Не удалось инициализировать коллекцию');
|
||||
} catch (exception $e) {
|
||||
// Запись в реестр ошибок
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Создать
|
||||
*
|
||||
* @param array $data Данные аккаунта
|
||||
* @param array &$errors Реестр ошибок
|
||||
*
|
||||
* @return bool Создан аккаунт?
|
||||
*/
|
||||
public static function create(array $data = [], array &$errors = []): bool
|
||||
{
|
||||
try {
|
||||
if (collection::init(static::$arangodb->session, self::COLLECTION))
|
||||
if (document::write(static::$arangodb->session, self::COLLECTION, $data)) return true;
|
||||
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 false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Записать
|
||||
*
|
||||
* Записывает свойство в инстанцию документа аккаунта из базы данных
|
||||
*
|
||||
* @param string $name Название
|
||||
* @param mixed $value Содержимое
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __set(string $name, mixed $value = null): void
|
||||
{
|
||||
$this->document->{$name} = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Прочитать
|
||||
*
|
||||
* Читает свойство из инстанции документа аккаунта из базы данных
|
||||
*
|
||||
* @param string $name Название
|
||||
*
|
||||
* @return mixed Данные свойства инстанции аккаунта или инстанции документа аккаунта из базы данных
|
||||
*/
|
||||
public function __get(string $name): mixed
|
||||
{
|
||||
return $this->document->{$name};
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить инициализированность
|
||||
*
|
||||
* Проверяет инициализированность свойства в инстанции документа аккаунта из базы данных
|
||||
*
|
||||
* @param string $name Название
|
||||
*
|
||||
* @return bool Свойство инициализировано?
|
||||
*/
|
||||
public function __isset(string $name): bool
|
||||
{
|
||||
return isset($this->document->{$name});
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить
|
||||
*
|
||||
* Деинициализировать свойство в инстанции документа аккаунта из базы данных
|
||||
*
|
||||
* @param string $name Название
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __unset(string $name): void
|
||||
{
|
||||
unset($this->document->{$name});
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполнить метод
|
||||
*
|
||||
* Выполнить метод в инстанции документа аккаунта из базы данных
|
||||
*
|
||||
* @param string $name Название
|
||||
* @param array $arguments Аргументы
|
||||
*/
|
||||
public function __call(string $name, array $arguments = [])
|
||||
{
|
||||
if (method_exists($this->document, $name)) return $this->document->{$name}($arguments);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,275 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\ebala\models;
|
||||
|
||||
// Фреймворк PHP
|
||||
use mirzaev\minimal\model;
|
||||
|
||||
// Фреймворк ArangoDB
|
||||
use mirzaev\arangodb\connection as arangodb,
|
||||
mirzaev\arangodb\collection,
|
||||
mirzaev\arangodb\document;
|
||||
|
||||
// Библиотека для ArangoDB
|
||||
use ArangoDBClient\Document as _document,
|
||||
ArangoDBClient\DocumentHandler as _document_handler;
|
||||
|
||||
// Встроенные библиотеки
|
||||
use exception;
|
||||
|
||||
/**
|
||||
* Ядро моделей
|
||||
*
|
||||
* @package mirzaev\ebala\models
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
class core extends model
|
||||
{
|
||||
/**
|
||||
* Постфикс
|
||||
*/
|
||||
final public const POSTFIX = '';
|
||||
|
||||
/**
|
||||
* Путь до файла с настройками подключения к базе данных ArangoDB
|
||||
*/
|
||||
final public const ARANGODB = '../settings/arangodb.php';
|
||||
|
||||
/**
|
||||
* Соединение с базой данных ArangoDB
|
||||
*/
|
||||
protected static arangodb $arangodb;
|
||||
|
||||
/**
|
||||
* Коллекция
|
||||
*/
|
||||
public const COLLECTION = 'THIS_COLLECTION_SHOULD_NOT_EXIST';
|
||||
|
||||
/**
|
||||
* Конструктор
|
||||
*
|
||||
* @param bool $initialize Инициализировать модель?
|
||||
* @param ?arangodb $arangodb Инстанция соединения с базой данных ArangoDB
|
||||
*/
|
||||
public function __construct(bool $initialize = true, ?arangodb $arangodb = null)
|
||||
{
|
||||
parent::__construct($initialize);
|
||||
|
||||
if ($initialize) {
|
||||
// Запрошена инициализация
|
||||
|
||||
if (isset($arangodb)) {
|
||||
// Получена инстанция соединения с базой данных
|
||||
|
||||
// Запись и инициализация соединения с базой данных
|
||||
$this->__set('arangodb', $arangodb);
|
||||
} else {
|
||||
// Не получена инстанция соединения с базой данных
|
||||
|
||||
// Инициализация соединения с базой данных по умолчанию
|
||||
$this->__get('arangodb');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read from ArangoDB
|
||||
*
|
||||
* @param string $filter Выражения для фильтрации на языке AQL
|
||||
* @param string $sort Выражение для сортировки на языке AQL
|
||||
* @param int $amount Количество документов для выборки
|
||||
* @param int $page Страница
|
||||
* @param string $return Выражение описываемое возвращаемые данные на языке AQL
|
||||
* @param array &$errors Реестр ошибок
|
||||
*
|
||||
* @return _document|array|null Массив инстанций документов в базе данных, если найдены
|
||||
*/
|
||||
public static function read(
|
||||
string $filter = '',
|
||||
string $sort = 'd.created DESC',
|
||||
int $amount = 1,
|
||||
int $page = 1,
|
||||
string $return = 'd',
|
||||
array &$errors = []
|
||||
): _document|array|null {
|
||||
try {
|
||||
if (collection::init(static::$arangodb->session, static::COLLECTION)) {
|
||||
// Инициализирована коллекция
|
||||
|
||||
// Exit (success)
|
||||
return collection::search(
|
||||
static::$arangodb->session,
|
||||
sprintf(
|
||||
<<<'AQL'
|
||||
FOR d IN %s
|
||||
%s
|
||||
%s
|
||||
LIMIT %d, %d
|
||||
RETURN %s
|
||||
AQL,
|
||||
static::COLLECTION,
|
||||
empty($filter) ? '' : "FILTER $filter",
|
||||
empty($sort) ? '' : "SORT $sort",
|
||||
--$page <= 0 ? 0 : $amount * $page,
|
||||
$amount,
|
||||
$return
|
||||
)
|
||||
);
|
||||
} else throw new exception('Не удалось инициализировать коллекцию');
|
||||
} catch (exception $e) {
|
||||
// Запись в реестр ошибок
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete from ArangoDB
|
||||
*
|
||||
* @param _document $instance Instance of the document in ArangoDB
|
||||
* @param array &$errors Реестр ошибок
|
||||
*
|
||||
* @return bool Deleted without errors?
|
||||
*/
|
||||
public static function delete(_document $instance, array &$errors = []): bool
|
||||
{
|
||||
try {
|
||||
if (collection::init(static::$arangodb->session, static::COLLECTION)) {
|
||||
// Инициализирована коллекция
|
||||
|
||||
return (new _document_handler(static::$arangodb->session))->remove($instance);
|
||||
} else throw new exception('Не удалось инициализировать коллекцию');
|
||||
} catch (exception $e) {
|
||||
// Запись в реестр ошибок
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить инстанцию в базе данных
|
||||
*
|
||||
* Да поебать мне
|
||||
*
|
||||
* @param _document $instance Инстанция документа ArangoDB
|
||||
*
|
||||
* @return bool Записано в базу данных?
|
||||
*/
|
||||
public static function update(_document $instance): bool
|
||||
{
|
||||
return document::update(static::$arangodb->session, $instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Записать свойство
|
||||
*
|
||||
* @param string $name Название
|
||||
* @param mixed $value Значение
|
||||
*/
|
||||
public function __set(string $name, mixed $value = null): void
|
||||
{
|
||||
match ($name) {
|
||||
'arangodb' => (function () use ($value) {
|
||||
if ($this->__isset('arangodb')) {
|
||||
// Свойство уже было инициализировано
|
||||
|
||||
// Выброс исключения (неудача)
|
||||
throw new exception('Запрещено реинициализировать соединение с базой данных ArangoDB ($this::$arangodb)', 500);
|
||||
} else {
|
||||
// Свойство ещё не было инициализировано
|
||||
|
||||
if ($value instanceof arangodb) {
|
||||
// Передано подходящее значение
|
||||
|
||||
// Запись свойства (успех)
|
||||
self::$arangodb = $value;
|
||||
} else {
|
||||
// Передано неподходящее значение
|
||||
|
||||
// Выброс исключения (неудача)
|
||||
throw new exception('Соединение с базой данных ArangoDB ($this::$arangodb) должно быть инстанцией mirzaev\arangodb\connection', 500);
|
||||
}
|
||||
}
|
||||
})(),
|
||||
default => parent::__set($name, $value)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Прочитать свойство
|
||||
*
|
||||
* @param string $name Название
|
||||
*
|
||||
* @return mixed Содержимое
|
||||
*/
|
||||
public function __get(string $name): mixed
|
||||
{
|
||||
return match ($name) {
|
||||
'arangodb' => (function () {
|
||||
try {
|
||||
if (!$this->__isset('arangodb')) {
|
||||
// Свойство не инициализировано
|
||||
|
||||
// Инициализация значения по умолчанию исходя из настроек
|
||||
$this->__set('arangodb', new arangodb(require static::ARANGODB));
|
||||
}
|
||||
|
||||
return self::$arangodb;
|
||||
} catch (exception) {
|
||||
return null;
|
||||
}
|
||||
})(),
|
||||
default => parent::__get($name)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить свойство на инициализированность
|
||||
*
|
||||
* @param string $name Название
|
||||
*/
|
||||
public function __isset(string $name): bool
|
||||
{
|
||||
return parent::__isset($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить свойство
|
||||
*
|
||||
* @param string $name Название
|
||||
*/
|
||||
public function __unset(string $name): void
|
||||
{
|
||||
parent::__unset($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Статический вызов
|
||||
*
|
||||
* @param string $name Название
|
||||
* @param array $arguments Параметры
|
||||
*/
|
||||
public static function __callStatic(string $name, array $arguments): mixed
|
||||
{
|
||||
match ($name) {
|
||||
'arangodb' => (new static)->__get('arangodb'),
|
||||
default => throw new exception("Не найдено свойство или функция: $name", 500)
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,245 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\ebala\models;
|
||||
|
||||
// Project files
|
||||
use mirzaev\ebala\models\traits\instance,
|
||||
mirzaev\ebala\models\traits\status;
|
||||
|
||||
// Фреймворк ArangoDB
|
||||
use mirzaev\arangodb\collection;
|
||||
|
||||
// Библиотека для ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
// Встроенные библиотеки
|
||||
use exception;
|
||||
|
||||
/**
|
||||
* Модель магазина
|
||||
*
|
||||
* @package mirzaev\ebala\models
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class market extends core
|
||||
{
|
||||
use instance, status;
|
||||
|
||||
/**
|
||||
* Коллекция
|
||||
*
|
||||
* @todo Исправить "markets" на "market"
|
||||
*/
|
||||
/* final public const COLLECTION = 'market'; */
|
||||
final public const COLLECTION = 'markets';
|
||||
|
||||
/**
|
||||
* Инстанция документа в базе данных
|
||||
*/
|
||||
protected readonly _document $document;
|
||||
|
||||
/**
|
||||
* Конструктор
|
||||
*
|
||||
* @param _document $instance Инстанция в базе данных
|
||||
* @param array &$errors Реестр ошибок
|
||||
*
|
||||
* @return static Инстанция магазина
|
||||
*/
|
||||
public function __construct(_document $instance, array &$errors = [])
|
||||
{
|
||||
try {
|
||||
// Инициализация инстанции документа магазина в базе данных
|
||||
$this->document = $instance;
|
||||
} catch (exception $e) {
|
||||
// Запись в реестр ошибок
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read markets from ArangoDB
|
||||
*
|
||||
* @param ?string $before Injection of AQL-code before search of market and market
|
||||
* @param int $amount Amount of markets
|
||||
* @param int $page Offset by amount
|
||||
* @param array $errors Errors registry
|
||||
*
|
||||
* @return array Instances from ArangoDB
|
||||
*/
|
||||
public static function list(
|
||||
?string $before = '',
|
||||
int $amount = 100,
|
||||
int $page = 1,
|
||||
string $sort = 'market.created DESC',
|
||||
array &$errors = []
|
||||
): array {
|
||||
try {
|
||||
if (collection::init(static::$arangodb->session, self::COLLECTION)) {
|
||||
// Инициализирована коллекция
|
||||
|
||||
// Search the session data in ArangoDB
|
||||
$markets = collection::search(static::$arangodb->session, sprintf(
|
||||
<<<AQL
|
||||
FOR market IN %s
|
||||
FILTER market.status != 'deleted'
|
||||
%s
|
||||
SORT %s
|
||||
LIMIT %d, %d
|
||||
RETURN {market}
|
||||
AQL,
|
||||
self::COLLECTION,
|
||||
$before,
|
||||
$sort,
|
||||
--$page <= 0 ? 0 : $amount * $page,
|
||||
$amount
|
||||
));
|
||||
|
||||
// Exit (success)
|
||||
return empty($markets) ? [] : (is_array($markets) ? $markets : [$markets]);
|
||||
} else throw new exception('Не удалось инициализировать коллекции');
|
||||
} catch (exception $e) {
|
||||
// Write to the errors registry
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Найти связанный аккаунт
|
||||
*
|
||||
* @param string $_id Идентификатор магазина для которого будет искаться аккаунт
|
||||
* @param array &$errors Реестр ошибок
|
||||
*
|
||||
* @return _document|array|null Массив инстанций документов в базе данных, если найдены
|
||||
*/
|
||||
public static function account(string $_id, array &$errors = []): _document|array|null
|
||||
{
|
||||
try {
|
||||
if (
|
||||
collection::init(static::$arangodb->session, static::COLLECTION)
|
||||
&& collection::init(static::$arangodb->session, account::COLLECTION)
|
||||
&& collection::init(static::$arangodb->session, account::COLLECTION . '_edge_market', true)
|
||||
) {
|
||||
// Инициализированы коллекции
|
||||
|
||||
return collection::search(
|
||||
static::$arangodb->session,
|
||||
sprintf(
|
||||
<<<'AQL'
|
||||
FOR d IN %s
|
||||
LET e = (
|
||||
FOR e IN %s
|
||||
FILTER e._to == "%s"
|
||||
SORT e.created DESC
|
||||
LIMIT 1
|
||||
RETURN e
|
||||
)
|
||||
FILTER d._id == e[0]._from && d.status == "active"
|
||||
SORT d.created DESC
|
||||
LIMIT 1
|
||||
RETURN d
|
||||
AQL,
|
||||
account::COLLECTION,
|
||||
account::COLLECTION . '_edge_market',
|
||||
$_id
|
||||
)
|
||||
);
|
||||
} else throw new exception('Не удалось инициализировать коллекции');
|
||||
} catch (exception $e) {
|
||||
// Запись в реестр ошибок
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Записать
|
||||
*
|
||||
* Записывает свойство в инстанцию документа аккаунта из базы данных
|
||||
*
|
||||
* @param string $name Название
|
||||
* @param mixed $value Содержимое
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __set(string $name, mixed $value = null): void
|
||||
{
|
||||
$this->document->{$name} = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Прочитать
|
||||
*
|
||||
* Читает свойство из инстанции документа аккаунта из базы данных
|
||||
*
|
||||
* @param string $name Название
|
||||
*
|
||||
* @return mixed Данные свойства инстанции аккаунта или инстанции документа аккаунта из базы данных
|
||||
*/
|
||||
public function __get(string $name): mixed
|
||||
{
|
||||
return $this->document->{$name};
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить инициализированность
|
||||
*
|
||||
* Проверяет инициализированность свойства в инстанции документа аккаунта из базы данных
|
||||
*
|
||||
* @param string $name Название
|
||||
*
|
||||
* @return bool Свойство инициализировано?
|
||||
*/
|
||||
public function __isset(string $name): bool
|
||||
{
|
||||
return isset($this->document->{$name});
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить
|
||||
*
|
||||
* Деинициализировать свойство в инстанции документа аккаунта из базы данных
|
||||
*
|
||||
* @param string $name Название
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __unset(string $name): void
|
||||
{
|
||||
unset($this->document->{$name});
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполнить метод
|
||||
*
|
||||
* Выполнить метод в инстанции документа аккаунта из базы данных
|
||||
*
|
||||
* @param string $name Название
|
||||
* @param array $arguments Аргументы
|
||||
*/
|
||||
public function __call(string $name, array $arguments = [])
|
||||
{
|
||||
if (method_exists($this->document, $name)) return $this->document->{$name}($arguments);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,413 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\ebala\models;
|
||||
|
||||
// Файлы проекта
|
||||
use mirzaev\ebala\models\account,
|
||||
mirzaev\ebala\models\traits\status;
|
||||
|
||||
// Фреймворк ArangoDB
|
||||
use mirzaev\arangodb\collection,
|
||||
mirzaev\arangodb\document;
|
||||
|
||||
// Библиотека для ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
// Встроенные библиотеки
|
||||
use exception;
|
||||
|
||||
/**
|
||||
* Модель сессий
|
||||
*
|
||||
* @package mirzaev\ebala\models
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class session extends core
|
||||
{
|
||||
use status;
|
||||
|
||||
/**
|
||||
* Collection name in ArangoDB
|
||||
*/
|
||||
final public const COLLECTION = 'session';
|
||||
|
||||
/**
|
||||
* Инстанция документа в базе данных
|
||||
*/
|
||||
protected readonly _document $document;
|
||||
|
||||
/**
|
||||
* Конструктор
|
||||
*
|
||||
* Инициализация сессии и запись в свойство $this->document
|
||||
*
|
||||
* @param ?string $hash Хеш сессии в базе данных
|
||||
* @param ?int $expires Дата окончания работы сессии (используется при создании новой сессии)
|
||||
* @param array &$errors Реестр ошибок
|
||||
*
|
||||
* @return static Инстанция сессии
|
||||
*/
|
||||
public function __construct(?string $hash = null, ?int $expires = null, array &$errors = [])
|
||||
{
|
||||
try {
|
||||
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(
|
||||
<<<AQL
|
||||
FOR d IN %s
|
||||
FILTER %s && d.hash == '%s' && d.expires > %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(
|
||||
<<<AQL
|
||||
FOR d IN %s
|
||||
FILTER d.hash == '%s' && d.expires > %d && d.status == 'active'
|
||||
RETURN d
|
||||
AQL,
|
||||
self::COLLECTION,
|
||||
$hash,
|
||||
time()
|
||||
))
|
||||
) {
|
||||
// Received session hash and session found
|
||||
|
||||
// Запись в свойство
|
||||
$this->document = $session;
|
||||
} else {
|
||||
// Не найдена сессия
|
||||
|
||||
// Запись сессии в базу данных
|
||||
$_id = document::write($this::$arangodb->session, self::COLLECTION, [
|
||||
'status' => 'active',
|
||||
'expires' => $expires ?? time() + 604800,
|
||||
'ip' => $_SERVER['REMOTE_ADDR'],
|
||||
'x-forwarded-for' => $_SERVER['HTTP_X_FORWARDED_FOR'] ?? null,
|
||||
'referer' => $_SERVER['HTTP_REFERER'] ?? null,
|
||||
'useragent' => $_SERVER['HTTP_USER_AGENT'] ?? null
|
||||
]);
|
||||
|
||||
if ($session = collection::search($this::$arangodb->session, sprintf(
|
||||
<<<AQL
|
||||
FOR d IN %s
|
||||
FILTER d._id == '$_id' && d.expires > %d && d.status == 'active'
|
||||
RETURN d
|
||||
AQL,
|
||||
self::COLLECTION,
|
||||
time()
|
||||
))) {
|
||||
// Найдена только что созданная сессия
|
||||
|
||||
// Запись хеша
|
||||
$session->hash = sodium_bin2hex(sodium_crypto_generichash($_id));
|
||||
|
||||
if (document::update($this::$arangodb->session, $session)) {
|
||||
// Записано обновление
|
||||
|
||||
// Запись в свойство
|
||||
$this->document = $session;
|
||||
} else throw new exception('Не удалось записать данные сессии');
|
||||
} else throw new exception('Не удалось создать или найти созданную сессию');
|
||||
}
|
||||
} else throw new exception('Не удалось инициализировать коллекцию');
|
||||
} catch (exception $e) {
|
||||
// Запись в реестр ошибок
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function search(string $hash, array &$errors = []): bool
|
||||
{
|
||||
try {
|
||||
search_arangodb:
|
||||
|
||||
// Search the session data in ArangoDB
|
||||
$_document = collection::search(static::$arangodb->session, sprintf(
|
||||
<<<AQL
|
||||
FOR d IN %s
|
||||
FILTER d.hash == '$hash' && d.expires > %d && d.status == 'active'
|
||||
RETURN d
|
||||
AQL,
|
||||
self::COLLECTION,
|
||||
time()
|
||||
));
|
||||
|
||||
if ($_document instanceof _document) {
|
||||
// The instance found
|
||||
|
||||
// Write the session data to the property
|
||||
$this->document = $_document;
|
||||
|
||||
// Exit (success)
|
||||
return true;
|
||||
}
|
||||
} catch (exception $e) {
|
||||
// Write to the errors registry
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
// Закрыть сессию
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализировать связь сессии с аккаунтом
|
||||
*
|
||||
* Ищет связь сессии с аккаунтом, если не находит, то создаёт её
|
||||
*
|
||||
* @param account $account Инстанция аккаунта
|
||||
* @param array &$errors Реестр ошибок
|
||||
*
|
||||
* @return bool Связана сессия с аккаунтом?
|
||||
*/
|
||||
public function connect(account $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)
|
||||
) {
|
||||
// Инициализированы коллекции
|
||||
|
||||
if (
|
||||
collection::search($this::$arangodb->session, sprintf(
|
||||
<<<AQL
|
||||
FOR document IN %s
|
||||
FILTER document._from == '%s' && document._to == '%s'
|
||||
LIMIT 1
|
||||
RETURN document
|
||||
AQL,
|
||||
self::COLLECTION . '_edge_' . account::COLLECTION,
|
||||
$this->document->getId(),
|
||||
$account->getId()
|
||||
)) instanceof _document
|
||||
|| document::write($this::$arangodb->session, self::COLLECTION . '_edge_' . account::COLLECTION, [
|
||||
'_from' => $this->document->getId(),
|
||||
'_to' => $account->getId()
|
||||
])
|
||||
) {
|
||||
// Найдено, либо создано ребро: session -> account
|
||||
|
||||
return true;
|
||||
} else throw new exception('Не удалось создать ребро: session -> account');
|
||||
} else throw new exception('Не удалось инициализировать коллекции');
|
||||
} catch (exception $e) {
|
||||
// Запись в реестр ошибок
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Найти связанный аккаунт
|
||||
*
|
||||
* @param array &$errors Реестр ошибок
|
||||
*
|
||||
* @return ?account Инстанция аккаунта, если удалось найти
|
||||
*/
|
||||
public function account(array &$errors = []): ?account
|
||||
{
|
||||
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)
|
||||
) {
|
||||
// Инициализированы коллекции
|
||||
|
||||
// Инициализация инстанции аккаунта
|
||||
$account = new account;
|
||||
|
||||
// Поиск инстанции аккаунта в базе данных
|
||||
$instance = $account->instance(collection::search($this::$arangodb->session, sprintf(
|
||||
<<<AQL
|
||||
FOR d IN %s
|
||||
LET e = (
|
||||
FOR e IN %s
|
||||
FILTER e._from == '%s'
|
||||
SORT e._key DESC
|
||||
LIMIT 1
|
||||
RETURN e
|
||||
)
|
||||
FILTER d._id == e[0]._to
|
||||
SORT d.created DESC
|
||||
LIMIT 1
|
||||
RETURN d
|
||||
AQL,
|
||||
account::COLLECTION,
|
||||
self::COLLECTION . '_edge_' . account::COLLECTION,
|
||||
$this->getId()
|
||||
)));
|
||||
|
||||
// Возврат (успех)
|
||||
return $instance instanceof _document ? $account : 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Записать в буфер сессии
|
||||
*
|
||||
* @param array $data Данные для записи
|
||||
* @param array &$errors Реестр ошибок
|
||||
*
|
||||
* @return bool Записаны данные в буфер сессии?
|
||||
*/
|
||||
public function write(array $data, array &$errors = []): bool
|
||||
{
|
||||
try {
|
||||
if (collection::init($this::$arangodb->session, self::COLLECTION)) {
|
||||
// Инициализирована коллекция
|
||||
|
||||
// Проверка инициализированности инстанции документа из базы данных
|
||||
if (!isset($this->document)) throw new exception('Не инициализирована инстанция документа из базы данных');
|
||||
|
||||
// Запись параметров в инстанцию документа из базы данных
|
||||
$this->document->buffer = array_replace_recursive(
|
||||
$this->document->buffer ?? [],
|
||||
[$_SERVER['INTERFACE'] => array_replace_recursive($this->document->buffer[$_SERVER['INTERFACE']] ?? [], $data)]
|
||||
);
|
||||
|
||||
// Запись в базу данных и возврат (успех)
|
||||
return document::update($this::$arangodb->session, $this->document) ? true : throw new exception('Не удалось записать данные в буфер сессии');
|
||||
} else throw new exception('Не удалось инициализировать коллекцию');
|
||||
} catch (exception $e) {
|
||||
// Запись в реестр ошибок
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Записать
|
||||
*
|
||||
* Записывает свойство в инстанцию документа сессии из базы данных
|
||||
*
|
||||
* @param string $name Название
|
||||
* @param mixed $value Содержимое
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __set(string $name, mixed $value = null): void
|
||||
{
|
||||
$this->document->{$name} = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Прочитать
|
||||
*
|
||||
* Читает свойство из инстанции документа сессии из базы данных
|
||||
*
|
||||
* @param string $name Название
|
||||
*
|
||||
* @return mixed Данные свойства инстанции сессии или инстанции документа сессии из базы данных
|
||||
*/
|
||||
public function __get(string $name): mixed
|
||||
{
|
||||
return match ($name) {
|
||||
'arangodb' => $this::$arangodb,
|
||||
default => $this->document->{$name}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить инициализированность
|
||||
*
|
||||
* Проверяет инициализированность свойства в инстанции документа сессии из базы данных
|
||||
*
|
||||
* @param string $name Название
|
||||
*
|
||||
* @return bool Свойство инициализировано?
|
||||
*/
|
||||
public function __isset(string $name): bool
|
||||
{
|
||||
return isset($this->document->{$name});
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить
|
||||
*
|
||||
* Деинициализировать свойство в инстанции документа сессии из базы данных
|
||||
*
|
||||
* @param string $name Название
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __unset(string $name): void
|
||||
{
|
||||
unset($this->document->{$name});
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполнить метод
|
||||
*
|
||||
* Выполнить метод в инстанции документа сессии из базы данных
|
||||
*
|
||||
* @param string $name Название
|
||||
* @param array $arguments Аргументы
|
||||
*/
|
||||
public function __call(string $name, array $arguments = [])
|
||||
{
|
||||
if (method_exists($this->document, $name)) return $this->document->{$name}($arguments);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\ebala\models;
|
||||
|
||||
// Файлы проекта
|
||||
use mirzaev\ebala\models\traits\status;
|
||||
|
||||
// Фреймворк ArangoDB
|
||||
use mirzaev\arangodb\collection,
|
||||
mirzaev\arangodb\document;
|
||||
|
||||
// Библиотека для ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
// Встроенные библиотеки
|
||||
use exception;
|
||||
|
||||
/**
|
||||
* Модель заданий
|
||||
*
|
||||
* @package mirzaev\ebala\models
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class task extends core
|
||||
{
|
||||
use status;
|
||||
|
||||
/**
|
||||
* Collection name in ArangoDB
|
||||
*/
|
||||
final public const COLLECTION = 'task';
|
||||
|
||||
/**
|
||||
* Инстанция документа в базе данных
|
||||
*/
|
||||
protected readonly _document $document;
|
||||
|
||||
/**
|
||||
* Create task in ArangoDB
|
||||
*
|
||||
* @param ?string $date
|
||||
* @param ?string $worker
|
||||
* @param ?string $work
|
||||
* @param ?string $start
|
||||
* @param ?string $end
|
||||
* @param ?string $market
|
||||
* @param bool $confirmed
|
||||
* @param bool $published
|
||||
* @param bool $hided
|
||||
* @param bool $problematic
|
||||
* @param bool $completed
|
||||
* @param array $errors
|
||||
*
|
||||
* @return ?string Identificator of instance of ArangoDB
|
||||
*/
|
||||
public static function create(
|
||||
?string $date = null,
|
||||
?string $worker = null,
|
||||
?string $work = null,
|
||||
?string $start = null,
|
||||
?string $end = null,
|
||||
?string $market = null,
|
||||
bool $confirmed = false,
|
||||
bool $published = false,
|
||||
bool $hided = false,
|
||||
bool $problematic = false,
|
||||
bool $completed = false,
|
||||
array &$errors = []
|
||||
): ?string {
|
||||
try {
|
||||
if (
|
||||
collection::init(static::$arangodb->session, self::COLLECTION)
|
||||
&& collection::init(static::$arangodb->session, worker::COLLECTION)
|
||||
&& collection::init(static::$arangodb->session, market::COLLECTION)
|
||||
) {
|
||||
// Инициализированы коллекции
|
||||
|
||||
// Запись документа в базу данны и возврат (успех)
|
||||
return document::write(static::$arangodb->session, self::COLLECTION, [
|
||||
'date' => (int) ($date ?? strtotime('+1 day')),
|
||||
'worker' => $worker,
|
||||
'work' => $work,
|
||||
'start' => $start,
|
||||
'end' => $end,
|
||||
'market' => $market,
|
||||
'confirmed' => $confirmed,
|
||||
'published' => $published,
|
||||
'hided' => $hided,
|
||||
'problematic' => $problematic,
|
||||
'completed' => $completed,
|
||||
]);
|
||||
} else throw new exception('Не удалось инициализировать коллекции');
|
||||
} catch (exception $e) {
|
||||
// Write to the errors registry
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read tasks from ArangoDB
|
||||
*
|
||||
* @param ?string $before Injection of AQL-code before search of worker and market
|
||||
* @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 array $errors Errors registry
|
||||
*
|
||||
* @return array Instances from ArangoDB
|
||||
*/
|
||||
public static function list(
|
||||
?string $before = '',
|
||||
?string $after = '',
|
||||
int $amount = 100,
|
||||
int $page = 1,
|
||||
string $sort = 'task.created DESC',
|
||||
array &$errors = []
|
||||
): array {
|
||||
try {
|
||||
if (
|
||||
collection::init(static::$arangodb->session, self::COLLECTION)
|
||||
&& collection::init(static::$arangodb->session, worker::COLLECTION)
|
||||
&& collection::init(static::$arangodb->session, market::COLLECTION)
|
||||
) {
|
||||
// Инициализированы коллекции
|
||||
|
||||
// Search the session data in ArangoDB (LET можно заменить на поиск по рёбрам но тут и так сойдёт)
|
||||
$tasks = collection::search(static::$arangodb->session, sprintf(
|
||||
<<<AQL
|
||||
FOR task IN %s
|
||||
FILTER task.status != 'deleted'
|
||||
%s
|
||||
LET worker = (FOR worker in %s FILTER worker.id LIKE task.worker SORT worker.created DESC LIMIT 1 RETURN worker)[0]
|
||||
LET market = (FOR market in %s FILTER market.id LIKE task.market SORT market.created DESC LIMIT 1 RETURN market)[0]
|
||||
%s
|
||||
SORT %s
|
||||
LIMIT %d, %d
|
||||
RETURN {task, worker, market}
|
||||
AQL,
|
||||
self::COLLECTION,
|
||||
$before,
|
||||
'worker',
|
||||
'markets',
|
||||
$after,
|
||||
$sort,
|
||||
--$page <= 0 ? 0 : $amount * $page,
|
||||
$amount
|
||||
));
|
||||
|
||||
// Exit (success)
|
||||
return empty($tasks) ? [] : (is_array($tasks) ? $tasks : [$tasks]);
|
||||
} else throw new exception('Не удалось инициализировать коллекции');
|
||||
} catch (exception $e) {
|
||||
// Write to the errors registry
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\ebala\models\traits;
|
||||
|
||||
// Library of ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
/**
|
||||
* Trait with instance of document in database handler
|
||||
*
|
||||
* @package mirzaev\ebala\models\treits
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
trait instance
|
||||
{
|
||||
/**
|
||||
* Инициализация инстанции документа в базе данных
|
||||
*
|
||||
* @param ?_document $document Инстанция документа в базе данных для записи
|
||||
*
|
||||
* @return ?_document Инстанция документа в базе данных, если инициализирована
|
||||
*/
|
||||
public function instance(?_document $document = null): ?_document
|
||||
{
|
||||
// Проверка инициализированности и возврат (успех)
|
||||
if (isset($this->document)) return $this->document;
|
||||
|
||||
// Проверка инстанции документа в базе данных для записи и возврат (провал)
|
||||
if ($document === null) return null;
|
||||
|
||||
// Запись в свойство и возврат (успех)
|
||||
return $this->document = $document;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\ebala\models\traits;
|
||||
|
||||
use exception;
|
||||
|
||||
/**
|
||||
* Заготовка для инициализации статуса
|
||||
*
|
||||
* @package mirzaev\ebala\controllers\traits
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
trait status
|
||||
{
|
||||
/**
|
||||
* Инициализировать статус
|
||||
*
|
||||
* @param array &$errors Реестр ошибок
|
||||
*
|
||||
* @return ?bool Статус (null - не найден)
|
||||
*/
|
||||
public function status(array &$errors = []): ?bool
|
||||
{
|
||||
try {
|
||||
return match ($this->document->status ?? null) {
|
||||
'active' => true,
|
||||
'inactive' => false,
|
||||
default => null
|
||||
};
|
||||
} catch (exception $e) {
|
||||
// Запись в реестр ошибок
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\ebala\models;
|
||||
|
||||
// Project files
|
||||
use mirzaev\ebala\models\traits\instance,
|
||||
mirzaev\ebala\models\traits\status;
|
||||
|
||||
// Фреймворк ArangoDB
|
||||
use mirzaev\arangodb\collection;
|
||||
|
||||
// Библиотека для ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
// Встроенные библиотеки
|
||||
use exception;
|
||||
|
||||
/**
|
||||
* Модель сотрудника
|
||||
*
|
||||
* @package mirzaev\ebala\models
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class worker extends core
|
||||
{
|
||||
use instance, status;
|
||||
|
||||
/**
|
||||
* Коллекция
|
||||
*/
|
||||
final public const COLLECTION = 'worker';
|
||||
|
||||
/**
|
||||
* Инстанция документа в базе данных
|
||||
*/
|
||||
protected readonly _document $document;
|
||||
|
||||
/**
|
||||
* Конструктор
|
||||
*
|
||||
* @param _document $instance Инстанция в базе данных
|
||||
* @param array &$errors Реестр ошибок
|
||||
*
|
||||
* @return static Инстанция магазина
|
||||
*/
|
||||
public function __construct(_document $instance, array &$errors = [])
|
||||
{
|
||||
try {
|
||||
// Инициализация инстанции документа магазина в базе данных
|
||||
$this->document = $instance;
|
||||
} catch (exception $e) {
|
||||
// Запись в реестр ошибок
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read workers from ArangoDB
|
||||
*
|
||||
* @param ?string $before Injection of AQL-code before search of worker and market
|
||||
* @param int $amount Amount of workers
|
||||
* @param int $page Offset by amount
|
||||
* @param array $errors Errors registry
|
||||
*
|
||||
* @return array Instances from ArangoDB
|
||||
*/
|
||||
public static function list(
|
||||
?string $before = '',
|
||||
int $amount = 100,
|
||||
int $page = 1,
|
||||
string $sort = 'worker.created DESC',
|
||||
array &$errors = []
|
||||
): array {
|
||||
try {
|
||||
if (collection::init(static::$arangodb->session, self::COLLECTION)) {
|
||||
// Инициализирована коллекция
|
||||
|
||||
// Search the session data in ArangoDB
|
||||
$workers = collection::search(static::$arangodb->session, sprintf(
|
||||
<<<AQL
|
||||
FOR worker IN %s
|
||||
FILTER worker.status != 'deleted'
|
||||
%s
|
||||
SORT %s
|
||||
LIMIT %d, %d
|
||||
RETURN {worker}
|
||||
AQL,
|
||||
self::COLLECTION,
|
||||
$before,
|
||||
$sort,
|
||||
--$page <= 0 ? 0 : $amount * $page,
|
||||
$amount
|
||||
));
|
||||
|
||||
// Exit (success)
|
||||
return empty($workers) ? [] : (is_array($workers) ? $workers : [$workers]);
|
||||
} else throw new exception('Не удалось инициализировать коллекции');
|
||||
} catch (exception $e) {
|
||||
// Write to the errors registry
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Записать
|
||||
*
|
||||
* Записывает свойство в инстанцию документа аккаунта из базы данных
|
||||
*
|
||||
* @param string $name Название
|
||||
* @param mixed $value Содержимое
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __set(string $name, mixed $value = null): void
|
||||
{
|
||||
$this->document->{$name} = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Прочитать
|
||||
*
|
||||
* Читает свойство из инстанции документа аккаунта из базы данных
|
||||
*
|
||||
* @param string $name Название
|
||||
*
|
||||
* @return mixed Данные свойства инстанции аккаунта или инстанции документа аккаунта из базы данных
|
||||
*/
|
||||
public function __get(string $name): mixed
|
||||
{
|
||||
return $this->document->{$name};
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить инициализированность
|
||||
*
|
||||
* Проверяет инициализированность свойства в инстанции документа аккаунта из базы данных
|
||||
*
|
||||
* @param string $name Название
|
||||
*
|
||||
* @return bool Свойство инициализировано?
|
||||
*/
|
||||
public function __isset(string $name): bool
|
||||
{
|
||||
return isset($this->document->{$name});
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить
|
||||
*
|
||||
* Деинициализировать свойство в инстанции документа аккаунта из базы данных
|
||||
*
|
||||
* @param string $name Название
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __unset(string $name): void
|
||||
{
|
||||
unset($this->document->{$name});
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполнить метод
|
||||
*
|
||||
* Выполнить метод в инстанции документа аккаунта из базы данных
|
||||
*
|
||||
* @param string $name Название
|
||||
* @param array $arguments Аргументы
|
||||
*/
|
||||
public function __call(string $name, array $arguments = [])
|
||||
{
|
||||
if (method_exists($this->document, $name)) return $this->document->{$name}($arguments);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
main {
|
||||
z-index: 1000;
|
||||
margin: 20vh auto;
|
||||
position: relative;
|
||||
height: unset;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: unset;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
section.panel {
|
||||
--display: flex;
|
||||
z-index: 1000;
|
||||
width: 400px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
div.column > section.panel {
|
||||
position: unset;
|
||||
}
|
||||
|
||||
section.panel.medium {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
section.panel.small {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
section.panel#mnemonic {
|
||||
margin-left: -570px;
|
||||
}
|
||||
|
||||
section.panel#classic {
|
||||
margin-left: 570px;
|
||||
}
|
||||
|
||||
section.panel > section.body > ul {
|
||||
margin: 0 5%;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
list-style: square;
|
||||
}
|
||||
|
||||
section.panel > section.body > ul > li {
|
||||
font-size: 0.8rem;
|
||||
word-break: break-word;
|
||||
animation-duration: 0.35s;
|
||||
animation-name: uprise;
|
||||
animation-fill-mode: forwards;
|
||||
animation-timing-function: cubic-bezier(0.47, 0, 0.74, 0.71);
|
||||
}
|
||||
|
||||
section.panel > section.body > dl {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
section.panel > section.body > dl > * {
|
||||
word-break: break-word;
|
||||
animation-duration: 0.35s;
|
||||
animation-name: uprise;
|
||||
animation-fill-mode: forwards;
|
||||
animation-timing-function: cubic-bezier(0.47, 0, 0.74, 0.71);
|
||||
}
|
||||
|
||||
section.panel > section.body > dl > dt {
|
||||
margin-left: 20px;
|
||||
display: none;
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
section.panel > section.body > dl > dd {
|
||||
margin-left: unset;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
section.panel > section.header {
|
||||
z-index: 1000;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
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) {
|
||||
margin-bottom: unset;
|
||||
}
|
||||
|
||||
section.panel > section.body {
|
||||
padding: 20px 30px;
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 0 0 3px 3px;
|
||||
background-color: var(--background-above);
|
||||
}
|
||||
|
||||
section.panel > section.postscript {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
section#entry.panel > section.body > form > label > button {
|
||||
height: 100%;
|
||||
padding: 0 10px;
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
@keyframes input-error {
|
||||
0%,
|
||||
20% {
|
||||
background-color: var(--input-error);
|
||||
}
|
||||
|
||||
50%,
|
||||
100% {
|
||||
background-color: var(--background-above-1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes icon-error {
|
||||
0%,
|
||||
50% {
|
||||
color: var(--icon-error);
|
||||
}
|
||||
|
||||
80%,
|
||||
100% {
|
||||
color: var(--text-inverse-below-1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes uprise {
|
||||
0% {
|
||||
opacity: 0;
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
filter: blur(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes window-vertical-open {
|
||||
0% {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
height: var(--height, inherit);
|
||||
opacity: var(--opacity, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes window-vertical-close {
|
||||
0% {
|
||||
height: var(--height, inherit);
|
||||
opacity: var(--opacity, 1);
|
||||
}
|
||||
|
||||
100% {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes list-triangle-left {
|
||||
0% {
|
||||
left: -18px;
|
||||
}
|
||||
|
||||
40%,
|
||||
100% {
|
||||
left: -30px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes list-triangle-right {
|
||||
0% {
|
||||
right: -18px;
|
||||
}
|
||||
|
||||
40%,
|
||||
100% {
|
||||
right: -30px;
|
||||
}
|
||||
}
|
||||
|
||||
.animation.window:not(.hidden, .horizontal) {
|
||||
overflow: hidden;
|
||||
animation-duration: 0.1s;
|
||||
animation-name: window-vertical-open;
|
||||
animation-fill-mode: forwards;
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
.animation.window.hidden:not(.horizontal) {
|
||||
overflow: hidden;
|
||||
animation-duration: 0.05s;
|
||||
animation-name: window-vertical-close;
|
||||
animation-fill-mode: forwards;
|
||||
animation-timing-function: ease-out;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
i.icon.arrow.right {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 22px;
|
||||
--height: 22px;
|
||||
height: var(--height);
|
||||
}
|
||||
|
||||
i.icon.arrow.right::after,
|
||||
i.icon.arrow.right::before {
|
||||
content: "";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
right: 3px;
|
||||
}
|
||||
|
||||
i.icon.arrow.right::after {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-top: 2px solid;
|
||||
border-right: 2px solid;
|
||||
transform: rotate(45deg);
|
||||
bottom: 7px;
|
||||
}
|
||||
|
||||
i.icon.arrow.right::before {
|
||||
width: 16px;
|
||||
height: 2px;
|
||||
bottom: 10px;
|
||||
background: currentColor;
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
i.icon.home {
|
||||
position: relative;
|
||||
margin-bottom: -2px;
|
||||
--width: 18px;
|
||||
--height: 14px;
|
||||
width: 18px;
|
||||
height: 14px;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: 2px solid;
|
||||
border-top: 0;
|
||||
border-bottom: 0;
|
||||
border-top-right-radius: 3px;
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
background: linear-gradient(to left, currentColor 5px, transparent 0)
|
||||
no-repeat 0 bottom/4px 2px,
|
||||
linear-gradient(to left, currentColor 5px, transparent 0) no-repeat right
|
||||
bottom/4px 2px;
|
||||
}
|
||||
i.icon.home::after,
|
||||
i.icon.home::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
i.icon.home::before {
|
||||
left: 0;
|
||||
top: -5px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 3px;
|
||||
transform: rotate(45deg);
|
||||
border-top: 2px solid;
|
||||
border-left: 2px solid;
|
||||
border-top-left-radius: 4px;
|
||||
}
|
||||
i.icon.home::after {
|
||||
left: 3px;
|
||||
bottom: 0;
|
||||
width: 8px;
|
||||
height: 10px;
|
||||
border: 2px solid;
|
||||
border-radius: 100px;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom: 0;
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
i.icon.keyhole,
|
||||
i.icon.keyhole::after,
|
||||
i.icon.keyhole::before {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
i.icon.keyhole {
|
||||
--width: 20px;
|
||||
--height: 20px;
|
||||
position: relative;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
i.icon.keyhole::after,
|
||||
i.icon.keyhole::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
}
|
||||
|
||||
i.icon.keyhole::before {
|
||||
left: 5px;
|
||||
top: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
i.icon.keyhole::after {
|
||||
left: 7px;
|
||||
bottom: 3px;
|
||||
width: 2px;
|
||||
height: 5px;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
label > i.icon.keyhole:first-child {
|
||||
left: 7px;
|
||||
scale: 0.9;
|
||||
border: 2.1px solid;
|
||||
}
|
||||
|
||||
i.icon.keyhole + input {
|
||||
padding-left: 34px;
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
i.icon.lock {
|
||||
--width: 12px;
|
||||
--height: 11px;
|
||||
position: relative;
|
||||
margin-top: -12px;
|
||||
width: 12px;
|
||||
height: 11px;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: 2px solid;
|
||||
border-top-right-radius: 50%;
|
||||
border-top-left-radius: 50%;
|
||||
border-bottom: transparent;
|
||||
}
|
||||
|
||||
i.icon.lock::after {
|
||||
left: -4px;
|
||||
top: 9px;
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 10px;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
content: "";
|
||||
box-shadow: 0 0 0 2px;
|
||||
border-radius: 2px;
|
||||
border: 2px solid transparent;
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
i.icon.mail,
|
||||
i.icon.mail::after {
|
||||
--width: 18px;
|
||||
--height: 14px;
|
||||
height: 14px;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
i.icon.mail {
|
||||
position: relative;
|
||||
width: 18px;
|
||||
overflow: hidden;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
i.icon.mail::after {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 3px;
|
||||
width: 14px;
|
||||
transform: rotate(-45deg);
|
||||
content: "";
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
i.icon.mail + input {
|
||||
padding-left: 36px;
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
i.icon.nametag {
|
||||
position: relative;
|
||||
--width: 6px;
|
||||
--height: 6px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
i.icon.nametag::before {
|
||||
left: -5px;
|
||||
top: -5px;
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
content: "";
|
||||
box-shadow: -5px -5px 0 -3px, 5px 5px 0 -3px, 5px -5px 0 -3px, -5px 5px 0 -3px;
|
||||
}
|
||||
|
||||
label > i.icon.nametag:first-of-type {
|
||||
left: 13px;
|
||||
}
|
||||
|
||||
i.icon.nametag + input {
|
||||
padding-left: 32px;
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
i.icon.search {
|
||||
--width: 16px;
|
||||
--height: 16px;
|
||||
margin-left: -4px;
|
||||
margin-top: -4px;
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
transform: scale(var(--ggs, 1));
|
||||
border-radius: 100%;
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
i.icon.search::after {
|
||||
content: "";
|
||||
left: 12px;
|
||||
top: 10px;
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 8px;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border-radius: 3px;
|
||||
transform: rotate(-45deg);
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
label > i.icon.search:first-of-type {
|
||||
left: 18px;
|
||||
top: 12px;
|
||||
}
|
||||
|
||||
i.icon.search + input {
|
||||
padding-left: 50px !important;
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
i.icon.shopping.cart {
|
||||
position: relative;
|
||||
--width: 20px;
|
||||
--height: 21px;
|
||||
width: 20px;
|
||||
height: 21px;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(to left, currentColor 12px, transparent 0)
|
||||
no-repeat -1px 6px/18px 2px,
|
||||
linear-gradient(to left, currentColor 12px, transparent 0) no-repeat 6px
|
||||
14px/11px 2px,
|
||||
linear-gradient(to left, currentColor 12px, transparent 0) no-repeat 0 2px/4px
|
||||
2px,
|
||||
radial-gradient(circle, currentColor 60%, transparent 40%) no-repeat 12px
|
||||
17px/4px 4px,
|
||||
radial-gradient(circle, currentColor 60%, transparent 40%) no-repeat 6px
|
||||
17px/4px 4px;
|
||||
}
|
||||
i.icon.shopping.cart::after,
|
||||
i.icon.shopping.cart::before {
|
||||
left: 4px;
|
||||
top: 2px;
|
||||
position: absolute;
|
||||
content: "";
|
||||
width: 2px;
|
||||
height: 14px;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
transform: skew(12deg);
|
||||
background: currentColor;
|
||||
}
|
||||
i.icon.shopping.cart::after {
|
||||
left: 16px;
|
||||
top: 6px;
|
||||
height: 10px;
|
||||
transform: skew(-12deg);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
i.icon.smartphone {
|
||||
position: relative;
|
||||
--width: 14px;
|
||||
--height: 20px;
|
||||
width: 14px;
|
||||
height: 20px;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
transform: scale(var(--ggs, 1));
|
||||
border: 2px solid;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(to left, currentColor 5px, transparent 0)
|
||||
no-repeat 4px 12px/2px 2px;
|
||||
}
|
||||
|
||||
label > i.icon.nametag:first-child {
|
||||
left: 5px;
|
||||
}
|
||||
|
||||
i.icon.nametag + input {
|
||||
padding-left: 32px;
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
i.timer,
|
||||
i.timer::before {
|
||||
--width: 18px;
|
||||
--height: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 40px;
|
||||
border: 2px solid;
|
||||
}
|
||||
i.timer {
|
||||
position: relative;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border-top-color: transparent;
|
||||
background: linear-gradient(to left, currentColor 10px, transparent 0)
|
||||
no-repeat 6px -2px/2px 6px;
|
||||
}
|
||||
i.timer::after,
|
||||
i.timer::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
i.timer::before {
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
transform: rotate(45deg);
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
i.timer::after {
|
||||
left: 4px;
|
||||
bottom: 5px;
|
||||
width: 2px;
|
||||
height: 6px;
|
||||
border-radius: 100px;
|
||||
transform: rotate(-50deg);
|
||||
background: currentColor;
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
i.icon.user {
|
||||
--width: 12px;
|
||||
--height: 18px;
|
||||
width: 12px;
|
||||
height: 18px;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
i.icon.user::before,
|
||||
i.icon.user::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
i.icon.user.bold::before,
|
||||
i.icon.user.bold::after {
|
||||
border: 3px solid;
|
||||
}
|
||||
|
||||
i.icon.user::before {
|
||||
left: 2px;
|
||||
top: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
i.icon.user.bold::before {
|
||||
left: 2px;
|
||||
top: -2px;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
}
|
||||
|
||||
i.icon.user::after {
|
||||
top: 9px;
|
||||
width: 12px;
|
||||
height: 9px;
|
||||
border-bottom: 0;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
|
||||
i.icon.user.bold::after {
|
||||
top: 9px;
|
||||
width: 13px;
|
||||
height: 10px;
|
||||
border-bottom: 0;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
i.icon.user.add {
|
||||
--width: 20px;
|
||||
--height: 18px;
|
||||
width: 20px;
|
||||
height: 18px;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(to left, currentColor 8px, transparent 0)
|
||||
no-repeat 14px 6px/6px 2px,
|
||||
linear-gradient(to left, currentColor 8px, transparent 0) no-repeat 16px 4px/2px
|
||||
6px;
|
||||
}
|
||||
|
||||
i.icon.user.add::after,
|
||||
i.icon.user.add::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
i.icon.user.add::before {
|
||||
left: 2px;
|
||||
top: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
i.icon.user.add::after {
|
||||
top: 9px;
|
||||
width: 12px;
|
||||
height: 9px;
|
||||
border-bottom: 0;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
|
||||
label > i.icon.user.add:first-child {
|
||||
left: 9px;
|
||||
}
|
||||
|
||||
i.icon.user.add + input {
|
||||
padding-left: 37px;
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
i.icon.work.alt,
|
||||
i.icon.work.alt::after {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 0 0 2px;
|
||||
}
|
||||
i.icon.work.alt {
|
||||
position: relative;
|
||||
--width: 14px;
|
||||
--height: 10px;
|
||||
width: 14px;
|
||||
height: 10px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
i.icon.work.alt::after {
|
||||
left: 4px;
|
||||
top: -3px;
|
||||
position: absolute;
|
||||
content: "";
|
||||
width: 6px;
|
||||
height: 1px;
|
||||
border-top-left-radius: 1px;
|
||||
border-top-right-radius: 1px;
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
main {
|
||||
z-index: 1000;
|
||||
margin-top: 15vh;
|
||||
position: relative;
|
||||
height: unset;
|
||||
display: flex;
|
||||
flex-direction: unset;
|
||||
justify-content: center;
|
||||
align-items: unset;
|
||||
}
|
||||
|
||||
section.panel.list {
|
||||
min-width: 800px;
|
||||
}
|
||||
|
||||
section.panel.list.medium {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
section.panel.list > form.row.menu {
|
||||
margin-bottom: 10px;
|
||||
transition: 0s;
|
||||
}
|
||||
|
||||
section.panel.list > form.row.menu > label {
|
||||
height: max-content;
|
||||
min-height: 30px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
section.panel.list > form.row.menu > label:not(.solid) {
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
section.panel.list > form.row.menu.wide > label {
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
section.panel.list > form.row.menu:is(.separated, :last-of-type) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
section.panel.list > form.row.menu > label > button {
|
||||
height: 30px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
section.panel.list > form.row.menu.stretched > label > button,
|
||||
section.panel.list > form.row.menu.stretched > label > input[type="search"] {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
section.panel.list > form.row.menu > label > input {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
section.panel.list > form.row.menu > label > input:not(.merged) {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
section.panel.list > form.row.menu > label > input[type="date"] {
|
||||
width: 115px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
section.panel.list > form.row.menu > label > input[type="search"] + button {
|
||||
height: 100%;
|
||||
padding: 0 30px;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
section.panel.list > div#title {
|
||||
height: 50px;
|
||||
background-color: var(--background-below-6);
|
||||
}
|
||||
|
||||
section.panel.list > div#title > span {
|
||||
font-weight: unset;
|
||||
font-size: unset;
|
||||
color: unset;
|
||||
}
|
||||
|
||||
section.panel.list > div.row {
|
||||
position: relative;
|
||||
height: 35px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 0 12px;
|
||||
background-color: var(--background-above-1);
|
||||
}
|
||||
|
||||
section.panel.list > div.row:first-of-type {
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
section.panel.list > div.row:last-of-type {
|
||||
border-radius: 0 0 3px 3px;
|
||||
}
|
||||
|
||||
section.panel.list > div.row:hover * {
|
||||
transition: unset;
|
||||
}
|
||||
|
||||
section.panel.list > div.row:nth-of-type(2n + 1) {
|
||||
background-color: var(--background-above-2);
|
||||
}
|
||||
|
||||
section.panel.list > div.row[data-selected="true"]::before {
|
||||
left: -25px;
|
||||
top: 0.08rem;
|
||||
position: absolute;
|
||||
align-self: center;
|
||||
content: "";
|
||||
display: inline list-item;
|
||||
font-size: 1.5rem;
|
||||
list-style-type: disclosure-closed;
|
||||
list-style-position: inside;
|
||||
animation-duration: 0.8s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: alternate-reverse;
|
||||
animation-name: list-triangle-left;
|
||||
animation-fill-mode: forwards;
|
||||
animation-timing-function: ease-out;
|
||||
color: var(--interface-brown);
|
||||
}
|
||||
|
||||
section.panel.list > div.row[data-selected="true"]::after {
|
||||
right: -25px;
|
||||
bottom: 0.08rem;
|
||||
rotate: 180deg;
|
||||
position: absolute;
|
||||
align-self: center;
|
||||
content: "";
|
||||
display: inline list-item;
|
||||
font-size: 1.5rem;
|
||||
list-style-type: disclosure-closed;
|
||||
list-style-position: inside;
|
||||
animation-duration: 0.8s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: alternate-reverse;
|
||||
animation-name: list-triangle-right;
|
||||
animation-fill-mode: forwards;
|
||||
animation-timing-function: ease-out;
|
||||
color: var(--interface-brown);
|
||||
}
|
||||
|
||||
section.panel.list > div.row.confirmed {
|
||||
background-color: var(--grass-background-above-2);
|
||||
}
|
||||
|
||||
section.panel.list > div.row.confirmed:nth-of-type(2n + 1) {
|
||||
background-color: var(--grass-background-above-1);
|
||||
}
|
||||
|
||||
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.hided:hover {
|
||||
box-shadow: unset;
|
||||
-webkit-box-shadow: unset;
|
||||
-moz-box-shadow: unset;
|
||||
} */
|
||||
|
||||
section.panel.list > div.row.hided * {
|
||||
filter: blur(1px);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
section.panel.list > div.row.hided:hover * {
|
||||
filter: unset;
|
||||
opacity: unset;
|
||||
}
|
||||
|
||||
section.panel.list > div.row > span {
|
||||
position: relative;
|
||||
margin: auto 0;
|
||||
padding: 8px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
section.panel.list > div.row:nth-of-type(1) > span {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
section.panel.list > div.row:nth-of-type(1) > span > i {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
section.panel.list > div.row > span[onclick] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
section.panel.list > div.row > span.field {
|
||||
cursor: text;
|
||||
}
|
|
@ -0,0 +1,739 @@
|
|||
/*@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;
|
||||
|
||||
/* --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;
|
||||
}
|
||||
/*}*/
|
||||
|
||||
::selection {
|
||||
color: var(--clay-text-above);
|
||||
background-color: var(--clay-background-above);
|
||||
}
|
||||
|
||||
:root {
|
||||
--link: #3c76ff;
|
||||
--link-hover: #6594ff;
|
||||
--link-active: #3064dd;
|
||||
}
|
||||
|
||||
.unselectable {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.hidden:not(.animation) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
* {
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
font-family: Fira, sans-serif;
|
||||
transition: 0.1s ease-out;
|
||||
}
|
||||
|
||||
pre,
|
||||
code {
|
||||
font-family: Hack, monospace;
|
||||
}
|
||||
|
||||
button,
|
||||
input[type="submit"],
|
||||
input[type="range"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
margin: unset;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-track,
|
||||
input[type="range"]::-webkit-slider-runnable-track {
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-progress {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
input[type="range"].sand::-moz-range-progress {
|
||||
background-color: var(--sand-text);
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb,
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: grab;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
input[type="range"].sand::-moz-range-thumb,
|
||||
input[type="range"].sand::-webkit-slider-thumb {
|
||||
border: 2px solid var(--sand-text-below);
|
||||
background-color: var(--sand-text-above);
|
||||
}
|
||||
|
||||
input[type="range"]:active::-moz-range-thumb,
|
||||
input[type="range"]:active::-webkit-slider-thumb {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
input[type="range"] + datalist > option {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
div.range {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
div.range.small {
|
||||
width: 140px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
div.range.small > input[type="range"] {
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
div.range.small > input[type="range"] + i.value {
|
||||
position: absolute;
|
||||
left: var(--left, 0px);
|
||||
margin-left: -10.5px;
|
||||
width: 21px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
pointer-events: none;
|
||||
transition: 0s;
|
||||
}
|
||||
|
||||
div.range.small > input[type="range"].sand + i.value {
|
||||
color: var(--sand-text-below-1);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
:is(input, button).merged.right {
|
||||
margin-right: 0;
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
|
||||
:is(input, button).merged.left {
|
||||
margin-left: 0;
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
button:is(.transparent, .transparent:hover, .transparent:active) {
|
||||
background: unset;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--link-hover);
|
||||
}
|
||||
|
||||
a:active {
|
||||
color: var(--link-active);
|
||||
transition: unset;
|
||||
}
|
||||
|
||||
label {
|
||||
position: relative;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
label > i:first-of-type {
|
||||
left: 8px;
|
||||
top: calc((26px - var(--height)) / 2);
|
||||
position: absolute !important;
|
||||
margin: auto;
|
||||
color: var(--text-inverse-below-1);
|
||||
}
|
||||
|
||||
label * {
|
||||
/* color: var(--text-inverse); */
|
||||
}
|
||||
|
||||
*[disabled],
|
||||
*[readonly="true"] {
|
||||
cursor: not-allowed;
|
||||
/* filter: contrast(2) grayscale(0.8); */
|
||||
filter: contrast(0.6) grayscale(0.7);
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 3px 13px;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
background-color: var(--background-below);
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 396px;
|
||||
min-width: calc(100% - 28px);
|
||||
min-height: 120px;
|
||||
max-width: calc(100% - 28px);
|
||||
max-height: 300px;
|
||||
padding: 8px 14px;
|
||||
font-size: smaller;
|
||||
overflow: hidden;
|
||||
border-radius: 3px;
|
||||
transition: 0s;
|
||||
}
|
||||
|
||||
input {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
input.small {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
input.medium {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
input.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;
|
||||
}
|
||||
|
||||
i.icon {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
i.icon + input {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
i.icon.error {
|
||||
animation-duration: 3s;
|
||||
animation-name: icon-error;
|
||||
animation-fill-mode: forwards;
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
:is(input, textarea).error {
|
||||
animation-duration: 1s;
|
||||
animation-name: input-error;
|
||||
animation-fill-mode: forwards;
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
section.header > h1 {
|
||||
font-size: 1.3rem;
|
||||
line-height: 1.3rem;
|
||||
}
|
||||
|
||||
section.header > :is(h2, h3) {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.1rem;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
body > div.background {
|
||||
z-index: -50000;
|
||||
left: -350%;
|
||||
top: -350%;
|
||||
width: 500%;
|
||||
height: 500%;
|
||||
position: absolute;
|
||||
filter: blur(200px);
|
||||
animation-duration: 15s;
|
||||
animation-name: page-background-gradient;
|
||||
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%
|
||||
);
|
||||
}
|
||||
|
||||
aside {
|
||||
z-index: 500;
|
||||
grid-column: 1/ 4;
|
||||
grid-row: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
z-index: 5000;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
main {
|
||||
height: unset;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: unset;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transition: 0s;
|
||||
}
|
||||
|
||||
footer {
|
||||
z-index: 3000;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.stretched {
|
||||
width: 100%;
|
||||
flex-grow: 10;
|
||||
}
|
||||
|
||||
button {
|
||||
height: auto;
|
||||
padding: 0 20px;
|
||||
font-weight: bold;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
button.grass {
|
||||
color: var(--grass-text);
|
||||
background-color: var(--grass-background);
|
||||
}
|
||||
|
||||
button.grass * {
|
||||
color: var(--grass-text);
|
||||
}
|
||||
|
||||
button.grass:hover {
|
||||
color: var(--grass-text-above);
|
||||
background-color: var(--grass-background-above);
|
||||
}
|
||||
|
||||
button.grass:hover * {
|
||||
color: var(--grass-text-above);
|
||||
}
|
||||
|
||||
button.grass:active {
|
||||
color: var(--grass-text-below);
|
||||
background-color: var(--grass-background-below);
|
||||
transition: unset;
|
||||
}
|
||||
|
||||
button.grass:active * {
|
||||
color: var(--grass-text-below);
|
||||
transition: unset;
|
||||
}
|
||||
|
||||
button.blue {
|
||||
color: var(--blue-text);
|
||||
background-color: var(--blue-background);
|
||||
}
|
||||
|
||||
button.blue * {
|
||||
color: var(--blue-text);
|
||||
}
|
||||
|
||||
button.blue:hover {
|
||||
color: var(--blue-text-above);
|
||||
background-color: var(--blue-background-above);
|
||||
}
|
||||
|
||||
button.blue:hover * {
|
||||
color: var(--blue-text-above);
|
||||
}
|
||||
|
||||
button.blue:active {
|
||||
color: var(--blue-text-below);
|
||||
background-color: var(--blue-background-below);
|
||||
transition: unset;
|
||||
}
|
||||
|
||||
button.blue:active * {
|
||||
color: var(--blue-text-below);
|
||||
transition: unset;
|
||||
}
|
||||
|
||||
.clay {
|
||||
color: var(--clay-text);
|
||||
background-color: var(--clay-background);
|
||||
}
|
||||
|
||||
.clay * {
|
||||
color: var(--clay-text);
|
||||
}
|
||||
|
||||
button.clay:hover {
|
||||
color: var(--clay-text-above);
|
||||
background-color: var(--clay-background-above);
|
||||
}
|
||||
|
||||
button.clay:hover * {
|
||||
color: var(--clay-text-above);
|
||||
}
|
||||
|
||||
button.clay:active {
|
||||
color: var(--clay-text-below);
|
||||
background-color: var(--clay-background-below);
|
||||
transition: unset;
|
||||
}
|
||||
|
||||
button.clay:active * {
|
||||
color: var(--clay-text-below);
|
||||
transition: unset;
|
||||
}
|
||||
|
||||
.earth {
|
||||
color: var(--earth-text);
|
||||
background-color: var(--earth-background);
|
||||
}
|
||||
|
||||
.earth * {
|
||||
color: var(--earth-text);
|
||||
}
|
||||
|
||||
button.earth:hover {
|
||||
color: var(--earth-text-above);
|
||||
background-color: var(--earth-background-above);
|
||||
}
|
||||
|
||||
button.earth:hover * {
|
||||
color: var(--earth-text-above);
|
||||
}
|
||||
|
||||
button.earth:active {
|
||||
color: var(--earth-text-below);
|
||||
background-color: var(--earth-background-below);
|
||||
transition: unset;
|
||||
}
|
||||
|
||||
button.earth:active * {
|
||||
color: var(--earth-text-below);
|
||||
transition: unset;
|
||||
}
|
||||
|
||||
.sand {
|
||||
color: var(--sand-text);
|
||||
background-color: var(--sand-background);
|
||||
}
|
||||
|
||||
.sand * {
|
||||
color: var(--sand-text);
|
||||
}
|
||||
|
||||
button.sand:hover {
|
||||
color: var(--sand-text-above);
|
||||
background-color: var(--sand-background-above);
|
||||
}
|
||||
|
||||
button.sand:hover * {
|
||||
color: var(--sand-text-above);
|
||||
}
|
||||
|
||||
button.sand:active {
|
||||
color: var(--sand-text-below);
|
||||
background-color: var(--sand-background-below);
|
||||
transition: unset;
|
||||
}
|
||||
|
||||
button.sand:active * {
|
||||
color: var(--sand-text-below);
|
||||
transition: unset;
|
||||
}
|
||||
|
||||
.river {
|
||||
color: var(--river-text);
|
||||
background-color: var(--river-background);
|
||||
}
|
||||
|
||||
.river * {
|
||||
color: var(--river-text);
|
||||
}
|
||||
|
||||
button.river:hover {
|
||||
color: var(--river-text-above);
|
||||
background-color: var(--river-background-above);
|
||||
}
|
||||
|
||||
button.river:hover * {
|
||||
color: var(--river-text-above);
|
||||
}
|
||||
|
||||
button.river:active {
|
||||
color: var(--river-text-below);
|
||||
background-color: var(--river-background-below);
|
||||
transition: unset;
|
||||
}
|
||||
|
||||
button.river:active * {
|
||||
color: var(--river-text-below);
|
||||
transition: unset;
|
||||
}
|
||||
|
||||
.sky {
|
||||
color: var(--sky-text);
|
||||
background-color: var(--sky-background);
|
||||
}
|
||||
|
||||
.sky * {
|
||||
color: var(--sky-text);
|
||||
}
|
||||
|
||||
button.sky:hover {
|
||||
color: var(--sky-text-above);
|
||||
background-color: var(--sky-background-above);
|
||||
}
|
||||
|
||||
button.sky:hover * {
|
||||
color: var(--sky-text-above);
|
||||
}
|
||||
|
||||
button.sky:active {
|
||||
color: var(--sky-text-below);
|
||||
background-color: var(--sky-background-below);
|
||||
transition: unset;
|
||||
}
|
||||
|
||||
button.sky:active * {
|
||||
color: var(--sky-text-below);
|
||||
transition: unset;
|
||||
}
|
||||
|
||||
.sea {
|
||||
color: var(--sea-text);
|
||||
background-color: var(--sea-background);
|
||||
}
|
||||
|
||||
.sea * {
|
||||
color: var(--sea-text);
|
||||
}
|
||||
|
||||
button.sea:hover {
|
||||
color: var(--sea-text-above);
|
||||
background-color: var(--sea-background-above);
|
||||
}
|
||||
|
||||
button.sea:hover * {
|
||||
color: var(--sea-text-above);
|
||||
}
|
||||
|
||||
button.sea:active {
|
||||
color: var(--sea-text-below);
|
||||
background-color: var(--sea-background-below);
|
||||
transition: unset;
|
||||
}
|
||||
|
||||
button.sea:active * {
|
||||
color: var(--sea-text-below);
|
||||
transition: unset;
|
||||
}
|
||||
|
||||
button.wide {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
section#menu {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
section#menu > nav {
|
||||
top: 0;
|
||||
position: sticky;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: var(--menu-background);
|
||||
}
|
||||
|
||||
section#menu > nav > ul {
|
||||
margin: 0;
|
||||
min-width: 800px;
|
||||
width: 80%;
|
||||
height: 45px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
section#menu > nav > ul > li {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
section#menu > nav > ul > li.divided {
|
||||
margin-left: 9px;
|
||||
}
|
||||
|
||||
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 {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
section#menu > nav > ul > li > button {
|
||||
height: 100%;
|
||||
padding: 0 30px;
|
||||
color: var(--menu-text);
|
||||
}
|
||||
|
||||
section#menu > nav > ul > li > button:hover {
|
||||
color: var(--menu-text-above);
|
||||
}
|
||||
|
||||
section#menu > nav > ul > li > button:active {
|
||||
transition: 0s;
|
||||
color: var(--menu-text-below);
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
section#tasks.panel.list > div.row:nth-of-type(1) > span[data-column="end"] > i.home {
|
||||
margin-top: 5px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
section#tasks.panel.list
|
||||
> div.row
|
||||
> span:is(
|
||||
[data-column="worker"],
|
||||
[data-column="name"],
|
||||
[data-column="task"],
|
||||
[data-column="address"],
|
||||
[data-column="type"],
|
||||
[data-column="tax"],
|
||||
[data-column="commentary"],
|
||||
[data-column="chat"]
|
||||
) {
|
||||
min-width: 220px;
|
||||
width: 220px;
|
||||
display: ruby;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
section#tasks.panel.list > div.row > span[data-column="date"] {
|
||||
min-width: 101px;
|
||||
width: 101px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
section#tasks.panel.list > div.row > span[data-column="worker"] {
|
||||
min-width: 67px;
|
||||
width: 67px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
section#tasks.panel.list > div.row:nth-of-type(1) > span[data-column="worker"] {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
section#tasks.panel.list > div.row > span[data-column="name"] {
|
||||
min-width: 200px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
section#tasks.panel.list > div.row:nth-of-type(1) > span[data-column="start"] {
|
||||
margin-top: 13px;
|
||||
}
|
||||
|
||||
section#tasks.panel.list > div.row > span[data-column="start"] {
|
||||
min-width: 37px;
|
||||
width: 37px;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
section#tasks.panel.list > div.row > span[data-column="end"] {
|
||||
min-width: 37px;
|
||||
width: 37px;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
section#tasks.panel.list > div.row:nth-of-type(1) > span[data-column="hours"] {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
section#tasks.panel.list > div.row > span[data-column="hours"] {
|
||||
min-width: 27px;
|
||||
width: 27px;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
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: 46px;
|
||||
width: 46px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
section#tasks.panel.list > div.row > span[data-column="address"] {
|
||||
min-width: 180px;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
section#tasks.panel.list > div.row > span[data-column="type"] {
|
||||
min-width: 53px;
|
||||
width: 53px;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
section#tasks.panel.list > div.row > span[data-column="tax"] {
|
||||
min-width: 55px;
|
||||
width: 55px;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
section#tasks.panel.list > div.row > span[data-column="commentary"] {
|
||||
min-width: unset;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
section#tasks.panel.list > div.row > span[data-column="chat"] {
|
||||
min-width: 65px;
|
||||
width: 65px;
|
||||
text-align: center;
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
section#workers.panel.list
|
||||
> div.row:nth-of-type(1)
|
||||
> span[data-column="end"]
|
||||
> i.home {
|
||||
margin-top: 5px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
section#workers.panel.list
|
||||
> div.row
|
||||
> span:is(
|
||||
[data-column="id"],
|
||||
[data-column="name"],
|
||||
[data-column="birth"],
|
||||
[data-column="number"],
|
||||
[data-column="passport"],
|
||||
[data-column="department"],
|
||||
[data-column="city"],
|
||||
[data-column="address"],
|
||||
[data-column="requisites"],
|
||||
[data-column="tax"],
|
||||
[data-column="commentary"],
|
||||
[data-column="status"],
|
||||
) {
|
||||
min-width: 220px;
|
||||
width: 220px;
|
||||
display: ruby;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
section#workers.panel.list > div.row > span[data-column="id"] {
|
||||
min-width: 67px;
|
||||
width: 67px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
section#workers.panel.list > div.row:nth-of-type(1) > span[data-column="id"] {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
section#workers.panel.list > div.row > span[data-column="name"] {
|
||||
min-width: 200px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
section#workers.panel.list > div.row > span[data-column="birth"] {
|
||||
min-width: 80px;
|
||||
width: 80px;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
section#workers.panel.list > div.row > span[data-column="number"] {
|
||||
min-width: 120px;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
section#workers.panel.list > div.row > span[data-column="passport"] {
|
||||
min-width: 150px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
section#workers.panel.list > div.row > span[data-column="city"] {
|
||||
min-width: 90px;
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
section#workers.panel.list > div.row > span[data-column="address"] {
|
||||
min-width: 180px;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
section#workers.panel.list > div.row > span[data-column="requisites"] {
|
||||
min-width: 180px;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
section#workers.panel.list > div.row > span[data-column="tax"] {
|
||||
min-width: 55px;
|
||||
width: 55px;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
div#popup {
|
||||
z-index: 999999999999;
|
||||
left: 0;
|
||||
top: 0;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(18, 3, 3, 0.4);
|
||||
}
|
||||
|
||||
div#popup > section {
|
||||
background-color: var(--background-above-2);
|
||||
}
|
||||
|
||||
div#popup > section.errors {
|
||||
top: var(--top, 80%);
|
||||
position: absolute;
|
||||
height: var(--height);
|
||||
padding: 8px 30px !important;
|
||||
}
|
||||
|
||||
div#popup > section.small {
|
||||
width: 420px;
|
||||
}
|
||||
|
||||
div#popup > section.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 30px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
div#popup > section.list > h3 {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 18px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div#popup > section.list > span {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div#popup > section.list > span > b {
|
||||
margin-right: auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div#popup > section.list > :is(div, select).row.buttons {
|
||||
height: 33px;
|
||||
}
|
||||
|
||||
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 {
|
||||
height: fit-content;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
transition: 0s;
|
||||
}
|
||||
|
||||
div#popup > section.list > div.row:not(.monolithic) {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
div#popup > section.list > div.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) {
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div#popup > section.list > div.row > label > input[type="time"]:not(.small, .medium) {
|
||||
width: 55px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div#popup > section.list > div.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) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* div#popup > section.list > div.row > label > input:only-of-type:not(.center) {
|
||||
padding-left: 37px;
|
||||
} */
|
||||
|
||||
div#popup > section.list.errors > section.body > dl > dd {
|
||||
margin-left: 20px;
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\ebala;
|
||||
|
||||
// Файлы проекта
|
||||
use mirzaev\ebala\controllers\core as controller,
|
||||
mirzaev\ebala\models\core as model;
|
||||
|
||||
// Фреймворк
|
||||
use mirzaev\minimal\core,
|
||||
mirzaev\minimal\router;
|
||||
|
||||
ini_set('error_reporting', E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
|
||||
define('VIEWS', realpath('..' . DIRECTORY_SEPARATOR . 'views'));
|
||||
define('STORAGE', realpath('..' . DIRECTORY_SEPARATOR . 'storage'));
|
||||
define('INDEX', __DIR__);
|
||||
|
||||
// Автозагрузка
|
||||
require __DIR__
|
||||
. DIRECTORY_SEPARATOR . '..'
|
||||
. DIRECTORY_SEPARATOR . '..'
|
||||
. DIRECTORY_SEPARATOR . '..'
|
||||
. DIRECTORY_SEPARATOR . '..'
|
||||
. DIRECTORY_SEPARATOR . 'vendor'
|
||||
. DIRECTORY_SEPARATOR . 'autoload.php';
|
||||
|
||||
// Инициализация маршрутазитора
|
||||
$router = new router;
|
||||
|
||||
// Запись маршрутов
|
||||
$router->write('/', 'index', 'index', 'GET');
|
||||
$router->write('/', 'index', 'index', 'POST');
|
||||
$router->write('/workers', 'worker', 'index', 'GET');
|
||||
$router->write('/workers', 'worker', 'index', 'POST');
|
||||
$router->write('/markets', 'market', 'index', 'GET');
|
||||
$router->write('/markets', 'market', 'index', 'POST');
|
||||
$router->write('/operators', 'operator', 'index', 'GET');
|
||||
$router->write('/operators', 'operator', 'index', 'POST');
|
||||
$router->write('/administrators', 'administrators', 'index', 'GET');
|
||||
$router->write('/administrators', 'administrators', 'index', '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('/session/worker', 'session', 'worker', 'POST');
|
||||
$router->write('/session/write', 'session', 'write', 'POST');
|
||||
$router->write('/session/read', 'session', 'read', 'POST');
|
||||
$router->write('/session/administrator', 'session', 'administrator', 'POST');
|
||||
$router->write('/session/operator', 'session', 'operator', 'POST');
|
||||
$router->write('/session/market', 'session', 'market', 'POST');
|
||||
$router->write('/session/password', 'session', 'password', 'POST');
|
||||
$router->write('/session/invite', 'session', 'invite', 'POST');
|
||||
$router->write('/tasks/create', 'task', 'create', 'POST');
|
||||
$router->write('/tasks/read', 'task', 'read', 'POST');
|
||||
$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/hide', 'task', 'hide', 'POST');
|
||||
$router->write('/task/$task/remove', 'task', 'remove', 'POST');
|
||||
$router->write('/task/$task/work', 'task', 'work', 'POST');
|
||||
$router->write('/task/$task/date', 'task', 'date', 'POST');
|
||||
$router->write('/task/$task/works', 'task', 'works', 'POST');
|
||||
$router->write('/task/$task/description', 'task', 'description', 'POST');
|
||||
$router->write('/task/$task/commentary', 'task', 'commentary', 'POST');
|
||||
$router->write('/task/$task/worker/update', 'task', 'update', 'POST');
|
||||
$router->write('/task/$task/market/update', 'task', 'update', 'POST');
|
||||
$router->write('/worker/$worker/read', 'task', 'worker', 'POST');
|
||||
$router->write('/workers/read', 'worker', 'read', 'POST');
|
||||
$router->write('/workers/list', 'worker', 'datalist', 'POST');
|
||||
$router->write('/market/$market/read', 'task', 'market', 'POST');
|
||||
$router->write('/markets/list', 'market', 'datalist', 'POST');
|
||||
$router->write('/elements/menu', 'index', 'menu', 'POST');
|
||||
|
||||
// Инициализация ядра
|
||||
$core = new core(namespace: __NAMESPACE__, router: $router, controller: new controller(false), model: new model(false));
|
||||
|
||||
|
||||
// Обработка запроса
|
||||
echo $core->start();
|
|
@ -0,0 +1,84 @@
|
|||
"use strict";
|
||||
|
||||
if (typeof window.buffer !== "function") {
|
||||
// Not initialized
|
||||
|
||||
// Initialize of the class in global namespace
|
||||
window.buffer = class buffer {
|
||||
/**
|
||||
* Записать в буфер
|
||||
*
|
||||
* @param {string} name Название
|
||||
* @param {string|number} value Значение
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
static write(name, value) {
|
||||
if (
|
||||
typeof core === "function" && typeof name === "string" &&
|
||||
(typeof value === "string" || typeof value === "number")
|
||||
) {
|
||||
// Инициализировано ядро и переданы название со значением
|
||||
|
||||
// Инициализация даты удаления cookie
|
||||
const expires = new Date();
|
||||
|
||||
// Расчёт даты удаления (через 1 час после создания)
|
||||
expires.setTime(expires.getTime() + 3600000);
|
||||
|
||||
// Запись в cookie
|
||||
Cookies.set(name, value, {
|
||||
expires,
|
||||
path: "/",
|
||||
secure: true,
|
||||
httponly: false,
|
||||
samesite: "strict",
|
||||
});
|
||||
|
||||
// Запрос к серверу для записи в сессию (базу данных)
|
||||
return fetch("/session/write", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: `name=${name}&value=${value}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Прочитать из буфера
|
||||
*
|
||||
* @param {string} name Название
|
||||
*
|
||||
* @return {mixed}
|
||||
*/
|
||||
static async read(name) {
|
||||
if (typeof core === "function" && typeof name === "string") {
|
||||
// Инициализировано ядро и переданы название со значением
|
||||
|
||||
// Запрос к серверу для чтения из сессии (базы данных)
|
||||
return await fetch("/session/read", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: `name=${name}`,
|
||||
})
|
||||
.then((response) => response.text())
|
||||
.then((data) => {
|
||||
return data.length > 0
|
||||
? data
|
||||
: Cookies.get(`buffer_${core.interface}_${name}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Вызов события: "инициализировано"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("buffer.initialized", {
|
||||
detail: { buffer: window.buffer },
|
||||
}),
|
||||
);
|
|
@ -0,0 +1 @@
|
|||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var n=e.Cookies,o=e.Cookies=t();o.noConflict=function(){return e.Cookies=n,o}}())}(this,(function(){"use strict";function e(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var o in n)e[o]=n[o]}return e}return function t(n,o){function r(t,r,i){if("undefined"!=typeof document){"number"==typeof(i=e({},o,i)).expires&&(i.expires=new Date(Date.now()+864e5*i.expires)),i.expires&&(i.expires=i.expires.toUTCString()),t=encodeURIComponent(t).replace(/%(2[346B]|5E|60|7C)/g,decodeURIComponent).replace(/[()]/g,escape);var c="";for(var u in i)i[u]&&(c+="; "+u,!0!==i[u]&&(c+="="+i[u].split(";")[0]));return document.cookie=t+"="+n.write(r,t)+c}}return Object.create({set:r,get:function(e){if("undefined"!=typeof document&&(!arguments.length||e)){for(var t=document.cookie?document.cookie.split("; "):[],o={},r=0;r<t.length;r++){var i=t[r].split("="),c=i.slice(1).join("=");try{var u=decodeURIComponent(i[0]);if(o[u]=n.read(c,u),e===u)break}catch(e){}}return e?o[e]:o}},remove:function(t,n){r(t,"",e({},n,{expires:-1}))},withAttributes:function(n){return t(this.converter,e({},this.attributes,n))},withConverter:function(n){return t(e({},this.converter,n),this.attributes)}},{attributes:{value:Object.freeze(o)},converter:{value:Object.freeze(n)}})}({read:function(e){return'"'===e[0]&&(e=e.slice(1,-1)),e.replace(/(%[\dA-F]{2})+/gi,decodeURIComponent)},write:function(e){return encodeURIComponent(e).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,decodeURIComponent)}},{path:"/"})}));
|
|
@ -0,0 +1,34 @@
|
|||
"use strict";
|
||||
|
||||
if (typeof window.core !== "function") {
|
||||
// Not initialized
|
||||
|
||||
// Initialize of the class in global namespace
|
||||
window.core = class core {
|
||||
// Subdomain
|
||||
static subdomain = window.location.host.split(".")[0];
|
||||
|
||||
// Page
|
||||
static page = window.location.pathname === "/"
|
||||
? "tasks"
|
||||
: window.location.pathname.replace(/^.*\//, "");
|
||||
|
||||
// Interface
|
||||
static interface = this.subdomain === "xn--80aksgi6f"
|
||||
? "worker"
|
||||
: (this.subdomain === "xn--80aj0acchco"
|
||||
? "operator"
|
||||
: (this.subdomain === "xn--80aairftm"
|
||||
? "market"
|
||||
: (this.subdomain === "xn--80aalqawikqchmc"
|
||||
? "administrator"
|
||||
: "worker")));
|
||||
};
|
||||
}
|
||||
|
||||
// Вызов события: "инициализировано"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("core.initialized", {
|
||||
detail: { core: window.core },
|
||||
}),
|
||||
);
|
|
@ -0,0 +1,31 @@
|
|||
"use strict";
|
||||
|
||||
/**
|
||||
* Демпфер
|
||||
*
|
||||
* @param {function} func Функция
|
||||
* @param {number} timeout Таймер (ms)
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
function damper(func, timeout = 300) {
|
||||
// Инициализация таймера
|
||||
let timer;
|
||||
|
||||
return (...args) => {
|
||||
// Деинициализация таймера
|
||||
clearTimeout(timer);
|
||||
|
||||
// Вызов функции (вход в рекурсию)
|
||||
timer = setTimeout(() => {
|
||||
func.apply(this, args);
|
||||
}, timeout);
|
||||
};
|
||||
}
|
||||
|
||||
// Вызов события "Инициализирован демпфер"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("damper.initialized", {
|
||||
detail: { damper },
|
||||
}),
|
||||
);
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,183 @@
|
|||
"use strict";
|
||||
|
||||
if (typeof window.loader !== "function") {
|
||||
// Not initialized
|
||||
|
||||
// Initialize of the class in global namespace
|
||||
window.loader = class pages {
|
||||
/**
|
||||
* Storage for history
|
||||
*/
|
||||
static storage = {};
|
||||
|
||||
/**
|
||||
* Element: menu
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
static async menu() {
|
||||
return await fetch("/elements/menu", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
})
|
||||
.then((response) => response.text())
|
||||
.then((data) => {
|
||||
// Deleting outdated elements
|
||||
for (
|
||||
const element of document.querySelectorAll(
|
||||
`section[id="menu"]`,
|
||||
)
|
||||
) element.remove();
|
||||
|
||||
const section = document.createElement("section");
|
||||
document.body.prepend(section);
|
||||
section.outerHTML = data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Page: tasks
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
static async index() {
|
||||
// Запрос к серверу
|
||||
return await fetch("/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
})
|
||||
.then((response) => response.text())
|
||||
.then((data) => {
|
||||
// Write path in history
|
||||
history.pushState(this.storage, "/", "/");
|
||||
|
||||
// Write path to the current directory buffer
|
||||
core.page = 'tasks';
|
||||
|
||||
// Write content in document
|
||||
document.body.getElementsByTagName("main")[0].innerHTML = data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Page: administrators
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
static async administrators() {
|
||||
return await fetch("/administrators", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
})
|
||||
.then((response) => response.text())
|
||||
.then((data) => {
|
||||
document.body.getElementsByTagName("main")[0].innerHTML = data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Page: operators
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
static async operators() {
|
||||
return await fetch("/operators", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
})
|
||||
.then((response) => response.text())
|
||||
.then((data) => {
|
||||
document.body.getElementsByTagName("main")[0].innerHTML = data;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Page: markets
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
static async markets() {
|
||||
return await fetch("/markets", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
})
|
||||
.then((response) => response.text())
|
||||
.then((data) => {
|
||||
document.body.getElementsByTagName("main")[0].innerHTML = data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Page: workers
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
static async workers() {
|
||||
return await fetch("/workers", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
})
|
||||
.then((response) => response.text())
|
||||
.then((data) => {
|
||||
// Write path in history
|
||||
history.pushState(this.storage, "/workers", "/workers");
|
||||
|
||||
// Write path to the current directory buffer
|
||||
core.page = 'workers';
|
||||
|
||||
// Write content in document
|
||||
document.body.getElementsByTagName("main")[0].innerHTML = data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Page: settings
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
static async settings() {
|
||||
return await fetch("/settings", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
})
|
||||
.then((response) => response.text())
|
||||
.then((data) => {
|
||||
document.body.getElementsByTagName("main")[0].innerHTML = data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Page: account
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
static async account() {
|
||||
// Initialization of the account identifier
|
||||
account = Cookies.get(`account_id`) ?? "account";
|
||||
|
||||
return await fetch(`/${account}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
})
|
||||
.then((response) => response.text())
|
||||
.then((data) => {
|
||||
document.body.getElementsByTagName("main")[0].innerHTML = data;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
"use strict";
|
||||
|
||||
if (typeof window.markets !== "function") {
|
||||
// Not initialized
|
||||
|
||||
// Initialize of the class in global namespace
|
||||
window.markets = class markets {
|
||||
/**
|
||||
* Сгенерировать список
|
||||
*
|
||||
* @return {array|null} Массив HTML-элементов <option>
|
||||
*/
|
||||
static async list() {
|
||||
// Запрос к серверу
|
||||
return await fetch(`/markets/list`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
})
|
||||
.then((response) => response.text())
|
||||
.then((data) => {
|
||||
if (data.length > 0) {
|
||||
// Получены данные
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Вызов события: "инициализировано"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("markets.initialized", {
|
||||
detail: { markets: window.markets },
|
||||
}),
|
||||
);
|
|
@ -0,0 +1,250 @@
|
|||
"use strict";
|
||||
|
||||
if (typeof window.reinitializer !== "function") {
|
||||
// Not initialized
|
||||
|
||||
// Initialize of the class in global namespace
|
||||
window.reinitializer = class reinitializer {
|
||||
/**
|
||||
* Parent element for location <link> elements
|
||||
*/
|
||||
css = document.head;
|
||||
|
||||
/**
|
||||
* Parent element for location <script> elements
|
||||
*/
|
||||
js = document.body;
|
||||
|
||||
/**
|
||||
* Target element for searching new <link> and <script> elements
|
||||
*/
|
||||
root = document.body.getElementsByTagName("main")[0];
|
||||
|
||||
/**
|
||||
* Instance of the observer
|
||||
*/
|
||||
observer = new MutationObserver(() => this.handle());
|
||||
|
||||
/**
|
||||
* Construct
|
||||
*
|
||||
* @param {object} root Entry point
|
||||
*/
|
||||
constructor(root) {
|
||||
// Initialize of the root element
|
||||
this.root = root ?? this.root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinitialize <link> and <script> elements
|
||||
*
|
||||
* @param {bool} download Reinitialize elements with downloading or just move
|
||||
*
|
||||
* @return {bool} Processing status
|
||||
*/
|
||||
handle(download = true) {
|
||||
// Check for a dublicate execute launch
|
||||
if (this.started) return false;
|
||||
|
||||
// Initialization an observation status
|
||||
this.started = true;
|
||||
|
||||
for (
|
||||
let links;
|
||||
(links = this.root.getElementsByTagName("link")).length > 0;
|
||||
) {
|
||||
// Enumeration <link> elements
|
||||
|
||||
// Initialization of the <link> element
|
||||
const link = links[0];
|
||||
|
||||
if (download !== true || link.getAttribute("data-reinitializer-ignore") === "true") {
|
||||
// Download is disabled or marked as ignored
|
||||
|
||||
// Move element
|
||||
this.css.appendChild(link);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Initialization link of the <link> element
|
||||
const href = link.getAttribute("href");
|
||||
|
||||
if (
|
||||
link.getAttribute("data-reinitializer-once") === "true" &&
|
||||
this.css.querySelector(`:scope > link[href="${href}"]`)
|
||||
) {
|
||||
// Marked as executing once and already executed
|
||||
|
||||
// Stop listening
|
||||
this.stop();
|
||||
|
||||
// Delete outdated <link> element from the document
|
||||
link.remove();
|
||||
|
||||
// Start listening
|
||||
this.start();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Initialization outerHTML of the <link> element
|
||||
const html = link.outerHTML;
|
||||
|
||||
// Stop listening
|
||||
this.stop();
|
||||
|
||||
// Delete outdated <link> element from the document
|
||||
link.remove();
|
||||
|
||||
// Start listening
|
||||
this.start();
|
||||
|
||||
// Deleting outdated elements
|
||||
for (const element of this.css.querySelectorAll(
|
||||
`:scope > link[href="${href}"]`
|
||||
)) element.remove();
|
||||
|
||||
// Initialization of new <link> element
|
||||
const element = document.createElement("link");
|
||||
element.setAttribute("href", href);
|
||||
element.setAttribute("rel", "stylesheet");
|
||||
|
||||
// Write new element
|
||||
this.css.appendChild(element);
|
||||
|
||||
// Write content to the new <link> element
|
||||
element.outerHTML = html;
|
||||
}
|
||||
|
||||
for (
|
||||
let scripts;
|
||||
(scripts = this.root.getElementsByTagName("script")).length > 0;
|
||||
) {
|
||||
// Enumeration of <script> elements
|
||||
|
||||
// Initialization of the <script> element
|
||||
const script = scripts[0];
|
||||
|
||||
if (download !== true || script.getAttribute("data-reinitializer-ignore") === "true") {
|
||||
// Download is disabled or marked as ignored
|
||||
|
||||
|
||||
// Move element
|
||||
this.js.appendChild(script);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Initialization link of the <script> element
|
||||
const src = script.getAttribute("src");
|
||||
|
||||
// Initialization text of the <script> element
|
||||
const text = script.textContent;
|
||||
|
||||
if (
|
||||
script.getAttribute("data-reinitializer-once") === "true" &&
|
||||
(this.js.querySelector(`:scope > script[src="${src}"]`) ||
|
||||
Array.from(this.js.querySelectorAll(`:scope > script`)).filter(
|
||||
(e) => e.textContent === text
|
||||
).length > 0)
|
||||
) {
|
||||
// Marked as executing once and already executed
|
||||
|
||||
// Stop listening
|
||||
this.stop();
|
||||
|
||||
// Delete outdated <script> element from the document
|
||||
script.remove();
|
||||
|
||||
// Start listening
|
||||
this.start();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Initialization outerHTML of the <script> element
|
||||
const html = script.outerHTML;
|
||||
|
||||
// Stop listening
|
||||
this.stop();
|
||||
|
||||
// Delete outdated <script> element from the document
|
||||
script.remove();
|
||||
|
||||
// Start listening
|
||||
this.start();
|
||||
|
||||
// Initialization of new <script> element
|
||||
const element = document.createElement("script");
|
||||
|
||||
if (typeof src === "string") {
|
||||
// File
|
||||
|
||||
// Deleting outdated elements
|
||||
for (const element of this.js.querySelectorAll(
|
||||
`:scope > script[src="${src}"]`
|
||||
))
|
||||
element.remove();
|
||||
|
||||
// Copy link from outdated <script> element
|
||||
element.setAttribute("src", src);
|
||||
|
||||
// Write a type of <script> element
|
||||
element.setAttribute('type', 'text/javascript');
|
||||
} else {
|
||||
// Script
|
||||
|
||||
// Deleting outdated elements
|
||||
for (const element of Array.from(
|
||||
this.js.querySelectorAll(`:scope > script`)
|
||||
).filter((e) => e.textContent === text)) {
|
||||
element.remove();
|
||||
}
|
||||
|
||||
// Copy text from outdated <script> element
|
||||
element.textContent = text;
|
||||
}
|
||||
|
||||
// Write the new <script> element to end of <body> element
|
||||
this.js.appendChild(element);
|
||||
|
||||
// Write content to the new <script> element
|
||||
element.outerHTML = html;
|
||||
}
|
||||
|
||||
// Initialize of observation status
|
||||
this.started = false;
|
||||
|
||||
// Return (success)
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start observation
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
start() {
|
||||
this.observer.observe(this.root, {
|
||||
childList: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop observation
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
stop() {
|
||||
this.observer.disconnect();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Вызов события: "инициализировано"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("reinitializer.initialized", {
|
||||
detail: { reinitializer: window.reinitializer },
|
||||
})
|
||||
);
|
|
@ -0,0 +1,149 @@
|
|||
"use strict";
|
||||
|
||||
if (typeof window.session !== "function") {
|
||||
// Not initialized
|
||||
|
||||
// Initialize of the class in global namespace
|
||||
window.session = class session {
|
||||
/**
|
||||
* Отправить номер на сервер
|
||||
*
|
||||
* Записывает номер в сессию, а так же проверяет существование аккаунта с ним
|
||||
*
|
||||
* @param {string} number Номер
|
||||
*
|
||||
* @return {object} {(bool) exist, (array) errors}
|
||||
*/
|
||||
static async worker(number) {
|
||||
// Запрос к серверу
|
||||
return await fetch("/session/worker", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: `worker=${number}&remember=1&return=exist,verify,errors`,
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправить идентификатор администратора на сервер
|
||||
*
|
||||
* Записывает идентификатор оператора в сессию,
|
||||
* а так же проверяет существование аккаунта с ним
|
||||
*
|
||||
* @param {string} _key Идентификатор оператора
|
||||
*
|
||||
* @return {object} {(bool) exist, (array) errors}
|
||||
*/
|
||||
static async administrator(_key) {
|
||||
// Запрос к серверу
|
||||
return await fetch("/session/administrator", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: `administrator=${_key}&remember=1&return=exist,verify,errors`,
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправить идентификатор оператора на сервер
|
||||
*
|
||||
* Записывает идентификатор оператора в сессию,
|
||||
* а так же проверяет существование аккаунта с ним
|
||||
*
|
||||
* @param {string} _key Идентификатор оператора
|
||||
*
|
||||
* @return {object} {(bool) exist, (array) errors}
|
||||
*/
|
||||
static async operator(_key) {
|
||||
// Запрос к серверу
|
||||
return await fetch("/session/operator", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: `operator=${_key}&remember=1&return=exist,verify,errors`,
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправить идентификатор магазина на сервер
|
||||
*
|
||||
* Записывает идентификатор магазина в сессию,
|
||||
* а так же проверяет существование аккаунта с ним
|
||||
*
|
||||
* @param {string} id Идентификатор магазина
|
||||
*
|
||||
* @return {object} {(bool) exist, (array) errors}
|
||||
*/
|
||||
static async market(id) {
|
||||
// Запрос к серверу
|
||||
return await fetch("/session/market", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: `market=${id}&remember=1&return=exist,verify,errors`,
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправить пароль на сервер
|
||||
*
|
||||
* Записывает пароль в сессию, а так же проверяет его на соответствование требованиям
|
||||
*
|
||||
* @param {string} password Пароль
|
||||
*
|
||||
* @return {object} {(bool) verify, (bool) account, (array) errors}
|
||||
*/
|
||||
static async password(password) {
|
||||
// Инициализация названия поддомена
|
||||
const subdomain = window.location.host.split(".")[0];
|
||||
|
||||
// Инициализация типа аккаунта
|
||||
const type = subdomain === "xn--80aksgi6f"
|
||||
? "worker"
|
||||
: (subdomain === "xn--80aj0acchco"
|
||||
? "operator"
|
||||
: (subdomain === "xn--80aairftm" ? "market" : "worker"));
|
||||
|
||||
// Запрос к серверу
|
||||
return await fetch("/session/password", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body:
|
||||
`password=${password}&type=${type}&remember=1&return=account,verify,errors`,
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
return data;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Вызов события: "инициализировано"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("session.initialized", {
|
||||
detail: { session: window.session },
|
||||
}),
|
||||
);
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,3 @@
|
|||
*
|
||||
!.gitignore
|
||||
!*.sample
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'endpoint' => 'unix:///var/run/arangodb3/arango.sock',
|
||||
'database' => '',
|
||||
'name' => '',
|
||||
'password' => ''
|
||||
];
|
|
@ -0,0 +1,34 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html lang="ru">
|
||||
|
||||
<head>
|
||||
{% use 'head.html' with title as head_title, meta as head_meta, css as head_css %}
|
||||
|
||||
{% block title %}
|
||||
{{ block('head_title') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block meta %}
|
||||
{{ block('head_meta') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css_TEMPORARY %}
|
||||
{{ block('head_css') }}
|
||||
{% endblock %}
|
||||
{% block css %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% block body %}
|
||||
{% endblock %}
|
||||
|
||||
{% block js_TEMPORARY %}
|
||||
{% include 'js.html' %}
|
||||
{% endblock %}
|
||||
{% block js %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,14 @@
|
|||
<!-- MARKET #{{ market.id.value }} -->
|
||||
<h3>{{ market.id.value }}</h3>
|
||||
{% for key, data in market | filter((data, key) => key != '_key' and key != 'id' and key != 'status' and key !=
|
||||
'transfer_to_sheets') -%}
|
||||
{% if key == 'created' or key == 'updated' %}
|
||||
<span id="{{ market.id.value }}_{{ key }}"><b>{{ data.label }}:</b>{{ data.value is empty ? 'Никогда' :
|
||||
data.value|date('Y.m.d h:i:s') }}</span>
|
||||
{% elseif key == 'phone' or key == 'number' %}
|
||||
<span id="{{ market.id.value }}_number"><b>{{ data.label }}:</b><a href="tel:{{ data.value }}" title="Позвонить">{{
|
||||
data.value }}</a></span>
|
||||
{% else %}
|
||||
<span id="{{ market.id.value }}_{{ key }}"><b>{{ data.label }}:</b>{% if data.value is same as(true) %}Да{% elseif data.value is same as(false) %}Нет{% elseif data.value is empty %}{% else %}{{ data.value }}{% endif %}</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
|
@ -0,0 +1,14 @@
|
|||
<!-- TASK #{{ task._key.value }} -->
|
||||
<h3>{{ task._key.value }}</h3>
|
||||
{% for key, data in task | filter((data, key) => key != '_key') -%}
|
||||
{% if (key == 'created' or key == 'updated') %}
|
||||
<span id="{{ task.id.value }}_{{ key }}"><b>{{ data.label }}:</b>{{ data.value is empty ? 'Никогда' :
|
||||
data.value|date('Y.m.d h:i:s') }}</span>
|
||||
{% elseif key == 'confirmed' or key == 'hided' %}
|
||||
<span id="{{ task.id.value }}_{{ key }}"><b>{{ data.label }}:</b>{% if data.value is same as(true) %}Да{% elseif
|
||||
data.value is same as(false) or data.value is empty %}Нет{% else %}{{ data.value }}{% endif %}</span>
|
||||
{% else %}
|
||||
<span id="{{ task.id.value }}_{{ key }}"><b>{{ data.label }}:</b>{% if data.value is same as(true) %}Да{% elseif
|
||||
data.value is same as(false) %}Нет{% elseif data.value is empty %}{% else %}{{ data.value }}{% endif %}</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
|
@ -0,0 +1,16 @@
|
|||
<!-- WORKER #{{ worker.id.value }} -->
|
||||
<h3>{{ worker.name.value }}</h3>
|
||||
{% for key, data in worker | filter((data, key) => key != '_key' and key != 'name' and key != 'status'
|
||||
and key != 'transfer_to_sheets') -%}
|
||||
{% if key == 'created' or key == 'updated' %}
|
||||
<span id="{{ worker.id.value }}_{{ key }}"><b>{{ data.label }}:</b>{{ data.value is empty ? 'Никогда' :
|
||||
data.value|date('Y.m.d h:i:s') }}</span>
|
||||
{% elseif key == 'hiring' or key == 'birth' %}
|
||||
<span id="{{ worker.id.value }}_{{ key }}"><b>{{ data.label }}:</b>{{ data.value|date('Y.m.d') }}</span>
|
||||
{% elseif key == 'phone' or key == 'number' %}
|
||||
<span id="{{ worker.id.value }}_number"><b>{{ data.label }}:</b><a href="tel:{{ data.value }}" title="Позвонить">{{
|
||||
data.value }}</a></span>
|
||||
{% else %}
|
||||
<span id="{{ worker.id.value }}_{{ key }}"><b>{{ data.label }}:</b>{% if data.value is same as(true) %}Да{% elseif data.value is same as(false) %}Нет{% elseif data.value is empty %}{% else %}{{ data.value }}{% endif %}</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
|
@ -0,0 +1,38 @@
|
|||
{% if page != null %}<!-- PAGE #{{ page }} -->{% endif %}
|
||||
{% for row in rows %}
|
||||
<div id="{{ row.task._key }}"
|
||||
class="row{% if row.task.confirmed %} confirmed{% endif %}{% if row.task.hided %} hided{% endif %}">
|
||||
<span data-column="date" title="Заявка создана: {{ row.task.created is empty ? 'Никогда' :
|
||||
row.task.created|date('Y.m.d h:i:s') }}; Заявка обновлена: {{ row.task.updated is empty ? 'Никогда' :
|
||||
row.task.updated|date('Y.m.d h:i:s') }}" onclick="tasks.popup(this.parentElement)">{{ row.task.date|date('d.m.Y')
|
||||
}}</span>
|
||||
<span data-column="worker" title="{{ row.worker.id }}" onclick="tasks.worker.popup(this.parentElement)">{{
|
||||
row.worker.id }}</span>
|
||||
<span data-column="name" title="{{ row.worker.name }}" onclick="tasks.worker.popup(this.parentElement)">{{
|
||||
row.worker.name }}</span>
|
||||
<span data-column="task" title="{{ row.task.description }}" onclick="tasks.popup(this.parentElement)">{{ row.task.work
|
||||
}}</span>
|
||||
<span data-column="start" onclick="tasks.popup(this.parentElement)">{{
|
||||
row.task.generated.start }}</span>
|
||||
<span data-column="end" onclick="tasks.popup(this.parentElement)">{{
|
||||
row.task.generated.end }}</span>
|
||||
<span data-column="hours" onclick="tasks.popup(this.parentElement)">{{
|
||||
row.task.generated.hours }}</span>
|
||||
<span data-column="market" onclick="tasks.market.popup(this.parentElement)">{{
|
||||
row.market.id }}</span>
|
||||
<span data-column="address"
|
||||
title="{% if row.market.city is not null %}{{ row.market.city }}, {% endif %}{{ row.market.address }}"
|
||||
onclick="tasks.market.popup(this.parentElement)">{% if row.market.city is not null %}{{ row.market.city }}, {% endif
|
||||
%}{{
|
||||
row.market.address|replace({'ул.': '', 'ул ': '', 'пр ': ''}) }}</span>
|
||||
<span data-column="type" title="{{ row.market.type }}" onclick="tasks.market.popup(this.parentElement)">{{
|
||||
row.market.type }}</span>
|
||||
<span data-column="tax" title="{{ row.worker.tax }}" onclick="tasks.worker.popup(this.parentElement)">{{
|
||||
row.worker.tax }}</span>
|
||||
<span data-column="commentary" title="{{ row.task.commentary }}"
|
||||
onclick="tasks.commentary.popup(this.parentElement)">{{
|
||||
row.task.commentary }}</span>
|
||||
<span data-column="chat" title="Непрочитанные сообщения" onclick="tasks.chat(this.parentElement)">{{
|
||||
row.task.chat ?? 0 }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
|
@ -0,0 +1,24 @@
|
|||
{% if page != null %}<!-- PAGE #{{ page }} -->{% endif %}
|
||||
{% for row in rows %}
|
||||
<div id="{{ row.worker._key }}" class="row {{ row.worker.status }}">
|
||||
<span data-column="id" title="{{ row.worker.id }}" onclick="workers.worker.popup(this.parentElement)">{{
|
||||
row.worker.id }}</span>
|
||||
<span data-column="name" title="{{ row.worker.name }}" onclick="workers.worker.popup(this.parentElement)">{{
|
||||
row.worker.name }}</span>
|
||||
<span data-column="birth" onclick="workers.worker.popup(this.parentElement)">{{ row.worker.birth }}</span>
|
||||
<span data-column="number" onclick="workers.worker.popup(this.parentElement)">{{ row.worker.number }}</span>
|
||||
<span data-column="passport" title="{{ row.worker.passport }} {{ row.worker.department }} {{ row.worker.issued }}"
|
||||
onclick="workers.worker.popup(this.parentElement)">{{ row.worker.passport }} {{ row.worker.department }} {{
|
||||
row.worker.issued }}</span>
|
||||
<span data-column="address" title="{{ row.worker.city }} {{ row.worker.district }} {{ row.worker.address }}"
|
||||
onclick="workers.worker.popup(this.parentElement)">{{ row.worker.city }} {{ row.worker.district }} {{
|
||||
row.worker.address }}</span>
|
||||
<span data-column="tax" onclick="workers.worker.popup(this.parentElement)">{{ row.worker.tax }}</span>
|
||||
<span data-column="requisites" onclick="workers.worker.popup(this.parentElement)">{{ row.worker.requisites }} {{
|
||||
row.worker.payment }}</span>
|
||||
<span data-column="commentary" title="{{ row.worker.commentary }}"
|
||||
onclick="workers.commentary.popup(this.parentElement)">{{ row.worker.commentary }}</span>
|
||||
<span data-column="status" title="Непрочитанные сообщения" onclick="workers.status(this.parentElement)">{{
|
||||
row.worker.status ?? 'active' }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
|
@ -0,0 +1,4 @@
|
|||
<footer>
|
||||
<!-- <p><a href="http://www.anybrowser.org/campaign/"><img src="/img/logos/any_browser.gif" width="278" height="44" alt="Доступно на любом браузере" /></a></p> -->
|
||||
<!-- <p><a href="/browsers"><img src="/img/logos/any_browser.gif" width="278" height="44" alt="Доступно на любом браузере" /></a></p> -->
|
||||
</footer>
|
|
@ -0,0 +1,17 @@
|
|||
{% block title %}
|
||||
<title>{% if head.title != empty %}{{head.title}}{% else %}Спецресурс{% endif %}</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block meta %}
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
{% for meta in head.metas %}
|
||||
<meta {% for name, value in meta.attributes %}{{name}}="{{value}}" {% endfor %}>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link type="text/css" rel="stylesheet" href="/css/main.css" />
|
||||
<link type="text/css" rel="stylesheet" href="/css/popup.css" />
|
||||
<link type="text/css" rel="stylesheet" href="/css/animations.css" />
|
||||
{% endblock %}
|
|
@ -0,0 +1,10 @@
|
|||
{% block css %}
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<header>
|
||||
</header>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,28 @@
|
|||
{% extends "core.html" %}
|
||||
|
||||
{% use "core.html" with css as core_css, body as core_body, js as core_js %}
|
||||
{% use "header.html" with css as header_css, body as header_body, js as header_js %}
|
||||
{% use "menu.html" with css as menu_css, body as menu_body, js as menu_js %}
|
||||
|
||||
{% block css %}
|
||||
{{ block('core_css') }}
|
||||
{{ block('header_css') }}
|
||||
{{ block('menu_css') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% if account is not null %}
|
||||
{{ block('menu_body') }}
|
||||
{% endif %}
|
||||
<main>
|
||||
{% block main %}
|
||||
{{ main|raw }}
|
||||
{% endblock %}
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ block('header_js') }}
|
||||
{{ block('menu_js') }}
|
||||
{{ block('core_js') }}
|
||||
{% endblock %}
|
|
@ -0,0 +1,22 @@
|
|||
{% block js %}
|
||||
<script>
|
||||
document.addEventListener('reinitializer.initialized', function (e) {
|
||||
// Initialized reinitializer
|
||||
|
||||
// Initialize of instance of reinitializer
|
||||
const reinitializer = new e.detail.reinitializer();
|
||||
|
||||
// First reinitialization with prevented downloading
|
||||
reinitializer.handle(false);
|
||||
|
||||
// Start observation
|
||||
reinitializer.start();
|
||||
});
|
||||
</script>
|
||||
<script type="text/javascript" src="/js/reinitializer.js" defer></script>
|
||||
<script type="text/javascript" src="/js/damper.js" defer></script>
|
||||
<script type="text/javascript" src="/js/cookies.min.js" defer></script>
|
||||
<script type="text/javascript" src="/js/core.js" defer></script>
|
||||
<script type="text/javascript" src="/js/buffer.js" defer></script>
|
||||
<script type="text/javascript" data-reinitializer-once="true" src="/js/loader.js" defer></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,3 @@
|
|||
{% for market in markets %}
|
||||
<option value="{{ market.id }}">{{ market.id }}{% if market.director is not null %} {{ market.director }}{% endif %}</option>
|
||||
{% endfor %}
|
|
@ -0,0 +1,3 @@
|
|||
{% for worker in workers %}
|
||||
<option value="{{ worker.id }}">{{ worker.id }}{% if worker.name is not null %} {{ worker.name }}{% endif %}</option>
|
||||
{% endfor %}
|
|
@ -0,0 +1,16 @@
|
|||
{% if exist is same as(true) %}
|
||||
{% for work in works %}
|
||||
<option value="{{ work }}" {% if task.work==work %} selected{% endif %}>{{ work }}</option>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% if task is not null %}
|
||||
<optgroup label="Текущее">
|
||||
<option value="{{ task.work }}" selected>{{ task.work }}</option>
|
||||
</optgroup>
|
||||
{% endif %}
|
||||
<optgroup label="Доступное">
|
||||
{% for work in works %}
|
||||
<option value="{{ work }}">{{ work }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endif %}
|
|
@ -0,0 +1,43 @@
|
|||
{% block css %}
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<section id="menu">
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<button class="transparent" onclick="loader.index()" title="Список заявок">Заявки</button>
|
||||
</li>
|
||||
{% if account.type == 'administrator' %}
|
||||
<li class="divided">
|
||||
<button class="transparent" onclick="loader.administrators()"
|
||||
title="Управление администраторами">Администраторы</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="transparent" onclick="loader.operators()" title="Управление операторами">Операторы</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if account.type == 'administrator' or account.type == 'operator' %}
|
||||
<li{% if account.type !='administrator' %} class="divided" {% endif %}>
|
||||
<button class="transparent" onclick="loader.markets()" title="Управление магазинами">Магазины</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="transparent" onclick="loader.workers()" title="Управление сотрудниками">Сотрудники</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if account.type == 'administrator' %}
|
||||
<!-- <li class="divided">
|
||||
<button class="transparent" onclick="loader.settings()" title="Глобальные настройки сайта">Настройки</button>
|
||||
</li> -->
|
||||
{% endif %}
|
||||
<li id="account">
|
||||
<button class="transparent" onclick="loader.profile()" title="Настройки профиля">{{ account.name.first }} {{
|
||||
account.name.second }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,316 @@
|
|||
{% block css %}
|
||||
<link type="text/css" rel="stylesheet" href="/css/account.css">
|
||||
<link type="text/css" rel="stylesheet" href="/css/icons/arrow_right.css">
|
||||
<link type="text/css" rel="stylesheet" href="/css/icons/nametag.css">
|
||||
<link type="text/css" rel="stylesheet" href="/css/icons/keyhole.css">
|
||||
<link type="text/css" rel="stylesheet" href="/css/icons/user_add.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<section id="entry" class="panel medium">
|
||||
<section class="header unselectable">
|
||||
<h1>Идентификация</h1>
|
||||
</section>
|
||||
<section class="body">
|
||||
<iframe name="void" style="display:none"></iframe>
|
||||
<form id="identification" method="POST" action="/entry" target="void" autocomplete="on" novalidate="true">
|
||||
<label id="administrator" form="identification" for="_administrator">
|
||||
<i class="icon nametag"></i>
|
||||
<input id="_administrator" class="stretched merged left" name="administrator" type="text" placeholder="Идентификатор администратора" list="administrators"
|
||||
value="{{ session.buffer.entry.administrator._key ?? cookie.buffer_entry_administrator__key }}"
|
||||
onkeypress="if (event.keyCode === 13) this.nextElementSibling.click()" autocomplete="username"
|
||||
autofocus="true">
|
||||
<button type="submit" class="clay merged right" onclick="__administrator(); return false"><i class="icon arrow right"></i></button>
|
||||
<datalist id="administrators">
|
||||
{% for account in accounts %}
|
||||
<option value="{{ account.getKey() }}">{{ account.getKey() }} - {{ account.name.first }} {{ account.name.second }}</option>
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
</label>
|
||||
<label id="password" class="hidden" form="identification" for="_password">
|
||||
<i class="icon keyhole"></i>
|
||||
<input id="_password" class="stretched merged left" name="password" type="password" placeholder="Пароль"
|
||||
onkeypress="if (event.keyCode === 13) this.nextElementSibling.click()" autocomplete="current-password">
|
||||
<button type="submit" class="clay merged right" onclick="__password()"><i class="icon arrow right"></i></button>
|
||||
</label>
|
||||
</form>
|
||||
|
||||
</section>
|
||||
</section>
|
||||
<section id="errors" class="panel medium animation window hidden" style="--height: 300px">
|
||||
<section class="body">
|
||||
<dl></dl>
|
||||
</section>
|
||||
</section>
|
||||
<script>
|
||||
// Инициализация функций в глобальной области видимости
|
||||
let _administrator, __administrator, _password, __password, _errors, balls, trash;
|
||||
|
||||
document.addEventListener('damper.initialized', function (e) {
|
||||
// Инициализирован демпфер
|
||||
|
||||
// Защита от вызова на других страницах
|
||||
if (trash) return;
|
||||
|
||||
// Инициализация HTML-элемента с блоком входа в аккаунт
|
||||
const entry = {wrap: document.getElementById('entry')};
|
||||
entry.title = entry.wrap.getElementsByTagName('h1')[0];
|
||||
|
||||
// Инициализация HTML-элемента с блоком ошибок
|
||||
const errors = {wrap: document.getElementById('errors')};
|
||||
errors.list = errors.wrap.getElementsByTagName('dl')[0];
|
||||
|
||||
// Инициализация HTML-элементов-оболочек полей ввода
|
||||
const fields = {
|
||||
administrator: {label: document.getElementById('administrator')},
|
||||
password: {label: document.getElementById('password')},
|
||||
};
|
||||
fields.administrator.input = fields.administrator.label.getElementsByTagName('input')[0];
|
||||
fields.password.input = fields.password.label.getElementsByTagName('input')[0];
|
||||
fields.administrator.button = fields.administrator.label.getElementsByTagName('button')[0];
|
||||
fields.password.button = fields.password.label.getElementsByTagName('button')[0];
|
||||
|
||||
// Инициализация маски идентификатора администратора
|
||||
IMask(fields.administrator.input, {mask: '000000000000'});
|
||||
|
||||
/**
|
||||
* Отправить входной псевдоним на сервер
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
_administrator = async () => {
|
||||
// Деинициализация индикатора и анимации об ошибке
|
||||
fields.administrator.input.classList.remove('error');
|
||||
fields.administrator.input.previousElementSibling.classList.remove('error');
|
||||
|
||||
// Инициализация функции разблокировки
|
||||
function unblock() {
|
||||
// Разблокировка поля ввода
|
||||
fields.administrator.input.removeAttribute('readonly');
|
||||
fields.administrator.button.removeAttribute('disabled');
|
||||
|
||||
// Инициализация отображения ошибки
|
||||
fields.administrator.input.classList.add('error');
|
||||
fields.administrator.input.previousElementSibling.classList.add('error');
|
||||
|
||||
// Фокусировка на поле ввода
|
||||
fields.administrator.input.focus();
|
||||
}
|
||||
|
||||
// Запуск отсрочки разблокировки на случай, если сервер не отвечает
|
||||
const timeout = setTimeout(() => {_errors(['Сервер не отвечает']); unblock()}, 5000);
|
||||
|
||||
// Запрос к серверу
|
||||
const response = await session.administrator(fields.administrator.input.value);
|
||||
|
||||
// Удаление отсрочки разблокировки
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (_errors(response.errors)) {
|
||||
// Сгенерированы ошибки
|
||||
|
||||
// Разблокировка
|
||||
unblock();
|
||||
} else {
|
||||
// Не сгенерированы ошибки (подразумевается их отсутствие)
|
||||
|
||||
// Деинициализация интерфейса идентификации
|
||||
fields.administrator.label.classList.add('hidden');
|
||||
|
||||
// Инициализация интерфейса аутентификации
|
||||
entry.title.innerText = 'Аутентификация';
|
||||
fields.password.label.classList.remove('hidden');
|
||||
fields.password.input.focus();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Отправить входной псевдоним на сервер с дополнительной подготовкой
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
__administrator = e.detail.damper(async () => {
|
||||
// Блокировка поля ввода
|
||||
fields.administrator.input.setAttribute('readonly', true);
|
||||
fields.administrator.button.setAttribute('disabled', true);
|
||||
|
||||
// Реинициализация блока ошибок
|
||||
_errors();
|
||||
|
||||
// Запуск процесса отправки входного псевдонима
|
||||
_administrator();
|
||||
}, 1000);
|
||||
|
||||
/**
|
||||
* Отправить пароль на сервер
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
_password = async () => {
|
||||
// Деинициализация индикатора и анимации об ошибке
|
||||
fields.password.input.classList.remove('error');
|
||||
fields.password.input.previousElementSibling.classList.remove('error');
|
||||
|
||||
// Инициализация функции разблокировки
|
||||
function unblock() {
|
||||
// Разблокировка поля ввода
|
||||
fields.password.input.removeAttribute('readonly');
|
||||
fields.password.button.removeAttribute('disabled');
|
||||
|
||||
// Инициализация отображения ошибки
|
||||
fields.password.input.classList.add('error');
|
||||
fields.password.input.previousElementSibling.classList.add('error');
|
||||
|
||||
// Фокусировка на поле ввода
|
||||
fields.password.input.focus();
|
||||
}
|
||||
|
||||
// Запуск отсрочки разблокировки на случай, если сервер не отвечает
|
||||
const timeout = setTimeout(() => {_errors(['Сервер не отвечает']); unblock()}, 5000);
|
||||
|
||||
// Запрос к серверу
|
||||
const response = await session.password(fields.password.input.value);
|
||||
|
||||
// Удаление отсрочки разблокировки
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (_errors(response.errors) || response.verify !== true) {
|
||||
// Сгенерированы ошибки
|
||||
|
||||
// Разблокировка
|
||||
unblock();
|
||||
} else {
|
||||
// Не сгенерированы ошибки (подразумевается их отсутствие)
|
||||
|
||||
if (response.account) {
|
||||
// Инициализирован аккаунт
|
||||
|
||||
if (typeof loader === 'function') {
|
||||
// Initialized the loader class
|
||||
|
||||
// Отметить данный HTML-элемент с JS-кодом в очередь на очистку?
|
||||
trash = true;
|
||||
|
||||
// Деинициализация неактуального CSS-документа
|
||||
document.head.querySelector('link[href="/css/account.css"]').remove();
|
||||
|
||||
// Инициализация главной страницы
|
||||
loader.index();
|
||||
loader.menu();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Отправить пароль на сервер с дополнительной подготовкой
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
__password = e.detail.damper(async () => {
|
||||
// Блокировка поля ввода
|
||||
fields.password.input.setAttribute('readonly', true);
|
||||
fields.password.button.setAttribute('disabled', true);
|
||||
|
||||
// Реинициализация блока ошибок
|
||||
_errors();
|
||||
|
||||
// Запуск процесса отправки входного псевдонима
|
||||
_password();
|
||||
}, 100)
|
||||
|
||||
/**
|
||||
* Сгенерировать HTML-элемент с блоком ошибок
|
||||
*
|
||||
* @param {object} registry Реестр ошибок
|
||||
* @param {bool} reinitialization Реинициализировать?
|
||||
*
|
||||
* @return {bool} Сгенерированы ошибки?
|
||||
*/
|
||||
_errors = (registry, reinitialization = true) => {
|
||||
function height() {
|
||||
// Скрытие HTML-элемента
|
||||
errors.wrap.style.zIndex = '-99999';
|
||||
errors.wrap.style.left = '-99999px';
|
||||
errors.wrap.style.top = '-99999px';
|
||||
errors.wrap.style.position = 'absolute';
|
||||
errors.wrap.style.display = 'var(--display, unset)';
|
||||
errors.wrap.style.opacity = '0';
|
||||
errors.wrap.style.animationName = 'unset';
|
||||
|
||||
// Реинициализация переменной с данными о высоте HTML-элемента
|
||||
errors.wrap.style.setProperty('--height', errors.wrap.offsetHeight + 'px');
|
||||
|
||||
// Отмена скрытия HTML-элемента
|
||||
errors.wrap.style.zIndex =
|
||||
errors.wrap.style.left =
|
||||
errors.wrap.style.top =
|
||||
errors.wrap.style.position =
|
||||
errors.wrap.style.display =
|
||||
errors.wrap.style.opacity =
|
||||
errors.wrap.style.animationName = null;
|
||||
}
|
||||
|
||||
// Удаление ошибок из прошлой генерации
|
||||
if (reinitialization) errors.list.innerHTML = null;
|
||||
|
||||
for (error in registry) {
|
||||
// Генерация HTML-элементов с текстами ошибок
|
||||
|
||||
// Инициализация HTML-элемента текста ошибки
|
||||
const samp = document.createElement('samp');
|
||||
|
||||
if (typeof registry[error] === 'object') {
|
||||
// Категория ошибок
|
||||
|
||||
// Проверка наличия ошибок
|
||||
if (registry[error].length === 0) continue;
|
||||
|
||||
// Инициализация HTML-элемента-оболочки
|
||||
const wrap = document.createElement('dt');
|
||||
|
||||
// Запись текста категории
|
||||
samp.innerText = error;
|
||||
|
||||
// Запись HTML-элементов в список
|
||||
wrap.appendChild(samp);
|
||||
errors.list.appendChild(wrap);
|
||||
|
||||
// Реинициализация высоты
|
||||
height();
|
||||
|
||||
// Обработка вложенных ошибок (вход в рекурсию)
|
||||
_errors(registry[error], false);
|
||||
} else {
|
||||
// Текст ошибки (подразумевается)
|
||||
|
||||
// Инициализация HTML-элемента
|
||||
const wrap = document.createElement('dd');
|
||||
|
||||
// Запись текста ошибки
|
||||
samp.innerText = registry[error];
|
||||
|
||||
// Запись HTML-элемента в список
|
||||
wrap.appendChild(samp);
|
||||
errors.list.appendChild(wrap);
|
||||
|
||||
// Реинициализация высоты
|
||||
height();
|
||||
}
|
||||
}
|
||||
|
||||
// Реинициализация HTML-элемента с текстом ошибок
|
||||
if (reinitialization && errors.list.childElementCount === 0) errors.wrap.classList.add('hidden');
|
||||
else errors.wrap.classList.remove('hidden');
|
||||
|
||||
return errors.list.childElementCount === 0 ? false : true;
|
||||
};
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script type="text/javascript" src="/js/session.js"></script>
|
||||
<script type="text/javascript" src="/js/loader.js"></script>
|
||||
<script type="text/javascript" src="/js/imask-7.1.0-alpha.js"></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,317 @@
|
|||
{% block css %}
|
||||
<link type="text/css" rel="stylesheet" href="/css/account.css">
|
||||
<link type="text/css" rel="stylesheet" href="/css/icons/arrow_right.css">
|
||||
<link type="text/css" rel="stylesheet" href="/css/icons/nametag.css">
|
||||
<link type="text/css" rel="stylesheet" href="/css/icons/keyhole.css">
|
||||
<link type="text/css" rel="stylesheet" href="/css/icons/user_add.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<section id="entry" class="panel medium">
|
||||
<section class="header unselectable">
|
||||
<h1>Идентификация</h1>
|
||||
</section>
|
||||
<section class="body">
|
||||
<iframe name="void" style="display:none"></iframe>
|
||||
<form id="identification" method="POST" action="/entry" target="void" autocomplete="on" novalidate="true">
|
||||
<label id="market" form="identification" for="_market">
|
||||
<i class="icon nametag"></i>
|
||||
<input id="_market" class="stretched merged left" name="market" type="text" placeholder="Идентификатор магазина" list="markets"
|
||||
value="{{ session.buffer.entry.market.id ?? cookie.buffer_entry_market_id ?? 'K' }}"
|
||||
onkeypress="if (event.keyCode === 13) this.nextElementSibling.click()" autocomplete="username"
|
||||
autofocus="true">
|
||||
<button type="submit" class="clay merged right" onclick="__market(); return false"><i class="icon arrow right"></i></button>
|
||||
<datalist id="markets">
|
||||
{% for account in accounts %}
|
||||
<option value="{{ account.market.id }}">{{ account.market.id }} - {{
|
||||
account.vendor.name.first }} {{ account.vendor.name.second }}</option>
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
</label>
|
||||
<label id="password" class="hidden" form="identification" for="_password">
|
||||
<i class="icon keyhole"></i>
|
||||
<input id="_password" class="stretched merged left" name="password" type="password" placeholder="Пароль"
|
||||
onkeypress="if (event.keyCode === 13) this.nextElementSibling.click()" autocomplete="current-password">
|
||||
<button type="submit" class="clay merged right" onclick="__password()"><i class="icon arrow right"></i></button>
|
||||
</label>
|
||||
</form>
|
||||
|
||||
</section>
|
||||
</section>
|
||||
<section id="errors" class="panel medium animation window hidden" style="--height: 300px">
|
||||
<section class="body">
|
||||
<dl></dl>
|
||||
</section>
|
||||
</section>
|
||||
<script>
|
||||
// Инициализация функций в глобальной области видимости
|
||||
let _market, __market, _password, __password, _errors, balls, trash;
|
||||
|
||||
document.addEventListener('damper.initialized', function (e) {
|
||||
// Инициализирован демпфер
|
||||
|
||||
// Защита от вызова на других страницах
|
||||
if (trash) return;
|
||||
|
||||
// Инициализация HTML-элемента с блоком входа в аккаунт
|
||||
const entry = {wrap: document.getElementById('entry')};
|
||||
entry.title = entry.wrap.getElementsByTagName('h1')[0];
|
||||
|
||||
// Инициализация HTML-элемента с блоком ошибок
|
||||
const errors = {wrap: document.getElementById('errors')};
|
||||
errors.list = errors.wrap.getElementsByTagName('dl')[0];
|
||||
|
||||
// Инициализация HTML-элементов-оболочек полей ввода
|
||||
const fields = {
|
||||
market: {label: document.getElementById('market')},
|
||||
password: {label: document.getElementById('password')},
|
||||
};
|
||||
fields.market.input = fields.market.label.getElementsByTagName('input')[0];
|
||||
fields.password.input = fields.password.label.getElementsByTagName('input')[0];
|
||||
fields.market.button = fields.market.label.getElementsByTagName('button')[0];
|
||||
fields.password.button = fields.password.label.getElementsByTagName('button')[0];
|
||||
|
||||
// Инициализация маски идентификатора магазина
|
||||
IMask(fields.market.input, {mask: 'K000000'});
|
||||
|
||||
/**
|
||||
* Отправить входной псевдоним на сервер
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
_market = async () => {
|
||||
// Деинициализация индикатора и анимации об ошибке
|
||||
fields.market.input.classList.remove('error');
|
||||
fields.market.input.previousElementSibling.classList.remove('error');
|
||||
|
||||
// Инициализация функции разблокировки
|
||||
function unblock() {
|
||||
// Разблокировка поля ввода
|
||||
fields.market.input.removeAttribute('readonly');
|
||||
fields.market.button.removeAttribute('disabled');
|
||||
|
||||
// Инициализация отображения ошибки
|
||||
fields.market.input.classList.add('error');
|
||||
fields.market.input.previousElementSibling.classList.add('error');
|
||||
|
||||
// Фокусировка на поле ввода
|
||||
fields.market.input.focus();
|
||||
}
|
||||
|
||||
// Запуск отсрочки разблокировки на случай, если сервер не отвечает
|
||||
const timeout = setTimeout(() => {_errors(['Сервер не отвечает']); unblock()}, 5000);
|
||||
|
||||
// Запрос к серверу
|
||||
const response = await session.market(fields.market.input.value);
|
||||
|
||||
// Удаление отсрочки разблокировки
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (_errors(response.errors)) {
|
||||
// Сгенерированы ошибки
|
||||
|
||||
// Разблокировка
|
||||
unblock();
|
||||
} else {
|
||||
// Не сгенерированы ошибки (подразумевается их отсутствие)
|
||||
|
||||
// Деинициализация интерфейса идентификации
|
||||
fields.market.label.classList.add('hidden');
|
||||
|
||||
// Инициализация интерфейса аутентификации
|
||||
entry.title.innerText = 'Аутентификация';
|
||||
fields.password.label.classList.remove('hidden');
|
||||
fields.password.input.focus();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Отправить входной псевдоним на сервер с дополнительной подготовкой
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
__market = e.detail.damper(async () => {
|
||||
// Блокировка поля ввода
|
||||
fields.market.input.setAttribute('readonly', true);
|
||||
fields.market.button.setAttribute('disabled', true);
|
||||
|
||||
// Реинициализация блока ошибок
|
||||
_errors();
|
||||
|
||||
// Запуск процесса отправки входного псевдонима
|
||||
_market();
|
||||
}, 1000);
|
||||
|
||||
/**
|
||||
* Отправить пароль на сервер
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
_password = async () => {
|
||||
// Деинициализация индикатора и анимации об ошибке
|
||||
fields.password.input.classList.remove('error');
|
||||
fields.password.input.previousElementSibling.classList.remove('error');
|
||||
|
||||
// Инициализация функции разблокировки
|
||||
function unblock() {
|
||||
// Разблокировка поля ввода
|
||||
fields.password.input.removeAttribute('readonly');
|
||||
fields.password.button.removeAttribute('disabled');
|
||||
|
||||
// Инициализация отображения ошибки
|
||||
fields.password.input.classList.add('error');
|
||||
fields.password.input.previousElementSibling.classList.add('error');
|
||||
|
||||
// Фокусировка на поле ввода
|
||||
fields.password.input.focus();
|
||||
}
|
||||
|
||||
// Запуск отсрочки разблокировки на случай, если сервер не отвечает
|
||||
const timeout = setTimeout(() => {_errors(['Сервер не отвечает']); unblock()}, 5000);
|
||||
|
||||
// Запрос к серверу
|
||||
const response = await session.password(fields.password.input.value);
|
||||
|
||||
// Удаление отсрочки разблокировки
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (_errors(response.errors) || response.verify !== true) {
|
||||
// Сгенерированы ошибки
|
||||
|
||||
// Разблокировка
|
||||
unblock();
|
||||
} else {
|
||||
// Не сгенерированы ошибки (подразумевается их отсутствие)
|
||||
|
||||
if (response.account) {
|
||||
// Инициализирован аккаунт
|
||||
|
||||
if (typeof loader === 'function') {
|
||||
// Initialized the loader class
|
||||
|
||||
// Отметить данный HTML-элемент с JS-кодом в очередь на очистку?
|
||||
trash = true;
|
||||
|
||||
// Деинициализация неактуального CSS-документа
|
||||
document.head.querySelector('link[href="/css/account.css"]').remove();
|
||||
|
||||
// Инициализация главной страницы
|
||||
loader.index();
|
||||
loader.menu();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Отправить пароль на сервер с дополнительной подготовкой
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
__password = e.detail.damper(async () => {
|
||||
// Блокировка поля ввода
|
||||
fields.password.input.setAttribute('readonly', true);
|
||||
fields.password.button.setAttribute('disabled', true);
|
||||
|
||||
// Реинициализация блока ошибок
|
||||
_errors();
|
||||
|
||||
// Запуск процесса отправки входного псевдонима
|
||||
_password();
|
||||
}, 100)
|
||||
|
||||
/**
|
||||
* Сгенерировать HTML-элемент с блоком ошибок
|
||||
*
|
||||
* @param {object} registry Реестр ошибок
|
||||
* @param {bool} reinitialization Реинициализировать?
|
||||
*
|
||||
* @return {bool} Сгенерированы ошибки?
|
||||
*/
|
||||
_errors = (registry, reinitialization = true) => {
|
||||
function height() {
|
||||
// Скрытие HTML-элемента
|
||||
errors.wrap.style.zIndex = '-99999';
|
||||
errors.wrap.style.left = '-99999px';
|
||||
errors.wrap.style.top = '-99999px';
|
||||
errors.wrap.style.position = 'absolute';
|
||||
errors.wrap.style.display = 'var(--display, unset)';
|
||||
errors.wrap.style.opacity = '0';
|
||||
errors.wrap.style.animationName = 'unset';
|
||||
|
||||
// Реинициализация переменной с данными о высоте HTML-элемента
|
||||
errors.wrap.style.setProperty('--height', errors.wrap.offsetHeight + 'px');
|
||||
|
||||
// Отмена скрытия HTML-элемента
|
||||
errors.wrap.style.zIndex =
|
||||
errors.wrap.style.left =
|
||||
errors.wrap.style.top =
|
||||
errors.wrap.style.position =
|
||||
errors.wrap.style.display =
|
||||
errors.wrap.style.opacity =
|
||||
errors.wrap.style.animationName = null;
|
||||
}
|
||||
|
||||
// Удаление ошибок из прошлой генерации
|
||||
if (reinitialization) errors.list.innerHTML = null;
|
||||
|
||||
for (error in registry) {
|
||||
// Генерация HTML-элементов с текстами ошибок
|
||||
|
||||
// Инициализация HTML-элемента текста ошибки
|
||||
const samp = document.createElement('samp');
|
||||
|
||||
if (typeof registry[error] === 'object') {
|
||||
// Категория ошибок
|
||||
|
||||
// Проверка наличия ошибок
|
||||
if (registry[error].length === 0) continue;
|
||||
|
||||
// Инициализация HTML-элемента-оболочки
|
||||
const wrap = document.createElement('dt');
|
||||
|
||||
// Запись текста категории
|
||||
samp.innerText = error;
|
||||
|
||||
// Запись HTML-элементов в список
|
||||
wrap.appendChild(samp);
|
||||
errors.list.appendChild(wrap);
|
||||
|
||||
// Реинициализация высоты
|
||||
height();
|
||||
|
||||
// Обработка вложенных ошибок (вход в рекурсию)
|
||||
_errors(registry[error], false);
|
||||
} else {
|
||||
// Текст ошибки (подразумевается)
|
||||
|
||||
// Инициализация HTML-элемента
|
||||
const wrap = document.createElement('dd');
|
||||
|
||||
// Запись текста ошибки
|
||||
samp.innerText = registry[error];
|
||||
|
||||
// Запись HTML-элемента в список
|
||||
wrap.appendChild(samp);
|
||||
errors.list.appendChild(wrap);
|
||||
|
||||
// Реинициализация высоты
|
||||
height();
|
||||
}
|
||||
}
|
||||
|
||||
// Реинициализация HTML-элемента с текстом ошибок
|
||||
if (reinitialization && errors.list.childElementCount === 0) errors.wrap.classList.add('hidden');
|
||||
else errors.wrap.classList.remove('hidden');
|
||||
|
||||
return errors.list.childElementCount === 0 ? false : true;
|
||||
};
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script type="text/javascript" src="/js/session.js"></script>
|
||||
<script type="text/javascript" src="/js/loader.js"></script>
|
||||
<script type="text/javascript" src="/js/imask-7.1.0-alpha.js"></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,316 @@
|
|||
{% block css %}
|
||||
<link type="text/css" rel="stylesheet" href="/css/account.css">
|
||||
<link type="text/css" rel="stylesheet" href="/css/icons/arrow_right.css">
|
||||
<link type="text/css" rel="stylesheet" href="/css/icons/nametag.css">
|
||||
<link type="text/css" rel="stylesheet" href="/css/icons/keyhole.css">
|
||||
<link type="text/css" rel="stylesheet" href="/css/icons/user_add.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<section id="entry" class="panel medium">
|
||||
<section class="header unselectable">
|
||||
<h1>Идентификация</h1>
|
||||
</section>
|
||||
<section class="body">
|
||||
<iframe name="void" style="display:none"></iframe>
|
||||
<form id="identification" method="POST" action="/entry" target="void" autocomplete="on" novalidate="true">
|
||||
<label id="operator" form="identification" for="_operator">
|
||||
<i class="icon nametag"></i>
|
||||
<input id="_operator" class="stretched merged left" name="operator" type="text" placeholder="Идентификатор оператора" list="operators"
|
||||
value="{{ session.buffer.entry.operator._key ?? cookie.buffer_entry_operator__key }}"
|
||||
onkeypress="if (event.keyCode === 13) this.nextElementSibling.click()" autocomplete="username"
|
||||
autofocus="true">
|
||||
<button type="submit" class="clay merged right" onclick="__operator(); return false"><i class="icon arrow right"></i></button>
|
||||
<datalist id="operators">
|
||||
{% for account in accounts %}
|
||||
<option value="{{ account.getKey() }}">{{ account.getKey() }} - {{ account.name.first }} {{ account.name.second }}</option>
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
</label>
|
||||
<label id="password" class="hidden" form="identification" for="_password">
|
||||
<i class="icon keyhole"></i>
|
||||
<input id="_password" class="stretched merged left" name="password" type="password" placeholder="Пароль"
|
||||
onkeypress="if (event.keyCode === 13) this.nextElementSibling.click()" autocomplete="current-password">
|
||||
<button type="submit" class="clay merged right" onclick="__password()"><i class="icon arrow right"></i></button>
|
||||
</label>
|
||||
</form>
|
||||
|
||||
</section>
|
||||
</section>
|
||||
<section id="errors" class="panel medium animation window hidden" style="--height: 300px">
|
||||
<section class="body">
|
||||
<dl></dl>
|
||||
</section>
|
||||
</section>
|
||||
<script>
|
||||
// Инициализация функций в глобальной области видимости
|
||||
let _operator, __operator, _password, __password, _errors, balls, trash;
|
||||
|
||||
document.addEventListener('damper.initialized', function (e) {
|
||||
// Инициализирован демпфер
|
||||
|
||||
// Защита от вызова на других страницах
|
||||
if (trash) return;
|
||||
|
||||
// Инициализация HTML-элемента с блоком входа в аккаунт
|
||||
const entry = {wrap: document.getElementById('entry')};
|
||||
entry.title = entry.wrap.getElementsByTagName('h1')[0];
|
||||
|
||||
// Инициализация HTML-элемента с блоком ошибок
|
||||
const errors = {wrap: document.getElementById('errors')};
|
||||
errors.list = errors.wrap.getElementsByTagName('dl')[0];
|
||||
|
||||
// Инициализация HTML-элементов-оболочек полей ввода
|
||||
const fields = {
|
||||
operator: {label: document.getElementById('operator')},
|
||||
password: {label: document.getElementById('password')},
|
||||
};
|
||||
fields.operator.input = fields.operator.label.getElementsByTagName('input')[0];
|
||||
fields.password.input = fields.password.label.getElementsByTagName('input')[0];
|
||||
fields.operator.button = fields.operator.label.getElementsByTagName('button')[0];
|
||||
fields.password.button = fields.password.label.getElementsByTagName('button')[0];
|
||||
|
||||
// Инициализация маски идентификатора оператора
|
||||
IMask(fields.operator.input, {mask: '000000000000'});
|
||||
|
||||
/**
|
||||
* Отправить входной псевдоним на сервер
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
_operator = async () => {
|
||||
// Деинициализация индикатора и анимации об ошибке
|
||||
fields.operator.input.classList.remove('error');
|
||||
fields.operator.input.previousElementSibling.classList.remove('error');
|
||||
|
||||
// Инициализация функции разблокировки
|
||||
function unblock() {
|
||||
// Разблокировка поля ввода
|
||||
fields.operator.input.removeAttribute('readonly');
|
||||
fields.operator.button.removeAttribute('disabled');
|
||||
|
||||
// Инициализация отображения ошибки
|
||||
fields.operator.input.classList.add('error');
|
||||
fields.operator.input.previousElementSibling.classList.add('error');
|
||||
|
||||
// Фокусировка на поле ввода
|
||||
fields.operator.input.focus();
|
||||
}
|
||||
|
||||
// Запуск отсрочки разблокировки на случай, если сервер не отвечает
|
||||
const timeout = setTimeout(() => {_errors(['Сервер не отвечает']); unblock()}, 5000);
|
||||
|
||||
// Запрос к серверу
|
||||
const response = await session.operator(fields.operator.input.value);
|
||||
|
||||
// Удаление отсрочки разблокировки
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (_errors(response.errors)) {
|
||||
// Сгенерированы ошибки
|
||||
|
||||
// Разблокировка
|
||||
unblock();
|
||||
} else {
|
||||
// Не сгенерированы ошибки (подразумевается их отсутствие)
|
||||
|
||||
// Деинициализация интерфейса идентификации
|
||||
fields.operator.label.classList.add('hidden');
|
||||
|
||||
// Инициализация интерфейса аутентификации
|
||||
entry.title.innerText = 'Аутентификация';
|
||||
fields.password.label.classList.remove('hidden');
|
||||
fields.password.input.focus();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Отправить входной псевдоним на сервер с дополнительной подготовкой
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
__operator = e.detail.damper(async () => {
|
||||
// Блокировка поля ввода
|
||||
fields.operator.input.setAttribute('readonly', true);
|
||||
fields.operator.button.setAttribute('disabled', true);
|
||||
|
||||
// Реинициализация блока ошибок
|
||||
_errors();
|
||||
|
||||
// Запуск процесса отправки входного псевдонима
|
||||
_operator();
|
||||
}, 1000);
|
||||
|
||||
/**
|
||||
* Отправить пароль на сервер
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
_password = async () => {
|
||||
// Деинициализация индикатора и анимации об ошибке
|
||||
fields.password.input.classList.remove('error');
|
||||
fields.password.input.previousElementSibling.classList.remove('error');
|
||||
|
||||
// Инициализация функции разблокировки
|
||||
function unblock() {
|
||||
// Разблокировка поля ввода
|
||||
fields.password.input.removeAttribute('readonly');
|
||||
fields.password.button.removeAttribute('disabled');
|
||||
|
||||
// Инициализация отображения ошибки
|
||||
fields.password.input.classList.add('error');
|
||||
fields.password.input.previousElementSibling.classList.add('error');
|
||||
|
||||
// Фокусировка на поле ввода
|
||||
fields.password.input.focus();
|
||||
}
|
||||
|
||||
// Запуск отсрочки разблокировки на случай, если сервер не отвечает
|
||||
const timeout = setTimeout(() => {_errors(['Сервер не отвечает']); unblock()}, 5000);
|
||||
|
||||
// Запрос к серверу
|
||||
const response = await session.password(fields.password.input.value);
|
||||
|
||||
// Удаление отсрочки разблокировки
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (_errors(response.errors) || response.verify !== true) {
|
||||
// Сгенерированы ошибки
|
||||
|
||||
// Разблокировка
|
||||
unblock();
|
||||
} else {
|
||||
// Не сгенерированы ошибки (подразумевается их отсутствие)
|
||||
|
||||
if (response.account) {
|
||||
// Инициализирован аккаунт
|
||||
|
||||
if (typeof loader === 'function') {
|
||||
// Initialized the loader class
|
||||
|
||||
// Отметить данный HTML-элемент с JS-кодом в очередь на очистку?
|
||||
trash = true;
|
||||
|
||||
// Деинициализация неактуального CSS-документа
|
||||
document.head.querySelector('link[href="/css/account.css"]').remove();
|
||||
|
||||
// Инициализация главной страницы
|
||||
loader.index();
|
||||
loader.menu();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Отправить пароль на сервер с дополнительной подготовкой
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
__password = e.detail.damper(async () => {
|
||||
// Блокировка поля ввода
|
||||
fields.password.input.setAttribute('readonly', true);
|
||||
fields.password.button.setAttribute('disabled', true);
|
||||
|
||||
// Реинициализация блока ошибок
|
||||
_errors();
|
||||
|
||||
// Запуск процесса отправки входного псевдонима
|
||||
_password();
|
||||
}, 100)
|
||||
|
||||
/**
|
||||
* Сгенерировать HTML-элемент с блоком ошибок
|
||||
*
|
||||
* @param {object} registry Реестр ошибок
|
||||
* @param {bool} reinitialization Реинициализировать?
|
||||
*
|
||||
* @return {bool} Сгенерированы ошибки?
|
||||
*/
|
||||
_errors = (registry, reinitialization = true) => {
|
||||
function height() {
|
||||
// Скрытие HTML-элемента
|
||||
errors.wrap.style.zIndex = '-99999';
|
||||
errors.wrap.style.left = '-99999px';
|
||||
errors.wrap.style.top = '-99999px';
|
||||
errors.wrap.style.position = 'absolute';
|
||||
errors.wrap.style.display = 'var(--display, unset)';
|
||||
errors.wrap.style.opacity = '0';
|
||||
errors.wrap.style.animationName = 'unset';
|
||||
|
||||
// Реинициализация переменной с данными о высоте HTML-элемента
|
||||
errors.wrap.style.setProperty('--height', errors.wrap.offsetHeight + 'px');
|
||||
|
||||
// Отмена скрытия HTML-элемента
|
||||
errors.wrap.style.zIndex =
|
||||
errors.wrap.style.left =
|
||||
errors.wrap.style.top =
|
||||
errors.wrap.style.position =
|
||||
errors.wrap.style.display =
|
||||
errors.wrap.style.opacity =
|
||||
errors.wrap.style.animationName = null;
|
||||
}
|
||||
|
||||
// Удаление ошибок из прошлой генерации
|
||||
if (reinitialization) errors.list.innerHTML = null;
|
||||
|
||||
for (error in registry) {
|
||||
// Генерация HTML-элементов с текстами ошибок
|
||||
|
||||
// Инициализация HTML-элемента текста ошибки
|
||||
const samp = document.createElement('samp');
|
||||
|
||||
if (typeof registry[error] === 'object') {
|
||||
// Категория ошибок
|
||||
|
||||
// Проверка наличия ошибок
|
||||
if (registry[error].length === 0) continue;
|
||||
|
||||
// Инициализация HTML-элемента-оболочки
|
||||
const wrap = document.createElement('dt');
|
||||
|
||||
// Запись текста категории
|
||||
samp.innerText = error;
|
||||
|
||||
// Запись HTML-элементов в список
|
||||
wrap.appendChild(samp);
|
||||
errors.list.appendChild(wrap);
|
||||
|
||||
// Реинициализация высоты
|
||||
height();
|
||||
|
||||
// Обработка вложенных ошибок (вход в рекурсию)
|
||||
_errors(registry[error], false);
|
||||
} else {
|
||||
// Текст ошибки (подразумевается)
|
||||
|
||||
// Инициализация HTML-элемента
|
||||
const wrap = document.createElement('dd');
|
||||
|
||||
// Запись текста ошибки
|
||||
samp.innerText = registry[error];
|
||||
|
||||
// Запись HTML-элемента в список
|
||||
wrap.appendChild(samp);
|
||||
errors.list.appendChild(wrap);
|
||||
|
||||
// Реинициализация высоты
|
||||
height();
|
||||
}
|
||||
}
|
||||
|
||||
// Реинициализация HTML-элемента с текстом ошибок
|
||||
if (reinitialization && errors.list.childElementCount === 0) errors.wrap.classList.add('hidden');
|
||||
else errors.wrap.classList.remove('hidden');
|
||||
|
||||
return errors.list.childElementCount === 0 ? false : true;
|
||||
};
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script type="text/javascript" src="/js/session.js"></script>
|
||||
<script type="text/javascript" src="/js/loader.js"></script>
|
||||
<script type="text/javascript" src="/js/imask-7.1.0-alpha.js"></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,311 @@
|
|||
{% block css %}
|
||||
<link type="text/css" rel="stylesheet" href="/css/account.css">
|
||||
<link type="text/css" rel="stylesheet" href="/css/icons/arrow_right.css">
|
||||
<link type="text/css" rel="stylesheet" href="/css/icons/nametag.css">
|
||||
<link type="text/css" rel="stylesheet" href="/css/icons/keyhole.css">
|
||||
<link type="text/css" rel="stylesheet" href="/css/icons/user_add.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<section id="entry" class="panel medium">
|
||||
<section class="header unselectable">
|
||||
<h1>Идентификация</h1>
|
||||
</section>
|
||||
<section class="body">
|
||||
<iframe name="void" style="display:none"></iframe>
|
||||
<form id="identification" method="POST" action="/entry" target="void" autocomplete="on" novalidate="true">
|
||||
<label id="worker" form="identification" for="_worker">
|
||||
<i class="icon nametag"></i>
|
||||
<input id="_worker" class="stretched merged left" name="worker" type="tel" placeholder="Номер"
|
||||
value="{{ session.buffer.entry.worker.number ?? cookie.buffer_entry_worker_number }}"
|
||||
onkeypress="if (event.keyCode === 13) this.nextElementSibling.click()" autocomplete="username"
|
||||
autofocus="true">
|
||||
<button type="submit" class="clay merged right" onclick="__worker(); return false"><i class="icon arrow right"></i></button>
|
||||
</label>
|
||||
<label id="password" class="hidden" form="identification" for="_password">
|
||||
<i class="icon keyhole"></i>
|
||||
<input id="_password" class="stretched merged left" name="password" type="password" placeholder="Пароль"
|
||||
onkeypress="if (event.keyCode === 13) this.nextElementSibling.click()" autocomplete="current-password">
|
||||
<button type="submit" class="clay merged right" onclick="__password()"><i class="icon arrow right"></i></button>
|
||||
</label>
|
||||
</form>
|
||||
|
||||
</section>
|
||||
</section>
|
||||
<section id="errors" class="panel medium animation window hidden" style="--height: 300px">
|
||||
<section class="body">
|
||||
<dl></dl>
|
||||
</section>
|
||||
</section>
|
||||
<script>
|
||||
// Инициализация функций в глобальной области видимости
|
||||
let _worker, __worker, _password, __password, _errors, balls, trash;;
|
||||
|
||||
document.addEventListener('damper.initialized', function (e) {
|
||||
// Инициализирован демпфер
|
||||
|
||||
// Защита от вызова на других страницах
|
||||
if (trash) return;
|
||||
|
||||
// Инициализация HTML-элемента с блоком входа в аккаунт
|
||||
const entry = {wrap: document.getElementById('entry')};
|
||||
entry.title = entry.wrap.getElementsByTagName('h1')[0];
|
||||
|
||||
// Инициализация HTML-элемента с блоком ошибок
|
||||
const errors = {wrap: document.getElementById('errors')};
|
||||
errors.list = errors.wrap.getElementsByTagName('dl')[0];
|
||||
|
||||
// Инициализация HTML-элементов-оболочек полей ввода
|
||||
const fields = {
|
||||
worker: {label: document.getElementById('worker')},
|
||||
password: {label: document.getElementById('password')},
|
||||
};
|
||||
fields.worker.input = fields.worker.label.getElementsByTagName('input')[0];
|
||||
fields.password.input = fields.password.label.getElementsByTagName('input')[0];
|
||||
fields.worker.button = fields.worker.label.getElementsByTagName('button')[0];
|
||||
fields.password.button = fields.password.label.getElementsByTagName('button')[0];
|
||||
|
||||
// Инициализация маски номера
|
||||
IMask(fields.worker.input, {mask: '+{7} (000) 000-00-00'});
|
||||
|
||||
/**
|
||||
* Отправить входной псевдоним на сервер
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
_worker = async () => {
|
||||
// Деинициализация индикатора и анимации об ошибке
|
||||
fields.worker.input.classList.remove('error');
|
||||
fields.worker.input.previousElementSibling.classList.remove('error');
|
||||
|
||||
// Инициализация функции разблокировки
|
||||
function unblock() {
|
||||
// Разблокировка поля ввода
|
||||
fields.worker.input.removeAttribute('readonly');
|
||||
fields.worker.button.removeAttribute('disabled');
|
||||
|
||||
// Инициализация отображения ошибки
|
||||
fields.worker.input.classList.add('error');
|
||||
fields.worker.input.previousElementSibling.classList.add('error');
|
||||
|
||||
// Фокусировка на поле ввода
|
||||
fields.worker.input.focus();
|
||||
}
|
||||
|
||||
// Запуск отсрочки разблокировки на случай, если сервер не отвечает
|
||||
const timeout = setTimeout(() => {_errors(['Сервер не отвечает']); unblock()}, 5000);
|
||||
|
||||
// Запрос к серверу
|
||||
const response = await session.worker(fields.worker.input.value);
|
||||
|
||||
// Удаление отсрочки разблокировки
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (_errors(response.errors)) {
|
||||
// Сгенерированы ошибки
|
||||
|
||||
// Разблокировка
|
||||
unblock();
|
||||
} else {
|
||||
// Не сгенерированы ошибки (подразумевается их отсутствие)
|
||||
|
||||
// Деинициализация интерфейса идентификации
|
||||
fields.worker.label.classList.add('hidden');
|
||||
|
||||
// Инициализация интерфейса аутентификации
|
||||
entry.title.innerText = 'Аутентификация';
|
||||
fields.password.label.classList.remove('hidden');
|
||||
fields.password.input.focus();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Отправить входной псевдоним на сервер с дополнительной подготовкой
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
__worker = e.detail.damper(async () => {
|
||||
// Блокировка поля ввода
|
||||
fields.worker.input.setAttribute('readonly', true);
|
||||
fields.worker.button.setAttribute('disabled', true);
|
||||
|
||||
// Реинициализация блока ошибок
|
||||
_errors();
|
||||
|
||||
// Запуск процесса отправки входного псевдонима
|
||||
_worker();
|
||||
}, 1000);
|
||||
|
||||
/**
|
||||
* Отправить пароль на сервер
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
_password = async () => {
|
||||
// Деинициализация индикатора и анимации об ошибке
|
||||
fields.password.input.classList.remove('error');
|
||||
fields.password.input.previousElementSibling.classList.remove('error');
|
||||
|
||||
// Инициализация функции разблокировки
|
||||
function unblock() {
|
||||
// Разблокировка поля ввода
|
||||
fields.password.input.removeAttribute('readonly');
|
||||
fields.password.button.removeAttribute('disabled');
|
||||
|
||||
// Инициализация отображения ошибки
|
||||
fields.password.input.classList.add('error');
|
||||
fields.password.input.previousElementSibling.classList.add('error');
|
||||
|
||||
// Фокусировка на поле ввода
|
||||
fields.password.input.focus();
|
||||
}
|
||||
|
||||
// Запуск отсрочки разблокировки на случай, если сервер не отвечает
|
||||
const timeout = setTimeout(() => {_errors(['Сервер не отвечает']); unblock()}, 5000);
|
||||
|
||||
// Запрос к серверу
|
||||
const response = await session.password(fields.password.input.value);
|
||||
|
||||
// Удаление отсрочки разблокировки
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (_errors(response.errors) || response.verify !== true) {
|
||||
// Сгенерированы ошибки
|
||||
|
||||
// Разблокировка
|
||||
unblock();
|
||||
} else {
|
||||
// Не сгенерированы ошибки (подразумевается их отсутствие)
|
||||
|
||||
if (response.account) {
|
||||
// Инициализирован аккаунт
|
||||
|
||||
if (typeof loader === 'function') {
|
||||
// Initialized the loader class
|
||||
|
||||
// Отметить данный HTML-элемент с JS-кодом в очередь на очистку?
|
||||
trash = true;
|
||||
|
||||
// Деинициализация неактуального CSS-документа
|
||||
document.head.querySelector('link[href="/css/account.css"]').remove();
|
||||
|
||||
// Инициализация главной страницы
|
||||
loader.index();
|
||||
loader.menu();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Отправить пароль на сервер с дополнительной подготовкой
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
__password = e.detail.damper(async () => {
|
||||
// Блокировка поля ввода
|
||||
fields.password.input.setAttribute('readonly', true);
|
||||
fields.password.button.setAttribute('disabled', true);
|
||||
|
||||
// Реинициализация блока ошибок
|
||||
_errors();
|
||||
|
||||
// Запуск процесса отправки входного псевдонима
|
||||
_password();
|
||||
}, 100)
|
||||
|
||||
/**
|
||||
* Сгенерировать HTML-элемент с блоком ошибок
|
||||
*
|
||||
* @param {object} registry Реестр ошибок
|
||||
* @param {bool} reinitialization Реинициализировать?
|
||||
*
|
||||
* @return {bool} Сгенерированы ошибки?
|
||||
*/
|
||||
_errors = (registry, reinitialization = true) => {
|
||||
function height() {
|
||||
// Скрытие HTML-элемента
|
||||
errors.wrap.style.zIndex = '-99999';
|
||||
errors.wrap.style.left = '-99999px';
|
||||
errors.wrap.style.top = '-99999px';
|
||||
errors.wrap.style.position = 'absolute';
|
||||
errors.wrap.style.display = 'var(--display, unset)';
|
||||
errors.wrap.style.opacity = '0';
|
||||
errors.wrap.style.animationName = 'unset';
|
||||
|
||||
// Реинициализация переменной с данными о высоте HTML-элемента
|
||||
errors.wrap.style.setProperty('--height', errors.wrap.offsetHeight + 'px');
|
||||
|
||||
// Отмена скрытия HTML-элемента
|
||||
errors.wrap.style.zIndex =
|
||||
errors.wrap.style.left =
|
||||
errors.wrap.style.top =
|
||||
errors.wrap.style.position =
|
||||
errors.wrap.style.display =
|
||||
errors.wrap.style.opacity =
|
||||
errors.wrap.style.animationName = null;
|
||||
}
|
||||
|
||||
// Удаление ошибок из прошлой генерации
|
||||
if (reinitialization) errors.list.innerHTML = null;
|
||||
|
||||
for (error in registry) {
|
||||
// Генерация HTML-элементов с текстами ошибок
|
||||
|
||||
// Инициализация HTML-элемента текста ошибки
|
||||
const samp = document.createElement('samp');
|
||||
|
||||
if (typeof registry[error] === 'object') {
|
||||
// Категория ошибок
|
||||
|
||||
// Проверка наличия ошибок
|
||||
if (registry[error].length === 0) continue;
|
||||
|
||||
// Инициализация HTML-элемента-оболочки
|
||||
const wrap = document.createElement('dt');
|
||||
|
||||
// Запись текста категории
|
||||
samp.innerText = error;
|
||||
|
||||
// Запись HTML-элементов в список
|
||||
wrap.appendChild(samp);
|
||||
errors.list.appendChild(wrap);
|
||||
|
||||
// Реинициализация высоты
|
||||
height();
|
||||
|
||||
// Обработка вложенных ошибок (вход в рекурсию)
|
||||
_errors(registry[error], false);
|
||||
} else {
|
||||
// Текст ошибки (подразумевается)
|
||||
|
||||
// Инициализация HTML-элемента
|
||||
const wrap = document.createElement('dd');
|
||||
|
||||
// Запись текста ошибки
|
||||
samp.innerText = registry[error];
|
||||
|
||||
// Запись HTML-элемента в список
|
||||
wrap.appendChild(samp);
|
||||
errors.list.appendChild(wrap);
|
||||
|
||||
// Реинициализация высоты
|
||||
height();
|
||||
}
|
||||
}
|
||||
|
||||
// Реинициализация HTML-элемента с текстом ошибок
|
||||
if (reinitialization && errors.list.childElementCount === 0) errors.wrap.classList.add('hidden');
|
||||
else errors.wrap.classList.remove('hidden');
|
||||
|
||||
return errors.list.childElementCount === 0 ? false : true;
|
||||
};
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script type="text/javascript" src="/js/session.js"></script>
|
||||
<script type="text/javascript" src="/js/loader.js"></script>
|
||||
<script type="text/javascript" src="/js/imask-7.1.0-alpha.js"></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,3 @@
|
|||
oninput="this.nextElementSibling.innerText = this.value; this.nextElementSibling.style.setProperty('--left', (((this.value / 5) * 96) + 12) + 'px');"
|
||||
вычисляется по формуле
|
||||
(((value - minValue) / (valueMax - valueMin)) * ((totalInputWidth - thumbHalfWidth) - thumbHalfWidth)) + thumbHalfWidth;
|
|
@ -0,0 +1,118 @@
|
|||
{% extends('index.html') %}
|
||||
|
||||
{% block css %}
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/list.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/pages/markets.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/user.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/user_add.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/work_alt.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/home.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/timer.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/shopping_cart.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/search.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<section id="markets" class="panel medium list">
|
||||
<form id="actions" class="row menu separated" onsubmit="return false">
|
||||
<label for="actions">
|
||||
{% if account.type == 'administrator' or account.type == 'operator' or account.type == 'market' %}
|
||||
<button class="grass" onclick="markets.create()">Создать</button>
|
||||
{% endif %}
|
||||
</label>
|
||||
</form>
|
||||
<form id="filters" class="row menu stretched" onsubmit="return false">
|
||||
<label for="filters">
|
||||
<div class="range small">
|
||||
<input id="ratings" class="sand" type="range" value="0" min="0" max="5" step="1"
|
||||
oninput="this.nextElementSibling.innerText = this.value; this.nextElementSibling.style.setProperty('--left', (((this.value / 5) * 116) + 12) + 'px');"
|
||||
onchange="markets.filter('rating', this.value); markets.reinit();" title="Минимальный рейтинг" />
|
||||
<i style="--left: 0;" class="value unselectable">0</i>
|
||||
<script>
|
||||
// Initialization of input-event
|
||||
document.getElementById('ratings').oninput();
|
||||
</script>
|
||||
</div>
|
||||
<button class="{{ active ?? 'earth' }}" onclick="markets.filter('active', null, this); markets.reinit()" {% if
|
||||
active=='sand' %}title="... и активные" {% elseif active=='river' %}title="... или активные" {% endif
|
||||
%}>Активный</button>
|
||||
<button class="{{ inactive ?? 'earth' }}" onclick="markets.filter('inactive', null, this); markets.reinit()" {% if
|
||||
inactive=='sand' %}title="... и неактивные" {% elseif inactive=='river' %}title="... или неактивные" {% endif
|
||||
%}>Неактивный</button>
|
||||
<button class="{{ fined ?? 'earth' }}" onclick="markets.filter('fined', null, this); markets.reinit()" {% if
|
||||
fined=='sand' %}title="... и имеющие штрафы" {% elseif fined=='river' %}title="... или имеющие штрафы" {% endif
|
||||
%}>Штраф</button>
|
||||
<button class="{{ decent ?? 'earth' }}" onclick="markets.filter('decent', null, this); markets.reinit()" {% if
|
||||
decent=='sand' %}title="... и не имеющие штрафы" {% elseif decent=='river' %}title="... или не имеющие штрафы"
|
||||
{% endif %}>Нет штрафов</button>
|
||||
<button class="{{ hided ?? 'earth' }}" onclick="markets.filter('hided', null, this); markets.reinit()" {% if
|
||||
hided=='sand' %}title="... и скрытые" {% elseif hided=='river' %}title="... или скрытые" {% endif
|
||||
%}>Скрыт</button>
|
||||
<button class="{{ fired ?? 'earth' }}" onclick="markets.filter('fired', null, this); markets.reinit()" {% if
|
||||
fired=='sand' %}title="... и уволенные" {% elseif fired=='river' %}title="... или уволенные" {% endif
|
||||
%}>Уволен</button>
|
||||
</label>
|
||||
</form>
|
||||
<form class="row menu wide stretched" onsubmit="return false">
|
||||
<label class="solid">
|
||||
<i class="icon search"></i>
|
||||
<input class="clue merged right" type="search" name="search" id="search"
|
||||
placeholder="Глобальный поиск по сотрудникам" />
|
||||
<button class="sea merged left" onclick="markets.search(this.previousSiblingElement, this)">Поиск</button>
|
||||
</label>
|
||||
</form>
|
||||
<div id="title" class="row unselectable">
|
||||
<span data-column="id" class="button" title="Идентификатор"><i class="icon bold user"></i></span>
|
||||
<span data-column="name" class="button">ФИО</span>
|
||||
<span data-column="birth" class="button">Дата</span>
|
||||
<span data-column="number" class="button">Номер</span>
|
||||
<span data-column="passport" class="button">Паспорт</span>
|
||||
<span data-column="address" class="button">Адрес</span>
|
||||
<span data-column="tax" class="button">ИНН</span>
|
||||
<span data-column="requisites" class="button">Реквизиты</span>
|
||||
<span data-column="commentary" class="button">Комментарий</span>
|
||||
<span data-column="status" class="button">Статус</span>
|
||||
</div>
|
||||
</section>
|
||||
<script data-reinitializer-once="true">
|
||||
if (typeof window.markets_main_initialized === 'undefined') {
|
||||
// Не выполнялся скрипт
|
||||
|
||||
document.addEventListener('markets.initialized', (e) => {
|
||||
// Инициализированы сотрудники
|
||||
|
||||
// Инициализация допустимой страницы для выполнения
|
||||
e.detail.markets.page = 'markets';
|
||||
|
||||
// Инициализация страниц
|
||||
e.detail.markets.init();
|
||||
|
||||
// Блокировка от повторного выполнения
|
||||
window.markets_main_initialized = true;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script data-reinitializer-once="true">
|
||||
if (typeof window.markets_scroll_initialized === 'undefined') {
|
||||
// Не выполнялся скрипт
|
||||
|
||||
window.onscroll = function(e) {
|
||||
// Инициализация чтения новых задач при достижения конца страницы
|
||||
|
||||
if (core.page === 'markets') {
|
||||
// Инициализирована требуемая для выполнения страница
|
||||
|
||||
if ((window.innerHeight + Math.round(window.scrollY)) >= document.body.offsetHeight) markets.read();
|
||||
}
|
||||
};
|
||||
|
||||
// Блокировка от повторного выполнения
|
||||
window.markets_scroll_initialized = true;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script type="text/javascript" data-reinitializer-once="true" src="/js/imask-7.1.0-alpha.js" defer></script>
|
||||
<script type="text/javascript" src="/js/markets.js" defer></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,137 @@
|
|||
{% extends('index.html') %}
|
||||
|
||||
{% block css %}
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/list.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/pages/tasks.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/user.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/user_add.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/work_alt.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/home.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/timer.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/shopping_cart.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/search.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<section id="tasks" class="panel medium list">
|
||||
<form id="actions" class="row menu separated" onsubmit="return false">
|
||||
<label for="actions">
|
||||
{% if account.type == 'administrator' or account.type == 'operator' or account.type == 'market' %}
|
||||
<button class="grass" onclick="tasks.create()">Создать</button>
|
||||
{% endif %}
|
||||
{% if account.type == 'administrator' or account.type == 'operator' %}
|
||||
<button class="sea" onclick="">Выгрузка</button>
|
||||
{% endif %}
|
||||
</label>
|
||||
</form>
|
||||
<form id="filters" class="row menu stretched" onsubmit="return false">
|
||||
<label for="filters">
|
||||
<input class="sky" type="date" value="{{ from|date('Y-m-d') }}"
|
||||
onchange="this.setAttribute('value', this.value); tasks.filter('from', new Date(this.value) / 1000); tasks.reinit()"
|
||||
title="Временной промежуток и ..." />
|
||||
<input class="sky" type="date" value="{{ to is empty ? '' : to|date('Y-m-d') }}"
|
||||
onchange="this.setAttribute('value', this.value); tasks.filter('to', new Date(this.value) / 1000); tasks.reinit()"
|
||||
title="Временной промежуток и ..." />
|
||||
<div class="range small">
|
||||
<input id="rating" class="sand" type="range"
|
||||
value="{{ cookies.tasks_filter_rating ?? buffer.tasks.filters.rating ?? 0 }}" min="0" max="5" step="1"
|
||||
oninput="this.nextElementSibling.innerText = this.value; this.nextElementSibling.style.setProperty('--left', (((this.value / 5) * 116) + 12) + 'px');"
|
||||
onchange="tasks.filter('rating', this.value); tasks.reinit();" title="... и с данным минимальным рейтингом" />
|
||||
<i style="--left: 0;" class="value unselectable">{{ cookies.tasks_filter_rating ?? buffer.tasks.filters.rating
|
||||
?? 0 }}</i>
|
||||
<script>
|
||||
// Initialization of input-event
|
||||
document.getElementById('rating').oninput();
|
||||
</script>
|
||||
</div>
|
||||
<button class="{{ confirmed ?? 'earth' }}" onclick="tasks.filter('confirmed', null, this); tasks.reinit()" {% if
|
||||
confirmed=='sand' %}title="... и подтверждённые" {% elseif confirmed=='river'
|
||||
%}title="... или подтверждённые" {% endif %}>Подтверждён</button>
|
||||
<button class="{{ waiting ?? 'earth' }}" onclick="tasks.filter('waiting', null, this); tasks.reinit()" {% if
|
||||
waiting=='sand' %}title="... и ожидающие" {% elseif waiting=='river' %}title="... или ожидающие"
|
||||
{% endif %}>Ожидает</button>
|
||||
<button class="{{ published ?? 'earth' }}" onclick="tasks.filter('published', null, this); tasks.reinit()" {% if
|
||||
published=='sand' %}title="... и опубликованные" {% elseif published=='river'
|
||||
%}title="... или" {% endif %}>Опубликован</button>
|
||||
<button class="{{ unpublished ?? 'earth' }}" onclick="tasks.filter('unpublished', null, this); tasks.reinit()" {%
|
||||
if unpublished=='sand' %}title="... и неопубликованные" {% elseif unpublished=='river'
|
||||
%}title="... или неопубликованные" {% endif %}>Неопубликован</button>
|
||||
<button class="{{ problematic ?? 'earth' }}" onclick="tasks.filter('problematic', null, this); tasks.reinit()" {%
|
||||
if problematic=='sand' %}title="... и проблемные" {% elseif problematic=='river'
|
||||
%}title="... или проблемные" {% endif %}>Проблемный</button>
|
||||
<button class="{{ hided ?? 'earth' }}" onclick="tasks.filter('hided', null, this); tasks.reinit()" {% if
|
||||
hided=='sand' %}title="... и скрытые" {% elseif hided=='river' %}title="... или скрытые" {% endif
|
||||
%}>Скрыт</button>
|
||||
<button class="{{ completed ?? 'earth' }}" onclick="tasks.filter('completed', null, this); tasks.reinit()" {% if
|
||||
completed=='sand' %}title="... и завершённые" {% elseif completed=='river'
|
||||
%}title="... или завершённые" {% endif %}>Завершён</button>
|
||||
</label>
|
||||
</form>
|
||||
<form class="row menu wide stretched" onsubmit="return false">
|
||||
<label class="solid">
|
||||
<i class="icon search"></i>
|
||||
<input class="clue merged right" type="search" name="search" id="search"
|
||||
placeholder="Глобальный поиск по задачам" />
|
||||
<button class="sea merged left" onclick="tasks.search(this.previousSiblingElement, this)">Поиск</button>
|
||||
</label>
|
||||
</form>
|
||||
<div id="title" class="row unselectable">
|
||||
<span data-column="date" class="button">Дата</span>
|
||||
<span data-column="worker" class="button" title="Сотрудник"><i class="icon bold user"></i></span>
|
||||
<span data-column="name" class="button">ФИО</span>
|
||||
<span data-column="task" class="button">Работа</span>
|
||||
<span data-column="start" class="button" title="Начало"><i class="icon work alt"></i></span>
|
||||
<span data-column="end" class="button" title="Окончание"><i class="icon home"></i></span>
|
||||
<span data-column="hours" class="button" title="Время работы"><i class="icon timer"></i></span>
|
||||
<span data-column="market" class="button" title="Магазин"><i class="icon shopping cart"></i></span>
|
||||
<span data-column="address" class="button">Адрес</span>
|
||||
<span data-column="type" class="button">Тип</span>
|
||||
<span data-column="tax" class="button">ИНН</span>
|
||||
<span data-column="commentary" class="button">Комментарий</span>
|
||||
<span data-column="chat" class="button">Чат</span>
|
||||
</div>
|
||||
</section>
|
||||
<script data-reinitializer-once="true">
|
||||
if (typeof window.tasks_main_initialized === 'undefined') {
|
||||
// Не выполнялся скрипт
|
||||
|
||||
document.addEventListener('tasks.initialized', (e) => {
|
||||
// Инициализированы задачи
|
||||
|
||||
// Инициализация допустимой страницы для выполнения
|
||||
e.detail.tasks.page = 'tasks';
|
||||
|
||||
// Инициализация страниц
|
||||
e.detail.tasks.init();
|
||||
|
||||
// Блокировка от повторного выполнения
|
||||
window.tasks_main_initialized = true;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script data-reinitializer-once="true">
|
||||
if (typeof window.tasks_scroll_initialized === 'undefined') {
|
||||
// Не выполнялся скрипт
|
||||
|
||||
window.onscroll = function(e) {
|
||||
// Инициализация чтения новых задач при достижения конца страницы
|
||||
|
||||
if (core.page === 'tasks') {
|
||||
// Инициализирована требуемая для выполнения страница
|
||||
|
||||
if ((window.innerHeight + Math.round(window.scrollY)) >= document.body.offsetHeight) tasks.read();
|
||||
}
|
||||
};
|
||||
|
||||
// Блокировка от повторного выполнения
|
||||
window.tasks_scroll_initialized = true;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script type="text/javascript" data-reinitializer-once="true" src="/js/imask-7.1.0-alpha.js" defer></script>
|
||||
<script type="text/javascript" src="/js/tasks.js" defer></script>
|
||||
<script type="text/javascript" src="/js/workers.js" defer></script>
|
||||
<script type="text/javascript" src="/js/markets.js" defer></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,140 @@
|
|||
{% block css %}
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/list.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/pages/tasks.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/user.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/user_add.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/work_alt.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/home.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/timer.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/shopping_cart.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/search.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<section id="tasks" class="panel medium list">
|
||||
<form id="actions" class="row menu separated" onsubmit="return false">
|
||||
<label for="actions">
|
||||
{% if account.type == 'administrator' or account.type == 'operator' or account.type == 'market' %}
|
||||
<button class="grass" onclick="tasks.create()">Создать</button>
|
||||
{% endif %}
|
||||
{% if account.type == 'administrator' or account.type == 'operator' %}
|
||||
<button class="sea" onclick="">Выгрузка</button>
|
||||
{% endif %}
|
||||
</label>
|
||||
</form>
|
||||
<form id="filters" class="row menu stretched" onsubmit="return false">
|
||||
<label for="filters">
|
||||
<input class="sky" type="date" value="{{ from|date('Y-m-d') }}"
|
||||
onchange="this.setAttribute('value', this.value); tasks.filter('from', new Date(this.value) / 1000); tasks.reinit()"
|
||||
title="Временной промежуток и ..." />
|
||||
<input class="sky" type="date" value="{{ to is empty ? '' : to|date('Y-m-d') }}"
|
||||
onchange="this.setAttribute('value', this.value); tasks.filter('to', new Date(this.value) / 1000); tasks.reinit()"
|
||||
title="Временной промежуток и ..." />
|
||||
<div class="range small">
|
||||
<input id="rating" class="sand" type="range"
|
||||
value="{{ cookies.tasks_filter_rating ?? buffer.tasks.filters.rating ?? 0 }}" min="0" max="5" step="1"
|
||||
oninput="this.nextElementSibling.innerText = this.value; this.nextElementSibling.style.setProperty('--left', (((this.value / 5) * 116) + 12) + 'px');"
|
||||
onchange="tasks.filter('rating', this.value); tasks.reinit();" title="... и с данным минимальным рейтингом" />
|
||||
<i style="--left: 0;" class="value unselectable">{{ cookies.tasks_filter_rating ?? buffer.tasks.filters.rating
|
||||
?? 0 }}</i>
|
||||
<script>
|
||||
// Initialization of input-event
|
||||
document.getElementById('rating').oninput();
|
||||
</script>
|
||||
</div>
|
||||
<button class="{{ confirmed ?? 'earth' }}" onclick="tasks.filter('confirmed', null, this); tasks.reinit()" {% if
|
||||
confirmed=='sand' %}title="... и подтверждённые" {% elseif confirmed=='river'
|
||||
%}title="... или подтверждённые" {% endif %}>Подтверждён</button>
|
||||
<button class="{{ waiting ?? 'earth' }}" onclick="tasks.filter('waiting', null, this); tasks.reinit()" {% if
|
||||
waiting=='sand' %}title="... и ожидающие" {% elseif waiting=='river' %}title="... или ожидающие"
|
||||
{% endif %}>Ожидает</button>
|
||||
<button class="{{ published ?? 'earth' }}" onclick="tasks.filter('published', null, this); tasks.reinit()" {% if
|
||||
published=='sand' %}title="... и опубликованные" {% elseif published=='river'
|
||||
%}title="... или" {% endif %}>Опубликован</button>
|
||||
<button class="{{ unpublished ?? 'earth' }}" onclick="tasks.filter('unpublished', null, this); tasks.reinit()" {%
|
||||
if unpublished=='sand' %}title="... и неопубликованные" {% elseif unpublished=='river'
|
||||
%}title="... или неопубликованные" {% endif %}>Неопубликован</button>
|
||||
<button class="{{ problematic ?? 'earth' }}" onclick="tasks.filter('problematic', null, this); tasks.reinit()" {%
|
||||
if problematic=='sand' %}title="... и проблемные" {% elseif problematic=='river'
|
||||
%}title="... или проблемные" {% endif %}>Проблемный</button>
|
||||
<button class="{{ hided ?? 'earth' }}" onclick="tasks.filter('hided', null, this); tasks.reinit()" {% if
|
||||
hided=='sand' %}title="... и скрытые" {% elseif hided=='river' %}title="... или скрытые" {% endif
|
||||
%}>Скрыт</button>
|
||||
<button class="{{ completed ?? 'earth' }}" onclick="tasks.filter('completed', null, this); tasks.reinit()" {% if
|
||||
completed=='sand' %}title="... и завершённые" {% elseif completed=='river'
|
||||
%}title="... или завершённые" {% endif %}>Завершён</button>
|
||||
</label>
|
||||
</form>
|
||||
<form class="row menu wide stretched" onsubmit="return false">
|
||||
<label class="solid">
|
||||
<i class="icon search"></i>
|
||||
<input class="clue merged right" type="search" name="search" id="search"
|
||||
placeholder="Глобальный поиск по задачам" />
|
||||
<button class="sea merged left" onclick="tasks.search(this.previousSiblingElement, this)">Поиск</button>
|
||||
</label>
|
||||
</form>
|
||||
<div id="title" class="row unselectable">
|
||||
<span data-column="date" class="button">Дата</span>
|
||||
<span data-column="worker" class="button" title="Сотрудник"><i class="icon bold user"></i></span>
|
||||
<span data-column="name" class="button">ФИО</span>
|
||||
<span data-column="task" class="button">Работа</span>
|
||||
<span data-column="start" class="button" title="Начало"><i class="icon work alt"></i></span>
|
||||
<span data-column="end" class="button" title="Окончание"><i class="icon home"></i></span>
|
||||
<span data-column="hours" class="button" title="Время работы"><i class="icon timer"></i></span>
|
||||
<span data-column="market" class="button" title="Магазин"><i class="icon shopping cart"></i></span>
|
||||
<span data-column="address" class="button">Адрес</span>
|
||||
<span data-column="type" class="button">Тип</span>
|
||||
<span data-column="tax" class="button">ИНН</span>
|
||||
<span data-column="commentary" class="button">Комментарий</span>
|
||||
<span data-column="chat" class="button">Чат</span>
|
||||
</div>
|
||||
</section>
|
||||
<script data-reinitializer-once="true">
|
||||
if (typeof window.tasks_main_initialized === 'undefined') {
|
||||
// Не выполнялся скрипт
|
||||
|
||||
document.addEventListener('tasks.initialized', (e) => {
|
||||
// Инициализированы задачи
|
||||
|
||||
// Инициализация допустимой страницы для выполнения
|
||||
e.detail.tasks.page = 'tasks';
|
||||
|
||||
// Инициализация страниц
|
||||
e.detail.tasks.init();
|
||||
|
||||
// Блокировка от повторного выполнения
|
||||
window.tasks_main_initialized = true;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script data-reinitializer-once="true">
|
||||
if (typeof window.tasks_scroll_initialized === 'undefined') {
|
||||
// Не выполнялся скрипт
|
||||
|
||||
window.onscroll = function(e) {
|
||||
// Инициализация чтения новых задач при достижения конца страницы
|
||||
|
||||
if (core.page === 'tasks') {
|
||||
// Инициализирована требуемая для выполнения страница
|
||||
|
||||
if ((window.innerHeight + Math.round(window.scrollY)) >= document.body.offsetHeight) tasks.read();
|
||||
}
|
||||
};
|
||||
|
||||
// Блокировка от повторного выполнения
|
||||
window.tasks_scroll_initialized = true;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{% if server.REQUEST_METHOD == 'POST' %}
|
||||
<script type="text/javascript" src="/js/damper.js" defer></script>
|
||||
<script type="text/javascript" data-reinitializer-once="true" src="/js/imask-7.1.0-alpha.js" defer></script>
|
||||
<script type="text/javascript" src="/js/core.js" defer></script>
|
||||
<script type="text/javascript" data-reinitializer-once="true" src="/js/loader.js" defer></script>
|
||||
{% endif %}
|
||||
<script type="text/javascript" src="/js/tasks.js" defer></script>
|
||||
<script type="text/javascript" src="/js/workers.js" defer></script>
|
||||
<script type="text/javascript" src="/js/markets.js" defer></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,118 @@
|
|||
{% extends('index.html') %}
|
||||
|
||||
{% block css %}
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/list.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/pages/workers.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/user.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/user_add.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/work_alt.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/home.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/timer.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/shopping_cart.css">
|
||||
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/search.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<section id="workers" class="panel medium list">
|
||||
<form id="actions" class="row menu separated" onsubmit="return false">
|
||||
<label for="actions">
|
||||
{% if account.type == 'administrator' or account.type == 'operator' or account.type == 'market' %}
|
||||
<button class="grass" onclick="workers.create()">Создать</button>
|
||||
{% endif %}
|
||||
</label>
|
||||
</form>
|
||||
<form id="filters" class="row menu stretched" onsubmit="return false">
|
||||
<label for="filters">
|
||||
<div class="range small">
|
||||
<input id="ratings" class="sand" type="range" value="0" min="0" max="5" step="1"
|
||||
oninput="this.nextElementSibling.innerText = this.value; this.nextElementSibling.style.setProperty('--left', (((this.value / 5) * 116) + 12) + 'px');"
|
||||
onchange="workers.filter('rating', this.value); workers.reinit();" title="Минимальный рейтинг" />
|
||||
<i style="--left: 0;" class="value unselectable">0</i>
|
||||
<script>
|
||||
// Initialization of input-event
|
||||
document.getElementById('ratings').oninput();
|
||||
</script>
|
||||
</div>
|
||||
<button class="{{ active ?? 'earth' }}" onclick="workers.filter('active', null, this); workers.reinit()" {% if
|
||||
active=='sand' %}title="... и активные" {% elseif active=='river' %}title="... или активные" {% endif
|
||||
%}>Активный</button>
|
||||
<button class="{{ inactive ?? 'earth' }}" onclick="workers.filter('inactive', null, this); workers.reinit()" {% if
|
||||
inactive=='sand' %}title="... и неактивные" {% elseif inactive=='river' %}title="... или неактивные" {% endif
|
||||
%}>Неактивный</button>
|
||||
<button class="{{ fined ?? 'earth' }}" onclick="workers.filter('fined', null, this); workers.reinit()" {% if
|
||||
fined=='sand' %}title="... и имеющие штрафы" {% elseif fined=='river' %}title="... или имеющие штрафы" {% endif
|
||||
%}>Штраф</button>
|
||||
<button class="{{ decent ?? 'earth' }}" onclick="workers.filter('decent', null, this); workers.reinit()" {% if
|
||||
decent=='sand' %}title="... и не имеющие штрафы" {% elseif decent=='river' %}title="... или не имеющие штрафы"
|
||||
{% endif %}>Нет штрафов</button>
|
||||
<button class="{{ hided ?? 'earth' }}" onclick="workers.filter('hided', null, this); workers.reinit()" {% if
|
||||
hided=='sand' %}title="... и скрытые" {% elseif hided=='river' %}title="... или скрытые" {% endif
|
||||
%}>Скрыт</button>
|
||||
<button class="{{ fired ?? 'earth' }}" onclick="workers.filter('fired', null, this); workers.reinit()" {% if
|
||||
fired=='sand' %}title="... и уволенные" {% elseif fired=='river' %}title="... или уволенные" {% endif
|
||||
%}>Уволен</button>
|
||||
</label>
|
||||
</form>
|
||||
<form class="row menu wide stretched" onsubmit="return false">
|
||||
<label class="solid">
|
||||
<i class="icon search"></i>
|
||||
<input class="clue merged right" type="search" name="search" id="search"
|
||||
placeholder="Глобальный поиск по сотрудникам" />
|
||||
<button class="sea merged left" onclick="workers.search(this.previousSiblingElement, this)">Поиск</button>
|
||||
</label>
|
||||
</form>
|
||||
<div id="title" class="row unselectable">
|
||||
<span data-column="id" class="button" title="Идентификатор"><i class="icon bold user"></i></span>
|
||||
<span data-column="name" class="button">ФИО</span>
|
||||
<span data-column="birth" class="button">Дата</span>
|
||||
<span data-column="number" class="button">Номер</span>
|
||||
<span data-column="passport" class="button">Паспорт</span>
|
||||
<span data-column="address" class="button">Адрес</span>
|
||||
<span data-column="tax" class="button">ИНН</span>
|
||||
<span data-column="requisites" class="button">Реквизиты</span>
|
||||
<span data-column="commentary" class="button">Комментарий</span>
|
||||
<span data-column="status" class="button">Статус</span>
|
||||
</div>
|
||||
</section>
|
||||
<script data-reinitializer-once="true">
|
||||
if (typeof window.workers_main_initialized === 'undefined') {
|
||||
// Не выполнялся скрипт
|
||||
|
||||
document.addEventListener('workers.initialized', (e) => {
|
||||
// Инициализированы сотрудники
|
||||
|
||||
// Инициализация допустимой страницы для выполнения
|
||||
e.detail.workers.page = 'workers';
|
||||
|
||||
// Инициализация страниц
|
||||
e.detail.workers.init();
|
||||
|
||||
// Блокировка от повторного выполнения
|
||||
window.workers_main_initialized = true;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script data-reinitializer-once="true">
|
||||
if (typeof window.workers_scroll_initialized === 'undefined') {
|
||||
// Не выполнялся скрипт
|
||||
|
||||
window.onscroll = function(e) {
|
||||
// Инициализация чтения новых задач при достижения конца страницы
|
||||
|
||||
if (core.page === 'workers') {
|
||||
// Инициализирована требуемая для выполнения страница
|
||||
|
||||
if ((window.innerHeight + Math.round(window.scrollY)) >= document.body.offsetHeight) workers.read();
|
||||
}
|
||||
};
|
||||
|
||||
// Блокировка от повторного выполнения
|
||||
window.workers_scroll_initialized = true;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script type="text/javascript" data-reinitializer-once="true" src="/js/imask-7.1.0-alpha.js" defer></script>
|
||||
<script type="text/javascript" src="/js/workers.js" defer></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,191 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\ebala\views;
|
||||
|
||||
// Файлы проекта
|
||||
use mirzaev\ebala\models\session,
|
||||
mirzaev\ebala\models\account;
|
||||
|
||||
// Фреймворк PHP
|
||||
use mirzaev\minimal\controller;
|
||||
|
||||
// Шаблонизатор представлений
|
||||
use Twig\Loader\FilesystemLoader,
|
||||
Twig\Environment as twig,
|
||||
Twig\Extra\Intl\IntlExtension as intl;
|
||||
|
||||
// Встроенные библиотеки
|
||||
use ArrayAccess;
|
||||
|
||||
/**
|
||||
* Шаблонизатор представлений
|
||||
*
|
||||
* @package mirzaev\ebala\controllers
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class templater extends controller implements ArrayAccess
|
||||
{
|
||||
/**
|
||||
* Реестр глобальных переменных
|
||||
*/
|
||||
public array $variables = [];
|
||||
|
||||
/**
|
||||
* Инстанция окружения twig
|
||||
*/
|
||||
public twig $twig;
|
||||
|
||||
/**
|
||||
* Конструктор
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(?session &$session = null, ?account &$account = null)
|
||||
{
|
||||
// Инициализация шаблонизатора
|
||||
$this->twig = new twig(new FilesystemLoader(VIEWS));
|
||||
|
||||
// Инициализация глобальных переменных
|
||||
$this->twig->addGlobal('server', $_SERVER);
|
||||
$this->twig->addGlobal('cookie', $_COOKIE); // @todo DELETE THIS
|
||||
$this->twig->addGlobal('cookies', $_COOKIE);
|
||||
if (!empty($session->status())) {
|
||||
$this->twig->addGlobal('session', $session);
|
||||
$this->twig->addGlobal('buffer', $session->buffer[$_SERVER['INTERFACE']] ?? null);
|
||||
}
|
||||
if (!empty($account->status())) $this->twig->addGlobal('account', $account);
|
||||
|
||||
// Инициализация расширений
|
||||
$this->twig->addExtension(new intl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Отрисовка HTML-документа
|
||||
*
|
||||
* @param string $file Относительный директории представлений путь до файла представления
|
||||
* @param ?array $variables Реестр переменных
|
||||
*
|
||||
* @return ?string HTML-документ
|
||||
*/
|
||||
public function render(string $file, ?array $variables = null): ?string
|
||||
{
|
||||
// Генерация представления
|
||||
return $this->twig->render($file, $variables ?? $this->variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Записать
|
||||
*
|
||||
* Записывает переменную в реестр глобальных переменных
|
||||
*
|
||||
* @param string $name Название
|
||||
* @param mixed $value Содержимое
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __set(string $name, mixed $value = null): void
|
||||
{
|
||||
$this->variables[$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Прочитать
|
||||
*
|
||||
* Читает переменную из реестра глобальных переменных
|
||||
*
|
||||
* @param string $name Название
|
||||
*
|
||||
* @return mixed Данные переменной из реестра глобальных переменных
|
||||
*/
|
||||
public function __get(string $name): mixed
|
||||
{
|
||||
return $this->variables[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить инициализированность
|
||||
*
|
||||
* Проверяет инициализированность переменной в буфере переменных представления
|
||||
*
|
||||
* @param string $name Название
|
||||
*
|
||||
* @return bool Переменная инициализирована?
|
||||
*/
|
||||
public function __isset(string $name): bool
|
||||
{
|
||||
return isset($this->variables[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить
|
||||
*
|
||||
* Деинициализирует переменную в реестре глобальных переменных
|
||||
*
|
||||
* @param string $name Название
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __unset(string $name): void
|
||||
{
|
||||
unset($this->variables[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Записать
|
||||
*
|
||||
* Записывает переменную в реестр глобальных переменных
|
||||
*
|
||||
* @param mixed $offset Сдвиг, либо идентификатор
|
||||
* @param mixed $value Содержимое
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
$this->variables[$offset] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Прочитать
|
||||
*
|
||||
* Читает переменную из реестра глобальных переменных
|
||||
*
|
||||
* @param mixed $offset Сдвиг, либо идентификатор
|
||||
*
|
||||
* @return mixed Данные переменной из реестра глобальных переменных
|
||||
*/
|
||||
public function offsetGet(mixed $offset): mixed
|
||||
{
|
||||
return $this->variables[$offset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить инициализированность
|
||||
*
|
||||
* Проверяет инициализированность переменной в реестре глобальных переменных
|
||||
*
|
||||
* @param mixed $offset Сдвиг, либо идентификатор
|
||||
*
|
||||
* @return bool Инициализирована переменная?
|
||||
*/
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
return isset($this->variables[$offset]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить
|
||||
*
|
||||
* Деинициализирует переменную в реестре глобальных переменных
|
||||
*
|
||||
* @param mixed $offset Сдвиг, либо идентификатор
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
unset($this->variables[$offset]);
|
||||
}
|
||||
}
|
Reference in New Issue