mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +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: [],
|
||||
lettergrade: false,
|
||||
newsSource: "australia",
|
||||
iconOnlySidebar: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+86
-1
@@ -455,6 +455,63 @@ ul.magicDelete > li.deleting {
|
||||
width: 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___"] {
|
||||
-ms-overflow-style: none !important;
|
||||
scrollbar-width: none !important;
|
||||
@@ -921,6 +978,17 @@ html.transparencyEffects
|
||||
-webkit-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) {
|
||||
@@ -1677,6 +1745,22 @@ iframe.userHTML {
|
||||
background-image: unset;
|
||||
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___"] {
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
@@ -3712,7 +3796,8 @@ div.day-empty {
|
||||
}
|
||||
|
||||
.notice-content-body {
|
||||
overflow-y: hidden;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
|
||||
@@ -41,3 +41,13 @@
|
||||
#menu > ul:has(li.hasChildren.active) > li.active {
|
||||
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"
|
||||
}
|
||||
},
|
||||
{
|
||||
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",
|
||||
description: "Enables animations on certain pages.",
|
||||
|
||||
@@ -180,8 +180,52 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
|
||||
`).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;
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
|
||||
interface FilterOptions {
|
||||
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 {
|
||||
const letterMap: Record<number, string> = {
|
||||
100: "A+",
|
||||
@@ -41,48 +43,108 @@
|
||||
|
||||
let filteredAssessments: any[] = [];
|
||||
let statusGroups: Record<string, any[]> = {};
|
||||
let columns: { key: string; title: string; className: string; icon: string }[] = [];
|
||||
|
||||
function updateAssessments() {
|
||||
filteredAssessments = data.assessments.filter((a: any) => {
|
||||
const subjectMatch =
|
||||
currentFilters.subject === "all" || a.code === currentFilters.subject;
|
||||
return subjectMatch;
|
||||
});
|
||||
function getAssessmentYear(a: any): number {
|
||||
const dateStr = a.due || a.date || a.dueDate || a.created;
|
||||
return dateStr ? new Date(dateStr).getFullYear() : 0;
|
||||
}
|
||||
|
||||
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) {
|
||||
case "due":
|
||||
return new Date(a.due).getTime() - new Date(b.due).getTime();
|
||||
case "grade":
|
||||
const gradeA = getGradeValue(a);
|
||||
const gradeB = getGradeValue(b);
|
||||
if (gradeA === null && gradeB === null) return 0;
|
||||
if (gradeA === null) return 1;
|
||||
if (gradeB === null) return -1;
|
||||
return gradeB - gradeA;
|
||||
return determineStatus(assessment);
|
||||
case "year":
|
||||
return String(getAssessmentYear(assessment) || "Unknown");
|
||||
case "subject":
|
||||
return a.code.localeCompare(b.code);
|
||||
return assessment.code || "Unknown";
|
||||
case "grade":
|
||||
return getAssessmentGrade(assessment);
|
||||
case "title":
|
||||
return a.title.localeCompare(b.title);
|
||||
const first = (assessment.title || "?")[0].toUpperCase();
|
||||
return /[A-Z0-9]/.test(first) ? first : "#";
|
||||
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 = {
|
||||
UPCOMING: [],
|
||||
DUE_SOON: [],
|
||||
OVERDUE: [],
|
||||
SUBMITTED: [],
|
||||
MARKS_RELEASED: [],
|
||||
};
|
||||
|
||||
filteredAssessments.forEach((assessment) => {
|
||||
const status = determineStatus(assessment);
|
||||
if (statusGroups[status]) {
|
||||
statusGroups[status].push(assessment);
|
||||
}
|
||||
const groups: Record<string, any[]> = {};
|
||||
filtered.forEach((assessment) => {
|
||||
const key = getGroupKey(assessment);
|
||||
if (!groups[key]) groups[key] = [];
|
||||
groups[key].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 {
|
||||
@@ -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() {
|
||||
const overdueCount = statusGroups.OVERDUE?.length || 0;
|
||||
const dueSoonCount = statusGroups.DUE_SOON?.length || 0;
|
||||
@@ -201,6 +313,20 @@
|
||||
}
|
||||
|
||||
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) {
|
||||
event.stopPropagation();
|
||||
@@ -211,44 +337,13 @@
|
||||
openMenuId = null;
|
||||
}
|
||||
|
||||
$: {
|
||||
if (data) {
|
||||
$: if (data) {
|
||||
initSubjectFilters();
|
||||
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>
|
||||
|
||||
<svelte:window on:click={closeAllMenus} />
|
||||
@@ -263,15 +358,58 @@
|
||||
<option value={subject.code}>{subject.code} - {subject.title}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<select class="filter-select" bind:value={currentFilters.sortBy}>
|
||||
<option value="due">Sort by Due Date</option>
|
||||
<option value="grade">Sort by Grade</option>
|
||||
<option value="subject">Sort by Subject</option>
|
||||
<option value="title">Sort by Title</option>
|
||||
<select class="filter-select" bind:value={currentFilters.sortBy} title="Group by - columns change based on this">
|
||||
<option value="due">Group: Status</option>
|
||||
<option value="year">Group: Year</option>
|
||||
<option value="subject">Group: Subject</option>
|
||||
<option value="grade">Group: Grade</option>
|
||||
<option value="title">Group: Title (A-Z)</option>
|
||||
</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>
|
||||
|
||||
{#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">
|
||||
{#if filteredAssessments.length === 0}
|
||||
<div class="empty-state">
|
||||
@@ -340,6 +478,12 @@
|
||||
Mark as Not Complete
|
||||
</button>
|
||||
{/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>
|
||||
{/if}
|
||||
@@ -349,7 +493,7 @@
|
||||
{#if !assessment.results && !isCompleted}
|
||||
<div class="assessment-meta">
|
||||
<div class="due-date {dueDateClass}">
|
||||
📅 {formatDate(assessment.due, assessment.submitted)}
|
||||
📅 {formatDate(assessment.due || assessment.date || assessment.dueDate || "", assessment.submitted)}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -56,6 +56,18 @@ async function loadUpcoming(student: number) {
|
||||
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[]) {
|
||||
const map: Record<number, any> = {};
|
||||
await Promise.all(
|
||||
@@ -65,10 +77,22 @@ async function loadPast(student: number, subjects: Subject[]) {
|
||||
metaclass: s.metaclass,
|
||||
student,
|
||||
});
|
||||
if (res.payload.tasks) {
|
||||
res.payload.tasks.forEach((t: any) => {
|
||||
map[t.id] = t;
|
||||
});
|
||||
const processAssessment = (t: any) => {
|
||||
if (t && t.id) {
|
||||
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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
@@ -455,6 +590,10 @@
|
||||
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 .column-upcoming .column-header {
|
||||
background: linear-gradient(135deg, var(--background-secondary) 0%, #1e3a8a 100%);
|
||||
@@ -476,6 +615,10 @@
|
||||
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-section {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
@@ -612,6 +612,7 @@ export function init() {
|
||||
if (settingsState.onoff) {
|
||||
console.info("[BetterSEQTA+] Enabled");
|
||||
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");
|
||||
ObserveMenuItemPosition();
|
||||
|
||||
@@ -617,12 +617,15 @@ export function getMockAssessmentsData() {
|
||||
{ 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 template = statusTemplates[i % statusTemplates.length];
|
||||
const due = new Date();
|
||||
due.setDate(due.getDate() + template.dayOffset());
|
||||
if (i >= 10) due.setFullYear(currentYear - 1);
|
||||
|
||||
const types = ["Assignment", "Test", "Exam", "Project", "Presentation", "Report"];
|
||||
const assessment: any = {
|
||||
id: i + 1,
|
||||
title: mockData.assessmentTitles[i % mockData.assessmentTitles.length],
|
||||
@@ -631,6 +634,7 @@ export function getMockAssessmentsData() {
|
||||
metaclassID: subj.metaclass,
|
||||
due: due.toISOString(),
|
||||
submitted: template.submitted,
|
||||
type: types[i % types.length],
|
||||
};
|
||||
|
||||
if (template.score && typeof template.score === 'function') {
|
||||
|
||||
@@ -30,6 +30,18 @@ export class StorageChangeHandler {
|
||||
"subjectfilters",
|
||||
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() {
|
||||
|
||||
@@ -40,6 +40,7 @@ export interface SettingsState {
|
||||
newsSource?: string;
|
||||
mockNotices?: boolean;
|
||||
hideSensitiveContent?: boolean;
|
||||
iconOnlySidebar?: boolean;
|
||||
|
||||
// depreciated keys
|
||||
animatedbk: boolean;
|
||||
|
||||
Reference in New Issue
Block a user