mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
fix: fix assement overview not choosing actuve subjects and improve styling
This commit is contained in:
@@ -3,6 +3,12 @@
|
|||||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
|
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
|
||||||
import { buildEngageAssessmentPagePath } from "@/seqta/utils/engageAssessmentStudent";
|
import { buildEngageAssessmentPagePath } from "@/seqta/utils/engageAssessmentStudent";
|
||||||
|
import OverviewIcon from "./OverviewIcon.svelte";
|
||||||
|
import {
|
||||||
|
GROUP_SORT_ICONS,
|
||||||
|
STATUS_COLUMN_ICONS,
|
||||||
|
type OverviewIconName,
|
||||||
|
} from "./icons";
|
||||||
import confetti from "canvas-confetti";
|
import confetti from "canvas-confetti";
|
||||||
|
|
||||||
export let data: any;
|
export let data: any;
|
||||||
@@ -50,7 +56,12 @@
|
|||||||
|
|
||||||
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: OverviewIconName;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
function getAssessmentYear(a: any): number {
|
function getAssessmentYear(a: any): number {
|
||||||
const dateStr = a.due || a.date || a.dueDate || a.created;
|
const dateStr = a.due || a.date || a.dueDate || a.created;
|
||||||
@@ -89,14 +100,23 @@
|
|||||||
return new Date(a.due || a.date || 0).getTime() - new Date(b.due || b.date || 0).getTime();
|
return new Date(a.due || a.date || 0).getTime() - new Date(b.due || b.date || 0).getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_COLUMNS = [
|
const STATUS_COLUMNS: {
|
||||||
{ key: "UPCOMING", title: "Upcoming", className: "column-upcoming", icon: "📅" },
|
key: string;
|
||||||
{ key: "DUE_SOON", title: "Due Soon", className: "column-due-soon", icon: "⏰" },
|
title: string;
|
||||||
{ key: "OVERDUE", title: "Overdue", className: "column-overdue", icon: "🚨" },
|
className: string;
|
||||||
{ key: "SUBMITTED", title: "Submitted", className: "column-submitted", icon: "📝" },
|
icon: OverviewIconName;
|
||||||
{ key: "MARKS_RELEASED", title: "Marked", className: "column-marked", icon: "✅" },
|
}[] = [
|
||||||
|
{ key: "UPCOMING", title: "Upcoming", className: "column-upcoming", icon: "calendar-days" },
|
||||||
|
{ key: "DUE_SOON", title: "Due Soon", className: "column-due-soon", icon: "clock" },
|
||||||
|
{ key: "OVERDUE", title: "Overdue", className: "column-overdue", icon: "exclamation-triangle" },
|
||||||
|
{ key: "SUBMITTED", title: "Submitted", className: "column-submitted", icon: "document-check" },
|
||||||
|
{ key: "MARKS_RELEASED", title: "Marked", className: "column-marked", icon: "check-circle" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function groupSortIcon(): OverviewIconName {
|
||||||
|
return GROUP_SORT_ICONS[currentFilters.sortBy] ?? "queue-list";
|
||||||
|
}
|
||||||
|
|
||||||
function buildGroupsAndColumns() {
|
function buildGroupsAndColumns() {
|
||||||
if (!data?.assessments) return { filteredAssessments: [], statusGroups: {}, columns: [] };
|
if (!data?.assessments) return { filteredAssessments: [], statusGroups: {}, columns: [] };
|
||||||
const subjectFilters = settingsState.subjectfilters || {};
|
const subjectFilters = settingsState.subjectfilters || {};
|
||||||
@@ -131,18 +151,19 @@
|
|||||||
groups[key].sort(sortCompare);
|
groups[key].sort(sortCompare);
|
||||||
});
|
});
|
||||||
|
|
||||||
let cols: { key: string; title: string; className: string; icon: string }[];
|
let cols: { key: string; title: string; className: string; icon: OverviewIconName }[];
|
||||||
if (currentFilters.sortBy === "due") {
|
if (currentFilters.sortBy === "due") {
|
||||||
cols = STATUS_COLUMNS;
|
cols = STATUS_COLUMNS;
|
||||||
} else {
|
} else {
|
||||||
const keys = Object.keys(groups).filter((k) => groups[k]?.length > 0);
|
const keys = Object.keys(groups).filter((k) => groups[k]?.length > 0);
|
||||||
|
const sortIcon = groupSortIcon();
|
||||||
if (currentFilters.sortBy === "year") {
|
if (currentFilters.sortBy === "year") {
|
||||||
cols = keys.sort((a, b) => Number(b) - Number(a)).map((k) => ({ key: k, title: k, className: "column-custom", icon: "📆" }));
|
cols = keys.sort((a, b) => Number(b) - Number(a)).map((k) => ({ key: k, title: k, className: "column-custom", icon: sortIcon }));
|
||||||
} else if (currentFilters.sortBy === "subject") {
|
} else if (currentFilters.sortBy === "subject") {
|
||||||
const subjectTitles = new Map(data?.subjects?.map((s: any) => [s.code, `${s.code} - ${s.title}`]) || []);
|
const subjectTitles = new Map(data?.subjects?.map((s: any) => [s.code, `${s.code} - ${s.title}`]) || []);
|
||||||
cols = keys.sort().map((k) => ({ key: k, title: subjectTitles.get(k) || k, className: "column-custom", icon: "📚" }));
|
cols = keys.sort().map((k) => ({ key: k, title: subjectTitles.get(k) || k, className: "column-custom", icon: sortIcon }));
|
||||||
} else {
|
} else {
|
||||||
cols = keys.sort().map((k) => ({ key: k, title: k, className: "column-custom", icon: "📋" }));
|
cols = keys.sort().map((k) => ({ key: k, title: k, className: "column-custom", icon: sortIcon }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,10 +400,13 @@
|
|||||||
|
|
||||||
<svelte:window on:click={closeAllMenus} />
|
<svelte:window on:click={closeAllMenus} />
|
||||||
|
|
||||||
<div id="grid-view-container">
|
<div class="bsplus-overview-page">
|
||||||
<div class="grid-view-header">
|
<header class="grid-view-header bsplus-overview-animate">
|
||||||
|
<div class="grid-view-header-text">
|
||||||
<h1 class="grid-view-title">Assessments</h1>
|
<h1 class="grid-view-title">Assessments</h1>
|
||||||
<div class="grid-view-filters">
|
<p class="grid-view-subtitle">Track upcoming tasks, submissions, and released marks</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid-view-filters bsplus-overview-toolbar">
|
||||||
{#if showStudentFilter}
|
{#if showStudentFilter}
|
||||||
<select class="filter-select" bind:value={currentFilters.student}>
|
<select class="filter-select" bind:value={currentFilters.student}>
|
||||||
<option value="all">All Students</option>
|
<option value="all">All Students</option>
|
||||||
@@ -411,14 +435,15 @@
|
|||||||
on:click={() => (showVisibilityPanel = !showVisibilityPanel)}
|
on:click={() => (showVisibilityPanel = !showVisibilityPanel)}
|
||||||
title="Manage hidden subjects and assessments"
|
title="Manage hidden subjects and assessments"
|
||||||
>
|
>
|
||||||
👁 Visibility ({hiddenSubjects.length + hiddenAssessmentsWithInfo.length})
|
<OverviewIcon name="eye" size={18} />
|
||||||
|
<span>Visibility ({hiddenSubjects.length + hiddenAssessmentsWithInfo.length})</span>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
|
|
||||||
{#if showVisibilityPanel && hasHiddenItems}
|
{#if showVisibilityPanel && hasHiddenItems}
|
||||||
<div class="visibility-panel">
|
<div class="visibility-panel bsplus-overview-animate">
|
||||||
<h4 class="visibility-panel-title">Hidden items</h4>
|
<h4 class="visibility-panel-title">Hidden items</h4>
|
||||||
{#if hiddenSubjects.length > 0}
|
{#if hiddenSubjects.length > 0}
|
||||||
<div class="visibility-section">
|
<div class="visibility-section">
|
||||||
@@ -449,10 +474,10 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div id="main-grid-content">
|
<div id="main-grid-content" class="bsplus-overview-animate bsplus-overview-delay-1">
|
||||||
{#if filteredAssessments.length === 0}
|
{#if filteredAssessments.length === 0}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-icon">📋</div>
|
<OverviewIcon name="clipboard-document-list" size={40} class="empty-icon" />
|
||||||
<p>No assessments found matching your filters</p>
|
<p>No assessments found matching your filters</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -463,7 +488,13 @@
|
|||||||
<div class="kanban-column {column.className}">
|
<div class="kanban-column {column.className}">
|
||||||
<div class="column-header">
|
<div class="column-header">
|
||||||
<div class="column-title">
|
<div class="column-title">
|
||||||
{column.icon} {column.title}
|
<span class="column-title-main">
|
||||||
|
<OverviewIcon
|
||||||
|
name={column.icon ?? STATUS_COLUMN_ICONS[column.key] ?? "queue-list"}
|
||||||
|
size={18}
|
||||||
|
/>
|
||||||
|
{column.title}
|
||||||
|
</span>
|
||||||
<span class="column-count">{statusGroups[column.key].length}</span>
|
<span class="column-count">{statusGroups[column.key].length}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -504,11 +535,7 @@
|
|||||||
on:click={(e) => toggleMenu(assessment.id, e)}
|
on:click={(e) => toggleMenu(assessment.id, e)}
|
||||||
aria-label="Open menu"
|
aria-label="Open menu"
|
||||||
>
|
>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
<OverviewIcon name="ellipsis-vertical" size={16} />
|
||||||
<circle cx="12" cy="5" r="2"/>
|
|
||||||
<circle cx="12" cy="12" r="2"/>
|
|
||||||
<circle cx="12" cy="19" r="2"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
<div class="menu-dropdown" style="display: {openMenuId === assessment.id ? 'block' : 'none'};">
|
<div class="menu-dropdown" style="display: {openMenuId === assessment.id ? 'block' : 'none'};">
|
||||||
{#if status !== "MARKS_RELEASED"}
|
{#if status !== "MARKS_RELEASED"}
|
||||||
@@ -535,7 +562,8 @@
|
|||||||
{#if !assessment.results && !isCompleted}
|
{#if !assessment.results && !isCompleted}
|
||||||
<div class="assessment-meta">
|
<div class="assessment-meta">
|
||||||
<div class="due-date {dueDateClass}">
|
<div class="due-date {dueDateClass}">
|
||||||
📅 {formatDate(assessment.due || assessment.date || assessment.dueDate || "", assessment.submitted)}
|
<OverviewIcon name="calendar-days" size={14} />
|
||||||
|
{formatDate(assessment.due || assessment.date || assessment.dueDate || "", assessment.submitted)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import OverviewIcon from "./OverviewIcon.svelte";
|
||||||
|
|
||||||
export let error: string;
|
export let error: string;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="error-container">
|
<div class="error-container bsplus-overview-animate">
|
||||||
|
<OverviewIcon name="exclamation-circle" size={40} class="error-icon" />
|
||||||
<p class="error-text">Failed to load assessments</p>
|
<p class="error-text">Failed to load assessments</p>
|
||||||
<p style="color: #94a3b8; font-size: 0.875rem;">{error}</p>
|
<p class="error-detail">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { OVERVIEW_ICON_PATHS, type OverviewIconName } from "./icons";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
name: OverviewIconName;
|
||||||
|
class?: string;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { name, class: className = "", size = 20 }: Props = $props();
|
||||||
|
|
||||||
|
const paths = $derived.by(() => {
|
||||||
|
const raw = OVERVIEW_ICON_PATHS[name];
|
||||||
|
return Array.isArray(raw) ? raw : [raw];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
class="bsplus-overview-icon {className}"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{#each paths as path, index (index)}
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d={path} />
|
||||||
|
{/each}
|
||||||
|
</svg>
|
||||||
@@ -1,7 +1,28 @@
|
|||||||
<div id="grid-view-container">
|
<script lang="ts">
|
||||||
<div class="grid-view-header">
|
import OverviewIcon from "./OverviewIcon.svelte";
|
||||||
|
import type { OverviewIconName } from "./icons";
|
||||||
|
|
||||||
|
const columns: {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
className: string;
|
||||||
|
icon: OverviewIconName;
|
||||||
|
skeletonCount: number;
|
||||||
|
}[] = [
|
||||||
|
{ key: "UPCOMING", title: "Upcoming", className: "column-upcoming", icon: "calendar-days", skeletonCount: 3 },
|
||||||
|
{ key: "DUE_SOON", title: "Due Soon", className: "column-due-soon", icon: "clock", skeletonCount: 2 },
|
||||||
|
{ key: "OVERDUE", title: "Overdue", className: "column-overdue", icon: "exclamation-triangle", skeletonCount: 1 },
|
||||||
|
{ key: "MARKS_RELEASED", title: "Marked", className: "column-marked", icon: "check-circle", skeletonCount: 4 },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="bsplus-overview-page">
|
||||||
|
<header class="grid-view-header bsplus-overview-animate">
|
||||||
|
<div class="grid-view-header-text">
|
||||||
<h1 class="grid-view-title">Assessments</h1>
|
<h1 class="grid-view-title">Assessments</h1>
|
||||||
<div class="grid-view-filters">
|
<p class="grid-view-subtitle">Loading your assessment overview…</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid-view-filters bsplus-overview-toolbar">
|
||||||
<select class="filter-select" disabled>
|
<select class="filter-select" disabled>
|
||||||
<option value="all">Loading subjects...</option>
|
<option value="all">Loading subjects...</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -9,17 +30,20 @@
|
|||||||
<option value="due">Sort by Due Date</option>
|
<option value="due">Sort by Due Date</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
|
|
||||||
<div id="main-grid-content">
|
<div id="main-grid-content" class="bsplus-overview-animate bsplus-overview-delay-1">
|
||||||
<div class="kanban-board">
|
<div class="kanban-board">
|
||||||
{#each columns as column}
|
{#each columns as column}
|
||||||
<div class="kanban-column-parent">
|
<div class="kanban-column-parent">
|
||||||
<div class="kanban-column {column.className}">
|
<div class="kanban-column {column.className}">
|
||||||
<div class="column-header">
|
<div class="column-header">
|
||||||
<div class="column-title">
|
<div class="column-title">
|
||||||
{column.icon} {column.title}
|
<span class="column-title-main">
|
||||||
<span class="column-count">...</span>
|
<OverviewIcon name={column.icon} size={18} />
|
||||||
|
{column.title}
|
||||||
|
</span>
|
||||||
|
<span class="column-count">…</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column-cards" id="{column.key.toLowerCase()}-cards">
|
<div class="column-cards" id="{column.key.toLowerCase()}-cards">
|
||||||
@@ -43,36 +67,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
key: "UPCOMING",
|
|
||||||
title: "Upcoming",
|
|
||||||
className: "column-upcoming",
|
|
||||||
icon: "📅",
|
|
||||||
skeletonCount: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "DUE_SOON",
|
|
||||||
title: "Due Soon",
|
|
||||||
className: "column-due-soon",
|
|
||||||
icon: "⏰",
|
|
||||||
skeletonCount: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "OVERDUE",
|
|
||||||
title: "Overdue",
|
|
||||||
className: "column-overdue",
|
|
||||||
icon: "🚨",
|
|
||||||
skeletonCount: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "MARKS_RELEASED",
|
|
||||||
title: "Marked",
|
|
||||||
className: "column-marked",
|
|
||||||
icon: "✅",
|
|
||||||
skeletonCount: 4,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
interface Subject {
|
import {
|
||||||
code: string;
|
activeSubjectsFromLearnPayload,
|
||||||
programme: number;
|
assessmentBelongsToActiveSubjects,
|
||||||
metaclass: number;
|
filterAssessmentsForActiveSubjects,
|
||||||
title: string;
|
type OverviewSubject,
|
||||||
}
|
} from "./utils";
|
||||||
|
|
||||||
interface PrefItem {
|
interface PrefItem {
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
@@ -31,11 +32,9 @@ async function fetchJSON(url: string, body: any) {
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSubjects() {
|
async function loadSubjects(): Promise<OverviewSubject[]> {
|
||||||
const res = await fetchJSON("/seqta/student/load/subjects?", {});
|
const res = await fetchJSON("/seqta/student/load/subjects?", {});
|
||||||
return res.payload
|
return activeSubjectsFromLearnPayload(res.payload);
|
||||||
.filter((s: any) => s.active === 1)
|
|
||||||
.flatMap((s: any) => s.subjects);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadPrefs(student: number) {
|
async function loadPrefs(student: number) {
|
||||||
@@ -61,7 +60,7 @@ async function loadUpcoming(student: number) {
|
|||||||
return res.payload;
|
return res.payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeAssessmentDates(t: any, subject: Subject): any {
|
function normalizeAssessmentDates(t: any, subject: OverviewSubject): any {
|
||||||
const normalized = { ...t };
|
const normalized = { ...t };
|
||||||
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;
|
||||||
@@ -72,7 +71,7 @@ function normalizeAssessmentDates(t: any, subject: Subject): any {
|
|||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadPast(student: number, subjects: Subject[]) {
|
async function loadPast(student: number, subjects: OverviewSubject[]) {
|
||||||
const map: Record<number, any> = {};
|
const map: Record<number, any> = {};
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
subjects.map(async (s) => {
|
subjects.map(async (s) => {
|
||||||
@@ -141,14 +140,20 @@ async function getLearnAssessmentsData(studentId: number) {
|
|||||||
const pastMap = await loadPast(studentId, 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) => {
|
||||||
|
if (assessmentBelongsToActiveSubjects(a, subjects)) {
|
||||||
map[a.id] = { ...a };
|
map[a.id] = { ...a };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
Object.values(pastMap).forEach((t: any) => {
|
Object.values(pastMap).forEach((t: any) => {
|
||||||
|
if (!assessmentBelongsToActiveSubjects(t, subjects)) return;
|
||||||
if (map[t.id]) Object.assign(map[t.id], t);
|
if (map[t.id]) Object.assign(map[t.id], t);
|
||||||
else map[t.id] = t;
|
else map[t.id] = t;
|
||||||
});
|
});
|
||||||
|
|
||||||
const allAssessments = Object.values(map);
|
const allAssessments = filterAssessmentsForActiveSubjects(
|
||||||
|
Object.values(map),
|
||||||
|
subjects,
|
||||||
|
);
|
||||||
const submissions = await loadSubmissions(studentId, allAssessments);
|
const submissions = await loadSubmissions(studentId, allAssessments);
|
||||||
|
|
||||||
allAssessments.forEach((assessment: any) => {
|
allAssessments.forEach((assessment: any) => {
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { getEngageAssessmentStudentId } from "@/seqta/utils/engageAssessmentStudent";
|
import { getEngageAssessmentStudentId } from "@/seqta/utils/engageAssessmentStudent";
|
||||||
|
import {
|
||||||
interface Subject {
|
activeSubjectsFromEngageChild,
|
||||||
code: string;
|
assessmentBelongsToActiveSubjects,
|
||||||
programme: number;
|
filterAssessmentsForActiveSubjects,
|
||||||
metaclass: number;
|
type OverviewSubject,
|
||||||
title: string;
|
} from "./utils";
|
||||||
}
|
|
||||||
|
|
||||||
interface PrefItem {
|
interface PrefItem {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -58,17 +57,8 @@ export async function resolveEngageStudentId(): Promise<number> {
|
|||||||
throw new Error("Could not resolve Engage student ID");
|
throw new Error("Could not resolve Engage student ID");
|
||||||
}
|
}
|
||||||
|
|
||||||
function subjectsFromChild(child: EngageChildPayload): Subject[] {
|
function subjectsFromChild(child: EngageChildPayload): OverviewSubject[] {
|
||||||
return (child.terms ?? [])
|
return activeSubjectsFromEngageChild(child);
|
||||||
.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>> {
|
async function loadEngagePrefs(): Promise<Record<string, string>> {
|
||||||
@@ -94,7 +84,7 @@ async function loadEngageUpcoming(studentId: number) {
|
|||||||
return res.payload ?? [];
|
return res.payload ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeAssessmentDates(t: any, subject: Subject): any {
|
function normalizeAssessmentDates(t: any, subject: OverviewSubject): any {
|
||||||
const normalized = { ...t };
|
const normalized = { ...t };
|
||||||
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;
|
||||||
@@ -105,7 +95,7 @@ function normalizeAssessmentDates(t: any, subject: Subject): any {
|
|||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadEngagePast(studentId: number, subjects: Subject[]) {
|
async function loadEngagePast(studentId: number, subjects: OverviewSubject[]) {
|
||||||
const map: Record<number, any> = {};
|
const map: Record<number, any> = {};
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@@ -179,14 +169,20 @@ async function loadEngageAssessmentsForStudent(
|
|||||||
|
|
||||||
const map: Record<number, any> = {};
|
const map: Record<number, any> = {};
|
||||||
upcoming.forEach((assessment: any) => {
|
upcoming.forEach((assessment: any) => {
|
||||||
|
if (assessmentBelongsToActiveSubjects(assessment, subjects)) {
|
||||||
map[assessment.id] = { ...assessment };
|
map[assessment.id] = { ...assessment };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
Object.values(pastMap).forEach((task: any) => {
|
Object.values(pastMap).forEach((task: any) => {
|
||||||
|
if (!assessmentBelongsToActiveSubjects(task, subjects)) return;
|
||||||
if (map[task.id]) Object.assign(map[task.id], task);
|
if (map[task.id]) Object.assign(map[task.id], task);
|
||||||
else map[task.id] = task;
|
else map[task.id] = task;
|
||||||
});
|
});
|
||||||
|
|
||||||
const assessments = Object.values(map).map((assessment) => ({
|
const assessments = filterAssessmentsForActiveSubjects(
|
||||||
|
Object.values(map),
|
||||||
|
subjects,
|
||||||
|
).map((assessment) => ({
|
||||||
...assessment,
|
...assessment,
|
||||||
studentId,
|
studentId,
|
||||||
studentName,
|
studentName,
|
||||||
@@ -218,7 +214,7 @@ export async function getEngageAssessmentsData() {
|
|||||||
Promise.all(childrenPayload.map((child) => loadEngageAssessmentsForStudent(child))),
|
Promise.all(childrenPayload.map((child) => loadEngageAssessmentsForStudent(child))),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const subjectsMap = new Map<string, Subject>();
|
const subjectsMap = new Map<string, OverviewSubject>();
|
||||||
childrenPayload.forEach((child) => {
|
childrenPayload.forEach((child) => {
|
||||||
subjectsFromChild(child).forEach((subject) => {
|
subjectsFromChild(child).forEach((subject) => {
|
||||||
if (!subjectsMap.has(subject.code)) {
|
if (!subjectsMap.has(subject.code)) {
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/** Heroicons v2 outline paths (https://heroicons.com) */
|
||||||
|
export type OverviewIconName =
|
||||||
|
| "calendar-days"
|
||||||
|
| "clock"
|
||||||
|
| "exclamation-triangle"
|
||||||
|
| "document-check"
|
||||||
|
| "check-circle"
|
||||||
|
| "book-open"
|
||||||
|
| "calendar"
|
||||||
|
| "chart-bar"
|
||||||
|
| "queue-list"
|
||||||
|
| "eye"
|
||||||
|
| "clipboard-document-list"
|
||||||
|
| "ellipsis-vertical"
|
||||||
|
| "exclamation-circle";
|
||||||
|
|
||||||
|
export const OVERVIEW_ICON_PATHS: Record<
|
||||||
|
OverviewIconName,
|
||||||
|
string | string[]
|
||||||
|
> = {
|
||||||
|
"calendar-days":
|
||||||
|
"M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5",
|
||||||
|
clock: "M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z",
|
||||||
|
"exclamation-triangle":
|
||||||
|
"M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z",
|
||||||
|
"document-check":
|
||||||
|
"M10.125 2.25h-4.5c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9zm3.75 8.625a2.625 2.625 0 100-5.25 2.625 2.625 0 000 5.25zm0 0l-3 3m3-3l3 3",
|
||||||
|
"check-circle":
|
||||||
|
"M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
|
||||||
|
"book-open":
|
||||||
|
"M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25",
|
||||||
|
calendar:
|
||||||
|
"M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5",
|
||||||
|
"chart-bar":
|
||||||
|
"M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z",
|
||||||
|
"queue-list":
|
||||||
|
"M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 010 3.75H5.625a1.875 1.875 0 010-3.75z",
|
||||||
|
eye: [
|
||||||
|
"M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z",
|
||||||
|
"M15 12a3 3 0 11-6 0 3 3 0 016 0z",
|
||||||
|
],
|
||||||
|
"clipboard-document-list": [
|
||||||
|
"M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 17.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z",
|
||||||
|
"M8.25 6.75V4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V6.75H8.25z",
|
||||||
|
],
|
||||||
|
"ellipsis-vertical":
|
||||||
|
"M12 6.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 12.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 18.75a.75.75 0 110-1.5.75.75 0 010 1.5z",
|
||||||
|
"exclamation-circle":
|
||||||
|
"M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const STATUS_COLUMN_ICONS: Record<string, OverviewIconName> = {
|
||||||
|
UPCOMING: "calendar-days",
|
||||||
|
DUE_SOON: "clock",
|
||||||
|
OVERDUE: "exclamation-triangle",
|
||||||
|
SUBMITTED: "document-check",
|
||||||
|
MARKS_RELEASED: "check-circle",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GROUP_SORT_ICONS: Record<string, OverviewIconName> = {
|
||||||
|
year: "calendar",
|
||||||
|
subject: "book-open",
|
||||||
|
grade: "chart-bar",
|
||||||
|
title: "queue-list",
|
||||||
|
};
|
||||||
@@ -110,7 +110,7 @@ const assessmentsOverviewPlugin: Plugin<{}> = {
|
|||||||
.querySelector('[data-key="assessments"]')
|
.querySelector('[data-key="assessments"]')
|
||||||
?.classList.add("active");
|
?.classList.add("active");
|
||||||
|
|
||||||
main.innerHTML = '<div id="grid-view-container"></div>';
|
main.innerHTML = '<div id="grid-view-container" class="bsplus-overview-host"></div>';
|
||||||
const container = document.getElementById(
|
const container = document.getElementById(
|
||||||
"grid-view-container",
|
"grid-view-container",
|
||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,45 +1,155 @@
|
|||||||
import renderSvelte from "@/interface/main";
|
import renderSvelte from "@/interface/main";
|
||||||
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
import AssessmentsOverview from "./AssessmentsOverview.svelte";
|
import AssessmentsOverview from "./AssessmentsOverview.svelte";
|
||||||
import SkeletonLoader from "./SkeletonLoader.svelte";
|
import SkeletonLoader from "./SkeletonLoader.svelte";
|
||||||
import ErrorState from "./ErrorState.svelte";
|
import ErrorState from "./ErrorState.svelte";
|
||||||
import { unmount } from "svelte";
|
import { unmount } from "svelte";
|
||||||
|
|
||||||
let currentApp: any = null;
|
let currentApp: any = null;
|
||||||
|
let themeObserver: MutationObserver | null = null;
|
||||||
|
type ThemeSettingKey =
|
||||||
|
| "selectedColor"
|
||||||
|
| "DarkMode"
|
||||||
|
| "adaptiveThemeColour"
|
||||||
|
| "adaptiveThemeGradient"
|
||||||
|
| "selectedTheme";
|
||||||
|
|
||||||
export function renderGrid(container: HTMLElement, data: any) {
|
let themeListeners: Array<{ key: ThemeSettingKey; listener: () => void }> = [];
|
||||||
if (currentApp) {
|
|
||||||
unmount(currentApp);
|
const THEME_CSS_VARS = [
|
||||||
|
"--better-main",
|
||||||
|
"--better-pale",
|
||||||
|
"--better-light",
|
||||||
|
"--text-color",
|
||||||
|
"--background-primary",
|
||||||
|
"--background-secondary",
|
||||||
|
"--text-primary",
|
||||||
|
"--theme-offset-bg",
|
||||||
|
"--better-sub",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const ACCENT_CSS_VARS = [
|
||||||
|
"--better-main",
|
||||||
|
"--accent-color-value",
|
||||||
|
"--accentColor",
|
||||||
|
"--colour-betterseqta-blue",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function extractSolidColor(value: string): string | null {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed || trimmed === "initial") return null;
|
||||||
|
if (
|
||||||
|
trimmed.startsWith("#") ||
|
||||||
|
trimmed.startsWith("rgb") ||
|
||||||
|
trimmed.startsWith("hsl")
|
||||||
|
) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
if (trimmed.includes("gradient")) {
|
||||||
|
const match = trimmed.match(
|
||||||
|
/#[0-9A-Fa-f]{6}|#[0-9A-Fa-f]{3}|rgba?\([^)]+\)/i,
|
||||||
|
);
|
||||||
|
return match?.[0] ?? null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePageAccentColor(): string {
|
||||||
|
const computed = getComputedStyle(document.documentElement);
|
||||||
|
for (const name of ACCENT_CSS_VARS) {
|
||||||
|
const solid = extractSolidColor(computed.getPropertyValue(name));
|
||||||
|
if (solid) return solid;
|
||||||
|
}
|
||||||
|
const fromSettings = settingsState.selectedColor?.trim();
|
||||||
|
if (fromSettings) {
|
||||||
|
const solid = extractSolidColor(fromSettings);
|
||||||
|
if (solid) return solid;
|
||||||
|
}
|
||||||
|
return "#007bff";
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncOverviewTheme(target: HTMLElement) {
|
||||||
|
const computed = getComputedStyle(document.documentElement);
|
||||||
|
for (const name of THEME_CSS_VARS) {
|
||||||
|
const value = document.documentElement.style.getPropertyValue(name).trim()
|
||||||
|
|| computed.getPropertyValue(name).trim();
|
||||||
|
if (value) target.style.setProperty(name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = "";
|
const accent = resolvePageAccentColor();
|
||||||
container.className = "";
|
target.style.setProperty("--bsplus-overview-accent", accent);
|
||||||
|
target.style.setProperty("--better-main", accent);
|
||||||
|
target.classList.toggle(
|
||||||
|
"dark",
|
||||||
|
document.documentElement.classList.contains("dark"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function watchOverviewTheme(root: HTMLElement) {
|
||||||
|
for (const { key, listener } of themeListeners) {
|
||||||
|
settingsState.unregister(key, listener);
|
||||||
|
}
|
||||||
|
themeListeners = [];
|
||||||
|
|
||||||
|
const listener = () => syncOverviewTheme(root);
|
||||||
|
for (const key of [
|
||||||
|
"selectedColor",
|
||||||
|
"DarkMode",
|
||||||
|
"adaptiveThemeColour",
|
||||||
|
"adaptiveThemeGradient",
|
||||||
|
"selectedTheme",
|
||||||
|
] satisfies ThemeSettingKey[]) {
|
||||||
|
settingsState.register(key, listener);
|
||||||
|
themeListeners.push({ key, listener });
|
||||||
|
}
|
||||||
|
|
||||||
|
themeObserver?.disconnect();
|
||||||
|
themeObserver = new MutationObserver(() => syncOverviewTheme(root));
|
||||||
|
themeObserver.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["style", "class"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareContainer(container: HTMLElement) {
|
||||||
|
container.innerHTML = "";
|
||||||
|
container.className = "bsplus-overview-host";
|
||||||
|
container.classList.add("bsplus-overview-root");
|
||||||
|
syncOverviewTheme(container);
|
||||||
|
watchOverviewTheme(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderGrid(container: HTMLElement, data: any) {
|
||||||
|
if (currentApp) unmount(currentApp);
|
||||||
|
prepareContainer(container);
|
||||||
currentApp = renderSvelte(AssessmentsOverview, container, { data });
|
currentApp = renderSvelte(AssessmentsOverview, container, { data });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderSkeletonLoader(container: HTMLElement) {
|
export function renderSkeletonLoader(container: HTMLElement) {
|
||||||
if (currentApp) {
|
if (currentApp) unmount(currentApp);
|
||||||
unmount(currentApp);
|
prepareContainer(container);
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = "";
|
|
||||||
container.className = "";
|
|
||||||
|
|
||||||
currentApp = renderSvelte(SkeletonLoader, container);
|
currentApp = renderSvelte(SkeletonLoader, container);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function renderLoadingState(container: HTMLElement) {
|
export function renderLoadingState(container: HTMLElement) {
|
||||||
renderSkeletonLoader(container);
|
renderSkeletonLoader(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderErrorState(container: HTMLElement, error: string) {
|
export function renderErrorState(container: HTMLElement, error: string) {
|
||||||
if (currentApp) {
|
if (currentApp) unmount(currentApp);
|
||||||
unmount(currentApp);
|
prepareContainer(container);
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = "";
|
|
||||||
container.className = "";
|
|
||||||
|
|
||||||
currentApp = renderSvelte(ErrorState, container, { error });
|
currentApp = renderSvelte(ErrorState, container, { error });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function teardownOverviewUi() {
|
||||||
|
for (const { key, listener } of themeListeners) {
|
||||||
|
settingsState.unregister(key, listener);
|
||||||
|
}
|
||||||
|
themeListeners = [];
|
||||||
|
themeObserver?.disconnect();
|
||||||
|
themeObserver = null;
|
||||||
|
if (currentApp) {
|
||||||
|
unmount(currentApp);
|
||||||
|
currentApp = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,115 @@
|
|||||||
|
export interface OverviewSubject {
|
||||||
|
code: string;
|
||||||
|
programme: number;
|
||||||
|
metaclass: number;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActiveTermFlag(active: unknown): boolean {
|
||||||
|
return active === 1 || active === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeOverviewSubject(raw: unknown): OverviewSubject | null {
|
||||||
|
if (!raw || typeof raw !== "object") return null;
|
||||||
|
|
||||||
|
const subject = raw as Record<string, unknown>;
|
||||||
|
const programme = Number(subject.programme ?? subject.programmeID);
|
||||||
|
const metaclass = Number(subject.metaclass ?? subject.metaclassID);
|
||||||
|
if (!programme || !metaclass || Number.isNaN(programme) || Number.isNaN(metaclass)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = String(subject.code ?? subject.subject ?? "").trim();
|
||||||
|
if (!code) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
programme,
|
||||||
|
metaclass,
|
||||||
|
title: String(subject.title ?? subject.description ?? code),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Subjects from the active programme-year folder(s) in `/seqta/student/load/subjects`. */
|
||||||
|
export function activeSubjectsFromLearnPayload(payload: unknown): OverviewSubject[] {
|
||||||
|
if (!Array.isArray(payload)) return [];
|
||||||
|
|
||||||
|
const subjects: OverviewSubject[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
|
for (const folder of payload) {
|
||||||
|
if (!folder || typeof folder !== "object") continue;
|
||||||
|
const term = folder as { active?: unknown; subjects?: unknown[] };
|
||||||
|
if (!isActiveTermFlag(term.active) || !Array.isArray(term.subjects)) continue;
|
||||||
|
|
||||||
|
for (const raw of term.subjects) {
|
||||||
|
const subject = normalizeOverviewSubject(raw);
|
||||||
|
if (!subject) continue;
|
||||||
|
const key = `${subject.programme}-${subject.metaclass}`;
|
||||||
|
if (seen.has(key)) continue;
|
||||||
|
seen.add(key);
|
||||||
|
subjects.push(subject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return subjects;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function activeSubjectsFromEngageChild(child: {
|
||||||
|
terms?: { active?: number; subjects?: unknown[] }[];
|
||||||
|
}): OverviewSubject[] {
|
||||||
|
const subjects: OverviewSubject[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
|
for (const term of child.terms ?? []) {
|
||||||
|
if (term.active !== 1) continue;
|
||||||
|
for (const raw of term.subjects ?? []) {
|
||||||
|
const subject = normalizeOverviewSubject(raw);
|
||||||
|
if (!subject) continue;
|
||||||
|
const key = `${subject.programme}-${subject.metaclass}`;
|
||||||
|
if (seen.has(key)) continue;
|
||||||
|
seen.add(key);
|
||||||
|
subjects.push(subject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return subjects;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assessmentBelongsToActiveSubjects(
|
||||||
|
assessment: Record<string, unknown>,
|
||||||
|
activeSubjects: OverviewSubject[],
|
||||||
|
): boolean {
|
||||||
|
if (!activeSubjects.length) return false;
|
||||||
|
|
||||||
|
const programme = Number(
|
||||||
|
assessment.programmeID ?? assessment.programme,
|
||||||
|
);
|
||||||
|
const metaclass = Number(
|
||||||
|
assessment.metaclassID ?? assessment.metaclass,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (programme && metaclass && !Number.isNaN(programme) && !Number.isNaN(metaclass)) {
|
||||||
|
return activeSubjects.some(
|
||||||
|
(subject) =>
|
||||||
|
subject.programme === programme && subject.metaclass === metaclass,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = String(assessment.code ?? assessment.subject ?? "").trim();
|
||||||
|
if (!code) return false;
|
||||||
|
return activeSubjects.some((subject) => subject.code === code);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterAssessmentsForActiveSubjects<T extends Record<string, unknown>>(
|
||||||
|
assessments: T[],
|
||||||
|
activeSubjects: OverviewSubject[],
|
||||||
|
): T[] {
|
||||||
|
return assessments.filter((assessment) =>
|
||||||
|
assessmentBelongsToActiveSubjects(assessment, activeSubjects),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function formatDate(dateStr: string, submitted?: boolean): string {
|
export function formatDate(dateStr: string, submitted?: boolean): string {
|
||||||
const d = new Date(dateStr);
|
const d = new Date(dateStr);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|||||||
Reference in New Issue
Block a user