203 lines
7.6 KiB
Vue
203 lines
7.6 KiB
Vue
<script setup lang="ts">
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import { computed, onMounted, ref, useTemplateRef, type Ref } from 'vue'
|
|
import EntriesTable from '@/apps/classroom_compliance/EntriesTable.vue'
|
|
import { useRouter } from 'vue-router'
|
|
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
|
import { ClassroomComplianceAPIClient, type Class, type ClassNote } from '@/api/classroom_compliance'
|
|
import ClassNoteItem from './ClassNoteItem.vue'
|
|
|
|
const props = defineProps<{
|
|
id: string
|
|
}>()
|
|
const authStore = useAuthStore()
|
|
const router = useRouter()
|
|
const cls: Ref<Class | null> = ref(null)
|
|
const notes: Ref<ClassNote[]> = ref([])
|
|
const noteContent: Ref<string> = ref('')
|
|
const scoreExpression: Ref<string> = ref('')
|
|
const scorePeriod: Ref<string> = ref('')
|
|
const canUpdateScoreParameters = computed(() => {
|
|
return cls.value && (
|
|
cls.value.scoreExpression !== scoreExpression.value ||
|
|
cls.value.scorePeriod !== scorePeriod.value
|
|
)
|
|
})
|
|
|
|
const entriesTable = useTemplateRef('entries-table')
|
|
const deleteClassDialog = useTemplateRef('deleteClassDialog')
|
|
|
|
const apiClient = new ClassroomComplianceAPIClient(authStore)
|
|
|
|
onMounted(() => {
|
|
loadClass()
|
|
})
|
|
|
|
function loadClass() {
|
|
const idNumber = parseInt(props.id, 10)
|
|
apiClient.getClass(idNumber).handleErrorsWithAlert().then(result => {
|
|
if (result) {
|
|
cls.value = result
|
|
refreshNotes()
|
|
scoreExpression.value = cls.value.scoreExpression
|
|
scorePeriod.value = cls.value.scorePeriod
|
|
} else {
|
|
router.back();
|
|
}
|
|
})
|
|
}
|
|
|
|
async function deleteThisClass() {
|
|
if (!cls.value || !(await deleteClassDialog.value?.show())) return
|
|
await apiClient.deleteClass(cls.value.id).handleErrorsWithAlert()
|
|
await router.replace('/classroom-compliance')
|
|
}
|
|
|
|
async function submitNote() {
|
|
if (noteContent.value.trim().length < 1 || !cls.value) return
|
|
const note = await apiClient.createClassNote(cls.value?.id, noteContent.value).handleErrorsWithAlert()
|
|
if (note) {
|
|
noteContent.value = ''
|
|
const updatedNotes = await apiClient.getClassNotes(cls.value?.id).handleErrorsWithAlert()
|
|
if (updatedNotes !== null) {
|
|
notes.value = updatedNotes
|
|
}
|
|
}
|
|
}
|
|
|
|
function refreshNotes() {
|
|
if (cls.value === null) {
|
|
notes.value = []
|
|
return
|
|
}
|
|
apiClient.getClassNotes(cls.value.id).handleErrorsWithAlert().then(notesResult => {
|
|
if (notesResult !== null) {
|
|
notes.value = notesResult
|
|
} else {
|
|
notes.value = []
|
|
}
|
|
})
|
|
}
|
|
|
|
async function resetStudentDesks() {
|
|
if (!cls.value) return
|
|
await apiClient.resetStudentDesks(cls.value.id).handleErrorsWithAlert()
|
|
// Reload the table!
|
|
await entriesTable.value?.loadEntries()
|
|
}
|
|
|
|
async function submitScoreParametersUpdate() {
|
|
if (!cls.value) return
|
|
const result = await apiClient.updateScoreParameters(
|
|
cls.value.id,
|
|
scoreExpression.value,
|
|
scorePeriod.value
|
|
).handleErrorsWithAlert()
|
|
if (result === null) return
|
|
// The class was updated successfully, so update the page's values.
|
|
cls.value = result
|
|
scoreExpression.value = cls.value.scoreExpression
|
|
scorePeriod.value = cls.value.scorePeriod
|
|
// Reload the table with the updated scores.
|
|
await entriesTable.value?.loadEntries()
|
|
}
|
|
|
|
function resetScoreParameters() {
|
|
if (!cls.value) return
|
|
scoreExpression.value = cls.value.scoreExpression
|
|
scorePeriod.value = cls.value.scorePeriod
|
|
}
|
|
</script>
|
|
<template>
|
|
<div v-if="cls">
|
|
<h1 class="align-center" style="margin-bottom: 0;">Class <span v-text="cls.number"></span></h1>
|
|
<p class="align-center" style="margin-top: 0; margin-bottom: 2em;">For the {{ cls.schoolYear }} school year.</p>
|
|
<div class="button-bar align-center" style="margin-bottom: 1em;">
|
|
<button type="button" @click="router.push(`/classroom-compliance/classes/${cls.id}/edit-student`)">Add
|
|
Student</button>
|
|
<button type="button" @click="router.push(`/classroom-compliance/classes/${cls.id}/import-students`)">Import
|
|
Students</button>
|
|
<button type="button" @click="resetStudentDesks">Clear Assigned Desks</button>
|
|
<button type="button" @click="deleteThisClass">Delete this Class</button>
|
|
</div>
|
|
|
|
<EntriesTable :classId="cls.id" ref="entries-table" />
|
|
|
|
<div>
|
|
<h3 style="margin-bottom: 0.25em;">Notes</h3>
|
|
<form @submit.prevent="submitNote">
|
|
<textarea style="min-height: 50px; min-width: 300px;" maxlength="2000" minlength="1"
|
|
v-model="noteContent"></textarea>
|
|
<button style="vertical-align: top; margin-left: 0.5em;" type="submit">Add Note</button>
|
|
</form>
|
|
<ClassNoteItem v-for="note in notes" :key="note.id" :note="note" @noteDeleted="refreshNotes()" />
|
|
</div>
|
|
|
|
<div>
|
|
<h3 style="margin-bottom: 0.25em;">Scoring</h3>
|
|
<p style="margin-top: 0.25em; margin-bottom: 0.25em;">
|
|
Change how scores are calculated for this class here.
|
|
</p>
|
|
<form @submit.prevent="submitScoreParametersUpdate">
|
|
<div>
|
|
<label for="score-expression-input">Expression</label>
|
|
<textarea id="score-expression-input" v-model="scoreExpression" class="text-mono" minlength="1"
|
|
maxlength="255" style="min-width: 500px; min-height: 50px;"></textarea>
|
|
</div>
|
|
<p class="form-input-hint">
|
|
The expression to use to calculate each student's score. This should be a simple mathematical expression that
|
|
can use the following variables:
|
|
</p>
|
|
<ul class="form-input-hint">
|
|
<li><span class="score-expression-variable">phone</span> - The student's phone score, defined as the number of
|
|
compliant days divided by total days present.</li>
|
|
<li><span class="score-expression-variable">behavior_good</span> - The proportion of days that the student had
|
|
good behavior.</li>
|
|
<li><span class="score-expression-variable">behavior_mediocre</span> - The proportion of days that the student
|
|
had mediocre behavior.</li>
|
|
<li><span class="score-expression-variable">behavior_poor</span> - The proportion of days that the student had
|
|
poor behavior.</li>
|
|
<li><span class="score-expression-variable">behavior</span> - A general behavior score, where a student gets
|
|
full points for good behavior, half-points for mediocre behavior, and no points for poor behavior.</li>
|
|
</ul>
|
|
<p class="form-input-hint">
|
|
As an example, a common scoring expression might be 50% phone-compliance, and 50% behavior. The expression
|
|
below would achieve that:
|
|
</p>
|
|
<p style="font-family: 'SourceCodePro', monospace; font-size: smaller;">
|
|
0.5 * phone + 0.5 * behavior
|
|
</p>
|
|
<div>
|
|
<label for="score-period-select">Period</label>
|
|
<select v-model="scorePeriod">
|
|
<option value="week">Weekly</option>
|
|
</select>
|
|
</div>
|
|
<p class="form-input-hint">
|
|
The period over which to calculate scores.
|
|
</p>
|
|
<div class="button-bar">
|
|
<button type="submit" :disabled="!canUpdateScoreParameters">Save</button>
|
|
<button type="button" :disabled="!canUpdateScoreParameters" @click="resetScoreParameters">Reset</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Confirmation dialog used for attempts at deleting this class. -->
|
|
<ConfirmDialog ref="deleteClassDialog">
|
|
<p>
|
|
Are you sure you want to delete this class? All data associated with it (settings, students,
|
|
entries, grades, etc.) will be <strong>permanently deleted</strong>, and deleted data is not
|
|
recoverable.
|
|
</p>
|
|
</ConfirmDialog>
|
|
</div>
|
|
</template>
|
|
<style scoped>
|
|
.score-expression-variable {
|
|
font-family: 'SourceCodePro', monospace;
|
|
font-style: normal;
|
|
color: rgb(165, 210, 253);
|
|
}
|
|
</style>
|