ENCOA-267

This commit is contained in:
Tiago Ribeiro
2024-12-23 10:18:52 +00:00
parent 9cf13e3f26
commit e9c961e633
6 changed files with 157 additions and 142 deletions

View File

@@ -8,163 +8,166 @@ import useGroups from "@/hooks/useGroups";
import useRecordStore from "@/stores/recordStore"; import useRecordStore from "@/stores/recordStore";
import { EntityWithRoles } from "@/interfaces/entity"; import { EntityWithRoles } from "@/interfaces/entity";
import { mapBy } from "@/utils"; import { mapBy } from "@/utils";
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
type TimeFilter = "months" | "weeks" | "days"; type TimeFilter = "months" | "weeks" | "days";
type Filter = TimeFilter | "assignments" | undefined; type Filter = TimeFilter | "assignments" | undefined;
interface Props { interface Props {
user: User; user: User;
entities: EntityWithRoles[] entities: EntityWithRoles[]
users: User[] users: User[]
filterState: { filterState: {
filter: Filter, filter: Filter,
setFilter: React.Dispatch<React.SetStateAction<Filter>> setFilter: React.Dispatch<React.SetStateAction<Filter>>
}, },
assignments?: boolean; assignments?: boolean;
children?: ReactNode children?: ReactNode
} }
const defaultSelectableCorporate = { const defaultSelectableCorporate = {
value: "", value: "",
label: "All", label: "All",
}; };
const RecordFilter: React.FC<Props> = ({ const RecordFilter: React.FC<Props> = ({
user, user,
entities, entities,
users, users,
filterState, filterState,
assignments = true, assignments = true,
children children
}) => { }) => {
const { filter, setFilter } = filterState; const { filter, setFilter } = filterState;
const [entity, setEntity] = useState<string>() const [entity, setEntity] = useState<string>()
const [, setStatsUserId] = useRecordStore((state) => [ const [, setStatsUserId] = useRecordStore((state) => [
state.selectedUser, state.selectedUser,
state.setSelectedUser state.setSelectedUser
]); ]);
const entityUsers = useMemo(() => !entity ? users : users.filter(u => mapBy(u.entities, 'id').includes(entity)), [users, entity]) const allowedViewEntities = useAllowedEntities(user, entities, 'view_student_record')
useEffect(() => setStatsUserId(user.id), [setStatsUserId, user.id]) const entityUsers = useMemo(() => !entity ? users : users.filter(u => mapBy(u.entities, 'id').includes(entity)), [users, entity])
const toggleFilter = (value: "months" | "weeks" | "days" | "assignments") => { useEffect(() => setStatsUserId(user.id), [setStatsUserId, user.id])
setFilter((prev) => (prev === value ? undefined : value));
};
return ( const toggleFilter = (value: "months" | "weeks" | "days" | "assignments") => {
<div className="w-full flex -xl:flex-col -xl:gap-4 justify-between items-center"> setFilter((prev) => (prev === value ? undefined : value));
<div className="xl:w-3/4 flex gap-2"> };
{checkAccess(user, ["developer", "admin", "mastercorporate"]) && !children && (
<>
<div className="flex flex-col gap-2 w-full">
<label className="font-normal text-base text-mti-gray-dim">Entity</label>
<Select return (
options={entities.map((e) => ({value: e.id, label: e.label}))} <div className="w-full flex -xl:flex-col -xl:gap-4 justify-between items-center">
onChange={(value) => setEntity(value?.value || undefined)} <div className="xl:w-3/4 flex gap-2">
isClearable {checkAccess(user, ["developer", "admin", "mastercorporate"]) && !children && (
styles={{ <>
menuPortal: (base) => ({ ...base, zIndex: 9999 }), <div className="flex flex-col gap-2 w-full">
option: (styles, state) => ({ <label className="font-normal text-base text-mti-gray-dim">Entity</label>
...styles,
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
color: state.isFocused ? "black" : styles.color,
}),
}} />
</div>
<div className="flex flex-col gap-2 w-full">
<label className="font-normal text-base text-mti-gray-dim">User</label>
<Select <Select
options={entityUsers.map((x) => ({ options={allowedViewEntities.map((e) => ({ value: e.id, label: e.label }))}
value: x.id, onChange={(value) => setEntity(value?.value || undefined)}
label: `${x.name} - ${x.email}`, isClearable
}))} styles={{
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}} menuPortal: (base) => ({ ...base, zIndex: 9999 }),
onChange={(value) => setStatsUserId(value?.value!)} option: (styles, state) => ({
styles={{ ...styles,
menuPortal: (base) => ({ ...base, zIndex: 9999 }), backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
option: (styles, state) => ({ color: state.isFocused ? "black" : styles.color,
...styles, }),
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", }} />
color: state.isFocused ? "black" : styles.color, </div>
}), <div className="flex flex-col gap-2 w-full">
}} <label className="font-normal text-base text-mti-gray-dim">User</label>
/>
</div>
</>
)}
{(user.type === "corporate" || user.type === "teacher") && !children && (
<div className="flex flex-col gap-2">
<label className="font-normal text-base text-mti-gray-dim">User</label>
<Select <Select
options={users options={entityUsers.map((x) => ({
.map((x) => ({ value: x.id,
value: x.id, label: `${x.name} - ${x.email}`,
label: `${x.name} - ${x.email}`, }))}
}))} defaultValue={{ value: user.id, label: `${user.name} - ${user.email}` }}
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}} onChange={(value) => setStatsUserId(value?.value!)}
onChange={(value) => setStatsUserId(value?.value!)} styles={{
styles={{ menuPortal: (base) => ({ ...base, zIndex: 9999 }),
menuPortal: (base) => ({ ...base, zIndex: 9999 }), option: (styles, state) => ({
option: (styles, state) => ({ ...styles,
...styles, backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", color: state.isFocused ? "black" : styles.color,
color: state.isFocused ? "black" : styles.color, }),
}), }}
}} />
/> </div>
</div> </>
)} )}
{children} {(user.type === "corporate" || user.type === "teacher") && !children && (
</div> <div className="flex flex-col gap-2">
<div className="flex gap-4 w-full justify-center xl:justify-end"> <label className="font-normal text-base text-mti-gray-dim">User</label>
{assignments && (
<button <Select
className={clsx( options={users
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light", .map((x) => ({
"transition duration-300 ease-in-out", value: x.id,
filter === "assignments" && "!bg-mti-purple-light !text-white", label: `${x.name} - ${x.email}`,
)} }))}
onClick={() => toggleFilter("assignments")}> defaultValue={{ value: user.id, label: `${user.name} - ${user.email}` }}
Assignments onChange={(value) => setStatsUserId(value?.value!)}
</button> styles={{
)} menuPortal: (base) => ({ ...base, zIndex: 9999 }),
<button option: (styles, state) => ({
className={clsx( ...styles,
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light", backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
"transition duration-300 ease-in-out", color: state.isFocused ? "black" : styles.color,
filter === "months" && "!bg-mti-purple-light !text-white", }),
)} }}
onClick={() => toggleFilter("months")}> />
Last month </div>
</button> )}
<button {children}
className={clsx( </div>
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light", <div className="flex gap-4 w-full justify-center xl:justify-end">
"transition duration-300 ease-in-out", {assignments && (
filter === "weeks" && "!bg-mti-purple-light !text-white", <button
)} className={clsx(
onClick={() => toggleFilter("weeks")}> "bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
Last week "transition duration-300 ease-in-out",
</button> filter === "assignments" && "!bg-mti-purple-light !text-white",
<button )}
className={clsx( onClick={() => toggleFilter("assignments")}>
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light", Assignments
"transition duration-300 ease-in-out", </button>
filter === "days" && "!bg-mti-purple-light !text-white", )}
)} <button
onClick={() => toggleFilter("days")}> className={clsx(
Last day "bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
</button> "transition duration-300 ease-in-out",
</div> filter === "months" && "!bg-mti-purple-light !text-white",
</div> )}
); onClick={() => toggleFilter("months")}>
Last month
</button>
<button
className={clsx(
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
"transition duration-300 ease-in-out",
filter === "weeks" && "!bg-mti-purple-light !text-white",
)}
onClick={() => toggleFilter("weeks")}>
Last week
</button>
<button
className={clsx(
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
"transition duration-300 ease-in-out",
filter === "days" && "!bg-mti-purple-light !text-white",
)}
onClick={() => toggleFilter("days")}>
Last day
</button>
</div>
</div>
);
} }
export default RecordFilter; export default RecordFilter;

View File

@@ -82,7 +82,7 @@ interface StatsGridItemProps {
selectedTrainingExams?: string[]; selectedTrainingExams?: string[];
maxTrainingExams?: number; maxTrainingExams?: number;
setSelectedTrainingExams?: React.Dispatch<React.SetStateAction<string[]>>; setSelectedTrainingExams?: React.Dispatch<React.SetStateAction<string[]>>;
renderPdfIcon: (session: string, color: string, textColor: string) => React.ReactNode; renderPdfIcon?: (session: string, color: string, textColor: string) => React.ReactNode;
} }
const StatsGridItem: React.FC<StatsGridItemProps> = ({ const StatsGridItem: React.FC<StatsGridItemProps> = ({
@@ -236,7 +236,7 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
{renderLevelScore()} {renderLevelScore()}
</span> </span>
)} )}
{shouldRenderPDFIcon() && renderPdfIcon(session, textColor, textColor)} {shouldRenderPDFIcon() && renderPdfIcon && renderPdfIcon(session, textColor, textColor)}
</div> </div>
{examNumber === undefined ? ( {examNumber === undefined ? (
<> <>

View File

@@ -79,6 +79,8 @@ const CLASSROOM_MANAGEMENT: PermissionLayout[] = [
{ 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: "Download Student Report", key: "download_student_record" },
] ]
const ENTITY_MANAGEMENT: PermissionLayout[] = [ const ENTITY_MANAGEMENT: PermissionLayout[] = [

View File

@@ -22,7 +22,7 @@ import { Assignment } from "@/interfaces/results";
import { getEntitiesUsers, getUsers } from "@/utils/users.be"; import { getEntitiesUsers, getUsers } from "@/utils/users.be";
import { getAssignments, getEntitiesAssignments } from "@/utils/assignments.be"; import { getAssignments, getEntitiesAssignments } from "@/utils/assignments.be";
import useGradingSystem from "@/hooks/useGrading"; import useGradingSystem from "@/hooks/useGrading";
import { mapBy, redirect, serialize } from "@/utils"; import { findBy, mapBy, redirect, serialize } from "@/utils";
import { getEntitiesWithRoles } from "@/utils/entities.be"; import { getEntitiesWithRoles } from "@/utils/entities.be";
import { checkAccess } from "@/utils/permissions"; import { checkAccess } from "@/utils/permissions";
import { getGroups, getGroupsByEntities } from "@/utils/groups.be"; import { getGroups, getGroupsByEntities } from "@/utils/groups.be";
@@ -31,6 +31,7 @@ import { Grading } from "@/interfaces";
import { EntityWithRoles } from "@/interfaces/entity"; import { EntityWithRoles } from "@/interfaces/entity";
import CardList from "@/components/High/CardList"; import CardList from "@/components/High/CardList";
import { requestUser } from "@/utils/api"; import { requestUser } from "@/utils/api";
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
const user = await requestUser(req, res) const user = await requestUser(req, res)
@@ -74,6 +75,7 @@ export default function History({ user, users, assignments, entities, gradingSys
const [filter, setFilter] = useState<Filter>(); const [filter, setFilter] = useState<Filter>();
const { data: stats, isLoading: isStatsLoading } = useFilterRecordsByUser<Stat[]>(statsUserId || user?.id); const { data: stats, isLoading: isStatsLoading } = useFilterRecordsByUser<Stat[]>(statsUserId || user?.id);
const allowedDownloadEntities = useAllowedEntities(user, entities, 'download_student_record')
const renderPdfIcon = usePDFDownload("stats"); const renderPdfIcon = usePDFDownload("stats");
@@ -155,6 +157,9 @@ export default function History({ user, users, assignments, entities, gradingSys
const customContent = (timestamp: string) => { const customContent = (timestamp: string) => {
const dateStats = groupedStats[timestamp]; const dateStats = groupedStats[timestamp];
const statUser = findBy(users, 'id', dateStats[0]?.user)
const canDownload = mapBy(statUser?.entities, 'id').some(e => mapBy(allowedDownloadEntities, 'id').includes(e))
return ( return (
<StatsGridItem <StatsGridItem
@@ -169,7 +174,7 @@ export default function History({ user, users, assignments, entities, gradingSys
selectedTrainingExams={selectedTrainingExams} selectedTrainingExams={selectedTrainingExams}
setSelectedTrainingExams={setSelectedTrainingExams} setSelectedTrainingExams={setSelectedTrainingExams}
maxTrainingExams={MAX_TRAINING_EXAMS} maxTrainingExams={MAX_TRAINING_EXAMS}
renderPdfIcon={renderPdfIcon} renderPdfIcon={canDownload ? renderPdfIcon : undefined}
/> />
); );
}; };

View File

@@ -61,7 +61,9 @@ export type RolePermission =
"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" |
"download_student_record"
export const DEFAULT_PERMISSIONS: RolePermission[] = [ export const DEFAULT_PERMISSIONS: RolePermission[] = [
"view_students", "view_students",
@@ -136,5 +138,7 @@ export const ADMIN_PERMISSIONS: RolePermission[] = [
"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",
"download_student_record"
] ]

View File

@@ -3,6 +3,7 @@ import { WithEntity } from "@/interfaces/entity";
import { Assignment } from "@/interfaces/results"; import { Assignment } from "@/interfaces/results";
import { CorporateUser, Group, GroupWithUsers, MasterCorporateUser, StudentUser, TeacherUser, Type, User } from "@/interfaces/user"; import { CorporateUser, Group, GroupWithUsers, MasterCorporateUser, StudentUser, TeacherUser, Type, User } from "@/interfaces/user";
import client from "@/lib/mongodb"; import client from "@/lib/mongodb";
import { uniq } from "lodash";
import moment from "moment"; import moment from "moment";
import { getLinkedUsers, getUser } from "./users.be"; import { getLinkedUsers, getUser } from "./users.be";
import { getSpecificUsers } from "./users.be"; import { getSpecificUsers } from "./users.be";
@@ -116,7 +117,7 @@ export const getUsersGroups = async (ids: string[]) => {
export const convertToUsers = (group: Group, users: User[]): GroupWithUsers => export const convertToUsers = (group: Group, users: User[]): GroupWithUsers =>
Object.assign(group, { Object.assign(group, {
admin: users.find((u) => u.id === group.admin), admin: users.find((u) => u.id === group.admin),
participants: group.participants.map((p) => users.find((u) => u.id === p)).filter((x) => !!x) as User[], participants: uniq(group.participants).map((p) => users.find((u) => u.id === p)).filter((x) => !!x) as User[],
}); });
export const getAllAssignersByCorporate = async (corporateID: string, type: Type): Promise<string[]> => { export const getAllAssignersByCorporate = async (corporateID: string, type: Type): Promise<string[]> => {