mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
Merge pull request #396 from AdenMGB/assement-overview
Assement overview improvements, notices fix and icon only sidebar
This commit is contained in:
@@ -305,6 +305,7 @@ function getDefaultValues(): SettingsState {
|
|||||||
customshortcuts: [],
|
customshortcuts: [],
|
||||||
lettergrade: false,
|
lettergrade: false,
|
||||||
newsSource: "australia",
|
newsSource: "australia",
|
||||||
|
iconOnlySidebar: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+86
-1
@@ -455,6 +455,63 @@ ul.magicDelete > li.deleting {
|
|||||||
width: 28px !important;
|
width: 28px !important;
|
||||||
height: 28px !important;
|
height: 28px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Icon Only Sidebar - compact mode. When submenu is open, all styles are disabled so it behaves as normal sidebar. */
|
||||||
|
body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) {
|
||||||
|
#menu {
|
||||||
|
width: 70px !important;
|
||||||
|
min-width: 70px !important;
|
||||||
|
max-width: 70px !important;
|
||||||
|
transition: width 0.2s ease;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu > ul {
|
||||||
|
min-width: 0 !important;
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
left: 70px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu li > label span,
|
||||||
|
#menu section > label span {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu ul > li > svg {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu .sub {
|
||||||
|
left: 70px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu ul li,
|
||||||
|
#menu ul section {
|
||||||
|
width: 100% !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
padding: 8px !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu li > label,
|
||||||
|
#menu section > label {
|
||||||
|
flex: 0 0 auto !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu li > label > svg,
|
||||||
|
#menu section > label > svg {
|
||||||
|
margin: 0 auto !important;
|
||||||
|
flex-shrink: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
[class*="notifications__items___"] {
|
[class*="notifications__items___"] {
|
||||||
-ms-overflow-style: none !important;
|
-ms-overflow-style: none !important;
|
||||||
scrollbar-width: none !important;
|
scrollbar-width: none !important;
|
||||||
@@ -921,6 +978,17 @@ html.transparencyEffects
|
|||||||
-webkit-transform: translatex(270px);
|
-webkit-transform: translatex(270px);
|
||||||
transform: translatex(270px);
|
transform: translatex(270px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) {
|
||||||
|
#menu {
|
||||||
|
-webkit-transform: translatex(-70px);
|
||||||
|
transform: translatex(-70px);
|
||||||
|
}
|
||||||
|
.menuShown #content {
|
||||||
|
-webkit-transform: translatex(70px);
|
||||||
|
transform: translatex(70px);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1145px) {
|
@media (max-width: 1145px) {
|
||||||
@@ -1677,6 +1745,22 @@ iframe.userHTML {
|
|||||||
background-image: unset;
|
background-image: unset;
|
||||||
background-color: var(--auto-background);
|
background-color: var(--auto-background);
|
||||||
}
|
}
|
||||||
|
[class*="OverallResult__OverallResult___"] {
|
||||||
|
--fill-colour: var(--assessment-accent-colour, var(--item-colour, var(--container-accent, rgb(var(--theme-sel-bg-parts)))));
|
||||||
|
}
|
||||||
|
[class*="OverallResult__OverallResult___"] [class*="Thermoscore__Thermoscore___"],
|
||||||
|
[class*="OverallResult__OverallResult___"] [class*="Thermoscore__fill___"],
|
||||||
|
[class*="OverallResult__OverallResult___"] > div > div {
|
||||||
|
--fill-colour: var(--assessment-accent-colour, var(--item-colour, var(--container-accent, rgb(var(--theme-sel-bg-parts)))));
|
||||||
|
}
|
||||||
|
[class*="AssessableCriterion__header___"] {
|
||||||
|
--fill-colour: var(--assessment-accent-colour, var(--item-colour, var(--container-accent, rgb(var(--theme-sel-bg-parts)))));
|
||||||
|
}
|
||||||
|
[class*="AssessableCriterion__header___"] [class*="Thermoscore__Thermoscore___"],
|
||||||
|
[class*="AssessableCriterion__header___"] [class*="Thermoscore__fill___"],
|
||||||
|
[class*="AssessableCriterion__header___"] > div > div > div {
|
||||||
|
--fill-colour: var(--assessment-accent-colour, var(--item-colour, var(--container-accent, rgb(var(--theme-sel-bg-parts)))));
|
||||||
|
}
|
||||||
.dark [class*="Thermoscore__Thermoscore___"] {
|
.dark [class*="Thermoscore__Thermoscore___"] {
|
||||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
@@ -3712,7 +3796,8 @@ div.day-empty {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.notice-content-body {
|
.notice-content-body {
|
||||||
overflow-y: hidden;
|
overflow-y: auto;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
|
|||||||
@@ -41,3 +41,13 @@
|
|||||||
#menu > ul:has(li.hasChildren.active) > li.active {
|
#menu > ul:has(li.hasChildren.active) > li.active {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Icon-only collapsed: submenu slides over narrow icons */
|
||||||
|
body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) #menu > ul:has(li.hasChildren.active) > li::before,
|
||||||
|
body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) #menu > ul ul:has(li.hasChildren.active) > li::before,
|
||||||
|
body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) #menu > ul:has(li.hasChildren.active) > li > label,
|
||||||
|
body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) #menu > ul:has(li.hasChildren.active) > li > svg,
|
||||||
|
body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) #menu > ul ul:has(li.hasChildren.active) > li > label,
|
||||||
|
body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) #menu > ul ul:has(li.hasChildren.active) > li > svg {
|
||||||
|
transform: translateX(-70px);
|
||||||
|
}
|
||||||
|
|||||||
@@ -149,6 +149,16 @@
|
|||||||
text: "Edit"
|
text: "Edit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Icon Only Sidebar",
|
||||||
|
description: "Show only icons in the sidebar for a compact layout.",
|
||||||
|
id: 14,
|
||||||
|
Component: Switch,
|
||||||
|
props: {
|
||||||
|
state: $settingsState.iconOnlySidebar ?? false,
|
||||||
|
onChange: (isOn: boolean) => settingsState.iconOnlySidebar = isOn
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Animations",
|
title: "Animations",
|
||||||
description: "Enables animations on certain pages.",
|
description: "Enables animations on certain pages.",
|
||||||
|
|||||||
@@ -180,8 +180,52 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
|
|||||||
`).firstChild!,
|
`).firstChild!,
|
||||||
assessmentsList.firstChild,
|
assessmentsList.firstChild,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
applySubjectColourToOverallResult();
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
applySubjectColourToOverallResult();
|
||||||
|
});
|
||||||
|
const wrapper = document.querySelector(".assessmentsWrapper");
|
||||||
|
if (wrapper) {
|
||||||
|
observer.observe(wrapper, { childList: true, subtree: true });
|
||||||
|
setTimeout(() => observer.disconnect(), 10000);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function applySubjectColourToOverallResult() {
|
||||||
|
const selectedAssessmentItem = document.querySelector(
|
||||||
|
"[class*='AssessmentItem__AssessmentItem___'][class*='selected___']",
|
||||||
|
) || document.querySelector(
|
||||||
|
"[class*='Collapsible__content___'] [class*='AssessmentItem__AssessmentItem___']",
|
||||||
|
);
|
||||||
|
const assessmentThermoscore = selectedAssessmentItem?.querySelector(
|
||||||
|
"[class*='Thermoscore__Thermoscore___']",
|
||||||
|
) as HTMLElement | null;
|
||||||
|
const overallResult = document.querySelector(
|
||||||
|
"[class*='OverallResult__OverallResult___']",
|
||||||
|
) as HTMLElement | null;
|
||||||
|
const assessableCriterionHeaders = document.querySelectorAll(
|
||||||
|
"[class*='AssessableCriterion__header___']",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (assessmentThermoscore && (overallResult || assessableCriterionHeaders.length > 0)) {
|
||||||
|
const accentColour =
|
||||||
|
getComputedStyle(assessmentThermoscore).getPropertyValue("--assessment-accent-colour").trim() ||
|
||||||
|
getComputedStyle(assessmentThermoscore).getPropertyValue("--fill-colour").trim() ||
|
||||||
|
getComputedStyle(assessmentThermoscore.closest("[class*='Collapsible__Collapsible___']") || assessmentThermoscore).getPropertyValue("--assessment-accent-colour").trim() ||
|
||||||
|
getComputedStyle(assessmentThermoscore.closest("[class*='Collapsible__Collapsible___']") || assessmentThermoscore).getPropertyValue("--item-colour").trim();
|
||||||
|
if (accentColour) {
|
||||||
|
overallResult?.style.setProperty("--assessment-accent-colour", accentColour);
|
||||||
|
overallResult?.style.setProperty("--fill-colour", accentColour);
|
||||||
|
assessableCriterionHeaders.forEach((el) => {
|
||||||
|
(el as HTMLElement).style.setProperty("--assessment-accent-colour", accentColour);
|
||||||
|
(el as HTMLElement).style.setProperty("--fill-colour", accentColour);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default assessmentsAveragePlugin;
|
export default assessmentsAveragePlugin;
|
||||||
|
|||||||
@@ -7,9 +7,11 @@
|
|||||||
|
|
||||||
interface FilterOptions {
|
interface FilterOptions {
|
||||||
subject: string;
|
subject: string;
|
||||||
sortBy: "due" | "grade" | "subject" | "title";
|
sortBy: "due" | "grade" | "subject" | "title" | "year";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HIDDEN_ASSESSMENTS_KEY = "betterseqta-hidden-assessments";
|
||||||
|
|
||||||
function percentageToLetter(percentage: number): string {
|
function percentageToLetter(percentage: number): string {
|
||||||
const letterMap: Record<number, string> = {
|
const letterMap: Record<number, string> = {
|
||||||
100: "A+",
|
100: "A+",
|
||||||
@@ -41,48 +43,108 @@
|
|||||||
|
|
||||||
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 }[] = [];
|
||||||
|
|
||||||
function updateAssessments() {
|
function getAssessmentYear(a: any): number {
|
||||||
filteredAssessments = data.assessments.filter((a: any) => {
|
const dateStr = a.due || a.date || a.dueDate || a.created;
|
||||||
const subjectMatch =
|
return dateStr ? new Date(dateStr).getFullYear() : 0;
|
||||||
currentFilters.subject === "all" || a.code === currentFilters.subject;
|
}
|
||||||
return subjectMatch;
|
|
||||||
});
|
|
||||||
|
|
||||||
filteredAssessments.sort((a: any, b: any) => {
|
function getAssessmentType(a: any): string {
|
||||||
|
return (a.type || a.assessmentType || a.taskType || "Other").toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAssessmentGrade(a: any): string {
|
||||||
|
const val = getGradeValue(a);
|
||||||
|
if (val === null) return "No grade";
|
||||||
|
return percentageToLetter(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGroupKey(assessment: any): string {
|
||||||
switch (currentFilters.sortBy) {
|
switch (currentFilters.sortBy) {
|
||||||
case "due":
|
case "due":
|
||||||
return new Date(a.due).getTime() - new Date(b.due).getTime();
|
return determineStatus(assessment);
|
||||||
case "grade":
|
case "year":
|
||||||
const gradeA = getGradeValue(a);
|
return String(getAssessmentYear(assessment) || "Unknown");
|
||||||
const gradeB = getGradeValue(b);
|
|
||||||
if (gradeA === null && gradeB === null) return 0;
|
|
||||||
if (gradeA === null) return 1;
|
|
||||||
if (gradeB === null) return -1;
|
|
||||||
return gradeB - gradeA;
|
|
||||||
case "subject":
|
case "subject":
|
||||||
return a.code.localeCompare(b.code);
|
return assessment.code || "Unknown";
|
||||||
|
case "grade":
|
||||||
|
return getAssessmentGrade(assessment);
|
||||||
case "title":
|
case "title":
|
||||||
return a.title.localeCompare(b.title);
|
const first = (assessment.title || "?")[0].toUpperCase();
|
||||||
|
return /[A-Z0-9]/.test(first) ? first : "#";
|
||||||
default:
|
default:
|
||||||
return 0;
|
return determineStatus(assessment);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortCompare(a: any, b: any): number {
|
||||||
|
return new Date(a.due || a.date || 0).getTime() - new Date(b.due || b.date || 0).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUS_COLUMNS = [
|
||||||
|
{ key: "UPCOMING", title: "Upcoming", className: "column-upcoming", icon: "📅" },
|
||||||
|
{ key: "DUE_SOON", title: "Due Soon", className: "column-due-soon", icon: "⏰" },
|
||||||
|
{ key: "OVERDUE", title: "Overdue", className: "column-overdue", icon: "🚨" },
|
||||||
|
{ key: "SUBMITTED", title: "Submitted", className: "column-submitted", icon: "📝" },
|
||||||
|
{ key: "MARKS_RELEASED", title: "Marked", className: "column-marked", icon: "✅" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function buildGroupsAndColumns() {
|
||||||
|
if (!data?.assessments) return { filteredAssessments: [], statusGroups: {}, columns: [] };
|
||||||
|
const subjectFilters = settingsState.subjectfilters || {};
|
||||||
|
const hiddenAssessmentIds = new Set(
|
||||||
|
(JSON.parse(localStorage.getItem(HIDDEN_ASSESSMENTS_KEY) || "[]")).map(String)
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
});
|
});
|
||||||
|
|
||||||
statusGroups = {
|
const groups: Record<string, any[]> = {};
|
||||||
UPCOMING: [],
|
filtered.forEach((assessment) => {
|
||||||
DUE_SOON: [],
|
const key = getGroupKey(assessment);
|
||||||
OVERDUE: [],
|
if (!groups[key]) groups[key] = [];
|
||||||
SUBMITTED: [],
|
groups[key].push(assessment);
|
||||||
MARKS_RELEASED: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
filteredAssessments.forEach((assessment) => {
|
|
||||||
const status = determineStatus(assessment);
|
|
||||||
if (statusGroups[status]) {
|
|
||||||
statusGroups[status].push(assessment);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Object.keys(groups).forEach((key) => {
|
||||||
|
groups[key].sort(sortCompare);
|
||||||
|
});
|
||||||
|
|
||||||
|
let cols: { key: string; title: string; className: string; icon: string }[];
|
||||||
|
if (currentFilters.sortBy === "due") {
|
||||||
|
cols = STATUS_COLUMNS;
|
||||||
|
} else {
|
||||||
|
const keys = Object.keys(groups).filter((k) => groups[k]?.length > 0);
|
||||||
|
if (currentFilters.sortBy === "year") {
|
||||||
|
cols = keys.sort((a, b) => Number(b) - Number(a)).map((k) => ({ key: k, title: k, className: "column-custom", icon: "📆" }));
|
||||||
|
} else if (currentFilters.sortBy === "subject") {
|
||||||
|
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: "📚" }));
|
||||||
|
} else {
|
||||||
|
cols = keys.sort().map((k) => ({ key: k, title: k, className: "column-custom", icon: "📋" }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { filteredAssessments: filtered, statusGroups: groups, columns: cols };
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (data) {
|
||||||
|
const _ = currentFilters.sortBy && currentFilters.subject;
|
||||||
|
const result = buildGroupsAndColumns();
|
||||||
|
filteredAssessments = result.filteredAssessments;
|
||||||
|
statusGroups = result.statusGroups;
|
||||||
|
columns = result.columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAssessments() {
|
||||||
|
const result = buildGroupsAndColumns();
|
||||||
|
filteredAssessments = result.filteredAssessments;
|
||||||
|
statusGroups = result.statusGroups;
|
||||||
|
columns = result.columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDueDateClass(assessment: any): string {
|
function getDueDateClass(assessment: any): string {
|
||||||
@@ -123,6 +185,56 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hideAssessment(assessment: any) {
|
||||||
|
const hidden = JSON.parse(localStorage.getItem(HIDDEN_ASSESSMENTS_KEY) || "[]");
|
||||||
|
const id = String(assessment.id);
|
||||||
|
if (!hidden.includes(id)) {
|
||||||
|
hidden.push(id);
|
||||||
|
localStorage.setItem(HIDDEN_ASSESSMENTS_KEY, JSON.stringify(hidden));
|
||||||
|
visibilityRefresh++;
|
||||||
|
closeAllMenus();
|
||||||
|
updateAssessments();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideSubject(subjectCode: string) {
|
||||||
|
const filters = { ...(settingsState.subjectfilters || {}) };
|
||||||
|
filters[subjectCode] = false;
|
||||||
|
settingsState.subjectfilters = filters;
|
||||||
|
closeAllMenus();
|
||||||
|
updateAssessments();
|
||||||
|
}
|
||||||
|
|
||||||
|
function unhideSubject(subjectCode: string) {
|
||||||
|
const filters = { ...(settingsState.subjectfilters || {}) };
|
||||||
|
filters[subjectCode] = true;
|
||||||
|
settingsState.subjectfilters = filters;
|
||||||
|
updateAssessments();
|
||||||
|
}
|
||||||
|
|
||||||
|
function unhideAssessment(assessmentId: string) {
|
||||||
|
const hidden = JSON.parse(localStorage.getItem(HIDDEN_ASSESSMENTS_KEY) || "[]");
|
||||||
|
const idStr = String(assessmentId);
|
||||||
|
const filtered = hidden.filter((id: string) => id !== idStr);
|
||||||
|
localStorage.setItem(HIDDEN_ASSESSMENTS_KEY, JSON.stringify(filtered));
|
||||||
|
visibilityRefresh++;
|
||||||
|
updateAssessments();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSubjectFilters() {
|
||||||
|
const filters = settingsState.subjectfilters || {};
|
||||||
|
let updated = false;
|
||||||
|
data.subjects.forEach((s: any) => {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(filters, s.code)) {
|
||||||
|
filters[s.code] = true;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (updated) {
|
||||||
|
settingsState.subjectfilters = filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function checkForCelebration() {
|
function checkForCelebration() {
|
||||||
const overdueCount = statusGroups.OVERDUE?.length || 0;
|
const overdueCount = statusGroups.OVERDUE?.length || 0;
|
||||||
const dueSoonCount = statusGroups.DUE_SOON?.length || 0;
|
const dueSoonCount = statusGroups.DUE_SOON?.length || 0;
|
||||||
@@ -201,6 +313,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let openMenuId: string | null = null;
|
let openMenuId: string | null = null;
|
||||||
|
let showVisibilityPanel = false;
|
||||||
|
let visibilityRefresh = 0;
|
||||||
|
|
||||||
|
$: hiddenSubjects = data?.subjects?.filter(
|
||||||
|
(s: any) => (settingsState.subjectfilters || {})[s.code] === false
|
||||||
|
) || [];
|
||||||
|
$: hiddenAssessmentIds = (() => {
|
||||||
|
visibilityRefresh; // Dependency for reactivity
|
||||||
|
return new Set((JSON.parse(localStorage.getItem(HIDDEN_ASSESSMENTS_KEY) || "[]")).map(String));
|
||||||
|
})();
|
||||||
|
$: hiddenAssessmentsWithInfo = data?.assessments?.filter(
|
||||||
|
(a: any) => hiddenAssessmentIds.has(String(a.id))
|
||||||
|
) || [];
|
||||||
|
$: hasHiddenItems = hiddenSubjects.length > 0 || hiddenAssessmentsWithInfo.length > 0;
|
||||||
|
|
||||||
function toggleMenu(assessmentId: string, event: Event) {
|
function toggleMenu(assessmentId: string, event: Event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -211,44 +337,13 @@
|
|||||||
openMenuId = null;
|
openMenuId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
$: if (data) {
|
||||||
if (data) {
|
initSubjectFilters();
|
||||||
updateAssessments();
|
updateAssessments();
|
||||||
}
|
void currentFilters.sortBy;
|
||||||
|
void currentFilters.subject;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
key: "UPCOMING",
|
|
||||||
title: "Upcoming",
|
|
||||||
className: "column-upcoming",
|
|
||||||
icon: "📅",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "DUE_SOON",
|
|
||||||
title: "Due Soon",
|
|
||||||
className: "column-due-soon",
|
|
||||||
icon: "⏰",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "OVERDUE",
|
|
||||||
title: "Overdue",
|
|
||||||
className: "column-overdue",
|
|
||||||
icon: "🚨",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "SUBMITTED",
|
|
||||||
title: "Submitted",
|
|
||||||
className: "column-submitted",
|
|
||||||
icon: "📝",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "MARKS_RELEASED",
|
|
||||||
title: "Marked",
|
|
||||||
className: "column-marked",
|
|
||||||
icon: "✅",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:click={closeAllMenus} />
|
<svelte:window on:click={closeAllMenus} />
|
||||||
@@ -263,15 +358,58 @@
|
|||||||
<option value={subject.code}>{subject.code} - {subject.title}</option>
|
<option value={subject.code}>{subject.code} - {subject.title}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
<select class="filter-select" bind:value={currentFilters.sortBy}>
|
<select class="filter-select" bind:value={currentFilters.sortBy} title="Group by - columns change based on this">
|
||||||
<option value="due">Sort by Due Date</option>
|
<option value="due">Group: Status</option>
|
||||||
<option value="grade">Sort by Grade</option>
|
<option value="year">Group: Year</option>
|
||||||
<option value="subject">Sort by Subject</option>
|
<option value="subject">Group: Subject</option>
|
||||||
<option value="title">Sort by Title</option>
|
<option value="grade">Group: Grade</option>
|
||||||
|
<option value="title">Group: Title (A-Z)</option>
|
||||||
</select>
|
</select>
|
||||||
|
{#if hasHiddenItems}
|
||||||
|
<button
|
||||||
|
class="visibility-toggle"
|
||||||
|
class:active={showVisibilityPanel}
|
||||||
|
on:click={() => (showVisibilityPanel = !showVisibilityPanel)}
|
||||||
|
title="Manage hidden subjects and assessments"
|
||||||
|
>
|
||||||
|
👁 Visibility ({hiddenSubjects.length + hiddenAssessmentsWithInfo.length})
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if showVisibilityPanel && hasHiddenItems}
|
||||||
|
<div class="visibility-panel">
|
||||||
|
<h4 class="visibility-panel-title">Hidden items</h4>
|
||||||
|
{#if hiddenSubjects.length > 0}
|
||||||
|
<div class="visibility-section">
|
||||||
|
<span class="visibility-label">Subjects:</span>
|
||||||
|
<div class="visibility-chips">
|
||||||
|
{#each hiddenSubjects as subject}
|
||||||
|
<span class="visibility-chip">
|
||||||
|
{subject.code}
|
||||||
|
<button class="visibility-unhide" on:click={() => unhideSubject(subject.code)}>Show</button>
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if hiddenAssessmentsWithInfo.length > 0}
|
||||||
|
<div class="visibility-section">
|
||||||
|
<span class="visibility-label">Assessments:</span>
|
||||||
|
<div class="visibility-chips">
|
||||||
|
{#each hiddenAssessmentsWithInfo as assessment}
|
||||||
|
<span class="visibility-chip">
|
||||||
|
{assessment.title}
|
||||||
|
<button class="visibility-unhide" on:click={() => unhideAssessment(assessment.id)}>Show</button>
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div id="main-grid-content">
|
<div id="main-grid-content">
|
||||||
{#if filteredAssessments.length === 0}
|
{#if filteredAssessments.length === 0}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
@@ -340,6 +478,12 @@
|
|||||||
Mark as Not Complete
|
Mark as Not Complete
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
<button class="menu-item menu-item-hide" on:click={() => hideAssessment(assessment)}>
|
||||||
|
Hide assessment
|
||||||
|
</button>
|
||||||
|
<button class="menu-item menu-item-hide" on:click={() => hideSubject(assessment.code)}>
|
||||||
|
Hide subject ({assessment.code})
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -349,7 +493,7 @@
|
|||||||
{#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.submitted)}
|
📅 {formatDate(assessment.due || assessment.date || assessment.dueDate || "", assessment.submitted)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -56,6 +56,18 @@ async function loadUpcoming(student: number) {
|
|||||||
return res.payload;
|
return res.payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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 loadPast(student: number, subjects: Subject[]) {
|
async function loadPast(student: number, subjects: Subject[]) {
|
||||||
const map: Record<number, any> = {};
|
const map: Record<number, any> = {};
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@@ -65,10 +77,22 @@ async function loadPast(student: number, subjects: Subject[]) {
|
|||||||
metaclass: s.metaclass,
|
metaclass: s.metaclass,
|
||||||
student,
|
student,
|
||||||
});
|
});
|
||||||
if (res.payload.tasks) {
|
const processAssessment = (t: any) => {
|
||||||
res.payload.tasks.forEach((t: any) => {
|
if (t && t.id) {
|
||||||
map[t.id] = t;
|
const merged = {
|
||||||
});
|
...t,
|
||||||
|
programmeID: t.programmeID || t.programme || s.programme,
|
||||||
|
metaclassID: t.metaclassID || t.metaclass || s.metaclass,
|
||||||
|
code: t.code || t.subject || s.code,
|
||||||
|
};
|
||||||
|
map[t.id] = normalizeAssessmentDates(merged, s);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (res.payload?.pending && Array.isArray(res.payload.pending)) {
|
||||||
|
res.payload.pending.forEach(processAssessment);
|
||||||
|
}
|
||||||
|
if (res.payload?.tasks && Array.isArray(res.payload.tasks)) {
|
||||||
|
res.payload.tasks.forEach(processAssessment);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -335,6 +335,141 @@
|
|||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-item.menu-item-hide {
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .menu-item.menu-item-hide {
|
||||||
|
color: var(--text-primary);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-toggle {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #64748b;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-toggle:hover {
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-toggle.active {
|
||||||
|
border-color: #d41e3a;
|
||||||
|
background: rgba(212, 30, 58, 0.08);
|
||||||
|
color: #d41e3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .visibility-toggle {
|
||||||
|
background: var(--background-primary);
|
||||||
|
border-color: var(--background-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .visibility-toggle:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .visibility-toggle.active {
|
||||||
|
border-color: #d41e3a;
|
||||||
|
background: rgba(212, 30, 58, 0.15);
|
||||||
|
color: #d41e3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-panel {
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
margin: 0 1rem 1rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .visibility-panel {
|
||||||
|
background: var(--background-secondary);
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-panel-title {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .visibility-panel-title {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-section {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #64748b;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .visibility-label {
|
||||||
|
color: var(--text-primary);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
background: #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: #1a1a1a;
|
||||||
|
max-width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .visibility-chip {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-unhide {
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
background: #d41e3a;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-unhide:hover {
|
||||||
|
background: #b91c33;
|
||||||
|
}
|
||||||
|
|
||||||
.assessment-title {
|
.assessment-title {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -455,6 +590,10 @@
|
|||||||
background: linear-gradient(135deg, #ffffff 0%, #f0fdf4 100%);
|
background: linear-gradient(135deg, #ffffff 0%, #f0fdf4 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.column-custom .column-header {
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f1f5f9 100%);
|
||||||
|
}
|
||||||
|
|
||||||
/* Dark mode column headers */
|
/* Dark mode column headers */
|
||||||
.dark .column-upcoming .column-header {
|
.dark .column-upcoming .column-header {
|
||||||
background: linear-gradient(135deg, var(--background-secondary) 0%, #1e3a8a 100%);
|
background: linear-gradient(135deg, var(--background-secondary) 0%, #1e3a8a 100%);
|
||||||
@@ -476,6 +615,10 @@
|
|||||||
background: linear-gradient(135deg, var(--background-secondary) 0%, #065f46 100%);
|
background: linear-gradient(135deg, var(--background-secondary) 0%, #065f46 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark .column-custom .column-header {
|
||||||
|
background: linear-gradient(135deg, var(--background-secondary) 0%, #1e3a5f 100%);
|
||||||
|
}
|
||||||
|
|
||||||
/* Subject filter view */
|
/* Subject filter view */
|
||||||
.subject-section {
|
.subject-section {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
|
|||||||
@@ -612,6 +612,7 @@ export function init() {
|
|||||||
if (settingsState.onoff) {
|
if (settingsState.onoff) {
|
||||||
console.info("[BetterSEQTA+] Enabled");
|
console.info("[BetterSEQTA+] Enabled");
|
||||||
if (settingsState.DarkMode) document.documentElement.classList.add("dark");
|
if (settingsState.DarkMode) document.documentElement.classList.add("dark");
|
||||||
|
if (settingsState.iconOnlySidebar) document.body.classList.add("icon-only-sidebar");
|
||||||
|
|
||||||
document.querySelector(".legacy-root")?.classList.add("hidden");
|
document.querySelector(".legacy-root")?.classList.add("hidden");
|
||||||
ObserveMenuItemPosition();
|
ObserveMenuItemPosition();
|
||||||
|
|||||||
@@ -617,12 +617,15 @@ export function getMockAssessmentsData() {
|
|||||||
{ submitted: false, score: null, dayOffset: () => Math.floor(Math.random() * -3) - 1 }, // Recently overdue
|
{ submitted: false, score: null, dayOffset: () => Math.floor(Math.random() * -3) - 1 }, // Recently overdue
|
||||||
];
|
];
|
||||||
|
|
||||||
const assessments = Array.from({ length: 12 }, (_, i) => {
|
const currentYear = new Date().getFullYear();
|
||||||
|
const assessments = Array.from({ length: 14 }, (_, i) => {
|
||||||
const subj = subjects[i % subjects.length];
|
const subj = subjects[i % subjects.length];
|
||||||
const template = statusTemplates[i % statusTemplates.length];
|
const template = statusTemplates[i % statusTemplates.length];
|
||||||
const due = new Date();
|
const due = new Date();
|
||||||
due.setDate(due.getDate() + template.dayOffset());
|
due.setDate(due.getDate() + template.dayOffset());
|
||||||
|
if (i >= 10) due.setFullYear(currentYear - 1);
|
||||||
|
|
||||||
|
const types = ["Assignment", "Test", "Exam", "Project", "Presentation", "Report"];
|
||||||
const assessment: any = {
|
const assessment: any = {
|
||||||
id: i + 1,
|
id: i + 1,
|
||||||
title: mockData.assessmentTitles[i % mockData.assessmentTitles.length],
|
title: mockData.assessmentTitles[i % mockData.assessmentTitles.length],
|
||||||
@@ -631,6 +634,7 @@ export function getMockAssessmentsData() {
|
|||||||
metaclassID: subj.metaclass,
|
metaclassID: subj.metaclass,
|
||||||
due: due.toISOString(),
|
due: due.toISOString(),
|
||||||
submitted: template.submitted,
|
submitted: template.submitted,
|
||||||
|
type: types[i % types.length],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (template.score && typeof template.score === 'function') {
|
if (template.score && typeof template.score === 'function') {
|
||||||
|
|||||||
@@ -30,6 +30,18 @@ export class StorageChangeHandler {
|
|||||||
"subjectfilters",
|
"subjectfilters",
|
||||||
FilterUpcomingAssessments.bind(this),
|
FilterUpcomingAssessments.bind(this),
|
||||||
);
|
);
|
||||||
|
settingsState.register(
|
||||||
|
"iconOnlySidebar",
|
||||||
|
this.handleIconOnlySidebarChange.bind(this),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleIconOnlySidebarChange(newValue: boolean | undefined) {
|
||||||
|
if (newValue) {
|
||||||
|
document.body.classList.add("icon-only-sidebar");
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove("icon-only-sidebar");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleDarkModeChange() {
|
private handleDarkModeChange() {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export interface SettingsState {
|
|||||||
newsSource?: string;
|
newsSource?: string;
|
||||||
mockNotices?: boolean;
|
mockNotices?: boolean;
|
||||||
hideSensitiveContent?: boolean;
|
hideSensitiveContent?: boolean;
|
||||||
|
iconOnlySidebar?: boolean;
|
||||||
|
|
||||||
// depreciated keys
|
// depreciated keys
|
||||||
animatedbk: boolean;
|
animatedbk: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user