This repository has been archived on 2024-10-16. You can view files and clone it, but cannot push or open issues or pull requests.
ebala/mirzaev/ebala/system/public/js/tasks.js

5677 lines
239 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 Количество кассиров <input>
* @param {HTMLElement} displayers Количество выкладчиков <input>
* @param {HTMLElement} loaders Количество грузчиков <input>
* @param {HTMLElement} gastronomes Количество гастрономов <input>
* @param {HTMLElement} start Начало работы (00:00) <input>
* @param {HTMLElement} end Конец работы (00:00) <input>
* @param {HTMLElement} date Дата работы (d.m.Y) <input>
* @param {HTMLElement} button Кнопка заявки <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 Количество кассиров <input>
* @param {HTMLElement} displayers Количество выкладчиков <input>
* @param {HTMLElement} loaders Количество грузчиков <input>
* @param {HTMLElement} gastronomes Количество гастрономов <input>
* @param {HTMLElement} start Начало работы (00:00) <input>
* @param {HTMLElement} end Конец работы (00:00) <input>
* @param {HTMLElement} date Дата работы (d.m.Y) <input>
* @param {HTMLElement} button Кнопка заявки <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(
/<!--(?!>)[\S\s]*?-->/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 Поле для ввода описания проблемы <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 Поле для ввода описания проблемы <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 Кнопка <button>
* @param {HTMLElement} rating Рейтинг <input>
* @param {HTMLElement} review Отзыв <input>
* @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 Кнопка <button>
* @param {HTMLElement} rating Рейтинг <input>
* @param {HTMLElement} review Отзыв <input>
* @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-элементов <option>
*/
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-элемент <select>
*
* @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-элемент <textarea>
*
* @return {void}
*/
static description(row, textarea) {
// Сброс анимации ошибки
textarea.classList.remove("error");
// Запуск выполнения
this._description(row, textarea);
}
/**
* Записать описание (системное)
*
* @param {HTMLElement} row Строка
* @param {HTMLElement} textarea HTML-элемент <textarea>
*
* @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-элемент <textarea>
*
* @return {void}
*/
static update(row, textarea) {
// Сброс анимации ошибки
textarea.classList.remove("error");
// Запуск выполнения
this._update(row, textarea);
}
/**
* Записать обновление (системное)
*
* @param {HTMLElement} row Строка
* @param {HTMLElement} textarea HTML-элемент <textarea>
*
* @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 },
}),
);