Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 48 additions & 11 deletions packages/alea-frontend/components/instructor-panel/CourseInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,47 @@ import { useRouter } from 'next/router';
import { getLocaleObject } from '../../lang/utils';
import { useEffect, useState } from 'react';

function parseInstructors(val: unknown): Array<{ id: string; name: string }> {
if (val == null) return [];
if (Array.isArray(val)) {
return val.map((v: any) => {
if (typeof v === 'string') {
return { id: v, name: v };
}
return {
id: v.id || v.name || '',
name: v.name || v.id || '',
};
});
}
if (typeof val === 'string') {
const s = val.trim();
if (!s) return [];

try {
const parsed = JSON.parse(s);

if (Array.isArray(parsed)) {
return parsed.map((v: any) => {
if (typeof v === 'string') {
return { id: v, name: v };
}
return {
id: v.id || v.name || '',
name: v.name || v.id || '',
};
});
}

return [];
} catch {
return [{ id: s, name: s }];
}
}

return [];
}

interface CourseInfoTabProps {
courseId: string;
instanceId: string;
Expand Down Expand Up @@ -114,9 +155,9 @@ export default function CourseInfoTab({ courseId, instanceId }: CourseInfoTabPro
}

try {
const savedInstructors = resolvedInfo!.instructors ?? [];
const savedInstructors = parseInstructors(resolvedInfo!.instructors);
const savedMap = new Map<string, InstructorInfo>(
resolvedInfo.instructors.map((s) => [s.id, s])
savedInstructors.map((s) => [s.id, s])
);

const aclIds = await getCourseAcls(courseId, instanceId);
Expand Down Expand Up @@ -147,7 +188,6 @@ export default function CourseInfoTab({ courseId, instanceId }: CourseInfoTabPro
memberMap.set(s.id, { userId: s.id, fullName: s.name || '' });
}
}

const merged: CourseInstructorExt[] = savedInstructors.map((saved) => ({
id: saved.id,
name: saved.name,
Expand Down Expand Up @@ -323,8 +363,6 @@ export default function CourseInfoTab({ courseId, instanceId }: CourseInfoTabPro
checked={courseInfo.hasHomework || false}
onChange={async (e) => {
const next = e.target.checked;
if (!confirm(t.confirmUpdateHomework)) return;

try {
await updateHasHomework({ courseId, instanceId, hasHomework: next });
setField('hasHomework', next);
Expand All @@ -344,8 +382,6 @@ export default function CourseInfoTab({ courseId, instanceId }: CourseInfoTabPro
checked={courseInfo.hasQuiz || false}
onChange={async (e) => {
const next = e.target.checked;
if (!confirm(t.confirmUpdateQuiz)) return;

try {
await updateHasQuiz({ courseId, instanceId, hasQuiz: next });
setField('hasQuiz', next);
Expand All @@ -365,7 +401,6 @@ export default function CourseInfoTab({ courseId, instanceId }: CourseInfoTabPro
checked={courseInfo.hasCheatsheet || false}
onChange={async (e) => {
const next = e.target.checked;
if (!confirm(t.confirmUpdateCheatsheet)) return;

try {
await updateHasCheatsheet({ courseId, instanceId, hasCheatsheet: next });
Expand All @@ -385,10 +420,12 @@ export default function CourseInfoTab({ courseId, instanceId }: CourseInfoTabPro
checked={courseInfo.canStudentUploadCheatsheet || false}
onChange={async (e) => {
const next = e.target.checked;
if (!confirm(t.confirmUpdateCanStudentUploadCheatsheet)) return;

try {
await updateCanStudentUploadCheatsheet({ courseId, instanceId, canStudentUploadCheatsheet: next });
await updateCanStudentUploadCheatsheet({
courseId,
instanceId,
canStudentUploadCheatsheet: next,
});
setField('canStudentUploadCheatsheet', next);
} catch (err) {
console.error('Failed to update student cheatsheet upload', err);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,33 +36,35 @@ export const DashboardHeader: React.FC<DashboardHeaderProps> = ({
University Admin Dashboard
</Typography>
<Stack direction="row" spacing={2} mb={3}>
<FormControl size="small" sx={{ minWidth: 160 }}>
<InputLabel id="semester-select-label">Semester</InputLabel>
<Select
labelId="semester-select-label"
value={semester}
label="Semester"
onChange={(e) => onSemesterChange(e.target.value)}
disabled={loadingOptions}
>
{loadingOptions ? (
<MenuItem disabled>Loading...</MenuItem>
) : (
semesterOptions.map((opt) => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
))
)}
</Select>
</FormControl>
{!showSemForm && (
<FormControl size="small" sx={{ minWidth: 160 }}>
<InputLabel id="semester-select-label">Semester</InputLabel>
<Select
labelId="semester-select-label"
value={semester}
label="Semester"
onChange={(e) => onSemesterChange(e.target.value)}
disabled={loadingOptions}
>
{loadingOptions ? (
<MenuItem disabled>Loading...</MenuItem>
) : (
semesterOptions.map((opt) => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
))
)}
</Select>
</FormControl>
)}
<Button
variant="outlined"
color="primary"
sx={{ fontWeight: 600 }}
onClick={onToggleSemForm}
>
{showSemForm ? 'Hide' : 'Add'} Sem Detail
{showSemForm ? 'Exit' : 'Create New Semester'}
</Button>
</Stack>
</>
Expand Down
4 changes: 2 additions & 2 deletions packages/alea-frontend/hooks/useStudentCount.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getStudentCountInCourse } from '@alea/spec';
import { useEffect, useState } from 'react';

export function useStudentCount(courseId?: string, instanceId?: string) {
export function useStudentCount(courseId?: string, instanceId?: string, reFetch?: boolean) {
const [studentCount, setStudentCount] = useState<number | null>(null);

useEffect(() => {
Expand All @@ -18,7 +18,7 @@ export function useStudentCount(courseId?: string, instanceId?: string) {
console.error('Error fetching student count:', err);
setStudentCount(null);
});
}, [courseId]);
}, [courseId, instanceId, reFetch]);

return studentCount;
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
ResourceName,
pathToCheatSheet,
} from '@alea/utils';
import { getSemesterInfo } from '@alea/spec';
import { SafeFTMLDocument } from '@alea/stex-react-renderer';
import ArticleIcon from '@mui/icons-material/Article';
import AssignmentTurnedInIcon from '@mui/icons-material/AssignmentTurnedIn';
Expand Down Expand Up @@ -501,7 +502,6 @@ const CourseHomePage: NextPage = () => {
const [searchQuery, setSearchQuery] = useState('');
const [seriesId, setSeriesId] = useState<string>('');

const studentCount = useStudentCount(courseId, currentTerm);
const queryClient = useQueryClient();
useEffect(() => {
if (!courseId || !currentTerm) return;
Expand Down Expand Up @@ -533,7 +533,7 @@ const CourseHomePage: NextPage = () => {
},
});
const enrolled = !isFetching && isEnrolled === true;

const studentCount = useStudentCount(courseId, currentTerm, enrolled);
const { data: hasInstructorAccess, isFetching: isInstructorFetching } = useQuery({
queryKey: ['is-instructor', courseId, currentTerm],
enabled: Boolean(courseId && currentTerm),
Expand All @@ -557,6 +557,18 @@ const CourseHomePage: NextPage = () => {
enabled: Boolean(courseId && currentTerm),
queryFn: () => getCourseInfoMetadata(courseId!, currentTerm!),
});

const { data: semesterInfo } = useQuery({
queryKey: ['semester-info', institutionId, currentTerm],
enabled: Boolean(institutionId && currentTerm),
queryFn: () => getSemesterInfo(institutionId!, currentTerm!),
});

const isSemesterOver =
semesterInfo && semesterInfo.length > 0
? new Date() > new Date(semesterInfo[0].semesterEnd)
: false;

if (isValidating) return null;
if (validationError) {
return (
Expand Down Expand Up @@ -749,7 +761,7 @@ const CourseHomePage: NextPage = () => {
)}
</Box>
<InstructorDetails details={instructorDetails} />
{enrolled === false && (
{enrolled === false && !isSemesterOver && (
<Box
sx={{
display: 'flex',
Expand Down Expand Up @@ -782,7 +794,7 @@ const CourseHomePage: NextPage = () => {
</Box>
)}

{enrolled && (
{enrolled && !isSemesterOver && (
<Box sx={{ m: 2, textAlign: 'center' }}>
{studentCount !== null && (
<Typography
Expand Down Expand Up @@ -874,4 +886,4 @@ const CourseHomePage: NextPage = () => {
);
};

export default CourseHomePage;
export default CourseHomePage;
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (directMembers.length === 0) {
return res.status(200).send([]);
}
const userIdPlaceholders = directMembers.map(() => '?').join(', ');
const userInfoResults: { firstname: string; lastname: string; userId: string }[] =
const placeholders = directMembers.map(() => '?').join(',');
const userInfoResults: { firstName: string; lastName: string; userId: string }[] =
await executeDontEndSet500OnError(
`SELECT firstName as firstname, lastName as lastname, userId FROM userInfo WHERE userId IN (${userIdPlaceholders})`,
`select firstName, lastName, userId from userInfo where userId IN (${placeholders})`,
directMembers,
res
);
const result = directMembers.map((userId) => {
const userInfo = userInfoResults.find((record) => record.userId === userId);
const fullName = userInfo ? `${userInfo.firstname} ${userInfo.lastname}` : '';
const fullName = userInfo ? `${userInfo.firstName} ${userInfo.lastName}` : '';
return { fullName, userId };
});
res.status(200).send(result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(200).send([]);
}

const result: { firstname: string; lastname: string; userId: string }[] =
const placeholders = members.map(() => '?').join(',');
const result: { firstName: string; lastName: string; userId: string }[] =
await executeDontEndSet500OnError(
`select firstname, lastname, userId from userInfo where userId IN (?)`,
[members],
`select firstName, lastName, userId from userInfo where userId IN (${placeholders})`,
members,
res
);
res
.status(200)
.send(result.map((c) => ({ fullName: `${c.firstname} ${c.lastname}`, userId: c.userId })));
.send(result.map((c) => ({ fullName: `${c.firstName} ${c.lastName}`, userId: c.userId })));
}
48 changes: 48 additions & 0 deletions packages/alea-frontend/pages/api/course/enroll-unenroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { NextApiRequest, NextApiResponse } from 'next';
import {
getUserIdOrSetError,
checkIfPostOrSetError,
} from '../comment-utils';
import { getSemesterInfoFromDb } from '../calendar/create-calendar';
import { addRemoveMemberOrSetError } from '../access-control/add-remove-member';

function getCourseEnrollmentAcl(courseId: string, instanceId: string) {
return `${courseId}-${instanceId}-enrollments`;
}

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!checkIfPostOrSetError(req, res)) return;
const userId = await getUserIdOrSetError(req, res);
if (!userId) return;
const { courseId, instanceId, action, universityId = 'FAU' } = req.body;

if (!courseId || !instanceId || !action) {
return res.status(422).send('Missing required fields: courseId, instanceId, or action.');
}

const aclId = getCourseEnrollmentAcl(courseId, instanceId);

if (action === 'enroll') {
const semesterData = await getSemesterInfoFromDb(universityId, instanceId);
if (semesterData?.semesterEnd && new Date() > new Date(semesterData.semesterEnd)) {
return res.status(403).send('Enrollment closed: Semester has ended.');
}
} else if (action !== 'unenroll') {
return res.status(400).send('Invalid action. Use "enroll" or "unenroll".');
}

const success = await addRemoveMemberOrSetError(
{
memberId: userId,
aclId: aclId,
isAclMember: false,
toBeAdded: action === 'enroll',
},
req,
res
);

if (!success) return;

res.status(200).json({ success: true, action });
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
[courseId, instanceId],
res
);

if (!existing) return;

if (Array.isArray(existing) && existing.length > 0) {
return res.status(200).json({ message: 'Course already exists for this semester' });
}

const alreadyExistCourseIdRow = await executeAndEndSet500OnError(
`SELECT courseName, notes, landing, slides, teaser
`SELECT courseName, notes, landing, slides, teaser, instructors
FROM courseMetadata
WHERE courseId = ? AND universityId = ?
LIMIT 1`,
Expand All @@ -48,6 +47,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
let landing = '';
let slides = '';
let teaser: string | null = null;
let instructors: any = [];

if (Array.isArray(alreadyExistCourseIdRow) && alreadyExistCourseIdRow.length > 0) {
const template = alreadyExistCourseIdRow[0] as any;
Expand All @@ -56,6 +56,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
landing = template?.landing || landing;
slides = template?.slides || slides;
teaser = template?.teaser ?? teaser;
instructors = template?.instructors || instructors;
}


Expand Down Expand Up @@ -92,7 +93,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
landing,
slides,
teaser,
JSON.stringify([]),
typeof instructors === 'string' ? instructors : JSON.stringify(instructors),
],
res
);
Expand Down
Loading
Loading