// распознает строки вида t_2 const singleNumRegexp = /\w_(\d+)/; // распознает строки вида row_3_1 const doubleNumReg = /.*_(\d+)_(\d+)$/; class Base { static parseid($cell) { singleNumRegexp.lastIndex = 0; let found = singleNumRegexp.exec($cell.attr('id')); return found ? parseInt(found[1]) : 0; } static parseClass($cell) { singleNumRegexp.lastIndex = 0; let found = singleNumRegexp.exec($cell.attr('class')); return found ? parseInt(found[1]) : 0; } static parsePosition($cell) { doubleNumReg.lastIndex = 0; let [col, row] = doubleNumReg.exec($cell.attr('id')).splice(1, 2); return [parseInt(col), parseInt(row)]; } static parseSettings() { let $discipline = $('#json_discipline'); let settings = {}; settings.discipline = {}; let disciplineRaw = $.parseJSON($discipline.html() || '{}'); Object.keys(disciplineRaw).forEach((keyRaw) => { let key = keyRaw.slice(0, 1).toLowerCase() + keyRaw.slice(1); if (keyRaw === "ID") key = 'id'; settings.discipline[key] = +disciplineRaw[keyRaw]; }); $discipline.remove(); return settings; } static parseRateForCell($cell, server) { if ($cell.hasClass('static')) { return this.parseRate($cell, $cell.text(), server); } else { return this.parseRate($cell, $cell.children('input').val(), server); } } static parseRate($cell, rate, server) { let def = server ? '-1' : '0'; return parseInt(rate || def); } static filterNumbers($input) { let scoreInputVal = $input.val(); let numbers = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; scoreInputVal = Array.prototype.filter.call(scoreInputVal, chr => chr in numbers).join(''); $input.val(scoreInputVal); } } class Cursor { constructor() { this.col = 0; this.row = 0; } update(col, row) { this.col = col; this.row = row; } get() { return [this.col, this.row]; } // TODO: REFACTOR // нреправильное название parseLocation для функции, которая // еще и обновляет значения стобца и строки parseLocation($cell) { let [col, row] = Base.parsePosition($cell); this.update(col, row); } isEmpty() { return (this.col === 0) && (this.row === 0); } } class Cell { constructor() { this.update(0, 0, 0); } update(recordBook, submodule, maxRate) { this.recordBook = recordBook; this.submodule = submodule; this.maxRate = maxRate; } } class CellInfo { constructor(cursor, cell) { this.cursor = cursor; this.cell = cell; this.$modulesHead = $('#modulesHead'); this.$submodulesHead = $('#submodulesHead'); this.$submodulesMaxRate = $('#submodulesHeadMaxRate'); } /** * * @param $cell выбранная ячейка * @desc определяет мероприятие, максимальный балл и id студента для ячейки таблицы баллов * делает это по id ячейки, тексту в заголовке таблицы и id ячейки с именем студента */ select($cell) { this.cursor.parseLocation($cell); let [col] = this.cursor.get(); let $curSubmoduleMaxRate = this.$submodulesMaxRate.children(`.col_${col}`); let recordBook = Base.parseid($cell.siblings('.name')); let submodule = Base.parseid($curSubmoduleMaxRate); let maxRate = parseInt($curSubmoduleMaxRate.text()); if ($cell.hasClass('extra')) { maxRate -= Base.parseRateForCell($cell.siblings('.semester')); maxRate -= Base.parseRateForCell($cell.prev('.extra')); maxRate = Math.max(0, maxRate); } this.cell.update(recordBook, submodule, maxRate); } reset() { this.cell.update(0, 0, 0); } } /** * @desc Класс, контролирующий строку состояния таблицы баллов * показывает студента, мероприятие и макс. балл, если выделена ячейка * */ class RateInfo { constructor(cursor, cell) { this.cursor = cursor; this.cell = cell; this.$modulesHead = $('#modulesHead'); this.$submodulesHead = $('#submodulesHead'); this.$submodulesMaxRate = $('#submodulesHeadMaxRate'); this.$cellInfo = $('#cellInfo'); this.$studentInfo = this.$cellInfo.find('#student .value'); this.$submoduleInfo = this.$cellInfo.find('#submodule .value'); this.$rateInfo = this.$cellInfo.find('#maxRate .value'); } /** * @desc обновляет имя студента, мероприятие и макс. балл по выбранной ячейке, выбирая текст из столбца и строки таблицы * @param $cell ячейка, информацию по которой нужно вывести в строке состояния * */ show($cell) { let [col] = this.cursor.get(); let studentName = $cell.siblings('.name').text(); let submoduleTitle = this.$submodulesHead.children(`.col_${col}`).text(); this.$studentInfo.html(studentName); this.$submoduleInfo.html(submoduleTitle); this.$rateInfo.html(this.cell.maxRate); this.$cellInfo.show(); } hide() { this.$cellInfo.hide(); } } /** * @desc класс, управляющий фильтром по группам в таблице баллов * следит за выпадающим списком групп и скрывает или показывает строки таблицы */ class GroupFilter { constructor(settings, $table, $selector) { this.settings = settings; this.$table = $table.children('tbody'); let groupFilter = JSON.parse(localStorage.groupFilter || "{}"); let groupid = groupFilter[this.settings.discipline.id] || 0; $selector.children('[value=' + groupid + ']').prop('selected', true); this.filterGroups(groupid); this.listen($selector); } filterGroups(groupid) { let $trs = this.$table.children(); $trs.show(); if (groupid) { $trs.not(`.group_${groupid}`).hide(); } } listen($selector) { $selector.change(() => { let groupid = parseInt($selector.val()); let groupFilter = JSON.parse(localStorage.groupFilter || "{}"); groupFilter[this.settings.discipline.id] = groupid; localStorage.groupFilter = JSON.stringify(groupFilter); this.filterGroups(groupid); }); } } /** * @desc класс, управляющий вводом баллов в таблицу */ class Rating { constructor() { this.settings = Base.parseSettings(); this.cell = new Cell(); this.cursor = new Cursor(); this.cellInfo = new CellInfo(this.cursor, this.cell); this.rateInfo = new RateInfo(this.cursor, this.cell); this.filter = new GroupFilter(this.settings, $('#studentsRate'), $('#groupSelector')); this.undoManager = new ActionHistory(); this.Direction = { Up: 0, Right: 1, Down: 2, Left: 3 }; let keyDirs = { 13: this.Direction.Down, // enter 40: this.Direction.Down, // down arrow 38: this.Direction.Up, // up arrow 39: this.Direction.Right, // right arrow 37: this.Direction.Left // left arrow }; let self = this; let oldInputVal = ''; // при нажатии на ячейку таблицы передаем фокус элементу input // обовляем курсор // обновляем строку статуса // запоминаем предыдущее значение в нем $('#studentsRate tbody').on('focusin', '.rate', function () { $(this).children('input').select(); self.cellInfo.select($(this)); self.rateInfo.show($(this)); $(this).parents('tr').addClass('focus'); oldInputVal = $(this).children('input').val(); }).on('focusout', '.rate', function () { self.rateInfo.hide($(this)); self.cellInfo.reset(); $(this).parents('tr').removeClass('focus'); }).on('keydown', '.rate', function (event) { // Разрешаем нажимать только цифры keyDownOnlyNumber(event); let key = event.keyCode; if (key == 90 && event.ctrlKey === true) { let didUndo = self.undoManager.undo(); if (!didUndo) { Popup.error("Еще нельзя отменить"); } return; } if (key == 89 && event.ctrlKey === true) { let didRedo = self.undoManager.redo(); if (!didRedo) { Popup.error("Еще нельзя повторить"); } return; } // esc if (key === 27) { $(this).children('input').val(oldInputVal); $(this).children('input').blur(); } if (!(key in keyDirs)) { return; } let direction = keyDirs[key]; let $cell = self.getDesiredCell(direction); if ($cell.length !== 0) { // Если ячейка найдена self.selectCell($cell); } else { $cell = $(this); let scoreInputVal = $cell.children('input').val(); if (scoreInputVal !== oldInputVal) { $cell.one('turnOn', () => self.selectCell($cell)); $cell.chlidren('input').blur(); } else { self.selectCell($cell); } } }).on('change', '.rate', function () { Base.filterNumbers($(this).children('input')); self.setRate($(this), oldInputVal); }).on('change', '.checkbox', function () { self.cellInfo.select($(this)); self.setExam($(this)); self.cellInfo.reset(); }).on('click', '.download button', function () { $.fileDownload(g_URLdir + 'handler/FileCreator/GenerateFinalForm', { httpMethod: 'POST', data: { 'disciplineID': self.settings.discipline.id, 'groupID': Base.parseid($(this)), 'stage': parseInt($(this).siblings('select').val()), 'lock': true }, }); }); } /** * @desc находит соседнюю ячейку в заданном направлении * @param direction направление движения * @returns {*} */ getDesiredCell(direction) { let [col, row] = this.cursor.get(); let $cell; do { if (direction === this.Direction.Left) col--; if (direction === this.Direction.Right) col++; if (direction === this.Direction.Up) row--; if (direction === this.Direction.Down) row++; $cell = $(`#col_row_${col}_${row}`); } while ($cell.length && !$cell.hasClass('rate')); return $cell; } selectCell($cell) { $cell.children('input').focus(); // Выбрать весь текст в ячейке setTimeout(() => $cell.children('input').select(), 0); } /** * @desc отправляет выставленный в ячейку балл на сервер и выводит сообщение об ошибке/успехе * @param $cell * @param oldInputVal */ setRate($cell, oldInputVal) { let scoreInputVal = $cell.children('input').val(); if (scoreInputVal === oldInputVal) return; if (!this.settings.discipline.isLocked) { if (window.confirm("Если выставить первый балл, то невозможно будет редактировать учебную карту дисциплины. Продолжить?")) { this.settings.discipline.isLocked = 1; } else { $cell.children('input').val(oldInputVal); return; } } let rate = Base.parseRateForCell($cell, true); let successText; let successUndoText; let errorText; if (scoreInputVal !== '' && oldInputVal !== '') { successText = 'Балл обновлен'; successUndoText = 'Балл исправлен'; errorText = 'Не удалось обновить балл'; } else if (scoreInputVal !== '' && oldInputVal === '') { successText = 'Балл добавлен'; successUndoText = 'Балл удален'; errorText = 'Не удалось добавить балл'; } else { successText = 'Балл удален'; successUndoText = 'Балл добавлен'; errorText = 'Не удалось удалить балл'; } let self = this; let oldGrade = Base.parseRate($cell, oldInputVal, true); let newGrade = rate; const newGradeDisplay = newGrade === -1? '' : newGrade; const oldGradeDisplay = oldGrade === -1? '' : oldGrade; let targetCellId = $cell.attr("id"); let newAction = this.undoManager.addAction(targetCellId, function (action) { // redo $targetCell = $("#" + targetCellId); let $input = $cell.children('input'); $input.turnOff(); $input.val(newGradeDisplay); $input.turnOn(); self.cellInfo.select($targetCell); action.deactivate(); self.sendRate($targetCell, 'handler/rating/SetRate', {rate: newGrade}, function () { action.activate(); }, function ($input) { $input.val(oldGradeDisplay); action.remove(); }, successText, errorText); self.selectCell($targetCell); }, function (action) { // undo $targetCell = $("#" + targetCellId); let $input = $cell.children('input'); $input.turnOff(); $input.val(oldGradeDisplay); $input.turnOn(); self.cellInfo.select($targetCell); action.deactivate(); self.sendRate($targetCell, 'handler/rating/SetRate', {rate: oldGrade}, function () { action.activate(); }, function ($input) { $input.val(newGradeDisplay); action.remove(); }, successUndoText, errorText ); self.selectCell($targetCell); }); this.sendRate($cell, 'handler/rating/SetRate', {rate}, function () { newAction.activate() }, function ($input) { newAction.remove(); $input.val(oldInputVal); }, successText, errorText); } /** * @desc отправляет выставленную неявку или автомат на экзамене на сервер и выводит сообщение об ошибке/успехе * @param $cell */ setExam($cell) { let optionVal; if ($cell.hasClass('absence')) optionVal = 'absence'; if ($cell.hasClass('autopass')) optionVal = 'pass'; if (!$cell.children('input').prop('checked')) optionVal = `drop_${optionVal}`; let successText; let errorText; let option; let oldOption; switch (optionVal) { case 'absence': successText = 'Неявка установлена'; errorText = 'Не удалось установить неявку'; option = 'absence'; oldOption = 'null'; break; case 'drop_absence': successText = 'Неявка удалена'; errorText = 'Не удалось удалить неявку'; option = 'null'; oldOption = 'absence'; break; case 'pass': successText = 'Автомат установлен'; errorText = 'Не удалось установить автомат'; option = 'pass' oldOption = 'null';; break; case 'drop_pass': successText = 'Автомат удален'; errorText = 'Не удалось удалить автомат'; option = 'null'; oldOption = 'pass'; break; } let self = this; let targetCellId = $cell.attr("id"); // let newAction = this.undoManager.addAction(targetCellId, // function (action) { // $targetCell = $("#" + targetCellId); // let $input = $cell.children('input'); // $input.prop("checked", !$input.prop("checked")); // //self.cellInfo.select($targetCell); // self.sendRate($targetCell, 'handler/rating/SetExamPeriodOption', {option}, // function () { // action.activate(); // }, // function ($input) { // $input.prop("checked", !$input.prop("checked")) // action.remove(); // }, successText, errorText); // //self.selectCell($targetCell); // }, // function (action) { // $targetCell = $("#" + targetCellId); // let $input = $cell.children('input'); // $input.prop("checked", !$input.prop("checked")); // //self.cellInfo.select($targetCell); // self.sendRate($targetCell, 'handler/rating/SetExamPeriodOption', {option: oldOption}, // function () { // action.activate(); // }, // function ($input) { // $input.prop("checked", !$input.prop("checked")) // action.remove(); // }, successText, errorText); // //self.selectCell($targetCell); // }); this.sendRate($cell, 'handler/rating/SetExamPeriodOption', {option}, function () { //newAction.activate() }, function ($input) { //newAction.remove(); $input.prop("checked", !$input.prop("checked")); }, successText, errorText); } sendRate($cell, url, data, onSuccess, onFail, successText, errorText) { let $input = $cell.children('input'); // Действия необходимые для безопасной обработки в асинхронном режиме $input.turnOff(); window.onbeforeunload = () => { return "Запрос на добавление/изменение/удалени баллов еще не обработан!"; }; data.recordBookID = this.cell.recordBook; data.disciplineID = this.settings.discipline.id; data.submoduleID = this.cell.submodule; $.postJSON(g_URLdir + url, data).success(() => { this.recountScores($cell, data.submoduleID); Popup.success(successText); window.onbeforeunload = undefined; onSuccess(); }).fail(jqXHR => { onFail($input); const status = parseInt(jqXHR.status); let message = errorText; try { if (status === 400) message = JSON.parse(jqXHR.responseText).message; } catch(error) { } if (status !== 0) Popup.error(message); window.onbeforeunload = undefined; }).always(() => $input.turnOn()); } recountScores($cell, submoduleID) { let $cells = $cell.parent().children(); let [col, row] = Base.parsePosition($cell); let examCol = Base.parseClass($(`#exam_${submoduleID}`)); let absenceCol = Base.parseClass($(`#absence_${submoduleID}`)); let autopassCol = Base.parseClass($(`#autopass_${submoduleID}`)); if ($cell.hasClass('exam')) { $cells.filter(`#col_row_${absenceCol}_${row}`).children('input').prop('checked', false); $cells.filter(`#col_row_${autopassCol}_${row}`).children('input').prop('checked', false); } if ($cell.hasClass('absence')) { $cells.filter(`#col_row_${examCol}_${row}`).children('input').val(''); $cells.filter(`#col_row_${autopassCol}_${row}`).children('input').prop('checked', false); } if ($cell.hasClass('autopass')) { $cells.filter(`#col_row_${examCol}_${row}`).children('input').val(''); $cells.filter(`#col_row_${absenceCol}_${row}`).children('input').prop('checked', false); } let $regularCells = $cells.filter('.regular'); if ($regularCells.length) { let semesterRate = $regularCells.aggregate((init, elem) => init + Base.parseRateForCell($(elem))); $cells.filter('.semester').text(semesterRate); } let semesterRate = Base.parseRateForCell($cells.filter('.semester')); let bonusRate = Base.parseRateForCell($cells.filter('.bonus')); let extraRate = $cells.filter('.extra').aggregate((init, elem) => init + Base.parseRateForCell($(elem))); let examRate = $cells.filter('.exam').aggregate((init, elem) => Math.max(init, Base.parseRateForCell($(elem)))); let rateResult = semesterRate + bonusRate + extraRate + examRate; let rateResultText = (rateResult > 100) ? '100+' : rateResult; $cells.filter('.result').text(rateResultText); if ($cell.hasClass('extra')) { let $nextExtraCell = $cell.next('.extra'); if ($nextExtraCell.length) { let maxExtraRate = this.settings.discipline.type == 'exam' ? 38 : 60; let curRate = Base.parseRateForCell($cell, true); let maxVal = maxExtraRate - semesterRate - curRate; if (maxVal > 0 && curRate !== -1) { $nextExtraCell.find('input').attr('placeholder', 'макс. ' + maxVal); } else { $nextExtraCell.find('input').attr('placeholder', '–').val(''); } } } } } $(() => new Rating());