diff --git a/src/app/portal/activities/_components/ActivityDetails/ActivityInfoBody.tsx b/src/app/portal/activities/_components/ActivityDetails/ActivityInfoBody.tsx index 0d33c4ad..44a0b28a 100644 --- a/src/app/portal/activities/_components/ActivityDetails/ActivityInfoBody.tsx +++ b/src/app/portal/activities/_components/ActivityDetails/ActivityInfoBody.tsx @@ -18,14 +18,14 @@ import { ActivityDetailsProps } from '@/libs/supabase/api/_response'; import { getAssignedFaculties } from '@/libs/supabase/api/faculty-assignments'; import { getActivityReports } from '@/libs/supabase/api/storage'; import { - IconFileText, + IconLibrary, IconRosetteDiscountCheck, IconScanEye, IconUsersGroup, } from '@tabler/icons-react'; import { downloadActivityFile } from '@portal/activities/actions'; import dayjs from '@/libs/dayjs'; -import { isElevated, isInternal } from '@/utils/access-control'; +import { isPrivate, isInternal } from '@/utils/access-control'; import { identifyFileType } from '@/utils/file-types'; const RTEditor = dynamic( @@ -167,7 +167,7 @@ function ActivityDetailsBody({ <> - {isElevated(role) && ( + {isPrivate(role) && ( <> - {isElevated(role) && files && ( + {isInternal(role) && files && ( <> - + Reports } @@ -263,51 +263,45 @@ function ActivityDetailsBody({ {files.length > 0 ? ( <> {files.map((file) => ( - <> - - - {identifyFileType(file.type)} - + + + {identifyFileType(file.type)} + -
- saveFile(file.name, file.checksum)} - size="sm" - ta="left" - > - {file.name} - - - - {dayjs(file.uploaded_at).fromNow()} - +
+ saveFile(file.name, file.checksum)} + size="sm" + ta="left" + > + {file.name} + + + + {dayjs(file.uploaded_at).fromNow()} + - - - } - onClick={() => clipboard.copy(file.checksum)} - size="xs" - variant="transparent" - > - {file.checksum.slice(0, 8)} - - - -
-
- + + } + onClick={() => clipboard.copy(file.checksum)} + size="xs" + variant="transparent" + > + {file.checksum.slice(0, 8)} + + + +
+
))} ) : ( diff --git a/src/app/portal/activities/_components/ActivityDetails/ActivityInfoHeader.tsx b/src/app/portal/activities/_components/ActivityDetails/ActivityInfoHeader.tsx index 1868b780..f7883aec 100644 --- a/src/app/portal/activities/_components/ActivityDetails/ActivityInfoHeader.tsx +++ b/src/app/portal/activities/_components/ActivityDetails/ActivityInfoHeader.tsx @@ -27,6 +27,7 @@ import { IconRss, IconTrash, IconUpload, + IconUsersGroup, } from '@tabler/icons-react'; import { useProgress } from 'react-transition-progress'; import { formatDateRange } from 'little-date'; @@ -40,8 +41,9 @@ import { } from '@portal/activities/actions'; import { ActivityFormProps } from '../Forms/ActivityFormModal'; import type { Enums } from '@/libs/supabase/_database'; -import { isInternal } from '@/utils/access-control'; +import { isElevated, isInternal, isStudent } from '@/utils/access-control'; import { useUser } from '@/components/Providers/UserProvider'; +import { FacultyAssignmentModal } from '../Forms/FacultyAssignmentModal'; const ActivityFormModal = dynamic( () => @@ -138,7 +140,10 @@ function ActivityDetailsHeader({ }) { const { id: userId } = useUser(); - const [opened, { open, close }] = useDisclosure(false); + const [editOpened, { open: editOpen, close: editClose }] = + useDisclosure(false); + const [assignOpened, { open: assignOpen, close: assignClose }] = + useDisclosure(false); const [localFiles, setLocalFiles] = useState(); const [subscribed, setSubscribed] = useState(false); @@ -188,12 +193,16 @@ function ActivityDetailsHeader({ <> + - + {activity?.title} @@ -235,81 +244,94 @@ function ActivityDetailsHeader({ {/* Activity control buttons */} <Group gap="xs" mt={16}> - {isInternal(role) ? ( - <> - <Button.Group> + <> + <Button.Group> + {isStudent(role) && ( <Button - leftSection={<IconCalendarEvent size={16} />} - onClick={open} - variant="default" + leftSection={<IconRss size={16} />} + onClick={() => + onUserSubscribe( + activity.id as string, + userId, + subscribed ? !subscribed : true, + setSubscribed, + ) + } + variant={subscribed ? 'default' : 'filled'} > - Adjust Details + {subscribed ? 'Unsubscribe' : 'Subscribe'} </Button> + )} + {/* Internal-only controls */} + {isInternal(role) && ( + <> + <Button + leftSection={<IconCalendarEvent size={16} />} + onClick={editOpen} + variant="default" + > + Adjust Details + </Button> + + <Button + leftSection={ + editable ? ( + <IconEditOff size={16} /> + ) : ( + <IconEdit size={16} /> + ) + } + onClick={toggleEdit} + variant="default" + > + {editable ? 'Hide Toolbars' : 'Edit Description'} + </Button> + </> + )} + {/* Faculty Assignment */} + {isElevated(role) && ( <Button - leftSection={ - editable ? ( - <IconEditOff size={16} /> - ) : ( - <IconEdit size={16} /> - ) - } - onClick={toggleEdit} + leftSection={<IconUsersGroup size={16} />} + onClick={assignOpen} variant="default" > - {editable ? 'Hide Toolbars' : 'Edit Description'} + Assign Faculty </Button> - </Button.Group> + )} + </Button.Group> - <Divider orientation="vertical" /> + <Divider orientation="vertical" /> - <FileButton - accept=".odt,.doc,.docx,.pdf,.pptx,.ppt,.xls,.xlsx,.csv" - multiple - onChange={setLocalFiles} - > - {(props) => ( - <Tooltip label="Only visible to admins/staffs. Max. 50mb per file"> - <Button - leftSection={<IconUpload size={16} />} - variant="default" - {...props} - > - Upload Reports - </Button> - </Tooltip> - )} - </FileButton> + <FileButton + accept=".odt,.doc,.docx,.pdf,.pptx,.ppt,.xls,.xlsx,.csv" + multiple + onChange={setLocalFiles} + > + {(props) => ( + <Tooltip label="Only visible to admins/staffs. Max. 50mb per file"> + <Button + leftSection={<IconUpload size={16} />} + variant="default" + {...props} + > + Upload Reports + </Button> + </Tooltip> + )} + </FileButton> - <Button - color="red" - leftSection={<IconTrash size={16} />} - onClick={() => - deleteModal(activity.id as string, router, startProgress) - } - variant="filled" - > - Delete Activity - </Button> - </> - ) : ( - <> - <Button - leftSection={<IconRss size={16} />} - onClick={() => - onUserSubscribe( - activity.id as string, - userId, - subscribed ? !subscribed : true, - setSubscribed, - ) - } - variant={subscribed ? 'default' : 'filled'} - > - {subscribed ? 'Unsubscribe' : 'Subscribe'} - </Button> - </> - )} + <Button + color="red" + leftSection={<IconTrash size={16} />} + onClick={() => + deleteModal(activity.id as string, router, startProgress) + } + variant="outline" + > + Delete Activity + </Button> + </> </Group> </Stack> <Image diff --git a/src/app/portal/activities/_components/Forms/ActivityFormModal.tsx b/src/app/portal/activities/_components/Forms/ActivityFormModal.tsx index 529eb613..1ed65852 100644 --- a/src/app/portal/activities/_components/Forms/ActivityFormModal.tsx +++ b/src/app/portal/activities/_components/Forms/ActivityFormModal.tsx @@ -1,13 +1,11 @@ 'use client'; import { memo, useEffect, useState } from 'react'; -import dynamic from 'next/dynamic'; import Image from 'next/image'; import { Badge, Blockquote, Button, - Checkbox, Divider, Grid, Group, @@ -34,23 +32,11 @@ import { } from '@tabler/icons-react'; import { formatDateRange } from 'little-date'; import type { Tables, Enums } from '@/libs/supabase/_database'; -import { PageLoader } from '@/components/Loader/PageLoader'; import { getActivitiesInRange } from '@/libs/supabase/api/activity'; import { submitActivity } from '@portal/activities/actions'; import { SeriesInput } from './SeriesInput'; import classes from '@/styles/forms/ContainedInput.module.css'; -const FacultyList = dynamic( - () => - import('./FacultyList').then((mod) => ({ - default: mod.FacultyList, - })), - { - loading: () => <PageLoader />, - ssr: false, - }, -); - export interface ActivityFormProps { id?: string; title: string; @@ -84,7 +70,6 @@ export function ActivityFormModalComponent({ Tables<'activities_details_view'>[] >([]); const [original, setOriginal] = useState<ActivityFormProps>(); - const [isInternal, setIsInternal] = useState(false); // image file preview state const [coverFile, setCoverFile] = useState<FileWithPath[]>([]); @@ -120,13 +105,6 @@ export function ActivityFormModalComponent({ }, onValuesChange: async (values) => { - // check if visibility is set to internal - if (values.visibility === 'Internal') { - setIsInternal(true); - } else { - setIsInternal(false); - } - // clear end date if start date is empty if (!values.date_starting) { form.setFieldValue('date_ending', null); @@ -209,7 +187,13 @@ export function ActivityFormModalComponent({ }, [activity]); return ( - <Modal onClose={close} opened={opened} size="auto" title="New Activity"> + <Modal + key={activity?.id} + onClose={close} + opened={opened} + size="auto" + title="New Activity" + > <form onSubmit={form.onSubmit(handleSubmit)}> <Grid grow> {/* Left Column Grid */} @@ -331,12 +315,12 @@ export function ActivityFormModalComponent({ icon={<IconInfoCircle size={20} />} iconSize={36} ml={8} - mt="lg" + mt={20} p={24} pb="xs" > - There are currently {conflicts.length} activities scheduled on - the selected date range. + {conflicts.length} activities are scheduled within the date + range. {conflicts.length > 0 && ( <ul> {conflicts.map((activity) => ( @@ -357,31 +341,7 @@ export function ActivityFormModalComponent({ <Divider my="xs" /> - {/* Faculty Assignment */} - <Checkbox.Group - defaultValue={activity?.handled_by} - description="Faculty members assigned to this activity. (can be set later)" - key={form.key('faculty')} - label="Assign Faculty" - mt="sm" - {...form.getInputProps('handled_by', { type: 'checkbox' })} - > - {isInternal ? ( - <Text c="dimmed" mt={32} size="sm" ta="center"> - Assigning of faculty members is not available for internal - activities. - </Text> - ) : ( - <> - {/* Faculty Table Checkbox */} - <FacultyList - defaultSelection={activity?.handled_by} - endDate={form.getValues().date_ending} - startDate={form.getValues().date_starting} - /> - </> - )} - </Checkbox.Group> + {/* TODO: Objectives and Outcomes */} </Grid.Col> </Grid> diff --git a/src/app/portal/activities/_components/Forms/FacultyAssignmentModal.tsx b/src/app/portal/activities/_components/Forms/FacultyAssignmentModal.tsx new file mode 100644 index 00000000..89464dcf --- /dev/null +++ b/src/app/portal/activities/_components/Forms/FacultyAssignmentModal.tsx @@ -0,0 +1,153 @@ +'use client'; + +import { memo, useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { Button, Checkbox, Group, Modal } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { notifications } from '@mantine/notifications'; +import { IconArrowRight } from '@tabler/icons-react'; +import { PageLoader } from '@/components/Loader/PageLoader'; +import { ActivityFormProps } from './ActivityFormModal'; +import { assignFaculty } from '../../actions'; + +interface Props { + handled_by: string[]; +} + +const FacultyList = dynamic( + () => + import('./FacultyList').then((mod) => ({ + default: mod.FacultyList, + })), + { + loading: () => <PageLoader />, + ssr: false, + }, +); + +/** + * Modal form for creating or updating an activity. + * + * @param activity - The activity data to edit/update (optional). + * @param opened - The state of the modal. + * @param close - The function to close the modal. + */ +export function FacultyAssignment({ + activity, + opened, + close, +}: { + activity: ActivityFormProps; + opened: boolean; + close: () => void; +}) { + const [original, setOriginal] = useState<Props['handled_by']>(); + const [pending, setPending] = useState(false); + + // form submission + const form = useForm<Props>({ + mode: 'uncontrolled', + validateInputOnChange: true, + + initialValues: { + handled_by: [], + }, + }); + + // form handler & submission + const handleSubmit = async (values: Props) => { + setPending(true); + // todo - submit faculty assignment + const result = await assignFaculty( + activity.id!, + values.handled_by, + original, + ); + setPending(false); + + // only show error notification, if any + if (result?.status !== 0) { + notifications.show({ + title: result?.title, + message: result?.message, + color: result?.status === 1 ? 'yellow' : 'red', + withBorder: true, + withCloseButton: true, + autoClose: 8000, + }); + } else { + notifications.show({ + title: result?.title, + message: result?.message, + color: 'green', + withBorder: true, + withCloseButton: true, + autoClose: 4000, + }); + } + + form.reset(); + close(); + }; + + useEffect(() => { + if (activity) { + // keep record of the original activity data + setOriginal(activity.handled_by ?? []); + + form.setValues({ + handled_by: activity.handled_by, + }); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activity]); + + return ( + <Modal + key={activity.id} + onClose={close} + opened={opened} + size="xl" + withCloseButton={false} + > + <form onSubmit={form.onSubmit(handleSubmit)}> + {/* Faculty Assignment */} + <Checkbox.Group + defaultValue={activity.handled_by} + description="Faculty members assigned to this activity. (can be set later)" + key={form.key('faculty')} + label="Assign Faculty" + {...form.getInputProps('handled_by', { type: 'checkbox' })} + > + {/* Faculty Table Checkbox */} + <FacultyList + defaultSelection={activity.handled_by} + endDate={activity.date_ending} + startDate={activity.date_starting} + /> + </Checkbox.Group> + + {/* Save Buttons */} + <Group justify="flex-end"> + <Button mt="md" onClick={close} variant="subtle"> + Cancel + </Button> + + <Button + loaderProps={{ type: 'dots' }} + loading={pending || !form.isValid} + mt="md" + rightSection={!pending && <IconArrowRight size={16} />} + type="submit" + variant={pending ? 'default' : 'filled'} + > + {activity ? 'Reassign' : 'Assign'} + </Button> + </Group> + </form> + </Modal> + ); +} + +export const FacultyAssignmentModal = memo(FacultyAssignment); diff --git a/src/app/portal/activities/_components/Forms/FacultyList.tsx b/src/app/portal/activities/_components/Forms/FacultyList.tsx index f3a0f600..8b6c76b8 100644 --- a/src/app/portal/activities/_components/Forms/FacultyList.tsx +++ b/src/app/portal/activities/_components/Forms/FacultyList.tsx @@ -174,16 +174,14 @@ export function FacultyListComponent({ {item.department} </Badge> </Table.Td> - <Table.Td> + <Table.Td align="center"> <Suspense fallback={<Loader size="sm" type="dots" />}> {assignments.includes(item.id) ? ( - <Badge color="gray" fullWidth variant="outline"> - Busy + <Badge color="gray" variant="outline"> + Assigned </Badge> ) : ( - <Badge color="lime" fullWidth variant="outline"> - Available - </Badge> + <Badge variant="outline">Unassigned</Badge> )} </Suspense> </Table.Td> @@ -192,7 +190,7 @@ export function FacultyListComponent({ }); return ( - <ScrollArea mah={460}> + <ScrollArea mah={600}> <Group gap="xs" grow mt="md"> <TextInput leftSection={ @@ -220,7 +218,7 @@ export function FacultyListComponent({ </Table.Th> <Table.Th>Name</Table.Th> <Table.Th className="text-center">Department</Table.Th> - <Table.Th className="text-center">Availability</Table.Th> + <Table.Th className="text-center">Assignment</Table.Th> </Table.Tr> </Table.Thead> <Table.Tbody> diff --git a/src/app/portal/activities/actions.ts b/src/app/portal/activities/actions.ts index bd55f000..47b2d798 100644 --- a/src/app/portal/activities/actions.ts +++ b/src/app/portal/activities/actions.ts @@ -137,69 +137,20 @@ export async function submitActivity( }; } } else { - // save assigned faculty - if (activity.handled_by?.length) { - const assignResponse = await postFacultyAssignment({ - userId: activity.handled_by, - activityId, - supabase, + // schedule (delay) email reminders + if (existingId && activity.date_starting) { + rescheduleReminders({ + activityId: activityId, + activityStartingDate: activity.date_starting, + }); + } else { + scheduleReminders({ + activityId: activityId, + activityStartingDate: activity.date_starting!, }); - - // check if there are changes in assignments - if ( - existingId && - (original?.handled_by !== activity.handled_by || - activity.handled_by.length) - ) { - // remove unassigned faculties - await deleteFacultyAssignment({ - userId: activity.handled_by.filter( - (id) => !original?.handled_by?.includes(id), - ), - activityId, - supabase, - }); - - // send email notice to unassigned faculties - emailUnassigned.trigger({ - activity: activity.title, - ids: activity.handled_by.filter( - (id) => !original?.handled_by?.includes(id), - ), - }); - // send email notice to newly assigned faculties - emailAssigned.trigger({ - activity: activity.title, - ids: activity.handled_by.filter( - (id) => !original?.handled_by?.includes(id), - ), - }); - } else { - // send email notice to assign faculties - emailAssigned.trigger({ - activity: activity.title, - ids: activity.handled_by, - }); - } - if (!assignResponse.data) return assignResponse; } } - // schedule (delay) email reminders - if (existingId && activity.date_starting) { - rescheduleReminders({ - activityId: activityId, - activityTitle: activity.title, - activityStartingDate: activity.date_starting, - }); - } else { - scheduleReminders({ - activityId: activityId, - activityTitle: activity.title, - activityStartingDate: activity.date_starting!, - }); - } - if (existingId) { return { status: 0, @@ -215,6 +166,62 @@ export async function submitActivity( } } +/** + * Assign faculty to an activity. + * + * @param faculty - The faculty ids to assign. + * @param activityId - The activity id to assign. + */ +export async function assignFaculty( + activityId: string, + faculty: string[], + original?: string[] | null, +): Promise<ApiResponse> { + const cookieStore = cookies(); + const supabase = await createServerClient(cookieStore); + + const assignResponse = await postFacultyAssignment({ + userId: faculty, + activityId, + supabase, + }); + + // check if there are changes in assignments + if (original !== faculty || faculty.length) { + // remove unassigned faculties + await deleteFacultyAssignment({ + userId: faculty.filter((id) => !original?.includes(id)), + activityId, + supabase, + }); + + // send email notice to unassigned faculties + emailUnassigned.trigger({ + activityId: activityId, + ids: faculty.filter((id) => !original?.includes(id)), + }); + // send email notice to newly assigned faculties + emailAssigned.trigger({ + activityId: activityId, + ids: faculty.filter((id) => !original?.includes(id)), + }); + } else { + // send email notice to assign faculties + emailAssigned.trigger({ + activityId: activityId, + ids: faculty, + }); + } + + if (!assignResponse.data) return assignResponse; + + return { + status: 0, + title: 'Faculty assigned', + message: 'Faculty has been successfully assigned.', + }; +} + /** * Delete an activity. * diff --git a/src/libs/triggerdev/reminders.ts b/src/libs/triggerdev/reminders.ts index 8aeb9520..4bcd547b 100644 --- a/src/libs/triggerdev/reminders.ts +++ b/src/libs/triggerdev/reminders.ts @@ -12,11 +12,9 @@ import { emailReminders } from '@/trigger/email-reminders'; */ export async function scheduleReminders({ activityId, - activityTitle, activityStartingDate, }: { activityId: string; - activityTitle: string; activityStartingDate: Date; }) { // check if the activity starting date is not within 1 day from now @@ -24,10 +22,7 @@ export async function scheduleReminders({ if (activityStartingDate.getTime() - 1 * 24 * 60 * 60 * 1000 > Date.now()) { // schedule new reminder task, 1 day before the activity await emailReminders.trigger( - { - activityId: activityId, - activityTitle: activityTitle, - }, + { activityId: activityId }, { idempotencyKey: activityId + '_1d', tags: [`activity_${activityId}`, 'action_reminders', 'in_1'], @@ -43,10 +38,7 @@ export async function scheduleReminders({ if (activityStartingDate.getTime() - 3 * 24 * 60 * 60 * 1000 > Date.now()) { // schedule new reminder task, 3 and 7 days before the activity await emailReminders.trigger( - { - activityId: activityId, - activityTitle: activityTitle, - }, + { activityId: activityId }, { idempotencyKey: activityId + '_3d', tags: [`activity_${activityId}`, 'action_reminders', 'in_3'], @@ -62,10 +54,7 @@ export async function scheduleReminders({ if (activityStartingDate.getTime() - 7 * 24 * 60 * 60 * 1000 > Date.now()) { // schedule new reminder task, 7 days before the activity await emailReminders.trigger( - { - activityId: activityId, - activityTitle: activityTitle, - }, + { activityId: activityId }, { idempotencyKey: activityId + '_7d', tags: [`activity_${activityId}`, 'action_reminders', 'in_7'], @@ -89,11 +78,9 @@ export async function scheduleReminders({ */ export async function rescheduleReminders({ activityId, - activityTitle, activityStartingDate, }: { activityId: string; - activityTitle: string; activityStartingDate: Date; }) { // get the queued reminder task @@ -143,7 +130,6 @@ export async function rescheduleReminders({ // schedule new reminder task await scheduleReminders({ activityId: activityId, - activityTitle: activityTitle, activityStartingDate: activityStartingDate, }); } diff --git a/src/trigger/email-assigned.ts b/src/trigger/email-assigned.ts index abce0baf..7f30f5a3 100644 --- a/src/trigger/email-assigned.ts +++ b/src/trigger/email-assigned.ts @@ -13,7 +13,7 @@ export const emailAssigned = task({ id: 'email-assigned', run: async ( payload: { - activity: string; + activityId: string; ids: string[]; }, { ctx }, @@ -35,7 +35,7 @@ export const emailAssigned = task({ const activityQuery = supabase .from('activities') .select() - .eq('title', payload.activity) + .eq('id', payload.activityId) .limit(1) .single(); diff --git a/src/trigger/email-reminders.ts b/src/trigger/email-reminders.ts index f40f498e..13a7af8f 100644 --- a/src/trigger/email-reminders.ts +++ b/src/trigger/email-reminders.ts @@ -11,10 +11,7 @@ import { createAdminClient } from '@/libs/supabase/admin-client'; */ export const emailReminders = task({ id: 'email-reminders', - run: async ( - payload: { activityId: string; activityTitle: string }, - { ctx }, - ) => { + run: async (payload: { activityId: string }, { ctx }) => { await envvars.retrieve('SUPABASE_URL'); await envvars.retrieve('SUPABASE_SERVICE_KEY'); @@ -50,7 +47,7 @@ export const emailReminders = task({ const activityQuery = supabase .from('activities') .select() - .eq('title', payload.activityTitle) + .eq('id', payload.activityId) .limit(1) .single(); diff --git a/src/trigger/email-unassigned.ts b/src/trigger/email-unassigned.ts index 70e25660..eee782b0 100644 --- a/src/trigger/email-unassigned.ts +++ b/src/trigger/email-unassigned.ts @@ -13,7 +13,7 @@ export const emailUnassigned = task({ id: 'email-unassigned', run: async ( payload: { - activity: string; + activityId: string; ids: string[]; }, { ctx }, @@ -35,7 +35,7 @@ export const emailUnassigned = task({ const activityQuery = supabase .from('activities') .select() - .eq('title', payload.activity) + .eq('id', payload.activityId) .limit(1) .single(); diff --git a/src/utils/access-control.ts b/src/utils/access-control.ts index c020a507..bc3058d1 100644 --- a/src/utils/access-control.ts +++ b/src/utils/access-control.ts @@ -13,12 +13,28 @@ export const isInternal = (role: Enums<'roles_user'> | null): boolean => { return role === 'admin' || role === 'staff'; }; +/** + * Check if the user is internal or faculty chair. + * + * @param role - The user's role. + */ +export const isElevated = ( + role: Enums<'roles_user'> | null, + pos?: Enums<'roles_pos'> | null, +): boolean => { + if (!role) { + return false; + } + + return pos?.includes('chair') || isInternal(role); +}; + /** * Check if the user is internal or faculty. * * @param role - The user's role. */ -export const isElevated = (role: Enums<'roles_user'> | null): boolean => { +export const isPrivate = (role: Enums<'roles_user'> | null): boolean => { if (!role) { return false; } @@ -39,6 +55,19 @@ export const isAdmin = (role: Enums<'roles_user'> | null): boolean => { return role === 'admin'; }; +/** + * Check if the user is a student. + * + * @param role - The user's role. + */ +export const isStudent = (role: Enums<'roles_user'> | null): boolean => { + if (!role) { + return false; + } + + return role === 'student'; +}; + /** * Check if the user is allowed to access or view the activity, * based on the activity's visibility and the user's role. @@ -54,7 +83,7 @@ export const canAccessActivity = ( case 'Internal': return isInternal(role); case 'Faculty': - return isElevated(role); + return isPrivate(role); default: return true; }