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