feat: add certificates, groups, support system, and moderation

Backend changes:
- Add Certificate generation service with beautiful HTML templates
- Add CourseGroup, GroupMember, GroupMessage models for group collaboration
- Add Homework and HomeworkSubmission models with AI + teacher grading
- Add SupportTicket and TicketMessage models for help desk
- Add Moderation API for admin/moderator course approval workflow
- All new modules: CertificatesModule, GroupsModule, SupportModule, ModerationModule

Frontend changes:
- Add certificate download button when course completed
- Update course page to load enrollment progress from backend
- Integrate lesson completion with backend API

Database schema now supports:
- Course groups with chat functionality
- Homework assignments with dual AI/human grading
- Support ticket system with admin responses
- Full moderation workflow (PENDING_REVIEW -> PUBLISHED/REJECTED)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
root
2026-02-06 10:50:04 +00:00
parent 2ed65f5678
commit 5ddb3db1ac
16 changed files with 605 additions and 1 deletions

View File

@ -66,6 +66,7 @@ export default function CoursePage() {
const [enrollmentProgress, setEnrollmentProgress] = useState<any>(null);
const [showQuiz, setShowQuiz] = useState(false);
const [quizQuestions, setQuizQuestions] = useState<any[]>([]);
const [generatingCertificate, setGeneratingCertificate] = useState(false);
// Flat list of all lessons
const flatLessons = useMemo(() => {
@ -188,6 +189,19 @@ export default function CoursePage() {
}
};
const handleGetCertificate = async () => {
if (!id || generatingCertificate) return;
setGeneratingCertificate(true);
try {
const { certificateUrl } = await api.getCertificate(id);
window.open(certificateUrl, '_blank');
} catch {
// silent
} finally {
setGeneratingCertificate(false);
}
};
const goToNextLesson = () => {
if (currentLessonIndex < flatLessons.length - 1) {
markComplete();
@ -506,8 +520,13 @@ export default function CoursePage() {
Следующий урок
<ChevronRight className="ml-1.5 h-4 w-4" />
</Button>
) : completedCount >= totalLessons ? (
<Button size="sm" onClick={handleGetCertificate} disabled={generatingCertificate}>
<GraduationCap className="mr-1.5 h-4 w-4" />
{generatingCertificate ? 'Генерация...' : 'Получить сертификат'}
</Button>
) : (
<Button size="sm" variant="outline" disabled={completedCount < totalLessons}>
<Button size="sm" variant="outline" disabled>
<GraduationCap className="mr-1.5 h-4 w-4" />
Завершить курс
</Button>

View File

@ -294,6 +294,11 @@ class ApiClient {
return this.request<{ data: any[]; meta: any }>(`/enrollment/${courseId}/reviews${params}`);
}
// Certificates
async getCertificate(courseId: string) {
return this.request<{ certificateUrl: string; html: string }>(`/certificates/${courseId}`);
}
// Search
async searchCourses(query: string, filters?: { category?: string; difficulty?: string }) {
const searchParams = new URLSearchParams({ q: query });