293 lines
9.6 KiB
Vue
293 lines
9.6 KiB
Vue
<script setup lang="ts">
|
|
import {
|
|
ClassroomComplianceAPIClient,
|
|
getDefaultEntry,
|
|
type EntriesPayload,
|
|
type EntriesPayloadStudent,
|
|
type EntriesResponseStudent,
|
|
} from '@/api/classroom_compliance'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import { computed, onMounted, ref, watch, type Ref } from 'vue'
|
|
import EntryTableCell from '@/apps/classroom_compliance/entries_table/EntryTableCell.vue'
|
|
import StudentScoreCell from '@/apps/classroom_compliance/entries_table/StudentScoreCell.vue'
|
|
import DateHeaderCell from '@/apps/classroom_compliance/entries_table/DateHeaderCell.vue'
|
|
import StudentNameCell from '@/apps/classroom_compliance/entries_table/StudentNameCell.vue'
|
|
|
|
const authStore = useAuthStore()
|
|
const props = defineProps<{
|
|
classId: number
|
|
}>()
|
|
const apiClient = new ClassroomComplianceAPIClient(authStore)
|
|
|
|
enum TableView {
|
|
FULL = "Full",
|
|
GRADING = "Grading",
|
|
WHITEBOARD = "Whiteboard",
|
|
TODAY = "Today"
|
|
}
|
|
|
|
const students: Ref<EntriesResponseStudent[]> = ref([])
|
|
const sortingChoice: Ref<string> = ref('name')
|
|
const selectedView: Ref<TableView> = ref(TableView.FULL)
|
|
|
|
const lastSaveState: Ref<string | null> = ref(null)
|
|
const lastSaveStateTimestamp: Ref<number> = ref(0)
|
|
|
|
const dates: Ref<string[]> = ref([])
|
|
const toDate: Ref<Date> = ref(new Date())
|
|
const fromDate: Ref<Date> = ref(new Date())
|
|
|
|
const entriesChangedSinceLastSave = computed(() => {
|
|
const studentsClone: EntriesResponseStudent[] = JSON.parse(JSON.stringify(students.value))
|
|
studentsClone.sort(sortEntriesByName)
|
|
return lastSaveState.value === null || lastSaveState.value !== JSON.stringify(studentsClone)
|
|
})
|
|
const assignedDesks = computed(() => {
|
|
return students.value.length > 0 && students.value.some(s => s.deskNumber > 0)
|
|
})
|
|
|
|
onMounted(async () => {
|
|
showThisWeek()
|
|
watch(sortingChoice, () => {
|
|
if (sortingChoice.value === 'name') {
|
|
students.value.sort(sortEntriesByName)
|
|
} else if (sortingChoice.value === 'desk') {
|
|
students.value.sort(sortEntriesByDeskNumber)
|
|
}
|
|
saveSortPreference()
|
|
})
|
|
// If the user selects "Today View", forcibly load the current week, so they'll only see today.
|
|
watch(selectedView, () => {
|
|
if (selectedView.value === TableView.TODAY) {
|
|
showThisWeek()
|
|
}
|
|
})
|
|
attemptSortByStoredPreference()
|
|
})
|
|
|
|
async function loadEntries() {
|
|
const entries = await apiClient
|
|
.getEntries(props.classId, fromDate.value, toDate.value)
|
|
.handleErrorsWithAlert()
|
|
if (entries) {
|
|
lastSaveState.value = JSON.stringify(entries.students)
|
|
lastSaveStateTimestamp.value = Date.now()
|
|
if (sortingChoice.value === 'name') {
|
|
entries.students.sort(sortEntriesByName)
|
|
} else if (sortingChoice.value === 'desk') {
|
|
entries.students.sort(sortEntriesByDeskNumber)
|
|
}
|
|
students.value = entries.students
|
|
dates.value = entries.dates
|
|
} else {
|
|
students.value = []
|
|
lastSaveState.value = null
|
|
lastSaveStateTimestamp.value = Date.now()
|
|
dates.value = []
|
|
}
|
|
}
|
|
|
|
function sortEntriesByName(a: EntriesResponseStudent, b: EntriesResponseStudent): number {
|
|
if (a.name < b.name) return -1
|
|
if (a.name > b.name) return 1
|
|
return 0
|
|
}
|
|
|
|
function sortEntriesByDeskNumber(a: EntriesResponseStudent, b: EntriesResponseStudent): number {
|
|
if (a.deskNumber < b.deskNumber) return -1
|
|
if (a.deskNumber > b.deskNumber) return 1
|
|
return sortEntriesByName(a, b)
|
|
}
|
|
|
|
function attemptSortByStoredPreference() {
|
|
const storedPreferences = localStorage.getItem(`entries-table.${props.classId}.preferences`)
|
|
if (storedPreferences !== null) {
|
|
const preferences: Record<string, string> = JSON.parse(storedPreferences)
|
|
if ('sort' in preferences) {
|
|
sortingChoice.value = preferences['sort']
|
|
}
|
|
}
|
|
}
|
|
|
|
function saveSortPreference() {
|
|
const preferences = {
|
|
'sort': sortingChoice.value
|
|
}
|
|
localStorage.setItem(`entries-table.${props.classId}.preferences`, JSON.stringify(preferences))
|
|
}
|
|
|
|
function shiftDateRange(days: number) {
|
|
toDate.value.setDate(toDate.value.getDate() + days)
|
|
fromDate.value.setDate(fromDate.value.getDate() + days)
|
|
}
|
|
|
|
async function showPreviousWeek() {
|
|
shiftDateRange(-7)
|
|
await loadEntries()
|
|
}
|
|
|
|
async function showThisWeek() {
|
|
const today = new Date()
|
|
today.setHours(0, 0, 0, 0)
|
|
// First set the to-date to the next upcoming end-of-week (Friday).
|
|
toDate.value = new Date()
|
|
toDate.value.setHours(0, 0, 0, 0)
|
|
if (today.getDay() >= 1 && today.getDay() <= 5) {
|
|
// If we're anywhere in the week, shift up to Friday.
|
|
const dayDiff = 5 - today.getDay()
|
|
toDate.value.setDate(today.getDate() + dayDiff)
|
|
} else {
|
|
// If it's Saturday or Sunday, shift back to the previous Friday.
|
|
if (today.getDay() === 6) {
|
|
toDate.value.setDate(today.getDate() - 1)
|
|
} else {
|
|
toDate.value.setDate(today.getDate() - 2)
|
|
}
|
|
}
|
|
|
|
// Then set the from-date to the Monday of that week.
|
|
fromDate.value = new Date()
|
|
fromDate.value.setHours(0, 0, 0, 0)
|
|
fromDate.value.setDate(toDate.value.getDate() - 4)
|
|
await loadEntries()
|
|
}
|
|
|
|
async function showNextWeek() {
|
|
shiftDateRange(7)
|
|
await loadEntries()
|
|
}
|
|
|
|
async function saveEdits() {
|
|
if (!lastSaveState.value) {
|
|
console.warn('No lastSaveState, cannot determine what edits were made.')
|
|
await loadEntries()
|
|
return
|
|
}
|
|
|
|
const payload: EntriesPayload = { students: [] }
|
|
// Get a list of edits which have changed.
|
|
const lastSaveStateObj: EntriesResponseStudent[] = JSON.parse(lastSaveState.value)
|
|
for (let i = 0; i < students.value.length; i++) {
|
|
const student: EntriesResponseStudent = students.value[i]
|
|
const studentPayload: EntriesPayloadStudent = { id: student.id, entries: {} }
|
|
for (const [dateStr, entry] of Object.entries(student.entries)) {
|
|
const lastSaveStateEntry = lastSaveStateObj[i].entries[dateStr]
|
|
if (JSON.stringify(lastSaveStateEntry) !== JSON.stringify(entry)) {
|
|
studentPayload.entries[dateStr] = JSON.parse(JSON.stringify(entry))
|
|
}
|
|
}
|
|
if (Object.keys(studentPayload.entries).length > 0) {
|
|
payload.students.push(studentPayload)
|
|
}
|
|
}
|
|
await apiClient.saveEntries(props.classId, payload).handleErrorsWithAlert()
|
|
await loadEntries()
|
|
}
|
|
|
|
async function discardEdits() {
|
|
if (lastSaveState.value) {
|
|
students.value = JSON.parse(lastSaveState.value)
|
|
} else {
|
|
await loadEntries()
|
|
}
|
|
}
|
|
|
|
function getVisibleDates(): string[] {
|
|
if (selectedView.value === TableView.FULL) return dates.value
|
|
if (selectedView.value === TableView.TODAY) {
|
|
const today = new Date()
|
|
today.setUTCHours(0, 0, 0, 0)
|
|
const todayStr = today.toISOString().substring(0, 10)
|
|
for (const date of dates.value) {
|
|
if (date === todayStr) return [date]
|
|
}
|
|
}
|
|
return []
|
|
}
|
|
|
|
function addAllEntriesForDate(dateStr: string) {
|
|
for (let i = 0; i < students.value.length; i++) {
|
|
const student = students.value[i]
|
|
if (student.removed) continue
|
|
if (!(dateStr in student.entries) || student.entries[dateStr] === null) {
|
|
student.entries[dateStr] = getDefaultEntry(dateStr)
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<template>
|
|
<div>
|
|
<div class="button-bar">
|
|
<button type="button" @click="showPreviousWeek" :disabled="selectedView === TableView.TODAY">Previous
|
|
Week</button>
|
|
<button type="button" @click="showThisWeek" :disabled="selectedView === TableView.TODAY">This Week</button>
|
|
<button type="button" @click="showNextWeek" :disabled="selectedView === TableView.TODAY">Next Week</button>
|
|
<button type="button" @click="saveEdits" :disabled="!entriesChangedSinceLastSave">
|
|
Save
|
|
</button>
|
|
<button type="button" @click="discardEdits" :disabled="!entriesChangedSinceLastSave">
|
|
Discard Edits
|
|
</button>
|
|
|
|
<select style="margin-left: 0.5em;" v-model="sortingChoice">
|
|
<option value="name">Sort by Name</option>
|
|
<option value="desk">Sort by Desk</option>
|
|
</select>
|
|
|
|
<select v-model="selectedView">
|
|
<option :value="TableView.FULL">Full View</option>
|
|
<option :value="TableView.GRADING">Grading View</option>
|
|
<option :value="TableView.WHITEBOARD">Whiteboard View</option>
|
|
<option :value="TableView.TODAY">Today View</option>
|
|
</select>
|
|
</div>
|
|
|
|
<p v-if="selectedView === TableView.GRADING">
|
|
Grading for the week from {{ fromDate.toLocaleDateString() }} to {{ toDate.toLocaleDateString() }}.
|
|
</p>
|
|
|
|
<table class="entries-table" :class="{ 'entries-table-reduced-view': selectedView !== TableView.FULL }">
|
|
<thead>
|
|
<tr>
|
|
<th>Student</th>
|
|
<th v-if="assignedDesks">Desk</th>
|
|
<DateHeaderCell v-for="date in getVisibleDates()" :key="date" :date-str="date"
|
|
@add-all-entries-clicked="addAllEntriesForDate(date)" />
|
|
<th v-if="selectedView !== TableView.WHITEBOARD">Score</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="student in students" :key="student.id">
|
|
<StudentNameCell :student="student" :class-id="classId" />
|
|
<!-- Desk Number: -->
|
|
<td v-if="assignedDesks" v-text="student.deskNumber"></td>
|
|
<!-- A cell for each entry in the table's date range: -->
|
|
<EntryTableCell v-for="(entry, date) in selectedView === TableView.FULL ? student.entries : []" :key="date"
|
|
v-model="student.entries[date]" :date-str="date" :last-save-state-timestamp="lastSaveStateTimestamp" />
|
|
<!-- Score cell: -->
|
|
<StudentScoreCell :score="student.score" v-if="selectedView !== TableView.WHITEBOARD" />
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</template>
|
|
<style scoped>
|
|
.entries-table {
|
|
margin-top: 0.5em;
|
|
margin-bottom: 1em;
|
|
width: 100%;
|
|
}
|
|
|
|
.entries-table-reduced-view {
|
|
width: auto;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.entries-table,
|
|
.entries-table th,
|
|
.entries-table td {
|
|
border: 1px solid black;
|
|
border-collapse: collapse;
|
|
}
|
|
</style>
|