From 5a685ebe805a5e08a9a11e65c75c7e8eaa702bf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Marques=20Lima?= Date: Sun, 2 Feb 2025 23:58:23 +0000 Subject: [PATCH] Remove unused imports and changed and improved layout design and responsiveness in some components and fixed some bugs. --- .../ExamEditor/SettingsEditor/index.tsx | 2 +- .../SettingsEditor/listening/components.tsx | 2 +- .../SettingsEditor/reading/components.tsx | 217 ++-- .../ExamEditor/Shared/ExerciseLabel.tsx | 4 +- src/components/ExamEditor/index.tsx | 455 +++++---- .../Exercises/MatchSentences/DragNDrop.tsx | 2 +- src/components/High/CardList.tsx | 2 - src/components/High/Table.tsx | 248 +++-- src/components/Sidebar.tsx | 2 +- src/components/Solutions/MatchSentences.tsx | 2 +- src/exams/Selection.tsx | 10 +- src/hooks/useEntitiesUsers.tsx | 9 +- src/pages/(admin)/CorporateGradingSystem.tsx | 459 ++++++--- src/pages/(admin)/Lists/UserList.tsx | 932 +++++++++++------- src/pages/_app.tsx | 2 - src/pages/api/tickets/assignedToUser/index.ts | 2 +- src/pages/dashboard/admin.tsx | 13 +- src/pages/dashboard/corporate.tsx | 1 - src/pages/dashboard/index.tsx | 1 - src/pages/dashboard/mastercorporate.tsx | 1 - src/pages/dashboard/student.tsx | 21 +- src/pages/generation.tsx | 2 +- src/pages/official-exam.tsx | 1 - src/styles/globals.css | 136 ++- src/utils/users.ts | 9 +- 25 files changed, 1504 insertions(+), 1031 deletions(-) diff --git a/src/components/ExamEditor/SettingsEditor/index.tsx b/src/components/ExamEditor/SettingsEditor/index.tsx index 306a578b..faf6bcbc 100644 --- a/src/components/ExamEditor/SettingsEditor/index.tsx +++ b/src/components/ExamEditor/SettingsEditor/index.tsx @@ -95,7 +95,7 @@ const SettingsEditor: React.FC = ({ }, [updateLocalAndScheduleGlobal]); return ( -
+
{sectionLabel} Settings
= ({ currentSection, localSettings, u setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isAudioContextOpen: isOpen }, false)} contentWrapperClassName={level ? `border border-ielts-listening` : ''} > -
+
, schedule?: boolean) => void; - currentSection: ReadingPart | LevelPart; - generatePassageDisabled?: boolean; - levelId?: number; - level?: boolean; + localSettings: ReadingSectionSettings | LevelSectionSettings; + updateLocalAndScheduleGlobal: ( + updates: Partial, + schedule?: boolean + ) => void; + currentSection: ReadingPart | LevelPart; + generatePassageDisabled?: boolean; + levelId?: number; + level?: boolean; } -const ReadingComponents: React.FC = ({localSettings, updateLocalAndScheduleGlobal, currentSection, levelId, level = false, generatePassageDisabled = false}) => { - const { currentModule } = useExamEditorStore(); - const { - focusedSection, - difficulty, - } = useExamEditorStore(state => state.modules[currentModule]); +const ReadingComponents: React.FC = ({ + localSettings, + updateLocalAndScheduleGlobal, + currentSection, + levelId, + level = false, + generatePassageDisabled = false, +}) => { + const { currentModule } = useExamEditorStore(); + const { focusedSection, difficulty } = useExamEditorStore( + (state) => state.modules[currentModule] + ); - const generatePassage = useCallback(() => { - generate( - levelId ? levelId : focusedSection, - "reading", - "passage", - { - method: 'GET', - queryParams: { - difficulty, - ...(localSettings.readingTopic && { topic: localSettings.readingTopic }) - } - }, - (data: any) => [{ - title: data.title, - text: data.text - }], - level ? focusedSection : undefined, - level - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [localSettings.readingTopic, difficulty, focusedSection, levelId]); - - const onTopicChange = useCallback((readingTopic: string) => { - updateLocalAndScheduleGlobal({ readingTopic }); - }, [updateLocalAndScheduleGlobal]); - - return ( - <> - updateLocalAndScheduleGlobal({ isPassageOpen: isOpen }, false)} - contentWrapperClassName={level ? `border border-ielts-reading`: ''} - disabled={generatePassageDisabled} - > -
-
- - -
-
- -
-
-
- updateLocalAndScheduleGlobal({ isReadingTopicOpean: isOpen })} - contentWrapperClassName={level ? `border border-ielts-reading`: ''} - disabled={currentSection === undefined || currentSection.text === undefined || currentSection.text.content === "" || currentSection.text.title === ""} - > - - - + const generatePassage = useCallback(() => { + generate( + levelId ? levelId : focusedSection, + "reading", + "passage", + { + method: "GET", + queryParams: { + difficulty, + ...(localSettings.readingTopic && { + topic: localSettings.readingTopic, + }), + }, + }, + (data: any) => [ + { + title: data.title, + text: data.text, + }, + ], + level ? focusedSection : undefined, + level ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [localSettings.readingTopic, difficulty, focusedSection, levelId]); + + const onTopicChange = useCallback( + (readingTopic: string) => { + updateLocalAndScheduleGlobal({ readingTopic }); + }, + [updateLocalAndScheduleGlobal] + ); + + return ( + <> + + updateLocalAndScheduleGlobal({ isPassageOpen: isOpen }, false) + } + contentWrapperClassName={level ? `border border-ielts-reading` : ""} + disabled={generatePassageDisabled} + > +
+
+ + +
+
+ +
+
+
+ + updateLocalAndScheduleGlobal({ isReadingTopicOpean: isOpen }) + } + contentWrapperClassName={level ? `border border-ielts-reading` : ""} + disabled={ + currentSection === undefined || + currentSection.text === undefined || + currentSection.text.content === "" || + currentSection.text.title === "" + } + > + + + + ); }; export default ReadingComponents; diff --git a/src/components/ExamEditor/Shared/ExerciseLabel.tsx b/src/components/ExamEditor/Shared/ExerciseLabel.tsx index 024d0b59..fab965c3 100644 --- a/src/components/ExamEditor/Shared/ExerciseLabel.tsx +++ b/src/components/ExamEditor/Shared/ExerciseLabel.tsx @@ -19,8 +19,8 @@ const label = (type: string, firstId: string, lastId: string) => { const ExerciseLabel: React.FC = ({type, firstId, lastId, prompt}) => { return (
- {label(type, firstId, lastId)} -
{previewLabel(prompt)}
+ {label(type, firstId, lastId)} +
{previewLabel(prompt)}
); } diff --git a/src/components/ExamEditor/index.tsx b/src/components/ExamEditor/index.tsx index 54e6b87d..a55ff259 100644 --- a/src/components/ExamEditor/index.tsx +++ b/src/components/ExamEditor/index.tsx @@ -24,216 +24,265 @@ import ListeningInstructions from "./Standalone/ListeningInstructions"; const DIFFICULTIES: Difficulty[] = ["A1", "A2", "B1", "B2", "C1", "C2"]; const ExamEditor: React.FC<{ levelParts?: number }> = ({ levelParts = 0 }) => { - const { currentModule, dispatch } = useExamEditorStore(); - const { - sections, - minTimer, - expandedSections, - examLabel, - isPrivate, - difficulty, - sectionLabels, - importModule - } = useExamEditorStore(state => state.modules[currentModule]); + const { currentModule, dispatch } = useExamEditorStore(); + const { + sections, + minTimer, + expandedSections, + examLabel, + isPrivate, + difficulty, + sectionLabels, + importModule, + } = useExamEditorStore((state) => state.modules[currentModule]); - const [numberOfLevelParts, setNumberOfLevelParts] = useState(levelParts !== 0 ? levelParts : 1); - const [isResetModuleOpen, setIsResetModuleOpen] = useState(false); + const [numberOfLevelParts, setNumberOfLevelParts] = useState( + levelParts !== 0 ? levelParts : 1 + ); + const [isResetModuleOpen, setIsResetModuleOpen] = useState(false); - // For exam edits - useEffect(() => { - if (levelParts !== 0) { - setNumberOfLevelParts(levelParts); - dispatch({ - type: 'UPDATE_MODULE', - payload: { - updates: { - sectionLabels: Array.from({ length: levelParts }).map((_, i) => ({ - id: i + 1, - label: `Part ${i + 1}` - })) - }, - module: "level" - } - }) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [levelParts]) + // For exam edits + useEffect(() => { + if (levelParts !== 0) { + setNumberOfLevelParts(levelParts); + dispatch({ + type: "UPDATE_MODULE", + payload: { + updates: { + sectionLabels: Array.from({ length: levelParts }).map((_, i) => ({ + id: i + 1, + label: `Part ${i + 1}`, + })), + }, + module: "level", + }, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [levelParts]); - useEffect(() => { - const currentSections = sections; - const currentLabels = sectionLabels; - let updatedSections: SectionState[]; - let updatedLabels: any; - if (currentModule === "level" && currentSections.length !== currentLabels.length || numberOfLevelParts !== currentSections.length) { - const newSections = [...currentSections]; - const newLabels = [...currentLabels]; - for (let i = currentLabels.length; i < numberOfLevelParts; i++) { - if (currentSections.length !== numberOfLevelParts) newSections.push(defaultSectionSettings(currentModule, i + 1)); - newLabels.push({ - id: i + 1, - label: `Part ${i + 1}` - }); - } - updatedSections = newSections; - updatedLabels = newLabels; - } else if (numberOfLevelParts < currentSections.length) { - updatedSections = currentSections.slice(0, numberOfLevelParts); - updatedLabels = currentLabels.slice(0, numberOfLevelParts); - } else { - return; - } - - const updatedExpandedSections = expandedSections.filter( - sectionId => updatedSections.some(section => section.sectionId === sectionId) - ); - - dispatch({ - type: 'UPDATE_MODULE', - payload: { - updates: { - sections: updatedSections, - sectionLabels: updatedLabels, - expandedSections: updatedExpandedSections - } - } + useEffect(() => { + const currentSections = sections; + const currentLabels = sectionLabels; + let updatedSections: SectionState[]; + let updatedLabels: any; + if ( + (currentModule === "level" && + currentSections.length !== currentLabels.length) || + numberOfLevelParts !== currentSections.length + ) { + const newSections = [...currentSections]; + const newLabels = [...currentLabels]; + for (let i = currentLabels.length; i < numberOfLevelParts; i++) { + if (currentSections.length !== numberOfLevelParts) + newSections.push(defaultSectionSettings(currentModule, i + 1)); + newLabels.push({ + id: i + 1, + label: `Part ${i + 1}`, }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [numberOfLevelParts]); - - const sectionIds = sections.map((section) => section.sectionId) - - const updateModule = useCallback((updates: Partial) => { - dispatch({ type: 'UPDATE_MODULE', payload: { updates } }); - }, [dispatch]); - - const toggleSection = (sectionId: number) => { - if (expandedSections.length === 1 && sectionIds.includes(sectionId)) { - toast.error("Include at least one section!"); - return; - } - dispatch({ type: 'TOGGLE_SECTION', payload: { sectionId } }); - }; - - const ModuleSettings: Record = { - reading: ReadingSettings, - writing: WritingSettings, - speaking: SpeakingSettings, - listening: ListeningSettings, - level: LevelSettings - }; - - const Settings = ModuleSettings[currentModule]; - const showImport = importModule && ["reading", "listening", "level"].includes(currentModule); - - const updateLevelParts = (parts: number) => { - setNumberOfLevelParts(parts); + } + updatedSections = newSections; + updatedLabels = newLabels; + } else if (numberOfLevelParts < currentSections.length) { + updatedSections = currentSections.slice(0, numberOfLevelParts); + updatedLabels = currentLabels.slice(0, numberOfLevelParts); + } else { + return; } - return ( - <> - {showImport ? : ( - <> - {isResetModuleOpen && } -
-
- - updateModule({ minTimer: parseInt(e) < 15 ? 15 : parseInt(e) })} - value={minTimer} - className="max-w-[300px]" - /> -
-
- - setNumberOfLevelParts(parseInt(v))} value={numberOfLevelParts} /> -
- )} -
-
- updateModule({ isPrivate: checked })}> - Privacy (Only available for Assignments) - -
-
-
-
- - updateModule({ examLabel: text })} - roundness="xl" - value={examLabel} - required - /> -
- {currentModule === "listening" && } - -
-
- -
- -
-
- - )} - + const updatedExpandedSections = expandedSections.filter((sectionId) => + updatedSections.some((section) => section.sectionId === sectionId) ); + + dispatch({ + type: "UPDATE_MODULE", + payload: { + updates: { + sections: updatedSections, + sectionLabels: updatedLabels, + expandedSections: updatedExpandedSections, + }, + }, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [numberOfLevelParts]); + + const sectionIds = sections.map((section) => section.sectionId); + + const updateModule = useCallback( + (updates: Partial) => { + dispatch({ type: "UPDATE_MODULE", payload: { updates } }); + }, + [dispatch] + ); + + const toggleSection = (sectionId: number) => { + if (expandedSections.length === 1 && sectionIds.includes(sectionId)) { + toast.error("Include at least one section!"); + return; + } + dispatch({ type: "TOGGLE_SECTION", payload: { sectionId } }); + }; + + const ModuleSettings: Record = { + reading: ReadingSettings, + writing: WritingSettings, + speaking: SpeakingSettings, + listening: ListeningSettings, + level: LevelSettings, + }; + + const Settings = ModuleSettings[currentModule]; + const showImport = + importModule && ["reading", "listening", "level"].includes(currentModule); + + const updateLevelParts = (parts: number) => { + setNumberOfLevelParts(parts); + }; + + return ( + <> + {showImport ? ( + + ) : ( + <> + {isResetModuleOpen && ( + + )} +
+
+
+ + + updateModule({ + minTimer: parseInt(e) < 15 ? 15 : parseInt(e), + }) + } + value={minTimer} + className="max-w-[300px]" + /> +
+
+ + setNumberOfLevelParts(parseInt(v))} + value={numberOfLevelParts} + /> +
+ )} +
+
+ updateModule({ isPrivate: checked })} + > + Privacy (Only available for Assignments) + +
+
+
+
+ + updateModule({ examLabel: text })} + roundness="xl" + value={examLabel} + required + /> +
+ {currentModule === "listening" && } + +
+
+ +
+ +
+
+ + )} + + ); }; export default ExamEditor; diff --git a/src/components/Exercises/MatchSentences/DragNDrop.tsx b/src/components/Exercises/MatchSentences/DragNDrop.tsx index 237e3d46..5adbc252 100644 --- a/src/components/Exercises/MatchSentences/DragNDrop.tsx +++ b/src/components/Exercises/MatchSentences/DragNDrop.tsx @@ -24,7 +24,7 @@ const DroppableQuestionArea: React.FC = ({ question,
+ className={clsx("w-48 h-10 border-2 border-mti-purple-light self-center rounded-xl flex items-center justify-center", isOver && "border-mti-purple-dark")}> {answer && `Paragraph ${answer}`}
diff --git a/src/components/High/CardList.tsx b/src/components/High/CardList.tsx index 31075dcd..24befffb 100644 --- a/src/components/High/CardList.tsx +++ b/src/components/High/CardList.tsx @@ -2,8 +2,6 @@ import {useListSearch} from "@/hooks/useListSearch"; import usePagination from "@/hooks/usePagination"; import { clsx } from "clsx"; import {ReactNode} from "react"; -import Checkbox from "../Low/Checkbox"; -import Separator from "../Low/Separator"; interface Props { list: T[]; diff --git a/src/components/High/Table.tsx b/src/components/High/Table.tsx index 479e614c..2d588ffc 100644 --- a/src/components/High/Table.tsx +++ b/src/components/High/Table.tsx @@ -1,109 +1,159 @@ -import { useListSearch } from "@/hooks/useListSearch" -import { ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, PaginationState, useReactTable } from "@tanstack/react-table" -import clsx from "clsx" -import { useEffect, useState } from "react" -import { BsArrowDown, BsArrowUp } from "react-icons/bs" -import Button from "../Low/Button" +import { useListSearch } from "@/hooks/useListSearch"; +import { + ColumnDef, + flexRender, + getCoreRowModel, + getPaginationRowModel, + getSortedRowModel, + PaginationState, + useReactTable, +} from "@tanstack/react-table"; +import clsx from "clsx"; +import { useEffect, useState } from "react"; +import { BsArrowDown, BsArrowUp } from "react-icons/bs"; +import Button from "../Low/Button"; interface Props { - data: T[] - columns: ColumnDef[] - searchFields: string[][] - size?: number - onDownload?: (rows: T[]) => void - isDownloadLoading?: boolean - searchPlaceholder?: string + data: T[]; + columns: ColumnDef[]; + searchFields: string[][]; + size?: number; + onDownload?: (rows: T[]) => void; + isDownloadLoading?: boolean; + searchPlaceholder?: string; + isLoading?: boolean; } -export default function Table({ data, columns, searchFields, size = 16, onDownload, isDownloadLoading, searchPlaceholder }: Props) { - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: size, - }) +export default function Table({ + data, + columns, + searchFields, + size = 16, + onDownload, + isDownloadLoading, + searchPlaceholder, + isLoading, +}: Props) { + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: size, + }); - const { rows, renderSearch } = useListSearch(searchFields, data, searchPlaceholder); + const { rows, renderSearch } = useListSearch( + searchFields, + data, + searchPlaceholder + ); - const table = useReactTable({ - data: rows, - columns, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - getPaginationRowModel: getPaginationRowModel(), - onPaginationChange: setPagination, - state: { - pagination - } - }); + const table = useReactTable({ + data: rows, + columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onPaginationChange: setPagination, + state: { + pagination, + }, + }); - return ( -
-
- {renderSearch()} - {onDownload && ( - - ) - } -
+ return ( +
+
+ {renderSearch()} + {onDownload && ( + + )} +
-
-
- -
-
- -
Page
- - {table.getState().pagination.pageIndex + 1} of{' '} - {table.getPageCount().toLocaleString()} - -
| Total: {table.getRowCount().toLocaleString()}
-
- -
-
+
+
+ +
+
+ +
Page
+ + {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount().toLocaleString()} + +
| Total: {table.getRowCount().toLocaleString()}
+
+ +
+
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} - - ))} - -
-
- {flexRender( - header.column.columnDef.header, - header.getContext() - )} - {{ - asc: , - desc: , - }[header.column.getIsSorted() as string] ?? null} -
-
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
-
- ) + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} + {{ + asc: , + desc: , + }[header.column.getIsSorted() as string] ?? null} +
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ {isLoading && ( +
+ +
+ )} +
+ ); } diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index a4b6f39e..2c25099b 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -260,7 +260,7 @@ export default function Sidebar({
diff --git a/src/components/Solutions/MatchSentences.tsx b/src/components/Solutions/MatchSentences.tsx index 3f177a46..47fd2792 100644 --- a/src/components/Solutions/MatchSentences.tsx +++ b/src/components/Solutions/MatchSentences.tsx @@ -29,7 +29,7 @@ function QuestionSolutionArea({
), label: "Reading", - value: reading, + value: reading || 0, tooltip: "The amount of reading exams performed.", }, { @@ -97,7 +97,7 @@ export default function Selection({ user, page, onStart }: Props) { ), label: "Listening", - value: listening, + value: listening || 0, tooltip: "The amount of listening exams performed.", }, { @@ -105,7 +105,7 @@ export default function Selection({ user, page, onStart }: Props) { ), label: "Writing", - value: writing, + value: writing || 0, tooltip: "The amount of writing exams performed.", }, { @@ -113,7 +113,7 @@ export default function Selection({ user, page, onStart }: Props) { ), label: "Speaking", - value: speaking, + value: speaking || 0, tooltip: "The amount of speaking exams performed.", }, { @@ -121,7 +121,7 @@ export default function Selection({ user, page, onStart }: Props) { ), label: "Level", - value: level, + value: level || 0, tooltip: "The amount of level exams performed.", }, ]} diff --git a/src/hooks/useEntitiesUsers.tsx b/src/hooks/useEntitiesUsers.tsx index f1c7be6a..17385409 100644 --- a/src/hooks/useEntitiesUsers.tsx +++ b/src/hooks/useEntitiesUsers.tsx @@ -1,6 +1,5 @@ -import { EntityWithRoles, WithLabeledEntities } from "@/interfaces/entity"; -import { Discount } from "@/interfaces/paypal"; -import { Code, Group, Type, User } from "@/interfaces/user"; +import { WithLabeledEntities } from "@/interfaces/entity"; +import { Type, User } from "@/interfaces/user"; import axios from "axios"; import { useEffect, useState } from "react"; @@ -12,7 +11,9 @@ export default function useEntitiesUsers(type?: Type) { const getData = () => { setIsLoading(true); axios - .get[]>(`/api/entities/users${type ? "?type=" + type : ""}`) + .get[]>( + `/api/entities/users${type ? "?type=" + type : ""}` + ) .then((response) => setUsers(response.data)) .finally(() => setIsLoading(false)); }; diff --git a/src/pages/(admin)/CorporateGradingSystem.tsx b/src/pages/(admin)/CorporateGradingSystem.tsx index 28c006b6..9506108d 100644 --- a/src/pages/(admin)/CorporateGradingSystem.tsx +++ b/src/pages/(admin)/CorporateGradingSystem.tsx @@ -5,192 +5,329 @@ import Separator from "@/components/Low/Separator"; import { Grading, Step } from "@/interfaces"; import { Entity } from "@/interfaces/entity"; import { User } from "@/interfaces/user"; -import { CEFR_STEPS, GENERAL_STEPS, IELTS_STEPS, TOFEL_STEPS } from "@/resources/grading"; -import { mapBy } from "@/utils"; +import { + CEFR_STEPS, + GENERAL_STEPS, + IELTS_STEPS, + TOFEL_STEPS, +} from "@/resources/grading"; import { checkAccess } from "@/utils/permissions"; import axios from "axios"; import clsx from "clsx"; -import { Divider } from "primereact/divider"; -import { useEffect, useState } from "react"; +import { + Dispatch, + memo, + SetStateAction, + useCallback, + useEffect, + useState, +} from "react"; import { BsPlusCircle, BsTrash } from "react-icons/bs"; import { toast } from "react-toastify"; const areStepsOverlapped = (steps: Step[]) => { - for (let i = 0; i < steps.length; i++) { - if (i === 0) continue; + for (let i = 0; i < steps.length; i++) { + if (i === 0) continue; - const step = steps[i]; - const previous = steps[i - 1]; + const step = steps[i]; + const previous = steps[i - 1]; - if (previous.max >= step.min) return true; - } + if (previous.max >= step.min) return true; + } - return false; + return false; }; +interface RowProps { + min: number; + max: number; + index: number; + label: string; + isLast: boolean; + isLoading: boolean; + setSteps: Dispatch>; + addRow: (index: number) => void; +} +function GradingRow({ + min, + max, + label, + index, + isLoading, + isLast, + setSteps, + addRow, +}: RowProps) { + const onChangeMin = useCallback( + (e: string) => { + setSteps((prev) => + prev.map((x, i) => (i === index ? { ...x, min: parseInt(e) } : x)) + ); + }, + [index, setSteps] + ); + + const onChangeMax = useCallback( + (e: string) => { + setSteps((prev) => + prev.map((x, i) => (i === index ? { ...x, max: parseInt(e) } : x)) + ); + }, + [index, setSteps] + ); + + const onChangeLabel = useCallback( + (e: string) => { + setSteps((prev) => + prev.map((x, i) => (i === index ? { ...x, label: e } : x)) + ); + }, + [index, setSteps] + ); + + const onAddRow = useCallback(() => addRow(index), [addRow, index]); + + const removeRow = useCallback( + () => setSteps((prev) => prev.filter((_, i) => i !== index)), + [index, setSteps] + ); + return ( + <> +
+
+ + + +
+ {index !== 0 && !isLast && ( + + )} +
+ + {!isLast && ( + + )} + + ); +} +const GradingRowMemo = memo(GradingRow); interface Props { - user: User; - entitiesGrading: Grading[]; - entities: Entity[] - mutate: () => void + user: User; + entitiesGrading: Grading[]; + entities: Entity[]; + mutate: () => void; } -export default function CorporateGradingSystem({ user, entitiesGrading = [], entities = [], mutate }: Props) { - const [entity, setEntity] = useState(entitiesGrading[0]?.entity || undefined) - const [isLoading, setIsLoading] = useState(false); - const [steps, setSteps] = useState([]); - const [otherEntities, setOtherEntities] = useState([]) +export default function CorporateGradingSystem({ + user, + entitiesGrading = [], + entities = [], + mutate, +}: Props) { + const [entity, setEntity] = useState(entitiesGrading[0]?.entity || undefined); + const [isLoading, setIsLoading] = useState(false); + const [steps, setSteps] = useState([]); + const [otherEntities, setOtherEntities] = useState([]); - useEffect(() => { - if (entity) { - const entitySteps = entitiesGrading.find(e => e.entity === entity)!.steps - setSteps(entitySteps || []) - } - }, [entitiesGrading, entity]) + useEffect(() => { + if (entity) { + const entitySteps = entitiesGrading.find( + (e) => e.entity === entity + )!.steps; + setSteps(entitySteps || []); + } + }, [entitiesGrading, entity]); - const saveGradingSystem = () => { - if (!steps.every((x) => x.min < x.max)) return toast.error("One of your steps has a minimum threshold inferior to its superior threshold."); - if (areStepsOverlapped(steps)) return toast.error("There seems to be an overlap in one of your steps."); - if ( - steps.reduce((acc, curr) => { - return acc - (curr.max - curr.min + 1); - }, 100) > 0 - ) - return toast.error("There seems to be an open interval in your steps."); + const saveGradingSystem = () => { + if (!steps.every((x) => x.min < x.max)) + return toast.error( + "One of your steps has a minimum threshold inferior to its superior threshold." + ); + if (areStepsOverlapped(steps)) + return toast.error("There seems to be an overlap in one of your steps."); + if ( + steps.reduce((acc, curr) => { + return acc - (curr.max - curr.min + 1); + }, 100) > 0 + ) + return toast.error("There seems to be an open interval in your steps."); - setIsLoading(true); - axios - .post("/api/grading", { user: user.id, entity, steps }) - .then(() => toast.success("Your grading system has been saved!")) - .then(mutate) - .catch(() => toast.error("Something went wrong, please try again later")) - .finally(() => setIsLoading(false)); - }; + setIsLoading(true); + axios + .post("/api/grading", { user: user.id, entity, steps }) + .then(() => toast.success("Your grading system has been saved!")) + .then(mutate) + .catch(() => toast.error("Something went wrong, please try again later")) + .finally(() => setIsLoading(false)); + }; - const applyToOtherEntities = () => { - if (!steps.every((x) => x.min < x.max)) return toast.error("One of your steps has a minimum threshold inferior to its superior threshold."); - if (areStepsOverlapped(steps)) return toast.error("There seems to be an overlap in one of your steps."); - if ( - steps.reduce((acc, curr) => { - return acc - (curr.max - curr.min + 1); - }, 100) > 0 - ) - return toast.error("There seems to be an open interval in your steps."); + const applyToOtherEntities = () => { + if (!steps.every((x) => x.min < x.max)) + return toast.error( + "One of your steps has a minimum threshold inferior to its superior threshold." + ); + if (areStepsOverlapped(steps)) + return toast.error("There seems to be an overlap in one of your steps."); + if ( + steps.reduce((acc, curr) => { + return acc - (curr.max - curr.min + 1); + }, 100) > 0 + ) + return toast.error("There seems to be an open interval in your steps."); - if (otherEntities.length === 0) return toast.error("Select at least one entity") + if (otherEntities.length === 0) + return toast.error("Select at least one entity"); - setIsLoading(true); - axios - .post("/api/grading/multiple", { user: user.id, entities: otherEntities, steps }) - .then(() => toast.success("Your grading system has been saved!")) - .then(mutate) - .catch(() => toast.error("Something went wrong, please try again later")) - .finally(() => setIsLoading(false)); - }; + setIsLoading(true); + axios + .post("/api/grading/multiple", { + user: user.id, + entities: otherEntities, + steps, + }) + .then(() => toast.success("Your grading system has been saved!")) + .then(mutate) + .catch(() => toast.error("Something went wrong, please try again later")) + .finally(() => setIsLoading(false)); + }; - return ( -
- -
- - ({ value: e.id, label: e.label }))} - onChange={(e) => !e ? setOtherEntities([]) : setOtherEntities(e.map(o => o.value!))} - isMulti - /> - - - - )} + return ( +
+ +
+ + ({ value: e.id, label: e.label }))} + onChange={(e) => + !e + ? setOtherEntities([]) + : setOtherEntities(e.map((o) => o.value!)) + } + isMulti + /> + + + + )} - {steps.map((step, index) => ( - <> -
-
- setSteps((prev) => prev.map((x, i) => (i === index ? { ...x, min: parseInt(e) } : x)))} - name="min" - /> - setSteps((prev) => prev.map((x, i) => (i === index ? { ...x, label: e } : x)))} - name="min" - /> - setSteps((prev) => prev.map((x, i) => (i === index ? { ...x, max: parseInt(e) } : x)))} - name="max" - /> -
- {index !== 0 && index !== steps.length - 1 && ( - - )} -
+ +
+ + + + +
- {index < steps.length - 1 && ( - - )} - - ))} + {steps.map((step, index) => ( + + ))} - -
- ); + +
+ ); } diff --git a/src/pages/(admin)/Lists/UserList.tsx b/src/pages/(admin)/Lists/UserList.tsx index 63ac86e9..e11524e8 100644 --- a/src/pages/(admin)/Lists/UserList.tsx +++ b/src/pages/(admin)/Lists/UserList.tsx @@ -6,7 +6,12 @@ import clsx from "clsx"; import { capitalize } from "lodash"; import moment from "moment"; import { useEffect, useMemo, useState } from "react"; -import { BsCheck, BsCheckCircle, BsFillExclamationOctagonFill, BsTrash } from "react-icons/bs"; +import { + BsCheck, + BsCheckCircle, + BsFillExclamationOctagonFill, + BsTrash, +} from "react-icons/bs"; import { toast } from "react-toastify"; import { countries, TCountries } from "countries-list"; import countryCodes from "country-codes-list"; @@ -24,426 +29,597 @@ import { WithLabeledEntities } from "@/interfaces/entity"; import Table from "@/components/High/Table"; import useEntities from "@/hooks/useEntities"; import { useAllowedEntities } from "@/hooks/useEntityPermissions"; -import { findAllowedEntities } from "@/utils/permissions"; const columnHelper = createColumnHelper>(); const searchFields = [["name"], ["email"], ["entities", ""]]; export default function UserList({ - user, - filters = [], - type, - renderHeader, + user, + filters = [], + type, + renderHeader, }: { - user: User; - filters?: ((user: User) => boolean)[]; - type?: Type; - renderHeader?: (total: number) => JSX.Element; + user: User; + filters?: ((user: User) => boolean)[]; + type?: Type; + renderHeader?: (total: number) => JSX.Element; }) { - const [showDemographicInformation, setShowDemographicInformation] = useState(false); - const [selectedUser, setSelectedUser] = useState(); + const [showDemographicInformation, setShowDemographicInformation] = + useState(false); + const [selectedUser, setSelectedUser] = useState(); - const { users, reload } = useEntitiesUsers(type) - const { entities } = useEntities() + const { users, isLoading, reload } = useEntitiesUsers(type); + const { entities } = useEntities(); - const isAdmin = useMemo(() => ["admin", "developer"].includes(user?.type), [user?.type]) + const isAdmin = useMemo( + () => ["admin", "developer"].includes(user?.type), + [user?.type] + ); - const entitiesViewStudents = useAllowedEntities(user, entities, "view_students") - const entitiesEditStudents = useAllowedEntities(user, entities, "edit_students") - const entitiesDeleteStudents = useAllowedEntities(user, entities, "delete_students") + const entitiesViewStudents = useAllowedEntities( + user, + entities, + "view_students" + ); + const entitiesEditStudents = useAllowedEntities( + user, + entities, + "edit_students" + ); + const entitiesDeleteStudents = useAllowedEntities( + user, + entities, + "delete_students" + ); - const entitiesViewTeachers = useAllowedEntities(user, entities, "view_teachers") - const entitiesEditTeachers = useAllowedEntities(user, entities, "edit_teachers") - const entitiesDeleteTeachers = useAllowedEntities(user, entities, "delete_teachers") + const entitiesViewTeachers = useAllowedEntities( + user, + entities, + "view_teachers" + ); + const entitiesEditTeachers = useAllowedEntities( + user, + entities, + "edit_teachers" + ); + const entitiesDeleteTeachers = useAllowedEntities( + user, + entities, + "delete_teachers" + ); - const entitiesViewCorporates = useAllowedEntities(user, entities, "view_corporates") - const entitiesEditCorporates = useAllowedEntities(user, entities, "edit_corporates") - const entitiesDeleteCorporates = useAllowedEntities(user, entities, "delete_corporates") + const entitiesViewCorporates = useAllowedEntities( + user, + entities, + "view_corporates" + ); + const entitiesEditCorporates = useAllowedEntities( + user, + entities, + "edit_corporates" + ); + const entitiesDeleteCorporates = useAllowedEntities( + user, + entities, + "delete_corporates" + ); - const entitiesViewMasterCorporates = useAllowedEntities(user, entities, "view_mastercorporates") - const entitiesEditMasterCorporates = useAllowedEntities(user, entities, "edit_mastercorporates") - const entitiesDeleteMasterCorporates = useAllowedEntities(user, entities, "delete_mastercorporates") + const entitiesViewMasterCorporates = useAllowedEntities( + user, + entities, + "view_mastercorporates" + ); + const entitiesEditMasterCorporates = useAllowedEntities( + user, + entities, + "edit_mastercorporates" + ); + const entitiesDeleteMasterCorporates = useAllowedEntities( + user, + entities, + "delete_mastercorporates" + ); - const entitiesDownloadUsers = useAllowedEntities(user, entities, "download_user_list") + const entitiesDownloadUsers = useAllowedEntities( + user, + entities, + "download_user_list" + ); - const appendUserFilters = useFilterStore((state) => state.appendUserFilter); - const router = useRouter(); + const appendUserFilters = useFilterStore((state) => state.appendUserFilter); + const router = useRouter(); - const expirationDateColor = (date: Date) => { - const momentDate = moment(date); - const today = moment(new Date()); + const expirationDateColor = (date: Date) => { + const momentDate = moment(date); + const today = moment(new Date()); - if (today.isAfter(momentDate)) return "!text-mti-red-light font-bold line-through"; - if (today.add(1, "weeks").isAfter(momentDate)) return "!text-mti-red-light"; - if (today.add(2, "weeks").isAfter(momentDate)) return "!text-mti-rose-light"; - if (today.add(1, "months").isAfter(momentDate)) return "!text-mti-orange-light"; - }; + if (today.isAfter(momentDate)) + return "!text-mti-red-light font-bold line-through"; + if (today.add(1, "weeks").isAfter(momentDate)) return "!text-mti-red-light"; + if (today.add(2, "weeks").isAfter(momentDate)) + return "!text-mti-rose-light"; + if (today.add(1, "months").isAfter(momentDate)) + return "!text-mti-orange-light"; + }; - const allowedUsers = useMemo(() => users.filter((u) => { - if (isAdmin) return true - if (u.id === user?.id) return false + const allowedUsers = useMemo( + () => + users.filter((u) => { + if (isAdmin) return true; + if (u.id === user?.id) return false; - switch (u.type) { - case "student": return mapBy((u.entities || []), 'id').some((id) => mapBy(entitiesViewStudents, 'id').includes(id)) - case "teacher": return mapBy((u.entities || []), 'id').some((id) => mapBy(entitiesViewTeachers, 'id').includes(id)) - case 'corporate': return mapBy((u.entities || []), 'id').some((id) => mapBy(entitiesViewCorporates, 'id').includes(id)) - case 'mastercorporate': return mapBy((u.entities || []), 'id').some((id) => mapBy(entitiesViewMasterCorporates, 'id').includes(id)) - default: return false - } - }) - , [entitiesViewCorporates, entitiesViewMasterCorporates, entitiesViewStudents, entitiesViewTeachers, isAdmin, user?.id, users]) + switch (u.type) { + case "student": + return mapBy(u.entities || [], "id").some((id) => + mapBy(entitiesViewStudents, "id").includes(id) + ); + case "teacher": + return mapBy(u.entities || [], "id").some((id) => + mapBy(entitiesViewTeachers, "id").includes(id) + ); + case "corporate": + return mapBy(u.entities || [], "id").some((id) => + mapBy(entitiesViewCorporates, "id").includes(id) + ); + case "mastercorporate": + return mapBy(u.entities || [], "id").some((id) => + mapBy(entitiesViewMasterCorporates, "id").includes(id) + ); + default: + return false; + } + }), + [ + entitiesViewCorporates, + entitiesViewMasterCorporates, + entitiesViewStudents, + entitiesViewTeachers, + isAdmin, + user?.id, + users, + ] + ); - const displayUsers = useMemo(() => - filters.length > 0 ? filters.reduce((d, f) => d.filter(f), allowedUsers) : allowedUsers, - [filters, allowedUsers]) + const displayUsers = useMemo( + () => + filters.length > 0 + ? filters.reduce((d, f) => d.filter(f), allowedUsers) + : allowedUsers, + [filters, allowedUsers] + ); - const deleteAccount = (user: User) => { - if (!confirm(`Are you sure you want to delete ${user.name}'s account?`)) return; + const deleteAccount = (user: User) => { + if (!confirm(`Are you sure you want to delete ${user.name}'s account?`)) + return; - axios - .delete<{ ok: boolean }>(`/api/user?id=${user.id}`) - .then(() => { - toast.success("User deleted successfully!"); - reload() - }) - .catch(() => { - toast.error("Something went wrong!", { toastId: "delete-error" }); - }) - .finally(reload); - }; + axios + .delete<{ ok: boolean }>(`/api/user?id=${user.id}`) + .then(() => { + toast.success("User deleted successfully!"); + reload(); + }) + .catch(() => { + toast.error("Something went wrong!", { toastId: "delete-error" }); + }) + .finally(reload); + }; - const verifyAccount = (user: User) => { - axios - .post<{ user?: User; ok?: boolean }>(`/api/users/update?id=${user.id}`, { - ...user, - isVerified: true, - }) - .then(() => { - toast.success("User verified successfully!"); - reload(); - }) - .catch(() => { - toast.error("Something went wrong!", { toastId: "update-error" }); - }); - }; + const verifyAccount = (user: User) => { + axios + .post<{ user?: User; ok?: boolean }>(`/api/users/update?id=${user.id}`, { + ...user, + isVerified: true, + }) + .then(() => { + toast.success("User verified successfully!"); + reload(); + }) + .catch(() => { + toast.error("Something went wrong!", { toastId: "update-error" }); + }); + }; - const toggleDisableAccount = (user: User) => { - if ( - !confirm( - `Are you sure you want to ${user.status === "disabled" ? "enable" : "disable"} ${user.name - }'s account? This change is usually related to their payment state.`, - ) - ) - return; + const toggleDisableAccount = (user: User) => { + if ( + !confirm( + `Are you sure you want to ${ + user.status === "disabled" ? "enable" : "disable" + } ${ + user.name + }'s account? This change is usually related to their payment state.` + ) + ) + return; - axios - .post<{ user?: User; ok?: boolean }>(`/api/users/update?id=${user.id}`, { - ...user, - status: user.status === "disabled" ? "active" : "disabled", - }) - .then(() => { - toast.success(`User ${user.status === "disabled" ? "enabled" : "disabled"} successfully!`); - reload(); - }) - .catch(() => { - toast.error("Something went wrong!", { toastId: "update-error" }); - }); - }; + axios + .post<{ user?: User; ok?: boolean }>(`/api/users/update?id=${user.id}`, { + ...user, + status: user.status === "disabled" ? "active" : "disabled", + }) + .then(() => { + toast.success( + `User ${ + user.status === "disabled" ? "enabled" : "disabled" + } successfully!` + ); + reload(); + }) + .catch(() => { + toast.error("Something went wrong!", { toastId: "update-error" }); + }); + }; - const getEditPermission = (type: Type) => { - if (type === "student") return entitiesEditStudents - if (type === "teacher") return entitiesEditTeachers - if (type === "corporate") return entitiesEditCorporates - if (type === "mastercorporate") return entitiesEditMasterCorporates + const getEditPermission = (type: Type) => { + if (type === "student") return entitiesEditStudents; + if (type === "teacher") return entitiesEditTeachers; + if (type === "corporate") return entitiesEditCorporates; + if (type === "mastercorporate") return entitiesEditMasterCorporates; - return [] - } + return []; + }; - const getDeletePermission = (type: Type) => { - if (type === "student") return entitiesDeleteStudents - if (type === "teacher") return entitiesDeleteTeachers - if (type === "corporate") return entitiesDeleteCorporates - if (type === "mastercorporate") return entitiesDeleteMasterCorporates + const getDeletePermission = (type: Type) => { + if (type === "student") return entitiesDeleteStudents; + if (type === "teacher") return entitiesDeleteTeachers; + if (type === "corporate") return entitiesDeleteCorporates; + if (type === "mastercorporate") return entitiesDeleteMasterCorporates; - return [] - } + return []; + }; - const canEditUser = (u: User) => - isAdmin || u.entities.some(e => mapBy(getEditPermission(u.type), 'id').includes(e.id)) + const canEditUser = (u: User) => + isAdmin || + u.entities.some((e) => + mapBy(getEditPermission(u.type), "id").includes(e.id) + ); - const canDeleteUser = (u: User) => - isAdmin || u.entities.some(e => mapBy(getDeletePermission(u.type), 'id').includes(e.id)) + const canDeleteUser = (u: User) => + isAdmin || + u.entities.some((e) => + mapBy(getDeletePermission(u.type), "id").includes(e.id) + ); - const actionColumn = ({ row }: { row: { original: User } }) => { - const canEdit = canEditUser(row.original) - const canDelete = canDeleteUser(row.original) + const actionColumn = ({ row }: { row: { original: User } }) => { + const canEdit = canEditUser(row.original); + const canDelete = canDeleteUser(row.original); - return ( -
- {!row.original.isVerified && canEdit && ( -
verifyAccount(row.original)}> - -
- )} - {canEdit && ( -
toggleDisableAccount(row.original)}> - {row.original.status === "disabled" ? ( - - ) : ( - - )} -
- )} - {canDelete && ( -
deleteAccount(row.original)}> - -
- )} -
- ); - }; + return ( +
+ {!row.original.isVerified && canEdit && ( +
verifyAccount(row.original)} + > + +
+ )} + {canEdit && ( +
toggleDisableAccount(row.original)} + > + {row.original.status === "disabled" ? ( + + ) : ( + + )} +
+ )} + {canDelete && ( +
deleteAccount(row.original)} + > + +
+ )} +
+ ); + }; - const demographicColumns = [ - columnHelper.accessor("name", { - header: "Name", - cell: ({ row, getValue }) => ( -
- canEditUser(row.original) ? setSelectedUser(row.original) : null - }> - {getValue()} -
- ), - }), - columnHelper.accessor("demographicInformation.country", { - header: "Country", - cell: (info) => - info.getValue() - ? `${countryCodes.findOne("countryCode" as any, info.getValue())?.flag} ${countries[info.getValue() as unknown as keyof TCountries]?.name - } (+${countryCodes.findOne("countryCode" as any, info.getValue())?.countryCallingCode})` - : "N/A", - }), - columnHelper.accessor("demographicInformation.phone", { - header: "Phone", - cell: (info) => info.getValue() || "N/A", - enableSorting: true, - }), - columnHelper.accessor( - (x) => - x.type === "corporate" || x.type === "mastercorporate" ? x.demographicInformation?.position : x.demographicInformation?.employment, - { - id: "employment", - header: "Employment", - cell: (info) => (info.row.original.type === "corporate" ? info.getValue() : capitalize(info.getValue())) || "N/A", - enableSorting: true, - }, - ), - columnHelper.accessor("lastLogin", { - header: "Last Login", - cell: (info) => (!!info.getValue() ? moment(info.getValue()).format("YYYY-MM-DD HH:mm") : "N/A"), - }), - columnHelper.accessor("demographicInformation.gender", { - header: "Gender", - cell: (info) => capitalize(info.getValue()) || "N/A", - enableSorting: true, - }), - { - header: ( - setShowDemographicInformation((prev) => !prev)}> - Switch - - ), - id: "actions", - cell: actionColumn, - sortable: false - }, - ]; + const demographicColumns = [ + columnHelper.accessor("name", { + header: "Name", + cell: ({ row, getValue }) => ( +
+ canEditUser(row.original) ? setSelectedUser(row.original) : null + } + > + {getValue()} +
+ ), + }), + columnHelper.accessor("demographicInformation.country", { + header: "Country", + cell: (info) => + info.getValue() + ? `${ + countryCodes.findOne("countryCode" as any, info.getValue())?.flag + } ${ + countries[info.getValue() as unknown as keyof TCountries]?.name + } (+${ + countryCodes.findOne("countryCode" as any, info.getValue()) + ?.countryCallingCode + })` + : "N/A", + }), + columnHelper.accessor("demographicInformation.phone", { + header: "Phone", + cell: (info) => info.getValue() || "N/A", + enableSorting: true, + }), + columnHelper.accessor( + (x) => + x.type === "corporate" || x.type === "mastercorporate" + ? x.demographicInformation?.position + : x.demographicInformation?.employment, + { + id: "employment", + header: "Employment", + cell: (info) => + (info.row.original.type === "corporate" + ? info.getValue() + : capitalize(info.getValue())) || "N/A", + enableSorting: true, + } + ), + columnHelper.accessor("lastLogin", { + header: "Last Login", + cell: (info) => + !!info.getValue() + ? moment(info.getValue()).format("YYYY-MM-DD HH:mm") + : "N/A", + }), + columnHelper.accessor("demographicInformation.gender", { + header: "Gender", + cell: (info) => capitalize(info.getValue()) || "N/A", + enableSorting: true, + }), + { + header: ( + setShowDemographicInformation((prev) => !prev)} + > + Switch + + ), + id: "actions", + cell: actionColumn, + sortable: false, + }, + ]; - const defaultColumns = [ - columnHelper.accessor("name", { - header: "Name", - cell: ({ row, getValue }) => ( -
- canEditUser(row.original) ? setSelectedUser(row.original) : null - }> - {getValue()} -
- ), - }), - columnHelper.accessor("email", { - header: "E-mail", - cell: ({ row, getValue }) => ( -
(canEditUser(row.original) ? setSelectedUser(row.original) : null)}> - {getValue()} -
- ), - }), - columnHelper.accessor("type", { - header: "Type", - cell: (info) => USER_TYPE_LABELS[info.getValue()], - }), - columnHelper.accessor("studentID", { - header: "Student ID", - cell: (info) => info.getValue() || "N/A", - }), - columnHelper.accessor("entities", { - header: "Entities", - cell: ({ getValue }) => mapBy(getValue(), 'label').join(', '), - }), - columnHelper.accessor("subscriptionExpirationDate", { - header: "Expiration", - cell: (info) => ( - - {!info.getValue() ? "No expiry date" : moment(info.getValue()).format("DD/MM/YYYY")} - - ), - }), - columnHelper.accessor("isVerified", { - header: "Verified", - cell: (info) => ( -
-
- -
-
- ), - }), - { - header: ( - setShowDemographicInformation((prev) => !prev)}> - Switch - - ), - id: "actions", - cell: actionColumn, - sortable: false - }, - ]; + const defaultColumns = [ + columnHelper.accessor("name", { + header: "Name", + cell: ({ row, getValue }) => ( +
+ canEditUser(row.original) ? setSelectedUser(row.original) : null + } + > + {getValue()} +
+ ), + }), + columnHelper.accessor("email", { + header: "E-mail", + cell: ({ row, getValue }) => ( +
+ canEditUser(row.original) ? setSelectedUser(row.original) : null + } + > + {getValue()} +
+ ), + }), + columnHelper.accessor("type", { + header: "Type", + cell: (info) => USER_TYPE_LABELS[info.getValue()], + }), + columnHelper.accessor("studentID", { + header: "Student ID", + cell: (info) => info.getValue() || "N/A", + }), + columnHelper.accessor("entities", { + header: "Entities", + cell: ({ getValue }) => mapBy(getValue(), "label").join(", "), + }), + columnHelper.accessor("subscriptionExpirationDate", { + header: "Expiration", + cell: (info) => ( + + {!info.getValue() + ? "No expiry date" + : moment(info.getValue()).format("DD/MM/YYYY")} + + ), + }), + columnHelper.accessor("isVerified", { + header: "Verified", + cell: (info) => ( +
+
+ +
+
+ ), + }), + { + header: ( + setShowDemographicInformation((prev) => !prev)} + > + Switch + + ), + id: "actions", + cell: actionColumn, + sortable: false, + }, + ]; - const downloadExcel = (rows: WithLabeledEntities[]) => { - if (entitiesDownloadUsers.length === 0) return toast.error("You are not allowed to download the user list.") + const downloadExcel = (rows: WithLabeledEntities[]) => { + if (entitiesDownloadUsers.length === 0) + return toast.error("You are not allowed to download the user list."); - const allowedRows = rows.filter(r => mapBy(r.entities, 'id').some(e => mapBy(entitiesDownloadUsers, 'id').includes(e))) - const csv = exportListToExcel(allowedRows); + const allowedRows = rows.filter((r) => + mapBy(r.entities, "id").some((e) => + mapBy(entitiesDownloadUsers, "id").includes(e) + ) + ); + const csv = exportListToExcel(allowedRows); - const element = document.createElement("a"); - const file = new Blob([csv], { type: "text/csv" }); - element.href = URL.createObjectURL(file); - element.download = "users.csv"; - document.body.appendChild(element); - element.click(); - document.body.removeChild(element); - }; + const element = document.createElement("a"); + const file = new Blob([csv], { type: "text/csv" }); + element.href = URL.createObjectURL(file); + element.download = "users.csv"; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + }; - const viewStudentFilter = (x: User) => x.type === "student"; - const viewTeacherFilter = (x: User) => x.type === "teacher"; - const belongsToAdminFilter = (x: User) => x.entities.some(({ id }) => mapBy(selectedUser?.entities || [], 'id').includes(id)); + const viewStudentFilter = (x: User) => x.type === "student"; + const viewTeacherFilter = (x: User) => x.type === "teacher"; + const belongsToAdminFilter = (x: User) => + x.entities.some(({ id }) => + mapBy(selectedUser?.entities || [], "id").includes(id) + ); - const viewStudentFilterBelongsToAdmin = (x: User) => viewStudentFilter(x) && belongsToAdminFilter(x); - const viewTeacherFilterBelongsToAdmin = (x: User) => viewTeacherFilter(x) && belongsToAdminFilter(x); + const viewStudentFilterBelongsToAdmin = (x: User) => + viewStudentFilter(x) && belongsToAdminFilter(x); + const viewTeacherFilterBelongsToAdmin = (x: User) => + viewTeacherFilter(x) && belongsToAdminFilter(x); - const renderUserCard = (selectedUser: User) => { - const studentsFromAdmin = users.filter(viewStudentFilterBelongsToAdmin); - const teachersFromAdmin = users.filter(viewTeacherFilterBelongsToAdmin); - return ( -
- 0 - ? () => { - appendUserFilters({ - id: "view-students", - filter: viewStudentFilter, - }); - appendUserFilters({ - id: "belongs-to-admin", - filter: belongsToAdminFilter, - }); + const renderUserCard = (selectedUser: User) => { + const studentsFromAdmin = users.filter(viewStudentFilterBelongsToAdmin); + const teachersFromAdmin = users.filter(viewTeacherFilterBelongsToAdmin); + return ( +
+ 0 + ? () => { + appendUserFilters({ + id: "view-students", + filter: viewStudentFilter, + }); + appendUserFilters({ + id: "belongs-to-admin", + filter: belongsToAdminFilter, + }); - router.push("/users"); - } - : undefined - } - onViewTeachers={ - (selectedUser.type === "corporate" || selectedUser.type === "student") && teachersFromAdmin.length > 0 - ? () => { - appendUserFilters({ - id: "view-teachers", - filter: viewTeacherFilter, - }); - appendUserFilters({ - id: "belongs-to-admin", - filter: belongsToAdminFilter, - }); + router.push("/users"); + } + : undefined + } + onViewTeachers={ + (selectedUser.type === "corporate" || + selectedUser.type === "student") && + teachersFromAdmin.length > 0 + ? () => { + appendUserFilters({ + id: "view-teachers", + filter: viewTeacherFilter, + }); + appendUserFilters({ + id: "belongs-to-admin", + filter: belongsToAdminFilter, + }); - router.push("/users"); - } - : undefined - } - onViewCorporate={ - selectedUser.type === "teacher" || selectedUser.type === "student" - ? () => { - appendUserFilters({ - id: "view-corporate", - filter: (x: User) => x.type === "corporate", - }); - appendUserFilters({ - id: "belongs-to-admin", - filter: belongsToAdminFilter - }); + router.push("/users"); + } + : undefined + } + onViewCorporate={ + selectedUser.type === "teacher" || selectedUser.type === "student" + ? () => { + appendUserFilters({ + id: "view-corporate", + filter: (x: User) => x.type === "corporate", + }); + appendUserFilters({ + id: "belongs-to-admin", + filter: belongsToAdminFilter, + }); - router.push("/users"); - } - : undefined - } - onClose={(shouldReload) => { - setSelectedUser(undefined); - if (shouldReload) reload(); - }} - user={selectedUser} - /> -
- ); - }; + router.push("/users"); + } + : undefined + } + onClose={(shouldReload) => { + setSelectedUser(undefined); + if (shouldReload) reload(); + }} + user={selectedUser} + /> +
+ ); + }; - return ( - <> - {renderHeader && renderHeader(displayUsers.length)} -
- setSelectedUser(undefined)}> - {selectedUser && renderUserCard(selectedUser)} - - > - data={displayUsers} - columns={(!showDemographicInformation ? defaultColumns : demographicColumns) as any} - searchFields={searchFields} - onDownload={entitiesDownloadUsers.length > 0 ? downloadExcel : undefined} - /> -
- - ); + return ( + <> + {renderHeader && renderHeader(displayUsers.length)} +
+ setSelectedUser(undefined)} + > + {selectedUser && renderUserCard(selectedUser)} + + > + data={displayUsers} + columns={ + (!showDemographicInformation + ? defaultColumns + : demographicColumns) as any + } + searchFields={searchFields} + onDownload={ + entitiesDownloadUsers.length > 0 ? downloadExcel : undefined + } + isLoading={isLoading} + /> +
+ + ); } diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 6702ac02..b5cdc2c9 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -12,8 +12,6 @@ import useExamStore from "@/stores/exam"; import usePreferencesStore from "@/stores/preferencesStore"; import Layout from "../components/High/Layout"; import useEntities from "../hooks/useEntities"; -import UserProfileSkeleton from "../components/Medium/UserProfileSkeleton"; - export default function App({ Component, pageProps }: AppProps) { const [loading, setLoading] = useState(false); diff --git a/src/pages/api/tickets/assignedToUser/index.ts b/src/pages/api/tickets/assignedToUser/index.ts index 8db00dbc..c362ba2a 100644 --- a/src/pages/api/tickets/assignedToUser/index.ts +++ b/src/pages/api/tickets/assignedToUser/index.ts @@ -33,7 +33,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) { res.status(401).json({ ok: false }); return; } - const docs = await db.collection("tickets").find({ assignedTo: req.session.user.id }).toArray(); + const docs = await db.collection("tickets").find({ assignedTo: req.session.user.id, status: { $ne: "completed" } }).toArray(); res.status(200).json(docs); } diff --git a/src/pages/dashboard/admin.tsx b/src/pages/dashboard/admin.tsx index bd925183..edacde1f 100644 --- a/src/pages/dashboard/admin.tsx +++ b/src/pages/dashboard/admin.tsx @@ -80,13 +80,12 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { { _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 } ), ]); - - const assignmentsCount = await countEntitiesAssignments( - mapBy(entities, "id"), - { archived: { $ne: true } } - ); - - const stats = await getStatsByUsers(mapBy(students, "id")); + const [assignmentsCount, stats] = await Promise.all([ + countEntitiesAssignments(mapBy(entities, "id"), { + archived: { $ne: true }, + }), + getStatsByUsers(mapBy(students, "id")), + ]); return { props: serialize({ diff --git a/src/pages/dashboard/corporate.tsx b/src/pages/dashboard/corporate.tsx index e4d2c0c8..99f8562f 100644 --- a/src/pages/dashboard/corporate.tsx +++ b/src/pages/dashboard/corporate.tsx @@ -32,7 +32,6 @@ import { import { ToastContainer } from "react-toastify"; import { useAllowedEntities } from "@/hooks/useEntityPermissions"; import { isAdmin } from "@/utils/users"; -import { count } from "console"; interface Props { user: User; diff --git a/src/pages/dashboard/index.tsx b/src/pages/dashboard/index.tsx index d73c8f29..ea1076f7 100644 --- a/src/pages/dashboard/index.tsx +++ b/src/pages/dashboard/index.tsx @@ -1,4 +1,3 @@ -import { User } from "@/interfaces/user"; import { sessionOptions } from "@/lib/session"; import { redirect } from "@/utils"; import { requestUser } from "@/utils/api"; diff --git a/src/pages/dashboard/mastercorporate.tsx b/src/pages/dashboard/mastercorporate.tsx index f8350f2f..6559cce0 100644 --- a/src/pages/dashboard/mastercorporate.tsx +++ b/src/pages/dashboard/mastercorporate.tsx @@ -34,7 +34,6 @@ import { } from "react-icons/bs"; import { ToastContainer } from "react-toastify"; import { isAdmin } from "@/utils/users"; -import { count } from "console"; interface Props { user: User; diff --git a/src/pages/dashboard/student.tsx b/src/pages/dashboard/student.tsx index bb06b641..e56fcf2b 100644 --- a/src/pages/dashboard/student.tsx +++ b/src/pages/dashboard/student.tsx @@ -16,7 +16,6 @@ import useExamStore from "@/stores/exam"; import { findBy, mapBy, redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; import { getAssignmentsForStudent } from "@/utils/assignments.be"; -import { getEntities } from "@/utils/entities.be"; import { getExamsByIds } from "@/utils/exams.be"; import { getGradingSystemByEntity } from "@/utils/grading.be"; import { @@ -78,14 +77,12 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { }), ]); const assignmentsIDs = mapBy(assignments, "id"); - - const sessions = await getSessionsByUser(user.id, 10, { - ["assignment.id"]: { $in: assignmentsIDs }, - }); - - const formattedInvites = await Promise.all( - invites.map(convertInvitersToEntity) - ); + const [sessions, ...formattedInvites] = await Promise.all([ + getSessionsByUser(user.id, 10, { + ["assignment.id"]: { $in: assignmentsIDs }, + }), + ...invites.map(convertInvitersToEntity), + ]); const examIDs = uniqBy( assignments.reduce<{ module: Module; id: string; key: string }[]>( @@ -110,7 +107,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { props: serialize({ user, assignments, - stats, + stats: stats , exams, sessions, invites: formattedInvites, @@ -184,7 +181,7 @@ export default function Dashboard({ icon: ( ), - value: stats.fullExams, + value: stats?.fullExams || 0, label: "Exams", tooltip: "Number of all conducted completed exams", }, @@ -192,7 +189,7 @@ export default function Dashboard({ icon: ( ), - value: stats.uniqueModules, + value: stats?.uniqueModules || 0, label: "Modules", tooltip: "Number of all exam modules performed including Level Test", diff --git a/src/pages/generation.tsx b/src/pages/generation.tsx index 94f9de64..65903172 100644 --- a/src/pages/generation.tsx +++ b/src/pages/generation.tsx @@ -173,7 +173,7 @@ export default function Generation({ id, user, exam, examModule, permissions }: updateRoot({ currentModule })} - className="flex flex-row -2xl:flex-wrap w-full gap-4 -md:justify-center justify-between"> + className="flex flex-row flex-wrap w-full gap-4 -md:justify-center justify-between"> {[...MODULE_ARRAY].filter(m => permissions[m]).map((x) => ( {({ checked }) => ( diff --git a/src/pages/official-exam.tsx b/src/pages/official-exam.tsx index b7b50638..b9aba7c1 100644 --- a/src/pages/official-exam.tsx +++ b/src/pages/official-exam.tsx @@ -27,7 +27,6 @@ import axios from "axios"; import clsx from "clsx"; import { withIronSessionSsr } from "iron-session/next"; import { uniqBy } from "lodash"; -import moment from "moment"; import Head from "next/head"; import { useRouter } from "next/router"; import { useMemo, useState } from "react"; diff --git a/src/styles/globals.css b/src/styles/globals.css index 1db0b8a7..0cd22c97 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -3,88 +3,126 @@ @tailwind utilities; @layer utilities { - .scrollbar-hide { - -ms-overflow-style: none; - /* IE and Edge */ - scrollbar-width: none; - /* Firefox */ - } + .scrollbar-hide { + -ms-overflow-style: none; + /* IE and Edge */ + scrollbar-width: none; + /* Firefox */ + } - .scrollbar-hide::-webkit-scrollbar { - display: none; - /* Chrome, Safari and Opera */ - } + .scrollbar-hide::-webkit-scrollbar { + display: none; + /* Chrome, Safari and Opera */ + } +} + +.ellipsis { + @apply line-clamp-1 max-h-20 overflow-hidden text-ellipsis whitespace-break-spaces leading-relaxed [-webkit-box-orient:vertical] [display:-webkit-box]; +} + +.ellipsis-2 { + @apply line-clamp-2 max-h-20 overflow-hidden text-ellipsis whitespace-break-spaces leading-relaxed [-webkit-box-orient:vertical] [display:-webkit-box]; +} + +.ellipsis-3 { + @apply line-clamp-3 max-h-20 overflow-hidden text-ellipsis whitespace-break-spaces leading-relaxed [-webkit-box-orient:vertical] [display:-webkit-box]; } .training-scrollbar::-webkit-scrollbar { - @apply w-1.5; + @apply w-1.5; } .training-scrollbar::-webkit-scrollbar-track { - @apply bg-transparent; + @apply bg-transparent; } .training-scrollbar::-webkit-scrollbar-thumb { - @apply bg-gray-400 hover:bg-gray-500 rounded-full transition-colors opacity-50 hover:opacity-75; + @apply bg-gray-400 hover:bg-gray-500 rounded-full transition-colors opacity-50 hover:opacity-75; } .training-scrollbar { - scrollbar-width: thin; - scrollbar-color: rgba(156, 163, 175, 0.5) transparent; + scrollbar-width: thin; + scrollbar-color: rgba(156, 163, 175, 0.5) transparent; } :root { - --max-width: 1100px; - --border-radius: 12px; - --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", - "Fira Mono", "Droid Sans Mono", "Courier New", monospace; + --max-width: 1100px; + --border-radius: 12px; + --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", + "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", + "Fira Mono", "Droid Sans Mono", "Courier New", monospace; - --foreground-rgb: 53, 51, 56; - --background-start-rgb: 245, 245, 245; - --background-end-rgb: 245, 245, 245; + --foreground-rgb: 53, 51, 56; + --background-start-rgb: 245, 245, 245; + --background-end-rgb: 245, 245, 245; - --primary-glow: conic-gradient(from 180deg at 50% 50%, #16abff33 0deg, #0885ff33 55deg, #54d6ff33 120deg, #0071ff33 160deg, transparent 360deg); - --secondary-glow: radial-gradient(rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); + --primary-glow: conic-gradient( + from 180deg at 50% 50%, + #16abff33 0deg, + #0885ff33 55deg, + #54d6ff33 120deg, + #0071ff33 160deg, + transparent 360deg + ); + --secondary-glow: radial-gradient( + rgba(255, 255, 255, 1), + rgba(255, 255, 255, 0) + ); - --tile-start-rgb: 239, 245, 249; - --tile-end-rgb: 228, 232, 233; - --tile-border: conic-gradient(#00000080, #00000040, #00000030, #00000020, #00000010, #00000010, #00000080); + --tile-start-rgb: 239, 245, 249; + --tile-end-rgb: 228, 232, 233; + --tile-border: conic-gradient( + #00000080, + #00000040, + #00000030, + #00000020, + #00000010, + #00000010, + #00000080 + ); - --callout-rgb: 238, 240, 241; - --callout-border-rgb: 172, 175, 176; - --card-rgb: 180, 185, 188; - --card-border-rgb: 131, 134, 135; + --callout-rgb: 238, 240, 241; + --callout-border-rgb: 172, 175, 176; + --card-rgb: 180, 185, 188; + --card-border-rgb: 131, 134, 135; } * { - box-sizing: border-box; - padding: 0; - margin: 0; + box-sizing: border-box; + padding: 0; + margin: 0; } html { - min-height: 100vh !important; - height: 100%; - max-width: 100vw; - overflow-x: hidden; - overflow-y: auto; - font-family: "Open Sans", system-ui, -apple-system, "Helvetica Neue", sans-serif; + min-height: 100vh !important; + height: 100%; + max-width: 100vw; + overflow-x: hidden; + overflow-y: auto; + font-family: "Open Sans", system-ui, -apple-system, "Helvetica Neue", + sans-serif; } body { - min-height: 100vh !important; - height: 100%; - max-width: 100vw; - overflow-x: hidden; - font-family: "Open Sans", system-ui, -apple-system, "Helvetica Neue", sans-serif; + min-height: 100vh !important; + height: 100%; + max-width: 100vw; + overflow-x: hidden; + font-family: "Open Sans", system-ui, -apple-system, "Helvetica Neue", + sans-serif; } body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb)); + color: rgb(var(--foreground-rgb)); + background: linear-gradient( + to bottom, + transparent, + rgb(var(--background-end-rgb)) + ) + rgb(var(--background-start-rgb)); } a { - color: inherit; - text-decoration: none; + color: inherit; + text-decoration: none; } diff --git a/src/utils/users.ts b/src/utils/users.ts index 75ec3e7d..319be4f1 100644 --- a/src/utils/users.ts +++ b/src/utils/users.ts @@ -1,11 +1,8 @@ -import { EntityWithRoles, WithLabeledEntities } from "@/interfaces/entity"; -import { Group, User } from "@/interfaces/user"; -import { getUserCompanyName, USER_TYPE_LABELS } from "@/resources/user"; +import { WithLabeledEntities } from "@/interfaces/entity"; +import { User } from "@/interfaces/user"; +import { USER_TYPE_LABELS } from "@/resources/user"; import { capitalize } from "lodash"; import moment from "moment"; -import { mapBy } from "."; -import { findAllowedEntities } from "./permissions"; -import { getEntitiesUsers } from "./users.be"; export interface UserListRow { name: string;