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
|
CONSTRAINT unique_entry_per_date
|
||||||
UNIQUE(class_id, student_id, 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";
|
const CLASS_PATH = ROOT_PATH ~ "/classes/:classId:ulong";
|
||||||
handler.addMapping(Method.GET, CLASS_PATH, &getClass);
|
handler.addMapping(Method.GET, CLASS_PATH, &getClass);
|
||||||
handler.addMapping(Method.DELETE, CLASS_PATH, &deleteClass);
|
handler.addMapping(Method.DELETE, CLASS_PATH, &deleteClass);
|
||||||
|
handler.addMapping(Method.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.POST, CLASS_PATH ~ "/students", &createStudent);
|
||||||
handler.addMapping(Method.GET, CLASS_PATH ~ "/students", &getStudents);
|
handler.addMapping(Method.GET, CLASS_PATH ~ "/students", &getStudents);
|
||||||
|
|
|
@ -103,3 +103,41 @@ void deleteClass(ref HttpRequestContext ctx) {
|
||||||
ps.setUlong(2, user.id);
|
ps.setUlong(2, user.id);
|
||||||
ps.executeUpdate();
|
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
|
entryCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ClassNote {
|
||||||
|
id: number
|
||||||
|
classId: number
|
||||||
|
createdAt: number
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
export class ClassroomComplianceAPIClient extends APIClient {
|
export class ClassroomComplianceAPIClient extends APIClient {
|
||||||
constructor(authStore: AuthStoreType) {
|
constructor(authStore: AuthStoreType) {
|
||||||
super(BASE_URL, authStore)
|
super(BASE_URL, authStore)
|
||||||
|
@ -121,6 +128,18 @@ export class ClassroomComplianceAPIClient extends APIClient {
|
||||||
return super.delete(`/classes/${classId}`)
|
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[]> {
|
getStudents(classId: number): APIResponse<Student[]> {
|
||||||
return super.get(`/classes/${classId}/students`)
|
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 EntriesTable from '@/apps/classroom_compliance/EntriesTable.vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
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<{
|
const props = defineProps<{
|
||||||
id: string
|
id: string
|
||||||
|
@ -12,6 +13,8 @@ const props = defineProps<{
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const cls: Ref<Class | null> = ref(null)
|
const cls: Ref<Class | null> = ref(null)
|
||||||
|
const notes: Ref<ClassNote[]> = ref([])
|
||||||
|
const noteContent: Ref<string> = ref('')
|
||||||
const apiClient = new ClassroomComplianceAPIClient(authStore)
|
const apiClient = new ClassroomComplianceAPIClient(authStore)
|
||||||
|
|
||||||
const deleteClassDialog = useTemplateRef('deleteClassDialog')
|
const deleteClassDialog = useTemplateRef('deleteClassDialog')
|
||||||
|
@ -20,6 +23,7 @@ onMounted(() => {
|
||||||
apiClient.getClass(idNumber).handleErrorsWithAlert().then(result => {
|
apiClient.getClass(idNumber).handleErrorsWithAlert().then(result => {
|
||||||
if (result) {
|
if (result) {
|
||||||
cls.value = result
|
cls.value = result
|
||||||
|
refreshNotes()
|
||||||
} else {
|
} else {
|
||||||
router.back();
|
router.back();
|
||||||
}
|
}
|
||||||
|
@ -31,6 +35,32 @@ async function deleteThisClass() {
|
||||||
await apiClient.deleteClass(cls.value.id).handleErrorsWithAlert()
|
await apiClient.deleteClass(cls.value.id).handleErrorsWithAlert()
|
||||||
await router.replace('/classroom-compliance')
|
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>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div v-if="cls">
|
<div v-if="cls">
|
||||||
|
@ -44,6 +74,14 @@ async function deleteThisClass() {
|
||||||
<button type="button" @click="deleteThisClass">Delete this Class</button>
|
<button type="button" @click="deleteThisClass">Delete this Class</button>
|
||||||
</div>
|
</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" />
|
<EntriesTable :classId="cls.id" />
|
||||||
|
|
||||||
<!-- Confirmation dialog used for attempts at deleting this class. -->
|
<!-- Confirmation dialog used for attempts at deleting this class. -->
|
||||||
|
|
Loading…
Reference in New Issue