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