Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f4a8c5d
feat: add backend
pestler Oct 21, 2025
6f1c6cd
feat: add page report
pestler Oct 22, 2025
32f26fd
style: format code with prettier
pestler Oct 22, 2025
ee8c36f
feat: Implement dynamic leave survey and expelled stats delete
pestler Oct 22, 2025
026de65
feat: refactor export csv
pestler Oct 23, 2025
f77c85c
feat: formatting
pestler Oct 23, 2025
bc6a99e
feat: refactoring client code
pestler Oct 28, 2025
fa5aaac
feat: done. changes in order of existing migrations
pestler Oct 28, 2025
711e07f
feat: rename otherComment
pestler Oct 28, 2025
fb640d3
feat: delete try-catch
pestler Oct 28, 2025
2344135
feat: change course leave service for nest js
pestler Oct 30, 2025
10aa996
feat: delete ExternalAccount
pestler Oct 30, 2025
734164d
fix: error
pestler Oct 30, 2025
e819da0
Merge branch 'master' into feat/2846-add-reports-page
pestler Oct 30, 2025
023c535
feat: refactoring
pestler Oct 30, 2025
d4b8aa6
Merge branch 'master' into feat/2846-add-reports-page
AlreadyBored Nov 3, 2025
779bb7b
feat: course leave in nest js
pestler Nov 3, 2025
a1dcf9a
Merge branch 'feat/2846-add-reports-page' of https://github.com/rolli…
pestler Nov 3, 2025
8cd60f3
feat: format
pestler Nov 3, 2025
27618bc
feat: fix other coment table
pestler Nov 3, 2025
b84fb78
feat: add text leave course
pestler Nov 3, 2025
4973ec3
Merge branch 'master' into feat/2846-add-reports-page
pestler Nov 4, 2025
6c9e279
feat: del duplicate backend
pestler Nov 4, 2025
de66458
fix: fix error type
pestler Nov 4, 2025
b3961ae
Merge branch 'master' into feat/2846-add-reports-page
AlreadyBored Nov 7, 2025
7503206
feat: refactor data
pestler Nov 10, 2025
4e57672
feat: fix error
pestler Nov 10, 2025
4b34f64
merge branch master into feat/2846
pestler Nov 27, 2025
738a5f3
feat: generate spec and api
pestler Nov 27, 2025
8c2bfc2
fix: error building
pestler Nov 27, 2025
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
77 changes: 64 additions & 13 deletions client/src/components/Profile/StudentLeaveCourse.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,96 @@
import { WarningOutlined } from '@ant-design/icons';
import { Divider, Modal, theme, Typography } from 'antd';
import { Divider, Modal, theme, Typography, Checkbox, Space, Form, Input } from 'antd';

const { Title, Paragraph } = Typography;

type SurveyResponses = {
reasonForLeaving?: string[];
otherComments?: string;
};

type ReasonOption = {
value: string;
labelEn: string;
labelRu: string;
};

type StudentLeaveCourseProps = {
isOpen: boolean;
onOk: () => void;
onOk: (surveyData: SurveyResponses) => void;
onCancel: () => void;
confirmLoading?: boolean;
reasonsOptions: ReasonOption[];
};

const messages = ['Are you sure you want to leave the course?', 'Your learning will be stopped.'];

const messagesRu = ['Вы уверены, что хотите покинуть курс?', 'Ваше обучение будет прекращено.'];

export default function StudentLeaveCourse({ isOpen, onOk, onCancel }: StudentLeaveCourseProps) {
export default function StudentLeaveCourse({
isOpen,
onOk,
onCancel,
confirmLoading,
reasonsOptions,
}: StudentLeaveCourseProps) {
const {
token: { colorError },
} = theme.useToken();
const [form] = Form.useForm();

const handleOkClick = () => {
form
.validateFields()
.then(values => {
onOk(values);
})
.catch(info => {
console.log('Validate Failed:', info);
});
};

return (
<Modal
title={
<Title level={4}>
<WarningOutlined style={{ color: colorError }} /> Leaving Course ?
<Title level={4} style={{ color: colorError, margin: 0 }}>
<WarningOutlined style={{ marginRight: 8 }} />
Confirm Leaving Course
</Title>
}
open={isOpen}
onOk={onOk}
onOk={handleOkClick}
okText="Leave Course"
okButtonProps={{ danger: true }}
okButtonProps={{ danger: true, loading: confirmLoading }}
onCancel={onCancel}
cancelText="Continue studying"
>
<>
<Divider />
{messages.map((text, i) => (
<Paragraph key={i}>{text}</Paragraph>
))}

<Divider />
{messagesRu.map((text, i) => (
<Paragraph key={i}>{text}</Paragraph>
))}
<Form form={form} layout="vertical" name="survey_form">
<Form.Item
name="reasonForLeaving"
label="Why are you leaving the course?"
rules={[{ required: true, message: 'Please select at least one reason.' }]}
>
<Checkbox.Group>
<Space direction="vertical">
{reasonsOptions.map(option => (
<Checkbox key={option.value} value={option.value}>
{option.labelEn}
<br />
{option.labelRu}
</Checkbox>
))}
</Space>
</Checkbox.Group>
</Form.Item>

<Form.Item name="otherComments" label="Any other comments or suggestions?">
<Input.TextArea rows={4} placeholder="Enter your feedback here..." />
</Form.Item>
</Form>
</>
</Modal>
);
Expand Down
84 changes: 80 additions & 4 deletions client/src/components/Profile/StudentStatsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,64 @@ type State = {
isStudentStatsModalVisible: boolean;
isExpelConfirmationModalVisible: boolean;
courseId?: number;
isLoading: boolean;
};

const coursesService = new CoursesApi();

const reasonsOptions = [
{
value: 'too_difficult',
labelEn: 'Course was too difficult',
labelRu: 'Курс был слишком сложным',
},
{
value: 'not_useful',
labelEn: 'Course was not useful',
labelRu: 'Курс был бесполезным',
},
{
value: 'lack_of_time',
labelEn: 'Lack of time',
labelRu: 'Нехватка времени',
},
{
value: 'other',
labelEn: 'Other',
labelRu: 'Другое',
},
{
value: 'no_interest',
labelEn: 'Lost interest in the subject',
labelRu: 'Потерял интерес к предмету',
},
{
value: 'poor_quality',
labelEn: 'Course quality was poor',
labelRu: 'Низкое качество курса',
},
{
value: 'found_alternative',
labelEn: 'Found an alternative course/opportunity',
labelRu: 'Нашел альтернативный курс/возможность',
},
{
value: 'personal_reasons',
labelEn: 'Personal reasons',
labelRu: 'Личные причины',
},
{
value: 'got_job',
labelEn: 'Got a job',
labelRu: 'Устроился на работу',
},
{
value: 'got_internship',
labelEn: 'Got an internship',
labelRu: 'Устроился на стажировку',
},
];

class StudentStatsCard extends React.Component<Props, State> {
state = {
courseIndex: 0,
Expand All @@ -41,11 +95,13 @@ class StudentStatsCard extends React.Component<Props, State> {
isStudentStatsModalVisible: false,
isExpelConfirmationModalVisible: false,
courseId: undefined,
isLoading: false,
};

shouldComponentUpdate = (_nextProps: Props, nextState: State) =>
!isEqual(nextState.isStudentStatsModalVisible, this.state.isStudentStatsModalVisible) ||
!isEqual(nextState.isExpelConfirmationModalVisible, this.state.isExpelConfirmationModalVisible) ||
!isEqual(nextState.isLoading, this.state.isLoading) ||
!isEqual(nextState.coursesProgress, this.state.coursesProgress);

private showStudentStatsModal = (courseIndex: number) => {
Expand All @@ -70,10 +126,28 @@ class StudentStatsCard extends React.Component<Props, State> {
});
};

private selfExpelStudent = async (courseId?: number) => {
private selfExpelStudent = async (courseId: number | undefined, surveyData: any) => {
if (!courseId) return;
await coursesService.leaveCourse(courseId);
window.location.reload();

this.setState({ isLoading: true });

try {
const response = await fetch(`/api/course/${courseId}/leave-survey`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(surveyData),
});

if (!response.ok) {
throw new Error('Failed to submit survey');
}
window.location.reload();
} catch (error) {
console.error('Failed to submit survey or leave course:', error);
this.setState({ isLoading: false });
}
};

private rejoinAsStudent = async (courseId: number) => {
Expand Down Expand Up @@ -107,8 +181,10 @@ class StudentStatsCard extends React.Component<Props, State> {
) : null}
<StudentLeaveCourse
isOpen={this.state.isExpelConfirmationModalVisible}
onOk={this.selfExpelStudent.bind(this, this.state.courseId)}
onOk={surveyData => this.selfExpelStudent(this.state.courseId, surveyData)}
onCancel={this.hideExpelConfirmationModal}
confirmLoading={this.state.isLoading}
reasonsOptions={reasonsOptions}
/>
<CommonCard
title="Student Statistics"
Expand Down
6 changes: 6 additions & 0 deletions client/src/components/Sider/data/menuItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,12 @@ const courseManagementMenuItems: CourseManagementMenuItemsData[] = [
getUrl: (course: Course) => `/course/admin/interviews?course=${course.alias}`,
courseAccess: isCourseManager,
},
{
name: 'Reports',
key: 'reports',
getUrl: (course: Course) => `/course/admin/reports?course=${course.alias}`,
courseAccess: some(isAdmin, isCourseManager, isCourseSupervisor),
},
{
name: 'Mentor Tasks Review',
key: 'mentorTasksReview',
Expand Down
Loading
Loading