Added the permission to update the privacy of an exam
This commit is contained in:
@@ -3,12 +3,12 @@ import SectionRenderer from "./SectionRenderer";
|
|||||||
import Checkbox from "../Low/Checkbox";
|
import Checkbox from "../Low/Checkbox";
|
||||||
import Input from "../Low/Input";
|
import Input from "../Low/Input";
|
||||||
import Select from "../Low/Select";
|
import Select from "../Low/Select";
|
||||||
import { capitalize } from "lodash";
|
import {capitalize} from "lodash";
|
||||||
import { Difficulty } from "@/interfaces/exam";
|
import {Difficulty} from "@/interfaces/exam";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import {useCallback, useEffect, useMemo, useState} from "react";
|
||||||
import { toast } from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import { ModuleState, SectionState } from "@/stores/examEditor/types";
|
import {ModuleState, SectionState} from "@/stores/examEditor/types";
|
||||||
import { Module } from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import useExamEditorStore from "@/stores/examEditor";
|
import useExamEditorStore from "@/stores/examEditor";
|
||||||
import WritingSettings from "./SettingsEditor/writing";
|
import WritingSettings from "./SettingsEditor/writing";
|
||||||
import ReadingSettings from "./SettingsEditor/reading";
|
import ReadingSettings from "./SettingsEditor/reading";
|
||||||
@@ -16,273 +16,243 @@ import LevelSettings from "./SettingsEditor/level";
|
|||||||
import ListeningSettings from "./SettingsEditor/listening";
|
import ListeningSettings from "./SettingsEditor/listening";
|
||||||
import SpeakingSettings from "./SettingsEditor/speaking";
|
import SpeakingSettings from "./SettingsEditor/speaking";
|
||||||
import ImportOrStartFromScratch from "./ImportExam/ImportOrFromScratch";
|
import ImportOrStartFromScratch from "./ImportExam/ImportOrFromScratch";
|
||||||
import { defaultSectionSettings } from "@/stores/examEditor/defaults";
|
import {defaultSectionSettings} from "@/stores/examEditor/defaults";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import ResetModule from "./Standalone/ResetModule";
|
import ResetModule from "./Standalone/ResetModule";
|
||||||
import ListeningInstructions from "./Standalone/ListeningInstructions";
|
import ListeningInstructions from "./Standalone/ListeningInstructions";
|
||||||
|
import {EntityWithRoles} from "@/interfaces/entity";
|
||||||
|
|
||||||
const DIFFICULTIES: Difficulty[] = ["A1", "A2", "B1", "B2", "C1", "C2"];
|
const DIFFICULTIES: Difficulty[] = ["A1", "A2", "B1", "B2", "C1", "C2"];
|
||||||
|
|
||||||
const ExamEditor: React.FC<{ levelParts?: number }> = ({ levelParts = 0 }) => {
|
const ExamEditor: React.FC<{levelParts?: number; entitiesAllowEditPrivacy: EntityWithRoles[]}> = ({
|
||||||
const { currentModule, dispatch } = useExamEditorStore();
|
levelParts = 0,
|
||||||
const {
|
entitiesAllowEditPrivacy = [],
|
||||||
sections,
|
}) => {
|
||||||
minTimer,
|
const {currentModule, dispatch} = useExamEditorStore();
|
||||||
expandedSections,
|
const {sections, minTimer, expandedSections, examLabel, isPrivate, difficulty, sectionLabels, importModule} = useExamEditorStore(
|
||||||
examLabel,
|
(state) => state.modules[currentModule],
|
||||||
isPrivate,
|
);
|
||||||
difficulty,
|
|
||||||
sectionLabels,
|
|
||||||
importModule,
|
|
||||||
} = useExamEditorStore((state) => state.modules[currentModule]);
|
|
||||||
|
|
||||||
const [numberOfLevelParts, setNumberOfLevelParts] = useState(
|
const [numberOfLevelParts, setNumberOfLevelParts] = useState(levelParts !== 0 ? levelParts : 1);
|
||||||
levelParts !== 0 ? levelParts : 1
|
const [isResetModuleOpen, setIsResetModuleOpen] = useState(false);
|
||||||
);
|
|
||||||
const [isResetModuleOpen, setIsResetModuleOpen] = useState(false);
|
|
||||||
|
|
||||||
// For exam edits
|
// For exam edits
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (levelParts !== 0) {
|
if (levelParts !== 0) {
|
||||||
setNumberOfLevelParts(levelParts);
|
setNumberOfLevelParts(levelParts);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "UPDATE_MODULE",
|
type: "UPDATE_MODULE",
|
||||||
payload: {
|
payload: {
|
||||||
updates: {
|
updates: {
|
||||||
sectionLabels: Array.from({ length: levelParts }).map((_, i) => ({
|
sectionLabels: Array.from({length: levelParts}).map((_, i) => ({
|
||||||
id: i + 1,
|
id: i + 1,
|
||||||
label: `Part ${i + 1}`,
|
label: `Part ${i + 1}`,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
module: "level",
|
module: "level",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [levelParts]);
|
}, [levelParts]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentSections = sections;
|
const currentSections = sections;
|
||||||
const currentLabels = sectionLabels;
|
const currentLabels = sectionLabels;
|
||||||
let updatedSections: SectionState[];
|
let updatedSections: SectionState[];
|
||||||
let updatedLabels: any;
|
let updatedLabels: any;
|
||||||
if (
|
if ((currentModule === "level" && currentSections.length !== currentLabels.length) || numberOfLevelParts !== currentSections.length) {
|
||||||
(currentModule === "level" &&
|
const newSections = [...currentSections];
|
||||||
currentSections.length !== currentLabels.length) ||
|
const newLabels = [...currentLabels];
|
||||||
numberOfLevelParts !== currentSections.length
|
for (let i = currentLabels.length; i < numberOfLevelParts; i++) {
|
||||||
) {
|
if (currentSections.length !== numberOfLevelParts) newSections.push(defaultSectionSettings(currentModule, i + 1));
|
||||||
const newSections = [...currentSections];
|
newLabels.push({
|
||||||
const newLabels = [...currentLabels];
|
id: i + 1,
|
||||||
for (let i = currentLabels.length; i < numberOfLevelParts; i++) {
|
label: `Part ${i + 1}`,
|
||||||
if (currentSections.length !== numberOfLevelParts)
|
});
|
||||||
newSections.push(defaultSectionSettings(currentModule, i + 1));
|
}
|
||||||
newLabels.push({
|
updatedSections = newSections;
|
||||||
id: i + 1,
|
updatedLabels = newLabels;
|
||||||
label: `Part ${i + 1}`,
|
} else if (numberOfLevelParts < currentSections.length) {
|
||||||
});
|
updatedSections = currentSections.slice(0, numberOfLevelParts);
|
||||||
}
|
updatedLabels = currentLabels.slice(0, numberOfLevelParts);
|
||||||
updatedSections = newSections;
|
} else {
|
||||||
updatedLabels = newLabels;
|
return;
|
||||||
} else if (numberOfLevelParts < currentSections.length) {
|
}
|
||||||
updatedSections = currentSections.slice(0, numberOfLevelParts);
|
|
||||||
updatedLabels = currentLabels.slice(0, numberOfLevelParts);
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedExpandedSections = expandedSections.filter((sectionId) =>
|
const updatedExpandedSections = expandedSections.filter((sectionId) => updatedSections.some((section) => section.sectionId === sectionId));
|
||||||
updatedSections.some((section) => section.sectionId === sectionId)
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "UPDATE_MODULE",
|
type: "UPDATE_MODULE",
|
||||||
payload: {
|
payload: {
|
||||||
updates: {
|
updates: {
|
||||||
sections: updatedSections,
|
sections: updatedSections,
|
||||||
sectionLabels: updatedLabels,
|
sectionLabels: updatedLabels,
|
||||||
expandedSections: updatedExpandedSections,
|
expandedSections: updatedExpandedSections,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [numberOfLevelParts]);
|
}, [numberOfLevelParts]);
|
||||||
|
|
||||||
const sectionIds = sections.map((section) => section.sectionId);
|
const sectionIds = sections.map((section) => section.sectionId);
|
||||||
|
|
||||||
const updateModule = useCallback(
|
const updateModule = useCallback(
|
||||||
(updates: Partial<ModuleState>) => {
|
(updates: Partial<ModuleState>) => {
|
||||||
dispatch({ type: "UPDATE_MODULE", payload: { updates } });
|
dispatch({type: "UPDATE_MODULE", payload: {updates}});
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleSection = (sectionId: number) => {
|
const toggleSection = (sectionId: number) => {
|
||||||
if (expandedSections.length === 1 && sectionIds.includes(sectionId)) {
|
if (expandedSections.length === 1 && sectionIds.includes(sectionId)) {
|
||||||
toast.error("Include at least one section!");
|
toast.error("Include at least one section!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch({ type: "TOGGLE_SECTION", payload: { sectionId } });
|
dispatch({type: "TOGGLE_SECTION", payload: {sectionId}});
|
||||||
};
|
};
|
||||||
|
|
||||||
const ModuleSettings: Record<Module, React.ComponentType> = {
|
const ModuleSettings: Record<Module, React.ComponentType> = {
|
||||||
reading: ReadingSettings,
|
reading: ReadingSettings,
|
||||||
writing: WritingSettings,
|
writing: WritingSettings,
|
||||||
speaking: SpeakingSettings,
|
speaking: SpeakingSettings,
|
||||||
listening: ListeningSettings,
|
listening: ListeningSettings,
|
||||||
level: LevelSettings,
|
level: LevelSettings,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Settings = ModuleSettings[currentModule];
|
const Settings = ModuleSettings[currentModule];
|
||||||
const showImport =
|
const showImport = importModule && ["reading", "listening", "level"].includes(currentModule);
|
||||||
importModule && ["reading", "listening", "level"].includes(currentModule);
|
|
||||||
|
|
||||||
const updateLevelParts = (parts: number) => {
|
const updateLevelParts = (parts: number) => {
|
||||||
setNumberOfLevelParts(parts);
|
setNumberOfLevelParts(parts);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showImport ? (
|
{showImport ? (
|
||||||
<ImportOrStartFromScratch
|
<ImportOrStartFromScratch module={currentModule} setNumberOfLevelParts={updateLevelParts} />
|
||||||
module={currentModule}
|
) : (
|
||||||
setNumberOfLevelParts={updateLevelParts}
|
<>
|
||||||
/>
|
{isResetModuleOpen && (
|
||||||
) : (
|
<ResetModule
|
||||||
<>
|
module={currentModule}
|
||||||
{isResetModuleOpen && (
|
isOpen={isResetModuleOpen}
|
||||||
<ResetModule
|
setIsOpen={setIsResetModuleOpen}
|
||||||
module={currentModule}
|
setNumberOfLevelParts={setNumberOfLevelParts}
|
||||||
isOpen={isResetModuleOpen}
|
/>
|
||||||
setIsOpen={setIsResetModuleOpen}
|
)}
|
||||||
setNumberOfLevelParts={setNumberOfLevelParts}
|
<div className="flex gap-4 w-full items-center -xl:flex-col">
|
||||||
/>
|
<div className="flex flex-row gap-3 w-full">
|
||||||
)}
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex gap-4 w-full items-center -xl:flex-col">
|
<label className="font-normal text-base text-mti-gray-dim">Timer</label>
|
||||||
<div className="flex flex-row gap-3 w-full">
|
<Input
|
||||||
<div className="flex flex-col gap-3">
|
type="number"
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
name="minTimer"
|
||||||
Timer
|
onChange={(e) =>
|
||||||
</label>
|
updateModule({
|
||||||
<Input
|
minTimer: parseInt(e) < 15 ? 15 : parseInt(e),
|
||||||
type="number"
|
})
|
||||||
name="minTimer"
|
}
|
||||||
onChange={(e) =>
|
value={minTimer}
|
||||||
updateModule({
|
className="max-w-[300px]"
|
||||||
minTimer: parseInt(e) < 15 ? 15 : parseInt(e),
|
/>
|
||||||
})
|
</div>
|
||||||
}
|
<div className="flex flex-col gap-3 flex-grow">
|
||||||
value={minTimer}
|
<label className="font-normal text-base text-mti-gray-dim">Difficulty</label>
|
||||||
className="max-w-[300px]"
|
<Select
|
||||||
/>
|
isMulti={true}
|
||||||
</div>
|
options={DIFFICULTIES.map((x) => ({
|
||||||
<div className="flex flex-col gap-3 flex-grow">
|
value: x,
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
label: capitalize(x),
|
||||||
Difficulty
|
}))}
|
||||||
</label>
|
onChange={(values) => {
|
||||||
<Select
|
const selectedDifficulties = values ? values.map((v) => v.value as Difficulty) : [];
|
||||||
isMulti={true}
|
updateModule({difficulty: selectedDifficulties});
|
||||||
options={DIFFICULTIES.map((x) => ({
|
}}
|
||||||
value: x,
|
value={
|
||||||
label: capitalize(x),
|
difficulty
|
||||||
}))}
|
? difficulty.map((d) => ({
|
||||||
onChange={(values) => {
|
value: d,
|
||||||
const selectedDifficulties = values
|
label: capitalize(d),
|
||||||
? values.map((v) => v.value as Difficulty)
|
}))
|
||||||
: [];
|
: null
|
||||||
updateModule({ difficulty: selectedDifficulties });
|
}
|
||||||
}}
|
/>
|
||||||
value={
|
</div>
|
||||||
difficulty
|
</div>
|
||||||
? difficulty.map((d) => ({
|
{sectionLabels.length != 0 && currentModule !== "level" ? (
|
||||||
value: d,
|
<div className="flex flex-col gap-3 -xl:w-full">
|
||||||
label: capitalize(d),
|
<label className="font-normal text-base text-mti-gray-dim">{sectionLabels[0].label.split(" ")[0]}</label>
|
||||||
}))
|
<div className="flex flex-row gap-8">
|
||||||
: null
|
{sectionLabels.map(({id, label}) => (
|
||||||
}
|
<span
|
||||||
/>
|
key={id}
|
||||||
</div>
|
className={clsx(
|
||||||
</div>
|
"px-6 py-4 w-48 h-[72px] flex justify-center items-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
||||||
{sectionLabels.length != 0 && currentModule !== "level" ? (
|
"transition duration-300 ease-in-out",
|
||||||
<div className="flex flex-col gap-3 -xl:w-full">
|
sectionIds.includes(id)
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
? `bg-ielts-${currentModule}/70 border-ielts-${currentModule} text-white`
|
||||||
{sectionLabels[0].label.split(" ")[0]}
|
: "bg-white border-mti-gray-platinum",
|
||||||
</label>
|
)}
|
||||||
<div className="flex flex-row gap-8">
|
onClick={() => toggleSection(id)}>
|
||||||
{sectionLabels.map(({ id, label }) => (
|
{label}
|
||||||
<span
|
</span>
|
||||||
key={id}
|
))}
|
||||||
className={clsx(
|
</div>
|
||||||
"px-6 py-4 w-48 h-[72px] flex justify-center items-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
</div>
|
||||||
"transition duration-300 ease-in-out",
|
) : (
|
||||||
sectionIds.includes(id)
|
<div className="flex flex-col gap-3 w-1/3">
|
||||||
? `bg-ielts-${currentModule}/70 border-ielts-${currentModule} text-white`
|
<label className="font-normal text-base text-mti-gray-dim">Number of Parts</label>
|
||||||
: "bg-white border-mti-gray-platinum"
|
<Input
|
||||||
)}
|
type="number"
|
||||||
onClick={() => toggleSection(id)}
|
name="Number of Parts"
|
||||||
>
|
min={1}
|
||||||
{label}
|
onChange={(v) => setNumberOfLevelParts(parseInt(v))}
|
||||||
</span>
|
value={numberOfLevelParts}
|
||||||
))}
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
) : (
|
<div className="flex flex-col gap-3 w-fit h-fit">
|
||||||
<div className="flex flex-col gap-3 w-1/3">
|
<div className="h-6" />
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
<Checkbox
|
||||||
Number of Parts
|
isChecked={isPrivate}
|
||||||
</label>
|
onChange={(checked) => updateModule({isPrivate: checked})}
|
||||||
<Input
|
disabled={entitiesAllowEditPrivacy.length === 0}>
|
||||||
type="number"
|
Privacy (Only available for Assignments)
|
||||||
name="Number of Parts"
|
</Checkbox>
|
||||||
min={1}
|
</div>
|
||||||
onChange={(v) => setNumberOfLevelParts(parseInt(v))}
|
</div>
|
||||||
value={numberOfLevelParts}
|
<div className="flex flex-row gap-3 w-full">
|
||||||
/>
|
<div className="flex flex-col gap-3 flex-grow">
|
||||||
</div>
|
<label className="font-normal text-base text-mti-gray-dim">Exam Label *</label>
|
||||||
)}
|
<Input
|
||||||
<div className="flex flex-col gap-3 w-fit h-fit">
|
type="text"
|
||||||
<div className="h-6" />
|
placeholder="Exam Label"
|
||||||
<Checkbox
|
name="label"
|
||||||
isChecked={isPrivate}
|
onChange={(text) => updateModule({examLabel: text})}
|
||||||
onChange={(checked) => updateModule({ isPrivate: checked })}
|
roundness="xl"
|
||||||
>
|
value={examLabel}
|
||||||
Privacy (Only available for Assignments)
|
required
|
||||||
</Checkbox>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{currentModule === "listening" && <ListeningInstructions />}
|
||||||
<div className="flex flex-row gap-3 w-full">
|
<Button
|
||||||
<div className="flex flex-col gap-3 flex-grow">
|
onClick={() => setIsResetModuleOpen(true)}
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
customColor={`bg-ielts-${currentModule}/70 hover:bg-ielts-${currentModule} border-ielts-${currentModule}`}
|
||||||
Exam Label *
|
className={`text-white self-end`}>
|
||||||
</label>
|
Reset Module
|
||||||
<Input
|
</Button>
|
||||||
type="text"
|
</div>
|
||||||
placeholder="Exam Label"
|
<div className="flex flex-row gap-8 -2xl:flex-col">
|
||||||
name="label"
|
<Settings />
|
||||||
onChange={(text) => updateModule({ examLabel: text })}
|
<div className="flex-grow max-w-[66%] -2xl:max-w-full">
|
||||||
roundness="xl"
|
<SectionRenderer />
|
||||||
value={examLabel}
|
</div>
|
||||||
required
|
</div>
|
||||||
/>
|
</>
|
||||||
</div>
|
)}
|
||||||
{currentModule === "listening" && <ListeningInstructions />}
|
</>
|
||||||
<Button
|
);
|
||||||
onClick={() => setIsResetModuleOpen(true)}
|
|
||||||
customColor={`bg-ielts-${currentModule}/70 hover:bg-ielts-${currentModule} border-ielts-${currentModule}`}
|
|
||||||
className={`text-white self-end`}
|
|
||||||
>
|
|
||||||
Reset Module
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row gap-8 -2xl:flex-col">
|
|
||||||
<Settings />
|
|
||||||
<div className="flex-grow max-w-[66%] -2xl:max-w-full">
|
|
||||||
<SectionRenderer />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ExamEditor;
|
export default ExamEditor;
|
||||||
|
|||||||
@@ -1,33 +1,32 @@
|
|||||||
import { useMemo, useState } from "react";
|
import {useMemo, useState} from "react";
|
||||||
import { PERMISSIONS } from "@/constants/userPermissions";
|
import {PERMISSIONS} from "@/constants/userPermissions";
|
||||||
import useExams from "@/hooks/useExams";
|
import useExams from "@/hooks/useExams";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import { Module } from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import { Exam } from "@/interfaces/exam";
|
import {Exam} from "@/interfaces/exam";
|
||||||
import { Type, User } from "@/interfaces/user";
|
import {User} from "@/interfaces/user";
|
||||||
import useExamStore from "@/stores/exam";
|
import useExamStore from "@/stores/exam";
|
||||||
import { getExamById } from "@/utils/exams";
|
import {getExamById} from "@/utils/exams";
|
||||||
import { countExercises } from "@/utils/moduleUtils";
|
import {countExercises} from "@/utils/moduleUtils";
|
||||||
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import {capitalize, uniq} from "lodash";
|
||||||
import { capitalize, uniq } from "lodash";
|
import {useRouter} from "next/router";
|
||||||
import { useRouter } from "next/router";
|
import {BsBan, BsCheck, BsCircle, BsPencil, BsTrash, BsUpload, BsX} from "react-icons/bs";
|
||||||
import { BsBan, BsBanFill, BsCheck, BsCircle, BsPencil, BsStop, BsTrash, BsUpload, BsX } from "react-icons/bs";
|
import {toast} from "react-toastify";
|
||||||
import { toast } from "react-toastify";
|
import {useListSearch} from "@/hooks/useListSearch";
|
||||||
import { useListSearch } from "@/hooks/useListSearch";
|
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import { checkAccess } from "@/utils/permissions";
|
import {checkAccess} from "@/utils/permissions";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import {EntityWithRoles} from "@/interfaces/entity";
|
||||||
import { FiEdit, FiArrowRight } from 'react-icons/fi';
|
import {BiEdit} from "react-icons/bi";
|
||||||
import { HiArrowRight } from "react-icons/hi";
|
import {findBy, mapBy} from "@/utils";
|
||||||
import { BiEdit } from "react-icons/bi";
|
import {getUserName} from "@/utils/users";
|
||||||
|
|
||||||
const searchFields = [["module"], ["id"], ["createdBy"]];
|
const searchFields = [["module"], ["id"], ["createdBy"]];
|
||||||
|
|
||||||
const CLASSES: { [key in Module]: string } = {
|
const CLASSES: {[key in Module]: string} = {
|
||||||
reading: "text-ielts-reading",
|
reading: "text-ielts-reading",
|
||||||
listening: "text-ielts-listening",
|
listening: "text-ielts-listening",
|
||||||
speaking: "text-ielts-speaking",
|
speaking: "text-ielts-speaking",
|
||||||
@@ -37,45 +36,20 @@ const CLASSES: { [key in Module]: string } = {
|
|||||||
|
|
||||||
const columnHelper = createColumnHelper<Exam>();
|
const columnHelper = createColumnHelper<Exam>();
|
||||||
|
|
||||||
const ExamOwnerSelector = ({ options, exam, onSave }: { options: User[]; exam: Exam; onSave: (owners: string[]) => void }) => {
|
export default function ExamList({user, entities}: {user: User; entities: EntityWithRoles[]}) {
|
||||||
const [owners, setOwners] = useState(exam.owners || []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full flex flex-col gap-4">
|
|
||||||
<div className="grid grid-cols-4 mt-4">
|
|
||||||
{options.map((c) => (
|
|
||||||
<Button
|
|
||||||
variant={owners.includes(c.id) ? "solid" : "outline"}
|
|
||||||
onClick={() => setOwners((prev) => (prev.includes(c.id) ? prev.filter((x) => x !== c.id) : [...prev, c.id]))}
|
|
||||||
className="max-w-[200px] w-full"
|
|
||||||
key={c.id}>
|
|
||||||
{c.name}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<Button onClick={() => onSave(owners)} className="w-full max-w-[200px] self-end">
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ExamList({ user, entities }: { user: User; entities: EntityWithRoles[]; }) {
|
|
||||||
const [selectedExam, setSelectedExam] = useState<Exam>();
|
const [selectedExam, setSelectedExam] = useState<Exam>();
|
||||||
|
|
||||||
const { exams, reload } = useExams();
|
const {exams, reload} = useExams();
|
||||||
const { users } = useUsers();
|
const {users} = useUsers();
|
||||||
const { groups } = useGroups({ admin: user?.id, userType: user?.type });
|
|
||||||
|
|
||||||
const filteredExams = useMemo(() => exams.filter((e) => {
|
const filteredExams = useMemo(
|
||||||
if (!e.private) return true
|
() =>
|
||||||
return (e.owners || []).includes(user?.id || "")
|
exams.filter((e) => {
|
||||||
}), [exams, user?.id])
|
if (!e.private) return true;
|
||||||
|
return (e.entities || []).some((ent) => mapBy(user.entities, "id").includes(ent));
|
||||||
const filteredCorporates = useMemo(() => {
|
}),
|
||||||
const participantsAndAdmins = uniq(groups.flatMap((x) => [...x.participants, x.admin])).filter((x) => x !== user?.id);
|
[exams, user?.entities],
|
||||||
return users.filter((x) => participantsAndAdmins.includes(x.id) && x.type === "corporate");
|
);
|
||||||
}, [users, groups, user]);
|
|
||||||
|
|
||||||
const parsedExams = useMemo(() => {
|
const parsedExams = useMemo(() => {
|
||||||
return filteredExams.map((exam) => {
|
return filteredExams.map((exam) => {
|
||||||
@@ -93,7 +67,7 @@ export default function ExamList({ user, entities }: { user: User; entities: Ent
|
|||||||
});
|
});
|
||||||
}, [filteredExams, users]);
|
}, [filteredExams, users]);
|
||||||
|
|
||||||
const { rows: filteredRows, renderSearch } = useListSearch<Exam>(searchFields, parsedExams);
|
const {rows: filteredRows, renderSearch} = useListSearch<Exam>(searchFields, parsedExams);
|
||||||
|
|
||||||
const dispatch = useExamStore((state) => state.dispatch);
|
const dispatch = useExamStore((state) => state.dispatch);
|
||||||
|
|
||||||
@@ -108,7 +82,7 @@ export default function ExamList({ user, entities }: { user: User; entities: Ent
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch({ type: "INIT_EXAM", payload: { exams: [exam], modules: [module] } })
|
dispatch({type: "INIT_EXAM", payload: {exams: [exam], modules: [module]}});
|
||||||
|
|
||||||
router.push("/exam");
|
router.push("/exam");
|
||||||
};
|
};
|
||||||
@@ -117,7 +91,7 @@ export default function ExamList({ user, entities }: { user: User; entities: Ent
|
|||||||
if (!confirm(`Are you sure you want to make this ${capitalize(exam.module)} exam ${exam.private ? "public" : "private"}?`)) return;
|
if (!confirm(`Are you sure you want to make this ${capitalize(exam.module)} exam ${exam.private ? "public" : "private"}?`)) return;
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.patch(`/api/exam/${exam.module}/${exam.id}`, { private: !exam.private })
|
.patch(`/api/exam/${exam.module}/${exam.id}`, {private: !exam.private})
|
||||||
.then(() => toast.success(`Updated the "${exam.id}" exam`))
|
.then(() => toast.success(`Updated the "${exam.id}" exam`))
|
||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
if (reason.response.status === 404) {
|
if (reason.response.status === 404) {
|
||||||
@@ -135,29 +109,6 @@ export default function ExamList({ user, entities }: { user: User; entities: Ent
|
|||||||
.finally(reload);
|
.finally(reload);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateExam = async (exam: Exam, body: object) => {
|
|
||||||
if (!confirm(`Are you sure you want to update this ${capitalize(exam.module)} exam?`)) return;
|
|
||||||
|
|
||||||
axios
|
|
||||||
.patch(`/api/exam/${exam.module}/${exam.id}`, body)
|
|
||||||
.then(() => toast.success(`Updated the "${exam.id}" exam`))
|
|
||||||
.catch((reason) => {
|
|
||||||
if (reason.response.status === 404) {
|
|
||||||
toast.error("Exam not found!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reason.response.status === 403) {
|
|
||||||
toast.error("You do not have permission to update this exam!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.error("Something went wrong, please try again later.");
|
|
||||||
})
|
|
||||||
.finally(reload)
|
|
||||||
.finally(() => setSelectedExam(undefined));
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteExam = async (exam: Exam) => {
|
const deleteExam = async (exam: Exam) => {
|
||||||
if (!confirm(`Are you sure you want to delete this ${capitalize(exam.module)} exam?`)) return;
|
if (!confirm(`Are you sure you want to delete this ${capitalize(exam.module)} exam?`)) return;
|
||||||
|
|
||||||
@@ -222,12 +173,12 @@ export default function ExamList({ user, entities }: { user: User; entities: Ent
|
|||||||
}),
|
}),
|
||||||
columnHelper.accessor("createdBy", {
|
columnHelper.accessor("createdBy", {
|
||||||
header: "Created By",
|
header: "Created By",
|
||||||
cell: (info) => info.getValue(),
|
cell: (info) => (!info.getValue() ? "System" : findBy(users, "id", info.getValue())?.name || "N/A"),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
header: "",
|
header: "",
|
||||||
id: "actions",
|
id: "actions",
|
||||||
cell: ({ row }: { row: { original: Exam } }) => {
|
cell: ({row}: {row: {original: Exam}}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
{(row.original.owners?.includes(user.id) || checkAccess(user, ["admin", "developer"])) && (
|
{(row.original.owners?.includes(user.id) || checkAccess(user, ["admin", "developer"])) && (
|
||||||
@@ -270,7 +221,7 @@ export default function ExamList({ user, entities }: { user: User; entities: Ent
|
|||||||
|
|
||||||
const handleExamEdit = () => {
|
const handleExamEdit = () => {
|
||||||
router.push(`/generation?id=${selectedExam!.id}&module=${selectedExam!.module}`);
|
router.push(`/generation?id=${selectedExam!.id}&module=${selectedExam!.module}`);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 w-full h-full">
|
<div className="flex flex-col gap-4 w-full h-full">
|
||||||
@@ -286,30 +237,17 @@ export default function ExamList({ user, entities }: { user: User; entities: Ent
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-50 rounded-lg p-4 mb-3">
|
<div className="bg-gray-50 rounded-lg p-4 mb-3">
|
||||||
<p className="font-medium mb-1">
|
<p className="font-medium mb-1">Exam ID: {selectedExam.id}</p>
|
||||||
Exam ID: {selectedExam.id}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-gray-500 text-sm">
|
<p className="text-gray-500 text-sm">Click 'Next' to proceed to the exam editor.</p>
|
||||||
Click 'Next' to proceed to the exam editor.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between gap-4 mt-8">
|
<div className="flex justify-between gap-4 mt-8">
|
||||||
<Button
|
<Button color="purple" variant="outline" onClick={() => setSelectedExam(undefined)} className="w-32">
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => setSelectedExam(undefined)}
|
|
||||||
className="w-32"
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button color="purple" onClick={handleExamEdit} className="w-32 text-white flex items-center justify-center gap-2">
|
||||||
color="purple"
|
|
||||||
onClick={handleExamEdit}
|
|
||||||
className="w-32 text-white flex items-center justify-center gap-2"
|
|
||||||
>
|
|
||||||
Proceed
|
Proceed
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,136 +1,132 @@
|
|||||||
import Checkbox from "@/components/Low/Checkbox";
|
import Checkbox from "@/components/Low/Checkbox";
|
||||||
import Separator from "@/components/Low/Separator";
|
import Separator from "@/components/Low/Separator";
|
||||||
import { useEntityPermission } from "@/hooks/useEntityPermissions";
|
import {useEntityPermission} from "@/hooks/useEntityPermissions";
|
||||||
import { EntityWithRoles, Role } from "@/interfaces/entity";
|
import {EntityWithRoles, Role} from "@/interfaces/entity";
|
||||||
import { User } from "@/interfaces/user";
|
import {User} from "@/interfaces/user";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import { RolePermission } from "@/resources/entityPermissions";
|
import {RolePermission} from "@/resources/entityPermissions";
|
||||||
import { findBy, mapBy, redirect, serialize } from "@/utils";
|
import {findBy, mapBy, redirect, serialize} from "@/utils";
|
||||||
import { requestUser } from "@/utils/api";
|
import {requestUser} from "@/utils/api";
|
||||||
import { getEntityWithRoles } from "@/utils/entities.be";
|
import {getEntityWithRoles} from "@/utils/entities.be";
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||||
import { doesEntityAllow } from "@/utils/permissions";
|
import {doesEntityAllow} from "@/utils/permissions";
|
||||||
import { isAdmin } from "@/utils/users";
|
import {isAdmin} from "@/utils/users";
|
||||||
import { countEntityUsers } from "@/utils/users.be";
|
import {countEntityUsers} from "@/utils/users.be";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import {withIronSessionSsr} from "iron-session/next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import { Divider } from "primereact/divider";
|
import {Divider} from "primereact/divider";
|
||||||
import { useState } from "react";
|
import {useState} from "react";
|
||||||
import {
|
import {BsCheck, BsChevronLeft, BsTag, BsTrash} from "react-icons/bs";
|
||||||
BsCheck,
|
import {toast} from "react-toastify";
|
||||||
BsChevronLeft,
|
|
||||||
BsTag,
|
|
||||||
BsTrash,
|
|
||||||
} from "react-icons/bs";
|
|
||||||
import { toast } from "react-toastify";
|
|
||||||
|
|
||||||
type PermissionLayout = { label: string, key: RolePermission }
|
type PermissionLayout = {label: string; key: RolePermission};
|
||||||
|
|
||||||
const USER_MANAGEMENT: PermissionLayout[] = [
|
const USER_MANAGEMENT: PermissionLayout[] = [
|
||||||
{ label: "View Students", key: "view_students" },
|
{label: "View Students", key: "view_students"},
|
||||||
{ label: "View Teachers", key: "view_teachers" },
|
{label: "View Teachers", key: "view_teachers"},
|
||||||
{ label: "View Corporate Accounts", key: "view_corporates" },
|
{label: "View Corporate Accounts", key: "view_corporates"},
|
||||||
{ label: "View Master Corporate Accounts", key: "view_mastercorporates" },
|
{label: "View Master Corporate Accounts", key: "view_mastercorporates"},
|
||||||
{ label: "Edit Students", key: "edit_students" },
|
{label: "Edit Students", key: "edit_students"},
|
||||||
{ label: "Edit Teachers", key: "edit_teachers" },
|
{label: "Edit Teachers", key: "edit_teachers"},
|
||||||
{ label: "Edit Corporate Accounts", key: "edit_corporates" },
|
{label: "Edit Corporate Accounts", key: "edit_corporates"},
|
||||||
{ label: "Edit Master Corporate Accounts", key: "edit_mastercorporates" },
|
{label: "Edit Master Corporate Accounts", key: "edit_mastercorporates"},
|
||||||
{ label: "Delete Students", key: "delete_students" },
|
{label: "Delete Students", key: "delete_students"},
|
||||||
{ label: "Delete Teachers", key: "delete_teachers" },
|
{label: "Delete Teachers", key: "delete_teachers"},
|
||||||
{ label: "Delete Corporate Accounts", key: "delete_corporates" },
|
{label: "Delete Corporate Accounts", key: "delete_corporates"},
|
||||||
{ label: "Delete Master Corporate Accounts", key: "delete_mastercorporates" },
|
{label: "Delete Master Corporate Accounts", key: "delete_mastercorporates"},
|
||||||
{ label: "Create a Single User", key: "create_user" },
|
{label: "Create a Single User", key: "create_user"},
|
||||||
{ label: "Create Users in Batch", key: "create_user_batch" },
|
{label: "Create Users in Batch", key: "create_user_batch"},
|
||||||
{ label: "Create a Single Code", key: "create_code" },
|
{label: "Create a Single Code", key: "create_code"},
|
||||||
{ label: "Create Codes in Batch", key: "create_code_batch" },
|
{label: "Create Codes in Batch", key: "create_code_batch"},
|
||||||
{ label: "Download User List", key: "download_user_list" },
|
{label: "Download User List", key: "download_user_list"},
|
||||||
{ label: "View Code List", key: "view_code_list" },
|
{label: "View Code List", key: "view_code_list"},
|
||||||
{ label: "Delete Code", key: "delete_code" },
|
{label: "Delete Code", key: "delete_code"},
|
||||||
]
|
];
|
||||||
|
|
||||||
const EXAM_MANAGEMENT: PermissionLayout[] = [
|
const EXAM_MANAGEMENT: PermissionLayout[] = [
|
||||||
{ label: "View Reading", key: "view_reading" },
|
{label: "View Reading", key: "view_reading"},
|
||||||
{ label: "Generate Reading", key: "generate_reading" },
|
{label: "Generate Reading", key: "generate_reading"},
|
||||||
{ label: "Delete Reading", key: "delete_reading" },
|
{label: "Delete Reading", key: "delete_reading"},
|
||||||
{ label: "View Listening", key: "view_listening" },
|
{label: "View Listening", key: "view_listening"},
|
||||||
{ label: "Generate Listening", key: "generate_listening" },
|
{label: "Generate Listening", key: "generate_listening"},
|
||||||
{ label: "Delete Listening", key: "delete_listening" },
|
{label: "Delete Listening", key: "delete_listening"},
|
||||||
{ label: "View Writing", key: "view_writing" },
|
{label: "View Writing", key: "view_writing"},
|
||||||
{ label: "Generate Writing", key: "generate_writing" },
|
{label: "Generate Writing", key: "generate_writing"},
|
||||||
{ label: "Delete Writing", key: "delete_writing" },
|
{label: "Delete Writing", key: "delete_writing"},
|
||||||
{ label: "View Speaking", key: "view_speaking" },
|
{label: "View Speaking", key: "view_speaking"},
|
||||||
{ label: "Generate Speaking", key: "generate_speaking" },
|
{label: "Generate Speaking", key: "generate_speaking"},
|
||||||
{ label: "Delete Speaking", key: "delete_speaking" },
|
{label: "Delete Speaking", key: "delete_speaking"},
|
||||||
{ label: "View Level", key: "view_level" },
|
{label: "View Level", key: "view_level"},
|
||||||
{ label: "Generate Level", key: "generate_level" },
|
{label: "Generate Level", key: "generate_level"},
|
||||||
{ label: "Delete Level", key: "delete_level" },
|
{label: "Delete Level", key: "delete_level"},
|
||||||
{ label: "View Statistics", key: "view_statistics" },
|
{label: "Set as Private/Public", key: "update_exam_privacy"},
|
||||||
]
|
{label: "View Statistics", key: "view_statistics"},
|
||||||
|
];
|
||||||
|
|
||||||
const CLASSROOM_MANAGEMENT: PermissionLayout[] = [
|
const CLASSROOM_MANAGEMENT: PermissionLayout[] = [
|
||||||
{ label: "View Classrooms", key: "view_classrooms" },
|
{label: "View Classrooms", key: "view_classrooms"},
|
||||||
{ label: "Create Classrooms", key: "create_classroom" },
|
{label: "Create Classrooms", key: "create_classroom"},
|
||||||
{ label: "Rename Classrooms", key: "rename_classrooms" },
|
{label: "Rename Classrooms", key: "rename_classrooms"},
|
||||||
{ label: "Add to Classroom", key: "add_to_classroom" },
|
{label: "Add to Classroom", key: "add_to_classroom"},
|
||||||
{ label: "Upload to Classroom", key: "upload_classroom" },
|
{label: "Upload to Classroom", key: "upload_classroom"},
|
||||||
{ label: "Remove from Classroom", key: "remove_from_classroom" },
|
{label: "Remove from Classroom", key: "remove_from_classroom"},
|
||||||
{ label: "Delete Classroom", key: "delete_classroom" },
|
{label: "Delete Classroom", key: "delete_classroom"},
|
||||||
{ label: "View Student Record", key: "view_student_record" },
|
{label: "View Student Record", key: "view_student_record"},
|
||||||
{ label: "Download Student Report", key: "download_student_record" },
|
{label: "Download Student Report", key: "download_student_record"},
|
||||||
]
|
];
|
||||||
|
|
||||||
const ENTITY_MANAGEMENT: PermissionLayout[] = [
|
const ENTITY_MANAGEMENT: PermissionLayout[] = [
|
||||||
{ label: "View Entities", key: "view_entities" },
|
{label: "View Entities", key: "view_entities"},
|
||||||
{ label: "View Entity Statistics", key: "view_entity_statistics" },
|
{label: "View Entity Statistics", key: "view_entity_statistics"},
|
||||||
{ label: "Rename Entity", key: "rename_entity" },
|
{label: "Rename Entity", key: "rename_entity"},
|
||||||
{ label: "Add to Entity", key: "add_to_entity" },
|
{label: "Add to Entity", key: "add_to_entity"},
|
||||||
{ label: "Remove from Entity", key: "remove_from_entity" },
|
{label: "Remove from Entity", key: "remove_from_entity"},
|
||||||
{ label: "Delete Entity", key: "delete_entity" },
|
{label: "Delete Entity", key: "delete_entity"},
|
||||||
{ label: "View Entity Roles", key: "view_entity_roles" },
|
{label: "View Entity Roles", key: "view_entity_roles"},
|
||||||
{ label: "Create Entity Role", key: "create_entity_role" },
|
{label: "Create Entity Role", key: "create_entity_role"},
|
||||||
{ label: "Rename Entity Role", key: "rename_entity_role" },
|
{label: "Rename Entity Role", key: "rename_entity_role"},
|
||||||
{ label: "Edit Role Permissions", key: "edit_role_permissions" },
|
{label: "Edit Role Permissions", key: "edit_role_permissions"},
|
||||||
{ label: "Assign Role to User", key: "assign_to_role" },
|
{label: "Assign Role to User", key: "assign_to_role"},
|
||||||
{ label: "Delete Entity Role", key: "delete_entity_role" },
|
{label: "Delete Entity Role", key: "delete_entity_role"},
|
||||||
{ label: "Download Statistics Report", key: "download_statistics_report" },
|
{label: "Download Statistics Report", key: "download_statistics_report"},
|
||||||
{ label: "Edit Grading System", key: "edit_grading_system" },
|
{label: "Edit Grading System", key: "edit_grading_system"},
|
||||||
{ label: "View Student Performance", key: "view_student_performance" },
|
{label: "View Student Performance", key: "view_student_performance"},
|
||||||
{ label: "Pay for Entity", key: "pay_entity" },
|
{label: "Pay for Entity", key: "pay_entity"},
|
||||||
{ label: "View Payment Record", key: "view_payment_record" }
|
{label: "View Payment Record", key: "view_payment_record"},
|
||||||
]
|
];
|
||||||
|
|
||||||
const ASSIGNMENT_MANAGEMENT: PermissionLayout[] = [
|
const ASSIGNMENT_MANAGEMENT: PermissionLayout[] = [
|
||||||
{ label: "View Assignments", key: "view_assignments" },
|
{label: "View Assignments", key: "view_assignments"},
|
||||||
{ label: "Create Assignments", key: "create_assignment" },
|
{label: "Create Assignments", key: "create_assignment"},
|
||||||
{ label: "Start Assignments", key: "start_assignment" },
|
{label: "Start Assignments", key: "start_assignment"},
|
||||||
{ label: "Edit Assignments", key: "edit_assignment" },
|
{label: "Edit Assignments", key: "edit_assignment"},
|
||||||
{ label: "Delete Assignments", key: "delete_assignment" },
|
{label: "Delete Assignments", key: "delete_assignment"},
|
||||||
{ label: "Archive Assignments", key: "archive_assignment" },
|
{label: "Archive Assignments", key: "archive_assignment"},
|
||||||
]
|
];
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({req, res, params}) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res);
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login");
|
||||||
|
|
||||||
if (shouldRedirectHome(user)) return redirect("/")
|
if (shouldRedirectHome(user)) return redirect("/");
|
||||||
|
|
||||||
const { id, role } = params as { id: string, role: string };
|
const {id, role} = params as {id: string; role: string};
|
||||||
|
|
||||||
if (!mapBy(user.entities, 'id').includes(id) && !["admin", "developer"].includes(user.type)) return redirect("/entities")
|
if (!mapBy(user.entities, "id").includes(id) && !["admin", "developer"].includes(user.type)) return redirect("/entities");
|
||||||
|
|
||||||
const entity = await getEntityWithRoles(id);
|
const entity = await getEntityWithRoles(id);
|
||||||
if (!entity) return redirect("/entities")
|
if (!entity) return redirect("/entities");
|
||||||
|
|
||||||
const entityRole = findBy(entity.roles, 'id', role)
|
const entityRole = findBy(entity.roles, "id", role);
|
||||||
if (!entityRole) return redirect(`/entities/${id}/roles`)
|
if (!entityRole) return redirect(`/entities/${id}/roles`);
|
||||||
|
|
||||||
if (!doesEntityAllow(user, entity, "view_entity_roles")) return redirect(`/entities/${id}`)
|
if (!doesEntityAllow(user, entity, "view_entity_roles")) return redirect(`/entities/${id}`);
|
||||||
const disableEdit = !isAdmin(user) && findBy(user.entities, 'id', entity.id)?.role === entityRole.id
|
const disableEdit = !isAdmin(user) && findBy(user.entities, "id", entity.id)?.role === entityRole.id;
|
||||||
|
|
||||||
const userCount = await countEntityUsers(id, { "entities.role": role });
|
const userCount = await countEntityUsers(id, {"entities.role": role});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({
|
props: serialize({
|
||||||
@@ -138,7 +134,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }
|
|||||||
entity,
|
entity,
|
||||||
role: entityRole,
|
role: entityRole,
|
||||||
userCount,
|
userCount,
|
||||||
disableEdit
|
disableEdit,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
@@ -148,19 +144,18 @@ interface Props {
|
|||||||
entity: EntityWithRoles;
|
entity: EntityWithRoles;
|
||||||
role: Role;
|
role: Role;
|
||||||
userCount: number;
|
userCount: number;
|
||||||
disableEdit?: boolean
|
disableEdit?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EntityRole({ user, entity, role, userCount, disableEdit }: Props) {
|
export default function EntityRole({user, entity, role, userCount, disableEdit}: Props) {
|
||||||
const [permissions, setPermissions] = useState(role.permissions)
|
const [permissions, setPermissions] = useState(role.permissions);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const canEditPermissions = useEntityPermission(user, entity, "edit_role_permissions")
|
const canEditPermissions = useEntityPermission(user, entity, "edit_role_permissions");
|
||||||
const canRenameRole = useEntityPermission(user, entity, "rename_entity_role")
|
const canRenameRole = useEntityPermission(user, entity, "rename_entity_role");
|
||||||
const canDeleteRole = useEntityPermission(user, entity, "delete_entity_role")
|
const canDeleteRole = useEntityPermission(user, entity, "delete_entity_role");
|
||||||
|
|
||||||
|
|
||||||
const renameRole = () => {
|
const renameRole = () => {
|
||||||
if (!canRenameRole || disableEdit) return;
|
if (!canRenameRole || disableEdit) return;
|
||||||
@@ -170,7 +165,7 @@ export default function EntityRole({ user, entity, role, userCount, disableEdit
|
|||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
axios
|
axios
|
||||||
.patch(`/api/roles/${role.id}`, { label })
|
.patch(`/api/roles/${role.id}`, {label})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("The role has been updated successfully!");
|
toast.success("The role has been updated successfully!");
|
||||||
router.replace(router.asPath);
|
router.replace(router.asPath);
|
||||||
@@ -202,12 +197,12 @@ export default function EntityRole({ user, entity, role, userCount, disableEdit
|
|||||||
};
|
};
|
||||||
|
|
||||||
const editPermissions = () => {
|
const editPermissions = () => {
|
||||||
if (!canEditPermissions || disableEdit) return
|
if (!canEditPermissions || disableEdit) return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.patch(`/api/roles/${role.id}`, { permissions })
|
.patch(`/api/roles/${role.id}`, {permissions})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("This role has been successfully updated!");
|
toast.success("This role has been successfully updated!");
|
||||||
router.replace(router.asPath);
|
router.replace(router.asPath);
|
||||||
@@ -217,21 +212,23 @@ export default function EntityRole({ user, entity, role, userCount, disableEdit
|
|||||||
toast.error("Something went wrong!");
|
toast.error("Something went wrong!");
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
}
|
};
|
||||||
|
|
||||||
const enableCheckbox = (permission: RolePermission) => {
|
const enableCheckbox = (permission: RolePermission) => {
|
||||||
if (!canEditPermissions || disableEdit) return false
|
if (!canEditPermissions || disableEdit) return false;
|
||||||
return doesEntityAllow(user, entity, permission)
|
return doesEntityAllow(user, entity, permission);
|
||||||
}
|
};
|
||||||
|
|
||||||
const togglePermissions = (p: RolePermission) => setPermissions(prev => prev.includes(p) ? prev.filter(x => x !== p) : [...prev, p])
|
const togglePermissions = (p: RolePermission) => setPermissions((prev) => (prev.includes(p) ? prev.filter((x) => x !== p) : [...prev, p]));
|
||||||
const toggleMultiplePermissions = (p: RolePermission[]) =>
|
const toggleMultiplePermissions = (p: RolePermission[]) =>
|
||||||
setPermissions(prev => [...prev.filter(x => !p.includes(x)), ...(p.every(x => prev.includes(x)) ? [] : p)])
|
setPermissions((prev) => [...prev.filter((x) => !p.includes(x)), ...(p.every((x) => prev.includes(x)) ? [] : p)]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{role.label} | {entity.label} | EnCoach</title>
|
<title>
|
||||||
|
{role.label} | {entity.label} | EnCoach
|
||||||
|
</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
@@ -249,7 +246,9 @@ export default function EntityRole({ user, entity, role, userCount, disableEdit
|
|||||||
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
||||||
<BsChevronLeft />
|
<BsChevronLeft />
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="font-bold text-2xl">{role.label} Role ({userCount} users)</h2>
|
<h2 className="font-bold text-2xl">
|
||||||
|
{role.label} Role ({userCount} users)
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between w-full">
|
<div className="flex items-center justify-between w-full">
|
||||||
@@ -286,16 +285,19 @@ export default function EntityRole({ user, entity, role, userCount, disableEdit
|
|||||||
<b>User Management</b>
|
<b>User Management</b>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
disabled={!canEditPermissions || disableEdit}
|
disabled={!canEditPermissions || disableEdit}
|
||||||
isChecked={mapBy(USER_MANAGEMENT, 'key').every(k => permissions.includes(k))}
|
isChecked={mapBy(USER_MANAGEMENT, "key").every((k) => permissions.includes(k))}
|
||||||
onChange={() => toggleMultiplePermissions(mapBy(USER_MANAGEMENT, 'key').filter(enableCheckbox))}
|
onChange={() => toggleMultiplePermissions(mapBy(USER_MANAGEMENT, "key").filter(enableCheckbox))}>
|
||||||
>
|
|
||||||
Select all
|
Select all
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{USER_MANAGEMENT.map(({ label, key }) => (
|
{USER_MANAGEMENT.map(({label, key}) => (
|
||||||
<Checkbox disabled={!enableCheckbox(key)} key={key} isChecked={permissions.includes(key)} onChange={() => togglePermissions(key)}>
|
<Checkbox
|
||||||
|
disabled={!enableCheckbox(key)}
|
||||||
|
key={key}
|
||||||
|
isChecked={permissions.includes(key)}
|
||||||
|
onChange={() => togglePermissions(key)}>
|
||||||
{label}
|
{label}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
))}
|
))}
|
||||||
@@ -307,16 +309,19 @@ export default function EntityRole({ user, entity, role, userCount, disableEdit
|
|||||||
<b>Exam Management</b>
|
<b>Exam Management</b>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
disabled={!canEditPermissions || disableEdit}
|
disabled={!canEditPermissions || disableEdit}
|
||||||
isChecked={mapBy(EXAM_MANAGEMENT, 'key').every(k => permissions.includes(k))}
|
isChecked={mapBy(EXAM_MANAGEMENT, "key").every((k) => permissions.includes(k))}
|
||||||
onChange={() => toggleMultiplePermissions(mapBy(EXAM_MANAGEMENT, 'key').filter(enableCheckbox))}
|
onChange={() => toggleMultiplePermissions(mapBy(EXAM_MANAGEMENT, "key").filter(enableCheckbox))}>
|
||||||
>
|
|
||||||
Select all
|
Select all
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
{EXAM_MANAGEMENT.map(({ label, key }) => (
|
{EXAM_MANAGEMENT.map(({label, key}) => (
|
||||||
<Checkbox disabled={!enableCheckbox(key)} key={key} isChecked={permissions.includes(key)} onChange={() => togglePermissions(key)}>
|
<Checkbox
|
||||||
|
disabled={!enableCheckbox(key)}
|
||||||
|
key={key}
|
||||||
|
isChecked={permissions.includes(key)}
|
||||||
|
onChange={() => togglePermissions(key)}>
|
||||||
{label}
|
{label}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
))}
|
))}
|
||||||
@@ -328,16 +333,19 @@ export default function EntityRole({ user, entity, role, userCount, disableEdit
|
|||||||
<b>Clasroom Management</b>
|
<b>Clasroom Management</b>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
disabled={!canEditPermissions || disableEdit}
|
disabled={!canEditPermissions || disableEdit}
|
||||||
isChecked={mapBy(CLASSROOM_MANAGEMENT, 'key').every(k => permissions.includes(k))}
|
isChecked={mapBy(CLASSROOM_MANAGEMENT, "key").every((k) => permissions.includes(k))}
|
||||||
onChange={() => toggleMultiplePermissions(mapBy(CLASSROOM_MANAGEMENT, 'key').filter(enableCheckbox))}
|
onChange={() => toggleMultiplePermissions(mapBy(CLASSROOM_MANAGEMENT, "key").filter(enableCheckbox))}>
|
||||||
>
|
|
||||||
Select all
|
Select all
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{CLASSROOM_MANAGEMENT.map(({ label, key }) => (
|
{CLASSROOM_MANAGEMENT.map(({label, key}) => (
|
||||||
<Checkbox disabled={!enableCheckbox(key)} key={key} isChecked={permissions.includes(key)} onChange={() => togglePermissions(key)}>
|
<Checkbox
|
||||||
|
disabled={!enableCheckbox(key)}
|
||||||
|
key={key}
|
||||||
|
isChecked={permissions.includes(key)}
|
||||||
|
onChange={() => togglePermissions(key)}>
|
||||||
{label}
|
{label}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
))}
|
))}
|
||||||
@@ -349,16 +357,19 @@ export default function EntityRole({ user, entity, role, userCount, disableEdit
|
|||||||
<b>Entity Management</b>
|
<b>Entity Management</b>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
disabled={!canEditPermissions || disableEdit}
|
disabled={!canEditPermissions || disableEdit}
|
||||||
isChecked={mapBy(ENTITY_MANAGEMENT, 'key').every(k => permissions.includes(k))}
|
isChecked={mapBy(ENTITY_MANAGEMENT, "key").every((k) => permissions.includes(k))}
|
||||||
onChange={() => toggleMultiplePermissions(mapBy(ENTITY_MANAGEMENT, 'key').filter(enableCheckbox))}
|
onChange={() => toggleMultiplePermissions(mapBy(ENTITY_MANAGEMENT, "key").filter(enableCheckbox))}>
|
||||||
>
|
|
||||||
Select all
|
Select all
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{ENTITY_MANAGEMENT.map(({ label, key }) => (
|
{ENTITY_MANAGEMENT.map(({label, key}) => (
|
||||||
<Checkbox disabled={!enableCheckbox(key)} key={key} isChecked={permissions.includes(key)} onChange={() => togglePermissions(key)}>
|
<Checkbox
|
||||||
|
disabled={!enableCheckbox(key)}
|
||||||
|
key={key}
|
||||||
|
isChecked={permissions.includes(key)}
|
||||||
|
onChange={() => togglePermissions(key)}>
|
||||||
{label}
|
{label}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
))}
|
))}
|
||||||
@@ -370,16 +381,19 @@ export default function EntityRole({ user, entity, role, userCount, disableEdit
|
|||||||
<b>Assignment Management</b>
|
<b>Assignment Management</b>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
disabled={!canEditPermissions || disableEdit}
|
disabled={!canEditPermissions || disableEdit}
|
||||||
isChecked={mapBy(ASSIGNMENT_MANAGEMENT, 'key').every(k => permissions.includes(k))}
|
isChecked={mapBy(ASSIGNMENT_MANAGEMENT, "key").every((k) => permissions.includes(k))}
|
||||||
onChange={() => toggleMultiplePermissions(mapBy(ASSIGNMENT_MANAGEMENT, 'key').filter(enableCheckbox))}
|
onChange={() => toggleMultiplePermissions(mapBy(ASSIGNMENT_MANAGEMENT, "key").filter(enableCheckbox))}>
|
||||||
>
|
|
||||||
Select all
|
Select all
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{ASSIGNMENT_MANAGEMENT.map(({ label, key }) => (
|
{ASSIGNMENT_MANAGEMENT.map(({label, key}) => (
|
||||||
<Checkbox disabled={!enableCheckbox(key)} key={key} isChecked={permissions.includes(key)} onChange={() => togglePermissions(key)}>
|
<Checkbox
|
||||||
|
disabled={!enableCheckbox(key)}
|
||||||
|
key={key}
|
||||||
|
isChecked={permissions.includes(key)}
|
||||||
|
onChange={() => togglePermissions(key)}>
|
||||||
{label}
|
{label}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,39 +1,40 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import {withIronSessionSsr} from "iron-session/next";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import { ToastContainer } from "react-toastify";
|
import {ToastContainer} from "react-toastify";
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||||
import { Radio, RadioGroup } from "@headlessui/react";
|
import {Radio, RadioGroup} from "@headlessui/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { MODULE_ARRAY } from "@/utils/moduleUtils";
|
import {MODULE_ARRAY} from "@/utils/moduleUtils";
|
||||||
import { capitalize } from "lodash";
|
import {capitalize} from "lodash";
|
||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
import { findAllowedEntities } from "@/utils/permissions";
|
import {findAllowedEntities} from "@/utils/permissions";
|
||||||
import { User } from "@/interfaces/user";
|
import {User} from "@/interfaces/user";
|
||||||
import useExamEditorStore from "@/stores/examEditor";
|
import useExamEditorStore from "@/stores/examEditor";
|
||||||
import ExamEditorStore from "@/stores/examEditor/types";
|
import ExamEditorStore from "@/stores/examEditor/types";
|
||||||
import ExamEditor from "@/components/ExamEditor";
|
import ExamEditor from "@/components/ExamEditor";
|
||||||
import { mapBy, redirect, serialize } from "@/utils";
|
import {mapBy, redirect, serialize} from "@/utils";
|
||||||
import { requestUser } from "@/utils/api";
|
import {requestUser} from "@/utils/api";
|
||||||
import { Module } from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import { getExam, } from "@/utils/exams.be";
|
import {getExam} from "@/utils/exams.be";
|
||||||
import { Exam, Exercise, InteractiveSpeakingExercise, ListeningPart, SpeakingExercise } from "@/interfaces/exam";
|
import {Exam, Exercise, InteractiveSpeakingExercise, ListeningPart, SpeakingExercise} from "@/interfaces/exam";
|
||||||
import { useEffect, useState } from "react";
|
import {useEffect, useState} from "react";
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
import {getEntitiesWithRoles} from "@/utils/entities.be";
|
||||||
import { isAdmin } from "@/utils/users";
|
import {isAdmin} from "@/utils/users";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import {EntityWithRoles} from "@/interfaces/entity";
|
||||||
|
|
||||||
type Permission = { [key in Module]: boolean }
|
type Permission = {[key in Module]: boolean};
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res, query }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({req, res, query}) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res);
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login");
|
||||||
|
|
||||||
if (shouldRedirectHome(user)) return redirect("/")
|
if (shouldRedirectHome(user)) return redirect("/");
|
||||||
|
|
||||||
const entityIDs = mapBy(user.entities, 'id')
|
const entityIDs = mapBy(user.entities, "id");
|
||||||
const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDs)
|
const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDs);
|
||||||
|
|
||||||
const permissions: Permission = {
|
const permissions: Permission = {
|
||||||
reading: findAllowedEntities(user, entities, `generate_reading`).length > 0,
|
reading: findAllowedEntities(user, entities, `generate_reading`).length > 0,
|
||||||
@@ -41,29 +42,46 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, query })
|
|||||||
writing: findAllowedEntities(user, entities, `generate_writing`).length > 0,
|
writing: findAllowedEntities(user, entities, `generate_writing`).length > 0,
|
||||||
speaking: findAllowedEntities(user, entities, `generate_speaking`).length > 0,
|
speaking: findAllowedEntities(user, entities, `generate_speaking`).length > 0,
|
||||||
level: findAllowedEntities(user, entities, `generate_level`).length > 0,
|
level: findAllowedEntities(user, entities, `generate_level`).length > 0,
|
||||||
}
|
};
|
||||||
|
|
||||||
if (Object.keys(permissions).every(p => !permissions[p as Module])) return redirect("/")
|
const entitiesAllowEditPrivacy = findAllowedEntities(user, entities, "update_exam_privacy");
|
||||||
|
console.log(entitiesAllowEditPrivacy);
|
||||||
|
|
||||||
const { id, module: examModule } = query as { id?: string, module?: Module }
|
if (Object.keys(permissions).every((p) => !permissions[p as Module])) return redirect("/");
|
||||||
if (!id || !examModule) return { props: serialize({ user, permissions }) };
|
|
||||||
|
const {id, module: examModule} = query as {id?: string; module?: Module};
|
||||||
|
if (!id || !examModule) return {props: serialize({user, permissions})};
|
||||||
|
|
||||||
//if (!permissions[module]) return redirect("/generation")
|
//if (!permissions[module]) return redirect("/generation")
|
||||||
|
|
||||||
const exam = await getExam(examModule, id)
|
const exam = await getExam(examModule, id);
|
||||||
if (!exam) return redirect("/generation")
|
if (!exam) return redirect("/generation");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({ id, user, exam, examModule, permissions }),
|
props: serialize({id, user, exam, examModule, permissions, entitiesAllowEditPrivacy}),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
export default function Generation({ id, user, exam, examModule, permissions }: { id: string, user: User; exam?: Exam, examModule?: Module, permissions: Permission }) {
|
export default function Generation({
|
||||||
const { title, currentModule, modules, dispatch } = useExamEditorStore();
|
id,
|
||||||
|
user,
|
||||||
|
exam,
|
||||||
|
examModule,
|
||||||
|
permissions,
|
||||||
|
entitiesAllowEditPrivacy,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
user: User;
|
||||||
|
exam?: Exam;
|
||||||
|
examModule?: Module;
|
||||||
|
permissions: Permission;
|
||||||
|
entitiesAllowEditPrivacy: EntityWithRoles[];
|
||||||
|
}) {
|
||||||
|
const {title, currentModule, modules, dispatch} = useExamEditorStore();
|
||||||
const [examLevelParts, setExamLevelParts] = useState<number | undefined>(undefined);
|
const [examLevelParts, setExamLevelParts] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
const updateRoot = (updates: Partial<ExamEditorStore>) => {
|
const updateRoot = (updates: Partial<ExamEditorStore>) => {
|
||||||
dispatch({ type: 'UPDATE_ROOT', payload: { updates } });
|
dispatch({type: "UPDATE_ROOT", payload: {updates}});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -71,16 +89,16 @@ export default function Generation({ id, user, exam, examModule, permissions }:
|
|||||||
if (examModule === "level" && exam.module === "level") {
|
if (examModule === "level" && exam.module === "level") {
|
||||||
setExamLevelParts(exam.parts.length);
|
setExamLevelParts(exam.parts.length);
|
||||||
}
|
}
|
||||||
updateRoot({currentModule: examModule})
|
updateRoot({currentModule: examModule});
|
||||||
dispatch({ type: "INIT_EXAM_EDIT", payload: { exam, id, examModule } })
|
dispatch({type: "INIT_EXAM_EDIT", payload: {exam, id, examModule}});
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [id, exam, module])
|
}, [id, exam, module]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchAvatars = async () => {
|
const fetchAvatars = async () => {
|
||||||
const response = await axios.get("/api/exam/avatars");
|
const response = await axios.get("/api/exam/avatars");
|
||||||
updateRoot({ speakingAvatars: response.data });
|
updateRoot({speakingAvatars: response.data});
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchAvatars();
|
fetchAvatars();
|
||||||
@@ -96,48 +114,61 @@ export default function Generation({ id, user, exam, examModule, permissions }:
|
|||||||
URL.revokeObjectURL(state.writing.academic_url);
|
URL.revokeObjectURL(state.writing.academic_url);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.listening.sections.forEach(section => {
|
state.listening.sections.forEach((section) => {
|
||||||
const listeningPart = section.state as ListeningPart;
|
const listeningPart = section.state as ListeningPart;
|
||||||
if (listeningPart.audio?.source) {
|
if (listeningPart.audio?.source) {
|
||||||
URL.revokeObjectURL(listeningPart.audio.source);
|
URL.revokeObjectURL(listeningPart.audio.source);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||||
sectionId: section.sectionId, module: "listening", field: "state", value: { ...listeningPart, audio: undefined }
|
payload: {
|
||||||
}
|
sectionId: section.sectionId,
|
||||||
})
|
module: "listening",
|
||||||
|
field: "state",
|
||||||
|
value: {...listeningPart, audio: undefined},
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (state.listening.instructionsState.customInstructionsURL.startsWith('blob:')) {
|
if (state.listening.instructionsState.customInstructionsURL.startsWith("blob:")) {
|
||||||
URL.revokeObjectURL(state.listening.instructionsState.customInstructionsURL);
|
URL.revokeObjectURL(state.listening.instructionsState.customInstructionsURL);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.speaking.sections.forEach(section => {
|
state.speaking.sections.forEach((section) => {
|
||||||
const sectionState = section.state as Exercise;
|
const sectionState = section.state as Exercise;
|
||||||
if (sectionState.type === 'speaking') {
|
if (sectionState.type === "speaking") {
|
||||||
const speakingExercise = sectionState as SpeakingExercise;
|
const speakingExercise = sectionState as SpeakingExercise;
|
||||||
URL.revokeObjectURL(speakingExercise.video_url);
|
URL.revokeObjectURL(speakingExercise.video_url);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||||
sectionId: section.sectionId, module: "listening", field: "state", value: { ...speakingExercise, video_url: undefined }
|
payload: {
|
||||||
}
|
sectionId: section.sectionId,
|
||||||
})
|
module: "listening",
|
||||||
|
field: "state",
|
||||||
|
value: {...speakingExercise, video_url: undefined},
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (sectionState.type === 'interactiveSpeaking') {
|
if (sectionState.type === "interactiveSpeaking") {
|
||||||
const interactiveSpeaking = sectionState as InteractiveSpeakingExercise;
|
const interactiveSpeaking = sectionState as InteractiveSpeakingExercise;
|
||||||
interactiveSpeaking.prompts.forEach(prompt => {
|
interactiveSpeaking.prompts.forEach((prompt) => {
|
||||||
URL.revokeObjectURL(prompt.video_url);
|
URL.revokeObjectURL(prompt.video_url);
|
||||||
});
|
});
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||||
sectionId: section.sectionId, module: "listening", field: "state", value: {
|
payload: {
|
||||||
...interactiveSpeaking, prompts: interactiveSpeaking.prompts.map((p) => ({ ...p, video_url: undefined }))
|
sectionId: section.sectionId,
|
||||||
}
|
module: "listening",
|
||||||
}
|
field: "state",
|
||||||
})
|
value: {
|
||||||
|
...interactiveSpeaking,
|
||||||
|
prompts: interactiveSpeaking.prompts.map((p) => ({...p, video_url: undefined})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
dispatch({ type: 'FULL_RESET' });
|
dispatch({type: "FULL_RESET"});
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
@@ -163,7 +194,7 @@ export default function Generation({ id, user, exam, examModule, permissions }:
|
|||||||
placeholder="Insert a title here"
|
placeholder="Insert a title here"
|
||||||
name="title"
|
name="title"
|
||||||
label="Title"
|
label="Title"
|
||||||
onChange={(title) => updateRoot({ title })}
|
onChange={(title) => updateRoot({title})}
|
||||||
roundness="xl"
|
roundness="xl"
|
||||||
value={title}
|
value={title}
|
||||||
defaultValue={title}
|
defaultValue={title}
|
||||||
@@ -172,44 +203,46 @@ export default function Generation({ id, user, exam, examModule, permissions }:
|
|||||||
<label className="font-normal text-base text-mti-gray-dim">Module</label>
|
<label className="font-normal text-base text-mti-gray-dim">Module</label>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={currentModule}
|
value={currentModule}
|
||||||
onChange={(currentModule) => updateRoot({ currentModule })}
|
onChange={(currentModule) => updateRoot({currentModule})}
|
||||||
className="flex flex-row 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) => (
|
{[...MODULE_ARRAY]
|
||||||
<Radio value={x} key={x}>
|
.filter((m) => permissions[m])
|
||||||
{({ checked }) => (
|
.map((x) => (
|
||||||
<span
|
<Radio value={x} key={x}>
|
||||||
className={clsx(
|
{({checked}) => (
|
||||||
"px-6 py-4 w-64 h-[72px] flex justify-center items-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
<span
|
||||||
"transition duration-300 ease-in-out",
|
className={clsx(
|
||||||
x === "reading" &&
|
"px-6 py-4 w-64 h-[72px] flex justify-center items-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
||||||
(!checked
|
"transition duration-300 ease-in-out",
|
||||||
? "bg-white border-mti-gray-platinum"
|
x === "reading" &&
|
||||||
: "bg-ielts-reading/70 border-ielts-reading text-white"),
|
(!checked
|
||||||
x === "listening" &&
|
? "bg-white border-mti-gray-platinum"
|
||||||
(!checked
|
: "bg-ielts-reading/70 border-ielts-reading text-white"),
|
||||||
? "bg-white border-mti-gray-platinum"
|
x === "listening" &&
|
||||||
: "bg-ielts-listening/70 border-ielts-listening text-white"),
|
(!checked
|
||||||
x === "writing" &&
|
? "bg-white border-mti-gray-platinum"
|
||||||
(!checked
|
: "bg-ielts-listening/70 border-ielts-listening text-white"),
|
||||||
? "bg-white border-mti-gray-platinum"
|
x === "writing" &&
|
||||||
: "bg-ielts-writing/70 border-ielts-writing text-white"),
|
(!checked
|
||||||
x === "speaking" &&
|
? "bg-white border-mti-gray-platinum"
|
||||||
(!checked
|
: "bg-ielts-writing/70 border-ielts-writing text-white"),
|
||||||
? "bg-white border-mti-gray-platinum"
|
x === "speaking" &&
|
||||||
: "bg-ielts-speaking/70 border-ielts-speaking text-white"),
|
(!checked
|
||||||
x === "level" &&
|
? "bg-white border-mti-gray-platinum"
|
||||||
(!checked
|
: "bg-ielts-speaking/70 border-ielts-speaking text-white"),
|
||||||
? "bg-white border-mti-gray-platinum"
|
x === "level" &&
|
||||||
: "bg-ielts-level/70 border-ielts-level text-white"),
|
(!checked
|
||||||
)}>
|
? "bg-white border-mti-gray-platinum"
|
||||||
{capitalize(x)}
|
: "bg-ielts-level/70 border-ielts-level text-white"),
|
||||||
</span>
|
)}>
|
||||||
)}
|
{capitalize(x)}
|
||||||
</Radio>
|
</span>
|
||||||
))}
|
)}
|
||||||
|
</Radio>
|
||||||
|
))}
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
<ExamEditor levelParts={examLevelParts} />
|
<ExamEditor levelParts={examLevelParts} entitiesAllowEditPrivacy={entitiesAllowEditPrivacy} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,72 +1,73 @@
|
|||||||
export type RolePermission =
|
export type RolePermission =
|
||||||
"view_students" |
|
| "view_students"
|
||||||
"view_teachers" |
|
| "view_teachers"
|
||||||
"view_corporates" |
|
| "view_corporates"
|
||||||
"view_mastercorporates" |
|
| "view_mastercorporates"
|
||||||
"edit_students" |
|
| "edit_students"
|
||||||
"edit_teachers" |
|
| "edit_teachers"
|
||||||
"edit_corporates" |
|
| "edit_corporates"
|
||||||
"edit_mastercorporates" |
|
| "edit_mastercorporates"
|
||||||
"delete_students" |
|
| "delete_students"
|
||||||
"delete_teachers" |
|
| "delete_teachers"
|
||||||
"delete_corporates" |
|
| "delete_corporates"
|
||||||
"delete_mastercorporates" |
|
| "delete_mastercorporates"
|
||||||
"generate_reading" |
|
| "generate_reading"
|
||||||
"view_reading" |
|
| "view_reading"
|
||||||
"delete_reading" |
|
| "delete_reading"
|
||||||
"generate_listening" |
|
| "generate_listening"
|
||||||
"view_listening" |
|
| "view_listening"
|
||||||
"delete_listening" |
|
| "delete_listening"
|
||||||
"generate_writing" |
|
| "generate_writing"
|
||||||
"view_writing" |
|
| "view_writing"
|
||||||
"delete_writing" |
|
| "delete_writing"
|
||||||
"generate_speaking" |
|
| "generate_speaking"
|
||||||
"view_speaking" |
|
| "view_speaking"
|
||||||
"delete_speaking" |
|
| "delete_speaking"
|
||||||
"generate_level" |
|
| "generate_level"
|
||||||
"view_level" |
|
| "view_level"
|
||||||
"delete_level" |
|
| "delete_level"
|
||||||
"view_classrooms" |
|
| "view_classrooms"
|
||||||
"create_classroom" |
|
| "create_classroom"
|
||||||
"rename_classrooms" |
|
| "rename_classrooms"
|
||||||
"add_to_classroom" |
|
| "add_to_classroom"
|
||||||
"remove_from_classroom" |
|
| "remove_from_classroom"
|
||||||
"delete_classroom" |
|
| "delete_classroom"
|
||||||
"view_entities" |
|
| "view_entities"
|
||||||
"rename_entity" |
|
| "rename_entity"
|
||||||
"add_to_entity" |
|
| "add_to_entity"
|
||||||
"remove_from_entity" |
|
| "remove_from_entity"
|
||||||
"delete_entity" |
|
| "delete_entity"
|
||||||
"view_entity_roles" |
|
| "view_entity_roles"
|
||||||
"create_entity_role" |
|
| "create_entity_role"
|
||||||
"rename_entity_role" |
|
| "rename_entity_role"
|
||||||
"edit_role_permissions" |
|
| "edit_role_permissions"
|
||||||
"assign_to_role" |
|
| "assign_to_role"
|
||||||
"delete_entity_role" |
|
| "delete_entity_role"
|
||||||
"view_assignments" |
|
| "view_assignments"
|
||||||
"create_assignment" |
|
| "create_assignment"
|
||||||
"edit_assignment" |
|
| "edit_assignment"
|
||||||
"delete_assignment" |
|
| "delete_assignment"
|
||||||
"start_assignment" |
|
| "start_assignment"
|
||||||
"archive_assignment" |
|
| "archive_assignment"
|
||||||
"view_entity_statistics" |
|
| "view_entity_statistics"
|
||||||
"create_user" |
|
| "create_user"
|
||||||
"create_user_batch" |
|
| "create_user_batch"
|
||||||
"create_code" |
|
| "create_code"
|
||||||
"create_code_batch" |
|
| "create_code_batch"
|
||||||
"view_code_list" |
|
| "view_code_list"
|
||||||
"delete_code" |
|
| "delete_code"
|
||||||
"view_statistics" |
|
| "view_statistics"
|
||||||
"download_statistics_report" |
|
| "download_statistics_report"
|
||||||
"edit_grading_system" |
|
| "edit_grading_system"
|
||||||
"view_student_performance" |
|
| "view_student_performance"
|
||||||
"upload_classroom" |
|
| "upload_classroom"
|
||||||
"download_user_list" |
|
| "download_user_list"
|
||||||
"view_student_record" |
|
| "view_student_record"
|
||||||
"download_student_record" |
|
| "download_student_record"
|
||||||
"pay_entity" |
|
| "pay_entity"
|
||||||
"view_payment_record" |
|
| "view_payment_record"
|
||||||
"view_approval_workflows"
|
| "view_approval_workflows"
|
||||||
|
| "update_exam_privacy";
|
||||||
|
|
||||||
export const DEFAULT_PERMISSIONS: RolePermission[] = [
|
export const DEFAULT_PERMISSIONS: RolePermission[] = [
|
||||||
"view_students",
|
"view_students",
|
||||||
@@ -76,8 +77,8 @@ export const DEFAULT_PERMISSIONS: RolePermission[] = [
|
|||||||
"view_entity_roles",
|
"view_entity_roles",
|
||||||
"view_statistics",
|
"view_statistics",
|
||||||
"download_statistics_report",
|
"download_statistics_report",
|
||||||
"view_approval_workflows"
|
"view_approval_workflows",
|
||||||
]
|
];
|
||||||
|
|
||||||
export const ADMIN_PERMISSIONS: RolePermission[] = [
|
export const ADMIN_PERMISSIONS: RolePermission[] = [
|
||||||
"view_students",
|
"view_students",
|
||||||
@@ -146,5 +147,6 @@ export const ADMIN_PERMISSIONS: RolePermission[] = [
|
|||||||
"view_student_record",
|
"view_student_record",
|
||||||
"download_student_record",
|
"download_student_record",
|
||||||
"pay_entity",
|
"pay_entity",
|
||||||
"view_payment_record"
|
"view_payment_record",
|
||||||
]
|
"update_exam_privacy",
|
||||||
|
];
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
import defaultModuleSettings from "./defaults";
|
import defaultModuleSettings from "./defaults";
|
||||||
import { Action, rootReducer } from "./reducers";
|
import {Action, rootReducer} from "./reducers";
|
||||||
import ExamEditorStore from "./types";
|
import ExamEditorStore from "./types";
|
||||||
import { create } from "zustand";
|
import {create} from "zustand";
|
||||||
|
|
||||||
|
|
||||||
const useExamEditorStore = create<
|
const useExamEditorStore = create<
|
||||||
ExamEditorStore & {
|
ExamEditorStore & {
|
||||||
dispatch: (action: Action) => void;
|
dispatch: (action: Action) => void;
|
||||||
}>((set) => ({
|
}
|
||||||
title: "",
|
>((set) => ({
|
||||||
globalEdit: [],
|
title: "",
|
||||||
currentModule: "reading",
|
globalEdit: [],
|
||||||
speakingAvatars: [],
|
currentModule: "reading",
|
||||||
modules: {
|
speakingAvatars: [],
|
||||||
reading: defaultModuleSettings("reading", 60),
|
modules: {
|
||||||
writing: defaultModuleSettings("writing", 60),
|
reading: defaultModuleSettings("reading", 60),
|
||||||
speaking: defaultModuleSettings("speaking", 14),
|
writing: defaultModuleSettings("writing", 60),
|
||||||
listening: defaultModuleSettings("listening", 30),
|
speaking: defaultModuleSettings("speaking", 14),
|
||||||
level: defaultModuleSettings("level", 60)
|
listening: defaultModuleSettings("listening", 30),
|
||||||
},
|
level: defaultModuleSettings("level", 60),
|
||||||
dispatch: (action) => set((state) => rootReducer(state, action))
|
},
|
||||||
}));
|
dispatch: (action) => set((state) => rootReducer(state, action)),
|
||||||
|
}));
|
||||||
|
|
||||||
export default useExamEditorStore;
|
export default useExamEditorStore;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { WithLabeledEntities } from "@/interfaces/entity";
|
import {WithLabeledEntities} from "@/interfaces/entity";
|
||||||
import { User } from "@/interfaces/user";
|
import {User} from "@/interfaces/user";
|
||||||
import { USER_TYPE_LABELS } from "@/resources/user";
|
import {USER_TYPE_LABELS} from "@/resources/user";
|
||||||
import { capitalize } from "lodash";
|
import {capitalize} from "lodash";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
|
||||||
export interface UserListRow {
|
export interface UserListRow {
|
||||||
@@ -22,7 +22,7 @@ export const exportListToExcel = (rowUsers: WithLabeledEntities<User>[]) => {
|
|||||||
name: user.name,
|
name: user.name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
type: USER_TYPE_LABELS[user.type],
|
type: USER_TYPE_LABELS[user.type],
|
||||||
entities: user.entities.map((e) => e.label).join(', '),
|
entities: user.entities.map((e) => e.label).join(", "),
|
||||||
expiryDate: user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited",
|
expiryDate: user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited",
|
||||||
country: user.demographicInformation?.country || "N/A",
|
country: user.demographicInformation?.country || "N/A",
|
||||||
phone: user.demographicInformation?.phone || "N/A",
|
phone: user.demographicInformation?.phone || "N/A",
|
||||||
@@ -41,8 +41,7 @@ export const exportListToExcel = (rowUsers: WithLabeledEntities<User>[]) => {
|
|||||||
|
|
||||||
export const getUserName = (user?: User) => {
|
export const getUserName = (user?: User) => {
|
||||||
if (!user) return "N/A";
|
if (!user) return "N/A";
|
||||||
if (user.type === "corporate" || user.type === "mastercorporate") return user.name;
|
|
||||||
return user.name;
|
return user.name;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isAdmin = (user: User) => ["admin", "developer"].includes(user?.type)
|
export const isAdmin = (user: User) => ["admin", "developer"].includes(user?.type);
|
||||||
|
|||||||
Reference in New Issue
Block a user