mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
feat: add more sorting options to kaban view
This commit is contained in:
@@ -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 getAssessmentYear(a: any): number {
|
||||||
|
const dateStr = a.due || a.date || a.dueDate || a.created;
|
||||||
|
return dateStr ? new Date(dateStr).getFullYear() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 determineStatus(assessment);
|
||||||
|
case "year":
|
||||||
|
return String(getAssessmentYear(assessment) || "Unknown");
|
||||||
|
case "subject":
|
||||||
|
return assessment.code || "Unknown";
|
||||||
|
case "grade":
|
||||||
|
return getAssessmentGrade(assessment);
|
||||||
|
case "title":
|
||||||
|
const first = (assessment.title || "?")[0].toUpperCase();
|
||||||
|
return /[A-Z0-9]/.test(first) ? first : "#";
|
||||||
|
default:
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
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() {
|
function updateAssessments() {
|
||||||
filteredAssessments = data.assessments.filter((a: any) => {
|
const result = buildGroupsAndColumns();
|
||||||
const subjectMatch =
|
filteredAssessments = result.filteredAssessments;
|
||||||
currentFilters.subject === "all" || a.code === currentFilters.subject;
|
statusGroups = result.statusGroups;
|
||||||
return subjectMatch;
|
columns = result.columns;
|
||||||
});
|
|
||||||
|
|
||||||
filteredAssessments.sort((a: any, b: any) => {
|
|
||||||
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;
|
|
||||||
case "subject":
|
|
||||||
return a.code.localeCompare(b.code);
|
|
||||||
case "title":
|
|
||||||
return a.title.localeCompare(b.title);
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
statusGroups = {
|
|
||||||
UPCOMING: [],
|
|
||||||
DUE_SOON: [],
|
|
||||||
OVERDUE: [],
|
|
||||||
SUBMITTED: [],
|
|
||||||
MARKS_RELEASED: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
filteredAssessments.forEach((assessment) => {
|
|
||||||
const status = determineStatus(assessment);
|
|
||||||
if (statusGroups[status]) {
|
|
||||||
statusGroups[status].push(assessment);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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}
|
||||||
@@ -381,4 +525,4 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
Reference in New Issue
Block a user