parent
7c92c600e6
commit
65e61a9fbf
|
@ -23,7 +23,8 @@ CREATE TABLE classroom_compliance_entry (
|
|||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
date TEXT 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 (
|
||||
|
|
|
@ -36,6 +36,7 @@ struct ClassroomComplianceEntry {
|
|||
const string date;
|
||||
const ulong createdAt;
|
||||
const bool absent;
|
||||
const string comment;
|
||||
}
|
||||
|
||||
struct ClassroomComplianceEntryPhone {
|
||||
|
@ -63,6 +64,7 @@ void registerApiEndpoints(PathHandler handler) {
|
|||
handler.addMapping(Method.GET, STUDENT_PATH, &getStudent);
|
||||
handler.addMapping(Method.PUT, STUDENT_PATH, &updateStudent);
|
||||
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.POST, CLASS_PATH ~ "/entries", &saveEntries);
|
||||
|
@ -344,6 +346,7 @@ void getEntries(ref HttpRequestContext ctx) {
|
|||
entry.date,
|
||||
entry.created_at,
|
||||
entry.absent,
|
||||
entry.comment,
|
||||
student.id,
|
||||
student.name,
|
||||
student.desk_number,
|
||||
|
@ -373,21 +376,22 @@ void getEntries(ref HttpRequestContext ctx) {
|
|||
entry.object["date"] = JSONValue(row.peek!string(1));
|
||||
entry.object["createdAt"] = JSONValue(row.peek!ulong(2));
|
||||
entry.object["absent"] = JSONValue(row.peek!bool(3));
|
||||
entry.object["comment"] = JSONValue(row.peek!string(4));
|
||||
|
||||
JSONValue phone = JSONValue(null);
|
||||
JSONValue behavior = JSONValue(null);
|
||||
if (!entry.object["absent"].boolean()) {
|
||||
phone = JSONValue.emptyObject;
|
||||
phone.object["compliant"] = JSONValue(row.peek!bool(8));
|
||||
phone.object["compliant"] = JSONValue(row.peek!bool(9));
|
||||
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["behavior"] = behavior;
|
||||
string dateStr = entry.object["date"].str();
|
||||
|
||||
// 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;
|
||||
foreach (idx, student; students) {
|
||||
if (student.id == studentId) {
|
||||
|
@ -525,11 +529,13 @@ private void insertNewEntry(
|
|||
) {
|
||||
ulong createdAt = getUnixTimestampMillis();
|
||||
bool absent = payload.object["absent"].boolean;
|
||||
string comment = payload.object["comment"].str;
|
||||
if (comment is null) comment = "";
|
||||
db.execute(
|
||||
"INSERT INTO classroom_compliance_entry
|
||||
(class_id, student_id, date, created_at, absent)
|
||||
VALUES (?, ?, ?, ?, ?)",
|
||||
classId, studentId, dateStr, createdAt, absent
|
||||
(class_id, student_id, date, created_at, absent, comment)
|
||||
VALUES (?, ?, ?, ?, ?, ?)",
|
||||
classId, studentId, dateStr, createdAt, absent, comment
|
||||
);
|
||||
if (!absent) {
|
||||
ulong entryId = db.lastInsertRowid();
|
||||
|
@ -564,11 +570,14 @@ private void updateEntry(
|
|||
JSONValue obj
|
||||
) {
|
||||
bool absent = obj.object["absent"].boolean;
|
||||
string comment = obj.object["comment"].str;
|
||||
if (comment is null) comment = "";
|
||||
db.execute(
|
||||
"UPDATE classroom_compliance_entry
|
||||
SET absent = ?
|
||||
SET absent = ?, comment = ?
|
||||
WHERE class_id = ? AND student_id = ? AND date = ? AND id = ?",
|
||||
absent, classId, studentId, dateStr, entryId
|
||||
absent, comment,
|
||||
classId, studentId, dateStr, entryId
|
||||
);
|
||||
if (absent) {
|
||||
db.execute(
|
||||
|
@ -726,3 +735,51 @@ private Optional!double calculateScore(
|
|||
double score = 0.3 * phoneScore + 0.7 * behaviorScore;
|
||||
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");
|
||||
}
|
||||
|
|
|
@ -63,16 +63,13 @@ void insertSampleData(ref Database db) {
|
|||
bool absent = uniform01(rand) < 0.05;
|
||||
bool phoneCompliant = uniform01(rand) < 0.85;
|
||||
ubyte behaviorRating = 3;
|
||||
string behaviorComment = null;
|
||||
if (uniform01(rand) < 0.25) {
|
||||
behaviorRating = 2;
|
||||
behaviorComment = "They did not participate enough.";
|
||||
if (uniform01(rand) < 0.5) {
|
||||
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,
|
||||
bool absent,
|
||||
bool phoneCompliant,
|
||||
ubyte behaviorRating,
|
||||
string behaviorComment
|
||||
ubyte behaviorRating
|
||||
) {
|
||||
const entryQuery = "
|
||||
INSERT INTO classroom_compliance_entry
|
||||
(class_id, student_id, date, created_at, absent)
|
||||
VALUES (?, ?, ?, ?, ?)";
|
||||
db.execute(entryQuery, classId, studentId, date.toISOExtString(), getUnixTimestampMillis(), absent);
|
||||
(class_id, student_id, date, created_at, absent, comment)
|
||||
VALUES (?, ?, ?, ?, ?, ?)";
|
||||
db.execute(
|
||||
entryQuery,
|
||||
classId,
|
||||
studentId,
|
||||
date.toISOExtString(),
|
||||
getUnixTimestampMillis(),
|
||||
absent,
|
||||
"Sample comment."
|
||||
);
|
||||
if (absent) return;
|
||||
ulong entryId = db.lastInsertRowid();
|
||||
const phoneQuery = "INSERT INTO classroom_compliance_entry_phone (entry_id, compliant) VALUES (?, ?)";
|
||||
db.execute(phoneQuery, entryId, phoneCompliant);
|
||||
const behaviorQuery = "
|
||||
INSERT INTO classroom_compliance_entry_behavior
|
||||
(entry_id, rating, comment)
|
||||
VALUES (?, ?, ?)";
|
||||
db.execute(behaviorQuery, entryId, behaviorRating, behaviorComment);
|
||||
(entry_id, rating)
|
||||
VALUES (?, ?)";
|
||||
db.execute(behaviorQuery, entryId, behaviorRating);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,14 @@ import { APIClient, type APIResponse, type AuthStoreType } from './base'
|
|||
|
||||
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 {
|
||||
id: number
|
||||
number: number
|
||||
|
@ -31,6 +39,7 @@ export interface Entry {
|
|||
absent: boolean
|
||||
phone: EntryPhone | null
|
||||
behavior: EntryBehavior | null
|
||||
comment: string
|
||||
}
|
||||
|
||||
export function getDefaultEntry(dateStr: string): Entry {
|
||||
|
@ -41,6 +50,7 @@ export function getDefaultEntry(dateStr: string): Entry {
|
|||
absent: false,
|
||||
phone: { compliant: true },
|
||||
behavior: { rating: 3 },
|
||||
comment: '',
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,4 +158,8 @@ export class ClassroomComplianceAPIClient extends APIClient {
|
|||
params.append('to', toDate.toISOString().substring(0, 10))
|
||||
return super.get(`/classes/${classId}/scores?${params.toString()}`)
|
||||
}
|
||||
|
||||
getStudentEntries(classId: number, studentId: number): APIResponse<Entry[]> {
|
||||
return super.get(`/classes/${classId}/students/${studentId}/entries`)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,8 +32,7 @@ async function deleteThisClass() {
|
|||
</script>
|
||||
<template>
|
||||
<div v-if="cls">
|
||||
<h1>Class #<span v-text="cls.number"></span></h1>
|
||||
<p>ID: <span v-text="cls.id"></span></p>
|
||||
<h1>Class <span v-text="cls.number"></span></h1>
|
||||
<p>School Year: <span v-text="cls.schoolYear"></span></p>
|
||||
<div class="button-bar" style="margin-bottom: 1em;">
|
||||
<button type="button" @click="router.push(`/classroom-compliance/classes/${cls.id}/edit-student`)">Add
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
type EntriesResponseStudent,
|
||||
} from '@/api/classroom_compliance'
|
||||
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 { RouterLink } from 'vue-router'
|
||||
import StudentScoreCell from '@/apps/classroom_compliance/StudentScoreCell.vue'
|
||||
|
@ -20,6 +20,8 @@ const props = defineProps<{
|
|||
const apiClient = new ClassroomComplianceAPIClient(authStore)
|
||||
|
||||
const students: Ref<EntriesResponseStudent[]> = ref([])
|
||||
const sortingChoice: Ref<string> = ref('name')
|
||||
const gradingView = ref(false)
|
||||
|
||||
const lastSaveState: Ref<string | null> = ref(null)
|
||||
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 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(() => {
|
||||
return students.value.length > 0 && students.value.some(s => s.deskNumber > 0)
|
||||
|
@ -37,6 +41,13 @@ const assignedDesks = computed(() => {
|
|||
|
||||
onMounted(async () => {
|
||||
showThisWeek()
|
||||
watch(sortingChoice, () => {
|
||||
if (sortingChoice.value === 'name') {
|
||||
students.value.sort(sortEntriesByName)
|
||||
} else if (sortingChoice.value === 'desk') {
|
||||
students.value.sort(sortEntriesByDeskNumber)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
async function loadEntries() {
|
||||
|
@ -44,9 +55,14 @@ async function loadEntries() {
|
|||
.getEntries(props.classId, fromDate.value, toDate.value)
|
||||
.handleErrorsWithAlert()
|
||||
if (entries) {
|
||||
students.value = entries.students
|
||||
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 = []
|
||||
|
@ -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) {
|
||||
toDate.value.setDate(toDate.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">
|
||||
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>
|
||||
|
||||
<input type="checkbox" id="grading-view-checkbox" v-model="gradingView" />
|
||||
<label for="grading-view-checkbox" style="display: inline">Grading View</label>
|
||||
</div>
|
||||
|
||||
<table class="entries-table">
|
||||
<table class="entries-table" :class="{ 'entries-table-grading-view': gradingView }">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Student</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)" />
|
||||
<th>Score</th>
|
||||
</tr>
|
||||
|
@ -174,8 +210,8 @@ function addAllEntriesForDate(dateStr: string) {
|
|||
</RouterLink>
|
||||
</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]"
|
||||
:date-str="date" :last-save-state-timestamp="lastSaveStateTimestamp" />
|
||||
<EntryTableCell v-for="(entry, date) in gradingView ? [] : student.entries" :key="date"
|
||||
v-model="student.entries[date]" :date-str="date" :last-save-state-timestamp="lastSaveStateTimestamp" />
|
||||
<StudentScoreCell :score="student.score" />
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -189,6 +225,11 @@ function addAllEntriesForDate(dateStr: string) {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.entries-table-grading-view {
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.entries-table,
|
||||
.entries-table th,
|
||||
.entries-table td {
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import { getDefaultEntry, type Entry } from '@/api/classroom_compliance'
|
||||
import { computed, onMounted, ref, watch, type Ref } from 'vue'
|
||||
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, useTemplateRef, watch, type Ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
dateStr: string
|
||||
lastSaveStateTimestamp: number
|
||||
}>()
|
||||
defineEmits<{
|
||||
(e: 'editComment'): void
|
||||
}>()
|
||||
|
||||
const model = defineModel<Entry | null>({
|
||||
required: false,
|
||||
|
@ -14,6 +17,10 @@ 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)
|
||||
|
||||
const previousCommentValue: Ref<string> = ref('')
|
||||
const commentEditorDialog = useTemplateRef('commentEditorDialog')
|
||||
|
||||
onMounted(() => {
|
||||
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() {
|
||||
if (model.value) {
|
||||
previouslyRemovedEntry.value = JSON.parse(JSON.stringify(model.value))
|
||||
|
@ -87,17 +107,22 @@ function addEntry() {
|
|||
<div v-if="model" class="cell-container">
|
||||
<div>
|
||||
<div class="status-item" @click="toggleAbsence">
|
||||
<span v-if="model.absent" title="Absent">❌</span>
|
||||
<span v-if="!model.absent" title="Present">✅</span>
|
||||
<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" @click="togglePhoneCompliance" v-if="!model.absent">
|
||||
<span v-if="model.phone?.compliant" title="Phone Compliant">📱</span>
|
||||
<span v-if="!model.phone?.compliant" title="Phone Non-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">{{ EMOJI_PHONE_NONCOMPLIANT }}</span>
|
||||
</div>
|
||||
<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 === 2" title="Mediocre Behavior">😐</span>
|
||||
<span v-if="model.behavior?.rating === 1" title="Poor 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">{{ EMOJI_BEHAVIOR_MEDIOCRE }}</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>
|
||||
|
@ -112,6 +137,17 @@ function 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()"></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>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
|
|
@ -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>
|
|
@ -1,9 +1,10 @@
|
|||
<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 { useAuthStore } from '@/stores/auth'
|
||||
import { onMounted, ref, useTemplateRef, type Ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import StudentEntryItem from '@/apps/classroom_compliance/StudentEntryItem.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
classId: string
|
||||
|
@ -11,8 +12,11 @@ const props = defineProps<{
|
|||
}>()
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
||||
const cls: Ref<Class | null> = ref(null)
|
||||
const student: Ref<Student | null> = ref(null)
|
||||
const entries: Ref<Entry[]> = ref([])
|
||||
|
||||
const apiClient = new ClassroomComplianceAPIClient(authStore)
|
||||
const deleteConfirmDialog = useTemplateRef('deleteConfirmDialog')
|
||||
onMounted(async () => {
|
||||
|
@ -28,6 +32,13 @@ onMounted(async () => {
|
|||
await router.replace(`/classroom-compliance/classes/${cls.value.id}`)
|
||||
return
|
||||
}
|
||||
|
||||
apiClient.getStudentEntries(cls.value.id, student.value.id).handleErrorsWithAlert()
|
||||
.then(values => {
|
||||
if (values !== null) {
|
||||
entries.value = values
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
async function deleteThisStudent() {
|
||||
|
@ -59,6 +70,14 @@ async function deleteThisStudent() {
|
|||
<button type="button" @click="deleteThisStudent">Delete</button>
|
||||
</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">
|
||||
<p>
|
||||
Are you sure you want to delete <span v-text="student.name"></span>? This will permanently
|
||||
|
|
|
@ -46,6 +46,7 @@ defineExpose({
|
|||
<style>
|
||||
.confirm-dialog-buttons {
|
||||
text-align: right;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.confirm-dialog-buttons>button {
|
||||
|
|
Loading…
Reference in New Issue