Added notes.
This commit is contained in:
parent
ac3196fb26
commit
45ac42aa80
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ClassNote[]> {
|
||||
return super.get(`/classes/${classId}/notes`)
|
||||
}
|
||||
|
||||
createClassNote(classId: number, content: string): APIResponse<ClassNote> {
|
||||
return super.post(`/classes/${classId}/notes`, { content: content })
|
||||
}
|
||||
|
||||
deleteClassNote(classId: number, noteId: number): APIResponse<void> {
|
||||
return super.delete(`/classes/${classId}/notes/${noteId}`)
|
||||
}
|
||||
|
||||
getStudents(classId: number): APIResponse<Student[]> {
|
||||
return super.get(`/classes/${classId}/students`)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
<script setup lang="ts">
|
||||
import { ClassroomComplianceAPIClient, type ClassNote } from '@/api/classroom_compliance';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
const props = defineProps<{
|
||||
note: ClassNote
|
||||
}>()
|
||||
const emit = defineEmits(['noteDeleted'])
|
||||
const authStore = useAuthStore()
|
||||
const apiClient = new ClassroomComplianceAPIClient(authStore)
|
||||
|
||||
async function deleteNote() {
|
||||
const result = await apiClient.deleteClassNote(props.note.classId, props.note.id).handleErrorsWithAlert()
|
||||
if (result !== null) {
|
||||
emit('noteDeleted')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="class-note-item">
|
||||
<p class="class-note-item-content">{{ note.content }}</p>
|
||||
<div class="class-note-item-attributes-container">
|
||||
<p class="class-note-item-timestamp">{{ new Date(note.createdAt).toLocaleString() }}</p>
|
||||
<button class="class-note-item-delete-button" @click="deleteNote()">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.class-note-item {
|
||||
padding: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
background-color: rgb(49, 49, 49);
|
||||
border-radius: 10px;
|
||||
font-size: smaller;
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.class-note-item-content {
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.class-note-item-attributes-container {
|
||||
min-width: 200px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.class-note-item-timestamp {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.class-note-item-delete-button {
|
||||
background-color: inherit;
|
||||
border: none;
|
||||
color: rgb(207, 207, 207);
|
||||
}
|
||||
|
||||
.class-note-item-delete-button:hover {
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
|
@ -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<Class | null> = ref(null)
|
||||
const notes: Ref<ClassNote[]> = ref([])
|
||||
const noteContent: Ref<string> = 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 = []
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div v-if="cls">
|
||||
|
@ -44,6 +74,14 @@ async function deleteThisClass() {
|
|||
<button type="button" @click="deleteThisClass">Delete this Class</button>
|
||||
</div>
|
||||
|
||||
<h3>Notes</h3>
|
||||
<form @submit.prevent="submitNote">
|
||||
<textarea style="min-height: 50px; min-width: 300px;" maxlength="2000" minlength="1"
|
||||
v-model="noteContent"></textarea>
|
||||
<button style="vertical-align: top; margin-left: 0.5em;" type="submit">Add Note</button>
|
||||
</form>
|
||||
<ClassNoteItem v-for="note in notes" :key="note.id" :note="note" @noteDeleted="refreshNotes()" />
|
||||
|
||||
<EntriesTable :classId="cls.id" />
|
||||
|
||||
<!-- Confirmation dialog used for attempts at deleting this class. -->
|
||||
|
|
Loading…
Reference in New Issue