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 { getAllPluginSettings } from "@/plugins"
|
||||||
|
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage"
|
||||||
import type { BooleanSetting, StringSetting, NumberSetting, SelectSetting, ButtonSetting, HotkeySetting, ComponentSetting } from "@/plugins/core/types"
|
import type { BooleanSetting, StringSetting, NumberSetting, SelectSetting, ButtonSetting, HotkeySetting, ComponentSetting } from "@/plugins/core/types"
|
||||||
|
|
||||||
// Union type representing all possible settings
|
// Union type representing all possible settings
|
||||||
@@ -79,7 +80,9 @@
|
|||||||
settings: Record<string, SettingType>;
|
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>>>({});
|
const pluginSettingsValues = $state<Record<string, Record<string, any>>>({});
|
||||||
|
|
||||||
let cloudState = $state(cloudAuth.state);
|
let cloudState = $state(cloudAuth.state);
|
||||||
|
|||||||
@@ -1,13 +1,4 @@
|
|||||||
const ENGAGE_STUDENT_STORAGE_KEY = () =>
|
import { getEngageAssessmentStudentId } from "@/seqta/utils/engageAssessmentStudent";
|
||||||
`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());
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomEngagePdfFileName(): string {
|
function randomEngagePdfFileName(): string {
|
||||||
const token = Math.random().toString(36).slice(2, 10);
|
const token = Math.random().toString(36).slice(2, 10);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { getUserInfo } from "@/seqta/ui/AddBetterSEQTAElements.ts";
|
import { getUserInfo } from "@/seqta/ui/AddBetterSEQTAElements.ts";
|
||||||
import ReactFiber from "@/seqta/utils/ReactFiber.ts";
|
import ReactFiber from "@/seqta/utils/ReactFiber.ts";
|
||||||
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
|
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
|
||||||
|
import { getEngageAssessmentStudentId } from "@/seqta/utils/engageAssessmentStudent";
|
||||||
import {
|
import {
|
||||||
getEngageAssessmentReportUrl,
|
getEngageAssessmentReportUrl,
|
||||||
getEngageAssessmentStudentId,
|
|
||||||
requestEngageAssessmentPdf,
|
requestEngageAssessmentPdf,
|
||||||
} from "./engage.ts";
|
} from "./engage.ts";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { determineStatus, formatDate, getGradeValue } from "./utils";
|
import { determineStatus, formatDate, getGradeValue } from "./utils";
|
||||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
|
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
|
||||||
|
import { buildEngageAssessmentPagePath } from "@/seqta/utils/engageAssessmentStudent";
|
||||||
import confetti from "canvas-confetti";
|
import confetti from "canvas-confetti";
|
||||||
|
|
||||||
export let data: any;
|
export let data: any;
|
||||||
|
|
||||||
interface FilterOptions {
|
interface FilterOptions {
|
||||||
subject: string;
|
subject: string;
|
||||||
|
student: string;
|
||||||
sortBy: "due" | "grade" | "subject" | "title" | "year";
|
sortBy: "due" | "grade" | "subject" | "title" | "year";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,9 +41,13 @@
|
|||||||
|
|
||||||
let currentFilters: FilterOptions = {
|
let currentFilters: FilterOptions = {
|
||||||
subject: "all",
|
subject: "all",
|
||||||
|
student: "all",
|
||||||
sortBy: "due",
|
sortBy: "due",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isEngage = isSeqtaEngageExperience();
|
||||||
|
$: showStudentFilter = isEngage && (data?.students?.length ?? 0) > 1;
|
||||||
|
|
||||||
let filteredAssessments: any[] = [];
|
let filteredAssessments: any[] = [];
|
||||||
let statusGroups: Record<string, any[]> = {};
|
let statusGroups: Record<string, any[]> = {};
|
||||||
let columns: { key: string; title: string; className: string; icon: string }[] = [];
|
let columns: { key: string; title: string; className: string; icon: string }[] = [];
|
||||||
@@ -100,7 +107,17 @@
|
|||||||
const filtered = data.assessments.filter((a: any) => {
|
const filtered = data.assessments.filter((a: any) => {
|
||||||
if (hiddenAssessmentIds.has(String(a.id))) return false;
|
if (hiddenAssessmentIds.has(String(a.id))) return false;
|
||||||
if (subjectFilters[a.code] === false) 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[]> = {};
|
const groups: Record<string, any[]> = {};
|
||||||
@@ -309,6 +326,19 @@
|
|||||||
if ((event.target as HTMLElement).closest(".card-menu")) {
|
if ((event.target as HTMLElement).closest(".card-menu")) {
|
||||||
return;
|
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}`;
|
window.location.hash = `#?page=/assessments/${assessment.programmeID}:${assessment.metaclassID}&item=${assessment.id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,6 +372,7 @@
|
|||||||
updateAssessments();
|
updateAssessments();
|
||||||
void currentFilters.sortBy;
|
void currentFilters.sortBy;
|
||||||
void currentFilters.subject;
|
void currentFilters.subject;
|
||||||
|
void currentFilters.student;
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -352,6 +383,14 @@
|
|||||||
<div class="grid-view-header">
|
<div class="grid-view-header">
|
||||||
<h1 class="grid-view-title">Assessments</h1>
|
<h1 class="grid-view-title">Assessments</h1>
|
||||||
<div class="grid-view-filters">
|
<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}>
|
<select class="filter-select" bind:value={currentFilters.subject}>
|
||||||
<option value="all">All Subjects</option>
|
<option value="all">All Subjects</option>
|
||||||
{#each data.subjects as subject}
|
{#each data.subjects as subject}
|
||||||
@@ -445,6 +484,9 @@
|
|||||||
on:keydown={(e) => e.key === 'Enter' && handleCardClick(assessment, e)}
|
on:keydown={(e) => e.key === 'Enter' && handleCardClick(assessment, e)}
|
||||||
>
|
>
|
||||||
<div class="card-labels">
|
<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>
|
<span class="card-label label-subject">{assessment.code}</span>
|
||||||
{#if assessment.submitted}
|
{#if assessment.submitted}
|
||||||
<span class="card-label label-submitted" style="background: #10b981; color: white;">Submitted</span>
|
<span class="card-label label-submitted" style="background: #10b981; color: white;">Submitted</span>
|
||||||
|
|||||||
@@ -9,12 +9,17 @@ interface PrefItem {
|
|||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { getUserInfo } from "@/seqta/ui/AddBetterSEQTAElements";
|
||||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
import { getMockAssessmentsData } from "@/seqta/ui/dev/hideSensitiveContent";
|
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 CACHE_MS = 10 * 60 * 1000;
|
||||||
const student = 69;
|
|
||||||
|
|
||||||
async function fetchJSON(url: string, body: any) {
|
async function fetchJSON(url: string, body: any) {
|
||||||
const res = await fetch(`${location.origin}${url}`, {
|
const res = await fetch(`${location.origin}${url}`, {
|
||||||
@@ -58,7 +63,6 @@ async function loadUpcoming(student: number) {
|
|||||||
|
|
||||||
function normalizeAssessmentDates(t: any, subject: Subject): any {
|
function normalizeAssessmentDates(t: any, subject: Subject): any {
|
||||||
const normalized = { ...t };
|
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)) {
|
if (!normalized.due && (t.date || t.dueDate || t.created || t.submittedDate)) {
|
||||||
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;
|
return submissionMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAssessmentsData() {
|
async function getLearnAssessmentsData(studentId: number) {
|
||||||
if (settingsState.mockNotices) {
|
|
||||||
return getMockAssessmentsData();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cache && Date.now() - cache.time < CACHE_MS) return cache.data;
|
|
||||||
const [subjects, colors, upcoming] = await Promise.all([
|
const [subjects, colors, upcoming] = await Promise.all([
|
||||||
loadSubjects(),
|
loadSubjects(),
|
||||||
loadPrefs(student),
|
loadPrefs(studentId),
|
||||||
loadUpcoming(student),
|
loadUpcoming(studentId),
|
||||||
]);
|
]);
|
||||||
const pastMap = await loadPast(student, subjects);
|
const pastMap = await loadPast(studentId, subjects);
|
||||||
const map: Record<number, any> = {};
|
const map: Record<number, any> = {};
|
||||||
upcoming.forEach((a: any) => {
|
upcoming.forEach((a: any) => {
|
||||||
map[a.id] = { ...a };
|
map[a.id] = { ...a };
|
||||||
@@ -150,13 +149,42 @@ export async function getAssessmentsData() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const allAssessments = Object.values(map);
|
const allAssessments = Object.values(map);
|
||||||
const submissions = await loadSubmissions(student, allAssessments);
|
const submissions = await loadSubmissions(studentId, allAssessments);
|
||||||
|
|
||||||
allAssessments.forEach((assessment: any) => {
|
allAssessments.forEach((assessment: any) => {
|
||||||
assessment.submitted = submissions[assessment.id] || false;
|
assessment.submitted = submissions[assessment.id] || false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = { assessments: allAssessments, subjects, colors };
|
return { assessments: allAssessments, subjects, colors, studentId };
|
||||||
cache = { time: Date.now(), data };
|
}
|
||||||
|
|
||||||
|
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;
|
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 styles from "./styles.css?inline";
|
||||||
import { delay } from "@/seqta/utils/delay";
|
import { delay } from "@/seqta/utils/delay";
|
||||||
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
|
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<{}> = {
|
const assessmentsOverviewPlugin: Plugin<{}> = {
|
||||||
id: "assessments-overview",
|
id: "assessments-overview",
|
||||||
@@ -17,35 +57,46 @@ const assessmentsOverviewPlugin: Plugin<{}> = {
|
|||||||
styles,
|
styles,
|
||||||
|
|
||||||
run: async () => {
|
run: async () => {
|
||||||
if (isSeqtaEngageExperience()) return;
|
const menu = await waitForAssessmentsSubmenu();
|
||||||
|
|
||||||
const menu = (await waitForElm(
|
|
||||||
'[data-key="assessments"] > .sub > ul',
|
|
||||||
true,
|
|
||||||
100,
|
|
||||||
60,
|
|
||||||
)) as HTMLElement;
|
|
||||||
const gridItem = document.createElement("li");
|
const gridItem = document.createElement("li");
|
||||||
gridItem.className = "item";
|
gridItem.className = "item";
|
||||||
|
gridItem.classList.add(OVERVIEW_MENU_CLASS);
|
||||||
const label = document.createElement("label");
|
const label = document.createElement("label");
|
||||||
label.textContent = "Overview";
|
label.textContent = "Overview";
|
||||||
gridItem.appendChild(label);
|
gridItem.appendChild(label);
|
||||||
menu.insertBefore(gridItem, menu.children[1] || null);
|
menu.insertBefore(gridItem, menu.firstChild);
|
||||||
|
|
||||||
if (window.location.hash.includes("/assessments/overview")) {
|
const menuObserver = new MutationObserver(() => {
|
||||||
loadGridView();
|
ensureOverviewMenuPosition(menu, gridItem);
|
||||||
|
});
|
||||||
|
menuObserver.observe(menu, { childList: true });
|
||||||
|
|
||||||
|
if (isOverviewRoute()) {
|
||||||
|
void loadGridView();
|
||||||
}
|
}
|
||||||
|
|
||||||
const clickHandler = (e: Event) => {
|
const clickHandler = (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
loadGridView();
|
void loadGridView();
|
||||||
};
|
};
|
||||||
gridItem.addEventListener("click", clickHandler);
|
gridItem.addEventListener("click", clickHandler);
|
||||||
|
|
||||||
async function loadGridView() {
|
async function loadGridView() {
|
||||||
await delay(1);
|
await delay(1);
|
||||||
window.history.pushState({}, "", "/#?page=/assessments/overview");
|
|
||||||
document.title = "Overview ― SEQTA Learn";
|
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");
|
const main = document.getElementById("main");
|
||||||
if (!main) return;
|
if (!main) return;
|
||||||
|
|
||||||
@@ -79,6 +130,7 @@ const assessmentsOverviewPlugin: Plugin<{}> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
menuObserver.disconnect();
|
||||||
gridItem.removeEventListener("click", clickHandler);
|
gridItem.removeEventListener("click", clickHandler);
|
||||||
gridItem.remove();
|
gridItem.remove();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -245,6 +245,15 @@
|
|||||||
background: var(--subject-color, #d41e3a);
|
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 {
|
.card-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.75rem;
|
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