Added an export feature for the master statisticl screen

This commit is contained in:
Joao Ramos
2024-09-05 22:42:46 +01:00
parent 70de97766e
commit a61ad2cc7e
6 changed files with 309 additions and 89 deletions

View File

@@ -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<boolean>(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 (
<span className={`text-mti-gray-dim loading loading-infinity w-6`} />
);
}
return (
<BsFileExcel
className={`text-mti-gray-dim text-2xl cursor-pointer`}
onClick={(e) => {
e.stopPropagation();
triggerDownload();
}}
/>
);
};
const consolidateResults = getStudentsConsolidateScore();
return (
<>
{renderIcon()}
<div className="flex flex-wrap gap-2 items-center text-center">
<IconCard
Icon={BsBank}
@@ -283,7 +329,9 @@ const MasterStatistical = (props: Props) => {
</div>
<div className="flex gap-3 w-full">
<div className="flex flex-col gap-3">
<label className="font-normal text-base text-mti-gray-dim">Date</label>
<label className="font-normal text-base text-mti-gray-dim">
Date
</label>
<ReactDatePicker
dateFormat="dd/MM/yyyy"
className="px-4 py-6 w-52 text-sm text-center font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none"

View File

@@ -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<T>(fields: string[][], rows: T[]) {
const [text, setText] = useState("");
@@ -20,22 +8,11 @@ export function useListSearch<T>(fields: string[][], rows: T[]) {
const renderSearch = () => <Input label="Search" type="text" name="search" onChange={setText} placeholder="Enter search text" value={text} />;
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,
};

View File

@@ -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});
}

View File

@@ -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" });
}

View File

@@ -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();
}

29
src/utils/search.ts Normal file
View File

@@ -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);
}
});
});
}