ДОХУЯ ДОБАВИЛ ОЧЕНЬ МОЩНО ЕБАНУЛ

This commit is contained in:
Arsen Mirzaev Tatyano-Muradovich 2023-12-21 00:06:16 +07:00
parent 525d71dfe4
commit 92417f7567
66 changed files with 19619 additions and 5492 deletions

View File

@ -0,0 +1,267 @@
<?php
declare(strict_types=1);
namespace mirzaev\ebala\controllers;
// Файлы проекта
use mirzaev\ebala\controllers\core,
mirzaev\ebala\controllers\traits\errors,
mirzaev\ebala\models\account as model,
mirzaev\ebala\models\registry,
mirzaev\ebala\models\core as _core;
// Фреймворк ArangoDB
use mirzaev\arangodb\document;
// Встроенные библиотеки
use exception;
/**
* Контроллер аккаунта
*
* @package mirzaev\ebala\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class account extends core
{
use errors;
/**
* Прочитать данные
*
* @param array $parameters Параметры запроса
*/
public function fields(array $parameters = []): ?string
{
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
// Инициализация данных аккаунта
$account = model::read('d._key == "' . $parameters['id'] . '"', return: '{ name: d.name, number: d.number, mail: d.mail, commentary: d.commentary }')->getAll();
if (!empty($account)) {
// Найдены данные аккаунта
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode($account);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
}
// Возврат (провал)
return null;
}
/**
* Обновить данные
*
* @param array $parameters Параметры запроса
*/
public function update(array $parameters = []): ?string
{
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
// Инициализация данных аккаунта
$account = model::read('d._key == "' . $parameters['id'] . '"');
if (!empty($account)) {
// Найден аккаунт
// Инициализация буфера изменённости пароля
$password = false;
// Инициализация параметров (перезапись переданными значениями)
if ($parameters['name_first'] !== $account->name['first']) $account->name = ['first' => $parameters['name_first']] + $account->name;
if ($parameters['name_second'] !== $account->name['second']) $account->name = ['second' => $parameters['name_second']] + $account->name;
if ($parameters['name_last'] !== $account->name['last']) $account->name = ['last' => $parameters['name_last']] + $account->name;
if ($parameters['number'] !== $account->number)
if (mb_strlen($parameters['number']) === 11) $account->number = $parameters['number'];
else throw new exception('Номер должен состоять из 11 символов');
if ($parameters['mail'] !== $account->mail) $account->mail = $parameters['mail'];
if (!empty($parameters['password']) && !sodium_crypto_pwhash_str_verify($parameters['password'], $account->password) && $password = true)
if (mb_strlen($parameters['password']) > 6) $account->password = sodium_crypto_pwhash_str(
$parameters['password'],
SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE,
SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE
);
else throw new exception('Пароль должен быть длиннее 6 символов');
if ($parameters['commentary'] !== $account->commentary) $account->commentary = $parameters['commentary'];
if (_core::update($account)) {
// Записаны данные аккаунта
if ($account->type === 'worker') {
// Сотрудник
// Инициализация строки в глобальную переменную шаблонизатора
$this->view->rows = registry::workers(
before: sprintf(
"FILTER a._id == '%s' && a.deleted != true",
$account->getId()
),
after: <<<AQL
let account = (IS_SAME_COLLECTION('account', a) ? a : b)
let worker = (IS_SAME_COLLECTION('worker', a) ? a : b)
FILTER account.type == 'worker' && b.deleted != true
AQL,
amount: 1
);
} else if ($account->type === 'market') {
// Магазин
// Инициализация строки в глобальную переменную шаблонизатора
$this->view->rows = registry::markets(
before: sprintf(
"FILTER a._id == '%s' && a.deleted != true",
$account->getId()
),
after: <<<AQL
let account = (IS_SAME_COLLECTION('account', a) ? a : b)
let market = (IS_SAME_COLLECTION('market', a) ? a : b)
FILTER account.type == 'market' && b.deleted != true
AQL,
amount: 1
);
} else {
// Администратор или оператор (подразумевается)
// Инициализация строки в глобальную переменную шаблонизатора
$this->view->rows = [['account' => $account->getAll()]];
}
// Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение)
$this->view->page = null;
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'updated' => true,
'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . $account->type . 's.html'),
'errors' => self::parse_only_text($this->errors)
];
if ($password) $return['clipboard'] = <<<TEXT
Идентификатор: {$account->getKey()}
Пароль: {$parameters['password']}
TEXT;
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не удалось записать изменения в базу данных');
} else throw new exception('Не удалось найти аккаунт');
}
// Возврат (провал)
return null;
}
/**
* Пометить удалённым
*
* @param array $parameters Параметры запроса
*/
public function delete(array $parameters = []): ?string
{
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
// Инициализация данных аккаунта
$account = model::read('d._key == "' . $parameters['id'] . '"');
if (!empty($account)) {
// Найден аккаунт
// Удаление
$account->active = false;
$account->deleted = true;
if ($account->type === 'worker') {
// Сотрудник
// Инициализация сотрудника
if (empty($worker = model::worker($account->getId()))) throw new exception('Не удалось инициализировать сотрудника');
// Удаление
$worker->active = false;
$worker->deleted = true;
// Запись в ArangoDB
if (!_core::update($worker)) throw throw new exception('Не удалось записать изменения в базу данных');
} else if ($account->type === 'market') {
// Магазин
// Инициализация магазина
if (empty($market = model::market($account->getId()))) throw new exception('Не удалось инициализировать магазин');
// Удаление
$market->active = false;
$market->deleted = true;
// Запись в ArangoDB
if (!_core::update($market)) throw throw new exception('Не удалось записать изменения в базу данных');
}
if (_core::update($account)) {
// Записаны данные аккаунта
// Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение)
$this->view->page = null;
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode([
'deleted' => true,
'errors' => self::parse_only_text($this->errors)
]);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не удалось записать изменения в базу данных');
} else throw new exception('Не удалось найти аккаунт');
}
// Возврат (провал)
return null;
}
}

View File

@ -7,10 +7,11 @@ namespace mirzaev\ebala\controllers;
// Файлы проекта
use mirzaev\ebala\controllers\core,
mirzaev\ebala\controllers\traits\errors,
mirzaev\ebala\models\account,
mirzaev\ebala\models\registry;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Встроенные библиотеки
use exception;
/**
* Контроллер администратора
@ -30,10 +31,10 @@ final class administrator extends core
public function index(array $parameters = []): ?string
{
// Авторизация
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'administrator')) {
// Авторизован аккаунт оператора или администратора
if ($this->account->status() && $this->account->type === 'administrator') {
// Авторизован аккаунт администратора
foreach (['confirmed', 'waiting', 'published', 'unpublished', 'problematic', 'hided', 'completed'] as $name) {
foreach (['active', 'inactive'] as $name) {
// Перебор фильтров статусов
// Инициализация значения (приоритет у cookie)
@ -70,62 +71,89 @@ final class administrator extends core
*/
public function read(array $parameters = []): ?string
{
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'administrator')) {
// Авторизован аккаунт оператора или администратора
if ($this->account->status() && $this->account->type === 'administrator') {
// Авторизован аккаунт администратора
// Реинициализация актуальной страницы
if (isset($parameters['page'])) $this->session->write(['administrators' => ['page' => $parameters['page']]]);
else if (empty($this->session->buffer[$_SERVER['INTERFACE']]['administrators']['page'])) $this->session->write(['administrators' => ['page' => 1]]);
// Инициализация буфера AQL-выражения для инъекции фильтра по интервалу
$polysemantic = '';
// Инициализация буферов AQL-выражений для инъекции фильтра по статусам
$filters_statuses_and = '';
$filters_statuses_or = '';
// Инициализация допустимых статусов
$statuses = ['active', 'inactive', 'fined', 'decent', 'hided', 'fired'];
// Инициализация буфера AQL-выражения для инъекции фильтра по статусам (И)
$statuses_and = '';
foreach ($statuses as $name) {
foreach (['active', 'inactive'] as $name) {
// Перебор фильтров статусов (И)
// Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue)
if (empty($value = $_COOKIE["administrators_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['administrators']['filters'][$name] ?? 0)) continue;
// Конвертация ярлыков
$converted = match ($name) {
'inactive' => 'active',
default => $name
};
// Генерация выражения
$expression = "account.$converted == " . ($name === $converted ? 'true' : 'false');
// Генерация AQL-выражения для инъекции в строку запроса
if ($value === '1') $statuses_and .= " && administrator.$name == true";
if ($value === '1') $filters_statuses_and .= " && " . $expression;
else if ($value === '2') $filters_statuses_or .= " || " . $expression;
}
// Очистка от бинарных операторов сравнения с только одним операндом (крайние)
$statuses_and = trim(trim(trim($statuses_and), '&&'));
// Инициализация буфера AQL-выражения для инъекции фильтра по статусам (ИЛИ)
$statuses_or = '';
foreach ($statuses as $name) {
// Перебор фильтров статусов (ИЛИ)
// Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue)
if (empty($value = $_COOKIE["administrators_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['administrators']['filters'][$name] ?? 0)) continue;
// Генерация AQL-выражения для инъекции в строку запроса
if ($value === '2') $statuses_or .= " || administrator.$name == true";
}
// Очистка от бинарных операторов сравнения с только одним операндом (крайние)
$statuses_or = trim(trim(trim($statuses_or), '||'));
$filters_statuses_and = trim(trim(trim($filters_statuses_and), '&&'));
$filters_statuses_or = trim(trim(trim($filters_statuses_or), '||'));
// Инициализация буфера с объёдинёнными буферами c AQL-выражениям "И" и "ИЛИ"
$statuses_merged = (empty($statuses_and) ? '' : "($statuses_and)") . (empty($statuses_or) ? '' : (empty($statuses_and) ? '' : ' || ') . "($statuses_or)");
$filters_statuses_merged = (empty($filters_statuses_and) ? '' : "($filters_statuses_and)") . (empty($filters_statuses_or) ? '' : (empty($filters_statuses_and) ? '' : ' || ') . "($filters_statuses_or)");
// Инициализация общего буфера с AQL-выражениями
$filters = '';
// Объединение фильров в единую строку с AQL-выражениями для инъекции
if (!empty($statuses_merged)) $filters .= empty($filters) ? $statuses_merged : " && ($statuses_merged)";
// Объединение фильтров в единую строку с AQL-выражениями для инъекции
if (!empty($filters_statuses_merged)) $filters .= empty($filters) ? $filters_statuses_merged : " && ($filters_statuses_merged)";
// Инициализация строки поиска
$search = $_COOKIE["administrators_filter_search"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['administrators']['filters']['search'] ?? '';
if (mb_strlen($search) < 3) $search = null;
$search_query = empty($search)
? null
: <<<AQL
SEARCH
account.commentary IN TOKENS(@search, 'text_ru')
|| STARTS_WITH(account._key, @search)
|| STARTS_WITH(account.name.first, @search)
|| STARTS_WITH(account.name.second, @search)
|| STARTS_WITH(account.name.last, @search)
|| STARTS_WITH(account.number, @search)
|| STARTS_WITH(account.mail, @search)
|| (LENGTH(@search) > 6 && LEVENSHTEIN_MATCH(account._key, TOKENS(@search, 'text_en')[0], 2, true))
|| (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(account.name.first, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(account.name.second, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(account.name.last, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 6 && LEVENSHTEIN_MATCH(account.number, TOKENS(@search, 'text_en')[0], 2, true))
|| (LENGTH(@search) > 6 && LEVENSHTEIN_MATCH(account.mail, TOKENS(@search, 'text_en')[0], 2, true))
AQL;
// Инициализация данных для генерации HTML-документа с таблицей
$this->view->rows = registry::administrators(before: empty($filters) ? null : "FILTER ($filters)", page: (int) $this->session->buffer[$_SERVER['INTERFACE']]['administrators']['page']);
$this->view->rows = registry::administrators(
before: sprintf(
<<<AQL
%s
FILTER account.type == 'administrator' && account.deleted != true%s
AQL,
$search_query,
empty($filters) ? null : " && ($filters)"
),
page: (int) $this->session->buffer[$_SERVER['INTERFACE']]['administrators']['page'],
target: empty($search) ? account::COLLECTION : 'registry_accounts',
binds: empty($search) ? [] : [
'search' => $search
]
);
// Запись в cookie (только таким методом можно записать "hostonly: true")
setcookie(
@ -150,4 +178,93 @@ final class administrator extends core
// Возврат (провал)
return null;
}
/**
* Создать
*
* @param array $parameters Параметры запроса
*
* @return void В буфер вывода JSON-документ с запрашиваемыми параметрами
*/
public function create(array $parameters = []): void
{
if ($this->account->status() && $this->account->type === 'administrator') {
// Авторизован аккаунт администратора
// Инициализация буфера ошибок
$this->errors['account'] ??= [];
try {
// Проверка наличия переданного пароля
if (!isset($parameters['password'])) throw new exception('Не получен пароль');
else if (strlen($parameters['password']) < 6) throw new exception('Пароль должен быть не менее 6 символов');
else if (!empty($parameters['number']) && strlen($parameters['number']) < 11) throw new exception('Несоответствие формату SIM-номера');
else if (!empty($parameters['mail']) && preg_match('/^.+@.+\.\w+$/', $parameters['mail']) === 0) throw new exception('Несоответствие формату почты');
// Универсализация
/* $parameters['number'] = (int) $parameters['number']; */
// Создание аккаунта
$account = account::create(
data: [
'type' => 'administrator',
'name' => [
'first' => $parameters['name_first'],
'second' => $parameters['name_second'],
'last' => $parameters['name_last']
],
'number' => $parameters['number'] === 0 ? '' : $parameters['number'],
'mail' => $parameters['mail'],
'password' => sodium_crypto_pwhash_str(
$parameters['password'],
SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE,
SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE
),
'commentary' => $parameters['commentary']
],
errors: $this->errors['account']
);
// Проверка существования созданного аккаунта
if (empty($account)) throw new exception('Не удалось создать аккаунт');
} catch (exception $e) {
// Write to the errors registry
$this->errors['account'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Инициализация идентификатора аккаунта (ключ документа инстанции аккаунта в базе данных)
$_key = preg_replace('/.+\//', '', $account ?? '');
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode(
[
'clipboard' => empty($this->errors['account']) ? <<<TEXT
Идентификатор: $_key
Пароль: {$parameters['password']}
TEXT : '',
'errors' => self::parse_only_text($this->errors['account'])
]
);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
}
}

View File

@ -37,11 +37,6 @@ class core extends controller
*/
protected readonly ?account $account;
/**
* Инстанция магазина
*/
protected readonly ?market $market;
/**
* Реестр ошибок
*/
@ -75,9 +70,9 @@ class core extends controller
$_COOKIE["session"] ??= null;
// Инициализация сессии
$this->session = new session($_COOKIE["session"], $expires);
if ($_COOKIE["session"] !== ($this->session->hash)) {
$this->session = new session($_COOKIE["session"], $expires, $this->errors['session']);
if (!empty($this->errors['session'])) die;
else if ($_COOKIE["session"] !== $this->session->hash) {
// Изменился хеш сессии (подразумевается, что сессия устарела)
// Запись хеша новой сессии
@ -100,9 +95,6 @@ class core extends controller
if ($this->account->status()) {
// Инициализирован аккаунт
// Инициализация магазина
if ($this->account->type === 'market') $this->market = new market(account::market($this->account->getId()));
if ($this->account->type !== $_SERVER['INTERFACE']) {
// Не соответствие типа аккаунта к запрошенному интерфейсу (например, если оператор зашел на интерфейс магазина)
@ -141,7 +133,7 @@ class core extends controller
// Оператор
// Инициализация данных аккаунтов для генерации представления
$this->view->accounts = account::read('d.type == "operator" && d.status == "active"', 'd.name.first DESC', 100, 1, errors: $this->errors['account']);
$this->view->accounts = account::read('d.type == "operator" && d.active == true', 'd.name.first DESC, d._key DESC', 100, 1, errors: $this->errors['account']);
// Преобразование в массив, если вернуло инстанцию одного документа, вместо массива инстанций документов
if (!is_array($this->view->accounts)) $this->view->accounts = [$this->view->accounts];
@ -149,7 +141,7 @@ class core extends controller
// Магазин
// Инициализация данных аккаунтов для генерации представления
$this->view->accounts = account::read('d.type == "market" && d.status == "active"', 'd.name.first DESC', 100, 1, $this->errors['account']);
$this->view->accounts = account::read('d.type == "market" && d.active == true', 'd.name.first DESC, d._key DESC', 100, 1, errors: $this->errors['account']);
// Преобразование в массив, если вернуло инстанцию одного документа, вместо массива инстанций документов
if (!is_array($this->view->accounts)) $this->view->accounts = [$this->view->accounts];
@ -158,7 +150,7 @@ class core extends controller
$buffer = [];
// Инициализация данных магазина для аккаунта для генерации представления
foreach ($this->view->accounts as $vendor) $buffer[] = ['vendor' => $vendor, 'market' => account::market($vendor->getId(), $this->errors['account'])];
foreach ($this->view->accounts as $vendor) $buffer[] = ['account' => $vendor, 'market' => account::market($vendor->getId(), errors: $this->errors['account'])];
// Запись в глобальную переменную из буфера
$this->view->accounts = $buffer;
@ -166,7 +158,7 @@ class core extends controller
// Администратор
// Инициализация данных аккаунтов для генерации представления
$this->view->accounts = account::read('d.type == "administrator" && d.status == "active"', 'd.name.first DESC', 100, 1, errors: $this->errors['account']);
$this->view->accounts = account::read('d.type == "administrator" && d.active == true', 'd.name.first DESC, d._key DESC', 100, 1, errors: $this->errors['account']);
// Преобразование в массив, если вернуло инстанцию одного документа, вместо массива инстанций документов
if (!is_array($this->view->accounts)) $this->view->accounts = [$this->view->accounts];

View File

@ -66,6 +66,19 @@ final class index extends core
return null;
}
/**
* Заглушка для отправки формы при аутентификации
*
* Используется для того, чтобы отработали события браузера связанные с отправкой формы
*
* @param array $parameters Параметры запроса
*/
public function entry(array $parameters = []): ?string
{
// Возврат (успех)
return 'хули смотришь сюда выродок';
}
/**
* Main menu
*
@ -76,8 +89,11 @@ final class index extends core
if ($this->account->status()) {
// Авторизован аккаунт
// Генерация представления
// Генерация и возврат (успех)
return $this->view->render(DIRECTORY_SEPARATOR . 'menu.html');
}
// Возврат (провал)
return null;
}
}

View File

@ -8,11 +8,17 @@ namespace mirzaev\ebala\controllers;
use mirzaev\ebala\controllers\core,
mirzaev\ebala\controllers\traits\errors,
mirzaev\ebala\models\registry,
mirzaev\ebala\models\market as model;
mirzaev\ebala\models\account,
mirzaev\ebala\models\market as model,
mirzaev\ebala\models\core as _core;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Встроенные библиотеки
use exception,
datetime;
/**
* Контроллер магазина
*
@ -34,7 +40,7 @@ final class market extends core
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт оператора или администратора
foreach (['confirmed', 'waiting', 'published', 'unpublished', 'problematic', 'hided', 'completed'] as $name) {
foreach (['active', 'inactive'] as $name) {
// Перебор фильтров статусов
// Инициализация значения (приоритет у cookie)
@ -52,7 +58,7 @@ final class market extends core
};
}
// Генерация представлениямя
// Генерация представления
$main = $this->view->render(DIRECTORY_SEPARATOR . 'pages' . DIRECTORY_SEPARATOR . 'markets.html');
} else $main = $this->authorization();
@ -78,55 +84,110 @@ final class market extends core
if (isset($parameters['page'])) $this->session->write(['markets' => ['page' => $parameters['page']]]);
else if (empty($this->session->buffer[$_SERVER['INTERFACE']]['markets']['page'])) $this->session->write(['markets' => ['page' => 1]]);
// Инициализация буфера AQL-выражения для инъекции фильтра по интервалу
$polysemantic = '';
// Инициализация буферов AQL-выражений для инъекции фильтра по статусам
$filters_statuses_before_and = '';
$filters_statuses_before_or = '';
$filters_statuses_after_and = '';
$filters_statuses_after_or = '';
// Инициализация допустимых статусов
$statuses = ['active', 'inactive', 'fined', 'decent', 'hided', 'fired'];
// Инициализация буфера AQL-выражения для инъекции фильтра по статусам (И)
$statuses_and = '';
foreach ($statuses as $name) {
// Перебор фильтров статусов (И)
foreach (['active', 'inactive'] as $name) {
// Перебор фильтров статусов
// Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue)
if (empty($value = $_COOKIE["markets_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['markets']['filters'][$name] ?? 0)) continue;
// Конвертация ярлыков
$converted = match ($name) {
'inactive' => 'active',
default => $name
};
// Принадлежность фильтра
$target = match ($converted) {
'active' => 'account',
default => 'market'
};
// Генерация выражения
$expression = "$target.$converted == " . ($name === $converted ? 'true' : 'false');
// Генерация AQL-выражения для инъекции в строку запроса
if ($value === '1') $statuses_and .= " && market.$name == true";
if ($value === '1') ${'filters_statuses_' . ($target === 'account' ? 'before' : 'after') . '_and'} .= " && " . $expression;
else if ($value === '2') ${'filters_statuses_' . ($target === 'account' ? 'before' : 'after') . '_or'} .= " || " . $expression;
}
// Очистка от бинарных операторов сравнения с только одним операндом (крайние)
$statuses_and = trim(trim(trim($statuses_and), '&&'));
// Инициализация буфера AQL-выражения для инъекции фильтра по статусам (ИЛИ)
$statuses_or = '';
foreach ($statuses as $name) {
// Перебор фильтров статусов (ИЛИ)
// Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue)
if (empty($value = $_COOKIE["markets_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['markets']['filters'][$name] ?? 0)) continue;
// Генерация AQL-выражения для инъекции в строку запроса
if ($value === '2') $statuses_or .= " || market.$name == true";
}
// Очистка от бинарных операторов сравнения с только одним операндом (крайние)
$statuses_or = trim(trim(trim($statuses_or), '||'));
$filters_statuses_before_and = trim(trim(trim($filters_statuses_before_and), '&&'));
$filters_statuses_before_or = trim(trim(trim($filters_statuses_before_or), '||'));
$filters_statuses_after_and = trim(trim(trim($filters_statuses_after_and), '&&'));
$filters_statuses_after_or = trim(trim(trim($filters_statuses_after_or), '||'));
// Инициализация буфера с объёдинёнными буферами c AQL-выражениям "И" и "ИЛИ"
$statuses_merged = (empty($statuses_and) ? '' : "($statuses_and)") . (empty($statuses_or) ? '' : (empty($statuses_and) ? '' : ' || ') . "($statuses_or)");
$filters_statuses_before_merged = (empty($filters_statuses_before_and) ? '' : "($filters_statuses_before_and)") . (empty($filters_statuses_before_or) ? '' : (empty($filters_statuses_before_and) ? '' : ' || ') . "($filters_statuses_before_or)");
$filters_statuses_after_merged = (empty($filters_statuses_after_and) ? '' : "($filters_statuses_after_and)") . (empty($filters_statuses_after_or) ? '' : (empty($filters_statuses_after_and) ? '' : ' || ') . "($filters_statuses_after_or)");
// Инициализация общего буфера с AQL-выражениями
$filters = '';
$filters_before = '';
$filters_after = '';
// Объединение фильров в единую строку с AQL-выражениями для инъекции
if (!empty($statuses_merged)) $filters .= empty($filters) ? $statuses_merged : " && ($statuses_merged)";
// Объединение фильтров в единую строку с AQL-выражениями для инъекции
if (!empty($filters_statuses_before_merged)) $filters_before .= empty($filters_before) ? $filters_statuses_before_merged : " && ($filters_statuses_before_merged)";
if (!empty($filters_statuses_after_merged)) $filters_after .= empty($filters_after) ? $filters_statuses_after_merged : " && ($filters_statuses_after_merged)";
// Инициализация строки поиска
$search = $_COOKIE["markets_filter_search"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['markets']['filters']['search'] ?? '';
if (mb_strlen($search) < 3) $search = null;
$search_query = empty($search)
? null
: <<<AQL
SEARCH
a.commentary IN TOKENS(@search, 'text_ru')
|| a.address IN TOKENS(@search, 'text_ru')
|| STARTS_WITH(a._key, @search)
|| STARTS_WITH(a.name.first, @search)
|| STARTS_WITH(a.name.second, @search)
|| STARTS_WITH(a.name.last, @search)
|| STARTS_WITH(a.address, @search)
|| STARTS_WITH(a.city, @search)
|| STARTS_WITH(a.number, @search)
|| STARTS_WITH(a.mail, @search)
|| (LENGTH(@search) > 5 && LEVENSHTEIN_MATCH(a._key, TOKENS(@search, 'text_en')[0], 2, true))
|| (LENGTH(@search) > 3 && LEVENSHTEIN_MATCH(a.name.first, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 3 && LEVENSHTEIN_MATCH(a.name.second, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 3 && LEVENSHTEIN_MATCH(a.name.last, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 7 && LEVENSHTEIN_MATCH(a.address, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(a.city, TOKENS(@search, 'text_ru')[0], 1, true))
|| (LENGTH(@search) > 5 && LEVENSHTEIN_MATCH(a.number, TOKENS(@search, 'text_en')[0], 2, true))
|| (LENGTH(@search) > 5 && LEVENSHTEIN_MATCH(a.mail, TOKENS(@search, 'text_en')[0], 2, true))
OPTIONS { collections: ["account", "market"] }
AQL;
// Инициализация данных для генерации HTML-документа с таблицей
$this->view->rows = registry::markets(before: empty($filters) ? null : "FILTER ($filters)", page: (int) $this->session->buffer[$_SERVER['INTERFACE']]['markets']['page']);
$this->view->rows = registry::markets(
before: sprintf(
<<<AQL
%s
FILTER a.deleted != true
AQL,
$search_query,
),
after: sprintf(
<<<AQL
let account = (IS_SAME_COLLECTION('account', a) ? a : b)
let market = (IS_SAME_COLLECTION('market', a) ? a : b)
FILTER account.type == 'market' && b.deleted != true
%s
%s
AQL,
empty($filters_before) ? null : "FILTER $filters_before",
empty($filters_after) ? null : "FILTER $filters_after"
),
page: (int) $this->session->buffer[$_SERVER['INTERFACE']]['markets']['page'],
target: empty($search) ? account::COLLECTION : 'registry_accounts',
binds: empty($search) ? [] : [
'search' => $search
]
);
// Запись в cookie (только таким методом можно записать "hostonly: true")
setcookie(
@ -152,6 +213,254 @@ final class market extends core
return null;
}
/**
* Создать
*
* @param array $parameters Параметры запроса
*
* @return void В буфер вывода JSON-документ с запрашиваемыми параметрами
*/
public function create(array $parameters = []): void
{
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
// Инициализация буфера ошибок
$this->errors['account'] ??= [];
try {
// Проверка наличия переданного пароля
if (!isset($parameters['account_password'])) throw new exception('Не получен пароль');
else if (strlen($parameters['account_password']) < 6) throw new exception('Пароль должен быть не менее 6 символов');
else if (!empty($parameters['market_number']) && strlen($parameters['market_number']) < 11) throw new exception('Несоответствие формату SIM-номера представителя');
else if (!empty($parameters['account_number']) && strlen($parameters['account_number']) < 11) throw new exception('Несоответствие формату SIM-номера аккаунта представителя');
else if (!empty($parameters['market_mail']) && preg_match('/^.+@.+\.\w+$/', $parameters['market_mail']) === 0) throw new exception('Несоответствие формату почты представителя');
else if (!empty($parameters['account_mail']) && preg_match('/^.+@.+\.\w+$/', $parameters['account_mail']) === 0) throw new exception('Несоответствие формату почты аккаунта представителя');
// Универсализация
/* $parameters['market_number'] = (int) $parameters['market_number']; */
/* $parameters['account_number'] = (int) $parameters['account_number']; */
// Создание аккаунта
$account = account::create(
data: [
'type' => 'market',
'name' => [
'first' => $parameters['account_name_first'],
'second' => $parameters['account_name_second'],
'last' => $parameters['account_name_last']
],
'number' => $parameters['account_number'] === 0 ? '' : $parameters['account_number'],
'mail' => $parameters['account_mail'],
'password' => sodium_crypto_pwhash_str(
$parameters['account_password'],
SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE,
SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE
),
'commentary' => $parameters['account_commentary']
],
errors: $this->errors['account']
);
// Проверка существования созданного аккаунта
if (empty($account)) throw new exception('Не удалось создать аккаунт');
} catch (exception $e) {
// Write to the errors registry
$this->errors['account'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Инициализация идентификатора аккаунта (ключ документа инстанции аккаунта в базе данных)
$_key = preg_replace('/.+\//', '', $account ?? '');
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode(
[
'clipboard' => empty($this->errors['account']) ? <<<TEXT
Идентификатор: $_key
Пароль: {$parameters['account_password']}
TEXT : '',
'errors' => self::parse_only_text($this->errors['account'])
]
);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
try {
// Создание магазина
$market = model::create(
data: [
'name' => [
'first' => $parameters['market_name_first'],
'second' => $parameters['market_name_second'],
'last' => $parameters['market_name_last']
],
'number' => $parameters['market_number'] === 0 ? '' : $parameters['market_number'],
'mail' => $parameters['market_mail'],
'type' => $parameters['market_type'],
'city' => $parameters['market_city'],
'district' => $parameters['market_district'],
'address' => $parameters['market_address'],
],
errors: $this->errors['account']
);
// Проверка существования созданного магазина
if (empty($market)) throw new exception('Не удалось создать магазин');
// Создание ребра: account -> market
account::connect($account, $market, 'market', $this->errors['account']);
} catch (exception $e) {
// Write to the errors registry
$this->errors['account'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
}
}
/**
* Прочитать данные
*
* @param array $parameters Параметры запроса
*/
public function fields(array $parameters = []): ?string
{
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
// Инициализация данных магазина
$market = model::read('d._key == "' . $parameters['id'] . '"', return: '{ name: d.name, number: d.number, mail: d.mail, type: d.type, city: d.city, district: d.district, address: d.address}')->getAll();
if (!empty($market)) {
// Найдены данные магазина
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode($market);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
}
// Возврат (провал)
return null;
}
/**
* Обновить данные
*
* @param array $parameters Параметры запроса
*/
public function update(array $parameters = []): ?string
{
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
// Инициализация данных магазина
$market = model::read('d._key == "' . $parameters['id'] . '"');
if (!empty($market)) {
// Найден магазин
// Инициализация параметров (перезапись переданными значениями)
if ($parameters['name_first'] !== $market->name['first']) $market->name = ['first' => $parameters['name_first']] + $market->name;
if ($parameters['name_second'] !== $market->name['second']) $market->name = ['second' => $parameters['name_second']] + $market->name;
if ($parameters['name_last'] !== $market->name['last']) $market->name = ['last' => $parameters['name_last']] + $market->name;
if ($parameters['number'] !== $market->number)
if (mb_strlen($parameters['number']) === 11) $market->number = $parameters['number'];
else throw new exception('Номер должен состоять из 11 символов');
if ($parameters['mail'] !== $market->mail) $market->mail = $parameters['mail'];
if ($parameters['type'] !== $market->type) $market->type = $parameters['type'];
if ($parameters['city'] !== $market->city) $market->city = $parameters['city'];
if ($parameters['district'] !== $market->district) $market->district = $parameters['district'];
if ($parameters['address'] !== $market->address) $market->address = $parameters['address'];
if (_core::update($market)) {
// Записаны данные магазина
// Инициализация строки в глобальную переменную шаблонизатора
$this->view->rows = registry::markets(
before: sprintf(
"FILTER a._id == '%s' && a.deleted != true",
$market->getId()
),
after: <<<AQL
let account = (IS_SAME_COLLECTION('account', a) ? a : b)
let market = (IS_SAME_COLLECTION('market', a) ? a : b)
FILTER account.type == 'market' && b.deleted != true
AQL,
amount: 1,
target: model::COLLECTION
);
// Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение)
$this->view->page = null;
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'updated' => true,
'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'markets.html'),
'errors' => self::parse_only_text($this->errors)
];
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не удалось записать изменения в базу данных');
} else throw new exception('Не удалось найти аккаунт');
}
// Возврат (провал)
return null;
}
/**
* Прочитать данные магазинов для <datalist>
*
@ -163,7 +472,7 @@ final class market extends core
// Авторизован аккаунт оператора или магазина
// Инициализация данных магазинов
$this->view->markets = model::read(filter: 'd.status == "active"', amount: 10000, return: '{ id: d.id, director: d.director }');
$this->view->markets = model::read(filter: 'd.active == true', amount: 10000, return: '{ _key: d._key, name: d.name }');
// Универсализация
if ($this->view->markets instanceof _document) $this->view->markets = [$this->view->markets];

View File

@ -7,10 +7,11 @@ namespace mirzaev\ebala\controllers;
// Файлы проекта
use mirzaev\ebala\controllers\core,
mirzaev\ebala\controllers\traits\errors,
mirzaev\ebala\models\account,
mirzaev\ebala\models\registry;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Встроенные библиотеки
use exception;
/**
* Контроллер оператора
@ -33,7 +34,7 @@ final class operator extends core
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт оператора или администратора
foreach (['confirmed', 'waiting', 'published', 'unpublished', 'problematic', 'hided', 'completed'] as $name) {
foreach (['confirmed', 'waiting'] as $name) {
// Перебор фильтров статусов
// Инициализация значения (приоритет у cookie)
@ -77,55 +78,81 @@ final class operator extends core
if (isset($parameters['page'])) $this->session->write(['operators' => ['page' => $parameters['page']]]);
else if (empty($this->session->buffer[$_SERVER['INTERFACE']]['operators']['page'])) $this->session->write(['operators' => ['page' => 1]]);
// Инициализация буфера AQL-выражения для инъекции фильтра по интервалу
$polysemantic = '';
// Инициализация буферов AQL-выражений для инъекции фильтра по статусам
$filters_statuses_and = '';
$filters_statuses_or = '';
// Инициализация допустимых статусов
$statuses = ['active', 'inactive', 'fined', 'decent', 'hided', 'fired'];
// Инициализация буфера AQL-выражения для инъекции фильтра по статусам (И)
$statuses_and = '';
foreach ($statuses as $name) {
foreach (['active', 'inactive'] as $name) {
// Перебор фильтров статусов (И)
// Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue)
if (empty($value = $_COOKIE["operators_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['operators']['filters'][$name] ?? 0)) continue;
// Конвертация ярлыков
$converted = match ($name) {
'inactive' => 'active',
default => $name
};
// Генерация выражения
$expression = "account.$converted == " . ($name === $converted ? 'true' : 'false');
// Генерация AQL-выражения для инъекции в строку запроса
if ($value === '1') $statuses_and .= " && operator.$name == true";
if ($value === '1') $filters_statuses_and .= " && " . $expression;
else if ($value === '2') $filters_statuses_or .= " || " . $expression;
}
// Очистка от бинарных операторов сравнения с только одним операндом (крайние)
$statuses_and = trim(trim(trim($statuses_and), '&&'));
// Инициализация буфера AQL-выражения для инъекции фильтра по статусам (ИЛИ)
$statuses_or = '';
foreach ($statuses as $name) {
// Перебор фильтров статусов (ИЛИ)
// Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue)
if (empty($value = $_COOKIE["operators_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['operators']['filters'][$name] ?? 0)) continue;
// Генерация AQL-выражения для инъекции в строку запроса
if ($value === '2') $statuses_or .= " || operator.$name == true";
}
// Очистка от бинарных операторов сравнения с только одним операндом (крайние)
$statuses_or = trim(trim(trim($statuses_or), '||'));
$filters_statuses_and = trim(trim(trim($filters_statuses_and), '&&'));
$filters_statuses_or = trim(trim(trim($filters_statuses_or), '||'));
// Инициализация буфера с объёдинёнными буферами c AQL-выражениям "И" и "ИЛИ"
$statuses_merged = (empty($statuses_and) ? '' : "($statuses_and)") . (empty($statuses_or) ? '' : (empty($statuses_and) ? '' : ' || ') . "($statuses_or)");
$filters_statuses_merged = (empty($filters_statuses_and) ? '' : "($filters_statuses_and)") . (empty($filters_statuses_or) ? '' : (empty($filters_statuses_and) ? '' : ' || ') . "($filters_statuses_or)");
// Инициализация общего буфера с AQL-выражениями
$filters = '';
// Объединение фильров в единую строку с AQL-выражениями для инъекции
if (!empty($statuses_merged)) $filters .= empty($filters) ? $statuses_merged : " && ($statuses_merged)";
// Объединение фильтров в единую строку с AQL-выражениями для инъекции
if (!empty($filters_statuses_merged)) $filters .= empty($filters) ? $filters_statuses_merged : " && ($filters_statuses_merged)";
// Инициализация строки поиска
$search = $_COOKIE["operators_filter_search"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['operators']['filters']['search'] ?? '';
if (mb_strlen($search) < 3) $search = null;
$search_query = empty($search)
? null
: <<<AQL
SEARCH
account.commentary IN TOKENS(@search, 'text_ru')
|| STARTS_WITH(account._key, @search)
|| STARTS_WITH(account.name.first, @search)
|| STARTS_WITH(account.name.second, @search)
|| STARTS_WITH(account.name.last, @search)
|| STARTS_WITH(account.number, @search)
|| STARTS_WITH(account.mail, @search)
|| (LENGTH(@search) > 6 && LEVENSHTEIN_MATCH(account._key, TOKENS(@search, 'text_en')[0], 2, true))
|| (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(account.name.first, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(account.name.second, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(account.name.last, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 6 && LEVENSHTEIN_MATCH(account.number, TOKENS(@search, 'text_en')[0], 2, true))
|| (LENGTH(@search) > 6 && LEVENSHTEIN_MATCH(account.mail, TOKENS(@search, 'text_en')[0], 2, true))
AQL;
// Инициализация данных для генерации HTML-документа с таблицей
$this->view->rows = registry::operators(before: empty($filters) ? null : "FILTER ($filters)", page: (int) $this->session->buffer[$_SERVER['INTERFACE']]['operators']['page']);
$this->view->rows = registry::operators(
before: sprintf(
<<<AQL
%s
FILTER account.type == 'operator' && account.deleted != true%s
AQL,
$search_query,
empty($filters) ? null : " && ($filters)"
),
page: (int) $this->session->buffer[$_SERVER['INTERFACE']]['operators']['page'],
target: empty($search) ? account::COLLECTION : 'registry_accounts',
binds: empty($search) ? [] : [
'search' => $search
]
);
// Запись в cookie (только таким методом можно записать "hostonly: true")
setcookie(
@ -150,4 +177,93 @@ final class operator extends core
// Возврат (провал)
return null;
}
/**
* Создать
*
* @param array $parameters Параметры запроса
*
* @return void В буфер вывода JSON-документ с запрашиваемыми параметрами
*/
public function create(array $parameters = []): void
{
if ($this->account->status() && $this->account->type === 'administrator') {
// Авторизован аккаунт администратора
// Инициализация буфера ошибок
$this->errors['account'] ??= [];
try {
// Проверка наличия переданного пароля
if (!isset($parameters['password'])) throw new exception('Не получен пароль');
else if (strlen($parameters['password']) < 6) throw new exception('Пароль должен быть не менее 6 символов');
else if (!empty($parameters['number']) && strlen($parameters['number']) < 11) throw new exception('Несоответствие формату SIM-номера');
else if (!empty($parameters['mail']) && preg_match('/^.+@.+\.\w+$/', $parameters['mail']) === 0) throw new exception('Несоответствие формату почты');
// Универсализация
/* $parameters['number'] = (int) $parameters['number']; */
// Создание аккаунта
$account = account::create(
data: [
'type' => 'operator',
'name' => [
'first' => $parameters['name_first'],
'second' => $parameters['name_second'],
'last' => $parameters['name_last']
],
'number' => $parameters['number'] === 0 ? '' : $parameters['number'],
'mail' => $parameters['mail'],
'password' => sodium_crypto_pwhash_str(
$parameters['password'],
SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE,
SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE
),
'commentary' => $parameters['commentary']
],
errors: $this->errors['account']
);
// Проверка существования созданного аккаунта
if (empty($account)) throw new exception('Не удалось создать аккаунт');
} catch (exception $e) {
// Write to the errors registry
$this->errors['account'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Инициализация идентификатора аккаунта (ключ документа инстанции аккаунта в базе данных)
$_key = preg_replace('/.+\//', '', $account ?? '');
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode(
[
'clipboard' => empty($this->errors['account']) ? <<<TEXT
Идентификатор: $_key
Пароль: {$parameters['password']}
TEXT : '',
'errors' => self::parse_only_text($this->errors['account'])
]
);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
}
}

View File

@ -55,11 +55,11 @@ final class session extends core
$parameters['worker'] = isset($matches[1]) ? 7 . $matches[1] : $parameters['worker'];
// Вычисление длины
$length = strlen($parameters['worker']);
$length = mb_strlen($parameters['worker']);
// Проверка параметров на соответствование требованиям
if ($length === 0) throw new exception('Номер не может быть пустым');
if ($length != 11) throw new exception('Номер должен иметь 11 цифр');
if ($length !== 11) throw new exception('Номер должен иметь 11 цифр');
if (preg_match_all('/[^\d\(\)\-\s\r\n\t\0]+/u', $parameters['worker'], $matches) > 0) throw new exception('Нельзя использовать символы: ' . implode(', ', ...$matches));
if ($remember = isset($parameters['remember']) && $parameters['remember'] === '1') {
@ -157,11 +157,11 @@ final class session extends core
$parameters['administrator'] = $matches[0];
// Вычисление длины
$length = strlen($parameters['administrator']);
$length = mb_strlen($parameters['administrator']);
// Проверка параметров на соответствование требованиям
if ($length === 0) throw new exception('Идентификатор не может быть пустым');
if ($length > 12) throw new exception('Идентификатор должен иметь не более 12 цифр');
if ($length === 0) throw new exception('Идентификатор аккаунта не может быть пустым');
if ($length > 12) throw new exception('Идентификатор аккаунта должен иметь не более 12 цифр');
if (preg_match_all('/[^\d\(\)\-\s\r\n\t\0]+/u', $parameters['administrator'], $matches) > 0) throw new exception('Нельзя использовать символы: ' . implode(', ', ...$matches));
if ($remember = isset($parameters['remember']) && $parameters['remember'] === '1') {
@ -263,11 +263,11 @@ final class session extends core
$parameters['operator'] = $matches[0];
// Вычисление длины
$length = strlen($parameters['operator']);
$length = mb_strlen($parameters['operator']);
// Проверка параметров на соответствование требованиям
if ($length === 0) throw new exception('Идентификатор не может быть пустым');
if ($length > 12) throw new exception('Идентификатор должен иметь не более 12 цифр');
if ($length === 0) throw new exception('Идентификатор аккаунта не может быть пустым');
if ($length > 12) throw new exception('Идентификатор аккаунта должен иметь не более 12 цифр');
if (preg_match_all('/[^\d\(\)\-\s\r\n\t\0]+/u', $parameters['operator'], $matches) > 0) throw new exception('Нельзя использовать символы: ' . implode(', ', ...$matches));
if ($remember = isset($parameters['remember']) && $parameters['remember'] === '1') {
@ -359,19 +359,18 @@ final class session extends core
if (empty($parameters['market'])) throw new exception('Необходимо передать идентификатор');
// Вычисление длины
$length = strlen($parameters['market']);
$length = mb_strlen($parameters['market']);
// Проверка параметров на соответствование требованиям
if ($parameters['market'][0] !== 'K') throw new exception('Идентификатор должен начинаться с английской буквы "K"');
if ($length <= 1) throw new exception('Идентификатор не может быть пустым');
if ($length > 40) throw new exception('Идентификатор должен иметь не более 40 символов');
if (preg_match_all('/[^\dK]+/u', $parameters['market'], $matches) > 0) throw new exception('Нельзя использовать символы: ' . implode(', ', ...$matches));
if ($length === 0) throw new exception('Идентификатор аккаунта аккаунта не может быть пустым');
if ($length > 40) throw new exception('Идентификатор аккаунта аккаунта должен иметь не более 40 символов');
if (preg_match_all('/[^\d\(\)\-\s\r\n\t\0]+/u', $parameters['market'], $matches) > 0) throw new exception('Нельзя использовать символы: ' . implode(', ', ...$matches));
if ($remember = isset($parameters['remember']) && $parameters['remember'] === '1') {
// Запрошено запоминание
// Запись в cookie
setcookie('entry_id', $parameters['market'], [
setcookie('entry__key', $parameters['market'], [
'expires' => strtotime('+1 day'),
'path' => '/',
'secure' => true,
@ -380,18 +379,15 @@ final class session extends core
]);
}
// Поиск магазина
$market = market::read('d.id == "' . $parameters['market'] . '"', amount: 1);
// Поиск аккаунта
$account = market::account($market->getId());
$account = account::read('d._key == "' . $parameters['market'] . '"', amount: 1);
// Генерация ответа по запрашиваемым параметрам
foreach ($return as $parameter) match ($parameter) {
'exist' => $buffer['exist'] = isset($account),
'account' => (function () use ($parameters, $remember, &$buffer) {
// Запись в буфер сессии
if ($remember) $this->session->write(['entry' => ['id' => $parameters['market']]], $this->errors);
if ($remember) $this->session->write(['entry' => ['_key' => $parameters['market']]], $this->errors);
// Поиск аккаунта и запись в буфер вывода
$buffer['account'] = (new account($this->session, 'market', $this->errors))?->instance() instanceof _document;
@ -433,7 +429,7 @@ final class session extends core
// Запись в буфер сессии
if (!in_array('account', $return, true) && ($remember ?? false))
$this->session->write(['entry' => ['id' => $parameters['market']]]);
$this->session->write(['entry' => ['_key' => $parameters['market']]]);
}
/**

File diff suppressed because it is too large Load Diff

View File

@ -7,12 +7,18 @@ namespace mirzaev\ebala\controllers;
// Файлы проекта
use mirzaev\ebala\controllers\core,
mirzaev\ebala\controllers\traits\errors,
mirzaev\ebala\models\account,
mirzaev\ebala\models\registry,
mirzaev\ebala\models\worker as model;
mirzaev\ebala\models\worker as model,
mirzaev\ebala\models\core as _core;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Встроенные библиотеки
use exception,
datetime;
/**
* Контроллер сотрудника
*
@ -78,72 +84,125 @@ final class worker extends core
if (isset($parameters['page'])) $this->session->write(['workers' => ['page' => $parameters['page']]]);
else if (empty($this->session->buffer[$_SERVER['INTERFACE']]['workers']['page'])) $this->session->write(['workers' => ['page' => 1]]);
// Инициализация буфера AQL-выражения для инъекции фильтра по интервалу
$polysemantic = '';
// Инициализация буферов AQL-выражений для инъекции фильтра по статусам
$filters_statuses_before_and = '';
$filters_statuses_before_or = '';
$filters_statuses_after_and = '';
$filters_statuses_after_or = '';
foreach (['rating'] as $name) {
// Перебор фильтров с произвольными значениями (И)
// Инициализация значения (приоритет у cookie)
$value = $_COOKIE["workers_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['workers']['filters'][$name] ?? null;
// Найдено значение?
if ($value === null) continue;
// Генерация AQL-выражения для инъекции в строку запроса
if ($name === 'rating' && $value > 0) $polysemantic .= " && worker.rating >= $value";
}
// Очистка от бинарных операторов сравнения с только одним операндом (крайние)
$polysemantic = trim(trim(trim($polysemantic), '&&'));
// Инициализация допустимых статусов
$statuses = ['active', 'inactive', 'fined', 'decent', 'hided', 'fired'];
// Инициализация буфера AQL-выражения для инъекции фильтра по статусам (И)
$statuses_and = '';
foreach ($statuses as $name) {
// Перебор фильтров статусов (И)
foreach (['active', 'inactive', 'fined', 'decent', 'hided', 'fired'] as $name) {
// Перебор фильтров статусов
// Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue)
if (empty($value = $_COOKIE["workers_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['workers']['filters'][$name] ?? 0)) continue;
// Конвертация ярлыков
$converted = match ($name) {
'inactive' => 'active',
'decent' => 'fined',
default => $name
};
$target = match ($converted) {
'active' => 'account',
default => 'worker'
};
// Генерация выражения
$expression = "$target.$converted == " . ($name === $converted ? 'true' : 'false');
// Генерация AQL-выражения для инъекции в строку запроса
if ($value === '1') $statuses_and .= " && worker.$name == true";
if ($value === '1') ${'filters_statuses_' . ($target === 'account' ? 'before' : 'after') . '_and'} .= " && " . $expression;
else if ($value === '2') ${'filters_statuses_' . ($target === 'account' ? 'before' : 'after') . '_or'} .= " || " . $expression;
}
// Очистка от бинарных операторов сравнения с только одним операндом (крайние)
$statuses_and = trim(trim(trim($statuses_and), '&&'));
// Инициализация буфера AQL-выражения для инъекции фильтра по статусам (ИЛИ)
$statuses_or = '';
foreach ($statuses as $name) {
// Перебор фильтров статусов (ИЛИ)
// Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue)
if (empty($value = $_COOKIE["workers_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['workers']['filters'][$name] ?? 0)) continue;
// Генерация AQL-выражения для инъекции в строку запроса
if ($value === '2') $statuses_or .= " || worker.$name == true";
}
// Очистка от бинарных операторов сравнения с только одним операндом (крайние)
$statuses_or = trim(trim(trim($statuses_or), '||'));
$filters_statuses_before_and = trim(trim(trim($filters_statuses_before_and), '&&'));
$filters_statuses_before_or = trim(trim(trim($filters_statuses_before_or), '||'));
$filters_statuses_after_and = trim(trim(trim($filters_statuses_after_and), '&&'));
$filters_statuses_after_or = trim(trim(trim($filters_statuses_after_or), '||'));
// Инициализация буфера с объёдинёнными буферами c AQL-выражениям "И" и "ИЛИ"
$statuses_merged = (empty($statuses_and) ? '' : "($statuses_and)") . (empty($statuses_or) ? '' : (empty($statuses_and) ? '' : ' || ') . "($statuses_or)");
$filters_statuses_before_merged = (empty($filters_statuses_before_and) ? '' : "($filters_statuses_before_and)") . (empty($filters_statuses_before_or) ? '' : (empty($filters_statuses_before_and) ? '' : ' || ') . "($filters_statuses_before_or)");
$filters_statuses_after_merged = (empty($filters_statuses_after_and) ? '' : "($filters_statuses_after_and)") . (empty($filters_statuses_after_or) ? '' : (empty($filters_statuses_after_and) ? '' : ' || ') . "($filters_statuses_after_or)");
// Инициализация общего буфера с AQL-выражениями
$filters = '';
$filters_before = '';
$filters_after = '';
// Объединение фильров в единую строку с AQL-выражениями для инъекции
if (!empty($statuses_merged)) $filters .= empty($filters) ? $statuses_merged : " && ($statuses_merged)";
if (!empty($polysemantic)) $filters .= empty($filters) ? $polysemantic : " && $polysemantic";
// Объединение фильтров в единую строку с AQL-выражениями для инъекции
if (!empty($filters_statuses_before_merged)) $filters_before .= empty($filters_before) ? $filters_statuses_before_merged : " && ($filters_statuses_before_merged)";
if (!empty($filters_statuses_after_merged)) $filters_after .= empty($filters_after) ? $filters_statuses_after_merged : " && ($filters_statuses_after_merged)";
// Инициализация строки поиска
$search = $_COOKIE["workers_filter_search"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['workers']['filters']['search'] ?? '';
if (mb_strlen($search) < 3) $search = null;
$search_query = empty($search)
? null
: <<<AQL
SEARCH
a.commentary IN TOKENS(@search, 'text_ru')
|| a.address IN TOKENS(@search, 'text_ru')
|| a.passport IN TOKENS(@search, 'text_ru')
|| a.department.address IN TOKENS(@search, 'text_ru')
|| a.requisites IN TOKENS(@search, 'text_ru')
|| STARTS_WITH(a._key, @search)
|| STARTS_WITH(a.name.first, @search)
|| STARTS_WITH(a.name.second, @search)
|| STARTS_WITH(a.name.last, @search)
|| STARTS_WITH(a.address, @search)
|| STARTS_WITH(a.city, @search)
|| STARTS_WITH(a.district, @search)
|| STARTS_WITH(a.number, @search)
|| STARTS_WITH(a.mail, @search)
|| STARTS_WITH(a.passport, @search)
|| STARTS_WITH(a.department.number, @search)
|| STARTS_WITH(a.department.address, @search)
|| STARTS_WITH(a.requisites, @search)
|| STARTS_WITH(a.tax, @search)
|| (LENGTH(@search) > 5 && LEVENSHTEIN_MATCH(a._key, TOKENS(@search, 'text_en')[0], 2, true))
|| (LENGTH(@search) > 3 && LEVENSHTEIN_MATCH(a.name.first, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 3 && LEVENSHTEIN_MATCH(a.name.second, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 3 && LEVENSHTEIN_MATCH(a.name.last, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 7 && LEVENSHTEIN_MATCH(a.address, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(a.city, TOKENS(@search, 'text_ru')[0], 1, true))
|| (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(a.district, TOKENS(@search, 'text_ru')[0], 1, true))
|| (LENGTH(@search) > 5 && LEVENSHTEIN_MATCH(a.number, TOKENS(@search, 'text_en')[0], 2, true))
|| (LENGTH(@search) > 5 && LEVENSHTEIN_MATCH(a.mail, TOKENS(@search, 'text_en')[0], 2, true))
|| (LENGTH(@search) > 5 && LEVENSHTEIN_MATCH(a.passport, TOKENS(@search, 'text_ru')[0], 1, true))
|| (LENGTH(@search) > 6 && LEVENSHTEIN_MATCH(a.department.number, TOKENS(@search, 'text_en')[0], 1, true))
|| (LENGTH(@search) > 5 && LEVENSHTEIN_MATCH(a.department.address, TOKENS(@search, 'text_ru')[0], 1, true))
|| (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(a.requisites, TOKENS(@search, 'text_ru')[0], 1, true))
|| (LENGTH(@search) > 7 && LEVENSHTEIN_MATCH(a.tax, TOKENS(@search, 'text_ru')[0], 1, true))
OPTIONS { collections: ["account", "worker"] }
AQL;
// Инициализация данных для генерации HTML-документа с таблицей
$this->view->rows = registry::workers(before: empty($filters) ? null : "FILTER ($filters)", page: (int) $this->session->buffer[$_SERVER['INTERFACE']]['workers']['page']);
$this->view->rows = registry::workers(
before: sprintf(
<<<AQL
%s
FILTER a.deleted != true
AQL,
$search_query,
),
after: sprintf(
<<<AQL
let account = (IS_SAME_COLLECTION('account', a) ? a : b)
let worker = (IS_SAME_COLLECTION('worker', a) ? a : b)
FILTER account.type == 'worker' && b.deleted != true
%s
%s
AQL,
empty($filters_before) ? null : "FILTER $filters_before",
empty($filters_after) ? null : "FILTER $filters_after"
),
page: (int) $this->session->buffer[$_SERVER['INTERFACE']]['workers']['page'],
target: empty($search) ? account::COLLECTION : 'registry_accounts',
binds: empty($search) ? [] : [
'search' => $search
]
);
// Запись в cookie (только таким методом можно записать "hostonly: true")
setcookie(
@ -169,6 +228,282 @@ final class worker extends core
return null;
}
/**
* Создать
*
* @param array $parameters Параметры запроса
*
* @return void В буфер вывода JSON-документ с запрашиваемыми параметрами
*/
public function create(array $parameters = []): void
{
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
// Инициализация буфера ошибок
$this->errors['account'] ??= [];
try {
// Проверка наличия переданного пароля
if (!isset($parameters['account_password'])) throw new exception('Не получен пароль');
else if (strlen($parameters['account_password']) < 6) throw new exception('Пароль должен быть не менее 6 символов');
else if (!empty($parameters['worker_number']) && strlen($parameters['worker_number']) < 11) throw new exception('Несоответствие формату SIM-номера сотрудника');
else if (!empty($parameters['account_number']) && strlen($parameters['account_number']) < 11) throw new exception('Несоответствие формату SIM-номера аккаунта сотрудника');
else if (!empty($parameters['worker_mail']) && preg_match('/^.+@.+\.\w+$/', $parameters['worker_mail']) === 0) throw new exception('Несоответствие формату почты сотрудника');
else if (!empty($parameters['account_mail']) && preg_match('/^.+@.+\.\w+$/', $parameters['account_mail']) === 0) throw new exception('Несоответствие формату почты аккаунта сотрудника');
// Универсализация
/* $parameters['worker_number'] = (int) $parameters['worker_number']; */
/* $parameters['account_number'] = (int) $parameters['account_number']; */
if (!empty($parameters['requisites']) && $parameters['worker_requisites'][-1] === '.') $parameters['worker_requisites'] .= '.';
if (!empty($parameters['worker_birth'])) $parameters['worker_birth'] = DateTime::createFromFormat('Y-m-d', $parameters['worker_birth'])->getTimestamp();
if (!empty($parameters['worker_issued'])) $parameters['worker_issued'] = DateTime::createFromFormat('Y-m-d', $parameters['worker_issued'])->getTimestamp();
if (!empty($parameters['worker_hiring'])) $parameters['worker_hiring'] = DateTime::createFromFormat('Y-m-d', $parameters['worker_hiring'])->getTimestamp();
// Создание аккаунта
$account = account::create(
data: [
'type' => 'worker',
'name' => [
'first' => $parameters['account_name_first'],
'second' => $parameters['account_name_second'],
'last' => $parameters['account_name_last']
],
'number' => $parameters['account_number'] === 0 ? '' : $parameters['account_number'],
'mail' => $parameters['account_mail'],
'password' => sodium_crypto_pwhash_str(
$parameters['account_password'],
SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE,
SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE
),
'commentary' => $parameters['account_commentary']
],
errors: $this->errors['account']
);
// Проверка существования созданного аккаунта
if (empty($account)) throw new exception('Не удалось создать аккаунт');
} catch (exception $e) {
// Write to the errors registry
$this->errors['account'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Инициализация идентификатора аккаунта (ключ документа инстанции аккаунта в базе данных)
$_key = preg_replace('/.+\//', '', $account ?? '');
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode(
[
'clipboard' => empty($this->errors['account']) ? <<<TEXT
Идентификатор: $_key
Пароль: {$parameters['account_password']}
TEXT : '',
'errors' => self::parse_only_text($this->errors['account'])
]
);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
try {
// Создание сотрудника
$worker = model::create(
data: [
'name' => [
'first' => $parameters['worker_name_first'],
'second' => $parameters['worker_name_second'],
'last' => $parameters['worker_name_last']
],
'number' => $parameters['worker_number'] === 0 ? '' : $parameters['worker_number'],
'mail' => $parameters['worker_mail'],
'birth' => $parameters['worker_birth'],
'passport' => $parameters['worker_passport'],
'issued' => $parameters['worker_issued'],
'department' => [
'number' => $parameters['worker_department_number'],
'address' => $parameters['worker_department_address']
],
'requisites' => $parameters['worker_requisites'],
'payment' => $parameters['worker_payment'],
'tax' => $parameters['worker_tax'],
'city' => $parameters['worker_city'],
'district' => $parameters['worker_district'],
'address' => $parameters['worker_address'],
'hiring' => $parameters['worker_hiring'],
'rating' => 3
],
errors: $this->errors['account']
);
// Проверка существования созданного сотрудника
if (empty($worker)) throw new exception('Не удалось создать сотрудника');
// Создание ребра: account -> worker
account::connect($account, $worker, 'worker', $this->errors['account']);
} catch (exception $e) {
// Write to the errors registry
$this->errors['account'][] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
}
}
/**
* Прочитать данные
*
* @param array $parameters Параметры запроса
*/
public function fields(array $parameters = []): ?string
{
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
// Инициализация данных сотрудника
$worker = model::read('d._key == "' . $parameters['id'] . '"', return: '{ name: d.name, number: d.number, mail: d.mail, birth: d.birth, passport: d.passport, issued: d.issued, department: d.department, requisites: d.requisites, payment: d.payment, tax: d.tax, city: d.city, district: d.district, address: d.address, hiring: d.hiring}')->getAll();
if (!empty($worker)) {
// Найдены данные сотрудника
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode($worker);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
}
// Возврат (провал)
return null;
}
/**
* Обновить данные
*
* @param array $parameters Параметры запроса
*/
public function update(array $parameters = []): ?string
{
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
// Инициализация данных сотрудника
$worker = model::read('d._key == "' . $parameters['id'] . '"');
if (!empty($worker)) {
// Найден сотрудник
// Универсализация
if (!empty($parameters['birth'])) $parameters['birth'] = DateTime::createFromFormat('Y-m-d', $parameters['birth'])->getTimestamp();
if (!empty($parameters['issued'])) $parameters['issued'] = DateTime::createFromFormat('Y-m-d', $parameters['issued'])->getTimestamp();
if (!empty($parameters['hiring'])) $parameters['hiring'] = DateTime::createFromFormat('Y-m-d', $parameters['hiring'])->getTimestamp();
// Инициализация параметров (перезапись переданными значениями)
if ($parameters['name_first'] !== $worker->name['first']) $worker->name = ['first' => $parameters['name_first']] + $worker->name;
if ($parameters['name_second'] !== $worker->name['second']) $worker->name = ['second' => $parameters['name_second']] + $worker->name;
if ($parameters['name_last'] !== $worker->name['last']) $worker->name = ['last' => $parameters['name_last']] + $worker->name;
if ($parameters['number'] !== $worker->number)
if (mb_strlen($parameters['number']) === 11) $worker->number = $parameters['number'];
else throw new exception('Номер должен состоять из 11 символов');
if ($parameters['mail'] !== $worker->mail) $worker->mail = $parameters['mail'];
if ($parameters['birth'] !== $worker->birth) $worker->birth = $parameters['birth'];
if ($parameters['passport'] !== $worker->passport) $worker->passport = $parameters['passport'];
if ($parameters['issued'] !== $worker->issued) $worker->issued = $parameters['issued'];
if ($parameters['department_number'] !== $worker->department['number']) $worker->department = ['number' => $parameters['department_number']] + $worker->department;
if ($parameters['department_address'] !== $worker->department['address']) $worker->department = ['address' => $parameters['department_address']] + $worker->department;
if ($parameters['requisites'] !== $worker->requisites) $worker->requisites = $parameters['requisites'];
if ($parameters['payment'] !== $worker->payment) $worker->payment = $parameters['payment'];
if ($parameters['tax'] !== $worker->tax) $worker->tax = $parameters['tax'];
if ($parameters['city'] !== $worker->city) $worker->city = $parameters['city'];
if ($parameters['district'] !== $worker->district) $worker->district = $parameters['district'];
if ($parameters['address'] !== $worker->address) $worker->address = $parameters['address'];
if ($parameters['hiring'] !== $worker->hiring) $worker->hiring = $parameters['hiring'];
if (_core::update($worker)) {
// Записаны данные сотрудника
// Инициализация строки в глобальную переменную шаблонизатора
$this->view->rows = registry::workers(
before: sprintf(
"FILTER a._id == '%s' && a.deleted != true",
$worker->getId()
),
after: <<<AQL
let account = (IS_SAME_COLLECTION('account', a) ? a : b)
let worker = (IS_SAME_COLLECTION('worker', a) ? a : b)
FILTER account.type == 'worker' && b.deleted != true
AQL,
amount: 1,
target: model::COLLECTION
);
// Запись в глобальную переменную шаблонизатора обрабатываемой страницы (отключение)
$this->view->page = null;
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'updated' => true,
'row' => $this->view->render(DIRECTORY_SEPARATOR . 'elements' . DIRECTORY_SEPARATOR . 'workers.html'),
'errors' => self::parse_only_text($this->errors)
];
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не удалось записать изменения в базу данных');
} else throw new exception('Не удалось найти аккаунт');
}
// Возврат (провал)
return null;
}
/**
* Прочитать данные сотрудников для <datalist>
*
@ -180,7 +515,7 @@ final class worker extends core
// Авторизован аккаунт оператора или магазина
// Инициализация данных сотрудников
$this->view->workers = model::read(filter: 'd.status == "active"', amount: 10000, return: '{ id: d.id, name: d.name }');
$this->view->workers = model::read(filter: 'd.active == true', amount: 10000, return: '{ _key: d._key, name: d.name }');
// Универсализация
if ($this->view->workers instanceof _document) $this->view->workers = [$this->view->workers];

View File

@ -8,13 +8,13 @@ namespace mirzaev\ebala\models;
use mirzaev\ebala\models\traits\instance,
mirzaev\ebala\models\traits\status;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Фреймворк ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Встроенные библиотеки
use exception;
@ -60,7 +60,7 @@ final class account extends core
$this->document = $account->document;
// Связь сессии с аккаунтом
$session->connect($this, $errors);
session::connect($session->getId(), $this->document->getId(), $errors);
return $this;
} else {
@ -79,7 +79,7 @@ final class account extends core
if (!empty($session->buffer['worker']['entry']['password'])) {
// Найден пароль в буфере сессии
if (($account = self::read('d.number == ' . $session->buffer['worker']['entry']['number'], amount: 1, errors: $errors)) instanceof _document) {
if (($account = self::read('d.number == "' . $session->buffer['worker']['entry']['number'] . '" && d.type == "worker"', amount: 1, errors: $errors)) instanceof _document) {
// Найден аккаунт сотрудника (игнорируются ошибки)
if (sodium_crypto_pwhash_str_verify($account->password, $session->buffer['worker']['entry']['password'])) {
@ -89,7 +89,7 @@ final class account extends core
$this->document = $account;
// Связь сессии с аккаунтом
$session->connect($this, $errors);
session::connect($session->getId(), $this->document->getId(), $errors);
// Удаление использованных данных из буфера сессии
$session->write(['entry' => ['number' => null, 'password' => null]]);
@ -99,40 +99,6 @@ final class account extends core
} else throw new exception('Неправильный пароль');
throw new exception('Неизвестная ошибка на этапе проверки пароля');
} else if ($worker = static::worker($session->buffer['worker']['entry']['number'])) {
// Найден сотрудник
if (self::create([
'number' => $session->buffer['worker']['entry']['number'],
'password' => sodium_crypto_pwhash_str(
$session->buffer['worker']['entry']['password'],
SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE,
SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE
),
'status' => 'active',
'type' => 'worker'
], $errors)) {
// Зарегистрирован аккаунт
if (($account = self::read('d.number == "' . $session->buffer['worker']['entry']['number'] . '"', amount: 1, errors: $errors)) instanceof _document) {
// Найден аккаунт
// Инициализация инстанции документа аккаунта в базе данных
$this->document = $account;
// Связь сессии с аккаунтом
$session->connect($this, $errors);
// Связь аккаунта с сотрудником
$this->connect($worker, $errors);
// Удаление использованных данных из буфера сессии
$session->write(['entry' => ['number' => null, 'password' => null]]);
// Выход (успех)
return $this;
} else throw new exception('Не удалось аутентифицировать аккаунт после его регистрации');
} else throw new exception('Не удалось зарегистрировать аккаунт');
} else throw new exception('Не найден аккаунт');
} else throw new exception('Не найден пароль в буфере сессии');
} else if (!empty($session->buffer['operator']['entry']['_key'])) {
@ -141,7 +107,7 @@ final class account extends core
if (!empty($session->buffer['operator']['entry']['password'])) {
// Найден пароль в буфере сессии
if (($account = self::read('d._key == "' . $session->buffer['operator']['entry']['_key'] . '"', amount: 1)) instanceof _document) {
if (($account = self::read('d._key == "' . $session->buffer['operator']['entry']['_key'] . '" && d.type == "operator"', amount: 1)) instanceof _document) {
// Найден аккаунт оператора (игнорируются ошибки)
if (sodium_crypto_pwhash_str_verify($account->password, $session->buffer['operator']['entry']['password'])) {
@ -151,7 +117,7 @@ final class account extends core
$this->document = $account;
// Связь сессии с аккаунтом
$session->connect($this, $errors);
session::connect($session->getId(), $this->document->getId(), $errors);
// Удаление использованных данных из буфера сессии
$session->write(['entry' => ['_key' => null, 'password' => null]]);
@ -163,36 +129,32 @@ final class account extends core
throw new exception('Неизвестная ошибка на этапе проверки пароля');
}
} else throw new exception('Не найден пароль в буфере сессии');
} else if (!empty($session->buffer['market']['entry'])) {
} else if (!empty($session->buffer['market']['entry']['_key'])) {
// Найден идентификатор магазина в буфере сессии
if (!empty($session->buffer['market']['entry']['password'])) {
// Найден пароль в буфере сессии
if (($market = market::read('d.id == "' . $session->buffer['market']['entry']['id'] . '"', amount: 1)) instanceof _document) {
// Найден магазин (игнорируются ошибки)
if (($account = self::read('d._key == "' . $session->buffer['market']['entry']['_key'] . '" && d.type == "market"', amount: 1)) instanceof _document) {
// Найден аккаунт (игнорируются ошибки)
if (($account = market::account($market->getId())) instanceof _document) {
// Найден аккаунт (игнорируются ошибки)
if (sodium_crypto_pwhash_str_verify($account->password, $session->buffer['market']['entry']['password'])) {
// Аутентифицирован аккаунт (прошёл проверку пароль)
if (sodium_crypto_pwhash_str_verify($account->password, $session->buffer['market']['entry']['password'])) {
// Аутентифицирован аккаунт (прошёл проверку пароль)
// Инициализация инстанции документа аккаунта в базе данных
$this->document = $account;
// Инициализация инстанции документа аккаунта в базе данных
$this->document = $account;
// Связь сессии с аккаунтом
session::connect($session->getId(), $this->document->getId(), $errors);
// Связь сессии с аккаунтом
$session->connect($this, $errors);
// Удаление использованных данных из буфера сессии
$session->write(['entry' => ['_key' => null, 'password' => null]]);
// Удаление использованных данных из буфера сессии
$session->write(['entry' => ['id' => null, 'password' => null]]);
// Выход (успех)
return $this;
} else throw new exception('Неправильный пароль');
// Выход (успех)
return $this;
} else throw new exception('Неправильный пароль');
throw new exception('Неизвестная ошибка на этапе проверки пароля');
}
throw new exception('Неизвестная ошибка на этапе проверки пароля');
}
} else throw new exception('Не найден пароль в буфере сессии');
} else if (!empty($session->buffer['administrator']['entry'])) {
@ -201,7 +163,7 @@ final class account extends core
if (!empty($session->buffer['administrator']['entry']['password'])) {
// Найден пароль в буфере сессии
if (($account = self::read('d._key == "' . $session->buffer['administrator']['entry']['_key'] . '"', amount: 1)) instanceof _document) {
if (($account = self::read('d._key == "' . $session->buffer['administrator']['entry']['_key'] . '" && d.type == "administrator"', amount: 1)) instanceof _document) {
// Найден аккаунт администратора (игнорируются ошибки)
if (sodium_crypto_pwhash_str_verify($account->password, $session->buffer['administrator']['entry']['password'])) {
@ -211,7 +173,7 @@ final class account extends core
$this->document = $account;
// Связь сессии с аккаунтом
$session->connect($this, $errors);
session::connect($session->getId(), $this->document->getId(), $errors);
// Удаление использованных данных из буфера сессии
$session->write(['entry' => ['_key' => null, 'password' => null]]);
@ -239,30 +201,40 @@ final class account extends core
}
/**
* Найти сотрудника
* Найти связанного с аккаунтом сотрудника
*
* @param int|string $number Номер
* @param string $_id Идентификатор аккаунта связанного с сотрудником
* @param array &$errors Реестр ошибок
*
* @return ?_document Инстанция документа сотрудника в базе данных, если найдена
*/
public static function worker(int|string $number, array &$errors = []): ?_document
public static function worker(string $_id, array &$errors = []): ?_document
{
try {
if (collection::init(static::$arangodb->session, worker::COLLECTION)) {
// Инициализирована коллекция
// Поиск сотрудника
$worker = collection::search(
static::$arangodb->session,
sprintf(
<<<'AQL'
FOR d IN %s
FILTER d.phone == '%s'
SORT d.created DESC
LET e = (
FOR e IN %s
FILTER e._from == '%s'
SORT e.created DESC, e._key DESC
LIMIT 1
RETURN e
)
FILTER d._id == e[0]._to && d.active == true
SORT d.created DESC, d._key DESC
LIMIT 1
RETURN d
AQL,
worker::COLLECTION,
$number
static::COLLECTION . '_edge_worker',
$_id
)
);
@ -283,12 +255,12 @@ final class account extends core
}
/**
* Найти связанный магазин
* Найти связанный с аккаунтом магазин
*
* @param string $_id Идентификатор аккаунта связанного с магазином
* @param array &$errors Реестр ошибок
*
* @return _document|null Инстанция документа магазина в базе данных, если найдена
* @return ?_document Инстанция документа магазина в базе данных, если найдена
*/
public static function market(string $_id, array &$errors = []): ?_document
{
@ -300,7 +272,8 @@ final class account extends core
) {
// Инициализированы коллекции
return collection::search(
// Поиск магазина
$market = collection::search(
static::$arangodb->session,
sprintf(
<<<'AQL'
@ -308,12 +281,12 @@ final class account extends core
LET e = (
FOR e IN %s
FILTER e._from == '%s'
SORT e._key DESC
SORT e.created DESC, e._key DESC
LIMIT 1
RETURN e
)
FILTER d._id == e[0]._to && d.status == 'active'
SORT d.created DESC
FILTER d._id == e[0]._to && d.active == true
SORT d.created DESC, d._key DESC
LIMIT 1
RETURN d
AQL,
@ -322,6 +295,8 @@ final class account extends core
$_id
)
);
return $market instanceof _document ? $market : throw new exception('Не удалось найти инстанцию магазина в базе данных');
} else throw new exception('Не удалось инициализировать коллекции');
} catch (exception $e) {
// Запись в реестр ошибок
@ -341,43 +316,45 @@ final class account extends core
*
* Ищет связь аккаунта с сотрудником, если не находит, то создаёт её
*
* @param _document $worker Инстанция документа в базе данных сотрудника
* @param string $worker Идентификатор инстанции документа аккаунта в базе данных
* @param string $target Идентификатор инстанции документа цели в базе данны (подразумевается сотрудник или магазин)
* @param string $type Тип подключения (worker|market)
* @param array &$errors Реестр ошибок
*
* @return bool Связан аккаунт с сотрудником?
*/
public function connect(_document $worker, array &$errors = []): bool
public static function connect(string $account, string $target, string $type = 'worker', array &$errors = []): bool
{
try {
if (
collection::init($this::$arangodb->session, 'worker')
&& collection::init($this::$arangodb->session, self::COLLECTION)
&& collection::init($this::$arangodb->session, self::COLLECTION . '_edge_worker', true)
collection::init(static::$arangodb->session, $type)
&& collection::init(static::$arangodb->session, self::COLLECTION)
&& collection::init(static::$arangodb->session, self::COLLECTION . "_edge_$type", true)
) {
// Инициализированы коллекции
if (
collection::search($this::$arangodb->session, sprintf(
collection::search(static::$arangodb->session, sprintf(
<<<AQL
FOR d IN %s
FILTER d._from == '%s' && d._to == '%s'
SORT d.created DESC
SORT d.created DESC, d._key DESC
LIMIT 1
RETURN d
AQL,
self::COLLECTION . '_edge_worker',
$this->document->getId(),
$worker->getId()
self::COLLECTION . '_edge_' . worker::COLLECTION,
$account,
$target
)) instanceof _document
|| document::write($this::$arangodb->session, self::COLLECTION . '_edge_worker', [
'_from' => $this->document->getId(),
'_to' => $worker->getId()
|| $id = document::write(static::$arangodb->session, self::COLLECTION . "_edge_$type", [
'_from' => $account,
'_to' => $target
])
) {
// Найдено, либо создано ребро: account -> worker
// Найдено, либо создано ребро: account -> $type
return true;
} else throw new exception('Не удалось создать ребро: account -> worker');
} else throw new exception("Не удалось создать ребро: account -> $type");
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в реестр ошибок
@ -391,19 +368,20 @@ final class account extends core
return false;
}
/**
* Создать
*
* @param array $data Данные аккаунта
* @param array &$errors Реестр ошибок
*
* @return bool Создан аккаунт?
* @return string|null Идентификатор документа, если он был создан
*/
public static function create(array $data = [], array &$errors = []): bool
public static function create(array $data = [], array &$errors = []): ?string
{
try {
if (collection::init(static::$arangodb->session, self::COLLECTION))
if (document::write(static::$arangodb->session, self::COLLECTION, $data)) return true;
if ($id = document::write(static::$arangodb->session, self::COLLECTION, $data + ['active' => true])) return $id;
else throw new exception('Не удалось создать аккаунт');
else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
@ -416,7 +394,7 @@ final class account extends core
];
}
return false;
return null;
}
/**

View File

@ -9,7 +9,8 @@ use mirzaev\ebala\models\traits\instance,
mirzaev\ebala\models\traits\status;
// Фреймворк ArangoDB
use mirzaev\arangodb\collection;
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
@ -63,6 +64,34 @@ final class market extends core
}
}
/**
* Создать
*
* @param array $data Данные
* @param array &$errors Реестр ошибок
*
* @return string|null Идентификатор документа, если он был создан
*/
public static function create(array $data = [], array &$errors = []): ?string
{
try {
if (collection::init(static::$arangodb->session, self::COLLECTION))
if ($id = document::write(static::$arangodb->session, self::COLLECTION, $data + ['active' => true])) return $id;
else throw new exception('Не удалось создать магазин');
else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в реестр ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return null;
}
/**
* Найти связанный аккаунт
*
@ -93,7 +122,7 @@ final class market extends core
LIMIT 1
RETURN e
)
FILTER d._id == e[0]._from && d.status == "active"
FILTER d._id == e[0]._from && d.active == true
SORT d.created DESC
LIMIT 1
RETURN d

View File

@ -14,9 +14,6 @@ use mirzaev\ebala\models\traits\instance,
// Фреймворк ArangoDB
use mirzaev\arangodb\collection;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Встроенные библиотеки
use exception;
@ -35,18 +32,24 @@ final class registry extends core
/**
* Generate workers list
*
* @param ?string $before Injection of AQL-code before search
* @param ?string $before Injection of AQL-code before search of edges
* @param ?string $after Injection of AQL-code after search of edges
* @param int $amount Amount of workers
* @param int $page Offset by amount
* @param string $target Collection or view name
* @param array $binds Binds for query
* @param array $errors Errors registry
*
* @return array Instances from ArangoDB
*/
public static function workers(
?string $before = '',
?string $after = '',
int $amount = 100,
int $page = 1,
string $sort = 'worker.created DESC, worker._key DESC',
string $sort = 'account.created DESC, account._key DESC',
string $target = account::COLLECTION,
array $binds = [],
array &$errors = []
): array {
try {
@ -56,21 +59,25 @@ final class registry extends core
// Search the session data in ArangoDB
$workers = collection::search(static::$arangodb->session, sprintf(
<<<AQL
FOR account IN %s
FILTER account.type == 'worker' && account.status != 'deleted'
FOR a IN %s
%s
LET b = (IS_SAME_COLLECTION('worker', a)
? (FOR _account IN INBOUND a._id account_edge_worker FILTER _account.deleted != true LIMIT 1 SORT _account.created DESC, _account._key DESC RETURN _account)[0]
: (FOR _worker IN OUTBOUND a._id account_edge_worker FILTER _worker.deleted != true LIMIT 1 SORT _worker.created DESC, _worker._key DESC RETURN _worker)[0]
)
FILTER b != null
%s
LET worker = (FOR worker IN OUTBOUND account._id account_edge_worker FILTER worker.status != 'deleted' LIMIT 1 SORT worker.created DESC RETURN worker)[0]
FILTER worker != null
SORT %s
LIMIT %d, %d
RETURN {account, worker}
AQL,
account::COLLECTION,
$target,
$before,
$after,
$sort,
--$page <= 0 ? 0 : $amount * $page,
$amount
));
), $binds);
// Exit (success)
return empty($workers) ? [] : (is_array($workers) ? $workers : [$workers]);
@ -92,18 +99,24 @@ final class registry extends core
/**
* Generate markets list
*
* @param ?string $before Injection of AQL-code before search
* @param ?string $before Injection of AQL-code before search of edges
* @param ?string $after Injection of AQL-code after search of edges
* @param int $amount Amount of markets
* @param int $page Offset by amount
* @param string $target Collection or view name
* @param array $binds Binds for query
* @param array $errors Errors registry
*
* @return array Instances from ArangoDB
*/
public static function markets(
?string $before = '',
?string $after = '',
int $amount = 100,
int $page = 1,
string $sort = 'market.created DESC, market._key DESC',
string $sort = 'account.created DESC, account._key DESC',
string $target = account::COLLECTION,
array $binds = [],
array &$errors = []
): array {
try {
@ -113,21 +126,25 @@ final class registry extends core
// Search the session data in ArangoDB
$markets = collection::search(static::$arangodb->session, sprintf(
<<<AQL
FOR account IN %s
FILTER account.type == 'market' && account.status != 'deleted'
FOR a IN %s
%s
LET b = (IS_SAME_COLLECTION('market', a)
? (FOR _account IN INBOUND a._id account_edge_market FILTER _account.deleted != true LIMIT 1 SORT _account.created DESC, _account._key DESC RETURN _account)[0]
: (FOR _market IN OUTBOUND a._id account_edge_market FILTER _market.deleted != true LIMIT 1 SORT _market.created DESC, _market._key DESC RETURN _market)[0]
)
FILTER b != null
%s
LET market = (FOR market IN OUTBOUND account._id account_edge_market FILTER market.status != 'deleted' LIMIT 1 SORT market.created DESC RETURN market)[0]
FILTER market != null
SORT %s
LIMIT %d, %d
RETURN {account, market}
AQL,
account::COLLECTION,
$target,
$before,
$after,
$sort,
--$page <= 0 ? 0 : $amount * $page,
$amount
));
), $binds);
// Exit (success)
return empty($markets) ? [] : (is_array($markets) ? $markets : [$markets]);
@ -149,9 +166,11 @@ final class registry extends core
/**
* Generate operators list
*
* @param ?string $before Injection of AQL-code before search
* @param ?string $before Injection of AQL-code before search of edges
* @param int $amount Amount of operators
* @param int $page Offset by amount
* @param string $target Collection or view name
* @param array $binds Binds for query
* @param array $errors Errors registry
*
* @return array Instances from ArangoDB
@ -161,6 +180,8 @@ final class registry extends core
int $amount = 100,
int $page = 1,
string $sort = 'account.created DESC, account._key DESC',
string $target = account::COLLECTION,
array $binds = [],
array &$errors = []
): array {
try {
@ -171,18 +192,17 @@ final class registry extends core
$operators = collection::search(static::$arangodb->session, sprintf(
<<<AQL
FOR account IN %s
FILTER account.type == 'operator' && account.status != 'deleted'
%s
SORT %s
LIMIT %d, %d
RETURN {account}
AQL,
account::COLLECTION,
$target,
$before,
$sort,
--$page <= 0 ? 0 : $amount * $page,
$amount
));
), $binds);
// Exit (success)
return empty($operators) ? [] : (is_array($operators) ? $operators : [$operators]);
@ -204,9 +224,11 @@ final class registry extends core
/**
* Generate administrators list
*
* @param ?string $before Injection of AQL-code before search
* @param ?string $before Injection of AQL-code before search of edges
* @param int $amount Amount of administrators
* @param int $page Offset by amount
* @param string $target Collection or view name
* @param array $binds Binds for query
* @param array $errors Errors registry
*
* @return array Instances from ArangoDB
@ -216,6 +238,8 @@ final class registry extends core
int $amount = 100,
int $page = 1,
string $sort = 'account.created DESC, account._key DESC',
string $target = account::COLLECTION,
array $binds = [],
array &$errors = []
): array {
try {
@ -226,18 +250,17 @@ final class registry extends core
$administrators = collection::search(static::$arangodb->session, sprintf(
<<<AQL
FOR account IN %s
FILTER account.type == 'administrator' && account.status != 'deleted'
%s
SORT %s
LIMIT %d, %d
RETURN {account}
AQL,
account::COLLECTION,
$target,
$before,
$sort,
--$page <= 0 ? 0 : $amount * $page,
$amount
));
), $binds);
// Exit (success)
return empty($administrators) ? [] : (is_array($administrators) ? $administrators : [$administrators]);
@ -250,6 +273,7 @@ final class registry extends core
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
var_dump($errors);
}
// Exit (fail)

View File

@ -55,35 +55,12 @@ final class session extends core
if (collection::init(static::$arangodb->session, self::COLLECTION)) {
// Инициализирована коллекция
// Инициализация идентификации по IP (через прокси или напрямую)
/* $ip = isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? 'd[\'x-forwarded-for\'] == \'' . $_SERVER['HTTP_X_FORWARDED_FOR'] . '\'' : 'd.ip == \'' . $_SERVER['REMOTE_ADDR'] . '\'';
if (
isset($hash)
&& $session = collection::search($this::$arangodb->session, sprintf(
<<<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'
FILTER d.hash == '%s' && d.expires > %d && d.active == true
RETURN d
AQL,
self::COLLECTION,
@ -100,7 +77,7 @@ final class session extends core
// Запись сессии в базу данных
$_id = document::write($this::$arangodb->session, self::COLLECTION, [
'status' => 'active',
'active' => true,
'expires' => $expires ?? time() + 604800,
'ip' => $_SERVER['REMOTE_ADDR'],
'x-forwarded-for' => $_SERVER['HTTP_X_FORWARDED_FOR'] ?? null,
@ -111,7 +88,7 @@ final class session extends core
if ($session = collection::search($this::$arangodb->session, sprintf(
<<<AQL
FOR d IN %s
FILTER d._id == '$_id' && d.expires > %d && d.status == 'active'
FILTER d._id == '$_id' && d.expires > %d && d.active == true
RETURN d
AQL,
self::COLLECTION,
@ -151,7 +128,7 @@ final class session extends core
$_document = collection::search(static::$arangodb->session, sprintf(
<<<AQL
FOR d IN %s
FILTER d.hash == '$hash' && d.expires > %d && d.status == 'active'
FILTER d.hash == '$hash' && d.expires > %d && d.active == true
RETURN d
AQL,
self::COLLECTION,
@ -197,18 +174,18 @@ final class session extends core
*
* @return bool Связана сессия с аккаунтом?
*/
public function connect(account $account, array &$errors = []): bool
public static function connect(string $session, string $account, array &$errors = []): bool
{
try {
if (
collection::init($this::$arangodb->session, self::COLLECTION)
&& collection::init($this::$arangodb->session, account::COLLECTION)
&& collection::init($this::$arangodb->session, self::COLLECTION . '_edge_' . account::COLLECTION, true)
collection::init(static::$arangodb->session, self::COLLECTION)
&& collection::init(static::$arangodb->session, account::COLLECTION)
&& collection::init(static::$arangodb->session, self::COLLECTION . '_edge_' . account::COLLECTION, true)
) {
// Инициализированы коллекции
if (
collection::search($this::$arangodb->session, sprintf(
collection::search(static::$arangodb->session, sprintf(
<<<AQL
FOR document IN %s
FILTER document._from == '%s' && document._to == '%s'
@ -216,12 +193,12 @@ final class session extends core
RETURN document
AQL,
self::COLLECTION . '_edge_' . account::COLLECTION,
$this->document->getId(),
$account->getId()
$session,
$account
)) instanceof _document
|| document::write($this::$arangodb->session, self::COLLECTION . '_edge_' . account::COLLECTION, [
'_from' => $this->document->getId(),
'_to' => $account->getId()
|| document::write(static::$arangodb->session, self::COLLECTION . '_edge_' . account::COLLECTION, [
'_from' => $session,
'_to' => $account
])
) {
// Найдено, либо создано ребро: session -> account

View File

@ -113,16 +113,23 @@ final class task extends core
* @param ?string $after Injection of AQL-code after search of worker and market
* @param int $amount Amount of tasks
* @param int $page Offset by amount
* @param string $target Collection or view name
* @param array $binds Binds for query
* @param array $errors Errors registry
*
* @return array Instances from ArangoDB
*
* @TODO
* 1. Remake task.worker with task.market to separate documents (edges) (not important)
*/
public static function list(
?string $before = '',
?string $after = '',
int $amount = 100,
int $page = 1,
string $sort = 'task.created DESC, task._key DESC',
string $sort = 'task.date DESC, task.created DESC, task._key DESC',
string $target = self::COLLECTION,
array $binds = [],
array &$errors = []
): array {
try {
@ -133,28 +140,27 @@ final class task extends core
) {
// Инициализированы коллекции
// Search the session data in ArangoDB (LET можно заменить на поиск по рёбрам но тут и так сойдёт)
// Search the session data in ArangoDB with VIEW (LET можно заменить на поиск по рёбрам но тут и так сойдёт)
$tasks = collection::search(static::$arangodb->session, sprintf(
<<<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]
LET worker = (FOR worker in %s FILTER worker._key LIKE task.worker SORT worker.created DESC, worker._key DESC LIMIT 1 RETURN worker)[0]
LET market = (FOR market in %s FILTER market._key LIKE task.market SORT market.created DESC, market._key DESC LIMIT 1 RETURN market)[0]
%s
SORT %s
LIMIT %d, %d
RETURN {task, worker, market}
AQL,
self::COLLECTION,
$target,
$before,
'worker',
'market',
worker::COLLECTION,
market::COLLECTION,
$after,
$sort,
--$page <= 0 ? 0 : $amount * $page,
$amount
));
), $binds);
// Exit (success)
return empty($tasks) ? [] : (is_array($tasks) ? $tasks : [$tasks]);
@ -167,6 +173,8 @@ final class task extends core
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
var_dump($errors);
}
// Exit (fail)

View File

@ -24,11 +24,8 @@ trait status
public function status(array &$errors = []): ?bool
{
try {
return match ($this->document->status ?? null) {
'active' => true,
'inactive' => false,
default => null
};
// Чтение и возврат (успех)
return $this->document->active ?? false;
} catch (exception $e) {
// Запись в реестр ошибок
$errors[] = [
@ -39,6 +36,7 @@ trait status
];
}
// Возврат (провал)
return null;
}
}

View File

@ -8,12 +8,13 @@ namespace mirzaev\ebala\models;
use mirzaev\ebala\models\traits\instance,
mirzaev\ebala\models\traits\status;
// Фреймворк ArangoDB
use mirzaev\arangodb\collection;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Фреймворк ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Встроенные библиотеки
use exception;
@ -61,6 +62,34 @@ final class worker extends core
}
}
/**
* Создать
*
* @param array $data Данные
* @param array &$errors Реестр ошибок
*
* @return string|null Идентификатор документа, если он был создан
*/
public static function create(array $data = [], array &$errors = []): ?string
{
try {
if (collection::init(static::$arangodb->session, self::COLLECTION))
if ($id = document::write(static::$arangodb->session, self::COLLECTION, $data + ['active' => true])) return $id;
else throw new exception('Не удалось создать сотрудника');
else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в реестр ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return null;
}
/**
* Записать
*

View File

@ -1,8 +1,11 @@
@charset "UTF-8";
main {
z-index: 1000;
margin: 20vh auto;
margin: 30vh auto;
position: relative;
height: unset;
min-height: 60vh;
display: flex;
flex-direction: column;
justify-content: unset;
@ -11,12 +14,12 @@ main {
}
section.panel {
--display: flex;
z-index: 1000;
width: 400px;
position: relative;
display: flex;
flex-direction: column;
border-radius: 3px;
}
div.column > section.panel {
@ -92,7 +95,6 @@ section.panel > section.header {
align-items: end;
animation-duration: 120s;
border-radius: 3px 3px 0 0;
background-color: var(--background-above);
}
section.panel > section.header > :is(h1, h2, h3) {
@ -105,13 +107,16 @@ section.panel > section.body {
display: flex;
flex-direction: column;
border-radius: 0 0 3px 3px;
background-color: var(--background-above);
}
section.panel > section.postscript {
padding: 10px 12px;
}
section.panel > section.body *:is(input, textarea) {
--background: var(--snow-deep);
}
section#entry.panel > section.body > form > label > button {
height: 100%;
padding: 0 10px;

View File

@ -1,38 +1,33 @@
@charset "UTF-8";
@keyframes input-error {
0%,
20% {
background-color: var(--input-error);
background-color: var(--clay-above);
}
50%,
100% {
background-color: var(--background-above-1);
background-color: var(--background);
}
}
@keyframes icon-error {
0%,
50% {
70% {
color: var(--icon-error);
}
80%,
100% {
color: var(--text-inverse-below-1);
}
}
@keyframes row-reinitialized {
0%,
20% {
filter: contrast(0.4);
background-color: var(--earth-background);
filter: contrast(0.2);
}
50%,
100% {
filter: unset;
background-color: var(--background, --earth-background-above);
}
}

View File

@ -0,0 +1,28 @@
i.icon.enter,
i.icon.enter::before {
height: var(--height, 6px);
display: block;
box-sizing: border-box;
border-bottom: 2px solid;
}
i.icon.enter {
--width: 14px;
--height: 6px;
position: relative;
margin-top: -3px;
width: var(--width, 14px);
border-right: 2px solid;
border-bottom-right-radius: 4px;
}
i.icon.enter::before {
content: "";
position: absolute;
left: -1px;
bottom: -4px;
width: 6px;
border-left: 2px solid;
transform: rotate(45deg);
}

View File

@ -9,6 +9,14 @@ i.icon.nametag {
border: 2px solid;
}
i.icon.nametag.bold {
--width: 8px;
--height: 8px;
width: 8px;
height: 8px;
border: 3px solid;
}
i.icon.nametag::before {
left: -5px;
top: -5px;
@ -21,6 +29,14 @@ i.icon.nametag::before {
box-shadow: -5px -5px 0 -3px, 5px 5px 0 -3px, 5px -5px 0 -3px, -5px 5px 0 -3px;
}
i.icon.nametag.bold::before {
left: -6px;
top: -6px;
width: 14px;
height: 14px;
box-shadow: -6px -6px 0 -3px, 6px 6px 0 -3px, 6px -6px 0 -3px, -6px 6px 0 -3px;
}
label > i.icon.nametag:first-of-type {
left: 13px;
}

View File

@ -17,6 +17,20 @@ i.icon.shopping.cart {
radial-gradient(circle, currentColor 60%, transparent 40%) no-repeat 6px
17px/4px 4px;
}
i.icon.shopping.cart.bold {
--height: 22px;
height: 22px;
background: linear-gradient(to left, currentColor 13px, transparent 0)
no-repeat 0px 6px/19px 3px,
linear-gradient(to left, currentColor 14px, transparent 0) no-repeat 4px
14px/16px 3px,
linear-gradient(to left, currentColor 14px, transparent 0) no-repeat 0 2px/5px
3px,
radial-gradient(circle, currentColor 60%, transparent 40%) no-repeat 14px
18px/4px 4px,
radial-gradient(circle, currentColor 60%, transparent 40%) no-repeat 5px
18px/4px 4px;
}
i.icon.shopping.cart::after,
i.icon.shopping.cart::before {
left: 4px;
@ -30,9 +44,25 @@ i.icon.shopping.cart::before {
transform: skew(12deg);
background: currentColor;
}
i.icon.shopping.cart.bold::after,
i.icon.shopping.cart.bold::before {
width: 3px;
}
i.icon.shopping.cart::after {
left: 16px;
top: 6px;
height: 10px;
transform: skew(-12deg);
}
i.icon.shopping.cart.bold::after {
left: 18px;
}
label > i.icon.shopping.cart:first-of-type {
left: 10px;
top: 6px;
}
i.icon.shopping.cart + input {
padding-left: 37px;
}

View File

@ -53,3 +53,12 @@ i.icon.user.bold::after {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
label > i.icon.user:first-of-type {
left: 12px;
top: 7px;
}
i.icon.user + input {
padding-left: 33px;
}

View File

@ -17,63 +17,94 @@ section.panel.list.medium {
width: 80%;
}
section.panel.list > form.row.menu {
margin-bottom: 10px;
section.panel.list > :is(form, search).row.menu {
margin-bottom: 10px;%s"
transition: 0s;
}
section.panel.list > form.row.menu > label {
section.panel.list > :is(form, search).row.menu > label {
height: max-content;
min-height: 30px;
display: flex;
}
section.panel.list > form.row.menu > label:not(.solid) {
section.panel.list > :is(form, search).row.menu > label:not(.solid) {
gap: 15px;
}
section.panel.list > form.row.menu.wide > label {
section.panel.list > :is(form, search).row.menu.wide > label {
height: 36px;
}
section.panel.list > form.row.menu:is(.separated, :last-of-type) {
section.panel.list > :is(form, search).row.menu.separated {
margin-bottom: 20px;
}
section.panel.list > form.row.menu > label > button {
height: 30px;
align-self: center;
div#popup > section.list > div.row.endless {
height: auto;
}
section.panel.list > form.row.menu.stretched > label > button,
section.panel.list > form.row.menu.stretched > label > input[type="search"] {
section.panel.list > :is(form, search).row.menu > label > button {
position: relative;
display: flex;
justify-content: center;
align-items: center;
height: 30px;
}
section.panel.list > :is(form, search).row.menu > label > button.separated {
margin-left: 7px;
}
section.panel.list
> :is(form, search).row.menu
> label
> button.separated:before {
content: "";
left: -12px;
position: absolute;
margin: auto 0px;
height: 80%;
border-left: 2px solid var(--earth-above);
}
section.panel.list > :is(form, search).row.menu.stretched > label > button,
section.panel.list
> :is(form, search).row.menu.stretched
> label
> input[type="search"] {
flex-grow: 1;
}
section.panel.list > form.row.menu.stretched > label > button {
section.panel.list > :is(form, search).row.menu.stretched > label > button {
max-width: 250px;
}
section.panel.list > form.row.menu > label > input {
section.panel.list > :is(form, search).row.menu > label > input {
padding: 0 10px;
}
section.panel.list > form.row.menu > label > input:not(.merged) {
section.panel.list > :is(form, search).row.menu > label > input:not(.merged) {
border-radius: 3px;
}
section.panel.list > form.row.menu > label > input[type="date"] {
section.panel.list > :is(form, search).row.menu > label > input[type="date"] {
width: 115px;
flex-shrink: 0;
}
section.panel.list > form.row.menu > label > input[type="search"] + button {
section.panel.list
> :is(form, search).row.menu
> label
> input[type="search"]
+ button {
height: 100%;
padding: 0 30px;
flex-grow: 0;
}
section.panel.list > div#title {
margin-top: 20px;
height: 50px;
background-color: var(--background-below-6);
}
@ -85,21 +116,37 @@ section.panel.list > div#title > span {
}
section.panel.list > div.row {
--background: var(--background-above-1);
--gap: 12px;
--background: var(--cloud);
position: relative;
left: 0px;
width: calc(100% - 24px);
height: 35px;
display: flex;
gap: 12px;
padding: 0 12px;
gap: var(--gap, 12px);
padding: 0 var(--gap, 12px);
border-radius: 0px;
}
section.panel.list > div.row:not(:nth-of-type(1)) {
background-color: var(--background);
}
section.panel.list > div.row:not(:nth-of-type(1)):hover {
section.panel.list > div.row:not(:nth-of-type(1)) > span {
height: 100%;
line-height: 2.2;
padding: 0;
background-color: var(--background);
box-shadow: var(--box-shadow);
-webkit-box-shadow: var(--box-shadow);
-moz-box-shadow: var(--box-shadow);
}
section.panel.list > div.row:not(:nth-of-type(1)):is(:hover, :focus) {
--padding-left: 24px;
--padding-right: 24px;
left: -12px;
padding: 0 24px;
padding: 0 var(--padding-left, 24px) 0 var(--padding-right, 24px);
border-radius: 3px;
transition: 0s;
}
@ -112,15 +159,15 @@ section.panel.list > div.row:last-of-type {
border-radius: 0 0 3px 3px;
}
section.panel.list > div.row:hover * {
section.panel.list > div.row:is(:hover, :focus) * {
transition: unset;
}
section.panel.list > div.row:nth-of-type(2n + 1) {
--background: var(--background-above-2);
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1) {
--background: var(--cloud-above);
}
section.panel.list > div.row[data-selected="true"]::before {
section.panel.list > div.row[data-selected="true"]:before {
left: -25px;
top: 0.08rem;
position: absolute;
@ -139,7 +186,7 @@ section.panel.list > div.row[data-selected="true"]::before {
color: var(--interface-brown);
}
section.panel.list > div.row[data-selected="true"]::after {
section.panel.list > div.row[data-selected="true"]:after {
right: -25px;
bottom: 0.08rem;
rotate: 180deg;
@ -159,50 +206,70 @@ section.panel.list > div.row[data-selected="true"]::after {
color: var(--interface-brown);
}
section.panel.list > div.row.confirmed {
--background: var(--grass-background-above-2);
section.panel.list > div.row:not(:nth-of-type(1)).confirmed {
--background: var(--grass);
}
section.panel.list > div.row.confirmed:nth-of-type(2n + 1) {
--background: var(--grass-background-above-1);
section.panel.list
> div.row:not(:nth-of-type(1)):nth-child(2n + 1).confirmed {
--background: var(--grass-above);
}
section.panel.list > div.row.published {
--background: var(--river-background);
section.panel.list > div.row:not(:nth-of-type(1)).published {
--background: var(--river);
}
section.panel.list > div.row.published:nth-of-type(2n + 1) {
--background: var(--river-background-above);
section.panel.list
> div.row:not(:nth-of-type(1)):nth-child(2n + 1).published {
--background: var(--river-above);
}
section.panel.list > div.row.confirmed.published {
--background: var(--sea-background);
section.panel.list > div.row:not(:nth-of-type(1)).confirmed.published:not(.problematic) {
--background: var(--sea);
}
section.panel.list > div.row.confirmed.published:nth-of-type(2n + 1) {
--background: var(--sea-background-above);
section.panel.list
> div.row:not(:nth-of-type(1)).confirmed.published:not(.problematic):nth-child(2n + 1) {
--background: var(--sea-above);
}
section.panel.list > div.row.hided {
/* cursor: not-allowed !important; */
box-shadow: 0px 0px 0px 1000px rgba(0, 0, 0, 0.2) inset;
-webkit-box-shadow: 0px 0px 0px 1000px rgba(0, 0, 0, 0.2) inset;
-moz-box-shadow: 0px 0px 0px 1000px rgba(0, 0, 0, 0.2) inset;
section.panel.list > div.row:not(:nth-of-type(1)).problematic {
--background: var(--clay);
}
/*
section.panel.list > div.row.hided:hover {
box-shadow: unset;
-webkit-box-shadow: unset;
-moz-box-shadow: unset;
} */
section.panel.list
> div.row:not(:nth-of-type(1)):nth-child(2n + 1).problematic {
--background: var(--clay-above);
}
section.panel.list > div.row.hided * {
section.panel.list > div.row:not(:nth-of-type(1)).coming {
--background: var(--magma);
}
section.panel.list
> div.row:not(:nth-of-type(1)):nth-child(2n + 1).coming {
--background: var(--magma-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).completed:not(.problematic) {
--background: var(--sand);
}
section.panel.list
> div.row:not(:nth-of-type(1)):nth-child(2n + 1).completed:not(.problematic) {
--background: var(--sand-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).passed {
filter: brightness(0.8);
}
section.panel.list > div.row:not(:nth-of-type(1)).hided * {
filter: blur(1px);
opacity: 0.3;
}
section.panel.list > div.row.hided:hover * {
section.panel.list > div.row:not(:nth-of-type(1)).hided:is(:hover, :focus) * {
filter: unset;
opacity: unset;
}
@ -214,8 +281,11 @@ section.panel.list > div.row.reinitialized {
}
section.panel.list
> div.row:not(:nth-of-type(1), [data-selected="true"]).reinitializable:before {
content: var(--counter, "0");
> div.row:not(
:nth-of-type(1),
[data-selected="true"]
).reinitializable:before {
content: attr(data-counter);
position: absolute;
left: -95px;
align-self: center;
@ -223,7 +293,17 @@ section.panel.list
text-align: right;
font-size: small;
font-weight: bold;
color: var(--earth-text-above);
pointer-events: none;
color: var(--earth-text);
}
section.panel.list
> div.row:not(:nth-of-type(1), [data-selected="true"]).reinitializable:is(
:hover,
:focus
):before {
content: attr(id);
color: var(--earth-text-important-below);
}
section.panel.list > div.row > span {
@ -231,6 +311,35 @@ section.panel.list > div.row > span {
margin: auto 0;
padding: 8px 0;
text-align: left;
transition: 0s;
}
section.panel.list > div.row:is(:hover, :focus) > span {
transition: 0s;
}
section.panel.list > div.row > span:not(:first-child) {
--padding-left: calc(var(--gap) / 2);
}
section.panel.list > div.row > span:not(:last-child) {
--padding-right: calc(var(--gap) / 2);
}
section.panel.list > div.row > span:first-child {
border-radius: 3px 0 0 3px;
}
section.panel.list > div.row > span:last-child {
border-radius: 0 3px 3px 0;
}
section.panel.list > div.row:not(:hover, :focus) > span:first-child {
--padding-left: var(--gap, 12px);
}
section.panel.list > div.row:not(:hover, :focus) > span:last-child {
--padding-right: var(--gap, 12px);
}
section.panel.list > div.row:nth-of-type(1) > span {
@ -249,3 +358,63 @@ section.panel.list > div.row > span[onclick] {
section.panel.list > div.row > span.field {
cursor: text;
}
section.panel.list > div.row:not(:nth-of-type(1)) > span:is(.important, .interactive:is(:hover, :focus)) {
--margin: calc(var(--gap) / 2);
--border-left: calc(var(--padding-left, var(--margin, 0px)) * -1);
--border-right: var(--padding-right, var(--margin, 0px));
--background: var(--cloud-rainy);
--box-shadow: var(--border-left, 0) 0 0 0 var(--box-shadow-color, var(--background)), var(--border-right, 0) 0 0 0 var(--box-shadow-color, var(--background));
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1) > span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--cloud-rainy-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).published > span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--river-deep);
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1).published > span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--river-deep-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).confirmed > span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--grass-dense);
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1).confirmed > span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--grass-dense-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).confirmed.published:not(.problematic) > span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--sea-deep);
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1).confirmed.published:not(.problematic) > span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--sea-deep-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).problematic > span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--clay-important);
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1).problematic > span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--clay-important-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).coming > span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--magma-important);
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1).coming > span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--magma-important-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).completed:not(.problematic) > span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--sand-important);
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1).completed:not(.problematic) > span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--sand-important-above);
}

View File

@ -1,112 +1,17 @@
/*@media (prefers-color-scheme: above) {*/
:root {
--background-above-5: #a69c9c;
--background-above-4: #b9b8b8;
--background-above-3: #ecdfdf;
--background-above-2: #f0eded;
--background-above-1: #fffbfb;
--background-above: #f4eaea;
--background: #e8d9d9;
--background-below: #d7c5c5;
--background-below-6: #8e8282;
--background-inverse: #221e1e;
--background-inverse-below: #120f0f;
--node-background-important: #c3eac3;
--node-background-completed: #b0c0b0;
--node-background: #bdb;
--connection: #b2b7b2;
--connection-completed: #d1d1d1;
--text: #151313;
--text-hover: #463e3e;
--text-active: #0e0e0e;
--text-inverse-above: #fff;
--text-inverse: #efefef;
--text-inverse-below: #d0d0d0;
--text-inverse-below-1: #bbadad;
@charset "UTF-8";
/* --beige-text-above: #defaff;
--beige-text: #806d64;
--beige-text-below: #72c2d0;
--beige-background-above: #ffffda;
--beige-background: #f4f4e1;
--beige-background-below: #f4f4e1; */
--clay-text-above: #ffe9be;
--clay-text: #f7d1a1;
--clay-text-below: #c8af7d;
--clay-background-above-1: #dc4343;
--clay-background-above: #bf3737;
--clay-background: #a43333;
--clay-background-below: #8d2a2a;
--clay-background-below-1: #792727;
--grass-text-above: #fcffae;
--grass-text: #fbff80;
--grass-text-below: #e3e85e;
--grass-background-above-2: #79b757;
--grass-background-above-1: #89c866;
--grass-background-above: #42ce1c;
--grass-background: #3dbb1a;
--grass-background-below: #38a818;
--earth-text-above: #975151;
--earth-text: #794040;
--earth-text-below: #511f1f;
--earth-background-above: #d9c5b3;
--earth-background: #b0a296;
--earth-background-below: #918377;
--sand-text-above: #c8b25f;
--sand-text: #b0844a;
--sand-text-below: #976b31;
--sand-text-below-1: #6a4a1f;
--sand-background-above: #fbf4d9;
--sand-background: #fff1bb;
--sand-background-below: #dfc79a;
--river-text-above: #2b91c4;
--river-text: #335cbb;
--river-text-below: #2b4480;
--river-background-above: #bbc9ff;
--river-background: #9dadec;
--river-background-below: #7a8cd7;
--sea-text-above: #ccf8ff;
--sea-text: #b7f1fb;
--sea-text-below: #99d5df;
--sea-background-above: #6a68dd;
--sea-background: #5d5bc1;
--sea-background-below: #504f9f;
--sky-text-above: #43b0c1;
--sky-text: #1e4c53;
--sky-text-below: ;
--sky-background-above: #dfffff;
--sky-background: #e6f9ff;
--sky-background-below: ;
--input-error: #d25050;
--icon-error: #792323;
--interface-brown: #9f704e;
--menu-background: #775653;
--menu-text-above: #ffd88e;
/* --menu-text: #cab59b; */
--menu-text: #d0b88a;
--menu-text-below: #a48f68;
--input-beige: #fffce8;
--input-clay: #fff8f8;
* {
text-decoration: none;
outline: none;
border: none;
color: var(--coal-text);
font-family: Fira, sans-serif;
transition: 0.1s ease-out;
}
/*}*/
::selection {
color: var(--clay-text-above);
background-color: var(--clay-background-above);
}
:root {
--link: #3c76ff;
--link-hover: #6594ff;
--link-active: #3064dd;
background-color: var(--clay-above);
}
.unselectable {
@ -119,16 +24,26 @@
}
.hidden:not(.animation) {
position: absolute;
display: none !important;
}
* {
text-decoration: none;
outline: none;
border: none;
color: var(--text);
font-family: Fira, sans-serif;
transition: 0.1s ease-out;
*[data-interactive="true"] {
outline: 2px solid #00000000;
cursor: pointer;
}
*[data-interactive="true"] :not(input, textarea) {
cursor: pointer;
}
*[data-interactive="true"]:is(:hover, :focus) {
outline: 2px solid var(--earth-above);
background-color: var(--earth-above);
}
*[data-interactive="true"]:is(:hover, :focus)>label>input {
outline: 2px solid var(--earth-above);
}
pre,
@ -136,12 +51,20 @@ code {
font-family: Hack, monospace;
}
input[type="range"] {
--background: rgba(0, 0, 0, 0);
}
button,
input[type="submit"],
input[type="range"] {
cursor: pointer;
}
input {
outline: var(--outline-size) var(--outline-type) var(--outline-color);
}
input[type="range"] {
margin: unset;
background: transparent;
@ -155,8 +78,14 @@ input[type="range"]::-webkit-slider-runnable-track {
input[type="range"].sand::-moz-range-track,
input[type="range"].sand::-webkit-slider-runnable-track {
border: 1px solid var(--sand-background-below);
background-color: var(--sand-background);
border: 1px solid var(--sand-below);
background-color: var(--sand);
}
input[type="range"].cloud::-moz-range-track,
input[type="range"].cloud::-webkit-slider-runnable-track {
border: 1px solid var(--cloud-below);
background-color: var(--cloud);
}
input[type="range"]::-moz-range-progress {
@ -167,6 +96,10 @@ input[type="range"].sand::-moz-range-progress {
background-color: var(--sand-text);
}
input[type="range"].cloud::-moz-range-progress {
background-color: var(--cloud-text);
}
input[type="range"]::-moz-range-thumb,
input[type="range"]::-webkit-slider-thumb {
width: 20px;
@ -181,15 +114,34 @@ input[type="range"].sand::-webkit-slider-thumb {
background-color: var(--sand-text-above);
}
input[type="range"].cloud::-moz-range-thumb,
input[type="range"].cloud::-webkit-slider-thumb {
border: 2px solid var(--cloud-text-below);
background-color: var(--cloud-text-above);
}
input[type="range"]:active::-moz-range-thumb,
input[type="range"]:active::-webkit-slider-thumb {
cursor: grabbing;
}
input[type="range"] + datalist > option {
input[type="range"]+datalist>option {
cursor: help;
}
:is(input:is([type="text"], [type="password"]), textarea)[required="true"] {
--outline-size: 2px;
--outline-type: solid;
--outline-color: var(--earth-above);
}
:is(input:is([type="text"], [type="password"]), textarea):not([disabled], [readonly], .borderless):focus {
--outline-size: 2px;
--outline-type: solid;
--outline-color: var(--earth);
transition: 0s;
}
div.range {
position: relative;
display: flex;
@ -197,15 +149,15 @@ div.range {
}
div.range.small {
width: 140px;
flex-shrink: 0;
width: 100px;
flex-grow: 1;
}
div.range.small > input[type="range"] {
div.range.small>input[type="range"] {
margin: auto 0;
}
div.range.small > input[type="range"] + i.value {
div.range.small>input[type="range"]+i.value {
position: absolute;
left: var(--left, 0px);
margin-left: -10.5px;
@ -220,20 +172,16 @@ div.range.small > input[type="range"] + i.value {
transition: 0s;
}
div.range.small > input[type="range"].sand + i.value {
div.range.small>input[type="range"].sand+i.value {
color: var(--sand-text-below-1);
}
div.range > datalist {
div.range>datalist {
display: flex;
justify-content: space-between;
padding: 0 3px;
}
input.beige {
background-color: var(--input-beige);
}
input.clay {
background-color: var(--input-clay);
}
@ -248,7 +196,7 @@ input.clay {
border-radius: 0 3px 3px 0;
}
button:is(.transparent, .transparent:hover, .transparent:active) {
button:is(.transparent, .transparent:is(:hover, :focus), .transparent:active) {
background: unset;
}
@ -256,7 +204,7 @@ a {
color: var(--link);
}
a:hover {
a:is(:hover, :focus) {
color: var(--link-hover);
}
@ -266,19 +214,18 @@ a:active {
}
label {
--row-height: 26px;
position: relative;
height: 26px;
height: var(--row-height);
display: flex;
overflow: hidden;
border-radius: 2px;
}
label > i:first-of-type {
left: 8px;
top: calc((26px - var(--height)) / 2);
label>i:first-of-type {
position: absolute !important;
margin: auto;
color: var(--text-inverse-below-1);
color: var(--text-inverse-below-1) !important;
}
label * {
@ -292,63 +239,95 @@ label * {
filter: contrast(0.6) grayscale(0.7);
}
select {
padding: 3px 13px;
cursor: pointer;
.rounded {
border-radius: 3px;
background-color: var(--background-below);
}
textarea {
width: 396px;
min-width: calc(100% - 28px);
width: 100%;
min-width: calc(100% - 24px);
min-height: 120px;
max-width: calc(100% - 28px);
max-width: calc(100% - 24px);
max-height: 300px;
padding: 8px 14px;
padding: 8px 12px;
font-size: smaller;
overflow: hidden;
border-radius: 3px;
transition: 0s;
}
textarea:not(.horizontal) {
resize: vertical;
}
select {
--padding-left: 13px;
--padding-right: 13px;
padding: 3px var(--padding-right, 13px) 3px var(--padding-left, 13px);
cursor: pointer;
border-radius: 3px;
background-color: var(--background-below);
}
select,
small {
width: 60px;
}
select.medium {
width: 150px;
}
select.large {
width: 200px;
}
input {
border-radius: 3px;
}
label>input {
--padding-left: 8px;
--padding-right: 8px;
height: initial;
padding: 0 var(--padding-right, 8px) 0 var(--padding-left, 8px);
cursor: text;
}
input.small {
width: 50px;
width: calc(60px - var(--padding-left, 0px) - var(--padding-right, 0px));
}
input.medium {
width: 140px;
width: calc(150px - var(--padding-left, 0px) - var(--padding-right, 0px));
}
input.center {
input.large {
width: calc(200px - var(--padding-left, 0px) - var(--padding-right, 0px));
}
:is(input, select).center {
text-align: center;
}
label > input {
height: initial;
padding: 0 8px;
cursor: text;
background-color: var(--background-above-1);
label>*:first-child.separated.right {
margin: auto auto auto 0;
}
label > *:first-child.separated.right {
margin: auto auto auto 0;
label>i.icon {
top: calc((var(--row-height, 26px) - var(--height)) / 2)
}
i.icon {
z-index: 10;
}
i.icon + input {
i.icon+input {
padding-left: 30px;
}
i.icon.error {
animation-duration: 3s;
animation-duration: 2s;
animation-name: icon-error;
animation-fill-mode: forwards;
animation-timing-function: ease-out;
@ -361,25 +340,26 @@ i.icon.error {
animation-timing-function: ease-in;
}
section.header > h1 {
section.header>h1 {
font-size: 1.3rem;
line-height: 1.3rem;
}
section.header > :is(h2, h3) {
section.header> :is(h2, h3) {
font-size: 1.1rem;
line-height: 1.1rem;
}
body {
margin: 0;
min-height: 100vh;
padding: 0;
display: flex;
flex-direction: column;
background-color: var(--background);
}
body > div.background {
body>div.background {
z-index: -50000;
left: -350%;
top: -350%;
@ -392,11 +372,9 @@ body > div.background {
animation-iteration-count: infinite;
background-repeat: no-repeat;
animation-timing-function: linear;
background-image: radial-gradient(
circle,
var(--background-above) 0%,
rgba(0, 0, 0, 0) 100%
);
background-image: radial-gradient(circle,
var(--cloud-below) 0%,
rgba(0, 0, 0, 0) 100%);
}
aside {
@ -407,11 +385,14 @@ aside {
}
header {
--box-shadow: 2px 0 5px rgba(0, 0, 0, 0.3);
z-index: 5000;
position: absolute;
display: flex;
flex-direction: column;
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.3);
box-shadow: var(--box-shadow);
-webkit-box-shadow: var(--box-shadow);
-moz-box-shadow: var(--box-shadow);
}
main {
@ -430,8 +411,8 @@ footer {
}
.stretched {
width: 100%;
flex-grow: 10;
width: 100% !important;
flex-grow: 10 !important;
}
button {
@ -442,26 +423,27 @@ button {
}
button.grass {
--background: var(--grass);
color: var(--grass-text);
background-color: var(--grass-background);
background-color: var(--background);
}
button.grass * {
color: var(--grass-text);
}
button.grass:hover {
button.grass:is(:hover, :focus) {
color: var(--grass-text-above);
background-color: var(--grass-background-above);
background-color: var(--grass-above);
}
button.grass:hover * {
button.grass:is(:hover, :focus) * {
color: var(--grass-text-above);
}
button.grass:active {
color: var(--grass-text-below);
background-color: var(--grass-background-below);
background-color: var(--grass-below);
transition: unset;
}
@ -470,85 +452,88 @@ button.grass:active * {
transition: unset;
}
button.blue {
color: var(--blue-text);
background-color: var(--blue-background);
button.grass.dense {
--background: var(--grass-dense);
color: var(--grass-dense-text);
background-color: var(--background);
}
button.blue * {
color: var(--blue-text);
button.grass.dense * {
color: var(--grass-dense-text);
}
button.blue:hover {
color: var(--blue-text-above);
background-color: var(--blue-background-above);
button.grass.dense:is(:hover, :focus) {
color: var(--grass-dense-text-above);
background-color: var(--grass-dense-above);
}
button.blue:hover * {
color: var(--blue-text-above);
button.grass.dense:is(:hover, :focus) * {
color: var(--grass-dense-text-above);
}
button.blue:active {
color: var(--blue-text-below);
background-color: var(--blue-background-below);
button.grass.dense:active {
color: var(--grass-dense-text-below);
background-color: var(--grass-dense-below);
transition: unset;
}
button.blue:active * {
color: var(--blue-text-below);
button.grass.dense:active * {
color: var(--grass-dense-text-below);
transition: unset;
}
.clay {
--background: var(--clay);
color: var(--clay-text);
background-color: var(--clay-background);
background-color: var(--background);
}
.clay * {
color: var(--clay-text);
}
button.clay:hover {
.clay:is(:hover, :focus) {
color: var(--clay-text-above);
background-color: var(--clay-background-above);
background-color: var(--clay-above);
}
button.clay:hover * {
.clay:is(:hover, :focus) * {
color: var(--clay-text-above);
}
button.clay:active {
.clay:active {
color: var(--clay-text-below);
background-color: var(--clay-background-below);
background-color: var(--clay-below);
transition: unset;
}
button.clay:active * {
.clay:active * {
color: var(--clay-text-below);
transition: unset;
}
.earth {
--background: var(--earth);
color: var(--earth-text);
background-color: var(--earth-background);
background-color: var(--background);
}
.earth * {
color: var(--earth-text);
}
button.earth:hover {
button.earth:is(:hover, :focus) {
color: var(--earth-text-above);
background-color: var(--earth-background-above);
background-color: var(--earth-above);
}
button.earth:hover * {
button.earth:is(:hover, :focus) * {
color: var(--earth-text-above);
}
button.earth:active {
color: var(--earth-text-below);
background-color: var(--earth-background-below);
background-color: var(--earth-below);
transition: unset;
}
@ -558,26 +543,27 @@ button.earth:active * {
}
.sand {
--background: var(--sand);
color: var(--sand-text);
background-color: var(--sand-background);
background-color: var(--background);
}
.sand * {
color: var(--sand-text);
}
button.sand:hover {
button.sand:is(:hover, :focus) {
color: var(--sand-text-above);
background-color: var(--sand-background-above);
background-color: var(--sand-above);
}
button.sand:hover * {
button.sand:is(:hover, :focus) * {
color: var(--sand-text-above);
}
button.sand:active {
color: var(--sand-text-below);
background-color: var(--sand-background-below);
background-color: var(--sand-below);
transition: unset;
}
@ -587,26 +573,27 @@ button.sand:active * {
}
.river {
--background: var(--river);
color: var(--river-text);
background-color: var(--river-background);
background-color: var(--background);
}
.river * {
color: var(--river-text);
}
button.river:hover {
button.river:is(:hover, :focus) {
color: var(--river-text-above);
background-color: var(--river-background-above);
background-color: var(--river-above);
}
button.river:hover * {
button.river:is(:hover, :focus) * {
color: var(--river-text-above);
}
button.river:active {
color: var(--river-text-below);
background-color: var(--river-background-below);
background-color: var(--river-below);
transition: unset;
}
@ -615,56 +602,88 @@ button.river:active * {
transition: unset;
}
.sky {
color: var(--sky-text);
background-color: var(--sky-background);
.cloud {
--background: var(--cloud);
color: var(--cloud-text);
background-color: var(--background);
}
.sky * {
color: var(--sky-text);
.cloud * {
color: var(--cloud-text);
}
button.sky:hover {
color: var(--sky-text-above);
background-color: var(--sky-background-above);
button.cloud:is(:hover, :focus) {
color: var(--cloud-text-above);
background-color: var(--cloud-above);
}
button.sky:hover * {
color: var(--sky-text-above);
button.cloud:is(:hover, :focus) * {
color: var(--cloud-text-above);
}
button.sky:active {
color: var(--sky-text-below);
background-color: var(--sky-background-below);
button.cloud:active {
color: var(--cloud-text-below);
background-color: var(--cloud-below);
transition: unset;
}
button.sky:active * {
color: var(--sky-text-below);
button.cloud:active * {
color: var(--cloud-text-below);
transition: unset;
}
.snow {
--background: var(--snow);
color: var(--snow-text);
background-color: var(--background);
}
.snow *:not(.earth, .clay, .sand, .ice, .cloud, .river, .sea, .coal) {
color: var(--snow-text);
}
button.snow:is(:hover, :focus) {
color: var(--snow-text-above);
background-color: var(--snow-above);
}
button.snow:is(:hover, :focus) *:not(.earth, .clay, .sand, .ice, .cloud, .river, .sea, .coal) {
color: var(--snow-text-above);
}
button.snow:active {
color: var(--snow-text-below);
background-color: var(--snow-deep);
transition: unset;
}
button.snow:active *:not(.earth, .clay, .sand, .ice, .cloud, .river, .sea, .coal) {
color: var(--snow-text-below);
transition: unset;
}
.sea {
--background: var(--sea);
color: var(--sea-text);
background-color: var(--sea-background);
background-color: var(--background);
}
.sea * {
color: var(--sea-text);
}
button.sea:hover {
button.sea:is(:hover, :focus) {
color: var(--sea-text-above);
background-color: var(--sea-background-above);
background-color: var(--sea-above);
}
button.sea:hover * {
button.sea:is(:hover, :focus) * {
color: var(--sea-text-above);
}
button.sea:active {
color: var(--sea-text-below);
background-color: var(--sea-background-below);
background-color: var(--sea-below);
transition: unset;
}
@ -673,6 +692,37 @@ button.sea:active * {
transition: unset;
}
.coal {
--background: var(--coal);
color: var(--coal-text);
background-color: var(--background);
}
.coal * {
color: var(--coal-text);
}
button.coal:is(:hover, :focus) {
color: var(--coal-text-above);
background-color: var(--coal-above);
}
button.coal:is(:hover, :focus) * {
color: var(--coal-text-above);
}
button.coal:active {
color: var(--coal-text-below);
background-color: var(--coal-below);
transition: unset;
}
button.coal:active * {
color: var(--coal-text-below);
transition: unset;
}
button.wide {
flex-grow: 1;
}
@ -683,7 +733,7 @@ section#menu {
height: 120px;
}
section#menu > nav {
section#menu>nav {
top: 0;
position: sticky;
display: flex;
@ -691,7 +741,7 @@ section#menu > nav {
background: var(--menu-background);
}
section#menu > nav > ul {
section#menu>nav>ul {
margin: 0;
min-width: 800px;
width: 80%;
@ -702,38 +752,38 @@ section#menu > nav > ul {
list-style: none;
}
section#menu > nav > ul > li {
section#menu>nav>ul>li {
height: 100%;
display: flex;
align-items: center;
}
section#menu > nav > ul > li.divided {
section#menu>nav>ul>li.divided {
margin-left: 9px;
}
section#menu > nav > ul > li.divided:before {
section#menu>nav>ul>li.divided:before {
content: "";
margin-right: 9px;
height: 60%;
border-left: 1px solid var(--menu-text-below);
}
section#menu > nav > ul > li#account:last-child {
section#menu>nav>ul>li#account:last-child {
margin-left: auto;
}
section#menu > nav > ul > li > button {
section#menu>nav>ul>li>button {
height: 100%;
padding: 0 30px;
color: var(--menu-text);
}
section#menu > nav > ul > li > button:hover {
section#menu>nav>ul>li>button:is(:hover, :focus) {
color: var(--menu-text-above);
}
section#menu > nav > ul > li > button:active {
section#menu>nav>ul>li>button:active {
transition: 0s;
color: var(--menu-text-below);
}

View File

@ -1,3 +1,5 @@
@charset "UTF-8";
section#administrators.panel.list
> div.row:nth-of-type(1)
> span[data-column="end"]
@ -9,13 +11,11 @@ section#administrators.panel.list
section#administrators.panel.list
> div.row
> span:is(
[data-column="id"],
[data-column="account"],
[data-column="name"],
[data-column="number"],
[data-column="mail"],
[data-column="commentary"],
[data-column="status"],
[data-column="commentary"]
) {
min-width: 220px;
width: 220px;
@ -24,40 +24,47 @@ section#administrators.panel.list
overflow: hidden;
}
section#administrators.panel.list > div.row > span[data-column="id"] {
section#administrators.panel.list > div.row > span[data-column="account"] {
min-width: 102px;
width: 102px;
font-weight: bold;
text-align: center;
}
section#administrators.panel.list > div.row:nth-of-type(1) > span[data-column="id"] {
margin-top: 8px;
section#administrators.panel.list
> div.row:nth-of-type(1)
> span[data-column="account"] {
margin-top: 6px;
}
section#administrators.panel.list > div.row > span[data-column="name"] {
min-width: 180px;
width: 180px;
}
section#administrators.panel.list > div.row > span[data-column="number"] {
min-width: 130px;
width: 130px;
}
section#administrators.panel.list > div.row > span[data-column="number"] {
min-width: 160px;
width: 160px;
}
section#administrators.panel.list
> div.row:not(:nth-of-type(1))
> span[data-column="number"] {
text-align: center;
}
section#administrators.panel.list > div.row > span[data-column="mail"] {
min-width: 300px;
width: 300px;
min-width: 140px;
width: 140px;
}
section#administrators.panel.list
> div.row:not(:nth-of-type(1))
> span[data-column="mail"] {
text-align: center;
}
section#administrators.panel.list > div.row > span[data-column="commentary"] {
min-width: unset;
width: 100%;
}
section#administrators.panel.list > div.row > span[data-column="status"] {
min-width: 100px;
width: 100px;
font-size: small;
text-align: center;
}

View File

@ -1,3 +1,5 @@
@charset "UTF-8";
section#markets.panel.list
> div.row:nth-of-type(1)
> span[data-column="end"]
@ -9,8 +11,11 @@ section#markets.panel.list
section#markets.panel.list
> div.row
> span:is(
[data-column="id"],
[data-column="director"],
[data-column="account"],
[data-column="market"],
[data-column="name"],
[data-column="number"],
[data-column="mail"],
[data-column="address"],
[data-column="type"],
[data-column="commentary"],
@ -23,24 +28,47 @@ section#markets.panel.list
overflow: hidden;
}
section#markets.panel.list > div.row > span[data-column="id"] {
min-width: 67px;
width: 67px;
section#markets.panel.list > div.row > span:is([data-column="account"], [data-column="market"]) {
min-width: 102px;
width: 102px;
font-weight: bold;
text-align: center;
}
section#markets.panel.list > div.row:nth-of-type(1) > span[data-column="id"] {
margin-top: 8px;
section#workers.panel.list > div.row:nth-of-type(1) > span[data-column="account"] {
margin-top: 6px;
}
section#markets.panel.list > div.row > span[data-column="director"] {
min-width: 180px;
width: 180px;
section#workers.panel.list > div.row:nth-of-type(1) > span[data-column="market"] {
margin-top: 0px;
}
section#markets.panel.list > div.row > span[data-column="name"] {
min-width: 130px;
width: 130px;
}
section#markets.panel.list > div.row > span[data-column="number"] {
min-width: 160px;
width: 160px;
}
section#markets.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="number"] {
text-align: center;
}
section#markets.panel.list > div.row > span[data-column="mail"] {
min-width: 140px;
width: 140px;
}
section#markets.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="mail"] {
text-align: center;
}
section#markets.panel.list > div.row > span[data-column="address"] {
min-width: 450px;
width: 450px;
min-width: 180px;
width: 180px;
}
section#markets.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="type"] {
@ -51,6 +79,7 @@ section#markets.panel.list > div.row > span[data-column="type"] {
min-width: 80px;
width: 80px;
font-size: small;
line-height: 250%;
}
section#markets.panel.list > div.row > span[data-column="commentary"] {
@ -59,8 +88,12 @@ section#markets.panel.list > div.row > span[data-column="commentary"] {
}
section#markets.panel.list > div.row > span[data-column="status"] {
min-width: 100px;
width: 100px;
min-width: 80px;
width: 80px;
font-size: small;
line-height: 250%;
}
section#markets.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="status"] {
text-align: center;
}

View File

@ -1,3 +1,5 @@
@charset "UTF-8";
section#operators.panel.list
> div.row:nth-of-type(1)
> span[data-column="end"]
@ -9,13 +11,11 @@ section#operators.panel.list
section#operators.panel.list
> div.row
> span:is(
[data-column="id"],
[data-column="account"],
[data-column="name"],
[data-column="number"],
[data-column="mail"],
[data-column="commentary"],
[data-column="status"],
) {
min-width: 220px;
width: 220px;
@ -24,40 +24,41 @@ section#operators.panel.list
overflow: hidden;
}
section#operators.panel.list > div.row > span[data-column="id"] {
section#operators.panel.list > div.row > span[data-column="account"] {
min-width: 102px;
width: 102px;
font-weight: bold;
text-align: center;
}
section#operators.panel.list > div.row:nth-of-type(1) > span[data-column="id"] {
margin-top: 8px;
section#operators.panel.list > div.row:nth-of-type(1) > span[data-column="account"] {
margin-top: 6px;
}
section#operators.panel.list > div.row > span[data-column="name"] {
min-width: 180px;
width: 180px;
}
section#operators.panel.list > div.row > span[data-column="number"] {
min-width: 130px;
width: 130px;
}
section#operators.panel.list > div.row > span[data-column="number"] {
min-width: 160px;
width: 160px;
}
section#operators.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="number"] {
text-align: center;
}
section#operators.panel.list > div.row > span[data-column="mail"] {
min-width: 300px;
width: 300px;
min-width: 140px;
width: 140px;
}
section#operators.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="mail"] {
text-align: center;
}
section#operators.panel.list > div.row > span[data-column="commentary"] {
min-width: unset;
width: 100%;
}
section#operators.panel.list > div.row > span[data-column="status"] {
min-width: 100px;
width: 100px;
font-size: small;
text-align: center;
}

View File

@ -1,4 +1,9 @@
section#tasks.panel.list > div.row:nth-of-type(1) > span[data-column="end"] > i.home {
@charset "UTF-8";
section#tasks.panel.list
> div.row:nth-of-type(1)
> span[data-column="end"]
> i.home {
margin-top: 5px;
height: 12px;
}
@ -28,9 +33,9 @@ section#tasks.panel.list > div.row > span[data-column="date"] {
font-weight: bold;
}
section#tasks.panel.list > div.row > span[data-column="worker"] {
min-width: 67px;
width: 67px;
section#tasks.panel.list > div.row > span:is([data-column="worker"], [data-column="market"]) {
min-width: 102px;
width: 102px;
font-weight: bold;
}
@ -39,8 +44,8 @@ section#tasks.panel.list > div.row:nth-of-type(1) > span[data-column="worker"] {
}
section#tasks.panel.list > div.row > span[data-column="name"] {
min-width: 180px;
width: 180px;
min-width: 130px;
width: 130px;
}
section#tasks.panel.list > div.row > span[data-column="task"] {
@ -48,7 +53,9 @@ section#tasks.panel.list > div.row > span[data-column="task"] {
width: 100px;
}
section#tasks.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="task"] {
section#tasks.panel.list
> div.row:not(:nth-of-type(1))
> span[data-column="task"] {
text-align: right;
}
@ -60,12 +67,14 @@ section#tasks.panel.list > div.row > span[data-column="start"] {
min-width: 37px;
width: 37px;
font-size: small;
line-height: 2.8;
}
section#tasks.panel.list > div.row > span[data-column="end"] {
min-width: 37px;
width: 37px;
font-size: small;
line-height: 2.8;
}
section#tasks.panel.list > div.row:nth-of-type(1) > span[data-column="hours"] {
@ -76,9 +85,12 @@ section#tasks.panel.list > div.row > span[data-column="hours"] {
min-width: 27px;
width: 27px;
font-size: small;
line-height: 2.8;
}
section#tasks.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="hours"] {
section#tasks.panel.list
> div.row:not(:nth-of-type(1))
> span[data-column="hours"] {
text-align: center;
}
@ -86,12 +98,6 @@ section#tasks.panel.list > div.row:nth-of-type(1) > span[data-column="market"] {
margin-bottom: 8px;
}
section#tasks.panel.list > div.row > span[data-column="market"] {
min-width: 60px;
width: 60px;
font-weight: bold;
}
section#tasks.panel.list > div.row > span[data-column="address"] {
min-width: 180px;
width: 180px;
@ -101,9 +107,12 @@ section#tasks.panel.list > div.row > span[data-column="type"] {
min-width: 80px;
width: 80px;
font-size: small;
line-height: 2.8;
}
section#tasks.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="type"] {
section#tasks.panel.list
> div.row:not(:nth-of-type(1))
> span[data-column="type"] {
text-align: center;
}
@ -111,9 +120,12 @@ section#tasks.panel.list > div.row > span[data-column="tax"] {
min-width: 100px;
width: 100px;
font-size: small;
line-height: 2.8;
}
section#tasks.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="tax"] {
section#tasks.panel.list
> div.row:not(:nth-of-type(1))
> span[data-column="tax"] {
text-align: center;
}

View File

@ -1,3 +1,5 @@
@charset "UTF-8";
section#workers.panel.list
> div.row:nth-of-type(1)
> span[data-column="end"]
@ -9,18 +11,17 @@ section#workers.panel.list
section#workers.panel.list
> div.row
> span:is(
[data-column="id"],
[data-column="account"],
[data-column="worker"],
[data-column="name"],
[data-column="birth"],
[data-column="number"],
[data-column="mail"],
[data-column="passport"],
[data-column="department"],
[data-column="city"],
[data-column="address"],
[data-column="requisites"],
[data-column="tax"],
[data-column="requisites"],
[data-column="commentary"],
[data-column="status"],
) {
min-width: 220px;
width: 220px;
@ -29,30 +30,46 @@ section#workers.panel.list
overflow: hidden;
}
section#workers.panel.list > div.row > span[data-column="id"] {
min-width: 67px;
width: 67px;
section#workers.panel.list > div.row > span:is([data-column="account"], [data-column="worker"]) {
min-width: 102px;
width: 102px;
font-weight: bold;
text-align: center;
}
section#workers.panel.list > div.row:nth-of-type(1) > span[data-column="id"] {
section#workers.panel.list > div.row:nth-of-type(1) > span[data-column="account"] {
margin-top: 6px;
}
section#workers.panel.list > div.row:nth-of-type(1) > span[data-column="worker"] {
margin-top: 8px;
}
section#workers.panel.list > div.row > span[data-column="name"] {
min-width: 180px;
width: 180px;
}
section#workers.panel.list > div.row > span[data-column="birth"] {
min-width: 80px;
width: 80px;
font-size: small;
min-width: 130px;
width: 130px;
}
section#workers.panel.list > div.row > span[data-column="number"] {
min-width: 120px;
width: 120px;
min-width: 160px;
width: 160px;
}
section#workers.panel.list
> div.row:not(:nth-of-type(1))
> span[data-column="number"] {
text-align: center;
}
section#workers.panel.list > div.row > span[data-column="mail"] {
min-width: 140px;
width: 140px;
}
section#workers.panel.list
> div.row:not(:nth-of-type(1))
> span[data-column="mail"] {
text-align: center;
}
section#workers.panel.list > div.row > span[data-column="passport"] {
@ -76,22 +93,19 @@ section#workers.panel.list > div.row > span[data-column="requisites"] {
}
section#workers.panel.list > div.row > span[data-column="tax"] {
min-width: 55px;
width: 55px;
min-width: 100px;
width: 100px;
font-size: small;
line-height: 2.8;
}
section#workers.panel.list
> div.row:not(:nth-of-type(1))
> span[data-column="tax"] {
text-align: center;
}
section#workers.panel.list > div.row > span[data-column="commentary"] {
min-width: unset;
width: 100%;
}
section#workers.panel.list > div.row > span[data-column="status"] {
min-width: 100px;
width: 100px;
font-size: small;
}
section#workers.panel.list > div.row:not(:nth-of-type(1)) > span[data-column="status"] {
text-align: center;
}

View File

@ -1,3 +1,5 @@
@charset "UTF-8";
div#popup {
z-index: 999999999999;
left: 0;
@ -13,82 +15,87 @@ div#popup {
background-color: rgba(18, 3, 3, 0.4);
}
div#popup > section {
background-color: var(--background-above-2);
div#popup>section {
background-color: var(--cloud-above);
}
div#popup > section.errors {
div#popup>section.errors {
--padding-horizontal: 30px;
--padding-vertical: 8px;
top: var(--top, 80%);
position: absolute;
height: var(--height);
padding: 8px 30px !important;
padding: var(--padding-vertical, 0px) var(--padding-horizontal, 0px) !important;
}
div#popup > section.small {
div#popup>section.small {
width: 420px;
}
div#popup > section.list {
div#popup>section.stretched {
width: min-content;
max-width: 80wv;
flex-grow: unset;
}
div#popup>section.calculated {
width: calc(var(--calculated-width) - var(--padding-horizontal, 0px) * 2);
}
div#popup>section.list {
display: flex;
flex-direction: column;
gap: 8px;
padding: 30px;
border-radius: 3px;
}
div#popup > section.list > h3 {
div#popup>section.list>h3 {
margin-top: 4px;
margin-bottom: 18px;
margin-bottom: 22px;
width: 100%;
text-align: center;
}
div#popup > section.list > span {
div#popup>section.list h4 {
margin-top: unset;
margin-bottom: 10px;
width: 100%;
text-align: center;
}
div#popup>section.list>section.main {
display: flex;
gap: 15px;
}
div#popup>section.list>section.main>div.column {
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
div#popup>section.list>section.main>div.column:only-child {
width: 100%;
}
div#popup>section.list>section.main>div.column>span {
width: 100%;
display: flex;
text-align: right;
}
div#popup > section.list > span > b {
div#popup>section.list>section.main>div.column>span>b {
margin-right: auto;
text-align: left;
}
div#popup > section.list > :is(div, select).row.buttons {
height: 33px;
div#popup>section.list>section.main>div.column> :is(div, select).row {
border-radius: 3px;
}
div#popup > section.list > :is(div, select).row:not(.buttons, .stretchable),
div#popup > section.list > :is(div, select).row:not(.buttons, .stretchable) > button {
height: 29px;
}
div#popup > section.list > div.row:not(.merged) + div.row.merged {
margin-top: 8px;
}
div#popup > section.list > div.row:not(.merged) {
margin-top: 8px;
}
div#popup > section.list > div.row:not(.merged):last-of-type {
margin-top: 8px;
}
div#popup > section.list > div.row.divided {
margin-top: 8px;
}
div#popup > section.list > :not(h3):first-child:not(.divided),
div#popup > section.list > h3 + div.row:not(.divided) {
margin-top: unset !important;
}
div#popup > section.list > :only-child {
margin: unset !important;
}
div#popup > section.list > div.row {
div#popup>section.list>section.main>div.column>:is(div, section).row {
height: fit-content;
position: relative;
display: flex;
@ -96,42 +103,162 @@ div#popup > section.list > div.row {
transition: 0s;
}
div#popup > section.list > div.row:not(.monolithic) {
div#popup>section.list>section.main>div.column>:is(div, section).row:not(.monolithic) {
gap: 10px;
}
div#popup > section.list > div.row > label {
div#popup>section.list>section.main>div.column>:is(div, section).row>label {
display: contents;
}
div#popup > section.list > div.row > label > i {
left: 13px;
top: 7px;
position: absolute;
}
div#popup > section.list > div.row > label > input[type="date"]:not(.small, .medium) {
div#popup>section.list>section.main>div.column>:is(div, section).row>label>input[type="date"]:not(.small, .medium, .large) {
flex-grow: 1;
text-align: center;
}
div#popup > section.list > div.row > label > input[type="time"]:not(.small, .medium) {
div#popup>section.list>section.main>div.column>:is(div, section).row>label>input[type="time"]:not(.small, .medium, .large) {
width: 55px;
text-align: center;
}
div#popup > section.list > div.row > label > :is(input, button):only-child {
div#popup>section.list>section.main>div.column>:is(div, section).row>label> :is(input, button):only-child {
width: 100%;
}
div#popup > section.list > div.row > label > :is(input, select):only-of-type:not(.small, .medium) {
div#popup>section.list>section.main>div.column>:is(div, section).row>label> :is(input, select):only-of-type:not(.small, .medium, .large) {
width: 100%;
}
/* div#popup > section.list > div.row > label > input:only-of-type:not(.center) {
padding-left: 37px;
div#popup>section.list>section.main>div.column> :is(div, select).row[data-interactive="true"]:is(:hover, :focus, :active) {
padding-left: 12px;
transition: unset;
}
div#popup>section.list>section.main>div.column> :is(div, select).row.buttons {
height: 33px;
}
div#popup>section.list>section.main>div.column> :is(div, select).row:not(.buttons, .stretchable, .endless),
div#popup>section.list>section.main>div.column> :is(div, select).row:not(.buttons, .stretchable, .endless)>button {
height: 29px;
}
div#popup>section.list>section.main>div.column>:is(div, section).row:not(.merged)+:is(div, section).row.merged {
margin-top: 8px;
}
div#popup>section.list>section.main>div.column>:is(div, section).row:not(.merged, :first-of-type) {
margin-top: 8px;
}
div#popup>section.list>section.main>div.column>:is(div, section).row:not(.merged):last-of-type:not(:only-child) {
margin-top: 8px;
}
div#popup>section.list>section.main>div.column>:is(div, section).row.divided {
margin-top: 8px;
}
/* div#popup > section.list > :only-child {
margin: unset !important;
} */
div#popup > section.list.errors > section.body > dl > dd {
div#popup>section.list>section.main>div.column>section.row.chat {
width: 460px;
height: 60vh;
display: flex;
flex-direction: column;
justify-content: start;
gap: unset;
overflow-x: hidden;
overflow-y: scroll;
}
div#popup>section.list>section.main>div.column>section.row.chat>div.message {
padding: 14px 16px;
}
div#popup>section.list>section.main>div.column>section.row.chat>div.message:nth-child(even) {
backdrop-filter: brightness(0.8);
}
div#popup>section.list>section.main>div.column>section.row.chat>div.message:hover {
backdrop-filter: brightness(0.7);
}
div#popup>section.list>section.main>div.column>section.row.chat>div.message.problem {
background-color: var(--clay-above);
}
div#popup>section.list>section.main>div.column>section.row.chat>div.message.problem:nth-child(even) {
background-color: var(--clay);
}
div#popup>section.list>section.main>div.column>section.row.chat>div.message.problem:hover {
background-color: var(--clay-important-above);
}
div#popup>section.list>section.main>div.column>section.row.chat>div.message.problem:nth-child(even):hover {
background-color: var(--clay-important);
}
div#popup>section.list>section.main>div.column>section.row.chat>div.message.solution {
background-color: var(--grass);
}
div#popup>section.list>section.main>div.column>section.row.chat>div.message.solution {
background-color: var(--grass-above);
}
div#popup>section.list>section.main>div.column>section.row.chat>div.message.solution:nth-child(even) {
background-color: var(--grass);
}
div#popup>section.list>section.main>div.column>section.row.chat>div.message.solution:hover {
background-color: var(--grass-dense-above);
}
div#popup>section.list>section.main>div.column>section.row.chat>div.message.solution:nth-child(even):hover {
background-color: var(--grass-dense);
}
div#popup>section.list>section.main>div.column>section.row.chat>div.message:is(.solution, .problem) p {
color: var(--coal-text);
}
div#popup>section.list>section.main>div.column>section.row.chat>div.message>h3 {
width: 100%;
margin: unset;
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.9rem;
font-weight: normal;
}
div#popup>section.list>section.main>div.column>section.row.chat>div.message>h3>span.date {
margin-left: auto;
}
div#popup>section.list>section.main>div.column>section.row.chat>div.message>p {
margin: 5px 0 0 8px;
font-size: 0.9rem;
word-wrap: break-word;
}
div#popup>section.list>section.main>div.column>section.row.message>textarea {
min-height: 29px;
min-width: unset;
height: 29px;
resize: none;
overflow: scroll;
}
div#popup>section.list>section.main>div.column>section.row.message>textarea+button {
height: 100%;
padding: 0 25px;
}
div#popup>section.list.errors>section.body>dl>dd {
margin-left: 20px;
}

View File

@ -0,0 +1,178 @@
@charset "UTF-8";
:root {
--background-above-5: #a69c9c;
/* --background-above-4: #b9b8b8; */
--background-above-4: #d5cdcd;
--background-above-3: #ecdfdf;
--snow-above: #f9fbff;
--snow: #f4f7fd;
--snow-deep: #dadfe6;
--ice-above: #d0d3f7;
--ice: #bfc1ea;
--ice-below: red;
/* персиковый #dddd5c */
/* персиковый тёмный #bfbf6e */
/* @TODO
* 1. Change the amount of sun and moon rays depending on the time on the device
*/
--sun-intensity: 0.3;
--sun-above: ;
--sun: rgb(219, 219, 126, var(--sun-intensity));
--sun-below: ;
--moon-above: ;
--moon: ;
--moon-below: ;
--background: #e8d9d9;
--background-below: #d7c5c5;
--background-below-6: #8e8282;
--background-inverse: #221e1e;
--background-inverse-below: #120f0f;
--node-background-important: #c3eac3;
--node-background-completed: #b0c0b0;
--node-background: #bdb;
--connection: #b2b7b2;
--connection-completed: #d1d1d1;
--text: #151313;
--text-hover: #463e3e;
--text-active: #0e0e0e;
--text-inverse-above: #fff;
--text-inverse: #efefef;
--text-inverse-below: #d0d0d0;
--text-inverse-below-1: #bbadad;
--earth-text-above: #975151;
--earth-text: #794040;
--earth-text-below: #511f1f;
--earth-text-important-above: #a44242;
--earth-text-important: #371a1a;
--earth-text-important-below: #602525;
--earth-above: #d9c5b3;
--earth: #b0a296;
--earth-below: #918377;
--earth-important-above: #b0a296;
--earth-important: #6c5f53;
--earth-important-below: #b0a296;
--clay-text-above: #ffe9be;
--clay-text: #f7d1a1;
--clay-text-below: #c8af7d;
--clay-above-1: #dc4343;
--clay-above: #bf3737;
--clay: #a43333;
--clay-below: #8d2a2a;
--clay-below-1: #792727;
--clay-important-above: #9f0c0c;
--clay-important: #910b0b;
--clay-important-below: #8d2a2a;
--sand-text-above: #c8b25f;
--sand-text: #b0844a;
--sand-text-below: #976b31;
--sand-text-below-1: #6a4a1f;
--sand-above: #fff5ce;
--sand: #f0e2ab;
--sand-below: #dfc79a;
--sand-important-above: #e6d595;
--sand-important: #d7c06c;
--sand-important-below: #dfc79a;
--magma-text-above: ;
--magma-text: ;
--magma-text-below: ;
--magma-text-below-1: ;
--magma-above: #ffd325;
--magma: #e6bf26;
--magma-below: ;
--magma-important-above: #e8a21c;
--magma-important: #c6850c;
--magma-important-below: ;
--grass-above: #89c866;
--grass: #79b757;
--grass-below: #38a818;
--grass-text-above: #fcffae;
--grass-text: #fbff80;
--grass-text-below: #e3e85e;
--grass-dense-above: #1abb4a;
--grass-dense: #12ac4e;
--grass-dense-below: #109745;
--grass-dense-text-above: var(--grass-text-above);
--grass-dense-text: var(--grass-text);
--grass-dense-text-below: var(--grass-text-below);
--river-text-above: #2b91c4;
--river-text: #335cbb;
--river-text-below: #2b4480;
--river-text-important-above: #2b91c4;
--river-text-important: #335cbb;
--river-text-important-below: #2b4480;
--river-above: #b8c0ea;
--river: #93a2df;
--river-below: #7a8cd7;
--river-deep-above: #8397e3;
--river-deep: #677ed7;
--river-deep-below: #687dd5;
--sea-text-above: #ccf8ff;
--sea-text: #b7f1fb;
--sea-text-below: #99d5df;
--sea-text-important-above: #ccf8ff;
--sea-text-important: #b7f1fb;
--sea-text-important-below: #99d5df;
--sea-above: #6a68dd;
--sea: #5d5bc1;
--sea-below: #504f9f;
--sea-deep-above: #5148c4;
--sea-deep: #312ea8;
--sea-deep-below: #4443aa;
--cloud-text-above: #43b0c1;
--cloud-text: #1e4c53;
--cloud-text-below: ;
--cloud-text-important-above: #43b0c1;
--cloud-text-important: #1e4c53;
--cloud-text-important-below: ;
--cloud-above: #e6eaf0;
--cloud: #cfd3d9;
--cloud-below: #b7babf;
--cloud-rainy-above: #b2bbce;
--cloud-rainy: #a5a4b9;
--cloud-rainy-below: ;
--coal-text-above: ;
--coal-text: #392020;
--coal-text-below: ;
--coal-above: ;
--coal: ;
--coal-below: ;
--input-error: #d25050;
--icon-error: #792323;
--interface-brown: #9f704e;
--menu-background: #775653;
--menu-text-above: #ffd88e;
/* --menu-text: #cab59b; */
--menu-text: #d0b88a;
--menu-text-below: #a48f68;
--input-beige: #fffce8;
--input-clay: #fff8f8;
--link: #3c76ff;
--link-hover: #6594ff;
--link-active: #3064dd;
}
body {
--body-rays: inset 0px 45px 100vw 3vw var(--sun);
box-shadow: var(--body-rays);
-webkit-box-shadow: var(--body-rays);
-moz-box-shadow: var(--body-rays);
}

View File

@ -35,26 +35,39 @@ $router = new router;
// Запись маршрутов
$router->write('/', 'index', 'index', 'GET');
$router->write('/', 'index', 'index', 'POST');
$router->write('/entry', 'index', 'entry', 'POST');
$router->write('/worker/$worker/read', 'task', 'worker', 'POST');
$router->write('/worker/$id/fields', 'worker', 'fields', 'POST');
$router->write('/worker/$id/update', 'worker', 'update', 'POST');
$router->write('/worker/$id/fire', 'worker', 'fire', 'POST');
$router->write('/workers', 'worker', 'index', 'GET');
$router->write('/workers', 'worker', 'index', 'POST');
$router->write('/workers/read', 'worker', 'read', 'POST');
$router->write('/workers/list', 'worker', 'datalist', 'POST');
$router->write('/workers/create', 'worker', 'create', 'POST');
$router->write('/market/$market/read', 'task', 'market', 'POST');
$router->write('/market/$id/fields', 'market', 'fields', 'POST');
$router->write('/market/$id/update', 'market', 'update', 'POST');
$router->write('/markets', 'market', 'index', 'GET');
$router->write('/markets', 'market', 'index', 'POST');
$router->write('/markets/read', 'market', 'read', 'POST');
$router->write('/markets/list', 'market', 'datalist', 'POST');
$router->write('/markets/create', 'market', 'create', 'POST');
$router->write('/operators', 'operator', 'index', 'GET');
$router->write('/operators', 'operator', 'index', 'POST');
$router->write('/operators/read', 'operator', 'read', 'POST');
$router->write('/operators/create', 'operator', 'create', 'POST');
$router->write('/administrators', 'administrator', 'index', 'GET');
$router->write('/administrators', 'administrator', 'index', 'POST');
$router->write('/administrators/read', 'administrator', 'read', 'POST');
$router->write('/administrators/create', 'administrator', 'create', 'POST');
$router->write('/settings', 'settings', 'index', 'GET');
$router->write('/settings', 'settings', 'index', 'POST');
$router->write('/$id', 'account', 'index', 'GET');
$router->write('/$id', 'account', 'index', 'POST');
$router->write('/$id/fields', 'account', 'fields', 'POST');
$router->write('/$id/update', 'account', 'update', 'POST');
$router->write('/$id/delete', 'account', 'delete', 'POST');
$router->write('/session/worker', 'session', 'worker', 'POST');
$router->write('/session/write', 'session', 'write', 'POST');
$router->write('/session/read', 'session', 'read', 'POST');
@ -69,6 +82,8 @@ $router->write('/works/list', 'work', 'datalist', 'POST');
$router->write('/task/$task/read', 'task', 'task', 'POST');
$router->write('/task/$task/value', 'task', 'value', 'POST');
$router->write('/task/$task/confirm', 'task', 'confirm', 'POST');
$router->write('/task/$task/problem', 'task', 'problem', 'POST');
$router->write('/task/$task/complete', 'task', 'complete', 'POST');
$router->write('/task/$task/hide', 'task', 'hide', 'POST');
$router->write('/task/$task/remove', 'task', 'remove', 'POST');
$router->write('/task/$task/work', 'task', 'work', 'POST');
@ -80,6 +95,8 @@ $router->write('/task/$task/worker/update', 'task', 'update', 'POST');
$router->write('/task/$task/market/update', 'task', 'update', 'POST');
$router->write('/task/$task/publish', 'task', 'publish', 'POST');
$router->write('/task/$task/unpublish', 'task', 'unpublish', 'POST');
$router->write('/task/$task/chat', 'task', 'chat', 'POST');
$router->write('/task/$task/chat/send', 'task', 'message', 'POST');
$router->write('/elements/menu', 'index', 'menu', 'POST');
// Инициализация ядра

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,971 @@
"use strict";
if (typeof window.chat !== "function") {
// Not initialized
// Initialize of the class in global namespace
window.chat = class chat {
/**
* Заблокировать функцию закрытия всплывающего окна?
*/
static freeze = false;
/**
* Тело всплывающего окна (массив)
*/
static body = {};
/**
* Инициализирован класс?
*/
static initialized = false;
/**
* Управление кнопками (escape, enter...)
*
* Содержит функцию инициализирующую обработчики событий keydown для document
* функция деинициализируется с document при закрытии окна
*/
static buttons;
/**
* Инстанции активных чатов
*
* ['market', 'worker']
*/
static instances = [];
/**
* Ожидание зависимости: ядро
*
* @param {function} execute Функция, которая будет встроена в демпфер
* @param {mixed} args Аргументы встраиваемой функции
*
* @return {void}
*/
static core(execute, ...args) {
if (typeof execute === "function") {
// Получена функция
// Инициализация интервала для ожидания загрузки зависимостей
const interval = setInterval(() => {
if (typeof core === "function") {
// Инициализировано ядро
// Деинициализация интервала для ожидания загрузки записимостей
clearInterval(interval);
// Запуск выполнения
return execute(...args);
}
}, 100);
// Инициализация деинициализации интервала для ожидания загрузки зависимостей спустя большой срок времени
setTimeout(() => clearInterval(interval), 3000);
}
}
/**
* Ожидание зависимости: демпфер
*
* @param {function} execute Функция, которая будет встроена в демпфер
* @param {number} timer Количество миллисекунд для демпфера
* @param {mixed} args Аргументы встраиваемой функции
*
* @return {void}
*/
static damper(execute, timer = 3000, ...args) {
if (typeof execute === "function") {
// Получена функция
// Инициализация интервала для ожидания загрузки зависимостей
const interval = setInterval(() => {
if (typeof damper === "function") {
// Инициализирован демпфер
// Деинициализация интервала для ожидания загрузки записимостей
clearInterval(interval);
// Запуск выполнения
return damper(() => execute(...args), timer);
}
}, 100);
// Инициализация деинициализации интервала для ожидания загрузки зависимостей спустя большой срок времени
setTimeout(() => clearInterval(interval), 3000);
}
}
/**
* Авторизация
*
* @param {function} execute Функция, которая будет встроена в демпфер
* @param {mixed} args Аргументы встраиваемой функции
*
* @return {void}
*/
static authorization(execute, ...args) {
if (typeof execute === "function") {
// Получена функция
if (core.page === this.page) {
// Пройдена проверка на страницу
// Запуск выполнения
return execute(...args);
}
}
}
/**
* Сгенерировать окно с чатами
*
* @param {HTMLElement} row Строка
*
* @return {void}
*/
static popup = damper((row) => {
if (row instanceof HTMLElement) {
// Получена строка
// Инициализация идентификатора задачи
const id = row.getAttribute("id");
// Инициализация буфера выбранных строк
const buffer = [
...document.querySelectorAll('div[data-selected="true"]'),
];
// Удаление статуса активной строки у остальных строк
for (const element of buffer) {
element.removeAttribute("data-selected");
}
// Инициализация статуса активной строки (обрабатываемой в данный момент)
row.setAttribute("data-selected", "true");
// Инициализация оболочки всплывающего окна
this.body.wrap = document.createElement("div");
this.body.wrap.setAttribute("id", "popup");
// Инициализация оболочки всплывающего окна
const popup = document.createElement("section");
popup.classList.add("list");
// Инициализация заголовка всплывающего окна
const title = document.createElement("h3");
title.innerText = id;
// Инициализация оболочки с основной информацией
const main = document.createElement("section");
main.classList.add("main");
function generate_title(chat) {
if (typeof core === "function") {
// Найдено ядро
if (chat === "worker") {
// Чат: СОТРУДНИК <-> ОПЕРАТОР
if (core.interface === "worker") {
// Сотрудник
return "Оператор";
} else if (
core.interface === "operator" ||
core.interface === "administrator"
) {
// Оператор или администратор
return "Сотрудник";
} else return "Чат";
}
if (chat === "market") {
// Чат: МАГАЗИН <-> ОПЕРАТОР
if (core.interface === "market") {
// Магазин
return "Оператор";
} else if (
core.interface === "operator" ||
core.interface === "administrator"
) {
// Оператор или администратор
return "Магазин";
} else return "Чат";
}
} else return "Чат";
}
// Инициализация колонки чата: МАГАЗИН <-> ОПЕРАТОР
const market = document.createElement("div");
market.classList.add("column");
market.setAttribute("data-chat", "market");
// Инициализация чата
const market_chat = document.createElement("div");
// Инициализация заголовка всплывающего окна
const market_title = document.createElement("h4");
market_title.innerText = generate_title("market");
// Инициализация строки
const market_message = document.createElement("section");
market_message.classList.add("row", "merged", "message");
market_message.setAttribute("data-chat-element", "input");
// Инициализация поля воода сообщения
const market_textarea = document.createElement("textarea");
market_textarea.setAttribute("maxlength", "600");
market_textarea.setAttribute(
"placeholder",
"Текст сообщения",
);
market_textarea.setAttribute(
"onkeydown",
`if (this.value.length > 60) this.style.setProperty("resize", "vertical"); else this.style.removeProperty("resize");`,
);
// Инициализация кнопки отправки сообщения
const market_send = document.createElement("button");
market_send.classList.add("sea");
market_send.setAttribute(
"onclick",
`chat.send(this, this.previousElementSibling, document.getElementById('${id}'), 'market', 'message')`,
);
const market_icon = document.createElement("i");
market_icon.classList.add("icon", "enter");
market.prepend(market_title);
market.append(market_chat);
market_message.append(market_textarea);
market_send.append(market_icon);
market_message.append(market_send);
market.appendChild(market_message);
// Обновлять позицию окна с ошибками при изменении размера textarea (там position: absolute)
new MutationObserver(() => top(this.body.errors)).observe(
market_textarea,
{
attributes: true,
attributeFilter: ["style"],
},
);
// Инициализация функции игнорирования блокировки для выбранных кнопок
market_textarea.addEventListener("keydown", (e) => {
if (e.keyCode === 27) {
// Нажата кнопка: "escape"
// Вызов глобальной функции управления кнопками
this.buttons(e, true);
} else if (e.ctrlKey && e.keyCode === 13) {
// Нажаты кнопки: "control", "enter"
// Инициализация буфера с текущим статусом блокировки закрытия окна
const freeze = this.freeze;
// Блокировка закрытия окна (чтобы не вызвался click() через событие onclick)
this.freeze = true;
// Активация виртуальной кнопки "отправить сообщение"
market_send.click();
// Возвращение статуса блокировки закрытия окна
this.freeze = freeze;
}
});
// Добавление функции блокировки удаления окна и клавиш по событиям
market_textarea.addEventListener("focus", () => this.freeze = true);
market_textarea.addEventListener(
"focusout",
() => this.freeze = false,
);
// Инициализация колонки чата: СОТРУДНИК <-> ОПЕРАТОР
const worker = document.createElement("div");
worker.classList.add("column");
worker.setAttribute("data-chat", "worker");
// Инициализация чата
const worker_chat = document.createElement("div");
// Инициализация заголовка всплывающего окна
const worker_title = document.createElement("h4");
worker_title.innerText = generate_title("worker");
// Инициализация строки
const worker_message = document.createElement("section");
worker_message.classList.add("row", "merged", "message");
worker_message.setAttribute("data-chat-element", "input");
// Инициализация поля воода сообщения
const worker_textarea = document.createElement("textarea");
worker_textarea.setAttribute("maxlength", "600");
worker_textarea.setAttribute(
"placeholder",
"Текст сообщения",
);
worker_textarea.setAttribute(
"onkeydown",
`if (this.value.length > 60) this.style.setProperty("resize", "vertical"); else this.style.removeProperty("resize");`,
);
// Инициализация кнопки отправки сообщения
const worker_send = document.createElement("button");
worker_send.classList.add("sea");
worker_send.setAttribute(
"onclick",
`chat.send(this, this.previousElementSibling, document.getElementById('${id}'), 'worker', 'message')`,
);
const worker_icon = document.createElement("i");
worker_icon.classList.add("icon", "enter");
worker.prepend(worker_title);
worker.append(worker_chat);
worker_message.append(worker_textarea);
worker_send.append(worker_icon);
worker_message.append(worker_send);
worker.appendChild(worker_message);
// Обновлять позицию окна с ошибками при изменении размера textarea (там position: absolute)
new MutationObserver(() => top(this.body.errors)).observe(
worker_textarea,
{
attributes: true,
attributeFilter: ["style"],
},
);
// Инициализация функции игнорирования блокировки для выбранных кнопок
worker_textarea.addEventListener("keydown", (e) => {
if (e.keyCode === 27) {
// Нажата кнопка: "escape"
// Вызов глобальной функции управления кнопками
this.buttons(e, true);
} else if (e.ctrlKey && e.keyCode === 13) {
// Нажаты кнопки: "control", "enter"
// Инициализация буфера с текущим статусом блокировки закрытия окна
const freeze = this.freeze;
// Блокировка закрытия окна (чтобы не вызвался click() через событие onclick)
this.freeze = true;
// Активация виртуальной кнопки "отправить сообщение"
worker_send.click();
// Возвращение статуса блокировки закрытия окна
this.freeze = freeze;
}
});
// Добавление функции блокировки удаления окна и клавиш по событиям
worker_textarea.addEventListener("focus", () => this.freeze = true);
worker_textarea.addEventListener(
"focusout",
() => this.freeze = false,
);
// Инициализация окна с ошибками
this.body.errors = document.createElement("section");
this.body.errors.classList.add(
"errors",
"window",
"list",
"calculated",
"hidden",
);
this.body.errors.setAttribute("data-errors", true);
// Инициализация элемента-оболочки с ошибками для всплывающего окна
const errors = document.createElement("section");
errors.classList.add("body");
// Инициализация элемента-списка ошибок
const dl = document.createElement("dl");
// Инициализация активного всплывающего окна
const old = document.getElementById("popup");
if (old instanceof HTMLElement) {
// Найдено активное окно
// Деинициализация быстрых действий по кнопкам
document.removeEventListener("keydown", this.buttons);
// Сброс блокировки
this.freeze = false;
// Удаление активного окна
old.remove();
}
// Запись в документ
popup.appendChild(title);
if (typeof core === "function") {
// Найдено ядро
if (core.interface === "worker") {
// Сотрудник
main.appendChild(worker);
// Генерация чата
this.generate(row, worker_chat, "worker", true, false, true);
} else if (core.interface === "market") {
// Магазин
main.appendChild(market);
// Генерация чата
this.generate(row, market_chat, "market", true, false, true);
} else if (
core.interface === "operator" || core.interface === "administrator"
) {
// Оператор или администратор
main.appendChild(market);
main.appendChild(worker);
// Генерация чатов
this.generate(row, market_chat, "market", true, false, true);
this.generate(row, worker_chat, "worker", true, false, true);
}
}
popup.appendChild(main);
this.body.wrap.appendChild(popup);
errors.appendChild(dl);
this.body.errors.appendChild(errors);
this.body.wrap.appendChild(this.body.errors);
document.body.appendChild(this.body.wrap);
if (typeof core === "function") {
// Найдено ядро
if (core.interface === "worker") {
// Сотрудник
// Фокусировка
worker_textarea.focus();
} else if (core.interface === "market") {
// Магазин
// Фокусировка
market_textarea.focus();
} else if (
core.interface === "operator" || core.interface === "administrator"
) {
// Оператор или администратор
// Фокусировка
market_textarea.focus();
}
}
// Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup)
this.body.errors.calculate = (errors) => {
errors.style.setProperty(
"transition",
"0s",
);
errors.style.setProperty(
"--top",
popup.offsetTop + popup.offsetHeight + 12 + "px",
);
setTimeout(
() => errors.style.removeProperty("transition"),
100,
);
// Инициализация ширины окна с ошибками
this.body.errors.style.setProperty(
"--calculated-width",
popup.offsetWidth + "px",
);
};
this.body.errors.calculate(this.body.errors);
const resize = new ResizeObserver(() =>
this.body.errors.calculate(this.body.errors)
);
resize.observe(this.body.wrap);
// Инициализация функции закрытия всплывающего окна
const click = () => {
// Блокировка
if (this.freeze) return;
// Удаление всплывающего окна
this.body.wrap.remove();
// Инициализация буфера выбранных строк
const buffer = [
...document.querySelectorAll('div[data-selected="true"]'),
];
// Удаление статуса активной строки у остальных строк
for (const element of buffer) {
element.removeAttribute("data-selected");
}
// Деинициализация быстрых действий по кнопкам
document.removeEventListener("keydown", this.buttons);
// Деинициализация автообновления чата
for (const index in this.instances) {
clearInterval(this.instances[index]);
}
// Сброс блокировки
this.freeze = false;
};
// Инициализация функции добавления функции закрытия всплывающего окна
const enable = () => this.body.wrap.addEventListener("click", click);
// Инициализация функции удаления функции закрытия всплывающего окна
const disable = () =>
this.body.wrap.removeEventListener("click", click);
// Первичная активация функции удаления всплывающего окна
enable();
// Инициализация функции управления кнопками
this.buttons = (e, force = false) => {
// Блокировка
if (!force && this.freeze) return;
if (e.keyCode === 27) {
// Нажата кнопка: "escape"
// Удаление окна
click();
}
};
// Инициализация быстрых действий по кнопкам
document.addEventListener("keydown", this.buttons);
// Добавление функции удаления всплывающего окна по событиям
popup.addEventListener("mouseenter", disable);
popup.addEventListener("mouseleave", enable);
}
}, 300);
/**
* Сгенерировать чат (вызов демпфера)
*
* @param {HTMLElement} row Строка <div>
* @param {HTMLElement} wrap Оболочка с классом ".column" <div>
* @param {string} chat Тип чата (market, worker, both)
* @param {bool} scroll Прокрутить до последнего сообщения?
* @param {bool} sound Проигрывать звук уведомления о новом сообщении?
* @param {string} chat Тип чата (market, worker)
*
* @return {void}
*/
static generate(
row,
wrap,
chat = "worker",
scroll,
sound = true,
force = false,
) {
// Запуск выполнения
this._generate(row, wrap, chat, scroll, sound, force);
}
/**
* Сгенерировать чат (демпфер)
*
* @param {HTMLElement} row Строка <div>
* @param {HTMLElement} wrap Оболочка с классом ".column" <div>
* @param {string} chat Тип чата (market, worker, both)
* @param {bool} scroll Прокрутить до последнего сообщения?
* @param {bool} sound Проигрывать звук уведомления о новом сообщении?
* @param {bool} force Принудительное выполнение (используется в damper()
*
* @return {void}
*/
static _generate = damper(
async (
row,
wrap,
chat = "worker",
scroll,
sound = true,
force = false,
) => {
// Инициализация идентификатора
const id = row.getAttribute("id");
// Запуск отсрочки разблокировки на случай, если сервер не отвечает
const timeout = setTimeout(() => {
this.errors(["Сервер не отвечает"]);
}, 5000);
// Запрос к серверу
return await fetch(`/task/${id}/chat`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: `chat=${chat}`,
})
.then((response) => response.json())
.then((data) => {
// Удаление отсрочки разблокировки
clearTimeout(timeout);
if (this.errors(data.errors, false, false)) {
// Сгенерированы ошибки
} else {
// Не сгенерированы ошибки (подразумевается их отсутствие)
if (
typeof data.chat === "string" && data.chat.length > 0 &&
wrap instanceof HTMLElement && document.body.contains(wrap)
) {
// Сгенерирован чат
// Инициализация родителя для реинициализации оболочки
const parent = wrap.parentElement;
// Инициализация значения прокрутки
const scrollTop = wrap.scrollTop;
// Подсчёт количества сообщений
const messages = wrap.children.length;
// Запись в оболочку
wrap.outerHTML = data.chat;
// Реинициаизация оболочки
wrap = parent.querySelector(
'section[data-chat-element="messages"]',
);
// Если сообщений стало больше, то проиграть звук уведомления
if (sound && wrap.children.length > messages) {
window.notification.play();
}
// Перерасчёт положения окна ошибок
this.body.errors.calculate(this.body.errors);
// Прокрутка до актуального значения прокрутки
wrap.scrollTop = scrollTop;
if (
(data.unreaded || scroll) &&
wrap.lastElementChild instanceof HTMLElement
) {
// Получено новое сообщение, либо запрошена прокрутка, и найдено последнее сообщение в чате
// Прокрутка до последнего сообщения
wrap.lastElementChild.scrollIntoView();
}
}
if (typeof data.row === "string" && data.row.length > 0) {
// Получена строка
// Инициализация буфера списка классов
// const buffer = [...row.classList];
// Инициализация статуса активной строки
const selected = row.getAttribute("data-selected");
// Реинициализация строки
row.outerHTML = data.row;
// Реинициализация строки (выражение странное, но правильное)
row = document.getElementById(row.getAttribute("id"));
// Копирование классов из буфера классов удалённой строки
// row.classList.add(...buffer);
// Копирование статуса активной строки
if (typeof selected === "string" && selected === "true") row.setAttribute("data-selected", "true");
}
// Деинициализация устаревшего интервала обновления чата
if (typeof this.instances[chat] !== "undefined") {
clearTimeout(this.instances[chat]);
}
// Инициализация интервала обновления чата
this.instances[chat] = setTimeout(
() => this.generate(row, wrap, chat, false, true, force),
10000,
);
}
});
},
500,
5,
);
/**
* Отправить сообщение
*
* @param {HTMLElement} button Кнопка <button>
* @param {HTMLElement} textarea Поле ввода сообщения <textarea>
* @param {HTMLElement} row Строка <div>
* @param {string} chat Тип чата (market, worker, both)
* @param {string} type Тип сообщения (message, error)
* @param {bool} open Открыть чат после отправки
* @param {string} text Тест сообщения, вместо текста из второго аргумента "textarea"
*
* @return {void}
*/
static send = damper(
async (
button,
textarea,
row,
chat = "worker",
type = "message",
open = false,
text,
) => {
if (
button instanceof HTMLElement &&
(textarea instanceof HTMLElement ||
(typeof text === "string" && text.length > 0)) &&
row instanceof HTMLElement
) {
// Получена кнопка, поле ввода сообщения и строка
// Инициализация идентификатора строки
const id = row.getAttribute("id");
if (typeof id === "string") {
// Инициализирован идентификатор
// Запрос к серверу
return await fetch(`/task/${id}/chat/send`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: `chat=${chat}&type=${type}&text=${
typeof text === "string" && text.length > 0
? text
: textarea.value
}`,
})
.then((response) => response.json())
.then((data) => {
if (this.errors(data.errors)) {
// Сгенерированы ошибки
} else {
// Не сгенерированы ошибки (подразумевается их отсутствие)
if (open) {
// Запрошено открытие окна чата
// Деинициализация быстрых действий по кнопкам
document.removeEventListener("keydown", this.buttons);
// Сброс блокировки
this.freeze = false;
// Реинициализация активного окна (чат)
this.popup(row);
}
if (data.sended) {
// Отправлено сообщение
if (
textarea instanceof HTMLElement &&
document.body.contains(textarea)
) {
// Найдено поле для ввода сообщения
// Сброс поля для ввода сообщения
textarea.value = "";
textarea.style.removeProperty("height");
textarea.style.removeProperty("resize");
// Регенерация окна чата
this.generate(
row,
textarea.parentElement.previousElementSibling,
chat,
true,
false,
true,
);
}
}
if (typeof data.row === "string" && data.row.length > 0) {
// Получена строка
// Инициализация буфера списка классов
/* const buffer = [...row.classList].filter(function (index) {
return type !== "solution" ||
(type === "solution" && index !== "problematic");
}); */
// Инициализация статуса активной строки
const selected = row.getAttribute("data-selected");
// Реинициализация строки
row.outerHTML = data.row;
// Реинициализация строки (выражение странное, но правильное)
row = document.getElementById(row.getAttribute("id"));
// Копирование классов из буфера классов удалённой строки
// row.classList.add(...buffer);
// Копирование статуса активной строки
if (typeof selected === "string" && selected === "true") row.setAttribute("data-selected", "true");
}
}
});
}
}
},
300,
);
/**
* Сгенерировать HTML-элемент со списком ошибок
*
* @param {object} registry Реестр ошибок
* @param {bool} render Отобразить в окне с ошибками?
* @param {bool} clean Очистить окно с ошибками перед добавлением?
*
* @return {bool} Сгенерированы ошибки?
*/
static errors(registry, render = true, clean = true) {
// Инициализация ссылки на HTML-элемент с ошибками
const wrap = document.body.contains(this.body.errors)
? this.body.errors
: document.querySelector('[data-errors="true"]');
if (wrap instanceof HTMLElement && document.body.contains(wrap)) {
// Найден HTML-элемент с ошибками
// Перерасчёт высоты элемента
function height() {
wrap.classList.remove("hidden");
wrap.classList.remove("animation");
// Реинициализация переменной с данными о высоте HTML-элемента (16 - это padding-top + padding-bottom у div#popup > section.errors)
wrap.style.setProperty(
"--height",
wrap.offsetHeight - 16 + "px",
);
wrap.classList.add("animation");
wrap.classList.add("hidden");
}
// Инициализация элемента-списка ошибок
const list = wrap.getElementsByTagName("dl")[0];
// Удаление ошибок из прошлой генерации
if (clean) list.innerHTML = null;
for (const 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);
list.appendChild(wrap);
// Реинициализация высоты
height();
// Обработка вложенных ошибок (вход в рекурсию)
this.errors(registry[error], false);
} else {
// Текст ошибки (подразумевается)
// Инициализация HTML-элемента
const wrap = document.createElement("dd");
// Запись текста ошибки
samp.innerText = registry[error];
// Запись HTML-элемента в список
wrap.appendChild(samp);
list.appendChild(wrap);
// Реинициализация высоты
height();
}
}
if (render) {
// Запрошена отрисовка
if (
list.childElementCount !== 0
) {
// Найдены ошибки
// Сброс анимации
// УЛОВКА: таким образом не запускается анимация до взаимодействия с элементом (исправлял это в CSS, но не помню как)
wrap.classList.add("animation");
// Отображение
wrap.classList.remove("hidden");
} else {
// Не найдены ошибки
// Скрытие
wrap.classList.add("hidden");
}
}
return list.childElementCount === 0 ? false : true;
}
return false;
}
};
}
// Вызов события: "инициализировано"
document.dispatchEvent(
new CustomEvent("chat.initialized", {
detail: { chat: window.chat },
}),
);

View File

@ -5,10 +5,11 @@
*
* @param {function} func Функция
* @param {number} timeout Таймер (ms)
* @param {number} force Номер аргумента хранящего статус принудительного выполнения
*
* @return {void}
*/
function damper(func, timeout = 300) {
function damper(func, timeout = 300, force) {
// Инициализация таймера
let timer;
@ -16,10 +17,18 @@ function damper(func, timeout = 300) {
// Деинициализация таймера
clearTimeout(timer);
// Вызов функции (вход в рекурсию)
timer = setTimeout(() => {
if (typeof force === 'number' && args[force]) {
// Принудительное выполнение (игнорировать таймер)
func.apply(this, args);
}, timeout);
} else {
// Обычное выполнение
// Вызов функции (вход в рекурсию)
timer = setTimeout(() => {
func.apply(this, args);
}, timeout);
}
};
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -1,21 +1,25 @@
{% if page != null %}<!-- PAGE #{{ page }} -->{% endif %}
{% for row in rows %}
<div id="{{ row.account._key }}" class="row {{ row.account.status }}" data-row="administrator">
<span data-column="id" title="{{ row.account._key }}" onclick="administrators.administrator.popup(this.parentElement)">{{
<div id="{{ row.account._key }}"
class="row{% if row.account.active is same as(true) %} active{% else %} hided{% endif %}" data-row="administrator">
<span class="unselectable interactive" data-column="account" title="{{ row.account._key }}"
onclick="administrators.update(this.parentElement)">{{
row.account._key }}</span>
<span data-column="name"
<span class="unselectable interactive" data-column="name"
title="{% if row.account.name.first is not empty %}{{ row.account.name.first }}{% endif %}{% if row.account.name.second is not empty %} {{ row.account.name.second }}{% endif %}{% if row.account.name.last is not empty %} {{ row.account.name.last }}{% endif %}"
onclick="administrators.administrator.popup(this.parentElement)">{% if row.account.name.first is not empty %}{{
onclick="navigator.clipboard.writeText('{% if row.account.name.first is not empty %}{{ row.account.name.first }}{% endif %}{% if row.account.name.second is not empty %} {{ row.account.name.second }}{% endif %}{% if row.account.name.last is not empty %} {{ row.account.name.last }}{% endif %}')">{%
if row.account.name.first is not empty %}{{
row.account.name.first|slice(0, 1)|upper }}.{% endif %}{% if row.account.name.last is not empty %} {{
row.account.name.last|slice(0, 1)|upper }}.{% endif %}{% if row.account.name.second is not empty %} {{
row.account.name.second }}{% endif %}</span>
<span data-column="number" onclick="administrators.administrator.popup(this.parentElement)">{{ row.account.number }}</span>
<span data-column="mail" onclick="administrators.administrator.popup(this.parentElement)">{{ row.account.mail }}</span>
<span data-column="requisites" onclick="administrators.administrator.popup(this.parentElement)">{{ row.account.requisites }} {{
row.account.payment }}</span>
<span data-column="commentary" title="{{ row.account.commentary }}"
onclick="administrators.commentary.popup(this.parentElement)">{{ row.account.commentary }}</span>
<span data-column="status" title="Непрочитанные сообщения" onclick="administrators.status(this.parentElement)">{{
row.account.status }}</span>
<span class="unselectable interactive" data-column="number">{% if row.account.active %}<a
href="tel:{{ row.account.number }}" title="Позвонить">{{
row.account.number|storaged_number_to_readable }}</a>{% else %}{{ row.account.number|storaged_number_to_readable
}}{% endif %}</span>
<span class="unselectable interactive" data-column="mail">{% if row.account.active %}<a
href="mailto:{{ row.account.mail }}" title="Написать письмо">{{ row.account.mail }}</a>{%
else %}{{ row.account.mail }}{% endif %}</span>
<span class="unselectable interactive" data-column="commentary" title="{{ row.account.commentary }}">{{
row.account.commentary }}</span>
</div>
{% endfor %}

View File

@ -0,0 +1,8 @@
<section class="row merged chat cloud rounded" data-chat-element="messages">
{% for message in messages %}
<div class="message {% if message.type != 'message' %}{{ message.type }}{% endif %}">
<h3 class="coal"><b>{{ message.from._key }}</b> <span class="unselectable">{{ message.from.type|account_type_to_russian }}</span> <span class="date unselectable"><b>{{ message.date|date('H:i d.m.Y') }}</b></span></h3>
<p>{{ message.text }}</p>
</div>
{% endfor %}
</section>

View File

@ -1,13 +1,16 @@
<!-- 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') -%}
{% for key, data in market | filter((data, key) => key != '_key') -%}
{% 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' %}
data.value|date('Y.m.d H:i:s') }}</span>
{% elseif key == 'number' %}
<span id="{{ market.id.value }}_number"><b>{{ data.label }}:</b><a href="tel:{{ data.value }}" title="Позвонить">{{
data.value }}</a></span>
{% elseif key == 'mail' %}
<span id="{{ worker.id.value }}_number"><b>{{ data.label }}:</b><a href="mailto:{{ data.value }}" title="Написать">{{
data.value }}</a></span>
{% elseif key == 'name' %}
<span id="{{ worker._key.value }}_{{ key }}"><b>{{ data.label }}:</b>{% if data.value.first is not empty %}{{ data.value.first }}{% endif %}{% if data.value.second is not empty %} {{ data.value.second }}{% endif %}{% if data.value.last is not empty %} {{ data.value.last }}{% endif %}</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 %}

View File

@ -1,9 +1,8 @@
<!-- 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>
data.value|date('d.m.Y H:i') }}</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>

View File

@ -0,0 +1,13 @@
<!-- TASK #{{ task._key.value }} -->
{% for key, data in task | filter((data, key) => key != '_key') -%}
{% if (key == 'created' or key == 'updated' or key == 'start' or key == 'end') %}
<span id="{{ task.id.value }}_{{ key }}"><b>{{ data.label }}:</b>{{ data.value is empty ? 'Никогда' :
data.value|date('d.m.Y H:i', ) }}</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

@ -1,16 +1,21 @@
<!-- 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') -%}
{% for key, data in worker | filter((data, key) => key != '_key') -%}
{% 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>
<span id="{{ worker._key.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' or key == 'issued' %}
<span id="{{ worker._key.value }}_{{ key }}"><b>{{ data.label }}:</b>{{ data.value|date('Y.m.d') }}</span>
{% elseif key == 'number' %}
<span id="{{ worker.id.value }}_number"><b>{{ data.label }}:</b><a href="tel:{{ data.value }}" title="Позвонить">{{
<span id="{{ worker._key.value }}_number"><b>{{ data.label }}:</b><a href="tel:{{ data.value }}" title="Позвонить">{{
data.value }}</a></span>
{% elseif key == 'mail' %}
<span id="{{ worker.id.value }}_number"><b>{{ data.label }}:</b><a href="mailto:{{ data.value }}" title="Написать">{{
data.value }}</a></span>
{% elseif key == 'name' %}
<span id="{{ worker._key.value }}_{{ key }}"><b>{{ data.label }}:</b>{% if data.value.first is not empty %}{{ data.value.first }}{% endif %}{% if data.value.second is not empty %} {{ data.value.second }}{% endif %}{% if data.value.last is not empty %} {{ data.value.last }}{% endif %}</span>
{% elseif key == 'department' %}
<span id="{{ worker._key.value }}_{{ key }}"><b>{{ data.label }}:</b>{{ (data.value.number ~ ' ' ~ data.value.address)|trim }}</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>
<span id="{{ worker._key.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

@ -1,17 +1,16 @@
{% if page != null %}<!-- PAGE #{{ page }} -->{% endif %}
{% for row in rows %}
<div id="{{ row.market._key }}" class="row {{ row.market.status }}" data-row="market">
<span data-column="id" title="{{ row.market.id }}" onclick="markets.market.popup(this.parentElement)">{{
row.market.id }}</span>
<span data-column="director" title="{% if row.market.director.first is not empty %}{{ row.market.director.first }}{% endif %}{% if row.market.director.second is not empty %} {{ row.market.director.second }}{% endif %}{% if row.market.director.last is not empty %} {{ row.market.director.last }}{% endif %}" onclick="markets.market.popup(this.parentElement)">{% if row.market.director.first is not empty %}{{ row.market.director.first|slice(0, 1)|upper }}.{% endif %}{% if row.market.director.last is not empty %} {{ row.market.director.last|slice(0, 1)|upper }}.{% endif %}{% if row.market.director.second is not empty %} {{ row.market.director.second }}{% endif %}</span>
<span data-column="number" onclick="operators.market.popup(this.parentElement)">{{ row.market.number }}</span>
<span data-column="address" title="{{ row.market.city }} {{ row.market.district }} {{ row.market.address }}"
onclick="markets.market.popup(this.parentElement)">{{ row.market.city }} {{ row.market.district }} {{
<div id="{{ row.account._key }}" class="row{% if row.account.active is same as(true) %} active{% else %} hided{% endif %}" data-row="market">
<span class="unselectable interactive" data-column="account" title="Аккаунт" onclick="markets.account.update(this.parentElement)">{{ row.account._key }}</span>
<span class="unselectable interactive" data-column="market" title="Магазин" onclick="markets.update(this.parentElement)">{{ row.market._key }}</span>
<span class="unselectable interactive" data-column="name" title="{% if row.market.name.first is not empty %}{{ row.market.name.first }}{% endif %}{% if row.market.name.second is not empty %} {{ row.market.name.second }}{% endif %}{% if row.market.name.last is not empty %} {{ row.market.name.last }}{% endif %}">{% if row.market.name.first is not empty %}{{ row.market.name.first|slice(0, 1)|upper }}.{% endif %}{% if row.market.name.last is not empty %} {{ row.market.name.last|slice(0, 1)|upper }}.{% endif %}{% if row.market.name.second is not empty %} {{ row.market.name.second }}{% endif %}</span>
<span class="unselectable interactive" data-column="number"><a href="tel:{{ row.market.number }}" title="Позвонить">{{ row.market.number|storaged_number_to_readable }}</a></span>
<span class="unselectable interactive" data-column="mail"><a href="mailto:{{ row.market.mail }}" title="Написать письмо">{{ row.market.mail }}</a></span>
<span class="unselectable interactive" data-column="address" title="{{ row.market.city }} {{ row.market.district }} {{ row.market.address }}"
>{{ row.market.city }} {{ row.market.district }} {{
row.market.address }}</span>
<span data-column="type" onclick="markets.market.popup(this.parentElement)">{{ row.market.type }}</span>
<span data-column="commentary" title="{{ row.market.commentary }}"
onclick="markets.commentary.popup(this.parentElement)">{{ row.market.commentary }}</span>
<span data-column="status" title="Непрочитанные сообщения" onclick="markets.status(this.parentElement)">{{
row.market.status }}</span>
<span class="unselectable interactive" data-column="type">{{ row.market.type }}</span>
<span class="unselectable interactive" data-column="commentary" title="{{ row.market.commentary }}"
onclick="navigator.clipboard.writeText('{{ row.account.commentary }}')">{{ row.account.commentary }}</span>
</div>
{% endfor %}

View File

@ -1,21 +1,21 @@
{% if page != null %}<!-- PAGE #{{ page }} -->{% endif %}
{% for row in rows %}
<div id="{{ row.account._key }}" class="row {{ row.account.status }}" data-row="operator">
<span data-column="id" title="{{ row.account._key }}" onclick="operators.operator.popup(this.parentElement)">{{
<div id="{{ row.account._key }}"
class="row{% if row.account.active is same as(true) %} active{% else %} hided{% endif %}" data-row="operator">
<span class="unselectable interactive" data-column="account" title="{{ row.account._key }}"
onclick="operators.update(this.parentElement)">{{
row.account._key }}</span>
<span data-column="name"
<span class="unselectable interactive" data-column="name"
title="{% if row.account.name.first is not empty %}{{ row.account.name.first }}{% endif %}{% if row.account.name.second is not empty %} {{ row.account.name.second }}{% endif %}{% if row.account.name.last is not empty %} {{ row.account.name.last }}{% endif %}"
onclick="operators.operator.popup(this.parentElement)">{% if row.account.name.first is not empty %}{{
onclick="navigator.clipboard.writeText('{% if row.account.name.first is not empty %}{{ row.account.name.first }}{% endif %}{% if row.account.name.second is not empty %} {{ row.account.name.second }}{% endif %}{% if row.account.name.last is not empty %} {{ row.account.name.last }}{% endif %}')">{% if row.account.name.first is not empty %}{{
row.account.name.first|slice(0, 1)|upper }}.{% endif %}{% if row.account.name.last is not empty %} {{
row.account.name.last|slice(0, 1)|upper }}.{% endif %}{% if row.account.name.second is not empty %} {{
row.account.name.second }}{% endif %}</span>
<span data-column="number" onclick="operators.operator.popup(this.parentElement)">{{ row.account.number }}</span>
<span data-column="mail" onclick="operators.operator.popup(this.parentElement)">{{ row.account.mail }}</span>
<span data-column="requisites" onclick="operators.operator.popup(this.parentElement)">{{ row.account.requisites }} {{
row.account.payment }}</span>
<span data-column="commentary" title="{{ row.account.commentary }}"
onclick="operators.commentary.popup(this.parentElement)">{{ row.account.commentary }}</span>
<span data-column="status" title="Непрочитанные сообщения" onclick="operators.status(this.parentElement)">{{
row.account.status }}</span>
<span class="unselectable interactive" data-column="number"><a href="tel:{{ row.account.number }}"
title="Позвонить">{{ row.account.number|storaged_number_to_readable }}</a></span>
<span class="unselectable interactive" data-column="mail"><a href="mailto:{{ row.account.mail }}"
title="Написать письмо">{{ row.account.mail }}</a></span>
<span class="unselectable interactive" data-column="commentary" title="{{ row.account.commentary }}">{{
row.account.commentary }}</span>
</div>
{% endfor %}

View File

@ -1,37 +1,46 @@
{% if page != null %}<!-- PAGE #{{ page }} -->{% endif %}
{% for row in rows %}
<div id="{{ row.task._key }}" style="--columns: 12;"
class="row reinitializable{% if row.task.confirmed %} confirmed{% endif %}{% if row.task.published %} published{% endif %}{% if row.task.hided %} hided{% endif %}" data-row="task">
<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')
<div id="{{ row.task._key }}" style="--columns: 13;"
class="row reinitializable{% if row.task.confirmed %} confirmed{% endif %}{% if row.task.published %} published{% endif %}{% if row.task.hided %} hided{% endif %}{% if date() >= row.task.start %}{% if date() <= row.task.end %} coming{% else %} passed{% endif %}{% endif %}{% if row.task.completed %} completed{% endif %}{% if row.task.problematic %} problematic{% endif %}"
data-row="task">
<span class="unselectable interactive" data-column="date" title="Заявка создана: {{ row.task.created is empty ? 'Никогда' :
row.task.created|date('d.m.Y H:i') }}; Заявка обновлена: {{ row.task.updated is empty ? 'Никогда' :
row.task.updated|date('d.m.Y H:i') }}" {% if account.type !='worker' %}onclick="tasks.popup(this.parentElement)" {%
endif %}>{{ 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="{% if row.worker.name.first is not empty %}{{ row.worker.name.first }}{% endif %}{% if row.worker.name.second is not empty %} {{ row.worker.name.second }}{% endif %}{% if row.worker.name.last is not empty %} {{ row.worker.name.last }}{% endif %}" onclick="tasks.worker.popup(this.parentElement)">{% if row.worker.name.first is not empty %}{{ row.worker.name.first|slice(0, 1)|upper }}.{% endif %}{% if row.worker.name.last is not empty %} {{ row.worker.name.last|slice(0, 1)|upper }}.{% endif %}{% if row.worker.name.second is not empty %} {{ row.worker.name.second }}{% endif %}</span>
<span data-column="task" title="{{ row.task.description }}" onclick="tasks.popup(this.parentElement)">{{ row.task.work
<span class="unselectable interactive" data-column="worker" title="{{ row.worker.id }}" {% if account.type !='worker'
%}onclick="tasks.worker.popup(this.parentElement)" {% endif %}>{{
row.worker._key }}</span>
<span class="unselectable interactive" data-column="name"
title="{% if row.worker.name.first is not empty %}{{ row.worker.name.first }}{% endif %}{% if row.worker.name.second is not empty %} {{ row.worker.name.second }}{% endif %}{% if row.worker.name.last is not empty %} {{ row.worker.name.last }}{% endif %}">{%
if row.worker.name.first is not empty %}{{
row.worker.name.first|slice(0, 1)|upper }}.{% endif %}{% if row.worker.name.last is not empty %} {{
row.worker.name.last|slice(0, 1)|upper }}.{% endif %}{% if row.worker.name.second is not empty %} {{
row.worker.name.second }}{% endif %}</span>
<span class="unselectable interactive" data-column="task" title="{{ row.task.description }}">{{ row.task.work
}}</span>
<span data-column="start" onclick="tasks.popup(this.parentElement)">{{
<span class="unselectable interactive" data-column="start">{{
row.task.generated.start }}</span>
<span data-column="end" onclick="tasks.popup(this.parentElement)">{{
<span class="unselectable interactive" data-column="end">{{
row.task.generated.end }}</span>
<span data-column="hours" onclick="tasks.popup(this.parentElement)">{{
<span class="unselectable interactive" data-column="hours">{{
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
<span class="unselectable interactive" data-column="market" {% if account.type !='market' and account.type !='worker'
%}onclick="tasks.market.popup(this.parentElement)" {% endif %}>{{
row.market._key }}</span>
<span class="unselectable interactive" data-column="address"
title="{% if row.market.city is not null %}{{ row.market.city }}, {% endif %}{{ row.market.address }}">{% 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)">{{
<span class="unselectable interactive" data-column="type" title="{{ row.market.type }}">{{
row.market.type }}</span>
<span data-column="tax" title="{{ row.worker.tax }}" onclick="tasks.worker.popup(this.parentElement)">{{
<span class="unselectable interactive" data-column="tax" title="{{ row.worker.tax }}">{{
row.worker.tax }}</span>
<span data-column="commentary" title="{{ row.task.commentary }}"
onclick="tasks.commentary.popup(this.parentElement)">{{
<span class="unselectable interactive" data-column="commentary" title="{{ row.task.commentary }}" {% if account.type
!='market' and account.type !='worker' %}onclick="tasks.commentary.popup(this.parentElement)" {% endif %}>{{
row.task.commentary }}</span>
<span data-column="chat" title="Непрочитанные сообщения" onclick="tasks.chat(this.parentElement)">{{
row.task.chat ?? 0 }}</span>
<span class="unselectable interactive" data-column="chat" title="Непрочитанные сообщения"
onclick="chat.popup(this.parentElement)">{{ row.task.generated.chat.unreaded ?? 0 }}</span>
</div>
{% endfor %}

View File

@ -1,23 +1,40 @@
{% if page != null %}<!-- PAGE #{{ page }} -->{% endif %}
{% for row in rows %}
<div id="{{ row.worker._key }}" class="row {{ row.worker.status }}" data-row="worker">
<span data-column="id" title="{{ row.worker.id }}" onclick="workers.worker.popup(this.parentElement)">{{
row.worker.id }}</span>
<span data-column="name" title="{% if row.worker.name.first is not empty %}{{ row.worker.name.first }}{% endif %}{% if row.worker.name.second is not empty %} {{ row.worker.name.second }}{% endif %}{% if row.worker.name.last is not empty %} {{ row.worker.name.last }}{% endif %}" onclick="workers.worker.popup(this.parentElement)">{% if row.worker.name.first is not empty %}{{ row.worker.name.first|slice(0, 1)|upper }}.{% endif %}{% if row.worker.name.last is not empty %} {{ row.worker.name.last|slice(0, 1)|upper }}.{% endif %}{% if row.worker.name.second is not empty %} {{ row.worker.name.second }}{% endif %}</span>
<span data-column="birth" onclick="workers.worker.popup(this.parentElement)">{{ row.worker.birth }}</span>
<span data-column="worker" onclick="workers.worker.popup(this.parentElement)"><a href="tel:{{ row.worker.number }}" title="Позвонить">{{ row.worker.number }}</a></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" onclick="workers.status(this.parentElement)">{{
row.worker.status }}</span>
<div id="{{ row.account._key }}"
class="row{% if row.account.active is same as(true) %} active{% else %} hided{% endif %}" data-row="worker">
<span class="unselectable interactive" data-column="account" title="Настройки аккаунта" onclick="workers.account.update(this.parentElement)">{{
row.account._key }}</span>
<span class="unselectable interactive" data-column="worker" title="Настройки сотрудника" onclick="workers.update(this.parentElement)">{{
row.worker._key }}</span>
<span class="unselectable interactive" data-column="name"
title="{% if row.worker.name.first is not empty %}{{ row.worker.name.first }}{% endif %}{% if row.worker.name.second is not empty %} {{ row.worker.name.second }}{% endif %}{% if row.worker.name.last is not empty %} {{ row.worker.name.last }}{% endif %}{% if row.worker.birth is not empty %} {{ row.worker.birth|date_to_russian }}{% endif %}" onclick="navigator.clipboard.writeText('{% if row.worker.name.first is not empty %}{{ row.worker.name.first }}{% endif %}{% if row.worker.name.second is not empty %} {{ row.worker.name.second }}{% endif %}{% if row.worker.name.last is not empty %} {{ row.worker.name.last }}{% endif %}{% if row.worker.birth is not empty %} {{ row.worker.birth|date_to_russian }}{% endif %}')">{%
if row.worker.name.first is not empty %}{{
row.worker.name.first|slice(0, 1)|upper }}.{% endif %}{% if row.worker.name.last is not empty %} {{
row.worker.name.last|slice(0, 1)|upper }}.{% endif %}{% if row.worker.name.second is not empty %} {{
row.worker.name.second }}{% endif %}</span>
<span class="unselectable interactive" data-column="number"><a href="tel:{{ row.worker.number }}"
title="Позвонить">{{ row.worker.number|storaged_number_to_readable }}</a></span>
<span class="unselectable interactive" data-column="mail"><a href="mailto:{{ row.worker.mail }}"
title="Написать">{{ row.worker.mail }}</a></span>
<span class="unselectable interactive" data-column="address"
title="{{ (row.worker.city ~ ' ' ~ row.worker.district ~ ' ' ~ row.worker.address)|trim }}"
onclick="navigator.clipboard.writeText('{{ row.worker.city ~ ' ' ~ row.worker.district ~ ' ' ~ row.worker.address }}')">{% if row.worker.city is not empty and row.worker.district is not
empty and row.worker.district is not null %}{{ row.worker.city|slice(0,4) ~ '. ' ~ row.worker.district|slice(0,3) ~
'. ' ~ row.worker.address }}{% else %}{{ row.worker.city ~ ' ' ~ row.worker.district ~ ' ' ~ row.worker.address }}{%
endif %}</span>
<span class="unselectable interactive" data-column="passport"
title="{{ (row.worker.passport ~ ', ' ~ row.worker.issued|date_to_russian ~ ', ' ~ row.worker.department.number ~ ', ' ~ row.worker.department.address)|trim(', ') }}"
onclick="navigator.clipboard.writeText('{{ (row.worker.passport ~ ', ' ~ row.worker.issued|date_to_russian ~ ', ' ~ row.worker.department.number ~ ', ' ~ row.worker.department.address)|trim(', ') }}')">{{ (row.worker.passport ~ ', ' ~
row.worker.issued|date_to_russian ~ ', ' ~ row.worker.department.number ~ ', ' ~
row.worker.department.address)|trim(', ') }}</span>
<span class="unselectable interactive" data-column="tax" onclick="navigator.clipboard.writeText('{{ row.worker.tax }}')">{{ row.worker.tax }}</span>
<span class="unselectable interactive" data-column="requisites"
title="{% if row.worker.requisites is not empty and row.worker.payment is not empty %}{{ row.worker.requisites }} ({{ row.worker.payment }}){% else %}{{ row.worker.payment }}{% endif %}"
onclick="navigator.clipboard.writeText('{{ row.worker.requisites|storaged_requisites_to_card }}')">{% if
row.worker.requisites is not empty and row.worker.payment is not empty %}{{
row.worker.requisites|storaged_requisites_preview }} ({{
row.worker.payment }}){% else %}{{ row.worker.payment }}{% endif %}</span>
<span class="unselectable interactive" data-column="commentary" title="{{ row.account.commentary }}"
onclick="navigator.clipboard.writeText('{{ row.account.commentary }}')">{{ row.account.commentary }}</span>
</div>
{% endfor %}

View File

@ -12,6 +12,7 @@
{% block css %}
<link type="text/css" rel="stylesheet" href="/css/main.css" />
<link type="text/css" rel="stylesheet" href="/css/themes/harmony/earth.css" />
<link type="text/css" rel="stylesheet" href="/css/popup.css" />
<link type="text/css" rel="stylesheet" href="/css/animations.css" />
{% endblock %}

View File

@ -11,6 +11,7 @@
{% endblock %}
{% block body %}
<audio id="notification" class="hidden" src="/sounds/notification.mp3" preload="auto"></audio>
{% if account is not null %}
{{ block('menu_body') }}
{% endif %}

View File

@ -1,3 +1,5 @@
{% for market in markets %}
<option value="{{ market.id }}">{{ market.id }}{% if market.director is not null %} {{ market.director }}{% endif %}</option>
<option value="{{ market.getKey() }}">{{ market.getKey() }}{% if market.name.first is not empty %} {{
market.name.first|slice(0, 1)|upper }}.{% endif %}{% if market.name.last is not empty %} {{ market.name.last|slice(0,
1)|upper }}.{% endif %}{% if market.name.second is not empty %} {{ market.name.second }}{% endif %}</option>
{% endfor %}

View File

@ -1,3 +1,6 @@
{% for worker in workers %}
<option value="{{ worker.id }}">{{ worker.id }}{% if worker.name is not null %} {{ worker.name }}{% endif %}</option>
<option value="{{ worker.getKey() }}">{{ worker.getKey() }}{% if worker.name.first is not empty %} {{
worker.name.first|slice(0, 1)|upper }}.{% endif %}{% if worker.name.last is not empty %} {{
worker.name.last|slice(0, 1)|upper }}.{% endif %}{% if worker.name.second is not empty %} {{
worker.name.second }}{% endif %}</option>
{% endfor %}

View File

@ -3,7 +3,7 @@
{% 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/administrators.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/nametag.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">
@ -17,7 +17,7 @@
<form id="actions" class="row menu separated" onsubmit="return false">
<label for="actions">
{% if account.type == 'administrator' or account.type == 'administrator' or account.type == 'administrator' %}
<button class="grass" onclick="administrators.create()">Создать</button>
<button class="grass dense" onclick="administrators.create()">Создать</button>
{% endif %}
</label>
</form>
@ -31,21 +31,21 @@
%}>Неактивный</button>
</label>
</form>
<form class="row menu wide stretched" onsubmit="return false">
<search class="row menu wide stretched">
<label class="solid">
<i class="icon search"></i>
<input class="clue merged right" type="search" name="search" id="search"
<input id="search" type="search" name="search" class="snow merged right"
onkeydown="if (event.keyCode === 13) this.nextElementSibling.click()" value="{{ cookies.administrators_filter_search ?? buffer.administrators.filters.search }}"
placeholder="Глобальный поиск по администраторам" />
<button class="sea merged left" onclick="administrators.search(this.previousSiblingElement, this)">Поиск</button>
<button class="sea merged left" onclick="administrators.search(this.previousElementSibling, this)">Поиск</button>
</label>
</form>
</search>
<div id="title" class="row unselectable">
<span data-column="id" class="button" title="Идентификатор"><i class="icon bold user"></i></span>
<span data-column="account" class="button" title="Аккаунт"><i class="icon bold nametag"></i></span>
<span data-column="name" class="button">ФИО</span>
<span data-column="number" class="button">Номер</span>
<span data-column="mail" class="button">Почта</span>
<span data-column="commentary" class="button">Комментарий</span>
<span data-column="status" class="button">Статус</span>
</div>
</section>
<script data-reinitializer-once="true">

View File

@ -7,7 +7,7 @@
{% endblock %}
{% block body %}
<section id="entry" class="panel medium">
<section id="entry" class="panel medium snow">
<section class="header unselectable">
<h1>Идентификация</h1>
</section>
@ -16,28 +16,32 @@
<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"
<input id="_administrator" class="stretched merged right snow borderless" 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>
<button type="submit" class="clay merged left" 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>
<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>
<input id="_password" class="stretched merged right snow borderless" name="password" type="password"
placeholder="Пароль" onkeypress="if (event.keyCode === 13) this.nextElementSibling.click()"
autocomplete="current-password">
<button type="submit" class="clay merged left" 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 id="errors" class="panel medium animation window hidden snow" style="--height: 300px">
<section class="body">
<dl></dl>
</section>
@ -71,7 +75,7 @@
fields.password.button = fields.password.label.getElementsByTagName('button')[0];
// Инициализация маски идентификатора администратора
IMask(fields.administrator.input, {mask: '000000000000'});
fields.administrator.input.mask = IMask(fields.administrator.input, {mask: '000000000000'});
/**
* Отправить входной псевдоним на сервер

View File

@ -7,7 +7,7 @@
{% endblock %}
{% block body %}
<section id="entry" class="panel medium">
<section id="entry" class="panel medium snow">
<section class="header unselectable">
<h1>Идентификация</h1>
</section>
@ -16,29 +16,31 @@
<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' }}"
<input id="_market" class="stretched merged right snow borderless" name="market" type="text"
placeholder="Идентификатор магазина" list="markets"
value="{{ session.buffer.entry.market._key ?? cookie.buffer_entry_market__key ?? '' }}"
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>
<button type="submit" class="clay merged left" 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>
<option value="{{ account.account.getKey() }}">{{ account.account.getKey()}} {{
account.account.name.first }} {{ account.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>
<input id="_password" class="stretched merged right snow borderless" name="password" type="password"
placeholder="Пароль" onkeypress="if (event.keyCode === 13) this.nextElementSibling.click()"
autocomplete="current-password">
<button type="submit" class="clay merged left" 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 id="errors" class="panel medium animation window hidden snow" style="--height: 300px">
<section class="body">
<dl></dl>
</section>
@ -72,7 +74,7 @@
fields.password.button = fields.password.label.getElementsByTagName('button')[0];
// Инициализация маски идентификатора магазина
IMask(fields.market.input, {mask: 'K000000'});
fields.market.input.mask = IMask(fields.market.input, {mask: '000000000000'});
/**
* Отправить входной псевдоним на сервер

View File

@ -7,7 +7,7 @@
{% endblock %}
{% block body %}
<section id="entry" class="panel medium">
<section id="entry" class="panel medium snow">
<section class="header unselectable">
<h1>Идентификация</h1>
</section>
@ -16,28 +16,28 @@
<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"
<input id="_operator" class="stretched merged right snow borderless" 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>
<button type="submit" class="clay merged left" 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>
<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="Пароль"
<input id="_password" class="stretched merged right snow borderless" 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>
<button type="submit" class="clay merged left" 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 id="errors" class="panel medium animation window hidden snow" style="--height: 300px">
<section class="body">
<dl></dl>
</section>
@ -71,7 +71,7 @@
fields.password.button = fields.password.label.getElementsByTagName('button')[0];
// Инициализация маски идентификатора оператора
IMask(fields.operator.input, {mask: '000000000000'});
fields.operator.input.mask = IMask(fields.operator.input, {mask: '000000000000'});
/**
* Отправить входной псевдоним на сервер

View File

@ -7,7 +7,7 @@
{% endblock %}
{% block body %}
<section id="entry" class="panel medium">
<section id="entry" class="panel medium snow">
<section class="header unselectable">
<h1>Идентификация</h1>
</section>
@ -16,23 +16,23 @@
<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="Номер"
<input id="_worker" class="stretched merged right snow borderless" 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>
<button type="submit" class="clay merged left" 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="Пароль"
<input id="_password" class="stretched merged right snow borderless" 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>
<button type="submit" class="clay merged left" 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 id="errors" class="panel medium animation window hidden snow" style="--height: 300px">
<section class="body">
<dl></dl>
</section>
@ -66,7 +66,7 @@
fields.password.button = fields.password.label.getElementsByTagName('button')[0];
// Инициализация маски номера
IMask(fields.worker.input, {mask: '+{7} (000) 000-00-00'});
fields.worker.input.mask = IMask(fields.worker.input, {mask: '+{7} (000) 000-00-00'});
/**
* Отправить входной псевдоним на сервер
@ -96,7 +96,7 @@
const timeout = setTimeout(() => {_errors(['Сервер не отвечает']); unblock()}, 5000);
// Запрос к серверу
const response = await session.worker(fields.worker.input.value);
const response = await session.worker(fields.worker.input.mask.unmaskedValue);
// Удаление отсрочки разблокировки
clearTimeout(timeout);

View File

@ -1,3 +1,4 @@
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;
((value - minValue) / (valueMax - valueMin)) * (totalInputWidth - thumbWidth - thumbHalfWidth) + thumbHalfWidth;

View File

@ -3,7 +3,8 @@
{% 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/cart.css">
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/nametag.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">
@ -17,7 +18,7 @@
<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>
<button class="grass dense" onclick="markets.create()">Создать</button>
{% endif %}
</label>
</form>
@ -31,22 +32,24 @@
%}>Неактивный</button>
</label>
</form>
<form class="row menu wide stretched" onsubmit="return false">
<search class="row menu wide stretched">
<label class="solid">
<i class="icon search"></i>
<input class="clue merged right" type="search" name="search" id="search"
<input id="search" type="search" name="search" class="snow merged right"
onkeydown="if (event.keyCode === 13) this.nextElementSibling.click()" value="{{ cookies.markets_filter_search ?? buffer.markets.filters.search }}"
placeholder="Глобальный поиск по магазинам" />
<button class="sea merged left" onclick="markets.search(this.previousSiblingElement, this)">Поиск</button>
<button class="sea merged left" onclick="markets.search(this.previousElementSibling, this)">Поиск</button>
</label>
</form>
</search>
<div id="title" class="row unselectable">
<span data-column="id" class="button" title="Идентификатор"><i class="icon bold user"></i></span>
<span data-column="director" class="button">Директор</span>
<span data-column="account" class="button" title="Аккаунт"><i class="icon bold nametag"></i></span>
<span data-column="market" class="button" title="Магазин"><i class="icon bold shopping cart"></i></span>
<span data-column="name" class="button">ФИО</span>
<span data-column="number" class="button">Номер</span>
<span data-column="mail" class="button">Почта</span>
<span data-column="address" class="button">Адрес</span>
<span data-column="type" class="button">Тип</span>
<span data-column="commentary" class="button">Комментарий</span>
<span data-column="status" class="button">Статус</span>
</div>
</section>
<script data-reinitializer-once="true">

View File

@ -3,7 +3,7 @@
{% 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/operators.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/nametag.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">
@ -17,7 +17,7 @@
<form id="actions" class="row menu separated" onsubmit="return false">
<label for="actions">
{% if account.type == 'administrator' or account.type == 'operator' or account.type == 'operator' %}
<button class="grass" onclick="operators.create()">Создать</button>
<button class="grass dense" onclick="operators.create()">Создать</button>
{% endif %}
</label>
</form>
@ -31,21 +31,21 @@
%}>Неактивный</button>
</label>
</form>
<form class="row menu wide stretched" onsubmit="return false">
<search class="row menu wide stretched">
<label class="solid">
<i class="icon search"></i>
<input class="clue merged right" type="search" name="search" id="search"
<input id="search" type="search" name="search" class="snow merged right"
onkeydown="if (event.keyCode === 13) this.nextElementSibling.click()" value="{{ cookies.operators_filter_search ?? buffer.operators.filters.search }}"
placeholder="Глобальный поиск по операторам" />
<button class="sea merged left" onclick="operators.search(this.previousSiblingElement, this)">Поиск</button>
<button class="sea merged left" onclick="operators.search(this.previousElementSibling, this)">Поиск</button>
</label>
</form>
</search>
<div id="title" class="row unselectable">
<span data-column="id" class="button" title="Идентификатор"><i class="icon bold user"></i></span>
<span data-column="account" class="button" title="Аккаунт"><i class="icon bold nametag"></i></span>
<span data-column="name" class="button">ФИО</span>
<span data-column="number" class="button">Номер</span>
<span data-column="mail" class="button">Почта</span>
<span data-column="commentary" class="button">Комментарий</span>
<span data-column="status" class="button">Статус</span>
</div>
</section>
<script data-reinitializer-once="true">

View File

@ -10,6 +10,7 @@
<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">
<link type="text/css" rel="stylesheet" data-reinitializer-once="true" href="/css/icons/enter.css">
{% endblock %}
{% block body %}
@ -17,7 +18,7 @@
<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>
<button class="grass dense" onclick="tasks.create()">Создать</button>
{% endif %}
{% if account.type == 'administrator' or account.type == 'operator' %}
<button class="sea" onclick="">Выгрузка</button>
@ -26,55 +27,64 @@
</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') }}"
<input class="snow" 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') }}"
<input class="snow" 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');"
oninput="this.nextElementSibling.innerText = this.value; this.nextElementSibling.style.setProperty('--left', ((this.value / 5) * (this.offsetWidth - 24) + 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();
document.getElementById('rating').oninput();
document.addEventListener("DOMContentLoaded", function() {
// 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>
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>
{% endif %}>Неподтверждена</button>
{% if account.type == 'administrator' or account.type == 'operator' or account.type == 'market' %}
<button class="{{ published ?? 'earth' }}" onclick="tasks.filter('published', null, this); tasks.reinit()" {% if
published=='sand' %}title="... и опубликованные" {% elseif published=='river'
%}title="... или" {% endif %}>Опубликован</button>
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>
%}title="... или неопубликованные" {% endif %}>Неопубликована</button>
{% endif %}
<button class="{{ problematic ?? 'earth' }}" onclick="tasks.filter('problematic', null, this); tasks.reinit()" {%
if problematic=='sand' %}title="... и проблемные" {% elseif problematic=='river'
%}title="... или проблемные" {% endif %}>Проблемный</button>
if problematic=='sand' %}title="... и проблемные" {% elseif problematic=='river' %}title="... или проблемные" {%
endif %}>Проблемная</button>
{% if account.type == 'administrator' or account.type == 'operator' %}
<button class="{{ hided ?? 'earth' }}" onclick="tasks.filter('hided', null, this); tasks.reinit()" {% if
hided=='sand' %}title="... и скрытые" {% elseif hided=='river' %}title="... или скрытые" {% endif
%}>Скрыт</button>
%}>Скрыта</button>
{% endif %}
<button class="{{ completed ?? 'earth' }}" onclick="tasks.filter('completed', null, this); tasks.reinit()" {% if
completed=='sand' %}title="... и завершённые" {% elseif completed=='river'
%}title="... или завершённые" {% endif %}>Завершён</button>
completed=='sand' %}title="... и завершённые" {% elseif completed=='river' %}title="... или завершённые" {%
endif %}>Завершена</button>
</label>
</form>
<form class="row menu wide stretched" onsubmit="return false">
<search class="row menu wide stretched">
<label class="solid">
<i class="icon search"></i>
<input class="clue merged right" type="search" name="search" id="search"
<input id="search" type="search" name="search" class="snow merged right"
onkeydown="if (event.keyCode === 13) this.nextElementSibling.click()"
value="{{ cookies.tasks_filter_search ?? buffer.tasks.filters.search }}"
placeholder="Глобальный поиск по задачам" />
<button class="sea merged left" onclick="tasks.search(this.previousSiblingElement, this)">Поиск</button>
<button class="sea merged left" onclick="tasks.search(this.previousElementSibling, this)">Поиск</button>
</label>
</form>
</search>
<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>
@ -83,7 +93,7 @@
<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="market" class="button" title="Магазин"><i class="icon bold 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>
@ -134,4 +144,5 @@
<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>
<script type="text/javascript" src="/js/chat.js" defer></script>
{% endblock %}

View File

@ -1,140 +0,0 @@
{% 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

@ -4,6 +4,7 @@
<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/nametag.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">
@ -17,29 +18,19 @@
<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>
<button class="grass dense" 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
<button class="{{ fined ?? 'earth' }} separated" 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
@ -53,25 +44,26 @@
%}>Уволен</button>
</label>
</form>
<form class="row menu wide stretched" onsubmit="return false">
<search class="row menu wide stretched"">
<label class="solid">
<i class="icon search"></i>
<input class="clue merged right" type="search" name="search" id="search"
<input id="search" type="search" name="search" class="snow merged right"
onkeydown="if (event.keyCode === 13) this.nextElementSibling.click()" value="{{ cookies.workers_filter_search ?? buffer.workers.filters.search }}"
placeholder="Глобальный поиск по сотрудникам" />
<button class="sea merged left" onclick="workers.search(this.previousSiblingElement, this)">Поиск</button>
<button class="sea merged left" onclick="workers.search(this.previousElementSibling, this)">Поиск</button>
</label>
</form>
</search>
<div id="title" class="row unselectable">
<span data-column="id" class="button" title="Идентификатор"><i class="icon bold user"></i></span>
<span data-column="account" class="button" title="Аккаунт"><i class="icon bold nametag"></i></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="birth" class="button">Дата</span>
<span data-column="number" class="button">Номер</span>
<span data-column="passport" class="button">Паспорт</span>
<span data-column="mail" class="button">Почта</span>
<span data-column="address" class="button">Адрес</span>
<span data-column="passport" 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">

View File

@ -14,7 +14,9 @@ use mirzaev\minimal\controller;
// Шаблонизатор представлений
use Twig\Loader\FilesystemLoader,
Twig\Environment as twig,
Twig\Extra\Intl\IntlExtension as intl;
Twig\Extra\Intl\IntlExtension as intl,
Twig\TwigFilter;
// Встроенные библиотеки
use ArrayAccess;
@ -57,6 +59,55 @@ final class templater extends controller implements ArrayAccess
}
if (!empty($account->status())) $this->twig->addGlobal('account', $account);
// Инициализация фильтров
$this->twig->addFilter(
new TwigFilter(
'storaged_number_to_readable',
fn (int|string|null $number = null) => strlen($number = (string) $number) === 11 ? preg_replace('/(^\d)(\d{3})(\d{3})(\d{2})(\d{2}$)/', '+$1 ($2) $3-$4-$5', $number) : $number
)
);
// Инициализация фильтров
$this->twig->addFilter(
new TwigFilter(
'storaged_requisites_to_card',
function (string|null $requisites = null) {
preg_match('/^\d{4}\s\d{4}\s\d{4}\s\d{4}/', $requisites, $matches);
return isset($matches[0]) ? $matches[0] : $requisites;
}
)
);
// Инициализация фильтров
$this->twig->addFilter(
new TwigFilter(
'storaged_requisites_preview',
fn (string|null $requisites = null) => preg_replace('/^(\d{4}\s\d{4}\s\d{4}\s)(\d{4})\s([A-zА-я]{4})([A-zА-я]+)\s(.+)$/u', '...$2 $3. $5', $requisites ?? '')
)
);
// Инициализация фильтров
$this->twig->addFilter(
new TwigFilter(
'date_to_russian',
fn (string|null $date = null) => preg_replace('/^(\d{4})-(\d\d)-(\d\d)$/', '$3.$2.$1', $date ?? '')
)
);
// Инициализация фильтров
$this->twig->addFilter(
new TwigFilter(
'account_type_to_russian',
fn (?string $type = null) => match ($type) {
'worker' => 'Сотрудник',
'market' => 'Магазин',
'operator' => 'Оператор',
'administrator' => 'Администратор',
default => 'Аккаунт'
}
)
);
// Инициализация расширений
$this->twig->addExtension(new intl());
}