teacher-tools/app/src/apps/classroom_compliance/EntriesTable.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>