NetSchool Tweaks

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

当前为 2024-03-09 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name NetSchool Tweaks
  3. // @namespace https://greasyfork.org/users/843419
  4. // @description Дополнительные инструменты для NetSchool / NetCity (Сетевой Город. Образование)
  5. // @version 1.0.4
  6. // @author Zgoly
  7. // @match *://*/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=ir-tech.ru
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @grant GM_addStyle
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. try {
  16. console.log(language.Generic.Calendar.kTitle1, 'найден, NetSchool Tweaks активен')
  17. } catch {
  18. return
  19. }
  20.  
  21. let autoLogin = GM_getValue('autoLogin', false)
  22. let loginName = GM_getValue('loginName', 'Пользователь')
  23. let password = GM_getValue('password', '12345678')
  24. let schoolId = GM_getValue('schoolId', '')
  25. let autoSkip = GM_getValue('autoSkip', true)
  26.  
  27. function waitForElement(selector) {
  28. return new Promise(resolve => {
  29. if (document.querySelector(selector)) return resolve(document.querySelector(selector))
  30.  
  31. const observer = new MutationObserver(mutations => {
  32. if (document.querySelector(selector)) {
  33. observer.disconnect()
  34. resolve(document.querySelector(selector))
  35. }
  36. })
  37.  
  38. observer.observe(document.body, { childList: true, subtree: true })
  39. })
  40. }
  41.  
  42. if (autoLogin && window.location.pathname.startsWith('/authorize')) {
  43. function runAngularAction() {
  44. try {
  45. angular.element(document.body).scope().$$childTail.$ctrl.loginStrategiesService.loginWithLoginPassCheck(loginName, password, schoolId, null, { 'idpBindUser': 1 })
  46. } catch {
  47. requestAnimationFrame(runAngularAction)
  48. }
  49. }
  50.  
  51. runAngularAction()
  52. }
  53.  
  54. waitForElement('ns-modal').then((element) => {
  55. if (autoSkip && element.getAttribute('header') == language.Generic.Login.kTitleSecurityWarning) {
  56. element.querySelector('button').click()
  57. console.log('Security warning modal skipped')
  58. }
  59. })
  60.  
  61. function nstSwitch(parentElement) {
  62. let label = document.createElement('label')
  63. label.classList.add('nst-switch')
  64.  
  65. let input = document.createElement('input')
  66. input.type = 'checkbox'
  67. input.classList.add('nst-hide')
  68.  
  69. let div = document.createElement('div')
  70.  
  71. label.append(input)
  72. label.append(div)
  73.  
  74. parentElement.append(label)
  75.  
  76. return input
  77. }
  78.  
  79. function nstModal(headlineText, contentHTML, showSaveButton = true) {
  80. return new Promise((resolve, reject) => {
  81. let dialog = document.createElement('dialog')
  82. dialog.classList.add('nst-dialog')
  83.  
  84. let dialogWrapper = document.createElement('div')
  85. dialogWrapper.classList.add('nst-dialog-wrapper')
  86. dialog.append(dialogWrapper)
  87.  
  88. let headline = document.createElement('p')
  89. headline.classList.add('nst-headline')
  90. headline.textContent = headlineText
  91. dialogWrapper.append(headline)
  92.  
  93. let dialogAutofocus = document.createElement('input')
  94. dialogAutofocus.autofocus = 'autofocus'
  95. dialogAutofocus.style.display = 'none'
  96. dialogWrapper.append(dialogAutofocus)
  97.  
  98. let content = document.createElement('div')
  99. content.classList.add('nst-content')
  100. content.append(contentHTML)
  101. dialogWrapper.append(content)
  102.  
  103. let actions = document.createElement('div')
  104. actions.classList.add('nst-actions')
  105.  
  106. let closeButton = document.createElement('button')
  107. closeButton.classList.add('nst-close')
  108. closeButton.textContent = 'Закрыть'
  109. closeButton.addEventListener('click', () => closeDialog(false))
  110. actions.append(closeButton)
  111.  
  112. if (showSaveButton) {
  113. let saveButton = document.createElement('button')
  114. saveButton.classList.add('nst-save')
  115. saveButton.textContent = 'Сохранить'
  116. saveButton.addEventListener('click', () => closeDialog(true))
  117. actions.append(saveButton)
  118. }
  119.  
  120. dialogWrapper.append(actions)
  121.  
  122. document.body.append(dialog)
  123.  
  124. dialog.showModal()
  125. // Убираем фокус с поля ввода
  126. document.activeElement.blur()
  127.  
  128. document.body.classList.add('nst-no-scroll')
  129.  
  130. function closeDialog(result = false) {
  131. dialog.classList.add('nst-hide-dialog')
  132.  
  133. setTimeout(() => {
  134. dialog.remove()
  135. if (document.getElementsByTagName('dialog').length < 1) document.body.classList.remove('nst-no-scroll')
  136. }, 500)
  137.  
  138. resolve(result)
  139. }
  140.  
  141. contentHTML.closeDialog = closeDialog
  142.  
  143. dialog.addEventListener('click', (event) => {
  144. if (event.target === dialog) {
  145. closeDialog(false)
  146. }
  147. })
  148.  
  149. dialog.addEventListener('close', () => closeDialog())
  150. dialog.addEventListener('error', reject)
  151. })
  152. }
  153.  
  154. let settings = document.createElement('li')
  155. let settingsLink = document.createElement('a')
  156. settings.append(settingsLink)
  157.  
  158. let settingsBody = document.createElement('span')
  159. settingsBody.classList.add('cb-settings')
  160. settingsLink.append(settingsBody)
  161.  
  162. let settingsIcon = document.createElement('i')
  163. settingsIcon.classList.add('icon-gear', 'nst-settings-icon')
  164. settingsBody.append(settingsIcon)
  165.  
  166. settingsBody.addEventListener('click', () => {
  167. let div = document.createElement('div')
  168. let table = document.createElement('table')
  169.  
  170. // Переключатель авто входа
  171. let autoLoginRow = document.createElement('tr')
  172.  
  173. let autoLoginLabelCell = document.createElement('td')
  174.  
  175. let autoLoginLabelTitle = document.createElement('div')
  176. autoLoginLabelTitle.classList.add('nst-label-title')
  177. autoLoginLabelTitle.textContent = 'Авто вход'
  178. autoLoginLabelCell.append(autoLoginLabelTitle)
  179.  
  180. let autoLoginLabelDescription = document.createElement('div')
  181. autoLoginLabelDescription.classList.add('nst-label-description')
  182. autoLoginLabelDescription.textContent = 'Авто вход по логину и паролю.'
  183. autoLoginLabelCell.append(autoLoginLabelDescription)
  184.  
  185. let autoLoginInputCell = document.createElement('td')
  186. let autoLoginInput = nstSwitch(autoLoginInputCell)
  187. autoLoginInput.checked = autoLogin
  188.  
  189. autoLoginRow.append(autoLoginLabelCell)
  190. autoLoginRow.append(autoLoginInputCell)
  191.  
  192. table.append(autoLoginRow)
  193.  
  194. // Поле логина
  195. let loginNameRow = document.createElement('tr')
  196.  
  197. let loginNameLabelCell = document.createElement('td')
  198.  
  199. let loginNameLabelTitle = document.createElement('div')
  200. loginNameLabelTitle.classList.add('nst-label-title')
  201. loginNameLabelTitle.textContent = 'Логин'
  202. loginNameLabelCell.append(loginNameLabelTitle)
  203.  
  204. let loginNameLabelDescription = document.createElement('div')
  205. loginNameLabelDescription.classList.add('nst-label-description')
  206. loginNameLabelDescription.textContent = 'Логин для входа.'
  207. loginNameLabelCell.append(loginNameLabelDescription)
  208.  
  209. let loginNameInputCell = document.createElement('td')
  210. loginNameInputCell.classList.add('nst-flex')
  211. let loginNameInput = document.createElement('input')
  212. loginNameInput.type = 'text'
  213. loginNameInput.value = loginName
  214.  
  215. loginNameInputCell.append(loginNameInput)
  216.  
  217. loginNameRow.append(loginNameLabelCell)
  218. loginNameRow.append(loginNameInputCell)
  219.  
  220. table.append(loginNameRow)
  221.  
  222. // Поле пароля
  223. let passwordRow = document.createElement('tr')
  224.  
  225. let passwordLabelCell = document.createElement('td')
  226.  
  227. let passwordLabelTitle = document.createElement('div')
  228. passwordLabelTitle.classList.add('nst-label-title')
  229. passwordLabelTitle.textContent = 'Пароль'
  230. passwordLabelCell.append(passwordLabelTitle)
  231.  
  232. let passwordLabelDescription = document.createElement('div')
  233. passwordLabelDescription.classList.add('nst-label-description')
  234. passwordLabelDescription.textContent = 'Пароль для входа.'
  235. passwordLabelCell.append(passwordLabelDescription)
  236.  
  237. let passwordInputCell = document.createElement('td')
  238. passwordInputCell.classList.add('nst-password-input-cell', 'nst-flex')
  239. let passwordInput = document.createElement('input')
  240. passwordInput.type = 'password'
  241. passwordInput.classList.add('nst-password')
  242. passwordInput.value = password
  243.  
  244. let passwordEye = document.createElement('div')
  245. passwordEye.classList.add('nst-password-eye')
  246. passwordEye.addEventListener('click', () => {
  247. passwordInput.type = passwordInput.type === 'password' ? 'text' : 'password'
  248. })
  249.  
  250. let passwordEyeInner = document.createElement('div')
  251. passwordEyeInner.classList.add('nst-password-eye-inner')
  252. let passwordEyeOuter = document.createElement('div')
  253. passwordEyeOuter.classList.add('nst-password-eye-outer')
  254.  
  255. passwordEye.append(passwordEyeInner)
  256. passwordEye.append(passwordEyeOuter)
  257.  
  258. passwordInputCell.append(passwordInput)
  259. passwordInputCell.append(passwordEye)
  260.  
  261. passwordRow.append(passwordLabelCell)
  262. passwordRow.append(passwordInputCell)
  263.  
  264. table.append(passwordRow)
  265.  
  266. // Поле ID школы
  267. let schoolIdRow = document.createElement('tr')
  268.  
  269. let schoolIdLabelCell = document.createElement('td')
  270.  
  271. let schoolIdLabelTitle = document.createElement('div')
  272. schoolIdLabelTitle.classList.add('nst-label-title')
  273. schoolIdLabelTitle.textContent = 'ID школы'
  274. schoolIdLabelCell.append(schoolIdLabelTitle)
  275.  
  276. let schoolIdLabelDescription = document.createElement('div')
  277. schoolIdLabelDescription.classList.add('nst-label-description')
  278. schoolIdLabelDescription.textContent = 'ID школы для входа. Оставьте пустым, если не знаете.'
  279. schoolIdLabelCell.append(schoolIdLabelDescription)
  280.  
  281. let schoolIdInputCell = document.createElement('td')
  282. schoolIdInputCell.classList.add('nst-flex')
  283. let schoolIdInput = document.createElement('input')
  284. schoolIdInput.type = 'text'
  285. schoolIdInput.value = schoolId
  286. schoolIdInput.placeholder = schoolId
  287.  
  288. schoolIdInputCell.append(schoolIdInput)
  289.  
  290. schoolIdRow.append(schoolIdLabelCell)
  291. schoolIdRow.append(schoolIdInputCell)
  292.  
  293. table.append(schoolIdRow)
  294.  
  295. // Переключатель авто пропуска
  296. let autoSkipRow = document.createElement('tr')
  297.  
  298. let autoSkipLabelCell = document.createElement('td')
  299.  
  300. let autoSkipLabelTitle = document.createElement('div')
  301. autoSkipLabelTitle.classList.add('nst-label-title')
  302. autoSkipLabelTitle.textContent = 'Авто пропуск'
  303. autoSkipLabelCell.append(autoSkipLabelTitle)
  304.  
  305. let autoSkipLabelDescription = document.createElement('div')
  306. autoSkipLabelDescription.classList.add('nst-label-description')
  307. autoSkipLabelDescription.textContent = 'Авто пропуск навязчивых уведомлений.'
  308. autoSkipLabelCell.append(autoSkipLabelDescription)
  309.  
  310. let autoSkipInputCell = document.createElement('td')
  311. let autoSkipInput = nstSwitch(autoSkipInputCell)
  312. autoSkipInput.checked = autoSkip
  313.  
  314. autoSkipRow.append(autoSkipLabelCell)
  315. autoSkipRow.append(autoSkipInputCell)
  316.  
  317. table.append(autoSkipRow)
  318.  
  319. function toggleFields() {
  320. let fields = [loginNameInput, passwordInput, schoolIdInput]
  321. fields.forEach(field => {
  322. field.disabled = !autoLoginInput.checked
  323. })
  324. }
  325. toggleFields()
  326. autoLoginInput.addEventListener('change', toggleFields)
  327.  
  328. div.append(table)
  329.  
  330. // Сохранение настроек
  331. nstModal('Настройки', div).then(save => {
  332. if (save) {
  333. GM_setValue('autoLogin', autoLoginInput.checked)
  334. autoLogin = autoLoginInput.checked
  335. GM_setValue('loginName', loginNameInput.value)
  336. loginName = loginNameInput.value
  337. GM_setValue('password', passwordInput.value)
  338. password = passwordInput.value
  339. GM_setValue('schoolId', schoolIdInput.value == '' ? appContext.schoolId : schoolIdInput.value)
  340. schoolId = schoolIdInput.value == '' ? appContext.schoolId : schoolIdInput.value
  341. GM_setValue('autoSkip', autoSkipInput.checked)
  342. autoSkip = autoSkipInput.checked
  343. }
  344. })
  345.  
  346. let previewMarksWrapper = document.createElement('div')
  347. previewMarksWrapper.classList.add('preview-marks-wrapper')
  348. div.append(previewMarksWrapper)
  349.  
  350. let at = appContext.at
  351. let weekStart = appContext.weekStart
  352. let weekEnd = appContext.weekEnd
  353.  
  354. // Начало учебного года
  355. let startDateInput = document.createElement('input')
  356. startDateInput.type = 'date'
  357. startDateInput.value = weekStart
  358. previewMarksWrapper.append(startDateInput)
  359.  
  360. // Конец учебного года
  361. let endDateInput = document.createElement('input')
  362. endDateInput.type = 'date'
  363. endDateInput.value = weekEnd
  364. previewMarksWrapper.append(endDateInput)
  365.  
  366. if (weekStart == undefined || weekEnd == undefined) {
  367. fetch('/webapi/v2/reports/studenttotal', { 'headers': { 'at': at } }).then((response) => {
  368. return response.json()
  369. }).then((data) => {
  370. weekStart = data.filterSources[3].defaultRange.start.substring(0, 10)
  371. startDateInput.value = weekStart
  372. weekEnd = data.filterSources[3].defaultRange.end.substring(0, 10)
  373. endDateInput.value = weekEnd
  374. })
  375. }
  376.  
  377. // Кнопка предпросмотра оценок
  378. let previewMarksButton = document.createElement('button')
  379. previewMarksButton.innerText = 'Предпросмотр оценок'
  380. previewMarksButton.addEventListener('click', () => {
  381. let marksTableWrapper = document.createElement('div')
  382. marksTableWrapper.classList.add('nst-marks-table-wrapper')
  383.  
  384. let contentDiv = document.createElement('div')
  385. contentDiv.classList.add('nst-content')
  386. contentDiv.append(marksTableWrapper)
  387.  
  388. fetch('/webapi/student/diary/init', { 'headers': { 'at': at } }).then((response) => {
  389. return response.json()
  390. }).then((data) => {
  391. let studentId = data.students[0].studentId
  392. let yearId = appContext.yearId
  393. let startDate = startDateInput.value
  394. let endDate = endDateInput.value
  395. // Запрос дневика
  396. fetch(`/webapi/student/diary?studentId=${studentId}&weekStart=${startDate}&weekEnd=${endDate}&withLaAssigns=true&yearId=${yearId}`, { 'headers': { 'at': at } }).then((response) => {
  397. return response.json()
  398. }).then((data) => {
  399. // Повторный запрос дневника (с текущей датой для отображения правильной недели, игнорируется)
  400. let currentDate = date2strf(new Date(), 'yyyy\x01mm\x01dd\x01.')
  401. fetch(`/webapi/student/diary?studentId=${studentId}&weekStart=${currentDate}&weekEnd=${currentDate}&withLaAssigns=true&yearId=${yearId}`, { 'headers': { 'at': at } })
  402.  
  403. let marksTable = document.createElement('table')
  404. marksTableWrapper.append(marksTable)
  405.  
  406. let tableControlsDiv = document.createElement('div')
  407. tableControlsDiv.classList.add('nst-controls')
  408. contentDiv.append(tableControlsDiv)
  409.  
  410. let selectAllButton = document.createElement('button')
  411. selectAllButton.innerText = 'Выбрать все'
  412. selectAllButton.addEventListener('click', () => {
  413. for (let row of marksTable.rows) {
  414. row.classList.add('nst-row-selected')
  415. }
  416. updateButtons()
  417. })
  418. tableControlsDiv.append(selectAllButton)
  419.  
  420. let deselectAllButton = document.createElement('button')
  421. deselectAllButton.innerText = 'Отменить выбор'
  422. deselectAllButton.addEventListener('click', () => {
  423. for (let row of marksTable.rows) {
  424. row.classList.remove('nst-row-selected')
  425. }
  426. updateButtons()
  427. })
  428. tableControlsDiv.append(deselectAllButton)
  429.  
  430. let addRowButton = document.createElement('button')
  431. addRowButton.innerText = 'Cоздать'
  432. addRowButton.addEventListener('click', () => {
  433. let selectedRows = marksTable.querySelectorAll('.nst-row-selected')
  434. let newRow = createRow()
  435. if (selectedRows.length > 0) {
  436. selectedRows[selectedRows.length - 1].after(newRow)
  437. selectedRows.forEach(row => row.classList.remove('nst-row-selected'))
  438. } else {
  439. marksTable.prepend(newRow)
  440. }
  441. cookRow(newRow)
  442.  
  443. newRow.classList.add('nst-row-selected')
  444. newRow.scrollIntoView({behavior: "smooth"})
  445. updateButtons()
  446. })
  447. tableControlsDiv.append(addRowButton)
  448.  
  449. let cloneRowButton = document.createElement('button')
  450. cloneRowButton.innerText = 'Клонировать'
  451. cloneRowButton.addEventListener('click', () => {
  452. let selectedRows = marksTable.querySelectorAll('.nst-row-selected')
  453. selectedRows.forEach(row => {
  454. let clonedRow = row.cloneNode(true)
  455. row.after(clonedRow)
  456. cookRow(clonedRow)
  457.  
  458. let marksCell = clonedRow.querySelector('.nst-marks-cell')
  459. Array.from(marksCell.children).forEach(markDiv => {
  460. cookMark(markDiv)
  461. })
  462.  
  463. row.classList.remove('nst-row-selected')
  464. })
  465. updateButtons()
  466. })
  467. tableControlsDiv.append(cloneRowButton)
  468.  
  469. let removeRowButton = document.createElement('button')
  470. removeRowButton.innerText = 'Удалить'
  471. removeRowButton.addEventListener('click', () => {
  472. let selectedRows = marksTable.querySelectorAll('.nst-row-selected')
  473. selectedRows.forEach(row => row.remove())
  474. updateButtons()
  475. })
  476. tableControlsDiv.append(removeRowButton)
  477.  
  478. let addMarkButton = document.createElement('button')
  479. addMarkButton.innerText = 'Добавить оценку'
  480. addMarkButton.addEventListener('click', () => {
  481. let selectedRows = marksTable.querySelectorAll('.nst-row-selected')
  482. if (selectedRows.length == 0) return
  483.  
  484. let [templateTable, markInput, weightInput] = markModalTemplate(5, 20)
  485.  
  486. nstModal('Добавление оценки', templateTable).then(save => {
  487. if (save) {
  488. selectedRows.forEach(row => {
  489. let mark = createMark(markInput.value, weightInput.value)
  490. row.querySelector('.nst-marks-cell').append(mark)
  491. cookMark(mark)
  492. highlightMark(mark)
  493. row.classList.remove('nst-row-selected')
  494. })
  495.  
  496. updateButtons()
  497. }
  498. })
  499. })
  500. tableControlsDiv.append(addMarkButton)
  501.  
  502. function createMark(mark, weight, fullAssignment = null) {
  503. let markDiv = document.createElement('div')
  504. if (fullAssignment) markDiv.dataset.assignment = JSON.stringify(fullAssignment)
  505. markDiv.classList.add('nst-mark')
  506.  
  507. let markValue = document.createElement('p')
  508. markValue.innerText = mark
  509. markValue.classList.add('nst-mark-value')
  510. markDiv.append(markValue)
  511.  
  512. let weightValue = document.createElement('p')
  513. weightValue.innerText = weight
  514. weightValue.classList.add('nst-weight-value')
  515. markDiv.append(weightValue)
  516.  
  517. return markDiv
  518. }
  519.  
  520. function createRow(name = '') {
  521. let row = document.createElement('tr')
  522.  
  523. let nameCell = document.createElement('td')
  524. let nameInput = document.createElement('input')
  525. nameInput.value = name
  526. nameInput.classList.add('nst-name-input')
  527. nameInput.placeholder = "Имя предмета"
  528. nameCell.append(nameInput)
  529. row.append(nameCell)
  530.  
  531. let marksCell = document.createElement('td')
  532. marksCell.classList.add('nst-marks-cell')
  533. row.append(marksCell)
  534.  
  535. let totalCell = document.createElement('td')
  536. totalCell.classList.add('nst-total-cell')
  537. row.append(totalCell)
  538.  
  539. return row
  540. }
  541.  
  542. function markModalTemplate(mark, weight) {
  543. let templateTable = document.createElement('table')
  544.  
  545. let markRow = document.createElement('tr')
  546. templateTable.append(markRow)
  547.  
  548. let markLabelCell = document.createElement('td')
  549. markRow.append(markLabelCell)
  550.  
  551. let markLabelTitle = document.createElement('div')
  552. markLabelTitle.classList.add('nst-label-title')
  553. markLabelTitle.textContent = 'Оценка'
  554. markLabelCell.append(markLabelTitle)
  555.  
  556. let markLabelDescription = document.createElement('div')
  557. markLabelDescription.classList.add('nst-label-description')
  558. markLabelDescription.textContent = 'Оценка.'
  559. markLabelCell.append(markLabelDescription)
  560.  
  561. let markInputCell = document.createElement('td')
  562. markInputCell.classList.add('nst-flex')
  563. markRow.append(markInputCell)
  564.  
  565. let markInput = document.createElement('input')
  566. markInput.readOnly = true
  567. markInput.value = mark
  568. markInputCell.append(markInput)
  569.  
  570. let markSelectorsDiv = document.createElement('div')
  571. markSelectorsDiv.classList.add('nst-mark-selectors')
  572. markInputCell.append(markSelectorsDiv)
  573.  
  574. let marks = ['5', '4', '3', '2', '•']
  575.  
  576. marks.forEach(mark => {
  577. let markButton = document.createElement('button')
  578.  
  579. markButton.innerText = mark
  580. markButton.addEventListener('click', () => markInput.value = mark)
  581. markSelectorsDiv.append(markButton)
  582. })
  583.  
  584. let weightRow = document.createElement('tr')
  585. templateTable.append(weightRow)
  586.  
  587. let weightLabelCell = document.createElement('td')
  588. weightRow.append(weightLabelCell)
  589.  
  590. let weightLabelTitle = document.createElement('div')
  591. weightLabelTitle.classList.add('nst-label-title')
  592. weightLabelTitle.textContent = 'Вес'
  593. weightLabelCell.append(weightLabelTitle)
  594.  
  595. let weightLabelDescription = document.createElement('div')
  596. weightLabelDescription.classList.add('nst-label-description')
  597. weightLabelDescription.textContent = 'Вес оценки.'
  598. weightLabelCell.append(weightLabelDescription)
  599.  
  600. let weightInputCell = document.createElement('td')
  601. weightInputCell.classList.add('nst-flex')
  602. weightRow.append(weightInputCell)
  603.  
  604. let weightInput = document.createElement('input')
  605. weightInput.type = 'number'
  606. weightInput.value = weight
  607. weightInputCell.append(weightInput)
  608.  
  609. return [templateTable, markInput, weightInput]
  610. }
  611.  
  612. function highlightMark(mark) {
  613. mark.classList.remove('nst-mark-highlight');
  614. mark.offsetWidth;
  615. mark.classList.add('nst-mark-highlight');
  616. }
  617.  
  618. function cookMark(mark) {
  619. let markValue = mark.querySelector('.nst-mark-value')
  620. let weightValue = mark.querySelector('.nst-weight-value')
  621.  
  622. mark.addEventListener('click', () => {
  623. let modalDiv = document.createElement('div')
  624. let [templateTable, markInput, weightInput] = markModalTemplate(markValue.innerText, weightValue.innerText)
  625. modalDiv.append(templateTable)
  626.  
  627. let controlsDiv = document.createElement('div')
  628. controlsDiv.classList.add('nst-controls')
  629. modalDiv.append(controlsDiv)
  630.  
  631. let cloneMarkButton = document.createElement('button')
  632. cloneMarkButton.innerText = 'Клонировать'
  633. controlsDiv.append(cloneMarkButton)
  634. cloneMarkButton.addEventListener('click', () => {
  635. modalDiv.closeDialog(true)
  636. let newMark = mark.cloneNode(true)
  637. mark.after(newMark)
  638. cookMark(newMark)
  639. highlightMark(newMark)
  640. })
  641.  
  642. let deleteMarkButton = document.createElement('button')
  643. deleteMarkButton.innerText = 'Удалить'
  644. controlsDiv.append(deleteMarkButton)
  645. deleteMarkButton.addEventListener('click', () => {
  646. mark.remove()
  647. modalDiv.closeDialog(false)
  648. })
  649.  
  650. if (mark.dataset.assignment) {
  651. let assignment = JSON.parse(mark.dataset.assignment)
  652.  
  653. let restoreMarkButton = document.createElement('button')
  654. restoreMarkButton.innerText = 'Восстановить'
  655. controlsDiv.append(restoreMarkButton)
  656. restoreMarkButton.addEventListener('click', () => {
  657. markInput.value = assignment.mark
  658. weightInput.value = assignment.weight
  659. })
  660.  
  661. let assignmentMarkButton = document.createElement('button')
  662. assignmentMarkButton.innerText = 'Подробности'
  663. controlsDiv.append(assignmentMarkButton)
  664. assignmentMarkButton.addEventListener('click', () => {
  665. // TODO Доделать
  666. let assignmentTable = document.createElement('table')
  667.  
  668. let translations = {
  669. 'id': 'ID задания',
  670. 'assignmentName': 'Тема задания',
  671. 'activityName': 'Имя деятельности',
  672. 'problemName': 'Название задачи',
  673. 'studentId': 'ID ученика',
  674. 'subjectGroup.id': 'ID предмета',
  675. 'subjectGroup.name': 'Название предмета',
  676. 'teachers.0.id': 'ID учителя',
  677. 'teachers.0.name': 'Имя учителя',
  678. 'productId': 'ID продукта',
  679. 'isDeleted': 'Удалено',
  680. 'weight': 'Вес',
  681. 'date': 'Дата',
  682. 'description': 'Описание',
  683. 'mark': 'Оценка',
  684. 'typeId': 'ID типа задания',
  685. 'type': 'Тип задания'
  686. }
  687.  
  688. for (let key in assignment) {
  689. let translation = translations[key] || key
  690. let value = assignment[key]
  691. value = value === true ? "Да" : value === false ? "Нет" : value
  692.  
  693. let assignmentRow = document.createElement('tr')
  694. assignmentTable.append(assignmentRow)
  695.  
  696. let assignmentLabelCell = document.createElement('td')
  697. assignmentLabelCell.innerText = translation
  698. assignmentRow.append(assignmentLabelCell)
  699.  
  700. let assignmentInputCell = document.createElement('td')
  701. assignmentInputCell.classList.add('nst-flex')
  702. assignmentRow.append(assignmentInputCell)
  703.  
  704.  
  705. let assignmentInput
  706.  
  707. console.log(key, value, typeof value)
  708. if (key === 'date') {
  709. assignmentInput = document.createElement('input')
  710. assignmentInput.readOnly = true
  711. assignmentInput.type = 'date'
  712. assignmentInput.value = value
  713. } else if (typeof value === 'number') {
  714. assignmentInput = document.createElement('input')
  715. assignmentInput.readOnly = true
  716. assignmentInput.type = 'number'
  717. assignmentInput.value = value
  718. } else {
  719. assignmentInput = document.createElement('div')
  720. assignmentInput.innerText = value
  721. assignmentInput.classList.add('nst-area')
  722. }
  723.  
  724. assignmentInputCell.append(assignmentInput)
  725. }
  726.  
  727. nstModal('Подробности задания', assignmentTable, false)
  728. })
  729. }
  730.  
  731. nstModal('Редактирование оценки', modalDiv).then(save => {
  732. if (save) {
  733. markValue.innerText = markInput.value
  734. weightValue.innerText = weightInput.value
  735. highlightMark(mark)
  736. }
  737. })
  738. })
  739. }
  740. function updateButtons() {
  741. if (marksTable.querySelectorAll('.nst-row-selected').length > 0) {
  742. addMarkButton.disabled = false
  743. cloneRowButton.disabled = false
  744. removeRowButton.disabled = false
  745. } else {
  746. addMarkButton.disabled = true
  747. cloneRowButton.disabled = true
  748. removeRowButton.disabled = true
  749. }
  750. }
  751.  
  752. updateButtons()
  753.  
  754. function cookRow(row) {
  755. let marksCell = row.querySelector('.nst-marks-cell')
  756. let totalCell = row.querySelector('.nst-total-cell')
  757.  
  758. // Изменение цвета оценки / балла
  759. function calculateTotalScore() {
  760. let markSum = 0
  761. let weightSum = 0
  762.  
  763. Array.from(marksCell.children).forEach(markDiv => {
  764. let markValue = markDiv.querySelector('.nst-mark-value')
  765. let weightValue = markDiv.querySelector('.nst-weight-value')
  766. let mark = markValue.innerText.replaceAll('•', '2')
  767. let weight = Number(weightValue.innerText)
  768.  
  769. markValue.classList.remove(...['nst-mark-excellent', 'nst-mark-good', 'nst-mark-average', 'nst-mark-bad'])
  770. markValue.classList.add(getMarkClass(mark))
  771.  
  772. markSum += mark * weight
  773. weightSum += weight
  774. })
  775.  
  776. totalCell.innerText = weightSum ? Number((markSum / weightSum).toFixed(2)) : 0
  777. totalCell.classList.remove(...['nst-mark-excellent', 'nst-mark-good', 'nst-mark-average', 'nst-mark-bad'])
  778. totalCell.classList.add(getMarkClass(totalCell.innerText))
  779. }
  780.  
  781. function getMarkClass(mark) {
  782. 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'
  783. }
  784.  
  785. let observer = new MutationObserver(() => {
  786. calculateTotalScore()
  787. })
  788. observer.observe(marksCell, { childList: true, subtree: true })
  789.  
  790. calculateTotalScore()
  791.  
  792. // Выделение строки при нажатии на балл
  793. totalCell.addEventListener('click', () => {
  794. row.classList.toggle('nst-row-selected')
  795. updateButtons()
  796. })
  797. }
  798.  
  799. function flattenJson(json) {
  800. let result = {}
  801.  
  802. function flatten(obj, prefix = '') {
  803. for (let key in obj) {
  804. if (typeof obj[key] === 'object' && obj[key] !== null) {
  805. flatten(obj[key], prefix + key + '.')
  806. } else {
  807. result[prefix + key] = obj[key]
  808. }
  809. }
  810. }
  811.  
  812. flatten(json)
  813. return result
  814. }
  815.  
  816. fetch('/webapi/grade/assignment/types').then((response) => {
  817. return response.json()
  818. }).then((types) => {
  819. for (let day of data.weekDays) {
  820. for (let lesson of day.lessons) {
  821. if (Array.isArray(lesson.assignments)) {
  822. for (let assignment of lesson.assignments) {
  823. if (assignment.mark) {
  824. fetch(`/webapi/student/diary/assigns/${assignment.id}`, { 'headers': { 'at': at } }).then((response) => {
  825. return response.json()
  826. }).then((fullAssignment) => {
  827. // Модификация данных в удобный формат
  828. fullAssignment.mark = assignment.mark.mark
  829. fullAssignment.studentId = assignment.mark.studentId
  830. fullAssignment.typeId = assignment.typeId
  831. fullAssignment.date = fullAssignment.date.substring(0, 10)
  832.  
  833. let item = types.find(data => data.id == fullAssignment.typeId)
  834. fullAssignment.type = item.name
  835.  
  836. fullAssignment = flattenJson(fullAssignment)
  837.  
  838. for (let key in fullAssignment) {
  839. if (fullAssignment[key] == null) delete fullAssignment[key]
  840. }
  841.  
  842. // Объявление / создание ряда
  843. let row = Array.from(marksTable.rows).find(r => r.querySelector('.nst-name-input').value == lesson.subjectName)
  844. if (!row) {
  845. row = createRow(lesson.subjectName)
  846. marksTable.append(row)
  847. cookRow(row)
  848. }
  849.  
  850. // Добавление оценки
  851. let createdMark = createMark(fullAssignment.mark, fullAssignment.weight, fullAssignment)
  852. row.querySelector('.nst-marks-cell').append(createdMark)
  853. cookMark(createdMark)
  854. })
  855. }
  856. }
  857. }
  858. }
  859. }
  860. }).catch(err => {
  861. console.error('Произошла ошибка: ', err)
  862. })
  863. })
  864. })
  865.  
  866. nstModal('Предпросмотр оценок', contentDiv, false)
  867. })
  868. previewMarksWrapper.append(previewMarksButton)
  869. })
  870.  
  871. waitForElement('.top-right-menu').then((elm) => {
  872. elm.prepend(settings)
  873. })
  874.  
  875. GM_addStyle(`
  876. @import url('https://fonts.googleapis.com/css2?family=Nunito&display=swap');
  877.  
  878. :root {
  879. --nst-primary: #64a0c8;
  880. --nst-secondary: #415f78;
  881. --nst-tertiary: #374b5f;
  882. --nst-quaternary: #232a32;
  883. --nst-quinary: #1a1c1e;
  884. --nst-senary: #141618;
  885.  
  886. --nst-text-primary: #c8e6ff;
  887. --nst-text-secondary: #aadcff;
  888. --nst-text-tertiary: #87afc8;
  889. }
  890.  
  891. .nst-no-scroll {
  892. touch-action: none;
  893. overflow: hidden;
  894. }
  895.  
  896. .nst-flex {
  897. display: flex;
  898. flex-direction: column;
  899. }
  900.  
  901. .nst-flex input {
  902. flex: 1;
  903. }
  904.  
  905. .nst-settings-icon {
  906. display: flex !important;
  907. justify-content: center;
  908. color: white;
  909. scale: 0.75;
  910. }
  911.  
  912. .nst-dialog {
  913. border: none;
  914. outline: none;
  915. background: var(--nst-quinary);
  916. border-radius: 32px;
  917. box-shadow: rgba(0, 0, 0, 0.25) 0 0 25px;
  918. padding: 0;
  919. }
  920.  
  921. .nst-dialog-wrapper {
  922. display: flex;
  923. flex-direction: column;
  924. padding: 24px;
  925. max-height: calc(100vh - 48px);
  926. max-width: calc(100vw - 48px);
  927. }
  928.  
  929. .nst-content {
  930. flex: 1;
  931. display: flex;
  932. flex-direction: column;
  933. overflow: auto;
  934. }
  935.  
  936. .nst-dialog .preview-marks-wrapper {
  937. display: flex;
  938. flex-wrap: wrap;
  939. justify-content: center;
  940. padding-top: 16px;
  941. gap: 8px;
  942. }
  943.  
  944. .nst-dialog * {
  945. font-family: 'Nunito';
  946. color: var(--nst-text-secondary);
  947. }
  948.  
  949. .nst-dialog .nst-headline:first-child {
  950. margin-top: 0px;
  951. }
  952.  
  953. .nst-dialog .nst-headline {
  954. color: var(--nst-text-primary);
  955. font-size: 1.5em;
  956. margin: 0px;
  957. margin-bottom: 16px;
  958. }
  959.  
  960. .nst-dialog .nst-actions {
  961. margin-top: 16px;
  962. display: flex;
  963. gap: 8px;
  964. justify-content: flex-end;
  965. }
  966.  
  967. .nst-dialog button {
  968. cursor: pointer;
  969. color: var(--button-color);
  970. border-radius: 28px;
  971. padding: 12px;
  972. margin: 0;
  973. border: none;
  974. outline: none;
  975. transition: 0.2s;
  976. background: var(--nst-quaternary);
  977. }
  978.  
  979. .nst-dialog button:not([disabled]):hover {
  980. background: var(--nst-tertiary);
  981. }
  982.  
  983. .nst-dialog button:not([disabled]):active {
  984. background: var(--nst-secondary);
  985. }
  986.  
  987. .nst-dialog button[disabled] {
  988. opacity: 0.5;
  989. cursor: default;
  990. }
  991.  
  992. .nst-dialog input {
  993. text-shadow: none;
  994. box-shadow: none;
  995. line-height: normal;
  996. border: 2px solid var(--nst-tertiary);
  997. color: var(--nst-text-secondary);
  998. border-radius: 16px;
  999. padding: 12px;
  1000. background: var(--nst-quaternary);
  1001. transition: 0.2s;
  1002. }
  1003.  
  1004. .nst-dialog input::-webkit-outer-spin-button,
  1005. .nst-dialog input::-webkit-inner-spin-button {
  1006. -webkit-appearance: none;
  1007. margin: 0;
  1008. }
  1009.  
  1010. .nst-dialog input[disabled] {
  1011. border: 2px solid var(--nst-tertiary);
  1012. opacity: 0.5;
  1013. }
  1014.  
  1015. .nst-dialog input[disabled]:hover,
  1016. .nst-dialog input[disabled]:focus,
  1017. .nst-dialog input[disabled]:active {
  1018. border: 2px solid transparent;
  1019. border-radius: 16px;
  1020. color: var(--nst-text-secondary);
  1021. box-shadow: none;
  1022. padding: 12px;
  1023. }
  1024.  
  1025. .nst-dialog input:hover {
  1026. border: 2px solid transparent;
  1027. color: var(--nst-text-secondary);
  1028. box-shadow: none;
  1029. }
  1030.  
  1031. .nst-dialog input:focus,
  1032. .nst-dialog input:active {
  1033. border: 2px solid transparent;
  1034. color: var(--nst-text-secondary);
  1035. background: var(--nst-tertiary);
  1036. box-shadow: none;
  1037. }
  1038.  
  1039. .nst-password {
  1040. padding-right: 48px;
  1041. }
  1042.  
  1043. .nst-password ~ .nst-password-eye {
  1044. transition: 0.2s;
  1045. position: absolute;
  1046. display: flex;
  1047. justify-content: center;
  1048. align-items: center;
  1049. right: 8px;
  1050. top: 8px;
  1051. width: 48px;
  1052. height: 48px;
  1053. }
  1054.  
  1055. .nst-password ~ .nst-password-eye:hover {
  1056. opacity: 0.75;
  1057. }
  1058.  
  1059. .nst-password ~ .nst-password-eye:active {
  1060. opacity: 0.5;
  1061. }
  1062.  
  1063. .nst-password ~ .nst-password-eye::after {
  1064. transition: 0.2s;
  1065. content: "";
  1066. position: absolute;
  1067. width: 60%;
  1068. height: 2px;
  1069. background-color: currentColor;
  1070. transform: rotate(45deg);
  1071. border-radius: 2px;
  1072. }
  1073.  
  1074. .nst-password[type="password"] ~ .nst-password-eye::after {
  1075. width: 0%;
  1076. }
  1077.  
  1078. .nst-password ~ .nst-password-eye .nst-password-eye-inner {
  1079. width: 24px;
  1080. height: 16px;
  1081. border: 2px solid currentColor;
  1082. border-radius: 50%;
  1083. position: absolute;
  1084. }
  1085.  
  1086. .nst-password ~ .nst-password-eye .nst-password-eye-outer {
  1087. width: 8px;
  1088. height: 8px;
  1089. border: 2px solid currentColor;
  1090. border-radius: 50%;
  1091. position: absolute;
  1092. }
  1093.  
  1094. .nst-dialog .nst-area {
  1095. border-radius: 16px;
  1096. background: var(--nst-quaternary);
  1097. padding: 16px;
  1098. width: 100%;
  1099. box-sizing: border-box;
  1100. }
  1101.  
  1102. .nst-dialog .nst-switch {
  1103. position: relative;
  1104. display: inline-block;
  1105. width: 3.5em;
  1106. height: 2em;
  1107. margin: 0;
  1108. }
  1109.  
  1110. .nst-dialog .nst-switch .nst-hide {
  1111. opacity: 0;
  1112. width: 0;
  1113. height: 0;
  1114. }
  1115.  
  1116. .nst-dialog .nst-switch div {
  1117. position: absolute;
  1118. cursor: pointer;
  1119. top: 0;
  1120. left: 0;
  1121. right: 0;
  1122. bottom: 0;
  1123. background: var(--nst-quaternary);
  1124. border: 2px solid var(--nst-tertiary);
  1125. border-radius: 24px;
  1126. transition: .4s;
  1127. }
  1128.  
  1129. .nst-dialog .nst-switch div:before {
  1130. position: absolute;
  1131. content: "";
  1132. height: 1.2em;
  1133. width: 1.2em;
  1134. left: calc(0.4em - 2px);
  1135. top: calc(0.4em - 2px);
  1136. background: var(--nst-tertiary);
  1137. border-radius: 50%;
  1138. transition: .4s;
  1139. }
  1140.  
  1141. .nst-dialog .nst-switch input:checked + div {
  1142. background: var(--nst-primary);
  1143. border: 2px solid transparent;
  1144. }
  1145.  
  1146. .nst-dialog .nst-switch input:checked + div:before {
  1147. transform: translateX(1.4em);
  1148. background: rgba(0, 0, 0, 0.5);
  1149. }
  1150.  
  1151. .nst-dialog table {
  1152. margin-left: auto;
  1153. margin-right: auto;
  1154. }
  1155.  
  1156. .nst-dialog td {
  1157. padding: 8px;
  1158. position: relative;
  1159. }
  1160.  
  1161. .nst-total-cell {
  1162. right: 0;
  1163. position: sticky !important;
  1164. cursor: pointer;
  1165. background: var(--nst-quinary);
  1166. transition: 0.1s;
  1167. }
  1168.  
  1169. tr.nst-row-selected .nst-total-cell:last-child {
  1170. background: var(--nst-senary);
  1171. }
  1172.  
  1173. .nst-dialog .nst-marks-table-wrapper tr {
  1174. transition: 0.2s;
  1175. }
  1176.  
  1177. .nst-dialog .nst-marks-table-wrapper tr.nst-row-selected {
  1178. background: var(--nst-senary);
  1179. }
  1180.  
  1181. .nst-dialog .nst-marks-table-wrapper {
  1182. overflow-y: auto;
  1183. border-radius: 24px;
  1184. }
  1185.  
  1186. .nst-marks-cell {
  1187. display: flex;
  1188. }
  1189.  
  1190. .nst-controls {
  1191. display: flex;
  1192. flex-wrap: wrap;
  1193. justify-content: space-evenly;
  1194. gap: 8px;
  1195. padding-top: 8px;
  1196. }
  1197.  
  1198. .nst-controls button {
  1199. flex-grow: 1;
  1200. }
  1201.  
  1202. .nst-dialog[open] {
  1203. animation: nst-show-dialog 0.5s forwards;
  1204. }
  1205.  
  1206. .nst-dialog.nst-hide-dialog {
  1207. animation: nst-hide-dialog 0.5s forwards;
  1208. }
  1209.  
  1210. @keyframes nst-show-dialog {
  1211. from {
  1212. opacity: 0;
  1213. transform: scale(0.5);
  1214. }
  1215.  
  1216. to {
  1217. opacity: 1;
  1218. transform: scale(1);
  1219. }
  1220. }
  1221.  
  1222. @keyframes nst-hide-dialog {
  1223. to {
  1224. opacity: 0;
  1225. transform: scale(0.5);
  1226. }
  1227. }
  1228.  
  1229. .nst-dialog::backdrop {
  1230. background: rgba(0, 0, 0, 0.5);
  1231. backdrop-filter: blur(5px);
  1232. animation: none;
  1233. }
  1234.  
  1235. .nst-dialog[open]::backdrop {
  1236. animation: nst-show-opacity 0.5s forwards;
  1237. }
  1238.  
  1239. .nst-dialog.nst-hide-dialog::backdrop {
  1240. animation: nst-hide-opacity 0.5s forwards;
  1241. }
  1242.  
  1243. @keyframes nst-show-opacity {
  1244. from {
  1245. opacity: 0;
  1246. }
  1247.  
  1248. to {
  1249. opacity: 1;
  1250. }
  1251. }
  1252.  
  1253. @keyframes nst-hide-opacity {
  1254. to {
  1255. opacity: 0;
  1256. }
  1257. }
  1258.  
  1259. .nst-label-title {
  1260. font-size: 18px;
  1261. }
  1262.  
  1263. .nst-label-description {
  1264. font-size: 12px;
  1265. color: var(--nst-text-tertiary);
  1266. }
  1267.  
  1268. .nst-mark {
  1269. min-width: 24px;
  1270. display: flex;
  1271. flex-flow: column wrap;
  1272. align-items: stretch;
  1273. cursor: pointer;
  1274. animation: nst-show-opacity 0.5s forwards;
  1275. }
  1276.  
  1277. .nst-mark-selectors {
  1278. display: flex;
  1279. gap: 8px;
  1280. padding-top: 8px;
  1281. justify-content: space-between;
  1282. }
  1283.  
  1284. .nst-mark p {
  1285. text-align: center;
  1286. margin: 0px;
  1287. }
  1288.  
  1289. .nst-mark p:first-child {
  1290. font-size: large;
  1291. }
  1292.  
  1293. .nst-mark p:nth-child(2) {
  1294. color: gray;
  1295. font-size: x-small;
  1296. }
  1297.  
  1298. .nst-mark-highlight {
  1299. animation: nst-mark-highlight 3s forwards;
  1300. }
  1301.  
  1302. @keyframes nst-mark-highlight {
  1303. 0% {
  1304. opacity: 1;
  1305. } 16% {
  1306. opacity: 0;
  1307. } 32% {
  1308. opacity: 1;
  1309. } 48% {
  1310. opacity: 0;
  1311. } 64% {
  1312. opacity: 1;
  1313. } 80% {
  1314. opacity: 0;
  1315. } 96% {
  1316. opacity: 1;
  1317. }
  1318. }
  1319.  
  1320. .nst-mark-excellent {
  1321. color: #96e400;
  1322. }
  1323.  
  1324. .nst-mark-good {
  1325. color: #00c8ff;
  1326. }
  1327.  
  1328. .nst-mark-average {
  1329. color: #f09600;
  1330. }
  1331.  
  1332. .nst-mark-bad {
  1333. color: #ff3232;
  1334. }
  1335. `)