From fea58a7b4088b4b9306144d2281959b7530a0b75 Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Thu, 15 Aug 2024 10:34:31 +0100 Subject: [PATCH 1/9] Added initial implementation for master statistical --- src/dashboards/MasterCorporate.tsx | 1042 ++++++++++------- src/dashboards/MasterStatistical.tsx | 40 + src/hooks/useAssignmentCorporates.tsx | 34 + src/hooks/useAssignments.tsx | 2 +- .../{corporate.ts => corporate/[id].ts} | 0 src/pages/api/assignments/corporate/index.ts | 54 + 6 files changed, 722 insertions(+), 450 deletions(-) create mode 100644 src/dashboards/MasterStatistical.tsx create mode 100644 src/hooks/useAssignmentCorporates.tsx rename src/pages/api/assignments/{corporate.ts => corporate/[id].ts} (100%) create mode 100644 src/pages/api/assignments/corporate/index.ts diff --git a/src/dashboards/MasterCorporate.tsx b/src/dashboards/MasterCorporate.tsx index e5eb1b96..4a2682c0 100644 --- a/src/dashboards/MasterCorporate.tsx +++ b/src/dashboards/MasterCorporate.tsx @@ -2,496 +2,640 @@ import Modal from "@/components/Modal"; import useStats from "@/hooks/useStats"; import useUsers from "@/hooks/useUsers"; -import {Group, MasterCorporateUser, Stat, User} from "@/interfaces/user"; -import UserList from "@/pages/(admin)/Lists/UserList"; -import {dateSorter} from "@/utils"; -import moment from "moment"; -import {useEffect, useState} from "react"; import { - BsArrowLeft, - BsClipboard2Data, - BsClock, - BsPaperclip, - BsPersonFill, - BsPencilSquare, - BsPersonCheck, - BsPeople, - BsBank, - BsEnvelopePaper, - BsArrowRepeat, - BsPlus, + Group, + MasterCorporateUser, + Stat, + User, + CorporateUser, +} from "@/interfaces/user"; +import UserList from "@/pages/(admin)/Lists/UserList"; +import { dateSorter } from "@/utils"; +import moment from "moment"; +import { useEffect, useState } from "react"; +import { + BsArrowLeft, + BsClipboard2Data, + BsClock, + BsPaperclip, + BsPersonFill, + BsPencilSquare, + BsPersonCheck, + BsPeople, + BsBank, + BsEnvelopePaper, + BsArrowRepeat, + BsPlus, + BsDatabase, } from "react-icons/bs"; import UserCard from "@/components/UserCard"; import useGroups from "@/hooks/useGroups"; -import {calculateAverageLevel, calculateBandScore} from "@/utils/score"; -import {MODULE_ARRAY} from "@/utils/moduleUtils"; -import {Module} from "@/interfaces"; -import {groupByExam} from "@/utils/stats"; +import { calculateAverageLevel, calculateBandScore } from "@/utils/score"; +import { MODULE_ARRAY } from "@/utils/moduleUtils"; +import { Module } from "@/interfaces"; +import { groupByExam } from "@/utils/stats"; import IconCard from "./IconCard"; import GroupList from "@/pages/(admin)/Lists/GroupList"; import useFilterStore from "@/stores/listFilterStore"; -import {useRouter} from "next/router"; +import { useRouter } from "next/router"; import useCodes from "@/hooks/useCodes"; import useAssignments from "@/hooks/useAssignments"; -import {Assignment} from "@/interfaces/results"; +import { Assignment } from "@/interfaces/results"; import AssignmentView from "./AssignmentView"; import AssignmentCreator from "./AssignmentCreator"; import clsx from "clsx"; import AssignmentCard from "./AssignmentCard"; - +import MasterStatistical from "./MasterStatistical"; interface Props { - user: MasterCorporateUser; + user: MasterCorporateUser; } -export default function MasterCorporateDashboard({user}: Props) { - const [page, setPage] = useState(""); - const [selectedUser, setSelectedUser] = useState(); - const [showModal, setShowModal] = useState(false); - const [selectedAssignment, setSelectedAssignment] = useState(); - const [isCreatingAssignment, setIsCreatingAssignment] = useState(false); +export default function MasterCorporateDashboard({ user }: Props) { + const [page, setPage] = useState(""); + const [selectedUser, setSelectedUser] = useState(); + const [showModal, setShowModal] = useState(false); + const [selectedAssignment, setSelectedAssignment] = useState(); + const [isCreatingAssignment, setIsCreatingAssignment] = useState(false); - const {stats} = useStats(); - const {users, reload} = useUsers(); - const {codes} = useCodes(user.id); - const {groups} = useGroups(user.id, user.type); + const { stats } = useStats(); + const { users, reload } = useUsers(); + const { codes } = useCodes(user.id); + const { groups } = useGroups(user.id, user.type); - const masterCorporateUserGroups = [...new Set(groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants))]; - const corporateUserGroups = [...new Set(groups.flatMap((g) => g.participants))]; + const masterCorporateUserGroups = [ + ...new Set( + groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants) + ), + ]; + const corporateUserGroups = [ + ...new Set(groups.flatMap((g) => g.participants)), + ]; - const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id}); + const { + assignments, + isLoading: isAssignmentsLoading, + reload: reloadAssignments, + } = useAssignments({ corporate: user.id }); - const appendUserFilters = useFilterStore((state) => state.appendUserFilter); - const router = useRouter(); + const appendUserFilters = useFilterStore((state) => state.appendUserFilter); + const router = useRouter(); - useEffect(() => { - setShowModal(!!selectedUser && page === ""); - }, [selectedUser, page]); + useEffect(() => { + setShowModal(!!selectedUser && page === ""); + }, [selectedUser, page]); - const studentFilter = (user: User) => user.type === "student" && corporateUserGroups.includes(user.id); - const teacherFilter = (user: User) => user.type === "teacher" && corporateUserGroups.includes(user.id); + const studentFilter = (user: User) => + user.type === "student" && corporateUserGroups.includes(user.id); + const teacherFilter = (user: User) => + user.type === "teacher" && corporateUserGroups.includes(user.id); - const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id); + const getStatsByStudent = (user: User) => + stats.filter((s) => s.user === user.id); - const UserDisplay = (displayUser: User) => ( -
setSelectedUser(displayUser)} - className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300"> - {displayUser.name} -
- {displayUser.name} - {displayUser.email} -
-
- ); + const UserDisplay = (displayUser: User) => ( +
setSelectedUser(displayUser)} + className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300" + > + {displayUser.name} +
+ {displayUser.name} + {displayUser.email} +
+
+ ); - const StudentsList = () => { - const filter = (x: User) => - x.type === "student" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id)); + const StudentsList = () => { + const filter = (x: User) => + x.type === "student" && + (!!selectedUser + ? corporateUserGroups.includes(x.id) || false + : corporateUserGroups.includes(x.id)); - return ( - ( -
-
setPage("")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> - - Back -
-

Students ({total})

-
- )} - /> - ); - }; + return ( + ( +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" + > + + Back +
+

Students ({total})

+
+ )} + /> + ); + }; - const TeachersList = () => { - const filter = (x: User) => - x.type === "teacher" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id)); + const TeachersList = () => { + const filter = (x: User) => + x.type === "teacher" && + (!!selectedUser + ? corporateUserGroups.includes(x.id) || false + : corporateUserGroups.includes(x.id)); - return ( - ( -
-
setPage("")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> - - Back -
-

Teachers ({total})

-
- )} - /> - ); - }; + return ( + ( +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" + > + + Back +
+

Teachers ({total})

+
+ )} + /> + ); + }; - const corporateUserFilter = (x: User) => - x.type === "corporate" && (!!selectedUser ? masterCorporateUserGroups.includes(x.id) || false : masterCorporateUserGroups.includes(x.id)); + const corporateUserFilter = (x: User) => + x.type === "corporate" && + (!!selectedUser + ? masterCorporateUserGroups.includes(x.id) || false + : masterCorporateUserGroups.includes(x.id)); - const CorporateList = () => { - return ( - ( -
-
setPage("")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> - - Back -
-

Corporates ({total})

-
- )} - /> - ); - }; + const CorporateList = () => { + return ( + ( +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" + > + + Back +
+

Corporates ({total})

+
+ )} + /> + ); + }; - const GroupsList = () => { - return ( - <> -
-
setPage("")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> - - Back -
-

Groups ({groups.length})

-
+ const GroupsList = () => { + return ( + <> +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" + > + + Back +
+

Groups ({groups.length})

+
- - - ); - }; + + + ); + }; - const AssignmentsPage = () => { - const activeFilter = (a: Assignment) => - moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length; - const pastFilter = (a: Assignment) => (moment(a.endDate).isBefore(moment()) || a.assignees.length === a.results.length) && !a.archived; - const archivedFilter = (a: Assignment) => a.archived; - const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment()); + const AssignmentsPage = () => { + const activeFilter = (a: Assignment) => + moment(a.endDate).isAfter(moment()) && + moment(a.startDate).isBefore(moment()) && + a.assignees.length > a.results.length; + const pastFilter = (a: Assignment) => + (moment(a.endDate).isBefore(moment()) || + a.assignees.length === a.results.length) && + !a.archived; + const archivedFilter = (a: Assignment) => a.archived; + const futureFilter = (a: Assignment) => + moment(a.startDate).isAfter(moment()); - return ( - <> - { - setSelectedAssignment(undefined); - setIsCreatingAssignment(false); - reloadAssignments(); - }} - assignment={selectedAssignment} - /> - x.admin === user.id || x.participants.includes(user.id))} - users={users.filter( - (x) => - x.type === "student" && - (!!selectedUser - ? groups - .filter((g) => g.admin === selectedUser.id) - .flatMap((g) => g.participants) - .includes(x.id) || false - : groups.flatMap((g) => g.participants).includes(x.id)), - )} - assigner={user.id} - isCreating={isCreatingAssignment} - cancelCreation={() => { - setIsCreatingAssignment(false); - setSelectedAssignment(undefined); - reloadAssignments(); - }} - /> -
-
setPage("")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> - - Back -
-
- Reload - -
-
-
-

Active Assignments ({assignments.filter(activeFilter).length})

-
- {assignments.filter(activeFilter).map((a) => ( - setSelectedAssignment(a)} key={a.id} /> - ))} -
-
-
-

Planned Assignments ({assignments.filter(futureFilter).length})

-
-
setIsCreatingAssignment(true)} - className="w-[250px] h-[200px] flex flex-col gap-2 items-center justify-center bg-white hover:bg-mti-purple-ultralight text-mti-purple-light hover:text-mti-purple-dark border border-mti-gray-platinum hover:drop-shadow p-4 cursor-pointer rounded-xl transition ease-in-out duration-300"> - - New Assignment -
- {assignments.filter(futureFilter).map((a) => ( - { - setSelectedAssignment(a); - setIsCreatingAssignment(true); - }} - key={a.id} - /> - ))} -
-
-
-

Past Assignments ({assignments.filter(pastFilter).length})

-
- {assignments.filter(pastFilter).map((a) => ( - setSelectedAssignment(a)} - key={a.id} - allowDownload - reload={reloadAssignments} - allowArchive - /> - ))} -
-
-
-

Archived Assignments ({assignments.filter(archivedFilter).length})

-
- {assignments.filter(archivedFilter).map((a) => ( - setSelectedAssignment(a)} - key={a.id} - allowDownload - reload={reloadAssignments} - allowUnarchive - /> - ))} -
-
- - ); - }; + return ( + <> + { + setSelectedAssignment(undefined); + setIsCreatingAssignment(false); + reloadAssignments(); + }} + assignment={selectedAssignment} + /> + x.admin === user.id || x.participants.includes(user.id) + )} + users={users.filter( + (x) => + x.type === "student" && + (!!selectedUser + ? groups + .filter((g) => g.admin === selectedUser.id) + .flatMap((g) => g.participants) + .includes(x.id) || false + : groups.flatMap((g) => g.participants).includes(x.id)) + )} + assigner={user.id} + isCreating={isCreatingAssignment} + cancelCreation={() => { + setIsCreatingAssignment(false); + setSelectedAssignment(undefined); + reloadAssignments(); + }} + /> +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" + > + + Back +
+
+ Reload + +
+
+
+

+ Active Assignments ({assignments.filter(activeFilter).length}) +

+
+ {assignments.filter(activeFilter).map((a) => ( + setSelectedAssignment(a)} + key={a.id} + /> + ))} +
+
+
+

+ Planned Assignments ({assignments.filter(futureFilter).length}) +

+
+
setIsCreatingAssignment(true)} + className="w-[250px] h-[200px] flex flex-col gap-2 items-center justify-center bg-white hover:bg-mti-purple-ultralight text-mti-purple-light hover:text-mti-purple-dark border border-mti-gray-platinum hover:drop-shadow p-4 cursor-pointer rounded-xl transition ease-in-out duration-300" + > + + New Assignment +
+ {assignments.filter(futureFilter).map((a) => ( + { + setSelectedAssignment(a); + setIsCreatingAssignment(true); + }} + key={a.id} + /> + ))} +
+
+
+

+ Past Assignments ({assignments.filter(pastFilter).length}) +

+
+ {assignments.filter(pastFilter).map((a) => ( + setSelectedAssignment(a)} + key={a.id} + allowDownload + reload={reloadAssignments} + allowArchive + /> + ))} +
+
+
+

+ Archived Assignments ({assignments.filter(archivedFilter).length}) +

+
+ {assignments.filter(archivedFilter).map((a) => ( + setSelectedAssignment(a)} + key={a.id} + allowDownload + reload={reloadAssignments} + allowUnarchive + /> + ))} +
+
+ + ); + }; - const averageLevelCalculator = (studentStats: Stat[]) => { - const formattedStats = studentStats - .map((s) => ({ - focus: users.find((u) => u.id === s.user)?.focus, - score: s.score, - module: s.module, - })) - .filter((f) => !!f.focus); - const bandScores = formattedStats.map((s) => ({ - module: s.module, - level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!), - })); + const MasterStatisticalPage = () => { + return ( + <> +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" + > + + Back +
+

Master Statistical

+
+ { + const user = users.find((u) => u.id === id) as CorporateUser; + if (user) return [...accm, user]; + return accm; + }, + [] + )} + /> + + ); + }; - const levels: {[key in Module]: number} = { - reading: 0, - listening: 0, - writing: 0, - speaking: 0, - level: 0, - }; - bandScores.forEach((b) => (levels[b.module] += b.level)); + const averageLevelCalculator = (studentStats: Stat[]) => { + const formattedStats = studentStats + .map((s) => ({ + focus: users.find((u) => u.id === s.user)?.focus, + score: s.score, + module: s.module, + })) + .filter((f) => !!f.focus); + const bandScores = formattedStats.map((s) => ({ + module: s.module, + level: calculateBandScore( + s.score.correct, + s.score.total, + s.module, + s.focus! + ), + })); - return calculateAverageLevel(levels); - }; + const levels: { [key in Module]: number } = { + reading: 0, + listening: 0, + writing: 0, + speaking: 0, + level: 0, + }; + bandScores.forEach((b) => (levels[b.module] += b.level)); - const DefaultDashboard = () => ( - <> -
- setPage("students")} - Icon={BsPersonFill} - label="Students" - value={users.filter(studentFilter).length} - color="purple" - /> - setPage("teachers")} - Icon={BsPencilSquare} - label="Teachers" - value={users.filter(teacherFilter).length} - color="purple" - /> - groups.flatMap((g) => g.participants).includes(s.user)).length} - color="purple" - /> - groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)} - color="purple" - /> - setPage("groups")} Icon={BsPeople} label="Groups" value={groups.length} color="purple" /> - - - setPage("corporate")} - /> - -
+ return calculateAverageLevel(levels); + }; -
-
- Latest students -
- {users - .filter(studentFilter) - .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) - .map((x) => ( - - ))} -
-
-
- Latest teachers -
- {users - .filter(teacherFilter) - .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) - .map((x) => ( - - ))} -
-
-
- Highest level students -
- {users - .filter(studentFilter) - .sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels)) - .map((x) => ( - - ))} -
-
-
- Highest exam count students -
- {users - .filter(studentFilter) - .sort( - (a, b) => - Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length, - ) - .map((x) => ( - - ))} -
-
-
- - ); + const DefaultDashboard = () => ( + <> +
+ setPage("students")} + Icon={BsPersonFill} + label="Students" + value={users.filter(studentFilter).length} + color="purple" + /> + setPage("teachers")} + Icon={BsPencilSquare} + label="Teachers" + value={users.filter(teacherFilter).length} + color="purple" + /> + + groups.flatMap((g) => g.participants).includes(s.user) + ).length + } + color="purple" + /> + + groups.flatMap((g) => g.participants).includes(s.user) + ) + ).toFixed(1)} + color="purple" + /> + setPage("groups")} + Icon={BsPeople} + label="Groups" + value={groups.length} + color="purple" + /> + + + setPage("corporate")} + /> + setPage("statistical")} + /> + +
- return ( - <> - setSelectedUser(undefined)}> - <> - {selectedUser && ( -
- { - setSelectedUser(undefined); - if (shouldReload) reload(); - }} - onViewStudents={ - selectedUser.type === "corporate" || selectedUser.type === "teacher" - ? () => { - appendUserFilters({ - id: "view-students", - filter: (x: User) => x.type === "student", - }); - appendUserFilters({ - id: "belongs-to-admin", - filter: (x: User) => - groups - .filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id)) - .flatMap((g) => g.participants) - .includes(x.id), - }); +
+
+ Latest students +
+ {users + .filter(studentFilter) + .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) + .map((x) => ( + + ))} +
+
+
+ Latest teachers +
+ {users + .filter(teacherFilter) + .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) + .map((x) => ( + + ))} +
+
+
+ Highest level students +
+ {users + .filter(studentFilter) + .sort( + (a, b) => + calculateAverageLevel(b.levels) - + calculateAverageLevel(a.levels) + ) + .map((x) => ( + + ))} +
+
+
+ Highest exam count students +
+ {users + .filter(studentFilter) + .sort( + (a, b) => + Object.keys(groupByExam(getStatsByStudent(b))).length - + Object.keys(groupByExam(getStatsByStudent(a))).length + ) + .map((x) => ( + + ))} +
+
+
+ + ); - router.push("/list/users"); - } - : undefined - } - onViewTeachers={ - selectedUser.type === "corporate" || selectedUser.type === "student" - ? () => { - appendUserFilters({ - id: "view-teachers", - filter: (x: User) => x.type === "teacher", - }); - appendUserFilters({ - id: "belongs-to-admin", - filter: (x: User) => - groups - .filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id)) - .flatMap((g) => g.participants) - .includes(x.id), - }); + return ( + <> + setSelectedUser(undefined)}> + <> + {selectedUser && ( +
+ { + setSelectedUser(undefined); + if (shouldReload) reload(); + }} + onViewStudents={ + selectedUser.type === "corporate" || + selectedUser.type === "teacher" + ? () => { + appendUserFilters({ + id: "view-students", + filter: (x: User) => x.type === "student", + }); + appendUserFilters({ + id: "belongs-to-admin", + filter: (x: User) => + groups + .filter( + (g) => + g.admin === selectedUser.id || + g.participants.includes(selectedUser.id) + ) + .flatMap((g) => g.participants) + .includes(x.id), + }); - router.push("/list/users"); - } - : undefined - } - user={selectedUser} - /> -
- )} - -
- {page === "students" && } - {page === "teachers" && } - {page === "groups" && } - {page === "corporate" && } - {page === "assignments" && } - {page === "" && } - - ); + router.push("/list/users"); + } + : undefined + } + onViewTeachers={ + selectedUser.type === "corporate" || + selectedUser.type === "student" + ? () => { + appendUserFilters({ + id: "view-teachers", + filter: (x: User) => x.type === "teacher", + }); + appendUserFilters({ + id: "belongs-to-admin", + filter: (x: User) => + groups + .filter( + (g) => + g.admin === selectedUser.id || + g.participants.includes(selectedUser.id) + ) + .flatMap((g) => g.participants) + .includes(x.id), + }); + + router.push("/list/users"); + } + : undefined + } + user={selectedUser} + /> +
+ )} + +
+ {page === "students" && } + {page === "teachers" && } + {page === "groups" && } + {page === "corporate" && } + {page === "assignments" && } + {page === "statistical" && } + {page === "" && } + + ); } diff --git a/src/dashboards/MasterStatistical.tsx b/src/dashboards/MasterStatistical.tsx new file mode 100644 index 00000000..f1cdf091 --- /dev/null +++ b/src/dashboards/MasterStatistical.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import { CorporateUser } from "@/interfaces/user"; +import { BsBank } from "react-icons/bs"; +import IconCard from "./IconCard"; +import useAssignmentsCorporates from '@/hooks/useAssignmentCorporates'; +interface Props { + users: CorporateUser[]; +} +const MasterStatistical = (props: Props) => { + const { users } = props; + + const usersList = React.useMemo(() => users.map((x) => x.id), [users]); + + const { assignments } = useAssignmentsCorporates({ corporates: usersList }); + + console.log('Assignments', assignments); + return ( +
+ console.log("clicked")} + /> + {users.map((group) => ( + console.log("clicked", group)} + /> + ))} +
+ ); +}; + +export default MasterStatistical; diff --git a/src/hooks/useAssignmentCorporates.tsx b/src/hooks/useAssignmentCorporates.tsx new file mode 100644 index 00000000..825a3509 --- /dev/null +++ b/src/hooks/useAssignmentCorporates.tsx @@ -0,0 +1,34 @@ +import { Assignment } from "@/interfaces/results"; +import axios from "axios"; +import { useEffect, useState } from "react"; + +export default function useAssignmentsCorporates({ + corporates, +}: { + corporates: string[]; +}) { + const [assignments, setAssignments] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + + const getData = () => { + if (corporates.length === 0) { + setAssignments([]); + return; + } + + setIsLoading(true); + axios + .get( + `/api/assignments/corporate?ids=${corporates.join(",")}` + ) + .then(async (response) => { + setAssignments(response.data); + }) + .finally(() => setIsLoading(false)); + }; + + useEffect(getData, [corporates]); + + return { assignments, isLoading, isError, reload: getData }; +} diff --git a/src/hooks/useAssignments.tsx b/src/hooks/useAssignments.tsx index 2d2f58ef..5a1c30d3 100644 --- a/src/hooks/useAssignments.tsx +++ b/src/hooks/useAssignments.tsx @@ -10,7 +10,7 @@ export default function useAssignments({assigner, assignees, corporate}: {assign const getData = () => { setIsLoading(true); axios - .get(!corporate ? "/api/assignments" : `/api/assignments/corporate?id=${corporate}`) + .get(!corporate ? "/api/assignments" : `/api/assignments/corporate/${corporate}`) .then(async (response) => { if (assigner) { setAssignments(response.data.filter((a) => a.assigner === assigner)); diff --git a/src/pages/api/assignments/corporate.ts b/src/pages/api/assignments/corporate/[id].ts similarity index 100% rename from src/pages/api/assignments/corporate.ts rename to src/pages/api/assignments/corporate/[id].ts diff --git a/src/pages/api/assignments/corporate/index.ts b/src/pages/api/assignments/corporate/index.ts new file mode 100644 index 00000000..1c36fd16 --- /dev/null +++ b/src/pages/api/assignments/corporate/index.ts @@ -0,0 +1,54 @@ +// 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, + query, + where, + setDoc, + doc, + getDoc, +} from "firebase/firestore"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { uuidv4 } from "@firebase/util"; +import { Module } from "@/interfaces"; +import { getExams } from "@/utils/exams.be"; +import { Exam, InstructorGender, Variant } from "@/interfaces/exam"; +import { capitalize, flatten, uniqBy } from "lodash"; +import { User } from "@/interfaces/user"; +import moment from "moment"; +import { sendEmail } from "@/email"; +import { getAllAssignersByCorporate } from "@/utils/groups.be"; +import { getAssignmentsByAssigners } from "@/utils/assignments.be"; + +const db = getFirestore(app); + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ ok: false }); + return; + } + + if (req.method === "GET") return await GET(req, res); + + res.status(404).json({ ok: false }); +} + +async function GET(req: NextApiRequest, res: NextApiResponse) { + const { ids } = req.query as { ids: string }; + try { + const idsList = ids.split(","); + + const assigners = await Promise.all(idsList.map(getAllAssignersByCorporate)); + const assignmentList = [...assigners.flat(), ...idsList]; + const assignments = await getAssignmentsByAssigners(assignmentList); + res.status(200).json(assignments); + } catch (err) { + res.status(500).json({ error: err.message }); + } +} From b6015b64333978e668837e750a335a0644591b1c Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Thu, 15 Aug 2024 10:35:08 +0100 Subject: [PATCH 2/9] Added any to error to prevent an error --- src/pages/api/assignments/corporate/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/api/assignments/corporate/index.ts b/src/pages/api/assignments/corporate/index.ts index 1c36fd16..8216abee 100644 --- a/src/pages/api/assignments/corporate/index.ts +++ b/src/pages/api/assignments/corporate/index.ts @@ -48,7 +48,7 @@ async function GET(req: NextApiRequest, res: NextApiResponse) { const assignmentList = [...assigners.flat(), ...idsList]; const assignments = await getAssignmentsByAssigners(assignmentList); res.status(200).json(assignments); - } catch (err) { + } catch (err: any) { res.status(500).json({ error: err.message }); } } From cf2fd06d39172f7c0032a86ff01470251984cf4f Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Thu, 15 Aug 2024 10:38:56 +0100 Subject: [PATCH 3/9] Added Icon ofr consolidate highest student --- src/dashboards/MasterStatistical.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/dashboards/MasterStatistical.tsx b/src/dashboards/MasterStatistical.tsx index f1cdf091..018039de 100644 --- a/src/dashboards/MasterStatistical.tsx +++ b/src/dashboards/MasterStatistical.tsx @@ -1,6 +1,6 @@ import React from "react"; import { CorporateUser } from "@/interfaces/user"; -import { BsBank } from "react-icons/bs"; +import { BsBank, BsPersonFill } from "react-icons/bs"; import IconCard from "./IconCard"; import useAssignmentsCorporates from '@/hooks/useAssignmentCorporates'; interface Props { @@ -33,6 +33,12 @@ const MasterStatistical = (props: Props) => { onClick={() => console.log("clicked", group)} /> ))} + console.log("clicked")} + Icon={BsPersonFill} + label="Consolidate Highest Student" + color="purple" + /> ); }; From e84cc8ddd8105c455c3c59b6155cf764512ec6ac Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Thu, 15 Aug 2024 13:39:42 +0100 Subject: [PATCH 4/9] Cleaned up some code --- src/pages/api/assignments/corporate/index.ts | 20 -------------------- src/utils/assignments.be.ts | 5 +++++ 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/src/pages/api/assignments/corporate/index.ts b/src/pages/api/assignments/corporate/index.ts index 8216abee..d70197ae 100644 --- a/src/pages/api/assignments/corporate/index.ts +++ b/src/pages/api/assignments/corporate/index.ts @@ -1,30 +1,10 @@ // 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, - query, - where, - setDoc, - doc, - getDoc, -} from "firebase/firestore"; import { withIronSessionApiRoute } from "iron-session/next"; import { sessionOptions } from "@/lib/session"; -import { uuidv4 } from "@firebase/util"; -import { Module } from "@/interfaces"; -import { getExams } from "@/utils/exams.be"; -import { Exam, InstructorGender, Variant } from "@/interfaces/exam"; -import { capitalize, flatten, uniqBy } from "lodash"; -import { User } from "@/interfaces/user"; -import moment from "moment"; -import { sendEmail } from "@/email"; import { getAllAssignersByCorporate } from "@/utils/groups.be"; import { getAssignmentsByAssigners } from "@/utils/assignments.be"; -const db = getFirestore(app); export default withIronSessionApiRoute(handler, sessionOptions); diff --git a/src/utils/assignments.be.ts b/src/utils/assignments.be.ts index 96725e67..d7b42bc9 100644 --- a/src/utils/assignments.be.ts +++ b/src/utils/assignments.be.ts @@ -9,6 +9,11 @@ export const getAssignmentsByAssigner = async (id: string) => { return docs.map((x) => ({...x.data(), id: x.id})) as Assignment[]; }; +export const getAssignmentsByAssignerBetweenDates = async (id: string, startDate: Date, endDate: Date) => { + const {docs} = await getDocs(query(collection(db, "assignments"), where("assigner", "==", id), )); + return docs.map((x) => ({...x.data(), id: x.id})) as Assignment[]; +}; + export const getAssignmentsByAssigners = async (ids: string[]) => { return (await Promise.all(ids.map(getAssignmentsByAssigner))).flat(); }; From 1950d5f15d261a22d1200f9dc1393eb1700efac6 Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Thu, 15 Aug 2024 14:56:14 +0100 Subject: [PATCH 5/9] Added initial Excel changes --- package.json | 1 + src/dashboards/AssignmentCard.tsx | 4 + src/dashboards/Corporate.tsx | 2 + src/hooks/usePDFDownload.tsx | 12 +- .../api/assignments/[id]/[export]/excel.ts | 154 +++++++ .../[id]/{export.tsx => [export]/pdf.tsx} | 0 .../[id]/{export.tsx => [export]/pdf.tsx} | 0 yarn.lock | 434 +++++++++++++++++- 8 files changed, 590 insertions(+), 17 deletions(-) create mode 100644 src/pages/api/assignments/[id]/[export]/excel.ts rename src/pages/api/assignments/[id]/{export.tsx => [export]/pdf.tsx} (100%) rename src/pages/api/stats/[id]/{export.tsx => [export]/pdf.tsx} (100%) diff --git a/package.json b/package.json index edebc0e7..d7536158 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "daisyui": "^3.1.5", "eslint": "8.33.0", "eslint-config-next": "13.1.6", + "exceljs": "^4.4.0", "express-handlebars": "^7.1.2", "firebase": "9.19.1", "firebase-admin": "^11.10.1", diff --git a/src/dashboards/AssignmentCard.tsx b/src/dashboards/AssignmentCard.tsx index 1886cedd..6d1c608f 100644 --- a/src/dashboards/AssignmentCard.tsx +++ b/src/dashboards/AssignmentCard.tsx @@ -18,6 +18,7 @@ interface Props { reload?: Function; allowArchive?: boolean; allowUnarchive?: boolean; + allowExcelDownload?: boolean; } export default function AssignmentCard({ @@ -35,10 +36,12 @@ export default function AssignmentCard({ reload, allowArchive, allowUnarchive, + allowExcelDownload, }: Assignment & Props) { const {users} = useUsers(); const renderPdfIcon = usePDFDownload("assignments"); + const renderExcelIcon = usePDFDownload("assignments", "excel"); const renderArchiveIcon = useAssignmentArchive(id, reload); const renderUnarchiveIcon = useAssignmentUnarchive(id, reload); @@ -63,6 +66,7 @@ export default function AssignmentCard({

{name}

{allowDownload && renderPdfIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")} + {allowExcelDownload && renderExcelIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")} {allowArchive && !archived && renderArchiveIcon("text-mti-gray-dim", "text-mti-gray-dim")} {allowUnarchive && archived && renderUnarchiveIcon("text-mti-gray-dim", "text-mti-gray-dim")}
diff --git a/src/dashboards/Corporate.tsx b/src/dashboards/Corporate.tsx index be87e851..d9e8e52e 100644 --- a/src/dashboards/Corporate.tsx +++ b/src/dashboards/Corporate.tsx @@ -264,6 +264,7 @@ export default function CorporateDashboard({user}: Props) { allowDownload reload={reloadAssignments} allowArchive + allowExcelDownload /> ))} @@ -279,6 +280,7 @@ export default function CorporateDashboard({user}: Props) { allowDownload reload={reloadAssignments} allowUnarchive + allowExcelDownload /> ))} diff --git a/src/hooks/usePDFDownload.tsx b/src/hooks/usePDFDownload.tsx index 54b83884..b7a81a5c 100644 --- a/src/hooks/usePDFDownload.tsx +++ b/src/hooks/usePDFDownload.tsx @@ -1,15 +1,16 @@ import React from "react"; import axios from "axios"; import { toast } from "react-toastify"; -import { BsFilePdf } from "react-icons/bs"; +import { BsFilePdf, BsFileExcel} from "react-icons/bs"; type DownloadingPdf = { [key: string]: boolean; }; type PdfEndpoint = "stats" | "assignments"; +type FileType = "pdf" | "excel"; -export const usePDFDownload = (endpoint: PdfEndpoint) => { +export const usePDFDownload = (endpoint: PdfEndpoint, file: FileType = 'pdf') => { const [downloadingPdf, setDownloadingPdf] = React.useState( {} ); @@ -17,7 +18,7 @@ export const usePDFDownload = (endpoint: PdfEndpoint) => { const triggerDownload = async (id: string) => { try { setDownloadingPdf((prev) => ({ ...prev, [id]: true })); - const res = await axios.post(`/api/${endpoint}/${id}/export`); + const res = await axios.post(`/api/${endpoint}/${id}/export/${file}`); toast.success("Report ready!"); const link = document.createElement("a"); link.href = res.data; @@ -45,8 +46,11 @@ export const usePDFDownload = (endpoint: PdfEndpoint) => { ); } + + const Icon = file === "excel" ? BsFileExcel : BsFilePdf; + return ( - { e.stopPropagation(); diff --git a/src/pages/api/assignments/[id]/[export]/excel.ts b/src/pages/api/assignments/[id]/[export]/excel.ts new file mode 100644 index 00000000..2d4cbf23 --- /dev/null +++ b/src/pages/api/assignments/[id]/[export]/excel.ts @@ -0,0 +1,154 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { app, storage } from "@/firebase"; +import { + getFirestore, + doc, + getDoc, + updateDoc, + getDocs, + query, + collection, + where, + documentId, +} 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 { 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"; + +interface GroupScoreSummaryHelper { + score: [number, number]; + label: string; + sessions: string[]; +} +const db = getFirestore(app); + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + // if (req.method === "GET") return get(req, res); + if (req.method === "POST") return await post(req, res); +} + +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; + }; + }; + if (!data) { + res.status(400).end(); + return; + } + + if ( + data.excel && + data.excel.path && + data.excel.version === process.env.EXCEL_VERSION + ) { + // if it does, return the excel url + const fileRef = ref(storage, data.excel.path); + const url = await getDownloadURL(fileRef); + res.status(200).end(url); + return; + } + + const docsSnap = await getDocs( + query(collection(db, "users"), where(documentId(), "in", data.assignees)) + ); + const users = docsSnap.docs.map((d) => ({ + ...d.data(), + id: d.id, + })) as User[]; + + const docUser = await getDoc(doc(db, "users", req.session.user.id)); + 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 results = data.results.map((r: any) => r.score.correct); + const highestScore = Math.max(...results); + const lowestScore = Math.min(...results); + + const dates = data.results + .map((r: any) => r.date) + .map((d: number) => moment(d)); + const firstDate = moment.min(dates); + const lastDate = moment.max(dates); + + const firstSectionData = [ + { + label: "Corporate Name :", + value: user.corporateInformation.companyInformation.name, + }, + { + label: "Report Download date :", + value: moment().format("DD/MM/YYYY"), + }, + { label: "Test Information :", value: "TODO" }, + { 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: "", 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) + }); + + // Set the response headers for downloading an Excel file + res.setHeader( + "Content-Type", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ); + res.setHeader( + "Content-Disposition", + "attachment; filename=ReportData.xlsx" + ); + + // Write the workbook to the response + await workbook.xlsx.write(res); + res.end(); + + return; + } + } + + res.status(401).json({ message: "Unauthorized" }); +} diff --git a/src/pages/api/assignments/[id]/export.tsx b/src/pages/api/assignments/[id]/[export]/pdf.tsx similarity index 100% rename from src/pages/api/assignments/[id]/export.tsx rename to src/pages/api/assignments/[id]/[export]/pdf.tsx diff --git a/src/pages/api/stats/[id]/export.tsx b/src/pages/api/stats/[id]/[export]/pdf.tsx similarity index 100% rename from src/pages/api/stats/[id]/export.tsx rename to src/pages/api/stats/[id]/[export]/pdf.tsx diff --git a/yarn.lock b/yarn.lock index 98c50bc2..c5d0eba2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -322,6 +322,31 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@fast-csv/format@4.3.5": + version "4.3.5" + resolved "https://registry.yarnpkg.com/@fast-csv/format/-/format-4.3.5.tgz#90d83d1b47b6aaf67be70d6118f84f3e12ee1ff3" + integrity sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A== + dependencies: + "@types/node" "^14.0.1" + lodash.escaperegexp "^4.1.2" + lodash.isboolean "^3.0.3" + lodash.isequal "^4.5.0" + lodash.isfunction "^3.0.9" + lodash.isnil "^4.0.0" + +"@fast-csv/parse@4.3.6": + version "4.3.6" + resolved "https://registry.yarnpkg.com/@fast-csv/parse/-/parse-4.3.6.tgz#ee47d0640ca0291034c7aa94039a744cfb019264" + integrity sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA== + dependencies: + "@types/node" "^14.0.1" + lodash.escaperegexp "^4.1.2" + lodash.groupby "^4.6.0" + lodash.isfunction "^3.0.9" + lodash.isnil "^4.0.0" + lodash.isundefined "^3.0.1" + lodash.uniq "^4.5.0" + "@fastify/busboy@^1.2.1": version "1.2.1" resolved "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.2.1.tgz" @@ -1558,6 +1583,11 @@ resolved "https://registry.npmjs.org/@types/node/-/node-18.13.0.tgz" integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg== +"@types/node@^14.0.1": + version "14.18.63" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b" + integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ== + "@types/node@^17.0.41": version "17.0.45" resolved "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz" @@ -1842,6 +1872,51 @@ anymatch@~3.1.2: resolved "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz" integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== +archiver-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2" + integrity sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw== + dependencies: + glob "^7.1.4" + graceful-fs "^4.2.0" + lazystream "^1.0.0" + lodash.defaults "^4.2.0" + lodash.difference "^4.5.0" + lodash.flatten "^4.4.0" + lodash.isplainobject "^4.0.6" + lodash.union "^4.6.0" + normalize-path "^3.0.0" + readable-stream "^2.0.0" + +archiver-utils@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-3.0.4.tgz#a0d201f1cf8fce7af3b5a05aea0a337329e96ec7" + integrity sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw== + dependencies: + glob "^7.2.3" + graceful-fs "^4.2.0" + lazystream "^1.0.0" + lodash.defaults "^4.2.0" + lodash.difference "^4.5.0" + lodash.flatten "^4.4.0" + lodash.isplainobject "^4.0.6" + lodash.union "^4.6.0" + normalize-path "^3.0.0" + readable-stream "^3.6.0" + +archiver@^5.0.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.2.tgz#99991d5957e53bd0303a392979276ac4ddccf3b0" + integrity sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw== + dependencies: + archiver-utils "^2.1.0" + async "^3.2.4" + buffer-crc32 "^0.2.1" + readable-stream "^3.6.0" + readdir-glob "^1.1.2" + tar-stream "^2.2.0" + zip-stream "^4.1.0" + are-we-there-yet@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz" @@ -1945,6 +2020,11 @@ async-retry@^1.3.3: dependencies: retry "0.13.1" +async@^3.2.4: + version "3.2.5" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" + integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" @@ -2052,6 +2132,11 @@ bidi-js@^1.0.2: dependencies: require-from-string "^2.0.2" +big-integer@^1.6.17: + version "1.6.52" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" + integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg== + bignumber.js@^9.0.0: version "9.1.2" resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz" @@ -2062,11 +2147,33 @@ binary-extensions@^2.0.0: resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +binary@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" + integrity sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg== + dependencies: + buffers "~0.1.1" + chainsaw "~0.1.0" + +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + bluebird@^3.7.2, bluebird@~3.7.2: version "3.7.2" resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== +bluebird@~3.4.1: + version "3.4.7" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" + integrity sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" @@ -2113,11 +2220,29 @@ browserslist@^4.21.5: node-releases "^2.0.8" update-browserslist-db "^1.0.10" +buffer-crc32@^0.2.1, buffer-crc32@^0.2.13: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz" integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== +buffer-indexof-polyfill@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz#d2732135c5999c64b277fcf9b1abe3498254729c" + integrity sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A== + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + buffer@^6: version "6.0.3" resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" @@ -2126,6 +2251,11 @@ buffer@^6: base64-js "^1.3.1" ieee754 "^1.2.1" +buffers@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" + integrity sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ== + call-bind@^1.0.2, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz" @@ -2164,6 +2294,13 @@ catharsis@^0.9.0: dependencies: lodash "^4.17.15" +chainsaw@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" + integrity sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ== + dependencies: + traverse ">=0.3.0 <0.4" + chalk@^2.4.2: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" @@ -2300,6 +2437,16 @@ commander@^4.0.0: resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +compress-commons@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.2.tgz#6542e59cb63e1f46a8b21b0e06f9a32e4c8b06df" + integrity sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg== + dependencies: + buffer-crc32 "^0.2.13" + crc32-stream "^4.0.2" + normalize-path "^3.0.0" + readable-stream "^3.6.0" + compressible@^2.0.12: version "2.0.18" resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz" @@ -2369,6 +2516,19 @@ country-flag-icons@^1.5.11: resolved "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.5.13.tgz" integrity sha512-4JwHNqaKZ19doQoNcBjsoYA+I7NqCH/mC/6f5cBWvdKzcK5TMmzLpq3Z/syVHMHJuDGFwJ+rPpGizvrqJybJow== +crc-32@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" + integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== + +crc32-stream@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-4.0.3.tgz#85dd677eb78fa7cad1ba17cc506a597d41fc6f33" + integrity sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw== + dependencies: + crc-32 "^1.2.0" + readable-stream "^3.4.0" + create-emotion@^10.0.14, create-emotion@^10.0.27: version "10.0.27" resolved "https://registry.npmjs.org/create-emotion/-/create-emotion-10.0.27.tgz" @@ -2462,6 +2622,11 @@ date-fns@^2.0.1, date-fns@^2.30.0: dependencies: "@babel/runtime" "^7.21.0" +dayjs@^1.8.34: + version "1.11.12" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.12.tgz#5245226cc7f40a15bf52e0b99fd2a04669ccac1d" + integrity sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg== + debug@4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" @@ -3078,6 +3243,21 @@ events@^3.3.0: resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +exceljs@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/exceljs/-/exceljs-4.4.0.tgz#cfb1cb8dcc82c760a9fc9faa9e52dadab66b0156" + integrity sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg== + dependencies: + archiver "^5.0.0" + dayjs "^1.8.34" + fast-csv "^4.3.1" + jszip "^3.10.1" + readable-stream "^3.6.0" + saxes "^5.0.1" + tmp "^0.2.0" + unzipper "^0.10.11" + uuid "^8.3.0" + express-handlebars@^7.1.2: version "7.1.3" resolved "https://registry.npmjs.org/express-handlebars/-/express-handlebars-7.1.3.tgz" @@ -3092,6 +3272,14 @@ extend@^3.0.2: resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +fast-csv@^4.3.1: + version "4.3.6" + resolved "https://registry.yarnpkg.com/fast-csv/-/fast-csv-4.3.6.tgz#70349bdd8fe4d66b1130d8c91820b64a21bc4a63" + integrity sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw== + dependencies: + "@fast-csv/format" "4.3.5" + "@fast-csv/parse" "4.3.6" + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" @@ -3337,6 +3525,11 @@ framer-motion@^9.0.2: optionalDependencies: "@emotion/is-prop-valid" "^0.8.2" +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + fs-extra@^11.2.0: version "11.2.0" resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz" @@ -3363,6 +3556,16 @@ fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== +fstream@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" + integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + function-bind@^1.1.1, function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" @@ -3500,6 +3703,18 @@ glob@^10.4.2: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" +glob@^7.1.4, glob@^7.2.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^8.0.0: version "8.1.0" resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz" @@ -3613,7 +3828,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.2, graceful-fs@^4.2.4: +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.2, graceful-fs@^4.2.4: version "4.2.11" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -3773,7 +3988,7 @@ idb@7.0.1: resolved "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz" integrity sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg== -ieee754@^1.2.1: +ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -3783,6 +3998,11 @@ ignore@^5.2.0: resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" @@ -3804,7 +4024,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@~2.0.3: +inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -4202,6 +4422,16 @@ jsonwebtoken@^9.0.0: array-includes "^3.1.5" object.assign "^4.1.3" +jszip@^3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" + integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + setimmediate "^1.0.5" + jwa@^1.4.1: version "1.4.1" resolved "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz" @@ -4267,6 +4497,13 @@ language-tags@=1.0.5: dependencies: language-subtag-registry "~0.3.2" +lazystream@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" + integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw== + dependencies: + readable-stream "^2.0.5" + levn@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" @@ -4288,6 +4525,13 @@ libphonenumber-js@^1.11.2: resolved "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.4.tgz" integrity sha512-F/R50HQuWWYcmU/esP5jrH5LiWYaN7DpN0a/99U8+mnGGtnx8kmRE+649dQh3v+CowXXZc8vpkf5AmYkO0AQ7Q== +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + lilconfig@^2.0.5, lilconfig@^2.0.6: version "2.1.0" resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz" @@ -4310,6 +4554,11 @@ linkify-it@^5.0.0: dependencies: uc.micro "^2.0.0" +listenercount@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" + integrity sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ== + load-script@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz" @@ -4339,6 +4588,31 @@ lodash.clonedeep@^4.5.0: resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz" integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + +lodash.difference@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" + integrity sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA== + +lodash.escaperegexp@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" + integrity sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw== + +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g== + +lodash.groupby@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.groupby/-/lodash.groupby-4.6.0.tgz#0b08a1dcf68397c397855c3239783832df7403d1" + integrity sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw== + lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz" @@ -4349,11 +4623,26 @@ lodash.isboolean@^3.0.3: resolved "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz" integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + +lodash.isfunction@^3.0.9: + version "3.0.9" + resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051" + integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw== + lodash.isinteger@^4.0.4: version "4.0.4" resolved "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz" integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== +lodash.isnil@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/lodash.isnil/-/lodash.isnil-4.0.0.tgz#49e28cd559013458c814c5479d3c663a21bfaa6c" + integrity sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng== + lodash.isnumber@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz" @@ -4369,6 +4658,11 @@ lodash.isstring@^4.0.1: resolved "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz" integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== +lodash.isundefined@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz#23ef3d9535565203a66cefd5b830f848911afb48" + integrity sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" @@ -4379,6 +4673,16 @@ lodash.once@^4.0.0: resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz" integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== +lodash.union@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" + integrity sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw== + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== + lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" @@ -4500,14 +4804,14 @@ mime@^3.0.0: resolved "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz" integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.2: +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1: +minimatch@^5.0.1, minimatch@^5.1.0: version "5.1.6" resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz" integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== @@ -4551,6 +4855,13 @@ minizlib@^2.1.1: minipass "^3.0.0" yallist "^4.0.0" +"mkdirp@>=0.5 0": + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" @@ -4861,7 +5172,7 @@ pako@^0.2.5: resolved "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz" integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA== -pako@~1.0.5: +pako@~1.0.2, pako@~1.0.5: version "1.0.11" resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== @@ -5433,7 +5744,7 @@ read-excel-file@^5.7.1: fflate "^0.7.3" unzipper "^0.12.2" -readable-stream@^2.0.2: +readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -5446,7 +5757,7 @@ readable-stream@^2.0.2: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.1.1, readable-stream@^3.6.0: +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.2" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -5455,6 +5766,13 @@ readable-stream@^3.1.1, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readdir-glob@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.3.tgz#c3d831f51f5e7bfa62fa2ffbe4b508c640f09584" + integrity sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA== + dependencies: + minimatch "^5.1.0" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" @@ -5549,6 +5867,13 @@ reusify@^1.0.4: resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rimraf@2: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + rimraf@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" @@ -5582,6 +5907,13 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" +saxes@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" + integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== + dependencies: + xmlchars "^2.2.0" + scheduler@^0.17.0: version "0.17.0" resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.17.0.tgz" @@ -5646,6 +5978,11 @@ set-function-length@^1.2.1: gopd "^1.0.1" has-property-descriptors "^1.0.2" +setimmediate@^1.0.5, setimmediate@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz" @@ -5746,7 +6083,16 @@ stream-shift@^1.0.2: resolved "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz" integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -5810,7 +6156,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -5956,6 +6309,17 @@ tapable@^2.2.0: resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== +tar-stream@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + tar@^6.1.11: version "6.2.1" resolved "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz" @@ -6016,7 +6380,7 @@ tiny-inflate@^1.0.0, tiny-inflate@^1.0.3: resolved "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz" integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw== -tmp@^0.2.1: +tmp@^0.2.0, tmp@^0.2.1: version "0.2.3" resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz" integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== @@ -6038,6 +6402,11 @@ tr46@~0.0.3: resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +"traverse@>=0.3.0 <0.4": + version "0.3.9" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" + integrity sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ== + ts-interface-checker@^0.1.9: version "0.1.13" resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz" @@ -6149,6 +6518,22 @@ universalify@^2.0.0: resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== +unzipper@^0.10.11: + version "0.10.14" + resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.10.14.tgz#d2b33c977714da0fbc0f82774ad35470a7c962b1" + integrity sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g== + dependencies: + big-integer "^1.6.17" + binary "~0.3.0" + bluebird "~3.4.1" + buffer-indexof-polyfill "~1.0.0" + duplexer2 "~0.1.4" + fstream "^1.0.12" + graceful-fs "^4.2.2" + listenercount "~1.0.1" + readable-stream "~2.3.6" + setimmediate "~1.0.4" + unzipper@^0.12.2: version "0.12.2" resolved "https://registry.npmjs.org/unzipper/-/unzipper-0.12.2.tgz" @@ -6197,7 +6582,7 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -uuid@^8.0.0: +uuid@^8.0.0, uuid@^8.3.0: version "8.3.2" resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== @@ -6335,7 +6720,7 @@ wordwrap@^1.0.0: resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -6353,6 +6738,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" @@ -6367,6 +6761,11 @@ wrappy@1: resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + xmlcreate@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz" @@ -6445,6 +6844,15 @@ yoga-layout@^2.0.1: resolved "https://registry.npmjs.org/yoga-layout/-/yoga-layout-2.0.1.tgz" integrity sha512-tT/oChyDXelLo2A+UVnlW9GU7CsvFMaEnd9kVFsaiCQonFAXd3xrHhkLYu+suwwosrAEQ746xBU+HvYtm1Zs2Q== +zip-stream@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.1.1.tgz#1337fe974dbaffd2fa9a1ba09662a66932bd7135" + integrity sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ== + dependencies: + archiver-utils "^3.0.4" + compress-commons "^4.1.2" + readable-stream "^3.6.0" + zustand@^4.3.6: version "4.3.7" resolved "https://registry.npmjs.org/zustand/-/zustand-4.3.7.tgz" From bf1bdd935c875471b49e2702f034b8ac0affc8d0 Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Sun, 18 Aug 2024 21:25:18 +0100 Subject: [PATCH 6/9] Improvements on excel rendering --- .../api/assignments/[id]/[export]/excel.ts | 176 +++++++++++++++--- 1 file changed, 150 insertions(+), 26 deletions(-) diff --git a/src/pages/api/assignments/[id]/[export]/excel.ts b/src/pages/api/assignments/[id]/[export]/excel.ts index 2d4cbf23..25a96d4a 100644 --- a/src/pages/api/assignments/[id]/[export]/excel.ts +++ b/src/pages/api/assignments/[id]/[export]/excel.ts @@ -46,6 +46,15 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method === "POST") return await post(req, res); } +function logWorksheetData(worksheet: any) { + worksheet.eachRow((row: any, rowNumber: number) => { + console.log(`Row ${rowNumber}:`); + row.eachCell((cell: any, colNumber: number) => { + console.log(` Cell ${colNumber}: ${cell.value}`); + }); + }); +} + async function post(req: NextApiRequest, res: NextApiResponse) { // verify if it's a logged user that is trying to export if (req.session.user) { @@ -68,17 +77,17 @@ async function post(req: NextApiRequest, res: NextApiResponse) { return; } - if ( - data.excel && - data.excel.path && - data.excel.version === process.env.EXCEL_VERSION - ) { - // if it does, return the excel url - const fileRef = ref(storage, data.excel.path); - const url = await getDownloadURL(fileRef); - res.status(200).end(url); - return; - } + // if ( + // data.excel && + // data.excel.path && + // data.excel.version === process.env.EXCEL_VERSION + // ) { + // // if it does, return the excel url + // const fileRef = ref(storage, data.excel.path); + // const url = await getDownloadURL(fileRef); + // res.status(200).end(url); + // return; + // } const docsSnap = await getDocs( query(collection(db, "users"), where(documentId(), "in", data.assignees)) @@ -106,20 +115,29 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const firstSectionData = [ { label: "Corporate Name :", - value: user.corporateInformation.companyInformation.name, + value: user.corporateInformation?.companyInformation?.name || "", }, { label: "Report Download date :", value: moment().format("DD/MM/YYYY"), }, { label: "Test Information :", value: "TODO" }, - { label: "Date of Test :", value: moment(data.startDate).format("DD/MM/YYYY") }, + { + 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: "", 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") }, + { + 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 @@ -132,19 +150,125 @@ async function post(req: NextApiRequest, res: NextApiResponse) { worksheet.getCell(`B${index + 1}`).value = value; // Second column (values) }); - // Set the response headers for downloading an Excel file - res.setHeader( - "Content-Type", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - ); - res.setHeader( - "Content-Disposition", - "attachment; filename=ReportData.xlsx" + logWorksheetData(worksheet); + + const testSectionsArray = [1, 2, 3, 4, 5]; + + // 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", "Level"]; + + // Calculate the dynamic columns based on the testSectionsArray + const numberOfTestSections = testSectionsArray.length; + const testSectionHeaders = testSectionsArray.map( + (section, index) => `Part ${index + 1}` ); - // Write the workbook to the response - await workbook.xlsx.write(res); - res.end(); + // Add the main header row, merging static columns and "Test Sections" + worksheet.addRow([ + ...staticHeaders, + ...testSectionsArray.map((a) => "Test Sections"), + ...additionalHeaders, + ]); + + // 1 headers rows + const startIndexTable = firstSectionData.length + 1; + + logWorksheetData(worksheet); + + // // Merge "Test Sections" over dynamic number of columns + // const tableColumns = staticHeaders.length + numberOfTestSections; + // worksheet.mergeCells(1, staticHeaders.length + 1, 1, tableColumns); + + // logWorksheetData(worksheet); + // worksheet.mergeCells(`G1:G3`); // Time Spent + // worksheet.mergeCells(`H1:H3`); // Score + // worksheet.mergeCells(`I1:I3`); // Level + + // 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(""), + ...testSectionsArray.map(() => "Grammar"), + "", + "", + "", + ]); + + // Merging static headers and "Test Sections" over dynamic columns + worksheet.mergeCells(`A${startIndexTable}:A${startIndexTable + 2}`); // "Sr N" + worksheet.mergeCells(`B${startIndexTable}:B${startIndexTable + 2}`); // "Candidate ID" + worksheet.mergeCells(`C${startIndexTable}:C${startIndexTable + 2}`); // "First and Last Name" + worksheet.mergeCells(`D${startIndexTable}:D${startIndexTable + 2}`); // "Passport/ID" + worksheet.mergeCells(`E${startIndexTable}:E${startIndexTable + 2}`); // "Email ID" + worksheet.mergeCells(`F${startIndexTable}:F${startIndexTable + 2}`); // "Gender" + + // worksheet.addRow(users.map((a) => { + // })) + + for ( + let i = 0; + i < staticHeaders.length + additionalHeaders.length + 1; + 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); + + // upload the pdf to storage + const snapshot = await uploadBytes(fileRef, buffer, { + contentType: + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }); + + // update the stats entries with the pdf url to prevent duplication + await updateDoc(docSnap.ref, { + excel: { + path: refName, + version: process.env.EXCEL_VERSION, + }, + }); + const url = await getDownloadURL(fileRef); + res.status(200).end(url); return; } From 2d69fdac3cd0d3fb7144e38713cd696a7c04b5e6 Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Mon, 19 Aug 2024 23:39:38 +0100 Subject: [PATCH 7/9] Added missing updated lockfile --- yarn.lock | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8fd2c418..cfeed0d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2525,19 +2525,17 @@ buffer@^6: base64-js "^1.3.1" ieee754 "^1.2.1" -<<<<<<< HEAD buffers@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" integrity sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ== -======= + busboy@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== dependencies: streamsearch "^1.1.0" ->>>>>>> develop call-bind@^1.0.2, call-bind@^1.0.7: version "1.0.7" @@ -6431,7 +6429,11 @@ stream-shift@^1.0.2: resolved "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz" integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== -<<<<<<< HEAD +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" @@ -6442,14 +6444,6 @@ stream-shift@^1.0.2: strip-ansi "^6.0.1" "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: -======= -streamsearch@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" - integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== - -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: ->>>>>>> develop version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -7107,12 +7101,7 @@ wordwrap@^1.0.0: resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -<<<<<<< HEAD "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": -======= -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: - name wrap-ansi-cjs ->>>>>>> develop version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From bf5dd62b3582e4eb5ba746493511df9f9d180f3f Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Tue, 20 Aug 2024 01:01:50 +0100 Subject: [PATCH 8/9] Updated Excel document --- .../api/assignments/[id]/[export]/excel.ts | 120 ++++++++++++++---- 1 file changed, 92 insertions(+), 28 deletions(-) diff --git a/src/pages/api/assignments/[id]/[export]/excel.ts b/src/pages/api/assignments/[id]/[export]/excel.ts index 25a96d4a..bd7da652 100644 --- a/src/pages/api/assignments/[id]/[export]/excel.ts +++ b/src/pages/api/assignments/[id]/[export]/excel.ts @@ -71,6 +71,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { path: string; version: string; }; + name: string; }; if (!data) { res.status(400).end(); @@ -102,11 +103,41 @@ async function post(req: NextApiRequest, res: NextApiResponse) { // we'll need the user in order to get the user data (name, email, focus, etc); const user = docUser.data() as CorporateUser; - const results = data.results.map((r: any) => r.score.correct); + 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); + 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 } + ), + 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 dates = data.results + const dates = assigneesData + .flatMap((r: any) => r.stats) .map((r: any) => r.date) .map((d: number) => moment(d)); const firstDate = moment.min(dates); @@ -121,7 +152,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { label: "Report Download date :", value: moment().format("DD/MM/YYYY"), }, - { label: "Test Information :", value: "TODO" }, + { label: "Test Information :", value: data.name}, { label: "Date of Test :", value: moment(data.startDate).format("DD/MM/YYYY"), @@ -129,6 +160,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { { 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 :", @@ -152,8 +184,6 @@ async function post(req: NextApiRequest, res: NextApiResponse) { logWorksheetData(worksheet); - const testSectionsArray = [1, 2, 3, 4, 5]; - // Define the static part of the headers (before "Test Sections") const staticHeaders = [ "Sr N", @@ -165,20 +195,23 @@ async function post(req: NextApiRequest, res: NextApiResponse) { ]; // Define additional headers after "Test Sections" - const additionalHeaders = ["Time Spent", "Score", "Level"]; + const additionalHeaders = ["Time Spent", "Score"]; // Calculate the dynamic columns based on the testSectionsArray - const numberOfTestSections = testSectionsArray.length; - const testSectionHeaders = testSectionsArray.map( + const testSectionHeaders = uniqueExercises.map( (section, index) => `Part ${index + 1}` ); - // Add the main header row, merging static columns and "Test Sections" - worksheet.addRow([ + const tableColumnHeadersFirstPart = [ ...staticHeaders, - ...testSectionsArray.map((a) => "Test Sections"), + ...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; @@ -187,7 +220,12 @@ async function post(req: NextApiRequest, res: NextApiResponse) { // // Merge "Test Sections" over dynamic number of columns // const tableColumns = staticHeaders.length + numberOfTestSections; - // worksheet.mergeCells(1, staticHeaders.length + 1, 1, tableColumns); + worksheet.mergeCells( + 1, + staticHeaders.length + 1, + 1, + tableColumnHeadersFirstPart.length + ); // logWorksheetData(worksheet); // worksheet.mergeCells(`G1:G3`); // Time Spent @@ -200,32 +238,58 @@ async function post(req: NextApiRequest, res: NextApiResponse) { ...testSectionHeaders, "", "", + ]); + worksheet.addRow([ + ...Array(staticHeaders.length).fill(""), + ...uniqueExercises.map(() => "Grammar & Vocabulary"), + "", "", ]); worksheet.addRow([ ...Array(staticHeaders.length).fill(""), - ...testSectionsArray.map(() => "Grammar"), - "", + ...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 + 2}`); // "Sr N" - worksheet.mergeCells(`B${startIndexTable}:B${startIndexTable + 2}`); // "Candidate ID" - worksheet.mergeCells(`C${startIndexTable}:C${startIndexTable + 2}`); // "First and Last Name" - worksheet.mergeCells(`D${startIndexTable}:D${startIndexTable + 2}`); // "Passport/ID" - worksheet.mergeCells(`E${startIndexTable}:E${startIndexTable + 2}`); // "Email ID" - worksheet.mergeCells(`F${startIndexTable}:F${startIndexTable + 2}`); // "Gender" + 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" - // worksheet.addRow(users.map((a) => { - // })) + 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}`; + }), + `${ + data.stats.reduce( + (acc: number, curr: any) => acc + curr.timeSpent, + 0 + ) / 60 + } minutes`, + data.correct, + ]); + }); - for ( - let i = 0; - i < staticHeaders.length + additionalHeaders.length + 1; - i++ - ) { + worksheet.addRow([""]); + worksheet.addRow([""]); + + for (let i = 0; i < tableColumnHeaders.length; i++) { worksheet.getColumn(i + 1).width = 30; } From 1cd4dfc3972be9343eabad75161ab15c761ce54a Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Tue, 20 Aug 2024 01:15:43 +0100 Subject: [PATCH 9/9] Added download option to master Corporate and teacher --- src/dashboards/MasterCorporate.tsx | 2 ++ src/dashboards/Teacher.tsx | 2 ++ src/pages/api/assignments/[id]/[export]/excel.ts | 12 ++---------- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/dashboards/MasterCorporate.tsx b/src/dashboards/MasterCorporate.tsx index 03b8c840..6c25f391 100644 --- a/src/dashboards/MasterCorporate.tsx +++ b/src/dashboards/MasterCorporate.tsx @@ -614,6 +614,7 @@ export default function MasterCorporateDashboard({user}: Props) { allowDownload reload={reloadAssignments} allowArchive + allowExcelDownload /> ))} @@ -630,6 +631,7 @@ export default function MasterCorporateDashboard({user}: Props) { allowDownload reload={reloadAssignments} allowUnarchive + allowExcelDownload /> ))} diff --git a/src/dashboards/Teacher.tsx b/src/dashboards/Teacher.tsx index 243df376..e1ddbbb5 100644 --- a/src/dashboards/Teacher.tsx +++ b/src/dashboards/Teacher.tsx @@ -260,6 +260,7 @@ export default function TeacherDashboard({user}: Props) { allowDownload reload={reloadAssignments} allowArchive + allowExcelDownload /> ))} @@ -276,6 +277,7 @@ export default function TeacherDashboard({user}: Props) { allowDownload reload={reloadAssignments} allowUnarchive + allowExcelDownload /> ))} diff --git a/src/pages/api/assignments/[id]/[export]/excel.ts b/src/pages/api/assignments/[id]/[export]/excel.ts index bd7da652..20661676 100644 --- a/src/pages/api/assignments/[id]/[export]/excel.ts +++ b/src/pages/api/assignments/[id]/[export]/excel.ts @@ -182,8 +182,6 @@ async function post(req: NextApiRequest, res: NextApiResponse) { worksheet.getCell(`B${index + 1}`).value = value; // Second column (values) }); - logWorksheetData(worksheet); - // Define the static part of the headers (before "Test Sections") const staticHeaders = [ "Sr N", @@ -216,8 +214,6 @@ async function post(req: NextApiRequest, res: NextApiResponse) { // 1 headers rows const startIndexTable = firstSectionData.length + 1; - logWorksheetData(worksheet); - // // Merge "Test Sections" over dynamic number of columns // const tableColumns = staticHeaders.length + numberOfTestSections; worksheet.mergeCells( @@ -227,10 +223,6 @@ async function post(req: NextApiRequest, res: NextApiResponse) { tableColumnHeadersFirstPart.length ); - // logWorksheetData(worksheet); - // worksheet.mergeCells(`G1:G3`); // Time Spent - // worksheet.mergeCells(`H1:H3`); // Score - // worksheet.mergeCells(`I1:I3`); // Level // Add the dynamic second and third header rows for test sections and sub-columns worksheet.addRow([ @@ -277,10 +269,10 @@ async function post(req: NextApiRequest, res: NextApiResponse) { return `${score.correct}/${score.total}`; }), `${ - data.stats.reduce( + Math.ceil(data.stats.reduce( (acc: number, curr: any) => acc + curr.timeSpent, 0 - ) / 60 + ) / 60) } minutes`, data.correct, ]);