6 Commits

31 changed files with 2007 additions and 486 deletions

View File

@@ -94,7 +94,7 @@ final class account extends core
if (mb_strlen($parameters['number']) === 11) $account->number = $parameters['number'];
else throw new exception('Номер должен состоять из 11 символов');
if ($parameters['mail'] !== $account->mail) $account->mail = $parameters['mail'];
if (!empty($parameters['password']) && !sodium_crypto_pwhash_str_verify($parameters['password'], $account->password) && $password = true)
if (!empty($parameters['password']) && !@sodium_crypto_pwhash_str_verify($parameters['password'], $account->password ?? '') && $password = true)
if (mb_strlen($parameters['password']) > 6) $account->password = sodium_crypto_pwhash_str(
$parameters['password'],
SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE,
@@ -268,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

@@ -14,6 +14,9 @@ use mirzaev\ebala\views\templater,
// Фреймворк PHP
use mirzaev\minimal\controller;
// Встроенные библиотеки
use exception;
/**
* Ядро контроллеров
*

View File

@@ -359,7 +359,7 @@ final class market extends core
// Авторизован аккаунт администратора или оператора
// Инициализация данных магазина
$market = model::read('d.id == "' . $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)) {
// Найдены данные магазина
@@ -399,7 +399,7 @@ final class market extends core
// Авторизован аккаунт администратора или оператора
// Инициализация данных магазина
$market = model::read('d.id == "' . $parameters['id'] . '"');
$market = model::read('d.id == "' . urldecode($parameters['id']) . '"');
if (!empty($market)) {
// Найден магазин

View File

@@ -364,8 +364,8 @@ 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') {
// Запрошено запоминание

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,6 +32,11 @@ final class task extends core
{
use errors;
/**
* Типы работ
*/
final public const WORKS = ['Кассир', 'Выкладчик', 'Гастроном', 'Бригадир', 'Грузчик', 'Мобильный грузчик', 'Мобильный универсал'];
/**
* Создать
*
@@ -477,7 +483,7 @@ final class task extends core
];
if ($account->type === 'worker') {
// Оператор или администратор
// Сотрудник
foreach ($link->task['chats']['worker'] ?? [] as $message) {
// Перебор сообщений из чата: СОТРУДНИК <-> ОПЕРАТОР
@@ -486,7 +492,7 @@ final class task extends core
if (!array_key_exists((string) $account->getKey(), $message['readed'] ?? [])) ++$generated['chat']['unreaded'];
}
} else if ($account->type === 'market') {
// Оператор или администратор
// Магазин
foreach ($link->task['chats']['market'] ?? [] as $message) {
// Перебор сообщений из чата: МАГАЗИН <-> ОПЕРАТОР
@@ -537,8 +543,46 @@ final class task extends core
// Заявка подтверждена?
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'])) {
// Передан сотрудник
@@ -781,7 +825,7 @@ final class task extends core
'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();
// Заявка не принадлежит запросившему магазину?
@@ -816,7 +860,18 @@ final class task extends core
// Перевод ключей на русский язык
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
@@ -904,7 +959,6 @@ final class task extends core
}
}
/**
* Прочитать данные сотрудника
*
@@ -953,6 +1007,7 @@ final class task extends core
'tax' => 'ИНН',
'city' => 'Город',
'payment' => 'Форма оплаты',
'works' => 'Формы работ',
default => $key
},
'value' => $value
@@ -1153,6 +1208,12 @@ final class task extends core
// Изменение статуса подтверждения
$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)) {
// Записано изменение в базу данных
@@ -1272,6 +1333,12 @@ final class task extends core
}
}
// Запись в реестре последних обновивших
$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)) {
// Записано изменение в базу данных
@@ -1387,7 +1454,7 @@ final class task extends core
else {
// Получена оценка
// Запись оценики
// Запись оценки
$task->rating = $parameters['rating'];
if (!empty($parameters['review'])) {
@@ -1402,8 +1469,43 @@ final class task extends core
// Снятие с публикации
$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)) {
// Записано изменение в базу данных
@@ -1495,6 +1597,12 @@ final class task extends core
// Изменение статуса скрытия
$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)) {
// Записано изменение в базу данных
@@ -1586,28 +1694,40 @@ final class task extends core
// Заявка подтверждена?
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) {
// Найдена заявка
@@ -1615,6 +1735,12 @@ final class task extends core
// Изменение статуса
$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)) {
// Помечено как удалённое
@@ -1756,7 +1882,6 @@ final class task extends core
}
}
/**
* Прочитать данные работ для <datalist>
*
@@ -1767,21 +1892,111 @@ final class task extends core
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;
// Запись заголовков ответа
header('Content-Type: application/json');
header('Content-Encoding: none');
@@ -1804,8 +2019,8 @@ final class task extends core
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
} else throw new exception('Не найдена заявка');
} else throw new exception('Вы не авторизованы');
} else throw new exception('Вы не авторизованы');
}
} catch (exception $e) {
// Запись в реестр ошибок
$this->errors[] = [
@@ -1862,24 +2077,40 @@ final class task extends core
// Заявка подтверждена?
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('Запрещено редактировать тип работы начатой заявки');
// Заявка уже прошла?
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) {
// Найдена заявка
@@ -1890,6 +2121,12 @@ final class task extends core
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
@@ -2006,6 +2243,12 @@ final class task extends core
// Изменение статуса
$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
@@ -2090,24 +2333,40 @@ final class task extends core
// Заявка подтверждена?
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('Запрещено редактировать дату и время начатой заявки');
// Заявка уже прошла?
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) {
// Найдена заявка
@@ -2117,6 +2376,12 @@ final class task extends core
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
@@ -2204,6 +2469,12 @@ final class task extends core
// Запись комментария
$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
@@ -2291,6 +2562,12 @@ final class task extends core
// Запись статуса о публикации
$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
@@ -2378,6 +2655,12 @@ final class task extends core
// Запись статуса о публикации
$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
@@ -2727,6 +3010,12 @@ final class task extends core
$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
@@ -2799,6 +3088,12 @@ final class task extends core
$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
@@ -2812,7 +3107,6 @@ final class task extends core
// Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode(
[

View File

@@ -29,6 +29,11 @@ final class worker extends core
{
use errors;
/**
* Типы работ
*/
final public const WORKS = ['Кассир', 'Выкладчик', 'Гастроном', 'Бригадир', 'Грузчик', 'Мобильный грузчик', 'Мобильный универсал'];
/**
* Главная страница
*
@@ -261,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();
// Создание аккаунта
@@ -353,6 +359,7 @@ final class worker extends core
'city' => $parameters['worker_city'],
'district' => $parameters['worker_district'],
'address' => $parameters['worker_address'],
'work' => $parameters['worker_work'],
'hiring' => $parameters['worker_hiring'],
'rating' => 3
],
@@ -389,7 +396,7 @@ final class worker extends core
// Авторизован аккаунт администратора или оператора
// Инициализация данных сотрудника
$worker = model::read('d.id == "' . $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)) {
// Найдены данные сотрудника
@@ -429,7 +436,7 @@ final class worker extends core
// Авторизован аккаунт администратора или оператора
// Инициализация данных сотрудника
$worker = model::read('d.id == "' . $parameters['id'] . '"');
$worker = model::read('d.id == "' . urldecode($parameters['id']) . '"');
if (!empty($worker)) {
// Найден сотрудник
@@ -437,6 +444,7 @@ final class worker extends core
// Универсализация
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();
// Инициализация параметров (перезапись переданными значениями)
@@ -458,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)) {
@@ -513,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>
*

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,8 +137,6 @@ 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']['id'])) {
@@ -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,7 +291,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.id DESC
LIMIT 1
RETURN d

View File

@@ -206,7 +206,7 @@ final class payments extends core
->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($market->city, $task->work))
->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))
@@ -431,6 +431,15 @@ final class payments extends core
// Инициализация счётчика строк
$row = 9;
// Инициализация буфера объединённых данных всех магазинов
$total = [
'workers' => 0,
'hours' => 0,
'hour' => [],
'payment' => 0,
'vat' => 0
];
foreach ($merged as $id => $dates) {
// Перебор магазинов
@@ -465,7 +474,7 @@ final class payments extends core
->setCellValue("E$row", $work)
->setCellValue("F$row", $task['workers'])
->setCellValue("G$row", $task['hours'])
->setCellValue("H$row", $hour = static::hour($market->city, $work))
->setCellValue("H$row", $hour = static::hour('market', $market->city, $work))
->setCellValue("I$row", $payment = $hour * $task['hours'])
->setCellValue("J$row", $payment);
@@ -491,10 +500,17 @@ final class payments extends core
->setCellValue("E$row", '')
->setCellValue("F$row", $result['workers'])
->setCellValue("G$row", $result['hours'])
->setCellValue("H$row", array_sum($result['hour']) / count($result['hour']))
->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()
@@ -508,6 +524,36 @@ final class payments extends core
}
}
// Запись строки с общими данными всех магазинов
$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');
@@ -533,46 +579,84 @@ final class payments extends core
/**
* 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 $city, string $work): int|float
public static function hour(string $type, string $city, string $work): int|float
{
return match ($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,
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
},
'Железногорск', 'Сосновоборск', 'Тыва' => 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
};
};
}
/**

View File

@@ -245,4 +245,51 @@ final class task extends core
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 {
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;
}

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"],

View File

@@ -38,7 +38,6 @@ div#popup>section.stretched {
flex-grow: unset;
}
div#popup>section.calculated {
width: calc(var(--calculated-width) - var(--padding-horizontal, 0px) * 2);
}
@@ -53,6 +52,11 @@ div#popup>section.list {
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;
@@ -292,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');

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

@@ -339,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");
@@ -1774,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

@@ -2210,9 +2210,37 @@ if (typeof window.tasks !== "function") {
});
});
// Инициализация оболочки для кнопок
const buttons_worker = document.createElement("div");
buttons_worker.classList.add("row", "buttons", "divided", "merged");
// Инициализация кнопки подтверждения
const ban = document.createElement("button");
if (true) {
// Забанен сотрудник
ban.classList.add("earth", "stretched");
ban.innerText = "Разблокировать";
ban.setAttribute('title', 'Сотрудник снова сможет записываться на ваши заявки');
ban.setAttribute(
"onclick",
`tasks.unban(this, this.parentElement.previousElementSibling.previousElementSibling.children[0], this.parentElement.previousElementSibling, document.getElementById('${task}'))`
);
} else {
// Не забанен сотрудник
ban.classList.add("clay", "stretched");
ban.innerText = "Заблокировать";
ban.setAttribute('title', 'Сотрудник не сможет записываться на ваши заявки');
ban.setAttribute(
"onclick",
`tasks.ban(this, this.parentElement.previousElementSibling.previousElementSibling.children[0], this.parentElement.previousElementSibling, document.getElementById('${task}'))`
);
}
// Инициализация оболочки для кнопок
const buttons = document.createElement("div");
buttons.classList.add("row", "buttons");
buttons.classList.add("row", "buttons", "merged");
// Инициализация кнопки заявления о проблеме
const problem = document.createElement("button");
@@ -2245,7 +2273,7 @@ if (typeof window.tasks !== "function") {
complete.innerText = "Завершить";
complete.setAttribute(
"onclick",
`tasks.complete(this, this.parentElement.previousElementSibling.previousElementSibling.children[0], this.parentElement.previousElementSibling, document.getElementById('${task}'))`
`tasks.complete(this, this.parentElement.previousElementSibling.previousElementSibling.previousElementSibling.children[0], this.parentElement.previousElementSibling.previousElementSibling, document.getElementById('${task}'))`
);
// Инициализация окна с ошибками
@@ -2297,6 +2325,9 @@ if (typeof window.tasks !== "function") {
if (data.completed !== true) {
// Зявка не завершена
buttons_worker.appendChild(ban);
column.appendChild(buttons_worker);
buttons.appendChild(problem);
buttons.appendChild(complete);
column.appendChild(buttons);
@@ -2573,8 +2604,12 @@ if (typeof window.tasks !== "function") {
// Инициализация списка выбора типа работы
const work = document.createElement("select");
work.classList.add("row", "connected", "stretched");
this.works(row).then((html) => (work.innerHTML = html));
this.works(task).then((html) => (work.innerHTML = html));
work.setAttribute("title", "Тип работы");
work.setAttribute(
'onchange',
`tasks.work(document.getElementById('${task}'), this)`
);
// Инициализация поля ввода описания
const description = document.createElement("textarea");
@@ -2907,8 +2942,19 @@ if (typeof window.tasks !== "function") {
// Блокировка закрытия окна (чтобы не вызвался click() через событие onclick)
this.freeze = true;
// Активация виртуальной кнопки "подтвердить"
confirm.click();
if (
typeof core === "function" &&
core.interface === "operator" &&
core.interface === "administrator"
) {
// Оператор или администратор
// Активация виртуальной кнопки "подтвердить"
confirm.click();
} else {
// Магазин или сотрудник
}
// Возвращение статуса блокировки закрытия окна
this.freeze = freeze;
@@ -3752,8 +3798,6 @@ if (typeof window.tasks !== "function") {
// Инициализация идентификатора строки
const id = row.getAttribute("id");
alert(228);
if (typeof id === "string") {
// Инициализирован идентификатор
@@ -3840,41 +3884,33 @@ if (typeof window.tasks !== "function") {
}
/**
* Сгенерировать список работ и выбрать в нём ту, что записана в базе данных у заявки
* Список работ
*
* @param {HTMLElement} row Строка
* Сгенерировать список работ и выбрать в нём ту, что записана в базе данных у заявки, если передана
*
* @param {number|null} id Идентификатор заявки
*
* @return {array|null} Массив HTML-элементов <option>
*/
static async works(row) {
if (row instanceof HTMLElement) {
// Получена строка
static async works(id) {
// Запрос к серверу
return await fetch(`/tasks/works`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: `task=${id}`,
})
.then((response) => response.json())
.then((data) => {
if (this.errors(data.errors)) {
// Сгенерированы ошибки
} else {
// Не сгенерированы ошибки (подразумевается их отсутствие)
// Инициализация идентификатора строки
const id = row.getAttribute("id");
if (typeof id === "string") {
// Инициализирован идентификатор
// Запрос к серверу
return await fetch(`/task/${id}/works`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
})
.then((response) => response.json())
.then((data) => {
if (this.errors(data.errors)) {
// Сгенерированы ошибки
} else {
// Не сгенерированы ошибки (подразумевается их отсутствие)
return data.works;
}
});
}
}
return data.works;
}
});
}
/**

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
<!-- MARKET #{{ market.id.value }} -->
{% for key, data in market | filter((data, key) => key != 'id') -%}
{% 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>

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

View File

@@ -17,7 +17,7 @@
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="work" 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>

View File

@@ -1,7 +1,7 @@
{% 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">
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>
@@ -17,8 +17,7 @@
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="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 }}')">{%

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,7 +25,7 @@
class="icon arrow right"></i></button>
<datalist id="markets">
{% for account in accounts %}
<option value="{{ account.market }}">{{ account.market }} {{
<option value="{{ account.market.id }}">{{ account.market.id }} {{
account.market.name.first }} {{ account.market.name.second }}</option>
{% endfor %}
</datalist>
@@ -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,8 +21,8 @@
<button class="grass dense" onclick="tasks.create()">Создать</button>
{% endif %}
{% if account.type == 'administrator' or account.type == 'operator' %}
<button class="sea" onclick="payments.workers()">Зарплата</button>
<button class="sea" onclick="payments.markets()">Сверка</button>
<button class="sea" onclick="payments.workers()">Сотрудники</button>
<button class="sea" onclick="payments.markets()">Магазины</button>
{% endif %}
</label>
</form>

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