Implemented most of the app.
This commit is contained in:
		
							parent
							
								
									7166b995f7
								
							
						
					
					
						commit
						3a682e046d
					
				| 
						 | 
				
			
			@ -19,7 +19,8 @@ CREATE TABLE classroom_compliance_entry (
 | 
			
		|||
    id INTEGER PRIMARY KEY,
 | 
			
		||||
    class_id INTEGER NOT NULL REFERENCES classroom_compliance_class(id)
 | 
			
		||||
        ON UPDATE CASCADE ON DELETE CASCADE,
 | 
			
		||||
    student_id INTEGER NOT NULL REFERENCES classroom_compliance_student(id),
 | 
			
		||||
    student_id INTEGER NOT NULL REFERENCES classroom_compliance_student(id)
 | 
			
		||||
        ON UPDATE CASCADE ON DELETE CASCADE,
 | 
			
		||||
    date TEXT NOT NULL,
 | 
			
		||||
    created_at INTEGER NOT NULL,
 | 
			
		||||
    absent INTEGER NOT NULL DEFAULT 0
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,10 @@ import d2sqlite3;
 | 
			
		|||
import slf4d;
 | 
			
		||||
import std.typecons : Nullable;
 | 
			
		||||
import std.datetime;
 | 
			
		||||
import std.format;
 | 
			
		||||
import std.json;
 | 
			
		||||
import std.algorithm;
 | 
			
		||||
import std.array;
 | 
			
		||||
 | 
			
		||||
import db;
 | 
			
		||||
import data_utils;
 | 
			
		||||
| 
						 | 
				
			
			@ -58,11 +62,12 @@ void registerApiEndpoints(PathHandler handler) {
 | 
			
		|||
    handler.addMapping(Method.POST, CLASS_PATH ~ "/students", &createStudent);
 | 
			
		||||
    handler.addMapping(Method.GET, CLASS_PATH ~ "/students", &getStudents);
 | 
			
		||||
    const STUDENT_PATH = CLASS_PATH ~ "/students/:studentId:ulong";
 | 
			
		||||
    handler.addMapping(Method.GET, STUDENT_PATH, &getStudent);
 | 
			
		||||
    handler.addMapping(Method.PUT, STUDENT_PATH, &updateStudent);
 | 
			
		||||
    handler.addMapping(Method.DELETE, STUDENT_PATH, &deleteStudent);
 | 
			
		||||
 | 
			
		||||
    handler.addMapping(Method.POST, CLASS_PATH ~ "/entries", &createEntry);
 | 
			
		||||
    handler.addMapping(Method.GET, CLASS_PATH ~ "/entries", &getEntries);
 | 
			
		||||
    handler.addMapping(Method.POST, CLASS_PATH ~ "/entries", &saveEntries);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void createClass(ref HttpRequestContext ctx) {
 | 
			
		||||
| 
						 | 
				
			
			@ -82,7 +87,7 @@ void createClass(ref HttpRequestContext ctx) {
 | 
			
		|||
    );
 | 
			
		||||
    if (classNumberExists) {
 | 
			
		||||
        ctx.response.status = HttpStatus.BAD_REQUEST;
 | 
			
		||||
        ctx.response.writeBodyString("There is already a class with this number, for this school year.");
 | 
			
		||||
        ctx.response.writeBodyString("There is already a class with this number, for the same school year.");
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    auto stmt = db.prepare("INSERT INTO classroom_compliance_class (number, school_year, user_id) VALUES (?, ?, ?)");
 | 
			
		||||
| 
						 | 
				
			
			@ -139,6 +144,7 @@ void createStudent(ref HttpRequestContext ctx) {
 | 
			
		|||
    struct StudentPayload {
 | 
			
		||||
        string name;
 | 
			
		||||
        ushort deskNumber;
 | 
			
		||||
        bool removed;
 | 
			
		||||
    }
 | 
			
		||||
    auto payload = readJsonPayload!(StudentPayload)(ctx);
 | 
			
		||||
    auto db = getDb();
 | 
			
		||||
| 
						 | 
				
			
			@ -150,7 +156,7 @@ void createStudent(ref HttpRequestContext ctx) {
 | 
			
		|||
    );
 | 
			
		||||
    if (studentExists) {
 | 
			
		||||
        ctx.response.status = HttpStatus.BAD_REQUEST;
 | 
			
		||||
        ctx.response.writeBodyString("Student with that name already exists.");
 | 
			
		||||
        ctx.response.writeBodyString("A student with that name already exists in this class.");
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    bool deskAlreadyOccupied = payload.deskNumber != 0 && canFind(
 | 
			
		||||
| 
						 | 
				
			
			@ -161,11 +167,11 @@ void createStudent(ref HttpRequestContext ctx) {
 | 
			
		|||
    );
 | 
			
		||||
    if (deskAlreadyOccupied) {
 | 
			
		||||
        ctx.response.status = HttpStatus.BAD_REQUEST;
 | 
			
		||||
        ctx.response.writeBodyString("There is already a student assigned to that desk.");
 | 
			
		||||
        ctx.response.writeBodyString("There is already a student assigned to that desk number.");
 | 
			
		||||
    }
 | 
			
		||||
    db.execute(
 | 
			
		||||
        "INSERT INTO classroom_compliance_student (name, class_id, desk_number) VALUES (?, ?)",
 | 
			
		||||
        payload.name, cls.id, payload.deskNumber
 | 
			
		||||
        "INSERT INTO classroom_compliance_student (name, class_id, desk_number, removed) VALUES (?, ?, ?, ?)",
 | 
			
		||||
        payload.name, cls.id, payload.deskNumber, payload.removed
 | 
			
		||||
    );
 | 
			
		||||
    ulong studentId = db.lastInsertRowid();
 | 
			
		||||
    auto student = findOne!(ClassroomComplianceStudent)(
 | 
			
		||||
| 
						 | 
				
			
			@ -188,22 +194,30 @@ void getStudents(ref HttpRequestContext ctx) {
 | 
			
		|||
    writeJsonBody(ctx, students);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void getStudent(ref HttpRequestContext ctx) {
 | 
			
		||||
    User user = getUserOrThrow(ctx);
 | 
			
		||||
    auto student = getStudentOrThrow(ctx, user);
 | 
			
		||||
    writeJsonBody(ctx, student);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void updateStudent(ref HttpRequestContext ctx) {
 | 
			
		||||
    User user = getUserOrThrow(ctx);
 | 
			
		||||
    auto student = getStudentOrThrow(ctx, user);
 | 
			
		||||
    struct StudentUpdatePayload {
 | 
			
		||||
        string name;
 | 
			
		||||
        ushort deskNumber;
 | 
			
		||||
        bool removed;
 | 
			
		||||
    }
 | 
			
		||||
    auto payload = readJsonPayload!(StudentUpdatePayload)(ctx);
 | 
			
		||||
    // If there is nothing to update, quit.
 | 
			
		||||
    if (
 | 
			
		||||
        payload.name == student.name
 | 
			
		||||
        && payload.deskNumber == student.deskNumber
 | 
			
		||||
        && payload.removed == student.removed
 | 
			
		||||
    ) return;
 | 
			
		||||
    // Check that the new name doesn't already exist.
 | 
			
		||||
    auto db = getDb();
 | 
			
		||||
    bool newNameExists = canFind(
 | 
			
		||||
    bool newNameExists = payload.name != student.name && canFind(
 | 
			
		||||
        db,
 | 
			
		||||
        "SELECT id FROM classroom_compliance_student WHERE name = ? AND class_id = ?",
 | 
			
		||||
        payload.name,
 | 
			
		||||
| 
						 | 
				
			
			@ -211,11 +225,11 @@ void updateStudent(ref HttpRequestContext ctx) {
 | 
			
		|||
    );
 | 
			
		||||
    if (newNameExists) {
 | 
			
		||||
        ctx.response.status = HttpStatus.BAD_REQUEST;
 | 
			
		||||
        ctx.response.writeBodyString("Student with that name already exists.");
 | 
			
		||||
        ctx.response.writeBodyString("A student with that name already exists in this class.");
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    // Check that if a new desk number is assigned, that it's not already assigned to anyone else.
 | 
			
		||||
    bool newDeskOccupied = payload.deskNumber != 0 && canFind(
 | 
			
		||||
    bool newDeskOccupied = payload.deskNumber != 0 && payload.deskNumber != student.deskNumber && canFind(
 | 
			
		||||
        db,
 | 
			
		||||
        "SELECT id FROM classroom_compliance_student WHERE class_id = ? AND desk_number = ?",
 | 
			
		||||
        student.classId,
 | 
			
		||||
| 
						 | 
				
			
			@ -227,9 +241,10 @@ void updateStudent(ref HttpRequestContext ctx) {
 | 
			
		|||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    db.execute(
 | 
			
		||||
        "UPDATE classroom_compliance_student SET name = ?, desk_number = ? WHERE id = ?",
 | 
			
		||||
        "UPDATE classroom_compliance_student SET name = ?, desk_number = ?, removed = ? WHERE id = ?",
 | 
			
		||||
        payload.name,
 | 
			
		||||
        payload.deskNumber,
 | 
			
		||||
        payload.removed,
 | 
			
		||||
        student.id
 | 
			
		||||
    );
 | 
			
		||||
    auto updatedStudent = findOne!(ClassroomComplianceStudent)(
 | 
			
		||||
| 
						 | 
				
			
			@ -270,75 +285,6 @@ ClassroomComplianceStudent getStudentOrThrow(ref HttpRequestContext ctx, in User
 | 
			
		|||
    ).orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void createEntry(ref HttpRequestContext ctx) {
 | 
			
		||||
    User user = getUserOrThrow(ctx);
 | 
			
		||||
    auto cls = getClassOrThrow(ctx, user);
 | 
			
		||||
    struct EntryPhonePayload {
 | 
			
		||||
        bool compliant;
 | 
			
		||||
    }
 | 
			
		||||
    struct EntryBehaviorPayload {
 | 
			
		||||
        int rating;
 | 
			
		||||
        Nullable!string comment;
 | 
			
		||||
    }
 | 
			
		||||
    struct EntryPayload {
 | 
			
		||||
        ulong studentId;
 | 
			
		||||
        string date;
 | 
			
		||||
        bool absent;
 | 
			
		||||
        Nullable!EntryPhonePayload phoneCompliance;
 | 
			
		||||
        Nullable!EntryBehaviorPayload behaviorCompliance;
 | 
			
		||||
    }
 | 
			
		||||
    auto payload = readJsonPayload!(EntryPayload)(ctx);
 | 
			
		||||
    auto db = getDb();
 | 
			
		||||
    bool entryAlreadyExists = canFind(
 | 
			
		||||
        db,
 | 
			
		||||
        "SELECT id FROM classroom_compliance_entry WHERE class_id = ? AND student_id = ? AND date = ?",
 | 
			
		||||
        cls.id,
 | 
			
		||||
        payload.studentId,
 | 
			
		||||
        payload.date
 | 
			
		||||
    );
 | 
			
		||||
    if (entryAlreadyExists) {
 | 
			
		||||
        ctx.response.status = HttpStatus.BAD_REQUEST;
 | 
			
		||||
        ctx.response.writeBodyString("An entry already exists for this student and date.");
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    // Insert the entry and its attached entities in a transaction.
 | 
			
		||||
    db.begin();
 | 
			
		||||
    try {
 | 
			
		||||
        db.execute(
 | 
			
		||||
            "INSERT INTO classroom_compliance_entry (class_id, student_id, date, created_at, absent)
 | 
			
		||||
            VALUES (?, ?, ?, ?, ?)",
 | 
			
		||||
            cls.id,
 | 
			
		||||
            payload.studentId,
 | 
			
		||||
            payload.date,
 | 
			
		||||
            getUnixTimestampMillis(),
 | 
			
		||||
            payload.absent
 | 
			
		||||
        );
 | 
			
		||||
        ulong entryId = db.lastInsertRowid();
 | 
			
		||||
        if (!payload.absent && !payload.phoneCompliance.isNull) {
 | 
			
		||||
            db.execute(
 | 
			
		||||
                "INSERT INTO classroom_compliance_entry_phone (entry_id, compliant) VALUES (?, ?)",
 | 
			
		||||
                entryId,
 | 
			
		||||
                payload.phoneCompliance.get().compliant
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        if (!payload.absent && !payload.behaviorCompliance.isNull) {
 | 
			
		||||
            Nullable!string comment = payload.behaviorCompliance.get().comment;
 | 
			
		||||
            if (!comment.isNull && (comment.get() is null || comment.get().length == 0)) {
 | 
			
		||||
                comment.nullify();
 | 
			
		||||
            }
 | 
			
		||||
            db.execute(
 | 
			
		||||
                "INSERT INTO classroom_compliance_entry_behavior (entry_id, rating, comment) VALUES (?, ?, ?)",
 | 
			
		||||
                entryId,
 | 
			
		||||
                payload.behaviorCompliance.get().rating,
 | 
			
		||||
                comment
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        db.commit();
 | 
			
		||||
    } catch (Exception e) {
 | 
			
		||||
        db.rollback();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void getEntries(ref HttpRequestContext ctx) {
 | 
			
		||||
    User user = getUserOrThrow(ctx);
 | 
			
		||||
    auto cls = getClassOrThrow(ctx, user);
 | 
			
		||||
| 
						 | 
				
			
			@ -367,6 +313,22 @@ void getEntries(ref HttpRequestContext ctx) {
 | 
			
		|||
    infoF!"Getting entries from %s to %s"(fromDate.toISOExtString(), toDate.toISOExtString());
 | 
			
		||||
 | 
			
		||||
    auto db = getDb();
 | 
			
		||||
 | 
			
		||||
    ClassroomComplianceStudent[] students = findAll!(ClassroomComplianceStudent)(
 | 
			
		||||
        db,
 | 
			
		||||
        "SELECT * FROM classroom_compliance_student WHERE class_id = ? ORDER BY name ASC",
 | 
			
		||||
        cls.id
 | 
			
		||||
    );
 | 
			
		||||
    JSONValue[] studentObjects = students.map!((s) {
 | 
			
		||||
        JSONValue obj = JSONValue.emptyObject;
 | 
			
		||||
        obj.object["id"] = JSONValue(s.id);
 | 
			
		||||
        obj.object["deskNumber"] = JSONValue(s.deskNumber);
 | 
			
		||||
        obj.object["name"] = JSONValue(s.name);
 | 
			
		||||
        obj.object["removed"] = JSONValue(s.removed);
 | 
			
		||||
        obj.object["entries"] = JSONValue.emptyObject;
 | 
			
		||||
        return obj;
 | 
			
		||||
    }).array;
 | 
			
		||||
 | 
			
		||||
    const query = "
 | 
			
		||||
    SELECT
 | 
			
		||||
        entry.id,
 | 
			
		||||
| 
						 | 
				
			
			@ -392,32 +354,16 @@ void getEntries(ref HttpRequestContext ctx) {
 | 
			
		|||
        AND entry.date >= ?
 | 
			
		||||
        AND entry.date <= ?
 | 
			
		||||
    ORDER BY
 | 
			
		||||
        entry.date ASC,
 | 
			
		||||
        student.desk_number ASC,
 | 
			
		||||
        student.name ASC
 | 
			
		||||
        student.id ASC,
 | 
			
		||||
        entry.date ASC
 | 
			
		||||
    ";
 | 
			
		||||
    ResultRange result = db.execute(query, cls.id, fromDate.toISOExtString(), toDate.toISOExtString());
 | 
			
		||||
    // Serialize the results into a custom-formatted response object.
 | 
			
		||||
    import std.json;
 | 
			
		||||
    JSONValue response = JSONValue.emptyObject;
 | 
			
		||||
    JSONValue[ulong] studentObjects;
 | 
			
		||||
    foreach (row; result) {
 | 
			
		||||
        ulong studentId = row.peek!ulong(4);
 | 
			
		||||
        if (studentId !in studentObjects) {
 | 
			
		||||
            JSONValue student = JSONValue.emptyObject;
 | 
			
		||||
            student.object["id"] = JSONValue(row.peek!ulong(4));
 | 
			
		||||
            student.object["name"] = JSONValue(row.peek!string(5));
 | 
			
		||||
            student.object["deskNumber"] = JSONValue(row.peek!ushort(6));
 | 
			
		||||
            student.object["removed"] = JSONValue(row.peek!bool(7));
 | 
			
		||||
            student.object["entries"] = JSONValue.emptyObject;
 | 
			
		||||
            studentObjects[studentId] = student;
 | 
			
		||||
        }
 | 
			
		||||
        JSONValue studentObj = studentObjects[studentId];
 | 
			
		||||
 | 
			
		||||
        JSONValue entry = JSONValue.emptyObject;
 | 
			
		||||
        entry.object["id"] = JSONValue(row.peek!ulong(0));
 | 
			
		||||
        entry.object["date"] = JSONValue(row.peek!string(1));
 | 
			
		||||
        entry.object["createdAt"] = JSONValue(row.peek!string(2));
 | 
			
		||||
        entry.object["createdAt"] = JSONValue(row.peek!ulong(2));
 | 
			
		||||
        entry.object["absent"] = JSONValue(row.peek!bool(3));
 | 
			
		||||
 | 
			
		||||
        JSONValue phone = JSONValue(null);
 | 
			
		||||
| 
						 | 
				
			
			@ -431,12 +377,27 @@ void getEntries(ref HttpRequestContext ctx) {
 | 
			
		|||
        }
 | 
			
		||||
        entry.object["phone"] = phone;
 | 
			
		||||
        entry.object["behavior"] = behavior;
 | 
			
		||||
        
 | 
			
		||||
        string dateStr = entry.object["date"].str();
 | 
			
		||||
        studentObj.object["entries"].object[dateStr] = entry;
 | 
			
		||||
 | 
			
		||||
        // Find the student object this entry belongs to, then add it to their list.
 | 
			
		||||
        ulong studentId = row.peek!ulong(4);
 | 
			
		||||
        bool studentFound = false;
 | 
			
		||||
        foreach (idx, student; students) {
 | 
			
		||||
            if (student.id == studentId) {
 | 
			
		||||
                studentObjects[idx].object["entries"].object[dateStr] = entry;
 | 
			
		||||
                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;
 | 
			
		||||
    
 | 
			
		||||
    // Also fill in "null" for any students that don't have an entry on one of these days.
 | 
			
		||||
    Date d = fromDate;
 | 
			
		||||
    while (d <= toDate) {
 | 
			
		||||
| 
						 | 
				
			
			@ -449,8 +410,180 @@ void getEntries(ref HttpRequestContext ctx) {
 | 
			
		|||
        }
 | 
			
		||||
        d += days(1);
 | 
			
		||||
    }
 | 
			
		||||
    response.object["students"] = JSONValue(studentObjects.values);
 | 
			
		||||
    response.object["students"] = JSONValue(studentObjects);
 | 
			
		||||
 | 
			
		||||
    string jsonStr = response.toJSON();
 | 
			
		||||
    ctx.response.writeBodyString(jsonStr, "application/json");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void saveEntries(ref HttpRequestContext ctx) {
 | 
			
		||||
    User user = getUserOrThrow(ctx);
 | 
			
		||||
    auto cls = getClassOrThrow(ctx, user);
 | 
			
		||||
    JSONValue bodyContent = ctx.request.readBodyAsJson();
 | 
			
		||||
    auto db = getDb();
 | 
			
		||||
    db.begin();
 | 
			
		||||
    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);
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Optional!ClassroomComplianceEntry existingEntry = findOne!(ClassroomComplianceEntry)(
 | 
			
		||||
                db,
 | 
			
		||||
                "SELECT * FROM classroom_compliance_entry WHERE class_id = ? AND student_id = ? AND date = ?",
 | 
			
		||||
                cls.id, studentId, dateStr
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            ulong entryId = entry.object["id"].integer();
 | 
			
		||||
            bool creatingNewEntry = entryId == 0;
 | 
			
		||||
 | 
			
		||||
            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
 | 
			
		||||
                        )
 | 
			
		||||
                    );
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                insertNewEntry(db, cls.id, studentId, dateStr, entry);
 | 
			
		||||
            } else {
 | 
			
		||||
                if (existingEntry.isNull) {
 | 
			
		||||
                    ctx.response.status = HttpStatus.BAD_REQUEST;
 | 
			
		||||
                    ctx.response.writeBodyString(
 | 
			
		||||
                        format!"Cannot update entry %d because it doesn't exist."(
 | 
			
		||||
                            entryId
 | 
			
		||||
                        )
 | 
			
		||||
                    );
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                updateEntry(db, cls.id, studentId, dateStr, entryId, entry);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    db.commit();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private void insertNewEntry(
 | 
			
		||||
    ref Database db,
 | 
			
		||||
    ulong classId,
 | 
			
		||||
    ulong studentId,
 | 
			
		||||
    string dateStr,
 | 
			
		||||
    JSONValue payload
 | 
			
		||||
) {
 | 
			
		||||
    ulong createdAt = getUnixTimestampMillis();
 | 
			
		||||
    bool absent = payload.object["absent"].boolean;
 | 
			
		||||
    db.execute(
 | 
			
		||||
        "INSERT INTO classroom_compliance_entry
 | 
			
		||||
        (class_id, student_id, date, created_at, absent)
 | 
			
		||||
        VALUES (?, ?, ?, ?, ?)",
 | 
			
		||||
        classId, studentId, dateStr, createdAt, absent
 | 
			
		||||
    );
 | 
			
		||||
    if (!absent) {
 | 
			
		||||
        ulong entryId = db.lastInsertRowid();
 | 
			
		||||
        if ("phone" !in payload.object || payload.object["phone"].type != JSONType.object) {
 | 
			
		||||
            throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Missing phone data.");
 | 
			
		||||
        }
 | 
			
		||||
        if ("behavior" !in payload.object || payload.object["behavior"].type != JSONType.object) {
 | 
			
		||||
            throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Missing behavior data.");
 | 
			
		||||
        }
 | 
			
		||||
        bool phoneCompliance = payload.object["phone"].object["compliant"].boolean;
 | 
			
		||||
        db.execute(
 | 
			
		||||
            "INSERT INTO classroom_compliance_entry_phone (entry_id, compliant)
 | 
			
		||||
            VALUES (?, ?)",
 | 
			
		||||
            entryId, phoneCompliance
 | 
			
		||||
        );
 | 
			
		||||
        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, ""
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    infoF!"Created new entry for student %d: %s"(studentId, payload);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private void updateEntry(
 | 
			
		||||
    ref Database db,
 | 
			
		||||
    ulong classId,
 | 
			
		||||
    ulong studentId,
 | 
			
		||||
    string dateStr,
 | 
			
		||||
    ulong entryId,
 | 
			
		||||
    JSONValue obj
 | 
			
		||||
) {
 | 
			
		||||
    bool absent = obj.object["absent"].boolean;
 | 
			
		||||
    db.execute(
 | 
			
		||||
        "UPDATE classroom_compliance_entry
 | 
			
		||||
        SET absent = ?
 | 
			
		||||
        WHERE class_id = ? AND student_id = ? AND date = ? AND id = ?",
 | 
			
		||||
        absent, classId, studentId, dateStr, entryId
 | 
			
		||||
    );
 | 
			
		||||
    if (absent) {
 | 
			
		||||
        db.execute(
 | 
			
		||||
            "DELETE FROM classroom_compliance_entry_phone WHERE entry_id = ?",
 | 
			
		||||
            entryId
 | 
			
		||||
        );
 | 
			
		||||
        db.execute(
 | 
			
		||||
            "DELETE FROM classroom_compliance_entry_behavior WHERE entry_id = ?",
 | 
			
		||||
            entryId
 | 
			
		||||
        );
 | 
			
		||||
    } else {
 | 
			
		||||
        bool phoneCompliant = obj.object["phone"].object["compliant"].boolean;
 | 
			
		||||
        bool phoneDataExists = canFind(
 | 
			
		||||
            db,
 | 
			
		||||
            "SELECT * FROM classroom_compliance_entry_phone WHERE entry_id = ?",
 | 
			
		||||
            entryId
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (phoneDataExists) {
 | 
			
		||||
            db.execute(
 | 
			
		||||
                "UPDATE classroom_compliance_entry_phone
 | 
			
		||||
                SET compliant = ?
 | 
			
		||||
                WHERE entry_id = ?",
 | 
			
		||||
                phoneCompliant,
 | 
			
		||||
                entryId
 | 
			
		||||
            );
 | 
			
		||||
        } else {
 | 
			
		||||
            db.execute(
 | 
			
		||||
                "INSERT INTO classroom_compliance_entry_phone (entry_id, compliant)
 | 
			
		||||
                VALUES (?, ?)",
 | 
			
		||||
                entryId, phoneCompliant
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ubyte behaviorRating = cast(ubyte) obj.object["behavior"].object["rating"].integer;
 | 
			
		||||
        bool behaviorDataExists = canFind(
 | 
			
		||||
            db,
 | 
			
		||||
            "SELECT * FROM classroom_compliance_entry_behavior WHERE entry_id = ?",
 | 
			
		||||
            entryId
 | 
			
		||||
        );
 | 
			
		||||
        if (behaviorDataExists) {
 | 
			
		||||
            db.execute(
 | 
			
		||||
                "UPDATE classroom_compliance_entry_behavior
 | 
			
		||||
                SET rating = ?
 | 
			
		||||
                WHERE entry_id = ?",
 | 
			
		||||
                behaviorRating,
 | 
			
		||||
                entryId
 | 
			
		||||
            );
 | 
			
		||||
        } else {
 | 
			
		||||
            db.execute(
 | 
			
		||||
                "INSERT INTO classroom_compliance_entry_behavior (entry_id, rating)
 | 
			
		||||
                VALUES (?, ?)",
 | 
			
		||||
                entryId, behaviorRating
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    infoF!"Updated entry %d"(entryId);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,15 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import { RouterLink, RouterView } from 'vue-router'
 | 
			
		||||
import { RouterLink, RouterView, useRouter } from 'vue-router'
 | 
			
		||||
import { useAuthStore } from './stores/auth'
 | 
			
		||||
import AlertDialog from './components/AlertDialog.vue'
 | 
			
		||||
 | 
			
		||||
const authStore = useAuthStore()
 | 
			
		||||
const router = useRouter()
 | 
			
		||||
 | 
			
		||||
async function logOut() {
 | 
			
		||||
  authStore.logOut()
 | 
			
		||||
  await router.replace('/')
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
| 
						 | 
				
			
			@ -15,7 +22,7 @@ const authStore = useAuthStore()
 | 
			
		|||
        <span v-if="authStore.state">
 | 
			
		||||
          Welcome, <span v-text="authStore.state.username"></span>
 | 
			
		||||
        </span>
 | 
			
		||||
        <button type="button" @click="authStore.logOut" v-if="authStore.state">Log out</button>
 | 
			
		||||
        <button type="button" @click="logOut" v-if="authStore.state">Log out</button>
 | 
			
		||||
      </nav>
 | 
			
		||||
      <nav v-if="authStore.state">
 | 
			
		||||
        Apps:
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +33,9 @@ const authStore = useAuthStore()
 | 
			
		|||
  </header>
 | 
			
		||||
 | 
			
		||||
  <RouterView />
 | 
			
		||||
 | 
			
		||||
  <!-- Global dialog elements are included here below, hidden by default. -->
 | 
			
		||||
  <AlertDialog />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
export function showAlert(msg: string): Promise<void> {
 | 
			
		||||
  const dialog = document.getElementById('alert-dialog') as HTMLDialogElement
 | 
			
		||||
  const messageBox = document.getElementById('alert-dialog-message') as HTMLParagraphElement
 | 
			
		||||
  const closeButton = document.getElementById('alert-dialog-close-button') as HTMLButtonElement
 | 
			
		||||
  closeButton.addEventListener('click', () => dialog.close())
 | 
			
		||||
  messageBox.innerText = msg
 | 
			
		||||
  dialog.showModal()
 | 
			
		||||
  return new Promise((resolve) => {
 | 
			
		||||
    dialog.addEventListener('close', () => resolve())
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +1,5 @@
 | 
			
		|||
import { APIClient, APIResponse, type AuthStoreType } from './base'
 | 
			
		||||
 | 
			
		||||
const BASE_URL = import.meta.env.VITE_API_URL + '/auth'
 | 
			
		||||
 | 
			
		||||
export interface User {
 | 
			
		||||
| 
						 | 
				
			
			@ -8,22 +10,18 @@ export interface User {
 | 
			
		|||
  isAdmin: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getAuthHeaders(basicAuth: string) {
 | 
			
		||||
  return {
 | 
			
		||||
    Authorization: 'Basic ' + basicAuth,
 | 
			
		||||
export class AuthenticationAPIClient extends APIClient {
 | 
			
		||||
  constructor(authStore: AuthStoreType) {
 | 
			
		||||
    super(BASE_URL, authStore)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function login(username: string, password: string): Promise<User | null> {
 | 
			
		||||
  const basicAuth = btoa(username + ':' + password)
 | 
			
		||||
  const response = await fetch(BASE_URL + '/login', {
 | 
			
		||||
    method: 'POST',
 | 
			
		||||
    headers: {
 | 
			
		||||
      Authorization: 'Basic ' + basicAuth,
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
  if (!response.ok) {
 | 
			
		||||
    return null
 | 
			
		||||
  login(username: string, password: string): APIResponse<User> {
 | 
			
		||||
    const promise = fetch(this.baseUrl + '/login', {
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      headers: {
 | 
			
		||||
        Authorization: 'Basic ' + btoa(username + ':' + password),
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
    return new APIResponse(this.handleAPIResponse(promise))
 | 
			
		||||
  }
 | 
			
		||||
  return (await response.json()) as User
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,134 @@
 | 
			
		|||
import { showAlert } from '@/alerts'
 | 
			
		||||
import { useAuthStore } from '@/stores/auth'
 | 
			
		||||
 | 
			
		||||
export abstract class APIError {
 | 
			
		||||
  message: string
 | 
			
		||||
  constructor(message: string) {
 | 
			
		||||
    this.message = message
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class BadRequestError extends APIError {}
 | 
			
		||||
 | 
			
		||||
export class InternalServerError extends APIError {}
 | 
			
		||||
 | 
			
		||||
export class NetworkError extends APIError {}
 | 
			
		||||
 | 
			
		||||
export class AuthenticationError extends APIError {}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represents a generic API response that that will be completed in the future
 | 
			
		||||
 * using a given promise, which resolves either to a specified response type,
 | 
			
		||||
 * or an API Error type.
 | 
			
		||||
 */
 | 
			
		||||
export class APIResponse<T> {
 | 
			
		||||
  result: Promise<T | APIError>
 | 
			
		||||
  constructor(result: Promise<T | APIError>) {
 | 
			
		||||
    this.result = result
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async handleErrorsWithAlert(): Promise<T | null> {
 | 
			
		||||
    const value = await this.result
 | 
			
		||||
    if (value instanceof APIError) {
 | 
			
		||||
      await showAlert(value.message)
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
    return value
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getOrThrow(): Promise<T> {
 | 
			
		||||
    const value = await this.result
 | 
			
		||||
    if (value instanceof APIError) throw value
 | 
			
		||||
    return value
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type AuthStoreType = ReturnType<typeof useAuthStore>
 | 
			
		||||
 | 
			
		||||
export abstract class APIClient {
 | 
			
		||||
  readonly baseUrl: string
 | 
			
		||||
  authStore: AuthStoreType
 | 
			
		||||
  constructor(baseUrl: string, authStore: AuthStoreType) {
 | 
			
		||||
    this.baseUrl = baseUrl
 | 
			
		||||
    this.authStore = authStore
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected get<T>(url: string): APIResponse<T> {
 | 
			
		||||
    const promise = fetch(this.baseUrl + url, { headers: this.getAuthHeaders() })
 | 
			
		||||
    return new APIResponse(this.handleAPIResponse(promise))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected post<T, B>(url: string, body: B): APIResponse<T> {
 | 
			
		||||
    const promise = fetch(this.baseUrl + url, {
 | 
			
		||||
      headers: this.getAuthHeaders(),
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      body: JSON.stringify(body),
 | 
			
		||||
    })
 | 
			
		||||
    return new APIResponse(this.handleAPIResponse(promise))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected postWithNoExpectedResponse<B>(url: string, body: B): APIResponse<void> {
 | 
			
		||||
    const promise = fetch(this.baseUrl + url, {
 | 
			
		||||
      headers: this.getAuthHeaders(),
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      body: JSON.stringify(body),
 | 
			
		||||
    })
 | 
			
		||||
    return new APIResponse(this.handleAPIResponseWithNoBody(promise))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected put<T, B>(url: string, body: B): APIResponse<T> {
 | 
			
		||||
    const promise = fetch(this.baseUrl + url, {
 | 
			
		||||
      headers: this.getAuthHeaders(),
 | 
			
		||||
      method: 'PUT',
 | 
			
		||||
      body: JSON.stringify(body),
 | 
			
		||||
    })
 | 
			
		||||
    return new APIResponse(this.handleAPIResponse(promise))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected delete(url: string): APIResponse<void> {
 | 
			
		||||
    const promise = fetch(this.baseUrl + url, {
 | 
			
		||||
      headers: this.getAuthHeaders(),
 | 
			
		||||
      method: 'DELETE',
 | 
			
		||||
    })
 | 
			
		||||
    return new APIResponse(this.handleAPIResponseWithNoBody(promise))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected getAuthHeaders() {
 | 
			
		||||
    return {
 | 
			
		||||
      Authorization: 'Basic ' + this.authStore.getBasicAuth(),
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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())
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      return new NetworkError('' + error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected async handleAPIResponseWithNoBody(
 | 
			
		||||
    promise: Promise<Response>,
 | 
			
		||||
  ): Promise<void | APIError> {
 | 
			
		||||
    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())
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      return new NetworkError('' + error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import { getAuthHeaders } from '@/api/auth'
 | 
			
		||||
import { APIClient, type APIResponse, type AuthStoreType } from './base'
 | 
			
		||||
 | 
			
		||||
const BASE_URL = import.meta.env.VITE_API_URL + '/classroom-compliance'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -16,100 +16,120 @@ export interface Student {
 | 
			
		|||
  removed: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface EntryResponseItemPhone {
 | 
			
		||||
export interface EntryPhone {
 | 
			
		||||
  compliant: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface EntryResponseItemBehavior {
 | 
			
		||||
export interface EntryBehavior {
 | 
			
		||||
  rating: number
 | 
			
		||||
  comment?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface EntryResponseItem {
 | 
			
		||||
export interface Entry {
 | 
			
		||||
  id: number
 | 
			
		||||
  date: string
 | 
			
		||||
  createdAt: string
 | 
			
		||||
  createdAt: number
 | 
			
		||||
  absent: boolean
 | 
			
		||||
  phone?: EntryResponseItemPhone
 | 
			
		||||
  behavior?: EntryResponseItemBehavior
 | 
			
		||||
  phone: EntryPhone | null
 | 
			
		||||
  behavior: EntryBehavior | null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface EntryResponseStudent {
 | 
			
		||||
export function getDefaultEntry(dateStr: string): Entry {
 | 
			
		||||
  return {
 | 
			
		||||
    id: 0,
 | 
			
		||||
    date: dateStr,
 | 
			
		||||
    createdAt: Date.now(),
 | 
			
		||||
    absent: false,
 | 
			
		||||
    phone: { compliant: true },
 | 
			
		||||
    behavior: { rating: 3 },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface EntriesResponseStudent {
 | 
			
		||||
  id: number
 | 
			
		||||
  name: string
 | 
			
		||||
  deskNumber: number
 | 
			
		||||
  removed: boolean
 | 
			
		||||
  entries: Record<string, EntryResponseItem | null>
 | 
			
		||||
  entries: Record<string, Entry | null>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface EntriesResponse {
 | 
			
		||||
  students: EntryResponseStudent[]
 | 
			
		||||
  students: EntriesResponseStudent[]
 | 
			
		||||
  dates: string[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getClassPath(classId: number): string {
 | 
			
		||||
  return BASE_URL + '/classes/' + classId
 | 
			
		||||
export interface StudentDataPayload {
 | 
			
		||||
  name: string
 | 
			
		||||
  deskNumber: number
 | 
			
		||||
  removed: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function createClass(
 | 
			
		||||
  auth: string,
 | 
			
		||||
  number: number,
 | 
			
		||||
  schoolYear: string,
 | 
			
		||||
): Promise<Class> {
 | 
			
		||||
  const response = await fetch(BASE_URL + '/classes', {
 | 
			
		||||
    method: 'POST',
 | 
			
		||||
    headers: getAuthHeaders(auth),
 | 
			
		||||
    body: JSON.stringify({ number: number, schoolYear: schoolYear }),
 | 
			
		||||
  })
 | 
			
		||||
  return (await response.json()) as Class
 | 
			
		||||
export interface EntriesPayloadStudent {
 | 
			
		||||
  id: number
 | 
			
		||||
  entries: Record<string, Entry | null>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getClasses(auth: string): Promise<Class[]> {
 | 
			
		||||
  const response = await fetch(BASE_URL + '/classes', {
 | 
			
		||||
    headers: getAuthHeaders(auth),
 | 
			
		||||
  })
 | 
			
		||||
  return (await response.json()) as Class[]
 | 
			
		||||
export interface EntriesPayload {
 | 
			
		||||
  students: EntriesPayloadStudent[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getClass(auth: string, id: number): Promise<Class> {
 | 
			
		||||
  const response = await fetch(getClassPath(id), {
 | 
			
		||||
    headers: getAuthHeaders(auth),
 | 
			
		||||
  })
 | 
			
		||||
  return (await response.json()) as Class
 | 
			
		||||
}
 | 
			
		||||
export class ClassroomComplianceAPIClient extends APIClient {
 | 
			
		||||
  constructor(authStore: AuthStoreType) {
 | 
			
		||||
    super(BASE_URL, authStore)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
export async function deleteClass(auth: string, classId: number): Promise<void> {
 | 
			
		||||
  const response = await fetch(getClassPath(classId), {
 | 
			
		||||
    method: 'DELETE',
 | 
			
		||||
    headers: getAuthHeaders(auth),
 | 
			
		||||
  })
 | 
			
		||||
  if (!response.ok) {
 | 
			
		||||
    throw new Error('Failed to delete class.')
 | 
			
		||||
  createClass(number: number, schoolYear: string): APIResponse<Class> {
 | 
			
		||||
    return super.post('/classes', { number: number, schoolYear: schoolYear })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getClasses(): APIResponse<Class[]> {
 | 
			
		||||
    return super.get('/classes')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getClass(classId: number): APIResponse<Class> {
 | 
			
		||||
    return super.get(`/classes/${classId}`)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  deleteClass(classId: number): APIResponse<void> {
 | 
			
		||||
    return super.delete(`/classes/${classId}`)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getStudents(classId: number): APIResponse<Student[]> {
 | 
			
		||||
    return super.get(`/classes/${classId}/students`)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getStudent(classId: number, studentId: number): APIResponse<Student> {
 | 
			
		||||
    return super.get(`/classes/${classId}/students/${studentId}`)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  createStudent(classId: number, data: StudentDataPayload): APIResponse<Student> {
 | 
			
		||||
    return super.post(`/classes/${classId}/students`, data)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateStudent(
 | 
			
		||||
    classId: number,
 | 
			
		||||
    studentId: number,
 | 
			
		||||
    data: StudentDataPayload,
 | 
			
		||||
  ): APIResponse<Student> {
 | 
			
		||||
    return super.put(`/classes/${classId}/students/${studentId}`, data)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  deleteStudent(classId: number, studentId: number): APIResponse<void> {
 | 
			
		||||
    return super.delete(`/classes/${classId}/students/${studentId}`)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getEntries(classId: number, fromDate?: Date, toDate?: Date): APIResponse<EntriesResponse> {
 | 
			
		||||
    const params = new URLSearchParams()
 | 
			
		||||
    if (fromDate) {
 | 
			
		||||
      params.append('from', fromDate.toISOString().substring(0, 10))
 | 
			
		||||
    }
 | 
			
		||||
    if (toDate) {
 | 
			
		||||
      params.append('to', toDate.toISOString().substring(0, 10))
 | 
			
		||||
    }
 | 
			
		||||
    return super.get(`/classes/${classId}/entries?${params.toString()}`)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  saveEntries(classId: number, payload: EntriesPayload): APIResponse<void> {
 | 
			
		||||
    return super.postWithNoExpectedResponse(`/classes/${classId}/entries`, payload)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getStudents(auth: string, classId: number): Promise<Student[]> {
 | 
			
		||||
  const response = await fetch(getClassPath(classId) + '/students', {
 | 
			
		||||
    headers: getAuthHeaders(auth),
 | 
			
		||||
  })
 | 
			
		||||
  return (await response.json()) as Student[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getEntries(
 | 
			
		||||
  auth: string,
 | 
			
		||||
  classId: number,
 | 
			
		||||
  fromDate?: Date,
 | 
			
		||||
  toDate?: Date,
 | 
			
		||||
): Promise<EntriesResponse> {
 | 
			
		||||
  const params = new URLSearchParams()
 | 
			
		||||
  if (fromDate) {
 | 
			
		||||
    params.append('from', fromDate.toISOString().substring(0, 10))
 | 
			
		||||
  }
 | 
			
		||||
  if (toDate) {
 | 
			
		||||
    params.append('to', toDate.toISOString().substring(0, 10))
 | 
			
		||||
  }
 | 
			
		||||
  const response = await fetch(getClassPath(classId) + '/entries?' + params.toString(), {
 | 
			
		||||
    headers: getAuthHeaders(auth),
 | 
			
		||||
  })
 | 
			
		||||
  return (await response.json()) as EntriesResponse
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import { type Class } from '@/api/classroom_compliance';
 | 
			
		||||
import { type Class } from '@/api/classroom_compliance'
 | 
			
		||||
 | 
			
		||||
defineProps<{
 | 
			
		||||
  cls: Class
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,22 +1,34 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import { getClass, getStudents, type Class, type Student } from '@/api/classroom_compliance';
 | 
			
		||||
import { useAuthStore } from '@/stores/auth';
 | 
			
		||||
import { onMounted, ref, type Ref } from 'vue';
 | 
			
		||||
import EntriesTable from '@/apps/classroom_compliance/EntriesTable.vue';
 | 
			
		||||
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 ConfirmDialog from '@/components/ConfirmDialog.vue'
 | 
			
		||||
import { ClassroomComplianceAPIClient, type Class } from '@/api/classroom_compliance'
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  id: string
 | 
			
		||||
}>()
 | 
			
		||||
const authStore = useAuthStore()
 | 
			
		||||
const router = useRouter()
 | 
			
		||||
const cls: Ref<Class | null> = ref(null)
 | 
			
		||||
const students: Ref<Student[]> = ref([])
 | 
			
		||||
const apiClient = new ClassroomComplianceAPIClient(authStore)
 | 
			
		||||
 | 
			
		||||
const deleteClassDialog = useTemplateRef('deleteClassDialog')
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  const idNumber = parseInt(props.id, 10)
 | 
			
		||||
  cls.value = await getClass(authStore.getBasicAuth(), idNumber)
 | 
			
		||||
  getStudents(authStore.getBasicAuth(), idNumber).then(r => {
 | 
			
		||||
    students.value = r
 | 
			
		||||
  })
 | 
			
		||||
  cls.value = await apiClient.getClass(idNumber).handleErrorsWithAlert()
 | 
			
		||||
  if (!cls.value) {
 | 
			
		||||
    await router.back()
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
async function deleteThisClass() {
 | 
			
		||||
  if (!cls.value || !(await deleteClassDialog.value?.show())) return
 | 
			
		||||
  await apiClient.deleteClass(cls.value.id).handleErrorsWithAlert()
 | 
			
		||||
  await router.replace('/classroom-compliance')
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <div v-if="cls">
 | 
			
		||||
| 
						 | 
				
			
			@ -26,12 +38,22 @@ onMounted(async () => {
 | 
			
		|||
    <div>
 | 
			
		||||
      <div>
 | 
			
		||||
        <span>Actions: </span>
 | 
			
		||||
        <button type="button">Add Student - WIP</button>
 | 
			
		||||
        <button type="button">Delete this Class</button>
 | 
			
		||||
        <RouterLink :to="'/classroom-compliance/classes/' + cls.id + '/edit-student'"
 | 
			
		||||
          >Add Student</RouterLink
 | 
			
		||||
        >
 | 
			
		||||
        <button type="button" @click="deleteThisClass">Delete this Class</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <EntriesTable :classId="cls.id" />
 | 
			
		||||
 | 
			
		||||
    <ConfirmDialog ref="deleteClassDialog">
 | 
			
		||||
      <p>
 | 
			
		||||
        Are you sure you want to delete this class? All data associated with it (settings, students,
 | 
			
		||||
        entries, grades, etc.) will be <strong>permanently deleted</strong>, and deleted data is not
 | 
			
		||||
        recoverable.
 | 
			
		||||
      </p>
 | 
			
		||||
    </ConfirmDialog>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,17 +1,23 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import { type Class, getClasses } from '@/api/classroom_compliance';
 | 
			
		||||
import { useAuthStore } from '@/stores/auth';
 | 
			
		||||
import { type Ref, ref, onMounted } from 'vue';
 | 
			
		||||
import ClassItem from '@/apps/classroom_compliance/ClassItem.vue';
 | 
			
		||||
import { ClassroomComplianceAPIClient, type Class } from '@/api/classroom_compliance'
 | 
			
		||||
import { useAuthStore } from '@/stores/auth'
 | 
			
		||||
import { type Ref, ref, onMounted } from 'vue'
 | 
			
		||||
import ClassItem from '@/apps/classroom_compliance/ClassItem.vue'
 | 
			
		||||
 | 
			
		||||
const classes: Ref<Class[]> = ref([])
 | 
			
		||||
 | 
			
		||||
const authStore = useAuthStore()
 | 
			
		||||
const apiClient = new ClassroomComplianceAPIClient(authStore)
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  classes.value = await getClasses(authStore.getBasicAuth())
 | 
			
		||||
  classes.value = (await apiClient.getClasses().handleErrorsWithAlert()) ?? []
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <ClassItem v-for="cls in classes" :key="cls.id" :cls="cls" />
 | 
			
		||||
  <div>
 | 
			
		||||
    <div>
 | 
			
		||||
      <RouterLink to="/classroom-compliance/edit-class">Add Class</RouterLink>
 | 
			
		||||
    </div>
 | 
			
		||||
    <ClassItem v-for="cls in classes" :key="cls.id" :cls="cls" />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,78 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import { ClassroomComplianceAPIClient, type Class } from '@/api/classroom_compliance'
 | 
			
		||||
import { useAuthStore } from '@/stores/auth'
 | 
			
		||||
import { onMounted, ref, type Ref } from 'vue'
 | 
			
		||||
import { useRoute, useRouter } from 'vue-router'
 | 
			
		||||
 | 
			
		||||
const authStore = useAuthStore()
 | 
			
		||||
const route = useRoute()
 | 
			
		||||
const router = useRouter()
 | 
			
		||||
const cls: Ref<Class | null> = ref(null)
 | 
			
		||||
 | 
			
		||||
interface ClassFormData {
 | 
			
		||||
  number: number
 | 
			
		||||
  schoolYear: string
 | 
			
		||||
}
 | 
			
		||||
const formData: Ref<ClassFormData> = ref({ number: 1, schoolYear: '2024-2025' })
 | 
			
		||||
const apiClient = new ClassroomComplianceAPIClient(authStore)
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  if ('classId' in route.query && typeof route.query.classId === 'string') {
 | 
			
		||||
    const classId = parseInt(route.query.classId, 10)
 | 
			
		||||
    cls.value = await apiClient.getClass(classId).handleErrorsWithAlert()
 | 
			
		||||
    if (!cls.value) {
 | 
			
		||||
      await router.back()
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    formData.value.number = cls.value.number
 | 
			
		||||
    formData.value.schoolYear = cls.value.schoolYear
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
async function submitForm() {
 | 
			
		||||
  if (cls.value) {
 | 
			
		||||
    // TODO: Update class
 | 
			
		||||
  } else {
 | 
			
		||||
    const result = await apiClient
 | 
			
		||||
      .createClass(formData.value.number, formData.value.schoolYear)
 | 
			
		||||
      .handleErrorsWithAlert()
 | 
			
		||||
    if (result) {
 | 
			
		||||
      await router.replace(`/classroom-compliance/classes/${result.id}`)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function resetForm() {
 | 
			
		||||
  if (cls.value) {
 | 
			
		||||
    formData.value.number = cls.value.number
 | 
			
		||||
    formData.value.schoolYear = cls.value.schoolYear
 | 
			
		||||
  } else {
 | 
			
		||||
    formData.value.number = 1
 | 
			
		||||
    formData.value.schoolYear = '2024-2025'
 | 
			
		||||
  }
 | 
			
		||||
  router.back()
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <h2>
 | 
			
		||||
      <span v-if="cls" v-text="'Edit Class ' + cls.id"></span>
 | 
			
		||||
      <span v-if="!cls">Add New Class</span>
 | 
			
		||||
    </h2>
 | 
			
		||||
 | 
			
		||||
    <form @submit.prevent="submitForm" @reset.prevent="resetForm">
 | 
			
		||||
      <div>
 | 
			
		||||
        <label for="number-input">Number</label>
 | 
			
		||||
        <input id="number-input" type="number" v-model="formData.number" required />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div>
 | 
			
		||||
        <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>
 | 
			
		||||
        <button type="submit">Save</button>
 | 
			
		||||
        <button type="reset">Cancel</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </form>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,117 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import {
 | 
			
		||||
  ClassroomComplianceAPIClient,
 | 
			
		||||
  type Class,
 | 
			
		||||
  type Student,
 | 
			
		||||
  type StudentDataPayload,
 | 
			
		||||
} from '@/api/classroom_compliance'
 | 
			
		||||
import { useAuthStore } from '@/stores/auth'
 | 
			
		||||
import { onMounted, ref, type Ref } from 'vue'
 | 
			
		||||
import { useRoute, useRouter } from 'vue-router'
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  classId: string
 | 
			
		||||
}>()
 | 
			
		||||
const authStore = useAuthStore()
 | 
			
		||||
const route = useRoute()
 | 
			
		||||
const router = useRouter()
 | 
			
		||||
const cls: Ref<Class | null> = ref(null)
 | 
			
		||||
const student: Ref<Student | null> = ref(null)
 | 
			
		||||
const apiClient = new ClassroomComplianceAPIClient(authStore)
 | 
			
		||||
 | 
			
		||||
interface StudentFormData {
 | 
			
		||||
  name: string
 | 
			
		||||
  deskNumber: number | null
 | 
			
		||||
  removed: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const formData: Ref<StudentFormData> = ref({ name: '', deskNumber: null, removed: false })
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  const classIdNumber = parseInt(props.classId, 10)
 | 
			
		||||
  cls.value = await apiClient.getClass(classIdNumber).handleErrorsWithAlert()
 | 
			
		||||
  if (!cls.value) {
 | 
			
		||||
    await router.replace('/classroom-compliance')
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  if ('studentId' in route.query && typeof route.query.studentId === 'string') {
 | 
			
		||||
    const studentId = parseInt(route.query.studentId, 10)
 | 
			
		||||
    student.value = await apiClient.getStudent(classIdNumber, studentId).handleErrorsWithAlert()
 | 
			
		||||
    if (!student.value) {
 | 
			
		||||
      await router.replace(`/classroom-compliance/classes/${classIdNumber}`)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    formData.value.name = student.value.name
 | 
			
		||||
    formData.value.deskNumber = student.value.deskNumber
 | 
			
		||||
    formData.value.removed = student.value.removed
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
async function submitForm() {
 | 
			
		||||
  const classId = parseInt(props.classId, 10)
 | 
			
		||||
  const data: StudentDataPayload = {
 | 
			
		||||
    name: formData.value.name,
 | 
			
		||||
    deskNumber: formData.value.deskNumber ?? 0,
 | 
			
		||||
    removed: formData.value.removed,
 | 
			
		||||
  }
 | 
			
		||||
  if (student.value) {
 | 
			
		||||
    const updatedStudent = await apiClient
 | 
			
		||||
      .updateStudent(classId, student.value.id, data)
 | 
			
		||||
      .handleErrorsWithAlert()
 | 
			
		||||
    if (updatedStudent) {
 | 
			
		||||
      await router.replace(`/classroom-compliance/classes/${classId}/students/${updatedStudent.id}`)
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    const newStudent = await apiClient.createStudent(classId, data).handleErrorsWithAlert()
 | 
			
		||||
    if (newStudent) {
 | 
			
		||||
      await router.replace(`/classroom-compliance/classes/${classId}/students/${newStudent.id}`)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function resetForm() {
 | 
			
		||||
  if (student.value) {
 | 
			
		||||
    formData.value = {
 | 
			
		||||
      name: student.value.name,
 | 
			
		||||
      deskNumber: student.value.deskNumber,
 | 
			
		||||
      removed: student.value.removed,
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    formData.value = {
 | 
			
		||||
      name: '',
 | 
			
		||||
      deskNumber: null,
 | 
			
		||||
      removed: false,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  router.back()
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <div v-if="cls">
 | 
			
		||||
    <h2>
 | 
			
		||||
      <span v-if="student" v-text="'Edit ' + student.name"></span>
 | 
			
		||||
      <span v-if="!student">Add New Student</span>
 | 
			
		||||
    </h2>
 | 
			
		||||
 | 
			
		||||
    <p>From class <span v-text="cls.number + ', ' + cls.schoolYear"></span></p>
 | 
			
		||||
 | 
			
		||||
    <form @submit.prevent="submitForm" @reset.prevent="resetForm">
 | 
			
		||||
      <div>
 | 
			
		||||
        <label for="name-input">Name</label>
 | 
			
		||||
        <input id="name-input" type="text" v-model="formData.name" required />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div>
 | 
			
		||||
        <label for="desk-input">Desk Number</label>
 | 
			
		||||
        <input id="desk-input" type="number" v-model="formData.deskNumber" />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div>
 | 
			
		||||
        <label for="removed-checkbox">Removed</label>
 | 
			
		||||
        <input id="removed-checkbox" type="checkbox" v-model="formData.removed" />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div>
 | 
			
		||||
        <button type="submit">Save</button>
 | 
			
		||||
        <button type="reset">Cancel</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </form>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,18 +1,33 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import { getEntries, type EntryResponseStudent } from '@/api/classroom_compliance';
 | 
			
		||||
import { useAuthStore } from '@/stores/auth';
 | 
			
		||||
import { onMounted, ref, type Ref } from 'vue';
 | 
			
		||||
import EntryTableCell from './EntryTableCell.vue';
 | 
			
		||||
import {
 | 
			
		||||
  ClassroomComplianceAPIClient,
 | 
			
		||||
  getDefaultEntry,
 | 
			
		||||
  type EntriesPayload,
 | 
			
		||||
  type EntriesPayloadStudent,
 | 
			
		||||
  type EntriesResponseStudent,
 | 
			
		||||
} from '@/api/classroom_compliance'
 | 
			
		||||
import { useAuthStore } from '@/stores/auth'
 | 
			
		||||
import { computed, onMounted, ref, type Ref } from 'vue'
 | 
			
		||||
import EntryTableCell from './EntryTableCell.vue'
 | 
			
		||||
import { RouterLink } from 'vue-router'
 | 
			
		||||
 | 
			
		||||
const authStore = useAuthStore()
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  classId: number
 | 
			
		||||
}>()
 | 
			
		||||
const apiClient = new ClassroomComplianceAPIClient(authStore)
 | 
			
		||||
 | 
			
		||||
const students: Ref<EntryResponseStudent[]> = ref([])
 | 
			
		||||
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())
 | 
			
		||||
 | 
			
		||||
const entriesChangedSinceLastSave = computed(() => {
 | 
			
		||||
  return lastSaveState.value === null || lastSaveState.value !== JSON.stringify(students.value)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  toDate.value.setHours(0, 0, 0, 0)
 | 
			
		||||
  fromDate.value.setHours(0, 0, 0, 0)
 | 
			
		||||
| 
						 | 
				
			
			@ -21,14 +36,20 @@ onMounted(async () => {
 | 
			
		|||
})
 | 
			
		||||
 | 
			
		||||
async function loadEntries() {
 | 
			
		||||
  const entries = await getEntries(
 | 
			
		||||
    authStore.getBasicAuth(),
 | 
			
		||||
    props.classId,
 | 
			
		||||
    fromDate.value,
 | 
			
		||||
    toDate.value
 | 
			
		||||
  )
 | 
			
		||||
  students.value = entries.students
 | 
			
		||||
  dates.value = entries.dates
 | 
			
		||||
  const entries = await apiClient
 | 
			
		||||
    .getEntries(props.classId, fromDate.value, toDate.value)
 | 
			
		||||
    .handleErrorsWithAlert()
 | 
			
		||||
  if (entries) {
 | 
			
		||||
    students.value = entries.students
 | 
			
		||||
    lastSaveState.value = JSON.stringify(entries.students)
 | 
			
		||||
    lastSaveStateTimestamp.value = Date.now()
 | 
			
		||||
    dates.value = entries.dates
 | 
			
		||||
  } else {
 | 
			
		||||
    students.value = []
 | 
			
		||||
    lastSaveState.value = null
 | 
			
		||||
    lastSaveStateTimestamp.value = Date.now()
 | 
			
		||||
    dates.value = []
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function shiftDateRange(days: number) {
 | 
			
		||||
| 
						 | 
				
			
			@ -41,30 +62,113 @@ async function showPreviousDay() {
 | 
			
		|||
  await loadEntries()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function showToday() {
 | 
			
		||||
  toDate.value = new Date()
 | 
			
		||||
  toDate.value.setHours(0, 0, 0, 0)
 | 
			
		||||
  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)
 | 
			
		||||
  await loadEntries()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function saveEdits() {
 | 
			
		||||
  if (!lastSaveState.value) {
 | 
			
		||||
    console.warn('No lastSaveState, cannot determine what edits were made.')
 | 
			
		||||
    await loadEntries()
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const payload: EntriesPayload = { students: [] }
 | 
			
		||||
  // Get a list of edits which have changed.
 | 
			
		||||
  const lastSaveStateObj: EntriesResponseStudent[] = JSON.parse(lastSaveState.value)
 | 
			
		||||
  for (let i = 0; i < students.value.length; i++) {
 | 
			
		||||
    const student: EntriesResponseStudent = students.value[i]
 | 
			
		||||
    const studentPayload: EntriesPayloadStudent = { id: student.id, entries: {} }
 | 
			
		||||
    for (const [dateStr, entry] of Object.entries(student.entries)) {
 | 
			
		||||
      const lastSaveStateEntry = lastSaveStateObj[i].entries[dateStr]
 | 
			
		||||
      if (JSON.stringify(lastSaveStateEntry) !== JSON.stringify(entry)) {
 | 
			
		||||
        studentPayload.entries[dateStr] = JSON.parse(JSON.stringify(entry))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (Object.keys(studentPayload.entries).length > 0) {
 | 
			
		||||
      payload.students.push(studentPayload)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  await apiClient.saveEntries(props.classId, payload).handleErrorsWithAlert()
 | 
			
		||||
  await loadEntries()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function discardEdits() {
 | 
			
		||||
  if (lastSaveState.value) {
 | 
			
		||||
    students.value = JSON.parse(lastSaveState.value)
 | 
			
		||||
  } else {
 | 
			
		||||
    await loadEntries()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getDate(dateStr: string): Date {
 | 
			
		||||
  const year = parseInt(dateStr.substring(0, 4), 10)
 | 
			
		||||
  const month = parseInt(dateStr.substring(5, 7), 10)
 | 
			
		||||
  const day = parseInt(dateStr.substring(8, 10), 10)
 | 
			
		||||
  return new Date(year, month - 1, day)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getWeekday(date: Date): string {
 | 
			
		||||
  const names = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
 | 
			
		||||
  return names[date.getDay()]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addAllEntriesForDate(dateStr: string) {
 | 
			
		||||
  for (let i = 0; i < students.value.length; i++) {
 | 
			
		||||
    const student = students.value[i]
 | 
			
		||||
    if (student.removed) continue
 | 
			
		||||
    if (!(dateStr in student.entries) || student.entries[dateStr] === null) {
 | 
			
		||||
      student.entries[dateStr] = getDefaultEntry(dateStr)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <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>
 | 
			
		||||
      <button type="button" @click="saveEdits" :disabled="!entriesChangedSinceLastSave">
 | 
			
		||||
        Save
 | 
			
		||||
      </button>
 | 
			
		||||
      <button type="button" @click="discardEdits" :disabled="!entriesChangedSinceLastSave">
 | 
			
		||||
        Discard Edits
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
    <table class="entries-table">
 | 
			
		||||
      <thead>
 | 
			
		||||
        <tr>
 | 
			
		||||
          <th>Student</th>
 | 
			
		||||
          <th>Desk</th>
 | 
			
		||||
          <th v-for="date in dates" :key="date" v-text="date"></th>
 | 
			
		||||
          <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>
 | 
			
		||||
        </tr>
 | 
			
		||||
      </thead>
 | 
			
		||||
      <tbody>
 | 
			
		||||
        <tr v-for="student in students" :key="student.id">
 | 
			
		||||
          <td v-text="student.name" :class="{ 'student-removed': student.removed }"></td>
 | 
			
		||||
          <td :class="{ 'student-removed': student.removed }">
 | 
			
		||||
            <RouterLink :to="'/classroom-compliance/classes/' + classId + '/students/' + student.id">
 | 
			
		||||
              <span v-text="student.name"></span>
 | 
			
		||||
            </RouterLink>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td v-text="student.deskNumber"></td>
 | 
			
		||||
          <EntryTableCell v-for="(entry, date) in student.entries" :key="date" :entry="entry" />
 | 
			
		||||
          <EntryTableCell v-for="(entry, date) in student.entries" :key="date" v-model="student.entries[date]"
 | 
			
		||||
            :date-str="date" :last-save-state-timestamp="lastSaveStateTimestamp" />
 | 
			
		||||
        </tr>
 | 
			
		||||
      </tbody>
 | 
			
		||||
    </table>
 | 
			
		||||
| 
						 | 
				
			
			@ -87,4 +191,13 @@ async function showNextDay() {
 | 
			
		|||
.student-removed {
 | 
			
		||||
  background-color: lightgray;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.buttons-bar {
 | 
			
		||||
  margin-top: 0.5em;
 | 
			
		||||
  margin-bottom: 0.5em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.buttons-bar>button+button {
 | 
			
		||||
  margin-left: 0.5em;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,26 +1,103 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import type { EntryResponseItem } from '@/api/classroom_compliance';
 | 
			
		||||
import { getDefaultEntry, type Entry } from '@/api/classroom_compliance'
 | 
			
		||||
import { computed, onMounted, ref, watch, type Ref } from 'vue'
 | 
			
		||||
 | 
			
		||||
defineProps<{
 | 
			
		||||
  entry: EntryResponseItem | null
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  dateStr: string
 | 
			
		||||
  lastSaveStateTimestamp: number
 | 
			
		||||
}>()
 | 
			
		||||
 | 
			
		||||
const model = defineModel<Entry | null>({
 | 
			
		||||
  required: false,
 | 
			
		||||
})
 | 
			
		||||
const initialEntryJson: Ref<string> = ref('')
 | 
			
		||||
const previouslyRemovedEntry: Ref<Entry | null> = ref(null)
 | 
			
		||||
 | 
			
		||||
const entryChanged = computed(() => JSON.stringify(model.value) !== initialEntryJson.value)
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  initialEntryJson.value = JSON.stringify(model.value)
 | 
			
		||||
  watch(
 | 
			
		||||
    () => props.lastSaveStateTimestamp,
 | 
			
		||||
    () => {
 | 
			
		||||
      initialEntryJson.value = JSON.stringify(model.value)
 | 
			
		||||
      previouslyRemovedEntry.value = null
 | 
			
		||||
    },
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
function toggleAbsence() {
 | 
			
		||||
  if (model.value) {
 | 
			
		||||
    model.value.absent = !model.value.absent
 | 
			
		||||
    if (model.value.absent) {
 | 
			
		||||
      // Remove additional data if student is absent.
 | 
			
		||||
      model.value.phone = null
 | 
			
		||||
      model.value.behavior = null
 | 
			
		||||
    } else {
 | 
			
		||||
      // Populate default additional data if student is no longer absent.
 | 
			
		||||
      model.value.phone = { compliant: true }
 | 
			
		||||
      model.value.behavior = { rating: 3 }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function togglePhoneCompliance() {
 | 
			
		||||
  if (model.value && model.value.phone) {
 | 
			
		||||
    model.value.phone.compliant = !model.value.phone.compliant
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toggleBehaviorRating() {
 | 
			
		||||
  if (model.value && model.value.behavior) {
 | 
			
		||||
    model.value.behavior.rating = model.value.behavior.rating - 1
 | 
			
		||||
    if (model.value.behavior.rating < 1) {
 | 
			
		||||
      model.value.behavior.rating = 3
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function removeEntry() {
 | 
			
		||||
  if (model.value) {
 | 
			
		||||
    previouslyRemovedEntry.value = JSON.parse(JSON.stringify(model.value))
 | 
			
		||||
  }
 | 
			
		||||
  model.value = null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addEntry() {
 | 
			
		||||
  if (previouslyRemovedEntry.value) {
 | 
			
		||||
    model.value = JSON.parse(JSON.stringify(previouslyRemovedEntry.value))
 | 
			
		||||
  } else {
 | 
			
		||||
    model.value = getDefaultEntry(props.dateStr)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <td v-if="entry" :class="{ absent: entry.absent }">
 | 
			
		||||
    <span v-if="entry.absent">Absent</span>
 | 
			
		||||
    <div v-if="!entry.absent">
 | 
			
		||||
      <div class="status-item">
 | 
			
		||||
        <span v-if="entry.phone?.compliant">📱</span>
 | 
			
		||||
        <span v-if="!entry.phone?.compliant">📵</span>
 | 
			
		||||
  <td :class="{ absent: model?.absent, changed: entryChanged, 'missing-entry': !model }">
 | 
			
		||||
    <div v-if="model">
 | 
			
		||||
      <div class="status-item" @click="toggleAbsence">
 | 
			
		||||
        <span v-if="model.absent">Absent</span>
 | 
			
		||||
        <span v-if="!model.absent">Present</span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="status-item">
 | 
			
		||||
        <span v-if="entry.behavior?.rating === 3">😇</span>
 | 
			
		||||
        <span v-if="entry.behavior?.rating === 2">😐</span>
 | 
			
		||||
        <span v-if="entry.behavior?.rating === 1">😡</span>
 | 
			
		||||
      <div class="status-item" @click="togglePhoneCompliance" v-if="!model.absent">
 | 
			
		||||
        <span v-if="model.phone?.compliant">📱</span>
 | 
			
		||||
        <span v-if="!model.phone?.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>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="status-item" @click="removeEntry">
 | 
			
		||||
        <span>🗑️</span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div v-if="model === null">
 | 
			
		||||
      <div class="status-item" @click="addEntry">
 | 
			
		||||
        <span>➕</span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </td>
 | 
			
		||||
  <td v-if="entry === null" class="missing-entry"></td>
 | 
			
		||||
</template>
 | 
			
		||||
<style scoped>
 | 
			
		||||
td {
 | 
			
		||||
| 
						 | 
				
			
			@ -30,8 +107,7 @@ td {
 | 
			
		|||
 | 
			
		||||
.missing-entry {
 | 
			
		||||
  background-color: lightgray;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  font-style: italic;
 | 
			
		||||
  text-align: right;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.absent {
 | 
			
		||||
| 
						 | 
				
			
			@ -39,8 +115,16 @@ td {
 | 
			
		|||
  border: 1px solid black;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.changed {
 | 
			
		||||
  border: 2px solid orange !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.status-item {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.status-item+.status-item {
 | 
			
		||||
  margin-left: 0.25em;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,15 +1,17 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
</script>
 | 
			
		||||
<script setup lang="ts"></script>
 | 
			
		||||
<template>
 | 
			
		||||
  <main>
 | 
			
		||||
    <h1>Classroom Compliance</h1>
 | 
			
		||||
    <p>With this application, you can track each student's compliance to various classroom policies, like:</p>
 | 
			
		||||
    <p>
 | 
			
		||||
      With this application, you can track each student's compliance to various classroom policies,
 | 
			
		||||
      like:
 | 
			
		||||
    </p>
 | 
			
		||||
    <ul>
 | 
			
		||||
      <li>Attendance</li>
 | 
			
		||||
      <li>Phone Usage (or lack thereof)</li>
 | 
			
		||||
      <li>Behavior</li>
 | 
			
		||||
    </ul>
 | 
			
		||||
    <hr>
 | 
			
		||||
    <hr />
 | 
			
		||||
 | 
			
		||||
    <RouterView />
 | 
			
		||||
  </main>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,72 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import { ClassroomComplianceAPIClient, type Class, type Student } from '@/api/classroom_compliance'
 | 
			
		||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
 | 
			
		||||
import { useAuthStore } from '@/stores/auth'
 | 
			
		||||
import { onMounted, ref, useTemplateRef, type Ref } from 'vue'
 | 
			
		||||
import { useRouter } from 'vue-router'
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  classId: string
 | 
			
		||||
  studentId: string
 | 
			
		||||
}>()
 | 
			
		||||
const authStore = useAuthStore()
 | 
			
		||||
const router = useRouter()
 | 
			
		||||
const cls: Ref<Class | null> = ref(null)
 | 
			
		||||
const student: Ref<Student | null> = ref(null)
 | 
			
		||||
const apiClient = new ClassroomComplianceAPIClient(authStore)
 | 
			
		||||
const deleteConfirmDialog = useTemplateRef('deleteConfirmDialog')
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  const classIdNumber = parseInt(props.classId, 10)
 | 
			
		||||
  cls.value = await apiClient.getClass(classIdNumber).handleErrorsWithAlert()
 | 
			
		||||
  if (!cls.value) {
 | 
			
		||||
    await router.replace('/classroom-compliance')
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  const studentIdNumber = parseInt(props.studentId, 10)
 | 
			
		||||
  student.value = await apiClient.getStudent(classIdNumber, studentIdNumber).handleErrorsWithAlert()
 | 
			
		||||
  if (!student.value) {
 | 
			
		||||
    await router.replace(`/classroom-compliance/classes/${cls.value.id}`)
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
async function deleteThisStudent() {
 | 
			
		||||
  if (!cls.value || !student.value) return
 | 
			
		||||
  const choice = await deleteConfirmDialog.value?.show()
 | 
			
		||||
  if (!choice) return
 | 
			
		||||
  await apiClient.deleteStudent(cls.value.id, student.value.id)
 | 
			
		||||
  await router.replace(`/classroom-compliance/classes/${cls.value.id}`)
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <div v-if="student">
 | 
			
		||||
    <h2 v-text="student.name"></h2>
 | 
			
		||||
    <p>
 | 
			
		||||
      From
 | 
			
		||||
      <RouterLink :to="'/classroom-compliance/classes/' + classId">
 | 
			
		||||
        <span v-text="'class ' + cls?.number + ', ' + cls?.schoolYear"></span>
 | 
			
		||||
      </RouterLink>
 | 
			
		||||
    </p>
 | 
			
		||||
    <ul>
 | 
			
		||||
      <li>Internal ID: <span v-text="student.id"></span></li>
 | 
			
		||||
      <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>
 | 
			
		||||
    <button type="button" @click="deleteThisStudent">Delete</button>
 | 
			
		||||
 | 
			
		||||
    <ConfirmDialog ref="deleteConfirmDialog">
 | 
			
		||||
      <p>
 | 
			
		||||
        Are you sure you want to delete <span v-text="student.name"></span>? This will permanently
 | 
			
		||||
        delete all records for them.
 | 
			
		||||
      </p>
 | 
			
		||||
      <p>This <strong>cannot</strong> be undone!</p>
 | 
			
		||||
    </ConfirmDialog>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
<script setup lang="ts"></script>
 | 
			
		||||
<template>
 | 
			
		||||
  <dialog id="alert-dialog">
 | 
			
		||||
    <p id="alert-dialog-message"></p>
 | 
			
		||||
    <div>
 | 
			
		||||
      <button type="button" id="alert-dialog-close-button">Close</button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </dialog>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,54 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import { ref } from 'vue'
 | 
			
		||||
 | 
			
		||||
const dialog = ref<HTMLDialogElement>()
 | 
			
		||||
const result = ref(false)
 | 
			
		||||
 | 
			
		||||
function onCancelClicked() {
 | 
			
		||||
  result.value = false
 | 
			
		||||
  dialog.value?.close('false')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onConfirmClicked() {
 | 
			
		||||
  result.value = true
 | 
			
		||||
  dialog.value?.close('true')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function show(): Promise<boolean> {
 | 
			
		||||
  dialog.value?.showModal()
 | 
			
		||||
  return new Promise((resolve) => {
 | 
			
		||||
    dialog.value?.addEventListener('close', () => {
 | 
			
		||||
      if (dialog.value?.returnValue === 'true') {
 | 
			
		||||
        resolve(true)
 | 
			
		||||
      } else {
 | 
			
		||||
        resolve(false)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
  show,
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <dialog ref="dialog" method="dialog">
 | 
			
		||||
    <form>
 | 
			
		||||
      <slot></slot>
 | 
			
		||||
 | 
			
		||||
      <div class="confirm-dialog-buttons">
 | 
			
		||||
        <button @click.prevent="onConfirmClicked">Confirm</button>
 | 
			
		||||
        <button @click.prevent="onCancelClicked">Cancel</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </form>
 | 
			
		||||
  </dialog>
 | 
			
		||||
</template>
 | 
			
		||||
<style>
 | 
			
		||||
.confirm-dialog-buttons {
 | 
			
		||||
  text-align: right;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.confirm-dialog-buttons > button {
 | 
			
		||||
  margin-left: 0.5em;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +26,20 @@ const router = createRouter({
 | 
			
		|||
          component: () => import('@/apps/classroom_compliance/ClassView.vue'),
 | 
			
		||||
          props: true,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          path: 'edit-class',
 | 
			
		||||
          component: () => import('@/apps/classroom_compliance/EditClassView.vue'),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          path: 'classes/:classId/students/:studentId',
 | 
			
		||||
          component: () => import('@/apps/classroom_compliance/StudentView.vue'),
 | 
			
		||||
          props: true,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          path: 'classes/:classId/edit-student',
 | 
			
		||||
          component: () => import('@/apps/classroom_compliance/EditStudentView.vue'),
 | 
			
		||||
          props: true,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import { login } from '@/api/auth';
 | 
			
		||||
import { AuthenticationAPIClient } from '@/api/auth'
 | 
			
		||||
import { useAuthStore } from '@/stores/auth'
 | 
			
		||||
import { ref, type Ref } from 'vue'
 | 
			
		||||
import { useRouter } from 'vue-router'
 | 
			
		||||
| 
						 | 
				
			
			@ -12,14 +12,13 @@ interface Credentials {
 | 
			
		|||
const router = useRouter()
 | 
			
		||||
const authStore = useAuthStore()
 | 
			
		||||
const credentials: Ref<Credentials> = ref({ username: '', password: '' })
 | 
			
		||||
const apiClient = new AuthenticationAPIClient(authStore)
 | 
			
		||||
 | 
			
		||||
async function doLogin() {
 | 
			
		||||
  const user = await login(credentials.value.username, credentials.value.password)
 | 
			
		||||
  const user = await apiClient.login(credentials.value.username, credentials.value.password).handleErrorsWithAlert()
 | 
			
		||||
  if (user) {
 | 
			
		||||
    authStore.logIn(credentials.value.username, credentials.value.password, user)
 | 
			
		||||
    await router.replace('/')
 | 
			
		||||
  } else {
 | 
			
		||||
    console.warn('Invalid credentials.')
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue