diff --git a/api/schema/classroom_compliance.sql b/api/schema/classroom_compliance.sql index eaf109c..b1315e2 100644 --- a/api/schema/classroom_compliance.sql +++ b/api/schema/classroom_compliance.sql @@ -42,3 +42,13 @@ CREATE TABLE classroom_compliance_entry ( CONSTRAINT unique_entry_per_date UNIQUE(class_id, student_id, date) ); + +CREATE TABLE classroom_compliance_class_note ( + id BIGSERIAL PRIMARY KEY, + class_id BIGINT NOT NULL + REFERENCES classroom_compliance_class(id) + ON UPDATE CASCADE ON DELETE CASCADE, + created_at BIGINT NOT NULL + DEFAULT EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000, + content VARCHAR(2000) NOT NULL +); diff --git a/api/source/api_modules/classroom_compliance/api.d b/api/source/api_modules/classroom_compliance/api.d index e78e9bd..32f976d 100644 --- a/api/source/api_modules/classroom_compliance/api.d +++ b/api/source/api_modules/classroom_compliance/api.d @@ -15,6 +15,9 @@ void registerApiEndpoints(PathHandler handler) { const CLASS_PATH = ROOT_PATH ~ "/classes/:classId:ulong"; handler.addMapping(Method.GET, CLASS_PATH, &getClass); handler.addMapping(Method.DELETE, CLASS_PATH, &deleteClass); + handler.addMapping(Method.GET, CLASS_PATH ~ "/notes", &getClassNotes); + handler.addMapping(Method.POST, CLASS_PATH ~ "/notes", &createClassNote); + handler.addMapping(Method.DELETE, CLASS_PATH ~ "/notes/:noteId:ulong", &deleteClassNote); handler.addMapping(Method.POST, CLASS_PATH ~ "/students", &createStudent); handler.addMapping(Method.GET, CLASS_PATH ~ "/students", &getStudents); diff --git a/api/source/api_modules/classroom_compliance/api_class.d b/api/source/api_modules/classroom_compliance/api_class.d index f356b65..f763baa 100644 --- a/api/source/api_modules/classroom_compliance/api_class.d +++ b/api/source/api_modules/classroom_compliance/api_class.d @@ -103,3 +103,41 @@ void deleteClass(ref HttpRequestContext ctx) { ps.setUlong(2, user.id); ps.executeUpdate(); } + +void getClassNotes(ref HttpRequestContext ctx) { + Connection conn = getDb(); + scope(exit) conn.close(); + User user = getUserOrThrow(ctx, conn); + auto cls = getClassOrThrow(ctx, conn, user); + const query = "SELECT * FROM classroom_compliance_class_note WHERE class_id = ? ORDER BY created_at DESC"; + const notes = findAll(conn, query, &ClassroomComplianceClassNote.parse, cls.id); + writeJsonBody(ctx, notes); +} + +void createClassNote(ref HttpRequestContext ctx) { + Connection conn = getDb(); + scope(exit) conn.close(); + User user = getUserOrThrow(ctx, conn); + auto cls = getClassOrThrow(ctx, conn, user); + struct Payload {string content;} + Payload payload = readJsonPayload!(Payload)(ctx); + const query = "INSERT INTO classroom_compliance_class_note (class_id, content) VALUES (?, ?) RETURNING id"; + ulong id = insertOne(conn, query, cls.id, payload.content); + const note = findOne( + conn, + "SELECT * FROM classroom_compliance_class_note WHERE id = ?", + &ClassroomComplianceClassNote.parse, + id + ); + writeJsonBody(ctx, note); +} + +void deleteClassNote(ref HttpRequestContext ctx) { + Connection conn = getDb(); + scope(exit) conn.close(); + User user = getUserOrThrow(ctx, conn); + auto cls = getClassOrThrow(ctx, conn, user); + ulong noteId = ctx.request.getPathParamAs!ulong("noteId"); + const query = "DELETE FROM classroom_compliance_class_note WHERE class_id = ? AND id = ?"; + update(conn, query, cls.id, noteId); +} diff --git a/api/source/api_modules/classroom_compliance/model.d b/api/source/api_modules/classroom_compliance/model.d index f2e6613..88b122c 100644 --- a/api/source/api_modules/classroom_compliance/model.d +++ b/api/source/api_modules/classroom_compliance/model.d @@ -97,4 +97,18 @@ struct ClassroomComplianceEntry { } } +struct ClassroomComplianceClassNote { + const ulong id; + const ulong classId; + const ulong createdAt; + const string content; + static ClassroomComplianceClassNote parse(DataSetReader r) { + return ClassroomComplianceClassNote( + r.getUlong(1), + r.getUlong(2), + r.getUlong(3), + r.getString(4) + ); + } +} diff --git a/app/src/api/classroom_compliance.ts b/app/src/api/classroom_compliance.ts index d20cb13..a1bae7a 100644 --- a/app/src/api/classroom_compliance.ts +++ b/app/src/api/classroom_compliance.ts @@ -100,6 +100,13 @@ export interface StudentStatisticsOverview { entryCount: number } +export interface ClassNote { + id: number + classId: number + createdAt: number + content: string +} + export class ClassroomComplianceAPIClient extends APIClient { constructor(authStore: AuthStoreType) { super(BASE_URL, authStore) @@ -121,6 +128,18 @@ export class ClassroomComplianceAPIClient extends APIClient { return super.delete(`/classes/${classId}`) } + getClassNotes(classId: number): APIResponse { + return super.get(`/classes/${classId}/notes`) + } + + createClassNote(classId: number, content: string): APIResponse { + return super.post(`/classes/${classId}/notes`, { content: content }) + } + + deleteClassNote(classId: number, noteId: number): APIResponse { + return super.delete(`/classes/${classId}/notes/${noteId}`) + } + getStudents(classId: number): APIResponse { return super.get(`/classes/${classId}/students`) } diff --git a/app/src/apps/classroom_compliance/ClassNoteItem.vue b/app/src/apps/classroom_compliance/ClassNoteItem.vue new file mode 100644 index 0000000..c9465dd --- /dev/null +++ b/app/src/apps/classroom_compliance/ClassNoteItem.vue @@ -0,0 +1,64 @@ + + + diff --git a/app/src/apps/classroom_compliance/ClassView.vue b/app/src/apps/classroom_compliance/ClassView.vue index 882bf85..fc82057 100644 --- a/app/src/apps/classroom_compliance/ClassView.vue +++ b/app/src/apps/classroom_compliance/ClassView.vue @@ -4,7 +4,8 @@ import { onMounted, ref, useTemplateRef, type Ref } from 'vue' import EntriesTable from '@/apps/classroom_compliance/EntriesTable.vue' import { useRouter } from 'vue-router' import ConfirmDialog from '@/components/ConfirmDialog.vue' -import { ClassroomComplianceAPIClient, type Class } from '@/api/classroom_compliance' +import { ClassroomComplianceAPIClient, type Class, type ClassNote } from '@/api/classroom_compliance' +import ClassNoteItem from './ClassNoteItem.vue' const props = defineProps<{ id: string @@ -12,6 +13,8 @@ const props = defineProps<{ const authStore = useAuthStore() const router = useRouter() const cls: Ref = ref(null) +const notes: Ref = ref([]) +const noteContent: Ref = ref('') const apiClient = new ClassroomComplianceAPIClient(authStore) const deleteClassDialog = useTemplateRef('deleteClassDialog') @@ -20,6 +23,7 @@ onMounted(() => { apiClient.getClass(idNumber).handleErrorsWithAlert().then(result => { if (result) { cls.value = result + refreshNotes() } else { router.back(); } @@ -31,6 +35,32 @@ async function deleteThisClass() { await apiClient.deleteClass(cls.value.id).handleErrorsWithAlert() await router.replace('/classroom-compliance') } + +async function submitNote() { + if (noteContent.value.trim().length < 1 || !cls.value) return + const note = await apiClient.createClassNote(cls.value?.id, noteContent.value).handleErrorsWithAlert() + if (note) { + noteContent.value = '' + const updatedNotes = await apiClient.getClassNotes(cls.value?.id).handleErrorsWithAlert() + if (updatedNotes !== null) { + notes.value = updatedNotes + } + } +} + +function refreshNotes() { + if (cls.value === null) { + notes.value = [] + return + } + apiClient.getClassNotes(cls.value.id).handleErrorsWithAlert().then(notesResult => { + if (notesResult !== null) { + notes.value = notesResult + } else { + notes.value = [] + } + }) +}