diff --git a/README.md b/README.md index b682cfd..2e1a82f 100755 --- a/README.md +++ b/README.md @@ -2,4 +2,6 @@ Site-registry of tasks for outsourced employees From this project i earned **700 000** Russian rubles -*As long as commits appear in the repository, this means that i continue paid development* +*As long as commits appear in the repository, this means that i continue paid development* + +I am selling this site to **capitalist scum** for a lot of money, but you, friend, can use my code **for free** ✌️ diff --git a/mirzaev/ebala/system/controllers/task.php b/mirzaev/ebala/system/controllers/task.php index 580ad44..30f14f2 100755 --- a/mirzaev/ebala/system/controllers/task.php +++ b/mirzaev/ebala/system/controllers/task.php @@ -34,7 +34,7 @@ final class task extends core /** * Создать * - * @param array $parameters Параметры запроса + * @param array $parameters Параметры запроса (json в php://input) * * @return void В буфер вывода JSON-документ с запрашиваемыми параметрами */ @@ -47,33 +47,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())?->id : 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())?->id : 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())?->id : 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())?->id : 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))) { - // Запись заголовков ответа - header('Content-Type: application/json'); - header('Content-Encoding: none'); - header('X-Accel-Buffering: no'); - // Инициализация буфера вывода - ob_start(); + foreach ($json as $work => $tasks) { + // Перебор категорий (колонок) - // Генерация ответа - echo json_encode( - [ - 'errors' => self::parse_only_text($this->errors) - ] - ); + foreach ($tasks as $task) { + // Перебор заявок - // Запись заголовков ответа - header('Content-Length: ' . ob_get_length()); + // Создание заявки + 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'] + ); + } + } - // Отправка и деинициализация буфера вывода - ob_end_flush(); - flush(); + // Запись заголовков ответа + 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(); + } else throw new exception('Не удалось инициализировать JSON-документ с данными заявок'); } else throw new exception('Вы не авторизованы'); } catch (exception $e) { // Запись в реестр ошибок @@ -1758,7 +1775,7 @@ final class task extends core // Найдена заявка // Инициализация списка работ - $this->view->works = ['Кассир', 'Выкладчик', 'Грузчик', 'Гастроном']; + $this->view->works = ['Кассир', 'Выкладчик', 'Гастроном', 'Бригадир', 'Грузчик', 'Мобильный грузчик', 'Мобильный универсал']; // Проверка на существование записанной в задаче работы в списке существующих работ и запись об этом в глобальную переменную шаблонизатора foreach ($this->view->works as $work) if ($this->view->task->work === $work) $this->view->exist = true; diff --git a/mirzaev/ebala/system/models/account.php b/mirzaev/ebala/system/models/account.php index 12d7518..ef6fb55 100755 --- a/mirzaev/ebala/system/models/account.php +++ b/mirzaev/ebala/system/models/account.php @@ -135,7 +135,7 @@ final class account extends core if (!empty($session->buffer['market']['entry']['password'])) { // Найден пароль в буфере сессии - if (($account = market::account(market::read('d.id == "' . $session->buffer['market']['entry']['id'] . '"', amount: 1)?->getId())) 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'])) { diff --git a/mirzaev/ebala/system/models/task.php b/mirzaev/ebala/system/models/task.php index 9265bfe..870cb1d 100755 --- a/mirzaev/ebala/system/models/task.php +++ b/mirzaev/ebala/system/models/task.php @@ -40,7 +40,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,12 +51,13 @@ 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, @@ -67,6 +68,7 @@ final class task extends core bool $hided = false, bool $problematic = false, bool $completed = false, + ?string $commentary = null, array &$errors = [] ): ?string { try { @@ -90,6 +92,7 @@ final class task extends core 'hided' => $hided, 'problematic' => $problematic, 'completed' => $completed, + 'commentary' => $commentary, ]); } else throw new exception('Не удалось инициализировать коллекции'); } catch (exception $e) { @@ -180,4 +183,25 @@ final class task extends core // Exit (fail) return []; } + + /** + * 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 + }; + } } diff --git a/mirzaev/ebala/system/public/css/main.css b/mirzaev/ebala/system/public/css/main.css index 3eb91fc..14faedf 100755 --- a/mirzaev/ebala/system/public/css/main.css +++ b/mirzaev/ebala/system/public/css/main.css @@ -302,12 +302,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; diff --git a/mirzaev/ebala/system/public/css/popup.css b/mirzaev/ebala/system/public/css/popup.css index f11fad7..a2c99c2 100644 --- a/mirzaev/ebala/system/public/css/popup.css +++ b/mirzaev/ebala/system/public/css/popup.css @@ -44,9 +44,12 @@ div#popup>section.calculated { } 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; } @@ -65,17 +68,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 +156,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 { diff --git a/mirzaev/ebala/system/public/js/tasks.js b/mirzaev/ebala/system/public/js/tasks.js index 22ab48e..1e3e03f 100755 --- a/mirzaev/ebala/system/public/js/tasks.js +++ b/mirzaev/ebala/system/public/js/tasks.js @@ -1,5676 +1,6811 @@ "use strict"; if (typeof window.tasks !== "function") { - // Not initialized - - // Initialize of the class in global namespace - window.tasks = class tasks { - /** - * Заблокировать функцию закрытия всплывающего окна? - */ - static freeze = false; - - /** - * Тело всплывающего окна (массив) - */ - static body = {}; - - /** - * Инициализирован класс? - */ - static initialized = false; - - /** - * Создать заявку - * - * @param {HTMLElement} cashiers Количество кассиров - * @param {HTMLElement} displayers Количество выкладчиков - * @param {HTMLElement} loaders Количество грузчиков - * @param {HTMLElement} gastronomes Количество гастрономов - * @param {HTMLElement} start Начало работы (00:00) - * @param {HTMLElement} end Конец работы (00:00) - * @param {HTMLElement} date Дата работы (d.m.Y) - * @param {HTMLElement} button Кнопка заявки - * - * @return {void} - */ - static _create( - cashiers, - displayers, - loaders, - gastronomes, - start, - end, - date, - button, - ) { - // Блокировка полей ввода - cashiers.setAttribute("readonly", true); - displayers.setAttribute("readonly", true); - loaders.setAttribute("readonly", true); - gastronomes.setAttribute("readonly", true); - start.setAttribute("readonly", true); - end.setAttribute("readonly", true); - date.setAttribute("readonly", true); - - // Блокировка кнопки - button.setAttribute("disabled", true); - - // Запуск выполнения - this.__create( - cashiers, - displayers, - loaders, - gastronomes, - start, - end, - date, - button, - ); - } - - /** - * Создать заявку - * - * @param {HTMLElement} cashiers Количество кассиров - * @param {HTMLElement} displayers Количество выкладчиков - * @param {HTMLElement} loaders Количество грузчиков - * @param {HTMLElement} gastronomes Количество гастрономов - * @param {HTMLElement} start Начало работы (00:00) - * @param {HTMLElement} end Конец работы (00:00) - * @param {HTMLElement} date Дата работы (d.m.Y) - * @param {HTMLElement} button Кнопка заявки - * - * @return {void} - */ - static __create = damper( - async ( - cashiers, - displayers, - loaders, - gastronomes, - start, - end, - date, - button, - ) => { - // Инициализация функции разблокировки - function unblock() { - // Разблокировка полей ввода - cashiers.removeAttribute("readonly"); - displayers.removeAttribute("readonly"); - loaders.removeAttribute("readonly"); - gastronomes.removeAttribute("readonly"); - start.removeAttribute("readonly"); - end.removeAttribute("readonly"); - date.removeAttribute("readonly"); - - // Разблокировка кнопки - button.removeAttribute("disabled"); - } - - // Запуск отсрочки разблокировки на случай, если сервер не отвечает - const timeout = setTimeout(() => { - this.errors(["Сервер не отвечает"]); - unblock(); - }, 5000); - - // Запрос к серверу - return await fetch("/tasks/create", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: - `cashiers=${cashiers.value}&displayers=${displayers.value}&loaders=${loaders.value}&gastronomes=${gastronomes.value}&start=${start.value}&end=${end.value}&date=${ - date.valueAsDate / 1000 - }`, - }) - .then((response) => response.json()) - .then((data) => { - // Удаление отсрочки разблокировки - clearTimeout(timeout); - - if (this.errors(data.errors)) { - // Сгенерированы ошибки - - // Разблокировка полей ввода и кнопок - unblock(); - } else { - // Не сгенерированы ошибки (подразумевается их отсутствие) - - if (this.body.wrap instanceof HTMLElement) { - // Найдено активное окно - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.buttons); - - // Сброс блокировки - this.freeze = false; - - // Деинициализация активного окна - this.body.wrap.remove(); - } - - // Реинициализация строк - this.reinit(); - } - }); - }, - 300, - ); - - /** - * Сгенерировать окно создания заявок - * - * @param {HTMLElement} row Строка - * - * @return {void} - */ - static create = damper(() => { - // Инициализация оболочки всплывающего окна - this.body.wrap = document.createElement("div"); - this.body.wrap.setAttribute("id", "popup"); - - // Инициализация всплывающего окна - const popup = document.createElement("section"); - popup.classList.add("list", "small"); - - // Инициализация заголовка всплывающего окна - const title = document.createElement("h3"); - title.classList.add("unselectable"); - title.innerText = "Создание заявки"; - - // Инициализация оболочки с основной информацией - const main = document.createElement("section"); - main.classList.add("main"); - - // Инициализация колонки - const tasks = document.createElement("div"); - tasks.classList.add("column"); - tasks.setAttribute("data-column", "tasks"); - - // Инициализация строки - const row_1 = document.createElement("div"); - row_1.classList.add("row", "merged"); - - // Инициализация оболочки для строки с кассирами - const cashier = document.createElement("label"); - cashier.setAttribute("id", "cashier"); - - // Инициализация заголовка для поля ввода кассиров - const cashier_title = document.createElement("b"); - cashier_title.classList.add("separated", "right"); - cashier_title.innerText = "Кассиры:"; - - // Инициализация поля ввода кассиров - const cashier_input = document.createElement("input"); - cashier_input.classList.add("small", "center", "cloud"); - cashier_input.setAttribute("type", "number"); - cashier_input.setAttribute("min", "0"); - cashier_input.setAttribute("max", "50"); - cashier_input.setAttribute("title", "Количество кассиров"); - cashier_input.value = 0; - - // Инициализация строки - const row_2 = document.createElement("div"); - row_2.classList.add("row", "merged"); - - // Инициализация оболочки для строки с выкладчиками - const displayer = document.createElement("label"); - displayer.setAttribute("id", "displayer"); - - // Инициализация заголовка для поля ввода выкладчиков - const displayer_title = document.createElement("b"); - displayer_title.classList.add("separated", "right"); - displayer_title.innerText = "Выкладчики:"; - - // Инициализация поля ввода выкладчиков - const displayer_input = document.createElement("input"); - displayer_input.classList.add("small", "center", "cloud"); - displayer_input.setAttribute("type", "number"); - displayer_input.setAttribute("min", "0"); - displayer_input.setAttribute("max", "50"); - displayer_input.setAttribute("title", "Количество выкладчиков"); - displayer_input.value = 0; - - // Инициализация строки - const row_3 = document.createElement("div"); - row_3.classList.add("row", "merged"); - - // Инициализация оболочки для строки с грузчиками - const loader = document.createElement("label"); - loader.setAttribute("id", "loader"); - - // Инициализация заголовка для поля ввода грузчиков - const loader_title = document.createElement("b"); - loader_title.classList.add("separated", "right"); - loader_title.innerText = "Грузчики:"; - - // Инициализация поля ввода грузчиков - const loader_input = document.createElement("input"); - loader_input.classList.add("small", "center", "cloud"); - loader_input.setAttribute("type", "number"); - loader_input.setAttribute("min", "0"); - loader_input.setAttribute("max", "50"); - loader_input.setAttribute("title", "Количество грузчиков"); - loader_input.value = 0; - - // Инициализация строки - const row_4 = document.createElement("div"); - row_4.classList.add("row", "merged"); - - // Инициализация оболочки для строки с гастрономами - const gastronome = document.createElement("label"); - gastronome.setAttribute("id", "gastronome"); - - // Инициализация заголовка для поля ввода гастрономов - const gastronome_title = document.createElement("b"); - gastronome_title.classList.add("separated", "right"); - gastronome_title.innerText = "Гастрономы:"; - - // Инициализация поля ввода гастрономов - const gastronome_input = document.createElement("input"); - gastronome_input.classList.add("small", "center", "cloud"); - gastronome_input.setAttribute("type", "number"); - gastronome_input.setAttribute("min", "0"); - gastronome_input.setAttribute("max", "50"); - gastronome_input.setAttribute("title", "Количество гастрономов"); - gastronome_input.value = 0; - - // Инициализация строки - const row_5 = document.createElement("div"); - row_5.classList.add("row", "buttons"); - - // Инициализация оболочки для кнопок - const buttons = document.createElement("label"); - - // Инициализация поля ввода времени начала - const start_input = document.createElement("input"); - start_input.classList.add("cloud"); - start_input.setAttribute("type", "time"); - start_input.setAttribute("title", "Время начала работы по заявке"); - start_input.value = "09:00"; - - // Инициализация поля ввода времени окончания - const end_input = document.createElement("input"); - end_input.classList.add("cloud"); - end_input.setAttribute("type", "time"); - end_input.setAttribute("title", "Время конца работы по заявке"); - end_input.value = "18:00"; - - // Инициализация поля ввода даты (дня) - const day_input = document.createElement("input"); - day_input.classList.add("cloud"); - day_input.setAttribute("type", "date"); - day_input.setAttribute("title", "Дата заявки"); - const tomorrow = new Date(); - tomorrow.setDate(tomorrow.getDate() + 1); - day_input.valueAsDate = tomorrow; - - // Инициализация кнопки подтверждения даты - const create = document.createElement("button"); - create.classList.add("grass"); - create.innerText = "Создать"; - create.setAttribute( - "onclick", - `tasks._create(this.parentElement.parentElement.previousElementSibling.previousElementSibling.previousElementSibling.previousElementSibling.children[0].children[1], this.parentElement.parentElement.previousElementSibling.previousElementSibling.previousElementSibling.children[0].children[1], this.parentElement.parentElement.previousElementSibling.previousElementSibling.children[0].children[1], this.parentElement.parentElement.previousElementSibling.children[0].children[1], this.previousElementSibling.previousElementSibling.previousElementSibling, this.previousElementSibling.previousElementSibling, this.previousElementSibling, this)`, - ); - - // Инициализация окна с ошибками - this.body.errors = document.createElement("section"); - this.body.errors.classList.add( - "errors", - "window", - "list", - "small", - "hidden", - ); - this.body.errors.setAttribute("data-errors", true); - - // Инициализация элемента-тела (оболочки) окна с ошибками - const errors = document.createElement("section"); - errors.classList.add("body"); - - // Инициализация элемента-списка ошибок - const dl = document.createElement("dl"); - - // Инициализация активного всплывающего окна - const old = document.getElementById("popup"); - - if (old instanceof HTMLElement) { - // Найдено активное окно - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.buttons); - - // Сброс блокировки - this.freeze = false; - - // Удаление активного окна - old.remove(); - } - - // Запись в документ - popup.appendChild(title); - - cashier.appendChild(cashier_title); - cashier.appendChild(cashier_input); - row_1.appendChild(cashier); - tasks.appendChild(row_1); - - displayer.appendChild(displayer_title); - displayer.appendChild(displayer_input); - row_2.appendChild(displayer); - tasks.appendChild(row_2); - - loader.appendChild(loader_title); - loader.appendChild(loader_input); - row_3.appendChild(loader); - tasks.appendChild(row_3); - - gastronome.appendChild(gastronome_title); - gastronome.appendChild(gastronome_input); - row_4.appendChild(gastronome); - tasks.appendChild(row_4); - - buttons.appendChild(start_input); - buttons.appendChild(end_input); - buttons.appendChild(day_input); - buttons.appendChild(create); - row_5.appendChild(buttons); - tasks.appendChild(row_5); - - main.appendChild(tasks); - popup.appendChild(main); - - this.body.wrap.appendChild(popup); - document.body.appendChild(this.body.wrap); - - errors.appendChild(dl); - this.body.errors.appendChild(errors); - this.body.wrap.appendChild(this.body.errors); - - // Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup) - function top(errors) { - errors.style.setProperty( - "transition", - "0s", - ); - errors.style.setProperty( - "--top", - popup.offsetTop + popup.offsetHeight + 12 + "px", - ); - setTimeout( - () => errors.style.removeProperty("transition"), - 100, - ); - } - top(this.body.errors); - const resize = new ResizeObserver(() => top(this.body.errors)); - resize.observe(this.body.wrap); - - // Инициализация функции блокировки кнопки - const block = () => { - if ( - cashier_input.value > 0 || - displayer_input.value > 0 || - loader_input.value > 0 || - gastronome_input.value > 0 - ) create.removeAttribute("disabled"); - else create.setAttribute("disabled", "true"); - }; - - // Добавление функции блокировки кнопки по событиям - cashier_input.addEventListener("keyup", block); - displayer_input.addEventListener("keyup", block); - loader_input.addEventListener("keyup", block); - gastronome_input.addEventListener("keyup", block); - cashier_input.addEventListener("change", block); - displayer_input.addEventListener("change", block); - loader_input.addEventListener("change", block); - gastronome_input.addEventListener("change", block); - - // Первичная активация функции блокировки кнопки - block(); - - // Инициализация функции закрытия всплывающего окна - const click = () => { - // Блокировка - if (this.freeze) return; - - // Удаление всплывающего окна - this.body.wrap.remove(); - - // Удаление статуса активной строки - row.removeAttribute("data-selected"); - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.buttons); - - // Сброс блокировки - this.freeze = false; - }; - - // Инициализация функции добавления функции закрытия всплывающего окна - const enable = () => this.body.wrap.addEventListener("click", click); - - // Инициализация функции удаления функции закрытия всплывающего окна - const disable = () => this.body.wrap.removeEventListener("click", click); - - // Первичная активация функции удаления всплывающего окна - enable(); - - // Инициализация блокировки удаления окна при взаимодействии с select-элементом - for (const select of popup.getElementsByTagName("select")) { - // Перебор всех select-элементов - - // Инициализация функции блокировки удаления окна по событию - select.addEventListener("click", () => { - // Блокировка удаления окна - this.freeze = true; - - // Разблокировка удаления окна - setTimeout(() => this.freeze = false, 100); - }); - - for (const option of select.getElementsByTagName("option")) { - // Перебор всех option-элементов - - // Инициализация функции блокировки удаления окна по событию - option.addEventListener("click", () => { - // Блокировка удаления окна - this.freeze = true; - - // Разблокировка удаления окна - setTimeout(() => this.freeze = false, 100); - }); - } - } - - // Инициализация блокировки удаления окна при взаимодействии с textarea-элементом - for (const textarea of popup.getElementsByTagName("textarea")) { - // Перебор всех textarea-элементов - - // Обновлять позицию окна с ошибками при изменении размера textarea (там position: absolute) - new MutationObserver(() => top(this.body.errors)).observe(textarea, { - attributes: true, - attributeFilter: ["style"], - }); - - // Инициализация функции игнорирования блокировки для выбранных кнопок - textarea.addEventListener("keydown", (e) => { - if (e.keyCode === 27) { - // Нажата кнопка: "escape" - - // Вызов глобальной функции управления кнопками - this.buttons(e, true); - } else if (e.ctrlKey && e.keyCode === 13) { - // Нажаты кнопки: "control", "enter" - - // Вызов глобальной функции управления кнопками - this.buttons(e, true); - } - }); - - // Добавление функции блокировки удаления окна и клавиш по событиям - textarea.addEventListener("focus", () => this.freeze = true); - textarea.addEventListener("focusout", () => this.freeze = false); - } - - // Инициализация функции управления кнопками - this.buttons = (e, force = false) => { - // Блокировка - if (!force && this.freeze) return; - - if (e.keyCode === 27) { - // Нажата кнопка: "escape" - - // Удаление окна - click(); - } else if (event.keyCode === 13) { - // Нажата кнопка: "enter" - - // Инициализация буфера с текущим статусом блокировки закрытия окна - const freeze = this.freeze; - - // Блокировка закрытия окна (чтобы не вызвался click() через событие onclick) - this.freeze = true; - - // Активация виртуальной кнопки "создать" - create.click(); - - // Возвращение статуса блокировки закрытия окна - this.freeze = freeze; - } - }; - - // Инициализация быстрых действий по кнопкам - document.addEventListener("keydown", this.buttons); - - // Добавление функции удаления всплывающего окна по событиям - popup.addEventListener("mouseenter", disable); - popup.addEventListener("mouseleave", enable); - - // Фокусировка - cashier_input.focus(); - }, 300); - - /** - * Записать или переключить фильтр (0, 1, 2) - * - * @param {string} name Название - * @param {string|number|null} value Значение - * @param {HTMLElement|null} button Кнопка - * - * @return {void} - */ - static filter = damper(async (name, value, button) => { - if (typeof name === "string") { - // Получено название - - // Инициализация сериализованного пути к директории - const path = `tasks_filter_${name}`; - - if (typeof value === "string" || typeof value === "number") { - // Получено значение - - // Запись нового значения - buffer.write(path, value); - } else { - // Не получено значение - - // Чтение текущего значения - value = +(await buffer.read(path)); - - // Инициализация значения по умолчанию - if (isNaN(value)) value = 0; - - // Запись нового значения (инвертирование) - buffer.write(path, ++value < 3 ? value : 0); - - if (button instanceof HTMLElement) { - // Получена кнопка - - // Деинициализация классов статуса фильтра - button.classList.remove("earth", "sand", "river"); - - // Инициализация классов статуса фильтра - if (value === 1) button.classList.add("sand"); - else if (value === 2) button.classList.add("river"); - else button.classList.add("earth"); - } - } - } - }, 300); - - /** - * Записать фильтр поиска - * - * @param {HTMLElement} input Строка поиска - * @param {HTMLElement} button Кнопка отправки - * - * @return {void} - */ - static search = (input, button) => { - input.setAttribute("readonly", true); - button.setAttribute("disabled", true); - - this._search(input, button); - }; - - /** - * Записать фильтр поиска (системное) - * - * Используется как оболочка для большей временной задержки - * - * @param {HTMLElement} input Строка поиска - * @param {HTMLElement} button Кнопка отправки - * - * @return {void} - */ - static _search = damper((input, button) => { - this.filter("search", input.value); - this.reinit(() => { - input.removeAttribute("readonly"); - button.removeAttribute("disabled"); - }); - }, 1000); - - /** - * Инициализация страниц - * - * @param {number} amount Количество страниц для генерации - * @param {number} iteration Номер итерации (системное) - * - * @return {void} - */ - static init(amount = 3, iteration = 0) { - if ( - typeof amount === "number" && typeof iteration === "number" && - this.initialized === false - ) { - // Получены количество страниц и номер итерации - - // Инициализация страницы - tasks.read(++iteration); - - function generate() { - // Завершено выполнение итерации - - // Деинициализация слушателя завершения текущей итерации - document.removeEventListener("tasks.read." + iteration, generate); - - // Проверка условий и запуск следующей итерации - if (iteration < amount) tasks.init(amount, iteration); - else this.initialized = true; - } - - // Инициализация слушателя завершения текущей итерации - document.addEventListener("tasks.read." + iteration, generate); - } - } - - /** - * Реинициализация страниц - * - * @param {function} postprocessing Функция которая будет исполнена после реинициализации - * @param {number} amount Количество страниц для генерации при инициализации - * - * @return {void} - */ - static reinit = damper((postprocessing, amount = 3) => { - if (typeof amount === "number") { - // Получены количество страниц - - // Деинициализация cookie с номером страницы - Cookies.remove("tasks_page", { path: "/" }); - - // Инициализация оболочки - const tasks = document.getElementById("tasks"); - - if (tasks instanceof HTMLElement) { - // Найдена оболочка - - // Инициализация буфера активного элемента - const active = document.activeElement; - - // Инициализация буфера элементов - const buffer = document.createElement("div"); - - // Перенос элементов в буфер - buffer.replaceChildren(...tasks.children); - - // Удаление комментариев с индексами страниц - tasks.innerHTML = tasks.innerHTML.replace( - //g, - "", - ); - - // Перенос элементов из буфера - tasks.replaceChildren(...buffer.children); - - // Деинициализация страниц - for ( - const row of tasks.querySelectorAll( - ':scope > div[data-row="task"]', - ) - ) row.remove(); - - // Сброс статуса инициализированности - this.initialized = false; - - // Инициализация страниц - this.init(amount); - - // Возвращение фокуса на активный элемент - active.focus(); - - // Постобработка - if (typeof postprocessing === "function") postprocessing(); - } - } - }, 2000); - - /** - * Прочитать - * - * Читает список задач и модифицирует документ (записывает в общий список) - * - * @param {number} page Страница - * - * @return {void} - */ - static read = damper(async (page) => { - if (typeof page !== "number") { - // Не получена страница - - // Инициализация страницы (если не получена, то брать из cookie и прибавлять 1) - page = Cookies.get(`tasks_page`) ?? 0; - - // Запись идентификатора следующей страницы - ++page; - } - - // Запрос к серверу - await fetch("/tasks/read", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: `page=${page}`, - }) - .then((response) => response.json()) - .then((data) => { - if (this.errors(data.errors)) { - // Сгенерированы ошибки - } else { - // Не сгенерированы ошибки (подразумевается их отсутствие) - - // Инициализация буфера для записи в документ - const element = document.createElement("div"); - - // Запись пустого элемента из буфера в документ - document.getElementById("tasks").appendChild(element); - - // Запись в документ HTML-данных через буфер - element.outerHTML = data.rows; - - // Запуск реинициализатора строк - this.reinitializer.start(); - - // Вызов события: "итерация чтения завершена" - document.dispatchEvent(new CustomEvent("tasks.read." + page)); - } - }); - }, 50); - - /** - * Сгенерировать всплывающее окно c данными задачи - * - * @param {HTMLElement} row Строка - * - * @return {void} - */ - static popup = damper(async (row) => { - if (row instanceof HTMLElement) { - // Получена строка - - // Инициализация идентификатора сотрудника - const task = row.getAttribute("id"); - - // Запрос к серверу - return await fetch(`/task/${task}/read`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }) - .then((response) => response.json()) - .then((data) => { - if (this.errors(data.errors)) { - // Сгенерированы ошибки - } else { - // Не сгенерированы ошибки - - if ((Date.now() / 1000 | 0) - data.start > 0) { - // Началась заявка (возможно уже закончилась) - - // Инициализация буфера выбранных строк - const buffer = [ - ...document.querySelectorAll('div[data-selected="true"]'), - ]; - - // Удаление статуса активной строки у остальных строк - for (const element of buffer) { - element.removeAttribute("data-selected"); - } - - // Инициализация статуса активной строки (обрабатываемой в данный момент) - row.setAttribute("data-selected", "true"); - - // Инициализация оболочки всплывающего окна - this.body.wrap = document.createElement("div"); - this.body.wrap.setAttribute("id", "popup"); - - // Инициализация всплывающего окна - const popup = document.createElement("section"); - popup.classList.add("list", "small"); - - // Инициализация заголовка всплывающего окна - const title = document.createElement("h3"); - title.classList.add("unselectable"); - title.innerText = task; - - // Инициализация оболочки с основной информацией - const main = document.createElement("section"); - main.classList.add("main"); - - // Инициализация колонки - const column = document.createElement("div"); - column.classList.add("column"); - - // Инициализация буфера для записи во всплывающее окно - const list = document.createElement("div"); - - // Инициализация строки рейтинга - const rating_row = document.createElement("div"); - rating_row.classList.add( - "row", - "divided", - "range", - "small", - "stretched", - ); - - // Инициализация ползунка выбора рейтинга - const rating_input = document.createElement("input"); - rating_input.classList.add("cloud"); - rating_input.setAttribute("type", "range"); - rating_input.setAttribute("title", "Оценка"); - rating_input.setAttribute("min", "1"); - rating_input.setAttribute("max", "5"); - rating_input.setAttribute("step", "1"); - rating_input.setAttribute( - "oninput", - `this.nextElementSibling.innerText = this.value; setTimeout(() => {this.nextElementSibling.style.opacity = 1; setTimeout(() => this.nextElementSibling.style.transition = "0s", 100)}, 100); this.nextElementSibling.style.setProperty('--left', (((this.value - 1) / 4) * (this.offsetWidth - 24) + 12) + 'px');`, - ); - - // Инициализация значения в курсоре ползунка выбора рейтинга - const rating_value = document.createElement("i"); - rating_value.classList.add("value", "unselectable"); - rating_value.style.opacity = 0; - rating_value.style.transition = "0.1s ease-in"; - rating_value.setAttribute("title", "Оценка"); - rating_value.value = 5; - - // Инициализация поля ввода отзыва - const review = document.createElement("textarea"); - review.classList.add("snow"); - review.setAttribute("autofocus", "true"); - review.setAttribute("maxlength", "300"); - review.setAttribute("title", "Отзыв"); - review.setAttribute( - "placeholder", - "Отзыв о сотруднике", - ); - - // Запись актуальных значений - this.value(row, "completed").then( - (completed) => { - // Рейтинг - this.value(row, "rating") - .then((text) => { - if (completed) { - // Завершена заявка - - // Блокировка - rating_input.setAttribute("disabled", "true"); - } - - // Запись актуального рейтинга - rating_input.value = text ?? 5; - }); - - // Отзыв - this.value(row, "review") - .then((text) => { - if (completed) { - // Завершена заявка - - // Блокировка - review.setAttribute("readonly", "true"); - } - - // Запись актуального отзыва - review.value = text ?? ""; - }); - }, - ); - - // Инициализация оболочки для кнопок - const buttons = document.createElement("div"); - buttons.classList.add("row", "buttons"); - - // Инициализация кнопки заявления о проблеме - const problem = document.createElement("button"); - problem.classList.add("clay", "stretched"); - if (row.classList.contains("problematic")) { - // Проблемная заявка - - problem.classList.add("grass"); - problem.innerText = "Проблема решена"; - - problem.setAttribute( - "onclick", - `chat.send(this, null, document.getElementById('${task}'), 'market', 'solution', true, 'Проблема решена'); document.removeEventListener("keydown", tasks.buttons);`, - ); - } else { - // Не проблемная заявка - - problem.classList.add("clay"); - problem.innerText = "Проблема"; - - problem.setAttribute( - "onclick", - `tasks.problem(document.getElementById('${task}'))`, - ); - } - - // Инициализация кнопки подтверждения - const complete = document.createElement("button"); - complete.classList.add("grass", "stretched"); - complete.innerText = "Завершить"; - complete.setAttribute( - "onclick", - `tasks.complete(this, this.parentElement.previousElementSibling.previousElementSibling.children[0], this.parentElement.previousElementSibling, document.getElementById('${task}'))`, - ); - - // Инициализация окна с ошибками - this.body.errors = document.createElement("section"); - this.body.errors.classList.add( - "errors", - "window", - "list", - "small", - "hidden", - ); - this.body.errors.setAttribute("data-errors", true); - - // Инициализация элемента-тела (оболочки) окна с ошибками - const errors = document.createElement("section"); - errors.classList.add("body"); - - // Инициализация элемента-списка ошибок - const dl = document.createElement("dl"); - - // Инициализация активного всплывающего окна - const old = document.getElementById("popup"); - - if (old instanceof HTMLElement) { - // Найдено активное окно - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.buttons); - - // Сброс блокировки - this.freeze = false; - - // Удаление активного окна - old.remove(); - } - - // Удаление блокировки выполнения закрытия всплывающего окна - this.freeze = false; - - // Запись в документ - popup.appendChild(title); - - column.appendChild(list); - rating_row.appendChild(rating_input); - rating_row.appendChild(rating_value); - column.appendChild(rating_row); - column.appendChild(review); - - if (data.completed !== true) { - // Зявка не завершена - - buttons.appendChild(problem); - buttons.appendChild(complete); - column.appendChild(buttons); - } - - main.appendChild(column); - popup.appendChild(main); - this.body.wrap.appendChild(popup); - - errors.appendChild(dl); - this.body.errors.appendChild(errors); - this.body.wrap.appendChild(this.body.errors); - - document.body.appendChild(this.body.wrap); - - // Запись в документ HTML-данных через буфер - list.outerHTML = data.task; - - // Первичная инициализация (для генерации местоположения элемента со значением) - setTimeout(() => rating_input.oninput(), 300); - - // Инициализация ошибок - this.errors(data.errors); - - // Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup) - function top(errors) { - errors.style.setProperty( - "transition", - "0s", - ); - errors.style.setProperty( - "--top", - popup.offsetTop + popup.offsetHeight + 12 + "px", - ); - setTimeout( - () => errors.style.removeProperty("transition"), - 100, - ); - } - top(this.body.errors); - const resize = new ResizeObserver(() => top(this.body.errors)); - resize.observe(this.body.wrap); - - // Инициализация функции закрытия всплывающего окна - const click = () => { - // Блокировка - if (this.freeze) return; - - // Удаление всплывающего окна - this.body.wrap.remove(); - - // Инициализация буфера выбранных строк - const buffer = [ - ...document.querySelectorAll('div[data-selected="true"]'), - ]; - - // Удаление статуса активной строки у остальных строк - for (const element of buffer) { - element.removeAttribute("data-selected"); - } - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.buttons); - - // Сброс блокировки - this.freeze = false; - }; - - // Инициализация функции добавления функции закрытия всплывающего окна - const enable = () => - this.body.wrap.addEventListener("click", click); - - // Инициализация функции удаления функции закрытия всплывающего окна - const disable = () => - this.body.wrap.removeEventListener("click", click); - - // Первичная активация функции удаления всплывающего окна - enable(); - - // Инициализация блокировки удаления окна при взаимодействии с select-элементом - for (const select of popup.getElementsByTagName("select")) { - // Перебор всех select-элементов - - // Инициализация функции блокировки удаления окна по событию - select.addEventListener("click", () => { - // Блокировка удаления окна - this.freeze = true; - - // Разблокировка удаления окна - setTimeout(() => this.freeze = false, 100); - }); - - for (const option of select.getElementsByTagName("option")) { - // Перебор всех option-элементов - - // Инициализация функции блокировки удаления окна по событию - option.addEventListener("click", () => { - // Блокировка удаления окна - this.freeze = true; - - // Разблокировка удаления окна - setTimeout(() => this.freeze = false, 100); - }); - } - } - - // Инициализация блокировки удаления окна при взаимодействии с textarea-элементом - for (const textarea of popup.getElementsByTagName("textarea")) { - // Перебор всех textarea-элементов - - // Обновлять позицию окна с ошибками при изменении размера textarea (там position: absolute) - new MutationObserver(() => top(this.body.errors)).observe( - textarea, - { - attributes: true, - attributeFilter: ["style"], - }, - ); - - // Инициализация функции игнорирования блокировки для выбранных кнопок - textarea.addEventListener("keydown", (e) => { - if (e.keyCode === 27) { - // Нажата кнопка: "escape" - - // Вызов глобальной функции управления кнопками - this.buttons(e, true); - } else if (e.ctrlKey && e.keyCode === 13) { - // Нажаты кнопки: "control", "enter" - - // Вызов глобальной функции управления кнопками - this.buttons(e, true); - } - }); - - // Добавление функции блокировки удаления окна и клавиш по событиям - textarea.addEventListener("focus", () => this.freeze = true); - textarea.addEventListener( - "focusout", - () => this.freeze = false, - ); - } - - // Инициализация функции управления кнопками - this.buttons = (e, force = false) => { - // Блокировка - if (!force && this.freeze) return; - - if (e.keyCode === 27) { - // Нажата кнопка: "escape" - - // Удаление окна - click(); - } else if (event.keyCode === 13) { - // Нажата кнопка: "enter" - - // Инициализация буфера с текущим статусом блокировки закрытия окна - const freeze = this.freeze; - - // Блокировка закрытия окна (чтобы не вызвался click() через событие onclick) - this.freeze = true; - - // Активация виртуальной кнопки "завершить" - complete.click(); - - // Возвращение статуса блокировки закрытия окна - this.freeze = freeze; - } - }; - - // Инициализация быстрых действий по кнопкам - document.addEventListener("keydown", this.buttons); - - // Добавление функции удаления всплывающего окна по событиям - popup.addEventListener("mouseenter", disable); - popup.addEventListener("mouseleave", enable); - - // Фокусировка - review.focus(); - } else { - // Не началась заявка - - // Инициализация буфера выбранных строк - const buffer = [ - ...document.querySelectorAll('div[data-selected="true"]'), - ]; - - // Удаление статуса активной строки у остальных строк - for (const element of buffer) { - element.removeAttribute("data-selected"); - } - - // Инициализация статуса активной строки (обрабатываемой в данный момент) - row.setAttribute("data-selected", "true"); - - // Инициализация оболочки всплывающего окна - this.body.wrap = document.createElement("div"); - this.body.wrap.setAttribute("id", "popup"); - - // Инициализация всплывающего окна - const popup = document.createElement("section"); - popup.classList.add("list", "small"); - - // Инициализация заголовка всплывающего окна - const title = document.createElement("h3"); - title.classList.add("unselectable"); - title.innerText = task; - - // Инициализация оболочки с основной информацией - const main = document.createElement("section"); - main.classList.add("main"); - - // Инициализация колонки - const column = document.createElement("div"); - column.classList.add("column"); - - // Инициализация буфера для записи во всплывающее окно - const list = document.createElement("div"); - - // Инициализация оболочки для строки с полем ввода даты и времени, и кнопкой - const _row = document.createElement("div"); - _row.classList.add("row", "divided"); - - // Инициализация оболочки для строки с полем ввода даты - const label = document.createElement("label"); - - // Инициализация поля ввода даты - const date = document.createElement("input"); - date.classList.add("cloud"); - date.setAttribute("type", "date"); - date.setAttribute("title", "Дата заявки"); - this.value(row, "date").then((text) => - date.valueAsDate = new Date(text * 1000) - ); - - // Инициализация поля ввода времени начала - const start = document.createElement("input"); - start.classList.add("cloud"); - start.setAttribute("type", "time"); - start.setAttribute("title", "Время начала работы"); - this.value(row, "start").then((text) => - start.value = typeof text === "string" - ? (text.length === 7 - ? 0 + text.replace(/(?<=:00):00/, "") - : text.replace(/(?<=:00):00/, "")) - : "00:00" - ); - - // Инициализация поля ввода времени окончания - const end = document.createElement("input"); - end.classList.add("cloud"); - end.setAttribute("type", "time"); - end.setAttribute("title", "Время конца работы"); - this.value(row, "end").then((text) => - end.value = typeof text === "string" - ? (text.length === 7 - ? 0 + text.replace(/(?<=:00):00/, "") - : text.replace(/(?<=:00):00/, "")) - : "00:00" - ); - - // Инициализация кнопки подтверждения даты - const replace = document.createElement("button"); - replace.classList.add("sea"); - replace.innerText = "Записать"; - replace.setAttribute( - "onclick", - `tasks.date(document.getElementById('${task}'), this.previousElementSibling.previousElementSibling.previousElementSibling, this.previousElementSibling.previousElementSibling, this.previousElementSibling, this)`, - ); - - // Инициализация списка выбора типа работы - const work = document.createElement("select"); - work.classList.add("row", "connected", "stretched"); - this.works(row).then((html) => work.innerHTML = html); - work.setAttribute("title", "Тип работы"); - - // Инициализация поля ввода описания - const description = document.createElement("textarea"); - description.classList.add("snow"); - this.value(row, "description").then((text) => - description.value = text - ); - description.setAttribute("autofocus", "true"); - description.setAttribute("maxlength", "300"); - description.setAttribute( - "onkeyup", - `tasks.description(document.getElementById('${task}'), this)`, - ); - description.setAttribute("title", "Дополнительная информация"); - description.setAttribute( - "placeholder", - "Дополнительная информация о требуемой работе", - ); - - // Инициализация оболочки для кнопок - const buttons_1 = document.createElement("div"); - buttons_1.classList.add("row", "merged", "divided", "buttons"); - - // Инициализация кнопки подтверждения - const problem = document.createElement("button"); - problem.classList.add("wide"); - if (row.classList.contains("problematic")) { - // Проблемная заявка - - problem.classList.add("grass"); - problem.innerText = "Проблема решена"; - - problem.setAttribute( - "onclick", - `chat.send(this, null, document.getElementById('${task}'), 'market', 'solution', true, 'Проблема решена'); document.removeEventListener("keydown", tasks.buttons);`, - ); - } else { - // Не проблемная заявка - - problem.classList.add("clay"); - problem.innerText = "Проблема"; - - problem.setAttribute( - "onclick", - `tasks.problem(document.getElementById('${task}'))`, - ); - } - - // Инициализация оболочки для кнопок - const buttons_2 = document.createElement("div"); - buttons_2.classList.add("row", "merged", "buttons"); - - // Инициализация кнопки подтверждения - const confirm = document.createElement("button"); - confirm.classList.add("wide"); - if (row.classList.contains("confirmed")) { - // Подтверждена заявка - - confirm.classList.add("clay"); - confirm.innerText = "Отклонить"; - } else { - // Не подтверждена заявка - - confirm.classList.add("grass"); - confirm.innerText = "Подтвердить"; - } - confirm.setAttribute( - "onclick", - `tasks.confirm(this, document.getElementById('${task}'))`, - ); - - // Инициализация кнопки скрытия - const hide = document.createElement("button"); - if (row.classList.contains("hided")) { - // Скрыта заявка - - hide.classList.add("grass"); - hide.innerText = "Показать"; - } else { - // Не подтверждена заявка - - hide.classList.add("sea"); - hide.innerText = "Скрыть"; - } - hide.setAttribute( - "onclick", - `tasks.hide(this, document.getElementById('${task}'))`, - ); - - // Инициализация кнопки удаления - const remove = document.createElement("button"); - remove.classList.add("clay"); - remove.innerText = "Удалить"; - remove.setAttribute( - "onclick", - `tasks.remove(this, document.getElementById('${task}'))`, - ); - - if ( - typeof core === "function" && - (core.interface === "market" || - core.interface === "worker") - ) { - // Расширить кнопку для магазина и сотрудника - - remove.classList.add("stretched"); - } - - // Инициализация окна с ошибками - this.body.errors = document.createElement("section"); - this.body.errors.classList.add( - "errors", - "window", - "list", - "small", - "hidden", - ); - this.body.errors.setAttribute("data-errors", true); - - // Инициализация элемента-тела (оболочки) окна с ошибками - const errors = document.createElement("section"); - errors.classList.add("body"); - - // Инициализация элемента-списка ошибок - const dl = document.createElement("dl"); - - // Инициализация активного всплывающего окна - const old = document.getElementById("popup"); - - if (old instanceof HTMLElement) { - // Найдено активное окно - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.buttons); - - // Сброс блокировки - this.freeze = false; - - // Удаление активного окна - old.remove(); - } - - // Удаление блокировки выполнения закрытия всплывающего окна - this.freeze = false; - - // Запись в документ - popup.appendChild(title); - - column.appendChild(list); - label.appendChild(date); - label.appendChild(start); - label.appendChild(end); - label.appendChild(replace); - _row.appendChild(label); - column.appendChild(_row); - column.appendChild(work); - column.appendChild(description); - - if ( - typeof core === "function" && - core.interface !== "market" && - core.interface !== "worker" - ) { - // Скрытие для магазина и сотрудника - - buttons_1.appendChild(problem); - column.appendChild(buttons_1); - - buttons_2.appendChild(confirm); - buttons_2.appendChild(hide); - } else { - // Отображение для магазина и сотрудника - - problem.classList.add("stretched"); - buttons_2.appendChild(problem); - } - - buttons_2.appendChild(remove); - column.appendChild(buttons_2); - - main.appendChild(column); - popup.appendChild(main); - this.body.wrap.appendChild(popup); - - errors.appendChild(dl); - this.body.errors.appendChild(errors); - this.body.wrap.appendChild(this.body.errors); - - document.body.appendChild(this.body.wrap); - - // Запись в документ HTML-данных через буфер - list.outerHTML = data.task; - - // Инициализация ошибок - this.errors(data.errors); - - // Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup) - function top(errors) { - errors.style.setProperty( - "transition", - "0s", - ); - errors.style.setProperty( - "--top", - popup.offsetTop + popup.offsetHeight + 12 + "px", - ); - setTimeout( - () => errors.style.removeProperty("transition"), - 100, - ); - } - top(this.body.errors); - const resize = new ResizeObserver(() => top(this.body.errors)); - resize.observe(this.body.wrap); - - // Инициализация функции закрытия всплывающего окна - const click = () => { - // Блокировка - if (this.freeze) return; - - // Удаление всплывающего окна - this.body.wrap.remove(); - - // Инициализация буфера выбранных строк - const buffer = [ - ...document.querySelectorAll('div[data-selected="true"]'), - ]; - - // Удаление статуса активной строки у остальных строк - for (const element of buffer) { - element.removeAttribute("data-selected"); - } - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.buttons); - - // Сброс блокировки - this.freeze = false; - }; - - // Инициализация функции добавления функции закрытия всплывающего окна - const enable = () => - this.body.wrap.addEventListener("click", click); - - // Инициализация функции удаления функции закрытия всплывающего окна - const disable = () => - this.body.wrap.removeEventListener("click", click); - - // Первичная активация функции удаления всплывающего окна - enable(); - - // Инициализация блокировки удаления окна при взаимодействии с select-элементом - for (const select of popup.getElementsByTagName("select")) { - // Перебор всех select-элементов - - // Инициализация функции блокировки удаления окна по событию - select.addEventListener("click", () => { - // Блокировка удаления окна - this.freeze = true; - - // Разблокировка удаления окна - setTimeout(() => this.freeze = false, 100); - }); - - for (const option of select.getElementsByTagName("option")) { - // Перебор всех option-элементов - - // Инициализация функции блокировки удаления окна по событию - option.addEventListener("click", () => { - // Блокировка удаления окна - this.freeze = true; - - // Разблокировка удаления окна - setTimeout(() => this.freeze = false, 100); - }); - } - } - - // Инициализация блокировки удаления окна при взаимодействии с textarea-элементом - for (const textarea of popup.getElementsByTagName("textarea")) { - // Перебор всех textarea-элементов - - // Обновлять позицию окна с ошибками при изменении размера textarea (там position: absolute) - new MutationObserver(() => top(this.body.errors)).observe( - textarea, - { - attributes: true, - attributeFilter: ["style"], - }, - ); - - // Инициализация функции игнорирования блокировки для выбранных кнопок - textarea.addEventListener("keydown", (e) => { - if (e.keyCode === 27) { - // Нажата кнопка: "escape" - - // Вызов глобальной функции управления кнопками - this.buttons(e, true); - } else if (e.ctrlKey && e.keyCode === 13) { - // Нажаты кнопки: "control", "enter" - - // Вызов глобальной функции управления кнопками - this.buttons(e, true); - } - }); - - // Добавление функции блокировки удаления окна и клавиш по событиям - textarea.addEventListener("focus", () => this.freeze = true); - textarea.addEventListener( - "focusout", - () => this.freeze = false, - ); - } - - // Инициализация функции управления кнопками - this.buttons = (e, force = false) => { - // Блокировка - if (!force && this.freeze) return; - - if (e.keyCode === 27) { - // Нажата кнопка: "escape" - - // Удаление окна - click(); - } else if (event.keyCode === 13) { - // Нажата кнопка: "enter" - - // Инициализация буфера с текущим статусом блокировки закрытия окна - const freeze = this.freeze; - - // Блокировка закрытия окна (чтобы не вызвался click() через событие onclick) - this.freeze = true; - - // Активация виртуальной кнопки "подтвердить" - confirm.click(); - - // Возвращение статуса блокировки закрытия окна - this.freeze = freeze; - } - }; - - // Инициализация быстрых действий по кнопкам - document.addEventListener("keydown", this.buttons); - - // Добавление функции удаления всплывающего окна по событиям - popup.addEventListener("mouseenter", disable); - popup.addEventListener("mouseleave", enable); - - // Фокусировка - description.focus(); - } - } - }); - } - }, 300); - - /** - * Подтвердить - * - * @param {HTMLElement} button Кнопка - * @param {HTMLElement} row Строка - * - * @return {void} - */ - static confirm = damper(async (button, row) => { - if (row instanceof HTMLElement && button instanceof HTMLElement) { - // Получена кнопка и строка - - // Инициализация идентификатора строки - const id = row.getAttribute("id"); - - if (typeof id === "string") { - // Инициализирован идентификатор - - // Запрос к серверу - return await fetch(`/task/${id}/confirm`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }) - .then((response) => response.json()) - .then((data) => { - if (this.errors(data.errors)) { - // Сгенерированы ошибки - } else { - // Не сгенерированы ошибки (подразумевается их отсутствие) - - // Инициализация буфера списка классов - // const buffer = [...row.classList]; - - // Инициализация статуса активной строки - const selected = row.getAttribute("data-selected"); - - // Реинициализация строки - row.outerHTML = data.row; - - // Реинициализация строки (выражение странное, но правильное) - row = document.getElementById(row.getAttribute("id")); - - // Копирование классов из буфера классов удалённой строки - // row.classList.add(...buffer); - - // Копирование статуса активной строки - if ( - typeof selected === "string" && selected === "true" && - document.body.contains(document.getElementById("popup")) && - !document.body.contains( - document.querySelector('[data-selected="true"]'), - ) - ) { - row.setAttribute("data-selected", "true"); - } - - if (data.confirmed) { - // Подтверждена заявка - - // Реинициализация текста - button.innerText = "Отклонить"; - - // Реинициализация стиля - button.classList.remove("grass"); - button.classList.add("clay"); - - // УЛОВКА: браузер ещё не успевает перерисовать документ во время исполнения this.task(row) - row.classList.add("confirmed"); - } else { - // Не подтверждена заявка - - // Реинициализация текста - button.innerText = "Подтвердить"; - - // Реинициализация стиля - button.classList.remove("clay"); - button.classList.add("grass"); - - // УЛОВКА: браузер ещё не успевает перерисовать документ во время исполнения this.task(row) - row.classList.remove("confirmed"); - } - - if (this.body.wrap instanceof HTMLElement) { - // Найдено активное окно - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.buttons); - - // Сброс блокировки - this.freeze = false; - - // Реинициализация активного окна - tasks.popup(row); - } - } - }); - } - } - }, 300); - - /** - * Сгенерировать всплывающее окно c заявлением о проблеме - * - * @param {HTMLElement} row Строка - * - * @return {void} - */ - static problem = damper((row) => { - if (row instanceof HTMLElement) { - // Получена строка - - if (row.classList.contains("problematic")) { - // Проблемная заявка - - // Снятие статуса проблемной заявки - this.__problem(null, null, row); - } else { - // Не проблемная заявка - - // Инициализация идентификатора сотрудника - const task = row.getAttribute("id"); - - // Инициализация буфера выбранных строк - const buffer = [ - ...document.querySelectorAll('div[data-selected="true"]'), - ]; - - // Удаление статуса активной строки у остальных строк - for (const element of buffer) { - element.removeAttribute("data-selected"); - } - - // Инициализация статуса активной строки (обрабатываемой в данный момент) - row.setAttribute("data-selected", "true"); - - // Инициализация оболочки всплывающего окна - this.body.wrap = document.createElement("div"); - this.body.wrap.setAttribute("id", "popup"); - - // Инициализация всплывающего окна - const popup = document.createElement("section"); - popup.classList.add("list", "small"); - - // Инициализация заголовка всплывающего окна - const title = document.createElement("h3"); - title.classList.add("unselectable"); - title.innerText = "Заявление о проблеме"; - - // Инициализация оболочки с основной информацией - const main = document.createElement("section"); - main.classList.add("main"); - - // Инициализация колонки - const column = document.createElement("div"); - column.classList.add("column"); - - // Инициализация поля ввода отзыва - const problem = document.createElement("textarea"); - problem.classList.add("snow"); - problem.setAttribute("autofocus", "true"); - problem.setAttribute("maxlength", "300"); - problem.setAttribute("title", "Проблема"); - problem.setAttribute( - "placeholder", - "Описание проблемы", - ); - - // Инициализация оболочки для кнопок - const buttons = document.createElement("div"); - buttons.classList.add("row", "buttons"); - - // Инициализация кнопки подтверждения - const send = document.createElement("button"); - send.classList.add("grass", "stretched"); - send.innerText = "Отправить"; - send.setAttribute( - "onclick", - `chat.send(this, this.parentElement.previousElementSibling, document.getElementById('${task}'), 'market', 'problem', true); document.removeEventListener("keydown", tasks.buttons);`, - ); - - // Инициализация окна с ошибками - this.body.errors = document.createElement("section"); - this.body.errors.classList.add( - "errors", - "window", - "list", - "small", - "hidden", - ); - this.body.errors.setAttribute("data-errors", true); - - // Инициализация элемента-тела (оболочки) окна с ошибками - const errors = document.createElement("section"); - errors.classList.add("body"); - - // Инициализация элемента-списка ошибок - const dl = document.createElement("dl"); - - // Инициализация активного всплывающего окна - const old = document.getElementById("popup"); - - if (old instanceof HTMLElement) { - // Найдено активное окно - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.buttons); - - // Сброс блокировки - this.freeze = false; - - // Удаление активного окна - old.remove(); - } - - // Удаление блокировки выполнения закрытия всплывающего окна - this.freeze = false; - - // Запись в документ - popup.appendChild(title); - - column.appendChild(problem); - buttons.appendChild(send); - column.appendChild(buttons); - - main.appendChild(column); - popup.appendChild(main); - this.body.wrap.appendChild(popup); - - errors.appendChild(dl); - this.body.errors.appendChild(errors); - this.body.wrap.appendChild(this.body.errors); - - document.body.appendChild(this.body.wrap); - - // Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup) - function top(errors) { - errors.style.setProperty( - "transition", - "0s", - ); - errors.style.setProperty( - "--top", - popup.offsetTop + popup.offsetHeight + 12 + "px", - ); - setTimeout( - () => errors.style.removeProperty("transition"), - 100, - ); - } - top(this.body.errors); - const resize = new ResizeObserver(() => top(this.body.errors)); - resize.observe(this.body.wrap); - - // Инициализация функции закрытия всплывающего окна - const click = () => { - // Блокировка - if (this.freeze) return; - - // Удаление всплывающего окна - this.body.wrap.remove(); - - // Инициализация буфера выбранных строк - const buffer = [ - ...document.querySelectorAll('div[data-selected="true"]'), - ]; - - // Удаление статуса активной строки у остальных строк - for (const element of buffer) { - element.removeAttribute("data-selected"); - } - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.buttons); - - // Сброс блокировки - this.freeze = false; - }; - - // Инициализация функции добавления функции закрытия всплывающего окна - const enable = () => this.body.wrap.addEventListener("click", click); - - // Инициализация функции удаления функции закрытия всплывающего окна - const disable = () => - this.body.wrap.removeEventListener("click", click); - - // Первичная активация функции удаления всплывающего окна - enable(); - - // Инициализация блокировки удаления окна при взаимодействии с select-элементом - for (const select of popup.getElementsByTagName("select")) { - // Перебор всех select-элементов - - // Инициализация функции блокировки удаления окна по событию - select.addEventListener("click", () => { - // Блокировка удаления окна - this.freeze = true; - - // Разблокировка удаления окна - setTimeout(() => this.freeze = false, 100); - }); - - for (const option of select.getElementsByTagName("option")) { - // Перебор всех option-элементов - - // Инициализация функции блокировки удаления окна по событию - option.addEventListener("click", () => { - // Блокировка удаления окна - this.freeze = true; - - // Разблокировка удаления окна - setTimeout(() => this.freeze = false, 100); - }); - } - } - - // Инициализация блокировки удаления окна при взаимодействии с textarea-элементом - for (const textarea of popup.getElementsByTagName("textarea")) { - // Перебор всех textarea-элементов - - // Обновлять позицию окна с ошибками при изменении размера textarea (там position: absolute) - new MutationObserver(() => top(this.body.errors)).observe( - textarea, - { - attributes: true, - attributeFilter: ["style"], - }, - ); - - // Инициализация функции игнорирования блокировки для выбранных кнопок - textarea.addEventListener("keydown", (e) => { - if (e.keyCode === 27) { - // Нажата кнопка: "escape" - - // Вызов глобальной функции управления кнопками - this.buttons(e, true); - } else if (e.ctrlKey && e.keyCode === 13) { - // Нажаты кнопки: "control", "enter" - - // Вызов глобальной функции управления кнопками - this.buttons(e, true); - } - }); - - // Добавление функции блокировки удаления окна и клавиш по событиям - textarea.addEventListener("focus", () => this.freeze = true); - textarea.addEventListener( - "focusout", - () => this.freeze = false, - ); - } - - // Инициализация функции управления кнопками - this.buttons = (e, force = false) => { - // Блокировка - if (!force && this.freeze) return; - - if (e.keyCode === 27) { - // Нажата кнопка: "escape" - - // Удаление окна - click(); - } else if (event.keyCode === 13) { - // Нажата кнопка: "enter" - - // Инициализация буфера с текущим статусом блокировки закрытия окна - const freeze = this.freeze; - - // Блокировка закрытия окна (чтобы не вызвался click() через событие onclick) - this.freeze = true; - - // Активация виртуальной кнопки "отправить" - send.click(); - - // Возвращение статуса блокировки закрытия окна - this.freeze = freeze; - } - }; - - // Инициализация быстрых действий по кнопкам - document.addEventListener("keydown", this.buttons); - - // Добавление функции удаления всплывающего окна по событиям - popup.addEventListener("mouseenter", disable); - popup.addEventListener("mouseleave", enable); - - // Фокусировка - problem.focus(); - } - } - }, 300); - - /** - * Проблемная заявка (вызов демпфера) - * - * Изменить статус заявки на проблемную и не проблемную - * Если изменяется статус на проблемную, то необходимо передать текст сообщения - * - * @param {HTMLElement} button Кнопка - * @param {HTMLElement} textarea Поле для ввода описания проблемы - * @param {HTMLElement} row Строка - * - * @return {void} - */ - static _problem(button, textarea, row) { - // Блокировка полей ввода - textarea.setAttribute("readonly", true); - - // Блокировка кнопки - button.setAttribute("disabled", true); - - // Деинициализация индикатора и анимации об ошибке - textarea.classList.remove("error"); - - // Сброс анимации - this.body.errors.classList.add("hidden"); - - // Запуск выполнения - this.__problem(button, textarea, row); - } - - /** - * Проблемная заявка (демпфер) - * - * Изменить статус заявки на проблемную и не проблемную - * Если изменяется статус на проблемную, то необходимо передать текст сообщения - * - * @param {HTMLElement} button Кнопка - * @param {HTMLElement} textarea Поле для ввода описания проблемы - * @param {HTMLElement} row Строка - * - * @return {void} - */ - static __problem = damper(async (button, textarea, row) => { - if (row instanceof HTMLElement) { - // Получена строка - - // Инициализация идентификатора строки - const id = row.getAttribute("id"); - - if (typeof id === "string") { - // Инициализирован идентификатор - - // Инициализация функции разблокировки - function unblock() { - // Разблокировка поля ввода - textarea.removeAttribute("readonly"); - - // Разблокировка кнопки - button.removeAttribute("disabled"); - } - - // Запуск отсрочки разблокировки на случай, если сервер не отвечает - const timeout = setTimeout(() => { - this.errors(["Сервер не отвечает"]); - unblock(); - }, 5000); - - // Запрос к серверу - return await fetch(`/task/${id}/problem`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: textarea instanceof HTMLElement && textarea.value.length > 0 - ? `text=${textarea.value}` - : "", - }) - .then((response) => response.json()) - .then((data) => { - // Удаление отсрочки разблокировки - clearTimeout(timeout); - - if (this.errors(data.errors)) { - // Сгенерированы ошибки - - // Инициализация отображения ошибки - textarea.classList.add("error"); - - // Фокусировка на поле ввода - textarea.focus(); - - // Разблокировка полей ввода и кнопок - unblock(); - } else { - // Не сгенерированы ошибки (подразумевается их отсутствие) - - // Инициализация буфера списка классов - // const buffer = [...row.classList]; - - // Инициализация статуса активной строки - const selected = row.getAttribute("data-selected"); - - // Реинициализация строки - row.outerHTML = data.row; - - // Реинициализация строки (выражение странное, но правильное) - row = document.getElementById(row.getAttribute("id")); - - // Копирование классов из буфера классов удалённой строки - // row.classList.add(...buffer); - - // Копирование статуса активной строки - if ( - typeof selected === "string" && selected === "true" && - document.body.contains(document.getElementById("popup")) && - !document.body.contains( - document.querySelector('[data-selected="true"]'), - ) - ) { - row.setAttribute("data-selected", "true"); - } - - if (data.problematic) { - // Проблемная заявка - - // УЛОВКА: браузер ещё не успевает перерисовать документ во время исполнения this.task(row) - row.classList.add("problematic"); - } else { - // Не проблемная заявка - - // УЛОВКА: браузер ещё не успевает перерисовать документ во время исполнения this.task(row) - row.classList.remove("problematic"); - } - - if (this.body.wrap instanceof HTMLElement) { - // Найдено активное окно - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.buttons); - - // Сброс блокировки - this.freeze = false; - - // Деинициализация активного окна - this.body.wrap.remove(); - - // Удаление статуса активной строки - row.removeAttribute("data-selected"); - } - } - }); - } - } - }, 300); - - /** - * Подтверждение заявки (вызов демпфера) - * - * @param {HTMLElement} button Кнопка - * @param {HTMLElement} rating Рейтинг - * @param {HTMLElement} review Отзыв - * @param {HTMLElement} row Строка - * - * @return {void} - */ - static complete(button, rating, review, row) { - // Блокировка ползунка - rating.setAttribute("disabled", true); - - // Блокировка полей ввода - review.setAttribute("readonly", true); - - // Деинициализация индикатора и анимации об ошибке - rating.classList.remove("error"); - review.classList.remove("error"); - - // Блокировка кнопки - button.setAttribute("disabled", true); - - // Сброс анимации - this.body.errors.classList.add("hidden"); - - // Запуск выполнения - this._complete(button, rating, review, row); - } - - /** - * Подтверждение заявки (демпфер) - * - * @param {HTMLElement} button Кнопка - * @param {HTMLElement} rating Рейтинг - * @param {HTMLElement} review Отзыв - * @param {HTMLElement} row Строка - * - * @return {void} - */ - static _complete = damper(async (button, rating, review, row) => { - if ( - button instanceof HTMLElement && rating instanceof HTMLElement && - review instanceof HTMLElement && row instanceof HTMLElement - ) { - // Получена кнопка, рейтинг, отзыв и строка - - // Инициализация идентификатора строки - const id = row.getAttribute("id"); - - if (typeof id === "string") { - // Инициализирован идентификатор - - // Инициализация функции разблокировки - function unblock() { - // Разблокировка ползунка - rating.removeAttribute("disabled"); - - // Разблокировка поля ввода - review.removeAttribute("readonly"); - - // Разблокировка кнопки - button.removeAttribute("disabled"); - } - - // Запуск отсрочки разблокировки на случай, если сервер не отвечает - const timeout = setTimeout(() => { - this.errors(["Сервер не отвечает"]); - unblock(); - }, 5000); - - // Запрос к серверу - return await fetch(`/task/${id}/complete`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: `rating=${rating.value}&review=${review.value}`, - }) - .then((response) => response.json()) - .then((data) => { - // Удаление отсрочки разблокировки - clearTimeout(timeout); - - if (this.errors(data.errors)) { - // Сгенерированы ошибки - - // Инициализация отображения ошибки - rating.classList.add("error"); - review.classList.add("error"); - - // Фокусировка на поле ввода - review.focus(); - - if (!data.completed) { - // Не завершена заявка - - // Разблокировка полей ввода и кнопок - unblock(); - } - } else { - // Не сгенерированы ошибки (подразумевается их отсутствие) - - // Инициализация буфера списка классов - // const buffer = [...row.classList]; - - // Инициализация статуса активной строки - const selected = row.getAttribute("data-selected"); - - // Реинициализация строки - row.outerHTML = data.row; - - // Реинициализация строки (выражение странное, но правильное) - row = document.getElementById(row.getAttribute("id")); - - // Копирование классов из буфера классов удалённой строки - // row.classList.add(...buffer); - - // Копирование статуса активной строки - if ( - typeof selected === "string" && selected === "true" && - document.body.contains(document.getElementById("popup")) && - !document.body.contains( - document.querySelector('[data-selected="true"]'), - ) - ) { - row.setAttribute("data-selected", "true"); - } - - if (data.completed) { - // Завершена заявка - - // УЛОВКА: браузер ещё не успевает перерисовать документ во время исполнения this.task(row) - row.classList.add("completed"); - } else { - // Не завершена заявка - - // УЛОВКА: браузер ещё не успевает перерисовать документ во время исполнения this.task(row) - row.classList.remove("completed"); - } - - if (this.body.wrap instanceof HTMLElement) { - // Найдено активное окно - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.buttons); - - // Сброс блокировки - this.freeze = false; - - // Деинициализация активного окна - this.body.wrap.remove(); - - // Удаление статуса активной строки - row.removeAttribute("data-selected"); - } - } - }); - } - } - }, 300); - - /** - * Скрыть - * - * @param {HTMLElement} button Кнопка - * @param {HTMLElement} row Строка - * - * @return {void} - */ - static hide = damper(async (button, row) => { - if (row instanceof HTMLElement && button instanceof HTMLElement) { - // Получена кнопка и строка - - // Инициализация идентификатора строки - const id = row.getAttribute("id"); - - if (typeof id === "string") { - // Инициализирован идентификатор - - // Запрос к серверу - return await fetch(`/task/${id}/hide`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }) - .then((response) => response.json()) - .then((data) => { - if (this.errors(data.errors)) { - // Сгенерированы ошибки - } else { - // Не сгенерированы ошибки (подразумевается их отсутствие) - - // Инициализация буфера списка классов - // const buffer = [...row.classList]; - - // Инициализация статуса активной строки - const selected = row.getAttribute("data-selected"); - - // Реинициализация строки - row.outerHTML = data.row; - - // Реинициализация строки (выражение странное, но правильное) - row = document.getElementById(row.getAttribute("id")); - - // Копирование классов из буфера классов удалённой строки - // row.classList.add(...buffer); - - // Копирование статуса активной строки - if ( - typeof selected === "string" && selected === "true" && - document.body.contains(document.getElementById("popup")) && - !document.body.contains( - document.querySelector('[data-selected="true"]'), - ) - ) { - row.setAttribute("data-selected", "true"); - } - - if (data.hided) { - // Скрыта заявка - - // Реинициализация текста - button.innerText = "Показать"; - - // Реинициализация стиля - button.classList.remove("sea"); - button.classList.add("grass"); - - // УЛОВКА: браузер ещё не успевает перерисовать документ во время исполнения this.task(row) - row.classList.add("hided"); - } else { - // Не скрыта заявка - - // Реинициализация текста - button.innerText = "Скрыть"; - - // Реинициализация стиля - button.classList.remove("grass"); - button.classList.add("sea"); - - // УЛОВКА: браузер ещё не успевает перерисовать документ во время исполнения this.task(row) - row.classList.remove("hided"); - } - - if (this.body.wrap instanceof HTMLElement) { - // Найдено активное окно - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.buttons); - - // Сброс блокировки - this.freeze = false; - - // Реинициализация активного окна - tasks.popup(row); - } - } - }); - } - } - }, 300); - - /** - * Удалить - * - * @param {HTMLElement} button Кнопка - * @param {HTMLElement} row Строка - * - * @return {void} - */ - static remove = damper(async (button, row) => { - if (row instanceof HTMLElement && button instanceof HTMLElement) { - // Получена кнопка и строка - - // Инициализация идентификатора строки - const id = row.getAttribute("id"); - - alert(228); - - if (typeof id === "string") { - // Инициализирован идентификатор - - // Запрос к серверу - return await fetch(`/task/${id}/remove`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }) - .then((response) => response.json()) - .then((data) => { - if (this.errors(data.errors)) { - // Сгенерированы ошибки - } else { - // Не сгенерированы ошибки (подразумевается их отсутствие) - - if (data.deleted) { - // Удалена заявка - - if (this.body.wrap instanceof HTMLElement) { - // Найдено активное окно - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.buttons); - - // Сброс блокировки - this.freeze = false; - - // Деинициализация активного окна - this.body.wrap.remove(); - - // Удаление статуса активной строки - row.removeAttribute("data-selected"); - } - - // Удаление строки - row.remove(); - } - } - }); - } - } - }, 300); - - /** - * Прочитать значение из базы данных - * - * @param {HTMLElement} row Строка - * @param {string} name Название - * - * @return {string} Значение - */ - static async value(row, name) { - if (row instanceof HTMLElement && typeof name === "string") { - // Получена строка и название - - // Инициализация идентификатора строки - const id = row.getAttribute("id"); - - if (typeof id === "string") { - // Инициализирован идентификатор - - // Запрос к серверу - return await fetch(`/task/${id}/value`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: `name=${name}`, - }) - .then((response) => response.json()) - .then((data) => { - if (this.errors(data.errors)) { - // Сгенерированы ошибки - } else { - // Не сгенерированы ошибки (подразумевается их отсутствие) - - return data.value; - } - }); - } - } - } - - /** - * Сгенерировать список работ и выбрать в нём ту, что записана в базе данных у заявки - * - * @param {HTMLElement} row Строка - * - * @return {array|null} Массив HTML-элементов - */ - static async works(row) { - if (row instanceof HTMLElement) { - // Получена строка - - // Инициализация идентификатора строки - 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; - } - }); - } - } - } - - /** - * Записать тип работы - * - * @param {HTMLElement} row Строка - * @param {HTMLElement} select HTML-элемент - * - * @return {void} - */ - static work = damper(async (row, select) => { - if (row instanceof HTMLElement && select instanceof HTMLElement) { - // Получена строка и select-элемент - - // Инициализация идентификатора строки - const id = row.getAttribute("id"); - - if (typeof id === "string") { - // Инициализирован идентификатор - - // Запрос к серверу - return await fetch(`/task/${id}/work`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: `work=${select.value}`, - }) - .then((response) => response.json()) - .then((data) => { - if (this.errors(data.errors)) { - // Сгенерированы ошибки - } else { - // Не сгенерированы ошибки (подразумевается их отсутствие) - - if (data.writed) { - // Записано новое значение в базу данных - - // Инициализация буфера списка классов - // const buffer = [...row.classList]; - - // Инициализация статуса активной строки - const selected = row.getAttribute("data-selected"); - - // Реинициализация строки в документе - row.outerHTML = data.row; - - // Реинициализация строки (выражение странное, но правильное) - row = document.getElementById(row.getAttribute("id")); - - // Копирование классов из буфера классов удалённой строки - // row.classList.add(...buffer); - - // Копирование статуса активной строки - if ( - typeof selected === "string" && selected === "true" && - document.body.contains(document.getElementById("popup")) && - !document.body.contains( - document.querySelector('[data-selected="true"]'), - ) - ) { - row.setAttribute("data-selected", "true"); - } - } - - if (this.body.wrap instanceof HTMLElement) { - // Найдено активное окно - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.buttons); - - // Сброс блокировки - this.freeze = false; - - // Реинициализация активного окна - tasks.popup(row); - } - } - }); - } - } - }, 300); - - /** - * Записать дату - * - * @param {HTMLElement} row Строка - * @param {HTMLElement} date Дата - * @param {HTMLElement} start Время начала - * @param {HTMLElement} end Время окончания - * @param {HTMLElement} button Кнопка отправки - * - * @return {void} - */ - static date(row, date, start, end, button) { - // Блокировка полей ввода - date.setAttribute("readonly", true); - start.setAttribute("readonly", true); - end.setAttribute("readonly", true); - button.setAttribute("disabled", true); - - // Запуск выполнения - this._date(row, date, start, end, button); - } - - /** - * Записать дату - * - * @param {HTMLElement} row Строка - * @param {HTMLElement} date Дата - * @param {HTMLElement} start Время начала - * @param {HTMLElement} end Время окончания - * @param {HTMLElement} button Кнопка отправки - * - * @return {void} - */ - static _date = damper(async (row, date, start, end, button) => { - if ( - row instanceof HTMLElement && date instanceof HTMLElement && - start instanceof HTMLElement && end instanceof HTMLElement && - button instanceof HTMLElement - ) { - // Получены строка, поле даты, поле начала заявки, поле окончания заяки и кнопка - - // Инициализация идентификатора строки - const id = row.getAttribute("id"); - - if (typeof id === "string") { - // Инициализирован идентификатор - - // Запрос к серверу - return await fetch(`/task/${id}/date`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: `date=${ - date.valueAsDate / 1000 - }&start=${start.value}&end=${end.value}`, - }) - .then((response) => response.json()) - .then((data) => { - if (this.errors(data.errors)) { - // Сгенерированы ошибки - - // Разблокировка ввода - date.removeAttribute("readonly"); - start.removeAttribute("readonly"); - end.removeAttribute("readonly"); - button.removeAttribute("disabled"); - } else { - // Не сгенерированы ошибки (подразумевается их отсутствие) - - if (data.writed) { - // Записано новое значение в базу данных - - // Инициализация буфера списка классов - // const buffer = [...row.classList]; - - // Инициализация статуса активной строки - const selected = row.getAttribute("data-selected"); - - // Реинициализация строки в документе - row.outerHTML = data.row; - - // Реинициализация строки (выражение странное, но правильное) - row = document.getElementById(row.getAttribute("id")); - - // Копирование классов из буфера классов удалённой строки - // row.classList.add(...buffer); - - // Копирование статуса активной строки - if ( - typeof selected === "string" && selected === "true" && - document.body.contains(document.getElementById("popup")) && - !document.body.contains( - document.querySelector('[data-selected="true"]'), - ) - ) { - row.setAttribute("data-selected", "true"); - } - - if (this.body.wrap instanceof HTMLElement) { - // Найдено активное окно - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.buttons); - - // Сброс блокировки - this.freeze = false; - - // Реинициализация активного окна - tasks.popup(row); - } - } else { - // Не записано новое значение в базу данных - - // Разблокировка ввода - date.removeAttribute("readonly"); - start.removeAttribute("readonly"); - end.removeAttribute("readonly"); - button.removeAttribute("disabled"); - } - } - }); - } - } - }, 300); - - /** - * Записать описание - * - * @param {HTMLElement} row Строка - * @param {HTMLElement} textarea HTML-элемент - * - * @return {void} - */ - static description(row, textarea) { - // Сброс анимации ошибки - textarea.classList.remove("error"); - - // Запуск выполнения - this._description(row, textarea); - } - - /** - * Записать описание (системное) - * - * @param {HTMLElement} row Строка - * @param {HTMLElement} textarea HTML-элемент - * - * @return {void} - */ - static _description = damper(async (row, textarea) => { - if (row instanceof HTMLElement && textarea instanceof HTMLElement) { - // Получена строка и select-элемент - - // Инициализация идентификатора строки - const id = row.getAttribute("id"); - - if (typeof id === "string") { - // Инициализирован идентификатор - - // Запрос к серверу - return await fetch(`/task/${id}/description`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: `description=${textarea.value}`, - }) - .then((response) => response.json()) - .then((data) => { - if (this.errors(data.errors)) { - // Сгенерированы ошибки - - // Запись анимации ошибки - textarea.classList.add("error"); - } else { - // Не сгенерированы ошибки (подразумевается их отсутствие) - - if (data.writed) { - // Записано новое значение в базу данных - - // Удаление анимации ошибки - textarea.classList.remove("error"); - } else { - // Не записано новое значение в базу данных - - // Запись анимации ошибки - textarea.classList.add("error"); - } - } - }); - } - } - }, 300); - - /** - * Сгенерировать HTML-элемент со списком ошибок - * - * @param {object} registry Реестр ошибок - * @param {bool} render Отобразить в окне с ошибками? - * @param {bool} clean Очистить окно с ошибками перед добавлением? - * - * @return {bool} Сгенерированы ошибки? - */ - static errors(registry, render = true, clean = true) { - // Инициализация ссылки на HTML-элемент с ошибками - const wrap = document.body.contains(this.body.errors) - ? this.body.errors - : document.querySelector('[data-errors="true"]'); - - if (wrap instanceof HTMLElement && document.body.contains(wrap)) { - // Найден HTML-элемент с ошибками - - // Перерасчёт высоты элемента - function height() { - wrap.classList.remove("hidden"); - wrap.classList.remove("animation"); - // Реинициализация переменной с данными о высоте HTML-элемента (16 - это padding-top + padding-bottom у div#popup > section.errors) - wrap.style.setProperty( - "--height", - wrap.offsetHeight - 16 + "px", - ); - wrap.classList.add("animation"); - wrap.classList.add("hidden"); - } - - // Инициализация элемента-списка ошибок - const list = wrap.getElementsByTagName("dl")[0]; - - // Удаление ошибок из прошлой генерации - if (clean) list.innerHTML = null; - - for (const error in registry) { - // Генерация HTML-элементов с текстами ошибок - - // Инициализация HTML-элемента текста ошибки - const samp = document.createElement("samp"); - - if (typeof registry[error] === "object") { - // Категория ошибок - - // Проверка наличия ошибок - if (registry[error].length === 0) continue; - - // Инициализация HTML-элемента-оболочки - const wrap = document.createElement("dt"); - - // Запись текста категории - samp.innerText = error; - - // Запись HTML-элементов в список - wrap.appendChild(samp); - list.appendChild(wrap); - - // Реинициализация высоты - height(); - - // Обработка вложенных ошибок (вход в рекурсию) - this.errors(registry[error], false); - } else { - // Текст ошибки (подразумевается) - - // Инициализация HTML-элемента - const wrap = document.createElement("dd"); - - // Запись текста ошибки - samp.innerText = registry[error]; - - // Запись HTML-элемента в список - wrap.appendChild(samp); - list.appendChild(wrap); - - // Реинициализация высоты - height(); - } - } - - if (render) { - // Запрошена отрисовка - - if ( - list.childElementCount !== 0 - ) { - // Найдены ошибки - - // Сброс анимации - // УЛОВКА: таким образом не запускается анимация до взаимодействия с элементом (исправлял это в CSS, но не помню как) - wrap.classList.add("animation"); - - // Отображение - wrap.classList.remove("hidden"); - } else { - // Не найдены ошибки - - // Скрытие - wrap.classList.add("hidden"); - } - } - - return list.childElementCount === 0 ? false : true; - } - - return false; - } - - /** - * Сотрудник - */ - static worker = class { - /** - * Ссылка на ядро (родительский класс) - */ - static core; - - /** - * Сгенерировать всплывающее окно c данными сотрудника - * - * @param {HTMLElement} row Строка - * - * @return {void} - */ - static async popup(row) { - if ( - row instanceof HTMLElement && - (typeof core === "function" && - core.interface !== "worker") - ) { - // Получена строка и аккаунт не является сотрудником - - // Инициализация идентификатора сотрудника - const worker = - row.querySelector('span[data-column="worker"]').innerText; - - if (worker.length > 0) { - // Инициализирован идентификатор сотрудника - - // Инициализация идентификатора строки - const task = row.getAttribute("id"); - - if (task.length > 0) { - // Инициализирован идентификатор строки - - // Запрос к серверу - return await fetch(`/worker/${worker}/read`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }) - .then((response) => response.json()) - .then((data) => { - if (this.core.errors(data.errors)) { - // Сгенерированы ошибки - } else { - // Не сгенерированы ошибки (подразумевается их отсутствие) - - if (data.worker.length > 0) { - // Получены данные (подразумевается, что сотрудник найден) - - // Инициализация буфера выбранных строк - const buffer = [ - ...document.querySelectorAll( - 'div[data-selected="true"]', - ), - ]; - - // Удаление статуса активной строки у остальных строк - for (const element of buffer) { - element.removeAttribute("data-selected"); - } - - // Инициализация статуса активной строки (обрабатываемой в данный момент) - row.setAttribute("data-selected", "true"); - - // Инициализация оболочки всплывающего окна - this.core.body.wrap = document.createElement("div"); - this.core.body.wrap.setAttribute("id", "popup"); - - // Инициализация всплывающего окна - const popup = document.createElement("section"); - popup.classList.add("window", "list", "small"); - - // Инициализация заголовка всплывающего окна - const title = document.createElement("h3"); - title.innerText = worker; - - // Инициализация оболочки с основной информацией - const main = document.createElement("section"); - main.classList.add("main"); - - // Инициализация колонки - const column = document.createElement("div"); - column.classList.add("column"); - - // Инициализация буфера для записи во всплывающее окно - const list = document.createElement("div"); - - // Инициализация оболочки для кнопок - const buttons = document.createElement("div"); - buttons.classList.add("row", "divided", "buttons"); - - // Инициализация оболочки для строки с полем ввода - const label = document.createElement("label"); - - // Инициализация иконки для поля ввода замены - const icon = document.createElement("i"); - icon.classList.add("icon", "user"); - - // Инициализация поля ввода замены - const input = document.createElement("input"); - input.classList.add("cloud"); - input.setAttribute("placeholder", "Сотрудник"); - input.setAttribute("autocomplete", "username"); - input.setAttribute("autofocus", "true"); - input.setAttribute("title", "Идентификатор сотрудника"); - input.setAttribute("list", "popup_worker_datalist"); - - // Инициализация кнопки смены - const replace = document.createElement("button"); - replace.classList.add("sea"); - replace.innerText = "Заменить"; - replace.setAttribute( - "onclick", - `tasks.worker.update(document.getElementById('${task}'), this.previousElementSibling, this)`, - ); - - // Инициализация списка сотружников - const datalist = document.createElement("datalist"); - datalist.setAttribute("id", "popup_worker_datalist"); - workers.list().then((html) => datalist.innerHTML = html); - - // Инициализация кнопки удаления - const remove = document.createElement("button"); - remove.classList.add("clay"); - remove.innerText = "Удалить"; - remove.setAttribute( - "onclick", - `tasks.worker.remove(document.getElementById('${task}'), this)`, - ); - - // Инициализация окна с ошибками - this.core.body.errors = document.createElement("section"); - this.core.body.errors.classList.add( - "errors", - "window", - "list", - "small", - "hidden", - ); - this.core.body.errors.setAttribute("data-errors", true); - - // Инициализация элемента-тела (оболочки) окна с ошибками - const errors = document.createElement("section"); - errors.classList.add("body"); - - // Инициализация элемента-списка ошибок - const dl = document.createElement("dl"); - - // Инициализация активного всплывающего окна - const old = document.getElementById("popup"); - - if (old instanceof HTMLElement) { - // Найдено активное окно - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener( - "keydown", - this.core.buttons, - ); - - // Сброс блокировки - this.core.freeze = false; - - // Удаление активного окна - old.remove(); - } - - // Запись в документ - popup.appendChild(title); - - label.appendChild(icon); - label.appendChild(input); - label.appendChild(replace); - label.appendChild(remove); - label.appendChild(datalist); - buttons.appendChild(label); - column.appendChild(list); - column.appendChild(buttons); - - main.appendChild(column); - popup.appendChild(main); - this.core.body.wrap.appendChild(popup); - - errors.appendChild(dl); - this.core.body.errors.appendChild(errors); - this.core.body.wrap.appendChild(this.core.body.errors); - - document.body.appendChild(this.core.body.wrap); - - // Запись в документ HTML-данных через буфер - list.outerHTML = data.worker; - - // Инициализация элемента с номером в списке данных сотрудника - const number = document.getElementById(`${worker}_number`) - .getElementsByTagName("a")[0]; - - // Инициализация маски номера - const mask = IMask.createMask({ - mask: "+{7} (000) 000-00-00", - }); - - // Применение маски номера - mask.resolve(number.innerText); - - // Запись нового значения - number.innerText = mask.value; - - // Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup) - function top(errors) { - errors.style.setProperty( - "transition", - "0s", - ); - errors.style.setProperty( - "--top", - popup.offsetTop + popup.offsetHeight + 12 + "px", - ); - setTimeout( - () => errors.style.removeProperty("transition"), - 100, - ); - } - top(this.core.body.errors); - const resize = new ResizeObserver(() => - top(this.core.body.errors) - ); - resize.observe(this.core.body.wrap); - - // Инициалиация маски идентификатора сотрудника в поле ввода идентификатора сотрудника - IMask(input, { mask: "000000000" }); - - // Инициализация функции закрытия всплывающего окна - const click = () => { - // Блокировка - if (this.core.freeze) return; - - // Удаление всплывающего окна - this.core.body.wrap.remove(); - - // Инициализация буфера выбранных строк - const buffer = [ - ...document.querySelectorAll( - 'div[data-selected="true"]', - ), - ]; - - // Удаление статуса активной строки у остальных строк - for (const element of buffer) { - element.removeAttribute("data-selected"); - } - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener( - "keydown", - this.core.buttons, - ); - - // Сброс блокировки - this.core.freeze = false; - }; - - // Инициализация функции добавления функции закрытия всплывающего окна - const enable = () => - this.core.body.wrap.addEventListener("click", click); - - // Инициализация функции удаления функции закрытия всплывающего окна - const disable = () => - this.core.body.wrap.removeEventListener("click", click); - - // Первичная активация функции удаления всплывающего окна - enable(); - - // Инициализация блокировки удаления окна при взаимодействии с select-элементом - for ( - const select of popup.getElementsByTagName("select") - ) { - // Перебор всех select-элементов - - // Инициализация функции блокировки удаления окна по событию - select.addEventListener("click", () => { - // Блокировка удаления окна - this.core.freeze = true; - - // Разблокировка удаления окна - setTimeout(() => this.core.freeze = false, 100); - }); - - for ( - const option of select.getElementsByTagName("option") - ) { - // Перебор всех option-элементов - - // Инициализация функции блокировки удаления окна по событию - option.addEventListener("click", () => { - // Блокировка удаления окна - this.core.freeze = true; - - // Разблокировка удаления окна - setTimeout(() => this.core.freeze = false, 100); - }); - } - } - - // Инициализация блокировки удаления окна при взаимодействии с textarea-элементом - for ( - const textarea of popup.getElementsByTagName("textarea") - ) { - // Перебор всех textarea-элементов - - // Обновлять позицию окна с ошибками при изменении размера textarea (там position: absolute) - new MutationObserver(() => top(this.core.body.errors)) - .observe(textarea, { - attributes: true, - attributeFilter: ["style"], - }); - - // Инициализация функции игнорирования блокировки для выбранных кнопок - textarea.addEventListener("keydown", (e) => { - if (e.keyCode === 27) { - // Нажата кнопка: "escape" - - // Вызов глобальной функции управления кнопками - this.core.buttons(e, true); - } else if (e.ctrlKey && e.keyCode === 13) { - // Нажаты кнопки: "control", "enter" - - // Вызов глобальной функции управления кнопками - this.core.buttons(e, true); - } - }); - - // Добавление функции блокировки удаления окна и клавиш по событиям - textarea.addEventListener( - "focus", - () => this.core.freeze = true, - ); - textarea.addEventListener( - "focusout", - () => this.core.freeze = false, - ); - } - - // Инициализация функции управления кнопками - this.core.buttons = (e, force = false) => { - // Блокировка - if (!force && this.core.freeze) return; - - if (e.keyCode === 27) { - // Нажата кнопка: "escape" - - // Удаление окна - click(); - } else if (event.keyCode === 13) { - // Нажата кнопка: "enter" - - // Инициализация буфера с текущим статусом блокировки закрытия окна - const freeze = this.core.freeze; - - // Блокировка закрытия окна (чтобы не вызвался click() через событие onclick) - this.core.freeze = true; - - // Активация виртуальной кнопки "заменить" - replace.click(); - - // Возвращение статуса блокировки закрытия окна - this.core.freeze = freeze; - } - }; - - // Инициализация быстрых действий по кнопкам - document.addEventListener("keydown", this.core.buttons); - - // Добавление функции удаления всплывающего окна по событиям - popup.addEventListener("mouseenter", disable); - popup.addEventListener("mouseleave", enable); - - // Фокусировка - input.focus(); - } else { - // Не получены данные (подразумевается, что сотрудник не найден) - - // Открытие окна инициализации сотрудника - this.init(row); - } - } - }); - } - } else { - // Не инициализирован идентификатор сотрудника - - // Открытие окна инициализации сотрудника - this.init(row); - } - } - } - - static init(row) { - // Инициализация идентификатора строки - const task = row.getAttribute("id"); - - if (task.length > 0) { - // Инициализирован идентификатор строки - - // Инициализация статуса: "опубликовано" - const published = row.classList.contains("published"); - - // Инициализация статуса: "подтверждено" - const confirmed = row.classList.contains("confirmed"); - - // Инициализация статуса: "завершено" - const completed = row.classList.contains("completed"); - - // Инициализация буфера выбранных строк - const buffer = [ - ...document.querySelectorAll('div[data-selected="true"]'), - ]; - - // Удаление статуса активной строки у остальных строк - for (const element of buffer) { - element.removeAttribute("data-selected"); - } - - // Инициализация статуса активной строки (обрабатываемой в данный момент) - row.setAttribute("data-selected", "true"); - - // Инициализация оболочки всплывающего окна - this.core.body.wrap = document.createElement("div"); - this.core.body.wrap.setAttribute("id", "popup"); - - // Инициализация оболочки с основной информацией - const main = document.createElement("section"); - main.classList.add("main"); - - // Инициализация колонки - const column = document.createElement("div"); - column.classList.add("column"); - - // Инициализация всплывающего окна - const popup = document.createElement("section"); - popup.classList.add("window", "list", "small"); - - // Инициализация оболочки для кнопок - const buttons = document.createElement("div"); - buttons.classList.add("row", "buttons"); - - // Инициализация оболочки для строки с полем ввода - const label = document.createElement("label"); - - // Инициализация иконки для поля ввода замены - const icon = document.createElement("i"); - icon.classList.add("icon", "user"); - - // Инициализация поля ввода замены - const input = document.createElement("input"); - input.classList.add("cloud"); - input.setAttribute("placeholder", "Сотрудник"); - input.setAttribute("autocomplete", "username"); - input.setAttribute("autofocus", "true"); - input.setAttribute("title", "Идентификатор сотрудника"); - input.setAttribute("list", "popup_worker_datalist"); - if (published || confirmed || completed) { - input.setAttribute("readonly", "true"); - } - - // Инициализация кнопки замены - const connect = document.createElement("button"); - connect.classList.add("grass"); - connect.innerText = "Назначить"; - connect.setAttribute( - "onclick", - `tasks.worker.update(document.getElementById('${task}'), this.previousElementSibling, this)`, - ); - if (published || confirmed || completed) { - connect.setAttribute("disabled", "true"); - } - - // Инициализация списка сотрудников - const datalist = document.createElement("datalist"); - datalist.setAttribute("id", "popup_worker_datalist"); - workers.list().then((html) => datalist.innerHTML = html); - - // Инициализация оболочки для кнопок - const buttons_2 = document.createElement("div"); - buttons_2.classList.add("row", "buttons"); - - // Инициализация оболочки для строки с полем ввода - const label_2 = document.createElement("label"); - - // Инициализация кнопки замены - const publish = document.createElement("button"); - publish.classList.add(published ? "clay" : "sea"); - publish.innerText = published ? "Снять с публикации" : "Опубликовать"; - publish.setAttribute( - "onclick", - published - ? `tasks.worker.unpublish(document.getElementById('${task}'), this)` - : `tasks.worker.publish(document.getElementById('${task}'), this)`, - ); - - // Инициализация окна с ошибками - this.core.body.errors = document.createElement("section"); - this.core.body.errors.classList.add( - "errors", - "window", - "list", - "small", - "hidden", - ); - this.core.body.errors.setAttribute("data-errors", true); - - // Инициализация элемента-тела (оболочки) окна с ошибками - const body = document.createElement("section"); - body.classList.add("body"); - - // Инициализация элемента-списка ошибок - const dl = document.createElement("dl"); - - // Инициализация активного всплывающего окна - const old = document.getElementById("popup"); - - if (old instanceof HTMLElement) { - // Найдено активное окно - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.core.buttons); - - // Сброс блокировки - this.core.freeze = false; - - // Удаление активного окна - old.remove(); - } - - // Запись в документ - label.appendChild(icon); - label.appendChild(input); - label.appendChild(connect); - label.appendChild(datalist); - buttons.appendChild(label); - column.appendChild(buttons); - - if ( - typeof core === "function" && - core.interface !== "market" && - core.interface !== "worker" - ) { - // Скрытие для магазина и сотрудника - - label_2.appendChild(publish); - buttons_2.appendChild(label_2); - column.appendChild(buttons_2); - } - - main.appendChild(column); - popup.appendChild(main); - this.core.body.wrap.appendChild(popup); - - body.appendChild(dl); - this.core.body.errors.appendChild(body); - this.core.body.wrap.appendChild(this.core.body.errors); - - document.body.appendChild(this.core.body.wrap); - - // Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup) - function top(errors) { - errors.style.setProperty( - "transition", - "0s", - ); - errors.style.setProperty( - "--top", - popup.offsetTop + popup.offsetHeight + 12 + "px", - ); - setTimeout( - () => errors.style.removeProperty("transition"), - 100, - ); - } - top(this.core.body.errors); - const resize = new ResizeObserver(() => top(this.core.body.errors)); - resize.observe(this.core.body.wrap); - - // Инициалиация маски идентификатора сотрудника - IMask(input, { mask: "000000000" }); - - // Инициализация функции закрытия всплывающего окна - const click = () => { - // Блокировка - if (this.core.freeze) return; - - // Удаление всплывающего окна - this.core.body.wrap.remove(); - - // Инициализация буфера выбранных строк - const buffer = [ - ...document.querySelectorAll('div[data-selected="true"]'), - ]; - - // Удаление статуса активной строки у остальных строк - for (const element of buffer) { - element.removeAttribute("data-selected"); - } - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.core.buttons); - - // Сброс блокировки - this.core.freeze = false; - }; - - // Инициализация функции добавления функции закрытия всплывающего окна - const enable = () => - this.core.body.wrap.addEventListener("click", click); - - // Инициализация функции удаления функции закрытия всплывающего окна - const disable = () => - this.core.body.wrap.removeEventListener("click", click); - - // Первичная активация функции удаления всплывающего окна - enable(); - - // Инициализация блокировки удаления окна при взаимодействии с select-элементом - for (const select of popup.getElementsByTagName("select")) { - // Перебор всех select-элементов - - // Инициализация функции блокировки удаления окна по событию - select.addEventListener("click", () => { - // Блокировка удаления окна - this.core.freeze = true; - - // Разблокировка удаления окна - setTimeout(() => this.core.freeze = false, 100); - }); - - for ( - const option of select.getElementsByTagName("option") - ) { - // Перебор всех option-элементов - - // Инициализация функции блокировки удаления окна по событию - option.addEventListener("click", () => { - // Блокировка удаления окна - this.core.freeze = true; - - // Разблокировка удаления окна - setTimeout(() => this.core.freeze = false, 100); - }); - } - } - - // Инициализация блокировки удаления окна при взаимодействии с textarea-элементом - for ( - const textarea of popup.getElementsByTagName("textarea") - ) { - // Перебор всех textarea-элементов - - // Обновлять позицию окна с ошибками при изменении размера textarea (там position: absolute) - new MutationObserver(() => top(this.core.body.errors)).observe( - textarea, - { - attributes: true, - attributeFilter: ["style"], - }, - ); - - // Инициализация функции игнорирования блокировки для выбранных кнопок - textarea.addEventListener("keydown", (e) => { - if (e.keyCode === 27) { - // Нажата кнопка: "escape" - - // Вызов глобальной функции управления кнопками - this.core.buttons(e, true); - } else if (e.ctrlKey && e.keyCode === 13) { - // Нажаты кнопки: "control", "enter" - - // Вызов глобальной функции управления кнопками - this.core.buttons(e, true); - } - }); - - // Добавление функции блокировки удаления окна и клавиш по событиям - textarea.addEventListener( - "focus", - () => this.core.freeze = true, - ); - textarea.addEventListener( - "focusout", - () => this.core.freeze = false, - ); - } - - // Инициализация функции управления кнопками - this.core.buttons = (e, force = false) => { - // Блокировка - if (!force && this.core.freeze) return; - - if (e.keyCode === 27) { - // Нажата кнопка: "escape" - - // Удаление окна - click(); - } else if (event.keyCode === 13) { - // Нажата кнопка: "enter" - - // Инициализация буфера с текущим статусом блокировки закрытия окна - const freeze = this.core.freeze; - - // Блокировка закрытия окна (чтобы не вызвался click() через событие onclick) - this.core.freeze = true; - - // Активация виртуальной кнопки "назначить" - connect.click(); - - // Возвращение статуса блокировки закрытия окна - this.core.freeze = freeze; - } - }; - - // Инициализация быстрых действий по кнопкам - document.addEventListener("keydown", this.core.buttons); - - // Добавление функции удаления всплывающего окна по событиям - popup.addEventListener("mouseenter", disable); - popup.addEventListener("mouseleave", enable); - - // Фокусировка - input.focus(); - } - } - - /** - * Обновить - * - * @param {HTMLElement} row Строка - * @param {HTMLElement} input Поле для ввода идентификатора сотрудника - * @param {HTMLElement} button Кнопка - * - * @return {void} - */ - static update(row, input, button) { - // Блокировка поля ввода - input.setAttribute("readonly", true); - button.setAttribute("disabled", true); - - // Деинициализация индикатора и анимации об ошибке - input.classList.remove("error"); - input.previousElementSibling.classList.remove("error"); - - // Сброс анимации - this.core.body.errors.classList.add("hidden"); - - // Запуск выполнения - this._update(row, input, button); - } - - /** - * Обновить (системное) - * - * @param {HTMLElement} row Строка - * @param {HTMLElement} input Поле для ввода идентификатора сотрудника - * @param {HTMLElement} button Кнопка - * - * @return {void} - */ - static _update = damper(async (row, input, button) => { - if ( - row instanceof HTMLElement && - input instanceof HTMLElement && - button instanceof HTMLElement - ) { - // Получена строка, поле для ввода и кнопка - - // Инициализация идентификатора строки - const id = row.getAttribute("id"); - - if (typeof id === "string") { - // Инициализирован идентификатор - - // Инициализация функции разблокировки - function unblock() { - // Разблокировка поля ввода - input.removeAttribute("readonly"); - button.removeAttribute("disabled"); - } - - // Запуск отсрочки разблокировки на случай, если сервер не отвечает - const timeout = setTimeout(() => { - this.core.errors(["Сервер не отвечает"]); - unblock(); - }, 5000); - - // Запрос к серверу - return await fetch(`/task/${id}/worker/update`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: `worker=${input.value}`, - }) - .then((response) => response.json()) - .then((data) => { - // Удаление отсрочки разблокировки - clearTimeout(timeout); - - if (this.core.errors(data.errors)) { - // Сгенерированы ошибки - - // Инициализация отображения ошибки - input.classList.add("error"); - input.previousElementSibling.classList.add("error"); - - // Фокусировка на поле ввода - input.focus(); - - // Разблокировка полей ввода и кнопок - unblock(); - } else { - // Не сгенерированы ошибки (подразумевается их отсутствие) - - if (data.updated) { - // Записано обновление - - // Инициализация буфера списка классов - // const buffer = [...row.classList]; - - // Инициализация статуса активной строки - const selected = row.getAttribute("data-selected"); - - // Реинициализация строки - row.outerHTML = data.row; - - // Реинициализация строки (выражение странное, но правильное) - row = document.getElementById(row.getAttribute("id")); - - // Копирование классов из буфера классов удалённой строки - // row.classList.add(...buffer); - - // Копирование статуса активной строки - if ( - typeof selected === "string" && selected === "true" && - document.body.contains( - document.getElementById("popup"), - ) && - !document.body.contains( - document.querySelector('[data-selected="true"]'), - ) - ) { - row.setAttribute("data-selected", "true"); - } - } - - if (this.core.body.wrap instanceof HTMLElement) { - // Найдено активное окно - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.core.buttons); - - // Сброс блокировки - this.core.freeze = false; - - // Реинициализация активного окна - tasks.worker.popup(row); - } - } - }); - } - } - }, 300); - - /** - * Удалить - * - * @param {HTMLElement} row Строка - * @param {HTMLElement} button Кнопка - * - * @return {void} - */ - static remove(row, button) { - // Запуск выполнения - this._remove(row, button); - } - - /** - * Удалить (системное) - * - * @param {HTMLElement} row Строка - * @param {HTMLElement} button Кнопка - * - * @return {void} - */ - static _remove = damper(async (row, button) => { - if ( - row instanceof HTMLElement && - button instanceof HTMLElement - ) { - // Получена строка и кнопка - - // Инициализация идентификатора строки - const id = row.getAttribute("id"); - - if (typeof id === "string") { - // Инициализирован идентификатор - - // Запуск отсрочки разблокировки на случай, если сервер не отвечает - const timeout = setTimeout(() => { - this.core.errors(["Сервер не отвечает"]); - unblock(); - }, 5000); - - // Запрос к серверу - return await fetch(`/task/${id}/worker/update`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: `worker=delete`, - }) - .then((response) => response.json()) - .then((data) => { - // Удаление отсрочки разблокировки - clearTimeout(timeout); - - if (this.core.errors(data.errors)) { - // Сгенерированы ошибки - } else { - // Не сгенерированы ошибки (подразумевается их отсутствие) - - if (data.updated) { - // Записано обновление - - // Инициализация буфера списка классов - // const buffer = [...row.classList]; - - // Инициализация статуса активной строки - const selected = row.getAttribute("data-selected"); - - // Реинициализация строки - row.outerHTML = data.row; - - // Реинициализация строки (выражение странное, но правильное) - row = document.getElementById(row.getAttribute("id")); - - // Копирование классов из буфера классов удалённой строки - // row.classList.add(...buffer); - - // Копирование статуса активной строки - if ( - typeof selected === "string" && selected === "true" && - document.body.contains( - document.getElementById("popup"), - ) && - !document.body.contains( - document.querySelector('[data-selected="true"]'), - ) - ) { - row.setAttribute("data-selected", "true"); - } - } - - if (this.core.body.wrap instanceof HTMLElement) { - // Найдено активное окно - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.core.buttons); - - // Сброс блокировки - this.core.freeze = false; - - // Реинициализация активного окна - tasks.worker.popup(row); - } - } - }); - } - } - }, 300); - - /** - * Опубликовать - * - * @param {HTMLElement} row Строка - * @param {HTMLElement} button Кнопка - * - * @return {void} - */ - static publish(row, button) { - // Запуск выполнения - this._publish(row, button); - } - - /** - * Опубликовать (системное) - * - * @param {HTMLElement} row Строка - * @param {HTMLElement} button Кнопка - * - * @return {void} - */ - static _publish = damper(async (row, button) => { - if ( - row instanceof HTMLElement && - button instanceof HTMLElement - ) { - // Получена строка и кнопка - - // Инициализация идентификатора строки - const id = row.getAttribute("id"); - - if (typeof id === "string") { - // Инициализирован идентификатор - - // Запуск отсрочки разблокировки на случай, если сервер не отвечает - const timeout = setTimeout(() => { - this.core.errors(["Сервер не отвечает"]); - unblock(); - }, 5000); - - // Запрос к серверу - return await fetch(`/task/${id}/publish`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }) - .then((response) => response.json()) - .then((data) => { - // Удаление отсрочки разблокировки - clearTimeout(timeout); - - if (this.core.errors(data.errors)) { - // Сгенерированы ошибки - } else { - // Не сгенерированы ошибки (подразумевается их отсутствие) - - if (data.published) { - // Опубликована заявка - - // Инициализация буфера списка классов - // const buffer = [...row.classList]; - - // Инициализация статуса активной строки - const selected = row.getAttribute("data-selected"); - - // Инициализация идентификатора строки - const id = row.getAttribute("id"); - - // Реинициализация строки - row.outerHTML = data.row; - - // Реинициализация строки (выражение странное, но правильное) - row = document.getElementById(id); - - // Копирование классов из буфера классов удалённой строки и добавление соответствующего - // row.classList.add(...buffer, "published"); - row.classList.add("published"); - - // Копирование статуса активной строки - if ( - typeof selected === "string" && selected === "true" && - document.body.contains( - document.getElementById("popup"), - ) && - !document.body.contains( - document.querySelector('[data-selected="true"]'), - ) - ) { - row.setAttribute("data-selected", "true"); - } - - // Запись текста состояния кнопки - button.innerText = "Снять с публикации"; - - // Реинициализация стиля кнопки - button.classList.remove("sea"); - button.classList.add("clay"); - - // Инициализация вызова функции: "снять с публикации" - button.setAttribute( - "onclick", - `tasks.worker.unpublish(document.getElementById('${id}'), this)`, - ); - - // Инициализация оболочки элементов для блокировки - const wrap = - button.parentElement.parentElement.previousElementSibling - .children[0]; - - // Блокировка поля для ввода сотрудника - wrap.getElementsByTagName("input")[0].setAttribute( - "readonly", - "true", - ); - - // Блокировка кнопки назначения сотрудника - wrap.getElementsByTagName("button")[0].setAttribute( - "disabled", - "true", - ); - } - } - }); - } - } - }, 300); - - /** - * Снять с публикации - * - * @param {HTMLElement} row Строка - * @param {HTMLElement} button Кнопка - * - * @return {void} - */ - static unpublish(row, button) { - // Запуск выполнения - this._unpublish(row, button); - } - - /** - * Снять с публикации (системное) - * - * @param {HTMLElement} row Строка - * @param {HTMLElement} button Кнопка - * - * @return {void} - */ - static _unpublish = damper(async (row, button) => { - if ( - row instanceof HTMLElement && - button instanceof HTMLElement - ) { - // Получена строка и кнопка - - // Инициализация идентификатора строки - const id = row.getAttribute("id"); - - if (typeof id === "string") { - // Инициализирован идентификатор - - // Запуск отсрочки разблокировки на случай, если сервер не отвечает - const timeout = setTimeout(() => { - this.core.errors(["Сервер не отвечает"]); - unblock(); - }, 5000); - - // Запрос к серверу - return await fetch(`/task/${id}/unpublish`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }) - .then((response) => response.json()) - .then((data) => { - // Удаление отсрочки разблокировки - clearTimeout(timeout); - - if (this.core.errors(data.errors)) { - // Сгенерированы ошибки - } else { - // Не сгенерированы ошибки (подразумевается их отсутствие) - - if (data.unpublished) { - // Снята с публикации заявка - - // Инициализация буфера списка классов - // const buffer = [...row.classList]; - - // Инициализация статуса активной строки - const selected = row.getAttribute("data-selected"); - - // Реинициализация строки - row.outerHTML = data.row; - - // Реинициализация строки (выражение странное, но правильное) - row = document.getElementById(row.getAttribute("id")); - - // Копирование классов из буфера классов удалённой строки и добавление соответствующего - // row.classList.add(...buffer, "published"); - // row.classList.add("published"); - - // Удаление класса неактуального состояния - // row.classList.remove("published"); - - // Копирование статуса активной строки - if ( - typeof selected === "string" && selected === "true" && - document.body.contains( - document.getElementById("popup"), - ) && - !document.body.contains( - document.querySelector('[data-selected="true"]'), - ) - ) { - row.setAttribute("data-selected", "true"); - } - - // Запись текста состояния кнопки - button.innerText = "Опубликовать"; - - // Реинициализация стиля кнопки - button.classList.remove("clay"); - button.classList.add("sea"); - - // Инициализация вызова функции: "опубликовать" - button.setAttribute( - "onclick", - `tasks.worker.publish(document.getElementById('${id}'), this)`, - ); - - // Инициализация оболочки элементов для разблокировки - const wrap = - button.parentElement.parentElement.previousElementSibling - .children[0]; - - // Разблокировка поля для ввода сотрудника - wrap.getElementsByTagName("input")[0].removeAttribute( - "readonly", - ); - - // Разблокировка кнопки назначения сотрудника - wrap.getElementsByTagName("button")[0].removeAttribute( - "disabled", - ); - } - } - }); - } - } - }, 300); - }; - - /** - * Магазин - */ - static market = class { - /** - * Ссылка на ядро (родительский класс) - */ - static core; - - /** - * Сгенерировать всплывающее окно c данными магазина - * - * @param {HTMLElement} row Строка - * - * @return {void} - */ - static async popup(row) { - if ( - row instanceof HTMLElement && - (typeof core === "function" && - core.interface !== "market" && - core.interface !== "worker") - ) { - // Получена строка и аккаунт не является магазином или сотрудником - - // Инициализация идентификатора магазина - const market = - row.querySelector('span[data-column="market"]').innerText; - - if (market.length > 0) { - // Инициализирован идентификатор магазина - - // Инициализация идентификатора строки - const task = row.getAttribute("id"); - - if (task.length > 0) { - // Инициализирован идентификатор строки - - // Запрос к серверу - return await fetch(`/market/${market}/read`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }) - .then((response) => response.json()) - .then((data) => { - if (this.core.errors(data.errors)) { - // Сгенерированы ошибки - } else { - // Не сгенерированы ошибки (подразумевается их отсутствие) - - if (data.market.length > 0) { - // Получены данные (подразумевается, что сотрудник найден) - - // Инициализация буфера выбранных строк - const buffer = [ - ...document.querySelectorAll( - 'div[data-selected="true"]', - ), - ]; - - // Удаление статуса активной строки у остальных строк - for (const element of buffer) { - element.removeAttribute("data-selected"); - } - - // Инициализация статуса активной строки (обрабатываемой в данный момент) - row.setAttribute("data-selected", "true"); - - // Инициализация оболочки всплывающего окна - this.core.body.wrap = document.createElement("div"); - this.core.body.wrap.setAttribute("id", "popup"); - - // Инициализация всплывающего окна - const popup = document.createElement("section"); - popup.classList.add("window", "list", "small"); - - // Инициализация заголовка всплывающего окна - const title = document.createElement("h3"); - title.classList.add("unselectable"); - title.innerText = market; - - // Инициализация оболочки с основной информацией - const main = document.createElement("section"); - main.classList.add("main"); - - // Инициализация колонки - const column = document.createElement("div"); - column.classList.add("column"); - - // Инициализация буфера для записи во всплывающее окно - const list = document.createElement("div"); - - // Инициализация оболочки для кнопок - const buttons = document.createElement("div"); - buttons.classList.add("row", "buttons"); - - // Инициализация оболочки для строки с полем ввода - const label = document.createElement("label"); - - // Инициализация иконки для поля ввода замены - const icon = document.createElement("i"); - icon.classList.add("icon", "shopping", "cart"); - - // Инициализация поля ввода замены - const input = document.createElement("input"); - input.classList.add("cloud"); - input.setAttribute("placeholder", "Магазин"); - input.setAttribute("autocomplete", "username"); - input.setAttribute("autofocus", "true"); - input.setAttribute("title", "Идентификатор магазина"); - input.setAttribute("list", "popup_market_datalist"); - - // Инициализация кнопки смены - const replace = document.createElement("button"); - replace.classList.add("sea"); - replace.innerText = "Заменить"; - replace.setAttribute( - "onclick", - `tasks.market.update(document.getElementById('${task}'), this.previousElementSibling, this)`, - ); - - // Инициализация списка сотружников - const datalist = document.createElement("datalist"); - datalist.setAttribute("id", "popup_market_datalist"); - markets.list().then((html) => datalist.innerHTML = html); - - // Инициализация кнопки удаления - const remove = document.createElement("button"); - remove.classList.add("clay"); - remove.innerText = "Удалить"; - remove.setAttribute( - "onclick", - `tasks.market.remove(document.getElementById('${task}'), this)`, - ); - - // Инициализация окна с ошибками - this.core.body.errors = document.createElement("section"); - this.core.body.errors.classList.add( - "errors", - "window", - "list", - "small", - "hidden", - ); - this.core.body.errors.setAttribute("data-errors", true); - - // Инициализация элемента-тела (оболочки) окна с ошибками - const body = document.createElement("section"); - body.classList.add("body"); - - // Инициализация элемента-списка ошибок - const dl = document.createElement("dl"); - - // Инициализация активного всплывающего окна - const old = document.getElementById("popup"); - - if (old instanceof HTMLElement) { - // Найдено активное окно - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener( - "keydown", - this.core.buttons, - ); - - // Сброс блокировки - this.core.freeze = false; - - // Удаление активного окна - old.remove(); - } - - // Запись в документ - popup.appendChild(title); - - label.appendChild(icon); - label.appendChild(input); - label.appendChild(replace); - label.appendChild(remove); - label.appendChild(datalist); - buttons.appendChild(label); - column.appendChild(list); - column.appendChild(buttons); - - main.appendChild(column); - popup.appendChild(main); - this.core.body.wrap.appendChild(popup); - - body.appendChild(dl); - this.core.body.errors.appendChild(body); - this.core.body.wrap.appendChild(this.core.body.errors); - - document.body.appendChild(this.core.body.wrap); - - // Запись в документ HTML-данных через буфер - list.outerHTML = data.market; - - // Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup) - function top(errors) { - errors.style.setProperty( - "transition", - "0s", - ); - errors.style.setProperty( - "--top", - popup.offsetTop + popup.offsetHeight + 12 + "px", - ); - setTimeout( - () => errors.style.removeProperty("transition"), - 100, - ); - } - top(this.core.body.errors); - const resize = new ResizeObserver(() => - top(this.core.body.errors) - ); - resize.observe(this.core.body.wrap); - - // Инициалиация маски идентификатора магазина в поле ввода идентификатора магазина - IMask(input, { mask: "000000000" }); - - // Инициализация функции закрытия всплывающего окна - const click = () => { - // Блокировка - if (this.core.freeze) return; - - // Удаление всплывающего окна - this.core.body.wrap.remove(); - - // Инициализация буфера выбранных строк - const buffer = [ - ...document.querySelectorAll( - 'div[data-selected="true"]', - ), - ]; - - // Удаление статуса активной строки у остальных строк - for (const element of buffer) { - element.removeAttribute("data-selected"); - } - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener( - "keydown", - this.core.buttons, - ); - - // Сброс блокировки - this.core.freeze = false; - }; - - // Инициализация функции добавления функции закрытия всплывающего окна - const enable = () => - this.core.body.wrap.addEventListener("click", click); - - // Инициализация функции удаления функции закрытия всплывающего окна - const disable = () => - this.core.body.wrap.removeEventListener("click", click); - - // Первичная активация функции удаления всплывающего окна - enable(); - - // Инициализация блокировки удаления окна при взаимодействии с select-элементом - for ( - const select of popup.getElementsByTagName("select") - ) { - // Перебор всех select-элементов - - // Инициализация функции блокировки удаления окна по событию - select.addEventListener("click", () => { - // Блокировка удаления окна - this.core.freeze = true; - - // Разблокировка удаления окна - setTimeout(() => this.core.freeze = false, 100); - }); - - for ( - const option of select.getElementsByTagName("option") - ) { - // Перебор всех option-элементов - - // Инициализация функции блокировки удаления окна по событию - option.addEventListener("click", () => { - // Блокировка удаления окна - this.core.freeze = true; - - // Разблокировка удаления окна - setTimeout(() => this.core.freeze = false, 100); - }); - } - } - - // Инициализация блокировки удаления окна при взаимодействии с textarea-элементом - for ( - const textarea of popup.getElementsByTagName("textarea") - ) { - // Перебор всех textarea-элементов - - // Обновлять позицию окна с ошибками при изменении размера textarea (там position: absolute) - new MutationObserver(() => top(this.core.body.errors)) - .observe(textarea, { - attributes: true, - attributeFilter: ["style"], - }); - - // Инициализация функции игнорирования блокировки для выбранных кнопок - textarea.addEventListener("keydown", (e) => { - if (e.keyCode === 27) { - // Нажата кнопка: "escape" - - // Вызов глобальной функции управления кнопками - this.core.buttons(e, true); - } else if (e.ctrlKey && e.keyCode === 13) { - // Нажаты кнопки: "control", "enter" - - // Вызов глобальной функции управления кнопками - this.core.buttons(e, true); - } - }); - - // Добавление функции блокировки удаления окна и клавиш по событиям - textarea.addEventListener( - "focus", - () => this.core.freeze = true, - ); - textarea.addEventListener( - "focusout", - () => this.core.freeze = false, - ); - } - - // Инициализация функции управления кнопками - this.core.buttons = (e, force = false) => { - // Блокировка - if (!force && this.core.freeze) return; - - if (e.keyCode === 27) { - // Нажата кнопка: "escape" - - // Удаление окна - click(); - } else if (event.keyCode === 13) { - // Нажата кнопка: "enter" - - // Инициализация буфера с текущим статусом блокировки закрытия окна - const freeze = this.core.freeze; - - // Блокировка закрытия окна (чтобы не вызвался click() через событие onclick) - this.core.freeze = true; - - // Активация виртуальной кнопки "заменить" - replace.click(); - - // Возвращение статуса блокировки закрытия окна - this.core.freeze = freeze; - } - }; - - // Инициализация быстрых действий по кнопкам - document.addEventListener("keydown", this.core.buttons); - - // Добавление функции удаления всплывающего окна по событиям - popup.addEventListener("mouseenter", disable); - popup.addEventListener("mouseleave", enable); - - // Фокусировка - input.focus(); - } else { - // Не получены данные (подразумевается, что магазин не найден) - - // Открытие окна инициализации магазина - this.init(row); - } - } - }); - } - } else { - // Не инициализирован идентификатор магазина - - // Открытие окна инициализации магазина - this.init(row); - } - } - } - - static init(row) { - // Инициализация идентификатора строки - const task = row.getAttribute("id"); - - if (task.length > 0) { - // Инициализирован идентификатор строки - - // Инициализация буфера выбранных строк - const buffer = [ - ...document.querySelectorAll('div[data-selected="true"]'), - ]; - - // Удаление статуса активной строки у остальных строк - for (const element of buffer) { - element.removeAttribute("data-selected"); - } - - // Инициализация статуса активной строки (Магазин обрабатываемой в данный момент) - row.setAttribute("data-selected", "true"); - - // Инициализация оболочки всплывающего окна - this.core.body.wrap = document.createElement("div"); - this.core.body.wrap.setAttribute("id", "popup"); - - // Инициализация всплывающего окна - const popup = document.createElement("section"); - popup.classList.add("window", "list", "small"); - - // Инициализация оболочки с основной информацией - const main = document.createElement("section"); - main.classList.add("main"); - - // Инициализация колонки - const column = document.createElement("div"); - column.classList.add("column"); - - // Инициализация оболочки для кнопок - const buttons = document.createElement("div"); - buttons.classList.add("row", "buttons"); - - // Инициализация оболочки для строки с полем ввода - const label = document.createElement("label"); - - // Инициализация иконки для поля ввода замены - const icon = document.createElement("i"); - icon.classList.add("icon", "shopping", "cart"); - - // Инициализация поля ввода замены - const input = document.createElement("input"); - input.classList.add("cloud"); - input.setAttribute("placeholder", "Магазин"); - input.setAttribute("autocomplete", "username"); - input.setAttribute("autofocus", "true"); - input.setAttribute("title", "Идентификатор магазина"); - input.setAttribute("list", "popup_market_datalist"); - - // Инициализация кнопки замены - const connect = document.createElement("button"); - connect.classList.add("grass"); - connect.innerText = "Назначить"; - connect.setAttribute( - "onclick", - `tasks.market.update(document.getElementById('${task}'), this.previousElementSibling, this)`, - ); - - // Инициализация списка сотружников - const datalist = document.createElement("datalist"); - datalist.setAttribute("id", "popup_market_datalist"); - markets.list().then((html) => datalist.innerHTML = html); - - // Инициализация окна с ошибками - this.core.body.errors = document.createElement("section"); - this.core.body.errors.classList.add( - "errors", - "window", - "list", - "small", - "hidden", - ); - this.core.body.errors.setAttribute("data-errors", true); - - // Инициализация элемента-тела (оболочки) окна с ошибками - const body = document.createElement("section"); - body.classList.add("body"); - - // Инициализация элемента-списка ошибок - const dl = document.createElement("dl"); - - // Инициализация активного всплывающего окна - const old = document.getElementById("popup"); - - if (old instanceof HTMLElement) { - // Найдено активное окно - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.core.buttons); - - // Сброс блокировки - this.core.freeze = false; - - // Удаление активного окна - old.remove(); - } - - // Запись в документ - label.appendChild(icon); - label.appendChild(input); - label.appendChild(connect); - label.appendChild(datalist); - buttons.appendChild(label); - column.appendChild(buttons); - - main.appendChild(column); - popup.appendChild(main); - this.core.body.wrap.appendChild(popup); - - body.appendChild(dl); - this.core.body.errors.appendChild(body); - this.core.body.wrap.appendChild(this.core.body.errors); - - document.body.appendChild(this.core.body.wrap); - - // Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup) - function top(errors) { - errors.style.setProperty( - "transition", - "0s", - ); - errors.style.setProperty( - "--top", - popup.offsetTop + popup.offsetHeight + 12 + "px", - ); - setTimeout( - () => errors.style.removeProperty("transition"), - 100, - ); - } - top(this.core.body.errors); - const resize = new ResizeObserver(() => top(this.core.body.errors)); - resize.observe(this.core.body.wrap); - - // Инициалиация маски идентификатора магазина - IMask(input, { mask: "000000000" }); - - // Инициализация функции закрытия всплывающего окна - const click = () => { - // Блокировка - if (this.core.freeze) return; - - // Удаление всплывающего окна - this.core.body.wrap.remove(); - - // Инициализация буфера выбранных строк - const buffer = [ - ...document.querySelectorAll('div[data-selected="true"]'), - ]; - - // Удаление статуса активной строки у остальных строк - for (const element of buffer) { - element.removeAttribute("data-selected"); - } - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.core.buttons); - - // Сброс блокировки - this.core.freeze = false; - }; - - // Инициализация функции добавления функции закрытия всплывающего окна - const enable = () => - this.core.body.wrap.addEventListener("click", click); - - // Инициализация функции удаления функции закрытия всплывающего окна - const disable = () => - this.core.body.wrap.removeEventListener("click", click); - - // Первичная активация функции удаления всплывающего окна - enable(); - - // Инициализация блокировки удаления окна при взаимодействии с select-элементом - for (const select of popup.getElementsByTagName("select")) { - // Перебор всех select-элементов - - // Инициализация функции блокировки удаления окна по событию - select.addEventListener("click", () => { - // Блокировка удаления окна - this.core.freeze = true; - - // Разблокировка удаления окна - setTimeout(() => this.core.freeze = false, 100); - }); - - for (const option of select.getElementsByTagName("option")) { - // Перебор всех option-элементов - - // Инициализация функции блокировки удаления окна по событию - option.addEventListener("click", () => { - // Блокировка удаления окна - this.core.freeze = true; - - // Разблокировка удаления окна - setTimeout(() => this.core.freeze = false, 100); - }); - } - } - - // Инициализация блокировки удаления окна при взаимодействии с textarea-элементом - for (const textarea of popup.getElementsByTagName("textarea")) { - // Перебор всех textarea-элементов - - // Обновлять позицию окна с ошибками при изменении размера textarea (там position: absolute) - new MutationObserver(() => top(this.core.body.errors)).observe( - textarea, - { - attributes: true, - attributeFilter: ["style"], - }, - ); - - // Инициализация функции игнорирования блокировки для выбранных кнопок - textarea.addEventListener("keydown", (e) => { - if (e.keyCode === 27) { - // Нажата кнопка: "escape" - - // Вызов глобальной функции управления кнопками - this.core.buttons(e, true); - } else if (e.ctrlKey && e.keyCode === 13) { - // Нажаты кнопки: "control", "enter" - - // Вызов глобальной функции управления кнопками - this.core.buttons(e, true); - } - }); - - // Добавление функции блокировки удаления окна и клавиш по событиям - textarea.addEventListener( - "focus", - () => this.core.freeze = true, - ); - textarea.addEventListener( - "focusout", - () => this.core.freeze = false, - ); - } - - // Инициализация функции управления кнопками - this.core.buttons = (e, force = false) => { - // Блокировка - if (!force && this.core.freeze) return; - - if (e.keyCode === 27) { - // Нажата кнопка: "escape" - - // Удаление окна - click(); - } else if (event.keyCode === 13) { - // Нажата кнопка: "enter" - - // Инициализация буфера с текущим статусом блокировки закрытия окна - const freeze = this.core.freeze; - - // Блокировка закрытия окна (чтобы не вызвался click() через событие onclick) - this.core.freeze = true; - - // Активация виртуальной кнопки "назначить" - connect.click(); - - // Возвращение статуса блокировки закрытия окна - this.core.freeze = freeze; - } - }; - - // Инициализация быстрых действий по кнопкам - document.addEventListener("keydown", this.core.buttons); - - // Добавление функции удаления всплывающего окна по событиям - popup.addEventListener("mouseenter", disable); - popup.addEventListener("mouseleave", enable); - - // Фокусировка - input.focus(); - } - } - - /** - * Обновить - * - * @param {HTMLElement} row Строка - * @param {HTMLElement} input Поле для ввода идентификатора магазина - * @param {HTMLElement} button Кнопка - * - * @return {void} - */ - static update(row, input, button) { - // Блокировка поля ввода - input.setAttribute("readonly", true); - button.setAttribute("disabled", true); - - // Деинициализация индикатора и анимации об ошибке - input.classList.remove("error"); - input.previousElementSibling.classList.remove("error"); - - // Сброс анимации - this.core.body.errors.classList.add("hidden"); - - // Запуск выполнения - this._update(row, input, button); - } - - /** - * Обновить (системное) - * - * @param {HTMLElement} row Строка - * @param {HTMLElement} input Поле для ввода идентификатора магазина - * @param {HTMLElement} button Кнопка - * - * @return {void} - */ - static _update = damper(async (row, input, button) => { - if ( - row instanceof HTMLElement && - input instanceof HTMLElement && - button instanceof HTMLElement - ) { - // Получена строка, поле для ввода и кнопка - - // Инициализация идентификатора строки - const id = row.getAttribute("id"); - - if (typeof id === "string") { - // Инициализирован идентификатор - - // Инициализация функции разблокировки - function unblock() { - // Разблокировка поля ввода - input.removeAttribute("readonly"); - button.removeAttribute("disabled"); - } - - // Запуск отсрочки разблокировки на случай, если сервер не отвечает - const timeout = setTimeout(() => { - this.core.errors(["Сервер не отвечает"]); - unblock(); - }, 5000); - - // Запрос к серверу - return await fetch(`/task/${id}/market/update`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: `market=${input.value}`, - }) - .then((response) => response.json()) - .then((data) => { - // Удаление отсрочки разблокировки - clearTimeout(timeout); - - if (this.core.errors(data.errors)) { - // Сгенерированы ошибки - - // Инициализация отображения ошибки - input.classList.add("error"); - input.previousElementSibling.classList.add("error"); - - // Фокусировка на поле ввода - input.focus(); - - // Разблокировка полей ввода и кнопок - unblock(); - } else { - // Не сгенерированы ошибки (подразумевается их отсутствие) - - if (data.updated) { - // Записано обновление - - // Инициализация буфера списка классов - // const buffer = [...row.classList]; - - // Инициализация статуса активной строки - const selected = row.getAttribute("data-selected"); - - // Реинициализация строки - row.outerHTML = data.row; - - // Реинициализация строки (выражение странное, но правильное) - row = document.getElementById(row.getAttribute("id")); - - // Копирование классов из буфера классов удалённой строки - // row.classList.add(...buffer); - - // Копирование статуса активной строки - if ( - typeof selected === "string" && selected === "true" && - document.body.contains( - document.getElementById("popup"), - ) && - !document.body.contains( - document.querySelector('[data-selected="true"]'), - ) - ) { - row.setAttribute("data-selected", "true"); - } - } - - if (this.core.body.wrap instanceof HTMLElement) { - // Найдено активное окно - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.core.buttons); - - // Сброс блокировки - this.core.freeze = false; - - // Реинициализация активного окна - tasks.market.popup(row); - } - } - }); - } - } - }, 300); - - /** - * Удалить - * - * @param {HTMLElement} row Строка - * @param {HTMLElement} button Кнопка - * - * @return {void} - */ - static remove(row, button) { - // Запуск выполнения - this._remove(row, button); - } - - /** - * Удалить (системное) - * - * @param {HTMLElement} row Строка - * @param {HTMLElement} button Кнопка - * - * @return {void} - */ - static _remove = damper(async (row, button) => { - if (row instanceof HTMLElement && button instanceof HTMLElement) { - // Получена строка и кнопка - - // Инициализация идентификатора строки - const id = row.getAttribute("id"); - - if (typeof id === "string") { - // Инициализирован идентификатор - - // Запуск отсрочки разблокировки на случай, если сервер не отвечает - const timeout = setTimeout(() => { - this.core.errors(["Сервер не отвечает"]); - unblock(); - }, 5000); - - // Запрос к серверу - return await fetch(`/task/${id}/market/update`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: `market=delete`, - }) - .then((response) => response.json()) - .then((data) => { - // Удаление отсрочки разблокировки - clearTimeout(timeout); - - if (this.core.errors(data.errors)) { - // Сгенерированы ошибки - - // Разблокировка полей ввода и кнопок - unblock(); - } else { - // Не сгенерированы ошибки (подразумевается их отсутствие) - - if (data.updated) { - // Записано обновление - - // Инициализация буфера списка классов - // const buffer = [...row.classList]; - - // Инициализация статуса активной строки - const selected = row.getAttribute("data-selected"); - - // Реинициализация строки - row.outerHTML = data.row; - - // Реинициализация строки (выражение странное, но правильное) - row = document.getElementById(row.getAttribute("id")); - - // Копирование классов из буфера классов удалённой строки - // row.classList.add(...buffer); - - // Копирование статуса активной строки - if ( - typeof selected === "string" && selected === "true" && - document.body.contains( - document.getElementById("popup"), - ) && - !document.body.contains( - document.querySelector('[data-selected="true"]'), - ) - ) { - row.setAttribute("data-selected", "true"); - } - } - - if (this.core.body.wrap instanceof HTMLElement) { - // Найдено активное окно - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.core.buttons); - - // Сброс блокировки - this.core.freeze = false; - - // Реинициализация активного окна - tasks.market.popup(row); - } - } - }); - } - } - }, 300); - }; - - /** - * Комментарий - */ - static commentary = class { - /** - * Ссылка на ядро (родительский класс) - */ - static core; - - /** - * Сгенерировать всплывающее окно c комментарием задачи - * - * @param {HTMLElement} row Строка - * - * @return {void} - */ - static popup = damper((row) => { - if ( - row instanceof HTMLElement && - (typeof core === "function" && - core.interface !== "market" && - core.interface !== "worker") - ) { - // Получена строка и аккаунт не является магазином или сотрудником - - // Инициализация идентификатора сотрудника - const task = row.getAttribute("id"); - - // Инициализация буфера выбранных строк - const buffer = [ - ...document.querySelectorAll('div[data-selected="true"]'), - ]; - - // Удаление статуса активной строки у остальных строк - for (const element of buffer) { - element.removeAttribute("data-selected"); - } - - // Инициализация статуса активной строки (обрабатываемой в данный момент) - row.setAttribute("data-selected", "true"); - - // Инициализация оболочки всплывающего окна - this.core.body.wrap = document.createElement("div"); - this.core.body.wrap.setAttribute("id", "popup"); - - // Инициализация всплывающего окна - const popup = document.createElement("section"); - popup.classList.add("list", "small"); - - // Инициализация заголовка всплывающего окна - const title = document.createElement("h3"); - title.innerText = "Комментарий"; - - // Инициализация оболочки с основной информацией - const main = document.createElement("section"); - main.classList.add("main"); - - // Инициализация колонки - const column = document.createElement("div"); - column.classList.add("column"); - - // Инициализация строки - const row_1 = document.createElement("div"); - row_1.classList.add("row", "stretchable"); - - // Инициализация оболочки для поля ввода комментария - const label = document.createElement("label"); - - // Инициализация поля ввода комментария - const textarea = document.createElement("textarea"); - textarea.classList.add("snow"); - this.core.value(row, "commentary").then((text) => - textarea.value = text - ); - textarea.setAttribute("autofocus", "true"); - textarea.setAttribute("maxlength", "800"); - textarea.setAttribute( - "onkeyup", - `tasks.commentary.update(document.getElementById('${task}'), this)`, - ); - textarea.setAttribute("title", "Дополнительная информация"); - textarea.setAttribute( - "placeholder", - "Дополнительная информация о заявке", - ); - - // Инициализация окна с ошибками - this.core.body.errors = document.createElement("section"); - this.core.body.errors.classList.add( - "errors", - "window", - "list", - "small", - "hidden", - ); - this.core.body.errors.setAttribute("data-errors", true); - - // Инициализация элемента-тела (оболочки) окна с ошибками - const errors = document.createElement("section"); - errors.classList.add("body"); - - // Инициализация элемента-списка ошибок - const dl = document.createElement("dl"); - - // Инициализация активного всплывающего окна - const old = document.getElementById("popup"); - - if (old instanceof HTMLElement) { - // Найдено активное окно - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.core.buttons); - - // Сброс блокировки - this.core.freeze = false; - - // Удаление активного окна - old.remove(); - } - - // Удаление блокировки выполнения закрытия всплывающего окна - this.core.freeze = false; - - // Запись в документ - popup.appendChild(title); - - label.appendChild(textarea); - row_1.appendChild(label); - column.appendChild(row_1); - - main.appendChild(column); - popup.appendChild(main); - this.core.body.wrap.appendChild(popup); - - errors.appendChild(dl); - this.core.body.errors.appendChild(errors); - this.core.body.wrap.appendChild(this.core.body.errors); - - document.body.appendChild(this.core.body.wrap); - - // Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup) - function top(errors) { - errors.style.setProperty( - "transition", - "0s", - ); - errors.style.setProperty( - "--top", - popup.offsetTop + popup.offsetHeight + 12 + "px", - ); - setTimeout( - () => errors.style.removeProperty("transition"), - 100, - ); - } - top(this.core.body.errors); - const resize = new ResizeObserver(() => top(this.core.body.errors)); - resize.observe(this.core.body.wrap); - - // Инициализация функции закрытия всплывающего окна - const click = () => { - // Блокировка - if (this.core.freeze) return; - - // Удаление всплывающего окна - this.core.body.wrap.remove(); - - // Инициализация буфера выбранных строк - const buffer = [ - ...document.querySelectorAll('div[data-selected="true"]'), - ]; - - // Удаление статуса активной строки у остальных строк - for (const element of buffer) { - element.removeAttribute("data-selected"); - } - - // Деинициализация быстрых действий по кнопкам - document.removeEventListener("keydown", this.core.buttons); - - // Сброс блокировки - this.core.freeze = false; - }; - - // Инициализация функции добавления функции закрытия всплывающего окна - const enable = () => - this.core.body.wrap.addEventListener("click", click); - - // Инициализация функции удаления функции закрытия всплывающего окна - const disable = () => - this.core.body.wrap.removeEventListener("click", click); - - // Первичная активация функции удаления всплывающего окна - enable(); - - // Инициализация блокировки удаления окна при взаимодействии с select-элементом - for (const select of popup.getElementsByTagName("select")) { - // Перебор всех select-элементов - - // Инициализация функции блокировки удаления окна по событию - select.addEventListener("click", () => { - // Блокировка удаления окна - this.core.freeze = true; - - // Разблокировка удаления окна - setTimeout(() => this.core.freeze = false, 100); - }); - - for (const option of select.getElementsByTagName("option")) { - // Перебор всех option-элементов - - // Инициализация функции блокировки удаления окна по событию - option.addEventListener("click", () => { - // Блокировка удаления окна - this.core.freeze = true; - - // Разблокировка удаления окна - setTimeout(() => this.core.freeze = false, 100); - }); - } - } - - // Инициализация блокировки удаления окна при взаимодействии с textarea-элементом - for (const textarea of popup.getElementsByTagName("textarea")) { - // Перебор всех textarea-элементов - - // Обновлять позицию окна с ошибками при изменении размера textarea (там position: absolute) - new MutationObserver(() => top(this.core.body.errors)).observe( - textarea, - { - attributes: true, - attributeFilter: ["style"], - }, - ); - - // Инициализация функции игнорирования блокировки для выбранных кнопок - textarea.addEventListener("keydown", (e) => { - if (e.keyCode === 27) { - // Нажата кнопка: "escape" - - // Вызов глобальной функции управления кнопками - this.core.buttons(e, true); - } else if (e.ctrlKey && e.keyCode === 13) { - // Нажаты кнопки: "control", "enter" - - // Вызов глобальной функции управления кнопками - this.core.buttons(e, true); - } - }); - - // Добавление функции блокировки удаления окна и клавиш по событиям - textarea.addEventListener("focus", () => this.core.freeze = true); - textarea.addEventListener( - "focusout", - () => this.core.freeze = false, - ); - } - - // Инициализация функции управления кнопками - this.core.buttons = (e, force = false) => { - // Блокировка - if (!force && this.core.freeze) return; - - if (e.keyCode === 27) { - // Нажата кнопка: "escape" - - // Удаление окна - click(); - } - }; - - // Инициализация быстрых действий по кнопкам - document.addEventListener("keydown", this.core.buttons); - - // Добавление функции удаления всплывающего окна по событиям - popup.addEventListener("mouseenter", disable); - popup.addEventListener("mouseleave", enable); - - // Фокусировка - textarea.focus(); - } - }, 300); - - /** - * Записать обновление - * - * @param {HTMLElement} row Строка - * @param {HTMLElement} textarea HTML-элемент - * - * @return {void} - */ - static update(row, textarea) { - // Сброс анимации ошибки - textarea.classList.remove("error"); - - // Запуск выполнения - this._update(row, textarea); - } - - /** - * Записать обновление (системное) - * - * @param {HTMLElement} row Строка - * @param {HTMLElement} textarea HTML-элемент - * - * @return {void} - */ - static _update = damper(async (row, textarea) => { - if (row instanceof HTMLElement && textarea instanceof HTMLElement) { - // Получена строка и select-элемент - - // Инициализация идентификатора строки - const id = row.getAttribute("id"); - - if (typeof id === "string") { - // Инициализирован идентификатор - - // Запрос к серверу - return await fetch(`/task/${id}/commentary`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: `commentary=${textarea.value}`, - }) - .then((response) => response.json()) - .then((data) => { - if (this.core.errors(data.errors)) { - // Сгенерированы ошибки - - // Запись анимации ошибки - textarea.classList.add("error"); - } else { - // Не сгенерированы ошибки (подразумевается их отсутствие) - - if (data.writed) { - // Записано новое значение в базу данных - - // Удаление анимации ошибки - textarea.classList.remove("error"); - - // Инициализация буфера списка классов - // const buffer = [...row.classList]; - - // Инициализация статуса активной строки - const selected = row.getAttribute("data-selected"); - - // Реинициализация строки - row.outerHTML = data.row; - - // Реинициализация строки (выражение странное, но правильное) - row = document.getElementById(row.getAttribute("id")); - - // Копирование классов из буфера классов удалённой строки - // row.classList.add(...buffer); - - // Копирование статуса активной строки - if ( - typeof selected === "string" && selected === "true" && - document.body.contains( - document.getElementById("popup"), - ) && - !document.body.contains( - document.querySelector('[data-selected="true"]'), - ) - ) { - row.setAttribute("data-selected", "true"); - } - } else { - // Не записано новое значение в базу данных - - // Запись анимации ошибки - textarea.classList.add("error"); - } - } - }); - } - } - }, 300); - }; - - static reinitializer = class { - /** - * Ссылка на ядро (родительский класс) - */ - static core; - - /** - * Запустить - * - * @param {HTMLElement|null} target Строка - */ - static start(target) { - // Инициализация оболочки - const tasks = document.getElementById("tasks"); - - for ( - let row of typeof target === "undefined" - ? tasks.querySelectorAll(':scope > div[data-row="task"]') - : [target] - ) { - // Перебор строк - - // Инициализация номера итерации по умолчанию - if (typeof row.counter === "undefined") { - // Не инициализирован счётчик итерации - - row.counter = Math.floor(Math.random() * 280 + 20); - row.setAttribute("data-counter", row.counter); - } - - // Инициализация самообновления - if (typeof row.updater === "undefined") { - // Не инициализирована функция самообновления - - row.updater = setInterval( - () => { - if (--row.counter <= 0) { - // Отсчёт таймера завершён - - // Блокировка строки - - // Деинициализация реинициализатора строки (для защиты от повторного запроса) - this.stop(row); - - // Запись анимации (запуск) - row.classList.add("reinitialized"); - - // Запрос к серверу - fetch("/tasks/read", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: `row=${row.getAttribute("id")}`, - }) - .then((response) => response.json()) - .then((data) => { - if (this.core.errors(data.errors, false, false)) { - // Сгенерированы ошибки - } else { - // Не сгенерированы ошибки (подразумевается их отсутствие) - - // Инициализация идентифиатора - const id = row.getAttribute("id"); - - // Инициализация количества непрочитанных сообщений - const messages = row.lastElementChild.innerText; - - // Инициализация статуса активной строки - const selected = row.getAttribute("data-selected"); - - // Реинициализация строки - row.outerHTML = data.rows; - - // Реинициализация перезаписанной строки - row = document.getElementById(id); - - // Копирование статуса активной строки - if ( - typeof selected === "string" && selected === "true" && - document.body.contains( - document.getElementById("popup"), - ) - ) row.setAttribute("data-selected", "true"); - - // Если непрочитанных сообщений стало больше, то проиграть звук уведомления - if (row.lastElementChild.innerText > messages) { - window.notification.play(); - } - - // Инициализация реинициализатора строки (для пересчёта таймера для setInterval) - if (row instanceof HTMLElement) { - setTimeout(() => this.start(row), 5000); - } - } - }); - } else row.setAttribute("data-counter", row.counter); - }, - Math.floor( - Math.random() * tasks.querySelectorAll( - ':scope > div[data-row="task"]', - ).length * - 200 + 300, - ), - ); - } - } - } - - /** - * Остановить - * - * @param {HTMLElement|null} target Строка - */ - static stop(target) { - // Инициализация оболочки - const tasks = document.getElementById("tasks"); - - for ( - const row of typeof target === "undefined" - ? tasks.querySelectorAll(':scope > div[data-row="task"]') - : [target] - ) { - // Перебор строк - - // Деинициализация самообновления - clearInterval(row.updater); - - // Удаление идентификатора - row.updater = row.counter = undefined; - - // Удаление анимации (сброс) - row.classList.remove("reinitialized"); - } - } - }; - }; - - // Инициализация ссылок на ядро - window.tasks.worker.core = - window.tasks.market.core = - window.tasks.commentary.core = - window.tasks.reinitializer.core = - window.tasks; + // Not initialized + + // Initialize of the class in global namespace + window.tasks = class tasks { + /** + * Заблокировать функцию закрытия всплывающего окна? + */ + static freeze = false; + + /** + * Тело всплывающего окна (массив) + */ + static body = {}; + + /** + * Инициализирован класс? + */ + static initialized = false; + + /** + * Создать заявку + * + * @param {HTMLElement} cashiers Колонка с кассирами + * @param {HTMLElement} displayers Колонка с выкладчиками + * @param {HTMLElement} gastronomes Колонка с гастрономов + * @param {HTMLElement} brigadiers Колонка с бригадирами + * @param {HTMLElement} loaders Колонка с грузчиками + * @param {HTMLElement} loaders_mobile Колонка с мобильными грузчиками + * @param {HTMLElement} universals_mobile Колонка с мобильными универсалами + * @param {HTMLElement} button Кнопка создания + * + * @return {void} + */ + static _create( + cashiers, + displayers, + gastronomes, + brigadiers, + loaders, + loaders_mobile, + universals_mobile, + button + ) { + if (cashiers instanceof HTMLElement) { + // Кассиры получены + + // Блокировка полей ввода + for (let i = 1; i < cashiers.childElementCount; i += 2) + if ( + cashiers.children[i].children[0].children instanceof HTMLCollection + ) + for (const element of cashiers.children[i].children[0].children) + element.setAttribute("disabled", true); + for (let i = 2; i < cashiers.childElementCount; i += 2) + if (cashiers.children[i].children[0] instanceof HTMLElement) + cashiers.children[i].children[0].setAttribute("disabled", true); + } + + if (displayers instanceof HTMLElement) { + // Выкладчики получены + + // Блокировка полей ввода + for (let i = 1; i < displayers.childElementCount; i += 2) + if ( + displayers.children[i].children[0].children instanceof + HTMLCollection + ) + for (const element of displayers.children[i].children[0].children) + element.setAttribute("disabled", true); + for (let i = 2; i < displayers.childElementCount; i += 2) + if (displayers.children[i].children[0] instanceof HTMLElement) + displayers.children[i].children[0].setAttribute("disabled", true); + } + + if (gastronomes instanceof HTMLElement) { + // Гастрономы получены + + // Блокировка полей ввода + for (let i = 1; i < gastronomes.childElementCount; i += 2) + if ( + gastronomes.children[i].children[0].children instanceof + HTMLCollection + ) + for (const element of gastronomes.children[i].children[0].children) + element.setAttribute("disabled", true); + for (let i = 2; i < gastronomes.childElementCount; i += 2) + if (gastronomes.children[i].children[0] instanceof HTMLElement) + gastronomes.children[i].children[0].setAttribute("disabled", true); + } + + if (brigadiers instanceof HTMLElement) { + // Бригадиры получены + + // Блокировка полей ввода + for (let i = 1; i < brigadiers.childElementCount; i += 2) + if ( + brigadiers.children[i].children[0].children instanceof + HTMLCollection + ) + for (const element of brigadiers.children[i].children[0].children) + element.setAttribute("disabled", true); + for (let i = 2; i < brigadiers.childElementCount; i += 2) + if (brigadiers.children[i].children[0] instanceof HTMLElement) + brigadiers.children[i].children[0].setAttribute("disabled", true); + } + + if (loaders instanceof HTMLElement) { + // Грузчики получены + + // Блокировка полей ввода + for (let i = 1; i < loaders.childElementCount; i += 2) + if ( + loaders.children[i].children[0].children instanceof HTMLCollection + ) + for (const element of loaders.children[i].children[0].children) + element.setAttribute("disabled", true); + for (let i = 2; i < loaders.childElementCount; i += 2) + if (loaders.children[i].children[0] instanceof HTMLElement) + loaders.children[i].children[0].setAttribute("disabled", true); + } + + if (loaders_mobile instanceof HTMLElement) { + // Мобильные грузчики получены + + // Блокировка полей ввода + for (let i = 1; i < loaders_mobile.childElementCount; i += 2) + if ( + loaders_mobile.children[i].children[0].children instanceof + HTMLCollection + ) + for (const element of loaders_mobile.children[i].children[0] + .children) + element.setAttribute("disabled", true); + for (let i = 2; i < loaders_mobile.childElementCount; i += 2) + if (loaders_mobile.children[i].children[0] instanceof HTMLElement) + loaders_mobile.children[i].children[0].setAttribute( + "disabled", + true + ); + } + + if (universals_mobile instanceof HTMLElement) { + // Мобильные универсалы получены + + // Блокировка полей ввода + for (let i = 1; i < universals_mobile.childElementCount; i += 2) + if ( + universals_mobile.children[i].children[0].children instanceof + HTMLCollection + ) + for (const element of universals_mobile.children[i].children[0] + .children) + element.setAttribute("disabled", true); + for (let i = 2; i < universals_mobile.childElementCount; i += 2) + if (universals_mobile.children[i].children[0] instanceof HTMLElement) + universals_mobile.children[i].children[0].setAttribute( + "disabled", + true + ); + } + + // Блокировка кнопки + button.setAttribute("disabled", true); + + // Запуск выполнения + this.__create( + cashiers, + displayers, + gastronomes, + brigadiers, + loaders, + loaders_mobile, + universals_mobile, + button + ); + } + + /** + * Создать заявку + * + * @param {HTMLElement} cashiers Колонка с кассирами + * @param {HTMLElement} displayers Колонка с выкладчиками + * @param {HTMLElement} gastronomes Колонка с гастрономов + * @param {HTMLElement} brigadiers Колонка с бригадирами + * @param {HTMLElement} loaders Колонка с грузчиками + * @param {HTMLElement} loaders_mobile Колонка с мобильными грузчиками + * @param {HTMLElement} universals_mobile Колонка с мобильными универсалами + * @param {HTMLElement} button Кнопка создания + * + * @return {void} + */ + static __create = damper( + async ( + cashiers, + displayers, + gastronomes, + brigadiers, + loaders, + loaders_mobile, + universals_mobile, + button + ) => { + // Инициализация функции разблокировки + function unblock() { + if (cashiers instanceof HTMLElement) { + // Кассиры получены + + // Разблокировка полей ввода + for (let i = 1; i < cashiers.childElementCount; i += 2) + if ( + cashiers.children[i].children[0].children instanceof + HTMLCollection + ) + for (const element of cashiers.children[i].children[0].children) + element.removeAttribute("disabled"); + for (let i = 2; i < cashiers.childElementCount; i += 2) + if (cashiers.children[i].children[0] instanceof HTMLElement) + cashiers.children[i].children[0].removeAttribute("disabled"); + } + + if (displayers instanceof HTMLElement) { + // Выкладчики получены + + // Разблокировка полей ввода + for (let i = 1; i < displayers.childElementCount; i += 2) + if ( + displayers.children[i].children[0].children instanceof + HTMLCollection + ) + for (const element of displayers.children[i].children[0] + .children) + element.removeAttribute("disabled"); + for (let i = 2; i < displayers.childElementCount; i += 2) + if (displayers.children[i].children[0] instanceof HTMLElement) + displayers.children[i].children[0].removeAttribute("disabled"); + } + + if (gastronomes instanceof HTMLElement) { + // Гастрономы получены + + // Разблокировка полей ввода + for (let i = 1; i < gastronomes.childElementCount; i += 2) + if ( + gastronomes.children[i].children[0].children instanceof + HTMLCollection + ) + for (const element of gastronomes.children[i].children[0] + .children) + element.removeAttribute("disabled"); + for (let i = 2; i < gastronomes.childElementCount; i += 2) + if (gastronomes.children[i].children[0] instanceof HTMLElement) + gastronomes.children[i].children[0].removeAttribute("disabled"); + } + + if (brigadiers instanceof HTMLElement) { + // Бригадиры получены + + // Разблокировка полей ввода + for (let i = 1; i < brigadiers.childElementCount; i += 2) + if ( + brigadiers.children[i].children[0].children instanceof + HTMLCollection + ) + for (const element of brigadiers.children[i].children[0] + .children) + element.removeAttribute("disabled"); + for (let i = 2; i < brigadiers.childElementCount; i += 2) + if (brigadiers.children[i].children[0] instanceof HTMLElement) + brigadiers.children[i].children[0].removeAttribute("disabled"); + } + + if (loaders instanceof HTMLElement) { + // Грузчики получены + + // Разблокировка полей ввода + for (let i = 1; i < loaders.childElementCount; i += 2) + if ( + loaders.children[i].children[0].children instanceof + HTMLCollection + ) + for (const element of loaders.children[i].children[0].children) + element.removeAttribute("disabled"); + for (let i = 2; i < loaders.childElementCount; i += 2) + if (loaders.children[i].children[0] instanceof HTMLElement) + loaders.children[i].children[0].removeAttribute("disabled"); + } + + if (loaders_mobile instanceof HTMLElement) { + // Мобильные грузчики получены + + // Разблокировка полей ввода + for (let i = 1; i < loaders_mobile.childElementCount; i += 2) + if ( + loaders_mobile.children[i].children[0].children instanceof + HTMLCollection + ) + for (const element of loaders_mobile.children[i].children[0] + .children) + element.removeAttribute("disabled"); + for (let i = 2; i < loaders_mobile.childElementCount; i += 2) + if (loaders_mobile.children[i].children[0] instanceof HTMLElement) + loaders_mobile.children[i].children[0].removeAttribute( + "disabled" + ); + } + + if (universals_mobile instanceof HTMLElement) { + // Мобильные универсалы получены + + // Разблокировка полей ввода + for (let i = 1; i < universals_mobile.childElementCount; i += 2) + if ( + universals_mobile.children[i].children[0].children instanceof + HTMLCollection + ) + for (const element of universals_mobile.children[i].children[0] + .children) + element.removeAttribute("disabled"); + for (let i = 2; i < universals_mobile.childElementCount; i += 2) + if ( + universals_mobile.children[i].children[0] instanceof HTMLElement + ) + universals_mobile.children[i].children[0].removeAttribute( + "disabled" + ); + } + + // Разблокировка кнопки + button.removeAttribute("disabled"); + } + + // Запуск отсрочки разблокировки на случай, если сервер не отвечает + const timeout = setTimeout(() => { + this.errors(["Сервер не отвечает"]); + unblock(); + }, 5000); + + // Инициадизация буфера JSON для отправки + const body = {}; + + if (cashiers instanceof HTMLElement) { + // Кассиры получены + + // Инициализация реестра + body.cashiers = []; + + // Запись в буфер JSON для отправки + for (let i = 1; i < cashiers.childElementCount; i += 2) + if ( + cashiers.children[i].children[0].children instanceof + HTMLCollection + ) + body.cashiers.push({ + start: + cashiers.children[i].children[0].children[0].value ?? null, + end: cashiers.children[i].children[0].children[1].value ?? null, + date: + cashiers.children[i].children[0].children[2].valueAsDate / + 1000 ?? null, + commentary: cashiers.children[i + 1].children[0].value ?? null, + }); + } + + if (displayers instanceof HTMLElement) { + // Выкладчики получены + + // Инициализация реестра + body.displayers = []; + + // Запись в буфер JSON для отправки + for (let i = 1; i < displayers.childElementCount; i += 2) + if ( + displayers.children[i].children[0].children instanceof + HTMLCollection + ) + body.displayers.push({ + start: + displayers.children[i].children[0].children[0].value ?? null, + end: + displayers.children[i].children[0].children[1].value ?? null, + date: + displayers.children[i].children[0].children[2].valueAsDate / + 1000 ?? null, + commentary: + displayers.children[i + 1].children[0].value ?? null, + }); + } + + if (gastronomes instanceof HTMLElement) { + // Гастрономы получены + + // Инициализация реестра + body.gastronomes = []; + + // Запись в буфер JSON для отправки + for (let i = 1; i < gastronomes.childElementCount; i += 2) + if ( + gastronomes.children[i].children[0].children instanceof + HTMLCollection + ) + body.gastronomes.push({ + start: + gastronomes.children[i].children[0].children[0].value ?? null, + end: + gastronomes.children[i].children[0].children[1].value ?? null, + date: + gastronomes.children[i].children[0].children[2].valueAsDate / + 1000 ?? null, + commentary: + gastronomes.children[i + 1].children[0].value ?? null, + }); + } + + if (brigadiers instanceof HTMLElement) { + // Бригадиры получены + + // Инициализация реестра + body.brigadiers = []; + + // Запись в буфер JSON для отправки + for (let i = 1; i < brigadiers.childElementCount; i += 2) + if ( + brigadiers.children[i].children[0].children instanceof + HTMLCollection + ) + body.brigadiers.push({ + start: + brigadiers.children[i].children[0].children[0].value ?? null, + end: + brigadiers.children[i].children[0].children[1].value ?? null, + date: + brigadiers.children[i].children[0].children[2].valueAsDate / + 1000 ?? null, + commentary: + brigadiers.children[i + 1].children[0].value ?? null, + }); + } + + if (loaders instanceof HTMLElement) { + // Грузчики получены + + // Инициализация реестра + body.loaders = []; + + // Запись в буфер JSON для отправки + for (let i = 1; i < loaders.childElementCount; i += 2) + if ( + loaders.children[i].children[0].children instanceof HTMLCollection + ) + body.loaders.push({ + start: + loaders.children[i].children[0].children[0].value ?? null, + end: loaders.children[i].children[0].children[1].value ?? null, + date: loaders.children[i].children[0].children[2].value ?? null, + commentary: loaders.children[i + 1].children[0].value ?? null, + }); + } + + if (loaders_mobile instanceof HTMLElement) { + // Мобильные грузчики получены + + // Инициализация реестра + body.loaders_mobile = []; + + // Запись в буфер JSON для отправки + for (let i = 1; i < loaders_mobile.childElementCount; i += 2) + if ( + loaders_mobile.children[i].children[0].children instanceof + HTMLCollection + ) + body.loaders_mobile.push({ + start: + loaders_mobile.children[i].children[0].children[0].value ?? + null, + end: + loaders_mobile.children[i].children[0].children[1].value ?? + null, + date: + loaders_mobile.children[i].children[0].children[2] + .valueAsDate / 1000 ?? null, + commentary: + loaders_mobile.children[i + 1].children[0].value ?? null, + }); + } + + if (universals_mobile instanceof HTMLElement) { + // Мобильные универсалы получены + + // Инициализация реестра + body.universals_mobile = []; + + // Запись в буфер JSON для отправки + for (let i = 1; i < universals_mobile.childElementCount; i += 2) + if ( + universals_mobile.children[i].children[0].children instanceof + HTMLCollection + ) + body.universals_mobile.push({ + start: + universals_mobile.children[i].children[0].children[0].value ?? + null, + end: + universals_mobile.children[i].children[0].children[1].value ?? + null, + date: + universals_mobile.children[i].children[0].children[2] + .valueAsDate / 1000 ?? null, + commentary: + universals_mobile.children[i + 1].children[0].value ?? null, + }); + } + + // Запрос к серверу + return await fetch("/tasks/create", { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }) + .then((response) => response.json()) + .then((data) => { + // Удаление отсрочки разблокировки + clearTimeout(timeout); + + if (this.errors(data.errors)) { + // Сгенерированы ошибки + + // Разблокировка полей ввода и кнопок + unblock(); + } else { + // Не сгенерированы ошибки (подразумевается их отсутствие) + + if (this.body.wrap instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.buttons); + + // Сброс блокировки + this.freeze = false; + + // Деинициализация активного окна + this.body.wrap.remove(); + } + + // Реинициализация строк + this.reinit(); + } + }); + }, + 300 + ); + + /** + * Сгенерировать окно создания заявок + * + * @return {void} + */ + static create = damper(() => { + // Инициализация оболочки всплывающего окна + this.body.wrap = document.createElement("div"); + this.body.wrap.setAttribute("id", "popup"); + + // Инициализация всплывающего окна + const popup = document.createElement("section"); + popup.classList.add("list", "small"); + + // Инициализация заголовка всплывающего окна + const title = document.createElement("h3"); + title.classList.add("unselectable"); + title.innerText = "Создание заявки"; + + // Инициализация оболочки с основной информацией + const main = document.createElement("section"); + main.classList.add("main"); + + // Инициализация колонки с задачами + const tasks = document.createElement("div"); + tasks.classList.add("column"); + tasks.setAttribute("data-column", "tasks"); + + // Инициализация строки + const row_1 = document.createElement("div"); + row_1.classList.add("row", "merged"); + + // Инициализация оболочки для строки с кассирами + const cashier = document.createElement("label"); + cashier.setAttribute("id", "cashier"); + + // Инициализация заголовка для поля ввода кассиров + const cashier_title = document.createElement("b"); + cashier_title.classList.add("separated", "right", "unselectable"); + cashier_title.innerText = "Кассиры:"; + + // Инициализация поля ввода кассиров + const cashier_input = document.createElement("input"); + cashier_input.classList.add("small", "center", "cloud"); + cashier_input.setAttribute("type", "number"); + cashier_input.setAttribute("min", "0"); + cashier_input.setAttribute("max", "10"); + cashier_input.setAttribute("title", "Количество кассиров"); + cashier_input.value = 0; + + // Инициализация строки + const row_2 = document.createElement("div"); + row_2.classList.add("row", "merged"); + + // Инициализация оболочки для строки с выкладчиками + const displayer = document.createElement("label"); + displayer.setAttribute("id", "displayer"); + + // Инициализация заголовка для поля ввода выкладчиков + const displayer_title = document.createElement("b"); + displayer_title.classList.add("separated", "right", "unselectable"); + displayer_title.innerText = "Выкладчики:"; + + // Инициализация поля ввода выкладчиков + const displayer_input = document.createElement("input"); + displayer_input.classList.add("small", "center", "cloud"); + displayer_input.setAttribute("type", "number"); + displayer_input.setAttribute("min", "0"); + displayer_input.setAttribute("max", "10"); + displayer_input.setAttribute("title", "Количество выкладчиков"); + displayer_input.value = 0; + + // Инициализация строки + const row_3 = document.createElement("div"); + row_3.classList.add("row", "merged"); + + // Инициализация оболочки для строки с гастрономами + const gastronome = document.createElement("label"); + gastronome.setAttribute("id", "gastronome"); + + // Инициализация заголовка для поля ввода гастрономов + const gastronome_title = document.createElement("b"); + gastronome_title.classList.add("separated", "right", "unselectable"); + gastronome_title.innerText = "Гастрономы:"; + + // Инициализация поля ввода гастрономов + const gastronome_input = document.createElement("input"); + gastronome_input.classList.add("small", "center", "cloud"); + gastronome_input.setAttribute("type", "number"); + gastronome_input.setAttribute("min", "0"); + gastronome_input.setAttribute("max", "10"); + gastronome_input.setAttribute("title", "Количество гастрономов"); + gastronome_input.value = 0; + + // Инициализация строки + const row_4 = document.createElement("div"); + row_4.classList.add("row", "merged"); + + // Инициализация оболочки для строки с бригадирами + const brigadier = document.createElement("label"); + brigadier.setAttribute("id", "brigadier"); + + // Инициализация заголовка для поля ввода бригадиров + const brigadier_title = document.createElement("b"); + brigadier_title.classList.add("separated", "right", "unselectable"); + brigadier_title.innerText = "Бригадиры:"; + + // Инициализация поля ввода бригадиров + const brigadier_input = document.createElement("input"); + brigadier_input.classList.add("small", "center", "cloud"); + brigadier_input.setAttribute("type", "number"); + brigadier_input.setAttribute("min", "0"); + brigadier_input.setAttribute("max", "10"); + brigadier_input.setAttribute("title", "Количество бригадиров"); + brigadier_input.value = 0; + + // Инициализация строки + const row_5 = document.createElement("div"); + row_5.classList.add("row", "merged"); + + // Инициализация оболочки для строки с грузчиками + const loader = document.createElement("label"); + loader.setAttribute("id", "loader"); + + // Инициализация заголовка для поля ввода грузчиков + const loader_title = document.createElement("b"); + loader_title.classList.add("separated", "right", "unselectable"); + loader_title.innerText = "Грузчики:"; + + // Инициализация поля ввода грузчиков + const loader_input = document.createElement("input"); + loader_input.classList.add("small", "center", "cloud"); + loader_input.setAttribute("type", "number"); + loader_input.setAttribute("min", "0"); + loader_input.setAttribute("max", "10"); + loader_input.setAttribute("title", "Количество грузчиков"); + loader_input.value = 0; + + // Инициализация строки + const row_6 = document.createElement("div"); + row_6.classList.add("row", "merged"); + + // Инициализация оболочки для строки с мобильными грузчиками + const loader_mobile = document.createElement("label"); + loader_mobile.setAttribute("id", "loader_mobile"); + + // Инициализация заголовка для поля ввода мобильных грузчиков + const loader_mobile_title = document.createElement("b"); + loader_mobile_title.classList.add("separated", "right", "unselectable"); + loader_mobile_title.innerText = "Мобильные грузчики:"; + + // Инициализация поля ввода мобильных грузчиков + const loader_mobile_input = document.createElement("input"); + loader_mobile_input.classList.add("small", "center", "cloud"); + loader_mobile_input.setAttribute("type", "number"); + loader_mobile_input.setAttribute("min", "0"); + loader_mobile_input.setAttribute("max", "10"); + loader_mobile_input.setAttribute( + "title", + "Количество мобильных грузчиков" + ); + loader_mobile_input.value = 0; + + // Инициализация строки + const row_7 = document.createElement("div"); + row_7.classList.add("row", "merged"); + + // Инициализация оболочки для строки с мобильными универсалами + const universal_mobile = document.createElement("label"); + universal_mobile.setAttribute("id", "universal_mobile"); + + // Инициализация заголовка для поля ввода мобильных универсалов + const universal_mobile_title = document.createElement("b"); + universal_mobile_title.classList.add( + "separated", + "right", + "unselectable" + ); + universal_mobile_title.innerText = "Мобильные универсалы:"; + + // Инициализация поля ввода мобильных универсалов + const universal_mobile_input = document.createElement("input"); + universal_mobile_input.classList.add("small", "center", "cloud"); + universal_mobile_input.setAttribute("type", "number"); + universal_mobile_input.setAttribute("min", "0"); + universal_mobile_input.setAttribute("max", "10"); + universal_mobile_input.setAttribute( + "title", + "Количество мобильных универсалов" + ); + universal_mobile_input.value = 0; + + // Инициализация строки + const row_8 = document.createElement("div"); + row_8.classList.add("row", "buttons"); + + // Инициализация оболочки для кнопок + const buttons = document.createElement("label"); + + // Инициализация кнопки настройки создаваемых заявок + const next = document.createElement("button"); + next.classList.add("grass"); + next.innerText = "Продолжить"; + next.setAttribute( + "onclick", + `tasks.create_detailed( + this.parentElement.parentElement.parentElement.children[0].children[0].children[1], + this.parentElement.parentElement.parentElement.children[1].children[0].children[1], + this.parentElement.parentElement.parentElement.children[2].children[0].children[1], + this.parentElement.parentElement.parentElement.children[3].children[0].children[1], + this.parentElement.parentElement.parentElement.children[4].children[0].children[1], + this.parentElement.parentElement.parentElement.children[5].children[0].children[1], + this.parentElement.parentElement.parentElement.children[6].children[0].children[1], + )` + ); + + // Инициализация окна с ошибками + this.body.errors = document.createElement("section"); + this.body.errors.classList.add( + "errors", + "window", + "list", + "calculated", + "hidden" + ); + this.body.errors.setAttribute("data-errors", true); + + // Инициализация элемента-тела (оболочки) окна с ошибками + const errors = document.createElement("section"); + errors.classList.add("body"); + + // Инициализация элемента-списка ошибок + const dl = document.createElement("dl"); + + // Инициализация активного всплывающего окна + const old = document.getElementById("popup"); + + if (old instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.buttons); + + // Сброс блокировки + this.freeze = false; + + // Удаление активного окна + old.remove(); + } + + // Запись в документ + popup.appendChild(title); + + cashier.appendChild(cashier_title); + cashier.appendChild(cashier_input); + row_1.appendChild(cashier); + tasks.appendChild(row_1); + + displayer.appendChild(displayer_title); + displayer.appendChild(displayer_input); + row_2.appendChild(displayer); + tasks.appendChild(row_2); + + gastronome.appendChild(gastronome_title); + gastronome.appendChild(gastronome_input); + row_3.appendChild(gastronome); + tasks.appendChild(row_3); + + brigadier.appendChild(brigadier_title); + brigadier.appendChild(brigadier_input); + row_4.appendChild(brigadier); + tasks.appendChild(row_4); + + loader.appendChild(loader_title); + loader.appendChild(loader_input); + row_5.appendChild(loader); + tasks.appendChild(row_5); + + loader_mobile.appendChild(loader_mobile_title); + loader_mobile.appendChild(loader_mobile_input); + row_6.appendChild(loader_mobile); + tasks.appendChild(row_6); + + universal_mobile.appendChild(universal_mobile_title); + universal_mobile.appendChild(universal_mobile_input); + row_7.appendChild(universal_mobile); + tasks.appendChild(row_7); + + buttons.appendChild(next); + row_8.appendChild(buttons); + tasks.appendChild(row_8); + + main.appendChild(tasks); + popup.appendChild(main); + + this.body.wrap.appendChild(popup); + document.body.appendChild(this.body.wrap); + + errors.appendChild(dl); + this.body.errors.appendChild(errors); + this.body.wrap.appendChild(this.body.errors); + + // Инициализация ширины окна с ошибками + this.body.errors.style.setProperty( + "--calculated-width", + popup.offsetWidth + "px" + ); + + // Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup) + function top(errors) { + errors.style.setProperty("transition", "0s"); + errors.style.setProperty( + "--top", + popup.offsetTop + popup.offsetHeight + 12 + "px" + ); + setTimeout(() => errors.style.removeProperty("transition"), 100); + } + top(this.body.errors); + const resize = new ResizeObserver(() => top(this.body.errors)); + resize.observe(this.body.wrap); + + // Инициализация функции блокировки кнопки + const block = () => { + if ( + cashier_input.value > 0 || + displayer_input.value > 0 || + gastronome_input.value > 0 || + brigadier_input.value > 0 || + loader_input.value > 0 || + loader_mobile_input.value > 0 || + universal_mobile_input.value > 0 + ) { + next.removeAttribute("disabled"); + } else next.setAttribute("disabled", "true"); + }; + + // Добавление функции блокировки кнопки по событиям + cashier_input.addEventListener("keyup", block); + displayer_input.addEventListener("keyup", block); + gastronome_input.addEventListener("keyup", block); + brigadier_input.addEventListener("keyup", block); + loader_input.addEventListener("keyup", block); + loader_mobile_input.addEventListener("keyup", block); + universal_mobile_input.addEventListener("keyup", block); + + // Добавление функции блокировки кнопки по событиям + cashier_input.addEventListener("change", block); + displayer_input.addEventListener("change", block); + gastronome_input.addEventListener("change", block); + brigadier_input.addEventListener("change", block); + loader_input.addEventListener("change", block); + loader_mobile_input.addEventListener("change", block); + universal_mobile_input.addEventListener("change", block); + + // Первичная активация функции блокировки кнопки + block(); + + // Инициализация функции закрытия всплывающего окна + const click = () => { + // Блокировка + if (this.freeze) return; + + // Удаление всплывающего окна + this.body.wrap.remove(); + + // Удаление статуса активной строки + row.removeAttribute("data-selected"); + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.buttons); + + // Сброс блокировки + this.freeze = false; + }; + + // Инициализация функции добавления функции закрытия всплывающего окна + const enable = () => this.body.wrap.addEventListener("click", click); + + // Инициализация функции удаления функции закрытия всплывающего окна + const disable = () => this.body.wrap.removeEventListener("click", click); + + // Первичная активация функции удаления всплывающего окна + enable(); + + // Инициализация блокировки удаления окна при взаимодействии с select-элементом + for (const select of popup.getElementsByTagName("select")) { + // Перебор всех select-элементов + + // Инициализация функции блокировки удаления окна по событию + select.addEventListener("click", () => { + // Блокировка удаления окна + this.freeze = true; + + // Разблокировка удаления окна + setTimeout(() => (this.freeze = false), 100); + }); + + for (const option of select.getElementsByTagName("option")) { + // Перебор всех option-элементов + + // Инициализация функции блокировки удаления окна по событию + option.addEventListener("click", () => { + // Блокировка удаления окна + this.freeze = true; + + // Разблокировка удаления окна + setTimeout(() => (this.freeze = false), 100); + }); + } + } + + // Инициализация блокировки удаления окна при взаимодействии с textarea-элементом + for (const textarea of popup.getElementsByTagName("textarea")) { + // Перебор всех textarea-элементов + + // Обновлять позицию окна с ошибками при изменении размера textarea (там position: absolute) + new MutationObserver(() => top(this.body.errors)).observe(textarea, { + attributes: true, + attributeFilter: ["style"], + }); + + // Инициализация функции игнорирования блокировки для выбранных кнопок + textarea.addEventListener("keydown", (e) => { + if (e.keyCode === 27) { + // Нажата кнопка: "escape" + + // Вызов глобальной функции управления кнопками + this.buttons(e, true); + } else if (e.ctrlKey && e.keyCode === 13) { + // Нажаты кнопки: "control", "enter" + + // Вызов глобальной функции управления кнопками + this.buttons(e, true); + } + }); + + // Добавление функции блокировки удаления окна и клавиш по событиям + textarea.addEventListener("focus", () => (this.freeze = true)); + textarea.addEventListener("focusout", () => (this.freeze = false)); + } + + // Инициализация функции управления кнопками + this.buttons = (e, force = false) => { + // Блокировка + if (!force && this.freeze) return; + + if (e.keyCode === 27) { + // Нажата кнопка: "escape" + + // Удаление окна + click(); + } else if (event.keyCode === 13) { + // Нажата кнопка: "enter" + + // Инициализация буфера с текущим статусом блокировки закрытия окна + const freeze = this.freeze; + + // Блокировка закрытия окна (чтобы не вызвался click() через событие onclick) + this.freeze = true; + + // Активация виртуальной кнопки "создать" + create.click(); + + // Возвращение статуса блокировки закрытия окна + this.freeze = freeze; + } + }; + + // Инициализация быстрых действий по кнопкам + document.addEventListener("keydown", this.buttons); + + // Добавление функции удаления всплывающего окна по событиям + popup.addEventListener("mouseenter", disable); + popup.addEventListener("mouseleave", enable); + + // Фокусировка + cashier_input.focus(); + }, 300); + + /** + * Сгенерировать окно продолжения создания заявок (настройки) + * + * @param {HTMLElement} cashiers Количество кассиров + * @param {HTMLElement} displayers Количество выкладчиков + * @param {HTMLElement} gastronomes Количество гастрономов + * @param {HTMLElement} brigadiers Количество бригадиров + * @param {HTMLElement} loaders Количество грузчиков + * @param {HTMLElement} loaders_mobile Количество мобильных грузчиков + * @param {HTMLElement} universals_mobile Количество мобильных универсалов + * + * @return {void} + */ + static create_detailed = damper( + ( + cashiers, + displayers, + gastronomes, + brigadiers, + loaders, + loaders_mobile, + universals_mobile + ) => { + // Инициализация всплывающего окна + const popup = this.body.wrap.children[0]; + + // Инициализация оболочки с основной информацией + const main = popup.children[1]; + main.classList.add("flow"); + + // Увеличение отступов между колонками + main.style.setProperty("--gap", "30px"); + + // Деинициализация колонки с задачами + main.children[0].remove(); + + // Деактиация стандартизированного размера всплывающего окна + popup.classList.remove("small"); + + if (cashiers.value > 0) { + // Кассиры запрошены + + // Инициализация колонки + const column = document.createElement("div"); + column.classList.add("column"); + column.setAttribute("data-column", "cashiers"); + + // Инициализация строки + const row = document.createElement("div"); + row.classList.add("row", "merged"); + + // Инициализация оболочки + const label = document.createElement("label"); + + // Инициализация заголовка + const title = document.createElement("b"); + title.classList.add("separated", "right", "unselectable"); + title.innerText = "Кассиры:"; + + // Запись в документ + label.appendChild(title); + row.appendChild(label); + column.appendChild(row); + main.appendChild(column); + + for (let i = 0; i < cashiers.value; ++i) { + // Перебор кассиров + + // Инициализация строки + const row = document.createElement("div"); + row.classList.add("row", "merged"); + if (i !== 0) row.classList.add("divided"); + + // Инициализация оболочки + const label = document.createElement("label"); + label.setAttribute("id", `cashier_${i}`); + + // Инициализация поля ввода времени начала + const start = document.createElement("input"); + start.classList.add("cloud"); + start.setAttribute("type", "time"); + start.setAttribute("title", "Время начала работы по заявке"); + start.value = "09:00"; + + // Инициализация поля ввода времени окончания + const end = document.createElement("input"); + end.classList.add("cloud"); + end.setAttribute("type", "time"); + end.setAttribute("title", "Время конца работы по заявке"); + end.value = "18:00"; + + // Инициализация поля ввода даты (дня) + const day = document.createElement("input"); + day.classList.add("cloud"); + day.setAttribute("type", "date"); + day.setAttribute("title", "Дата заявки"); + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + day.valueAsDate = tomorrow; + + // Инициализация строки + const row_commentary = document.createElement("div"); + row_commentary.classList.add("row", "merged", "stretchable"); + + // Инициализация поля ввода + const commentary = document.createElement("textarea"); + commentary.classList.add("snow"); + commentary.setAttribute("maxlength", "800"); + commentary.setAttribute( + "title", + "Дополнительная информация для администраторов и операторов" + ); + commentary.setAttribute("placeholder", "Дополнительная информация"); + + // Запись в документ + label.appendChild(start); + label.appendChild(end); + label.appendChild(day); + row.appendChild(label); + column.appendChild(row); + + row_commentary.appendChild(commentary); + column.appendChild(row_commentary); + } + } + + if (displayers.value > 0) { + // Выкладчикы запрошены + + // Инициализация колонки + const column = document.createElement("div"); + column.classList.add("column"); + column.setAttribute("data-column", "displayers"); + + // Инициализация строки + const row = document.createElement("div"); + row.classList.add("row", "merged"); + + // Инициализация оболочки + const label = document.createElement("label"); + + // Инициализация заголовка + const title = document.createElement("b"); + title.classList.add("separated", "right", "unselectable"); + title.innerText = "Выкладчики:"; + + // Запись в документ + label.appendChild(title); + row.appendChild(label); + column.appendChild(row); + main.appendChild(column); + + for (let i = 0; i < displayers.value; ++i) { + // Перебор выкладчиков + + // Инициализация строки + const row = document.createElement("div"); + row.classList.add("row", "merged"); + if (i !== 0) row.classList.add("divided"); + + // Инициализация оболочки + const label = document.createElement("label"); + label.setAttribute("id", `displayer_${i}`); + + // Инициализация поля ввода времени начала + const start = document.createElement("input"); + start.classList.add("cloud"); + start.setAttribute("type", "time"); + start.setAttribute("title", "Время начала работы по заявке"); + start.value = "09:00"; + + // Инициализация поля ввода времени окончания + const end = document.createElement("input"); + end.classList.add("cloud"); + end.setAttribute("type", "time"); + end.setAttribute("title", "Время конца работы по заявке"); + end.value = "18:00"; + + // Инициализация поля ввода даты (дня) + const day = document.createElement("input"); + day.classList.add("cloud"); + day.setAttribute("type", "date"); + day.setAttribute("title", "Дата заявки"); + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + day.valueAsDate = tomorrow; + + // Инициализация строки + const row_commentary = document.createElement("div"); + row_commentary.classList.add("row", "merged", "stretchable"); + + // Инициализация поля ввода + const commentary = document.createElement("textarea"); + commentary.classList.add("snow"); + commentary.setAttribute("maxlength", "800"); + commentary.setAttribute( + "title", + "Дополнительная информация для администраторов и операторов" + ); + commentary.setAttribute("placeholder", "Дополнительная информация"); + + // Запись в документ + label.appendChild(start); + label.appendChild(end); + label.appendChild(day); + row.appendChild(label); + column.appendChild(row); + + row_commentary.appendChild(commentary); + column.appendChild(row_commentary); + } + } + + if (gastronomes.value > 0) { + // Гастрономы запрошены + + // Инициализация колонки + const column = document.createElement("div"); + column.classList.add("column"); + column.setAttribute("data-column", "gastronomes"); + + // Инициализация строки + const row = document.createElement("div"); + row.classList.add("row", "merged"); + + // Инициализация оболочки + const label = document.createElement("label"); + + // Инициализация заголовка + const title = document.createElement("b"); + title.classList.add("separated", "right", "unselectable"); + title.innerText = "Гастрономы:"; + + // Запись в документ + label.appendChild(title); + row.appendChild(label); + column.appendChild(row); + main.appendChild(column); + + for (let i = 0; i < gastronomes.value; ++i) { + // Перебор гастрономов + + // Инициализация строки + const row = document.createElement("div"); + row.classList.add("row", "merged"); + if (i !== 0) row.classList.add("divided"); + + // Инициализация оболочки + const label = document.createElement("label"); + label.setAttribute("id", `gastronome_${i}`); + + // Инициализация поля ввода времени начала + const start = document.createElement("input"); + start.classList.add("cloud"); + start.setAttribute("type", "time"); + start.setAttribute("title", "Время начала работы по заявке"); + start.value = "09:00"; + + // Инициализация поля ввода времени окончания + const end = document.createElement("input"); + end.classList.add("cloud"); + end.setAttribute("type", "time"); + end.setAttribute("title", "Время конца работы по заявке"); + end.value = "18:00"; + + // Инициализация поля ввода даты (дня) + const day = document.createElement("input"); + day.classList.add("cloud"); + day.setAttribute("type", "date"); + day.setAttribute("title", "Дата заявки"); + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + day.valueAsDate = tomorrow; + + // Инициализация строки + const row_commentary = document.createElement("div"); + row_commentary.classList.add("row", "merged", "stretchable"); + + // Инициализация поля ввода + const commentary = document.createElement("textarea"); + commentary.classList.add("snow"); + commentary.setAttribute("maxlength", "800"); + commentary.setAttribute( + "title", + "Дополнительная информация для администраторов и операторов" + ); + commentary.setAttribute("placeholder", "Дополнительная информация"); + + // Запись в документ + label.appendChild(start); + label.appendChild(end); + label.appendChild(day); + row.appendChild(label); + column.appendChild(row); + + row_commentary.appendChild(commentary); + column.appendChild(row_commentary); + } + } + + if (brigadiers.value > 0) { + // Бригадиры запрошены + + // Инициализация колонки + const column = document.createElement("div"); + column.classList.add("column"); + column.setAttribute("data-column", "brigadiers"); + + // Инициализация строки + const row = document.createElement("div"); + row.classList.add("row", "merged"); + + // Инициализация оболочки + const label = document.createElement("label"); + + // Инициализация заголовка + const title = document.createElement("b"); + title.classList.add("separated", "right", "unselectable"); + title.innerText = "Бригадиры:"; + + // Запись в документ + label.appendChild(title); + row.appendChild(label); + column.appendChild(row); + main.appendChild(column); + + for (let i = 0; i < brigadiers.value; ++i) { + // Перебор бригадиров + + // Инициализация строки + const row = document.createElement("div"); + row.classList.add("row", "merged"); + if (i !== 0) row.classList.add("divided"); + + // Инициализация оболочки + const label = document.createElement("label"); + label.setAttribute("id", `brigadier_${i}`); + + // Инициализация поля ввода времени начала + const start = document.createElement("input"); + start.classList.add("cloud"); + start.setAttribute("type", "time"); + start.setAttribute("title", "Время начала работы по заявке"); + start.value = "09:00"; + + // Инициализация поля ввода времени окончания + const end = document.createElement("input"); + end.classList.add("cloud"); + end.setAttribute("type", "time"); + end.setAttribute("title", "Время конца работы по заявке"); + end.value = "18:00"; + + // Инициализация поля ввода даты (дня) + const day = document.createElement("input"); + day.classList.add("cloud"); + day.setAttribute("type", "date"); + day.setAttribute("title", "Дата заявки"); + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + day.valueAsDate = tomorrow; + + // Инициализация строки + const row_commentary = document.createElement("div"); + row_commentary.classList.add("row", "merged", "stretchable"); + + // Инициализация поля ввода + const commentary = document.createElement("textarea"); + commentary.classList.add("snow"); + commentary.setAttribute("maxlength", "800"); + commentary.setAttribute( + "title", + "Дополнительная информация для администраторов и операторов" + ); + commentary.setAttribute("placeholder", "Дополнительная информация"); + + // Запись в документ + label.appendChild(start); + label.appendChild(end); + label.appendChild(day); + row.appendChild(label); + column.appendChild(row); + + row_commentary.appendChild(commentary); + column.appendChild(row_commentary); + } + } + + if (loaders.value > 0) { + // Грузчики запрошены + + // Инициализация колонки + const column = document.createElement("div"); + column.classList.add("column"); + column.setAttribute("data-column", "loaders"); + + // Инициализация строки + const row = document.createElement("div"); + row.classList.add("row", "merged"); + + // Инициализация оболочки + const label = document.createElement("label"); + + // Инициализация заголовка + const title = document.createElement("b"); + title.classList.add("separated", "right", "unselectable"); + title.innerText = "Грузчики:"; + + // Запись в документ + label.appendChild(title); + row.appendChild(label); + column.appendChild(row); + main.appendChild(column); + + for (let i = 0; i < loaders.value; ++i) { + // Перебор грузчиков + + // Инициализация строки + const row = document.createElement("div"); + row.classList.add("row", "merged"); + if (i !== 0) row.classList.add("divided"); + + // Инициализация оболочки + const label = document.createElement("label"); + label.setAttribute("id", `loader_${i}`); + + // Инициализация поля ввода времени начала + const start = document.createElement("input"); + start.classList.add("cloud"); + start.setAttribute("type", "time"); + start.setAttribute("title", "Время начала работы по заявке"); + start.value = "09:00"; + + // Инициализация поля ввода времени окончания + const end = document.createElement("input"); + end.classList.add("cloud"); + end.setAttribute("type", "time"); + end.setAttribute("title", "Время конца работы по заявке"); + end.value = "18:00"; + + // Инициализация поля ввода даты (дня) + const day = document.createElement("input"); + day.classList.add("cloud"); + day.setAttribute("type", "date"); + day.setAttribute("title", "Дата заявки"); + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + day.valueAsDate = tomorrow; + + // Инициализация строки + const row_commentary = document.createElement("div"); + row_commentary.classList.add("row", "merged", "stretchable"); + + // Инициализация поля ввода + const commentary = document.createElement("textarea"); + commentary.classList.add("snow"); + commentary.setAttribute("maxlength", "800"); + commentary.setAttribute( + "title", + "Дополнительная информация для администраторов и операторов" + ); + commentary.setAttribute("placeholder", "Дополнительная информация"); + + // Запись в документ + label.appendChild(start); + label.appendChild(end); + label.appendChild(day); + row.appendChild(label); + column.appendChild(row); + + row_commentary.appendChild(commentary); + column.appendChild(row_commentary); + } + } + + if (loaders_mobile.value > 0) { + // Мобильные грузчики запрошены + + // Инициализация колонки + const column = document.createElement("div"); + column.classList.add("column"); + column.setAttribute("data-column", "loaders_mobile"); + + // Инициализация строки + const row = document.createElement("div"); + row.classList.add("row", "merged"); + + // Инициализация оболочки + const label = document.createElement("label"); + + // Инициализация заголовка + const title = document.createElement("b"); + title.classList.add("separated", "right", "unselectable"); + title.innerText = "Мобильные грузчики:"; + + // Запись в документ + label.appendChild(title); + row.appendChild(label); + column.appendChild(row); + main.appendChild(column); + + for (let i = 0; i < loaders_mobile.value; ++i) { + // Перебор мобильных грузчиков + + // Инициализация строки + const row = document.createElement("div"); + row.classList.add("row", "merged"); + if (i !== 0) row.classList.add("divided"); + + // Инициализация оболочки + const label = document.createElement("label"); + label.setAttribute("id", `loader_mobile_${i}`); + + // Инициализация поля ввода времени начала + const start = document.createElement("input"); + start.classList.add("cloud"); + start.setAttribute("type", "time"); + start.setAttribute("title", "Время начала работы по заявке"); + start.value = "09:00"; + + // Инициализация поля ввода времени окончания + const end = document.createElement("input"); + end.classList.add("cloud"); + end.setAttribute("type", "time"); + end.setAttribute("title", "Время конца работы по заявке"); + end.value = "18:00"; + + // Инициализация поля ввода даты (дня) + const day = document.createElement("input"); + day.classList.add("cloud"); + day.setAttribute("type", "date"); + day.setAttribute("title", "Дата заявки"); + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + day.valueAsDate = tomorrow; + + // Инициализация строки + const row_commentary = document.createElement("div"); + row_commentary.classList.add("row", "merged", "stretchable"); + + // Инициализация поля ввода + const commentary = document.createElement("textarea"); + commentary.classList.add("snow"); + commentary.setAttribute("maxlength", "800"); + commentary.setAttribute( + "title", + "Дополнительная информация для администраторов и операторов" + ); + commentary.setAttribute("placeholder", "Дополнительная информация"); + + // Запись в документ + label.appendChild(start); + label.appendChild(end); + label.appendChild(day); + row.appendChild(label); + column.appendChild(row); + + row_commentary.appendChild(commentary); + column.appendChild(row_commentary); + } + } + + if (universals_mobile.value > 0) { + // Мобильные универсалы запрошены + + // Инициализация колонки + const column = document.createElement("div"); + column.classList.add("column"); + column.setAttribute("data-column", "universals_mobile"); + + // Инициализация строки + const row = document.createElement("div"); + row.classList.add("row", "merged"); + + // Инициализация оболочки + const label = document.createElement("label"); + + // Инициализация заголовка + const title = document.createElement("b"); + title.classList.add("separated", "right", "unselectable"); + title.innerText = "Мобильные универсалы:"; + + // Запись в документ + label.appendChild(title); + row.appendChild(label); + column.appendChild(row); + main.appendChild(column); + + for (let i = 0; i < universals_mobile.value; ++i) { + // Перебор мобильных универсалов + + // Инициализация строки + const row = document.createElement("div"); + row.classList.add("row", "merged"); + if (i !== 0) row.classList.add("divided"); + + // Инициализация оболочки + const label = document.createElement("label"); + label.setAttribute("id", `universal_mobile_${i}`); + + // Инициализация поля ввода времени начала + const start = document.createElement("input"); + start.classList.add("cloud"); + start.setAttribute("type", "time"); + start.setAttribute("title", "Время начала работы по заявке"); + start.value = "09:00"; + + // Инициализация поля ввода времени окончания + const end = document.createElement("input"); + end.classList.add("cloud"); + end.setAttribute("type", "time"); + end.setAttribute("title", "Время конца работы по заявке"); + end.value = "18:00"; + + // Инициализация поля ввода даты (дня) + const day = document.createElement("input"); + day.classList.add("cloud"); + day.setAttribute("type", "date"); + day.setAttribute("title", "Дата заявки"); + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + day.valueAsDate = tomorrow; + + // Инициализация строки + const row_commentary = document.createElement("div"); + row_commentary.classList.add("row", "merged", "stretchable"); + + // Инициализация поля ввода + const commentary = document.createElement("textarea"); + commentary.classList.add("snow"); + commentary.setAttribute("maxlength", "800"); + commentary.setAttribute( + "title", + "Дополнительная информация для администраторов и операторов" + ); + commentary.setAttribute("placeholder", "Дополнительная информация"); + + // Запись в документ + label.appendChild(start); + label.appendChild(end); + label.appendChild(day); + row.appendChild(label); + column.appendChild(row); + + row_commentary.appendChild(commentary); + column.appendChild(row_commentary); + } + } + + // Инициализация колонки + const column = document.createElement("div"); + column.classList.add("column"); + column.setAttribute("data-column", "buttons"); + + // Инициализация строки + const row = document.createElement("div"); + row.classList.add("row", "merged"); + + // Инициализация оболочки + const label = document.createElement("label"); + label.setAttribute("id", `create`); + + // Инициализация кнопки создания заявок + const create = document.createElement("button"); + create.classList.add("grass"); + create.innerText = "Создать"; + create.setAttribute( + "onclick", + `tasks._create( + this.parentElement.parentElement.parentElement.parentElement.querySelector('div[data-column="cashiers"]'), + this.parentElement.parentElement.parentElement.parentElement.querySelector('div[data-column="displayers"]'), + this.parentElement.parentElement.parentElement.parentElement.querySelector('div[data-column="gastronomes"]'), + this.parentElement.parentElement.parentElement.parentElement.querySelector('div[data-column="brigadiers"]'), + this.parentElement.parentElement.parentElement.parentElement.querySelector('div[data-column="loaders"]'), + this.parentElement.parentElement.parentElement.parentElement.querySelector('div[data-column="loaders_mobile"]'), + this.parentElement.parentElement.parentElement.parentElement.querySelector('div[data-column="universals_mobile"]'), + this + )` + ); + + // Запись в документ + label.appendChild(create); + row.appendChild(label); + column.appendChild(row); + main.appendChild(column); + + // Инициализация ширины окна с ошибками + this.body.errors.style.setProperty( + "--calculated-width", + popup.offsetWidth + "px" + ); + + // Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup) + function top(errors) { + errors.style.setProperty("transition", "0s"); + errors.style.setProperty( + "--top", + popup.offsetTop + popup.offsetHeight + 12 + "px" + ); + setTimeout(() => errors.style.removeProperty("transition"), 100); + } + top(this.body.errors); + }, + 300 + ); + + /** + * Записать или переключить фильтр (0, 1, 2) + * + * @param {string} name Название + * @param {string|number|null} value Значение + * @param {HTMLElement|null} button Кнопка + * + * @return {void} + */ + static filter = damper(async (name, value, button) => { + if (typeof name === "string") { + // Получено название + + // Инициализация сериализованного пути к директории + const path = `tasks_filter_${name}`; + + if (typeof value === "string" || typeof value === "number") { + // Получено значение + + // Запись нового значения + buffer.write(path, value); + } else { + // Не получено значение + + // Чтение текущего значения + value = +(await buffer.read(path)); + + // Инициализация значения по умолчанию + if (isNaN(value)) value = 0; + + // Запись нового значения (инвертирование) + buffer.write(path, ++value < 3 ? value : 0); + + if (button instanceof HTMLElement) { + // Получена кнопка + + // Деинициализация классов статуса фильтра + button.classList.remove("earth", "sand", "river"); + + // Инициализация классов статуса фильтра + if (value === 1) button.classList.add("sand"); + else if (value === 2) button.classList.add("river"); + else button.classList.add("earth"); + } + } + } + }, 300); + + /** + * Записать фильтр поиска + * + * @param {HTMLElement} input Строка поиска + * @param {HTMLElement} button Кнопка отправки + * + * @return {void} + */ + static search = (input, button) => { + input.setAttribute("readonly", true); + button.setAttribute("disabled", true); + + this._search(input, button); + }; + + /** + * Записать фильтр поиска (системное) + * + * Используется как оболочка для большей временной задержки + * + * @param {HTMLElement} input Строка поиска + * @param {HTMLElement} button Кнопка отправки + * + * @return {void} + */ + static _search = damper((input, button) => { + this.filter("search", input.value); + this.reinit(() => { + input.removeAttribute("readonly"); + button.removeAttribute("disabled"); + }); + }, 1000); + + /** + * Инициализация страниц + * + * @param {number} amount Количество страниц для генерации + * @param {number} iteration Номер итерации (системное) + * + * @return {void} + */ + static init(amount = 3, iteration = 0) { + if ( + typeof amount === "number" && + typeof iteration === "number" && + this.initialized === false + ) { + // Получены количество страниц и номер итерации + + // Инициализация страницы + tasks.read(++iteration); + + function generate() { + // Завершено выполнение итерации + + // Деинициализация слушателя завершения текущей итерации + document.removeEventListener("tasks.read." + iteration, generate); + + // Проверка условий и запуск следующей итерации + if (iteration < amount) tasks.init(amount, iteration); + else this.initialized = true; + } + + // Инициализация слушателя завершения текущей итерации + document.addEventListener("tasks.read." + iteration, generate); + } + } + + /** + * Реинициализация страниц + * + * @param {function} postprocessing Функция которая будет исполнена после реинициализации + * @param {number} amount Количество страниц для генерации при инициализации + * + * @return {void} + */ + static reinit = damper((postprocessing, amount = 3) => { + if (typeof amount === "number") { + // Получены количество страниц + + // Деинициализация cookie с номером страницы + Cookies.remove("tasks_page", { path: "/" }); + + // Инициализация оболочки + const tasks = document.getElementById("tasks"); + + if (tasks instanceof HTMLElement) { + // Найдена оболочка + + // Инициализация буфера активного элемента + const active = document.activeElement; + + // Инициализация буфера элементов + const buffer = document.createElement("div"); + + // Перенос элементов в буфер + buffer.replaceChildren(...tasks.children); + + // Удаление комментариев с индексами страниц + tasks.innerHTML = tasks.innerHTML.replace( + //g, + "" + ); + + // Перенос элементов из буфера + tasks.replaceChildren(...buffer.children); + + // Деинициализация страниц + for (const row of tasks.querySelectorAll( + ':scope > div[data-row="task"]' + )) { + row.remove(); + } + + // Сброс статуса инициализированности + this.initialized = false; + + // Инициализация страниц + this.init(amount); + + // Возвращение фокуса на активный элемент + active.focus(); + + // Постобработка + if (typeof postprocessing === "function") postprocessing(); + } + } + }, 2000); + + /** + * Прочитать + * + * Читает список задач и модифицирует документ (записывает в общий список) + * + * @param {number} page Страница + * + * @return {void} + */ + static read = damper(async (page) => { + if (typeof page !== "number") { + // Не получена страница + + // Инициализация страницы (если не получена, то брать из cookie и прибавлять 1) + page = Cookies.get(`tasks_page`) ?? 0; + + // Запись идентификатора следующей страницы + ++page; + } + + // Запрос к серверу + await fetch("/tasks/read", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `page=${page}`, + }) + .then((response) => response.json()) + .then((data) => { + if (this.errors(data.errors)) { + // Сгенерированы ошибки + } else { + // Не сгенерированы ошибки (подразумевается их отсутствие) + + // Инициализация буфера для записи в документ + const element = document.createElement("div"); + + // Запись пустого элемента из буфера в документ + document.getElementById("tasks").appendChild(element); + + // Запись в документ HTML-данных через буфер + element.outerHTML = data.rows; + + // Запуск реинициализатора строк + this.reinitializer.start(); + + // Вызов события: "итерация чтения завершена" + document.dispatchEvent(new CustomEvent("tasks.read." + page)); + } + }); + }, 50); + + /** + * Сгенерировать всплывающее окно c данными задачи + * + * @param {HTMLElement} row Строка + * + * @return {void} + */ + static popup = damper(async (row) => { + if (row instanceof HTMLElement) { + // Получена строка + + // Инициализация идентификатора сотрудника + const task = row.getAttribute("id"); + + // Запрос к серверу + return await fetch(`/task/${task}/read`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }) + .then((response) => response.json()) + .then((data) => { + if (this.errors(data.errors)) { + // Сгенерированы ошибки + } else { + // Не сгенерированы ошибки + + if (((Date.now() / 1000) | 0) - data.start > 0) { + // Началась заявка (возможно уже закончилась) + + // Инициализация буфера выбранных строк + const buffer = [ + ...document.querySelectorAll('div[data-selected="true"]'), + ]; + + // Удаление статуса активной строки у остальных строк + for (const element of buffer) { + element.removeAttribute("data-selected"); + } + + // Инициализация статуса активной строки (обрабатываемой в данный момент) + row.setAttribute("data-selected", "true"); + + // Инициализация оболочки всплывающего окна + this.body.wrap = document.createElement("div"); + this.body.wrap.setAttribute("id", "popup"); + + // Инициализация всплывающего окна + const popup = document.createElement("section"); + popup.classList.add("list", "small"); + + // Инициализация заголовка всплывающего окна + const title = document.createElement("h3"); + title.classList.add("unselectable"); + title.innerText = task; + + // Инициализация оболочки с основной информацией + const main = document.createElement("section"); + main.classList.add("main"); + + // Инициализация колонки + const column = document.createElement("div"); + column.classList.add("column"); + + // Инициализация буфера для записи во всплывающее окно + const list = document.createElement("div"); + + // Инициализация строки рейтинга + const rating_row = document.createElement("div"); + rating_row.classList.add( + "row", + "divided", + "range", + "small", + "stretched" + ); + + // Инициализация ползунка выбора рейтинга + const rating_input = document.createElement("input"); + rating_input.classList.add("cloud"); + rating_input.setAttribute("type", "range"); + rating_input.setAttribute("title", "Оценка"); + rating_input.setAttribute("min", "1"); + rating_input.setAttribute("max", "5"); + rating_input.setAttribute("step", "1"); + rating_input.setAttribute( + "oninput", + `this.nextElementSibling.innerText = this.value; setTimeout(() => {this.nextElementSibling.style.opacity = 1; setTimeout(() => this.nextElementSibling.style.transition = "0s", 100)}, 100); this.nextElementSibling.style.setProperty('--left', (((this.value - 1) / 4) * (this.offsetWidth - 24) + 12) + 'px');` + ); + + // Инициализация значения в курсоре ползунка выбора рейтинга + const rating_value = document.createElement("i"); + rating_value.classList.add("value", "unselectable"); + rating_value.style.opacity = 0; + rating_value.style.transition = "0.1s ease-in"; + rating_value.setAttribute("title", "Оценка"); + rating_value.value = 5; + + // Инициализация поля ввода отзыва + const review = document.createElement("textarea"); + review.classList.add("snow"); + review.setAttribute("autofocus", "true"); + review.setAttribute("maxlength", "300"); + review.setAttribute("title", "Отзыв"); + review.setAttribute("placeholder", "Отзыв о сотруднике"); + + // Запись актуальных значений + this.value(row, "completed").then((completed) => { + // Рейтинг + this.value(row, "rating").then((text) => { + if (completed) { + // Завершена заявка + + // Блокировка + rating_input.setAttribute("disabled", "true"); + } + + // Запись актуального рейтинга + rating_input.value = text ?? 5; + }); + + // Отзыв + this.value(row, "review").then((text) => { + if (completed) { + // Завершена заявка + + // Блокировка + review.setAttribute("readonly", "true"); + } + + // Запись актуального отзыва + review.value = text ?? ""; + }); + }); + + // Инициализация оболочки для кнопок + const buttons = document.createElement("div"); + buttons.classList.add("row", "buttons"); + + // Инициализация кнопки заявления о проблеме + const problem = document.createElement("button"); + problem.classList.add("clay", "stretched"); + if (row.classList.contains("problematic")) { + // Проблемная заявка + + problem.classList.add("grass"); + problem.innerText = "Проблема решена"; + + problem.setAttribute( + "onclick", + `chat.send(this, null, document.getElementById('${task}'), 'market', 'solution', true, 'Проблема решена'); document.removeEventListener("keydown", tasks.buttons);` + ); + } else { + // Не проблемная заявка + + problem.classList.add("clay"); + problem.innerText = "Проблема"; + + problem.setAttribute( + "onclick", + `tasks.problem(document.getElementById('${task}'))` + ); + } + + // Инициализация кнопки подтверждения + const complete = document.createElement("button"); + complete.classList.add("grass", "stretched"); + complete.innerText = "Завершить"; + complete.setAttribute( + "onclick", + `tasks.complete(this, this.parentElement.previousElementSibling.previousElementSibling.children[0], this.parentElement.previousElementSibling, document.getElementById('${task}'))` + ); + + // Инициализация окна с ошибками + this.body.errors = document.createElement("section"); + this.body.errors.classList.add( + "errors", + "window", + "list", + "small", + "hidden" + ); + this.body.errors.setAttribute("data-errors", true); + + // Инициализация элемента-тела (оболочки) окна с ошибками + const errors = document.createElement("section"); + errors.classList.add("body"); + + // Инициализация элемента-списка ошибок + const dl = document.createElement("dl"); + + // Инициализация активного всплывающего окна + const old = document.getElementById("popup"); + + if (old instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.buttons); + + // Сброс блокировки + this.freeze = false; + + // Удаление активного окна + old.remove(); + } + + // Удаление блокировки выполнения закрытия всплывающего окна + this.freeze = false; + + // Запись в документ + popup.appendChild(title); + + column.appendChild(list); + rating_row.appendChild(rating_input); + rating_row.appendChild(rating_value); + column.appendChild(rating_row); + column.appendChild(review); + + if (data.completed !== true) { + // Зявка не завершена + + buttons.appendChild(problem); + buttons.appendChild(complete); + column.appendChild(buttons); + } + + main.appendChild(column); + popup.appendChild(main); + this.body.wrap.appendChild(popup); + + errors.appendChild(dl); + this.body.errors.appendChild(errors); + this.body.wrap.appendChild(this.body.errors); + + document.body.appendChild(this.body.wrap); + + // Запись в документ HTML-данных через буфер + list.outerHTML = data.task; + + // Первичная инициализация (для генерации местоположения элемента со значением) + setTimeout(() => rating_input.oninput(), 300); + + // Инициализация ошибок + this.errors(data.errors); + + // Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup) + function top(errors) { + errors.style.setProperty("transition", "0s"); + errors.style.setProperty( + "--top", + popup.offsetTop + popup.offsetHeight + 12 + "px" + ); + setTimeout( + () => errors.style.removeProperty("transition"), + 100 + ); + } + top(this.body.errors); + const resize = new ResizeObserver(() => top(this.body.errors)); + resize.observe(this.body.wrap); + + // Инициализация функции закрытия всплывающего окна + const click = () => { + // Блокировка + if (this.freeze) return; + + // Удаление всплывающего окна + this.body.wrap.remove(); + + // Инициализация буфера выбранных строк + const buffer = [ + ...document.querySelectorAll('div[data-selected="true"]'), + ]; + + // Удаление статуса активной строки у остальных строк + for (const element of buffer) { + element.removeAttribute("data-selected"); + } + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.buttons); + + // Сброс блокировки + this.freeze = false; + }; + + // Инициализация функции добавления функции закрытия всплывающего окна + const enable = () => + this.body.wrap.addEventListener("click", click); + + // Инициализация функции удаления функции закрытия всплывающего окна + const disable = () => + this.body.wrap.removeEventListener("click", click); + + // Первичная активация функции удаления всплывающего окна + enable(); + + // Инициализация блокировки удаления окна при взаимодействии с select-элементом + for (const select of popup.getElementsByTagName("select")) { + // Перебор всех select-элементов + + // Инициализация функции блокировки удаления окна по событию + select.addEventListener("click", () => { + // Блокировка удаления окна + this.freeze = true; + + // Разблокировка удаления окна + setTimeout(() => (this.freeze = false), 100); + }); + + for (const option of select.getElementsByTagName("option")) { + // Перебор всех option-элементов + + // Инициализация функции блокировки удаления окна по событию + option.addEventListener("click", () => { + // Блокировка удаления окна + this.freeze = true; + + // Разблокировка удаления окна + setTimeout(() => (this.freeze = false), 100); + }); + } + } + + // Инициализация блокировки удаления окна при взаимодействии с textarea-элементом + for (const textarea of popup.getElementsByTagName("textarea")) { + // Перебор всех textarea-элементов + + // Обновлять позицию окна с ошибками при изменении размера textarea (там position: absolute) + new MutationObserver(() => top(this.body.errors)).observe( + textarea, + { + attributes: true, + attributeFilter: ["style"], + } + ); + + // Инициализация функции игнорирования блокировки для выбранных кнопок + textarea.addEventListener("keydown", (e) => { + if (e.keyCode === 27) { + // Нажата кнопка: "escape" + + // Вызов глобальной функции управления кнопками + this.buttons(e, true); + } else if (e.ctrlKey && e.keyCode === 13) { + // Нажаты кнопки: "control", "enter" + + // Вызов глобальной функции управления кнопками + this.buttons(e, true); + } + }); + + // Добавление функции блокировки удаления окна и клавиш по событиям + textarea.addEventListener( + "focus", + () => (this.freeze = true) + ); + textarea.addEventListener( + "focusout", + () => (this.freeze = false) + ); + } + + // Инициализация функции управления кнопками + this.buttons = (e, force = false) => { + // Блокировка + if (!force && this.freeze) return; + + if (e.keyCode === 27) { + // Нажата кнопка: "escape" + + // Удаление окна + click(); + } else if (event.keyCode === 13) { + // Нажата кнопка: "enter" + + // Инициализация буфера с текущим статусом блокировки закрытия окна + const freeze = this.freeze; + + // Блокировка закрытия окна (чтобы не вызвался click() через событие onclick) + this.freeze = true; + + // Активация виртуальной кнопки "завершить" + complete.click(); + + // Возвращение статуса блокировки закрытия окна + this.freeze = freeze; + } + }; + + // Инициализация быстрых действий по кнопкам + document.addEventListener("keydown", this.buttons); + + // Добавление функции удаления всплывающего окна по событиям + popup.addEventListener("mouseenter", disable); + popup.addEventListener("mouseleave", enable); + + // Фокусировка + review.focus(); + } else { + // Не началась заявка + + // Инициализация буфера выбранных строк + const buffer = [ + ...document.querySelectorAll('div[data-selected="true"]'), + ]; + + // Удаление статуса активной строки у остальных строк + for (const element of buffer) { + element.removeAttribute("data-selected"); + } + + // Инициализация статуса активной строки (обрабатываемой в данный момент) + row.setAttribute("data-selected", "true"); + + // Инициализация оболочки всплывающего окна + this.body.wrap = document.createElement("div"); + this.body.wrap.setAttribute("id", "popup"); + + // Инициализация всплывающего окна + const popup = document.createElement("section"); + popup.classList.add("list", "small"); + + // Инициализация заголовка всплывающего окна + const title = document.createElement("h3"); + title.classList.add("unselectable"); + title.innerText = task; + + // Инициализация оболочки с основной информацией + const main = document.createElement("section"); + main.classList.add("main"); + + // Инициализация колонки + const column = document.createElement("div"); + column.classList.add("column"); + + // Инициализация буфера для записи во всплывающее окно + const list = document.createElement("div"); + + // Инициализация оболочки для строки с полем ввода даты и времени, и кнопкой + const _row = document.createElement("div"); + _row.classList.add("row", "divided"); + + // Инициализация оболочки для строки с полем ввода даты + const label = document.createElement("label"); + + // Инициализация поля ввода даты + const date = document.createElement("input"); + date.classList.add("cloud"); + date.setAttribute("type", "date"); + date.setAttribute("title", "Дата заявки"); + this.value(row, "date").then( + (text) => (date.valueAsDate = new Date(text * 1000)) + ); + + // Инициализация поля ввода времени начала + const start = document.createElement("input"); + start.classList.add("cloud"); + start.setAttribute("type", "time"); + start.setAttribute("title", "Время начала работы"); + this.value(row, "start").then( + (text) => + (start.value = + typeof text === "string" + ? text.length === 7 + ? 0 + text.replace(/(?<=:00):00/, "") + : text.replace(/(?<=:00):00/, "") + : "00:00") + ); + + // Инициализация поля ввода времени окончания + const end = document.createElement("input"); + end.classList.add("cloud"); + end.setAttribute("type", "time"); + end.setAttribute("title", "Время конца работы"); + this.value(row, "end").then( + (text) => + (end.value = + typeof text === "string" + ? text.length === 7 + ? 0 + text.replace(/(?<=:00):00/, "") + : text.replace(/(?<=:00):00/, "") + : "00:00") + ); + + // Инициализация кнопки подтверждения даты + const replace = document.createElement("button"); + replace.classList.add("sea"); + replace.innerText = "Записать"; + replace.setAttribute( + "onclick", + `tasks.date(document.getElementById('${task}'), this.previousElementSibling.previousElementSibling.previousElementSibling, this.previousElementSibling.previousElementSibling, this.previousElementSibling, this)` + ); + + // Инициализация списка выбора типа работы + const work = document.createElement("select"); + work.classList.add("row", "connected", "stretched"); + this.works(row).then((html) => (work.innerHTML = html)); + work.setAttribute("title", "Тип работы"); + + // Инициализация поля ввода описания + const description = document.createElement("textarea"); + description.classList.add("snow"); + this.value(row, "description").then( + (text) => (description.value = text) + ); + description.setAttribute("autofocus", "true"); + description.setAttribute("maxlength", "300"); + description.setAttribute( + "onkeyup", + `tasks.description(document.getElementById('${task}'), this)` + ); + description.setAttribute("title", "Дополнительная информация"); + description.setAttribute( + "placeholder", + "Дополнительная информация о требуемой работе" + ); + + // Инициализация оболочки для кнопок + const buttons_1 = document.createElement("div"); + buttons_1.classList.add("row", "merged", "divided", "buttons"); + + // Инициализация кнопки подтверждения + const problem = document.createElement("button"); + problem.classList.add("wide"); + if (row.classList.contains("problematic")) { + // Проблемная заявка + + problem.classList.add("grass"); + problem.innerText = "Проблема решена"; + + problem.setAttribute( + "onclick", + `chat.send(this, null, document.getElementById('${task}'), 'market', 'solution', true, 'Проблема решена'); document.removeEventListener("keydown", tasks.buttons);` + ); + } else { + // Не проблемная заявка + + problem.classList.add("clay"); + problem.innerText = "Проблема"; + + problem.setAttribute( + "onclick", + `tasks.problem(document.getElementById('${task}'))` + ); + } + + // Инициализация оболочки для кнопок + const buttons_2 = document.createElement("div"); + buttons_2.classList.add("row", "merged", "buttons"); + + // Инициализация кнопки подтверждения + const confirm = document.createElement("button"); + confirm.classList.add("wide"); + if (row.classList.contains("confirmed")) { + // Подтверждена заявка + + confirm.classList.add("clay"); + confirm.innerText = "Отклонить"; + } else { + // Не подтверждена заявка + + confirm.classList.add("grass"); + confirm.innerText = "Подтвердить"; + } + confirm.setAttribute( + "onclick", + `tasks.confirm(this, document.getElementById('${task}'))` + ); + + // Инициализация кнопки скрытия + const hide = document.createElement("button"); + if (row.classList.contains("hided")) { + // Скрыта заявка + + hide.classList.add("grass"); + hide.innerText = "Показать"; + } else { + // Не подтверждена заявка + + hide.classList.add("sea"); + hide.innerText = "Скрыть"; + } + hide.setAttribute( + "onclick", + `tasks.hide(this, document.getElementById('${task}'))` + ); + + // Инициализация кнопки удаления + const remove = document.createElement("button"); + remove.classList.add("clay"); + remove.innerText = "Удалить"; + remove.setAttribute( + "onclick", + `tasks.remove(this, document.getElementById('${task}'))` + ); + + if ( + typeof core === "function" && + (core.interface === "market" || core.interface === "worker") + ) { + // Расширить кнопку для магазина и сотрудника + + remove.classList.add("stretched"); + } + + // Инициализация окна с ошибками + this.body.errors = document.createElement("section"); + this.body.errors.classList.add( + "errors", + "window", + "list", + "small", + "hidden" + ); + this.body.errors.setAttribute("data-errors", true); + + // Инициализация элемента-тела (оболочки) окна с ошибками + const errors = document.createElement("section"); + errors.classList.add("body"); + + // Инициализация элемента-списка ошибок + const dl = document.createElement("dl"); + + // Инициализация активного всплывающего окна + const old = document.getElementById("popup"); + + if (old instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.buttons); + + // Сброс блокировки + this.freeze = false; + + // Удаление активного окна + old.remove(); + } + + // Удаление блокировки выполнения закрытия всплывающего окна + this.freeze = false; + + // Запись в документ + popup.appendChild(title); + + column.appendChild(list); + label.appendChild(date); + label.appendChild(start); + label.appendChild(end); + label.appendChild(replace); + _row.appendChild(label); + column.appendChild(_row); + column.appendChild(work); + column.appendChild(description); + + if ( + typeof core === "function" && + core.interface !== "market" && + core.interface !== "worker" + ) { + // Скрытие для магазина и сотрудника + + buttons_1.appendChild(problem); + column.appendChild(buttons_1); + + buttons_2.appendChild(confirm); + buttons_2.appendChild(hide); + } else { + // Отображение для магазина и сотрудника + + problem.classList.add("stretched"); + buttons_2.appendChild(problem); + } + + buttons_2.appendChild(remove); + column.appendChild(buttons_2); + + main.appendChild(column); + popup.appendChild(main); + this.body.wrap.appendChild(popup); + + errors.appendChild(dl); + this.body.errors.appendChild(errors); + this.body.wrap.appendChild(this.body.errors); + + document.body.appendChild(this.body.wrap); + + // Запись в документ HTML-данных через буфер + list.outerHTML = data.task; + + // Инициализация ошибок + this.errors(data.errors); + + // Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup) + function top(errors) { + errors.style.setProperty("transition", "0s"); + errors.style.setProperty( + "--top", + popup.offsetTop + popup.offsetHeight + 12 + "px" + ); + setTimeout( + () => errors.style.removeProperty("transition"), + 100 + ); + } + top(this.body.errors); + const resize = new ResizeObserver(() => top(this.body.errors)); + resize.observe(this.body.wrap); + + // Инициализация функции закрытия всплывающего окна + const click = () => { + // Блокировка + if (this.freeze) return; + + // Удаление всплывающего окна + this.body.wrap.remove(); + + // Инициализация буфера выбранных строк + const buffer = [ + ...document.querySelectorAll('div[data-selected="true"]'), + ]; + + // Удаление статуса активной строки у остальных строк + for (const element of buffer) { + element.removeAttribute("data-selected"); + } + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.buttons); + + // Сброс блокировки + this.freeze = false; + }; + + // Инициализация функции добавления функции закрытия всплывающего окна + const enable = () => + this.body.wrap.addEventListener("click", click); + + // Инициализация функции удаления функции закрытия всплывающего окна + const disable = () => + this.body.wrap.removeEventListener("click", click); + + // Первичная активация функции удаления всплывающего окна + enable(); + + // Инициализация блокировки удаления окна при взаимодействии с select-элементом + for (const select of popup.getElementsByTagName("select")) { + // Перебор всех select-элементов + + // Инициализация функции блокировки удаления окна по событию + select.addEventListener("click", () => { + // Блокировка удаления окна + this.freeze = true; + + // Разблокировка удаления окна + setTimeout(() => (this.freeze = false), 100); + }); + + for (const option of select.getElementsByTagName("option")) { + // Перебор всех option-элементов + + // Инициализация функции блокировки удаления окна по событию + option.addEventListener("click", () => { + // Блокировка удаления окна + this.freeze = true; + + // Разблокировка удаления окна + setTimeout(() => (this.freeze = false), 100); + }); + } + } + + // Инициализация блокировки удаления окна при взаимодействии с textarea-элементом + for (const textarea of popup.getElementsByTagName("textarea")) { + // Перебор всех textarea-элементов + + // Обновлять позицию окна с ошибками при изменении размера textarea (там position: absolute) + new MutationObserver(() => top(this.body.errors)).observe( + textarea, + { + attributes: true, + attributeFilter: ["style"], + } + ); + + // Инициализация функции игнорирования блокировки для выбранных кнопок + textarea.addEventListener("keydown", (e) => { + if (e.keyCode === 27) { + // Нажата кнопка: "escape" + + // Вызов глобальной функции управления кнопками + this.buttons(e, true); + } else if (e.ctrlKey && e.keyCode === 13) { + // Нажаты кнопки: "control", "enter" + + // Вызов глобальной функции управления кнопками + this.buttons(e, true); + } + }); + + // Добавление функции блокировки удаления окна и клавиш по событиям + textarea.addEventListener( + "focus", + () => (this.freeze = true) + ); + textarea.addEventListener( + "focusout", + () => (this.freeze = false) + ); + } + + // Инициализация функции управления кнопками + this.buttons = (e, force = false) => { + // Блокировка + if (!force && this.freeze) return; + + if (e.keyCode === 27) { + // Нажата кнопка: "escape" + + // Удаление окна + click(); + } else if (event.keyCode === 13) { + // Нажата кнопка: "enter" + + // Инициализация буфера с текущим статусом блокировки закрытия окна + const freeze = this.freeze; + + // Блокировка закрытия окна (чтобы не вызвался click() через событие onclick) + this.freeze = true; + + // Активация виртуальной кнопки "подтвердить" + confirm.click(); + + // Возвращение статуса блокировки закрытия окна + this.freeze = freeze; + } + }; + + // Инициализация быстрых действий по кнопкам + document.addEventListener("keydown", this.buttons); + + // Добавление функции удаления всплывающего окна по событиям + popup.addEventListener("mouseenter", disable); + popup.addEventListener("mouseleave", enable); + + // Фокусировка + description.focus(); + } + } + }); + } + }, 300); + + /** + * Подтвердить + * + * @param {HTMLElement} button Кнопка + * @param {HTMLElement} row Строка + * + * @return {void} + */ + static confirm = damper(async (button, row) => { + if (row instanceof HTMLElement && button instanceof HTMLElement) { + // Получена кнопка и строка + + // Инициализация идентификатора строки + const id = row.getAttribute("id"); + + if (typeof id === "string") { + // Инициализирован идентификатор + + // Запрос к серверу + return await fetch(`/task/${id}/confirm`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }) + .then((response) => response.json()) + .then((data) => { + if (this.errors(data.errors)) { + // Сгенерированы ошибки + } else { + // Не сгенерированы ошибки (подразумевается их отсутствие) + + // Инициализация буфера списка классов + // const buffer = [...row.classList]; + + // Инициализация статуса активной строки + const selected = row.getAttribute("data-selected"); + + // Реинициализация строки + row.outerHTML = data.row; + + // Реинициализация строки (выражение странное, но правильное) + row = document.getElementById(row.getAttribute("id")); + + // Копирование классов из буфера классов удалённой строки + // row.classList.add(...buffer); + + // Копирование статуса активной строки + if ( + typeof selected === "string" && + selected === "true" && + document.body.contains(document.getElementById("popup")) && + !document.body.contains( + document.querySelector('[data-selected="true"]') + ) + ) { + row.setAttribute("data-selected", "true"); + } + + if (data.confirmed) { + // Подтверждена заявка + + // Реинициализация текста + button.innerText = "Отклонить"; + + // Реинициализация стиля + button.classList.remove("grass"); + button.classList.add("clay"); + + // УЛОВКА: браузер ещё не успевает перерисовать документ во время исполнения this.task(row) + row.classList.add("confirmed"); + } else { + // Не подтверждена заявка + + // Реинициализация текста + button.innerText = "Подтвердить"; + + // Реинициализация стиля + button.classList.remove("clay"); + button.classList.add("grass"); + + // УЛОВКА: браузер ещё не успевает перерисовать документ во время исполнения this.task(row) + row.classList.remove("confirmed"); + } + + if (this.body.wrap instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.buttons); + + // Сброс блокировки + this.freeze = false; + + // Реинициализация активного окна + tasks.popup(row); + } + } + }); + } + } + }, 300); + + /** + * Сгенерировать всплывающее окно c заявлением о проблеме + * + * @param {HTMLElement} row Строка + * + * @return {void} + */ + static problem = damper((row) => { + if (row instanceof HTMLElement) { + // Получена строка + + if (row.classList.contains("problematic")) { + // Проблемная заявка + + // Снятие статуса проблемной заявки + this.__problem(null, null, row); + } else { + // Не проблемная заявка + + // Инициализация идентификатора сотрудника + const task = row.getAttribute("id"); + + // Инициализация буфера выбранных строк + const buffer = [ + ...document.querySelectorAll('div[data-selected="true"]'), + ]; + + // Удаление статуса активной строки у остальных строк + for (const element of buffer) { + element.removeAttribute("data-selected"); + } + + // Инициализация статуса активной строки (обрабатываемой в данный момент) + row.setAttribute("data-selected", "true"); + + // Инициализация оболочки всплывающего окна + this.body.wrap = document.createElement("div"); + this.body.wrap.setAttribute("id", "popup"); + + // Инициализация всплывающего окна + const popup = document.createElement("section"); + popup.classList.add("list", "small"); + + // Инициализация заголовка всплывающего окна + const title = document.createElement("h3"); + title.classList.add("unselectable"); + title.innerText = "Заявление о проблеме"; + + // Инициализация оболочки с основной информацией + const main = document.createElement("section"); + main.classList.add("main"); + + // Инициализация колонки + const column = document.createElement("div"); + column.classList.add("column"); + + // Инициализация поля ввода отзыва + const problem = document.createElement("textarea"); + problem.classList.add("snow"); + problem.setAttribute("autofocus", "true"); + problem.setAttribute("maxlength", "300"); + problem.setAttribute("title", "Проблема"); + problem.setAttribute("placeholder", "Описание проблемы"); + + // Инициализация оболочки для кнопок + const buttons = document.createElement("div"); + buttons.classList.add("row", "buttons"); + + // Инициализация кнопки подтверждения + const send = document.createElement("button"); + send.classList.add("grass", "stretched"); + send.innerText = "Отправить"; + send.setAttribute( + "onclick", + `chat.send(this, this.parentElement.previousElementSibling, document.getElementById('${task}'), 'market', 'problem', true); document.removeEventListener("keydown", tasks.buttons);` + ); + + // Инициализация окна с ошибками + this.body.errors = document.createElement("section"); + this.body.errors.classList.add( + "errors", + "window", + "list", + "small", + "hidden" + ); + this.body.errors.setAttribute("data-errors", true); + + // Инициализация элемента-тела (оболочки) окна с ошибками + const errors = document.createElement("section"); + errors.classList.add("body"); + + // Инициализация элемента-списка ошибок + const dl = document.createElement("dl"); + + // Инициализация активного всплывающего окна + const old = document.getElementById("popup"); + + if (old instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.buttons); + + // Сброс блокировки + this.freeze = false; + + // Удаление активного окна + old.remove(); + } + + // Удаление блокировки выполнения закрытия всплывающего окна + this.freeze = false; + + // Запись в документ + popup.appendChild(title); + + column.appendChild(problem); + buttons.appendChild(send); + column.appendChild(buttons); + + main.appendChild(column); + popup.appendChild(main); + this.body.wrap.appendChild(popup); + + errors.appendChild(dl); + this.body.errors.appendChild(errors); + this.body.wrap.appendChild(this.body.errors); + + document.body.appendChild(this.body.wrap); + + // Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup) + function top(errors) { + errors.style.setProperty("transition", "0s"); + errors.style.setProperty( + "--top", + popup.offsetTop + popup.offsetHeight + 12 + "px" + ); + setTimeout(() => errors.style.removeProperty("transition"), 100); + } + top(this.body.errors); + const resize = new ResizeObserver(() => top(this.body.errors)); + resize.observe(this.body.wrap); + + // Инициализация функции закрытия всплывающего окна + const click = () => { + // Блокировка + if (this.freeze) return; + + // Удаление всплывающего окна + this.body.wrap.remove(); + + // Инициализация буфера выбранных строк + const buffer = [ + ...document.querySelectorAll('div[data-selected="true"]'), + ]; + + // Удаление статуса активной строки у остальных строк + for (const element of buffer) { + element.removeAttribute("data-selected"); + } + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.buttons); + + // Сброс блокировки + this.freeze = false; + }; + + // Инициализация функции добавления функции закрытия всплывающего окна + const enable = () => this.body.wrap.addEventListener("click", click); + + // Инициализация функции удаления функции закрытия всплывающего окна + const disable = () => + this.body.wrap.removeEventListener("click", click); + + // Первичная активация функции удаления всплывающего окна + enable(); + + // Инициализация блокировки удаления окна при взаимодействии с select-элементом + for (const select of popup.getElementsByTagName("select")) { + // Перебор всех select-элементов + + // Инициализация функции блокировки удаления окна по событию + select.addEventListener("click", () => { + // Блокировка удаления окна + this.freeze = true; + + // Разблокировка удаления окна + setTimeout(() => (this.freeze = false), 100); + }); + + for (const option of select.getElementsByTagName("option")) { + // Перебор всех option-элементов + + // Инициализация функции блокировки удаления окна по событию + option.addEventListener("click", () => { + // Блокировка удаления окна + this.freeze = true; + + // Разблокировка удаления окна + setTimeout(() => (this.freeze = false), 100); + }); + } + } + + // Инициализация блокировки удаления окна при взаимодействии с textarea-элементом + for (const textarea of popup.getElementsByTagName("textarea")) { + // Перебор всех textarea-элементов + + // Обновлять позицию окна с ошибками при изменении размера textarea (там position: absolute) + new MutationObserver(() => top(this.body.errors)).observe( + textarea, + { + attributes: true, + attributeFilter: ["style"], + } + ); + + // Инициализация функции игнорирования блокировки для выбранных кнопок + textarea.addEventListener("keydown", (e) => { + if (e.keyCode === 27) { + // Нажата кнопка: "escape" + + // Вызов глобальной функции управления кнопками + this.buttons(e, true); + } else if (e.ctrlKey && e.keyCode === 13) { + // Нажаты кнопки: "control", "enter" + + // Вызов глобальной функции управления кнопками + this.buttons(e, true); + } + }); + + // Добавление функции блокировки удаления окна и клавиш по событиям + textarea.addEventListener("focus", () => (this.freeze = true)); + textarea.addEventListener("focusout", () => (this.freeze = false)); + } + + // Инициализация функции управления кнопками + this.buttons = (e, force = false) => { + // Блокировка + if (!force && this.freeze) return; + + if (e.keyCode === 27) { + // Нажата кнопка: "escape" + + // Удаление окна + click(); + } else if (event.keyCode === 13) { + // Нажата кнопка: "enter" + + // Инициализация буфера с текущим статусом блокировки закрытия окна + const freeze = this.freeze; + + // Блокировка закрытия окна (чтобы не вызвался click() через событие onclick) + this.freeze = true; + + // Активация виртуальной кнопки "отправить" + send.click(); + + // Возвращение статуса блокировки закрытия окна + this.freeze = freeze; + } + }; + + // Инициализация быстрых действий по кнопкам + document.addEventListener("keydown", this.buttons); + + // Добавление функции удаления всплывающего окна по событиям + popup.addEventListener("mouseenter", disable); + popup.addEventListener("mouseleave", enable); + + // Фокусировка + problem.focus(); + } + } + }, 300); + + /** + * Проблемная заявка (вызов демпфера) + * + * Изменить статус заявки на проблемную и не проблемную + * Если изменяется статус на проблемную, то необходимо передать текст сообщения + * + * @param {HTMLElement} button Кнопка + * @param {HTMLElement} textarea Поле для ввода описания проблемы + * @param {HTMLElement} row Строка + * + * @return {void} + */ + static _problem(button, textarea, row) { + // Блокировка полей ввода + textarea.setAttribute("readonly", true); + + // Блокировка кнопки + button.setAttribute("disabled", true); + + // Деинициализация индикатора и анимации об ошибке + textarea.classList.remove("error"); + + // Сброс анимации + this.body.errors.classList.add("hidden"); + + // Запуск выполнения + this.__problem(button, textarea, row); + } + + /** + * Проблемная заявка (демпфер) + * + * Изменить статус заявки на проблемную и не проблемную + * Если изменяется статус на проблемную, то необходимо передать текст сообщения + * + * @param {HTMLElement} button Кнопка + * @param {HTMLElement} textarea Поле для ввода описания проблемы + * @param {HTMLElement} row Строка + * + * @return {void} + */ + static __problem = damper(async (button, textarea, row) => { + if (row instanceof HTMLElement) { + // Получена строка + + // Инициализация идентификатора строки + const id = row.getAttribute("id"); + + if (typeof id === "string") { + // Инициализирован идентификатор + + // Инициализация функции разблокировки + function unblock() { + // Разблокировка поля ввода + textarea.removeAttribute("readonly"); + + // Разблокировка кнопки + button.removeAttribute("disabled"); + } + + // Запуск отсрочки разблокировки на случай, если сервер не отвечает + const timeout = setTimeout(() => { + this.errors(["Сервер не отвечает"]); + unblock(); + }, 5000); + + // Запрос к серверу + return await fetch(`/task/${id}/problem`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: + textarea instanceof HTMLElement && textarea.value.length > 0 + ? `text=${textarea.value}` + : "", + }) + .then((response) => response.json()) + .then((data) => { + // Удаление отсрочки разблокировки + clearTimeout(timeout); + + if (this.errors(data.errors)) { + // Сгенерированы ошибки + + // Инициализация отображения ошибки + textarea.classList.add("error"); + + // Фокусировка на поле ввода + textarea.focus(); + + // Разблокировка полей ввода и кнопок + unblock(); + } else { + // Не сгенерированы ошибки (подразумевается их отсутствие) + + // Инициализация буфера списка классов + // const buffer = [...row.classList]; + + // Инициализация статуса активной строки + const selected = row.getAttribute("data-selected"); + + // Реинициализация строки + row.outerHTML = data.row; + + // Реинициализация строки (выражение странное, но правильное) + row = document.getElementById(row.getAttribute("id")); + + // Копирование классов из буфера классов удалённой строки + // row.classList.add(...buffer); + + // Копирование статуса активной строки + if ( + typeof selected === "string" && + selected === "true" && + document.body.contains(document.getElementById("popup")) && + !document.body.contains( + document.querySelector('[data-selected="true"]') + ) + ) { + row.setAttribute("data-selected", "true"); + } + + if (data.problematic) { + // Проблемная заявка + + // УЛОВКА: браузер ещё не успевает перерисовать документ во время исполнения this.task(row) + row.classList.add("problematic"); + } else { + // Не проблемная заявка + + // УЛОВКА: браузер ещё не успевает перерисовать документ во время исполнения this.task(row) + row.classList.remove("problematic"); + } + + if (this.body.wrap instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.buttons); + + // Сброс блокировки + this.freeze = false; + + // Деинициализация активного окна + this.body.wrap.remove(); + + // Удаление статуса активной строки + row.removeAttribute("data-selected"); + } + } + }); + } + } + }, 300); + + /** + * Подтверждение заявки (вызов демпфера) + * + * @param {HTMLElement} button Кнопка + * @param {HTMLElement} rating Рейтинг + * @param {HTMLElement} review Отзыв + * @param {HTMLElement} row Строка + * + * @return {void} + */ + static complete(button, rating, review, row) { + // Блокировка ползунка + rating.setAttribute("disabled", true); + + // Блокировка полей ввода + review.setAttribute("readonly", true); + + // Деинициализация индикатора и анимации об ошибке + rating.classList.remove("error"); + review.classList.remove("error"); + + // Блокировка кнопки + button.setAttribute("disabled", true); + + // Сброс анимации + this.body.errors.classList.add("hidden"); + + // Запуск выполнения + this._complete(button, rating, review, row); + } + + /** + * Подтверждение заявки (демпфер) + * + * @param {HTMLElement} button Кнопка + * @param {HTMLElement} rating Рейтинг + * @param {HTMLElement} review Отзыв + * @param {HTMLElement} row Строка + * + * @return {void} + */ + static _complete = damper(async (button, rating, review, row) => { + if ( + button instanceof HTMLElement && + rating instanceof HTMLElement && + review instanceof HTMLElement && + row instanceof HTMLElement + ) { + // Получена кнопка, рейтинг, отзыв и строка + + // Инициализация идентификатора строки + const id = row.getAttribute("id"); + + if (typeof id === "string") { + // Инициализирован идентификатор + + // Инициализация функции разблокировки + function unblock() { + // Разблокировка ползунка + rating.removeAttribute("disabled"); + + // Разблокировка поля ввода + review.removeAttribute("readonly"); + + // Разблокировка кнопки + button.removeAttribute("disabled"); + } + + // Запуск отсрочки разблокировки на случай, если сервер не отвечает + const timeout = setTimeout(() => { + this.errors(["Сервер не отвечает"]); + unblock(); + }, 5000); + + // Запрос к серверу + return await fetch(`/task/${id}/complete`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `rating=${rating.value}&review=${review.value}`, + }) + .then((response) => response.json()) + .then((data) => { + // Удаление отсрочки разблокировки + clearTimeout(timeout); + + if (this.errors(data.errors)) { + // Сгенерированы ошибки + + // Инициализация отображения ошибки + rating.classList.add("error"); + review.classList.add("error"); + + // Фокусировка на поле ввода + review.focus(); + + if (!data.completed) { + // Не завершена заявка + + // Разблокировка полей ввода и кнопок + unblock(); + } + } else { + // Не сгенерированы ошибки (подразумевается их отсутствие) + + // Инициализация буфера списка классов + // const buffer = [...row.classList]; + + // Инициализация статуса активной строки + const selected = row.getAttribute("data-selected"); + + // Реинициализация строки + row.outerHTML = data.row; + + // Реинициализация строки (выражение странное, но правильное) + row = document.getElementById(row.getAttribute("id")); + + // Копирование классов из буфера классов удалённой строки + // row.classList.add(...buffer); + + // Копирование статуса активной строки + if ( + typeof selected === "string" && + selected === "true" && + document.body.contains(document.getElementById("popup")) && + !document.body.contains( + document.querySelector('[data-selected="true"]') + ) + ) { + row.setAttribute("data-selected", "true"); + } + + if (data.completed) { + // Завершена заявка + + // УЛОВКА: браузер ещё не успевает перерисовать документ во время исполнения this.task(row) + row.classList.add("completed"); + } else { + // Не завершена заявка + + // УЛОВКА: браузер ещё не успевает перерисовать документ во время исполнения this.task(row) + row.classList.remove("completed"); + } + + if (this.body.wrap instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.buttons); + + // Сброс блокировки + this.freeze = false; + + // Деинициализация активного окна + this.body.wrap.remove(); + + // Удаление статуса активной строки + row.removeAttribute("data-selected"); + } + } + }); + } + } + }, 300); + + /** + * Скрыть + * + * @param {HTMLElement} button Кнопка + * @param {HTMLElement} row Строка + * + * @return {void} + */ + static hide = damper(async (button, row) => { + if (row instanceof HTMLElement && button instanceof HTMLElement) { + // Получена кнопка и строка + + // Инициализация идентификатора строки + const id = row.getAttribute("id"); + + if (typeof id === "string") { + // Инициализирован идентификатор + + // Запрос к серверу + return await fetch(`/task/${id}/hide`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }) + .then((response) => response.json()) + .then((data) => { + if (this.errors(data.errors)) { + // Сгенерированы ошибки + } else { + // Не сгенерированы ошибки (подразумевается их отсутствие) + + // Инициализация буфера списка классов + // const buffer = [...row.classList]; + + // Инициализация статуса активной строки + const selected = row.getAttribute("data-selected"); + + // Реинициализация строки + row.outerHTML = data.row; + + // Реинициализация строки (выражение странное, но правильное) + row = document.getElementById(row.getAttribute("id")); + + // Копирование классов из буфера классов удалённой строки + // row.classList.add(...buffer); + + // Копирование статуса активной строки + if ( + typeof selected === "string" && + selected === "true" && + document.body.contains(document.getElementById("popup")) && + !document.body.contains( + document.querySelector('[data-selected="true"]') + ) + ) { + row.setAttribute("data-selected", "true"); + } + + if (data.hided) { + // Скрыта заявка + + // Реинициализация текста + button.innerText = "Показать"; + + // Реинициализация стиля + button.classList.remove("sea"); + button.classList.add("grass"); + + // УЛОВКА: браузер ещё не успевает перерисовать документ во время исполнения this.task(row) + row.classList.add("hided"); + } else { + // Не скрыта заявка + + // Реинициализация текста + button.innerText = "Скрыть"; + + // Реинициализация стиля + button.classList.remove("grass"); + button.classList.add("sea"); + + // УЛОВКА: браузер ещё не успевает перерисовать документ во время исполнения this.task(row) + row.classList.remove("hided"); + } + + if (this.body.wrap instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.buttons); + + // Сброс блокировки + this.freeze = false; + + // Реинициализация активного окна + tasks.popup(row); + } + } + }); + } + } + }, 300); + + /** + * Удалить + * + * @param {HTMLElement} button Кнопка + * @param {HTMLElement} row Строка + * + * @return {void} + */ + static remove = damper(async (button, row) => { + if (row instanceof HTMLElement && button instanceof HTMLElement) { + // Получена кнопка и строка + + // Инициализация идентификатора строки + const id = row.getAttribute("id"); + + alert(228); + + if (typeof id === "string") { + // Инициализирован идентификатор + + // Запрос к серверу + return await fetch(`/task/${id}/remove`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }) + .then((response) => response.json()) + .then((data) => { + if (this.errors(data.errors)) { + // Сгенерированы ошибки + } else { + // Не сгенерированы ошибки (подразумевается их отсутствие) + + if (data.deleted) { + // Удалена заявка + + if (this.body.wrap instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.buttons); + + // Сброс блокировки + this.freeze = false; + + // Деинициализация активного окна + this.body.wrap.remove(); + + // Удаление статуса активной строки + row.removeAttribute("data-selected"); + } + + // Удаление строки + row.remove(); + } + } + }); + } + } + }, 300); + + /** + * Прочитать значение из базы данных + * + * @param {HTMLElement} row Строка + * @param {string} name Название + * + * @return {string} Значение + */ + static async value(row, name) { + if (row instanceof HTMLElement && typeof name === "string") { + // Получена строка и название + + // Инициализация идентификатора строки + const id = row.getAttribute("id"); + + if (typeof id === "string") { + // Инициализирован идентификатор + + // Запрос к серверу + return await fetch(`/task/${id}/value`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `name=${name}`, + }) + .then((response) => response.json()) + .then((data) => { + if (this.errors(data.errors)) { + // Сгенерированы ошибки + } else { + // Не сгенерированы ошибки (подразумевается их отсутствие) + + return data.value; + } + }); + } + } + } + + /** + * Сгенерировать список работ и выбрать в нём ту, что записана в базе данных у заявки + * + * @param {HTMLElement} row Строка + * + * @return {array|null} Массив HTML-элементов + */ + static async works(row) { + if (row instanceof HTMLElement) { + // Получена строка + + // Инициализация идентификатора строки + 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; + } + }); + } + } + } + + /** + * Записать тип работы + * + * @param {HTMLElement} row Строка + * @param {HTMLElement} select HTML-элемент + * + * @return {void} + */ + static work = damper(async (row, select) => { + if (row instanceof HTMLElement && select instanceof HTMLElement) { + // Получена строка и select-элемент + + // Инициализация идентификатора строки + const id = row.getAttribute("id"); + + if (typeof id === "string") { + // Инициализирован идентификатор + + // Запрос к серверу + return await fetch(`/task/${id}/work`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `work=${select.value}`, + }) + .then((response) => response.json()) + .then((data) => { + if (this.errors(data.errors)) { + // Сгенерированы ошибки + } else { + // Не сгенерированы ошибки (подразумевается их отсутствие) + + if (data.writed) { + // Записано новое значение в базу данных + + // Инициализация буфера списка классов + // const buffer = [...row.classList]; + + // Инициализация статуса активной строки + const selected = row.getAttribute("data-selected"); + + // Реинициализация строки в документе + row.outerHTML = data.row; + + // Реинициализация строки (выражение странное, но правильное) + row = document.getElementById(row.getAttribute("id")); + + // Копирование классов из буфера классов удалённой строки + // row.classList.add(...buffer); + + // Копирование статуса активной строки + if ( + typeof selected === "string" && + selected === "true" && + document.body.contains(document.getElementById("popup")) && + !document.body.contains( + document.querySelector('[data-selected="true"]') + ) + ) { + row.setAttribute("data-selected", "true"); + } + } + + if (this.body.wrap instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.buttons); + + // Сброс блокировки + this.freeze = false; + + // Реинициализация активного окна + tasks.popup(row); + } + } + }); + } + } + }, 300); + + /** + * Записать дату + * + * @param {HTMLElement} row Строка + * @param {HTMLElement} date Дата + * @param {HTMLElement} start Время начала + * @param {HTMLElement} end Время окончания + * @param {HTMLElement} button Кнопка отправки + * + * @return {void} + */ + static date(row, date, start, end, button) { + // Блокировка полей ввода + date.setAttribute("readonly", true); + start.setAttribute("readonly", true); + end.setAttribute("readonly", true); + button.setAttribute("disabled", true); + + // Запуск выполнения + this._date(row, date, start, end, button); + } + + /** + * Записать дату + * + * @param {HTMLElement} row Строка + * @param {HTMLElement} date Дата + * @param {HTMLElement} start Время начала + * @param {HTMLElement} end Время окончания + * @param {HTMLElement} button Кнопка отправки + * + * @return {void} + */ + static _date = damper(async (row, date, start, end, button) => { + if ( + row instanceof HTMLElement && + date instanceof HTMLElement && + start instanceof HTMLElement && + end instanceof HTMLElement && + button instanceof HTMLElement + ) { + // Получены строка, поле даты, поле начала заявки, поле окончания заяки и кнопка + + // Инициализация идентификатора строки + const id = row.getAttribute("id"); + + if (typeof id === "string") { + // Инициализирован идентификатор + + // Запрос к серверу + return await fetch(`/task/${id}/date`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `date=${date.valueAsDate / 1000}&start=${start.value}&end=${ + end.value + }`, + }) + .then((response) => response.json()) + .then((data) => { + if (this.errors(data.errors)) { + // Сгенерированы ошибки + + // Разблокировка ввода + date.removeAttribute("readonly"); + start.removeAttribute("readonly"); + end.removeAttribute("readonly"); + button.removeAttribute("disabled"); + } else { + // Не сгенерированы ошибки (подразумевается их отсутствие) + + if (data.writed) { + // Записано новое значение в базу данных + + // Инициализация буфера списка классов + // const buffer = [...row.classList]; + + // Инициализация статуса активной строки + const selected = row.getAttribute("data-selected"); + + // Реинициализация строки в документе + row.outerHTML = data.row; + + // Реинициализация строки (выражение странное, но правильное) + row = document.getElementById(row.getAttribute("id")); + + // Копирование классов из буфера классов удалённой строки + // row.classList.add(...buffer); + + // Копирование статуса активной строки + if ( + typeof selected === "string" && + selected === "true" && + document.body.contains(document.getElementById("popup")) && + !document.body.contains( + document.querySelector('[data-selected="true"]') + ) + ) { + row.setAttribute("data-selected", "true"); + } + + if (this.body.wrap instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.buttons); + + // Сброс блокировки + this.freeze = false; + + // Реинициализация активного окна + tasks.popup(row); + } + } else { + // Не записано новое значение в базу данных + + // Разблокировка ввода + date.removeAttribute("readonly"); + start.removeAttribute("readonly"); + end.removeAttribute("readonly"); + button.removeAttribute("disabled"); + } + } + }); + } + } + }, 300); + + /** + * Записать описание + * + * @param {HTMLElement} row Строка + * @param {HTMLElement} textarea HTML-элемент + * + * @return {void} + */ + static description(row, textarea) { + // Сброс анимации ошибки + textarea.classList.remove("error"); + + // Запуск выполнения + this._description(row, textarea); + } + + /** + * Записать описание (системное) + * + * @param {HTMLElement} row Строка + * @param {HTMLElement} textarea HTML-элемент + * + * @return {void} + */ + static _description = damper(async (row, textarea) => { + if (row instanceof HTMLElement && textarea instanceof HTMLElement) { + // Получена строка и select-элемент + + // Инициализация идентификатора строки + const id = row.getAttribute("id"); + + if (typeof id === "string") { + // Инициализирован идентификатор + + // Запрос к серверу + return await fetch(`/task/${id}/description`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `description=${textarea.value}`, + }) + .then((response) => response.json()) + .then((data) => { + if (this.errors(data.errors)) { + // Сгенерированы ошибки + + // Запись анимации ошибки + textarea.classList.add("error"); + } else { + // Не сгенерированы ошибки (подразумевается их отсутствие) + + if (data.writed) { + // Записано новое значение в базу данных + + // Удаление анимации ошибки + textarea.classList.remove("error"); + } else { + // Не записано новое значение в базу данных + + // Запись анимации ошибки + textarea.classList.add("error"); + } + } + }); + } + } + }, 300); + + /** + * Сгенерировать HTML-элемент со списком ошибок + * + * @param {object} registry Реестр ошибок + * @param {bool} render Отобразить в окне с ошибками? + * @param {bool} clean Очистить окно с ошибками перед добавлением? + * + * @return {bool} Сгенерированы ошибки? + */ + static errors(registry, render = true, clean = true) { + // Инициализация ссылки на HTML-элемент с ошибками + const wrap = document.body.contains(this.body.errors) + ? this.body.errors + : document.querySelector('[data-errors="true"]'); + + if (wrap instanceof HTMLElement && document.body.contains(wrap)) { + // Найден HTML-элемент с ошибками + + // Перерасчёт высоты элемента + function height() { + wrap.classList.remove("hidden"); + wrap.classList.remove("animation"); + // Реинициализация переменной с данными о высоте HTML-элемента (16 - это padding-top + padding-bottom у div#popup > section.errors) + wrap.style.setProperty("--height", wrap.offsetHeight - 16 + "px"); + wrap.classList.add("animation"); + wrap.classList.add("hidden"); + } + + // Инициализация элемента-списка ошибок + const list = wrap.getElementsByTagName("dl")[0]; + + // Удаление ошибок из прошлой генерации + if (clean) list.innerHTML = null; + + for (const error in registry) { + // Генерация HTML-элементов с текстами ошибок + + // Инициализация HTML-элемента текста ошибки + const samp = document.createElement("samp"); + + if (typeof registry[error] === "object") { + // Категория ошибок + + // Проверка наличия ошибок + if (registry[error].length === 0) continue; + + // Инициализация HTML-элемента-оболочки + const wrap = document.createElement("dt"); + + // Запись текста категории + samp.innerText = error; + + // Запись HTML-элементов в список + wrap.appendChild(samp); + list.appendChild(wrap); + + // Реинициализация высоты + height(); + + // Обработка вложенных ошибок (вход в рекурсию) + this.errors(registry[error], false); + } else { + // Текст ошибки (подразумевается) + + // Инициализация HTML-элемента + const wrap = document.createElement("dd"); + + // Запись текста ошибки + samp.innerText = registry[error]; + + // Запись HTML-элемента в список + wrap.appendChild(samp); + list.appendChild(wrap); + + // Реинициализация высоты + height(); + } + } + + if (render) { + // Запрошена отрисовка + + if (list.childElementCount !== 0) { + // Найдены ошибки + + // Сброс анимации + // УЛОВКА: таким образом не запускается анимация до взаимодействия с элементом (исправлял это в CSS, но не помню как) + wrap.classList.add("animation"); + + // Отображение + wrap.classList.remove("hidden"); + } else { + // Не найдены ошибки + + // Скрытие + wrap.classList.add("hidden"); + } + } + + return list.childElementCount === 0 ? false : true; + } + + return false; + } + + /** + * Сотрудник + */ + static worker = class { + /** + * Ссылка на ядро (родительский класс) + */ + static core; + + /** + * Сгенерировать всплывающее окно c данными сотрудника + * + * @param {HTMLElement} row Строка + * + * @return {void} + */ + static async popup(row) { + if ( + row instanceof HTMLElement && + typeof core === "function" && + core.interface !== "worker" + ) { + // Получена строка и аккаунт не является сотрудником + + // Инициализация идентификатора сотрудника + const worker = row.querySelector( + 'span[data-column="worker"]' + ).innerText; + + if (worker.length > 0) { + // Инициализирован идентификатор сотрудника + + // Инициализация идентификатора строки + const task = row.getAttribute("id"); + + if (task.length > 0) { + // Инициализирован идентификатор строки + + // Запрос к серверу + return await fetch(`/worker/${worker}/read`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }) + .then((response) => response.json()) + .then((data) => { + if (this.core.errors(data.errors)) { + // Сгенерированы ошибки + } else { + // Не сгенерированы ошибки (подразумевается их отсутствие) + + if (data.worker.length > 0) { + // Получены данные (подразумевается, что сотрудник найден) + + // Инициализация буфера выбранных строк + const buffer = [ + ...document.querySelectorAll( + 'div[data-selected="true"]' + ), + ]; + + // Удаление статуса активной строки у остальных строк + for (const element of buffer) { + element.removeAttribute("data-selected"); + } + + // Инициализация статуса активной строки (обрабатываемой в данный момент) + row.setAttribute("data-selected", "true"); + + // Инициализация оболочки всплывающего окна + this.core.body.wrap = document.createElement("div"); + this.core.body.wrap.setAttribute("id", "popup"); + + // Инициализация всплывающего окна + const popup = document.createElement("section"); + popup.classList.add("window", "list", "small"); + + // Инициализация заголовка всплывающего окна + const title = document.createElement("h3"); + title.innerText = worker; + + // Инициализация оболочки с основной информацией + const main = document.createElement("section"); + main.classList.add("main"); + + // Инициализация колонки + const column = document.createElement("div"); + column.classList.add("column"); + + // Инициализация буфера для записи во всплывающее окно + const list = document.createElement("div"); + + // Инициализация оболочки для кнопок + const buttons = document.createElement("div"); + buttons.classList.add("row", "divided", "buttons"); + + // Инициализация оболочки для строки с полем ввода + const label = document.createElement("label"); + + // Инициализация иконки для поля ввода замены + const icon = document.createElement("i"); + icon.classList.add("icon", "user"); + + // Инициализация поля ввода замены + const input = document.createElement("input"); + input.classList.add("cloud"); + input.setAttribute("placeholder", "Сотрудник"); + input.setAttribute("autocomplete", "username"); + input.setAttribute("autofocus", "true"); + input.setAttribute("title", "Идентификатор сотрудника"); + input.setAttribute("list", "popup_worker_datalist"); + + // Инициализация кнопки смены + const replace = document.createElement("button"); + replace.classList.add("sea"); + replace.innerText = "Заменить"; + replace.setAttribute( + "onclick", + `tasks.worker.update(document.getElementById('${task}'), this.previousElementSibling, this)` + ); + + // Инициализация списка сотружников + const datalist = document.createElement("datalist"); + datalist.setAttribute("id", "popup_worker_datalist"); + workers + .list() + .then((html) => (datalist.innerHTML = html)); + + // Инициализация кнопки удаления + const remove = document.createElement("button"); + remove.classList.add("clay"); + remove.innerText = "Удалить"; + remove.setAttribute( + "onclick", + `tasks.worker.remove(document.getElementById('${task}'), this)` + ); + + // Инициализация окна с ошибками + this.core.body.errors = document.createElement("section"); + this.core.body.errors.classList.add( + "errors", + "window", + "list", + "small", + "hidden" + ); + this.core.body.errors.setAttribute("data-errors", true); + + // Инициализация элемента-тела (оболочки) окна с ошибками + const errors = document.createElement("section"); + errors.classList.add("body"); + + // Инициализация элемента-списка ошибок + const dl = document.createElement("dl"); + + // Инициализация активного всплывающего окна + const old = document.getElementById("popup"); + + if (old instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener( + "keydown", + this.core.buttons + ); + + // Сброс блокировки + this.core.freeze = false; + + // Удаление активного окна + old.remove(); + } + + // Запись в документ + popup.appendChild(title); + + label.appendChild(icon); + label.appendChild(input); + label.appendChild(replace); + label.appendChild(remove); + label.appendChild(datalist); + buttons.appendChild(label); + column.appendChild(list); + column.appendChild(buttons); + + main.appendChild(column); + popup.appendChild(main); + this.core.body.wrap.appendChild(popup); + + errors.appendChild(dl); + this.core.body.errors.appendChild(errors); + this.core.body.wrap.appendChild(this.core.body.errors); + + document.body.appendChild(this.core.body.wrap); + + // Запись в документ HTML-данных через буфер + list.outerHTML = data.worker; + + // Инициализация элемента с номером в списке данных сотрудника + const number = document + .getElementById(`${worker}_number`) + .getElementsByTagName("a")[0]; + + // Инициализация маски номера + const mask = IMask.createMask({ + mask: "+{7} (000) 000-00-00", + }); + + // Применение маски номера + mask.resolve(number.innerText); + + // Запись нового значения + number.innerText = mask.value; + + // Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup) + function top(errors) { + errors.style.setProperty("transition", "0s"); + errors.style.setProperty( + "--top", + popup.offsetTop + popup.offsetHeight + 12 + "px" + ); + setTimeout( + () => errors.style.removeProperty("transition"), + 100 + ); + } + top(this.core.body.errors); + const resize = new ResizeObserver(() => + top(this.core.body.errors) + ); + resize.observe(this.core.body.wrap); + + // Инициалиация маски идентификатора сотрудника в поле ввода идентификатора сотрудника + IMask(input, { mask: "000000000" }); + + // Инициализация функции закрытия всплывающего окна + const click = () => { + // Блокировка + if (this.core.freeze) return; + + // Удаление всплывающего окна + this.core.body.wrap.remove(); + + // Инициализация буфера выбранных строк + const buffer = [ + ...document.querySelectorAll( + 'div[data-selected="true"]' + ), + ]; + + // Удаление статуса активной строки у остальных строк + for (const element of buffer) { + element.removeAttribute("data-selected"); + } + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener( + "keydown", + this.core.buttons + ); + + // Сброс блокировки + this.core.freeze = false; + }; + + // Инициализация функции добавления функции закрытия всплывающего окна + const enable = () => + this.core.body.wrap.addEventListener("click", click); + + // Инициализация функции удаления функции закрытия всплывающего окна + const disable = () => + this.core.body.wrap.removeEventListener("click", click); + + // Первичная активация функции удаления всплывающего окна + enable(); + + // Инициализация блокировки удаления окна при взаимодействии с select-элементом + for (const select of popup.getElementsByTagName( + "select" + )) { + // Перебор всех select-элементов + + // Инициализация функции блокировки удаления окна по событию + select.addEventListener("click", () => { + // Блокировка удаления окна + this.core.freeze = true; + + // Разблокировка удаления окна + setTimeout(() => (this.core.freeze = false), 100); + }); + + for (const option of select.getElementsByTagName( + "option" + )) { + // Перебор всех option-элементов + + // Инициализация функции блокировки удаления окна по событию + option.addEventListener("click", () => { + // Блокировка удаления окна + this.core.freeze = true; + + // Разблокировка удаления окна + setTimeout(() => (this.core.freeze = false), 100); + }); + } + } + + // Инициализация блокировки удаления окна при взаимодействии с textarea-элементом + for (const textarea of popup.getElementsByTagName( + "textarea" + )) { + // Перебор всех textarea-элементов + + // Обновлять позицию окна с ошибками при изменении размера textarea (там position: absolute) + new MutationObserver(() => + top(this.core.body.errors) + ).observe(textarea, { + attributes: true, + attributeFilter: ["style"], + }); + + // Инициализация функции игнорирования блокировки для выбранных кнопок + textarea.addEventListener("keydown", (e) => { + if (e.keyCode === 27) { + // Нажата кнопка: "escape" + + // Вызов глобальной функции управления кнопками + this.core.buttons(e, true); + } else if (e.ctrlKey && e.keyCode === 13) { + // Нажаты кнопки: "control", "enter" + + // Вызов глобальной функции управления кнопками + this.core.buttons(e, true); + } + }); + + // Добавление функции блокировки удаления окна и клавиш по событиям + textarea.addEventListener( + "focus", + () => (this.core.freeze = true) + ); + textarea.addEventListener( + "focusout", + () => (this.core.freeze = false) + ); + } + + // Инициализация функции управления кнопками + this.core.buttons = (e, force = false) => { + // Блокировка + if (!force && this.core.freeze) return; + + if (e.keyCode === 27) { + // Нажата кнопка: "escape" + + // Удаление окна + click(); + } else if (event.keyCode === 13) { + // Нажата кнопка: "enter" + + // Инициализация буфера с текущим статусом блокировки закрытия окна + const freeze = this.core.freeze; + + // Блокировка закрытия окна (чтобы не вызвался click() через событие onclick) + this.core.freeze = true; + + // Активация виртуальной кнопки "заменить" + replace.click(); + + // Возвращение статуса блокировки закрытия окна + this.core.freeze = freeze; + } + }; + + // Инициализация быстрых действий по кнопкам + document.addEventListener("keydown", this.core.buttons); + + // Добавление функции удаления всплывающего окна по событиям + popup.addEventListener("mouseenter", disable); + popup.addEventListener("mouseleave", enable); + + // Фокусировка + input.focus(); + } else { + // Не получены данные (подразумевается, что сотрудник не найден) + + // Открытие окна инициализации сотрудника + this.init(row); + } + } + }); + } + } else { + // Не инициализирован идентификатор сотрудника + + // Открытие окна инициализации сотрудника + this.init(row); + } + } + } + + static init(row) { + // Инициализация идентификатора строки + const task = row.getAttribute("id"); + + if (task.length > 0) { + // Инициализирован идентификатор строки + + // Инициализация статуса: "опубликовано" + const published = row.classList.contains("published"); + + // Инициализация статуса: "подтверждено" + const confirmed = row.classList.contains("confirmed"); + + // Инициализация статуса: "завершено" + const completed = row.classList.contains("completed"); + + // Инициализация буфера выбранных строк + const buffer = [ + ...document.querySelectorAll('div[data-selected="true"]'), + ]; + + // Удаление статуса активной строки у остальных строк + for (const element of buffer) { + element.removeAttribute("data-selected"); + } + + // Инициализация статуса активной строки (обрабатываемой в данный момент) + row.setAttribute("data-selected", "true"); + + // Инициализация оболочки всплывающего окна + this.core.body.wrap = document.createElement("div"); + this.core.body.wrap.setAttribute("id", "popup"); + + // Инициализация оболочки с основной информацией + const main = document.createElement("section"); + main.classList.add("main"); + + // Инициализация колонки + const column = document.createElement("div"); + column.classList.add("column"); + + // Инициализация всплывающего окна + const popup = document.createElement("section"); + popup.classList.add("window", "list", "small"); + + // Инициализация оболочки для кнопок + const buttons = document.createElement("div"); + buttons.classList.add("row", "buttons"); + + // Инициализация оболочки для строки с полем ввода + const label = document.createElement("label"); + + // Инициализация иконки для поля ввода замены + const icon = document.createElement("i"); + icon.classList.add("icon", "user"); + + // Инициализация поля ввода замены + const input = document.createElement("input"); + input.classList.add("cloud"); + input.setAttribute("placeholder", "Сотрудник"); + input.setAttribute("autocomplete", "username"); + input.setAttribute("autofocus", "true"); + input.setAttribute("title", "Идентификатор сотрудника"); + input.setAttribute("list", "popup_worker_datalist"); + if (published || confirmed || completed) { + input.setAttribute("readonly", "true"); + } + + // Инициализация кнопки замены + const connect = document.createElement("button"); + connect.classList.add("grass"); + connect.innerText = "Назначить"; + connect.setAttribute( + "onclick", + `tasks.worker.update(document.getElementById('${task}'), this.previousElementSibling, this)` + ); + if (published || confirmed || completed) { + connect.setAttribute("disabled", "true"); + } + + // Инициализация списка сотрудников + const datalist = document.createElement("datalist"); + datalist.setAttribute("id", "popup_worker_datalist"); + workers.list().then((html) => (datalist.innerHTML = html)); + + // Инициализация оболочки для кнопок + const buttons_2 = document.createElement("div"); + buttons_2.classList.add("row", "buttons"); + + // Инициализация оболочки для строки с полем ввода + const label_2 = document.createElement("label"); + + // Инициализация кнопки замены + const publish = document.createElement("button"); + publish.classList.add(published ? "clay" : "sea"); + publish.innerText = published ? "Снять с публикации" : "Опубликовать"; + publish.setAttribute( + "onclick", + published + ? `tasks.worker.unpublish(document.getElementById('${task}'), this)` + : `tasks.worker.publish(document.getElementById('${task}'), this)` + ); + + // Инициализация окна с ошибками + this.core.body.errors = document.createElement("section"); + this.core.body.errors.classList.add( + "errors", + "window", + "list", + "small", + "hidden" + ); + this.core.body.errors.setAttribute("data-errors", true); + + // Инициализация элемента-тела (оболочки) окна с ошибками + const body = document.createElement("section"); + body.classList.add("body"); + + // Инициализация элемента-списка ошибок + const dl = document.createElement("dl"); + + // Инициализация активного всплывающего окна + const old = document.getElementById("popup"); + + if (old instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.core.buttons); + + // Сброс блокировки + this.core.freeze = false; + + // Удаление активного окна + old.remove(); + } + + // Запись в документ + label.appendChild(icon); + label.appendChild(input); + label.appendChild(connect); + label.appendChild(datalist); + buttons.appendChild(label); + column.appendChild(buttons); + + if ( + typeof core === "function" && + core.interface !== "market" && + core.interface !== "worker" + ) { + // Скрытие для магазина и сотрудника + + label_2.appendChild(publish); + buttons_2.appendChild(label_2); + column.appendChild(buttons_2); + } + + main.appendChild(column); + popup.appendChild(main); + this.core.body.wrap.appendChild(popup); + + body.appendChild(dl); + this.core.body.errors.appendChild(body); + this.core.body.wrap.appendChild(this.core.body.errors); + + document.body.appendChild(this.core.body.wrap); + + // Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup) + function top(errors) { + errors.style.setProperty("transition", "0s"); + errors.style.setProperty( + "--top", + popup.offsetTop + popup.offsetHeight + 12 + "px" + ); + setTimeout(() => errors.style.removeProperty("transition"), 100); + } + top(this.core.body.errors); + const resize = new ResizeObserver(() => top(this.core.body.errors)); + resize.observe(this.core.body.wrap); + + // Инициалиация маски идентификатора сотрудника + IMask(input, { mask: "000000000" }); + + // Инициализация функции закрытия всплывающего окна + const click = () => { + // Блокировка + if (this.core.freeze) return; + + // Удаление всплывающего окна + this.core.body.wrap.remove(); + + // Инициализация буфера выбранных строк + const buffer = [ + ...document.querySelectorAll('div[data-selected="true"]'), + ]; + + // Удаление статуса активной строки у остальных строк + for (const element of buffer) { + element.removeAttribute("data-selected"); + } + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.core.buttons); + + // Сброс блокировки + this.core.freeze = false; + }; + + // Инициализация функции добавления функции закрытия всплывающего окна + const enable = () => + this.core.body.wrap.addEventListener("click", click); + + // Инициализация функции удаления функции закрытия всплывающего окна + const disable = () => + this.core.body.wrap.removeEventListener("click", click); + + // Первичная активация функции удаления всплывающего окна + enable(); + + // Инициализация блокировки удаления окна при взаимодействии с select-элементом + for (const select of popup.getElementsByTagName("select")) { + // Перебор всех select-элементов + + // Инициализация функции блокировки удаления окна по событию + select.addEventListener("click", () => { + // Блокировка удаления окна + this.core.freeze = true; + + // Разблокировка удаления окна + setTimeout(() => (this.core.freeze = false), 100); + }); + + for (const option of select.getElementsByTagName("option")) { + // Перебор всех option-элементов + + // Инициализация функции блокировки удаления окна по событию + option.addEventListener("click", () => { + // Блокировка удаления окна + this.core.freeze = true; + + // Разблокировка удаления окна + setTimeout(() => (this.core.freeze = false), 100); + }); + } + } + + // Инициализация блокировки удаления окна при взаимодействии с textarea-элементом + for (const textarea of popup.getElementsByTagName("textarea")) { + // Перебор всех textarea-элементов + + // Обновлять позицию окна с ошибками при изменении размера textarea (там position: absolute) + new MutationObserver(() => top(this.core.body.errors)).observe( + textarea, + { + attributes: true, + attributeFilter: ["style"], + } + ); + + // Инициализация функции игнорирования блокировки для выбранных кнопок + textarea.addEventListener("keydown", (e) => { + if (e.keyCode === 27) { + // Нажата кнопка: "escape" + + // Вызов глобальной функции управления кнопками + this.core.buttons(e, true); + } else if (e.ctrlKey && e.keyCode === 13) { + // Нажаты кнопки: "control", "enter" + + // Вызов глобальной функции управления кнопками + this.core.buttons(e, true); + } + }); + + // Добавление функции блокировки удаления окна и клавиш по событиям + textarea.addEventListener("focus", () => (this.core.freeze = true)); + textarea.addEventListener( + "focusout", + () => (this.core.freeze = false) + ); + } + + // Инициализация функции управления кнопками + this.core.buttons = (e, force = false) => { + // Блокировка + if (!force && this.core.freeze) return; + + if (e.keyCode === 27) { + // Нажата кнопка: "escape" + + // Удаление окна + click(); + } else if (event.keyCode === 13) { + // Нажата кнопка: "enter" + + // Инициализация буфера с текущим статусом блокировки закрытия окна + const freeze = this.core.freeze; + + // Блокировка закрытия окна (чтобы не вызвался click() через событие onclick) + this.core.freeze = true; + + // Активация виртуальной кнопки "назначить" + connect.click(); + + // Возвращение статуса блокировки закрытия окна + this.core.freeze = freeze; + } + }; + + // Инициализация быстрых действий по кнопкам + document.addEventListener("keydown", this.core.buttons); + + // Добавление функции удаления всплывающего окна по событиям + popup.addEventListener("mouseenter", disable); + popup.addEventListener("mouseleave", enable); + + // Фокусировка + input.focus(); + } + } + + /** + * Обновить + * + * @param {HTMLElement} row Строка + * @param {HTMLElement} input Поле для ввода идентификатора сотрудника + * @param {HTMLElement} button Кнопка + * + * @return {void} + */ + static update(row, input, button) { + // Блокировка поля ввода + input.setAttribute("readonly", true); + button.setAttribute("disabled", true); + + // Деинициализация индикатора и анимации об ошибке + input.classList.remove("error"); + input.previousElementSibling.classList.remove("error"); + + // Сброс анимации + this.core.body.errors.classList.add("hidden"); + + // Запуск выполнения + this._update(row, input, button); + } + + /** + * Обновить (системное) + * + * @param {HTMLElement} row Строка + * @param {HTMLElement} input Поле для ввода идентификатора сотрудника + * @param {HTMLElement} button Кнопка + * + * @return {void} + */ + static _update = damper(async (row, input, button) => { + if ( + row instanceof HTMLElement && + input instanceof HTMLElement && + button instanceof HTMLElement + ) { + // Получена строка, поле для ввода и кнопка + + // Инициализация идентификатора строки + const id = row.getAttribute("id"); + + if (typeof id === "string") { + // Инициализирован идентификатор + + // Инициализация функции разблокировки + function unblock() { + // Разблокировка поля ввода + input.removeAttribute("readonly"); + button.removeAttribute("disabled"); + } + + // Запуск отсрочки разблокировки на случай, если сервер не отвечает + const timeout = setTimeout(() => { + this.core.errors(["Сервер не отвечает"]); + unblock(); + }, 5000); + + // Запрос к серверу + return await fetch(`/task/${id}/worker/update`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `worker=${input.value}`, + }) + .then((response) => response.json()) + .then((data) => { + // Удаление отсрочки разблокировки + clearTimeout(timeout); + + if (this.core.errors(data.errors)) { + // Сгенерированы ошибки + + // Инициализация отображения ошибки + input.classList.add("error"); + input.previousElementSibling.classList.add("error"); + + // Фокусировка на поле ввода + input.focus(); + + // Разблокировка полей ввода и кнопок + unblock(); + } else { + // Не сгенерированы ошибки (подразумевается их отсутствие) + + if (data.updated) { + // Записано обновление + + // Инициализация буфера списка классов + // const buffer = [...row.classList]; + + // Инициализация статуса активной строки + const selected = row.getAttribute("data-selected"); + + // Реинициализация строки + row.outerHTML = data.row; + + // Реинициализация строки (выражение странное, но правильное) + row = document.getElementById(row.getAttribute("id")); + + // Копирование классов из буфера классов удалённой строки + // row.classList.add(...buffer); + + // Копирование статуса активной строки + if ( + typeof selected === "string" && + selected === "true" && + document.body.contains( + document.getElementById("popup") + ) && + !document.body.contains( + document.querySelector('[data-selected="true"]') + ) + ) { + row.setAttribute("data-selected", "true"); + } + } + + if (this.core.body.wrap instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.core.buttons); + + // Сброс блокировки + this.core.freeze = false; + + // Реинициализация активного окна + tasks.worker.popup(row); + } + } + }); + } + } + }, 300); + + /** + * Удалить + * + * @param {HTMLElement} row Строка + * @param {HTMLElement} button Кнопка + * + * @return {void} + */ + static remove(row, button) { + // Запуск выполнения + this._remove(row, button); + } + + /** + * Удалить (системное) + * + * @param {HTMLElement} row Строка + * @param {HTMLElement} button Кнопка + * + * @return {void} + */ + static _remove = damper(async (row, button) => { + if (row instanceof HTMLElement && button instanceof HTMLElement) { + // Получена строка и кнопка + + // Инициализация идентификатора строки + const id = row.getAttribute("id"); + + if (typeof id === "string") { + // Инициализирован идентификатор + + // Запуск отсрочки разблокировки на случай, если сервер не отвечает + const timeout = setTimeout(() => { + this.core.errors(["Сервер не отвечает"]); + unblock(); + }, 5000); + + // Запрос к серверу + return await fetch(`/task/${id}/worker/update`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `worker=delete`, + }) + .then((response) => response.json()) + .then((data) => { + // Удаление отсрочки разблокировки + clearTimeout(timeout); + + if (this.core.errors(data.errors)) { + // Сгенерированы ошибки + } else { + // Не сгенерированы ошибки (подразумевается их отсутствие) + + if (data.updated) { + // Записано обновление + + // Инициализация буфера списка классов + // const buffer = [...row.classList]; + + // Инициализация статуса активной строки + const selected = row.getAttribute("data-selected"); + + // Реинициализация строки + row.outerHTML = data.row; + + // Реинициализация строки (выражение странное, но правильное) + row = document.getElementById(row.getAttribute("id")); + + // Копирование классов из буфера классов удалённой строки + // row.classList.add(...buffer); + + // Копирование статуса активной строки + if ( + typeof selected === "string" && + selected === "true" && + document.body.contains( + document.getElementById("popup") + ) && + !document.body.contains( + document.querySelector('[data-selected="true"]') + ) + ) { + row.setAttribute("data-selected", "true"); + } + } + + if (this.core.body.wrap instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.core.buttons); + + // Сброс блокировки + this.core.freeze = false; + + // Реинициализация активного окна + tasks.worker.popup(row); + } + } + }); + } + } + }, 300); + + /** + * Опубликовать + * + * @param {HTMLElement} row Строка + * @param {HTMLElement} button Кнопка + * + * @return {void} + */ + static publish(row, button) { + // Запуск выполнения + this._publish(row, button); + } + + /** + * Опубликовать (системное) + * + * @param {HTMLElement} row Строка + * @param {HTMLElement} button Кнопка + * + * @return {void} + */ + static _publish = damper(async (row, button) => { + if (row instanceof HTMLElement && button instanceof HTMLElement) { + // Получена строка и кнопка + + // Инициализация идентификатора строки + const id = row.getAttribute("id"); + + if (typeof id === "string") { + // Инициализирован идентификатор + + // Запуск отсрочки разблокировки на случай, если сервер не отвечает + const timeout = setTimeout(() => { + this.core.errors(["Сервер не отвечает"]); + unblock(); + }, 5000); + + // Запрос к серверу + return await fetch(`/task/${id}/publish`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }) + .then((response) => response.json()) + .then((data) => { + // Удаление отсрочки разблокировки + clearTimeout(timeout); + + if (this.core.errors(data.errors)) { + // Сгенерированы ошибки + } else { + // Не сгенерированы ошибки (подразумевается их отсутствие) + + if (data.published) { + // Опубликована заявка + + // Инициализация буфера списка классов + // const buffer = [...row.classList]; + + // Инициализация статуса активной строки + const selected = row.getAttribute("data-selected"); + + // Инициализация идентификатора строки + const id = row.getAttribute("id"); + + // Реинициализация строки + row.outerHTML = data.row; + + // Реинициализация строки (выражение странное, но правильное) + row = document.getElementById(id); + + // Копирование классов из буфера классов удалённой строки и добавление соответствующего + // row.classList.add(...buffer, "published"); + row.classList.add("published"); + + // Копирование статуса активной строки + if ( + typeof selected === "string" && + selected === "true" && + document.body.contains( + document.getElementById("popup") + ) && + !document.body.contains( + document.querySelector('[data-selected="true"]') + ) + ) { + row.setAttribute("data-selected", "true"); + } + + // Запись текста состояния кнопки + button.innerText = "Снять с публикации"; + + // Реинициализация стиля кнопки + button.classList.remove("sea"); + button.classList.add("clay"); + + // Инициализация вызова функции: "снять с публикации" + button.setAttribute( + "onclick", + `tasks.worker.unpublish(document.getElementById('${id}'), this)` + ); + + // Инициализация оболочки элементов для блокировки + const wrap = + button.parentElement.parentElement.previousElementSibling + .children[0]; + + // Блокировка поля для ввода сотрудника + wrap + .getElementsByTagName("input")[0] + .setAttribute("readonly", "true"); + + // Блокировка кнопки назначения сотрудника + wrap + .getElementsByTagName("button")[0] + .setAttribute("disabled", "true"); + } + } + }); + } + } + }, 300); + + /** + * Снять с публикации + * + * @param {HTMLElement} row Строка + * @param {HTMLElement} button Кнопка + * + * @return {void} + */ + static unpublish(row, button) { + // Запуск выполнения + this._unpublish(row, button); + } + + /** + * Снять с публикации (системное) + * + * @param {HTMLElement} row Строка + * @param {HTMLElement} button Кнопка + * + * @return {void} + */ + static _unpublish = damper(async (row, button) => { + if (row instanceof HTMLElement && button instanceof HTMLElement) { + // Получена строка и кнопка + + // Инициализация идентификатора строки + const id = row.getAttribute("id"); + + if (typeof id === "string") { + // Инициализирован идентификатор + + // Запуск отсрочки разблокировки на случай, если сервер не отвечает + const timeout = setTimeout(() => { + this.core.errors(["Сервер не отвечает"]); + unblock(); + }, 5000); + + // Запрос к серверу + return await fetch(`/task/${id}/unpublish`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }) + .then((response) => response.json()) + .then((data) => { + // Удаление отсрочки разблокировки + clearTimeout(timeout); + + if (this.core.errors(data.errors)) { + // Сгенерированы ошибки + } else { + // Не сгенерированы ошибки (подразумевается их отсутствие) + + if (data.unpublished) { + // Снята с публикации заявка + + // Инициализация буфера списка классов + // const buffer = [...row.classList]; + + // Инициализация статуса активной строки + const selected = row.getAttribute("data-selected"); + + // Реинициализация строки + row.outerHTML = data.row; + + // Реинициализация строки (выражение странное, но правильное) + row = document.getElementById(row.getAttribute("id")); + + // Копирование классов из буфера классов удалённой строки и добавление соответствующего + // row.classList.add(...buffer, "published"); + // row.classList.add("published"); + + // Удаление класса неактуального состояния + // row.classList.remove("published"); + + // Копирование статуса активной строки + if ( + typeof selected === "string" && + selected === "true" && + document.body.contains( + document.getElementById("popup") + ) && + !document.body.contains( + document.querySelector('[data-selected="true"]') + ) + ) { + row.setAttribute("data-selected", "true"); + } + + // Запись текста состояния кнопки + button.innerText = "Опубликовать"; + + // Реинициализация стиля кнопки + button.classList.remove("clay"); + button.classList.add("sea"); + + // Инициализация вызова функции: "опубликовать" + button.setAttribute( + "onclick", + `tasks.worker.publish(document.getElementById('${id}'), this)` + ); + + // Инициализация оболочки элементов для разблокировки + const wrap = + button.parentElement.parentElement.previousElementSibling + .children[0]; + + // Разблокировка поля для ввода сотрудника + wrap + .getElementsByTagName("input")[0] + .removeAttribute("readonly"); + + // Разблокировка кнопки назначения сотрудника + wrap + .getElementsByTagName("button")[0] + .removeAttribute("disabled"); + } + } + }); + } + } + }, 300); + }; + + /** + * Магазин + */ + static market = class { + /** + * Ссылка на ядро (родительский класс) + */ + static core; + + /** + * Сгенерировать всплывающее окно c данными магазина + * + * @param {HTMLElement} row Строка + * + * @return {void} + */ + static async popup(row) { + if ( + row instanceof HTMLElement && + typeof core === "function" && + core.interface !== "market" && + core.interface !== "worker" + ) { + // Получена строка и аккаунт не является магазином или сотрудником + + // Инициализация идентификатора магазина + const market = row.querySelector( + 'span[data-column="market"]' + ).innerText; + + if (market.length > 0) { + // Инициализирован идентификатор магазина + + // Инициализация идентификатора строки + const task = row.getAttribute("id"); + + if (task.length > 0) { + // Инициализирован идентификатор строки + + // Запрос к серверу + return await fetch(`/market/${market}/read`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }) + .then((response) => response.json()) + .then((data) => { + if (this.core.errors(data.errors)) { + // Сгенерированы ошибки + } else { + // Не сгенерированы ошибки (подразумевается их отсутствие) + + if (data.market.length > 0) { + // Получены данные (подразумевается, что сотрудник найден) + + // Инициализация буфера выбранных строк + const buffer = [ + ...document.querySelectorAll( + 'div[data-selected="true"]' + ), + ]; + + // Удаление статуса активной строки у остальных строк + for (const element of buffer) { + element.removeAttribute("data-selected"); + } + + // Инициализация статуса активной строки (обрабатываемой в данный момент) + row.setAttribute("data-selected", "true"); + + // Инициализация оболочки всплывающего окна + this.core.body.wrap = document.createElement("div"); + this.core.body.wrap.setAttribute("id", "popup"); + + // Инициализация всплывающего окна + const popup = document.createElement("section"); + popup.classList.add("window", "list", "small"); + + // Инициализация заголовка всплывающего окна + const title = document.createElement("h3"); + title.classList.add("unselectable"); + title.innerText = market; + + // Инициализация оболочки с основной информацией + const main = document.createElement("section"); + main.classList.add("main"); + + // Инициализация колонки + const column = document.createElement("div"); + column.classList.add("column"); + + // Инициализация буфера для записи во всплывающее окно + const list = document.createElement("div"); + + // Инициализация оболочки для кнопок + const buttons = document.createElement("div"); + buttons.classList.add("row", "buttons"); + + // Инициализация оболочки для строки с полем ввода + const label = document.createElement("label"); + + // Инициализация иконки для поля ввода замены + const icon = document.createElement("i"); + icon.classList.add("icon", "shopping", "cart"); + + // Инициализация поля ввода замены + const input = document.createElement("input"); + input.classList.add("cloud"); + input.setAttribute("placeholder", "Магазин"); + input.setAttribute("autocomplete", "username"); + input.setAttribute("autofocus", "true"); + input.setAttribute("title", "Идентификатор магазина"); + input.setAttribute("list", "popup_market_datalist"); + + // Инициализация кнопки смены + const replace = document.createElement("button"); + replace.classList.add("sea"); + replace.innerText = "Заменить"; + replace.setAttribute( + "onclick", + `tasks.market.update(document.getElementById('${task}'), this.previousElementSibling, this)` + ); + + // Инициализация списка сотружников + const datalist = document.createElement("datalist"); + datalist.setAttribute("id", "popup_market_datalist"); + markets + .list() + .then((html) => (datalist.innerHTML = html)); + + // Инициализация кнопки удаления + const remove = document.createElement("button"); + remove.classList.add("clay"); + remove.innerText = "Удалить"; + remove.setAttribute( + "onclick", + `tasks.market.remove(document.getElementById('${task}'), this)` + ); + + // Инициализация окна с ошибками + this.core.body.errors = document.createElement("section"); + this.core.body.errors.classList.add( + "errors", + "window", + "list", + "small", + "hidden" + ); + this.core.body.errors.setAttribute("data-errors", true); + + // Инициализация элемента-тела (оболочки) окна с ошибками + const body = document.createElement("section"); + body.classList.add("body"); + + // Инициализация элемента-списка ошибок + const dl = document.createElement("dl"); + + // Инициализация активного всплывающего окна + const old = document.getElementById("popup"); + + if (old instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener( + "keydown", + this.core.buttons + ); + + // Сброс блокировки + this.core.freeze = false; + + // Удаление активного окна + old.remove(); + } + + // Запись в документ + popup.appendChild(title); + + label.appendChild(icon); + label.appendChild(input); + label.appendChild(replace); + label.appendChild(remove); + label.appendChild(datalist); + buttons.appendChild(label); + column.appendChild(list); + column.appendChild(buttons); + + main.appendChild(column); + popup.appendChild(main); + this.core.body.wrap.appendChild(popup); + + body.appendChild(dl); + this.core.body.errors.appendChild(body); + this.core.body.wrap.appendChild(this.core.body.errors); + + document.body.appendChild(this.core.body.wrap); + + // Запись в документ HTML-данных через буфер + list.outerHTML = data.market; + + // Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup) + function top(errors) { + errors.style.setProperty("transition", "0s"); + errors.style.setProperty( + "--top", + popup.offsetTop + popup.offsetHeight + 12 + "px" + ); + setTimeout( + () => errors.style.removeProperty("transition"), + 100 + ); + } + top(this.core.body.errors); + const resize = new ResizeObserver(() => + top(this.core.body.errors) + ); + resize.observe(this.core.body.wrap); + + // Инициалиация маски идентификатора магазина в поле ввода идентификатора магазина + IMask(input, { mask: "000000000" }); + + // Инициализация функции закрытия всплывающего окна + const click = () => { + // Блокировка + if (this.core.freeze) return; + + // Удаление всплывающего окна + this.core.body.wrap.remove(); + + // Инициализация буфера выбранных строк + const buffer = [ + ...document.querySelectorAll( + 'div[data-selected="true"]' + ), + ]; + + // Удаление статуса активной строки у остальных строк + for (const element of buffer) { + element.removeAttribute("data-selected"); + } + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener( + "keydown", + this.core.buttons + ); + + // Сброс блокировки + this.core.freeze = false; + }; + + // Инициализация функции добавления функции закрытия всплывающего окна + const enable = () => + this.core.body.wrap.addEventListener("click", click); + + // Инициализация функции удаления функции закрытия всплывающего окна + const disable = () => + this.core.body.wrap.removeEventListener("click", click); + + // Первичная активация функции удаления всплывающего окна + enable(); + + // Инициализация блокировки удаления окна при взаимодействии с select-элементом + for (const select of popup.getElementsByTagName( + "select" + )) { + // Перебор всех select-элементов + + // Инициализация функции блокировки удаления окна по событию + select.addEventListener("click", () => { + // Блокировка удаления окна + this.core.freeze = true; + + // Разблокировка удаления окна + setTimeout(() => (this.core.freeze = false), 100); + }); + + for (const option of select.getElementsByTagName( + "option" + )) { + // Перебор всех option-элементов + + // Инициализация функции блокировки удаления окна по событию + option.addEventListener("click", () => { + // Блокировка удаления окна + this.core.freeze = true; + + // Разблокировка удаления окна + setTimeout(() => (this.core.freeze = false), 100); + }); + } + } + + // Инициализация блокировки удаления окна при взаимодействии с textarea-элементом + for (const textarea of popup.getElementsByTagName( + "textarea" + )) { + // Перебор всех textarea-элементов + + // Обновлять позицию окна с ошибками при изменении размера textarea (там position: absolute) + new MutationObserver(() => + top(this.core.body.errors) + ).observe(textarea, { + attributes: true, + attributeFilter: ["style"], + }); + + // Инициализация функции игнорирования блокировки для выбранных кнопок + textarea.addEventListener("keydown", (e) => { + if (e.keyCode === 27) { + // Нажата кнопка: "escape" + + // Вызов глобальной функции управления кнопками + this.core.buttons(e, true); + } else if (e.ctrlKey && e.keyCode === 13) { + // Нажаты кнопки: "control", "enter" + + // Вызов глобальной функции управления кнопками + this.core.buttons(e, true); + } + }); + + // Добавление функции блокировки удаления окна и клавиш по событиям + textarea.addEventListener( + "focus", + () => (this.core.freeze = true) + ); + textarea.addEventListener( + "focusout", + () => (this.core.freeze = false) + ); + } + + // Инициализация функции управления кнопками + this.core.buttons = (e, force = false) => { + // Блокировка + if (!force && this.core.freeze) return; + + if (e.keyCode === 27) { + // Нажата кнопка: "escape" + + // Удаление окна + click(); + } else if (event.keyCode === 13) { + // Нажата кнопка: "enter" + + // Инициализация буфера с текущим статусом блокировки закрытия окна + const freeze = this.core.freeze; + + // Блокировка закрытия окна (чтобы не вызвался click() через событие onclick) + this.core.freeze = true; + + // Активация виртуальной кнопки "заменить" + replace.click(); + + // Возвращение статуса блокировки закрытия окна + this.core.freeze = freeze; + } + }; + + // Инициализация быстрых действий по кнопкам + document.addEventListener("keydown", this.core.buttons); + + // Добавление функции удаления всплывающего окна по событиям + popup.addEventListener("mouseenter", disable); + popup.addEventListener("mouseleave", enable); + + // Фокусировка + input.focus(); + } else { + // Не получены данные (подразумевается, что магазин не найден) + + // Открытие окна инициализации магазина + this.init(row); + } + } + }); + } + } else { + // Не инициализирован идентификатор магазина + + // Открытие окна инициализации магазина + this.init(row); + } + } + } + + static init(row) { + // Инициализация идентификатора строки + const task = row.getAttribute("id"); + + if (task.length > 0) { + // Инициализирован идентификатор строки + + // Инициализация буфера выбранных строк + const buffer = [ + ...document.querySelectorAll('div[data-selected="true"]'), + ]; + + // Удаление статуса активной строки у остальных строк + for (const element of buffer) { + element.removeAttribute("data-selected"); + } + + // Инициализация статуса активной строки (Магазин обрабатываемой в данный момент) + row.setAttribute("data-selected", "true"); + + // Инициализация оболочки всплывающего окна + this.core.body.wrap = document.createElement("div"); + this.core.body.wrap.setAttribute("id", "popup"); + + // Инициализация всплывающего окна + const popup = document.createElement("section"); + popup.classList.add("window", "list", "small"); + + // Инициализация оболочки с основной информацией + const main = document.createElement("section"); + main.classList.add("main"); + + // Инициализация колонки + const column = document.createElement("div"); + column.classList.add("column"); + + // Инициализация оболочки для кнопок + const buttons = document.createElement("div"); + buttons.classList.add("row", "buttons"); + + // Инициализация оболочки для строки с полем ввода + const label = document.createElement("label"); + + // Инициализация иконки для поля ввода замены + const icon = document.createElement("i"); + icon.classList.add("icon", "shopping", "cart"); + + // Инициализация поля ввода замены + const input = document.createElement("input"); + input.classList.add("cloud"); + input.setAttribute("placeholder", "Магазин"); + input.setAttribute("autocomplete", "username"); + input.setAttribute("autofocus", "true"); + input.setAttribute("title", "Идентификатор магазина"); + input.setAttribute("list", "popup_market_datalist"); + + // Инициализация кнопки замены + const connect = document.createElement("button"); + connect.classList.add("grass"); + connect.innerText = "Назначить"; + connect.setAttribute( + "onclick", + `tasks.market.update(document.getElementById('${task}'), this.previousElementSibling, this)` + ); + + // Инициализация списка сотружников + const datalist = document.createElement("datalist"); + datalist.setAttribute("id", "popup_market_datalist"); + markets.list().then((html) => (datalist.innerHTML = html)); + + // Инициализация окна с ошибками + this.core.body.errors = document.createElement("section"); + this.core.body.errors.classList.add( + "errors", + "window", + "list", + "small", + "hidden" + ); + this.core.body.errors.setAttribute("data-errors", true); + + // Инициализация элемента-тела (оболочки) окна с ошибками + const body = document.createElement("section"); + body.classList.add("body"); + + // Инициализация элемента-списка ошибок + const dl = document.createElement("dl"); + + // Инициализация активного всплывающего окна + const old = document.getElementById("popup"); + + if (old instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.core.buttons); + + // Сброс блокировки + this.core.freeze = false; + + // Удаление активного окна + old.remove(); + } + + // Запись в документ + label.appendChild(icon); + label.appendChild(input); + label.appendChild(connect); + label.appendChild(datalist); + buttons.appendChild(label); + column.appendChild(buttons); + + main.appendChild(column); + popup.appendChild(main); + this.core.body.wrap.appendChild(popup); + + body.appendChild(dl); + this.core.body.errors.appendChild(body); + this.core.body.wrap.appendChild(this.core.body.errors); + + document.body.appendChild(this.core.body.wrap); + + // Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup) + function top(errors) { + errors.style.setProperty("transition", "0s"); + errors.style.setProperty( + "--top", + popup.offsetTop + popup.offsetHeight + 12 + "px" + ); + setTimeout(() => errors.style.removeProperty("transition"), 100); + } + top(this.core.body.errors); + const resize = new ResizeObserver(() => top(this.core.body.errors)); + resize.observe(this.core.body.wrap); + + // Инициалиация маски идентификатора магазина + IMask(input, { mask: "000000000" }); + + // Инициализация функции закрытия всплывающего окна + const click = () => { + // Блокировка + if (this.core.freeze) return; + + // Удаление всплывающего окна + this.core.body.wrap.remove(); + + // Инициализация буфера выбранных строк + const buffer = [ + ...document.querySelectorAll('div[data-selected="true"]'), + ]; + + // Удаление статуса активной строки у остальных строк + for (const element of buffer) { + element.removeAttribute("data-selected"); + } + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.core.buttons); + + // Сброс блокировки + this.core.freeze = false; + }; + + // Инициализация функции добавления функции закрытия всплывающего окна + const enable = () => + this.core.body.wrap.addEventListener("click", click); + + // Инициализация функции удаления функции закрытия всплывающего окна + const disable = () => + this.core.body.wrap.removeEventListener("click", click); + + // Первичная активация функции удаления всплывающего окна + enable(); + + // Инициализация блокировки удаления окна при взаимодействии с select-элементом + for (const select of popup.getElementsByTagName("select")) { + // Перебор всех select-элементов + + // Инициализация функции блокировки удаления окна по событию + select.addEventListener("click", () => { + // Блокировка удаления окна + this.core.freeze = true; + + // Разблокировка удаления окна + setTimeout(() => (this.core.freeze = false), 100); + }); + + for (const option of select.getElementsByTagName("option")) { + // Перебор всех option-элементов + + // Инициализация функции блокировки удаления окна по событию + option.addEventListener("click", () => { + // Блокировка удаления окна + this.core.freeze = true; + + // Разблокировка удаления окна + setTimeout(() => (this.core.freeze = false), 100); + }); + } + } + + // Инициализация блокировки удаления окна при взаимодействии с textarea-элементом + for (const textarea of popup.getElementsByTagName("textarea")) { + // Перебор всех textarea-элементов + + // Обновлять позицию окна с ошибками при изменении размера textarea (там position: absolute) + new MutationObserver(() => top(this.core.body.errors)).observe( + textarea, + { + attributes: true, + attributeFilter: ["style"], + } + ); + + // Инициализация функции игнорирования блокировки для выбранных кнопок + textarea.addEventListener("keydown", (e) => { + if (e.keyCode === 27) { + // Нажата кнопка: "escape" + + // Вызов глобальной функции управления кнопками + this.core.buttons(e, true); + } else if (e.ctrlKey && e.keyCode === 13) { + // Нажаты кнопки: "control", "enter" + + // Вызов глобальной функции управления кнопками + this.core.buttons(e, true); + } + }); + + // Добавление функции блокировки удаления окна и клавиш по событиям + textarea.addEventListener("focus", () => (this.core.freeze = true)); + textarea.addEventListener( + "focusout", + () => (this.core.freeze = false) + ); + } + + // Инициализация функции управления кнопками + this.core.buttons = (e, force = false) => { + // Блокировка + if (!force && this.core.freeze) return; + + if (e.keyCode === 27) { + // Нажата кнопка: "escape" + + // Удаление окна + click(); + } else if (event.keyCode === 13) { + // Нажата кнопка: "enter" + + // Инициализация буфера с текущим статусом блокировки закрытия окна + const freeze = this.core.freeze; + + // Блокировка закрытия окна (чтобы не вызвался click() через событие onclick) + this.core.freeze = true; + + // Активация виртуальной кнопки "назначить" + connect.click(); + + // Возвращение статуса блокировки закрытия окна + this.core.freeze = freeze; + } + }; + + // Инициализация быстрых действий по кнопкам + document.addEventListener("keydown", this.core.buttons); + + // Добавление функции удаления всплывающего окна по событиям + popup.addEventListener("mouseenter", disable); + popup.addEventListener("mouseleave", enable); + + // Фокусировка + input.focus(); + } + } + + /** + * Обновить + * + * @param {HTMLElement} row Строка + * @param {HTMLElement} input Поле для ввода идентификатора магазина + * @param {HTMLElement} button Кнопка + * + * @return {void} + */ + static update(row, input, button) { + // Блокировка поля ввода + input.setAttribute("readonly", true); + button.setAttribute("disabled", true); + + // Деинициализация индикатора и анимации об ошибке + input.classList.remove("error"); + input.previousElementSibling.classList.remove("error"); + + // Сброс анимации + this.core.body.errors.classList.add("hidden"); + + // Запуск выполнения + this._update(row, input, button); + } + + /** + * Обновить (системное) + * + * @param {HTMLElement} row Строка + * @param {HTMLElement} input Поле для ввода идентификатора магазина + * @param {HTMLElement} button Кнопка + * + * @return {void} + */ + static _update = damper(async (row, input, button) => { + if ( + row instanceof HTMLElement && + input instanceof HTMLElement && + button instanceof HTMLElement + ) { + // Получена строка, поле для ввода и кнопка + + // Инициализация идентификатора строки + const id = row.getAttribute("id"); + + if (typeof id === "string") { + // Инициализирован идентификатор + + // Инициализация функции разблокировки + function unblock() { + // Разблокировка поля ввода + input.removeAttribute("readonly"); + button.removeAttribute("disabled"); + } + + // Запуск отсрочки разблокировки на случай, если сервер не отвечает + const timeout = setTimeout(() => { + this.core.errors(["Сервер не отвечает"]); + unblock(); + }, 5000); + + // Запрос к серверу + return await fetch(`/task/${id}/market/update`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `market=${input.value}`, + }) + .then((response) => response.json()) + .then((data) => { + // Удаление отсрочки разблокировки + clearTimeout(timeout); + + if (this.core.errors(data.errors)) { + // Сгенерированы ошибки + + // Инициализация отображения ошибки + input.classList.add("error"); + input.previousElementSibling.classList.add("error"); + + // Фокусировка на поле ввода + input.focus(); + + // Разблокировка полей ввода и кнопок + unblock(); + } else { + // Не сгенерированы ошибки (подразумевается их отсутствие) + + if (data.updated) { + // Записано обновление + + // Инициализация буфера списка классов + // const buffer = [...row.classList]; + + // Инициализация статуса активной строки + const selected = row.getAttribute("data-selected"); + + // Реинициализация строки + row.outerHTML = data.row; + + // Реинициализация строки (выражение странное, но правильное) + row = document.getElementById(row.getAttribute("id")); + + // Копирование классов из буфера классов удалённой строки + // row.classList.add(...buffer); + + // Копирование статуса активной строки + if ( + typeof selected === "string" && + selected === "true" && + document.body.contains( + document.getElementById("popup") + ) && + !document.body.contains( + document.querySelector('[data-selected="true"]') + ) + ) { + row.setAttribute("data-selected", "true"); + } + } + + if (this.core.body.wrap instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.core.buttons); + + // Сброс блокировки + this.core.freeze = false; + + // Реинициализация активного окна + tasks.market.popup(row); + } + } + }); + } + } + }, 300); + + /** + * Удалить + * + * @param {HTMLElement} row Строка + * @param {HTMLElement} button Кнопка + * + * @return {void} + */ + static remove(row, button) { + // Запуск выполнения + this._remove(row, button); + } + + /** + * Удалить (системное) + * + * @param {HTMLElement} row Строка + * @param {HTMLElement} button Кнопка + * + * @return {void} + */ + static _remove = damper(async (row, button) => { + if (row instanceof HTMLElement && button instanceof HTMLElement) { + // Получена строка и кнопка + + // Инициализация идентификатора строки + const id = row.getAttribute("id"); + + if (typeof id === "string") { + // Инициализирован идентификатор + + // Запуск отсрочки разблокировки на случай, если сервер не отвечает + const timeout = setTimeout(() => { + this.core.errors(["Сервер не отвечает"]); + unblock(); + }, 5000); + + // Запрос к серверу + return await fetch(`/task/${id}/market/update`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `market=delete`, + }) + .then((response) => response.json()) + .then((data) => { + // Удаление отсрочки разблокировки + clearTimeout(timeout); + + if (this.core.errors(data.errors)) { + // Сгенерированы ошибки + + // Разблокировка полей ввода и кнопок + unblock(); + } else { + // Не сгенерированы ошибки (подразумевается их отсутствие) + + if (data.updated) { + // Записано обновление + + // Инициализация буфера списка классов + // const buffer = [...row.classList]; + + // Инициализация статуса активной строки + const selected = row.getAttribute("data-selected"); + + // Реинициализация строки + row.outerHTML = data.row; + + // Реинициализация строки (выражение странное, но правильное) + row = document.getElementById(row.getAttribute("id")); + + // Копирование классов из буфера классов удалённой строки + // row.classList.add(...buffer); + + // Копирование статуса активной строки + if ( + typeof selected === "string" && + selected === "true" && + document.body.contains( + document.getElementById("popup") + ) && + !document.body.contains( + document.querySelector('[data-selected="true"]') + ) + ) { + row.setAttribute("data-selected", "true"); + } + } + + if (this.core.body.wrap instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.core.buttons); + + // Сброс блокировки + this.core.freeze = false; + + // Реинициализация активного окна + tasks.market.popup(row); + } + } + }); + } + } + }, 300); + }; + + /** + * Комментарий + */ + static commentary = class { + /** + * Ссылка на ядро (родительский класс) + */ + static core; + + /** + * Сгенерировать всплывающее окно c комментарием задачи + * + * @param {HTMLElement} row Строка + * + * @return {void} + */ + static popup = damper((row) => { + if ( + row instanceof HTMLElement && + typeof core === "function" && + core.interface !== "market" && + core.interface !== "worker" + ) { + // Получена строка и аккаунт не является магазином или сотрудником + + // Инициализация идентификатора сотрудника + const task = row.getAttribute("id"); + + // Инициализация буфера выбранных строк + const buffer = [ + ...document.querySelectorAll('div[data-selected="true"]'), + ]; + + // Удаление статуса активной строки у остальных строк + for (const element of buffer) { + element.removeAttribute("data-selected"); + } + + // Инициализация статуса активной строки (обрабатываемой в данный момент) + row.setAttribute("data-selected", "true"); + + // Инициализация оболочки всплывающего окна + this.core.body.wrap = document.createElement("div"); + this.core.body.wrap.setAttribute("id", "popup"); + + // Инициализация всплывающего окна + const popup = document.createElement("section"); + popup.classList.add("list", "small"); + + // Инициализация заголовка всплывающего окна + const title = document.createElement("h3"); + title.innerText = "Комментарий"; + + // Инициализация оболочки с основной информацией + const main = document.createElement("section"); + main.classList.add("main"); + + // Инициализация колонки + const column = document.createElement("div"); + column.classList.add("column"); + + // Инициализация строки + const row_1 = document.createElement("div"); + row_1.classList.add("row", "stretchable"); + + // Инициализация оболочки для поля ввода комментария + const label = document.createElement("label"); + + // Инициализация поля ввода комментария + const textarea = document.createElement("textarea"); + textarea.classList.add("snow"); + this.core + .value(row, "commentary") + .then((text) => (textarea.value = text)); + textarea.setAttribute("autofocus", "true"); + textarea.setAttribute("maxlength", "800"); + textarea.setAttribute( + "onkeyup", + `tasks.commentary.update(document.getElementById('${task}'), this)` + ); + textarea.setAttribute("title", "Дополнительная информация"); + textarea.setAttribute( + "placeholder", + "Дополнительная информация о заявке" + ); + + // Инициализация окна с ошибками + this.core.body.errors = document.createElement("section"); + this.core.body.errors.classList.add( + "errors", + "window", + "list", + "small", + "hidden" + ); + this.core.body.errors.setAttribute("data-errors", true); + + // Инициализация элемента-тела (оболочки) окна с ошибками + const errors = document.createElement("section"); + errors.classList.add("body"); + + // Инициализация элемента-списка ошибок + const dl = document.createElement("dl"); + + // Инициализация активного всплывающего окна + const old = document.getElementById("popup"); + + if (old instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.core.buttons); + + // Сброс блокировки + this.core.freeze = false; + + // Удаление активного окна + old.remove(); + } + + // Удаление блокировки выполнения закрытия всплывающего окна + this.core.freeze = false; + + // Запись в документ + popup.appendChild(title); + + label.appendChild(textarea); + row_1.appendChild(label); + column.appendChild(row_1); + + main.appendChild(column); + popup.appendChild(main); + this.core.body.wrap.appendChild(popup); + + errors.appendChild(dl); + this.core.body.errors.appendChild(errors); + this.core.body.wrap.appendChild(this.core.body.errors); + + document.body.appendChild(this.core.body.wrap); + + // Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup) + function top(errors) { + errors.style.setProperty("transition", "0s"); + errors.style.setProperty( + "--top", + popup.offsetTop + popup.offsetHeight + 12 + "px" + ); + setTimeout(() => errors.style.removeProperty("transition"), 100); + } + top(this.core.body.errors); + const resize = new ResizeObserver(() => top(this.core.body.errors)); + resize.observe(this.core.body.wrap); + + // Инициализация функции закрытия всплывающего окна + const click = () => { + // Блокировка + if (this.core.freeze) return; + + // Удаление всплывающего окна + this.core.body.wrap.remove(); + + // Инициализация буфера выбранных строк + const buffer = [ + ...document.querySelectorAll('div[data-selected="true"]'), + ]; + + // Удаление статуса активной строки у остальных строк + for (const element of buffer) { + element.removeAttribute("data-selected"); + } + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.core.buttons); + + // Сброс блокировки + this.core.freeze = false; + }; + + // Инициализация функции добавления функции закрытия всплывающего окна + const enable = () => + this.core.body.wrap.addEventListener("click", click); + + // Инициализация функции удаления функции закрытия всплывающего окна + const disable = () => + this.core.body.wrap.removeEventListener("click", click); + + // Первичная активация функции удаления всплывающего окна + enable(); + + // Инициализация блокировки удаления окна при взаимодействии с select-элементом + for (const select of popup.getElementsByTagName("select")) { + // Перебор всех select-элементов + + // Инициализация функции блокировки удаления окна по событию + select.addEventListener("click", () => { + // Блокировка удаления окна + this.core.freeze = true; + + // Разблокировка удаления окна + setTimeout(() => (this.core.freeze = false), 100); + }); + + for (const option of select.getElementsByTagName("option")) { + // Перебор всех option-элементов + + // Инициализация функции блокировки удаления окна по событию + option.addEventListener("click", () => { + // Блокировка удаления окна + this.core.freeze = true; + + // Разблокировка удаления окна + setTimeout(() => (this.core.freeze = false), 100); + }); + } + } + + // Инициализация блокировки удаления окна при взаимодействии с textarea-элементом + for (const textarea of popup.getElementsByTagName("textarea")) { + // Перебор всех textarea-элементов + + // Обновлять позицию окна с ошибками при изменении размера textarea (там position: absolute) + new MutationObserver(() => top(this.core.body.errors)).observe( + textarea, + { + attributes: true, + attributeFilter: ["style"], + } + ); + + // Инициализация функции игнорирования блокировки для выбранных кнопок + textarea.addEventListener("keydown", (e) => { + if (e.keyCode === 27) { + // Нажата кнопка: "escape" + + // Вызов глобальной функции управления кнопками + this.core.buttons(e, true); + } else if (e.ctrlKey && e.keyCode === 13) { + // Нажаты кнопки: "control", "enter" + + // Вызов глобальной функции управления кнопками + this.core.buttons(e, true); + } + }); + + // Добавление функции блокировки удаления окна и клавиш по событиям + textarea.addEventListener("focus", () => (this.core.freeze = true)); + textarea.addEventListener( + "focusout", + () => (this.core.freeze = false) + ); + } + + // Инициализация функции управления кнопками + this.core.buttons = (e, force = false) => { + // Блокировка + if (!force && this.core.freeze) return; + + if (e.keyCode === 27) { + // Нажата кнопка: "escape" + + // Удаление окна + click(); + } + }; + + // Инициализация быстрых действий по кнопкам + document.addEventListener("keydown", this.core.buttons); + + // Добавление функции удаления всплывающего окна по событиям + popup.addEventListener("mouseenter", disable); + popup.addEventListener("mouseleave", enable); + + // Фокусировка + textarea.focus(); + } + }, 300); + + /** + * Записать обновление + * + * @param {HTMLElement} row Строка + * @param {HTMLElement} textarea HTML-элемент + * + * @return {void} + */ + static update(row, textarea) { + // Сброс анимации ошибки + textarea.classList.remove("error"); + + // Запуск выполнения + this._update(row, textarea); + } + + /** + * Записать обновление (системное) + * + * @param {HTMLElement} row Строка + * @param {HTMLElement} textarea HTML-элемент + * + * @return {void} + */ + static _update = damper(async (row, textarea) => { + if (row instanceof HTMLElement && textarea instanceof HTMLElement) { + // Получена строка и select-элемент + + // Инициализация идентификатора строки + const id = row.getAttribute("id"); + + if (typeof id === "string") { + // Инициализирован идентификатор + + // Запрос к серверу + return await fetch(`/task/${id}/commentary`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `commentary=${textarea.value}`, + }) + .then((response) => response.json()) + .then((data) => { + if (this.core.errors(data.errors)) { + // Сгенерированы ошибки + + // Запись анимации ошибки + textarea.classList.add("error"); + } else { + // Не сгенерированы ошибки (подразумевается их отсутствие) + + if (data.writed) { + // Записано новое значение в базу данных + + // Удаление анимации ошибки + textarea.classList.remove("error"); + + // Инициализация буфера списка классов + // const buffer = [...row.classList]; + + // Инициализация статуса активной строки + const selected = row.getAttribute("data-selected"); + + // Реинициализация строки + row.outerHTML = data.row; + + // Реинициализация строки (выражение странное, но правильное) + row = document.getElementById(row.getAttribute("id")); + + // Копирование классов из буфера классов удалённой строки + // row.classList.add(...buffer); + + // Копирование статуса активной строки + if ( + typeof selected === "string" && + selected === "true" && + document.body.contains( + document.getElementById("popup") + ) && + !document.body.contains( + document.querySelector('[data-selected="true"]') + ) + ) { + row.setAttribute("data-selected", "true"); + } + } else { + // Не записано новое значение в базу данных + + // Запись анимации ошибки + textarea.classList.add("error"); + } + } + }); + } + } + }, 300); + }; + + static reinitializer = class { + /** + * Ссылка на ядро (родительский класс) + */ + static core; + + /** + * Запустить + * + * @param {HTMLElement|null} target Строка + */ + static start(target) { + // Инициализация оболочки + const tasks = document.getElementById("tasks"); + + for (let row of typeof target === "undefined" + ? tasks.querySelectorAll(':scope > div[data-row="task"]') + : [target]) { + // Перебор строк + + // Инициализация номера итерации по умолчанию + if (typeof row.counter === "undefined") { + // Не инициализирован счётчик итерации + + row.counter = Math.floor(Math.random() * 280 + 20); + row.setAttribute("data-counter", row.counter); + } + + // Инициализация самообновления + if (typeof row.updater === "undefined") { + // Не инициализирована функция самообновления + + row.updater = setInterval(() => { + if (--row.counter <= 0) { + // Отсчёт таймера завершён + + // Блокировка строки + + // Деинициализация реинициализатора строки (для защиты от повторного запроса) + this.stop(row); + + // Запись анимации (запуск) + row.classList.add("reinitialized"); + + // Запрос к серверу + fetch("/tasks/read", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `row=${row.getAttribute("id")}`, + }) + .then((response) => response.json()) + .then((data) => { + if (this.core.errors(data.errors, false, false)) { + // Сгенерированы ошибки + } else { + // Не сгенерированы ошибки (подразумевается их отсутствие) + + // Инициализация идентифиатора + const id = row.getAttribute("id"); + + // Инициализация количества непрочитанных сообщений + const messages = row.lastElementChild.innerText; + + // Инициализация статуса активной строки + const selected = row.getAttribute("data-selected"); + + // Реинициализация строки + row.outerHTML = data.rows; + + // Реинициализация перезаписанной строки + row = document.getElementById(id); + + // Копирование статуса активной строки + if ( + typeof selected === "string" && + selected === "true" && + document.body.contains(document.getElementById("popup")) + ) { + row.setAttribute("data-selected", "true"); + } + + // Если непрочитанных сообщений стало больше, то проиграть звук уведомления + if (row.lastElementChild.innerText > messages) { + window.notification.play(); + } + + // Инициализация реинициализатора строки (для пересчёта таймера для setInterval) + if (row instanceof HTMLElement) { + setTimeout(() => this.start(row), 5000); + } + } + }); + } else row.setAttribute("data-counter", row.counter); + }, Math.floor(Math.random() * tasks.querySelectorAll(':scope > div[data-row="task"]').length * 200 + 300)); + } + } + } + + /** + * Остановить + * + * @param {HTMLElement|null} target Строка + */ + static stop(target) { + // Инициализация оболочки + const tasks = document.getElementById("tasks"); + + for (const row of typeof target === "undefined" + ? tasks.querySelectorAll(':scope > div[data-row="task"]') + : [target]) { + // Перебор строк + + // Деинициализация самообновления + clearInterval(row.updater); + + // Удаление идентификатора + row.updater = row.counter = undefined; + + // Удаление анимации (сброс) + row.classList.remove("reinitialized"); + } + } + }; + }; + + // Инициализация ссылок на ядро + window.tasks.worker.core = + window.tasks.market.core = + window.tasks.commentary.core = + window.tasks.reinitializer.core = + window.tasks; } // Вызов события: "инициализировано" document.dispatchEvent( - new CustomEvent("tasks.initialized", { - detail: { tasks: window.tasks }, - }), + new CustomEvent("tasks.initialized", { + detail: { tasks: window.tasks }, + }) );