diff options
| -rw-r--r-- | cockpit.css | 228 | ||||
| -rw-r--r-- | cockpit.html | 267 | ||||
| -rw-r--r-- | eslint.config.js | 2 | ||||
| -rw-r--r-- | mass-graduation.css | 161 | ||||
| -rw-r--r-- | mass-graduation.html | 74 | ||||
| -rw-r--r-- | package.json | 50 | ||||
| -rw-r--r-- | src/admin-tools.js | 4 | ||||
| -rw-r--r-- | src/cockpit.js | 182 | ||||
| -rw-r--r-- | src/database-mock.ts | 9 | ||||
| -rw-r--r-- | src/database-prod.ts | 27 | ||||
| -rw-r--r-- | src/graduation-common.ts | 199 | ||||
| -rw-r--r-- | src/graduation-mock.ts | 4 | ||||
| -rw-r--r-- | src/graduation-prod.ts | 7 | ||||
| -rw-r--r-- | src/mass-graduation.js | 89 | ||||
| -rw-r--r-- | src/mock.ts | 49 | ||||
| -rw-r--r-- | src/selection.js | 32 | ||||
| -rw-r--r-- | src/student-mock.ts | 13 | ||||
| -rw-r--r-- | src/student-prod.ts | 35 | ||||
| -rw-r--r-- | src/utils-common.ts | 18 | ||||
| -rw-r--r-- | src/utils-mock.js | 20 | ||||
| -rw-r--r-- | src/utils-prod.js | 12 | ||||
| -rw-r--r-- | tsconfig.json | 26 | ||||
| -rw-r--r-- | webpack.config.js | 121 |
23 files changed, 827 insertions, 802 deletions
diff --git a/cockpit.css b/cockpit.css index 5e020a6..61c4a06 100644 --- a/cockpit.css +++ b/cockpit.css @@ -1,141 +1,141 @@ #student-selection-container { - margin: 0.2rem; + margin: 0.2rem; } #student-selection-search-container { - position: relative; + position: relative; } #student-selection { - box-sizing: border-box; - width: 100%; - padding: 0.75rem 2.5rem 0.75rem 1rem; - margin-bottom: 1.5rem; - border: 1px solid #ddd; - border-radius: 5pt; - font-size: 1.5rem; + box-sizing: border-box; + width: 100%; + padding: 0.75rem 2.5rem 0.75rem 1rem; + margin-bottom: 1.5rem; + border: 1px solid #ddd; + border-radius: 5pt; + font-size: 1.5rem; } #student-selection-clear { - position: absolute; - right: 0.75rem; - transform: translateY(-175%); - border: none; - background: transparent; - font-size: 1.875rem; - cursor: pointer; - color: #888; + position: absolute; + right: 0.75rem; + transform: translateY(-175%); + border: none; + background: transparent; + font-size: 1.875rem; + cursor: pointer; + color: #888; } #student-selection-clear:hover { - color: #333; + color: #333; } #student-selection:focus { - border-color: #0077ff; + border-color: #0077ff; } #student-selection-loader { - --size: 0.5px; - width: calc(48 * var(--size)); - height: calc(48 * var(--size)); - border: calc(5 * var(--size)) solid black; - border-bottom-color: transparent; - border-radius: 50%; - display: inline; - position: absolute; - top: 20px; - right: 0.75rem; - box-sizing: border-box; - animation: rotation 1s linear infinite; + --size: 0.5px; + width: calc(48 * var(--size)); + height: calc(48 * var(--size)); + border: calc(5 * var(--size)) solid black; + border-bottom-color: transparent; + border-radius: 50%; + display: inline; + position: absolute; + top: 20px; + right: 0.75rem; + box-sizing: border-box; + animation: rotation 1s linear infinite; } @keyframes rotation { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } #student-selection-list { - list-style: none; + list-style: none; } #student-selection-list li { - padding: 0.75rem 1rem; - border-bottom: 1px solid #eee; - cursor: pointer; - transition: background 0.2s; + padding: 0.75rem 1rem; + border-bottom: 1px solid #eee; + cursor: pointer; + transition: background 0.2s; } #student-selection-list li:hover { - background: #f0f8ff; + background: #f0f8ff; } #student-selection-list li.active { - background: #0077ff; - color: white; + background: #0077ff; + color: white; } #student-selection-list li > u.student-selection-list-highlight { - font-weight: bold; + font-weight: bold; } #student-selection-noresults { - color: red; - text-align: center; - margin-top: 1rem; + color: red; + text-align: center; + margin-top: 1rem; } .student-info-banner { - display: flex; + display: flex; } #student-info-icons { - width: 20%; - position: relative; + width: 20%; + position: relative; } #student-info-icon { - margin: 0.2rem; - clip-path: circle(); - width: 100%; + margin: 0.2rem; + clip-path: circle(); + width: 100%; } #student-level-icon { - width: 30%; - position: absolute; - top: 70%; - left: 70%; - border: solid 1px; - border-radius: 50%; - background: white; + width: 30%; + position: absolute; + top: 70%; + left: 70%; + border: solid 1px; + border-radius: 50%; + background: white; } #student-info-name-container { - flex: 50%; - margin: 0.2rem; + flex: 50%; + margin: 0.2rem; } #student-info-name { - font-size: 1.2rem; + font-size: 1.2rem; } #student-info-tag { - border-radius: 5pt; - background: gray; - font-size: 0.8rem; - padding: 3pt; - display: none; - width: min-content; + border-radius: 5pt; + background: gray; + font-size: 0.8rem; + padding: 3pt; + display: none; + width: min-content; } #student-info-coach-container { - margin: 0.2rem; - flex: 0 1 20%; - flex-direction: column; + margin: 0.2rem; + flex: 0 1 20%; + flex-direction: column; } #student-observation { @@ -144,82 +144,82 @@ } .student-info-mid-element { - padding: 5pt; - border: black solid 0.2pt; - border-radius: 5pt; - margin: 0.2rem; - text-align: center; - hyphens: auto; - flex: 1; + padding: 5pt; + border: black solid 0.2pt; + border-radius: 5pt; + margin: 0.2rem; + text-align: center; + hyphens: auto; + flex: 1; } .student-info-points { - display: block; - font-size: 2rem; - margin-bottom: 5px; + display: block; + font-size: 2rem; + margin-bottom: 5px; } #student-info-green { - color: green; + color: green; } #student-info-orange { - color: orange; + color: orange; } #student-info-red { - color: red; + color: red; } #student-info-level { - font-size: 1.2rem; - display: block; + font-size: 1.2rem; + display: block; } #student-info-point-reason-container { - margin: 0.2rem; + margin: 0.2rem; } #student-info-point-reason { - box-sizing: border-box; - padding: 10px; - width: 100%; - font-size: 0.8rem; - letter-spacing: 1px; - line-height: 1.5; - border-radius: 5px; - border: 1px solid #cccccc; + box-sizing: border-box; + padding: 10px; + width: 100%; + font-size: 0.8rem; + letter-spacing: 1px; + line-height: 1.5; + border-radius: 5px; + border: 1px solid #cccccc; } #student-info-point-reason-submit { - position: absolute; - right: 0.75rem; - transform: translateY(-175%); - border: none; - background: transparent; - font-size: 1.875rem; - cursor: pointer; - color: #888; + position: absolute; + right: 0.75rem; + transform: translateY(-175%); + border: none; + background: transparent; + font-size: 1.875rem; + cursor: pointer; + color: #888; } #student-info-stickers-container { - flex: 70%; + flex: 70%; } #student-info-bonus-container { - flex: 30%; + flex: 30%; } .student-info-bottom-element { - padding: 5pt; - border: black solid 0.2pt; - border-radius: 5pt; - margin: 0.2rem; - height: 5rem; + padding: 5pt; + border: black solid 0.2pt; + border-radius: 5pt; + margin: 0.2rem; + height: 5rem; } #student-info-bonus-amount { - font-size: 2rem; + font-size: 2rem; } .graduation-dropdown > summary { diff --git a/cockpit.html b/cockpit.html index 072060a..d40bc38 100644 --- a/cockpit.html +++ b/cockpit.html @@ -1,150 +1,147 @@ <!doctype html> <html lang="de"> -<head> - <meta charset="utf-8"> - <title>Cockpit</title> - <link rel="stylesheet" href="cockpit.css"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <script src="./dist/cockpit.js"></script> -</head> -<body> + <head> + <meta charset="utf-8" /> + <title>Cockpit</title> + <link rel="stylesheet" href="cockpit.css" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <script src="./dist/cockpit.js"></script> + </head> + <body> + <div id="student-selection-container" data-availability="if-lehrer"> + <div id="student-selection-search-container"> + <input + type="text" + id="student-selection" + class="student-selection" + placeholder="Vorname Nachname Klasse" + /> + <button id="student-selection-clear" style="display: none">×</button> + <span id="student-selection-loader" style="display: none"></span> + </div> + <ul id="student-selection-list"></ul> + <p id="student-selection-noresults" style="display: none">No results found</p> + </div> -<div id="student-selection-container" data-availability="if-lehrer"> - <div id="student-selection-search-container"> - <input - type="text" - id="student-selection" - class="student-selection" - placeholder="Vorname Nachname Klasse" - /> - <button id="student-selection-clear" style="display: none;">×</button> - <span id="student-selection-loader" style="display: none;"></span> - </div> - <ul id="student-selection-list"> - </ul> - <p id="student-selection-noresults" style="display: none;"> - No results found - </p> -</div> + <div id="student-content" style="display: none"> + <div id="student-info"> + <div id="student-info-top" class="student-info-banner"> + <div id="student-info-icons"> + <img id="student-info-icon" src="assets/user-profile-icon.svg" /> + <img id="student-level-icon" /> + </div> + <div id="student-info-name-container"> + <span id="student-info-name"></span> + <span id="student-info-tag"></span> + </div> + <div id="student-info-coach-container"> + <img id="student-info-coach-icon" /> + <div id="student-info-coach-column"> + Lernbegleiter: + <span id="student-info-coach"></span> + </div> + </div> + </div> -<div id="student-content" style="display: none"> - <div id="student-info"> - <div id="student-info-top" class="student-info-banner"> - <div id="student-info-icons"> - <img id="student-info-icon" src="assets/user-profile-icon.svg" /> - <img id="student-level-icon" /> - </div> - <div id="student-info-name-container"> - <span id="student-info-name"></span> - <span id="student-info-tag"></span> - </div> - <div id="student-info-coach-container"> - <img id="student-info-coach-icon" /> - <div id="student-info-coach-column"> - Lernbegleiter: - <span id="student-info-coach"></span> - </div> - </div> - </div> + <div id="student-observation" class="student-info-banner"></div> - <div id="student-observation" class="student-info-banner"> - </div> + <div id="student-info-mid" class="student-info-banner"> + <div id="student-info-green" class="student-info-mid-element"> + ⚫Grüne Punkte <span class="student-info-points" id="student-info-points-green">0</span> + </div> + <div id="student-info-orange" class="student-info-mid-element"> + ⚫Orangene Punkte + <span class="student-info-points" id="student-info-points-orange">0</span> + </div> + <div id="student-info-red" class="student-info-mid-element"> + ⚫Rote Punkte <span class="student-info-points" id="student-info-points-red">0</span> + </div> + <div id="student-info-level-container" class="student-info-mid-element"> + Graduierungsstatus: + <span id="student-info-level"></span> + </div> + </div> - <div id="student-info-mid" class="student-info-banner"> - <div id="student-info-green" class="student-info-mid-element"> - ⚫Grüne Punkte <span class="student-info-points" id="student-info-points-green">0</span> - </div> - <div id="student-info-orange" class="student-info-mid-element"> - ⚫Orangene Punkte <span class="student-info-points" id="student-info-points-orange">0</span> - </div> - <div id="student-info-red" class="student-info-mid-element"> - ⚫Rote Punkte <span class="student-info-points" id="student-info-points-red">0</span> - </div> - <div id="student-info-level-container" class="student-info-mid-element"> - Graduierungsstatus: - <span id="student-info-level"></span> - </div> - </div> + <div id="student-info-point-reason-container" style="display: none"> + <textarea id="student-info-point-reason" name="reason" rows="5" spellcheck="default"> + </textarea> + <button id="student-info-point-reason-submit">↵</button> + </div> - <div id="student-info-point-reason-container" style="display: none;"> - <textarea id="student-info-point-reason" name="reason" rows="5" spellcheck="default"> - </textarea> - <button id="student-info-point-reason-submit">↵</button> - </div> + <!-- <div id="student-info-bottom" class="student-info-banner"> --> + <!-- <div id="student-info-stickers-container" class="student-info-bottom-element"> --> + <!-- Monstersticker --> + <!-- <div id="student-info-stickers"> --> + <!-- </div> --> + <!-- </div> --> + <!-- <div id="student-info-bonus-container" class="student-info-bottom-element"> --> + <!-- Bonuskarten --> + <!-- <div id="student-info-bonus-row"> --> + <!-- <img /> --> + <!-- <span id="student-info-bonus-amount">0</span> --> + <!-- </div> --> + <!-- </div> --> + <!-- </div> --> + </div> - <!-- <div id="student-info-bottom" class="student-info-banner"> --> - <!-- <div id="student-info-stickers-container" class="student-info-bottom-element"> --> - <!-- Monstersticker --> - <!-- <div id="student-info-stickers"> --> - <!-- </div> --> - <!-- </div> --> - <!-- <div id="student-info-bonus-container" class="student-info-bottom-element"> --> - <!-- Bonuskarten --> - <!-- <div id="student-info-bonus-row"> --> - <!-- <img /> --> - <!-- <span id="student-info-bonus-amount">0</span> --> - <!-- </div> --> - <!-- </div> --> - <!-- </div> --> - </div> + <!-- <details id="progress"> --> + <!-- <summary>Lernfortschritt</summary> --> + <!-- </details> --> - <!-- <details id="progress"> --> - <!-- <summary>Lernfortschritt</summary> --> - <!-- </details> --> - - <!-- <details id="calendar"> --> - <!-- <summary>Kalender</summary> --> - <!-- </details> --> + <!-- <details id="calendar"> --> + <!-- <summary>Kalender</summary> --> + <!-- </details> --> - <details id="graduation" data-availability="if-lehrer"> - <summary>Graduierung</summary> + <details id="graduation" data-availability="if-lehrer"> + <summary>Graduierung</summary> - <details id="graduation-changes-container" class="graduation-dropdown"> - <summary>Übersicht über Grüne, Orangene und Rote Punkte</summary> - <table class="table table-striped"> - <thead> - <tr> - <th>Datum</th> - <th>Grund</th> - <th>Coach</th> - </tr> - </thead> - <tbody id="graduation-changes"> - </tbody> - </table> - </details> + <details id="graduation-changes-container" class="graduation-dropdown"> + <summary>Übersicht über Grüne, Orangene und Rote Punkte</summary> + <table class="table table-striped"> + <thead> + <tr> + <th>Datum</th> + <th>Grund</th> + <th>Coach</th> + </tr> + </thead> + <tbody id="graduation-changes"></tbody> + </table> + </details> - <details id="graduation-current-observation-period-container" class="graduation-dropdown"> - <summary>Aktueller Beobachtungszeitraum</summary> - <table class="table table-striped"> - <thead> - <tr> - <th></th> - <th>Ja</th> - <th>Nein</th> - </tr> - </thead> - <tbody id="graduation-current-observation-period"> - </tbody> - </table> + <details id="graduation-current-observation-period-container" class="graduation-dropdown"> + <summary>Aktueller Beobachtungszeitraum</summary> + <table class="table table-striped"> + <thead> + <tr> + <th></th> + <th>Ja</th> + <th>Nein</th> + </tr> + </thead> + <tbody id="graduation-current-observation-period"></tbody> + </table> - <div id="graduation-current-observation-period-end" style="display:none;"> - <textarea id="graduation-current-observation-period-end-reason" rows="5" spellcheck="default"> - </textarea> - Aktuellen Beobachtungszeitraum abschließen: - <button id="graduation-current-observation-period-end-success">Erfolreich</button> - <button id="graduation-current-observation-period-end-failure">Nicht Erfolreich</button> - </div> - </details> + <div id="graduation-current-observation-period-end" style="display: none"> + <textarea + id="graduation-current-observation-period-end-reason" + rows="5" + spellcheck="default" + > + </textarea> + Aktuellen Beobachtungszeitraum abschließen: + <button id="graduation-current-observation-period-end-success">Erfolreich</button> + <button id="graduation-current-observation-period-end-failure">Nicht Erfolreich</button> + </div> + </details> - <details id="graduation-observation-periods-container" class="graduation-dropdown"> - <summary>Vergangene Beobachtungszeiträume</summary> - <table class="table table-striped"> - <tbody id="graduation-observation-periods"> - </tbody> - </table> - </details> - </details> -</div> -</body> + <details id="graduation-observation-periods-container" class="graduation-dropdown"> + <summary>Vergangene Beobachtungszeiträume</summary> + <table class="table table-striped"> + <tbody id="graduation-observation-periods"></tbody> + </table> + </details> + </details> + </div> + </body> </html> diff --git a/eslint.config.js b/eslint.config.js index 731fd4b..a39cbb2 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -7,4 +7,4 @@ module.exports = [ }, }, eslintConfigPrettier, -];
\ No newline at end of file +]; diff --git a/mass-graduation.css b/mass-graduation.css index 5de091b..d32d7c1 100644 --- a/mass-graduation.css +++ b/mass-graduation.css @@ -1,122 +1,124 @@ #group-selection-container { - margin: 0.2rem; + margin: 0.2rem; } #group-selection-search-container { - position: relative; + position: relative; } #group-selection { - box-sizing: border-box; - width: 100%; - padding: 0.75rem 2.5rem 0.75rem 1rem; - margin-bottom: 1.5rem; - border: 1px solid #ddd; - border-radius: 5pt; - font-size: 1.5rem; + box-sizing: border-box; + width: 100%; + padding: 0.75rem 2.5rem 0.75rem 1rem; + margin-bottom: 1.5rem; + border: 1px solid #ddd; + border-radius: 5pt; + font-size: 1.5rem; } #group-selection-clear { - position: absolute; - right: 0.75rem; - transform: translateY(-175%); - border: none; - background: transparent; - font-size: 1.875rem; - cursor: pointer; - color: #888; + position: absolute; + right: 0.75rem; + transform: translateY(-175%); + border: none; + background: transparent; + font-size: 1.875rem; + cursor: pointer; + color: #888; } #group-selection-clear:hover { - color: #333; + color: #333; } #group-selection:focus { - border-color: #0077ff; + border-color: #0077ff; } #group-selection-loader { - --size: 0.5px; - width: calc(48 * var(--size)); - height: calc(48 * var(--size)); - border: calc(5 * var(--size)) solid black; - border-bottom-color: transparent; - border-radius: 50%; - display: inline; - position: absolute; - top: 20px; - right: 0.75rem; - box-sizing: border-box; - animation: rotation 1s linear infinite; + --size: 0.5px; + width: calc(48 * var(--size)); + height: calc(48 * var(--size)); + border: calc(5 * var(--size)) solid black; + border-bottom-color: transparent; + border-radius: 50%; + display: inline; + position: absolute; + top: 20px; + right: 0.75rem; + box-sizing: border-box; + animation: rotation 1s linear infinite; } @keyframes rotation { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } #group-selection-list { - list-style: none; + list-style: none; } #group-selection-list li { - padding: 0.75rem 1rem; - border-bottom: 1px solid #eee; - cursor: pointer; - transition: background 0.2s; + padding: 0.75rem 1rem; + border-bottom: 1px solid #eee; + cursor: pointer; + transition: background 0.2s; } #group-selection-list li:hover { - background: #f0f8ff; + background: #f0f8ff; } #group-selection-list li.active { - background: #0077ff; - color: white; + background: #0077ff; + color: white; } #group-selection-list li > u.group-selection-list-highlight { - font-weight: bold; + font-weight: bold; } #group-selection-noresults { - color: red; - text-align: center; - margin-top: 1rem; + color: red; + text-align: center; + margin-top: 1rem; } #group-content { } #group-members { - padding-left: 0; + padding-left: 0; } .group-member { - display: grid; - grid-template-columns: 30px 200px 30px repeat(3, 40px) auto 20px; + display: grid; + grid-template-columns: 30px 200px 30px repeat(3, 40px) auto 20px; } .group-member-icon { - width: 30px; - height: 30px; - align-self: center; + width: 30px; + height: 30px; + align-self: center; } .group-member-name { - align-self: center; + align-self: center; } .group-member-observation-icon { - border: none; - background: none; + border: none; + background: none; } -.group-member-green, .group-member-orange, .group-member-red { +.group-member-green, +.group-member-orange, +.group-member-red { margin: 5px; } @@ -134,14 +136,14 @@ } #group-member-point-reason { - box-sizing: border-box; - padding: 10px; - width: 50%; - font-size: 0.8rem; - letter-spacing: 1px; - line-height: 1.5; - border-radius: 5px; - border: 1px solid #cccccc; + box-sizing: border-box; + padding: 10px; + width: 50%; + font-size: 0.8rem; + letter-spacing: 1px; + line-height: 1.5; + border-radius: 5px; + border: 1px solid #cccccc; } .group-member-print { @@ -157,7 +159,6 @@ .group-member-observation-table { background: lightgray; - } .group-member-observation-table-description { max-width: 70%; @@ -171,16 +172,16 @@ } .submit-mass-graduation-inprogress { - --size: 0.5px; - width: calc(48 * var(--size)); - height: calc(48 * var(--size)); - border: calc(5 * var(--size)) solid black; - border-bottom-color: transparent; - border-radius: 50%; - display: inline; - position: absolute; - margin-left: 0.25rem; - margin-top: 0.25rem; - box-sizing: border-box; - animation: rotation 1s linear infinite; + --size: 0.5px; + width: calc(48 * var(--size)); + height: calc(48 * var(--size)); + border: calc(5 * var(--size)) solid black; + border-bottom-color: transparent; + border-radius: 50%; + display: inline; + position: absolute; + margin-left: 0.25rem; + margin-top: 0.25rem; + box-sizing: border-box; + animation: rotation 1s linear infinite; } diff --git a/mass-graduation.html b/mass-graduation.html index 16c50a0..93be247 100644 --- a/mass-graduation.html +++ b/mass-graduation.html @@ -1,46 +1,36 @@ <!doctype html> <html lang="de"> -<head> - <meta charset="utf-8"> - <title>Graduation</title> - <link rel="stylesheet" href="mass-graduation.css"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <script src="./dist/mass-graduation.js"></script> -</head> -<body> - <div id="group-selection-container"> - <div id="group-selection-search-container"> - <input - type="text" - id="group-selection" - class="group-selection" - placeholder="Klasse" - /> - <button id="group-selection-clear" style="display: none;">×</button> - <span id="group-selection-loader" style="display: none;"></span> - </div> - <ul id="group-selection-list"> - </ul> - <p id="group-selection-noresults" style="display: none;"> - No results found - </p> - </div> - - <div id="group-content" style="display: none"> - <div id="group-member-container"> - <ul id="group-members"> - </ul> - </div> - <button id="submit-mass-graduation" style="display: none;">Speichern</button> - <span id="submit-mass-graduation-state" style="display: none;"></span> + <head> + <meta charset="utf-8" /> + <title>Graduation</title> + <link rel="stylesheet" href="mass-graduation.css" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <script src="./dist/mass-graduation.js"></script> + </head> + <body> + <div id="group-selection-container"> + <div id="group-selection-search-container"> + <input type="text" id="group-selection" class="group-selection" placeholder="Klasse" /> + <button id="group-selection-clear" style="display: none">×</button> + <span id="group-selection-loader" style="display: none"></span> + </div> + <ul id="group-selection-list"></ul> + <p id="group-selection-noresults" style="display: none">No results found</p> + </div> - <div id="observation-requests-container" style="display: none;"> - <h3>Aktuelle Graduierungswünsche</h3> - <table> - <tbody id="observation-requests"> - </tbody> - </table> - </div> - </div> -</body> + <div id="group-content" style="display: none"> + <div id="group-member-container"> + <ul id="group-members"></ul> + </div> + <button id="submit-mass-graduation" style="display: none">Speichern</button> + <span id="submit-mass-graduation-state" style="display: none"></span> + + <div id="observation-requests-container" style="display: none"> + <h3>Aktuelle Graduierungswünsche</h3> + <table> + <tbody id="observation-requests"></tbody> + </table> + </div> + </div> + </body> </html> diff --git a/package.json b/package.json index b486a74..a2525bc 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,27 @@ { - "version": "1.0.0", - "description": "", - "name": "ByCS-SOL", - "scripts": { - "build": "webpack --mode=production --config-node-env=production", - "build:dev": "webpack --mode=development", - "build:mock": "webpack --mode=development --config-node-env=mock", - "types-check": "tsc --noEmit", - "lint": "eslint --config eslint.config.js src/", - "prettier-check": "prettier --check .", - "format": "prettier --write ." - }, - "devDependencies": { - "@babel/core": "^8.0.1", - "@babel/preset-env": "^8.0.2", - "@babel/preset-typescript": "^8.0.1", - "babel-loader": "^10.1.1", - "circular-dependency-plugin": "^5.2.2", - "eslint": "^10.8.0", - "eslint-config-prettier": "^10.1.8", - "prettier": "^3.9.6", - "typescript": "^7.0.2", - "webpack": "^5.107.2", - "webpack-cli": "^7.0.2" - } + "version": "1.0.0", + "description": "", + "name": "ByCS-SOL", + "scripts": { + "build": "webpack --mode=production --config-node-env=production", + "build:dev": "webpack --mode=development", + "build:mock": "webpack --mode=development --config-node-env=mock", + "types-check": "tsc --noEmit", + "lint": "eslint --config eslint.config.js src/", + "prettier-check": "prettier --check .", + "format": "prettier --write ." + }, + "devDependencies": { + "@babel/core": "^8.0.1", + "@babel/preset-env": "^8.0.2", + "@babel/preset-typescript": "^8.0.1", + "babel-loader": "^10.1.1", + "circular-dependency-plugin": "^5.2.2", + "eslint": "^10.8.0", + "eslint-config-prettier": "^10.1.8", + "prettier": "^3.9.6", + "typescript": "^7.0.2", + "webpack": "^5.107.2", + "webpack-cli": "^7.0.2" + } } diff --git a/src/admin-tools.js b/src/admin-tools.js index b0cf80d..d8e1bf7 100644 --- a/src/admin-tools.js +++ b/src/admin-tools.js @@ -1,5 +1,5 @@ -import { DB } from "./database.js"; -import { fetchStudents } from "./student-ENV.js"; +import { DB } from "./database-ENV.ts"; +import { fetchStudents } from "./student-ENV.ts"; let outputNode; function output(msg) { diff --git a/src/cockpit.js b/src/cockpit.js index 67619d8..97d6fed 100644 --- a/src/cockpit.js +++ b/src/cockpit.js @@ -1,6 +1,6 @@ // import { - // fetchSubjectsMock as fetchSubjects, - // fetchProgressMock as fetchProgress, +// fetchSubjectsMock as fetchSubjects, +// fetchProgressMock as fetchProgress, // } from "../../js/mock.ts"; import { @@ -8,15 +8,11 @@ import { LEVEL_NAMES, GraduationChange, fetchGraduation, - submitGraduationChange + submitGraduationChange, } from "./graduation-ENV.ts"; import { fetchStudent, fetchStudents } from "./student-ENV.ts"; -import { - getName, - DATE_FMT, - DATETIME_FMT, -} from "./utils-ENV.js"; +import { getName, DATE_FMT, DATETIME_FMT } from "./utils-ENV.js"; import { initSelection } from "./selection.js"; let name; @@ -35,7 +31,7 @@ function initStudentSelection() { "student-selection", fetchStudents, (student) => `${student.name} ${student.getGroup()}`, - loadStudent + loadStudent, ); } @@ -46,46 +42,46 @@ function pointClickHandler(color) { // TODO(muhq): Prefill the textarea with common reasons switch (color) { - case "green": - textarea.style.background = "rgb(from green r g b / 20%)"; - textarea.value = ""; - break; - case "orange": - textarea.style.background = "rgb(from orange r g b / 20%)"; - textarea.value = ""; - break; - case "red": - textarea.style.background = "rgb(from red r g b / 20%)"; - textarea.value = ""; - break; + case "green": + textarea.style.background = "rgb(from green r g b / 20%)"; + textarea.value = ""; + break; + case "orange": + textarea.style.background = "rgb(from orange r g b / 20%)"; + textarea.value = ""; + break; + case "red": + textarea.style.background = "rgb(from red r g b / 20%)"; + textarea.value = ""; + break; } textareaDiv.style.display = "block"; textarea.focus(); const coach = name; - const submit = function() { + const submit = function () { let change; switch (color) { - case "green": - change = GraduationChange.newGreenPointChange(textarea.value, coach); - break; - case "orange": - change = GraduationChange.newOrangePointChange(textarea.value, coach); - break; - case "red": - change = GraduationChange.newRedPointChange(textarea.value, coach); - break; - default: - throw new Error("Invalid color"); + case "green": + change = GraduationChange.newGreenPointChange(textarea.value, coach); + break; + case "orange": + change = GraduationChange.newOrangePointChange(textarea.value, coach); + break; + case "red": + change = GraduationChange.newRedPointChange(textarea.value, coach); + break; + default: + throw new Error("Invalid color"); } submitGraduationChange(student, change); textareaDiv.style.display = "none"; updateStudentInfo(); - student.fetchGraduation().then(() => updateStudentGraduation() ); + student.fetchGraduation().then(() => updateStudentGraduation()); }; // Use onEVENT to override any previous handler - textarea.onkeydown = function(e) { + textarea.onkeydown = function (e) { if (e.shiftKey && e.key === "Enter") { submit(); } @@ -108,12 +104,12 @@ function updateStudentInfo() { document.getElementById("student-info-icon").src = DEFAULT_STUDENT_ICON; // Try to replace the generic icon with the user's icon - var img = document.createElement('img'); - img.onload = function() { + var img = document.createElement("img"); + img.onload = function () { document.getElementById("student-info-icon").src = img.src; img = img.onload = null; }; - student.getIconUrl().then( (url) => img.src = url ); + student.getIconUrl().then((url) => (img.src = url)); document.getElementById("student-info-name").innerHTML = `${student.name}`; @@ -126,12 +122,15 @@ function updateStudentInfo() { document.getElementById("student-info-points-orange").innerHTML = student.graduation.orange; document.getElementById("student-info-points-red").innerHTML = student.graduation.red; - document.getElementById("student-info-green").addEventListener('click', - () => pointClickHandler("green")); - document.getElementById("student-info-orange").addEventListener('click', - () => pointClickHandler("orange")); - document.getElementById("student-info-red").addEventListener('click', - () => pointClickHandler("red")); + document + .getElementById("student-info-green") + .addEventListener("click", () => pointClickHandler("green")); + document + .getElementById("student-info-orange") + .addEventListener("click", () => pointClickHandler("orange")); + document + .getElementById("student-info-red") + .addEventListener("click", () => pointClickHandler("red")); renderGraduationLevel(student.graduation); @@ -152,11 +151,11 @@ function renderGraduationLevel(graduation) { } // function renderGraduationStickers(graduation, node) { - // node.innerHTML = ""; +// node.innerHTML = ""; // } // function renderGraduationBonus(graduation, node) { - // node.innerHTML = ""; +// node.innerHTML = ""; // } function renderObservationStatus(graduation, node) { @@ -167,9 +166,10 @@ function renderObservationStatus(graduation, node) { status = document.createElement("span"); status.classList.add("student-observation-status"); if (graduation.isObserved()) { - status.innerHTML = '<span style="font-size: larger;">👁</span> Ich befinde mich in der Beobachtungsphase zum Levelaufstieg.'; + status.innerHTML = + '<span style="font-size: larger;">👁</span> Ich befinde mich in der Beobachtungsphase zum Levelaufstieg.'; } else if (graduation.observationRequested) { - status.innerHTML = 'Beobachtungsphase noch nicht begonnen.'; + status.innerHTML = "Beobachtungsphase noch nicht begonnen."; } else { status.innerHTML = "Nicht in Beobachtung."; } @@ -178,7 +178,7 @@ function renderObservationStatus(graduation, node) { status.classList.add("student-request-observation-button"); // Level is 1-based status.innerHTML = `Ich will ${LEVEL_NAMES[graduation.level]} werden`; - status.addEventListener("click", async function() { + status.addEventListener("click", async function () { await graduation.requestObservation(); // Rerender the observation status renderObservationStatus(graduation, node); @@ -188,37 +188,37 @@ function renderObservationStatus(graduation, node) { } // function newProgressBar(subject) { - // let bar = document.createElement("div"); - // fetchProgress(subject).then((progress) => { - // bar.progress = progress; - // }); - - // let s1 = document.createElement("div"); - // s1.onhover - - // bar.appendChild(s1); - // let s2 = document.createElement("div"); - // bar.appendChild(s2); - // let s3 = document.createElement("div"); - // bar.appendChild(s3); - // return bar; +// let bar = document.createElement("div"); +// fetchProgress(subject).then((progress) => { +// bar.progress = progress; +// }); + +// let s1 = document.createElement("div"); +// s1.onhover + +// bar.appendChild(s1); +// let s2 = document.createElement("div"); +// bar.appendChild(s2); +// let s3 = document.createElement("div"); +// bar.appendChild(s3); +// return bar; // } // let selectedSubjects; // async function updateStudentProgress() { - // selectedSubjects = await fetchSubjects(student); - - // let prog = document.getElementById("progress"); - // for(const subj of selectedSubjects) { - // let subjDiv = document.createElement("div"); - // subjDiv.innerHTML = subj; - // subjDiv.appendChild(newProgressBar(subj)) - // prog.appendChild(subjDiv); - // } +// selectedSubjects = await fetchSubjects(student); + +// let prog = document.getElementById("progress"); +// for(const subj of selectedSubjects) { +// let subjDiv = document.createElement("div"); +// subjDiv.innerHTML = subj; +// subjDiv.appendChild(newProgressBar(subj)) +// prog.appendChild(subjDiv); +// } // } // async function updateStudentCalendar() { - // let cal = document.getElementById("calendar"); +// let cal = document.getElementById("calendar"); // } async function updateStudentGraduation() { @@ -227,26 +227,26 @@ async function updateStudentGraduation() { const changesTbl = document.getElementById("graduation-changes"); changesTbl.innerHTML = ""; - for (const change of graduation.changes.filter( - (ch) => ["Grün", "Rot", "Orange"].includes(ch.change) )) - { + for (const change of graduation.changes.filter((ch) => + ["Grün", "Rot", "Orange"].includes(ch.change), + )) { const changeTr = document.createElement("tr"); for (const key in change) { if (key == "json" || key == "id" || key == "name") continue; if (key == "change") { - switch(change.change) { - case "Grün": - changeTr.style.background = "green"; - break; - case "Rot": - changeTr.style.background = "red"; - break; - case "Orange": - changeTr.style.background = "orange"; - break; - default: - console.error(`unexpected change: ${change.change}`); + switch (change.change) { + case "Grün": + changeTr.style.background = "green"; + break; + case "Rot": + changeTr.style.background = "red"; + break; + case "Orange": + changeTr.style.background = "orange"; + break; + default: + console.error(`unexpected change: ${change.change}`); } continue; } @@ -291,7 +291,9 @@ async function updateStudentGraduation() { endDiv.style.display = "block"; - const reason = document.getElementById("graduation-current-observation-period-end-reason").value; + const reason = document.getElementById( + "graduation-current-observation-period-end-reason", + ).value; const successBtn = document.getElementById("graduation-current-observation-period-end-success"); successBtn.addEventListener("click", () => { submitGraduationChange(graduation.student, GraduationChange.newObservationEnd(reason, true)); @@ -336,7 +338,7 @@ async function initCockpit() { } } -window.onload = function() { +window.onload = function () { DEFAULT_STUDENT_ICON = document.getElementById("student-info-icon").src; name = getName(); initCockpit(); diff --git a/src/database-mock.ts b/src/database-mock.ts index 24cb08f..4d7bf55 100644 --- a/src/database-mock.ts +++ b/src/database-mock.ts @@ -14,17 +14,12 @@ class DB { async saveEntry(json: string) { console.log(`Mock DB save entry ${json}`); - return; + return; } - } async function getGraduationDb() { return new DB(); } -export { - DB, - DatabaseEntry, DatabaseParams, - getGraduationDb, -}; +export { DB, DatabaseEntry, DatabaseParams, getGraduationDb }; diff --git a/src/database-prod.ts b/src/database-prod.ts index 6a55f25..57b9974 100644 --- a/src/database-prod.ts +++ b/src/database-prod.ts @@ -23,7 +23,7 @@ const DB_TEMPLATE_LIST_FOOTER = ` let graduationDb = findGraduationDb(); -const ID_QUERY_PARAM_REGEX = /[?&]id=(\d+)/ +const ID_QUERY_PARAM_REGEX = /[?&]id=(\d+)/; async function findGraduationDb() { let graduationDbId = localStorage.getItem("gruaduationDbId"); @@ -33,16 +33,16 @@ async function findGraduationDb() { const courseUrl = `https://lernplattform.bycs.de/course/view.php?id=${getCourseId()}`; const courseRes = await fetch(courseUrl); - const courseDoc = new DOMParser().parseFromString(await courseRes.text(), 'text/html'); + const courseDoc = new DOMParser().parseFromString(await courseRes.text(), "text/html"); for (const a of courseDoc.querySelectorAll("div.activityname a.aalink")) { if (a.textContent.trim() === "Graduation Datenbank") { - const anchor = (a as HTMLAnchorElement); + const anchor = a as HTMLAnchorElement; const match = anchor.href.match(ID_QUERY_PARAM_REGEX); if (match === null) continue; const dbId = match[1]; localStorage.setItem("graduationDbId", dbId); - return new DB(Number(dbId)) + return new DB(Number(dbId)); } } console.assert(false, "Graduation DB not found"); @@ -64,13 +64,12 @@ class DB { } async fetchEntries(params?: DatabaseParams) { - return loadFromDatabaseActivity(this.id, params) + return loadFromDatabaseActivity(this.id, params); } async saveEntry(json: string) { return addEntryToDatabaseActivity(this.id, json); } - } async function loadFromDatabaseActivity(id: string | number, params?: DatabaseParams) { @@ -79,16 +78,16 @@ async function loadFromDatabaseActivity(id: string | number, params?: DatabasePa if (typeof params !== "undefined") { dbUrl += "&filter=1&advanced=1"; for (const param in params) { - dbUrl += `&${param}=${encodeURIComponent(params[param])}` + dbUrl += `&${param}=${encodeURIComponent(params[param])}`; } } const dbRes = await fetch(encodeURI(dbUrl)); - const dbDoc = new DOMParser().parseFromString(await dbRes.text(), 'text/html'); + const dbDoc = new DOMParser().parseFromString(await dbRes.text(), "text/html"); let data: any[] = []; - dbDoc.querySelectorAll('.db-row').forEach(row => { + dbDoc.querySelectorAll(".db-row").forEach((row) => { let entry: DatabaseEntry = {}; - row.querySelectorAll<HTMLElement>('.db-cell').forEach(field => { + row.querySelectorAll<HTMLElement>(".db-cell").forEach((field) => { const key = field.dataset.key!; let value; // Moodle includes the full date in the title attribute of the included span @@ -112,7 +111,7 @@ async function addEntryToDatabaseActivity(id: number, json: string) { // TODO(muhq): do not fetch the form const url = `/mod/data/edit.php?id=${id}`; const editRes = await fetch(url); - const editDoc = new DOMParser().parseFromString(await editRes.text(), 'text/html'); + const editDoc = new DOMParser().parseFromString(await editRes.text(), "text/html"); const form = editDoc.querySelector<HTMLFormElement>('div[role="main"] > form'); const formData = new FormData(form!); @@ -133,8 +132,4 @@ async function addEntryToDatabaseActivity(id: number, json: string) { }); } -export { - DB, - DatabaseEntry, DatabaseParams, - getGraduationDb, -}; +export { DB, DatabaseEntry, DatabaseParams, getGraduationDb }; diff --git a/src/graduation-common.ts b/src/graduation-common.ts index 61600d3..2c0fb69 100644 --- a/src/graduation-common.ts +++ b/src/graduation-common.ts @@ -1,5 +1,5 @@ -import { getGraduationDb, DatabaseEntry } from "./database-ENV.ts" -import type { Student } from "./student-common.ts" +import { getGraduationDb, DatabaseEntry } from "./database-ENV.ts"; +import type { Student } from "./student-common.ts"; import NeulingPng from "../assets/Neuling.png"; import EntdeckerPng from "../assets/Entdecker.png"; @@ -23,7 +23,14 @@ class GraduationChange { coach: string; context: any; - constructor(date: Date, studentId: number, change: string, reason: string, coach: string, context: any) { + constructor( + date: Date, + studentId: number, + change: string, + reason: string, + coach: string, + context: any, + ) { this.date = date; this.studentId = studentId; this.change = change; @@ -32,19 +39,26 @@ class GraduationChange { this.context = context; } - dbFields () { + dbFields() { return { - "studentId": this.studentId, - "change": this.change, - "reason": this.reason, - "context": this.context, + studentId: this.studentId, + change: this.change, + reason: this.reason, + context: this.context, }; } static fromDBEntry(e: DatabaseEntry) { - const {date, coach, json} = e; + const { date, coach, json } = e; const change = JSON.parse(json); - return new GraduationChange(new Date(date), change.change, change.student, change.reason, coach, change.context); + return new GraduationChange( + new Date(date), + change.change, + change.student, + change.reason, + coach, + change.context, + ); } static newObservationRequest() { @@ -56,11 +70,25 @@ class GraduationChange { } static newObservationEnd(reason: string, success: boolean) { - return new GraduationChange(new Date(), -1, "ObservationEnd", reason, "", JSON.stringify(success)); + return new GraduationChange( + new Date(), + -1, + "ObservationEnd", + reason, + "", + JSON.stringify(success), + ); } static newObservationChange(observation: Observation) { - return new GraduationChange(observation.date, -1, "Observation", "", "", JSON.stringify(observation.status)); + return new GraduationChange( + observation.date, + -1, + "Observation", + "", + "", + JSON.stringify(observation.status), + ); } static newGreenPointChange(reason: string, coach: string) { @@ -87,7 +115,17 @@ class Graduation { pastObservations: Array<ObservationPeriod>; changes: Array<GraduationChange>; - constructor(student: Student, green: number, orange: number, red: number, level: number, observationRequested: Date | null, observation: ObservationPeriod | null, pastObservations: Array<ObservationPeriod>, changes: Array<GraduationChange>) { + constructor( + student: Student, + green: number, + orange: number, + red: number, + level: number, + observationRequested: Date | null, + observation: ObservationPeriod | null, + pastObservations: Array<ObservationPeriod>, + changes: Array<GraduationChange>, + ) { this.student = student; this.green = green; this.orange = orange; @@ -115,63 +153,70 @@ class Graduation { for (const change of changes) { switch (change.change) { - case "Grün": - green++; - break; - - case "Orange": - orange++; - break; - - case "Rot": - red++; - break; - - case "ObservationRequest": - openObservationRequest = change.date; - break; - - case "ObservationStart": - console.assert(openObservationRequest === null, - "No request for observation start"); - - observationPeriod = new ObservationPeriod(change.date); - openObservationRequest = null; - break; - - case "ObservationEnd": - if (observationPeriod === null) { - console.error("No active observation period"); - continue; - } - observationPeriod.finishPeriod(change.context.success); - if (change.context.success) { - level++; - } - pastObservations.push(observationPeriod); - observationPeriod = null; - break; - - case "Observation": - if (observationPeriod === null) { - console.error("No active observation period"); - continue; - } - const observation = Observation.fromChange(change); - observationPeriod.addObservation(observation); - break; - - case "LevelChange": - level = change.context.level; - break; - - default: - throw new Error(`Unexpected change: ${change}`); + case "Grün": + green++; + break; + + case "Orange": + orange++; + break; + + case "Rot": + red++; + break; + + case "ObservationRequest": + openObservationRequest = change.date; + break; + + case "ObservationStart": + console.assert(openObservationRequest === null, "No request for observation start"); + + observationPeriod = new ObservationPeriod(change.date); + openObservationRequest = null; + break; + + case "ObservationEnd": + if (observationPeriod === null) { + console.error("No active observation period"); + continue; + } + observationPeriod.finishPeriod(change.context.success); + if (change.context.success) { + level++; + } + pastObservations.push(observationPeriod); + observationPeriod = null; + break; + + case "Observation": + if (observationPeriod === null) { + console.error("No active observation period"); + continue; + } + const observation = Observation.fromChange(change); + observationPeriod.addObservation(observation); + break; + + case "LevelChange": + level = change.context.level; + break; + + default: + throw new Error(`Unexpected change: ${change}`); } } - return new Graduation(student, green, orange, red, level, - openObservationRequest, observationPeriod, pastObservations, - changes); + return new Graduation( + student, + green, + orange, + red, + level, + openObservationRequest, + observationPeriod, + pastObservations, + changes, + ); } addChange(change: GraduationChange) { @@ -264,8 +309,7 @@ class ObservationPeriod { } ready() { - if (this.observations.length < 10) - return false; + if (this.observations.length < 10) return false; // at least 10 observation on different days let days = new Map(); @@ -277,14 +321,14 @@ class ObservationPeriod { } getObservationsSums() { - let sums = Array(OBSERVATION_TYPES.length).fill(null).map(() => ({"yes":0, "no":0})); + let sums = Array(OBSERVATION_TYPES.length) + .fill(null) + .map(() => ({ yes: 0, no: 0 })); for (const observation of this.observations) { for (let i = 0; i < sums.length; i++) { if (observation.status[i]) { - if (observation.status[i] === "yes") - sums[i].yes++; - else - sums[i].no++; + if (observation.status[i] === "yes") sums[i].yes++; + else sums[i].no++; } } } @@ -317,7 +361,7 @@ class Observation { } static fromChange(change: GraduationChange) { - return new Observation(change.date, JSON.parse(change.context)) + return new Observation(change.date, JSON.parse(change.context)); } } @@ -328,7 +372,7 @@ async function submitGraduationChange(graduation: Graduation, change: Graduation } async function submitObservation(graduation: Graduation, observation: Observation) { - if(graduation.observation === null) throw new Error("no active observation"); + if (graduation.observation === null) throw new Error("no active observation"); graduation.observation.addObservation(observation); await submitGraduationChange(graduation, GraduationChange.newObservationChange(observation)); } @@ -340,7 +384,6 @@ export { Graduation, submitGraduationChange, submitObservation, - OBSERVATION_TYPES, Observation, ObservationPeriod, diff --git a/src/graduation-mock.ts b/src/graduation-mock.ts index 84a7143..5c48a81 100644 --- a/src/graduation-mock.ts +++ b/src/graduation-mock.ts @@ -2,7 +2,6 @@ import { LEVEL_NAMES, GraduationChange, Graduation, - OBSERVATION_TYPES, Observation, ObservationPeriod, @@ -11,7 +10,7 @@ import { import { fetchGraduationMock as fetchGraduation, submitGraduationChangeMock as submitGraduationChange, - submitObservationMock as submitObservation + submitObservationMock as submitObservation, } from "./mock.ts"; export { @@ -21,7 +20,6 @@ export { fetchGraduation, submitGraduationChange, submitObservation, - OBSERVATION_TYPES, Observation, ObservationPeriod, diff --git a/src/graduation-prod.ts b/src/graduation-prod.ts index 975fd0f..663493a 100644 --- a/src/graduation-prod.ts +++ b/src/graduation-prod.ts @@ -1,8 +1,8 @@ -import { getGraduationDb } from "./database-ENV.ts" +import { getGraduationDb } from "./database-ENV.ts"; import { Graduation, GraduationChange } from "./graduation-common.ts"; import type { Student } from "./student-common.ts"; -const _graduationChangesPromise = async function() { +const _graduationChangesPromise = (async function () { const db = await getGraduationDb(); let changes = new Map(); let entries = await db.fetchEntries(); @@ -16,7 +16,7 @@ const _graduationChangesPromise = async function() { } } return changes; -}(); +})(); const graduations: Map<number, Graduation> = new Map(); @@ -36,7 +36,6 @@ export { Graduation, submitGraduationChange, submitObservation, - OBSERVATION_TYPES, Observation, ObservationPeriod, diff --git a/src/mass-graduation.js b/src/mass-graduation.js index b9240f0..48d23ad 100644 --- a/src/mass-graduation.js +++ b/src/mass-graduation.js @@ -3,7 +3,7 @@ import { GraduationChange, submitGraduationChange, Observation, - submitObservation + submitObservation, } from "./graduation-ENV.ts"; import { fetchGroups } from "./group-fetch.ts"; @@ -19,8 +19,7 @@ function toggle(btn, others) { } else { btn.innerHTML = "X"; if (others) { - for (const other of others) - other.innerHTML = "o"; + for (const other of others) other.innerHTML = "o"; } } @@ -31,7 +30,7 @@ function toggle(btn, others) { function observationClickHandler(li) { let observationDiv = li.observationDiv; // Build the observation container - if(!observationDiv) { + if (!observationDiv) { li.style.background = "lightgray"; observationDiv = document.createElement("div"); @@ -97,12 +96,11 @@ function observationClickHandler(li) { obsTbdy.appendChild(obsTr); } - // Toggle the observation container visibility + // Toggle the observation container visibility } else { if (observationDiv.style.display === "none") { observationDiv.style.display = "block"; li.style.background = "lightgray"; - } else { observationDiv.style.display = "none"; li.style.background = "unset"; @@ -148,7 +146,7 @@ async function loadGroup(selected) { return; } ob.innerHTML = "👁"; - ob.addEventListener("click", () => observationClickHandler(li) ); + ob.addEventListener("click", () => observationClickHandler(li)); }); graduationPromises.push(graduationPromise); @@ -158,7 +156,7 @@ async function loadGroup(selected) { const b = document.createElement("button"); b.classList.add(`group-member-${col}`); b.innerHTML = "o"; - b.addEventListener("click", () => toggle(b) ); + b.addEventListener("click", () => toggle(b)); li.appendChild(b); } @@ -169,7 +167,7 @@ async function loadGroup(selected) { const p = document.createElement("button"); p.classList.add("group-member-print"); p.innerHTML = "🖶"; - p.addEventListener("click", () => printNote(li) ); + p.addEventListener("click", () => printNote(li)); li.appendChild(p); ul.appendChild(li); @@ -177,7 +175,7 @@ async function loadGroup(selected) { const graduations = await Promise.all(graduationPromises); const observations = graduations.filter( - (g) => g.observationRequested || (g.observation && !g.observation.end) + (g) => g.observationRequested || (g.observation && !g.observation.end), ); const observationReuqestsContainer = document.getElementById("observation-requests-container"); @@ -234,7 +232,10 @@ async function loadGroup(selected) { startBtn.classList.add("start-observation"); } else { startBtn.addEventListener("click", () => { - window.open(`/mod/page/view.php?id=93017047&userToLoad=${graduation.student.id}`, '_blank'); + window.open( + `/mod/page/view.php?id=93017047&userToLoad=${graduation.student.id}`, + "_blank", + ); }); startBtn.innerHTML = `Graduierung abschließen`; startBtn.classList.add("end-observation"); @@ -256,50 +257,55 @@ function submitMassGraduation() { // TODO(muhq): await graduation promises const promises = []; - for(const li of document.querySelectorAll("li.group-member")) { + for (const li of document.querySelectorAll("li.group-member")) { const reason = li.querySelector(".group-member-point-reason").value; const student = li.student; if (li.querySelector(".group-member-green").innerHTML === "X") { - promises.push(submitGraduationChange(student, GraduationChange.newGreenPointChange(reason, name))); + promises.push( + submitGraduationChange(student, GraduationChange.newGreenPointChange(reason, name)), + ); } if (li.querySelector(".group-member-orange").innerHTML === "X") { - promises.push(submitGraduationChange(student, GraduationChange.newOrangePointChange(reason, name))); + promises.push( + submitGraduationChange(student, GraduationChange.newOrangePointChange(reason, name)), + ); } if (li.querySelector(".group-member-red").innerHTML === "X") { - promises.push(submitGraduationChange(student, GraduationChange.newRedPointChange(reason, name))); + promises.push( + submitGraduationChange(student, GraduationChange.newRedPointChange(reason, name)), + ); } if (li.observationDiv) { const status = []; for (const row of li.observationDiv.querySelectorAll("tbody > tr")) { - if (row.querySelector('td[data-yes="true"]').innerHTML === "X") - status.push("yes"); - else if (row.querySelector('td[data-no="true"]').innerHTML === "X") - status.push("no"); - else - status.push(""); + if (row.querySelector('td[data-yes="true"]').innerHTML === "X") status.push("yes"); + else if (row.querySelector('td[data-no="true"]').innerHTML === "X") status.push("no"); + else status.push(""); } promises.push(submitObservation(student, new Observation(new Date(), status))); } } - Promise.all(promises).then(() => { - state.style.display = "inline"; - state.innerHTML = "✓"; - state.style.color = "green"; - for (const li of document.querySelectorAll("li.group-member")) { - if (!li.observationDiv) - continue; - li.observationDiv.remove(); - li.observationDiv = null; - } - }).catch((error) => { - state.style.display = "inline"; - state.innerHTML = "X"; - state.style.color = "red"; - console.log(error); - }).finally(() => { - state.classList.remove("submit-mass-graduation-inprogress"); - }); + Promise.all(promises) + .then(() => { + state.style.display = "inline"; + state.innerHTML = "✓"; + state.style.color = "green"; + for (const li of document.querySelectorAll("li.group-member")) { + if (!li.observationDiv) continue; + li.observationDiv.remove(); + li.observationDiv = null; + } + }) + .catch((error) => { + state.style.display = "inline"; + state.innerHTML = "X"; + state.style.color = "red"; + console.log(error); + }) + .finally(() => { + state.classList.remove("submit-mass-graduation-inprogress"); + }); } let prePrintBody = null; @@ -342,7 +348,9 @@ async function init() { initSelection( "group-selection", - async function() { return (await fetchGroups()).values(); }, + async function () { + return (await fetchGroups()).values(); + }, (group) => `${group.name}`, loadGroup, ); @@ -353,4 +361,3 @@ if (document.readyState === "loading") { } else { init(); } - diff --git a/src/mock.ts b/src/mock.ts index 220b1e3..5afe1db 100644 --- a/src/mock.ts +++ b/src/mock.ts @@ -3,9 +3,9 @@ import { Student } from "./student-common.ts"; import { buildGroups } from "./group.ts"; const students = [ - new Student(1, 'foo bar', 'test'), - new Student(2, 'maxi mustermensch', 'test'), - new Student(3, 'b b b', '5a') + new Student(1, "foo bar", "test"), + new Student(2, "maxi mustermensch", "test"), + new Student(3, "b b b", "5a"), ]; // Build the groups from our mock students @@ -16,7 +16,7 @@ async function fetchStudentsMock(_id?: number, _name?: string, _group?: string) } async function fetchSubjectsMock() { - return ['Mathe', 'Deutsch', 'Englisch']; + return ["Mathe", "Deutsch", "Englisch"]; } function fetchProgressMock(subject: string) { @@ -36,23 +36,30 @@ function fetchProgressMock(subject: string) { const graduations = new Map(); for (const s of students) { switch (s.name) { - case "foo bar": - graduations.set(s, Graduation.from(s, [ - GraduationChange.newGreenPointChange("Gute Arbeit", "Florian Fischer"), - GraduationChange.newOrangePointChange("", "Florian Fischer"), - ])); - break; - case "maxi mustermensch": - graduations.set(s, Graduation.from(s, [ - GraduationChange.newGreenPointChange("", "Florian Fischer"), - GraduationChange.newGreenPointChange("", "Florian Fischer"), - new GraduationChange(new Date(), s.id, "ObservationStart", "", "Florian Fischer", ""), - ])); - break; - case "b b b": - graduations.set(s, Graduation.from(s, [ - GraduationChange.newRedPointChange("", "Florian Fischer"), - ])); + case "foo bar": + graduations.set( + s, + Graduation.from(s, [ + GraduationChange.newGreenPointChange("Gute Arbeit", "Florian Fischer"), + GraduationChange.newOrangePointChange("", "Florian Fischer"), + ]), + ); + break; + case "maxi mustermensch": + graduations.set( + s, + Graduation.from(s, [ + GraduationChange.newGreenPointChange("", "Florian Fischer"), + GraduationChange.newGreenPointChange("", "Florian Fischer"), + new GraduationChange(new Date(), s.id, "ObservationStart", "", "Florian Fischer", ""), + ]), + ); + break; + case "b b b": + graduations.set( + s, + Graduation.from(s, [GraduationChange.newRedPointChange("", "Florian Fischer")]), + ); } } diff --git a/src/selection.js b/src/selection.js index bfaebc0..b7968af 100644 --- a/src/selection.js +++ b/src/selection.js @@ -27,7 +27,7 @@ async function initSelection(idPrefix, fetchCandidates, fmtCandidate, onSelectio let currentFocus = -1; let visibleItems = []; function setActive() { - visibleItems.forEach(item => item.classList.remove("active")); + visibleItems.forEach((item) => item.classList.remove("active")); if (currentFocus >= 0 && visibleItems[currentFocus]) { visibleItems[currentFocus].classList.add("active"); } @@ -37,11 +37,11 @@ async function initSelection(idPrefix, fetchCandidates, fmtCandidate, onSelectio const item = document.createElement("li"); item.candidate = candidate; item.innerHTML = fmtCandidate(candidate); - item.style.display = 'none'; + item.style.display = "none"; - item.addEventListener('click', function() { + item.addEventListener("click", function () { sel.value = ""; - sel.dispatchEvent(new Event('keyup')); + sel.dispatchEvent(new Event("keyup")); onSelection(candidate); }); @@ -49,9 +49,8 @@ async function initSelection(idPrefix, fetchCandidates, fmtCandidate, onSelectio items.push(item); } - sel.addEventListener('keyup', function(e) { - if ((["ArrowDown", "ArrowUp", "Enter"]).includes(e.key)) - return; + sel.addEventListener("keyup", function (e) { + if (["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) return; const filter = sel.value.toLowerCase(); visibleItems = []; @@ -63,10 +62,10 @@ async function initSelection(idPrefix, fetchCandidates, fmtCandidate, onSelectio visibleItems.push(item); // Highlight matched text - const regex = new RegExp(`(${filter})`, 'gi'); + const regex = new RegExp(`(${filter})`, "gi"); item.innerHTML = item.textContent.replace( regex, - "<u class='student-selection-list-highlight'>$1</u>" + "<u class='student-selection-list-highlight'>$1</u>", ); } else { item.style.display = "none"; @@ -82,26 +81,23 @@ async function initSelection(idPrefix, fetchCandidates, fmtCandidate, onSelectio noResults.style.display = visibleItems.length || filter === "" ? "none" : "block"; }); - clearBtn.addEventListener('click', function() { + clearBtn.addEventListener("click", function () { sel.value = ""; - sel.dispatchEvent(new Event('keyup')); // reset list + sel.dispatchEvent(new Event("keyup")); // reset list sel.focus(); }); - sel.addEventListener('keydown', function(e) { + sel.addEventListener("keydown", function (e) { if (e.key === "ArrowDown" || e.key === "ArrowUp") { - if(e.key === "ArrowDown") - currentFocus = (currentFocus + 1) % visibleItems.length; - else - currentFocus = (currentFocus - 1 + visibleItems.length) % visibleItems.length; + if (e.key === "ArrowDown") currentFocus = (currentFocus + 1) % visibleItems.length; + else currentFocus = (currentFocus - 1 + visibleItems.length) % visibleItems.length; setActive(); e.preventDefault(); } else if (e.key === "Enter") { if (currentFocus > -1 && visibleItems[currentFocus]) { - const visibleItem = visibleItems[currentFocus]; sel.value = ""; - sel.dispatchEvent(new Event('keyup')); + sel.dispatchEvent(new Event("keyup")); onSelection(visibleItem.candidate); } e.preventDefault(); diff --git a/src/student-mock.ts b/src/student-mock.ts index 93b45b0..bf817d2 100644 --- a/src/student-mock.ts +++ b/src/student-mock.ts @@ -3,20 +3,19 @@ import { Student } from "./student-common.ts"; import { fetchStudentsMock as fetchStudents, fetchGraduationMock } from "./mock.ts"; declare module "./student-common.ts" { - interface Student { - getGraduation(): Promise<Graduation>; - getIconUrl(): Promise<string>; - } + interface Student { + getGraduation(): Promise<Graduation>; + getIconUrl(): Promise<string>; + } } Student.prototype.getGraduation = function () { - return fetchGraduationMock(this); + return fetchGraduationMock(this); }; Student.prototype.getIconUrl = async function () { - return ""; + return ""; }; - async function fetchStudent(_idFilter?: number, _nameFilter?: string, _groupFilter?: string) { const students = await fetchStudents(); return students[0]; diff --git a/src/student-prod.ts b/src/student-prod.ts index 24ac869..67379d9 100644 --- a/src/student-prod.ts +++ b/src/student-prod.ts @@ -1,20 +1,20 @@ import type { Graduation } from "./graduation-common.ts"; import { Student } from "./student-common.ts"; -import { getUserIconUrl, getCourseParticipantsUrl } from "./utils-prod.js" +import { getUserIconUrl, getCourseParticipantsUrl } from "./utils-prod.js"; import { fetchGraduation } from "./graduation-prod.ts"; declare module "./student-common.ts" { - interface Student { - getGraduation(): Promise<Graduation>; - getIconUrl(): Promise<string>; - } + interface Student { + getGraduation(): Promise<Graduation>; + getIconUrl(): Promise<string>; + } } Student.prototype.getGraduation = function () { - return fetchGraduation(this); + return fetchGraduation(this); }; Student.prototype.getIconUrl = function () { - return getUserIconUrl(this.id); + return getUserIconUrl(this.id); }; // Cache the fetched students. @@ -26,9 +26,8 @@ async function _fetchStudents() { const doc = new DOMParser().parseFromString(await res.text(), "text/html"); const table = doc.querySelector("tbody")!; const fetchedStudents: Array<Student> = []; - table.querySelectorAll("tr").forEach(row => { - if (row.classList.contains("emptyrow")) - return; + table.querySelectorAll("tr").forEach((row) => { + if (row.classList.contains("emptyrow")) return; const roleCell = row.querySelector(".c4")!; // TODO(muhq): Is this locale dependent? @@ -39,10 +38,8 @@ async function _fetchStudents() { const nameCell = row.querySelector<HTMLElement>(".c1")!; let name; const c = nameCell.firstChild!.firstChild! as HTMLElement; - if (c.tagName == "SPAN") - name = c.title; - else - name = nameCell.innerText; + if (c.tagName == "SPAN") name = c.title; + else name = nameCell.innerText; const profileUrl = nameCell.querySelector<HTMLAnchorElement>("a.aabtn")!.href; const match = idRegex.exec(profileUrl); @@ -76,10 +73,12 @@ async function fetchStudents(idFilter?: number, nameFilter?: string, groupFilter } } - return students.filter((s) => - (typeof idFilter === "undefined" || s.id === idFilter) && - (typeof nameFilter === "undefined" || s.name.includes(nameFilter)) && - (typeof groupFilter === "undefined" || s.group.includes(groupFilter))); + return students.filter( + (s) => + (typeof idFilter === "undefined" || s.id === idFilter) && + (typeof nameFilter === "undefined" || s.name.includes(nameFilter)) && + (typeof groupFilter === "undefined" || s.group.includes(groupFilter)), + ); } async function fetchStudent(idFilter: number, nameFilter: string, groupFilter: string) { diff --git a/src/utils-common.ts b/src/utils-common.ts index f3ad944..8053043 100644 --- a/src/utils-common.ts +++ b/src/utils-common.ts @@ -1,8 +1,18 @@ "use strict"; -export const DATE_FMT = new Intl.DateTimeFormat("de-DE", { month: "numeric", day: "numeric", year: "numeric"}); +export const DATE_FMT = new Intl.DateTimeFormat("de-DE", { + month: "numeric", + day: "numeric", + year: "numeric", +}); -export const DATETIME_FMT = new Intl.DateTimeFormat("de-DE", { month: "numeric", day: "numeric", year: "numeric", hour: "numeric", minute: "numeric" }); +export const DATETIME_FMT = new Intl.DateTimeFormat("de-DE", { + month: "numeric", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "numeric", +}); export function dateStringNow() { return DATE_FMT.format(new Date()); @@ -14,8 +24,8 @@ export function datetimeStringNow() { // TODO(muhq): Cache the retrieved URL export async function getUserIconUrl(userId: number): Promise<string> { - const userRes = await fetch(`https://lernplattform.bycs.de/user/profile.php?id=${userId}`) - const userDoc = new DOMParser().parseFromString(await userRes.text(), 'text/html') + const userRes = await fetch(`https://lernplattform.bycs.de/user/profile.php?id=${userId}`); + const userDoc = new DOMParser().parseFromString(await userRes.text(), "text/html"); const userpicture = userDoc.querySelector(".userpicture"); if (userpicture === null) { return ""; diff --git a/src/utils-mock.js b/src/utils-mock.js index ab87ddc..9694500 100644 --- a/src/utils-mock.js +++ b/src/utils-mock.js @@ -4,11 +4,11 @@ export { DATETIME_FMT, dateStringNow, datetimeStringNow, - getUserIconUrl + getUserIconUrl, } from "./utils-common.ts"; export function isTeacher() { - return !(new URLSearchParams(document.location.search)).has("student"); + return !new URLSearchParams(document.location.search).has("student"); } export function getFirstAndLastname() { @@ -16,10 +16,8 @@ export function getFirstAndLastname() { let lastname = ""; const params = new URLSearchParams(document.location.search); - if (params.has("firstname")) - firstname = params.get("firstname"); - if (params.has("lastname")) - lastname = params.get("lastname"); + if (params.has("firstname")) firstname = params.get("firstname"); + if (params.has("lastname")) lastname = params.get("lastname"); return [firstname, lastname]; } @@ -28,10 +26,8 @@ export function getName() { let name = ""; const params = new URLSearchParams(document.location.search); - if (params.has("firstname")) - name = params.get("firstname"); - if (params.has("lastname")) - name += params.get("lastname"); + if (params.has("firstname")) name = params.get("firstname"); + if (params.has("lastname")) name += params.get("lastname"); return name; } @@ -39,7 +35,7 @@ export function getName() { export function getUserId() { const name = getName(); - return fetchStudentsMock.filter((s) => s.name === name )[0].id; + return fetchStudentsMock.filter((s) => s.name === name)[0].id; } export function getCourseId() { @@ -50,5 +46,3 @@ export function getCourseParticipantsUrl() { console.error("Try to get mocked participations URL"); return ""; } - - diff --git a/src/utils-prod.js b/src/utils-prod.js index f7c89e4..94d724a 100644 --- a/src/utils-prod.js +++ b/src/utils-prod.js @@ -3,12 +3,14 @@ export { DATETIME_FMT, dateStringNow, datetimeStringNow, - getUserIconUrl + getUserIconUrl, } from "./utils-common.ts"; export function isTeacher() { - return Boolean(document.querySelector('.editmode-switch-form')) - || document.body.classList.contains('editing'); + return ( + Boolean(document.querySelector(".editmode-switch-form")) || + document.body.classList.contains("editing") + ); } export function getFirstAndLastname() { @@ -24,7 +26,8 @@ export function getFirstAndLastname() { } export function getName() { - return document.querySelector("bycs-topbar").shadowRoot.querySelector("div.user-name").textContent; + return document.querySelector("bycs-topbar").shadowRoot.querySelector("div.user-name") + .textContent; } export function getUserId() { @@ -40,4 +43,3 @@ export function getCourseId() { export function getCourseParticipantsUrl() { return `https://lernplattform.bycs.de/user/index.php?id=${getCourseId()}&perpage=5000`; } - diff --git a/tsconfig.json b/tsconfig.json index 8bc3dac..802bcd4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,14 @@ { - "compilerOptions": { - "noImplicitAny": true, - "target": "esnext", - "module": "esnext", - "moduleResolution": "bundler", - "esModuleInterop": true, - "allowJs": true, - "allowImportingTsExtensions": true, - "strict": true - }, - "include": ["src/**/*.ts", "src/**/*.js", "src/untyped.d.ts"], - "exclude": ["node_modules"] -}
\ No newline at end of file + "compilerOptions": { + "noImplicitAny": true, + "target": "esnext", + "module": "esnext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "allowJs": true, + "allowImportingTsExtensions": true, + "strict": true + }, + "include": ["src/**/*.ts", "src/**/*.js", "src/untyped.d.ts"], + "exclude": ["node_modules"] +} diff --git a/webpack.config.js b/webpack.config.js index 968fd2e..3e947d6 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,74 +1,65 @@ -const webpack = require('webpack'); -const path = require('path'); +const webpack = require("webpack"); +const path = require("path"); -const CircularDependencyPlugin = require('circular-dependency-plugin') +const CircularDependencyPlugin = require("circular-dependency-plugin"); -const isProduction = process.env.NODE_ENV === 'production'; -const isMock = process.env.NODE_ENV === 'mock'; +const isProduction = process.env.NODE_ENV === "production"; +const isMock = process.env.NODE_ENV === "mock"; const config = { - entry: { - 'cockpit': './src/cockpit.js', - 'mass-graduation': './src/mass-graduation.js', - 'admin-tools': './src/admin-tools.js', - }, - output: { - path: path.resolve(__dirname, 'dist'), - }, - module: { - rules: [ - { - test: /\.(js|ts)$/, - exclude: /node_modules/, - use: { - loader: 'babel-loader', - options: { - presets: [ - "@babel/preset-env", - "@babel/preset-typescript" - ], - } - } - }, - { - test: /\.png/, - type: 'asset/inline', - }, - ], - }, - plugins: [ - new webpack.NormalModuleReplacementPlugin( - /-ENV.(js|ts)/, - (resource) => { - resource.request = resource.request.replace( - /-ENV/, - `-${isMock ? "mock" : "prod"}`, - ); + entry: { + cockpit: "./src/cockpit.js", + "mass-graduation": "./src/mass-graduation.js", + "admin-tools": "./src/admin-tools.js", + }, + output: { + path: path.resolve(__dirname, "dist"), + }, + module: { + rules: [ + { + test: /\.(js|ts)$/, + exclude: /node_modules/, + use: { + loader: "babel-loader", + options: { + presets: ["@babel/preset-env", "@babel/preset-typescript"], + }, + }, + }, + { + test: /\.png/, + type: "asset/inline", + }, + ], + }, + plugins: [ + new webpack.NormalModuleReplacementPlugin(/-ENV.(js|ts)/, (resource) => { + resource.request = resource.request.replace(/-ENV/, `-${isMock ? "mock" : "prod"}`); - if (resource.createData) { - resource.createData.request = resource.request; - } - } - ), - new CircularDependencyPlugin({ - // exclude detection of files based on a RegExp - exclude: /node_modules/, - // add errors to webpack instead of warnings - failOnError: true, - // allow import cycles that include an asyncronous import, - // e.g. via import(/* webpackMode: "weak" */ './file.js') - allowAsyncCycles: false, - // set the current working directory for displaying module paths - cwd: process.cwd(), - }) - ], + if (resource.createData) { + resource.createData.request = resource.request; + } + }), + new CircularDependencyPlugin({ + // exclude detection of files based on a RegExp + exclude: /node_modules/, + // add errors to webpack instead of warnings + failOnError: true, + // allow import cycles that include an asyncronous import, + // e.g. via import(/* webpackMode: "weak" */ './file.js') + allowAsyncCycles: false, + // set the current working directory for displaying module paths + cwd: process.cwd(), + }), + ], }; module.exports = () => { - if (isProduction) { - config.mode = 'production'; - } else { - config.mode = 'development'; - } - return config; + if (isProduction) { + config.mode = "production"; + } else { + config.mode = "development"; + } + return config; }; |
