mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
feat: make assement overview for SEQTA Engage
This commit is contained in:
@@ -45,6 +45,7 @@
|
||||
}
|
||||
|
||||
import { getAllPluginSettings } from "@/plugins"
|
||||
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage"
|
||||
import type { BooleanSetting, StringSetting, NumberSetting, SelectSetting, ButtonSetting, HotkeySetting, ComponentSetting } from "@/plugins/core/types"
|
||||
|
||||
// Union type representing all possible settings
|
||||
@@ -79,7 +80,9 @@
|
||||
settings: Record<string, SettingType>;
|
||||
}
|
||||
|
||||
const pluginSettings = getAllPluginSettings() as Plugin[];
|
||||
const pluginSettings = getAllPluginSettings().filter(
|
||||
(plugin) => !(isSeqtaEngageExperience() && plugin.pluginId === "global-search"),
|
||||
) as Plugin[];
|
||||
const pluginSettingsValues = $state<Record<string, Record<string, any>>>({});
|
||||
|
||||
let cloudState = $state(cloudAuth.state);
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
const ENGAGE_STUDENT_STORAGE_KEY = () =>
|
||||
`bsplus.engageTimetable.student.${location.origin}`;
|
||||
|
||||
/** Engage assessments URLs: /#?page=/assessments/{studentId}/{programme}:{metaclass}:{studentId} */
|
||||
export function getEngageAssessmentStudentId(): string | null {
|
||||
const hashMatch = window.location.hash.match(/\/assessments\/(\d+)/);
|
||||
if (hashMatch?.[1]) return hashMatch[1];
|
||||
|
||||
return localStorage.getItem(ENGAGE_STUDENT_STORAGE_KEY());
|
||||
}
|
||||
import { getEngageAssessmentStudentId } from "@/seqta/utils/engageAssessmentStudent";
|
||||
|
||||
function randomEngagePdfFileName(): string {
|
||||
const token = Math.random().toString(36).slice(2, 10);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { getUserInfo } from "@/seqta/ui/AddBetterSEQTAElements.ts";
|
||||
import ReactFiber from "@/seqta/utils/ReactFiber.ts";
|
||||
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
|
||||
import { getEngageAssessmentStudentId } from "@/seqta/utils/engageAssessmentStudent";
|
||||
import {
|
||||
getEngageAssessmentReportUrl,
|
||||
getEngageAssessmentStudentId,
|
||||
requestEngageAssessmentPdf,
|
||||
} from "./engage.ts";
|
||||
import {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { determineStatus, formatDate, getGradeValue } from "./utils";
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
|
||||
import { buildEngageAssessmentPagePath } from "@/seqta/utils/engageAssessmentStudent";
|
||||
import confetti from "canvas-confetti";
|
||||
|
||||
export let data: any;
|
||||
|
||||
interface FilterOptions {
|
||||
subject: string;
|
||||
student: string;
|
||||
sortBy: "due" | "grade" | "subject" | "title" | "year";
|
||||
}
|
||||
|
||||
@@ -38,9 +41,13 @@
|
||||
|
||||
let currentFilters: FilterOptions = {
|
||||
subject: "all",
|
||||
student: "all",
|
||||
sortBy: "due",
|
||||
};
|
||||
|
||||
const isEngage = isSeqtaEngageExperience();
|
||||
$: showStudentFilter = isEngage && (data?.students?.length ?? 0) > 1;
|
||||
|
||||
let filteredAssessments: any[] = [];
|
||||
let statusGroups: Record<string, any[]> = {};
|
||||
let columns: { key: string; title: string; className: string; icon: string }[] = [];
|
||||
@@ -100,7 +107,17 @@
|
||||
const filtered = data.assessments.filter((a: any) => {
|
||||
if (hiddenAssessmentIds.has(String(a.id))) return false;
|
||||
if (subjectFilters[a.code] === false) return false;
|
||||
return currentFilters.subject === "all" || a.code === currentFilters.subject;
|
||||
if (currentFilters.subject !== "all" && a.code !== currentFilters.subject) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
isEngage &&
|
||||
currentFilters.student !== "all" &&
|
||||
String(a.studentId) !== currentFilters.student
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const groups: Record<string, any[]> = {};
|
||||
@@ -309,6 +326,19 @@
|
||||
if ((event.target as HTMLElement).closest(".card-menu")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSeqtaEngageExperience()) {
|
||||
const studentId = assessment.studentId ?? data?.studentId;
|
||||
if (!studentId) return;
|
||||
window.location.hash = buildEngageAssessmentPagePath(
|
||||
studentId,
|
||||
assessment.programmeID,
|
||||
assessment.metaclassID,
|
||||
assessment.id,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.hash = `#?page=/assessments/${assessment.programmeID}:${assessment.metaclassID}&item=${assessment.id}`;
|
||||
}
|
||||
|
||||
@@ -342,6 +372,7 @@
|
||||
updateAssessments();
|
||||
void currentFilters.sortBy;
|
||||
void currentFilters.subject;
|
||||
void currentFilters.student;
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -352,6 +383,14 @@
|
||||
<div class="grid-view-header">
|
||||
<h1 class="grid-view-title">Assessments</h1>
|
||||
<div class="grid-view-filters">
|
||||
{#if showStudentFilter}
|
||||
<select class="filter-select" bind:value={currentFilters.student}>
|
||||
<option value="all">All Students</option>
|
||||
{#each data.students as student}
|
||||
<option value={String(student.id)}>{student.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
<select class="filter-select" bind:value={currentFilters.subject}>
|
||||
<option value="all">All Subjects</option>
|
||||
{#each data.subjects as subject}
|
||||
@@ -445,6 +484,9 @@
|
||||
on:keydown={(e) => e.key === 'Enter' && handleCardClick(assessment, e)}
|
||||
>
|
||||
<div class="card-labels">
|
||||
{#if isEngage && assessment.studentName}
|
||||
<span class="card-label label-student">{assessment.studentName}</span>
|
||||
{/if}
|
||||
<span class="card-label label-subject">{assessment.code}</span>
|
||||
{#if assessment.submitted}
|
||||
<span class="card-label label-submitted" style="background: #10b981; color: white;">Submitted</span>
|
||||
|
||||
@@ -9,12 +9,17 @@ interface PrefItem {
|
||||
value: string;
|
||||
}
|
||||
|
||||
import { getUserInfo } from "@/seqta/ui/AddBetterSEQTAElements";
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||
import { getMockAssessmentsData } from "@/seqta/ui/dev/hideSensitiveContent";
|
||||
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
|
||||
import {
|
||||
getEngageAssessmentsData,
|
||||
} from "./engageApi";
|
||||
|
||||
let cache: { time: number; data: any } | null = null;
|
||||
let cache: { time: number; engageAll?: boolean; studentId: number; data: any } | null =
|
||||
null;
|
||||
const CACHE_MS = 10 * 60 * 1000;
|
||||
const student = 69;
|
||||
|
||||
async function fetchJSON(url: string, body: any) {
|
||||
const res = await fetch(`${location.origin}${url}`, {
|
||||
@@ -58,7 +63,6 @@ async function loadUpcoming(student: number) {
|
||||
|
||||
function normalizeAssessmentDates(t: any, subject: Subject): any {
|
||||
const normalized = { ...t };
|
||||
// Past API may use different date fields - ensure we have 'due' for year filter & display
|
||||
if (!normalized.due && (t.date || t.dueDate || t.created || t.submittedDate)) {
|
||||
normalized.due = t.date || t.dueDate || t.created || t.submittedDate;
|
||||
}
|
||||
@@ -128,18 +132,13 @@ async function loadSubmissions(student: number, assessments: any[]) {
|
||||
return submissionMap;
|
||||
}
|
||||
|
||||
export async function getAssessmentsData() {
|
||||
if (settingsState.mockNotices) {
|
||||
return getMockAssessmentsData();
|
||||
}
|
||||
|
||||
if (cache && Date.now() - cache.time < CACHE_MS) return cache.data;
|
||||
async function getLearnAssessmentsData(studentId: number) {
|
||||
const [subjects, colors, upcoming] = await Promise.all([
|
||||
loadSubjects(),
|
||||
loadPrefs(student),
|
||||
loadUpcoming(student),
|
||||
loadPrefs(studentId),
|
||||
loadUpcoming(studentId),
|
||||
]);
|
||||
const pastMap = await loadPast(student, subjects);
|
||||
const pastMap = await loadPast(studentId, subjects);
|
||||
const map: Record<number, any> = {};
|
||||
upcoming.forEach((a: any) => {
|
||||
map[a.id] = { ...a };
|
||||
@@ -150,13 +149,42 @@ export async function getAssessmentsData() {
|
||||
});
|
||||
|
||||
const allAssessments = Object.values(map);
|
||||
const submissions = await loadSubmissions(student, allAssessments);
|
||||
const submissions = await loadSubmissions(studentId, allAssessments);
|
||||
|
||||
allAssessments.forEach((assessment: any) => {
|
||||
assessment.submitted = submissions[assessment.id] || false;
|
||||
});
|
||||
|
||||
const data = { assessments: allAssessments, subjects, colors };
|
||||
cache = { time: Date.now(), data };
|
||||
return { assessments: allAssessments, subjects, colors, studentId };
|
||||
}
|
||||
|
||||
export async function getAssessmentsData() {
|
||||
if (settingsState.mockNotices) {
|
||||
return getMockAssessmentsData();
|
||||
}
|
||||
|
||||
if (isSeqtaEngageExperience()) {
|
||||
if (cache && Date.now() - cache.time < CACHE_MS && cache.engageAll) {
|
||||
return cache.data;
|
||||
}
|
||||
|
||||
const data = await getEngageAssessmentsData();
|
||||
cache = { time: Date.now(), studentId: 0, engageAll: true, data };
|
||||
return data;
|
||||
}
|
||||
|
||||
const studentId = (await getUserInfo()).id;
|
||||
|
||||
if (
|
||||
cache &&
|
||||
Date.now() - cache.time < CACHE_MS &&
|
||||
cache.studentId === studentId
|
||||
) {
|
||||
return cache.data;
|
||||
}
|
||||
|
||||
const data = await getLearnAssessmentsData(studentId);
|
||||
|
||||
cache = { time: Date.now(), studentId, data };
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
import { getEngageAssessmentStudentId } from "@/seqta/utils/engageAssessmentStudent";
|
||||
|
||||
interface Subject {
|
||||
code: string;
|
||||
programme: number;
|
||||
metaclass: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface PrefItem {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface EngageStudent {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface EngageChildPayload {
|
||||
id?: number;
|
||||
name?: string;
|
||||
terms?: {
|
||||
active?: number;
|
||||
subjects?: {
|
||||
code?: string;
|
||||
programme?: number;
|
||||
metaclass?: number;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}[];
|
||||
}[];
|
||||
}
|
||||
|
||||
async function fetchJSON(url: string, body: unknown) {
|
||||
const res = await fetch(`${location.origin}${url}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function loadEngageChildrenPayload(): Promise<EngageChildPayload[]> {
|
||||
const res = await fetchJSON("/seqta/parent/load/subjects", {});
|
||||
return Array.isArray(res.payload) ? res.payload : [];
|
||||
}
|
||||
|
||||
export async function resolveEngageStudentId(): Promise<number> {
|
||||
const fromUrlOrStorage = getEngageAssessmentStudentId();
|
||||
if (fromUrlOrStorage) return Number(fromUrlOrStorage);
|
||||
|
||||
const children = await loadEngageChildrenPayload();
|
||||
const firstChild = children[0];
|
||||
if (firstChild?.id != null) return Number(firstChild.id);
|
||||
|
||||
throw new Error("Could not resolve Engage student ID");
|
||||
}
|
||||
|
||||
function subjectsFromChild(child: EngageChildPayload): Subject[] {
|
||||
return (child.terms ?? [])
|
||||
.filter((term) => term.active === 1)
|
||||
.flatMap((term) =>
|
||||
(term.subjects ?? []).map((subject) => ({
|
||||
code: subject.code ?? "",
|
||||
programme: subject.programme ?? 0,
|
||||
metaclass: subject.metaclass ?? 0,
|
||||
title: subject.title ?? subject.description ?? subject.code ?? "",
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
async function loadEngagePrefs(): Promise<Record<string, string>> {
|
||||
const res = await fetchJSON("/seqta/parent/load/prefs?", {
|
||||
request: "userPrefs",
|
||||
asArray: true,
|
||||
});
|
||||
|
||||
const colors: Record<string, string> = {};
|
||||
(res.payload ?? []).forEach((pref: PrefItem) => {
|
||||
if (pref.name.startsWith("timetable.subject.colour.")) {
|
||||
const code = pref.name.replace("timetable.subject.colour.", "");
|
||||
colors[code] = pref.value;
|
||||
}
|
||||
});
|
||||
return colors;
|
||||
}
|
||||
|
||||
async function loadEngageUpcoming(studentId: number) {
|
||||
const res = await fetchJSON("/seqta/parent/assessment/list/upcoming?", {
|
||||
student: studentId,
|
||||
});
|
||||
return res.payload ?? [];
|
||||
}
|
||||
|
||||
function normalizeAssessmentDates(t: any, subject: Subject): any {
|
||||
const normalized = { ...t };
|
||||
if (!normalized.due && (t.date || t.dueDate || t.created || t.submittedDate)) {
|
||||
normalized.due = t.date || t.dueDate || t.created || t.submittedDate;
|
||||
}
|
||||
if (!normalized.programmeID) normalized.programmeID = subject.programme;
|
||||
if (!normalized.metaclassID) normalized.metaclassID = subject.metaclass;
|
||||
if (!normalized.code && t.subject) normalized.code = t.subject;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function loadEngagePast(studentId: number, subjects: Subject[]) {
|
||||
const map: Record<number, any> = {};
|
||||
|
||||
await Promise.all(
|
||||
subjects.map(async (subject) => {
|
||||
const res = await fetchJSON("/seqta/parent/assessment/list/past?", {
|
||||
programme: subject.programme,
|
||||
metaclass: subject.metaclass,
|
||||
student: studentId,
|
||||
});
|
||||
|
||||
const processAssessment = (task: any) => {
|
||||
if (task?.id) {
|
||||
const merged = {
|
||||
...task,
|
||||
programmeID: task.programmeID || task.programme || subject.programme,
|
||||
metaclassID: task.metaclassID || task.metaclass || subject.metaclass,
|
||||
code: task.code || task.subject || subject.code,
|
||||
};
|
||||
map[task.id] = normalizeAssessmentDates(merged, subject);
|
||||
}
|
||||
};
|
||||
|
||||
if (Array.isArray(res.payload?.pending)) {
|
||||
res.payload.pending.forEach(processAssessment);
|
||||
}
|
||||
if (Array.isArray(res.payload?.tasks)) {
|
||||
res.payload.tasks.forEach(processAssessment);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
async function loadEngageSubmissions(studentId: number, assessments: any[]) {
|
||||
const submissionMap: Record<number, boolean> = {};
|
||||
|
||||
await Promise.all(
|
||||
assessments.map(async (assessment) => {
|
||||
try {
|
||||
const res = await fetchJSON("/seqta/parent/assessment/submissions/get", {
|
||||
assessment: assessment.id,
|
||||
metaclass: assessment.metaclassID,
|
||||
student: studentId,
|
||||
});
|
||||
submissionMap[assessment.id] =
|
||||
Array.isArray(res.payload) && res.payload.length > 0;
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[BetterSEQTA+] Failed to fetch Engage submission for assessment ${assessment.id}:`,
|
||||
error,
|
||||
);
|
||||
submissionMap[assessment.id] = false;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return submissionMap;
|
||||
}
|
||||
|
||||
async function loadEngageAssessmentsForStudent(
|
||||
child: EngageChildPayload,
|
||||
): Promise<any[]> {
|
||||
const studentId = Number(child.id);
|
||||
const studentName = child.name ?? "Student";
|
||||
const subjects = subjectsFromChild(child);
|
||||
const [upcoming, pastMap] = await Promise.all([
|
||||
loadEngageUpcoming(studentId),
|
||||
loadEngagePast(studentId, subjects),
|
||||
]);
|
||||
|
||||
const map: Record<number, any> = {};
|
||||
upcoming.forEach((assessment: any) => {
|
||||
map[assessment.id] = { ...assessment };
|
||||
});
|
||||
Object.values(pastMap).forEach((task: any) => {
|
||||
if (map[task.id]) Object.assign(map[task.id], task);
|
||||
else map[task.id] = task;
|
||||
});
|
||||
|
||||
const assessments = Object.values(map).map((assessment) => ({
|
||||
...assessment,
|
||||
studentId,
|
||||
studentName,
|
||||
}));
|
||||
|
||||
const submissions = await loadEngageSubmissions(studentId, assessments);
|
||||
assessments.forEach((assessment) => {
|
||||
assessment.submitted = submissions[assessment.id] || false;
|
||||
});
|
||||
|
||||
return assessments;
|
||||
}
|
||||
|
||||
export async function getEngageAssessmentsData() {
|
||||
const childrenPayload = await loadEngageChildrenPayload();
|
||||
const students: EngageStudent[] = childrenPayload
|
||||
.filter((child) => child.id != null)
|
||||
.map((child) => ({
|
||||
id: Number(child.id),
|
||||
name: child.name ?? "Student",
|
||||
}));
|
||||
|
||||
if (!students.length) {
|
||||
throw new Error("No Engage students found");
|
||||
}
|
||||
|
||||
const [colors, assessmentsByChild] = await Promise.all([
|
||||
loadEngagePrefs(),
|
||||
Promise.all(childrenPayload.map((child) => loadEngageAssessmentsForStudent(child))),
|
||||
]);
|
||||
|
||||
const subjectsMap = new Map<string, Subject>();
|
||||
childrenPayload.forEach((child) => {
|
||||
subjectsFromChild(child).forEach((subject) => {
|
||||
if (!subjectsMap.has(subject.code)) {
|
||||
subjectsMap.set(subject.code, subject);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const defaultStudentId = await resolveEngageStudentId();
|
||||
|
||||
return {
|
||||
assessments: assessmentsByChild.flat(),
|
||||
subjects: Array.from(subjectsMap.values()),
|
||||
colors,
|
||||
students,
|
||||
studentId: defaultStudentId,
|
||||
};
|
||||
}
|
||||
@@ -5,6 +5,46 @@ import { renderErrorState, renderGrid, renderSkeletonLoader } from "./ui";
|
||||
import styles from "./styles.css?inline";
|
||||
import { delay } from "@/seqta/utils/delay";
|
||||
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
|
||||
import {
|
||||
isEngageAssessmentOverviewRoute,
|
||||
} from "@/seqta/utils/engageAssessmentStudent";
|
||||
import { resolveEngageStudentId } from "./engageApi";
|
||||
|
||||
const OVERVIEW_MENU_CLASS = "betterseqta-assessments-overview-item";
|
||||
|
||||
function ensureOverviewMenuPosition(
|
||||
menu: HTMLElement,
|
||||
gridItem: HTMLElement,
|
||||
) {
|
||||
if (menu.firstElementChild !== gridItem) {
|
||||
menu.insertBefore(gridItem, menu.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
function isOverviewRoute() {
|
||||
if (isSeqtaEngageExperience()) {
|
||||
return isEngageAssessmentOverviewRoute();
|
||||
}
|
||||
return window.location.hash.includes("/assessments/overview");
|
||||
}
|
||||
|
||||
async function waitForAssessmentsSubmenu(): Promise<HTMLElement> {
|
||||
if (!isSeqtaEngageExperience()) {
|
||||
return (await waitForElm(
|
||||
'[data-key="assessments"] > .sub > ul',
|
||||
true,
|
||||
100,
|
||||
60,
|
||||
)) as HTMLElement;
|
||||
}
|
||||
|
||||
return (await waitForElm(
|
||||
'[data-key="assessments"] .sub ul, [data-key="assessments"] ul',
|
||||
true,
|
||||
100,
|
||||
350,
|
||||
)) as HTMLElement;
|
||||
}
|
||||
|
||||
const assessmentsOverviewPlugin: Plugin<{}> = {
|
||||
id: "assessments-overview",
|
||||
@@ -17,35 +57,46 @@ const assessmentsOverviewPlugin: Plugin<{}> = {
|
||||
styles,
|
||||
|
||||
run: async () => {
|
||||
if (isSeqtaEngageExperience()) return;
|
||||
|
||||
const menu = (await waitForElm(
|
||||
'[data-key="assessments"] > .sub > ul',
|
||||
true,
|
||||
100,
|
||||
60,
|
||||
)) as HTMLElement;
|
||||
const menu = await waitForAssessmentsSubmenu();
|
||||
const gridItem = document.createElement("li");
|
||||
gridItem.className = "item";
|
||||
gridItem.classList.add(OVERVIEW_MENU_CLASS);
|
||||
const label = document.createElement("label");
|
||||
label.textContent = "Overview";
|
||||
gridItem.appendChild(label);
|
||||
menu.insertBefore(gridItem, menu.children[1] || null);
|
||||
menu.insertBefore(gridItem, menu.firstChild);
|
||||
|
||||
if (window.location.hash.includes("/assessments/overview")) {
|
||||
loadGridView();
|
||||
const menuObserver = new MutationObserver(() => {
|
||||
ensureOverviewMenuPosition(menu, gridItem);
|
||||
});
|
||||
menuObserver.observe(menu, { childList: true });
|
||||
|
||||
if (isOverviewRoute()) {
|
||||
void loadGridView();
|
||||
}
|
||||
|
||||
const clickHandler = (e: Event) => {
|
||||
e.preventDefault();
|
||||
loadGridView();
|
||||
void loadGridView();
|
||||
};
|
||||
gridItem.addEventListener("click", clickHandler);
|
||||
|
||||
async function loadGridView() {
|
||||
await delay(1);
|
||||
|
||||
if (isSeqtaEngageExperience()) {
|
||||
const studentId = await resolveEngageStudentId();
|
||||
window.history.pushState(
|
||||
{},
|
||||
"",
|
||||
`/#?page=/assessments/${studentId}/overview`,
|
||||
);
|
||||
document.title = "Overview ― SEQTA Engage";
|
||||
} else {
|
||||
window.history.pushState({}, "", "/#?page=/assessments/overview");
|
||||
document.title = "Overview ― SEQTA Learn";
|
||||
}
|
||||
|
||||
const main = document.getElementById("main");
|
||||
if (!main) return;
|
||||
|
||||
@@ -79,6 +130,7 @@ const assessmentsOverviewPlugin: Plugin<{}> = {
|
||||
}
|
||||
|
||||
return () => {
|
||||
menuObserver.disconnect();
|
||||
gridItem.removeEventListener("click", clickHandler);
|
||||
gridItem.remove();
|
||||
};
|
||||
|
||||
@@ -245,6 +245,15 @@
|
||||
background: var(--subject-color, #d41e3a);
|
||||
}
|
||||
|
||||
.label-student {
|
||||
background: rgba(99, 102, 241, 0.9);
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.dark .label-student {
|
||||
background: rgba(99, 102, 241, 0.75);
|
||||
}
|
||||
|
||||
.card-menu {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
export const ENGAGE_STUDENT_STORAGE_KEY = () =>
|
||||
`bsplus.engageTimetable.student.${location.origin}`;
|
||||
|
||||
/** Engage assessments URLs: /#?page=/assessments/{studentId}/{programme}:{metaclass}:{studentId} */
|
||||
export function getEngageAssessmentStudentId(): string | null {
|
||||
const hashMatch = window.location.hash.match(/\/assessments\/(\d+)/);
|
||||
if (hashMatch?.[1]) return hashMatch[1];
|
||||
|
||||
return localStorage.getItem(ENGAGE_STUDENT_STORAGE_KEY());
|
||||
}
|
||||
|
||||
export function buildEngageAssessmentPagePath(
|
||||
studentId: string | number,
|
||||
programmeId: string | number,
|
||||
metaclassId: string | number,
|
||||
assessmentId?: string | number,
|
||||
): string {
|
||||
const base = `#?page=/assessments/${studentId}/${programmeId}:${metaclassId}:${studentId}`;
|
||||
return assessmentId != null ? `${base}&item=${assessmentId}` : base;
|
||||
}
|
||||
|
||||
export function buildEngageAssessmentOverviewPath(
|
||||
studentId: string | number,
|
||||
): string {
|
||||
return `#?page=/assessments/${studentId}/overview`;
|
||||
}
|
||||
|
||||
export function isEngageAssessmentOverviewRoute(hash = window.location.hash): boolean {
|
||||
return /\/assessments\/\d+\/overview/.test(hash);
|
||||
}
|
||||
Reference in New Issue
Block a user