Skip to content

Commit

Permalink
feat: add Educator tab to Admin page for teacher management
Browse files Browse the repository at this point in the history
  • Loading branch information
saachibm authored and alee committed Oct 23, 2024
1 parent f7cf6aa commit 9048d1a
Show file tree
Hide file tree
Showing 7 changed files with 354 additions and 15 deletions.
34 changes: 34 additions & 0 deletions client/src/api/educator/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
StudentData,
ActiveRoomData,
ClassroomData,
TeacherData,
} from "@port-of-mars/shared/types";

export class EducatorAPI {
Expand Down Expand Up @@ -139,4 +140,37 @@ export class EducatorAPI {
}
);
}

// Teacher-related methods
async getTeachers(): Promise<Array<TeacherData>> {
return await this.ajax.get(url("/educator/teacher/list"), ({ data, status }) => {
return data;
});
}

async addTeacher(teacherData: { email: string; username: string; name: string }): Promise<any> {
const payload = { ...teacherData };
return await this.ajax.post(
url("/educator/teacher/add"),
({ data, status }) => {
if (status === 201) {
return data;
} else {
throw new Error(data.message || "Failed to add teacher");
}
},
payload
);
}

async deleteTeacher(teacherId: number): Promise<void> {
return await this.ajax.delete(
url(`/educator/teacher/delete/${teacherId}`),
({ data, status }) => {
if (status !== 200) {
throw new Error(data.message || "Failed to delete teacher");
}
}
);
}
}
35 changes: 22 additions & 13 deletions client/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Games from "@port-of-mars/client/views/admin/Games.vue";
import Rooms from "@port-of-mars/client/views/admin/Rooms.vue";
import Reports from "@port-of-mars/client/views/admin/Reports.vue";
import Settings from "@port-of-mars/client/views/admin/Settings.vue";
import Educator from "@port-of-mars/client/views/admin/Educator.vue";
import Login from "@port-of-mars/client/views/Login.vue";
import Leaderboard from "@port-of-mars/client/views/Leaderboard.vue";
import PlayerHistory from "@port-of-mars/client/views/PlayerHistory.vue";
Expand Down Expand Up @@ -101,31 +102,32 @@ const ADMIN_META = PAGE_META[ADMIN_PAGE].meta;
const FREE_PLAY_LOBBY_META = PAGE_META[FREE_PLAY_LOBBY_PAGE].meta;

const sharedRoutes = [
// routes shared between educator and default mode
{
...PAGE_META[ADMIN_PAGE],
component: Admin,
children: [
{ path: "", name: "Admin", redirect: { name: "AdminOverview" }, meta: ADMIN_META },
{ path: "overview", name: "AdminOverview", component: Overview, meta: ADMIN_META },
{ path: "games", name: "AdminGames", component: Games, meta: ADMIN_META },
{ path: "rooms", name: "AdminRooms", component: Rooms, meta: ADMIN_META },
{ path: "reports", name: "AdminReports", component: Reports, meta: ADMIN_META },
{ path: "settings", name: "AdminSettings", component: Settings, meta: ADMIN_META },
],
},
{ ...PAGE_META[GAME_PAGE], component: Game },
{ ...PAGE_META[LEADERBOARD_PAGE], component: Leaderboard },
{ ...PAGE_META[PLAYER_HISTORY_PAGE], component: PlayerHistory },
{ ...PAGE_META[MANUAL_PAGE], component: Manual },
{ ...PAGE_META[PRIVACY_PAGE], component: Privacy },
];

const adminRoute = {
...PAGE_META[ADMIN_PAGE],
component: Admin,
children: [
{ path: "", name: "Admin", redirect: { name: "AdminOverview" }, meta: ADMIN_META },
{ path: "overview", name: "AdminOverview", component: Overview, meta: ADMIN_META },
{ path: "games", name: "AdminGames", component: Games, meta: ADMIN_META },
{ path: "rooms", name: "AdminRooms", component: Rooms, meta: ADMIN_META },
{ path: "reports", name: "AdminReports", component: Reports, meta: ADMIN_META },
{ path: "settings", name: "AdminSettings", component: Settings, meta: ADMIN_META },
],
};

function getDefaultRouter() {
const router = new VueRouter({
mode: "hash",
routes: [
...sharedRoutes,
adminRoute,
{ ...PAGE_META[LOGIN_PAGE], component: Login },
{
...PAGE_META[FREE_PLAY_LOBBY_PAGE],
Expand Down Expand Up @@ -182,6 +184,13 @@ function getEducatorRouter() {
mode: "hash",
routes: [
...sharedRoutes,
{
...adminRoute,
children: [
...adminRoute.children,
{ path: "educator", name: "AdminEducator", component: Educator, meta: ADMIN_META },
],
},
// redirect straight to student login page
{ path: "", name: "Home", redirect: { name: STUDENT_LOGIN_PAGE } },
{ ...PAGE_META[STUDENT_LOGIN_PAGE], component: StudentLogin },
Expand Down
3 changes: 3 additions & 0 deletions client/src/views/Admin.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
<b-nav-item class="nav-link" active-class="active" to="/admin/settings">
Settings
</b-nav-item>
<b-nav-item class="nav-link" active-class="active" to="/admin/educator">
Educator
</b-nav-item>
</b-nav>
<router-view
class="h-100 backdrop"
Expand Down
207 changes: 207 additions & 0 deletions client/src/views/admin/Educator.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
<template>
<b-container fluid class="h-100 w-100 m-0 p-0 d-flex flex-column">
<div class="p-3 h-100">
<b-row class="h-100 m-0">
<b-col class="mh-100 p-2">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="header-nowrap">Manage Teachers</h4>
<div>
<b-button variant="success" class="mr-2" @click="showAddTeacherModal"
>Add Teacher</b-button
>
</div>
</div>

<div class="h-100-header w-100 content-container">
<b-table
dark
sticky-header
class="h-100 m-0 custom-table"
style="max-height: none"
:fields="fields"
:items="teachers"
>
<template #cell(name)="data">
{{ data.item.name }}
</template>

<template #cell(email)="data">
{{ data.item.email }}
</template>

<template #cell(actions)="data">
<b-button variant="primary" @click="confirmDeleteTeacher(data.item)"
>Delete</b-button
>
</template>
</b-table>
</div>
</b-col>
</b-row>
</div>

<!-- Add Teacher Modal -->
<b-modal
id="add-teacher-modal"
centered
title="Add New Teacher"
body-bg-variant="dark"
header-bg-variant="dark"
header-class="pb-0 border-bottom-0"
footer-class="pt-0 border-top-0"
footer-bg-variant="dark"
cancel-variant="outline-secondary"
okTitle="Add Teacher"
@ok="addTeacher"
>
<b-form>
<b-form-group label="Username" label-for="teacher-username-input">
<b-form-input
id="teacher-username-input"
v-model="newTeacher.username"
required
placeholder="Enter teacher username"
></b-form-input>
</b-form-group>
<b-form-group label="Teacher Name" label-for="teacher-name-input">
<b-form-input
id="teacher-name-input"
v-model="newTeacher.name"
required
placeholder="Enter teacher name"
></b-form-input>
</b-form-group>
<b-form-group label="Teacher Email" label-for="teacher-email-input">
<b-form-input
id="teacher-email-input"
v-model="newTeacher.email"
required
type="email"
placeholder="Enter teacher email"
></b-form-input>
</b-form-group>
</b-form>
</b-modal>

<!-- Show Password Modal -->
<b-modal
id="show-password-modal"
centered
title="Teacher Password"
body-bg-variant="dark"
header-bg-variant="dark"
header-class="pb-0 border-bottom-0"
footer-class="pt-0 border-top-0"
footer-bg-variant="dark"
ok-title="Close"
@ok="closePasswordModal"
>
<span
>Your password is: <b>{{ newTeacherPassword }}</b></span
>
<br />
<span>Please store it securely. You will not be able to view it again.</span>
</b-modal>

<!-- Delete Teacher Modal -->
<b-modal
id="delete-teacher-modal"
centered
title="Delete Confirmation"
body-bg-variant="dark"
header-bg-variant="dark"
header-class="pb-0 border-bottom-0"
footer-class="pt-0 border-top-0"
footer-bg-variant="dark"
cancel-variant="outline-secondary"
okTitle="Confirm"
@ok="deleteTeacher"
>
<span>Are you sure you want to delete this teacher? This action cannot be undone.</span>
</b-modal>
</b-container>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import { TeacherData } from "@port-of-mars/shared/types";
import { EducatorAPI } from "@port-of-mars/client/api/educator/request";
@Component({})
export default class Educator extends Vue {
teachers: TeacherData[] = [];
newTeacher = {
username: "",
name: "",
email: "",
};
newTeacherPassword: string | null = null;
selectedTeacher: TeacherData | null = null;
educatorApi = new EducatorAPI(this.$store, this.$ajax);
fields = [
{ key: "name", label: "Teacher Name" },
{ key: "email", label: "Email" },
{ key: "actions", label: "Actions", class: "text-center" },
];
async created() {
await this.fetchTeachers();
}
async fetchTeachers() {
try {
this.teachers = await this.educatorApi.getTeachers();
} catch (error) {
console.error("Failed to fetch teachers:", error);
}
}
showAddTeacherModal() {
this.newTeacher = { username: "", name: "", email: "" };
this.$bvModal.show("add-teacher-modal");
}
async addTeacher() {
if (!this.newTeacher.username || !this.newTeacher.name || !this.newTeacher.email) {
console.error("Username, name, and email are required");
return;
}
try {
const newTeacher = await this.educatorApi.addTeacher(this.newTeacher);
this.newTeacherPassword = newTeacher.password;
await this.fetchTeachers();
this.$bvModal.show("show-password-modal");
console.log("Added new teacher successfully");
} catch (error) {
console.error("Failed to add teacher:", error);
}
}
confirmDeleteTeacher(teacher: TeacherData) {
this.selectedTeacher = teacher;
this.$bvModal.show("delete-teacher-modal");
}
async deleteTeacher() {
if (!this.selectedTeacher) return;
try {
await this.educatorApi.deleteTeacher(this.selectedTeacher.teacherId);
await this.fetchTeachers();
console.log("Deleted teacher successfully");
} catch (error) {
console.error("Failed to delete teacher:", error);
} finally {
this.selectedTeacher = null;
}
}
closePasswordModal() {
this.newTeacherPassword = null;
}
}
</script>
53 changes: 52 additions & 1 deletion server/src/routes/educator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NextFunction, Request, Response, Router } from "express";
import { getServices } from "@port-of-mars/server/services";
import { User } from "@port-of-mars/server/entity/User";
import { isAuthenticated } from "@port-of-mars/server/routes/middleware";
import { isAuthenticated, isAdminAuthenticated } from "@port-of-mars/server/routes/middleware";
import { ValidationError } from "@port-of-mars/server/util";
import { getLogger } from "@port-of-mars/server/settings";

Expand Down Expand Up @@ -241,3 +241,54 @@ educatorRouter.post(
}
}
);

//teacher specfic
educatorRouter.post(
"/teacher/add",
isAdminAuthenticated,
async (req: Request, res: Response, next: NextFunction) => {
const user = req.user as User;

Check warning on line 250 in server/src/routes/educator.ts

View workflow job for this annotation

GitHub Actions / server

'user' is assigned a value but never used
const { username, name, email } = req.body;

try {
const services = getServices();
const teacher = await services.educator.createTeacher(email, username, name);
res.status(201).json(teacher);
} catch (e) {
logger.warn("Unable to add new teacher", e);
next(e);
}
}
);

educatorRouter.delete(
"/teacher/delete/:teacherId",
isAdminAuthenticated,
async (req: Request, res: Response, next: NextFunction) => {
const teacherId = Number(req.params.teacherId);

try {
const services = getServices();
await services.educator.deleteTeacher(teacherId);
res.status(200).json({ message: "Teacher deleted successfully" });
} catch (e) {
logger.warn("Unable to delete teacher", e);
next(e);
}
}
);

educatorRouter.get(
"/teacher/list",
isAdminAuthenticated,
async (req: Request, res: Response, next: NextFunction) => {
try {
const services = getServices();
const teachers = await services.educator.getAllTeachers();
res.status(200).json(teachers);
} catch (e) {
logger.warn("Unable to fetch teachers list", e);
next(e);
}
}
);
Loading

0 comments on commit 9048d1a

Please sign in to comment.