From 306230174e537f0684635f980983e6d296bf6a5b Mon Sep 17 00:00:00 2001 From: swve Date: Wed, 16 Jul 2025 18:47:04 +0200 Subject: [PATCH] feat: working UI with the database --- .../components/Dashboard/Misc/SaveState.tsx | 21 + .../EditCourseCertification.tsx | 480 ++++++++++++------ .../Course/EditCourseGeneral/CustomSelect.tsx | 27 +- apps/web/services/courses/certifications.ts | 62 +++ 4 files changed, 417 insertions(+), 173 deletions(-) create mode 100644 apps/web/services/courses/certifications.ts diff --git a/apps/web/components/Dashboard/Misc/SaveState.tsx b/apps/web/components/Dashboard/Misc/SaveState.tsx index 672fd36c..a362d9e3 100644 --- a/apps/web/components/Dashboard/Misc/SaveState.tsx +++ b/apps/web/components/Dashboard/Misc/SaveState.tsx @@ -11,6 +11,7 @@ import { useRouter } from 'next/navigation' import React, { useEffect, useState } from 'react' import { mutate } from 'swr' import { updateCourse } from '@services/courses/courses' +import { updateCertification } from '@services/courses/certifications' import { useLHSession } from '@components/Contexts/LHSessionContext' function SaveState(props: { orgslug: string }) { @@ -32,6 +33,8 @@ function SaveState(props: { orgslug: string }) { // Course metadata await changeMetadataBackend() mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta?with_unpublished_activities=${withUnpublishedActivities}`) + // Certification data (if present) + await saveCertificationData() await revalidateTags(['courses'], props.orgslug) dispatchCourse({ type: 'setIsSaved' }) } finally { @@ -66,6 +69,24 @@ function SaveState(props: { orgslug: string }) { dispatchCourse({ type: 'setIsSaved' }) } + // Certification data + const saveCertificationData = async () => { + if (course.courseStructure._certificationData) { + const certData = course.courseStructure._certificationData; + try { + await updateCertification( + certData.certification_uuid, + certData.config, + session.data?.tokens?.access_token + ); + console.log('Certification data saved successfully'); + } catch (error) { + console.error('Failed to save certification data:', error); + // Don't throw error to prevent breaking the main save flow + } + } + } + const handleCourseOrder = (course_structure: any) => { const chapters = course_structure.chapters const chapter_order_by_ids = chapters.map((chapter: any) => { diff --git a/apps/web/components/Dashboard/Pages/Course/EditCourseCertification/EditCourseCertification.tsx b/apps/web/components/Dashboard/Pages/Course/EditCourseCertification/EditCourseCertification.tsx index 4e1b4722..48107b92 100644 --- a/apps/web/components/Dashboard/Pages/Course/EditCourseCertification/EditCourseCertification.tsx +++ b/apps/web/components/Dashboard/Pages/Course/EditCourseCertification/EditCourseCertification.tsx @@ -1,15 +1,21 @@ -import FormLayout, { +import { FormField, FormLabelAndMessage, Input, Textarea, } from '@components/Objects/StyledElements/Form/Form'; import { useFormik } from 'formik'; -import { AlertTriangle, Award, CheckCircle, FileText, Settings } from 'lucide-react'; +import { AlertTriangle, Award, FileText, Settings } from 'lucide-react'; import CertificatePreview from './CertificatePreview'; import * as Form from '@radix-ui/react-form'; import React, { useEffect, useState } from 'react'; import { useCourse, useCourseDispatch } from '@components/Contexts/CourseContext'; +import { useLHSession } from '@components/Contexts/LHSessionContext'; +import { + createCertification, + updateCertification, + deleteCertification +} from '@services/courses/certifications'; import { CustomSelect, CustomSelectContent, @@ -17,6 +23,9 @@ import { CustomSelectTrigger, CustomSelectValue, } from "../EditCourseGeneral/CustomSelect"; +import useSWR, { mutate } from 'swr'; +import { getAPIUrl } from '@services/config/config'; +import toast from 'react-hot-toast'; type EditCourseCertificationProps = { orgslug: string @@ -43,9 +52,56 @@ const validate = (values: any) => { function EditCourseCertification(props: EditCourseCertificationProps) { const [error, setError] = useState(''); + const [isCreating, setIsCreating] = useState(false); const course = useCourse(); const dispatchCourse = useCourseDispatch() as any; const { isLoading, courseStructure } = course as any; + const session = useLHSession() as any; + const access_token = session?.data?.tokens?.access_token; + + // Fetch existing certifications + const { data: certifications, error: certificationsError, mutate: mutateCertifications } = useSWR( + courseStructure?.course_uuid && access_token ? + `certifications/course/${courseStructure.course_uuid}` : null, + async () => { + if (!courseStructure?.course_uuid || !access_token) return null; + const result = await fetch( + `${getAPIUrl()}certifications/course/${courseStructure.course_uuid}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${access_token}`, + }, + credentials: 'include', + } + ); + const response = await result.json(); + + + + if (result.status === 200) { + return { + success: true, + data: response, + status: result.status, + HTTPmessage: result.statusText, + }; + } else { + return { + success: false, + data: response, + status: result.status, + HTTPmessage: result.statusText, + }; + } + } + ); + + const existingCertification = certifications?.data?.[0]; // Assuming one certification per course + const hasExistingCertification = !!existingCertification; + + // Create initial values object const getInitialValues = () => { @@ -64,13 +120,16 @@ function EditCourseCertification(props: EditCourseCertificationProps) { return ''; }; + // Use existing certification data if available, otherwise fall back to course data + const config = existingCertification?.config || {}; + return { - enable_certification: courseStructure?.enable_certification || false, - certification_name: courseStructure?.certification_name || courseStructure?.name || '', - certification_description: courseStructure?.certification_description || courseStructure?.description || '', - certification_type: courseStructure?.certification_type || 'completion', - certificate_pattern: courseStructure?.certificate_pattern || 'professional', - certificate_instructor: courseStructure?.certificate_instructor || getInstructorName(), + enable_certification: hasExistingCertification, + certification_name: config.certification_name || courseStructure?.name || '', + certification_description: config.certification_description || courseStructure?.description || '', + certification_type: config.certification_type || 'completion', + certificate_pattern: config.certificate_pattern || 'professional', + certificate_instructor: config.certificate_instructor || getInstructorName(), }; }; @@ -78,26 +137,85 @@ function EditCourseCertification(props: EditCourseCertificationProps) { initialValues: getInitialValues(), validate, onSubmit: async values => { - try { - // Add your submission logic here - dispatchCourse({ type: 'setIsSaved' }); - } catch (e) { - setError('Failed to save certification settings.'); - } + // This is no longer used - saving is handled by the main Save button }, enableReinitialize: true, }) as any; - // Reset form when courseStructure changes + // Handle enabling/disabling certification + const handleCertificationToggle = async (enabled: boolean) => { + if (enabled && !hasExistingCertification) { + // Create new certification + setIsCreating(true); + try { + const config = { + certification_name: formik.values.certification_name || courseStructure?.name || '', + certification_description: formik.values.certification_description || courseStructure?.description || '', + certification_type: formik.values.certification_type || 'completion', + certificate_pattern: formik.values.certificate_pattern || 'professional', + certificate_instructor: formik.values.certificate_instructor || '', + }; + + const result = await createCertification( + courseStructure.id, + config, + access_token + ); + + + + // createCertification uses errorHandling which returns JSON directly on success + if (result) { + toast.success('Certification created successfully'); + mutateCertifications(); + formik.setFieldValue('enable_certification', true); + } else { + throw new Error('Failed to create certification'); + } + } catch (e) { + setError('Failed to create certification.'); + toast.error('Failed to create certification'); + formik.setFieldValue('enable_certification', false); + } finally { + setIsCreating(false); + } + } else if (!enabled && hasExistingCertification) { + // Delete existing certification + try { + const result = await deleteCertification( + existingCertification.certification_uuid, + access_token + ); + + // deleteCertification uses errorHandling which returns JSON directly on success + if (result) { + toast.success('Certification removed successfully'); + mutateCertifications(); + formik.setFieldValue('enable_certification', false); + } else { + throw new Error('Failed to delete certification'); + } + } catch (e) { + setError('Failed to remove certification.'); + toast.error('Failed to remove certification'); + formik.setFieldValue('enable_certification', true); + } + } else { + formik.setFieldValue('enable_certification', enabled); + } + }; + + // Reset form when certifications data changes useEffect(() => { - if (courseStructure && !isLoading) { + if (certifications && !isLoading) { const newValues = getInitialValues(); formik.resetForm({ values: newValues }); } - }, [courseStructure, isLoading]); + }, [certifications, isLoading]); + // Handle form changes - update course context with certification data useEffect(() => { - if (!isLoading) { + if (!isLoading && hasExistingCertification) { const formikValues = formik.values as any; const initialValues = formik.initialValues as any; const valuesChanged = Object.keys(formikValues).some( @@ -106,19 +224,35 @@ function EditCourseCertification(props: EditCourseCertificationProps) { if (valuesChanged) { dispatchCourse({ type: 'setIsNotSaved' }); + + // Store certification data in course context so it gets saved with the main save button const updatedCourse = { ...courseStructure, - ...formikValues, + // Store certification data for the main save functionality + _certificationData: { + certification_uuid: existingCertification.certification_uuid, + config: { + certification_name: formikValues.certification_name, + certification_description: formikValues.certification_description, + certification_type: formikValues.certification_type, + certificate_pattern: formikValues.certificate_pattern, + certificate_instructor: formikValues.certificate_instructor, + } + } }; dispatchCourse({ type: 'setCourseStructure', payload: updatedCourse }); } } - }, [formik.values, isLoading]); + }, [formik.values, isLoading, hasExistingCertification, existingCertification]); - if (isLoading || !courseStructure) { + if (isLoading || !courseStructure || (courseStructure.course_uuid && access_token && certifications === undefined)) { return
Loading...
; } + if (certificationsError) { + return
Error loading certifications
; + } + return (
{courseStructure && ( @@ -139,10 +273,16 @@ function EditCourseCertification(props: EditCourseCertificationProps) { type="checkbox" className="sr-only peer" checked={formik.values.enable_certification} - onChange={(e) => formik.setFieldValue('enable_certification', e.target.checked)} + onChange={(e) => handleCertificationToggle(e.target.checked)} + disabled={isCreating} />
+ {isCreating && ( +
+ +
+ )}
@@ -153,162 +293,160 @@ function EditCourseCertification(props: EditCourseCertificationProps) { )} - {/* Certification Configuration */} - {formik.values.enable_certification && ( + {/* Certification Configuration - Only show if enabled and has existing certification */} + {formik.values.enable_certification && hasExistingCertification && (
{/* Form Section */}
- -
- {/* Basic Information Section */} -
-

- - Basic Information -

-

- Configure the basic details of your certification -

-
+ + {/* Basic Information Section */} +
+

+ + Basic Information +

+

+ Configure the basic details of your certification +

+
-
- {/* Certification Name */} - - - - - - - - {/* Certification Type */} - - - - { - if (!value) return; - formik.setFieldValue('certification_type', value); - }} - > - - - {formik.values.certification_type === 'completion' ? 'Course Completion' : - formik.values.certification_type === 'achievement' ? 'Achievement Based' : - formik.values.certification_type === 'assessment' ? 'Assessment Based' : - formik.values.certification_type === 'participation' ? 'Participation' : - formik.values.certification_type === 'mastery' ? 'Skill Mastery' : - formik.values.certification_type === 'professional' ? 'Professional Development' : - formik.values.certification_type === 'continuing' ? 'Continuing Education' : - formik.values.certification_type === 'workshop' ? 'Workshop Attendance' : - formik.values.certification_type === 'specialization' ? 'Specialization' : 'Course Completion'} - - - - Course Completion - Achievement Based - Assessment Based - Participation - Skill Mastery - Professional Development - Continuing Education - Workshop Attendance - Specialization - - - - -
- - {/* Certification Description */} - +
+ {/* Certification Name */} + -