241 lines
8.1 KiB
Vue
241 lines
8.1 KiB
Vue
<script setup lang="ts">
|
|
import { EMOJI_ABSENT, EMOJI_BEHAVIOR_GOOD, EMOJI_BEHAVIOR_MEDIOCRE, EMOJI_BEHAVIOR_POOR, EMOJI_CLASSROOM_READY, EMOJI_NOT_CLASSROOM_READY, EMOJI_PHONE_COMPLIANT, EMOJI_PHONE_NONCOMPLIANT, EMOJI_PRESENT, getDefaultEntry, type Entry } from '@/api/classroom_compliance'
|
|
import { computed, onMounted, ref, useTemplateRef, watch, type Ref } from 'vue'
|
|
|
|
const COMMENT_CHECKLIST_ITEMS = {
|
|
"Classroom Readiness": [
|
|
"Tardy",
|
|
"Out of Uniform",
|
|
"Use of wireless tech",
|
|
"10+ minute bathroom pass",
|
|
"Not having laptop",
|
|
"Not in assigned seat"
|
|
],
|
|
"Behavior": [
|
|
"Talking out of turn",
|
|
"Throwing objects in class",
|
|
"Rude language / comments to peers",
|
|
"Disrespectful towards the teacher"
|
|
]
|
|
}
|
|
|
|
const props = defineProps<{
|
|
dateStr: string
|
|
lastSaveStateTimestamp: number
|
|
disabled?: boolean
|
|
}>()
|
|
defineEmits<{
|
|
(e: 'editComment'): void
|
|
}>()
|
|
|
|
const model = defineModel<Entry | null>({
|
|
required: false,
|
|
})
|
|
const initialEntryJson: Ref<string> = ref('')
|
|
const previouslyRemovedEntry: Ref<Entry | null> = ref(null)
|
|
|
|
const entryChanged = computed(() => JSON.stringify(model.value) !== initialEntryJson.value)
|
|
const hasComment = computed(() => model.value && (model.value.comment.trim().length > 0 || model.value.checklistItems.length > 0))
|
|
|
|
const previousCommentValue: Ref<string> = ref('')
|
|
const previousCommentChecklistItems: Ref<string[]> = ref([])
|
|
const commentEditorDialog = useTemplateRef('commentEditorDialog')
|
|
|
|
onMounted(() => {
|
|
initialEntryJson.value = JSON.stringify(model.value)
|
|
watch(
|
|
() => props.lastSaveStateTimestamp,
|
|
() => {
|
|
initialEntryJson.value = JSON.stringify(model.value)
|
|
previouslyRemovedEntry.value = null
|
|
},
|
|
)
|
|
})
|
|
|
|
function toggleAbsence() {
|
|
if (model.value && !props.disabled) {
|
|
model.value.absent = !model.value.absent
|
|
if (model.value.absent) {
|
|
// Remove additional data if student is absent.
|
|
model.value.classroomReadiness = null
|
|
model.value.behaviorRating = null
|
|
} else {
|
|
// Populate default additional data if student is no longer absent.
|
|
model.value.classroomReadiness = true
|
|
model.value.behaviorRating = 3
|
|
// If we have an initial entry known, restore data from that.
|
|
if (initialEntryJson.value) {
|
|
const initialEntry = JSON.parse(initialEntryJson.value) as Entry
|
|
if (initialEntry === null) return
|
|
if (initialEntry.absent) return
|
|
if (initialEntry.classroomReadiness) {
|
|
model.value.classroomReadiness = initialEntry.classroomReadiness
|
|
}
|
|
if (initialEntry.phoneCompliant) {
|
|
model.value.phoneCompliant = initialEntry.phoneCompliant
|
|
}
|
|
if (initialEntry.behaviorRating) {
|
|
model.value.behaviorRating = initialEntry.behaviorRating
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function togglePhoneCompliance() {
|
|
if (model.value && model.value.phoneCompliant !== null && !props.disabled) {
|
|
model.value.phoneCompliant = !model.value.phoneCompliant
|
|
}
|
|
}
|
|
|
|
function toggleClassroomReadiness() {
|
|
if (model.value && model.value.classroomReadiness !== null && !props.disabled) {
|
|
model.value.classroomReadiness = !model.value.classroomReadiness
|
|
}
|
|
}
|
|
|
|
function toggleBehaviorRating() {
|
|
if (model.value && model.value.behaviorRating && !props.disabled) {
|
|
model.value.behaviorRating = model.value.behaviorRating - 1
|
|
if (model.value.behaviorRating < 1) {
|
|
model.value.behaviorRating = 3
|
|
}
|
|
}
|
|
}
|
|
|
|
function showCommentEditor() {
|
|
if (!model.value) return
|
|
previousCommentValue.value = model.value?.comment
|
|
previousCommentChecklistItems.value = [...model.value?.checklistItems]
|
|
commentEditorDialog.value?.showModal()
|
|
}
|
|
|
|
function cancelCommentEdit() {
|
|
if (model.value) {
|
|
model.value.comment = previousCommentValue.value
|
|
model.value.checklistItems = previousCommentChecklistItems.value
|
|
}
|
|
commentEditorDialog.value?.close()
|
|
}
|
|
|
|
function removeEntry() {
|
|
if (props.disabled) return
|
|
if (model.value) {
|
|
previouslyRemovedEntry.value = JSON.parse(JSON.stringify(model.value))
|
|
}
|
|
model.value = null
|
|
}
|
|
|
|
function addEntry() {
|
|
if (props.disabled) return
|
|
if (previouslyRemovedEntry.value) {
|
|
model.value = JSON.parse(JSON.stringify(previouslyRemovedEntry.value))
|
|
} else {
|
|
model.value = getDefaultEntry(props.dateStr)
|
|
}
|
|
}
|
|
</script>
|
|
<template>
|
|
<td :class="{ absent: model?.absent, changed: entryChanged, 'missing-entry': !model }">
|
|
<div v-if="model" class="cell-container">
|
|
<div>
|
|
<div class="status-item" :class="{ 'status-item-disabled': disabled }" @click="toggleAbsence">
|
|
<span v-if="model.absent" title="Absent">{{ EMOJI_ABSENT }}</span>
|
|
<span v-if="!model.absent" title="Present">{{ EMOJI_PRESENT }}</span>
|
|
</div>
|
|
<div class="status-item" :class="{ 'status-item-disabled': disabled }" @click="togglePhoneCompliance"
|
|
v-if="!model.absent && model.phoneCompliant !== null">
|
|
<span v-if="model.phoneCompliant" title="Phone Compliant">{{ EMOJI_PHONE_COMPLIANT }}</span>
|
|
<span v-if="!model.phoneCompliant" title="Phone Non-Compliant">{{ EMOJI_PHONE_NONCOMPLIANT }}</span>
|
|
</div>
|
|
<div class="status-item" :class="{ 'status-item-disabled': disabled }" @click="toggleClassroomReadiness"
|
|
v-if="!model.absent && model.classroomReadiness !== null">
|
|
<span v-if="model.classroomReadiness" title="Ready for Class">{{ EMOJI_CLASSROOM_READY }}</span>
|
|
<span v-if="!model.classroomReadiness" title="Not Ready for Class">{{ EMOJI_NOT_CLASSROOM_READY }}</span>
|
|
</div>
|
|
<div class="status-item" :class="{ 'status-item-disabled': disabled }" @click="toggleBehaviorRating"
|
|
v-if="!model.absent">
|
|
<span v-if="model.behaviorRating === 3" title="Good Behavior">{{ EMOJI_BEHAVIOR_GOOD }}</span>
|
|
<span v-if="model.behaviorRating === 2" title="Mediocre Behavior">{{ EMOJI_BEHAVIOR_MEDIOCRE }}</span>
|
|
<span v-if="model.behaviorRating === 1" title="Poor Behavior">{{ EMOJI_BEHAVIOR_POOR }}</span>
|
|
</div>
|
|
<div class="status-item" @click="showCommentEditor">
|
|
<span v-if="hasComment"
|
|
style="position: relative; float: right; top: 0px; right: 5px; font-size: 6px;">🔴</span>
|
|
<span title="Comments">💬</span>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="status-item" @click="removeEntry">
|
|
<span>🗑️</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="model === null && !disabled">
|
|
<div class="status-item" @click="addEntry">
|
|
<span>+</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- A comment editor dialog that shows up when the user edits their comment. -->
|
|
<dialog ref="commentEditorDialog" v-if="model">
|
|
<textarea v-model="model.comment" style="min-width: 300px; min-height: 100px;"
|
|
@keydown.enter="commentEditorDialog?.close()" :readonly="disabled"></textarea>
|
|
<div>
|
|
<div v-for="options, category in COMMENT_CHECKLIST_ITEMS" :key="category">
|
|
<h3 v-text="category"></h3>
|
|
<label v-for="opt in options" :key="opt">
|
|
<input type="checkbox" v-model="model.checklistItems" :value="opt" :disabled="disabled" />
|
|
<span v-text="opt"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="button-bar" style="text-align: right;">
|
|
<button type="button" @click="commentEditorDialog?.close()" :disabled="disabled">Confirm</button>
|
|
<button type="button" @click="model.comment = ''; model.checklistItems = []; commentEditorDialog?.close()"
|
|
:disabled="disabled">Clear</button>
|
|
<button type="button" @click="cancelCommentEdit">Cancel</button>
|
|
</div>
|
|
</dialog>
|
|
</td>
|
|
</template>
|
|
<style scoped>
|
|
td {
|
|
border: 1px solid black;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.missing-entry {
|
|
text-align: right;
|
|
}
|
|
|
|
.absent {
|
|
color: blue;
|
|
border: 1px solid black;
|
|
}
|
|
|
|
.changed {
|
|
border: 2px solid orange !important;
|
|
}
|
|
|
|
.status-item {
|
|
display: inline-block;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.status-item-disabled {
|
|
cursor: default !important;
|
|
}
|
|
|
|
.status-item+.status-item {
|
|
margin-left: 0.5em;
|
|
}
|
|
|
|
.cell-container {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 0.25em;
|
|
}
|
|
</style>
|