Added entries table, and entries endpoint.
This commit is contained in:
parent
e7683a5c9d
commit
7166b995f7
|
@ -9,24 +9,16 @@ CREATE TABLE classroom_compliance_class (
|
||||||
CREATE TABLE classroom_compliance_student (
|
CREATE TABLE classroom_compliance_student (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
class_id INTEGER NOT NULL REFERENCES classroom_compliance_class(id)
|
|
||||||
ON UPDATE CASCADE ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE classroom_compliance_desk_assignment (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
class_id INTEGER NOT NULL REFERENCES classroom_compliance_class(id)
|
class_id INTEGER NOT NULL REFERENCES classroom_compliance_class(id)
|
||||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
desk_number INTEGER NOT NULL,
|
desk_number INTEGER NOT NULL DEFAULT 0,
|
||||||
student_id INTEGER REFERENCES classroom_compliance_student(id)
|
removed INTEGER NOT NULL DEFAULT 0
|
||||||
ON UPDATE CASCADE ON DELETE SET NULL
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE classroom_compliance_entry (
|
CREATE TABLE classroom_compliance_entry (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
class_id INTEGER NOT NULL REFERENCES classroom_compliance_class(id)
|
class_id INTEGER NOT NULL REFERENCES classroom_compliance_class(id)
|
||||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
class_description TEXT NOT NULL,
|
|
||||||
student_id INTEGER NOT NULL REFERENCES classroom_compliance_student(id),
|
student_id INTEGER NOT NULL REFERENCES classroom_compliance_student(id),
|
||||||
date TEXT NOT NULL,
|
date TEXT NOT NULL,
|
||||||
created_at INTEGER NOT NULL,
|
created_at INTEGER NOT NULL,
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
-- TEST DATA
|
|
||||||
|
|
||||||
-- username: test, password: test
|
|
||||||
INSERT INTO user (username, password_hash, created_at, is_locked, is_admin) VALUES (
|
|
||||||
'test',
|
|
||||||
'9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08',
|
|
||||||
1734380300,
|
|
||||||
0,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
INSERT INTO user (username, password_hash, created_at, is_locked, is_admin) VALUES (
|
|
||||||
'test2',
|
|
||||||
'9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08',
|
|
||||||
1734394062,
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
);
|
|
|
@ -2,8 +2,10 @@ module api_modules.classroom_compliance;
|
||||||
|
|
||||||
import handy_httpd;
|
import handy_httpd;
|
||||||
import handy_httpd.handlers.path_handler;
|
import handy_httpd.handlers.path_handler;
|
||||||
import std.typecons : Nullable;
|
|
||||||
import d2sqlite3;
|
import d2sqlite3;
|
||||||
|
import slf4d;
|
||||||
|
import std.typecons : Nullable;
|
||||||
|
import std.datetime;
|
||||||
|
|
||||||
import db;
|
import db;
|
||||||
import data_utils;
|
import data_utils;
|
||||||
|
@ -20,19 +22,13 @@ struct ClassroomComplianceStudent {
|
||||||
const ulong id;
|
const ulong id;
|
||||||
const string name;
|
const string name;
|
||||||
const ulong classId;
|
const ulong classId;
|
||||||
}
|
|
||||||
|
|
||||||
struct ClassroomComplianceDeskAssignment {
|
|
||||||
const ulong id;
|
|
||||||
const ulong classId;
|
|
||||||
const ushort deskNumber;
|
const ushort deskNumber;
|
||||||
const ulong studentId;
|
const bool removed;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ClassroomComplianceEntry {
|
struct ClassroomComplianceEntry {
|
||||||
const ulong id;
|
const ulong id;
|
||||||
const ulong classId;
|
const ulong classId;
|
||||||
const string classDescription;
|
|
||||||
const ulong studentId;
|
const ulong studentId;
|
||||||
const string date;
|
const string date;
|
||||||
const ulong createdAt;
|
const ulong createdAt;
|
||||||
|
@ -56,6 +52,7 @@ void registerApiEndpoints(PathHandler handler) {
|
||||||
handler.addMapping(Method.POST, ROOT_PATH ~ "/classes", &createClass);
|
handler.addMapping(Method.POST, ROOT_PATH ~ "/classes", &createClass);
|
||||||
handler.addMapping(Method.GET, ROOT_PATH ~ "/classes", &getClasses);
|
handler.addMapping(Method.GET, ROOT_PATH ~ "/classes", &getClasses);
|
||||||
const CLASS_PATH = ROOT_PATH ~ "/classes/:classId:ulong";
|
const CLASS_PATH = ROOT_PATH ~ "/classes/:classId:ulong";
|
||||||
|
handler.addMapping(Method.GET, CLASS_PATH, &getClass);
|
||||||
handler.addMapping(Method.DELETE, CLASS_PATH, &deleteClass);
|
handler.addMapping(Method.DELETE, CLASS_PATH, &deleteClass);
|
||||||
|
|
||||||
handler.addMapping(Method.POST, CLASS_PATH ~ "/students", &createStudent);
|
handler.addMapping(Method.POST, CLASS_PATH ~ "/students", &createStudent);
|
||||||
|
@ -64,11 +61,8 @@ void registerApiEndpoints(PathHandler handler) {
|
||||||
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.POST, CLASS_PATH ~ "/desk-assignments", &setDeskAssignments);
|
|
||||||
handler.addMapping(Method.GET, CLASS_PATH ~ "/desk-assignments", &getDeskAssignments);
|
|
||||||
handler.addMapping(Method.DELETE, CLASS_PATH ~ "/desk-assignments", &removeAllDeskAssignments);
|
|
||||||
|
|
||||||
handler.addMapping(Method.POST, CLASS_PATH ~ "/entries", &createEntry);
|
handler.addMapping(Method.POST, CLASS_PATH ~ "/entries", &createEntry);
|
||||||
|
handler.addMapping(Method.GET, CLASS_PATH ~ "/entries", &getEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
void createClass(ref HttpRequestContext ctx) {
|
void createClass(ref HttpRequestContext ctx) {
|
||||||
|
@ -115,6 +109,12 @@ void getClasses(ref HttpRequestContext ctx) {
|
||||||
writeJsonBody(ctx, classes);
|
writeJsonBody(ctx, classes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void getClass(ref HttpRequestContext ctx) {
|
||||||
|
User user = getUserOrThrow(ctx);
|
||||||
|
auto cls = getClassOrThrow(ctx, user);
|
||||||
|
writeJsonBody(ctx, cls);
|
||||||
|
}
|
||||||
|
|
||||||
void deleteClass(ref HttpRequestContext ctx) {
|
void deleteClass(ref HttpRequestContext ctx) {
|
||||||
User user = getUserOrThrow(ctx);
|
User user = getUserOrThrow(ctx);
|
||||||
auto cls = getClassOrThrow(ctx, user);
|
auto cls = getClassOrThrow(ctx, user);
|
||||||
|
@ -138,6 +138,7 @@ void createStudent(ref HttpRequestContext ctx) {
|
||||||
auto cls = getClassOrThrow(ctx, user);
|
auto cls = getClassOrThrow(ctx, user);
|
||||||
struct StudentPayload {
|
struct StudentPayload {
|
||||||
string name;
|
string name;
|
||||||
|
ushort deskNumber;
|
||||||
}
|
}
|
||||||
auto payload = readJsonPayload!(StudentPayload)(ctx);
|
auto payload = readJsonPayload!(StudentPayload)(ctx);
|
||||||
auto db = getDb();
|
auto db = getDb();
|
||||||
|
@ -152,7 +153,20 @@ void createStudent(ref HttpRequestContext ctx) {
|
||||||
ctx.response.writeBodyString("Student with that name already exists.");
|
ctx.response.writeBodyString("Student with that name already exists.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
db.execute("INSERT INTO classroom_compliance_student (name, class_id) VALUES (?, ?)", payload.name, cls.id);
|
bool deskAlreadyOccupied = payload.deskNumber != 0 && canFind(
|
||||||
|
db,
|
||||||
|
"SELECT id FROM classroom_compliance_student WHERE class_id = ? AND desk_number = ?",
|
||||||
|
cls.id,
|
||||||
|
payload.deskNumber
|
||||||
|
);
|
||||||
|
if (deskAlreadyOccupied) {
|
||||||
|
ctx.response.status = HttpStatus.BAD_REQUEST;
|
||||||
|
ctx.response.writeBodyString("There is already a student assigned to that desk.");
|
||||||
|
}
|
||||||
|
db.execute(
|
||||||
|
"INSERT INTO classroom_compliance_student (name, class_id, desk_number) VALUES (?, ?)",
|
||||||
|
payload.name, cls.id, payload.deskNumber
|
||||||
|
);
|
||||||
ulong studentId = db.lastInsertRowid();
|
ulong studentId = db.lastInsertRowid();
|
||||||
auto student = findOne!(ClassroomComplianceStudent)(
|
auto student = findOne!(ClassroomComplianceStudent)(
|
||||||
db,
|
db,
|
||||||
|
@ -179,10 +193,14 @@ void updateStudent(ref HttpRequestContext ctx) {
|
||||||
auto student = getStudentOrThrow(ctx, user);
|
auto student = getStudentOrThrow(ctx, user);
|
||||||
struct StudentUpdatePayload {
|
struct StudentUpdatePayload {
|
||||||
string name;
|
string name;
|
||||||
|
ushort deskNumber;
|
||||||
}
|
}
|
||||||
auto payload = readJsonPayload!(StudentUpdatePayload)(ctx);
|
auto payload = readJsonPayload!(StudentUpdatePayload)(ctx);
|
||||||
// If there is nothing to update, quit.
|
// If there is nothing to update, quit.
|
||||||
if (payload.name == student.name) return;
|
if (
|
||||||
|
payload.name == student.name
|
||||||
|
&& payload.deskNumber == student.deskNumber
|
||||||
|
) return;
|
||||||
// Check that the new name doesn't already exist.
|
// Check that the new name doesn't already exist.
|
||||||
auto db = getDb();
|
auto db = getDb();
|
||||||
bool newNameExists = canFind(
|
bool newNameExists = canFind(
|
||||||
|
@ -196,9 +214,22 @@ void updateStudent(ref HttpRequestContext ctx) {
|
||||||
ctx.response.writeBodyString("Student with that name already exists.");
|
ctx.response.writeBodyString("Student with that name already exists.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Check that if a new desk number is assigned, that it's not already assigned to anyone else.
|
||||||
|
bool newDeskOccupied = payload.deskNumber != 0 && canFind(
|
||||||
|
db,
|
||||||
|
"SELECT id FROM classroom_compliance_student WHERE class_id = ? AND desk_number = ?",
|
||||||
|
student.classId,
|
||||||
|
payload.deskNumber
|
||||||
|
);
|
||||||
|
if (newDeskOccupied) {
|
||||||
|
ctx.response.status = HttpStatus.BAD_REQUEST;
|
||||||
|
ctx.response.writeBodyString("That desk is already assigned to another student.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
db.execute(
|
db.execute(
|
||||||
"UPDATE classroom_compliance_student SET name = ? WHERE id = ?",
|
"UPDATE classroom_compliance_student SET name = ?, desk_number = ? WHERE id = ?",
|
||||||
payload.name,
|
payload.name,
|
||||||
|
payload.deskNumber,
|
||||||
student.id
|
student.id
|
||||||
);
|
);
|
||||||
auto updatedStudent = findOne!(ClassroomComplianceStudent)(
|
auto updatedStudent = findOne!(ClassroomComplianceStudent)(
|
||||||
|
@ -239,135 +270,187 @@ ClassroomComplianceStudent getStudentOrThrow(ref HttpRequestContext ctx, in User
|
||||||
).orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND));
|
).orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND));
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct DeskAssignmentPayloadEntry {
|
void createEntry(ref HttpRequestContext ctx) {
|
||||||
ushort deskNumber;
|
|
||||||
Nullable!ulong studentId;
|
|
||||||
}
|
|
||||||
private struct DeskAssignmentPayload {
|
|
||||||
DeskAssignmentPayloadEntry[] entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setDeskAssignments(ref HttpRequestContext ctx) {
|
|
||||||
User user = getUserOrThrow(ctx);
|
User user = getUserOrThrow(ctx);
|
||||||
auto cls = getClassOrThrow(ctx, user);
|
auto cls = getClassOrThrow(ctx, user);
|
||||||
auto payload = readJsonPayload!(DeskAssignmentPayload)(ctx);
|
struct EntryPhonePayload {
|
||||||
|
bool compliant;
|
||||||
|
}
|
||||||
|
struct EntryBehaviorPayload {
|
||||||
|
int rating;
|
||||||
|
Nullable!string comment;
|
||||||
|
}
|
||||||
|
struct EntryPayload {
|
||||||
|
ulong studentId;
|
||||||
|
string date;
|
||||||
|
bool absent;
|
||||||
|
Nullable!EntryPhonePayload phoneCompliance;
|
||||||
|
Nullable!EntryBehaviorPayload behaviorCompliance;
|
||||||
|
}
|
||||||
|
auto payload = readJsonPayload!(EntryPayload)(ctx);
|
||||||
auto db = getDb();
|
auto db = getDb();
|
||||||
auto validationError = validateDeskAssignments(db, payload, cls.id);
|
bool entryAlreadyExists = canFind(
|
||||||
if (validationError) {
|
db,
|
||||||
import slf4d;
|
"SELECT id FROM classroom_compliance_entry WHERE class_id = ? AND student_id = ? AND date = ?",
|
||||||
warnF!"Desk assignment validation failed: %s"(validationError.value);
|
cls.id,
|
||||||
|
payload.studentId,
|
||||||
|
payload.date
|
||||||
|
);
|
||||||
|
if (entryAlreadyExists) {
|
||||||
ctx.response.status = HttpStatus.BAD_REQUEST;
|
ctx.response.status = HttpStatus.BAD_REQUEST;
|
||||||
ctx.response.writeBodyString(validationError.value);
|
ctx.response.writeBodyString("An entry already exists for this student and date.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Insert the entry and its attached entities in a transaction.
|
||||||
db.begin();
|
db.begin();
|
||||||
try {
|
try {
|
||||||
db.execute(
|
db.execute(
|
||||||
"DELETE FROM classroom_compliance_desk_assignment WHERE class_id = ?",
|
"INSERT INTO classroom_compliance_entry (class_id, student_id, date, created_at, absent)
|
||||||
cls.id
|
VALUES (?, ?, ?, ?, ?)",
|
||||||
|
cls.id,
|
||||||
|
payload.studentId,
|
||||||
|
payload.date,
|
||||||
|
getUnixTimestampMillis(),
|
||||||
|
payload.absent
|
||||||
);
|
);
|
||||||
auto stmt = db.prepare(
|
ulong entryId = db.lastInsertRowid();
|
||||||
"INSERT INTO classroom_compliance_desk_assignment (class_id, desk_number, student_id) VALUES (?, ?, ?)"
|
if (!payload.absent && !payload.phoneCompliance.isNull) {
|
||||||
|
db.execute(
|
||||||
|
"INSERT INTO classroom_compliance_entry_phone (entry_id, compliant) VALUES (?, ?)",
|
||||||
|
entryId,
|
||||||
|
payload.phoneCompliance.get().compliant
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!payload.absent && !payload.behaviorCompliance.isNull) {
|
||||||
|
Nullable!string comment = payload.behaviorCompliance.get().comment;
|
||||||
|
if (!comment.isNull && (comment.get() is null || comment.get().length == 0)) {
|
||||||
|
comment.nullify();
|
||||||
|
}
|
||||||
|
db.execute(
|
||||||
|
"INSERT INTO classroom_compliance_entry_behavior (entry_id, rating, comment) VALUES (?, ?, ?)",
|
||||||
|
entryId,
|
||||||
|
payload.behaviorCompliance.get().rating,
|
||||||
|
comment
|
||||||
);
|
);
|
||||||
foreach (entry; payload.entries) {
|
|
||||||
stmt.bindAll(cls.id, entry.deskNumber, entry.studentId);
|
|
||||||
stmt.execute();
|
|
||||||
stmt.clearBindings();
|
|
||||||
stmt.reset();
|
|
||||||
}
|
}
|
||||||
db.commit();
|
db.commit();
|
||||||
// Return the new list of desk assignments to the user.
|
|
||||||
auto newAssignments = findAll!(ClassroomComplianceDeskAssignment)(
|
|
||||||
db,
|
|
||||||
"SELECT * FROM classroom_compliance_desk_assignment WHERE class_id = ?",
|
|
||||||
cls.id
|
|
||||||
);
|
|
||||||
writeJsonBody(ctx, newAssignments);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
db.rollback();
|
db.rollback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional!string validateDeskAssignments(Database db, in DeskAssignmentPayload payload, ulong classId) {
|
void getEntries(ref HttpRequestContext ctx) {
|
||||||
import std.algorithm : canFind, map;
|
|
||||||
import std.array;
|
|
||||||
// Check that desks are numbered 1 .. N.
|
|
||||||
for (int n = 1; n <= payload.entries.length; n++) {
|
|
||||||
bool deskPresent = false;
|
|
||||||
foreach (entry; payload.entries) {
|
|
||||||
if (entry.deskNumber == n) {
|
|
||||||
deskPresent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!deskPresent) return Optional!string.of("Desks should be numbered from 1 to N.");
|
|
||||||
}
|
|
||||||
auto allStudents = findAll!(ClassroomComplianceStudent)(
|
|
||||||
db,
|
|
||||||
"SELECT * FROM classroom_compliance_student WHERE class_id = ?",
|
|
||||||
classId
|
|
||||||
);
|
|
||||||
auto studentIds = allStudents.map!(s => s.id).array;
|
|
||||||
// Check that if a desk is assigned to a student, that it's one from this class.
|
|
||||||
foreach (entry; payload.entries) {
|
|
||||||
if (!entry.studentId.isNull && !canFind(studentIds, entry.studentId.get)) {
|
|
||||||
return Optional!string.of("Desks cannot be assigned to students that don't belong to this class.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that each student in the class is assigned a desk.
|
|
||||||
ushort[] takenDesks;
|
|
||||||
foreach (student; allStudents) {
|
|
||||||
ushort[] assignedDesks;
|
|
||||||
foreach (entry; payload.entries) {
|
|
||||||
if (!entry.studentId.isNull && entry.studentId.get() == student.id) {
|
|
||||||
assignedDesks ~= entry.deskNumber;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (assignedDesks.length != 1) {
|
|
||||||
if (assignedDesks.length > 1) {
|
|
||||||
return Optional!string.of("A student is assigned to more than one desk.");
|
|
||||||
} else {
|
|
||||||
return Optional!string.of("Not all students are assigned to a desk.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (canFind(takenDesks, assignedDesks[0])) {
|
|
||||||
return Optional!string.of("Cannot assign more than one student to the same desk.");
|
|
||||||
}
|
|
||||||
takenDesks ~= assignedDesks[0];
|
|
||||||
}
|
|
||||||
return Optional!string.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
void getDeskAssignments(ref HttpRequestContext ctx) {
|
|
||||||
User user = getUserOrThrow(ctx);
|
User user = getUserOrThrow(ctx);
|
||||||
auto cls = getClassOrThrow(ctx, user);
|
auto cls = getClassOrThrow(ctx, user);
|
||||||
|
// Default to getting entries from the last 5 days.
|
||||||
|
SysTime now = Clock.currTime();
|
||||||
|
Date toDate = Date(now.year, now.month, now.day);
|
||||||
|
Date fromDate = toDate - days(4);
|
||||||
|
if (ctx.request.queryParams.contains("to")) {
|
||||||
|
try {
|
||||||
|
toDate = Date.fromISOExtString(ctx.request.queryParams.getFirst("to").orElse(""));
|
||||||
|
} catch (DateTimeException e) {
|
||||||
|
ctx.response.status = HttpStatus.BAD_REQUEST;
|
||||||
|
ctx.response.writeBodyString("Invalid \"to\" date.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ctx.request.queryParams.contains("from")) {
|
||||||
|
try {
|
||||||
|
fromDate = Date.fromISOExtString(ctx.request.queryParams.getFirst("from").orElse(""));
|
||||||
|
} catch (DateTimeException e) {
|
||||||
|
ctx.response.status = HttpStatus.BAD_REQUEST;
|
||||||
|
ctx.response.writeBodyString("Invalid \"from\" date.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
infoF!"Getting entries from %s to %s"(fromDate.toISOExtString(), toDate.toISOExtString());
|
||||||
|
|
||||||
auto db = getDb();
|
auto db = getDb();
|
||||||
auto deskAssignments = findAll!(ClassroomComplianceDeskAssignment)(
|
const query = "
|
||||||
db,
|
SELECT
|
||||||
"
|
entry.id,
|
||||||
SELECT d.* FROM classroom_compliance_desk_assignment d
|
entry.date,
|
||||||
WHERE class_id = ?
|
entry.created_at,
|
||||||
ORDER BY desk_number ASC
|
entry.absent,
|
||||||
",
|
student.id,
|
||||||
cls.id
|
student.name,
|
||||||
);
|
student.desk_number,
|
||||||
writeJsonBody(ctx, deskAssignments);
|
student.removed,
|
||||||
|
phone.compliant,
|
||||||
|
behavior.rating,
|
||||||
|
behavior.comment
|
||||||
|
FROM classroom_compliance_entry entry
|
||||||
|
LEFT JOIN classroom_compliance_entry_phone phone
|
||||||
|
ON phone.entry_id = entry.id
|
||||||
|
LEFT JOIN classroom_compliance_entry_behavior behavior
|
||||||
|
ON behavior.entry_id = entry.id
|
||||||
|
LEFT JOIN classroom_compliance_student student
|
||||||
|
ON student.id = entry.student_id
|
||||||
|
WHERE
|
||||||
|
entry.class_id = ?
|
||||||
|
AND entry.date >= ?
|
||||||
|
AND entry.date <= ?
|
||||||
|
ORDER BY
|
||||||
|
entry.date ASC,
|
||||||
|
student.desk_number ASC,
|
||||||
|
student.name ASC
|
||||||
|
";
|
||||||
|
ResultRange result = db.execute(query, cls.id, fromDate.toISOExtString(), toDate.toISOExtString());
|
||||||
|
// Serialize the results into a custom-formatted response object.
|
||||||
|
import std.json;
|
||||||
|
JSONValue response = JSONValue.emptyObject;
|
||||||
|
JSONValue[ulong] studentObjects;
|
||||||
|
foreach (row; result) {
|
||||||
|
ulong studentId = row.peek!ulong(4);
|
||||||
|
if (studentId !in studentObjects) {
|
||||||
|
JSONValue student = JSONValue.emptyObject;
|
||||||
|
student.object["id"] = JSONValue(row.peek!ulong(4));
|
||||||
|
student.object["name"] = JSONValue(row.peek!string(5));
|
||||||
|
student.object["deskNumber"] = JSONValue(row.peek!ushort(6));
|
||||||
|
student.object["removed"] = JSONValue(row.peek!bool(7));
|
||||||
|
student.object["entries"] = JSONValue.emptyObject;
|
||||||
|
studentObjects[studentId] = student;
|
||||||
}
|
}
|
||||||
|
JSONValue studentObj = studentObjects[studentId];
|
||||||
|
|
||||||
void removeAllDeskAssignments(ref HttpRequestContext ctx) {
|
JSONValue entry = JSONValue.emptyObject;
|
||||||
User user = getUserOrThrow(ctx);
|
entry.object["id"] = JSONValue(row.peek!ulong(0));
|
||||||
auto cls = getClassOrThrow(ctx, user);
|
entry.object["date"] = JSONValue(row.peek!string(1));
|
||||||
auto db = getDb();
|
entry.object["createdAt"] = JSONValue(row.peek!string(2));
|
||||||
db.execute(
|
entry.object["absent"] = JSONValue(row.peek!bool(3));
|
||||||
"DELETE FROM classroom_compliance_desk_assignment WHERE class_id = ?",
|
|
||||||
cls.id
|
JSONValue phone = JSONValue(null);
|
||||||
);
|
JSONValue behavior = JSONValue(null);
|
||||||
|
if (!entry.object["absent"].boolean()) {
|
||||||
|
phone = JSONValue.emptyObject;
|
||||||
|
phone.object["compliant"] = JSONValue(row.peek!bool(8));
|
||||||
|
behavior = JSONValue.emptyObject;
|
||||||
|
behavior.object["rating"] = JSONValue(row.peek!ubyte(9));
|
||||||
|
behavior.object["comment"] = JSONValue(row.peek!string(10));
|
||||||
}
|
}
|
||||||
|
entry.object["phone"] = phone;
|
||||||
|
entry.object["behavior"] = behavior;
|
||||||
|
|
||||||
void createEntry(ref HttpRequestContext ctx) {
|
string dateStr = entry.object["date"].str();
|
||||||
User user = getUserOrThrow(ctx);
|
studentObj.object["entries"].object[dateStr] = entry;
|
||||||
auto cls = getClassOrThrow(ctx, user);
|
}
|
||||||
|
// Provide the list of dates that we're providing data for, to make it easier for the frontend.
|
||||||
|
response.object["dates"] = JSONValue.emptyArray;
|
||||||
|
// Also fill in "null" for any students that don't have an entry on one of these days.
|
||||||
|
Date d = fromDate;
|
||||||
|
while (d <= toDate) {
|
||||||
|
string dateStr = d.toISOExtString();
|
||||||
|
response.object["dates"].array ~= JSONValue(dateStr);
|
||||||
|
foreach (studentObj; studentObjects) {
|
||||||
|
if (dateStr !in studentObj.object["entries"].object) {
|
||||||
|
studentObj.object["entries"].object[dateStr] = JSONValue(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d += days(1);
|
||||||
|
}
|
||||||
|
response.object["students"] = JSONValue(studentObjects.values);
|
||||||
|
|
||||||
|
string jsonStr = response.toJSON();
|
||||||
|
ctx.response.writeBodyString(jsonStr, "application/json");
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,8 +28,8 @@ Database getDb() {
|
||||||
db.run(authSchema);
|
db.run(authSchema);
|
||||||
db.run(classroomComplianceSchema);
|
db.run(classroomComplianceSchema);
|
||||||
|
|
||||||
const string sampleData = import("schema/sample_data.sql");
|
import sample_data;
|
||||||
db.run(sampleData);
|
insertSampleData(db);
|
||||||
|
|
||||||
info("Initialized database schema.");
|
info("Initialized database schema.");
|
||||||
}
|
}
|
||||||
|
@ -92,8 +92,8 @@ private string[] getColumnNames(T)() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private string getArgsStr(T)() {
|
private string getArgsStr(T)() {
|
||||||
import std.traits : RepresentationTypeTuple;
|
import std.traits : Fields;
|
||||||
alias types = RepresentationTypeTuple!T;
|
alias types = Fields!T;
|
||||||
string argsStr = "";
|
string argsStr = "";
|
||||||
static foreach (i, type; types) {
|
static foreach (i, type; types) {
|
||||||
argsStr ~= "row.peek!(" ~ type.stringof ~ ")(" ~ i.to!string ~ ")";
|
argsStr ~= "row.peek!(" ~ type.stringof ~ ")(" ~ i.to!string ~ ")";
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
module sample_data;
|
||||||
|
|
||||||
|
import db;
|
||||||
|
import data_utils;
|
||||||
|
import d2sqlite3;
|
||||||
|
|
||||||
|
import std.random;
|
||||||
|
import std.algorithm;
|
||||||
|
import std.array;
|
||||||
|
import std.datetime;
|
||||||
|
|
||||||
|
private const STUDENT_NAMES = [
|
||||||
|
"Andrew",
|
||||||
|
"Richard",
|
||||||
|
"Klaus",
|
||||||
|
"John",
|
||||||
|
"Wilson",
|
||||||
|
"Grace",
|
||||||
|
"Sarah",
|
||||||
|
"Rebecca",
|
||||||
|
"Lily",
|
||||||
|
"Thomas",
|
||||||
|
"Michael",
|
||||||
|
"Jennifer",
|
||||||
|
"Robert",
|
||||||
|
"Christopher",
|
||||||
|
"Margaret",
|
||||||
|
"Mordecai",
|
||||||
|
"Rigby",
|
||||||
|
"Walter",
|
||||||
|
"Roy",
|
||||||
|
"Cindy"
|
||||||
|
];
|
||||||
|
|
||||||
|
void insertSampleData(ref Database db) {
|
||||||
|
db.begin();
|
||||||
|
ulong adminUserId = addUser(db, "test", "test", false, true);
|
||||||
|
ulong normalUserId = addUser(db, "test2", "test", false, false);
|
||||||
|
Random rand = Random(0);
|
||||||
|
const SysTime now = Clock.currTime();
|
||||||
|
const Date today = Date(now.year, now.month, now.day);
|
||||||
|
|
||||||
|
for (ushort i = 1; i <= 6; i++) {
|
||||||
|
ulong classId = addClass(db, "2024-2025", i, adminUserId);
|
||||||
|
bool classHasAssignedDesks = uniform01(rand) < 0.5;
|
||||||
|
size_t count = uniform(10, STUDENT_NAMES.length, rand);
|
||||||
|
auto studentsToAdd = randomSample(STUDENT_NAMES, count, rand);
|
||||||
|
ushort deskNumber = 1;
|
||||||
|
foreach (name; studentsToAdd) {
|
||||||
|
bool removed = uniform01(rand) < 0.1;
|
||||||
|
ushort assignedDeskNumber = 0;
|
||||||
|
if (classHasAssignedDesks) {
|
||||||
|
assignedDeskNumber = deskNumber++;
|
||||||
|
}
|
||||||
|
ulong studentId = addStudent(db, name, classId, assignedDeskNumber, removed);
|
||||||
|
|
||||||
|
// Add entries for the last N days
|
||||||
|
for (int n = 0; n < 30; n++) {
|
||||||
|
Date entryDate = today - days(n);
|
||||||
|
bool missingEntry = uniform01(rand) < 0.05;
|
||||||
|
if (missingEntry) continue;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong addUser(ref Database db, string username, string password, bool locked, bool admin) {
|
||||||
|
const query = "INSERT INTO user (username, password_hash, created_at, is_locked, is_admin) VALUES (?, ?, ?, ?, ?)";
|
||||||
|
import std.digest.sha;
|
||||||
|
import std.stdio;
|
||||||
|
string passwordHash = cast(string) sha256Of(password).toHexString().idup;
|
||||||
|
db.execute(query, username, passwordHash, getUnixTimestampMillis(), locked, admin);
|
||||||
|
return db.lastInsertRowid();
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong addClass(ref Database db, string schoolYear, ushort number, ulong userId) {
|
||||||
|
const query = "INSERT INTO classroom_compliance_class (number, school_year, user_id) VALUES (?, ?, ?)";
|
||||||
|
db.execute(query, number, schoolYear, userId);
|
||||||
|
return db.lastInsertRowid();
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong addStudent(ref Database db, string name, ulong classId, ushort deskNumber, bool removed) {
|
||||||
|
const query = "INSERT INTO classroom_compliance_student (name, class_id, desk_number, removed) VALUES (?, ?, ?, ?)";
|
||||||
|
db.execute(query, name, classId, deskNumber, removed);
|
||||||
|
return db.lastInsertRowid();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addEntry(
|
||||||
|
ref Database db,
|
||||||
|
ulong classId,
|
||||||
|
ulong studentId,
|
||||||
|
Date date,
|
||||||
|
bool absent,
|
||||||
|
bool phoneCompliant,
|
||||||
|
ubyte behaviorRating,
|
||||||
|
string behaviorComment
|
||||||
|
) {
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
}
|
|
@ -8,6 +8,49 @@ export interface Class {
|
||||||
schoolYear: string
|
schoolYear: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Student {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
classId: number
|
||||||
|
deskNumber: number
|
||||||
|
removed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EntryResponseItemPhone {
|
||||||
|
compliant: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EntryResponseItemBehavior {
|
||||||
|
rating: number
|
||||||
|
comment?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EntryResponseItem {
|
||||||
|
id: number
|
||||||
|
date: string
|
||||||
|
createdAt: string
|
||||||
|
absent: boolean
|
||||||
|
phone?: EntryResponseItemPhone
|
||||||
|
behavior?: EntryResponseItemBehavior
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EntryResponseStudent {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
deskNumber: number
|
||||||
|
removed: boolean
|
||||||
|
entries: Record<string, EntryResponseItem | null>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EntriesResponse {
|
||||||
|
students: EntryResponseStudent[]
|
||||||
|
dates: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getClassPath(classId: number): string {
|
||||||
|
return BASE_URL + '/classes/' + classId
|
||||||
|
}
|
||||||
|
|
||||||
export async function createClass(
|
export async function createClass(
|
||||||
auth: string,
|
auth: string,
|
||||||
number: number,
|
number: number,
|
||||||
|
@ -28,8 +71,15 @@ export async function getClasses(auth: string): Promise<Class[]> {
|
||||||
return (await response.json()) as Class[]
|
return (await response.json()) as Class[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getClass(auth: string, id: number): Promise<Class> {
|
||||||
|
const response = await fetch(getClassPath(id), {
|
||||||
|
headers: getAuthHeaders(auth),
|
||||||
|
})
|
||||||
|
return (await response.json()) as Class
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteClass(auth: string, classId: number): Promise<void> {
|
export async function deleteClass(auth: string, classId: number): Promise<void> {
|
||||||
const response = await fetch(BASE_URL + '/classes/' + classId, {
|
const response = await fetch(getClassPath(classId), {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: getAuthHeaders(auth),
|
headers: getAuthHeaders(auth),
|
||||||
})
|
})
|
||||||
|
@ -37,3 +87,29 @@ export async function deleteClass(auth: string, classId: number): Promise<void>
|
||||||
throw new Error('Failed to delete class.')
|
throw new Error('Failed to delete class.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getStudents(auth: string, classId: number): Promise<Student[]> {
|
||||||
|
const response = await fetch(getClassPath(classId) + '/students', {
|
||||||
|
headers: getAuthHeaders(auth),
|
||||||
|
})
|
||||||
|
return (await response.json()) as Student[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getEntries(
|
||||||
|
auth: string,
|
||||||
|
classId: number,
|
||||||
|
fromDate?: Date,
|
||||||
|
toDate?: Date,
|
||||||
|
): Promise<EntriesResponse> {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (fromDate) {
|
||||||
|
params.append('from', fromDate.toISOString().substring(0, 10))
|
||||||
|
}
|
||||||
|
if (toDate) {
|
||||||
|
params.append('to', toDate.toISOString().substring(0, 10))
|
||||||
|
}
|
||||||
|
const response = await fetch(getClassPath(classId) + '/entries?' + params.toString(), {
|
||||||
|
headers: getAuthHeaders(auth),
|
||||||
|
})
|
||||||
|
return (await response.json()) as EntriesResponse
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { type Class } from '@/api/classroom_compliance';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
cls: Class
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="class-item">
|
||||||
|
<h3>Class <span v-text="cls.number"></span></h3>
|
||||||
|
<p v-text="cls.schoolYear"></p>
|
||||||
|
<div>
|
||||||
|
<RouterLink :to="'/classroom-compliance/classes/' + cls.id">View</RouterLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.class-item {
|
||||||
|
border: 1px solid black;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,37 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getClass, getStudents, type Class, type Student } from '@/api/classroom_compliance';
|
||||||
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
import { onMounted, ref, type Ref } from 'vue';
|
||||||
|
import EntriesTable from '@/apps/classroom_compliance/EntriesTable.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
id: string
|
||||||
|
}>()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const cls: Ref<Class | null> = ref(null)
|
||||||
|
const students: Ref<Student[]> = ref([])
|
||||||
|
onMounted(async () => {
|
||||||
|
const idNumber = parseInt(props.id, 10)
|
||||||
|
cls.value = await getClass(authStore.getBasicAuth(), idNumber)
|
||||||
|
getStudents(authStore.getBasicAuth(), idNumber).then(r => {
|
||||||
|
students.value = r
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div v-if="cls">
|
||||||
|
<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>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<span>Actions: </span>
|
||||||
|
<button type="button">Add Student - WIP</button>
|
||||||
|
<button type="button">Delete this Class</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EntriesTable :classId="cls.id" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped></style>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { type Class, getClasses } from '@/api/classroom_compliance';
|
||||||
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
import { type Ref, ref, onMounted } from 'vue';
|
||||||
|
import ClassItem from '@/apps/classroom_compliance/ClassItem.vue';
|
||||||
|
|
||||||
|
const classes: Ref<Class[]> = ref([])
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
classes.value = await getClasses(authStore.getBasicAuth())
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ClassItem v-for="cls in classes" :key="cls.id" :cls="cls" />
|
||||||
|
</template>
|
|
@ -0,0 +1,90 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getEntries, type EntryResponseStudent } from '@/api/classroom_compliance';
|
||||||
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
import { onMounted, ref, type Ref } from 'vue';
|
||||||
|
import EntryTableCell from './EntryTableCell.vue';
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const props = defineProps<{
|
||||||
|
classId: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const students: Ref<EntryResponseStudent[]> = ref([])
|
||||||
|
const dates: Ref<string[]> = ref([])
|
||||||
|
const toDate: Ref<Date> = ref(new Date())
|
||||||
|
const fromDate: Ref<Date> = ref(new Date())
|
||||||
|
onMounted(async () => {
|
||||||
|
toDate.value.setHours(0, 0, 0, 0)
|
||||||
|
fromDate.value.setHours(0, 0, 0, 0)
|
||||||
|
fromDate.value.setDate(fromDate.value.getDate() - 4)
|
||||||
|
await loadEntries()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadEntries() {
|
||||||
|
const entries = await getEntries(
|
||||||
|
authStore.getBasicAuth(),
|
||||||
|
props.classId,
|
||||||
|
fromDate.value,
|
||||||
|
toDate.value
|
||||||
|
)
|
||||||
|
students.value = entries.students
|
||||||
|
dates.value = entries.dates
|
||||||
|
}
|
||||||
|
|
||||||
|
function shiftDateRange(days: number) {
|
||||||
|
toDate.value.setDate(toDate.value.getDate() + days)
|
||||||
|
fromDate.value.setDate(fromDate.value.getDate() + days)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showPreviousDay() {
|
||||||
|
shiftDateRange(-1)
|
||||||
|
await loadEntries()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showNextDay() {
|
||||||
|
shiftDateRange(1)
|
||||||
|
await loadEntries()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<button type="button" @click="showPreviousDay">Previous Day</button>
|
||||||
|
<button type="button" @click="showNextDay">Next Day</button>
|
||||||
|
</div>
|
||||||
|
<table class="entries-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Student</th>
|
||||||
|
<th>Desk</th>
|
||||||
|
<th v-for="date in dates" :key="date" v-text="date"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="student in students" :key="student.id">
|
||||||
|
<td v-text="student.name" :class="{ 'student-removed': student.removed }"></td>
|
||||||
|
<td v-text="student.deskNumber"></td>
|
||||||
|
<EntryTableCell v-for="(entry, date) in student.entries" :key="date" :entry="entry" />
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.entries-table {
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entries-table,
|
||||||
|
.entries-table th,
|
||||||
|
.entries-table td {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.student-removed {
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,46 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { EntryResponseItem } from '@/api/classroom_compliance';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
entry: EntryResponseItem | null
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<td v-if="entry" :class="{ absent: entry.absent }">
|
||||||
|
<span v-if="entry.absent">Absent</span>
|
||||||
|
<div v-if="!entry.absent">
|
||||||
|
<div class="status-item">
|
||||||
|
<span v-if="entry.phone?.compliant">📱</span>
|
||||||
|
<span v-if="!entry.phone?.compliant">📵</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span v-if="entry.behavior?.rating === 3">😇</span>
|
||||||
|
<span v-if="entry.behavior?.rating === 2">😐</span>
|
||||||
|
<span v-if="entry.behavior?.rating === 1">😡</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td v-if="entry === null" class="missing-entry"></td>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
td {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.missing-entry {
|
||||||
|
background-color: lightgray;
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.absent {
|
||||||
|
color: blue;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,16 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<h1>Classroom Compliance</h1>
|
||||||
|
<p>With this application, you can track each student's compliance to various classroom policies, like:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Attendance</li>
|
||||||
|
<li>Phone Usage (or lack thereof)</li>
|
||||||
|
<li>Behavior</li>
|
||||||
|
</ul>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<RouterView />
|
||||||
|
</main>
|
||||||
|
</template>
|
|
@ -15,7 +15,18 @@ const router = createRouter({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/classroom-compliance',
|
path: '/classroom-compliance',
|
||||||
component: () => import('@/views/apps/ClassroomCompliance.vue'),
|
component: () => import('@/apps/classroom_compliance/MainView.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: () => import('@/apps/classroom_compliance/ClassesView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'classes/:id',
|
||||||
|
component: () => import('@/apps/classroom_compliance/ClassView.vue'),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { getClasses, type Class } from '@/api/classroom_compliance';
|
|
||||||
import { useAuthStore } from '@/stores/auth';
|
|
||||||
import { onMounted, ref, type Ref } from 'vue';
|
|
||||||
|
|
||||||
const classes: Ref<Class[]> = ref([])
|
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
classes.value = await getClasses(authStore.getBasicAuth())
|
|
||||||
console.log(classes.value)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<main>
|
|
||||||
<h1>Classroom Compliance</h1>
|
|
||||||
<p>Here you can track stuff.</p>
|
|
||||||
<div v-for="cls in classes" :key="cls.id">
|
|
||||||
<h3 v-text="cls.number"></h3>
|
|
||||||
<p v-text="cls.schoolYear"></p>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</template>
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
meta {
|
||||||
|
name: Get Compliance Entries
|
||||||
|
type: http
|
||||||
|
seq: 9
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{base_url}}/classroom-compliance/classes/:classId/entries
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
params:path {
|
||||||
|
classId: {{class_id}}
|
||||||
|
}
|
|
@ -1,15 +0,0 @@
|
||||||
meta {
|
|
||||||
name: Get Desk Assignments
|
|
||||||
type: http
|
|
||||||
seq: 8
|
|
||||||
}
|
|
||||||
|
|
||||||
get {
|
|
||||||
url: {{base_url}}/classroom-compliance/classes/:classId/desk-assignments
|
|
||||||
body: none
|
|
||||||
auth: inherit
|
|
||||||
}
|
|
||||||
|
|
||||||
params:path {
|
|
||||||
classId: {{class_id}}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
meta {
|
|
||||||
name: Set Desk Assignments
|
|
||||||
type: http
|
|
||||||
seq: 9
|
|
||||||
}
|
|
||||||
|
|
||||||
post {
|
|
||||||
url: {{base_url}}/classroom-compliance/classes/:classId/desk-assignments
|
|
||||||
body: json
|
|
||||||
auth: inherit
|
|
||||||
}
|
|
||||||
|
|
||||||
params:path {
|
|
||||||
classId: {{class_id}}
|
|
||||||
}
|
|
||||||
|
|
||||||
body:json {
|
|
||||||
{
|
|
||||||
"entries": [
|
|
||||||
{
|
|
||||||
"deskNumber": 1,
|
|
||||||
"studentId": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"deskNumber": 2,
|
|
||||||
"studentId": 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue