13 Commits

49 changed files with 10474 additions and 6300 deletions

View File

@@ -1,10 +1,7 @@
# Ebala
### Site for providing outsourced employees to retail stores
Site-registry of tasks for outsourced employees
My customer uses this development to process thousands of applications throughout Krasnoyarsk and neighboring cities.<br/>
**9 000 000** Russian rubles pass through the site every **week** 🤟.
For this project I received **600 000** Russian rubles. At the current exchange rate this is **$6000**.<br/>
If I had not been lazy and submitted the order a few months ago, it would have been **$8000** 😥 (super sad).
I did not sign any documents prohibiting the dissemination of information. You can use the code under the **WTFPL** license 🤝 (read ./LICENSE)
From this project i earned **700 000** Russian rubles</br>
*As long as commits appear in the repository, this means that i continue paid development*</br>
</br>
I am selling this site to **capitalist scum** for a lot of money, but you, friend, can use my code **for free** ✌️

View File

@@ -163,10 +163,14 @@ final class account extends core
'errors' => self::parse_only_text($this->errors)
];
if ($password) $return['clipboard'] = <<<TEXT
Идентификатор: {$account->getKey()}
Пароль: {$parameters['password']}
TEXT;
if ($password) $return['clipboard'] = match ($account->type) {
'worker' => 'Номер: ' . model::worker($account->getId())?->number,
'market' => 'Идентификатор: ' . model::market($account->getId())?->id,
'operator' => "Идентификатор: {$account->getKey()}",
'administrator' => "Идентификатор: {$account->getKey()}",
default => "Идентификатор: {$account->getKey()}"
}
. "\nПароль: {$parameters['password']}";
// Генерация ответа
echo json_encode($return);
@@ -264,4 +268,106 @@ final class account extends core
// Возврат (провал)
return null;
}
/**
* Пометить заблокированным
*
* @param array $parameters Параметры запроса
*/
public function ban(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->banned = true;
if (_core::update($account)) {
// Записаны данные аккаунта
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode([
'banned' => true,
'errors' => self::parse_only_text($this->errors)
]);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не удалось записать изменения в базу данных');
} else throw new exception('Не удалось найти аккаунт');
}
// Возврат (провал)
return null;
}
/**
* Снять пометку заблокированного (разблокировать)
*
* @param array $parameters Параметры запроса
*/
public function unban(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 = true;
$account->banned = false;
if (_core::update($account)) {
// Записаны данные аккаунта
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode([
'unbanned' => true,
'errors' => self::parse_only_text($this->errors)
]);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не удалось записать изменения в базу данных');
} else throw new exception('Не удалось найти аккаунт');
}
// Возврат (провал)
return null;
}
}

View File

@@ -38,7 +38,7 @@ final class administrator extends core
// Перебор фильтров статусов
// Инициализация значения (приоритет у cookie)
$value = $_COOKIE["administrators_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['administrators']['filters'][$name] ?? 0;
$value = $_COOKIE["administrators_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['administrators']['filters'][$name] ?? 0;
// Инициализировано значение?
if ($value === null || $value === 0) continue;
@@ -86,7 +86,7 @@ final class administrator extends core
// Перебор фильтров статусов (И)
// Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue)
if (empty($value = $_COOKIE["administrators_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['administrators']['filters'][$name] ?? 0)) continue;
if (empty($value = $_COOKIE["administrators_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['administrators']['filters'][$name] ?? 0)) continue;
// Конвертация ярлыков
$converted = match ($name) {
@@ -116,7 +116,7 @@ final class administrator extends core
if (!empty($filters_statuses_merged)) $filters .= empty($filters) ? $filters_statuses_merged : " && ($filters_statuses_merged)";
// Инициализация строки поиска
$search = $_COOKIE["administrators_filter_search"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['administrators']['filters']['search'] ?? '';
$search = $_COOKIE["administrators_filter_search"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['administrators']['filters']['search'] ?? '';
if (mb_strlen($search) < 3) $search = null;
$search_query = empty($search)
? null

View File

@@ -14,6 +14,9 @@ use mirzaev\ebala\views\templater,
// Фреймворк PHP
use mirzaev\minimal\controller;
// Встроенные библиотеки
use exception;
/**
* Ядро контроллеров
*
@@ -150,7 +153,7 @@ class core extends controller
$buffer = [];
// Инициализация данных магазина для аккаунта для генерации представления
foreach ($this->view->accounts as $vendor) $buffer[] = ['account' => $vendor, 'market' => account::market($vendor->getId(), errors: $this->errors['account'])];
foreach ($this->view->accounts as $account) $buffer[] = ['account' => $account, 'market' => account::market($account->getId(), errors: $this->errors['account'])];
// Запись в глобальную переменную из буфера
$this->view->accounts = $buffer;

View File

@@ -30,7 +30,7 @@ final class index extends core
// Перебор фильтров временного промежутка
// Инициализация значения (приоритет у cookie)
if (empty($value = (int) ($_COOKIE["tasks_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? (($name === 'from') ? time() : strtotime('+1 month'))))) continue;
if (empty($value = (int) ($_COOKIE["tasks_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? (($name === 'from') ? time() : strtotime('+1 month'))))) continue;
// Генерация значения для аттрибута "value" для HTML-элемента <input>
$this->view->{$name} = (int) $value;
@@ -40,7 +40,7 @@ final class index extends core
// Перебор фильтров статусов
// Инициализация значения (приоритет у cookie)
$value = $_COOKIE["tasks_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? null;
$value = $_COOKIE["tasks_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? null;
// Найдено значение?
if ($value === null) continue;

View File

@@ -44,7 +44,7 @@ final class market extends core
// Перебор фильтров статусов
// Инициализация значения (приоритет у cookie)
$value = $_COOKIE["markets_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['markets']['filters'][$name] ?? 0;
$value = $_COOKIE["markets_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['markets']['filters'][$name] ?? 0;
// Инициализировано значение?
if ($value === null || $value === 0) continue;
@@ -94,7 +94,7 @@ final class market extends core
// Перебор фильтров статусов
// Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue)
if (empty($value = $_COOKIE["markets_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['markets']['filters'][$name] ?? 0)) continue;
if (empty($value = $_COOKIE["markets_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['markets']['filters'][$name] ?? 0)) continue;
// Конвертация ярлыков
$converted = match ($name) {
@@ -135,7 +135,7 @@ final class market extends core
if (!empty($filters_statuses_after_merged)) $filters_after .= empty($filters_after) ? $filters_statuses_after_merged : " && ($filters_statuses_after_merged)";
// Инициализация строки поиска
$search = $_COOKIE["markets_filter_search"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['markets']['filters']['search'] ?? '';
$search = $_COOKIE["markets_filter_search"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['markets']['filters']['search'] ?? '';
if (mb_strlen($search) < 3) $search = null;
$search_query = empty($search)
? null
@@ -144,6 +144,7 @@ final class market extends core
a.commentary IN TOKENS(@search, 'text_ru')
|| a.address IN TOKENS(@search, 'text_ru')
|| STARTS_WITH(a._key, @search)
|| STARTS_WITH(a.id, @search)
|| STARTS_WITH(a.name.first, @search)
|| STARTS_WITH(a.name.second, @search)
|| STARTS_WITH(a.name.last, @search)
@@ -152,6 +153,7 @@ final class market extends core
|| STARTS_WITH(a.number, @search)
|| STARTS_WITH(a.mail, @search)
|| (LENGTH(@search) > 5 && LEVENSHTEIN_MATCH(a._key, TOKENS(@search, 'text_en')[0], 2, true))
|| (LENGTH(@search) > 2 && LEVENSHTEIN_MATCH(a.id, TOKENS(@search, 'text_en')[0], 1, true))
|| (LENGTH(@search) > 3 && LEVENSHTEIN_MATCH(a.name.first, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 3 && LEVENSHTEIN_MATCH(a.name.second, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 3 && LEVENSHTEIN_MATCH(a.name.last, TOKENS(@search, 'text_ru')[0], 2, true))
@@ -236,6 +238,7 @@ final class market extends core
else if (!empty($parameters['account_number']) && strlen($parameters['account_number']) < 11) throw new exception('Несоответствие формату SIM-номера аккаунта представителя');
else if (!empty($parameters['market_mail']) && preg_match('/^.+@.+\.\w+$/', $parameters['market_mail']) === 0) throw new exception('Несоответствие формату почты представителя');
else if (!empty($parameters['account_mail']) && preg_match('/^.+@.+\.\w+$/', $parameters['account_mail']) === 0) throw new exception('Несоответствие формату почты аккаунта представителя');
else if (!empty($parameters['market_id']) && model::read('d.id == "' . $parameters['market_id'] . '"', errors: $this->errors['account']) instanceof _document) throw new exception('Уже существует магазин с данным идентификатором');
// Универсализация
/* $parameters['market_number'] = (int) $parameters['market_number']; */
@@ -274,8 +277,8 @@ final class market extends core
];
}
// Инициализация идентификатора аккаунта (ключ документа инстанции аккаунта в базе данных)
$_key = preg_replace('/.+\//', '', $account ?? '');
// Инициализация идентификатора магазина
$id = empty($parameters['market_id']) ? model::id() : $parameters['market_id'];
// Запись заголовков ответа
header('Content-Type: application/json');
@@ -289,7 +292,7 @@ final class market extends core
echo json_encode(
[
'clipboard' => empty($this->errors['account']) ? <<<TEXT
Идентификатор: $_key
Идентификатор: $id
Пароль: {$parameters['account_password']}
TEXT : '',
'errors' => self::parse_only_text($this->errors['account'])
@@ -304,29 +307,35 @@ final class market extends core
flush();
try {
// Создание магазина
$market = model::create(
data: [
'name' => [
'first' => $parameters['market_name_first'],
'second' => $parameters['market_name_second'],
'last' => $parameters['market_name_last']
if (isset($account)) {
// Инициализирован аккаунт
// Создание магазина
$market = model::create(
data: [
'id' => (string) $id,
'name' => [
'first' => $parameters['market_name_first'],
'second' => $parameters['market_name_second'],
'last' => $parameters['market_name_last']
],
'number' => $parameters['market_number'] === 0 ? '' : $parameters['market_number'],
'mail' => $parameters['market_mail'],
'type' => $parameters['market_type'],
'city' => $parameters['market_city'],
'district' => $parameters['market_district'],
'address' => $parameters['market_address'],
],
'number' => $parameters['market_number'] === 0 ? '' : $parameters['market_number'],
'mail' => $parameters['market_mail'],
'type' => $parameters['market_type'],
'city' => $parameters['market_city'],
'district' => $parameters['market_district'],
'address' => $parameters['market_address'],
],
errors: $this->errors['account']
);
errors: $this->errors['account']
);
// Проверка существования созданного магазина
if (empty($market)) throw new exception('Не удалось создать магазин');
// Проверка существования созданного магазина
if (empty($market)) throw new exception('Не удалось создать магазин');
// Создание ребра: account -> market
account::connect($account, $market, 'market', $this->errors['account']);
// Создание ребра: account -> market
account::connect($account, $market, 'market', $this->errors['account']);
}
throw new exception('Не инициализирован аккаунт');
} catch (exception $e) {
// Write to the errors registry
$this->errors['account'][] = [
@@ -350,7 +359,7 @@ final class market extends core
// Авторизован аккаунт администратора или оператора
// Инициализация данных магазина
$market = model::read('d._key == "' . $parameters['id'] . '"', return: '{ name: d.name, number: d.number, mail: d.mail, type: d.type, city: d.city, district: d.district, address: d.address}')->getAll();
$market = model::read('d.id == "' . urldecode($parameters['id']) . '"', return: '{ name: d.name, number: d.number, mail: d.mail, type: d.type, city: d.city, district: d.district, address: d.address}')->getAll();
if (!empty($market)) {
// Найдены данные магазина
@@ -390,12 +399,12 @@ final class market extends core
// Авторизован аккаунт администратора или оператора
// Инициализация данных магазина
$market = model::read('d._key == "' . $parameters['id'] . '"');
$market = model::read('d.id == "' . urldecode($parameters['id']) . '"');
if (!empty($market)) {
// Найден магазин
// Инициализация параметров (перезапись переданными значениями)
// Инициализация параметров (перезапись переданными значениями)
if ($parameters['name_first'] !== $market->name['first']) $market->name = ['first' => $parameters['name_first']] + $market->name;
if ($parameters['name_second'] !== $market->name['second']) $market->name = ['second' => $parameters['name_second']] + $market->name;
if ($parameters['name_last'] !== $market->name['last']) $market->name = ['last' => $parameters['name_last']] + $market->name;
@@ -472,7 +481,7 @@ final class market extends core
// Авторизован аккаунт оператора или магазина
// Инициализация данных магазинов
$this->view->markets = model::read(filter: 'd.active == true', amount: 10000, return: '{ _key: d._key, name: d.name }');
$this->view->markets = model::read(filter: 'd.active == true', amount: 10000, return: '{ id: d.id, name: d.name }');
// Универсализация
if ($this->view->markets instanceof _document) $this->view->markets = [$this->view->markets];

View File

@@ -38,7 +38,7 @@ final class operator extends core
// Перебор фильтров статусов
// Инициализация значения (приоритет у cookie)
$value = $_COOKIE["operators_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['operators']['filters'][$name] ?? 0;
$value = $_COOKIE["operators_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['operators']['filters'][$name] ?? 0;
// Инициализировано значение?
if ($value === null || $value === 0) continue;
@@ -86,7 +86,7 @@ final class operator extends core
// Перебор фильтров статусов (И)
// Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue)
if (empty($value = $_COOKIE["operators_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['operators']['filters'][$name] ?? 0)) continue;
if (empty($value = $_COOKIE["operators_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['operators']['filters'][$name] ?? 0)) continue;
// Конвертация ярлыков
$converted = match ($name) {
@@ -116,7 +116,7 @@ final class operator extends core
if (!empty($filters_statuses_merged)) $filters .= empty($filters) ? $filters_statuses_merged : " && ($filters_statuses_merged)";
// Инициализация строки поиска
$search = $_COOKIE["operators_filter_search"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['operators']['filters']['search'] ?? '';
$search = $_COOKIE["operators_filter_search"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['operators']['filters']['search'] ?? '';
if (mb_strlen($search) < 3) $search = null;
$search_query = empty($search)
? null

View File

@@ -0,0 +1,195 @@
<?php
declare(strict_types=1);
namespace mirzaev\ebala\controllers;
// Файлы проекта
use mirzaev\ebala\controllers\core,
mirzaev\ebala\controllers\traits\errors,
mirzaev\ebala\models\payments as model;
// System libraries
use exception;
/**
* Контроллер выплат
*
* @package mirzaev\ebala\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class payments extends core
{
use errors;
/**
* Сотрудники
*
* Расчитать стоимость работы сотрудников за выбранный период и сгенерировать excel-документ
*
* @param array $parameters Параметры запроса
*
* @return void В буфер вывода excel-документ
*/
public function workers(array $parameters = []): void
{
try {
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
// Инициализация буфера ошибок
$this->errors['export'] ??= [];
if (!empty($from = (int) ($_COOKIE["tasks_filter_from"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters']['from']))) {
// Инициализирован параметр: from
if (!empty($to = (int) ($_COOKIE["tasks_filter_to"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters']['to']))) {
// Инициализирован параметр: to
// Сброс буфера вывода
if (ob_get_level()) {
ob_end_clean();
}
// Инициализация буфера вывода
ob_start();
if (model::workers($from, $to, $this->errors['export'])) {
// Сгенерирован excel-документ с выплатами (и отправлен в буфер вывода)
// Запись заголовков ответа
header('Content-Description: Spreadsheet transfer');
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment;filename=workers ' . gmdate("d.m.Y", $from) . ' - ' . gmdate("d.m.Y", $to) . '.xlsx');
header('Access-Control-Expose-Headers: Content-Disposition');
header('Cache-Control: max-age=0');
} else throw new exception('Не удалось сгенерировать excel-документ');
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не инициализирован параметр: to');
} else throw new exception('Не инициализирован параметр: from');
} else throw new exception('Вы не авторизованы');
} catch (exception $e) {
// Запись в реестр ошибок
$this->errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode(
[
'errors' => self::parse_only_text($this->errors)
]
);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
}
/**
* Магазины
*
* Расчитать ... (сверку?) за выбранный период и сгенерировать excel-документ
*
* @param array $parameters Параметры запроса
*
* @return void В буфер вывода excel-документ
*/
public function markets(array $parameters = []): void
{
try {
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
// Инициализация буфера ошибок
$this->errors['export'] ??= [];
if (!empty($from = (int) ($_COOKIE["tasks_filter_from"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters']['from']))) {
// Инициализирован параметр: from
if (!empty($to = (int) ($_COOKIE["tasks_filter_to"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters']['to']))) {
// Инициализирован параметр: to
// Сброс буфера вывода
if (ob_get_level()) {
ob_end_clean();
}
// Инициализация буфера вывода
ob_start();
if (model::markets($from, $to, $this->errors['export'])) {
// Сгенерирован excel-документ с выплатами (и отправлен в буфер вывода)
// Запись заголовков ответа
header('Content-Description: Spreadsheet transfer');
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment;filename=markets ' . gmdate("d.m.Y", $from) . ' - ' . gmdate("d.m.Y", $to) . '.xlsx');
header('Access-Control-Expose-Headers: Content-Disposition');
header('Cache-Control: max-age=0');
} else throw new exception('Не удалось сгенерировать excel-документ');
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не инициализирован параметр: to');
} else throw new exception('Не инициализирован параметр: from');
} else throw new exception('Вы не авторизованы');
} catch (exception $e) {
// Запись в реестр ошибок
$this->errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode(
[
'errors' => self::parse_only_text($this->errors)
]
);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
}
}
}

View File

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

View File

@@ -11,6 +11,7 @@ use mirzaev\ebala\controllers\core,
mirzaev\ebala\models\account,
mirzaev\ebala\models\worker,
mirzaev\ebala\models\market,
mirzaev\ebala\models\payments,
mirzaev\ebala\models\core as _core;
// Библиотека для ArangoDB
@@ -31,10 +32,15 @@ final class task extends core
{
use errors;
/**
* Типы работ
*/
final public const WORKS = ['Кассир', 'Выкладчик', 'Гастроном', 'Бригадир', 'Грузчик', 'Мобильный грузчик', 'Мобильный универсал'];
/**
* Создать
*
* @param array $parameters Параметры запроса
* @param array $parameters Параметры запроса (json в php://input)
*
* @return void В буфер вывода JSON-документ с запрашиваемыми параметрами
*/
@@ -47,33 +53,50 @@ final class task extends core
// Инициализация буфера ошибок
$this->errors['tasks'] ??= [];
// Создание строк
for ($i = 0, $parameters['cashiers'] = (int) $parameters['cashiers']; $i < $parameters['cashiers']; ++$i) model::create(work: 'Кассир', market: $this->account->type === 'market' ? account::market($this->account->getId())?->getKey() : null, start: $parameters['start'], end: $parameters['end'], date: $parameters['date'], errors: $this->errors['tasks']);
for ($i = 0, $parameters['displayers'] = (int) $parameters['displayers']; $i < $parameters['displayers']; ++$i) model::create(work: 'Выкладчик', market: $this->account->type === 'market' ? account::market($this->account->getId())?->getKey() : null, start: $parameters['start'], end: $parameters['end'], date: $parameters['date'], errors: $this->errors['tasks']);
for ($i = 0, $parameters['loaders'] = (int) $parameters['loaders']; $i < $parameters['loaders']; ++$i) model::create(work: 'Грузчик', market: $this->account->type === 'market' ? account::market($this->account->getId())?->getKey() : null, start: $parameters['start'], end: $parameters['end'], date: $parameters['date'], errors: $this->errors['tasks']);
for ($i = 0, $parameters['gastronomes'] = (int) $parameters['gastronomes']; $i < $parameters['gastronomes']; ++$i) model::create(work: 'Гастроном', market: $this->account->type === 'market' ? account::market($this->account->getId())?->getKey() : null, start: $parameters['start'], end: $parameters['end'], date: $parameters['date'], errors: $this->errors['tasks']);
if (!empty($json = json_decode(file_get_contents('php://input'), true, 4))) {
// Найден и декодирован json-документ с данными для создания заявок
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
foreach ($json as $work => $tasks) {
// Перебор категорий (колонок)
// Инициализация буфера вывода
ob_start();
foreach ($tasks as $task) {
// Перебор заявок
// Генерация ответа
echo json_encode(
[
'errors' => self::parse_only_text($this->errors)
]
);
// Создание заявки
model::create(
work: model::label($work),
market: $this->account->type === 'market' ? account::market($this->account->getId())?->id : null,
start: $task['start'],
end: $task['end'],
date: $task['date'],
commentary: $task['commentary'],
errors: $this->errors['tasks']
);
}
}
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode(
[
'errors' => self::parse_only_text($this->errors)
]
);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не удалось инициализировать JSON-документ с данными заявок');
} else throw new exception('Вы не авторизованы');
} catch (exception $e) {
// Запись в реестр ошибок
@@ -128,7 +151,7 @@ final class task extends core
// Перебор фильтров временного промежутка (И)
// Инициализация значения (приоритет у cookie)
if (empty($value = (int) ($_COOKIE["tasks_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? (($name === 'from') ? time() : strtotime('+1 month'))))) continue;
if (empty($value = (int) ($_COOKIE["tasks_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? (($name === 'from') ? time() : strtotime('+1 month'))))) continue;
// Генерация AQL-выражения для инъекции в строку запроса
if ($name === 'from') $interval .= " && task.date >= $value";
@@ -145,7 +168,7 @@ final class task extends core
// Перебор фильтров с произвольными значениями (И)
// Инициализация значения (приоритет у cookie)
$value = $_COOKIE["tasks_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? null;
$value = $_COOKIE["tasks_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? null;
// Найдено значение?
if ($value === null) continue;
@@ -165,7 +188,7 @@ final class task extends core
// Перебор фильтров по статусам
// Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue)
if (empty($value = $_COOKIE["tasks_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? 0)) continue;
if (empty($value = $_COOKIE["tasks_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters'][$name] ?? 0)) continue;
// Конвертация ярлыков
$converted = match ($name) {
@@ -205,11 +228,11 @@ final class task extends core
// Инициализация данных для генерации HTML-документа с таблицей
if ($_SERVER['INTERFACE'] === 'worker')
$this->view->rows = model::list(before: 'FILTER task.worker == "' . account::worker($this->account->getId())?->getKey() . '"' . " && ($filters)");
$this->view->rows = model::list(before: 'FILTER task.worker == "' . account::worker($this->account->getId())?->id . '"' . " && ($filters)");
else if ($_SERVER['INTERFACE'] === 'operator')
$this->view->rows = model::list(before: "FILTER ($filters)");
else if ($_SERVER['INTERFACE'] === 'market')
$this->view->rows = model::list(before: 'FILTER task.market == "' . account::market($this->account->getId())?->getKey() . '"' . " && ($filters)");
$this->view->rows = model::list(before: 'FILTER task.market == "' . account::market($this->account->getId())?->id . '"' . " && ($filters)");
else if ($_SERVER['INTERFACE'] === 'administrator')
$this->view->rows = model::list(before: "FILTER ($filters)");
else $this->view->rows = [];
@@ -221,7 +244,7 @@ final class task extends core
else if (empty($this->session->buffer[$_SERVER['INTERFACE']]['tasks']['page'])) $this->session->write(['tasks' => ['page' => 1]]);
// Инициализация строки поиска
$search = $_COOKIE["tasks_filter_search"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters']['search'] ?? '';
$search = $_COOKIE["tasks_filter_search"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['tasks']['filters']['search'] ?? '';
if (mb_strlen($search) < 3) $search = null;
$search_query = empty($search)
? null
@@ -234,10 +257,12 @@ final class task extends core
|| (LENGTH(@search) > 6 && LEVENSHTEIN_MATCH(task._key, TOKENS(@search, 'text_en')[0], 2, true))
|| (LENGTH(@search) > 6 && LEVENSHTEIN_MATCH(task.worker, TOKENS(@search, 'text_en')[0], 2, true))
|| (LENGTH(@search) > 6 && LEVENSHTEIN_MATCH(task.market, TOKENS(@search, 'text_en')[0], 2, true))
AQL;
AQL;
// Инициализация данных для генерации HTML-документа с таблицей
if ($_SERVER['INTERFACE'] === 'worker')
if ($_SERVER['INTERFACE'] === 'worker') {
// Сотрудник
$this->view->rows = model::list(
before: sprintf(
<<<AQL
@@ -249,9 +274,11 @@ AQL;
),
page: (int) $this->session->buffer['worker']['tasks']['page'],
target: empty($search) ? model::COLLECTION : 'registry_tasks',
binds: ['worker' => account::worker($this->account->getId())?->getKey()] + (empty($search) ? [] : ['search' => $search])
binds: ['worker' => account::worker($this->account->getId())?->id] + (empty($search) ? [] : ['search' => $search])
);
else if ($_SERVER['INTERFACE'] === 'market')
} else if ($_SERVER['INTERFACE'] === 'market') {
// Магазин
$this->view->rows = model::list(
before: sprintf(
<<<AQL
@@ -263,9 +290,11 @@ AQL;
),
page: (int) $this->session->buffer['market']['tasks']['page'],
target: empty($search) ? model::COLLECTION : 'registry_tasks',
binds: ['market' => account::market($this->account->getId())?->getKey()] + (empty($search) ? [] : ['search' => $search])
binds: ['market' => account::market($this->account->getId())?->id] + (empty($search) ? [] : ['search' => $search])
);
else if ($_SERVER['INTERFACE'] === 'operator')
} else if ($_SERVER['INTERFACE'] === 'operator') {
// Оператор
$this->view->rows = model::list(
before: sprintf(
<<<AQL
@@ -281,7 +310,9 @@ AQL;
'search' => $search
]
);
else if ($_SERVER['INTERFACE'] === 'administrator')
} else if ($_SERVER['INTERFACE'] === 'administrator') {
// Администратор
$this->view->rows = model::list(
before: sprintf(
<<<AQL
@@ -297,9 +328,9 @@ AQL;
'search' => $search
]
);
else $this->view->rows = [];
} else $this->view->rows = [];
// Запись в cookie (только таким методом можно записать "hostonly: true")
// Запись в cookie
setcookie(
'tasks_page',
(string) $this->session->buffer[$_SERVER['INTERFACE']]['tasks']['page'],
@@ -382,6 +413,8 @@ AQL;
* @param array $rows Строки
*
* @return array Обработанные строки
*
* @todo Переделать в model::hours($from, $to);
*/
protected static function preprocessing(account $account, array $rows): array
{
@@ -435,7 +468,8 @@ AQL;
$link->task['start'] instanceof datetime
&& $link->task['end'] instanceof datetime
) {
$generated['hours'] = (float) $link->task['end']->diff($link->task['start'])->format('%H.%i');
$generated['hours'] = (float) $link->task['start']->diff($link->task['end'])->format('%R%H.%i');
if ($generated['hours'] < 0) $generated['hours'] += 24;
if ($generated['hours'] >= 6.5 && $generated['hours'] < 9) $generated['hours'] -= 0.5;
else if ($generated['hours'] >= 9 && $generated['hours'] < 12.5) $generated['hours'] -= 1;
else if ($generated['hours'] >= 12.5) $generated['hours'] -= 1.5;
@@ -449,7 +483,7 @@ AQL;
];
if ($account->type === 'worker') {
// Оператор или администратор
// Сотрудник
foreach ($link->task['chats']['worker'] ?? [] as $message) {
// Перебор сообщений из чата: СОТРУДНИК <-> ОПЕРАТОР
@@ -458,7 +492,7 @@ AQL;
if (!array_key_exists((string) $account->getKey(), $message['readed'] ?? [])) ++$generated['chat']['unreaded'];
}
} else if ($account->type === 'market') {
// Оператор или администратор
// Магазин
foreach ($link->task['chats']['market'] ?? [] as $message) {
// Перебор сообщений из чата: МАГАЗИН <-> ОПЕРАТОР
@@ -503,14 +537,52 @@ AQL;
// Найдена заявка
// Заявка не принадлежит запросившему магазину?
if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->getKey())
if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->id)
throw new exception('Вы не авторизованы для редактирования этой заявки');
// Заявка подтверждена?
if ($task->confirmed) throw new exception('Запрещено редактировать подтверждённую заявку');
// Заявка завершена?
if ($this->account->type === 'market' && $task->completed) throw new exception('Запрещено редактировать завершённую заявку');
if ($this->account->type === 'market') {
// Магазин
// Инициализация даты
$date = (new DateTime('@' . $task->date))->setTimezone(new DateTimeZone('Asia/Krasnoyarsk'));
// Инициализация времени
$start = datetime::createFromFormat('H:i', (string) $task->start);
$end = datetime::createFromFormat('H:i', (string) $task->end);
// Перенос времени в дату
$start = $date->setTime((int) $start->format('H'), (int) $start->format('i'))->format('U');
$end = $date->setTime((int) $end->format('H'), (int) $end->format('i'))->format('U');
// Заявка уже начата?
if (time() - $start > 0)
throw new exception('Запрещено редактировать начатую заявку');
// Заявка уже прошла?
if (time() - $end > 0)
throw new exception('Запрещено редактировать прошедшую заявку');
// Заявка уже завершена?
if ($task->completed === true)
throw new exception('Запрещено редактировать завершённую заявку');
// Прошло более 30 минут после создания заявки? (1800 секунд = 30 минут)
/* if (time() - $task->created > 1800)
throw new exception('Запрещено редактировать заявку спустя 30 минут после создания'); */
// До начала заявки осталось менее 16 часов? (57600 секунд = 16 часов)
if ($start - time() < 57600)
throw new exception('Запрещено редактировать заявку за менее 16 часов до её начала');
}
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (!empty($parameters['worker'])) {
// Передан сотрудник
@@ -557,14 +629,14 @@ AQL;
} else {
// Записать нового сотрудника
if (($worker = worker::read('d._key == "' . $parameters['worker'] . '" && d.active == true', amount: 1)) instanceof _document) {
if (($worker = worker::read('d.id == "' . $parameters['worker'] . '" && d.active == true', amount: 1)) instanceof _document) {
// Найден сотрудник (запрашиваемый для записи сотрудник существует в базе данных)
if ($task->worker !== $parameters['worker']) {
// Идентификатор запрашиваемого сотрудника не равен актуальному
// Запись сотрудника
$task->worker = $worker->getKey();
$task->worker = $worker->id;
// Снятие с публикации
$task->published = false;
@@ -653,14 +725,14 @@ AQL;
} else {
// Записать новый магазин
if (($market = market::read('d._key == "' . $parameters['market'] . '" && d.active == true', amount: 1)) instanceof _document) {
if (($market = market::read('d.id == "' . $parameters['market'] . '" && d.active == true', amount: 1)) instanceof _document) {
// Найден магазин (запрашиваемый для записи магазин существует в базе данных)
if ($task->market !== $parameters['market']) {
// Идентификатор запрашиваемого сотрудника не равен актуальному
// Запись магазина
$task->market = $market->getKey();
$task->market = $market->id;
if (_core::update($task)) {
// Записано изменение в базу данных
@@ -753,11 +825,11 @@ AQL;
'd._key == "' . $parameters['task'] . '"',
return: $this->account->type === 'market'
? '{_key: d._key, created: d.created, updated: d.updated, date: d.date, start: d.start, end: d.end, market: d.market, confirmed: d.confirmed, completed: d.completed }'
: '{_key: d._key, created: d.created, updated: d.updated, date: d.date, start: d.start, end: d.end, market: d.market, confirmed: d.confirmed, completed: d.completed, hided: d.hided }'
: '{_key: d._key, created: d.created, updated: d.updated, date: d.date, start: d.start, end: d.end, market: d.market, confirmed: d.confirmed, completed: d.completed, hided: d.hided, updates: d.updates }'
)->getAll();
// Заявка не принадлежит запросившему магазину?
if ($this->account->type === 'market' and $this->view->task['market'] !== account::market($this->account->getId())?->getKey())
if ($this->account->type === 'market' and $this->view->task['market'] !== account::market($this->account->getId())?->id)
throw new exception('Вы не авторизованы для чтения этой заявки');
// Удаление данных из выдачи
@@ -788,7 +860,18 @@ AQL;
// Перевод ключей на русский язык
foreach ($this->view->task as $key => $value)
if (match ($key) {
if ($key === 'updates')
foreach ($value as $key => $value) $buffer['updates'][$key] = [
'label' => match ($key) {
'operator' => 'Оператор',
'market' => 'Магазин',
'administrator' => 'Администратор',
'worker' => 'Сотрудник',
default => $key
},
'value' => $value
];
else if (match ($key) {
'created', 'updated', 'confirmed', 'hided', 'completed', '_key' => true,
'start', 'end' => $passed, // Только для завершённой заявки
default => false
@@ -876,7 +959,6 @@ AQL;
}
}
/**
* Прочитать данные сотрудника
*
@@ -892,9 +974,9 @@ AQL;
// Инициализация данных сотрудника
$this->view->worker = worker::read(
'd._key == "' . $parameters['worker'] . '"',
'd.id == "' . $parameters['worker'] . '"',
return: $this->account->type === 'market'
? '{_key: d._key, created: d.created, updated: d.updated, name: d.name, number: d.number, mail: d.mail, birth: d.birth, rating: d.rating}'
? '{id: d.id, created: d.created, updated: d.updated, name: d.name, number: d.number, mail: d.mail, birth: d.birth, rating: d.rating}'
: 'd'
)->getAll();
@@ -925,6 +1007,7 @@ AQL;
'tax' => 'ИНН',
'city' => 'Город',
'payment' => 'Форма оплаты',
'works' => 'Формы работ',
default => $key
},
'value' => $value
@@ -1011,7 +1094,7 @@ AQL;
// Авторизован аккаунт администратора или оператора
// Инициализация данных
$this->view->market = market::read('d._key == "' . $parameters['market'] . '"')?->getAll();
$this->view->market = market::read('d.id == "' . $parameters['market'] . '"')?->getAll();
if (!empty($this->view->market)) {
// Найдены данные магазина
@@ -1125,6 +1208,12 @@ AQL;
// Изменение статуса подтверждения
$task->confirmed = !$task->confirmed;
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано изменение в базу данных
@@ -1210,7 +1299,7 @@ AQL;
$task = model::read('d._key == "' . $parameters['task'] . '"');
// Заявка не принадлежит запросившему магазину?
if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->getKey())
if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->id)
throw new exception('Вы не авторизованы для заявления о проблеме с этой заявкой');
// Инициализация даты
@@ -1244,6 +1333,12 @@ AQL;
}
}
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано изменение в базу данных
@@ -1329,7 +1424,7 @@ AQL;
$task = model::read('d._key == "' . $parameters['task'] . '"');
// Заявка не принадлежит запросившему магазину?
if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->getKey())
if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->id)
throw new exception('Вы не авторизованы для завершения этой заявки');
// Заявка завершена?
@@ -1359,7 +1454,7 @@ AQL;
else {
// Получена оценка
// Запись оценики
// Запись оценки
$task->rating = $parameters['rating'];
if (!empty($parameters['review'])) {
@@ -1374,8 +1469,43 @@ AQL;
// Снятие с публикации
$task->published = false;
// Иниализация сотрудника
$worker = worker::read('d.id == "' . $task->worker . '"');
// Инициализация магазина
$market = market::read('d.id == "' . $task->market . '"');
// Подсчёт часов работы
$hours = model::hours($task->start, $task->end, $this->errors);
// Инициализация цены работы за 1 час
$hour = payments::hour($market->city, $task->work);
// Подсчёт оплаты за работу
$payment = $hour * $hours;
// Инициализация штрафа
$penalty = payments::penalty($task->rating ?? null);
// Инициализация премии
$bonus = payments::bonus($task->rating ?? null);
// Инициализация транзакции к оплате сотруднику
model::transaction(
$task->getId(),
$worker->getId(),
$payment - ($penalty === null ? $payment : -$penalty) + $bonus,
$this->errors
);
}
// Запись в реcстре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано изменение в базу данных
@@ -1467,6 +1597,12 @@ AQL;
// Изменение статуса скрытия
$task->hided = !$task->hided;
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано изменение в базу данных
@@ -1552,34 +1688,46 @@ AQL;
$task = model::read('d._key == "' . $parameters['task'] . '"');
// Заявка не принадлежит запросившему магазину?
if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->getKey())
if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->id)
throw new exception('Вы не авторизованы для редактирования типа работы этой заявки');
// Заявка подтверждена?
if ($task->confirmed) throw new exception('Запрещено удалять подтверждённую заявку');
// Инициализация даты
$date = (new DateTime('@' . $task->date))->setTimezone(new DateTimeZone('Asia/Krasnoyarsk'));
if ($this->account->type === 'market') {
// Магазин
// Инициализация времени
$start = datetime::createFromFormat('H:i', (string) $task->start);
$end = datetime::createFromFormat('H:i', (string) $task->end);
// Инициализация даты
$date = (new DateTime('@' . $task->date))->setTimezone(new DateTimeZone('Asia/Krasnoyarsk'));
// Перенос времени в дату
$start = $date->setTime((int) $start->format('H'), (int) $start->format('i'))->format('U');
$end = $date->setTime((int) $end->format('H'), (int) $end->format('i'))->format('U');
// Инициализация времени
$start = datetime::createFromFormat('H:i', (string) $task->start);
$end = datetime::createFromFormat('H:i', (string) $task->end);
// Заявка уже начата
if ($this->account->type === 'market' and time() - $start > 0)
throw new exception('Запрещено удалять начатую заявку');
// Перенос времени в дату
$start = $date->setTime((int) $start->format('H'), (int) $start->format('i'))->format('U');
$end = $date->setTime((int) $end->format('H'), (int) $end->format('i'))->format('U');
// Заявка уже завершена
if ($this->account->type === 'market' and $task->completed === true || time() - $end > 0)
throw new exception('Запрещено удалять завершённую заявку');
// Заявка уже начата?
if (time() - $start > 0)
throw new exception('Запрещено удалять начатую заявку');
// Прошло более 30 минут после создания заявки? (1800 секунд = 30 минут)
if ($this->account->type === 'market' and time() - $task->created > 1800)
throw new exception('Запрещено удалять заявку спустя 30 минут после создания');
// Заявка уже прошла?
if (time() - $end > 0)
throw new exception('Запрещено удалять прошедшую заявку');
// Заявка уже завершена?
if ($task->completed === true)
throw new exception('Запрещено удалять завершённую заявку');
// Прошло более 30 минут после создания заявки? (1800 секунд = 30 минут)
/* if (time() - $task->created > 1800)
throw new exception('Запрещено удалять заявку спустя 30 минут после создания'); */
// До начала заявки осталось менее 16 часов? (57600 секунд = 16 часов)
if ($start - time() < 57600)
throw new exception('Запрещено удалять заявку за менее 16 часов до её начала');
}
if ($task instanceof _document) {
// Найдена заявка
@@ -1587,6 +1735,12 @@ AQL;
// Изменение статуса
$task->status = 'deleted';
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Помечено как удалённое
@@ -1665,7 +1819,7 @@ AQL;
$task = model::read('d._key == "' . $parameters['task'] . '"');
// Заявка не принадлежит запросившему магазину?
if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->getKey())
if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->id)
throw new exception('Вы не авторизованы для чтения параметров этой заявки');
if ($task instanceof _document) {
@@ -1728,7 +1882,6 @@ AQL;
}
}
/**
* Прочитать данные работ для <datalist>
*
@@ -1739,20 +1892,110 @@ AQL;
public function works(array $parameters = []): void
{
try {
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'market')) {
// Авторизован аккаунт администратора, оператора или магазина
if (!empty($parameters['task'])) {
// Запрошены данные работ по заявке
// Инициализация данных
$this->view->task = model::read('d._key == "' . $parameters['task'] . '"');
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'market' || $this->account->type === 'worker')) {
// Авторизован аккаунт администратора, оператора или магазина
if ($this->view->task instanceof _document) {
// Найдена заявка
// Инициализация данных
$this->view->task = model::read('d._key == "' . $parameters['task'] . '"');
if ($this->view->task instanceof _document) {
// Найдена заявка
// Заявка не принадлежит запросившему магазину?
if ($this->account->type === 'market' and $this->view->task->market !== account::market($this->account->getId())?->id)
throw new exception('Вы не авторизованы для просмотра типа работы этой заявки');
// Заявка не принадлежит запросившему сотруднику?
if ($this->account->type === 'worker' and $this->view->task->worker !== account::worker($this->account->getId())?->id)
throw new exception('Вы не авторизованы для просмотра типа работы этой заявки');
// Инициализация списка работ
$this->view->works = static::WORKS;
// Проверка на существование записанной в задаче работы в списке существующих работ и запись об этом в глобальную переменную шаблонизатора
foreach ($this->view->works as $work) if ($this->view->task->work === $work) $this->view->exist = true;
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode(
[
'works' => $this->view->render(DIRECTORY_SEPARATOR . 'lists' . DIRECTORY_SEPARATOR . 'works.html'),
'errors' => self::parse_only_text($this->errors)
]
);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не найдена заявка');
} else throw new exception('Вы не авторизованы');
} else if (!empty($parameters['worker'])) {
// Запрошены данные работ по сотруднику
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator' || $this->account->type === 'worker')) {
// Авторизован аккаунт администратора, оператора или магазина
// Инициализация данных
$this->view->worker = worker::read('d.id == "' . $parameters['worker'] . '"');
if ($this->view->worker instanceof _document) {
// Найден сотрудник
// Сотрудник не принадлежит запросившему аккаунту?
if ($this->account->type === 'worker' and $this->view->worker->id !== account::worker($this->account->getId())?->id)
throw new exception('Вы не авторизованы для просмотра типа работы этого сотрудника');
// Инициализация списка работ
$this->view->works = static::WORKS;
// Проверка на существование записанной в задаче работы в списке существующих работ и запись об этом в глобальную переменную шаблонизатора
foreach ($this->view->works as $work) if ($this->view->worker->work === $work) $this->view->exist = true;
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode(
[
'works' => $this->view->render(DIRECTORY_SEPARATOR . 'lists' . DIRECTORY_SEPARATOR . 'works.html'),
'errors' => self::parse_only_text($this->errors)
]
);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не найден сотрудник');
} else throw new exception('Вы не авторизованы');
} else {
// Запрошен список работ
if ($this->account->status()) {
// Авторизован аккаунт
// Инициализация списка работ
$this->view->works = ['Кассир', 'Выкладчик', рузчик', астроном'];
// Проверка на существование записанной в задаче работы в списке существующих работ и запись об этом в глобальную переменную шаблонизатора
foreach ($this->view->works as $work) if ($this->view->task->work === $work) $this->view->exist = true;
$this->view->works = ['Кассир', 'Выкладчик', 'Гастроном', 'Бригадир', 'Грузчик', 'Мобильный грузчик', 'Мобильный универсал'];
// Запись заголовков ответа
header('Content-Type: application/json');
@@ -1776,8 +2019,8 @@ AQL;
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не найдена заявка');
} else throw new exception('Вы не авторизованы');
} else throw new exception('Вы не авторизованы');
}
} catch (exception $e) {
// Запись в реестр ошибок
$this->errors[] = [
@@ -1828,7 +2071,7 @@ AQL;
$task = model::read('d._key == "' . $parameters['task'] . '"');
// Заявка не принадлежит запросившему магазину?
if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->getKey())
if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->id)
throw new exception('Вы не авторизованы для редактирования типа работы этой заявки');
// Заявка подтверждена?
@@ -1862,6 +2105,12 @@ AQL;
default => 'Кассир'
};
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано в ArangoDB
@@ -1947,7 +2196,7 @@ AQL;
$task = model::read('d._key == "' . $parameters['task'] . '"');
// Заявка не принадлежит запросившему магазину?
if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->getKey())
if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->id)
throw new exception('Вы не авторизованы для редактирования описания этой заявки');
// Заявка подтверждена?
@@ -1978,6 +2227,12 @@ AQL;
// Изменение статуса
$task->description = $parameters['description'];
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано в ArangoDB
@@ -2056,7 +2311,7 @@ AQL;
$task = model::read('d._key == "' . $parameters['task'] . '"');
// Заявка не принадлежит запросившему магазину?
if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->getKey())
if ($this->account->type === 'market' and $task->market !== account::market($this->account->getId())?->id)
throw new exception('Вы не авторизованы для редактирования даты и времени этой заявки');
// Заявка подтверждена?
@@ -2089,6 +2344,12 @@ AQL;
if (!empty($parameters['start'])) $task->start = $parameters['start'];
if (!empty($parameters['end'])) $task->end = $parameters['end'];
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано в ArangoDB
@@ -2176,6 +2437,12 @@ AQL;
// Запись комментария
$task->commentary = $parameters['commentary'];
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано в ArangoDB
@@ -2263,6 +2530,12 @@ AQL;
// Запись статуса о публикации
$task->published = true;
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано в ArangoDB
@@ -2350,6 +2623,12 @@ AQL;
// Запись статуса о публикации
$task->published = false;
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано в ArangoDB
@@ -2447,7 +2726,7 @@ AQL;
if (
$this->account->type === 'operator'
|| $this->account->type === 'administrator'
|| ($this->account->type === 'worker' && $task->worker === account::worker($this->account->getId())?->getKey())
|| ($this->account->type === 'worker' && $task->worker === account::worker($this->account->getId())?->id)
) {
// Авторизован аккаунт (если сотрудник, то назначен на эту заявку)
@@ -2513,7 +2792,7 @@ AQL;
if (
$this->account->type === 'operator'
|| $this->account->type === 'administrator'
|| ($this->account->type === 'market' && $task->market === account::market($this->account->getId())?->getKey())
|| ($this->account->type === 'market' && $task->market === account::market($this->account->getId())?->id)
) {
// Авторизован аккаунт (если магазин, то назначен на эту заявку)
@@ -2664,7 +2943,7 @@ AQL;
if (
$this->account->type === 'operator'
|| $this->account->type === 'administrator'
|| ($this->account->type === 'worker' && $task->worker === account::worker($this->account->getId())?->getKey())
|| ($this->account->type === 'worker' && $task->worker === account::worker($this->account->getId())?->id)
) {
// Авторизован аккаунт (если сотрудник, то назначен на эту заявку)
@@ -2678,7 +2957,7 @@ AQL;
'from' => [
'_key' => $this->account->getKey(),
'type' => $this->account->type
],
] + ($this->account->type === 'worker' ? ['id' => $task->worker] : []),
'type' => $parameters['type'] ?? 'message',
'text' => $parameters['text'],
'readed' => [$this->account->getKey() => 0],
@@ -2699,6 +2978,12 @@ AQL;
$task->problematic = false;
}
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано в ArangoDB
@@ -2736,7 +3021,7 @@ AQL;
if (
$this->account->type === 'operator'
|| $this->account->type === 'administrator'
|| ($this->account->type === 'market' && $task->market === account::market($this->account->getId())?->getKey())
|| ($this->account->type === 'market' && $task->market === account::market($this->account->getId())?->id)
) {
// Авторизован аккаунт (если магазин, то назначен на эту заявку)
@@ -2750,7 +3035,7 @@ AQL;
'from' => [
'_key' => $this->account->getKey(),
'type' => $this->account->type
],
] + ($this->account->type === 'market' ? ['id' => $task->market] : []),
'type' => $parameters['type'] ?? 'message',
'text' => $parameters['text'],
'readed' => [$this->account->getKey() => 0],
@@ -2771,6 +3056,12 @@ AQL;
$task->problematic = false;
}
// Запись в реестре последних обновивших
$task->updates = [$this->account->type => match ($this->account->type) {
'worker', 'market' => account::{$this->account->type}($this->account->getId())?->id,
default => $this->account->getKey()
}] + ($task->updates ?? []);
if (_core::update($task)) {
// Записано в ArangoDB
@@ -2784,7 +3075,6 @@ AQL;
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode(
[

View File

@@ -29,6 +29,11 @@ final class worker extends core
{
use errors;
/**
* Типы работ
*/
final public const WORKS = ['Кассир', 'Выкладчик', 'Гастроном', 'Бригадир', 'Грузчик', 'Мобильный грузчик', 'Мобильный универсал'];
/**
* Главная страница
*
@@ -39,12 +44,12 @@ final class worker extends core
// Авторизация
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт оператора или администратора
foreach (['active', 'inactive', 'fined', 'decent', 'hided', 'fired'] as $name) {
// Перебор фильтров статусов
// Инициализация значения (приоритет у cookie)
$value = $_COOKIE["workers_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['workers']['filters'][$name] ?? 0;
$value = $_COOKIE["workers_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['workers']['filters'][$name] ?? 0;
// Инициализировано значение?
if ($value === null || $value === 0) continue;
@@ -94,7 +99,7 @@ final class worker extends core
// Перебор фильтров статусов
// Инициализация значения (приоритет у cookie) (отсутствие значения или значение 0 вызывают continue)
if (empty($value = $_COOKIE["workers_filter_$name"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['workers']['filters'][$name] ?? 0)) continue;
if (empty($value = $_COOKIE["workers_filter_$name"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['workers']['filters'][$name] ?? 0)) continue;
// Конвертация ярлыков
$converted = match ($name) {
@@ -135,7 +140,7 @@ final class worker extends core
if (!empty($filters_statuses_after_merged)) $filters_after .= empty($filters_after) ? $filters_statuses_after_merged : " && ($filters_statuses_after_merged)";
// Инициализация строки поиска
$search = $_COOKIE["workers_filter_search"] ?? $this->session->buffer[$_SERVER['INTERFACE']]['workers']['filters']['search'] ?? '';
$search = $_COOKIE["workers_filter_search"] ?? @$this->session->buffer[$_SERVER['INTERFACE']]['workers']['filters']['search'] ?? '';
if (mb_strlen($search) < 3) $search = null;
$search_query = empty($search)
? null
@@ -147,6 +152,7 @@ final class worker extends core
|| a.department.address IN TOKENS(@search, 'text_ru')
|| a.requisites IN TOKENS(@search, 'text_ru')
|| STARTS_WITH(a._key, @search)
|| STARTS_WITH(a.id, @search)
|| STARTS_WITH(a.name.first, @search)
|| STARTS_WITH(a.name.second, @search)
|| STARTS_WITH(a.name.last, @search)
@@ -161,6 +167,7 @@ final class worker extends core
|| STARTS_WITH(a.requisites, @search)
|| STARTS_WITH(a.tax, @search)
|| (LENGTH(@search) > 5 && LEVENSHTEIN_MATCH(a._key, TOKENS(@search, 'text_en')[0], 2, true))
|| (LENGTH(@search) > 4 && LEVENSHTEIN_MATCH(a.id, TOKENS(@search, 'text_en')[0], 1, true))
|| (LENGTH(@search) > 3 && LEVENSHTEIN_MATCH(a.name.first, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 3 && LEVENSHTEIN_MATCH(a.name.second, TOKENS(@search, 'text_ru')[0], 2, true))
|| (LENGTH(@search) > 3 && LEVENSHTEIN_MATCH(a.name.last, TOKENS(@search, 'text_ru')[0], 2, true))
@@ -251,6 +258,7 @@ final class worker extends core
else if (!empty($parameters['account_number']) && strlen($parameters['account_number']) < 11) throw new exception('Несоответствие формату SIM-номера аккаунта сотрудника');
else if (!empty($parameters['worker_mail']) && preg_match('/^.+@.+\.\w+$/', $parameters['worker_mail']) === 0) throw new exception('Несоответствие формату почты сотрудника');
else if (!empty($parameters['account_mail']) && preg_match('/^.+@.+\.\w+$/', $parameters['account_mail']) === 0) throw new exception('Несоответствие формату почты аккаунта сотрудника');
else if (!empty($parameters['worker_id']) && model::read('d.id == "' . $parameters['worker_id'] . '"', errors: $this->errors['account']) instanceof _document) throw new exception('Уже существует сотрудник с данным идентификатором');
// Универсализация
/* $parameters['worker_number'] = (int) $parameters['worker_number']; */
@@ -258,6 +266,7 @@ final class worker extends core
if (!empty($parameters['requisites']) && $parameters['worker_requisites'][-1] === '.') $parameters['worker_requisites'] .= '.';
if (!empty($parameters['worker_birth'])) $parameters['worker_birth'] = DateTime::createFromFormat('Y-m-d', $parameters['worker_birth'])->getTimestamp();
if (!empty($parameters['worker_issued'])) $parameters['worker_issued'] = DateTime::createFromFormat('Y-m-d', $parameters['worker_issued'])->getTimestamp();
if (!empty($parameters['work'])) $parameters['work'] = in_array($parameters['work'], static::WORKS) ? $parameters['work'] : static::WORKS[0];
if (!empty($parameters['worker_hiring'])) $parameters['worker_hiring'] = DateTime::createFromFormat('Y-m-d', $parameters['worker_hiring'])->getTimestamp();
// Создание аккаунта
@@ -269,7 +278,7 @@ final class worker extends core
'second' => $parameters['account_name_second'],
'last' => $parameters['account_name_last']
],
'number' => $parameters['account_number'] === 0 ? '' : $parameters['account_number'],
'number' => $parameters['account_number'],
'mail' => $parameters['account_mail'],
'password' => sodium_crypto_pwhash_str(
$parameters['account_password'],
@@ -293,8 +302,8 @@ final class worker extends core
];
}
// Инициализация идентификатора аккаунта (ключ документа инстанции аккаунта в базе данных)
$_key = preg_replace('/.+\//', '', $account ?? '');
// Инициализация идентификатора сотрудника
$id = empty($parameters['worker_id']) ? model::id() : $parameters['worker_id'];
// Запись заголовков ответа
header('Content-Type: application/json');
@@ -308,7 +317,7 @@ final class worker extends core
echo json_encode(
[
'clipboard' => empty($this->errors['account']) ? <<<TEXT
Идентификатор: $_key
Номер: {$parameters['account_number']}
Пароль: {$parameters['account_password']}
TEXT : '',
'errors' => self::parse_only_text($this->errors['account'])
@@ -323,40 +332,47 @@ final class worker extends core
flush();
try {
// Создание сотрудника
$worker = model::create(
data: [
'name' => [
'first' => $parameters['worker_name_first'],
'second' => $parameters['worker_name_second'],
'last' => $parameters['worker_name_last']
],
'number' => $parameters['worker_number'] === 0 ? '' : $parameters['worker_number'],
'mail' => $parameters['worker_mail'],
'birth' => $parameters['worker_birth'],
'passport' => $parameters['worker_passport'],
'issued' => $parameters['worker_issued'],
'department' => [
'number' => $parameters['worker_department_number'],
'address' => $parameters['worker_department_address']
],
'requisites' => $parameters['worker_requisites'],
'payment' => $parameters['worker_payment'],
'tax' => $parameters['worker_tax'],
'city' => $parameters['worker_city'],
'district' => $parameters['worker_district'],
'address' => $parameters['worker_address'],
'hiring' => $parameters['worker_hiring'],
'rating' => 3
],
errors: $this->errors['account']
);
if (isset($account)) {
// Инициализирован аккаунт
// Проверка существования созданного сотрудника
if (empty($worker)) throw new exception('Не удалось создать сотрудника');
// Создание сотрудника
$worker = model::create(
data: [
'id' => (string) $id,
'name' => [
'first' => $parameters['worker_name_first'],
'second' => $parameters['worker_name_second'],
'last' => $parameters['worker_name_last']
],
'number' => $parameters['worker_number'],
'mail' => $parameters['worker_mail'],
'birth' => $parameters['worker_birth'],
'passport' => $parameters['worker_passport'],
'issued' => $parameters['worker_issued'],
'department' => [
'number' => $parameters['worker_department_number'],
'address' => $parameters['worker_department_address']
],
'requisites' => $parameters['worker_requisites'],
'payment' => $parameters['worker_payment'],
'tax' => $parameters['worker_tax'],
'city' => $parameters['worker_city'],
'district' => $parameters['worker_district'],
'address' => $parameters['worker_address'],
'work' => $parameters['worker_work'],
'hiring' => $parameters['worker_hiring'],
'rating' => 3
],
errors: $this->errors['account']
);
// Создание ребра: account -> worker
account::connect($account, $worker, 'worker', $this->errors['account']);
// Проверка существования созданного сотрудника
if (empty($worker)) throw new exception('Не удалось создать сотрудника');
// Создание ребра: account -> worker
account::connect($account, $worker, 'worker', $this->errors['account']);
}
throw new exception('Не инициализирован аккаунт');
} catch (exception $e) {
// Write to the errors registry
$this->errors['account'][] = [
@@ -380,7 +396,7 @@ final class worker extends core
// Авторизован аккаунт администратора или оператора
// Инициализация данных сотрудника
$worker = model::read('d._key == "' . $parameters['id'] . '"', return: '{ name: d.name, number: d.number, mail: d.mail, birth: d.birth, passport: d.passport, issued: d.issued, department: d.department, requisites: d.requisites, payment: d.payment, tax: d.tax, city: d.city, district: d.district, address: d.address, hiring: d.hiring}')->getAll();
$worker = model::read('d.id == "' . urldecode($parameters['id']) . '"', return: '{ name: d.name, number: d.number, mail: d.mail, birth: d.birth, passport: d.passport, issued: d.issued, department: d.department, requisites: d.requisites, payment: d.payment, tax: d.tax, city: d.city, district: d.district, address: d.address, worl: d.work, hiring: d.hiring}')->getAll();
if (!empty($worker)) {
// Найдены данные сотрудника
@@ -420,14 +436,15 @@ final class worker extends core
// Авторизован аккаунт администратора или оператора
// Инициализация данных сотрудника
$worker = model::read('d._key == "' . $parameters['id'] . '"');
$worker = model::read('d.id == "' . urldecode($parameters['id']) . '"');
if (!empty($worker)) {
// Найден сотрудник
// Универсализация
if (!empty($parameters['birth'])) $parameters['birth'] = DateTime::createFromFormat('Y-m-d', $parameters['birth'])->getTimestamp();
if (!empty($parameters['issued'])) $parameters['issued'] = DateTime::createFromFormat('Y-m-d', $parameters['issued'])->getTimestamp();
if (!empty($parameters['work'])) $parameters['work'] = in_array($parameters['work'], static::WORKS) ? $parameters['work'] : static::WORKS[0];
if (!empty($parameters['hiring'])) $parameters['hiring'] = DateTime::createFromFormat('Y-m-d', $parameters['hiring'])->getTimestamp();
// Инициализация параметров (перезапись переданными значениями)
@@ -449,6 +466,7 @@ final class worker extends core
if ($parameters['city'] !== $worker->city) $worker->city = $parameters['city'];
if ($parameters['district'] !== $worker->district) $worker->district = $parameters['district'];
if ($parameters['address'] !== $worker->address) $worker->address = $parameters['address'];
if ($parameters['work'] !== $worker->work) $worker->work = $parameters['work'];
if ($parameters['hiring'] !== $worker->hiring) $worker->hiring = $parameters['hiring'];
if (_core::update($worker)) {
@@ -504,6 +522,114 @@ final class worker extends core
return null;
}
/**
* Пометить уволенным
*
* @param array $parameters Параметры запроса
*/
public function fire(array $parameters = []): ?string
{
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
// Инициализация данных сотрудника
$worker = model::read('d.id == "' . urldecode($parameters['id']) . '"');
if (!empty($worker)) {
// Найден сотрудник
// Увольнение
$worker->active = false;
$worker->fired = true;
if (_core::update($worker)) {
// Записаны данные сотрудника
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'fired' => true,
'errors' => self::parse_only_text($this->errors)
];
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не удалось записать изменения в базу данных');
} else throw new exception('Не удалось найти аккаунт');
}
// Возврат (провал)
return null;
}
/**
* Снять пометку уволенного (нанять)
*
* @param array $parameters Параметры запроса
*/
public function hire(array $parameters = []): ?string
{
if ($this->account->status() && ($this->account->type === 'administrator' || $this->account->type === 'operator')) {
// Авторизован аккаунт администратора или оператора
// Инициализация данных сотрудника
$worker = model::read('d.id == "' . urldecode($parameters['id']) . '"');
if (!empty($worker)) {
// Найден сотрудник
// Увольнение
$worker->active = true;
$worker->fired = false;
if (_core::update($worker)) {
// Записаны данные сотрудника
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Инициализация буфера вывода
ob_start();
// Инициализация буфера ответа
$return = [
'hired' => true,
'errors' => self::parse_only_text($this->errors)
];
// Генерация ответа
echo json_encode($return);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не удалось записать изменения в базу данных');
} else throw new exception('Не удалось найти аккаунт');
}
// Возврат (провал)
return null;
}
/**
* Прочитать данные сотрудников для <datalist>
*
@@ -515,7 +641,7 @@ final class worker extends core
// Авторизован аккаунт оператора или магазина
// Инициализация данных сотрудников
$this->view->workers = model::read(filter: 'd.active == true', amount: 10000, return: '{ _key: d._key, name: d.name }');
$this->view->workers = model::read(filter: 'd.active == true', amount: 10000, return: '{ id: d.id, name: d.name }');
// Универсализация
if ($this->view->workers instanceof _document) $this->view->workers = [$this->view->workers];

View File

@@ -42,7 +42,7 @@ final class account extends core
* Конструктор
*
* @param ?session $session Инстанция сессии
* @param ?string $authenticate Аутентифицировать аккаунт? Если да, то какой категории? ([worker|operator|market] из $_SERVER['INTERFACE'])
* @param ?string $authenticate Аутентифицировать аккаунт? Если да, то какой категории? ([worker|market|operator|administrator] из $_SERVER['INTERFACE'])
* @param array &$errors Реестр ошибок
*
* @return static Инстанция аккаунта
@@ -62,12 +62,19 @@ final class account extends core
// Связь сессии с аккаунтом
session::connect($session->getId(), $this->document->getId(), $errors);
// Блокировка доступа
if ($account?->active !== true) throw new exception('Свяжитесь с оператором');
else if ($account?->banned === true) throw new exception('Свяжитесь с оператором');
else if ($account->type === 'worker')
if (($worker = account::worker($account->getId()))?->active !== true) throw new exception('Свяжитесь с оператором');
else if ($worker?->fired === true) throw new exception('Свяжитесь с оператором');
return $this;
} else {
// Не найден связанный с сессией аккаунт
if (
match ($authenticate) {
'worker', 'operator', 'market', 'administrator' => true,
'worker', 'market', 'operator', 'administrator' => true,
default => false
}
) {
@@ -94,11 +101,16 @@ final class account extends core
// Удаление использованных данных из буфера сессии
$session->write(['entry' => ['number' => null, 'password' => null]]);
// Блокировка доступа
if ($account?->active !== true) throw new exception('Свяжитесь с оператором');
else if ($account?->banned === true) throw new exception('Свяжитесь с оператором');
else if ($account->type === 'worker')
if (($worker = account::worker($account->getId()))?->active !== true) throw new exception('Свяжитесь с оператором');
else if ($worker?->fired === true) throw new exception('Свяжитесь с оператором');
// Выход (успех)
return $this;
} else throw new exception('Неправильный пароль');
throw new exception('Неизвестная ошибка на этапе проверки пароля');
} else throw new exception('Не найден аккаунт');
} else throw new exception('Не найден пароль в буфере сессии');
} else if (!empty($session->buffer['operator']['entry']['_key'])) {
@@ -125,17 +137,15 @@ final class account extends core
// Выход (успех)
return $this;
} else throw new exception('Неправильный пароль');
throw new exception('Неизвестная ошибка на этапе проверки пароля');
}
} else throw new exception('Не найден пароль в буфере сессии');
} else if (!empty($session->buffer['market']['entry']['_key'])) {
} else if (!empty($session->buffer['market']['entry']['id'])) {
// Найден идентификатор магазина в буфере сессии
if (!empty($session->buffer['market']['entry']['password'])) {
// Найден пароль в буфере сессии
if (($account = self::read('d._key == "' . $session->buffer['market']['entry']['_key'] . '" && d.type == "market"', amount: 1)) instanceof _document) {
if (($account = market::account(market::read('d.id == "' . $session->buffer['market']['entry']['id'] . '"', amount: 1)?->getId()) ?? null) instanceof _document) {
// Найден аккаунт (игнорируются ошибки)
if (sodium_crypto_pwhash_str_verify($account->password, $session->buffer['market']['entry']['password'])) {
@@ -153,8 +163,6 @@ final class account extends core
// Выход (успех)
return $this;
} else throw new exception('Неправильный пароль');
throw new exception('Неизвестная ошибка на этапе проверки пароля');
}
} else throw new exception('Не найден пароль в буфере сессии');
} else if (!empty($session->buffer['administrator']['entry'])) {
@@ -181,8 +189,6 @@ final class account extends core
// Выход (успех)
return $this;
} else throw new exception('Неправильный пароль');
throw new exception('Неизвестная ошибка на этапе проверки пароля');
}
} else throw new exception('Не найден пароль в буфере сессии');
} else throw new exception('Не найдены данные первичной идентификации в буфере сессии');
@@ -227,7 +233,7 @@ final class account extends core
LIMIT 1
RETURN e
)
FILTER d._id == e[0]._to && d.active == true
FILTER d._id == e[0]._to
SORT d.created DESC, d._key DESC
LIMIT 1
RETURN d
@@ -285,8 +291,8 @@ final class account extends core
LIMIT 1
RETURN e
)
FILTER d._id == e[0]._to && d.active == true
SORT d.created DESC, d._key DESC
FILTER d._id == e[0]._to
SORT d.created DESC, d.id DESC
LIMIT 1
RETURN d
AQL,
@@ -316,7 +322,7 @@ final class account extends core
*
* Ищет связь аккаунта с сотрудником, если не находит, то создаёт её
*
* @param string $worker Идентификатор инстанции документа аккаунта в базе данных
* @param string $account Идентификатор инстанции документа аккаунта в базе данных
* @param string $target Идентификатор инстанции документа цели в базе данны (подразумевается сотрудник или магазин)
* @param string $type Тип подключения (worker|market)
* @param array &$errors Реестр ошибок
@@ -342,11 +348,11 @@ final class account extends core
LIMIT 1
RETURN d
AQL,
self::COLLECTION . '_edge_' . worker::COLLECTION,
self::COLLECTION . "_edge_$type",
$account,
$target
)) instanceof _document
|| $id = document::write(static::$arangodb->session, self::COLLECTION . "_edge_$type", [
|| document::write(static::$arangodb->session, self::COLLECTION . "_edge_$type", [
'_from' => $account,
'_to' => $target
])
@@ -381,7 +387,7 @@ final class account extends core
{
try {
if (collection::init(static::$arangodb->session, self::COLLECTION))
if ($id = document::write(static::$arangodb->session, self::COLLECTION, $data + ['active' => true])) return $id;
if ($id = (string) document::write(static::$arangodb->session, self::COLLECTION, $data + ['active' => true])) return $id;
else throw new exception('Не удалось создать аккаунт');
else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {

View File

@@ -132,6 +132,145 @@ class core extends model
return null;
}
/**
* Collect from ArangoDB
*
* @param string $filter Выражения для фильтрации на языке AQL
* @param string $sort Выражение для сортировки на языке AQL
* @param int $amount Количество документов для выборки
* @param int $page Страница
* @param string $index Параметр по которому будет производиться сборка
* @param string $return Выражение описываемое возвращаемые данные на языке AQL
* @param array &$errors Реестр ошибок
*
* @return _document|array|null Массив инстанций документов в базе данных, если найдены
*/
public static function collect(
string $filter = '',
string $sort = 'd.created DESC, d._key DESC',
int $amount = 1,
int $page = 1,
string $index = 'd.updated',
string $return = 'd',
array &$errors = []
): _document|array|null {
try {
if (collection::init(static::$arangodb->session, static::COLLECTION)) {
// Инициализирована коллекция
// Exit (success)
return collection::search(
static::$arangodb->session,
sprintf(
<<<'AQL'
FOR d IN %s
%s
%s
LIMIT %d, %d
COLLECT index = %s INTO group = %s
RETURN { [index]: group }
AQL,
static::COLLECTION,
empty($filter) ? '' : "FILTER $filter",
empty($sort) ? '' : "SORT $sort",
--$page <= 0 ? 0 : $amount * $page,
$amount,
$index,
$return
)
);
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в реестр ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Count documents in ArangoDB
*
* @param ?string $collection Коллекция для подсчёта
* @param array &$errors Реестр ошибок
*
* @return int|null Количество документов в базе данных, если найдены
*/
public static function count(?string $collection = null, array &$errors = []): int|null {
try {
if (collection::init(static::$arangodb->session, static::COLLECTION)) {
// Инициализирована коллекция
// Exit (success)
return collection::search(
static::$arangodb->session,
sprintf(
<<<'AQL'
RETURN LENGTH(%s)
AQL,
$collection ?? static::COLLECTION
)
);
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в реестр ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Generate identifier
*
* @param array &$errors Реестр ошибок
*
* @return int Идентиикатор (свободный)
*/
public static function id(array &$errors = []): int
{
try {
if (collection::init(static::$arangodb->session, static::COLLECTION)) {
// Инициализирована коллекция
// Exit (success)
return collection::search(
static::$arangodb->session,
sprintf(
<<<'AQL'
RETURN MAX((FOR d in %s RETURN +d.id))
AQL,
$collection ?? static::COLLECTION
)
) + 1;
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в реестр ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return 0;
}
/**
* Delete from ArangoDB
*

View File

@@ -0,0 +1,693 @@
<?php
declare(strict_types=1);
namespace mirzaev\ebala\models;
// Файлы проекта
use mirzaev\ebala\models\traits\status;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Фреймворк для работы с таблицами
use PhpOffice\PhpSpreadsheet\IOFactory,
PhpOffice\PhpSpreadsheet\Style\Color,
PhpOffice\PhpSpreadsheet\Style\Fill,
PhpOffice\PhpSpreadsheet\Style\Conditional,
PhpOffice\PhpSpreadsheet\Style\Alignment,
PhpOffice\PhpSpreadsheet\Spreadsheet;
// System libraries
use exception;
/**
* Модель выплат
*
* @package mirzaev\ebala\models
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class payments extends core
{
use status;
/**
* Сотрудники
*
* Расчитать стоимость работы сотрудников за выбранный период и сгенерировать excel-документ
*
* @param int $from Начальная дата для выборки заявок (unixtime)
* @param int $to Конечная дата для выборки заявок (unixtime)
* @param array $errors Errors registry
*
* @return bool Записан буфер вывода сгенерированный excel-документ?
*/
public static function workers(int $from, int $to, array &$errors = []): bool
{
try {
// Чтение заявок
$tasks = @task::read("d.date >= $from && d.date <= $to && d.problematic == false && d.completed == true", amount: 999999, return: '{worker: d.worker, market: d.market, date: d.date, work: d.work, start: d.start, end: d.end, commentary: d.commentary, rating: d.rating, review: d.review}', errors: $errors);
if (is_array($tasks) && count($tasks) > 0) {
// Найдены заявки
// Инициализация таблицы
$spreadsheet = new Spreadsheet();
// Конвертация unixtime в читаемую дату
$_from = gmdate("d.m.Y", $from);
$_to = gmdate("d.m.Y", $to);
// Запись настроек таблицы
$spreadsheet
->getProperties()
->setCreator('Спецресурс')
->setLastModifiedBy('Спецресурс')
->setTitle("$_from - $_to")
->setSubject("Зарплаты сотрудникам $_from - $_to")
->setDescription("Зарплаты сотрудникам за период с $_from по $_to")
->setKeywords('зарплата сотрудники');
// Открытие страницы
$spreadsheet->setActiveSheetIndex(0);
// Запись первой строки (названия колонок)
$spreadsheet
->getActiveSheet()
->setCellValue('A1', 'Адрес')
->setCellValue('B1', 'Дата выплаты')
->setCellValue('C1', 'Дата заявки')
->setCellValue('D1', 'Магазин')
->setCellValue('E1', 'Сотрудник')
->setCellValue('F1', 'Работа')
->setCellValue('G1', 'Начало')
->setCellValue('H1', 'Конец')
->setCellValue('I1', 'Часы')
->setCellValue('J1', 'Статус')
->setCellValue('K1', 'Рейтинг')
->setCellValue('L1', 'Отзыв')
->setCellValue('M1', 'ФИО')
->setCellValue('N1', 'Час')
->setCellValue('O1', 'Смена')
->setCellValue('P1', 'Штраф')
->setCellValue('Q1', 'Премия')
->setCellValue('R1', 'Полная оплата')
->setCellValue('S1', 'Наличными')
->setCellValue('T1', 'Наличные?')
->setCellValue('U1', 'Переводом')
->setCellValue('V1', 'Реквизиты')
->setCellValue('W1', 'Тариф')
->setCellValue('X1', 'Без НДС')
->setCellValue('Y1', 'Прибыль')
->setCellValue('Z1', 'Примечание')
->setCellValue('AA1', 'Долг сотрудника')
->setCellValue('AB1', 'Кто платит')
->setCellValue('AC1', 'Кто платит');
// Запись цвета верхнего колонтинула
$spreadsheet
->getActiveSheet()
->getStyle('A1:AC1')
->getFill()
->setFillType(Fill::FILL_SOLID)
->getStartColor()
->setARGB('ffffffb9');
// Запись толщины текста верхнего колонтинула
$spreadsheet
->getActiveSheet()
->getStyle('A1:AC1')
->getFont()
->setBold(true);
// Запись размера текста верхнего колонтинула
$spreadsheet
->getActiveSheet()
->getStyle('A1:AC1')
->getFont()
->setSize(13);
// Запись позиции текста верхнего колонтинула
$spreadsheet
->getActiveSheet()
->getStyle('A1:AC1')
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_CENTER)
->setVertical(Alignment::VERTICAL_CENTER);
// Запись ширины строки верхнего колонтинула
$spreadsheet
->getActiveSheet()
->getRowDimension(1)
->setRowHeight(24);
// Запись ширины колонок
$spreadsheet->getActiveSheet()->getColumnDimension('A')->setWidth(30);
$spreadsheet->getActiveSheet()->getColumnDimension('B')->setWidth(18);
$spreadsheet->getActiveSheet()->getColumnDimension('C')->setWidth(18);
$spreadsheet->getActiveSheet()->getColumnDimension('D')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('E')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('F')->setWidth(22);
$spreadsheet->getActiveSheet()->getColumnDimension('G')->setWidth(12);
$spreadsheet->getActiveSheet()->getColumnDimension('H')->setWidth(12);
$spreadsheet->getActiveSheet()->getColumnDimension('I')->setWidth(12);
$spreadsheet->getActiveSheet()->getColumnDimension('J')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('K')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('L')->setWidth(40);
$spreadsheet->getActiveSheet()->getColumnDimension('M')->setWidth(32);
$spreadsheet->getActiveSheet()->getColumnDimension('N')->setWidth(12);
$spreadsheet->getActiveSheet()->getColumnDimension('O')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('P')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('Q')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('R')->setWidth(16);
$spreadsheet->getActiveSheet()->getColumnDimension('S')->setWidth(16);
$spreadsheet->getActiveSheet()->getColumnDimension('T')->setWidth(22);
$spreadsheet->getActiveSheet()->getColumnDimension('U')->setWidth(22);
$spreadsheet->getActiveSheet()->getColumnDimension('V')->setWidth(80);
$spreadsheet->getActiveSheet()->getColumnDimension('W')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('X')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('Y')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('Z')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('AA')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('AB')->setWidth(14);
$spreadsheet->getActiveSheet()->getColumnDimension('AC')->setWidth(14);
// Инициализация счётчика строк
$row = 2;
foreach ($tasks as $task) {
// Перебор заявок
// Инициализация сотрудника
$worker = worker::read('d.id == "' . $task->worker . '"');
if ($worker instanceof _document) {
// Найден сотрудник
// Инициализация магазина
$market = market::read('d.id == "' . $task->market . '"');
if ($market instanceof _document) {
// Найден магазин
// Запись строки
$spreadsheet
->getActiveSheet()
->setCellValue("A$row", $market->city . ' ' . $market->address)
->setCellValue("B$row", '')
->setCellValue("C$row", gmdate("d.m.Y", $task->date))
->setCellValue("D$row", $market->id)
->setCellValue("E$row", $worker->id)
->setCellValue("F$row", $task->work)
->setCellValue("G$row", $task->start)
->setCellValue("H$row", $task->end)
->setCellValue("I$row", $hours = task::hours($task->start, $task->end, $errors))
->setCellValue("J$row", '')
->setCellValue("K$row", $task->rating ?? 'Отсутствует')
->setCellValue("L$row", $task->review ?? '')
->setCellValue("M$row", $worker->name['second'] . ' ' . $worker->name['first'] . ' ' . $worker->name['last'])
->setCellValue("N$row", $hour = static::hour('worker', $market->city, $task->work))
->setCellValue("O$row", $payment = $hour * $hours)
->setCellValue("P$row", ($penalty = static::penalty($task->rating ?? null)) === null ? $payment : $penalty)
->setCellValue("Q$row", $bonus = static::bonus($task->rating ?? null))
->setCellValue("R$row", $payment + (($penalty = static::penalty($task->rating ?? null)) === null ? $payment : $penalty) + $bonus)
->setCellValue("S$row", '')
->setCellValue("T$row", $worker->payment) // Наличные?
->setCellValue("U$row", '')
->setCellValue("V$row", $worker->requisites)
->setCellValue("W$row", '')
->setCellValue("X$row", '')
->setCellValue("Y$row", '')
->setCellValue("Z$row", '')
->setCellValue("AA$row", '')
->setCellValue("AB$row", '')
->setCellValue("AC$row", '');
// Инкрементация счётчика для генерации следующей строки
++$row;
}
}
}
// Write to output buffer
IOFactory::createWriter($spreadsheet, 'Xlsx')->save('php://output');
// Exit (success)
return true;
}
throw new exception('Не найдены заявки');
} catch (exception $e) {
// Write to the errors registry
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return false;
}
/**
* Магазины
*
* Расчитать ... и сгенерировать excel-документ
*
* @param int $from Начальная дата для выборки заявок (unixtime)
* @param int $to Конечная дата для выборки заявок (unixtime)
* @param array $errors Errors registry
*
* @return bool Записан буфер вывода сгенерированный excel-документ?
*/
public static function markets(int $from, int $to, array &$errors = []): bool
{
try {
// Чтение заявок
$tasks = @task::collect(
"d.date >= $from && d.date <= $to && d.problematic == false && d.completed == true",
sort: 'd.date DESC',
amount: 999999,
index: 'd.date',
return: '{worker: d.worker, market: d.market, date: d.date, work: d.work, start: d.start, end: d.end, commentary: d.commentary, rating: d.rating, review: d.review}',
errors: $errors
);
// Универсализация
if ($tasks instanceof _document) $tasks = [$tasks];
// Инициализация буфера объединённых заявок по дате (подразумеваются дни)
$merged = [];
foreach ($tasks as $groups) {
// Перебор групп заявок разделённых по датам
foreach ($groups->getAll() as $date => $_tasks) {
// Перебор дат (подразумевается только одна)
foreach ($_tasks as $task) {
// Перебор заявок
// Первичная инициализация данных в буфере объединённых заявок по дням
$merged[$task['market']] ??= [];
$merged[$task['market']][$date] ??= [];
$merged[$task['market']][$date][$task['work']] ??= ['workers' => 0, 'hours' => 0];
// Запись в буфер объединённых заявок по дням
$merged[$task['market']][$date][$task['work']]['workers']++;
$merged[$task['market']][$date][$task['work']]['hours'] += task::hours($task['start'], $task['end'], $errors);
}
}
}
if (count($merged) > 0) {
// Найдены сгенерированные данные
// Инициализация таблицы
$spreadsheet = new Spreadsheet();
// Конвертация unixtime в читаемую дату
$_from = gmdate("d.m.Y", $from);
$_to = gmdate("d.m.Y", $to);
// Запись настроек таблицы
$spreadsheet
->getProperties()
->setCreator('Спецресурс')
->setLastModifiedBy('Спецресурс')
->setTitle("$_from - $_to")
->setSubject(" $_from - $_to")
->setDescription(" за период с $_from по $_to")
->setKeywords('магазины');
// Открытие страницы
$spreadsheet->setActiveSheetIndex(0);
// Запись первых строк
$spreadsheet
->getActiveSheet()
->setCellValue('A1', 'К Договору от 0.0.20')
->setCellValue('A2', 'К Договору от 0.0.20')
->setCellValue('A4', 'Детализация выполненных заказов за период')
->setCellValue('A5', "Период: $_from - $_to")
->setCellValue('A6', "Заказчик: ")
->setCellValue('A8', "Магазин")
->setCellValue('B8', "Тип")
->setCellValue('C8', "Адрес")
->setCellValue('D8', "Дата")
->setCellValue('E8', "Работа")
->setCellValue('F8', "Сотрудники")
->setCellValue('G8', "Часы")
->setCellValue('H8', "Тариф")
->setCellValue('I8', "Без НДС")
->setCellValue('J8', "С НДС");
// Запись ширины колонок
$spreadsheet->getActiveSheet()->getColumnDimension('A')->setWidth(12);
$spreadsheet->getActiveSheet()->getColumnDimension('B')->setWidth(16);
$spreadsheet->getActiveSheet()->getColumnDimension('C')->setWidth(32);
$spreadsheet->getActiveSheet()->getColumnDimension('D')->setWidth(16);
$spreadsheet->getActiveSheet()->getColumnDimension('E')->setWidth(16);
$spreadsheet->getActiveSheet()->getColumnDimension('F')->setWidth(16);
$spreadsheet->getActiveSheet()->getColumnDimension('G')->setWidth(12);
$spreadsheet->getActiveSheet()->getColumnDimension('H')->setWidth(12);
$spreadsheet->getActiveSheet()->getColumnDimension('I')->setWidth(18);
$spreadsheet->getActiveSheet()->getColumnDimension('J')->setWidth(18);
// Фиксация верхнего колонтинула
$spreadsheet
->getActiveSheet()
->freezePane('K9');
// Объединение ячеек
$spreadsheet->getActiveSheet()->mergeCells('A1:J1');
$spreadsheet->getActiveSheet()->mergeCells('A2:J2');
$spreadsheet->getActiveSheet()->mergeCells('A4:J4');
$spreadsheet->getActiveSheet()->mergeCells('A5:J5');
$spreadsheet->getActiveSheet()->mergeCells('A6:J6');
// Запись позиций текстов "к договору"
$spreadsheet
->getActiveSheet()
->getStyle('A1:J2')
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_RIGHT);
// Запись позиций текста заголовка
$spreadsheet
->getActiveSheet()
->getStyle('A4:J4')
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_CENTER)
->setVertical(Alignment::VERTICAL_CENTER);
// Запись позиций текста верхнего колонтинула
$spreadsheet
->getActiveSheet()
->getStyle('A8:J8')
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_CENTER)
->setVertical(Alignment::VERTICAL_CENTER);
// Запись цвета верхнего колонтинула (левая половина)
$spreadsheet
->getActiveSheet()
->getStyle('A8:D8')
->getFill()
->setFillType(Fill::FILL_SOLID)
->getStartColor()
->setARGB('ffdfe4ec');
// Запись цвета верхнего колонтинула (правая половина)
$spreadsheet
->getActiveSheet()
->getStyle('E8:J8')
->getFill()
->setFillType(Fill::FILL_SOLID)
->getStartColor()
->setARGB('ff8093b3');
// Запись размера текста верхнего колонтинула
$spreadsheet
->getActiveSheet()
->getStyle('A8:J8')
->getFont()
->setSize(12);
// Запись толщины текста верхнего колонтинула
$spreadsheet
->getActiveSheet()
->getStyle('A8:J8')
->getFont()
->setBold(true);
// Запись ширины строки верхнего колонтинула
$spreadsheet
->getActiveSheet()
->getRowDimension(8)
->setRowHeight(32);
// Инициализация счётчика строк
$row = 9;
// Инициализация буфера объединённых данных всех магазинов
$total = [
'workers' => 0,
'hours' => 0,
'hour' => [],
'payment' => 0,
'vat' => 0
];
foreach ($merged as $id => $dates) {
// Перебор магазинов
// Инициализация магазина
$market = market::read('d.id == "' . $id . '"');
if ($market instanceof _document) {
// Найден магазин
// Инициализация буфера объединённых данных магазина
$result = [
'workers' => 0,
'hours' => 0,
'hour' => [],
'payment' => 0,
'vat' => 0
];
foreach ($dates as $date => $works) {
// Перебор дат заявок
foreach ($works as $work => $task) {
// Перебор заявок
// Запись строки с заявками по дате
$spreadsheet
->setActiveSheetIndex(0)
->setCellValue("A$row", $id)
->setCellValue("B$row", $market->type)
->setCellValue("C$row", $market->address)
->setCellValue("D$row", gmdate("d.m.Y", $date))
->setCellValue("E$row", $work)
->setCellValue("F$row", $task['workers'])
->setCellValue("G$row", $task['hours'])
->setCellValue("H$row", $hour = static::hour('market', $market->city, $work))
->setCellValue("I$row", $payment = $hour * $task['hours'])
->setCellValue("J$row", $payment);
// Запись в буфер объединённых данных магазина
$result['workers'] += $task['workers'];
$result['hours'] += $task['hours'];
$result['hour'][] = $hour;
$result['payment'] += $payment;
$result['vat'] += $payment;
// Инкрементация счётчика для генерации следующей строки
++$row;
}
}
// Запись строки с общими данными магазина
$spreadsheet
->setActiveSheetIndex(0)
->setCellValue("A$row", "Всего ($id)")
->setCellValue("B$row", '')
->setCellValue("C$row", '')
->setCellValue("D$row", '')
->setCellValue("E$row", '')
->setCellValue("F$row", $result['workers'])
->setCellValue("G$row", $result['hours'])
->setCellValue("H$row", $hour = array_sum($result['hour']) / count($result['hour']))
->setCellValue("I$row", $result['payment'])
->setCellValue("J$row", $result['vat']);
// Запись в буфер объединённых данных всех магазинов
$total['workers'] += $result['workers'];
$total['hours'] += $result['hours'];
$total['hour'][] = $hour;
$total['payment'] += $result['payment'];
$total['vat'] += $result['vat'];
// Запись цвета строки с общими данными магазина
$spreadsheet
->getActiveSheet()
->getStyle("A$row:J$row")
->getFill()
->setFillType(Fill::FILL_SOLID)
->getStartColor()
->setARGB('ffdfe4ec');
++$row;
}
}
// Запись строки с общими данными всех магазинов
$spreadsheet
->setActiveSheetIndex(0)
->setCellValue("A$row", "Итого")
->setCellValue("B$row", '')
->setCellValue("C$row", '')
->setCellValue("D$row", '')
->setCellValue("E$row", '')
->setCellValue("F$row", $total['workers'])
->setCellValue("G$row", $total['hours'])
->setCellValue("H$row", array_sum($total['hour']) / count($total['hour']))
->setCellValue("I$row", $total['payment'])
->setCellValue("J$row", $total['vat']);
// Запись цвета строки с общими данными всех магазинов
$spreadsheet
->getActiveSheet()
->getStyle("A$row:J$row")
->getFill()
->setFillType(Fill::FILL_SOLID)
->getStartColor()
->setARGB('ffdfe4ec');
// Запись жирного текста для строки с общими данными всех магазинов
$spreadsheet
->getActiveSheet()
->getStyle("A$row:J$row")
->getFont()
->setBold(true);
// Write to output buffer
IOFactory::createWriter($spreadsheet, 'Xlsx')->save('php://output');
// Exit (success)
return true;
}
throw new exception('Не найдены заявки');
} catch (exception $e) {
// Write to the errors registry
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return false;
}
/**
* Determine tariff
*
* @param string $type Type of tariffs (market, worker)
* @param string $city City in which the place of work is located
* @param string $work Type of work
*
* @return int|float Cost of work per hour (rubles)
*/
public static function hour(string $type, string $city, string $work): int|float
{
return
match (mb_strtolower($type)) {
'market', 'магазин' => match (mb_strtolower($city)) {
'красноярск' => match (mb_strtolower($work)) {
'cashiers', 'cashier', 'кассиры', 'кассир' => 257.07,
'displayers', 'displayer', 'выкладчики', 'выкладчик' => 257.07,
'gastronomes', 'gastronome', 'гастрономы', 'гастроном' => 257.07,
'brigadiers', 'brigadier', 'бригадиры', 'бригадир' => 360,
'loaders', 'loader', 'грузчики', 'грузчик' => 255.645,
'loaders_mobile', 'loader_mobile', 'мобильные грузчики', 'мобильный грузчик' => 305,
'universals_mobile', 'universal_mobile', 'мобильные универсалы', 'мобильный универсал' => 305,
default => 0
},
'железногорск', 'сосновоборск', 'тыва' => match (mb_strtolower($work)) {
'cashiers', 'cashier', 'кассиры', 'кассир' => 263.34,
'displayers', 'displayer', 'выкладчики', 'выкладчик' => 263.34,
'gastronomes', 'gastronome', 'гастрономы', 'гастроном' => 263.34,
'brigadiers', 'brigadier', 'бригадиры', 'бригадир' => 360,
'loaders', 'loader', 'грузчики', 'грузчик' => 255.645,
'loaders_mobile', 'loader_mobile', 'мобильные грузчики', 'мобильный грузчик' => 305,
'universals_mobile', 'universal_mobile', 'мобильные универсалы', 'мобильный универсал' => 305,
default => 0
},
'хакасия', 'иркутск' => match (mb_strtolower($work)) {
'cashiers', 'cashier', 'кассиры', 'кассир' => 245.385,
'displayers', 'displayer', 'выкладчики', 'выкладчик' => 245.385,
'gastronomes', 'gastronome', 'гастрономы', 'гастроном' => 245.385,
'brigadiers', 'brigadier', 'бригадиры', 'бригадир' => 360,
'loaders', 'loader', 'грузчики', 'грузчик' => 255.645,
'loaders_mobile', 'loader_mobile', 'мобильные грузчики', 'мобильный грузчик' => 305,
'universals_mobile', 'universal_mobile', 'мобильные универсалы', 'мобильный универсал' => 305,
default => 0
},
default => 0
},
'worker', 'сотрудник' => match (mb_strtolower($city)) {
'красноярск' => match (mb_strtolower($work)) {
'cashiers', 'cashier', 'кассиры', 'кассир' => 190.91,
'displayers', 'displayer', 'выкладчики', 'выкладчик' => 190.91,
'gastronomes', 'gastronome', 'гастрономы', 'гастроном' => 190.91,
'brigadiers', 'brigadier', 'бригадиры', 'бригадир' => 250,
'loaders', 'loader', 'грузчики', 'грузчик' => 177.27,
'loaders_mobile', 'loader_mobile', 'мобильные грузчики', 'мобильный грузчик' => 250,
'universals_mobile', 'universal_mobile', 'мобильные универсалы', 'мобильный универсал' => 250,
default => 0
},
'железногорск', 'сосновоборск', 'тыва' => match (mb_strtolower($work)) {
'cashiers', 'cashier', 'кассиры', 'кассир' => 190.91,
'displayers', 'displayer', 'выкладчики', 'выкладчик' => 190.91,
'gastronomes', 'gastronome', 'гастрономы', 'гастроном' => 190.91,
'brigadiers', 'brigadier', 'бригадиры', 'бригадир' => 250,
'loaders', 'loader', 'грузчики', 'грузчик' => 177.27,
'loaders_mobile', 'loader_mobile', 'мобильные грузчики', 'мобильный грузчик' => 250,
'universals_mobile', 'universal_mobile', 'мобильные универсалы', 'мобильный универсал' => 250,
default => 0
},
'хакасия', 'иркутск' => match (mb_strtolower($work)) {
'cashiers', 'cashier', 'кассиры', 'кассир' => 181.82,
'displayers', 'displayer', 'выкладчики', 'выкладчик' => 181.82,
'gastronomes', 'gastronome', 'гастрономы', 'гастроном' => 181.82,
'brigadiers', 'brigadier', 'бригадиры', 'бригадир' => 250,
'loaders', 'loader', 'грузчики', 'грузчик' => 168.18,
'loaders_mobile', 'loader_mobile', 'мобильные грузчики', 'мобильный грузчик' => 250,
'universals_mobile', 'universal_mobile', 'мобильные универсалы', 'мобильный универсал' => 250,
default => 0
},
default => 0
},
default => 0
};
}
/**
* Bonus on task
*
* @param int $rating Rating of the task from the market
*
* @return int Bonus (rubles)
*/
public static function bonus(int $rating): int
{
return match ($rating) {
5 => 100,
default => 0
};
}
/**
* Penalty on task
*
* @param int $rating Rating of the task from the market
*
* @return int|null Penalty (rubles) (null - all payment)
*/
public static function penalty(int $rating): ?int
{
return match ($rating) {
3 => -100,
2 => -500,
1 => null,
default => 0
};
}
}

View File

@@ -14,8 +14,9 @@ use mirzaev\arangodb\collection,
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Встроенные библиотеки
use exception;
// System libraries
use datetime,
exception;
/**
* Модель заданий
@@ -40,7 +41,7 @@ final class task extends core
/**
* Create task in ArangoDB
*
* @param ?string $date
* @param string|int|null $date
* @param ?string $worker
* @param ?string $work
* @param ?string $start
@@ -51,22 +52,24 @@ final class task extends core
* @param bool $hided
* @param bool $problematic
* @param bool $completed
* @param ?string $commentary
* @param array $errors
*
* @return ?string Identificator of instance of ArangoDB
*/
public static function create(
?string $date = null,
string|int|null $date = null,
?string $worker = null,
?string $work = null,
?string $start = null,
?string $end = null,
?string $market = null,
bool $confirmed = false,
bool $published = true,
bool $published = false,
bool $hided = false,
bool $problematic = false,
bool $completed = false,
?string $commentary = null,
array &$errors = []
): ?string {
try {
@@ -90,6 +93,7 @@ final class task extends core
'hided' => $hided,
'problematic' => $problematic,
'completed' => $completed,
'commentary' => $commentary,
]);
} else throw new exception('Не удалось инициализировать коллекции');
} catch (exception $e) {
@@ -145,8 +149,8 @@ final class task extends core
<<<AQL
FOR task IN %s
%s
LET worker = (FOR worker in %s FILTER worker._key LIKE task.worker SORT worker.created DESC, worker._key DESC LIMIT 1 RETURN worker)[0]
LET market = (FOR market in %s FILTER market._key LIKE task.market SORT market.created DESC, market._key DESC LIMIT 1 RETURN market)[0]
LET worker = (FOR worker in %s FILTER worker.id != null && worker.id LIKE task.worker SORT worker.created DESC, worker.id DESC LIMIT 1 RETURN worker)[0]
LET market = (FOR market in %s FILTER market.id != null && market.id LIKE task.market SORT market.created DESC, market.id DESC LIMIT 1 RETURN market)[0]
%s
SORT %s
LIMIT %d, %d
@@ -173,11 +177,119 @@ final class task extends core
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
var_dump($errors);
}
// Exit (fail)
return [];
}
/**
* Посчитать количество часов работы
*
* @param string $start Начало работы (H:i)
* @param string $end Конец работы (H:i)
* @param array $errors Errors registry
*
* @return ?float Количество часов, если удалось расчитать
*/
public static function hours(string $start, string $end, array &$errors = []): ?float
{
try {
if (
!empty($start = datetime::createFromFormat('H:i', (string) $start)) && $start instanceof datetime
&& !empty($end = datetime::createFromFormat('H:i', (string) $end)) && $end instanceof datetime
) {
// Инициализированы $start и $end
// Расчёт часов работы
$hours = (float) $start->diff($end)->format('%R%H.%i');
if ($hours < 0) $hours += 24;
if ($hours >= 6.5 && $hours < 9) $hours -= 0.5;
else if ($hours >= 9 && $hours < 12.5) $hours -= 1;
else if ($hours >= 12.5) $hours -= 1.5;
// Выход (успех)
return $hours;
}
} catch (exception $e) {
// Write to the errors registry
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Выход (провал)
return null;
}
/**
* Generate work type label in Russian
*
* @param string $work Type of work
*
* @return string
*/
public static function label(string $work): string
{
return match (mb_strtolower($work)) {
'cashiers', 'cashier', 'кассиры', 'кассир' => 'Кассир',
'displayers', 'displayer', 'выкладчики', 'выкладчик' => 'Выкладчик',
'gastronomes', 'gastronome', 'гастрономы', 'гастроном' => 'Гастроном',
'brigadiers', 'brigadier', 'бригадиры', 'бригадир' => 'Бригадир',
'loaders', 'loader', 'грузчики', 'грузчик' => 'Грузчик',
'loaders_mobile', 'loader_mobile', 'мобильные грузчики', 'мобильный грузчик' => 'Мобильный грузчик',
'universals_mobile', 'universal_mobile', 'мобильные универсалы', 'мобильный универсал' => 'Мобильный универсал',
default => $work
};
}
/**
* Create a transaction for work on a task
*
* @param string $task
* @param string $worker
* @param int $amount
* @param array $errors
*
* @return ?string Identificator of instance of ArangoDB
*/
public static function transaction(
string $task,
string $worker,
int $amount = 0,
array &$errors = []
): ?string {
try {
if (
collection::init(static::$arangodb->session, self::COLLECTION)
&& collection::init(static::$arangodb->session, worker::COLLECTION)
&& collection::init(static::$arangodb->session, 'transaction', true)
) {
// Инициализированы коллекции
// Запись документа в базу данны и возврат (успех)
return document::write(static::$arangodb->session, 'transaction', [
'_from' => $task,
'_to' => $worker,
'amount' => $amount,
'processed' => 0,
]);
} else throw new exception('Не удалось инициализировать коллекции');
} catch (exception $e) {
// Write to the errors registry
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
}

View File

@@ -17,34 +17,34 @@ section.panel.list.medium {
width: 80%;
}
section.panel.list > :is(form, search).row.menu {
margin-bottom: 10px;%s"
section.panel.list> :is(form, search).row.menu {
margin-bottom: 10px;
transition: 0s;
}
section.panel.list > :is(form, search).row.menu > label {
section.panel.list> :is(form, search).row.menu>label {
height: max-content;
min-height: 30px;
display: flex;
}
section.panel.list > :is(form, search).row.menu > label:not(.solid) {
section.panel.list> :is(form, search).row.menu>label:not(.solid) {
gap: 15px;
}
section.panel.list > :is(form, search).row.menu.wide > label {
section.panel.list> :is(form, search).row.menu.wide>label {
height: 36px;
}
section.panel.list > :is(form, search).row.menu.separated {
section.panel.list> :is(form, search).row.menu.separated {
margin-bottom: 20px;
}
div#popup > section.list > div.row.endless {
div#popup>section.list>div.row.endless {
height: auto;
}
section.panel.list > :is(form, search).row.menu > label > button {
section.panel.list> :is(form, search).row.menu>label>button {
position: relative;
display: flex;
justify-content: center;
@@ -52,14 +52,11 @@ section.panel.list > :is(form, search).row.menu > label > button {
height: 30px;
}
section.panel.list > :is(form, search).row.menu > label > button.separated {
section.panel.list> :is(form, search).row.menu>label>button.separated {
margin-left: 7px;
}
section.panel.list
> :is(form, search).row.menu
> label
> button.separated:before {
section.panel.list> :is(form, search).row.menu>label>button.separated:before {
content: "";
left: -12px;
position: absolute;
@@ -68,54 +65,47 @@ section.panel.list
border-left: 2px solid var(--earth-above);
}
section.panel.list > :is(form, search).row.menu.stretched > label > button,
section.panel.list
> :is(form, search).row.menu.stretched
> label
> input[type="search"] {
section.panel.list> :is(form, search).row.menu.stretched>label>button,
section.panel.list> :is(form, search).row.menu.stretched>label>input[type="search"] {
flex-grow: 1;
}
section.panel.list > :is(form, search).row.menu.stretched > label > button {
section.panel.list> :is(form, search).row.menu.stretched>label>button {
max-width: 250px;
}
section.panel.list > :is(form, search).row.menu > label > input {
section.panel.list> :is(form, search).row.menu>label>input {
padding: 0 10px;
}
section.panel.list > :is(form, search).row.menu > label > input:not(.merged) {
section.panel.list> :is(form, search).row.menu>label>input:not(.merged) {
border-radius: 3px;
}
section.panel.list > :is(form, search).row.menu > label > input[type="date"] {
section.panel.list> :is(form, search).row.menu>label>input[type="date"] {
width: 115px;
flex-shrink: 0;
}
section.panel.list
> :is(form, search).row.menu
> label
> input[type="search"]
+ button {
section.panel.list> :is(form, search).row.menu>label>input[type="search"]+button {
height: 100%;
padding: 0 30px;
flex-grow: 0;
}
section.panel.list > div#title {
section.panel.list>div#title {
margin-top: 20px;
height: 50px;
background-color: var(--background-below-6);
}
section.panel.list > div#title > span {
section.panel.list>div#title>span {
font-weight: unset;
font-size: unset;
color: unset;
}
section.panel.list > div.row {
section.panel.list>div.row {
--gap: 12px;
--background: var(--cloud);
position: relative;
@@ -128,11 +118,11 @@ section.panel.list > div.row {
border-radius: 0px;
}
section.panel.list > div.row:not(:nth-of-type(1)) {
section.panel.list>div.row:not(:nth-of-type(1)) {
background-color: var(--background);
}
section.panel.list > div.row:not(:nth-of-type(1)) > span {
section.panel.list>div.row:not(:nth-of-type(1))>span {
height: 100%;
line-height: 2.2;
padding: 0;
@@ -142,7 +132,7 @@ section.panel.list > div.row:not(:nth-of-type(1)) > span {
-moz-box-shadow: var(--box-shadow);
}
section.panel.list > div.row:not(:nth-of-type(1)):is(:hover, :focus) {
section.panel.list>div.row:not(:nth-of-type(1)):is(:hover, :focus) {
--padding-left: 24px;
--padding-right: 24px;
left: -12px;
@@ -151,23 +141,23 @@ section.panel.list > div.row:not(:nth-of-type(1)):is(:hover, :focus) {
transition: 0s;
}
section.panel.list > div.row:first-of-type {
section.panel.list>div.row:first-of-type {
border-radius: 3px 3px 0 0;
}
section.panel.list > div.row:last-of-type {
section.panel.list>div.row:last-of-type {
border-radius: 0 0 3px 3px;
}
section.panel.list > div.row:is(:hover, :focus) * {
section.panel.list>div.row:is(:hover, :focus) * {
transition: unset;
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1) {
section.panel.list>div.row:not(:nth-of-type(1)):nth-child(2n + 1) {
--background: var(--cloud-above);
}
section.panel.list > div.row[data-selected="true"]:before {
section.panel.list>div.row[data-selected="true"]:before {
left: -25px;
top: 0.08rem;
position: absolute;
@@ -186,7 +176,7 @@ section.panel.list > div.row[data-selected="true"]:before {
color: var(--interface-brown);
}
section.panel.list > div.row[data-selected="true"]:after {
section.panel.list>div.row[data-selected="true"]:after {
right: -25px;
bottom: 0.08rem;
rotate: 180deg;
@@ -206,85 +196,14 @@ section.panel.list > div.row[data-selected="true"]:after {
color: var(--interface-brown);
}
section.panel.list > div.row:not(:nth-of-type(1)).confirmed {
--background: var(--grass);
}
section.panel.list
> div.row:not(:nth-of-type(1)):nth-child(2n + 1).confirmed {
--background: var(--grass-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).published {
--background: var(--river);
}
section.panel.list
> div.row:not(:nth-of-type(1)):nth-child(2n + 1).published {
--background: var(--river-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).confirmed.published:not(.problematic) {
--background: var(--sea);
}
section.panel.list
> div.row:not(:nth-of-type(1)).confirmed.published:not(.problematic):nth-child(2n + 1) {
--background: var(--sea-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).problematic {
--background: var(--clay);
}
section.panel.list
> div.row:not(:nth-of-type(1)):nth-child(2n + 1).problematic {
--background: var(--clay-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).coming {
--background: var(--magma);
}
section.panel.list
> div.row:not(:nth-of-type(1)):nth-child(2n + 1).coming {
--background: var(--magma-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).completed:not(.problematic) {
--background: var(--sand);
}
section.panel.list
> div.row:not(:nth-of-type(1)):nth-child(2n + 1).completed:not(.problematic) {
--background: var(--sand-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).passed {
filter: brightness(0.8);
}
section.panel.list > div.row:not(:nth-of-type(1)).hided * {
filter: blur(1px);
opacity: 0.3;
}
section.panel.list > div.row:not(:nth-of-type(1)).hided:is(:hover, :focus) * {
filter: unset;
opacity: unset;
}
section.panel.list > div.row.reinitialized {
section.panel.list>div.row.reinitialized {
animation-duration: 3s;
animation-name: row-reinitialized;
animation-timing-function: ease-in;
}
section.panel.list
> div.row:not(
:nth-of-type(1),
[data-selected="true"]
).reinitializable:before {
section.panel.list>div.row:not(:nth-of-type(1),
[data-selected="true"]).reinitializable:before {
content: attr(data-counter);
position: absolute;
left: -95px;
@@ -297,16 +216,13 @@ section.panel.list
color: var(--earth-text);
}
section.panel.list
> div.row:not(:nth-of-type(1), [data-selected="true"]).reinitializable:is(
:hover,
:focus
):before {
section.panel.list>div.row:not(:nth-of-type(1), [data-selected="true"]).reinitializable:is(:hover,
:focus):before {
content: attr(id);
color: var(--earth-text-important-below);
}
section.panel.list > div.row > span {
section.panel.list>div.row>span {
position: relative;
margin: auto 0;
padding: 8px 0;
@@ -314,52 +230,52 @@ section.panel.list > div.row > span {
transition: 0s;
}
section.panel.list > div.row:is(:hover, :focus) > span {
section.panel.list>div.row:is(:hover, :focus)>span {
transition: 0s;
}
section.panel.list > div.row > span:not(:first-child) {
section.panel.list>div.row>span:not(:first-child) {
--padding-left: calc(var(--gap) / 2);
}
section.panel.list > div.row > span:not(:last-child) {
section.panel.list>div.row>span:not(:last-child) {
--padding-right: calc(var(--gap) / 2);
}
section.panel.list > div.row > span:first-child {
section.panel.list>div.row>span:first-child {
border-radius: 3px 0 0 3px;
}
section.panel.list > div.row > span:last-child {
section.panel.list>div.row>span:last-child {
border-radius: 0 3px 3px 0;
}
section.panel.list > div.row:not(:hover, :focus) > span:first-child {
section.panel.list>div.row:not(:hover, :focus)>span:first-child {
--padding-left: var(--gap, 12px);
}
section.panel.list > div.row:not(:hover, :focus) > span:last-child {
section.panel.list>div.row:not(:hover, :focus)>span:last-child {
--padding-right: var(--gap, 12px);
}
section.panel.list > div.row:nth-of-type(1) > span {
section.panel.list>div.row:nth-of-type(1)>span {
text-align: center;
}
section.panel.list > div.row:nth-of-type(1) > span > i {
section.panel.list>div.row:nth-of-type(1)>span>i {
position: relative;
margin: auto;
}
section.panel.list > div.row > span[onclick] {
section.panel.list>div.row>span[onclick] {
cursor: pointer;
}
section.panel.list > div.row > span.field {
section.panel.list>div.row>span.field {
cursor: text;
}
section.panel.list > div.row:not(:nth-of-type(1)) > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row:not(:nth-of-type(1))>span:is(.important, .interactive:is(:hover, :focus)) {
--margin: calc(var(--gap) / 2);
--border-left: calc(var(--padding-left, var(--margin, 0px)) * -1);
--border-right: var(--padding-right, var(--margin, 0px));
@@ -367,54 +283,196 @@ section.panel.list > div.row:not(:nth-of-type(1)) > span:is(.important, .interac
--box-shadow: var(--border-left, 0) 0 0 0 var(--box-shadow-color, var(--background)), var(--border-right, 0) 0 0 0 var(--box-shadow-color, var(--background));
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1) > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).confirmed {
--background: var(--grass);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).confirmed {
--background: var(--grass-above);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).published {
--background: var(--river);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).published {
--background: var(--river-above);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).confirmed.published:not(.problematic) {
--background: var(--sea);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).confirmed.published:not(.problematic):nth-child(2n + 1) {
--background: var(--sea-above);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).problematic {
--background: var(--clay);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).problematic {
--background: var(--clay-above);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).coming {
--background: var(--magma);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).coming {
--background: var(--magma-above);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).completed:not(.problematic) {
--background: var(--sand);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).completed:not(.problematic) {
--background: var(--sand-above);
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).passed {
filter: brightness(0.8);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)).banned {
--background: var(--clay);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)).banned a {
--color: var(--clay-text);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)).banned a:is(:hover, :focus) {
--color: var(--clay-text-above);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)).banned a:active {
--color: var(--clay-text-below);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)):nth-child(2n + 1).banned {
--background: var(--clay-above);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)):nth-child(2n + 1).banned a {
--color: var(--clay-text);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)):nth-child(2n + 1).banned a:is(:hover, :focus) {
--color: var(--clay-text-above);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)):nth-child(2n + 1).banned a:active {
--color: var(--clay-text-below);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)).fired {
--background: var(--magma);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)).fired a:is(:hover, :focus) {
--color: var(--magma-text-above);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)).fired a:active {
--color: var(--magma-text-below);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)).fired a {
--color: var(--magma-text);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)):nth-child(2n + 1).fired {
--background: var(--magma-above);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)):nth-child(2n + 1).fired a {
--color: var(--magma-text);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)):nth-child(2n + 1).fired a:is(:hover, :focus) {
--color: var(--magma-text-above);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)):nth-child(2n + 1).fired a:active {
--color: var(--magma-text-below);
}
section.panel.list>div.row:not(:nth-of-type(1)).hided * {
filter: blur(1px);
opacity: 0.3;
}
section.panel.list>div.row:not(:nth-of-type(1)).hided:is(:hover, :focus) * {
filter: unset;
opacity: unset;
}
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1)>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--cloud-rainy-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).published > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).published>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--river-deep);
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1).published > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).published>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--river-deep-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).confirmed > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).confirmed>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--grass-dense);
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1).confirmed > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).confirmed>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--grass-dense-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).confirmed.published:not(.problematic) > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).confirmed.published:not(.problematic)>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--sea-deep);
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1).confirmed.published:not(.problematic) > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).confirmed.published:not(.problematic)>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--sea-deep-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).problematic > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).problematic>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--clay-important);
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1).problematic > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).problematic>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--clay-important-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).coming > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).coming>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--magma-important);
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1).coming > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).coming>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--magma-important-above);
}
section.panel.list > div.row:not(:nth-of-type(1)).completed:not(.problematic) > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)).completed:not(.problematic)>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--sand-important);
}
section.panel.list > div.row:not(:nth-of-type(1)):nth-child(2n + 1).completed:not(.problematic) > span:is(.important, .interactive:is(:hover, :focus)) {
section.panel.list>div.row[data-row="task"]:not(:nth-of-type(1)):nth-child(2n + 1).completed:not(.problematic)>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--sand-important-above);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)).banned>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--clay-important);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)):nth-child(2n + 1).banned>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--clay-important-above);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)).fired>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--magma-important);
}
section.panel.list>div.row[data-row="worker"]:not(:nth-of-type(1)):nth-child(2n + 1).fired>span:is(.important, .interactive:is(:hover, :focus)) {
--background: var(--magma-important-above);
}

View File

@@ -259,15 +259,16 @@ button:is(.transparent, .transparent:is(:hover, :focus), .transparent:active) {
}
a {
color: var(--link);
--color: var(--link);
color: var(--color);
}
a:is(:hover, :focus) {
color: var(--link-hover);
--color: var(--link-hover);
}
a:active {
color: var(--link-active);
--color: var(--link-active);
transition: unset;
}
@@ -302,12 +303,14 @@ label * {
}
textarea {
--padding-x: 12px;
--padding-y: 8px;
width: 100%;
min-width: calc(100% - 24px);
min-height: 120px;
max-width: calc(100% - 24px);
max-height: 300px;
padding: 8px 12px;
padding: var(--padding-y, 8px) var(--padding-x, 12px);
font-size: smaller;
overflow: hidden;
border-radius: 3px;

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ section#tasks.panel.list
> span:is(
[data-column="worker"],
[data-column="name"],
[data-column="task"],
[data-column="work"],
[data-column="address"],
[data-column="type"],
[data-column="tax"],
@@ -34,14 +34,26 @@ section#tasks.panel.list > div.row > span[data-column="date"] {
font-weight: bold;
}
section#tasks.panel.list > div.row > span:is([data-column="worker"], [data-column="market"]) {
min-width: 102px;
width: 102px;
section#tasks.panel.list > div.row > span:is([data-column="market"], [data-column="worker"]) {
font-weight: bold;
}
section#tasks.panel.list > div.row:nth-of-type(1) > span[data-column="market"] {
text-align: center;
}
section#tasks.panel.list > div.row:nth-of-type(1) > span[data-column="worker"] {
margin-top: 8px;
margin-top: 8px;
}
section#tasks.panel.list > div.row > span[data-column="market"] {
min-width: 67px;
width: 67px;
text-align: right;
}
section#tasks.panel.list > div.row > span[data-column="worker"] {
min-width: 67px;
width: 67px;
}
section#tasks.panel.list > div.row > span[data-column="name"] {
@@ -49,14 +61,14 @@ section#tasks.panel.list > div.row > span[data-column="name"] {
width: 130px;
}
section#tasks.panel.list > div.row > span[data-column="task"] {
section#tasks.panel.list > div.row > span[data-column="work"] {
min-width: 100px;
width: 100px;
}
section#tasks.panel.list
> div.row:not(:nth-of-type(1))
> span[data-column="task"] {
> span[data-column="work"] {
text-align: right;
}

View File

@@ -15,7 +15,7 @@ section#workers.panel.list
[data-column="worker"],
[data-column="name"],
[data-column="number"],
[data-column="mail"],
[data-column="work"],
[data-column="passport"],
[data-column="address"],
[data-column="tax"],
@@ -32,18 +32,26 @@ section#workers.panel.list
}
section#workers.panel.list > div.row > span:is([data-column="account"], [data-column="worker"]) {
min-width: 102px;
width: 102px;
font-weight: bold;
text-align: center;
}
section#workers.panel.list > div.row:nth-of-type(1) > span[data-column="account"] {
margin-top: 6px;
text-align: center;
}
section#workers.panel.list > div.row:nth-of-type(1) > span[data-column="worker"] {
margin-top: 8px;
margin-top: 8px;
}
section#workers.panel.list > div.row > span[data-column="account"] {
min-width: 102px;
width: 102px;
text-align: center;
}
section#workers.panel.list > div.row > span[data-column="worker"] {
min-width: 67px;
width: 67px;
}
section#workers.panel.list > div.row > span[data-column="name"] {

View File

@@ -38,18 +38,25 @@ div#popup>section.stretched {
flex-grow: unset;
}
div#popup>section.calculated {
width: calc(var(--calculated-width) - var(--padding-horizontal, 0px) * 2);
}
div#popup>section.list {
max-width: max(70vw, 1300px);
max-height: max(62vh, 600px);
display: flex;
flex-direction: column;
padding: 30px;
overflow-y: scroll;
border-radius: 3px;
}
div#popup>section.list.extensive {
max-width: unset;
max-height: unset;
}
div#popup>section.list>h3 {
margin-top: 4px;
margin-bottom: 22px;
@@ -65,17 +72,30 @@ div#popup>section.list h4 {
}
div#popup>section.list>section.main {
--gap: 15px;
display: flex;
gap: 15px;
flex-flow: row wrap;
justify-content: space-between;
gap: var(--gap, 15px);
}
div#popup>section.list>section.main>div.column {
flex-grow: 1;
display: flex;
flex-grow: 1;
flex-direction: column;
gap: 8px;
}
div#popup>section.list>section.main.flow>div.column:not(:only-child) {
width: 300px;
}
div#popup>section.list>section.main>div.column:not(:only-child)[data-column="buttons"]:last-of-type {
margin-left: auto;
justify-content: end;
}
div#popup>section.list>section.main>div.column:only-child {
width: 100%;
}
@@ -140,7 +160,21 @@ div#popup>section.list>section.main>div.column> :is(div, select).row.buttons {
div#popup>section.list>section.main>div.column> :is(div, select).row:not(.buttons, .stretchable, .endless),
div#popup>section.list>section.main>div.column> :is(div, select).row:not(.buttons, .stretchable, .endless)>button {
height: 29px;
--height: 29px;
height: var(--height, 29px);
}
div#popup>section.list>section.main>div.column> :is(div, select).row:not(.buttons .endless).stretchable,
div#popup>section.list>section.main>div.column> :is(div, select).row:not(.buttons, .endless).stretchable>button {
--height: 29px;
height: max(var(--height, 29px), fit-content);
}
div#popup>section.list>section.main>div.column> :is(div, select).row:not(.buttons .endless).stretchable>textarea {
/* min-height: calc(var(--height, 29px) - var(--padding-y, 8ox) * 2); */
min-height: 1rem;
max-height: 3rem;
height: 1rem;
}
div#popup>section.list>section.main>div.column>:is(div, section).row:not(.merged)+:is(div, section).row.merged {
@@ -262,3 +296,10 @@ div#popup>section.list>section.main>div.column>section.row.message>textarea+butt
div#popup>section.list.errors>section.body>dl>dd {
margin-left: 20px;
}
div#popup > section.list .separator {
border-top: 2px solid var(--separator, var(--cloud));
padding-top: 10px;
margin-top: 10px;
margin-bottom: 10px;
}

View File

@@ -83,10 +83,9 @@
--sand-important: #d7c06c;
--sand-important-below: #dfc79a;
--magma-text-above: ;
--magma-text: ;
--magma-text-below: ;
--magma-text-below-1: ;
--magma-text-above: #111;
--magma-text: #5e1a1a;
--magma-text-below: #826d1c;
--magma-above: #ffd325;
--magma: #e6bf26;
--magma-below: ;

View File

@@ -40,6 +40,7 @@ $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('/worker/$id/hire', 'worker', 'hire', 'POST');
$router->write('/workers', 'worker', 'index', 'GET');
$router->write('/workers', 'worker', 'index', 'POST');
$router->write('/workers/read', 'worker', 'read', 'POST');
@@ -68,6 +69,8 @@ $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('/$id/ban', 'account', 'ban', 'POST');
$router->write('/$id/unban', 'account', 'unban', 'POST');
$router->write('/session/worker', 'session', 'worker', 'POST');
$router->write('/session/write', 'session', 'write', 'POST');
$router->write('/session/read', 'session', 'read', 'POST');
@@ -79,6 +82,7 @@ $router->write('/session/invite', 'session', 'invite', 'POST');
$router->write('/tasks/create', 'task', 'create', 'POST');
$router->write('/tasks/read', 'task', 'read', 'POST');
$router->write('/works/list', 'work', 'datalist', 'POST');
$router->write('/tasks/works', 'task', 'works', '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');
@@ -88,7 +92,6 @@ $router->write('/task/$task/hide', 'task', 'hide', 'POST');
$router->write('/task/$task/remove', 'task', 'remove', 'POST');
$router->write('/task/$task/work', 'task', 'work', 'POST');
$router->write('/task/$task/date', 'task', 'date', 'POST');
$router->write('/task/$task/works', 'task', 'works', 'POST');
$router->write('/task/$task/description', 'task', 'description', 'POST');
$router->write('/task/$task/commentary', 'task', 'commentary', 'POST');
$router->write('/task/$task/worker/update', 'task', 'update', 'POST');
@@ -98,6 +101,8 @@ $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');
$router->write('/payments/workers', 'payments', 'workers', 'POST');
$router->write('/payments/markets', 'payments', 'markets', 'POST');
// Инициализация ядра
$core = new core(namespace: __NAMESPACE__, router: $router, controller: new controller(false), model: new model(false));

View File

@@ -338,7 +338,7 @@ if (typeof window.administrators !== "function") {
// Инициализация оболочки всплывающего окна
const popup = document.createElement("section");
popup.classList.add("list", "small");
popup.classList.add("list", "extensive", "small");
// Инициализация заголовка всплывающего окна
const title = document.createElement("h3");
@@ -1056,7 +1056,7 @@ if (typeof window.administrators !== "function") {
);
/**
* Сгенерировать окно с формой создания аккаунт
* Сгенерировать окно с формой создания аккаунта
*
* @param {HTMLElement} row Строка
*
@@ -1069,7 +1069,7 @@ if (typeof window.administrators !== "function") {
// Инициализация оболочки всплывающего окна
const popup = document.createElement("section");
popup.classList.add("list", "small");
popup.classList.add("list", "extensive", "small");
// Инициализация заголовка всплывающего окна
const title = document.createElement("h3");

View File

@@ -13,7 +13,7 @@ if (typeof window.buffer !== "function") {
*
* @return {Promise}
*/
static write(name, value) {
static async write(name, value) {
if (
typeof core === "function" && typeof name === "string" &&
(typeof value === "string" || typeof value === "number")
@@ -36,7 +36,7 @@ if (typeof window.buffer !== "function") {
});
// Запрос к серверу для записи в сессию (базу данных)
return fetch("/session/write", {
return await fetch("/session/write", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",

View File

@@ -577,7 +577,7 @@ if (typeof window.chat !== "function") {
* @param {string} chat Тип чата (market, worker, both)
* @param {bool} scroll Прокрутить до последнего сообщения?
* @param {bool} sound Проигрывать звук уведомления о новом сообщении?
* @param {string} chat Тип чата (market, worker)
* @param {bool} force Принудительное выполнение (используется в damper())
*
* @return {void}
*/
@@ -601,7 +601,7 @@ if (typeof window.chat !== "function") {
* @param {string} chat Тип чата (market, worker, both)
* @param {bool} scroll Прокрутить до последнего сообщения?
* @param {bool} sound Проигрывать звук уведомления о новом сообщении?
* @param {bool} force Принудительное выполнение (используется в damper()
* @param {bool} force Принудительное выполнение (используется в damper())
*
* @return {void}
*/

View File

@@ -20,13 +20,13 @@ function damper(func, timeout = 300, force) {
if (typeof force === 'number' && args[force]) {
// Принудительное выполнение (игнорировать таймер)
func.apply(this, args);
return func.apply(this, args);
} else {
// Обычное выполнение
// Вызов функции (вход в рекурсию)
timer = setTimeout(() => {
func.apply(this, args);
return func.apply(this, args);
}, timeout);
}
};

View File

@@ -119,6 +119,7 @@ if (typeof window.markets !== "function") {
* @param {HTMLElement} market_name_last Отчество представителя магазина <input>
* @param {HTMLElement} market_number SIM-номер представителя магазина <input>
* @param {HTMLElement} market_mail Почта представителя магазина <input>
* @param {HTMLElement} market_id Идентификатор магазина <input>
* @param {HTMLElement} market_type Тип магазина <select>
* @param {HTMLElement} market_city Город магазина <select>
* @param {HTMLElement} market_district Регион магазина <input>
@@ -140,6 +141,7 @@ if (typeof window.markets !== "function") {
market_name_last,
market_number,
market_mail,
market_id,
market_type,
market_city,
market_district,
@@ -158,6 +160,7 @@ if (typeof window.markets !== "function") {
market_name_last.setAttribute("readonly", true);
market_number.setAttribute("readonly", true);
market_mail.setAttribute("readonly", true);
market_id.setAttribute("readonly", true);
market_type.setAttribute("readonly", true);
market_city.setAttribute("readonly", true);
market_district.setAttribute("readonly", true);
@@ -181,6 +184,7 @@ if (typeof window.markets !== "function") {
market_name_last,
market_number,
market_mail,
market_id,
market_type,
market_city,
market_district,
@@ -204,6 +208,7 @@ if (typeof window.markets !== "function") {
* @param {HTMLElement} market_name_last Отчество представителя магазина <input>
* @param {HTMLElement} market_number SIM-номер представителя магазина <input>
* @param {HTMLElement} market_mail Почта представителя магазина <input>
* @param {HTMLElement} market_id Идентификатор магазина <input>
* @param {HTMLElement} market_type Тип магазина <select>
* @param {HTMLElement} market_city Город магазина <select>
* @param {HTMLElement} market_district Регион магазина <input>
@@ -226,6 +231,7 @@ if (typeof window.markets !== "function") {
market_name_last,
market_number,
market_mail,
market_id,
market_type,
market_city,
market_district,
@@ -246,6 +252,7 @@ if (typeof window.markets !== "function") {
market_name_last.removeAttribute("readonly");
market_number.removeAttribute("readonly");
market_mail.removeAttribute("readonly");
market_id.removeAttribute("readonly");
market_type.removeAttribute("readonly");
market_city.removeAttribute("readonly");
market_district.removeAttribute("readonly");
@@ -275,7 +282,7 @@ if (typeof window.markets !== "function") {
"Content-Type": "application/x-www-form-urlencoded",
},
body:
`market_name_first=${market_name_first.value}&market_name_second=${market_name_second.value}&market_name_last=${market_name_last.value}&market_number=${market_number.mask.unmaskedValue}&market_mail=${market_mail.value}&market_type=${market_type.value}&market_city=${market_city.value}&market_district=${market_district.value}&market_address=${market_address.value}&account_name_first=${account_name_first.value}&account_name_second=${account_name_second.value}&account_name_last=${account_name_last.value}&account_number=${account_number.unmaskedValue}&account_mail=${account_mail.value}&account_password=${account_password.value}&account_commentary=${account_commentary.value}`,
`market_name_first=${market_name_first.value}&market_name_second=${market_name_second.value}&market_name_last=${market_name_last.value}&market_number=${market_number.mask.unmaskedValue}&market_mail=${market_mail.value}&market_id=${market_id.value}&market_type=${market_type.value}&market_city=${market_city.value}&market_district=${market_district.value}&market_address=${market_address.value}&account_name_first=${account_name_first.value}&account_name_second=${account_name_second.value}&account_name_last=${account_name_last.value}&account_number=${account_number.mask.unmaskedValue}&account_mail=${account_mail.value}&account_password=${account_password.value}&account_commentary=${account_commentary.value}`,
})
.then((response) => response.json())
.then((data) => {
@@ -332,7 +339,7 @@ if (typeof window.markets !== "function") {
// Инициализация всплывающего окна
const popup = document.createElement("section");
popup.classList.add("list", "medium");
popup.classList.add("list", "extensive", "medium");
// Инициализация заголовка всплывающего окна
const title = document.createElement("h3");
@@ -513,6 +520,29 @@ if (typeof window.markets !== "function") {
market_mail_input.setAttribute("title", "Почта представителя магазина");
market_mail_input.setAttribute("placeholder", "name@server");
// Инициализация строки
const market_id = document.createElement("div");
market_id.classList.add("row", "merged");
// Инициализация оболочки для строки
const market_id_label = document.createElement("label");
market_id_label.setAttribute("id", "market_id");
// Инициализация заголовка для поля ввода
const market_id_title = document.createElement("b");
market_id_title.classList.add("separated", "right", "unselectable");
market_id_title.innerText = "Идентификатор:";
// Инициализация поля ввода
const market_id_input = document.createElement("input");
market_id_input.classList.add("large");
market_id_input.setAttribute("name", "id");
market_id_input.setAttribute("type", "text");
market_id_input.setAttribute("minlength", "1");
market_id_input.setAttribute("maxlength", "3");
market_id_input.setAttribute("title", "Идентификатор магазина");
market_id_input.setAttribute("placeholder", "000");
// Инициализация строки
const market_type = document.createElement("div");
market_type.classList.add("row", "divided", "merged");
@@ -958,6 +988,11 @@ if (typeof window.markets !== "function") {
market_mail.appendChild(market_mail_label);
market.appendChild(market_mail);
market_id_label.appendChild(market_id_title);
market_id_label.appendChild(market_id_input);
market_id.appendChild(market_id_label);
market.appendChild(market_id);
market_type_label.appendChild(market_type_title);
market_type_select.appendChild(market_type_option_1);
market_type_select.appendChild(market_type_option_2);
@@ -1159,7 +1194,7 @@ if (typeof window.markets !== "function") {
this.body.wrap.remove();
// Удаление статуса активной строки
row.removeAttribute("data-selected");
// row.removeAttribute("data-selected"); // Это окно создания строки, её ещё не существует
// Деинициализация быстрых действий по кнопкам
document.removeEventListener("keydown", this.buttons);
@@ -1739,7 +1774,7 @@ if (typeof window.markets !== "function") {
// Инициализация всплывающего окна
const popup = document.createElement("section");
popup.classList.add("list", "medium");
popup.classList.add("list", "extensive", "medium");
// Инициализация заголовка всплывающего окна
const title = document.createElement("h3");

View File

@@ -338,7 +338,7 @@ if (typeof window.operators !== "function") {
// Инициализация оболочки всплывающего окна
const popup = document.createElement("section");
popup.classList.add("list", "small");
popup.classList.add("list", "extensive", "small");
// Инициализация заголовка всплывающего окна
const title = document.createElement("h3");
@@ -1069,7 +1069,7 @@ if (typeof window.operators !== "function") {
// Инициализация оболочки всплывающего окна
const popup = document.createElement("section");
popup.classList.add("list", "small");
popup.classList.add("list", "extensive", "small");
// Инициализация заголовка всплывающего окна
const title = document.createElement("h3");

View File

@@ -0,0 +1,274 @@
"use strict";
if (typeof window.payments !== "function") {
// Not initialized
// Initialize of the class in global namespace
window.payments = class payments {
/**
* Сотрудники
*
* Сгенерировать и скачать excel-документ с зарплатами сотрудников за выбранный период (cookies или session storage)
*
* @return {void} Вызывает функцию скачивания в браузере
*/
static workers = damper(() => {
// Инициализация оболочки фильтров
const filters = document.getElementById("filters").children[0];
tasks
.filter("from", new Date(filters.children[0].value) / 1000, null, true)
.then(() => {
tasks
.filter(
"to",
new Date(filters.children[1].value) / 1000,
null,
true,
)
.then(() => {
// Запрос к серверу
fetch("/payments/workers", { method: "POST" }).then(
(response) => {
if (response.ok) {
// Сервер вернул код успешного выполнения
response
.clone()
.json()
.then(
(data) => {
if (this.errors(data.errors)) {
// Сгенерированы ошибки
} else {
// Не сгенерированы ошибки (подразумевается их отсутствие)
}
},
() => {
// Инициализация имени файла
const header = response.headers.get(
"Content-Disposition",
);
if (header !== null) {
// Найден заголовок (подразумевается, что передан файл, а не случайная ошибка)
// Инициализация названия файла
const name = header.split(";")[1].split("=")[1];
// Чтение полученного файла (подразумевается ошибка при инициализации json)
response.blob().then((blob) => {
// Инициализация временного элемента для скачивания с именем файла (хак)
const element = document.createElement("a");
element.href = window.URL.createObjectURL(blob);
element.download = name;
element.style.setProperty("display", "none");
document.body.appendChild(element);
// Скачивание файла
element.click();
// Деинициализация временного элемента
element.remove();
});
}
},
);
}
},
);
});
});
}, 200);
/**
* Магазины
*
* Сгенерировать и скачать excel-документ со ... (сверкой?) за выбранный период (cookies или session storage)
*
* @return {void} Вызывает функцию скачивания в браузере
*/
static markets = damper(() => {
// Инициализация оболочки фильтров
const filters = document.getElementById("filters").children[0];
tasks
.filter("from", new Date(filters.children[0].value) / 1000, null, true)
.then(() => {
tasks
.filter(
"to",
new Date(filters.children[1].value) / 1000,
null,
true,
)
.then(() => {
// Запрос к серверу
fetch("/payments/markets", { method: "POST" }).then(
(response) => {
if (response.ok) {
// Сервер вернул код успешного выполнения
response
.clone()
.json()
.then(
(data) => {
if (this.errors(data.errors)) {
// Сгенерированы ошибки
} else {
// Не сгенерированы ошибки (подразумевается их отсутствие)
}
},
() => {
// Инициализация имени файла
const header = response.headers.get(
"Content-Disposition",
);
if (header !== null) {
// Найден заголовок (подразумевается, что передан файл, а не случайная ошибка)
// Инициализация названия файла
const name = header.split(";")[1].split("=")[1];
// Чтение полученного файла (подразумевается ошибка при инициализации json)
response.blob().then((blob) => {
// Инициализация временного элемента для скачивания с именем файла (хак)
const element = document.createElement("a");
element.href = window.URL.createObjectURL(blob);
element.download = name;
element.style.setProperty("display", "none");
document.body.appendChild(element);
// Скачивание файла
element.click();
// Деинициализация временного элемента
element.remove();
});
}
},
);
}
},
);
});
});
}, 200);
/**
* Сгенерировать HTML-элемент со списком ошибок
*
* @param {object} registry Реестр ошибок
* @param {bool} render Отобразить в окне с ошибками?
* @param {bool} clean Очистить окно с ошибками перед добавлением?
*
* @return {bool} Сгенерированы ошибки?
*
* @TODO Переделать под показ ошибок где-нибудь
*/
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("payments.initialized", {
detail: { payments: window.payments },
}),
);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
<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>
<h3 class="coal"><b>{{ message.from.id ?? 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 %}

View File

@@ -1,5 +1,5 @@
<!-- MARKET #{{ market.id.value }} -->
{% for key, data in market | filter((data, key) => key != '_key') -%}
{% for key, data in market | filter((data, key) => key != 'id' and 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>
@@ -7,10 +7,10 @@
<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="Написать">{{
<span id="{{ market.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>
<span id="{{ market.id.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,5 +1,5 @@
<!-- TASK #{{ task._key.value }} -->
{% for key, data in task | filter((data, key) => key != '_key') -%}
{% for key, data in task | filter((data, key) => key != '_key' and key != 'updates') -%}
{% if (key == 'created' or key == 'updated') %}
<span id="{{ task.id.value }}_{{ key }}"><b>{{ data.label }}:</b>{{ data.value is empty ? 'Никогда' :
data.value|date('d.m.Y H:i') }}</span>
@@ -11,3 +11,8 @@
data.value is same as(false) %}Нет{% elseif data.value is empty %}{% else %}{{ data.value }}{% endif %}</span>
{% endif %}
{% endfor %}
{% if account.type == 'administrator' or account.type == 'operator' %}
{% for key, data in task.updates %}
<span id="{{ task.id.value }}_{{ key }}"><b>{{ data.label }}:</b>{% if data.value is empty %}{% else %}{{ data.value }}{% endif %}</span>
{% endfor %}
{% endif %}

View File

@@ -1,8 +1,8 @@
<!-- TASK #{{ task._key.value }} -->
{% for key, data in task | filter((data, key) => key != '_key') -%}
{% for key, data in task | filter((data, key) => key != '_key' and key != 'updates') -%}
{% 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>
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>
@@ -11,3 +11,9 @@
data.value is same as(false) %}Нет{% elseif data.value is empty %}{% else %}{{ data.value }}{% endif %}</span>
{% endif %}
{% endfor %}
{% if account.type == 'administrator' or account.type == 'operator' %}
<h4 class="separator unselectable">Последние изменения</h4>
{% for key, data in task.updates %}
<span id="{{ task.id.value }}_{{ key }}"><b>{{ data.label }}:</b>{% if data.value is empty %}{% else %}{{ data.value }}{% endif %}</span>
{% endfor %}
{% endif %}

View File

@@ -1,21 +1,21 @@
<!-- WORKER #{{ worker.id.value }} -->
{% for key, data in worker | filter((data, key) => key != '_key') -%}
{% for key, data in worker | filter((data, key) => key != 'id' and key != '_key') -%}
{% if key == 'created' or key == 'updated' %}
<span id="{{ worker._key.value }}_{{ key }}"><b>{{ data.label }}:</b>{{ data.value is empty ? 'Никогда' :
<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' or key == 'issued' %}
<span id="{{ worker._key.value }}_{{ key }}"><b>{{ data.label }}:</b>{{ data.value|date('Y.m.d') }}</span>
<span id="{{ worker.id.value }}_{{ key }}"><b>{{ data.label }}:</b>{{ data.value|date('Y.m.d') }}</span>
{% elseif key == 'number' %}
<span id="{{ worker._key.value }}_number"><b>{{ data.label }}:</b><a href="tel:{{ data.value }}" title="Позвонить">{{
<span id="{{ worker.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>
<span id="{{ worker.id.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>
<span id="{{ worker.id.value }}_{{ key }}"><b>{{ data.label }}:</b>{{ (data.value.number ~ ' ' ~ data.value.address)|trim }}</span>
{% else %}
<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>
<span id="{{ worker.id.value }}_{{ key }}"><b>{{ data.label }}:</b>{% if data.value is same as(true) %}Да{% elseif data.value is same as(false) %}Нет{% elseif data.value is empty %}{% else %}{{ data.value }}{% endif %}</span>
{% endif %}
{% endfor %}

View File

@@ -2,7 +2,7 @@
{% for row in rows %}
<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="market" title="Магазин" onclick="markets.update(this.parentElement)">{{ row.market.id }}</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>

View File

@@ -10,14 +10,14 @@
}}</span>
<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>
row.worker.id }}</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 class="unselectable interactive" data-column="work" title="{{ row.task.description }}">{{ row.task.work|work
}}</span>
<span class="unselectable interactive" data-column="start">{{
row.task.generated.start }}</span>
@@ -27,7 +27,7 @@
row.task.generated.hours }}</span>
<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>
row.market.id }}</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

View File

@@ -1,38 +1,46 @@
{% if page != null %}<!-- PAGE #{{ page }} -->{% endif %}
{% for row in rows %}
<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)">{{
class="row{% if row.account.active is same as(true) %} active{% endif %}{% if row.account.banned is same as(true) %} banned{% endif %}{% if row.worker.fired is same as(true) %} fired{% 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="worker" title="Настройки сотрудника"
onclick="workers.update(this.parentElement)">{{
row.worker.id }}</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('d.m.Y') }}{% 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('d.m.Y') }}{% endif %}')">{%
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('d.m.Y') }}{% 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('d.m.Y') }}{% 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="number"><a href="tel:{{ row.worker.number }}" title="Позвонить">{{
row.worker.number|storaged_number_to_readable }}</a></span>
<span class="unselectable interactive" data-column="work">{{ row.worker.work }}</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
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('d.m.Y') ~ ', ' ~ row.worker.department.number ~ ', ' ~ row.worker.department.address)|trim(', ') }}"
onclick="navigator.clipboard.writeText('{{ (row.worker.passport ~ ', ' ~ row.worker.issued|date('d.m.Y') ~ ', ' ~ row.worker.department.number ~ ', ' ~ row.worker.department.address)|trim(', ') }}')">{{ (row.worker.passport ~ ', ' ~ row.worker.issued|date('d.m.Y') ~ ', ' ~ row.worker.department.number ~ ', ' ~
onclick="navigator.clipboard.writeText('{{ (row.worker.passport ~ ', ' ~ row.worker.issued|date('d.m.Y') ~ ', ' ~ row.worker.department.number ~ ', ' ~ row.worker.department.address)|trim(', ') }}')">{{
(row.worker.passport ~ ', ' ~ row.worker.issued|date('d.m.Y') ~ ', ' ~ 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="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>
title="{% if row.worker.requisites is not empty and row.worker.payment is not empty %}{{ row.worker.requisites }} ({{ row.worker.payment }}){% else %}{{ row.worker.requisites }} {{ 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.requisites }} {{ 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>

View File

@@ -1,5 +1,5 @@
{% for market in markets %}
<option value="{{ market.getKey() }}">{{ market.getKey() }}{% if market.name.first is not empty %} {{
<option value="{{ market.id }}">{{ market.id }}{% 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,5 +1,5 @@
{% for worker in workers %}
<option value="{{ worker.getKey() }}">{{ worker.getKey() }}{% if worker.name.first is not empty %} {{
<option value="{{ worker.id }}">{{ worker.id }}{% 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>

View File

@@ -1,3 +1,4 @@
{% if task %}
{% if exist is same as(true) %}
{% for work in works %}
<option value="{{ work }}" {% if task.work==work %} selected{% endif %}>{{ work }}</option>
@@ -14,3 +15,25 @@
{% endfor %}
</optgroup>
{% endif %}
{% elseif worker %}
{% if exist is same as(true) %}
{% for work in works %}
<option value="{{ work }}" {% if worker.work==work %} selected{% endif %}>{{ work }}</option>
{% endfor %}
{% else %}
{% if worker is not null %}
<optgroup label="Текущее">
<option value="{{ worker.work }}" selected>{{ worker.work }}</option>
</optgroup>
{% endif %}
<optgroup label="Доступное">
{% for work in works %}
<option value="{{ work }}">{{ work }}</option>
{% endfor %}
</optgroup>
{% endif %}
{% else %}
{% for work in works %}
<option value="{{ work }}">{{ work }}</option>
{% endfor %}
{% endif %}

View File

@@ -25,8 +25,8 @@
class="icon arrow right"></i></button>
<datalist id="markets">
{% for account in accounts %}
<option value="{{ account.account.getKey() }}">{{ account.account.getKey()}} {{
account.account.name.first }} {{ account.account.name.second }}</option>
<option value="{{ account.market.id }}">{{ account.market.id }} {{
account.market.name.first }} {{ account.market.name.second }}</option>
{% endfor %}
</datalist>
</label>
@@ -74,7 +74,7 @@
fields.password.button = fields.password.label.getElementsByTagName('button')[0];
// Инициализация маски идентификатора магазина
fields.market.input.mask = IMask(fields.market.input, {mask: '000000000000'});
fields.market.input.mask = IMask(fields.market.input, {mask: '000'});
/**
* Отправить входной псевдоним на сервер

View File

@@ -21,7 +21,8 @@
<button class="grass dense" onclick="tasks.create()">Создать</button>
{% endif %}
{% if account.type == 'administrator' or account.type == 'operator' %}
<button class="sea" onclick="">Выгрузка</button>
<button class="sea" onclick="payments.workers()">Сотрудники</button>
<button class="sea" onclick="payments.markets()">Магазины</button>
{% endif %}
</label>
</form>
@@ -89,7 +90,7 @@
<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="work" 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>
@@ -145,4 +146,5 @@
<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>
<script type="text/javascript" src="/js/payments.js" defer></script>
{% endblock %}

View File

@@ -58,7 +58,7 @@
<span data-column="worker" class="button" title="Сотрудник"><i class="icon bold user"></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="work" class="button">Работа</span>
<span data-column="address" class="button">Адрес</span>
<span data-column="passport" class="button">Паспорт</span>
<span data-column="tax" class="button">ИНН</span>

View File

@@ -59,7 +59,7 @@ 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',
@@ -67,7 +67,7 @@ final class templater extends controller implements ArrayAccess
)
);
// Инициализация фильтров
// Инициализация фильтра
$this->twig->addFilter(
new TwigFilter(
'storaged_requisites_to_card',
@@ -78,7 +78,7 @@ final class templater extends controller implements ArrayAccess
)
);
// Инициализация фильтров
// Инициализация фильтра
$this->twig->addFilter(
new TwigFilter(
'storaged_requisites_preview',
@@ -86,7 +86,7 @@ final class templater extends controller implements ArrayAccess
)
);
// Инициализация фильтров
// Инициализация фильтра
$this->twig->addFilter(
new TwigFilter(
'date_to_russian',
@@ -94,7 +94,7 @@ final class templater extends controller implements ArrayAccess
)
);
// Инициализация фильтров
// Инициализация фильтра
$this->twig->addFilter(
new TwigFilter(
'account_type_to_russian',
@@ -108,6 +108,14 @@ final class templater extends controller implements ArrayAccess
)
);
// Инициализация фильтра
$this->twig->addFilter(
new TwigFilter(
'work',
fn (string $work) => preg_replace('/^Мобильный/', 'Моб.', $work)
)
);
// Инициализация расширений
$this->twig->addExtension(new intl());
}