From c02388750ca1d1ed6b1442a61c3024d1697f9fa4 Mon Sep 17 00:00:00 2001 From: StroepWafel Date: Fri, 26 Jun 2026 16:18:20 +0930 Subject: [PATCH] fix(upcoming assessments): max assessments option --- src/interface/pages/settings/general.svelte | 63 +++++++++ src/seqta/utils/Loaders/LoadHomePage.ts | 132 +++++++++++++++--- src/seqta/utils/defaultSettings.ts | 3 + .../utils/ensureSyncableStorageDefaults.ts | 2 + src/seqta/utils/listeners/StorageChanges.ts | 2 + src/types/storage.ts | 6 + 6 files changed, 192 insertions(+), 16 deletions(-) diff --git a/src/interface/pages/settings/general.svelte b/src/interface/pages/settings/general.svelte index a25b46bf..4f6f3776 100644 --- a/src/interface/pages/settings/general.svelte +++ b/src/interface/pages/settings/general.svelte @@ -289,6 +289,69 @@ {@render Setting(option)} {/each} +
+
+
+
+

Home Page Assessments

+

Limit upcoming assessments shown on the home page by subject

+
+
+
+
+

Include Past Assessments

+

Show past-due assessments from the upcoming list, matching the Assessments page

+
+
+ (settingsState.homeUpcomingIncludePast = isOn)} + /> +
+
+
+
+

Maximum Subjects

+

Number of subjects to include, ordered by soonest due date

+
+
+ (settingsState.homeUpcomingAssessmentsPerSubjectMax = Number(value))} + options={[ + { value: "0", label: "All" }, + { value: "1", label: "1" }, + { value: "2", label: "2" }, + { value: "3", label: "3" }, + { value: "5", label: "5" }, + { value: "10", label: "10" }, + ]} + /> +
+
+
+
+
diff --git a/src/seqta/utils/Loaders/LoadHomePage.ts b/src/seqta/utils/Loaders/LoadHomePage.ts index 6e66b2ce..a28e9f89 100644 --- a/src/seqta/utils/Loaders/LoadHomePage.ts +++ b/src/seqta/utils/Loaders/LoadHomePage.ts @@ -154,6 +154,55 @@ export async function loadHomePage() { return cleanup; } +let upcomingRefreshTimeout: ReturnType | null = null; + +/** Re-render the home page upcoming assessments block when related settings change. */ +export function refreshHomeUpcomingAssessments() { + if (!document.getElementById("upcoming-items")) return; + + if (upcomingRefreshTimeout) clearTimeout(upcomingRefreshTimeout); + upcomingRefreshTimeout = setTimeout(() => { + upcomingRefreshTimeout = null; + void renderHomeUpcomingAssessments(); + }, 150); +} + +async function renderHomeUpcomingAssessments() { + const upcomingItems = document.getElementById("upcoming-items"); + if (!upcomingItems) return; + + upcomingItems.classList.add("loading"); + upcomingItems.innerHTML = ""; + const filterContainer = document.getElementById("upcoming-filters"); + if (filterContainer) filterContainer.innerHTML = ""; + + const [assessments, classes] = await Promise.all([ + GetUpcomingAssessments(), + GetActiveClasses(), + ]); + + const activeSubjects = activeSubjectsFromLearnPayload(classes); + const currentAssessments = filterAssessmentsForActiveSubjects( + assessments, + activeSubjects, + ).sort(comparedate); + + await CreateUpcomingSection(currentAssessments, activeSubjects); + upcomingItems.classList.remove("loading"); +} + +export function registerHomeUpcomingSettingsListeners() { + const keys = [ + "homeUpcomingSubjectsMax", + "homeUpcomingAssessmentsPerSubjectMax", + "homeUpcomingIncludePast", + ] as const; + + for (const key of keys) { + settingsState.register(key, () => refreshHomeUpcomingAssessments()); + } +} + async function GetUpcomingAssessments() { try { return fetch(`${location.origin}/seqta/student/assessment/list/upcoming?`, { @@ -285,7 +334,13 @@ function debounce any>( } function comparedate(obj1: any, obj2: any) { - return obj1.date < obj2.date ? -1 : obj1.date > obj2.date ? 1 : 0; + const d1 = new Date(obj1.due || obj1.date || 0).getTime(); + const d2 = new Date(obj2.due || obj2.date || 0).getTime(); + return d1 - d2; +} + +function startOfDay(date: Date): Date { + return new Date(date.getFullYear(), date.getMonth(), date.getDate()); } function processNotices(response: any, labelArray: string[]) { const NoticeContainer = document.getElementById("notice-container"); @@ -894,21 +949,74 @@ function CheckUnmarkedAttendance(lessonattendance: any) { return lessonattendance ? lessonattendance.label : " "; } +function applyHomeUpcomingLimits(assessments: any[], activeSubjects: any[]) { + const maxSubjects = settingsState.homeUpcomingSubjectsMax ?? 5; + const maxPerSubject = settingsState.homeUpcomingAssessmentsPerSubjectMax ?? 0; + + for (let i = 0; i < assessments.length; i++) { + const subject = activeSubjectForAssessment(assessments[i], activeSubjects); + assessments[i].filterCode = subject?.code ?? assessments[i].code; + } + + const bySubject = new Map(); + const subjectOrder: string[] = []; + + for (const assessment of assessments) { + const code = assessment.filterCode; + if (!bySubject.has(code)) { + bySubject.set(code, []); + subjectOrder.push(code); + } + bySubject.get(code)!.push(assessment); + } + + for (const items of bySubject.values()) { + items.sort( + (a, b) => new Date(a.due).getTime() - new Date(b.due).getTime(), + ); + } + + subjectOrder.sort( + (a, b) => + new Date(bySubject.get(a)![0].due).getTime() - + new Date(bySubject.get(b)![0].due).getTime(), + ); + + const subjectLimit = + maxSubjects > 0 ? maxSubjects : subjectOrder.length; + const allowedSubjects = subjectOrder.slice(0, subjectLimit); + + const limited: any[] = []; + for (const code of allowedSubjects) { + const items = bySubject.get(code)!; + const perSubjectLimit = maxPerSubject > 0 ? maxPerSubject : items.length; + limited.push(...items.slice(0, perSubjectLimit)); + } + + limited.sort( + (a, b) => new Date(a.due).getTime() - new Date(b.due).getTime(), + ); + + assessments.splice(0, assessments.length, ...limited); +} + async function CreateUpcomingSection(assessments: any, activeSubjects: any) { const upcomingitemcontainer = document.querySelector("#upcoming-items"); - const overdueDates = []; const upcomingDates = {}; const Today = new Date(); - for (let i = 0; i < assessments.length; i++) { - const assessmentdue = new Date(assessments[i].due); - if (assessmentdue < Today && !CheckSpecialDay(Today, assessmentdue)) { - overdueDates.push(assessments[i]); - assessments.splice(i, 1); - i--; + if (!(settingsState.homeUpcomingIncludePast ?? true)) { + const todayStart = startOfDay(Today); + for (let i = assessments.length - 1; i >= 0; i--) { + const assessmentdue = new Date(assessments[i].due); + if (assessmentdue < todayStart) { + assessments.splice(i, 1); + } } } + applyHomeUpcomingLimits(assessments, activeSubjects); + const colours = await GetLessonColours(); for (let i = 0; i < assessments.length; i++) { @@ -945,14 +1053,6 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) { CreateFilters(filterSubjects); - for (let i = 0; i < assessments.length; i++) { - const subject = activeSubjectForAssessment( - assessments[i], - activeSubjects, - ); - assessments[i].filterCode = subject?.code ?? assessments[i].code; - } - for (let i = 0; i < assessments.length; i++) { const element: any = assessments[i]; if (!upcomingDates[element.due as keyof typeof upcomingDates]) { diff --git a/src/seqta/utils/defaultSettings.ts b/src/seqta/utils/defaultSettings.ts index 4d95d704..0285a696 100644 --- a/src/seqta/utils/defaultSettings.ts +++ b/src/seqta/utils/defaultSettings.ts @@ -50,6 +50,9 @@ export function getDefaultSettingsState(): SettingsState { animations: !isLowEndDevice, assessmentsAverage: false, defaultPage: "home", + homeUpcomingSubjectsMax: 5, + homeUpcomingAssessmentsPerSubjectMax: 0, + homeUpcomingIncludePast: true, shortcuts: [ { name: "Outlook", enabled: true }, { name: "Office", enabled: true }, diff --git a/src/seqta/utils/ensureSyncableStorageDefaults.ts b/src/seqta/utils/ensureSyncableStorageDefaults.ts index 1c200714..d350a08b 100644 --- a/src/seqta/utils/ensureSyncableStorageDefaults.ts +++ b/src/seqta/utils/ensureSyncableStorageDefaults.ts @@ -32,6 +32,8 @@ const OPTIONAL_UNSET_MEANS_DEFAULT_KEYS = [ "verboseLogging", "hideSensitiveContent", "mockNotices", + "homeUpcomingAssessmentsPerSubjectMax", + "homeUpcomingIncludePast", "devGhReleaseVersionOverride", "lastSeenNightlyPublishedAt", "originalDarkMode", diff --git a/src/seqta/utils/listeners/StorageChanges.ts b/src/seqta/utils/listeners/StorageChanges.ts index 7d7693ab..74247b0c 100644 --- a/src/seqta/utils/listeners/StorageChanges.ts +++ b/src/seqta/utils/listeners/StorageChanges.ts @@ -5,6 +5,7 @@ import { applySelectedFont } from "@/seqta/ui/fonts/Manager"; // Shortcuts rendering import { renderShortcuts } from "@/seqta/utils/Render/renderShortcuts"; import { FilterUpcomingAssessments } from "@/seqta/utils/FilterUpcomingAssessments"; +import { registerHomeUpcomingSettingsListeners } from "@/seqta/utils/Loaders/LoadHomePage"; import { applyMenuItemVisibility } from "@/seqta/utils/menuItemVisibility"; import { ChangeMenuItemPositions } from "@/seqta/utils/Openers/OpenMenuOptions"; @@ -39,6 +40,7 @@ export class StorageChangeHandler { "subjectfilters", FilterUpcomingAssessments.bind(this), ); + registerHomeUpcomingSettingsListeners(); settingsState.register( "iconOnlySidebar", this.handleIconOnlySidebarChange.bind(this), diff --git a/src/types/storage.ts b/src/types/storage.ts index 8a7d7cb7..51889c53 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -48,6 +48,12 @@ export interface SettingsState { timeFormat?: string; animations: boolean; defaultPage: string; + /** Max subjects with upcoming assessments on the home page; 0 = no limit. */ + homeUpcomingSubjectsMax?: number; + /** Max assessments shown per subject on the home page; 0 = no limit. */ + homeUpcomingAssessmentsPerSubjectMax?: number; + /** When true, show past-due assessments from the upcoming list on the home page. */ + homeUpcomingIncludePast?: boolean; devMode?: boolean; /** Dev-only: emit verboseDebug / verboseInfo / verboseLog output. */ verboseLogging?: boolean;