NetSchool Tweaks

Дополнительные инструменты для NetSchool / NetCity (Сетевой Город. Образование)

目前為 2024-03-09 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         NetSchool Tweaks
// @namespace    https://greasyfork.org/users/843419
// @description  Дополнительные инструменты для NetSchool / NetCity (Сетевой Город. Образование)
// @version      1.0.4
// @author       Zgoly
// @match        *://*/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=ir-tech.ru
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

try {
    console.log(language.Generic.Calendar.kTitle1, 'найден, NetSchool Tweaks активен')
} catch {
    return
}

let autoLogin = GM_getValue('autoLogin', false)
let loginName = GM_getValue('loginName', 'Пользователь')
let password = GM_getValue('password', '12345678')
let schoolId = GM_getValue('schoolId', '')
let autoSkip = GM_getValue('autoSkip', true)

function waitForElement(selector) {
    return new Promise(resolve => {
        if (document.querySelector(selector)) return resolve(document.querySelector(selector))

        const observer = new MutationObserver(mutations => {
            if (document.querySelector(selector)) {
                observer.disconnect()
                resolve(document.querySelector(selector))
            }
        })

        observer.observe(document.body, { childList: true, subtree: true })
    })
}

if (autoLogin && window.location.pathname.startsWith('/authorize')) {
    function runAngularAction() {
        try {
            angular.element(document.body).scope().$$childTail.$ctrl.loginStrategiesService.loginWithLoginPassCheck(loginName, password, schoolId, null, { 'idpBindUser': 1 })
        } catch {
            requestAnimationFrame(runAngularAction)
        }
    }

    runAngularAction()
}

waitForElement('ns-modal').then((element) => {
    if (autoSkip && element.getAttribute('header') == language.Generic.Login.kTitleSecurityWarning) {
        element.querySelector('button').click()
        console.log('Security warning modal skipped')
    }
})

function nstSwitch(parentElement) {
    let label = document.createElement('label')
    label.classList.add('nst-switch')

    let input = document.createElement('input')
    input.type = 'checkbox'
    input.classList.add('nst-hide')

    let div = document.createElement('div')

    label.append(input)
    label.append(div)

    parentElement.append(label)

    return input
}

function nstModal(headlineText, contentHTML, showSaveButton = true) {
    return new Promise((resolve, reject) => {
        let dialog = document.createElement('dialog')
        dialog.classList.add('nst-dialog')

        let dialogWrapper = document.createElement('div')
        dialogWrapper.classList.add('nst-dialog-wrapper')
        dialog.append(dialogWrapper)

        let headline = document.createElement('p')
        headline.classList.add('nst-headline')
        headline.textContent = headlineText
        dialogWrapper.append(headline)

        let dialogAutofocus = document.createElement('input')
        dialogAutofocus.autofocus = 'autofocus'
        dialogAutofocus.style.display = 'none'
        dialogWrapper.append(dialogAutofocus)

        let content = document.createElement('div')
        content.classList.add('nst-content')
        content.append(contentHTML)
        dialogWrapper.append(content)

        let actions = document.createElement('div')
        actions.classList.add('nst-actions')

        let closeButton = document.createElement('button')
        closeButton.classList.add('nst-close')
        closeButton.textContent = 'Закрыть'
        closeButton.addEventListener('click', () => closeDialog(false))
        actions.append(closeButton)

        if (showSaveButton) {
            let saveButton = document.createElement('button')
            saveButton.classList.add('nst-save')
            saveButton.textContent = 'Сохранить'
            saveButton.addEventListener('click', () => closeDialog(true))
            actions.append(saveButton)
        }

        dialogWrapper.append(actions)

        document.body.append(dialog)

        dialog.showModal()
        // Убираем фокус с поля ввода
        document.activeElement.blur()

        document.body.classList.add('nst-no-scroll')

        function closeDialog(result = false) {
            dialog.classList.add('nst-hide-dialog')

            setTimeout(() => {
                dialog.remove()
                if (document.getElementsByTagName('dialog').length < 1) document.body.classList.remove('nst-no-scroll')
            }, 500)

            resolve(result)
        }

        contentHTML.closeDialog = closeDialog

        dialog.addEventListener('click', (event) => {
            if (event.target === dialog) {
                closeDialog(false)
            }
        })

        dialog.addEventListener('close', () => closeDialog())
        dialog.addEventListener('error', reject)
    })
}

let settings = document.createElement('li')
let settingsLink = document.createElement('a')
settings.append(settingsLink)

let settingsBody = document.createElement('span')
settingsBody.classList.add('cb-settings')
settingsLink.append(settingsBody)

let settingsIcon = document.createElement('i')
settingsIcon.classList.add('icon-gear', 'nst-settings-icon')
settingsBody.append(settingsIcon)

settingsBody.addEventListener('click', () => {
    let div = document.createElement('div')
    let table = document.createElement('table')

    // Переключатель авто входа
    let autoLoginRow = document.createElement('tr')

    let autoLoginLabelCell = document.createElement('td')

    let autoLoginLabelTitle = document.createElement('div')
    autoLoginLabelTitle.classList.add('nst-label-title')
    autoLoginLabelTitle.textContent = 'Авто вход'
    autoLoginLabelCell.append(autoLoginLabelTitle)

    let autoLoginLabelDescription = document.createElement('div')
    autoLoginLabelDescription.classList.add('nst-label-description')
    autoLoginLabelDescription.textContent = 'Авто вход по логину и паролю.'
    autoLoginLabelCell.append(autoLoginLabelDescription)

    let autoLoginInputCell = document.createElement('td')
    let autoLoginInput = nstSwitch(autoLoginInputCell)
    autoLoginInput.checked = autoLogin

    autoLoginRow.append(autoLoginLabelCell)
    autoLoginRow.append(autoLoginInputCell)

    table.append(autoLoginRow)

    // Поле логина
    let loginNameRow = document.createElement('tr')

    let loginNameLabelCell = document.createElement('td')

    let loginNameLabelTitle = document.createElement('div')
    loginNameLabelTitle.classList.add('nst-label-title')
    loginNameLabelTitle.textContent = 'Логин'
    loginNameLabelCell.append(loginNameLabelTitle)

    let loginNameLabelDescription = document.createElement('div')
    loginNameLabelDescription.classList.add('nst-label-description')
    loginNameLabelDescription.textContent = 'Логин для входа.'
    loginNameLabelCell.append(loginNameLabelDescription)

    let loginNameInputCell = document.createElement('td')
    loginNameInputCell.classList.add('nst-flex')
    let loginNameInput = document.createElement('input')
    loginNameInput.type = 'text'
    loginNameInput.value = loginName

    loginNameInputCell.append(loginNameInput)

    loginNameRow.append(loginNameLabelCell)
    loginNameRow.append(loginNameInputCell)

    table.append(loginNameRow)

    // Поле пароля
    let passwordRow = document.createElement('tr')

    let passwordLabelCell = document.createElement('td')

    let passwordLabelTitle = document.createElement('div')
    passwordLabelTitle.classList.add('nst-label-title')
    passwordLabelTitle.textContent = 'Пароль'
    passwordLabelCell.append(passwordLabelTitle)

    let passwordLabelDescription = document.createElement('div')
    passwordLabelDescription.classList.add('nst-label-description')
    passwordLabelDescription.textContent = 'Пароль для входа.'
    passwordLabelCell.append(passwordLabelDescription)

    let passwordInputCell = document.createElement('td')
    passwordInputCell.classList.add('nst-password-input-cell', 'nst-flex')
    let passwordInput = document.createElement('input')
    passwordInput.type = 'password'
    passwordInput.classList.add('nst-password')
    passwordInput.value = password

    let passwordEye = document.createElement('div')
    passwordEye.classList.add('nst-password-eye')
    passwordEye.addEventListener('click', () => {
        passwordInput.type = passwordInput.type === 'password' ? 'text' : 'password'
    })

    let passwordEyeInner = document.createElement('div')
    passwordEyeInner.classList.add('nst-password-eye-inner')
    let passwordEyeOuter = document.createElement('div')
    passwordEyeOuter.classList.add('nst-password-eye-outer')

    passwordEye.append(passwordEyeInner)
    passwordEye.append(passwordEyeOuter)

    passwordInputCell.append(passwordInput)
    passwordInputCell.append(passwordEye)

    passwordRow.append(passwordLabelCell)
    passwordRow.append(passwordInputCell)

    table.append(passwordRow)

    // Поле ID школы
    let schoolIdRow = document.createElement('tr')

    let schoolIdLabelCell = document.createElement('td')

    let schoolIdLabelTitle = document.createElement('div')
    schoolIdLabelTitle.classList.add('nst-label-title')
    schoolIdLabelTitle.textContent = 'ID школы'
    schoolIdLabelCell.append(schoolIdLabelTitle)

    let schoolIdLabelDescription = document.createElement('div')
    schoolIdLabelDescription.classList.add('nst-label-description')
    schoolIdLabelDescription.textContent = 'ID школы для входа. Оставьте пустым, если не знаете.'
    schoolIdLabelCell.append(schoolIdLabelDescription)

    let schoolIdInputCell = document.createElement('td')
    schoolIdInputCell.classList.add('nst-flex')
    let schoolIdInput = document.createElement('input')
    schoolIdInput.type = 'text'
    schoolIdInput.value = schoolId
    schoolIdInput.placeholder = schoolId

    schoolIdInputCell.append(schoolIdInput)

    schoolIdRow.append(schoolIdLabelCell)
    schoolIdRow.append(schoolIdInputCell)

    table.append(schoolIdRow)

    // Переключатель авто пропуска
    let autoSkipRow = document.createElement('tr')

    let autoSkipLabelCell = document.createElement('td')

    let autoSkipLabelTitle = document.createElement('div')
    autoSkipLabelTitle.classList.add('nst-label-title')
    autoSkipLabelTitle.textContent = 'Авто пропуск'
    autoSkipLabelCell.append(autoSkipLabelTitle)

    let autoSkipLabelDescription = document.createElement('div')
    autoSkipLabelDescription.classList.add('nst-label-description')
    autoSkipLabelDescription.textContent = 'Авто пропуск навязчивых уведомлений.'
    autoSkipLabelCell.append(autoSkipLabelDescription)

    let autoSkipInputCell = document.createElement('td')
    let autoSkipInput = nstSwitch(autoSkipInputCell)
    autoSkipInput.checked = autoSkip

    autoSkipRow.append(autoSkipLabelCell)
    autoSkipRow.append(autoSkipInputCell)

    table.append(autoSkipRow)

    function toggleFields() {
        let fields = [loginNameInput, passwordInput, schoolIdInput]
        fields.forEach(field => {
            field.disabled = !autoLoginInput.checked
        })
    }
    toggleFields()
    autoLoginInput.addEventListener('change', toggleFields)

    div.append(table)

    // Сохранение настроек
    nstModal('Настройки', div).then(save => {
        if (save) {
            GM_setValue('autoLogin', autoLoginInput.checked)
            autoLogin = autoLoginInput.checked
            GM_setValue('loginName', loginNameInput.value)
            loginName = loginNameInput.value
            GM_setValue('password', passwordInput.value)
            password = passwordInput.value
            GM_setValue('schoolId', schoolIdInput.value == '' ? appContext.schoolId : schoolIdInput.value)
            schoolId = schoolIdInput.value == '' ? appContext.schoolId : schoolIdInput.value
            GM_setValue('autoSkip', autoSkipInput.checked)
            autoSkip = autoSkipInput.checked
        }
    })

    let previewMarksWrapper = document.createElement('div')
    previewMarksWrapper.classList.add('preview-marks-wrapper')
    div.append(previewMarksWrapper)

    let at = appContext.at
    let weekStart = appContext.weekStart
    let weekEnd = appContext.weekEnd

    // Начало учебного года
    let startDateInput = document.createElement('input')
    startDateInput.type = 'date'
    startDateInput.value = weekStart
    previewMarksWrapper.append(startDateInput)

    // Конец учебного года
    let endDateInput = document.createElement('input')
    endDateInput.type = 'date'
    endDateInput.value = weekEnd
    previewMarksWrapper.append(endDateInput)

    if (weekStart == undefined || weekEnd == undefined) {
        fetch('/webapi/v2/reports/studenttotal', { 'headers': { 'at': at } }).then((response) => {
            return response.json()
        }).then((data) => {
            weekStart = data.filterSources[3].defaultRange.start.substring(0, 10)
            startDateInput.value = weekStart
            weekEnd = data.filterSources[3].defaultRange.end.substring(0, 10)
            endDateInput.value = weekEnd
        })
    }

    // Кнопка предпросмотра оценок
    let previewMarksButton = document.createElement('button')
    previewMarksButton.innerText = 'Предпросмотр оценок'
    previewMarksButton.addEventListener('click', () => {
        let marksTableWrapper = document.createElement('div')
        marksTableWrapper.classList.add('nst-marks-table-wrapper')

        let contentDiv = document.createElement('div')
        contentDiv.classList.add('nst-content')
        contentDiv.append(marksTableWrapper)

        fetch('/webapi/student/diary/init', { 'headers': { 'at': at } }).then((response) => {
            return response.json()
        }).then((data) => {
            let studentId = data.students[0].studentId
            let yearId = appContext.yearId
            let startDate = startDateInput.value
            let endDate = endDateInput.value
            // Запрос дневика
            fetch(`/webapi/student/diary?studentId=${studentId}&weekStart=${startDate}&weekEnd=${endDate}&withLaAssigns=true&yearId=${yearId}`, { 'headers': { 'at': at } }).then((response) => {
                return response.json()
            }).then((data) => {
                // Повторный запрос дневника (с текущей датой для отображения правильной недели, игнорируется)
                let currentDate = date2strf(new Date(), 'yyyy\x01mm\x01dd\x01.')
                fetch(`/webapi/student/diary?studentId=${studentId}&weekStart=${currentDate}&weekEnd=${currentDate}&withLaAssigns=true&yearId=${yearId}`, { 'headers': { 'at': at } })

                let marksTable = document.createElement('table')
                marksTableWrapper.append(marksTable)

                let tableControlsDiv = document.createElement('div')
                tableControlsDiv.classList.add('nst-controls')
                contentDiv.append(tableControlsDiv)

                let selectAllButton = document.createElement('button')
                selectAllButton.innerText = 'Выбрать все'
                selectAllButton.addEventListener('click', () => {
                    for (let row of marksTable.rows) {
                        row.classList.add('nst-row-selected')
                    }
                    updateButtons()
                })
                tableControlsDiv.append(selectAllButton)

                let deselectAllButton = document.createElement('button')
                deselectAllButton.innerText = 'Отменить выбор'
                deselectAllButton.addEventListener('click', () => {
                    for (let row of marksTable.rows) {
                        row.classList.remove('nst-row-selected')
                    }
                    updateButtons()
                })
                tableControlsDiv.append(deselectAllButton)

                let addRowButton = document.createElement('button')
                addRowButton.innerText = 'Cоздать'
                addRowButton.addEventListener('click', () => {
                    let selectedRows = marksTable.querySelectorAll('.nst-row-selected')
                    let newRow = createRow()
                    if (selectedRows.length > 0) {
                        selectedRows[selectedRows.length - 1].after(newRow)
                        selectedRows.forEach(row => row.classList.remove('nst-row-selected'))
                    } else {
                        marksTable.prepend(newRow)
                    }
                    cookRow(newRow)

                    newRow.classList.add('nst-row-selected')
                    newRow.scrollIntoView({behavior: "smooth"})
                    updateButtons()
                })
                tableControlsDiv.append(addRowButton)

                let cloneRowButton = document.createElement('button')
                cloneRowButton.innerText = 'Клонировать'
                cloneRowButton.addEventListener('click', () => {
                    let selectedRows = marksTable.querySelectorAll('.nst-row-selected')
                    selectedRows.forEach(row => {
                        let clonedRow = row.cloneNode(true)
                        row.after(clonedRow)
                        cookRow(clonedRow)

                        let marksCell = clonedRow.querySelector('.nst-marks-cell')
                        Array.from(marksCell.children).forEach(markDiv => {
                            cookMark(markDiv)
                        })

                        row.classList.remove('nst-row-selected')
                    })
                    updateButtons()
                })
                tableControlsDiv.append(cloneRowButton)

                let removeRowButton = document.createElement('button')
                removeRowButton.innerText = 'Удалить'
                removeRowButton.addEventListener('click', () => {
                    let selectedRows = marksTable.querySelectorAll('.nst-row-selected')
                    selectedRows.forEach(row => row.remove())
                    updateButtons()
                })
                tableControlsDiv.append(removeRowButton)

                let addMarkButton = document.createElement('button')
                addMarkButton.innerText = 'Добавить оценку'
                addMarkButton.addEventListener('click', () => {
                    let selectedRows = marksTable.querySelectorAll('.nst-row-selected')
                    if (selectedRows.length == 0) return

                    let [templateTable, markInput, weightInput] = markModalTemplate(5, 20)

                    nstModal('Добавление оценки', templateTable).then(save => {
                        if (save) {
                            selectedRows.forEach(row => {
                                let mark = createMark(markInput.value, weightInput.value)
                                row.querySelector('.nst-marks-cell').append(mark)
                                cookMark(mark)
                                highlightMark(mark)
                                row.classList.remove('nst-row-selected')
                            })

                            updateButtons()
                        }
                    })
                })
                tableControlsDiv.append(addMarkButton)

                function createMark(mark, weight, fullAssignment = null) {
                    let markDiv = document.createElement('div')
                    if (fullAssignment) markDiv.dataset.assignment = JSON.stringify(fullAssignment)
                    markDiv.classList.add('nst-mark')

                    let markValue = document.createElement('p')
                    markValue.innerText = mark
                    markValue.classList.add('nst-mark-value')
                    markDiv.append(markValue)

                    let weightValue = document.createElement('p')
                    weightValue.innerText = weight
                    weightValue.classList.add('nst-weight-value')
                    markDiv.append(weightValue)

                    return markDiv
                }

                function createRow(name = '') {
                    let row = document.createElement('tr')

                    let nameCell = document.createElement('td')
                    let nameInput = document.createElement('input')
                    nameInput.value = name
                    nameInput.classList.add('nst-name-input')
                    nameInput.placeholder = "Имя предмета"
                    nameCell.append(nameInput)
                    row.append(nameCell)

                    let marksCell = document.createElement('td')
                    marksCell.classList.add('nst-marks-cell')
                    row.append(marksCell)

                    let totalCell = document.createElement('td')
                    totalCell.classList.add('nst-total-cell')
                    row.append(totalCell)

                    return row
                }

                function markModalTemplate(mark, weight) {
                    let templateTable = document.createElement('table')

                    let markRow = document.createElement('tr')
                    templateTable.append(markRow)

                    let markLabelCell = document.createElement('td')
                    markRow.append(markLabelCell)

                    let markLabelTitle = document.createElement('div')
                    markLabelTitle.classList.add('nst-label-title')
                    markLabelTitle.textContent = 'Оценка'
                    markLabelCell.append(markLabelTitle)

                    let markLabelDescription = document.createElement('div')
                    markLabelDescription.classList.add('nst-label-description')
                    markLabelDescription.textContent = 'Оценка.'
                    markLabelCell.append(markLabelDescription)

                    let markInputCell = document.createElement('td')
                    markInputCell.classList.add('nst-flex')
                    markRow.append(markInputCell)

                    let markInput = document.createElement('input')
                    markInput.readOnly = true
                    markInput.value = mark
                    markInputCell.append(markInput)

                    let markSelectorsDiv = document.createElement('div')
                    markSelectorsDiv.classList.add('nst-mark-selectors')
                    markInputCell.append(markSelectorsDiv)

                    let marks = ['5', '4', '3', '2', '•']

                    marks.forEach(mark => {
                        let markButton = document.createElement('button')

                        markButton.innerText = mark
                        markButton.addEventListener('click', () => markInput.value = mark)
                        markSelectorsDiv.append(markButton)
                    })

                    let weightRow = document.createElement('tr')
                    templateTable.append(weightRow)

                    let weightLabelCell = document.createElement('td')
                    weightRow.append(weightLabelCell)

                    let weightLabelTitle = document.createElement('div')
                    weightLabelTitle.classList.add('nst-label-title')
                    weightLabelTitle.textContent = 'Вес'
                    weightLabelCell.append(weightLabelTitle)

                    let weightLabelDescription = document.createElement('div')
                    weightLabelDescription.classList.add('nst-label-description')
                    weightLabelDescription.textContent = 'Вес оценки.'
                    weightLabelCell.append(weightLabelDescription)

                    let weightInputCell = document.createElement('td')
                    weightInputCell.classList.add('nst-flex')
                    weightRow.append(weightInputCell)

                    let weightInput = document.createElement('input')
                    weightInput.type = 'number'
                    weightInput.value = weight
                    weightInputCell.append(weightInput)

                    return [templateTable, markInput, weightInput]
                }

                function highlightMark(mark) {
                    mark.classList.remove('nst-mark-highlight');
                    mark.offsetWidth;
                    mark.classList.add('nst-mark-highlight');
                }

                function cookMark(mark) {
                    let markValue = mark.querySelector('.nst-mark-value')
                    let weightValue = mark.querySelector('.nst-weight-value')

                    mark.addEventListener('click', () => {
                        let modalDiv = document.createElement('div')
                        let [templateTable, markInput, weightInput] = markModalTemplate(markValue.innerText, weightValue.innerText)
                        modalDiv.append(templateTable)

                        let controlsDiv = document.createElement('div')
                        controlsDiv.classList.add('nst-controls')
                        modalDiv.append(controlsDiv)

                        let cloneMarkButton = document.createElement('button')
                        cloneMarkButton.innerText = 'Клонировать'
                        controlsDiv.append(cloneMarkButton)
                        cloneMarkButton.addEventListener('click', () => {
                            modalDiv.closeDialog(true)
                            let newMark = mark.cloneNode(true)
                            mark.after(newMark)
                            cookMark(newMark)
                            highlightMark(newMark)
                        })

                        let deleteMarkButton = document.createElement('button')
                        deleteMarkButton.innerText = 'Удалить'
                        controlsDiv.append(deleteMarkButton)
                        deleteMarkButton.addEventListener('click', () => {
                            mark.remove()
                            modalDiv.closeDialog(false)
                        })

                        if (mark.dataset.assignment) {
                            let assignment = JSON.parse(mark.dataset.assignment)

                            let restoreMarkButton = document.createElement('button')
                            restoreMarkButton.innerText = 'Восстановить'
                            controlsDiv.append(restoreMarkButton)
                            restoreMarkButton.addEventListener('click', () => {
                                markInput.value = assignment.mark
                                weightInput.value = assignment.weight
                            })

                            let assignmentMarkButton = document.createElement('button')
                            assignmentMarkButton.innerText = 'Подробности'
                            controlsDiv.append(assignmentMarkButton)
                            assignmentMarkButton.addEventListener('click', () => {
                                // TODO Доделать
                                let assignmentTable = document.createElement('table')

                                let translations = {
                                    'id': 'ID задания',
                                    'assignmentName': 'Тема задания',
                                    'activityName': 'Имя деятельности',
                                    'problemName': 'Название задачи',
                                    'studentId': 'ID ученика',
                                    'subjectGroup.id': 'ID предмета',
                                    'subjectGroup.name': 'Название предмета',
                                    'teachers.0.id': 'ID учителя',
                                    'teachers.0.name': 'Имя учителя',
                                    'productId': 'ID продукта',
                                    'isDeleted': 'Удалено',
                                    'weight': 'Вес',
                                    'date': 'Дата',
                                    'description': 'Описание',
                                    'mark': 'Оценка',
                                    'typeId': 'ID типа задания',
                                    'type': 'Тип задания'
                                }

                                for (let key in assignment) {
                                    let translation = translations[key] || key
                                    let value = assignment[key]
                                    value = value === true ? "Да" : value === false ? "Нет" : value

                                    let assignmentRow = document.createElement('tr')
                                    assignmentTable.append(assignmentRow)

                                    let assignmentLabelCell = document.createElement('td')
                                    assignmentLabelCell.innerText = translation
                                    assignmentRow.append(assignmentLabelCell)

                                    let assignmentInputCell = document.createElement('td')
                                    assignmentInputCell.classList.add('nst-flex')
                                    assignmentRow.append(assignmentInputCell)


                                    let assignmentInput

                                    console.log(key, value, typeof value)
                                    if (key === 'date') {
                                        assignmentInput = document.createElement('input')
                                        assignmentInput.readOnly = true
                                        assignmentInput.type = 'date'
                                        assignmentInput.value = value
                                    } else if (typeof value === 'number') {
                                        assignmentInput = document.createElement('input')
                                        assignmentInput.readOnly = true
                                        assignmentInput.type = 'number'
                                        assignmentInput.value = value
                                    } else {
                                        assignmentInput = document.createElement('div')
                                        assignmentInput.innerText = value
                                        assignmentInput.classList.add('nst-area')
                                    }

                                    assignmentInputCell.append(assignmentInput)
                                }

                                nstModal('Подробности задания', assignmentTable, false)
                            })
                        }

                        nstModal('Редактирование оценки', modalDiv).then(save => {
                            if (save) {
                                markValue.innerText = markInput.value
                                weightValue.innerText = weightInput.value
                                highlightMark(mark)
                            }
                        })
                    })
                }
                function updateButtons() {
                    if (marksTable.querySelectorAll('.nst-row-selected').length > 0) {
                        addMarkButton.disabled = false
                        cloneRowButton.disabled = false
                        removeRowButton.disabled = false
                    } else {
                        addMarkButton.disabled = true
                        cloneRowButton.disabled = true
                        removeRowButton.disabled = true
                    }
                }

                updateButtons()

                function cookRow(row) {
                    let marksCell = row.querySelector('.nst-marks-cell')
                    let totalCell = row.querySelector('.nst-total-cell')

                    // Изменение цвета оценки / балла
                    function calculateTotalScore() {
                        let markSum = 0
                        let weightSum = 0

                        Array.from(marksCell.children).forEach(markDiv => {
                            let markValue = markDiv.querySelector('.nst-mark-value')
                            let weightValue = markDiv.querySelector('.nst-weight-value')
                            let mark = markValue.innerText.replaceAll('•', '2')
                            let weight = Number(weightValue.innerText)

                            markValue.classList.remove(...['nst-mark-excellent', 'nst-mark-good', 'nst-mark-average', 'nst-mark-bad'])
                            markValue.classList.add(getMarkClass(mark))

                            markSum += mark * weight
                            weightSum += weight
                        })

                        totalCell.innerText = weightSum ? Number((markSum / weightSum).toFixed(2)) : 0
                        totalCell.classList.remove(...['nst-mark-excellent', 'nst-mark-good', 'nst-mark-average', 'nst-mark-bad'])
                        totalCell.classList.add(getMarkClass(totalCell.innerText))
                    }

                    function getMarkClass(mark) {
                        return Number(mark) >= 4.60 ? 'nst-mark-excellent' : Number(mark) >= 3.60 ? 'nst-mark-good' : Number(mark) >= 2.60 ? 'nst-mark-average' : 'nst-mark-bad'
                    }

                    let observer = new MutationObserver(() => {
                        calculateTotalScore()
                    })
                    observer.observe(marksCell, { childList: true, subtree: true })

                    calculateTotalScore()

                    // Выделение строки при нажатии на балл
                    totalCell.addEventListener('click', () => {
                        row.classList.toggle('nst-row-selected')
                        updateButtons()
                    })
                }

                function flattenJson(json) {
                    let result = {}

                    function flatten(obj, prefix = '') {
                        for (let key in obj) {
                            if (typeof obj[key] === 'object' && obj[key] !== null) {
                                flatten(obj[key], prefix + key + '.')
                            } else {
                                result[prefix + key] = obj[key]
                            }
                        }
                    }

                    flatten(json)
                    return result
                }

                fetch('/webapi/grade/assignment/types').then((response) => {
                    return response.json()
                }).then((types) => {
                    for (let day of data.weekDays) {
                        for (let lesson of day.lessons) {
                            if (Array.isArray(lesson.assignments)) {
                                for (let assignment of lesson.assignments) {
                                    if (assignment.mark) {
                                        fetch(`/webapi/student/diary/assigns/${assignment.id}`, { 'headers': { 'at': at } }).then((response) => {
                                            return response.json()
                                        }).then((fullAssignment) => {
                                            // Модификация данных в удобный формат
                                            fullAssignment.mark = assignment.mark.mark
                                            fullAssignment.studentId = assignment.mark.studentId
                                            fullAssignment.typeId = assignment.typeId
                                            fullAssignment.date = fullAssignment.date.substring(0, 10)

                                            let item = types.find(data => data.id == fullAssignment.typeId)
                                            fullAssignment.type = item.name

                                            fullAssignment = flattenJson(fullAssignment)

                                            for (let key in fullAssignment) {
                                                if (fullAssignment[key] == null) delete fullAssignment[key]
                                            }

                                            // Объявление / создание ряда
                                            let row = Array.from(marksTable.rows).find(r => r.querySelector('.nst-name-input').value == lesson.subjectName)
                                            if (!row) {
                                                row = createRow(lesson.subjectName)
                                                marksTable.append(row)
                                                cookRow(row)
                                            }

                                            // Добавление оценки
                                            let createdMark = createMark(fullAssignment.mark, fullAssignment.weight, fullAssignment)
                                            row.querySelector('.nst-marks-cell').append(createdMark)
                                            cookMark(createdMark)
                                        })
                                    }
                                }
                            }
                        }
                    }
                }).catch(err => {
                    console.error('Произошла ошибка: ', err)
                })
            })
        })

        nstModal('Предпросмотр оценок', contentDiv, false)
    })
    previewMarksWrapper.append(previewMarksButton)
})

waitForElement('.top-right-menu').then((elm) => {
    elm.prepend(settings)
})

GM_addStyle(`
@import url('https://fonts.googleapis.com/css2?family=Nunito&display=swap');

:root {
    --nst-primary: #64a0c8;
    --nst-secondary: #415f78;
    --nst-tertiary: #374b5f;
    --nst-quaternary: #232a32;
    --nst-quinary: #1a1c1e;
    --nst-senary: #141618;

    --nst-text-primary: #c8e6ff;
    --nst-text-secondary: #aadcff;
    --nst-text-tertiary: #87afc8;
}

.nst-no-scroll {
    touch-action: none;
    overflow: hidden;
}

.nst-flex {
    display: flex;
    flex-direction: column;
}

.nst-flex input {
    flex: 1;
}

.nst-settings-icon {
    display: flex !important;
    justify-content: center;
    color: white;
    scale: 0.75;
}

.nst-dialog {
    border: none;
    outline: none;
    background: var(--nst-quinary);
    border-radius: 32px;
    box-shadow: rgba(0, 0, 0, 0.25) 0 0 25px;
    padding: 0;
}

.nst-dialog-wrapper {
    display: flex;
    flex-direction: column;
    padding: 24px;
    max-height: calc(100vh - 48px);
    max-width: calc(100vw - 48px);
}

.nst-content {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow: auto;
}

.nst-dialog .preview-marks-wrapper {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    padding-top: 16px;
    gap: 8px;
}

.nst-dialog * {
    font-family: 'Nunito';
    color: var(--nst-text-secondary);
}

.nst-dialog .nst-headline:first-child {
    margin-top: 0px;
}

.nst-dialog .nst-headline {
    color: var(--nst-text-primary);
    font-size: 1.5em;
    margin: 0px;
    margin-bottom: 16px;
}

.nst-dialog .nst-actions {
    margin-top: 16px;
    display: flex;
    gap: 8px;
    justify-content: flex-end;
}

.nst-dialog button {
    cursor: pointer;
    color: var(--button-color);
    border-radius: 28px;
    padding: 12px;
    margin: 0;
    border: none;
    outline: none;
    transition: 0.2s;
    background: var(--nst-quaternary);
}

.nst-dialog button:not([disabled]):hover {
    background: var(--nst-tertiary);
}

.nst-dialog button:not([disabled]):active {
    background: var(--nst-secondary);
}

.nst-dialog button[disabled] {
    opacity: 0.5;
    cursor: default;
}

.nst-dialog input {
    text-shadow: none;
    box-shadow: none;
    line-height: normal;
    border: 2px solid var(--nst-tertiary);
    color: var(--nst-text-secondary);
    border-radius: 16px;
    padding: 12px;
    background: var(--nst-quaternary);
    transition: 0.2s;
}

.nst-dialog input::-webkit-outer-spin-button,
.nst-dialog input::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

.nst-dialog input[disabled] {
    border: 2px solid var(--nst-tertiary);
    opacity: 0.5;
}

.nst-dialog input[disabled]:hover,
.nst-dialog input[disabled]:focus,
.nst-dialog input[disabled]:active {
    border: 2px solid transparent;
    border-radius: 16px;
    color: var(--nst-text-secondary);
    box-shadow: none;
    padding: 12px;
}

.nst-dialog input:hover {
    border: 2px solid transparent;
    color: var(--nst-text-secondary);
    box-shadow: none;
}

.nst-dialog input:focus,
.nst-dialog input:active {
    border: 2px solid transparent;
    color: var(--nst-text-secondary);
    background: var(--nst-tertiary);
    box-shadow: none;
}

.nst-password {
    padding-right: 48px;
}

.nst-password ~ .nst-password-eye {
    transition: 0.2s;
    position: absolute;
    display: flex;
    justify-content: center;
    align-items: center;
    right: 8px;
    top: 8px;
    width: 48px;
    height: 48px;
}

.nst-password ~ .nst-password-eye:hover {
    opacity: 0.75;
}

.nst-password ~ .nst-password-eye:active {
    opacity: 0.5;
}

.nst-password ~ .nst-password-eye::after {
    transition: 0.2s;
    content: "";
    position: absolute;
    width: 60%;
    height: 2px;
    background-color: currentColor;
    transform: rotate(45deg);
    border-radius: 2px;
}

.nst-password[type="password"] ~ .nst-password-eye::after {
    width: 0%;
}

.nst-password ~ .nst-password-eye .nst-password-eye-inner {
    width: 24px;
    height: 16px;
    border: 2px solid currentColor;
    border-radius: 50%;
    position: absolute;
}

.nst-password ~ .nst-password-eye .nst-password-eye-outer {
    width: 8px;
    height: 8px;
    border: 2px solid currentColor;
    border-radius: 50%;
    position: absolute;
}

.nst-dialog .nst-area {
    border-radius: 16px;
    background: var(--nst-quaternary);
    padding: 16px;
    width: 100%;
    box-sizing: border-box;
}

.nst-dialog .nst-switch {
    position: relative;
    display: inline-block;
    width: 3.5em;
    height: 2em;
    margin: 0;
}

.nst-dialog .nst-switch .nst-hide {
    opacity: 0;
    width: 0;
    height: 0;
}

.nst-dialog .nst-switch div {
    position: absolute;
    cursor: pointer;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: var(--nst-quaternary);
    border: 2px solid var(--nst-tertiary);
    border-radius: 24px;
    transition: .4s;
}

.nst-dialog .nst-switch div:before {
    position: absolute;
    content: "";
    height: 1.2em;
    width: 1.2em;
    left: calc(0.4em - 2px);
    top: calc(0.4em - 2px);
    background: var(--nst-tertiary);
    border-radius: 50%;
    transition: .4s;
}

.nst-dialog .nst-switch input:checked + div {
    background: var(--nst-primary);
    border: 2px solid transparent;
}

.nst-dialog .nst-switch input:checked + div:before {
    transform: translateX(1.4em);
    background: rgba(0, 0, 0, 0.5);
}

.nst-dialog table {
    margin-left: auto;
    margin-right: auto;
}

.nst-dialog td {
    padding: 8px;
    position: relative;
}

.nst-total-cell {
    right: 0;
    position: sticky !important;
    cursor: pointer;
    background: var(--nst-quinary);
    transition: 0.1s;
}

tr.nst-row-selected .nst-total-cell:last-child {
    background: var(--nst-senary);
}

.nst-dialog .nst-marks-table-wrapper tr {
    transition: 0.2s;
}

.nst-dialog .nst-marks-table-wrapper tr.nst-row-selected {
    background: var(--nst-senary);
}

.nst-dialog .nst-marks-table-wrapper {
    overflow-y: auto;
    border-radius: 24px;
}

.nst-marks-cell {
    display: flex;
}

.nst-controls {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-evenly;
    gap: 8px;
    padding-top: 8px;
}

.nst-controls button {
    flex-grow: 1;
}

.nst-dialog[open] {
    animation: nst-show-dialog 0.5s forwards;
}

.nst-dialog.nst-hide-dialog {
    animation: nst-hide-dialog 0.5s forwards;
}

@keyframes nst-show-dialog {
    from {
        opacity: 0;
        transform: scale(0.5);
    }

    to {
        opacity: 1;
        transform: scale(1);
    }
}

@keyframes nst-hide-dialog {
    to {
        opacity: 0;
        transform: scale(0.5);
    }
}

.nst-dialog::backdrop {
    background: rgba(0, 0, 0, 0.5);
    backdrop-filter: blur(5px);
    animation: none;
}

.nst-dialog[open]::backdrop {
    animation: nst-show-opacity 0.5s forwards;
}

.nst-dialog.nst-hide-dialog::backdrop {
    animation: nst-hide-opacity 0.5s forwards;
}

@keyframes nst-show-opacity {
    from {
        opacity: 0;
    }

    to {
        opacity: 1;
    }
}

@keyframes nst-hide-opacity {
    to {
        opacity: 0;
    }
}

.nst-label-title {
    font-size: 18px;
}

.nst-label-description {
    font-size: 12px;
    color: var(--nst-text-tertiary);
}

.nst-mark {
    min-width: 24px;
    display: flex;
    flex-flow: column wrap;
    align-items: stretch;
    cursor: pointer;
    animation: nst-show-opacity 0.5s forwards;
}

.nst-mark-selectors {
    display: flex;
    gap: 8px;
    padding-top: 8px;
    justify-content: space-between;
}

.nst-mark p {
    text-align: center;
    margin: 0px;
}

.nst-mark p:first-child {
    font-size: large;
}

.nst-mark p:nth-child(2) {
    color: gray;
    font-size: x-small;
}

.nst-mark-highlight {
    animation: nst-mark-highlight 3s forwards;
}

@keyframes nst-mark-highlight {
    0% {
        opacity: 1;
    } 16% {
        opacity: 0;
    } 32% {
        opacity: 1;
    } 48% {
        opacity: 0;
    } 64% {
        opacity: 1;
    } 80% {
        opacity: 0;
    } 96% {
        opacity: 1;
    }
}

.nst-mark-excellent {
    color: #96e400;
}

.nst-mark-good {
    color: #00c8ff;
}

.nst-mark-average {
    color: #f09600;
}

.nst-mark-bad {
    color: #ff3232;
}
`)