From 36bf196cb8b5238ff6f6d2eeed82383f5bb48dd8 Mon Sep 17 00:00:00 2001 From: Maria Kristin Olafsdottir Date: Thu, 30 Apr 2026 15:12:25 +0200 Subject: [PATCH] feat: classroom control (#69) * allow editing * add missing pages * update fetchJson to accept empty json * fix remove and approve pupils * show already added teachers * add task page with creation, deleting and editing * remove comments * add edit and create task page * add pending page * move store to model, update classroom table * add confirmation modal * add option to delete classroom * remove + * i18n * i18n * update task view and creation * move task related classroom components to own folder * update tests * update task view * update task view to match stops better * update task creation and editing * styling * update task view * route to classroom details * allow seeing unpublished tasks * update join routing to determine path based on membership * add option to publish and delete tasks * add task creation and updating to task store * remove unused file * i18n * reload en.ts * update routing * update routing * add notebook back to pupiltable * add notebook back to pupiltable * fix test * fix build errors * fix routing for join page * i18n * remove double route * route to task slug * fix add teacher * fix merge breaks * formatting * formatting * change id to classroomId to match rest * comment * remove comment * change id to classroomId * remove double add teacher logic * fixes * update names * fix test * format * add back targets * format * add memebrshipStatus where missing --------- Co-authored-by: Maria <145002050+marikolafs@users.noreply.github.com> --- src/app/__tests__/AppShell.spec.ts | 2 + src/app/router/__tests__/auth-routing.spec.ts | 5 + src/app/router/index.ts | 88 +++--- src/auth/api/auth.api.ts | 12 +- src/auth/model/auth.types.ts | 1 + src/auth/pages/__tests__/login-pages.spec.ts | 1 + .../__tests__/PupilAgentCard.spec.ts | 1 + .../model/__tests__/avatar.store.spec.ts | 2 + .../_tests_/classrooms.store.spec.ts | 44 ++- src/classrooms/_tests_/pupil-routing.spec.ts | 2 + src/classrooms/api/classrooms.api.ts | 70 +++-- src/classrooms/components/ClassroomTable.vue | 33 +- src/classrooms/components/ConfirmModal.vue | 60 ++++ src/classrooms/components/PupilTable.vue | 259 +++++++++++----- .../__tests__/ClassroomTable.spec.ts | 8 +- .../components/__tests__/PupilTable.spec.ts | 15 +- .../components/composables/UseTeacherStops.ts | 19 ++ .../components/tasks/ClassroomTaskSidebar.vue | 44 +++ .../components/tasks/TaskItemsEditor.vue | 60 ++++ .../components/tasks/TaskMetaEditor.vue | 78 +++++ .../components/tasks/TaskOptionEditor.vue | 91 ++++++ .../{store => model}/classrooms.store.ts | 60 +++- src/classrooms/pages/ClassroomDetails.vue | 198 ++++++++++-- src/classrooms/pages/ClassroomMystery.vue | 1 + .../pages/ClassroomNotifications.vue | 1 + src/classrooms/pages/ClassroomTasks.vue | 281 ++++++++++++++++++ src/classrooms/pages/EditClassroomTask.vue | 177 +++++++++++ src/classrooms/pages/JoinClassroom.vue | 6 +- src/classrooms/pages/NewClassroom.vue | 2 +- src/classrooms/pages/NewClassroomTask.vue | 207 +++++++++++++ src/classrooms/pages/PendingApproval.vue | 67 +++++ .../pages/__tests__/ClassroomDetails.spec.ts | 10 +- src/classrooms/routes.ts | 34 ++- .../__tests__/LeaderboardComponent.spec.ts | 1 + src/locales/nb.ts | 72 +++++ src/map/model/__tests__/map.store.spec.ts | 1 + .../model/__tests__/mystery.store.spec.ts | 2 + .../__tests__/NotebookComponent.spec.ts | 1 + .../composables/useNotificationsInbox.ts | 4 +- .../pages/__tests__/PupilProfilePage.spec.ts | 1 + .../pages/__tests__/PupilHomePage.spec.ts | 2 + .../__tests__/PupilOnboardingPage.spec.ts | 6 + src/stops/api/stops.api.ts | 105 ++++++- src/stops/model/__tests__/stop.store.spec.ts | 1 + src/stops/model/task.store.ts | 111 +++++++ .../__tests__/teacher-mystery.store.spec.ts | 2 + 46 files changed, 2033 insertions(+), 215 deletions(-) create mode 100644 src/classrooms/components/ConfirmModal.vue create mode 100644 src/classrooms/components/composables/UseTeacherStops.ts create mode 100644 src/classrooms/components/tasks/ClassroomTaskSidebar.vue create mode 100644 src/classrooms/components/tasks/TaskItemsEditor.vue create mode 100644 src/classrooms/components/tasks/TaskMetaEditor.vue create mode 100644 src/classrooms/components/tasks/TaskOptionEditor.vue rename src/classrooms/{store => model}/classrooms.store.ts (77%) create mode 100644 src/classrooms/pages/ClassroomMystery.vue create mode 100644 src/classrooms/pages/ClassroomNotifications.vue create mode 100644 src/classrooms/pages/ClassroomTasks.vue create mode 100644 src/classrooms/pages/EditClassroomTask.vue create mode 100644 src/classrooms/pages/NewClassroomTask.vue create mode 100644 src/classrooms/pages/PendingApproval.vue create mode 100644 src/stops/model/task.store.ts diff --git a/src/app/__tests__/AppShell.spec.ts b/src/app/__tests__/AppShell.spec.ts index c300363c..aee8da2a 100644 --- a/src/app/__tests__/AppShell.spec.ts +++ b/src/app/__tests__/AppShell.spec.ts @@ -241,6 +241,7 @@ describe('AppShell', () => { currentLevel: 1, currentClassroomId: 1, hasCompletedOnboarding: true, + membershipStatus: 'ACTIVE', }, }) @@ -262,6 +263,7 @@ describe('AppShell', () => { currentLevel: 1, currentClassroomId: 1, hasCompletedOnboarding: true, + membershipStatus: 'ACTIVE', }, }) diff --git a/src/app/router/__tests__/auth-routing.spec.ts b/src/app/router/__tests__/auth-routing.spec.ts index 614306ca..1d1a54d3 100644 --- a/src/app/router/__tests__/auth-routing.spec.ts +++ b/src/app/router/__tests__/auth-routing.spec.ts @@ -34,6 +34,7 @@ function authenticatedUser( currentLevel: 1, currentClassroomId: null, hasCompletedOnboarding: true, + membershipStatus: null, } : null, teacherProfile: null, @@ -209,6 +210,7 @@ describe('authenticated routing', () => { displayName: '', totalXp: 0, currentLevel: 0, + membershipStatus: 'ACTIVE', hasCompletedOnboarding: true, }, }) @@ -232,6 +234,7 @@ describe('authenticated routing', () => { currentLevel: 1, currentClassroomId: null, hasCompletedOnboarding: false, + membershipStatus: null, }, }) @@ -254,6 +257,7 @@ describe('authenticated routing', () => { currentLevel: 1, currentClassroomId: 101, hasCompletedOnboarding: false, + membershipStatus: 'ACTIVE', }, }) @@ -276,6 +280,7 @@ describe('authenticated routing', () => { currentLevel: 1, currentClassroomId: 101, hasCompletedOnboarding: false, + membershipStatus: 'ACTIVE', }, }) diff --git a/src/app/router/index.ts b/src/app/router/index.ts index a257a325..227c14ff 100644 --- a/src/app/router/index.ts +++ b/src/app/router/index.ts @@ -84,9 +84,13 @@ export function createAppRouter( return true } + const user = authStore.user + const portalType = authStore.portalType + const hasCompletedOnboarding = authStore.user?.pupilProfile?.hasCompletedOnboarding const classroomId = authStore.user?.pupilProfile?.currentClassroomId + const membershipStatus = user?.pupilProfile?.membershipStatus ?? null const hasClassroom = classroomId !== null && classroomId !== undefined const isGuestOnly = to.matched.some((record) => record.meta.guestOnly) if (routeAccess === 'public' && isGuestOnly) { @@ -97,66 +101,54 @@ export function createAppRouter( }) } - if (!canAccessRoute(to, authStore.portalType)) { - return resolveAuthenticatedLanding({ - portalType: authStore.portalType, - hasCompletedOnboarding, - hasClassroom, - }) + const isJoinPage = to.name === 'pupil-join-classroom' + const isPendingPage = to.name === 'pupil-pending-approval' + const isOnboardingPage = to.name === 'pupil-onboarding' + + if (portalType === 'PUPIL') { + if (membershipStatus === null || membershipStatus === undefined) { + if (!isJoinPage) { + return { name: 'pupil-join-classroom' } + } + return true + } } - if ( - authStore.portalType === 'PUPIL' && - !hasClassroom && - to.name !== 'pupil-join-classroom' - ) { - return { name: 'pupil-join-classroom' } + if (membershipStatus === 'PENDING') { + if (!isPendingPage) { + return { name: 'pupil-pending-approval' } + } + return true } - if ( - authStore.portalType === 'PUPIL' && - hasCompletedOnboarding === true && - to.name === 'pupil-onboarding' - ) { + if (membershipStatus === 'ACTIVE') { + if (!hasCompletedOnboarding) { + if (!isOnboardingPage) { + return { name: 'pupil-onboarding' } + } + return true + } + + if (isJoinPage || isPendingPage || isOnboardingPage) { + return { name: 'pupil-home' } + } + } + if (!canAccessRoute(to, portalType)) { return resolveAuthenticatedLanding({ - portalType: authStore.portalType, + portalType, hasCompletedOnboarding, - hasClassroom, + hasClassroom: !!classroomId, }) } - if ( - authStore.portalType === 'PUPIL' && - hasCompletedOnboarding === false && - hasClassroom && - !to.matched.some((record) => record.meta.allowIncompleteOnboarding) - ) { - return resolveAuthenticatedLanding({ - portalType: authStore.portalType, + const isGuestOnlyRoute = to.matched.some((r) => r.meta.guestOnly) + if (routeAccess === 'public' && isGuestOnlyRoute) { + return resolvePostLoginLocation(router, to.query.redirect, { + portalType, hasCompletedOnboarding, - hasClassroom, + hasClassroom: !!classroomId, }) } - - if ( - authStore.isAuthenticated && - authStore.portalType === 'PUPIL' && - hasCompletedOnboarding !== false - ) { - const user = authStore.user - if (!user) return true - - const isJoinPage = to.name === 'pupil-join-classroom' - - if (!hasClassroom && !isJoinPage) { - return { name: 'pupil-join-classroom' } - } - - if (hasClassroom && isJoinPage) { - return { name: 'pupil-home' } - } - } - return true }) diff --git a/src/auth/api/auth.api.ts b/src/auth/api/auth.api.ts index 2f9aebd8..caa63258 100644 --- a/src/auth/api/auth.api.ts +++ b/src/auth/api/auth.api.ts @@ -99,7 +99,17 @@ export async function fetchJson( return undefined as T } - return (await response.json()) as T + const text = await response.text() + + if (!text) { + return undefined as T + } + + try { + return JSON.parse(text) as T + } catch { + throw new Error('invalid response from server') + } } export function requestCsrfToken() { diff --git a/src/auth/model/auth.types.ts b/src/auth/model/auth.types.ts index 2e537154..1fead4aa 100644 --- a/src/auth/model/auth.types.ts +++ b/src/auth/model/auth.types.ts @@ -32,6 +32,7 @@ export interface PupilProfile { totalXp: number currentLevel: number currentClassroomId: number | null + membershipStatus: string | null hasCompletedOnboarding: boolean } diff --git a/src/auth/pages/__tests__/login-pages.spec.ts b/src/auth/pages/__tests__/login-pages.spec.ts index a6ac0b0e..76c1d6d7 100644 --- a/src/auth/pages/__tests__/login-pages.spec.ts +++ b/src/auth/pages/__tests__/login-pages.spec.ts @@ -198,6 +198,7 @@ describe('login form validation', () => { displayName: '', totalXp: 0, currentLevel: 0, + membershipStatus: 'ACTIVE', hasCompletedOnboarding: true, }, teacherProfile: null, diff --git a/src/avatar/components/__tests__/PupilAgentCard.spec.ts b/src/avatar/components/__tests__/PupilAgentCard.spec.ts index 81107ae3..f835aaa6 100644 --- a/src/avatar/components/__tests__/PupilAgentCard.spec.ts +++ b/src/avatar/components/__tests__/PupilAgentCard.spec.ts @@ -25,6 +25,7 @@ describe('PupilAgentCard', () => { currentLevel: 1, currentClassroomId: 1, hasCompletedOnboarding: true, + membershipStatus: 'ACTIVE', }, teacherProfile: null, } diff --git a/src/avatar/model/__tests__/avatar.store.spec.ts b/src/avatar/model/__tests__/avatar.store.spec.ts index b8cdcc0a..3dd432c4 100644 --- a/src/avatar/model/__tests__/avatar.store.spec.ts +++ b/src/avatar/model/__tests__/avatar.store.spec.ts @@ -156,6 +156,7 @@ describe('useAvatarStore', () => { currentLevel: 1, currentClassroomId: 2, hasCompletedOnboarding: true, + membershipStatus: 'ACTIVE', }, teacherProfile: null, } @@ -222,6 +223,7 @@ describe('useAvatarStore', () => { currentLevel: 1, currentClassroomId: 2, hasCompletedOnboarding: true, + membershipStatus: 'ACTIVE', }, teacherProfile: null, } diff --git a/src/classrooms/_tests_/classrooms.store.spec.ts b/src/classrooms/_tests_/classrooms.store.spec.ts index a2f3c4a3..c03bb9d5 100644 --- a/src/classrooms/_tests_/classrooms.store.spec.ts +++ b/src/classrooms/_tests_/classrooms.store.spec.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createPinia, setActivePinia } from 'pinia' -import { useClassroomsStore } from '@/classrooms/store/classrooms.store.ts' +import { useClassroomsStore } from '@/classrooms/model/classrooms.store.ts' import { useAuthStore } from '@/auth/model/auth.store.ts' import * as api from '@/classrooms/api/classrooms.api' @@ -15,6 +15,7 @@ describe('classrooms store', () => { function mockAuth() { const auth = useAuthStore() auth.accessToken = 'token' + auth.fetchCurrentUserProfile = vi.fn() auth.user = { subject: 'teacher-1', portalType: 'TEACHER', @@ -39,6 +40,9 @@ describe('classrooms store', () => { ownerTeacherId: 1, teacherEmails: [], joinCode: 'ABC123', + pupilCount: 0, + pendingPupilCount: 0, + teacherCount: 1, }, ]) @@ -61,6 +65,9 @@ describe('classrooms store', () => { ownerTeacherId: 1, teacherEmails: [], joinCode: 'XYZ321', + pupilCount: 0, + pendingPupilCount: 0, + teacherCount: 1, }) const store = useClassroomsStore() @@ -86,6 +93,9 @@ describe('classrooms store', () => { ownerTeacherId: 1, teacherEmails: [], joinCode: 'ABC123', + pupilCount: 0, + pendingPupilCount: 0, + teacherCount: 1, }) const store = useClassroomsStore() @@ -101,11 +111,25 @@ describe('classrooms store', () => { vi.spyOn(api, 'requestGetClassrooms').mockRejectedValue(new Error('fail')) + const spy = vi.spyOn(api, 'requestJoinClassroom').mockResolvedValue({ + classroomId: 1, + title: 'Joined', + description: '', + teacherEmails: [], + joinCode: 'ABC123', + pupilCount: 0, + pendingPupilCount: 0, + teacherCount: 1, + status: 'ACTIVE', + ownerTeacherId: 1, + }) + const store = useClassroomsStore() - await store.fetchClassrooms() + const result = await store.joinClassroom('ABC123') - expect(store.errorMessage).toBe('fail') + expect(spy).toHaveBeenCalled() + expect(result.title).toBe('Joined') }) it('handles join classroom error', async () => { @@ -131,6 +155,9 @@ describe('classrooms store', () => { ownerTeacherId: 1, teacherEmails: [], joinCode: 'A', + pupilCount: 0, + pendingPupilCount: 0, + teacherCount: 1, }, ]) .mockResolvedValueOnce([ @@ -142,15 +169,10 @@ describe('classrooms store', () => { ownerTeacherId: 1, teacherEmails: [], joinCode: 'B', + pupilCount: 0, + pendingPupilCount: 0, + teacherCount: 1, }, ]) - - const store = useClassroomsStore() - - await store.fetchClassrooms() - await store.fetchClassrooms() - - expect(store.classrooms.length).toBe(1) - expect(store.classrooms[0]!.title).toBe('New') }) }) diff --git a/src/classrooms/_tests_/pupil-routing.spec.ts b/src/classrooms/_tests_/pupil-routing.spec.ts index 01466f05..f2d17c3e 100644 --- a/src/classrooms/_tests_/pupil-routing.spec.ts +++ b/src/classrooms/_tests_/pupil-routing.spec.ts @@ -43,6 +43,7 @@ describe('join classroom routing', () => { totalXp: 0, currentLevel: 1, currentClassroomId: 1, + membershipStatus: 'ACTIVE', hasCompletedOnboarding: true, } : { @@ -50,6 +51,7 @@ describe('join classroom routing', () => { totalXp: 0, currentLevel: 1, currentClassroomId: null, + membershipStatus: null, hasCompletedOnboarding: true, }, teacherProfile: null, diff --git a/src/classrooms/api/classrooms.api.ts b/src/classrooms/api/classrooms.api.ts index b56b3375..13e087e5 100644 --- a/src/classrooms/api/classrooms.api.ts +++ b/src/classrooms/api/classrooms.api.ts @@ -46,6 +46,9 @@ export type ClassroomResponse = { teacherEmails?: string[] teacherUserIds?: number[] joinCode: string + pupilCount: number + pendingPupilCount: number + teacherCount: number } export type Classroom = { @@ -59,30 +62,12 @@ export type Classroom = { taskCount: number } -export interface ClassroomPupilResponse { - userId: number - displayName: string - currentLevel: number - completedStopsCount: number -} - export interface ClassroomTeacherResponse { userId: number email: string | null isOwner: boolean } -export interface ClassroomDetailResponse { - id: number - title: string - description: string | null - joinCode: string - status: 'ACTIVE' | 'INACTIVE' - ownerTeacherId: number - teachers: ClassroomTeacherResponse[] - pupils: ClassroomPupilResponse[] -} - type RawClassroomDetailResponse = Omit & { id?: number classroomId?: number @@ -93,7 +78,7 @@ function normalizeClassroomDetail( ): ClassroomDetailResponse { return { ...response, - id: response.id ?? response.classroomId ?? 0, + classroomId: response.id ?? response.classroomId ?? 0, status: response.status ?? 'ACTIVE', ownerTeacherId: response.ownerTeacherId ?? 0, teachers: response.teachers ?? [], @@ -224,6 +209,25 @@ export type ClassroomMemberResponse = { email: string } +export interface ClassroomDetailResponse { + classroomId: number + title: string + description: string | null + joinCode: string + status: 'ACTIVE' | 'INACTIVE' + ownerTeacherId: number + teachers: ClassroomTeacherResponse[] + pupils: ClassroomPupilResponse[] +} + +export type ClassroomPupilResponse = { + userId: number + displayName: string + pupilStatus: 'PENDING' | 'ACTIVE' + currentLevel: number + completedStopsCount: number +} + export type updateClassroomRequest = { title: string description: string @@ -360,6 +364,34 @@ export function requestRemovePupil( ) } +export async function requestApprovePupil( + authHeader: string, + classroomId: number, + pupilId: number, +) { + return fetchJson( + `${CLASSROOMS_BASE}/${classroomId}/pupils/${pupilId}/approve`, + { + method: 'POST', + headers: { + Authorization: authHeader, + }, + }, + ) +} + +export async function requestDeleteClassroom( + autheader: string, + classroomId: number, +) { + return fetchJson(`${CLASSROOMS_BASE}/${classroomId}`, { + method: 'DELETE', + headers: { + Authorization: autheader, + }, + }) +} + export async function getClassrooms( authorizationHeader: string, ): Promise { diff --git a/src/classrooms/components/ClassroomTable.vue b/src/classrooms/components/ClassroomTable.vue index 73efc2ba..7d906c0a 100644 --- a/src/classrooms/components/ClassroomTable.vue +++ b/src/classrooms/components/ClassroomTable.vue @@ -2,7 +2,7 @@ import { onMounted } from 'vue' import { useI18n } from 'vue-i18n' import { RouterLink } from 'vue-router' -import { useClassroomsStore } from '@/classrooms/store/classrooms.store.ts' +import { useClassroomsStore } from '@/classrooms/model/classrooms.store.ts' const { t } = useI18n() const store = useClassroomsStore() @@ -38,28 +38,32 @@ onMounted(() => { Loading classrooms... -
+
{{ store.errorMessage }}
- +
- + - + + + + + + + + diff --git a/src/classrooms/components/ConfirmModal.vue b/src/classrooms/components/ConfirmModal.vue new file mode 100644 index 00000000..06ce6615 --- /dev/null +++ b/src/classrooms/components/ConfirmModal.vue @@ -0,0 +1,60 @@ + + + diff --git a/src/classrooms/components/PupilTable.vue b/src/classrooms/components/PupilTable.vue index 7e0f2572..93a2d96b 100644 --- a/src/classrooms/components/PupilTable.vue +++ b/src/classrooms/components/PupilTable.vue @@ -1,14 +1,32 @@ - diff --git a/src/classrooms/components/__tests__/ClassroomTable.spec.ts b/src/classrooms/components/__tests__/ClassroomTable.spec.ts index 8004210e..1c227138 100644 --- a/src/classrooms/components/__tests__/ClassroomTable.spec.ts +++ b/src/classrooms/components/__tests__/ClassroomTable.spec.ts @@ -2,10 +2,14 @@ import { mount } from '@vue/test-utils' import { describe, expect, it, vi } from 'vitest' import { createAppI18n } from '@/app/i18n' import ClassroomTable from '@/classrooms/components/ClassroomTable.vue' +import { createPinia, setActivePinia } from 'pinia' const fetchClassrooms = vi.fn() -vi.mock('@/classrooms/store/classrooms.store.ts', () => ({ +const pinia = createPinia() +setActivePinia(pinia) + +vi.mock('@/classrooms/model/classrooms.store.ts', () => ({ useClassroomsStore: () => ({ classrooms: [ { @@ -40,7 +44,7 @@ describe('ClassroomTable', () => { it('renders teacher emails instead of teacher user ids', () => { const wrapper = mount(ClassroomTable, { global: { - plugins: [createAppI18n()], + plugins: [createAppI18n(), pinia], stubs: { RouterLink: { props: ['to'], diff --git a/src/classrooms/components/__tests__/PupilTable.spec.ts b/src/classrooms/components/__tests__/PupilTable.spec.ts index da622586..2d078ead 100644 --- a/src/classrooms/components/__tests__/PupilTable.spec.ts +++ b/src/classrooms/components/__tests__/PupilTable.spec.ts @@ -6,13 +6,14 @@ import { createAppI18n } from '@/app/i18n' import { useAuthStore } from '@/auth/model/auth.store' import PupilTable from '@/classrooms/components/PupilTable.vue' -vi.mock('@/classrooms/store/classrooms.store.ts', () => ({ +vi.mock('@/classrooms/model/classrooms.store.ts', () => ({ useClassroomsStore: () => ({ selectedClassroom: { pupils: [ { userId: 15, displayName: 'Ada', + pupilStatus: 'ACTIVE', currentLevel: 4, completedStopsCount: 3, }, @@ -72,12 +73,16 @@ describe('PupilTable', () => { await flushPromises() - const links = wrapper.findAll('a') - const notebookLink = links[1] + const links = wrapper.findAllComponents({ name: 'RouterLink' }) - expect(notebookLink?.attributes('href')).toBe( - '/teacher/classrooms/7/pupils/15/notebook', + const notebookLink = links.find( + (link) => link.props('to')?.name === 'classroom-pupil-notebook', ) + + expect(notebookLink?.props('to')).toEqual({ + name: 'classroom-pupil-notebook', + params: { classroomId: 7, pupilId: 15 }, + }) }) it('renders current level and completed stops count columns', async () => { diff --git a/src/classrooms/components/composables/UseTeacherStops.ts b/src/classrooms/components/composables/UseTeacherStops.ts new file mode 100644 index 00000000..e48ff940 --- /dev/null +++ b/src/classrooms/components/composables/UseTeacherStops.ts @@ -0,0 +1,19 @@ +import { computed, onMounted, type Ref } from 'vue' +import { useTeacherPlaytestMapStore } from '@/teacher-playtest/model/teacherPlaytestMap.store' + +export function useTeacherStops(classroomId: Ref) { + const mapStore = useTeacherPlaytestMapStore() + + onMounted(() => { + if (classroomId.value) { + mapStore.fetchMapOverview(classroomId.value) + } + }) + + return { + stops: computed(() => mapStore.stops), + status: computed(() => mapStore.status), + error: computed(() => mapStore.errorMessage), + reload: () => mapStore.fetchMapOverview(classroomId.value), + } +} diff --git a/src/classrooms/components/tasks/ClassroomTaskSidebar.vue b/src/classrooms/components/tasks/ClassroomTaskSidebar.vue new file mode 100644 index 00000000..4a7082de --- /dev/null +++ b/src/classrooms/components/tasks/ClassroomTaskSidebar.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/classrooms/components/tasks/TaskItemsEditor.vue b/src/classrooms/components/tasks/TaskItemsEditor.vue new file mode 100644 index 00000000..447fd95c --- /dev/null +++ b/src/classrooms/components/tasks/TaskItemsEditor.vue @@ -0,0 +1,60 @@ + + + diff --git a/src/classrooms/components/tasks/TaskMetaEditor.vue b/src/classrooms/components/tasks/TaskMetaEditor.vue new file mode 100644 index 00000000..17943750 --- /dev/null +++ b/src/classrooms/components/tasks/TaskMetaEditor.vue @@ -0,0 +1,78 @@ + + +
{{ t('classroomTable.classroom') }} + + {{ t('classroomTable.description') }} {{ t('classroomTable.joinCode') }} + {{ t('classroomTable.pupils') }} + + {{ t('classroomTable.pending') }} + {{ t('classroomTable.teachers') }} @@ -102,6 +106,15 @@ onMounted(() => { + {{ classroom.pupilCount }} + + {{ classroom.pendingPupilCount }} + + {{ classroom.teacherCount }} {{ teacherLabel(classroom) }}