Added comments, and fixed #5, #4, and #3.

This commit is contained in:
Andrew Lalis 2024-12-28 11:04:57 -05:00
parent 7c92c600e6
commit 65e61a9fbf
10 changed files with 265 additions and 41 deletions

View File

@ -23,7 +23,8 @@ CREATE TABLE classroom_compliance_entry (
ON UPDATE CASCADE ON DELETE CASCADE, ON UPDATE CASCADE ON DELETE CASCADE,
date TEXT NOT NULL, date TEXT NOT NULL,
created_at INTEGER NOT NULL, created_at INTEGER NOT NULL,
absent INTEGER NOT NULL DEFAULT 0 absent INTEGER NOT NULL DEFAULT 0,
comment TEXT NOT NULL DEFAULT ''
); );
CREATE TABLE classroom_compliance_entry_phone ( CREATE TABLE classroom_compliance_entry_phone (

View File

@ -36,6 +36,7 @@ struct ClassroomComplianceEntry {
const string date; const string date;
const ulong createdAt; const ulong createdAt;
const bool absent; const bool absent;
const string comment;
} }
struct ClassroomComplianceEntryPhone { struct ClassroomComplianceEntryPhone {
@ -63,6 +64,7 @@ void registerApiEndpoints(PathHandler handler) {
handler.addMapping(Method.GET, STUDENT_PATH, &getStudent); handler.addMapping(Method.GET, STUDENT_PATH, &getStudent);
handler.addMapping(Method.PUT, STUDENT_PATH, &updateStudent); handler.addMapping(Method.PUT, STUDENT_PATH, &updateStudent);
handler.addMapping(Method.DELETE, STUDENT_PATH, &deleteStudent); handler.addMapping(Method.DELETE, STUDENT_PATH, &deleteStudent);
handler.addMapping(Method.GET, STUDENT_PATH ~ "/entries", &getStudentEntries);
handler.addMapping(Method.GET, CLASS_PATH ~ "/entries", &getEntries); handler.addMapping(Method.GET, CLASS_PATH ~ "/entries", &getEntries);
handler.addMapping(Method.POST, CLASS_PATH ~ "/entries", &saveEntries); handler.addMapping(Method.POST, CLASS_PATH ~ "/entries", &saveEntries);
@ -344,6 +346,7 @@ void getEntries(ref HttpRequestContext ctx) {
entry.date, entry.date,
entry.created_at, entry.created_at,
entry.absent, entry.absent,
entry.comment,
student.id, student.id,
student.name, student.name,
student.desk_number, student.desk_number,
@ -373,21 +376,22 @@ void getEntries(ref HttpRequestContext ctx) {
entry.object["date"] = JSONValue(row.peek!string(1)); entry.object["date"] = JSONValue(row.peek!string(1));
entry.object["createdAt"] = JSONValue(row.peek!ulong(2)); entry.object["createdAt"] = JSONValue(row.peek!ulong(2));
entry.object["absent"] = JSONValue(row.peek!bool(3)); entry.object["absent"] = JSONValue(row.peek!bool(3));
entry.object["comment"] = JSONValue(row.peek!string(4));
JSONValue phone = JSONValue(null); JSONValue phone = JSONValue(null);
JSONValue behavior = JSONValue(null); JSONValue behavior = JSONValue(null);
if (!entry.object["absent"].boolean()) { if (!entry.object["absent"].boolean()) {
phone = JSONValue.emptyObject; phone = JSONValue.emptyObject;
phone.object["compliant"] = JSONValue(row.peek!bool(8)); phone.object["compliant"] = JSONValue(row.peek!bool(9));
behavior = JSONValue.emptyObject; behavior = JSONValue.emptyObject;
behavior.object["rating"] = JSONValue(row.peek!ubyte(9)); behavior.object["rating"] = JSONValue(row.peek!ubyte(10));
} }
entry.object["phone"] = phone; entry.object["phone"] = phone;
entry.object["behavior"] = behavior; entry.object["behavior"] = behavior;
string dateStr = entry.object["date"].str(); string dateStr = entry.object["date"].str();
// Find the student object this entry belongs to, then add it to their list. // Find the student object this entry belongs to, then add it to their list.
ulong studentId = row.peek!ulong(4); ulong studentId = row.peek!ulong(5);
bool studentFound = false; bool studentFound = false;
foreach (idx, student; students) { foreach (idx, student; students) {
if (student.id == studentId) { if (student.id == studentId) {
@ -525,11 +529,13 @@ private void insertNewEntry(
) { ) {
ulong createdAt = getUnixTimestampMillis(); ulong createdAt = getUnixTimestampMillis();
bool absent = payload.object["absent"].boolean; bool absent = payload.object["absent"].boolean;
string comment = payload.object["comment"].str;
if (comment is null) comment = "";
db.execute( db.execute(
"INSERT INTO classroom_compliance_entry "INSERT INTO classroom_compliance_entry
(class_id, student_id, date, created_at, absent) (class_id, student_id, date, created_at, absent, comment)
VALUES (?, ?, ?, ?, ?)", VALUES (?, ?, ?, ?, ?, ?)",
classId, studentId, dateStr, createdAt, absent classId, studentId, dateStr, createdAt, absent, comment
); );
if (!absent) { if (!absent) {
ulong entryId = db.lastInsertRowid(); ulong entryId = db.lastInsertRowid();
@ -564,11 +570,14 @@ private void updateEntry(
JSONValue obj JSONValue obj
) { ) {
bool absent = obj.object["absent"].boolean; bool absent = obj.object["absent"].boolean;
string comment = obj.object["comment"].str;
if (comment is null) comment = "";
db.execute( db.execute(
"UPDATE classroom_compliance_entry "UPDATE classroom_compliance_entry
SET absent = ? SET absent = ?, comment = ?
WHERE class_id = ? AND student_id = ? AND date = ? AND id = ?", WHERE class_id = ? AND student_id = ? AND date = ? AND id = ?",
absent, classId, studentId, dateStr, entryId absent, comment,
classId, studentId, dateStr, entryId
); );
if (absent) { if (absent) {
db.execute( db.execute(
@ -726,3 +735,51 @@ private Optional!double calculateScore(
double score = 0.3 * phoneScore + 0.7 * behaviorScore; double score = 0.3 * phoneScore + 0.7 * behaviorScore;
return Optional!double.of(score); return Optional!double.of(score);
} }
void getStudentEntries(ref HttpRequestContext ctx) {
auto db = getDb();
User user = getUserOrThrow(ctx, db);
auto student = getStudentOrThrow(ctx, db, user);
const query = "
SELECT
e.id,
e.date,
e.created_at,
e.absent,
e.comment,
p.compliant,
b.rating
FROM classroom_compliance_entry e
LEFT JOIN classroom_compliance_entry_phone p
ON p.entry_id = e.id
LEFT JOIN classroom_compliance_entry_behavior b
ON b.entry_id = e.id
WHERE
e.student_id = ?
ORDER BY e.date DESC
";
JSONValue response = JSONValue.emptyArray;
foreach (row; db.execute(query, student.id)) {
JSONValue e = JSONValue.emptyObject;
bool absent = row.peek!bool(3);
e.object["id"] = JSONValue(row.peek!ulong(0));
e.object["date"] = JSONValue(row.peek!string(1));
e.object["createdAt"] = JSONValue(row.peek!ulong(2));
e.object["absent"] = JSONValue(absent);
e.object["comment"] = JSONValue(row.peek!string(4));
if (absent) {
e.object["phone"] = JSONValue(null);
e.object["behavior"] = JSONValue(null);
} else {
JSONValue phone = JSONValue.emptyObject;
phone.object["compliant"] = JSONValue(row.peek!bool(5));
e.object["phone"] = phone;
JSONValue behavior = JSONValue.emptyObject;
behavior.object["rating"] = JSONValue(row.peek!ubyte(6));
e.object["behavior"] = behavior;
}
response.array ~= e;
}
ctx.response.writeBodyString(response.toJSON(), "application/json");
}

View File

@ -63,16 +63,13 @@ void insertSampleData(ref Database db) {
bool absent = uniform01(rand) < 0.05; bool absent = uniform01(rand) < 0.05;
bool phoneCompliant = uniform01(rand) < 0.85; bool phoneCompliant = uniform01(rand) < 0.85;
ubyte behaviorRating = 3; ubyte behaviorRating = 3;
string behaviorComment = null;
if (uniform01(rand) < 0.25) { if (uniform01(rand) < 0.25) {
behaviorRating = 2; behaviorRating = 2;
behaviorComment = "They did not participate enough.";
if (uniform01(rand) < 0.5) { if (uniform01(rand) < 0.5) {
behaviorRating = 3; behaviorRating = 3;
behaviorComment = "They are a horrible student.";
} }
} }
addEntry(db, classId, studentId, entryDate, absent, phoneCompliant, behaviorRating, behaviorComment); addEntry(db, classId, studentId, entryDate, absent, phoneCompliant, behaviorRating);
} }
} }
} }
@ -107,21 +104,28 @@ void addEntry(
Date date, Date date,
bool absent, bool absent,
bool phoneCompliant, bool phoneCompliant,
ubyte behaviorRating, ubyte behaviorRating
string behaviorComment
) { ) {
const entryQuery = " const entryQuery = "
INSERT INTO classroom_compliance_entry INSERT INTO classroom_compliance_entry
(class_id, student_id, date, created_at, absent) (class_id, student_id, date, created_at, absent, comment)
VALUES (?, ?, ?, ?, ?)"; VALUES (?, ?, ?, ?, ?, ?)";
db.execute(entryQuery, classId, studentId, date.toISOExtString(), getUnixTimestampMillis(), absent); db.execute(
entryQuery,
classId,
studentId,
date.toISOExtString(),
getUnixTimestampMillis(),
absent,
"Sample comment."
);
if (absent) return; if (absent) return;
ulong entryId = db.lastInsertRowid(); ulong entryId = db.lastInsertRowid();
const phoneQuery = "INSERT INTO classroom_compliance_entry_phone (entry_id, compliant) VALUES (?, ?)"; const phoneQuery = "INSERT INTO classroom_compliance_entry_phone (entry_id, compliant) VALUES (?, ?)";
db.execute(phoneQuery, entryId, phoneCompliant); db.execute(phoneQuery, entryId, phoneCompliant);
const behaviorQuery = " const behaviorQuery = "
INSERT INTO classroom_compliance_entry_behavior INSERT INTO classroom_compliance_entry_behavior
(entry_id, rating, comment) (entry_id, rating)
VALUES (?, ?, ?)"; VALUES (?, ?)";
db.execute(behaviorQuery, entryId, behaviorRating, behaviorComment); db.execute(behaviorQuery, entryId, behaviorRating);
} }

View File

@ -2,6 +2,14 @@ import { APIClient, type APIResponse, type AuthStoreType } from './base'
const BASE_URL = import.meta.env.VITE_API_URL + '/classroom-compliance' const BASE_URL = import.meta.env.VITE_API_URL + '/classroom-compliance'
export const EMOJI_PHONE_COMPLIANT = '📱'
export const EMOJI_PHONE_NONCOMPLIANT = '📵'
export const EMOJI_PRESENT = '✅'
export const EMOJI_ABSENT = '❌'
export const EMOJI_BEHAVIOR_GOOD = '😇'
export const EMOJI_BEHAVIOR_MEDIOCRE = '😐'
export const EMOJI_BEHAVIOR_POOR = '😡'
export interface Class { export interface Class {
id: number id: number
number: number number: number
@ -31,6 +39,7 @@ export interface Entry {
absent: boolean absent: boolean
phone: EntryPhone | null phone: EntryPhone | null
behavior: EntryBehavior | null behavior: EntryBehavior | null
comment: string
} }
export function getDefaultEntry(dateStr: string): Entry { export function getDefaultEntry(dateStr: string): Entry {
@ -41,6 +50,7 @@ export function getDefaultEntry(dateStr: string): Entry {
absent: false, absent: false,
phone: { compliant: true }, phone: { compliant: true },
behavior: { rating: 3 }, behavior: { rating: 3 },
comment: '',
} }
} }
@ -148,4 +158,8 @@ export class ClassroomComplianceAPIClient extends APIClient {
params.append('to', toDate.toISOString().substring(0, 10)) params.append('to', toDate.toISOString().substring(0, 10))
return super.get(`/classes/${classId}/scores?${params.toString()}`) return super.get(`/classes/${classId}/scores?${params.toString()}`)
} }
getStudentEntries(classId: number, studentId: number): APIResponse<Entry[]> {
return super.get(`/classes/${classId}/students/${studentId}/entries`)
}
} }

View File

@ -32,8 +32,7 @@ async function deleteThisClass() {
</script> </script>
<template> <template>
<div v-if="cls"> <div v-if="cls">
<h1>Class #<span v-text="cls.number"></span></h1> <h1>Class <span v-text="cls.number"></span></h1>
<p>ID: <span v-text="cls.id"></span></p>
<p>School Year: <span v-text="cls.schoolYear"></span></p> <p>School Year: <span v-text="cls.schoolYear"></span></p>
<div class="button-bar" style="margin-bottom: 1em;"> <div class="button-bar" style="margin-bottom: 1em;">
<button type="button" @click="router.push(`/classroom-compliance/classes/${cls.id}/edit-student`)">Add <button type="button" @click="router.push(`/classroom-compliance/classes/${cls.id}/edit-student`)">Add

View File

@ -7,7 +7,7 @@ import {
type EntriesResponseStudent, type EntriesResponseStudent,
} from '@/api/classroom_compliance' } from '@/api/classroom_compliance'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { computed, onMounted, ref, type Ref } from 'vue' import { computed, onMounted, ref, watch, type Ref } from 'vue'
import EntryTableCell from '@/apps/classroom_compliance/EntryTableCell.vue' import EntryTableCell from '@/apps/classroom_compliance/EntryTableCell.vue'
import { RouterLink } from 'vue-router' import { RouterLink } from 'vue-router'
import StudentScoreCell from '@/apps/classroom_compliance/StudentScoreCell.vue' import StudentScoreCell from '@/apps/classroom_compliance/StudentScoreCell.vue'
@ -20,6 +20,8 @@ const props = defineProps<{
const apiClient = new ClassroomComplianceAPIClient(authStore) const apiClient = new ClassroomComplianceAPIClient(authStore)
const students: Ref<EntriesResponseStudent[]> = ref([]) const students: Ref<EntriesResponseStudent[]> = ref([])
const sortingChoice: Ref<string> = ref('name')
const gradingView = ref(false)
const lastSaveState: Ref<string | null> = ref(null) const lastSaveState: Ref<string | null> = ref(null)
const lastSaveStateTimestamp: Ref<number> = ref(0) const lastSaveStateTimestamp: Ref<number> = ref(0)
@ -29,7 +31,9 @@ const toDate: Ref<Date> = ref(new Date())
const fromDate: Ref<Date> = ref(new Date()) const fromDate: Ref<Date> = ref(new Date())
const entriesChangedSinceLastSave = computed(() => { const entriesChangedSinceLastSave = computed(() => {
return lastSaveState.value === null || lastSaveState.value !== JSON.stringify(students.value) const studentsClone: EntriesResponseStudent[] = JSON.parse(JSON.stringify(students.value))
studentsClone.sort(sortEntriesByName)
return lastSaveState.value === null || lastSaveState.value !== JSON.stringify(studentsClone)
}) })
const assignedDesks = computed(() => { const assignedDesks = computed(() => {
return students.value.length > 0 && students.value.some(s => s.deskNumber > 0) return students.value.length > 0 && students.value.some(s => s.deskNumber > 0)
@ -37,6 +41,13 @@ const assignedDesks = computed(() => {
onMounted(async () => { onMounted(async () => {
showThisWeek() showThisWeek()
watch(sortingChoice, () => {
if (sortingChoice.value === 'name') {
students.value.sort(sortEntriesByName)
} else if (sortingChoice.value === 'desk') {
students.value.sort(sortEntriesByDeskNumber)
}
})
}) })
async function loadEntries() { async function loadEntries() {
@ -44,9 +55,14 @@ async function loadEntries() {
.getEntries(props.classId, fromDate.value, toDate.value) .getEntries(props.classId, fromDate.value, toDate.value)
.handleErrorsWithAlert() .handleErrorsWithAlert()
if (entries) { if (entries) {
students.value = entries.students
lastSaveState.value = JSON.stringify(entries.students) lastSaveState.value = JSON.stringify(entries.students)
lastSaveStateTimestamp.value = Date.now() 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 dates.value = entries.dates
} else { } else {
students.value = [] students.value = []
@ -56,6 +72,18 @@ async function loadEntries() {
} }
} }
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 shiftDateRange(days: number) { function shiftDateRange(days: number) {
toDate.value.setDate(toDate.value.getDate() + days) toDate.value.setDate(toDate.value.getDate() + days)
fromDate.value.setDate(fromDate.value.getDate() + days) fromDate.value.setDate(fromDate.value.getDate() + days)
@ -154,14 +182,22 @@ function addAllEntriesForDate(dateStr: string) {
<button type="button" @click="discardEdits" :disabled="!entriesChangedSinceLastSave"> <button type="button" @click="discardEdits" :disabled="!entriesChangedSinceLastSave">
Discard Edits Discard Edits
</button> </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>
<input type="checkbox" id="grading-view-checkbox" v-model="gradingView" />
<label for="grading-view-checkbox" style="display: inline">Grading View</label>
</div> </div>
<table class="entries-table"> <table class="entries-table" :class="{ 'entries-table-grading-view': gradingView }">
<thead> <thead>
<tr> <tr>
<th>Student</th> <th>Student</th>
<th v-if="assignedDesks">Desk</th> <th v-if="assignedDesks">Desk</th>
<DateHeaderCell v-for="date in dates" :key="date" :date-str="date" <DateHeaderCell v-for="date in gradingView ? [] : dates" :key="date" :date-str="date"
@add-all-entries-clicked="addAllEntriesForDate(date)" /> @add-all-entries-clicked="addAllEntriesForDate(date)" />
<th>Score</th> <th>Score</th>
</tr> </tr>
@ -174,8 +210,8 @@ function addAllEntriesForDate(dateStr: string) {
</RouterLink> </RouterLink>
</td> </td>
<td v-if="assignedDesks" v-text="student.deskNumber"></td> <td v-if="assignedDesks" v-text="student.deskNumber"></td>
<EntryTableCell v-for="(entry, date) in student.entries" :key="date" v-model="student.entries[date]" <EntryTableCell v-for="(entry, date) in gradingView ? [] : student.entries" :key="date"
:date-str="date" :last-save-state-timestamp="lastSaveStateTimestamp" /> v-model="student.entries[date]" :date-str="date" :last-save-state-timestamp="lastSaveStateTimestamp" />
<StudentScoreCell :score="student.score" /> <StudentScoreCell :score="student.score" />
</tr> </tr>
</tbody> </tbody>
@ -189,6 +225,11 @@ function addAllEntriesForDate(dateStr: string) {
width: 100%; width: 100%;
} }
.entries-table-grading-view {
width: auto;
max-width: 100%;
}
.entries-table, .entries-table,
.entries-table th, .entries-table th,
.entries-table td { .entries-table td {

View File

@ -1,11 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { getDefaultEntry, type Entry } from '@/api/classroom_compliance' import { EMOJI_ABSENT, EMOJI_BEHAVIOR_GOOD, EMOJI_BEHAVIOR_MEDIOCRE, EMOJI_BEHAVIOR_POOR, EMOJI_PHONE_COMPLIANT, EMOJI_PHONE_NONCOMPLIANT, EMOJI_PRESENT, getDefaultEntry, type Entry } from '@/api/classroom_compliance'
import { computed, onMounted, ref, watch, type Ref } from 'vue' import { computed, onMounted, ref, useTemplateRef, watch, type Ref } from 'vue'
const props = defineProps<{ const props = defineProps<{
dateStr: string dateStr: string
lastSaveStateTimestamp: number lastSaveStateTimestamp: number
}>() }>()
defineEmits<{
(e: 'editComment'): void
}>()
const model = defineModel<Entry | null>({ const model = defineModel<Entry | null>({
required: false, required: false,
@ -14,6 +17,10 @@ const initialEntryJson: Ref<string> = ref('')
const previouslyRemovedEntry: Ref<Entry | null> = ref(null) const previouslyRemovedEntry: Ref<Entry | null> = ref(null)
const entryChanged = computed(() => JSON.stringify(model.value) !== initialEntryJson.value) const entryChanged = computed(() => JSON.stringify(model.value) !== initialEntryJson.value)
const hasComment = computed(() => model.value && model.value.comment.trim().length > 0)
const previousCommentValue: Ref<string> = ref('')
const commentEditorDialog = useTemplateRef('commentEditorDialog')
onMounted(() => { onMounted(() => {
initialEntryJson.value = JSON.stringify(model.value) initialEntryJson.value = JSON.stringify(model.value)
@ -67,6 +74,19 @@ function toggleBehaviorRating() {
} }
} }
function showCommentEditor() {
if (!model.value) return
previousCommentValue.value = model.value?.comment
commentEditorDialog.value?.showModal()
}
function cancelCommentEdit() {
if (model.value) {
model.value.comment = previousCommentValue.value
}
commentEditorDialog.value?.close()
}
function removeEntry() { function removeEntry() {
if (model.value) { if (model.value) {
previouslyRemovedEntry.value = JSON.parse(JSON.stringify(model.value)) previouslyRemovedEntry.value = JSON.parse(JSON.stringify(model.value))
@ -87,17 +107,22 @@ function addEntry() {
<div v-if="model" class="cell-container"> <div v-if="model" class="cell-container">
<div> <div>
<div class="status-item" @click="toggleAbsence"> <div class="status-item" @click="toggleAbsence">
<span v-if="model.absent" title="Absent"></span> <span v-if="model.absent" title="Absent">{{ EMOJI_ABSENT }}</span>
<span v-if="!model.absent" title="Present"></span> <span v-if="!model.absent" title="Present">{{ EMOJI_PRESENT }}</span>
</div> </div>
<div class="status-item" @click="togglePhoneCompliance" v-if="!model.absent"> <div class="status-item" @click="togglePhoneCompliance" v-if="!model.absent">
<span v-if="model.phone?.compliant" title="Phone Compliant">📱</span> <span v-if="model.phone?.compliant" title="Phone Compliant">{{ EMOJI_PHONE_COMPLIANT }}</span>
<span v-if="!model.phone?.compliant" title="Phone Non-Compliant">📵</span> <span v-if="!model.phone?.compliant" title="Phone Non-Compliant">{{ EMOJI_PHONE_NONCOMPLIANT }}</span>
</div> </div>
<div class="status-item" @click="toggleBehaviorRating" v-if="!model.absent"> <div class="status-item" @click="toggleBehaviorRating" v-if="!model.absent">
<span v-if="model.behavior?.rating === 3" title="Good Behavior">😇</span> <span v-if="model.behavior?.rating === 3" title="Good Behavior">{{ EMOJI_BEHAVIOR_GOOD }}</span>
<span v-if="model.behavior?.rating === 2" title="Mediocre Behavior">😐</span> <span v-if="model.behavior?.rating === 2" title="Mediocre Behavior">{{ EMOJI_BEHAVIOR_MEDIOCRE }}</span>
<span v-if="model.behavior?.rating === 1" title="Poor Behavior">😡</span> <span v-if="model.behavior?.rating === 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>
<div> <div>
@ -112,6 +137,17 @@ function addEntry() {
<span>+</span> <span>+</span>
</div> </div>
</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()"></textarea>
<div class="button-bar" style="text-align: right;">
<button type="button" @click="commentEditorDialog?.close()">Confirm</button>
<button type="button" @click="model.comment = ''; commentEditorDialog?.close()">Clear Comment</button>
<button type="button" @click="cancelCommentEdit">Cancel</button>
</div>
</dialog>
</td> </td>
</template> </template>
<style scoped> <style scoped>

View File

@ -0,0 +1,52 @@
<script setup lang="ts">
import { EMOJI_ABSENT, EMOJI_BEHAVIOR_GOOD, EMOJI_BEHAVIOR_MEDIOCRE, EMOJI_BEHAVIOR_POOR, EMOJI_PHONE_COMPLIANT, EMOJI_PHONE_NONCOMPLIANT, EMOJI_PRESENT, type Entry } from '@/api/classroom_compliance';
defineProps<{
entry: Entry
}>()
</script>
<template>
<div class="student-entry-item">
<h6>{{ entry.date }}</h6>
<div class="icons-container">
<span v-if="entry.absent">{{ EMOJI_ABSENT }}</span>
<span v-if="!entry.absent">{{ EMOJI_PRESENT }}</span>
<span v-if="entry.phone && entry.phone.compliant">{{ EMOJI_PHONE_COMPLIANT }}</span>
<span v-if="entry.phone && !entry.phone.compliant">{{ EMOJI_PHONE_NONCOMPLIANT }}</span>
<span v-if="entry.behavior && entry.behavior.rating === 3">{{ EMOJI_BEHAVIOR_GOOD }}</span>
<span v-if="entry.behavior && entry.behavior.rating === 2">{{ EMOJI_BEHAVIOR_MEDIOCRE }}</span>
<span v-if="entry.behavior && entry.behavior.rating === 1">{{ EMOJI_BEHAVIOR_POOR }}</span>
</div>
<p v-if="entry.comment.trim().length > 0" class="comment">
{{ entry.comment }}
</p>
</div>
</template>
<style scoped>
.student-entry-item {
border: 1px solid;
border-radius: 10px;
padding: 0.5em;
max-width: 500px;
}
.student-entry-item+.student-entry-item {
margin-top: 1em;
}
.student-entry-item>h6 {
margin-top: 0;
margin-bottom: 0.25em;
}
.icons-container>span+span {
margin-left: 0.5em;
}
.comment {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
</style>

View File

@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ClassroomComplianceAPIClient, type Class, type Student } from '@/api/classroom_compliance' import { ClassroomComplianceAPIClient, type Class, type Entry, type Student } from '@/api/classroom_compliance'
import ConfirmDialog from '@/components/ConfirmDialog.vue' import ConfirmDialog from '@/components/ConfirmDialog.vue'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { onMounted, ref, useTemplateRef, type Ref } from 'vue' import { onMounted, ref, useTemplateRef, type Ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import StudentEntryItem from '@/apps/classroom_compliance/StudentEntryItem.vue'
const props = defineProps<{ const props = defineProps<{
classId: string classId: string
@ -11,8 +12,11 @@ const props = defineProps<{
}>() }>()
const authStore = useAuthStore() const authStore = useAuthStore()
const router = useRouter() const router = useRouter()
const cls: Ref<Class | null> = ref(null) const cls: Ref<Class | null> = ref(null)
const student: Ref<Student | null> = ref(null) const student: Ref<Student | null> = ref(null)
const entries: Ref<Entry[]> = ref([])
const apiClient = new ClassroomComplianceAPIClient(authStore) const apiClient = new ClassroomComplianceAPIClient(authStore)
const deleteConfirmDialog = useTemplateRef('deleteConfirmDialog') const deleteConfirmDialog = useTemplateRef('deleteConfirmDialog')
onMounted(async () => { onMounted(async () => {
@ -28,6 +32,13 @@ onMounted(async () => {
await router.replace(`/classroom-compliance/classes/${cls.value.id}`) await router.replace(`/classroom-compliance/classes/${cls.value.id}`)
return return
} }
apiClient.getStudentEntries(cls.value.id, student.value.id).handleErrorsWithAlert()
.then(values => {
if (values !== null) {
entries.value = values
}
})
}) })
async function deleteThisStudent() { async function deleteThisStudent() {
@ -59,6 +70,14 @@ async function deleteThisStudent() {
<button type="button" @click="deleteThisStudent">Delete</button> <button type="button" @click="deleteThisStudent">Delete</button>
</div> </div>
<h3>Entries</h3>
<p>
Below is a record of all entries saved for <span>{{ student.name }}</span>.
</p>
<div>
<StudentEntryItem v-for="entry in entries" :key="entry.id" :entry="entry" />
</div>
<ConfirmDialog ref="deleteConfirmDialog"> <ConfirmDialog ref="deleteConfirmDialog">
<p> <p>
Are you sure you want to delete <span v-text="student.name"></span>? This will permanently Are you sure you want to delete <span v-text="student.name"></span>? This will permanently

View File

@ -46,9 +46,10 @@ defineExpose({
<style> <style>
.confirm-dialog-buttons { .confirm-dialog-buttons {
text-align: right; text-align: right;
margin-top: 0.5em;
} }
.confirm-dialog-buttons > button { .confirm-dialog-buttons>button {
margin-left: 0.5em; margin-left: 0.5em;
} }
</style> </style>