From 70de97766ebb6767c383b95fa2be9f4c1b10a206 Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Thu, 5 Sep 2024 18:25:39 +0100 Subject: [PATCH 1/7] Added search to table --- src/dashboards/MasterStatistical.tsx | 32 ++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/dashboards/MasterStatistical.tsx b/src/dashboards/MasterStatistical.tsx index 3a4a2e57..69ec5d66 100644 --- a/src/dashboards/MasterStatistical.tsx +++ b/src/dashboards/MasterStatistical.tsx @@ -16,6 +16,7 @@ import { useReactTable, } from "@tanstack/react-table"; import Checkbox from "@/components/Low/Checkbox"; +import { useListSearch } from "@/hooks/useListSearch"; interface Props { corporateUsers: User[]; @@ -24,6 +25,7 @@ interface Props { interface TableData { user: string; + email: string; correct: number; corporate: string; submitted: boolean; @@ -37,6 +39,8 @@ interface UserCount { maxUserCount: number; } +const searchFilters = [["email"], ["user"], ["userId"], ]; + const MasterStatistical = (props: Props) => { const { users, corporateUsers } = props; @@ -72,10 +76,12 @@ const MasterStatistical = (props: Props) => { const userResults = a.assignees.map((assignee) => { const userStats = a.results.find((r) => r.user === assignee)?.stats || []; - const userName = users.find((u) => u.id === assignee)?.name || ""; + const userData = users.find((u) => u.id === assignee) const corporate = users.find((u) => u.id === a.assigner)?.name || ""; const commonData = { - user: userName, + user: userData?.name || "", + email: userData?.email || "", + userId: assignee, corporateId: a.corporateId, corporate, assignment: a.name, @@ -146,6 +152,13 @@ const MasterStatistical = (props: Props) => { return {info.getValue()}; }, }), + columnHelper.accessor("email", { + header: "Email", + id: "email", + cell: (info) => { + return {info.getValue()}; + }, + }), columnHelper.accessor("corporate", { header: "Corporate", id: "corporate", @@ -192,12 +205,19 @@ const MasterStatistical = (props: Props) => { }), ]; + const { rows: filteredRows, renderSearch } = useListSearch( + searchFilters, + tableResults, + ); + const table = useReactTable({ - data: tableResults, + data: filteredRows, columns: defaultColumns, getCoreRowModel: getCoreRowModel(), }); + + const areAllSelected = selectedCorporates.length === corporates.length; const getStudentsConsolidateScore = () => { @@ -261,7 +281,8 @@ const MasterStatistical = (props: Props) => { ); })} -
+
+
{ setEndDate(null); }} /> +
+ {renderSearch()}
+
From a61ad2cc7e95dd1f532cb3847fa19f15189b8218 Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Thu, 5 Sep 2024 22:42:46 +0100 Subject: [PATCH 2/7] Added an export feature for the master statisticl screen --- src/dashboards/MasterStatistical.tsx | 114 ++++++++---- src/hooks/useListSearch.tsx | 29 +--- src/pages/api/assignments/corporate/index.ts | 33 +--- .../api/assignments/statistical/excel.ts | 162 ++++++++++++++++++ src/utils/assignments.be.ts | 31 ++++ src/utils/search.ts | 29 ++++ 6 files changed, 309 insertions(+), 89 deletions(-) create mode 100644 src/pages/api/assignments/statistical/excel.ts create mode 100644 src/utils/search.ts diff --git a/src/dashboards/MasterStatistical.tsx b/src/dashboards/MasterStatistical.tsx index 69ec5d66..944b0972 100644 --- a/src/dashboards/MasterStatistical.tsx +++ b/src/dashboards/MasterStatistical.tsx @@ -1,23 +1,23 @@ import React from "react"; import { CorporateUser, User } from "@/interfaces/user"; -import { BsBank, BsPersonFill } from "react-icons/bs"; +import { BsFileExcel, BsBank, BsPersonFill } from "react-icons/bs"; import IconCard from "./IconCard"; + import useAssignmentsCorporates from "@/hooks/useAssignmentCorporates"; import ReactDatePicker from "react-datepicker"; + import moment from "moment"; -import { Assignment, AssignmentWithCorporateId } from "@/interfaces/results"; +import { AssignmentWithCorporateId } from "@/interfaces/results"; import { - CellContext, - createColumnHelper, flexRender, + createColumnHelper, getCoreRowModel, - HeaderGroup, - Table, useReactTable, } from "@tanstack/react-table"; import Checkbox from "@/components/Low/Checkbox"; import { useListSearch } from "@/hooks/useListSearch"; - +import axios from "axios"; +import { toast } from "react-toastify"; interface Props { corporateUsers: User[]; users: User[]; @@ -39,7 +39,7 @@ interface UserCount { maxUserCount: number; } -const searchFilters = [["email"], ["user"], ["userId"], ]; +const searchFilters = [["email"], ["user"], ["userId"]]; const MasterStatistical = (props: Props) => { const { users, corporateUsers } = props; @@ -70,13 +70,15 @@ const MasterStatistical = (props: Props) => { endDate, }); + const [downloading, setDownloading] = React.useState(false); + const tableResults = React.useMemo( () => assignments.reduce((accmA: TableData[], a: AssignmentWithCorporateId) => { const userResults = a.assignees.map((assignee) => { const userStats = a.results.find((r) => r.user === assignee)?.stats || []; - const userData = users.find((u) => u.id === assignee) + const userData = users.find((u) => u.id === assignee); const corporate = users.find((u) => u.id === a.assigner)?.name || ""; const commonData = { user: userData?.name || "", @@ -205,9 +207,9 @@ const MasterStatistical = (props: Props) => { }), ]; - const { rows: filteredRows, renderSearch } = useListSearch( + const { rows: filteredRows, renderSearch, text: searchText } = useListSearch( searchFilters, - tableResults, + tableResults ); const table = useReactTable({ @@ -216,8 +218,6 @@ const MasterStatistical = (props: Props) => { getCoreRowModel: getCoreRowModel(), }); - - const areAllSelected = selectedCorporates.length === corporates.length; const getStudentsConsolidateScore = () => { @@ -240,9 +240,55 @@ const MasterStatistical = (props: Props) => { ); }; + + const triggerDownload = async () => { + try { + setDownloading(true); + const res = await axios.post("/api/assignments/statistical/excel", { + ids: selectedCorporates, + ...(startDate ? { startDate: startDate.toISOString() } : {}), + ...(endDate ? { endDate: endDate.toISOString() } : {}), + searchText, + }); + toast.success("Report ready!"); + const link = document.createElement("a"); + link.href = res.data; + // download should have worked but there are some CORS issues + // https://firebase.google.com/docs/storage/web/download-files#cors_configuration + // link.download="report.pdf"; + link.target = "_blank"; + link.rel = "noreferrer"; + link.click(); + setDownloading(false); + } catch (err) { + toast.error("Failed to display the report!"); + console.error(err); + setDownloading(false); + } + }; + + const renderIcon = () => { + if (downloading) { + return ( + + ); + } + + return ( + { + e.stopPropagation(); + triggerDownload(); + }} + /> + ); + }; + const consolidateResults = getStudentsConsolidateScore(); return ( <> + {renderIcon()}
{
- - { - setStartDate(initialDate ?? moment("01/01/2023").toDate()); - if (finalDate) { - // basicly selecting a final day works as if I'm selecting the first - // minute of that day. this way it covers the whole day - setEndDate(moment(finalDate).endOf("day").toDate()); - return; - } - setEndDate(null); - }} - /> + + { + setStartDate(initialDate ?? moment("01/01/2023").toDate()); + if (finalDate) { + // basicly selecting a final day works as if I'm selecting the first + // minute of that day. this way it covers the whole day + setEndDate(moment(finalDate).endOf("day").toDate()); + return; + } + setEndDate(null); + }} + />
{renderSearch()}
diff --git a/src/hooks/useListSearch.tsx b/src/hooks/useListSearch.tsx index 9ed5abfb..4f2a23ac 100644 --- a/src/hooks/useListSearch.tsx +++ b/src/hooks/useListSearch.tsx @@ -1,18 +1,6 @@ import {useState, useMemo} from "react"; import Input from "@/components/Low/Input"; - -/*fields example = [ - ['id'], - ['companyInformation', 'companyInformation', 'name'] -]*/ - -const getFieldValue = (fields: string[], data: any): string => { - if (fields.length === 0) return data; - const [key, ...otherFields] = fields; - - if (data[key]) return getFieldValue(otherFields, data[key]); - return data; -}; +import { search } from "@/utils/search"; export function useListSearch(fields: string[][], rows: T[]) { const [text, setText] = useState(""); @@ -20,22 +8,11 @@ export function useListSearch(fields: string[][], rows: T[]) { const renderSearch = () => ; const updatedRows = useMemo(() => { - const searchText = text.toLowerCase(); - return rows.filter((row) => { - return fields.some((fieldsKeys) => { - const value = getFieldValue(fieldsKeys, row); - if (typeof value === "string") { - return value.toLowerCase().includes(searchText); - } - - if (typeof value === "number") { - return (value as Number).toString().includes(searchText); - } - }); - }); + return search(text, fields, rows); }, [fields, rows, text]); return { + text, rows: updatedRows, renderSearch, }; diff --git a/src/pages/api/assignments/corporate/index.ts b/src/pages/api/assignments/corporate/index.ts index cf6f8f92..e2557fe3 100644 --- a/src/pages/api/assignments/corporate/index.ts +++ b/src/pages/api/assignments/corporate/index.ts @@ -2,8 +2,7 @@ import type {NextApiRequest, NextApiResponse} from "next"; import {withIronSessionApiRoute} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; -import {getAllAssignersByCorporate} from "@/utils/groups.be"; -import {getAssignmentsByAssigners} from "@/utils/assignments.be"; +import {getAssignmentsByAssigner, getAssignmentsForCorporates} from "@/utils/assignments.be"; export default withIronSessionApiRoute(handler, sessionOptions); @@ -30,34 +29,8 @@ async function GET(req: NextApiRequest, res: NextApiResponse) { try { const idsList = ids.split(","); - const assigners = await Promise.all( - idsList.map(async (id) => { - const assigners = await getAllAssignersByCorporate(id); - return { - corporateId: id, - assigners, - }; - }), - ); - - const assignments = await Promise.all( - assigners.map(async (data) => { - try { - const assigners = [...new Set([...data.assigners, data.corporateId])]; - const assignments = await getAssignmentsByAssigners(assigners, startDateParsed, endDateParsed); - return assignments.map((assignment) => ({ - ...assignment, - corporateId: data.corporateId, - })); - } catch (err) { - console.error(err); - return []; - } - }), - ); - - // const assignments = await getAssignmentsByAssigners(assignmentList, startDateParsed, endDateParsed); - res.status(200).json(assignments.flat()); + const assignments = await getAssignmentsForCorporates(idsList, startDateParsed, endDateParsed); + res.status(200).json(assignments); } catch (err: any) { res.status(500).json({error: err.message}); } diff --git a/src/pages/api/assignments/statistical/excel.ts b/src/pages/api/assignments/statistical/excel.ts new file mode 100644 index 00000000..54c8153e --- /dev/null +++ b/src/pages/api/assignments/statistical/excel.ts @@ -0,0 +1,162 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { app, storage } from "@/firebase"; +import { getFirestore } from "firebase/firestore"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { ref, uploadBytes, getDownloadURL } from "firebase/storage"; +import { AssignmentWithCorporateId } from "@/interfaces/results"; +import moment from "moment-timezone"; +import ExcelJS from "exceljs"; +import { getSpecificUsers } from "@/utils/users.be"; +import { checkAccess } from "@/utils/permissions"; +import { getAssignmentsForCorporates } from "@/utils/assignments.be"; +import { search } from "@/utils/search"; + +const db = getFirestore(app); + +export default withIronSessionApiRoute(handler, sessionOptions); + +interface TableData { + user: string; + email: string; + correct: number; + corporate: string; + submitted: boolean; + date: moment.Moment; + assignment: string; + corporateId: string; +} + +async function handler(req: NextApiRequest, res: NextApiResponse) { + // if (req.method === "GET") return get(req, res); + if (req.method === "POST") return await post(req, res); +} + +const searchFilters = [["email"], ["user"], ["userId"]]; + +async function post(req: NextApiRequest, res: NextApiResponse) { + // verify if it's a logged user that is trying to export + if (req.session.user) { + if ( + !checkAccess(req.session.user, ["mastercorporate", "developer", "admin"]) + ) { + return res.status(401).json({ error: "Unauthorized" }); + } + const { ids, startDate, endDate, searchText } = req.body as { + ids: string[]; + startDate?: string; + endDate?: string; + searchText: string; + }; + const startDateParsed = startDate ? new Date(startDate) : undefined; + const endDateParsed = endDate ? new Date(endDate) : undefined; + const assignments = await getAssignmentsForCorporates( + ids, + startDateParsed, + endDateParsed + ); + + const assignmentUsers = [ + ...new Set(assignments.flatMap((a) => a.assignees)), + ]; + const users = await getSpecificUsers(assignmentUsers); + + const tableResults = assignments.reduce( + (accmA: TableData[], a: AssignmentWithCorporateId) => { + const userResults = a.assignees.map((assignee) => { + const userStats = + a.results.find((r) => r.user === assignee)?.stats || []; + const userData = users.find((u) => u.id === assignee); + const corporate = users.find((u) => u.id === a.assigner)?.name || ""; + const commonData = { + user: userData?.name || "", + email: userData?.email || "", + userId: assignee, + corporateId: a.corporateId, + corporate, + assignment: a.name, + }; + if (userStats.length === 0) { + return { + ...commonData, + correct: 0, + submitted: false, + // date: moment(), + }; + } + + return { + ...commonData, + correct: userStats.reduce((n, e) => n + e.score.correct, 0), + submitted: true, + date: moment.max(userStats.map((e) => moment(e.date))), + }; + }) as TableData[]; + + return [...accmA, ...userResults]; + }, + [] + ); + + // Create a new workbook and add a worksheet + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet("Master Statistical"); + + const headers = [ + { + label: "User", + value: (entry: TableData) => entry.user, + }, + { + label: "Email", + value: (entry: TableData) => entry.email, + }, + { + label: "Corporate", + value: (entry: TableData) => entry.corporate, + }, + { + label: "Assignment", + value: (entry: TableData) => entry.assignment, + }, + { + label: "Submitted", + value: (entry: TableData) => (entry.submitted ? "Yes" : "No"), + }, + { + label: "Correct", + value: (entry: TableData) => entry.correct, + }, + { + label: "Date", + value: (entry: TableData) => entry.date?.format("YYYY/MM/DD") || '', + }, + ]; + + const filteredSearch = searchText ? search(searchText, searchFilters, tableResults) : tableResults; + + worksheet.addRow(headers.map((h) => h.label)); + (filteredSearch as TableData[]).forEach((entry) => { + worksheet.addRow(headers.map((h) => h.value(entry))); + }); + + // Convert workbook to Buffer (Node.js) or Blob (Browser) + const buffer = await workbook.xlsx.writeBuffer(); + + // generate the file ref for storage + const fileName = `${Date.now().toString()}.xlsx`; + const refName = `statistical/${fileName}`; + const fileRef = ref(storage, refName); + // upload the pdf to storage + const snapshot = await uploadBytes(fileRef, buffer, { + contentType: + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }); + + const url = await getDownloadURL(fileRef); + res.status(200).end(url); + return; + } + + return res.status(401).json({ error: "Unauthorized" }); +} diff --git a/src/utils/assignments.be.ts b/src/utils/assignments.be.ts index 03be8da3..61832ab0 100644 --- a/src/utils/assignments.be.ts +++ b/src/utils/assignments.be.ts @@ -1,6 +1,7 @@ import {app} from "@/firebase"; import {Assignment} from "@/interfaces/results"; import {collection, getDocs, getFirestore, query, where} from "firebase/firestore"; +import {getAllAssignersByCorporate} from "@/utils/groups.be"; const db = getFirestore(app); @@ -34,3 +35,33 @@ export const getAssignmentsByAssignerBetweenDates = async (id: string, startDate export const getAssignmentsByAssigners = async (ids: string[], startDate?: Date, endDate?: Date) => { return (await Promise.all(ids.map((id) => getAssignmentsByAssigner(id, startDate, endDate)))).flat(); }; + +export const getAssignmentsForCorporates = async (idsList: string[], startDate?: Date, endDate?: Date) => { + const assigners = await Promise.all( + idsList.map(async (id) => { + const assigners = await getAllAssignersByCorporate(id); + return { + corporateId: id, + assigners, + }; + }), + ); + + const assignments = await Promise.all( + assigners.map(async (data) => { + try { + const assigners = [...new Set([...data.assigners, data.corporateId])]; + const assignments = await getAssignmentsByAssigners(assigners, startDate, endDate); + return assignments.map((assignment) => ({ + ...assignment, + corporateId: data.corporateId, + })); + } catch (err) { + console.error(err); + return []; + } + }), + ); + + return assignments.flat(); +} \ No newline at end of file diff --git a/src/utils/search.ts b/src/utils/search.ts new file mode 100644 index 00000000..eeb1fbc4 --- /dev/null +++ b/src/utils/search.ts @@ -0,0 +1,29 @@ + +/*fields example = [ + ['id'], + ['companyInformation', 'companyInformation', 'name'] +]*/ + +const getFieldValue = (fields: string[], data: any): string => { + if (fields.length === 0) return data; + const [key, ...otherFields] = fields; + + if (data[key]) return getFieldValue(otherFields, data[key]); + return data; +}; + +export const search = (text: string, fields: string[][], rows: any[]) => { + const searchText = text.toLowerCase(); + return rows.filter((row) => { + return fields.some((fieldsKeys) => { + const value = getFieldValue(fieldsKeys, row); + if (typeof value === "string") { + return value.toLowerCase().includes(searchText); + } + + if (typeof value === "number") { + return (value as Number).toString().includes(searchText); + } + }); + }); +} \ No newline at end of file From e433a150a9b54513f00b6e8655ecee076bb5bd4f Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Thu, 5 Sep 2024 23:30:03 +0100 Subject: [PATCH 3/7] Added level export to excel --- src/dashboards/MasterStatistical.tsx | 39 ++++------- .../api/assignments/statistical/excel.ts | 69 +++++++++++++++++-- src/pages/api/grading/index.ts | 16 +---- src/utils/grading.be.ts | 23 +++++++ 4 files changed, 104 insertions(+), 43 deletions(-) create mode 100644 src/utils/grading.be.ts diff --git a/src/dashboards/MasterStatistical.tsx b/src/dashboards/MasterStatistical.tsx index 944b0972..d693ef0d 100644 --- a/src/dashboards/MasterStatistical.tsx +++ b/src/dashboards/MasterStatistical.tsx @@ -18,6 +18,7 @@ import Checkbox from "@/components/Low/Checkbox"; import { useListSearch } from "@/hooks/useListSearch"; import axios from "axios"; import { toast } from "react-toastify"; +import Button from "@/components/Low/Button"; interface Props { corporateUsers: User[]; users: User[]; @@ -207,10 +208,11 @@ const MasterStatistical = (props: Props) => { }), ]; - const { rows: filteredRows, renderSearch, text: searchText } = useListSearch( - searchFilters, - tableResults - ); + const { + rows: filteredRows, + renderSearch, + text: searchText, + } = useListSearch(searchFilters, tableResults); const table = useReactTable({ data: filteredRows, @@ -240,7 +242,6 @@ const MasterStatistical = (props: Props) => { ); }; - const triggerDownload = async () => { try { setDownloading(true); @@ -267,28 +268,9 @@ const MasterStatistical = (props: Props) => { } }; - const renderIcon = () => { - if (downloading) { - return ( - - ); - } - - return ( - { - e.stopPropagation(); - triggerDownload(); - }} - /> - ); - }; - const consolidateResults = getStudentsConsolidateScore(); return ( <> - {renderIcon()}
{ />
{renderSearch()} +
+ +
diff --git a/src/pages/api/assignments/statistical/excel.ts b/src/pages/api/assignments/statistical/excel.ts index 54c8153e..ce4ebac0 100644 --- a/src/pages/api/assignments/statistical/excel.ts +++ b/src/pages/api/assignments/statistical/excel.ts @@ -11,7 +11,11 @@ import { getSpecificUsers } from "@/utils/users.be"; import { checkAccess } from "@/utils/permissions"; import { getAssignmentsForCorporates } from "@/utils/assignments.be"; import { search } from "@/utils/search"; - +import { getGradingSystem } from "@/utils/grading.be"; +import { Exam } from "@/interfaces/exam"; +import { User } from "@/interfaces/user"; +import { calculateBandScore, getGradingLabel } from "@/utils/score"; +import { Module } from "@/interfaces"; const db = getFirestore(app); export default withIronSessionApiRoute(handler, sessionOptions); @@ -25,6 +29,7 @@ interface TableData { date: moment.Moment; assignment: string; corporateId: string; + level: string; } async function handler(req: NextApiRequest, res: NextApiResponse) { @@ -59,7 +64,42 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const assignmentUsers = [ ...new Set(assignments.flatMap((a) => a.assignees)), ]; + const assigners = [...new Set(assignments.map((a) => a.assigner))]; const users = await getSpecificUsers(assignmentUsers); + const assignerUsers = await getSpecificUsers(assigners); + + const assignerUsersGradingSystems = await Promise.all( + assignerUsers.map(async (user: User) => { + const data = await getGradingSystem(user); + // in this context I need to override as I'll have to match to the assigner + return { ...data, user: user.id }; + }) + ); + + const getGradingSystemHelper = ( + exams: {id: string; module: Module; assignee: string}[], + assigner: string, + user: User, + correct: number, + total: number + ) => { + if (exams.some((e) => e.module === "level")) { + const gradingSystem = assignerUsersGradingSystems.find( + (gs) => gs.user === assigner + ); + if (gradingSystem) { + const bandScore = calculateBandScore( + correct, + total, + "level", + user.focus + ); + return getGradingLabel(bandScore, gradingSystem?.steps || []); + } + } + + return "N/A"; + }; const tableResults = assignments.reduce( (accmA: TableData[], a: AssignmentWithCorporateId) => { @@ -67,14 +107,25 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const userStats = a.results.find((r) => r.user === assignee)?.stats || []; const userData = users.find((u) => u.id === assignee); - const corporate = users.find((u) => u.id === a.assigner)?.name || ""; + const corporateUser = users.find((u) => u.id === a.assigner); + const correct = userStats.reduce((n, e) => n + e.score.correct, 0); + const total = userStats.reduce((n, e) => n + e.score.total, 0); + const level = getGradingSystemHelper( + a.exams, + a.assigner, + userData!, + correct, + total + ); + console.log("Level", level); const commonData = { user: userData?.name || "", email: userData?.email || "", userId: assignee, corporateId: a.corporateId, - corporate, + corporate: corporateUser?.name || "", assignment: a.name, + level, }; if (userStats.length === 0) { return { @@ -87,7 +138,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { return { ...commonData, - correct: userStats.reduce((n, e) => n + e.score.correct, 0), + correct, submitted: true, date: moment.max(userStats.map((e) => moment(e.date))), }; @@ -129,11 +180,17 @@ async function post(req: NextApiRequest, res: NextApiResponse) { }, { label: "Date", - value: (entry: TableData) => entry.date?.format("YYYY/MM/DD") || '', + value: (entry: TableData) => entry.date?.format("YYYY/MM/DD") || "", }, + { + label: "Level", + value: (entry: TableData) => entry.level, + } ]; - const filteredSearch = searchText ? search(searchText, searchFilters, tableResults) : tableResults; + const filteredSearch = searchText + ? search(searchText, searchFilters, tableResults) + : tableResults; worksheet.addRow(headers.map((h) => h.label)); (filteredSearch as TableData[]).forEach((entry) => { diff --git a/src/pages/api/grading/index.ts b/src/pages/api/grading/index.ts index 6beb7e24..bc829e00 100644 --- a/src/pages/api/grading/index.ts +++ b/src/pages/api/grading/index.ts @@ -15,6 +15,7 @@ import {Grading} from "@/interfaces"; import {getGroupsForUser} from "@/utils/groups.be"; import {uniq} from "lodash"; import {getUser} from "@/utils/users.be"; +import { getGradingSystem } from "@/utils/grading.be"; const db = getFirestore(app); @@ -31,19 +32,8 @@ async function get(req: NextApiRequest, res: NextApiResponse) { return; } - const snapshot = await getDoc(doc(db, "grading", req.session.user.id)); - if (snapshot.exists()) return res.status(200).json(snapshot.data()); - - if (req.session.user.type !== "teacher" && req.session.user.type !== "student") - return res.status(200).json({steps: CEFR_STEPS, user: req.session.user.id}); - - const corporate = await getUserCorporate(req.session.user.id); - if (!corporate) return res.status(200).json(CEFR_STEPS); - - const corporateSnapshot = await getDoc(doc(db, "grading", corporate.id)); - if (corporateSnapshot.exists()) return res.status(200).json(snapshot.data()); - - return res.status(200).json({steps: CEFR_STEPS, user: req.session.user.id}); + const gradingSystem = await getGradingSystem(req.session.user); + return res.status(200).json(gradingSystem); } async function post(req: NextApiRequest, res: NextApiResponse) { diff --git a/src/utils/grading.be.ts b/src/utils/grading.be.ts new file mode 100644 index 00000000..d934f839 --- /dev/null +++ b/src/utils/grading.be.ts @@ -0,0 +1,23 @@ +import { app } from "@/firebase"; +import { getFirestore, doc, getDoc } from "firebase/firestore"; +import { CEFR_STEPS } from "@/resources/grading"; +import { getUserCorporate } from "@/utils/groups.be"; +import { User } from "@/interfaces/user"; +import { Grading } from "@/interfaces"; +const db = getFirestore(app); + +export const getGradingSystem = async (user: User): Promise => { + const snapshot = await getDoc(doc(db, "grading", user.id)); + if (snapshot.exists()) return snapshot.data() as Grading; + + if (user.type !== "teacher" && user.type !== "student") + return { steps: CEFR_STEPS, user: user.id }; + + const corporate = await getUserCorporate(user.id); + if (!corporate) return { steps: CEFR_STEPS, user: user.id }; + + const corporateSnapshot = await getDoc(doc(db, "grading", corporate.id)); + if (corporateSnapshot.exists()) return corporateSnapshot.data() as Grading; + + return { steps: CEFR_STEPS, user: user.id }; +}; From 271ca7069ee11a4ee603e674cc99d3d0b0363e7a Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Fri, 6 Sep 2024 10:10:50 +0100 Subject: [PATCH 4/7] Updated it so it also appears to teachers --- src/pages/api/users/list.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/pages/api/users/list.ts b/src/pages/api/users/list.ts index d5e8c93d..b6d13ade 100644 --- a/src/pages/api/users/list.ts +++ b/src/pages/api/users/list.ts @@ -25,6 +25,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { if (!req.session.user) return res.status(200).json(users); if (req.session.user.type === "admin" || req.session.user.type === "developer") return res.status(200).json(users); + if (req.session.user.type === "teacher") { + const adminGroups = await getGroupsForUser(req.session.user.id); + const belongingGroups = await getGroupsForUser(undefined, req.session.user.id); + + const participants = uniq([...adminGroups.flatMap((x) => x.participants), ...belongingGroups.flat().flatMap((x) => x.participants)]); + return res.status(200).json(users.filter((x) => participants.includes(x.id))); + } const adminGroups = await getGroupsForUser(req.session.user.id); const groups = await Promise.all(adminGroups.flatMap((x) => x.participants).map(async (x) => await getGroupsForUser(x))); From b92a4285c993199a52cad30406a0d09f140e79d3 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Fri, 6 Sep 2024 10:12:19 +0100 Subject: [PATCH 5/7] Removed the user itself --- src/pages/api/users/list.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/api/users/list.ts b/src/pages/api/users/list.ts index b6d13ade..0a5273de 100644 --- a/src/pages/api/users/list.ts +++ b/src/pages/api/users/list.ts @@ -30,12 +30,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { const belongingGroups = await getGroupsForUser(undefined, req.session.user.id); const participants = uniq([...adminGroups.flatMap((x) => x.participants), ...belongingGroups.flat().flatMap((x) => x.participants)]); - return res.status(200).json(users.filter((x) => participants.includes(x.id))); + return res.status(200).json(users.filter((x) => participants.includes(x.id) && x.id !== req.session.user!.id)); } const adminGroups = await getGroupsForUser(req.session.user.id); const groups = await Promise.all(adminGroups.flatMap((x) => x.participants).map(async (x) => await getGroupsForUser(x))); const participants = uniq([...adminGroups.flatMap((x) => x.participants), ...groups.flat().flatMap((x) => x.participants)]); - res.status(200).json(users.filter((x) => participants.includes(x.id))); + res.status(200).json(users.filter((x) => participants.includes(x.id) && x.id !== req.session.user!.id)); } From 08945bfbdd8261d10ec6b05cee3504c5acb64da8 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Fri, 6 Sep 2024 10:13:30 +0100 Subject: [PATCH 6/7] Forgot to add this one --- src/utils/groups.be.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/groups.be.ts b/src/utils/groups.be.ts index 0a911581..269eded7 100644 --- a/src/utils/groups.be.ts +++ b/src/utils/groups.be.ts @@ -79,7 +79,7 @@ export const getAllAssignersByCorporate = async (corporateID: string): Promise !!x).flat() as string[]; }; -export const getGroupsForUser = async (admin: string, participant?: string) => { +export const getGroupsForUser = async (admin?: string, participant?: string) => { try { const queryConstraints = [ ...(admin ? [where("admin", "==", admin)] : []), From f9216637df5d8ce2b182e43a7beb7349fdd3283b Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Fri, 6 Sep 2024 10:39:16 +0100 Subject: [PATCH 7/7] Extracted the function --- src/pages/api/users/list.ts | 30 +++--------------------------- src/utils/users.be.ts | 25 ++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/pages/api/users/list.ts b/src/pages/api/users/list.ts index 0a5273de..3140c9bf 100644 --- a/src/pages/api/users/list.ts +++ b/src/pages/api/users/list.ts @@ -1,13 +1,8 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction import type {NextApiRequest, NextApiResponse} from "next"; -import {app} from "@/firebase"; -import {getFirestore, collection, getDocs} from "firebase/firestore"; import {withIronSessionApiRoute} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; -import {getGroupsForUser} from "@/utils/groups.be"; -import {uniq} from "lodash"; - -const db = getFirestore(app); +import {getLinkedUsers} from "@/utils/users.be"; export default withIronSessionApiRoute(handler, sessionOptions); @@ -17,25 +12,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { return; } - const snapshot = await getDocs(collection(db, "users")); - const users = snapshot.docs.map((doc) => ({ - id: doc.id, - ...doc.data(), - })); - - if (!req.session.user) return res.status(200).json(users); - if (req.session.user.type === "admin" || req.session.user.type === "developer") return res.status(200).json(users); - if (req.session.user.type === "teacher") { - const adminGroups = await getGroupsForUser(req.session.user.id); - const belongingGroups = await getGroupsForUser(undefined, req.session.user.id); - - const participants = uniq([...adminGroups.flatMap((x) => x.participants), ...belongingGroups.flat().flatMap((x) => x.participants)]); - return res.status(200).json(users.filter((x) => participants.includes(x.id) && x.id !== req.session.user!.id)); - } - - const adminGroups = await getGroupsForUser(req.session.user.id); - const groups = await Promise.all(adminGroups.flatMap((x) => x.participants).map(async (x) => await getGroupsForUser(x))); - const participants = uniq([...adminGroups.flatMap((x) => x.participants), ...groups.flat().flatMap((x) => x.participants)]); - - res.status(200).json(users.filter((x) => participants.includes(x.id) && x.id !== req.session.user!.id)); + const users = await getLinkedUsers(req.session.user?.id, req.session.user?.type); + res.status(200).json(users); } diff --git a/src/utils/users.be.ts b/src/utils/users.be.ts index be8d1f9b..24c74138 100644 --- a/src/utils/users.be.ts +++ b/src/utils/users.be.ts @@ -1,7 +1,7 @@ import {app} from "@/firebase"; import {collection, doc, getDoc, getDocs, getFirestore, query, where} from "firebase/firestore"; -import {CorporateUser, Group, User} from "@/interfaces/user"; +import {CorporateUser, Group, Type, User} from "@/interfaces/user"; import {getGroupsForUser} from "./groups.be"; import {uniq, uniqBy} from "lodash"; import {getUserCodes} from "./codes.be"; @@ -38,6 +38,29 @@ export async function getSpecificUsers(ids: string[]) { return groups; } +export async function getLinkedUsers(userID?: string, type?: Type) { + const snapshot = await getDocs(collection(db, "users")); + const users = snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })) as User[]; + + if (!userID) return users; + if (type === "admin" || type === "developer") return users; + + const adminGroups = await getGroupsForUser(userID); + const groups = await Promise.all(adminGroups.flatMap((x) => x.participants).map(async (x) => await getGroupsForUser(x))); + const belongingGroups = await getGroupsForUser(undefined, userID); + + const participants = uniq([ + ...adminGroups.flatMap((x) => x.participants), + ...groups.flat().flatMap((x) => x.participants), + ...(type === "teacher" ? belongingGroups.flatMap((x) => x.participants) : []), + ]); + + return users.filter((x) => participants.includes(x.id) && x.id !== userID); +} + export async function getUserBalance(user: User) { const codes = await getUserCodes(user.id); if (user.type !== "corporate" && user.type !== "mastercorporate") return codes.length;