mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 17:21:05 +00:00
feat: minor fixes before update
This commit is contained in:
@@ -8,6 +8,7 @@ export const WHATS_NEW_CHANGELOG: WhatsNewRelease[] = [
|
||||
"title": "3.7.3 – Timetable sync to Calendar & Bugfix Bundle",
|
||||
"items": [
|
||||
"Added an option in the Timetable to sync to Google Calendar and Outlook Calendar",
|
||||
"Improved the sidebar to be more stable and performant.",
|
||||
"Fixed dropdown contrast and readability in settings and across SEQTA pages.",
|
||||
"Fixed Analytics sidebar item not hiding when toggled off in Edit Sidebar.",
|
||||
"Fixed timetable subject colour picker not reopening after closing (#221).",
|
||||
@@ -17,7 +18,6 @@ export const WHATS_NEW_CHANGELOG: WhatsNewRelease[] = [
|
||||
"Fixed assessment overview showing <code>Undefined%</code> for letter grades (#430).",
|
||||
"Fixed notifications older than a year being removed; added local per-account archive (#443).",
|
||||
"Fixed notices on the home screen sometimes failing to load (#388).",
|
||||
"Fixed nightly/CI release builds (Windows runners, zip packaging, layerchart compile).",
|
||||
"Fixed multi-target builds exiting early and masking Vite errors.",
|
||||
"Improved background music autoplay with a tap-to-start hint when blocked.",
|
||||
"Added verbose logging toggle under Developer settings.",
|
||||
|
||||
@@ -61,6 +61,31 @@ describe("lessonToGoogleEvent", () => {
|
||||
);
|
||||
expect(event?.colorId).toBe("11");
|
||||
});
|
||||
|
||||
it("maps appointments with notes", () => {
|
||||
const event = lessonToGoogleEvent(
|
||||
ORIGIN,
|
||||
{
|
||||
date: "2026-07-20",
|
||||
from: "09:30",
|
||||
until: "13:10",
|
||||
description: "Advisor meeting",
|
||||
type: "appointment",
|
||||
calendarid: "event:5",
|
||||
colour: "#ffc107",
|
||||
notes: "Bring forms",
|
||||
},
|
||||
"Australia/Perth",
|
||||
);
|
||||
expect(event).toMatchObject({
|
||||
summary: "Advisor meeting",
|
||||
startDateTime: "2026-07-20T09:30:00",
|
||||
endDateTime: "2026-07-20T13:10:00",
|
||||
seqtaKey: `${ORIGIN}:cal:event:5`,
|
||||
});
|
||||
expect(event?.description).toContain("Type: Appointment");
|
||||
expect(event?.description).toContain("Bring forms");
|
||||
});
|
||||
});
|
||||
|
||||
describe("googleApiEventBody", () => {
|
||||
|
||||
@@ -50,10 +50,18 @@ export function lessonToGoogleEvent(
|
||||
const staff = lesson.staff?.trim();
|
||||
const room = lesson.room?.trim();
|
||||
|
||||
const isAppointment = (lesson.type ?? "").toLowerCase() === "appointment";
|
||||
const notes = lesson.notes?.trim();
|
||||
const descriptionLines = ["Synced by BetterSEQTA+"];
|
||||
if (staff) descriptionLines.push(`Teacher: ${staff}`);
|
||||
if (lesson.code) descriptionLines.push(`Code: ${lesson.code}`);
|
||||
if (lesson.period) descriptionLines.push(`Period: ${lesson.period}`);
|
||||
if (isAppointment) {
|
||||
descriptionLines.push("Type: Appointment");
|
||||
if (notes) descriptionLines.push(`Notes: ${notes}`);
|
||||
} else {
|
||||
if (staff) descriptionLines.push(`Teacher: ${staff}`);
|
||||
if (lesson.code) descriptionLines.push(`Code: ${lesson.code}`);
|
||||
if (lesson.period) descriptionLines.push(`Period: ${lesson.period}`);
|
||||
if (notes) descriptionLines.push(`Notes: ${notes}`);
|
||||
}
|
||||
|
||||
const colorId = nearestGoogleEventColorId(lesson.colour);
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from "@jest/globals";
|
||||
import { appointmentToLesson, parseSeqtaDateTime } from "./fetchTimetable";
|
||||
|
||||
describe("parseSeqtaDateTime", () => {
|
||||
it("parses SEQTA event timestamps", () => {
|
||||
expect(parseSeqtaDateTime("2026-07-20 09:30:00.0")).toEqual({
|
||||
date: "2026-07-20",
|
||||
time: "09:30",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects empty values", () => {
|
||||
expect(parseSeqtaDateTime(undefined)).toBeNull();
|
||||
expect(parseSeqtaDateTime("")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("appointmentToLesson", () => {
|
||||
it("maps appointment payload rows into timetable lessons", () => {
|
||||
expect(
|
||||
appointmentToLesson({
|
||||
id: 5,
|
||||
from: "2026-07-20 09:30:00.0",
|
||||
until: "2026-07-20 13:10:00.0",
|
||||
event: {
|
||||
id: 5,
|
||||
title: "fsdfsdsdfdsfdsf",
|
||||
notes: "sdfsfddfsdsfdsfdfssdcfdsfsdfdsfds",
|
||||
colour: "#ffc107",
|
||||
event_type: "appointment",
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
date: "2026-07-20",
|
||||
from: "09:30",
|
||||
until: "13:10",
|
||||
description: "fsdfsdsdfdsfdsf",
|
||||
type: "appointment",
|
||||
calendarid: "event:5",
|
||||
colour: "#ffc107",
|
||||
notes: "sdfsfddfsdsfdsfdfssdcfdsfsdfdsfds",
|
||||
});
|
||||
});
|
||||
|
||||
it("skips incomplete appointment rows", () => {
|
||||
expect(
|
||||
appointmentToLesson({
|
||||
from: "2026-07-20 09:30:00.0",
|
||||
until: "2026-07-20 13:10:00.0",
|
||||
event: { title: "" },
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -69,19 +69,42 @@ function withSubjectColours(
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveStudentId(): Promise<number | undefined> {
|
||||
type SeqtaLoginPayload = {
|
||||
id?: number;
|
||||
student?: number;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
let cachedLogin: SeqtaLoginPayload | null = null;
|
||||
|
||||
async function resolveLoginPayload(): Promise<SeqtaLoginPayload | undefined> {
|
||||
if (cachedLogin?.id != null) return cachedLogin;
|
||||
try {
|
||||
const json = await postSeqtaJson<{ payload?: { id?: number; student?: number } }>(
|
||||
const json = await postSeqtaJson<{ payload?: SeqtaLoginPayload }>(
|
||||
"/seqta/student/login",
|
||||
{ mode: "normal", query: null, redirect_url: location.origin },
|
||||
{ mode: "normal", query: null, redirect_url: location.href },
|
||||
);
|
||||
const id = json?.payload?.id ?? json?.payload?.student;
|
||||
return typeof id === "number" && Number.isFinite(id) ? id : undefined;
|
||||
const payload = json?.payload;
|
||||
if (!payload) return undefined;
|
||||
cachedLogin = payload;
|
||||
return payload;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveStudentId(): Promise<number | undefined> {
|
||||
const payload = await resolveLoginPayload();
|
||||
const id = payload?.id ?? payload?.student;
|
||||
return typeof id === "number" && Number.isFinite(id) ? id : undefined;
|
||||
}
|
||||
|
||||
async function resolvePersonType(): Promise<string> {
|
||||
const payload = await resolveLoginPayload();
|
||||
const type = payload?.type?.trim().toLowerCase();
|
||||
return type || "student";
|
||||
}
|
||||
|
||||
function isEngageParentContext(): boolean {
|
||||
return (
|
||||
location.pathname.includes("/parent/") ||
|
||||
@@ -90,11 +113,83 @@ function isEngageParentContext(): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
type SeqtaAppointmentPayload = {
|
||||
id?: number;
|
||||
from?: string;
|
||||
until?: string;
|
||||
event?: {
|
||||
id?: number;
|
||||
title?: string;
|
||||
notes?: string;
|
||||
colour?: string;
|
||||
event_type?: string;
|
||||
};
|
||||
};
|
||||
|
||||
/** Parse SEQTA datetimes like `2026-07-20 09:30:00.0` into lesson date/time fields. */
|
||||
export function parseSeqtaDateTime(value: string | undefined): { date: string; time: string } | null {
|
||||
if (!value) return null;
|
||||
const match = value.trim().match(/^(\d{4}-\d{2}-\d{2})[ T](\d{1,2}:\d{2})/);
|
||||
if (!match) return null;
|
||||
return { date: match[1], time: match[2] };
|
||||
}
|
||||
|
||||
export function appointmentToLesson(item: SeqtaAppointmentPayload): SeqtaTimetableLesson | null {
|
||||
const start = parseSeqtaDateTime(item.from);
|
||||
const end = parseSeqtaDateTime(item.until);
|
||||
const title = item.event?.title?.trim();
|
||||
const eventId = item.event?.id ?? item.id;
|
||||
if (!start || !end || !title || eventId == null) return null;
|
||||
|
||||
const notes = item.event?.notes?.trim();
|
||||
return {
|
||||
date: start.date,
|
||||
from: start.time,
|
||||
until: end.time,
|
||||
description: title,
|
||||
type: item.event?.event_type?.trim() || "appointment",
|
||||
calendarid: `event:${eventId}`,
|
||||
colour: item.event?.colour?.trim() || undefined,
|
||||
...(notes ? { notes } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchAppointments(range: SyncDateRange): Promise<SeqtaTimetableLesson[]> {
|
||||
if (isEngageParentContext()) return [];
|
||||
|
||||
try {
|
||||
const person = await resolveStudentId();
|
||||
if (person == null) return [];
|
||||
|
||||
const personType = await resolvePersonType();
|
||||
const data = await postSeqtaJson<{ payload?: SeqtaAppointmentPayload[] }>(
|
||||
"/seqta/student/events/load",
|
||||
{
|
||||
dateFrom: range.from,
|
||||
dateTo: range.until,
|
||||
person,
|
||||
personType,
|
||||
},
|
||||
);
|
||||
|
||||
const payload = Array.isArray(data?.payload) ? data.payload : [];
|
||||
const lessons: SeqtaTimetableLesson[] = [];
|
||||
for (const item of payload) {
|
||||
const lesson = appointmentToLesson(item);
|
||||
if (lesson) lessons.push(lesson);
|
||||
}
|
||||
return lessons;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchTimetableLessons(
|
||||
range: SyncDateRange,
|
||||
): Promise<SeqtaTimetableLesson[]> {
|
||||
const { from, until } = range;
|
||||
const coloursPromise = fetchSubjectColours();
|
||||
const appointmentsPromise = fetchAppointments(range);
|
||||
|
||||
let lessons: SeqtaTimetableLesson[] = [];
|
||||
|
||||
@@ -125,7 +220,9 @@ export async function fetchTimetableLessons(
|
||||
lessons = Array.isArray(data?.payload?.items) ? data.payload.items : [];
|
||||
}
|
||||
|
||||
return withSubjectColours(lessons, await coloursPromise);
|
||||
const coloured = withSubjectColours(lessons, await coloursPromise);
|
||||
const appointments = await appointmentsPromise;
|
||||
return [...coloured, ...appointments];
|
||||
}
|
||||
|
||||
export async function fetchTimetableForSync(weeksAhead?: number): Promise<SeqtaTimetableLesson[]> {
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface SeqtaTimetableLesson {
|
||||
code?: string;
|
||||
type?: string;
|
||||
period?: string;
|
||||
notes?: string;
|
||||
calendarid?: string | number;
|
||||
ci?: number;
|
||||
/** SEQTA subject colour (hex/rgb) used for Google Calendar event colour. */
|
||||
|
||||
Reference in New Issue
Block a user