Added lots more improvements, including scores.
This commit is contained in:
parent
3a682e046d
commit
3f47be9653
|
@ -35,6 +35,5 @@ CREATE TABLE classroom_compliance_entry_phone (
|
|||
CREATE TABLE classroom_compliance_entry_behavior (
|
||||
entry_id INTEGER PRIMARY KEY REFERENCES classroom_compliance_entry(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
rating INTEGER NOT NULL,
|
||||
comment TEXT
|
||||
rating INTEGER NOT NULL
|
||||
);
|
||||
|
|
|
@ -25,7 +25,7 @@ private struct UserResponse {
|
|||
bool isAdmin;
|
||||
}
|
||||
|
||||
Optional!User getUser(ref HttpRequestContext ctx) {
|
||||
Optional!User getUser(ref HttpRequestContext ctx, ref Database db) {
|
||||
import std.base64;
|
||||
import std.string : startsWith;
|
||||
import std.digest.sha;
|
||||
|
@ -40,7 +40,6 @@ Optional!User getUser(ref HttpRequestContext ctx) {
|
|||
size_t idx = countUntil(decoded, ':');
|
||||
string username = decoded[0..idx];
|
||||
auto passwordHash = toHexString(sha256Of(decoded[idx+1 .. $]));
|
||||
Database db = getDb();
|
||||
Optional!User optUser = findOne!(User)(db, "SELECT * FROM user WHERE username = ?", username);
|
||||
if (!optUser.isNull && optUser.value.passwordHash != passwordHash) {
|
||||
return Optional!User.empty;
|
||||
|
@ -48,8 +47,8 @@ Optional!User getUser(ref HttpRequestContext ctx) {
|
|||
return optUser;
|
||||
}
|
||||
|
||||
User getUserOrThrow(ref HttpRequestContext ctx) {
|
||||
Optional!User optUser = getUser(ctx);
|
||||
User getUserOrThrow(ref HttpRequestContext ctx, ref Database db) {
|
||||
Optional!User optUser = getUser(ctx, db);
|
||||
if (optUser.isNull) {
|
||||
throw new HttpStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials.");
|
||||
}
|
||||
|
@ -57,7 +56,8 @@ User getUserOrThrow(ref HttpRequestContext ctx) {
|
|||
}
|
||||
|
||||
void loginEndpoint(ref HttpRequestContext ctx) {
|
||||
Optional!User optUser = getUser(ctx);
|
||||
Database db = getDb();
|
||||
Optional!User optUser = getUser(ctx, db);
|
||||
if (optUser.isNull) {
|
||||
ctx.response.status = HttpStatus.UNAUTHORIZED;
|
||||
ctx.response.writeBodyString("Invalid credentials.");
|
||||
|
|
|
@ -6,7 +6,6 @@ import d2sqlite3;
|
|||
import slf4d;
|
||||
import std.typecons : Nullable;
|
||||
import std.datetime;
|
||||
import std.format;
|
||||
import std.json;
|
||||
import std.algorithm;
|
||||
import std.array;
|
||||
|
@ -47,7 +46,6 @@ struct ClassroomComplianceEntryPhone {
|
|||
struct ClassroomComplianceEntryBehavior {
|
||||
const ulong entryId;
|
||||
const ubyte rating;
|
||||
const string comment;
|
||||
}
|
||||
|
||||
void registerApiEndpoints(PathHandler handler) {
|
||||
|
@ -71,13 +69,13 @@ void registerApiEndpoints(PathHandler handler) {
|
|||
}
|
||||
|
||||
void createClass(ref HttpRequestContext ctx) {
|
||||
User user = getUserOrThrow(ctx);
|
||||
auto db = getDb();
|
||||
User user = getUserOrThrow(ctx, db);
|
||||
struct ClassPayload {
|
||||
ushort number;
|
||||
string schoolYear;
|
||||
}
|
||||
auto payload = readJsonPayload!(ClassPayload)(ctx);
|
||||
auto db = getDb();
|
||||
const bool classNumberExists = canFind(
|
||||
db,
|
||||
"SELECT id FROM classroom_compliance_class WHERE number = ? AND school_year = ? AND user_id = ?",
|
||||
|
@ -104,8 +102,8 @@ void createClass(ref HttpRequestContext ctx) {
|
|||
}
|
||||
|
||||
void getClasses(ref HttpRequestContext ctx) {
|
||||
User user = getUserOrThrow(ctx);
|
||||
auto db = getDb();
|
||||
User user = getUserOrThrow(ctx, db);
|
||||
auto classes = findAll!(ClassroomComplianceClass)(
|
||||
db,
|
||||
"SELECT * FROM classroom_compliance_class WHERE user_id = ? ORDER BY school_year DESC, number ASC",
|
||||
|
@ -115,21 +113,21 @@ void getClasses(ref HttpRequestContext ctx) {
|
|||
}
|
||||
|
||||
void getClass(ref HttpRequestContext ctx) {
|
||||
User user = getUserOrThrow(ctx);
|
||||
auto cls = getClassOrThrow(ctx, user);
|
||||
auto db = getDb();
|
||||
User user = getUserOrThrow(ctx, db);
|
||||
auto cls = getClassOrThrow(ctx, db, user);
|
||||
writeJsonBody(ctx, cls);
|
||||
}
|
||||
|
||||
void deleteClass(ref HttpRequestContext ctx) {
|
||||
User user = getUserOrThrow(ctx);
|
||||
auto cls = getClassOrThrow(ctx, user);
|
||||
auto db = getDb();
|
||||
User user = getUserOrThrow(ctx, db);
|
||||
auto cls = getClassOrThrow(ctx, db, user);
|
||||
db.execute("DELETE FROM classroom_compliance_class WHERE id = ? AND user_id = ?", cls.id, user.id);
|
||||
}
|
||||
|
||||
ClassroomComplianceClass getClassOrThrow(ref HttpRequestContext ctx, in User user) {
|
||||
ClassroomComplianceClass getClassOrThrow(ref HttpRequestContext ctx, ref Database db, in User user) {
|
||||
ulong classId = ctx.request.getPathParamAs!ulong("classId");
|
||||
auto db = getDb();
|
||||
return findOne!(ClassroomComplianceClass)(
|
||||
db,
|
||||
"SELECT * FROM classroom_compliance_class WHERE user_id = ? AND id = ?",
|
||||
|
@ -139,15 +137,15 @@ ClassroomComplianceClass getClassOrThrow(ref HttpRequestContext ctx, in User use
|
|||
}
|
||||
|
||||
void createStudent(ref HttpRequestContext ctx) {
|
||||
User user = getUserOrThrow(ctx);
|
||||
auto cls = getClassOrThrow(ctx, user);
|
||||
auto db = getDb();
|
||||
User user = getUserOrThrow(ctx, db);
|
||||
auto cls = getClassOrThrow(ctx, db, user);
|
||||
struct StudentPayload {
|
||||
string name;
|
||||
ushort deskNumber;
|
||||
bool removed;
|
||||
}
|
||||
auto payload = readJsonPayload!(StudentPayload)(ctx);
|
||||
auto db = getDb();
|
||||
bool studentExists = canFind(
|
||||
db,
|
||||
"SELECT id FROM classroom_compliance_student WHERE name = ? AND class_id = ?",
|
||||
|
@ -183,9 +181,9 @@ void createStudent(ref HttpRequestContext ctx) {
|
|||
}
|
||||
|
||||
void getStudents(ref HttpRequestContext ctx) {
|
||||
User user = getUserOrThrow(ctx);
|
||||
auto cls = getClassOrThrow(ctx, user);
|
||||
auto db = getDb();
|
||||
User user = getUserOrThrow(ctx, db);
|
||||
auto cls = getClassOrThrow(ctx, db, user);
|
||||
auto students = findAll!(ClassroomComplianceStudent)(
|
||||
db,
|
||||
"SELECT * FROM classroom_compliance_student WHERE class_id = ? ORDER BY name ASC",
|
||||
|
@ -195,14 +193,16 @@ void getStudents(ref HttpRequestContext ctx) {
|
|||
}
|
||||
|
||||
void getStudent(ref HttpRequestContext ctx) {
|
||||
User user = getUserOrThrow(ctx);
|
||||
auto student = getStudentOrThrow(ctx, user);
|
||||
auto db = getDb();
|
||||
User user = getUserOrThrow(ctx, db);
|
||||
auto student = getStudentOrThrow(ctx, db, user);
|
||||
writeJsonBody(ctx, student);
|
||||
}
|
||||
|
||||
void updateStudent(ref HttpRequestContext ctx) {
|
||||
User user = getUserOrThrow(ctx);
|
||||
auto student = getStudentOrThrow(ctx, user);
|
||||
auto db = getDb();
|
||||
User user = getUserOrThrow(ctx, db);
|
||||
auto student = getStudentOrThrow(ctx, db, user);
|
||||
struct StudentUpdatePayload {
|
||||
string name;
|
||||
ushort deskNumber;
|
||||
|
@ -216,7 +216,6 @@ void updateStudent(ref HttpRequestContext ctx) {
|
|||
&& payload.removed == student.removed
|
||||
) return;
|
||||
// Check that the new name doesn't already exist.
|
||||
auto db = getDb();
|
||||
bool newNameExists = payload.name != student.name && canFind(
|
||||
db,
|
||||
"SELECT id FROM classroom_compliance_student WHERE name = ? AND class_id = ?",
|
||||
|
@ -256,9 +255,9 @@ void updateStudent(ref HttpRequestContext ctx) {
|
|||
}
|
||||
|
||||
void deleteStudent(ref HttpRequestContext ctx) {
|
||||
User user = getUserOrThrow(ctx);
|
||||
auto student = getStudentOrThrow(ctx, user);
|
||||
auto db = getDb();
|
||||
User user = getUserOrThrow(ctx, db);
|
||||
auto student = getStudentOrThrow(ctx, db, user);
|
||||
db.execute(
|
||||
"DELETE FROM classroom_compliance_student WHERE id = ? AND class_id = ?",
|
||||
student.id,
|
||||
|
@ -266,10 +265,9 @@ void deleteStudent(ref HttpRequestContext ctx) {
|
|||
);
|
||||
}
|
||||
|
||||
ClassroomComplianceStudent getStudentOrThrow(ref HttpRequestContext ctx, in User user) {
|
||||
ClassroomComplianceStudent getStudentOrThrow(ref HttpRequestContext ctx, ref Database db, in User user) {
|
||||
ulong classId = ctx.request.getPathParamAs!ulong("classId");
|
||||
ulong studentId = ctx.request.getPathParamAs!ulong("studentId");
|
||||
auto db = getDb();
|
||||
string query = "
|
||||
SELECT s.*
|
||||
FROM classroom_compliance_student s
|
||||
|
@ -286,8 +284,9 @@ ClassroomComplianceStudent getStudentOrThrow(ref HttpRequestContext ctx, in User
|
|||
}
|
||||
|
||||
void getEntries(ref HttpRequestContext ctx) {
|
||||
User user = getUserOrThrow(ctx);
|
||||
auto cls = getClassOrThrow(ctx, user);
|
||||
auto db = getDb();
|
||||
User user = getUserOrThrow(ctx, db);
|
||||
auto cls = getClassOrThrow(ctx, db, user);
|
||||
// Default to getting entries from the last 5 days.
|
||||
SysTime now = Clock.currTime();
|
||||
Date toDate = Date(now.year, now.month, now.day);
|
||||
|
@ -310,10 +309,19 @@ void getEntries(ref HttpRequestContext ctx) {
|
|||
return;
|
||||
}
|
||||
}
|
||||
if (fromDate > toDate) {
|
||||
ctx.response.status = HttpStatus.BAD_REQUEST;
|
||||
ctx.response.writeBodyString("Invalid date range. From-date must be less than or equal to the to-date.");
|
||||
return;
|
||||
}
|
||||
if (toDate - fromDate > days(10)) {
|
||||
ctx.response.status = HttpStatus.BAD_REQUEST;
|
||||
ctx.response.writeBodyString("Date range is too big. Only ranges of 10 days or less are allowed.");
|
||||
return;
|
||||
}
|
||||
infoF!"Getting entries from %s to %s"(fromDate.toISOExtString(), toDate.toISOExtString());
|
||||
|
||||
auto db = getDb();
|
||||
|
||||
// First prepare a list of all students, including ones which don't have any entries.
|
||||
ClassroomComplianceStudent[] students = findAll!(ClassroomComplianceStudent)(
|
||||
db,
|
||||
"SELECT * FROM classroom_compliance_student WHERE class_id = ? ORDER BY name ASC",
|
||||
|
@ -326,10 +334,11 @@ void getEntries(ref HttpRequestContext ctx) {
|
|||
obj.object["name"] = JSONValue(s.name);
|
||||
obj.object["removed"] = JSONValue(s.removed);
|
||||
obj.object["entries"] = JSONValue.emptyObject;
|
||||
obj.object["score"] = JSONValue(null);
|
||||
return obj;
|
||||
}).array;
|
||||
|
||||
const query = "
|
||||
const entriesQuery = "
|
||||
SELECT
|
||||
entry.id,
|
||||
entry.date,
|
||||
|
@ -340,8 +349,7 @@ void getEntries(ref HttpRequestContext ctx) {
|
|||
student.desk_number,
|
||||
student.removed,
|
||||
phone.compliant,
|
||||
behavior.rating,
|
||||
behavior.comment
|
||||
behavior.rating
|
||||
FROM classroom_compliance_entry entry
|
||||
LEFT JOIN classroom_compliance_entry_phone phone
|
||||
ON phone.entry_id = entry.id
|
||||
|
@ -357,9 +365,9 @@ void getEntries(ref HttpRequestContext ctx) {
|
|||
student.id ASC,
|
||||
entry.date ASC
|
||||
";
|
||||
ResultRange result = db.execute(query, cls.id, fromDate.toISOExtString(), toDate.toISOExtString());
|
||||
ResultRange entriesResult = db.execute(entriesQuery, cls.id, fromDate.toISOExtString(), toDate.toISOExtString());
|
||||
// Serialize the results into a custom-formatted response object.
|
||||
foreach (row; result) {
|
||||
foreach (row; entriesResult) {
|
||||
JSONValue entry = JSONValue.emptyObject;
|
||||
entry.object["id"] = JSONValue(row.peek!ulong(0));
|
||||
entry.object["date"] = JSONValue(row.peek!string(1));
|
||||
|
@ -373,7 +381,6 @@ void getEntries(ref HttpRequestContext ctx) {
|
|||
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;
|
||||
|
@ -394,6 +401,23 @@ void getEntries(ref HttpRequestContext ctx) {
|
|||
}
|
||||
}
|
||||
|
||||
// Find scores for each student for this timeframe.
|
||||
Optional!double[ulong] scores = getScores(db, cls.id, fromDate, toDate);
|
||||
foreach (studentId, score; scores) {
|
||||
JSONValue scoreValue = score.isNull ? JSONValue(null) : JSONValue(score.value);
|
||||
bool studentFound = false;
|
||||
foreach (idx, student; students) {
|
||||
if (studentId == student.id) {
|
||||
studentObjects[idx].object["score"] = scoreValue;
|
||||
studentFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!studentFound) {
|
||||
throw new Exception("Failed to find student.");
|
||||
}
|
||||
}
|
||||
|
||||
JSONValue response = JSONValue.emptyObject;
|
||||
// Provide the list of dates that we're providing data for, to make it easier for the frontend.
|
||||
response.object["dates"] = JSONValue.emptyArray;
|
||||
|
@ -417,23 +441,18 @@ void getEntries(ref HttpRequestContext ctx) {
|
|||
}
|
||||
|
||||
void saveEntries(ref HttpRequestContext ctx) {
|
||||
User user = getUserOrThrow(ctx);
|
||||
auto cls = getClassOrThrow(ctx, user);
|
||||
JSONValue bodyContent = ctx.request.readBodyAsJson();
|
||||
auto db = getDb();
|
||||
User user = getUserOrThrow(ctx, db);
|
||||
auto cls = getClassOrThrow(ctx, db, user);
|
||||
JSONValue bodyContent = ctx.request.readBodyAsJson();
|
||||
db.begin();
|
||||
try {
|
||||
foreach (JSONValue studentObj; bodyContent.object["students"].array) {
|
||||
ulong studentId = studentObj.object["id"].integer();
|
||||
JSONValue entries = studentObj.object["entries"];
|
||||
foreach (string dateStr, JSONValue entry; entries.object) {
|
||||
if (entry.isNull) {
|
||||
db.execute(
|
||||
"DELETE FROM classroom_compliance_entry WHERE class_id = ? AND student_id = ? AND date = ?",
|
||||
cls.id,
|
||||
studentId,
|
||||
dateStr
|
||||
);
|
||||
infoF!"Deleted entry for student %s on %s"(studentId, dateStr);
|
||||
deleteEntry(db, cls.id, studentId, dateStr);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -449,12 +468,7 @@ void saveEntries(ref HttpRequestContext ctx) {
|
|||
if (creatingNewEntry) {
|
||||
if (!existingEntry.isNull) {
|
||||
ctx.response.status = HttpStatus.BAD_REQUEST;
|
||||
ctx.response.writeBodyString(
|
||||
format!"Cannot create a new entry for student %d on %s when one already exists."(
|
||||
studentId,
|
||||
dateStr
|
||||
)
|
||||
);
|
||||
ctx.response.writeBodyString("Cannot create a new entry when one already exists.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -462,11 +476,7 @@ void saveEntries(ref HttpRequestContext ctx) {
|
|||
} else {
|
||||
if (existingEntry.isNull) {
|
||||
ctx.response.status = HttpStatus.BAD_REQUEST;
|
||||
ctx.response.writeBodyString(
|
||||
format!"Cannot update entry %d because it doesn't exist."(
|
||||
entryId
|
||||
)
|
||||
);
|
||||
ctx.response.writeBodyString("Cannot update an entry which doesn't exist.");
|
||||
return;
|
||||
}
|
||||
updateEntry(db, cls.id, studentId, dateStr, entryId, entry);
|
||||
|
@ -474,6 +484,36 @@ void saveEntries(ref HttpRequestContext ctx) {
|
|||
}
|
||||
}
|
||||
db.commit();
|
||||
} catch (HttpStatusException e) {
|
||||
db.rollback();
|
||||
ctx.response.status = e.status;
|
||||
ctx.response.writeBodyString(e.message);
|
||||
} catch (JSONException e) {
|
||||
db.rollback();
|
||||
ctx.response.status = HttpStatus.BAD_REQUEST;
|
||||
ctx.response.writeBodyString("Invalid JSON payload.");
|
||||
warn(e);
|
||||
} catch (Exception e) {
|
||||
db.rollback();
|
||||
ctx.response.status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
ctx.response.writeBodyString("An internal server error occurred: " ~ e.msg);
|
||||
error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteEntry(
|
||||
ref Database db,
|
||||
ulong classId,
|
||||
ulong studentId,
|
||||
string dateStr
|
||||
) {
|
||||
db.execute(
|
||||
"DELETE FROM classroom_compliance_entry WHERE class_id = ? AND student_id = ? AND date = ?",
|
||||
classId,
|
||||
studentId,
|
||||
dateStr
|
||||
);
|
||||
infoF!"Deleted entry for student %s on %s"(studentId, dateStr);
|
||||
}
|
||||
|
||||
private void insertNewEntry(
|
||||
|
@ -507,9 +547,9 @@ private void insertNewEntry(
|
|||
);
|
||||
ubyte behaviorRating = cast(ubyte) payload.object["behavior"].object["rating"].integer;
|
||||
db.execute(
|
||||
"INSERT INTO classroom_compliance_entry_behavior (entry_id, rating, comment)
|
||||
VALUES (?, ?, ?)",
|
||||
entryId, behaviorRating, ""
|
||||
"INSERT INTO classroom_compliance_entry_behavior (entry_id, rating)
|
||||
VALUES (?, ?)",
|
||||
entryId, behaviorRating
|
||||
);
|
||||
}
|
||||
infoF!"Created new entry for student %d: %s"(studentId, payload);
|
||||
|
@ -587,3 +627,102 @@ private void updateEntry(
|
|||
}
|
||||
infoF!"Updated entry %d"(entryId);
|
||||
}
|
||||
|
||||
Optional!double[ulong] getScores(
|
||||
ref Database db,
|
||||
ulong classId,
|
||||
Date fromDate,
|
||||
Date toDate
|
||||
) {
|
||||
infoF!"Getting scores from %s to %s"(fromDate.toISOExtString(), toDate.toISOExtString());
|
||||
|
||||
// First populate all students with an initial "null" score.
|
||||
Optional!double[ulong] scores;
|
||||
ResultRange studentsResult = db.execute(
|
||||
"SELECT id FROM classroom_compliance_student WHERE class_id = ?",
|
||||
classId
|
||||
);
|
||||
foreach (row; studentsResult) {
|
||||
scores[row.peek!ulong(0)] = Optional!double.empty;
|
||||
}
|
||||
|
||||
const query = "
|
||||
SELECT
|
||||
e.student_id,
|
||||
COUNT(e.id) AS entry_count,
|
||||
SUM(e.absent) AS absence_count,
|
||||
SUM(NOT p.compliant) AS phone_noncompliance_count,
|
||||
SUM(b.rating = 3) AS behavior_good,
|
||||
SUM(b.rating = 2) AS behavior_mediocre,
|
||||
SUM(b.rating = 1) AS behavior_poor
|
||||
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.date >= ?
|
||||
AND e.date <= ?
|
||||
AND e.class_id = ?
|
||||
GROUP BY e.student_id
|
||||
";
|
||||
ResultRange result = db.execute(
|
||||
query,
|
||||
fromDate.toISOExtString(),
|
||||
toDate.toISOExtString(),
|
||||
classId
|
||||
);
|
||||
foreach (row; result) {
|
||||
ulong studentId = row.peek!ulong(0);
|
||||
uint entryCount = row.peek!uint(1);
|
||||
uint absenceCount = row.peek!uint(2);
|
||||
uint phoneNonComplianceCount = row.peek!uint(3);
|
||||
uint behaviorGoodCount = row.peek!uint(4);
|
||||
uint behaviorMediocreCount = row.peek!uint(5);
|
||||
uint behaviorPoorCount = row.peek!uint(6);
|
||||
scores[studentId] = calculateScore(
|
||||
entryCount,
|
||||
absenceCount,
|
||||
phoneNonComplianceCount,
|
||||
behaviorGoodCount,
|
||||
behaviorMediocreCount,
|
||||
behaviorPoorCount
|
||||
);
|
||||
}
|
||||
return scores;
|
||||
}
|
||||
|
||||
private Optional!double calculateScore(
|
||||
uint entryCount,
|
||||
uint absenceCount,
|
||||
uint phoneNonComplianceCount,
|
||||
uint behaviorGoodCount,
|
||||
uint behaviorMediocreCount,
|
||||
uint behaviorPoorCount
|
||||
) {
|
||||
if (
|
||||
entryCount == 0
|
||||
|| entryCount <= absenceCount
|
||||
) return Optional!double.empty;
|
||||
|
||||
const uint presentCount = entryCount - absenceCount;
|
||||
|
||||
// Phone subscore:
|
||||
uint phoneCompliantCount;
|
||||
if (presentCount < phoneNonComplianceCount) {
|
||||
phoneCompliantCount = 0;
|
||||
} else {
|
||||
phoneCompliantCount = presentCount - phoneNonComplianceCount;
|
||||
}
|
||||
double phoneScore = phoneCompliantCount / cast(double) presentCount;
|
||||
|
||||
// Behavior subscore:
|
||||
double behaviorScore = (
|
||||
behaviorGoodCount * 1.0
|
||||
+ behaviorMediocreCount * 0.5
|
||||
+ behaviorPoorCount * 0
|
||||
) / cast(double) presentCount;
|
||||
|
||||
double score = 0.3 * phoneScore + 0.7 * behaviorScore;
|
||||
return Optional!double.of(score);
|
||||
}
|
||||
|
|
|
@ -15,19 +15,19 @@ async function logOut() {
|
|||
<template>
|
||||
<header>
|
||||
<div>
|
||||
<nav>
|
||||
<nav class="global-navbar">
|
||||
<div>
|
||||
<RouterLink to="/">Home</RouterLink>
|
||||
<RouterLink to="/login" v-if="!authStore.state">Login</RouterLink>
|
||||
<RouterLink to="/my-account" v-if="authStore.state">My Account</RouterLink>
|
||||
</div>
|
||||
|
||||
<span v-if="authStore.state">
|
||||
Welcome, <span v-text="authStore.state.username"></span>
|
||||
<div>
|
||||
<RouterLink to="/login" v-if="!authStore.state">Login</RouterLink>
|
||||
<span v-if="authStore.state" style="margin-right: 0.5em">
|
||||
Logged in as <span v-text="authStore.state.username" style="font-weight: bold;"></span>
|
||||
</span>
|
||||
<button type="button" @click="logOut" v-if="authStore.state">Log out</button>
|
||||
</nav>
|
||||
<nav v-if="authStore.state">
|
||||
Apps:
|
||||
|
||||
<RouterLink to="/classroom-compliance">Classroom Compliance</RouterLink>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
@ -38,4 +38,17 @@ async function logOut() {
|
|||
<AlertDialog />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.global-navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.global-navbar>div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.global-navbar>div>a+a {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,7 +5,7 @@ const BASE_URL = import.meta.env.VITE_API_URL + '/auth'
|
|||
export interface User {
|
||||
id: number
|
||||
username: string
|
||||
createdAt: Date
|
||||
createdAt: number
|
||||
isLocked: boolean
|
||||
isAdmin: boolean
|
||||
}
|
||||
|
|
|
@ -102,18 +102,12 @@ export abstract class APIClient {
|
|||
protected async handleAPIResponse<T>(promise: Promise<Response>): Promise<T | APIError> {
|
||||
try {
|
||||
const response = await promise
|
||||
if (response.ok) {
|
||||
return (await response.json()) as T
|
||||
}
|
||||
if (response.status === 400) {
|
||||
return new BadRequestError(await response.text())
|
||||
}
|
||||
if (response.status === 401) {
|
||||
return new AuthenticationError(await response.text())
|
||||
}
|
||||
return new InternalServerError(await response.text())
|
||||
if (response.ok) return (await response.json()) as T
|
||||
return this.transformErrorResponse(response)
|
||||
} catch (error) {
|
||||
return new NetworkError('' + error)
|
||||
return new NetworkError(
|
||||
'' + error + " (We couldn't connect to the remote server; it might be down!)",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -123,12 +117,17 @@ export abstract class APIClient {
|
|||
try {
|
||||
const response = await promise
|
||||
if (response.ok) return undefined
|
||||
if (response.status === 401) {
|
||||
return new AuthenticationError(await response.text())
|
||||
}
|
||||
return new InternalServerError(await response.text())
|
||||
return this.transformErrorResponse(response)
|
||||
} catch (error) {
|
||||
return new NetworkError('' + error)
|
||||
return new NetworkError(
|
||||
'' + error + " (We couldn't connect to the remote server; it might be down!)",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private async transformErrorResponse(r: Response): Promise<APIError> {
|
||||
if (r.status === 401) return new AuthenticationError(await r.text())
|
||||
if (r.status === 400) return new BadRequestError(await r.text())
|
||||
return new InternalServerError(await r.text())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ export interface EntryPhone {
|
|||
|
||||
export interface EntryBehavior {
|
||||
rating: number
|
||||
comment?: string
|
||||
}
|
||||
|
||||
export interface Entry {
|
||||
|
@ -51,6 +50,7 @@ export interface EntriesResponseStudent {
|
|||
deskNumber: number
|
||||
removed: boolean
|
||||
entries: Record<string, Entry | null>
|
||||
score: number | null
|
||||
}
|
||||
|
||||
export interface EntriesResponse {
|
||||
|
@ -73,6 +73,15 @@ export interface EntriesPayload {
|
|||
students: EntriesPayloadStudent[]
|
||||
}
|
||||
|
||||
export interface StudentScore {
|
||||
id: number
|
||||
score: number | null
|
||||
}
|
||||
|
||||
export interface ScoresResponse {
|
||||
scores: StudentScore[]
|
||||
}
|
||||
|
||||
export class ClassroomComplianceAPIClient extends APIClient {
|
||||
constructor(authStore: AuthStoreType) {
|
||||
super(BASE_URL, authStore)
|
||||
|
@ -132,4 +141,11 @@ export class ClassroomComplianceAPIClient extends APIClient {
|
|||
saveEntries(classId: number, payload: EntriesPayload): APIResponse<void> {
|
||||
return super.postWithNoExpectedResponse(`/classes/${classId}/entries`, payload)
|
||||
}
|
||||
|
||||
getScores(classId: number, fromDate: Date, toDate: Date): APIResponse<ScoresResponse> {
|
||||
const params = new URLSearchParams()
|
||||
params.append('from', fromDate.toISOString().substring(0, 10))
|
||||
params.append('to', toDate.toISOString().substring(0, 10))
|
||||
return super.get(`/classes/${classId}/scores?${params.toString()}`)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,36 @@
|
|||
<script setup lang="ts">
|
||||
import { type Class } from '@/api/classroom_compliance'
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
defineProps<{
|
||||
cls: Class
|
||||
}>()
|
||||
const router = useRouter()
|
||||
</script>
|
||||
<template>
|
||||
<div class="class-item">
|
||||
<div class="class-item" @click="router.push(`/classroom-compliance/classes/${cls.id}`)">
|
||||
<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;
|
||||
border: 2px solid;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.class-item:hover {
|
||||
border: 2px dashed;
|
||||
}
|
||||
|
||||
.class-item+.class-item {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.class-item>h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { useAuthStore } from '@/stores/auth'
|
||||
import { onMounted, ref, useTemplateRef, type Ref } from 'vue'
|
||||
import EntriesTable from '@/apps/classroom_compliance/EntriesTable.vue'
|
||||
import { RouterLink, useRouter } from 'vue-router'
|
||||
import { useRouter } from 'vue-router'
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
import { ClassroomComplianceAPIClient, type Class } from '@/api/classroom_compliance'
|
||||
|
||||
|
@ -35,18 +35,15 @@ async function deleteThisClass() {
|
|||
<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>
|
||||
<RouterLink :to="'/classroom-compliance/classes/' + cls.id + '/edit-student'"
|
||||
>Add Student</RouterLink
|
||||
>
|
||||
<div class="button-bar" style="margin-bottom: 1em;">
|
||||
<button type="button" @click="router.push(`/classroom-compliance/classes/${cls.id}/edit-student`)">Add
|
||||
Student</button>
|
||||
<button type="button" @click="deleteThisClass">Delete this Class</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EntriesTable :classId="cls.id" />
|
||||
|
||||
<!-- Confirmation dialog used for attempts at deleting this class. -->
|
||||
<ConfirmDialog ref="deleteClassDialog">
|
||||
<p>
|
||||
Are you sure you want to delete this class? All data associated with it (settings, students,
|
||||
|
|
|
@ -3,10 +3,12 @@ import { ClassroomComplianceAPIClient, type Class } from '@/api/classroom_compli
|
|||
import { useAuthStore } from '@/stores/auth'
|
||||
import { type Ref, ref, onMounted } from 'vue'
|
||||
import ClassItem from '@/apps/classroom_compliance/ClassItem.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const classes: Ref<Class[]> = ref([])
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
const apiClient = new ClassroomComplianceAPIClient(authStore)
|
||||
|
||||
onMounted(async () => {
|
||||
|
@ -15,9 +17,11 @@ onMounted(async () => {
|
|||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<RouterLink to="/classroom-compliance/edit-class">Add Class</RouterLink>
|
||||
<div class="button-bar">
|
||||
<button type="button" @click="router.push('/classroom-compliance/edit-class')">Add Class</button>
|
||||
</div>
|
||||
<div>
|
||||
<ClassItem v-for="cls in classes" :key="cls.id" :cls="cls" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -69,7 +69,7 @@ function resetForm() {
|
|||
<label for="school-year-input">School Year (example: "2024-2025")</label>
|
||||
<input id="school-year-input" type="text" v-model="formData.schoolYear" required />
|
||||
</div>
|
||||
<div>
|
||||
<div class="button-bar">
|
||||
<button type="submit">Save</button>
|
||||
<button type="reset">Cancel</button>
|
||||
</div>
|
||||
|
|
|
@ -93,7 +93,7 @@ function resetForm() {
|
|||
<span v-if="!student">Add New Student</span>
|
||||
</h2>
|
||||
|
||||
<p>From class <span v-text="cls.number + ', ' + cls.schoolYear"></span></p>
|
||||
<p>In class <span v-text="cls.number + ', ' + cls.schoolYear"></span></p>
|
||||
|
||||
<form @submit.prevent="submitForm" @reset.prevent="resetForm">
|
||||
<div>
|
||||
|
@ -105,10 +105,10 @@ function resetForm() {
|
|||
<input id="desk-input" type="number" v-model="formData.deskNumber" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="removed-checkbox">Removed</label>
|
||||
<label for="removed-checkbox" style="display: inline">Removed</label>
|
||||
<input id="removed-checkbox" type="checkbox" v-model="formData.removed" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="button-bar">
|
||||
<button type="submit">Save</button>
|
||||
<button type="reset">Cancel</button>
|
||||
</div>
|
||||
|
|
|
@ -18,8 +18,10 @@ const props = defineProps<{
|
|||
const apiClient = new ClassroomComplianceAPIClient(authStore)
|
||||
|
||||
const students: Ref<EntriesResponseStudent[]> = ref([])
|
||||
|
||||
const lastSaveState: Ref<string | null> = ref(null)
|
||||
const lastSaveStateTimestamp: Ref<number> = ref(0)
|
||||
|
||||
const dates: Ref<string[]> = ref([])
|
||||
const toDate: Ref<Date> = ref(new Date())
|
||||
const fromDate: Ref<Date> = ref(new Date())
|
||||
|
@ -27,12 +29,12 @@ const fromDate: Ref<Date> = ref(new Date())
|
|||
const entriesChangedSinceLastSave = computed(() => {
|
||||
return lastSaveState.value === null || lastSaveState.value !== JSON.stringify(students.value)
|
||||
})
|
||||
const assignedDesks = computed(() => {
|
||||
return students.value.length > 0 && students.value.some(s => s.deskNumber > 0)
|
||||
})
|
||||
|
||||
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()
|
||||
showThisWeek()
|
||||
})
|
||||
|
||||
async function loadEntries() {
|
||||
|
@ -57,22 +59,27 @@ function shiftDateRange(days: number) {
|
|||
fromDate.value.setDate(fromDate.value.getDate() + days)
|
||||
}
|
||||
|
||||
async function showPreviousDay() {
|
||||
shiftDateRange(-1)
|
||||
async function showPreviousWeek() {
|
||||
shiftDateRange(-7)
|
||||
await loadEntries()
|
||||
}
|
||||
|
||||
async function showToday() {
|
||||
async function showThisWeek() {
|
||||
// First set the to-date to the next upcoming end-of-week (Friday).
|
||||
toDate.value = new Date()
|
||||
toDate.value.setHours(0, 0, 0, 0)
|
||||
while (toDate.value.getDay() < 5) {
|
||||
toDate.value.setDate(toDate.value.getDate() + 1)
|
||||
}
|
||||
// Then set the from-date to the Monday of that week.
|
||||
fromDate.value = new Date()
|
||||
fromDate.value.setHours(0, 0, 0, 0)
|
||||
fromDate.value.setDate(fromDate.value.getDate() - 4)
|
||||
await loadEntries()
|
||||
}
|
||||
|
||||
async function showNextDay() {
|
||||
shiftDateRange(1)
|
||||
async function showNextWeek() {
|
||||
shiftDateRange(7)
|
||||
await loadEntries()
|
||||
}
|
||||
|
||||
|
@ -135,10 +142,10 @@ function addAllEntriesForDate(dateStr: string) {
|
|||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div class="buttons-bar">
|
||||
<button type="button" @click="showPreviousDay">Previous Day</button>
|
||||
<button type="button" @click="showToday">Today</button>
|
||||
<button type="button" @click="showNextDay">Next Day</button>
|
||||
<div class="button-bar">
|
||||
<button type="button" @click="showPreviousWeek">Previous Week</button>
|
||||
<button type="button" @click="showThisWeek">This Week</button>
|
||||
<button type="button" @click="showNextWeek">Next Week</button>
|
||||
<button type="button" @click="saveEdits" :disabled="!entriesChangedSinceLastSave">
|
||||
Save
|
||||
</button>
|
||||
|
@ -146,17 +153,19 @@ function addAllEntriesForDate(dateStr: string) {
|
|||
Discard Edits
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table class="entries-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Student</th>
|
||||
<th>Desk</th>
|
||||
<th v-if="assignedDesks">Desk</th>
|
||||
<th v-for="date in dates" :key="date">
|
||||
<span>{{ getDate(date).toLocaleDateString() }}</span>
|
||||
<span @click="addAllEntriesForDate(date)">➕</span>
|
||||
<br />
|
||||
<span>{{ getWeekday(getDate(date)) }}</span>
|
||||
</th>
|
||||
<th>Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -166,9 +175,15 @@ function addAllEntriesForDate(dateStr: string) {
|
|||
<span v-text="student.name"></span>
|
||||
</RouterLink>
|
||||
</td>
|
||||
<td 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]"
|
||||
:date-str="date" :last-save-state-timestamp="lastSaveStateTimestamp" />
|
||||
<td style="text-align: right; padding-right: 0.25em;">
|
||||
<span v-if="student.score" style="font-family: monospace; font-size: large;">
|
||||
{{ (student.score * 100).toFixed(1) }}%
|
||||
</span>
|
||||
<span v-if="!student.score" style="font-size: small; font-style: italic;">No score</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -176,7 +191,7 @@ function addAllEntriesForDate(dateStr: string) {
|
|||
</template>
|
||||
<style scoped>
|
||||
.entries-table {
|
||||
margin-top: 1em;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 1em;
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -189,15 +204,6 @@ function addAllEntriesForDate(dateStr: string) {
|
|||
}
|
||||
|
||||
.student-removed {
|
||||
background-color: lightgray;
|
||||
}
|
||||
|
||||
.buttons-bar {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.buttons-bar>button+button {
|
||||
margin-left: 0.5em;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -37,6 +37,17 @@ function toggleAbsence() {
|
|||
// Populate default additional data if student is no longer absent.
|
||||
model.value.phone = { compliant: true }
|
||||
model.value.behavior = { rating: 3 }
|
||||
// If we have an initial entry known, restore data from that.
|
||||
if (initialEntryJson.value) {
|
||||
const initialEntry = JSON.parse(initialEntryJson.value) as Entry
|
||||
if (initialEntry.absent) return
|
||||
if (initialEntry.phone) {
|
||||
model.value.phone = { compliant: initialEntry.phone?.compliant }
|
||||
}
|
||||
if (initialEntry.behavior) {
|
||||
model.value.behavior = { rating: initialEntry.behavior.rating }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -73,28 +84,32 @@ function addEntry() {
|
|||
</script>
|
||||
<template>
|
||||
<td :class="{ absent: model?.absent, changed: entryChanged, 'missing-entry': !model }">
|
||||
<div v-if="model">
|
||||
<div v-if="model" class="cell-container">
|
||||
<div>
|
||||
<div class="status-item" @click="toggleAbsence">
|
||||
<span v-if="model.absent">Absent</span>
|
||||
<span v-if="!model.absent">Present</span>
|
||||
<span v-if="model.absent" title="Absent">❌</span>
|
||||
<span v-if="!model.absent" title="Present">✅</span>
|
||||
</div>
|
||||
<div class="status-item" @click="togglePhoneCompliance" v-if="!model.absent">
|
||||
<span v-if="model.phone?.compliant">📱</span>
|
||||
<span v-if="!model.phone?.compliant">📵</span>
|
||||
<span v-if="model.phone?.compliant" title="Phone Compliant">📱</span>
|
||||
<span v-if="!model.phone?.compliant" title="Phone Non-Compliant">📵</span>
|
||||
</div>
|
||||
<div class="status-item" @click="toggleBehaviorRating" v-if="!model.absent">
|
||||
<span v-if="model.behavior?.rating === 3">😇</span>
|
||||
<span v-if="model.behavior?.rating === 2">😐</span>
|
||||
<span v-if="model.behavior?.rating === 1">😡</span>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="status-item" @click="removeEntry">
|
||||
<span>🗑️</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="model === null">
|
||||
<div class="status-item" @click="addEntry">
|
||||
<span>➕</span>
|
||||
<span>+</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
@ -106,7 +121,6 @@ td {
|
|||
}
|
||||
|
||||
.missing-entry {
|
||||
background-color: lightgray;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
@ -125,6 +139,12 @@ td {
|
|||
}
|
||||
|
||||
.status-item+.status-item {
|
||||
margin-left: 0.25em;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.cell-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.25em;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -52,14 +52,12 @@ async function deleteThisStudent() {
|
|||
<li>Removed: <span v-text="student.removed"></span></li>
|
||||
<li>Desk number: <span v-text="student.deskNumber"></span></li>
|
||||
</ul>
|
||||
<RouterLink
|
||||
:to="
|
||||
'/classroom-compliance/classes/' + student.classId + '/edit-student?studentId=' + student.id
|
||||
"
|
||||
>
|
||||
Edit
|
||||
</RouterLink>
|
||||
|
||||
<div class="button-bar">
|
||||
<button type="button"
|
||||
@click="router.push(`/classroom-compliance/classes/${student.classId}/edit-student?studentId=${student.id}`)">Edit</button>
|
||||
<button type="button" @click="deleteThisStudent">Delete</button>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog ref="deleteConfirmDialog">
|
||||
<p>
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
:root {
|
||||
color-scheme: light dark;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.button-bar {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.button-bar > button + button {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
form > div + div {
|
||||
margin-top: 0.5em;
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import '@/assets/base.css'
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import HomeView from '@/views/HomeView.vue'
|
||||
import LoginView from '@/views/LoginView.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import MyAccountView from '@/views/MyAccountView.vue'
|
||||
|
||||
function enforceAuth() {
|
||||
const authStore = useAuthStore()
|
||||
if (!authStore.state) return '/login'
|
||||
return true
|
||||
}
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
|
@ -13,9 +21,15 @@ const router = createRouter({
|
|||
path: '/login',
|
||||
component: LoginView,
|
||||
},
|
||||
{
|
||||
path: '/my-account',
|
||||
component: MyAccountView,
|
||||
beforeEnter: [enforceAuth],
|
||||
},
|
||||
{
|
||||
path: '/classroom-compliance',
|
||||
component: () => import('@/apps/classroom_compliance/MainView.vue'),
|
||||
beforeEnter: [enforceAuth],
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
|
|
|
@ -6,5 +6,20 @@
|
|||
<p>
|
||||
Welcome to Teacher-Tools, a website with tools that help teachers to manager their classrooms.
|
||||
</p>
|
||||
<hr>
|
||||
<h2>Applications</h2>
|
||||
<p>
|
||||
The following list of applications are available for you:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<RouterLink to="/classroom-compliance">Classroom Compliance</RouterLink>
|
||||
-
|
||||
Track your students' phone usage and behavior patterns, and calculate weighted grades.
|
||||
</li>
|
||||
<li>
|
||||
<em>... and more to come soon!</em>
|
||||
</li>
|
||||
</ul>
|
||||
</main>
|
||||
</template>
|
||||
|
|
|
@ -34,7 +34,7 @@ async function doLogin() {
|
|||
<label for="password-input">Password</label>
|
||||
<input id="password-input" name="password" type="password" v-model="credentials.password" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="button-bar">
|
||||
<button type="button" @click="doLogin">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
<script setup lang="ts">
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
const authStore = useAuthStore()
|
||||
</script>
|
||||
<template>
|
||||
<main v-if="authStore.state">
|
||||
<h1>My Account</h1>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Internal ID</th>
|
||||
<td>{{ authStore.state.user.id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<td>{{ authStore.state.user.username }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Created At</th>
|
||||
<td>{{ new Date(authStore.state.user.createdAt).toLocaleString() }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Account Locked</th>
|
||||
<td>{{ authStore.state.user.isLocked }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Administrator</th>
|
||||
<td>{{ authStore.state.user.isAdmin }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
</template>
|
Loading…
Reference in New Issue