From 9db33e6a51ac8a10e31e402e2e708961ff96d9c7 Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Tue, 20 Aug 2024 09:17:48 +0100 Subject: [PATCH 1/2] Fixed date usage --- src/pages/api/assignments/[id]/[export]/excel.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/pages/api/assignments/[id]/[export]/excel.ts b/src/pages/api/assignments/[id]/[export]/excel.ts index 20661676..633c51e5 100644 --- a/src/pages/api/assignments/[id]/[export]/excel.ts +++ b/src/pages/api/assignments/[id]/[export]/excel.ts @@ -112,6 +112,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const assigneesData = data.assignees .map((assignee: string) => { const userStats = allStats.filter((s: any) => s.user === assignee); + const dates = userStats.map((s: any) => moment(s.date)); return { userId: assignee, user: users.find((u) => u.id === assignee), @@ -126,6 +127,8 @@ async function post(req: NextApiRequest, res: NextApiResponse) { }, { correct: 0, missing: 0, total: 0 } ), + firstDate: moment.min(...dates), + lastDate: moment.max(...dates), stats: userStats, }; }) @@ -135,13 +138,8 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const highestScore = Math.max(...results); const lowestScore = Math.min(...results); const averageScore = results.reduce((a, b) => a + b, 0) / results.length; - - const dates = assigneesData - .flatMap((r: any) => r.stats) - .map((r: any) => r.date) - .map((d: number) => moment(d)); - const firstDate = moment.min(dates); - const lastDate = moment.max(dates); + const firstDate = moment.min(assigneesData.map((r: any) => r.firstDate)); + const lastDate = moment.max(assigneesData.map((r: any) => r.lastDate)); const firstSectionData = [ { From 158324a70587bfee4f05ef4deb937ce6bdf9e3f4 Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Tue, 20 Aug 2024 11:10:51 +0100 Subject: [PATCH 2/2] Added second excel for master corporate export --- .../api/assignments/[id]/[export]/excel.ts | 568 +++++++++++------- src/pages/api/groups/index.ts | 26 +- src/utils/groups.be.ts | 51 ++ src/utils/users.be.ts | 43 +- 4 files changed, 424 insertions(+), 264 deletions(-) diff --git a/src/pages/api/assignments/[id]/[export]/excel.ts b/src/pages/api/assignments/[id]/[export]/excel.ts index 633c51e5..bf19569e 100644 --- a/src/pages/api/assignments/[id]/[export]/excel.ts +++ b/src/pages/api/assignments/[id]/[export]/excel.ts @@ -13,30 +13,34 @@ import { } from "firebase/firestore"; import { withIronSessionApiRoute } from "iron-session/next"; import { sessionOptions } from "@/lib/session"; -import ReactPDF from "@react-pdf/renderer"; -import GroupTestReport from "@/exams/pdf/group.test.report"; import { ref, uploadBytes, getDownloadURL } from "firebase/storage"; -import { Stat, CorporateUser } from "@/interfaces/user"; -import { User, DemographicInformation } from "@/interfaces/user"; +import { CorporateUser, MasterCorporateUser } from "@/interfaces/user"; +import { User } from "@/interfaces/user"; import { Module } from "@/interfaces"; -import { ModuleScore, StudentData } from "@/interfaces/module.scores"; -import { SkillExamDetails } from "@/exams/pdf/details/skill.exam"; -import { LevelExamDetails } from "@/exams/pdf/details/level.exam"; -import { calculateBandScore, getLevelScore } from "@/utils/score"; -import { - generateQRCode, - getRadialProgressPNG, - streamToBuffer, -} from "@/utils/pdf"; -import { Group } from "@/interfaces/user"; import moment from "moment-timezone"; import ExcelJS from "exceljs"; - +import { getStudentGroupsForUsersWithoutAdmin } from "@/utils/groups.be"; +import { getSpecificUsers, getUser } from "@/utils/users.be"; +import { getUserName } from "@/utils/users"; interface GroupScoreSummaryHelper { score: [number, number]; label: string; sessions: string[]; } + +interface AssignmentData { + assigner: string; + assignees: string[]; + results: any; + exams: { module: Module }[]; + startDate: string; + excel: { + path: string; + version: string; + }; + name: string; +} + const db = getFirestore(app); export default withIronSessionApiRoute(handler, sessionOptions); @@ -55,24 +59,317 @@ function logWorksheetData(worksheet: any) { }); } +function commonExcel({ + data, + userName, + users, + sectionName, + customTable, + customTableHeaders, + renderCustomTableData, +}: { + data: AssignmentData; + userName: string; + users: User[]; + sectionName: string; + customTable: string[][]; + customTableHeaders: string[]; + renderCustomTableData: (data: any) => string[]; +}) { + const allStats = data.results.flatMap((r: any) => r.stats); + + const uniqueExercises = [...new Set(allStats.map((s: any) => s.exercise))]; + + const assigneesData = data.assignees + .map((assignee: string) => { + const userStats = allStats.filter((s: any) => s.user === assignee); + const dates = userStats.map((s: any) => moment(s.date)); + return { + userId: assignee, + user: users.find((u) => u.id === assignee), + ...userStats.reduce( + (acc: any, curr: any) => { + return { + ...acc, + correct: acc.correct + curr.score.correct, + missing: acc.missing + curr.score.missing, + total: acc.total + curr.score.total, + }; + }, + { correct: 0, missing: 0, total: 0 } + ), + firstDate: moment.min(...dates), + lastDate: moment.max(...dates), + stats: userStats, + }; + }) + .sort((a, b) => b.correct - a.correct); + + const results = assigneesData.map((r: any) => r.correct); + const highestScore = Math.max(...results); + const lowestScore = Math.min(...results); + const averageScore = results.reduce((a, b) => a + b, 0) / results.length; + const firstDate = moment.min(assigneesData.map((r: any) => r.firstDate)); + const lastDate = moment.max(assigneesData.map((r: any) => r.lastDate)); + + const firstSectionData = [ + { + label: sectionName, + value: userName, + }, + { + label: "Report Download date :", + value: moment().format("DD/MM/YYYY"), + }, + { label: "Test Information :", value: data.name }, + { + label: "Date of Test :", + value: moment(data.startDate).format("DD/MM/YYYY"), + }, + { label: "Number of Candidates :", value: data.assignees.length }, + { label: "Highest score :", value: highestScore }, + { label: "Lowest score :", value: lowestScore }, + { label: "Average score :", value: averageScore }, + { label: "", value: "" }, + { + label: "Date and time of First submission :", + value: firstDate.format("DD/MM/YYYY"), + }, + { + label: "Date and time of Last submission :", + value: lastDate.format("DD/MM/YYYY"), + }, + ]; + + // Create a new workbook and add a worksheet + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet("Report Data"); + + // Populate the worksheet with the data + firstSectionData.forEach(({ label, value }, index) => { + worksheet.getCell(`A${index + 1}`).value = label; // First column (labels) + worksheet.getCell(`B${index + 1}`).value = value; // Second column (values) + }); + + // added empty arrays to force row spacings + const customTableAndLine = [[],...customTable, []]; + customTableAndLine.forEach((row: string[], index) => { + worksheet.addRow(row); + }); + + // Define the static part of the headers (before "Test Sections") + const staticHeaders = [ + "Sr N", + "Candidate ID", + "First and Last Name", + "Passport/ID", + "Email ID", + "Gender", + ...customTableHeaders, + ]; + + // Define additional headers after "Test Sections" + const additionalHeaders = ["Time Spent", "Date", "Score"]; + + // Calculate the dynamic columns based on the testSectionsArray + const testSectionHeaders = uniqueExercises.map( + (section, index) => `Part ${index + 1}` + ); + + const tableColumnHeadersFirstPart = [ + ...staticHeaders, + ...uniqueExercises.map((a) => "Test Sections"), + ]; + // Add the main header row, merging static columns and "Test Sections" + const tableColumnHeaders = [ + ...tableColumnHeadersFirstPart, + ...additionalHeaders, + ]; + worksheet.addRow(tableColumnHeaders); + + // 1 headers rows + const startIndexTable = firstSectionData.length + customTableAndLine.length + 1; + + // // Merge "Test Sections" over dynamic number of columns + // const tableColumns = staticHeaders.length + numberOfTestSections; + + // K10:M12 = 10,11,12,13 + // horizontally group Test Sections + worksheet.mergeCells( + startIndexTable, + staticHeaders.length + 1, + startIndexTable, + tableColumnHeadersFirstPart.length + ); + + // Add the dynamic second and third header rows for test sections and sub-columns + worksheet.addRow([ + ...Array(staticHeaders.length).fill(""), + ...testSectionHeaders, + "", + "", + "", + ]); + worksheet.addRow([ + ...Array(staticHeaders.length).fill(""), + ...uniqueExercises.map(() => "Grammar & Vocabulary"), + "", + "", + "", + ]); + worksheet.addRow([ + ...Array(staticHeaders.length).fill(""), + ...uniqueExercises.map( + (exercise) => allStats.find((s: any) => s.exercise === exercise).type + ), + "", + "", + "", + ]); + + // vertically group based on the part, exercise and type + staticHeaders.forEach((header, index) => { + worksheet.mergeCells(startIndexTable, index + 1, startIndexTable + 3, index + 1); + }); + + assigneesData.forEach((data, index) => { + worksheet.addRow([ + index + 1, + data.userId, + data.user.name, + data.user.demographicInformation?.passportId, + data.user.email, + data.user.demographicInformation?.gender, + ...renderCustomTableData(data), + ...uniqueExercises.map((exercise) => { + const score = data.stats.find( + (s: any) => s.exercise === exercise && s.user === data.userId + ).score; + return `${score.correct}/${score.total}`; + }), + `${Math.ceil( + data.stats.reduce((acc: number, curr: any) => acc + curr.timeSpent, 0) / + 60 + )} minutes`, + data.lastDate.format("DD/MM/YYYY HH:mm"), + data.correct, + ]); + }); + + worksheet.addRow([""]); + worksheet.addRow([""]); + + for (let i = 0; i < tableColumnHeaders.length; i++) { + worksheet.getColumn(i + 1).width = 30; + } + + // Apply styles to the headers + [startIndexTable].forEach((rowNumber) => { + worksheet.getRow(rowNumber).eachCell((cell) => { + if (cell.value) { + cell.fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "FFBFBFBF" }, // Grey color for headers + }; + cell.font = { bold: true }; + cell.alignment = { vertical: "middle", horizontal: "center" }; + } + }); + }); + + worksheet.addRow(["Printed by: Confidential Information"]); + worksheet.addRow(["info@encoach.com"]); + + // Convert workbook to Buffer (Node.js) or Blob (Browser) + return workbook.xlsx.writeBuffer(); +} + +function corporateAssignment( + user: CorporateUser, + data: AssignmentData, + users: User[] +) { + return commonExcel({ + data, + userName: user.corporateInformation?.companyInformation?.name || "", + users, + sectionName: "Corporate Name :", + customTable: [], + customTableHeaders: [], + renderCustomTableData: () => [], + }); +} + +async function mastercorporateAssignment( + user: MasterCorporateUser, + data: AssignmentData, + users: User[] +) { + const userGroups = await getStudentGroupsForUsersWithoutAdmin( + user.id, + data.assignees + ); + const adminUsers = [...new Set(userGroups.map((g) => g.admin))]; + + const userGroupsParticipants = userGroups.flatMap((g) => g.participants); + const adminsData = await getSpecificUsers(adminUsers); + const companiesData = adminsData.map((user) => { + const name = getUserName(user); + const users = userGroupsParticipants + .filter((p) => data.assignees.includes(p)); + + const stats = data.results + .flatMap((r: any) => r.stats) + .filter((s: any) => users.includes(s.user)); + const correct = stats.reduce((acc: number, s: any) => acc + s.score.correct, 0); + const total = stats.reduce( + (acc: number, curr: any) => acc + curr.score.total, + 0 + ); + + return { + name, + correct, + total, + }; + }); + + const customTable = [ + ...companiesData, + { + name: "Total", + correct: companiesData.reduce((acc, curr) => acc + curr.correct, 0), + total: companiesData.reduce((acc, curr) => acc + curr.total, 0), + }, + ].map((c) => [c.name, `${c.correct}/${c.total}`]) + + const customTableHeaders = [{ name: "Corporate", helper: (data: any) => data.user.corporateName}]; + return commonExcel({ + data, + userName: user.corporateInformation?.companyInformation?.name || "", + users: users.map((u) => { + const userGroup = userGroups.find((g) => g.participants.includes(u.id)); + const admin = adminsData.find((a) => a.id === userGroup?.admin); + return { + ...u, + corporateName: getUserName(admin), + } + }), + sectionName: "Master Corporate Name :", + customTable: [['Corporate Summary'], ...customTable], + customTableHeaders: customTableHeaders.map((h) => h.name), + renderCustomTableData: (data) => customTableHeaders.map((h) => h.helper(data)), + }); +} + async function post(req: NextApiRequest, res: NextApiResponse) { // verify if it's a logged user that is trying to export if (req.session.user) { const { id } = req.query as { id: string }; const docSnap = await getDoc(doc(db, "assignments", id)); - const data = docSnap.data() as { - assigner: string; - assignees: string[]; - results: any; - exams: { module: Module }[]; - startDate: string; - excel: { - path: string; - version: string; - }; - name: string; - }; + const data = docSnap.data() as AssignmentData; if (!data) { res.status(400).end(); return; @@ -98,216 +395,29 @@ async function post(req: NextApiRequest, res: NextApiResponse) { id: d.id, })) as User[]; - const docUser = await getDoc(doc(db, "users", req.session.user.id)); + const docUser = await getDoc(doc(db, "users", data.assigner)); if (docUser.exists()) { // we'll need the user in order to get the user data (name, email, focus, etc); - const user = docUser.data() as CorporateUser; + const user = docUser.data() as User; - const allStats = data.results.flatMap((r: any) => r.stats); - - const uniqueExercises = [ - ...new Set(allStats.map((s: any) => s.exercise)), - ]; - - const assigneesData = data.assignees - .map((assignee: string) => { - const userStats = allStats.filter((s: any) => s.user === assignee); - const dates = userStats.map((s: any) => moment(s.date)); - return { - userId: assignee, - user: users.find((u) => u.id === assignee), - ...userStats.reduce( - (acc: any, curr: any) => { - return { - ...acc, - correct: acc.correct + curr.score.correct, - missing: acc.missing + curr.score.missing, - total: acc.total + curr.score.total, - }; - }, - { correct: 0, missing: 0, total: 0 } - ), - firstDate: moment.min(...dates), - lastDate: moment.max(...dates), - stats: userStats, - }; - }) - .sort((a, b) => b.correct - a.correct); - - const results = assigneesData.map((r: any) => r.correct); - const highestScore = Math.max(...results); - const lowestScore = Math.min(...results); - const averageScore = results.reduce((a, b) => a + b, 0) / results.length; - const firstDate = moment.min(assigneesData.map((r: any) => r.firstDate)); - const lastDate = moment.max(assigneesData.map((r: any) => r.lastDate)); - - const firstSectionData = [ - { - label: "Corporate Name :", - value: user.corporateInformation?.companyInformation?.name || "", - }, - { - label: "Report Download date :", - value: moment().format("DD/MM/YYYY"), - }, - { label: "Test Information :", value: data.name}, - { - label: "Date of Test :", - value: moment(data.startDate).format("DD/MM/YYYY"), - }, - { label: "Number of Candidates :", value: data.assignees.length }, - { label: "Highest score :", value: highestScore }, - { label: "Lowest score :", value: lowestScore }, - { label: "Average score :", value: averageScore }, - { label: "", value: "" }, - { - label: "Date and time of First submission :", - value: firstDate.format("DD/MM/YYYY"), - }, - { - label: "Date and time of Last submission :", - value: lastDate.format("DD/MM/YYYY"), - }, - ]; - - // Create a new workbook and add a worksheet - const workbook = new ExcelJS.Workbook(); - const worksheet = workbook.addWorksheet("Report Data"); - - // Populate the worksheet with the data - firstSectionData.forEach(({ label, value }, index) => { - worksheet.getCell(`A${index + 1}`).value = label; // First column (labels) - worksheet.getCell(`B${index + 1}`).value = value; // Second column (values) - }); - - // Define the static part of the headers (before "Test Sections") - const staticHeaders = [ - "Sr N", - "Candidate ID", - "First and Last Name", - "Passport/ID", - "Email ID", - "Gender", - ]; - - // Define additional headers after "Test Sections" - const additionalHeaders = ["Time Spent", "Score"]; - - // Calculate the dynamic columns based on the testSectionsArray - const testSectionHeaders = uniqueExercises.map( - (section, index) => `Part ${index + 1}` - ); - - const tableColumnHeadersFirstPart = [ - ...staticHeaders, - ...uniqueExercises.map((a) => "Test Sections"), - ]; - // Add the main header row, merging static columns and "Test Sections" - const tableColumnHeaders = [ - ...tableColumnHeadersFirstPart, - ...additionalHeaders, - ]; - worksheet.addRow(tableColumnHeaders); - - // 1 headers rows - const startIndexTable = firstSectionData.length + 1; - - // // Merge "Test Sections" over dynamic number of columns - // const tableColumns = staticHeaders.length + numberOfTestSections; - worksheet.mergeCells( - 1, - staticHeaders.length + 1, - 1, - tableColumnHeadersFirstPart.length - ); - - - // Add the dynamic second and third header rows for test sections and sub-columns - worksheet.addRow([ - ...Array(staticHeaders.length).fill(""), - ...testSectionHeaders, - "", - "", - ]); - worksheet.addRow([ - ...Array(staticHeaders.length).fill(""), - ...uniqueExercises.map(() => "Grammar & Vocabulary"), - "", - "", - ]); - worksheet.addRow([ - ...Array(staticHeaders.length).fill(""), - ...uniqueExercises.map( - (exercise) => allStats.find((s: any) => s.exercise === exercise).type - ), - "", - "", - ]); - - // Merging static headers and "Test Sections" over dynamic columns - worksheet.mergeCells(`A${startIndexTable}:A${startIndexTable + 3}`); // "Sr N" - worksheet.mergeCells(`B${startIndexTable}:B${startIndexTable + 3}`); // "Candidate ID" - worksheet.mergeCells(`C${startIndexTable}:C${startIndexTable + 3}`); // "First and Last Name" - worksheet.mergeCells(`D${startIndexTable}:D${startIndexTable + 3}`); // "Passport/ID" - worksheet.mergeCells(`E${startIndexTable}:E${startIndexTable + 3}`); // "Email ID" - worksheet.mergeCells(`F${startIndexTable}:F${startIndexTable + 3}`); // "Gender" - - assigneesData.forEach((data, index) => { - worksheet.addRow([ - index + 1, - data.userId, - data.user.name, - data.user.demographicInformation?.passportId, - data.user.email, - data.user.demographicInformation?.gender, - ...uniqueExercises.map((exercise) => { - const score = data.stats.find( - (s: any) => s.exercise === exercise && s.user === data.userId - ).score; - return `${score.correct}/${score.total}`; - }), - `${ - Math.ceil(data.stats.reduce( - (acc: number, curr: any) => acc + curr.timeSpent, - 0 - ) / 60) - } minutes`, - data.correct, - ]); - }); - - worksheet.addRow([""]); - worksheet.addRow([""]); - - for (let i = 0; i < tableColumnHeaders.length; i++) { - worksheet.getColumn(i + 1).width = 30; - } - - // Apply styles to the headers - [startIndexTable].forEach((rowNumber) => { - worksheet.getRow(rowNumber).eachCell((cell) => { - if (cell.value) { - cell.fill = { - type: "pattern", - pattern: "solid", - fgColor: { argb: "FFBFBFBF" }, // Grey color for headers - }; - cell.font = { bold: true }; - cell.alignment = { vertical: "middle", horizontal: "center" }; - } - }); - }); - - worksheet.addRow(["Printed by: Confidential Information"]); - worksheet.addRow(["info@encoach.com"]); - - // 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 = `assignment_report/${fileName}`; const fileRef = ref(storage, refName); + const getExcelFn = () => { + switch (user.type) { + case "teacher": + case "corporate": + return corporateAssignment(user as CorporateUser, data, users); + case "mastercorporate": + return mastercorporateAssignment(user as MasterCorporateUser, data, users); + default: + throw new Error("Invalid user type"); + } + }; + const buffer = await getExcelFn(); + // upload the pdf to storage const snapshot = await uploadBytes(fileRef, buffer, { contentType: diff --git a/src/pages/api/groups/index.ts b/src/pages/api/groups/index.ts index d42bbae1..ee5bd098 100644 --- a/src/pages/api/groups/index.ts +++ b/src/pages/api/groups/index.ts @@ -14,7 +14,7 @@ import { withIronSessionApiRoute } from "iron-session/next"; import { sessionOptions } from "@/lib/session"; import { Group } from "@/interfaces/user"; import { v4 } from "uuid"; -import { updateExpiryDateOnGroup } from "@/utils/groups.be"; +import { updateExpiryDateOnGroup, getGroupsForUser } from "@/utils/groups.be"; const db = getFirestore(app); @@ -30,30 +30,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method === "POST") await post(req, res); } -const getGroupsForUser = async (admin: string, participant: string) => { - try { - const queryConstraints = [ - ...(admin ? [where("admin", "==", admin)] : []), - ...(participant - ? [where("participants", "array-contains", participant)] - : []), - ]; - const snapshot = await getDocs( - queryConstraints.length > 0 - ? query(collection(db, "groups"), ...queryConstraints) - : collection(db, "groups") - ); - const groups = snapshot.docs.map((doc) => ({ - id: doc.id, - ...doc.data(), - })) as Group[]; - - return groups; - } catch (e) { - console.error(e); - return []; - } -}; async function get(req: NextApiRequest, res: NextApiResponse) { const { admin, participant } = req.query as { admin: string; diff --git a/src/utils/groups.be.ts b/src/utils/groups.be.ts index 5b793f8d..bba422ff 100644 --- a/src/utils/groups.be.ts +++ b/src/utils/groups.be.ts @@ -54,3 +54,54 @@ export const getAllAssignersByCorporate = async (corporateID: string): Promise !!x).flat() as string[]; }; + +export const getGroupsForUser = async (admin: string, participant: string) => { + try { + const queryConstraints = [ + ...(admin ? [where("admin", "==", admin)] : []), + ...(participant + ? [where("participants", "array-contains", participant)] + : []), + ]; + const snapshot = await getDocs( + queryConstraints.length > 0 + ? query(collection(db, "groups"), ...queryConstraints) + : collection(db, "groups") + ); + const groups = snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })) as Group[]; + + return groups; + } catch (e) { + console.error(e); + return []; + } + }; + + export const getStudentGroupsForUsersWithoutAdmin = async (admin: string, participants: string[]) => { + try { + const queryConstraints = [ + ...(admin ? [where("admin", "!=", admin)] : []), + ...(participants + ? [where("participants", "array-contains-any", participants)] + : []), + where("name", "==", "Students"), + ]; + const snapshot = await getDocs( + queryConstraints.length > 0 + ? query(collection(db, "groups"), ...queryConstraints) + : collection(db, "groups") + ); + const groups = snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })) as Group[]; + + return groups; + } catch (e) { + console.error(e); + return []; + } + }; \ No newline at end of file diff --git a/src/utils/users.be.ts b/src/utils/users.be.ts index aef0634e..2f3bd51c 100644 --- a/src/utils/users.be.ts +++ b/src/utils/users.be.ts @@ -1,20 +1,43 @@ -import {app} from "@/firebase"; +import { app } from "@/firebase"; -import {collection, doc, getDoc, getDocs, getFirestore} from "firebase/firestore"; -import {User} from "@/interfaces/user"; +import { + collection, + doc, + getDoc, + getDocs, + getFirestore, + query, + where, +} from "firebase/firestore"; +import { User } from "@/interfaces/user"; const db = getFirestore(app); export async function getUsers() { - const snapshot = await getDocs(collection(db, "users")); + const snapshot = await getDocs(collection(db, "users")); - return snapshot.docs.map((doc) => ({ - id: doc.id, - ...doc.data(), - })) as User[]; + return snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })) as User[]; } export async function getUser(id: string) { - const userDoc = await getDoc(doc(db, "users", id)); + const userDoc = await getDoc(doc(db, "users", id)); - return {...userDoc.data(), id} as User; + return { ...userDoc.data(), id } as User; +} + +export async function getSpecificUsers(ids: string[]) { + if (ids.length === 0) return []; + + const snapshot = await getDocs( + query(collection(db, "users"), where("id", "in", ids)) + ); + + const groups = snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })) as User[]; + + return groups; }