Added changes for new school year.
This commit is contained in:
parent
2c08f1bdbd
commit
6177943db7
|
@ -6,8 +6,9 @@ CREATE TABLE classroom_compliance_class (
|
||||||
user_id BIGINT NOT NULL
|
user_id BIGINT NOT NULL
|
||||||
REFERENCES auth_user(id)
|
REFERENCES auth_user(id)
|
||||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
score_expression VARCHAR(255) NOT NULL DEFAULT '0.5 * phone + 0.5 * behavior',
|
score_expression VARCHAR(255) NOT NULL DEFAULT '0.5 * classroom_readiness + 0.5 * behavior',
|
||||||
score_period VARCHAR(64) NOT NULL DEFAULT 'week',
|
score_period VARCHAR(64) NOT NULL DEFAULT 'week',
|
||||||
|
archived BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
CONSTRAINT unique_class_numbers_per_school_year
|
CONSTRAINT unique_class_numbers_per_school_year
|
||||||
UNIQUE(number, school_year, user_id)
|
UNIQUE(number, school_year, user_id)
|
||||||
);
|
);
|
||||||
|
@ -36,15 +37,24 @@ CREATE TABLE classroom_compliance_entry (
|
||||||
absent BOOLEAN NOT NULL DEFAULT FALSE,
|
absent BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
comment VARCHAR(2000) NOT NULL DEFAULT '',
|
comment VARCHAR(2000) NOT NULL DEFAULT '',
|
||||||
phone_compliant BOOLEAN NULL DEFAULT NULL,
|
phone_compliant BOOLEAN NULL DEFAULT NULL,
|
||||||
|
classroom_readiness BOOLEAN NULL DEFAULT NULL,
|
||||||
behavior_rating INT NULL DEFAULT NULL,
|
behavior_rating INT NULL DEFAULT NULL,
|
||||||
CONSTRAINT absence_nulls_check CHECK (
|
CONSTRAINT absence_nulls_check CHECK (
|
||||||
(absent AND phone_compliant IS NULL AND behavior_rating IS NULL) OR
|
(absent AND classroom_readiness IS NULL AND behavior_rating IS NULL) OR
|
||||||
(NOT absent AND phone_compliant IS NOT NULL AND behavior_rating IS NOT NULL)
|
(NOT absent AND classroom_readiness IS NOT NULL AND behavior_rating IS NOT NULL)
|
||||||
),
|
),
|
||||||
CONSTRAINT unique_entry_per_date
|
CONSTRAINT unique_entry_per_date
|
||||||
UNIQUE(class_id, student_id, date)
|
UNIQUE(class_id, student_id, date)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE classroom_compliance_entry_comment_checklist (
|
||||||
|
entry_id BIGINT NOT NULL
|
||||||
|
REFERENCES classroom_compliance_entry(id)
|
||||||
|
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
item VARCHAR(2000) NOT NULL,
|
||||||
|
PRIMARY KEY (entry_id, item)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE classroom_compliance_class_note (
|
CREATE TABLE classroom_compliance_class_note (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
class_id BIGINT NOT NULL
|
class_id BIGINT NOT NULL
|
||||||
|
|
|
@ -10,9 +10,9 @@ import data_utils;
|
||||||
import api_modules.auth : getAdminUserOrThrow;
|
import api_modules.auth : getAdminUserOrThrow;
|
||||||
|
|
||||||
struct Announcement {
|
struct Announcement {
|
||||||
const ulong id;
|
ulong id;
|
||||||
const string type;
|
string type;
|
||||||
const string message;
|
string message;
|
||||||
|
|
||||||
static Announcement parse(DataSetReader r) {
|
static Announcement parse(DataSetReader r) {
|
||||||
return Announcement(
|
return Announcement(
|
||||||
|
|
|
@ -13,12 +13,12 @@ import db;
|
||||||
import data_utils;
|
import data_utils;
|
||||||
|
|
||||||
struct User {
|
struct User {
|
||||||
const ulong id;
|
ulong id;
|
||||||
const string username;
|
string username;
|
||||||
const string passwordHash;
|
string passwordHash;
|
||||||
const ulong createdAt;
|
ulong createdAt;
|
||||||
const bool isLocked;
|
bool isLocked;
|
||||||
const bool isAdmin;
|
bool isAdmin;
|
||||||
|
|
||||||
static User parse(DataSetReader r) {
|
static User parse(DataSetReader r) {
|
||||||
return User(
|
return User(
|
||||||
|
|
|
@ -16,6 +16,7 @@ void registerApiEndpoints(PathHandler handler) {
|
||||||
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.GET, CLASS_PATH, &getClass);
|
||||||
handler.addMapping(Method.DELETE, CLASS_PATH, &deleteClass);
|
handler.addMapping(Method.DELETE, CLASS_PATH, &deleteClass);
|
||||||
|
handler.addMapping(Method.POST, CLASS_PATH ~ "/archived", &setArchived);
|
||||||
handler.addMapping(Method.GET, CLASS_PATH ~ "/notes", &getClassNotes);
|
handler.addMapping(Method.GET, CLASS_PATH ~ "/notes", &getClassNotes);
|
||||||
handler.addMapping(Method.POST, CLASS_PATH ~ "/notes", &createClassNote);
|
handler.addMapping(Method.POST, CLASS_PATH ~ "/notes", &createClassNote);
|
||||||
handler.addMapping(Method.DELETE, CLASS_PATH ~ "/notes/:noteId:ulong", &deleteClassNote);
|
handler.addMapping(Method.DELETE, CLASS_PATH ~ "/notes/:noteId:ulong", &deleteClassNote);
|
||||||
|
|
|
@ -48,7 +48,7 @@ void getClasses(ref HttpRequestContext ctx) {
|
||||||
User user = getUserOrThrow(ctx, conn);
|
User user = getUserOrThrow(ctx, conn);
|
||||||
const query = "
|
const query = "
|
||||||
SELECT
|
SELECT
|
||||||
c.id, c.number, c.school_year,
|
c.id, c.number, c.school_year, c.archived,
|
||||||
COUNT(DISTINCT s.id) AS student_count,
|
COUNT(DISTINCT s.id) AS student_count,
|
||||||
COUNT(DISTINCT e.id) AS entry_count,
|
COUNT(DISTINCT e.id) AS entry_count,
|
||||||
MAX(e.date) AS last_entry_date
|
MAX(e.date) AS last_entry_date
|
||||||
|
@ -63,6 +63,7 @@ void getClasses(ref HttpRequestContext ctx) {
|
||||||
ulong id;
|
ulong id;
|
||||||
ushort number;
|
ushort number;
|
||||||
string schoolYear;
|
string schoolYear;
|
||||||
|
bool archived;
|
||||||
uint studentCount;
|
uint studentCount;
|
||||||
uint entryCount;
|
uint entryCount;
|
||||||
string lastEntryDate;
|
string lastEntryDate;
|
||||||
|
@ -74,9 +75,10 @@ void getClasses(ref HttpRequestContext ctx) {
|
||||||
r.getUlong(1),
|
r.getUlong(1),
|
||||||
r.getUshort(2),
|
r.getUshort(2),
|
||||||
r.getString(3),
|
r.getString(3),
|
||||||
r.getUint(4),
|
r.getBoolean(4),
|
||||||
r.getUint(5),
|
r.getUint(5),
|
||||||
r.getDate(6).toISOExtString()
|
r.getUint(6),
|
||||||
|
r.getDate(7).toISOExtString()
|
||||||
),
|
),
|
||||||
user.id
|
user.id
|
||||||
);
|
);
|
||||||
|
@ -96,6 +98,7 @@ void deleteClass(ref HttpRequestContext ctx) {
|
||||||
scope(exit) conn.close();
|
scope(exit) conn.close();
|
||||||
User user = getUserOrThrow(ctx, conn);
|
User user = getUserOrThrow(ctx, conn);
|
||||||
auto cls = getClassOrThrow(ctx, conn, user);
|
auto cls = getClassOrThrow(ctx, conn, user);
|
||||||
|
if (cls.archived) throw new HttpStatusException(HttpStatus.FORBIDDEN, "Class is archived.");
|
||||||
const query = "DELETE FROM classroom_compliance_class WHERE id = ? AND user_id = ?";
|
const query = "DELETE FROM classroom_compliance_class WHERE id = ? AND user_id = ?";
|
||||||
PreparedStatement ps = conn.prepareStatement(query);
|
PreparedStatement ps = conn.prepareStatement(query);
|
||||||
scope(exit) ps.close();
|
scope(exit) ps.close();
|
||||||
|
@ -119,6 +122,7 @@ void createClassNote(ref HttpRequestContext ctx) {
|
||||||
scope(exit) conn.close();
|
scope(exit) conn.close();
|
||||||
User user = getUserOrThrow(ctx, conn);
|
User user = getUserOrThrow(ctx, conn);
|
||||||
auto cls = getClassOrThrow(ctx, conn, user);
|
auto cls = getClassOrThrow(ctx, conn, user);
|
||||||
|
if (cls.archived) throw new HttpStatusException(HttpStatus.FORBIDDEN, "Class is archived.");
|
||||||
struct Payload {string content;}
|
struct Payload {string content;}
|
||||||
Payload payload = readJsonPayload!(Payload)(ctx);
|
Payload payload = readJsonPayload!(Payload)(ctx);
|
||||||
const query = "INSERT INTO classroom_compliance_class_note (class_id, content) VALUES (?, ?) RETURNING id";
|
const query = "INSERT INTO classroom_compliance_class_note (class_id, content) VALUES (?, ?) RETURNING id";
|
||||||
|
@ -137,6 +141,7 @@ void deleteClassNote(ref HttpRequestContext ctx) {
|
||||||
scope(exit) conn.close();
|
scope(exit) conn.close();
|
||||||
User user = getUserOrThrow(ctx, conn);
|
User user = getUserOrThrow(ctx, conn);
|
||||||
auto cls = getClassOrThrow(ctx, conn, user);
|
auto cls = getClassOrThrow(ctx, conn, user);
|
||||||
|
if (cls.archived) throw new HttpStatusException(HttpStatus.FORBIDDEN, "Class is archived.");
|
||||||
ulong noteId = ctx.request.getPathParamAs!ulong("noteId");
|
ulong noteId = ctx.request.getPathParamAs!ulong("noteId");
|
||||||
const query = "DELETE FROM classroom_compliance_class_note WHERE class_id = ? AND id = ?";
|
const query = "DELETE FROM classroom_compliance_class_note WHERE class_id = ? AND id = ?";
|
||||||
update(conn, query, cls.id, noteId);
|
update(conn, query, cls.id, noteId);
|
||||||
|
@ -147,10 +152,20 @@ void resetStudentDesks(ref HttpRequestContext ctx) {
|
||||||
scope(exit) conn.close();
|
scope(exit) conn.close();
|
||||||
User user = getUserOrThrow(ctx, conn);
|
User user = getUserOrThrow(ctx, conn);
|
||||||
auto cls = getClassOrThrow(ctx, conn, user);
|
auto cls = getClassOrThrow(ctx, conn, user);
|
||||||
|
if (cls.archived) throw new HttpStatusException(HttpStatus.FORBIDDEN, "Class is archived.");
|
||||||
const query = "UPDATE classroom_compliance_student SET desk_number = 0 WHERE class_id = ?";
|
const query = "UPDATE classroom_compliance_student SET desk_number = 0 WHERE class_id = ?";
|
||||||
update(conn, query, cls.id);
|
update(conn, query, cls.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setArchived(ref HttpRequestContext ctx) {
|
||||||
|
Connection conn = getDb();
|
||||||
|
scope(exit) conn.close();
|
||||||
|
User user = getUserOrThrow(ctx, conn);
|
||||||
|
auto cls = getClassOrThrow(ctx, conn, user);
|
||||||
|
const query = "UPDATE classroom_compliance_class SET archived = ? WHERE id = ?";
|
||||||
|
update(conn, query, !cls.archived, cls.id);
|
||||||
|
}
|
||||||
|
|
||||||
void updateScoreParameters(ref HttpRequestContext ctx) {
|
void updateScoreParameters(ref HttpRequestContext ctx) {
|
||||||
import api_modules.classroom_compliance.score_eval : validateExpression;
|
import api_modules.classroom_compliance.score_eval : validateExpression;
|
||||||
import std.algorithm : canFind;
|
import std.algorithm : canFind;
|
||||||
|
@ -158,6 +173,7 @@ void updateScoreParameters(ref HttpRequestContext ctx) {
|
||||||
scope(exit) conn.close();
|
scope(exit) conn.close();
|
||||||
User user = getUserOrThrow(ctx, conn);
|
User user = getUserOrThrow(ctx, conn);
|
||||||
auto cls = getClassOrThrow(ctx, conn, user);
|
auto cls = getClassOrThrow(ctx, conn, user);
|
||||||
|
if (cls.archived) throw new HttpStatusException(HttpStatus.FORBIDDEN, "Class is archived.");
|
||||||
struct Payload {
|
struct Payload {
|
||||||
string scoreExpression;
|
string scoreExpression;
|
||||||
string scorePeriod;
|
string scorePeriod;
|
||||||
|
|
|
@ -22,7 +22,9 @@ struct EntriesTableEntry {
|
||||||
ulong createdAt;
|
ulong createdAt;
|
||||||
bool absent;
|
bool absent;
|
||||||
string comment;
|
string comment;
|
||||||
|
string[] checklistItems;
|
||||||
Optional!bool phoneCompliant;
|
Optional!bool phoneCompliant;
|
||||||
|
Optional!bool classroomReadiness;
|
||||||
Optional!ubyte behaviorRating;
|
Optional!ubyte behaviorRating;
|
||||||
|
|
||||||
JSONValue toJsonObj() const {
|
JSONValue toJsonObj() const {
|
||||||
|
@ -32,12 +34,21 @@ struct EntriesTableEntry {
|
||||||
obj.object["createdAt"] = JSONValue(createdAt);
|
obj.object["createdAt"] = JSONValue(createdAt);
|
||||||
obj.object["absent"] = JSONValue(absent);
|
obj.object["absent"] = JSONValue(absent);
|
||||||
obj.object["comment"] = JSONValue(comment);
|
obj.object["comment"] = JSONValue(comment);
|
||||||
|
obj.object["checklistItems"] = JSONValue.emptyArray;
|
||||||
|
foreach (ck; checklistItems) {
|
||||||
|
obj.object["checklistItems"].array ~= JSONValue(ck);
|
||||||
|
}
|
||||||
if (absent) {
|
if (absent) {
|
||||||
obj.object["phoneCompliant"] = JSONValue(null);
|
obj.object["phoneCompliant"] = JSONValue(null);
|
||||||
|
obj.object["classroomReadiness"] = JSONValue(null);
|
||||||
obj.object["behaviorRating"] = JSONValue(null);
|
obj.object["behaviorRating"] = JSONValue(null);
|
||||||
} else {
|
} else {
|
||||||
obj.object["phoneCompliant"] = JSONValue(phoneCompliant.value);
|
JSONValue pcValue = phoneCompliant.isNull ? JSONValue(null) : JSONValue(phoneCompliant.value);
|
||||||
obj.object["behaviorRating"] = JSONValue(behaviorRating.value);
|
JSONValue crValue = classroomReadiness.isNull ? JSONValue(null) : JSONValue(classroomReadiness.value);
|
||||||
|
JSONValue bValue = behaviorRating.isNull ? JSONValue(null) : JSONValue(behaviorRating.value);
|
||||||
|
obj.object["phoneCompliant"] = pcValue;
|
||||||
|
obj.object["classroomReadiness"] = crValue;
|
||||||
|
obj.object["behaviorRating"] = bValue;
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
@ -125,7 +136,14 @@ void getEntries(ref HttpRequestContext ctx) {
|
||||||
entry.created_at,
|
entry.created_at,
|
||||||
entry.absent,
|
entry.absent,
|
||||||
entry.comment,
|
entry.comment,
|
||||||
|
(
|
||||||
|
SELECT STRING_AGG(ck.item, '|||')
|
||||||
|
FROM classroom_compliance_entry_comment_checklist ck
|
||||||
|
WHERE ck.entry_id = entry.id
|
||||||
|
|
||||||
|
),
|
||||||
entry.phone_compliant,
|
entry.phone_compliant,
|
||||||
|
entry.classroom_readiness,
|
||||||
entry.behavior_rating,
|
entry.behavior_rating,
|
||||||
student.id,
|
student.id,
|
||||||
student.name,
|
student.name,
|
||||||
|
@ -155,25 +173,34 @@ void getEntries(ref HttpRequestContext ctx) {
|
||||||
const absent = r.getBoolean(4);
|
const absent = r.getBoolean(4);
|
||||||
Optional!bool phoneCompliant = absent
|
Optional!bool phoneCompliant = absent
|
||||||
? Optional!bool.empty
|
? Optional!bool.empty
|
||||||
: Optional!bool.of(r.getBoolean(6));
|
: Optional!bool.of(r.getBoolean(7));
|
||||||
|
if (r.isNull(7)) phoneCompliant.isNull = true;
|
||||||
|
Optional!bool classroomReadiness = absent
|
||||||
|
? Optional!bool.empty
|
||||||
|
: Optional!bool.of(r.getBoolean(8));
|
||||||
|
if (r.isNull(8)) classroomReadiness.isNull = true;
|
||||||
Optional!ubyte behaviorRating = absent
|
Optional!ubyte behaviorRating = absent
|
||||||
? Optional!ubyte.empty
|
? Optional!ubyte.empty
|
||||||
: Optional!ubyte.of(r.getUbyte(7));
|
: Optional!ubyte.of(r.getUbyte(9));
|
||||||
|
if (r.isNull(9)) behaviorRating.isNull = true;
|
||||||
|
import std.string : split;
|
||||||
EntriesTableEntry entryData = EntriesTableEntry(
|
EntriesTableEntry entryData = EntriesTableEntry(
|
||||||
r.getUlong(1),
|
r.getUlong(1),
|
||||||
r.getDate(2),
|
r.getDate(2),
|
||||||
r.getUlong(3),
|
r.getUlong(3),
|
||||||
r.getBoolean(4),
|
r.getBoolean(4),
|
||||||
r.getString(5),
|
r.getString(5),
|
||||||
|
r.getString(6).split("|||"),
|
||||||
phoneCompliant,
|
phoneCompliant,
|
||||||
|
classroomReadiness,
|
||||||
behaviorRating
|
behaviorRating
|
||||||
);
|
);
|
||||||
ClassroomComplianceStudent student = ClassroomComplianceStudent(
|
ClassroomComplianceStudent student = ClassroomComplianceStudent(
|
||||||
r.getUlong(8),
|
r.getUlong(10),
|
||||||
r.getString(9),
|
r.getString(11),
|
||||||
r.getUlong(12),
|
r.getUlong(14),
|
||||||
r.getUshort(10),
|
r.getUshort(12),
|
||||||
r.getBoolean(11)
|
r.getBoolean(13)
|
||||||
);
|
);
|
||||||
string dateStr = entryData.date.toISOExtString();
|
string dateStr = entryData.date.toISOExtString();
|
||||||
|
|
||||||
|
@ -252,6 +279,7 @@ void saveEntries(ref HttpRequestContext ctx) {
|
||||||
scope(exit) conn.close();
|
scope(exit) conn.close();
|
||||||
User user = getUserOrThrow(ctx, conn);
|
User user = getUserOrThrow(ctx, conn);
|
||||||
auto cls = getClassOrThrow(ctx, conn, user);
|
auto cls = getClassOrThrow(ctx, conn, user);
|
||||||
|
if (cls.archived) throw new HttpStatusException(HttpStatus.FORBIDDEN, "Class is archived.");
|
||||||
JSONValue bodyContent = ctx.request.readBodyAsJson();
|
JSONValue bodyContent = ctx.request.readBodyAsJson();
|
||||||
try {
|
try {
|
||||||
foreach (JSONValue studentObj; bodyContent.object["students"].array) {
|
foreach (JSONValue studentObj; bodyContent.object["students"].array) {
|
||||||
|
@ -265,7 +293,16 @@ void saveEntries(ref HttpRequestContext ctx) {
|
||||||
|
|
||||||
Optional!ClassroomComplianceEntry existingEntry = findOne(
|
Optional!ClassroomComplianceEntry existingEntry = findOne(
|
||||||
conn,
|
conn,
|
||||||
"SELECT * FROM classroom_compliance_entry WHERE class_id = ? AND student_id = ? AND date = ?",
|
"SELECT id, class_id, student_id, date, created_at,
|
||||||
|
absent, comment,
|
||||||
|
(
|
||||||
|
SELECT STRING_AGG(ck.item, '|||')
|
||||||
|
FROM classroom_compliance_entry_comment_checklist ck
|
||||||
|
WHERE ck.entry_id = id
|
||||||
|
),
|
||||||
|
phone_compliant, classroom_readiness, behavior_rating
|
||||||
|
FROM classroom_compliance_entry
|
||||||
|
WHERE class_id = ? AND student_id = ? AND date = ?",
|
||||||
&ClassroomComplianceEntry.parse,
|
&ClassroomComplianceEntry.parse,
|
||||||
cls.id, studentId, dateStr
|
cls.id, studentId, dateStr
|
||||||
);
|
);
|
||||||
|
@ -333,17 +370,25 @@ private void insertNewEntry(
|
||||||
bool absent = payload.object["absent"].boolean;
|
bool absent = payload.object["absent"].boolean;
|
||||||
string comment = payload.object["comment"].str;
|
string comment = payload.object["comment"].str;
|
||||||
if (comment is null) comment = "";
|
if (comment is null) comment = "";
|
||||||
Optional!bool phoneCompliant = Optional!bool.empty;
|
string[] checklistItems;
|
||||||
|
if ("checklistItems" in payload.object) {
|
||||||
|
checklistItems = payload.object["checklistItems"].array
|
||||||
|
.map!(v => v.str)
|
||||||
|
.array;
|
||||||
|
}
|
||||||
|
Optional!bool classroomReadiness = Optional!bool.empty;
|
||||||
Optional!ubyte behaviorRating = Optional!ubyte.empty;
|
Optional!ubyte behaviorRating = Optional!ubyte.empty;
|
||||||
if (!absent) {
|
if (!absent) {
|
||||||
phoneCompliant = Optional!bool.of(payload.object["phoneCompliant"].boolean);
|
classroomReadiness = Optional!bool.of(payload.object["classroomReadiness"].boolean);
|
||||||
behaviorRating = Optional!ubyte.of(cast(ubyte) payload.object["behaviorRating"].integer);
|
behaviorRating = Optional!ubyte.of(cast(ubyte) payload.object["behaviorRating"].integer);
|
||||||
}
|
}
|
||||||
|
// Do the main insert first.
|
||||||
|
import std.variant;
|
||||||
|
Variant newEntryId;
|
||||||
const query = "
|
const query = "
|
||||||
INSERT INTO classroom_compliance_entry
|
INSERT INTO classroom_compliance_entry
|
||||||
(class_id, student_id, date, absent, comment, phone_compliant, behavior_rating)
|
(class_id, student_id, date, absent, comment, classroom_readiness, behavior_rating)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)";
|
VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING id";
|
||||||
|
|
||||||
PreparedStatement ps = conn.prepareStatement(query);
|
PreparedStatement ps = conn.prepareStatement(query);
|
||||||
scope(exit) ps.close();
|
scope(exit) ps.close();
|
||||||
ps.setUlong(1, classId);
|
ps.setUlong(1, classId);
|
||||||
|
@ -355,10 +400,11 @@ private void insertNewEntry(
|
||||||
ps.setNull(6);
|
ps.setNull(6);
|
||||||
ps.setNull(7);
|
ps.setNull(7);
|
||||||
} else {
|
} else {
|
||||||
ps.setBoolean(6, phoneCompliant.value);
|
ps.setBoolean(6, classroomReadiness.value);
|
||||||
ps.setUbyte(7, behaviorRating.value);
|
ps.setUbyte(7, behaviorRating.value);
|
||||||
}
|
}
|
||||||
ps.executeUpdate();
|
ps.executeUpdate(newEntryId);
|
||||||
|
updateEntryCommentChecklistItems(conn, newEntryId.coerce!ulong, checklistItems);
|
||||||
|
|
||||||
infoF!"Created new entry for student %d: %s"(studentId, payload);
|
infoF!"Created new entry for student %d: %s"(studentId, payload);
|
||||||
}
|
}
|
||||||
|
@ -374,15 +420,21 @@ private void updateEntry(
|
||||||
bool absent = obj.object["absent"].boolean;
|
bool absent = obj.object["absent"].boolean;
|
||||||
string comment = obj.object["comment"].str;
|
string comment = obj.object["comment"].str;
|
||||||
if (comment is null) comment = "";
|
if (comment is null) comment = "";
|
||||||
Optional!bool phoneCompliant = Optional!bool.empty;
|
string[] checklistItems;
|
||||||
|
if ("checklistItems" in obj.object) {
|
||||||
|
checklistItems = obj.object["checklistItems"].array
|
||||||
|
.map!(v => v.str)
|
||||||
|
.array;
|
||||||
|
}
|
||||||
|
Optional!bool classroomReadiness = Optional!bool.empty;
|
||||||
Optional!ubyte behaviorRating = Optional!ubyte.empty;
|
Optional!ubyte behaviorRating = Optional!ubyte.empty;
|
||||||
if (!absent) {
|
if (!absent) {
|
||||||
phoneCompliant = Optional!bool.of(obj.object["phoneCompliant"].boolean);
|
classroomReadiness = Optional!bool.of(obj.object["classroomReadiness"].boolean);
|
||||||
behaviorRating = Optional!ubyte.of(cast(ubyte) obj.object["behaviorRating"].integer);
|
behaviorRating = Optional!ubyte.of(cast(ubyte) obj.object["behaviorRating"].integer);
|
||||||
}
|
}
|
||||||
const query = "
|
const query = "
|
||||||
UPDATE classroom_compliance_entry
|
UPDATE classroom_compliance_entry
|
||||||
SET absent = ?, comment = ?, phone_compliant = ?, behavior_rating = ?
|
SET absent = ?, comment = ?, classroom_readiness = ?, behavior_rating = ?
|
||||||
WHERE class_id = ? AND student_id = ? AND date = ? AND id = ?
|
WHERE class_id = ? AND student_id = ? AND date = ? AND id = ?
|
||||||
";
|
";
|
||||||
PreparedStatement ps = conn.prepareStatement(query);
|
PreparedStatement ps = conn.prepareStatement(query);
|
||||||
|
@ -393,7 +445,7 @@ private void updateEntry(
|
||||||
ps.setNull(3);
|
ps.setNull(3);
|
||||||
ps.setNull(4);
|
ps.setNull(4);
|
||||||
} else {
|
} else {
|
||||||
ps.setBoolean(3, phoneCompliant.value);
|
ps.setBoolean(3, classroomReadiness.value);
|
||||||
ps.setUbyte(4, behaviorRating.value);
|
ps.setUbyte(4, behaviorRating.value);
|
||||||
}
|
}
|
||||||
ps.setUlong(5, classId);
|
ps.setUlong(5, classId);
|
||||||
|
@ -401,6 +453,27 @@ private void updateEntry(
|
||||||
ps.setString(7, dateStr);
|
ps.setString(7, dateStr);
|
||||||
ps.setUlong(8, entryId);
|
ps.setUlong(8, entryId);
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
|
updateEntryCommentChecklistItems(conn, entryId, checklistItems);
|
||||||
|
|
||||||
infoF!"Updated entry %d"(entryId);
|
infoF!"Updated entry %d"(entryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateEntryCommentChecklistItems(Connection conn, ulong entryId, string[] items) {
|
||||||
|
PreparedStatement ps1 = conn.prepareStatement(
|
||||||
|
"DELETE FROM classroom_compliance_entry_comment_checklist WHERE entry_id = ?"
|
||||||
|
);
|
||||||
|
scope(exit) ps1.close();
|
||||||
|
ps1.setUlong(1, entryId);
|
||||||
|
ps1.executeUpdate();
|
||||||
|
const checklistItemsQuery = "
|
||||||
|
INSERT INTO classroom_compliance_entry_comment_checklist (entry_id, item)
|
||||||
|
VALUES (?, ?)
|
||||||
|
";
|
||||||
|
PreparedStatement ps2 = conn.prepareStatement(checklistItemsQuery);
|
||||||
|
scope(exit) ps2.close();
|
||||||
|
foreach (item; items) {
|
||||||
|
ps2.setUlong(1, entryId);
|
||||||
|
ps2.setString(2, item);
|
||||||
|
ps2.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ void getFullExport(ref HttpRequestContext ctx) {
|
||||||
e.created_at AS entry_created_at,
|
e.created_at AS entry_created_at,
|
||||||
e.absent AS absent,
|
e.absent AS absent,
|
||||||
e.phone_compliant AS phone_compliant,
|
e.phone_compliant AS phone_compliant,
|
||||||
|
e.classroom_readiness AS classroom_readiness,
|
||||||
e.behavior_rating AS behavior_rating,
|
e.behavior_rating AS behavior_rating,
|
||||||
e.comment AS comment
|
e.comment AS comment
|
||||||
FROM classroom_compliance_class c
|
FROM classroom_compliance_class c
|
||||||
|
@ -62,6 +63,7 @@ void getFullExport(ref HttpRequestContext ctx) {
|
||||||
CSVColumnDef("Entry Created At", (r, i) => formatCreationTimestamp(r.getUlong(i))),
|
CSVColumnDef("Entry Created At", (r, i) => formatCreationTimestamp(r.getUlong(i))),
|
||||||
CSVColumnDef("Absence", (r, i) => r.getBoolean(i) ? "Absent" : "Present"),
|
CSVColumnDef("Absence", (r, i) => r.getBoolean(i) ? "Absent" : "Present"),
|
||||||
CSVColumnDef("Phone Compliant", &formatPhoneCompliance),
|
CSVColumnDef("Phone Compliant", &formatPhoneCompliance),
|
||||||
|
CSVColumnDef("Classroom Readiness", &formatClassroomReadiness),
|
||||||
CSVColumnDef("Behavior Rating", &formatBehaviorRating),
|
CSVColumnDef("Behavior Rating", &formatBehaviorRating),
|
||||||
CSVColumnDef("Comment", &formatComment)
|
CSVColumnDef("Comment", &formatComment)
|
||||||
];
|
];
|
||||||
|
@ -95,6 +97,11 @@ private string formatPhoneCompliance(DataSetReader r, int i) {
|
||||||
return r.getBoolean(i) ? "Compliant" : "Non-compliant";
|
return r.getBoolean(i) ? "Compliant" : "Non-compliant";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string formatClassroomReadiness(DataSetReader r, int i) {
|
||||||
|
if (r.isNull(i)) return "N/A";
|
||||||
|
return r.getBoolean(i) ? "Ready" : "Not Ready";
|
||||||
|
}
|
||||||
|
|
||||||
private string formatBehaviorRating(DataSetReader r, int i) {
|
private string formatBehaviorRating(DataSetReader r, int i) {
|
||||||
if (r.isNull(i)) return "N/A";
|
if (r.isNull(i)) return "N/A";
|
||||||
ubyte score = r.getUbyte(i);
|
ubyte score = r.getUbyte(i);
|
||||||
|
|
|
@ -15,6 +15,7 @@ void createStudent(ref HttpRequestContext ctx) {
|
||||||
scope(exit) conn.close();
|
scope(exit) conn.close();
|
||||||
User user = getUserOrThrow(ctx, conn);
|
User user = getUserOrThrow(ctx, conn);
|
||||||
auto cls = getClassOrThrow(ctx, conn, user);
|
auto cls = getClassOrThrow(ctx, conn, user);
|
||||||
|
if (cls.archived) throw new HttpStatusException(HttpStatus.FORBIDDEN, "Class is archived.");
|
||||||
struct StudentPayload {
|
struct StudentPayload {
|
||||||
string name;
|
string name;
|
||||||
ushort deskNumber;
|
ushort deskNumber;
|
||||||
|
@ -83,6 +84,8 @@ void updateStudent(ref HttpRequestContext ctx) {
|
||||||
Connection conn = getDb();
|
Connection conn = getDb();
|
||||||
scope(exit) conn.close();
|
scope(exit) conn.close();
|
||||||
User user = getUserOrThrow(ctx, conn);
|
User user = getUserOrThrow(ctx, conn);
|
||||||
|
auto cls = getClassOrThrow(ctx, conn, user);
|
||||||
|
if (cls.archived) throw new HttpStatusException(HttpStatus.FORBIDDEN, "Class is archived.");
|
||||||
auto student = getStudentOrThrow(ctx, conn, user);
|
auto student = getStudentOrThrow(ctx, conn, user);
|
||||||
struct StudentUpdatePayload {
|
struct StudentUpdatePayload {
|
||||||
string name;
|
string name;
|
||||||
|
@ -139,6 +142,8 @@ void deleteStudent(ref HttpRequestContext ctx) {
|
||||||
Connection conn = getDb();
|
Connection conn = getDb();
|
||||||
scope(exit) conn.close();
|
scope(exit) conn.close();
|
||||||
User user = getUserOrThrow(ctx, conn);
|
User user = getUserOrThrow(ctx, conn);
|
||||||
|
auto cls = getClassOrThrow(ctx, conn, user);
|
||||||
|
if (cls.archived) throw new HttpStatusException(HttpStatus.FORBIDDEN, "Class is archived.");
|
||||||
auto student = getStudentOrThrow(ctx, conn, user);
|
auto student = getStudentOrThrow(ctx, conn, user);
|
||||||
update(
|
update(
|
||||||
conn,
|
conn,
|
||||||
|
@ -154,7 +159,18 @@ void getStudentEntries(ref HttpRequestContext ctx) {
|
||||||
auto student = getStudentOrThrow(ctx, conn, user);
|
auto student = getStudentOrThrow(ctx, conn, user);
|
||||||
auto entries = findAll(
|
auto entries = findAll(
|
||||||
conn,
|
conn,
|
||||||
"SELECT * FROM classroom_compliance_entry WHERE student_id = ? ORDER BY date DESC",
|
"SELECT id, class_id, student_id, date, created_at,
|
||||||
|
absent, comment,
|
||||||
|
(
|
||||||
|
SELECT STRING_AGG(ck.item, '|||')
|
||||||
|
FROM classroom_compliance_entry_comment_checklist ck
|
||||||
|
WHERE ck.entry_id = id
|
||||||
|
),
|
||||||
|
phone_compliant, classroom_readiness, behavior_rating
|
||||||
|
FROM classroom_compliance_entry
|
||||||
|
WHERE student_id = ?
|
||||||
|
GROUP BY classroom_compliance_entry.id
|
||||||
|
ORDER BY date DESC",
|
||||||
&ClassroomComplianceEntry.parse,
|
&ClassroomComplianceEntry.parse,
|
||||||
student.id
|
student.id
|
||||||
);
|
);
|
||||||
|
@ -221,6 +237,8 @@ void moveStudentToOtherClass(ref HttpRequestContext ctx) {
|
||||||
scope(exit) conn.close();
|
scope(exit) conn.close();
|
||||||
conn.setAutoCommit(false);
|
conn.setAutoCommit(false);
|
||||||
User user = getUserOrThrow(ctx, conn);
|
User user = getUserOrThrow(ctx, conn);
|
||||||
|
auto cls = getClassOrThrow(ctx, conn, user);
|
||||||
|
if (cls.archived) throw new HttpStatusException(HttpStatus.FORBIDDEN, "Class is archived.");
|
||||||
auto student = getStudentOrThrow(ctx, conn, user);
|
auto student = getStudentOrThrow(ctx, conn, user);
|
||||||
struct Payload {
|
struct Payload {
|
||||||
ulong classId;
|
ulong classId;
|
||||||
|
|
|
@ -4,20 +4,23 @@ import ddbc : DataSetReader;
|
||||||
import handy_httpd.components.optional;
|
import handy_httpd.components.optional;
|
||||||
import std.json;
|
import std.json;
|
||||||
import std.datetime;
|
import std.datetime;
|
||||||
|
import std.string : split;
|
||||||
|
|
||||||
struct ClassroomComplianceClass {
|
struct ClassroomComplianceClass {
|
||||||
const ulong id;
|
ulong id;
|
||||||
const ushort number;
|
ushort number;
|
||||||
const string schoolYear;
|
string schoolYear;
|
||||||
const ulong userId;
|
bool archived;
|
||||||
const string scoreExpression;
|
ulong userId;
|
||||||
const string scorePeriod;
|
string scoreExpression;
|
||||||
|
string scorePeriod;
|
||||||
|
|
||||||
static ClassroomComplianceClass parse(DataSetReader r) {
|
static ClassroomComplianceClass parse(DataSetReader r) {
|
||||||
return ClassroomComplianceClass(
|
return ClassroomComplianceClass(
|
||||||
r.getUlong(1),
|
r.getUlong(1),
|
||||||
r.getUshort(2),
|
r.getUshort(2),
|
||||||
r.getString(3),
|
r.getString(3),
|
||||||
|
r.getBoolean(7),
|
||||||
r.getUlong(4),
|
r.getUlong(4),
|
||||||
r.getString(5),
|
r.getString(5),
|
||||||
r.getString(6)
|
r.getString(6)
|
||||||
|
@ -26,11 +29,11 @@ struct ClassroomComplianceClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ClassroomComplianceStudent {
|
struct ClassroomComplianceStudent {
|
||||||
const ulong id;
|
ulong id;
|
||||||
const string name;
|
string name;
|
||||||
const ulong classId;
|
ulong classId;
|
||||||
const ushort deskNumber;
|
ushort deskNumber;
|
||||||
const bool removed;
|
bool removed;
|
||||||
|
|
||||||
static ClassroomComplianceStudent parse(DataSetReader r) {
|
static ClassroomComplianceStudent parse(DataSetReader r) {
|
||||||
return ClassroomComplianceStudent(
|
return ClassroomComplianceStudent(
|
||||||
|
@ -44,35 +47,32 @@ struct ClassroomComplianceStudent {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ClassroomComplianceEntry {
|
struct ClassroomComplianceEntry {
|
||||||
const ulong id;
|
ulong id;
|
||||||
const ulong classId;
|
ulong classId;
|
||||||
const ulong studentId;
|
ulong studentId;
|
||||||
const Date date;
|
Date date;
|
||||||
const ulong createdAt;
|
ulong createdAt;
|
||||||
const bool absent;
|
bool absent;
|
||||||
const string comment;
|
string comment;
|
||||||
const Optional!bool phoneCompliant;
|
string[] checklistItems;
|
||||||
const Optional!ubyte behaviorRating;
|
Optional!bool phoneCompliant;
|
||||||
|
Optional!bool classroomReadiness;
|
||||||
|
Optional!ubyte behaviorRating;
|
||||||
|
|
||||||
static ClassroomComplianceEntry parse(DataSetReader r) {
|
static ClassroomComplianceEntry parse(DataSetReader r) {
|
||||||
Optional!bool phone = !r.isNull(8)
|
ClassroomComplianceEntry entry;
|
||||||
? Optional!bool.of(r.getBoolean(8))
|
entry.id = r.getUlong(1);
|
||||||
: Optional!bool.empty;
|
entry.classId = r.getUlong(2);
|
||||||
Optional!ubyte behavior = !r.isNull(9)
|
entry.studentId = r.getUlong(3);
|
||||||
? Optional!ubyte.of(r.getUbyte(9))
|
entry.date = r.getDate(4);
|
||||||
: Optional!ubyte.empty;
|
entry.createdAt = r.getUlong(5);
|
||||||
|
entry.absent = r.getBoolean(6);
|
||||||
return ClassroomComplianceEntry(
|
entry.comment = r.getString(7);
|
||||||
r.getUlong(1),
|
entry.checklistItems = r.getString(8).split("|||");
|
||||||
r.getUlong(2),
|
entry.phoneCompliant = r.isNull(9) ? Optional!bool.empty : Optional!bool.of(r.getBoolean(9));
|
||||||
r.getUlong(3),
|
entry.classroomReadiness = r.isNull(10) ? Optional!bool.empty : Optional!bool.of(r.getBoolean(10));
|
||||||
r.getDate(4),
|
entry.behaviorRating = r.isNull(11) ? Optional!ubyte.empty : Optional!ubyte.of(r.getUbyte(11));
|
||||||
r.getUlong(5),
|
return entry;
|
||||||
r.getBoolean(6),
|
|
||||||
r.getString(7),
|
|
||||||
phone,
|
|
||||||
behavior
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
JSONValue toJsonObj() const {
|
JSONValue toJsonObj() const {
|
||||||
|
@ -84,28 +84,22 @@ struct ClassroomComplianceEntry {
|
||||||
obj.object["createdAt"] = JSONValue(createdAt);
|
obj.object["createdAt"] = JSONValue(createdAt);
|
||||||
obj.object["absent"] = JSONValue(absent);
|
obj.object["absent"] = JSONValue(absent);
|
||||||
obj.object["comment"] = JSONValue(comment);
|
obj.object["comment"] = JSONValue(comment);
|
||||||
if (absent) {
|
obj.object["checklistItems"] = JSONValue.emptyArray;
|
||||||
if (!phoneCompliant.isNull || !behaviorRating.isNull) {
|
foreach (item; checklistItems) {
|
||||||
throw new Exception("Illegal entry state! Absent is true while values are not null!");
|
obj.object["checklistItems"].array ~= JSONValue(item);
|
||||||
}
|
|
||||||
obj.object["phoneCompliant"] = JSONValue(null);
|
|
||||||
obj.object["behaviorRating"] = JSONValue(null);
|
|
||||||
} else {
|
|
||||||
if (phoneCompliant.isNull || behaviorRating.isNull) {
|
|
||||||
throw new Exception("Illegal entry state! Absent is false while values are null!");
|
|
||||||
}
|
|
||||||
obj.object["phoneCompliant"] = JSONValue(phoneCompliant.value);
|
|
||||||
obj.object["behaviorRating"] = JSONValue(behaviorRating.value);
|
|
||||||
}
|
}
|
||||||
|
obj.object["phoneCompliant"] = phoneCompliant.isNull ? JSONValue(null) : JSONValue(phoneCompliant.value);
|
||||||
|
obj.object["classroomReadiness"] = classroomReadiness.isNull ? JSONValue(null) : JSONValue(classroomReadiness.value);
|
||||||
|
obj.object["behaviorRating"] = behaviorRating.isNull ? JSONValue(null) : JSONValue(behaviorRating.value);
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ClassroomComplianceClassNote {
|
struct ClassroomComplianceClassNote {
|
||||||
const ulong id;
|
ulong id;
|
||||||
const ulong classId;
|
ulong classId;
|
||||||
const ulong createdAt;
|
ulong createdAt;
|
||||||
const string content;
|
string content;
|
||||||
|
|
||||||
static ClassroomComplianceClassNote parse(DataSetReader r) {
|
static ClassroomComplianceClassNote parse(DataSetReader r) {
|
||||||
return ClassroomComplianceClassNote(
|
return ClassroomComplianceClassNote(
|
||||||
|
|
|
@ -15,7 +15,7 @@ enum ScorePeriod : string {
|
||||||
|
|
||||||
private struct ScoringParameters {
|
private struct ScoringParameters {
|
||||||
Expr expr;
|
Expr expr;
|
||||||
const DateRange dateRange;
|
DateRange dateRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,6 +54,7 @@ Optional!double[ulong] getScores(
|
||||||
COUNT(id) AS entry_count,
|
COUNT(id) AS entry_count,
|
||||||
SUM(CASE WHEN absent = TRUE THEN 1 ELSE 0 END) AS absence_count,
|
SUM(CASE WHEN absent = TRUE THEN 1 ELSE 0 END) AS absence_count,
|
||||||
SUM(CASE WHEN phone_compliant = FALSE THEN 1 ELSE 0 END) AS phone_noncompliance_count,
|
SUM(CASE WHEN phone_compliant = FALSE THEN 1 ELSE 0 END) AS phone_noncompliance_count,
|
||||||
|
SUM(CASE WHEN classroom_readiness = FALSE THEN 1 ELSE 0 END) AS not_classroom_ready_count,
|
||||||
SUM(CASE WHEN behavior_rating = 3 THEN 1 ELSE 0 END) AS behavior_good,
|
SUM(CASE WHEN behavior_rating = 3 THEN 1 ELSE 0 END) AS behavior_good,
|
||||||
SUM(CASE WHEN behavior_rating = 2 THEN 1 ELSE 0 END) AS behavior_mediocre,
|
SUM(CASE WHEN behavior_rating = 2 THEN 1 ELSE 0 END) AS behavior_mediocre,
|
||||||
SUM(CASE WHEN behavior_rating = 1 THEN 1 ELSE 0 END) AS behavior_poor
|
SUM(CASE WHEN behavior_rating = 1 THEN 1 ELSE 0 END) AS behavior_poor
|
||||||
|
@ -74,14 +75,16 @@ Optional!double[ulong] getScores(
|
||||||
uint entryCount = r.getUint(2);
|
uint entryCount = r.getUint(2);
|
||||||
uint absenceCount = r.getUint(3);
|
uint absenceCount = r.getUint(3);
|
||||||
uint phoneNonComplianceCount = r.getUint(4);
|
uint phoneNonComplianceCount = r.getUint(4);
|
||||||
uint behaviorGoodCount = r.getUint(5);
|
uint notClassroomReadyCount = r.getUint(5);
|
||||||
uint behaviorMediocreCount = r.getUint(6);
|
uint behaviorGoodCount = r.getUint(6);
|
||||||
uint behaviorPoorCount = r.getUint(7);
|
uint behaviorMediocreCount = r.getUint(7);
|
||||||
|
uint behaviorPoorCount = r.getUint(8);
|
||||||
scores[studentId] = calculateScore(
|
scores[studentId] = calculateScore(
|
||||||
params.expr,
|
params.expr,
|
||||||
entryCount,
|
entryCount,
|
||||||
absenceCount,
|
absenceCount,
|
||||||
phoneNonComplianceCount,
|
phoneNonComplianceCount,
|
||||||
|
notClassroomReadyCount,
|
||||||
behaviorGoodCount,
|
behaviorGoodCount,
|
||||||
behaviorMediocreCount,
|
behaviorMediocreCount,
|
||||||
behaviorPoorCount
|
behaviorPoorCount
|
||||||
|
@ -132,6 +135,7 @@ private Optional!double calculateScore(
|
||||||
uint entryCount,
|
uint entryCount,
|
||||||
uint absenceCount,
|
uint absenceCount,
|
||||||
uint phoneNonComplianceCount,
|
uint phoneNonComplianceCount,
|
||||||
|
uint notClassroomReadyCount,
|
||||||
uint behaviorGoodCount,
|
uint behaviorGoodCount,
|
||||||
uint behaviorMediocreCount,
|
uint behaviorMediocreCount,
|
||||||
uint behaviorPoorCount
|
uint behaviorPoorCount
|
||||||
|
@ -151,6 +155,14 @@ private Optional!double calculateScore(
|
||||||
phoneCompliantCount = presentCount - phoneNonComplianceCount;
|
phoneCompliantCount = presentCount - phoneNonComplianceCount;
|
||||||
}
|
}
|
||||||
double phoneScore = phoneCompliantCount / cast(double) presentCount;
|
double phoneScore = phoneCompliantCount / cast(double) presentCount;
|
||||||
|
// Classroom readiness score:
|
||||||
|
uint classroomReadyCount;
|
||||||
|
if (presentCount < notClassroomReadyCount) {
|
||||||
|
classroomReadyCount = 0;
|
||||||
|
} else {
|
||||||
|
classroomReadyCount = presentCount - notClassroomReadyCount;
|
||||||
|
}
|
||||||
|
double classroomReadinessScore = classroomReadyCount / cast(double) presentCount;
|
||||||
|
|
||||||
double behaviorGoodScore = behaviorGoodCount / cast(double) presentCount;
|
double behaviorGoodScore = behaviorGoodCount / cast(double) presentCount;
|
||||||
double behaviorMediocreScore = behaviorMediocreCount / cast(double) presentCount;
|
double behaviorMediocreScore = behaviorMediocreCount / cast(double) presentCount;
|
||||||
|
@ -159,6 +171,7 @@ private Optional!double calculateScore(
|
||||||
|
|
||||||
return Optional!double.of(scoreExpression.eval([
|
return Optional!double.of(scoreExpression.eval([
|
||||||
"phone": phoneScore,
|
"phone": phoneScore,
|
||||||
|
"classroom_readiness": classroomReadinessScore,
|
||||||
"behavior": typicalBehaviorScore,
|
"behavior": typicalBehaviorScore,
|
||||||
"behavior_good": behaviorGoodScore,
|
"behavior_good": behaviorGoodScore,
|
||||||
"behavior_mediocre": behaviorMediocreScore,
|
"behavior_mediocre": behaviorMediocreScore,
|
||||||
|
|
|
@ -35,7 +35,8 @@ void initializeSchema() {
|
||||||
scope(exit) stmt.close();
|
scope(exit) stmt.close();
|
||||||
const string AUTH_SCHEMA = import("schema/auth.sql");
|
const string AUTH_SCHEMA = import("schema/auth.sql");
|
||||||
const string CLASSROOM_COMPLIANCE_SCHEMA = import("schema/classroom_compliance.sql");
|
const string CLASSROOM_COMPLIANCE_SCHEMA = import("schema/classroom_compliance.sql");
|
||||||
const schemas = [AUTH_SCHEMA, CLASSROOM_COMPLIANCE_SCHEMA];
|
const string ANNOUNCEMENT_SCHEMA = import("schema/announcement.sql");
|
||||||
|
const schemas = [AUTH_SCHEMA, CLASSROOM_COMPLIANCE_SCHEMA, ANNOUNCEMENT_SCHEMA];
|
||||||
uint schemaNumber = 1;
|
uint schemaNumber = 1;
|
||||||
foreach (schema; schemas) {
|
foreach (schema; schemas) {
|
||||||
infoF!"Intializing schema #%d."(schemaNumber++);
|
infoF!"Intializing schema #%d."(schemaNumber++);
|
||||||
|
|
|
@ -137,8 +137,8 @@ void addEntry(
|
||||||
) {
|
) {
|
||||||
const entryQuery = "
|
const entryQuery = "
|
||||||
INSERT INTO classroom_compliance_entry
|
INSERT INTO classroom_compliance_entry
|
||||||
(class_id, student_id, date, absent, comment, phone_compliant, behavior_rating)
|
(class_id, student_id, date, absent, comment, phone_compliant, classroom_readiness, behavior_rating)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)";
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
||||||
PreparedStatement ps = conn.prepareStatement(entryQuery);
|
PreparedStatement ps = conn.prepareStatement(entryQuery);
|
||||||
scope(exit) ps.close();
|
scope(exit) ps.close();
|
||||||
ps.setUlong(1, classId);
|
ps.setUlong(1, classId);
|
||||||
|
@ -150,12 +150,13 @@ void addEntry(
|
||||||
} else {
|
} else {
|
||||||
ps.setString(5, comment);
|
ps.setString(5, comment);
|
||||||
}
|
}
|
||||||
|
ps.setNull(6); // Always set phone_compliant as null from now on.
|
||||||
if (absent) {
|
if (absent) {
|
||||||
ps.setNull(6);
|
|
||||||
ps.setNull(7);
|
ps.setNull(7);
|
||||||
|
ps.setNull(8);
|
||||||
} else {
|
} else {
|
||||||
ps.setBoolean(6, phoneCompliant);
|
ps.setBoolean(7, true);
|
||||||
ps.setUint(7, behaviorRating);
|
ps.setUint(8, behaviorRating);
|
||||||
}
|
}
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
}
|
}
|
||||||
|
@ -167,6 +168,7 @@ void deleteAllData(Connection conn) {
|
||||||
const tables = [
|
const tables = [
|
||||||
"announcement",
|
"announcement",
|
||||||
"classroom_compliance_class_note",
|
"classroom_compliance_class_note",
|
||||||
|
"classroom_compliance_entry_comment_checklist",
|
||||||
"classroom_compliance_entry",
|
"classroom_compliance_entry",
|
||||||
"classroom_compliance_student",
|
"classroom_compliance_student",
|
||||||
"classroom_compliance_class",
|
"classroom_compliance_class",
|
||||||
|
|
|
@ -4,6 +4,8 @@ export const BASE_URL = import.meta.env.VITE_API_URL + '/classroom-compliance'
|
||||||
|
|
||||||
export const EMOJI_PHONE_COMPLIANT = '📱'
|
export const EMOJI_PHONE_COMPLIANT = '📱'
|
||||||
export const EMOJI_PHONE_NONCOMPLIANT = '📵'
|
export const EMOJI_PHONE_NONCOMPLIANT = '📵'
|
||||||
|
export const EMOJI_CLASSROOM_READY = '🍎'
|
||||||
|
export const EMOJI_NOT_CLASSROOM_READY = '🐛'
|
||||||
export const EMOJI_PRESENT = '✅'
|
export const EMOJI_PRESENT = '✅'
|
||||||
export const EMOJI_ABSENT = '❌'
|
export const EMOJI_ABSENT = '❌'
|
||||||
export const EMOJI_BEHAVIOR_GOOD = '😇'
|
export const EMOJI_BEHAVIOR_GOOD = '😇'
|
||||||
|
@ -16,6 +18,7 @@ export interface Class {
|
||||||
schoolYear: string
|
schoolYear: string
|
||||||
scoreExpression: string
|
scoreExpression: string
|
||||||
scorePeriod: string
|
scorePeriod: string
|
||||||
|
archived: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClassesResponseClass {
|
export interface ClassesResponseClass {
|
||||||
|
@ -25,6 +28,7 @@ export interface ClassesResponseClass {
|
||||||
studentCount: number
|
studentCount: number
|
||||||
entryCount: number
|
entryCount: number
|
||||||
lastEntryDate: string
|
lastEntryDate: string
|
||||||
|
archived: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Student {
|
export interface Student {
|
||||||
|
@ -41,8 +45,10 @@ export interface Entry {
|
||||||
createdAt: number
|
createdAt: number
|
||||||
absent: boolean
|
absent: boolean
|
||||||
phoneCompliant: boolean | null
|
phoneCompliant: boolean | null
|
||||||
|
classroomReadiness: boolean | null
|
||||||
behaviorRating: number | null
|
behaviorRating: number | null
|
||||||
comment: string
|
comment: string
|
||||||
|
checklistItems: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaultEntry(dateStr: string): Entry {
|
export function getDefaultEntry(dateStr: string): Entry {
|
||||||
|
@ -51,9 +57,11 @@ export function getDefaultEntry(dateStr: string): Entry {
|
||||||
date: dateStr,
|
date: dateStr,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
absent: false,
|
absent: false,
|
||||||
phoneCompliant: true,
|
phoneCompliant: null,
|
||||||
|
classroomReadiness: true,
|
||||||
behaviorRating: 3,
|
behaviorRating: 3,
|
||||||
comment: '',
|
comment: '',
|
||||||
|
checklistItems: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,6 +160,10 @@ export class ClassroomComplianceAPIClient extends APIClient {
|
||||||
return new APIResponse(this.handleAPIResponseWithNoBody(promise))
|
return new APIResponse(this.handleAPIResponseWithNoBody(promise))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleArchived(classId: number): APIResponse<void> {
|
||||||
|
return super.postWithNoExpectedResponse(`/classes/${classId}/archived`, {})
|
||||||
|
}
|
||||||
|
|
||||||
updateScoreParameters(
|
updateScoreParameters(
|
||||||
classId: number,
|
classId: number,
|
||||||
scoreExpression: string,
|
scoreExpression: string,
|
||||||
|
|
|
@ -9,7 +9,9 @@ const router = useRouter()
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="class-item" @click="router.push(`/classroom-compliance/classes/${cls.id}`)">
|
<div class="class-item" @click="router.push(`/classroom-compliance/classes/${cls.id}`)">
|
||||||
<h3>Class <span v-text="cls.number"></span></h3>
|
<h3>Class <span v-text="cls.number"></span> <span v-text="cls.schoolYear"
|
||||||
|
style="font-weight: lighter; font-size: small;"></span>
|
||||||
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
{{ cls.studentCount }} students,
|
{{ cls.studentCount }} students,
|
||||||
{{ cls.entryCount }} entries.
|
{{ cls.entryCount }} entries.
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { useAuthStore } from '@/stores/auth';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
note: ClassNote
|
note: ClassNote
|
||||||
|
disabled?: boolean
|
||||||
}>()
|
}>()
|
||||||
const emit = defineEmits(['noteDeleted'])
|
const emit = defineEmits(['noteDeleted'])
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
@ -21,7 +22,7 @@ async function deleteNote() {
|
||||||
<p class="class-note-item-content">{{ note.content }}</p>
|
<p class="class-note-item-content">{{ note.content }}</p>
|
||||||
<div class="class-note-item-attributes-container">
|
<div class="class-note-item-attributes-container">
|
||||||
<p class="class-note-item-timestamp">{{ new Date(note.createdAt).toLocaleString() }}</p>
|
<p class="class-note-item-timestamp">{{ new Date(note.createdAt).toLocaleString() }}</p>
|
||||||
<button class="class-note-item-delete-button" @click="deleteNote()">Delete</button>
|
<button class="class-note-item-delete-button" @click="deleteNote()" :disabled="disabled">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -17,6 +17,7 @@ const notes: Ref<ClassNote[]> = ref([])
|
||||||
const noteContent: Ref<string> = ref('')
|
const noteContent: Ref<string> = ref('')
|
||||||
const scoreExpression: Ref<string> = ref('')
|
const scoreExpression: Ref<string> = ref('')
|
||||||
const scorePeriod: Ref<string> = ref('')
|
const scorePeriod: Ref<string> = ref('')
|
||||||
|
const archived = computed(() => cls.value !== null && cls.value.archived)
|
||||||
const canUpdateScoreParameters = computed(() => {
|
const canUpdateScoreParameters = computed(() => {
|
||||||
return cls.value && (
|
return cls.value && (
|
||||||
cls.value.scoreExpression !== scoreExpression.value ||
|
cls.value.scoreExpression !== scoreExpression.value ||
|
||||||
|
@ -53,6 +54,12 @@ async function deleteThisClass() {
|
||||||
await router.replace('/classroom-compliance')
|
await router.replace('/classroom-compliance')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleArchived() {
|
||||||
|
if (!cls.value) return
|
||||||
|
await apiClient.toggleArchived(cls.value.id).handleErrorsWithAlertNoBody()
|
||||||
|
loadClass()
|
||||||
|
}
|
||||||
|
|
||||||
async function submitNote() {
|
async function submitNote() {
|
||||||
if (noteContent.value.trim().length < 1 || !cls.value) return
|
if (noteContent.value.trim().length < 1 || !cls.value) return
|
||||||
const note = await apiClient.createClassNote(cls.value?.id, noteContent.value).handleErrorsWithAlert()
|
const note = await apiClient.createClassNote(cls.value?.id, noteContent.value).handleErrorsWithAlert()
|
||||||
|
@ -113,22 +120,28 @@ function resetScoreParameters() {
|
||||||
<h1 class="align-center" style="margin-bottom: 0;">Class <span v-text="cls.number"></span></h1>
|
<h1 class="align-center" style="margin-bottom: 0;">Class <span v-text="cls.number"></span></h1>
|
||||||
<p class="align-center" style="margin-top: 0; margin-bottom: 2em;">For the {{ cls.schoolYear }} school year.</p>
|
<p class="align-center" style="margin-top: 0; margin-bottom: 2em;">For the {{ cls.schoolYear }} school year.</p>
|
||||||
<div class="button-bar align-center" style="margin-bottom: 1em;">
|
<div class="button-bar align-center" 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`)"
|
||||||
|
:disabled="archived">Add
|
||||||
Student</button>
|
Student</button>
|
||||||
<button type="button" @click="router.push(`/classroom-compliance/classes/${cls.id}/import-students`)">Import
|
<button type="button" @click="router.push(`/classroom-compliance/classes/${cls.id}/import-students`)"
|
||||||
|
:disabled="archived">Import
|
||||||
Students</button>
|
Students</button>
|
||||||
<button type="button" @click="resetStudentDesks">Clear Assigned Desks</button>
|
<button type="button" @click="resetStudentDesks" :disabled="archived">Clear Assigned Desks</button>
|
||||||
<button type="button" @click="deleteThisClass">Delete this Class</button>
|
<button type="button" @click="deleteThisClass" :disabled="archived">Delete this Class</button>
|
||||||
|
<button type="button" @click="toggleArchived">
|
||||||
|
<span v-if="archived">Unarchive</span>
|
||||||
|
<span v-if="!archived">Archive</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EntriesTable :classId="cls.id" ref="entries-table" />
|
<EntriesTable :classId="cls.id" :disabled="archived" ref="entries-table" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 style="margin-bottom: 0.25em;">Notes</h3>
|
<h3 style="margin-bottom: 0.25em;">Notes</h3>
|
||||||
<form @submit.prevent="submitNote">
|
<form @submit.prevent="submitNote">
|
||||||
<textarea style="min-height: 50px; min-width: 300px;" maxlength="2000" minlength="1"
|
<textarea style="min-height: 50px; min-width: 300px;" maxlength="2000" minlength="1" v-model="noteContent"
|
||||||
v-model="noteContent"></textarea>
|
:readonly="archived"></textarea>
|
||||||
<button style="vertical-align: top; margin-left: 0.5em;" type="submit">Add Note</button>
|
<button style="vertical-align: top; margin-left: 0.5em;" type="submit" :disabled="archived">Add Note</button>
|
||||||
</form>
|
</form>
|
||||||
<ClassNoteItem v-for="note in notes" :key="note.id" :note="note" @noteDeleted="refreshNotes()" />
|
<ClassNoteItem v-for="note in notes" :key="note.id" :note="note" @noteDeleted="refreshNotes()" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -142,7 +155,7 @@ function resetScoreParameters() {
|
||||||
<div>
|
<div>
|
||||||
<label for="score-expression-input">Expression</label>
|
<label for="score-expression-input">Expression</label>
|
||||||
<textarea id="score-expression-input" v-model="scoreExpression" class="text-mono" minlength="1"
|
<textarea id="score-expression-input" v-model="scoreExpression" class="text-mono" minlength="1"
|
||||||
maxlength="255" style="min-width: 500px; min-height: 50px;"></textarea>
|
maxlength="255" style="min-width: 500px; min-height: 50px;" :readonly="archived"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<p class="form-input-hint">
|
<p class="form-input-hint">
|
||||||
The expression to use to calculate each student's score. This should be a simple mathematical expression that
|
The expression to use to calculate each student's score. This should be a simple mathematical expression that
|
||||||
|
@ -169,7 +182,7 @@ function resetScoreParameters() {
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<label for="score-period-select">Period</label>
|
<label for="score-period-select">Period</label>
|
||||||
<select v-model="scorePeriod">
|
<select v-model="scorePeriod" :disabled="archived">
|
||||||
<option value="week">Weekly</option>
|
<option value="week">Weekly</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
@ -177,8 +190,9 @@ function resetScoreParameters() {
|
||||||
The period over which to calculate scores.
|
The period over which to calculate scores.
|
||||||
</p>
|
</p>
|
||||||
<div class="button-bar">
|
<div class="button-bar">
|
||||||
<button type="submit" :disabled="!canUpdateScoreParameters">Save</button>
|
<button type="submit" :disabled="!canUpdateScoreParameters || archived">Save</button>
|
||||||
<button type="button" :disabled="!canUpdateScoreParameters" @click="resetScoreParameters">Reset</button>
|
<button type="button" :disabled="!canUpdateScoreParameters || archived"
|
||||||
|
@click="resetScoreParameters">Reset</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ClassroomComplianceAPIClient, type ClassesResponseClass } from '@/api/classroom_compliance'
|
import { ClassroomComplianceAPIClient, type ClassesResponseClass } from '@/api/classroom_compliance'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { type Ref, ref, onMounted } from 'vue'
|
import { type Ref, ref, onMounted, computed } from 'vue'
|
||||||
import ClassItem from '@/apps/classroom_compliance/ClassItem.vue'
|
import ClassItem from '@/apps/classroom_compliance/ClassItem.vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const classes: Ref<ClassesResponseClass[]> = ref([])
|
const classes: Ref<ClassesResponseClass[]> = ref([])
|
||||||
|
const archivedClasses = computed(() => classes.value.filter(c => c.archived))
|
||||||
|
const activeClasses = computed(() => classes.value.filter(c => !c.archived))
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -23,7 +25,11 @@ onMounted(() => {
|
||||||
<button type="button" @click="router.push('/classroom-compliance/edit-class')">Add Class</button>
|
<button type="button" @click="router.push('/classroom-compliance/edit-class')">Add Class</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<ClassItem v-for="cls in classes" :key="cls.id" :cls="cls" />
|
<ClassItem v-for="cls in activeClasses" :key="cls.id" :cls="cls" />
|
||||||
|
</div>
|
||||||
|
<div v-if="archivedClasses.length > 0">
|
||||||
|
<h3 style="text-align: center;">Archived Classes</h3>
|
||||||
|
<ClassItem v-for="cls in archivedClasses" :key="cls.id" :cls="cls" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -16,7 +16,8 @@ import StudentNameCell from '@/apps/classroom_compliance/entries_table/StudentNa
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
classId: number
|
classId: number,
|
||||||
|
disabled: boolean
|
||||||
}>()
|
}>()
|
||||||
const apiClient = new ClassroomComplianceAPIClient(authStore)
|
const apiClient = new ClassroomComplianceAPIClient(authStore)
|
||||||
|
|
||||||
|
@ -160,6 +161,11 @@ async function showNextWeek() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveEdits() {
|
async function saveEdits() {
|
||||||
|
if (props.disabled) {
|
||||||
|
console.warn('Cannot save edits when disabled.')
|
||||||
|
await loadEntries()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!lastSaveState.value) {
|
if (!lastSaveState.value) {
|
||||||
console.warn('No lastSaveState, cannot determine what edits were made.')
|
console.warn('No lastSaveState, cannot determine what edits were made.')
|
||||||
await loadEntries()
|
await loadEntries()
|
||||||
|
@ -237,6 +243,7 @@ function getVisibleStudentEntries(student: EntriesResponseStudent): Record<strin
|
||||||
}
|
}
|
||||||
|
|
||||||
function addAllEntriesForDate(dateStr: string) {
|
function addAllEntriesForDate(dateStr: string) {
|
||||||
|
if (props.disabled) return
|
||||||
for (let i = 0; i < students.value.length; i++) {
|
for (let i = 0; i < students.value.length; i++) {
|
||||||
const student = students.value[i]
|
const student = students.value[i]
|
||||||
if (student.removed) continue
|
if (student.removed) continue
|
||||||
|
@ -293,7 +300,7 @@ defineExpose({
|
||||||
<th v-if="selectedView !== TableView.WHITEBOARD">#</th>
|
<th v-if="selectedView !== TableView.WHITEBOARD">#</th>
|
||||||
<th>Student</th>
|
<th>Student</th>
|
||||||
<th v-if="assignedDesks">Desk</th>
|
<th v-if="assignedDesks">Desk</th>
|
||||||
<DateHeaderCell v-for="date in getVisibleDates()" :key="date" :date-str="date"
|
<DateHeaderCell v-for="date in getVisibleDates()" :key="date" :date-str="date" :disabled="disabled"
|
||||||
@add-all-entries-clicked="addAllEntriesForDate(date)" />
|
@add-all-entries-clicked="addAllEntriesForDate(date)" />
|
||||||
<th v-if="selectedView !== TableView.WHITEBOARD">Score</th>
|
<th v-if="selectedView !== TableView.WHITEBOARD">Score</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -301,11 +308,12 @@ defineExpose({
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(student, idx) in getVisibleStudents()" :key="student.id" style="height: 2em;">
|
<tr v-for="(student, idx) in getVisibleStudents()" :key="student.id" style="height: 2em;">
|
||||||
<td v-if="selectedView !== TableView.WHITEBOARD" style="text-align: right; padding-right: 0.5em;">{{ idx + 1
|
<td v-if="selectedView !== TableView.WHITEBOARD" style="text-align: right; padding-right: 0.5em;">{{ idx + 1
|
||||||
}}.</td>
|
}}.</td>
|
||||||
<StudentNameCell :student="student" :class-id="classId" />
|
<StudentNameCell :student="student" :class-id="classId" />
|
||||||
<td v-if="assignedDesks" v-text="student.deskNumber"></td>
|
<td v-if="assignedDesks" v-text="student.deskNumber"></td>
|
||||||
<EntryTableCell v-for="(entry, date) in getVisibleStudentEntries(student)" :key="date"
|
<EntryTableCell v-for="(entry, date) in getVisibleStudentEntries(student)" :key="date"
|
||||||
v-model="student.entries[date]" :date-str="date" :last-save-state-timestamp="lastSaveStateTimestamp" />
|
v-model="student.entries[date]" :date-str="date" :last-save-state-timestamp="lastSaveStateTimestamp"
|
||||||
|
:disabled="disabled" />
|
||||||
<StudentScoreCell :score="student.score" v-if="selectedView !== TableView.WHITEBOARD" />
|
<StudentScoreCell :score="student.score" v-if="selectedView !== TableView.WHITEBOARD" />
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<script setup lang="ts">
|
<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';
|
import { EMOJI_ABSENT, EMOJI_BEHAVIOR_GOOD, EMOJI_BEHAVIOR_MEDIOCRE, EMOJI_BEHAVIOR_POOR, EMOJI_CLASSROOM_READY, EMOJI_NOT_CLASSROOM_READY, EMOJI_PHONE_COMPLIANT, EMOJI_PHONE_NONCOMPLIANT, EMOJI_PRESENT, type Entry } from '@/api/classroom_compliance';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
entry: Entry
|
entry: Entry
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
function getFormattedDate() {
|
function getFormattedDate() {
|
||||||
const d = new Date(props.entry.date)
|
const d = new Date(props.entry.date + 'T00:00:00')
|
||||||
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||||
return days[d.getDay()] + ', ' + d.toLocaleDateString()
|
return days[d.getDay()] + ', ' + d.toLocaleDateString()
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,9 @@ function getFormattedDate() {
|
||||||
<span v-if="entry.phoneCompliant === true">{{ EMOJI_PHONE_COMPLIANT }}</span>
|
<span v-if="entry.phoneCompliant === true">{{ EMOJI_PHONE_COMPLIANT }}</span>
|
||||||
<span v-if="entry.phoneCompliant === false">{{ EMOJI_PHONE_NONCOMPLIANT }}</span>
|
<span v-if="entry.phoneCompliant === false">{{ EMOJI_PHONE_NONCOMPLIANT }}</span>
|
||||||
|
|
||||||
|
<span v-if="entry.classroomReadiness === true">{{ EMOJI_CLASSROOM_READY }}</span>
|
||||||
|
<span v-if="entry.classroomReadiness === false">{{ EMOJI_NOT_CLASSROOM_READY }}</span>
|
||||||
|
|
||||||
<span v-if="entry.behaviorRating === 3">{{ EMOJI_BEHAVIOR_GOOD }}</span>
|
<span v-if="entry.behaviorRating === 3">{{ EMOJI_BEHAVIOR_GOOD }}</span>
|
||||||
<span v-if="entry.behaviorRating === 2">{{ EMOJI_BEHAVIOR_MEDIOCRE }}</span>
|
<span v-if="entry.behaviorRating === 2">{{ EMOJI_BEHAVIOR_MEDIOCRE }}</span>
|
||||||
<span v-if="entry.behaviorRating === 1">{{ EMOJI_BEHAVIOR_POOR }}</span>
|
<span v-if="entry.behaviorRating === 1">{{ EMOJI_BEHAVIOR_POOR }}</span>
|
||||||
|
@ -28,6 +31,9 @@ function getFormattedDate() {
|
||||||
<p v-if="entry.comment.trim().length > 0" class="comment">
|
<p v-if="entry.comment.trim().length > 0" class="comment">
|
||||||
{{ entry.comment }}
|
{{ entry.comment }}
|
||||||
</p>
|
</p>
|
||||||
|
<ul v-if="entry.checklistItems">
|
||||||
|
<li v-for="item in entry.checklistItems" :key="item" v-text="item"></li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { ClassroomComplianceAPIClient, type Class, type Entry, type Student, type StudentStatisticsOverview } from '@/api/classroom_compliance'
|
import { ClassroomComplianceAPIClient, type Class, type Entry, type Student, type StudentStatisticsOverview } 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 { computed, onMounted, ref, useTemplateRef, type Ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import StudentEntriesList from './StudentEntriesList.vue'
|
import StudentEntriesList from './StudentEntriesList.vue'
|
||||||
import { APIError } from '@/api/base'
|
import { APIError } from '@/api/base'
|
||||||
|
@ -18,9 +18,38 @@ 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 entries: Ref<Entry[]> = ref([])
|
||||||
const statistics: Ref<StudentStatisticsOverview | null> = ref(null)
|
const statistics: Ref<StudentStatisticsOverview | null> = ref(null)
|
||||||
|
// Filtered set of entries for "last week", used in the week overview dialog.
|
||||||
|
const lastWeeksEntries = computed(() => {
|
||||||
|
const now = new Date()
|
||||||
|
now.setHours(0, 0, 0, 0)
|
||||||
|
const toDate = new Date()
|
||||||
|
if (now.getDay() >= 1 && now.getDay() <= 5) {
|
||||||
|
// If we're currently in a week, shift the to-date to the Friday of this week.
|
||||||
|
const dayDiff = 5 - now.getDay()
|
||||||
|
toDate.setDate(now.getDate() + dayDiff)
|
||||||
|
} else {
|
||||||
|
// If it's saturday or sunday, shift back to the previous Friday.
|
||||||
|
if (now.getDay() === 6) {
|
||||||
|
toDate.setDate(now.getDate() - 1)
|
||||||
|
} else {
|
||||||
|
toDate.setDate(now.getDate() - 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromDate = new Date()
|
||||||
|
fromDate.setHours(0, 0, 0, 0)
|
||||||
|
fromDate.setDate(toDate.getDate() - 4)
|
||||||
|
|
||||||
|
const filtered = entries.value
|
||||||
|
.filter(e => new Date(e.date + 'T00:00:00').getTime() >= fromDate.getTime() &&
|
||||||
|
new Date(e.date + 'T00:00:00').getTime() <= toDate.getTime())
|
||||||
|
filtered.sort((a, b) => new Date(b.date + 'T00:00:00').getTime() - new Date(a.date + 'T00:00:00').getTime())
|
||||||
|
return filtered
|
||||||
|
})
|
||||||
|
|
||||||
const apiClient = new ClassroomComplianceAPIClient(authStore)
|
const apiClient = new ClassroomComplianceAPIClient(authStore)
|
||||||
const deleteConfirmDialog = useTemplateRef('deleteConfirmDialog')
|
const deleteConfirmDialog = useTemplateRef('deleteConfirmDialog')
|
||||||
|
const weekOverviewDialog = useTemplateRef('weekOverviewDialog')
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const classIdNumber = parseInt(props.classId, 10)
|
const classIdNumber = parseInt(props.classId, 10)
|
||||||
cls.value = await apiClient.getClass(classIdNumber).handleErrorsWithAlert()
|
cls.value = await apiClient.getClass(classIdNumber).handleErrorsWithAlert()
|
||||||
|
@ -58,6 +87,12 @@ async function deleteThisStudent() {
|
||||||
await apiClient.deleteStudent(cls.value.id, student.value.id)
|
await apiClient.deleteStudent(cls.value.id, student.value.id)
|
||||||
await router.replace(`/classroom-compliance/classes/${cls.value.id}`)
|
await router.replace(`/classroom-compliance/classes/${cls.value.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getFormattedDate(entry: Entry) {
|
||||||
|
const d = new Date(entry.date + 'T00:00:00')
|
||||||
|
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||||
|
return days[d.getDay()] + ', ' + d.toLocaleDateString()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div v-if="student" class="centered-content">
|
<div v-if="student" class="centered-content">
|
||||||
|
@ -73,6 +108,7 @@ async function deleteThisStudent() {
|
||||||
<button type="button"
|
<button type="button"
|
||||||
@click="router.push(`/classroom-compliance/classes/${student.classId}/edit-student?studentId=${student.id}`)">Edit</button>
|
@click="router.push(`/classroom-compliance/classes/${student.classId}/edit-student?studentId=${student.id}`)">Edit</button>
|
||||||
<button type="button" @click="deleteThisStudent">Delete</button>
|
<button type="button" @click="deleteThisStudent">Delete</button>
|
||||||
|
<button type="button" @click="weekOverviewDialog?.showModal()">Week overview</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="student-properties-table">
|
<table class="student-properties-table">
|
||||||
|
@ -116,6 +152,49 @@ async function deleteThisStudent() {
|
||||||
</p>
|
</p>
|
||||||
<p>This <strong>cannot</strong> be undone!</p>
|
<p>This <strong>cannot</strong> be undone!</p>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<dialog ref="weekOverviewDialog" method="dialog" class="weekly-overview-dialog">
|
||||||
|
<div>
|
||||||
|
<h2>This week's overview for <span v-text="student.name"></span></h2>
|
||||||
|
<div v-for="entry in lastWeeksEntries" :key="entry.id" class="weekly-overview-dialog-day">
|
||||||
|
<h4>{{ getFormattedDate(entry) }}</h4>
|
||||||
|
|
||||||
|
<div v-if="entry.classroomReadiness !== null">
|
||||||
|
<h5>Classroom Readiness</h5>
|
||||||
|
<p v-if="entry.classroomReadiness">✅ Ready for class</p>
|
||||||
|
<p v-if="!entry.classroomReadiness">❌ Not ready for class</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="entry.behaviorRating !== null">
|
||||||
|
<h5>Behavior</h5>
|
||||||
|
<p v-if="entry.behaviorRating === 1">
|
||||||
|
Poor
|
||||||
|
</p>
|
||||||
|
<p v-if="entry.behaviorRating === 2">
|
||||||
|
Mediocre
|
||||||
|
</p>
|
||||||
|
<p v-if="entry.behaviorRating === 3">
|
||||||
|
Good
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="entry.comment.length > 0">
|
||||||
|
<h5>Instructor Remarks</h5>
|
||||||
|
<p>{{ entry.comment }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="entry.checklistItems.length > 0">
|
||||||
|
<h5>Classroom Readiness & Behavioral Issues</h5>
|
||||||
|
<ul>
|
||||||
|
<li v-for="item in entry.checklistItems" :key="item" v-text="item"></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="button-bar align-right">
|
||||||
|
<button @click.prevent="weekOverviewDialog?.close()">Close</button>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -133,4 +212,27 @@ async function deleteThisStudent() {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-family: 'SourceCodePro', monospace;
|
font-family: 'SourceCodePro', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.weekly-overview-dialog-day {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-overview-dialog h4 {
|
||||||
|
margin: 1rem 0 -0.5rem -1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-overview-dialog h5 {
|
||||||
|
margin: 1rem 0 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-overview-dialog p {
|
||||||
|
margin: 0.5rem 0 0.5rem 1rem;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-overview-dialog ul {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { computed } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
dateStr: string
|
dateStr: string
|
||||||
|
disabled?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
|
@ -27,7 +28,7 @@ function getWeekday(date: Date): string {
|
||||||
<template>
|
<template>
|
||||||
<th>
|
<th>
|
||||||
<div class="date-header-container">
|
<div class="date-header-container">
|
||||||
<div class="date-header-button">
|
<div class="date-header-button" v-if="!disabled">
|
||||||
<span title="Add All Entries" @click="$emit('addAllEntriesClicked')">+</span>
|
<span title="Add All Entries" @click="$emit('addAllEntriesClicked')">+</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -1,10 +1,27 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
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 { EMOJI_ABSENT, EMOJI_BEHAVIOR_GOOD, EMOJI_BEHAVIOR_MEDIOCRE, EMOJI_BEHAVIOR_POOR, EMOJI_CLASSROOM_READY, EMOJI_NOT_CLASSROOM_READY, EMOJI_PHONE_COMPLIANT, EMOJI_PHONE_NONCOMPLIANT, EMOJI_PRESENT, getDefaultEntry, type Entry } from '@/api/classroom_compliance'
|
||||||
import { computed, onMounted, ref, useTemplateRef, watch, type Ref } from 'vue'
|
import { computed, onMounted, ref, useTemplateRef, watch, type Ref } from 'vue'
|
||||||
|
|
||||||
|
const COMMENT_CHECKLIST_ITEMS = {
|
||||||
|
"Classroom Readiness": [
|
||||||
|
"Tardy",
|
||||||
|
"Out of Uniform",
|
||||||
|
"Use of wireless tech",
|
||||||
|
"10+ minute bathroom pass",
|
||||||
|
"Not having laptop"
|
||||||
|
],
|
||||||
|
"Behavior": [
|
||||||
|
"Talking out of turn",
|
||||||
|
"Throwing objects in class",
|
||||||
|
"Rude language / comments to peers",
|
||||||
|
"Disrespectful towards the teacher"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
dateStr: string
|
dateStr: string
|
||||||
lastSaveStateTimestamp: number
|
lastSaveStateTimestamp: number
|
||||||
|
disabled?: boolean
|
||||||
}>()
|
}>()
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
(e: 'editComment'): void
|
(e: 'editComment'): void
|
||||||
|
@ -17,7 +34,7 @@ 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 hasComment = computed(() => model.value && (model.value.comment.trim().length > 0 || model.value.checklistItems.length > 0))
|
||||||
|
|
||||||
const previousCommentValue: Ref<string> = ref('')
|
const previousCommentValue: Ref<string> = ref('')
|
||||||
const commentEditorDialog = useTemplateRef('commentEditorDialog')
|
const commentEditorDialog = useTemplateRef('commentEditorDialog')
|
||||||
|
@ -34,21 +51,24 @@ onMounted(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
function toggleAbsence() {
|
function toggleAbsence() {
|
||||||
if (model.value) {
|
if (model.value && !props.disabled) {
|
||||||
model.value.absent = !model.value.absent
|
model.value.absent = !model.value.absent
|
||||||
if (model.value.absent) {
|
if (model.value.absent) {
|
||||||
// Remove additional data if student is absent.
|
// Remove additional data if student is absent.
|
||||||
model.value.phoneCompliant = null
|
model.value.classroomReadiness = null
|
||||||
model.value.behaviorRating = null
|
model.value.behaviorRating = null
|
||||||
} else {
|
} else {
|
||||||
// Populate default additional data if student is no longer absent.
|
// Populate default additional data if student is no longer absent.
|
||||||
model.value.phoneCompliant = true
|
model.value.classroomReadiness = true
|
||||||
model.value.behaviorRating = 3
|
model.value.behaviorRating = 3
|
||||||
// If we have an initial entry known, restore data from that.
|
// If we have an initial entry known, restore data from that.
|
||||||
if (initialEntryJson.value) {
|
if (initialEntryJson.value) {
|
||||||
const initialEntry = JSON.parse(initialEntryJson.value) as Entry
|
const initialEntry = JSON.parse(initialEntryJson.value) as Entry
|
||||||
if (initialEntry === null) return
|
if (initialEntry === null) return
|
||||||
if (initialEntry.absent) return
|
if (initialEntry.absent) return
|
||||||
|
if (initialEntry.classroomReadiness) {
|
||||||
|
model.value.classroomReadiness = initialEntry.classroomReadiness
|
||||||
|
}
|
||||||
if (initialEntry.phoneCompliant) {
|
if (initialEntry.phoneCompliant) {
|
||||||
model.value.phoneCompliant = initialEntry.phoneCompliant
|
model.value.phoneCompliant = initialEntry.phoneCompliant
|
||||||
}
|
}
|
||||||
|
@ -61,13 +81,19 @@ function toggleAbsence() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function togglePhoneCompliance() {
|
function togglePhoneCompliance() {
|
||||||
if (model.value && model.value.phoneCompliant !== null) {
|
if (model.value && model.value.phoneCompliant !== null && !props.disabled) {
|
||||||
model.value.phoneCompliant = !model.value.phoneCompliant
|
model.value.phoneCompliant = !model.value.phoneCompliant
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleClassroomReadiness() {
|
||||||
|
if (model.value && model.value.classroomReadiness !== null && !props.disabled) {
|
||||||
|
model.value.classroomReadiness = !model.value.classroomReadiness
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleBehaviorRating() {
|
function toggleBehaviorRating() {
|
||||||
if (model.value && model.value.behaviorRating) {
|
if (model.value && model.value.behaviorRating && !props.disabled) {
|
||||||
model.value.behaviorRating = model.value.behaviorRating - 1
|
model.value.behaviorRating = model.value.behaviorRating - 1
|
||||||
if (model.value.behaviorRating < 1) {
|
if (model.value.behaviorRating < 1) {
|
||||||
model.value.behaviorRating = 3
|
model.value.behaviorRating = 3
|
||||||
|
@ -89,6 +115,7 @@ function cancelCommentEdit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeEntry() {
|
function removeEntry() {
|
||||||
|
if (props.disabled) return
|
||||||
if (model.value) {
|
if (model.value) {
|
||||||
previouslyRemovedEntry.value = JSON.parse(JSON.stringify(model.value))
|
previouslyRemovedEntry.value = JSON.parse(JSON.stringify(model.value))
|
||||||
}
|
}
|
||||||
|
@ -96,6 +123,7 @@ function removeEntry() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function addEntry() {
|
function addEntry() {
|
||||||
|
if (props.disabled) return
|
||||||
if (previouslyRemovedEntry.value) {
|
if (previouslyRemovedEntry.value) {
|
||||||
model.value = JSON.parse(JSON.stringify(previouslyRemovedEntry.value))
|
model.value = JSON.parse(JSON.stringify(previouslyRemovedEntry.value))
|
||||||
} else {
|
} else {
|
||||||
|
@ -107,15 +135,22 @@ function addEntry() {
|
||||||
<td :class="{ absent: model?.absent, changed: entryChanged, 'missing-entry': !model }">
|
<td :class="{ absent: model?.absent, changed: entryChanged, 'missing-entry': !model }">
|
||||||
<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" :class="{ 'status-item-disabled': disabled }" @click="toggleAbsence">
|
||||||
<span v-if="model.absent" title="Absent">{{ EMOJI_ABSENT }}</span>
|
<span v-if="model.absent" title="Absent">{{ EMOJI_ABSENT }}</span>
|
||||||
<span v-if="!model.absent" title="Present">{{ EMOJI_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" :class="{ 'status-item-disabled': disabled }" @click="togglePhoneCompliance"
|
||||||
|
v-if="!model.absent && model.phoneCompliant !== null">
|
||||||
<span v-if="model.phoneCompliant" title="Phone Compliant">{{ EMOJI_PHONE_COMPLIANT }}</span>
|
<span v-if="model.phoneCompliant" title="Phone Compliant">{{ EMOJI_PHONE_COMPLIANT }}</span>
|
||||||
<span v-if="!model.phoneCompliant" title="Phone Non-Compliant">{{ EMOJI_PHONE_NONCOMPLIANT }}</span>
|
<span v-if="!model.phoneCompliant" title="Phone Non-Compliant">{{ EMOJI_PHONE_NONCOMPLIANT }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-item" @click="toggleBehaviorRating" v-if="!model.absent">
|
<div class="status-item" :class="{ 'status-item-disabled': disabled }" @click="toggleClassroomReadiness"
|
||||||
|
v-if="!model.absent && model.classroomReadiness !== null">
|
||||||
|
<span v-if="model.classroomReadiness" title="Ready for Class">{{ EMOJI_CLASSROOM_READY }}</span>
|
||||||
|
<span v-if="!model.classroomReadiness" title="Not Ready for Class">{{ EMOJI_NOT_CLASSROOM_READY }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item" :class="{ 'status-item-disabled': disabled }" @click="toggleBehaviorRating"
|
||||||
|
v-if="!model.absent">
|
||||||
<span v-if="model.behaviorRating === 3" title="Good Behavior">{{ EMOJI_BEHAVIOR_GOOD }}</span>
|
<span v-if="model.behaviorRating === 3" title="Good Behavior">{{ EMOJI_BEHAVIOR_GOOD }}</span>
|
||||||
<span v-if="model.behaviorRating === 2" title="Mediocre Behavior">{{ EMOJI_BEHAVIOR_MEDIOCRE }}</span>
|
<span v-if="model.behaviorRating === 2" title="Mediocre Behavior">{{ EMOJI_BEHAVIOR_MEDIOCRE }}</span>
|
||||||
<span v-if="model.behaviorRating === 1" title="Poor Behavior">{{ EMOJI_BEHAVIOR_POOR }}</span>
|
<span v-if="model.behaviorRating === 1" title="Poor Behavior">{{ EMOJI_BEHAVIOR_POOR }}</span>
|
||||||
|
@ -142,10 +177,20 @@ function addEntry() {
|
||||||
<!-- A comment editor dialog that shows up when the user edits their comment. -->
|
<!-- A comment editor dialog that shows up when the user edits their comment. -->
|
||||||
<dialog ref="commentEditorDialog" v-if="model">
|
<dialog ref="commentEditorDialog" v-if="model">
|
||||||
<textarea v-model="model.comment" style="min-width: 300px; min-height: 100px;"
|
<textarea v-model="model.comment" style="min-width: 300px; min-height: 100px;"
|
||||||
@keydown.enter="commentEditorDialog?.close()"></textarea>
|
@keydown.enter="commentEditorDialog?.close()" :readonly="disabled"></textarea>
|
||||||
|
<div>
|
||||||
|
<div v-for="options, category in COMMENT_CHECKLIST_ITEMS" :key="category">
|
||||||
|
<h3 v-text="category"></h3>
|
||||||
|
<label v-for="opt in options" :key="opt">
|
||||||
|
<input type="checkbox" v-model="model.checklistItems" :value="opt" />
|
||||||
|
<span v-text="opt"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="button-bar" style="text-align: right;">
|
<div class="button-bar" style="text-align: right;">
|
||||||
<button type="button" @click="commentEditorDialog?.close()">Confirm</button>
|
<button type="button" @click="commentEditorDialog?.close()" :disabled="disabled">Confirm</button>
|
||||||
<button type="button" @click="model.comment = ''; commentEditorDialog?.close()">Clear Comment</button>
|
<button type="button" @click="model.comment = ''; model.checklistItems = []; commentEditorDialog?.close()"
|
||||||
|
:disabled="disabled">Clear</button>
|
||||||
<button type="button" @click="cancelCommentEdit">Cancel</button>
|
<button type="button" @click="cancelCommentEdit">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
@ -175,6 +220,10 @@ td {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-item-disabled {
|
||||||
|
cursor: default !important;
|
||||||
|
}
|
||||||
|
|
||||||
.status-item+.status-item {
|
.status-item+.status-item {
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useAuthStore } from '@/stores/auth';
|
||||||
import { onMounted, onUnmounted, ref, useTemplateRef, type Ref } from 'vue';
|
import { onMounted, onUnmounted, ref, useTemplateRef, type Ref } from 'vue';
|
||||||
import ConfirmDialog from './ConfirmDialog.vue';
|
import ConfirmDialog from './ConfirmDialog.vue';
|
||||||
|
|
||||||
const REFRESH_INTERVAL_MS = 5000
|
const REFRESH_INTERVAL_MS = 30000
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const client = new AnnouncementAPIClient(authStore)
|
const client = new AnnouncementAPIClient(authStore)
|
||||||
|
|
Loading…
Reference in New Issue