parent
7c92c600e6
commit
65e61a9fbf
|
@ -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 (
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
<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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue