плотно навалил

This commit is contained in:
Arsen Mirzaev Tatyano-Muradovich 2023-10-21 05:52:07 +07:00
parent c73ddc6ba8
commit 68aab813c4
79 changed files with 22295 additions and 0 deletions

3
.gitignore vendored Executable file
View File

@ -0,0 +1,3 @@
!.gitignore
composer.phar
vendor

0
LICENSE Normal file → Executable file
View File

0
README.md Normal file → Executable file
View File

57
composer.json Executable file
View File

@ -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"
}
}
}

5401
composer.lock generated Executable file

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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');
}
}
}

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 [];
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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();

View File

@ -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 },
}),
);

View File

@ -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:"/"})}));

View File

@ -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 },
}),
);

View File

@ -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

View File

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

View File

@ -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 },
}),
);

View File

@ -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 },
})
);

View File

@ -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

3
mirzaev/ebala/system/settings/.gitignore vendored Executable file
View File

@ -0,0 +1,3 @@
*
!.gitignore
!*.sample

View File

@ -0,0 +1,8 @@
<?php
return [
'endpoint' => 'unix:///var/run/arangodb3/arango.sock',
'database' => '',
'name' => '',
'password' => ''
];

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -0,0 +1,10 @@
{% block css %}
{% endblock %}
{% block body %}
<header>
</header>
{% endblock %}
{% block js %}
{% endblock %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

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

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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]);
}
}